diff --git a/@config/shared-theme-zh.ts b/@config/shared-theme-zh.ts index c4f0f19974..f4a00c28c7 100644 --- a/@config/shared-theme-zh.ts +++ b/@config/shared-theme-zh.ts @@ -1,6 +1,7 @@ import { defineConfig, type DefaultTheme } from 'vitepress' import { github, branch } from './config' + export default function(version: string) { return defineConfig({ lang: 'zh-Hans', diff --git a/README.md b/README.md index 738b0cbb7e..d1aecc873f 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,17 @@ npm install ## Generate Sidebar -node ./scripts/create-sidebar.js versions/3.8/zh/SUMMARY.md +node ./scripts/create-sidebar.js versions/4.0/zh/SUMMARY.md -node ./scripts/create-sidebar.js versions/3.8/en/SUMMARY.md +node ./scripts/create-sidebar.js versions/4.0/en/SUMMARY.md ## Run In Dev Mode -npx vitepress dev versions/3.8 +npx vitepress dev versions/4.0 ## Build -node ./scripts/publish.js --version=versions/3.8 +node ./scripts/publish.js --version=versions/4.0 ## Build All @@ -29,6 +29,6 @@ node ./scripts/publish.js --version=versions/all ## Preview -npx vitepress preview versions/3.8 +npx vitepress preview versions/4.0 -> Change the '3.8' above to operate another version. \ No newline at end of file +> Change the '4.0' above to operate another version. \ No newline at end of file diff --git a/package.json b/package.json index 61aeaedebd..17f0a3e489 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "cocos-creator-docs", "type": "module", "scripts": { - "docs:dev": "cross-env __APIDOC__=https://docs.cocos.com/creator/3.8/api vitepress dev versions/3.8 --port=5001 --host", - "docs:build": "cross-env __APIDOC__=https://docs.cocos.com/creator/3.8/api vitepress build versions/3.8", - "docs:preview": "vitepress preview versions/3.8" + "docs:dev": "cross-env __APIDOC__=https://docs.cocos.com/creator/4.0/api vitepress dev versions/4.0 --port=5001 --host", + "docs:build": "cross-env __APIDOC__=https://docs.cocos.com/creator/4.0/api vitepress build versions/4.0", + "docs:preview": "vitepress preview versions/4.0" }, "keywords": [], "author": "cocos", diff --git a/versions.json b/versions.json index b23f82bad3..c698c6e269 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,7 @@ [ + { + "version": "4.0" + }, { "version": "3.8" }, diff --git a/versions/4.0/.vitepress/config/index.ts b/versions/4.0/.vitepress/config/index.ts new file mode 100644 index 0000000000..fcab5171f3 --- /dev/null +++ b/versions/4.0/.vitepress/config/index.ts @@ -0,0 +1,58 @@ +import { defineConfig } from 'vitepress' +import { shared } from '../../../../@config/shared' + +// 每个语言的配置 +import configZhFn from '../../../../@config/shared-theme-zh' +import configEnFn from '../../../../@config/shared-theme-en' +import { nav } from '../../../../@config/shared'; + +// 侧边栏数据 +import menusEN from '../../en/summary.json' with {type: 'json'}; +import menusZH from '../../zh/summary.json' with {type: 'json'}; + +import { version } from './version.ts'; + +// 英文配置 +const configEn = configEnFn(version); +const en = defineConfig({ + lang: configEn.lang, + description: configEn.description, + + themeConfig: { + ...configEn.themeConfig, + + nav: nav(version, 'en'), + + sidebar: { + '/en/': { base: '/en/', items: menusEN }, + } + } +}) + +// 中文配置 +const configZh = configZhFn(version); +const zh = defineConfig({ + lang: configZh.lang, + description: configZh.description, + + themeConfig: { + ...configZh.themeConfig, + + nav: nav(version, 'zh'), + + sidebar: { + '/zh/': { base: '/zh/', items: menusZH }, + } + } +}) + +export default defineConfig({ + ...shared, + base: `/creator/${version}/manual/` , // `/${version}/` + locales: { + // 我们的文档都在 /en/ /zh/ 等文件夹里面,没有在根目录的文档 所以不用配 root + // root: { label: 'English', ...en }, + en: { label: 'English', ...en }, + zh: { label: '简体中文', ...zh } + } +}) diff --git a/versions/4.0/.vitepress/config/version.ts b/versions/4.0/.vitepress/config/version.ts new file mode 100644 index 0000000000..54aea805fc --- /dev/null +++ b/versions/4.0/.vitepress/config/version.ts @@ -0,0 +1 @@ +export const version = '4.0'; \ No newline at end of file diff --git a/versions/4.0/.vitepress/theme/index.ts b/versions/4.0/.vitepress/theme/index.ts new file mode 100644 index 0000000000..cb25ea07fb --- /dev/null +++ b/versions/4.0/.vitepress/theme/index.ts @@ -0,0 +1,6 @@ + +import theme from '../../../../@config/theme/index.ts' +import { version } from '../config/version.ts' + +export default theme(version) + diff --git a/versions/4.0/en/2d-object/2d-render/add-render-component.png b/versions/4.0/en/2d-object/2d-render/add-render-component.png new file mode 100644 index 0000000000..00384113be Binary files /dev/null and b/versions/4.0/en/2d-object/2d-render/add-render-component.png differ diff --git a/versions/4.0/en/2d-object/2d-render/create-2d.png b/versions/4.0/en/2d-object/2d-render/create-2d.png new file mode 100644 index 0000000000..c0400a8c67 Binary files /dev/null and b/versions/4.0/en/2d-object/2d-render/create-2d.png differ diff --git a/versions/4.0/en/2d-object/2d-render/index.md b/versions/4.0/en/2d-object/2d-render/index.md new file mode 100644 index 0000000000..e1ebc76e74 --- /dev/null +++ b/versions/4.0/en/2d-object/2d-render/index.md @@ -0,0 +1,45 @@ +# 2D Render Description + +All rendering objects in the engine that do not own a model are 2D rendering objects. Unlike 3D objects, 2D objects do not have model information, and their vertex information is held by the Rect information of the UITransform component and created by the engine, and they have no thickness themselves. Due to the design requirements of the engine, the 2D render object needs to be a child of the RenderRoot node (the node with the RenderRoot2D component) in order to complete the data collection operation. + +The rendering requirements for the 2D render object are twofold: +1. It needs to have a UITransform component +2. It needs to be a child of a RenderRoot node + +## 2D Rendering Object Visibility Description + +Since there is no difference between 2D rendering objects and 3D renderable nodes in terms of camera visibility, users need to control the layer property of the node and set the Visibility of the camera to match the group rendering. If there are multiple cameras in the scene, wrong layer settings may cause the node to be rendered repeatedly or not at all. + +## 2D Renderable Components + +Components that have rendering capabilities in their own right are called 2D Renderable components, including: + +- [Sprite Component Reference](../../ui-system/components/editor/sprite.md) +- [Label Component Reference](../../ui-system/components/editor/label.md) +- [Mask Component Reference](../../ui-system/components/editor/mask.md) +- [Graphics Component Reference](../../ui-system/components/editor/graphics.md) +- [RichText Component Reference](../../ui-system/components/editor/richtext.md) +- [UIStaticBatch Component Reference](../../ui-system/components/editor/ui-static.md) +- [TiledMap Component Reference](../../editor/components/tiledmap.md) +- [TiledTile Component Reference](../../editor/components/tiledtile.md) +- [Spine Skeletal Component Reference](../../editor/components/spine.md) +- [DragonBones ArmatureDisplay Component Reference](../../editor/components/dragonbones.md) +- [MotionStreak Component Reference](../../editor/components/motion-streak.md) + +## How to add 2D Renderable Components + +Some 2D renderable components are built into the editor. After creating a RenderRoot node, create a node with 2D renderable components under this node: + +![create-2d](./create-2d.png) + +2D renderable components can also be added by adding components to the node, the nodes under the 2D menu in the components menu are all 2D renderable components: + +![add-render-component](./add-render-component.png) + +> **Note**: only one renderable component can be added to each node, and repeated additions will result in error reporting. + +## 2D Renderable Component Rules Introduction + +- [Rendering Order Rules](../../ui-system/components/engine/priority.md) +- [2D Renderable Component Batching Rules](../../ui-system/components/engine/ui-batch.md) +- [Custom Materials for 2D Rendering Objects](../../ui-system/components/engine/ui-material.md) diff --git a/versions/4.0/en/2d-object/index.md b/versions/4.0/en/2d-object/index.md new file mode 100644 index 0000000000..338ff9ae25 --- /dev/null +++ b/versions/4.0/en/2d-object/index.md @@ -0,0 +1,20 @@ +# 2D Objects Overview + +Unlike 3D model objects, 2D rendering objects are referred to as the rendering of images that do not involve models. 2D rendering objects are handled differently from 3D models in terms of the underlying data submission, and follow their own rules with some specific adjustments for better efficiency and experience. + +## 2D Object Rendering Structure Description + +The RenderRoot node (the node with the RenderRoot2D component) is the entry point for 2D object data collection, and all 2D rendering objects must be rendered under the RenderRoot node. Since the Canvas component inherits from the RenderRoot2D component, the Canvas component can also be the entry point for data collection. 2D rendering nodes must have a UITransform component as a requirement for rendering vertex data, click or alignment policies, etc. + +2D rendering can also support rendering of models, the only condition is that nodes with model components (e.g. `MeshRenderer`/`SkinnedMeshRenderer`) must have **UI/UIMeshRenderer** components added to them in order to render on the same pipeline as the UI. + +The 2D rendering flow is as follows: + +![render](render.png) + +## 2D object classification + +2D objects can be broadly classified into two categories, **2D rendering objects** and **User-interface (UI)**. The difference is that 2D rendering objects are generally only responsible for rendering 2D objects, while UI is more about user interaction. For more details on the differences, users can refer to the specific descriptions: + +- [2D Renderable Component Description](2d-render/index.md) +- [UI System](ui-system/index.md) diff --git a/versions/4.0/en/2d-object/render.png b/versions/4.0/en/2d-object/render.png new file mode 100644 index 0000000000..87660bc658 Binary files /dev/null and b/versions/4.0/en/2d-object/render.png differ diff --git a/versions/4.0/en/2d-object/ui-system/add-ui-component.png b/versions/4.0/en/2d-object/ui-system/add-ui-component.png new file mode 100644 index 0000000000..c329cb01c9 Binary files /dev/null and b/versions/4.0/en/2d-object/ui-system/add-ui-component.png differ diff --git a/versions/4.0/en/2d-object/ui-system/create-ui.png b/versions/4.0/en/2d-object/ui-system/create-ui.png new file mode 100644 index 0000000000..c7042f971d Binary files /dev/null and b/versions/4.0/en/2d-object/ui-system/create-ui.png differ diff --git a/versions/4.0/en/2d-object/ui-system/index.md b/versions/4.0/en/2d-object/ui-system/index.md new file mode 100644 index 0000000000..9ef6cd1886 --- /dev/null +++ b/versions/4.0/en/2d-object/ui-system/index.md @@ -0,0 +1,49 @@ +# UI System + +This section introduces the powerful and flexible UI (User Interface) system in Cocos Creator. By assembling different UI components to produce UI interfaces that can be adapted to multiple screen resolutions, dynamically generate and update display content from data, and support multiple layout styles. + +## Getting Started with UI + +The difference between defining UI and 2D rendering objects in the engine mainly lies in adaptation and interaction. All UI needs to be under a Canvas node to make adaptation behavior, and the Canvas component itself inherits from `RenderRoot2D` component, it can also be used as an entry point for data collection. + +The UI is a necessary interactive part of game development. Generally, buttons, text, backgrounds, etc. on the game are made through the UI. When starting to create a UI, first it is necessary to determine the size of the display area (design resolution) of the current design content, which can be set in the **Project -> Project Settings -> Project Data** panel in the menu bar. + +![resolution-config](resolution_config.png) + +Once the design resolution is set, start creating UI elements, all of which are contained under the Canvas node. The Canvas node can be created by clicking the **+** button in the **Hierarchy** panel on the top left and selecting **UI Component -> Canvas**. The Canvas node has a [Canvas](../../ui-system/components/editor/canvas.md) component, which can be associated with a camera. + +> **Notes**: +> +> 1. Multiple Canvas nodes can exist in a scene, but a Canvas node should not be nested under another Canvas node or its children. +> 2. Canvas components are not one-to-one with camera, their previous rendering depends on the layer of the node and the Visibility of the camera, so you should pay extra attention to layer management to get the desired rendering effect when you have multiple Canvas. + +Next, create UI nodes under the Canvas node. The editor comes with the following UI nodes: + +![create-ui](./create-ui.png) + +UI components can be viewed by selecting the node and clicking **Add Component** in the **Inspector** panel. + +![add-ui-component](./add-ui-component.png) + +The order in which UI components are rendered is a depth ordering scheme, which means that the ordering of the child nodes under the Canvas node already determines the entire [rendering order](../../ui-system/components/engine/priority.md). + +In general game development, the necessary UI elements are not only basic 2D renderable components such as Sprite, Label (text), Mask, but also Layout, Widget (alignment), etc., which are used to quickly build the interface. Sprite and Label are used to render images and text, Mask is mainly used to limit the display content, more commonly used in chat boxes and backpacks, etc. Layout is generally used for single arrangement of buttons, neat arrangement of props in backpacks, etc.
+The last important feature is the Widget, which is mainly used for display alignment. When finishing designing the UI and publish it to different platforms, the actual device resolution of the platform is bound to be different from our design resolution, therefore some trade-offs need to be made in order to adapt it. It is necessary to add a widget component to it, and always ensure that it is aligned to the top left of our design resolution. Please review the [Alignment Strategy](../../ui-system/components/engine/widget-align.md) and [Alignment](../../ui-system/components/editor/widget.md) documentation. + +Once the interface is created, some people may notice that the iPhone 7 displays differently than the iPhone X. This is actually the same problem with the device resolution we mentioned above. When you design in design resolution and finally publish in device resolution, there is a pixel deviation because the resolution of different models of mobile devices may not be the same, so there is another conversion process that needs to be done that is screen adaptation.
+Notice in the **Projects -> Project Settings -> Project Data** page in the menu bar, there are two more options **Fit Width / Fit Height**, which can be easily adapted to different devices by following the screen adaptation rules and combining with the Widget component. The specific adaptation rules can be found in the [Multi-Resolution Adaptation Scheme](../../ui-system/components/engine/multi-resolution.md) documentation. + +## UI components + +UI components mostly do not have rendering capabilities themselves, but hold 2D renderable components for rendering, which themselves have more ability to quickly form user-interactive interfaces, and take on functions such as event response, typography adaptation, etc. + +For specific descriptions of each UI component, please refer to the [UI Components](../../ui-system/components/editor/base-component.md) documentation. + +## UI Practice Guide + +- [Multi-Resolution Adaptation Scheme](../../ui-system/components/engine/multi-resolution.md) +- [Alignment Strategy](../../ui-system/components/engine/widget-align.md) +- [Label Layout](../../ui-system/components/engine/label-layout.md) +- [Auto Layout Container](../../ui-system/components/engine/auto-layout.md) +- [Create a List of Dynamically Generated Content](../../ui-system/components/engine/list-with-data.md) +- [Use a Sliced Sprite to make a UI image](../../ui-system/components/engine/sliced-sprite.md) diff --git a/versions/4.0/en/2d-object/ui-system/resolution_config.png b/versions/4.0/en/2d-object/ui-system/resolution_config.png new file mode 100644 index 0000000000..6ab88f3dfe Binary files /dev/null and b/versions/4.0/en/2d-object/ui-system/resolution_config.png differ diff --git a/versions/4.0/en/CONTRIBUTING.md b/versions/4.0/en/CONTRIBUTING.md new file mode 100644 index 0000000000..aee8654f65 --- /dev/null +++ b/versions/4.0/en/CONTRIBUTING.md @@ -0,0 +1,309 @@ +# English document writing standard + +The purpose of the format specification is to provide uniform writing guidelines and make the finished document have a better reading experience. + +## Use # for the title, separate the upper and lower text with a blank line + +Use # for the first-level title.
+Use ## for the second-level title.
+Use ### for the third-level title.
+and so on... + +Under normal circumstances, do not skip the use of headings, for example, the third-level headings cannot appear directly under the first-level heading. + +Example (correct): + +> \# Main Heading +> +> \## Sub-heading 1 +> +> \### Sub-heading 2 + +Example (NOT correct): + +> \# Main Heading +> +> \### Sub-heading 1 + +## Use correct initial capital letters for proper English nouns and component names + +Correct usage: + +> Log in with GitHub +> +> Sprite component + +Incorrect usage: + +> Login with github +> +> sprite component + +## Use spaces + +### A space needs to be added between the number and the unit + +Correct usage: + +> The bandwidth of my home is 1 Gbps, and the total hard disk is 10 TB. + +Incorrect usage: + +> The bandwidth of my home is 1Gbps, and the total hard disk is 10TB. + +**Exception: There is no need to add a space between degree/percentage and number** + +Correct usage: + +> Today is a high temperature of 233°. +> +> The new MacBook Pro has a 15% increase in CPU performance. + +Incorrect usage: + +> Today is a high temperature of 233 °. +> +> The new MacBook Pro has a 15% increase in CPU performance. + +### Add a jump link to a document in this repository + +URL link format: **[url document name]\(url document path)**. Use halfwidth English punctuation. + +> e.g.: [Monitor and launch events]\(../scripting/events.md). + +Correct usage: + +> For details, please refer to the [Monitor and Launch Events](../scripting/events.md) documentation. + +Incorrect usage: + +> For details, please refer to the [Monitor and Launch Events] (../scripting/events.md) documentation. + +### Add a URL to the API document + +URL link format: **[url document name]\(url document directory)**. Use half-width English punctuation, and no spaces between [] and () + +> e.g: [Mask API]\(\_\_APIDOC\_\_/en/class/Mask). Use **.html** for file name suffixes across documents + +## Use bold between adjacent text + +**Panel names, components or other important interface elements in the editor** are expressed in bold. + +> Format: open \*\*Inspector\*\* panel to view properties. + +Correct usage: + +> Open **Inspector** panel to view properties. +> +> Click the **Create** button to create a new node. +> +> Drag any picture resource to the **Sprite Frame** property. + +Note that the property name in the editor should be written in the format displayed in the Inspector panel. + +## Use backtick between the adjacent text + +**Script attributes and method names are written according to the format displayed in the API**, expressed by backtick. + +> Format: Set the scale of the node through \`this.node.scale\` + +Correct usage: + +> Set the scale of the node through `this.node.scale`. + +**File name and file path**, use backtick to indicate. + +Format: \`/mypath/myfile.ts\` + +If it is a full path, you need to add / before it, if it is not a full path, you don’t need it. + +Correct usage: + +> The subpackage directory is under the `build/quickgame/dist` directory. + +## Use blank lines + +### The code paragraph and the context need to be separated by a blank line + +Example: + +> Save Prefab, the code example is as follows:
+> +> \```js
+> Editor.Ipc.sendToPanel('scene','scene:apply-prefab', node.uuid);
+> \``` +> +> Continue text part. + +The effect is as follows: + +> Save Prefab, the code example is as follows: +> +> ```js +> Editor.Ipc.sendToPanel('scene','scene:apply-prefab', node.uuid); +> ``` +> +> Continue text part. + +### The picture and the upper and lower text need to be separated by a blank line + +Picture format: **![Picture description]\(relative path of the picture)**. Use halfwidth English punctuation, and no spaces between !, [], (). + +> e.g.: !\[background]\(quick-start/background.png) + +If the picture is added to the text, it needs to add a space between the adjacent text + +## Use blank lines or \ for line breaks + +If you use the Enter key to wrap, there will be no wrap effect on GitBook and a space will be added + +**Use blank line wrap effect**: + +> First line +> +> Second line + +**Use \ wrapping effect**: + +> First line
+> Second line + +**Use the enter key to wrap the line effect (not recommended)**: + +> First line +> Second line + +## Note writing format + +> Format: **> \*\*Note\*\*: do not mix Chinese and English symbols in Chinese and English documents.** + +When the note is more than two points, the writing format is as follows: + +> \> \*\*Notes\*\*:
+> \> 1. The first point.
+> \> 2. The second point.
+> \> 3. One last point.
+ +The effect is as follows: + +> **Notes**: +> 1. The first point. +> 2. The second point. +> 3. One last point. + +## Introduction + +### Used in the text-point introduction + +When the main text is introduced with-points, the main text in the points, including pictures, need to be indented with 2/4 spaces. Generally, indents with two spaces are used. e.g.: + +> \- Point introduction 1 +> +> (Two spaces) The body part that begins the first division. +> +> \- Introduction 2 +> +> (Two spaces) The body part that begins the second division. +> +> (Two spaces) !\[image]\(image link) + +The effect is as follows: + +> - Point introduction 1 +> +> Start the text part of the first division. +> +> - Introduction 2 +> +> Start the text part of the second point. +> +> !\[image]\(image link) + +### Introduction to the use of digital points in the text + +When the main text is introduced with **number** points, the main text in the points including pictures need to be indented with **4** spaces. e.g.: + +> 1\. Point introduction 1 +> +> (4 spaces) The part of the body that begins the first division. +> +> 2\. Point introduction 2 +> +> (4 spaces) The part of the body that begins the second division. +> +> (4 spaces) !\[image]\(image link) + +The effect is as follows: + +> 1. Sub-point introduction 1 +> +> Start the text part of the first division. +> +> 2. Sub-point introduction 2 +> +> Start the text part of the second point. +> +> !\[image]\(image link) + +## The table uniformly uses left alignment + +e.g.: + +> | Property | Function Description |
+> | \:---- | :------ |
+> | Property 1 | Description 1 |
+> | Property 2 | Description 2 | + +Appropriate spaces can be reserved before and after for easy editing. + +## Grammar suggestions + +- __Firstly__ -> __First__. +- At present, —> Currently, +- Related Reference Links —> Reference documentation +- do not use phrases like: "Now let me explain", "he should do this" +- text like this should be avoided: "If you have never written a program and don’t worry, we will provide all the required code in the tutorial, just copy and paste it to the correct location, and then find your programmer partner to solve this part of the work. Let's start creating the script that drives the main character's actions." +- text like this should be avoided: "So, " + +## About product name + +1. Cocos Creator +2. Cocos Creator 2.4.3 +3. v2.4.3 +4. v3.0 +5. The engine (where refer to the runtime) +6. The editor (where refer to the IDE) + +**Versions**: + +- 2.4.3 (3.0.0) +- 2.4.3 Preview (GA/RC/Alpha/Beta ...) +- 2.4.x (3.0.x) +- 2.4 (3.0) +- 2.x (3.x) + +**Never use**: + +1. CCC or ccc +2. Cocos (where refer to Cocos Creator) +3. IDE (where refer to Cocos Creator) + +## Technical designations + +- json -> JSON +- js or JS -> JavaScript +- ts or TS -> TypeScript + +## Footnote + +Example: + +Text prior to footnote reference.\[^1] + +\[^1]: Comment to include in footnote. + +The effect is as follows: + +Text prior to footnote reference.[^1] + +[^1]: Comment to include in footnote. diff --git a/versions/4.0/en/SUMMARY.md b/versions/4.0/en/SUMMARY.md new file mode 100644 index 0000000000..89b80e8c92 --- /dev/null +++ b/versions/4.0/en/SUMMARY.md @@ -0,0 +1,517 @@ +# Summary + +## User Manual 4.0 (LTS) + +- [Cocos Creator 4.0 LTS](index.md) +- [About Cocos Creator](getting-started/introduction/index.md) +- [Support](getting-started/support.md) + +## Understanding the Basics + +- [Getting Started](getting-started/index.md) + + - [Install and Launch](getting-started/install/index.md) + - [Dashboard](getting-started/dashboard/index.md) + - [Hello World!](getting-started/helloworld/index.md) + - [Project Structure](getting-started/project-structure/index.md) +- [Editor Interfaces](editor/index.md) + - [Scene](editor/scene/index.md) + - [Hierarchy](editor/hierarchy/index.md) + - [Assets](editor/assets/index.md) + - [Inspector](editor/inspector/index.md) + - [Console](editor/console/index.md) + - [Preferences](editor/preferences/index.md) + - [Project Settings](editor/project/index.md) + - [Main Menu](editor/mainMenu/index.md) + - [Tool Bar](editor/toolbar/index.md) + - [Editor Layout](editor/editor-layout/index.md) + - [Preview & Debugging](editor/preview/index.md) +- [Glossary](glossary/index.md) +- [Cocos Creator Quick Guide for Unity Developers](guide/unity/index.md) + +## Tutorials + +- [Quick Start: Making Your First 2D Game](getting-started/first-game-2d/index.md) + - [Handle Touch Events](getting-started/first-game-2d/touch.md) +- [Quick Start: Make Your First 3D Game](getting-started/first-game/index.md) + - [Advanced: Add Light, Shadow and Skeleton Animation](getting-started/first-game/advance.md) +- [Examples & Tutorials](cases-and-tutorials/index.md) + +## Basic Workflow + +- [Upgrade Guide](release-notes/index.md) + - [Cocos Creator 3.0 Upgrade Guide](release-notes/upgrade-guide-v3.0.md) + - [Cocos Creator 3.0 Material Upgrade Guide](material-system/effect-2.x-to-3.0.md) + - [Cocos Creator 3.1 Material Upgrade Guide](material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md) + - [Cocos Creator 3.5 Material Upgrade Guide](material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md) + - [Cocos Creator 3.5 Native Built Project Upgrade Guide](engine/template/native-upgrade-to-v3.5.md) + - [Cocos Creator 3.6 Native Built Project Upgrade Guide](engine/template/native-upgrade-to-v3.6.md) + - [Cocos Creator 3.6.0 Build Template and settings.json Upgrade Guide](release-notes/build-template-settings-upgrade-guide-v3.6.md) + - [Upgrade Guide: Effect from v3.5.x to v3.6.0](material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md) + - [Upgrade Guide: Particle from v3.5.x to v3.6.0](particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md) + +- [Scene Creation](concepts/scene/index.md) + - [Scene Assets](asset/scene.md) + - [Nodes and Components](concepts/scene/node-component.md) + - [Coordinate Systems and Transformations](concepts/scene/coord.md) + - [Node Hierarchy and Rendering Order](concepts/scene/node-tree.md) + - [Build a Scene Image Using the Scene Panel](concepts/scene/scene-editing.md) + - [Level of Details](editor/rendering/lod.md) + +- [Assets](asset/index.md) + - [Asset Workflow](asset/asset-workflow.md) + - [Images](asset/image.md) + - [Textures](asset/texture.md) + - [Sprite Frames](asset/sprite-frame.md) + - [Auto Trim for SpriteFrame](ui-system/components/engine/trim.md) + - [Texture Compression](asset/compress-texture.md) + - [Texture Cube](asset/texture-cube.md) + - [Atlas](asset/atlas.md) + - [Auto Atlas](asset/auto-atlas.md) + - [Label Atlas](asset/label-atlas.md) + - [Prefab](asset/prefab.md) + - [Fonts](asset/font.md) + - [Audio](asset/audio.md) + - [Material](asset/material.md) + - [FBX Smart Material Conversion](importer/materials/fbx-materials.md) + - [Model](asset/model/mesh.md) + - [Importing Models Exported from DCC Tools](asset/model/dcc-export-mesh.md) + - [Export FBX file from 3ds Max](asset/model/max-export-fbx.md) + - [Export FBX file from Maya](asset/model/maya-export-fbx.md) + - [glTF](asset/model/glTF.md) + - [Programmatically Create Meshes](asset/model/scripting-mesh.md) + - [Spine Skeletal Animation](asset/spine.md) + - [DragonBones Skeletal Animation](asset/dragonbones.md) + - [TiledMap](asset/tiledmap.md) + +- [Scripting Guide and Event System](scripting/index.md) + - [Programming Language Support](scripting/language-support.md) + - [Scripting Basics](scripting/script-basics.md) + - [Script Creation](scripting/setup.md) + - [Coding Environment Setup](scripting/coding-setup.md) + - [Operating Environment](scripting/basic.md) + - [Decorator](scripting/decorator.md) + - [Property Attributes](scripting/reference/attributes.md) + - [Life Cycle Callbacks](scripting/life-cycle-callbacks.md) + - [Development Notes](scripting/readonly.md) + - [Using Scripts](scripting/usage.md) + - [Access Nodes and Components](scripting/access-node-component.md) + - [Common Node and Component Interfaces](scripting/basic-node-api.md) + - [Create and Destroy Nodes](scripting/create-destroy.md) + - [Scheduler](scripting/scheduler.md) + - [Components and Component Execution Order](scripting/component.md) + - [Loading and Switching Scenes](scripting/scene-managing.md) + - [Obtaining and Loading Assets](scripting/load-assets.md) + - [tsconfig Configuration](scripting/tsconfig.md) + - [Advanced Scripting](scripting/reference-class.md) + - [Events](engine/event/index.md) + - [Listening to and Launching Events](engine/event/event-emit.md) + - [Input Event System](engine/event/event-input.md) + - [Node Event System](engine/event/event-node.md) + - [Screen Event System](engine/event/event-screen.md) + - [Event API](engine/event/event-api.md) + - [Modules](scripting/modules/index.md) + - [Engine Modules](scripting/modules/engine.md) + - [External Module Usage Case](scripting/modules/example.md) + - [Module Specification](scripting/modules/spec.md) + - [Import Maps](scripting/modules/import-map.md) + - [Plugin Scripts](scripting/external-scripts.md) + +- [Cross-platform Publishing](editor/publish/index.md) + - [General Build Options](editor/publish/build-options.md) + - [Fundamentals for Publishing to Native Platforms](editor/publish/publish-native-index.md) + - [General Native Build Options](editor/publish/native-options.md) + - [Setting up Native Development Environment](editor/publish/setup-native-development.md) + - [Debugging JavaScript on Native Platforms](editor/publish/debug-jsb.md) + - [Integrating Input SDK](editor/publish/gpg-input-sdk.md) + - [Publishing Android Apps](editor/publish/android/index.md) + - [Android Publishing Example](editor/publish/android/build-example-android.md) + - [Build Options - Android](editor/publish/android/build-options-android.md) + - [Upgrading Android Project to v3.8](release-notes/upgrade-3.8-android.md) + - [Publish to Google Play on PC](editor/publish/google-play-games/index.md) + - [Build and Run](editor/publish/google-play-games/build-and-run.md) + - [Integrating Input SDK](editor/publish/gpg-input-sdk.md) + - [Publishing iOS Apps](editor/publish/ios/index.md) + - [iOS Publishing Example](editor/publish/ios/build-example-ios.md) + - [Build Options - iOS](editor/publish/ios/build-options-ios.md) + - [Publish to HuaWei AppGallery Connect](editor/publish/publish-huawei-agc.md) + - [Publishing Huawei HarmonyOS Apps](editor/publish/publish-huawei-ohos.md) + - [Publishing macOS Desktop Application](editor/publish/mac/index.md) + - [macOS Publishing Example](editor/publish/mac/build-example-mac.md) + - [Build Options - macOS](editor/publish/mac/build-options-mac.md) + - [Publishing Windows Desktop Application](editor/publish/windows/index.md) + - [Windows Publishing Example](editor/publish/windows/build-example-windows.md) + - [Build Options - Windows](editor/publish/windows/build-options-windows.md) + - [Publish to Mini Game Platforms](editor/publish/publish-mini-game.md) + - [Publish to HUAWEI AppGallery Connect](editor/publish/publish-huawei-agc.md) + - [Publish to Alipay Mini Game](editor/publish/publish-alipay-mini-game.md) + - [Publish to Taobao Mini Game](editor/publish/publish-taobao-mini-game.md) + - [Publish to WeChat Mini Games](editor/publish/publish-wechatgame.md) + - [WeChat Mini Games Engine Plugin Instructions](editor/publish/wechatgame-plugin.md) + - [Access to WeChat PC Mini Games](editor/publish/publish-pc-wechatgame.md) + - [Publish to Douyin Mini Games](editor/publish/publish-bytedance-mini-game.md) + - [Access to Douyin PC Mini Games](editor/publish/publish-pc-bytedance.md) + - [Publish to Huawei Quick Games](editor/publish/publish-huawei-quick-game.md) + - [Publish to OPPO Mini Games](editor/publish/publish-oppo-mini-game.md) + - [Publish to vivo Mini Games](editor/publish/publish-vivo-mini-game.md) + - [Publish to Honor Mini Games](editor/publish/publish-honor-mini-game.md) + - [Publish to Baidu Mini Games](editor/publish/publish-baidu-mini-game.md) + - [Access to Open Data Context](editor/publish/build-open-data-context.md) + - [Mini Game Subpackage](editor/publish/subpackage.md) + - [Introduction to the Build Process and FAQ](editor/publish/build-guide.md) + - [Publish to Facebook Instant Games](editor/publish/publish-fb-instant-games.md) + - [Publish to Web Platforms](editor/publish/publish-web.md) + - [Publish from the Command Line](editor/publish/publish-in-command-line.md) + - [Custom Project Build Template](editor/publish/custom-project-build-template.md) + - [Build Process and FAQ](editor/publish/build-guide.md) + +## Function Modules + +- [Graphics](module-map/graphics.md) + - [Render Pipeline](render-pipeline/overview.md) + - [Built-in Render Pipeline](render-pipeline/builtin-pipeline.md) + - [Custom rendering of pipelines](render-pipeline/custom-pipeline.md) + - [Use Post Process](render-pipeline/use-post-process.md) + - [Custom Post Process](render-pipeline/post-process/custom.md) + + - [Camera](editor/components/camera-component.md) + - [Lighting](concepts/scene/light.md) + - [Physically Based Lighting](concepts/scene/light/pbr-lighting.md) + - [Lights](concepts/scene/light/lightType/index.md) + - [Directional Lights](concepts/scene/light/lightType/dir-light.md) + - [Spherical Lights](concepts/scene/light/lightType/sphere-light.md) + - [Spotlights](concepts/scene/light/lightType/spot-light.md) + - [Ambient Light](concepts/scene/light/lightType/ambient.md) + - [Additive Per-Pixel Lights](concepts/scene/light/additive-per-pixel-lights.md) + - [Shadows](concepts/scene/light/shadow.md) + - [Imaged Based Lighting](concepts/scene/light/probe/index.md) + - [Lightmapping](concepts/scene/light/lightmap.md) + - [Light Probes](concepts/scene/light/probe/light-probe.md) + - [Light Probe Panel](concepts/scene/light/probe/light-probe-panel.md) + - [Reflection Probe](concepts/scene/light/probe/reflection-probe.md) + - [Reflection Probe Panel](concepts/scene/light/probe/reflection-probe-panel.md) + - [Reflection Probe Art Workflow](concepts/scene/light/probe/reflection-art-workflow.md) + - [IBL Example](concepts/scene/light/probe/example.md) + - [Meshes](module-map/mesh/index.md) + - [MeshRenderer](engine/renderable/model-component.md) + - [SkinnedMeshRenderer](module-map/mesh/skinnedMeshRenderer.md) + - [SkinnedMeshBatchRenderer](module-map/mesh/skinnedMeshBatchRenderer.md) + - [Importing Models Exported from DCC Tools](asset/model/dcc-export-mesh.md) + - [Export FBX file from 3ds Max](asset/model/max-export-fbx.md) + - [Export FBX file from Maya](asset/model/maya-export-fbx.md) + - [glTF](asset/model/glTF.md) + - [Programmatically Create Meshes](asset/model/scripting-mesh.md) + - [Vertex Animation Texture (VAT)](asset/model/vat.md) + - [Textures](module-map/texture/index.md) + - [Textures](asset/texture.md) + - [Texture Cube](asset/texture-cube.md) + - [Texture Compression](asset/compress-texture.md) + - [RenderTexture](asset/render-texture.md) + - [Material System Overview](material-system/overview.md) + - [Programmatic Use of Materials](material-system/material-script.md) + - [Builtin Materials](material-system/builtin-material.md) + - [Material System Classes Diagram](material-system/material-structure.md) + - [Shader Overview](shader/index.md) + - [Create and Use Shader](shader/effect-inspector.md) + - [Built-in Shaders](shader/effect-builtin.md) + - [Physically Based Rendering - PBR](shader/effect-builtin-pbr.md) + - [Toon Shading](shader/effect-builtin-toon.md) + - [Unlit Shading](shader/effect-builtin-unlit.md) + - [Syntax](shader/effect-syntax.md) + - [Optional Pass Parameters](shader/pass-parameter-list.md) + - [YAML 101](shader/yaml-101.md) + - [GLSL](shader/glsl.md) + - [Preprocessor Macro Definition](shader/macros.md) + - [Chunk](shader/effect-chunk-index.md) + - [Built-in Uniforms](shader/uniform.md) + - [Common Functions](shader/common-functions.md) + - [Render Flow of Forward Rendering and Deferred Shading](shader/forward-and-deferred.md) + - [Surface Shader](shader/surface-shader.md) + - [Guide to Built-in Surface Shader](shader/surface-shader/builtin-surface-shader.md) + - [Surface Shader Overview](shader/surface-shader/surface-shader-structure.md) + - [Surface Shader Execution Flow](shader/surface-shader/shader-code-flow.md) + - [Include](shader/surface-shader/includes.md) + - [Macro Remapping](shader/surface-shader/macro-remapping.md) + - [Function Replacement Using Macros](shader/surface-shader/function-replace.md) + - [Surface Shader Built-in Replaceable Functions](shader/surface-shader/surface-function.md) + - [Render Usages](shader/surface-shader/render-usage.md) + - [Lighting Models](shader/surface-shader/lighting-mode.md) + - [Surface Material Data Structure](shader/surface-shader/surface-data-struct.md) + - [Shader Stages](shader/surface-shader/shader-stage.md) + - [Shader Assembly](shader/surface-shader/shader-assembly.md) + - [VS Inputs](shader/surface-shader/vs-input.md) + - [FS Inputs](shader/surface-shader/fs-input.md) + - [Customize Surface Shader](shader/surface-shader/customize-surface-shader.md) + - [Rendering Debug View](shader/surface-shader/rendering-debug-view.md) + - [Legacy Shader](shader/legacy-shader/legacy-shader.md) + - [Guide to Built-in Legacy Shaders](shader/legacy-shader/legacy-shader-builtins.md) + - [Legacy Shader Key Functions and Structures](shader/legacy-shader/legacy-shader-func-struct.md) + - [Write Shaders](shader/write-effect-overview.md) + - [2D Sprite Shader: Gradient](shader/write-effect-2d-sprite-gradient.md) + - [3D Shader: RimLight](write-effect-3d-rim-light.md) + - [Skin material](shader/advanced-shader/skin.md) + - [Instanced Attributes](shader/instanced-attributes.md) + - [UBO Layout](shader/ubo-layout.md) + - [Fallback to WebGL 1.0](shader/webgl-100-fallback.md) + - [VSCode Extension - Cocos Effect](shader/vscode-plugin.md) + - [Compute Shader](shader/compute-shader.md) + + - [2D Rendering Sorting](engine/rendering/sorting-2d.md) + - [3D Rendering Sorting](engine/rendering/sorting.md) + - [Effects](module-map/effects/index.md) + - [Billboard](particle-system/billboard-component.md) + - [Line](particle-system/line-component.md) + - [Skybox](concepts/scene/skybox.md) + - [Global Fog](concepts/scene/fog.md) + - [Geometry Renderer](geometry-renderer/index.md) + +- [2D Objects](2d-object/index.md) + - [2D Render](2d-object/2d-render/index.md) + - [Rendering Order](ui-system/components/engine/priority.md) + - [2D Renderable Component Batching Rules](ui-system/components/engine/ui-batch.md) + - [Custom Materials for 2D Rendering Objects](ui-system/components/engine/ui-material.md) + - [2D Renderable Components](ui-system/components/editor/render-component.md) + - [Sprite Component Reference](ui-system/components/editor/sprite.md) + - [Label Component Reference](ui-system/components/editor/label.md) + - [Mask Component Reference](ui-system/components/editor/mask.md) + - [Graphics Component Reference](ui-system/components/editor/graphics.md) + - [RichText Component Reference](ui-system/components/editor/richtext.md) + - [UIStaticBatch Component Reference](ui-system/components/editor/ui-static.md) + - [Spine Skeleton Component Reference](editor/components/spine.md) + - [DragonBones ArmatureDisplay Component Reference](editor/components/dragonbones.md) + - [TiledMap Component Reference](editor/components/tiledmap.md) + - [TiledTile Component Reference](editor/components/tiledtile.md) + - [MotionStreak Component Reference](editor/components/motion-streak.md) + - [UI System](2d-object/ui-system/index.md) + - [UI Components](ui-system/components/editor/base-component.md) + - [Canvas Component Reference](ui-system/components/editor/canvas.md) + - [UITransform Component Reference](ui-system/components/editor/ui-transform.md) + - [Widget Component Reference](ui-system/components/editor/widget.md) + - [Button Component Reference](ui-system/components/editor/button.md) + - [Layout Component Reference](ui-system/components/editor/layout.md) + - [EditBox Component Reference](ui-system/components/editor/editbox.md) + - [ScrollView Component Reference](ui-system/components/editor/scrollview.md) + - [ScrollBar Component Reference](ui-system/components/editor/scrollbar.md) + - [ProgressBar Component Reference](ui-system/components/editor/progress.md) + - [LabelOutline Component Reference](ui-system/components/editor/label-outline.md) + - [LabelShadow Component Reference](ui-system/components/editor/label-shadow.md) + - [Toggle Component Reference](ui-system/components/editor/toggle.md) + - [ToggleContainer Component Reference](ui-system/components/editor/toggleContainer.md) + - [Slider Component Reference](ui-system/components/editor/slider.md) + - [PageView Component Reference](ui-system/components/editor/pageview.md) + - [PageViewIndicator Component Reference](ui-system/components/editor/pageviewindicator.md) + - [UIMeshRenderer Component Reference](ui-system/components/editor/ui-model.md) + - [UICoordinateTracker Component Reference](ui-system/components/editor/ui-coordinate-tracker.md) + - [UIOpacity Component Reference](ui-system/components/editor/ui-opacity.md) + - [UISkew Component Reference](ui-system/components/editor/ui-skew.md) + - [BlockInputEvents Component Reference](ui-system/components/editor/block-input-events.md) + - [WebView Component Reference](ui-system/components/editor/webview.md) + - [VideoPlayer Component Reference](ui-system/components/editor/videoplayer.md) + - [SafeArea Component Reference](ui-system/components/editor/safearea.md) + - [UI Practice Guide](ui-system/components/engine/usage-ui.md) + - [Multi-Resolution Adaption](ui-system/components/engine/multi-resolution.md) + - [Widget Alignment](ui-system/components/engine/widget-align.md) + - [Label Layout](ui-system/components/engine/label-layout.md) + - [Auto Layout Container](ui-system/components/engine/auto-layout.md) + - [Create a List of Dynamically Generated Content](ui-system/components/engine/list-with-data.md) + - [Stretchable UI Sprite](ui-system/components/engine/sliced-sprite.md) + - [Android Large Screen Adaptation](ui-system/components/engine/large-screen.md) + +- [Animation](animation/index.md) + - [Animation Clip](animation/animation-clip.md) + - [Animation Component Reference](animation/animation-comp.md) + - [Animation Panel](animation/animation.md) + - [Creating Animation Components and Animation Clips](animation/animation-create.md) + - [Get Familiar with the Animation Panel](animation/animation-editor.md) + - [Editing Animation Clips](animation/edit-animation-clip.md) + - [Editing Animation Easing Curve](animation/animation-curve.md) + - [Adding Animation Events](animation/animation-event.md) + - [Using Animation Curves](animation/use-animation-curve.md) + - [Curve Editor](animation/curve-editor.md) + - [Skeletal Animation](animation/skeletal-animation.md) + - [Joint Texture Layout Settings](animation/joint-texture-layout.md) + - [Controlling Animation with Scripts](animation/animation-component.md) + - [Animation State](animation/animation-state.md) + - [Embedded Player](animation/embedded-player.md) + - [Marionette Animation System](animation/marionette/index.md) + - [Animation Graph Assets](animation/marionette/animation-graph.md) + - [Animation Controller Reference](animation/marionette/animation-controller.md) + - [Animation Graph Panel](animation/marionette/animation-graph-panel.md) + - [Animation Graph Layer](animation/marionette/animation-graph-layer.md) + - [Animation State Machine](animation/marionette/animation-graph-basics.md) + - [State Transition](animation/marionette/state-transition.md) + - [Animation Mask](animation/marionette/animation-mask.md) + - [Animation Graph Variants](animation/marionette/animation-variant.md) + - [Procedural Animation](animation/marionette/procedural-animation/index.md) + - [Introduction to Procedural Animation](animation/marionette/procedural-animation/introduce.md) + - [Enable Procedural Animation](animation/marionette/procedural-animation/enabling.md) + - [Pose Graph](animation/marionette/procedural-animation/pose-graph/index.md) + - [Pose Graph View](animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md) + - [Pose Nodes](animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md) + - [Blend Pose](animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md) + - [Modify Poses](animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md) + - [Play or Sample Animation](animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md) + +- [Audio System](audio-system/overview.md) + - [AudioSource Component Reference](audio-system/audiosource.md) + - [AudioMgr Example](audio-system/audioExample.md) + - [Compatibility Notes](audio-system/audioLimit.md) + +- [Physics System](physics/index.md) + - [Physics 2D](physics-2d/physics-2d.md) + - [2D Physics Manager](physics-2d/physics-2d-system.md) + - [2D RigidBody](physics-2d/physics-2d-rigid-body.md) + - [2D Physics Collider](physics-2d/physics-2d-collider.md) + - [2D Contact Callback](physics-2d/physics-2d-contact-callback.md) + - [2D Physics Joint](physics-2d/physics-2d-joint.md) + - [Physics 3D](physics/physics.md) + - [Physics Engines](physics/physics-engine.md) + - [Physics System Configuration](physics/physics-configs.md) + - [Group and Mask](physics/physics-group-mask.md) + - [Physics Components](physics/physics-component.md) + - [Collider](physics/physics-collider.md) + - [Rigidbody](physics/physics-rigidbody.md) + - [Constant Force](physics/physics-constantForce.md) + - [Constraint](physics/physics-constraint.md) + - [Physics Material](physics/physics-material.md) + - [Physics Event](physics/physics-event.md) + - [Raycast Detection](physics/physics-raycast.md) + - [Geometry Cast Detection](physics/physics-sweep.md) + - [Continuous Collision Detection](physics/physics-ccd.md) + - [Character Controller](physics/character-controller/index.md) + - [Physics Application Cases](physics/physics-example.md) + +- [Particle System](particle-system/index.md) + - [2D Particle System](particle-system/2d-particle/2d-particle.md) + - [3D Particle System](particle-system/overview.md) + - [Particle System Module](particle-system/module.md) + - [Main Module](particle-system/main-module.md) + - [Shape Module](particle-system/emitter.md) + - [Velocity Overtime Module](particle-system/velocity-module.md) + - [Force Overtime Module](particle-system/force-module.md) + - [Size Overtime Module](particle-system/size-module.md) + - [Rotation Overtime Module](particle-system/rotation-module.md) + - [Color Over Life Time Module](particle-system/color-module.md) + - [Texture Animation Module](particle-system/texture-animation-module.md) + - [Limit Velocity Overtime Module](particle-system/limit-velocity-module.md) + - [Trail Module](particle-system/trail-module.md) + - [Renderer Module](particle-system/renderer.md) + - [Particle Properties Editor](particle-system/editor/index.md) + - [Curve Editor](particle-system/editor/curve-editor.md) + - [Gradient Editor](particle-system/editor/gradient-editor.md) + - [Particle Editor](particle-system/editor/particle-effect-panel.md) + +- [Tween System](tween/index.md) + - [Tween Interface](tween/tween-interface.md) + - [Tween Function](tween/tween-function.md) + - [Tween Examples](tween/tween-example.md) + +- [Terrain System](editor/terrain/index.md) + +- [Asset Manager](asset/asset-manager.md) + - [AssetManager Upgrade Guide](asset/asset-manager-upgrade-guide.md) + - [Asset Bundle Upgrade Guide](asset/subpackage-upgrade-guide.md) + - [Asset Loading](asset/dynamic-load-resources.md) + - [Asset Bundle](asset/bundle.md) + - [Release Of Assets](asset/release-manager.md) + - [Download and Parse](asset/downloader-parser.md) + - [Loading and Preloading](asset/preload-load.md) + - [Cache Manager](asset/cache-manager.md) + - [Optional Parameters](asset/options.md) + - [Pipeline and Task](asset/pipeline-task.md) + - [Resource Management Considerations --- meta files](asset/meta.md) + +- [Localization](editor/l10n/overview.md) + - [Translation Service Provider](editor/l10n/translation-service.md) + - [Collect and Count](editor/l10n/collect-and-count.md) + - [Compile Language](editor/l10n/compile-language.md) + - [L10nLabel](editor/l10n/l10n-label.md) + - [Sample](editor/l10n/script-using.md) + +- [XR](xr/index.md) + - [Version History](xr/version-history.md) + - [Architecture](xr/architecture/index.md) + - [Built-in Resources and Prefabs](xr/architecture/assets.md) + - [XR Components](xr/architecture/component.md) + - [XR Preview](xr/architecture/preview.md) + - [XR Video Player](xr/architecture/xr-video-player.md) + - [XR Preview in Browser](xr/architecture/xr-webview.md) + - [XR Spatial Audio](xr/architecture/xr-spatial-audio.md) + - [XR Composition Layer](xr/architecture/xr-composition-layer.md) + - [Passthrough](xr/architecture/xr-pass-through.md) + - [AR](xr/architecture/ar-introduce.md) + - [AR Camera](xr/architecture/ar-camera.md) + - [AR Manager](xr/architecture/ar-manager.md) + - [AR Automated Behavior Editing](xr/architecture/ar-tracking-component.md) + - [AR Interaction](xr/architecture/ar-interaction.md) + - [Quick Start](xr/project-deploy/index.md) + - [VR Project Creation](xr/project-deploy/vr-proj-deploy.md) + - [VR Building and Publishing](xr/project-deploy/vr-proj-pub.md) + - [AR Project Creation](xr/project-deploy/ar-proj-deploy.md) + - [AR Building and Publishing](xr/project-deploy/ar-proj-pub.md) + - [WebXR Project Setup](xr/project-deploy/webxr-proj-deploy.md) + - [WebXR Building and Publishing](xr/project-deploy/webxr-proj-pub.md) +- [Native Development](native/overview.md) + - [Native Platform Secondary Development Guide](advanced-topics/native-secondary-development.md) + - [JavaScript and Android Communication with Reflection](advanced-topics/java-reflection.md) + - [JavaScript and iOS/macOS Communication with Reflection](advanced-topics/oc-reflection.md) + - [JavaScript and Java Communication using JsbBridge](advanced-topics/js-java-bridge.md) + - [JavaScript and Objective-C Communication using JsbBridge](advanced-topics/js-oc-bridge.md) + - [JsbBridgeWrapper - An Event Mechanism based on JsbBridge](advanced-topics/jsb-bridge-wrapper.md) + - [Tutorial: JSB 2.0](advanced-topics/JSB2.0-learning.md) + - [JSB Manual Binding](advanced-topics/jsb-manual-binding.md) + - [JSB Auto Binding](advanced-topics/jsb-auto-binding.md) + - [Swig](advanced-topics/jsb-swig.md) + - [Swig Tutorial](advanced-topics/jsb/swig/tutorial/index.md) + - [CMake Usage Introduction](advanced-topics/cmake-learning.md) + - [Native Engine Memory Leak Detection System](advanced-topics/memory-leak-detector.md) + - [Native Scene Culling](advanced-topics/native-scene-culling.md) + - [Native Profiler](advanced-topics/profiler.md) + - [Native Plugins](advanced-topics/native-plugins/brief.md) + - [Cocos Native Plugin Quick Tutorial](advanced-topics/native-plugins/tutorial.md) + - [Optimization of Cross-Language Invocation](advanced-topics/jsb-optimizations.md) + +## Advanced Tutorials + +- [Editor Extension](editor/extension/readme.md) + - [Extension Manager](editor/extension/extension-manager.md) + - [Extension Templates and Compile Builds](editor/extension/create-extension.md) + - [Getting Started Example - Menu](editor/extension/first.md) + - [Getting Started Example - Panel](editor/extension/first-panel.md) + - [Getting Started Example - First Data Interaction](editor/extension/first-communication.md) + - [Change the Name of a Extension](editor/extension/extension-change-name.md) + - [Install and Share](editor/extension/install.md) + - [Submitting Resources to Cocos Store](editor/extension/store/upload-store.md) + - [Extend Existing Functionality](editor/extension/contributions.md) + - [Customize the Main Menu](editor/extension/contributions-menu.md) + - [Customized Messages](editor/extension/contributions-messages.md) + - [Calling the Engine API and Project Script](editor/extension/scene-script.md) + - [Extending the Assets Panel](editor/assets/extension.md) + - [Custom Asset Database](editor/extension/contributions-database.md) + - [Custom Inspector Panel](editor/extension/inspector.md) + - [Extending Build Process](editor/publish/custom-build-plugin.md) + - [Extending Project Settings Panel](editor/extension/contributions-project.md) + - [Extending the Preferences Panels](editor/extension/contributions-preferences.md) + - [Extending Shortcut](editor/extension/contributions-shortcuts.md) + - [Extension Details](editor/extension/basic.md) + - [Extension Infrastructure](editor/extension/package.md) + - [Definition of Extension](editor/extension/define.md) + - [Message System](editor/extension/messages.md) + - [Configuration System](editor/extension/profile.md) + - [Extension Panel](editor/extension/panel.md) + - [UI Components](editor/extension/ui.md) +- [Advanced Topics](advanced-topics/index.md) + - [Submit Code to Cocos Engine Repository](submit-pr/submit-pr.md) + - [User Data Storage](advanced-topics/data-storage.md) + - [Custom loading Wasm/Asm files and modules](advanced-topics/wasm-asm-load.md) + - [Converting Native Code to Wasm/Asm Files Using Emscripten](advanced-topics/wasm-asm-create.md) + - [Engine Customization Workflow](advanced-topics/engine-customization.md) + - [Web Preview Customization Workflow](editor/preview/browser.md) + - [Dynamic Atlas](advanced-topics/dynamic-atlas.md) + - [Mangle Engine Internal Properties](advanced-topics/mangle-properties.md) + - [Hot Update Tutorial](advanced-topics/hot-update.md) + - [AssetManager for Hot Update](advanced-topics/hot-update-manager.md) + - [HTTP Request](advanced-topics/http.md) + - [WebSocket Introduction](advanced-topics/websocket-introduction.md) + - [WebSocket Client](advanced-topics/websocket.md) + - [WebSocket Server](advanced-topics/websocket-server.md) diff --git a/versions/4.0/en/_layouts/website/header.html b/versions/4.0/en/_layouts/website/header.html new file mode 100644 index 0000000000..1a997e5dba --- /dev/null +++ b/versions/4.0/en/_layouts/website/header.html @@ -0,0 +1,48 @@ +{% extends template.self %} +{% block book_header %} + + +{% endblock %} \ No newline at end of file diff --git a/versions/4.0/en/_layouts/website/page.html b/versions/4.0/en/_layouts/website/page.html new file mode 100644 index 0000000000..a5f5f58425 --- /dev/null +++ b/versions/4.0/en/_layouts/website/page.html @@ -0,0 +1,56 @@ +{% extends template.self %} +{% block book_sidebar %} + + {{ super() }} +{% endblock %} +{% block javascript %} + {{ super() }} + +{% endblock %} diff --git a/versions/4.0/en/advanced-topics/JSB2.0-learning.md b/versions/4.0/en/advanced-topics/JSB2.0-learning.md new file mode 100644 index 0000000000..27d90dde3c --- /dev/null +++ b/versions/4.0/en/advanced-topics/JSB2.0-learning.md @@ -0,0 +1,1200 @@ +# Tutorial: JSB 2.0 + +## The Abstraction Layer of Script Engine + +### Architecture + +![JSB2.0-Architecture](jsb/JSB2.0-Architecture.png) + +### Macro + +The abstraction layer is bound to take more CPU execution time than using the JS engine API directly. How to minimize the overhead of the abstraction layer becomes the first goal of the design. + +Most of work in JS binding is actually setting JS related operations with CPP callbacks and associating CPP object within the callback function. In fact, it mainly contains the following two situation: + +- Register JS functions (including global functions, class constructors, class destructors, class member functions, and class static member functions), binding revenant CPP callbacks +- Register accessors for JS properties, bind CPP callbacks for reading and writing properties respectively + +How to achieve the minimum overhead for the abstract layer and expose the unified API? + +For example, to register a JS function in CPP, there are different definitions in JavaScriptCore, SpiderMonkey, V8, ChakraCore as follows: + +- JavaScriptCore + + ```c++ + JSValueRef JSB_foo_func( + JSContextRef _cx, + JSObjectRef _function, + JSObjectRef _thisObject, + size_t argc, + const JSValueRef _argv[], + JSValueRef* _exception + ); + ``` + +- SpiderMonkey + + ```c++ + bool JSB_foo_func( + JSContext* _cx, + unsigned argc, + JS::Value* _vp + ); + ``` + +- V8 + + ```c++ + void JSB_foo_func( + const v8::FunctionCallbackInfo& v8args + ); + ``` + +- ChakraCore + + ```c++ + JsValueRef JSB_foo_func( + JsValueRef _callee, + bool _isConstructCall, + JsValueRef* _argv, + unsigned short argc, + void* _callbackState + ); + ``` + +We evaluated several options and eventually decided to use `macros` to reduce the differences between the definition and parameter types of different JS engine callbacks, regardless of which engine is used, and developers could use an unified callback definition. We refer to the definition of Lua callback function. The definition of all JS to CPP callback functions in the abstract layer is defined as: + +```c++ +bool foo(se::State& s) +{ + ... + ... +} +SE_BIND_FUNC(foo) // Binding a JS function as an example +``` + +After a developer has bound a JS function, remember to wrap the callback function with the macros which start with `SE_BIND_`. Currently, we provide the following macros: + +- **SE_BIND_PROP_GET**: Wrap a JS object property read callback function +- **SE_BIND_PROP_SET**: Wrap a JS object property written callback function +- **SE_BIND_FUNC_AS_PROP_GET**: Wrap a member function as a JS property getter callback function. +- **SE_BIND_FUNC_AS_PROP_SET**: Wrap a member function as a JS property setter callback function +- **SE_BIND_FUNC**: Wrap a JS function that can be used for global functions, class member functions or class static functions +- **SE_BIND_FUNC_FAST**: Wrap a parameter-less C++ function into a JS function in a faster way. +- **SE_DECLARE_FUNC**: Declare a JS function, generally used in the header file +- **SE_BIND_CTOR**: Wrap a JS constructor +- **SE_BIND_SUB_CLS_CTOR**: Wrap the constructor of a JS subclass. +- **SE_BIND_FINALIZE_FUNC**: Wrap the finalize function of a JS object, finalize function is invoked when the object is released by Garbage Collector +- **SE_DECLARE_FINALIZE_FUNC**: Declares the finalize function of a JS object +- **_SE**: The macro for making callback be recognized by different JS engine. Note that the first character is underscored, similar to `_T ('xxx')` in Windows for wrapping Unicode or MultiBytes string + + > **Note**: the first character is an underscore, similar to `_T("xxx")` used in Windows to wrap Unicode or MultiBytes strings. + +## API + +### CPP Namespace + +All types of the abstraction layer are under the `se` namespace, which is an abbreviation of `ScriptEngine`. + +### Types + +#### se::ScriptEngine + +`se::ScriptEngine` is the JS engine administrator, responsible for JS engine initialization, destruction, restart, native module registration, loading scripts, doing garbage collection, JS exception cleanup and whether to enable the debugger. It is a singleton that could be accessed via `se::ScriptEngine::getInstance()`. + +#### se::Value + +`se::Value` can be understood as a JS variable reference in the CPP layer. There are six types of JS variables: `object`, `number`, `bigint` `string`, `boolean`, `null`, `undefined`, so `se::Value` uses an `union` to include `object`, `number`, `string`, `boolean`, `int64_t` these 5 kinds of `value types`, `non-value types` like `null` and `undefined` can be represented by `_type` directly. + +```c++ +namespace se { + class Value { + enum class Type : char + { + Undefined = 0, + Null, + Number, + Boolean, + String, + Object, + BigInt, // mostly used to store a 8 bytes pointer + }; + ... + ... + private: + union { + bool _boolean; + double _number; + std::string* _string; + Object* _object; + int64_t _bigint; + } _u; + + Type _type; + ... + ... + }; +} +``` + +If `se::Value` stores the underlying data types, such as `number`, `string`, `boolean`, which is directly stored by `value copy`.
+The storage of `object` is special because it is a `weak reference` to JS objects via `se::Object*`. + +#### se::Object + +`se::Object` extends from `se::RefCounter` which is a class for reference count management. Currently, only `se::Object` inherits from `se::RefCounter` in the abstraction layer. + +As we mentioned in the last section, `se::Object` is a weak reference to the JS object, therefore I will explain why it's a weak reference. + +**Reason 1: The requirement of controlling the life cycle of CPP objects by JS objects** + +After creating a Sprite in the script layer via `var xhr = new XMLHttpRequest();`, we create a `se::Object` in the constructor callback and leave it in a global map (NativePtrToObjectMap), this map is used to query the `XMLHttpRequest*` to get the corresponding JS object `se::Object*`. + +```c++ +/// native/cocos/bindings/manual/jsb_xmlhttprequest.cpp +static bool XMLHttpRequest_constructor(se::State& s) +{ + XMLHttpRequest* cobj = JSB_ALLOC(XMLHttpRequest); + s.thisObject()->setPrivateData(cobj); + return true; +} +SE_BIND_CTOR(XMLHttpRequest_constructor, __jsb_XMLHttpRequest_class, XMLHttpRequest_finalize) + +/// native/cocos/bindings/jswrapper/v8/Object.cpp +void Object::setPrivateObject(PrivateObjectBase *data) { + // ... + if (data != nullptr) { + _privateData = data->getRaw(); + NativePtrToObjectMap::emplace(_privateData, this); + } else { + _privateData = nullptr; + } +} +``` + +Imagine if you force `se::Object` to be a strong reference to a JS object that leaves JS objects out of GC control and the finalize callback will never be fired because `se::Object` is always present in map which will cause memory leak. + +**Reason 2: More flexible, supporting strong reference by calling the se::Object::root method manually** + +`se::Object` provides `root/unroot` method for developers to invoke, `root` will put JS object into the area not be scanned by the GC. After calling `root`, `se::Object*` is a strong reference to the JS object. JS object will be put back to the area scanned by the GC only when `se::Object` is destructed or `unroot` is called to make root count to zero. + +Under normal circumstances, if the C++ object is not a subclass of `cc::Ref`, the C++ object will be used to control the life cycle of the JS object in binding. Binding the engine modules, like Spine, DragonBones, Box2d and other third-party libraries uses this method. When the C++ object is released, it is necessary to find the corresponding `se::Object` in the `NativePtrToObjectMap`, then manually `unroot` and `decRef` it. Take the binding of `spTrackEntry` in Spine as an example: + +```c++ +spTrackEntry_setDisposeCallback([](spTrackEntry* entry){ + se::Object* seObj = nullptr; + + auto iter = se::NativePtrToObjectMap::find(entry); + if (iter != se::NativePtrToObjectMap::end()) + { + // Save se::Object pointer for being used in cleanup method. + seObj = iter->second; + // Unmap native and js object since native object was destroyed. + // Otherwise, it may trigger 'assertion' in se::Object::setPrivateData later + // Since native obj is already released and the new native object may be assigned with the same address. + se::NativePtrToObjectMap::erase(iter); + } else { + return; + } + + auto cleanup = [seObj](){ + + auto se = se::ScriptEngine::getInstance(); + if (!se->isValid() || se->isInCleanup()) + return; + + se::AutoHandleScope hs; + se->clearException(); + + // The mapping of native object & se::Object was cleared in above code. + // The private data (native object) may be a different object associated with other se::Object. + // Therefore, don't clear the mapping again. + seObj->clearPrivateData(false); + seObj->unroot(); // Unroot, making JS objects subject to GC management + seObj->decRef(); // Release se::Object + }; + + if (!se::ScriptEngine::getInstance()->isGarbageCollecting()) { + cleanup(); + } + else { + CleanupTask::pushTaskToAutoReleasePool(cleanup); + } + }); +``` + +**C++ Object lifecycle management** + +Prior to 3.6, the destructor callback `_finalize` would call `delete` or `release` to release the corresponding C++ object, depending on the object type and whether it existed in `se::NonRefNativePtrCreatedByCtorMap`. Since 3.6, the `_finalize` callback has been deprecated and left empty for the time being for debugging purposes. `se::Object` establishes a lifecycle association with C++ objects via `se::PrivateObjectBase` objects. The three subclasses of `se::PrivateObjectBase` correspond to different release policies. + +- `se::CCSharedPtrPrivateObject` + +uses `cc::IntrusivePtr` to store pointers to C++ objects, requiring that the C++ class inherits from `cc::RefCounted`. Where `cc::IntrusivePtr` is a smart pointer type that automatically increments or decrements the reference count of `cc::RefCounted`. When the reference count is 0, destructions are triggered. + +- `se::SharedPrivateObject` + +Use `std::shared_ptr` to store C++ object pointers, requiring that C++ classes **do not inherit** from `cc::RefCounted`. Due to the nature of `shared_ptr` itself, all strong references are required to be `shared_ptr`. Destructing C++ objects is triggered when all `shared_ptr`s are destroyed. + +- `se::RawRefPrivateObject` + +Uses a bare pointer, which defaults to a weak reference to a C++ object. It can be converted to a strong reference by calling `tryAllowDestroyInGC`. As a weak reference, GC does not trigger destructing the object. + +**Associate native objects**. + +After 3.6 `se::Object::setPrivateData(void *)` is extended to: +```c++ +template +inline void setPrivateData(T *data); +``` +can automatically create `SharedPrivateObject` or `CCSharedPtrPrivateObject` based on type information, but does not support `RawRefPrivateObject`. + +We can use `setPrivateObject` to display the type of the specified `PrivateObject`: +```c++ +// se::SharedPrivateObject +obj->setPrivateObject(se::shared_private_object(v)); + +// se::CCSharedPtrPrivateObject +obj->setPrivateObject(se::ccshared_private_object(v)); + +// se::RawRefPrivateObject +obj->setPrivateObject(se::rawref_private_object(v)); +``` + + +__Object Types__ + +The creation of native binding object has been hidden in the `SE_BIND_CTOR` and `SE_BIND_SUB_CLS_CTOR` macros, if developers need to use the `se::Object` in the binding callback, just get it by invoking `s.thisObject()`. Where `s` is `se::State&` which will be described in the following chapters. + +In addition, `se::Object` currently supports the manual creation of the following objects: + +- Plain Object: Created by `se::Object::createPlainObject`, similar to `var a = {};` in JS +- Array Object: Created by `se::Object::createArrayObject`, similar to `var a = [];` in JS +- Uint8 Typed Array Object: Created by `se::Object::createTypedArray`, like `var a = new Uint8Array(buffer);` in JS +- Array Buffer Object: Created by `se::Object::createArrayBufferObject` similar to `var a = new ArrayBuffer(len);` in JS + +__The Release of The Objects Created Manually__ + +`se::Object::createXXX` is unlike the create method in Cocos Creator, the abstraction layer is a completely separate module which does not rely on the autorelease mechanism in Cocos Creator. Although `se::Object` also inherits the reference count class `se::RefCounter`, developers need to handle the release for **objects created manually**. + +```c++ +se::Object* obj = se::Object::createPlainObject(); +... +... +obj->decRef(); // Decrease the reference count to avoid memory leak +``` + +#### se::HandleObject (recommended helper class for managing the objects created manually) + +- If using manual creation of objects in complex logic, developers often forget to deal with `decRef` in different conditions + + ```c++ + bool foo() + { + se::Object* obj = se::Object::createPlainObject(); + if (var1) + return false; // Return directly, forget to do 'decRef' operation + + if (var2) + return false; // Return directly, forget to do 'decRef' operation + ... + ... + obj->decRef(); + return true; + } + ``` + + Plus adding `decRef` to different return condition branches can result in logically complex and difficult to maintain, and it is easy to forget about `decRef` if you make another return branch later. + +- If the JS engine did a GC operationJS engine right after `se::Object::createXXX`, which will result in the `se::Object` reference to an illegal pointer, the program may crash. + +In order to solve the above problems, the abstraction layer defines a type that assists in the management of **manually created objects**, namely `se::HandleObject`. + +`se::HandleObject` is a helper class for easier management of the `release (decRef)`, `root`, and `unroot` operations of manually created `se::Object` objects. + +The following two code snippets are equivalent, the use of `se::HandleObject` significantly smaller amount of code, and more secure. + +```c++ +{ + se::HandleObject obj(se::Object::createPlainObject()); + obj->setProperty(...); + otherObject->setProperty("foo", se::Value(obj)); +} +``` + +Is equal to: + +```C++ +{ + se::Object* obj = se::Object::createPlainObject(); + obj->root(); // Root the object immediately to prevent the object being garbage collected. + + obj->setProperty(...); + otherObject->setProperty("foo", se::Value(obj)); + + obj->unroot(); // Call unroot while the object is needed anymore. + obj->decRef(); // Decrease the reference count to avoid memory leak. +} +``` + +> **NOTES**: +> +> 1. Do not try to use `se::HandleObject` to create a native binding object. In the `JavaScript controls of C++` mode, the release of the bound object will be automatically handled by the abstraction layer. In the `C++ controls JavaScript` mode, the previous chapter has already described it. +> 2. The `se::HandleObject` object can only be allocated on the stack, and a `se::Object` pointer must be passed in. + +#### se::Class + +`se::Class` is used to expose CPP classes to JS, it creates a constructor function in JS that has a corresponding name. + +It has the following methods: + +- `static se::Class* create(className, obj, parentProto, ctor)`: **Creating a Class**. If the registration is successful, it is then possible to create an object by calling `var xxx = new SomeClass ();` in the JavaScript layer. +- `bool defineFunction(name, func)`: Define a member function for a class. +- `bool defineProperty(name, getter, setter)`: Define a property accessor for a class. +- `bool defineStaticFunction(name, func)`: Define a static function for a class, the JavaScript function could be accessed by `SomeClass.foo()` rather than calling `var obj = new SomeClass(); obj.foo()`, this means it's a class method instead of an instance method. +- `bool defineStaticProperty(name, getter, setter)`: Define a static property accessor which could be invoked by `SomeClass.propertyA`, it's nothing about instance object. +- `bool defineFinalizeFunction(func)`: Define the finalize callback function after JS object is garbage collected. +- `bool install()`: Install a class JS engine. +- `Object* getProto()`: Get the prototype of JS constructor installed, similar to `Foo.prototype` of `function Foo(){}` in JS. +- `const char* getName() const`: Get the class name which is also the name of JS constructor. + +> **NOTE**: you do not need to release memory manually after `se::Class` type is created, it will be automatically encapsulated layer. + +You could look through the API documentation or code comments for more specific API instructions. + +#### se::AutoHandleScope + +The `se::AutoHandleScope` object type is purely a concept introduced to address V8 compatibility issues. In V8, any action that calls `v8::Local<>` on a CPP function that needs to trigger a JS related operation, such as calling a JS function, accessing a JS property, etc, requires a `v8::HandleScope` function be invoked before calling these operations, otherwise it will cause the program to crash. + +So the concept of `se::AutoHandleScope` was introduced into the abstraction layer, which is implemented only on V8, and the other JS engines are currently just empty implementations. + +Developers need to remember that in any code execution from CPP, you need to declare a `se::AutoHandleScope` before calling JS's logic. For example: + +```c++ +class SomeClass { + void update(float dt) { + se::ScriptEngine::getInstance()->clearException(); // Clear JS exceptions + se::AutoHandleScope hs; // Declare a handle scope, it's needed for V8 + + se::Object* obj = ...; + obj->setProperty(...); + ... + ... + obj->call(...); + } +}; +``` + +#### se::State + +In the previous section, we have mentioned the `se::State` type, which is an environment in the binding callback. We can get the current CPP pointer, `se::Object` object pointer, parameter list and return value reference through `se::State` argument. + +```c++ +bool foo(se::State& s) +{ + // Get native object pointer bound with the current JS object. + SomeClass* cobj = (SomeClass*)s.nativeThisObject(); + // Get se::Object pointer that represents the current JS object. + se::Object* thisObject = s.thisObject(); + // Get argument list of the current function. + const se::ValueArray& args = s.args(); + // Set return value for current function. + s.rval().setInt32(100); + // Return true to indicate the function is executed successfully. + return true; +} +SE_BIND_FUNC(foo) +``` + +## Does The Abstraction Layer Depend on Cocos Creator? + +**No, it doesn't.** + +This abstraction layer was originally designed as a stand-alone module which is completely independent of Cocos Creator engine. Developers can copy the abstraction layer code in `cocos/scripting/jswrapper` directory and paste them to other projects directly. + +## Manual Binding + +### Define A Callback Function + +```c++ +static bool Foo_balabala(se::State& s) +{ + const auto& args = s.args(); + int argc = (int)args.size(); + + if (argc >= 2) // Limit the number of parameters must be greater than or equal to 2, or throw an error to the JS layer and return false. { + ... + ... + return true; + } + + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 2) ; + return false; +} + +// If binding a function, we use SE_BIND_FUNC macro. For binding a constructor, destructor, subclass constructor, please use SE_BIND_balabala macros memtioned above. +SE_BIND_FUNC(Foo_balabala) +``` + +### Set A Property Value for JS object + +```c++ +se::Object* globalObj = se::ScriptEngine::getInstance()->getGlobalObject(); // We get the global object just for easier demonstration. +globalObj->setProperty("foo", se::Value(100)); // Set a property called `foo` with a value of 100 to the global object. +``` + +Next, use the `foo` global variable in JS directly. + +```js +log("foo value: " + foo); // Print `foo value: 100`. +``` + +### Set A Property Accessor for JS Object + +```c++ +// The read callback of "foo" property of the global object +static bool Global_get_foo(se::State& s) +{ + NativeObj* cobj = (NativeObj*)s.nativeThisObject(); + int32_t ret = cobj->getValue(); + s.rval().setInt32(ret); + return true; +} +SE_BIND_PROP_GET(Global_get_foo) + +// The write callback of "foo" property of the global object +static bool Global_set_foo(se::State& s) +{ + const auto& args = s.args(); + int argc = (int)args.size(); + if (argc >= 1) + { + NativeObj* cobj = (NativeObj*)s.nativeThisObject(); + int32_t arg1 = args[0].toInt32(); + cobj->setValue(arg1); + // Do not need to call "s.rval().set(se::Value::Undefined)" for functions without return value. + return true; + } + + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 1) ; + return false; +} +SE_BIND_PROP_SET(Global_set_foo) + +void some_func() +{ + se::Object* globalObj = se::ScriptEngine::getInstance()->getGlobalObject(); // We get the global object just for easier demonstration. + globalObj->defineProperty("foo", _SE(Global_get_foo), _SE(Global_set_foo)); // Use _SE macro to package specific function name. +} +``` + +### Define A Function for JS Object + +```c++ +static bool Foo_function(se::State& s) +{ + ... + ... +} +SE_BIND_FUNC(Foo_function) + +void some_func() +{ + se::Object* globalObj = se::ScriptEngine::getInstance()->getGlobalObject(); // We get the global object just for easier demonstration. + globalObj->defineFunction("foo", _SE(Foo_function)); // Use _SE macro to package specific function name. +} +``` + +### Register A CPP Class to JS Virtual Machine + +```c++ +static se::Object* __jsb_ns_SomeClass_proto = nullptr; +static se::Class* __jsb_ns_SomeClass_class = nullptr; + +namespace ns { + class SomeClass + { + public: + SomeClass() + : xxx(0) + {} + + void foo() { + printf("SomeClass::foo\n"); + + Director::getInstance()->getScheduler()->schedule([this](float dt){ + static int counter = 0; + ++counter; + if (_cb != nullptr) + _cb(counter); + }, this, 1.0f, CC_REPEAT_FOREVER, 0.0f, false, "iamkey"); + } + + static void static_func() { + printf("SomeClass::static_func\n"); + } + + void setCallback(const std::function& cb) { + _cb = cb; + if (_cb != nullptr) + { + printf("setCallback(cb)\n"); + } + else + { + printf("setCallback(nullptr)\n"); + } + } + + int xxx; + private: + std::function _cb; + }; +} // namespace ns { + +static bool js_SomeClass_finalize(se::State& s) +{ + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + delete cobj; + return true; +} +SE_BIND_FINALIZE_FUNC(js_SomeClass_finalize) + +static bool js_SomeClass_constructor(se::State& s) +{ + ns::SomeClass* cobj = new ns::SomeClass(); + s.thisObject()->setPrivateData(cobj); + return true; +} +SE_BIND_CTOR(js_SomeClass_constructor, __jsb_ns_SomeClass_class, js_SomeClass_finalize) + +static bool js_SomeClass_foo(se::State& s) +{ + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + cobj->foo(); + return true; +} +SE_BIND_FUNC(js_SomeClass_foo) + +static bool js_SomeClass_get_xxx(se::State& s) +{ + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + s.rval().setInt32(cobj->xxx); + return true; +} +SE_BIND_PROP_GET(js_SomeClass_get_xxx) + +static bool js_SomeClass_set_xxx(se::State& s) +{ + const auto& args = s.args(); + int argc = (int)args.size(); + if (argc > 0) + { + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + cobj->xxx = args[0].toInt32(); + return true; + } + + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 1); + return false; +} +SE_BIND_PROP_SET(js_SomeClass_set_xxx) + +static bool js_SomeClass_static_func(se::State& s) +{ + ns::SomeClass::static_func(); + return true; +} +SE_BIND_FUNC(js_SomeClass_static_func) + +bool js_register_ns_SomeClass(se::Object* global) +{ + // Make sure the namespace exists + se::Value nsVal; + if (!global->getProperty("ns", &nsVal)) + { + // If it doesn't exist, create one. Similar as "var ns = {};" in JS. + se::HandleObject jsobj(se::Object::createPlainObject()); + nsVal.setObject(jsobj); + + // Set the object to the global object with the property name `ns`. + global->setProperty("ns", nsVal); + } + se::Object* ns = nsVal.toObject(); + + // Create a se::Class object, developers do not need to consider the release of the se::Class object, which is automatically handled by the ScriptEngine. + auto cls = se::Class::create("SomeClass", ns, nullptr, _SE(js_SomeClass_constructor)); // If the registered class doesn't need a constructor, the last argument can be passed in with nullptr, it will make "new SomeClass();" illegal. + + // Define member functions, member properties. + cls->defineFunction("foo", _SE(js_SomeClass_foo)); + cls->defineProperty("xxx", _SE(js_SomeClass_get_xxx), _SE(js_SomeClass_set_xxx)); + + // Define finalize callback function + cls->defineFinalizeFunction(_SE(js_SomeClass_finalize)); + + // Install the class to JS virtual machine + cls->install(); + + // JSBClassType::registerClass is a helper function in the Cocos Creator native binding code, which is not a part of the ScriptEngine. + JSBClassType::registerClass(cls); + + // Save the result to global variable for easily use in other places, for example class inheritance. + __jsb_ns_SomeClass_proto = cls->getProto(); + __jsb_ns_SomeClass_class = cls; + + // Set a property "yyy" with the string value "helloyyy" for each object instantiated by this class. + __jsb_ns_SomeClass_proto->setProperty("yyy", se::Value("helloyyy")); + + // Register static member variables and static member functions + se::Value ctorVal; + if (ns->getProperty("SomeClass", &ctorVal) && ctorVal.isObject()) + { + ctorVal.toObject()->setProperty("static_val", se::Value(200)); + ctorVal.toObject()->defineFunction("static_func", _SE(js_SomeClass_static_func)); + } + + // Clear JS exceptions + se::ScriptEngine::getInstance()->clearException(); + return true; +} +``` + +### How to Bind A CPP Callback Function + +```c++ +static bool js_SomeClass_setCallback(se::State& s) +{ + const auto& args = s.args(); + int argc = (int)args.size(); + if (argc >= 1) + { + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + + se::Value jsFunc = args[0]; + se::Value jsTarget = argc > 1 ? args[1] : se::Value::Undefined; + + if (jsFunc.isNullOrUndefined()) + { + cobj->setCallback(nullptr); + } + else + { + assert(jsFunc.isObject() && jsFunc.toObject()->isFunction()); + + se::Object *jsTargetObj = jsTarget.isObject() ? jsTarget.toObject() : nullptr; + + // If the current SomeClass is a class that can be created by "new", we use "se::Object::attachObject" to associate jsFunc with jsTarget to the current object. + s.thisObject()->attachObject(jsFunc.toObject()); + if(jsTargetObj) s.thisObject()->attachObject(jsTargetObj); + + // If the current SomeClass class is a singleton, or a class that always has only one instance, we can not associate it with "se::Object::attachObject". + // Instead, you must use "se::Object::root", developers do not need to unroot since unroot operation will be triggered in the destruction of lambda which makes the "se::Value" jsFunc be destroyed, then "se::Object" destructor will do the unroot operation automatically. + // The binding function "js_audio_AudioEngine_setFinishCallback" implements it in this way because "AudioEngine" is always a singleton. + // Using "s.thisObject->attachObject(jsFunc.toObject);" for binding addCustomEventListener will cause jsFunc and jsTarget variables can't be released, which will result in memory leak. + + // jsFunc.toObject()->root(); + // jsTarget.toObject()->root(); + + cobj->setCallback([jsFunc, jsTargetObj](int counter) { + + // Add the following two lines of code in CPP callback function before passing data to the JS. + se::ScriptEngine::getInstance()->clearException(); + se::AutoHandleScope hs; + + + se::ValueArray args; + args.push_back(se::Value(counter)); + + jsFunc.toObject()->call(args, jsTargetObj); + }); + } + + return true; + } + + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 1); + return false; +} +SE_BIND_FUNC(js_SomeClass_setCallback) +``` + +After SomeClass is registered, use it in JS like the following: + +```js + var myObj = new ns.SomeClass(); + myObj.foo(); + ns.SomeClass.static_func(); + log("ns.SomeClass.static_val: " + ns.SomeClass.static_val); + log("Old myObj.xxx:" + myObj.xxx); + myObj.xxx = 1234; + log("New myObj.xxx:" + myObj.xxx); + log("myObj.yyy: " + myObj.yyy); + + var delegateObj = { + onCallback: function(counter) { + log("Delegate obj, onCallback: " + counter + ", this.myVar: " + this.myVar); + this.setVar(); + }, + + setVar: function() { + this.myVar++; + }, + + myVar: 100 + }; + + myObj.setCallback(delegateObj.onCallback, delegateObj); + + setTimeout(function(){ + myObj.setCallback(null); + }, 6000); // Clear callback after 6 seconds. +``` + +There will be some logs outputted in console: + +``` +SomeClass::foo +SomeClass::static_func +ns.SomeClass.static_val: 200 +Old myObj.xxx:0 +New myObj.xxx:1234 +myObj.yyy: helloyyy +setCallback(cb) +Delegate obj, onCallback: 1, this.myVar: 100 +Delegate obj, onCallback: 2, this.myVar: 101 +Delegate obj, onCallback: 3, this.myVar: 102 +Delegate obj, onCallback: 4, this.myVar: 103 +Delegate obj, onCallback: 5, this.myVar: 104 +Delegate obj, onCallback: 6, this.myVar: 105 +setCallback(nullptr) +``` + +### How to Use The Helper Functions in Cocos Creator Binding for Easier Native<->JS Type Conversions + +The helper functions for native<->JS type conversions are located in `cocos/bindings/manual/jsb_conversions.h`, it includes: + +#### Convert se::Value to C++ Type + +Support base types `int*t`/`uint*_t`/`float`/`double`/`const char*`/`bool`, `std::string`, bound types, their container types `std::vector`, `std::array`, `std::map`, `std:: unordered_map`, etc. + +```c++ +template +bool sevalue_to_native(const se::Value &from, T *to, se::Object *ctx); + +template +bool sevalue_to_native(const se::Value &from, T *to); +``` + +#### Convert C++ Type to se::Value + +```c++ +template +bool nativevalue_to_se(const T &from, se::Value &to, se::Object *ctx); + +template +bool nativevalue_to_se(const T &from, se::Value &to); + +``` + +**The following conversion functions before 3.6 have been deprecated and need to be replaced with the above two groups of functions** + + +```c++ +bool seval_to_int32(const se::Value &v, int32_t *ret); +bool seval_to_uint32(const se::Value &v, uint32_t *ret); +bool seval_to_int8(const se::Value &v, int8_t *ret); +bool seval_to_uint8(const se::Value &v, uint8_t *ret); +bool seval_to_int16(const se::Value &v, int16_t *ret); +bool seval_to_uint16(const se::Value &v, uint16_t *ret); +bool seval_to_boolean(const se::Value &v, bool *ret); +bool seval_to_float(const se::Value &v, float *ret); +bool seval_to_double(const se::Value &v, double *ret); +bool seval_to_size(const se::Value &v, size_t *ret); +bool seval_to_std_string(const se::Value &v, std::string *ret); +bool seval_to_Vec2(const se::Value &v, cc::Vec2 *pt); +bool seval_to_Vec3(const se::Value &v, cc::Vec3 *pt); +bool seval_to_Vec4(const se::Value &v, cc::Vec4 *pt); +bool seval_to_Mat4(const se::Value &v, cc::Mat4 *mat); +bool seval_to_Size(const se::Value &v, cc::Size *size); +bool seval_to_ccvalue(const se::Value &v, cc::Value *ret); +bool seval_to_ccvaluemap(const se::Value &v, cc::ValueMap *ret); +bool seval_to_ccvaluemapintkey(const se::Value &v, cc::ValueMapIntKey *ret); +bool seval_to_ccvaluevector(const se::Value &v, cc::ValueVector *ret); +bool sevals_variadic_to_ccvaluevector(const se::ValueArray &args, cc::ValueVector *ret); +bool seval_to_std_vector_string(const se::Value &v, std::vector *ret); +bool seval_to_std_vector_int(const se::Value &v, std::vector *ret); +bool seval_to_std_vector_uint16(const se::Value &v, std::vector *ret); +bool seval_to_std_vector_float(const se::Value &v, std::vector *ret); +bool seval_to_std_vector_Vec2(const se::Value &v, std::vector *ret); +bool seval_to_Uint8Array(const se::Value &v, uint8_t *ret); +bool seval_to_uintptr_t(const se::Value &v, uintptr_t *ret); +bool seval_to_std_map_string_string(const se::Value &v, std::map *ret); +bool seval_to_Data(const se::Value &v, cc::Data *ret); +bool seval_to_DownloaderHints(const se::Value &v, cc::network::DownloaderHints *ret); +template +bool seval_to_native_ptr(const se::Value& v, T* ret); +template +typename std::enable_if::value && !std::is_same::value, T>::type +seval_to_type(const se::Value &v, bool &ok); +template +typename std::enable_if::value, T>::type +seval_to_type(const se::Value &v, bool &ok); +template +typename std::enable_if::value, T>::type +seval_to_type(const se::Value &v, bool &ok); +template +typename std::enable_if::value, T>::type +seval_to_type(const se::Value &v, bool &ok); + +template +typename std::enable_if::value, T>::type +seval_to_type(const se::Value &v, bool &ok); +template +typename std::enable_if::value && std::is_class::type>::value, bool>::type +seval_to_std_vector(const se::Value &v, std::vector *ret); + +template +typename std::enable_if::value, bool>::type +seval_to_std_vector(const se::Value &v, std::vector *ret); +template +bool seval_to_Map_string_key(const se::Value& v, cc::Map* ret) +``` + +Replace with `sevalue_to_native` + +```c++ +bool int8_to_seval(int8_t v, se::Value *ret); +bool uint8_to_seval(uint8_t v, se::Value *ret); +bool int32_to_seval(int32_t v, se::Value *ret); +bool uint32_to_seval(uint32_t v, se::Value *ret); +bool int16_to_seval(uint16_t v, se::Value *ret); +bool uint16_to_seval(uint16_t v, se::Value *ret); +bool boolean_to_seval(bool v, se::Value *ret); +bool float_to_seval(float v, se::Value *ret); +bool double_to_seval(double v, se::Value *ret); +bool long_to_seval(long v, se::Value *ret); +bool ulong_to_seval(unsigned long v, se::Value *ret); +bool longlong_to_seval(long long v, se::Value *ret); +bool uintptr_t_to_seval(uintptr_t v, se::Value *ret); +bool size_to_seval(size_t v, se::Value *ret); +bool std_string_to_seval(const std::string &v, se::Value *ret); +bool Vec2_to_seval(const cc::Vec2 &v, se::Value *ret); +bool Vec3_to_seval(const cc::Vec3 &v, se::Value *ret); +bool Vec4_to_seval(const cc::Vec4 &v, se::Value *ret); +bool Mat4_to_seval(const cc::Mat4 &v, se::Value *ret); +bool Size_to_seval(const cc::Size &v, se::Value *ret); +bool Rect_to_seval(const cc::Rect &v, se::Value *ret); +bool ccvalue_to_seval(const cc::Value &v, se::Value *ret); +bool ccvaluemap_to_seval(const cc::ValueMap &v, se::Value *ret); +bool ccvaluemapintkey_to_seval(const cc::ValueMapIntKey &v, se::Value *ret); +bool ccvaluevector_to_seval(const cc::ValueVector &v, se::Value *ret); +bool std_vector_string_to_seval(const std::vector &v, se::Value *ret); +bool std_vector_int_to_seval(const std::vector &v, se::Value *ret); +bool std_vector_uint16_to_seval(const std::vector &v, se::Value *ret); +bool std_vector_float_to_seval(const std::vector &v, se::Value *ret); +bool std_map_string_string_to_seval(const std::map &v, se::Value *ret); +bool ManifestAsset_to_seval(const cc::extension::ManifestAsset &v, se::Value *ret); +bool Data_to_seval(const cc::Data &v, se::Value *ret); +bool DownloadTask_to_seval(const cc::network::DownloadTask &v, se::Value *ret); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T *v_c, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value && !std::is_pointer::value, bool>::type +native_ptr_to_seval(T &v_ref, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +bool native_ptr_to_rooted_seval( + typename std::enable_if::value, T>::type *v, + se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T *vp, se::Class *cls, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T &v_ref, se::Class *cls, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +bool native_ptr_to_rooted_seval( + typename std::enable_if::value, T>::type *v, + se::Class *cls, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T *vp, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T *vp, se::Class *cls, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +bool std_vector_to_seval(const std::vector &v, se::Value *ret); +template +bool seval_to_reference(const se::Value &v, T **ret); +``` + +Replace with `nativelue_to_se` + +Auxiliary conversion functions are not part of the abstraction layer (`Script Engine Wrapper`), they belong to the Cocos Creator binding layer and are encapsulated to facilitate more convenient conversion in the binding code. +Each conversion function returns the type `bool` indicating whether the conversion was successful or not. Developers need to check the return value after calling these interfaces. + +The specific usage is directly known according to interface names. The first parameter in the interface is input, and the second parameter is the output parameter. The usage is as follows: + +```c++ +se::Value v; +bool ok = nativevalue_to_se(100, v); // The second parameter is the output parameter, passing in the address of the output parameter +``` + +```c++ +int32_t v; +bool ok = sevalue_to_native(args[0], &v); // The second parameter is the output parameter, passing in the address of the output parameter +``` + +More on manual binding can be found in the [Using JSB Manual Binding](jsb-manual-binding.md) documentation. + +## Automatic Binding + +### Configure Module `.ini` Files + +For more specific, please refer to the engine directory `tools/tojs/cocos.ini` file. + +### Understand The Meaning of Each Field in The `.ini` file + +```ini +# Module name +[cocos] + +# The prefix for callback functions and the binding file name. +prefix = engine + +# The namespace of the binding class attaches to. +target_namespace = jsb + +# Automatic binding tools is based on the Android NDK. The android_headers field configures the search path of Android header file. +android_headers = + +# Configure building parameters for Android. +android_flags = -target armv7-none-linux-androideabi -D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS -DANDROID -D__ANDROID_API__=14 -gcc-toolchain %(gcc_toolchain_dir)s --sysroot=%(androidndkdir)s/platforms/android-14/arch-arm -idirafter %(androidndkdir)s/sources/android/support/include -idirafter %(androidndkdir)s/sysroot/usr/include -idirafter %(androidndkdir)s/sysroot/usr/include/arm-linux-androideabi -idirafter %(clangllvmdir)s/lib64/clang/5.0/include -I%(androidndkdir)s/sources/cxx-stl/llvm-libc++/include + +# Configure the search path for clang header file. +clang_headers = + +# Configure building parameters for clang +clang_flags = -nostdinc -x c++ -std=c++17 -fsigned-char -mfloat-abi=soft -U__SSE__ + +# Configure the search path for engine header file +cocos_headers = -I%(cocosdir)s/cocos -I%(cocosdir)s/cocos/platform/android -I%(cocosdir)s/external/sources + +# Configure building parameters for engine +cocos_flags = -DANDROID -DCC_PLATFORM=3 -DCC_PLATFORM_MAC_IOS=1 -DCC_PLATFORM_MAC_OSX=4 -DCC_PLATFORM_WINDOWS=2 -DCC_PLATFORM_ANDROID=3 + +# Configure extra building parameters +extra_arguments = %(android_headers)s %(clang_headers)s %(cxxgenerator_headers)s %(cocos_headers)s %(android_flags)s %(clang_flags)s %(cocos_flags)s %(extra_flags)s + +# Which header files needed to be parsed +headers = %(cocosdir)s/cocos/platform/FileUtils.h %(cocosdir)s/cocos/platform/CanvasRenderingContext2D.h %(cocosdir)s/cocos/platform/Device.h %(cocosdir)s/cocos/platform/SAXParser.h + +# Rename the header file in the generated binding code +replace_headers = + +# Which classes need to be bound, you can use regular expressions, separated by space. +classes = FileUtils$ SAXParser CanvasRenderingContext2D CanvasGradient Device DownloaderHints + +# Which classes need to be extended at the JS level, separated by spaces. +classes_need_extend = + +# Which classes need to bind properties, separated by commas +field = + +# Which classes need to be skipped, separated by commas +skip = FileUtils::[getFileData setFilenameLookupDictionary destroyInstance getFullPathCache getContents listFilesRecursively], + SAXParser::[(?!(init))], + Device::[getDeviceMotionValue], + CanvasRenderingContext2D::[setCanvasBufferUpdatedCallback set_.+ fillText strokeText fillRect measureText], + Data::[takeBuffer getBytes fastSet copy], + Value::[asValueVector asValueMap asIntKeyMap] + +# Which classes need to define getters and setters, separated by commas +getter_setter = CanvasRenderingContext2D::[width//setWidth height//setHeight fillStyle//setFillStyle font//setFont globalCompositeOperation//setGlobalCompositeOperation lineCap//setLineCap lineJoin//setLineJoin lineWidth//setLineWidth strokeStyle//setStrokeStyle textAlign//setTextAlign textBaseline//setTextBaseline] + +# Which functions need to be renamed, separated by commas +rename_functions = FileUtils::[loadFilenameLookupDictionaryFromFile=loadFilenameLookup] + +# Which classes need to be renamed, separated by commas +rename_classes = SAXParser::PlistParser + + +# Which classes do not have parents in JS +classes_have_no_parents = SAXParser + +# Which C++ base classes need to be skipped +base_classes_to_skip = Ref Clonable + +# Which classes are abstract classes which do not have a constructor in JS +abstract_classes = SAXParser Device +``` + +## Remote Debugging and Profile + +By default, remote debugging and Profile are enabled in debug mode. If you need to enable them in release mode, you need to manually modify the macro switches in the file `native/engine/common/CMakeLists.txt` generated after building the native platform. + +```cmake +# ... +if(NOT RES_DIR) + message(FATAL_ERROR "RES_DIR is not set!") +endif() + +include(${RES_DIR}/proj/cfg.cmake) + +if(NOT COCOS_X_PATH) + message(FATAL_ERROR "COCOS_X_PATH is not set!") +endif() +# ... + +``` + +Change to: + +```cmake +if(NOT RES_DIR) + message(FATAL_ERROR "RES_DIR is not set!") +endif() + +include(${RES_DIR}/proj/cfg.cmake) +set(USE_V8_DEBUGGER_FORCE ON) ## 覆盖 USE_V8_DEBUGGER_FORCE 的值 + +if(NOT COCOS_X_PATH) + message(FATAL_ERROR "COCOS_X_PATH is not set!") +endif() +# ... +``` + +And edit file `native/engine/common/Classes/Game.cpp` + +```c++ +#if CC_DEBUG + _debuggerInfo.enabled = true; +#else + _debuggerInfo.enabled = false; +#endif + // Override the value above + _debuggerInfo.enabled = true; +``` + +### Remote Debugging V8 in Chrome + +#### Windows/Mac + +- Compile, run the game (or run directly in the simulator of Creator) + +- Open with Chrome: . (If you are using an older version of Chrome, you need to change the `devtools` at the beginning of the address to `chrome-devtools`) + +>If the port is occupied, the port will auto-increment by +1. If you cannot connect, please check the port number printed in the console when the App starts. + +- Breakpoint debugging: + + ![v8-win32-debug](jsb/v8-win32-debug.jpg) + +- Catch JS Heap: + + ![v8-win32-memory](jsb/v8-win32-memory.jpg) + +- Profile: + + ![v8-win32-profile](jsb/v8-win32-profile.jpg) + +#### Android/iOS + +- Make sure your Android/iOS device is on the same network as your PC or Mac + +- Compile and run your game + +- Open with Chrome: , `xxx.xxx.xxx.xxx` is the IP address of Android/iOS device. (If you are using an older version of Chrome, you need to change the `devtools` at the beginning of the address to `chrome-devtools`) +- The remote debugging interface is the same as debugging Windows. + +>If the port is occupied, the port will auto-increment by +1. If you cannot connect, please check the port number printed in the console when the App starts. + +## Q & A + +### What's The Difference between se::ScriptEngine and ScriptingCore? Why to keep ScriptingCore? + +In Creator v1.7, the abstraction layer was designed as a stand-alone module that had no relation to the engine. The management of the JS engine was moved from the `ScriptingCore` to `se::ScriptEngine` class. `ScriptingCore` was retained in hopes of passing engine events to the abstraction layer, which acts like a adapter. + +ScriptingCore only needs to be used once in AppDelegate.cpp, and all subsequent operations only require `se::ScriptEngine`. + +```c++ +bool AppDelegate::applicationDidFinishLaunching() +{ + ... + ... + director->setAnimationInterval(1.0 / 60); + + // These two lines set the ScriptingCore adapter to the engine for passing engine events, such as Node's onEnter, onExit, Action's update + ScriptingCore* sc = ScriptingCore::getInstance(); + ScriptEngineManager::getInstance()->setScriptEngine(sc); + + se::ScriptEngine* se = se::ScriptEngine::getInstance(); + ... + ... +} +``` + +### What's The Difference between `se::Object::root/unroot` and `se::Object::incRef/decRef`? + +`root`/`unroot` is used to control whether JS objects are controlled by GC, `root` means JS object should not be controlled by GC, `unroot` means it should be controlled by GC. For a `se::Object`, `root` and `unroot` can be called multiple times, `se::Object`'s internal `_rootCount` variables is used to indicate the count of `root` operation. When `unroot` is called and `_rootCount` reach **0**, the JS object associated with `se::Object` is handed over to the GC. Another situation is that if `se::Object` destructor is triggered and `_rootCount` is still greater than 0, it will force the JS object to be controlled by the GC. + +`incRef`/`decRef` is used to control the life cycle of `se::Object` CPP object. As mentioned in the previous section, it is recommended that you use `se::HandleObject` to control the manual creation of unbound objects's life cycle. So, in general, developers do not need to touch `incRef`/`decRef`. + +### The Association and Disassociation of Object's Life Cycle + +Use `se::Object::attachObject` to associate object's life cycle.
+Use `se::Object::dettachObject` to disassociate object's life cycle. + +`objA->attachObject(objB);` is similar as `objA.__ nativeRefs[index] = objB` in JS. Only when `objA` is garbage collected, `objB` will be possible garbage collected.
+`objA->dettachObject(objB);` is similar as `delete objA.__nativeRefs[index];` in JS. After invoking dettachObject, objB's life cycle will not be controlled by objA. + +### Please DO NOT Assign A Subclass of cc::RefCounted on The Stack + +Subclasses of `cc::RefCounted` must be allocated on the heap, via `new`, and then released by `release`. In JS object's finalize callback function, we should use `release` to release. If it is allocated on the stack, the reference count is likely to be 0, and then calling `release` in finalize callback will result `delete` is invoked, which causing the program to crash. So in order to prevent this behavior from happening, developers can identify destructors as `protected` or `private` in the binding classes that inherit from `cc::RefCounted`, ensuring that this problem can be found during compilation. + +Example: + +```c++ +class CC_EX_DLL EventAssetsManagerEx : public EventCustom +{ +public: + ... + ... +private: + virtual ~EventAssetsManagerEx() {} + ... + ... +}; + +EventAssetsManagerEx event(...); // Compilation ERROR +dispatcher->dispatchEvent(&event); + +// Must modify to: + +EventAssetsManagerEx* event = new EventAssetsManagerEx(...); +dispatcher->dispatchEvent(event); +event->release(); +``` + +### How to Observe JS Exception? + +In AppDelegate.cpp, using `se::ScriptEngine::getInstance()->setExceptionCallback(...)` to set the callback of JS exception. + +```c++ +bool AppDelegate::applicationDidFinishLaunching() +{ + ... + ... + se::ScriptEngine* se = se::ScriptEngine::getInstance(); + + se->setExceptionCallback([](const char* location, const char* message, const char* stack){ + // Send exception information to server like Tencent Bugly. + // ... + // ... + }); + + jsb_register_all_modules(); + ... + ... + return true; +} +``` diff --git a/versions/4.0/en/advanced-topics/cmak-learning/code1.png b/versions/4.0/en/advanced-topics/cmak-learning/code1.png new file mode 100644 index 0000000000..45ce7a8e21 Binary files /dev/null and b/versions/4.0/en/advanced-topics/cmak-learning/code1.png differ diff --git a/versions/4.0/en/advanced-topics/cmak-learning/code2.png b/versions/4.0/en/advanced-topics/cmak-learning/code2.png new file mode 100644 index 0000000000..f003e6f321 Binary files /dev/null and b/versions/4.0/en/advanced-topics/cmak-learning/code2.png differ diff --git a/versions/4.0/en/advanced-topics/cmak-learning/folder3.png b/versions/4.0/en/advanced-topics/cmak-learning/folder3.png new file mode 100644 index 0000000000..f6e3a1e0d1 Binary files /dev/null and b/versions/4.0/en/advanced-topics/cmak-learning/folder3.png differ diff --git a/versions/4.0/en/advanced-topics/cmak-learning/folder4.png b/versions/4.0/en/advanced-topics/cmak-learning/folder4.png new file mode 100644 index 0000000000..45d5877074 Binary files /dev/null and b/versions/4.0/en/advanced-topics/cmak-learning/folder4.png differ diff --git a/versions/4.0/en/advanced-topics/cmak-learning/project.png b/versions/4.0/en/advanced-topics/cmak-learning/project.png new file mode 100644 index 0000000000..c0e5a23585 Binary files /dev/null and b/versions/4.0/en/advanced-topics/cmak-learning/project.png differ diff --git a/versions/4.0/en/advanced-topics/cmake-learning.md b/versions/4.0/en/advanced-topics/cmake-learning.md new file mode 100644 index 0000000000..7276b84b72 --- /dev/null +++ b/versions/4.0/en/advanced-topics/cmake-learning.md @@ -0,0 +1,368 @@ +# Introduction to CMake Usage + +CMake is a very powerful build tool that can greatly simplify the software compilation process and improve development efficiency. Cocos Creator uses CMake on various native platforms. Here are some quick advantages of CMake: + +- CMake uses the CMakeLists.txt file to describe the entire project's build process, rather than using script files like other build tools. +- CMake is cross-platform and can run on operating systems such as Windows, Linux, and macOS. +- CMake can automatically generate Makefiles, Visual Studio, and other IDE project files, simplifying the software compilation process. +- CMake can easily manage dependency libraries, organize code into modules, etc. +- CMake supports multiple programming languages, including C, C++, Fortran, Java, Python, etc. + +Although CMake is a very powerful build tool, it also has some drawbacks, such as its complex syntax and the need for a certain learning cost. + +Developers can learn the syntax of CMake and add their modules to perform specific tasks during the build process. For example, they can define their own preprocessor macros or compiler options to execute custom operations during the build. In addition, they can write scripts to modify project files for different configurations on different platforms, and review [Secondary Development](../editor/publish/native-options.md#secondary-development). + +If you want to learn CMake in-depth, you can read other chapters of this tutorial to learn how to install CMake, create CMakeLists.txt files, specify compilers, add source files, add dependency libraries, and build projects, etc. Due to length limitations, this article cannot provide detailed information on how to use FindPackage, set build types, install and test, etc. To master it better, developers need to consult other documents and engage in more practice. + +## Installing CMake + +For the convenience of developers, Cocos Creator integrates the CMake program internally, which will be used in the build process. Therefore, in general, developers do not need to manually install CMake. + +If developers want to use the CMake on their device, they can edit the relevant configuration to do so. + +If developers want to use CMake in the command line, they can go to [the official website](https://cmake.org/download/) to download it. On the Mac platform, they can also use Homebrew to install it by executing the following command: + +```bash +brew install cmake + +``` + +## **Getting Started with CMake** + +The `CMakeLists.txt` file is the core file of CMake, which describes the entire project's build process. Using this file, you can easily manage the project's building and compilation processes. It contains a series of commands and variables used to specify project names, version numbers, source files, dependency libraries, compiler specifications, compilation options, and other parameters. + +Here is a simple example of a CMake hello-world project: + +First, create a file named `CMakeLists.txt`. In this file, add the following content: + +``` +# CMake Version +cmake_minimum_required(VERSION 3.10) + +# Set project name and main language +project(helloworld CXX) + +add_executable(helloworld main.cpp) + +``` + +Then, create a file named `main.cpp` in the root directory of the project and add the following content:: + +``` +#include + +int main() { + std::cout << "Hello, world!" << std::endl; + return 0; +} + +``` + +Finally, create a directory named `build` in the root directory of the project and execute the following command inside it: + +```bash +# Generate the default project files in the build directory. If Visual Studio is installed, it defaults to a Visual Studio project; on Mac, it defaults to a Makefile project. The project file type can be set by specifying -G, for example, -GXcode. +cmake -B build -S . +# Generate executable target +cmake --build build +``` + +After executing these commands, an executable file named `helloworld` will be generated in the `build` directory. Running the file will output "Hello, world!". + +Two commands used here are `project` and `add_executable`. + +`project` is a command in CMake used to specify project name, version number, programming language, and other information. Its syntax is as follows: + +``` +project(project_name [version] [LANGUAGES languages...]) + +``` + +Here, `project_name` specifies the project name, `version` specifies the version number of the project, and `languages` specifies the programming languages used in the project. If `version` or `languages` are not specified, they can be omitted. For example: + +``` +project(MyProject) + +``` + +This command sets the project name as `MyProject`, without specifying the version number and programming language. + +`add_executable` is used to add build rules for executable files, and its syntax is as follows: + +``` +add_executable(executable_name [source1] [source2] ...) + +``` + +Where `executable_name` is used to specify the name of the executable file, and `source1`, `source2`, and other parameters are used to specify the names of source files. For example: + +``` +add_executable(MyProject main.cpp) + +``` + +This command will set the executable file name to `MyProject` and add the `main.cpp` file as a source file to the project. + +### Other commonly used CMake commands + +### **`message`** + +The `message()` command is used to display messages in the CMake console during the build process. It can be used for debugging, testing, and other purposes. Its syntax is as follows: + +``` +message([] "message text" ...) + +``` + +The `mode` parameter specifies the message type, which can be `STATUS`, `WARNING`, `AUTHOR_WARNING`, `SEND_ERROR`, `FATAL_ERROR`, or `DEPRECATION`. The `message text` parameter specifies the message to be displayed. For example: + +``` +message(STATUS "This is a status message.") +message(WARNING "This is a warning message.") +message(AUTHOR_WARNING "This is an author warning message.") +message(SEND_ERROR "This is a send error message.") +message(FATAL_ERROR "This is a fatal error message.") +message(DEPRECATION "This is a deprecation message.") + +``` + +The message type determines the text color and icon displayed in the CMake console. For example, `STATUS` messages are displayed in green, while `WARNING` messages are displayed in yellow with a warning icon. The `FATAL_ERROR` message will stop the build process and display an error message in the console. + +### `set` + +The `set()` command is mainly used to create or modify variables. The command accepts at least two arguments: the variable name and its value. For example, you can use `set(SRC_FILES main.cpp)` to set the value of the variable `SRC_FILES` to `main.cpp`. If you want to set multiple values (such as a list) for a variable, you can add more arguments to the command, like `set(SRC_FILES main.cpp util.cpp)`. To read the value of a variable, you can use the `${}` syntax, like `message(${SRC_FILES})`. + +You can use the `set` command to add elements to a list variable. Specifically, you can use the command `set(SRC_FILES ${SRC_FILES} util.cpp)` to add `util.cpp` to the end of the `SRC_FILES` list. `${SRC_FILES}` represents the current value of the `SRC_FILES` variable. This command can also use other `set` command options, such as `CACHE` and `APPEND`. + +### `list` + +The `list()` command is used for handling list-type variables. It can accept multiple subcommands, such as `APPEND` (adds an element to the end of the list), `INSERT` (inserts an element at a specified position), `REMOVE_ITEM` (removes a specified element), and so on. For example, the `list(APPEND SRC_FILES util.cpp)` command adds `util.cpp` to the end of the `SRC_FILES` list. + +### `add_library` + +The `add_library` command is used to define a library target. It requires at least two arguments: the name of the library and the source file. If you provide only one source file, CMake will create a library built from that file. For example, `add_library(MyLib main.cpp)`. If you have multiple source files, you can put them all in the `add_library()` command, like `add_library(MyLib main.cpp util.cpp)`. + +CMake supports creating static libraries and dynamic libraries. By default, the `add_library()` command creates a static library. If you want to create a dynamic library, you need to add the `SHARED` parameter to the command, like `add_library(MyLib SHARED main.cpp)`. + +If you want to create both static and dynamic libraries, you can list them both, like `add_library(MyLibStatic STATIC main.cpp)` and `add_library(MyLibShared SHARED main.cpp)`. + +A static library is a library linked to the executable file at compile time, while a dynamic library is a library loaded at runtime. Static libraries usually only contain the code needed by the executable file, so they are smaller. Dynamic libraries usually contain more code and data, as they need to be executed at runtime. The advantage of dynamic libraries is that they can be updated without recompiling the executable file. They can also be shared among multiple executable files, saving disk space. + +When using the `add_library()` command, you can specify the name and type (static or dynamic) of the library, as well as the source files and header files to include. For example, the `add_library(MyLib STATIC main.cpp)` command adds the `main.cpp` source file to the static library named `MyLib`. + +### `find_library` + +The command is used to locate library files on the system. You need to provide a variable name (to store the path of the found library) and the name of the library. For example, `find_library(MY_LIB NAMES MyLib)`. In this example, CMake will search for the library named `MyLib` in the system's library paths. If found, the value of the `MY_LIB` variable will be set to the full path of the library. + +You can use the `find_library` command to locate library files on the system. You need to provide a variable name (to store the path of the found library) and the name of the library. + +For example, suppose you want to locate a library named `libexample` that is in the `/usr/local/lib` directory. The absolute path can be converted to a relative path using variables such as `${CMAKE_CURRENT_LIST_DIR}`. You can add the following command to your `CMakeLists.txt` file: + +``` +find_library(EXAMPLE_LIB libexample /usr/local/lib) + +``` + +This command will store the path of the `libexample` library in the `EXAMPLE_LIB` variable. If the library cannot be found, the value of the `EXAMPLE_LIB` variable will be empty. + +When using the `find_library` command, you can specify the name, path, version, and language of the library. For example, the command `find_library(EXAMPLE_LIB NAMES example PATHS /usr/local/lib VERSION 1.0 LANGUAGES CXX)` will locate a library named `example` with version `1.0` and written in the `C++` language, and store its path in the `EXAMPLE_LIB` variable. + +If you want to locate multiple libraries, you can add multiple library names to the command. For example, the command `find_library(LIB1 NAMES lib1 lib1.a PATHS /usr/local/lib)` will locate a library named `lib1` or `lib1.a`, and store its path in the `LIB1` variable. + +Note that when using the `find_library` command, you need to ensure that the name, path, version, and language of the library match your project. Otherwise, your project may not link to the library file correctly. + +### `target_link_libraries` + +The `target_link_libraries()` command is used to link specified libraries to a target. This command requires at least two arguments: the target name and the library name. For example, `target_link_libraries(MyApp MyLib)` will link the `MyLib` library to the `MyApp` target. This means that `MyApp` will use `MyLib` during the build process. + +### `target_include_directories` + +The `target_include_directories()` command is used to add include directories to a specified target. This command requires at least two arguments: the target name and the directory to be added. For example, `target_include_directories(MyApp PRIVATE include/)` will add the `include/` directory to the include directories of the `MyApp` target. This means that during compilation of `MyApp`, the compiler will search for header files in the `include/` directory. + +### `target_compile_options` + +The `target_compile_options()` command is used to set compilation options for a specified target. This command requires at least two arguments: the target name and the compilation option. For example, `target_compile_options(MyApp PRIVATE -Wall)` will add the `-Wall` option to the compilation options of `MyApp`. This means that `MyApp` will enable all warnings during compilation (which is the effect of the `-Wall` option). + +## Common Tasks + +### Adding Source Files + +You can use the following command to add source files: + +``` +add_executable(MyProject main.cpp math/vec3.cpp math/vec4.cpp) + +``` + +In this example, the source files for `MyProject` include **`main.cpp`**, `math/vec3.cpp`, and `math/vec4.cpp`. If there are more source files, simply add them to this list. + +### Adding Dependency Libraries + +You can use the following command to add dependency libraries: + +``` +target_link_libraries(MyProject MyLibrary) + +``` + +In the example below, the `find_library()` command searches for a static library named `libexample` in the `libs` directory and saves its path to the `LIBS` variable. The `target_link_libraries()` command links this library to the `MyProject` target. + +``` +find_library(LIBS libexample libs PATHS ${CMAKE_CURRENT_LIST_DIR}/libs/android/${ANDROID_ABI}) + +add_executable(MyProject main.cpp) +target_link_libraries(MyProject ${LIBS}) + +# add include search path +target_include_directories(MyProject PUBLIC ${CMAKE_CURRENT_LIST_DIR}/libs/include) +``` + +### CMake Variables + +CMake has some built-in variables starting with `CMAKE_` that are useful for interacting with the environment. Using these variables can make your CMakeLists.txt file more concise and easier to maintain. For example, the `CMAKE_CURRENT_LIST_DIR` variable is used to store the path of the directory where the currently processed CMakeLists.txt file is located. An example of using this variable in a CMakeLists.txt file is as follows: + +``` +add_library(MyLibrary STATIC ${CMAKE_CURRENT_LIST_DIR}/src/my_library.cpp) + +``` + +In the above example, we use the `CMAKE_CURRENT_LIST_DIR` variable to specify the path of the source file. Similarly, the `CMAKE_BINARY_DIR` variable is used to store the path of the root directory of the binary files. An example of using this variable in a CMakeLists.txt file is as follows: + +``` +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) + +``` + +Here, the CMAKE_BINARY_DIR variable is used to specify the root directory for executable file output. When compiling the project, the executable file will be output to the ${CMAKE_BINARY_DIR}/bin directory. + +Note the difference between the `${CMAKE_BINARY_DIR}` and `${CMAKE_CURRENT_BINARY_DIR}` variables. `${CMAKE_BINARY_DIR}` refers to the root directory of the binary files, while `${CMAKE_CURRENT_BINARY_DIR}` refers to the binary directory of the currently processed CMakeLists.txt file. + +Additionally, other commonly used variables include but are not limited to: + +- CMAKE_SOURCE_DIR: The directory where the CMakeLists.txt file is located +- CMAKE_CURRENT_SOURCE_DIR: The directory where the currently processed CMakeLists.txt file is located +- CMAKE_BINARY_DIR: The root directory of the binary files +- CMAKE_CURRENT_BINARY_DIR: The binary directory of the currently processed CMakeLists.txt file +- CMAKE_INSTALL_PREFIX: The root directory of the installation path +- CMAKE_MODULE_PATH: The root directory of the CMake modules +- CMAKE_BUILD_TYPE: The build type +- CMAKE_CXX_FLAGS: C++ compiler options + +## Using CMake in Cocos + +CMake is natively supported in Android for compiling C++ code. We use Gradle to configure parameters and call CMake commands to generate, compile, and package C++ code. For other native platforms, we use build plugins to call the corresponding CMake commands to generate project files, such as Visual Studio projects on Windows and Xcode projects on Mac. Subsequent development can be completed through the IDE. + +Due to the nature of CMake, there may be differences due to different development environments and configurations, so it is not recommended to share generated project files. In addition, modifications to the generated project can be easily overwritten by subsequent generated projects. Instead, the CMakeLists.txt file should be included in the project, and CMake should be used in each development environment to generate the corresponding project files. All modifications to the project should be written in CMake instructions in CMakeLists.txt. + +When using CMake and Xcode cocoapods, some issues may arise. The main issue is that the Xcode project files generated by CMake are incompatible with cocoapods integration. This is because cocoapods uses its way of managing Xcode project files, and the generated project files by CMake do not consider this. This can cause problems such as compilation errors, link errors, and overwritten modifications. + +To solve this problem, we can select the "Skip Xcode Project Update" option when building for Mac/iOS platforms. This means that subsequent CMake configuration updates for the engine or project will not be synchronized to the Xcode project. After selecting this option, we can collaborate with CocoaPods and build in a regular Xcode project format. + +### Share Generated Xcode Project Files + +The Xcode project generated by CMake records dependency paths from the following sources: + +- The installation path of Xcode +- The version and installation path of Cocos Creator +- The version and installation path of the project +- The path where the project file is located + +By using the same directory structure on different devices, Xcode can achieve sharing between different devices. + +Another approach is to modify the paths referenced internally in the Xcode project file, which is a more hacky method and will not be discussed in detail here. + +In contrast to Xcode, Android Studio compiles CMakeLists.txt directly as the configuration file instead of generating project files. Therefore, the native library generated by CMake for the Android platform should behave consistently on different devices. In addition, the Gradle plugin of Android Studio automatically handles dependency relationships, so there is no need to manually manage dependency directories like in Xcode. + + +### Directory structure + +When selecting a native platform to build on, the `native\engine` directory will generate a `current build platform name` folder (e.g.: `android`) and a `common` folder, and CMake will generate `CMakeLists.txt` files in each of these two directories when it is first run, with which serve different purposes: + +- In the `current build platform name` folder: `CMakeLists.txt` is mainly used to configure the corresponding build platform. For the Android platform, for example: + + ![folder2](./cmak-learning/folder3.png) + +- In the `common` folder: `CMakeLists.txt` is mainly used to configure the whole project. + + ![folder2](./cmak-learning/folder4.png) + +The syntax of `CMakeLists.txt` is relatively simple, consisting of **commands**, **comments** and **spaces**. The commands are case-insensitive, but the parameters and variables in the commands are case-sensitive. + +### CMakeLists.txt comments + +How to use CMake to compile a project into a dynamic library for other projects? Simply put, enter the compilation information first, and then the CMake command generates the Makefile file needed for compilation according to the configuration in `CMakeLists.txt`. + +Taking the Android platform as an example and see how to configure `CMakeLists.txt` in the project directory `native/engine/android`. + +```CMake +# Require CMake version 3.8 or higher +cmake_minimum_required(VERSION 3.8) + +# Set project name option +option(APP_NAME "Project name" "NewProject") + +# Set project name and enable C++ +project(${APP_NAME} CXX) + +# Set library name +set(CC_LIB_NAME cocos) + +# Set project directory +set(CC_PROJECT_DIR ${CMAKE_CURRENT_LIST_DIR}) + +# Set project source files +set(CC_PROJ_SOURCES) + +# Set common source files +set(CC_COMMON_SOURCES) + +# Set all source files +set(CC_ALL_SOURCES) + +# Include common CMake functions +include(${CC_PROJECT_DIR}/../common/CMakeLists.txt) + +# If you need to add source code, you can modify CC_PROJ_SOURCES here + +# Call Android pre-build steps +cc_android_before_target(${CC_LIB_NAME}) + +# Add library +add_library(${CC_LIB_NAME} SHARED ${CC_ALL_SOURCES}) + +# Call Android post-build steps +cc_android_after_target(${CC_LIB_NAME}) +``` + +The `CMakeLists.txt` file in the project directory `native/engine/common` is configured in the same way but with some more basic configuration. For example: + +```CMake +option(USE_SPINE "Enable Spine" ON) +``` + +The release package directory generated after the build (e.g.: `build/android`) has a `proj/cfg.cmake` file to store some configuration for the current project. Since `CMakeLists.txt` introduces the `cfg.cmake` file, when the configuration in the `cfg.cmake` file is modified, it will be synchronized to `CMakeLists.txt`; if the configuration is the same, it will be overwritten directly, and the one in the `cfg.cmake` file will prevail. + +Starting from 3.6.2, developers can override the options written in `cfg.cmake` through `native/engine/common/localCfg.cmake`, and the file `localCfg.cmake` will be ignored from the GIT. + +```CMake +CMakeLists.txt + +# Introduce cfg.cmake +include(${RES_DIR}/proj/cfg.CMake) +``` + +For example, uncheck **Spine Animation** in the editor's main menu **Project -> Project Settings -> Feature Cropping**. + +![project](./cmak-learning/project.png) + +Then `USE_SPINE` will be set to `OFF` in the re-generated `cfg.make` when building again: + +![code1](./cmak-learning/code1.png) + +Then at compile time, CMake generates a **CMakeCache.txt** file based on the configuration (e.g.: `CMakeLists.txt` and the `cfg.make` configuration file introduced in `CMakeLists.txt`), which contains the **various input parameters that the project needs to rely on** when building. + +![code2](./cmak-learning/code2.png) + +### **Further Learning** + +You can refer to the following resources for further learning: [CMake Official Documentation](https://cmake.org/documentation/) diff --git a/versions/4.0/en/advanced-topics/data-storage.md b/versions/4.0/en/advanced-topics/data-storage.md new file mode 100644 index 0000000000..2f76a5ecc9 --- /dev/null +++ b/versions/4.0/en/advanced-topics/data-storage.md @@ -0,0 +1,78 @@ +# User Data Storage + +We will have a lot of user data to store to the disk, such as music toggle and language preferences. If it's an offline game you will also need to store game progress locally. In Cocos Creator we use `cc.sys.localStorage` API to save and read user data. + +`cc.sys.localStorage` API is quite similar to [Web Storage API](http://devdocs.io/dom/storage). When running on Web platform it will call Web Storage API directly, on native platform it will use sqlite to save and read data on disk. + +There's an example project you can see data storage API work in action: [Tutorial Data Storage](https://github.com/cocos-creator/tutorial-storage). + +## Save data + +`cc.sys.localStorage.setItem(key, value)` + +This method requires 2 parameter: +- `key` is the key you use to index the data. +- `value` is the data itself. + +If we want to store the how much gold player owns, we use `gold` as the key: + +`cc.sys.localStorage.setItem('gold', 100);` + +For complex data as object, we can first serialize it to JSON: + +```js +userData = { + name: 'Tracer', + level: 1, + gold: 100 +}; + +cc.sys.localStorage.setItem('userData', JSON.stringify(userData)); +``` + +## Read data + +`cc.sys.localStorage.getItem(key)` + +`getItem` method only needs one parameter: the key. Continue with above example: + +```js +var userData = JSON.parse(cc.sys.localStorage.getItem('userData')); +``` + +## Clear data + +Data can be cleared from storage when it is no longer needed: + +`cc.sys.localStorage.clear()` + +## Remove key-value pair + +We can remove a key-value data from the storage when we don't need it anymore: + +`cc.sys.localStorage.removeItem(key)` + +## Encryption + +For single player game, apply encryption to user profile data can make it harder to hack your game. You can import any third party encryption library and use it to encrypt your data before using `setItem`. + +Let's take [encryptjs](https://www.npmjs.com/package/encryptjs) as example, put the library file into your project. To save data: + +```js +var encrypt=require('encryptjs'); +var secretkey= 'open_sesame'; // you can specify the key for encryption, beware for web game it's visible on client side! + +var dataString = JSON.stringify(userData); +var encrypted = encrypt.encrypt(dataString,secretkey,256); + +cc.sys.localStorage.setItem('userData', encrypted); +``` + +To load data: + +```js +var cipherText = cc.sys.localStorage.getItem('userData'); +var userData=JSON.parse(encrypt.decrypt(cipherText,secretkey,256)); +``` + +> **Note**: data encryption cannot guarantee the safety of user profile data. If you want to make sure it's not hackable please load and save data with a secured server. diff --git a/versions/4.0/en/advanced-topics/dynamic-atlas.md b/versions/4.0/en/advanced-topics/dynamic-atlas.md new file mode 100644 index 0000000000..e191d74e85 --- /dev/null +++ b/versions/4.0/en/advanced-topics/dynamic-atlas.md @@ -0,0 +1,37 @@ +# Dynamic Atlas + +Reducing the number of DrawCalls is a very direct and effective way to improve game rendering efficiency. One of the most important factors for two DrawCalls to be merged into one is whether both DrawCalls use the same texture. + +Cocos Creator provides **Dynamic Atlas**, which dynamically merges the textures into one large texture at project runtime. When a map is rendered, the Dynamic Atlas system automatically detects if the map has been merged into an atlas (collection of images). If not, the system merges the texture into the atlas if it meets the conditions for dynamic atlas. + +Dynamic atlas selects which textures are merged into a larger image in **rendering order**, which ensures that adjacent DrawCalls are merged into a single DrawCall (aka "batching"). + +## Enable and Disable Dynamic Atlas + +During initialization, Cocos Creator sets different [CLEANUP_IMAGE_CACHE](%__APIDOC__%/en/interface/Macro?id=CLEANUP_IMAGE_CACHE) parameter for different platforms, and when `CLEANUP_IMAGE_CACHE` is disabled, dynamic atlas will be enabled by default.
+Enabling dynamic atlas will take up extra memory, and the size of the memory used varies by platform. It is currently disabled by default on mini games and native platforms, but it is recommended to turn it on if your project still has room in memory. + +**If you want to force dynamic atlas to be enabled**, please add the following code to your code: + +```ts +macro.CLEANUP_IMAGE_CACHE = false; +DynamicAtlasManager.instance.enabled = true; +``` + +> **Note**: write the code above in the outermost part of the project script, not in the `onLoad`/`start` class functions, to ensure that they take effect instantly during the project loading process. Otherwise, it may cause an error if the dynamic atlas is enabled only when part of the texture cache has been released. + +**To forcibly disable dynamic atlas**, control it directly by code: + +```ts +dynamicAtlasManager.enabled = false; +``` + +## Texture Restrictions + +The dynamic atlas system limits the size of the texture that can be merged. By default, only textures with a width and height less than **512** can be entered into the dynamic atlas system. This limit can be modified by the developer as required with the following code. + +```ts +dynamicAtlasManager.maxFrameSize = 512; +``` + +For details, look up `DynamicAtlasManager` in the [API documentation](%__APIDOC__%/en/). diff --git a/versions/4.0/en/advanced-topics/engine-customization.md b/versions/4.0/en/advanced-topics/engine-customization.md new file mode 100644 index 0000000000..4cece5090f --- /dev/null +++ b/versions/4.0/en/advanced-topics/engine-customization.md @@ -0,0 +1,203 @@ +# Engine Customization Workflow + +Cocos Creator has two engine kernels: C++ kernel and TypeScript kernel. The C++ kernel is used for native platforms, while the TypeScript kernel is used for web and mini-game platforms. On top of the engine kernels is the engine framework layer written in TypeScript, which aims to unify the differences between the two kernels and make development more convenient. + +![engine-core](./engine-customization/engine-core-architecture.png) + +## Getting the Engine Source Code + +![download repo js](engine-customization/select-repo.png) + +### 1. Accessing the Cocos Engine Repository + +Cocos is an open-source engine, and you can obtain different versions of the engine source code from the official Cocos Engine repository. The repository links are as follows: + +- cocos/cocos-engine:[GitHub](https://github.com/cocos/cocos4/) + +Open the repository homepage, and you will see the content shown in the left sub-figure of the above image. You can decide which version to use based on your requirements. + +### 2. Selecting a Branch + +If you want to use the latest branch, select the "develop" branch as shown in the middle sub-figure of the image. + +>**Note:** This branch is the latest in-developing branch and may not guarantee stability. + +### Selecting a Tag + +If you want to use a tag (a stable branch) corresponding to a specific version of Cocos Creator, follow the steps shown in the right sub-figure of the image. Switch to the "Tags" tab and select the desired version. For example, if you are using Cocos Creator 3.7.3, select version 3.7.3. + +>**Note:** Different versions of Cocos Creator require corresponding engine branches or tags. Make sure the versions match. + +### 3. Clone/Download + +![download-repo](engine-customization/download-repo.png) + +As shown in the image above, after selecting the appropriate branch or tag, you will be in the corresponding repository (1). + +Click on the "Code" button on the right (2) to open the code-getting panel. + +You can either copy the repository link (3) and clone it to local using Git, or choose **Download ZIP**(4) to download the source code as a ZIP file and extract it in your disk. + +>Don't forget to give a star to the Cocos Engine repository in the top right corner (5). Thank you! + +## Configuring Development Environment + +### 1. Placing the Engine Source Code in a Specified Directory + +Obtain the specific version of cocos-engine source code from the Cocos Engine repository according to your project's needs and place it in a suitable directory. + +- If the custom engine is only used for a specific project, it is recommended to put it in the project directory. +- If multiple projects will share the same custom engine, place it in a suitable shared directory based on your project management approach. + +### 2. Downloading external + +The compilation of the Cocos native engine depends on the libraries in `cocos-engine/native/external/`. These libraries are several hundred megabytes in size and are not included by default in the source code package. You can obtain them in the following three ways: + +**The First Way:** Use git command-line + +```ts + cd cocos-engine/native + git clone https://github.com/cocos/cocos-engine-external external + cd external + git checkout -b branch_name tag +``` + +- branch_name:The new branch name +- tag:The tag name corresponding to the third-party library. You can get it from the `checkout` field in `cocos-engine/native/external-config.json` file. + +> Example: If we want to use version 3.7.1-1, it can be written as `git checkout -b 3.7.3-1 3.7.3-1` + +**The Second Way:** Use gulp + +The following shell will only download the third-party libraries corresponding to the specific tag. + +```ts + cd cocos-engine/native + npm install + gulp init +``` + +> Whenever the used tag changes, you need to download it again. It requires downloading several hundred megabytes of content each time. + +**The Third Way:** Download the ZIP package + +If, for some reason, you cannot download the `external` source code package using `NodeJS` and `git commands`, you can directly download it from the [cocos-engine-external repository](https://github.com/cocos/cocos-engine-external) by following these steps. + +![download-external-zip](engine-customization/download-external-zip.png) + +1. Select the corresponding tag. You can check which one to use from the `checkout` field of `cocos-engine/native/external-config.json`. +2. Click the **Code** button on the right to open the code-getting panel. +3. Click the **Download ZIP** button to download the source code as a ZIP package. +4. Exact the package, rename it to `external`, and place it in the `cocos-engine/native` folder. + +> **Note:** The version of `external` and `cocos-engine` must match, otherwise, it may result in compilation errors. + +### 3. Installing Compilation Dependencies + +The compilation dependencies require `NodeJS`. Make sure you have installed `NodeJS v12.0` or above on your computer. If not, please download and install it from [https://nodejs.org/](https://nodejs.org/). + +After installation, navigate to the root directory of the custom engine and execute the following commands: + +```bash +# Install gulp +npm install -g gulp +# Install the required modules +npm install +``` + +### 4. Modifying the Engine Path + +Use the **Cocos Creator/File -> Preferences -> Engine Manager** tab to set the path to the engine source code. Note that **you need to restart the editor after modifying the engine path**. + +![custom ts engine](engine-customization/custom-engine.png) + +#### Specifying the TypeScript Engine + +By default, the `Use Built-in` option is selected, indicating that the built-in engine of Cocos Creator is being used. To use the custom engine, switch to the `Use Custom` tab and specify the path to the engine source code root directory. + +#### Specifying the Native(C++)Engine + +If you need to customize the engine functionality related to native platforms, you also need to check the `Use Custom` option under `Native Module`. It will automatically recognize the `native` folder under the custom engine's root directory as the path, which cannot be changed. + +#### Modifying the Storage Location + +![custom-engine-store-path](engine-customization/custom-engine-store-path.png) + +By default, modifications to the engine path are saved as global configurations, which will affect all projects using the current version of Cocos Creator. + +Hover your mouse over the "Cocos Engine" text, and a settings button will appear in front of the text. Click on the button to select a storage location. + +#### Relaunching Cocos Creator + +After modifying the engine path, you need to restart Cocos Creator editor for the changes to take effect. You can either close Cocos Creator and open it again or click on the Relaunch button on the panel. + +**Note:** Once the custom engine is in working, the scene editor in Cocos Creator will also use the custom engine for rendering. By default, it uses the TypeScript kernel. If you want the editor to use the native engine for rendering as well, you need to enable the `Use native engine for scene editor` option in the **Preferences -> Laboratory** panel. + +![use-native-engine-for-editor](engine-customization/use-native-engine-for-editor.png) + +## Modifying the TypeScript Engine + +If you only need to customize the engine functionality for web and mini-game platforms or modify the TypeScript modules of the engine (such as the UI system, animation system, etc.), you only need to compile the engine by clicking **Developer -> Compile the Engine** on the menu bar of Cocos Creator. + +![build](engine-customization/build-ts-engine.png) + +This command will generate a bin folder under the engine directory and compile the engine source code there. + +## Modifying the Native (C++) Engine + +If you need to customize the engine functionality related to native platforms, in addition to modifying the TypeScript code, you may also need to modify the C++ code. + +To ensure that the native code can be compiled properly, make sure that the `Use Custom` option in the `Native Module` is selected. + +### Compiling in Cocos Creator + +After modifying the engine, open the **Build** panel, click **Build**, and after the build is complete, click **Make** to Compile. + +### Compiling in an IDE + +After configuring the custom engine path for the first time, you need to click the `Build` button in the **Build** panel to synchronize the relevant configuration to your IDE (such as Xcode, Android Studio, Visual Studio, etc.). + +The engine code used by the IDE project will then reference to the custom engine. + +From now on, each time when you modify the native engine code, you can compile it using IDE directly. + +## Rebuild the Native Engine Simulator + +Cocos Creator provides a feature to preview in a simulator based on the native engine. + +![custom-native-simulator](engine-customization/custom-native-simulator.png) + +### Modified TypeScript Engine + +If you are using custom TypeScript engine, the native simulator will attempt to load the TS engine modules under `custom-engine-path/native/simulator/`. + +The first time you launch the simulator in this situation, you need to click **Developer-> Rebuild Native Simulator Engine** in the top menu bar, otherwise, the TS engine modules will not be loaded successfully. + +### Modified Native(C++) Engine + +If you have selected to use a custom native engine, Cocos Creator will launch the simulator located at `custom-engine-path/native/simulator/` when previewing in simulator. + +To avoid a large installation package, Cocos Creator excludes the native simulator project before publishing. Therefore, you need to follow these steps to configure the environment and rebuild it,otherwise, the simulator won't start. + +### Installing and Configuring CMake + +>Cocos Creator comes with its own CMake and has configured the paths, so you don't need to do additional configuration when building, compiling, and publishing your project. + +The compilation of the native simulator is not part of the build process, so you need to configure the CMake environment yourself. + +Since there are so many differences among different system, platforms, and versions, please refer to the [CMake official website](https://cmake.org/install/) for installing CMake and configuring the system environment variables. + +### Compilation + +After configuring `CMake`, navigate to the `custom-engine-path/native/` folder and execute the following commands. + +```bash +# Install the required modules +npm install +# compile and generate native simulator +gulp gen-simulator +``` + +After the compilation is complete, a simulator project and an executable application will be generated under the `custom-engine-path/native/simulator/` directory. You can then run the **Preview in Simulator**. + +Once the native simulator is successfully compiled and generated, if you only make changes to the TypeScript part, you only need to click **Developer->Rebuild the Native Simulator Engine** to make it in effect. diff --git a/versions/4.0/en/advanced-topics/engine-customization/bin.png b/versions/4.0/en/advanced-topics/engine-customization/bin.png new file mode 100644 index 0000000000..d459f2b6bb Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/bin.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/build-ts-engine.png b/versions/4.0/en/advanced-topics/engine-customization/build-ts-engine.png new file mode 100644 index 0000000000..3877879b96 Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/build-ts-engine.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/build.png b/versions/4.0/en/advanced-topics/engine-customization/build.png new file mode 100644 index 0000000000..0a4673d45a Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/build.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/custom-engine-store-path.png b/versions/4.0/en/advanced-topics/engine-customization/custom-engine-store-path.png new file mode 100644 index 0000000000..65665bef5f Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/custom-engine-store-path.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/custom-engine.png b/versions/4.0/en/advanced-topics/engine-customization/custom-engine.png new file mode 100644 index 0000000000..ae880a4455 Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/custom-engine.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/custom-native-engine.png b/versions/4.0/en/advanced-topics/engine-customization/custom-native-engine.png new file mode 100644 index 0000000000..7ed3f1de28 Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/custom-native-engine.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/custom-native-simulator.png b/versions/4.0/en/advanced-topics/engine-customization/custom-native-simulator.png new file mode 100644 index 0000000000..b9e6adf35a Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/custom-native-simulator.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/custom-ts-engine.png b/versions/4.0/en/advanced-topics/engine-customization/custom-ts-engine.png new file mode 100644 index 0000000000..7ed3f1de28 Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/custom-ts-engine.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/download-external-zip.png b/versions/4.0/en/advanced-topics/engine-customization/download-external-zip.png new file mode 100644 index 0000000000..2f9b226387 Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/download-external-zip.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/download-repo-js.png b/versions/4.0/en/advanced-topics/engine-customization/download-repo-js.png new file mode 100644 index 0000000000..e5338fbfad Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/download-repo-js.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/download-repo.png b/versions/4.0/en/advanced-topics/engine-customization/download-repo.png new file mode 100644 index 0000000000..e7758ca928 Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/download-repo.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/engine-core-architecture.png b/versions/4.0/en/advanced-topics/engine-customization/engine-core-architecture.png new file mode 100644 index 0000000000..74fe96434f Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/engine-core-architecture.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/open-engine.png b/versions/4.0/en/advanced-topics/engine-customization/open-engine.png new file mode 100644 index 0000000000..8f9e2c038b Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/open-engine.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/select-repo.png b/versions/4.0/en/advanced-topics/engine-customization/select-repo.png new file mode 100644 index 0000000000..38559e4130 Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/select-repo.png differ diff --git a/versions/4.0/en/advanced-topics/engine-customization/use-native-engine-for-editor.png b/versions/4.0/en/advanced-topics/engine-customization/use-native-engine-for-editor.png new file mode 100644 index 0000000000..124cc9aa80 Binary files /dev/null and b/versions/4.0/en/advanced-topics/engine-customization/use-native-engine-for-editor.png differ diff --git a/versions/4.0/en/advanced-topics/hot-update-manager.md b/versions/4.0/en/advanced-topics/hot-update-manager.md new file mode 100644 index 0000000000..3c99ef785c --- /dev/null +++ b/versions/4.0/en/advanced-topics/hot-update-manager.md @@ -0,0 +1,243 @@ +# Hot Update AssetsManager + +This document will fully cover the `AssetsManager` module for hot update, includes technical details and usage. As the requirements of the hot update process for developers may be different, and each developer may also face different problems. Developers need to fully understand the details of the hot update mechanism to be able to customize the workflow to meet their needs. + +This document is relatively long, it tries to introduce the hot update mechanism gradually, but will not introduce too much user level code. First, understand how to use the hot update mechanism to update a game, try the [simple tutorial](hot-update.md) example to get started. + +## Design goals and basic principles + +The hot update mechanism essentially downloads the required assets from the server to the client, and can execute new game logic so that new assets can be used by the game. This means two of the most central goals are: to download new assets, overwrite game logic and assets. At the same time, since the hot update mechanism was originally designed in Cocos2d-JS, we considered what kind of hot update mechanism was more suitable for Cocos's JavaScript user base. Finally, we decided to use a mechanic similar to how Web page update its content to update the game content. Let's take a look at the Web content update mechanic: + +1. The server has complete files of all the page content +2. The browser requests a web page to cache its content locally +3. When the browser re-request this page, it will query the version on server version by the last modified time (Last-Modified) or unique identification (Etag). If these two value are different, then download a new file to update the local cache, if not, continue to use the cache. + +The browser cache mechanism is more complex than the above description, but we can use similar basic idea. For game assets, it is possible to keep a copy of complete assets on the asset server. The client compare the version list with the server during updates, and download the different files and replace the cache. For the rest continues to use the client version or the cached file. So here's what we need to update the game: + +1. The server keeps the latest version of all the game assets (the developer can update the server at any time) +2. The client sends request to get the asset different file list compare to server version +3. Download all assets changed in the new version from the server +4. Overwrite the old cache with the new assets and the files in the application package + +This is the whole hot update process. Of course, there are very many specific details we will introduce later. What the take away here is: + +**Cocos default hot update mechanism is not based on the patch update mechanism, the traditional hot update is often generate patches between multiple version of packages. Client need to download patches one by one according to the order of versions. Cocos's hot update mechanism generates a list of differences by updating the differences between the latest remote version and current local version directly.** + +This can naturally support cross-version update, such as the local version is A, remote version is C, then directly update the difference between A and C, do not need to generate A to B and B to C update patches. Therefore, in this design, we can upload new version of the game files separately to the server. + +## Hot update basic workflow + +After understanding the basic design above, take a look at a typical hot update process. Use the manifest description file to describe the asset file list and asset version that is stored locally or remotely. The manifest file definition is described later. The runtime environment assumes that the installer version is updated for the first time after the user has installed the app: + +![asset manager](hot-update/assets-manager.png) + +The figure is divided into three parts, the middle is the hot update process, the left is the process of updating the AssetsManager to send the message to the user, the right is the middle output of each step. The bold words indicates the location of the middle output, such as in memory / temporary folder / cache folder. + +After reading this picture you may have a lot of questions. We will discuss details of the various steps that need to pay attention to or not easy to understand in the first place. + +## Technical details + +### Manifest format + +The manifest file is a json format that we use to compare local and remote asset differences, including master version information, engine version information, asset file lists, and asset information: + +``` +{ +    "packageUrl": The local cache root path of the remote asset +    "remoteVersionUrl": [optional] the path of the remote version of the file used to determine whether the server has a new version of the assets +    "remoteManifestUrl": Path of the remote asset manifest file, including version information and all asset information +    "version": the version of the asset +    "engineVersion": engine version +    "assets": all asset lists +        "key": the relative path of the asset (relative to the asset root) +        "md5": The md5 hash represents the version information of the asset file +        "compressed": [optional] If the value is true, the file is automatically downloaded after being extracted, currently only supports zip compression format +        "size": [optional] The byte size of the file used to quickly get progress information +    "searchPaths": A list of search paths that need to be added to FileUtils +} +``` + +The manifest files can be generated by using the [Version generator script](https://github.com/cocos-creator/tutorial-hot-update/blob/master/version_generator.js) in the hot update example. + +It is important to note that the remote information (including `packageUrl`, `remoteVersionUrl`, `remoteManifestUrl`) is for the remote package. That is, the `manifest` only takes effect when installed or downloaded with a local package. It is possible to also update the remote package url during version update. In addition, `md5` key is only for file identification, it is necessary, use other algorithm or rule to generate the file identification, such as modified date. When client compare `manifest` with the remote version, as long as the md5 information is different, we think this file has changed. + +### The difference between the assets in the project and the published package + +Everyone in the creation of a Cocos Creator project, you can see it catalog under the assets of the catalog, which saved your scenes, scripts, prefab, etc., corresponding to the editor in the assets of the panel. But these engineering assets are not the same as the packaged assets, in the use of building a building to build the original version, we will find the directory to find res and src folder, these two folders are saved really let the game run up Of the game package within the assets. Where src contains all the scripts, res contains all the assets. + +The asset hot update should naturally update the built assets, not the project's assets directory. + +### Package assets, local cache assets and temporary assets + +When the player's game is installed on the user's phone, its game is in the form of `.ipa` (iOS) or `.apk` (Android), which, after installation, can not be modified or added Of any assets within the application package will always exist. So the hot update mechanism, we can only update the local cache to the phone's writable directory (application storage space or SD card specified directory), and through the FileUtils search path mechanism to complete the local cache on the package of assets coverage. At the same time in order to protect the reliability of the update, we will update the process will first put the new version of the assets into a temporary folder, only when the update is completed, will be replaced to the local cache folder. If the midrange interrupt update or update fails, the failed version will not pollute the existing local cache. This step is described in detail in the previous section of the flow chart: + +![](hot-update/am-part2.png) + +In the case of long-term updates, the local cache will always be replaced with the latest version, and the application package only until the user in the application store to update to the new version will be modified. + +### Progress information + +In the previous section of the flow chart, you can see the hot update manager has sent `UPDATE_PROGRESSION` message to the user. In the current version, the progress information received by the user contains: + +1. byte-level progress (percentage) +2. File level progress (percentage) +3. The number of bytes received +4. The total number of bytes +5. The number of files received +6. The total number of documents + +```js +function updateCb (event) { +    switch (event.getEventCode ()) +    { +        case native.EventAssetsManager.UPDATE_PROGRESSION: + log("Byte progression : " + event.getPercent() / 100); + log("File progression : " + event.getPercentByFile() / 100); + log("Total files : " + event.getTotalFiles()); + log("Downloaded files : " + event.getDownloadedFiles()); + log("Total bytes : " + event.getTotalBytes()); + log("Downloaded bytes : " + event.getDownloadedBytes()); +            break; +    } +} +``` + +### Resume broken transfer + +Definitely a developer will ask, if the process of updating the network will be how? The answer is that the hot update manager supports resume broken transfer, and also supports file-level and byte-level breakpoints. + +So what exactly is it done? First of all, we use the manifest file to identify the status of each asset, such as not started, download, download success, in the hot update process, the file download will be identified to the memory of the manifest, when the number of files to download each The progress node (defaults to **10%** of a node) will serialize manifest in memory and save it to a temporary folder. The concrete steps are shown in the flowcharts of the multi-threaded concurrent download asset section: + +![](hot-update/am-part1.png) + +After the interruption, start the hot update process again, it will check whether there is an outstanding update in the temporary folder. After checking the version and the remote match, use the manifest in the temporary folder to continue the update as the Remote manifest. At this point, for the download status is completed, will not re-download, for the downloaded file, will try to send a request to the server (the server needs to support `Accept-Ranges`, or start from scratch). + +### Control concurrency + +Hot update manager provide control to download the number of concurrent API, the use of the following: + +```js +assetsManager.setMaxConcurrentTask(10); +``` + +### Version comparison function + +A very important step in the hot update process is to compare the client and server versions, by default only when the primary version of the server is updated over the client major version. The engine implements a version of the comparison function, support the x.x.x.x four sequence versions of the comparison function (x is a pure number), do not conform to this version number mode will continue to use the string comparison function. + +In addition, we also allow users to use their own version of the contrast: + +```js +// both versionA and versionB are string types. +assetsManager.setVersionCompareHandle(function (versionA, versionB) { + var sub = parseFloat(versionA) - parseFloat(versionB); + // When the return value is greater than 0, versionA > versionB. + // When the return value is equal to 0, versionA = versionB + // When the return value is less then 0, versionA < versionB + return sub; +}); +``` + +### Verify file after downloaded + +During the download process, problems with the downloaded file contents may occur due to network problem or other network libraries. So we provide the user file verification interface, which is called by the Assets Manager after the file is downloaded (in the case of user implementation). If returning `true` indicates that the file is OK, returning `false` indicates that there is a problem with the file. + +```js +assetsManager.setVerifyCallback(function (filePath, asset) { + var md5 = calculateMD5(filePath); + if (md5 === asset.md5) + return true; + else + return false; +}); +``` + +The asset version in manifest is recommended to use md5. You can determine whether the file is correct by calculating the md5 code of the downloaded file in the verify function and comparing it with the md5 of the asset. In addition to md5, the asset object also contains the following properties: + +| Property | Function Explanation | +| :---- | :---- | +| path | The relative path of the server side | +| compressed | Whether it is compressed | +| size | File state | +| downloadState | Download size, includes `UNSTARTED`, `DOWNLOADING`, `SUCCESSED`, `UNMARKED` | + +### Error message handling and download retry + +In the left side of the flowchart, you can see a number of user messages that can be notified through the event listener of the Assets Manager. For details, you can refer to the [example](https://github.com/cocos-creator/tutorial-hot-update/blob/master/assets/hotupdate/HotUpdate.ts). The flowchart identifies the trigger and cause of all error messages, and you can handle them according to your system design. + +The most important thing is that when an exception occurs during the download process, such as a failed download, a failed decompression, or a failed verification, the `UPDATE_FAILED` event will be triggered. And a list of all assets that failed to download will be recorded in the Assets Manager and can be downloaded again as follows: + +```js +assetsManager.downloadFailedAssets(); +``` + +When the interface is called, it will restart the hot update process and only download the assets failed before. The entire process is the same as the normal hot update process. + +### The necessity of restart + +If you want to use hot updated assets, you need to restart the game. + +There are two reasons, the first is that the updated script requires a clean JS environment. The second is the assets configuration that used by `AssetManager` needs to be updated to the latest to load the scene and assets properly. + +1. Refresh of JS script + + Before the hot update, all the scripts in the game have been executed, and all the classes, components, objects have already been stored in JS context. So if you load the script directly after the hot update without restarting the game, the classes and objects of the same name will be overwritten, but the objects created by the old scripts will still exist. Further, as a result of the overwriting, their dynamic state is also reset, causing the two versions of the objects to mix together, which comes with an overhead of memory usage. + +2. Refresh of asset configuration + + Creator's scenes and assets depend on the [Asset Bundle](../asset/bundle.md). That is, the game will not be able to load the new scenes and assets without the Asset Bundle being reloaded and parsed by [Asset Manager](../asset/asset-manager.md). + +Enabling new assets relies on the search path mechanism of the Cocos engine. All files in the Cocos are read by FileUtils, which finds files in the priority order of the search path. So we add the hot update cache directory to the search path and promote it so that the assets in the cache directory are searched first. Here is the code example: + +```js +import {NATIVE} from 'cc/env'; +// ... +if (NATIVE) { + // Create AssetsManager + var assetsManager = new native.AssetsManager(manifestUrl, storagePath); + // The local manifest in the initialized AssetsManager is the manifest in the cache directory + var hotUpdateSearchPaths = assetsManager.getLocalManifest().getSearchPaths(); + // The search path by default + var searchPaths = native.fileUtils.getSearchPaths(); + + // Insert hotUpdateSearchPaths to the beginning of searchPaths array + Array.prototype.unshift.apply(searchPaths, hotUpdateSearchPaths); + + native.fileUtils.setSearchPaths(searchPaths); +} +``` + +Note: that this code must be placed in `main.js` and executed before it require other script, otherwise it will still load the script from the application package. + +## Advanced Topics + +The above sections describe most of the implementation and usage of the Hot Update AssetsManager. But in some special scenarios, you may need some special tricks to avoid the problems caused by hot updates. But in some specific application scenes, you may need some special techniques to avoid problems caused by hot updates. + +### Iterative upgrade + +Hot update is a frequent requirement for game developers, and multiple hot updates may be released during the upgrade from one major version to another. So the following two questions are of more concern to developers: + +1. What happens during a local cache coverage? + + When a local cached version already exists in the user's environment, the Assets Manager compares the cached version and the in-app version, and then uses the newer version as the local version. If the remote version is updated at this time, the Assets Manager uses a temporary folder to download the remote version during the update process. When the remote version is successfully updated, the contents of the temporary folder are copied to the local cache folder. If there are files of the same name in the local cache folder, they are overridden. And the other files are retained, because these files may still be valid, they just have not been modified in this release. Finally delete the temporary folder. + + So, in theory, there's no problem with continuous hot update for minor version. + +2. How to clean the local cache during a game's major release? + + During a package update, there are various ways to thoroughly clean up the local hot update cache, such as recording the current game version number, checking if it matches the saved version in `sys.localStorage`, and performing the following cleanup if they don't match: + + ```js + // The version number previously saved in local Storage, if not, is considered as new version + var previousVersion = parseFloat(sys.localStorage.getItem('currentVersion')); + // game.currentVersion is the current version's number. + if (previousVersion < game.currentVersion) { + // The hot update cache directory. If there are multiple versions, you may need to record in a list and clean all. + native.fileUtils.removeDirectory(storagePath); + } + ``` + +### Update engine + +Upgrading the engine version can have a huge impact on the hot update, and you may have noticed that there is a `src/cocos-js/cc.js` file in the native project, which is compiled by the JS engine and contains some interface encapsulations for the JS engine framework. In different versions of engine, its code will be quite different, and the bottom layer of C++ will also change simultaneously. Once the C++ engine version in the game pack does not match the engine version in the `src/cocos-js/cc.js`, it can cause serious problems and even prevent the game from running. + +It is recommended to publish the major version to app stores as much as possible after updating the engine. If you decide to use hot update, please carefully complete the test of updating the old version to the new version. diff --git a/versions/4.0/en/advanced-topics/hot-update.md b/versions/4.0/en/advanced-topics/hot-update.md new file mode 100644 index 0000000000..c654e4c083 --- /dev/null +++ b/versions/4.0/en/advanced-topics/hot-update.md @@ -0,0 +1,151 @@ +# Assets Hot Update Tutorial + +## Preface + +In current Cocos Creator version, assets hot update workflow has not been fully integrated into the editor. But the engine itself has complete support for hot update system, so with some of the external script and tool the workflow is complete. + +The sample project for this document is available from [GitHub Repo](https://github.com/cocos-creator/tutorial-hot-update/tree/master). + +![Hot update](./hot-update/title.jpg) + +## Usage scenarios and design ideas + +Game developers are very familiar with the pattern that the game has been released in app market but each time user launch the game it will automatically look for updates from the server, to keep the client always updated. Of course, there are some other usage scenarios for hot update, but are not related to what we discuss here. We will mainly discuss how to implement hot update system in Cocos Creator. + +Hot update in Cocos Creator comes mainly from the `AssetsManager` module in the Cocos engine. It has a very important feature: + +**Server and client both keep a full list of the game asset (manifest)**, during hot update process, by comparing server and client asset manifest, the client should know what to download to get the latest content. This can naturally support cross-version updates, such as when client version is A, remote version is C, then we can directly update the assets from A to C. We do not need to generate A to B and B to C update package. Therefore, when we push new asset version to server, we can save new version files discretely on server, and during update the client will download each file needed separately. + +Please be aware that **hot update system is for native games only**, since Web game would always request assets from web server. So `AssetsManager` class exists only in the native namespace, please check runtime environment before implement these API. + +## Manifest file + +For different versions of file-level differences, AssetsManager uses a manifest file for version control. The client and server manifest files each contains an asset file list and version for each file. This allows comparing the version of each file to determine whether a file should be downloaded. + +The manifest file contains the following important information: + +1. The root path of remote assets +2. Remote manifest file url +3. Remote version file url (optionally) +4. Major version number +5. File list: index of file paths, including file version information. We recommend using the file md5 as the version number +6. List of search path. + +The version file can be part of the contents of the manifest file and do not contain a list of files. But the manifest file may be large, each time client need to download the whole file for checking version update. So developer can provide a very small version file for version check only. `AssetsManager` will first check the version number provided by the version file to determine if it is necessary to continue downloading the manifest file and update it. + +## Implement hot update in the Cocos Creator project + +In this tutorial, we will provide a hot update workflow for Cocos Creator project. We have also offered `Downloader` JavaScript interface, so users are free to develop their own hot update solution. + +Before starting to explain in detail, developers can take a look at the directory structure of published native version of any game in Cocos Creator. The engine scripts will be packaged into the `src` directory, and other assets will be exported to the `assets` directory. + +Based on this project structure, the hot update process in this tutorial is simple: + +1. Generate the local manifest file based on the `assets` and `src` directories in the native published directory. +2. Create a component script to be responsible for the hot update logic. +3. After release of the game, if you need to update the version, you need to generate a set of remote asset versions, including the `assets` directory, `src` directory and manifest file. Then deploy these files to your server. +4. When the hot update script detects that the server manifest version does not match local version, the hot update starts + +To show the hot update process, the example project used in the tutorial has a full version with 1.1.0 information saved in the `remote-assets` directory, and the default build of the project generates version 1.0.0. At the beginning of the game it will check whether there is a version of the remote update, if you find a remote version the user is prompted to update. When update is complete, the user re-enter the game and see the version 1.1.0 information. + +![table](./hot-update/table.png) + +> **Note**: the project contains `remote-assets` is for debug mode, the developer must use the debug mode when building the test project, otherwise the release mode jsc file priority will be higher than `remote-assets` in the assets and cause the script to fail. + +### Use the version generator to generate the manifest file + +In the example project provided, [version_generator.js](https://github.com/cocos-creator/tutorial-hot-update/blob/master/version_generator.js) is a script file, which is a Nodejs script for generating the manifest file. Use it as follows: + +```bash +> node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/ +``` + +The following is a description of the parameters: + +- `-v` Specifies the major version number of the manifest file. +- `-u` Specifies the url of the server remote package, which needs to be the same as the remote package url of the manifest file in the original release version, otherwise the update can not be detected. +- `-s` local native published directory relative to the current path. +- `-d` the path of the output manifest file. + +### Hot update component script + +In the example project, the implementation of the hot update component is located at [assets/hotupdate/HotUpdate.ts](https://github.com/cocos-creator/tutorial-hot-update/blob/master/assets/hotupdate/HotUpdate.ts), the developer can refer to this implementation, but also free to modify according to their own needs. + +In addition, the sample project is also equipped with a `Scene/Canvas/update` node for prompting to update and display the progress of the update. + +![component](./hot-update/editor.png) + +### Deploy to remote server + +In order to allow the game to detect remote versions, it is possible to simulate a remote server on the machine, there are a variety of server solutions (such as [SimpleHTTPServer](https://docs.python.org/2/library/simplehttpserver.html) for Python). We will not discuss detail here, developers can use their own preferred way. Once the remote server is up, modify the following places to allow the game to successfully find the remote package: + +1. `assets/project.manifest`: `packageUrl`, `remoteManifestUrl` and `remoteVersionUrl` in the client manifest file of the game +2. `remote-assets/project.manifest`: `packageUrl`, `remoteManifestUrl` and `remoteVersionUrl` in the manifest file of the remote package +3. `remote-assets/version.manifest`: `packageUrl`, `remoteManifestUrl` and `remoteVersionUrl` in the remote package's version file + +### Publish the original version + +After downloading the sample project, use Cocos Creator to open the project directly. Open **Build** panel, build for native platform, choose Windows/Mac as the target to test. + +> **Note**: +> 1. Do not check MD5 Cache when building, otherwise it will cause the hot update to be invalid. +> 2. Make sure to import editor plugin hot-update into the extensions folder (the demo project has imported the plugin). + +The editor plugin automatically adds the search path logic and fix code to `main.js` every time we build a successful native version: + +```js +// Add the following code at the beginning of main.js +(function () { + if (typeof window.jsb === 'object') { + var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths'); + if (hotUpdateSearchPaths) { + var paths = JSON.parse(hotUpdateSearchPaths); + jsb.fileUtils.setSearchPaths(paths); + + var fileList = []; + var storagePath = paths[0] || ''; + var tempPath = storagePath + '_temp/'; + var baseOffset = tempPath.length; + + if (jsb.fileUtils.isDirectoryExist(tempPath) && !jsb.fileUtils.isFileExist(tempPath + 'project.manifest.temp')) { + jsb.fileUtils.listFilesRecursively(tempPath, fileList); + fileList.forEach(srcPath => { + var relativePath = srcPath.substr(baseOffset); + var dstPath = storagePath + relativePath; + + if (srcPath[srcPath.length] == '/') { + jsb.fileUtils.createDirectory(dstPath) + } + else { + if (jsb.fileUtils.isFileExist(dstPath)) { + jsb.fileUtils.removeFile(dstPath) + } + jsb.fileUtils.renameFile(srcPath, dstPath); + } + }) + jsb.fileUtils.removeDirectory(tempPath); + } + } + } +})(); +``` + +> **Note**: `fileUtils` is located in the traditional `jsb` namespace instead of `native`, and currently, the script compile system only supports importing `native` objects via `import` in TypeScript. + +This step must be done because the essence of the hot update is to replace the files in the original game package with a remotely downloaded file. The search path in Cocos engine just meet this demand, it can be used to specify the remote package download url as the default search path, so the game will run the process of downloading a good remote version. In addition, the search path is used in the last update process using `localStorage` (which conforms to the WEB standard [Local Storage API](https://developer.mozilla.org/en/docs/Web/API/Window/localStorage)) to store on the user's machine. The `HotUpdateSearchPaths` key is specified in `HotUpdate.js`, and the name used for the save and read process must match. + +In addition, if encountering this warning during the opening of the project, ignore: `loader for [.manifest] not exists!`. + +### Run the example project + +If everything is alright, run the native version of the sample project. A new version will be detected, suggesting that the update will automatically restart the game after the game, then enter the table scene. + +![update](./hot-update/update.png) + +## Conclusion + +The above is a hot update solution, Cocos Creator in the future version to provide more mature hot update process, directly integrated into the editor. Of course, the underlying Downloader API will also be provided to allow users to freely implement their own hot update scheme and to build a complete visual workflow in the editor through the plug-in mechanism. This tutorial and sample project is for your reference and we encourage developers to customize their own workflows. If you have questions and communication also welcome feedback to [Forum](https://discuss.cocos2d-x.org/c/creator). + +## Next Step + +[AssetsManager Document](hot-update-manager.md) diff --git a/versions/4.0/en/advanced-topics/hot-update/am-part1.png b/versions/4.0/en/advanced-topics/hot-update/am-part1.png new file mode 100644 index 0000000000..bb03431e1f Binary files /dev/null and b/versions/4.0/en/advanced-topics/hot-update/am-part1.png differ diff --git a/versions/4.0/en/advanced-topics/hot-update/am-part2.png b/versions/4.0/en/advanced-topics/hot-update/am-part2.png new file mode 100644 index 0000000000..72b95c4e1d Binary files /dev/null and b/versions/4.0/en/advanced-topics/hot-update/am-part2.png differ diff --git a/versions/4.0/en/advanced-topics/hot-update/assets-manager.png b/versions/4.0/en/advanced-topics/hot-update/assets-manager.png new file mode 100644 index 0000000000..eb9840499b Binary files /dev/null and b/versions/4.0/en/advanced-topics/hot-update/assets-manager.png differ diff --git a/versions/4.0/en/advanced-topics/hot-update/editor.png b/versions/4.0/en/advanced-topics/hot-update/editor.png new file mode 100644 index 0000000000..89a4aa7d8b Binary files /dev/null and b/versions/4.0/en/advanced-topics/hot-update/editor.png differ diff --git a/versions/4.0/en/advanced-topics/hot-update/table.png b/versions/4.0/en/advanced-topics/hot-update/table.png new file mode 100644 index 0000000000..bb0527bc63 Binary files /dev/null and b/versions/4.0/en/advanced-topics/hot-update/table.png differ diff --git a/versions/4.0/en/advanced-topics/hot-update/title.jpg b/versions/4.0/en/advanced-topics/hot-update/title.jpg new file mode 100644 index 0000000000..a86dcc07d4 Binary files /dev/null and b/versions/4.0/en/advanced-topics/hot-update/title.jpg differ diff --git a/versions/4.0/en/advanced-topics/hot-update/update.png b/versions/4.0/en/advanced-topics/hot-update/update.png new file mode 100644 index 0000000000..8b886d5622 Binary files /dev/null and b/versions/4.0/en/advanced-topics/hot-update/update.png differ diff --git a/versions/4.0/en/advanced-topics/http.md b/versions/4.0/en/advanced-topics/http.md new file mode 100644 index 0000000000..838969083d --- /dev/null +++ b/versions/4.0/en/advanced-topics/http.md @@ -0,0 +1,65 @@ +# HTTP Request + +In some cases, it may be necessary to make requests to a server to fetch data. In Cocos Creator, you can use the fetch method, which is part of JavaScript. + +The prototype is defined as follows. + +```ts +declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise; +``` + +## Setting Up a Test Sever + +First, set up a simple HTTP server using a backend language you are familiar with. Non-backend developers can skip this section. In this example, we'll use Golang to set up the server. Here is an example code: + +```go +package main + +import ( +"fmt" +"log" +"net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "http request received") +} + +func main() { + http.HandleFunc("/", handler) + log.Fatal(http.ListenAndServe(":8080", nil)) +} +``` + +The above code will set up a network server listening on port 8080. + +## Making an HTTP Request + +In Cocos Creator, you can use the `fetch` method to make requests to the server. In this example, we'll use the `GET` method to request data from the server and expect the server to respond with text data. Here's an example code: + +```ts +import { _decorator, Component } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('HttpTest') +export class HttpTest extends Component { + start() { + + fetch("http://127.0.0.1:8080").then((response: Response) => { + return response.text() + }).then((value) => { + console.log(value); + }) + } +} +``` + +After running the scene, the following log will be printed: + +```bash +> http request received. +``` + +You can also use `response.json()` to retrieve the response in JSON format. + +You can refer to the [MDN Web Docs](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch) for more detailed information. diff --git a/versions/4.0/en/advanced-topics/index.md b/versions/4.0/en/advanced-topics/index.md new file mode 100644 index 0000000000..200cc82a3d --- /dev/null +++ b/versions/4.0/en/advanced-topics/index.md @@ -0,0 +1,16 @@ +# Advanced Topics + +- [Submit Code to Cocos Engine Repository](../submit-pr/submit-pr.md) +- [User Data Storage](data-storage.md) +- [Custom loading Wasm/Asm files and modules](wasm-asm-load.md) +- [Converting Native Code to Wasm/Asm Files Using Emscripten](wasm-asm-create.md) +- [Engine Customization Workflow](engine-customization.md) +- [Web Preview Customization Workflow](../editor/preview/browser.md) +- [Dynamic Atlas](dynamic-atlas.md) +- [Mangle Engine Internal Properties](mangle-properties.md) +- [Hot Update Tutorial](hot-update.md) +- [AssetManager for Hot Update](hot-update-manager.md) +- [HTTP Request](http.md) +- [WebSocket Introduction](websocket-introduction.md) + - [WebSocket Client](websocket.md) + - [WebSocket Server](websocket-server.md) diff --git a/versions/4.0/en/advanced-topics/java-reflection.md b/versions/4.0/en/advanced-topics/java-reflection.md new file mode 100644 index 0000000000..83b37d556a --- /dev/null +++ b/versions/4.0/en/advanced-topics/java-reflection.md @@ -0,0 +1,253 @@ +# JavaScript and Android Communication with Reflection + +## Calling Java Static Methods from JavaScript + +In an Android native application created with Cocos Creator, we can directly call Java static methods from JavaScript using the Java reflection mechanism. The method is defined as follows. + +```js +import { native } from 'cc'; +var o = native.reflection.callStaticMethod(className, methodName, methodSignature, parameters...) +``` + +- className: the name of the Java class. +- methodName: the name of the static method. +- methodSignature: the signature of the method, to indicate a unique method. +- parameters: the list of parameters. + +Now, let's take the `Test` class under the `com.cocos.game` package as an example. + +```java +// package "com.cocos.game"; + +public class Test { + + public static void hello (String msg) { + System.out.println (msg); + } + + public static int sum (int a, int b) { + return a + b; + } + + public static int sum (int a) { + return a + 2; + } +} +``` + +### className + +The `className` should include the package path. If we want to call a static method in the `Test` class mentioned above, the `className` should be "com/cocos/game/Test". + +> **Note**: Here we use a `/` instead of a `.` in Java package name. + +### methodName + +The `methodName` is simply the name of the method. For example, if we want to call the `sum` method, the `methodName` parameter should be "sum". + +### methodSignature + +Since Java supports method overloading, the method signature is used to specify the parameter types and return type, ensuring a unique method is invoked. + +The format of the method signature is: **(parameter types)return type**. + +Cocos Creator currently supports the following 4 Java type signatures: + +| Java Type | Signature | +| :------ | :----- | +| int | I | +| float | F | +| boolean | Z | +| String | Ljava/lang/String; | + +> **Note**: the signature of `String` type is `Ljava/lang/String;`, don't miss the `;` in the end. + +Here are some examples: + +- `()V` represents a method with no parameters and no return value. +- `(I)V` represents a method with one parameter of type int and no return value. +- `(I)I` represents a method with one parameter of type int and a return value of type int. +- `(IF)Z` represents a method with two parameters, one of type int and one of type float, and a return value of type boolean. +- `(ILjava/lang/String;F)Ljava/lang/String;` represents a method with three parameters, one of type int, one of type String, and one of type float, and a return value of type String. + +### parameters + +The parameters passed should match the method signature. It supports number, boolean, and string types. + +### Examples + +Here are some examples of calling static methods in the Test class: + +```js +if(sys.os == sys.OS.ANDROID && sys.isNative){ + // call hello + native.reflection.callStaticMethod("com/cocos/game/Test", "hello", "(Ljava/lang/String;)V", "this is a message from JavaScript"); + + // call the first sum + var result = native.reflection.callStaticMethod("com/cocos/game/Test", "sum", "(II)I", 3, 7); + log(result); // 10 + + // call the second sum + var result = native.reflection.callStaticMethod("com/cocos/game/Test", "sum", "(I)I", 3); + log(result); // 5 +} +``` + +The `sys.isNative` is used to check if it's running on a native platform, and the `sys.os` is used to determine the current operating system. Since the communication mechanisms vary across different platforms, it is recommended to perform the check before call `native.reflection.callStaticMethod`. + +After running the code, you can see the corresponding output. + +## Calling JavaScript from Java + +In addition to JavaScript calling Java, the engine also provides a mechanism for Java to call JavaScript. + +By using the `CocosJavascriptJavaBridge.evalString` method provided by the engine, you can execute JavaScript code. It's important to note that since JavaScript code in Cocos Engine is executed on the GL thread, we need to use `CocosHelper.runOnGameThread` to ensure that the thread is correct. + +Next, we will add a button to the Alert dialog and execute a piece of JavaScript code in its response function. + +```java +alertDialog.setButton("OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // Must execute in GL thread + CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("cc.log(\"Javascript Java bridge!\")"); + } + }); + } +}); +``` + +Next, let's take a look at how to call JavaScript code in different situations. + +### Calling Global Function + +We can add a new global function in the script using the following code: + +```js +window.callByNative = function(){ + //to do +} +``` + +>`window` is the global object in the Cocos Engine script environment. If you want a variable, function, object, or class to be globally accessible, you need to add it as a property of `window`. You can access it using `window.variableName` or `variableName` directly. + +Then, you can call it like this: + +```java +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("window.callByNative()"); + } +}); +``` + +Or: + +```js +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("callByNative()"); + } +}); +``` + +### Calling Static Function of an Class + +Suppose there is an object in the TypeScript script with the following static function: + +```ts +export class NativeAPI{ + public static callByNative(){ + //to do + } +} +//Register NativeAPI as a global class, otherwise it cannot be called in Objective-C. +window.NativeAPI = NativeAPI; +``` + +Then you can call it like this: + +```java +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("NativeAPI.callByNative()"); + } +}); +``` + +### Calling Singleton Function + +If the script code implements a singleton object that can be globally accessed: + +```ts +export class NativeAPIMgr{ + private static _inst:NativeAPIMgr; + + public static get inst():NativeAPIMgr{ + if(!this._inst){ + this._inst = new NativeAPIMgr(); + } + return this._inst; + } + + public static callByNative(){ + //to do + } +} + +//Register NativeAPIMgr as a global class, otherwise it cannot be called in Objective-C. +window.NativeAPIMgr = NativeAPIMgr; +``` + +You can call it like this: + +```java +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("NativeAPIMgr.inst.callByNative()"); + } +}); +``` + +### Calling with Parameters + +The above mentioned ways of calling JS from Java all support parameter passing. However, the parameters only support the three basic types: `string`, `number`, and `boolean`. + +Taking the global function as an example: + +```js +window.callByNative = function(a:string, b:number, c:bool){ + //to do +} +``` + +You can call it like this: + +```java +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("window.callByNative('test',1,true)"); + } +}); +``` + +## Calling JavaScript from C++ + +If you want to call `evalString` in C++, you can refer to the following approach to ensure that `evalString` is executed in the GL thread of the engine: + +```c++ +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=]() { + se::ScriptEngine::getInstance()->evalString(script.c_str()); +}); +``` + +## Thread Safety + +As you can see in the code above, `CocosHelper.runOnGameThread` and `CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread` are used. This is to ensure that the code is executed in the correct thread. For more details, please refer to the [Thread Safety](./thread-safety.md) documentation. diff --git a/versions/4.0/en/advanced-topics/js-java-bridge.md b/versions/4.0/en/advanced-topics/js-java-bridge.md new file mode 100644 index 0000000000..f652c9ff07 --- /dev/null +++ b/versions/4.0/en/advanced-topics/js-java-bridge.md @@ -0,0 +1,159 @@ +# JavaScript and Java Communication using JsbBridge + +## Background + +In [JavaScript and Android Communication with Reflection](./java-reflection.md), We not only need to strictly declare package names and function signatures but also need to carefully verify the number of parameters to ensure proper operation. The steps involved are quite complex. + +Therefore, we provide an additional method to simplify the communication between the script and the native. This method acts as a channel or bridge, and we named it `JsbBridge`, which stands for a bridge between scripts and the native app through JSB binding. + +> **Note**: Both methods can be used effectively, and developers can choose the one that suits their needs based on their specific requirements. + +## JsbBridge Mechanism + +### JavaScript API + +In the script layer, there are only two interfaces: sendToNative and onNative, defined as follows: + +```js +// JavaScript +export namespace bridge{ + /** + * Send to native with at least one argument. + */ + export function sendToNative(arg0: string, arg1?: string): void; + /** + * Save your own callback controller with a JavaScript function, + * Use 'jsb.bridge.onNative = (arg0: String, arg1: String | null)=>{...}' + * @param args : received from native + */ + export function onNative(arg0: string, arg1?: string | null): void; +} +``` + +As the names suggest, `sendToNative` is used to invoke code in the native layer, while `onNative` is used to respond to calls from the native layer. + +When using these interfaces, please note the following: + +- Since this feature is still in the experimental stage, it only supports the transmission of `string` data. If you need to transmit objects with multiple parameters, consider converting them to `Json` format for transmission and parse them before using. +- `onNative` Only one function is recorded at a time for `onNative`, and setting the property again will override the previously set `onNative` method. +- The `sendToNative` method is a one-way communication and does not have return value. It does not inform JavaScript of the success or failure of the operation. Developers need to handle the operation status themselves. + +### Java API + +In Java, there are also two corresponding interfaces: `sendToScript` and `ICallback.onScript`, defined as follows: + +```JAVA +// JAVA +public class JsbBridge { + public interface ICallback{ + /** + * Applies this callback to the given argument. + * + * @param arg0 as input + * @param arg1 as input + */ + void onScript(String arg0, String arg1); + } + /** Add a callback which you would like to apply + * @param f ICallback, the method which will be actually applied. multiple calls will override + * */ + public static void setCallback(ICallback f); + /** + * Java dispatch Js event, use native c++ code + * @param arg0 input values + */ + public static void sendToScript(String arg0, String arg1); + public static void sendToScript(String arg0); +} +``` + +Among them, `sendToScript` is used to invoke code in the script layer, while `onScript` is used to respond to calls from the script layer. + +We need to implement the `ICallback` interface and use `setCallback` to register and respond to the specific behavior of `onScript`. + +## Basic Usage + +### Calling Java from JavaScript + +Suppose we have implemented Java interface for opening an advertisement. When the player clicks the button to open the advertisement, it should be triggered by the corresponding Java interface through JavaScript. + +We need to first implement an `ICallback` interface to respond to the operation and register it using `JsbBridge.setCallback`. The code is as follows: + +```JAVA +JsbBridge.setCallback(new JsbBridge.ICallback() { + @Override + public void onScript(String arg0, String arg1) { + //TO DO + if(arg0.equals("open_ad")){ + //call openAd method. + } + } +}); +``` + +> In actual projects, the above code is usually called directly or indirectly in the `onCreate` method of `AppActivity.java` to ensure that it can respond to all calls from the script layer. + +In the JavaScript script, we can call it as follows: + +```ts +import { native } from 'cc' +public static onclick(){ + native.bridge.sendToNative('open_ad', defaultAdUrl); +} +``` + +### Calling JavaScript from Java + +Suppose that after our advertisement finishes playing, we need to notify the JavaScript layer. We can do it as follows. + +First, in JavaScript, use `onNative` to respond to the event: + +```ts +native.bridge.onNative = (arg0:string, arg1: string):void=>{ + if(arg0 == 'ad_close'){ + if(arg1 == "finished") { + //ad playback completed. + } + else{ + //ad cancel. + } + } + return; +} +``` + +> In actual projects, you can place the above code in the `onload` function of a script component that needs to be loaded when the program starts to ensure early listening to events from the native layer. + +Then, in Java, call it as follows: + +```JAVA +JsbBridge.sendToScript("ad_close", "finished"); +``` + +Through the above operations, we can notify JavaScript about the playback result of the advertisement. + +## Best Practices + +JsbBridge provides two string-type parameters, `arg0` and `arg1`, to pass information, which can be allocated according to different needs. + +### 1. Both arg0 and arg1 used as parameters + +If the communication requirements are relatively simple and do not require categorization, you can use `arg0` and `arg1` as parameters. + +### 2. arg0 used as command type, arg1 used as a parameter + +If the communication requirements are relatively complex, you can use arg0 as a command type to process different commands, and arg1 can be used as a parameter. + +### 3. arg0 used as a command type, arg1 used as a JSON string + +For particularly complex requirements where simple string-type parameters are not sufficient, you can convert the objects that need to be passed into a string using `JSON.stringify` and pass them through `arg1`. When using them, you can restore them to objects using `JSON.parse` for further processing. + +> Since it involves serialization and deserialization operations of JSON, this usage is not recommended for frequent calls. + +## Thread Safety + +Note that if the related code involves native UI, you need to consider thread safety issues. For more details, please refer to:[Thread Safety](./thread-safety.md)。 + +## Sample: Multiple Event Calls + +Cocos provides the [native-script-bridge](https://github.com/cocos/cocos-example-projects/tree/v3.8/native-script-bridge) example, which developers can download for reference use as needed. diff --git a/versions/4.0/en/advanced-topics/js-oc-bridge.md b/versions/4.0/en/advanced-topics/js-oc-bridge.md new file mode 100644 index 0000000000..84e33916a8 --- /dev/null +++ b/versions/4.0/en/advanced-topics/js-oc-bridge.md @@ -0,0 +1,151 @@ +# JavaScript and Objective-C Communication using JsbBridge + +## Background + +[JavaScript and iOS/macOS Communication with Reflection](./oc-reflection.md), We need to carefully verify the number of parameters to ensure proper operation. The steps involved are quite complex. + +Therefore, we provide an additional method to simplify the communication between the script and the native. This method acts as a channel or bridge, and we named it `JsbBridge`, which stands for a bridge between scripts and the native app through JSB binding. + +> **Note**: Both methods can be used effectively, and developers can choose the one that suits their needs based on their specific requirements. + +## JsbBridge Mechanism + +### JavaScript API + +```js +// JavaScript +export namespace bridge{ + /** + * Send to native with at least one argument. + */ + export function sendToNative(arg0: string, arg1?: string): void; + /** + * Save your own callback controller with a JavaScript function, + * Use 'jsb.bridge.onNative = (arg0: String, arg1: String | null)=>{...}' + * @param args : received from native + */ + export function onNative(arg0: string, arg1?: string | null): void; +} +``` + +As the names suggest, `sendToNative` is used to invoke code in the native layer, while `onNative` is used to respond to calls from the native layer. + +When using these interfaces, please note the following: + +- Since this feature is still in the experimental stage, it only supports the transmission of `string` data. If you need to transmit objects with multiple parameters, consider converting them to `Json` format for transmission and parse them before using. +- `onNative` Only one function is recorded at a time for `onNative`, and setting the property again will override the previously set `onNative` method. +- The `sendToScript` method is a one-way communication and does not have return value. It does not inform JavaScript of the success or failure of the operation. Developers need to handle the operation status themselves. + +### Objective-C API + +In `Objective-C`, there are also two corresponding interfaces: `sendToScript` and `callByScript`, defined as follows: + +```objc +//Objective-c +typedef void (^ICallback)(NSString*, NSString*); + +@interface JsbBridge : NSObject + ++(instancetype)sharedInstance; +-(bool)setCallback:(ICallback)cb; +-(bool)callByScript:(NSString*)arg0 arg1:(NSString*)arg1; +-(void)sendToScript:(NSString*)arg0 arg1:(NSString*)arg1; +-(void)sendToScript:(NSString*)arg0; + +@end +``` + +Among them, `sendToScript` is used to invoke code in the script layer, while `callByScript` is used to respond to calls from the script layer. + +We need to implement the `ICallback` interface and use `setCallback` to register and respond to the specific behavior of `callByScript`. + +## Basic Usage + +### Calling Objective-C from JavaScript + +Suppose we have written an Objective-C interface to open an advertisement, and when the player clicks the button to open the advertisement, it should be triggered by JavaScript calling the corresponding Objective-C interface. + +First, we need to implement an `ICallback` interface to respond to the operation, and then register it with `JsbBridge` using the `setCallback` method. + +Here is the Objective-C code: + +```ObjC +#include "platform/apple/JsbBridge.h" + +static ICallback cb = ^void (NSString* _arg0, NSString* _arg1){ + if([_arg0 isEqual:@"open_ad"]){ + //open Ad + } +}; + +JsbBridge* m = [JsbBridge sharedInstance]; +[m setCallback:cb]; +``` + +In JavaScript, we can call it like this: + +```ts +import { native } from 'cc' +public static onclick(){ + native.bridge.sendToNative('open_ad', defaultAdUrl); +} +``` + +### Calling JavaScript from Objective-C + +Suppose that after our advertisement finishes playing, we need to notify the JavaScript layer. We can do it as follows. + +First, in JavaScript, use `onNative` to respond to the event: + +```ts +native.bridge.onNative = (arg0:string, arg1: string):void=>{ + if(arg0 == 'ad_close'){ + if(arg1 == "finished") { + //ad playback completed. + } + else{ + //ad cancel. + } + } + return; +} +``` + +> In actual projects, you can place the above code in the `onload` function of a script component that needs to be loaded when the program starts to ensure early listening to events from the native layer. + +Then, in Objective-C, call it as follows: + +```ObjC +#include "platform/apple/JsbBridge.h" + +JsbBridge* m = [JsbBridge sharedInstance]; +[m sendToScript:@"ad_close" arg1:@"finished"]; +``` + +Through the above operations, we can notify JavaScript about the playback result of the advertisement. + +## Best Practices + +JsbBridge provides two string-type parameters, `arg0` and `arg1`, to pass information, which can be allocated according to different needs. + +### 1. Both arg0 and arg1 used as parameters + +If the communication requirements are relatively simple and do not require categorization, you can use `arg0` and `arg1` as parameters. + +### 2. arg0 used as command type, arg1 used as a parameter + +If the communication requirements are relatively complex, you can use arg0 as a command type to process different commands, and arg1 can be used as a parameter. + +### 3. arg0 used as a command type, arg1 used as a JSON string + +For particularly complex requirements where simple string-type parameters are not sufficient, you can convert the objects that need to be passed into a string using `JSON.stringify` and pass them through `arg1`. When using them, you can restore them to objects using `JSON.parse` for further processing. + +> Since it involves serialization and deserialization operations of JSON, this usage is not recommended for frequent calls. + +## Thread Safety + +Note that if the related code involves native UI, you need to consider thread safety issues. For more details, please refer to:[Thread Safety](./thread-safety.md)。 + +## Sample: Multiple Event Calls + +Cocos provides the [native-script-bridge](https://github.com/cocos/cocos-example-projects/tree/v3.8/native-script-bridge) example, which developers can download for reference use as needed. diff --git a/versions/4.0/en/advanced-topics/jsb-auto-binding.md b/versions/4.0/en/advanced-topics/jsb-auto-binding.md new file mode 100644 index 0000000000..a779bd0ca3 --- /dev/null +++ b/versions/4.0/en/advanced-topics/jsb-auto-binding.md @@ -0,0 +1,380 @@ +# Using JSB Auto Binding + +> This article is republished from [Tencent Online Education Department Technical Blog](https://oedx.github.io/2019/07/03/cocos-creator-js-binding-auto/)
+> Author: kevinxzhang + +Creator provides `native.reflection.callStaticMethod` method to support the interface of calling Native side (Android/iOS/Mac) directly from TypeScript side, but after much practice, we found that the performance of this interface is very low under a lot of frequent calls, especially on Android side, such as calling the interface implemented on Native side for printing logs. And it is easy to cause some native crashes, such as `local reference table overflow` and other problems. Throughout the implementation of Cocos native code, basically all interface methods are implemented based on the JSB approach, so this article mainly explains the JSB auto-binding logic to help you quickly implement the `callStaticMethod` to JSB transformation process. + +## Background + +For those who have used Cocos Creator (or CC for convenience), `native.reflection.callStaticMethod` is certainly no stranger to providing the ability to call the Native side from the TypeScript side. For example, if we want to call the Native implementation of the log printing and persistence interface, we can easily do so in JavaScript as follows: + +```javascript +import {NATIVE} from 'cc/env'; + +if (NATIVE && sys.os == sys.OS.IOS) { + msg = this.buffer_string + '\n[cclog][' + clock + '][' + tag + ']' + msg; + native.reflection.callStaticMethod("ABCLogService", "log:module:level:", msg, 'cclog', level); + return; +} else if (NATIVE && sys.os == sys.OS.ANDROID) { + msg = this.buffer_string + '\n[cclog][' + clock + '][' + tag + ']' + msg; + native.reflection.callStaticMethod("com/example/test/CommonUtils", "log", "(ILjava/lang/String;Ljava/lang/String;)V", level, 'cclog', msg); + return; +} +``` + +It is very simple to use and can be called cross-platform in one line of code, a little look at its implementation shows that the C++ layer is implemented in the Android side by way of jni, and the iOS side is called dynamically by way of runtime. However, in order to take into account the generality and support all methods, the Android side does not have a caching mechanism for jni-related objects, which can lead to serious performance problems when a large number of calls are made in a short period of time. Previously we encountered more cases is printing log in the downloader, some application scenarios trigger a large number of download operations in a short period of time, resulting in `local reference table overflow` crashes, or even interface lag on low-end machines that cannot be loaded out. + +Fixing this problem requires a JSB modification for log calls, along with a caching mechanism related to jni to optimize performance. jSB binding is simply the process of converting objects between the C++ and script layers, and forwarding script layer function calls to the C++ layer. + +JSB binding is usually done in two ways: **manual binding** and **automatic binding**. The manual binding method can be found in the [Using JSB Manual Binding](jsb-manual-binding.md) documentation. +- The advantage of manual binding is that it is flexible and customizable; the disadvantage is the necessity to write all the code yourself, especially the conversion between C++ type and TypeScript type, which can easily lead to memory leaks and some pointers or objects not being freed. +- Auto-binding will save a lot of trouble, which directly generates the relevant code through a script in one click, and subsequently, if there are new codes or changes, simply re-execute the script once. Auto-binding is perfect for cases where it is not necessary to do strong customization and then need to finish JSB quickly. The following is a step-by-step explanation of how to implement automatic JSB binding. + +## Environment Configuration and Auto-Binding Demonstration + +### Environment Configuration + +Auto-binding, to put it simply, is a matter of executing a python script to automatically generate the corresponding `.cpp` and `.h` files. First,make sure that the computer has a python runtime environment. Example of how to install it on a Mac: + +1. To install python 3.0, download the installation package from the python website: + + + +2. Install some of the python dependencies via pip3: + + ```shell + sudo pip3 install pyyaml==5.4.1 + sudo pip3 install Cheetah3 + ``` + +3. Install the NDK, which is definitely essential when it comes to C++. It is recommended to install the [Android NDK r21e](https://github.com/android/ndk/wiki/Unsupported-Downloads#r16b) version, set the `PYTH_profile` in `~/.bash_ profile`, then set `PYTHON_ROOT` and `NDK_ROOT` in `~/.bash_profile`, because these two environment variables will be used directly in the python file that will be executed later. + + ```shell + export NDK_ROOT=/Users/kevin/android-ndk-r21e + export PYTHON_BIN=python3 + ``` + +In Windows, directly install the modules by referring to the above, and remember to configure the environment variables at the end. + +### Auto-Binding Demonstration + +Here's a demonstration of how the files under the cocos engine, i.e. the **cocos/bindings/auto** directory (shown below) are automatically generate: + +![](jsb/auto-file.png) + +In fact, the beginning of these file names that there are some specific rules for naming these files, so how are these files generated? First, open a terminal, `cd` to the **tools/tojs** directory, and then run `./genbindings.py`. + +![](jsb/generate-file.png) + +After about a minute or so of running, the following message will appear, indicating that it has been successfully generated: + +![](jsb/generate-file-complete.png) + +After the above steps, all the files under **cocos/bindings/auto** will be automatically generated, which is very convenient. + +The following is an example of how the TypeScript layer prints logs by calling the Native layer log method through JSB, and how to implement the auto-binding tool to generate the corresponding auto-binding files based on the C++ code written. + +## Writing the C++ layer implementation + +C++ is the bridge between TypeScript and Native layers. To implement JSB calls, the first step is to prepare the header files and implementation for the C++ layer, here we create a `test` folder in `cocos` directory to store the relevant files: + +![](jsb/store-file.png) + +`ABCJSBBridge.h`, declares an `abcLog` function for the TypeScript layer to call the logging, and because the logging method will certainly be used in many places in the TypeScript layer, a singleton pattern is used here, providing `getInstance()` to get the current instance of the class. + +```cpp +#pragma once + +#include + +namespace abc +{ + class JSBBridge + { + public: + void abcLog(const std::string& msg); + /** + * Returns a shared instance of the director. + * @js _getInstance + */ + static JSBBridge* getInstance(); + + /** @private */ + JSBBridge(); + /** @private */ + ~JSBBridge(); + bool init(); + }; +} +``` + +The following is the corresponding implementation of `ABCJSBBridge.cpp`: + +```cpp +#include +#include "ABCJSBBridge.h" + +namespace abc +{ + // singleton stuff + static JSBBridge *s_SharedJSBBridge = nullptr; + + JSBBridge::JSBBridge() + { + CC_LOG_ERROR("Construct JSBBridge %p", this); + init(); + } + + JSBBridge::~JSBBridge() + { + CC_LOG_ERROR("Destruct JSBBridge %p", this); + s_SharedJSBBridge = nullptr; + } + + JSBBridge* JSBBridge::getInstance() + { + if (!s_SharedJSBBridge) + { + CC_LOG_ERROR("getInstance JSBBridge "); + s_SharedJSBBridge = new (std::nothrow) JSBBridge(); + CCASSERT(s_SharedJSBBridge, "FATAL: Not enough memory for create JSBBridge"); + } + + return s_SharedJSBBridge; + } + + bool JSBBridge::init(void) + { + CC_LOG_ERROR("init JSBBridge "); + return true; + } + + void JSBBridge::abcLog(const std::string& msg) + { + CC_LOG_ERROR("%s", msg.c_str()); + } +} +``` + +## JSB Configuration Scripting + +Find the `genbindings.py` script in the **tools/tojs** directory, copy it and rename it to `genbindings_test.py`, then modify the `genbindings_test.py` module configuration to keep only the cocos2dx_test module. + +![](jsb/cancel-output_dir.png) + +The next step is to add a custom configuration file `cocos2dx_test.ini` to the **tools/tojs** directory, which is actually similar to the other `.ini` files under **tools/tojs**, mainly to let the auto-binding tool know which APIs to bind and in what way. Refer directly to Cocos' existing `.ini` file to write this, here is the contents of `cocos2dx_test.ini`: + +```ini +[cocos2dx_test] +# the prefix to be added to the generated functions. You might or might not use this in your own +# templates +prefix = cocos2dx_test + +# create a target namespace (in javascript, this would create some code like the equiv. to `ns = ns || {}`) +# all classes will be embedded in that namespace +target_namespace = abc + +macro_judgement = + +android_headers = + +android_flags = -target armv7-none-linux-androideabi -D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS -DANDROID -D__ANDROID_API__=14 -gcc-toolchain %(gcc_toolchain_dir)s --sysroot=%(androidndkdir)s/platforms/android-14/arch-arm -idirafter %(androidndkdir)s/sources/android/support/include -idirafter %(androidndkdir)s/sysroot/usr/include -idirafter %(androidndkdir)s/sysroot/usr/include/arm-linux-androideabi -idirafter %(clangllvmdir)s/lib64/clang/5.0/include -I%(androidndkdir)s/sources/cxx-stl/llvm-libc++/include + +clang_headers = +clang_flags = -nostdinc -x c++ -std=c++17 -fsigned-char -mfloat-abi=soft -U__SSE__ + +cocos_headers = -I%(cocosdir)s/cocos -I%(cocosdir)s/cocos/platform/android -I%(cocosdir)s/external/sources + +cocos_flags = -DANDROID -DCC_PLATFORM=3 -DCC_PLATFORM_MAC_IOS=1 -DCC_PLATFORM_MAC_OSX=4 -DCC_PLATFORM_WINDOWS=2 -DCC_PLATFORM_ANDROID=3 + + +cxxgenerator_headers = + +# extra arguments for clang +extra_arguments = %(android_headers)s %(clang_headers)s %(cxxgenerator_headers)s %(cocos_headers)s %(android_flags)s %(clang_flags)s %(cocos_flags)s %(extra_flags)s + +# what headers to parse +headers = %(cocosdir)s/cocos/test/ABCJSBBridge.h + +# cpp_headers = network/js_network_manual.h + +# what classes to produce code for. You can use regular expressions here. When testing the regular +# expression, it will be enclosed in "^$", like this: "^Menu*$". +classes = JSBBridge + +# what should we skip? in the format ClassName::[function function] +# ClassName is a regular expression, but will be used like this: "^ClassName$" functions are also +# regular expressions, they will not be surrounded by "^$". If you want to skip a whole class, just +# add a single "*" as functions. See bellow for several examples. A special class name is "*", which +# will apply to all class names. This is a convenience wildcard to be able to skip similar named +# functions from all classes. +skip = JSBBridge::[init] + +rename_functions = + +rename_classes = + +# for all class names, should we remove something when registering in the target VM? +remove_prefix = + +# classes for which there will be no "parent" lookup +classes_have_no_parents = JSBBridge + +# base classes which will be skipped when their sub-classes found them. +base_classes_to_skip = Clonable + +# classes that create no constructor +# Set is special and we will use a hand-written constructor +abstract_classes = JSBBridge +``` + +In fact, the annotations inside are also very detailed, and here are a few of the main properties and their meanings: + + +| Property | Description | +|:--- | :--- | +| prefix | Define the name of the function inside the generated binding file. The combination of function name is `js + prefix + the function name in the header file`. For example, if we define `JSBBridge_ abcLog` in the header file and set the `prefix` to `cocos2dx test`, the function name in the final binding file will be `js_cocos2dx_test_JSBBridge_abcLog`. | +| target_namespace | The target namespace in the script, e.g.: `cc`, `spine`, etc. | +| headers | List of headers to be bound, separated by spaces, headers will be scanned recursively. | +| cpp_headers | List of header files that need to be included in the binding code but do not need to be scanned by the binding tool. | +| classes | List of class names to be bound, separated by spaces. | +| abstract_classes | When configured as an abstract class, its constructors will not be bound, separated by spaces. | + +Once the above configuration is done, `cd` to the `tools/tojs` directory and run `./genbindings_test.py` to automatically generate the bindings file. Notice the two bindings under `cocos/bindings/auto`: + +![](jsb/binding-file.png) + +Open the generated `jsb_cocos2dx_test_auto.cpp`: + +```cpp +#include "cocos/bindings/auto/jsb_cocos2dx_test_auto.h" +#include "cocos/bindings/manual/jsb_conversions.h" +#include "cocos/bindings/manual/jsb_global.h" +#include "test/ABCJSBBridge.h" + +#ifndef JSB_ALLOC +#define JSB_ALLOC(kls, ...) new (std::nothrow) kls(__VA_ARGS__) +#endif + +#ifndef JSB_FREE +#define JSB_FREE(ptr) delete ptr +#endif +se::Object* __jsb_abc_JSBBridge_proto = nullptr; +se::Class* __jsb_abc_JSBBridge_class = nullptr; + +static bool js_cocos2dx_test_JSBBridge_abcLog(se::State& s) // NOLINT(readability-identifier-naming) +{ + auto* cobj = SE_THIS_OBJECT(s); + SE_PRECONDITION2(cobj, false, "js_cocos2dx_test_JSBBridge_abcLog : Invalid Native Object"); + const auto& args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 1) { + HolderType arg0 = {}; + ok &= sevalue_to_native(args[0], &arg0, s.thisObject()); + SE_PRECONDITION2(ok, false, "js_cocos2dx_test_JSBBridge_abcLog : Error processing arguments"); + cobj->abcLog(arg0.value()); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 1); + return false; +} +SE_BIND_FUNC(js_cocos2dx_test_JSBBridge_abcLog) + +static bool js_cocos2dx_test_JSBBridge_getInstance(se::State& s) // NOLINT(readability-identifier-naming) +{ + const auto& args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 0) { + abc::JSBBridge* result = abc::JSBBridge::getInstance(); + ok &= nativevalue_to_se(result, s.rval(), nullptr /*ctx*/); + SE_PRECONDITION2(ok, false, "js_cocos2dx_test_JSBBridge_getInstance : Error processing arguments"); + SE_HOLD_RETURN_VALUE(result, s.thisObject(), s.rval()); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 0); + return false; +} +SE_BIND_FUNC(js_cocos2dx_test_JSBBridge_getInstance) + +bool js_register_cocos2dx_test_JSBBridge(se::Object* obj) // NOLINT(readability-identifier-naming) +{ + auto* cls = se::Class::create("JSBBridge", obj, nullptr, nullptr); + + cls->defineFunction("abcLog", _SE(js_cocos2dx_test_JSBBridge_abcLog)); + cls->defineStaticFunction("getInstance", _SE(js_cocos2dx_test_JSBBridge_getInstance)); + cls->install(); + JSBClassType::registerClass(cls); + + __jsb_abc_JSBBridge_proto = cls->getProto(); + __jsb_abc_JSBBridge_class = cls; + + se::ScriptEngine::getInstance()->clearException(); + return true; +} +bool register_all_cocos2dx_test(se::Object* obj) +{ + // Get the ns + se::Value nsVal; + if (!obj->getProperty("abc", &nsVal)) + { + se::HandleObject jsobj(se::Object::createPlainObject()); + nsVal.setObject(jsobj); + obj->setProperty("abc", nsVal); + } + se::Object* ns = nsVal.toObject(); + + js_register_cocos2dx_test_JSBBridge(ns); + return true; +} +``` + +Doesn't it look familiar? It's exactly the same as Cocos' existing `.cpp` files, even including the registration functions and class definitions that are all automatically generated. + +## Cocos Compilation Configuration + +Although we have generated the bindings after the above step, the TypeScript layer can't be used directly because we still need to configure the generated bindings into the `CMakeLists.txt` file to be compiled with other C++ files, which is the last part of the `CMakeLists.txt` compilation configuration. + +1. Open the `CMakeLists.txt` file and add the initial `ABCJSBBridge.h` and `ABCJSBBridge.cpp` to it, as well as the `jsb_cocos2dx_test_auto.h` and `jsb_cocos2dx_test_auto.cpp` files generated by the automatic bindings: + + ![](jsb/111.png) + +2. Open `cocos/bindings/manual/jsb_module_register.cpp` and add the registration code for the cocos2dx_test module: + + ![](jsb/112.png) + + +After the above configuration, call it directly from the TypeScript layer like this: + +``` typescript +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Test') +export class Test extends Component { + start () { + // @ts-ignore + abc.JSBBridge.getInstance().abcLog("JSB binding test success") + } +} +``` + +## Restrictions on Auto-Binding + +Auto-binding relies on the [Bindings Generator tool](https://github.com/cocos-creator/bindings-generator). The Bindings Generator tool can Bind public methods and public properties of C++ classes to the scripting layer. The automatic binding tool, although very powerful, has a few limitations: +1. It can only generate bindings for classes, not structs, independent functions, etc. +2. It is not possible to generate `Delegate` type APIs, because objects in scripts cannot inherit from the `Delegate` class in C++ and override the `Delegate` functions in it. +3. The child class overrides the API of the parent class while overriding this API. +4. Part of the API implementation is not fully reflected in its API definition. +5. The API is actively called by C++ at runtime. + +**NOTICE:** +Since 3.6 the types of parameters and return values involved in auto-binding need to be bound as well, or the conversion methods `sevalue_to_native`/`nativevalue_to_se` need to be provided, otherwise they will be saved at compile time. Before 3.5, it was reported as a runtime error. + +## Summary + +In summary, automatic binding of JSBs only requires the developer to write the relevant C++ implementation class, a configuration file, and then execute a single command to complete the entire binding process. If there is no special customization, it is still a lot more efficient than manual binding. The actual work can be done on a case-by-case basis by first using the auto-binding feature, and then manually modifying the generated binding file to achieve half the result with twice the effort. + diff --git a/versions/4.0/en/advanced-topics/jsb-bridge-wrapper.md b/versions/4.0/en/advanced-topics/jsb-bridge-wrapper.md new file mode 100644 index 0000000000..edf02be9f4 --- /dev/null +++ b/versions/4.0/en/advanced-topics/jsb-bridge-wrapper.md @@ -0,0 +1,191 @@ +# JsbBridgeWrapper - An Event Mechanism based on JsbBridge + +> **Note**: After v3.6.0, `jsb` module is about to be deprecated, APIs will be moved to the `native` module of namespace `cc`. + +## An eventify system based on JsbBridge + +`JsbBridgeWrapper` is an eventify system based on `JsbBridge`, which is more convenient and easier to use. Developers can trigger multiple events without manually implementing one. But it is not mthread safe. If you're working on a complex situation, it is still recommended to implement the eventify system by yourself. + +### JsbBridgeWrapper interface introduction + +```js +/** + * Listener for jsbBridgeWrapper's event. + * It takes one argument as string which is transferred by jsbBridge. + */ +export type OnNativeEventListener = (arg: string) => void; +export namespace jsbBridgeWrapper { + /** If there's no event registered, the wrapper will create one */ + export function addNativeEventListener(eventName: string, listener: OnNativeEventListener); + /** + * Dispatch the event registered on Objective-C, Java etc. + * No return value in JS to tell you if it works. + */ + export function dispatchEventToNative(eventName: string, arg?: string); + /** + * Remove all listeners relative. + */ + export function removeAllListenersForEvent(eventName: string); + /** + * Remove the listener specified + */ + export function removeNativeEventListener(eventName: string, listener: OnNativeEventListener); + /** + * Remove all events, use it carefully! + */ + export function removeAllListeners(); +} +``` + +`OnNativeEventListener` is the type of callback to register here, yet you can also code with anonymous function, for example: + +```js +import { native } from 'cc' +// When event ‘A’ is triggered, the function ‘this.A’ will be applied +native.jsbBridgeWrapper.addNativeEventListener("A", (usr: string) => { + this.A(usr); +}); +``` + +### Platform implementations + +JsbBridgeWrapper has different implementations on different platforms. + +As for Objective-C and Java, you will see `JsbBridgeWrapper` with a similar declaration. + +- [Objective-C](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/platform/apple/JsbBridgeWrapper.h): + + ```objc + // In Objective-C + typedef void (^OnScriptEventListener)(NSString*); + + @interface JsbBridgeWrapper : NSObject + /** + * Get the instance of JsbBridgetWrapper + */ + + (instancetype)sharedInstance; + /** + * Add a listener to specified event, if the event does not exist, the wrapper will create one. Concurrent listener will be ignored + */ + - (void)addScriptEventListener:(NSString*)eventName listener:(OnScriptEventListener)listener; + /** + * Remove listener for specified event, concurrent event will be deleted. Return false only if the event does not exist + */ + - (bool)removeScriptEventListener:(NSString*)eventName listener:(OnScriptEventListener)listener; + /** + * Remove all listener for event specified. + */ + - (void)removeAllListenersForEvent:(NSString*)eventName; + /** + * Remove all event registered. Use it carefully! + */ + - (void)removeAllListeners; + /** + * Dispatch the event with argument, the event should be registered in javascript, or other script language in future. + */ + - (void)dispatchEventToScript:(NSString*)eventName arg:(NSString*)arg; + /** + * Dispatch the event which is registered in javascript, or other script language in future. + */ + - (void)dispatchEventToScript:(NSString*)eventName; + @end + + ``` +- [JAVA](https://github.com/cocos/cocos-engine/blob/v3.5.0/native/cocos/platform/android/java/src/com/cocos/lib/JsbBridgeWrapper.java) or [Huawei HarmonyOS](https://github.com/cocos/cocos-engine/blob/v3.5.0/native/cocos/platform/ohos/libcocos/src/main/java/com/cocos/lib/JsbBridgeWrapper.java): + + ```JAVA + // In JAVA + public class JsbBridgeWrapper { + public interface OnScriptEventListener { + void onScriptEvent(String arg); + } + /** + * Add a listener to specified event, if the event does not exist, the wrapper will create one. Concurrent listener will be ignored + */ + public void addScriptEventListener(String eventName, OnScriptEventListener listener); + /** + * Remove listener for specified event, concurrent event will be deleted. Return false only if the event does not exist + */ + public boolean removeScriptEventListener(String eventName, OnScriptEventListener listener); + /** + * Remove all listener for event specified. + */ + public void removeAllListenersForEvent(String eventName); + /** + * Remove all event registered. Use it carefully! + */ + public void removeAllListeners() { + this.eventMap.clear(); + } + /** + * Dispatch the event with argument, the event should be registered in javascript, or other script language in future. + */ + public void dispatchEventToScript(String eventName, String arg); + /** + * Dispatch the event which is registered in javascript, or other script language in future. + */ + public void dispatchEventToScript(String eventName); + } + ``` + +## Basic usage + +### Register JS event, and using Objective-C/JAVA to trigger JavaScript event + +We can still do with the simple demand: Change the label content with native callback result. once the native event is triggered, it will return the specified text to JavaScript. Before that, we should add an event listener for `changeLabelContent`. + +#### Register JS event + +```js +public changeLabelContent(user: string): void { + console.log("Hello " + user + " I'm K"); + this.labelForContent!.string = "Hello " + user + " ! I'm K"; +} +native.jsbBridgeWrapper.addNativeEventListener("changeLabelContent", (usr: string) => { + this.changeLabelContent(usr); +}); +``` + +#### Dispatch event to JS + +Once `changeLabelContent` event is triggered, label content will change to composition of specified strings. Before that, we should register the native event. + +- Objective-C code sample: + + ```Objc + // Objective-C + JsbBridgeWrapper* m = [JsbBridgeWrapper sharedInstance]; + OnScriptEventListener requestLabelContent = ^void(NSString* arg){ + JsbBridgeWrapper* m = [JsbBridgeWrapper sharedInstance]; + [m dispatchEventToScript:@"changeLabelContent" arg:@"Charlotte"]; + }; + [m addScriptEventListener:@"requestLabelContent" listener:requestLabelContent]; + ``` + +- JAVA code sample: + + ```JAVA + // JAVA + JsbBridgeWrapper jbw = JsbBridgeWrapper.getInstance(); + jbw.addScriptEventListener("requestLabelContent", arg ->{ + System.out.print("@JAVA: here is the argument transport in" + arg); + jbw.dispatchEventToScript("changeLabelContent","Charlotte"); + }); + ``` + + > **Note**: You can also use anonymous function in JAVA too. + +#### Dispatch event to native + +The return value here is set to a const value, when JsbBridgeWrapper in Objective-C/JAVA receives the event notification of `requestLabelContent`, it will trigger `changeLabelContent` with a return value. + +The final step, add a button to start our test. the code example is as follows: + +```js +// Button click event for SAY HELLO +public sayHelloBtn() { + native.jsbBridgeWrapper.dispatchEventToNative("requestLabelContent"); +} +``` + +The effect is same as the test case of JsbBridge, click the `SAY HELLO` button, the label content will become a welcome message. Also, example case of JsbBridge is updated in repository. diff --git a/versions/4.0/en/advanced-topics/jsb-manual-binding.md b/versions/4.0/en/advanced-topics/jsb-manual-binding.md new file mode 100644 index 0000000000..3cd448c45b --- /dev/null +++ b/versions/4.0/en/advanced-topics/jsb-manual-binding.md @@ -0,0 +1,398 @@ +# JSB Manual Binding + +> This article is republished from [Tencent Online Education Department Technical Blog](https://oedx.github.io/2019/05/29/cocos-creator-js-binding-manual/)
+> Author: xepherjin + +## Background + +The overall JS/Native communication call structure in the ABCmouse project has always been based on the `callStaticMethod <-> evalString` approach. The `callStaticMethod` method makes it possible to call `Java/Objective-C` static methods directly in JavaScript through the reflection mechanism. With the `evalString` method, we can execute JS code, so that we can communicate with each other. + + +

New ABCmouse application architecture: communication with evalString based on callStaticMethod

+ +Although it is easier to add new business logic after encapsulating the interface in the upper layer based on this approach. However, over-reliance on evalString often brings some pitfalls. As an example on the Android side. + +```js +CocosJavascriptJavaBridge.evalString("window.sample.testEval('" + param + "',JSON.stringify(" + jsonObj + "))"); +``` + +For common parameter structures, this works fine, however, based on real-world scenarios, we find that controlling the **quotes** is particularly important. As the code shows, in order to ensure that JS code is executed correctly, we must be clear about the use of `'` and `"` when concatenating strings, which can lead to `evalString` failures if we are not careful. We know from a lot of feedback on the official Cocos forums that this is a very easy place to get into trouble. On the other hand, for our project, the uncertainties caused by over-reliance on `evalString` are often difficult to control, and we can't just try/catch` to solve them. Fortunately, after global business troubleshooting, the majority of the project is currently in the project, so after reviewing the official documentation, we decided to bypass `evalString` and communicate directly based on JSB binding. + +Here is an example of downloader access. In our project, the downloader is implemented separately on the Android and iOS sides. In the previous version, the downloader calls and callbacks were based on the `callStaticMethod <-> evalString` approach. + +Each call to download needs to be executed like this. + +```ts +import {NATIVE} from 'cc/env'; + +if(NATVE && sys.os == sys.OS.IOS) { + jsb.reflection.callStaticMethod('ABCFileDownloader', 'downloadFileWithUrl:cookie:savePath:', url, cookies, savePath); +} else if(NATVE && sys.os == sys.OS.ANDROID) { + jsb.reflection.callStaticMethod("com/tencent/abcmouse/downloader/ABCFileDownloader", "downloadFileWithUrl", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", url, cookies, savePath); +} +``` + +A successful or unsuccessful download requires the execution of a JS by splicing together a statement like the following. + +```java +StringBuilder sb = new StringBuilder(JS_STRING_ON_DOWNLOAD_FINISH + "("); +sb.append("'" + success + "',"); +sb.append("'" + url + "',"); +sb.append("'" + savePath + "',"); +sb.append("'" + msg + "',"); +sb.append("'" +code + "')"); +CocosJavascriptJavaBridge.evalString(sb.toString()); +``` + +Whether it is a call or a callback are cumbersome and error-prone splicing, all the data had to be converted into a string ~ ~ (emmmmm also not beautiful) ~ ~, and also take into account the `evalString` implementation efficiency issues. If only a few business scenarios in the use of still barely acceptable, but when the business is increasingly complex and large, if you have to write this, and there is no detailed documentation to regulate constraints, its post-maintenance costs can be imagined. + +When using JSB transformation, we call only a few lines of code and do not need to distinguish between platforms, not to worry about the above-mentioned splicing hidden problems, compared to the logic is much clearer. + +```js +jsb.fileDownloader.requestDownload(url, savePath, cookies, options, (success, url, savePath, msg, code) => { + // do whatever you want +}); +``` + +So the next step is to take the simplest downloader binding process as an example, and I'll take you through the general process of JSB manual binding.
+**(Although Cocos Creator is very user-friendly and provides automatic binding configuration files, you can generate target files directly with some configurations, which reduces a lot of work. However, to complete the manual binding process by hand will help to understand the whole process of binding more comprehensively and help to deepen the understanding. (On the other hand, when there are special needs that cannot be met by automatic binding, manual binding is often more flexible)** + +## Pre-work + +Before we start, we need to know about the ScriptEngine abstraction layer, related APIs and other related knowledge, this part can be skipped if you already know from the Cocos Creator documentation and go straight to the **Practice** part. + +### Abstract layer + +![JSB2.0-Architecture](jsb/JSB2.0-Architecture.png) + +In version 1.7, the abstraction layer was designed as a separate module with no relationship to the engine, and the management of the JS engine was moved from `ScriptingCore` to the `se::ScriptEngine` class, with `ScriptingCore` being retained in the hope that It is hoped that it will act as an adapter by passing some of the engine's events to the wrapper layer. This abstraction layer provides wrappers for a variety of optional JS execution engines such as JavaScriptCore, SpiderMonkey, V8, ChakraCore, etc. Most of the work of JSB is actually setting up C++ callbacks for JS-related operations and associating C++ objects in the callback functions. It actually contains the following two main types. + +- Register JS functions (including global functions, class constructors, class destructors, class member functions, class static member functions) and bind a C++ callback +- Register JS object property read and write accessors, and bind separate read and write C++ callbacks + +Given that the definition of key methods varies across JS engines, the Cocos team uses **macros** to smooth out this difference in callback function definitions and parameter types, which you can read in detail in the Cocos Creator documentation at the end of this article.
+** It's worth mentioning that the ScriptEngine layer was designed by the Cocos team to be a standalone module that doesn't depend on the Cocos engine at all. **We developers can port all the source code of the abstraction layer under **cocos/bindings/jswrapper** and use it directly in other projects. + +### SE Types + +All types of the C++ abstraction layer are under the `se` namespace, which stands for ScriptEngine. + +- **se::ScriptEngine** + + It is the administrator of the JS engine and is in charge of JS engine initialization, destruction, restart, Native module registration, loading scripts, forced garbage collection, JS exception cleanup, and whether to enable the debugger. It is a single instance, and the corresponding instance can be obtained via `se::ScriptEngine::getInstance()`. + +- **se::Value** + + JS variables have six types `object`, `number`, `string`, `boolean`, `null`, `undefined`, so `se::Value` uses union to contain `object`, `number`, `string`, `boolean`, and `null`. string`, `boolean`. The non-valued types: `null`, `undefined` can be directly represented by the private variable `_type`. + + If `se::Value` stores the base data type, such as `number`, `string`, `boolean`, it stores a copy of the value directly internally. The storage of `object` is special in that it is a weak reference to a JS object via `se::Object*`. + +- **se::Object** + + Inherits from the `se::RefCounter` reference counter manager class, which holds a weak reference to a JS object. If we need to use the corresponding `se::Object` of the current object in the binding callback, we just need to get it with `s.thisObject()`. where s is of type `se::State`. + +- **se::Class** + + After a Class type is created, there is no need to manually free memory, it is automatically handled by the wrapper layer. `se::Class` provides some APIs for defining class creation, static/dynamic member functions, property reading and writing, etc., which will be introduced later in the practice. The full contents can be found in the Cocos Creator documentation. + +- **se::State** + + It is an environment in the binding callback where we can get the current C++ pointer, `se::Object` object pointer, parameter list, return value reference through `se::State`. + +### Macro + +As mentioned earlier, the abstraction layer uses macros to smooth out the differences in key function definitions and parameter types across JS engines, so that developers use one function definition regardless of the underlying engine. + +For example, all JS to C++ callback functions in the abstraction layer are defined as + +```cpp +bool foo(se::State& s) +{ + ... + ... +} +SE_BIND_FUNC(foo) // Here is an example of the definition of a callback function +``` + +After we write the callback function, we need to remember to wrap the callback function using the `SE_BIND_XXX` series of macros. The full set of `SE_BIND_XXX` macros is currently shown below. + +- `SE_BIND_PROP_GET`: Wraps a JS object property read callback function +- `SE_BIND_PROP_SET`: wraps a JS object property write callback function +- `SE_BIND_FUNC`: wraps a JS function that can be used as a global function, class member function, or class static function +- `SE_DECLARE_FUNC`: declares a JS function, usually used in the `.h` header file +- `SE_BIND_CTOR`: wraps a JS constructor +- `SE_BIND_SUB_CLS_CTOR`: wraps the constructor of a JS subclass that can inherit +- `SE_BIND_FINALIZE_FUNC`: wraps a JS object callback function after it has been reclaimed by GC +- `SE_DECLARE_FINALIZE_FUNC`: declares a callback function for a JS object after it has been reclaimed by GC +- `_SE`: the name of the wrapper callback function, escaped to the definition of the callback function recognized by each JS engine, note that the first character is an underscore, similar to the _T("xxx") used under Windows to wrap Unicode or MultiBytes strings + +In our simplified example, only `SE_DECLARE_FUNC`, `SE_BIND_FUNC` will be used. + +### Type conversion helper functions + +The type conversion helper functions are located in **cocos/bindings/manual/jsb_conversions.h** and contain methods for interconverting `se::Value` to C++ types. The two main ones are as follows: + +- `bool sevalue_to_native(const se::Value &from, T *to, se::Object * /*ctx*/)`, from `se::Value` to a C++ type +- `bool nativevalue_to_se(const T &from, se::Value& out, se::Object * /*ctx*/)`, from C++ type to `se::Value` + +> The third argument can be passed directly as `nullptr` in most cases, currently only the `function` type has a dependency on `ctx`. + +## Practices + +Before we get started, we need to clarify the process of JSB binding, which is simply the process of implementing some class libraries in the C++ layer, and then calling the corresponding methods on the JS side after some specific processing. Because JS is the main business language, we are limited in what we can do with Native functionality, such as file, network, and other related operations. + +SkeletonRenderer` in the Cocos Creator documentation, for example, if you call the `spine.SkeletonRenderer` constructor with the `new` operator in JSB, you will actually call the `js_spine_SkeletonRenderer _constructor` function. In this C++ function, memory is allocated for the skeleton object, it is added to the auto-recycle pool, and then the JS-level `_ctor` function is called to complete the initialization. The `_ctor` function calls different init functions depending on the type and number of arguments, and these init functions are also C++ function bindings. + +```cpp +#define SE_BIND_CTOR(funcName, cls, finalizeCb) \ + void funcName##Registry(const v8::FunctionCallbackInfo& _v8args) \ + { \ + v8::Isolate* _isolate = _v8args.GetIsolate(); \ + v8::HandleScope _hs(_isolate); \ + bool ret = true; \ + se::ValueArray args; \ + se::internal::jsToSeArgs(_v8args, &args); \ + se::Object* thisObject = se::Object::_createJSObject(cls, _v8args.This()); \ + thisObject->_setFinalizeCallback(_SE(finalizeCb)); \ + se::State state(thisObject, args); \ + ret = funcName(state); \ + if (!ret) { \ + SE_LOGE("[ERROR] Failed to invoke %s, location: %s:%d\n", #funcName, __FILE__, __LINE__); \ + } \ + se::Value _property; \ + bool _found = false; \ + _found = thisObject->getProperty("_ctor", &_property); \ + if (_found) _property.toObject()->call(args, thisObject); \ + } +``` + +The methodological correspondence of the three layers is as follows. + +| Javascript | JSB | Cocos Creator | +| :--------- | :-----| :----------- | +| jsb.SkeletonRenderer.initWithSkeleton | js_spine_SkeletonRenderer_initWithSkeleton | spine::SkeletonRenderer::initWithSkeleton | +| jsb.SkeletonRenderer.initWithUUID | js_spine_SkeletonRenderer_initWithUUID | spine::SkeletonRenderer::initWithUUID | + +The timing of this call process is as follows. + + +

Call timing diagram (quoted from Cocos Creator documentation)

+ +The process is similar to the one above. First, we need to define the interface and the fields, let's draw up the simplest downloader `FileDownloader`, which has the `download(url, path, callback)` interface, and in the `callback` we need to get the `code`, `msg`. And to make it easy to use, we mount it under the `jsb` object, so we can call it simply with the following code: + +```js +jsb.fileDownloader.download(url, path, (msg, code) => { + // do whatever you want +}); +``` + +Once the interface is defined, we can start coding the C++ part. First of all, let's take a look at `FileDownloader.h`, which is a public header file for Android/iOS. Then Android/iOS implement their own specific download implementations (skipped here), and `reqCtx` is used to store the callback correspondence. + +```cpp +class FileDownloader { + public: + typedef std::function ResultCallback; + static FileDownloader* getInstance(); + static void destroyInstance(); + void download(const std::string& url, + const std::string& savePath, + const ResultCallback& callback); + void onDownloadResult(const std::string msg, const int code); + ... ... + protected: + static FileDownloader* s_sharedFileDownloader; + std::unordered_map reqCtx; +}; +``` + +Next we proceed to the most critical part of the binding.
+Since the downloader is functionally classified as a network module, we can choose to implement our `FileDownloader` bindings in the existing `jsb_cocos_network_auto` in the Cocos source code. Declare the JS function in `jsb_cocos_network_auto.h` as follows + +```cpp +SE_DECLARE_FUNC(js_network_FileDownloader_download); // Declare member functions, download calls +SE_DECLARE_FUNC(js_network_FileDownloader_getInstance); // Declare static functions to get a single instance +``` + +Then register `FileDownloader` and the two newly declared functions to the JS virtual machine in `jsb_cocos_network_auto.cpp`. Start by writing the corresponding two method implementations and leave them blank, and then fill in the blanks when the registration logic is complete. + +```cpp +static bool js_network_FileDownloader_download(se::State &s) { // The method name is the same as when it was declared + // TODO +} + +SE_BIND_FUNC(js_network_FileDownloader_download); // Wrapping the method + +static bool js_network_FileDownloader_getInstance(se::State& s) { // The method name is the same as when it was declared + // TODO +} + +SE_BIND_FUNC(js_network_FileDownloader_getInstance); // Wrapping the method +``` + +Now let's start writing the registration logic and add a new registration method to collect all the registration logic for `FileDownloader`. + +```cpp +bool js_register_network_FileDownloader(se::Object* obj) { + auto cls = se::Class::create("FileDownloader", obj, nullptr, nullptr); + cls->defineFunction("download", _SE(js_network_FileDownloader_download)); + cls->defineStaticFunction("getInstance", _SE(js_network_FileDownloader_getInstance)); + cls->install(); + JSBClassType::registerClass(cls); + se::ScriptEngine::getInstance()->clearException(); + return true; +} +``` + +Let's see what important things are done in this method. + +1. Call the `se::Class::create(className, obj, parentProto, ctor)` method to create a Class named `FileDownloader`. After successful registration, an instance can be created in the JS layer by `let xxx = new FileDownloader() After registration, you can create an instance in the JS layer by `let xxx = new FileDownloader() ;`. 2. +2. Call the `defineFunction(name, func)` method to define a member function `download` and bind its implementation to the wrapped `js_network_FileDownloader_download`. +3. call the `defineStaticFunction(name, func)` method, which defines a static member function `getInstance` and binds its implementation to the wrapped `js_network_FileDownloader_getInstance`. +4. call the `install()` method to register itself to the JS virtual machine. +5. Call the `JSBClassType::registerClass` method to map the generated class to a C++-level class (internally implemented via `std::unordered_map`). + +With these steps, we have completed the crucial registration part, but of course don't forget to add the `js_register_network_FileDownloader` call to the registration portal of the `network` module: `js_register_network_FileDownloader`. + +```cpp +bool register_all_cocos_network(se::Object* obj) +{ + // Get the ns + se::Value nsVal; + if (!obj->getProperty("jsb", &nsVal)) + { + se::HandleObject jsobj(se::Object::createPlainObject()); + nsVal.setObject(jsobj); + obj->setProperty("jsb", nsVal); + } + se::Object* ns = nsVal.toObject(); + + ... ... + // Set the Class registration generated earlier to a property of jsb so that we can pass + // let downloader = new jsb.FileDownloader(); + // Get the instance + js_register_network_FileDownloader(ns); + return true; +} +``` + +With this step done, our class has been successfully bound, so now we come back to refine the methods we left blank. + +First is `getInstance()`. + +```cpp +static bool js_network_FileDownloader_getInstance(se::State& s) +{ + const auto& args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 0) { + FileDownloader* result = FileDownloader::getInstance(); // C++ 单例 + ok &= nativevalue_to_se(result, s.rval(), nullptr); + SE_PRECONDITION2(ok, false, "js_network_FileDownloader_getInstance : Error processing arguments"); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 0); + return false; +} +``` + +As mentioned earlier, we can get C++ pointer, `se::Object` object pointer, parameter list, return value reference through `se::State`. The logic is sorted out as follows. + +1. `args()` gets all the arguments (vector of `se::Value`) brought by JS. +2. the number of arguments, because here `getInstance()` does not need additional arguments, so the argument is 0. +3. `native_ptr_to_seval()` is used to get a `se::Value` at the binding level based on a C++ object pointer and assign the return value to `rval()` to the JS level. + +At this point, the binding layer logic of `getInstance()` is all done and we can already get the instance via `let downloader = jsb.FileDownloader.getInstance()`. + +Next is `download()`. + +```cpp +static bool js_network_FileDownloader_download(se::State &s) { + FileDownloader *cobj = (FileDownloader *) s.nativeThisObject(); + SE_PRECONDITION2(cobj, false, + "js_network_FileDownloader_download : Invalid Native Object"); + const auto &args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 3) { + std::string url; + std::string path; + ok &= sevalue_to_native(args[0], &url, nullptr); // Converted to ::string url + ok &= sevalue_to_native(args[1], &path, nullptr); // Converted to ::string path + std::function callback; + do { + if (args[2].isObject() && args[2].toObject()->isFunction()) + { + se::Value jsThis(s.thisObject()); + // Get JS callbacks + se::Value jsFunc(args[2]); + // If the target class is a singleton, it cannot be associated with se::Object::attachObject + // You must use se::Object::root and do not care about unroot. The unroot operation will trigger the destruct of jsFunc with the destruction of the lambda, and the unroot operation will be performed in the destructor of se::Object. + // If s.thisObject->attachObject(jsFunc.toObject); is used, the corresponding func and target will never be freed and a memory leak will occur. + jsFunc.toObject()->root(); + auto lambda = [=](const std::string& msg, + const int code) -> void { + se::ScriptEngine::getInstance()->clearException(); + se::AutoHandleScope hs; + CC_UNUSED bool ok = true; + se::ValueArray args; + args.resize(2); + ok &= nativevalue_to_se(msg, args[0], nullptr); + ok &= nativevalue_to_se(code, args[1], nullptr); + se::Value rval; + se::Object* thisObj = jsThis.isObject() ? jsThis.toObject() : nullptr; + se::Object* funcObj = jsFunc.toObject(); + // Execute JS method callbacks + bool succeed = funcObj->call(args, thisObj, &rval); + if (!succeed) { + se::ScriptEngine::getInstance()->clearException(); + } + }; + callback = lambda; + } + else + { + callback = nullptr; + } + } while(false); + SE_PRECONDITION2(ok, false, "js_network_FileDownloader_download : Error processing arguments"); + cobj->download(url, path, callback); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int) argc, 3); + return false; +} +``` + +1. Get the url, path parameters and the original jsFunc after C++ conversion by `seval_to_std_string` method. +2. Manually construct the callback function to convert msg and code to `se::Value`. 3. +3. execute the JS method for callback via `funcObj->call`. + +Finally, given the risk of memory release, we also need to do the relevant recycling in the `close()` method in `Application.cpp`. + +```cpp +network::FileDownloader::destroyInstance(); +``` + +================================================ + +The above is the whole binding process, after compiling to Android/iOS environment respectively, we will be able to make download calls via `jsb.fileDownloader.download()`.
+(PS: Be sure to remember to perform `NATIVE` macro judgment before use, because the non-JSB environment can not be used) + +```typescript +import {NATIVE} from 'cc/env'; +... +if(NATIVE) { + // JSB Related Logic +} + +``` + +## Summary + +Let's now summarize the detailed process of manual binding transformation. In general, the transformation process for commonly used JSBs is roughly as follows. +- Determine the method interface and JS/Native public fields +- Declare header files and implement Android JNI and OC specific business code respectively +- Write abstraction layer code to register the necessary classes and corresponding methods in the JS virtual machine +- Mount the bound class in a specified object (like a namespace) in JS diff --git a/versions/4.0/en/advanced-topics/jsb-optimizations.md b/versions/4.0/en/advanced-topics/jsb-optimizations.md new file mode 100644 index 0000000000..bd8e71becd --- /dev/null +++ b/versions/4.0/en/advanced-topics/jsb-optimizations.md @@ -0,0 +1,872 @@ +# Optimization of Cross-Language Invocation + +## Introduction + +In the native implementation of Cocos Creator version 3.6.0, we have made improvements in the native (CPP) code. This mainly involves the implementation of the node tree (Scene, Node), assets (Asset and its subclasses), material system (Material, Pass, ProgramLib), 3D renderer (including Model, SubModel), and 2D renderer (Batch2D, RenderEntity) in CPP. And these functionalities are exposed to the JS through binding techniques. + +As known to all, in Cocos Creator 3.x, developers can only use TypeScript (TS) scripts to develop game logic. Although we have implemented more engine code in CPP, developers cannot directly use CPP for game logic development. Therefore, we use the Script Engine Wrapper API (referred to as SE API) to bind and expose the types implemented in the CPP to JS. The interfaces exposed to JS remain consistent with those in the web environment. + +The primary benefit of moving core code to the native is that the execution performance of engine code running on native platforms is improved, especially on platforms that do not support JIT compilation (such as iOS). However, before the official release of version 3.6.0, we also faced a series of side effects resulting from the "elevation of the native level". The main issue was that the number of JSB calls (interactions between JS and CPP languages) increased significantly compared to previous versions. This directly offset the benefits brought by the elevation of the native level and even resulted in poorer performance compared to the previous version(v3.5). This article will introduce some optimization methods to reduce JSB calls. If developers encounter similar issues with excessive JSB calls in their own CPP code, we hope this article can provide some optimization insights. + +## Shared Memory + +For properties in Node that require frequent synchronization, we utilize shared memory between CPP and JS to avoid JSB calls. To facilitate the sharing of CPP memory with JS, we have encapsulated the bindings::NativeMemorySharedToScriptActor helper class. + +bindings/utils/BindingUtils.h + +```c++ +namespace cc::bindings { + +class NativeMemorySharedToScriptActor final { +public: + NativeMemorySharedToScriptActor() = default; + ~NativeMemorySharedToScriptActor(); + + void initialize(void *ptr, uint32_t byteLength); + void destroy(); + + inline se::Object *getSharedArrayBufferObject() const { return _sharedArrayBufferObject; } + +private: + se::Object *_sharedArrayBufferObject{nullptr}; + + CC_DISALLOW_COPY_MOVE_ASSIGN(NativeMemorySharedToScriptActor) +}; + +} // namespace cc::bindings +``` + +bindings/utils/BindingUtils.h + +```c++ +#include "bindings/utils/BindingUtils.h" +#include "bindings/jswrapper/SeApi.h" + +namespace cc::bindings { + +NativeMemorySharedToScriptActor::~NativeMemorySharedToScriptActor() { + destroy(); +} + +void NativeMemorySharedToScriptActor::initialize(void* ptr, uint32_t byteLength) { + CC_ASSERT_NULL(_sharedArrayBufferObject); + // The callback of freeing buffer is empty since the memory is managed in native, + //The external array buffer just holds a reference to the memory. + _sharedArrayBufferObject = se::Object::createExternalArrayBufferObject(ptr, byteLength, [](void* /*contents*/, size_t /*byteLength*/, void* /*userData*/) {}); + // Root this object to prevent it from being garbage collected (GC), we will invoke `unroot` in the destroy function. + _sharedArrayBufferObject->root(); +} + +void NativeMemorySharedToScriptActor::destroy() { + if (_sharedArrayBufferObject != nullptr) { + _sharedArrayBufferObject->unroot(); + _sharedArrayBufferObject->decRef(); + _sharedArrayBufferObject = nullptr; + } +} + +} // namespace cc::bindings +``` + +bindings/jswrapper/v8/Object.h + +```c++ +using BufferContentsFreeFunc = void (*)(void *contents, size_t byteLength, void *userData); +static Object *createExternalArrayBufferObject(void *contents, size_t byteLength, BufferContentsFreeFunc freeFunc, void *freeUserData = nullptr); +``` + +bindings/jswrapper/v8/Object.cpp + +```c++ +/* static */ +Object *Object::createExternalArrayBufferObject(void *contents, size_t byteLength, BufferContentsFreeFunc freeFunc, void *freeUserData /* = nullptr*/) { + Object *obj = nullptr; + std::shared_ptr backingStore = v8::ArrayBuffer::NewBackingStore(contents, byteLength, freeFunc, freeUserData); + v8::Local jsobj = v8::ArrayBuffer::New(__isolate, backingStore); + + if (!jsobj.IsEmpty()) { + obj = Object::_createJSObject(nullptr, jsobj); + } + return obj; +} +``` + +Analysis of the code above reveals that NativeMemorySharedToScriptActor calls the v8::ArrayBuffer::NewBackingStore and v8::ArrayBuffer::New functions to create an External type of ArrayBuffer. It is named "External" because its memory is not allocated and managed internally by V8. Instead, its memory is entirely managed by the code out of V8. When the ArrayBuffer object is garbage collected, the freeFunc callback function is triggered. In Node, the memory that needs to be shared consists of several contiguous properties in the Node. The creation and release of this memory are entirely handled by CPP Node itself, and the destruction of CPP Node instances is controlled by the GC. Therefore, when NativeMemorySharedToScriptActor::initialize internally calls se::Object::createExternalArrayBufferObject, it passes an empty implementation of the callback function. + +Node.h + +```c++ +class Node : public CCObject { + ...... + inline se::Object *_getSharedArrayBufferObject() const { return _sharedMemoryActor.getSharedArrayBufferObject(); } // NOLINT + ...... + bindings::NativeMemorySharedToScriptActor _sharedMemoryActor; + ...... + // Shared memory with JS + // NOTE: TypeArray created in node.jsb.ts _ctor should have the same memory layout + uint32_t _eventMask{0}; // Uint32: 0 + uint32_t _layer{static_cast(Layers::LayerList::DEFAULT)}; // Uint32: 1 + uint32_t _transformFlags{0}; // Uint32: 2 + index_t _siblingIndex{0}; // Int32: 0 + uint8_t _activeInHierarchy{0}; // Uint8: 0 + uint8_t _active{1}; // Uint8: 1 + uint8_t _isStatic{0}; // Uint8: 2 + uint8_t _padding{0}; // Uint8: 3 + ...... +}; +``` + +Node.cpp + +```c++ +Node::Node(const ccstd::string &name) { +#define NODE_SHARED_MEMORY_BYTE_LENGTH (20) + static_assert(offsetof(Node, _padding) + sizeof(_padding) - offsetof(Node, _eventMask) == NODE_SHARED_MEMORY_BYTE_LENGTH, "Wrong shared memory size"); + _sharedMemoryActor.initialize(&_eventMask, NODE_SHARED_MEMORY_BYTE_LENGTH); +#undef NODE_SHARED_MEMORY_BYTE_LENGTH + + _id = idGenerator.getNewId(); + if (name.empty()) { + _name.append("New Node"); + } else { + _name = name; + } +} +``` + +In the Node constructor, `_sharedMemoryActor.initialize(&_eventMask, NODE_SHARED_MEMORY_BYTE_LENGTH);` is called to set the first 20 bytes starting from the `_eventMask` property as shared memory. + +node.jsb.ts + +> **Note**: All files ending with `.jsb.ts` will be replaced with their corresponding versions without the `.jsb` extension during the packaging process. For example, `node.jsb.ts` will replace `node.ts`. You can refer to the `cc.config.json` file in the root directory of the engine for more details. It contains the corresponding `overrides` field, such as `"cocos/scene-graph/node.ts": "cocos/scene-graph/node.jsb.ts"`. + +```ts +// The _ctor callback function in JS is triggered during the final stage of the `var node = new Node();` process in JS, i.e., after the CPP Node object is created. Therefore, the ArrayBuffer returned by the _getSharedArrayBufferObject binding function must exist. +nodeProto._ctor = function (name?: string) { + ...... + // Get the CPP shared ArrayBuffer object through the _getSharedArrayBufferObject binding method + const sharedArrayBuffer = this._getSharedArrayBufferObject(); + // Uint32Array with 3 elements, offset from the start: eventMask, layer, dirtyFlags + this._sharedUint32Arr = new Uint32Array(sharedArrayBuffer, 0, 3); + // Int32Array with 1 element, offset from the 12th byte: siblingIndex + this._sharedInt32Arr = new Int32Array(sharedArrayBuffer, 12, 1); + // Uint8Array with 3 elements, offset from the 16th byte: activeInHierarchy, active, static + this._sharedUint8Arr = new Uint8Array(sharedArrayBuffer, 16, 3); + // + + this._sharedUint32Arr[1] = Layers.Enum.DEFAULT; // this._sharedUint32Arr[1] represents the layer + ...... +}; +``` + +By using shared memory, it also means that we cannot pass the values to be set from JS to CPP through JSB binding functions. Therefore, we need to define corresponding getter/setter functions in the `.jsb.ts` file. These functions will internally modify the shared memory by directly manipulating the TypedArray. + +```ts +Object.defineProperty(nodeProto, 'activeInHierarchy', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[0] != 0; // Uint8, 0: activeInHierarchy + }, + set (v) { + this._sharedUint8Arr[0] = (v ? 1 : 0); // Uint8, 0: activeInHierarchy + }, +}); + +Object.defineProperty(nodeProto, '_activeInHierarchy', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[0] != 0; // Uint8, 0: activeInHierarchy + }, + set (v) { + this._sharedUint8Arr[0] = (v ? 1 : 0); // Uint8, 0: activeInHierarchy + }, +}); + +Object.defineProperty(nodeProto, 'layer', { + configurable: true, + enumerable: true, + get () { + return this._sharedUint32Arr[1]; // Uint32, 1: layer + }, + set (v) { + this._sharedUint32Arr[1] = v; // Uint32, 1: layer + if (this._uiProps && this._uiProps.uiComp) { + this._uiProps.uiComp.setNodeDirty(); + this._uiProps.uiComp.markForUpdateRenderData(); + } + this.emit(NodeEventType.LAYER_CHANGED, v); + }, +}); + +Object.defineProperty(nodeProto, '_layer', { + configurable: true, + enumerable: true, + get () { + return this._sharedUint32Arr[1]; // Uint32, 1: layer + }, + set (v) { + this._sharedUint32Arr[1] = v; // Uint32, 1: layer + }, +}); + +Object.defineProperty(nodeProto, '_eventMask', { + configurable: true, + enumerable: true, + get () { + return this._sharedUint32Arr[0]; // Uint32, 0: eventMask + }, + set (v) { + this._sharedUint32Arr[0] = v; // Uint32, 0: eventMask + }, +}); + +Object.defineProperty(nodeProto, '_siblingIndex', { + configurable: true, + enumerable: true, + get () { + return this._sharedInt32Arr[0]; // Int32, 0: siblingIndex + }, + set (v) { + this._sharedInt32Arr[0] = v; // Int32, 0: siblingIndex + }, +}); + +nodeProto.getSiblingIndex = function getSiblingIndex() { + return this._sharedInt32Arr[0]; // Int32, 0: siblingIndex +}; + +Object.defineProperty(nodeProto, '_transformFlags', { + configurable: true, + enumerable: true, + get () { + return this._sharedUint32Arr[2]; // Uint32, 2: _transformFlags + }, + set (v) { + this._sharedUint32Arr[2] = v; // Uint32, 2: _transformFlags + }, +}); + +Object.defineProperty(nodeProto, '_active', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[1] != 0; // Uint8, 1: active + }, + set (v) { + this._sharedUint8Arr[1] = (v ? 1 : 0); // Uint8, 1: active + }, +}); + +Object.defineProperty(nodeProto, 'active', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[1] != 0; // Uint8, 1: active + }, + set (v) { + this.setActive(!!v); + }, +}); + +Object.defineProperty(nodeProto, '_static', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[2] != 0; + }, + set (v) { + this._sharedUint8Arr[2] = (v ? 1 : 0); + }, +}); +``` + +### Performance Comparison Results + +![jsb/opt-1.jpg](jsb/opt-1.jpg) + +## Avoiding Parameters Passing + +If JSB function calls involve parameters, V8 internally needs to validate the reasonableness of the parameters. These validation tasks can also impact the performance of the calls. For JSB functions that may be frequently called in Node, we avoid passing floating-point parameters by reusing a global Float32Array. + +scene-graph/utils.jsb.ts + +```ts +import { IMat4Like, Mat4 } from '../core/math'; + +declare const jsb: any; + +// For optimizing getPosition, getRotation, getScale +export const _tempFloatArray = new Float32Array(jsb.createExternalArrayBuffer(20 * 4)); + +export const fillMat4WithTempFloatArray = function fillMat4WithTempFloatArray (out: IMat4Like) { + Mat4.set(out, + _tempFloatArray[0], _tempFloatArray[1], _tempFloatArray[2], _tempFloatArray[3], + _tempFloatArray[4], _tempFloatArray[5], _tempFloatArray[6], _tempFloatArray[7], + _tempFloatArray[8], _tempFloatArray[9], _tempFloatArray[10], _tempFloatArray[11], + _tempFloatArray[12], _tempFloatArray[13], _tempFloatArray[14], _tempFloatArray[15] + ); +}; +// +``` + +The above code defines a global `_tempFloatArray` that is used to store number or composite types (such as Vec3, Vec4, Mat4, etc.) parameters. + +node.jsb.ts + +```ts +// ...... +// Set the FloatArray to the CPP code +Node._setTempFloatArray(_tempFloatArray.buffer); +// ...... +// Reimplement the setPosition function in JS with parameters +nodeProto.setPosition = function setPosition(val: Readonly | number, y?: number, z?: number) { + if (y === undefined && z === undefined) { + // When both y and z are undefined, it means the first parameter is of type Vec3 + _tempFloatArray[0] = 3; + const pos = val as Vec3; + // Assign the new pos to the FloatArray and this._lpos cache + this._lpos.x = _tempFloatArray[1] = pos.x; + this._lpos.y = _tempFloatArray[2] = pos.y; + this._lpos.z = _tempFloatArray[3] = pos.z; + } else if (z === undefined) { + // If z is undefined, there are only 2 parameters x and y + _tempFloatArray[0] = 2; + this._lpos.x = _tempFloatArray[1] = val as number; + this._lpos.y = _tempFloatArray[2] = y as number; + } else { + _tempFloatArray[0] = 3; + this._lpos.x = _tempFloatArray[1] = val as number; + this._lpos.y = _tempFloatArray[2] = y as number; + this._lpos.z = _tempFloatArray[3] = z as number; + } + this._setPosition(); // This is a native binding function without parameters +}; +``` + +jsb_scene_manual.cpp + +```ts +namespace { + +/** + * Helper class for operating on the shared global FloatArray + */ +class TempFloatArray final { +public: + TempFloatArray() = default; + ~TempFloatArray() = default; + + inline void setData(float* data) { _data = data; } + + ...... + + inline const float& operator[](size_t index) const { return _data[index]; } + inline float& operator[](size_t index) { return _data[index]; } + +private: + float* _data{ nullptr }; + + CC_DISALLOW_ASSIGN(TempFloatArray) +}; + +TempFloatArray tempFloatArray; + +} // namespace +``` + +```c++ +static bool js_scene_Node_setTempFloatArray(se::State& s) // NOLINT(readability-identifier-naming) +{ + const auto& args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 1) { + uint8_t* buffer = nullptr; + args[0].toObject()->getArrayBufferData(&buffer, nullptr); + // Initialize the associated data of TempFloatArray + tempFloatArray.setData(reinterpret_cast(buffer)); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 1); + return false; +} +SE_BIND_FUNC(js_scene_Node_setTempFloatArray) +``` + +```c++ +bool register_all_scene_manual(se::Object* obj) // NOLINT(readability-identifier-naming) +{ + ...... + __jsb_cc_Node_proto->defineFunction("_setPosition", _SE(js_scene_Node_setPosition)); + __jsb_cc_Node_proto->defineFunction("_setScale", _SE(js_scene_Node_setScale)); + __jsb_cc_Node_proto->defineFunction("_setRotation", _SE(js_scene_Node_setRotation)); + __jsb_cc_Node_proto->defineFunction("_setRotationFromEuler", _SE(js_scene_Node_setRotationFromEuler)); + __jsb_cc_Node_proto->defineFunction("_rotateForJS", _SE(js_scene_Node_rotateForJS)); + ...... +} +``` + +```c++ +// Binding for node._setPosition() without parameters +static bool js_scene_Node_setPosition(void* s) // NOLINT(readability-identifier-naming) +{ + auto* cobj = reinterpret_cast(s); + auto argc = static_cast(tempFloatArray[0]); + if (argc == 2) { + // Get the parameters from tempFloatArray + cobj->setPositionInternal(tempFloatArray[1], tempFloatArray[2], true); + } else { + cobj->setPositionInternal(tempFloatArray[1], tempFloatArray[2], tempFloatArray[3], true); + } + return true; +} +SE_BIND_FUNC_FAST(js_scene_Node_setPosition) // Note that the new SE_BIND_FUNC_FAST macro is used here +``` + +Node.h + +```c++ + inline void setPositionInternal(float x, float y, bool calledFromJS) { setPositionInternal(x, y, _localPosition.z, calledFromJS); } + void setPositionInternal(float x, float y, float z, bool calledFromJS); +``` + +bindings/jswrapper/v8/HelperMacros.h + +```c++ +#define SE_BIND_FUNC(funcName) \ + void funcName##Registry(const v8::FunctionCallbackInfo &_v8args) { \ + JsbInvokeScope(#funcName); \ + jsbFunctionWrapper(_v8args, funcName, #funcName); \ + } + +#define SE_BIND_FUNC_FAST(funcName) \ + void funcName##Registry(const v8::FunctionCallbackInfo &_v8args) { \ + auto *thisObject = static_cast(_v8args.This()->GetAlignedPointerFromInternalField(0)); \ + auto *nativeObject = thisObject != nullptr ? thisObject->getPrivateData() : nullptr; \ + funcName(nativeObject); \ + } +``` + +bindings/jswrapper/v8/HelperMacros.cpp + +```c++ +// The SE_BIND_FUNC macro calls the jsbFunctionWrapper function, which performs additional tasks internally. +SE_HOT void jsbFunctionWrapper(const v8::FunctionCallbackInfo &v8args, se_function_ptr func, const char *funcName) { + bool ret = false; + v8::Isolate *isolate = v8args.GetIsolate(); + v8::HandleScope scope(isolate); + bool needDeleteValueArray{false}; + se::ValueArray &args = se::gValueArrayPool.get(v8args.Length(), needDeleteValueArray); + se::CallbackDepthGuard depthGuard{args, se::gValueArrayPool._depth, needDeleteValueArray}; + se::internal::jsToSeArgs(v8args, args); + se::Object *thisObject = se::internal::getPrivate(isolate, v8args.This()); + se::State state(thisObject, args); + ret = func(state); + if (!ret) { + SE_LOGE("[ERROR] Failed to invoke %s\n", funcName); + } + se::internal::setReturnValue(state.rval(), v8args); +} +``` + +The internal implementation of the `SE_BIND_FUNC_FAST` macro is extremely fast because it is very simple. It immediately triggers the callback function once it obtains the private data, without any se-related API calls. This avoids the conversion between `se::Value` and `jsvalue` and also bypasses parameter validation by V8. You can compare it with the standard implementation of the `SE_BIND_FUNC` macro. + +Furthermore, you may wonder why we don't share the position, rotation, and scale information directly through the approach described in the 'Shared Memory' section, but using `_setPosition()`, `_setRotation()`, and `_setScale()` JSB methods without parameters. The reason is that this JSB calls cannot be removed. Let's take a look at the implementation of `Node::setPosition()`: + +```c++ +class Node : public CCObject { + ...... + inline void setPosition(const Vec3 &pos) { setPosition(pos.x, pos.y, pos.z); } + inline void setPosition(float x, float y) { setPosition(x, y, _localPosition.z); } + inline void setPosition(float x, float y, float z) { setPositionInternal(x, y, z, false); } + inline void setPositionInternal(float x, float y, bool calledFromJS) { setPositionInternal(x, y, _localPosition.z, calledFromJS); } + void setPositionInternal(float x, float y, float z, bool calledFromJS); + + ...... + inline uint32_t getChangedFlags() const { + return _hasChangedFlagsVersion == globalFlagChangeVersion ? _hasChangedFlags : 0; + } + inline void setChangedFlags(uint32_t value) { + _hasChangedFlagsVersion = globalFlagChangeVersion; + _hasChangedFlags = value; + } + ...... +}; +``` + +```c++ +void Node::setPositionInternal(float x, float y, float z, bool calledFromJS) { + _localPosition.set(x, y, z); + invalidateChildren(TransformBit::POSITION); + + if (_eventMask & TRANSFORM_ON) { + emit(TransformBit::POSITION); + } + + if (!calledFromJS) { + notifyLocalPositionUpdated(); + } +} +``` + +`setPosition` not only assigns a value to `_localPosition`, but it also needs to trigger the invocation of the `invalidateChildren` function. `invalidateChildren` is a recursive function that internally traverses all child nodes and modifies other properties such as `_transformFlags`, `_hasChangedFlagsVersion`, and `_hasChangedFlags`. Therefore, we cannot remove `setPosition` through the "shared memory" approach mentioned in the first section. + +### Performance Comparison Results + +![opt-2.jpg](jsb/opt-2.jpg) + +## Caching Properties + +Caching properties in the JS can help avoid accessing the C++ interface through getters and reduce JSB(JavaScript Bindings) calls. + +node.jsb.ts + +```ts +Object.defineProperty(nodeProto, 'position', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._lpos; + }, + set (v: Readonly) { + this.setPosition(v as Vec3); + }, +}); + +nodeProto.getPosition = function getPosition (out?: Vec3): Vec3 { + if (out) { + return Vec3.set(out, this._lpos.x, this._lpos.y, this._lpos.z); + } + return Vec3.copy(new Vec3(), this._lpos); +}; + +nodeProto._ctor = function (name?: string) { + ...... + this._lpos = new Vec3(); + this._lrot = new Quat(); + this._lscale = new Vec3(1, 1, 1); + this._euler = new Vec3(); + ....... +}; +``` + +### Performance Comparison Results + +![opt-3.jpg](jsb/opt-3.jpg) + +## Node Synchronization + +In the user's logic code, it is common to use the following pattern: + +```ts +const children = node.children; +for (let i = 0; i < children.length; ++i) { + const child = children[i]; + // do something with child +} +``` + +The `.children` getter may be called frequently, which can result in excessive JSB (JavaScript Bindings) calls. The getter for `children` is bound uniquely because it represents a JS array, while in CPP it corresponds to the type `ccstd::vector _children;`. As a result, there is no straightforward way to synchronize data between JS Array and CPP's std::vector. If the `.children` getter is called using JSB each time, it will generate a temporary JS Array using `se::Object::createArrayObject`, and then convert each CPP child to JS child using `nativevalue_to_se` and assign it to the JS Array. This incurs a heavy conversion overhead and generates temporary arrays that need to be garbage collected, adding additional pressure to the GC. + +To address this issue, we cache the `_children` property in the JS. When `_children` is modified in the CPP, we use the event system to notify the JS code's `_children` to update its content. This is achieved by listening to `ChildAdded` and `ChildRemoved` events. + +Let's illustrate this with the example of `node.addChild(child);`: + +```c++ +class Node : public CCObject { + ...... + inline void addChild(Node *node) { node->setParent(this); } + + inline void removeChild(Node *node) const { + auto idx = getIdxOfChild(_children, node); + if (idx != -1) { + node->setParent(nullptr); + } + } + inline void removeFromParent() { + if (_parent) { + _parent->removeChild(this); + } + } + void removeAllChildren(); + ...... +}; +``` + +```c++ +void Node::setParent(Node* parent, bool isKeepWorld /* = false */) { + ...... + onSetParent(oldParent, isKeepWorld); + emit(oldParent); + if (oldParent) { + if (!(oldParent->_objFlags & Flags::DESTROYING)) { + index_t removeAt = getIdxOfChild(oldParent->_children, this); + if (removeAt < 0) { + return; + } + // Remove the child from the old parent node + oldParent->_children.erase(oldParent->_children.begin() + removeAt); + oldParent->updateSiblingIndex(); + oldParent->emit(this); + } + } + if (newParent) { + ...... + // Add the child to the new parent node + newParent->_children.emplace_back(this); + _siblingIndex = static_cast(newParent->_children.size() - 1); + newParent->emit(this); + } + onHierarchyChanged(oldParent); +} +``` + +After calling `addChild`, the `Node::_children` in the CPP is modified, it triggers the `emit` event. This event is listened to during Node initialization, as seen in the code snippet in `jsb_scene_manual.cpp`. + +```c++ +// jsb_scene_manual.cpp +static void registerOnChildAdded(cc::Node *node, se::Object *jsObject) { + node->on( + [jsObject](cc::Node * /*emitter*/, cc::Node *child) { + se::AutoHandleScope hs; + se::Value arg0; + nativevalue_to_se(child, arg0); + // Call the private JS function _onChildAdded + se::ScriptEngine::getInstance()->callFunction(jsObject, "_onChildAdded", 1, &arg0); + }); +} + +static bool js_scene_Node_registerOnChildAdded(se::State &s) // NOLINT(readability-identifier-naming) +{ + auto *cobj = SE_THIS_OBJECT(s); + SE_PRECONDITION2(cobj, false, "Invalid Native Object"); + + auto *jsObject = s.thisObject(); + + registerOnChildAdded(cobj, jsObject); + return true; +} +SE_BIND_FUNC(js_scene_Node_registerOnChildAdded) // NOLINT(readability-identifier-naming) + +bool register_all_scene_manual(se::Object *obj) // NOLINT(readability-identifier-naming) +{ + ...... + __jsb_cc_Node_proto->defineFunction("_registerOnChildAdded", _SE(js_scene_Node_registerOnChildAdded)); + ...... +} +``` + +node.jsb.ts + +```ts +Object.defineProperty(nodeProto, 'children', { + configurable: true, + enumerable: true, + get () { + return this._children; + }, + set (v) { + this._children = v; + }, +}); + +nodeProto._onChildRemoved = function (child) { + this.emit(NodeEventType.CHILD_REMOVED, child); +}; + +// This function is called by the CPP's registerOnChildAdded function +nodeProto._onChildAdded = function (child) { + this.emit(NodeEventType.CHILD_ADDED, child); +}; + +nodeProto.on = function (type, callback, target, useCapture: any = false) { + switch (type) { + ...... + case NodeEventType.CHILD_ADDED: + if (!(this._registeredNodeEventTypeMask & REGISTERED_EVENT_MASK_CHILD_ADDED_CHANGED)) { + this._registerOnChildAdded(); // Call the JSB method to register the listener + this._registeredNodeEventTypeMask |= REGISTERED_EVENT_MASK_CHILD_ADDED_CHANGED; + } + break; + ...... + default: + break; + } + this._eventProcessor.on(type, callback, target, useCapture); +}; + +nodeProto._ctor = function (name?: string) { + ...... + this._children = []; + + // Use the on interface to listen for CHILD_ADDED and CHILD_REMOVED events + this.on(NodeEventType.CHILD_ADDED, (child) => { + // Synchronize the this._children array in the JS + this._children.push(child); + }); + + this.on(NodeEventType.CHILD_REMOVED, (child) => { + const removeAt = this._children.indexOf(child); + if (removeAt < 0) { + errorID(1633); + return; + } + this._children.splice(removeAt, 1); + }); + + ...... +}; +``` + +### Performance Comparison Results + +![opt-4.jpg](jsb/opt-4.jpg) + +## Parameter Array Object Pool + +Up to version 3.6.0, Cocos Creator engine does not have an efficient memory pool implementation. When using the se (Script Engine Wrapper) for JS -> CPP interactions, temporary `se::ValueArray args(argCount)` objects need to be created. `se::ValueArray` is a `ccstd::vector` typedef, which leads to a significant amount of temporary memory allocations and deallocations, greatly impacting performance. This issue was not exposed in previous versions because the native code was relatively low-level and had fewer JSB calls. However, in version 3.6.0, with the increased native level hierarchy and JSB calls, this problem became more severe. + +To address this issue, we came up with a solution: using an object pool to reuse `se::ValueArray` objects. The implementation of the object pool, `se::ValueArrayPool`, is relatively simple and is outlined below: + +bindings/jswrapper/ValueArrayPool.h + +```c++ +// CallbackDepthGuard class for resetting se::Value objects in a se::ValueArray after use +// If the se::ValueArray is allocated with `new`, it will handle the `delete` process +class CallbackDepthGuard final { +public: + CallbackDepthGuard(ValueArray &arr, uint32_t &depth, bool needDelete) + : _arr(arr), _depth(depth), _needDelete(needDelete) { + ++_depth; + } + + ~CallbackDepthGuard() { + --_depth; + for (auto &e : _arr) { + e.setUndefined(); + } + if (_needDelete) { + delete &_arr; + } + } + +private: + ValueArray &_arr; + uint32_t &_depth; + const bool _needDelete{false}; +}; + +class ValueArrayPool final { +public: + // The maximum number of arguments for a bound function is 20 + // If there are more than 20 arguments, consider refactoring the function parameters + static const uint32_t MAX_ARGS = 20; + + ValueArrayPool(); + + ValueArray &get(uint32_t argc, bool &outNeedDelete); + + uint32_t _depth{0}; + +private: + void initPool(uint32_t index); + ccstd::vector> _pools; +}; + +extern ValueArrayPool gValueArrayPool; +``` + +bindings/jswrapper/ValueArrayPool.cpp + +```c++ +ValueArrayPool gValueArrayPool; + +// Define the maximum depth as 5. If the depth exceeds this value, the object pool will not be used. +#define SE_DEFAULT_MAX_DEPTH (5) + +ValueArrayPool::ValueArrayPool() { + _pools.resize(SE_DEFAULT_MAX_DEPTH); + for (uint32_t i = 0; i < SE_DEFAULT_MAX_DEPTH; ++i) { + initPool(i); + } +} + +ValueArray &ValueArrayPool::get(uint32_t argc, bool &outNeedDelete) { + // If the depth is greater than the size of the object pool, directly create a new ValueArray object. + if (SE_UNLIKELY(_depth >= _pools.size())) { + outNeedDelete = true; + auto *ret = ccnew ValueArray(); + ret->resize(argc); + return *ret; + } + + outNeedDelete = false; + CC_ASSERT_LE(argc, MAX_ARGS); + // Retrieve a ValueArray object from the object pool. + auto &ret = _pools[_depth][argc]; + CC_ASSERT(ret.size() == argc); + return ret; +} + +// Initialize pool +void ValueArrayPool::initPool(uint32_t index) { + auto &pool = _pools[index]; + uint32_t i = 0; + for (auto &arr : pool) { + arr.resize(i); + ++i; + } +} +``` + +Let's look at the implementation of jsbFunctionWrapper function: + +```c++ +SE_HOT void jsbFunctionWrapper(const v8::FunctionCallbackInfo &v8args, se_function_ptr func, const char *funcName) { + bool ret = false; + v8::Isolate *isolate = v8args.GetIsolate(); + v8::HandleScope scope(isolate); + + /* Original code + se::ValueArray args; + args.reserve(10); + */ + + // Optimized implementation - Start + bool needDeleteValueArray{false}; + // Retrieve a se::ValueArray from the global object pool, note that needDeleteValueArray is an output parameter + se::ValueArray &args = se::gValueArrayPool.get(v8args.Length(), needDeleteValueArray); + // Define a "callback depth guard" variable depthGuard, which automatically cleans up the object pool when it's destroyed + se::CallbackDepthGuard depthGuard{args, se::gValueArrayPool._depth, needDeleteValueArray}; + // Optimized implementation - End + + se::internal::jsToSeArgs(v8args, args); + se::Object *thisObject = se::internal::getPrivate(isolate, v8args.This()); + se::State state(thisObject, args); + ret = func(state); + if (!ret) { + SE_LOGE("[ERROR] Failed to invoke %s\n", funcName); + } + se::internal::setReturnValue(state.rval(), v8args); +} +``` + +### Performance Comparison Results + +![opt-5.jpg](jsb/opt-5.jpg) + +## Conclusion + +The main optimization techniques employed in Cocos Creator 3.6.0 native engine are as follows: + +1. Implementing the engine's core modules in the native(C++) to leverage the performance of C++ code execution. +2. Minimizing the frequency of cross-language interactions (JS <-> CPP) through the following five methods: + - Shared memory: Improving performance by reducing memory copies. + - Avoiding parameter passing: Using member variables instead of frequent parameter passing. + - Caching properties: Storing frequently accessed properties in cache to avoid redundant retrieval. + - Node synchronization: Synchronizing node changes through an event listening mechanism during node operations. + - Parameter array object pool: Reusing parameter array objects using an object pool to reduce memory allocation and deallocation overhead. + +By implementing these optimization measures, we have improved the performance of the Cocos Creator engine. These optimization techniques significantly reduce the overhead of cross-language interactions and enhance the overall performance and responsiveness of the engine. diff --git a/versions/4.0/en/advanced-topics/jsb-sebind.md b/versions/4.0/en/advanced-topics/jsb-sebind.md new file mode 100644 index 0000000000..966588726f --- /dev/null +++ b/versions/4.0/en/advanced-topics/jsb-sebind.md @@ -0,0 +1,567 @@ +# `sebind` Tutorial + +Before `sebind`, both manual and automatic bindings required more steps for developers to complete the binding and required more knowledge of JSB. `sebind` takes advantage of C++ templates to minimize intermediate code. + +## A simple example + +We will define an object `simpleMath` in the global space, and add a `lerp` function. + +```js +let v = simpleMath.lerp(a, b, t); +``` +The function is implemented in C++. + +### Preparation + +We need to create a Cocos Creator project, save a scene, and create a new build task for any native platform. This example uses the Windows platform. + +### Step 1: Add the binding code + +In the `native/engine/common/Classes` directory, create a new file `HelloSEBind.cpp` and write the following code: + +```c++ +// HelloSEBind.cpp +#include "bindings/sebind/sebind.h" + +namespace { +struct Empty {}; // act as a namespace +float lerp(float a, float b, float t) { return (1 - t) * a + t * b; } +} // namespace + +bool jsb_register_simple_math(se::Object *globalThis) { + + sebind::class_ demoMathClass("simpleMath"); + { + // invoke through simpleMath.lerp(a, b, t); + demoMathClass.staticFunction("lerp", &lerp).install(globalThis); + } + return true; +} +``` + +Include the file `HelloSEBind.cpp` to `native/engine/common/CMakeLists.txt` + +```cmake + +# ... + +list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp + ${CMAKE_CURRENT_LIST_DIR}/Classes/HelloSEBind.cpp # new file +) +``` +### Step 2: Register to `ScriptEngine` + +Edit `Game.cpp`, located in the `native/engine/common/Classes` directory + +```c++ +// ... + +// declare registry function +extern bool jsb_register_simple_math(se::Object *); // #1 + +Game::Game() = default; + + +int Game::init() { + +// ... + _xxteaKey = SCRIPT_XXTEAKEY; + + // add callback to script engine + auto *seengine = se::ScriptEngine::getInstance(); // #2 + seengine->addRegisterCallback(jsb_register_simple_math); // #3 + + BaseGame::init(); + //.... +``` + +### Step 3: Verification + +Start the program in Debug mode, and connect with Chrome devtools: + +![devtool](./sebind/devtool-test.png) + +Done. + +Compared to the previous approachs, the use of the `SE` APIs is greatly reduced, and so is the amount of code. + +## A more complex binding example + +`sebind` does interface binding on a class-by-class basis. Each JS class requires the construction of a corresponding `sebind::class_` instance. All class bindings are done through the methods provided by `sebind::class_`. + +In the following, we will familiarize ourselves with the `sebind` binding process by exporting the sample code for the `User` class. + +> **Note**: The sample code is only used to illustrate the usage of `sebind`, the internal interface design and implementation is not useful in practice. + +```c++ +class User { +private: + static int userCount; + +public: + // static methods + static int doubleUserCount() { return 2 * userCount; } + // static attributes + static int getUserCount() { return userCount; } + static void setUserCount(int v) { userCount = v; } + + // constructors with different parameters + User() { userCount++; } + User(const std::string &name_) : User() { name = name_; } + User(const std::string &name_, const std::string &token) + : User() { + name = name_; + _token = token; + } + User(const std::string &name_, const std::string &token, int credit) + : User(name, token) { + _credit = credit; + name = name_; + _token = token; + } + + + ~User() = default; + + // attributes + std::string getToken() const { return _token; } + void setToken(const std::string &t) { _token = t; } + + // override function + std::string toString() const { return name + ":" + _token; } + std::string toString(const std::string &tag) const { + return "[" + tag + "]:" + name + ":" + _token; + } + + // function args with bound type + std::string mergeName1(User &other) { return name + "|" + other.name; } + std::string mergeName2(User *other) { return name + "|" + other->name; } + std::string mergeName3(const std::shared_ptr &other) { + return name + "|" + other->name; + } + // public fields + std::string name{"unset"}; + +private: + std::string _token{"unset"}; + int _credit{-1}; +}; + +int User::userCount = 0; + +} // namespace + +``` +#### Instantiate `sebind::class_` + +Relates a C++ class to a specified JS class name + +```c++ +sebind::class_ userClass("User"); +``` + +#### Bind constructors + +```c++ + userClass.constructor<>() // JS: new User + .constructor() // JS: new User("Jone") + .constructor() // JS: new User("Jone", "343453") + .constructor() //JS: new User("Jone", "343453", 5678) +``` + +There are 4 constructor patterns declared here, with 0,1,2,3 parameters. Each template parameter corresponds to the constructor's parameter type. +When calling `new User(...)` will trigger the corresponding C++ constructor depending on the number of arguments + +> **Note**: If you don't declare any `constructor`, `sebind:class_` will use the default parameterless constructor. + +We can also define common functions as constructors, for example: + +```c++ +User *createUser(int credit) { + return = new User("Lambda", "ctor", credit); +} + +// ... +.constructor(&createUser) // JS: new User(234) +``` + +The return value needs to be a `User*` type. This is equivalent to declaring the constructor `constructor(credit:number)` in JS. + +#### Exporting member properties + +Export a C++ public field as a JS property + +```c++ +.property("name", &User::name) // JS: user.name +``` + +You can also define `getter`/`setter` functions as properties. Here the `getter` function needs to have a return value, and no parameters. The `setter` function takes one argument. + +```c++ +.property("token", &User::getToken, &User::setToken) // JS: user.token +``` + +> **Note**: `getter`/`setter` can not be both `nullptr`. + +Common functions, with `User*` as the first argument, can be used as member functions. For example: + +```c++ +std::string tokenLong_get(User *u) { + return "token[" + u->getToken() + "]"; +} +void tokenLong_set(User *u, const std::string &s) { + u->setToken("token[" + u->getToken() + "]"); +} +//... +.property("tokenPrefix", &tokenLong_get, &tokenLong_set) // JS: user.tokenPrefix +``` + +#### Export Member Functions + +Exporting C++ member functions to JS. + +```c++ +.function("mergeName1", &User::mergeName1) // JS: user1.mergeName1(user2) +.function("mergeName2", &User::mergeName2) // JS: user2.mergeName1(user2) +.function("mergeName3", &User::mergeName3) // JS: user3.mergeName1(user2) +``` + +Instances of bound types in JS can be passed as arguments to C++ bound functions. C++ functions can take instances of bound objects by *reference*, *pointer* or *smart pointer*. Here, if `User` inherits `cc::RefCounted`, we can use `cc::IntrusivePtr` to hold it. If we don't inherit `cc::RefCounted`, as is the case now, we can also hold it with `std::shared_ptr`. After holding with `shared_ptr/IntrusivePtr`, the associated JS object is GC'd, and the object held at the C++ level will not be destroyed. + +> **Note**: The binding type needs to be registered with the macro `JSB_REGISTER_OBJECT_TYPE(User);` before calling the sebind APIs. Only then will the subsequent `jsb_conversions` method handle the type conversion correctly. + +If a function is overloaded, we need to specify the specific type of the function pointer with `static_cast`. + +```c++ +.function("toString", static_cast(&User::toString)) ///JS: (new User).toString() +.function("toString", static_cast(&User::toString)) //JS: (new User).toString("1111") +``` + +Similar to constructors, overloaded functions are matched based on the number of arguments, and the same number of arguments should be avoided. If you need to determine the type of parameters at runtime, you can refer to bind [`SE` functions](#manual-type-conversion). + +#### Exporting static methods of a class + +Exporting static functions of a C++ class + +```c++ +.staticFunction("doubleUserCount", &User::doubleUserCount) // JS: User.doubleUserCount() +``` +It is also possible to export common functions as class static functions + +```c++ +int static_add(int a, int b) { return a + b; } +///... +.staticFunction("add", &static_add) //JS: User.add(1,2) +``` + +#### Export Class Static Properties + +Export a C++ class static function as a static property of a JS class. + +```c++ +.staticProperty("userCount", &User::getUserCount, &User::setUserCount) //JS: User.userCount +``` +or common functions +```c++ +int gettime() { return time(nullptr); } +/// ... +.staticProperty("time", &gettime, nullptr) //JS: User.time +``` + +#### Registering A Destruct Callback + +Register a callback for when the bound object is GC'd. + +```c++ +.finalizer([](User *usr) { + std::cout << "release " << usr->name << std::endl; +}) +``` + +#### Export the class to JS global namespace + +Mounts the `User` class to the `globalThis` object, completing the export. `User` class can be accessed globally in the JS script. + +```c++ +.install(globalThis); +``` + +#### Class Inheritance + +Specify the parent class's prototype in the second parameter of the `sebind::class_` constructor. Here the `SuperUser` class inherits from the `User` class. + +```c++ +sebind::class_ superUser("SuperUser", userClass.prototype()); +{ + superUser.constructor() + .function( + "superName", +[](User *user) { return user->name + ".super"; + }) // JS: (new SuperUser("Mok")).superName() + .install(globalThis); +} +``` + +> **Note**: that the static methods of the parent class are not inherited by the child class. + +## Other Uses + +### Calling JS functions in C++ + +Since 3.6.1, with `sebind::bindFunction` you can bind `se::Value` to `std::function` in C++, without having to deal with parameter conversions. Similarly, you can use `sebind::callFunction` to call JS functions directly. + +For example: +```c++ +demo.staticFunction( + "add", + +[](const se::Value &func, int a, int b) { + // bind js function as a std::function + auto addFunc = sebind::bindFunction(func); + // .. + // invoke std::function + auto result = addFunc(a, b); + + // call JS function with automatic arguments assembling + auto result2 = sebind::callFunction(func, a, b); + auto result3 = sebind::callFunction(func, 6, 8); + + // argument type computing + auto result4 = sebind::callFunction(func, a, b); + auto result5 = sebind::callFunction(func, 6, 8); + + std::cout << "result 1 " << result << std::endl; + std::cout << "result 2 " << result2 << std::endl; + }); +``` + +### Binding abstract classes + +`sebind::class_` requires a constructor to be provided, but the constructor for the abstract class is not available. Resolve this conflict by providing an empty constructor to enable registration of the abstract type. + +For example: + +```c++ + +class AbstractClass { +public: + virtual bool tick() = 0; +}; + +class SubClass : public AbstractClass { +public: + bool tick() override { return true; } +}; + +AbstractClass *fakeConstructor() { + assert(false); // Abstract class cannot be instantiated + return nullptr; +} + +//.. +sebind::class_ base("AbstractBase"); + +base.constructor<>(&fakeConstructor) // add constructor + .function("tick", &AbstractClass::tick) + .install(globalThis); + +sebind::class_ sub("SubClass", base.prototype()); +sub.install(globalThis); +``` + +### Manual Type Conversion + +`sebind` supports binding traditional `SE` functions to perform the conversion manually. For example: + +```c++ +bool jsb_sum(se::State &state) { + double result = 0; + auto &args = state.args(); + for (int i = 0; i < args.size(); i++) { + result += (args[i].isNumber() ? args[i].toDouble() : 0); + } + state.rval().setDouble(result); + return true; +} +/// +.staticFunction("sum", &jsb_sum) // JS: User.sum(1,2,3,4,5) +``` + +This allows for variable-length parameters and flexible parameter conversions. It's more flexible than automatic conversions and requires developers to be more familiar with the `SE API`. + +### Requirng JS `this` Object within constructor + +Requring the corresponding JS `this` object in a C++ constructor is a common requirement, and simplifies access from C++ to JS. + +All we need to do is specify the placeholder `sebind::ThisObject` in the argument type of the `constructor` and declare the argument type of the corresponding constructor as `se::Object *`. + +```c++ +// constructor +User(se::Object *self, const std::string &name_) { + self->setProperty("fromNative", se::Value(true)); + name = name_; +} +/// ... +superUser.constructor() // JS: new SuperUser("Jone") +``` + +> **Note**: Common functions are not supported here for now. + +When calling the corresponding constructor in JS, all `sebind::ThisObject` parameters should be ignored. + +![sebind::ThisObject](./sebind/thisobject_placeholder.png) + +
+ +Full content of *HelloSEBind.cpp* + + +```c++ + +#include "bindings/sebind/sebind.h" +#include + +namespace { + + +struct Empty {}; // act as a namespace +float lerp(float a, float b, float t) { return (1 - t) * a + t * b; } + +class User { + static int userCount; + +public: + static int doubleUserCount() { return 2 * userCount; } + static int getUserCount() { return userCount; } + static void setUserCount(int v) { userCount = v; } + + User() { userCount++; } + // User(const std::string &name_) : User() { name = name_; } + User(const std::string &name_, const std::string &token) : User() { + name = name_; + _token = token; + } + User(const std::string &name_, const std::string &token, int credit) + : User(name, token) { + _credit = credit; + name = name_; + _token = token; + } + + User(se::Object *self, const std::string &name_) { + self->setProperty("fromNative", se::Value(true)); + name = name_; + } + + ~User() { userCount--; } + + std::string getToken() const { return _token; } + void setToken(const std::string &t) { _token = t; } + std::string toString() const { return name + ":" + _token; } + std::string toString(const std::string &tag) const { + return "[" + tag + "]:" + name + ":" + _token; + } + + std::string mergeName1(User &other) { return name + "|" + other.name; } + std::string mergeName2(const std::shared_ptr &other) { + return name + "|" + other->name; + } + std::string mergeName3(User *other) { return name + "|" + other->name; } + + std::string name{"unset"}; + +private: + std::string _token{"unset"}; + int _credit{-1}; +}; + +int User::userCount = 0; + +class UserExt : public User { +public: + using User::User; +}; + +///////////////////////////////////////////////////// + +User *createUser(int credit) { return new User("Lambda", "ctor", credit); } + +std::string tokenLong_get(User *u) { return "token[" + u->getToken() + "]"; } +void tokenLong_set(User *u, const std::string &s) { + u->setToken("token[" + u->getToken() + "]"); +} + +int static_add(int a, int b) { return a + b; } + +bool jsb_sum(se::State &state) { + double result = 0; + auto &args = state.args(); + for (int i = 0; i < args.size(); i++) { + result += (args[i].isNumber() ? args[i].toDouble() : 0); + } + state.rval().setDouble(result); + return true; +} + +int gettime() { return time(nullptr); } + + +} // namespace + +JSB_REGISTER_OBJECT_TYPE(User); + +bool jsb_register_simple_math(se::Object *globalThis) { + + sebind::class_ demoMathClass("simpleMath"); + { + // invoke through simpleMath.lerp(a, b, t); + demoMathClass.staticFunction("lerp", &lerp).install(globalThis); + } + + sebind::class_ userClass("User"); + { + + userClass + .constructor<>() + .constructor() + .constructor() + .constructor() + // .constructor(&createUser) + .property("name", &User::name) + .property("token", &User::getToken, &User::setToken) + .property("tokenPrefix", &tokenLong_get, nullptr) + .function("mergeName1", &User::mergeName1) + // .function("mergeName2", &User::mergeName2) + .function("mergeName3", &User::mergeName3) + .function("toString", + static_cast(&User::toString)) + .function("toString", + static_cast( + &User::toString)) + .staticFunction("doubleUserCount", &User::doubleUserCount) + .staticProperty("userCount", &User::getUserCount, &User::setUserCount) + .staticProperty("time", &gettime, nullptr) + .staticFunction("sum", &jsb_sum) + .finalizer([](User *usr) { + std::cout << "release " << usr->name << std::endl; + }) + .install(globalThis); + } + + sebind::class_ superUser("SuperUser", userClass.prototype()); + { + superUser.constructor() + .function( + "superName", +[](UserExt *user) { return user->name + ".super"; }) + .install(globalThis); + } + + return true; +} +``` + +
+ + + + diff --git a/versions/4.0/en/advanced-topics/jsb-swig.md b/versions/4.0/en/advanced-topics/jsb-swig.md new file mode 100644 index 0000000000..e8d691f2a9 --- /dev/null +++ b/versions/4.0/en/advanced-topics/jsb-swig.md @@ -0,0 +1,71 @@ +# Introduction + +From Cocos Creator 3.7.0, we switch the approach of generating JS binding code from [bindings-generator](https://github.com/cocos/cocos4/tree/d08a11244d2a31da1aac7af7d2aa8f1b6152e30c/native/tools/bindings-generator) to [Swig](https://www.swig.org). Swig has many benefits in generating glue code by parsing its custom `interface` file (IDL) which is compatible with `C++`. For more about why we switch to Swig, you could refer to [this issue](https://github.com/cocos/cocos-engine/issues/10792) . + +## Generate JS Binding Code for Engine + +In 3.8 and later versions, developers no longer need to manually trigger the generation of binding code because CMake will automatically call the SWIG command during the build process to generate the binding code. + +Please note that if the generation process fails, you should pay attention to the output logs in the console. + +However, if you add or remove `.i` files for the engine, you need to regenerate the project to ensure that the updated references to the `.i` files are taken into account and that the updated binding code is generated. + +## Generate JS Bindings Code for Developer's Project + +- Make sure you have installed NodeJS (`>= v8.9.4`) + +- Open Terminal (macOS/Linux) or Command Line Tool (Windows) + +- Create a directory for generated code, e.g. `/Users/abc/my-project/native/engine/common/Classes/bindings/auto` + +- Write a JS configuration file + + - Create the JS configruation file, e.g. `/Users/abc/my-project/tools/swig-config/swig-config.js` with the following content + + ```js + 'use strict'; + const path = require('path'); + + // Developer's custom module configuration + // configList is required + const configList = [ + [ 'your_module_interface_0.i', 'jsb_your_module_interface_0_auto.cpp' ], + [ 'your_module_interface_1.i', 'jsb_your_module_interface_1_auto.cpp' ], + // ...... + ]; + + const projectRoot = path.resolve(path.join(__dirname, '..', '..')); + // interfaceDir is optional + const interfacesDir = path.join(projectRoot, 'tools', 'swig-config'); + // bindingsOutDir is optional + const bindingsOutDir = path.join(projectRoot, 'native', 'engine', 'common', 'Classes', 'bindings', 'auto'); + + module.exports = { + interfacesDir, // optional, if it isn't exported, the items in configList should be absolute or relative to current directory of swig-config.js + bindingsOutDir, // optional, if it isn't exported, the items in configList should be absolute or relative to current directory of swig-config.js + configList // required + }; + ``` + + - Run the following command + + ```bash + # If current workspace is not in '/Users/abc/my-project/tools/swig-config' + $ node < Engine Root Path >/native/tools/swig-config/genbindings.js -c /Users/abc/my-project/tools/swig-config/swig-config.js + ``` + + ```bash + # If you have already navigate to '/Users/abc/my-project/tools/swig-config' directory, you could run the command without -c argument like: + $ cd /Users/abc/my-project/tools/swig-config + $ node < Engine Root Path >/native/tools/swig-config/genbindings.js + ``` + +## Swig Interface File + +- There is a [swig-interface-template.i](https://github.com/cocos/cocos4/blob/1f928364f4cad22681e7830c53dc7da71a87d11f/native/tools/swig-config/swig-interface-template.i) in `engine/native/tools/swig-config` directory, just copy and rename it to some place in your project. There some comments demonstrate how to configure your module in `.i` file. You could also reference engine internal `.i` files in `engine/native/tools/swig-config`, for instance, `scene.i` or `assets.i` for a quick start. +- If you're using `Visual Studio Code`, you could install `SWIG Language` extension which was developed by `Hong-She Liang` for highlight syntax support. +- For more details of writing `.i` file, please visit [tutorial](#tutorial) section. + +## Tutorial + +Visit [The Tutorial of Swig Workflow in Cocos Creator](jsb/swig/tutorial/index.md), which includes binding a new module in engine or user's project step by step. diff --git a/versions/4.0/en/advanced-topics/jsb-web-difference.md b/versions/4.0/en/advanced-topics/jsb-web-difference.md new file mode 100644 index 0000000000..1fc98f67de --- /dev/null +++ b/versions/4.0/en/advanced-topics/jsb-web-difference.md @@ -0,0 +1,90 @@ +# The differences between Pure JS and JS-Binding objects + +Cocos Creator Engine uses `V8` to export CPP classes to Javascript, we call it `JS-Binding`. There're some differences between Pure JS and JS-Binding objects. If your application needs to run on all platforms, especially on native platforms, it's better not to use some APIs that behave differently between `Web` and `Native` platforms. If you have to use them, you need to write some platform specified code like `if (NATIVE) {...} else {...}` . + +## Properties + +The properties of pure JS objects are assigned to instance, but they're assigned to `prototype` for JS-Binding objects. + +### Pure JS objects + +```typescript +class MyClass { + constructor() { + this.a = 'a'; + this.b = false; + this.c = 100; + } +}; + +const myobj = new MyClass(); +const ownA = myobj.hasOwnProperty('a'); +console.log(`ownA: ${ownA}`); // ownA: true +``` + +### JS-Binding objects + +```c++ +// C++ class +class MyClass { +public: + std::string a; + bool b; + int32_t c; +}; + +// The snippet of JS-Binding glue code. +bool js_register_cc_MyClass(se::Object* obj) { + auto* cls = se::Class::create("MyClass", obj, nullptr, _SE(js_new_cc_MyClass)); + cls->defineStaticProperty("__isJSB", se::Value(true), se::PropertyAttribute::READ_ONLY | se::PropertyAttribute::DONT_ENUM | se::PropertyAttribute::DONT_DELETE); + + cls->defineProperty("a", _SE(js_cc_MyClass_a_get), _SE(js_cc_MyClass_a_set)); + cls->defineProperty("b", _SE(js_cc_MyClass_b_get), _SE(js_cc_MyClass_b_set)); + cls->defineProperty("c", _SE(js_cc_MyClass_c_get), _SE(js_cc_MyClass_c_set)); + + cls->defineFinalizeFunction(_SE(js_delete_cc_MyClass)); + cls->install(); + JSBClassType::registerClass(cls); + + __jsb_cc_MyClass_proto = cls->getProto(); + __jsb_cc_MyClass_class = cls; + se::ScriptEngine::getInstance()->clearException(); + return true; +} +// +``` + +```typescript +const myobj = new jsb.MyClass(); +const ownA = myobj.hasOwnProperty('a'); +console.log(`ownA: ${ownA}`); // ownA: false +const protoOwnA = myobj.__proto__.hasOwnProperty('a'); +console.log(`protoOwnA: ${protoOwnA}`); // protoOwnA: true +``` + +So if you have written some JS code depends on `hasOwnProperty`, you need to keep in mind of the above difference since it may cause your application to work correctly on Web platform, but be broken on Native platform. + +### Possible Solution + +If the application needs to run on Native platform, while checking whether a property exists on a JS-Binding object, you need to check its `__proto__` either. For example: + +```typescript +function doSomething(v) { + // ...... +} + +function foo() { + for (const key in myObj) { + if (myObj.hasOwnProperty('a')) { + doSomething(myObj['a']); + } else if (NATIVE) { + if (myObj.__proto__.hasOwnProperty('a')) { + doSomething(myObj['a']); + } + } + } +} +``` + + + diff --git a/versions/4.0/en/advanced-topics/jsb/100.png b/versions/4.0/en/advanced-topics/jsb/100.png new file mode 100644 index 0000000000..49fddd9197 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/100.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/110.png b/versions/4.0/en/advanced-topics/jsb/110.png new file mode 100644 index 0000000000..b83f27e9a2 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/110.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/111.png b/versions/4.0/en/advanced-topics/jsb/111.png new file mode 100644 index 0000000000..6b4e659937 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/111.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/112.png b/versions/4.0/en/advanced-topics/jsb/112.png new file mode 100644 index 0000000000..90baa96c38 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/112.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/JSB2.0-Architecture.png b/versions/4.0/en/advanced-topics/jsb/JSB2.0-Architecture.png new file mode 100644 index 0000000000..c7b115ed21 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/JSB2.0-Architecture.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/auto-file.png b/versions/4.0/en/advanced-topics/jsb/auto-file.png new file mode 100644 index 0000000000..4777dc508d Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/auto-file.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/binding-file.png b/versions/4.0/en/advanced-topics/jsb/binding-file.png new file mode 100644 index 0000000000..ca182f89c9 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/binding-file.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/called-injs.png b/versions/4.0/en/advanced-topics/jsb/called-injs.png new file mode 100644 index 0000000000..ee07044673 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/called-injs.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/cancel-output_dir.png b/versions/4.0/en/advanced-topics/jsb/cancel-output_dir.png new file mode 100644 index 0000000000..c118a809e2 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/cancel-output_dir.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/error-1.png b/versions/4.0/en/advanced-topics/jsb/error-1.png new file mode 100644 index 0000000000..d25b0fd6f5 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/error-1.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/error-2.png b/versions/4.0/en/advanced-topics/jsb/error-2.png new file mode 100644 index 0000000000..6778af5cb1 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/error-2.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/generate-binding-file.png b/versions/4.0/en/advanced-topics/jsb/generate-binding-file.png new file mode 100644 index 0000000000..57282f7503 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/generate-binding-file.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/generate-file-complete.png b/versions/4.0/en/advanced-topics/jsb/generate-file-complete.png new file mode 100644 index 0000000000..caf7eea7f6 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/generate-file-complete.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/generate-file.png b/versions/4.0/en/advanced-topics/jsb/generate-file.png new file mode 100644 index 0000000000..5d1363e0f9 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/generate-file.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/infrastructure.png b/versions/4.0/en/advanced-topics/jsb/infrastructure.png new file mode 100644 index 0000000000..7b7f6a2c65 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/infrastructure.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/jsb_process.jpg b/versions/4.0/en/advanced-topics/jsb/jsb_process.jpg new file mode 100644 index 0000000000..b4e9c0f273 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/jsb_process.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/opt-1.jpg b/versions/4.0/en/advanced-topics/jsb/opt-1.jpg new file mode 100644 index 0000000000..3529892643 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/opt-1.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/opt-2.jpg b/versions/4.0/en/advanced-topics/jsb/opt-2.jpg new file mode 100644 index 0000000000..1e3599ac7c Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/opt-2.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/opt-3.jpg b/versions/4.0/en/advanced-topics/jsb/opt-3.jpg new file mode 100644 index 0000000000..4dbaf1927f Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/opt-3.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/opt-4.jpg b/versions/4.0/en/advanced-topics/jsb/opt-4.jpg new file mode 100644 index 0000000000..355f57f538 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/opt-4.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/opt-5.jpg b/versions/4.0/en/advanced-topics/jsb/opt-5.jpg new file mode 100644 index 0000000000..b5a9c45165 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/opt-5.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/store-file.png b/versions/4.0/en/advanced-topics/jsb/store-file.png new file mode 100644 index 0000000000..bd2a0b0f75 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/store-file.png differ diff --git a/versions/4.0/en/advanced-topics/jsb/swig/tutorial/MyRefCompileError.jpg b/versions/4.0/en/advanced-topics/jsb/swig/tutorial/MyRefCompileError.jpg new file mode 100644 index 0000000000..dee74edb03 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/swig/tutorial/MyRefCompileError.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/swig/tutorial/another-module-compile-error.jpg b/versions/4.0/en/advanced-topics/jsb/swig/tutorial/another-module-compile-error.jpg new file mode 100644 index 0000000000..bc3fb929a8 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/swig/tutorial/another-module-compile-error.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/swig/tutorial/index.md b/versions/4.0/en/advanced-topics/jsb/swig/tutorial/index.md new file mode 100644 index 0000000000..477bcb86ac --- /dev/null +++ b/versions/4.0/en/advanced-topics/jsb/swig/tutorial/index.md @@ -0,0 +1,1144 @@ +# The Tutorial of Swig Workflow in Cocos Creator + +## How to Bind a New Module in Engine + +### Add a new module interface file + +- Add a new module interface file to `native/tools/swig-config` directory, e.g. `new-engine-module.i` + +- Copy the content in [swig-interface-template.i](https://github.com/cocos/cocos4/blob/1f928364f4cad22681e7830c53dc7da71a87d11f/native/tools/swig-config/swig-interface-template.i) to new-engine-module.i + +- Add necessary configuration, you refer to the existed `.i` files in `native/tools/swig-config` directory or refer to [the following section](#How to Bind a New Module in Developer's Project) + +### Modify `engine/native/cocos/CMakeLists.txt` + +```cmake +######## auto +cocos_source_files( + NO_WERROR NO_UBUILD ${SWIG_OUTPUT}/jsb_cocos_auto.cpp # Add this line + ${SWIG_OUTPUT}/jsb_cocos_auto.h # Add this line + NO_WERROR NO_UBUILD ${SWIG_OUTPUT}/jsb_cocos_auto.cpp + ${SWIG_OUTPUT}/jsb_cocos_auto.h + ...... +``` + +### Register the new module to Script Engine + +Open `jsb_module_register.cpp` and do the following modifications + +```c++ +...... +#if CC_USE_PHYSICS_PHYSX + #include "cocos/bindings/auto/jsb_physics_auto.h" +#endif +#include "cocos/bindings/auto/jsb_new_engine_module_auto.h" // Add this line + +bool jsb_register_all_modules() { + se::ScriptEngine *se = se::ScriptEngine::getInstance(); + ...... + se->addRegisterCallback(register_all_my_new_engine_module); // Add this line + + se->addAfterCleanupHook([]() { + cc::DeferredReleasePool::clear(); + JSBClassType::cleanup(); + }); + return true; +} +``` + +## How to Bind a New Module in Developer's Project + +Suppose we have a Cocos Creator project located at `/Users/james/NewProject` directory. + +Build a native project in Cocos Creator's build panel, we get `/Users/james/NewProject/native` directory. + +### Bind a simple class + +#### Create a simple class + +Create a header file in `/Users/james/NewProject/native/engine/common/Classes/MyObject.h` , its content is + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +namespace my_ns { +class MyObject { +public: + MyObject() = default; + MyObject(int a, bool b) {} + virtual ~MyObject() = default; + void print() { + CC_LOG_DEBUG("==> a: %d, b: %d\n", _a, (int)_b); + } + + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; +}; +} // namespace my_ns { +``` + +#### Write an interface file + +Create an interface called `my-module.i` file in `/Users/james/NewProject/tools/swig-config` + +```c++ +// my-module.i +%module(target_namespace="my_ns") my_module + +// Insert code at the beginning of generated header file (.h) +%insert(header_file) %{ +#pragma once +#include "bindings/jswrapper/SeApi.h" +#include "bindings/manual/jsb_conversions.h" + +#include "MyObject.h" // Add this line +%} + +// Insert code at the beginning of generated source file (.cpp) +%{ +#include "bindings/auto/jsb_my_module_auto.h" +%} + +%include "MyObject.h" +``` + +#### Write a swig config file + +Create a file called swig-config.js in `/Users/james/NewProject/tools/swig-config` + +```js +// swig-config.js +'use strict'; +const path = require('path'); +const configList = [ + [ 'my-module.i', 'jsb_my_module_auto.cpp' ], +]; + +const projectRoot = path.resolve(path.join(__dirname, '..', '..')); +const interfacesDir = path.join(projectRoot, 'tools', 'swig-config'); +const bindingsOutDir = path.join(projectRoot, 'native', 'engine', 'common', 'bindings', 'auto'); +// includeDirs means header search path for Swig parser +const includeDirs = [ + path.join(projectRoot, 'native', 'engine', 'common', 'Classes'), +]; + +module.exports = { + interfacesDir, + bindingsOutDir, + includeDirs, + configList +}; +``` + +#### Generate bindings for project + +```bash +$ cd /Users/james/NewProject/tools/swig-config +$ node < Engine Root >/native/tools/swig-config/genbindings.js +``` + +If succeed, the files ( `jsb_my_module_auto.cpp/.h` ) contain JS binding code will be generated at `/Users/james/NewProject/native/engine/bindings/auto` directory + +#### Modify project's CMakeLists.txt + +- Open `/Users/james/NewProject/native/engine/common/CMakeLists.txt`, add `MyObject.h` and its binding code + + ```cmake + include(${COCOS_X_PATH}/CMakeLists.txt) + + list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp + ############### Add the following lines ############## + ${CMAKE_CURRENT_LIST_DIR}/Classes/MyObject.h + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.h + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.cpp + ######################################################## + ) + ``` + +- Modify `/Users/james/NewProject/native/engine/mac/CMakeLists.txt` + + ```cmake + cmake_minimum_required(VERSION 3.8) + # ...... + cc_mac_before_target(${EXECUTABLE_NAME}) + add_executable(${EXECUTABLE_NAME} ${CC_ALL_SOURCES}) + ############### Add the following lines ############## + target_include_directories(${EXECUTABLE_NAME} PRIVATE + ${CC_PROJECT_DIR}/../common + ) + ######################################################## + cc_mac_after_target(${EXECUTABLE_NAME}) + ``` + + +#### Open project + +macOS: `/Users/james/NewProject/build/mac/proj/NewProject.xcodeproj ` + +Windows: `< A specific directory >/NewProject/build/win64/proj/NewProject.sln` + +#### Register the new module to Script Engine + +Modify `Game.cpp` : + +```c++ +#include "Game.h" +#include "bindings/auto/jsb_my_module_auto.h" // Add this line +//...... +int Game::init() { + // ...... + se::ScriptEngine::getInstance()->addRegisterCallback(register_all_my_module); // Add this line + BaseGame::init(); + return 0; +} +// ...... +``` + +#### Test binding + +- Add a `my-module.d.ts` file in the root of project directory to make TS compiler know our binding class. + + ```ts + // my-module.d.ts + declare namespace my_ns { + class MyObject { + constructor(); + constructor(a: number, b: number); + + publicFloatProperty : number; + print() : void; + } + } + ``` + +- Modify `/Users/james/NewProject/temp/tsconfig.cocos.json` file + + ```js + { + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2015", + "module": "ES2015", + "strict": true, + "types": [ + "./temp/declarations/cc.custom-macro", + "./temp/declarations/jsb", + "./temp/declarations/cc", + "./temp/declarations/cc.env", + "./my-module" // Add this line + ], + // ...... + "forceConsistentCasingInFileNames": true + } + } + ``` + +- Open NewProject in Cocos Creator, create a cube object in scene and attach a script to cube, the script's content is + + ```ts + import { _decorator, Component } from 'cc'; + const { ccclass } = _decorator; + + @ccclass('MyComponent') + export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyObject(); + myObj.print(); // Invoke native print method + console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); // Get property defined in native + } + } + ``` + +- Run project, if succeed, you could find the following logs in console + + ``` + 17:31:44 [DEBUG]: ==> a: 100, b: 1 + 17:31:44 [DEBUG]: D/ JS: ==> myObj.publicFloatProperty: 1.2300000190734863 + ``` + +#### Section Conclusion + +In this section, we have learned how to use `Swig` tool to bind a simple class, export its public methods and properties to JS. This section also cover the entire flow of binding native classes. Start from next section, we will focus on using more `Swig` features to satisfy more needs of JS bindings, for example: + +- How to import depended header files +- How to ignore classes, methods, properties +- How to rename classes, methods, properties +- How to define attributes which bind c++ getter and setter as a JS property +- How to configure C++ modules in .i file + +### Import depended header files + +Suppose we let MyObject class be inherited from MyRef class. But we don't want to bind MyRef class. + +```c++ +// MyRef.h +#pragma once +namespace my_ns { +class MyRef { +public: + MyRef() = default; + virtual ~MyRef() = default; + void addRef() { _ref++; } + void release() { --_ref; } +private: + unsigned int _ref{0}; +}; +} // namespace my_ns { +``` + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" +namespace my_ns { +// MyObject inherits from MyRef +class MyObject : public MyRef { +public: + MyObject() = default; + MyObject(int a, bool b) {} + virtual ~MyObject() = default; + void print() { + CC_LOG_DEBUG("==> a: %d, b: %d\n", _a, (int)_b); + } + + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; +}; +} // namespace my_ns { +``` + +When Swig parses MyObject.h, it will not know what `MyRef` is, it will output a warning in console. + +```bash +.../Classes/MyObject.h:7: Warning 401: Nothing known about base class 'MyRef'. Ignored. +``` + +It's simple to fix this issue, we need to let Swig know that MyRef exists by using `%import` directive. + +```c++ +// ...... +// Insert code at the beginning of generated source file (.cpp) +%{ +#include "bindings/auto/jsb_my_module_auto.h" +%} + +%import "MyRef.h" // Add this line to fix the warning +%include "MyObject.h" +``` + +Although Swig doesn't report the error now, the binding code will not be compiled, the error is: + +![MyRefCompileError](MyRefCompileError.jpg) + +We fix this in the next section by `%ignore` directive. + +### Ignore classes, methods, properties + +#### Ignore classes + +In last section, we got a compile error in `js_register_my_ns_MyObject`. Since MyRef should not be bound, we could use`%ignore` directive to ignore it. + +```c++ +// my-module.i +// ...... +%ignore my_ns::MyRef; // Add this line +%import "MyRef.h" +%include "MyObject.h" +``` + +Generate binding again, it compiles ok. + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject)); // parentProto will be set to nullptr + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set)); + cls->defineFunction("print", _SE(js_my_ns_MyObject_print)); + // ...... +} +``` + +#### Ignore methods and properties + +We add a new method `methodToBeIgnored` and a new property `propertyToBeIgnored` to `MyObject` class. + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" +namespace my_ns { +// MyObject inherits from MyRef +class MyObject : public MyRef { +public: +// ..... + void methodToBeIgnored() {} // Add this line + float propertyToBeIgnored{345.123F}; // Add this line +// ...... + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; +}; +} // namespace my_ns { + +``` + +Re-generate bindings, we'll get `methodToBeIgnored` and `propertyToBeIgnored` bound. + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject)); + cls->defineProperty("propertyToBeIgnored", _SE(js_my_ns_MyObject_propertyToBeIgnored_get), _SE(js_my_ns_MyObject_propertyToBeIgnored_set)); // this property should not be bound + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set)); + cls->defineFunction("print", _SE(js_my_ns_MyObject_print)); + cls->defineFunction("methodToBeIgnored", _SE(js_my_ns_MyObject_methodToBeIgnored)); // this method should not be bound + // ...... +} +``` + +Modify `my-module.i` to skip binding them. + +```c++ +// my-module.i +// ...... + +%ignore my_ns::MyRef; +%ignore my_ns::MyObject::methodToBeIgnored; // Add this line +%ignore my_ns::MyObject::propertyToBeIgnored; // Add this line + +%import "MyRef.h" +%include "MyObject.h" +``` + +Re-generate bindings, they're ignored now. + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject)); + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set)); + cls->defineFunction("print", _SE(js_my_ns_MyObject_print)); +// ...... +} +``` + +### Rename classes, methods, properties + +Swig has defined a directive called `%rename` to rename classes, methods or properties. To demonstrate, we modify MyObject again. + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" +namespace my_ns { +// MyObject inherits from MyRef +class MyObject : public MyRef { +public: +// ...... + void methodToBeRenamed() { // Add this method + CC_LOG_DEBUG("==> hello MyObject::methodToBeRenamed"); + } + int propertyToBeRenamed{1234}; // Add this property + + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; +}; +} // namespace my_ns { +``` + +Generate bindings, we get: + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject)); + cls->defineProperty("propertyToBeRenamed", _SE(js_my_ns_MyObject_propertyToBeRenamed_get), _SE(js_my_ns_MyObject_propertyToBeRenamed_set)); + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set)); + + cls->defineFunction("print", _SE(js_my_ns_MyObject_print)); + cls->defineFunction("methodToBeRenamed", _SE(js_my_ns_MyObject_methodToBeRenamed)); +``` + +If we want to rename `propertyToBeRenamed` to `coolProperty` and rename `methodToBeRenamed` to `coolMethod`, modify `my-module.i` as follows: + +```c++ +// my-module.i +// ...... +%ignore my_ns::MyRef; +%ignore my_ns::MyObject::methodToBeIgnored; +%ignore my_ns::MyObject::propertyToBeIgnored; +%rename(coolProperty) my_ns::MyObject::propertyToBeRenamed; // Add this line +%rename(coolMethod) my_ns::MyObject::methodToBeRenamed; // Add this line + +%import "MyRef.h" +%include "MyObject.h" +``` + +If we want to rename `MyObject` class to `MyCoolObject`, I guess you have already known how to do. Yes, add this line: + +```c++ +%rename(MyCoolObject) my_ns::MyObject; +``` + +Re-generate bindings, get the correct name exported to JS. + +```c++ +// jsb_my_module_auto.cpp +// MyCoolObject, coolProperty, coolMethod are all what we want now. +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyCoolObject", obj, nullptr, _SE(js_new_MyCoolObject)); + cls->defineProperty("coolProperty", _SE(js_my_ns_MyCoolObject_coolProperty_get), _SE(js_my_ns_MyCoolObject_coolProperty_set)); + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyCoolObject_publicFloatProperty_get), _SE(js_my_ns_MyCoolObject_publicFloatProperty_set)); + cls->defineFunction("print", _SE(js_my_ns_MyCoolObject_print)); + cls->defineFunction("coolMethod", _SE(js_my_ns_MyCoolObject_coolMethod)); + // ...... +} +``` + +Test it, update `my-module.d.ts` and `MyComponent.ts`. The code example is as follows: + +```c++ +// my-module.d.ts +declare namespace my_ns { +class MyCoolObject { + constructor(); + constructor(a: number, b: number); + + publicFloatProperty : number; + print() : void; + coolProperty: number; + coolMethod() : void; +} +} +``` + +```ts +// MyComponent.ts +import { _decorator, Component } from 'cc'; +const { ccclass } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyCoolObject(); // Renamed to MyCoolObject + myObj.print(); + console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); + // Add the follow lines + console.log(`==> old: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolProperty = 666; + console.log(`==> new: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolMethod(); + } +} +``` + +Build and run project, get log: + +``` +17:53:28 [DEBUG]: ==> a: 100, b: 1 +17:53:28 [DEBUG]: D/ JS: ==> myObj.publicFloatProperty: 1.2300000190734863 +17:53:28 [DEBUG]: D/ JS: ==> old: myObj.coolProperty: 1234 +17:53:28 [DEBUG]: D/ JS: ==> new: myObj.coolProperty: 666 +17:53:28 [DEBUG]: ==> hello MyObject::methodToBeRenamed +``` + +### Define an attribute + +`%attribute` directive is used for bind C++ getter and setter functions as a JS property. + +#### Usage + +1. Define an attribute (JS property) without `setter` + + ```c++ + %attribute(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_getter_function_name) + ``` + +2. Define an attribute (JS property) with `getter` and `setter` + + ```c++ + %attribute(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_getter_function_name, cpp_setter_function_name) + ``` + +3. Define an attribute (JS property) without `getter` + + ```c++ + %attribute_writeonly(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_setter_function_name) + ``` + +#### Demo + +To demonstrate, we add two new methods for MyObject class. + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" +namespace my_ns { +// MyObject inherits from MyRef +class MyObject : public MyRef { +public: +// ...... + void setType(int v) { _type = v; CC_LOG_DEBUG("==> setType: v: %d", v); } // Add this line + int getType() const { return _type; } // Add this line + + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; + int _type{333}; +}; +} // namespace my_ns { +``` + +```c++ +// my-module.i +// ...... +%attribute(my_ns::MyObject, int, type, getType, setType); // Add this line + +%import "MyRef.h" +%include "MyObject.h" +``` + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { +// ...... + cls->defineProperty("type", _SE(js_my_ns_MyCoolObject_type_get), _SE(js_my_ns_MyCoolObject_type_set)); +// ...... +} +``` + +```ts +// MyComponent.ts +import { _decorator, Component } from 'cc'; +const { ccclass } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyCoolObject(); + myObj.print(); + console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); + console.log(`==> old: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolProperty = 666; + console.log(`==> new: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolMethod(); + console.log(`==> old: myObj.type: ${myObj.type}`); + myObj.type = 888; + console.log(`==> new: myObj.type: ${myObj.type}`); + } +} +``` + +Build and run project + +``` +18:09:53 [DEBUG]: ==> a: 100, b: 1 +18:09:53 [DEBUG]: D/ JS: ==> myObj.publicFloatProperty: 1.2300000190734863 +18:09:53 [DEBUG]: D/ JS: ==> old: myObj.coolProperty: 1234 +18:09:53 [DEBUG]: D/ JS: ==> new: myObj.coolProperty: 666 +18:09:53 [DEBUG]: ==> hello MyObject::methodToBeRenamed +18:09:53 [DEBUG]: D/ JS: ==> old: myObj.type: 333 +18:09:53 [DEBUG]: ==> setType: v: 888 // Cool, C++ setType is invoked +18:09:53 [DEBUG]: D/ JS: ==> new: myObj.type: 888 // Cool, C++ getType is invoked, 888 is return from C++ +``` + +#### %attribute_writeonly directive + +`%attribute_writeonly` directive is an extension we added in swig `Cocos` backend, it's used for the purpose that C++ class only has a `set` function and there isn't a `get` function. + +In `native/tools/swig-config/cocos.i`, there are: + +```c++ +%attribute_writeonly(cc::ICanvasRenderingContext2D, float, width, setWidth); +%attribute_writeonly(cc::ICanvasRenderingContext2D, float, height, setHeight); +%attribute_writeonly(cc::ICanvasRenderingContext2D, float, lineWidth, setLineWidth); +%attribute_writeonly(cc::ICanvasRenderingContext2D, ccstd::string&, fillStyle, setFillStyle); +%attribute_writeonly(cc::ICanvasRenderingContext2D, ccstd::string&, font, setFont); +``` + +This is the similar functionality in JS: + +```javascript +Object.defineProperty(MyNewClass.prototype, 'width', { + configurable: true, + enumerable: true, + set(v) { + this._width = v; + }, + // No get() for property +}); +``` + +#### Reference type + +If C++ `get` function returns a reference data type or `set` function accesses a reference data type, don't forget to add `&` suffix in %attribute or %attribute_writeonly directives. The following `ccstd::string&` is an example. + +```c++ +%attribute_writeonly(cc::ICanvasRenderingContext2D, ccstd::string&, fillStyle, setFillStyle); +``` + +If `&` is missing, a temporary `ccstd::string` instance will be created while the binding function is invoked. + +#### %arg() directive + +Sometimes, the type of C++ variable is a describled by C++ template, for instance: + +```c++ +class MyNewClass { + public: + const std::map& getConfig() const { return _config; } + void setConfig(const std::map &config) { _config = config; } + private: + std::map _config; +}; +``` + +We may write an `%attribute` in `.i` file like: + +```c++ +%attribute(MyNewClass, std::map&, config, getConfig, setConfig); +``` + +You will get an error while invoking `node genbindings.js`. + +``` +Error: Macro '%attribute_custom' expects 7 arguments +``` + +This is because `swig` doesn't know how to deal with comma (`,`) in `std::map&`, it will split it to two parts: + +1. std::map& + +Therefore, this line of %attribute directive will be parsed with 6 arguments instead of 5. + +To avoid making `swig` confused, we need to use `%arg` directive to tell `swig` that `std::map&` is a complete declaration. + +```c++ +%attribute(MyNewClass, %arg(std::map&), config, getConfig, setConfig); +``` + +Re-run `node genbindings.js`, the error will no longer be reported. + +#### Don't add `const` + +In the above sample, `%arg(std::map&)` is used as a C++ data type in %attribue directive. You may consider to add a `const` prefix before `std::map` like `%arg(const std::map&)`. If you do that, you will make a readyonly `config` property which only binds `MyNewClass::getConfig`. That's obviously not what we expect. If we need a readonly property, just don't assign a `set` function. + +```c++ +// Don't assign setConfig means the property doesn't need a setter. +%attribute(MyNewClass, %arg(std::map&), config, getConfig); +``` + +So to keep things simple, never add `const` prefix while writing a `%attribute` directive. + +### Configure C++ modules in .i file + +Sometimes, whether to compile a class depends on whether a macro is enabled. For example, we add a `MyFeatureObject` class in `MyObject.h` + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" + +#ifndef USE_MY_FEATURE +#define USE_MY_FEATURE 1 // Enable USE_MY_FEATURE +#endif + +namespace my_ns { + +#if USE_MY_FEATURE +class MyFeatureObject { +public: + void foo() { + CC_LOG_DEBUG("==> MyFeatureObject::foo"); + } +}; +#else +class MyFeatureObject; +#endif + +// MyObject inherits from MyRef +class MyObject : public MyRef { +public: +//...... + MyFeatureObject* getFeatureObject() { +#if USE_MY_FEATURE // getFeatureObject only returns valid value when USE_MY_FEATURE is enabled + if (_featureObject == nullptr) { + _featureObject = new MyFeatureObject(); + } +#endif + return _featureObject; + } +private: + int _a{100}; + bool _b{true}; + int _type{333}; + MyFeatureObject* _featureObject{nullptr}; // Add this line +}; +} // namespace my_ns { +``` + +```c++ +// my-module.i +// ...... +%rename(MyCoolObject) my_ns::MyObject; + +%attribute(my_ns::MyObject, int, type, getType, setType); + +%module_macro(USE_MY_FEATURE) my_ns::MyFeatureObject; // Add this line to let Swig know the generated code for MyFeatureObject needs to be wrapped by USE_MY_FEATURE macro +%module_macro(USE_MY_FEATURE) my_ns::MyObject::getFeatureObject; // Add this line to let Swig know the generated code for MyObject::getFeatureObject should be wrapped by USE_MY_FEATURE macro + +#define USE_MY_FEATURE 1 // Must be 1 to trick Swig that we need to generate binding code +// even this macro is disabled in C++. NOTE: this line should be after %module_macro + +%import "MyRef.h" +%include "MyObject.h" +``` + +```c++ +// my-module.d.ts +declare namespace my_ns { +class MyFeatureObject { + foo() : void; +} + +class MyCoolObject { + constructor(); + constructor(a: number, b: number); + + publicFloatProperty : number; + print() : void; + coolProperty: number; + coolMethod() : void; + type: number; + getFeatureObject() : MyFeatureObject; +} +} +``` + +```ts +// MyComponent.ts +import { _decorator, Component } from 'cc'; +const { ccclass } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyCoolObject(); + myObj.print(); + console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); + console.log(`==> old: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolProperty = 666; + console.log(`==> new: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolMethod(); + console.log(`==> old: myObj.type: ${myObj.type}`); + myObj.type = 888; + console.log(`==> new: myObj.type: ${myObj.type}`); + const featureObj = myObj.getFeatureObject(); + console.log(`==> featureObj: ${featureObj}`); + if (featureObj) { + featureObj.foo(); + } + } +} +``` + +After generating bindings, the binding code is as follows: + +```c++ +#if USE_MY_FEATURE // NOTE THAT, all binding code of MyFeatureObject is wrapped by USE_MY_FEATURE macro + +se::Class* __jsb_my_ns_MyFeatureObject_class = nullptr; +se::Object* __jsb_my_ns_MyFeatureObject_proto = nullptr; +SE_DECLARE_FINALIZE_FUNC(js_delete_my_ns_MyFeatureObject) + +static bool js_my_ns_MyFeatureObject_foo(se::State& s) +{ +// ...... +} +// ...... +bool js_register_my_ns_MyFeatureObject(se::Object* obj) { + auto* cls = se::Class::create("MyFeatureObject", obj, nullptr, _SE(js_new_my_ns_MyFeatureObject)); +// ...... +} + +#endif // USE_MY_FEATURE + +// ...... +static bool js_my_ns_MyCoolObject_getFeatureObject(se::State& s) +{ +#if USE_MY_FEATURE // getFeatureObject function is also wrapped by USE_MY_FEATURE +// ...... + ok &= nativevalue_to_se(result, s.rval(), s.thisObject() /*ctx*/); + SE_PRECONDITION2(ok, false, "MyCoolObject_getFeatureObject, Error processing arguments"); + SE_HOLD_RETURN_VALUE(result, s.thisObject(), s.rval()); +#endif // USE_MY_FEATURE + return true; +} +SE_BIND_FUNC(js_my_ns_MyCoolObject_getFeatureObject) + +// ...... +bool register_all_my_module(se::Object* obj) { + // Get the ns + se::Value nsVal; + if (!obj->getProperty("my_ns", &nsVal, true)) + { + se::HandleObject jsobj(se::Object::createPlainObject()); + nsVal.setObject(jsobj); + obj->setProperty("my_ns", nsVal); + } + se::Object* ns = nsVal.toObject(); + /* Register classes */ +#if USE_MY_FEATURE + js_register_my_ns_MyFeatureObject(ns); // js_register_my_ns_MyFeatureObject is wrapped by USE_MY_FEATURE +#endif // USE_MY_FEATURE + js_register_my_ns_MyObject(ns); + return true; +} +``` + +Build and run the project, the output is as follows: + +``` +18:32:20 [DEBUG]: D/ JS: ==> featureObj: [object Object] // featureObj is valid if USE_MY_FEATURE macro is enabled +18:32:20 [DEBUG]: ==> MyFeatureObject::foo // Invoke C++ foo method +``` + +When we don't need `MyFeatureObject`, assign the macro to 0, the code is as follows: + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" + +#ifndef USE_MY_FEATURE +#define USE_MY_FEATURE 0 // Disable USE_MY_FEATURE +#endif +``` + +Build and run the project, the following output can be seen. + +``` +18:54:00 [DEBUG]: D/ JS: ==> featureObj: undefined // getFeatureObject returns undefined if USE_MY_FEATURE is disabled. +``` + +### Multiple swig modules configuration + +Let's create another header file `MyAnotherObject.h`. + +```c++ +// MyAnotherObject.h +#pragma once +namespace my_another_ns { +struct MyAnotherObject { + float a{135.246}; + int b{999}; +}; +} // namespace my_another_ns { +``` + +Update `MyObject.h`: + +```c++ +// MyObject.h +//...... +class MyObject : public MyRef { +public: +// ...... + void helloWithAnotherObject(const my_another_ns::MyAnotherObject &obj) { + CC_LOG_DEBUG("==> helloWithAnotherObject, a: %f, b: %d", obj.a, obj.b); + } +// ...... +}; +} // namespace my_ns { +``` + +Create `/Users/james/NewProject/tools/swig-config/another-module.i` + +```c++ +// another-module.i +%module(target_namespace="another_ns") another_module + +// Insert code at the beginning of generated header file (.h) +%insert(header_file) %{ +#pragma once +#include "bindings/jswrapper/SeApi.h" +#include "bindings/manual/jsb_conversions.h" + +#include "MyAnotherObject.h" // Add this line +%} + +// Insert code at the beginning of generated source file (.cpp) +%{ +#include "bindings/auto/jsb_another_module_auto.h" +%} + +%include "MyAnotherObject.h" +``` + +Modify `/Users/james/NewProject/tools/swig-config/swig-config.js` + +```c++ +'use strict'; + +const path = require('path'); + +const configList = [ + [ 'my-module.i', 'jsb_my_module_auto.cpp' ], + [ 'another-module.i', 'jsb_another_module_auto.cpp' ], // Add this line +]; + +const projectRoot = path.resolve(path.join(__dirname, '..', '..')); +const interfacesDir = path.join(projectRoot, 'tools', 'swig-config'); +const bindingsOutDir = path.join(projectRoot, 'native', 'engine', 'common', 'bindings', 'auto'); +const includeDirs = [ + path.join(projectRoot, 'native', 'engine', 'common', 'Classes'), +]; + +module.exports = { + interfacesDir, + bindingsOutDir, + includeDirs, + configList +}; +``` + +Modify `/Users/james/NewProject/native/engine/common/CMakeLists.txt` + +```cmake +# /Users/james/NewProject/native/engine/common/CMakeLists.txt +list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp + ${CMAKE_CURRENT_LIST_DIR}/Classes/MyObject.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/MyAnotherObject.h # Add this line + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.h + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.cpp + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_another_module_auto.h # Add this line + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_another_module_auto.cpp # Add this line +) +``` + +Generate bindings again. + +Update `Game.cpp`: + +```c++ +#include "Game.h" +#include "bindings/auto/jsb_my_module_auto.h" +#include "bindings/auto/jsb_another_module_auto.h" // Add this line +//...... + +int Game::init() { +//...... + se::ScriptEngine::getInstance()->addRegisterCallback(register_all_my_module); + se::ScriptEngine::getInstance()->addRegisterCallback(register_all_another_module); // Add this line +// + BaseGame::init(); + return 0; +} +``` + +Build and compile, but the following errors will be reported: + +![another-module-compile-error](another-module-compile-error.jpg) + +Since MyObject class depends on MyAnotherObject which is defined on another module. We need to update `my-module.i` and add `#include "bindings/auto/jsb_another_module_auto.h"`. + +```c++ +// my-module.i +%module(target_namespace="my_ns") my_module + +// Insert code at the beginning of generated header file (.h) +%insert(header_file) %{ +#pragma once +#include "bindings/jswrapper/SeApi.h" +#include "bindings/manual/jsb_conversions.h" + +#include "MyObject.h" +%} + +// Insert code at the beginning of generated source file (.cpp) +%{ +#include "bindings/auto/jsb_my_module_auto.h" +#include "bindings/auto/jsb_another_module_auto.h" // Add this line +%} + +// ...... +``` + +Compile project. It should compile success now. + +Next, we update .d.ts + +```ts +// my-module.d.ts +declare namespace my_ns { +class MyFeatureObject { + foo() : void; +} + +class MyCoolObject { + constructor(); + constructor(a: number, b: number); + + publicFloatProperty : number; + print() : void; + coolProperty: number; + coolMethod() : void; + type: number; + getFeatureObject() : MyFeatureObject; + helloWithAnotherObject(obj: another_ns.MyAnotherObject) : void; // Add this line +} +} + +// Add the following lines +declare namespace another_ns { +class MyAnotherObject { + a: number; + b: number; +} +} +``` + +We add some more test code of reading the properties of `MyAnotherObject.` + +```ts +// MyComponent.ts +import { _decorator, Component } from 'cc'; +const { ccclass } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyCoolObject(); + // ...... + const anotherObj = new another_ns.MyAnotherObject(); // Add this line + myObj.helloWithAnotherObject(anotherObj); // Add this line + } +} +``` + +Build and run project, the following output should be seen. + +``` +15:05:36 [DEBUG]: ==> helloWithAnotherObject, a: 135.246002, b: 999 +``` diff --git a/versions/4.0/en/advanced-topics/jsb/v8-win32-debug.jpg b/versions/4.0/en/advanced-topics/jsb/v8-win32-debug.jpg new file mode 100644 index 0000000000..40b6aac3cd Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/v8-win32-debug.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/v8-win32-memory.jpg b/versions/4.0/en/advanced-topics/jsb/v8-win32-memory.jpg new file mode 100644 index 0000000000..54f8ba2a7c Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/v8-win32-memory.jpg differ diff --git a/versions/4.0/en/advanced-topics/jsb/v8-win32-profile.jpg b/versions/4.0/en/advanced-topics/jsb/v8-win32-profile.jpg new file mode 100644 index 0000000000..3297816571 Binary files /dev/null and b/versions/4.0/en/advanced-topics/jsb/v8-win32-profile.jpg differ diff --git a/versions/4.0/en/advanced-topics/mangle-properties.md b/versions/4.0/en/advanced-topics/mangle-properties.md new file mode 100644 index 0000000000..9f5985ddd8 --- /dev/null +++ b/versions/4.0/en/advanced-topics/mangle-properties.md @@ -0,0 +1,72 @@ +# Mangle Engine Internal Properties + +To help users further reduce game package size, the Cocos Engine team has introduced engine internal property mangling functionality starting from version 3.8.6. This feature can mangle properties in the engine's TypeScript code, effectively reducing game package size without compromising game performance. + +> Note: This feature currently does not support native platforms. + +## Enable Engine Property Mangling During Build + +![open-mangle-properties](mangle-properties/open-mangle-properties.png) + +## Generate Mangle Configuration + +During the build process, an `engine-mangle-config.json` file will be generated in the project root directory. If users want to quickly generate this configuration file, they can start a build and interrupt the build task after the configuration file is generated in the project root directory. + +## Mangle Configuration Fields + +By default, this feature mangles private properties in the engine's TypeScript code. If you need to mangle public and protected properties, you'll need to configure this in the configuration file. + +| Field | Description | +| :-------------- | :----------- | +| mangleProtected | Whether to mangle protected properties (default is false) | +| mangleList | Add engine properties that need to be mangle | +| dontMangleList | Add engine properties that should not be mangle | + +### Configuration Example + +#### "Profiler._meshRenderer" is a custom added property, while others are default mangle configurations. + +```json +{ + "COMMON": { + "mangleProtected": false, + "mangleList": [ + "UITransform._sortSiblings", + "UITransform._cleanChangeMap", + "Node._findComponents", + "Node._findChildComponent", + "Node._findChildComponents", + "Node.idGenerator", + "Node._stacks", + "Node._stackId", + "Node._setScene", + "EffectAsset._layoutValid", + "EffectAsset._effects", + "ReflectionProbe.DEFAULT_CUBE_SIZE", + "ReflectionProbe.DEFAULT_PLANER_SIZE", + "WebGLDeviceManager.setInstance", + "WebGL2DeviceManager.setInstance", + "CanvasPool", + + ], + "dontMangleList": [ + "Component", + "Profiler._meshRenderer" + ] + }, + "MINIGAME": { + "extends": "COMMON", + "mangleList": [], + "dontMangleList": [] + }, + "WECHAT": { + "extends": "MINIGAME", + "mangleList": [], + "dontMangleList": [] + } +} +``` + +## Important Notes + +When this feature is enabled and debug mode is active during packaging, it will slightly increase the size of the debug package. \ No newline at end of file diff --git a/versions/4.0/en/advanced-topics/mangle-properties/open-mangle-properties.png b/versions/4.0/en/advanced-topics/mangle-properties/open-mangle-properties.png new file mode 100644 index 0000000000..a6e41befec Binary files /dev/null and b/versions/4.0/en/advanced-topics/mangle-properties/open-mangle-properties.png differ diff --git a/versions/4.0/en/advanced-topics/memory-leak-detector.md b/versions/4.0/en/advanced-topics/memory-leak-detector.md new file mode 100644 index 0000000000..5e5c53024f --- /dev/null +++ b/versions/4.0/en/advanced-topics/memory-leak-detector.md @@ -0,0 +1,57 @@ +# Native Engine Memory Leak Detection System + +The native engine is developed using the C++ language. In order to facilitate game & engine developers to quickly find memory leaks, Cocos Creator provides a **memory leak detection system** since v3.4.0. + +Compared with other memory leak detection tools, the built-in memory leak detection tool in Cocos Creator has the following advantages: + +- **Cross-platform**: support Windows/Android/Mac/iOS platforms. +- **Ease of use**: no need to download additional tools and perform complex configurations. Support output of stack information at memory leaks, which is convenient for quickly locating leaks. +- **Consistency**: the usage process of each platform is almost the same: start the game from the IDE -> run for a period of time -> close the game -> view the IDE output log. +- **Real-time**: although the frame rate of the game in the profiling mode has dropped, it still maintains the real-time running frame rate. +- **Accuracy**: theoretically zero false positives. + +## Usage steps + +1. The memory leak detection system is disabled by default. To enable it, you need to modify the value of the macro `USE_MEMORY_LEAK_DETECTOR` in the `engine/native/cocos/base/Config.h` file of the engine directory to **1**. + + ```c++ + #ifndef USE_MEMORY_LEAK_DETECTOR + #define USE_MEMORY_LEAK_DETECTOR 1 + #endif + ``` + +2. The Android platform requires one additional step as a result of the different implementation mechanisms amongst platforms: + + Add a line of code `set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -finstrument-functions")` to the `native/engine/android/CMakeLists.txt` file in the project directory, as follows: + + ``` + set(CC_PROJ_SOURCES) + set(CC_COMMON_SOURCES) + set(CC_ALL_SOURCES) + + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -finstrument-functions") + ``` + +3. Start the game from the IDE corresponding to the native platform (such as Visual Studio, Android Studio, Xcode), and close the game after running for a period of time. If there is any memory leak, the detailed information of the memory leak will be output in the output window of the IDE at this time. + + - **Windows platform** + + ![visual studio](./memory-leak-detector/visualstudio.png) + + In the Release version, if more friendly stack information is needed, right-click the executable project-properties, open the project properties page, and make the following settings: + + - Linker -> Debugging -> Generate Debug Info: Generate Debug Information(/DEBUG) + + - C/C++ -> Optimization -> Optimization: Disabled(/Od)\ + + - C/C++ -> Optimization -> Inline Function Expansion:Disabled(/Ob0) + + - **Android platform** + + ![android studio](./memory-leak-detector/androidstudio.png) + + - **Mac/iOS platform** + + ![xcode](./memory-leak-detector/xcode.png) + +4. Fix the leak according to the information output by the Native platform IDE, and repeat until there is no leak. diff --git a/versions/4.0/en/advanced-topics/memory-leak-detector/androidstudio.png b/versions/4.0/en/advanced-topics/memory-leak-detector/androidstudio.png new file mode 100644 index 0000000000..ba37bb8981 Binary files /dev/null and b/versions/4.0/en/advanced-topics/memory-leak-detector/androidstudio.png differ diff --git a/versions/4.0/en/advanced-topics/memory-leak-detector/visualstudio.png b/versions/4.0/en/advanced-topics/memory-leak-detector/visualstudio.png new file mode 100644 index 0000000000..87c3998f88 Binary files /dev/null and b/versions/4.0/en/advanced-topics/memory-leak-detector/visualstudio.png differ diff --git a/versions/4.0/en/advanced-topics/memory-leak-detector/xcode.png b/versions/4.0/en/advanced-topics/memory-leak-detector/xcode.png new file mode 100644 index 0000000000..cd9b357773 Binary files /dev/null and b/versions/4.0/en/advanced-topics/memory-leak-detector/xcode.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/brief.md b/versions/4.0/en/advanced-topics/native-plugins/brief.md new file mode 100644 index 0000000000..a75143e986 --- /dev/null +++ b/versions/4.0/en/advanced-topics/native-plugins/brief.md @@ -0,0 +1,111 @@ +# Native Plugins + +> **Note**: +> +> * Please use the native plugin feature in CocosCreator 3.6.3 and above. +> * The native plugin feature is not yet adapted for HarmonyOS related platforms. + +Native plugins are part of the editor plugin system. Developers can use native plugins to call script binding interfaces (such as sebind) to extend the capability of JS scripts calling C++ interfaces, which is very beneficial for solving script performance bottlenecks and reusing existing codebases. + +## Relationship with Existing Plugin System + +Native plugins can exist independently of editor plugins. Users can use native plugins by copying them to specified directories. + +At the same time, native plugins also serve as a supplement to the existing editor plugin system, extending the capabilities of the game runtime. They leverage the capabilities of editor plugins to manage native plugins, such as: download/on-off switching/version upgrade and other functions. + +### Plugin Structure + +Each plugin has a plugin description file `cc_plugin.json` in its root directory, which is a standard JSON file. + +When building native projects, the build system will recursively search for all `cc_plugin.json` files from the project's `extensions` and `native` directories to locate native plugins. Once `cc_plugins.json` is found in a directory, it will not search subdirectories further. + +## Installing Dependencies + +In a few environments where the editor is not installed, [NodeJS](https://nodejs.org/en/download/) version 8.0 or above needs to be installed to support plugin configuration parsing. Developers can set the NodeJS environment variable `PATH`, or specify it by setting `NODE_EXECUTABLE` in `CMakeLists.txt`. + +You can also set the environment variable `NODE_EXECUTABLE` to the full path of node. Starting from version 3.6.2, if CMake still cannot locate nodejs, you can directly set `NODE_EXECUTABLE` in `native/engine/common/localCfg.cmake`. + +## Basic Directory Structure Example + +``` +├── cc_plugin.json +├── android +│ ├── arm64-v8a +│ ├── armeabi-v7a +│ ├── x86 +│ └── x86_64 +│__ google-play +│ ├── arm64-v8a +│ ├── armeabi-v7a +│ ├── x86 +│ └── x86_64 +├── ios +│ ├── include +│ └── lib +├── mac +│ ├── include +│ └── lib +│── windows +│ ├── include +│ └── lib +``` + +The file `cc_plugin.json` provides the necessary information for loading plugins and serves as the identifier for native plugins. Each supported native platform corresponds to a directory, which contains at least one `-Config.cmake` file. The build system uses CMake's [`find_package`](https://cmake.org/cmake/help/latest/command/find_package.html#id7) mechanism to locate or link to the required library files. + +If there are cross-platform source files or CMake configurations in the plugin, these files can be merged into the top-level directory. For details, please refer to the [example project](https://github.com/PatriceJiang/ccplugin_tutorial/tree/main/NewProject/native/plugins/hello_cocos). + +## Description File `cc_plugin.json` Format + +```ts +{ + "name": string; // Required: Plugin name + "version": string; // Required: Plugin version + "engine-version":string; // Required: Corresponding engine version range + "author": string; // Required: Plugin author + "description": string; // Required: Plugin description + "platforms":string[]; // Optional: Supported platform list, defaults to all native platforms if not specified. Includes windows, android, google-play, mac, ios + "disabled":true; // Optional: Disable plugin + "disable-by-platforms":string[]; // Optional: Disable plugin on specified platforms + "modules": [{ // Required: Libraries included in the plugin + "target":string; // Required: Corresponding `find_package` name, must be consistent with the first parameter of `CC_PLUGIN_ENTRY` + "depends": string|string[]; // Optional: Dependencies on other module names + "platforms":string[]; // Optional: Re-specify supported native platforms + }] + +} +``` + +`engine-version` can specify version ranges and exclude specific versions. Code example as follows: + +```ts +"engine-version": ">=3.3 <= 3.6.0 !3.5.2|| 4.x" +``` + +### File Example + +```json +{ + "name":"hello-cocos-demo", + "version":"1.0.0", + "author":"cocos", + "engine-version":">=3.6.3", + "disabled":false, + "modules":[ + { + "target":"hello_cocos_glue" + } + ], + "platforms":["windows", "android", "mac", "ios", "google-play"] +} +``` +## Creating Native Plugins + +Cocos native projects use CMake for management. Native plugins are managed through the search paths/directories of find_package, so as long as the directory conforms to CMake find_package search rules, the plugin can be loaded correctly. Therefore, the development process of native plugins involves providing CMake configurations and related resources, as well as writing cc_plugin.json. For related examples, please refer to [Native Plugin Creation and Usage Example](./tutorial.md). + +## Installing and Disabling Native Plugins + +Download the pre-built plugin package from the [Native Plugin Creation and Usage Example](https://github.com/zhefengzhang/cocos-native-plugins), then select a folder and copy it to the `extensions` or `native` directory according to your project requirements. + +If you want to disable the plugin, or disable it only on specific platforms, you can modify the `disabled` and `disable-by-platforms` fields in cc_plugin.json. + +> **Note**: Native plugins require CMake 3.12+. Android needs to [specify CMake version](https://developer.android.com/studio/projects/install-ndk#vanilla_cmake) as 3.18.1. Other platforms use the built-in CMake in the editor, so the version number does not need to be specified. diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/1_2_save_emtpy_scene.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/1_2_save_emtpy_scene.png new file mode 100644 index 0000000000..1b0cdd7053 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/1_2_save_emtpy_scene.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/1_3_create_windows_build.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/1_3_create_windows_build.png new file mode 100644 index 0000000000..b0ad424636 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/1_3_create_windows_build.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/1_create_empty_project.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/1_create_empty_project.png new file mode 100644 index 0000000000..b9fb948ee2 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/1_create_empty_project.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_1_link_error.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_1_link_error.png new file mode 100644 index 0000000000..14d679daf9 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_1_link_error.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_1_vs_project.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_1_vs_project.png new file mode 100644 index 0000000000..566a607565 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_1_vs_project.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_3_debug_url.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_3_debug_url.png new file mode 100644 index 0000000000..d44f661a3e Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_3_debug_url.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_3_empty_window.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_3_empty_window.png new file mode 100644 index 0000000000..1f4ca1f600 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_3_empty_window.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_4_devtool.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_4_devtool.png new file mode 100644 index 0000000000..6756351ddc Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_4_devtool.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_5_devtool.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_5_devtool.png new file mode 100644 index 0000000000..6d9dd69bd6 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/2_5_devtool.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/doc/images/3_1_android_build.png b/versions/4.0/en/advanced-topics/native-plugins/doc/images/3_1_android_build.png new file mode 100644 index 0000000000..0aa1a08654 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-plugins/doc/images/3_1_android_build.png differ diff --git a/versions/4.0/en/advanced-topics/native-plugins/tutorial.md b/versions/4.0/en/advanced-topics/native-plugins/tutorial.md new file mode 100644 index 0000000000..8ac117fe7b --- /dev/null +++ b/versions/4.0/en/advanced-topics/native-plugins/tutorial.md @@ -0,0 +1,387 @@ +# Native Plugin Creation Example + +If you want to use third-party native libraries in native projects, you can follow the steps in this article. + +This article requires some understanding of native project compilation and generation. Developers can learn about it through the [CMake official website](https://cmake.org/). We have also prepared a [Native Plugin Creation and Usage Example](https://github.com/zhefengzhang/cocos-native-plugins) for reference. + +## Creating Native Plugins + +### Compiling Dependent Libraries or Static Libraries + +Use the compilation tools provided by the application platform to compile c or cpp files into .lib or .a files. In the [Native Plugin Creation and Usage Example](https://github.com/zhefengzhang/cocos-native-plugins) repository, the hello_cocos.cpp file in the src folder has been compiled into .lib and .a files and added to the plugin directories of various platforms. The jni directory in the repository provides the configuration and code used when compiling .a files with the `ndk-build` command on the Android platform for developers' reference. Please compile for other platforms yourself. + +### Windows Configuration for Plugin Development Project + +In this example, we will introduce hello_cocos.lib as a plugin on the Windows platform, integrate it into the engine, and make it available for use in TS/JS. Other platforms will use hello_cocos.a as an example. If you want to use other libraries, please compile them for the corresponding platform in advance. + +- Create a project using Cocos Creator 3.6.3 or higher + + Start CocosCreator and execute `Create Empty Project` in the specified directory. + + ![create](doc/images/1_create_empty_project.png) + +- Create and save an empty scene + + ![save scene](doc/images/1_2_save_emtpy_scene.png) + +- Export the native project through the **Build & Publish** panel and build it to generate the `native/` directory + + Here we create a new build task for Windows. + + ![build windows](doc/images/1_3_create_windows_build.png) + + Execute **Build**, which will also generate the `native/` directory. + + View the contents of the directory through the console (Windows CMD or PowerShell or similar software): + + ```console + $ tree native/ -L 2 + native/ + └── engine + ├── common + └── win64 + + ``` + +- Create a directory for plugin storage in `native/` + + ```console + mkdir -p native/native-plugin/ + ``` + +### Adding Native Plugin Support for Windows + +- Add Windows platform-related subdirectories: + + ```console + mkdir -p native/native-plugin/windows/ + ``` + +- Copy the pre-compiled dependency library `hello_cocos.lib` and header files to the corresponding directory: + + ```console + $ tree native/native-plugin/ + + native/native-plugin/ + ├── include + │ └── hello_cocos.h + └── windows + └── lib + ├── hello_cocos.lib + └── hello_cocosd.lib + ``` + +- Add files `hello_cocos_glue.cpp`, `CMakeLists.txt` and `hello_cocos_glue-config.cmake`: + + ```console + mkdir native/native-plugin/src + touch native/native-plugin/src/hello_cocos_glue.cpp + touch native/native-plugin/src/CMakeLists.txt + touch native/native-plugin/hello_cocos_glue-config.cmake + ``` + + Current plugin directory contents: + + ```console + $ tree native/native-plugin/ + native/native-plugin/ + ├── include + │ └── hello_cocos.h + ├── src + │ ├── CMakeLists.txt + │ └── hello_cocos_glue.cpp + └── windows + ├── hello_cocos_glue-config.cmake + └── lib + ├── hello_cocos.lib + └── hello_cocosd.lib + ``` + +- Edit `hello_cocos_glue-config.cmake` to add declarations for `hello_cocos.lib` and imported content: + + ```cmake + set(_hello_cocos_GLUE_DIR ${CMAKE_CURRENT_LIST_DIR}) + + add_library(hello_cocos STATIC IMPORTED GLOBAL) + set_target_properties(hello_cocos PROPERTIES + IMPORTED_LOCATION ${_hello_cocos_GLUE_DIR}/lib/hello_cocos.lib + IMPORTED_LOCATION_DEBUG ${_hello_cocos_GLUE_DIR}/lib/hello_cocosd.lib + ) + + include(${_hello_cocos_GLUE_DIR}/../src/CMakeLists.txt) + ``` + +- Edit `native/native-plugin/src/CMakeLists.txt` and add the following content: + + ```cmake + set(_hello_cocos_GLUE_SRC_DIR ${CMAKE_CURRENT_LIST_DIR}) + + add_library(hello_cocos_glue ${_hello_cocos_GLUE_SRC_DIR}/hello_cocos_glue.cpp) + + target_link_libraries(hello_cocos_glue + hello_cocos + ${ENGINE_NAME} # cocos_engine + ) + + target_include_directories(hello_cocos_glue PRIVATE + ${_hello_cocos_GLUE_SRC_DIR}/../include + ) + ``` + +- Create configuration file `cc_plugin.json` in directory `native/native-plugin/` + + ```json + { + "name":"hello-cocos-demo", + "version":"1.0.0", + "author":"cocos", + "engine-version":">=3.6.3", + "disabled":false, + "modules":[ + { + "target":"hello_cocos_glue" + } + ], + "platforms":["windows"] + } + ``` + + The files required for the native plugin have now been created, but they cannot be compiled yet. The file `hello_cocos_glue.cpp` needs to register the plugin's initialization function. + + Execute **Build** again to trigger the update of the Visual Studio project. + +- Open the sln file in directory `build/windows/proj/` with Visual Studio + + - A `plugin_registry` target is automatically generated for initializing all enabled plugins: + + ![Solution Explorer](./doc/images/2_1_vs_project.png) + + - Directly running the target will result in similar error reports: + + ![link error](./doc/images/2_1_link_error.png) + +- Edit `hello_cocos_glue.cpp` + + ```c++ + #include "hello_cocos.h" + #include "bindings/sebind/sebind.h" + #include "plugins/bus/EventBus.h" + #include "plugins/Plugins.h" + + // export c++ methods to JS + static bool register_demo(se::Object *ns) { + + sebind::class_ klass("Demo"); + + klass.constructor() + .function("hello", &Demo::hello); + klass.install(ns); + return true; + } + + void add_demo_class() { + using namespace cc::plugin; + static Listener listener(BusType::SCRIPT_ENGINE); + listener.receive([](ScriptEngineEvent event) { + if (event == ScriptEngineEvent::POST_INIT) { + se::ScriptEngine::getInstance()->addRegisterCallback(register_demo); + } + }); + } + + /** + * Regist a new cc plugin entry function + * first param: should match the name in cc_plugin.json + * second param: callback when engine initialized + */ + CC_PLUGIN_ENTRY(hello_cocos_glue, add_demo_class); + ``` + + After compiling again, there are no more errors, and the project can now be compiled and run correctly. + +- Run the target project: + + ![empty window](./doc/images/2_3_empty_window.png) + +- To verify whether our native plugin has been loaded, we need to connect to devtools: + + From the `Output` panel, obtain the debugging connection. + + ![debug url](./doc/images/2_3_debug_url.png) + + Open the browser, enter the debugging link address from the image above, and type the following code in the console: + + ```javascript + new Demo("World").hello("Cocos") + ``` + + ![devtools](./doc/images/2_5_devtool.png) + + Based on the output, it can be confirmed that our interface has been successfully exported through the native plugin. + +### Adding Native Plugin Support for Android + +- Add Android build task + +- Create Android-related native plugin directory + + ```console + mkdir native/native-plugin/android + ``` + +- Copy pre-compiled dependency libraries and header files to the corresponding directory, create `hello_cocos_glue-config.cmake` + + Android directory status: + + ```console + $ tree native/native-plugin/android/ + native/native-plugin/android/ + ├── hello_cocos_glue-config.cmake + ├── arm64-v8a + │ └── lib + │ └── libhello_cocos.a + └── armeabi-v7a + └── lib + └── libhello_cocos.a + + ``` + +- Edit `hello_cocos_glue-config.cmake` + + ```cmake + set(_hello_cocos_GLUE_DIR ${CMAKE_CURRENT_LIST_DIR}) + + add_library(hello_cocos STATIC IMPORTED GLOBAL) + set_target_properties(hello_cocos PROPERTIES + IMPORTED_LOCATION ${_hello_cocos_GLUE_DIR}/${ANDROID_ABI}/lib/libhello_cocos.a + ) + + include(${_hello_cocos_GLUE_DIR}/../src/CMakeLists.txt) + ``` + +- Update `cc_plugin.json`, add `android` to the `platforms` field + + ```json + { + "name":"hello-cocos-demo", + "version":"1.0.0", + "author":"cocos", + "engine-version":">=3.6.3", + "disabled":false, + "modules":[ + { + "target":"hello_cocos_glue" + } + ], + "platforms":["windows", "android"] + } + + ``` + +- Add new Android build task + + ![Android build](./doc/images/3_1_android_build.png) + +After building, you can open the project with Android Studio and use devtool for debugging verification. + +### Adding Native Plugin Support for iOS + +- Add iOS build task + +- Create iOS-related native plugin directory + + ``` + mkdir -p native/native-plugin/ios/lib + ``` + +- Copy pre-compiled dependency libraries and header files to the corresponding directory, create `hello_cocos_glue-config.cmake`, edit according to the following example: + + ```cmake + set(_hello_cocos_GLUE_DIR ${CMAKE_CURRENT_LIST_DIR}) + + + add_library(hello_cocos STATIC IMPORTED GLOBAL) + set_target_properties(hello_cocos PROPERTIES + IMPORTED_LOCATION ${_hello_cocos_GLUE_DIR}/lib/libhello_cocos.a + ) + + include(${_hello_cocos_GLUE_DIR}/../src/CMakeLists.txt) + ``` + +### Adding Native Plugin Support for MacOS + +- Add MacOS build task + +- Create MacOS-related native plugin directory + + ```console + mkdir -p native/native-plugin/mac/lib + ``` + +- Copy pre-compiled dependency libraries and header files to the corresponding directory, create `hello_cocos_glue-config.cmake` + + ```cmake + set(_hello_cocos_GLUE_DIR ${CMAKE_CURRENT_LIST_DIR}) + + add_library(hello_cocos STATIC IMPORTED GLOBAL) + set_target_properties(hello_cocos PROPERTIES + IMPORTED_LOCATION ${_hello_cocos_GLUE_DIR}/lib/libhello_cocos.a + ) + + include(${_hello_cocos_GLUE_DIR}/../src/CMakeLists.txt) + ``` + +- Update `cc_plugin.json`, add `iOS` and `mac` to the `platforms` field + + ```json + { + "name":"hello-cocos-demo", + "version":"1.0.0", + "author":"cocos", + "engine-version":">=3.6.3", + "disabled":false, + "modules":[ + { + "target":"hello_cocos_glue" + } + ], + "platforms":["windows", "android", "iOS", "mac"] + } + + ``` + +At this point, a native plugin that supports Android, Windows, MacOS, and iOS has been developed. + +The final contents of the native plugin directory are as follows: + +```console +$ tree native/native-plugin/ +native/native-plugin +├── cc_plugin.json +├── include +│ └── hello_cocos.h +├── src +│ ├── CMakeLists.txt +│ └── hello_cocos_glue.cpp +├── android +│ ├── hello_cocos_glue-config.cmake +│ ├── arm64-v8a +│ │ └── lib +│ │ └── libhello_cocos.a +│ └── armeabi-v7a +│ └── lib +│ └── libhello_cocos.a +├── ios +│ ├── hello_cocos_glue-config.cmake +│ └── lib +│ └── libhello_cocos.a +├── mac +│ ├── hello_cocos_glue-config.cmake +│ └── lib +│ └── libhello_cocos.a +└── windows + ├── hello_cocos_glue-config.cmake + └── lib + ├── hello_cocos.lib + └── hello_cocosd.lib +``` \ No newline at end of file diff --git a/versions/4.0/en/advanced-topics/native-profiler/add-stats.png b/versions/4.0/en/advanced-topics/native-profiler/add-stats.png new file mode 100644 index 0000000000..2ff1942433 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-profiler/add-stats.png differ diff --git a/versions/4.0/en/advanced-topics/native-profiler/enable-profiler.png b/versions/4.0/en/advanced-topics/native-profiler/enable-profiler.png new file mode 100644 index 0000000000..e218692666 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-profiler/enable-profiler.png differ diff --git a/versions/4.0/en/advanced-topics/native-profiler/profiler.png b/versions/4.0/en/advanced-topics/native-profiler/profiler.png new file mode 100644 index 0000000000..67a0ca8621 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-profiler/profiler.png differ diff --git a/versions/4.0/en/advanced-topics/native-scene-culling.md b/versions/4.0/en/advanced-topics/native-scene-culling.md new file mode 100644 index 0000000000..80a96bf5a7 --- /dev/null +++ b/versions/4.0/en/advanced-topics/native-scene-culling.md @@ -0,0 +1,39 @@ +# Native Scene Culling + +Creator supports native scene culling starting from v3.4.0, including **Octree Scene Culling** and **Occlusion Query Culling** for native platforms only. + +## Octree Scene Culling + +Generally, the engine culls models that are not in the viewing frustum (the visible range of the camera) by checking whether the bounding boxes of the models are in the viewing frustum one by one, which is slower. If octree culling is enabled, the models that are not in the viewing frustum can be quickly culled by the octree. + +This feature is disabled by default, to enable it, select the scene root node **Scene** in the **Hierarchy** panel, then notice the **Octree Scene Culling** option in the **Inspector** panel, and check **Enabled**. + +![octree scene culling](./native-scene-culling/octree-scene-culling.png) + +When octree culling is enabled, the whole world's bounding box (e.g. the white bordered cube in the scene above) will be displayed in the **Scene** panel. + +The properties of **Octree Scene Culling** are described as follows: + +| Property | Description | +| :-- | :-- | +| Enabled | Check this option to enable Octree Scene Culling, only for native platforms. | +| World MinPos | The minimum vertex coordinates of the world bounding box (objects beyond the bounding box are not rendered). | +| World MaxPos | The maximum vertex coordinates of the world bounding box (objects beyond the bounding box are not rendered). | +| Depth | Depth of the octree, the default value is 8. If the scene is small, it is recommended not to set this value too large, otherwise it may consume memory. | + +## Occlusion Query Culling + +**Occlusion Query Culling** is off by default, if it is on, the GFX backend will perform an occlusion query through the graphics API and if the object is occluded, only the simplified bounding box and material will be used to render the object to improve performance. + +This feature can be turned on with code that says + +```ts +director.root.pipeline.setOcclusionQueryEnabled(true); +``` + +> **Note**: GLES 2.0 does not support occlusion query culling, and some GLES 3.0 devices do not support it without the `GL_EXT_occlusion_query_boolean` extension. + +## Performance optimization suggestions + +- If most of the objects in the scene are visible, it is recommended not to enable **Octree Scene Culling** and **Occlusion Query Culling**. +- Performance may vary slightly from device to device, so decide whether to turn on the corresponding culling function after comparing performance tests. diff --git a/versions/4.0/en/advanced-topics/native-scene-culling/octree-scene-culling.png b/versions/4.0/en/advanced-topics/native-scene-culling/octree-scene-culling.png new file mode 100644 index 0000000000..c537849390 Binary files /dev/null and b/versions/4.0/en/advanced-topics/native-scene-culling/octree-scene-culling.png differ diff --git a/versions/4.0/en/advanced-topics/native-secondary-development.md b/versions/4.0/en/advanced-topics/native-secondary-development.md new file mode 100644 index 0000000000..0b2ea34ecc --- /dev/null +++ b/versions/4.0/en/advanced-topics/native-secondary-development.md @@ -0,0 +1,150 @@ +# Native Platform Secondary Development Guide + +If you need to integrate third-party SDK libraries or modify, add or remove C++, Objective-C, or Java code files, the following information can help you do it faster. + +## Native Project Directories + +When you click the **Build** button, three native platform-related folders are generated. + +There is 3 directories related to native project. + +### Common Directory + +Location: `native/engine/common` + +This directory is used to store common content such as engine library configurations and some third-party libraries that are used by all platforms. + +> The code in this directory is mostly written in C/C++. + +### Native Platforms Directories + +Location: `native/engine/` + +This directory is used to store platform-specific content. For example: + +- native/engine/android +- native/engine/ios +- native/engine/win64 +- native/engine/mac + +> `win64` is used for Windows, and `win32` is no longer supported. Only `win64` applications are supported for release by Cocos Creator. + +### Project Directory + +Location: `build/` + +This directory contains the final generated native projects used for compilation, debugging, and publishing. For example: + +- build/android +- build/ios +- build/windows +- build/mac + +During each build, the engine combines the common directory, the native platform directory, and the resources and scripts from the Cocos Creator project to generate the project directory. + +The code and related configurations in the project directory reference the files in the corresponding native platform directory and the common directory. If you make changes to the corresponding parts in the IDE, the files in the referenced directories will be modified accordingly. + +**Note:** The `native/engine/ios/info.plist` and `native/engine/mac/info.plist` files copied due to the mechanism of `CMake`. If you want to modify `info.plist`, please be cautious. + +The project directory contains the following contents: +- `assets`: `data` A symbolic link to the data directory, used for compatibility across platforms. +- `data`: The contents generated from the resources and scripts in the Cocos Creator project. +- `proj`: The native platform project for the current build, used for compilation, debugging, and publishing in the corresponding platform's IDE (e.g., Xcode, Android Studio). +- `cocos.compile.config.json`: The build options configuration used for this build. + +## Native Project Customization + +Sometimes, due to project requirements, we may need to modify, add, or remove native platform-related source code files, integrate third-party SDKs, or modify project configurations. These tasks are referred to as project customization development. + +Below, we will categorize different scenarios and explain how to perform the operations. + +### Modifying Engine Code + +Please refer to the [Engine Customization Workflow](./engine-customization.md)。 + +### Modifying Project Code + +If you need to modify project-related code, simply locate the corresponding files and make the necessary changes. Once modified, you can compile the project without any additional configuration. + +### Adding/Removing Code Files + +Adding or removing code files involves modifying the build configuration, which may differ for different programming languages and platforms. Let's categorize them as follows. + +#### Adding/Removing C++ Files + +If you need to add or remove project-related C++ files, you need to modify the corresponding `CMakeLists.txt` file. + +If the C++ files to be added or removed are in the `native/engine/common/` directory, you need to modify `native/engine/common/CMakeLists.txt`. + +```bat +list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp +) +``` + +As shown in the code above, find the corresponding location and add or remove your own source code. + +If the code files to be added or removed are platform-specific C++ files, you need to modify `native/engine//CMakeLists.txt`. Refer to the code below: + +```bat +include(${CC_PROJECT_DIR}/../common/CMakeLists.txt) + +//Add your own C++ files here +list(APPEND CC_PROJ_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/MyTest2.hpp + ${CMAKE_CURRENT_LIST_DIR}/MyTest2.cpp +) +``` + +#### Adding/Removing Objective-C Files + +The management of Objective-C files in the iOS/macOS native projects generated by Cocos Creator is exactly the same as for C++. Please refer to the previous content. + +#### Adding/Removing Java Files + +Java is based on a path-based package management mechanism, so adding or removing Java files does not require any special handling. + +### Integrating C++/OC Libraries + +If the library you want to integrate is written in C++ or Objective-C, place the SDK in the `native/engine/common/` or `native/engine//` directory depending on the situation, and modify the corresponding `CMakeLists.txt` in the respective directory. + +Libraries written by Objective-C should only be placed in the platform-specific directory and should not be placed in `native/engine/common/`, as it may cause compilation errors on other native platforms. + +Most C++ SDKs provide their own `CMakeLists.txt`, so you can integrate them by including the files. + +For CMake configuration, you can refer to the existing `CMakeLists.txt` in the project for modification. For more details on using CMake, you can refer to [Introduce to CMake Usage](../advanced-topics/cmake-learning.md). + +### Integrating Jar Libraries + +If the library you want to integrate is written by Java and specific to the Android platform, simply place it in the corresponding `native/engine/android/` directory and configure `native/android/build.gradle`. + +## Interacting with Native + +For newly created native methods or newly integrated native SDKs that you want to export for use in script code, you can use the following approaches. + +### Using JsbBridge + +If you need to call simple and infrequent functions, you can use the `JsbBridge` mechanism for communication. + +- [JavaScript and Java Communication using JsbBridge](js-java-bridge.md) +- [JavaScript and Objective-C Communication using JsbBridge](js-oc-bridge.md) + +### Using JSB Auto Binding + +For frequent C++ function calls or batch C++ API exports to the script layer, it is recommended to use the [JSB Automatic Binding]((jsb-auto-binding.md)) mechanism for script-native interaction. + +### Using Reflection + +Communication based on the reflection mechanism in Java and Objective-C can also be used for convenient script-native interaction. However, due to stricter review rules on iOS, there is a risk of review failure when using reflection mechanisms on iOS. + +- [JavaScript and Android Communication with Reflection](java-reflection.md) +- [JavaScript and iOS/macOS Communication with Reflection](oc-reflection.md) + +## Source Code Version Control + +If your team uses source code version control software for collaborative work, the `native/` directory should be included in the version control. + +All project customization work should be done in the `/` directory as much as possible, so that the `build/` directory can be deleted at any time and does not need to be included in the source code version control. + +For special project requirements that cannot be achieved within the `native/` directory, modifications to the content under the `build/` directory are required. In this case, the corresponding folders should be included in version control based on the requirements. diff --git a/versions/4.0/en/advanced-topics/oc-reflection.md b/versions/4.0/en/advanced-topics/oc-reflection.md new file mode 100644 index 0000000000..64b79df487 --- /dev/null +++ b/versions/4.0/en/advanced-topics/oc-reflection.md @@ -0,0 +1,230 @@ +# JavaScript and iOS/macOS Communication with Reflection + +## Calling Objective-C from JavaScript + +In Cocos Creator, there is a cross-language communication method based on language reflection, which allows JavaScript to directly call Objective-C functions. The method prototype is as follows: + +```js +var result = native.reflection.callStaticMethod(className, methodName, arg1, arg2, .....); +``` + +In the `native.reflection.callStaticMethod method`, by passing the Objective-C class name, method name, and arguments, you can directly call the Objective-C static method and obtain the return value. + +> **Note**: +> 1. Only static methods of accessible classes can be called +> +> 2. In March 2017, Apple App Store issued warnings to some applications that used risky methods. Among them, `respondsToSelector` and `performSelector` are included, which are core APIs used in the reflection mechanism. Please pay attention to Apple's official stance on this issue. Related discussions can be found at: [JSPatch](https://github.com/bang590/JSPatch/issues/746)、[React-Native](https://github.com/facebook/react-native/issues/12778)、[Weex](https://github.com/alibaba/weex/issues/2875)。 + +To reduce the risk of rejection during app review, it is recommended to [JavaScript and Objective-C Communication using JsbBridge](./js-oc-bridge.md). + +### Class Name and Static Methods + +The class name in the parameter does not require a path; you only need to pass the class name as it is in Objective-C. For example, if you create a class called `NativeOcClass` in any file in your project directory, you just need to import it into your project. + +Once again, it's important to note that it only supports call the static methods of Objective-C classes from JavaScript. + +### Method with Parameters + +When it comes to method names, pay attention to their exact syntax. You need to provide the complete method name, especially when a method has parameters. The colons `:` in the method name should also be included. + +Here's an example of a method with parameters: + +```objc +import +@interface NativeOcClass : NSObject ++(BOOL)callNativeUIWithTitle:(NSString *) title andContent:(NSString *)content; +@end +``` + +Example of calling a method with parameters: + +```js +if(sys.isNative && (sys.os == sys.OS.IOS || sys.os == sys.OS.OSX)){ + var ret = native.reflection.callStaticMethod("NativeOcClass", + "callNativeUIWithTitle:andContent:", + "cocos2d-js", + "Yes! you call a Native UI from Reflection"); +} +``` + +The `sys.isNative` is used to check if it's running on a native platform, and the `sys.os` is used to determine the current operating system. Since the communication mechanisms vary across different platforms, it is recommended to perform the check before call `native.reflection.callStaticMethod`. + +> **Note**: The method name should be `callNativeUIWithTitle:andContent:`, don't forget the **:** at the end. + +### Method without Parameters + +If a method does not have any parameters, then you don't need to include the colons `:`, the `methodName` remains the same as in Objective-C. + +Here's an example of a method without parameters: + +```objc ++(NSString *)callNativeWithReturnString; +``` + +Example of calling a method without parameters: + +```js +var ret = native.reflection.callStaticMethod("NativeOcClass", + "callNativeWithReturnString"); +``` + +### Return Values + +Here's the Objective-C implementation of the method that shows a native dialog. It sets the title and content to the passed parameters and returns a boolean value. + +```objc ++(BOOL)callNativeUIWithTitle:(NSString *) title andContent:(NSString *)content{ + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title message:content delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil]; + [alertView show]; + return true; +} +``` + +At this point, you can receive the returned value (true) from Objective-C in the variable `ret`. + +### Type Conversion + +When dealing with float, int, and bool parameters in Objective-C implementation, you should use the following types for conversion: + +- **Use NSNumber for float and int** +- **Use BOOL for bool** + +For example, in the code snippet below, we pass two float numbers to calculate their sum. We use `NSNumber` as the parameter type instead of int and float. + +```objc ++(float) addTwoNumber:(NSNumber *)num1 and:(NSNumber *)num2{ + float result = [num1 floatValue]+[num2 floatValue]; + return result; +} +``` + +Currently, the parameters and return values support the following types: + +- `int` +- `float` +- `bool` +- `string` + +Other types are not supported at the moment. + +If you're not familiar with how to add Objective-C files to your project, you can refer to the [Native Platform Secondary Development Guide](native-secondary-development.md)。 + +## Calling JavaScript in Objective-C + +In a Cocos Creator native project, we can also execute JavaScript code in C++ or Objective-C using the `evalString` method. + +Here's an example of how to call JavaScript code: + +```c++ +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString(script.c_str()); +}); +``` + +> **Note**: Unless you are certain that the current thread is the main thread ( Cocos Thread ), you need to use the performFunctionInCocosThread method to dispatch the function to the main thread for execution. + +### Global Function + +We can add a new global function in the script using the following code: + +```js +window.callByNative = function(){ + //to do +} +``` + +>`window` is the global object in the Cocos Engine script environment. If you want a variable, function, object, or class to be globally accessible, you need to add it as a property of `window`. You can access it using `window.variableName` or `variableName` directly. + +Then, you can call it like this: + +```c++ +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("window.callByNative()"); +}); +``` + +Or: + +```c++ +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("callByNative()"); +}); +``` + +### Calling Static Function of an Class + +Suppose there is an object in the TypeScript script with the following static function: + +```ts +export class NativeAPI{ + public static callByNative(){ + //to do + } +} +//Register NativeAPI as a global class, otherwise it cannot be called in Objective-C. +window.NativeAPI = NativeAPI; +``` + +We can call it like this: + +```c++ +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("NativeAPI.callByNative()"); +}); +``` + +### Calling Singleton Function + +If the script code implements a singleton object that can be globally accessed: + +```ts +export class NativeAPIMgr{ + private static _inst:NativeAPIMgr; + + public static get inst():NativeAPIMgr{ + if(!this._inst){ + this._inst = new NativeAPIMgr(); + } + return this._inst; + } + + public static callByNative(){ + //to do + } +} + +//Register NativeAPIMgr as a global class, otherwise it cannot be called in Objective-C. +window.NativeAPIMgr = NativeAPIMgr; +``` + +We can call it like this. + +```c++ +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("NativeAPIMgr.inst.callByNative()"); +}); +``` + +### Calling with Parameters + +The above mentioned ways of calling JS from Java all support parameter passing. However, the parameters only support the three basic types: `string`, `number`, and `boolean`. + +Taking the global function as an example: + +```js +window.callByNative = function(a:string, b:number, c:bool){ + //to do +} +``` + +You can call it like this: + +```c++ +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("window.callByNative('test',1,true)"); +}); +``` + +## Thread Safety + +As you can see in the code above, `CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread` is used. This is to ensure that the code is executed in the correct thread. For more details, please refer to the [Thread Safety](./thread-safety.md) documentation. diff --git a/versions/4.0/en/advanced-topics/profiler.md b/versions/4.0/en/advanced-topics/profiler.md new file mode 100644 index 0000000000..53e60fb8dd --- /dev/null +++ b/versions/4.0/en/advanced-topics/profiler.md @@ -0,0 +1,101 @@ +# Native Performance Profiler + +The performance profiler is a tool for performance analysis and statistics that is currently only available on native platforms. + +## Default statistics + +- The performance profiler is shown as follows: + + ![profiler](native-profiler/profiler.png) + +- Different from the old version of `Profiler` in the lower left corner, the new version of `Profiler` provides more detailed game performance and memory statistics, and supports expansion. Developers can freely add performance and memory data that they care about. The default data is as follows: + + 1. Core statistics `CoreStats`, including: + - frame rate + - render time per frame + - GFX backend + - Whether to enable multithreading + - Whether to enable occlusion query + - Whether to enable shadow maps + - Screen Resolution + + 2. Object count statistics `ObjectStats`, including: + - number of render calls + - Number of instantiations + - Number of triangles + - Number of 2D rendering batches + - Number of rendered models + - Number of cameras + + 3. Memory usage statistics `MemoryStats`, including: + - Vertex buffer size for DebugRenderer + - Native font memory size + - Video memory occupied by textures + - The video memory occupied by Buffer + - GeometryRenderer vertex buffer size + + 4. Performance Statistics `PerformanceStats`, showing the logical thread call stack, contains for each profiled code segment: + - total execution time per frame + - Maximum time for a single execution per frame + - total number of executions per frame + - average execution time per frame + - Total historical execution time + - The maximum time of a single execution in history + - Total number of historical executions + - The average time of a single execution in history + +## How to use + +- In the main menu of the editor: **Project** -> **Project Settings** -> **Feature cropping**, check **Debug Renderer**, this option is closed by default, you need to open it to display the debug information: + + ![enable profiler](native-profiler/enable-profiler.png) + +- Change the macro definition of `CC_USE_PROFILER` to `1` in `native/cocos/base/Config.h`, and then change it back to `0` after the performance and memory optimization is completed. At this time, the performance profiler is completely closed and will not Cause any side effects on the code: + + ```c++ + #ifndef CC_USE_PROFILER + #define CC_USE_PROFILER 0 + #endif + ``` + +- If you want to add the statistical information of `ObjectStats`, such as counting the number of rendered models per frame (need to be called in a function such as update for each frame): + + The following macros are defined in [native/cocos/profiler/Profiler.h](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/profiler/Profiler.h). Developers can use it as needed. + + ```c++ + void RenderScene::update(uint32_t stamp) { + ... + CC_PROFILE_OBJECT_UPDATE(Models, _models.size()); + } + ``` + + - `CC_PROFILE_OBJECT_UPDATE` for updating statistics + - `CC_PROFILE_OBJECT_INC` for incrementing statistics + - `CC_PROFILE_OBJECT_DEC` for decrementing statistics + +- If you want to add `MemoryStats` statistics, such as counting the memory usage of `GeometryRenderer` vertex buffer: + + ```c++ + void GeometryVertexBuffer::init(gfx::Device *device, + uint32_t maxVertices, const gfx::AttributeList &attributes) { + ... + CC_PROFILE_MEMORY_INC(GeometryVertexBuffer, static_cast(_maxVertices * sizeof(T))); + } + ``` + + - `CC_PROFILE_MEMORY_UPDATE` for updating memory usage (bytes) + - `CC_PROFILE_MEMORY_INC` for incrementing memory usage (bytes) + - `CC_PROFILE_MEMORY_DEC` for decrementing memory usage (bytes) + +- If you want to add `PerformanceStats` statistics, such as the execution time (in milliseconds) of the `ForwardPipeline::render` function: + + ```c++ + void ForwardPipeline::render(const ccstd::vector &cameras) { + CC_PROFILE(ForwardPipelineRender); + ... + } + ``` + +- After the above modification, compile and run, you can see the new statistics on the screen: + + ![add-stats](native-profiler/add-stats.png) diff --git a/versions/4.0/en/advanced-topics/sebind/devtool-test.png b/versions/4.0/en/advanced-topics/sebind/devtool-test.png new file mode 100644 index 0000000000..20f59eb095 Binary files /dev/null and b/versions/4.0/en/advanced-topics/sebind/devtool-test.png differ diff --git a/versions/4.0/en/advanced-topics/sebind/thisobject_placeholder.png b/versions/4.0/en/advanced-topics/sebind/thisobject_placeholder.png new file mode 100644 index 0000000000..b1a11679cd Binary files /dev/null and b/versions/4.0/en/advanced-topics/sebind/thisobject_placeholder.png differ diff --git a/versions/4.0/en/advanced-topics/supported-versions.md b/versions/4.0/en/advanced-topics/supported-versions.md new file mode 100644 index 0000000000..1186730303 --- /dev/null +++ b/versions/4.0/en/advanced-topics/supported-versions.md @@ -0,0 +1,22 @@ +# Features and System Versions + +## Modules and System Versions + +| Module | Android | iOS | +| :-- | :--- | :-- | +| VULKAN | API Level 24(7.0) | - | +| Google Play Instant | API Level 23(6.0) | - | +| TBB | API Level 21(5.0) | 10.0 | +| TaskFlow | API Level 18(4.3) | 12.0 | +| Deferred Rendering Pipeline | API Level 21(5.0) | 10.0 | + +## C++ 与与最低支持的系统版本 + +| Cocos Creator | C++ | Android | iOS | +| :-- | :--- | :-- | :-- | +| 3.0 | C++14 | API Level 18(4.3) | 10.0 | +| 3.1~3.3.1 | C++17 | API Level 21(5.0) | 11.0 | +| 3.3.2 ~ 3.5.1 | C++14 | API Level 18 (4.3) | 10.0 | +| 3.6.0 以上 | C++17 | API Level 21(5.0) | 11.0 + +> **Note**: Enabling the `TaskFlow` will automatically enable C++17 support for compilation, and `TaskFlow` requires iOS 12+ for its required features. diff --git a/versions/4.0/en/advanced-topics/thread-safety.md b/versions/4.0/en/advanced-topics/thread-safety.md new file mode 100644 index 0000000000..b7ab23ec00 --- /dev/null +++ b/versions/4.0/en/advanced-topics/thread-safety.md @@ -0,0 +1,78 @@ +# Thread Safety + +In a native application built with Cocos Creator, there are at least two threads: the **GL Thread** and **Native UI Thread**。 +- **GL Thread**: Executes rendering-related code of the Cocos Engine and JavaScript code. +- **UI Thread**: Handles the creation, response, and update of the platform's native UI. + +Therefore, we need to be aware of the following two issues: +1. When code in the **UI thread** needs to call code in the **GL thread**, we need to handle thread safety. +2. When code in the **GL thread** needs to call code in the **UI thread**, we need to handle thread safety. + +Now let's take a look at how to handle thread safety in different situations. + +## Executing on the UI Thread + +When we write Java methods on the Android platform that involve UI-related operations, and they are called in the GL thread, we need to use the `app.runOnUiThread` method to execute the code on the UI thread, ensuring thread safety. + +Let's take an example of displaying an Alert dialog on Android. + +```java +public class AppActivity extends CocosActivity { + + private static AppActivity app = null; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + app = this; + } + + public static void showAlertDialog(final String title,final String message) { + app.runOnUiThread(new Runnable() { + @Override + public void run() { + AlertDialog alertDialog = new AlertDialog.Builder(app).create(); + alertDialog.setTitle(title); + alertDialog.setMessage(message); + alertDialog.setIcon(R.drawable.icon); + alertDialog.show(); + } + }); + } +} +``` + +In this example, the showAlert method creates and displays an Alert dialog. By using app.runOnUiThread, we ensure that the code runs on the UI thread, preventing any thread safety issues. + +## Executing on the GL Thread + +When code running on the UI thread needs to call code in the GL thread, we need to use `CocosHelper.runOnGameThread` to ensure thread safety. + +Let's take an example of a button event response: + +```java +alertDialog.setButton("OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // Must run on GL Thread + CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("cc.log(\"Javascript Java bridge!\")"); + } + }); + } +}); +``` + +## Objective-C / C++ + +If you are writing Objective-C or C++ code and want to ensure thread safety, you can use the `CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread` method to execute the code on the GL thread. + +Here's an example: + +```c++ +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + //TO DO +}); +``` + +Please note that the above code is in C++. If you want to call it from Objective-C code, you need to use the `*.mm` file extension for your Objective-C code file. diff --git a/versions/4.0/en/advanced-topics/wasm-asm-create.md b/versions/4.0/en/advanced-topics/wasm-asm-create.md new file mode 100644 index 0000000000..c2c05346b9 --- /dev/null +++ b/versions/4.0/en/advanced-topics/wasm-asm-create.md @@ -0,0 +1,110 @@ +# Converting Native Code to Wasm/Asm Files Using Emscripten + +## Installing Emscripten on Windows + +1. **Download emsdk:** Clone the emsdk repository by running the git command: `git clone https://github.com/emscripten-core/emsdk.git` + + > **Note:** You need to have git installed on your computer to run git commands. If not installed, download and install it from [git official website](https://git-scm.com/). + +2. **Choose an emsdk version:** The latest version of emsdk is 4.0.10, but testing has revealed unexpected compatibility issues. We recommend using version 3.1.41, which is the version used by the engine team when compiling Spine module wasm files. + +3. **Install emsdk dependencies:** In the emsdk root directory, run the command: `./emsdk install 3.1.41`. This will automatically install the required runtime libraries for emsdk. + + > **Notes:** + > * When switching emsdk versions, you need to both switch the source code version and reinstall the runtime libraries. Simply append the version number string after `./emsdk install`. + > + > * To install the latest version of emsdk dependencies, append `latest` after `./emsdk install`. + > + > * If downloads fail during dependency installation, try using a VPN to accelerate the connection. + +4. **Configure system environment variables for emsdk:** In the emsdk root directory, run the command: `./emsdk_env.bat` to automatically configure the required system environment variables for emsdk runtime libraries. + +5. **Verify emsdk environment variables:** Open the upstream/emscripten/.emscripten file in the emsdk directory and check if LLVM_ROOT and BINARYEN_ROOT match the following reference paths: + + ```python + LLVM_ROOT = 'D:\\git\\emsdk\\upstream\\bin' + BINARYEN_ROOT = 'D:\\git\\emsdk\\upstream' + ``` + + If they don't match, manually modify them to point to the local paths where llvm tools and binaryen tools can be found. + +6. **Activate emsdk environment:** In the emsdk root directory, run the command: `./emsdk activate --system` to activate the emsdk environment. After this, you can use the `emcc` command in command line windows. + + > **Notes:** + > * Adding `--system` makes the emsdk environment available in all command line windows. Otherwise, you would need to run `./emsdk activate` each time you open a new command line window. + > + > * Older versions typically used `./emsdk activate --global` for global activation. + +7. **Verify successful emsdk installation:** Run the command: `emcc --version` in a command line window. If you see output like `emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.41`, the installation was successful. + +## Generating Wasm Files + +1. **Create a C code file:** Create a Cocos.c file in any directory with the following content: + + ```C + #include + + int main() { + printf("Hello World\n"); + } + ``` + +2. **Convert C code to Wasm/Asm file:** In the root directory where Cocos.c is located (assuming it's in D:\) run the command: `emcc D:\Cocos.c -s WASM=1`. After execution, the wasm file will be generated in the current directory. + + > **Notes:** + > + > * Adding `-s WASM=1` specifies generation of wasm files. Using `-s WASM=0` generates Asm files for platforms without WebAssembly support. + > + > * When generating Asm files, if the native code requires large memory initialization, a .mem file will also be generated and needs to be loaded. + > + > * For more command line parameters, run `emcc --help` or search for related documentation online. + +## Loading Wasm/Asm Files + +Refer to [Custom Loading of Wasm/Asm Files and Modules](./wasm-asm-load.md) for instructions on loading Wasm/Asm files. + +## Compiling Cocos Engine's Spine Wasm (Available for 3.8.0 ~ 3.8.4 engine versions) + +1. **Install CMake:** Download and install from [CMake official website](https://cmake.org/) + +2. **Configure CMake system environment variables:** Add `C:\Program Files\CMake\bin` to your system environment variables. + +3. **Install MinGW:** Download the package containing `win32-seh-ucrt` from [MinGW releases page](https://github.com/niXman/mingw-builds-binaries/releases), then extract it to your computer. + +4. **Configure MinGW system environment variables:** Add `C:\mingw64\bin` to your system environment variables. + +5. **Navigate to Spine Wasm C++ code folder:** Open a command line tool and use cd to enter the Cocos engine folder containing Spine Wasm C++ code: + + ```cmd + cd D:\CocosCreator\Creator\3.8.3\resources\resources\3d\engine-native\cocos\editor-support\spine-wasm + ``` + + > **Note:** This is a reference path - modify it according to your actual installation path. + +6. **Generate CMakeFiles:** Run the command `emcmake cmake -B ./build` to generate CMakeFiles in the build folder. + +7. **Enter build folder:** Use cd to enter the build folder: `cd ./build`. + +8. **Generate Spine Wasm files:** Run the command `emmake make`. You should now see spine.js and spine.wasm generated in the build folder. + + ![spine-wasm-create](../../zh/advanced-topics/wasm-asm-create/spine-wasm-create.png) + +9. **Modify emcc commands in CMakeLists.txt:** The CMakeLists.txt file is located in the spine-wasm folder. Open it and modify the `emcc` command parameters to change the final output. + + > **Note:** + > + > * To compile a Debug version, set CMAKE_BUILD_TYPE to Debug in CMakeLists.txt, remove the -O3 optimization option from emcc commands, and add the -g compilation option. + +10. **Update Cocos Engine's Built-in Spine Wasm:** Copy the generated spine Wasm and spine.js files to the folder at relative path `engine-native/external/emscripten/spine`, rename spine.js to spine.wasm.js, and replace the old file. + + > **Note:** You need to restart the editor after switching between debug and release modes. + +11. **Update Editor's Cached spine.wasm.js:** Delete spine.wasm.js in the editor's cache folder at relative path `resources\3d\engine\bin\.cache`, then restart the editor. + +## Compiling Cocos Engine's Spine Wasm (For engine versions 3.8.5 and above) + +Refer to the content in [Spine WASM Compilation Guide](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/editor-support/spine-wasm/README.md). + +## Debugging Debug Version of Spine Wasm + +Use Google Chrome and install the [C/C++ DevTools Support extension](https://chromewebstore.google.com/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb). After installation, you can open and debug Spine Wasm C++ related code files in the Chrome DevTools console. diff --git a/versions/4.0/en/advanced-topics/wasm-asm-load.md b/versions/4.0/en/advanced-topics/wasm-asm-load.md new file mode 100644 index 0000000000..7fcb60d460 --- /dev/null +++ b/versions/4.0/en/advanced-topics/wasm-asm-load.md @@ -0,0 +1,264 @@ +# Custom Loading of Wasm/Asm Files and Modules + +The CocosCreator engine provides interfaces for manually loading Wasm/Asm modules such as Spine and Box2D. However, if users want to load custom Wasm/Asm files and modules, they need to implement the loading logic themselves within their project. + +> #### Note: This article does not cover how to create Wasm/Asm files but focuses on loading them in CocosCreator. + +1. Add support for Wasm/Asm files in loaders and parsers. Insert the following example code outside the script's class or translate it into JavaScript and include it as a JS plugin in the project. + + ```js + // Determine if the current platform supports loading wasm files. The Cocos engine currently does not support loading wasm files on iOS platforms. + if (sys.hasFeature(sys.Feature.WASM) || sys.os !== sys.OS.IOS) { + if (sys.isNative) { + //@ts-ignore + assetManager.downloader.register('.wasm', assetManager.downloader._downloaders[".bin"]); + //@ts-ignore + assetManager.parser.register('.wasm', assetManager.parser._parsers[".bin"]); + } + else if (sys.isBrowser || sys.platform === sys.Platform.WECHAT_GAME) { + //@ts-ignore + assetManager.downloader.register('.wasm', assetManager.downloader._downloadArrayBuffer); + //@ts-ignore + assetManager.downloader.register('.mem', assetManager.downloader._downloadArrayBuffer); + } + } else { + if (sys.isNative) { + //@ts-ignore + assetManager.downloader.register('.mem', assetManager.downloader._downloaders[".bin"]); + //@ts-ignore + assetManager.parser.register('.mem', assetManager.parser._parsers[".bin"]); + } + } + ``` + +* Both wasm and mem files are binary files, so the built-in binary file loading functions of the engine are used as download and parsing functions for these formats. + +2. Modify the tsconfig.json file in the project to add `CommonJS` module support, enabling dynamic imports of JS scripts. + + ```json + { + /* Base configuration. Do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + /* Add your custom configuration here. */ + "compilerOptions": { + "strict": false, + "module": "CommonJS" + } + } + ``` + +3. Import Wasm/Asm files into the resource manager of the CocosCreator editor and place both .wasm and .mem files in the same bundle folder. Below is an example of the file distribution structure for reference: + + ![](../../zh/advanced-topics/wasm-asm-load/wasm-asm-load-01.png) + + ![](../../zh/advanced-topics/wasm-asm-load/wasm-asm-load-02.png) + +4. Add the wasmOrAsmLoadTest function to import Wasm/Asm module scripts. Add success and failure callbacks, invoking the loading of Wasm/Asm binary files in the success callback. + + > #### Note: The third parameter received by the loadWasmOrAsm function is the resource Uuid of the Wasm/Asm file in the editor. Developers should replace it with the Uuid of resources in their own projects. + + ```js + wasmOrAsmLoadTest () { + if (sys.hasFeature(sys.Feature.WASM) || sys.os !== sys.OS.IOS) { + import('./effekseer.js').then(({ default: wasmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer", "44cacb3c-e901-455d-b3e1-1c38a69718e1").then((wasmFile)=>{ + //TODO: Initialize wasm file + + }, (err)=> { + console.error("wasm load failed", err); + }) + }); + } else { + import('./effekseer.asm.js').then(({ default: asmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer.asm", "3400003e-dc3c-43c1-8757-3e082429125a").then((asmFile)=> { + //TODO: Initialize asm file + + }, (err)=> { + console.error("asm load failed", err); + }); + }); + } + } + ``` + +5. Add the loadWasmOrAsm method, which is used to load Wasm/Asm binary files. + + ```js + loadWasmOrAsm (bundleName, fileName, editorWasmOrAsmUuid): Promise { + return new Promise((resolve, reject) => { + if (EDITOR) { + // Loading resources via UUID is more convenient in the editor; cannot load through bundles. + if (editorWasmOrAsmUuid) { + assetManager.loadAny(editorWasmOrAsmUuid, (err, file: Asset)=> { + if (!err) { + //@ts-ignore + resolve(file); + } else { + reject(err); + } + }) + } + } else { + if (bundleName && fileName) { + assetManager.loadBundle(bundleName, (err, bundle)=>{ + if (!err) { + bundle.load(fileName, Asset, (err2: any, file: Asset) => { + if (!err2) { + //@ts-ignore + resolve(file); + } else { + reject(err2); + } + }) + } else { + reject(err); + } + }) + } + } + }) + } + ``` + +6. Write Wasm initialization code. After the Wasm binary file is loaded, call the instantiateWasm function to complete initialization. + + ```js + initWasm (wasmFactory, file): Promise { + var self = this; + return new Promise((resolve, reject) => { + wasmFactory({ + instantiateWasm (importObject, receiveInstance) { + self.instantiateWasm(file, importObject).then((result) => { + receiveInstance(result.instance, result.module); + }).catch((err) => reject(err)); + } + }).then((instance: any)=>{ + resolve(instance); + }).catch((err) => reject(err)); + }); + } + + instantiateWasm (wasmFile: Asset, importObject: WebAssembly.Imports): Promise { + if (sys.isBrowser || sys.isNative) { + //@ts-ignore + return WebAssembly.instantiate(wasmFile._file, importObject); + } else if (sys.platform === sys.Platform.WECHAT_GAME){ + //@ts-ignore + return CCWebAssembly.instantiate(wasmFile.nativeUrl, importObject) + } + } + ``` + +7. Add a call to the initWasm function in the wasmOrAsmLoadTest function to complete the loading of Wasm. + + ```js + wasmOrAsmLoadTest () { + if (sys.hasFeature(sys.Feature.WASM) || sys.os !== sys.OS.IOS) { + import('./effekseer.js').then(({ default: wasmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer", "44cacb3c-e901-455d-b3e1-1c38a69718e1").then((wasmFile)=>{ + this.initWasm(wasmFactory, wasmFile).then((instance: any)=> { + Effekseer = instance; + Effekseer._myFunction(); + console.log("effekseer wasm module inited", Effekseer); + }, (err) => { + console.error("effekseer wasm module init failed", err); + }); + + }, (err)=> { + console.error("wasm load failed", err); + }) + }); + } else { + import('./effekseer.asm.js').then(({ default: asmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer.asm", "3400003e-dc3c-43c1-8757-3e082429125a").then((asmFile)=> { + //TODO: Initialize asm file + + }, (err)=> { + console.error("asm load failed", err); + }); + }); + } + } + ``` + +* After Wasm is loaded, you should see the following output in the browser console in this example: + + ![](../../zh/advanced-topics/wasm-asm-load/wasm-asm-load-03.png) + + 8. Write asm initialization code. First, add constants for the memory size of the asm module, then add the initAsm function to initialize the Asm module. + + ```js + // Add constants for the memory size of the asm module, used during asm module initialization. + const PAGESIZE = 65536; // 64KiB + + // Number of pages in the wasm memory + // TODO: Make this configurable by the user. + const PAGECOUNT = 32 * 16; + + // Total memory size of the wasm memory + const MEMORYSIZE = PAGESIZE * PAGECOUNT; // 32 MiB + ``` + + ```js + initAsm (asmFactory, file): Promise { + const asmMemory: any = {}; + asmMemory.buffer = new ArrayBuffer(MEMORYSIZE); + const module = { + asmMemory, + memoryInitializerRequest: { + //@ts-ignore + response: file._file, + status: 200, + } as Partial, + }; + return asmFactory(module); + } + ``` + +9. Add a call to the initAsm function in the wasmOrAsmLoadTest function to complete the loading of Asm. + + ```js + wasmOrAsmLoadTest () { + if (sys.hasFeature(sys.Feature.WASM) || sys.os !== sys.OS.IOS) { + import('./effekseer.js').then(({ default: wasmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer", "44cacb3c-e901-455d-b3e1-1c38a69718e1").then((wasmFile)=>{ + this.initWasm(wasmFactory, wasmFile).then((instance: any)=> { + Effekseer = instance; + Effekseer._myFunction(); + console.log("effekseer wasm module inited", Effekseer); + }, (err) => { + console.error("effekseer wasm module init failed", err); + }); + + }, (err)=> { + console.error("wasm load failed", err); + }) + }); + } else { + import('./effekseer.asm.js').then(({ default: asmFactory })=> { + + this.loadWasmOrAsm("wasmFiles", "effekseer.asm", "3400003e-dc3c-43c1-8757-3e082429125a").then((asmFile)=> { + this.initAsm(asmFactory, asmFile).then((instance: any)=>{ + Effekseer = instance; + Effekseer._myFunction(); + console.log("effekseer asm module inited", Effekseer); + }, (err) => { + console.error("effekseer asm module init failed", err); + }); + + }, (err)=> { + console.error("asm load failed", err); + }); + }); + } + } + ``` + +* After Asm is loaded, you should see the following output in the browser console in this example: + + ![](../../zh/advanced-topics/wasm-asm-load/wasm-asm-load-04.png) + +### Example Project Repository Address + +[GitHub Repository Address](https://github.com/cocos/cocos-awesome-tech-solutions/tree/3.8.x-release/demo/Creator3.8.6_WasmOrAsmLoad) diff --git a/versions/4.0/en/advanced-topics/web-socket/http.png b/versions/4.0/en/advanced-topics/web-socket/http.png new file mode 100644 index 0000000000..add0572dd3 Binary files /dev/null and b/versions/4.0/en/advanced-topics/web-socket/http.png differ diff --git a/versions/4.0/en/advanced-topics/web-socket/web-socket.png b/versions/4.0/en/advanced-topics/web-socket/web-socket.png new file mode 100644 index 0000000000..5d643cb619 Binary files /dev/null and b/versions/4.0/en/advanced-topics/web-socket/web-socket.png differ diff --git a/versions/4.0/en/advanced-topics/websocket-introduction.md b/versions/4.0/en/advanced-topics/websocket-introduction.md new file mode 100644 index 0000000000..4fae540a26 --- /dev/null +++ b/versions/4.0/en/advanced-topics/websocket-introduction.md @@ -0,0 +1,24 @@ +# WebSocket Introduction + +Often when developing web applications or games, for data security or other goals, user data needs to be stored in a web back-end server, and browsers generally only support the HTTP protocol. + +The HTTP (HyperText Transfer Protocol) protocol [^1] is an application layer transport protocol based on the TCP protocol and is used by requesting data from the client (web browser or via [API](http.md)) to the server, which stores the data in the HTTP protocol response and returns it to the client. + +![http](web-socket/http.png) + +In the early days of the HTTP protocol (before 1.1), one request corresponded to one network connection, which meant that each time an HTTP request was made, the client and server had to re-establish a TCP connection, which was a waste of resources. After HTTP/1.1, the HTTP protocol can be merged by keep-alive, so that multiple requests (Request) and responses (Respnose) can be merged. However, the HTTP protocol at this time is still one request for one response. + +Usually, such communication is sufficient for the server to passively accept the return data from the client's request. In specific cases, such as when the server needs to push data to the client, the client needs to poll the request, i.e., the client keeps requesting data from the server through timing or other loops. The HTTP protocol itself carries a large HTTP header and the constant polling behavior can cause waste of resources and bandwidth on the client and server side. + +To solve this problem, the W3C Advisory Committee developed the WebSocket protocol in 2008, and in 2011, WebSocket became the browser standard, which means that all browsers support WebSocket. + +The HTTP/1.1 protocol can be upgraded to the Websocket protocol by upgrading the connection [^2], after which both the client and the server can actively push the required data to each other. This solves the problem of real-time client-server interaction. + +![web-socket](web-socket/web-socket.png) + +WebSocket transfers can optionally use Binary or String to increase the specificity and security of the private protocol. + +To use Websocket within Cocos Creator, see [Using WebSocket Server](websocket-server.md) and [WebSocket Client](websocket.md). + +[^1]: [The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455) +[^2]: [HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) diff --git a/versions/4.0/en/advanced-topics/websocket-server.md b/versions/4.0/en/advanced-topics/websocket-server.md new file mode 100644 index 0000000000..b896802010 --- /dev/null +++ b/versions/4.0/en/advanced-topics/websocket-server.md @@ -0,0 +1,102 @@ +# WebSocket Server + +Developers can launch a WebSocket Server during the game and provide an **RPC interface**. By improving and calling these **RPC interfaces**, developers can monitor the internal state of the game process and increase the ability to manage the game process state. + +## How To Enable + +The **WebSocket Server** is disabled by default. To enable it, need to check the editor's main menu **Project -> Project Settings -> Feature Cropping** , and enable the **WebSocket Server** configuration setting. + +## How to use WebSocket + +Refer to the example code below: + +```ts +// In the native platform's Release mode or in Web / WeChat Mini Games and other platforms, WebSocketServer may not be defined. +if (typeof WebSocketServer === "undefined") { + console.error("WebSocketServer is not enabled!"); + return; +} + +let s = new WebSocketServer(); +s.onconnection = function (conn) { + conn.onmessage = function (data) { + conn.send(data, (err) => {}); + } + conn.onclose = function () { + console.log("connection gone!"); + }; +}; + +s.onclose = function () { + console.log("server is closed!"); +} +s.listen(8080, (err) => { + if (!err); + console.log("server booted!"); +}); +``` + +## API + +The interface is defined as follows + +```typescript +/** + * Server object + */ +class WebSocketServer { + /** + * Close the server + */ + close(cb?: WsCallback): void; + /** + * Listen and launch the service + */ + listen(port: number, cb?: WsCallback): void; + /** + * Handle new requests + */ + set onconnection(cb: (client: WebSocketServerConnection) => void); + /** + * Set up the server shutdown callback + */ + set onclose(cb: WsCallback); + /** + * Gets all the connection objects + */ + get connections(): WebSocketServerConnection[]; +} + +/** + * The client connection object in the server + */ +class WebSocketServerConnection { + /** + * Close the connection + */ + close(code?: number, reason?: string): void; + /** + * Send the data + */ + send(data: string|ArrayBuffer, cb?: WsCallback): void; + + set ontext(cb: (data: string) => void); + set onbinary(cb: (data: ArrayBuffer) => void); + set onmessage(cb: (data: string|ArrayBuffer) => void); + set onconnect(cb: () => void;); + set onclose(cb: WsCallback); + set onerror(cb: WsCallback); + + get readyState(): number; +} + +interface WsCallback { + (err?: string): void; +} +``` + +> **Note**: After v3.7.0, `ondata` callback has been deprecated, please use `onmessage` to instead. + +## Reference links + +The interface design refers to the [nodejs-websocket](https://www.npmjs.com/package/nodejs-websocket#server) server. diff --git a/versions/4.0/en/advanced-topics/websocket.md b/versions/4.0/en/advanced-topics/websocket.md new file mode 100644 index 0000000000..17132e363a --- /dev/null +++ b/versions/4.0/en/advanced-topics/websocket.md @@ -0,0 +1,15 @@ +# WebSocket Client + +The native environment supports the [Web Standard](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket) WebSocket interface. + +## Differences + +Prior to version 3.5, the WebSocket API on Android and Windows was implemented using [libwebsockets](https://github.com/warmcat/libwebsockets). It requires the developer to specify the CA file path with the third parameter. + +```ts +this.wsInstance = new WebSocket('wss://echo.websocket.org', [], caURL); +```` + +See [demo code](https://github.com/cocos/cocos-test-projects/blob/07f5671e18ef3ed4494d8cba6c2f9499766467a6/assets/cases/network/NetworkCtrl.ts#L113-L120). CA certificates can be downloaded from [here](https://curl.se/docs/caextract.html). + +After version 3.5, certificates are no longer mandatory on Android. diff --git a/versions/4.0/en/animation/animation-auxiliary-curve.md b/versions/4.0/en/animation/animation-auxiliary-curve.md new file mode 100644 index 0000000000..832a92df35 --- /dev/null +++ b/versions/4.0/en/animation/animation-auxiliary-curve.md @@ -0,0 +1,49 @@ +# The Auxillary Curve + +The Auxillary Curve is a new feature in Cocos Creator 3.8, and you can read the [Auxillary Curve](./marionette/procedural-animation/auxiliary-curve/index.md) to help understanding the basic concepts. + +Please make sure the **Animation Auxillary Curve** option is enabled in the **Laboratory** page of the **Preference** panel. + +![enable.png](./animation-auxiliary-curve/enable.png) + +In this section, we will introduce to you how to use the auxillary curve in the animation editor. Auxillary curve is a part of a curve editor, the basic curve options can be refer to [Curve Editor](./curve-editor.md). + +Select the node with the animation component (Animation, SkeletalAnimation or AnimationController). Click on the button in the figure below to enter the editing mode. + +![start-edit.png](./animation-auxiliary-curve/start-edit.png) + +The Auxiliary Curves section in the figure below shows the auxiliary curves. + +![overview.png](./animation-auxiliary-curve/overview.png) + +## Adding and Removing Curves + +Clicking the "+" button on an auxiliary curve in the animation editor will add an auxiliary curve to the current animation. + +![add-curve.png](./animation-auxiliary-curve/add-curve.png) + +The name of the auxiliary curve can be modified after it has been added. It can also be changed later by clicking on the auxiliary curve with the right mouse button and using the Rename menu in the popup menu. + +![add-curve.png](./animation-auxiliary-curve/menu.png) + +In the popup menu, click the Remove menu to remove the currently selected auxiliary curve or rename the curve. + +## New Keyframe + +Keyframes can be added by clicking the right mouse button on the right side of the Auxiliary Curve Editor. + +![create-key-frame.png](./animation-auxiliary-curve/create-key-frame.png) + +After creating a keyframe, the pop-up menu with a right-click on the keyframe allows you to adjust the information of the keyframe or interpolation. + +![keyframe-menu.png](./animation-auxiliary-curve/keyframe-menu.png) + +| Menu | Description | +| :--- | :--- | +| Delete | Delete this keyframe | Duplicate | Auxiliary keyframe. +| Copy | Auxiliary keyframe, after copying, you can paste the information of the keyframe when clicking other keyframes or blank space | +| Paste | Copy the previously copied keyframe to the mouse clicked position or replace the information of the selected keyframe | +| Edit | Click the popup box to change the time and value of the current keyframe.
![keyframe-menu.png](./animation-auxiliary-curve/edit-pop.png) +| Flag tangent | When clicked, the tangent line of the curve at that point will switch to horizontal mode
![flat.png](./animation-auxiliary-curve/flat.png) +| Interpolation Mode | See [Linear Interpolation Mode](./curve-editor.md#Interpolation%20Mode) | +| Tangent weight mode | See [Tangent weight mode](./curve-editor.md#Tangent%20Weight%20Mode) | diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/add-curve.png b/versions/4.0/en/animation/animation-auxiliary-curve/add-curve.png new file mode 100644 index 0000000000..46af0aad67 Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/add-curve.png differ diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/create-key-frame.png b/versions/4.0/en/animation/animation-auxiliary-curve/create-key-frame.png new file mode 100644 index 0000000000..c4fb841c31 Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/create-key-frame.png differ diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/edit-pop.png b/versions/4.0/en/animation/animation-auxiliary-curve/edit-pop.png new file mode 100644 index 0000000000..80d1da9650 Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/edit-pop.png differ diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/enable.png b/versions/4.0/en/animation/animation-auxiliary-curve/enable.png new file mode 100644 index 0000000000..312fa506ac Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/enable.png differ diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/flat.png b/versions/4.0/en/animation/animation-auxiliary-curve/flat.png new file mode 100644 index 0000000000..1981efb9ff Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/flat.png differ diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/keyframe-menu.png b/versions/4.0/en/animation/animation-auxiliary-curve/keyframe-menu.png new file mode 100644 index 0000000000..fd35e3cf65 Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/keyframe-menu.png differ diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/menu.png b/versions/4.0/en/animation/animation-auxiliary-curve/menu.png new file mode 100644 index 0000000000..0499bf0f25 Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/menu.png differ diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/overview.png b/versions/4.0/en/animation/animation-auxiliary-curve/overview.png new file mode 100644 index 0000000000..ca0b76b2f6 Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/overview.png differ diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/right-menu.png b/versions/4.0/en/animation/animation-auxiliary-curve/right-menu.png new file mode 100644 index 0000000000..7fec349e70 Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/right-menu.png differ diff --git a/versions/4.0/en/animation/animation-auxiliary-curve/start-edit.png b/versions/4.0/en/animation/animation-auxiliary-curve/start-edit.png new file mode 100644 index 0000000000..3fbc6f8dcc Binary files /dev/null and b/versions/4.0/en/animation/animation-auxiliary-curve/start-edit.png differ diff --git a/versions/4.0/en/animation/animation-clip.md b/versions/4.0/en/animation/animation-clip.md new file mode 100644 index 0000000000..1d6be67f44 --- /dev/null +++ b/versions/4.0/en/animation/animation-clip.md @@ -0,0 +1,29 @@ +# Animation Clip + +An Animation Clip is an asset containing animation data, which is one of the core elements of the animation system. By attaching an Animation Clip on the [Animation Component](animation-comp.md), this animation data can be applied to the node where the Animation Component is located. + +Currently, Creator supports importing Skeletal Animation assets produced by external art tools or by creating a new Animation Clip asset directly inside Creator. + +## Animations created inside Creator + +New Animation Clips can be created, edited, and previewed directly through the **Animation** panel, please refer to the [Animation Editor](animation.md) documentation for details. + +This can also be done by scripting, please refer to the [Using Animation Curves](use-animation-curve.md) documentation for details. + +## Externally imported skeleton animations + +Externally imported animations include the following: + +1. Skeletal Animations produced by third-party art tools. + +2. Skeletal Animations that come with the model after it is imported. + +When a model that contains animations is imported, the animation contained in the model will be imported at the same time. This animation is used in the same way as the internal new assets, and the crop of skeletal animation can be referred to [Introduction to the animation module of model assets](../asset/model/mesh.md) documentation. + +Starting with v3.4, Cocos Creator has introduced the new Marionette animation system which implements a skeletal animation flow controlled by a state machine, please refer to the [Marionette Animation System](./marionette/index.md) documentation. + +For additional details about Skeletal Animation settings, please refer to the [Skeletal Animation](skeletal-animation.md) documentation. + +> **Note**: skeletal animations imported externally cannot be viewed and edited in the **Animation** panel. The nodes are locked, and can only be edited in external art tools. +> +> ![skeletal animation](animation-clip/skeletal-animation.png) diff --git a/versions/4.0/en/animation/animation-clip/skeletal-animation.png b/versions/4.0/en/animation/animation-clip/skeletal-animation.png new file mode 100644 index 0000000000..06dcb920f1 Binary files /dev/null and b/versions/4.0/en/animation/animation-clip/skeletal-animation.png differ diff --git a/versions/4.0/en/animation/animation-comp.md b/versions/4.0/en/animation/animation-comp.md new file mode 100644 index 0000000000..022d3e148d --- /dev/null +++ b/versions/4.0/en/animation/animation-comp.md @@ -0,0 +1,25 @@ +# Animation Component Reference + +The Animation component can drive node and component properties on their nodes and children in an animated manner, including properties in user-defined scripts. + +![animation component](./animation-create/animation-component.png) + +There are several ways to add Animation components: + +1. Add an Animation component in the **Animation** panel, review the [Creating Animation Components and Animation Clips](animation-create.md) documentation for details. +2. Select the node to be animated in the **Hierarchy** panel, and then select **Add Component -> Animation -> Animation** in the **Inspector** panel to add an Animation component to the node. +3. Added via script, review the [Controlling Animation with Scripts](animation-component.md) documentation for details. + +## Animation Component Properties + +| Property | Description | +| :-- | :------ | +| Clips | The added animation clip asset, default is empty, support adding multiple. The AnimationClip added here can be edited directly in the **Animation** panel. | +| DefaultClip | The default animation clip. If this option is attached with the **PlayOnLoad** property checked, then the animation will automatically play the content of the Default Clip when it is loaded. +| PlayOnLoad | Boolean type. If this option is checked, the content of the Default Clip will be played automatically after the animation is loaded. + +## Detailed Description + +If an animation needs to contain multiple nodes, usually one creates a new node as the **root node** of the animation, and then the Animation component will be attached to the root node. All the other children nodes under this root node will be automatically entered into this animation clip and displayed in the **Node List** area of the **Animation** panel. For details, please refer to the [Get Familiar with the Animation Panel - Node list](animation-editor.md#2-node-list) documentation. + +The Animation component also provides some common animation control functions. To control animations using scripts, please refer to the [Controlling Animation with Scripts](animation-component.md) documentation. diff --git a/versions/4.0/en/animation/animation-component.md b/versions/4.0/en/animation/animation-component.md new file mode 100644 index 0000000000..c77d4279b8 --- /dev/null +++ b/versions/4.0/en/animation/animation-component.md @@ -0,0 +1,179 @@ +# Controlling Animation with Scripts + +## Animation Component + +The Animation Component manages a set of animation states that control the play, pause, resume, stop, switch, etc. of each animation. The Animation Component creates a corresponding [Animation State](animation-state.md) object for each Animation Clip. The Animation State is used to control the Animation Clips that need to be used on the object. + +In the Animation Component, Animation States are identified by name, and the default name of each animation state is the name of its Animation Clip. + +Adding an Animation Component to a node in a script is done in the following way: + +```ts +import { Animation, Node } from 'cc'; + +function (node: Node) { + const animationComponent = node.addComponent(Animation); +} +``` + +### Playing and switching animations + +#### Playing animations + +The Animation Component controls the playback of the specified animation via the [play()](%__APIDOC__%/en/class/Animation?id=play) API, e.g: + +```ts +// Play the animation state 'idle' +animationComponent.play('idle'); +``` + +If no specific animation is specified and `defaultClip` is set when using `play` to play an animation, the `defaultClip` animation will be played. If the `playOnLoad` of the Animation Component is also set to `true`, the Animation Component will automatically play the contents of `defaultClip` on the first run. + +```ts +// The defaultClip animation will be played if the defaultClip is set without specifying the animation to be played +animationComponent.play(); +``` + +#### Switching animation + +When using the `play` interface to play an animation, if there are other animations playing at the same time, the other animations will be stopped immediately. This switch is very abrupt, and in some cases we want the switch to be a "fade in and fade out" effect, use [crossFade()](%__APIDOC__%/en/class/Animation?id=crossFade) to smoothly complete the switch within the specified period. For example: + +```ts +// Play the animation state 'walk' +animationComponent.play('walk'); + +/* ... */ + +// Smoothly switch from walk to run animation in 0.3 seconds +animationComponent.crossFade('run', 0.3); +``` + +This fade-in and fade-out mechanism of `crossFade()` makes it possible for more than one Animation State to be playing at the same moment. Therefore, the Animation Component has no concept of the **current animation**. + +Even so, the Animation Component provides `pause()`, `resume()`, and `stop()` methods that pause, resume, and stop all the animation states being played while also pausing, resuming, and stopping the animation switching. + +**Notes:Only take effect on REALTIME Mode spine Animation** + +For more details about the control interface related to the Animation Component, please refer to the [Class Animation](%__APIDOC__%/en/class/Animation). + +## Animation State + +The Animation Component only provides some simple control functions, which are mostly sufficient and easy to use, but for more animation information and animation control interfaces, review the [Animation State](animation-state.md) documentation. + +## Frame Events + +The **Animation** panel supports visual editing of [event frames](animation-event.md), or frame events can be added directly to the script. + +The `events` of an `AnimationClip` contain all the frame events of this animation, each with the following properties: + +```ts +{ + frame: number; + func: string; + params: any[]; +} +``` + +- `frame`: the point in time when the event will be triggered, in seconds. For example, `0.618` means the event will be triggered when the animation reaches the 0.618th second. For the conversion between timeline scale units, please refer to the [Scale Unit Display of the Timeline](animation-editor.md#scale-unit-display-of-the-timeline) documentation. +- `func`: indicates the name of the function that will be called back when the event is triggered. When the event is triggered, the animation system will search **all the components in the animation root node**, if there is a component that implements the function specified in `func` of the animation event, it will call it and pass in the parameters in `params`. + +For example, if an event frame is added at 0.5s of the animation timeline: + +![keyframe](./animation/keyframe.png) + +The code implemented in the script is as follows: + +```ts +{ + frame: 0.5; + func: 'onTrigger'; + params: [ 0 ]; +} +``` + +### Example + +The following code indicates that the default Animation Clip of the Animation Component of the node where the `MyScript` script component is located will call the `onTriggered()` method of the `MyScript` component when it reaches the 0.5 second mark and pass the parameter `0`. + +```ts +import { Animation, Component, _decorator } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("MyScript") +class MyScript extends Component { + public start() { + const animation = this.node.getComponent(Animation); + if (animation && animation.defaultClip) { + const { defaultClip } = animation; + defaultClip.events = [ + { + frame: 0.5, // Triggers the event on the 0.5 second + func: 'onTriggered', // The name of the function to call when the event is triggered + params: [ 0 ], // Parameters passed to `func` + } + ]; + + animation.clips = animation.clips; + } + } + + public onTriggered(arg: number) { + console.log('I am triggered!', arg); + } +} +``` + +## Animation Events + +In addition to the callbacks provided by the frame events in the **Animation** panel, the animation system also provides a way to call back animation events. The currently supported callback events include: + +- `PLAY`: triggered when playback starts +- `STOP`: triggered when playback is stopped +- `PAUSE`: triggered when playback is paused +- `RESUME`: triggered when playback is resumed +- `LASTFRAME`: if the animation loop is greater than 1, triggered when the animation reaches the last frame. +- `FINISHED`: trigger when the animation is finished. + +These events are defined in `Animation.EventType` and the code examples is as follows. + +SkeletalAnimation: + +```ts +import { _decorator, Component, Node, SkeletalAnimation, Animation, SkeletalAnimationState } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('SkeletonAnimationEvent') +export class SkeletonAnimationEvent extends Component { + start() { + + let skeletalAnimation = this.node.getComponent(SkeletalAnimation); + skeletalAnimation.on(Animation.EventType.FINISHED, this.onAnimationFinished, this); + } + + onAnimationFinished(type:Animation.EventType, state:SkeletalAnimationState){ + + } +} +``` + +Animation: + +```ts +import { _decorator, Component, Node, Animation, AnimationState } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('AnimationEvent') +export class AnimationEvent extends Component { + + start() { + let animation = this.node.getComponent(Animation); + animation.on(Animation.EventType.FINISHED, this.onAnimationEvent, this) + } + + onAnimationEvent(type: Animation.EventType, state: AnimationState) { + + } +} +``` + +For more details, please refer to the [Animation.EventType](%__APIDOC__%/en/namespace/Animation?id=EventType) API. diff --git a/versions/4.0/en/animation/animation-create.md b/versions/4.0/en/animation/animation-create.md new file mode 100644 index 0000000000..bda9c2c92f --- /dev/null +++ b/versions/4.0/en/animation/animation-create.md @@ -0,0 +1,43 @@ +# Creating Animation Components and Animation Clips + +Before using the **Animation** panel to create animation, select the node to add animation to in the **Hierarchy** panel or the **Scene** panel, then add the [Animation Component](./animation-comp.md) and mount the **Animation Clip** on the component to edit the animation data, and the edited animation data will be saved in the current Animation Clip. The animation data of the nodes that do not have a Clip mounted cannot be edited. + +If the currently selected node does not have an Animation Component, the **Add Animation Component** button will be displayed on the interface of the **Animation** panel, click it to add **Animation Component** on the **Inspector** panel. + +![add component](./animation-create/add-component.png) + +Go ahead and click the **Create a new AnimationClip file** button in the **Animation** panel and name it (e.g.: `animation`). + +![add clip](./animation-create/add-clip.png) + +An animation clip (`animation.anim`) is automatically created in the **Assets** panel and mounted to the `DefaultClip` property of the Animation component: + +![mount clip](./animation-create/mount-clip.png) + +The above briefly describes how to create Animation components and animation clips in the **Animation** panel, for more information about the creation and properties of Animation components, please refer to the [Animation Component](./animation-comp.md) documentation. For additional information on how to create animation clips, please refer to the end of this article. + +Next, click **Enter animation editing mode** to start [edit-animation-clip](edit-animation-clip.md). The newly created empty Animation Clip is displayed in the animation editor as follows: + +![empty clip](./animation-create/empty-clip.png) + +## Attaching new animation clips + +An Animation Component can have multiple Animation Clips attached to it. To additionally create and attach a new animation clip on the object of an existing Animation Clip, there are several ways to do so: + +1. Click the **+** button at the top left of the **Assets** panel, or right-click on a blank area and select **Animation Clip**, which will generate an animation clip file (default name `animation`) in the **Assets** panel. + + Next, select the corresponding node in the **Hierarchy** panel, find the Animation Component (`cc.Animation`) in the **Inspector** panel, and change the value of the `Clips` property. For example, if only one clip file is attached, and to add another one, change the original **1** to **2**. + + ![add-clip](./animation-create/add-new-clip.png) + + Finally, drag the Animation Clip just created in the **Assets** panel to the `cc.AnimationClip` selection box in the above image. + +2. Find the Animation component (`cc.Animation`) in the **Inspector** panel and change the value of the `Clips` property. + + Next, click the **Find** button behind the new empty `cc.AnimationClip` selection box, and click the **Create** button at the top right of the pop-up search window to automatically create an Animation Clip in the **Assets** panel and mount it to the `cc.AnimationClip` selection box. + + ![add-clip](./animation-create/add-new-clip2.png) + +3. To create the Animation Clip dynamically by using a script, please refer to the [use-animation-curve](use-animation-curve.md) documentation for details. + +The Animation Clips to edit can be switched from the **Clips** drop-down list in the top left corner of **Animation** panel. diff --git a/versions/4.0/en/animation/animation-create/add-clip.png b/versions/4.0/en/animation/animation-create/add-clip.png new file mode 100644 index 0000000000..a028d15c4f Binary files /dev/null and b/versions/4.0/en/animation/animation-create/add-clip.png differ diff --git a/versions/4.0/en/animation/animation-create/add-component.png b/versions/4.0/en/animation/animation-create/add-component.png new file mode 100644 index 0000000000..b61266c623 Binary files /dev/null and b/versions/4.0/en/animation/animation-create/add-component.png differ diff --git a/versions/4.0/en/animation/animation-create/add-new-clip.png b/versions/4.0/en/animation/animation-create/add-new-clip.png new file mode 100644 index 0000000000..9f7cbba5f5 Binary files /dev/null and b/versions/4.0/en/animation/animation-create/add-new-clip.png differ diff --git a/versions/4.0/en/animation/animation-create/add-new-clip2.png b/versions/4.0/en/animation/animation-create/add-new-clip2.png new file mode 100644 index 0000000000..93c05e0268 Binary files /dev/null and b/versions/4.0/en/animation/animation-create/add-new-clip2.png differ diff --git a/versions/4.0/en/animation/animation-create/animation-component.png b/versions/4.0/en/animation/animation-create/animation-component.png new file mode 100644 index 0000000000..f778df4aba Binary files /dev/null and b/versions/4.0/en/animation/animation-create/animation-component.png differ diff --git a/versions/4.0/en/animation/animation-create/empty-clip.png b/versions/4.0/en/animation/animation-create/empty-clip.png new file mode 100644 index 0000000000..7e985ddf20 Binary files /dev/null and b/versions/4.0/en/animation/animation-create/empty-clip.png differ diff --git a/versions/4.0/en/animation/animation-create/mount-clip.png b/versions/4.0/en/animation/animation-create/mount-clip.png new file mode 100644 index 0000000000..b605e29a7e Binary files /dev/null and b/versions/4.0/en/animation/animation-create/mount-clip.png differ diff --git a/versions/4.0/en/animation/animation-curve.md b/versions/4.0/en/animation/animation-curve.md new file mode 100644 index 0000000000..f5de8843df --- /dev/null +++ b/versions/4.0/en/animation/animation-curve.md @@ -0,0 +1,30 @@ +# Curve Editing View + +After creating the basic animation clip, one can **control how the keyframes change over time between keyframes by editing the animation curve**. An animation curve is actually a linear track made up of individual keyframes connected on the current property track, with each keyframe being a control point on this curve path. Under the curve editing view, the change of keyframe values is more visually noticed, and keyframe value changes can be adjusted more delicately. + +![curve btn](animation-curve/curve-btn.png) + +Click the switch view button on the side of the properties list to switch to the curve editing view. + +After switching to the curve editing view, you can see that the line markers will be displayed on the property list, and the line markers will not be displayed on the property tracks that do not support curve editing, and the curves will not be displayed normally when the corresponding property track is selected. When the corresponding curve is displayed in the curve editing area, the line marker will show the corresponding curve color, and the name of the currently selected curve will be displayed in the property menu area. + +![show-line](animation-curve/show-line.png) + +## Getting to know the curve editor + +The **Animation** panel has a built-in curve editor for animation curves, which is actually the same as the particle curve editor. On top of the general curve editor, the **Animation** panel has some customization to better support curve keyframe editing. + +For general functions of the curve editor, please refer to the [Curve Editor](./curve-editor.md) documentation for more information. + +In addition to general curve editing operations, the curve editor area within the **Animation** panel supports the following functions. + +- To show all keyframes, click the button above the curve editor area ![show-all-keys](./animation-curve/show-all-keys.png), or press the F shortcut to quickly scale the current view area to show all keyframes. +- Shortcuts and handling of all keyframes in the keyframe view of the **Animation** panel, such as double-clicking on the keyframe time line to move to the keyframe position, copy and paste shortcuts, etc. +- Copy and paste in curve editing area is shared with keyframe view mode, so you can paste copied keyframe data with each other in different display views. +- The number of intervals used for the interval alignment keyframe function of the curve editor will be synchronized with the number of intervals in the menu bar of the **Animation** panel. + +### Bezier Curve Preset + +There are some standard Bezier curve presets built into the animation editor. After entering the curve editing view, click the curve preset panel in the sidebar to show the curve presets. Click the curve to select the curve clip, and then click the preset on the curve preset panel you can applying the curve preset. After applying the preset, the interpMode of the keyframes at both ends of the curve clip will be adjusted to curve mode by default, and the weightMode of the keyframes will be modified to a state where the left and right sides of the curve clip can be stretched freely to achieve the same effect as the curve preset. + +![animation-curve-preset](animation-curve/animation-curve-preset.gif) diff --git a/versions/4.0/en/animation/animation-curve/add-keyframe-to-curve.png b/versions/4.0/en/animation/animation-curve/add-keyframe-to-curve.png new file mode 100644 index 0000000000..c327b1f885 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/add-keyframe-to-curve.png differ diff --git a/versions/4.0/en/animation/animation-curve/add-keyframe.png b/versions/4.0/en/animation/animation-curve/add-keyframe.png new file mode 100644 index 0000000000..816239da4e Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/add-keyframe.png differ diff --git a/versions/4.0/en/animation/animation-curve/animation-curve-preset.gif b/versions/4.0/en/animation/animation-curve/animation-curve-preset.gif new file mode 100644 index 0000000000..3f7a16801c Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/animation-curve-preset.gif differ diff --git a/versions/4.0/en/animation/animation-curve/change-preset.gif b/versions/4.0/en/animation/animation-curve/change-preset.gif new file mode 100644 index 0000000000..e5ca97aef6 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/change-preset.gif differ diff --git a/versions/4.0/en/animation/animation-curve/constant.png b/versions/4.0/en/animation/animation-curve/constant.png new file mode 100644 index 0000000000..e0b397f328 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/constant.png differ diff --git a/versions/4.0/en/animation/animation-curve/cubic.png b/versions/4.0/en/animation/animation-curve/cubic.png new file mode 100644 index 0000000000..d0cfaff2f6 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/cubic.png differ diff --git a/versions/4.0/en/animation/animation-curve/curve-btn.png b/versions/4.0/en/animation/animation-curve/curve-btn.png new file mode 100644 index 0000000000..36eda835b4 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/curve-btn.png differ diff --git a/versions/4.0/en/animation/animation-curve/edit-key.png b/versions/4.0/en/animation/animation-curve/edit-key.png new file mode 100644 index 0000000000..997827d5aa Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/edit-key.png differ diff --git a/versions/4.0/en/animation/animation-curve/interopMode-key.gif b/versions/4.0/en/animation/animation-curve/interopMode-key.gif new file mode 100644 index 0000000000..74dac4f745 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/interopMode-key.gif differ diff --git a/versions/4.0/en/animation/animation-curve/interopMode.png b/versions/4.0/en/animation/animation-curve/interopMode.png new file mode 100644 index 0000000000..ae1a9d40ab Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/interopMode.png differ diff --git a/versions/4.0/en/animation/animation-curve/linear.png b/versions/4.0/en/animation/animation-curve/linear.png new file mode 100644 index 0000000000..d88fa70b5c Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/linear.png differ diff --git a/versions/4.0/en/animation/animation-curve/main.png b/versions/4.0/en/animation/animation-curve/main.png new file mode 100644 index 0000000000..78333b342f Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/main.png differ diff --git a/versions/4.0/en/animation/animation-curve/move-keys.gif b/versions/4.0/en/animation/animation-curve/move-keys.gif new file mode 100644 index 0000000000..0eb5726707 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/move-keys.gif differ diff --git a/versions/4.0/en/animation/animation-curve/preset.gif b/versions/4.0/en/animation/animation-curve/preset.gif new file mode 100644 index 0000000000..3ed087067a Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/preset.gif differ diff --git a/versions/4.0/en/animation/animation-curve/save-curve.gif b/versions/4.0/en/animation/animation-curve/save-curve.gif new file mode 100644 index 0000000000..2550586434 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/save-curve.gif differ diff --git a/versions/4.0/en/animation/animation-curve/scale-keys.gif b/versions/4.0/en/animation/animation-curve/scale-keys.gif new file mode 100644 index 0000000000..369a73cce1 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/scale-keys.gif differ diff --git a/versions/4.0/en/animation/animation-curve/select-key.gif b/versions/4.0/en/animation/animation-curve/select-key.gif new file mode 100644 index 0000000000..92f57f44cf Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/select-key.gif differ diff --git a/versions/4.0/en/animation/animation-curve/select-preset.gif b/versions/4.0/en/animation/animation-curve/select-preset.gif new file mode 100644 index 0000000000..77afdc90b4 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/select-preset.gif differ diff --git a/versions/4.0/en/animation/animation-curve/show-all-keys.png b/versions/4.0/en/animation/animation-curve/show-all-keys.png new file mode 100644 index 0000000000..828928e7c9 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/show-all-keys.png differ diff --git a/versions/4.0/en/animation/animation-curve/show-line.png b/versions/4.0/en/animation/animation-curve/show-line.png new file mode 100644 index 0000000000..eb269b990d Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/show-line.png differ diff --git a/versions/4.0/en/animation/animation-curve/tangentWeightMode-key.gif b/versions/4.0/en/animation/animation-curve/tangentWeightMode-key.gif new file mode 100644 index 0000000000..278837b6a1 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/tangentWeightMode-key.gif differ diff --git a/versions/4.0/en/animation/animation-curve/tangentWeightMode.png b/versions/4.0/en/animation/animation-curve/tangentWeightMode.png new file mode 100644 index 0000000000..ee15949957 Binary files /dev/null and b/versions/4.0/en/animation/animation-curve/tangentWeightMode.png differ diff --git a/versions/4.0/en/animation/animation-editor.md b/versions/4.0/en/animation/animation-editor.md new file mode 100644 index 0000000000..cfda69fcb2 --- /dev/null +++ b/versions/4.0/en/animation/animation-editor.md @@ -0,0 +1,178 @@ +# Get Familiar with the Animation Panel + +The **Animation** panel is used to edit and preview the animation clip of the currently selected node. When editing animation data or related properties, it will take effect immediately after mouse focus leaves. + +The **Animation** panel is already included in the default layout of Cocos Creator, or open the **Animation** panel by clicking **Panel -> Animation -> Animation Editor** in the main menu above the editor. + +## Panel Introduction + +The **Animation** panel can be divided into the following main sections. + +![animation-editor](animation-editor/animation-panel.png) + +1. **Toolbar**: some common function buttons, such as play/pause/stop animation, add event frame, save animation, quit editing, etc. +2. **Node List**: used to display/set the currently selected node and its child nodes. +3. **Animation timeline and Keyframe Preview**: used to display/set the event frames added to the current node (and its children) and the preview of all keyframes on the added animation property. +4. **Animation Property List**: used to display/set all animation properties included in the currently edited animation clip for the currently selected node. +5. **Animation Property Keyframe Preview**: used to display/set all keyframes on each animation property of the currently selected node. +6. **Related properties of the current animation clip**: used to set the playback mode, playback speed, etc. of the current animation clip. + +For specific content, please refer to the following introduction. + +### 1. Menu Toolbar + +The function buttons in the **Menu toolbar** are, from left to right. + +| Icon | Description | Shortcut | +| :---- | :--- | :--- | +| ![menu clips](animation-editor/menu_clips.png) | Toggle animation clips. When multiple animation clips are attached on the Animation component, this button can be used to toggle between clips. | - | +| ![jump to first frame](animation-editor/menu_first.png) | Moves the red time control line to the beginning of the current animation clip | Ctrl/Cmd + | +| ![jump to previous frame](animation-editor/menu_prev.png) | Move time control line to previous frame | | +| ![play/pause](animation-editor/menu_play.png) | Play/pause current animation | Ctrl/Cmd + P | +| ![jump to next frame](animation-editor/menu_next.png) | Move the time control line to the next frame | | +| ![jump to last frame](animation-editor/menu_last.png) | Move the time control line to the end of the current animation clip | Ctrl/Cmd + | +| ![stop](animation-editor/menu_stop.png) | Click to stop the current animation, the time control line will move to the first frame after stopping | Ctrl/Cmd + S | +| ![time](animation-editor/menu_time.png) | The scale unit of the timeline are displayed in three ways:
**Time: 0-00**
**Frame: 0**
**Time (s): 0 s**
See the **Scale Units of the Timeline** section below for details. | - | +| ![spacing](animation-editor/menu-spacing.png)| Used to set the spacing size between keyframes when multiple keyframes are generated at the same time. | - | +| ![spacing](animation-editor/menu_spacing_btn.png) | Spacing the selected keyframes at the same time. The selected keyframes will be spaced according to the first frame, using the value in the **Spacing** input box as the spacing size. +| ![add event](animation-editor/menu_event.png) | Add event frame, click this button to add an event frame above the current timeline | - | +| ![menu save](animation-editor/menu_save.png) | Save the currently edited animation data. | - | +| ![menu doc](animation-editor/menu_doc.png) | Click this button to jump to the official manual section on the **Animation** panel | Ctrl/Cmd + S | +| ![shortcuts](./animation-editor/shortcuts.png) | Open the shortcuts panel, which supports custom shortcuts. See the **Shortcuts** section below for details | - | +| ![exit](animation-editor/menu_exit.png)| Exit animation editing mode | Ctrl + Q | + +### 2. Node list + +![node list](animation-editor/2-node-list.png) + +This area shows the currently selected animation nodes and their children, arranged in a way that corresponds to the node tree in the **Hierarchy** panel. Click on the top ![hide](animation-editor/hide-node.png) button to hide/show nodes without animation data, or just type the node name in the input box to quickly find the node. + +Currently, it is possible to right-click on a node to clear, migrate, copy and paste node data. For details, please refer to the [Common operations on node data](edit-animation-clip.md#common-operations-on-node-data) documentation. + +![node operation](animation-editor/node-operation.png) + +### 3. Animation timeline and keyframe preview + +![animation timeline](animation-editor/animation-timeline.png) + +The animation timeline is mainly used to display/set the custom [event frames](animation-event.md) added to the current node, the [keyframes](edit-animation-clip.md) (blue diamond) of all animation properties added to the node (and its children) and to show the preview. + +The time control line (red vertical line) indicates the time the current animation is at, and the currently selected time can be changed in several ways: + +- Drag and drop the time control line directly +- Double click on the keyframe +- Using the relevant move control buttons in the menu toolbar +- Use shortcut keys, which can be found in the **Shortcuts** section below +- Click anywhere in the upper area of the animation timeline + + ![about the timeline](animation-editor/above-timeline.png) + +If the time control line is moved, the nodes in the **Scene** panel will also move accordingly to the animation track. + +![move line](animation-editor/move-line.gif) + +#### Scale unit display of the timeline + +The ![time](animation-editor/menu_time.png) button in the **Toolbar** can be used to toggle the way the scale unit of the timeline is displayed. The value in the input box will change with the movement of the time control line. Manual input is supported, and the time control line will be automatically positioned to the corresponding position after the input is finished. + +The following three display methods are currently supported: + +- **Time: 0-00** (default): the animation timeline scale is displayed as a combination of seconds and frames. The value in front of the input box means **seconds**, and the value after it means **frames**, for example `01-05` means 1 second and 5 frames. + + ![time](animation-editor/time.png) + +- **Frame: 0**: display the scale of the animation timeline in terms of frames. + + ![frame](animation-editor/frame.png) + +- **Time(s): 0 s**: display the scale of the animation timeline in seconds. + + ![times](animation-editor/times.png) + +The frame rate (Sample) is generally used to indicate how many frames a second is to be divided into, which can be adjusted in the **Sample** option at the bottom of the **Animation** panel. This is affected to a different extent when the scale unit of the timeline is displayed in different ways. + +When the scale unit is set to **Frame**, it is in frames and is not affected by the frame rate.
+When set to **Time** or **Time(s)**, the same scale represents a different point in time as the frame rate changes, and the conversion between the two is calculated as follows: + +| Frame Rate (Sample) | Time: 00-00 | Time (s): 0 s | +| :-- | :--------- | :------------ | +| 30 | 01-05 | 1 + 5/30 = 1.17 s | +| 10 | 01-05 | 1 + 5/10 = 1.5 s | + +For example, if the frame rate is set to 30 and a keyframe is added to the `01-05` scale, the keyframe will be at frame 35 after the animation starts. Changing the frame rate to 10, the total number of frames where the keyframe is located does not change, it is still at frame 35 after the start of the animation, and the scale reading at the keyframe location becomes `03-05`, which translates to exactly 3 times the previous time (s). + +#### Changing the animation timeline scale + +If the **Animation** panel display is too small and needs to be enlarged for more keyframes to be displayed within the editor, Just simply scroll the mouse wheel in either **Animation Timeline** and **Animation Property Keyframe Preview** area to scale the timeline up or down. + +![scale](./animation-editor/scale.gif) + +#### Moving the animation timeline display area + +Drag and drop the middle/right mouse button in any area of the **Animation Timeline** or **Animation Property Keyframe preview** to see the hidden keyframes on the left/right side of the animation timeline that are outside the display area. + +![scale canvas](./animation-editor/scale-canvas.gif) + +### 4. List of animation property + +![node list](./animation-editor/4-pro-list.png) + +This area is mainly used to display/add/set the animation property of the currently selected node in the current Animation Clip. The animation properties include the node's own properties, component properties (including those in user-defined script components), and the animation properties are added by clicking the **+** button in the upper right corner. Component properties are prefixed with the name of the component, e.g.: `cc.Sprite.spriteFrame`. + +Right-click on the animation property or click ![property](./animation-editor/set-pro.png) button to the right of the animation property to select **Remove Prop Track**, **Clear Keyframe Data**, or **Copy Prop Track**. For details, please refer to the [Common operations on animation property data](edit-animation-clip.md#Common-operations-on-animation-property-data) documentation. + +![property operation](./animation-editor/pro-operation.png) + +### 5. Keyframe preview of the animation property + +![animation property track](./animation-editor/animation-property-track.png) + +This area is mainly used to display the specific keyframe settings of the currently selected node on each animation property, and is also the main area for keyframe editing. Keyframes can be added by right-clicking on the animation property on the right side. This area also supports the ability to move, copy, and paste keyframes by checking and tapping on them. For details, please refer to the [Common keyframe operations](edit-animation-clip.md#common-keyframe-operations) documentation. + +When clicking on a keyframe, it will turn from blue to white and information about it will be displayed above the area, and double-clicking on the keyframe will move the time control line to its location. + +![key info](animation-editor/key-info.png) + +### 6. Related properties of the current animation clip + +The properties of the current Animation Clip that can be set directly on the **Animation** panel include: **WrapMode**, **Sample**, **Speed** and **Duration**. For details, please refer to the [Animation Clip Properties Settings](edit-animation-clip.md#animation-clip-properties-settings) documentation. + +## Adjusting the Layout of the Animation Panel + +The dividers between the node list and the timeline, and between the property list and the timeline are available for dragging and dropping to change the layout. By dragging and dropping it is possible to freely adjust the **Animation** panel to a layout effect suitable for editing. + +![layout](./animation-editor/layout.gif) + +## Shortcut keys + +Click on the menu toolbar's ![shortcuts](./animation-editor/shortcuts.png) button in the menu toolbar to open the shortcuts panel: + +![shortcuts manager](./animation-editor/shortcuts-manager.png) + +Shortcuts can be customized in the **Animation** panel. Directly click the shortcut key combination of the function to be modified. A prompt will appear as shown below. + +![shortcuts change](./animation-editor/shortcuts-change.png) + +### Default shortcut key summary + +Function | Shortcut | Description +:--- | :----- | :--- +Enter/exit **Animation** panel | Ctrl/Cmd + E | - +Save animation data | Ctrl/Cmd + S | - +Move forward one frame | | Ignore current operation if already at frame 0. Move the time control line when the keyframe is not selected, move the keyframe when the keyframe is selected. +Move back one frame | | Move the time control line when the keyframe is not selected, move the keyframe when the keyframe is selected. +Move to the first frame | Ctrl/Cmd + | Move the red time control line to the beginning of the current animation clip. +Move to the last frame | Ctrl/Cmd + | Move the time control line to the end of the current animation clip. +Delete the currently selected keyframe | Delete / Cmd + Backspace | - +Play/Pause Animation | P | - +Stop animation | Alt + S | The current time will change to 0 and the time control line will move to the first frame. +Add keyframe | K | When any animation property is selected, a keyframe will be added at the location of the time control line, ignored if no animation property is selected. +Jump to previous keyframe | Ctrl/Cmd + Shift + | Move the time control line to the nearest keyframe on the left (on the selected property track or on the selected node). +Jump to the next keyframe | Ctrl/Cmd + Shift + | Move the time control line to the nearest keyframe on the right (on the selected property track or on the selected node). +Multi-select keyframes | Ctrl | Hold down Ctrl and click on a keyframe to multi-select a keyframe. +Select all animation property keyframes | Ctrl/Cmd + A | Select all keyframes on the selected animation property. +Copy selected animation data | Ctrl/Cmd + C | Supports copying selected keyframes, property tracks, and node data, while only one type of data can be copied, and the copying priority is determined in this order. +Paste last copied animation data | Ctrl/Cmd + V | Support to paste last copied animation data (e.g.: animation keyframes, animation properties, animation node data, and check the currently available data in that order) across editors (v3.x only). +Unchecked keyframes/event frames/property tracks | Esc | - + +> **Note**: currently, all shortcuts require mouse focus on the Animation Editor panel to take effect, except for the shortcuts **Enter/Exit Animation Editor** and **Save Animation** which are globally available. diff --git a/versions/4.0/en/animation/animation-editor/2-node-list.png b/versions/4.0/en/animation/animation-editor/2-node-list.png new file mode 100644 index 0000000000..e1f55f79a8 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/2-node-list.png differ diff --git a/versions/4.0/en/animation/animation-editor/4-pro-list.png b/versions/4.0/en/animation/animation-editor/4-pro-list.png new file mode 100644 index 0000000000..ed3b197dd7 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/4-pro-list.png differ diff --git a/versions/4.0/en/animation/animation-editor/above-timeline.png b/versions/4.0/en/animation/animation-editor/above-timeline.png new file mode 100644 index 0000000000..3debddb753 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/above-timeline.png differ diff --git a/versions/4.0/en/animation/animation-editor/animation-panel.png b/versions/4.0/en/animation/animation-editor/animation-panel.png new file mode 100644 index 0000000000..e702b7d5e2 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/animation-panel.png differ diff --git a/versions/4.0/en/animation/animation-editor/animation-property-track.png b/versions/4.0/en/animation/animation-editor/animation-property-track.png new file mode 100644 index 0000000000..0c33b10e25 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/animation-property-track.png differ diff --git a/versions/4.0/en/animation/animation-editor/animation-timeline.png b/versions/4.0/en/animation/animation-editor/animation-timeline.png new file mode 100644 index 0000000000..e1e325bbed Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/animation-timeline.png differ diff --git a/versions/4.0/en/animation/animation-editor/frame.png b/versions/4.0/en/animation/animation-editor/frame.png new file mode 100644 index 0000000000..4093b2aea4 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/frame.png differ diff --git a/versions/4.0/en/animation/animation-editor/hide-node.png b/versions/4.0/en/animation/animation-editor/hide-node.png new file mode 100644 index 0000000000..d5258d84f2 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/hide-node.png differ diff --git a/versions/4.0/en/animation/animation-editor/key-info.png b/versions/4.0/en/animation/animation-editor/key-info.png new file mode 100644 index 0000000000..75d00cf5ee Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/key-info.png differ diff --git a/versions/4.0/en/animation/animation-editor/layout.gif b/versions/4.0/en/animation/animation-editor/layout.gif new file mode 100644 index 0000000000..a882fe8ce8 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/layout.gif differ diff --git a/versions/4.0/en/animation/animation-editor/menu-spacing.png b/versions/4.0/en/animation/animation-editor/menu-spacing.png new file mode 100644 index 0000000000..e20bce5e81 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu-spacing.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_clips.png b/versions/4.0/en/animation/animation-editor/menu_clips.png new file mode 100644 index 0000000000..48699315e8 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_clips.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_doc.png b/versions/4.0/en/animation/animation-editor/menu_doc.png new file mode 100644 index 0000000000..ca00e9c0ba Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_doc.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_event.png b/versions/4.0/en/animation/animation-editor/menu_event.png new file mode 100644 index 0000000000..e8eb1457a5 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_event.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_exit.png b/versions/4.0/en/animation/animation-editor/menu_exit.png new file mode 100644 index 0000000000..2e6ae62e00 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_exit.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_first.png b/versions/4.0/en/animation/animation-editor/menu_first.png new file mode 100644 index 0000000000..9d6a0f26fe Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_first.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_last.png b/versions/4.0/en/animation/animation-editor/menu_last.png new file mode 100644 index 0000000000..258dbac7bf Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_last.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_next.png b/versions/4.0/en/animation/animation-editor/menu_next.png new file mode 100644 index 0000000000..d4f1f50abc Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_next.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_play.png b/versions/4.0/en/animation/animation-editor/menu_play.png new file mode 100644 index 0000000000..920a812f01 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_play.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_prev.png b/versions/4.0/en/animation/animation-editor/menu_prev.png new file mode 100644 index 0000000000..3bd9d542f0 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_prev.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_save.png b/versions/4.0/en/animation/animation-editor/menu_save.png new file mode 100644 index 0000000000..c1eec3cbcd Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_save.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_spacing_btn.png b/versions/4.0/en/animation/animation-editor/menu_spacing_btn.png new file mode 100644 index 0000000000..9a732235a7 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_spacing_btn.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_stop.png b/versions/4.0/en/animation/animation-editor/menu_stop.png new file mode 100644 index 0000000000..63b7867cc1 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_stop.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_time.png b/versions/4.0/en/animation/animation-editor/menu_time.png new file mode 100644 index 0000000000..0193f7b9ff Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_time.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_time_frame.png b/versions/4.0/en/animation/animation-editor/menu_time_frame.png new file mode 100644 index 0000000000..0154ab7379 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_time_frame.png differ diff --git a/versions/4.0/en/animation/animation-editor/menu_time_times.png b/versions/4.0/en/animation/animation-editor/menu_time_times.png new file mode 100644 index 0000000000..b3e16b326f Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/menu_time_times.png differ diff --git a/versions/4.0/en/animation/animation-editor/move-line.gif b/versions/4.0/en/animation/animation-editor/move-line.gif new file mode 100644 index 0000000000..d5ee2c1393 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/move-line.gif differ diff --git a/versions/4.0/en/animation/animation-editor/mult-select.png b/versions/4.0/en/animation/animation-editor/mult-select.png new file mode 100644 index 0000000000..f40f5ade3a Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/mult-select.png differ diff --git a/versions/4.0/en/animation/animation-editor/node-operation.png b/versions/4.0/en/animation/animation-editor/node-operation.png new file mode 100644 index 0000000000..13c7917ebe Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/node-operation.png differ diff --git a/versions/4.0/en/animation/animation-editor/pro-operation.png b/versions/4.0/en/animation/animation-editor/pro-operation.png new file mode 100644 index 0000000000..1d0d00172e Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/pro-operation.png differ diff --git a/versions/4.0/en/animation/animation-editor/scale-canvas.gif b/versions/4.0/en/animation/animation-editor/scale-canvas.gif new file mode 100644 index 0000000000..5961c9f5d3 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/scale-canvas.gif differ diff --git a/versions/4.0/en/animation/animation-editor/scale.gif b/versions/4.0/en/animation/animation-editor/scale.gif new file mode 100644 index 0000000000..cd091b90fb Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/scale.gif differ diff --git a/versions/4.0/en/animation/animation-editor/set-pro.png b/versions/4.0/en/animation/animation-editor/set-pro.png new file mode 100644 index 0000000000..8ab665f3be Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/set-pro.png differ diff --git a/versions/4.0/en/animation/animation-editor/shortcuts-change.png b/versions/4.0/en/animation/animation-editor/shortcuts-change.png new file mode 100644 index 0000000000..a45048436a Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/shortcuts-change.png differ diff --git a/versions/4.0/en/animation/animation-editor/shortcuts-manager.png b/versions/4.0/en/animation/animation-editor/shortcuts-manager.png new file mode 100644 index 0000000000..f18d8fca14 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/shortcuts-manager.png differ diff --git a/versions/4.0/en/animation/animation-editor/shortcuts.png b/versions/4.0/en/animation/animation-editor/shortcuts.png new file mode 100644 index 0000000000..74d05bfd13 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/shortcuts.png differ diff --git a/versions/4.0/en/animation/animation-editor/time.png b/versions/4.0/en/animation/animation-editor/time.png new file mode 100644 index 0000000000..4e4f4ae13c Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/time.png differ diff --git a/versions/4.0/en/animation/animation-editor/times.png b/versions/4.0/en/animation/animation-editor/times.png new file mode 100644 index 0000000000..cd00284823 Binary files /dev/null and b/versions/4.0/en/animation/animation-editor/times.png differ diff --git a/versions/4.0/en/animation/animation-event.md b/versions/4.0/en/animation/animation-event.md new file mode 100644 index 0000000000..9b6ca37481 --- /dev/null +++ b/versions/4.0/en/animation/animation-event.md @@ -0,0 +1,50 @@ +# Adding Animation Events + +Animation clips can be better enriched by calling **Animation Event** functions on the specified frame of the animation timeline. After adding **event frames** to a frame of the animation timeline, the animation system will match the corresponding function method in the root node of the animation with the name of the trigger function set in the event frame and execute it when the animation reaches that frame. + +To add Animation Events via scripts, please refer to the [Frame Event](animation-component.md#frame-event) documentation for more details. + +## Adding event frames + +Adding event frames in the **Animation** panel consists of two ways: + +1. Drag the time control line to the position where you want to add the event frame, and then click ![add event](animation-editor/menu_event.png) button in the **Animation Timeline** to add an event frame above the **Animation Timeline**. + +2. Right click on the area above the animation timeline and select **New Event Frame**. + +After the event frame is added, move the mouse over the event frame and the color will change from white to yellow. Right-click the event frame to perform **Edit**, **Delete**, **Copy-Paste** operations. Event frames can be batched while holding down Ctrl. + +![add-event](animation-event/animation-event-menu.png) + +- **Edit**: open the event editor and add event functions +- **Delete**: delete event frames +- **Copy/Paste**: copy and paste event frame data, supports cross-editor (v3.x) use. + +### Editing event frames + +Right-click on the added event frame and select **Edit** or double-click directly to open the event editor. In the event editor, it is possible to manually enter the name of the event function that needs to be triggered, and the trigger will match the corresponding function method in each component of the animation root node according to this function name, and call it with parameters. + +![event editor](animation-event/event-editor.png) + +- 1 - add a new trigger function. +- 2 - save the event function. +- 3 - fill in the name of the function to be triggered. +- 4 - delete the current event function. +- 5 - add incoming parameters, currently supports **String**, **Number**, **Boolean** types. + + ![add animation event](animation-event/add-animation-event.png) + +- 1 - add incoming parameters by choosing the parameter type as needed. +- 2 - delete all the added incoming parameters below. +- 3 - when the mouse moves over a parameter, this button will appear, click on it to delete the currently selected parameter. + +### Delete event frames + +Right-click on an event frame that has been added to the animation timeline (multiple selections are possible) and select **Delete** or use the shortcut Delete to delete the event frame and all event functions. + +### Copy and paste event frames + +Copy and paste of event frames and their event functions is supported across editors (v3.x). Copy and paste includes the following two ways of use: + +- After selecting an event frame (multiple selection is allowed), use the shortcut keys Ctrl + C and Ctrl + V to copy and paste. Note that the shortcut paste will start at the position of the current time control line. +- Once selecting an event frame (multiple selection is allowed), right-click on (any) event frame and select **Copy** in the pop-up menu, then right-click on top of the animation timeline and select **Paste Event Frame**. diff --git a/versions/4.0/en/animation/animation-event/add-animation-event.png b/versions/4.0/en/animation/animation-event/add-animation-event.png new file mode 100644 index 0000000000..4ae7ede0ff Binary files /dev/null and b/versions/4.0/en/animation/animation-event/add-animation-event.png differ diff --git a/versions/4.0/en/animation/animation-event/animation-event-menu.png b/versions/4.0/en/animation/animation-event/animation-event-menu.png new file mode 100644 index 0000000000..2b8e934ad2 Binary files /dev/null and b/versions/4.0/en/animation/animation-event/animation-event-menu.png differ diff --git a/versions/4.0/en/animation/animation-event/event-editor.png b/versions/4.0/en/animation/animation-event/event-editor.png new file mode 100644 index 0000000000..0b16cfcb69 Binary files /dev/null and b/versions/4.0/en/animation/animation-event/event-editor.png differ diff --git a/versions/4.0/en/animation/animation-keyFrames.md b/versions/4.0/en/animation/animation-keyFrames.md new file mode 100644 index 0000000000..290b3df0a6 --- /dev/null +++ b/versions/4.0/en/animation/animation-keyFrames.md @@ -0,0 +1,85 @@ +# Keyframe Editing View + +The node track display area and the property track display area are both displayed in keyframe editing view by default. In this view, one can edit the overall keyframe layout, add and delete keyframes more easily. The following describes the various keyframe manipulation methods supported in the keyframe edit view, and how to understand these methods and techniques to edit animation clips faster and more easily. + +## Selecting keyframes + +The selected keyframe will change from blue to white, including the following: + +- Clicking on a keyframe in the animation property track will select it. +- Double-clicking a keyframe will move the time control line to the current keyframe while it is selected. +- Clicking on a node's keyframe in the animation timeline selects all keyframes of the node's animation properties at the same location. + + ![choose keyframe](edit-animation-clip/choose-keyframe.png) + +Multiple selection of keyframes is also supported, including the following: + +- Holding Ctrl while clicking multiple keyframes + +- Selecting multiple keyframes by box selection will show the number of frames in the animation timeline for the first and last keyframes on the left and right side of the box. + + ![choose by box](edit-animation-clip/choose-by-box.png) + +- After **selecting any property track**, press Ctrl/Cmd + A to select all the keyframes on the current animation property track. + +## Adding keyframes + +In addition to adding keyframes by modifying properties as described in **Creating Animation Curves**, they can also be added in the following ways: + +1. Select the animation property and use the shortcut K, which will add a keyframe at the location of the time control line, or ignore it if the animation property is not checked. + +2. Move the time control line to the position where you want to add the keyframe, and click the ![add keyframe](edit-animation-clip/add-keyframe-button.png) button on the right side of the animation property to add a keyframe to the current animation property track. + +3. In the animation property track, right-click the position where you want to add the keyframe, and then select **Add keyframe** in the popup menu, the menu will also show the number of frames in the current added keyframe position. + + ![add keyframe](edit-animation-clip/add-keyframe.png) + +4. When dragging a batch of assets corresponding to an animation property from the **Assets** panel to the property track, keyframes will be added in the order in which the assets are selected, according to the spacing size set in ![spacing](edit-animation-clip/menu-spacing.png) in the toolbar. + +## Removing keyframes + +1. Select the keyframe to be deleted (multiple selections are allowed) and press Delete (Windows) or Cmd + Backspace (macOS). + +2. Select the keyframe you want to delete (you can select more than one), then right click and select **Remove Keyframe** in the popup menu. + +3. Drag the time control line to the position of the keyframe to be removed or **double-click** the keyframe, and then click ![remove keyframe](edit-animation-clip/remove-key-btn.png) button to the right of the corresponding animation property. + +![remove keyframe](edit-animation-clip/remove-keyframes.gif) + +## Moving keyframes + +Dragging a keyframe while it is selected will move it to the desired position. + +- When dragging a single keyframe, a small white box will appear below the keyframe, showing the number of frames and the distance moved during the move. +- When dragging multiple keyframes, the box will show the number of frames in the animation timeline for the first and last keyframes during the move, on the left and right side respectively. + +![move keyframe](edit-animation-clip/move-keyframes.gif) + +## Scaling keyframes + +When multiple keyframes are selected, the first and last keyframes will show two control levers, drag any of the levers to move them to scale the selected keyframes as a whole. + +![scale keyframe](edit-animation-clip/scale-keyframes.gif) + +## Spacing keyframes + +With multiple keyframes selected, click the ![spacing](edit-animation-clip/menu_spacing_btn.png) button in the upper menu bar, the selected keyframes will be arranged in order with the first keyframe as the base and the value set in the ![spacing](edit-animation-clip/menu-spacing.png) input box as the number of spacing frames. + +## Copying/pasting keyframes + +Copy and paste keyframe data, support cross-node and cross-clip usage. The following two usage methods are included: + +- After selecting a keyframe (multiple selection is possible), use the shortcut keys Ctrl + C and Ctrl + V to copy and paste. Note that the shortcut paste will start at the position of the current time control line. +- After selecting a keyframe (multiple selections are allowed), right-click on (any) keyframe and select **Copy Keyframe** in the popup menu, then right-click on the target animation property track and select **Paste Keyframe** to paste the keyframe on the current animation property track. + +The above two pasting methods differ when copying and pasting data from multiple property tracks, the difference mainly lies in: + +- When using shortcut keys to paste keyframes, the keyframes will be pasted on the animation property track where they were copied, in the order of the copied keyframes. + + ![copy keyFrames1](edit-animation-clip/copy-keyframes2.gif) + +- When right-clicking on the target property track and select Paste, only the copied data will be pasted on the target property track. + + ![copy keyFrames1](edit-animation-clip/copy-keyframes1.gif) + +Component property tracks (e.g.: `x`, `y`, `z` properties under the animation property track `position`) as separate property tracks will also follow this rule. If right-clicking on the target component track after copying the keyframe data on the main track (e.g.: animation property track `position`) to paste it, only the target component track will be pasted with the keyframe data. Be sure to copy the data according to the actual position you want to paste, to avoid unintended results. diff --git a/versions/4.0/en/animation/animation-state.md b/versions/4.0/en/animation/animation-state.md new file mode 100644 index 0000000000..e7f2a2704d --- /dev/null +++ b/versions/4.0/en/animation/animation-state.md @@ -0,0 +1,95 @@ +# Animation State + +Animation clips only describe the animation data of a certain type of object, such as a character running, walking, jumping, etc., but are not bound to the specific object to be animated. Animation states are Animation Clips that are used to control the animation on a certain object, similar to a player. In addition to providing simple control functions that are also available for Animation components, they also provide more animation information and an animation control interface that allows control of animation playback such as speed control, setting loop mode, etc. An Animation Clip can be used by multiple animation states at the same time. + +Animation states are managed by [Class `AnimationState`](%__APIDOC__%/en/class/AnimationState). + +## Setting the playback speed + +First get the animation state by [getState()](%__APIDOC__%/en/class/Animation?id=getState): + +```ts +// Get the Animation component +const animationComponent = node.getComponent(Animation); + +// Get the animation clips on the Animation component +const [ idleClip, runClip ] = animationComponent.clips; + +// Get the animation state of 'idleClip' +const idleState = animationComponent.getState(idleClip.name); +``` + +Then set the speed of the animation to play at: + +```ts +// Play the 'idleClip' animation at two times the speed +animationComponent.getState('idle').speed = 2.0; // the larger the speed value the faster it is, the smaller the value the slower it is +``` + +The animation state also provides `play()`, `pause()`, `resume()`, `stop()` methods for playback control, please refer to the **play state** section below for more details. + +## Play Time + +The animation state records the **cumulative play time** of the animation. Initially, the accumulated play time is 0. As the animation plays naturally, the time will be accumulated. For example, when the animation loops, the cumulative play time will be **animation period * 2** just after the second loop. + +The playback position of the animation at any given moment is called **progress time**, so the progress time is always in the range `[0, animation period]`. + +- The **cumulative play time** is obtained from the [time](%__APIDOC__%/en/class/AnimationState?id=time) field of `AnimationState`, and can be set explicitly. +- The **progress time** is obtained from the [current](%__APIDOC__%/en/class/AnimationState?id=current) field of `AnimationState`, and is **read-only**. + +The wrap mode and repeat count of the animation determines the progress time of the animation when it reaches a certain time, whether the **cumulative play time** is increased by time or changed by direct setting, the **progress time** will be changed accordingly. + +## Wrap Mode and Repeat Count + +Animation can be played to the end and stop, or it can be looped all the time, or it can be played to the end and then looped from the end to the beginning, these are collectively called wrap modes and are represented by the enumeration [`AnimationClip.WrapMode`](%__APIDOC__%/en/class/AnimationState?id=current), the following are included: + +| Wrap Mode | Description | +| :--- | :--- | +| `AnimationClip.WrapMode.Normal` | Play from the beginning to the end and then stop. | +| `AnimationClip.WrapMode.Loop` | Play from the beginning to the end continuously. | +| `AnimationClip.WrapMode.PingPong` | Play from the beginning to the end, then reverse from the end to the beginning, and so on. |PingPong + +In addition, there are **reverse** looping modes for each of the looping modes in the table above: + +| Wrap Mode | Description | +| :--- | :--- | +| `AnimationClip.WrapMode.Reverse` | Play from the end to the beginning and then stop. | +| `AnimationClip.WrapMode.LoopReverse` | Play from the end to the beginning continuously. | +| `AnimationClip.WrapMode.PingPongReverse` | Play from the end to the beginning, then reverse from the beginning to the end, and so on. | + +The initial wrap mode of the animation state will be read from the animation clip. When you need to change the wrap mode of the animation state, simply set the `wrapMode` field of the animation state. + +> **Note**: setting the wrap mode will reset the **cumulative play time** of the animation state. + +Except `AnimationClip.WrapMode.Normal` and its counterpart `AnimationClip.WrapMode.Reverse` (which can be interpreted as a single loop), all other wrap modes perform an infinite loop. The infinite loop needs to be used in conjunction with `repeatCount` of `AnimationState` to achieve the effect, and the number of loops can be set and retrieved via the `repeatCount` field. + +When the animation wrap mode is: +- Single loop mode: `repeatCount` will be set to **1**. +- Infinite loop mode: `repeatCount` will be set to `Number.Infinity`, i.e. infinite loop. + +> **Note**: setting the repeat count should be done after setting the wrap mode, because resetting the wrap mode will reset the repeat count. + +## Playback Control + +The animation state provides the following methods to control the play, pause, resume and stop of the animation: + +| Method | Description | +| :--- | :--- | +| `play()` | Reset the playback time to 0 and start the animation. | +| `pause()` | Pause the animation. | +| `resume()` | Resume the animation from the current time. | +| `stop()` | Stop the animation. | + +The playing status of the animation can be queried by the following fields: + +| Field (read-only) | Description | +| :--- | :--- | +| `isPlaying` | If or not the animation is in playing state. | +| `isPaused` | If or not the animation is paused. | +| `isMotionless` | If or not the animation is paused or has been stopped. | + +The relationship between the playback control and the playback state is shown in the following figure: + +![Playback control](./animation-state/playback-control.svg) + +The animation state allows getting information about all the animations in order to use this information to determine what needs to be done. For more interfaces, please refer to [Class `AnimationState`](%__APIDOC__%/en/class/AnimationState). diff --git a/versions/4.0/en/animation/animation-state/playback-control.dot b/versions/4.0/en/animation/animation-state/playback-control.dot new file mode 100644 index 0000000000..267f5b44e3 --- /dev/null +++ b/versions/4.0/en/animation/animation-state/playback-control.dot @@ -0,0 +1,22 @@ +strict digraph { + graph [fontname = "Courier New"]; + node [fontname = "Courier New"]; + edge [fontname = "Courier New"]; + + entry [labe="",shape="diamond"] + { + rank=same; + playing [label="isPlaying"] + playingPaused [label="isPlaying &&\nisPaused"] + } + stopped [label=""] + + entry -> stopped + + stopped -> playing [label="play()"] + playing -> playingPaused [label="play()/resume()"] + playingPaused -> playing [label="pause()"] + + playing -> stopped [label="stop()"] + playingPaused -> stopped [label="stop()"] +} \ No newline at end of file diff --git a/versions/4.0/en/animation/animation-state/playback-control.svg b/versions/4.0/en/animation/animation-state/playback-control.svg new file mode 100644 index 0000000000..5b7cbee67e --- /dev/null +++ b/versions/4.0/en/animation/animation-state/playback-control.svg @@ -0,0 +1,78 @@ + + + + + + + + + +entry + +entry + + + +stopped + +<stopped> + + + +entry->stopped + + + + + +playing + +isPlaying + + + +playingPaused + +isPlaying && +isPaused + + + +playing->playingPaused + + +play()/resume() + + + +playing->stopped + + +stop() + + + +playingPaused->playing + + +pause() + + + +playingPaused->stopped + + +stop() + + + +stopped->playing + + +play() + + + diff --git a/versions/4.0/en/animation/animation.md b/versions/4.0/en/animation/animation.md new file mode 100644 index 0000000000..4b7d2906c8 --- /dev/null +++ b/versions/4.0/en/animation/animation.md @@ -0,0 +1,32 @@ +# Animation Panel + +Creator supports creating, editing and previewing animation clips directly in the **Animation** panel. In addition to animating the basic properties of nodes, it also supports animating the properties of materials and some components, and it can enrich animation clips by calling the event functions of the [animation event](animation-event.md). + +To edit an **Animation Clip** using the **Animation** panel, add an **Animation Component** to a node and attach the clip to the Animation Component. For details, please refer to the [Creating Animation Components and Animation Clips](animation-create.md) documentation. + +![animation play](animation/animation-play.gif) + +## Animation editing mode + +The animation data in an Animation Clip can only be edited in **Animation Editing Mode**. In Animation Editing Mode, nodes cannot be added/deleted/renamed and property changes that are not recorded within animation keyframes will also be restored after exiting animation editing mode. + +When in the Animation Editing Mode, nodes that are not involved in animation data editing are grayed out in the **Hierarchy** panel. When a node is selected in the **Animation** panel, the **Hierarchy** panel will highlight the corresponding node and vice versa. + +![select node](./animation/select_node.gif) + +**Opening the edit mode includes the following two ways**: + +- Select a node with an Animation component in the **Hierarchy** panel that contains more than one clip file, then click the **Enter Animation Edit Mode** button in **Animation** panel. +- Shortcut Ctrl/Cmd + E. + +**Saving the edited animation data includes the following three ways**. + +Click the ![save](./animation/save.png) (Save) button in the top right corner of the **Animation** panel. +- Click the **Save** button in the top left corner of the **Scene** panel. +- Shortcut Ctrl/Cmd + S. + +**Exit edit mode includes the following three ways**: + +- Click the ![exit](./animation/exit.png) (Exit) button in the top right corner of the **Animation** panel. +- Click the **Close** button in the top left corner of the **Scene** panel. +- Shortcut Ctrl/Cmd + E. diff --git a/versions/4.0/en/animation/animation/animation-play.gif b/versions/4.0/en/animation/animation/animation-play.gif new file mode 100644 index 0000000000..a0ae8b7ae1 Binary files /dev/null and b/versions/4.0/en/animation/animation/animation-play.gif differ diff --git a/versions/4.0/en/animation/animation/batched-skinning-model-component.png b/versions/4.0/en/animation/animation/batched-skinning-model-component.png new file mode 100644 index 0000000000..0e908185f1 Binary files /dev/null and b/versions/4.0/en/animation/animation/batched-skinning-model-component.png differ diff --git a/versions/4.0/en/animation/animation/exit.png b/versions/4.0/en/animation/animation/exit.png new file mode 100644 index 0000000000..3b0d07833c Binary files /dev/null and b/versions/4.0/en/animation/animation/exit.png differ diff --git a/versions/4.0/en/animation/animation/keyframe.png b/versions/4.0/en/animation/animation/keyframe.png new file mode 100644 index 0000000000..392398ed28 Binary files /dev/null and b/versions/4.0/en/animation/animation/keyframe.png differ diff --git a/versions/4.0/en/animation/animation/save.png b/versions/4.0/en/animation/animation/save.png new file mode 100644 index 0000000000..8324f2a20a Binary files /dev/null and b/versions/4.0/en/animation/animation/save.png differ diff --git a/versions/4.0/en/animation/animation/select_node.gif b/versions/4.0/en/animation/animation/select_node.gif new file mode 100644 index 0000000000..0ea70b2d35 Binary files /dev/null and b/versions/4.0/en/animation/animation/select_node.gif differ diff --git a/versions/4.0/en/animation/animation/sockets-attach0.png b/versions/4.0/en/animation/animation/sockets-attach0.png new file mode 100644 index 0000000000..4850285819 Binary files /dev/null and b/versions/4.0/en/animation/animation/sockets-attach0.png differ diff --git a/versions/4.0/en/animation/animation/sockets-attach1.gif b/versions/4.0/en/animation/animation/sockets-attach1.gif new file mode 100644 index 0000000000..58e6468c53 Binary files /dev/null and b/versions/4.0/en/animation/animation/sockets-attach1.gif differ diff --git a/versions/4.0/en/animation/curve-editor.md b/versions/4.0/en/animation/curve-editor.md new file mode 100644 index 0000000000..a5ab433163 --- /dev/null +++ b/versions/4.0/en/animation/curve-editor.md @@ -0,0 +1,124 @@ +# Curve Editor + +The Curve Editor is mainly used to edit the curve trajectory that changes between keyframes. The keyframe curves of animations and particles in the editor are edited using the Curve Editor. For the key point on each curve, **the horizontal coordinate is the keyframe time/frame rate, and the vertical coordinate is the value of the current curve property at the corresponding time**. The Curve Editor supports editing multiple curves at the same time, but only single curve editing is currently available in the particle usage scene. + +## Scaling and moving the curve display area + +- **Scroll the mouse wheel** directly within the curve display area **to zoom in and out of the horizontal and vertical timeline at the same time**. +- Hold down the right mouse button and drag to pan the current display area. + +In addition, shortcut keys can be used to control the pan and zoom of the horizontal and vertical timeline individually. + +- Hold down Shift and roll the mouse wheel to **pan** the curve view area to the left or right; +- Hold Ctrl/Cmd + Shift and roll the mouse wheel to **zoom** the curve view area to the left or right; +- Hold down Alt/Option and roll the mouse wheel to **pan** the curve view area up or down; +- Hold down Ctrl+Alt / Cmd+Option and roll the mouse wheel to **zoom** the curve view area up or down. + +## Editing Curve Trajectory + +### Adding keyframes + +1. When there is only one curve, right-clicking on the curve or any blanket, and choose **Create key frame** on the popup menu to create a new key frame. + + Take the particle system as an example: + + ![add-keyframe-to-curve.png](animation-curve/add-keyframe-to-curve.png) + +2. For animation editor, key frames could be added by clicking the following button: + + ![add-keyframe.png](animation-curve/add-keyframe.png) + +### Selecting keyframes + +A keyframe can be selected by clicking on it directly, or by framing it, and the selected keyframe will be shown in yellow. The selected keyframe will be shown in yellow. When the keyframe is selected individually, the value of the current keyframe will be shown on the left and right axes. + +![select-key](animation-curve/select-key.gif) + +### Moving keyframes + +Drag the keyframe directly after it is selected. + +![move-keys](animation-curve/move-keys.gif) + +### Scaling keyframes + +After selecting multiple keyframes, drag the border of the selected area to scale the keyframes as a whole. + +![scale-keys](animation-curve/scale-keys.gif) + +### Spacing keyframes + +After framing multiple keyframes, click on the selected area and select **Spacing selected keyframes** in the pop-up menu. + +### Removing keyframes + +After selecting a keyframe, press Delete (Windows) or Cmd + Backspace (macOS) or right-click the selected area and select `Delete` in the pop-up menu. + +### Copying and pasting keyframes + +- Copying keyframe +After selecting a keyframe, right-click on the selected area and select **Copy**, or press Ctrl/Cmd + C. + +- Pasting keyframe +In any blank space, right-click on the selected area and select **Paste**, or just use the shortcut Ctrl/Cmd + V. + +### Editing keyframe data + +In the Curve Editor, the keyframe values are already displayed visually, dragging the keyframes up, down, left, and right is actually modifying the underlying keyframe data. In some cases where controlling the keyframe value precisely is necessary, right-click on the keyframe and select **Edit Keyframe Data** in the pop-up menu. + +![edit-key](animation-curve/edit-key.png) + +### Interpolation Mode + +Right-click on the keyframe and select the corresponding value for `Interpolation Mode` in the pop-up menu. The linear interpolation mode will affect the way the value changes between the current keyframe and the next frame, and different option values will have different display effects. See below for more details on the options. + +![interopMode](./animation-curve/interopMode.png) + +![interopMode Key](./animation-curve/interopMode-key.gif) + +The interpolation mode includes the following option values: + +- **Linear**: linear mode, linearly varying the value between the current frame and the next keyframe, with the tangent direction pointing to the next keyframe. + + ![linear](animation-curve/linear.png) + +- **Constant**: constant mode, where changes between the current keyframe and the next keyframe keep the current keyframe value unchanged. + + ![constant](animation-curve/constant.png) + +- **Cubic**: free curve mode, where the value between the current frame and the next keyframe changes in a curve. Only in the current mode the left and right tangent levers appear when the keyframe is selected. + + ![cubic](animation-curve/cubic.png) + +### Editing keyframe tangents + +A keyframe has two tangents on the left and right, the length and direction of the tangents will control the shape of the curve between keyframes. In `Cubic` interpolation mode, just drag the tangent control point to edit it directly. In different tangent weight modes, the tangent lever can be operated in different ranges, please refer to the next paragraph. + +#### Tangent Weight Mode + +Right-click on the keyframe to see the modification options menu for the interpolation mode, and click on Modify. The tangent weight mode value will only work if the `Interpolation Mode` is `Cubic`. + +The tangent option range is actually two main types: + +- **No-Weighting Mode**: the variation between keyframes uses the Hermite algorithm, where the direction of the curve depends on the direction of the left and right tangents of the keyframes. In this case, the left and right tangent levers of the keyframe can only change the tangent direction. The tangent control points at this point are only drawn to facilitate the direction change, so the tangent lever length does not change when scaling the curve view area at this point. + +- **Weighting Mode**: the change between keyframes uses the Bessel algorithm, and the curve orientation is also influenced by the left and right tangent control points of the keyframes. The left tangent control point can be moved at will to adjust the control bar length, the left tangent control point is the real coordinate point at this time, and the control bar will be scaled simultaneously when scaling the curve view area. + + ![interopMode Key](./animation-curve/tangentWeightMode-key.gif) + +The keyframe has left and right tangents, and the tangent weight mode has the following option values: + +![tangentWeightMode](./animation-curve/tangentWeightMode.png) + +- `None`: the left and right tangents are in no-weighting mode. +- `Left`: left tangent is in weighting mode, right tangent is in no-weighting mode. +- `Right`: right tangent is in in weighting mode, left tangent is in no-weighting mode. +- `Both`: both left and right tangents are in weighting mode. + +#### Broken + +By default, the left and right tangent slopes of the keyframe are kept the same. Adjusting the tangent lever on either side will follow the adjustment on the other side, which makes it easier for us to adjust the transition more naturally. In some cases, the curve does not always transition smoothly. At this time, right-click the keyframe and click the Broken option in the right-click menu to disconnect the key left and right joysticks. After disconnecting, the keyframes can be adjusted separately using the left and right joysticks. + +### Moving curves up and down + +The whole curve can be dragged up and down directly after double tapping the curve. diff --git a/versions/4.0/en/animation/curve-example-test.html b/versions/4.0/en/animation/curve-example-test.html new file mode 100644 index 0000000000..2105cefba7 --- /dev/null +++ b/versions/4.0/en/animation/curve-example-test.html @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/versions/4.0/en/animation/curve-example.js b/versions/4.0/en/animation/curve-example.js new file mode 100644 index 0000000000..8e6a2c5363 --- /dev/null +++ b/versions/4.0/en/animation/curve-example.js @@ -0,0 +1,98 @@ + +(() => { + /** + * @param {HTMLCanvasElement} canvas + * @param {{xAxisText?: string; yAxisText?: string;}} config + */ + window.drawCurve = (canvas, countGenPoint, config) => { + config = config || {}; + + const xrange = { min: 0.1, max: 0.9 }; + const yrange = { min: 0.1, max: 0.8 }; + const xAxisLine = { start: 0.0 * canvas.width, end: 0.7 * canvas.width }; // In pixel. + const yAxisLine = { start: 0.7 * canvas.height, end: 0.1 * canvas.height }; // In pixel. + const xAxisLength = xAxisLine.end - xAxisLine.start; + const yAxisLength = yAxisLine.start - yAxisLine.end; + + const generateIncresingNumbers = (count, min, max) => { + const intervals = new Array(count); + let sum = 0; + for (let i = 0; i < count; ++i) { + const interval = Math.random(); + intervals[i] = interval; + sum += interval; + } + const extent = max - min; + let sumNi = 0; + return intervals.map((interval) => { + const ni = interval / sum; + const result = min + sumNi * extent; + sumNi += ni; + return result; + }); + }; + + const context = canvas.getContext('2d'); + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + context.fill(); + + context.strokeStyle = 'black'; + + const xs = generateIncresingNumbers(countGenPoint, xrange.min, xrange.max); + const points = xs.map((x) => { + return { + x, + y: yrange.min + (yrange.max - yrange.min) * Math.random(), + }; + }); + const pointPixels = points.map((point) => { + return { + x: xAxisLine.start + point.x * xAxisLength, + y: yAxisLine.start - point.y * yAxisLength, + }; + }); + + // Draw axises. + context.moveTo(xAxisLine.start, yAxisLine.start); + context.lineTo(xAxisLine.end, yAxisLine.start); + context.moveTo(xAxisLine.start, yAxisLine.start); + context.lineTo(xAxisLine.start, yAxisLine.end); + + // Draw curve. + if (pointPixels.length > 1) { + context.moveTo(pointPixels[0].x, pointPixels[0].y); + for (let iPoint = 1; iPoint < points.length; ++iPoint) { + context.lineTo(pointPixels[iPoint].x, pointPixels[iPoint].y); + } + } + + context.stroke(); + + // Draw frames. + context.setLineDash([1, 3]); + pointPixels.forEach((pointPixel) => { + context.moveTo(pointPixel.x, pointPixel.y); + context.lineTo(pointPixel.x, yAxisLine.start); + }); + context.stroke(); + + // Draw points. + context.fillStyle = 'red'; + pointPixels.forEach((pointPixel) => { + const rectSize = 4; + context.fillRect(pointPixel.x - rectSize / 2, pointPixel.y - rectSize / 2, rectSize, rectSize); + }); + + if (config.xAxisText) { + context.fillText(config.xAxisText, xAxisLine.end + 1, yAxisLine.start); + } + + if (config.yAxisText) { + context.fillText(config.yAxisText, xAxisLine.start, yAxisLine.end - 1); + } + }; +})(); + +var myFoo = ()=>{} +export default myFoo; \ No newline at end of file diff --git a/versions/4.0/en/animation/easing-method-example.js b/versions/4.0/en/animation/easing-method-example.js new file mode 100644 index 0000000000..872f35d462 --- /dev/null +++ b/versions/4.0/en/animation/easing-method-example.js @@ -0,0 +1,83 @@ + +(() => { + const makeOutIn = (infx, outfx) => { + return (k) => { + if (k < 0.5) { + return outfx(k * 2) / 2; + } + return infx(2 * k - 1) / 2 + 0.5; + }; + }; + + const quadInFx = (k) => k * k; + + const quadOutFx = (k) => k * (2 - k); + + const methods = [ + {name: 'linear', fx: ((k) => k),}, + {name: 'constant', fx: ((k) => 0)}, + {name: 'quadIn', fx: quadInFx}, + {name: 'quadOut', fx: quadOutFx}, + {name: 'quadInOut', fx: makeOutIn(quadInFx, quadOutFx)}, + {name: 'quadOutIn', fx: ((k) => k)}, + ]; + + window.initializeEasingMethodsExample = (parent) => { + const table = parent.appendChild(document.createElement('table')); + const showcases = []; + for (const method of methods) { + const { name, fx } = method; + const tr = table.appendChild( + document.createElement('tr')); + + const label = tr.appendChild(document.createElement('td')). + appendChild(document.createElement('label')); + label.textContent = name; + + const image = tr.appendChild(document.createElement('td')). + appendChild(document.createElement('img')); + image.src = "https://forum.cocos.org/images/logo.png"; + + const slider = tr.appendChild(document.createElement('td')). + appendChild(document.createElement('input')); + slider.type = 'range'; + slider.min = 0; + slider.max = 100; + slider.value = 0; + + showcases.push({ + label, + image, + fx, + slider, + }); + } + + const firstTime = Date.now(); + let duration = 3000; + setInterval(() => { + const timePast = Date.now() - firstTime; + const ratio = (timePast % duration) / duration; + for (const showcase of showcases) { + const mappedRatio = showcase.fx(ratio); + showcase.image.style.opacity = (1.0- mappedRatio); + showcase.slider.value = 100 * mappedRatio; + } + }, 16); + }; +})(); + +(() => { + let easingMethodsExampleInitialized = false; + window.onEasingMethodExampleButtonClicked = () => { + const panel = document.getElementById('easing-method-example-panel'); + if (!easingMethodsExampleInitialized) { + easingMethodsExampleInitialized = true; + initializeEasingMethodsExample(panel); + } else { + panel.hidden = !panel.hidden; + } + }; +})(); + +export default ()=>{}; diff --git a/versions/4.0/en/animation/edit-animation-clip.md b/versions/4.0/en/animation/edit-animation-clip.md new file mode 100644 index 0000000000..b639a2028e --- /dev/null +++ b/versions/4.0/en/animation/edit-animation-clip.md @@ -0,0 +1,138 @@ +# Editing Animation Clips + +Once an Animation Clip is attached on to an Animation Component of a node, click **Enter animation editing mode** or use the shortcut Ctrl/Cmd + E to enter Animation Editing Mode and add keyframe data to the Animation Clip to animate the node. Please refer to the [Animation panel](animation-editor.md) documentation before editing animation clips. + +An Animation Clip may contain multiple nodes (nodes and their children), and multiple animation properties can be attached to each node. By moving, rotating, or scaling the nodes, keyframes are added to the animation property corresponding to the currently selected node. All the keyframes added to the animation property are displayed in the corresponding animation property as a list pattern of linear trajectories, which are called animation curves. + +## Creating an Animation Curve + +Before adding keyframes, it is necessary to know about animation properties. The animation properties include the node's own `position`, `rotation`, `scale`, and other properties, and also include the custom properties in the component `Component`. The properties included in the component are preceded by the name of the component, e.g.: `cc.Sprite.spriteFrame`. + +Click the **+** button in the upper-right corner of the **Property List** area to add animation properties as needed, depending on the node type. Added animation properties are grayed out and cannot be added repeatedly. + +![add property](edit-animation-clip/add-property.png) + +Once the animation properties are added, keyframes can be added to the properties track on the right. When the node and its animation properties are shown in blue in the list, it is determined that the node's properties are the target object for the current keyframe creation. Then, when the corresponding property is modified in the **Animation** / **Inspector** / **Scene** panel, a solid blue diamond will be created on the right side of the animation property where the time control line is located, which is the keyframe (The selected keyframe data can also be modified by this method). + +![add keyframe](edit-animation-clip/add-keyframe.gif) + +For more information on the design of Animation Curves and how to control them via scripts, please refer to the [Using Animation Curve](use-animation-curve.md) documentation. + +### Editing Sprite animations + +Next, let's take a look at the process of creating a Sprite animation as an example. + +1. Create a Sprite node. + + Create a Sprite node in the **Hierarchy** panel. Or add a Sprite component to the node by selecting the node and clicking the **Add Component** button in the **Inspector** panel and selecting **2D -> Sprite**. + +2. Add the Animation Component to the node, attach the Clip file, and enter the Animation Editing Mode. For details, please refer to the [Creating Animation Component and Animation Clip](animation-create.md) documentation. + +3. Add the animation property `cc.Sprite.spriteFrame` to the property list. + + Click the **+** button in the upper right corner of the property list, then select **cc.Sprite -> spriteFrame**. + + ![add SpriteFrame](edit-animation-clip/add-spriteframe.gif) + +4. Add a keyframe. + + Drag the spriteFrame asset from **Assets** panel to the property track on the right side of the `cc.Sprite.spriteFrame` animation property, and then drag the spriteFrame to be displayed in the next keyframe to the specified position, or select the needed spriteFrame in the property box above the property track. Play to preview the animation that is just created. + + ![add SpriteFrame](edit-animation-clip/animation-sprite.gif) + +## Keyframe data editing + +### Common operations with node data + +The animation clip defines the position of the animation data by the name of the node, itself ignoring the root node, and the rest of the child nodes find the corresponding data by the relative path index to the root node. + +Right-clicking on a node in the **Node List** area of the **Animation** panel is currently supported to choose between clearing node data, migrating node data, and copying and pasting node data. + +![node operation](edit-animation-clip/node-operation.png) + +#### Clearing node data + +Right-click the node that needs to clear all animation data (keyframes), then select **Clear data**, and click **Clear** in the popup window. + +#### Migrating node data + +Right-click the node that needs to migrate all animation data, then select **Move Data**, the node will show a dynamic dashed box, when the mouse moves to other nodes, it will show the prompt of "Move data to this node", click it and select **Move** in the pop-up window. If there is no need to migrate, just select **Cancel**. + +![move data](edit-animation-clip/move-data.png) + +> **Note**: node data migration will overwrite the data on the original node by default. + +Because the animation clip will record all the node path information that participated in the animation data editing, so when the node information changes (e.g.: node renaming/deletion/move position), which is not consistent with the original saved in the animation clip, the **Animation** panel will show it in the node list as a yellow uneditable lost state, and the keyframes are also grayed out as uneditable. In this case, the **Migrate Data** function of the node can be used to migrate the animation data of the missing node to another node. + +![change node](edit-animation-clip/change-node.png) + +#### Copy and paste node data + +Copy and paste of node animation data, supports cross-editor (v3.x) usage. + +- **Copy**: Select the node you want to copy data from in the node list, right click and select **Copy Data**, or use the shortcut Ctrl/Cmd + C to do so. +- **Paste**: Select the target node in the node list, right click and select **Paste Data**, or use the shortcut Ctrl/Cmd + V to paste the animation data. + +> **Note**: the paste function does not support automatic creation of animation properties, so the copied and pasted nodes must have at least one of the same animation property, if not, please create it in advance. When pasting animation data of multiple nodes on the same node, the animation data of the overlapping part will be overwritten by the latter, and the different parts will be fused with each other. + +#### Edit child node data with the same name + +When editing animation data in the animation clip of the parent node, it is possible to also edit the animation data of the child nodes. The animation data will all be saved in the animation clip of the parent node, which is used to achieve effects like moving the hands of the character model with different speeds along with the body, etc. However, if the parent node contains more than one child node with the same name at the same time, only the first child node with the same name will be edited, and the other nodes with the same name will be grayed out to uneditable state. + +![same-name node](edit-animation-clip/same-name-node.png) + +However, it is supported to add Animation components and animation clips for the same name child node separately and edit them separately. + +### Common operations for animation property data + +Right-click on the animation property or click ![property](edit-animation-clip/set-pro.png) button to the right of the track to select **Remove property track**, **Clear keyframe data**, or **Copy property track**. + +![property operation](edit-animation-clip/pro-operation.png) + +- Remove property track: remove the currently selected animation property, including all keyframes on that property track. Or use the shortcut Delete key (Windows) or Cmd + Backspace (macOS). + +- Clear keyframe data: delete all keyframes on the currently selected animation property track. + +- Copy animation property track: copy all animation data on the currently selected animation property track and paste it to the target animation property track, supporting the shortcut keys Ctrl/Cmd + C and Ctrl/Cmd + V. When pasting, the overlapping keyframes are overwritten by the latter, and the different parts are blended with each other. + + Copy/paste of animation property data between different editors (v3.x) is currently supported. Copy/paste of animation data is only supported for `ccType` data of the same type. + +As with nodes, there is a possibility of missing animation properties. For example, if an property corresponding to an animation property is removed from the **Inspector** panel, it will be displayed in the animation property list as a yellow uneditable missing state, and the keyframe will be grayed out as uneditable. In this case, either exit edit mode and add the relevant property to the corresponding node or simply remove the missing property track. + + ![change property](edit-animation-clip/change-pro.png) + +### Node / Property track keyframe data editing + +The **Animation** panel has two types of view for keyframes: **Keyframe Editing View** and **Curve Editing View**. When you enter the **Animation** panel, the default is keyframe mode, by clicking the switch view button on the side of the property list ![curve btn](animation-curve/curve-btn.png) to switch the keyframes in the property track area to the curve editing view, and click it again to return to the keyframe editing view. + +#### Keyframe Editing Mode + +For some common keyframe operations in keyframe editing mode, see [keyframe editing mode](./animation-keyFrames.md) for a description of the documentation. + +#### Curve Editing Mode + +For some details of the keyframe operations in curve editing mode, see the [curve editing mode](./animation-curve.md) documentation. + +## Animation clip property settings + +The properties of the current animation clip that can be set directly on the **Animation** panel include: **WrapMode**, **Sample**, **Speed** and **Duration**. + +![set AnimationClip](edit-animation-clip/set-animation-clip.png) + +- **WrapMode**: use to set the loop mode of the current animation clip, currently include + - Default: the effect is the same as Normal + - Normal: forward single play + - Loop: loop forward + - PingPong: loop in forward and then reverse mode + - Reverse: reverse single play + - LoopReverse: loop reverse + + For more information about how to set the loop mode by script code, please refer to [Loop Mode](use-animation-curve.md#wrap-mode). + +- **Sample**: define the frame rate per second of the current animation data, i.e. how many frames a second to divide into, default is 60. please refer to the **Timeline's scale unit display** section above for more details. + +- **Speed**: set the speed of current animation, default is 1, the smaller the value the slower the speed. + +- **Duration**: the number in front indicates the duration of the animation when the speed is 1, it is determined by the position of the last keyframe. The number in brackets after it indicates the actual running duration, which will change when Speed is adjusted. + +Animation clips are reusable and their states are stored in objects called animation states. Through the animation state interface the animation can be controlled by playing, pausing, stopping, shifting, etc. For details, please refer to [Animation State](animation-state.md). diff --git a/versions/4.0/en/animation/edit-animation-clip/add-keyframe-button.png b/versions/4.0/en/animation/edit-animation-clip/add-keyframe-button.png new file mode 100644 index 0000000000..2d2fba2a79 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/add-keyframe-button.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/add-keyframe.gif b/versions/4.0/en/animation/edit-animation-clip/add-keyframe.gif new file mode 100644 index 0000000000..ed98e7a201 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/add-keyframe.gif differ diff --git a/versions/4.0/en/animation/edit-animation-clip/add-keyframe.png b/versions/4.0/en/animation/edit-animation-clip/add-keyframe.png new file mode 100644 index 0000000000..a385de7a60 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/add-keyframe.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/add-property.png b/versions/4.0/en/animation/edit-animation-clip/add-property.png new file mode 100644 index 0000000000..2d0dae9cb8 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/add-property.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/add-spriteframe.gif b/versions/4.0/en/animation/edit-animation-clip/add-spriteframe.gif new file mode 100644 index 0000000000..bb3d5e53ec Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/add-spriteframe.gif differ diff --git a/versions/4.0/en/animation/edit-animation-clip/animation-sprite.gif b/versions/4.0/en/animation/edit-animation-clip/animation-sprite.gif new file mode 100644 index 0000000000..efb2e33175 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/animation-sprite.gif differ diff --git a/versions/4.0/en/animation/edit-animation-clip/change-node.png b/versions/4.0/en/animation/edit-animation-clip/change-node.png new file mode 100644 index 0000000000..444cf0e582 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/change-node.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/change-pro.png b/versions/4.0/en/animation/edit-animation-clip/change-pro.png new file mode 100644 index 0000000000..f8a5d1b1eb Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/change-pro.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/choose-by-box.png b/versions/4.0/en/animation/edit-animation-clip/choose-by-box.png new file mode 100644 index 0000000000..9469d70017 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/choose-by-box.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/choose-keyframe.png b/versions/4.0/en/animation/edit-animation-clip/choose-keyframe.png new file mode 100644 index 0000000000..158648516a Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/choose-keyframe.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/copy-keyframes1.gif b/versions/4.0/en/animation/edit-animation-clip/copy-keyframes1.gif new file mode 100644 index 0000000000..4b1462fad9 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/copy-keyframes1.gif differ diff --git a/versions/4.0/en/animation/edit-animation-clip/copy-keyframes2.gif b/versions/4.0/en/animation/edit-animation-clip/copy-keyframes2.gif new file mode 100644 index 0000000000..f875d59cdb Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/copy-keyframes2.gif differ diff --git a/versions/4.0/en/animation/edit-animation-clip/menu-spacing.png b/versions/4.0/en/animation/edit-animation-clip/menu-spacing.png new file mode 100644 index 0000000000..e20bce5e81 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/menu-spacing.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/menu_spacing_btn.png b/versions/4.0/en/animation/edit-animation-clip/menu_spacing_btn.png new file mode 100644 index 0000000000..9a732235a7 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/menu_spacing_btn.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/move-data.png b/versions/4.0/en/animation/edit-animation-clip/move-data.png new file mode 100644 index 0000000000..bb636f90a5 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/move-data.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/move-keyframes.gif b/versions/4.0/en/animation/edit-animation-clip/move-keyframes.gif new file mode 100644 index 0000000000..866ed1996e Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/move-keyframes.gif differ diff --git a/versions/4.0/en/animation/edit-animation-clip/node-operation.png b/versions/4.0/en/animation/edit-animation-clip/node-operation.png new file mode 100644 index 0000000000..13c7917ebe Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/node-operation.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/pro-operation.png b/versions/4.0/en/animation/edit-animation-clip/pro-operation.png new file mode 100644 index 0000000000..1d0d00172e Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/pro-operation.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/remove-key-btn.png b/versions/4.0/en/animation/edit-animation-clip/remove-key-btn.png new file mode 100644 index 0000000000..3c5b52cf70 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/remove-key-btn.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/remove-keyframes.gif b/versions/4.0/en/animation/edit-animation-clip/remove-keyframes.gif new file mode 100644 index 0000000000..8baa243885 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/remove-keyframes.gif differ diff --git a/versions/4.0/en/animation/edit-animation-clip/same-name-node.png b/versions/4.0/en/animation/edit-animation-clip/same-name-node.png new file mode 100644 index 0000000000..064d6d8d26 Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/same-name-node.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/scale-keyframes.gif b/versions/4.0/en/animation/edit-animation-clip/scale-keyframes.gif new file mode 100644 index 0000000000..eff95666ce Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/scale-keyframes.gif differ diff --git a/versions/4.0/en/animation/edit-animation-clip/set-animation-clip.png b/versions/4.0/en/animation/edit-animation-clip/set-animation-clip.png new file mode 100644 index 0000000000..56927bef3b Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/set-animation-clip.png differ diff --git a/versions/4.0/en/animation/edit-animation-clip/set-pro.png b/versions/4.0/en/animation/edit-animation-clip/set-pro.png new file mode 100644 index 0000000000..8ab665f3be Binary files /dev/null and b/versions/4.0/en/animation/edit-animation-clip/set-pro.png differ diff --git a/versions/4.0/en/animation/embedded-player.md b/versions/4.0/en/animation/embedded-player.md new file mode 100644 index 0000000000..97a14c0c5e --- /dev/null +++ b/versions/4.0/en/animation/embedded-player.md @@ -0,0 +1,78 @@ +# Embedded Player (experimental feature) + +The role of the embedded player is to allow the production of animation clips that can be bound at the same time particle effects or other animation, etc., used to meet the function of playing animation at the same time play dynamic effects. It can be used to achieve the function of waving a weapon will appear knife light, or feet on the ground will appear smoke and so on. + +The entire nesting system can be viewed by expanding the **Player Track List** in the Animation window. + +![overview](./embedded/embedded-overview.png) + +> **Note**: In v3.6, this feature is experimental and developers need to enable it in **Preferences** -> **Laboratory**.
+> Any experience problems or suggestions, please feel free to feedback to us in [forum](https://forum.cocos.org/t/topic/137740). +> +> ![enable](embedded/enable.png) + +## Track manipulation + +The embedded player supports **animation player** as well as **particle player**. + +Different tracks can be added by clicking the **+** buttons on the right side of the **animation track list**: + +![track](embedded/add-track.png) + +Right click on the added track to **Remove Animation EmbeddedPlayer Track** and **Clear Data**. + +![delete](embedded/clear-data.png) + +> **Note**: The clear data operation clears the entire track. + +You can select **Copy** and **Delete** operations by clicking the right mouse button on the track data. + +The delete operation only deletes the currently selected data. + +![menu](embedded/track-data-menu.png) + +When no data is selected, you can select **Create Animation EmbeddedPlayer**, **Paste** and **Clear Data**. + +![unselect](embedded/unselect.png) + +After adding a track, the position of the track data and the duration can be adjusted: + +![duration](embedded/track-adjust.gif) + +### Particle player track + +Select the added Particle Player track on the left in the **Animation Track List**, and adjust the track's properties in the **Inspector** panel. + +! [inspector](embedded/particle-inspector.png) + +- **Reconciled speed**: keep the embedded particle system playing at the same rate as the current animation clip +- **Child Path**: the drop-down box allows to select the particle system on different sub-nodes. + + ![select child](embedded/inspector-select-child.png) + +### Animation player tracks + +Select the added animation player track on the left in the **Animation Track List**, and adjust the track's properties in the **Inspector** panel. + +![anim](embedded/inspector-select-anim.png) + +- **Reconciled speed**: keeps the embedded animation at the same playback rate as the current animation clip +- **Child Path**: the drop-down box allows to select the animation components on different sub-nodes. + + ![anim](embedded/inspector-select-child-anim.png) + +- **Animation clips**: The drop-down menu allows you to select different animation clips: + + ![clip](embedded/inspector-select-clip.png) + +## Preview + +The above operation can be used to nest different particle players and animation players when the animation is played, and the effect is displayed as follows. + +![preview](embedded/preview.gif) + +## Adding frame events + +You can also add frame events by right-clicking on the nested bar, in the same way as [Adding Animation Events](animation-event.md). + +![evt](embedded/add-keyframe-event.png) diff --git a/versions/4.0/en/animation/embedded/add-keyframe-event.png b/versions/4.0/en/animation/embedded/add-keyframe-event.png new file mode 100644 index 0000000000..7556452996 Binary files /dev/null and b/versions/4.0/en/animation/embedded/add-keyframe-event.png differ diff --git a/versions/4.0/en/animation/embedded/add-patricle.png b/versions/4.0/en/animation/embedded/add-patricle.png new file mode 100644 index 0000000000..104e7ea268 Binary files /dev/null and b/versions/4.0/en/animation/embedded/add-patricle.png differ diff --git a/versions/4.0/en/animation/embedded/add-track.png b/versions/4.0/en/animation/embedded/add-track.png new file mode 100644 index 0000000000..c8677ffe96 Binary files /dev/null and b/versions/4.0/en/animation/embedded/add-track.png differ diff --git a/versions/4.0/en/animation/embedded/clear-data.png b/versions/4.0/en/animation/embedded/clear-data.png new file mode 100644 index 0000000000..528ac6d871 Binary files /dev/null and b/versions/4.0/en/animation/embedded/clear-data.png differ diff --git a/versions/4.0/en/animation/embedded/embedded-overview.png b/versions/4.0/en/animation/embedded/embedded-overview.png new file mode 100644 index 0000000000..690807d04c Binary files /dev/null and b/versions/4.0/en/animation/embedded/embedded-overview.png differ diff --git a/versions/4.0/en/animation/embedded/enable.png b/versions/4.0/en/animation/embedded/enable.png new file mode 100644 index 0000000000..ffd17af49f Binary files /dev/null and b/versions/4.0/en/animation/embedded/enable.png differ diff --git a/versions/4.0/en/animation/embedded/inspector-select-anim.png b/versions/4.0/en/animation/embedded/inspector-select-anim.png new file mode 100644 index 0000000000..4d63c0ab7f Binary files /dev/null and b/versions/4.0/en/animation/embedded/inspector-select-anim.png differ diff --git a/versions/4.0/en/animation/embedded/inspector-select-child-anim.png b/versions/4.0/en/animation/embedded/inspector-select-child-anim.png new file mode 100644 index 0000000000..3e948ace44 Binary files /dev/null and b/versions/4.0/en/animation/embedded/inspector-select-child-anim.png differ diff --git a/versions/4.0/en/animation/embedded/inspector-select-child.png b/versions/4.0/en/animation/embedded/inspector-select-child.png new file mode 100644 index 0000000000..41eb922cf5 Binary files /dev/null and b/versions/4.0/en/animation/embedded/inspector-select-child.png differ diff --git a/versions/4.0/en/animation/embedded/inspector-select-clip.png b/versions/4.0/en/animation/embedded/inspector-select-clip.png new file mode 100644 index 0000000000..cf8c4fbf49 Binary files /dev/null and b/versions/4.0/en/animation/embedded/inspector-select-clip.png differ diff --git a/versions/4.0/en/animation/embedded/particle-inspector.png b/versions/4.0/en/animation/embedded/particle-inspector.png new file mode 100644 index 0000000000..c500231223 Binary files /dev/null and b/versions/4.0/en/animation/embedded/particle-inspector.png differ diff --git a/versions/4.0/en/animation/embedded/preview.gif b/versions/4.0/en/animation/embedded/preview.gif new file mode 100644 index 0000000000..e4ab7cd308 Binary files /dev/null and b/versions/4.0/en/animation/embedded/preview.gif differ diff --git a/versions/4.0/en/animation/embedded/track-adjust.gif b/versions/4.0/en/animation/embedded/track-adjust.gif new file mode 100644 index 0000000000..c494906b21 Binary files /dev/null and b/versions/4.0/en/animation/embedded/track-adjust.gif differ diff --git a/versions/4.0/en/animation/embedded/track-data-menu.png b/versions/4.0/en/animation/embedded/track-data-menu.png new file mode 100644 index 0000000000..2d1a4ab716 Binary files /dev/null and b/versions/4.0/en/animation/embedded/track-data-menu.png differ diff --git a/versions/4.0/en/animation/embedded/unselect.png b/versions/4.0/en/animation/embedded/unselect.png new file mode 100644 index 0000000000..803c2fd7e2 Binary files /dev/null and b/versions/4.0/en/animation/embedded/unselect.png differ diff --git a/versions/4.0/en/animation/index.md b/versions/4.0/en/animation/index.md new file mode 100644 index 0000000000..57cf391f56 --- /dev/null +++ b/versions/4.0/en/animation/index.md @@ -0,0 +1,31 @@ +# Animation System + +Cocos Creator has a built-in universal animation system for implementing keyframe-based animations. In addition to standard moving, rotation, scaling and frame animations, it also supports arbitrary component properties and user-defined property drivers, plus arbitrary time curves and innovative trajectory editing, allowing content producers to create detailed dynamic effects without writing a single line of code. + +![animation cover](index/main.gif) + +> **Note**: the **Animation** panel that comes with Cocos Creator is suitable for creating less complex animations that need to be linked with logic, such as UI animations. If you want to create complex effects, character animations, nested animations, etc., consider using Spine, DragonBones or skeletal animation editor for 3D models instead. + +## Content + +- [Animation Clip](animation-clip.md): asset containing animation data, reusable. Animation clips can be produced by the **Animation** panel, or imported by some external asset that already contains skeletal animation. + +- [Animation Component Reference](animation-comp.md): the Animation component can drive node and component properties on their nodes and children in an animated manner, including properties in user-defined scripts. + +- [Animation Editor](animation.md): learn how to use the Animation Editor and create/modify/generate animation clip assets through the Animation Editor. + +- [Skeletal Animation](skeletal-animation.md): a common but special type of animation, this article mainly introduces it and explains its usage. + +- [Controlling Animation with Scripts](animation-component.md): the Animation component manages a set of animation states, which are used to control the play, pause, continue, stop, switch, etc. of each animation. + + - [Animation State](animation-state.md): the state of animation clips is stored in an object called animation state, animation state can control the animation clips that need to be used on the object. Animation state provides more animation control interfaces, through which animation can be played, stopped, shifted, set to loop mode and other more detailed control. + +- [Embedded Player](embedded-player.md): embeded-player is used to embed other particle players or animation players in the animation clips. + +- [Marionette Animation System](./marionette/index.md): added in v3.4 and implements an automated and reusable skeletal animation process controlled by a state machine. + +- [Procedural Animation](./marionette/procedural-animation/index.md): New in v3.8, you can programmatically control the sampling process of the animation through different animation nodes. + +- [Auxiliary Curve](./animation-auxiliary-curve.md): New in v3.8, used to add a curve editor that can be used to get information in animation clips. + +According to different animation requirements, the operation steps and code implementation for specific animations are different, please refer to the official example [animation](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/animation), which mainly introduces some common editing operations and code examples for reference. diff --git a/versions/4.0/en/animation/index/main.gif b/versions/4.0/en/animation/index/main.gif new file mode 100644 index 0000000000..6b202df4ea Binary files /dev/null and b/versions/4.0/en/animation/index/main.gif differ diff --git a/versions/4.0/en/animation/joint-texture-layout.md b/versions/4.0/en/animation/joint-texture-layout.md new file mode 100644 index 0000000000..2a44bbe62e --- /dev/null +++ b/versions/4.0/en/animation/joint-texture-layout.md @@ -0,0 +1,88 @@ +# Joint Texture Layout Settings + +To ensure that [Skeletal Animation](./skeletal-animation.md) can also participate fully and correctly in [Dynamic Instancing](../engine/renderable/model-component.md#instancing-%E5%90%88%E6%89%B9), the developer needs to manually specify how the data for each skeletal texture is assigned. + +For example, a scene where a large number of identical characters are to be drawn, each of which may be walking/jumping/attacking. If one Drawcall should be able to draw all characters correctly, an important prerequisite is that **the data for all three animations (walk, jump, attack) are stored inside the same joint texture**. + +Currently, in the default [pre-baked skeletal animation mode](./skeletal-animation.md#pre-baked-Skeletal-Animation-System), the joint textures are already globally auto-reused, but the size of each texture and which animations they store are not available. and which animations are stored in each of them are unpredictable. If instancing of the skinned model is enabled without any processing, The final runtime effect may have some animations that are correct and some that are completely wrong, which are completely unpredictable. + +A **Joint Texture Layout Settings** page was added to the editor **Project Setting** panel to manually specify which animation information is stored for which skeletons in each joint texture. + +![joint texture layout panel](./joint-texture-layout/joint-texture-layout-panel.png) + +> **Note**: the joint texture layout settings panel provides, essentially, runtime **memory allocation guidance rules**. For specified skeleton and animation assets, they are guaranteed to be allocated according to the specified rules. However, if an asset is used at runtime that is not specified by the rules, it will go back to the automatic allocation mode of global reuse. + +## Joint texture layout settings + +Use the **instanced-skinning** scenario from the example project [show-cases](https://github.com/cocos/cocos-example-projects/tree/v3.8/show-cases/assets/scenes) as an example to see how the joint texture layout is set up and how it works. The following figure shows a sample scene. + +The following figure shows a sample scenario with multiple instances from the same model, playing completely different animations at the same time. Notice the current scene, plus the UI, has a total Drawcall of 60 and an instance count of 0. This state will be used as the basis for later changes to compare. + +![Baseline](./joint-texture-layout/instancing_baseline.gif) + +To create a model with instancing version turned on, the following steps are required: + +1. In the **Inspector** panel, check the `UseBakedAnimation` property in the SkeletalAnimation component (using pre-baked animation mode). + + ![use baked animation](./joint-texture-layout/use-baked-animation.png) + +2. check `USE INSTANCING` for all materials used by SkinningModel. + + ![Enabling](./joint-texture-layout/enabling_instancing.png) + +In the example scene, two sets of Prefabs were made and set the material diffuse color to blue in the instancing version in order to see and distinguish the performance of both systems. Notice that the effect has been exactly correct and that only 5 drawcalls (each model is divided into 5 parts) were used, with 45 instances. + +![Normal](./joint-texture-layout/instancing_normal.gif) + +> **Note**: the reason for rendering all models correctly here is that the amount of animation data is still relatively small, and the generic joint texture global reuse logic already writes all animation data to the same texture, so the effect is correct. However, if the size of the default joint texture (360 * 360) is exceeded by new animations that may be added at any time, the animation will definitely go wrong, which is why the joint texture layout panel must exist. + +## Joint Texture Layout Effects + +For demonstration purposes, we can intentionally put each animation on a separate texture in the **Joint Texture Layout Settings** panel to see the final rendering effect. + +First, open the **Joint Texture Layout Settings** panel: + +![Panel](./joint-texture-layout/joint_texture_layout_new.png) + +The three **+** in the panel are used as follows: + +- ① - Used to add Texture units, a Texture unit consists of multiple Skeleton units. +- ② - Used to add Skeleton units, a Skeleton unit consists of a Skeleton asset and one to more AnimationClip assets. +- ③ - Used to add AnimationClip asset slots. + +Here there are 9 different animations separated into 9 Texture cells. + +![WrongLayout](./joint-texture-layout/joint_texture_layout_wrong.png) + +Rerun the scene and the effect becomes: + +![Wrong](./joint-texture-layout/instancing_wrong.gif) + +Notice there is a problem with the animation, all the animations become attack actions, and the model disappears from time to time. The reason behind this can be precisely analyzed. + +- Each drawcall draws 9 instances, which are broadcasting 9 different animations. +- However, each drawcall can only use one skeletal animation texture, and here it is obvious that the Texture unit 0 is used and there is only one attack animation. +- The length of different animation segments is different, and some of them are longer than the attack segment, so at the last time they will be read outside the valid area of Texture unit 0, where the data is not defined (usually the default is all 0), not valid skeleton transformation data, so naturally they cannot be rendered correctly. + +> **Note**: the 9 textures here all have the same skeletal animation information, so only the action is wrong in the final result, even if the texture is already wrong, but if there are multiple skeletons with animation information in one texture, and the textures don't match at the same time, the rendering effect will be completely wrong. + +For the example scene above, since the model does need to play the 9 animation clips on the same screen at the same time, the correct joint texture layout setting should be: + +![CorrectLayout](./joint-texture-layout/joint_texture_layout_correct.png) + +This will **guarantee** that it renders correctly. Observe the change in the relevant data on the panel: + +- Texture cell 0 has a total size of 276 x 276 (automatically generated by the algorithm, the minimum size sufficient to hold all the specified animation data). +- The specified 9 groups of animation data take up 94.41% of this texture, with 5.59% excess space (this space is not involved in global reuse at runtime). + +In addition, the color of the icon next to the texture size indicates the device adaptation of the current texture: + +- Green (up to 1024 sides): all devices are guaranteed to work. +- Yellow (1024 ~ 2048 sides): Some mobile devices or mini-game platforms that do not support floating point texture may not support it. +- Red (side length 2048 or more): Not supported on many mobile devices. + +> **Note**: this is just a set of 9 animations for one skeleton, on one texture, but **any number of animations for any number of skeletons** can be put on each texture, as long as the total size does not exceed the device limit. It is often more common to have multiple sets of skeletons on a single texture, for example for [flat shadows for skinned models](./skeletal-animation.md#about-dynamic-instancing). + +As more instances continue being adding to the scene, notice the number of drawcalls does not change, only the number of instances increases: + +![Bulk](./joint-texture-layout/instancing_bulk.gif) diff --git a/versions/4.0/en/animation/joint-texture-layout/enabling_instancing.png b/versions/4.0/en/animation/joint-texture-layout/enabling_instancing.png new file mode 100644 index 0000000000..572a50a021 Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/enabling_instancing.png differ diff --git a/versions/4.0/en/animation/joint-texture-layout/instancing_baseline.gif b/versions/4.0/en/animation/joint-texture-layout/instancing_baseline.gif new file mode 100644 index 0000000000..8ccfa39a27 Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/instancing_baseline.gif differ diff --git a/versions/4.0/en/animation/joint-texture-layout/instancing_bulk.gif b/versions/4.0/en/animation/joint-texture-layout/instancing_bulk.gif new file mode 100644 index 0000000000..9913a49ff6 Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/instancing_bulk.gif differ diff --git a/versions/4.0/en/animation/joint-texture-layout/instancing_normal.gif b/versions/4.0/en/animation/joint-texture-layout/instancing_normal.gif new file mode 100644 index 0000000000..be18cef6f3 Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/instancing_normal.gif differ diff --git a/versions/4.0/en/animation/joint-texture-layout/instancing_wrong.gif b/versions/4.0/en/animation/joint-texture-layout/instancing_wrong.gif new file mode 100644 index 0000000000..bee0f02ba6 Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/instancing_wrong.gif differ diff --git a/versions/4.0/en/animation/joint-texture-layout/joint-texture-layout-panel.png b/versions/4.0/en/animation/joint-texture-layout/joint-texture-layout-panel.png new file mode 100644 index 0000000000..9ef1a7bd3f Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/joint-texture-layout-panel.png differ diff --git a/versions/4.0/en/animation/joint-texture-layout/joint_texture_layout_correct.png b/versions/4.0/en/animation/joint-texture-layout/joint_texture_layout_correct.png new file mode 100644 index 0000000000..12abeb7c26 Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/joint_texture_layout_correct.png differ diff --git a/versions/4.0/en/animation/joint-texture-layout/joint_texture_layout_new.png b/versions/4.0/en/animation/joint-texture-layout/joint_texture_layout_new.png new file mode 100644 index 0000000000..41af682980 Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/joint_texture_layout_new.png differ diff --git a/versions/4.0/en/animation/joint-texture-layout/joint_texture_layout_wrong.png b/versions/4.0/en/animation/joint-texture-layout/joint_texture_layout_wrong.png new file mode 100644 index 0000000000..50648f4818 Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/joint_texture_layout_wrong.png differ diff --git a/versions/4.0/en/animation/joint-texture-layout/use-baked-animation.png b/versions/4.0/en/animation/joint-texture-layout/use-baked-animation.png new file mode 100644 index 0000000000..430acb7490 Binary files /dev/null and b/versions/4.0/en/animation/joint-texture-layout/use-baked-animation.png differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/figures/cover.png b/versions/4.0/en/animation/marionette/additive-animation/figures/cover.png new file mode 100644 index 0000000000..d7b539b8cd Binary files /dev/null and b/versions/4.0/en/animation/marionette/additive-animation/figures/cover.png differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/figures/crouch+leaning-pose.png b/versions/4.0/en/animation/marionette/additive-animation/figures/crouch+leaning-pose.png new file mode 100644 index 0000000000..1dc0cb55f6 Binary files /dev/null and b/versions/4.0/en/animation/marionette/additive-animation/figures/crouch+leaning-pose.png differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/figures/crouch-pose.png b/versions/4.0/en/animation/marionette/additive-animation/figures/crouch-pose.png new file mode 100644 index 0000000000..79f6775083 Binary files /dev/null and b/versions/4.0/en/animation/marionette/additive-animation/figures/crouch-pose.png differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/figures/import-setting-import-as-additive.png b/versions/4.0/en/animation/marionette/additive-animation/figures/import-setting-import-as-additive.png new file mode 100644 index 0000000000..cf6ee09495 Binary files /dev/null and b/versions/4.0/en/animation/marionette/additive-animation/figures/import-setting-import-as-additive.png differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/figures/layer-additive.png b/versions/4.0/en/animation/marionette/additive-animation/figures/layer-additive.png new file mode 100644 index 0000000000..cd141e20b9 Binary files /dev/null and b/versions/4.0/en/animation/marionette/additive-animation/figures/layer-additive.png differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/figures/leaning-pose.png b/versions/4.0/en/animation/marionette/additive-animation/figures/leaning-pose.png new file mode 100644 index 0000000000..de5ab96e7e Binary files /dev/null and b/versions/4.0/en/animation/marionette/additive-animation/figures/leaning-pose.png differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/figures/standing-pose.png b/versions/4.0/en/animation/marionette/additive-animation/figures/standing-pose.png new file mode 100644 index 0000000000..8f4dafe4b4 Binary files /dev/null and b/versions/4.0/en/animation/marionette/additive-animation/figures/standing-pose.png differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/index.md b/versions/4.0/en/animation/marionette/additive-animation/index.md new file mode 100644 index 0000000000..b4f2c5f051 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/index.md @@ -0,0 +1,62 @@ +# Additive Animation + +Additive animation is a way to reuse animation effects. + +Imagine your character has two basic poses: standing and crouching: + +![img](./figures/standing-pose.png) + +![img](./figures/crouch-pose.png) + +In standing, you make a pose for the character where the upper body is tilted at an angle to the right: + +![](./figures/leaning-pose.png) + +If you want the character to have a similar tilted stance in the crouching state, one way to do it would of course be to make another separate stance for the crouching state, but that would inevitably be a repetitive endeavor. + +Since the "change" from standing to standing tilt and from crouching to crouching tilt is similar, it is possible to "subtract" the standing stance from the tilted stance to get a difference stance. By superimposing such a differential stance on the crouching stance, one obtains a crouching tilted stance: + +![](./figures/crouch%2Bleaning-pose.png) + +The schematic is shown below: + +![](./figures/cover.png) + +A tilted pose "minus" a standing pose is called an **Additive Pose**, and the animation effect created by applying it to a crouching pose is called an **Additive Animation**. + +Formally, an additive animation is the difference of an animation relative to a reference pose that can be superimposed on other animations. The underlying logic for using additive animation is that you can add the same type of changes to different animations, such as leaning forward, backward, left, or right when a character is moving differently. If you use additive animation, you can add the same effect to walking, running, jumping, standing, etc., instead of having to create a set of similar animations for each state, which reduces the amount of animation assets needed for each state. + +## Generating Additive Animations + +Currently, additive animations can only be generated from animation clips. + +By checking the "Import as Additive" box in the Animation Import Configuration panel, the selected animation clip will generate an additive animation when it is used in an animation drawing: + +![](./figures/import-setting-import-as-additive.png) + +> Note that an animation is either imported as an additive animation or as a non-additive animation. +> If you want an animation to be used as both an additive and non-additive animation, you can copy this animation in the import panel and configure it separately. + +## Apply Additive animation + +The animation map applies additive animation in two ways, namely: + +- Configure the level as an additive level. + +- Use [Additive blend node](../procedural-animation/pose-graph/pose-nodes/blend-poses.md#Additive-blending) in the graph map. + +> Note that the animation used in the additive hierarchy must produce additive poses. +> For example, if a additive layer ends up outputting non-additive poses, this will result in anomalous animations - often the model will be found to be "bigger". +> Conversely, if a non-additive layer ends up with a additive animation, this will also result in an anomalous animation - the model will often be found to have "disappeared" or become smaller. + +## Additive Layers + +In the Animation Graph Inspector panel, you can set the selected layer to be **Additive**: + +![](./figures/layer-additive.png) + +When checked, when the animation effect (graph) generated by this layer will be blended to the preceeding layer as an additive. + +## Pose Graph: additive blending nodes + +See: [additive blend node](../procedural-animation/pose-graph/pose-nodes/blend-poses.md#additive-blending). diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration.blend b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration.blend new file mode 100644 index 0000000000..485fd67c49 Binary files /dev/null and b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration.blend differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/.gitignore b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/.gitignore new file mode 100644 index 0000000000..576a8e3741 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/.gitignore @@ -0,0 +1,26 @@ + +#/////////////////////////// +# Cocos Creator 3D Project +#/////////////////////////// + +/library/ +/temp/ +/local/ +/build/ +/profiles/ +/native/engine/android/**/*/assets + +#////////////////////////// +# NPM +#////////////////////////// +node_modules/ + +#////////////////////////// +# VSCode +#////////////////////////// +.vscode/ + +#////////////////////////// +# WebStorm +#////////////////////////// +.idea/ diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration.meta b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration.meta new file mode 100644 index 0000000000..e0e66f3f8f --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "29781f57-d558-4cd4-b7ae-6d6a37d4ae71", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph new file mode 100644 index 0000000000..5897720292 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph @@ -0,0 +1,557 @@ +[ + { + "__type__": "cc.animation.AnimationGraph", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "_layers": [ + { + "__id__": 1 + }, + { + "__id__": 34 + } + ], + "_variables": { + "BaseLayerPose": { + "__id__": 44 + } + } + }, + { + "__type__": "cc.animation.Layer", + "_stateMachine": { + "__id__": 2 + }, + "name": "Base", + "weight": 1, + "mask": null, + "additive": false, + "_stashes": {} + }, + { + "__type__": "cc.animation.StateMachine", + "__editorExtras__": { + "name": "", + "id": "16866441677320.5891144306941865", + "clone": null, + "viewport": { + "scale": 1, + "top": 0, + "left": -1 + } + }, + "_states": [ + { + "__id__": 3 + }, + { + "__id__": 4 + }, + { + "__id__": 5 + }, + { + "__id__": 6 + }, + { + "__id__": 10 + }, + { + "__id__": 14 + } + ], + "_transitions": [ + { + "__id__": 18 + }, + { + "__id__": 19 + }, + { + "__id__": 24 + }, + { + "__id__": 29 + } + ], + "_entryState": { + "__id__": 3 + }, + "_exitState": { + "__id__": 4 + }, + "_anyState": { + "__id__": 5 + } + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441677320.3764511364334071", + "clone": null, + "centerX": -265, + "centerY": 3 + }, + "name": "Entry" + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441677320.3923465698017483", + "clone": null, + "centerX": 125, + "centerY": 0 + }, + "name": "Exit" + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441677320.3550700390662027", + "clone": null, + "centerX": 125, + "centerY": 0 + }, + "name": "Any" + }, + { + "__type__": "cc.animation.Motion", + "__editorExtras__": { + "name": "", + "id": "16866441797990.8001792367096607", + "clone": null, + "centerX": -122, + "centerY": 23 + }, + "name": "Crouch", + "_components": [], + "motion": { + "__id__": 7 + }, + "speed": 1, + "speedMultiplier": "", + "speedMultiplierEnabled": false, + "transitionInEventBinding": { + "__id__": 8 + }, + "transitionOutEventBinding": { + "__id__": 9 + } + }, + { + "__type__": "cc.animation.ClipMotion", + "__editorExtras__": { + "name": "", + "id": "16866441798030.8594645607386218", + "clone": null + }, + "clip": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@2ff01", + "__expectedType__": "cc.AnimationClip" + } + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Motion", + "__editorExtras__": { + "name": "", + "id": "16866458424730.7772643494706266", + "clone": null, + "centerX": -128, + "centerY": -29 + }, + "name": "Standing", + "_components": [], + "motion": { + "__id__": 11 + }, + "speed": 1, + "speedMultiplier": "", + "speedMultiplierEnabled": false, + "transitionInEventBinding": { + "__id__": 12 + }, + "transitionOutEventBinding": { + "__id__": 13 + } + }, + { + "__type__": "cc.animation.ClipMotion", + "__editorExtras__": { + "name": "", + "id": "16866458424760.04644098735663049", + "clone": null + }, + "clip": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e7f91", + "__expectedType__": "cc.AnimationClip" + } + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Motion", + "__editorExtras__": { + "name": "", + "id": "16866487662470.8543040974437142", + "clone": null, + "centerX": -117, + "centerY": 87 + }, + "name": "Leaning_Original", + "_components": [], + "motion": { + "__id__": 15 + }, + "speed": 1, + "speedMultiplier": "", + "speedMultiplierEnabled": false, + "transitionInEventBinding": { + "__id__": 16 + }, + "transitionOutEventBinding": { + "__id__": 17 + } + }, + { + "__type__": "cc.animation.ClipMotion", + "__editorExtras__": { + "name": "", + "id": "16866487662500.9587811069492036", + "clone": null + }, + "clip": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@d03ee", + "__expectedType__": "cc.AnimationClip" + } + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Transition", + "__editorExtras__": null, + "from": { + "__id__": 3 + }, + "to": { + "__id__": 10 + }, + "conditions": [] + }, + { + "__type__": "cc.animation.AnimationTransition", + "__editorExtras__": null, + "from": { + "__id__": 5 + }, + "to": { + "__id__": 10 + }, + "conditions": [ + { + "__id__": 20 + } + ], + "destinationStart": 0, + "relativeDestinationStart": false, + "startEventBinding": { + "__id__": 22 + }, + "endEventBinding": { + "__id__": 23 + }, + "duration": 0.3, + "relativeDuration": false, + "exitConditionEnabled": false, + "_exitCondition": 1 + }, + { + "__type__": "cc.animation.BinaryCondition", + "operator": 0, + "lhs": 0, + "lhsBinding": { + "__id__": 21 + }, + "rhs": 0 + }, + { + "__type__": "cc.animation.TCVariableBinding", + "type": 3, + "variableName": "BaseLayerPose" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationTransition", + "__editorExtras__": null, + "from": { + "__id__": 5 + }, + "to": { + "__id__": 6 + }, + "conditions": [ + { + "__id__": 25 + } + ], + "destinationStart": 0, + "relativeDestinationStart": false, + "startEventBinding": { + "__id__": 27 + }, + "endEventBinding": { + "__id__": 28 + }, + "duration": 0.3, + "relativeDuration": false, + "exitConditionEnabled": false, + "_exitCondition": 1 + }, + { + "__type__": "cc.animation.BinaryCondition", + "operator": 0, + "lhs": 0, + "lhsBinding": { + "__id__": 26 + }, + "rhs": 1 + }, + { + "__type__": "cc.animation.TCVariableBinding", + "type": 3, + "variableName": "BaseLayerPose" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationTransition", + "__editorExtras__": null, + "from": { + "__id__": 5 + }, + "to": { + "__id__": 14 + }, + "conditions": [ + { + "__id__": 30 + } + ], + "destinationStart": 0, + "relativeDestinationStart": false, + "startEventBinding": { + "__id__": 32 + }, + "endEventBinding": { + "__id__": 33 + }, + "duration": 0.3, + "relativeDuration": false, + "exitConditionEnabled": false, + "_exitCondition": 1 + }, + { + "__type__": "cc.animation.BinaryCondition", + "operator": 0, + "lhs": 0, + "lhsBinding": { + "__id__": 31 + }, + "rhs": 2 + }, + { + "__type__": "cc.animation.TCVariableBinding", + "type": 3, + "variableName": "BaseLayerPose" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Layer", + "_stateMachine": { + "__id__": 35 + }, + "name": "Leaning", + "weight": 1, + "mask": null, + "additive": true, + "_stashes": {} + }, + { + "__type__": "cc.animation.StateMachine", + "__editorExtras__": { + "name": "", + "id": "16866441867500.6467469582792678", + "clone": null, + "viewport": { + "scale": 1, + "top": 0, + "left": 0 + } + }, + "_states": [ + { + "__id__": 36 + }, + { + "__id__": 37 + }, + { + "__id__": 38 + }, + { + "__id__": 39 + } + ], + "_transitions": [ + { + "__id__": 43 + } + ], + "_entryState": { + "__id__": 36 + }, + "_exitState": { + "__id__": 37 + }, + "_anyState": { + "__id__": 38 + } + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441867500.2990931519407911", + "clone": null, + "centerX": -125, + "centerY": 0 + }, + "name": "Entry" + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441867500.16966542714941157", + "clone": null, + "centerX": 125, + "centerY": 0 + }, + "name": "Exit" + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441867500.650266379833792", + "clone": null, + "centerX": 125, + "centerY": 0 + }, + "name": "Any" + }, + { + "__type__": "cc.animation.Motion", + "__editorExtras__": { + "name": "", + "id": "16866449866630.2924323274845875", + "clone": null, + "centerX": -1, + "centerY": -4 + }, + "name": "Leaning", + "_components": [], + "motion": { + "__id__": 40 + }, + "speed": 1, + "speedMultiplier": "", + "speedMultiplierEnabled": false, + "transitionInEventBinding": { + "__id__": 41 + }, + "transitionOutEventBinding": { + "__id__": 42 + } + }, + { + "__type__": "cc.animation.ClipMotion", + "__editorExtras__": { + "name": "", + "id": "16866449866670.9509605487815147", + "clone": null + }, + "clip": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@94e83", + "__expectedType__": "cc.AnimationClip" + } + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Transition", + "__editorExtras__": null, + "from": { + "__id__": 36 + }, + "to": { + "__id__": 39 + }, + "conditions": [] + }, + { + "__type__": "cc.animation.PlainVariable", + "_type": 3, + "_value": 0 + } +] \ No newline at end of file diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph.meta b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph.meta new file mode 100644 index 0000000000..d25c93115e --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.2.0", + "importer": "animation-graph", + "imported": true, + "uuid": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb new file mode 100644 index 0000000000..39805bcb0d Binary files /dev/null and b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb differ diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb.meta b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb.meta new file mode 100644 index 0000000000..0b68f5da70 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb.meta @@ -0,0 +1,400 @@ +{ + "ver": "2.3.8", + "importer": "gltf", + "imported": true, + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe", + "files": [ + "__original-animation-0.cconb", + "__original-animation-1.cconb", + "__original-animation-2.cconb", + "__original-animation-3.cconb" + ], + "subMetas": { + "60865": { + "importer": "gltf-skeleton", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@60865", + "displayName": "", + "id": "60865", + "name": "Rig.skeleton", + "userData": { + "gltfIndex": 0, + "jointsLength": 27 + }, + "ver": "1.0.1", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "e2349": { + "importer": "gltf-mesh", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e2349", + "displayName": "", + "id": "e2349", + "name": "Mesh.mesh", + "userData": { + "gltfIndex": 0, + "triangleCount": 324 + }, + "ver": "1.1.1", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {} + }, + "2ff01": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@2ff01", + "displayName": "", + "id": "2ff01", + "name": "Crouch.animation", + "userData": { + "gltfIndex": 0, + "wrapMode": 2, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [] + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "94e83": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@94e83", + "displayName": "", + "id": "94e83", + "name": "Leaning_Additive.animation", + "userData": { + "gltfIndex": 1, + "wrapMode": 2, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [], + "additive": { + "enabled": true, + "refClip": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e7f91" + } + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "d03ee": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@d03ee", + "displayName": "", + "id": "d03ee", + "name": "Leaning_Original.animation", + "userData": { + "gltfIndex": 1, + "wrapMode": 2, + "speed": 1, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [] + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "aff49": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@aff49", + "displayName": "", + "id": "aff49", + "name": "ReferencePose.animation", + "userData": { + "gltfIndex": 2, + "wrapMode": 2, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [] + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "e7f91": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e7f91", + "displayName": "", + "id": "e7f91", + "name": "Standing.animation", + "userData": { + "gltfIndex": 3, + "wrapMode": 2, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [], + "additive": { + "enabled": false + } + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "b4e6b": { + "importer": "gltf-material", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@b4e6b", + "displayName": "", + "id": "b4e6b", + "name": "Material.material", + "userData": { + "gltfIndex": 0 + }, + "ver": "1.0.14", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "bbd5d": { + "importer": "gltf-scene", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "displayName": "", + "id": "bbd5d", + "name": "AdditivePoseDemonstration.prefab", + "userData": { + "gltfIndex": 0 + }, + "ver": "1.0.13", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + } + }, + "userData": { + "imageMetas": [], + "animationImportSettings": [ + { + "name": "Crouch", + "duration": 0.0416666679084301, + "fps": 30, + "splits": [ + { + "name": "Crouch", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "previousId": "2ff01" + } + ] + }, + { + "name": "Leaning", + "duration": 0.0416666679084301, + "fps": 30, + "splits": [ + { + "name": "Leaning_Additive", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "previousId": "94e83", + "additive": { + "enabled": true, + "refClip": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e7f91" + } + }, + { + "name": "Leaning_Original", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "speed": 1, + "previousId": "d03ee" + } + ] + }, + { + "name": "ReferencePose", + "duration": 0.0416666679084301, + "fps": 30, + "splits": [ + { + "name": "ReferencePose", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "previousId": "aff49" + } + ] + }, + { + "name": "Standing", + "duration": 0.0416666679084301, + "fps": 30, + "splits": [ + { + "name": "Standing", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "previousId": "e7f91", + "additive": { + "enabled": false + } + } + ] + } + ], + "redirect": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "lods": { + "enable": false, + "hasBuiltinLOD": false, + "options": [ + { + "screenRatio": 0.25, + "faceCount": 1 + }, + { + "screenRatio": 0.125, + "faceCount": 0.25 + }, + { + "screenRatio": 0.01, + "faceCount": 0.1 + } + ] + }, + "assetFinder": { + "meshes": [ + "3705c9df-8a1c-4d36-92d3-512664e51cfe@e2349" + ], + "skeletons": [ + "3705c9df-8a1c-4d36-92d3-512664e51cfe@60865" + ], + "textures": [], + "materials": [ + "3705c9df-8a1c-4d36-92d3-512664e51cfe@b4e6b" + ], + "scenes": [ + "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d" + ] + }, + "materials": { + "3705c9df-8a1c-4d36-92d3-512664e51cfe@b4e6b": { + "__type__": "cc.Material", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "_effectAsset": { + "__uuid__": "c8f66d17-351a-48da-a12c-0212d28575c4", + "__expectedType__": "cc.EffectAsset" + }, + "_techIdx": 0, + "_defines": [ + {}, + {}, + {}, + {}, + {} + ], + "_states": [ + { + "rasterizerState": { + "cullMode": 0 + }, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + }, + { + "rasterizerState": {}, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + }, + { + "rasterizerState": {}, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + }, + { + "rasterizerState": {}, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + }, + { + "rasterizerState": {}, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + } + ], + "_props": [ + { + "albedoScale": { + "__type__": "cc.Vec3", + "x": 0.2694019675254822, + "y": 0.2722896635532379, + "z": 0.8000000715255737 + }, + "roughness": 0, + "specularIntensity": 0.1 + }, + {}, + {}, + {}, + {} + ] + } + } + } +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene new file mode 100644 index 0000000000..58c63c17d4 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene @@ -0,0 +1,1990 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "AdditivePoseDemonstration", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "scene": { + "__id__": 1 + } + }, + { + "__type__": "cc.Scene", + "_name": "AdditivePoseDemonstration", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 5 + }, + { + "__id__": 7 + }, + { + "__id__": 22 + }, + { + "__id__": 39 + }, + { + "__id__": 56 + }, + { + "__id__": 73 + }, + { + "__id__": 76 + }, + { + "__id__": 83 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 90 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "autoReleaseAssets": false, + "_globals": { + "__id__": 91 + }, + "_id": "71e36a35-a552-4298-90b5-3d1064fa30cb" + }, + { + "__type__": "cc.Node", + "_name": "Main Light", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 3 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.06397656665577071, + "y": -0.44608233363525845, + "z": -0.8239028751062036, + "w": -0.3436591377065261 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -117.894, + "y": -194.909, + "z": 38.562 + }, + "_id": "c0y6F5f+pAvI805TdmxIjx" + }, + { + "__type__": "cc.DirectionalLight", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 250, + "b": 240, + "a": 255 + }, + "_useColorTemperature": false, + "_colorTemperature": 6550, + "_staticSettings": { + "__id__": 4 + }, + "_visibility": -325058561, + "_illuminanceHDR": 65000, + "_illuminance": 65000, + "_illuminanceLDR": 1.6927083333333335, + "_shadowEnabled": false, + "_shadowPcf": 0, + "_shadowBias": 0.00001, + "_shadowNormalBias": 0, + "_shadowSaturation": 1, + "_shadowDistance": 50, + "_shadowInvisibleOcclusionRange": 200, + "_csmLevel": 4, + "_csmLayerLambda": 0.75, + "_csmOptimizationMode": 2, + "_csmAdvancedOptions": false, + "_csmLayersTransition": false, + "_csmTransitionRange": 0.05, + "_shadowFixedArea": false, + "_shadowNear": 0.1, + "_shadowFar": 10, + "_shadowOrthoSize": 5, + "_id": "597uMYCbhEtJQc0ffJlcgA" + }, + { + "__type__": "cc.StaticLightSettings", + "_baked": false, + "_editorOnly": false, + "_castShadow": false + }, + { + "__type__": "cc.Node", + "_name": "Main Camera", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 6 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.6228552625332178, + "y": 1.2214186893935963, + "z": 4.468293757497586 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "c9DMICJLFO5IeO07EPon7U" + }, + { + "__type__": "cc.Camera", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 5 + }, + "_enabled": true, + "__prefab": null, + "_projection": 0, + "_priority": 0, + "_fov": 45, + "_fovAxis": 0, + "_orthoHeight": 1.5, + "_near": 0.1, + "_far": 100, + "_color": { + "__type__": "cc.Color", + "r": 51, + "g": 51, + "b": 51, + "a": 255 + }, + "_depth": 1, + "_stencil": 0, + "_clearFlags": 14, + "_rect": { + "__type__": "cc.Rect", + "x": 0, + "y": 0, + "width": 1, + "height": 1 + }, + "_aperture": 19, + "_shutter": 7, + "_iso": 0, + "_screenScale": 1, + "_visibility": 1822425087, + "_targetTexture": null, + "_postProcess": null, + "_usePostProcess": false, + "_cameraType": -1, + "_trackingType": 0, + "_id": "7dWQTpwS5LrIHnc1zAPUtf" + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 8 + }, + "__editorExtras__": {} + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 7 + }, + "asset": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "__expectedType__": "cc.Prefab" + }, + "fileId": "6eteDuTGJdMpDat7uq2H51", + "instance": { + "__id__": 9 + }, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "a74QLAgbVA66mIBu/fvUTR", + "prefabRootNode": null, + "mountedChildren": [], + "mountedComponents": [ + { + "__id__": 10 + }, + { + "__id__": 13 + } + ], + "propertyOverrides": [ + { + "__id__": 17 + }, + { + "__id__": 19 + } + ], + "removedComponents": [ + { + "__id__": 20 + }, + { + "__id__": 21 + } + ] + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 11 + }, + "components": [ + { + "__id__": 12 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "71nT0fGWxfJ7Ln9zk1h3p9" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 7 + } + }, + "node": { + "__id__": 7 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "b6E+SRjatAP6rP7rSbIwm4" + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 14 + }, + "components": [ + { + "__id__": 15 + }, + { + "__id__": 16 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 7 + } + }, + "node": { + "__id__": 7 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "04yDjhtuFECLr+R5dbRpvX" + }, + { + "__type__": "9177brmDQ1BxYgGRYI7jZZd", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 7 + } + }, + "node": { + "__id__": 7 + }, + "_enabled": true, + "__prefab": null, + "baseLayerPose": 2, + "additiveLayerWeight": 0, + "_id": "0dW2UVM+ZHDoaXsOVlxnmL" + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 18 + }, + "propertyPath": [ + "_name" + ], + "value": "Leaning_Original" + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 18 + }, + "propertyPath": [ + "_lpos" + ], + "value": { + "__type__": "cc.Vec3", + "x": -2.863, + "y": 0, + "z": 0 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "46p/wNB7td07WS89tf2KfI" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b1BxQNz2Fd1qVeG4pkCjo+" + ] + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 23 + }, + "__editorExtras__": {} + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 22 + }, + "asset": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "__expectedType__": "cc.Prefab" + }, + "fileId": "6eteDuTGJdMpDat7uq2H51", + "instance": { + "__id__": 24 + }, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "1bq5EPIq5NvrvsCKGygUQs", + "prefabRootNode": null, + "mountedChildren": [], + "mountedComponents": [ + { + "__id__": 25 + }, + { + "__id__": 28 + } + ], + "propertyOverrides": [ + { + "__id__": 32 + }, + { + "__id__": 34 + }, + { + "__id__": 35 + }, + { + "__id__": 36 + } + ], + "removedComponents": [ + { + "__id__": 37 + }, + { + "__id__": 38 + } + ] + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 26 + }, + "components": [ + { + "__id__": 27 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "71nT0fGWxfJ7Ln9zk1h3p9" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 22 + } + }, + "node": { + "__id__": 22 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "2ewNSgCdBMfJdOFfHmji7x" + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 29 + }, + "components": [ + { + "__id__": 30 + }, + { + "__id__": 31 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 22 + } + }, + "node": { + "__id__": 22 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "69m0LjdqRFdLaM/zqAkw5I" + }, + { + "__type__": "9177brmDQ1BxYgGRYI7jZZd", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 22 + } + }, + "node": { + "__id__": 22 + }, + "_enabled": true, + "__prefab": null, + "baseLayerPose": 0, + "additiveLayerWeight": 0, + "_id": "1bPjyjBAhGFI0thugs+fo9" + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 33 + }, + "propertyPath": [ + "_name" + ], + "value": "Standing" + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 33 + }, + "propertyPath": [ + "_lpos" + ], + "value": { + "__type__": "cc.Vec3", + "x": -1.411, + "y": 0, + "z": 0 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 33 + }, + "propertyPath": [ + "_lrot" + ], + "value": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 33 + }, + "propertyPath": [ + "_euler" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "46p/wNB7td07WS89tf2KfI" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b1BxQNz2Fd1qVeG4pkCjo+" + ] + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 40 + }, + "__editorExtras__": {} + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 39 + }, + "asset": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "__expectedType__": "cc.Prefab" + }, + "fileId": "6eteDuTGJdMpDat7uq2H51", + "instance": { + "__id__": 41 + }, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "d54z2Ijy5P9auHqubsgUaK", + "prefabRootNode": null, + "mountedChildren": [], + "mountedComponents": [ + { + "__id__": 42 + }, + { + "__id__": 45 + } + ], + "propertyOverrides": [ + { + "__id__": 49 + }, + { + "__id__": 51 + }, + { + "__id__": 52 + }, + { + "__id__": 53 + } + ], + "removedComponents": [ + { + "__id__": 54 + }, + { + "__id__": 55 + } + ] + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 43 + }, + "components": [ + { + "__id__": 44 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "71nT0fGWxfJ7Ln9zk1h3p9" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 39 + } + }, + "node": { + "__id__": 39 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "d5zFrWqnhAxakZStdvmRZA" + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 46 + }, + "components": [ + { + "__id__": 47 + }, + { + "__id__": 48 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 39 + } + }, + "node": { + "__id__": 39 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "81d14rOElFe7/iPZEHkQT4" + }, + { + "__type__": "9177brmDQ1BxYgGRYI7jZZd", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 39 + } + }, + "node": { + "__id__": 39 + }, + "_enabled": true, + "__prefab": null, + "baseLayerPose": 1, + "additiveLayerWeight": 0, + "_id": "e2ulA7F8xLfZBW2GTmZecQ" + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 50 + }, + "propertyPath": [ + "_name" + ], + "value": "Crouch" + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 50 + }, + "propertyPath": [ + "_lpos" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 50 + }, + "propertyPath": [ + "_lrot" + ], + "value": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 50 + }, + "propertyPath": [ + "_euler" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "46p/wNB7td07WS89tf2KfI" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b1BxQNz2Fd1qVeG4pkCjo+" + ] + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 57 + }, + "__editorExtras__": {} + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 56 + }, + "asset": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "__expectedType__": "cc.Prefab" + }, + "fileId": "6eteDuTGJdMpDat7uq2H51", + "instance": { + "__id__": 58 + }, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "7daGB70FlJ24/isQrmW3WQ", + "prefabRootNode": null, + "mountedChildren": [], + "mountedComponents": [ + { + "__id__": 59 + }, + { + "__id__": 62 + } + ], + "propertyOverrides": [ + { + "__id__": 66 + }, + { + "__id__": 68 + }, + { + "__id__": 69 + }, + { + "__id__": 70 + } + ], + "removedComponents": [ + { + "__id__": 71 + }, + { + "__id__": 72 + } + ] + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 60 + }, + "components": [ + { + "__id__": 61 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "71nT0fGWxfJ7Ln9zk1h3p9" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 56 + } + }, + "node": { + "__id__": 56 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "d5R8c8TaVHlbC4CkeP6OZ0" + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 63 + }, + "components": [ + { + "__id__": 64 + }, + { + "__id__": 65 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 56 + } + }, + "node": { + "__id__": 56 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "4apPF/cRRBa6W/mcXucxjA" + }, + { + "__type__": "9177brmDQ1BxYgGRYI7jZZd", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 56 + } + }, + "node": { + "__id__": 56 + }, + "_enabled": true, + "__prefab": null, + "baseLayerPose": 1, + "additiveLayerWeight": 1, + "_id": "46f+xniiRKIZ5Rq59QZthK" + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 67 + }, + "propertyPath": [ + "_name" + ], + "value": "Result" + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 67 + }, + "propertyPath": [ + "_lpos" + ], + "value": { + "__type__": "cc.Vec3", + "x": 1.553, + "y": 0, + "z": 0 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 67 + }, + "propertyPath": [ + "_lrot" + ], + "value": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 67 + }, + "propertyPath": [ + "_euler" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "46p/wNB7td07WS89tf2KfI" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b1BxQNz2Fd1qVeG4pkCjo+" + ] + }, + { + "__type__": "cc.Node", + "_name": "Minus", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 74 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": -2.11, + "y": 0.883, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "017nKpSLdEP6NxjvHU06X4" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 73 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 75 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "71XyR0nzdCRKtTw8wMmWIP" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.Node", + "_name": "Add", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 77 + }, + { + "__id__": 80 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.791, + "y": 0.906, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "a5V6Eg9ZZA3Zf/TOqw9lmm" + }, + { + "__type__": "cc.Node", + "_name": "Minus-001", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 76 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 78 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "853FDaObxMdYj1iraJPtSL" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 77 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 79 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "efmgEXaWFKi6lYVacN/BQ+" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.Node", + "_name": "Minus-002", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 76 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 81 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0.7071067811865475, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 90 + }, + "_id": "0d4Kvp7KxKIbgRbYuiSVD9" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 80 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 82 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "14JoQDoc5Af5mrbw3mrlaC" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.Node", + "_name": "Equal", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 84 + }, + { + "__id__": 87 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.768, + "y": 1.014, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "55tRriDhFAhobge/nkmlEq" + }, + { + "__type__": "cc.Node", + "_name": "Minus-001", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 83 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 85 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "cevtUbQ6xI8axpqv2Z40Mc" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 84 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 86 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "9a+ZDOoUNCKrpS9Rc4Mkj8" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.Node", + "_name": "Minus-002", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 83 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 88 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -0.207, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "6dd9ucUANKFIyA8Fk6cljr" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 87 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 89 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "adhfUXA8VFBI93rDl6+Nrs" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.PrefabInfo", + "root": null, + "asset": null, + "fileId": "71e36a35-a552-4298-90b5-3d1064fa30cb", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": [ + { + "__id__": 7 + }, + { + "__id__": 22 + }, + { + "__id__": 39 + }, + { + "__id__": 56 + } + ] + }, + { + "__type__": "cc.SceneGlobals", + "ambient": { + "__id__": 92 + }, + "shadows": { + "__id__": 93 + }, + "_skybox": { + "__id__": 94 + }, + "fog": { + "__id__": 95 + }, + "octree": { + "__id__": 96 + }, + "skin": { + "__id__": 97 + }, + "lightProbeInfo": { + "__id__": 98 + }, + "bakedWithStationaryMainLight": false, + "bakedWithHighpLightmap": false + }, + { + "__type__": "cc.AmbientInfo", + "_skyColorHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyColor": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyIllumHDR": 20000, + "_skyIllum": 20000, + "_groundAlbedoHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_groundAlbedo": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_skyColorLDR": { + "__type__": "cc.Vec4", + "x": 0.452588, + "y": 0.607642, + "z": 0.755699, + "w": 0 + }, + "_skyIllumLDR": 0.8, + "_groundAlbedoLDR": { + "__type__": "cc.Vec4", + "x": 0.618555, + "y": 0.577848, + "z": 0.544564, + "w": 0 + } + }, + { + "__type__": "cc.ShadowsInfo", + "_enabled": false, + "_type": 0, + "_normal": { + "__type__": "cc.Vec3", + "x": 0, + "y": 1, + "z": 0 + }, + "_distance": 0, + "_shadowColor": { + "__type__": "cc.Color", + "r": 76, + "g": 76, + "b": 76, + "a": 255 + }, + "_maxReceived": 4, + "_size": { + "__type__": "cc.Vec2", + "x": 1024, + "y": 1024 + } + }, + { + "__type__": "cc.SkyboxInfo", + "_envLightingType": 0, + "_envmapHDR": { + "__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0", + "__expectedType__": "cc.TextureCube" + }, + "_envmap": { + "__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0", + "__expectedType__": "cc.TextureCube" + }, + "_envmapLDR": { + "__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0", + "__expectedType__": "cc.TextureCube" + }, + "_diffuseMapHDR": null, + "_diffuseMapLDR": null, + "_enabled": true, + "_useHDR": true, + "_editableMaterial": null, + "_reflectionHDR": null, + "_reflectionLDR": null, + "_rotationAngle": 0 + }, + { + "__type__": "cc.FogInfo", + "_type": 0, + "_fogColor": { + "__type__": "cc.Color", + "r": 200, + "g": 200, + "b": 200, + "a": 255 + }, + "_enabled": false, + "_fogDensity": 0.3, + "_fogStart": 0.5, + "_fogEnd": 300, + "_fogAtten": 5, + "_fogTop": 1.5, + "_fogRange": 1.2, + "_accurate": false + }, + { + "__type__": "cc.OctreeInfo", + "_enabled": false, + "_minPos": { + "__type__": "cc.Vec3", + "x": -1024, + "y": -1024, + "z": -1024 + }, + "_maxPos": { + "__type__": "cc.Vec3", + "x": 1024, + "y": 1024, + "z": 1024 + }, + "_depth": 8 + }, + { + "__type__": "cc.SkinInfo", + "_enabled": true, + "_blurRadius": 0.01, + "_sssIntensity": 3 + }, + { + "__type__": "cc.LightProbeInfo", + "_giScale": 1, + "_giSamples": 1024, + "_bounces": 2, + "_reduceRinging": 0, + "_showProbe": true, + "_showWireframe": true, + "_showConvex": false, + "_data": null, + "_lightProbeSphereVolume": 1 + } +] \ No newline at end of file diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene.meta b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene.meta new file mode 100644 index 0000000000..d6504e354d --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.1.45", + "importer": "scene", + "imported": true, + "uuid": "71e36a35-a552-4298-90b5-3d1064fa30cb", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts new file mode 100644 index 0000000000..a24a50dcbc --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts @@ -0,0 +1,27 @@ +import { _decorator, animation, ccenum, Component, Node } from 'cc'; +const { ccclass, property, requireComponent } = _decorator; + +enum BaseLayerPose { + Standing, + Crouch, + Leaning_Original, +} +ccenum(BaseLayerPose); + +@ccclass('AdditivePoseDemonstration') +@requireComponent(animation.AnimationController) +export class AdditivePoseDemonstration extends Component { + @property({ type: BaseLayerPose }) + baseLayerPose = BaseLayerPose.Standing; + + @property({ range: [0.0, 1.0, 0.01], slide: true }) + additiveLayerWeight = 0.0; + + update(deltaTime: number) { + const controller = this.node.getComponent(animation.AnimationController); + controller.setValue('BaseLayerPose', this.baseLayerPose); + controller.setLayerWeight(1, this.additiveLayerWeight); + } +} + + diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts.meta b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts.meta new file mode 100644 index 0000000000..6df311a021 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.23", + "importer": "typescript", + "imported": true, + "uuid": "9177bae6-0d0d-41c5-8806-45823b8d965d", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/package.json b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/package.json new file mode 100644 index 0000000000..c6a30e5e2c --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/package.json @@ -0,0 +1,7 @@ +{ + "name": "additive-pose-demonstration", + "uuid": "578bc671-b84c-4255-b3d5-49e0ac13ad5e", + "creator": { + "version": "3.8.0" + } +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/builder.json b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/builder.json new file mode 100644 index 0000000000..b2b2e5b558 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/builder.json @@ -0,0 +1,3 @@ +{ + "__version__": "1.3.5" +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/cocos-service.json b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/cocos-service.json new file mode 100644 index 0000000000..5ba4c7387f --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/cocos-service.json @@ -0,0 +1,23 @@ +{ + "__version__": "3.0.7", + "game": { + "name": "未知游戏", + "app_id": "UNKNOW", + "c_id": "0" + }, + "appConfigMaps": [ + { + "app_id": "UNKNOW", + "config_id": "b34724" + } + ], + "configs": [ + { + "app_id": "UNKNOW", + "config_id": "b34724", + "config_name": "Default", + "config_remarks": "", + "services": [] + } + ] +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/device.json b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/device.json new file mode 100644 index 0000000000..70e599e6c9 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/device.json @@ -0,0 +1,3 @@ +{ + "__version__": "1.0.1" +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/engine.json b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/engine.json new file mode 100644 index 0000000000..9f2f363f4b --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/engine.json @@ -0,0 +1,3 @@ +{ + "__version__": "1.0.7" +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/information.json b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/information.json new file mode 100644 index 0000000000..9a4b0a4f55 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/information.json @@ -0,0 +1,23 @@ +{ + "__version__": "1.0.0", + "information": { + "customSplash": { + "id": "customSplash", + "label": "customSplash", + "enable": true, + "customSplash": { + "complete": false, + "form": "https://creator-api.cocos.com/api/form/show?sid=e6da5f66f96264c0a291dafa04093ac7" + } + }, + "removeSplash": { + "id": "removeSplash", + "label": "removeSplash", + "enable": true, + "removeSplash": { + "complete": false, + "form": "https://creator-api.cocos.com/api/form/show?sid=e6da5f66f96264c0a291dafa04093ac7" + } + } + } +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/program.json b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/program.json new file mode 100644 index 0000000000..356db6b721 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/program.json @@ -0,0 +1,3 @@ +{ + "__version__": "1.0.3" +} diff --git a/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/tsconfig.json b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/tsconfig.json new file mode 100644 index 0000000000..7dc649a956 --- /dev/null +++ b/versions/4.0/en/animation/marionette/additive-animation/source/additive-pose-demonstration/tsconfig.json @@ -0,0 +1,9 @@ +{ + /* Base configuration. Do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + /* Add your custom configuration here. */ + "compilerOptions": { + "strict": false + } +} diff --git a/versions/4.0/en/animation/marionette/animation-controller.md b/versions/4.0/en/animation/marionette/animation-controller.md new file mode 100644 index 0000000000..284044d309 --- /dev/null +++ b/versions/4.0/en/animation/marionette/animation-controller.md @@ -0,0 +1,71 @@ +# Animation Controller + +The animation controller component is used to apply the [Animation Graph Assets](animation-graph.md) to the object. + +Select the node in **Hierarchy** panel where you need to apply the animation graph, and then click **Add Component -> Animation -> Animation Controller** below in **Inspector** panel to add an animation controller component to the node. + +![add-animation-controller](animation-controller/add-animation-controller.png) + +> **Note**: Because the Marionette animation system and the old animation system cannot be used together, **animation controller components also cannot be mounted on the same node as animation components or skeletal animation components**. + +## Properties + +![animation-controller](animation-graph/animation-controller.png) + +| Properties | Description | +| :---- | :----------- | +| Graph | Added [animation-graph resource](animation-graph.md), default is empty | + +## Programmatically Controlling Animation Graphs + +The animation controller component provides a number of methods for controlling the [Animation State Machine](animation-graph-basics.md) of the animation graph at runtime. + +| Method | Description | +| :----------------------- | :----------------------- | +| `getVariables` | Get all variables and their names/types/values, etc. || +| `setValue` | Set the value of the variable. | `getValue +| `getValue` | Get the value of the variable. | `getValue +| `getCurrentStateStatus` | Get the current state information. | `getCurrentStateStatus` +| `getCurrentClipStatuses` | Get the current state of the clip. | `getCurrentClipStatuses` +| `getCurrentTransition` | Get the current state transition. | `getCurrentClipStatuses` +| `getNextStateStatus` | Get information about the next state. | `getNextStateStatus +| `getNextClipStatuses` | Get the status of the next animation clip. | `setLayerWeight +| `setLayerWeight` | Set the weight of a certain [animation-graph layer](animation-graph-layer.md). | | + +- **Variable** Control + + For example, we have a variable in the [Animation Graph Panel](./animation-graph-panel.md#Variables) added several variables, the code can get and modify the value of the variables, the code example is as follows. + + ```ts + // Get the animation controller component + let animationController:animation.AnimationController = this.node.getComponent(animation.AnimationController) + + // Get all variables + let variables= animationController.getVariables(); + + // Get the value of the variable named 'vertical' + let vertical: Number = animationController.getValue("vertical"); + + // Change the value of the variable named 'vertical' to 1.0, which will calculate the conditional transition on the next update frame + animationController.setValue("vertical", 1.0) + ``` + + > **Note**: Variables for the animation state machine can currently only be added and removed in the [Animation Graph Panel](animation-graph-panel.md#Variables). + +- Get the current **state** + + Get the normalized progress of the current **state** of the state machine at layer 0, with the following code example. + + ```ts + let states: animation.MotionStateStatus = animationController.getCurrentStateStatus(0) + console.log(states.progress); + ``` + +- Get the current **transition** + + Gets information about the upcoming **transition** of the state machine at layer 0. + + ```ts + let transition: animation.TransitionStatus = animationController.getCurrentTransition(0) + console.log(transition.duration, transition.time) + ``` diff --git a/versions/4.0/en/animation/marionette/animation-controller/add-animation-controller.png b/versions/4.0/en/animation/marionette/animation-controller/add-animation-controller.png new file mode 100644 index 0000000000..289921eb51 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-controller/add-animation-controller.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics.md b/versions/4.0/en/animation/marionette/animation-graph-basics.md new file mode 100644 index 0000000000..11cb02399a --- /dev/null +++ b/versions/4.0/en/animation/marionette/animation-graph-basics.md @@ -0,0 +1,223 @@ +# Animation State Machine + +A state refers to an action performed by an animated actor in the form of animations being triggered and played, such as being idle, moving, running, attacking, etc. + +An actor may have multiple states and switches between them as different actions are performed. The act of switching from one state to another is called a [transition](state-transition.md). The condition that predetermines the transition is called a **transition condition**. + +A state machines manages all the states and transitions of an actor. Typically, the logical connections of states and transitions are depicted as a flow chart, which is editable in the [Animation Graph panel](animation-graph-panel.md). Currently, one animation graph corresponds to one state machine. When the state machine is at a certain state, animation corresponding that state will be played. Depending on the intricacy of the animation graph, the actor can perform a myriad of complex actions by switching between simple skeletal animations. + +## State Basics + +Besides states that contains animations, users can also create 4 special states in the animation graph including **Entry**, **Exit**, **Any** and **Sub State Machine**. As these states do not contain any animation, they will be referred as **pseudo states** from this point on. + +A [sub state machine](#sub-state-machine) serves as a container for a state machine nested within another state machine, primarily used to break down complex state machine designs into controllable chunks. By convention, the state machine with the highest hierarchy is referred as the **Top Level State Machine**. It is comparable to the root node in the scene. + +**Entry**, **Exit** and **Any** are mandatory pseudo states that cannot be deleted from the graph. They are accessible in the Animation Graph panel by default. Please take note that the **Exit** state is only available in a **sub state machine**. + +![animation-graph-panel](animation-graph-panel/animation-graph-panel.png) + +- **Entry**: Represents the entry point of the current state machine. It can only be used as the beginning state of a transition and never the end state. + +- **Exit**: Represents the end point of the current state machine. When the state machine reaches the end point, it is considered completed and will shut down. **Exit** can only be used as the end state of a transition and never the beginning state. + + As the current sub state machine is completed, it will be shut down and transitioned to the next nearest state in its parent state machine. The actor will cease all animation if the top level state machine is shut down, which defeats the purpose of having the animation system in the first place. For this reason, **Exit** is only available in sub state machines. + + To manually cease all animation for the actor, users may disable the **Animation Controller** component of the actor. + +- **Any**: Represents any state in the graph that is not a pseudo state. It can only be used as the beginning state of a transition and never the end state. + + A transition beginning from **Any** is equivalent to a transition from all available states currently in the graph. In other words, no matter which state it is currently at, it will be transitioned to the end state as long as the transition conditions are met. For instance, a character can switch to the wounding animation from any other animation, be it walking, running, jumping, etc. + + > **Note**: **Any** also includes any state from the sub state machines of the current state machine. Transitions from the sub state machines will be triggered if the transition conditions are met. + +## Creating states + +A state machine is the collection of all states, pseudo states and the transitions that connect them together under specific conditions. + +Right click on the grid in the [Animation Graph panel](animation-graph-panel.md) and select one of the following to add to the graph: +- **Add State – Animation Clip**: Add an animation clip asset as a state, named `Clip Motion` by default. +- **Add State – Blend 1D / 2D**: Add a new animation by blending multiple animations as a new state with a default name of `Blend 1D` / `Blend 2D`. Please see below in section **Animation Blending** for more details. +- **Add Sub State Machine**: Add a new sub state machine named `State Machine` by default, Please see below in section **Sub State Machine** for more details. + +All states except sub state machines are represented as blue rectangles in the graph. + +![Edit](animation-graph-basics/edit.png) + +### Setting state properties + +Select a state in the graph and the blue rectangle will be highlighted with yellow edges. Properties of the selected state will also be available in the **Inspector** panel to the left of the Animation Graph panel. + +![state-property](animation-graph-basics/state-property.png) + +- Name of the selected state is displayed on the top of the **Inspector** panel. Clicking the gear icon will yield options for the selected state to be renamed, deleted, or replaced by a state of a different type. + +- **Speed**: Playback speed for the animation attached to the selected state with a default value of 1. Lower value will result in slower animation. +- **Speed Multiplier**: When enabled, a multiplication factor can be specified for the speed. The speed multiplier factor will base speed multiplication to control the speed of the animation state. + + ![speed-multiplier](animation-graph-basics/speed-multiplier.png) + +- **Animation Clip Motion**: Specify the animation clip attached to the selected state. Users can drag and drop the animation clip asset from the Assets panel to the **Clip** property or select from its drop-down menu. Once specified, the animation clip will be attached to the selected state, and the selected state will be automatically renamed according to the attached animation clip. + + > **Note**: If the animation clip is left unspecified, it could cause errors to the state machine. + + ![add-motion](animation-graph-basics/add-motion.png) + +- **State Component**: Attach an animation graph script to the selected state. Users may call from the StateMachineComponent to insert events at certain junctions of the flow chart. For more information, please see below in section **Animation State Component** for more details. + +- Clicking the gear icon will yield options for the selected state to be renamed, deleted, or replaced by a state of a different type. Right click on the state note in the graph will also yield the same options. + + ![change-state](animation-graph-basics/change-state.png) + +## Animation Blending + +Blend Motion creates new animations by blending existing ones with pre-determined parameters, commonly used to create mixed animations such as jumping while running. To achieve optimal results, it is not advisable to blend animations that are excessively different from one another. Based on the parameters used, Blend Motion can be categorized as follows: + +- **Blend 1D**: Use **one parameter** to **linearly interpolate** between multiple animations. + +- **Blend 2D**: Use **two parameters** to **barycentrically interpolate** between multiple animations. + +Blend Motion should be distinguished from [State Transitions](state-transition.md) as: + +- Transitions create a brief intersection period for one animation to morph into a second animation. + +- Blend Motion combines multiple animations with varying parameters, yielding one combined animation. + +### Adding animation blending + +Right click on the grid in the Animation Graph panel and select **Add State – Blend 1D** or **Add State – Blend 2D** to create new states. Their respective properties can be accessed in the **Inspector** panel on the left. + +![blend edit](animation-graph-basics/blend-edit.png) + +Users can access the parameters for blending in either of the following ways: + +- Select the Blend Motion state and click the **Edit** button in the Inspector panel +- Double click on the Blend Motion state node as seen in the animation graph +- Right click on the Blend Motion state node and select **Edit** + +To exit the Blend Motion state, users may: + +- Right click on the grid and select **Return to Parent** +- Select a higher hierarchy in the **Position** bar on top of the grid window + +![create blend](animation-graph-basics/create-blend.gif) + +### 1D Animation Blending + +Blend 1D **linearly interpolates** multiple animation with **one parameter**, with each target animation assigned with a threshold value. + +- When the parameter’s value equates to the threshold value of a target animation, said animation will be the output of the Blend 1D state. +- When the parameter’s value is between the threshold value of 2 target animations, Blend 1D will output the linear interpolation between the two. The formula is `A * (1 - t) + B * t` where A and B are the threshold value of target animations, and t the percentage value of the parameter value in proportion to the range between the two thresholds. + +Double click on the Blend Motion state node to enter its sub graph. In it, users may import animation clips by creating a new Clip Motion state, or create additional Blend Motion states. + +![add-motion-of-blend](animation-graph-basics/add-motion-of-blend1D.png) + +- **Parameter**: Select the parameter to be used to blend between states. A parameter needs to be created under the **Variables** tab first. Blend Motion states require a float type parameter as the input variable. Users may also select from pre-existing variables to be used as the parameter at the Blend Motion state nodes in the graph. For more information, please see [Animation Graph Panel - Variables](animation-graph-panel.md) for details. + +- **Threshold**: With every animation clip imported, a new corresponding threshold variable will be added. Users may manually define the value, or enable **Automata Threshold** to allow Cocos Creator to assign threshold values automatically. Threshold variables will be listed in an ascending order. + +- Animation clips can be assigned from the drop-down menu from the state nodes in the graph, or from the **Inspector** panel while a Clip Motion state node is selected. + + ![choose clip](animation-graph-basics/choose-clip.png) + + To remove an animation, select the Clip Motion state, go to the Inspector panel and click the gear icon, then select **Remove this Motion**. + +As an example, a state machine that plays different animations when the character is at different movement speeds should look like the screenshot below: + +![blend-speed](animation-graph-basics/blend1-speed.png) + +### 2D Animation Blending + +Blend 2D **barycentrically interpolates** multiple animation with **two parameters**, with each target animation assigned with a threshold value. + +Users may set up a Blend 2D state in the same way as setting up a Blend 1D state, with a few exceptions: + +- Blend 2D requires 2 parameters (X, Y) to blend animations. +- Clip Motion states have threshold value in 2 dimensions (`[x, y]`). Threshold variables are listed by the order of their creation. There is no option for **Automata Threshold**. +- Parameter X corresponds to the x dimension of threshold variables while parameter Y the y dimension. + +Blend 2D is commonly used to create state machine for character motions, such as: + +![animation-blend 2D](animation-graph-basics/blend2.png) + +In the example above, the range for parameter X is `[-1, 1]`, and the range for parameter Y is `[-1. 1]`. + +## Sub State Machine + +Users can create sub state machines to be used recursively anywhere in the animation graph. By convention, the state machine with the highest hierarchy is referred as the **Top Level State Machine**. It is comparable to the root node in the scene. + +Right click on the grid and select **Add Sub State Machine** to create a new sub state machine with the default name of `State Machine`. + +![set sub state](animation-graph-basics/set-sub-state.png) + +- Button ① is for entering the sub graph of the sub state machine to edit its content. It can also be done by double clicking the sub state machine node in the graph or right click on the node and select **Edit**. + +- Button 2 is for adding state components. Please see below in section **Animation State Components** for more details. + +- Click the gear icon to rename, remove, or add state components for the selected sub state machine. + +### Editing Sub State Machine + +The sub graph of a sub state machine is identical to a typical graph except for the **Exit** pseudo state. As the current sub state machine is completed, it will be shut down and transitioned to the next nearest state in its parent state machine. + +![edit sub state](animation-graph-basics/edit-sub-state.png) + +To exit the Blend Motion state, users may right click on the grid and select **Return to Parent** or select a higher hierarchy in the **Position** bar on top of the grid window. + + + +## Animation State Component + +States and sub state machines can be attached with state components, which can be created in the **Assets** panel by clicking the + button on the top and select **Animation Graph Script**. + +![create-graph-script](animation-graph-basics/create-graph-script.png) + +A new Animation Graph Script component will be named `AnimationGraphComponent` by default which is a .ts script. + +![animation-graph-component](animation-graph-basics/animation-graph-component.png) + +The Animation Graph Script can be attached to a state node in the animation graph by clicking the Add State Component button in the Inspector panel, or click the gear icon on the top and select Add State Component. A state node can be attached with multiple State Components. + +Click the gear icon on the right of the added state component and select Remove State Component to remove it. + +Animation Graph Script assets can call from the `animation.StateMachineComponent` class which includes methods that registers events of the state machine, which includes: + +| Method Name | Description | +| :--- | :--- | +| `onMotionStateEnter` | Triggered when entering a state | +| `onMotionStateExit` | Triggered when exiting a state | +| `onMotionStateUpdate` | Triggered when a state is updated | +| `onStateMachineEnter` | Triggered when entering a sub state machine | +| `onStateMachineExit` | Triggered when exiting a sub state machine | + +Methods listed above can also use the following parameters: + +- `controller: animation.AnimationController`: The Animation Controller component which the state machine is attached to. This parameter is appliable to all the methods above. + +- `motionStateStatus: animation.MotionStateStatus`: Status of the current state. This parameter is only applicable to methods intended for states (`onMotionStateEnter`, `onMotionStateExit`.) + +![add-graph-script](animation-graph-basics/add-graph-script.png) + +### Example: Enable particles when entering a state + +```ts +import { _decorator, animation, PhysicsSystem, ParticleSystem } from "cc"; +const { ccclass, property } = _decorator; + +@ccclass("AnimationGraphComponent") +export class AnimationGraphComponent extends animation.StateMachineComponent { + /** + * Called when a motion state right after it entered. + * @param controller The animation controller it within. + * @param stateStatus The status of the motion. + */ + public onMotionStateEnter (controller: animation.AnimationController, stateStatus: Readonly): void { + // Enable all particles attached to the node where the animation controller is also located + for (const particleSystem of controller.node.getComponents(ParticleSystem)) { + particleSystem.play(); + } + } +} +``` diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/add-graph-script.png b/versions/4.0/en/animation/marionette/animation-graph-basics/add-graph-script.png new file mode 100644 index 0000000000..64e57ef77e Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/add-graph-script.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/add-motion-of-blend1D.png b/versions/4.0/en/animation/marionette/animation-graph-basics/add-motion-of-blend1D.png new file mode 100644 index 0000000000..d95b22c1c2 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/add-motion-of-blend1D.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/add-motion.png b/versions/4.0/en/animation/marionette/animation-graph-basics/add-motion.png new file mode 100644 index 0000000000..aea7919dd5 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/add-motion.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/animation-graph-component.png b/versions/4.0/en/animation/marionette/animation-graph-basics/animation-graph-component.png new file mode 100644 index 0000000000..1b5cd4cfcf Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/animation-graph-component.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/blend-edit-panel.png b/versions/4.0/en/animation/marionette/animation-graph-basics/blend-edit-panel.png new file mode 100644 index 0000000000..b3a0cf1c10 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/blend-edit-panel.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/blend-edit.png b/versions/4.0/en/animation/marionette/animation-graph-basics/blend-edit.png new file mode 100644 index 0000000000..98a5cb3dab Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/blend-edit.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/blend-property.png b/versions/4.0/en/animation/marionette/animation-graph-basics/blend-property.png new file mode 100644 index 0000000000..dd43f4bf9b Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/blend-property.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/blend1-speed.png b/versions/4.0/en/animation/marionette/animation-graph-basics/blend1-speed.png new file mode 100644 index 0000000000..ed9f0060ec Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/blend1-speed.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/blend2.png b/versions/4.0/en/animation/marionette/animation-graph-basics/blend2.png new file mode 100644 index 0000000000..103bf1a557 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/blend2.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/change-state.png b/versions/4.0/en/animation/marionette/animation-graph-basics/change-state.png new file mode 100644 index 0000000000..c63203fc07 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/change-state.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/choose-clip.png b/versions/4.0/en/animation/marionette/animation-graph-basics/choose-clip.png new file mode 100644 index 0000000000..e51a0f12f6 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/choose-clip.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/clip-menu.png b/versions/4.0/en/animation/marionette/animation-graph-basics/clip-menu.png new file mode 100644 index 0000000000..037b6d0e46 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/clip-menu.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/create-blend.gif b/versions/4.0/en/animation/marionette/animation-graph-basics/create-blend.gif new file mode 100644 index 0000000000..596cd7ca0b Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/create-blend.gif differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/create-graph-script.png b/versions/4.0/en/animation/marionette/animation-graph-basics/create-graph-script.png new file mode 100644 index 0000000000..ee42c28c16 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/create-graph-script.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/edit-sub-state.png b/versions/4.0/en/animation/marionette/animation-graph-basics/edit-sub-state.png new file mode 100644 index 0000000000..07141a1e47 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/edit-sub-state.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/edit.png b/versions/4.0/en/animation/marionette/animation-graph-basics/edit.png new file mode 100644 index 0000000000..6caa4d878a Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/edit.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/example.png b/versions/4.0/en/animation/marionette/animation-graph-basics/example.png new file mode 100644 index 0000000000..584eee47ea Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/example.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/preview-blend1.png b/versions/4.0/en/animation/marionette/animation-graph-basics/preview-blend1.png new file mode 100644 index 0000000000..a7efb4d208 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/preview-blend1.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/preview-blend2.png b/versions/4.0/en/animation/marionette/animation-graph-basics/preview-blend2.png new file mode 100644 index 0000000000..66037279fa Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/preview-blend2.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/preview-clip.png b/versions/4.0/en/animation/marionette/animation-graph-basics/preview-clip.png new file mode 100644 index 0000000000..05fd2e8adc Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/preview-clip.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/remove-graph-script.png b/versions/4.0/en/animation/marionette/animation-graph-basics/remove-graph-script.png new file mode 100644 index 0000000000..894c222930 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/remove-graph-script.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/set-sub-state.png b/versions/4.0/en/animation/marionette/animation-graph-basics/set-sub-state.png new file mode 100644 index 0000000000..4f767b4723 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/set-sub-state.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/speed-multiplier.png b/versions/4.0/en/animation/marionette/animation-graph-basics/speed-multiplier.png new file mode 100644 index 0000000000..af77ddcfee Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/speed-multiplier.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/state-property.png b/versions/4.0/en/animation/marionette/animation-graph-basics/state-property.png new file mode 100644 index 0000000000..359f57d8bb Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/state-property.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-basics/sub-state.png b/versions/4.0/en/animation/marionette/animation-graph-basics/sub-state.png new file mode 100644 index 0000000000..51f4862185 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-basics/sub-state.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-layer.md b/versions/4.0/en/animation/marionette/animation-graph-layer.md new file mode 100644 index 0000000000..5f5abf1a8e --- /dev/null +++ b/versions/4.0/en/animation/marionette/animation-graph-layer.md @@ -0,0 +1,93 @@ +# Animation Graph Layer + +The animation graph can have multiple layers, with layers running simultaneously between them, and the animation effects played on them can be mixed according to the configuration of the layers. + +## Create layers + +![add-layer](animation-graph-panel/add-layer.png) + +Click the **Layers** button in the animation graph, click the ![add layer](animation-graph-panel/btn-add-layer.png) button, and enter the name of the layer in the popup input box to create the layer. + +## Delete Layers + +After hovering over the layers, you can click **×** on the right side of the layers to delete them, this operation only takes effect when there are multiple layers. + +![remove](animation-graph-panel/remove-layer.png) + +## Rename the layers + +Rename a layer by double-clicking the left mouse button at the layer name. + +![rename](animation-graph-panel/rename-layer.png) + +## Layer Properties + +After selecting any layer in the animation graph, you can view the layer properties in the **Inspector** panel. + +![inspector](animation-graph-panel/layer-inspector.png) + +| Properties | Description | +| :-- | :-- | +| **Mask** | The animation mask to specify the corresponding [Animation Mask](animation-mask.md) | +| **Weight** | The weight of the layer to be used when mixing with **previous layers**, in the range [0, 1] | + +### Animation Masks + +A mask can be assigned to a layer by dragging an animation mask from the **Assets** panel to the **Mask** property of the Animation Graph window or by checking the **Mask** property in the layer. + +Refer to the following figure and follow steps 1, 2 and 3. + +- Select any layer +- Click on the drop-down menu in the **Inspector** panel. +- Select a pre-created **Mask** resource or drag it from the **Assets** panel. + +![select](animation-mask/select.png) + +When specified, all animation effects on that layer are affected by that mask. + +For example, you can specify an animation mask to a layer that keeps only the upper body skeleton, with all lower body skeletons disabled, and the animation effect on that layer will only work on the upper body. + +### Layer weights + +The animation effect of each layer will be blended with the animation effect of the previous layer in a certain proportion, the proportion of the blending is specified by the weight property of the layer. + +0 means that the animation of the previous level is used completely, 1 means that the level completely overwrites the animation of the previous layer, when it is at [0,1] then the blending is done properly. + +Blending of layers will only blend those bones that are not disabled by the animation mask. For example, if Layer 1 has a skeleton enabled, but a subsequent Layer 2 has disabled it with an animation mask, then +The skeleton will only play the animation of Layer 1 in full, regardless of the weight specified for Layer 2. + +You can also dynamically modify the layer weights in code via `AnimationController.prototype.setLayerWeight`. + +## Grid layout area manipulation + +Right-clicking on a blank space within the grid opens the layer menu. + +![Edit](animation-graph-panel/edit.png) + +You can add **states, sub-state machines, blends and empty states** to the menu. + +- See [Animation State Machine](animation-graph-basics.md) for how to use **sub-states, sub-state machines** and **blends**. + +- **Add Empty State**: This menu creates a default empty state named Empty on the hierarchy, please refer to the **Empty State** section below for more information. + + ![create empty](animation-layer/create-empty.png) + +- **Return to center view**: This menu returns the view of the layer to the center of the layer. + +- Hold down the right or middle mouse button to move the layer grid; use the scroll wheel to zoom in and out of the layer. + +## Empty state + +In many cases, a requirement is to enable a layer only in certain situations; in all other cases make this layer inactive. + +For this requirement, **Empty State** can be used. The **Empty State** can be considered as an animation whose animation effect is the animation effect of the previous level. + +The transition from the **Empty State** state is equivalent to the gradual transition from the animation effect of the previous level to the target animation. Transition conditions and transition period can be specified on this transition. However, the transition period can only be specified as an absolute duration (in seconds). + +![layer](animation-layer/empty-state.png) + +The transition leading to the **Empty** state is equivalent to the source animation's animation decaying until the transition is complete, which is equivalent to the current level being disabled. + +As in the example below, when **Empty** transitions to the **RunShoot** state only after the trigger **Trigger** is triggered, and in the **Empty** state, the layer does not affect the animation effects of the preceding layers. + +![duration](animation-layer/transit-empty-state.png) diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel.md b/versions/4.0/en/animation/marionette/animation-graph-panel.md new file mode 100644 index 0000000000..63c72d5fb4 --- /dev/null +++ b/versions/4.0/en/animation/marionette/animation-graph-panel.md @@ -0,0 +1,80 @@ +# Animation Graph Panel + +The Animation Graph panel is used to view and edit animation graph assets. Once you have prepared the skeletal animations required for an object, you can assemble and combine them into a complete animation flow in the **Animation Graph Panel**. + +## Opening the Panel + +Select the animation graph asset in the **Assets** panel, then click **Edit** in the **Inspector** panel: + +![open-animation-graph-panel](animation-graph-panel/open-animation-graph-panel.png) + +This opens the Animation Graph panel, which consists of **Layers**, **Variables**, **Inspector** tab on the left, and the grid layout area on the right: + +![animation-graph-panel](animation-graph-panel/animation-graph-panel.png) + +When there are multiple animation graph assets, click the **Switch Animation Graphs** button in the upper right corner of the panel to switch other animation graphs that need to be edited. + +## Layers + +This tab is mainly used to create, view and edit layers of animation graphs, each layer is controlled by a state machine respectively, but currently only one animation graph layer is supported. The animation layer can be renamed by clicking on the gear icon button to the right of the layer. + +![Layers](animation-graph-panel/layers.png) + +For more informations, please refer to [Animation Graph Layer](animation-graph-layer.md)。 + +## Grid Layout Area + +This area mainly shows the state machine corresponding to the layer selected in the **Layers** tab, where the user can visually create, edit and arrange the states in the state machine, as well as set the transition between states. Right-click in the area to create as many states as needed: + +![Edit](animation-graph-panel/edit.png) + +When finished with editing, click **Save** or **Reset** (reset to the last save) in the upper right corner as needed. Please refer to the [Animation State Machine](animation-graph-basics.md) documentation for more operations. + +## Variables + +This tab is mainly used to create, view and edit the variables of the state machine in the current animation graph layer. The user can manipulate the flow of the animation graph expression in the animation graph by means of custom **variables**, which serve the following purposes: + +- as **transition conditions** for transitions between states + +- as an **input parameter** for mixing animation states to mix different animation clips + +![variables](animation-graph-panel/variables.png) + +### Creating variables + +In the following figure, for example, the steps are: +- Fill in the variable name in the **Name** input box +- Select the variable type in the **Type** drop-down box. The supported variable types include **Float**, **Boolean**, **Trigger** and **Integer**, please refer to **Variable Types** section below for details. +- Finally, click the **Add** button to create the variables, and the created variables will be displayed in order at the bottom of the tab. + +![add variables](animation-graph-panel/add-variables.png) + +Each created variable will have a gear icon button on the right side, click it to delete the corresponding variable. Please don't forget to click the **Save** button in the top right corner of the Animation Graph panel to save the settings when done. + +### Variable Types + +The currently supported variable types to create include: + +- **Float**: the value of the variable can be any real number + +- **Boolean**: the value of the variable can be true/false + +- **Integer**: the value of the variable can be any integer + +- **Trigger**: the variable is a trigger, if checked, the value is a trigger. Trigger is used for one-time condition judgment, when the trigger condition is satisfied, the state transition occurs. After the transition is completed, all the triggers referenced will be reset to **untriggered state**. + + > **Note**: the trigger will be reset only when it transitions to a certain state. If it transitions to a "pseudo-state", the trigger will not be reset. + + For example, in the following figure, if the trigger is set to T1 or T2 in ① once, state A will reach state B or C in the sub-state machine without setting the triggers in ② and ③ again: + + ![variable](animation-graph-panel/variable-eg.png) + + + +## Inspector + +This tab is mainly used to set the properties and transition conditions of the various states in the state machine, please refer to the [Animation State Machine](animation-graph-basics.md) documentation for details. + +![Inspector](animation-graph-panel/inspector.png) diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/add-layer.png b/versions/4.0/en/animation/marionette/animation-graph-panel/add-layer.png new file mode 100644 index 0000000000..23d9cc20b8 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/add-layer.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/add-variables.png b/versions/4.0/en/animation/marionette/animation-graph-panel/add-variables.png new file mode 100644 index 0000000000..0c6a966031 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/add-variables.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/animation-graph-panel.png b/versions/4.0/en/animation/marionette/animation-graph-panel/animation-graph-panel.png new file mode 100644 index 0000000000..961b5e4542 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/animation-graph-panel.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/btn-add-layer.png b/versions/4.0/en/animation/marionette/animation-graph-panel/btn-add-layer.png new file mode 100644 index 0000000000..01e6b73f53 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/btn-add-layer.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/delete-variable.png b/versions/4.0/en/animation/marionette/animation-graph-panel/delete-variable.png new file mode 100644 index 0000000000..90d2f03485 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/delete-variable.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/edit.png b/versions/4.0/en/animation/marionette/animation-graph-panel/edit.png new file mode 100644 index 0000000000..99fa0d4387 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/edit.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/inspector.png b/versions/4.0/en/animation/marionette/animation-graph-panel/inspector.png new file mode 100644 index 0000000000..56f49212b1 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/inspector.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/layer-inspector.png b/versions/4.0/en/animation/marionette/animation-graph-panel/layer-inspector.png new file mode 100644 index 0000000000..bb2a152b09 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/layer-inspector.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/layers.png b/versions/4.0/en/animation/marionette/animation-graph-panel/layers.png new file mode 100644 index 0000000000..1f8c16d5c8 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/layers.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/open-animation-graph-panel.png b/versions/4.0/en/animation/marionette/animation-graph-panel/open-animation-graph-panel.png new file mode 100644 index 0000000000..3abcc30d4a Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/open-animation-graph-panel.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/remove-layer.png b/versions/4.0/en/animation/marionette/animation-graph-panel/remove-layer.png new file mode 100644 index 0000000000..7eabda741a Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/remove-layer.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/rename-layer.png b/versions/4.0/en/animation/marionette/animation-graph-panel/rename-layer.png new file mode 100644 index 0000000000..8204adc8f8 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/rename-layer.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/reset-on-nextframe.png b/versions/4.0/en/animation/marionette/animation-graph-panel/reset-on-nextframe.png new file mode 100644 index 0000000000..76925ac4c8 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/reset-on-nextframe.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/variable-eg.png b/versions/4.0/en/animation/marionette/animation-graph-panel/variable-eg.png new file mode 100644 index 0000000000..25f39f35f3 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/variable-eg.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph-panel/variables.png b/versions/4.0/en/animation/marionette/animation-graph-panel/variables.png new file mode 100644 index 0000000000..0e3c14b715 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph-panel/variables.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph.md b/versions/4.0/en/animation/marionette/animation-graph.md new file mode 100644 index 0000000000..1ac2b13566 --- /dev/null +++ b/versions/4.0/en/animation/marionette/animation-graph.md @@ -0,0 +1,35 @@ +# Animation Graph Assets + +The animation graph asset is used to store the entire animation flow data of the object. The flow of the animation is described by a state machine, currently an animation graph only supports one state machine. + +## Creating + +In the **Assets** panel click the **+** button on the top left, then select **Animation Graph**. + +![create-animation-graph](animation-graph/create-animation-graph.png) + +This creates an animation graph asset named `Animation Graph` by default. + +![animation-graph-asset](animation-graph/animation-graph-asset.png) + +## Editing + +Once the animation graph is created, it can be edited it in the Animation Graph panel, please refer to the [Animation Graph Panel](animation-graph-panel.md) documentation for details. + +## Applying + +Animation graph assets need to depend on the Animation Controller component to be applied to the object. + +Click **Add Component -> Animation -> Animation Controller** at the bottom of the **Inspector** panel to add the Animation Controller component to the node. + +![add-animation-controller](animation-graph/add-animation-controller.png) + +> **Note**: since the Marionette animation system and the old animation system cannot be used together, the animation controller component cannot be attached on the same node as the animation component or the skeletal animation component either. + +Then click the arrow icon button behind the **Graph** property box of the animation controller component to select the animation graph asset. + +![animation-graph-select](animation-graph/animation-graph-select.png) + +Or just drag and drop the animation graph asset from the **Assets** panel into the **Graph** property box of the Animation Controller component: + +![animation-controller](animation-graph/animation-controller.png) diff --git a/versions/4.0/en/animation/marionette/animation-graph/add-animation-controller.png b/versions/4.0/en/animation/marionette/animation-graph/add-animation-controller.png new file mode 100644 index 0000000000..7dc532411d Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph/add-animation-controller.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph/animation-controller.png b/versions/4.0/en/animation/marionette/animation-graph/animation-controller.png new file mode 100644 index 0000000000..50a05c5abb Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph/animation-controller.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph/animation-graph-asset.png b/versions/4.0/en/animation/marionette/animation-graph/animation-graph-asset.png new file mode 100644 index 0000000000..9c902f373f Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph/animation-graph-asset.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph/animation-graph-select.png b/versions/4.0/en/animation/marionette/animation-graph/animation-graph-select.png new file mode 100644 index 0000000000..2a15927c1c Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph/animation-graph-select.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph/create-animation-graph.png b/versions/4.0/en/animation/marionette/animation-graph/create-animation-graph.png new file mode 100644 index 0000000000..e20b078933 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph/create-animation-graph.png differ diff --git a/versions/4.0/en/animation/marionette/animation-graph/create-empty-state.png b/versions/4.0/en/animation/marionette/animation-graph/create-empty-state.png new file mode 100644 index 0000000000..a9b29aaaa2 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-graph/create-empty-state.png differ diff --git a/versions/4.0/en/animation/marionette/animation-layer/create-empty.png b/versions/4.0/en/animation/marionette/animation-layer/create-empty.png new file mode 100644 index 0000000000..48d6c42ede Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-layer/create-empty.png differ diff --git a/versions/4.0/en/animation/marionette/animation-layer/empty-state.png b/versions/4.0/en/animation/marionette/animation-layer/empty-state.png new file mode 100644 index 0000000000..42a8561bc3 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-layer/empty-state.png differ diff --git a/versions/4.0/en/animation/marionette/animation-layer/transit-empty-state.png b/versions/4.0/en/animation/marionette/animation-layer/transit-empty-state.png new file mode 100644 index 0000000000..ba999f76fc Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-layer/transit-empty-state.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask.md b/versions/4.0/en/animation/marionette/animation-mask.md new file mode 100644 index 0000000000..d5d0393548 --- /dev/null +++ b/versions/4.0/en/animation/marionette/animation-mask.md @@ -0,0 +1,57 @@ +# Animation Mask + +An animation mask is an asset used in the animation layer to mask the animation effects on a given bone. + +For example, if you want to create a walking animation while shooting, you can use the animation mask to mix the upper half of the shooting animation with the lower half of the walking animation. + +## Creation + +In the **Assets Manager** click on the **+** button at the top left and select **Animation Mask**. + +![create-animation-mask](animation-mask/create.png) + +An animation mask named **Animation Mask** can be created by default. + +![animation-mask-asset](animation-mask/default-mask.png) + +## Edit Mask + +After selecting an animation mask in the **Assets** panel, you can edit the animation mask resource in the **Inspector** panel. + +![inspector](animation-mask/inspector.png) + +### Bone import + +Before everything starts, you need to make sure that all the bones affected by the skeleton mask are selected. + +On the **Inspector** panel, click the ![import](animation-mask/import.png) button, and in the pop-up prefab selection dialog, we select the prefab where the bone hierarchy is located. In general, we select the imported Prefab from the model file (FBX, glTF) where the target model is located, and usually there is only one. + +![import mask](animation-mask/import-skeleton.png) + +When selected, all bones contained in the Prefab will be included in the Bone Mask resource at + +![bones-imported](animation-mask/import-finish.png)。 + +You can also clear all imported bones by clicking ![clear](animation-mask/clear.png) to clear all imported bones. + +### Bone Enable and Disable + +When you want the animation effect on a bone to be disabled, make sure the checkbox in front of it remains unchecked. + +This is done by clicking on the check box next to the name of the imported skeleton ![checkbox](animation-mask/checkbox.png). + +If you want to operate only a single bone, you can select it by pressing Alt and using the left mouse button at the same time. + +Clicking ![checkbox](animation-mask/checkbox.png) check box will check/uncheck both the current skeleton and its children. + +By clicking ![expand](animation-mask/expand.png) to expand/hide the sub-bones. + +> **Note**: The direct enable/disable relationship between child and parent skeletons is mutually exclusive. That is, even if the parent skeleton is disabled, the child skeleton can still take effect. + +If the animation of the layer where the mask is located is not selected, all animations related to that bone will not be played. In the image below, **Bip001 Pelvis** is unchecked, so all animations at that level will not affect that bone. + +![check](animation-mask/check-skeleton.png) + +## Example + +For the full example, please refer to ([GitHub](https://github.com/cocos-creator/example-marionette)) diff --git a/versions/4.0/en/animation/marionette/animation-mask/check-skeleton.png b/versions/4.0/en/animation/marionette/animation-mask/check-skeleton.png new file mode 100644 index 0000000000..b5f33fc50d Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/check-skeleton.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/checkbox.png b/versions/4.0/en/animation/marionette/animation-mask/checkbox.png new file mode 100644 index 0000000000..b6dc2e1f2d Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/checkbox.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/clear.png b/versions/4.0/en/animation/marionette/animation-mask/clear.png new file mode 100644 index 0000000000..a4d18352bb Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/clear.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/create.png b/versions/4.0/en/animation/marionette/animation-mask/create.png new file mode 100644 index 0000000000..749ff88b60 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/create.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/default-mask.png b/versions/4.0/en/animation/marionette/animation-mask/default-mask.png new file mode 100644 index 0000000000..0ee6b8b021 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/default-mask.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/expand.png b/versions/4.0/en/animation/marionette/animation-mask/expand.png new file mode 100644 index 0000000000..213dfb498e Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/expand.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/import-finish.png b/versions/4.0/en/animation/marionette/animation-mask/import-finish.png new file mode 100644 index 0000000000..77c2a811c2 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/import-finish.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/import-skeleton.png b/versions/4.0/en/animation/marionette/animation-mask/import-skeleton.png new file mode 100644 index 0000000000..31eadfcb22 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/import-skeleton.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/import.png b/versions/4.0/en/animation/marionette/animation-mask/import.png new file mode 100644 index 0000000000..7e18574bf7 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/import.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/inspector.png b/versions/4.0/en/animation/marionette/animation-mask/inspector.png new file mode 100644 index 0000000000..f16b64e557 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/inspector.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/mask-example-skeleton.png b/versions/4.0/en/animation/marionette/animation-mask/mask-example-skeleton.png new file mode 100644 index 0000000000..a479a526ee Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/mask-example-skeleton.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/revert.png b/versions/4.0/en/animation/marionette/animation-mask/revert.png new file mode 100644 index 0000000000..1ade8e1f56 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/revert.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/save.png b/versions/4.0/en/animation/marionette/animation-mask/save.png new file mode 100644 index 0000000000..4efd79783d Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/save.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/select.png b/versions/4.0/en/animation/marionette/animation-mask/select.png new file mode 100644 index 0000000000..3c0060fa99 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/select.png differ diff --git a/versions/4.0/en/animation/marionette/animation-mask/weight.png b/versions/4.0/en/animation/marionette/animation-mask/weight.png new file mode 100644 index 0000000000..77d8f58cf0 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-mask/weight.png differ diff --git a/versions/4.0/en/animation/marionette/animation-variant.md b/versions/4.0/en/animation/marionette/animation-variant.md new file mode 100644 index 0000000000..a92d1268fc --- /dev/null +++ b/versions/4.0/en/animation/marionette/animation-variant.md @@ -0,0 +1,26 @@ +# Animation Graph Variants + +If two animation graphs have the same animation logic but different corresponding animation clips, re-creating an animation graph will create an additional maintenance burden. By using animation variants, this problem can be better solved. + +An animated graph variant is an asset that is created by clicking **Animation Graph Variant** within **Assets Manager**. + +![create](animation-variant/create-asset.png) + +The created variant is shown in the following figure. + +![default](animation-variant/variant-asset.png) + +## Properties + +![default](animation-variant/empty-varint.png) + +| Properties | Description | +| :-- | :-- | +| **Animation Graph** | Animated graph that require variants, selected by drop-down or from within **Assets Manager**
![select](animation-variant/select.png)| +| **Clip Overrides** | Animation clips that need to be covered
![override](animation-variant/overrides.png)
With the Overrides column on the right, select the animation clips to be replaced from the **Assets Manager** | + +## Use of variants + +The different variants can be selected by pulling down the **Animation Graph** property of the Animation Controller component. + +![controller](animation-variant/controller.png) diff --git a/versions/4.0/en/animation/marionette/animation-variant/controller.png b/versions/4.0/en/animation/marionette/animation-variant/controller.png new file mode 100644 index 0000000000..65f186361a Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-variant/controller.png differ diff --git a/versions/4.0/en/animation/marionette/animation-variant/create-asset.png b/versions/4.0/en/animation/marionette/animation-variant/create-asset.png new file mode 100644 index 0000000000..f62a61ade1 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-variant/create-asset.png differ diff --git a/versions/4.0/en/animation/marionette/animation-variant/empty-varint.png b/versions/4.0/en/animation/marionette/animation-variant/empty-varint.png new file mode 100644 index 0000000000..317cd26341 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-variant/empty-varint.png differ diff --git a/versions/4.0/en/animation/marionette/animation-variant/overrides.png b/versions/4.0/en/animation/marionette/animation-variant/overrides.png new file mode 100644 index 0000000000..5bef6ed4ec Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-variant/overrides.png differ diff --git a/versions/4.0/en/animation/marionette/animation-variant/select.png b/versions/4.0/en/animation/marionette/animation-variant/select.png new file mode 100644 index 0000000000..08ad16e3b2 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-variant/select.png differ diff --git a/versions/4.0/en/animation/marionette/animation-variant/variant-asset.png b/versions/4.0/en/animation/marionette/animation-variant/variant-asset.png new file mode 100644 index 0000000000..d8efd39e94 Binary files /dev/null and b/versions/4.0/en/animation/marionette/animation-variant/variant-asset.png differ diff --git a/versions/4.0/en/animation/marionette/flow-chart.pos b/versions/4.0/en/animation/marionette/flow-chart.pos new file mode 100644 index 0000000000..5826a7b2f1 --- /dev/null +++ b/versions/4.0/en/animation/marionette/flow-chart.pos @@ -0,0 +1 @@ +{"diagram":{"image":{"x":-129,"width":1455,"y":-35,"pngdata":"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SZL3338/efjhh3c7rjU8//zzuzx///3379G4lrZhw4bklltuSR9XV1cn3/ve91p8nR1fl/qy/bnZF2s29bV67rnn7naeRx99NFm9enWT5vzXf/3XpLq6OkmSJPnrX/+afPnLX07Pk+lfDwAAABrjzmsAWtXMmTPj1FNPTR9369YtbrzxxujSpctO1xYWFsb06dN3O9/o0aOjZ8+eDc7l5+c3WCMTamtr49BDD93p/MqVK+MPf/hDs8ftC1OmTIlzzz03fZybmxvnn3/+Pl+3LTw3+2LNpr5WG9uKes2aNXHYYYc1ac7y8vJYtmxZRET06dMn1q1bt1efAwAAQGtqn+kAABxYFi1aFDfeeONO53dV2HXp0qXRbQ4+Xtzt0L1791i0aFHMnTs3Jk+eHDNnzoyZM2fG888/HxdeeGGMHTs2Vq1aFY899lj07t07Vq9eHQUFBXHBBRdERMTs2bPjzjvvjFtvvTWWLVsWK1asiBNPPDFd7j733HPx2muvRUFBQcyYMSNGjx4d1157bURErF+/PubPnx933313PP300+lMb775ZqxcuTI2bNgQTzzxREREnHbaadGpU6fdjtvhwQcfjLy8vMjJyYmtW7fGF7/4xSZl/STz5s2Lr3zlKw3ODR8+fK/Xq66ujp/+9Kdx8MEHR15eXtTV1aXnbCvPTWNr7u7r/0l291ptqhdffDFOOOGEJs/54x//OA4//PCIiFi7dm36YwAAAACgniRJkgsvvHCXWyHsamuJJEmSz372s7vdQqEpc5x33nnJb3/726S2tjZ59dVXk1dffTVJkiS54447knnz5iVJkiS1tbXJBRdc0GDcGWeckd4yorq6Orn00kvTj11yySVJXV1dkiRJUllZmUyYMKFZeZr7eTzyyCPJ9OnT08ePPvpoMnPmzCZl/SQf/3xbar2f//znSWlpafqxsWPHNulz3N353T2+L56bxtZsyte/uXM25bFJkybt0bgkSZKJEycmL7zwQvo4078eAAAANMa2IQC0qurq6qiqqmrStWvWrGlw1+6e+vDDD2PMmDGRk5MTw4YNi2HDhkVExL/927/FyJEjo7y8PJ544onYtm1bg3EdO3aMk046KSI+2lKj/uM9evSIe++9NzZv3hxdunSJb3/723udc3emTp0aY8eOTR+fccYZMW3atCZlbe31SktL45RTTkk/1r79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//3vx8qVK1vj09mtL3zhC7s8/8ADD+zRuJa2cePGmDx5clxzzTXx+c9/PiZNmhRVVVUtukZxcXF06tRpp/PZ/tzsizUbe632798/LrvssqisrGyR9VrKO++8E5WVldGzZ8/48MMPY9KkSXHmmWfGtddeG8uWLYs777wzIiK9jcg777yz2/l8z+4537NN01rfs//5n/8Z48ePj6uuuiq++tWvxg9/+MO9XvO6B6bEN6dP77Vw4cItez0ZAABkAeU10JpuLh5/zV5NMGPGjLjooosilUrFkiVL4u23346OHTtGRMTFF18cy5Yti4qKioiIGDt2bMyYMWO3840ePTp69uzZ4Fx+fn6ceuqpe5Vzb9XW1sahhx660/mVK1fGH/7wh2aP2xemTJkS5557bvo4Nzc3zj///H2+blt4bvbFmk19rWbbthvTp0+PMWPGRETE66+/HqWlpdG9e/do165dXHLJJfHoo4+mrx0xYkRMnz59t/P5nt1zvmcb15rfs+Xl5bFs2bKIiOjTp0+sW7euRdYFAID9iT2vgVZTVlY24TszHrtrb+aYN29e/Nd//VdERAwYMCDOO++89GPvvfde5Obmpt/AcfDgwfHzn/88/umf/ukT5/t4sbBD9+7dY9GiRTF37tyYPHlyzJw5M2bOnBnPP/98XHjhhTF27NhYtWpVPPbYY9G7d+9YvXp1FBQUxAUXXBAREbNnz44777wzbr311li2bFn8/+zdeXiU5bk/8HsSEhBZRCQRlEXcUJRVjRCsKLjVDZci1l1re7S1P4+12tpa7VG7uB0ttta6VC24VRTFulCVaIkQBAUEcRfBhV02MWSZ+f3hYQpFSAJJJomfz3VxyfPOe7/PdybMi9fNM8/MnTs3DjjggHSjaOLEiTFr1qzIy8uLsWPHxqBBg+L73/9+REQsXrw4pkyZEjfffHO88MIL6UxvvfVWfPTRR7FkyZJ47rnnIiLiW9/6VnqV46bq1hk9enQ0b948srKyYs2aNXH66adXK+vmfhbnnHPOBsf69u271fOtXbs2br311mjfvn00b948kslk+pqN5bWpas7N/fw3ZXN/Vhuy6dOnx49+9KOI+Ko5PXr06PRjM2fOjKFDh6bHbdq0iVmzZm32et6z3rN18dpUNWddvGdvuumm2HnnnSMiYuHChenfAwAAABny5OKVqS39lUqlUscd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+bIXDz3ne+nXYcrYMYcsmTf31qrqVi1dXHzY9y9I/7ymPP5o4ZJ5c2/fXE1ExKrFi1498sIfp/freXXc2P2WzJtbZZf9P+umjnui15J5c0dtriYiYuWiBTOTlRXpP8evjX9qzyXz5v59czURESsWfPZWsrIi/a8WM1/8Z7cl8+Y+WVXd55998mGysiL9Pq123aeffJKsrDhq3fjtlyd0WjJv7rObq4mIyMrOXpKsrEjfhz58bWr7JfPmVvmvTolE1spkZcWgdeNP35rdesm8ucVV1yXWJisr0v8at3LJotwl8+ZOraou4qv77frjJfPmzqxO3coli/bbLi+/bN140Qfvv5pKpZpXVffpW7MLu/buu2rd+LN33p6YSiXbVFX34WtTD9nroIOXrht/PGf2i8nKyh1qWjfvjTeeqawo36mqurdfnnBkv+OGfZq+zvTXnqgsL9+lqrqZL/7zuEHDT51b07rXxj/1nSFnnf/2uvEHU199pLxsbY+q6qaOe+L0Iy+8KP0ze2/K5FFlpaW9NlcTEbH4ow9fGzx48I5FRUVV/4sDAAA0cLYNAerdgQce2C2RSLSu6rxkMvlRSUlJevndoEGDulRWVrbdXE1ERFlZ2bxp06atWDfef//9Ozdr1my7quq+/PLL+dOnT1++blxQULBzVlZWu6rqIuKTSZMmpZf7FRYWdkomk+03VxARUVlZ+emUKVPSjZf+/ft3zM3NrbJh8591BQUF+VlZWXlV1ZWWli54/fXXF68bDxw4MC+VSuVXVZdIJBa+8sori9aN+/bt26FFixY7bq4mIiKZTC4qKSlJL0usbl1ZWdmSadOmfbZufMABB7TPzs7uVFVdVlbW0uLi4nRDasCAAdtHRJWNrGQy+XlJSUl6GWSfPn2222abbTpvriYioqKiYvmrr746f924f//+bXNzc7tUVZednb1i4sSJ89aNCwoK2mRlZXWtqi6VSq2aPHny3HXjnj17tmrTpk2VjbPKysrVU6ZM+XDduFevXttuu+223TdX83V1AwYM2CYidquqLpFIrHnllVfeXzfu1q1bi44dO+5eVV1EfDlp0qT31g1222235h06dNijqqJkMllaUlLybk3rsrKyyoqLi9MNxf79++fk5uZW2VCMiPJJkya9td642YABA/aqRs7KkpKS9T9ykjVgwICe1ciZLC4unr3eocSA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tZAUAGhsiouL7+hz5NGZjgEAUO80r6EOTZ48Obp27ZpuYldXVlZWtG7duo5SNbx5m4LafO1Wr14d3/ve96J///4REbHHHntEWVlZrF69ulauDwAAANAYaF5DHVm1alXMnj07zj///Jg6dWp6pe+6lb9Tp06N4447Lg466KB46qmnNqj9utXB647dcsstMWjQoPjlL38Z1157bQwcODCuvfba9Hnz58+Pc889NwoKCuK4446LkpKSWLNmTfTv3z+++93vRkTEAQccEP37949p06ZVOW9ExJdffhnXXHNNHHTQQXHYYYfFXXfdFes+WV+d57MlPv/887jkkktiwIABcfLJJ8ecOXOqNd/s2bPj1FNPjYKCgjjttNPi2muvjQEDBlT7NXj99de36Hls6rUbO3ZsHH300XHAAQfESSedFCUlJVVeq23btjFs2LCIiEilUnH//ffHXnvtFR06dKiydkvmAwAAAGiINK8s22dUAAAgAElEQVShjkyZMiXy8vJi6NChUVZWFrNmzdrg8b/85S8xcuTIGDZsWPzpT3+q9nW33Xbb+NWvfhXPPPNM7LjjjnH11VfHuHHj0o9fd9110aVLl3j66afjhz/8YVx33XXRsmXLKCoqirvuuisiIl588cUoKiqKPn36VGvOa665JubOnRujRo2KW265JZ588skYNWpUrTyfTbnyyitjzZo18eijj8bhhx8eP//5z2P9rWi/br6Kior47//+7zj66KNj3Lhxseeee8Ybb7wR//jHP6r9GtTm81i4cGFce+21cckll8Szzz4bAwYMiCuvvLLa9UuXLo2jjz46/vrXv27wDxR1NR8A0DAVFhb++bnbR2Y6BgBAvdO8hjoyefLk6Nu3b+Tk5MQ+++yz0dYh5513XnTt2jWOPPLIWLhwYbWve9ppp0XXrl0jIuL000+Prl27RkVFRfrx3/72t3HFFVfE6tWrY/HixfHpp59GRETr1q2jZcuWERHRqlWraN26dbX2Z165cmWMHz8+rrjiiujatWv07NkzfvjDH8YjjzxSK8/n6yxfvjwmTZoUF110Uey0005xzjnnxIIFC+LDDz/c7HyfffZZLF26NIYPHx55eXkxbNiw+PDDD2P77bev9mtQm8+jRYsWkZ2dHalUKlq1ahWXXHJJjVZzt2/fPm6//fb41re+FVdeeWUkk8k6nY8tl5z7Qaz5zlGx5qQj4ouhBV/9/jtHZS7PJx/HmtOOr/OaLZGcP/ffr9Fxh0bppRdGaumSrbvmVmZPzp8bXxx10FZlAKhjP5j+3NOZzgAAUO80r6GOlJSUxHPPPRcDBgyI6dOnb/Sljb169YqIiGbNmtXous2bN0//vkWLFhs9/uKLL8YxxxwTP/vZz2L27NkbrFbeEosXL45UKhWdO3dOH+vcuXMsWrRog/O29Pl8naVLl0ZExAUXXBAHH3xwDB06NCoqKmLBggWbna9Tp07RvXv3mDhxYkRETJw4MfbYY48azV2bz6Nt27Zx7bXXxgMPPBCHH354nH/++entT6qra9eucc0118T7778fs2fPrvP52DJZ3bpHy78/Ey1uuSMiJzda/v2ZaPn3ZzKXZ6edo+XoJzY6nlqxPMpH31Ojmjqx7jV64vnI6r5blN1241Zdrl6zAwAAUG+2vjsDbGTevHnxySefxJ/+9KfIz8+Pt956K6688spYtWpV+pzc3Nxan3fp0qXxu9/9LkaNGhV77rlnzJw5M8aPH59+PJFI1PiaHTp0iKysrPj444+je/fuERHx8ccfR35+/gbn1ebzad++fURE3H333bHttttGxFf7bufn58cnn3yyyfmys7PjpJNOiquuuiquuOKKyM/Pj5tuummDc6p6DWrzeXz55ZfRqVOnuOeee6KysjLuv//+uPzyy+PZZ5/dbN2bb74ZY8aMSW/50axZs8jJyYmsrM3/e+OWzsc3R2rl8igb9dfIOe3cTEf5SiIrmh1+dJT+9EeZTgJAPUslk/Hu1CkxZeyYmD9ndqxdsybTkagDiaysaLNDh9j30KGx/9HHRdu8/KqLGokDhp0UJY/9/abZs2dXVn02AFvKymuoA5MnT478/PwoKCiIbt26xZAhQyI3NzemTJlSp/OWlpZGMpmM1atXx9tvvx033HDDBo/n5eVFVlZWTJ48OZYsWbLRPtxfp02bNnH44YfHddddFx999FHMnj07brvtthg+fHhdPY3YbrvtYuDAgTF27NjIzs6Ozz77LM4888x043pTFi9eHA8//HDcd999MXr06Hj44Ydjt9122+CcLXkNtlRZWVlceOGF8eqrr8aKFSuisrIyysvLq6zr0qVLvPjii3H//ffHsmXL4u677462bdtGjx7/n737jquq/OMA/jkXLnupICopWColZopb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diYAAAAAgHscxWsAAHJq3z6pUCFp927pySelzp1vHouOlurVs7bQqFxZWrYsY8zChVLVqlLx4tJXX92MGz/een65ctZVx0uWZD5nevXrS02bSjduSA88YN0uX84+RpL275caNJDKlpWee066eNGxHB1lsUhpnTj27pWeeMI6V2CgdPJkxrkye4a5yV2Stmyx5u3jI33xRc7zBgAAAAC4FIrXAADkRmqqtTXG+PHSd9/d3P/ll9J770lnz0offCC9/fbNY0lJ0sqV1n7aEydKw4db9586JQ0cKG3eLJ07J336qbRggf0ctm27va2Jl5f9uC5drKujz52TypSR3n3Xfo6OunHDWvBu2dI67tzZem/nz0tPPy0FB988N6tnmJvcDUN68UVp8GDpxAlrHgAAAACAexrFawAAciMlxVp0rVdPKlLk5v6JE60vCXz7bWncOGtbjzSGYS1Mu7tLDRveXDXs7W19yeE770gTJlhXDi9cmD95nzxpLUz36WNdHf3mm9Lq1fZztCdt9bePj3WOL7+0FpF/+836LB54QJo0ybqiOk1WzzA3uZ84IR09KvXsaT32+uuOPxMAAAAAgEsq4OwEAODvJiEhQYcOHZK3t7e8vb2dnQ5yq2BByc8v476kJGshtn1769ajh9Sixc3jHh5ShQrWr9PaakiSm5u15cWWLdY2GoMGWYvA8+fn/31I1oK1vRztSVv9nd6JE9Ye2MePW4vhqakZ25pk9gxzKi13iyVjzm5ud3bdPFa3bt3/M5vNO6Kjo2OcnUt+2bdvnzZt2qSkpCTVrVtXTZo0cXZKDjt37pxmzZolSTKZTCpTpozatWunMmXKODkzAAAA4O+NlddALuzatUsBAQE6ceJEjuL279+vgIAApaam5lNmrjOvs+41r/PI6/vYunWrOnTooM8++0zPP/+85s6dmyfXhYs4dUo6eFAaOtTax3nmzIzHsyoGx8VJNWtK1apJb7xhbbPx00+OzVmypJScbG2jYRjShQvZn+/jY50rNNR6fmio1K6d/Rxz48EHrfcUGmodT5t2e2/rnMgu90qVrEX3hQutx77++s7zz0Nms/l9Sbv8/f0P+vn5/cvPz6+epDx82M41f/589ezZU9OnT9e3336rd955R+PGjctwzrlz5zR27Fht2LAhV3PcaXx24uPjNW/ePM2bN09z587Vl19+qZde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Z6UKFaTTp629m//807oaOjtZxdSpI73+unWeN96w9paeOTP7a127Zu1z/eab1r7RvXpZcx00KPs4Pz/rKu++faXVq633e+yY5O6eu9ylzD+TtH07d2b87/XrWT9XO5+HyXRrFdx18PMQOdGnTx9FR0dLUtvo6Oi1zs4HeScwMHBa3aefef3pN/s5OxUAf/m8U3tZUlN18eLFgrGxsUnOzgcA7lesvAYctH//fgUEBNy2XblyJUfxqamp+Zyp68rrZ7B9+3Y999xzatq0qb755huH4zL7HAMCAvIkJz7n+0yZMtZWEw0bWltPDBwoTZx453GLFlmLyKVLS7NnS9On3zzm7W194WHt2tZi7rJl9ufLKmbRIum776z7Tp+WJkywf63ChaWNG639qN94w1q8fv9967EDB7JehT13rjRrlvWehg+Xli+3X7jO7f1mJavnmtvPEQBcx+s71612dg4AAAB3ncuuNMLfmyuuNNu/f7969OihjRs3ymy++XufokWLypFFexaLRVevXlWx9C9CS3fdrVu3ys3NLc/zzmt3km9WzyA34uPj1aVLFw0fPlzly5dX37599cknn6h+/fp2Y2/9hcOyZcsUERGhKVOm2I21d/95eY/3ont65TX+tlh5jfsFK6/vX2n9kgcvW+PsVAD8hZXXAHB3sPIayKGiRYuqWLFits3RmofZbP7bFjTT5OUzWLFihZo1a6Ynn3xSjz76qF544QX98MMPDsWm//w8PDw0b9489e/fP0/y4nMGAAAA7n8NOnaSyWQaFxsby59cAkA+ongN5IG4uDg9+eST2rFjhyRp1qxZ6tGjhywWi+2cW9tJJCYmKiAgQD169JBkXbEaEBDg8Oq6pKQkff7552rRooUaN26sfv366fTp07bj165d06hRo9S0aVO1atVKM2bMUNoi01tzSRun/3r79u169tln1bRpU61cufKO881s3jTLli1Tu3bt1KBBA3Xq1EmRkZF2r3XgwIEMq6xr1aql3377zeFc0ixdulQ1atRQ9erVsz3P0fvP7B7T9k2YMEFNmjTRsGHDFBISosaNGyskJMR2Xnx8vAYMGKBGjRqpc+fO2rt3r+1Ybp4RAAAAgPzRrNcr6j99+keSKF4DQD6ieA3kUIsWLdSsWTM1a9ZM7//VB7ZMmTJ644039OWXX+ry5cv69ttvNWTIkAztRW7l6empsLAwzZgxQ5K0ceNGhYWFqW5dh17wrVGjRum3335TaGio5s2bp8TERA0ePDjD8SNHjmj27NmaMGGCli9frtmzZzt8n1999ZUmTZqkjh07asqUKXecb1bOnj2rkJAQDRgwQGvXrlWjRo0UHBxsNy4xMVHe3t62cYkSJXT+/PkczW2xWDRv3jxbQTo7eXH/RYoU0fDhw7VmzRo98MAD+vjjj7VixQrb8eDgYCUmJmrx4sVq3bq1PvzwQxmGketnBAAAAAAAcC8r4OwEgHvNnDlzbEXpQoUK2fZ3795dy5cv19tvv60WLVqoTp06dq9VrFgxeXp6SrK2I3G0h/Tly5e1du1azZ8/X4888ogkacSIEdq/f7/t+Pr167VgwQL5+vpKkt566y1NnjxZvXr1cmiO1157Tb6+vmrTpo3mzp17R/lmp1ChQnJzc5NhGCpatKgGDBjgUAsPNzc3eXh42Mbu7u66ceNGjubevHmzChcu7PDLGu/0/l966SUdP35cktSzZ08dPXpUKSnWttAXL15URESEZs2aJR8fH73yyiv65ptvdPjwYZUuXTpXzwj2uXKvYwAAAAAA/u5YeQ3kUPny5VWhQgVVqFBBpUqVsu03m816/vnnFRsbq86dO+drDufPn5fFYlHFihVt+ypVqqRWrVrZjhuGkeF4xYoVde7cuUyvl1ZATS+t+F6gQP7+jqt48eIKCQnR3Llz1bp1a/Xp0ydDu4yseHl5KT4+3jZOSEhwuP94mlWrVqlt27Y5zjm3ChYsaPs6/S8+JOnChQuSpDfffFPNmjVTUFCQUlJSdObMmVw/IwAAAAD5Y+uyxZrUt+/7oq4CAPmKb7JAHklMTNR3332nRo0aaerUqQ7H5WbhZ9myZWUymXTixAnbvn379ql3796242azOcPxEydO2NpspM2Z1pP74MGDt82RflXzneabnWvXrqlChQr65ptvtGHDBjVp0iRD+5OsVKtWLUOP63379qlcuXIOz5uYmKhNmzbZCv6Oyq+FuqVLl5Ykff3115o3b57mzZunBQsWqE6dOrl+RgAAAADyx8/fz5QlJeWTmjVr8hftAJCPKF4DOZSQkKArV67YtrRVy1OnTlX16tU1fvx4HT58WGvXrnXoeuXKlZPZbNaWLVsUFxenPXv22I3x8vJS69atFRISokOHDunYsWOaMmWKKlSokOH46NGjdfToUcXGxmry5Mnq2rWrJGuPbpPJpL179yoxMVHz5s1z+P5zk292kpKS1LdvX23btk2XLl1SamqqkpOT7cY1b95cy5Yt0/nz55WQkKClS5fqiSeekGQtyl+5ciXb+K1bt6ps2bLy8fHJUb55ff9pSpQoocaNG2vZsmVyc3PT6dOn1bt3b508eTLXzwgAAAAAAOBexm8IgRxq0aJFhvGYMWPk4+OjpUuXauHChfLw8ND777+v0aNHq3HjxvLy8sr2eiVKlNDAgQMVHBysa9euqXPnzqpVq5bdPIKDg/XFF1/olVdekcVi0VNPPaVBgwbZjg8bNkyff/65evbsqYIFC6p79+566aWXJEmlSpXSSy+9pHfffVcPPfSQunbtqpCQEIfuP7f5ZqV48eL68MMPFRISolOnTqlChQoaPny43bgyZcqoV69e6tSpk0wmk4oUKaJ//vOfkqQ//vhDPXr00NatW7PsS71t27ZcvWwyr+8/vZEjR2rUqFHq2LGjbZ5q1apJUq6eEQAAuG+88fQbb09zdhIAAAB3Gy+qgkvy9/c3JCkqKsrZqcDFHT16VIcPH1b9+vVVpEgRZ6fzt9egQQOlpqbKy8vLPSws7PZm6gByhJ+HyIk+ffooOjpaktpGR0c79idguGcsP3/ZcHYOAG76vFN7WVJTdfHixYKxsbFJzs4HAO5XrLwGcE/z9fWVr6+vs9MAAAAAAABAHqPnNQAAAAAAAADA5VC8BgAAAAAXFhgYOG3d1EnOTgMAAOCuo3gNAAAAAK7t9Z3rVjs7BwAAgLuO4jUAAAAAAAAAwOVQvAYAAAAAAMiBBh07yWQyjYuNjU11di4AcD+jeA0AAAAAAJADzXq9ov7Tp38kieI1AOQjitdwVYmSlJCQ4Ow8ADjoxo0bSk1NlWEYSWFhYSnOzge4TyRK0pUrV5ydB+4BiYmJaV9edWYeAAAAQF6heA2XZBhGrCQdPnzY2akAcNCpU6ckSSaT6ZCTUwHuG2k/D//44w9npwIXl5qaqjNnzsgwDCMpKel3Z+cDAAAA5AWK13BV30jSZ599pv379+vqVRYQAa7q2rVrOnjwoMaOHZu2a4kz8wHuM7afh/v27ePnIW6TlJSkkydP6rPPPtPFixdlMpk27tmz56yz8wKA+93WZYs1qW/f90VdBQDylcnZCQBZcPfz81tjMplaOjsRAI4zDGPblStXmh44cOCGs3MB7hPufn5+/zWZTG2dnQjuCacsFkvznTt3slT/PhMYGGhI0uBla5ydSp4xLBb9sX2rti5bouN7Y3XjZtsb3EdMZrO8ypRV7RZBqt/uWRUv5+3slPLM553ay5KaqosXLxaMjY1NcnY+AHC/4jeEcFXJO3bsaGsYxoeSYiRdc3ZCALJ0XdJewzBCKFwDeS55x44dz1oslg8Mw9ipv3pgA+kkG4ZxVNJXSUlJ/hSucS8wLBb9d/xnmhs8RAeitlG4vo8ZFosunTurzfPnaPI/euvkvr3OTgkAcI9h5TUAAAAAuLDAwMDXn37j7Wl127Rzdip5Ys/PG7X4k5EqU6aMBg8eLD8/P5UsWdLZaSEfJCcn6+TJk5o/f74WLVqkkg+U19tffy+3AgWcndodY+U1ANwdrLwGAAAAABcWHh4+/X4pXEtS1OqVkqSRI0eqRYsWFK7vY+7u7qpcubKGDBmihg0bKv7MaR3eGe3stAAA9xCK1wAAAACAu+bkfmvriOrVqzs5E9xN1apVkySd+mO/kzMBANxLKF4DAAAAAO6apGvW19kUL17cyZngbkr7vOlxDgDICYrXAAAAAODCAgMDp62bOsnZaQAAANx1FK8BAAAAwLW9vnPdamfnAAAAcNdRvAYAAAAAAAAAuByK1wAAAAAAADnQoGMnmUymcbGxsanOzgUA7mcUrwEAAAAAAHKgWa9X1H/69I8kUbwGgHxE8RoAAAAAAAAA4HIoXgMAAAAAAAAAXA7FawAAAAAAgBzYumyxJvXt+76oqwBAvuKbLAAAAAAAQA78/P1MWVJSPqlZs2YBZ+cCAPczitcAAAAAgL89i8Wi77//Xt27d1ejRo3UqFEjtWvXTvPnz8/0fMMwcj3XncTeaurUqQoICFBAQID+97//5dl1AQBwBRSvAQAAAMC1vfH0G287O4f73pgxYzRhwgQlJiaqY8eOeuGFF+Tj46PDhw9nOG/48OFq3bq1Ll26lOM57iQ2K2vWrJGnp6ckae3atXl2XQAAXAF/3gIAAAAALiw8PHz64GVrpjk7j/vdqlWrJEmTJ09WpUqVbPuvXbuW6Xl3Mkde2bVrl06ePKlWrVopPDxcmzZt0tWrV1WkSJE8nQcAAGdh5TUAAAAA4G+vaNGikqRx48bp6NGjtv2FCxe2fR0QEGD7umXLlhnGcXFx+uCDD9SyZUs1bNhQvXv31v79+x2KlaSffvpJ3bt31xNPPKFWrVpp3LhxSkpKyjbn1atXS5KaNGmiBg0aKCkpSRs3bszJbQMA4NIoXgMAAAAA/vbeffddmc1mbd68WZ06dVL//v21Z8+eDOe8/PLLtq+7d++eYXzixAkdO3ZMbdq0UUBAgGJjYxUcHOxQ7Lp16zR48GAlJyerS5cuevDBBzV37lxNmDAhy3xTUlL0448/ymQyqXHjxmrUqJEkaxsRAADuF7QNAQAAAAAXFhgYOG3d1El6+s1+zk7lvtamTRv5+vrq66+/1qZNmxQeHq6IiAgNHz5cHTp0kCT169dPM2fOlCT16dNHJUqUsMU/9thjmj17to4ePaoDBw4oMjJSBw8eVGJiojw9PbONnTFjhiSpXr168vDwUNWqVRUTE6N169Zp0KBBmeYbHh6uS5cuqVq1aipVqpQCAwMlSdu2bdOFCxdUunTpvH5EAFyHuW7dum3MZnN/SY0keTk7IeSLVMMwjkual5ycPHXPnj3HnZ2QM7DyGgAAAABc2+s71612dg5/CzVq1NDYsWO1YsUKtWjRQhaLRZMmTXIoduPGjQoKCtKrr76qpUuX2vbba/0hSceOHZMkLV68WDNnztTixYslSRcvXswyJq1liCRNmjRJixcvVqFChWSxWLR+/XqHcgZwTzL7+/t/bTabV0l6WhSu72duJpOpsslk+tDd3X1vQEBAQ2cn5AysvAYAAAAA/O3FxMSoTp06kiRvb2+99dZb2rhxoxITEzM9/9ai9GeffaaEhAQtXrxYJUqUUFBQUJZz3RpbunRpnT17VnPnzlW1atXs5nr16lVt2rRJkrR///4MvbUla+uQF1980e51ANx7Hn/88c6SXi5TpowGDx4sPz8/lSxZ0tlpIR8kJyfr5MmTmj9/vhYtWlTEMIy5kqpJSnF2bncTxWsAAAAAwN/eK6+8ojp16uixxx6TxWLR5s2bJUlPP/10hvO8vb119uxZDRs2TL6+vho6dKgkyWKxSJImT56s+Pj4TOfIKvaFF17Q1KlT1a9fP7Vo0UIeHh46ePCgnn/++UyL4D/99JOSkpLk7e2tlStXymy2/lH1gQMH1K1bN8XGxur48eOqWLFi3jwc3KZBx06KXLpoXGxsbKqzc8Hfi5ub2+uSNHLkSDVs+LdciPu34e7ursqVK2vIkCE6duyYIiMjH/b39w+Kjo5e6+zc7ibahgAAAAAA/vaCgoJ0+vRpLVq0SD/88IMKFSqkN998U0OGDMlw3kcffSQfHx/t2LHDVuCWpPfff19eXl7auXOnnn322UznyCr21Vdf1TvvvCNPT08tXbpUP/zwg65fv67KlStnep20liFt27a1Fa4lqUqVKraV22vX/q1qG3dds16vqP/06R9JoniNu62+JFWvXt3ZeeAuSvdXOQHOzMMZWHkNAAAAAPjbGzNmjEPnNWnSRE2aNLltf4cOHWwvdpSkjh07OhxrNpvVu3dv9e7d26Ecpk+fnuWxuXPnOnQNAPesYpJUvHhxZ+eBuyjd5/2363HOymsAAAAAAAAAgMuheA0AAAAAAJADW5ct1qS+fd8XdRUAyFd8kwUAAAAAAMiBn7+fKUtKyic1a9akHSsA5COK1wAAAAAAAAAAl0PxGgAAAABc2xtPv/G2s3MAAAC46yheAwAAAIALCw8Pn163TTtnpwEAAHDX0ZsJAAAAAADAxRiGYTg7B+SeyWQyOTsH4H7AymsAAAAAAAAAgMuheA0AAAAALiwwMHDauqmTnJ0GAADAXUfxGgAAAABc2+s71612dg55poC7hyTp2rVrTs4Ed9P169cl3fz84bhDhw7pn//8pxo1aqTAwEBNnz7dKXkEBARkuuW3+Ph4devW7Y7Pyau58pq/v//btWvXfviuTpoPLBaLvv/+e3Xv3l2NGjVSo0aN1K5dO82fPz/T8++kK05edNRJ/2+4Xr16atmypYYOHaoLFy7c8bWRtyheAwAAAADumlI+PpKkkydPOjkT3E0nTpyQJJWq4OPkTO4tFotF/fr1U9OmTRUWFqZly5bJz8/Pdvzs2bN64YUX7mgOR68RFRWlqKgoLVy4UGaz2TbObyVLltSCBQts48zyvfWcvJrrLpnk7u5+0N/fP9rPz+/92rVrV7/bCeSFMWPGaMKECUpMTFTHjh31wgsvyMfHR4cPH85w3vDhw9W6dWtdunQpx3PcSWxWunfvrtPP97oAAB/xSURBVC5duqhgwYJau3atPvzwwzy7NvIGL2wEAAAAANw1tZ8K0oZvQzVx4kSNHz9e7u7uzk4J+WzXrl3asGGD3AsWVPVGgc5OJ0806NhJkUsXjYuNjU3Nz3lOnDihM2fOqHPnzipYsKDKli2rsmXL2o4nJCTo+PHjub6+YRh3fI3Mrpmf7yrM63xdiJ/JZPJzd3cf6+fn95thGLNTU1NX7t69e7ezE3PEqlWrJEmTJ09WpUqVbPtv/SubtPPuZI681KdPH5UoUULPP/+8XnzxRcXExOT5HLgzFK8BAAAAAHdN/Q4dtXP9Gv36669q0aKFypYtq8KFCzs7LeQDi8Wiy5cv6+zZszIMQ637vKmCRYo4O6080azXK2r4dNBHm2vWzNfidZkyZVSoUCHNmjVL//znP28rCnft2lWSbO070lZCb926VV988YUOHTokb29vDR8+XPXq1dPBgwfVtWtXBQcHa9y4cRoxYoQGDx6c6TUcldk1vby8sp1/9OjRGjdunEwmk0JCQtSgQQNJ0uLFixUaGqrLly/rxRdfVP/+/W0xaXllds+3npOYmKgxY8bof//7n/XfXuvWGjx4sDw8PLLNIf117OV65coVjR49Wr/88ouKFy+url276j//+Y+2bduWo+eXGZPJ9JjJZPrEbDZ/4u/vf1DSbEkroqOjo+/44vmkaNGiunbtmsaNG6cBAwbI19dXkjJ8f0/fZqZly5aSbv57i4uL05gxYxQdHa2EhARVq1ZNQ4cOVbVq1ezGStJPP/2kGTNm6MiRIypWrJjatGmjfv36ycPDsVZFiYmJkqTatWvn+N6RvyheAwAAAADumkJFiujVLybrx9Bp2vnTOh09etTZKSGfeZUpq9Z93lCt5i2dnUqe8/f3t9yyK0Mz3lt785pMJnvNejMc9/T01AcffKBPPvlE27dv10cffaSHHnrIdnzhwoXq3r37bQXThIQEDRs2TI8++qhCQ0P12WefaeHChbbjBw4c0I8//ijDMLK8Rk6lv2ZERES28//xxx9asWKFJk6cqIkTJ2rOnDmKi4vTp59+qpkzZ+qhhx7K8nuDI/mOHDlS169f1/Lly3Xjxg198MEHmjp1qt55551sc8hMVueNHj1aV65c0fLlyyVJH330UYY4f3//81mkl9OGzY9IGiFphL+/v8suOX/33XcVHByszZs3Kzw8XI0bN9Y///lP1apVy3bOyy+/rJkzZ0qytusoVKiQ7diJEyd07NgxtWnTRocPH1ZkZKSCg4Nt/26yi123bp0++ugjVa5cWV26dNGePXs0d+5cpaamatCgQdnmHRoaKovFop9++kn169fXqFGj8uiJIK9QvAYAAAAA3FVFipdQx4FD1LZvf106d1YpycnOTgn5wGSSChUtphLeD+RrGwlnOXP0qMnI5s1xJqvbdmfxdZY6duyoqlWr6pNPPlHPnj01ceJE1atXL9uYFi1a6MaNGzp8+LCKFCmiY8eOZTj+4osvZij+5YX017Q3/0svvaRChQrpmWee0X//+19Jkru7u8xms06ePKlatWrpsccey1UeV69e1YYNG7RkyRKVKFFCkrU1xKeffpqheJ1ZDpnJ7LzExERt2LBBCxcuVOnSpSVJffv21T/+8Y/0oWVydQPZMAzDw1X/t9SmTRv5+vrq66+/1qZNmxQeHq6IiAgNHz5cHTp0kCT169fPVoBOa9eR5rHHHtPs2bN19OhRHThwQJGRkTp48KASExPl6emZbeyMGTMkSfXq1ZOHh4eqVq2qmJgYrVu3zm7xOv0LJUuWLKljx45laM0D56N4DQAAAABwioKenipX+SH7JwIuZuuyxfp59ncDduzY4S7p1tXXecGU/rqPPfaYZs2apX/9618aP3685s6dm23wjBkztGjRItWqVUtFihRRamrG7ibly5fP84TTX9Pe/KVKlZJkXVme/Ncvr4oXL65PPvlEX3zxhb777jsNHTpUNWvWzHEecXFxslgsevDBB237ypUrpz///NNuDpnJ7Lzz58/LYrHYWmNI1rYZ/9/enUdVWe1/HP+cg4Ki4pCz5lAOKcV1xAFzSsXUkrJrZpmatxzKsswsK6/dSn85G6ZoWaRlg2Zex4gkSKksjUFPVnbTBDUUBUEQkHOe3x9eTpri4SDyHLnv11qtxT7P/u79wZK1/Lrbz/lyc3Ovs9vthXaaHQ6H5b91hZ3QliQZhpFssVi2StoYFxe36RKn/T1Gq1atNHfuXKWkpGju3LmKiopSaGios3l9OVFRUZo1a5YMw9BNN/35zsq8vDz5+vpetrbgL0fWrl17wefp6eku9922bZt8fX31+eef65///KeeeeYZRUZGymq1uqxF6aB5DQAAAAAA4IaYVeFy2O0z/f3959lstryrsMVFJ7qtVquGDh2qsWPHXrbwxIkTCgsL06ZNm1S3bl3FxcVp69atF61V0grWLMr+henTp4969eql5cuX67nnnnNeyeGO6tWry2Kx6OjRo2rQoIEk6dixY6pdu7bbaxWmSpUqkqSUlBRn0z4lJeWCOTab7eRFhZfQrl27iz4zDOOAxWLZ4nA4VsfHx399pXlLQ2JiogICAiRJderU0aOPPqqoqCjnXdJ/lZd34W+b2bNn6/Tp01q7dq2qVaumPn36FLrXX2uvu+46paSkaPXq1c47st3h7e2tLl26SDrX8M7NzeVdDB6E5jUAAAAAAIAHSkpKUkxMjAYOHChvb29t2rTpghfKVa5cWQ6HQ3v27FHjxo3l5+en/Px8GYahpKQk+fr6auXKlZfd41JrXAl39y9w7NgxJSUlKSAgQA0bNlRubm6x8vr5+albt26aP3++pk+frry8PK1YsUKDBw++ou/rfDVq1FBAQIAWLlyoF154QRkZGc6rK67Az4ZhbLZYLKvj4uLce2umBxg9erQCAgLUunVrORwO7dixQ5IUHBx8wbw6deooJSVFL7zwgho3bqznn39e0rkXvErS4sWLlZaWdsk9Cqu9++67tXTpUk2cOFG9e/d2vpjzrrvuumwTXDp357WXl5czb8eOHWlcexia1wAAAADg2cYFj3sszOwQAEpfpUqVFBUVpSVLlshqtapjx46aMWOG83mdOnU0bNgwPfLII6pcubIiIyNVp04djRo1SpMmTVKNGjU0YsQIZ2PuUi61xpVwd/8CDodDM2fOVHJysho0aKCXXnqp2HlfeuklvfrqqxowYIB8fHwUEhKiUaNGXdH39VevvPKKpk+frr59++qGG27Q8OHD9dNPP7m9jmEYsxwOx/sJCQm2Eg1Yyvr06aOEhATZbDZZrVZdf/31Gj9+vEaOHHnBvGnTpmn27NmKi4tTUtKf75+cPHmy5s+fr/j4eE2cOFEJCQkX7VFY7UMPPSRvb2+tW7dO69atk4+Pj1q0aKEmTZq4zP3hhx/KYrGoWrVqGjx4sCZNmlT8XwRcFZ55yzsAAAAAwGnD8YxCXwoHoPTNGTJIDrtd6enpPlfp2hBd7mWQ8DxffPGFwsLCnPcuX+ptnSWhXbt2hiTt3n3NHc7GFQgPD1doaKgkzf7hhx+mmp2nNHH7OAAAAAAAAOCGL7/8UsnJybLb7dq3b5/eeOONEr2aBMA5XBsCAAAAAAAAuOHQoUN67bXXlJaWplq1aikkJET333+/2bGAMofmNQAAAAB4sKCgoLCIpaEKHj/R7CgAgP8aOXLkRfc5Ayh5NK8BAAAAwLONjY/YQvMa+B9zte5MBoBrCXdeAwAAAAAAAAA8Ds1rAAAAAAAANwSGDJHFYplns9nsZmcBgLKM5jUAAAAAAIAbeowYrceXLZsmieY1AFxFNK8BAAAAAAAAAB6H5jUAAAAAAKUoLmKLMk+kmh2jRCT9uFe//bBL+WfzzI4CACiDypkdAAAAAACA/yX/nj9bktT45gC17t5TLTp1UfW69UxOVTyH9u5R5Ipl8vH1VatuPdT61h5qEtBG3hUqmB3tqvpu/VrFvPfuZEmvSXKYnQf/U3IkVThz5owqVqxodhaUkpycHEmSYRg5JkcpdTSvAQAAAAAwwe97E/X73kRtXfK66jdvoVt691XzwM6q2fB6s6O5LTc7W/Gfb1X851vlXaGCburaTa269dANbdvLx9fX7HglLmZVuBx2+0x/f/95NpuNY+coTb9Kuvnw4cNq1qyZ2VlQSpKTkwu+/NXMHGageQ0AAAAAgMmO7P9FR/b/oohlb6h2k6a6uWdvtezcVXWa3njBvGMHD2jJ2NGaERFtTtAiyMvJUWLUF0qM+kKS1LpbD7Xu3kM3tuuoilWqFGtNwzCUvM+mfbHb1e/h8YXOS01O0ra3l+tAQpzy8/LUJOBvuvPJZ+RXs1ax9gU8jWEY71ssllmLFi3S/PnzVb58ebMj4SpLSEjQtm3bZBhGltVq3WB2ntJmMTsAAAAAAKBwQUFBY4PHPRbWpv9As6OUiBnBPc2OcE2pXq++0o4eMTtGibCWK6fmHQLl36O3mnXoKF+/qkWunX//PTIchrLS0zR9a1Sh83Z8vFreFSrqll63yWG3a+PCucrLydGD/zevJL4FpzlDBslhtys9Pd2Hk9coTc2aNfOrUqXK9xaLpYWvr69q1arF9SFllMPhUEZGhlJSUmQYhgzDeDouLq5kf5hdAzh5DQAAAAAeLDY2dtnU9VvDzM4Bc+TnlaG+qGHoTGamMk+kyn727GWmGbJYLjxrN2zGTJUrX15h48dcdouu9wyT1Wp1joPuHa6VUydfWW7Ag/z6668Zbdu27SZpdlZW1oPZ2dlWl0W41h3+b+P6Q7ODmIHmNQAAAACg1HjydRelxdXp8+r16qtZh0Dd0us2Xd/65gsauZe7NiT3TLa2LF6kn77eIRmG/Hv20oAJT6ict7fL5wXrPvDqbG15Y5EyUo/rxvYdddfTz6lC5cqSpF2bNyjmvXd15nSmOoUMUd8xYxX78QeKXLHsst9P0zbt1KxjoI788rN+/f47pfz2H504nHTR3ndOmqLPli1WyORn1frWHhesUb95Cx07eMDlr+35jWtJykpPl18trgxB2RIXF3dc0uiWLVs+XqFChUZeXl5l+w2p/6PsdrvDMIz0xMTEg5IMs/OYheY1AAAAAAAmq3l9IzUP7KyA3n1Ur1mLYq2xYf4cnc3N0RPhq5V/Nk8f/Wu6vlz5tvr+Y1yRnktSfORnGrNgsRx2uz5+ZYYi3wrTHZOeVubJE9ocukBjFi5RrUaNdOLPl4ddxKu8t+z5ZzVo4pPy795LFatU0ZpXX3K597HfD2jKR5/KKKEWjcPh0NdrP1TbfreXzIKAh/n5558zJdnMzgFcTfyvBQAAAADgwYKCgsIiloaaHQNXQd0bmunWYQ/o0eXheuytlQp+ZEKxG9e52Vn6cXuMgsc+Kt+qVeVXs5Z63P+g9sZ8WaTnBfo9PF6VqlVXletqqsf9I/Xzt19LkrzKlZfFalX6H0fl41tJ9Vu0vKCuvI+PWnbuqpDJz+qheYtktVjUYeCdqlilSpH37jT4bpX3qSDvCiVziPSzpaEyHIa6DBlaIusBAEofJ68BAAAAwLONjY/YouDxE83OgRLSe9QYBfTuq2p16pbYmpknT8owHKper77zM7+atZSVnlak5+d/VqBy9erKzc6SJPn6+WnIsy/q8zeXKnbNBxr0+FNq0LKVat9wo+6ZNl0tO3dVeZ9zTee/Xu9R1L2r1S25X49t77ypgwlxGjX3dXmVo/UBANcqTl4DAAAAAFCKut83okQb15JUqWpVWSwWnUr5w/lZRupx+V1Xs0jPC+ScPu38+vih31W1Vm3n2L97Tz0RvlrNAztr7ayXJUnNOwTq5h69nY3r4mQrYLGUTIviy5Vv66evd2jk7AXy9fMrkTUBAOageQ0AAAAAcMuxgwc0I7inZgT31CuD+uq956coIzXV+Xlp2bV5g+bce5devTO4RNbbFv6W5gwN0b8G3KblE8fqyP5fSmTdq+F02knnP7nZWapYxU/NAzsrYvkSZWdkKPNEqr76YJXaBg+QJJfPC0SuWKbcM9k6kZykmPfC1ea/90VnpKbqYGKCDMOhGvUaKD83t8hZi7p3SYh+L1y2r6I1cvZCVapWvcTXLxAYMkQWi2WezWazX7VNAABcGwIAAAAAKJ4ZEdHKPnVKGxfN1cZFc9V3zNhS2zsrPU2bQxdq5OwFF92/XFz1m7dUl7eGyqucl6LCV2jtqzP0ePjqElm7pM0ddrfz63b9B+rOJ6co5OnntGnRXC14YKjK+3irbf+BCrp3uHOeq+eSVL95Cy16cJgcDofa9rtdXf8+TJJkOOza9Po8pR09oup16ynk6WfdyluUvS/n/L8UKfi64L+/d6c+qfFhb0uSoleFS5LmDrvLOd9qtWr61ii38rrSY8RodQruM22Hvz/NawC4iixmBwAAAAAAFC4oKMiQpKnrt5odxenYwQNaMna0ZkRES5IO2fbqveen6B8Ll1zw+VXN8PtBhY17qNhNScMwZLEU/kfiw7/8pJXPPKXn1m8pbsRryl//ncK1nGNJPkP9/fPMzgEAZRnXhgAAAAAAroj9bN4l7zz+Le4HhY0fo5cH9tGiUcN1ICFOZzIz9PLAvjr6659XciRui9TyiedObe/avEHz7huiV+7op8gVywrdc8kjo+RwOJzXl0hS7plsfTpnlmbdNVCzQgZow8I5ys8711ssuNLkh62bNTPkdu3b8dUl1zUMQxmpqfrmk4/VfuAdhe5f1JwAAKD4uDYEAAAAAFBsWafSteOj1fLv3vOiZ7nZp3XHk1NUp8kNilm9UluXvK4Jy95Ry85dZIuJVr1mLSRJe6K3qU3f/so8eUKbQxdozMIlqtWokU4kJxe674Rl7yhs/JgLTl5vmD9HZ3Nz9ET4auWfzdNH/5quL1e+rb7/GOecc+z3A5ry0acyjIvXTN73o96aNEGS1KxDoEImX/pqDHdyomz6bv1axbz37mRJr0lymJ0HAMoqTl4DAAAAAIplRnBPvT5yuKrVqXtBg7hAq6Duqt24qY4f+l0+vr7OJm+bfrfLtj1aknQmM0O/70nQLb1uk1e58rJYrUr/46h8fCu5dZd1bnaWftweo+Cxj8q3alX51aylHvc/qL0xX14wr9Pgu1Xep4K8K1x8Urxhq9b652dRmrhilc7m5Gjz4oWX3OtKcnqq2k2acmWIG2JWhcuRnz/T39+fQ4EAcBXxQxYAAAAAUCyump0x76/Urk3/VoOWN8nHt5IMx7l32zXrEKizOTk6sv8XHd3/s5p1CFTFKn6SpCHPvqjP31yq2DUfaNDjT6lBy1ZFypJ58qQMw6Hq9eo7P/OrWUtZ6WkXzKtWt+5l17FYrLqu4fXqPWqMVk+fpsFPPXPRHF8/v2LnBAAARcfJawAAAADwbOOCxz1mdga3nU47qehV7+gfi5Zo2IxX1W7AIOczq5eXAnr31Y9fRWtvdJTa9Lvd+cy/e089Eb5azQM7a+2sl4u8X6WqVWWxWHQq5Q/nZxmpx+V3Xc0L5lksRfxjsMWicuXLF/q4uDkBAEDR0bwGAAAAAA8WGxu7rE3/gWbHcJvDbpdhGDp55LDOnM7U12s+vOB5m379tSd6m04cTlKzDoGSpIzUVB1MTJBhOFSjXgPl5+YWeb+KVfzUPLCzIpYvUXZGhjJPpOqrD1apbfCAItWnHT2i+M8/U15OjjJSUxW98h359+h1yblXkhMAABQd14YAAAAAAEqcX81a6nbvcK2ePk2VqlVT13vu1f7vvnU+r92kqSpWrqwb2nWQ1XruXJXhsGvT6/OUdvSIqtetp5CnL/3CxMKEPP2cNi2aqwUPDFV5H2+17T9QQfcOL1Jt+QoV9P3G9dq4aK68K1TUzT17X/Ie78vlzD51Su9OfVLjw952KzcAALg0i9kBAAAAAACXt+F4hmF2hpJ2NjdH8+4boocWvKHajZuYHQfFtGvzBsW8967OnM5Up5Ah6jtmrNmRSsWcIYPksNuVnp7uY7PZ8szOAwBlFSevAQAAAMCDBQUFhUUsDVXw+IlmRykx2RkZ+nrth6rf4iYa19ewzJMntDl0gcYsXKJajRrpRHKy2ZEAAGUMd14DAAAAgGcbGx+xxewMJWrefUP0n927NPipZ8yOgivgVa68LFar0v84Kh/fSqrfoqXZkQAAZQwnrwEAAAAAperFzZFmR0AJ8PXz05BnX9Tnby5V7JoPNOjxp9SgZSuzYwEAyhBOXgMAAAAAgGLx795TT4SvVvPAzlo762Wz45SawJAhslgs82w2m93sLABQltG8BgAAAAAAbstITdXBxAQZhkM16jVQfm6u2ZFKTY8Ro/X4smXTJNG8BoCriGtDAAAAAACA2wyHXZten6e0o0dUvW49hTz9rCQp+9QpvTv1SY0Pe9vkhACAa53F7AAAAAAAgMIFBQUZkjR1/VazowA4T86xJJ+h/v55ZucAgLKMa0MAAAAAAADc8N36tQqdMGGy6KsAwFXFD1kAAAAAAAA3xKwKlyM/f6a/vz/XsQLAVUTzGgAAAAAAAADgcWheAwAAAIBnGxc87jGzMwAAAJQ6mtcAAAAA4MFiY2OXtek/0OwYAAAApY7mNQAAAAAAAADA49C8BgAAAAAAAAB4HJrXAAAAAODBgoKCwiKWhpodAwAAoNTRvAYAAAAAzzY2PmKL2RkAAABKHc1rAAAAAAAAAIDHoXkNAAAAAADghsCQIbJYLPNsNpvd7CwAUJZZzA4AAAAAAChcUFCQIUlT1281OwqA8+QcS/IZ6u+fZ3YOACjLOHkNAAAAAAAAAPA4NK8BAAAAAAAAAB6H5jUAAAAAAIAbvlu/VqETJkwWfRUAuKr4IQsAAAAAAOCGmFXhcuTnz/T39y9ndhYAKMtoXgMAAAAAAAAAPA7NawAAAADwbOOCxz1mdgYAAIBSR/MaAAAAADxYbGzssjb9B5odAwAAoNRxNxMAAAAAXAM2LZijlAO/uZw3YOIk1Wve0jneEjpfR/fvd1kXPP4xNWzl7xxHLHldyT/tc1nX9+FxanTL35zjyOVLdGjvHpd1vR96WE3btHOOt729XAfj49yui373bf1n9/cu63o+OFo3dgh0jr96/13t3/mty7pbh49Qi85dnePYj97XT7E7XNYFDb1PN3Xr7nZdp7uG6OZefZzjnevWaG90lMu69oPuVJt+tzvHuzauV0JkhMu6Nv36q/2gwc5xfMQW7d680WXdzb1uU6e77nGO90Z9oZ3rP3FZd1O3WxU0dLhzvG9HjL7++EOXdc0DO6n7A6Oc4/07v9FX7690u+5A3G5FvfOWy7qmbdup9+iHneNDexMVuXypc+yw212uAQC4cjSvAQAAAMDDOez5Ab/s/Gbl2ZwzbVzN/W7DJ2PumPSMs5v7yzexb+VmZwderkaSvl//ybj6LVp+XTDeF/vV0tysrCBXdd+sW/N4w9b+0QXjH7+Kfj3ndGZPV3XfrlszufEtAZHOuuhtc7MzMvq5W7cnKnJW9ql0l0fTd65b82zTtu22FIwTv4j4V1ZaWojL/T79eHqzjoHrC8bxEVtfPH3yxN9d1619uUWXrmvcrdv570/mtO7ec1XBePfWjVMyjx8f4aru+w3rQgNu6/ums27zhonpfxx9+HI1krRr04blbW8fuPjPdT59+OTh5Imu6n7YunFlxztD5jpzb1g3IvXQwSmu6uI/y/y4y5ChrzjrPv3k76mHDr7oqi47I31Dt/seeKFg/O36NXemHjr4yuVqLlm3bk2/1EMH516uRpKy0k9G9HxwtPP72fnp2u6phw4uPn+OYRjHbDbbWVdrAQCKz2J2AAAAAACAa926dbvBbrdXcjUvKyvrt8TExKyCcWBgYFMvL6/Krup8fHwOREdHny4Yd+7cuYnFYqniqs5qtR6MjY3NLBh37dq1sWEYfq7q8vLyDu3evftUwbhbt26N7HZ7VXfrOnbseH25cuWquao7c+ZMUnx8fHrBuFOnTg2tVmt1V3VeXl7JO3bsSCsYd+nSpYGkGq7qJB3+5ptvTrpbl5eXd3T37t2pBeP27dvX8/b2rumqrly5cn9s3779eMG4U6dOdaxWa21XdQ6H49jOnTtTCsa33nprrfz8/LpF2C91+/btR8/LWdPb27ueqzqr1XoiNjb2SMG4S5cuNSQ1KELOtJ07dyYXjNu0aVOtYsWK17tb1759+6re3t6NXNV5eXmd2rFjx6GCcVBQUBWHw9Hk/DmnT59O3rNnT9pFxQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKXv/wGbJiJ8IxMSQgAAAABJRU5ErkJggg==","height":1094},"elements":{"elements":{"QuxAPESuAl939574":{"fontStyle":{"fontFamily":"黑体","size":14},"props":{"zindex":31},"dataAttributes":[],"points":[{"x":385,"y":205.2881851196289},{"x":385,"y":205.2881851196289}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":385,"y":157.7881851196289,"angle":4.71238898038469,"id":"syaefAixCt765305"},"textPos":{"t":16,"pos":"in","x":389,"y":195.78818511962893},"to":{"x":385,"y":252.7881851196289,"angle":1.570796326794897,"id":"LrfDSUkhXm666325"},"id":"QuxAPESuAl939574","text":"① Transition Condition is T1/T2","locked":false,"group":""},"agukVAlhCz403244":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","x":0,"h":"h","y":0},"text":"The last frame stays for 1.2 s"}],"anchors":[],"title":"","fontStyle":{"size":12},"dataAttributes":[],"props":{"zindex":53,"w":92.72222222222172,"x":865.1388888888896,"h":29.370370370370438,"y":726.8055477142334,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardText","fillStyle":{},"theme":{},"id":"agukVAlhCz403244","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"category":"standard","locked":false,"group":""},"SypMMbosuj035158":{"fontStyle":{"fontFamily":"黑体","size":14},"props":{"zindex":32},"dataAttributes":[],"points":[{"x":385,"y":351.64409255981445},{"x":225.01388549804688,"y":351.64409255981445}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":385,"y":303.7881851196289,"angle":4.71238898038469,"id":"LrfDSUkhXm666325"},"textPos":{"t":35,"pos":"in","x":265,"y":351},"to":{"x":225.01388549804688,"y":399.5,"angle":1.570796326794897,"id":"DSkMisjWOm425681"},"id":"SypMMbosuj035158","text":"② Transition Condition is T1","locked":false,"group":""},"LrfDSUkhXm666325":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"Sub-State Machine"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":16,"color":"255,255,255"},"dataAttributes":[{"name":"序号","id":"dTtcktIOLC729055","type":"number","category":"default","value":""},{"name":"名称","id":"sGiYRnWpmR375338","type":"string","category":"default","value":""},{"name":"所有者","id":"ekqNGGdDgI080445","type":"string","category":"default","value":""},{"name":"连接","id":"FSvorrBldx827818","type":"link","category":"default","value":""},{"name":"便笺","id":"TrWluBgnfu492975","type":"string","category":"default","value":""}],"props":{"zindex":28,"w":100,"x":335,"h":51,"y":252.7881851196289,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{"lineColor":"125,200,227","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{"color":"51,102,153","type":"solid"},"theme":{},"id":"LrfDSUkhXm666325","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"MATMaFbgjN204862":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State A
Animation length is 3 s
Exit count is (3 - 0.3)/3 = 0.9 times"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":14},"dataAttributes":[{"name":"序号","id":"pRACDmrZfe160856","type":"number","category":"default","value":""},{"name":"名称","id":"vZKjYqFBFO852812","type":"string","category":"default","value":""},{"name":"所有者","id":"diNTnKgWyI303854","type":"string","category":"default","value":""},{"name":"连接","id":"nREDFhSrMO478942","type":"link","category":"default","value":""},{"name":"便笺","id":"tsEPHrBSOp105062","type":"string","category":"default","value":""}],"props":{"zindex":34,"w":258.38571240283824,"x":31.243917226791382,"h":64.7673568725586,"y":636.8489646911621,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"MATMaFbgjN204862","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"GzvfAXRGKz669720":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"fontFamily":"黑体","size":24,"vAlign":"top"},"title":"矩形","dataAttributes":[{"name":"序号","id":"hptjuCleeV248452","category":"default","type":"number","value":""},{"name":"名称","id":"axXhaRVApX210753","category":"default","type":"string","value":""},{"name":"所有者","id":"YasBcpEkcp107705","category":"default","type":"string","value":""},{"name":"连接","id":"SIvgjbwyhD242661","category":"default","type":"link","value":""},{"name":"便笺","id":"oLstSpiqzZ729966","category":"default","type":"string","value":""}],"props":{"zindex":26,"w":473.0451383590698,"h":255.1562614440918,"x":148.4774308204651,"angle":0,"y":222.79110697696083},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineStyle":"dashed","lineColor":"59,59,59","lineWidth":2},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"178,235,242","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"GzvfAXRGKz669720","category":"basic","locked":false,"group":""},"vGOfwwnBKx332586":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State C"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":16,"color":"255,255,255"},"dataAttributes":[{"name":"序号","id":"srOHaCwHbp078562","type":"number","category":"default","value":""},{"name":"名称","id":"NoozbVQUGa425311","type":"string","category":"default","value":""},{"name":"所有者","id":"wSmxXkEpQc737187","type":"string","category":"default","value":""},{"name":"连接","id":"NsYavWwerg813225","type":"link","category":"default","value":""},{"name":"便笺","id":"OlNLtfaCzX103361","type":"string","category":"default","value":""}],"props":{"zindex":30,"w":100,"x":492.0138854980469,"h":51,"y":399.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{"lineColor":"125,200,227","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{"color":"51,102,153","type":"solid"},"theme":{},"id":"vGOfwwnBKx332586","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"eluChmQMLQ844377":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":14,"bold":true},"dataAttributes":[{"name":"序号","id":"wcvfIDBMKB939913","type":"number","category":"default","value":""},{"name":"名称","id":"gRnbANycCM219711","type":"string","category":"default","value":""},{"name":"所有者","id":"dWIDtjxoTO597808","type":"string","category":"default","value":""},{"name":"连接","id":"BRTDyVwwfR565453","type":"link","category":"default","value":""},{"name":"便笺","id":"KIWkNhCXeZ891688","type":"string","category":"default","value":""}],"props":{"zindex":38,"w":139.77777099609375,"x":481.74479818344116,"h":70,"y":817.2326431274414,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"eluChmQMLQ844377","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"wHvLfYVkjp576282":{"fontStyle":{"fontFamily":"黑体","size":13,"color":"255,0,0"},"props":{"zindex":39},"dataAttributes":[],"points":[{"x":375.625,"y":852.2326431274414},{"x":375.625,"y":852.2326431274414}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":269.50520181655884,"y":852.2326431274414,"angle":3.141592653589793,"id":"eylwQzJALs204157"},"to":{"x":481.74479818344116,"y":852.2326431274414,"angle":0,"id":"eluChmQMLQ844377"},"id":"wHvLfYVkjp576282","text":"Transition Period\nis set to 0.3 times","locked":false,"group":""},"eJraDwpUGc777013":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"fontFamily":"黑体","size":24,"vAlign":"top"},"title":"矩形","dataAttributes":[{"name":"序号","id":"FGkIfyXTbq664794","category":"default","type":"number","value":""},{"name":"名称","id":"VFdjstByFV351886","category":"default","type":"string","value":""},{"name":"所有者","id":"qpqwyqZuMp850782","category":"default","type":"string","value":""},{"name":"连接","id":"YCJufzsDap172106","category":"default","type":"link","value":""},{"name":"便笺","id":"vBlcmZhZzw375489","category":"default","type":"string","value":""}],"props":{"zindex":56,"w":229.5225691795349,"h":126.1562614440918,"x":741.4774308204651,"angle":0,"y":862.8437385559082},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineStyle":"dashed","lineColor":"59,59,59","lineWidth":2},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"178,235,242","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"eJraDwpUGc777013","category":"basic","locked":false,"group":""},"yondKCrIIK620777":{"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":793,"y":721},"to":{"x":868.5,"y":721},"id":"yondKCrIIK620777","text":"","locked":false,"props":{"zindex":52},"dataAttributes":[],"group":"","points":[{"x":830.75,"y":721},{"x":830.75,"y":721}]},"UcVpfHyYlq805818":{"fontStyle":{"fontFamily":"黑体","size":13,"color":"244,67,54"},"props":{"zindex":36},"dataAttributes":[],"points":[{"x":386.09659119503397,"y":671.8489646911621},{"x":386.09659119503397,"y":671.8489646911621}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":290.4483842066268,"y":671.8489646911621,"angle":3.141592653589793,"id":"MATMaFbgjN204862"},"to":{"x":481.74479818344116,"y":671.8489646911621,"angle":0,"id":"NZAPcPzVpQ732100"},"id":"UcVpfHyYlq805818","text":"Transition Period\nis set to 0.3 s","locked":false,"group":""},"TjeXVcYEDA847150":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"fontFamily":"黑体","size":24,"vAlign":"top"},"title":"矩形","dataAttributes":[{"name":"序号","id":"ZEhNvlenyx576941","category":"default","type":"number","value":""},{"name":"名称","id":"aeGgSdyumS256807","category":"default","type":"string","value":""},{"name":"所有者","id":"yGsvNFuzdg690269","category":"default","type":"string","value":""},{"name":"连接","id":"nAykxhgNfH631668","category":"default","type":"link","value":""},{"name":"便笺","id":"inzdjBpjRE331276","category":"default","type":"string","value":""}],"props":{"zindex":47,"w":229.5225691795349,"h":126.1562614440918,"x":741.4774308204651,"angle":0,"y":644.8437385559082},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineStyle":"dashed","lineColor":"59,59,59","lineWidth":2},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"178,235,242","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"TjeXVcYEDA847150","category":"basic","locked":false,"group":""},"syaefAixCt765305":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State A"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":16},"dataAttributes":[{"name":"序号","id":"COcKBsDOgf128282","type":"number","category":"default","value":""},{"name":"名称","id":"ZftffmbwyT593976","type":"string","category":"default","value":""},{"name":"所有者","id":"ZESvkXVynb543594","type":"string","category":"default","value":""},{"name":"连接","id":"wTTUNJrNDJ147164","type":"link","category":"default","value":""},{"name":"便笺","id":"iczxGnUMNn801395","type":"string","category":"default","value":""}],"props":{"zindex":64,"w":100,"x":335,"h":51,"y":106.7881851196289,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"syaefAixCt765305","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"IUAYCHpwvR585542":{"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":792,"y":945},"to":{"x":867.5,"y":945},"id":"IUAYCHpwvR585542","text":"","locked":false,"props":{"zindex":58},"dataAttributes":[],"group":"","points":[{"x":829.75,"y":945},{"x":829.75,"y":945}]},"eylwQzJALs204157":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State A
Animation length is 3 s
Exit count is 0.7 times"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":14},"dataAttributes":[{"name":"序号","id":"pYeCfEHYee569827","type":"number","category":"default","value":""},{"name":"名称","id":"kTsRKlpAAQ519464","type":"string","category":"default","value":""},{"name":"所有者","id":"zyFOmOanwF025376","type":"string","category":"default","value":""},{"name":"连接","id":"OJYzTJqrzL111338","type":"link","category":"default","value":""},{"name":"便笺","id":"jEaMAeEiFr520764","type":"string","category":"default","value":""}],"props":{"zindex":37,"w":239.01388549804688,"x":30.491316318511963,"h":70,"y":817.2326431274414,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"eylwQzJALs204157","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"EuWypzMFNb181054":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","x":0,"h":"h","y":0},"text":"Loops for 1.2 s."}],"anchors":[],"title":"","fontStyle":{"size":12},"dataAttributes":[],"props":{"zindex":61,"w":89.01851851851802,"x":866.9907407407414,"h":23.000000000000114,"y":944.8055477142336,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardText","fillStyle":{},"theme":{},"id":"EuWypzMFNb181054","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"category":"standard","locked":false,"group":""},"DbetzvYcev176085":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","x":0,"h":"h","y":0},"text":"Plays for 3 s."}],"anchors":[],"title":"","fontStyle":{"size":12},"dataAttributes":[],"props":{"zindex":60,"w":94.68518518518499,"x":766.6666666666667,"h":23,"y":944.8055477142336,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardText","fillStyle":{},"theme":{},"id":"DbetzvYcev176085","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"category":"standard","locked":false,"group":""},"XPNpRTzEXZ529813":{"fontStyle":{"fontFamily":"黑体","size":14},"props":{"zindex":33},"dataAttributes":[],"points":[{"x":385,"y":351.64409255981445},{"x":542.0138854980469,"y":351.64409255981445}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":385,"y":303.7881851196289,"angle":4.71238898038469,"id":"LrfDSUkhXm666325"},"textPos":{"t":35,"pos":"in","x":502,"y":351},"to":{"x":542.0138854980469,"y":399.5,"angle":1.570796326794897,"id":"vGOfwwnBKx332586"},"id":"XPNpRTzEXZ529813","text":"③ Transition Condition is T2","locked":false,"group":""},"DSkMisjWOm425681":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":16,"color":"255,255,255"},"dataAttributes":[{"name":"序号","id":"GnMJNreToX513402","type":"number","category":"default","value":""},{"name":"名称","id":"NyedfxulLk937048","type":"string","category":"default","value":""},{"name":"所有者","id":"TCeMmDPXDJ177212","type":"string","category":"default","value":""},{"name":"连接","id":"lAMTzTrEpp916241","type":"link","category":"default","value":""},{"name":"便笺","id":"XYMjxIlXzv579719","type":"string","category":"default","value":""}],"props":{"zindex":29,"w":100,"x":175.01388549804688,"h":51,"y":399.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{"lineColor":"125,200,227","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{"color":"51,102,153","type":"solid"},"theme":{},"id":"DSkMisjWOm425681","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"jpuvdHUhVy414669":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State A"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"bold":true},"dataAttributes":[{"name":"序号","id":"riQgeTtery132433","type":"number","category":"default","value":""},{"name":"名称","id":"bJfLOfXqib044660","type":"string","category":"default","value":""},{"name":"所有者","id":"muQlwSsYUd247625","type":"string","category":"default","value":""},{"name":"连接","id":"loILkILonf339531","type":"link","category":"default","value":""},{"name":"便笺","id":"urwjujTXHo818864","type":"string","category":"default","value":""}],"props":{"zindex":57,"w":126.47743082046509,"x":793,"h":37.767356872558594,"y":887.2326431274414,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"jpuvdHUhVy414669","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"iMNighSMHQ583553":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","x":0,"h":"h","y":0},"text":"Plays for 3 s"}],"anchors":[],"title":"","fontStyle":{"size":12},"dataAttributes":[],"props":{"zindex":50,"w":84,"x":777.3518518518517,"h":39,"y":713.4814814814815,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardText","fillStyle":{},"theme":{},"id":"iMNighSMHQ583553","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"category":"standard","locked":false,"group":""},"rNUmsWvSyG945511":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State A"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"bold":true},"dataAttributes":[{"name":"序号","id":"WSIubHDThP178530","type":"number","category":"default","value":""},{"name":"名称","id":"PhXHvppcko311621","type":"string","category":"default","value":""},{"name":"所有者","id":"KHOHNXqmnP773144","type":"string","category":"default","value":""},{"name":"连接","id":"bicQHSmgPf506557","type":"link","category":"default","value":""},{"name":"便笺","id":"rjugFMfNLN374927","type":"string","category":"default","value":""}],"props":{"zindex":48,"w":126.47743082046509,"x":793,"h":37.767356872558594,"y":663.8489646911621,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"rNUmsWvSyG945511","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"wfKxHaQHWV679637":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":""}],"anchors":[{"x":"0","y":"h*0.5"},{"x":"0","y":"h*0.5"},{"x":"0","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"title":"注释","fontStyle":{},"dataAttributes":[{"name":"序号","id":"coAPIVfOPt538260","type":"number","category":"default","value":""},{"name":"名称","id":"IkKOvcwkHP406545","type":"string","category":"default","value":""},{"name":"所有者","id":"UqxLxMoiiK828196","type":"string","category":"default","value":""},{"name":"连接","id":"PTgmSFCyLY968905","type":"link","category":"default","value":""},{"name":"便笺","id":"guBUjfiJTw426706","type":"string","category":"default","value":""},{"name":"成本","id":"JyDKRbpXuh400233","type":"number","category":"default","value":""},{"name":"时间","id":"tTKkVDVKPD165262","type":"number","category":"default","value":""},{"name":"部门","id":"BQqcsMOTTQ865175","type":"string","category":"default","value":""},{"name":"输入","id":"dBoLsFiMjh541980","type":"string","category":"default","value":""},{"name":"输出","id":"qMRbAwdWtn409176","type":"string","category":"default","value":""},{"name":"风险","id":"uMJLGXKpVU618435","type":"string","category":"default","value":""},{"name":"备注","id":"TOMQrNnuyk448030","type":"string","category":"default","value":""}],"props":{"zindex":49,"w":44,"x":874.5,"h":47,"y":677.5,"angle":4.71238898038469},"path":[{"fillStyle":{"type":"none"},"actions":[{"x":"Math.min(w/6, 20)","action":"move","y":"0"},{"x":"0","action":"line","y":"0"},{"x":"0","action":"line","y":"h"},{"x":"Math.min(w/6, 20)","action":"line","y":"h"}]},{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"annotation","fillStyle":{},"theme":{},"id":"wfKxHaQHWV679637","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"flow","locked":false,"group":""},"cOovlqhAvB974537":{"fontStyle":{"size":12},"props":{"zindex":54},"dataAttributes":[],"points":[{"x":1048,"y":707.9218692779541},{"x":1048,"y":707.9218692779541}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":971,"y":707.9218692779541,"angle":3.141592653589793,"id":"TjeXVcYEDA847150"},"to":{"x":1125,"y":707.9218692779541},"id":"cOovlqhAvB974537","text":"Start Transitioning","locked":false,"group":""},"vfMAMAEdfU585662":{"fontStyle":{"size":12},"props":{"zindex":62},"dataAttributes":[],"points":[{"x":1049.5,"y":925.9218692779541},{"x":1049.5,"y":925.9218692779541}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":971,"y":925.9218692779541,"angle":3.141592653589793,"id":"eJraDwpUGc777013"},"to":{"x":1128,"y":925.9218692779541},"id":"vfMAMAEdfU585662","text":"Start Transitioning","locked":false,"group":""},"grihhKRoFz459452":{"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":869,"y":945},"to":{"x":919.4774308204651,"y":945},"id":"grihhKRoFz459452","text":"","locked":false,"props":{"zindex":59},"dataAttributes":[],"group":"","points":[{"x":894.2387154102325,"y":945},{"x":894.2387154102325,"y":945}]},"HMrWDfiIXv077034":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"bold":true},"dataAttributes":[{"name":"序号","id":"CmutjSuqBd983227","type":"number","category":"default","value":""},{"name":"名称","id":"AhcGkGJIFf625730","type":"string","category":"default","value":""},{"name":"所有者","id":"ufAieBEiym148768","type":"string","category":"default","value":""},{"name":"连接","id":"dYzQnpDfCR698604","type":"link","category":"default","value":""},{"name":"便笺","id":"naBDauKLHr554632","type":"string","category":"default","value":""}],"props":{"zindex":55,"w":126.47743082046509,"x":1130,"h":37.767356872558594,"y":689.0381908416748,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"HMrWDfiIXv077034","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"NZAPcPzVpQ732100":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":14,"bold":true},"dataAttributes":[{"name":"序号","id":"UBWmkFvEsl100123","type":"number","category":"default","value":""},{"name":"名称","id":"daeLZQzlZu890923","type":"string","category":"default","value":""},{"name":"所有者","id":"rwaZGiiTiX954509","type":"string","category":"default","value":""},{"name":"连接","id":"FJFnjBxCGz270128","type":"link","category":"default","value":""},{"name":"便笺","id":"YJQsKopFiI143414","type":"string","category":"default","value":""}],"props":{"zindex":35,"w":139.77777099609375,"x":481.74479818344116,"h":70,"y":636.8489646911621,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"NZAPcPzVpQ732100","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"pexwZPjfQk789747":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"State B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"bold":true},"dataAttributes":[{"name":"序号","id":"TINjkTVodC371430","type":"number","category":"default","value":""},{"name":"名称","id":"KvaEbizvRU877134","type":"string","category":"default","value":""},{"name":"所有者","id":"OdHfVqpYja209101","type":"string","category":"default","value":""},{"name":"连接","id":"gJJETHlUJD839151","type":"link","category":"default","value":""},{"name":"便笺","id":"bEdRACVLhP446660","type":"string","category":"default","value":""}],"props":{"zindex":63,"w":126.47743082046509,"x":1130,"h":37.767356872558594,"y":907.0381908416748,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"pexwZPjfQk789747","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""}},"page":{"padding":20,"backgroundColor":"transparent","orientation":"portrait","gridSize":15,"width":1622,"showGrid":false,"lineJumps":false,"height":2346}}},"meta":{"exportTime":"2021-12-13 14:59:06","member":"61b6e52c7d9c086a676ad789","diagramInfo":{"creator":"61b6e52c7d9c086a676ad789","created":"2021-12-13 14:16:39","modified":"2021-12-13 14:58:22","title":"flow-chart","category":"flow"},"id":"61b6e5470e3e7404518a1830","type":"ProcessOn Schema File","version":"1.0"}} \ No newline at end of file diff --git a/versions/4.0/en/animation/marionette/index.md b/versions/4.0/en/animation/marionette/index.md new file mode 100644 index 0000000000..054b9d431c --- /dev/null +++ b/versions/4.0/en/animation/marionette/index.md @@ -0,0 +1,42 @@ +# Marionette Animation System + +Cocos Creator 3.5 introduces a new **Marionette** animation system that controls the skeletal animation of objects via state machines, enabling an automated, and reusable animation process. + +To distinguish from the animation system from prior to v3.4 which is also accessible in the later versions of Cocos Creator, the new animation system will be referred as the **Marionette** system in this documentation while the animation system from prior to v3.4 will be referred as the **legacy** system. In general, the distinctions between the two include: + +- **Legacy**: Powered by the **Animation** component, the legacy system animates objects with animation states containing animation clips (which are keyframe animations created in Cocos Creator) and skeletal animations imported from external sources in the format of `.fbx`, `.gltf` and `.glb`. + +- **Marionette**: Powered by the **Animation Controller** component, the Marionette system animate objects with pre-constructed animation graphs, which functions as a controller of multiple animation states that can only contain skeletal animations imported from external sources in the format of `.fbx`, `.gltf` and `.glb`. + +## Content + +The Marionette system consists of four modules: + +- [Animation Graph Assets](animation-graph.md) +- [Animation Controller Reference](animation-controller.md) +- [Animation Graph Panel](animation-graph-panel.md) +- [Animation Graph Layer](animation-graph-layer.md) +- [Animation State Machine](animation-graph-basics.md) +- [State Transition](state-transition.md) +- [Animation Mask](animation-mask.md) +- [Animation Graph Variants](animation-variant.md) +- [Procedural Animation](./procedural-animation/index.md) + +## Terminology + +| Function | Description | +| :--- | :--- | +| Animation Graph Assets | Asset that contains the flow chart of animation states, Can be created in the **Assets** panel. For more information, please refer to [Animation Graph Assets](animation-graph.md). | +| Animation Controller Component | References to the Animation Graph Assets and applies it to a node in the scene. | +| Animation Graph Panel |Animation clips can be imported and stored as animation states, which can be assembled into a flow chart to indicate the transition from one clip to another. This is done in the Animation Graph panel once an Animation Graph asset is created. For more information, please see [Animation Graph Panel](animation-graph-panel.md). | +| State | A state is an action that is usually portrayed by an animation clip, such as standby, walk, move, attack, etc. This should be distinguished from an [animation state](../animation-state.md) as used in the legacy system. | +| State Transition | An animated actor is likely to perform multiple actions, each portrayed by an animation clip and registered as an animation state. A [state transition](state-transition.md) contains the logical check for switching between different states. For instance, to create the death animation of a walking character, a state transition is needed to switch the walk state to the death state. | +| Animated State Machine | Collectively, all the animation states and the transitions between them can be constructed in a flow chart, which is known as an [Animation State Machine](animation-graph-basics.md). | + +A state machine as displayed in the Animation Graph panel is demonstrated in the screenshot below. In it, each blue rectangle represents a state, while the arrows between them the transitions between different states. + +![example](animation-graph-basics/example.png) + +## Example Project + +Creator provides the [Ms.Amoy](https://github.com/cocos-creator/example-marionette) demo project, which demonstrates the use of the Marionette animation system, can be downloaded and used as needed. diff --git a/versions/4.0/en/animation/marionette/preview-bar/end.png b/versions/4.0/en/animation/marionette/preview-bar/end.png new file mode 100644 index 0000000000..c5c1cb1ff2 Binary files /dev/null and b/versions/4.0/en/animation/marionette/preview-bar/end.png differ diff --git a/versions/4.0/en/animation/marionette/preview-bar/next.png b/versions/4.0/en/animation/marionette/preview-bar/next.png new file mode 100644 index 0000000000..2a04bda7d8 Binary files /dev/null and b/versions/4.0/en/animation/marionette/preview-bar/next.png differ diff --git a/versions/4.0/en/animation/marionette/preview-bar/pause.png b/versions/4.0/en/animation/marionette/preview-bar/pause.png new file mode 100644 index 0000000000..059f6da6b9 Binary files /dev/null and b/versions/4.0/en/animation/marionette/preview-bar/pause.png differ diff --git a/versions/4.0/en/animation/marionette/preview-bar/play.png b/versions/4.0/en/animation/marionette/preview-bar/play.png new file mode 100644 index 0000000000..fdaf9c5103 Binary files /dev/null and b/versions/4.0/en/animation/marionette/preview-bar/play.png differ diff --git a/versions/4.0/en/animation/marionette/preview-bar/prev.png b/versions/4.0/en/animation/marionette/preview-bar/prev.png new file mode 100644 index 0000000000..d8de20e414 Binary files /dev/null and b/versions/4.0/en/animation/marionette/preview-bar/prev.png differ diff --git a/versions/4.0/en/animation/marionette/preview-bar/start.png b/versions/4.0/en/animation/marionette/preview-bar/start.png new file mode 100644 index 0000000000..be3e88ab98 Binary files /dev/null and b/versions/4.0/en/animation/marionette/preview-bar/start.png differ diff --git a/versions/4.0/en/animation/marionette/preview-bar/stop.png b/versions/4.0/en/animation/marionette/preview-bar/stop.png new file mode 100644 index 0000000000..623a23e7a7 Binary files /dev/null and b/versions/4.0/en/animation/marionette/preview-bar/stop.png differ diff --git a/versions/4.0/en/animation/marionette/preview-bar/time.png b/versions/4.0/en/animation/marionette/preview-bar/time.png new file mode 100644 index 0000000000..3ee904d103 Binary files /dev/null and b/versions/4.0/en/animation/marionette/preview-bar/time.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/auxiliary-curve/img/auxiliary-curve.png b/versions/4.0/en/animation/marionette/procedural-animation/auxiliary-curve/img/auxiliary-curve.png new file mode 100644 index 0000000000..747d9aa672 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/auxiliary-curve/img/auxiliary-curve.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/auxiliary-curve/img/inspector-property.png b/versions/4.0/en/animation/marionette/procedural-animation/auxiliary-curve/img/inspector-property.png new file mode 100644 index 0000000000..d52b7978a0 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/auxiliary-curve/img/inspector-property.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/auxiliary-curve/index.md b/versions/4.0/en/animation/marionette/procedural-animation/auxiliary-curve/index.md new file mode 100644 index 0000000000..b43ac23e4d --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/auxiliary-curve/index.md @@ -0,0 +1,58 @@ +# Auxiliary Curves (experimental) + +![auxiliary-curve.png](./img/auxiliary-curve.png) + +> **Note**: auxiliary curve is an experimental feature, please use it with caution.Cocos Creator looks forward to your feedback. + +## Motivation + +Sometimes, when a character performs a certain action, it is accompanied by changes in some other associated attributes in addition to the visual changes in the bones. For example: + +- Selective use of IK to correct foot position: when the character is standing in place; not when the character is walking. + +- When starting a jump from a run, it may be necessary to decide whether to use a left-footed jump or a right-footed jump, depending on the orientation of the feet at the start of the jump. This is essentially a change in the orientation of the feet that needs to be reflected in the running movement. + +- Along with the movement, there are texture changes, particle effect changes, sound changes, and so on. + +Some of these changes can be visualized by [Embedded Player](../../../embedded-player.md). But sometimes the embedded player is not up to the task: + +- The changes described by the embedded player can't participate in the blending in the same way that bones can. + + For example, when the action fades out, the embedded player stops abruptly; the embedded players for two actions are independent - even though they may be describing color changes in the same material, their effects cannot be blended with the action. + +- Changes in the values described by the embedded player cannot be used directly in the animation diagram. + It is not possible to read the results of the embedded player directly into the animation diagram. Even if it were possible, it would require a tedious setup step. + +> This is not to say that we no longer encourage the use of embedded players. When you don't have the blending needs described above, the embedded player is still a quick way to attach other effects to your actions. + +Therefore, Cocos Creator introduces a new feature to solve the above problem. + +## Concept: Auxiliary Curves + +An **Auxiliary Curve** is a named **float** curve whose value is used as an attribute of the pose in addition to the bones: + +- When sampling a pose from an animation clip, the auxiliary curve is also sampled; + +- When a pose is blended, the value of an auxiliary curve of the same name is also blended. + +As their name implies, auxiliary curves play a supporting role in skeletal animation. The source and application of auxiliary curves are defined by the user. + +> The main difference between auxiliary curves and skeletal animations is that the values of skeletal animations are written to the scene when applied, driving the bones to produce visual model animations, whereas auxiliary curves do not produce direct visual effects, they are only readable, and it is up to the user to determine their usefulness. + +## Editing Auxiliary Curves + +In the Animation Clip Editor, you can edit the [Auxiliary Curve](../../../animation-auxiliary-curve.md). + +In the pose graph, you can set the auxiliary curve nodes with [Set Auxiliary Curve Node](../pose-graph/pose-nodes/modify-pose.md##Set%20auxiliary%20curve) to set or modify the current value of the auxiliary curve for a pose. + +## Using Auxiliary curves + +Auxiliary curves can be read and used in animation graphs and animation controllers: + +- In [Transition Conditions](../../state-transition.md). + +- Some of the properties of the pose nodes in the pose map can be specified by the current value of the auxiliary curve. Example: + +- [apply-transition-node](../pose-graph/pose-nodes/modify-pose.md#Apply%20Transformations) The intensity value of [Apply Transformations Node](../pose-graph/pose-nodes/modify-pose.md#Apply%20Transformations) can be specified using an auxiliary curve. + +- [Animation Controller](../../animation-controller.md) component provides methods to read the current value of the auxiliary curve. diff --git a/versions/4.0/en/animation/marionette/procedural-animation/enabling.md b/versions/4.0/en/animation/marionette/procedural-animation/enabling.md new file mode 100644 index 0000000000..8a50c3501b --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/enabling.md @@ -0,0 +1,14 @@ +# Enable Procedural Animation + +Procedural animation is currently an experimental feature, to enable it, find the **Laboratory** feature in **Preferences** and turn on the following options: + +- Animation auxiliary curve +- Enable Pose-Express Function + +![enable.png](./index/enable.png) + +Open the **Project Settings** panel in the **Project** menu , and navigate to the **Feature Cropping** page, and enable the **Procedural Animation** option. + +![cropping.png](index/cropping.png) + +Then restart the editor or use Ctrl + R to refresh. diff --git a/versions/4.0/en/animation/marionette/procedural-animation/index.md b/versions/4.0/en/animation/marionette/procedural-animation/index.md new file mode 100644 index 0000000000..4a68d06390 --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/index.md @@ -0,0 +1,19 @@ +# Procedural Animation (experimental) + +> **Note**: Procedural animation is an experimental feature, please use it with caution. Cocos Creator looks forward to your feedback. + +Cocos Creator 3.8 starts to experimentally support procedural animation effects, giving developers the ability to implement some complex skeletal animations. + +## Content + +- In [Introduction to Procedural Animation](./introduce.md) describes the origins and foundations of procedural animation. + +- Procedural animation is currently turned off by default as an experimental feature in Cocos Creator, and is described in [Enabling Procedural Animation](./enabling.md) describes how to turn it on. + +- [Pose Graph](./pose-graph/index.md) describes the pose graph, the core building block of procedural animation. + +- [Additive Animation](../additive-animation/index.md) is a means of reusing animation assets. + +- In addition to gesture maps, [auxiliary curve](. /auxiliary-curve/index.md) is also an effective means of implementing procedural animation. + +- [Pose Stash](./pose-stash/index.md) is used to realize the reuse of pose graphs. diff --git a/versions/4.0/en/animation/marionette/procedural-animation/index/cropping.png b/versions/4.0/en/animation/marionette/procedural-animation/index/cropping.png new file mode 100644 index 0000000000..8f86942ac7 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/index/cropping.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/index/enable.png b/versions/4.0/en/animation/marionette/procedural-animation/index/enable.png new file mode 100644 index 0000000000..55cf98a893 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/index/enable.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/introduce.md b/versions/4.0/en/animation/marionette/procedural-animation/introduce.md new file mode 100644 index 0000000000..7da26974d1 --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/introduce.md @@ -0,0 +1,43 @@ +# Introduction to Procedural Animation + +Before we can understand procedural animation, we need to understand the "Pose" referred to in animation, and I'm sure you've seen the pose in the image below: + +![T-Pose](introduce/t-pose.png) + +> Image from [https://sketchfab.com](https://sketchfab.com/3d-models/man-in-coat-character-human-riged-model-758a855697be47a1be0d707623e3907e) + +This is the *binding pose* in animation. It is also known as a T-Pose because the whole human figure stretches its arms out to make a shape like a T. + +> Besides T-Pose, A-Pose is also widely used on human skeleton. + +I don't know if you have ever thought about why there is such a "weird" pose as T-Pose. + +The original reason may not be known, but nowadays, T-Pose is the most suitable starting animation for animators to animate a character from. So the T-Pose has been preserved as part of the animation workflow. + +We can understand T-Pose as a mathematical concept. If the positive X-axis starts at 0, then the starting pose of the animation is T-Pose. + +Understanding this allows us to understand the "pose" of the character. We can understand this as the appearance that the character embodies at a given moment in time, and these appearances are determined by the transformations (position, rotation, etc.) provided within its animation information, as well as elements such as skinning. In other words, when we move the "cursor" on the numerical axis to a certain moment, we get the current value on the numerical axis. Similarly, if we move the animation on the timeline, the way the character looks when the timeline stops at a certain moment is called a pose. + +In the engine, a character's **pose**, is the information about all the transformations (positions, rotations) of all the character's bones at a given moment. The solved animation map will generate the poses, which are then written to the scene, along with skinned rendering to realize the skeletal animation. + +Procedural animation, which is a flowchart-like representation of the pose generation and changes. + +![flow.png](introduce/flow.png) + +A typical procedural animation is shown above. + +The difference between procedural animation and normal animation is that normal animation usually means that the developer cannot intervene in the sampling behavior of the animation, whereas procedural animation provides additional capabilities for the developer to extract certain animation, blending, or pose information in the animation sampling to perform weighted blending and other operations on the current animation. + +The procedural animation system provides the ability to manipulate animation clips, blends, and poses, which allows the developer to dynamically adjust the animation sampling process according to the needs of the runtime and generate outputs, which can either be used as the final outputs to the screen or as inputs for other animation samples, thus forming a complex animation system. + +Let's take a simple example to illustrate procedural animation. + +For example, if a character walks on uneven ground, this is usually not taken into account when creating an animation clip because the producer of the art asset does not know what height the character's feet might be at, so an animation of the character walking on flat ground is created, and at runtime, due to the height of the ground, the height at which the feet should be located needs to be dynamically calculated, and the character's feet "Move" the character's feet to the corresponding height, which is usually done using Inverse Kinematic (IK) algorithms. + +With procedural animation, we only need to add IK-related nodes to the pose graph, and pass the information needed to calculate IK to the pose graph system at runtime, and then get the correct result through the IK solver. + +You can see that procedural animation makes the creation of complex animations easier to follow and understand by providing a visual view. + +## When you need procedural animation + +Simply put, you should consider using procedural animation capabilities when you need to modify bones dynamically, especially when you need to adjust bones to the environment. For example, when a humanoid character touches a switch, you need to move the hand bones to the switch position and make sure the arm adjusts accordingly. \ No newline at end of file diff --git a/versions/4.0/en/animation/marionette/procedural-animation/introduce/flow.png b/versions/4.0/en/animation/marionette/procedural-animation/introduce/flow.png new file mode 100644 index 0000000000..40ddcb7af1 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/introduce/flow.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/introduce/t-pose.png b/versions/4.0/en/animation/marionette/procedural-animation/introduce/t-pose.png new file mode 100644 index 0000000000..66cd7278c8 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/introduce/t-pose.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/index.md b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/index.md new file mode 100644 index 0000000000..d00eb4c12f --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/index.md @@ -0,0 +1,79 @@ +# Pose Graph (Experimental) + +**Pose graph** are node graph that express the creation and transformation of a pose, and are the central building blocks for realizing procedural animation. + +The pose graph is dependent on the other building blocks of the animation graph: + +- In the state machine, each [procedural pose](../../state-machine/procedural-pose-state.md) (state) contains a pose graph. + +- Each [pose stash](../pose-stash/index.md) contains a pose graph. + +This section describes the structure of a pose-stash. + +Before getting started with procedural animation, we recommend that you read the documentation related to the [Marionette Animation System](.../../../index.md) and animation graph. + +## Values and Types + +Broadly speaking, a pose graph describes the flow of values. Every value has a type, and the types of values supported by the pose graph are listed below: + +| Type | Description | +| :--- | :--- | +| Pose | Role Pose. | +| Floating Point | Floating point value. | +| Integer | Integer values. | +| Integer | Three-dimensional vectors. | +| Quaternions | Quaternions. | + +Where pose objects cannot be generated and specified out of thin air and are only generated by pose nodes (see below). + +## Node + +![nodes-type.png](./pose-nodes/img/nodes-type.png) + +A Pose Graph **Node** describes an operation. There may be zero or more **inputs** or zero or more **outputs** on the node. Some nodes have a variable number of inputs and can be deleted. Currently, all nodes have a fixed number of outputs and cannot be deleted. + +> A Pose Node is a node in the pose graph that is used to handle procedural animation, not a node of the engine. + +Inputs and outputs are associated with types. When a node has an input of a certain type, it means that the node can accept a value of that type from that input; when a node has an output of a certain type, it means that the node will output a value of that type to that output. When seeking a value, the node operates on the value of its inputs and produces the resulting value to the output. + +A node output can be connected to the **same type** input of another node, indicating that when seeking a value, it passes the value of its output to the other node's as input. Sometimes this behavior is called **binding**. + +> Note that such a connection is unidirectional - it is not possible to connect inputs to outputs, which makes no sense. + +The output of a node can be connected to the inputs of multiple nodes. An exception exists: graph outputs can be connected to only one node. + +> If you need to reference a pose in more than one place, consider using [pose-stash](../pose-stash/index.md). + +The following types of nodes exist in the pose graph: + +| Node Type | Description | Input | Output | +| :-- | :-- | :-- | :-- | +| Pose Output Node | Acts as the output pose for the entire pose graph. | A pose input | None | +| Pose Node | An output pose object. | zero or more | A pose output | +| Variable Get Node | Get the specified variable and output it | None | One output, of type variable | + +You can refer to the [Pose Graph View](./pose-nodes/node-operation.md) to see how to visualize the operation of a pose graph node. + +### Pose Graph Output Node + +![output.png](./pose-nodes/img/output.png) + +The **Pose Graph Output Node** is a special kind of node that represents the output pose of the whole pose-graph. It is built into the pose graph and cannot be deleted or created. + +A pose graph output node has one and only one pose input. When a node is connected to that pose input, the input pose becomes the output pose of the pose graph. + +> If the pose graph output node is not connected, the pose graph output pose is the default pose. + +The pose graph output node has no output. + +### Pose Node + +**Pose nodes** are all nodes that output pose objects. Currently, all pose nodes produce only one pose output. However, a pose node may have zero or more inputs of any type depending on the node itself. + +[Pose Nodes](./pose-nodes/index.md) lists all types of pose nodes. + +### Variable Get Nodes + +The **Variable Get Node** gets the specified animation graph variable and outputs its value. This type of node has no inputs and only one output, which is the type of the variable. + +A variable get node cannot get trigger variables. diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md new file mode 100644 index 0000000000..b587040839 --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md @@ -0,0 +1,72 @@ +# Blend Pose + +There are several type of blend nodes in pose graph. + +> All blend nodes are calculated in the local space. + +Blend poses can be created in the pose graph by right-clicking on the **Pose Nodes** -> **Blend**. + +![blend.png](./img/create-blend.png) + +## Blend Two Pose + +![blend-two-pose.png](./img/blend-two-pose.png) + +The **Blend Two Pose** will blend the two specified poses by ratio. + +| Input | Type | Description| +| :-- | :-- | :-- | +| **Pose 1** | Pose | First pose in the blend | +| **Pose 2** | Pose | Second pose in the blend | +| **Ratio** | Float | Blend ratio. For example, 0.2 means that the final pose will contain 80% of pose 1 and 20% of pose 2. | + +## Blend In Proportion + +![blend-in-proportion.png](./img/blend-in-proportion.png) + +The **Blend In Proportion** node blends the nodes by proportion of each pose. + +| Input | Type | Description| +| :-- | :-- | :-- | +| **Pose N** | Pose | Pose in the blend. | +| **Pose N Proportion** | Float | The ratio of the poses to each other. For example, 0.2 means that the final pose will contain 20% of the pose N.| + +More poses can be added by clicking the "+" button on the right and selecting the **poses** menu. + +![add-blend-in-proportion.png](./img/add-blend-in-proportion.png) + +After adding a pose, enter the proportion in the input box to set the percentage of the pose in the final pose. + +![blend-in-proportion-ratio.png](./img/blend-in-proportion-ratio.png) + +> All proportional weighted results should be 1, otherwise there may be undefined behavior. + +## Filtering Blend + +![filtering-blend](./img/filtering-blend.png) + +The **Filtering Blend** node blends Pose 1 to Pose 2 by the specified ratio and mask. + +| Input | Type | Description| +| :-- | :-- | :-- | +| **Pose 1** | Pose | First pose engaged in the blend | +| **Pose 2** | Pose | Second pose engaged in the blend| +| **Ratio** | Float | Blend Ratio. For example. 0.2 means the final pose contains 80% from Pose1 and 20% from Pos2. | + +![filtering-blend-inspector.png](./img/filtering-blend-inspector.png) + +| Inspector | Description | +| :-- | :-- | +| **Mask** | The animation mask to use when blending.Only the parts of pose 2 that are bounded by the mask will be blended into pose 1. | + +## Additively Blend + +![additively-blend.png](./img/additively-blend.png) + +The **Additively Blend** node "adds" the additive blend to the base pose. + +| Input | Type | Description| +| :-- | :-- | :-- | +| **Base Pose** | Pose | The base pose. | +| **Additive Pose** | Pose | The additive pose | +| **Ratio** | Floating Point | Blend Ratio. For example, 0.2 means that only 20% of the stacked poses will be blended into the base pose. | diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/add-blend-in-proportion.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/add-blend-in-proportion.png new file mode 100644 index 0000000000..0754b0cb76 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/add-blend-in-proportion.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/additively-blend.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/additively-blend.png new file mode 100644 index 0000000000..a638e3616b Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/additively-blend.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform-inspector.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform-inspector.png new file mode 100644 index 0000000000..92b66da80d Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform-inspector.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform.png new file mode 100644 index 0000000000..9ebada5067 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion-ratio.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion-ratio.png new file mode 100644 index 0000000000..5a65f42a8f Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion-ratio.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion.png new file mode 100644 index 0000000000..e5022cffc5 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-two-pose.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-two-pose.png new file mode 100644 index 0000000000..788e74a153 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-two-pose.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend.png new file mode 100644 index 0000000000..9457f25506 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform-inspector.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform-inspector.png new file mode 100644 index 0000000000..7e41773c1c Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform-inspector.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform.png new file mode 100644 index 0000000000..cfaabcdfb9 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-blend.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-blend.png new file mode 100644 index 0000000000..cd5b40fee8 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-blend.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-link.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-link.png new file mode 100644 index 0000000000..75d4fcc2a0 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-link.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-pose-node.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-pose-node.png new file mode 100644 index 0000000000..1533975040 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-pose-node.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/curve.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/curve.png new file mode 100644 index 0000000000..dd45865934 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/curve.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend-inspector.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend-inspector.png new file mode 100644 index 0000000000..70d38b3399 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend-inspector.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend.png new file mode 100644 index 0000000000..4e7318cfac Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/get-value.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/get-value.png new file mode 100644 index 0000000000..478fea4f7c Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/get-value.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/input.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/input.png new file mode 100644 index 0000000000..e53ae6fe1c Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/input.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link-success.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link-success.png new file mode 100644 index 0000000000..4eb6e79b56 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link-success.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link.gif b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link.gif new file mode 100644 index 0000000000..b756c72d8d Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link.gif differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/menu.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/menu.png new file mode 100644 index 0000000000..79d45267bd Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/menu.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/name.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/name.png new file mode 100644 index 0000000000..2b433ca041 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/name.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/node-options.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/node-options.png new file mode 100644 index 0000000000..f3ff5fd8cc Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/node-options.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/nodes-type.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/nodes-type.png new file mode 100644 index 0000000000..9a46862283 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/nodes-type.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/output.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/output.png new file mode 100644 index 0000000000..65922277e9 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/output.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/remove-link.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/remove-link.png new file mode 100644 index 0000000000..d1aa889788 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/remove-link.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/shrink.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/shrink.png new file mode 100644 index 0000000000..400c2f662c Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/shrink.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-inspector.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-inspector.png new file mode 100644 index 0000000000..121f5a8928 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-inspector.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-solver.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-solver.png new file mode 100644 index 0000000000..17b7518ca1 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-solver.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md new file mode 100644 index 0000000000..90c93d0e70 --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md @@ -0,0 +1,27 @@ +# Pose Nodes + +An animation graph allows you to create a pose node and edit the pose node, the result of which will be stored inside the animation graph as part of the animation graph assets. + +You can refer to the Animation Graph usage to create, delete, and modify a pose graph. + +In this chapter, we will introduce you to the concepts of pose graphs and their usage. + +A pose node outputs a pose. The engine provides pose nodes with the following roles: + +- [play-or-sample-animation](./play-or-sample-motion.md) + +- [Blend poses](./blend-poses.md) + +- [Choose Pose](./choose-pose.md) + +- [modify-pose](./modify-pose.md) + +- [State Machine](./state-machine.md) + +- [Use stashed pose](./use-stashed-pose.md) + +For the visualization of the pose graph node editor, see [pose graph node view](./node-operation.md). + +## Contents + +- [Pose graph node operation](./node-operation.md) \ No newline at end of file diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md new file mode 100644 index 0000000000..857a17c195 --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md @@ -0,0 +1,116 @@ +# Modify Poses + +The pose graph provides several nodes for modifying poses. Each of these nodes accepts an input pose and outputs the modified pose. + +| Input | Type | Description | +| :-- | :-- | :-- | +| `Pose` | Pose | The pose to be modified. | + +For the operation of a pose node, see [pose graph node view](./node-operation.md). + +## Apply Transform + +![apply-transform.png](./img/apply-transform.png) + +**Apply transform node** Applies a transform (translate and rotation) to the specified node of the input pose. + +| Input | Type | Description | +| :-- | :-- | :-- | +| `Position` | 3d-vector| The amount of modification to the position. | +| `Rotation` | Quaternion | The amount of modification to the rotation. | +| `Intensity` | Float |The intensity of application of the modification, in the range [0, 1]. 0 means that the modification is not applied at all, 1 means that the modification is applied completely. | + +![apply-transform-inspector.png](./img/apply-transform-inspector.png) + +| Properties | Description | +| :---| :---| +| `Node` | The name of the node to be modified. | +| `Position Operation` | Specifies how to modify the position. See [Transform Operations](./modify-pose.md#Transform%Operation) | +| `Rotation Operation` | Specifies how to modify the rotation. See [Transform Operation](./modify-pose.md#Transform%Operation) | +| `Transform Space` | Specifies the space in which the modification occurs; that is, the space in which the specified `position` input and `rotation` input reside. | + +### Transform Operation + +The transform operation options specify how the applied transform node modifies the position or rotation. The options are listed below: + +| Properties | Description | +| :-- | :-- | +| **LEAVE_UNCHANGED** | No modification. | +| **REPLACE** | When used as an application option for position, the `position` input is used directly as the new position of the node; when used as an application option for rotation, the `rotation` input is used directly as the new rotation of the node. | +| **ADD** | Adds the `position` input to the node's current position when used as an application option for position, or the `rotation` input to the node's current rotation when used as an application option for rotation. | + +## Copy Transform + +![copy-transform.png](./img/copy-transform.png) + +The **Copy Transform** node copies the transform of a node in the input pose to another node. + +![copy-transform-inspector.png](./img/copy-transform-inspector.png) + +| Properties | Description | +| :---| :---| +|`Source node` | The name of the node to be copied. | +|`Target Node` | The name of the node to copy to. | +|`Space` |Specifies the space where the copy occurs. When `COMPONENT` indicates that the copy occurs in component space; when `LOCAL` indicates that the copy occurs in local space. | + +## Set Auxiliary Curves + +![curve.png](./img/curve.png) + +The **Set Auxiliary Curve** node modifies the current value of the specified auxiliary curve in the input pose. + +| Input | Type | Description | +| :-- | :-- | :-- | +| `Value` | Float | The amount of modification to the auxiliary curve. | + +![inspector-property.png](../../auxiliary-curve/img/inspector-property.png) + +| Properties | Description | +| :---| :---| +| `Curve Name` | The name of the auxiliary curve to be modified. | +| `Flag` | Specifies how to modify the auxiliary curve.
When `LEAVE_UNCHANGED` it is not modified; when `REPLACE` it means that the `Curve Value` input is used as the current value of the auxiliary curve;
when `ADD` it means that the `Curve Value` input is added to the current value of the auxiliary curve. | + +## Double Skeleton IK + +![two-bone-ik-solver.png](./img/two-bone-ik-solver.png) + +IK solving is often used to move or rotate a bone to a target position and drive the parent bone so that it remains the same distance from the parent (or within an acceptable range). For example, when moving a foot, the knee bone and thigh bone should bend accordingly. + +**Two-bone IK solver node** solves IK problems consisting of two sections of (three bones) bones for the input pose. These three bones form a direct parent-child relationship, i.e. "child level - parent level - parent of parent level". The "child" is called the **end-effector** or **end-bone**, the "parent of parent" is called the **root-bone** (in this IK problem), and the "parent of parent " is called the **intermediate bone**. + +> Therefore, the "two" in "two-bone IK" should be interpreted as "two segments of bones" rather than "two bones". + +When evaluating, the solver will transform the end and middle bones, and rotate the root bone so that the end bone reaches the specified position; and keep the distance between the parent and child levels unchanged, and the position of the root bone unchanged in the process. + +![Two Bone IK Demonstration](./two-bone-ik-demonstration.gif) + +For fixed end-bone and root-bone positions, an infinite number of solutions exist for the intermediate bones when solving. As shown in Fig: + +![Pole Target Demonstration](./pole-target-demonstration.gif) + +The solver will try to find the "best" solution, but there may still be more than one such solution, and it may not be the desired one. + +Therefore, in order to explicitly specify the orientation of the intermediate bones, the solver also allows to specify a **pole-target** to determine the bending direction of the skeletal chain. + +![two-bone-ik-inspector.png](./img/two-bone-ik-inspector.png) + +| Input | Type | Description | +| :---| :---|---| +| `End-effector-target` |3d-vector|Target position of the end-effector. | +| `Polar target` |3d-vector | The position of the polar target. | + +| Attributes | Description | +| :---| :---| +| `End-effector node` | The name of the end-effector node. | +| `End-Executor-Target` | The target setting of the end-effector. | +| `Polar target` |The polar target setting. | + +### Target settings + +The target setting is used to describe the end-effector target or the poleward target. The options are as follows: + +| Attributes | Description | +| :-- | :-- | +|`Type` |Target type. When `VALUE`, it means that the target position is represented as the specified (3D vector) value; when `BONE`, it means that the position of another bone is used as the target position; and when `NONE`, it means that the current position is used as the target position, i.e., the target is not solved for. | +|`Target Bone`| The name of the target bone when the target type is `BONE`. | +|`Target Position Space` | The space of the target position when the target type is `VALUE`. | diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md new file mode 100644 index 0000000000..0a91b97149 --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md @@ -0,0 +1,95 @@ +# Pose Graph View + +## Create Node + +Before creating a node, you need to create a pose graph in the animation graph first, and then double-click the mouse to enter the visual editing interface of the pose graph. + +Right click on the pose graph and click on Post Nodes in the popup menu to create a node. + +![create-pose-node.png](./img/create-pose-node.png) + +| Menu | Description | +| :-- | :-- | +| **Play Animation** | See [play-or-sample-motion](./play-or-sample-motion.md) +| **State Machine** | See [state machine](./state-machine.md) +| **Sample Animation** | See [play-or-sample-motion](./play-or-sample-motion.md) +| **Blend** | See [blend poses](./blend-poses.md) +| **Select** | Developers can select poses and animations with different types of data, including booleans and indices | +| **Apply Transform** | See [Apply Transformations](./modify-pose.md#%E5%BA%94%E7%94%A8%E5%8F%98%E6%8D%A2)| | **Copy Transform** | **Copy Transformations +| **Copy Transform** | See [Copy Transformations](./modify-pose.md#%E6%8B%B7%E8%B4%9D%E5%8F%98%E6%8D%A2)| | **Copy Transform** | See [Copy Transform](. +| **Set Auxiliary Curve** | See [Auxiliary Curve](../../auxiliary-curve/index.md) | +| **Reverse Dynamics** | Calculate the reverse computation of the skeleton via the reverse dynamics checker, currently supported by [Two-Bone IK](./modify-pose.md#%E5%8F%8C%E9%AA%A8%E9%AA%BC-ik) | + +Once created, it will be displayed in the attitude graph as a rectangular box of a different color. + +![nodes-type.png](./img/nodes-type.png) + +We use **blend** node as an example to illustrate the structure of nodes, other types of nodes can be referred to. + +![blend.png](./img/blend.png) + +You can see that the node has several components: + +- Type: the type of the node displayed at the top. +- Expand/collapse: click on the arrow symbols at the top left of some nodes to expand/collapse them to keep a cleaner view. + + ![shrink.png](./img/shrink.png) + +- Inputs: usually on the left side of the node, indicates the inputs to the node, indicated by different colored hollow (unconnected)/solid (connected) diamonds, the outputs of other nodes can be used as inputs, and some of the nodes can also be input directly within the pose graph within the right **Inspector** panel. The inputs are different for different node types, please refer to the documentation for different types of nodes for details. + + ![input.png](./img/input.png) + + Some node types, such as variable nodes, have no inputs but only outputs. + +- Outputs: The output part is usually on the right side of the node, represented by a hollow (unconnected)/solid (connected) diamond, which can be formed by pressing and dragging the left mouse button to form a connection. + + ![output.png](./img/output.png) + + Generally speaking, a node's output can only be used on another node's input, and only once, which means that if the output has already been used, connecting to the other node through the input again will disconnect the previous connection. + +## Node Operations + +Clicking the right mouse button on any node brings up the node operations menu. + +![node-options.png](./img/node-options.png) + +- Copy this node: copy operation will copy the currently selected node for other use. +- Clone this node: clones the selected node in the current pose map. +- Delete this node: removes the selected node from the pose map. + +## Menus + +Clicking the right mouse button inside the empty space of the pose map will bring up the menu of the pose map. + +![menu.png](./img/menu.png) + +- Delete Pose Node: copy the node that was operated by copying this node above to this node +- Pose node: create a new pose node, please refer to the **Create Node** section above +- Get Variable: this menu can get the variables created in the animation graph, all the non-trigger variables configured in the animation graph can be obtained here. + + ![get-value.png](img/get-value.png) + +- Stash This Graph: the current pose graph will be dumped as a new pose graph +- Return to center view: The view will automatically return to the center view with the appropriate viewpoint. + +## Create Connection + +![create-link.png](./img/create-link.png) + +The connection means taking the output of one node and passing it to another node as an input parameter. + +For different nodes, it is possible to connect the output of a node to the input of another node by dragging the node's output to the input of the other node, provided of course that the other node accepts the output type. + +By dragging and dropping the output of a node to the input of another node. + +![link.gif](./img/link.gif) + +Successful connection is indicated by a solid diamond shape on the node: + +![link-success.png](./img/link-success.png) + +## Delete Connections + +A connection can be deleted by clicking the right mouse button on the connected line. A blue highlighting flood light will be displayed around the selected line. + +![remove-link.png](./img/remove-link.png) diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md new file mode 100644 index 0000000000..f60e3d0b8f --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md @@ -0,0 +1,31 @@ +# Play or Sample Animation + +The pose graph provides two types of nodes for reading poses from animations: play animation nodes and sample animation nodes. + +In the following, "action" refers to animation clips or animation blends. + +## Play Animation + +The **Play animation node** node plays the specified action, updates it every frame, and gets the pose of the current frame of the animation as output. + +| Input | Description | +| :-- | :-- | +| `Start Time` | When to start playing the action from whenever this node is reentered. Unit is in seconds. | +| `Speed Multiplier` | The playback rate of the action. | + +| Properties | Description | +| :-- | :-- | +| `Action` | The action to sample. | + +## Sampled Animation + +The **Sampled Animation Node** samples the pose of the specified action at a given moment as output. + +| Input | Description | +|:-- |:-- | +| `Time` | The time to sample. | + +| Properties | Description | +| :-- |:-- | +| `Action` | The action to sample. | +| `Use Normalized Time` | Whether the node input `time` specifies a normalized time. The normalized time is the progress of the action in the range [0, 1]. For example, 1 means the last frame of the action, and 0.5 means 50% of the action progress. | diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/pole-target-demonstration.gif b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/pole-target-demonstration.gif new file mode 100644 index 0000000000..d091cacf95 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/pole-target-demonstration.gif differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/two-bone-ik-demonstration.gif b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/two-bone-ik-demonstration.gif new file mode 100644 index 0000000000..7ad832ade5 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-graph/pose-nodes/two-bone-ik-demonstration.gif differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/delete.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/delete.png new file mode 100644 index 0000000000..a73a3a6dfd Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/delete.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/edit.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/edit.png new file mode 100644 index 0000000000..a2dbd18aec Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/edit.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/index.md b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/index.md new file mode 100644 index 0000000000..476806099e --- /dev/null +++ b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/index.md @@ -0,0 +1,63 @@ +# Pose Stash + +It is not possible to connect the output of a pose node to multiple nodes in a pose graph, when you try to connect a pose node to a new node, the old connection will be broken. If you need to reuse a pose in multiple places, you need to use pose stash. + +## Concepts + +**Pose stash** is a mechanism for storing the results of one pose graph and using the resulting poses in other pose graphs. A **pose stash** (hereafter referred to as a pose stash or stash) is a means of implementing such a mechanism, which associates a pose graph. + +> A visual, but incomplete, understanding of a pose stash is to think of a pose stash as a cache variable that holds pose objects. + +Pose stash is attributed to animation graph hierarchies. Multiple stashes can be created at each layer, and once created, the stash can be referenced in any pose graph in that layer via the **Use Stash Pose** node. + +> We can think of this as taking some already edited pose node and saving it as some sort of blueprint or resource for use elsewhere. It's just that this blueprint has a limited scope and can only be in a specific layer in this animation graph. +> This also means that we can treat stash as some special pose node and use it just like a normal pose node. + +Pose stash is allowed to be referenced multiple times, and the associated pose graph will only be updated and evaluated once per frame. + +## Editing a pose stash + +The **Property Inspector** of a selected animation graph layer allows you to view, create, and edit all pose stash for that layer. + +Click on a layer in the animation graph and find the **Layer Stashes** property in the **Property Inspector**. + +![Alt text](layer-stashes.png) + +Click the ![edit.png](edit.png) button to start editing the pose graph associated with the selected stash. Click ![delete.png](delete.png) to delete the temporary storage. Deletion is not restorable, so please be careful. + +The information text in this property below will indicate how many times the stash has been referenced, if it is greater than 0 then the stash has been referenced. + +Click the **New Stash** button to create a new stash. + +## Usage of Stashed Pose + +The **Use Stashed Pose** menu lists all the stash that can be used in the pose graph. Clicking on this will create the **Use Stashed Pose** node. + +![Alt text](use-stash-node-list.png) + +![Alt text](use-stash-node.png) + +The **Use Stashed Node** node outputs the pose generated by the specified stash; there are no additional inputs for this node. + +Double-click the **Use Stash** node to access the edit view for the pose stashed associated with that node. + +## Stash This Graph + +Often it is not always possible to think of which poses to stash at the beginning of the design; more often than not, a graph has been developed and it is realized that it needs to be reused in more than one place. +To do this, right-click in any pose graph to access the context menu and click the Execute menu item **Stash This Graph**: + +![Alt text](menu-item-stash-this-pose.png) + +When executed, the original contents of the current graph will be moved to a newly created pose stash, and a **Use Stashed Pose** node will be created in the current graph to reference the newly created pose stash. + +Clicking **Stash This Graph** creates a new Stashed Pose. + +Here is an example of a pose that plays an animation clip. + +![original-graph.png](./original-graph.png) + +Click the **Stash This Graph** menu, and you can see the change in the following image: + +![stash2.png](./stash2.png) + +You can observe that the previous pose node, the **Play Animation Clip** node, no longer exists, and has been replaced by a node named **Use Stash StashX** (X is the number, the name of the stash can be changed in the **Inspector** panel). This node is the new **Stashed Pose**. diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/layer-stashes.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/layer-stashes.png new file mode 100644 index 0000000000..77d0fa5281 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/layer-stashes.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/menu-item-stash-this-pose.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/menu-item-stash-this-pose.png new file mode 100644 index 0000000000..6e66e347ad Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/menu-item-stash-this-pose.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/original-graph.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/original-graph.png new file mode 100644 index 0000000000..5bddbfe35c Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/original-graph.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/stash2.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/stash2.png new file mode 100644 index 0000000000..951c84e220 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/stash2.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/use-stash-node-list.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/use-stash-node-list.png new file mode 100644 index 0000000000..933fa117e7 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/use-stash-node-list.png differ diff --git a/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/use-stash-node.png b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/use-stash-node.png new file mode 100644 index 0000000000..0c6277f206 Binary files /dev/null and b/versions/4.0/en/animation/marionette/procedural-animation/pose-stash/use-stash-node.png differ diff --git a/versions/4.0/en/animation/marionette/state-machine/procedural-pose-state.md b/versions/4.0/en/animation/marionette/state-machine/procedural-pose-state.md new file mode 100644 index 0000000000..fabcc116d0 --- /dev/null +++ b/versions/4.0/en/animation/marionette/state-machine/procedural-pose-state.md @@ -0,0 +1,5 @@ +# Procedural Poses (states) + +![state-machine.png](./state-machine.png) + +A **Procedural Poses** (state) is a state in a state machine. The state holds a pose graph, and when the state machine runs into this state, the pose it produces is the output gesture of the pose graph. \ No newline at end of file diff --git a/versions/4.0/en/animation/marionette/state-machine/state-machine.png b/versions/4.0/en/animation/marionette/state-machine/state-machine.png new file mode 100644 index 0000000000..49b159e94e Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-machine/state-machine.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition.md b/versions/4.0/en/animation/marionette/state-transition.md new file mode 100644 index 0000000000..34f6c58ad8 --- /dev/null +++ b/versions/4.0/en/animation/marionette/state-transition.md @@ -0,0 +1,169 @@ +# State Transitions + +**Transition** represents a transition between two states, which can be classified depending on the source where the transition occurs. + +- **Ordinary Transition**: the source where the transition occurs is a pseudo-state **entry** or **sub-state machine**. + + ![transition](state-transition/transition.png) + +- **Animation Transition**: the transition occurs with a source of a **State** or pseudo-state **Any**. Compared to ordinary transitions, animation transitions can also control the transition period to make the animation switch smoothly. + + ![animation-transition](state-transition/animation-transition.png) + +## Creating Transition + +Right-click the state that is the source of the transition in the grid layout area, then select **Add Transition**, when the mouse leaves the current state, an arrow will be created, then select the state that is the target of the transition to create a transition between the two: + +![add-transition](state-transition/add-transition.gif) + +Multiple same/reverse direction transitions are allowed between two states, and the number of same/reverse direction transitions is marked on the arrow, and the configuration items are displayed in the **Inspector** tab.
+When there are multiple transitions on the state that satisfy both, the transition at the top of the transition list is used first. However, Creator does not recommend relying on this prioritization, and it is best to filter the transitions using the **filter conditions** described below whenever possible. + +![add-transition](state-transition/add-more-transitions.png) + +States also allow transitions to themselves: + +![add-transition](state-transition/add-transition-for-self.png) + +## Setting Transitions + +With the transition selected in the grid layout area, the **Inspector** tab on the left side allows you to set the trigger conditions associated with the transition, including **Duration**, **Enable Exit Times**, **Exit Times**, **Conditions**. + +> **Note**: **ordinary transition** can only specify **Conditions**, which is more like a selector. + +![condition-properties](state-transition/condition-properties.png) + +- **Duration**: set the period to complete the current transition, so the animation will switch smoothly. The unit is **seconds** or **times**, default is 0.3 seconds, you can click the unit after the input box to switch. + + ![change duration](state-transition/change-duration.gif) + + - When set to **seconds** (default): indicates that the state transition is completed within the set number of seconds + + - When set to **times**: means the period of the state transition is based on the number of times the transition source state is played. For example, if the animation length of the transition source state is 3 s, when **Duration** is set to 0.9 times, it means the transition period is 0.9 ✖️ 3s = 2.7s. + +- **Enable Exit Times**: indicates whether the transition source state ends playing as one of the transition conditions. If checked, the transition needs to occur to meet the **Exit Times** condition set below. Play count is used as a special condition, when play count is enabled, the transition will happen only when both the play count condition and the transition condition are satisfied. + +- **Exit Times**: sets the total amount of time the animation plays before the transition source state begins (Total = Exit Times ✖️ Animation Length), defaults to 1 time, and takes effect only when **Enable Exit Times** is checked. It is important to note that animations that do not loop will stop when the first play is completed and wait for the remaining number of plays to complete before starting the transition. To loop the animation automatically according to the number of times, please set the wrap mode in the skeleton animation asset beforehand. For example: + + - If the animation of the transition source is an **non-loop animation**, the playing time is 3 s, **Exit Times** is set to 1.4, then the animation of the transition source will continue to stay in the last frame for 1.2s (3s × 0.4) after playing for 3s, before starting the transition. + + ![non-loop](state-transition/non-loop.png) + + - If the animation of the transition source is an **loop animation**, playing time is 3 s, **Exit Times** is set to 1.4, then the animation of the transition source will continue to loop the animation to 1.2s (3s × 0.4) after playing 3s, before starting the transition. + + ![loop](state-transition/loop.png) + +- **Condition**: there are certain conditions that need to be met when setting up transitions that occur between states, as described below: + +Without using other transition conditions, for the animation of the source state to gradually switch to other states when it is almost finished playing once, and the animation of the source state is just finished playing when the switch is done, then we can set **Duration** and **Exit Times** to achieve this, it is recommended to set the unit of **Duration** to **times**, and then make the sum of **Duration** and **Exit Times** sum to **1**. + +For example, if the animation length of the transition source state is 3 seconds, set **Duration** to 0.3 times, then **Exit Times** is 0.7. If **Duration** is set to 0.3 seconds, it is more troublesome, **Exit Times** need to convert seconds to times: (3 - 0.3)/ 3 = 0.9 times. + +![set example](state-transition/set-example.png) + +### Transition Conditions + +Some transitions between states need to satisfy certain conditions to be triggered, such conditions are called **transition conditions**, or **conditions** for short. If no condition is set for the transition, it will be triggered directly. Conditions can be created in the [Variables](animation-graph-panel.md) tab of the Animation Graph panel. + +Once the transition is selected in the grid layout area, the condition can be added by selecting the gear icon button in the **Inspector** tab on the left. The added conditions are displayed below the **Inspector** tab on the left. + +![add-condition](state-transition/add-condition.png) + +The currently supported transition conditions include the following three: + +- **Boolean condition**: determines if a variable of Boolean type is true/false. Click the gear icon button on the right to remove the current condition. + + ![boolean-condition](state-transition/boolean-condition.png "boolean-condition") + +- **Numeric condition**: determines the logical relationship between a variable of numeric type and another fixed value, including **equal to**, **not equal to**, **greater than**, **less than**, **greater than or equal to**, **less than or equal to**. The condition is satisfied when the equation (inequality) holds. The type of the variable matches the type of the value. If the variable is set to Float type, the type of the value is also Float type. + + ![numeric-condition](state-transition/number-condition.png "numeric condition") + +- **Trigger condition**: condition is satisfied when the trigger variable is triggered. + + ![trigger-condition](state-transition/trigger-condition.png "trigger condition") + +**Transition supports specifying multiple transition conditions at the same time, and the transition will occur when and only when all conditions are satisfied.** + +### Unconditional Transitions + +For **ordinary transitions**, it is allowed to not specify any transition conditions, for example, many times **Entry** needs to transition to other states unconditionally. + +However, for **animation transitions**, transitions where neither **conditions** nor **Exit Times** conditions are specified are meaningless and the Creator will simply ignore the transition. + +## An edge case + +There are cases that cause a state machine to stay on **Entry** or **Exit**, e.g. + +- Transitioned to the **Entry** of a sub-state machine, but there is no transition in the sub-state machine that would satisfy the condition. + +- Transitioned to the **Exit** of a sub-state machine, but there is no transition in the parent state machine that would satisfy the condition for that sub-state machine. + +This situation is called **state machine hover**. + +When a state machine hover occurs, it directly interrupts the update until a subsequent transition to the state can be made, at which point it is represented as the animation being paused. + +> **Note**: it is not recommended to rely on this behavior, and subsequent adjustments may be made to this behavior. + +## Preview + +Once any transition is selected, a preview of the current transition is available in the **Inspector** panel. + +![preview](state-transition/preview-overview.png) + +During the preview, the user can operate with the following buttons. + +![bar](state-transition/preview-bar.png) + +The properties and descriptions are as follows. + +- ![start](preview-bar/start.png) Skip to first frame +- ![play](preview-bar/prev.png) Preview previous frame +- ![play](preview-bar/play.png)/![pause](preview-bar/pause.png) 开始/Start/pause playback +- ![stop](preview-bar/stop.png) Stop Play +- ![end](preview-bar/next.png) Jump to the next frame +- ![end](preview-bar/end.png) Jump to the last frame +- ![time](preview-bar/time.png) Display the current time + +A quick preview and adjustment of the duration of the transition can also be done via the progress bar: + +![progress](state-transition/progress.png) + +- Click or drag the puller at ① to quickly preview the transfer +- At ② you can adjust the **Duration** by dragging the edge with the mouse + + ![duration](state-transition/duration.png) + +- You can adjust the **Destination Start Time** of the transfer by dragging the left mouse button at the ③ sign + + ![dst-start-time](state-transition/dest-start-time.png) + +After clicking on Play button, you can preview both in Preview view. + +![preview-view](state-transition/preview-view.png) + +## Transition Interruptions + +Transition interruptions are experimental features. Editing of transition interrupts can be enabled by checking **Transition interrupts** in **Preferences** -> **Laboratory**. + +![interruptible-laboratory](state-transition/interruptible-laboratory.png) + +> **Note**: This option only controls the editing of the **Transition Interruption** field in the Animation Transition panel; it is not a switch for global functionality. If **Transition Interruption** is checked, the **Transition Interruption** field will take effect even if the option is subsequently disabled. + +Once enabled, check **Transition Interruption** in the Animation Transition Configuration panel to configure the transition as interruptible: + +![interruptible-inspector](state-transition/interruptible-inspector.png) + +When an animation transition is configured to be interruptible, that animation transition in progress can be interrupted by the following transitions (in the priority listed). + +- All transitions originating from an **any** state. + +- All transitions originating from the starting state of the current transition, but not the transition itself. + +- All transitions originating from the end state of the current transition. + +When a transition is interrupted, the current transition is saved and the transition begins toward the end point of the new transition. During this process, the two states of the interrupted transition are not updated. For example, the following clip demonstrates the effect of a **Standby to Jump** transition (with a transition period of 0.5 seconds) being interrupted by a **Standby to Kick** transition. + +![interruption-demo](state-transition/interruption-demo.gif) + +During the interruption, the standby animation and jump animation are no longer updated, while the kick animation continues to stay updated. diff --git a/versions/4.0/en/animation/marionette/state-transition/add-condition.png b/versions/4.0/en/animation/marionette/state-transition/add-condition.png new file mode 100644 index 0000000000..b11da6ccad Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/add-condition.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/add-more-transitions.png b/versions/4.0/en/animation/marionette/state-transition/add-more-transitions.png new file mode 100644 index 0000000000..2ba7d07021 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/add-more-transitions.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/add-transition-for-self.png b/versions/4.0/en/animation/marionette/state-transition/add-transition-for-self.png new file mode 100644 index 0000000000..04f6c27b26 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/add-transition-for-self.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/add-transition.gif b/versions/4.0/en/animation/marionette/state-transition/add-transition.gif new file mode 100644 index 0000000000..fedbf66b82 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/add-transition.gif differ diff --git a/versions/4.0/en/animation/marionette/state-transition/animation-transition.png b/versions/4.0/en/animation/marionette/state-transition/animation-transition.png new file mode 100644 index 0000000000..7cd34c1c3f Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/animation-transition.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/boolean-condition.png b/versions/4.0/en/animation/marionette/state-transition/boolean-condition.png new file mode 100644 index 0000000000..190762062e Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/boolean-condition.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/change-duration.gif b/versions/4.0/en/animation/marionette/state-transition/change-duration.gif new file mode 100644 index 0000000000..7828cdabc1 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/change-duration.gif differ diff --git a/versions/4.0/en/animation/marionette/state-transition/condition-properties.png b/versions/4.0/en/animation/marionette/state-transition/condition-properties.png new file mode 100644 index 0000000000..0476987b32 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/condition-properties.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/dest-start-time.png b/versions/4.0/en/animation/marionette/state-transition/dest-start-time.png new file mode 100644 index 0000000000..9b4475c87e Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/dest-start-time.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/duration.png b/versions/4.0/en/animation/marionette/state-transition/duration.png new file mode 100644 index 0000000000..de3e9f31bf Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/duration.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/interruptible-inspector.png b/versions/4.0/en/animation/marionette/state-transition/interruptible-inspector.png new file mode 100644 index 0000000000..55d1390fc4 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/interruptible-inspector.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/interruptible-laboratory.png b/versions/4.0/en/animation/marionette/state-transition/interruptible-laboratory.png new file mode 100644 index 0000000000..bf9eb563c8 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/interruptible-laboratory.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/interruption-demo.gif b/versions/4.0/en/animation/marionette/state-transition/interruption-demo.gif new file mode 100644 index 0000000000..13bcc90e80 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/interruption-demo.gif differ diff --git a/versions/4.0/en/animation/marionette/state-transition/loop.png b/versions/4.0/en/animation/marionette/state-transition/loop.png new file mode 100644 index 0000000000..7c921635e5 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/loop.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/non-loop.png b/versions/4.0/en/animation/marionette/state-transition/non-loop.png new file mode 100644 index 0000000000..a47c85f0f2 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/non-loop.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/number-condition.png b/versions/4.0/en/animation/marionette/state-transition/number-condition.png new file mode 100644 index 0000000000..9186066424 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/number-condition.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/preview-bar.png b/versions/4.0/en/animation/marionette/state-transition/preview-bar.png new file mode 100644 index 0000000000..d0bbbd8286 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/preview-bar.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/preview-overview.png b/versions/4.0/en/animation/marionette/state-transition/preview-overview.png new file mode 100644 index 0000000000..1323f03cbf Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/preview-overview.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/preview-view.png b/versions/4.0/en/animation/marionette/state-transition/preview-view.png new file mode 100644 index 0000000000..fdecb0b2f3 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/preview-view.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/progress.png b/versions/4.0/en/animation/marionette/state-transition/progress.png new file mode 100644 index 0000000000..865bd66531 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/progress.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/set-example.png b/versions/4.0/en/animation/marionette/state-transition/set-example.png new file mode 100644 index 0000000000..2a3f063e60 Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/set-example.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/transition.png b/versions/4.0/en/animation/marionette/state-transition/transition.png new file mode 100644 index 0000000000..32b89b075c Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/transition.png differ diff --git a/versions/4.0/en/animation/marionette/state-transition/trigger-condition.png b/versions/4.0/en/animation/marionette/state-transition/trigger-condition.png new file mode 100644 index 0000000000..1a8ee9073a Binary files /dev/null and b/versions/4.0/en/animation/marionette/state-transition/trigger-condition.png differ diff --git a/versions/4.0/en/animation/skeletal-animation.md b/versions/4.0/en/animation/skeletal-animation.md new file mode 100644 index 0000000000..aa077d6a52 --- /dev/null +++ b/versions/4.0/en/animation/skeletal-animation.md @@ -0,0 +1,124 @@ +# Skeletal Animation + +Skeletal animation is a common but special type of animation. We provide two systems, **pre-baked skeletal animation** and **realtime computed skeletal animation**, optimized for different directions. + +The only switch between these two systems is the `useBakedAnimation` property in the **SkeletalAnimation** component, which can also be switched seamlessly at runtime. + +- When `useBakedAnimation` is enabled, the pre-baked skeletal animation system is used. +- When `useBakedAnimation` is disabled, the real-time computed skeletal animation system will be used. + +For the component interface of skeletal animation, please refer to the [SkeletalAnimation API](%__APIDOC__%/en/class/SkeletalAnimation). + +## Pre-baked Skeletal Animation System + +The overriding purpose of this system is performance, so some of the sacrifice of expressiveness is considered acceptable. We have targeted a number of underlying optimizations, and the current runtime flow is roughly as follows: + +- All animation data is pre-sampled and baked in advance to a globally reused skeletal animation texture ensemble at a specified frame rate. +- Depending on whether the runtime platform supports floating point textures or not, an alternate scheme in **RGBA32F** or **RGBA8** format is used (this step of the process is of no concern to the user and will have no impact on the final performance, but is only a final underwriting strategy for low-end platforms). +- Each skeletal animation component (**SkeletalAnimation**) is responsible for maintaining the current playback progress, stored as a UBO (a Vec4). +- Each skinning model component (**SkinnedMeshRenderer**) holds the pre-baked skinning model class (BakedSkinningModel), calculates culling based on the same pre-baked wrap-around box information, updates the UBO, and finishes skinning by fetching the current data from within the texture ensemble on the GPU. + +## Real-time Computed Skeletal Animation System + +The overwhelming purpose of this system is expressiveness, ensuring that all details are displayed correctly, and complete programmatic control. + +The current runtime flow is roughly as follows: + +- All animation data is dynamically interpolated and calculated based on the current global time. +- Animation data is exported to the skeletal node tree of the scene. +- The user and any other system can have an effect on the skinning effect by manipulating this skeleton node tree. +- Each skinning model component (SkinnedMeshRenderer) holds the common skinning model class (SkinningModel), extracts the skeleton node tree information each frame to calculate culling, uploads the complete skeleton transformation information of the current frame to UBO, and finishes skinning inside the GPU. For more information about skinning, please refer to the **Skinning Algorithm** section below. + +This provides the most basic support for all the following functions: + +- blendshape support +- Blending and masking of an arbitrary number of animation clips +- IK, secondary physical effects +- purely procedural control of joint positions + +## Selection of two systems and best practices + +Currently all model assets are imported with **pre-baking system** in Prefab by default for best performance. It is recommended to use the **real-time computing system** only when it becomes apparent that the performance of the pre-baking system is not up to par. + +> **Note**: although the two systems can be switched seamlessly at runtime, try not to do that at high frequencies, as each switch involves the reconstruction of the underlying rendering data. + +## Skinning Algorithms + +Creator provides two common standard skinning algorithms built-in, which have similar performance and only have an impact on the final performance. + +1. **LBS (Linear Blending Skinning)**: skeletal information is stored as a **3 x 4** matrix, and skinning is achieved by direct linear interpolation of the matrix. Currently there are typical known issues such as volume loss. +2. **DQS (Dual Quaternion Skinning)**: skeletal information is interpolated as **Dual Quaternion**, which is more accurate and natural for skeletal animations that do not contain scaling transformations, but has approximate simplification for all scaling animations for performance reasons. + +The engine uses LBS by default, and the masking algorithm can be switched by modifying the `updateJointData` function reference in the engine `skeletal-animation-utils.ts` and the header file reference in `cc-skinning.chunk`. + +It is recommended that projects with high quality skinning animations try to enable DQS, but since there is no `fma` instruction until GLSL 400, operations such as `cross` cannot bypass the floating point offset problem on some GPUs and have large errors, which may introduce some visible defects. + +## Socket System + +If you need to attach some external nodes to a given skeletal joint, making them transform with skeletal joint together, you may need to use the **Socket System** of the skeletal animation component. Here is a simple example. + +### Implementing via the editor + +1. Create a new child node under the skeletal animation component to be docked (the immediate parent node should be the node where the animation component is located). +2. Add an array element to the **Sockets** property of the skeletal animation component, select the **Path** of the skeleton to be attached from the drop-down list (note that the defaultClip of the skeletal animation component must have a value, the options in the drop-down list depend on this property), and specify the child node just created as the **Target**. +3. This child node becomes the target socket, any external node can be put under this child node and will follow the transformation of the specified skeleton. + +![attach0](./animation/sockets-attach0.png) + +### Implementing via code + +```ts +let target = new Node(); +this.cubeNode.parent = target; // The cubeNode contains a cube model +let skeletalAnimation = this.node.getComponent(SkeletalAnimation); +target.parent = skeletalAnimation.node; // Setting target's parent node which contain a SkeletalAnimation component. +let path = "root/_rootJoint/b_Root_00/b_Hip_01/b_Tail01_012/b_Tail02_013/b_Tail03_014"; +let socket = new SkeletalAnimation.Socket(path, target); // Create Socket object with path and target +skeletalAnimation.sockets.push(socket); +``` +Click the Preview button, you will see the cube handing from the fox's tail and shaking along with the fox's tail. + +![attach1](./animation/sockets-attach1.gif) + +The socket model in the **FBX** or **glTF** asset will automatically interface to the socket system without any manual operation. + +## About Dynamic Instancing + +Based on the framework design of the **pre-baking system**, instancing of the skinned model is also within reach, but there is some more underlying information that needs to be collected to ensure correctness. + +The fundamental problem here is that each model within the same drawcall must use the same skeleton texture, if not, the display will be completely misaligned. So how the animation data is assigned to each skeleton texture becomes a user-defined piece of information that can be configured in the [Joint Texture Layout](joint-texture-layout.md) panel in the editor menu bar **Panel -> Animation**. + +> **Notes**: +> +> 1. Instancing is only supported under the **pre-baking system**. we have not strictly forbidden to enable instancing under the **real-time computation framework** (only in-editor warnings), but the animation effect will definitely be problematic, depending on the actual material assignment of the model. In the best case, the animation will be identical from instance to instance, in the worst case it will cause the model to be completely misaligned. +> 2. For models with instancing enabled in the material, the planar shading system will automatically draw with instancing as well. In particular, shading batches of skinned models require a higher level of joint texture layout, since the pipeline state of shading is uniform, and **all animations of skinned models with shadow enabled** need to be on the same texture (as opposed to drawing the model itself, which only requires consistent joint textures between instances within the same Drawcall). + +## Skinned Mesh Batch Renderer + +Currently, the joint textures uploaded to GPU on the bottom layer has been automatically batched and reused globally, and the upper layer data can now be combined by using the **SkinnedMeshBatchRenderer** component to merge all sub-skinned models controlled by the same skeletal animation component: + +![batched-skinning-model](./animation/batched-skinning-model-component.png) + +The batched version of effect is a bit more complicated to write, but basically it can be based on the normal effect used for sub-materials, with some relatively straightforward preprocessing and interface changes. See `builtin-unlit` in the editor's built-in asset (`util/batched-unlit`). + +> **Note**: only the pre-baked system can guarantee correctness when using the batch skinned model component, although it can be used in the real-time computing framework, there will be rendering problems when the number of merged skeletons exceeds 30 (the maximum number of Uniform arrays). + +### SkinnedMeshBatchRenderer component properties + +| Property | Description | +| :--- | :-- | +| Operation | Any changes will not take effect until **Cook** button is clicked to recalculate and apply. +| Materials | Custom effect is needed for this final material - the shader code should handle all the intricacies of the batching process. +| LightmapSettings | Used for lightmapping, please refer to [Lightmapping](.../../../concepts/scene/light/lightmap.md) for details. +| ShadowCastingMode | Specifies whether the current model will cast shadows, which needs to [enable shadow effect](.../../../concepts/scene/light/shadow.md#enable-shadow-effect) in the scene first. +| ReceiveShadow | Specifies whether the current model will receive and display shadow effects generated by other objects, which needs to [enable shadow effect](.../../../concepts/scene/light/shadow.md#enable-shadow-effect) in the scene first. This property takes effect only when the type of shadows is ShadowMap. +| SkinningRoot | The root node of the skeletal skin, usually the node where the SkeletalAnimationComponent is located. +| AtlasSize | The side length of the generated final atlas. +| BatchableTextureNames | The properties of the textures in the material that are actually involved in the atlas, and those are not involved use the texture of the first unit uniformly. +| Units | The sub-model infos before batching, which is the main source of data. +| Mesh | The model data of the current sub-model, usually from glTF or FBX. +| Skeleton | The skeletal data of the current sub-model, usually from glTF or FBX. +| Material | The "sub-material" used by the current sub-model is a non-batched version of the normal effect, and the effect used by different sub-models should be consistent. +| Offset | The offset of the current sub-model's textures inside the atlas, with the top-left corner of the atlas as the origin, in the range [0, 1], e.g.: the data in the figure represents that the sub-texture overlaps with the top-left corner of the atlas. +| Size | The size occupied by the current sub-model's textures inside the atlas, in the range [0, 1], e.g.: the data in the figure represents that the sub-texture occupies 1/2 of the entire atlas. +| CopyFrom | The target properties (except offset and size) can be copied automatically by dragging in the SkinningModelComponent for easy operation. diff --git a/versions/4.0/en/animation/use-animation-curve.md b/versions/4.0/en/animation/use-animation-curve.md new file mode 100644 index 0000000000..aa48ab66be --- /dev/null +++ b/versions/4.0/en/animation/use-animation-curve.md @@ -0,0 +1,323 @@ +# Using Animation Curves + +All keyframes added to an Animation Property of a node in an Animation Clip are shown as linear traces in the corresponding animation property track, which is an animation curve. That is, an Animation Curve describes the change in an Animation Property on an object over time. + +The Animation Curve stores internally a series of time points, each of which corresponds to a (curve) value, called a frame (keyframe). When the animation system runs, the Animation Component calculates the (result) value that should be assigned to the object at the specified time point based on the current animation state and completes the property change, a calculation process called sampling. + +The following code snippet demonstrates how to create Animation Clips programmatically: + +```ts +import { AnimationClip, animation, js } from 'cc'; +const animationClip = new AnimationClip(); +animationClip.duration = 1.0; // The entire period of the animation clip, the frame time of any keyframe should not be greater than this property +animationClip.keys = [ [ 0.3, 0.6, 0.9 ] ]; // The frame time shared by all curves of the clip +animationClip.curves = [{ // Animation curves on the Animation component + modifiers: [ // Address the target object from the current node object. See the "Target Objects" section below for details + // The target object is the "Body" child of the current node. + HierarchyPath('Body'), + // The target object is the "MyComponent" component on the "Body" child of the current node. + ComponentPath(js.getClassName(MyComponent)), + // The target object is the "value" property on the "MyComponent" component of the "Body" child of the current node. + 'value', + ], + data: { + // Indexed to 'animationClip.keys', i.e. [ 0.3, 0.6, 0.9 ] + keys: 0, + // Keyframe data + values: [ 0.0, 0.5, 1.0 ], + }, +}]; +``` + +The above Animation Clip contains an Animation Curve that controls the `value` property of the **MyComponent** component in the **Body** child node. The Animation Curve includes three keyframes that make the `value` property change to 0.0 at 0.3 seconds, 0.5 at 0.6 seconds, and 1.0 at 0.9 seconds. + +> **Note**: the frame times of the keyframes on the animation curve are indexed by reference to the `AnimationClip.keys` array. In this way, multiple curves can share frame times, which will result in additional performance optimization. + +## Target objects (`modifiers`) + +The target of an animation curve can be any JavaScript object. The `modifiers` field specifies how the target object is addressed from the current node object at **runtime**. + +`modifiers` is an array, each element of which expresses how to address an object from a higher level to another object, with the last element addressed as the target of the curve. This behavior is like a file system path, so each element is called a "target path". + +When the target path is `string` or `number`, it addresses the property of the higher-level object, which itself specifies the property name. Otherwise, the target path must be an object that implements the interface `animation.TargetPath`. + +Cocos Creator has the following built-in classes that implement the interface `animation.TargetPath`: +- `animation.HierarchyPath` treats a higher-level object as a node and addresses one of its children +- `animation.ComponentPath` treats a higher-level object as a node and addresses one of its components + +The target paths can be any combination, as long as they have the correct meaning: + +```ts +modifiers: [ + // The target object is the first element of the "names" property of the "BlahBlahComponent" component on the "nested_3" child node of the "nested_2" child node of the "nested_1" child node + new animation.HierarchyPath('nested_1/nested_2/nested_3'), + new animation.ComponentPath(js.getClassName(BlahBlahComponent)), + 'names', + 0, +] +``` + +Custom target paths are useful when the target object is not an property, but must be returned from a method: + +```ts +class BlahBlahComponent extends Component { + public getName(index: number) { return _names[index]; } + private _names: string[] = []; +} + +modifiers: [ + // The target object is the first "name" of the "BlahBlahComponent" component on the "nested_3" child node of the "nested_2" child node of the "nested_1" child node + new animation.HierarchyPath('nested_1/nested_2/nested_3'), + new animation.ComponentPath(js.getClassName(BlahBlahComponent)), + { + get: (target: BlahBlahComponent) => target.getName(0), + }, +] +``` + +For custom target paths to be serializable, declare them as classes: + +```ts +@ccclass +class MyPath implements animation.TargetPath { + @property + public index = 0; + constructor(index: number) { this.index = index; } + get (target: BlahBlahComponent) { + return target.getName(this.index); + } +} + +modifiers: [ + // The target object is the first "name" of the "BlahBlahComponent" component on the "nested_3" child node of the "nested_2" child node of the "nested_1" child node + new animation.HierarchyPath('nested_1/nested_2/nested_3'), + new animation.ComponentPath(js.getClassName(BlahBlahComponent)), + new MyPath(0), +] +``` + +The addressing of the target object is done at runtime, a feature that allows animation clips to be reused on multiple objects. + +## Assignment + +When a value is sampled, by default the value will be set to the target object using the assignment operator `=`. + +Sometimes, the assignment operator cannot be used to complete the setting. For example, when setting the Uniform of a material object, it can't be done with the assignment operator. For this case, the curve field `valueAdapter` provides a mechanism to customize the value to the target object. + +Example: + +```ts +class BlahBlahComponent { + public setUniform(index: number, value: number) { /* */ } +} + +{ // Curve + valueAdapter: { + // Called when the curve is instantiated + forTarget(target: BlahBlahComponent) { + // Do something useful here + return { + // Called each time the value of the target object is set + set(value: number) { + target.setUniform(0, value); + } + }; + } + }, +}; +``` + +For the **custom assignment** to be serializable, declare them as classes: + +```ts +@ccclass +class MyValueProxy implements animation. + @property + public index: number = 0; + constructor(index: number) { this.index = index; } + // Called when the curve is instantiated + public forTarget(target: BlahBlahComponent) { + // Do something useful here + return { + // Called each time the value of the target object is set + set(value: number) { + target.setUniform(0, value); + } + }; + } +} +``` + +`UniformProxyFactory` is one such **custom assignment** class that implements setting the uniform value of a material: + +```ts +{ + modifiers: [ + // The target object is the first material of the "sharedMaterials" property of the "MeshRenderer" component + ComponentPath(js.getClassName(MeshRenderer)), + 'sharedMaterials', + 0, + ], + valueAdapter: new animation.UniformProxyFactory( + 0, // Pass index + 'albedo', // Uniform name + ), +}; +``` + +## Sampling + +If the sampled time point is exactly equal to the time point of a keyframe, then the animation data on that keyframe is used. Otherwise, when the sampling time point is between two frames, the result value is affected by the data of both frames, and the scale of the sampling time point on the moment interval of the two keyframes (`[0, 1]`) reflects the extent of the effect. + +Cocos Creator allows this scale to be mapped to another scale to achieve a different "fade" effect. These mapping methods are called **fading methods** in Creator. After the scale is determined, the final result is calculated based on the specified **interpolation method**. + +> **Note**: both the fade method and the interpolation method affect the smoothness of the animation. + +### Fade method + +The fade method for each frame can be specified, or a uniform fade method can be used for all frames. The fade method can be the name of a built-in fade method or a Bessel control point. + +The following are some of the commonly used fade methods: + +- `linear`: maintains the original scale, i.e. linear gradient, which is used by default when no gradient is specified +- `constant`: always use scale 0, i.e. no gradient, similar to interpolation `Step`. +- `quadIn`: fade from slow to fast +- `quadOut`: fade from fast to slow +- `quadInOut`: fading from slow to fast to slow again +- `quadOutIn`: fade from fast to slow to fast + + + +### Curve values and interpolation methods + +Some interpolation algorithms require additional data to be stored in the curve value for each frame, so the curve value is not necessarily the same as the value type of the target property. For numeric types or value types, Cocos Creator provides several general interpolation methods. Also, it is possible to define custom interpolation methods. + +- When the `interpolate` property of the curve data is `true`, the curve will try to use the interpolation function. + + - If the curve value is of type `number`, `Number`, linear interpolation will be applied. + - If the curve value inherits from `ValueType`, the `lerp` function of `ValueType` will be called to complete the interpolation. `lerp` methods for most value types built into Cocos Creator are implemented as linear interpolation, e.g.: `Vec3`, `vec4`, etc. + - If the curve value is [interpolable](%__APIDOC__%/en/interface/ILerpable), the `lerp` function for the curve value will be called to complete the interpolation[^1]. + +- If the curve value does not satisfy any of the above conditions, or when the `interpolate` property of the curve data is `false`, no interpolation will be performed and the curve value from the previous frame will always be used as the result. + + ```ts + import { AnimationClip, color, IPropertyCurveData, SpriteFrame, Vec3 } from 'cc'; + + const animationClip = new AnimationClip(); + + const keys = [ 0, 0.5, 1.0, 2.0 ]; + animationClip.duration = keys.length === 0 ? 0 : keys[keys.length - 1]; + animationClip.keys = [ keys ]; // All curves share one column of frame time + + // Linear interpolation using values + const numberCurve: IPropertyCurveData = { + keys: 0, + values: [ 0, 1, 2, 3 ], + /* interpolate: true, */ // "interpolate" property is on by default + }; + + // Using lerp() of value type Vec3 + const vec3Curve: IPropertyCurveData = { + keys: 0, + values: [ new Vec3(0), new Vec3(2), new Vec3(4), new Vec3(6) ], + interpolate: true, + }; + + // No interpolation (because interpolation is explicitly disabled) + const colorCuve: IPropertyCurveData = { + keys: 0, + values: [ color(255), color(128), color(61), color(0) ], + interpolate: false, // No interpolation + }; + + // No interpolation (because SpriteFrame cannot be interpolated) + const spriteCurve: IPropertyCurveData = { + keys: 0, + values: [ + new SpriteFrame(), + new SpriteFrame(), + new SpriteFrame(), + new SpriteFrame() + ], + }; + ``` + +- Custom interpolation algorithm + + The sample code is as follows: + + ```ts + import { ILerpable, IPropertyCurveData, Quat, quat, Vec3, vmath } from 'cc'; + + class MyCurveValue implements ILerpable { + public position: Vec3; + public rotation: Quat; + + constructor(position: Vec3, rotation: Quat) { + this.position = position; + this.rotation = rotation; + } + + /** This method will be called for interpolation + * @param this initial curve value + * @param to target curve value + * @param t interpolation ratio, in the range [0, 1] + * @param dt the frame time interval between the starting curve value and the target curve value + */ + lerp (to: MyCurveValue, t: number, dt: number) { + return new MyCurveValue( + // The position property is not interpolated + this.position.clone(), + // The rotation property uses Quat's lerp() method + this.rotation.lerp(to.rotation, t), // + ); + } + + /** This method is called when not interpolating + * It is optional, if this method is not defined, then the curve value itself (i.e. this) is used as the result value + */ + getNoLerp () { + return this; + } + } + + /** + * Creates a curve that implements a smooth rotation but abrupt position change throughout the cycle. + */ + function createMyCurve (): IPropertyCurveData { + const rotation1 = quat(); + const rotation2 = quat(); + const rotation3 = quat(); + + vmath.quat.rotateY(rotation1, rotation1, 0); + vmath.quat.rotateY(rotation2, rotation2, Math.PI); + vmath.quat.rotateY(rotation3, rotation3, 0); + + return { + keys: 0 /* frame time */, + values: [ + new MyCurveValue(new Vec3(0), rotation1), + new MyCurveValue(new Vec3(10), rotation2), + new MyCurveValue(new Vec3(0), rotation3), + ], + }; + } + ``` + +## Wrap mode + +Different wrap modes can be set for Animation Clips by setting `AnimationClip.wrapMode`. The following lists several common wrap modes: + +| Property | Description | +| :--- | :--- | +| Normal | Playback stops at the end | +| WrapMode.Loop | Loop +| PingPong | Play from the beginning to the end of the animation, then play back to the beginning from the end, and so on. + +For more wrap modes, please refer to the [WrapMode](%__APIDOC__%/en/class/AnimationState?id=wrapMode) API and the [Wrap Mode and Repeat Count](./animation-state.md#wrap-mode-and-repeat-count) documentation. + +[^1]: For numeric, quaternion, and various vectors, Cocos Creator provides the appropriate interpolate classes to implement [Cubic Spline Interpolation](https://en.wikipedia.org/wiki/Spline_interpolation). diff --git a/versions/4.0/en/asset/asset-manager-upgrade-guide.md b/versions/4.0/en/asset/asset-manager-upgrade-guide.md new file mode 100644 index 0000000000..dd5a26415f --- /dev/null +++ b/versions/4.0/en/asset/asset-manager-upgrade-guide.md @@ -0,0 +1,380 @@ +# Asset Manager Upgrade Guide + +> This article details what to expect when upgrading from loader to assetManager. + +Before Creator v2.4, [Acquire and load asset](https://github.com/cocos/cocos-docs/blob/e02ac31bab12d3ee767c0549050b0e42bd22bc5b/en/scripting/load-assets.md) was implemented through the `loader` module (including the APIs `loader.load`, `loader.loadRes`, `loader.loadResDir`, etc.), which was primarily used to load resources. However, with the continuous development of Creator, developers' demands for resource management have been increasing. The original `loader` module has been unable to meet a large number of resource management needs, and a new resource management module is in the air. + +Therefore, Creator in **v2.4** introduced a new resource management module -- **Asset Manager**. Compared to the previous `loader` module, **Asset Manager** not only provides better loading performance, but also supports **Asset Bundle**, preload resources and more convenient resource release management. And **Asset Manager** also has strong extensibility, which greatly improves the development efficiency and user experience of developers. We recommend that all developers upgrade. + +To bring a smooth upgrade experience, we will maintain compatibility with `loader` related APIs, and most of the game project can run as usual, except for a few that use incompatible special usage APIs that must be manually upgraded. And we will only remove full compatibility with `loader` when the time comes. If developers are temporarily uncomfortable upgrading due to the project cycle, etc., keep the original writing while making sure the test passes. + +Currently, when using those old APIs, the engine will output warnings and suggestions for upgradation. Please adjust the code according to the warnings and the instructions in this document and upgrade to the new usage. Unfortunately, due to an upgrade of the underlying layer, we have left behind a few incompatible APIs that will output error messages while running. If deciding to make the upgrade, please read the following carefully: + +- For the **Artist and Game Designer**, all resources in your project (e.g.: scenes, animations, prefab) do not need to be modified or upgraded. +- For **Programmers**, all APIs in the `loader` module that were used in the original code need to be changed to APIs from `assetManager`. The related content will be described in detail in this document. + +> **Note**: as v2.4 supports **Asset Bundle**, the subpackage feature in the project also needs to be upgraded, please refer to the [Subpackage Upgrade Guide](./subpackage-upgrade-guide.md) documentation for details. + +## Situations that require upgrading manually + +- Using APIs that start with `loader` in custom code, such as `loader.loaderRes`, `loader.loadResDir`, `loader.release`, etc. +- Using APIs that start with `AssetLibrary` in custom code, such as `AssetLibrary.loadAsset`. +- Using an API that starts with `url` in custom code, such as `url.raw`. +- Using types such as `Pipeline`, `LoadingItems` in custom code. +- Using the `macro.DOWNLOAD_MAX_CONCURRENT` property in custom code. + +## Upgrade steps + +- **Back up your old projects** +- Use Cocos Creator **v2.4** in the **Dashboard** to open the project that needs to be upgraded, Creator will reimport the affected resources. The first import will take a little longer, and the main editor window will open after the import is complete. And more error or warning may appear on the **Console** panel, don't worry, open the code editor to update your code according to the error or warning message. + +### Replace the `loader` related API with the `assetManager` related API + +As of v2.4, `loader` is no longer recommended and will be completely removed in subsequent releases, please replace it with the new resource management module `assetManager`. + +#### The relevant interface replacement about loading + +If using `loader.loadRes`, `loader.loadResArray`, `loader.loadResDir` in custom code, use the corresponding API in `assetManager` for the replacement. Refer to the following replacements: + +- **loader.loadRes** + + The parameters of `resources.load` are exactly equal to `loader.loadRes`. Replace with the following: + + ```typescript + // before + loader.loadRes(...); + + // after + resources.load(...); + ``` + +- **loader.loadResArray** + + For reducing learning costs, `loadResArray` has been merged with `load` and the first parameter of `resources.load` can support multiple paths, use `resources.load` to replace. + + ```typescript + // before + loader.loadResArray(...); + + // after + resources.load(...); + ``` + +- **loader.loadResDir** + + The parameters of `resources.loadDir` are equal to those of `loader.loadResDir`. + + ```typescript + // before + loader.loadResDir(...); + + // after + resources.loadDir(...); + ``` + + > **Note**: to simplify the interface, the load completion callback for `resources.loadDir` will **no longer** provide a list of paths. Please avoid using the following: + + ```typescript + loader.loadResDir('images', Texture2D, (err, assets, paths) => console.log(paths)); + ``` + + If you want to query the paths list, use the following form: + + ```typescript + const infos = resources.getDirWithPath('images', Texture2D); + let paths = infos.map(function (info) { + return info.path; + }); + ``` + +- **loader.load** + + If using `loader.load` to load remote images or audios in custom code, there is a special API for this in the `assetManager` for ease of understanding, as follows: + + - **Loading remote images** + + ```typescript + // before + loader.load('http://example.com/remote.jpg', (err, texture) => console.log(texture)); + + // after + assetManager.loadRemote('http://example.com/remote.jpg', (err, texture) => console.log(texture)); + ``` + + - **Loading remote audio** + + ```typescript + // before + loader.load('http://example.com/remote.mp3', (err, audioClip) => console.log(audioClip)); + + // after + assetManager.loadRemote('http://example.com/remote.mp3', (err, audioClip) => console.log(audioClip)); + ``` + + - **Loading remote text** + + ```typescript + // before + loader.load('http://example.com/equipment.txt', (err, text) => console.log(text)); + + // after + assetManager.loadRemote('http://example.com/equipment.txt', (err, textAsset) => console.log(textAsset.text)); + ``` + +> **Notes**: +> 1. If using `loader.downloader.loadSubpackage` in custom code to load a subpackage, please refer to the [Subpackage Upgrade Guide](./subpackage-upgrade-guide.md) to upgrade it. +> 2. To avoid unnecessary errors, `loader.onProgress` has no equivalent implementation in `assetManager`. To implement a custom global callback mechanism, but it is recommended to pass callbacks to each load function to avoid interfering with each other during concurrent loading. + +#### The relevant interface replacement about releasing + +If using `loader.release`, `loader.releaseAsset`, `loader.releaseRes`, `loader.releaseResDir` in custom code, please use the corresponding API in `assetManager` for replacement. Refer to the following replacements: + +- **loader.release** + + `loader.release` can be replaced with `assetManager.releaseAsset`. + + > **Note**: in order to avoid user attention to some obscure properties of the resource, `assetManager.releaseAsset` **no longer** accepts arrays, resource UUIDs, resource URLs for release, only the resource itself can be accepted for release. + + ```typescript + // before + loader.release(texture); + // after + assetManager.releaseAsset(texture); + + // before + loader.release([texture1, texture2, texture3]); + // after + [texture1, texture2, texture3].forEach(t => assetManager.releaseAsset(t)); + + // before + const uuid = texture._uuid; + loader.release(uuid); + // after + assetManager.releaseAsset(texture); + + // before + const url = texture.url; + loader.release(url); + // after + assetManager.releaseAsset(texture); + ``` + + > **Note**: to increase ease of use, releasing resource dependencies in `assetManager` will **no longer require** manual access to resource dependencies, and an attempt will be made within `assetManager.releaseAsset` to automatically release the associated dependencies, for example: + + ```typescript + // before + const assets = loader.getDependsRecursively(texture); + loader.release(assets); + + // after + assetManager.releaseAsset(texture); + ``` + +- **loader.releaseAsset** + + `loader.releaseAsset` can be replaced directly with `assetManager.releaseAsset`. + + ```typescript + // before + loader.releaseAsset(texture); + + // after + assetManager.releaseAsset(texture); + ``` + +- **loader.releaseRes** + + `operator.releaseRes` can be replaced directly with `resources.release`. + + ```typescript + // before + loader.releaseRes('images/a', Texture2D); + + // after + resources.release('images/a', Texture2D); + ``` + +- **loader.releaseAll** + + `loader.releaseAll` can be replaced directly with `assetManager.releaseAll`. + + ```typescript + // before + loader.releaseAll(); + + // after + assetManager.releaseAll(); + ``` + +> **Notse**: +> 1. For security reasons, `loader.releaseResDir` does not have a corresponding implementation in `assetManager`, please use `assetManager.releaseAsset` or `resources.release` for individual resource releases. +> 2. Since the `assetManager.releaseAsset` automatically releases dependent resources, it is no longer necessary to explicitly call `loader.getDependsRecursively`. If needing to find the dependency of the resource, please refer to the relevant API in `assetManager.dependUtil`. +> 3. For security reasons, `assetManager` only supports the Auto Release property set in the scene, and `loader.setAutoRelease`, `loader.setAutoReleaseRecursively`, `loader.isAutoRelease` APIs have been removed. It is recommended to use the new auto-release mechanism based on reference counting. Please refer to the [Release Of Resources](release-manager.md) documentation for details. + +#### Extension-related interface replacements + +- **Pipeline** + + If using methods in custom code that use `loader.insertPipe`, `loader.insertPipeAfter`, `loader.appendPipe`, `loader.addDownloadHandlers`, `loader.addLoadHandlers` series APIs to extend the loading process of `loader`, or directly use `loader.assetLoader`, `loader.md5Pipe`, `loader.downloader`, `loader.loader`, `loader.subPackPipe`, here are the detailed alternatives. + + Because `assetManager` is a more general module and no longer inherits from `Pipeline`, `assetManager` no longer implements `handler.insertPipe`, `handler.insertPipeAfter`, `handler.appendPipe`. Please replace with the following code: + + ```typescript + // before + const pipe1 = { + id: 'pipe1', + handle: (item, done) => { + let result = doSomething(item.uuid); + done(null, result); + } + }; + + const pipe2 = { + id: 'pipe2', + handle: (item, done) => { + let result = doSomething(item.content); + done(null, result); + } + }; + + loader.insertPipe(pipe1, 1); + loader.appendPipe(pipe2); + + // after + function pipe1 (task, done) { + let output = []; + for (let i = 0; i < task.input.length; i++) { + let item = task.input[i]; + item.content = doSomething(item.uuid); + output.push(item); + } + + task.output = output; + done(null); + } + + function pipe2 (task, done) { + let output = []; + for (let i = 0; i < task.input.length; i++) { + let item = task.input[i]; + item.content = doSomething(item.content); + output.push(item); + } + + task.output = output; + done(null); + } + + assetManager.pipeline.insert(pipe1, 1); + assetManager.pipeline.append(pipe2); + ``` + + > **Notes**: + > 1. `assetManager` **no longer** inherits by `Pipeline`, but by multiple `Pipeline` instances owned under `assetManager`. Please refer to the [Pipeline and Task](pipeline-task.md) documentation for details. + > 2. For ease of use, the definition of Pipe no longer requires the definition of an object with a `handle` method and an `id`, just a single method. See [Pipeline and Task](pipeline-task.md) documentation for details. + > 3. In order to simplify the logic and improve performance, what is processed in Pipe is no longer a `item` but a `task` object, see [Pipeline and Task](pipeline-task.md) documentation for details. + > 4. In order to reduce learning costs, APIs in the form of `insertPipeAfter` are no longer supported in `Pipeline`, so please use `insert` to insert the specified location. + +- **addDownloadHandlers, addLoadHandlers** + + For modularity reasons, `addDownloadHandlers` and `addLoadHandlers` are not implemented in `assetManager`, please refer to the following for replacement: + + ```typescript + // before + const customHandler = (item, cb) => { + let result = doSomething(item.url); + cb(null, result); + }; + + loader.addDownloadHandlers({png: customHandler}); + + // after + const customHandler = (url, options, cb) => { + let result = doSomething(url); + cb(null, result); + }; + + assetManager.downloader.register('.png', customHandler); + ``` + + Or: + + ```typescript + // before + const customHandler = (item, cb) => { + let result = doSomething(item.content); + cb(null, result); + }; + + loader.addLoadHandlers({png: customHandler}); + + // after + const customHandler = (file, options, cb) => { + let result = doSomething(file); + cb(null, result); + }; + + assetManager.parser.register('.png', customHandler); + ``` + + > **Notes**: + > 1. Since both the **download module** and the **parsing module** rely on **extensions** to match the corresponding processing method. So when calling `register`, the incoming first parameter needs to start with `.`. + > 2. For the sake of modularity, the custom processing method will no longer pass in an `item` object, but will pass in its associated information directly. The custom processing method of `downloader` passes in **the URL to be downloaded**, and `parser` passes in **the file to be parsed**. For more information about `downloader` and `parser`, please refer to the [Download and Parse](downloader-parser.md) documentation. + > 3. The new expansion mechanism provides an additional `options` parameter that can greatly increase flexibility. However, if it is not necessary to configure the engine's built-in or custom parameters, ignore it. Please refer to the [Optional parameter](options.md) documentation for details. + +- **downloader, loader, md5Pipe, subPackPipe** + + `loader.downloader` can be replaced by `assetManager.downloader`, and `loader.loader` can be replaced by `assetManager.parser`. For details, see [Download and Parse](downloader-parser.md) documentation or the corresponding API documentation [assetManager.downloader](%__APIDOC__%/en/class/AssetManager?id=downloader) and [assetManager.parser](__APIDOC__/en/class/AssetManager?id=parser). + + > **Note**: for performance, modularity and readability reasons, `loader.assetLoader`, `loader.md5Pipe`, `loader.subPackPipe` have been merged into `assetManager.transformPipeline`. Remember to avoid using any of the methods and properties in these three modules. Details about `assetManager.transformPipeline` can be found in [Pipeline and Tasks](pipeline-task.md) documentation. + +### Other changes + +The `url` and `AssetLibrary` have been removed, so avoid using any methods and properties of `url` and `AssetLibrary`. + +`Pipeline` can be replaced by `AssetManager.Pipeline`: + +```typescript +// before +const pipe1 = { + id: 'pipe1', + handle: function (item, cb) { + let result = doSomething(item); + cb(null, result); + } +} + +const pipeline = new Pipeline([pipe1]); + +// after +function pipe1 (task, cb) { + task.output = doSomething(task.input); + cb(null); +} + +const pipeline = new AssetManager.Pipeline('test', [pipe1]); +``` + +> **Note**: `LoadingItem` is no longer supported in `assetManager`, please avoid using this type. + +To support more loading strategies, `macro.DOWNLOAD_MAX_CONCURRENT` has been removed from `macro` and replace it with the following: + +```typescript +// before +macro.DOWNLOAD_MAX_CONCURRENT = 10; + +// after +assetManager.downloader.maxConcurrency = 10; +``` + +Or: + +```typescript +// before +macro.DOWNLOAD_MAX_CONCURRENT = 10; + +// after (set a preset value) +assetManager.presets['default'].maxConcurrency = 10; +``` + +Please refer to the [Download and Parse](downloader-parser.md) documentation for details. diff --git a/versions/4.0/en/asset/asset-manager.md b/versions/4.0/en/asset/asset-manager.md new file mode 100644 index 0000000000..0fb355b612 --- /dev/null +++ b/versions/4.0/en/asset/asset-manager.md @@ -0,0 +1,151 @@ +# Asset Manager Overview + +> Author: Santy-Wang, Xunyi + +During the development of the game, it is generally necessary to use a large number of images, audio and other resources to enrich the entire game, and a large number of resources can cause management difficulties. That's why Creator provides the **Asset Manager** resource management module to help developers manage the use of their resources and greatly improve the development efficiency and experience. + +**Asset Manager** is a new resource manager from Creator in v2.4 that replaces the previous `loader`. The new **Asset Manager** resource management module has features for loading resources, finding resources, destroying resources, caching resources, Asset Bundle, and more. Compared with previous `loader`, Asset Manager has better performance, easier-to-use APIs, and greater extensibility. All functions and methods are accessible via `assetManager` and all types and enumerations are accessible via the `AssetManager` namespace. + +> **Note**: we will maintain compatibility with `loader` for a period of time, but we strongly recommend using **Asset Manager** consistently for new projects. + +Refer to the following documentations for upgrading: +- [AssetManager Upgrade Guide](asset-manager-upgrade-guide.md) +- [Asset Bundle Upgrade Guide](subpackage-upgrade-guide.md) + +## Load Resources + +### Dynamic Loading Of Resources + +In addition to applying resources to the corresponding components while editing scenes, Creator also supports dynamic loading and setting up of resources while the game is running. Asset Manager provides two ways to dynamically load resources: + +1. By placing resources in the `resources` directory, and working with APIs such as `resources.load` to achieve dynamic loading. +2. developers can plan their own resource creation as Asset Bundle and load resources through the Asset Bundle's `load` family of APIs. For example: + + ```typescript + resources.load('images/background/spriteFrame', SpriteFrame, (err, asset) => { + this.getComponent(Sprite).spriteFrame = asset; + }); + ``` + +The relevant APIs are listed below: + +| Type | Support | Loading | Releasing | Preloading | Querying | Search | +| :-- | :-- | :-- | :-- | :-- | :-- |:-- | +| **Single Asset** | Asset Bundle | load | release | preload | get | getInfoWithPath | +| **Directory** | Asset Bundle | loadDir | releaseAsset | preloadDir | N/A | getDirWithPath | +| **Scene** | Asset Bundle | loadScene | N/A | preloadScene | N/A | getSceneInfo | +| **Single Asset** | `resources` | load | release | preload | get | getInfoWithPath | +| **Directory** | `resources` | loadDir | releaseAsset | preloadDir | N/A | getDirWithPath | +| **Remote Asset** | Asset Manager | loadRemote | releaseAsset | N/A  | N/A | N/A | + +References documentations: + +- [Dynamic Load Asset](dynamic-load-resources.md) + +All loaded resources are cached in `assetManager`. + +### Preloading + +To reduce download latency, `assetManager` and Asset Bundle not only provides interfaces for loading resources, each interface also provides a corresponding preloaded version. Preloading in a game is an option that then allows finishing loading when it is really need it. Preloading will only download the necessary resources and will not perform deserialization or initialization. Therefore, it consumes less performance and is suitable for use during the game. + +```typescript +start () { + resources.preload('images/background/spriteFrame', SpriteFrame); + setTimeOut(this.loadAsset.bind(this), 10000); +} + +loadAsset () { + resources.load('images/background/spriteFrame', SpriteFrame, (err, asset) => { + this.getComponent(Sprite).spriteFrame = asset; + }); +} +``` + +For more information on preloading, please refer to the [Preloading and Loading](preload-load.md) documentation. + +## Asset Bundle + +Partitioning scenes, resources, and code into multiple Asset Bundles and the loading of resources dynamically at runtime results in modularity of resources and allows loading corresponding resources when needed. For example: + +```typescript +assetManager.loadBundle('testBundle', function (err, bundle) { + bundle.load('textures/background/texture', (err, asset) => { + // ... + }); +}); +``` + +Please refer to the [Asset Bundle](bundle.md) documentation for more information on Asset Bundle. + +## Release of resources + +Asset Manager provides a more convenient mechanism for releasing resources. When releasing a resource, you only need to focus on the resource itself and no longer on its dependent resources. The engine attempts to release its dependent resources based on the number of references, to reduce the complexity of managing resource releases for users. For example: + +```typescript +resources.load('prefabs/enemy', Prefab, function (err, asset) { + assetManager.releaseAsset(asset); +}); +``` + +Creator also provides a reference counting mechanism to help you control the reference and release of resources. For example: + +- When you need to hold a resource, call `addRef` to add a reference to ensure that the resource is not automatically released by other references to it. + + ```typescript + resources.load('textures/armor/texture', Texture2D, function (err, texture) { + texture.addRef(); + this.texture = texture; + }); + ``` + +- When you no longer need to hold the resource, call `decRef` to reduce reference, `decRef` will also attempt an automatic release based on the reference count. + + ```typescript + this.texture.decRef(); + this.texture = null; + ``` + +Please refer to the [Release of Resources](release-manager.md) documentation for more details. + +## Cache Manager + +On some platforms, such as WeChat, it is possible to use the file system to cache some remote resources because a file system exists. In this case, a cache manager is required to manage all cache resources, such as caching resources, clearing cache resources, modifying cache cycles, etc. . Creator provides a cache manager on all platforms where file systems exist that allows adding, deleting, changing, and checking the cache. For example: + +```typescript +// Get the cache of a resource. +assetManager.cacheManager.getCache('http://example.com/bundle1/import/9a/9aswe123-dsqw-12xe-123xqawe12.json'); + +// Clear the cache of a resource. +assetManager.cacheManager.removeCache('http://example.com/bundle1/import/9a/9aswe123-dsqw-12xe-123xqawe12.json'); +``` + +Please refer to the [Cache Manager](cache-manager.md) documentation for more information on Cache Manager. + +## Optional Parameters + +Some of the interfaces for `assetManager` and Asset Bundle have an additional `options` parameter, which greatly increase the flexibility and extend the space. In addition to configuring the builtin parameters of Creator, it is also possible to customize any of the parameters in `options`, and these parameters will be provided to the downloader, parser, and loading pipeline. + +```typescript +bundle.loadScene('test', {priority: 3}, callback); +``` + +For more information on `options` parameter, please refer to the [Optional Parameters](options.md) documentation. + +If it is not necessary to configure the engine's builtin parameters or custom parameters to extend the engine's functionality, ignore it and use the simpler API interfaces, such as `resources.load`. + +## Loading Pipeline + +To make it easier to extend the resource loading process, the underlying layer of Asset Manager uses mechanisms called **Pipeline and Task** and **Download and Parse** to load resources, greatly increasing flexibility and scalability. To expand the load pipeline or customize it, refer to the following documentation: + +- [Pipeline and Task](pipeline-task.md) +- [Download and Parse](downloader-parser.md) + +## More Reference + +- [Asset Bundle](bundle.md) +- [Release Of Resources](release-manager.md) +- [Download and Parse](downloader-parser.md) +- [Loading and Preloading](preload-load.md) +- [Cache Manager](cache-manager.md) +- [Optional Parameters](options.md) +- [Pipeline and Task](pipeline-task.md) diff --git a/versions/4.0/en/asset/asset-workflow.md b/versions/4.0/en/asset/asset-workflow.md new file mode 100644 index 0000000000..a8f4386063 --- /dev/null +++ b/versions/4.0/en/asset/asset-workflow.md @@ -0,0 +1,43 @@ +# Assets workflow + +## Importing assets + +There are three ways to **import assets**: + +- Create a new file through the **Assets** panel in Cocos Creator. Use the **Create button** to start the importing process. +- By copying files, in the **file manager of the operating system**, to the project asset folder, and then open the editor or activate the editor window to automatically refresh the asset list of the **Assets** panel to finish importing assets. +- Drag and drop asset files from the **file manager of the operating system** to a folder location on the **Assets** panel. This will trigger an import of the selected assets. + +## Syncing Assets + +The assets in the **Assets** panel are synchronized with the project asset files seen in the **file manager of the operating system**. Assets are **moved**, **renamed**, and **deleted** in the **Assets** panel. +- If deleted in Cocos Creator, it will be deleted in the **file manager of the operating system**. +- If deleted in the **file manager of the operating system**, it will be deleted in Cocos Creator. + +## Asset Configuration Information `.meta` File + +All asset files will generate a `.meta` configuration file with the same name when imported. This configuration file provides the unique identification (**UUID**) of the asset in the project, small image references, cropping data of texture assets, as well as other configuration information. This data is a necessary factor in identifying a legitimate asset that Cocos Creator is using. + +The `.meta` file is not visible in the **Assets** panel. When operating in the **Assets** panel, the **renaming**, **moving**, and **deleting** of an __asset__ will automatically synchronize the `.meta` file that corresponds to the __asset__ by the Editor. To ensure that configuration information such as the **UUID** remains unchanged, that is, it does not affect existing references. + +It is not recommended to operate the asset file directly in the __file manager of the operating system__. If there is such an operation, please manually operate the corresponding `.meta` file along with the __asset__ file. The following suggestions are recommended: + +- Close the editor you are using to avoid update failures due to file locks or identical asset names. +- When **deleting**, **renaming**, or **moving** assets, please **delete**, **rename**, and **move** the `.meta` file as needed. +- When copying assets together with `.meta` files, the copied `.meta` files will be used directly instead of generating new `.meta` files; if only the asset files are copied, a new `.meta` file with the corresponding name will be generated when you re-open the editor. This asset will become a new asset to the editor. + +## Assets in Library + +After the asset is imported, new data will be generated and stored in the project's **Library** folder. The structure and assets of the files in **Library** are engine-oriented and the format required for the final game, that is, __machine-friendly__, but __not human-friendly__. + +When a library is lost or damaged, just delete the entire library folder and open the project, and the asset library will be rebuilt. + +## How to locate assets + +A asset has a unique **UUID**, used to locate the asset, but this method is not intuitive enough. There is another intuitive way: **Database URL** format, such as an `asset-db`. The corresponding protocol header is `db://assets`, the protocol header for `internal-db` is `db://internal`. + +There are folder-level asset formats, such as `db://assets/prefabs/fire.prefab` + +## SVN or GIT syncing of assets + +> **Note**: there are line breaks in the `.meta` file. It is recommended to unify the line break styles and rules of the team members' computers to avoid opening the project after synchronizing the project assets. diff --git a/versions/4.0/en/asset/atlas.md b/versions/4.0/en/asset/atlas.md new file mode 100644 index 0000000000..d614813b8f --- /dev/null +++ b/versions/4.0/en/asset/atlas.md @@ -0,0 +1,47 @@ +# Atlas assets + +__Atlas__, also called a __Sprite Sheet__, is a common art asset in game development. __Atlas__ is an asset for merging multiple pictures into a large picture through a special tool, and indexing through a file such as a **.plist**. __Atlas__ assets available for __Cocos Creator__ consist of a **.plist** and at least one **.png** file, although usually many **.png** files make up an __Atlas__. The following is an image file used in an __Atlas__: + +![atlas sheep](atlas/sheep_atlas.png) + +## Why use atlas assets + +In a game, using an __Atlas__ composed of multiple pictures as art assets has the following advantages: + +- The blank area around each picture will be removed when synthesizing the __Atlas__, plus various optimization algorithms can be implemented as a whole. After synthesizing the __Atlas__, the game package and memory consumption can be greatly reduced. +- When multiple __Sprites__ are rendering pictures from the same atlas, these __Sprites__ can be processed using the same rendering batch, which greatly reduces the CPU's computing time and improves operating efficiency. + +For a more comprehensive explanation, watch a teaching video: [__What is a Sprite Sheet__](https://www.codeandweb.com/what-is-a-sprite-sheet) from __CodeAndWeb__. + +## Atlas Assets + +To generate an __Atlas__, you should first prepare a set of original pictures. Example: + +![single sheep](atlas/single_sheep.png) + +__Next__, use special software to generate the __Atlas__. Examples: + +- [TexturePacker 4.x](https://www.codeandweb.com/texturepacker) + + +When using these software packages to generate an __Atlas__, please select a `.plist` file in **Cocos2d-x** format. The resulting __Atlas__ files are a `.plist` and `.png` with the same name. Example: `myAtlas.plist` and `myAtlas.png`. + +![atlas files](atlas/atlas_files.png) + +When using __TexturePacker__ software, please note that use __v4.x__ only, v3.x and below is not supported. + +## Importing Atlas Assets + +Drag the **.plist** and the **.png** files shown above into the **Assets** panel at the same time. It is possible to generate __Atlas__ assets that can be used in the editor and scripts. + +### Atlas and SpriteFrame + +In the [Image Resource Document](../ui-system/components/editor/sprite.md), the relationship between __Texture__ and __SpriteFrame__ was introduced. After importing the __Atlas__, we can see that the __Atlas__ is of type __Atlas__ and can be expanded by clicking the triangle icon on the left. After expanding, we can see that the __Atlas__ contains many sub-assets of type __SpriteFrame__. Assets are pictures that can be used and referenced individually. + +![sprite frame](atlas/spriteframes.png) + +The use of the __Sprite Frame__ is the same as that described in the image asset. Please refer to the related documents. + + diff --git a/versions/4.0/en/asset/atlas/atlas_files.png b/versions/4.0/en/asset/atlas/atlas_files.png new file mode 100644 index 0000000000..89a36353ce Binary files /dev/null and b/versions/4.0/en/asset/atlas/atlas_files.png differ diff --git a/versions/4.0/en/asset/atlas/sheep_atlas.png b/versions/4.0/en/asset/atlas/sheep_atlas.png new file mode 100644 index 0000000000..4be0dbd0ee Binary files /dev/null and b/versions/4.0/en/asset/atlas/sheep_atlas.png differ diff --git a/versions/4.0/en/asset/atlas/single_sheep.png b/versions/4.0/en/asset/atlas/single_sheep.png new file mode 100644 index 0000000000..f14dd4f2f4 Binary files /dev/null and b/versions/4.0/en/asset/atlas/single_sheep.png differ diff --git a/versions/4.0/en/asset/atlas/spriteframes.png b/versions/4.0/en/asset/atlas/spriteframes.png new file mode 100644 index 0000000000..fe647d1ac3 Binary files /dev/null and b/versions/4.0/en/asset/atlas/spriteframes.png differ diff --git a/versions/4.0/en/asset/audio-clip.png b/versions/4.0/en/asset/audio-clip.png new file mode 100644 index 0000000000..b37bd89d6d Binary files /dev/null and b/versions/4.0/en/asset/audio-clip.png differ diff --git a/versions/4.0/en/asset/audio.md b/versions/4.0/en/asset/audio.md new file mode 100644 index 0000000000..1afa123381 --- /dev/null +++ b/versions/4.0/en/asset/audio.md @@ -0,0 +1,21 @@ +# Audio Assets + +Cocos Creator supports importing most common audio file formats, just drag and drop them directly into the **Assets** panel, and the corresponding **AudioClip** will be generated in the **Assets** panel after import. + +![audio-clip](audio-clip.png) + +The audio can be divided into longer length **Music** and shorter length **Sound Effects** based on their length. Creator controls the playback of different audio assets through the AudioSource component to implement in-game background music and sound effects. For more details, please refer to the [AudioSource Component Reference](../audio-system/audiosource.md). + +## Supported Audio Formats + +Currently Cocos Creator supports importing audio files in the following formats: + +| Audio Format | Description | +|:-- | :-- | +| `.ogg` | `.ogg` is an open source lossy audio compression format, which has the advantage of supporting multi-channel encoding and using a more advanced acoustic model to reduce the loss of sound quality, while the file size is smaller than `.mp3` format under the same conditions. Currently all the built-in ringtones for Android also use `.ogg` files. [This format is not supported by iOS](https://developer.apple.com/documentation/audiotoolbox/audiofiletypeid) | +|`.mp3` | `.mp3` is the most common digital audio encoding and lossy compression format. The purpose of compression is to compress PCM audio material into smaller files by discarding parts of the material that are not important to human hearing. MP3 is supported by a large number of hardware and software, and is widely used, and is currently the mainstream. | +| `.wav` | `.wav` is a standard digital audio file developed by Microsoft and IBM specifically for Windows. This file can record various mono or stereo sound information, and can ensure that the sound is not distorted because the audio format is not compressed. However, the file size is relatively large. | +| `.mp4` | `.mp4` is a set of compression coding standards for audio and video information. Different coding algorithms can be used for different objects to further improve the compression efficiency. | +| `.m4a` | `.m4a` is an audio-only MP4 file. The audio quality is very high among the compression formats, and the file footprint is smaller at the same bit rate. | + +With different audio encoding formats, the generated audio files vary in size and sound quality under the same conditions. diff --git a/versions/4.0/en/asset/auto-atlas.md b/versions/4.0/en/asset/auto-atlas.md new file mode 100644 index 0000000000..2c4a410f75 --- /dev/null +++ b/versions/4.0/en/asset/auto-atlas.md @@ -0,0 +1,56 @@ +# Auto Atlas + +**Auto Atlas** is the picture-combining method that comes as part of Cocos Creator. **Auto Atlas** packs a specified series of images into a __sprite sheet__. This capability is very similar to the function of __Texture Packer__. + +## Creating Auto Atlas Assets + +Create a new **auto-atlas.pac** resource in the **Assets** panel by clicking the **+** Create button in the upper left corner and then selecting **Auto Atlas**. + +![create auto atlas](auto-atlas/create-auto-atlas.png) + +**AutoAtlas** will pack all **SpriteFrame** assets in the same folder into a big **Sprite Atlas** asset during the build process. We might add other ways to choose assets for packing in the future. If the original **SpriteFrame** asset have been configured, then all configurations will be preserved. + +## Configuring Auto Atlas Assets + +After selecting an **Auto Atlas Resource** in the __Assets__ panel, the **Inspector** panel will display all of the configurable items for the **Auto Atlas Resource**. + +![auto atlas properties](auto-atlas/autoatlas-properties.png) + +| Property | Functional Description +| :-------------- | :----------- | +| **Max Width** | Single atlas maximum width | +| **Max Height** | Maximum Height of a single atlas | +| **Padding** | Spacing between shreds in the atlas | +| **Allow Rotation** | Whether rotate fragments | +| **Force Squared** | Whether to force the size of the atlas to be square | +| **Power of Two** | Whether to set the size of the atlas to a multiple of a square | +| **Algorithm** | Atlas packaging strategy, currently only one option `MaxRects` | +| **Padding Bleed** | Expand a pixel outer frame outside the border of the broken image, and copy the adjacent broken image pixels to the outer frame. This feature is also called **Extrude**. | +| **Filter Unused Resources** | If this option is checked, unreferenced assets will not be included when building. This option only takes effect after the build. | +| **Remove unused texture force in Bundle** | If this option is checked, the unused Texture2D resources in the Bundle will be excluded when building. +| **Remove unused image force in Bundle** | If this option is checked, the unused image resources in the Bundle will be excluded when building. +| **Remove unused sprite atlas in Bundle** | If this option is checked, the unused sprite atlas in the Bundle will be excluded when building. +| **UseCompressTexture** | Whether to use the compress texture, please refer to the [Texture Compression](compress-texture.md) for details. + +The rest of the properties are the same as Texture, please refer to the [Texture](./texture.md#sub-asset-texture2d-properties-panel) documentation for details. + +After the configuration is complete, click the **Preview** button to preview the packaged results. The related results generated according to the current automatic atlas configuration will be displayed in the area below the **Inspector** panel. + +> **Note**: after each configuration, re-click **Preview** to update the preview image. (Generating a preview is not required). + +The results are divided into: + +- __Packed Textures__: Display the packaged atlas pictures and picture-related information. If there are multiple pictures to be generated, they will be listed below in the **Inspector** panel. +- __Unpacked Textures__: Display the broken image assets that cannot be packed into the atlas. The cause may be that the size of these broken image assets is larger than the size of the atlas assets. At this time, the configuration or fragmentation of the following atlas may need to be adjusted. The size of the figure is increased. + +## Generating an Atlas + +When inside the editor or previewing the project, __Cocos Creator__ is directly using the split **SpriteFrame** assets, only after building, the **Atlas** asset will be generated and be used instead of all split assets. + +In general, after **Atlas** asset is generated, the **Texture2D** assets and **Image** assets related by the original split assets will be deleted in the package. The following two special cases will have special process: + +1. When **Atlas** asset is in a `Bundle` directory, the **Texture2D** and **Image** assets related by the original **SpriteFrame** assets in the **AutoAtlas** asset's directory will also be generated. If you have a specific scope of use for the atlas, please check the corresponding exclusion option in the **Inspector** panel to avoid oversizing the package. + +2. When any **Texture2D** asset depended by **SpriteFrame** assets in the **Atlas** asset's folder is directly used by another asset, the dependent **Texture2D** asset and **Image** asset will also be generated. + +The two situations above will increase the package size, please do not use an **Atlas** like this unless necessary, **Builder** will also produce a console warning to prompt you to consider your package size. diff --git a/versions/4.0/en/asset/auto-atlas/autoatlas-properties.png b/versions/4.0/en/asset/auto-atlas/autoatlas-properties.png new file mode 100644 index 0000000000..6767a7b192 Binary files /dev/null and b/versions/4.0/en/asset/auto-atlas/autoatlas-properties.png differ diff --git a/versions/4.0/en/asset/auto-atlas/create-auto-atlas.png b/versions/4.0/en/asset/auto-atlas/create-auto-atlas.png new file mode 100644 index 0000000000..1b46867f85 Binary files /dev/null and b/versions/4.0/en/asset/auto-atlas/create-auto-atlas.png differ diff --git a/versions/4.0/en/asset/bundle.md b/versions/4.0/en/asset/bundle.md new file mode 100644 index 0000000000..a3c97662fa --- /dev/null +++ b/versions/4.0/en/asset/bundle.md @@ -0,0 +1,448 @@ +# Asset Bundle Overview + +> Author: Santy-Wang, Xunyi + +Starting with v2.4, Creator officially supports **Asset Bundle**. The Asset Bundle is a modular resource tool that allows developers to divide the resources such as textures, scripts, scenes, etc. into different Asset Bundles according to the project requirements. Then, as the game runs, load different Asset Bundles as needed to minimize the number of resources to be loaded at startup. thus reducing the time required for the first download and loading of the game. + +The Asset Bundle can be placed in different places as needed, such as on a remote server, locally, or in a subpackage of a mini game platform. + +## The built-in Asset Bundle + +In addition to the custom Asset Bundle, there are three built-in Asset Bundles in the project, which like other custom Asset Bundles, can be configured for different platforms. + +![builtinBundles](bundle/builtin-bundles.png) + +| Built-in Asset Bundle | Function Explanation | Configuration | +| :--------------- | :--------- | :-------- | +| `main` | Store all scenes checked in the **Included Scenes** selection box of the **Build** panel and their dependent resources | By configuring the **Main Bundle Compression Type** and **Main Bundle Is Remote** options of the **Build** panel. | +| `resources` | Store all resources in the `resources` directory and their dependent resources | By configuring the `assets -> resources` folder in the **Assets** panel. | +| `start-scene` | If you check the **Start Scene Asset Bundle** option in the **Build** panel, the first scene will be built into the `start-scene` folder. | Cannot be configured. | +| `internal` | Some default resources built into the engine module | Cannot be configured. | + +After the build, the built-in Asset Bundle will be generated in different locations depending on the configuration, see the [Configure the Asset Bundle](bundle.md#configuration) documentation for the configuration methods and generation rules. + +The built-in Asset Bundle is loaded in `application.js`, and you can modify the code in `application.js` by using the custom build template feature, as shown below. + +```typescript +// ... + +function loadAssetBundle (hasResourcesBundle, hasStartSceneBundle) { + let promise = Promise.resolve(); + const mainBundleRoot = 'http://myserver.com/assets/main'; + const resourcesBundleRoot = 'http://myserver.com/assets/resources'; + const bundleRoot = hasResourcesBundle ? [resourcesBundleRoot, mainBundleRoot] : [mainBundleRoot]; + return bundleRoot.reduce((pre, name) => pre.then(() => loadBundle(name)), Promise.resolve()); +} + +function loadBundle (name) { + return new Promise((resolve, reject) => { + assetManager.loadBundle(name, (err, bundle) => { + if (err) { + return reject(err); + } + resolve(bundle); + }); + }); +} +``` + +## Configuration + +The custom Asset Bundle is configured in **folders**. When we select a folder in the **Assets** panel, the **Properties** panel will show a **Is Bundle** option, if set, the folder-related configuration options will appear: + +![bundle](./subpackage/inspector.png) + +| Configuration Options | Function Explanation | +| :--- | :---- | +| **Bundle Name** | The name of the Asset Bundle after it is built, which will use the name of this folder by default, can be modified as needed. | +| **Bundle Priority** | Creator opens up 20 configurable priorities, and the Asset Bundle will be built in descending order of priority. For more detail, see [Asset Bundle -- Priority](bundle.md#priority) documentation. | +| **Target Platform** | An Asset Bundle can have different settings on different platforms and the editor will choose the corresponding setting at build time. | +| **Compression Type** | Determines the final output form of the Asset Bundle, including the five compression types **Merge Depend**, **None**, **Merge All JSON**, **Mini Game Subpackage** and **Zip**. For more detail, see [Asset Bundle -- Compression Type](bundle.md#compression-type) documentation. | +| **Is Remote Bundle** | Whether to configure the Asset Bundle as a remote package.
If checked, the Asset Bundle will be placed in the **remote** folder after the build, and you will need to place the entire **remote** folder on the remote server.
When building mini game platforms such as OPPO, vivo, Huawei, etc., the Asset Bundle will not be packaged into rpk if this option is checked. | +| **Bundle Filter Config** | Use **Bundle Filter Config** to filter some assets in a bundle, and to preview the assets in the bundle by clicking on the **Preview** button. For more, see [Bundle Filter Config](./bundle.md#Bundle%20Filter%20Config) below. | +| **Build Bundle** | Click to build the selected or specified bundle. For [more](./bundle#Build%20Bundle) | + +After the configuration, click the **green tick button** at the top right of the panel, and the folder will be configured as an Asset Bundle, then select the corresponding platform in the **Build** panel to build. + +> **Notes**: +> +> 1. There are 4 [built-in Asset Bundles](bundle.md#the-built-in-asset-bundle) in Creator, including **internal**, **resources**, **main** and **start-scene**. When setting the **Bundle Name**, **do not** use these names. +> 2. The [mini game subpackage](../editor/publish/subpackage.md) can only be placed locally and cannot be configured as remote packages. So the **Is Remote Bundle** option cannot be checked when the compression type is set to **Mini Game Subpackage**. +> 3. The Zip compression type is primarily used to reduce the number of network requests and is used by default with the **Is Remote Bundle** option. Since the package doesn't need network requests if it's local, there's no need to use Zip compression. +> 4. The folder configuration set to Bundle is used as a collection of options for the Asset Bundle and we do not recommend that you place all assets in it very directly. Similar to the previous version of resources, the Bundle configuration folder should preferably hold entry assets such as Scene, Prefab, or assets that need to be dynamically loaded within the script, and the final build phase will export all referenced assets based on dependencies to eventually populate the entire Asset Bundle.In this way, unnecessary resource exports can be minimized. + +## Priority + +When the folder is set to Asset Bundle, the resources in the folder and the related dependent resources outside the folder are merged into the same Asset Bundle. It is possible to have a resource that is not in the Asset Bundle folder, but belongs to both Asset Bundles, because it is depended upon by both Asset Bundles. As shown in the figure: + +![shared](bundle/shared.png) + +Another possibility is that a resource is in one Asset Bundle folder, but is also depended upon by other Asset Bundles. As shown in the figure: + +![shared2](bundle/shared2.png) + +In both cases, `resource c` belongs to both `Asset Bundle A` and `Asset Bundle B`. So which Asset Bundle does `resource c` actually exist in? This needs to be specified by adjusting the priority of the Asset Bundle.
+Creator opens up 20 configurable priorities, and the editor will build the Asset Bundle in **descending order** of priority at build time. + +- When the same resource is referenced by multiple Asset Bundles with **different priorities**, the resource will be placed in the high-priority Asset Bundle, while the lower-priority Asset Bundle stores only one record message. In this case the lower-priority Asset Bundle relies on the higher-priority Asset Bundle.
+If you want to load this shared resource in a lower-priority Asset Bundle, you must load the higher-priority Asset Bundle before loading the lower-priority Asset Bundle. + +- When the same resource is referenced by multiple Asset Bundles of the **same priority**, the resource will be copied in each Asset Bundle, with no dependencies between the different Asset Bundles, and they can be loaded in any order. So try to make sure that the Asset Bundle that the shared resource (e.g.: `Texture`, `SpriteFrame`, `Audio`, etc.) is in has a higher priority, so that more lower-priority Asset Bundles can share resources, thus minimizing the package size. + +The four built-in Asset Bundle folders are prioritized as follows: + +| Asset Bundle | Priority | +| :--- | :--- | +| `main` | 7 | +| `resources` | 8 | +| `start-scene` | 20 | +| `internal` | 21 | + +When the four built-in Asset Bundles contain the same resources, the resources are stored in the higher priority Asset Bundle. It is recommended that other custom Asset Bundle priorities are not higher than the built-in Asset Bundle, so that the resources in the built-in Asset Bundle can be shared whenever possible. + +## Compression Type + +Creator currently provides **Merge Depend**, **None**, **Merge All Json**, **Mini Game Subpackage**, and **Zip** compression types for optimizing the Asset Bundle. All Asset Bundles use the **Merge Depend** compression type by default, and you can reset the compression type for all Asset Bundles including the built-in Asset Bundle. + +| Compression Type | Function Explanation | +| :------ | :------ | +| **Merge Depend** | When building the Asset Bundle, JSON files for interdependent resources are merged together to reduce the number of load requests at runtime. | +| **None** | When building the Asset Bundle, there is no compression operation. | +| **Merge All Json** | When building the Asset Bundle, the JSON files for all resources are merged into one, which minimizes the number of requests, but may increase the load time for a single resource. | +| **Mini Game Subpackage** | On a mini game platform that provides subpackaging, the Asset Bundle will be set as a subpackage of the mini game. | +| **Zip** | On some mini game platforms, the resource file will be compressed into a Zip file when building the Asset Bundle, reducing the number of load requests at runtime. | + +If you use different compression types for the Asset Bundle on different platforms, then the Asset Bundle will be built according to the settings of the corresponding platform. + +## Building of the Asset Bundle + +At build time, resources (including scenes, code, and other resources) in the folder configured as an Asset Bundle, as well as relevant dependent resources outside the folder, are merged into the same Asset Bundle folder. For example, if "scene A" is placed in "folder a", when "folder a" is configured as an Asset Bundle, "scene A" and its dependent resources will be merged into the "Asset Bundle a" folder. + +All the **code** and **resources** in the folder configured as the Asset Bundle are treated as follows. + +- **Code**: All the code in the folder is merged into an entry script file named `index.js` or `game.js`, depending on the release platform, and removed from the main package. +- **Resources**: All resources in the folder and the related dependent resources outside the folder are placed in the `import` or `native` directory. +- **Resource Configuration**: All resource configuration information including path, type, and version information is merged into a file named `config.json`. + +The structure of the Asset Bundle directory generated after build is shown below: + +![export](bundle/exported.png) + +After building, the Asset Bundle folder will be packaged into the **assets** folder in the release package directory of the corresponding platform. However, there are two special cases. + +- If the **Is Remote Bundle** option is checked when configuring the Asset Bundle, this Asset Bundle folder will be packaged into the **remote** folder in the release package directory of the corresponding platform. +- If the **Compression Type** is set to **Mini Game Subpackage** when configuring the Asset Bundle, this Asset Bundle folder will be packaged into the **subpackages** folder in the release package directory of the corresponding platform. + +Each folder contained within these three folders **assets**, **remote** and **subpackages** is an Asset Bundle. + +**For example**, if the **cases/01_graphics** folder in the [example-case](https://github.com/cocos/cocos-example-projects) is configured as an Asset Bundle on the Web Mobile platform, then after the project is built, a **01_graphics** folder is generated in the **assets** folder in the release package directory, and the **01_graphics** folder is an Asset Bundle. + +![asset-bundle](./subpackage/asset-bundle.png) + +### Scripts in the Asset Bundle + +The Asset Bundle supports script subpackaging. If your Asset Bundle includes the script files, then all the scripts will be merged into a single **js** file and removed from the main package. When loading the Asset Bundle, this **js** file will be attempted to be loaded. + +> **Notes**: +> 1. Some platforms do not allow the loading of remote script files, such as the WeChat Mini Game, and Creator will copy the code of the Asset Bundle to the `src/bundle-scripts` directory to ensure normal loading. +> 2. It is recommended that scripts in the different Asset Bundles do not reference each other, otherwise you may not find the corresponding script at runtime. If you need to reference certain classes or variables, you can share them by exposing them in your own global namespace. + +## Load the Asset Bundle + +The engine provides a unified API `assetManager.loadBundle` to load the Asset Bundle, which you need to pass in either the Asset Bundle's **URL** or the **Bundle Name** set in the Asset Bundle configuration panel, but if you want to reuse Asset Bundles from other projects, you can only do so through the **URL**. The usage is as follows: + +```typescript +assetManager.loadBundle('01_graphics', (err, bundle) => { + bundle.load('xxx'); +}); + +// Reuse Asset Bundles from other projects +assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => { + bundle.load('xxx'); +}); +``` + +`assetManager.loadBundle` also supports loading an Asset Bundle from user space with the path in user space. Use the download interface provided by the corresponding platform to pre-download the Asset Bundle into your user space and then use `loadBundle` to load it, so that you can manage the download and cache process of the Asset Bundle by yourself. + +```typescript +// Download an Asset Bundle in advance to the "pathToBundle" directory in your user space, and it's necessary to ensure +// that the structure and content of the Asset Bundle in your user space is identical to that of the original Asset Bundle. +// ... + +// Load with the path of the Asset Bundle in user space +// On native platform +assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => { + // ... +}); + +// On WeChat Mini Game +assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => { + // ... +}); +``` + +> **Note**: if you check the **Is Remote Bundle** option when configuring the Asset Bundle, then please fill in the **Resource Server Address** in the **Build** panel when building. + +When you load the Asset Bundle via the API, instead of loading all the resources in the Asset Bundle, the engine loads the Asset Bundle's **resource manifest** and **all the scripts** it contains.
+When the Asset Bundle is loaded, the engine triggers a callback and returns an error message and an instance of `AssetManager.Bundle` class, which is the main entrance of the Asset Bundle API that can be used to load the various resources in the Asset Bundle. + +### Versions of the Asset Bundle + +Asset Bundle continues the MD5 scheme of Creator for updates. When you need to update the Asset Bundle on the remote server, check the **MD5 Cache** option in the **Build** panel. Then the filename of the `config.json` in the built Asset Bundle will come with a Hash value. As shown in the following figure: + +![md5 cache](subpackage/bundle_md5.png) + +It is not necessary to provide an additional Hash value when loading the Asset Bundle, Creator will search for the corresponding Hash value in the `settings.json` and make adjustments automatically.
+However, if you want to store the relevant version configuration information on the server and dynamically fetch the version information at startup for hot updates, you can manually specify a version Hash value and pass in the `loadBundle`, and the Asset Bundle will be built based on the incoming Hash value: + +```typescript +assetManager.loadBundle('01_graphics', {version: 'fbc07'}, function (err, bundle) { + if (err) { + return console.error(err); + } + console.log('load bundle successfully.'); +}); +``` + +Then bypass the old version files in the cache and redownload the latest version of the Asset Bundle. + +## Load the resources in the Asset Bundle + +After the Asset Bundle is loaded, the engine returns an instance of `AssetManager.Bundle` class. Load the resources in the Asset Bundle by using `load` method of the instance, which has the same arguments as the `resources.load`, you just need to pass in the path of the resource relative to the Asset Bundle, and the end of the path **must not** contain the file extension. + +```typescript +// Load Prefab +bundle.load(`prefab`, Prefab, function (err, prefab) { + let newNode = instantiate(prefab); + director.getScene().addChild(newNode); +}); + +// Load Texture +bundle.load(`image/texture`, Texture2D, function (err, texture) { + console.log(texture) +}); +``` + +Like the `resources.load`, the `load` method also provides a type parameter, which is useful for loading resources with the same name or loading a SpriteFrame. + +```typescript +// Load SpriteFrame +bundle.load(`image/spriteFrame`, SpriteFrame, function (err, spriteFrame) { + console.log(spriteFrame); +}); +``` + +### Bulk load resources + +The Asset Bundle provides `loadDir` method to bulk load multiple resources in the same directory. The arguments of this method are similar to the `resources.loadDir`, you just need to pass in the path of the directory relative to the Asset Bundle. + +```typescript +// Load all resources in the "textures" directory +bundle.loadDir("textures", function (err, assets) { + // ... +}); + +// Load all Texture resources in the "textures" directory +bundle.loadDir("textures", Texture2D, function (err, assets) { + // ... +}); +``` + +### Load scenes + +The Asset Bundle provides `loadScene` methods for loading scenes from the specified bundle, you just need to pass in the **scene name**.
+The difference between `loadScene` and `director.loadScene` is that `loadScene` will only load the scene from the specified bundle and will not run the scene, you also need to use `director.runScene` to run the scene. + +```typescript +bundle.loadScene('test', function (err, scene) { + director.runScene(scene); +}); +``` + +## Get the Asset Bundle + +After the Asset Bundle has been loaded, it will be cached, and the name can be used to get the Asset Bundle. For example: + +```typescript +let bundle = assetManager.getBundle('01_graphics'); +``` + +## Preload resources + +In addition to scenes, other resources can also be preloaded. The loading arguments for preloading are the same as for normal loading, but because preloading only downloads the necessary resources, and does not perform the resources' deserialization and initialization, so it consumes less performance and is suitable for use during gameplay. + +The Asset Bundle provides `preload` and `preloadDir` interfaces to preload the resources in the Asset Bundle, which are used in the same way as the `assetManager`, see [Loading and Preloading](preload-load.md) documentation for details. + +## Release resources in the Asset Bundle + +After loading the resources, all the resources are temporarily cached in `assetManager` to avoid reloading. Of course, the resources in the cache also occupy memory, and some resources you can release in the following three ways if they are no longer needed: + +1. Use the regular `assetManager.releaseAsset` method for release. + + ```typescript + bundle.load(`image/spriteFrame`, SpriteFrame, function (err, spriteFrame) { + assetManager.releaseAsset(spriteFrame); + }); + ``` + +2. Use `release` method provided by the Asset Bundle, then pass in the path and type to release resources, but can only release the single resource in the Asset Bundle. The arguments can be the same as those used in the `load` method of the Asset Bundle. + + ```typescript + bundle.load(`image/spriteFrame`, SpriteFrame, function (err, spriteFrame) { + bundle.release(`image`, SpriteFrame); + }); + ``` + +3. Use `releaseAll` method provided by the Asset Bundle, which is similar to the `assetManager.releaseAll`, but the `releaseAll` will release all resources that belong to the Asset Bundle (including resources in the Asset Bundle and the related dependent resources outside the Asset Bundle), so please use caution. + + ```typescript + bundle.load(`image/spriteFrame`, SpriteFrame, function (err, spriteFrame) { + bundle.releaseAll(); + }); + ``` + +> **Note**: when releasing a resource, Creator will automatically handle the resource's dependent resources, and you don't need to manage their dependency resources. + +For more information about releasing resources, see [Release of Resources](release-manager.md) documentation. + +## Remove the Asset Bundle + +After the Asset Bundle is loaded, it will remain in the entire gameplay, unless you manually remove it from the game. When an unneeded Asset Bundle is manually removed, the cache for the bundle is also removed. If you need to use it again, you must reload it again. + +```typescript +let bundle = assetManager.getBundle('bundle1'); +assetManager.removeBundle(bundle); +``` + +> **Note**: when you destroy an Asset Bundle, the resources already loaded in the bundle are not released. If you need to release it, use the Asset Bundle's `release`/`releaseAll` method first. + +```typescript +let bundle = assetManager.getBundle('bundle1'); +// Releases a single resource in the Asset Bundle. +bundle.release(`image`, SpriteFrame); +assetManager.removeBundle(bundle); + +let bundle = assetManager.getBundle('bundle1'); +// Releases all resources belonging to the Asset Bundle. +bundle.releaseAll(); +assetManager.removeBundle(bundle); +``` + +## Bundle Filter Config + +The **Build Filter Config** allows you to config the selected bundle, and choose which asset should be included or excluded from the bundle. + +![filter.png](./bundle/filter.png) + +### Filter Type + +There are two types of filters, **Asset** and **URL**, the default value is **URL**. + +![filter-type](./bundle/filter-type.png) + +- **Asset**: Take an asset as a filter type. Drag an asset from the **Assets** panel or click the lock button ![lock.png](./bundle/lock.png) to select one. +- **ULR**: Filter all assets in the selected bundle according to one of the four rules including **Glob Expression**, **Begin With**, **End With**, and **Containing**. After the rule is set, type any string in the right input box to filter assets. + + ![filter-rule.png](./bundle/filter-rule.png) + +| Rule | Description | +| :---| :---| +| **Glob** | Glob Expression which can be referred by [npm](https://www.npmjs.com/package/glob).| +| **Begin With** | Filter all assets beginning with what is in the input-box | +| **End With** | Filter all assets end with what is in the input-box | +| **Containing** | Filter all assets containing what in the input box | + +To add a new filter rule, click the "+" button on the right. +Click the "-" button to delete the last added or selected rule. + +### Include + +A **Include** rule will include all the assets it configs. + +### Exclude + +A **Exclude** rule will exclude all the assets that meet the rule out of the bundle. + +### Preview + +Click the **Preview** button to preview all the assets in the bundle. + +## Build Bundle + +Since v3.8, Cocos Creator supports the Build Bundle feature which allows the users can build a specified bundle instead of building all the asset bundles. + +To open the **Build Bundle** panel, after selecting a bundle, click the **Build Bundle** button at the bottom of the **Inspector** panel. + +![bundle-build.png](./bundle/bundle-build.png) + +You need to have at least one build task to build a bundle. Click the **Open Build Panel** button to create a new build task. + +![build-task.png](./bundle/build-task.png) + +After a build task is created, now you can see the build bundle panel. + +![build-budle-withtask.png](./bundle/build-budle-withtask.png) + +Select a bundle from the drop-down list, and click the **Build** button at the bottom, and the Bundle will be built to the specified **Build Path**. + +![select-bundle.png](./bundle/select-bundle.png) + +There are two types of **Build Path**. + +![build-path.png](./bundle/build-path.png) + +- file: The absolute path of the output bundle. +- project: A related path based on the project. + +![select-open.png](./bundle/select-open.png) + +To select a different path, click the selected button ![select.png](./bundle/select.png), or after the build task is finished, use the open button ![open.png](./bundle/open.png) to locate the output directory.。 + +Select platforms to build on the **Publish Config** list. + +![task.png](./bundle/task.png) + +After clicking the **Build** button, it can be canceled by clicking on the **Cancel** button. + +![building.png](./bundle/building.png) + +This feature is designed for big projects or some time-consuming bundles. Developers can build these bundles separately to reduce the build time. + +### Bundle Build via Command Line + +Please refer to [Executing Independent Release Bundle in Command Line](./../editor/publish/publish-in-command-line.md) for details. + +## FAQ + +- **Q**: What is the difference between Asset Bundle and resource subpackage?
+ **A**: + 1. Resource subpackage is actually splitting out some textures, meshes into a separate packages, but the package is incomplete and illogical and cannot be reused.
+ while Asset Bundle is modularizing resources through logical division. The Asset Bundle includes resources, scripts, metadata and resource lists, so it is complete, logical and reusable, and we can load an entire scene or any other resources from Asset Bundle. By splitting the Asset Bundle, you can greatly reduce the number of `json` and the size of `settings.json` in the first package. + + 2. Resource subpackage is essentially a basic function controlled by the mini game platform. For example, the WeChat Mini Game supports subpackage, and then Creator made a layer of encapsulation on top of that to help the developers set up resource subpackage. If the WeChat Mini Game doesn't support subpackage anymore, neither does Creator.
+ While the Asset Bundle is designed and implemented entirely by Creator, it is a modular tool to help developers divide their resources, independent of the platform, and can theoretically be supported on all platforms. + + 3. Resource subpackage is related to the platform, meaning that it needs to be set up in the way required by the platform. For example, the subpackage of the WeChat Mini Game cannot be placed on the remote server, and can only be placed on Tencent's server.
+ While the Asset Bundle doesn't have these restrictions, the Asset Bundle can be placed locally, on a remote server, or even in a subpackage of the WeChat Mini Game. + +- **Q**: Does the Asset Bundle support the lobby plus sub games mode?
+ **A**: Absolutely, subgame scenes can be placed in the Asset Bundle and loaded when needed, and subgames can even be pre-built as an Asset Bundle in other projects and then loaded for use in the main project. + +- **Q**: Can the Asset Bundle reduce the size of `settings.json`?
+ **A**: Absolutely. In fact, as of v2.4, the packaged project is entirely based on the Asset Bundle, and the `settings.json` no longer stores any configuration information related to the resource, all configuration information are stored in the `config.json` of each Asset Bundle. Each `config.json` stores only the resource information in the respective Asset Bundle, which reduces the size of the first package. This can simply be understood as all the `config.json` combined equal to the previous `settings.json`. + +- **Q**: Does the Asset Bundle support cross project reuse?
+ **A**: The current version supports it, but we **do not recommend reusing**across projects. As the engine is updated and iterated, this can create all kinds of compatibility issues. cross-project reuse requires the following conditions to be met for now: + 1. The engine version is the same + 2. All scripts referenced in the Asset bundle are placed under the Asset bundle. + 3. The Asset Bundle has no other external dependency bundle, and if it does, it must be loaded. + 4. No reuse of scripts between Asset Bundles where possible

+ +- **Q**: Does the Asset Bundle support split first scene?
+ **A**: Currently only supported on mini game platforms. You can check the **Start Scene Asset Bundle** in the **Build** panel and the first scene will be put into the `start-scene` of the built-in Asset Bundle to separate the first scene. + +- **Q**: Does the Asset Bundle support nesting? For example, if there is a folder B in folder A, can both A and B be set as Asset Bundle?
+ **A**: Asset Bundle does not support nesting, please avoid using it as such. + +- **Q**: Why might placing an atlas inside an Asset Bundle cause the package to grow larger? + **A**:**When an atlas is placed in Bundle, by default the SpriteAtlas itself, the image of the atlas, the small image in the atlas folder, etc., may be loaded by the script, and all the resources contained in the Bundle will be packaged according to the established rules**. Therefore, it is not recommended to put the atlas directly in the Bundle, but to package it naturally into the final Asset Bundle by reference to the resources in the Bundle. There are currently some culling configurations open on the atlas resources, so if you really need to place them in the Bundle folder, you can do so as needed. diff --git a/versions/4.0/en/asset/bundle/build-budle-withtask.png b/versions/4.0/en/asset/bundle/build-budle-withtask.png new file mode 100644 index 0000000000..44a1b1cfa3 Binary files /dev/null and b/versions/4.0/en/asset/bundle/build-budle-withtask.png differ diff --git a/versions/4.0/en/asset/bundle/build-path.png b/versions/4.0/en/asset/bundle/build-path.png new file mode 100644 index 0000000000..53f4d12713 Binary files /dev/null and b/versions/4.0/en/asset/bundle/build-path.png differ diff --git a/versions/4.0/en/asset/bundle/build-task.png b/versions/4.0/en/asset/bundle/build-task.png new file mode 100644 index 0000000000..5fdce6b94d Binary files /dev/null and b/versions/4.0/en/asset/bundle/build-task.png differ diff --git a/versions/4.0/en/asset/bundle/building.png b/versions/4.0/en/asset/bundle/building.png new file mode 100644 index 0000000000..841d3c6093 Binary files /dev/null and b/versions/4.0/en/asset/bundle/building.png differ diff --git a/versions/4.0/en/asset/bundle/builtin-bundles.png b/versions/4.0/en/asset/bundle/builtin-bundles.png new file mode 100644 index 0000000000..cc1447e38c Binary files /dev/null and b/versions/4.0/en/asset/bundle/builtin-bundles.png differ diff --git a/versions/4.0/en/asset/bundle/bundle-build.png b/versions/4.0/en/asset/bundle/bundle-build.png new file mode 100644 index 0000000000..5f71ba2e43 Binary files /dev/null and b/versions/4.0/en/asset/bundle/bundle-build.png differ diff --git a/versions/4.0/en/asset/bundle/export-config.png b/versions/4.0/en/asset/bundle/export-config.png new file mode 100644 index 0000000000..ae97c802fa Binary files /dev/null and b/versions/4.0/en/asset/bundle/export-config.png differ diff --git a/versions/4.0/en/asset/bundle/exported.png b/versions/4.0/en/asset/bundle/exported.png new file mode 100644 index 0000000000..a0591ef16e Binary files /dev/null and b/versions/4.0/en/asset/bundle/exported.png differ diff --git a/versions/4.0/en/asset/bundle/filter-rule.png b/versions/4.0/en/asset/bundle/filter-rule.png new file mode 100644 index 0000000000..e70f232a6f Binary files /dev/null and b/versions/4.0/en/asset/bundle/filter-rule.png differ diff --git a/versions/4.0/en/asset/bundle/filter-type.png b/versions/4.0/en/asset/bundle/filter-type.png new file mode 100644 index 0000000000..7b6165f841 Binary files /dev/null and b/versions/4.0/en/asset/bundle/filter-type.png differ diff --git a/versions/4.0/en/asset/bundle/filter.png b/versions/4.0/en/asset/bundle/filter.png new file mode 100644 index 0000000000..345a7b4f44 Binary files /dev/null and b/versions/4.0/en/asset/bundle/filter.png differ diff --git a/versions/4.0/en/asset/bundle/lock.png b/versions/4.0/en/asset/bundle/lock.png new file mode 100644 index 0000000000..53b80d9934 Binary files /dev/null and b/versions/4.0/en/asset/bundle/lock.png differ diff --git a/versions/4.0/en/asset/bundle/open.png b/versions/4.0/en/asset/bundle/open.png new file mode 100644 index 0000000000..5d607f2318 Binary files /dev/null and b/versions/4.0/en/asset/bundle/open.png differ diff --git a/versions/4.0/en/asset/bundle/select-bundle.png b/versions/4.0/en/asset/bundle/select-bundle.png new file mode 100644 index 0000000000..68c16b4385 Binary files /dev/null and b/versions/4.0/en/asset/bundle/select-bundle.png differ diff --git a/versions/4.0/en/asset/bundle/select-open.png b/versions/4.0/en/asset/bundle/select-open.png new file mode 100644 index 0000000000..4f61abe38a Binary files /dev/null and b/versions/4.0/en/asset/bundle/select-open.png differ diff --git a/versions/4.0/en/asset/bundle/select.png b/versions/4.0/en/asset/bundle/select.png new file mode 100644 index 0000000000..f8932354f1 Binary files /dev/null and b/versions/4.0/en/asset/bundle/select.png differ diff --git a/versions/4.0/en/asset/bundle/shared.png b/versions/4.0/en/asset/bundle/shared.png new file mode 100644 index 0000000000..2c000537fd Binary files /dev/null and b/versions/4.0/en/asset/bundle/shared.png differ diff --git a/versions/4.0/en/asset/bundle/shared2.png b/versions/4.0/en/asset/bundle/shared2.png new file mode 100644 index 0000000000..353024dd05 Binary files /dev/null and b/versions/4.0/en/asset/bundle/shared2.png differ diff --git a/versions/4.0/en/asset/bundle/task.png b/versions/4.0/en/asset/bundle/task.png new file mode 100644 index 0000000000..5f9390ab21 Binary files /dev/null and b/versions/4.0/en/asset/bundle/task.png differ diff --git a/versions/4.0/en/asset/cache-manager.md b/versions/4.0/en/asset/cache-manager.md new file mode 100644 index 0000000000..44ae6b6e5f --- /dev/null +++ b/versions/4.0/en/asset/cache-manager.md @@ -0,0 +1,105 @@ +# Cache Manager + +> Author: Santy-Wang, Xunyi + +On the Web platforms, after resources are downloaded, the cache is managed by the browser, not the engine.
+On some non-Web platforms, such as WeChat Mini Game, such platforms have a file system that can be used to cache some remote resources but do not implement a caching mechanism for the resources. In this case, the engine needs to implement a set of caching mechanisms to manage the resources downloaded from the network, including caching resources, clearing cached resources, querying cached resources, and other features. + +Starting from v2.4, Creator provides a cache manager on all platforms with file systems to add, delete, and check caches, which can be accessed by developers through `assetManager.cacheManager`. + +## Resource downloading, caching and versioning + +The logic of downloading resources by the engine is as follows: + +1. Determine whether the resource is in the game package, and if so, use it directly. + +2. If not, query whether the resource is in the local cache, and if so, use it directly. + +3. If not, query if the resource is in the temporary directory, and if so, use it directly (the native platform does not have a temporary directory, skip this step). + +4. If not, download the resource from the remote server and use it directly after it is downloaded to the temporary directory (the native platform downloads the resource to the cache directory). + +5. The backend slowly saves the resources in the temporary directory to the local cache directory to be used when accessed again (the native platform skips this step) + +6. The resources will fail to be saved when the cache space is full, at which point the older resources will be deleted using the LRU algorithm (the native platform has no size limit on the cache space, so skip this step, and developers can manually invoke cleanup). + +For the mini-game platform, once the cache space is full, all resources that need to be downloaded cannot be saved, and only resources downloaded and saved in the temporary directory can be used. And when quitting the mini-game, all temporary directories will be cleaned up, and when running the game again, these resources will be downloaded again, and so on and so forth. + +> **Note**: the problem of file saving failure due to cache space exceeding the limit does not occur on WeChat Mini Game's **WeChat DevTools**, because WeChat DevTools does not limit the cache size, so testing the cache needs to be done in a real WeChat environment. + +When the engine's **md5Cache** feature is enabled, the URL of the file will change with the content of the file, so that when a new version of the game is released, the resources of the old version will be naturally invalidated in the cache and only new resources can be requested from the server, which also achieves the effect of versioning. + +### Uploading resources to a remote server + +When the package is too large (in size), it is necessary to upload resources to a remote server and configure the Asset Bundle where the resources are located as a remote package. Next, taking the WeChat Mini Game as an example, and look at the specific steps: + +1. Reasonably allocate resources, configure the resource folder that needs to be managed modularly as Asset Bundles, and check **Is Remote Bundle**. For additional details, please refer to the [Configure Asset Bundle](./bundle.md#configuration) documentation. + + ![bundle_is_remote](./cache-manager/remote-bundle.png) + +2. If the main bundle needs to be configured as a remote bundle, check **Main Bundle is Remote** in the **Build** panel. + +3. Then check **MD5 Cache** in the **Build** panel, set **Resource Server Address**, and click **Build**. + +4. After the build is complete, upload the `remote` folder in the release package directory to the server which is filled in the previous step. + +5. Delete the `remote` folder from the local release package directory. + +> **Note**: During the testing phase of WeChat Mini Games, developers may not be able to deploy the project to the official server, and need to test it on the local server. Please open **Tools -> Details -> Local Settings** page in the menu bar of WeChat DevTools, and check the **Does not verify valid domain names, web-view (business domain names), TLS versions and HTTPS certificates** option. +> +> ![details](./cache-manager/details.png) + +### Querying cache files + +The cache manager provides the `getCache` interface to query all cached resources, and developers can query the cache path by passing in the original path of the resource. + +```typescript +resources.load('images/background/texture', Texture2D, function (err, texture) { + const cachePath = assetManager.cacheManager.getCache(texture.nativeUrl); + console.log(cachePath); +}); +``` + +### Querying temporary files + +When a resource is downloaded locally, it may be stored as a temporary file in a temporary directory. The cache manager provides the `tempFiles` interface to query all resources downloaded into the temporary directory, and developers can do so by passing in the original path of the resource. + +```typescript +assetManager.loadRemote('http://example.com/background.jpg', function (err, texture) { + const tempPath = assetManager.cacheManager.getTemp(texture.nativeUrl); + console.log(tempPath); +}); +``` + +## Caching resources + +A number of parameters are provided in the cache manager to control the caching of resources: + +- `cacheManager.cacheDir` -- controls the directory where the cached resources are stored. +- `cacheManager.cacheInterval` -- controls the period of caching a single resource, default 500 ms once. +- `cacheManager.cacheEnabled` -- controls whether to cache the resource, default is cache. Alternatively, developers can override the global setting by specifying the optional parameter `cacheEnabled`. Example: + + ```typescript + assetManager.loadRemote('http://example.com/background.jpg', {cacheEnabled: true}, callback); + ``` + +### Clearing cache resources + +If the cache resource exceeds the limit and the developer needs to clear the resource manually, use `removeCache`, `clearCache`, `clearLRU` provided by the cache manager `assetManager.cacheManager` to clear the cache resource. + +- `clearCache` -- clears all cache resources in the cache directory, please use with caution. +- `clearLRU` -- clears the older resources in the cache directory. The mini-game platform will automatically call `clearLRU` when the cache space is full. +- `removeCache` -- clears a single cache resource. To use it, provide the original path of the resource. Example: + + ```typescript + assetManager.loadRemote('http://example.com/background.jpg', function (err, texture) { + assetManager.cacheManager.removeCache(texture.nativeUrl); + }); + ``` + +When the developer upgrades the engine version, the cached resources left locally are still the resources corresponding to the previous old version of the engine and are not automatically cleared. This may lead to problems such as errors in loading or rendering of resources. There are two solutions: + +1. Check the **MD5 Cache** option in the **Build** panel at build time, which will ensure that the latest version of resources are used. +2. Manually clear the previously cached resources. + - Clear the cache with `assetManager.cacheManager.clearCache()` on the **physical device**. + - For WeChat Mini Game, clear the cache by clicking **Tools -> Clear Cache -> Clear All** in the menu bar in **WeChat DevTools**. diff --git a/versions/4.0/en/asset/cache-manager/details.png b/versions/4.0/en/asset/cache-manager/details.png new file mode 100644 index 0000000000..63d84ed764 Binary files /dev/null and b/versions/4.0/en/asset/cache-manager/details.png differ diff --git a/versions/4.0/en/asset/cache-manager/remote-bundle.png b/versions/4.0/en/asset/cache-manager/remote-bundle.png new file mode 100644 index 0000000000..cecc1ef8e0 Binary files /dev/null and b/versions/4.0/en/asset/cache-manager/remote-bundle.png differ diff --git a/versions/4.0/en/asset/compress-texture.md b/versions/4.0/en/asset/compress-texture.md new file mode 100644 index 0000000000..7c1cdac378 --- /dev/null +++ b/versions/4.0/en/asset/compress-texture.md @@ -0,0 +1,105 @@ +# Compressed Textures + +Cocos Creator allows you to directly set the compression method for textures in the editor, and then automatically compresses textures during project publishing. It supports exporting multiple image formats for the same platform simultaneously, and the engine will load the appropriate compressed texture based on the device's support for compressed texture formats. + +## Advantages of Compressed Textures + +* For compressed textures like PNG, JPG, WEBP, etc. + - Configuring compressed textures allows compressing texture pixel data during project build to reduce resource size and improve game resource download speed. +* For GPU compressed textures like ASTC, ETC1, ETC2, PVRTC, etc. + - Configuring compressed textures allows converting texture pixel data into GPU-specific compressed formats during project build. These formats can be used directly in GPU memory without runtime decompression, significantly reducing memory usage, lowering bandwidth requirements, and improving game rendering performance and loading speed. + +> **Note**: +> * For non-GPU compressed texture formats like PNG, JPG, WEBP, compressing image quality does not reduce the time to decode image resources or reduce game memory. +> * Compressing PNG, JPG, WEBP and other format images uses the [sharp](https://github.com/lovell/sharp) open source library. Its compression ratio is slightly lower than [tinypng](https://tinypng.com/) and may result in larger images after compression. If you need to optimize this issue, it is recommended to use [Custom Texture Compression](../editor/publish/custom-build-plugin.md#custom-texture-compression-processing) to solve it yourself. + +## Compressed Texture Support + +Cocos Creator supports loading images in various formats (see table below for details). + +| Image Format | Android | iOS | Mini Games | Web | Windows | Mac | +| :----------- | :------ | :-- | :--------- | :-- | :------ | :-- | +| PNG | Supported | Supported | Supported | Supported | Supported | Supported | +| JPG | Supported | Supported | Supported | Supported | Supported | Supported | +| WEBP | Natively supported on Android 4.0+, other versions can use [parsing library](https://github.com/alexey-pelykh/webp-android-backport) | Can use [parsing library](https://github.com/carsonmcdonald/WebP-iOS-example) | Supported | [Partially supported](https://caniuse.com/#feat=webp) | Not supported | Not supported | +| PVR | Not supported | Supported | Supported on iOS devices | Supported on iOS devices | Not supported | Not supported | +| ETC1 | Supported | Not supported | Supported on Android devices | Supported on Android devices | Not supported | Not supported | +| ETC2 | Partially supported, depends on mobile hardware | Not supported | Not supported | Supported on some Android devices | Not supported | Not supported | +| ASTC | Supported (Android 5.0+) | Supported (iOS 9.0+/iPhone6+) | Supported on platforms like WeChat, Douyin, Alipay, Taobao, etc. For details, see [Compressed Texture Support Details for Each Platform](#Compressed-Texture-Support-Details-for-Each-Mobile-Platform). Note: Developer tools do not support, requires real device debugging | Partially supported | Not supported | Not supported | + +### Compressed Texture Support Details for Each Mobile Platform + +In addition to `JPG` and `PNG` which are supported across all platforms, the support for other texture compression formats is as follows: + +| Platform Name | Supported Compression Formats | +| :------------ | :---------------------------- | +| Web Mobile | ASTC / ETC1 / ETC2 / PVR / WEBP | +| WeChat Mini Game | ASTC / ETC1 / ETC2 / PVR | +| ByteDance Mini Game | ASTC / ETC1 / ETC2 / PVR | +| Alipay Mini Game | ASTC / ETC1 / PVR | +| Taobao Mini Game | ASTC / ETC1 / PVR | +| OPPO Mini Game | ETC1 | +| vivo Mini Game | ETC1 / ASTC | +| Huawei Quick Game | ETC1 | +| iOS | ASTC / ETC1 / ETC2 / PVR / WEBP | +| Android / Huawei AGC | ASTC / ETC1 / ETC2 / WEBP | + +**Note**: Currently, adding ASTC compressed textures in the general configuration for mini game platforms temporarily does not support the vivo mini game platform, but you can use [Platform Override Configuration](#platform-override-configuration) to enable ASTC compressed textures for the vivo mini game platform as well. + +## About ASTC + +For projects that require GPU compressed texture formats, it is recommended to use the ASTC compression format because it has high compression ratio and better quality, while also supporting HDR textures. ASTC was released in 2012, 12 years ago, and has nearly 100% support rate on mobile devices. + +For games with higher quality requirements, you can choose ASTC 5x5 for 2D elements and UI, and ASTC 6x6 for model textures. + +For games with lower quality requirements, you can choose 6x6 for 2D elements and UI, and 8x8 for model textures. + +## Compressed Texture Configuration + +When no compressed texture configuration is set, the project build outputs the original images. If you need to compress image resources in the project or large images generated by auto-atlas, you can select the image or auto-atlas in the **Assets Manager**, then check `useCompressTexture` in the **Inspector**, and select the texture compression preset for the image in `presetId`. After setting, click the green checkmark button in the upper right corner, and then the image resource will become a compressed texture resource after project build. + +![compress-texture](compress-texture/compress-texture.png) + +When configuring compressed texture presets, platform general configuration is used by default. This configuration will stop generating compressed textures of this format based on whether the build platform supports the configured compressed texture format. + +`presetId` provides the editor's default compressed texture presets, which cannot be edited. If you need to customize presets, click the **Edit Preset** button next to `presetId` to go to the [Project Settings -> Compressed Textures](../editor/project/index.md#compressed-textures) panel for configuration. + +The compressed texture options on image resources will be stored in the resource meta file, where `presetId` is the ID of the selected compressed texture preset. Projects with many images can manage the `presetId` used for image compression in batches through plugins or scripts. + +![meta](compress-texture/meta.png) + +**Note**: +After configuring compressed textures, only images of the corresponding format will be generated during build. If some image formats are not supported on certain devices, it may cause display abnormalities. To avoid this problem, when adding texture compression presets, please additionally select some general image formats (such as PNG, JPG) as default images. + +## Platform Override Configuration +When configuring compressed texture presets for mini game platforms, if developers need to ignore internal rules to ensure that configured compressed texture resources are generated during build, they need to add platform override configuration. + +![Platform Override Configuration](compress-texture/compress-3.jpg) + +## Compressed Texture Details + +Cocos Creator 3.0, when building images, will check whether the current image has compressed texture configuration. If not, it will output the original image. + +If compressed texture configuration is found, it will perform texture compression on the image according to the found configuration. The compressed texture configuration in project settings is divided by platform categories, and there may be some differences in the actual support level for specific platforms. The build will perform certain filtering and priority selection on the configured texture formats based on the **actual build platform** and the current **transparency channel situation of the image texture**. You can refer to the examples below to understand these rules. + +Not all generated images will be loaded into the engine. The engine will select and load images of appropriate formats based on the configuration in [macro.SUPPORT_TEXTURE_FORMATS](%__APIDOC__%/en/interface/Macro?id=SUPPORT_TEXTURE_FORMATS). `macro.SUPPORT_TEXTURE_FORMATS` lists all image formats supported by the current platform. When loading images, the engine will find the format that is **higher in priority** (i.e., ranked higher) in this list from the generated images to load. + +Developers can modify `macro.SUPPORT_TEXTURE_FORMATS` to customize the image resource support situation of the platform and the loading order priority. + +## Smart Filtering + +### Example 1 + +![1](compress-texture/compress-1.png) + +As shown in the figure above, for the compressed texture preset of the MiniGame platform, if **building for Huawei Quick Game or other platforms that only run on Android devices, PVR texture format will not be packaged during build**. For more platform filtering details, please refer to the [Compressed Texture Support Details for Each Platform](#Compressed-Texture-Support-Details-for-Each-Mobile-Platform) at the end of the document. + +### Example 2 + +![2](compress-texture/compress-2.png) + +In the example diagram above, both ETC1 and PVR types have **configured both RGB and RGBA texture formats simultaneously. In this case, the build will prioritize one of the formats based on whether the current image has a transparency channel**. The image in the example diagram has a transparency channel, so the build will only package compressed texture formats with RGBA type. Of course, this filtering only occurs when both exist. If only RGB image format is configured in the configuration, even if the current image has a transparency channel, it will be packaged normally. + +## Custom Build Texture Compression Processing + +Texture compression currently takes effect after build, and the editor comes with a set of processing tools. If you need to customize compression tools, please refer to [Custom Texture Compression](../editor/publish/custom-build-plugin.md#custom-texture-compression-processing). diff --git a/versions/4.0/en/asset/compress-texture/compress-1.png b/versions/4.0/en/asset/compress-texture/compress-1.png new file mode 100644 index 0000000000..884bf938f6 Binary files /dev/null and b/versions/4.0/en/asset/compress-texture/compress-1.png differ diff --git a/versions/4.0/en/asset/compress-texture/compress-2.png b/versions/4.0/en/asset/compress-texture/compress-2.png new file mode 100644 index 0000000000..637aad67e6 Binary files /dev/null and b/versions/4.0/en/asset/compress-texture/compress-2.png differ diff --git a/versions/4.0/en/asset/compress-texture/compress-3.jpg b/versions/4.0/en/asset/compress-texture/compress-3.jpg new file mode 100644 index 0000000000..9c84cd6324 Binary files /dev/null and b/versions/4.0/en/asset/compress-texture/compress-3.jpg differ diff --git a/versions/4.0/en/asset/compress-texture/compress-texture.png b/versions/4.0/en/asset/compress-texture/compress-texture.png new file mode 100644 index 0000000000..7f6da63642 Binary files /dev/null and b/versions/4.0/en/asset/compress-texture/compress-texture.png differ diff --git a/versions/4.0/en/asset/compress-texture/meta.png b/versions/4.0/en/asset/compress-texture/meta.png new file mode 100644 index 0000000000..e53caa434e Binary files /dev/null and b/versions/4.0/en/asset/compress-texture/meta.png differ diff --git a/versions/4.0/en/asset/downloader-parser.md b/versions/4.0/en/asset/downloader-parser.md new file mode 100644 index 0000000000..5f61d5d993 --- /dev/null +++ b/versions/4.0/en/asset/downloader-parser.md @@ -0,0 +1,167 @@ +# Download and Parse + +> Author: Santy-Wang, Xunyi + +The underlying of the Asset Manager uses multiple loading pipelines to load and parse resources, each of which uses the `downloader` and `parser` modules, that is, the downloader and the parser. You can access them via `assetManager.downloader` and `assetManager.parser`. + +## Downloader + +The downloader is a global single instance with features such as download retry, download priority, and download concurrency limits. + +### Download retry + +If the downloader fails to download a resource, it will automatically retry the download, and you can set the parameters for the download retry via `maxRetryCount` and `retryInterval`. + +- `maxRetryCount` is used to set the maximum number of retry downloads, the default is 3. If you do not need to retry the download, set to 0 and an error will be returned immediately if the download fails. + + ```typescript + assetManager.downloader.maxRetryCount = 0; + ``` + +- `retryInterval` is used to set the interval of retry downloads, the default is 2000 ms. If it is set to 4000 ms, it will wait for 4000 ms before re-downloading if the download fails. + + ```typescript + assetManager.downloader.retryInterval = 4000; + ``` + +### Download priority + +Creator opens up four download priorities, and the downloader will download resources in **descending order** of priority. + +| Resource | Priority | Explanation | +| :-- | :---- | :--- | +| **Script or Asset Bundle** | 2 | Highest priority first | +| **Scene resource** | 1 | Include all resources in the scene, ensuring that the scene loads quickly | +| **Manually loaded resource** | 0 | | +| **Preload resource** | -1 | The lowest priority, because preloading is more like loading resources in advance, and time requirements are relatively lenient | + +You can also control the load order by passing a priority over the default setting with the optional parameter `priority`. For details, refer to the "Set by optional parameters" section below. + +### Set the number of concurrent downloads + +You can set limits such as the maximum number of concurrent downloads in the downloader via `maxConcurrency` and `maxRequestsPerFrame`. + +- `maxConcurrency` is used to set the maximum number of concurrent connections for the download, if the current number of connections exceeds the limit, a waiting queue will be entered. + + ```typescript + assetManager.downloader.maxConcurrency = 10; + ``` + +- `maxRequestsPerFrame` is used to set the maximum number of requests that can be initiated per frame, which spreads the CPU overhead of initiating requests, avoiding too much jams in a single frame. If the maximum number of connections initiated in this frame has been reached, the request will be delayed until the next frame. + + ```typescript + assetManager.downloader.maxRequestsPerFrame = 6; + ``` + +In addition, `downloader` uses an instance of the `jsb.Downloader` class to download resources from the server on **native platforms**. `jsb.Downloader` is similar to the Web's [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). Currently the `jsb.Downloader` class instances have a default download concurrency limit of **32** and a default timeout of **30s**, if you want to change the default values, you can do so in `main.js`. + +```typescript +// main.js +assetManager.init({ + bundleVers: settings.bundleVers, + remoteBundles: settings.remoteBundles, + server: settings.server, + jsbDownloaderMaxTasks: 32, // Max concurrency + jsbDownloaderTimeout: 60 // Timeout +}); +``` + +## Parser + +The parser is used to parse the files into the resources that can be recognized by the engine, and you can access them via `assetManager.parser`. + +## Set by optional parameters + +The settings in both the downloader and the parser are global, if you need to set up a resource individually, you can override the global settings by passing in the proprietary settings via the optional parameters. For example: + +```typescript +assetManager.loadAny({'path': 'test'}, {priority: 2, maxRetryCount: 1, maxConcurrency: 10}, callback); +``` + +For details, refer to the [Optional Parameters](options.md) documentation. + +## Preset + +Creator presets the download/parsing parameters for the six load cases: normal loading, preloading, scene loading, Asset Bundle loading, remote resource loading, and script loading, and preloading is more restrictive because of performance considerations, and the maximum number of concurrency is smaller. As shown below: + +```typescript +{ + 'default': { + priority: 0, + }, + + 'preload': { + maxConcurrency: 2, + maxRequestsPerFrame: 2, + priority: -1, + }, + + 'scene': { + maxConcurrency: 8, + maxRequestsPerFrame: 8, + priority: 1, + }, + + 'bundle': { + maxConcurrency: 8, + maxRequestsPerFrame: 8, + priority: 2, + }, + + 'remote': { + maxRetryCount: 4 + }, + + 'script': { + priority: 2 + } +} +``` + +In addition to the above, you can also set each preset via `assetManager.presets`. Note that the restrictions can be different for each preset, so `presets` is a table and you need to pass the name of the preset to access the corresponding parameter options, for example: + +```typescript +// Modify the preset priority of the preload to 1. +let preset = assetManager.presets.preload; +preset.priority = 1; +``` + +You can also add custom presets and pass them in with the optional parameter `preset`. + +```typescript +// Customize the preset and pass it in with the optional parameter preset. +assetManager.presets.mypreset = {maxConcurrency: 10, maxRequestsPerFrame: 6}; +assetManager.loadAny({'path': 'test'}, {preset: 'mypreset'}, callback); +``` + +> **Note**: parameters related to the download and parsing process (e.g.: number of download concurrent, number of download retries, etc.) can be set via optional parameters, presets, and the downloader/parser itself. When the same parameter is set in multiple ways, the engine selects to use it in the order of selectable **optional parameter > preset > downloader/parser**. That is, if the engine can't find the relevant settings in the optional parameter, it will look in the preset, and if it can't find them in the preset, it will look in the downloader/parser. + +## Custom handlers + +Both the downloader and the parser have a registration table. When you use `downloader` or `parser`, the downloader and parser will look for the corresponding download and parsing methods in the registry based on the incoming suffix name, and pass the parameters into the corresponding handler, in order to extend the engine by registering the custom handlers when you need to add a custom format to your project, or modify the handlers of the current format. Both the downloader and the parser provide `register` interfaces for registration handlers, which are used as follows: + +```typescript +assetManager.downloader.register('.myformat', function (url, options, callback) { + // Download the resource + ...... +}); + +assetManager.parser.register('.myformat', function (file, options, callback) { + // Parsing a downloaded file + ...... +}); +``` + +A custom handler needs to receive three arguments: + +- The first argument is the handler object, which is the URL in the downloader and the file in the parser. +- The second argument is optional, and optional parameters can be specified when you call the load interface. +- The third argument is to complete the callback, which needs to be called when you register the handler and pass in an error message or result. + +After registered the handler, the corresponding handler will be used if the downloader/parser encounters a request with the same extension, and these custom handlers will be available to all loading pipelines globally. For example: + +```typescript +assetManager.loadAny({'url': 'http://example.com/myAsset.myformat'}, callback); +``` + +> **Note**: the handler can receive incoming optional parameters, which can be used to implement custom extensions, see the [Optional Parameters](options.md#expand-engine) documentation for details. diff --git a/versions/4.0/en/asset/dragonbones.md b/versions/4.0/en/asset/dragonbones.md new file mode 100644 index 0000000000..b8cbd30f0e --- /dev/null +++ b/versions/4.0/en/asset/dragonbones.md @@ -0,0 +1,33 @@ +# Skeletal Animation Assets (DragonBones) + +DragonBones skeletal animation assets are data formats exported by [DragonBones](http://dragonbones.com/) (supports DragonBones v5.6.3 and below). + +## Import DragonBones Skeleton Animation Assets + +DragonBones skeletal animation assets include + +- `.json`/`.dbbin` skeleton data +- `.json` atlas data +- `.png` atlas textures + + ![DragonBones](dragonbones/import.png) + +## Create Skeletal Animation Assets + +Using DragonBones skeletal animation assets in a scene requires two steps: + +1. Create nodes and add DragonBones components + + Drag the skeletal animation asset from the **Assets** panel to the Dragon Asset property of the created DragonBones component: + + ![DragonBones](dragonbones/set_asset.png) + +2. Set the atlas data for the DragonBones component + + Drag the atlas data from the **Assets** panel to the Dragon Atlas Asset property of the DragonBones component: + + ![DragonBones](dragonbones/set_atlas.png) + +## Storage in the Project + +For efficient asset management, it is recommended to store the imported asset files in a separate directory so that they won't mix with other assets. diff --git a/versions/4.0/en/asset/dragonbones/import.png b/versions/4.0/en/asset/dragonbones/import.png new file mode 100644 index 0000000000..2bc6358e3a Binary files /dev/null and b/versions/4.0/en/asset/dragonbones/import.png differ diff --git a/versions/4.0/en/asset/dragonbones/set_asset.png b/versions/4.0/en/asset/dragonbones/set_asset.png new file mode 100644 index 0000000000..5415425b43 Binary files /dev/null and b/versions/4.0/en/asset/dragonbones/set_asset.png differ diff --git a/versions/4.0/en/asset/dragonbones/set_atlas.png b/versions/4.0/en/asset/dragonbones/set_atlas.png new file mode 100644 index 0000000000..404a1197ff Binary files /dev/null and b/versions/4.0/en/asset/dragonbones/set_atlas.png differ diff --git a/versions/4.0/en/asset/dynamic-load-resources.md b/versions/4.0/en/asset/dynamic-load-resources.md new file mode 100644 index 0000000000..5cb3e2a935 --- /dev/null +++ b/versions/4.0/en/asset/dynamic-load-resources.md @@ -0,0 +1,179 @@ +# Asset Loading + +## Dynamic loading of resources + +Usually we will place the resources that need to be dynamically loaded in the project in the `resources` directory, along with interfaces such as `resources.load` to load them dynamically. You just need to pass in the path relative to `resources` directory, and the end of the path **must not** contain the file extension. + +```typescript +// load Prefab +resources.load("test_assets/prefab", Prefab, (err, prefab) => { + const newNode = instantiate(prefab); + director.getScene().addChild(newNode); +}); + +// load AnimationClip +resources.load("test_assets/anim", AnimationClip, (err, clip) => { + this.node.getComponent(Animation).addClip(clip, "anim"); +}); +``` + +- All resources that need to be dynamically loaded by **script** must be placed in the `resources` folder or one of its sub-folders. `resources` needs to be created manually in the `assets` folder and must be located in the `assets` root directory, like this: + + ![asset-in-properties-null](load-assets/resources-file-tree.png) + + > The assets in the `resources` folder can refer to other assets outside the folder, and can also be referenced by external scenes or assets. When the project is built, all assets in the `resources` folder, along with assets outside the `resources` folder they are associated with, will be exported, in addition to the scenes that have been checked in the **Build** panel. + > + > If an asset is only depended on by other assets in the `resources` and does not need to be called directly by `resources.load`, then please don't put it in the `resources` folder. Otherwise, the size of `config.json` will increase, and useless assets in the project will not be automatically culled during the build process. At the same time, in the build process, the automatic merge strategy of JSON will also be affected, unable to merge the fragmented JSON as much as possible. + +- The second to note is that compared to previous Cocos2d-JS, dynamic loading of resources in Creator is **asynchronous**, you need to get the loaded resources in the callback function. This is done because in addition to the resources associated with the scene, Creator has no additional resources preload list, and the dynamically loaded resources are really dynamically loaded. + + > **Note**: as of v2.4, the `loader` interface is deprecated, please use `assetManager` instead. You can refer to the [Asset Manager Upgrade Guide](asset-manager-upgrade-guide.md) documentation for details. + +### Loading a SpriteFrame or a Texture2D + +After the image is set to a spriteframe, texture or other image types, an asset of the corresponding type will be generated in the **Assets Panel**. But if `test_assets/image` is loaded directly, and the type will be `ImageAsset`. You must specify the full path of sub asset, then the generated SpriteFrame can be loaded. + +```typescript +// load a SpriteFrame, image is ImageAsset, spriteFrame is image/spriteFrame, texture is image/texture +resources.load("test_assets/image/spriteFrame", SpriteFrame, (err, spriteFrame) => { + this.node.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +```typescript +// load a texture +resources.load("test_assets/image/texture", Texture2D ,(err: any, texture: Texture2D) => { + const spriteFrame = new SpriteFrame(); + spriteFrame.texture = texture; + this.node.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +> **Note**: if a __type__ parameter is specified, an asset of the specified type will be found under the path. When you are in the same path includes multiple names simultaneously under a resource (for example, contains both `player.clip` and `player.psd`), should need to declare types. When you need to get a "sub-asset" (such as getting the sub-asset __SpriteFrame__ of __ImageAsset__), you need to specify the path of the sub-asset. + +### Load SpriteFrames from Atlas + +For an atlas imported from a third-party tool such as TexturePacker, if you want to load the SpriteFrame, you can only load the atlas first, and then get the SpriteFrame. This is a special case. + +```typescript +// load SpriteAtlas, and get one of them SpriteFrame +// Note Atlas resource file (plist) usually of the same name and a picture file (PNG) placed in a directory, +// So should need to in the second parameter specifies the resource type. +resources.load("test_assets/sheep", SpriteAtlas, (err, atlas) => { + const frame = atlas.getSpriteFrame('sheep_down_0'); + this.node.getComponent(Sprite).spriteFrame = frame; +}); +``` + +### Load resources in the FBX or glTF model + +After importing the FBX model or glTF model into the editor, it will parse out the related resources which includes meshes, materials, skeletons, animations, etc. contained in the model, as shown in the following figure. + +![model](./load-assets/model.png) + +It is possible to dynamically load a single resource in the model at runtime by simply specifying the path to a specific sub-resource, as follows. + +```typescript +// Load the mesh in the model +resources.load("Monster/monster", Mesh, (err, mesh) => { + this.node.getComponent(MeshRenderer).mesh = mesh; +}); + +// Load the material in the model +resources.load("Monster/monster-effect", Material, (err, material) => { + this.node.getComponent(MeshRenderer).material = material; +}); + +// Load the skeleton in the model +resources.load("Monster/Armature", Skeleton, (err, skeleton) => { + this.node.getComponent(SkinnedMeshRenderer).skeleton = skeleton; +}); +``` + +### Resource bulk loading + +`resources.loadDir` can load multiple resources under the same path: + +```ts +// loading all resource in the test_assets directory +resources.loadDir("test_assets", function (err, assets) { + // ... +}); + +// Load all SpriteFrames in the `test_assets` directory and get their urls +resources.loadDir("test_assets", SpriteFrame, function (err, assets) { + // ... +}); +``` + +## Preload resources + +Starting with v2.4, in addition to scenes that can be preloaded, other resources can also be preloaded. Preloading has the same loading parameters as normal loading, but it will only download the necessary resources, and will not perform deserialization or initialization. Therefore, it consumes less performance and is suitable for use during the game. + +`resources` provides `preload` and `preloadDir` for preloading resources. + +```typescript +resources.preload('test_assets/image/spriteFrame', SpriteFrame); + // wait for while +resources.load('test_assets/image/spriteFrame', SpriteFrame, (err, spriteFrame) => { + this.node.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +Use the preload related interface to load resources in advance, without waiting for the preload to finish. Then use the normal load interface to load, the normal load interface will directly reuse the content that has been downloaded during the preload process to shorten the load time. + +For more information on preloading, please refer to the [Preloading and Loading](preload-load.md) documentation. + +## How to load remote assets or files in device + +Currently in Cocos Creator, we support loading the remote image files, which is very useful to load user picture from social network websites. To load files from such urls, you should call `assetManager.loadRemote`. At the same time the same API can be used to load resources on the local device storage. The `resources.load` APIs mentioned above only apply to the application package resources and hot update resources. Here is how to load remote assets and local +device files: + +```typescript +// Remote texture url with file extensions +let remoteUrl = "http://unknown.org/someres.png"; +assetManager.loadRemote(remoteUrl, function (err, imageAsset) { + const spriteFrame = new SpriteFrame(); + const texture = new Texture2D(); + texture.image = imageAsset; + spriteFrame.texture = texture; + // ... +}); + +// Remote texture url without file extensions, then you need to define the file type explicitly +remoteUrl = "http://unknown.org/emoji?id=124982374"; +assetManager.loadRemote(remoteUrl, {ext: '.png'}, function (err, imageAsset) { + const spriteFrame = new SpriteFrame(); + const texture = new Texture2D(); + texture.image = imageAsset; + spriteFrame.texture = texture; + // ... +}); + +// Use absolute path to load files on device storage +let absolutePath = "/dara/data/some/path/to/image.png" +assetManager.loadRemote(absolutePath, function (err, imageAsset) { + const spriteFrame = new SpriteFrame(); + const texture = new Texture2D(); + texture.image = imageAsset; + spriteFrame.texture = texture; + // ... +}); + +// Remote Audio +remoteUrl = "http://unknown.org/sound.mp3"; +assetManager.loadRemote(remoteUrl, function (err, audioClip) { + // play audio clip +}); + +// remote Text +remoteUrl = "http://unknown.org/skill.txt"; +assetManager.loadRemote(remoteUrl, function (err, textAsset) { + // use string to do something +}); +``` + +There still remains some restrictions currently, the most important are: + +1. This loading method supports only native resource types such as textures, audios, text, etc., and does not support direct loading and analysis of resources such as SpriteFrame, SpriteAtlas, TiledMap. (If you want to load all resources remotely, use the [Asset Bundle](bundle.md)) +2. Remote loading ability on Web is limited by the browser's [CORS cross-domain policy restriction](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). If the server forbid cross-domain access, loading request will fail, and due to WebGL security policy restrictions, even if the server allows CORS http request, textures loaded can not be rendered. diff --git a/versions/4.0/en/asset/font.md b/versions/4.0/en/asset/font.md new file mode 100644 index 0000000000..280088ea2c --- /dev/null +++ b/versions/4.0/en/asset/font.md @@ -0,0 +1,64 @@ +# Fonts + +There are three types of font assets available to games made with __Cocos Creator__: __system fonts__, __dynamic fonts__, and __bitmap fonts__. + +The __system font__ renders text by calling the __system font__ that comes with the game running platform, and does not require the user to add any related assets to the project. To use __system fonts__, use the __Use System Font__ property in the [Label](../ui-system/components/editor/label.md) documentation. + +## Importing font assets + +### Dynamic fonts + +__Cocos Creator__ currently supports dynamic fonts in __True Type__ format. Simply drag a font file with an extension of `.ttf` into the __Assets__ panel, and you can import the font asset. + +### Bitmap fonts + +The bitmap font is composed of a font file in `.fnt` format and a `.png` image. The `.fnt` file provides an index of each character thumbnail. Fonts in this format can be generated by specialized software, please refer to: + +- [Glyph Designer](https://71squared.com/glyphdesigner) +- [Hiero](https://github.com/libgdx/libgdx/wiki/Hiero) +- [BMFont (Windows)](http://www.angelcode.com/products/bmfont/) +- [swnob-bmf](https://github.com/SilenceLeo/snowb-bmf) + +When importing bitmap fonts, be sure to drag both the `.fnt` file and the `.png` file into the __Assets__ panel at the same time. + +> __Note__: after importing the bitmap font, you need to change the type of the `.png` file to __sprite-frame__, otherwise the bitmap font will not work properly. + +The imported fonts are displayed in the __Assets__ panel, as follows: + +![imported font asset](font/imported.png) + +> __Note__: in order to improve the efficiency of asset management, it is recommended that the imported `.fnt` and `.png` files be stored in separate directories and not mixed with other assets. + +## Using font assets + +The font asset needs to be rendered through the `Label` component. Here is how to create a `Node` with a `Label` component in the scene. + +### Creating a Label (Font) Node Using the Menu + +Click on the __Create Node__ button in the upper left corner of the __Hierarchy__ panel and select `Create Render Node --> Label (Text)`, and a component with `Label` will be created in the `Scene` node. + +![from hierarchy](font/create_label.png) + +You can also complete the creation through `Node --> Create Render Node --> Label (Text)` of the main menu, the effect is the same as the above method. + +![from main menu](font/create_label_main_menu.png) + +### Associated Font Assets + +The font components created using the above method use the __system font__ as the associated asset, by default. If you want to use a __TTF__ or __bitmap fonts__ in the project, you can drag your font assets to the created `Label` component. + +![assign font file](font/assign_font_file.png) + +Currently, the font used in a scene will be immediately rendered using the font asset specified. You can also freely switch the `Font` property of the same `Label` component to use __TTF__ or __bitmap fonts__ according to the needs of the project. When switching font files, other properties of the `Label` component are not affected. + +If you want to restore the use of __system fonts__, you can click the property check box of __Use System Font__ to clear the font file specified in the __Font__ property. + +### Dragging and dropping to create a Label node + +Another quick way to create font nodes using specified assets is to directly __drag and drop__ font files, either __TTF__ or __bitmap fonts__, from the __Assets__ panel into the __Hierarchy__ panel. The only difference from the menu created above is that text nodes created using drag and drop will automatically use the dragged font asset to set the `Font` property of the __Label__ component. + + + + diff --git a/versions/4.0/en/asset/font/assign_font_file.png b/versions/4.0/en/asset/font/assign_font_file.png new file mode 100644 index 0000000000..afd049c6cd Binary files /dev/null and b/versions/4.0/en/asset/font/assign_font_file.png differ diff --git a/versions/4.0/en/asset/font/create_label.png b/versions/4.0/en/asset/font/create_label.png new file mode 100644 index 0000000000..9b6b4154a4 Binary files /dev/null and b/versions/4.0/en/asset/font/create_label.png differ diff --git a/versions/4.0/en/asset/font/create_label_main_menu.png b/versions/4.0/en/asset/font/create_label_main_menu.png new file mode 100644 index 0000000000..fdfad297c8 Binary files /dev/null and b/versions/4.0/en/asset/font/create_label_main_menu.png differ diff --git a/versions/4.0/en/asset/font/imported.png b/versions/4.0/en/asset/font/imported.png new file mode 100644 index 0000000000..42d072368e Binary files /dev/null and b/versions/4.0/en/asset/font/imported.png differ diff --git a/versions/4.0/en/asset/image.md b/versions/4.0/en/asset/image.md new file mode 100644 index 0000000000..5f7f73d24b --- /dev/null +++ b/versions/4.0/en/asset/image.md @@ -0,0 +1,42 @@ +# Images + +__Image__ assets are generally created using image processing software (such as __Photoshop__, __Paint__ on Windows, etc) and output into file formats that __Cocos Creator__ can use, currently including __JPG__、__PNG__、__BMP__、__TGA__、__HDR__、__WEBBP__、__PSD__、__PSD__、__TIFF__. + +## Importing image assets + +After importing images into __Cocos Creator__, they can be seen in __Assets__ panel. + +![imported](texture/imported.png) + +The properties are described as follows: + +| Property | Description | +| :--- | :--- | +| __useCompressTexture__ | Whether to use compressed texture.
For more information, please refer to [Texture Compression](compress-texture.md) | +| __Type__ | Used to set the type of image resources, including __raw__, __texture__(default), __normal map__, __sprite-frame__, __texture cube__. For more information, please refer to the following section: __Types of image assets__ | +| __Flip Vertical__ | Whether to flip the imported image vertically along the X axis | +| __Bake Offline Mimmaps__ | Whether to bake offline mipmaps | +| __Fix Alpha Transparency Artifacts__ | Fix alpha transparency artifacts
Fill transparent pixels with color of nearest solid pixel. These filled pixels would fix the dark halos at transparent borders of textures. Please turn on this option when you use the Alpha transparency channel in textures.
Only when __texture__, __raw__, __sprite-frame__ or __texture cube__ take effect | +| __Is RGBE__ | Whether it is RGBE compression format.
This option only works if the __Type__ property is [TextureCube](texture.md#TextureCube) | +| __Flip Green Channel__ | Flip Green Channel
The normal map used in Cocos Creator is OpenGL, if you are importing a DirectX based normal map, then check this option to fix the error if the depression occurs. | + +## Types of image assets + +On the right side of the __Inspector__ panel, you can choose different ways to use the image asset. There are currently 4 ways to use it for developers, as shown below: + +![type-change](texture/type-change.png) + +The details of each type of image asset are described in detail in the following sections: + +- The raw type is the original picture type. It has no effect and users do not need to use it. +- The texture type is the image asset type, which is also the default type for import. For details, see: [Texture](texture.md) +- normal map type is normal map type +- The sprite-frame type is a sprite frame asset, which is used for 2D/UI production. For details, see: [SpriteFrame](sprite-frame.md) +- The texture cube type is a cube map type, which is used on the panorama to make a sky box. For details, see: [Sky Box](../concepts/scene/skybox.md#Modifytheenvironmentmapoftheskybox) + +In the __Assets__ panel, a __triangle icon__ similar to a folder will be displayed on the left of the image. __Click__ to expand to see its __sub-assets__. After each image is imported, the editor will automatically create a __selected type__ asset of the same name. __Select__ the asset itself to __change the asset type__, __set the image flip__, and __set the quality__ of the image on each platform. For detailed descriptions of __sub-assets__, please refer to the [Sub-asset Properties Panel](texture.md#Sub-AssetTexture2D'sPropertyPanel) documentation. + + \ No newline at end of file diff --git a/versions/4.0/en/asset/index.md b/versions/4.0/en/asset/index.md new file mode 100644 index 0000000000..0037ec9b68 --- /dev/null +++ b/versions/4.0/en/asset/index.md @@ -0,0 +1,40 @@ +# About Assets + +This section will introduce the overall workflow of the assets in __Cocos Creator__ in detail, and explain the use of various types of assets and items that may require attention. + +## Assets Manager + +The **Assets** panel is an important tool for accessing and managing assets, developers are recommended to familiarize themselves with the use of the **Assets** panel for managing assets. Read the [Assets Manager](../editor/assets/index.md) documentation for a detailed introduction. + +## Assets Workflow + +The general Assets workflow including importing assets, synchronizing assets, locating assets, etc. can be found in the [Assets Workflow](asset-workflow.md) documentation. + +## Common Assets type workflow + +Next we will introduce the main **Asset** types in __Cocos Creator__: + +- [Scene Assets](scene.md) +- [Image Assets](image.md) + - [Texture Map Assets](texture.md) + - [Sprite Frame Assets](sprite-frame.md) + - [Cube Map Assets](../concepts/scene/skybox.md#cubemap) + - [Auto Crop of Image Assets](../ui-system/components/engine/trim.md) + - [Atlas Assets](atlas.md) + - [Render Texture](render-texture.md) +- [Prefabricated Assets](prefab.md) +- [Script Assets](script.md) +- [Font Assets](font.md) +- [Audio Assets](audio.md) +- [Material Assets](material.md) +- [Model Assets](./model/mesh.md) + - [Importing models exported from DCC tools](./model/dcc-export-mesh.md) + - [glTF](./model/glTF.md) +- [Animation Assets](../animation/animation-clip.md) +- [Spine Skeletal Animation Assets](spine.md) +- [DragonBones Skeletal Animation Assets](dragonbones.md) +- [TiledMap Assets](tiledmap.md) + +## Asset Manager + +For the section about asset management at runtime, please refer to the [Asset Manager](asset-manager.md) documentation. diff --git a/versions/4.0/en/asset/label-atlas.md b/versions/4.0/en/asset/label-atlas.md new file mode 100644 index 0000000000..c7c1315727 --- /dev/null +++ b/versions/4.0/en/asset/label-atlas.md @@ -0,0 +1,32 @@ +# LabelAtlas Asset + +**LabelAtlas** asset is a user-defined asset, it's used for configuring a **LabelAtlas**. + +## Create LabelAtlas Asset + +In the **Assets** panel right-click on a folder, and click the context menu item **Create -> Label Atlas**. It will create a **LabelAtlas.labelatlas** asset. + +![create label atlas](label-atlas/create-label-atlas.png) + +Before using the **LabelAtlas** asset, it needs some configuration. Configure a pre-drawn picture that contains the font style, as shown below: + +![raw_texture_file](label-atlas/raw_texture_file.png) + +## Configuration of LabelAtlas asset + +After selecting a **LabelAtlas** asset in the **Assets** panel, the **Inspector** panel will display all configurable properties for the **LabelAtlas** asset. + +| Property | Description +| :-------------- | :----------- +| **SpriteFrame** | Specify a pre-drawn picture that contains the font style you want +| **Item Width** | Specify the width of each character +| **Item Height** | Specify the height of each character +| **Start Char** | Specify the start char, even if the start char is a *space*, you also need insert a space. | + +When the configuration is complete, click the tick button at the top right of the **Inspector** panel to save the settings. + +![save label atlas](label-atlas/save-label-atlas.png) + +## Using LabelAtlas asset + +It's quite simple to use the **LabelAtlas** asset. Just setup a new Label component and drag the **LabelAtlas** asset to the **font** property of the Label component. Please refer to the [Label Component](../ui-system/components/editor/label.md) documentation for details. diff --git a/versions/4.0/en/asset/label-atlas/create-label-atlas.png b/versions/4.0/en/asset/label-atlas/create-label-atlas.png new file mode 100644 index 0000000000..288f02a86b Binary files /dev/null and b/versions/4.0/en/asset/label-atlas/create-label-atlas.png differ diff --git a/versions/4.0/en/asset/label-atlas/raw_texture_file.png b/versions/4.0/en/asset/label-atlas/raw_texture_file.png new file mode 100644 index 0000000000..ff58433135 Binary files /dev/null and b/versions/4.0/en/asset/label-atlas/raw_texture_file.png differ diff --git a/versions/4.0/en/asset/label-atlas/save-label-atlas.png b/versions/4.0/en/asset/label-atlas/save-label-atlas.png new file mode 100644 index 0000000000..2c3152a36c Binary files /dev/null and b/versions/4.0/en/asset/label-atlas/save-label-atlas.png differ diff --git a/versions/4.0/en/asset/load-assets/asset-dep.png b/versions/4.0/en/asset/load-assets/asset-dep.png new file mode 100644 index 0000000000..94dd9241b9 Binary files /dev/null and b/versions/4.0/en/asset/load-assets/asset-dep.png differ diff --git a/versions/4.0/en/asset/load-assets/asset-in-inspector-dnd.jpg b/versions/4.0/en/asset/load-assets/asset-in-inspector-dnd.jpg new file mode 100644 index 0000000000..d416b6fea8 Binary files /dev/null and b/versions/4.0/en/asset/load-assets/asset-in-inspector-dnd.jpg differ diff --git a/versions/4.0/en/asset/load-assets/asset-in-inspector-null.jpg b/versions/4.0/en/asset/load-assets/asset-in-inspector-null.jpg new file mode 100644 index 0000000000..150deff22f Binary files /dev/null and b/versions/4.0/en/asset/load-assets/asset-in-inspector-null.jpg differ diff --git a/versions/4.0/en/asset/load-assets/asset-in-inspector.jpg b/versions/4.0/en/asset/load-assets/asset-in-inspector.jpg new file mode 100644 index 0000000000..75f123700e Binary files /dev/null and b/versions/4.0/en/asset/load-assets/asset-in-inspector.jpg differ diff --git a/versions/4.0/en/asset/load-assets/model.png b/versions/4.0/en/asset/load-assets/model.png new file mode 100644 index 0000000000..6f6faf9ccc Binary files /dev/null and b/versions/4.0/en/asset/load-assets/model.png differ diff --git a/versions/4.0/en/asset/load-assets/resources-file-tree.jpg b/versions/4.0/en/asset/load-assets/resources-file-tree.jpg new file mode 100644 index 0000000000..5aa5d3a4b8 Binary files /dev/null and b/versions/4.0/en/asset/load-assets/resources-file-tree.jpg differ diff --git a/versions/4.0/en/asset/load-assets/resources-file-tree.png b/versions/4.0/en/asset/load-assets/resources-file-tree.png new file mode 100644 index 0000000000..d44fcfdfe0 Binary files /dev/null and b/versions/4.0/en/asset/load-assets/resources-file-tree.png differ diff --git a/versions/4.0/en/asset/material.md b/versions/4.0/en/asset/material.md new file mode 100644 index 0000000000..836bd82bd3 --- /dev/null +++ b/versions/4.0/en/asset/material.md @@ -0,0 +1,29 @@ +# Material assets + +## Material creation + +__Material__ is created as follows: + +![material-create](material/material-create.png) + +or + +![material-create-menu](material/material-create-menu.png) + +The __Material__ controls the final shading of each model. The __Material__ is composed of __Effects__, and the shading process of an __Effect__ is controlled by the __Material__. The __Material__ itself can also be regarded as a container of __Effect__ assets. The __Material__ can switch the __Effect__ assets to be used at will. The following figure is the __Effect__ asset of the __Material__ we have selected by default. + +![default-effect](material/default-effect.png) + +At the same time, we can also switch the __Effect__ of the current __Material__ by clicking the box to the right of the __Effect__ property. + +![effects](material/effects.png) + +## Effect Creation + +__Effects__ are created in a similar way to `Materials`. + +![effect-create](material/effect-create.png) + +The created __Effect__ is a __PBR Effect__ by default. + +![effect-show](material/effect-show.png) diff --git a/versions/4.0/en/asset/material/default-effect.png b/versions/4.0/en/asset/material/default-effect.png new file mode 100644 index 0000000000..70f82a30da Binary files /dev/null and b/versions/4.0/en/asset/material/default-effect.png differ diff --git a/versions/4.0/en/asset/material/effect-create.png b/versions/4.0/en/asset/material/effect-create.png new file mode 100644 index 0000000000..043a2cdcab Binary files /dev/null and b/versions/4.0/en/asset/material/effect-create.png differ diff --git a/versions/4.0/en/asset/material/effect-show.png b/versions/4.0/en/asset/material/effect-show.png new file mode 100644 index 0000000000..b1f44fe73f Binary files /dev/null and b/versions/4.0/en/asset/material/effect-show.png differ diff --git a/versions/4.0/en/asset/material/effects.png b/versions/4.0/en/asset/material/effects.png new file mode 100644 index 0000000000..5636540ab6 Binary files /dev/null and b/versions/4.0/en/asset/material/effects.png differ diff --git a/versions/4.0/en/asset/material/material-create-menu.png b/versions/4.0/en/asset/material/material-create-menu.png new file mode 100644 index 0000000000..03a151d45d Binary files /dev/null and b/versions/4.0/en/asset/material/material-create-menu.png differ diff --git a/versions/4.0/en/asset/material/material-create.png b/versions/4.0/en/asset/material/material-create.png new file mode 100644 index 0000000000..8556ce84ca Binary files /dev/null and b/versions/4.0/en/asset/material/material-create.png differ diff --git a/versions/4.0/en/asset/meta.md b/versions/4.0/en/asset/meta.md new file mode 100644 index 0000000000..9b32163a25 --- /dev/null +++ b/versions/4.0/en/asset/meta.md @@ -0,0 +1,141 @@ +# Resource Management Considerations --- meta files + +> **Note**: the full text of this article is reproduced from [WeChat Official Account: Quetta Planet [cn]](https://mp.weixin.qq.com/s/MykJaytb3t_oacude1cvIg), authorized by the author before reprinting
+> **Author**: Shawn Zhang + +Cocos Creator will generate a **meta** file with the same name for every file and directory in the **assets** directory. Understanding the role and mechanics of Cocos Creator's generation of **meta** files can help developers solve resource conflicts, file loss, and missing component properties that are often encountered with team development. What is the **meta** file used for? Let's take a look! + +![missingscript](meta/missingscript.png) + +## The Role of a Meta File + +Let's first look at what the **meta** file looks like in the scene: + +```json +{ + "ver": "1.0.0", //version + "uuid": "911560ae-98b2-4f4f-862f-36b7499f7ce3", //global unique id + "asyncLoadAssets": false, // asynchronous loading + "autoReleaseAssets": false, // automatically release resources + "subMetas": {} // child metadata +} +``` + +The **meta** file for the prefab is the same as the scene. Let's take a look at the **meta** file of the png image: + +```json +{ + "ver": "1.0.0", + "uuid": "19110ebf-4dda-4c90-99d7-34b2aef4d048", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "subMetas": { + "img_circular": { + "ver": "1.0.3", + "uuid": "a2d1f885-6c18-4f67-9ad6-97b35f1fcfcf", + "rawTextureUuid": "19110ebf-4dda-4c90-99d7-34b2aef4d048", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 100, + "height": 100, + "rawWidth": 100, + "rawHeight": 100, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} +``` + +The **meta** file for the png image has more information. In addition to the basic `ver` and `uuid`, it also records the width, height, offset, and borders of the image. There is a lot of information that is stored. **UUID** and it is particularly important. + +> UUID: Universally Unique Identifier + +**UUIDs** in Cocos Creator are used to manage the resources of the game. It assigns a unique id to each file. This means that in the Cocos Creator engine, identifying a file is not simply by `path + filename`, but by **UUID**. Therefore, you can delete and move files at will in **Asset Resource Management**. + +## When will a meta file be updated + +Cocos Creator generates **meta** files the following situations: + +**1. when opening the project** + + When you open a project, Cocos Creator, scans the **assets** directory first, and if it does not have a **meta** file, one will be generated. + +**2. when updating resources** + +Updating resources also triggers updates to the corresponding **meta** files: + + - In **Assets**, you can modify the file name of a resource, change the directory a resource belongs in, delete resources, and add new resources. You can also drag files directly into **Assets** from your desktop or the operating system's file manager. Please refer to [Assets](../getting-started/basics/editor-panels/assets.md). + + ![add](meta/add.png) + + - Files in the `assets` directory can be added, deleted, changed, on your local **file-system**. When the changes are complete, switch to editor interface of Creator, where you can see the process of **Assets** panel refresh. The **Assets** panel is refreshed with each change, as to always show the current state of the resources. + + ![refresh](meta/refresh.png) + +If a file's **meta** file does not exist, the above two conditions will trigger the engine to generate a new **meta** file automatically. + +## Meta File Error Cases And Solutions + +Let's analyze several possible cases that produce a **meta** file error. + +### UUID Conflicts + +**UUID** is globally unique. A conflict occurs if, by chance, two files have the same **UUID**. If this problem occurs, the Cocos Creator resource manager directory structure is incompletely loaded. As shown in the figure below, when this happens, the errors look scary: + +![conflict](meta/conflict.png) + +**UUID** conflicts can be viewed from the **Console**, and then opened from your local file-system or favorite code editor. Once opened you can search for the conflicting **UUID**: + +![search_uuid](meta/search_uuid.png) + +At this point, close the Cocos Creator editor then delete one of the meta files. Next, open the Cocos Creator editor. + +Even though this method can solve the problem, if the resource is referenced in the editor, a resource loss will occur. This means the resource needs to be re-edited or reconfigured again. It is best to restore this **meta** file with the version management tool. + +There are two reasons for this problem: + + - When moving files on the **file-system**, copying and pasting operations also result in the **meta** file also being copied. This causes two identical **meta** files to appear simultaneously in the project. + + - When you have multi-person collaboration, from the version management tool, when you update the resource, you may encounter that the **UUID** has been generated by someone else as well as by the file on your computer. This is a very rare occurence, but could still happen. + +In general, to reduce the occurrence of **UUID** conflicts, it is best to add and move files in the Cocos Creator resource management tool. + +### UUID Changes + +Another situation is when the **UUID** has changed, so that the resources corresponding to the old **UUID** cannot be found. In this case, the interface you have edited will have the resources, but the pictures may be lost as well as the components properties may also be lost. + +![lost](meta/lost.png) + +If you can't find the resources corresponding to the old **UUID**, you can see that Cocos Creator gives a very detailed warning message in the **console**. These details include the **scene file name**, **node path**, **component**, **UUID**, etc. The warning message allows you to quickly locate the error. + +How is this situation caused exactly? When someone adds a new resource to the project, they forget to switch to the editor interface to generate a **meta** file, and at the same time submit the new file to version management (without the **meta** file). Then, another person updates the resources he/she submitted and switches to the editor interface for editing. At this point, Cocos Creator will check that the new resource is generated without a **meta** file. The **meta** file is also generated when the first person switches to the editor, so that the two people have the same file on the computer, but the **UUID** in the generated **meta** file is different. + +In this case, those who submit or update resources later will certainly encounter conflicts. If they are unclear, they will forcibly resolve the conflicts, and the problems mentioned above will arise. The following sequence diagram depicts this erroneous workflow: + +![resources](meta/resources.png) + +A classmate forgets to submit the **meta** file to version control. Other classmates then edit the project resulting in everyone having the file on their computer, each with a different **UUID**. + +To solve this problem, note the following: + + - When submitting resources, pre-check for new files. If there are new documents, pay attention to whether **meta** files need to be submitted together. + + - When pulling files, pay attention to whether there are new files, and if there are **meta** files in pairs. If not, remind team-mates who submitted the files to submit the files and **meta** files together. + + - When submitting, if you find that there is only one new **meta** file, then the **meta** file must have been generated by yourself. You need to pay attention to whether the resource corresponding to the **meta** file (the file with the same name) has been used. If you haven't used it, please submit the **meta** file to the first submitter. Never submit this **meta** file. + +Note that the above points can basically eliminate the engineering error caused by the **meta** file **UUID** change. + +## Summary + +The **meta** file is an important tool for Cocos Creator to use for resource management. It is easy to generate resource errors when it is slightly inadvertent in a multi-person collaborative development. To solve this problem, you not only need to understand the mechanism that generates the **meta** file but also the cause of the conflict. Proper thought and control of resource submission helps too. diff --git a/versions/4.0/en/asset/meta/add.png b/versions/4.0/en/asset/meta/add.png new file mode 100644 index 0000000000..a5d0bc042e Binary files /dev/null and b/versions/4.0/en/asset/meta/add.png differ diff --git a/versions/4.0/en/asset/meta/conflict.png b/versions/4.0/en/asset/meta/conflict.png new file mode 100644 index 0000000000..ab79e55cbc Binary files /dev/null and b/versions/4.0/en/asset/meta/conflict.png differ diff --git a/versions/4.0/en/asset/meta/lost.png b/versions/4.0/en/asset/meta/lost.png new file mode 100644 index 0000000000..f2859768c7 Binary files /dev/null and b/versions/4.0/en/asset/meta/lost.png differ diff --git a/versions/4.0/en/asset/meta/missingscript.png b/versions/4.0/en/asset/meta/missingscript.png new file mode 100644 index 0000000000..e6a8c96a2c Binary files /dev/null and b/versions/4.0/en/asset/meta/missingscript.png differ diff --git a/versions/4.0/en/asset/meta/refresh.png b/versions/4.0/en/asset/meta/refresh.png new file mode 100644 index 0000000000..bfa510ff46 Binary files /dev/null and b/versions/4.0/en/asset/meta/refresh.png differ diff --git a/versions/4.0/en/asset/meta/resources.png b/versions/4.0/en/asset/meta/resources.png new file mode 100644 index 0000000000..ccedd691eb Binary files /dev/null and b/versions/4.0/en/asset/meta/resources.png differ diff --git a/versions/4.0/en/asset/meta/search_uuid.png b/versions/4.0/en/asset/meta/search_uuid.png new file mode 100644 index 0000000000..25ab061545 Binary files /dev/null and b/versions/4.0/en/asset/meta/search_uuid.png differ diff --git a/versions/4.0/en/asset/model/dcc-export-mesh.md b/versions/4.0/en/asset/model/dcc-export-mesh.md new file mode 100644 index 0000000000..36ce563f38 --- /dev/null +++ b/versions/4.0/en/asset/model/dcc-export-mesh.md @@ -0,0 +1,53 @@ +# Importing Models Exported from DCC Tools + +Currently, most __Digital Content Creation (DCC)__ tools ([__3DS Max__](max-export-fbx.md), [__Maya__](maya-export-fbx.md), __Blender__) can export models in __FBX__ and __glTF__ formats. These formats, exported by these tools, can be well received in __Cocos Creator 3.0__. + +## Exporting FBX + +Because the coordinate system of the DCC tool and the game engine's coordinate system are not necessarily the same, some transformations are required when exporting a model to get the desired result in the engine. For example, Blender's coordinate system is __X-axis right__, __Y-axis inward__, and __Z-axis upward__, while Cocos Creator 3.x's coordinate system is __X-axis right__, __Y-axis upward__, and __Z-axis outward__, so the rotation needs to be adjusted to make the axes consistent. + +The following uses Blender 2.8 as an example to introduce the model import process. First, create a model in Blender. + +![blender model](./mesh/blender_model.png) + +In [Blender's FBX Export Options](https://docs.blender.org/manual/en/2.80/addons/io_scene_fbx.html) documentation, choose __Up__ as __Y Up__ and __Forward__ as __-Z Forward__. + +![blender export](./mesh/blender_export_fbx_1.png) + +Imported into __Cocos Creator__, notice that the nodes are rotated by __-90__ on the __X-axis__ in order to combine the __axis__ with __Cocos Creator__. The axes are aligned. + +![blender export c3d](./mesh/blender_model_c3d.png) + +To use a different rotation value, Blender's FBX export plugin provides an experimental function, __Apply Transform__, which can directly transform the rotation data into the model's vertex data. + +![blender export bake](./mesh/blender_export_bake.png) + +Notice that the rotation data is gone in __Cocos Creator 3.x__. + +![blender export bake c3d](./mesh/blender_model_bake_c3d.png) + +## Exporting glTF + +Please read the following documents: + +- [glTF also uses a right-handed coordinate system](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#coordinate-system-and-units) +- [Options for exporting glTF in Blender](https://docs.blender.org/manual/en/2.80/addons/io_scene_gltf2.html). It is relatively simple, as long as the __+Y Up__ option is checked, there is no rotation value in the exported data. + + ![blender export glTF](./mesh/blender_export_gltf.png) + +## Possible issues + +During the game development process, the orientation of the model may be used. For example, if some objects should face the player (using the `LookAt()` method), consider the initial orientation of the model. Here are two methods to adjust the initial orientation of the model. + +1. In __Cocos Creator 3.x__, the __-Z-axis__ is used as the forward direction, while in Blender, the forward direction is __+Y-axis__, when making a model, the positive direction of the __Y-axis__ should be used as the orientation of the object, and the derived transformation later, in __Cocos Creator__, the __-Z-axis__ will be used as the front direction. +2. To not change the orientation in the DCC tool, try adding a parent node to the imported model in the scene, and then rotate the model so that the initial orientation of the model is the __-Z-axis__. All subsequent rotation-related operations are based on the parent. A node is an operation object. + +## Artist's production specifications + +1. Reasonably formulating a sub-assets name under model assets (e.g __mesh__ or __material__). Each modification of the sub-assets name will result in the loss of the place associated with the sub-assets in the project. + +2. When a part of the model needs to be transparent and a part does not need to be transparent, it should be exported into two materials. If it is a material export that is prone to model penetration, you need to manually adjust the material. + +3. External asset references, use relative path when exporting. Otherwise, under the cooperation of multiple people, the original asset path will not be recognized, resulting in the model's built-in materials cannot obtain the texture correctly and appear yellow. __Autodesk 3ds Max__ export local path is modified as follows: + + ![relative path](./mesh/relative_path.png) diff --git a/versions/4.0/en/asset/model/glTF.md b/versions/4.0/en/asset/model/glTF.md new file mode 100644 index 0000000000..2a41a86212 --- /dev/null +++ b/versions/4.0/en/asset/model/glTF.md @@ -0,0 +1,163 @@ +# glTF Models + +Cocos Creator supports glTF 2.0 and earlier file formats. + +## URI Parsing + +Creator supports URIs in the following form specified in glTF: + +- Data URI + +- Relative URI path + +- File URL + +- File path + +## Conversion Relationships + +When importing a glTF model into Creator, the assets in glTF will be converted to assets in Creator according to the following relationships: + +| glTF Asset | Cocos Creator Asset | +| :---------- | :---------------- | +| [glTF Scene](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-scene) | Prefab | +| [glTF Mesh](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-mesh) | Mesh | +| [glTF Skin](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-skin) | Skeleton | +| [glTF Material](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-material) | Material | +| [glTF Texture](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-texture) | Texture | +| [glTF Image](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-image) | Image | +| [glTF Animation](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-animation) | Animation Clip | + +### glTF Scene + +After import, the glTF scene will be converted to a prefab asset in Creator, and the nodes recursively contained in the glTF scene will be converted to nodes in the prefab one by one according to the same hierarchical relationship. + +#### Scene Root Node + +The prefab will use a node without any spatial transformation information as the root node, and all [root nodes](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenenodes) of the glTF scene will be the children of this node. + +#### Node Conversion + +The properties in the glTF node will be converted to properties in the prefab node according to the texture relationships in the following table: + +| glTF Node Property | Prefab Node Property | +| :----------- | :----------- | +| [Hierarchy](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodechildren) | Hierarchy | +| [Displacement](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodetranslation) | Position | +| [Rotation](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#noderotation) | Rotation | +| [Scaling](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodescale) | Scaling | +| [Matrix](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodematrix) | Decompress and set the position, rotation, and scaling respectively | +| [Mesh](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodemesh) | MeshRenderer component | +| [Skin](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodeskin) | SkinnedMeshRenderer component | +| [Initial Weight](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodeweights) | (Skinned) MeshRenderer component weight | + +#### MeshRenderer + +If the glTF node references a mesh, then the corresponding prefab node will also have a MeshRenderer component added to it after import. If the glTF node also references a skin, then the corresponding prefab node will also have a SkinnedMeshRenderer added to it. + +The mesh, skeletons, and materials in the SkinnedMeshRenderer component correspond to the transformed glTF mesh, skin, and material assets. + +If the glTF node specifies an initial weight, the converted (skinned) MeshRenderer will also carry this weight. + +### glTF Mesh + +After import, the glTF mesh will be converted to a mesh asset in Cocos Creator. + +All [primitives](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#meshprimitives-white_check_mark) in the glTF mesh will be converted to sub-meshes in the Creator one by one. + +If [weight](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#meshweights) is specified for the glTF mesh, the corresponding weights will be stored in the converted Creator mesh. + +#### glTF Primitive + +The index arrays of the glTF primitives will correspond to the index arrays of the converted Cocos Creator sub-meshes. + +The glTF primitive schema will be converted to the Cocos Creator primitive schema according to the texture in the following table. + +| glTF Primitive Schema | Cocos Creator Primitive Schema | +| :--------------- | :------------------------------- | +| `POINTS` | `gfx.PrimitiveMode.POINT_LIST` | +| `LINES` | `gfx.PrimitiveMode.LINE_LIST` | +| `LINE_LOOP` | `gfx.PrimitiveMode.LINE_LOOP` | +| `LINE_STRIP` | `gfx.PrimitiveMode.LINE_STRIP` | +| `TRIANGLES` | `gfx.PrimitiveMode.TRIANGLE_LIST` | +| `TRIANGLE_STRIP` | `gfx.PrimitiveMode.TRIANGLE_STRIP` | +| `TRIANGLE_FAN` | `gfx.PrimitiveMode.TRIANGLE_FAN` | + +glTF vertex attributes will be converted to Cocos Creator vertex attributes, and the attribute names will be converted as shown in the following table: + +| glTF Vertex Attribute Name | Cocos Creator Vertex Attribute Name | +| :------------------------ | :----------------------------------------------------------------------- | +| `POSITION` | `gfx.AttributeName.ATTR_POSITION` | +| `NORMAL` | `gfx.AttributeName.ATTR_NORMAL` | +| `TANGENT` | `gfx.AttributeName.ATTR_TANGENT` | +| `TEXCOORD_0` | `gfx.AttributeName.ATTR_TEX_COORD` | +| `TEXCOORD_1`..`TEXCOORD_8` | `gfx.AttributeName.ATTR_TEX_COORD1`..`gfx.AttributeName.ATTR_TEX_COORD8` | +| `COLOR_0` | `gfx.AttributeName.ATTR_COLOR` | +| `COLOR_1`..`COLOR_2` | `gfx.AttributeName.ATTR_COLOR1`..`gfx.AttributeName.ATTR_COLOR2` | +| `JOINTS_0` | `gfx.AttributeName.ATTR_JOINTS` | +| `WEIGHTS_0` | `gfx.AttributeName.ATTR_WEIGHTS` | + +> **Note**: if there are other `JOINTS`, `WEIGHTS` vertex attributes in the glTF primitive, such as `JOINTS_1`, `WEIGHTS_1`, it means that the vertices of this glTF mesh may be affected by more than 4 skeletons. + +For each vertex, all the weight information determined by `JOINTS_{}`, `WEIGHTS_{}` will be sorted by weight value and the four skeletons with the highest influence weight will be taken as `gfx.AttributeName.ATTR_JOINTS` and `gfx.AttributeName.ATTR_WEIGHTS`. + +glTF deformation targets will be converted to Cocos Creator sub-mesh deformation data. + +### glTF Skins + +After import, glTF skins will be converted to skeletal assets in Cocos Creator. + +### glTF Material + +After import, glTF materials will be converted to material assets in Cocos Creator. + +### glTF Texture + +After import, glTF texture will be converted to a texture asset in Cocos Creator. + +The glTF image referenced in the glTF texture will be converted to a reference to the corresponding converted Cocos Creator image. + +glTF texture properties will be converted to Cocos Creator texture properties according to the texture in the following table: + +| glTF Texture Property | Cocos Creator Texture Property | +| :----------------------- | :----------------------- | +| [Magnification Filter](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplermagfilter) | Magnification Filter | +| [Minification Filter](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplerminfilter) | Minification Filter, Mip Map Filter | +| [S Wrap Mode](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplerwraps) | S Wrap Mode | +| [T Wrap Mode](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplerwrapt) | Wrap Mode | + +The glTF texture magnification filter will be converted to the Cocos Creator texture magnification filter according to the texture in the following table: + +| glTF Texture Magnification Filter | Cocos Creator Texture Magnification Filter | +| :---------------- | :--------------------------- | +| `NEAREST` | `TextureBase.Filter.NEAREST` | +| `LINEAR` | `TextureBase.Filter.LINEAR` | + +The glTF Texture Minification Filter will be converted to Cocos Creator Texture Minification Filter and Cocos Creator Texture Mip Map Filter according to the texture relationships in the following table: + +| glTF Texture Minification Filter | Cocos Creator Texture Minification Filter | Cocos Creator Mip Map Filter | +|:------------------------ | :---------------------------- | :----------------------------- | +| `NEAREST` | `TextureBase.Filter.NEAREST` | `TextureBase.Filter.NONE` | +| `LINEAR_MIPMAP_LINEAR` | `TextureBase.Filter.LINEAR` | `TextureBase.Filter.LINEAR` | +| `LINEAR_MIPMAP_NEAREST` | `TextureBase.Filter.LINEAR` | `TextureBase.Filter.NEAREST` | +| `LINEAR` | `TextureBase.Filter.LINEAR` | `TextureBase.Filter.NONE` | +| `NEAREST_MIPMAP_LINEAR` | `TextureBase.Filter.NEAREST` | `TextureBase.Filter.LINEAR` | +| `NEAREST_MIPMAP_NEAREST` | `TextureBase.Filter.NEAREST` | `TextureBase.Filter.NEAREST` | + +glTF Texture Wrap mode will be converted to Cocos Creator Texture Wrap mode according to the texture in the following table: + +| glTF Texture Wrap Mode | Cocos Creator Texture Wrap Mode | +| :---------------- | :------------------------------------- | +| `CLAMP_TO_EDGE` | `TextureBase.WrapMode.CLAMP_TO_EDGE` | +| `REPEAT` | `TextureBase.WrapMode.REPEAT` | +| `MIRRORED_REPEAT` | `TextureBase.WrapMode.MIRRORED_REPEAT` | + +### glTF Image + +After import, the glTF image will be converted to an image asset in Cocos Creator. + +When the [URI](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#imageuri) of the glTF image is a Data URI, the image data will be fetched from the Data URI. Otherwise, the image data will be resolved from the Data URI according to the [Cocos Creator Image Location Resolution Algorithm](./image-location-resolution.md), where `url` is the URI of the glTF image and `startDir` is the directory where the glTF file is located. + +### glTF Animation + +After import, glTF animations will be converted to Cocos Creator animation assets. diff --git a/versions/4.0/en/asset/model/image-location-resolution.md b/versions/4.0/en/asset/model/image-location-resolution.md new file mode 100644 index 0000000000..c0be3a8811 --- /dev/null +++ b/versions/4.0/en/asset/model/image-location-resolution.md @@ -0,0 +1,35 @@ +# Cocos Creator Image Location Resolution Algorithm + +The parameters given in the Cocos Creator image location resolution algorithm and their descriptions are as follows: + +| Parameter | Description | +| :---- | :------ | +| `url` | The desired URL | +| `startDir` | The starting search directory | +| `DEPTH` | Search depth, fixed to **2** | +| `SEARCH_DIR_NAMES` | Array of texture folder names, default is: `textures`, `materials` | +| `SEARCH_EXT_NAMES` | Array of extensions to search for, fixed to `.jpg`, `.jpeg`, `.png`, `.tga`, `.webp` | + +The Cocos Creator image location resolution algorithm is given by the following procedure: + +- If the file corresponding to `url` exists, return `url`. + +- Let `expectedExtName` be the extension of `url`. + +- let `expectedBaseName` be the extensionless filename of `url`. + +- Let `searchExtNames` be the array of `[expectedExtName, ...SEARCH_EXT_NAMES]` after de-duplication. + +- Let `currentDir` be `startDir` and loop for `DEPTH` times: + + - If `currentDir` is outside the project's `assets` directory, then exit the loop. + + - If none of the subdirectories in the `currentDir` directory have names matching `SEARCH_DIR_NAMES`, then the next loop is executed. + + - Make `dir` a subdirectory of the `currentDir` directory with a name matching `SEARCH_DIR_NAMES`. + + - Search `dir` for files with base names matching `expectedBaseName` and extensions matching `searchExtNames`, and if so, return their paths. + + - Set `currentDir` to its parent directory. + +- Return search failure. diff --git a/versions/4.0/en/asset/model/max-export-fbx.md b/versions/4.0/en/asset/model/max-export-fbx.md new file mode 100644 index 0000000000..88ecb00f39 --- /dev/null +++ b/versions/4.0/en/asset/model/max-export-fbx.md @@ -0,0 +1,38 @@ +# Export FBX file from 3ds Max + +## Steps + +1. Select the model: + + ![Select the model ](../../../zh/asset/model/max/01-select-mesh.png) + +2. Go to **File -> Export -> Export Selected**: + + ![Export Selected](../../../zh/asset/model/max/02-export-selected.png) + +3. Set **Save as type** to **.fbx**, click the **save** button: + + ![Name the file](../../../zh/asset/model/max/03-export-file-name.png) + +4. Set **Current Preset** to **Autodesk Media and Entertainment**: + + ![Export Preset](../../../zh/asset/model/max/04-export-preset-selection.png) + +5. Enable **Embed Media** check box: + + ![Enable Embed Media](../../../zh/asset/model/max/05-embed-media.png) + +6. Click **OK** to export the file: + + ![Export file](../../../zh/asset/model/max/06-export-file.png) + +**Remarks** : For more information, please refer to [Max Fbx Files](https://help.autodesk.com/view/3DSMAX/2022/ENU/?guid=GUID-26E80277-1645-4C4E-A6B2-44399376490F) + +## Import fbx into your project + +1. Place the export file **simpleBox** in the **Asset** folder of your project directory. More info about [FBX Import](mesh.md). +2. Comparison of import results. + +| 3ds Max Viewport | Cocos Creator Scene Viewport | +|-----------------------------------------------------------------------------|-------------------------------------------------------------------------------| +| | | diff --git a/versions/4.0/en/asset/model/maya-export-fbx.md b/versions/4.0/en/asset/model/maya-export-fbx.md new file mode 100644 index 0000000000..fe4e062777 --- /dev/null +++ b/versions/4.0/en/asset/model/maya-export-fbx.md @@ -0,0 +1,38 @@ +# Export FBX file from Maya + +## Steps + +1. Select the model: + + ![Select the model](../../../zh/asset/model/maya/01-select-mesh.png) + +2. Go to **File -> Export Selection Option**: + + ![Export Selection Option](../../../zh/asset/model/maya/02-export-selection-option.png) + +3. Set **File type** to **FBX export**, click **export selection**: + + ![Export File Type](../../../zh/asset/model/maya/03-export-type-selection.png) + +4. Set **Current Preset** to **Autodesk Media and Entertainment**: + + ![Current Preset](../../../zh/asset/model/maya/04-export-preset-selection.png) + +5. Check on **Embed Media**: + + ![Enable Embed Media](../../../zh/asset/model/maya/05-embed-media.png) + +6. Name the file to **simpleBox**, click **Export Selection**: + + ![Export the file](../../../zh/asset/model/maya/06-export-file.png) + +**Remarks**: For more information, please refer to [Maya FBX Plugin-in](https://help.autodesk.com/view/MAYAUL/2022/ENU/index.html?guid=GUID-BD85FA4C-4D40-457C-BE66-47BC08B82FC3). + +## Import fbx into your project + +1. Place the export file **simpleBox** in the **Asset** folder of your project directory. More info about [FBX import](mesh.md). +2. Comparison of import results. + +| Maya Viewport | Cocos Creator Scene Viewport | +|-------------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| | | diff --git a/versions/4.0/en/asset/model/maya/01-select-mesh.png b/versions/4.0/en/asset/model/maya/01-select-mesh.png new file mode 100644 index 0000000000..048de976e6 Binary files /dev/null and b/versions/4.0/en/asset/model/maya/01-select-mesh.png differ diff --git a/versions/4.0/en/asset/model/maya/02-export-selection-option.png b/versions/4.0/en/asset/model/maya/02-export-selection-option.png new file mode 100644 index 0000000000..9478922a90 Binary files /dev/null and b/versions/4.0/en/asset/model/maya/02-export-selection-option.png differ diff --git a/versions/4.0/en/asset/model/maya/03-export-type-selection.png b/versions/4.0/en/asset/model/maya/03-export-type-selection.png new file mode 100644 index 0000000000..b2f3f03d07 Binary files /dev/null and b/versions/4.0/en/asset/model/maya/03-export-type-selection.png differ diff --git a/versions/4.0/en/asset/model/maya/04-export-preset-selection.png b/versions/4.0/en/asset/model/maya/04-export-preset-selection.png new file mode 100644 index 0000000000..9d244a97bc Binary files /dev/null and b/versions/4.0/en/asset/model/maya/04-export-preset-selection.png differ diff --git a/versions/4.0/en/asset/model/maya/05-embed-media.png b/versions/4.0/en/asset/model/maya/05-embed-media.png new file mode 100644 index 0000000000..1b5361d970 Binary files /dev/null and b/versions/4.0/en/asset/model/maya/05-embed-media.png differ diff --git a/versions/4.0/en/asset/model/maya/06-export-file.png b/versions/4.0/en/asset/model/maya/06-export-file.png new file mode 100644 index 0000000000..3876add5dc Binary files /dev/null and b/versions/4.0/en/asset/model/maya/06-export-file.png differ diff --git a/versions/4.0/en/asset/model/maya/07-1-maya-viewport.png b/versions/4.0/en/asset/model/maya/07-1-maya-viewport.png new file mode 100644 index 0000000000..d3a7deaaf4 Binary files /dev/null and b/versions/4.0/en/asset/model/maya/07-1-maya-viewport.png differ diff --git a/versions/4.0/en/asset/model/maya/07-2-cocos-viewport.png b/versions/4.0/en/asset/model/maya/07-2-cocos-viewport.png new file mode 100644 index 0000000000..3de3e9c8f1 Binary files /dev/null and b/versions/4.0/en/asset/model/maya/07-2-cocos-viewport.png differ diff --git a/versions/4.0/en/asset/model/mesh.md b/versions/4.0/en/asset/model/mesh.md new file mode 100644 index 0000000000..f6fd37ea53 --- /dev/null +++ b/versions/4.0/en/asset/model/mesh.md @@ -0,0 +1,159 @@ +# Model Assets + +Currently, Creator supports model files in __FBX__ and __glTF__ formats. + +- FBX: FBX 2020 and earlier file formats are supported. +- glTF: glTF 2.0 and earlier file formats are supported, please refer to the [glTF models](./glTF.md) documentation for details. + +For how to export these two model files from third-party tools, please refer to the [Importing Models Exported from DCC Tools](./dcc-export-mesh.md) documentation. + +## Model Import + +After importing into the editor, from the outside, the corresponding model asset file can be obtained in the __Assets__ panel. It's directory structure is as follows: + +- The structure of a model file without animations is as follows: + + ![mesh_list](mesh/mesh_list.png) + +- The structure of the model file that contains animations is as follows: + + ![mesh_list](mesh/mesh_list_1.png) + + - `.material` -- Material files + - `.mesh` -- Model files + - `.texture` -- Model texture files + - `.animation` -- Model animation files + - `.skeleton` -- Model bone files + - `.prefab` -- Prefab files that are automatically generated on import + +### Mesh File + +Mesh File contains vertices, index, and texture coordinate data. When a mesh file is selected in the __Assets__, the __Inspector__ will show the information of the mesh, also the mesh can be previewed in the bottom panel. + +![Mesh Asset](mesh/mesh-asset-preview.png) + +| Property | Description | +| :-- | :-- | +| Vertices | number of vertices | +| Triangles | number of triangle | +| UV | indices of texture coordinate | +| MinPos | minimum position of mesh | +| MaxPos | maximum position of mesh | + +## Using Models + +After importing a __model__ file, drag the __root node__ of the __model__ file directly from the __Assets__ panel to the __node__ you want to place in the __Hierarchy__ panel to complete the node creation. At this point the model is successfully created in the scene.
+Alternatively, to expand the node of the __model__ file, select the `.prefab` file under the model file node, and drag it from the __Assets__ panel into the __Hierarchy__ panel to complete the creation. + +![mesh_use](mesh/mesh_use.gif) + +## Model asset Properties + +When the model asset file (`.fbx` or `.gltf`) is selected in the __Assets__ panel, the properties of the model asset can be set in the __Inspector__ panel. + +There are four tabs to choose from: +| Tab | Description | +| :--- | :--- | +| __Model__ | Used to set options related to model mesh import. | +| __Animation__ | Used to view and edit model animation clips. | +| __Material__ | Used to set options related to the import of model materials. | +| __FBX__ | Used to set options related to FBX file. | + +### Model + +![mesh_model](mesh/mesh-model.jpg) + +| Property | Description | +| :--- | :--- | +| Normals | Normals import setting, including the following four options:
1. __Optional__: Import normals only if the model file contains normals.
2. __Exclude__: Do not import normals.
3. __Required__: Import normals that are contained in the model file, or recalculate if not contained. It is recommended to use this option if the model data itself is fine, without additional processing.
4. __Recalculate__: Recalculate normals and import, ignoring whether if the model file contain normals. Selecting this option will increase the calculated amount, but it will eliminate the subsequent problems caused by the absence of normalization of the model's original normal data. | +| Tangents | Tangents import setting, including Optional, Exclude, Require and Recalculate four options, option feature can refer to the description of __Normals__, the two are not very different. | +| Morph Normals | Import the deformation normal information, including:
__Optional__: Import only the deformation normals contained in the model file, for cases where you know your model data very well.
__Exclude__: Not to import deformation normals. | +| Skip Validation | Skip validation of the model file. | +| Disable mesh split | Currently there is a joint-counting-based mesh splitting process during the import pipeline to workaround the max uniform vector limit problem for real-time calculated skeletal animation system on many platforms. This process has a preference impact on other runtime system too. So if it can be pre-determined that the real-time calculated skeletal animations (when `useBakedAnimation` option of the __SkeletalAnimation__ component is unchecked) will not be used, this option can be checked to improve preference. But note that toggling this would update the corresponding prefab, so all the references in the scene should be updated as well to accompany that.
Please refer to the following for details. | +| Allow Data Access| Identifies whether all mesh data in this model can be read or written. If unchecked, the grid data will be automatically released after it is committed to the CPU | +| Promote Single Root Node| If enabled and there is only one root node at the top of the model scene, then that node will be the root node of the Prefab, otherwise all root nodes of the scene will be the children of the Prefab | + +### Disable Mesh Split + +The policy for Disable mesh split in v3.6 is as follows: + +- By default, the model is not split and no changes are made to the imported model data (the previous model settings are also maintained) +- If the number of bones does not exceed the actual runtime drive limit, pass it directly using uniform +- If the number of bones exceeds the limit, use texture pass + +![mesh-advopts](mesh/mesh-advopts.jpg) + +### Mesh Optimize +| Property | Desc | +| :-- | :-- | +| __Vetex Cache__ | Reorder triangles to improve the vertex cache hit rate.| +| __Vertex Fetch__| Reorder triangles to improve the vertex fetch efficiency. | +| __Overdraw__ | Reorder triangles to reduce overdraw. | + +> It is recommended to enable the above options for meshes with high vertex count. +> If multiple options are enabled simultaneously, the optimization algorithm will automatically adjust parameters to achieve the best result. + +### Mesh Simplify +| Property | Desc | +| :-- | :-- | +| __Target Ratio__ | Target ratio of the simplified mesh data, recommended to set to 0.5 | +| __Auto Error Rate__ | Whether to calculate the error rate automatically | +| __Error Rate__ | Tune it until you get a good result | +| __Lock Boundary__ | Whether to lock the boundary of the simplified mesh data | + +> The known issue is that there may be a loss of UV layout after mesh reduction. Developers should pay attention to warnings at the bottom of the inspector to determine whether to use this option. + +### Mesh Cluster +| Property | Desc | +| :-- | :-- | +| __Generate Bounding__ | Whether to generate bounding sphere and normal cone for clustered mesh | + +### Mesh Compress +| Property | Desc | +| :-- | :-- | +| __Encode__ | Encode to binary format to reduce file size | +| __Compress__ | Use zlib, which is based on LZ777 and Huffman coding, for compression | +| __Quantize__ | Quantize the mesh data, compress float-point values to reduce file size | + +### LODS + +The mesh importer will automatically take sub-meshes end with _lodN as a LOD node, if none, you can use the auto LOD by checking the LOD check box. + +The LOD1 and LOD2 options are used to set the ratios of the triangle count for different levels. + +For more, please refer to [Level Of Details](../../editor/rendering/lod.md). + +>The algorithm details for the above options can be found in the open-source library: https://github.com/zeux/meshoptimizer + +### Animation + +![mesh_animation](mesh/mesh_animation.png) + +The above image is all the animation asset information under the current model, and the editing area of ​​the specific frame number information of the currently selected animation. You can change the animation name or perform simple animation cropping here. To do so: + +- Click the __+__ button in the red box on the image to add an animation clip asset. The new file added by default copies a complete clip data. You can input the number of frames in the `Start` and `End` input box to crop the animation. (Drag and drop animation is not currently supported) + +- Click the __-__ button in the red box on the image to delete the currently selected animation file + +### Material + +![mesh_material](mesh/mesh_material.png) + +The top half of the properties are described below, while the bottom half shows the materials contained in the current model. + +| Property | Description | +| :--- | :--- | +| Dump material | It is possible to customize or modify the material that comes with the model file. Enable this option to dump the material files in the file structure directory out of the model assets for modifying the materials. | +| Material dumper directory | Specify or view the directory location for the dumped files. | +| Use vertex colors | Whether to use vertex colors. | +| Depth-write if blending | Enable depth-write when Alpha mode is __Blend__. | + +### FBX + +![mesh material](mesh/mesh_fbx.png) + +| Property | Description | +| :--- | :--- | +| Animation Baking Rate | Units are __frames per second__ and options include __auto__, __24__, __25__, __30__ and __60__. | +| Promote single root node | When this option is enabled, if the FBX asset contains a scene with only one root node, that root node will be used as the root node of the prefab when converting the FBX scene to a Creator's prefab. Otherwise, the FBX scene will be used as the root node. | +| Prefer Local Time Span | If this option is checked, imported FBX animations will use the animation time range recorded in the FBX file as a priority.
If this option is not checked, the animation time range from the FBX will not be used, and the animation time range will be calculated roughly.
Some FBX tools may not export animation time range information, so the animation time range is also calculated roughly. | diff --git a/versions/4.0/en/asset/model/mesh/blender_export_bake.png b/versions/4.0/en/asset/model/mesh/blender_export_bake.png new file mode 100644 index 0000000000..be4bfc8568 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/blender_export_bake.png differ diff --git a/versions/4.0/en/asset/model/mesh/blender_export_fbx_1.png b/versions/4.0/en/asset/model/mesh/blender_export_fbx_1.png new file mode 100644 index 0000000000..c81837e020 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/blender_export_fbx_1.png differ diff --git a/versions/4.0/en/asset/model/mesh/blender_export_gltf.png b/versions/4.0/en/asset/model/mesh/blender_export_gltf.png new file mode 100644 index 0000000000..b58b289886 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/blender_export_gltf.png differ diff --git a/versions/4.0/en/asset/model/mesh/blender_model.png b/versions/4.0/en/asset/model/mesh/blender_model.png new file mode 100644 index 0000000000..13a4a92716 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/blender_model.png differ diff --git a/versions/4.0/en/asset/model/mesh/blender_model_bake_c3d.png b/versions/4.0/en/asset/model/mesh/blender_model_bake_c3d.png new file mode 100644 index 0000000000..9e3af8a0bd Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/blender_model_bake_c3d.png differ diff --git a/versions/4.0/en/asset/model/mesh/blender_model_c3d.png b/versions/4.0/en/asset/model/mesh/blender_model_c3d.png new file mode 100644 index 0000000000..39a391a4ca Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/blender_model_c3d.png differ diff --git a/versions/4.0/en/asset/model/mesh/dynamic-mesh.gif b/versions/4.0/en/asset/model/mesh/dynamic-mesh.gif new file mode 100644 index 0000000000..c9342983e1 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/dynamic-mesh.gif differ diff --git a/versions/4.0/en/asset/model/mesh/mesh-advopts.jpg b/versions/4.0/en/asset/model/mesh/mesh-advopts.jpg new file mode 100644 index 0000000000..b64bf7a7f5 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh-advopts.jpg differ diff --git a/versions/4.0/en/asset/model/mesh/mesh-asset-preview.png b/versions/4.0/en/asset/model/mesh/mesh-asset-preview.png new file mode 100644 index 0000000000..50f53cff64 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh-asset-preview.png differ diff --git a/versions/4.0/en/asset/model/mesh/mesh-model.jpg b/versions/4.0/en/asset/model/mesh/mesh-model.jpg new file mode 100644 index 0000000000..513b1865d1 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh-model.jpg differ diff --git a/versions/4.0/en/asset/model/mesh/mesh-optimizer-warn.png b/versions/4.0/en/asset/model/mesh/mesh-optimizer-warn.png new file mode 100644 index 0000000000..e1d24f1478 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh-optimizer-warn.png differ diff --git a/versions/4.0/en/asset/model/mesh/mesh-optimizer.png b/versions/4.0/en/asset/model/mesh/mesh-optimizer.png new file mode 100644 index 0000000000..b3b126092d Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh-optimizer.png differ diff --git a/versions/4.0/en/asset/model/mesh/mesh_animation.png b/versions/4.0/en/asset/model/mesh/mesh_animation.png new file mode 100644 index 0000000000..e4c9e9e120 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh_animation.png differ diff --git a/versions/4.0/en/asset/model/mesh/mesh_fbx.png b/versions/4.0/en/asset/model/mesh/mesh_fbx.png new file mode 100644 index 0000000000..8f25396e36 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh_fbx.png differ diff --git a/versions/4.0/en/asset/model/mesh/mesh_list.png b/versions/4.0/en/asset/model/mesh/mesh_list.png new file mode 100644 index 0000000000..41fe8f050d Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh_list.png differ diff --git a/versions/4.0/en/asset/model/mesh/mesh_list_1.png b/versions/4.0/en/asset/model/mesh/mesh_list_1.png new file mode 100644 index 0000000000..52db5e7097 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh_list_1.png differ diff --git a/versions/4.0/en/asset/model/mesh/mesh_material.png b/versions/4.0/en/asset/model/mesh/mesh_material.png new file mode 100644 index 0000000000..ea787416d7 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh_material.png differ diff --git a/versions/4.0/en/asset/model/mesh/mesh_model.png b/versions/4.0/en/asset/model/mesh/mesh_model.png new file mode 100644 index 0000000000..55bdf47e3d Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh_model.png differ diff --git a/versions/4.0/en/asset/model/mesh/mesh_use.gif b/versions/4.0/en/asset/model/mesh/mesh_use.gif new file mode 100644 index 0000000000..64ddde3749 Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/mesh_use.gif differ diff --git a/versions/4.0/en/asset/model/mesh/relative_path.png b/versions/4.0/en/asset/model/mesh/relative_path.png new file mode 100644 index 0000000000..a34cff8e8d Binary files /dev/null and b/versions/4.0/en/asset/model/mesh/relative_path.png differ diff --git a/versions/4.0/en/asset/model/scripting-mesh.md b/versions/4.0/en/asset/model/scripting-mesh.md new file mode 100644 index 0000000000..863a5e1ac0 --- /dev/null +++ b/versions/4.0/en/asset/model/scripting-mesh.md @@ -0,0 +1,22 @@ +# Programmatically Create Meshes + +Meshes can be created through the API when the model created by the DCC (Digital Content Creation) software or by the in-engine terrain editor does not meet the needs. If you need to create some kind of snake that can grow at runtime, edit the model dynamically, or implement certain surfaces, you can create the mesh programmatically. + +## Create Mesh + +The engine supports two kinds of meshes: **static meshes** and **dynamic meshes** for different scenarios, which developers can use on demand. + +- Static mesh, created with `utils.MeshUtils.createMesh`, once created successfully, the geometry inside the mesh is not editable. +- Dynamic mesh: created with `utils.MeshUtils.createDynamicMesh`, once created, the geometry inside the mesh can still be modified. + +The return value is the `Mesh` component, so it is convenient to assign it to the `mesh` property of the `MeshRenderer` so that it can be displayed on the screen. + +## API + +API please refer to [MeshUtils](%__APIDOC__%/zh/class/utils.MeshUtils). + +## Example + +![dynamic mesh](./mesh/dynamic-mesh.gif) + +See [GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8) for an example of a dynamic mesh. diff --git a/versions/4.0/en/asset/model/vat.md b/versions/4.0/en/asset/model/vat.md new file mode 100644 index 0000000000..8ed13dd3c9 --- /dev/null +++ b/versions/4.0/en/asset/model/vat.md @@ -0,0 +1,68 @@ +# Vertex Animation Texture (VAT) + +Vertex Animation Texture (VAT) is commonly used to express physics simulation animations such as rigid body fragmentation, fabric, fluid, etc. Physics simulation animations computed in PCG software such as Houdini can be baked into VAT data for export and playback in real-time rendering engines to reproduce complex physics effects with very low performance consumption. + +Currently, the supported VAT data formats are rigid body, flexible body and fluid data exported from Houdini, and fluid data from Zeno. + +## Data Export and Usage + +The exported VAT data contains three parts: fbx model, animation texture, and json file containing VAT material properties (frame count and box max/min value, etc.). + +### Rigid Body + +1、Export:After Houdini set up rigid body collision, crushing and other animations, choose **VAT2.0 version, UE mode, no paddle, LDR way** to export. + +2、Material:Create a material in Cocos Creator, choose **util/dcc/vat/houdini-rigidbody-v2** for Effect. + +3、Parameter:Check the exported json file, fill the data of Animation Speed, NumOfFrames, PivotMin/Max, PosMin/Max into the material. + +4, Texture: Expand the exported Position shape map, drag the body into PositionMap, and the subordinate Sign and Alpha into PosSignMap and PosAlphaMap respectively (if any). +Rotation normal map is the same. + +### Softbody + +1, Export: After Houdini set up the animation of soft body collision and crushing, choose **VAT3.0 version, Unity mode, no paddle, LDR way** to export. + +2、Material:Create a material in Cocos Creator, choose **util/dcc/vat/houdini-softbody-v3** for Effect. + +3、Parameter:Check the exported json file, fill the data of Animation Speed and NumOfFrames into the material. + +4、Texture:Expand the exported Position shape map, drag the body into PositionMap, and the subordinate Sign and Alpha into PosSignMap and PosAlphaMap respectively (if any). +Same for the Rotation normal map. + +### Fluid (Houdini) + +1、Export:After Houdini set up the fluid animation, choose **VAT3.0 version, Unity mode, no paddle, LDR way** to export. + +2、Material:Create a material in Cocos Creator, choose **util/dcc/vat/houdini-fluid-v3-liquid** for Effect. + +3、Parameter: Check the exported json file, fill the data of Animation Speed and NumOfFrames into the material. + + - Check Use Lookup Texture, drag the exported lookup image into LookupMap. + - Check Use Lookup Auto Frame, fill the resolution of the exported lookup into LUTTextureWidth/Height. + - Select the exported fbx model, record its vertex count and fill in FBXVertexCount. + +4. Mapping: Expand the exported Position map, drag the body into PositionMap, and the subordinate Sign and Alpha into PosSignMap and PosAlphaMap respectively (if any). +Same for Rotation normal map. + +### Fluid (Zeno) + +1, Export: After Zeno set up the fluid animation, choose **VAT mode** to export. + +2、Material:Create a material in Cocos Creator, choose **util/dcc/vat/zeno-fluid-liquid** for Effect. + +3、Parameters:Check the exported json file, and fill the data such as Animation Speed and NumOfFrames into the material. + +4、Texture:Expand the exported Position map and drag it to PositionMap, Rotation map and drag it to RotationMap. + +### Error effect debugging + +### Rigid Body + +If there are animation anomalies such as the pieces going around in circles, please make sure the export option is in VAT2.0 UE mode, if you choose the wrong Unity mode, it may lead to this situation. + +### Fluid + +If the faces are broken or even a little bit messed up, please check if the NumOfFrames are the same as the ones in the DCC software. + +If the whole fluid is shaking or has interspersed faces, make sure the filter mode of both maps is Nearest, and make sure fix alpha transparency artifacts is unchecked. \ No newline at end of file diff --git a/versions/4.0/en/asset/options.md b/versions/4.0/en/asset/options.md new file mode 100644 index 0000000000..e8192f460a --- /dev/null +++ b/versions/4.0/en/asset/options.md @@ -0,0 +1,61 @@ +# Optional Parameters + +> Author: Santy-Wang, Xunyi + +For added flexibility and extensibility, most of the load interfaces in Asset Manager, including `assetManager.loadAny` and `assetManager.preloadAny`, provide `options` parameters. In addition to configuring the built-in parameters of Creator, `options` also allows you to customize any parameters to extend engine functionality. If you do not need to configure the engine's built-in parameters or extend the engine's functionality, you can ignore it and just use the simpler API interfaces, such as `resources.load`. + +Parameters that are currently used by the engine in `options` include the following: + +`uuid`, `url`, `path`, `dir`, `scene`, `type`, `priority`, `preset`, `audioLoadMode`, `ext`, +`bundle`, `onFileProgress`, `maxConcurrency`, `maxRequestsPerFrame`, `maxRetryCount`, `version`, `xhrResponseType`, +`xhrWithCredentials`, `xhrMimeType`, `xhrTimeout`, `xhrHeader`, `reloadAsset`, `cacheAsset`, `cacheEnabled`, + +**DO NOT** use the above fields as your custom parameter names to avoid conflicts with engine functions. +## Engine Extension + +You can extend the loading capabilities of the engine by using optional parameters in the [Pipeline](pipeline-task.md) and [Custom Handlers](downloader-parser.md#custom-handlers). For example: + +```typescript +// Extend the pipeline +assetManager.pipeline.insert(function (task, done) { + let input = task.input; + for (let i = 0; i < input.length; i++) { + if (input[i].options.myParam === 'important') { + console.log(input[i].url); + } + } + task.output = task.input; + done(); +}, 1); + +assetManager.loadAny({'path': 'images/background'}, {'myParam': 'important'}, callback); + +// Register the handler +assetManager.downloader.register('.myformat', function (url, options, callback) { + // Download the resource + const img = new Image(); + if (options.isCrossOrigin) { + img.crossOrigin = 'anonymous'; + } + + img.onload = function () { + callback(null, img); + }; + + img.onerror = function () { + callback(new Error('download failed'), null); + }; + + img.src = url; + +}); + +assetManager.parser.register('.myformat', function (file, options, callback) { + // Parse the downloaded file + callback(null, file); +}); + +assetManager.loadAny({'url': 'http://example.com/myAsset.myformat'}, {isCrossOrigin: true}, callback); +``` + +The engine can be extremely extensible by using optional parameters, combined with pipelines and custom handlers, and Asset Bundle can be seen as the first instance of extension using optional parameters. diff --git a/versions/4.0/en/asset/pipeline-task.md b/versions/4.0/en/asset/pipeline-task.md new file mode 100644 index 0000000000..c3f725ce6a --- /dev/null +++ b/versions/4.0/en/asset/pipeline-task.md @@ -0,0 +1,73 @@ +# Pipeline and Task + +> Author: Santy-Wang, Xunyi +> +> This article is for advanced developers who have customization needs for their loading process. + +To make it easier to modify or extend the loading process of engine, the underlying layer of Asset Manager uses a mechanism called **Pipeline and Task**, **Download and Parser** to load resources. This article will focus on **Pipeline and Task**. + +Although `loader` used before v2.4 already used the concept of **Pipeline** for resource loading, in Asset Manager we have refactored the pipeline to make the logic clearer and easier to extend. You can extend an existing pipeline, or customize a pipeline using the class `AssetManager.Pipeline` provided by the engine. + +## Pipeline + +The **Pipeline** (defined in `AssetManager.Pipeline`) can be understood as a series of processes in series, and as a request passes through the pipeline, it is processed in turn by each stage of the pipeline, with the result of the processing finally being output. The schematic is as follows: + +![pipeline](pipeline-task/pipeline.png) + +The advantage of a pipeline over a regular fixed process is that all the links in the pipeline are spliceable and combinable, which means you can insert new stages or remove old stages at any point in the existing pipeline, greatly increasing flexibility and scalability. + +### The built-in Pipeline + +There are three pipelines built into the Asset Manager, as shown in the figure: + +![builtin-pipeline](pipeline-task/builtin-pipeline.jpg) + +- The first pipeline is used to convert resource paths and find the real resource paths. +- The second pipeline is used for the normal loading process. +- The third pipeline is used for the preload process. + +> **Note**: the second pipeline uses a downloader and a parser, and the third pipeline uses a downloader. See document [Download and Parse](downloader-parser.md) for details. + +### Custom Pipeline + +You can extend the built-in pipeline to achieve your own customization needs: + +```typescript +assetManager.pipeline.insert(function (task, done) { + task.output = task.input; + for (let i = 0; i < task.input; i++) { + console.log(task.input[i].content); + } + done(); +}, 1); +``` + +You can also build a new pipeline: + +```typescript +const pipeline = new AssetManager.Pipeline('test', [(task, done) => { + console.log('first stage'); + done(); +}, (task, done) => { + console.log('second stage'); + done(); +}]); +``` + +Building the pipeline requires a series of methods, each of which requires passing in a task parameter and a completion callback parameter. You can access everything about the task in the method and just call the completion callback on completion. + +## Task + +**Task** is a request flowing in the pipeline, a task contains inputs, outputs, completion callbacks, [optional parameters](options.md) and so on. As the task flows through the pipeline, each stage of the pipeline takes out the input of the task, makes some processing and saves it back to the output. For example: + +```typescript +assetManager.pipeline.insert(function (task, done) { + for (let i = 0; i < task.input.length; i++) { + task.input[i].content = null; + } + task.output = task.input; + done(); +}, 1); +``` + +For details, please refer to the type [AssetManager.Task](%__APIDOC__%/en/class/AssetManager.Task). diff --git a/versions/4.0/en/asset/pipeline-task/builtin-pipeline.jpg b/versions/4.0/en/asset/pipeline-task/builtin-pipeline.jpg new file mode 100644 index 0000000000..a0d18a27a6 Binary files /dev/null and b/versions/4.0/en/asset/pipeline-task/builtin-pipeline.jpg differ diff --git a/versions/4.0/en/asset/pipeline-task/pipeline.png b/versions/4.0/en/asset/pipeline-task/pipeline.png new file mode 100644 index 0000000000..d44c781b84 Binary files /dev/null and b/versions/4.0/en/asset/pipeline-task/pipeline.png differ diff --git a/versions/4.0/en/asset/prefab.md b/versions/4.0/en/asset/prefab.md new file mode 100644 index 0000000000..b0c81ba3c9 --- /dev/null +++ b/versions/4.0/en/asset/prefab.md @@ -0,0 +1,98 @@ +# Prefab + +Prefab is used to store some scene objects that can be reused, it can contain nodes, components, and data in components. The instances generated by the prefab asset can not only inherit the data of the template, but also have it's own customized data modification. + +## Basic Concepts + +| Name | Description | Example | +| :--- | :--- | :--- | +| Prefab Asset | The asset of prefab in __Assets__ panel, which is the serialize file of Prefab.| ![prefab asset](prefab/prefab-asset.png) | +| Prefab Instance | When the __Prefab Asset__ is drag to the __Hierarchy__ panel, it will generate a __Prefab Instance__. Its root node is currently marked in dark green, and its child nodes are bright green | ![prefab instance](prefab/prefab-instance.png) | +| Prefab Editing Mode | Double-click the __Prefab Asset__ to enter the prefab editing mode. At this time, all non-nested prefab nodes are displayed in bright green | ![prefab edit mode](prefab/prefab-edit-mode.png) | +| Nested Prefab Instance | When a child node in a __Prefab Asset__ is an instance of another __Prefab Asset__, we call this child prefab a nested prefab instance | ![nested-prefab](prefab/nested-prefab.png) | + +## Creating a Prefab + +There are two ways to create a prefab: + +1. After editing the __Node__ in the __Scene__, drag the __Nodes__ directly from the __Hierarchy__ panel to the __Assets__ panel to complete the creation of the __Prefab__. + +2. Click the **+** button at the top left of __Assets__, or click the blank space of the panel, and then select __Node Prefab__. (New in v3.1.1) + + ![create](prefab/create.png) + +After the creation is complete, the original __Node__ will automatically become a __Prefab Instance__, its root node is currently marked in bright green, and its child nodes are dark green. + +![create-prefab](prefab/create-prefab.gif) + +## Prefab Preview + +In version 3.8.5, we have added preview support to the inspector panel of Prefab. After users select a certain prefab, they can view the rendering content of the Prefab in the panel, which can greatly improve the efficiency of resource inspection. + +![preview](prefab/prefab-preview.png) + +## Using Prefabs + +Drag a __Prefab Asset__ from the __Assets__ panel to the __Hierarchy__ panel or __Scene__ panel to create a __Prefab Node__ in the __Scene__. + +In the __Scene__, the __Prefab Instance__ objects data source comes from the deserialization of the __Prefab Asset__ , so its data is synchronized with the __Prefab Asset__ by default. If the attributes in the prefab instance are modified, the modified data will be stored In the __Prefab Instance__, it will not affect the __Prefab Asset__ and the data of other __Prefab Instance__ generated by it. + +![use-prefab](prefab/use-prefab.gif) + +## Entering prefab editing mode + +__Double-click__ the __Prefab Asset__ in the __Assets__ panel to switch from __Scene__ editing mode to __Prefab editing__ mode. It is possible to edit the __Prefab Assets__ in the __Editor__. After editing, click __Save__ button in the __Scene__ panel to save the edited __Prefab Assets__. Next, click the __Close__ button to return to the Scene editing mode. + +> __Note__: please avoid multiple people modifying the same prefab asset at the same time, as this may lead to conflicts that cannot be resolved by `git` merging. + +![edit mode](prefab/prefab-edit-mode.gif) + +## Status of prefab nodes + +__Prefab Nodes__ in the __Inspector__ panel render __green__ to indicate normal association with assets and render __red__ to indicate that the associated assets no longer exist. + +## Editing Prefab Nodes in a Scene + +### General Operations + +In the __Hierarchy__ panel, select the __Prefab Node__, and notice there are several buttons that can be clicked at the top of the __Inspector__ panel: + +![edit prefab](prefab/edit-prefab.png) + +| Button Icon | Description | +| :--- | :--- | +| ![unlink prefab](prefab/unlink-prefab-button.png) | Revert to __normal node__. __Prefab Instance__ can become ordinary __Nodes__, that is, completely separated from the relationship between assets. This function is also available in the top-level menu `Node`. Support batch disassociation in v3.4. | +| ![locate prefab](prefab/locate-prefab-button.png) | Locating assets. It is convenient to quickly locate __Prefab Asset__ in the __Assets__ panel. | +| ![revert prefab](prefab/revert-prefab-button.png) | Restore from asset. Restore the data of the current __Prefab Instance__ to the data in the __Prefab Asset__, but the name, location, and rotation will not be restored. | +| ![apply prefab](prefab/apply-prefab-button.png) | Update to asset. Update all data of the current __Prefab Instance__ to the associated __Prefab Asset__. | + +### Add New Node + +A new node added under the __Prefab Instance__ will have a __+__ sign in the lower right corner of the node name, and its data is stored under the __Prefab Instance__, so it will not affect the data of the associated __Prefab Asset__. + +![prefab mounted](prefab/prefab-mounted-children.png) + +### Add New Component + +A new component added under the __Prefab Instance__ will have a **+** sign at the end of the node name, and its data is stored under the __Prefab Instance__, it will not affect the data of the associated __Prefab Asset__. + +![instance-add-component](prefab/instance-add-component.png) + +### Remove Component + +A new component added under the __Prefab Instance__ will have a deleted data item show in the __Inspector__, its data is stored under the __Prefab Instance__, it will not affect the data of the associated __Prefab Asset__. + +![instance-remove-component](prefab/instance-remove-component.png) + +It will show two buttons in the end of deleted data item: + +Button Icon | Description +:--- | :--- +![revert-remove-component](prefab/revert-remove-component.png) | Revert the removed component. +![apply-remove-component](prefab/apply-remove-component.png) | Apply the operation of removing the component to the associated __Prefab Asset__. + +## Some Current Restrictions + +- It is not allowed to delete the node created from the __Prefab Asset__ in the __Prefab Instance__. +- It is not allowed to change the hierarchical relationship of nodes created from the __Prefab Asset__ in the __Prefab Instance__. +- Do not allow prefabs to nest themselves. diff --git a/versions/4.0/en/asset/prefab/apply-prefab-button.png b/versions/4.0/en/asset/prefab/apply-prefab-button.png new file mode 100644 index 0000000000..14bbe2c0da Binary files /dev/null and b/versions/4.0/en/asset/prefab/apply-prefab-button.png differ diff --git a/versions/4.0/en/asset/prefab/apply-remove-component.png b/versions/4.0/en/asset/prefab/apply-remove-component.png new file mode 100644 index 0000000000..e42783e37a Binary files /dev/null and b/versions/4.0/en/asset/prefab/apply-remove-component.png differ diff --git a/versions/4.0/en/asset/prefab/create-prefab.gif b/versions/4.0/en/asset/prefab/create-prefab.gif new file mode 100644 index 0000000000..7b42b57359 Binary files /dev/null and b/versions/4.0/en/asset/prefab/create-prefab.gif differ diff --git a/versions/4.0/en/asset/prefab/create.png b/versions/4.0/en/asset/prefab/create.png new file mode 100644 index 0000000000..86ca61d3bd Binary files /dev/null and b/versions/4.0/en/asset/prefab/create.png differ diff --git a/versions/4.0/en/asset/prefab/edit-prefab.png b/versions/4.0/en/asset/prefab/edit-prefab.png new file mode 100644 index 0000000000..83c10dbb9c Binary files /dev/null and b/versions/4.0/en/asset/prefab/edit-prefab.png differ diff --git a/versions/4.0/en/asset/prefab/instance-add-component.png b/versions/4.0/en/asset/prefab/instance-add-component.png new file mode 100644 index 0000000000..b6c48344b0 Binary files /dev/null and b/versions/4.0/en/asset/prefab/instance-add-component.png differ diff --git a/versions/4.0/en/asset/prefab/instance-remove-component.png b/versions/4.0/en/asset/prefab/instance-remove-component.png new file mode 100644 index 0000000000..b192763ca7 Binary files /dev/null and b/versions/4.0/en/asset/prefab/instance-remove-component.png differ diff --git a/versions/4.0/en/asset/prefab/locate-prefab-button.png b/versions/4.0/en/asset/prefab/locate-prefab-button.png new file mode 100644 index 0000000000..bfd99f6179 Binary files /dev/null and b/versions/4.0/en/asset/prefab/locate-prefab-button.png differ diff --git a/versions/4.0/en/asset/prefab/nested-prefab.png b/versions/4.0/en/asset/prefab/nested-prefab.png new file mode 100644 index 0000000000..ed989bf6be Binary files /dev/null and b/versions/4.0/en/asset/prefab/nested-prefab.png differ diff --git a/versions/4.0/en/asset/prefab/persistent.png b/versions/4.0/en/asset/prefab/persistent.png new file mode 100644 index 0000000000..e591262b53 Binary files /dev/null and b/versions/4.0/en/asset/prefab/persistent.png differ diff --git a/versions/4.0/en/asset/prefab/prefab-asset.png b/versions/4.0/en/asset/prefab/prefab-asset.png new file mode 100644 index 0000000000..a8fe20ce25 Binary files /dev/null and b/versions/4.0/en/asset/prefab/prefab-asset.png differ diff --git a/versions/4.0/en/asset/prefab/prefab-edit-mode.gif b/versions/4.0/en/asset/prefab/prefab-edit-mode.gif new file mode 100644 index 0000000000..2b3a543abf Binary files /dev/null and b/versions/4.0/en/asset/prefab/prefab-edit-mode.gif differ diff --git a/versions/4.0/en/asset/prefab/prefab-edit-mode.png b/versions/4.0/en/asset/prefab/prefab-edit-mode.png new file mode 100644 index 0000000000..5204fc1a47 Binary files /dev/null and b/versions/4.0/en/asset/prefab/prefab-edit-mode.png differ diff --git a/versions/4.0/en/asset/prefab/prefab-instance.png b/versions/4.0/en/asset/prefab/prefab-instance.png new file mode 100644 index 0000000000..61a77415b7 Binary files /dev/null and b/versions/4.0/en/asset/prefab/prefab-instance.png differ diff --git a/versions/4.0/en/asset/prefab/prefab-mounted-children.png b/versions/4.0/en/asset/prefab/prefab-mounted-children.png new file mode 100644 index 0000000000..1721c66177 Binary files /dev/null and b/versions/4.0/en/asset/prefab/prefab-mounted-children.png differ diff --git a/versions/4.0/en/asset/prefab/prefab-preview.png b/versions/4.0/en/asset/prefab/prefab-preview.png new file mode 100644 index 0000000000..de89d431c4 Binary files /dev/null and b/versions/4.0/en/asset/prefab/prefab-preview.png differ diff --git a/versions/4.0/en/asset/prefab/revert-prefab-button.png b/versions/4.0/en/asset/prefab/revert-prefab-button.png new file mode 100644 index 0000000000..10107f5855 Binary files /dev/null and b/versions/4.0/en/asset/prefab/revert-prefab-button.png differ diff --git a/versions/4.0/en/asset/prefab/revert-remove-component.png b/versions/4.0/en/asset/prefab/revert-remove-component.png new file mode 100644 index 0000000000..0b10bbaff5 Binary files /dev/null and b/versions/4.0/en/asset/prefab/revert-remove-component.png differ diff --git a/versions/4.0/en/asset/prefab/unlink-prefab-button.png b/versions/4.0/en/asset/prefab/unlink-prefab-button.png new file mode 100644 index 0000000000..254f2ec0a5 Binary files /dev/null and b/versions/4.0/en/asset/prefab/unlink-prefab-button.png differ diff --git a/versions/4.0/en/asset/prefab/use-prefab.gif b/versions/4.0/en/asset/prefab/use-prefab.gif new file mode 100644 index 0000000000..bf9284a350 Binary files /dev/null and b/versions/4.0/en/asset/prefab/use-prefab.gif differ diff --git a/versions/4.0/en/asset/preload-load.md b/versions/4.0/en/asset/preload-load.md new file mode 100644 index 0000000000..7844d9676c --- /dev/null +++ b/versions/4.0/en/asset/preload-load.md @@ -0,0 +1,26 @@ +# Loading and Preloading + +> Author: Santy-Wang, Xunyi + +In order to minimize download times, most of the loading interfaces in Asset Manager, including `load`, `loadDir`, and `loadScene` have their own corresponding preloaded versions. The parameters used for the loading interfaces and the preloading interfaces are exactly the same, with the following differences: + +1. Preloading will only download the resources and will not parse or initialize them. +2. Preloading will be more limited during the loading process, e.g.: the maximum number of download concurrently will be smaller. +3. Preloading has a lower priority, and when multiple resources are waiting to be downloaded, the preloaded resources will be downloaded last. +4. Since the preload does not do any parsing, no available resources are returned when all the preloads load are complete. + +Compared to previous versions of Creator v2.4, these optimizations reduce the preloading performance loss and ensure a smooth gaming experience. You can make full use of the network bandwidth during the game to reduce the loading time of subsequent resources. + +Since the preload does not parse the resources, you need to parse and initialize the resources with the loading interface to complete the resource loading after the preload is complete. For example: + +```typescript +resources.preload('images/background/spriteFrame', SpriteFrame); + +// Wait for while +resources.load('images/background/spriteFrame', SpriteFrame, function (err, spriteFrame) { + spriteFrame.addRef(); + self.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +> **Note**: loading does not need to wait until the preload is complete, you can load at any time. The normal loading interface will directly reuse the content that was already downloaded during the preload process, reducing the load time diff --git a/versions/4.0/en/asset/release-manager.md b/versions/4.0/en/asset/release-manager.md new file mode 100644 index 0000000000..24f903713e --- /dev/null +++ b/versions/4.0/en/asset/release-manager.md @@ -0,0 +1,134 @@ +# Releasing of Resources + +> Author: Santy-Wang, Xunyi + +A resource release module is provided in Asset Manager to manage the release of resources. + +When a resource is loaded, it is temporarily cached in `assetManager` for the next reuse. However, this also causes a constant growth in memory and video memory, so some resources can be released either by **Auto Release** or **Manual Release** if you don't need to use them. + +Releasing a resource will destroy all internal properties of the resource, such as data associated with the rendering layer, and move out of the cache, thus freeing up memory and video memory (for textures). + +**First and most important: Resources depends on each other.** + +For example, in the following graph, the Prefab resource contains the Sprite component, the Sprite component depends on the SpriteFrame, the SpriteFrame resource depends on the Texture resource, then the Prefab, SpriteFrame, and Texture resources are all cached by the `assetManager`. The advantage of doing so is that there may be another SpriteAtlas resource that depends on the same SpriteFrame and Texture, then when you manually load the SpriteAtlas, `assetManager` do not need to request the existing SpriteFrame and Texture again it will use the cache directly. + +![asset-dep](load-assets/asset-dep.png) + +## Auto Release + +The automatic release of a scene can be set directly in the editor. When you select the scene in the **Assets** panel, the **Auto Release Assets** option appears in the **Properties** panel. + +![auto-release](release-manager/auto-release.png) + +Once checked, click the **Apply** button on the top right, and then all dependent resources of the scene will be automatically released when you switch the scene. It is recommended to check the **Auto Release Assets** option for all scenes to ensure low memory consumption, except for some high usage scenes (such as the main scene). + +In addition, all `Asset` instances have member functions `Asset.addRef` and `Asset.decRef` for increasing and decreasing the reference count, respectively. Once the reference count is 0, Creator will automatically release the resource (it needs to pass a release check first, see the following section for details) + +```typescript +start () { + resources.load('images/background/texture', Texture2D, (err, texture) => { + this.texture = texture; + // Add references to resources when you need to use them. + texture.addRef(); + // ... + }); +} + +onDestroy () { + // Reduce references to resources when you don't need to use them. Creator will try to auto-release them after calling decRef. + this.texture.decRef(); +} +``` + +The advantage of auto-release is that you don't have to explicitly call the release interface, you just need to maintain the reference count of the resource and Creator will release it automatically based on the reference count. This greatly reduces the possibility of releasing resources by mistake, and you don't need to understand the complex referencing relationships between resources. For projects with no special requirements, it is recommended that you use automatic release to release resources as much as possible. + +### Release Check + +To prevent rendering or other problems caused by incorrectly releasing resources being used, Creator will perform a series of checks before auto-releasing resources: + +1. If the reference count of the resource is 0, that is, there are no references to it elsewhere, then no follow-up check is required, the resource is destroyed directly and the cache is removed. + +2. Once the resource is removed, a release check for its dependent resources is triggered synchronously, and the reference counts of all direct dependent resources (excluding descendants) of the resource after the cache is removed are reduced by 1, and a release check is triggered synchronously. + +3. If the reference count of the resource is not 0, that is, there are references to it elsewhere, a circular reference check is required at this point to avoid having its own offspring refer to it. If the reference count is still not 0 after the cyclic reference check, terminate the release. Otherwise, destroy the resource directly, remove the cache, and trigger a release check for its dependent resources (as in step 2). + +### Manual Release + +When a more complex resource release mechanism is used in a project, you can call the relevant interfaces of the Asset Manager to manually release resources. For example: + +```typescript +assetManager.releaseAsset(texture); +``` + +Since the resource management module was upgraded in v2.4, the release interface differs slightly from the previous version: + +1. The `assetManager.releaseAsset` interface can only release a single resource, and for the sake of uniformity, the interface can only release resources through the resource itself, not via attributes such as resource UUID, resource url, etc. + +2. When releasing a resource, you only need to focus on the resource itself and the engine will automatically release its dependent resources instead of fetching them manually via `getDependsRecursively`. + +> **Note**: the `release` series interfaces (such as `release`, `releaseAsset`, `releaseAll`) will release the resource directly without a release check, only resource's dependent resources will have a release check. So when the `release` series interfaces are called explicitly, you can be sure that the resource itself will always be released. + +## Reference Count Statistics + +Before v2.4, Creator chose to give the developer control over the release of all resources, both the resource itself and its dependencies, and the developer had to manually obtain all the dependencies of the resource and select the dependencies to be released. This way gave the developer the most control, and worked well for small projects. But as Creator grows, the size of the project grows, the resources referenced by the scene grows, and other scenes may reuse those resources, which causes increasing complexity in releasing resources and it is very difficult for the developer to master the usage of all resources. + +To address this pain point, the Asset Manager provides a set of resource release mechanism based on the reference counting, so that developers can release resources simply and efficiently, without worrying about rapid expansion of the project size.
+It should be noted that the Asset Manager only automatically counts static references between resources and does not truly reflect how the resources are dynamically referenced in the game, you need to control the dynamic references yourself to ensure that the resources are released correctly. The reasons are as follows: + +- JavaScript is a language with a garbage collection mechanism that manages its memory, so the engine has no way of knowing if a resource has been destroyed in the browser environment. +- JavaScript does not provide the assignment operator overloading, which the reference count statistics are highly dependent on. + +### Static Referencing of Resources + +When you edit the resources in the editor (such as the scene, prefab, material, etc.), you need to configure some other resources in the properties of those resources, such as setting the texture in the materials, setting the SpriteFrame on the Sprite component of the scene. Then these references will be recorded in the serialized data of the resources, which the engine can use to analyze the list of dependent resources, and a reference relationship like this is the static reference. + +The statistics for the static references of resources by the engine are as follows: + +1. When loading a resource using `assetManager` or Asset Bundle, the engine records all **direct dependent resources** for the resource in the underlying load pipe, adds 1 to the reference count of all **direct dependent resources**, and then initializes the reference count to 0 for the resource. + +2. When releasing a resource, obtain all the **direct dependent resources** information recorded previous for the resource, and subtract the reference count of all dependent resources by 1. + +This is because a resource can only be auto-released if its reference count is 0 in the release check. So the above steps ensure that the dependent resources of the resource cannot be released before the resource itself, because the reference count of the dependent resources is definitely not 0. That is, as long as a resource itself is not released, its dependent resources will not be released, thus ensuring that the release will not be done incorrectly when reusing the resource. For example: + +1. Suppose you now have a Prefab A that depends on both Material a and Material b. Material a references Texture α, and Material b references Texture β. After loading Prefab A, the reference count for both Material a and Material b are 1, and the reference count for both Texture α and Texture β are also 1. + + ![pica](release-manager/pica.png) + +2. Suppose you now add a Prefab B that depends on both Material b and Material c. After you load the Prefab B, the reference count to Material b is 2, because it is referenced by both the Prefab A and B. The reference count to Material c is 1, and the reference counts for Textures α and β are still 1. + + ![picb](release-manager/picb.png) + +3. When the Prefab A is released, the reference count for the Materials a and b each decreases by 1. + - The reference count of the Material a changes to 0 and is released; so the reference count of the Texture α minus 1 changes to 0 and is also released. + - The reference count of the Material b changes to 1 and is retained, so the reference count of the Texture β is still 1 and is also retained. + - Because the Prefab B is not released, the reference count for the Material c remains at 1 and is retained. + + ![picc](release-manager/picc.png) + +### Dynamic Referencing of Resources + +When you do not make any settings for a resource in the editor, but instead dynamically load the resource and set it to the component of the scene via code, the reference relationships of the resource are not recorded in the serialization data, and the engine cannot count this part of the reference relationships, which are dynamic references. + +If you are using dynamically loaded resources in your project for dynamic referencing, for example: + +```typescript +resources.load('images/background/spriteFrame', SpriteFrame, function (err, spriteFrame) { + self.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +At this point, the SpriteFrame resource is set to the Sprite component and the engine does not do anything special, the reference count of the SpriteFrame remains 0. If the dynamically loaded resources need to be referenced, held, or reused over time, it is recommended to use the `addRef` interface to manually increase the reference count. For example: + +```typescript +resources.load('images/background/spriteFrame', SpriteFrame, function (err, spriteFrame) { + self.getComponent(Sprite).spriteFrame = spriteFrame; + spriteFrame.addRef(); +}); +``` + +Increasing the reference count ensures that the resource will not be released early by mistake. Always remember to use `decRef` to remove the reference count and set the resource reference to `null` if you do not need to reference the resource and related components, or if the node is destroyed. For example: + +```typescript +this.spriteFrame.decRef(); +this.spriteFrame = null; +``` diff --git a/versions/4.0/en/asset/release-manager/auto-release.png b/versions/4.0/en/asset/release-manager/auto-release.png new file mode 100644 index 0000000000..23f6dddb3d Binary files /dev/null and b/versions/4.0/en/asset/release-manager/auto-release.png differ diff --git a/versions/4.0/en/asset/release-manager/pica.png b/versions/4.0/en/asset/release-manager/pica.png new file mode 100644 index 0000000000..b51f82075d Binary files /dev/null and b/versions/4.0/en/asset/release-manager/pica.png differ diff --git a/versions/4.0/en/asset/release-manager/picb.png b/versions/4.0/en/asset/release-manager/picb.png new file mode 100644 index 0000000000..dd0f43d691 Binary files /dev/null and b/versions/4.0/en/asset/release-manager/picb.png differ diff --git a/versions/4.0/en/asset/release-manager/picc.png b/versions/4.0/en/asset/release-manager/picc.png new file mode 100644 index 0000000000..063aacc9d4 Binary files /dev/null and b/versions/4.0/en/asset/release-manager/picc.png differ diff --git a/versions/4.0/en/asset/render-texture.md b/versions/4.0/en/asset/render-texture.md new file mode 100644 index 0000000000..8a56955b9b --- /dev/null +++ b/versions/4.0/en/asset/render-texture.md @@ -0,0 +1,147 @@ +# Render Texture + +A render texture is a texture on the GPU. Usually, we set it to the camera's **target texture**, so that the content illuminated by the camera is drawn to the texture via an off-screen `frambuffer`. This can typically be used to create car mirrors, dynamic shadows, etc. + +## Creating a RenderTexture + +Click the __+__ button in the top left of the __Assets__ panel and select __RenderTexture__ to create a Render Texture: + +![add-render-texture](render-texture/add-render-texture.png) + +The properties associated with the render texture asset can then be set in the __Inspector__ panel. + +![render-texture-property](render-texture/render-texture-property.png) + +| Property | Description | +| :--- | :--- | +| __Width__ | Set the width of the render texture. | +| __Height__ | Set the height of the render texture. | +| __Anisotropy__ | Anisotropy value. | +| __Min Filter__ | Narrowing filtering algorithm. | +| __Mag Filter__ | Amplification filtering algorithm. | +| __Mip Filter__ | Multi-level texture filtering algorithm. | +| __Wrap Mode S__ | S(U) direction texture addressing mode. | +| __Wrap Mode T__ | T(V) direction texture addressing mode. | + +## Use RenderTexture in editor + +In the camera component, assigning RenderTexture to the camera's __TargetTexture__ property will draw the result of the camera's illumination onto the RenderTexture. + +![camera](render-texture/camera.png) + +### Use RenderTexture in 2D / UI + +RenderTexture can be used like a normal texture. Take __Sprite Component__ as an example, drag and drop from __Assets__ panel onto the __SpriteFrame__ property on Sprite's __Inspector__ panel. + +![sprite rt](render-texture/sprite-rt.png) + +## Use RenderTexture in material + +To use RenderTexture in material includes the following two steps: + +1. Handle uv in __effect__ asset. Determine `SAMPLE_FROM_RT`, and call the `CC_HANDLE_RT_SAMPLE_FLIP` function: + + ``` + #if USE_TEXTURE + v_uv = a_texCoord * tilingOffset.xy + tilingOffset.zw; + #if SAMPLE_FROM_RT + CC_HANDLE_RT_SAMPLE_FLIP(v_uv); + #endif + #endif + ``` + +2. Select the corresponding material in the __Assets__ panel, then check `SAMPLE FROM RT` in the __Inspector__ panel: + + ![SAMPLE_FROM_RT](render-texture/SampleFormRT.png) + +## RenderTexture program guide + +There are two ways to use RenderTexture programmatically: + +- __Method 1__: Draw the contents illuminated by the 3D camera to the sprite frame of the UI. + + ```typescript + import { _decorator, Component, RenderTexture, SpriteFrame, Sprite, Camera } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('CaptureToWeb') + export class CaptureToWeb extends Component { + @property(Sprite) + sprite: Sprite = null; + @property(Camera) + camera: Camera = null; + + protected _renderTex: RenderTexture = null; + + start() { + const sp = new SpriteFrame(); + const renderTex = this._renderTex = new RenderTexture(); + renderTex.reset({ + width: 256, + height: 256, + }); + this.camera.targetTexture = renderTex; + sp.texture = renderTex; + this.sprite.spriteFrame = sp; + } + } + ``` + +- __Method 2__: Draw the contents illuminated by the 3D camera to the 3D model. + + ```typescript + import { _decorator, Component, MeshRenderer, RenderTexture, Camera, Material } from 'cc'; + const { ccclass, property, requireComponent } = _decorator; + + @ccclass("RenderCameraToModel") + @requireComponent(Camera) + export class RenderCameraToModel extends Component { + @property(MeshRenderer) + model: MeshRenderer = null; + + start() { + const renderTex = new RenderTexture(); + renderTex.reset({ + width: 256, + height: 256, + }); + const cameraComp = this.getComponent(Camera); + cameraComp.targetTexture = renderTex; + const pass = this.model.material.passes[0]; + // The purpose of setting the 'SAMPLE_FROM_RT' Macro to 'true' is to enable the RenderTexture to display correctly on all platforms + const defines = { SAMPLE_FROM_RT: true, ...pass.defines }; + const renderMat = new Material(); + renderMat.initialize({ + effectAsset: this.model.material.effectAsset, + defines, + }); + this.model.setMaterial(renderMat, 0); + renderMat.setProperty('mainTexture', renderTex, 0); + } + } + ``` +- __Method 3__:Read the content drawn by the 3D camera into the ArrayBuffer through the readPixels method + + ```typescript + import { _decorator, Component, RenderTexture } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass("RenderReadPixels") + export class RenderReadPixels extends Component { + @property(RenderTexture) + public renderTexture: RenderTexture; + start() { + const width = this.renderTexture.width; + const height = this.renderTexture.height; + const texPixels = new Uint8Array(width * height * 4); + this.renderTexture.readPixels(0, 0, + this.renderTexture.width, this.renderTexture.height, + texPixels); + } + } + ``` + Make sure that the shader has a `mainTexture` property and is enabled in the material if you want to display the drawing results correctly. For example, if using the `builtin-standard` shader, make sure the __USE ALBEDO MAP__ option is checked: + + ![use albedo](render-texture/use-albedo.png) + +For more information about the usage, please refer to the example **RenderTexture** ([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/rendertexture)). diff --git a/versions/4.0/en/asset/render-texture/SampleFormRT.png b/versions/4.0/en/asset/render-texture/SampleFormRT.png new file mode 100644 index 0000000000..82da886d34 Binary files /dev/null and b/versions/4.0/en/asset/render-texture/SampleFormRT.png differ diff --git a/versions/4.0/en/asset/render-texture/add-render-texture.png b/versions/4.0/en/asset/render-texture/add-render-texture.png new file mode 100644 index 0000000000..904f5cf6f3 Binary files /dev/null and b/versions/4.0/en/asset/render-texture/add-render-texture.png differ diff --git a/versions/4.0/en/asset/render-texture/camera.png b/versions/4.0/en/asset/render-texture/camera.png new file mode 100644 index 0000000000..71368ff093 Binary files /dev/null and b/versions/4.0/en/asset/render-texture/camera.png differ diff --git a/versions/4.0/en/asset/render-texture/render-texture-property.png b/versions/4.0/en/asset/render-texture/render-texture-property.png new file mode 100644 index 0000000000..6c61f52114 Binary files /dev/null and b/versions/4.0/en/asset/render-texture/render-texture-property.png differ diff --git a/versions/4.0/en/asset/render-texture/sprite-rt.png b/versions/4.0/en/asset/render-texture/sprite-rt.png new file mode 100644 index 0000000000..a4c23ed7b9 Binary files /dev/null and b/versions/4.0/en/asset/render-texture/sprite-rt.png differ diff --git a/versions/4.0/en/asset/render-texture/use-albedo.png b/versions/4.0/en/asset/render-texture/use-albedo.png new file mode 100644 index 0000000000..553c3883d6 Binary files /dev/null and b/versions/4.0/en/asset/render-texture/use-albedo.png differ diff --git a/versions/4.0/en/asset/scene.md b/versions/4.0/en/asset/scene.md new file mode 100644 index 0000000000..0e662bfcfa --- /dev/null +++ b/versions/4.0/en/asset/scene.md @@ -0,0 +1,73 @@ +# Scene Assets + +In **Cocos Creator**, the **Scene** is the cornerstone for organizing game content during development, and presenting all game content to the players. The **Scene** itself is a file, also considered a game asset, and saves most of the game's information. + +> **Note**: please avoid multiple people modifying the same scene asset at the same time, as this may lead to conflicts that cannot be resolved by `git` merging. + +## Creating a Scene + +There are **three** ways to create a **Scene**: + +1. Select the folder to create the **Scene** file in the **Assets** panel. On the folder right-click and select **Create -> Scene**, and then type the desired **Scene** name. + + ![new_scene_1](scene/new_scene_1.png) + + In order to have a good directory structure in the project, it is strongly recommended that use this method to create a **Scene**. + +2. Click the **Create (+)** button in the upper left corner of the **Assets** panel, and then select **Scene**. + + ![new_scene_2](scene/new_scene_2.png) + +3. Select **File -> New Scene** in the top menu bar, a new scene will be created directly in the **Scene** panel. But the new scene file will not appear in the **Assets** panel, it is required to save the scene file manually in the **Save Scene** window that pops up when saving the scene, and the `scene.scene` file will appear in the root directory of the **Assets** panel only after the saving is done. + + ![new_scene_3](scene/new_scene_3.png) + +## Saving a Scene + +Method 1: Use the shortcut keys Ctrl + S (Windows) or Command + S (MacOS) to quickly save the scene. + +Method 2: Select **File -> Save Scene** in the top menu bar. + +## Switching Scenes + +In the **Assets** panel, **double-click** the **Scene** to be opened.
+When needing to switch **Scenes** in the game, use the [director.loadScene()](%__APIDOC__%/en/class/Director?id=loadScene) API to implement dynamic scene loading and switching in the game. + +## Scene Properties + +After double-clicking to open the **scene** file, the `scene` is the root node of the scene node tree in the **Hierarchy** panel. Select the `scene` node, and set whether the scene is automatically released or not in the **Inspector** panel, as well as the whole scene related properties, including **ambient light** settings, **shadows** settings, **global fog** and **sky box** settings. + +![scene_node](scene/scene_node.png) + +Automatic release of scene assets, please refer to section **Change the policy of auto releasing assets from previous scene** below for details. + +For a detailed description of each property, see the following documentations: + +- [Ambient light](../concepts/scene/light/lightType/ambient.md) +- [Shadow](../concepts/scene/light/shadow.md) +- [Global Fog](../concepts/scene/fog.md) +- [Skybox](../concepts/scene/skybox.md) +- [Native Scene Culling](../advanced-topics/native-scene-culling.md) + +## Change the Policy of Auto Releasing Assets from Previous Scene + +In a large game where there are many scenes, as the engine continues to load different scenes, the memory usage will continue to increase. Besides using API such as `assetManager.releaseAsset` to accurately release unused assets, one can also use scene's auto releasing feature. + +After double-clicking to open the scene file, select the `scene` node in the **Hierarchy** panel to set whether the scene is automatically released in the **Inspector** panel: + +![scene node set](scene/scene_node_set.png) + +When switching from current scene to the next scene, if current scene disabled the auto releasing, then all assets (directly or indirectly) referenced by current scene (except loaded dynamically in scripts) will not release by default. On the other hand, if enable the auto releasing, then these assets will release by default. + +### Prevent auto releasing for some specified assets + +With the auto releasing enabled for a scene, if some of the scene's assets are saved in the script as "special reference", these references will become invalid once the scene switched, since the asset has been released, may cause rendering problems. To prevent these assets from being released automatically, we can use [Asset.addRef](%__APIDOC__%/en/class/Asset?id=addRef) to retain them. + +> "Special reference" refer to references in the form of global variables, singleton, closures, "dynamic assets", and so on.
+> "Dynamic assets" refer to assets that are dynamically created or dynamically modified in a script. + +The above content of the automatic release of scene assets can be summed up in the following diagram of several cases: + +![release assets](scene/release-assets.png) + +For additional information on asset release, please refer to the [Release of Assets](./release-manager.md#auto-release) documentation. diff --git a/versions/4.0/en/asset/scene/enable-extension.png b/versions/4.0/en/asset/scene/enable-extension.png new file mode 100644 index 0000000000..ae6450541f Binary files /dev/null and b/versions/4.0/en/asset/scene/enable-extension.png differ diff --git a/versions/4.0/en/asset/scene/new_scene_1.png b/versions/4.0/en/asset/scene/new_scene_1.png new file mode 100644 index 0000000000..e601708be7 Binary files /dev/null and b/versions/4.0/en/asset/scene/new_scene_1.png differ diff --git a/versions/4.0/en/asset/scene/new_scene_2.png b/versions/4.0/en/asset/scene/new_scene_2.png new file mode 100644 index 0000000000..64be724193 Binary files /dev/null and b/versions/4.0/en/asset/scene/new_scene_2.png differ diff --git a/versions/4.0/en/asset/scene/new_scene_3.png b/versions/4.0/en/asset/scene/new_scene_3.png new file mode 100644 index 0000000000..565c953d4b Binary files /dev/null and b/versions/4.0/en/asset/scene/new_scene_3.png differ diff --git a/versions/4.0/en/asset/scene/release-assets.png b/versions/4.0/en/asset/scene/release-assets.png new file mode 100644 index 0000000000..c9b59857b7 Binary files /dev/null and b/versions/4.0/en/asset/scene/release-assets.png differ diff --git a/versions/4.0/en/asset/scene/scene_node.png b/versions/4.0/en/asset/scene/scene_node.png new file mode 100644 index 0000000000..fd090ce009 Binary files /dev/null and b/versions/4.0/en/asset/scene/scene_node.png differ diff --git a/versions/4.0/en/asset/scene/scene_node_set.png b/versions/4.0/en/asset/scene/scene_node_set.png new file mode 100644 index 0000000000..af7dbe2d37 Binary files /dev/null and b/versions/4.0/en/asset/scene/scene_node_set.png differ diff --git a/versions/4.0/en/asset/script.md b/versions/4.0/en/asset/script.md new file mode 100644 index 0000000000..00ab1ddc9e --- /dev/null +++ b/versions/4.0/en/asset/script.md @@ -0,0 +1,7 @@ +# Script Assets + +In __Cocos Creator__, scripts are also part of the __Asset__. + +For a detailed introduction to __scripting__, please refer to the [Scripting Guide](../scripting/index.md) documentation. + +For details on the creation and use of __script assets__, please refer to the [Script Creation](../scripting/setup.md) documentation. diff --git a/versions/4.0/en/asset/spine.md b/versions/4.0/en/asset/spine.md new file mode 100644 index 0000000000..3fc543583c --- /dev/null +++ b/versions/4.0/en/asset/spine.md @@ -0,0 +1,82 @@ +# Skeletal Animation Assets (Spine) + +Skeletal animation assets in Creator are exported from [Spine](http://esotericsoftware.com/), which currently supports the [JSON](http://esotericsoftware.com/spine-export/#JSON) and [binary](http://esotericsoftware.com/spine-export/#%E4%BA%8C%E8%BF%9B%E5%88%B6) data formats. + +The supported Spine versions for each Creator version are as follows: + +| Creator Version | Spine Version | +| :---------- | :-------- | +| v3.0 and above | v3.8 (v3.8.75 is not supported on the native platform) | +| v2.3 and above | v3.8 | +| v2.2 | v3.7 | +| v2.0.8 to v2.1 | v3.6 | +| v2.0.7 and below | v2.5 | + +## Import Skeletal Animation Assets + +The assets required for skeletal animation are: + +- `.json/.skel` skeletal data +- `.png` atlas textures +- `.txt/.atlas` atlas data + + ![spine](spine/import.png) + +## Spine Preview + +In version 3.8.5, we have added preview support to the inspector panel of Spine. After users select a certain Spine file, they can see the corresponding rendering content in the panel, which can greatly improve the efficiency of resource inspection. + +![preview](spine/spine-preview.png) + +## Create Skeletal Animation + +Drag the skeletal animation asset from the **Assets** panel to the `SkeletonData` property of the spine component in the **Inspector** panel. + +![spine](spine/set_skeleton.png) + +## How to load Spine remotely from a server + +### Load the Spine assets in json format + +```ts +let comp = this.getComponent('sp.Skeleton') as sp.Skeleton; + +let image = "http://localhost/download/spineres/1/1.png"; +let ske = "http://localhost/download/spineres/1/1.json"; +let atlas = "http://localhost/download/spineres/1/1.atlas"; +assetManager.loadAny([{ url: atlas, ext: '.txt' }, { url: ske, ext: '.txt' }], (error, assets) => { + assetManager.loadRemote(image, (error, img: ImageAsset) => { + let texture = new Texture2D(); + texture.image = img; + asset.skeletonJson = assets[1]; + asset.atlasText = assets[0]; + asset.textures = [texture]; + asset.textureNames = ['1.png']; + asset._uuid = ske; // Any string can be passed in, but it cannot be empty. + skeleton.skeletonData = asset; + }); +}); +``` + +### Load the Spine assets in binary format + +```ts +let comp = this.getComponent('sp.Skeleton') as sp.Skeleton; + +let image = "http://localhost/download/spineres/1/1.png"; +let ske = "http://localhost/download/spineres/1/1.skel"; +let atlas = "http://localhost/download/spineres/1/1.atlas"; +assetManager.loadAny([{ url: atlas, ext: '.txt' }, { url: ske, ext: '.bin' }], (error, assets) => { + assetManager.loadRemote(image, (error, texture: Texture2D) => { + let asset = new sp.SkeletonData(); + asset._nativeAsset = assets[1]; + asset.atlasText = assets[0]; + asset.textures = [texture]; + asset.textureNames = ['1.png']; + asset._uuid = ske; // Any string can be passed in, but it cannot be empty. + asset._nativeURL = ske; // Pass in a binary path to be used as the 'filePath' parameter when using 'initSkeleton'. + comp.skeletonData = asset; + let ani = comp.setAnimation(0, 'walk', true); + }); +}); +``` diff --git a/versions/4.0/en/asset/spine/import.png b/versions/4.0/en/asset/spine/import.png new file mode 100644 index 0000000000..a1f2340347 Binary files /dev/null and b/versions/4.0/en/asset/spine/import.png differ diff --git a/versions/4.0/en/asset/spine/set_skeleton.png b/versions/4.0/en/asset/spine/set_skeleton.png new file mode 100644 index 0000000000..61e9dfbc63 Binary files /dev/null and b/versions/4.0/en/asset/spine/set_skeleton.png differ diff --git a/versions/4.0/en/asset/spine/spine-preview.png b/versions/4.0/en/asset/spine/spine-preview.png new file mode 100644 index 0000000000..fe8a1c69bc Binary files /dev/null and b/versions/4.0/en/asset/spine/spine-preview.png differ diff --git a/versions/4.0/en/asset/sprite-frame.md b/versions/4.0/en/asset/sprite-frame.md new file mode 100644 index 0000000000..1828ed7df8 --- /dev/null +++ b/versions/4.0/en/asset/sprite-frame.md @@ -0,0 +1,110 @@ +# Sprite Frame Assets + +**Sprite Frame** is a container for UI rendering and basic graphics, which manages the clipping and tiling data. It by default holds a reference to a Texture2D asset of the same level as it. + +## Importing Sprite Frame Assets + +Use the default [asset import](asset-workflow.md) method to import image assets into the project, then set the type of image as **sprite-frame** in the **Inspector**, and save it by clicking the green checkmark button in the upper right corner. + +![set sprite-frame](sprite-frame/set-spriteframe.png) + +Creator will automatically create a **spriteFrame** asset under the imported image assets as shown below. + +![spriteframe](sprite-frame/spriteframe.png) + +An image asset will have a thumbnail of its own image as icon in **Assets**. When an image sub-asset is selected in **Assets**, a thumbnail of the image is displayed below the **Inspector**. + +## Property + +The `spriteFrame` has the following properties: + +| Property | Description | +| :--- | :--- | +| Packable | Whether to participate in Dynamic Atlas and automatic atlas building processes. Please refer to the **Packable** section below for details. | +| Rotated | Read-only property, cannot be changed. Used to see if the sub-asset in the Texture Packer asset is rotated. | +| Offset X, Y | Read-only property, cannot be changed. Used to view the offset of the rectangle in Texture Packer asset. | +| Trim Type | Set the trim type, including:
1. Auto -- Automatic trim. For details, please refer to the [Auto Trim for SpriteFrame](../ui-system/components/engine/trim.md) documentation.
2. Custom -- Custom trim
3. None -- No trim, use original texture. | +| Trim Threshold | Set the transparency threshold, trim off the pixels whose transparency is below the set value.
The default value is 1, and the range of values is 0~1.
Only takes effect when **Trim Type** is set to **Auto**. | +| Trim X, Y, Width, Height | Sets the trim rect, only takes effect when **Trim Type** is set to **Custom**. | +| Border Top, Bottom, Left, Right | Set the texture margins of the 9-sliced, which can be edited visually by clicking on the **Edit** button below. | + +### Packable + +If the engine has [Dynamic-Atlas](../advanced-topics/dynamic-atlas.md) enabled, Dynamic-Atlas will automatically merge the appropriate textures into one large texture at the start of the scene to reduce Drawcall. If the UV coordinates of the texture are used in the custom `effect`, the UV calculation in the `effect` will be wrong and the **Packable** property of the texture needs to be set to **false** to prevent the texture from being packed into the Dynamic-Atlas. + +## Using a Sprite Frame + +### The object contained in the container is using textures + +In the editor, drag the SpriteFrame assets into the **SpriteFrame** property of the [Sprite Component](../ui-system/components/editor/sprite.md) to switch the image displayed by Sprite. + +![use spriteframe](sprite-frame/use-spriteframe.png) + +At runtime, taking the imported image named **content** above as an example, the entire asset is divided into three parts: + +- **content**:Image source assets ImageAsset +- Sub-asset **spriteFrame** of **content**, the sprite frame asset SpriteFrame +- Sub-asset **texture** of **content**, the mapping asset Texture2D + +When assets are stored in the `resources` directory, we can load directly to the spriteFrame asset with the following code example. + +```typescript +const url = 'test_assets/test_atlas/content/spriteFrame'; +resources.load(url, SpriteFrame, (err: any, spriteFrame) => { + const sprite = this.getComponent(Sprite); + sprite.spriteFrame = spriteFrame; +}); +``` + +In some cases only the source ImageAsset can be loaded, so we provide the [createWithImage](%__APIDOC__%/en/class/SpriteFrame?id=createWithImage) method to help users create a SpriteFrame asset from the loaded ImageAsset. Depending on the source of the ImageAsset, there are two ways to create it: + +1. Assets on the server can only be loaded to the source ImageAsset. For specific methods, please refer to the [dynamic load asset](./dynamic-load-resources.md) documentation. The code example for creating a SpriteFrame asset is as follows: + + ```typescript + const self = this; + const url = 'test_assets/test_atlas/content'; + resources.load(url, ImageAsset, (err: any, imageAsset) => { + const sprite = this.getComponent(Sprite); + sprite.spriteFrame = SpriteFrame.createWithImage(imageAsset); + }); + ``` + + Users can also fill in the information manually. Example: + + ```typescript + const self = this; + const url = 'test_assets/test_atlas/content'; + resources.load(url, ImageAsset, (err: any, imageAsset) => { + const sprite = this.getComponent(Sprite); + const spriteFrame = new SpriteFrame(); + const tex = new Texture2D(); + tex.image = imageAsset; + spriteFrame.texture = tex; + sprite.spriteFrame = spriteFrame; + }); + ``` + +2. Create from an ImageAsset drawn by Canvas. Example: + + ```typescript + const sprite = this.getComponent(Sprite); + sprite.spriteFrame = SpriteFrame.createWithImage(canvas); + ``` + + Users can also fill in the information manually. Example: + + ```typescript + const sprite = this.getComponent(Sprite); + const img = new ImageAsset(canvas); + const tex = new Texture2D(); + tex.image = img; + const sp = new SpriteFrame(); + sp.texture = tex; + sprite.spriteFrame = sp; + ``` + +### The container contains objects that are used by RenderTexture + +RenderTexture is a rendering texture that renders content from the camera directly to a texture instead of the screen. SpriteFrame can easily display 3D camera content on the UI by managing RenderTexture. For specific usage and code example, please refer to [Render Texture](render-texture.md). + +For API information, please refer to the [SpriteFrame](%__APIDOC__%/en/class/SpriteFrame) documentation. diff --git a/versions/4.0/en/asset/sprite-frame/set-spriteframe.png b/versions/4.0/en/asset/sprite-frame/set-spriteframe.png new file mode 100644 index 0000000000..7b0e3924e1 Binary files /dev/null and b/versions/4.0/en/asset/sprite-frame/set-spriteframe.png differ diff --git a/versions/4.0/en/asset/sprite-frame/spriteframe.png b/versions/4.0/en/asset/sprite-frame/spriteframe.png new file mode 100644 index 0000000000..f7abdeee29 Binary files /dev/null and b/versions/4.0/en/asset/sprite-frame/spriteframe.png differ diff --git a/versions/4.0/en/asset/sprite-frame/use-spriteframe.png b/versions/4.0/en/asset/sprite-frame/use-spriteframe.png new file mode 100644 index 0000000000..0bb011a738 Binary files /dev/null and b/versions/4.0/en/asset/sprite-frame/use-spriteframe.png differ diff --git a/versions/4.0/en/asset/subpackage-upgrade-guide.md b/versions/4.0/en/asset/subpackage-upgrade-guide.md new file mode 100644 index 0000000000..a656a41623 --- /dev/null +++ b/versions/4.0/en/asset/subpackage-upgrade-guide.md @@ -0,0 +1,41 @@ +# Subpackage upgrade guide + +> Author: Santy-Wang, Xunyi +> This article details the considerations for upgrading mini game sub-package to the Asset Bundle. + +Prior to v2.4, [Subpackage Loading](https://github.com/cocos/cocos-docs/blob/e02ac31bab12d3ee767c0549050b0e42bd22bc5b/en/scripting/subpackage.md) was only supported various mini game platforms, such as WeChat Mini Games, OPPO Mini Games, etc.. However, with the continuous development of Creator, developers' demands for subpackage have been increasing, such as multi-platform support, and the original subpackage loading is no longer enough. Therefore, starting from v2.4, Creator officially supports the more complete **Asset Bundle**. + +- For the **Artist and Game Designer**, all resources in your project (e.g.: scenes, animations, prefab) do not need to be modified or upgraded. +- For **Programmers**, the `loader.downloader.loadSubpackage` needs to be changed to the `assetManager.loadBundle` from **Asset Manager**. The related content will be described in detail in this document. + +> **Note**: if you used Subpackage Loading in your old project, that is, if you checked the **Subpackage** option in the **Properties** panel, then when the project is upgraded to the v3.0, it will automatically convert to an normal folder. You can configure the Asset Bundle as following reference. + +[Configura Asset Bundle](bundle.md) + +## Situations that require upgrading manually + +Use the `loader.downloader.loadSubpackage` API to load the subpackage. + +## Upgrade steps + +- **Back up your old projects** +- Use Cocos Creator **v3.0** in the **Dashboard** to open an old project that needs to upgrade the subpackage, Creator will reimport the affected resources. The first import will take a little longer, and the main editor window will open after the import is complete. And then open the code editor to replace all `loader.downloader.loadSubpackage` with `assetManager.loadBundle`. + + ```typescript + // before + loader.downloader.loadSubpackage('sub1', (err) => { + loader.loadRes('sub1/sprite-frames/background', SpriteFrame); + }); + + // after + assetManager.loadBundle('sub1', (err, bundle) => { + // The relative path to the Asset Bundle root + bundle.load('sprite-frames/background/spriteFrame', SpriteFrame); + }); + ``` + +## How to use the Asset Bundle + +For details on how to use the **Asset Bundle**, please refer to the [Asset Bundle](bundle.md) documentation. + +For APIs related to the **Asset Bundle**, please refer to the [Asset Bundle API](%__APIDOC__%/en/class/AssetManager.Bundle) documentation. diff --git a/versions/4.0/en/asset/subpackage/asset-bundle.png b/versions/4.0/en/asset/subpackage/asset-bundle.png new file mode 100644 index 0000000000..965b850792 Binary files /dev/null and b/versions/4.0/en/asset/subpackage/asset-bundle.png differ diff --git a/versions/4.0/en/asset/subpackage/bundle1.png b/versions/4.0/en/asset/subpackage/bundle1.png new file mode 100644 index 0000000000..11f78be994 Binary files /dev/null and b/versions/4.0/en/asset/subpackage/bundle1.png differ diff --git a/versions/4.0/en/asset/subpackage/bundle_md5.png b/versions/4.0/en/asset/subpackage/bundle_md5.png new file mode 100644 index 0000000000..0683a785a2 Binary files /dev/null and b/versions/4.0/en/asset/subpackage/bundle_md5.png differ diff --git a/versions/4.0/en/asset/subpackage/inspector.png b/versions/4.0/en/asset/subpackage/inspector.png new file mode 100644 index 0000000000..29410362f2 Binary files /dev/null and b/versions/4.0/en/asset/subpackage/inspector.png differ diff --git a/versions/4.0/en/asset/subpackage/package.jpg b/versions/4.0/en/asset/subpackage/package.jpg new file mode 100644 index 0000000000..3536c73f68 Binary files /dev/null and b/versions/4.0/en/asset/subpackage/package.jpg differ diff --git a/versions/4.0/en/asset/subpackage/profile.png b/versions/4.0/en/asset/subpackage/profile.png new file mode 100644 index 0000000000..57818c878c Binary files /dev/null and b/versions/4.0/en/asset/subpackage/profile.png differ diff --git a/versions/4.0/en/asset/subpackage/subpackage.jpg b/versions/4.0/en/asset/subpackage/subpackage.jpg new file mode 100644 index 0000000000..2159a988f1 Binary files /dev/null and b/versions/4.0/en/asset/subpackage/subpackage.jpg differ diff --git a/versions/4.0/en/asset/subpackage/subpackage2.png b/versions/4.0/en/asset/subpackage/subpackage2.png new file mode 100644 index 0000000000..aecb36c8e3 Binary files /dev/null and b/versions/4.0/en/asset/subpackage/subpackage2.png differ diff --git a/versions/4.0/en/asset/texture-cube.md b/versions/4.0/en/asset/texture-cube.md new file mode 100644 index 0000000000..418e5787e9 --- /dev/null +++ b/versions/4.0/en/asset/texture-cube.md @@ -0,0 +1,21 @@ +# Texture Cube + +TextureCube is a cube texture, often used to set the scene's [skybox](../concepts/scene/skybox.md). The cube texture can be obtained by setting the panorama ImageAsset to the TextureCube type, or it can be generated in the Creator. + +## Set to Cube Mapping + +After [importing](asset-workflow.md#) the ImageAsset into Creator, you can set it to **texture cube** type in the **Inspector** panel, and click the green checkbox in the upper right corner to save the changes. + +![set-texture-cube](texture/set-texture-cube.png) + +Once set up, you can see in the **Assets** panel that a **textureCube** subresource has been created below the original image resource, along with the six textures that make up the TextureCube. + +![texture-cube](texture/texture-cube.png) + +## Create CubeMap + +The TextureCube obtained by making a CubeMap in Creator is as follows: + +![CubeMap](../concepts/scene/skybox/cubemap-properties.png) + +For the specific use of TextureCube and how to make CubeMap, please refer to [Skybox - Setting up CubeMap](../concepts/scene/skybox.md). diff --git a/versions/4.0/en/asset/texture.md b/versions/4.0/en/asset/texture.md new file mode 100644 index 0000000000..aac942152a --- /dev/null +++ b/versions/4.0/en/asset/texture.md @@ -0,0 +1,127 @@ +# Texture Assets + +**Texture** assets are assets used for procedural sampling, such as **textures on models** and the **UI on Sprites**. When the UI or model are rendered, the corresponding texture is sampled, then filled on the model grid, plus a series of processing such as lighting to render the entire scene. + +**Texture assets** can be generated from **ImageAsset**. Some common image formats, including `.png`, `.jpeg`, etc. can be used in **ImageAsset**. + +## Texture2D + +Texture2D is a type of texture mapping asset, usually used for rendering 3D models, such as reflection mapping in model materials, ambient light mask mapping, etc. + +After you [import](asset-workflow.md) the image asset into Creator, you can set it to **texture** type in **Inspector** panel, the texture type is the Texture2D asset. + +![Texture2D](texture/set-texture.png) + +## Properties + +When importing an image asset, the editor sets it to **texture** type by default, and one or more sub-assets will be created automatically under the imported image asset, click the triangle icon to the left of the image asset in **Asset Manager** to expand it to see all the sub-assets, as shown in the following figure: + +![sub-texture](texture/sub-texture.png) + +When the generated Texture2D sub-asset is selected, the relevant properties can be set in the **Inspector** panel. + +![Texture2D subresource](texture/sub-texture-pro.png) + +| Properties | Description | +| :----------------- |:--- | +| **Anisotropy** | Anisotropy value, applying the maximum threshold of the anisotropy filtering algorithm | +| **Filter Mode** | Filter mode, options are **Nearest (None)**, **Bilinear**, **Bilinear with mipmaps**, **Trilinear with mipmaps** and **Advanced**, please refer to **Filter mode** below for more information | +| **Wrap Mode** | Set the addressing mode, the options are **Repeat**, **Clamp**, **Mirror**, and **Advanced**
When selecting **Advanced**, set the texture addressing mode in the S(U)/T(V) direction, that is, the pixel-to-texture mapping mode in the S(U) or T(V) direction, see **Addressing Mode** below for more information | + +> **Note**: Since the default Wrap Mode may show black edges when rendering transparent edges of images, the Creator automatically adjusts the Wrap Mode S and Wrap Mode T properties of texture asset to **clamp-to-edge** automatically when setting the image asset type to **sprite-frame**. Developers can change this if they have special needs. + +Next we give a brief explanation of some of the properties. + +### Filter Mode + +![filter mode](./texture/filter-mode.png) + +When the original size of Texture2D does not match the size of the texture image mapped to the screen, different texture filtering methods for texture cell-to-pixel mapping will produce different results. + +The **Min Filter** and **Mag Filter** properties in Texture2D are used to set the texture filtering method to be used when scaling down or scaling up the mapping, respectively. + +1. Neareast + + Neareast filtering is the **default** texture filtering method used. The color value of the texture cell whose center position is closest to the sample point is used as the color value of that sample point, without considering the influence of other adjacent pixels.
+ It should be noted that the use of proximity filtering may result in uneven edges and more pronounced jaggedness. + +2. Linear + + Linear filtering uses a 2 x 2 matrix of texture cells nearest to the sampling point for sampling. The average of the four texture cell color values is taken as the color of the sampling point, and the color value transition between pixels will be smoother.
+ Note that using the linear filtering method may result in black edges and, in the case of pixel-based games, may result in blurring. + +### Generate Mipmaps + +In order to speed up 3D scene rendering and reduce image jaggies, the mapping is processed as a sequence of pre-computed and optimized images, called a mipmap. mipmap is a scaled-down detailed replica of the original image at each level of the mipmap, and when the mapping is scaled down or only needs to be viewed from a distance, the mipmap is converted to the appropriate level. + +![generate-mipmaps](./texture/generate-mipmaps.png) + +When the **Generate Mipmaps** property is checked or **Filter Mode** for texture is selected as **Bilinear with mipmaps** or **Trilinear with mipmaps**, the mipmap is automatically generated by interpolating between two similar layers. mipmap mapping is smaller than the original image when rendering distant objects, which improves the cache hit rate during graphics card sampling, so the rendering speed is improved. At the same time, the mipmap mapping is less precise, which reduces moiré and reduces jaggedness on the screen. In addition, mipmap takes up about a third of the memory space because of the extra mipmap generation. + +Texture2D can dynamically select the range of the mipmap at runtime. After setting the mipmap range, only the mipmaps within the range can be used. This allows us to save bandwidth by skipping low levels and avoid using too many high levels to reduce the effect. + +The mipmap level range for Texture2D can be set as follows. + +```Javascript +texture.setMipRange(minLevel, maxLevel); +``` + +Where `minLevel` specifies the minimum limit and `maxLevel` specifies the maximum limit. + +> **Note**. +> 1. The limit cannot exceed the existing mipmap level. +> This method is not available for WebGL and GLES2 backends. + +### Wrap Mode + +In general, the texture coordinates in the horizontal (U) and vertical (V) axes are in the range `[0, 1]`. When the texture coordinates in the passed vertex data are out of the range `[0, 1]`, different addressing modes can be used to control how the out-of-range texture coordinates are mapped. + +The **Wrap Mode S** and **Wrap Mode T** properties in Texture2D are used to set the addressing mode of the texture in the UV direction, respectively. + +1. Repeat + + Repeat addressing mode is the default texture addressing mode used. For texture coordinates outside the `[0, 1]` range, the contents of the texture coordinates inside `[0, 1]` are repeated, i.e. the texture map is repeatedly placed outside the texture coordinate range. + + > **Note**: On WebGL 1.0, the `repeat` addressing mode is disabled when the texture width and height are not powers of two. The runtime will automatically switch it to `clamp-to-edge` addressing mode, which will disable properties like `tilingOffset` of the material. + + When the **Wrap Mode S** and **Wrap Mode T** properties of Texture2D are both set to `repeat`, the effect looks like this. + + ![repeat](texture/repeat.png) + +2. Clamp-to-edge + + Constrain the texture coordinates to be between 0 and 1, copying the texture coordinates of `[0, 1]` only once. For the part outside of `[0, 1]`, the texture coordinate content of the edge will be used to extend, producing an effect similar to the edge being stretched. + + When both properties are set to `clamp-to-edge`, the effect looks like this. + + ![clamp-to-edge](texture/clamp-to-edge.png) + +3. Mirrored-repeat + + Similar to repeat-addressing mode, except that the mapping is mirrored and repeated. + + When both properties are set to `mirrored-repeat`, the effect looks like this. + + ![mirrored-repeat](texture/mirrored-repeat.png) + +## Use Texture2D + +Texture2D is a very widely used asset, and its use in Creator consists of using it in the editor and getting it dynamically. + +- In the editor's **Inspector** panel, any property marked as Texture2D type can be used by dragging and dropping a Texture2D asset into the property box. For example, to set a Texture2D type asset for a material asset. + + ![mirrored-repeat](texture/use-texture2d.png) + + > **Note**: If `USE TEXTURE` is not defined in the material, there is no such property. + +- To use it dynamically, you need to get the ImageAsset first, and then instantiate the Texture2D asset based on the ImageAsset you got. + + ```ts + resources.load("testAssets/image/texture", Texture2D, (err: any, texture: Texture2D) => { + const spriteFrame = new SpriteFrame(); + spriteFrame.texture = texture; + this.node.getComponent(Sprite).spriteFrame = spriteFrame; + }); + ``` + + For more details, please refer to [Asset Loading](./dynamic-load-resources.md)。 diff --git a/versions/4.0/en/asset/texture/clamp-to-edge.png b/versions/4.0/en/asset/texture/clamp-to-edge.png new file mode 100644 index 0000000000..4d27cd9d6b Binary files /dev/null and b/versions/4.0/en/asset/texture/clamp-to-edge.png differ diff --git a/versions/4.0/en/asset/texture/filter-mode.png b/versions/4.0/en/asset/texture/filter-mode.png new file mode 100644 index 0000000000..3486273560 Binary files /dev/null and b/versions/4.0/en/asset/texture/filter-mode.png differ diff --git a/versions/4.0/en/asset/texture/generate-mipmaps.png b/versions/4.0/en/asset/texture/generate-mipmaps.png new file mode 100644 index 0000000000..e75acbcc24 Binary files /dev/null and b/versions/4.0/en/asset/texture/generate-mipmaps.png differ diff --git a/versions/4.0/en/asset/texture/imported.png b/versions/4.0/en/asset/texture/imported.png new file mode 100644 index 0000000000..d319b4992e Binary files /dev/null and b/versions/4.0/en/asset/texture/imported.png differ diff --git a/versions/4.0/en/asset/texture/mirrored-repeat.png b/versions/4.0/en/asset/texture/mirrored-repeat.png new file mode 100644 index 0000000000..1e07ceac2a Binary files /dev/null and b/versions/4.0/en/asset/texture/mirrored-repeat.png differ diff --git a/versions/4.0/en/asset/texture/normal-map.png b/versions/4.0/en/asset/texture/normal-map.png new file mode 100644 index 0000000000..2d28fc5c03 Binary files /dev/null and b/versions/4.0/en/asset/texture/normal-map.png differ diff --git a/versions/4.0/en/asset/texture/repeat.png b/versions/4.0/en/asset/texture/repeat.png new file mode 100644 index 0000000000..203af666f4 Binary files /dev/null and b/versions/4.0/en/asset/texture/repeat.png differ diff --git a/versions/4.0/en/asset/texture/set-texture-cube.png b/versions/4.0/en/asset/texture/set-texture-cube.png new file mode 100644 index 0000000000..cbd7e68270 Binary files /dev/null and b/versions/4.0/en/asset/texture/set-texture-cube.png differ diff --git a/versions/4.0/en/asset/texture/set-texture.png b/versions/4.0/en/asset/texture/set-texture.png new file mode 100644 index 0000000000..b26e343336 Binary files /dev/null and b/versions/4.0/en/asset/texture/set-texture.png differ diff --git a/versions/4.0/en/asset/texture/sub-texture-pro.png b/versions/4.0/en/asset/texture/sub-texture-pro.png new file mode 100644 index 0000000000..a78a4b9437 Binary files /dev/null and b/versions/4.0/en/asset/texture/sub-texture-pro.png differ diff --git a/versions/4.0/en/asset/texture/sub-texture.png b/versions/4.0/en/asset/texture/sub-texture.png new file mode 100644 index 0000000000..1dc1b8b828 Binary files /dev/null and b/versions/4.0/en/asset/texture/sub-texture.png differ diff --git a/versions/4.0/en/asset/texture/texture-cube.png b/versions/4.0/en/asset/texture/texture-cube.png new file mode 100644 index 0000000000..f9fb7100c0 Binary files /dev/null and b/versions/4.0/en/asset/texture/texture-cube.png differ diff --git a/versions/4.0/en/asset/texture/type-change.png b/versions/4.0/en/asset/texture/type-change.png new file mode 100644 index 0000000000..3107a3a43d Binary files /dev/null and b/versions/4.0/en/asset/texture/type-change.png differ diff --git a/versions/4.0/en/asset/texture/use-texture2d.png b/versions/4.0/en/asset/texture/use-texture2d.png new file mode 100644 index 0000000000..1a7242ad4f Binary files /dev/null and b/versions/4.0/en/asset/texture/use-texture2d.png differ diff --git a/versions/4.0/en/asset/tiledmap.md b/versions/4.0/en/asset/tiledmap.md new file mode 100644 index 0000000000..17a6fef0a0 --- /dev/null +++ b/versions/4.0/en/asset/tiledmap.md @@ -0,0 +1,104 @@ +# Tiled Map Resources (TiledMap) +## Problem +When using a texture atlas, if a 1-pixel stretch is not applied, adjacent images may blend together, leading to incorrect edge colors, such as the appearance of black edges. Therefore, when creating the texture atlas, it is necessary to stretch each image by one pixel to serve as the edge pixel. + +The previous implementation of the engine handled this automatically and would indent by 1 pixel. The code implementation was as follows: +```javascript +if (spFrame) { + grid._name = spFrame.name; + const lm = spFrame.unbiasUV[0]; + const bm = spFrame.rotated ? spFrame.unbiasUV[1] : spFrame.unbiasUV[5]; + grid.l = lm + (grid.x + 0.5) / texWidth; + grid.t = bm + (grid.y + 0.5) / texHeight; + grid.r = lm + (grid.x + grid.width - 0.5) / texWidth; + grid.b = bm + (grid.y + grid.height - 0.5) / texHeight; + grid._rect = new Rect(grid.x, grid.y, grid.width, grid.height); +} +``` + +But there's another problem, it makes the UI design of a good map may look indented, as seen in this [issue](https://github.com/cocos/cocos-engine/issues/17257). + +To fix this, in version **3.8.5** we are removing the code that automatically indents by 1 pixel, and will need to require the user to make a texture altas that needs to be set to extend by 1 pixel. + +## Creating a Texture Atlas for Tiled Maps + +Generally, you'll need to create a texture atlas. You can use the [TexturePacker](https://www.codeandweb.com/texturepacker) tool for this. + +- Add the materials to the tool, as shown in the image below: + ![alt text](tiledmap/image.png) + +- Arrange the exported atlas layout and set the parameters. + ![alt text](tiledmap/image-14.png) + + **Note: You need to stretch by 1 pixel here. If you don't stretch by one pixel, the edges of adjacent textures may appear abnormal when used.** + +- Export the sprite sheet and then set the save path. + ![alt text](tiledmap/image-2.png) + +## Creating and Editing Maps Using Tiled + +Tiled map resources are exported in a data format by the [Tiled Editor](https://www.mapeditor.org/). + +| Creator Version | Tiled Version | +| :------------- | :----------- | +| v3.0 and above | v1.4 | +| v2.2 and above | v1.2.0 | +| v2.1 and below | v1.0.0 | + +- Create map tiles. + ![alt text](tiledmap/image-3.png) + +- Set the tile size. + ![alt text](tiledmap/image-4.png) + ![alt text](tiledmap/image-5.png) + +- Create a new tileset. + ![alt text](tiledmap/image-6.png) + +- Select the texture atlas created using TexturePacker. + ![alt text](tiledmap/image-8.png) + + **Set the size to match the original image, i.e., 32x32 pixels. Also, set the margin to 1 (offset the image by 1 pixel on all sides) and set the spacing to 2 (1 pixel on each side, making it 2 pixels total).If you haven't set stretching in TexturePacker, you need to set the margin to 0 and the spacing to 0 here** + +- Complete the map, as shown below: + ![alt text](tiledmap/image-12.png) + +- Save the `.tmx` file. + +## Importing Map Resources + +The resources required for the map include: + +- `.tmx` map data +- `.png` texture atlas +- `.tsx` tileset configuration file (required by some `.tmx` files) + + ![tiledmap](tiledmap/import.png) + +## Creating Tiled Map Resources + +Drag the map resources from the **Assets** panel to the Tmx File property of the created TiledMap component: + +![tiledmap](tiledmap/set_asset.png) + +## Organizing in the Project + +To improve resource management efficiency, it is recommended to store the imported `.tmx`, `.tsx`, and `.png` files in a separate directory, rather than mixing them with other resources. Make sure to keep the `.tmx` and `.tsx` files in the same directory; otherwise, the resources might not load correctly. + +### Notes + +1. If the texture is not stretched in the TexturePacker, the result may be an error. + + Without stretching, the effect is as shown below: + ![alt text](tiledmap/image-15.png) + + With stretching, the effect is as shown below: + ![alt text](tiledmap/image-16.png) + +2. If you have existing atlases and maps, you can update the atlas without having to recreate it. + + (1) Add a new atlas with new configurations, making sure not to embed it in the map. + ![alt text](tiledmap/image-1.png) + + (2) In the old atlas tab, select and click Replace Tileset, then choose the new atlas created in step (1) to complete the update. + ![alt text](tiledmap/image-7.png) \ No newline at end of file diff --git a/versions/4.0/en/asset/tiledmap/image-1.png b/versions/4.0/en/asset/tiledmap/image-1.png new file mode 100644 index 0000000000..9c6c7bfa3b Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-1.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-12.png b/versions/4.0/en/asset/tiledmap/image-12.png new file mode 100644 index 0000000000..a7a73477b8 Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-12.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-14.png b/versions/4.0/en/asset/tiledmap/image-14.png new file mode 100644 index 0000000000..8f09ceabb6 Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-14.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-15.png b/versions/4.0/en/asset/tiledmap/image-15.png new file mode 100644 index 0000000000..9d4c72d889 Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-15.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-16.png b/versions/4.0/en/asset/tiledmap/image-16.png new file mode 100644 index 0000000000..07e5b59f19 Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-16.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-2.png b/versions/4.0/en/asset/tiledmap/image-2.png new file mode 100644 index 0000000000..db3b0f9ccc Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-2.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-3.png b/versions/4.0/en/asset/tiledmap/image-3.png new file mode 100644 index 0000000000..4e46fd421b Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-3.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-4.png b/versions/4.0/en/asset/tiledmap/image-4.png new file mode 100644 index 0000000000..40b0ad4dbb Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-4.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-5.png b/versions/4.0/en/asset/tiledmap/image-5.png new file mode 100644 index 0000000000..cbe1f952a9 Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-5.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-6.png b/versions/4.0/en/asset/tiledmap/image-6.png new file mode 100644 index 0000000000..9bae80d7e3 Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-6.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-7.png b/versions/4.0/en/asset/tiledmap/image-7.png new file mode 100644 index 0000000000..71666db7aa Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-7.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-8.png b/versions/4.0/en/asset/tiledmap/image-8.png new file mode 100644 index 0000000000..ce19449306 Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-8.png differ diff --git a/versions/4.0/en/asset/tiledmap/image-9.png b/versions/4.0/en/asset/tiledmap/image-9.png new file mode 100644 index 0000000000..6d4f2a1078 Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image-9.png differ diff --git a/versions/4.0/en/asset/tiledmap/image.png b/versions/4.0/en/asset/tiledmap/image.png new file mode 100644 index 0000000000..553e7ac8e0 Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/image.png differ diff --git a/versions/4.0/en/asset/tiledmap/import.png b/versions/4.0/en/asset/tiledmap/import.png new file mode 100644 index 0000000000..ad4698cdab Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/import.png differ diff --git a/versions/4.0/en/asset/tiledmap/set_asset.png b/versions/4.0/en/asset/tiledmap/set_asset.png new file mode 100644 index 0000000000..a5844582bf Binary files /dev/null and b/versions/4.0/en/asset/tiledmap/set_asset.png differ diff --git a/versions/4.0/en/audio-system/audio/audioEdit.png b/versions/4.0/en/audio-system/audio/audioEdit.png new file mode 100644 index 0000000000..f9c7f5677b Binary files /dev/null and b/versions/4.0/en/audio-system/audio/audioEdit.png differ diff --git a/versions/4.0/en/audio-system/audio/audiocilp.gif b/versions/4.0/en/audio-system/audio/audiocilp.gif new file mode 100644 index 0000000000..869f06cbcf Binary files /dev/null and b/versions/4.0/en/audio-system/audio/audiocilp.gif differ diff --git a/versions/4.0/en/audio-system/audio/audiocontroller.png b/versions/4.0/en/audio-system/audio/audiocontroller.png new file mode 100644 index 0000000000..02cfaece28 Binary files /dev/null and b/versions/4.0/en/audio-system/audio/audiocontroller.png differ diff --git a/versions/4.0/en/audio-system/audio/audiosource.png b/versions/4.0/en/audio-system/audio/audiosource.png new file mode 100644 index 0000000000..fbbd089f84 Binary files /dev/null and b/versions/4.0/en/audio-system/audio/audiosource.png differ diff --git a/versions/4.0/en/audio-system/audioExample.md b/versions/4.0/en/audio-system/audioExample.md new file mode 100644 index 0000000000..c5b03f2693 --- /dev/null +++ b/versions/4.0/en/audio-system/audioExample.md @@ -0,0 +1,124 @@ +# Exapmle of AudioMgr + +Since Cocos Creator 3.x removed the v2.x `cc.audioEngine` related API, only AudioSource can be used to control audio playback. + +However, in actual project development, we still need an audio playback manager that can be conveniently called anytime and anywhere, which can refer to or directly use the following code. + +```typescript +//AudioMgr.ts +import { Node, AudioSource, AudioClip, resources, director } from 'cc'; +/** + * @en + * this is a sington class for audio play, can be easily called from anywhere in you project. + * @zh + * 这是一个用于播放音频的单件类,可以很方便地在项目的任何地方调用。 + */ +export class AudioMgr { + private static _inst: AudioMgr; + public static get inst(): AudioMgr { + if (this._inst == null) { + this._inst = new AudioMgr(); + } + return this._inst; + } + + private _audioSource: AudioSource; + constructor() { + //@en create a node as audioMgr + //@zh 创建一个节点作为 audioMgr + let audioMgr = new Node(); + audioMgr.name = '__audioMgr__'; + + //@en add to the scene. + //@zh 添加节点到场景 + director.getScene().addChild(audioMgr); + + //@en make it as a persistent node, so it won't be destroied when scene change. + //@zh 标记为常驻节点,这样场景切换的时候就不会被销毁了 + director.addPersistRootNode(audioMgr); + + //@en add AudioSource componrnt to play audios. + //@zh 添加 AudioSource 组件,用于播放音频。 + this._audioSource = audioMgr.addComponent(AudioSource); + } + + public get audioSource() { + return this._audioSource; + } + + /** + * @en + * play short audio, such as strikes,explosions + * @zh + * 播放短音频,比如 打击音效,爆炸音效等 + * @param sound clip or url for the audio + * @param volume + */ + playOneShot(sound: AudioClip | string, volume: number = 1.0) { + if (sound instanceof AudioClip) { + this._audioSource.playOneShot(sound, volume); + } + else { + resources.load(sound, (err, clip: AudioClip) => { + if (err) { + console.log(err); + } + else { + this._audioSource.playOneShot(clip, volume); + } + }); + } + } + + /** + * @en + * play long audio, such as the bg music + * @zh + * 播放长音频,比如 背景音乐 + * @param sound clip or url for the sound + * @param volume + */ + play(sound: AudioClip | string, volume: number = 1.0) { + if (sound instanceof AudioClip) { + this._audioSource.stop(); + this._audioSource.clip = sound; + this._audioSource.play(); + this.audioSource.volume = volume; + } + else { + resources.load(sound, (err, clip: AudioClip) => { + if (err) { + console.log(err); + } + else { + this._audioSource.stop(); + this._audioSource.clip = clip; + this._audioSource.play(); + this.audioSource.volume = volume; + } + }); + } + } + + /** + * stop the audio play + */ + stop() { + this._audioSource.stop(); + } + + /** + * pause the audio play + */ + pause() { + this._audioSource.pause(); + } + + /** + * resume the audio play + */ + resume(){ + this._audioSource.play(); + } +} +``` diff --git a/versions/4.0/en/audio-system/audioLimit.md b/versions/4.0/en/audio-system/audioLimit.md new file mode 100644 index 0000000000..12397fab43 --- /dev/null +++ b/versions/4.0/en/audio-system/audioLimit.md @@ -0,0 +1,21 @@ +# Compatibility Notes + +## Loading modes for audio assets on the Web platform + +Audio assets on the Web platform are special because the Web standard supports loading audio assets in two different ways, namely: +- Web Audio: provides a relatively more modern sound control interface, which is cached in the engine as an audio buffer. The advantage of this approach is good compatibility and fewer problems. +- DOM Audio: plays audio assets by generating a standard audio element, which is cached in the engine. When using standard audio elements to play audio assets, you may encounter some compatibility issues in some browsers, for example, iOS browsers do not support adjusting the volume, and all volume related properties will not take effect. + +Currently, Cocos Creator loads audio assets as Web Audio by default, but if it detects that the current browser does not support loading Web Audio, it will switch to load audio as DOM Audio. + +If the project needs to force audio assets to be loaded via DOM Audio, please load them dynamically using the following way: + +```typescript +assetManager.loadRemote('http://example.com/background.mp3', { + audioLoadMode: AudioClip.AudioType.DOM_AUDIO +}, (err, clip: AudioClip) => { + if(err){ + console.log(err); + } +}); +``` diff --git a/versions/4.0/en/audio-system/audiosource.md b/versions/4.0/en/audio-system/audiosource.md new file mode 100644 index 0000000000..e2ebf20dfd --- /dev/null +++ b/versions/4.0/en/audio-system/audiosource.md @@ -0,0 +1,156 @@ +# AudioSource Component Reference + +The AudioSource component is used to control the playback of music and sound effects. + +![audioSource](audio/audiosource.png) + +Select the node in the **Hierarchy** panel, then click the **Add Component** button at the bottom of the **Inspector** panel and select **Audio -> AudioSource** to add the AudioSource component to the node. + +## AudioSource Properties + +| Property | Description | +| :-- | :-- | +| Clip | The added [audio asset](../asset/audio.md) for playback , default is empty, click the arrow button behind it to select. | +| Loop | Whether to loop. | +| PlayOnAwake | Whether the audio will be played automatically when the game is running (component is active). | +| Volume | Volume, in the range 0~1. | + +## Audio Playback + +Cocos Creator 3.x uses AudioSource to control the playback of audio, which is a component that can be added to the scene, set by the **Editor**, or called in a **script**. + +In addition, Creator divides audio into longer **music** and shorter **sound effects** based on their length. + +- If you control audio playback through the editor, there is no difference between playing music and sound effects, but long music is recommended. See the **Playback via Editor** section below for details. +- If audio playback is controlled via script, the AudioSource component additionally provides the `playOneShot` interface for playing short sound effects, see the **Sound Effect Playback** section below for details. + +> **Note**: Cocos Creator 3.x removes the `audioEngine` API from v2.x and uses the AudioSource component for audio playback. + +### Via the editor + +1. Add the AudioSource component to the node. + +2. Drag and drop the required audio asset from the **Assets** panel into the **Clip** property box of the AudioSource component as follows: + + ![audioClip](audio/audiocilp.gif) + +3. Set the other properties of the AudioSource component as needed. + +### Via script + +For more flexible control of AudioSource playback, you can add a custom script to the node where the **AudioSource component** is located and then call the appropriate API to control audio playback. + +1. Add the AudioSource component to the node and specify the audio asset. + +2. [Create script](../scripting/setup.md) in the **Assets** panel and name it (e.g. `AudioController`), then double-click to open the script for writing, as follows: + + ```typescript + import { _decorator, Component, Node, AudioSource, assert } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass("AudioController") + export class AudioController extends Component { + + @property(AudioSource) + public _audioSource: AudioSource = null! + + onLoad () { + // Get the AudioSource component + const audioSource = this.node.getComponent(AudioSource)! ; + // Check if it contains AudioSource, if not, output an error message + assert(audioSource); + // Assign the component to the global variable _audioSource + this._audioSource = audioSource; + } + + play () { + // Play the music + this._audioSource.play(); + } + + pause () { + // Pause the music + this._audioSource.pause(); + } + } + ``` + +3. Select the node in the **Hierarchy** panel, then drag and drop the script from the **Assets** panel to the **Inspector** panel to add the script component to the node. As shown below: + + ![audioSource](audio/audiocontroller.png) + +#### Audio Playback + +Compared to long music playback, sound effect playback has the following characteristics: + +- Short playback time +- Large number of simultaneous playback + +The **AudioSource** component provides the `playOneShot` interface to play sound effects. The specific code implementation is as follows: + +```typescript +// AudioController.ts + +import { AudioClip, AudioSource, Component, _decorator } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("AudioController") +export class AudioController extends Component { + + @property(AudioClip) + public clip: AudioClip = null! + + @property(AudioSource) + public audioSource: AudioSource = null! + + playOneShot () { + this.audioSource.playOneShot(this.clip, 1); + } +} +``` + +> **Note**: `playOneShot` is a one-time play operation, the sound after playing cannot be paused or stopped, and cannot listen to the end-of-play event callback. + +For more audio-related scripting interfaces, please refer to [AudioSource API](%__APIDOC__%/en/class/AudioSource). + +For more control over audio playback, please refer to the [AudioSource playback example](./audioExample.md) documentation. + +## Register AudioSource Event Callback + +Cocos Creator supports registering event callbacks on AudioSource components, with the following usage examples: + +```typescript +import { _decorator, Component, Node, AudioSource } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('AudioDemo') +export class AudioDemo extends Component { + + @property(AudioSource) + audioSource: AudioSource = null!; + + onEnable () { + // Register the started event callback + this.audioSource.node.on(AudioSource.EventType.STARTED, this.onAudioStarted, this); + // Register the ended event callback + this.audioSource.node.on(AudioSource.EventType.ENDED, this.onAudioEnded, this); + } + + onDisable () { + this.audioSource.node.off(AudioSource.EventType.STARTED, this.onAudioStarted, this); + this.audioSource.node.off(AudioSource.EventType.ENDED, this.onAudioEnded, this); + } + + onAudioStarted () { + // TODO... + } + + onAudioEnded () { + // TODO... + } +} +``` + +## Web platform playback restrictions + +Audio playback on the Web platform currently requires compliance with the latest [Audio Play Policy](https://www.chromium.org/audio-video/autoplay), and even if the **AudioSource** component has `playOnAwake` set, It also needs to be played after the first user click event occurs. diff --git a/versions/4.0/en/audio-system/overview.md b/versions/4.0/en/audio-system/overview.md new file mode 100644 index 0000000000..919eddaef3 --- /dev/null +++ b/versions/4.0/en/audio-system/overview.md @@ -0,0 +1,8 @@ +# Audio System + +Music is an integral part of a game, and good music makes a game more realistic and immersive. the audio system in Cocos Creator supports importing and playing most common audio file formats, for details please refer to the following: + +- [Audio Assets](../asset/audio.md) +- [AudioSource Component Reference](./audiosource.md) +- [AudioSource Playback Example](./audioExample.md) +- [Compatibility Notes](./audioLimit.md) diff --git a/versions/4.0/en/book.json b/versions/4.0/en/book.json new file mode 100644 index 0000000000..11a0c4ee1b --- /dev/null +++ b/versions/4.0/en/book.json @@ -0,0 +1,108 @@ +{ + "structure": { + "summary": "SUMMARY.md", + "readme": "index.md" + }, + "variables": { + "docType": "Manual", + "products": [ + { + "name": "Cocos Creator", + "links": [ + { + "name": "Manual", + "link": "https://docs.cocos.com/creator/4.0/manual/en" + }, + { + "name": "API Ref", + "link": "https://docs.cocos.com/creator/4.0/api/en/" + } + ] + }, + { + "name": "Cocos2d-x", + "links": [ + { + "name": "Manual", + "link": "https://docs.cocos.com/cocos2d-x/manual/en" + }, + { + "name": "API Ref", + "link": "http://docs.cocos2d-x.org/api-ref/index.html" + } + ] + } + ], + "version": [ + { + "name": "3.8", + "links": [ + { + "name": "3.8", + "link": "https://docs.cocos.com/creator/4.0/manual/en" + }, + { + "name": "3.7", + "link": "https://docs.cocos.com/creator/3.7/manual/en" + }, + { + "name": "3.6", + "link": "https://docs.cocos.com/creator/3.6/manual/en" + }, + { + "name": "3.5", + "link": "https://docs.cocos.com/creator/3.5/manual/en" + }, + { + "name": "3.4", + "link": "https://docs.cocos.com/creator/3.4/manual/en" + }, + { + "name": "3.3", + "link": "https://docs.cocos.com/creator/3.3/manual/en" + }, + { + "name": "3.2", + "link": "https://docs.cocos.com/creator/3.2/manual/en" + }, + { + "name": "3.1", + "link": "https://docs.cocos.com/creator/3.1/manual/en" + }, + { + "name": "3.0", + "link": "https://docs.cocos.com/creator/3.0/manual/en" + }, + { + "name": "2.4", + "link": "https://docs.cocos.com/creator/2.4/manual/en" + }, + { + "name": "2.3", + "link": "https://docs.cocos.com/creator/2.3/manual/en" + }, + { + "name": "2.2", + "link": "https://docs.cocos.com/creator/2.2/manual/en" + }, + { + "name": "2.1", + "link": "https://docs.cocos.com/creator/2.1/manual/en" + }, + { + "name": "2.0", + "link": "https://docs.cocos.com/creator/2.0/manual/en" + }, + { + "name": "1.10", + "link": "https://docs.cocos.com/creator/1.10/manual/en" + }, + { + "name": "1.9", + "link": "https://docs.cocos.com/creator/1.9/manual/en" + } + ] + } + ] + } +} diff --git a/versions/4.0/en/cases-and-tutorials/index.md b/versions/4.0/en/cases-and-tutorials/index.md new file mode 100644 index 0000000000..f412bc0adc --- /dev/null +++ b/versions/4.0/en/cases-and-tutorials/index.md @@ -0,0 +1,37 @@ +# Examples and Tutorials + +## Examples + +- **One Step, Two Steps**([GitHub](https://github.com/cocos/tutorial-mind-your-step-3d)): A game with step-by-step tutorial in the [Quick Start](../getting-started/first-game/index.md). +- **Material Examples**([GitHub](https://github.com/cocos/cocos-example-materials)): Shows a variety of engine-built and customized material effects, including standard PBR, cartoon rendering, and also includes advanced materials such as skin, hair, eyeballs, leaves, and more. +- **Render Pipeline Usage Demo**([GitHub](https://github.com/cocos/cocos-example-render-pipeline)): Demonstrates in Sponza how to use builtin post process render pipeline with all post-process features enabled. +- **Examples of Physics**([GitHub](https://github.com/cocos/cocos-example-physics)): Includes 2D and 3D physics test cases and examples, such as **Engulfing Black Hole**, **Simple Car**, **Falling ball**, etc. The test cases introduce some basic functions and usage methods. It helps you understand the physical functions with documentation easily. +- **Marionette Animation Examples**([GitHub](https://github.com/cocos/cocos-example-marionette)): Demonstrates how to use [Marionette Aniamtion System](../animation/marionette/index.md) in details, including state machine, pose graph, events, IK, etc. +- **UI Example**([GitHub](https://github.com/cocos/cocos-example-ui/) | [Cocos Store](https://store.cocos.com/app/detail/2799)): A complex UI example, including the home page, store, backpack, and other common game UI interface. +- **Example Cases**([GitHub](https://github.com/cocos/cocos-example-projects)): Includes examples of networking, npm modules, and other features. +- **Modules Test Cases**([GitHub](https://github.com/cocos/cocos-test-projects)): The example project of each function of the engine, which basically covers most of the function modules of the engine. Users can refer to the usage in this project when using the functions for development. +- **Practical Solutions**([GitHub](https://github.com/cocos/cocos-awesome-tech-solutions)): A collection of practical solutions provided by technical support engineers. +- **Jump Ball 3D**([GitHub](https://github.com/cocos/cocos-example-ball) | [Cocos Store](https://store.cocos.com/app/detail/2802)): Users can make jump ball games through this project. +- **Taxi Game 3D**([GitHub](https://github.com/cocos/cocos-tutorial-taxi-game) | [Cocos Store](https://store.cocos.com/app/en/detail/2796)): A physics-based 3d game, users can make taxi games through this project. + +> All cases and examples on GitHub are updated with the version, and the main branch corresponds to the latest **Cocos Creator** version, so please pay attention when downloading. + +## Tutorials + +### Videos + +- [Cocos Creator 3.x for Beginners](https://www.youtube.com/watch?v=JSOXYPqZ1-8&list=PLbvpmJKjO3NA4dlW43GzhJUMaXyIp3xpc) +- [Cocos Quick Look](https://www.youtube.com/watch?v=R4Mxm55x56Q&list=PLbvpmJKjO3NCgicfQ_N32Oo62LgPVs4kf) +- [Introduction to Shaders with Cocos Creator](https://www.youtube.com/watch?v=tUQHK42UcHc&list=PLbvpmJKjO3ND91HTUSKWNzMboob-7J_wq) +- [Introduction to Cocos Creator 3.0](https://www.youtube.com/watch?v=3v4pq0tDo5g&list=PLbvpmJKjO3NDv_eb6vlN70Zo-pdAv8MNW) + +### Articles + +- [Best Tutorials - Part One](https://www.cocos.com/en/post/33e9da7fc3825a8aeb66ff6fbb7f5dd4) +- [Best Tutorials - Part Two](https://www.cocos.com/en/post/52fnhtaqlEYYTafjBYjMMYWKyA6d3qIZ) + +## More + +- [Cocos Forum](https://discuss.cocos2d-x.org/c/creator/33): ask questions, find answers, communicate with other developers. +- [Cocos Store](http://store.cocos.com/): various art assets, extensions, source codes and learning demos. +- [Youtube channel — Cocos](https://www.youtube.com/@CocosEngine):news and tutorials. diff --git a/versions/4.0/en/concepts/scene/coord.md b/versions/4.0/en/concepts/scene/coord.md new file mode 100644 index 0000000000..3ba1fa1e07 --- /dev/null +++ b/versions/4.0/en/concepts/scene/coord.md @@ -0,0 +1,85 @@ +# Coordinate Systems and Node Transformation Properties + +In the documents, [Scene Panel](../../editor/scene/index.md), and [Nodes and Components](node-component.md), the ability to change the display behavior of nodes by using the **transformation tool Gizmo** and editing the properties of nodes in the **Inspector** panel was introduced. This document will take a deeper look at the coordinate system of the scene space in which the node is located and how the **Position**, **Rotation**, and **Scale** transformation properties of the node work. + +## Coordinate Systems + +Position properties for nodes can be set, but where will a node with a specific position property be rendered on the screen when the game is running? Just as longitude and latitude specify coordinates for a location on a planet it is necessary to understand the coordinate system of Cocos Creator 3.x to understand the meaning of node positions. + +### World Coordinate + +The world coordinate system, also called absolute coordinate system, represents a unified coordinate system in the scene space in Cocos Creator game development, and "world" is used to represent our game scene. + +The world coordinate system of Creator 3.x uses a Cartesian right-handed coordinate system with default **x** to the right, **y** to the top, **z** to the outside, and the **-z** axis for the front. + +![right hand](coord/right_hand.png) + +### Local Coordinate + +The local coordinate system, also called the relative coordinate system, is the coordinate system associated with the node. Each node has a separate coordinate system, and when the node moves or changes direction, the coordinate system associated with that node will move or change direction with it. + +Creator has a hierarchy of parent-child relationships between **Nodes**, and the position of a node set by modifying its `Position` property is the node's **local coordinate system** with respect to its parent, not the world coordinate system. + +Finally when drawing the whole scene, Creator will map the local coordinates of these nodes to world coordinate system coordinates.
+Suppose there are three nodes in the scene: NodeA, NodeB, and NodeC. The structure of the nodes is shown in the following figure. + +![node tree](coord/node-tree.png) + +When the scene contains nodes at different levels, the position of each node under the world coordinate system is determined according to the following steps: + +1. Process each node starting from the root level, NodeA in the above figure is a root level node. The first step is to determine the position of the origin of NodeA's local coordinate system (i.e. `Position`) in the world coordinate system based on NodeA's **Position** property. +2. Next, process all the direct children of NodeA, which is NodeB in the above figure (as well as other nodes of the same level as NodeB). Based on NodeB's `Position` property, determine NodeB's position in the world coordinate system in NodeA's local coordinate system. +3. Each node uses the parent's coordinate system and its own position property to determine its position in the world coordinate system. + +## Transformation Properties + +Nodes include three main transformation properties, **Position**, **Rotation** and **Scale**, which are described below in turn. + +![transform properties](coord/transform-properties.png) + +### Position + +**Position** consists of the `X`, `Y` and `Z` properties, which specify the coordinates of the node on the X-axis, Y-axis and Z-axis of the current coordinate system, respectively, and default to `(0, 0, 0)`. + +![position](coord/position-nodeA.png) + +![position](coord/position-nodeB.png) + +In the above figure, the world coordinates of NodeA are `(50, 50, 50)` and the local coordinates of child NodeB are `(0, 2, 0)`. If NodeB is moved to the root of the scene, the world coordinates of NodeB become `(50, 52, 50)`. + +![position](coord/position-nodeB-world.png) + +The `Position` of the child NodeB is based on the `Position` of the parent NodeA as the origin of the coordinate system. + +If the parent NodeA changes its `Position`, the child NodeB will also change its position (world coordinate system), but the `Position` property of the child NodeB will not change, because the child NodeB does not change in the local coordinate system with the parent NodeA's `Position` as the origin. + +In the **Scene** panel, use the [Move Transform Tool](../../editor/toolbar/index.md) to change the node position. + +### Rotation + +**Rotation** consists of the `X`, `Y` and `Z` properties, which default to `(0, 0, 0)`, and is another important property that affects the node's local coordinate system. When the `X` property is changed, it means that the node will be rotated counterclockwise/clockwise around the x-axis, and so on, and the same applies when the `Y` or `Z` property is changed. + +- When the property value is **positive**, the node rotates **counterclockwise**. +- When the property value is **negative**, the node rotates **clockwise**. + +![rotation](coord/rotation.png) + +The node hierarchy shown above is the same as the previous figure, except that the **Rotation** property of NodeA on the z-axis is set to **60** degrees, notice that in addition to NodeA itself being **counterclockwise** rotated by 60 degrees on the z-axis, its child NodeB is also **centered** on the z-axis. NodeB is also rotated **counterclockwise** on NodeA's z-axis. This also means that the rotation property affects the child nodes. + +> **Note**: the quaternion [rotation](%__APIDOC__%/en/class/Node?id=rotation) property on a node represents the angle of rotation about any axis. The property corresponding to `Rotation` in the **Inspector** panel is the property [EulerAngles](__APIDOC__/en/class/Node?id=eulerAngles), and the value of `Rotation` can be obtained in the script via `Node.eulerAngles`. These two properties can be used separately according to your needs. When using the API, make sure to pay attention to the difference between them and the editor panel property names. + +In the **Scene** panel, use the [Rotate Transform Tool](../../editor/toolbar/index.md) to set the rotation of the node. + +### Scale + +The **Scale** property also consists of the `X`, `Y` and `Z` properties, which represent the scaling of the node on the x-axis, y-axis and z-axis, respectively, and defaults to `(1, 1, 1)`. + +![scale](coord/scale.png) + +The node hierarchy shown above is the same as when `Position` was introduced. Setting the scale property of NodeA to `(2, 1, 1)` means that NodeA is scaled to **2** times its original size in the x-axis direction, while the y-axis and z-axis remain unchanged. You can see that the child node NodeB is also scaled to twice its original size in the x-axis direction, so the scaling property affects all child nodes. + +The `Scale` property set on a child node are superimposed on its parent node's scaling, and the child nodes of the child node **multiply** all the `Scale` properties at each level to obtain the scaling multiplier displayed in the world coordinate system. This is actually the same as the **position** and **rotation** properties, except that the **position** and **rotation** properties are **additive**, while the **scaling** property is **multiplicative**, which has a more pronounced effect. + +The **Scale** property does not affect the **Position** and **Rotation** of the current node, but it does affect the **Position** of the child nodes. + +In the **Scene** panel, use the [Scale Transform Tool](../../editor/toolbar/index.md) to modify the node scaling. diff --git a/versions/4.0/en/concepts/scene/coord/node-tree.png b/versions/4.0/en/concepts/scene/coord/node-tree.png new file mode 100644 index 0000000000..8dcab96f23 Binary files /dev/null and b/versions/4.0/en/concepts/scene/coord/node-tree.png differ diff --git a/versions/4.0/en/concepts/scene/coord/position-nodeA.png b/versions/4.0/en/concepts/scene/coord/position-nodeA.png new file mode 100644 index 0000000000..e444abf77f Binary files /dev/null and b/versions/4.0/en/concepts/scene/coord/position-nodeA.png differ diff --git a/versions/4.0/en/concepts/scene/coord/position-nodeB-world.png b/versions/4.0/en/concepts/scene/coord/position-nodeB-world.png new file mode 100644 index 0000000000..5d53d779e4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/coord/position-nodeB-world.png differ diff --git a/versions/4.0/en/concepts/scene/coord/position-nodeB.png b/versions/4.0/en/concepts/scene/coord/position-nodeB.png new file mode 100644 index 0000000000..1caae1b87d Binary files /dev/null and b/versions/4.0/en/concepts/scene/coord/position-nodeB.png differ diff --git a/versions/4.0/en/concepts/scene/coord/right_hand.png b/versions/4.0/en/concepts/scene/coord/right_hand.png new file mode 100644 index 0000000000..ff5af90ef8 Binary files /dev/null and b/versions/4.0/en/concepts/scene/coord/right_hand.png differ diff --git a/versions/4.0/en/concepts/scene/coord/rotation.png b/versions/4.0/en/concepts/scene/coord/rotation.png new file mode 100644 index 0000000000..aafc29915e Binary files /dev/null and b/versions/4.0/en/concepts/scene/coord/rotation.png differ diff --git a/versions/4.0/en/concepts/scene/coord/scale.png b/versions/4.0/en/concepts/scene/coord/scale.png new file mode 100644 index 0000000000..ec40dc4e7d Binary files /dev/null and b/versions/4.0/en/concepts/scene/coord/scale.png differ diff --git a/versions/4.0/en/concepts/scene/coord/transform-properties.png b/versions/4.0/en/concepts/scene/coord/transform-properties.png new file mode 100644 index 0000000000..c0c1807d32 Binary files /dev/null and b/versions/4.0/en/concepts/scene/coord/transform-properties.png differ diff --git a/versions/4.0/en/concepts/scene/fog.md b/versions/4.0/en/concepts/scene/fog.md new file mode 100644 index 0000000000..6e5909f2c3 --- /dev/null +++ b/versions/4.0/en/concepts/scene/fog.md @@ -0,0 +1,119 @@ +# Global Fog + +Global fog is used to simulate fog effects in outdoor environments in games. In addition to fog effects in the game, it can also be used to hide the model outside the camera's far clipping plane to improve the rendering performance. + +The global fog currently includes four types: Linear Fog, Exponential Fog, Exponential Squared Fog, and Layer Fog, please refer to the **Global Fog Types** section below for additional details. + +## Enable Global Fog + +Check **Scene** in the **Hierarchy** panel, then check the **Enabled** property in the **Fog** component of the **Inspector** panel to enable global fog. + +![image](./fog/enable-fog.png) + +## Accurate Fog Computing + +Prior to v3.4, Creator used vertex fog by default, which caused unusual fog transitions on objects with a few vertices and a large volume. Starting with v3.4, Creator added the **Accurate** option to enable pixel fog to solve this problem. + +![accurate](./fog/accurate.png) + +The effect when the **Accurate** option is unchecked, i.e., the pixel fog is not enabled, is as follows: + +![accuracy_off](./fog/accuracy_off.png) + +The effect when the **Accurate** option is checked, i.e., the pixel fog is enabled, is as follows: + +![accuracy_on](./fog/accuracy_on.png) + +### Version Upgrade - `Effect` Migration + +When a project is upgraded to v3.4, all the Effect code that calls to `CC_APPLY_FOG` needs to be modified to add a second parameter, `worldPos`. e.g.: + +- The original code is as follows: + + ```ts + CC_APPLY_FOG(finalColor); + ``` + +- After upgrading to v3.4, the original code needs to be changed to the following: + + ```ts + CC_APPLY_FOG(finalColor, v_position.xyz); + ``` + +## Types of Global Fog + +The types of global fog include Linear Fog, Exponential Fog, Exponential Squared Fog, and Layer Fog. The type depends on the calculation result of **Camera** and **Model Vertex**, which is called **Fog Blend Factor**. The fog blend factor determines how the fog color and the model color are blended, resulting in a different global fog effect. + +### Linear Fog + +![linear_fog](./fog/linear_fog.png) + +| Property | Description | +| :--- | :--- | +| **Enabled** | Whether to enable the global fog. | +| **Accurate** | Whether to enable accurate fog computing. | +| **FogColor** | The color of the global fog. | +| **Type** | The type of the global fog. | +| **FogStart** | The starting position of the fog effect. | +| **FogEnd** | The end position of the fog effect. | + +The fog blend factor of Linear Fog is calculated by the formula: + +**f = (FogEnd - Cam_dis) / (FogEnd - FogStart)** + +- When `Cam_dis = FogEnd`, i.e., the distance between the camera and the model vertex is equal to `FogEnd`, the blend factor is calculated as 0, and the object is fully fogged. + +- When `Cam_dis = FogStart`, i.e. the distance between the camera and the model vertex is equal to `FogStart`, the blend factor is calculated as 1, and the object is not affected by fogging. + +To increase the density of Linear Fog when the distance between the camera and the model vertex is fixed, there are two ways: + +1. Fix the value of `FogStart` and decrease the value of `FogEnd`. +2. Decrease the value of `FogStart` and fix the value of `FogEnd`. + +To adjust the fog effect to the right consistency, it is best to adjust both the `FogStart` and `FogEnd` properties appropriately. + +### Exponential Fog and Exponential Squared Fog + +![exp-properties](./fog/exp-properties.png) + +The properties of Exponential Fog and Exponential Squared Fog are the same, and the Exponential Fog is used here as an example: + +| Property | Description | +| :--- | :--- | +| **Enabled** | Whether to enable the global fog. | +| **Accurate** | Whether to enable accurate fog computing. | +| **FogColor** | The color of the global fog. | +| **Type** | The type of the global fog. | +| **FogDensity** | The fog density, the value range is 0 ~ 1, the larger the value the higher the density. | +| **FogStart** | The starting position of the fog effect. | +| **FogAtten** | Fog attenuation coefficient, the smaller the value the denser the fog. | + +The fog blend factor for Exponential Fog is calculated as: + +**f = e^(-max(0, distance-fogStart) * fogDensity)** + +The fog blend factor for Exponential Squared Fog is calculated as: + +**f = e^(-max(0, distance-fogStart) * fogDensity)²** + +Developers can adjust the distribution of global fog in the near and far distance with `FogStart`, and the fog concentration at different locations with `FogDensity` and `FogAtten`. + +### Layered Fog + +Layered Fog is parallel to the horizontal plane and has a specific height. The height of the fog can be determined by setting the top of the Layered Fog at any position in the vertical direction of the scene world coordinate system. + +![image](./fog/layerfog.png) + +| Property | Description | +| :--- | :--- | +| **Enabled** | Whether to enable the global fog. | +| **Accurate** | Whether to enable accurate fog computing. | +| **FogColor** | The color of the global fog. | +| **Type** | The fog type of the global fog. | +| **FogAtten** | Fog attenuation coefficient, the smaller the value the denser the fog. | +| **FogTop** | The position of the model vertices in the vertical direction of the world coordinate system, below which all vertices will be affected by the fog effect. | +| **FogRange** | The range of the fog effect from the set **FogTop** downwards. | + +The fog calculation of Layered Fog is a bit more complicated than the other three fog types, as it introduces the concept of `FogTop` and also requires distance calculation in the **X-Z** plane. + +Layered Fog is more common in reality, with towering mountains and buildings. If it is used wisely, it is believed to have a good effect on scene presentation, but at the same time, the computation will be increased, developers can decide according to their needs. diff --git a/versions/4.0/en/concepts/scene/fog/accuracy_off.png b/versions/4.0/en/concepts/scene/fog/accuracy_off.png new file mode 100644 index 0000000000..8d6fbfb2cc Binary files /dev/null and b/versions/4.0/en/concepts/scene/fog/accuracy_off.png differ diff --git a/versions/4.0/en/concepts/scene/fog/accuracy_on.png b/versions/4.0/en/concepts/scene/fog/accuracy_on.png new file mode 100644 index 0000000000..7adf8b8eee Binary files /dev/null and b/versions/4.0/en/concepts/scene/fog/accuracy_on.png differ diff --git a/versions/4.0/en/concepts/scene/fog/accurate.png b/versions/4.0/en/concepts/scene/fog/accurate.png new file mode 100644 index 0000000000..227d445a05 Binary files /dev/null and b/versions/4.0/en/concepts/scene/fog/accurate.png differ diff --git a/versions/4.0/en/concepts/scene/fog/enable-fog.png b/versions/4.0/en/concepts/scene/fog/enable-fog.png new file mode 100644 index 0000000000..ef4b189ba2 Binary files /dev/null and b/versions/4.0/en/concepts/scene/fog/enable-fog.png differ diff --git a/versions/4.0/en/concepts/scene/fog/exp-properties.png b/versions/4.0/en/concepts/scene/fog/exp-properties.png new file mode 100644 index 0000000000..7dbf579a74 Binary files /dev/null and b/versions/4.0/en/concepts/scene/fog/exp-properties.png differ diff --git a/versions/4.0/en/concepts/scene/fog/layerfog.png b/versions/4.0/en/concepts/scene/fog/layerfog.png new file mode 100644 index 0000000000..ad1f81f339 Binary files /dev/null and b/versions/4.0/en/concepts/scene/fog/layerfog.png differ diff --git a/versions/4.0/en/concepts/scene/fog/linear_fog.png b/versions/4.0/en/concepts/scene/fog/linear_fog.png new file mode 100644 index 0000000000..a297a51ab0 Binary files /dev/null and b/versions/4.0/en/concepts/scene/fog/linear_fog.png differ diff --git a/versions/4.0/en/concepts/scene/index.md b/versions/4.0/en/concepts/scene/index.md new file mode 100644 index 0000000000..cdf162b27f --- /dev/null +++ b/versions/4.0/en/concepts/scene/index.md @@ -0,0 +1,21 @@ +# Scene Creation Workflow + +A scene is an abstract collection of environmental factors in a game, a local unit for creating a game environment. Game developers represent a part of the game's world content by creating a scene in the editor. + +![scene world](./scene/world01.jpg) + +## Scene Structure + +Cocos Creator implements a free scene structure using a node tree and a node component system. The **Node** is responsible for managing the parent-child relationship of the node tree and the spatial matrix transformation **Transform**, so that all entity nodes can be easily managed and placed in the scene. + +The component system gives nodes a variety of advanced features, such as **MeshRenderer** component, **Animation** component, **Light** component, **Terrain** component, and more. One of the necessary elements of the 3D scene is the **Camera** component, which represents the player's viewpoint in the game, without which nothing can be seen. Therefore, when creating a scene, Creator will create a node with the **Camera** component mounted by default. + +## Scene Creation Related Workflow + +- [Nodes and Components](node-component.md) +- [Coordinate System and Transformations](coord.md) +- [Node Hierarchy and Display Order](node-tree.md) +- [Building a Scene with the Scene Panel](scene-editing.md) +- [Skybox](skybox.md) +- [Global Fog](fog.md) +- [Shadows](./light/shadow.md) diff --git a/versions/4.0/en/concepts/scene/layer.md b/versions/4.0/en/concepts/scene/layer.md new file mode 100644 index 0000000000..0e560fc4b2 --- /dev/null +++ b/versions/4.0/en/concepts/scene/layer.md @@ -0,0 +1,26 @@ +# Layer + +The `Layer` property of the Node is an unsigned 32-bit integer, supporting up to 32 different types of `Layer`, which can be set in **Project -> Project Settings -> [Layers](../../editor/project/index.md#layers)** in the menu bar above the editor. The developer can customize the Layer **0 ~ 19**, and the remaining 12 Layers are the engine's built-in ones. + +The `Visibility` property of the Camera and the `Layer` property of the Node are both used to control the visibility of nodes. However, a node can only be seen by the camera if the `Layer` property set in the node is included in the `Visibility` of the camera. The `Visibility` property of the camera uses bitwise operators (such as `|` and `&`) to determine whether a node's `Layer` should be visible, and supports selecting multiple Layers at the same time. See the [Camera — Set the Visibility property](../../editor/components/camera-component.md) documentation for details. + +## The engine's built-in Layers + +![layer gizmo](scene/layer-gizmo.png) + +| Property | Description | Property Value | +| :--- | :--- | :--- | +| **NONE** | Set all invisible | 0 | +| **IGNORE_RAYCAST** | Setting to ignore ray detection | 1 << 20 | +| **GIZMOS** | Set gizmo information visible | 1 << 21 | +| **EDITOR** | Set editor visible | 1 << 22 | +| **UI_3D** | Set the `3D UI` node to be visible | 1 << 23 | +| **SCENE_GIZMO** | Set scene gizmo visible | 1 << 24 | +| **UI_2D** | Set `2D UI` nodes visible | 1 << 25 | +| **PROFILER** | Set the profiler node to be visible | 1 << 28 | +| **DEFAULT** | Set the default node to be visible | 1 << 30 | +| **ALL** | Set all nodes to be visible | 0xffffffff | + +## User-defined Layers + +![layer gizmo](scene/layer-edit.png) diff --git a/versions/4.0/en/concepts/scene/light.md b/versions/4.0/en/concepts/scene/light.md new file mode 100644 index 0000000000..e1641c4352 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light.md @@ -0,0 +1,17 @@ +# Lighting + +This section introduces how lighting works and how to use it in Cocos Creator. + +The implementation of lighting in Creator simulates the effect of light on the real world. Adding lights to the scene can make the scene produce corresponding lighting and shading effects to get better visual effects. + +![light scene](light/lighting.png) + +## Additional References + +- [Physically Based Lighting](light/pbr-lighting.md) +- [Types of Light](light/lightType/index.md) +- [Additive per-pixel lights](light/additive-per-pixel-lights.md) +- [Shadows](light/shadow.md) +- [Lightmapping](light/lightmap.md) + +Cocos Creator provides some [lighting examples](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/light), which mainly introduces some common editing operations for lighting and code examples for reference. diff --git a/versions/4.0/en/concepts/scene/light/additive-per-pixel-lights.md b/versions/4.0/en/concepts/scene/light/additive-per-pixel-lights.md new file mode 100644 index 0000000000..6d2e14db55 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/additive-per-pixel-lights.md @@ -0,0 +1,39 @@ +# Additive Per-Pixel Lights + +Uber Shader is still the dominant solution on some performance-constrained platforms, but as hardware performance increases and the demand for higher picture quality increases, a fixed number of lights can no longer meet the needs of practical applications, so there is a solution that supports multiple lights -- **Multi-Pass Drawing**. + +The following is an example of how to implement multiple lights based on multiple `Pass` with the default lighting material `default-material.mtl` in Cocos Creator. + +![default-material](additivelights/default-material.png) + +First, create a new **Sphere** node in the **Hierarchy** panel, then continue to add a Directional Light and two Spotlights, setting them to surround the Sphere, as shown in the following image: + +![using Light](additivelights/usingLight.png) + +After the scene is built, select the browser preview above the editor and you can see the draw call in the bottom left corner of the web preview: + +![Draw Call](additivelights/drawCall.png) + +We can use a third party software such as RenderDoc to open the Frame Debug to see how these lights are rendered to the screen: + +![Frame Debug](additivelights/debug.png) + +As shown in the image above, the first rendering is the lighting of **Directional Light**: + +![main light pass](additivelights/pass1.png) + +The second rendering is the lighting of **Spotlight 1**: + +![ForwardAdd pass](additivelights/pass2.png) + +The third rendering is the lighting of **Spotlight 2**: + +![ForwardAdd pass](additivelights/pass3.png) + +This type of rendering is the **Forward-Pipeline** that supports multiple lighting models, and **Forward** generally consists of two `Pass`: + +- The first `Pass` is **BasePass**, which is used to render the lighting of Directional Light. + +- The second `Pass` is **LightPass**, which is used to render the lighting of the remaining lights. + +Therefore, when an object is illuminated by more than one light at the same time, the Draw Call will also increase. diff --git a/versions/4.0/en/concepts/scene/light/additivelights/debug.png b/versions/4.0/en/concepts/scene/light/additivelights/debug.png new file mode 100644 index 0000000000..4f4c480978 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/additivelights/debug.png differ diff --git a/versions/4.0/en/concepts/scene/light/additivelights/default-material.png b/versions/4.0/en/concepts/scene/light/additivelights/default-material.png new file mode 100644 index 0000000000..86d522a9b0 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/additivelights/default-material.png differ diff --git a/versions/4.0/en/concepts/scene/light/additivelights/drawCall.png b/versions/4.0/en/concepts/scene/light/additivelights/drawCall.png new file mode 100644 index 0000000000..65cb910191 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/additivelights/drawCall.png differ diff --git a/versions/4.0/en/concepts/scene/light/additivelights/pass1.png b/versions/4.0/en/concepts/scene/light/additivelights/pass1.png new file mode 100644 index 0000000000..3951c220c0 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/additivelights/pass1.png differ diff --git a/versions/4.0/en/concepts/scene/light/additivelights/pass2.png b/versions/4.0/en/concepts/scene/light/additivelights/pass2.png new file mode 100644 index 0000000000..a13ad81e76 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/additivelights/pass2.png differ diff --git a/versions/4.0/en/concepts/scene/light/additivelights/pass3.png b/versions/4.0/en/concepts/scene/light/additivelights/pass3.png new file mode 100644 index 0000000000..98f80309ea Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/additivelights/pass3.png differ diff --git a/versions/4.0/en/concepts/scene/light/additivelights/usingLight.png b/versions/4.0/en/concepts/scene/light/additivelights/usingLight.png new file mode 100644 index 0000000000..e339d31af7 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/additivelights/usingLight.png differ diff --git a/versions/4.0/en/concepts/scene/light/light-bulbs.jpg b/versions/4.0/en/concepts/scene/light/light-bulbs.jpg new file mode 100644 index 0000000000..6ea594a3f0 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/light-bulbs.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/light-cd.jpeg b/versions/4.0/en/concepts/scene/light/light-cd.jpeg new file mode 100644 index 0000000000..346d24fbf1 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/light-cd.jpeg differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/ambient.md b/versions/4.0/en/concepts/scene/light/lightType/ambient.md new file mode 100644 index 0000000000..4c73e7d50f --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/lightType/ambient.md @@ -0,0 +1,23 @@ +# Ambient Light + +In life, intricate light and uneven object surfaces reflect off each other, making the entire environment illuminated as if by an even layer of light, this light is generally called **ambient light**, also known as **diffuse ambient light**. + +Because the ambient light can evenly illuminate all objects in the scene, it is often used to solve the problem of the backlight surface of the model is completely black, generally need to be used with other types of lights together. For example, if there is only directional light in the scene, it will look very dark at the backlight of the model. + +![ambient](ambient/ambient.png) + +Check **Scene** in the **Hierarchy** panel, and then set the ambient light property in the **ambient** component of the **Inspector** panel. + +> **Note**: since ambient light is undirected, it cannot produce shadows. + +## Ambient Light Properties + +![ambient panel](ambient/ambient-prop.png) + +| Property | Description | +| :--- | :--- | +| SkyLightingColor | Sets the sky color. | +| SkyIllum | Adjusts the sky brightness. | +| GroundLightingColor | Sets the color of the reflected light from the ground. | + +The ambient light can be used together with the sky box, for details, please refer to the [Skybox](../../skybox.md) documentation. diff --git a/versions/4.0/en/concepts/scene/light/lightType/ambient/ambient-prop.png b/versions/4.0/en/concepts/scene/light/lightType/ambient/ambient-prop.png new file mode 100644 index 0000000000..e6300390ba Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/ambient/ambient-prop.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/ambient/ambient.png b/versions/4.0/en/concepts/scene/light/lightType/ambient/ambient.png new file mode 100644 index 0000000000..b289dbff97 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/ambient/ambient.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/dir-light.md b/versions/4.0/en/concepts/scene/light/lightType/dir-light.md new file mode 100644 index 0000000000..6e3f3bbd7b --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/lightType/dir-light.md @@ -0,0 +1,83 @@ +# Directional Lights + +Directional Light is the most common type of light that simulates the light emitted by a source at infinity and is often used to simulate sunlight. + +![image](dirlights/dir-light.jpg) + +Because the distance between the light and the illuminated target is undefined (infinite distance), the lighting effect is not affected by the **position** and **orientation** of the light (as shown below, directional light produces the same lighting brightness in all planes). However, **rotation** affects the direction of directional light, which in turn affects the extent to which the model receives light and where the model produces shadows. This can be done with the [Rotate Gizmo](../../../../editor/toolbar/index.md#rotate-gizmo) in the upper left corner of the editor to adjust the direction of directional light. + +![image](dirlights/dir-light-scene.jpg) + +Adding directional lights to the scene can be done as described in [Adding Lights](index.md#adding-lights). + +> **Note**: Cocos Creator currently supports only one directional light. If adding more than one at the same time, the last one added will prevail. + +A directional light node `Main Light` will be created automatically by default when creating a new scene. + +For the interface of the directional light component, please refer to [DirectionalLight API](%__APIDOC__%/en/class/DirectionalLight). + +> **Note**: Starting with v3.5, **Parallel Light Shadows** are separated from the Scene Settings panel and do not set shadow properties globally. + +## Directional Light Properties + +![image](dirlights/dir-light-prop.png) + +| Property | Description | +| :------ | :-- | +| Color | Sets the light color. | +| UseColorTemperature | Whether to enable the color temperature. | +| ColorTemperature | Adjusts the color temperature. | +| StaticSettings | Sets static lighting, please refer to the [Lightmapping](../lightmap.md) documentation for details. | +| Illumination | Illumination in **lux (lx)**. | + +### Directional Light Shadow Properties + +![image](dirlights/dir-light-shadow-prop.png) + +| Property | Explanation | +| :--- | :--- | +| ShadowEnabled | Whether to enable the shadow effect. | +| ShadowPcf | Set the anti-aliasing level of the shadow edge, currently including **HARD**, **SOFT**, **SOFT_2X**. Please refer to the section **PCF Soft Shadow** below for details. | +| ShadowBias | Set the shadow offset value to prevent z-fitting. | +| ShadowNormalBias | Set the normal offset value. | +| ShadowSaturation | Set the shadow saturation. It is recommended to set as **1**. If it is necessary to reduce the saturation of the directional light shadows, it is recommended to do it by increasing the ambient light instead of adjusting this value. | +| ShadowInvisibleOcclusionRange | Set whether shadows from objects outside of the Camera's visible range are cast into the visible range, and if so, turns up the value. | +| ShadowDistance | Set the range of shadow effects displayed within the visible range of the Camera, with the shadow quality inversely proportional to the size of this value. | +| Enable CSM | Whether to enable Cascaded Shadow Map mode. | +| FixedArea | Whether to manually control the display of shadow effects within the visible range of the Camera. | + +#### Enable CSM Mode + +The Cascaded Shadow Maps (CSM) method solves this problem by providing depth textures in chunks based on the distance from the object to the observer. It splits the camera's view cone into several parts, and then generates separate depth maps for each part of the split. + +A higher quality shadow map is used for near scenes, and a lower quality shadow map is used for far scenes, with one of the two shadow maps chosen for use where they transition. This ensures that the observer sees higher quality shadows in the near area and lower quality shadows in the far area, because the distant objects occupy only a small fraction of the screen, while the near objects occupy a large fraction of the screen. + +- CSM OFF: + + ![image](../shadow/csm-off.png) + +- CSM ON: + + ![image](../shadow/csm-on.png) + +#### FixedArea Mode + +FixedArea mode is used to set whether to manually control the range of shadow effects displayed within the visible range of the Camera: + +- If this option is unchecked (default), the engine uses the same crop process and camera calculations as CSM (Cascaded Shadow Maps), calculating the range of shadows generated based on the orientation and position of the Camera. +- If this option is checked, the range of shadow generation is controlled according to the `Near`, `Far`, and `OrthoSize` properties set manually. The shadows follow the position of the directional light nodes and are distributed around the directional light bounding box, rather than following the camera. + +![image](dirlights/dir-light-fixed-shadow-prop.png) + +| Property | Explanation | +| :--- | :--- | +| ShadowFixedArea | Set whether to manually set the following properties to control the range of shadow effects displayed within the visible range of the Camera, as described in the **FixedArea Mode** section below. | +| ShadowNear | Set the near clipping plane of the main lights shadow camera. | +| ShadowFar | Set the far clipping plane of the main lights shadow camera. | +| ShadowOrthoSize | Set the ortho viewport size of the main lights shadow camera, with the shadow quality inversely proportional to the size of this value. | + +#### PCF Soft Shadow + +Percentage Closer Filtering (PCF) is a simple, common technique used to achieve shadow edge desampling, by smoothing shadow edges to eliminate jaggedness in shadow mapping. The principle is to sample around the current pixel (also called a fragment), then calculate the ratio of the sample closer to the lights compared to the fragment, use this ratio to scale the scattered light and specular light, and then color the fragment to blur the shadow edges. + +Cocos Creator currently supports **hard sampler (HARD mode)**, **4x sampler (SOFT mode)**, **9x sampler (SOFT_2X mode)**. The larger the magnification, the larger the sampling area and the more softer the shadow edges. diff --git a/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-fixedarea.png b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-fixedarea.png new file mode 100644 index 0000000000..2bfcb576fd Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-fixedarea.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-fixed-shadow-prop.png b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-fixed-shadow-prop.png new file mode 100644 index 0000000000..68b215a6ef Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-fixed-shadow-prop.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-panel.png b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-panel.png new file mode 100644 index 0000000000..d38d31542d Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-panel.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-prop.png b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-prop.png new file mode 100644 index 0000000000..7d9cc8d8f1 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-prop.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-scene.jpg b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-scene.jpg new file mode 100644 index 0000000000..95d8adcb49 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-scene.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-shadow-prop.png b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-shadow-prop.png new file mode 100644 index 0000000000..02ed89695f Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light-shadow-prop.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light.jpg b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light.jpg new file mode 100644 index 0000000000..c8c096791e Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/dirlights/dir-light.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/dirlights/spot-light-shadow-prop.png b/versions/4.0/en/concepts/scene/light/lightType/dirlights/spot-light-shadow-prop.png new file mode 100644 index 0000000000..84d44759f3 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/dirlights/spot-light-shadow-prop.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/index.md b/versions/4.0/en/concepts/scene/light/lightType/index.md new file mode 100644 index 0000000000..efc4a764d9 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/lightType/index.md @@ -0,0 +1,20 @@ +# Lights + +The light determines the color, color temperature, intensity, direction, and shadow effect of the light to which the object is exposed. The types of lights currently supported by Creator include: + +- [Directional Lights](dir-light.md) +- [Spherical Lights](sphere-light.md) +- [Spotlights](spot-light.md) +- [Ambient Light](ambient.md) + +## Adding Lights + +There are two ways to add lights: + +1. Click the **+** button in the top left corner of the **Hierarchy** panel, select **Light**, and then select the light type as needed to create a node with the corresponding type of **Light Component** to the scene. + + ![add light](index/add-light.png) + +2. In the **Hierarchy** panel, select the node to which you want to add a light, then click the **Add Component** button under the **Inspector** panel and select **Light** to select the desired light component to the node. + + ![add light2](index/add-light2.png) diff --git a/versions/4.0/en/concepts/scene/light/lightType/index/add-light.png b/versions/4.0/en/concepts/scene/light/lightType/index/add-light.png new file mode 100644 index 0000000000..135f0d33f6 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/index/add-light.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/index/add-light2.png b/versions/4.0/en/concepts/scene/light/lightType/index/add-light2.png new file mode 100644 index 0000000000..a2ef6ed771 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/index/add-light2.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/sphere-light.md b/versions/4.0/en/concepts/scene/light/lightType/sphere-light.md new file mode 100644 index 0000000000..3d95d3f512 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/lightType/sphere-light.md @@ -0,0 +1,35 @@ +# Spherical Lights + +Cocos Creator 3.x's spherical light is similar to v2.x's point light. + +Spherical light spreads light evenly in all directions, similar to the light produced by a candle. The intensity of light on an object decreases as the distance from the light increases, and is zero when the distance exceeds the set light effect range. + +In practical applications, it can be used to simulate lights such as torches, candles, and light bulbs to illuminate the surrounding environment within a certain distance. + +![sphere-light-edit](spherelight/sphere-light.jpg) + +The light location and color, as well as its illumination range can be clearly seen in the editor, as shown in the image below. The light range of the spherical light can be adjusted by modifying the `Range` property of the spherical light component in the **Inspector** panel. + +![sphere-light-edit](spherelight/sphere-light-edit.png) + +Adding spherical lights to the scene can be done as described in [Adding Lights](index.md#adding-lights). + +For the spherical light component interface, please refer to [SphereLight API](%__APIDOC__%/en/class/SphereLight). + +## Sphere Light Properties + +![image](spherelight/sphere-light-prop.png) + +| Property | Description | +| :---- | :---- | +| Color | Sets the light color. | +| UseColorTemperature | Whether to enable the color temperature. | +| ColorTemperature |Adjusts the color temperature.| +| Size | Sets the light size. | +| Range | Sets the range of light effect. | +| Term | Sets the type of light intensity unit, including **LUMINOUS_POWER** and **LUMINANCE**. +| LuminousPower | Luminous flux in **lumens (lm)**.
Effective when **Term** is set to **LUMINOUS_POWER**. | +| Luminance | Brightness in **Candela per square meter (cd/m2)**.
Effective when **Term** is set to **LUMINANCE**. | +| StaticSettings | Static lighting settings, please refer to [Lightmapping](../lightmap.md). | + +> **Note**: currently, the `Size` property of the spherical light does not take effect in actual operation, as well as the display of shadows is not supported for now, we will optimize it in subsequent versions, please pay attention to the update announcement. diff --git a/versions/4.0/en/concepts/scene/light/lightType/spherelight/sphere-light-edit.png b/versions/4.0/en/concepts/scene/light/lightType/spherelight/sphere-light-edit.png new file mode 100644 index 0000000000..5db157a9a1 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/spherelight/sphere-light-edit.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/spherelight/sphere-light-prop.png b/versions/4.0/en/concepts/scene/light/lightType/spherelight/sphere-light-prop.png new file mode 100644 index 0000000000..a2932b9234 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/spherelight/sphere-light-prop.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/spherelight/sphere-light.jpg b/versions/4.0/en/concepts/scene/light/lightType/spherelight/sphere-light.jpg new file mode 100644 index 0000000000..f1dcee109b Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/spherelight/sphere-light.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/spot-light.md b/versions/4.0/en/concepts/scene/light/lightType/spot-light.md new file mode 100644 index 0000000000..19462d21fa --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/lightType/spot-light.md @@ -0,0 +1,65 @@ +# Spotlights + +A **Spotlight** is a cone-shaped beam of light emitted from a point in one direction, similar to the light produced by a flashlight or stage lighting. Compared to other lights, spotlights have an additional `SpotAngle` property, which is used to adjust the light range of the spotlight. + +![spotlight](spotlight/spot-light.jpg) + +In the editor, one can clearly see the position, color and light range of the light as well as its spot angle, as shown in the following image. With the [Transform Gizmos](../../../../editor/toolbar/index.md) in the upper left corner of the editor, it is possible to adjust the position and direction of the spotlight. + +![spotlight](spotlight/spot-light-scene.jpg) + +See [Adding Lights](index.md#adding-lights) for how to add spotlights to a scene. + +For the Spotlight component interface, please refer to [SpotLight API](%__APIDOC__%/en/class/SpotLight). + +> **Note**: Starting with v3.5, **Spotlight Shadows** are separated from the Scene Settings panel and are no longer affected by the Global Shadows parameter. + +## Spotlight Properties + +![image](spotlight/spot-light-prop.png) + +| Property | Description | +| :------ | :--- | +| Color | Sets the color of the light. | +| UseColorTemperature | Whether to enable the color temperature. | +| ColorTemperature |Adjusts the color temperature. | +| Size | Sets the light size. | +| Range | Sets the range of light effect. | +| SpotAngle | Adjusts the spot angle to control the light range. | +| AngleAttenuationStrength | The larger the value, the softer the edge, and the smaller the value, the harder the edge. | +| Term | Sets the light intensity unit type, including **LUMINOUS_POWER** and **LUMINANCE**. +| LuminousPower | Luminous flux in **lumens (lm)**.
Effective when **Term** is set to **LUMINOUS_POWER**. | +| Luminance | Brightness in **Candela per square meter (cd/m2)**.
Effective when **Term** is set to **LUMINANCE**. | +| StaticSettings | Static lighting settings, please refer to the [Lightmapping](../lightmap.md) documentation. | + +### **Note**: +* Due to the significant performance overhead of real-time shadows in 3D scenes on mobile native platforms such as openharmony, harmonyNext, android, and ios, the engine by default does not allow multiple spot lights to enable shadows on these platforms. To lift this restriction, you need to call the relevant modification code before the engine completes rendering pipeline initialization. Refer to the following code example: + ```ts + import { _decorator, Component, macro, rendering } from 'cc'; + const { ccclass, property } = _decorator; + + // Lift the restriction on multiple spot lights enabling shadows on mobile platforms + var builder = rendering.getCustomPipeline(macro.CUSTOM_PIPELINE_NAME) as any; + builder._configs.isMobile = false; + + @ccclass('NewComponent') + export class NewComponent extends Component {} + ``` +* If the game enables MSAA anti-aliasing, it will also prevent multiple spot lights from enabling shadows on mobile native platforms. + +### Spotlight Shadow Properties + +![image](spotlight/spot-light-shadow-prop.png) + +| Property | Explanation | +| :--- | :--- | +| ShadowEnabled | Whether to enable the shadow effect. | +| ShadowPcf | Set the anti-aliasing level of the shadow edge, currently including **HARD**, **SOFT**, **SOFT_2X**. Please refer to the section **PCF Soft Shadow** below for details. | +| ShadowBias | Set the shadow offset value to prevent z-fitting. | +| ShadowNormalBias | Set the normal offset value. | + +#### PCF Soft Shadow + +Percentage Closer Filtering (PCF) is a simple, common technique used to achieve shadow edge desampling, by smoothing shadow edges to eliminate jaggedness in shadow mapping. The principle is to sample around the current pixel (also called a fragment), then calculate the ratio of the sample closer to the lights compared to the fragment, use this ratio to scale the scattered light and specular light, and then color the fragment to blur the shadow edges. + +Cocos Creator currently supports **hard sampler (HARD mode)**, **4x sampler (SOFT mode)**, **9x sampler (SOFT_2X mode)**. The larger the magnification, the larger the sampling area and the more softer the shadow edges. diff --git a/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light-prop.png b/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light-prop.png new file mode 100644 index 0000000000..ee1df2c401 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light-prop.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light-scene.jpg b/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light-scene.jpg new file mode 100644 index 0000000000..2334e5a24d Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light-scene.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light-shadow-prop.png b/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light-shadow-prop.png new file mode 100644 index 0000000000..b38952d612 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light-shadow-prop.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light.jpg b/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light.jpg new file mode 100644 index 0000000000..f9866e9bcc Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightType/spotlight/spot-light.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/lighting.png b/versions/4.0/en/concepts/scene/light/lighting.png new file mode 100644 index 0000000000..b759377766 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lighting.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap.md b/versions/4.0/en/concepts/scene/light/lightmap.md new file mode 100644 index 0000000000..caf9b64e95 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/lightmap.md @@ -0,0 +1,91 @@ +# Lightmapping + +The **Baking System** pre-calculates the lighting, shadows, etc. that a static object with a stable light will receive. The result of this calculation is stored in a texture map, which is called a **lightmap**. + +Cocos Creator automatically processes and uses the generated lightmap at runtime. In scenes with fixed lights, using lightmaps instead of real-time lighting calculations can reduce resource consumption and thus increase the efficiency of the scene. + +## Lightmapping Panel + +Click **Project -> Lightmapping** in the editor menu bar to open the Lightmapping panel. The panel consists of two pages, **Scene** and **Baked**. + +![bake result](./lightmap/lightmap-panel.png) + +- **Scene**: mainly used to configure the parameters related to generating the lightmap. +- **Baked**: mainly used to display the generated lightmap and its related information. + +For details, please see the **Generating Lightmaps** section below. + +### Property Description + +The description of each property on the **Scene** page is as follows: + +| Property | Description | +| :--- | :--- | +| MSAA | Multiple Sampling, optional values include: 1, 2, 4, 8 | +| Resolution | The resolution of the generated lightmap, optional values are 128, 256, 512, 1024, 2048 | +| Gamma | Gamma correction value | +| GIScale | Global lighting scaling factor | +| GISamples | Global illumination sampling factor | +| AOLevel | AO (Ambient Occlusion) level | +| AOStrength | AO intensity | +| AORadius | AO radius | +| AOColor | AO color | + +## Generating lightmaps + +1. Select the node with the light component in the **Hierarchy** panel, then set the **StaticSettings** of the light component in the **Inspector** panel, and check the `Bakeable` property (currently multiple [Directional Lights](./lightType/dir-light.md) are not supported). + + ![enable lightbake](./lightmap/light-bakeable.png) + + - **EditorOnly**: whether to take effect only in the editor + + - **Bakeable**: whether to bake static lighting + + - **CastShadow**: whether to cast static shadows + +2. Select the node with the [MeshRenderer component](./../../../engine/renderable/model-component.md) in the **Hierarchy** panel to generate the lightmap, then set **LightmapSettings** in the **Inspector** panel and check the `Bakeable` property. + + ![model lightmap settings](./lightmap/meshrenderer-bakeable.png) + + - **Bakeable**: whether to bake static lighting + + - **CastShadow**: whether to cast static shadows + + - **ReceiveShadow**: whether to receive static shadows + + - **LightmapSize**: the size of the model lightmap + + > **Notes**: to generate lightmaps for a model, there are two requirements: + > + > 1. When the artist creates a model resource, in addition to the UVs of the model itself, another set of UVs for lightmapping needs to be included. + > + > 2. The model's Materials need to have the **USE LIGHTMAP** rendering option turned on, for example: + > + > ![materials use lightmap](./lightmap/materials.png) + +3. lightmap UVs + Unlike texture UVs, lightmap UVs cannot overlap + + > **Notes**:Incorrect UVs produce errors: + > + > 1. UV of different planes are interlaced + > + > ![lightmap uv overlap](./lightmap/overlap_back.png) + > ![lightmap uv overlap](./lightmap/overlap_front.png) + > ![lightmap uv overlap](./lightmap/overlap_lightmap.png) + > + > 2. There is no reserved interval between UV blocks + > + > ![lightmap uv space](./lightmap/uvspace_lightmap.png) + +4. Open the **Lightmapping** panel and set the corresponding properties. Then click the **Lightmap Generate** button, a file storage dialog will pop up, you need to specify a folder (must be in the `assets` directory) to store the generated lightmap data information. Notice the baking progress log at the bottom of the **Lightmapping** panel. + + ![bake param](./lightmap/lightmap-generate.png) + +5. After baking, the generated lightmap, as well as the file name, size and other related information can be viewed on the **Baked** page of the **Lightmapping** panel. The generated lightmaps are automatically processed by the engine and do not need to be manipulated by the developer. + + ![bake result](./lightmap/lightmap-result.png) + + 1. **Bake result**: shows the generated lightmap after baking, in format **RGBE8**, with **R**/**G**/**B** options to view the corresponding channels of the lightmap as required. + 2. **Lightmap clear**: used to delete the generated lightmap and related information. + 3. **Information output panel**: shows the generated lightmap file name, size and other related information. diff --git a/versions/4.0/en/concepts/scene/light/lightmap/bake-menu.png b/versions/4.0/en/concepts/scene/light/lightmap/bake-menu.png new file mode 100644 index 0000000000..265a3e2ffe Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/bake-menu.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/light-bakeable.png b/versions/4.0/en/concepts/scene/light/lightmap/light-bakeable.png new file mode 100644 index 0000000000..dd986c3278 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/light-bakeable.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/lightmap-generate.png b/versions/4.0/en/concepts/scene/light/lightmap/lightmap-generate.png new file mode 100644 index 0000000000..34bbd0383e Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/lightmap-generate.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/lightmap-panel.png b/versions/4.0/en/concepts/scene/light/lightmap/lightmap-panel.png new file mode 100644 index 0000000000..99434dbc39 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/lightmap-panel.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/lightmap-result.png b/versions/4.0/en/concepts/scene/light/lightmap/lightmap-result.png new file mode 100644 index 0000000000..ad77c5f2b4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/lightmap-result.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/materials.png b/versions/4.0/en/concepts/scene/light/lightmap/materials.png new file mode 100644 index 0000000000..5a777e91c5 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/materials.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/meshrenderer-bakeable.png b/versions/4.0/en/concepts/scene/light/lightmap/meshrenderer-bakeable.png new file mode 100644 index 0000000000..d1f6927d4f Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/meshrenderer-bakeable.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/overlap_back.png b/versions/4.0/en/concepts/scene/light/lightmap/overlap_back.png new file mode 100644 index 0000000000..149465e671 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/overlap_back.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/overlap_front.png b/versions/4.0/en/concepts/scene/light/lightmap/overlap_front.png new file mode 100644 index 0000000000..4f89856ac8 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/overlap_front.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/overlap_lightmap.png b/versions/4.0/en/concepts/scene/light/lightmap/overlap_lightmap.png new file mode 100644 index 0000000000..f9e77b4d15 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/overlap_lightmap.png differ diff --git a/versions/4.0/en/concepts/scene/light/lightmap/uvspace_lightmap.png b/versions/4.0/en/concepts/scene/light/lightmap/uvspace_lightmap.png new file mode 100644 index 0000000000..9b4f9e4ad9 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lightmap/uvspace_lightmap.png differ diff --git a/versions/4.0/en/concepts/scene/light/lux-outdoor.jpg b/versions/4.0/en/concepts/scene/light/lux-outdoor.jpg new file mode 100644 index 0000000000..971e3ec7b3 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/lux-outdoor.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/pbr-lighting.md b/versions/4.0/en/concepts/scene/light/pbr-lighting.md new file mode 100644 index 0000000000..cb09eba094 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/pbr-lighting.md @@ -0,0 +1,58 @@ +# Physically Based Lighting + +The light parameters are described in Cocos Creator using photometric units. Based on the photometric units, all the relevant parameters of the light can be converted into real-world physical values. In this way, designers can adjust the **light intensity**, **color**, **range**, and other information based on lighting-related industrial parameters and the actual physical parameters of the real environment to make the overall lighting effect more in line with the real natural environment. + +![pbr lighting](pbrlighting/pbr-lighting.jpg) + +## Lights in the Real World + +The physically based lighting matches the description of lights in the real world. In real environments, light products have their own industrial parameters: + +![light bulb size](pbrlighting/light-bulb.jpg) + +The product packaging states many important industrial parameters of a bulb: +- **Luminous Flux** +- **Color Temperature** +- **Size** + +These three important parameters affect the performance of lights in the real world, and it is important to focus on the physical meaning of these three parameters below. + +## Photometric Units + +**Photometric Units** are used to calculate the intensity (size) and direction of light. + +- **Luminous Flux** + + The unit is **lumens (lm)**, the total light energy emitted by a light or received by an illuminated object per unit of time. Changing the size of the light will not affect the scene lighting effect. + +- **Luminance** + + The unit is **Candela per square meter (cd/m2)**, the total luminous flux emitted by a light per unit area in a given direction, per unit area. Changing the size of the light affects the scene lighting effect. + +- **Illuminance** + + The unit is **lux or lx**, the amount of luminous flux received per unit area. This value is affected by the distance the light travels, and for the same light, when the light is twice as far away, the illuminance is reduced to one-fourth of the original, in an inverse square relationship. + +In the real world, because the important physical parameters describing lights are different, we usually use **Luminous Flux** and **Luminance** to describe common household lights with illuminated areas, and **Illuminance** to describe sunlight. + +![light power](pbrlighting/light-power.jpg) + +## Color Temperature + +**Color Temperature** is the color of an absolute blackbody after it has been warmed from absolute zero (-273°C). + +Color temperature is an important property that affects the color of a light and is an optional property that also participates in the color component of the light when color temperature is enabled. + +The ambient color temperature also changes dynamically in real-world environments at different times of the day: + +![color temp of day](pbrlighting/color-temp-of-day.jpg) + +Refer to the following table: + +![kelvin](pbrlighting/kelvin.jpg) + +## Size + +Real-world lights have real physical dimensions, and for the **same luminous flux**, the size of the light affects **brightness** and **illuminance**. + +![light bulb size](pbrlighting/light-bulb-size.png) diff --git a/versions/4.0/en/concepts/scene/light/pbrlighting/color-temp-of-day.jpg b/versions/4.0/en/concepts/scene/light/pbrlighting/color-temp-of-day.jpg new file mode 100644 index 0000000000..31a1d979a2 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/pbrlighting/color-temp-of-day.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/pbrlighting/kelvin.jpg b/versions/4.0/en/concepts/scene/light/pbrlighting/kelvin.jpg new file mode 100644 index 0000000000..68607c7a6d Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/pbrlighting/kelvin.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/pbrlighting/light-bulb-size.png b/versions/4.0/en/concepts/scene/light/pbrlighting/light-bulb-size.png new file mode 100644 index 0000000000..bd9a644d4b Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/pbrlighting/light-bulb-size.png differ diff --git a/versions/4.0/en/concepts/scene/light/pbrlighting/light-bulb.jpg b/versions/4.0/en/concepts/scene/light/pbrlighting/light-bulb.jpg new file mode 100644 index 0000000000..668c5057b9 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/pbrlighting/light-bulb.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/pbrlighting/light-power.jpg b/versions/4.0/en/concepts/scene/light/pbrlighting/light-power.jpg new file mode 100644 index 0000000000..a370edb2b3 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/pbrlighting/light-power.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/pbrlighting/pbr-lighting.jpg b/versions/4.0/en/concepts/scene/light/pbrlighting/pbr-lighting.jpg new file mode 100644 index 0000000000..5e919970b4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/pbrlighting/pbr-lighting.jpg differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example.md b/versions/4.0/en/concepts/scene/light/probe/example.md new file mode 100644 index 0000000000..5062a78f87 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/probe/example.md @@ -0,0 +1,90 @@ +# IBL Example + +In Cocos Creator developers can combine image lighting based features. These features include. + +- via [skybox](../../skybox.md) with baked reflection convolution to provide better ambient reflection effects +- Bake lighting information into the environment with [Lightmapping](../lightmap.md) to bake lighting information into the map to improve lighting performance +- Bake lighting information into maps to improve lighting performance via [light-probe](./light-probe.md)/[reflection-probe](./reflection-probe.md) to detect reflections between objects + +This article will demonstrate how to bake image-based lighting into your scenes from an art asset worker's workflow. + +## Preparation + +Since both Light Probe and Reflection Probe are for physically based lighting models, please follow the PBR workflow when creating art resources. + +Please prepare the material file in advance using the following shaders; or import the model exported from the DCC tool via [Importing Models Exported from DCC Tools](../../../../asset/model/dcc-export-mesh.md). Importing to **Resource Manager** will automatically recognize the materials in the model and convert its shaders to PBR shaders supported by the engine. + +![effects](example/effects.png) ![effects](example/surface-standard-effect.png) + +- builtin-standard: built-in standard PBR shader +- dcc/imported-metallic-roughness: shader for models exported based on the metallic-roughness workflow +- dcc/imported-specular-glossiness: shader for models exported based on the specular-glossiniess workflow +- surface/standard: built-in standard surface PBR shader +- dcc/surface-imported-metallic-roughness: built-in shader for models exported from the metallic-roughness workflow based on the standard surface +- dcc/surface-imported-specular-glossiness: built-in shader for models exported from the specular-glossiniess workflow for standard surfaces + +It can also be manually adjusted to use the above standard shaders after the model has been imported into **Assets Manager**. + +Developers can also review the following documentation to understand the entire PBR workflow. + +- [Physically Based Lighting](../pbr-lighting.md) +- [Physically Based Rendering (PBR)](../../../../shader/effect-builtin-pbr.md) +- [Importing Models Exported from DCC Tools](../../../../asset/model/dcc-export-mesh.md). +- [FBX Smart Material Conversion](../../../../importer/materials/fbx-materials.md) + +## Bake Lights + +The [light-probe panel](light-probe-panel.md), and [lightmap](../lightmap.md) can be used for light baking to generate image-based lighting. + +### Bakeing Process + +As an example of a manually built scene. + +![scene](example/scene.png) + +- Add [light-probe](light-probe.md) + + ![light-probe](example/light-probe.png) + +- Add [reflection probes](relfection-probe.md) + + ![reflection-probe](example/reflection-probe.png) + +- Adjusting the properties of a node + - For the nodes that need to use the baked results, adjust their properties as follows: + + ![mobility](example/mobility.png) + + ![setting](example/probe-setting.png) + + - For the node to be baked + - Ensure that the **Mobility** property within its **Inspector** is **Static**. + + ![static](example/static.png) + + - Check the **Bake To Light Probe** and **Bake To Reflection Probe** properties of its **MeshRenderer** property and select **Reflection Probe** wisely + + ![relfection-probe](example/bake-option.png) + +- Open the **Reflection Probe** and **Light Probe** panels. + + - Click the **Bake** button on the above panel and wait for the baking process to finish. + + ![baking-panels](example/baking-panels.png) + +- Optionally, you can bake [lightmap] via the **Light Baking** panel (../lightmap.md): + + ![lightmap manel](example/lightmap-panel.png) + +- Within **Hierarchy** **Scene Node**, find [Skybox](../../skybox.md) on its **Inspector** component and adjust the corresponding properties to get better results + + ![skybox](example/skybox.png) + + - Adjust **Env Lighting Type** to **AUTOGEN_HEMISPHERE_DIFFUSE_WITH_REFLECTION** + - Click the **Bake** button on the **Skybox** component to bake the reflection convolution map. + + This will give you a more realistic ambient reflection lighting effect + +- Check the baking result: + + ![result](example/baking-result.png) diff --git a/versions/4.0/en/concepts/scene/light/probe/example/bake-option.png b/versions/4.0/en/concepts/scene/light/probe/example/bake-option.png new file mode 100644 index 0000000000..85113581e5 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/bake-option.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/baking-panels.png b/versions/4.0/en/concepts/scene/light/probe/example/baking-panels.png new file mode 100644 index 0000000000..9f1f80c000 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/baking-panels.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/baking-result.png b/versions/4.0/en/concepts/scene/light/probe/example/baking-result.png new file mode 100644 index 0000000000..11f9070bf8 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/baking-result.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/basic.png b/versions/4.0/en/concepts/scene/light/probe/example/basic.png new file mode 100644 index 0000000000..679b0d823b Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/basic.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/effects.png b/versions/4.0/en/concepts/scene/light/probe/example/effects.png new file mode 100644 index 0000000000..c259a50011 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/effects.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/light-probe.png b/versions/4.0/en/concepts/scene/light/probe/example/light-probe.png new file mode 100644 index 0000000000..a540ddb2f0 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/light-probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/lightmap-panel.png b/versions/4.0/en/concepts/scene/light/probe/example/lightmap-panel.png new file mode 100644 index 0000000000..dabf9de4a0 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/lightmap-panel.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/mobility.png b/versions/4.0/en/concepts/scene/light/probe/example/mobility.png new file mode 100644 index 0000000000..55af8a6b7b Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/mobility.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/probe-setting.png b/versions/4.0/en/concepts/scene/light/probe/example/probe-setting.png new file mode 100644 index 0000000000..94767bc455 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/probe-setting.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/reflection-probe.png b/versions/4.0/en/concepts/scene/light/probe/example/reflection-probe.png new file mode 100644 index 0000000000..7e63463705 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/reflection-probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/scene.png b/versions/4.0/en/concepts/scene/light/probe/example/scene.png new file mode 100644 index 0000000000..e4a0525763 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/scene.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/skybox.png b/versions/4.0/en/concepts/scene/light/probe/example/skybox.png new file mode 100644 index 0000000000..a8648e1557 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/skybox.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/static.png b/versions/4.0/en/concepts/scene/light/probe/example/static.png new file mode 100644 index 0000000000..bee765c372 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/static.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/example/surface-standard-effect.png b/versions/4.0/en/concepts/scene/light/probe/example/surface-standard-effect.png new file mode 100644 index 0000000000..556a0828b4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/example/surface-standard-effect.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/img/index.png b/versions/4.0/en/concepts/scene/light/probe/img/index.png new file mode 100644 index 0000000000..0862cae2e6 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/img/index.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/index.md b/versions/4.0/en/concepts/scene/light/probe/index.md new file mode 100644 index 0000000000..ecc64d4cf5 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/probe/index.md @@ -0,0 +1,40 @@ +# Imaged Based Lighting + +![preview](img/index.png) + +Since computing lighting in real time is too demanding for hardware, most of the games or 3D simulations use Image Based Lighting (IBL) for higher performance, which means that the lighting information is stored on the computer's storage media and the lighting is reconstructed by sampling the information in the graphics at runtime. + +In the engine, image based lighting usually consists of the following functions. + +## Light Mapping + +With [Lightmapping](../lightmap.md) developers can record the lighting information of the scene on the mapping and use it at runtime. Thus, the use of real time within the scene can be avoided or reduced and rendering efficiency can be improved. + +## Skybox + +In [Skybox](../../skybox.md), **Bake Reflection Convolution Map** can be used to generate a pre-convolved reflection effect for the environment map to improve the quality of the ambient lighting. + +## Light Probe + +Starting from v3.7, Cocos Creator supports [light-probe](light-probe.md) and [reflection-probe](reflection-probe.md). + +The light-probe is part of the global illumination. The probe is placed inside the scene to detect the light bouncing around the scene and store the offline results to improve the lighting effect and enhance the rendering quality. + +## Reflection Probe + +The [reflection-probe](reflection-probe.md) currently supports both real-time and baked cases. In the real-time case, developers can configure the reflection-probe to render the reflection effect of objects in real time. The baked case allows the + +By combining [Skybox](../../skybox.md), [Lightmapping](../lightmap.md), [Light Probe](./light-probe.md) and [Reflection Probe](reflection-probe.md) to get more realistic and reliable lighting effects with performance. + +## Contents + +This chapter will contain the following. + +- [Light Probe](light-probe.md) + - [Light Probe Panel](light-probe-panel.md) +- [Reflection Probe](reflection-probe.md) + - [Reflection Probe Panel](reflection-probe-panel.md) + - [Art workflow example](reflection-art-workflow.md) +- [Skybox](../../skybox.md) +- [lightmap](../lightmap.md) +- [Image Based lighting Example](example.md) diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe-panel.md b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel.md new file mode 100644 index 0000000000..e4008e55f4 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel.md @@ -0,0 +1,36 @@ +# Light Probe Panel + +From the top menu of the editor, select **Projects** -> **Light Baking** -> **Light Probe** to open the Light Probe Baking Panel. + +The **Light Baking Panel** will bake the [light-probe](light-probe.md) already laid out in the **Scene Editor**. After baking, the indirect light information of the static object will be recorded to the storage medium; all nodes configured with the **Movable** property will have access to more detailed and realistic indirect light effects. + +## Properties + +![light-probe-panel](light-probe-panel/bake-panel.png) + +| Properties | Description | +| :-- | :-- | +| **GIScale** | GI([Global Illumination](https://en.wikipedia.org/wiki/Global_illumination))scale factor | +| **GISample** | GI sample count of each light probe | +| **Bounces** | Number of light bounces, in the range [1, 4]
The meaning of bounce is the process of the current light source hitting an object and bouncing back to the probe.
If the number of light bounces exceeds this property, it will not be recorded in the baking result | +| **Reduceing Ringing** | In some cases, the light from the light probe may penetrate from bright to dark areas thus forming a ring. Modifying this property may improve the situation, but may result in unrealistic lighting | +| **Show Probe** | Whether to show light probes
The node with the **LightProbeGroup** component needs to be selected in the **Hierarchy**
![probe](light-probe-panel/probe.png) | +| **Show Wireframe** | Whether to display the connection between light probes
The node with the **LightProbeGroup** component needs to be selected in the **Hierarchy**
![wireframe](light-probe-panel/wireframe.png) | +| **Show Convex** | Whether to display the convex wrapping frame and vertex normals
The node with the **LightProbeGroup** component needs to be selected in the **Hierarchy**
![convex](light-probe-panel/convex.png)| +| **Bake Light Probe** | Bake light probes | +| **Clear Result** | Clear bake results | +| **Cancel** | Cancel the baking process, this button will only be enabled when **Bake Light Probe** is clicked | + +## Sample + +Click **Bake Light Probe** to bake the light probe, and click **Clear Result** to clear the baked result. + +You can click **Cancel** to cancel the baking process. + +The progress of baking can be viewed in the information panel below. + +![info](light-probe-panel/info.png) + +Developers can also use the **Show Probe**, **Show Wireframe**, and **Show Convex** options to see within the scene if the probe matches the expected results. + +See the [IBL Example](example.md) to see the art workflow. diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/bake-panel.png b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/bake-panel.png new file mode 100644 index 0000000000..0a1956b249 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/bake-panel.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/convex.png b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/convex.png new file mode 100644 index 0000000000..3aaf09cb30 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/convex.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/info.png b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/info.png new file mode 100644 index 0000000000..d2a80c9fb4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/info.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/probe.png b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/probe.png new file mode 100644 index 0000000000..c1022530a4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/wireframe.png b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/wireframe.png new file mode 100644 index 0000000000..083dd7c9df Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe-panel/wireframe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe.md b/versions/4.0/en/concepts/scene/light/probe/light-probe.md new file mode 100644 index 0000000000..bd9f0a946d --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/probe/light-probe.md @@ -0,0 +1,102 @@ +# Light Probes + +![result](light-probe/probe.png) + +The Light Probe is designed to provide high quality light information for objects in the scene by pre-calculating; it also provides light information for objects that require LOD. + +After hitting an object in a scene, light may bounce back to another object, and such bounces may occur multiple times. + +The following figure demonstrates how light bounces once in the scene and is captured by the light probe. + +![basic](light-probe/../example/basic.png) + +After the light hits the surface of object A, it is reflected to object B. Through the light probe, the reflection of this light from object B is captured on the map, and then the light is reconstructed at runtime to get the indirect reflected light information of static objects in the scene. + +Cocos Creator supports up to 4 bounces, please refer to [Light Probe Panel](light-probe-panel.md) for details. + +## Add light probe group + +In the **Main Menu** select **Nodes** -> **Light** -> **Light Probe Group** to add light probes to the scene. + +![add-component](light-probe/light-probe.png) + +## Properties + +![property](light-probe/probe-property.png) + +| Properties | Description | +| :-- | :-- | +| **Generating Method** | Generated in a way that currently only supports built-in | +| **Generating Min Pos** | The minimum value of the bounding box at the time of generation | +| **Generating Max Pos** | The maximum value of the bounding box at the time of generation | +| **Number Of Probes X** | Number of probes on the X-axis | +| **Number Of Probes Y** | Number of probes on the Y-axis | +| **Number Of Probes Z** | Number of probes on the Z-axis | +| **Edit Area Box** | Editing the generated enclosing box
Clicking this button will put you in the editing mode of the probe enclosing box in the scene, please refer to the editing documentation below for more detailed information
The editing process will affect the values of **Generating Min Pos** and **Generating Max Pos** above | +| **Generate Probes** | Generate Light Probes Clicking on the button will regenerate all probes and the old ones will be deleted The generated probes are uniform, if you need to adjust them manually, please click the decentralized **Enter Probe Edit Mode** button | +| **Enter Probe Edit Mode** | Enter edit probe mode
Click this button to select different probes in the scene editor with **left mouse button** and modify the probe position with **transform tool** | + +## Edit Probes + +### Edit Probe Range + +Clicking on **Edit Area Box** to edit the bounding box. The scene will be displayed as a **Green** box, which can be resized by clicking the green Gizmo on the box. + +![edit area box](light-probe/edit-area-box.gif) + +Click the **Done Edit** button to exit edit mode. + +![done edit](light-probe/done-edit.png) + +You can then click **Generate Probes** to regenerate the probes. + +### Generate Probes + +Probes can be generated by clicking the **Generate Probes** button. At this point the old probes will be deleted, if you need to re-bake the probes, please refer to the **Bake Light Probes** section below. + +![generate probes](light-probe/generate-probes.png) + +### Edit Probe Position + +After generating a probe, you can click the **Enter Probe Edit Mode** button to enter the probe editing mode. + +Only then the probe of the scene can be selected. + +![select](light-probe/select-probe.png) + +The position of the probes can be adjusted by dragging and dropping these Gizmo's with the mouse. + +![edit](light-probe/edit-probe.gif) + +## Use Probes + +Check **Use Light Probe** in the node's **Inspector** panel and set the node's **Mobility** property to **Movable** to enable node probes. + +![mobility](light-probe/mobility.png) + +![use light probe](light-probe/use-light-probe.png) + +When enabled, select the node within **Hierarchy** to see which probes the node is affected by. The white dots in the illustration are light probes. + +![probes](light-probe/node-probes.png) + +## Bake Light Probes + +For nodes that need to be baked, check the node's **Bake To Light Probe**. + +![bake to light probe](light-probe/bake-to-light-probe.png) + +After editing the light probe, you can open the [Light Probe Panel](light-probe-panel.md) by selecting **Project** -> **Light-Bake** -> **Light-Probe**. + +![result](light-probe/result.png) + +More examples can be found in [IBL Example](example.md). + +## Theoretical reference + +Usually light probes are calculated by means of spherical harmonic functions. For developers interested in technical details, we have also prepared some reference documents. + +- [Spherical Harmonic Lighting: The Gritty Details](http://www.cse.chalmers.se/~uffe/xjobb/Readings/GlobalIllumination/Spherical%20Harmonic%20Lighting%20-%20the%20gritty%20details.pdf) +- [Spherical harmonic lighting Wiki](https://en.wikipedia.org/wiki/Spherical_harmonic_lighting) +- [Fourier Transform Wiki](https://en.wikipedia.org/wiki/Fourier_transform) +- [Spherical Harmonic](https://en.wikipedia.org/wiki/Spherical_Harmonic#:~:text=.%20In%20mathematics%20and%20physical%20science%2C%20spherical%20harmonics,solving%20partial%20differential%20equations%20in%20many%20scientific%20fields.) diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/bake-to-light-probe.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/bake-to-light-probe.png new file mode 100644 index 0000000000..70525dedb2 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/bake-to-light-probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/done-edit.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/done-edit.png new file mode 100644 index 0000000000..570fa2b7de Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/done-edit.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/edit-area-box.gif b/versions/4.0/en/concepts/scene/light/probe/light-probe/edit-area-box.gif new file mode 100644 index 0000000000..5f2aa74e8c Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/edit-area-box.gif differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/edit-probe.gif b/versions/4.0/en/concepts/scene/light/probe/light-probe/edit-probe.gif new file mode 100644 index 0000000000..af09f34a78 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/edit-probe.gif differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/generate-probes.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/generate-probes.png new file mode 100644 index 0000000000..c1b83c142d Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/generate-probes.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/light-probe.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/light-probe.png new file mode 100644 index 0000000000..04bd4381d4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/light-probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/mobility.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/mobility.png new file mode 100644 index 0000000000..ce3d1265e1 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/mobility.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/node-probes.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/node-probes.png new file mode 100644 index 0000000000..3b23c51be4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/node-probes.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/probe-property.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/probe-property.png new file mode 100644 index 0000000000..ec1e973d75 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/probe-property.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/probe.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/probe.png new file mode 100644 index 0000000000..80f68402a8 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/result.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/result.png new file mode 100644 index 0000000000..46cd209a21 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/result.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/select-probe.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/select-probe.png new file mode 100644 index 0000000000..1576153cd3 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/select-probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/light-probe/use-light-probe.png b/versions/4.0/en/concepts/scene/light/probe/light-probe/use-light-probe.png new file mode 100644 index 0000000000..fd6482a280 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/light-probe/use-light-probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-art-workflow.md b/versions/4.0/en/concepts/scene/light/probe/reflection-art-workflow.md new file mode 100644 index 0000000000..a3948ec910 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/probe/reflection-art-workflow.md @@ -0,0 +1,53 @@ +# Reflection Probe Art Workflow + +## Bake Reflection Probe Workflow + +- Create **Reflection Probe** nodes within the scene + +- Modify the **Mobility** property of the node that needs to bake the reflection to **Static** + + ![static](reflection-probe/static.png) + +- Find **Reflection Probe Settings** by scrolling down on the **Inspector** of the node where the reflection needs to be baked, and adjust its corresponding property to. + + ![setting](reflection-probe/mesh-renderer-reflect-probe.png) + + - **Reflection Probe**: Select the type of reflection probe + - **Bake To Reflection Probe**: Check whether to bake the reflection information of this mesh renderer to the reflection probe related map + + For detail, please refer to [MeshRenderer Component Reference](../../../../engine/renderable/model-component.md) + +- Bake + + - Click the **Bake** button on the **Inspector** to bake the currently selected reflection probe. + + ![bake](reflection-probe/bake.png) + + - Select **Projects** -> **Light** -> **Reflection Probe** on the main menu to open the [Reflection Probe Panel](reflection-probe-panel.md) and bake by clicking the bake button on the panel. + +- Check baking results + + After baking is complete, **Asset Manager** creates mappings named starting with **reflectionProbe_** within the **Asset Manager**. Developers can see if these mappings meet expectations. + +For more examples, please refer to [IBL Example](example.md). + +## Real-time Reflection Probe Workflow + +- Build the scene as shown in the figure. + + ![scene](reflection-probe/plannar-scene.png) + +- Create **Reflection Probe** nodes in the scene: + + - Modify **Probe Type** to **PLANNAR** + - Configure the **Source Camera** property to be the **Main Camera** node created in the above step + + ![inspector](reflection-probe/plannar-probe-property.png) + +- Modify the **Reflection Probe** of the **MeshRenderer** property of the **Plane** node in the scene to **PLANNAR_REFLECTION**. + + ![inspector](reflection-probe/plane-reflection-probe-property.png) + +- At this point it can be observed that within the scene, the reflection of the plane changes. + + ![plannar-reflection-result](reflection-probe/plannar-reflection-result.png) diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel.md b/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel.md new file mode 100644 index 0000000000..908951315b --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel.md @@ -0,0 +1,23 @@ +# Reflection Probe Panel + +The Reflection Probe Baking panel can be opened via **Projects** -> **Light Baking** -> **Reflection Probe** on the main menu. + +![open-panel](reflection-probe-panel/open-panel.png) + +The Reflection Probe Baking panel bakes all nodes in the project that contain the [Reflection Probe](reflection-probe.md) component. + +The baked results are placed in the project's **Resource Manager** under the name **reflectionProbe_**. + +## Properties + +![panel](reflection-probe-panel/reflection-panel.png) + +| Properties | Description | +| :-- | :-- | +| **Bake All Reflection Probes** | Bake button, when clicked, will start baking all the reflection probes in the whole scene | +| **Clear All Bake Results** | Clear baked results, when clicked, will bake the results of reflection probes that already exist in the project | +| **Cancel** | Cancels the current baking process, which only takes effect when the bake button is pressed
![baking](reflection-probe-panel/baking.png) | + +## Art Workflow + +Please refer to [reflection-probe](reflection-probe.md) or [IBL Example](example.md) for the art workflow. diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel/baking.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel/baking.png new file mode 100644 index 0000000000..9f582f6d04 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel/baking.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel/open-panel.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel/open-panel.png new file mode 100644 index 0000000000..0b88de90e3 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel/open-panel.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel/reflection-panel.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel/reflection-panel.png new file mode 100644 index 0000000000..948bbfe4e4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe-panel/reflection-panel.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe.md b/versions/4.0/en/concepts/scene/light/probe/reflection-probe.md new file mode 100644 index 0000000000..0d2eef8f90 --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/probe/reflection-probe.md @@ -0,0 +1,52 @@ +# Reflection Probe + +Starting from v3.7, Cocos Creator supports reflection probes. + +A reflection probe is a component that takes reflected light from a selected range and applies it to the current scene using baking or realtime to improve the scene lighting confidence. + +You can create reflection probes within a scene by selecting **Light** -> **Reflection Probes** in the **Hierarchy** or on the top menu. + +![add-component](reflection-probe/add-reflect-probe.png) + +## Properties + +![property](reflection-probe/property.png) + +| Properties | Description | +| :-- | :-- | +| **Size** | The range of the reflection probe, which can be adjusted by manipulating Gizmo within the scene | +| **Probe Type** | Types of Reflection Probes
Options:
**CUBE**: Reflection probe with baking support
**PLANNAR**: Reflection probe with real-time reflection support
![probe-type](reflection-probe/probe-type.png)| +| **Resolution** | Resolution of each face of the cube mapping after reflective probe baking
Options: **Low_256x256**/**Medium_512x512**/**Hight_768x768**
This option is only available when **Probe Type** is **CUBE**.
![resolution](reflection-probe/resolution.png)| +| **Background Color** | Background color, effective only when **Probe Type** is **PLANNAR** | +| **Clear Flag** | The clear flag of the camera specifies which part of the frame buffer is to be cleared each frame.
Contains SOLID_COLOR: clears the color, depth, and stencil buffers SKYBOX: enables the skybox and only clears the depth
![clear-flag](reflection-probe/clear-flag.png)| +| **Visibility** | Visibility mask declaring the set of node hierarchies visible in the current reflection probe
Selectable via drop-down menu
![visibility](reflection-probe/visibility.png)| +| **Source Camera** | Specify the camera for real-time reflections
This property is only available when **Probe Type** is **PLANNAR** | +| **Bake** | Bake button, click it to bake the reflection probe + +### Probe Type + +![probe-type](reflection-probe/probe-type.png) + +There are two types of reflection probes for Cocos Creator. + +- **CUBE**:Bake the reflection information in the area onto a CUBE Map. + + ![cube](reflection-probe/cube.png) + + With the reflection probe selected as **CUBE**, developers can select the size of the final baked map via the **RESOLUTION** drop-down menu below. + +- **PLANNAR**:Real-time relection probe type. + + Commonly used to simulate the surface of water, mirrors, marble or wet floors, etc. + + ![plannar](reflection-probe/plannar.png) + + When the type of the reflection probe is modified to **PLANNAR**, the developer needs to configure the **Source Camera** property to determine which camera to use as the camera for the reflection probe. + +The **Size** property can be adjusted via Gizmo within the **Scene Editor** as a way to modify the range of the reflection probe. + +![edit](reflection-probe/edit-area-box.gif) + +## Art Workflow + +The two art workflows supported by Reflection Probe can be found in [Reflection Probe Art Workflow](./reflection-art-workflow.md) and [IBL Example](example.md). diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/add-reflect-probe.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/add-reflect-probe.png new file mode 100644 index 0000000000..523f86bb58 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/add-reflect-probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/bake-result.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/bake-result.png new file mode 100644 index 0000000000..25f40a0610 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/bake-result.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/bake.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/bake.png new file mode 100644 index 0000000000..f45923b3ca Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/bake.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/clear-flag.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/clear-flag.png new file mode 100644 index 0000000000..53ad29b4b5 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/clear-flag.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/cube.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/cube.png new file mode 100644 index 0000000000..4eef5eea91 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/cube.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/edit-area-box.gif b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/edit-area-box.gif new file mode 100644 index 0000000000..f16e1d4503 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/edit-area-box.gif differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/mesh-renderer-reflect-probe.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/mesh-renderer-reflect-probe.png new file mode 100644 index 0000000000..7c70d8483a Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/mesh-renderer-reflect-probe.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plane-reflection-probe-property.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plane-reflection-probe-property.png new file mode 100644 index 0000000000..5813c67182 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plane-reflection-probe-property.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-camera-config.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-camera-config.png new file mode 100644 index 0000000000..40863d64e7 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-camera-config.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-probe-property.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-probe-property.png new file mode 100644 index 0000000000..96ba9c3d90 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-probe-property.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-reflection-result.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-reflection-result.png new file mode 100644 index 0000000000..8b26fe39a4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-reflection-result.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-scene.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-scene.png new file mode 100644 index 0000000000..cd769af020 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar-scene.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar.png new file mode 100644 index 0000000000..6001be79f2 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/plannar.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/probe-type.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/probe-type.png new file mode 100644 index 0000000000..69abda969f Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/probe-type.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/property.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/property.png new file mode 100644 index 0000000000..093069f0c7 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/property.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/reflection-camera.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/reflection-camera.png new file mode 100644 index 0000000000..b1b43037d5 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/reflection-camera.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/resolution.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/resolution.png new file mode 100644 index 0000000000..961ffcf403 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/resolution.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/static.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/static.png new file mode 100644 index 0000000000..84ff2ac170 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/static.png differ diff --git a/versions/4.0/en/concepts/scene/light/probe/reflection-probe/visibility.png b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/visibility.png new file mode 100644 index 0000000000..209f5ea1b2 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/probe/reflection-probe/visibility.png differ diff --git a/versions/4.0/en/concepts/scene/light/shadow.md b/versions/4.0/en/concepts/scene/light/shadow.md new file mode 100644 index 0000000000..576fc1141f --- /dev/null +++ b/versions/4.0/en/concepts/scene/light/shadow.md @@ -0,0 +1,134 @@ +# Shadow + +In the 3D world, light and shadow have always been extremely important components that enrich the entire environment. High quality shadows can make the game world look more realistic. + +Cocos Creator 3.0 currently supports both **Planar** and **ShadowMap** shadow types. + +![shadow](shadow/shadowExample.png) + +## Enable Shadow Effect + +To enable the shadow effect for an object, proceed as follows: + +1. Check **Scene** in the **Hierarchy** panel, and then check the **Enabled** property in the **Shadows** component of the **Inspector** panel. + + ![enable-shadow](shadow/enable-shadow.png) + +2. Check **light** in the **Hierarchy** panel, and then check the **Shadow Enabled** property in the **Dynamic Shadow Settings** component of the **Inspector** panel. + + ![enable-shadow](shadow/enable-light-shadow.png) + +3. Select the 3D node that needs to display shadows in the **Hierarchy** panel, and then set the **Cast Shadows** property to **ON** in the **MeshRenderer** component of the **Inspector** panel. + + If the shadow type is **ShadowMap**, set the **Receive Shadows** property on the **MeshRenderer** component to **ON**. + +> **Note**: if the shadows are not displayed properly, adjust the direction of the directional light. + +## Shadow Type + +The shadow type can be set in the **Type** property of the **Shadows** component. + +### Planar Shadow + +The Planar shadow type is generally used for simpler scenes. + +![planar-properties](shadow/planar-properties.png) + +| Property | Description | +| :--- | :--- | +| **Enabled** | Whether to enable shadow effect | +| **Type** | Shadow type | +| **Saturation** | Set the shadow saturation, it is recommended to set it as **1**. If it is necessary to reduce the saturation of the directional light shadows, it is recommended to do it by increasing the ambient light instead of adjusting this value | +| **ShadowColor** | Shadow color | +| **Normal** | The normal line perpendicular to the shadow, used to adjust the slope of the shadow | +| **Distance** | The distance of the shadow in the direction of the normal to the origin of the coordinate | + +Adjust the direction of the directional light to adjust the position of the shadow. + +> **Note**: planar shadows are only cast on planar surfaces, not on objects, which means that the **ReceiveShadow** property in the **MeshRenderer** component is invalid. + +### ShadowMap + +ShadowMap renders the scene with the lights as the viewpoint. From the position of the lights, the places in the scene that are not visible are where the shadows are created. + +![shadow map panel details](shadow/shadowmap-properties.png) + +| Property | Explanation | +| :--- | :--- | +| **Enabled** | Whether to enable the shadow effect. | +| **Type** | Choose the shadow type. | +| **MaxReceived** | The maximum number of lights supported for shadow generation, default is 4, can be adjusted as needed. | +| **ShadowMapSize** | Set the texture size of the shadow, Currently supports **Low_256x256**, **Medium_512x512**, **High_1024x1024**, **Ultra_2048x2048** four kinds of precision textures. | + +> **Note**: starting with v3.3, the **Linear** and **Packing** options for Shadows in the **Inspector** panel have been removed, and Creator will automatically determine the hardware capabilities and choose the best way to render the shadows. + +ShadowMap receives and displays shadow effects generated by other objects when **ReceiveShadow** on the object **MeshRenderer** component is enabled. + +ShadowMap is generally used for scenes that require more realistic and complex light and shadow effects. The downside is that if the lights is not moved, then the previously generated Shadow Map can be reused, while once the lights is moved, then a new ShadowMap needs to be recalculated. + +## Shadow Properties + +After enabling shadows of type **ShadowMap**, the **Dynamic Shadow Settings** option will appear in the light source's **Inspector**. If not properly enabled, please refer to the **Enable Shadows** section above. + +The current engine support for **ShadowMap** across different light sources is as follows: + +| Light Type | Support | +| :-- | :-- | +| Directional Light | Supported | +| Spot Light | Supported | +| Sphere Light | Not Supported | + +### Directional Light Shadow Properties + +![image](./lightType/dirlights/dir-light-shadow-prop.png) + +| Property | Description | +| :------ | :-- | +| ShadowEnabled | Whether to enable directional light shadows | +| ShadowPcf | Sets shadow edge anti-aliasing level, currently supporting **HARD**, **SOFT**, **SOFT_2X**, **SOFT_4X**. See **PCF Soft Shadows** section below for details. | +| ShadowBias | Sets shadow offset value to prevent Z-Fighting | +| ShadowNormalBias | Sets normal offset value to prevent jagged edges on curved surfaces | +| ShadowSaturation | Adjusts shadow saturation, recommended to set to **1.0**. To reduce directional light shadow saturation, increase ambient light instead of adjusting this value | +| ShadowInvisibleOcclusionRange | Sets whether shadows from objects outside Camera's visible range should be cast into visible range. Increase value if needed | +| ShadowDistance | Sets shadow effect range within Camera's visible range. Shadow quality is inversely proportional to this value | + +For directional light usage, refer to [Directional Light](./lightType/dir-light.md). + +#### FixedArea Mode + +FixedArea mode controls whether to manually set the shadow effect range within Camera's visible range: + +- When unchecked (default), engine uses same clipping process and camera calculations as CSM (Cascaded Shadow Map) mode, calculating shadow range based on Camera's direction and position. +- When checked, shadow range is controlled by manually setting `Near`, `Far`, and `OrthoSize` properties. Shadows follow directional light node's position, distributed near its bounding box rather than following camera. + +![image](./lightType/dirlights/dir-fixedarea.png) + +| Property | Description | +| :------ | :-- | +| ShadowFixedArea | Whether to enable fixed area shadows | +| ShadowNear | Sets main light camera's near clipping plane | +| ShadowFar | Sets main light camera's far clipping plane | +| ShadowOrthoSize | Sets main light camera's orthographic viewport size. Shadow quality is inversely proportional to this value | + +### Spot Light Shadow Properties + +![image](./lightType/dirlights/spot-light-shadow-prop.png) + +| Property | Description | +| :------ | :-- | +| ShadowEnabled | Whether to enable spot light shadows | +| ShadowPcf | Sets shadow edge anti-aliasing level, currently supporting **HARD**, **SOFT**, **SOFT_2X**, **SOFT_4X**. See **PCF Soft Shadows** section below for details. | +| ShadowBias | Sets shadow offset value to prevent z-fighting | +| ShadowNormalBias | Sets normal offset value to prevent jagged edges on curved surfaces | + +For spot light usage, refer to [Spot Light](./lightType/spot-light.md). + +### PCF Soft Shadows + +Percentage Closer Filtering (PCF) is a simple, common technique for shadow edge anti-aliasing that smooths shadow edges to eliminate jagged artifacts in shadow maps. It works by sampling around the current pixel (fragment), calculating the proportion of samples closer to the light source compared to the fragment, then using this proportion to scale diffuse and specular light components before shading the fragment to achieve blurred shadow edges. + +Currently Cocos Creator supports **Hard Sampling**, **4x Sampling (SOFT mode)**, **9x Sampling (SOFT_2X mode)**, and **16x Sampling (SOFT_4X mode)**. Higher multipliers mean larger sampling areas and softer shadow edges. + +## Support dynamic batching to improve performance + +For models with `instancing` enabled in the material, the planar shadow will automatically draw with `instancing` as well, see the [Dynamic Batching](../../../engine/renderable/model-component.md#about-dynamic-batching) documentation for details. diff --git a/versions/4.0/en/concepts/scene/light/shadow/csm-off.png b/versions/4.0/en/concepts/scene/light/shadow/csm-off.png new file mode 100644 index 0000000000..f9f18ae337 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/shadow/csm-off.png differ diff --git a/versions/4.0/en/concepts/scene/light/shadow/csm-on.png b/versions/4.0/en/concepts/scene/light/shadow/csm-on.png new file mode 100644 index 0000000000..232f280df1 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/shadow/csm-on.png differ diff --git a/versions/4.0/en/concepts/scene/light/shadow/enable-light-shadow.png b/versions/4.0/en/concepts/scene/light/shadow/enable-light-shadow.png new file mode 100644 index 0000000000..5e3a98f25d Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/shadow/enable-light-shadow.png differ diff --git a/versions/4.0/en/concepts/scene/light/shadow/enable-shadow.png b/versions/4.0/en/concepts/scene/light/shadow/enable-shadow.png new file mode 100644 index 0000000000..cb889f1b36 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/shadow/enable-shadow.png differ diff --git a/versions/4.0/en/concepts/scene/light/shadow/planar-properties.png b/versions/4.0/en/concepts/scene/light/shadow/planar-properties.png new file mode 100644 index 0000000000..9e9286f903 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/shadow/planar-properties.png differ diff --git a/versions/4.0/en/concepts/scene/light/shadow/set-meshrenderer.png b/versions/4.0/en/concepts/scene/light/shadow/set-meshrenderer.png new file mode 100644 index 0000000000..4e4d627634 Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/shadow/set-meshrenderer.png differ diff --git a/versions/4.0/en/concepts/scene/light/shadow/shadowExample.png b/versions/4.0/en/concepts/scene/light/shadow/shadowExample.png new file mode 100644 index 0000000000..ac1f48447f Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/shadow/shadowExample.png differ diff --git a/versions/4.0/en/concepts/scene/light/shadow/shadowmap-properties.png b/versions/4.0/en/concepts/scene/light/shadow/shadowmap-properties.png new file mode 100644 index 0000000000..9b2d91c9da Binary files /dev/null and b/versions/4.0/en/concepts/scene/light/shadow/shadowmap-properties.png differ diff --git a/versions/4.0/en/concepts/scene/node-component.md b/versions/4.0/en/concepts/scene/node-component.md new file mode 100644 index 0000000000..bc2ff8fbdb --- /dev/null +++ b/versions/4.0/en/concepts/scene/node-component.md @@ -0,0 +1,114 @@ +# Nodes and Components + +The workflow of Cocos Creator is centered on component-based development, also known as an **Entity-Component System**, which simply means that the various elements of the game are built in a combinatorial rather than an inherited manner. + +In Cocos Creator, a **Node** is an entity that hosts a component, and we mount **Component** with various functions on it to give it a variety of representations and functions. Let's see how to create nodes and add components to a scene. + +## Nodes + +Nodes are the basic building blocks of a scene. Nodes are organized in a tree-like relationship, and each node can have multiple children: + +![nodes](scene/nodes.jpg) + +Nodes have the following properties: + +- A node contains a set of base attributes (Position, Rotation, Scale), and nodes are organized together by a set of relative transformation relationships, as described in [coordinate systems and node transformation properties](./coord.md). +- The update order between nodes is cascading. The update of child nodes depends on the parent node, and child nodes follow the parent node transformations. +- Components can be added to a node to associate multiple components with the node. + +### Create Nodes + +The quickest way to get a node with a specific function is to use the **Create Node** button in the top left corner of the **Hierarchy** panel. Take the simplest example of creating a Sphere node by clicking on the **+**, creating a Node button in the upper left corner and then selecting **Create 3D Object -> Create Sphere**. + +![create](scene/create.png) + +Notice the newly added Sphere node in the **Scene** and **Hierarchy** panel. The new node is named `Sphere` by default, indicating that it is a node whose functionality is primarily provided by the **Sphere** component. Try clicking the **Create Node** button again to select another node type and see that they will be named and behave differently. Also, note that creating a UI node automatically creates a Canvas node as the root node of the UI node, as described in the document [UI Structure Description](../../2d-object/ui-system/index.md). + +For more information about the operations of single-select, multi-select, copy, delete, etc. of nodes in the **Hierarchy** panel, please refer to the [Hierarchy Panel](../../editor/hierarchy/index.md) documentation. + +To create nodes dynamically in a script, refer to the [Create and Destroy Nodes](../../scripting/create-destroy.md) documentation. + +## Node Properties + +![property](node-tree/node-property.png) + +- **Mobility**:Mobility of nodes. Different mobility can lead to different properties and performance of nodes in terms of illumination + + ![mobility](node-tree/mobility.png) + + For nodes containing light components, Mobility behaves slightly differently, as described separately below. + - For nodes holding light components + - Static: bakes direct and indirect light, and does not participate in the calculation at runtime after baking + - Stationary: only indirect light is baked, and only direct light is calculated at runtime + - Movable: does not bake, only calculates direct light at runtime + - For nodes with MeshRenderer component + - Static & Stationary Static objects: can use light mapping + - Movable dynamic objects: can use light probes + In general it is possible to add a light component to a node that holds a MeshRenderer component, but it is not recommended, consider separating multiple nodes to achieve such a requirement. + +- **Layer**:Sets the visibility capability of the node. Please refer to the following **Setting the visibility of nodes** for details. + +## Components + +What are components and how do they relate nodes? + +Select the `Sphere` node, created above, and notice what the **Inspector** panel shows: + +![Properties](scene/inspector.png) + +The part of the **Inspector** panel that starts with the `Node` title is the node's properties, which include information about the node's `Position`, `Rotation`, `Scale`, and other transformations. This is covered in detail in the [Coordinate Systems and Node Transformation Properties](coord.md) documentation. + +Starting with the `cc.MeshRenderer` title is the property of the MeshRenderer component mounted on the Sphere. In Cocos Creator, the **MeshRenderer** component is used to render static 3D models, where the `Mesh` property is used to specify the mesh resources used for rendering. As the Sphere node was just created the default is `sphere.mesh`. + +The `Materials` property is used to specify the [material](../../asset/material.md) used for rendering. You can try dragging any material from the **Assets** into the `Materials` property of the **Inspector** panel and you can see that the default material just became the specified material. + +> **Note**: any resources set on the component, such as `sphere.mesh` in this case, will be loaded automatically at the same time as the scene loads. Types of resources that need to be set and automatically loaded can also be declared in custom components, refer to the [Getting and loading resources](../../scripting/load-assets.md) documentation. + +In addition to adding components manually in the editor, they can also be added via scripts, for more details see the [Component creation and destruction](../../scripting/component.md) documentation. + +### Effect of Node Properties on Components + +Once the node and **MeshRenderer** component are combined, the rendering of the mesh resources can be controlled by modifying the node properties. The node can also be adjust according to the properties marked by the red line in the figure below, notice that the rotation and scaling of the model have changed. + +**Before adjustment**: + +![node property](scene/node-before.png) + +**After adjustment**: + +![node property](scene/node-after.png) + +As previously mentioned, component-based structure is combined to achieve functional extensions. The combination of the node and **MeshRenderer** component is shown in the following figure: + +![node component relationship](scene/node-chart.png) + +## Add Additional Components + +Multiple components can be added to a node to add more functionality to the node. + +For example, select the node `Sphere` in the above example, then click the **Add Component** button at the bottom of the **Inspector** panel and select **Light -> DirectionalLight** to add a **Directional Light** component. + +Next, set the properties of the **Directional Light** component, e.g.: adjust the `Color` property of the directional light to red, and notice that the color of the sphere model has changed, i.e. the **DirectionalLight** component that was added to the node has taken effect! + +![button property](scene/directional-light.png) + +> **Notes**: +> +> 1. This is just a brief example of a more obvious effect. It is not recommended to add a **DirectionalLight** component to a sphere node. +> 2. That only one renderable component can be added to a node. Renderable components include: **MeshRenderer**, **Sprite**, **Label**, **Graphics**, **Mask**, **RichText**, **UIStaticBatch**, etc. + +## Setting the visibility of nodes + +The engine uses more generic nodes that match the camera to control the visibility of nodes. When the `Layer` property set in the node is included in the [Visibility property](../../editor/components/camera-component.md) of the camera, the node can be seen by the camera, and it supports mixed rendering of 3D and 2D components. This allows more flexibility in controlling the visibility of node components and diversifying the group display. + +For details on the implementation of layers, please refer to the [layer](layer.md) documentation. + +### Set the layer property of the node + +![node layer gizmo](scene/node-layer-gizmo.png) + +The `Layer` property of a node is global and unique, but different nodes can set the same `Layer` property so that they are viewed by the same camera. Developers can use the `Layer` property built-in with the engine, or they can use a custom `Layer` property, which can be set by clicking the **Edit** button in the image below and going to the **Project Settings -> Layers** page. Please refer to the [Layer](layer.md) documentation for details. + +![node layer edit](scene/node-layer-edit.png) + +Where `User Layer 0` - `User Layer 19` are layer properties provided for user-defined settings, the user only needs to fill in the custom layer name after the layer to enable this layer property and edit it on the node. diff --git a/versions/4.0/en/concepts/scene/node-tree.md b/versions/4.0/en/concepts/scene/node-tree.md new file mode 100644 index 0000000000..98c7add8e9 --- /dev/null +++ b/versions/4.0/en/concepts/scene/node-tree.md @@ -0,0 +1,43 @@ +# Node Hierarchy and Rendering Order + +Combining Nodes and Components together creates all kinds of images, texts and interactive elements in a scene. When there are more and more elements in the scene, we can use the **Hierarchy** panel to arrange their hierarchy and rendering order to make things organized. + +## Hierarchy Panel + +When creating and editing nodes, the **Scene** panel can display an intuitive visualization of scene elements. The hierarchical relationships between nodes need to be checked and manipulated using the **Hierarchy** panel. Please refer to the [Hierarchy panel](../../editor/hierarchy/index.md) documentation first to learn how to use it. + +## Node Trees + +The complete logical relationship between nodes established by the operations of the **Hierarchy** panel or runtime scripts is called a **node tree**. + +Using a simple game scenario demonstrates what a node tree is. The following picture includes a background image, a main character (the blob), a title, a springboard, diamonds and a button to start the game. + +![rolling-ball](node-tree/rolling-ball.png) + +Each visual element is a node. Usually all nodes are not laid flat on the scene, but rather, organized into a node tree according to a certain classification and order (e.g.: according to our own preferences). Example: + +![node-tree](node-tree/node-tree.png) + +The nodes displayed in the upper level are referred to as parent nodes and those displayed in the lower level are referred to as children. In the **Hierarchy** panel, the node tree in the above figure would look like this: + +![in_hierarchy](node-tree/in_hierarchy.png) + +In Cocos Creator 3.0, UI nodes require any parent node to have at least one **UITransform** component, if it does not comply with the rules, a Canvas node will be automatically added as its parent, the node tree in the above figure puts all UI nodes under the Canvas node. Next, parent nodes are created according to the category and put nodes of the same category under one parent node to build the node tree. + +In real game projects, other methods (such as game logic) can be used to organize the node tree as needed. + +## Rendering Order of Nodes + +The rendering of 3D nodes is related to the Z-coordinate value of the distance between the node and the camera, transparency, etc. + +The rendering and occlusion relationship of UI nodes, on the other hand, is influenced by the node tree, which is rendered in order of node arrangement from top to bottom in the **Hierarchy** panel, meaning that nodes displayed above the list are occluded by the nodes below them in the scene. Therefore, the child nodes will always cover the parent nodes. For details, please refer to the [UI rendering ordering rules](../../ui-system/components/engine/priority.md) documentation. + +Other rendering-related references can be found in. + +- [Graphics Rendering](../../module-map/graphics.md) +- [Particle Renderer](../../particle-system/renderer.md) +- [Model Group Rendering](../../engine/renderable/model-component.md#model-group-rendering) + +## Performance Considerations + +> **Note**: although it is said that the parent node can be used to organize logical relationships or even as a container to host child nodes, the scene loading speed will be affected when there are too many nodes, avoid a large number of meaningless nodes when creating a scene and merge nodes with the same function as much as possible. diff --git a/versions/4.0/en/concepts/scene/node-tree/game-scene.jpg b/versions/4.0/en/concepts/scene/node-tree/game-scene.jpg new file mode 100644 index 0000000000..9a5da28d52 Binary files /dev/null and b/versions/4.0/en/concepts/scene/node-tree/game-scene.jpg differ diff --git a/versions/4.0/en/concepts/scene/node-tree/in_hierarchy.png b/versions/4.0/en/concepts/scene/node-tree/in_hierarchy.png new file mode 100644 index 0000000000..1e0581e48e Binary files /dev/null and b/versions/4.0/en/concepts/scene/node-tree/in_hierarchy.png differ diff --git a/versions/4.0/en/concepts/scene/node-tree/mobility.png b/versions/4.0/en/concepts/scene/node-tree/mobility.png new file mode 100644 index 0000000000..47081928db Binary files /dev/null and b/versions/4.0/en/concepts/scene/node-tree/mobility.png differ diff --git a/versions/4.0/en/concepts/scene/node-tree/node-op.jpg b/versions/4.0/en/concepts/scene/node-tree/node-op.jpg new file mode 100644 index 0000000000..d790243402 Binary files /dev/null and b/versions/4.0/en/concepts/scene/node-tree/node-op.jpg differ diff --git a/versions/4.0/en/concepts/scene/node-tree/node-property.png b/versions/4.0/en/concepts/scene/node-tree/node-property.png new file mode 100644 index 0000000000..97971345c7 Binary files /dev/null and b/versions/4.0/en/concepts/scene/node-tree/node-property.png differ diff --git a/versions/4.0/en/concepts/scene/node-tree/node-tree.png b/versions/4.0/en/concepts/scene/node-tree/node-tree.png new file mode 100644 index 0000000000..12671235c6 Binary files /dev/null and b/versions/4.0/en/concepts/scene/node-tree/node-tree.png differ diff --git a/versions/4.0/en/concepts/scene/node-tree/rolling-ball.png b/versions/4.0/en/concepts/scene/node-tree/rolling-ball.png new file mode 100644 index 0000000000..6915444476 Binary files /dev/null and b/versions/4.0/en/concepts/scene/node-tree/rolling-ball.png differ diff --git a/versions/4.0/en/concepts/scene/scene-editing.md b/versions/4.0/en/concepts/scene/scene-editing.md new file mode 100644 index 0000000000..f768fbe3a0 --- /dev/null +++ b/versions/4.0/en/concepts/scene/scene-editing.md @@ -0,0 +1,95 @@ +# Build a Scene Image Using the Scene Panel + +This document will introduce the workflow and tips of using [Scene](../../editor/scene/index.md) panel to create and edit scene images. + +## Use the "Create Node" Menu to Quickly Add Basic Node Types + +When we start adding content to the scene, we typically start with the **Create Node** menu in the **Hierarchy** panel, which is the menu that pops up when you click the **+** button in the upper left corner, and select the base node type we need from several simple node categories and add it to the scene. + +When adding nodes, the node selected in **Hierarchy** panel will become the parent of the newly created node. If you select a node that is collapsed and then add a new node through the menu, you need to expand the node you just selected to see the newly added node. + +### Empty Node + +Select **Create Empty Node** in the **Create Node** menu to create a node that does not contain any components. Empty nodes can be used as containers for organizing other nodes or for mounting logic and control components written by the developer. Also in the following we will describe how to create controls that meet your specific requirements by combining empty nodes and components. + +### 3D Objects + +Select **Create 3D Object** in the **Create Node** menu to create some of the more basic static model controls that come with the editor, which currently include cub, cylinder, sphere, capsule, cone, torus, plane, and quad. If you need to create other types of models, you can refer to the [MeshRenderer component](../../engine/renderable/model-component.md). + +### UI Nodes + +UI nodes can be created by selecting **Create UI** from the **Create Node** menu. UI nodes in Creator 3.0 require that any parent node must have at least one **UITransform** component. And every UI node itself will also have a **UITransform** component. + +So the Canvas node is the **root node** of UI rendering, and all rendering-related UI nodes are placed under the Canvas, which has the following benefits: + +- Canvas can provide multi-resolution adaptive scaling, and using Canvas as the root node can ensure that our scenes will look good on larger or smaller screens, see [Multi-resolution adaptation scheme](../../ui-system/components/engine/multi-resolution.md). +- Canvas nodes are automatically centered according to the screen size, so UI nodes under Canvas will have the center of the screen as the origin of the coordinate system. In our experience, this simplifies the scene and UI setup (e.g.: having the button element's text appear right in the center of the button node by default), and makes it easier to script the UI node position control. + +#### 2D Rendering Nodes + +The **Create Node** menu allows you to create node types consisting of nodes and base renderable components like **ParticleSystem**, **Sprite**, **Label**, **Mask**, etc. + +The base 2D renderable component here cannot be replaced by a combination of other components. Note that only one renderable component can be added to each node, and repeated additions will result in an error. However, complex interface controls can be implemented by combining different rendering nodes, such as the UI control node below. + +#### UI Control Node + +Common UI controls including Button, Widget, Layout, ScrollView, EditBox, etc. nodes can be created from the **UI** category in the **Create Node** menu. + +UI nodes are mostly a combination of rendered nodes, for example, the **Button** node we created through the menu includes a button background node with a **Button** component + a **Sprite** component, and a label node with a **Label** component: + +![button-breakdown](scene-editing/button-breakdown.png) + +Creating nodes of the base type using the menu is the recommended way to quickly add content to the scene, and then we can edit the nodes created using the menu as needed to create the combinations we need. + +For more on UI nodes, see [UI Structure Description](../../2d-object/ui-system/index.md). + +### Attribution of Logical Nodes + +In addition to nodes with specific rendering and other tasks, it is recommended to have some nodes in the root directory of the scene that are only responsible for mounting scripts and executing game logic, without any rendering and other related content. Usually we place these nodes at the root level of the scene, side by side with Canvas nodes, so that other developers can find the game logic and bind relevant data in the first place when collaborating. + +## Tips to Improve the Efficiency of Scene Creation + +The **Scene** panel includes both 3D and 2D views. The 3D view is used for 3D scene editing, and the 2D view is used mainly for editing 2D elements such as UI nodes. The scene view can be switched via the 3D/2D button in the toolbar at the top left of the editor.
+The following shortcuts work for both views. + +- Select a node in the **Hierarchy** panel and double-click or press F to focus the node in the **Scene** panel. +- Pressing Cmd/Ctrl + D after selecting a node will copy and paste an identical node at the same location as the node, which can be used for efficiency when we need to make multiple similar nodes quickly. +- In **Scene**/**Hierarchy** panel, hold down the Cmd/Ctrl key and click on the node you want in order to select multiple nodes at the same time. The Shift key allows you to select nodes in succession, without selecting them one by one. + +### Align/Evenly Distribute Nodes + +When the **Scene** panel is in **2D** view, there is a row of buttons in the upper left corner that can be used to align or evenly distribute multiple nodes when they are selected. The specific rules are as follows: + +![alignment](scene-editing/alignment.png) + +Assuming that all three Label nodes are selected, the six alignment buttons from left to right will align these nodes in turn: + +- Top-aligned to the nearest upper border (not the upper border of the topmost node) +- Vertically centered, aligned to the overall horizontal center line +- Bottom-aligned, aligned to the nearest lower boundary +- Left-aligned to its closest left border +- Horizontally centered, aligned to the overall vertical center line +- Right-aligned, aligned to the nearest right border + +The 6 distribution buttons in the second half, from left to right, will align these nodes in turn: + +- Top distribution, evenly distributed along the top boundary of the node +- Vertically centered distribution, evenly distributed along the horizontal midline of the node +- Bottom distribution, evenly distributed along the lower boundary of the node +- Left distribution, evenly distributed along the left border of the node +- Horizontally centered distribution, evenly distributed along the vertical center line of the node +- Right distribution, evenly distributed along the right boundary of the node + +> **Note**: whether the left and right boundaries and the center line are determined at the beginning, or the reference when aligning/evenly distributing each node later, it is the center of the node bounding box or one of the boundaries rather than the position coordinates of the nodes. + +For example, in the figure below, when we align the three Label nodes with different widths to the right, we get the alignment of the right boundaries of the three node bounding boxes, not the `x` coordinates of the three node positions becoming the same. + +![align to right](scene-editing/align-to-right.png) + +## Scene Display Effects + +There is also support for setting the Skybox, Global Fog, and Shadows in the scene to better enrich the scene and render and display the scene environment. For details, please refer to: + +- [Skybox](./skybox.md) +- [Global Fog](./fog.md) +- [Shadows](./light/shadow.md) diff --git a/versions/4.0/en/concepts/scene/scene-editing/align-to-right.png b/versions/4.0/en/concepts/scene/scene-editing/align-to-right.png new file mode 100644 index 0000000000..d621bdbf76 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene-editing/align-to-right.png differ diff --git a/versions/4.0/en/concepts/scene/scene-editing/alignment.png b/versions/4.0/en/concepts/scene/scene-editing/alignment.png new file mode 100644 index 0000000000..a152de01b1 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene-editing/alignment.png differ diff --git a/versions/4.0/en/concepts/scene/scene-editing/button-breakdown.png b/versions/4.0/en/concepts/scene/scene-editing/button-breakdown.png new file mode 100644 index 0000000000..0b0bb80167 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene-editing/button-breakdown.png differ diff --git a/versions/4.0/en/concepts/scene/scene.md b/versions/4.0/en/concepts/scene/scene.md new file mode 100644 index 0000000000..0453a4852e --- /dev/null +++ b/versions/4.0/en/concepts/scene/scene.md @@ -0,0 +1,12 @@ +# Scene structure + +__Cocos Creator__ adds a 3D scene structure to __Creator’s__ __EC (entity component) framework__. The 3D scene is represented by __RenderScene__. The corresponding __Component__ in the __EC structure__ references the *Model*, *Camera*, and *Light* are maintained in `RenderScene`. Other elements are also linked together through __Node__, including the *Transformations* in `RenderScene` is also manipulated through the __Node API__. + +> __Note__: the differences between __Scene__ in __EC structure__ and __RenderScene__ in a __3D__ scene structure. __Scene__ in __EC structure__ is +the logical organization structure of the __Node hierarchy__. __RenderScene__ in __3D__ is the organization structure of scene rendering elements. Elements in __EC scene__'s contain references to the correspond rendering objects in `RenderScene`. + +The relationship between __EC structure__ and __3D__ scene structure is shown in the following figure: + +![ec & scene](scene/ecs-scene.jpg) + +The entire __3D__ scene structure is encapsulated under __Component__, and the organizational relationship is established through __Node__. This is completely transparent relationship between __EC structure__ and a __3D__ scene. diff --git a/versions/4.0/en/concepts/scene/scene/create.png b/versions/4.0/en/concepts/scene/scene/create.png new file mode 100644 index 0000000000..f32562b34c Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/create.png differ diff --git a/versions/4.0/en/concepts/scene/scene/directional-light.png b/versions/4.0/en/concepts/scene/scene/directional-light.png new file mode 100644 index 0000000000..ed82696731 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/directional-light.png differ diff --git a/versions/4.0/en/concepts/scene/scene/ecs-scene.jpg b/versions/4.0/en/concepts/scene/scene/ecs-scene.jpg new file mode 100644 index 0000000000..895a266640 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/ecs-scene.jpg differ diff --git a/versions/4.0/en/concepts/scene/scene/inspector.png b/versions/4.0/en/concepts/scene/scene/inspector.png new file mode 100644 index 0000000000..4e70ae296d Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/inspector.png differ diff --git a/versions/4.0/en/concepts/scene/scene/layer-edit.png b/versions/4.0/en/concepts/scene/scene/layer-edit.png new file mode 100644 index 0000000000..9e5d933ba6 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/layer-edit.png differ diff --git a/versions/4.0/en/concepts/scene/scene/layer-gizmo.png b/versions/4.0/en/concepts/scene/scene/layer-gizmo.png new file mode 100644 index 0000000000..68e8fae78c Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/layer-gizmo.png differ diff --git a/versions/4.0/en/concepts/scene/scene/node-after.png b/versions/4.0/en/concepts/scene/scene/node-after.png new file mode 100644 index 0000000000..c8da0ad838 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/node-after.png differ diff --git a/versions/4.0/en/concepts/scene/scene/node-before.png b/versions/4.0/en/concepts/scene/scene/node-before.png new file mode 100644 index 0000000000..0c59049360 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/node-before.png differ diff --git a/versions/4.0/en/concepts/scene/scene/node-chart.png b/versions/4.0/en/concepts/scene/scene/node-chart.png new file mode 100644 index 0000000000..5615cd6ada Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/node-chart.png differ diff --git a/versions/4.0/en/concepts/scene/scene/node-layer-edit.png b/versions/4.0/en/concepts/scene/scene/node-layer-edit.png new file mode 100644 index 0000000000..910a8dba2d Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/node-layer-edit.png differ diff --git a/versions/4.0/en/concepts/scene/scene/node-layer-gizmo.png b/versions/4.0/en/concepts/scene/scene/node-layer-gizmo.png new file mode 100644 index 0000000000..77226e00f1 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/node-layer-gizmo.png differ diff --git a/versions/4.0/en/concepts/scene/scene/nodes.jpg b/versions/4.0/en/concepts/scene/scene/nodes.jpg new file mode 100644 index 0000000000..2d998652d5 Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/nodes.jpg differ diff --git a/versions/4.0/en/concepts/scene/scene/world01.jpg b/versions/4.0/en/concepts/scene/scene/world01.jpg new file mode 100644 index 0000000000..3a7f31548e Binary files /dev/null and b/versions/4.0/en/concepts/scene/scene/world01.jpg differ diff --git a/versions/4.0/en/concepts/scene/skin.md b/versions/4.0/en/concepts/scene/skin.md new file mode 100644 index 0000000000..0db847590a --- /dev/null +++ b/versions/4.0/en/concepts/scene/skin.md @@ -0,0 +1,10 @@ +# Global skin material configuration panel + +![image](skin/ScenePanel.png) + +The intensity of the skin material is set globally. The skin material is turned on by default and cannot be turned off, which will not bring additional performance loss if no skin material is used in the scene. + +| Attributes | Descriptions | +| :---| :--- | +| **Enabled** | Whether to turn on skin material | +| **Sss Intensity** | Global skin subcutaneous scattering intensity setting | diff --git a/versions/4.0/en/concepts/scene/skin/ScenePanel.png b/versions/4.0/en/concepts/scene/skin/ScenePanel.png new file mode 100644 index 0000000000..1d4d7f5080 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skin/ScenePanel.png differ diff --git a/versions/4.0/en/concepts/scene/skybox.md b/versions/4.0/en/concepts/scene/skybox.md new file mode 100644 index 0000000000..7f1a46898a --- /dev/null +++ b/versions/4.0/en/concepts/scene/skybox.md @@ -0,0 +1,156 @@ +# Skybox + +The skybox in a video game is a cube that wraps around the entire scene and can render and display the entire scene environment very well. The Skybox can also contribute very important IBL ambient lighting the in PBR-based workflow. + +## Enabling Skybox + +Check the scene root node in the **Hierarchy** panel, then check the **Enabled** property in the **Skybox** component of the **Inspector** panel to enable the skybox. + + ![enable skybox](skybox/enable-skybox.png) + +The **Skybox** component properties are as follows: + +| Property | Description | +| :---| :--- | +| **Enabled** | Check this option to enable skybox. | +| **Env Lighting Type** | Environment lighting type. Please refer to the **Diffuse Illumination** section below for more information. | +| **UseHDR** | If this option is checked, HDR (High Dynamic Range) will be turned on, if not checked, LDR (Low Dynamic Range) will be used. For details, please refer to the section **Switching HDR/LDR Mode** below. | +| **Envmap** | Environment map, TextureCube type, see below for details on how to set it.
When this property is empty, the skybox uses and displays pixel texture by default. | +| **Reflection Convolution** | Click the bake button will generates a low resolution environment map and perform convolution calculation on this map, convolution map will be used for environment reflection. | +| **Reflection Map** | Automatically generated convolutional maps for environmental reflection, currently doesn't support manually editing. Rendering effects, please refer to the section **bake reflection convolution map** below.| +| **DiffuseMap** | The convolution map for advanced diffuse lighting. It's automatically generated and managed by the engine, currently doesn't support manually editing. This option is only shown when **Env Lighting Type** is **DIFFUSEMAP_WITH_REFLECTION**. | +| **Reflection Convolution** | Please refer to the **Reflection Convolution** section below for detail | +| **SkyboxMaterial** | Add custom material for the skybox. Please refer to the **Skybox Material** section below for detail | + +## Setting the Environment Map of the Skybox + +After enabling the Skybox, it is also necessary to set the environment map, which is used to generate ambient lighting in the scene. Drag and drop the map asset into the **Envmap** property box of the Skybox component, or click the arrow button behind the **Envmap** property box to select the desired map asset. If not set, the Skybox will use and display pixel maps by default. + +![envmap](skybox/envmap.png) + +The skybox supports the following environment map assets: + +1. A single texture of type TextureCube, which can be set in Creator. + + - Cube Cross images + + - PNG or HDR format images + +2. CubeMap in the form of image files + +3. CubeMap created manually in Creator by combining six texture maps + +### By Setting the Texture Assets of TextureCube Type + +1. To import a texture asset, drag and drop it directly into the **Assets** panel. + +2. Select the imported texture asset, set the **Type** property to **texture cube** in the **Inspector** panel on the right, then click the green checkbox in the upper right corner to save the settings. + + ![Set to TextureCube](skybox/texturecube.png) + +3. Check **Scene** in the **Hierarchy** panel, then drag the set texture asset to the **Envmap** property box of the **Skybox** component in the **Inspector** panel. + + ![Set environment map for skybox](skybox/set-envmap.png) + +The setup is done. The developer can directly see the set environment map of the skybox in the **Scene** panel. If the map is not displayed correctly, check if the value of **SkyIllum parameter** is too low, or **modify the Clear Flag** of Camera. + +### Use engine builtin resources + +In the internal database of the Assets Manager Panel, the engine provides some builtin TextureCube resources that developers can use on demand following the same instructions described above. + +![builtin skybox](skybox/builtin.png) + +#### SkyIllum Property + +The SkyIllum property can be found in the **Ambient** component of the **Inspector** panel by selecting the scene root node in the **Hierarchy** panel, with a default value of 20000. + +If the SkyIllum property is set **too low**, the environment map of the skybox may not be displayed correctly in the **Scene** panel. General: + +- When the SkyIllum property value is less than 300, the environment map of the Skybox will not be displayed properly. + +- When the SkyIllum property is 5000, the effect is equivalent to the light intensity of a moonlit night. + +#### Modifying ClearFlags of Camera + +If the environment map of the skybox is already displayed correctly in the **Scene** panel but still does not take effect after the project is run, you need to change the **ClearFlags** of the **Camera** component to **SKYBOX**: + +![skybox-camera](skybox/skybox-camera.png) + +### By Setting the CubeMap + +A cube map can be generated manually in Creator from six normal maps by following the steps below: + +1. Select all the six prepared texture assets in the **Assets** panel, and then set the **Type** property of these texture assets to **texture** in the **Inspector** panel, and click the green checkbox in the upper right corner. + + ![cubeMap-texture-type](skybox/cubemap-texture-type.png) + +2. Create a new CubeMap asset. Select the folder where you want to store CubeMap in the **Assets** panel, click the **+** button in the upper left corner, and then select **CubeMap**. Or you can right-click the folder where you want to store the CubeMap, and select **New -> CubeMap**. + + ![create CubeMap](skybox/create-cubemap.png) + +3. Drag and drop the six images you just set as texture type into the corresponding property box of the CubeMap, and click the green tick button on the top right then you are done. + + ![Set CubeMap](skybox/cubemap-properties.png) + + > **Notes**: + > + > 1. The property boxes in CubeMap that do not have a texture asset yet will be populated using the default asset. + > 2. The six property boxes in CubeMap **do not use the same texture**, otherwise they will not be displayed properly for some platforms. + +## Diffuse Illumination + +Creator supports the following three types of ambient diffuse lighting, which can be select in the pull-down menu of **Env Lighting Type**. + +![type](skybox/sky-box-type.png) + +The types are described as follows: + +1. **Hemisphere Diffuse**: when the **Env Lighting Type** options is **HEMISPHERE_DIFFUSE**, hemispheric light diffusion will be used. This method is controlled by the **SkyLightingColor** and **GroundLightingColor** properties in the **Ambient** component, and has higher rendering performance, but less detail and poor lighting directionality. **The properties are manually adjustable, but may become inconsistent with the environment map**. + + ![ambient-diffuse](skybox/hemisphere.png) + +2. **Hemisphere Diffuse And Environment Reflection**: when the **Env Lighting Type** options is **AUTOGEN_HEMISPHERE_DIFFUSE_WITH_REFLECTION**, diffuse reflection is controlled by the **SkyLightingColor** and **GroundLightingColor** properties in the **Ambient** component. It also reflects the specular reflection generated by the environment map. + + ![autogen-hemisphere](skybox/autogen-hemisphere.png) + +3. **Diffuse Convolution Map And Environment Reflection**: when the **Env Lighting Type** options is **DIFFUSEMAP_WITH_REFLECTION**, the convolution map diffuse will be used. This method is advanced diffuse reflection, which can correctly express the diffuse lighting generated by environment map, with better lighting directionality and details. However, the convolution map in the **DiffuseMap** property is automatically generated for diffuse reflection and does not allow manual editing. + + ![apply-diffuseMap](skybox/diffuse-map-with-reflection.png) + +The comparison between the **AUTOGEN_HEMISPHERE_DIFFUSE_WITH_REFLECTION** and **DIFFUSEMAP_WITH_REFLECTION** is more obvious from the GIF below,when the **Env Lighting Type** is **DIFFUSEMAP_WITH_REFLECTION** The backlight is dark, highlighting the overall sense of layering, light and shade contrast details are also greatly improved. + +![Compare](skybox/compare.gif) + +> **NOTICE**: When replacing the environment map in the **Envmap** property, Creator will automatically calculate the corresponding ambient lighting information, as well as the diffuse lighting (only CubeMap in the form of image files is supported, manually created CubeMap is not supported). + +## Toggling HDR/LDR mode + +The **UseHDR** option in the Skybox component is used to toggle the HDR/LDR mode, which is used when checked. + +![use-hdr](skybox/use-hdr.png) + +- HDR (High Dynamic Range): High dynamic range, with **photometric intensity of the lights** and **exposure parameters of the camera** to achieve a more realistic level of contrast between light and dark. If this mode is used, the intensity of all lights (including parallel light, point light, spot light, etc.) **will become photometric physical units** and ambient light cube map should use **HDR format images** to provide a high dynamic range data source. + +- LDR (Low Dynamic Range): Low dynamic range. If this mode is used, the **lights intensity becomes unitless** and no longer has any connection to photometry or camera exposure. This mode is suitable for scenarios where you want the original map color to be reflected without any color tinting. Ambient light cube map can be done using images in formats such as **PNG**. + +## Skybox Material + +![material](skybox/material.png) + +The default shader used for the skybox material is **Assets -> internal/pipeline/skybox.effect**, if you want to customize the material, you can refer to [New Material](.../.../asset/material.md) to create a new material and select [Custom Shader](.../.../.../shader/write-effect-overview.md). + +![cusom skybox material](skybox/custom-skybox-material.png) + +## Reflective Convolution + +Use GGX BRDF lighting model to generate a pre-filtered reflection result for the environment map, which will take into consideration the effect of roughness and store the final result in Mipmap in order of roughness (roughness from 0 to 1, with Mipmap level 0 for roughness 0). + +Since this algorithm takes into consideration the effect of roughness on the environment reflection (i.e., as roughness increases, the environment reflection becomes more blurred), it results in more realistic reflections for the PBR material. + +## Bake Reflection Convolution Map + +Click the ![bake](skybox/bake.png) button in the **Inspector** panel to bake the reflection convolution results. After the bake is complete, you can also choose to click the ![remove](skybox/remove.png) to delete it. + +The generated environment reflection convolution map will fill to the mipmaps for TextureCube, Sampling mipmap in the shader based on material roughness, thus providing a more realistic IBL effect, You can clearly see the comparison effect by the following GIF image. + +![Compare](skybox/convolution-map.gif) diff --git a/versions/4.0/en/concepts/scene/skybox/autogen-hemisphere.png b/versions/4.0/en/concepts/scene/skybox/autogen-hemisphere.png new file mode 100644 index 0000000000..490850b5ec Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/autogen-hemisphere.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/bake.png b/versions/4.0/en/concepts/scene/skybox/bake.png new file mode 100644 index 0000000000..0508854cc4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/bake.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/builtin.png b/versions/4.0/en/concepts/scene/skybox/builtin.png new file mode 100644 index 0000000000..396f475878 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/builtin.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/compare.gif b/versions/4.0/en/concepts/scene/skybox/compare.gif new file mode 100644 index 0000000000..ba7d6b376f Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/compare.gif differ diff --git a/versions/4.0/en/concepts/scene/skybox/convolution-map.gif b/versions/4.0/en/concepts/scene/skybox/convolution-map.gif new file mode 100644 index 0000000000..e5fbc2eb30 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/convolution-map.gif differ diff --git a/versions/4.0/en/concepts/scene/skybox/create-cubemap.png b/versions/4.0/en/concepts/scene/skybox/create-cubemap.png new file mode 100644 index 0000000000..8d00089d3c Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/create-cubemap.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/cubemap-properties.png b/versions/4.0/en/concepts/scene/skybox/cubemap-properties.png new file mode 100644 index 0000000000..9637da61b4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/cubemap-properties.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/cubemap-texture-type.png b/versions/4.0/en/concepts/scene/skybox/cubemap-texture-type.png new file mode 100644 index 0000000000..bfd5145c89 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/cubemap-texture-type.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/custom-skybox-material.png b/versions/4.0/en/concepts/scene/skybox/custom-skybox-material.png new file mode 100644 index 0000000000..9f2a26a012 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/custom-skybox-material.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/diffuse-map-with-reflection.png b/versions/4.0/en/concepts/scene/skybox/diffuse-map-with-reflection.png new file mode 100644 index 0000000000..e47e4dff14 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/diffuse-map-with-reflection.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/enable-skybox.jpg b/versions/4.0/en/concepts/scene/skybox/enable-skybox.jpg new file mode 100644 index 0000000000..92c9b77d5b Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/enable-skybox.jpg differ diff --git a/versions/4.0/en/concepts/scene/skybox/enable-skybox.png b/versions/4.0/en/concepts/scene/skybox/enable-skybox.png new file mode 100644 index 0000000000..801f14822c Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/enable-skybox.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/envmap.png b/versions/4.0/en/concepts/scene/skybox/envmap.png new file mode 100644 index 0000000000..11f17e8441 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/envmap.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/generate-rc.png b/versions/4.0/en/concepts/scene/skybox/generate-rc.png new file mode 100644 index 0000000000..56ec995c34 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/generate-rc.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/hemisphere.png b/versions/4.0/en/concepts/scene/skybox/hemisphere.png new file mode 100644 index 0000000000..67025c5d3e Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/hemisphere.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/material.png b/versions/4.0/en/concepts/scene/skybox/material.png new file mode 100644 index 0000000000..1f3d48646f Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/material.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/remove.png b/versions/4.0/en/concepts/scene/skybox/remove.png new file mode 100644 index 0000000000..75eca35630 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/remove.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/set-envmap.png b/versions/4.0/en/concepts/scene/skybox/set-envmap.png new file mode 100644 index 0000000000..08024c5a7a Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/set-envmap.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/sky-box-type.png b/versions/4.0/en/concepts/scene/skybox/sky-box-type.png new file mode 100644 index 0000000000..0546441ce4 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/sky-box-type.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/skybox-camera.png b/versions/4.0/en/concepts/scene/skybox/skybox-camera.png new file mode 100644 index 0000000000..880b9b1051 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/skybox-camera.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/texturecube.png b/versions/4.0/en/concepts/scene/skybox/texturecube.png new file mode 100644 index 0000000000..9912b3b253 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/texturecube.png differ diff --git a/versions/4.0/en/concepts/scene/skybox/use-hdr.png b/versions/4.0/en/concepts/scene/skybox/use-hdr.png new file mode 100644 index 0000000000..127fef4c82 Binary files /dev/null and b/versions/4.0/en/concepts/scene/skybox/use-hdr.png differ diff --git a/versions/4.0/en/editor/assets/extension.md b/versions/4.0/en/editor/assets/extension.md new file mode 100644 index 0000000000..dbfff20294 --- /dev/null +++ b/versions/4.0/en/editor/assets/extension.md @@ -0,0 +1,181 @@ +# Extending the Assets Panel + +## Extending the Right-Click Menu + +The right-click menu display mechanism is to get the live menu data before it is displayed. The extension pre-registers the `assets` module in its own `package.json` to get the right-click menu display events for the **Assets** panel, and returns the menu data to be displayed via the events, and then displays it uniformly. The registered menu is usually displayed after the existing menu. + +Assuming a location (`where`) in the **Assets** panel has a right-click menu event, the currently supported extended locations (`where`) are: + +- `createMenu` -- two entry points for creating assets: one is the **+** button in the top left corner of the panel; the other is the **Create** option in the right-click menu. +- `dbMenu` -- the asset database root node +- `assetMenu` -- asset general node +- `panelMenu` -- blank area of the panel + +Specific implementation steps: + +- Select **Extension** --> **Create Extension** in the menu bar at the top of the editor and create a new extension in the **Global/Projects** directory as needed. The extension package will then be generated in the `extensions` directory of the root/project directory. +- Open the `package.json` file of the extension package and configure the `contributions.assets.menu` property, where `methods` is introduced into the `assets-menu.js` file. Other properties such as `createMenu` are explicit declarations of `where` above, and the corresponding `onCreateMenu` is the method exported from `assets-menu.js`: + + ```json5 + // package.json + { + contributions: { + assets: { + menu: { + methods: '. /assets-menu.js', // This file is in the following demo example + createMenu: 'onCreateMenu', + assetMenu: 'onAssetMenu', + dbMenu: 'onDBMenu', + panelMenu: 'onPanelMenu', + }, + }, + }, + } + ``` + +- The `assets-menu.js` part of the code example in the **Demo example** at the end of the article is as follows: + + ```javascript + // assets-menu.js + exports.onCreateMenu = function (assetInfo) { + return [ + { + label: 'i18n:extend-assets-demo.menu.createAsset', + click() { + if (!assetInfo) { + console.log('get create command from header menu'); + } else { + console.log('get create command, the detail of diretory asset is:'); + console.log(assetInfo); + } + }, + }, + ]; + }; + + exports.onAssetMenu = function (assetInfo) { + return [ + { + label: 'i18n:extend-assets-demo.menu.assetCommandParent', + submenu: [ + { + label: 'i18n:extend-assets-demo.menu.assetCommand1', + enabled: assetInfo.isDirectory, + click() { + console.log('get it'); + console.log(assetInfo); + }, + }, + { + label: 'i18n:extend-assets-demo.menu.assetCommand2', + enabled: !assetInfo.isDirectory, + click() { + console.log('yes, you clicked'); + console.log(assetInfo); + }, + }, + ], + }, + ]; + }; + ``` + +- The `onCreateMenu(assetInfo)` function in `assets-menu.js` and similar functions are described as follows: + + - Parameter `assetInfo` Object + + - `displayName` String - the name of the asset to be displayed + - `extends` (optional) String[] - the class to inherit from + - `importer` String - the name of the importer + - `isDirectory` Boolean - whether it is a folder + - `instantiation` (optional) String - will carry this extension name if the virtual asset can be instantiated as an entity + - `imported` Boolean - whether the import is complete + - `invalid` Boolean - whether the import is failed + - `name` String - the name of the asset + - `file` String - the absolute path to the disk where the asset file is located + - `redirect` Object - jump to the asset + - `type` String - the asset type + - `uuid` String - the asset ID + - `readonly` Boolean - whether to be read-only + - `type` String - asset type + - `url` String - the address of the asset starting with db:// + - `uuid` String - asset ID + + - Return Value `MenuItem[]`, `MenuItem` Object + + - `type` (optional) String - optional, normal, separator, submenu, checkbox or radio + - `label` (optional) String - the text displayed + - `sublabel` (optional) String - the secondary text displayed + - `submenu` (optional) MenuItem[] - the submenu + - `click` (optional) Function - the click event + - `enable` (optional) Boolean - whether it is available, if not it will be grayed out style + - `visible` (optional) Boolean - whether to show + - `accelerator` (optional) String - Show shortcut keys + - `checked` (optional) Boolean - whether checked when type is `checkbox` / `radio` + +More properties can be found in the data format of the [electron menu-item](https://www.electronjs.org/docs/api/menu-item) documentation. + +The effect of implementing the extension is illustrated as follows: + +![extend-create-menu](img/extend-create-menu.png) + +## Extending Drag-and-Drop Recognition + +Recognizing an acceptable type requires the support of the editor UI component ``, where an important property is `type`, ``. Customize a drag-in type and inject it into the **Assets** panel's scope. If a `` element containing that custom type is subsequently dragged into the **Assets** panel from another editor panel, the **Assets** panel recognizes it and sends a message to the registrant (plugin) of the custom type. The registrant can then perform a custom action, such as creating a new set of assets. + +The specific implementation steps are roughly the same as the above **extension right-click menu**, open the extension's `package.json` file to do the corresponding configuration. + +```json5 +// package.json +{ + contributions: { + assets: { + drop: [ + { + type: 'my-defined-asset-type-for-drop', // corresponds to the usage of panel.html in the demo example + message: 'drop-asset', + }, + menu: ',] + menu: '. /assets-menu.js', + }, + }, + messages: { + 'drop-asset': { + methods: ['default.dropAsset'], // 'default' is the current extension's default panel + }, + }, +} +``` + +- The `panel.js` file in the **Demo example** at the end of the article: + + ```javascript + exports.methods = { + dropAsset(assetInfo, dragInfo) { + console.log(Editor.I18n.t('extend-assets-demo.drop.callback')); + console.log(assetInfo); + console.log(dragInfo); + }, + }; + ``` + + `assetInfo` parameter description: + + - `uuid` String - The UUID of the asset at the mouse release location when dragging an asset in the **Assets** panel. + - `type` String - the asset type + - `isDirectory` Boolean - whether the asset is a folder + +- The `panel.html` file in the **Demo example** at the end of the article: + + ```html + + Drag me to assets panel, and look conosole log. + + ``` + +## Demo examples + +For executable code for both extensions, please download the workingexamples diff --git a/versions/4.0/en/editor/assets/img/create.png b/versions/4.0/en/editor/assets/img/create.png new file mode 100644 index 0000000000..7569f34f79 Binary files /dev/null and b/versions/4.0/en/editor/assets/img/create.png differ diff --git a/versions/4.0/en/editor/assets/img/drag.png b/versions/4.0/en/editor/assets/img/drag.png new file mode 100644 index 0000000000..1f70ec2042 Binary files /dev/null and b/versions/4.0/en/editor/assets/img/drag.png differ diff --git a/versions/4.0/en/editor/assets/img/extend-assets-demo.zip b/versions/4.0/en/editor/assets/img/extend-assets-demo.zip new file mode 100644 index 0000000000..7dd7e5282b Binary files /dev/null and b/versions/4.0/en/editor/assets/img/extend-assets-demo.zip differ diff --git a/versions/4.0/en/editor/assets/img/extend-create-menu.png b/versions/4.0/en/editor/assets/img/extend-create-menu.png new file mode 100644 index 0000000000..510fcea2b9 Binary files /dev/null and b/versions/4.0/en/editor/assets/img/extend-create-menu.png differ diff --git a/versions/4.0/en/editor/assets/img/package-asset/exporting.png b/versions/4.0/en/editor/assets/img/package-asset/exporting.png new file mode 100644 index 0000000000..f67659081d Binary files /dev/null and b/versions/4.0/en/editor/assets/img/package-asset/exporting.png differ diff --git a/versions/4.0/en/editor/assets/img/package-asset/importing.png b/versions/4.0/en/editor/assets/img/package-asset/importing.png new file mode 100644 index 0000000000..0d676eb29f Binary files /dev/null and b/versions/4.0/en/editor/assets/img/package-asset/importing.png differ diff --git a/versions/4.0/en/editor/assets/img/preview.png b/versions/4.0/en/editor/assets/img/preview.png new file mode 100644 index 0000000000..eb1a6cbaa1 Binary files /dev/null and b/versions/4.0/en/editor/assets/img/preview.png differ diff --git a/versions/4.0/en/editor/assets/img/right-click.png b/versions/4.0/en/editor/assets/img/right-click.png new file mode 100644 index 0000000000..3a609fe8e7 Binary files /dev/null and b/versions/4.0/en/editor/assets/img/right-click.png differ diff --git a/versions/4.0/en/editor/assets/img/search-name.png b/versions/4.0/en/editor/assets/img/search-name.png new file mode 100644 index 0000000000..082b13deae Binary files /dev/null and b/versions/4.0/en/editor/assets/img/search-name.png differ diff --git a/versions/4.0/en/editor/assets/img/search-types.png b/versions/4.0/en/editor/assets/img/search-types.png new file mode 100644 index 0000000000..fb524308d7 Binary files /dev/null and b/versions/4.0/en/editor/assets/img/search-types.png differ diff --git a/versions/4.0/en/editor/assets/img/search-uuid.png b/versions/4.0/en/editor/assets/img/search-uuid.png new file mode 100644 index 0000000000..1ae821a2b7 Binary files /dev/null and b/versions/4.0/en/editor/assets/img/search-uuid.png differ diff --git a/versions/4.0/en/editor/assets/img/thumb.gif b/versions/4.0/en/editor/assets/img/thumb.gif new file mode 100644 index 0000000000..7d4d273a6b Binary files /dev/null and b/versions/4.0/en/editor/assets/img/thumb.gif differ diff --git a/versions/4.0/en/editor/assets/index.md b/versions/4.0/en/editor/assets/index.md new file mode 100644 index 0000000000..5566d59f3d --- /dev/null +++ b/versions/4.0/en/editor/assets/index.md @@ -0,0 +1,168 @@ +# Assets Panel + +The **Assets** panel is an important work area for accessing and managing project assets. When starting a game, **Import Assets** is usually a mandatory step. Use the **HelloWorld** template project when creating a new project, and notice that the **Assets** panel already contains some basic asset types. + +![panel action preview](img/thumb.gif) + +## Panel introduction + +The **Assets** panel can be divided into two main sections: **Toolbar** and **Asset List**. + +- The functions in **Toolbar** include **New Asset (+)**, **Sort by**, **Search Filter**, **Search Box**, **Collapse/Expand All** and **Refresh**. For details, please refer to the description below. + +- The **Asset List** presents the contents of the project's asset folder in a tree structure, which by default includes the **assets** and **internal** base repositories (DBs for short). + + - **assets** is similar to the file manager in the operating system, only assets placed in the `assets` directory of the project folder will be displayed here. For a description of the project folder structure, please refer to the [Project Structure](../../getting-started/project-structure/index.md) documentation. + - The **internal** contains the default built-in assets (read-only). Built-in assets can be copied, but cannot be added, deleted, or changed. It can be dragged and dropped into **assets** as an asset template, i.e. a new project asset is created. + +- The panel currently supports shortcuts for manipulating assets, including: + - **Copy**: Ctrl/Cmd + C + - **Paste**: Ctrl/Cmd + V + - **Duplicate**: Ctrl/Cmd + D, Ctrl + drag assets + - **Delete**: Delete + - **Up and down selection**: Up arrow, Down arrow + - **Previous hierarchy**: Left arrow + - **Next hierarchy**: Right arrow + - **Multi-select**: Ctrl/Cmd + Click + - **Multi-select continuously**: Shift + Click + - **Select All**: Ctrl/Cmd + A + - **Rename**: Enter/F2 + - **Cancel input**: Esc + +### Creating assets + +There are two ways to create an asset. +1. Click the **+** button at the top left of the **Assets** panel. +2. Right-click in the **Assets** panel and select **Create**. + +![create](img/create.png) + +> **Note**: when creating an asset, an **INPUT BOX** will first appear asking for the name of the new asset, which cannot be empty. + +When creating an asset, select the folder where to place the asset in the tree asset list, then the new asset will be created in the currently selected folder directory. If a folder isn't selected, the new asset will be created in the current root directory (`assets`) by default. + +For additional information about Assets, please refer to the [Asset](../../asset/index.md) documentation. + +### Selecting assets + +The following asset selection actions can be used in the asset list: + +- Click to select a single asset. The selected asset will be highlighted in **yellow on blue**. +- Double-click to open an asset, e.g.: scene, script, image; double-click on a folder to collapse/expand it +- Hold Ctrl/Cmd and select an asset to select multiple assets at once +- Hold Shift and select an asset to select multiple assets in a row + +For selected assets, you can move, delete, etc. + +### Moving assets + +After selecting an asset (multiple selections are possible), move the asset to another location by holding down the mouse and dragging it. When dragging the asset to be moved to the target folder, notice the folder where the mouse is hovering appears highlighted in yellow and has a blue box around it. Release the mouse at this point and the asset will be moved to the highlighted folder directory. + +![drag](img/drag.png) + +Asset movement also includes the following actions: + +- Drag and drop assets from **Assets** panel to the **Scene** panel or **Hierarchy** panel to generate nodes, currently supporting dragging and dropping `cc.Prefab`, `cc.Mesh`, `cc.SpriteFrame` assets. +- Drag and drop files from the **system's file manager** to the **Assets** panel list to import assets. +- Drag and drop a node from the **Hierarchy** panel to a folder in the **Assets** panel to save the node as a `cc.Prefab` asset. Please review the [Prefab Assets](../../asset/prefab.md) documentation. + +### Sorting assets + +The **Sort by** in the toolbar includes **Sort by name** and **Sort by type**. The current sorting method is recorded and will be maintained the next time the editor is opened. + +### Collapsing assets + +Collapses are divided into single collapses or all collapses with subsets: + +- The **Collapse/Expand All** button in the **Toolbar** works globally +- Click on the triangle icon of a parent asset (e.g.: folder) to expand or collapse its subsets. Use the shortcut key **Alt** and click on the triangle icon to expand or collapse all children of the asset + +The current collapsed state of the asset will be recorded and will be maintained the next time the editor is opened. + +### Searching assets + +The search function can limit the search type and specify the search field, both of which can be used to achieve a type filtering effect. Depending on the selected type/field, all the corresponding assets will be displayed in the panel. + +![search-types](img/search-types.png) + +- **Limit search types**: multiple selections are possible. The type is the asset type `assetType`, not the suffix name or importer name. + +- **Specify search fields**: the search name is not case sensitive and includes the following search types: + + 1. **search Name or UUID**, support searching for asset file extensions, such as `.png`: + + ![search-name](img/search-name.png) + + 2. **Search UUID** + + 3. **Search URL**, starting with the `db://` protocol. + + 4. **Find UUID usage**, used to find out which assets the UUID asset is used by. For example: + + ![search-uuid](img/search-uuid.png) + 5. **Only Show Imported Failed**: show asserts that failed to import. + 6. **Search Bundle**: search for bundle assets/ + +If you want to search only in a parent asset (e.g.: folder), right-click and select **Find in Folder** to narrow the search. + +Selecting an asset in the search result list and double-clicking on it is equivalent to the operation in normal mode. After clearing the search content, it will redirect to the selected asset. + +### Assets preview + +Selecting an asset in the **Assets** panel displays a thumbnail image of the asset in the **Assets Preview** panel. Selecting the folder where the asset is located, displays thumbnails of all assets under the folder for easy viewing. + +![preview](img/preview.png) + +## Right-click menu for assets + +After selecting a specific asset/folder, right-click on it to perform a series of actions on the asset in the pop-up menu: + +![right-click](img/right-click.png) + +- **Create**: the same function as the **Create** button in the **Assets** panel, it will add the asset to the currently selected folder, if the currently selected is an asset file, it will add the new asset to the same folder as the currently selected asset. +- **Copy**/**Cut**/**Paste**: copy/cut the selected asset (multiple options available) and paste it under that folder or another folder. +- **Duplicate**: duplicate the selected asset (can be multi-selected), and the duplicated asset will be in the same hierarchy as the selected asset. +- **Rename**: modify the asset name, please check the description below for details. +- **Delete**: delete the asset (can be multi-selected), or you can use the shortcut **Delete**. The asset will remain in the **Recycle Bin** of the system after deletion, and can be restored if necessary. +- **Select All**: selects all assets in the same hierarchy. +- **Search in Folder**: when using the search function, only search the assets in that folder. +- **Find UUID Usages**: find which assets the asset is referenced by by the asset's UUID. +- **Import New Files/New Folders**: import new files or folder to the project. +- **Import/Export Asset Package**: please refer to the introduction below for details. +- **Preview this scene in the browser**: only available for scene assets. +- **Reveal in Explorer (Windows) or Finder (Mac)**: opens the folder where this asset is located in the operating system's file manager window. +- **Reimport Asset**: updates the asset to the project's `./library` folder of the project, with multiple selections for bulk import. +- **Reveal in Library**: opens the asset in the `Library` folder of the project, please read the [Project Structure](../../getting-started/project-structure/index.md) documentation. +- **UUID/URL/PATH**: copies the UUID/URL/PATH of the asset and outputs it in the **Console** panel. + +Also for specific asset types, double-clicking on an asset allows you to enter the edit state of that asset, such as scene assets and script assets. + +### Rename asset + +Select the asset to rename, then right-click and select **Rename** to change the asset name, or use the shortcut keys **Enter** or **F2**. Click elsewhere in the panel or press the shortcut **Esc** to cancel the renaming. + +### Exporting asset package + +Select the asset to be exported and right-click on it, then select **Export Asset Package**. The **Export Asset** panel will automatically list the currently selected asset with its dependencies. If you don't need to export related dependent assets, uncheck **Include Dependency** in the lower left corner of the **Export Assets** panel. + +After determining the assets to be exported, click the **Export** button, a file storage dialog will pop up, the user needs to specify a folder location and file name, click **Save**, and a **filename.zip** file will be generated, containing all the exported assets. + +> **Note**: the extension will automatically break the assets into a zip package, which is only available for use with the **Import Asset Package** extension. + +![exporting](img/package-asset/exporting.png) + +### Importing asset package + +Select and right-click on the folder you want to import assets from, or right-click on the **blank space** in the **Assets** panel, then select **Import Package** and select the zip package exported by **Export Package** in the pop-up file browse dialog. The assets in the zip package will be automatically parsed to the pop-up **Import Asset Package** panel. + +During the import process, the user will be asked to confirm the imported assets again, uncheck some assets to remove the ones you don't want to import. + +![importing](img/package-asset/importing.png) + +> **Notes**: +> 1. Imported zip packages are only supported by Cocos Creator 3.0.0 and above using **Export Asset Package**. +> 2. Importing scripts with the same class name is not supported. + +## Extending the Assets Panel + +Currently supported extensions include **right-click menu** and **drop-in recognition**, please refer to the [Extending the Assets Panel](./extension.md) documentation. diff --git a/versions/4.0/en/editor/components/camera-component.md b/versions/4.0/en/editor/components/camera-component.md new file mode 100644 index 0000000000..7b36aff32d --- /dev/null +++ b/versions/4.0/en/editor/components/camera-component.md @@ -0,0 +1,77 @@ +# Camera + +The __Camera__ in a game is the main tool used to capture __Scenes__. The visible range of the __Camera__ is controlled by adjusting camera-related parameters. The __Camera__ is represented as follows in the __Cocos Creator__ editor: + +![camera](camera/camera.png) + +The __Camera__'s visual range is composed of __6__ planes forming a __Frustum__, a __Near Plane__, and a __Far Plane__ to control the visible distance and range of near and far distance, at the same time, they also constitute the size of the viewport. + +![camera view](camera/camera-view.gif) + +To use `Camera`, please refer to the [Camera API](%__APIDOC__%/en/class/Camera). + +## Camera component + +The __Camera Component__ is an important functional component that we use to present a __Scene__. + +![camera component](camera/camera-component.png) + +| Property | Description | +|:-------|:---| +| __Priority__ | Render priority of the camera. High-priority __Camera__'s will be rendered first in the rendering process | +| __Visibility__ | The `Visibility` of the __Camera__. Used to control the `Visibility` of different models in the same __Camera__. | +| __ClearFlags__ | Clearing flags of the Camera, spaces which part of the framebuffer will be actually clearedevery frame. Contains:
__DONT_CLEAR__: not clear.
__DEPTH_ONLY__: only clear the depth.
__SLOD_COLOR__: clear the color, depth and template buffer.
__SKYBOX__: enable skybox to clear depth only. | +| __ClearColor__ | Clear the specified color | +| __ClearDepth__ | Clear the specified depth | +| __ClearStencil__ | Clear the specified template buffer | +| __Projection__ | Projection mode. Divided into __perspective projection (PERSPECTIVE)__ and __orthogonal projection (ORTHO)__ | +| __FovAxis__ | The axis on which the FOV would be fixed regardless of screen aspect changes | +| __Fov__ | Field of view of the camera | +| __OrthoHeight__ | Viewport height in orthogonal mode | +| __Near__ | The near clipping distance of the camera, should be as large as possible within acceptable range | +| __Far__ | The far clipping distance of the camera, should be as small as possible within acceptable range | +| __Aperture__ | The camera aperture, which affects the exposure parameters of the camera | +| __Shutter__ | The camera shutter, which affects the exposure parameters of the camera | +| __Iso__ | The camera ISO, which affects the exposure parameters of the camera
Please refer to __Exposure__ below for more information | +| __Rect__ | The position and size of the viewport that the camera will eventually render to the screen | +| __TargetTexture__ | Output render target texture of the camera, which renders directly to the screen. Defaults to null | + +## Camera group rendering + +The __Camera__'s group rendering function is determined by the `Visibility` property and the [Layer property](../../concepts/scene/node-component.md#set-the-layer-property-of-the-node) of the node. The user can set the __Visibility__ value through code to complete the group rendering. It should be noted that the __Visibility__ value is __bitwise comparison__, and the user can manipulate the __top 20 bits of Visibility__ through __bit operations__ to complete the grouping. + +The __Camera__ and models provided by default are all __rendered without grouping__. You do not need to change this value if the game has no special requirements to do so. + +### Set the Visibility property + +The `Visibility` property is used to set which layers of nodes should be observed by the camera, and multiple Layers can be selected at the same time. + +> __Note__: the rendering of 2d elements (such as Sprite) also follows the `Layer` and `Visibility` judgement, adjust the `Layer` and `Visibility` as required. + +When you check multiple Layers in the `Visibility` property, the value of the `Visibility` property is calculated by performing a `|` operation on the property values of multiple Layers. + +For example, in the following image, the `Visibility` property of the camera has both __UI_3D__ and __DEFAULT__ Layer checked, and by looking up [the value of the Layer property](../../concepts/scene/layer.md), notice the value of the __UI_3D__ property is __1 << 23__ , the value of the __DEFAULT__ property is __1 << 30__, and the value of the `Visibility` property is __1 << 23 | 1 << 30 = 1082130432__. + +![camera visibility gizmo](camera/camera-visibility-gizmo.png) + +For additional information on the implementation of Layer, please refer to the [Layer](../../concepts/scene/layer.md) documentation. + +### Visibility calculations for the camera + +The `Visibility` property allows multiple Layers to be selected at the same time, while the `Layer` on the Node has its own value, so the `Visibility` property of the camera is an __232__ bit integer. Each visible Layer occupies one bit, using bitwise operations, and supports up to 32 different Layer labels (one bit for each Layer value, that is, represented by __232__). When the camera is culling, the Layer's property value of each node will perform a `&` operation with the camera, and if the `Visibility` property of the camera contains this `Layer`, then the current node will be visible to the camera, and vice versa. + +### Exposure + +__Aperture__, __Shutter__, __Iso__: These three parameters of __Camera__ will determine the amount of incoming light, which in turn affects the exposure value. Only takes effect if the scene has HDR turned on. The algorithm is usually as follows: + +ev = log 2 (ApertureValue2 / ShutterValue*k / IsoValue) + +`ApertureValue`, `ShutterValue` and `IsoValue` are obtained by looking up the enumeration values of the three attributes __Aperture__, __Shutter__, __Iso__. Where k is a cosntant. + +The following images demonstrate the effect of exposure on a scene: + +![hdr1](./camera/hdr1.png) + +![hdr2](./camera/hdr2.png) + +To enable scene HDR, please refer to [Skybox - Toggling HDR/LDR mode](../../concepts/scene/skybox.md#Toggling%HDR/LDR%mode). diff --git a/versions/4.0/en/editor/components/camera/camera-component.png b/versions/4.0/en/editor/components/camera/camera-component.png new file mode 100644 index 0000000000..f5d33fb4d4 Binary files /dev/null and b/versions/4.0/en/editor/components/camera/camera-component.png differ diff --git a/versions/4.0/en/editor/components/camera/camera-frustum.jpg b/versions/4.0/en/editor/components/camera/camera-frustum.jpg new file mode 100644 index 0000000000..cd53fe1768 Binary files /dev/null and b/versions/4.0/en/editor/components/camera/camera-frustum.jpg differ diff --git a/versions/4.0/en/editor/components/camera/camera-view.gif b/versions/4.0/en/editor/components/camera/camera-view.gif new file mode 100644 index 0000000000..417e3314d9 Binary files /dev/null and b/versions/4.0/en/editor/components/camera/camera-view.gif differ diff --git a/versions/4.0/en/editor/components/camera/camera-visibility-gizmo.png b/versions/4.0/en/editor/components/camera/camera-visibility-gizmo.png new file mode 100644 index 0000000000..e267e40a33 Binary files /dev/null and b/versions/4.0/en/editor/components/camera/camera-visibility-gizmo.png differ diff --git a/versions/4.0/en/editor/components/camera/camera.png b/versions/4.0/en/editor/components/camera/camera.png new file mode 100644 index 0000000000..9c196ee77b Binary files /dev/null and b/versions/4.0/en/editor/components/camera/camera.png differ diff --git a/versions/4.0/en/editor/components/camera/hdr1.png b/versions/4.0/en/editor/components/camera/hdr1.png new file mode 100644 index 0000000000..ab580824fc Binary files /dev/null and b/versions/4.0/en/editor/components/camera/hdr1.png differ diff --git a/versions/4.0/en/editor/components/camera/hdr2.png b/versions/4.0/en/editor/components/camera/hdr2.png new file mode 100644 index 0000000000..0f885981d3 Binary files /dev/null and b/versions/4.0/en/editor/components/camera/hdr2.png differ diff --git a/versions/4.0/en/editor/components/dragonbones.md b/versions/4.0/en/editor/components/dragonbones.md new file mode 100644 index 0000000000..1ff0390078 --- /dev/null +++ b/versions/4.0/en/editor/components/dragonbones.md @@ -0,0 +1,123 @@ +# DragonBones ArmatureDisplay Component References + +The ArmatureDisplay component can render and play DragonBones assets. + +![dragonbones](./dragonbones/properties.png) + +In the **Hierarchy** panel, select the node where you want to add the ArmatureDisplay component, and then click the **Add Components -> DragonBones -> ArmatureDisplay** button under the **Inspector** panel to add the ArmatureDisplay component to the node. + +- For information on using the ArmatureDisplay component, please refer to the [DragonBones example case](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/dragonbones) for details. +- For DragonBones ArmatureDisplay related scripting interfaces, please refer to the [ArmatureDisplay API](%__APIDOC__%/en/class/dragonBones.ArmatureDisplay) for details. + +## DragonBones Properties + +| Property | Description +| :-------- | :---------- | +| CustomMaterial | Custom materials that can be used to achieve rendering effects such as dissolve, external glow, etc. Please refer to the [Custom Materials](../../ui-system/components/engine/ui-material.md) documentation for details. +| Color | Set the skeleton animation color. +| DragonAsset | Skeletal information data that contains skeletal information (bound skeletal actions, slots, render order, attachments, skins, etc.) and animations, but does not hold any state.
Multiple ArmatureDisplays can share the same skeletal data.
Skeletal assets exported by DragonBones can be dragged and dropped here. +| DragonAtlasAsset | The Atlas Texture data needed for the skeleton data. Drag and drop DragonBones exported Atlas assets here. +| Armature | The name of the Armature currently in use +| Animation | Name of the currently playing animation +| Animation Cache Mode | Rendering modes, including **REALTIME** (default setting), **SHARED_CACHE** and **PRIVATE_CACHE**.
1. **REALTIME** mode, real-time computing, supports all DragonBones features.
2. **SHARED_CACHE** mode, caches and shares skeletal animations and texture data, equivalent to pre-baked skeletal animations. **SHARED_CACHE** mode has higher performance, but does not support action fusion, action overlay, skeleton nesting, and only supports action start and end events. As for memory, it has a memory advantage when creating N (N>=3) animations with the same skeleton and the same action, the larger the N value, the more obvious the advantage. In summary, **SHARED_CACHE** mode is suitable for scene animations, effects, replica monsters, NPCs, etc., and can greatly improve frame rates and reduce memory consumption.
3. **PRIVATE_CACHE** mode, similar to **SHARED_CACHE** but does not share animation and texture data, so there is no memory advantage, only performance advantage. Use **PRIVATE_CACHE** to take advantage of the high performance of the cache mode and to also implement texture replacement (which cannot share the texture data). +| TimeScale | The time scale of all animations in the current skeleton. The default value is **1**, which means no scaling. +| PlayTimes | The number of cycles to play the default animation.
**-1** means use the default value from the DragonBones resource file.
**0** means infinite loop.
**>0** means loop times. +| PremultipliedAlpha | Whether the image is enabled for texture pre-multiplied, default is True.
This item needs to be turned off when the transparent area of the image appears color blocked.
Enabled when the translucent areas of the image become black. +| DebugBones | Whether to show debug information for bone +| Sockets | Used to attach certain external nodes to the specified skeleton joints. The value of the property indicates the number of attachment points. For details, please refer to the description below. +| Enable Batch | Whether to enable batching | + +> **Notes**: +> 1. The **Anchor** and **Size** properties on the Node component in the **Inspector** panel are disabled when using the ArmatureDisplay component. +> 2. The ArmatureDisplay component is a UI renderable component, and the `Canvas` node is the rendering root for UI rendering, so the node with this component must be a child of the `Canvas` node (or a node with a `RenderRoot2D` component) to be displayed properly in the scene. + +## DragonBones ReplaceTexture + +The following is an example of how DragonBones replaces the texture. Replace the weapon in the left or right hand of the robot below with the weapon in the red box by replacing the slot's display object. + +![dragonbones-cloth](./dragonbones/cloth.png) + +1. First create a new `Canvas` node in the **Hierarchy** panel, and then create a new empty node and name it to `replaceDBNode` under the `Canvas` node. Select `replaceDBNode` and add the **ArmatureDisplay** component to the **Inspector** panel. Drag and drop the weapon resource in the green box to the property boxes of the **ArmatureDisplay** component, as shown below: + + ![dragonbones-cloth](./dragonbones/cloth2.png) + +2. Create a new empty node again and name it to `dbNode`. Then add the **ArmatureDisplay** component to the **Inspector** panel and drag the robot's assets to the property boxes of the **ArmatureDisplay** component, as shown below. Change the `Animation` property of the **ArmatureDisplay** component to set the animation you want to play. + + ![dragonbones-cloth](./dragonbones/cloth3.png) + +3. Create a new TypeScript script and name it `ReplaceSlotDisplay` in the **Assets** panel to write the component script. The script code is as follows: + + ```ts + import { _decorator, Component, dragonBones } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('ReplaceSlotDisplay') + export class ReplaceSlotDisplay extends Component { + + @property({ type: dragonBones.ArmatureDisplay }) + armatureDisplay: dragonBones.ArmatureDisplay | null = null + @property({ type: dragonBones.ArmatureDisplay }) + replaceArmatureDisplay: dragonBones.ArmatureDisplay | null = null; + + _leftWeaponIndex = 0; + _rightDisplayIndex = 0; + _rightDisplayNames:string[] = []; + _rightDisplayOffset:{x: number, y: number}[] = []; + + start () { + this.replaceArmatureDisplay!.node.active = false; + this._leftWeaponIndex = 0; + this._rightDisplayIndex = 0; + this._rightDisplayNames = ["weapon_1004s_r", "weapon_1004e_r"]; + this._rightDisplayOffset = [{ x: 0, y: 0 }, { x: -60, y: 100 }]; + } + + left () { + let armature = this.armatureDisplay!.armature(); + let slot = armature!.getSlot("weapon_hand_l"); + slot!.displayIndex = slot!.displayIndex == 0 ? 4 : 0; + } + + right () { + this._rightDisplayIndex++; + this._rightDisplayIndex %= this._rightDisplayNames.length; + let armature = this.armatureDisplay!.armature(); + let slot = armature!.getSlot("weapon_hand_r"); + let replaceArmatureName = this.replaceArmatureDisplay!.armatureName; + const displayName = this._rightDisplayNames[this._rightDisplayIndex]; + let factory = dragonBones.CCFactory.getInstance() as any; + factory.replaceSlotDisplay(this.replaceArmatureDisplay!.getArmatureKey(), replaceArmatureName , "weapon_r", displayName, slot); + + let offset = this._rightDisplayOffset[this._rightDisplayIndex]; + slot!.parent.offset.x = offset.x; + slot!.parent.offset.y = offset.y; + armature!.invalidUpdate(); + } + } + ``` + +4. Next, attach the script component to the `Canvas` node, i.e. drag and drop the script into the **Inspector** panel of the `Canvas` node. Drag and drop the `dbNode` node and `replaceDBNode` node from the **Hierarchy** panel into the corresponding property boxes of the script component and save the scene. + + ![dragonbones-cloth](./dragonbones/dragonbone_tscomponent.png) + +5. Next, use the Button component's click event to trigger the `left` and `right` callback in the `ReplaceSlotDisplay` script to replace the texture by clicking the button. + + Create two new Button nodes under the `Canvas` node in the **Assets** panel and name them `left` and `right`. Adjust their position, size, text display and other properties as needed. + + Set the click events for the `left` and `right` nodes in the **Inspector** panel, drag the `Canvas` node with the `ReplaceSlotDisplay` script component attached to the `cc.Node` property box of the `ClickEvents` property of the `left` and `right` nodes respectively, specify script component as `ReplaceSlotDisplay`, and set the callback to `left`/`right` (the following figure shows the `right` node as an example). + + ![spine-cloth](./dragonbones/click-events.png) + +6. After saving the scene, click the **Preview** button at the top of the editor, and click the **Left**/**Right** button to see that the robot's left/right hand weapons have been replaced. For details, please refer to the [ReplaceSlotDisplay](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/dragonbones) example. + + ![dragonbones-cloth](./dragonbones/cloth4.png) + + > **Notes**: + > 1. After the example is run, the weapon style replaced by the right hand is not consistent with the weapon style prepared in the Scene, which is caused by the resource problem, please refer to the specific code implementation. + > 2. If the scene is not displayed when previewing, check that the `Layer` property of each node is consistent with that of the `Camera` node. + > + > ![layer](./dragonbones/layer.png) + +## DragonBones Attachment and Collision Detection + +DragonBones attachment and collision detection are done in exactly the same way as Spine, please refer to the [Spine Attachment and Collision Detection](./spine.md) documentation. diff --git a/versions/4.0/en/editor/components/dragonbones/click-events.png b/versions/4.0/en/editor/components/dragonbones/click-events.png new file mode 100644 index 0000000000..fab4088ec8 Binary files /dev/null and b/versions/4.0/en/editor/components/dragonbones/click-events.png differ diff --git a/versions/4.0/en/editor/components/dragonbones/cloth.png b/versions/4.0/en/editor/components/dragonbones/cloth.png new file mode 100644 index 0000000000..39a09e6b5c Binary files /dev/null and b/versions/4.0/en/editor/components/dragonbones/cloth.png differ diff --git a/versions/4.0/en/editor/components/dragonbones/cloth2.png b/versions/4.0/en/editor/components/dragonbones/cloth2.png new file mode 100644 index 0000000000..cec303adc5 Binary files /dev/null and b/versions/4.0/en/editor/components/dragonbones/cloth2.png differ diff --git a/versions/4.0/en/editor/components/dragonbones/cloth3.png b/versions/4.0/en/editor/components/dragonbones/cloth3.png new file mode 100644 index 0000000000..8f84946a37 Binary files /dev/null and b/versions/4.0/en/editor/components/dragonbones/cloth3.png differ diff --git a/versions/4.0/en/editor/components/dragonbones/cloth4.png b/versions/4.0/en/editor/components/dragonbones/cloth4.png new file mode 100644 index 0000000000..93774e2510 Binary files /dev/null and b/versions/4.0/en/editor/components/dragonbones/cloth4.png differ diff --git a/versions/4.0/en/editor/components/dragonbones/dragonbone_tscomponent.png b/versions/4.0/en/editor/components/dragonbones/dragonbone_tscomponent.png new file mode 100644 index 0000000000..15378ac77a Binary files /dev/null and b/versions/4.0/en/editor/components/dragonbones/dragonbone_tscomponent.png differ diff --git a/versions/4.0/en/editor/components/dragonbones/layer.png b/versions/4.0/en/editor/components/dragonbones/layer.png new file mode 100644 index 0000000000..f77f10b251 Binary files /dev/null and b/versions/4.0/en/editor/components/dragonbones/layer.png differ diff --git a/versions/4.0/en/editor/components/dragonbones/properties.png b/versions/4.0/en/editor/components/dragonbones/properties.png new file mode 100644 index 0000000000..a0152ad984 Binary files /dev/null and b/versions/4.0/en/editor/components/dragonbones/properties.png differ diff --git a/versions/4.0/en/editor/components/layer-gizmo.png b/versions/4.0/en/editor/components/layer-gizmo.png new file mode 100644 index 0000000000..68e8fae78c Binary files /dev/null and b/versions/4.0/en/editor/components/layer-gizmo.png differ diff --git a/versions/4.0/en/editor/components/motion-streak.md b/versions/4.0/en/editor/components/motion-streak.md new file mode 100644 index 0000000000..760561bcd0 --- /dev/null +++ b/versions/4.0/en/editor/components/motion-streak.md @@ -0,0 +1,26 @@ +# MotionStreak Component Reference + +MotionStreak is used to implement a streak effect on the motion track of a game object. + +![MotionStreak](motion-streak/motionstreak.png) + +Click the **Add Component** button at the bottom of the **Inspector** panel and select **MotionStreak** from **Effects** to add the MotionStreak component to the node. + +For details on how to use it, please refer to the [MotionStreak](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/24.motion-streak) example. + +![add motionStreak](motion-streak/add-motion-streak.png) + +For script interfaces of MotionStreak, please refer to [MotionStreak API](%__APIDOC__%/en/class/MotionStreak). + +## MotionStreak Properties + +| Property | Description +| :-------------- | :----------- | +| CustomMaterial | Custom Material, please refer to the [Custom Material](./../../ui-system/components/engine/ui-material.md) documentation for usage. | +| Preview | Whether to enable preview. If this option is enabled, the MotionStreak effect can be previewed in the **Scene** panel. | +| FadeTime | The fade time in seconds of the segment. | +| MinSeg | The minimum segment length (the size of the fading segment). | +| Stroke | The width of the MotionStreak. | +| Texture | The texture of the MotionStreak. | +| FastMode | Whether to enable the fast mode. When fast mode is enabled, new vertices will be added faster, but with less precision. | +| Color | The color of the MotionStreak. | diff --git a/versions/4.0/en/editor/components/motion-streak/add-motion-streak.png b/versions/4.0/en/editor/components/motion-streak/add-motion-streak.png new file mode 100644 index 0000000000..1571e733ee Binary files /dev/null and b/versions/4.0/en/editor/components/motion-streak/add-motion-streak.png differ diff --git a/versions/4.0/en/editor/components/motion-streak/motionstreak.png b/versions/4.0/en/editor/components/motion-streak/motionstreak.png new file mode 100644 index 0000000000..eb120c85ed Binary files /dev/null and b/versions/4.0/en/editor/components/motion-streak/motionstreak.png differ diff --git a/versions/4.0/en/editor/components/spine.md b/versions/4.0/en/editor/components/spine.md new file mode 100644 index 0000000000..acb07fe342 --- /dev/null +++ b/versions/4.0/en/editor/components/spine.md @@ -0,0 +1,290 @@ +# Spine Skeletal Component References + +The Spine Skeleton components support the data format exported by the official Spine tools, and renders and plays Spine assets. + +![spine](./spine/spine-properties.png) + +Select the node and choose **Add Component -> Spine -> Skeleton** on the **Inspector** panel to add the Skeleton component to the node. + +- For information on using the Spine Skeleton component, please refer to the [Spine Skeleton example case](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/spine) for details. +- For Spine Skeleton component related scripting interfaces, please refer to the [Spine Skeleton API](%__APIDOC__%/en/class/Skeleton) for details. + +## Spine Properties + +| Property | Description +| :-------------------- | :----------------- | +| CustomMaterial | Custom materials that can be used to achieve rendering effects such as dissolve, external glow, etc. Please refer to the [Custom Materials](../../ui-system/components/engine/ui-material.md) documentation for details. +| Color | Set the skeleton animation color. +| SkeletonData | Skeleton information data, drag and drop the skeleton assets exported from Spine. into this property +| Default Skin | Select the default skin. +| Animation | The name of the currently playing animation. +| Animation Cache Mode | Rendering mode, the default is **REALTIME** mode.
1. **REALTIME** mode, real-time computing, supports all Spine features.
2. **SHARED_CACHE** mode, caches, and shares skeletal animations and texture data, equivalent to pre-baked skeletal animations. **SHARED_CACHE** mode has higher performance, but does not support motion fusion, motion overlay, and only supports motion start and end events. As for memory, when creating N (N>=3) animations with the same skeleton and the same action, the memory advantage is obvious. In summary, **SHARED_CACHE** mode is suitable for scene animations, effects, replica monsters, NPCs, etc., and can greatly improve frame rates and reduce memory consumption.
3. **PRIVATE_CACHE** mode, similar to **SHARED_CACHE**, but does not share animation and texture data, and will occupy extra memory, there is only a performance advantage, and it may cause stutter if using this mode a lot to play animation. Use **PRIVATE_CACHE** to take advantage of the high performance of the cache mode and to also implement texture replacement (which cannot share the texture data). +| Loop | Whether to loop the current animation. +| Premultiplied Alpha | Whether to enable premultiplied alpha for the image, default is True.
This item needs to be disabled when the transparent area of the image appears as a color block, and enabled when the translucent area of the image turns black. +| Time Scale | The time scale of all animations in the current skeleton. +| Debug Slots | Whether to show debug information of slots. +| Debug Bones | Whether to show debug information of skeletons. +| Debug Mesh | Whether to show debug information of mesh. +| Use Tint | Whether to turn on the tinting effect, off by default. +| Sockets | Used to attach certain external nodes to the specified skeleton joints. The value of the property indicates the number of attachment points. For details, please refer to the description below. +| Enable Batch | Whether to enable Spine batching | + +> **Notes**: +> 1. The `Anchor` and `Size` properties on the Node component in the **Inspector** panel are invalid when using the Skeleton component. +> 2. The Spine Skeletal component is a UI renderable component, and the `Canvas` node is the rendering root for UI rendering, the node with this component must be a child of the `Canvas` node (or a node with a `RenderRoot2D` component) to be displayed properly in the scene. +> 3. When using the Spine Skeleton component, since it has the `UseTint` property, its custom material needs to have two color information, otherwise the Spine staining effect may be wrong. Please refer to the [builtin-spine.effect](https://github.com/cocos/cocos-engine/blob/v3.0.0/editor/assets/effects/builtin-spine.effect) example to achieve this. + +## Spine ReplaceTexture + +Here is an example of how Spine replaces the texture. + +![spine-cloth](./spine/cloth0.png) + +1. First, create a new `Canvas` node in the **Hierarchy** panel, and then create a new empty node and name it to `girl` under the `Canvas` node. Select the `girl` node and add the Skeleton component in the **Inspector** panel, drag and drop the asset into the `SkeletonData` property box of the Skeleton component. The `Animation` property of the Skeleton component can be changed to set the animation that the developer wants to play. + + ![spine-cloth](./spine/cloth1.png) + +2. Create a new TypeScript script and name it `SpineSkin` in the **Assets** panel to write the component's script. The script code is as follows: + + ```ts + import { _decorator, Component, sp } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('SpineSkin') + export class SpineSkin extends Component { + + + @property({ type:sp.Skeleton }) + spine: sp.Skeleton | null = null; + + skinId: number = 0; + + start () { + // Your initialization goes here. + sp.Skeleton } + + change() { + const skins = ['girl', 'boy', 'girl-blue-cape', 'girl-spring-dress'].map(x=> `full-skins/${x}`); + this.skinId = (this.skinId + 1) % skins.length; + this.skin!.setSkin(skins[this.skinId]); + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } + } + ``` + +3. Next, attach the `SpineSkin` script to the `Canvas` node, i.e. drag and drop the script into the **Inspector** panel of the `Canvas` node. Drag and drop the `girl` node in the **Assets** panel into the property boxes corresponding to the `SpineSkin` script component and save the scene. + + ![spine-jscom](./spine/spine-jscom.png) + +4. Next, use the Button component's click event to trigger the `change` callback in the `SpineSkin` script to replace the texture by clicking the button. + + Create a new Button node under the `Canvas` node in the **Assets** panel and name it `change_skin`. Adjust its position, size, text display, and other properties as needed. + + Set the click event of the `change_skin` node in the **Inspector** panel, drag the `Canvas` node attached with the `SpineSkin` script component into the `cc.Node` property box of the `ClickEvents` property, specify script component as `SpineSkin`, and set the callback to `change`. + + ![spine-cloth](./spine/click_event.png) + +5. Adjust the scene structure as needed, save the scene and click the **Preview** button at the top of the editor. Click the **change skin** button to see that the character skin has been replaced. + + ![spine-cloth](./spine/cloth2.png) + + > **Note**: if the scene is not displayed when previewing, check that the `Layer` property of each node is consistent with that of the `Camera` node. + > + > ![layer](./spine/layer.png) + +## Spine Vertex Effect + +The vertex effect is only available when Spine's `Animation Cache Mode` is in the **REALTIME** mode. Here is an example of how to set the vertex effect in Spine. + +1. First, create a new `Canvas` node in the **Hierarchy** panel, and then create a new empty node under the `Canvas` node. Select the node and add the Skeleton component in the **Inspector** panel, drag and drop the asset to the `SkeletonData` property box of the Skeleton component, and set the Skeleton component properties. + +2. Create a new TypeScript script in the **Assets** panel and name it `SpineExample`, write the component script. The script code is as follows: + + ```ts + import { _decorator, Component, sp } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('SpineExample') + export class SpineExample extends Component { + + @property({ type:sp.Skeleton }) + skeleton: sp.Skeleton | null = null; + + private _jitterEffect?:sp.VertexEffectDelegate; + + start () { + this._jitterEffect = new sp.VertexEffectDelegate(); + // Set the jitter parameters. + this._jitterEffect.initJitter(20, 20); + // Call the 'setVertexEffectDelegate' method of the Skeleton component to set the effect. + this.skeleton!.setVertexEffectDelegate(this._jitterEffect!); + } + + }; + ``` + +3. Next, attach the `SpineExample` script to the `Canvas` node, i.e. drag and drop the script into the node's **Inspector** panel. Drag the `Spine` node with the **Skeleton** component attached in the **Hierarchy** panel to the corresponding `Skeleton` property box of the script component, and save the scene. + +4. Click the **Preview** button at the top of the editor to see the effect of vertex jitter of the Spine animation. For example, please refer to the [SpineMesh](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/spine) example. + +## Spine Attachment + +When using skeleton animation, nodes are often attached on a certain part of the skeleton animation to achieve the effect of linkage between the nodes and the skeleton animation. + +Spine Attachments can be implemented by using both editor and script. Here is an example of how Spine uses an attachment to attach a star to the dragon's tail, and shake it with the dragon's tail. + +![attach0](./spine/attach0.png) + +### Implementing via the editor + +1. First, create a new `Canvas` node in the **Hierarchy** panel, and then create a new empty node and name it to `Spine` under the `Canvas` node. Select `Spine` and add the **Skeleton** component to the **Inspector** panel. Drag and drop the asset into the `SkeletonData` property box of the **Skeleton** component and set the Skeleton component properties. + +2. Second, right-click on the Spine node in the **Hierarchy** panel and select **Create -> Empty Node** to add a child node for it and name it `targetNode`. **Create -> 2D Object -> Sprite** to add a sprite component named `star` under `targetNode`. Drag the star asset to the **SpriteFrame** property of the Sprite component in the **Inspector** panel. + + ![attach1](./spine/attach1.png) + +3. Select the Spine node in the **Hierarchy** panel, and set the Skeleton component's `Sockets` property to **1** in the **Inspector** panel (the value of the `Sockets` property represents the number of attachments). + + ![attach2](./spine/attach2.png) + +4. Next, set the **Path** and **Target** properties of the **Sockets**. The **Path** drop-down box will list all the skeletons, select the target bone you want to attach, here take the dragon's tail as an example, drag the `targetNode` node to the **Target** property box. Notice the star attached on the dragon's tail in the **Scene** panel. + + > **Note**:Do not set the `star` node as **Target** node, because this will make UITransform of `star` invalid. Please create a new empty node as the **Target** node and set components which to be attached as children nodes of **Target ** node. + + ![attach3](./spine/attach3.png) + +5. Finally, save the scene and click the **Preview** button on top of the editor to see the star hanging on the dragon's tail and swaying along with it. Please refer to the [SpineAttach](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/spine) example for details. + +### Implementing via code + +1. The first two steps are the same as those implemented via the editor. + +2. Create a new TypeScript script in the Explorer panel and name it `SpineAttach`. Write the component script. The script code is as follows: + + ```ts + import { _decorator, Component, sp, Label, Node } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('SpineAttach') + export class SpineAttach extends Component { + + @property({ type: sp.Skeleton }) + skeleton: sp.Skeleton = null!; + + @property({ type: Node }) + attachNode: Node = null!; + + start () { + var socket = new sp.SpineSocket("root/hip/tail1/tail2/tail3/tail4/tail5/tail6/tail7/tail8/tail9/tail10", this.attachNode); // The first incoming parameter is the target bone of the attachment, and the second incoming parameter is the node of the attachment. + this.skeleton!.sockets.push(socket); + this.skeleton!.sockets = this.skeleton!.sockets; + } + + } + ``` + + If the name of the target bone is unknown, set the `Sockets` property in the Skeleton component to **1** and then look for the name of the desired target bone in the `Path` drop-down box. When the search is complete, restore the `Sockets` property to 0. + +3. Next, attach the `SpineAttach` script to the `Canvas` node, i.e. drag and drop the script into the node's **Inspector** panel. Drag and drop the `Spine` node with the Skeleton component attached and the `targetNode` node in the **Hierarchy** panel to the **Skeleton** property box and the **AttachNode** property box of the script component, respectively, and save the scene. + +4. Click the **Preview** button at the top of the editor to see the star hanging from the dragon's tail and shaking along with the dragon's tail. + + ![attach-ts](./spine/attach-ts.gif) + +## Spine Collision Detection + +The Spine attachment function allows for the detection of a collision of a part of the skeleton animation. The following is an example of how Spine implements collision detection, by determining whether the character's feet are in contact with the ground or not to dynamically change the ground color when the character is running. + +![collider](./spine/collider0.png) + +1. First, set the **2D Physics System** to **Builtin 2D Physics System** in the **Project -> Project Settings -> Feature Cropping** of the editor menu bar. + + ![collider](./spine/collider1.png) + +2. Create the Spine node and its children (an empty node and named `frontFoot`), as well as the Sprite node as the ground (named `Ground`), and set the position, size, and other properties, as in the first two steps for the Spine attachment. + + ![collider](./spine/collider2.png) + +3. Select the `frontFoot` node in the **Hierarchy** panel, click **Add Component -> Physics2D -> Colliders -> Polygon Collider2D** in the **Inspector** panel to add a collision component, and set the collision component parameters. + + ![collider](./spine/collider3.png) + + Referring to steps 3 and 4 of the Spine attachment, attach the `frontFoot` node to the target bone ("foot" for example) of the Sprite node. The `frontFoot` node will then move along with the skeletal animation, and thus the collision component's bounding box will be synchronized with the skeletal animation in real time. + + ![collider](./spine/collider4.png) + +4. Select the `Ground` node in the **Hierarchy** panel, click **Add Component -> Physics2D -> Colliders -> BoxCollider2D** in the **Inspector** panel to add a collision component, and set the collision component parameters. + +5. Create a new TypeScript script in the **Assets** panel and name it `SpineCollider`. Then attach the script on the `Ground` node. The script code is as follows: + + ```ts + import { _decorator, Component, Node, PhysicsSystem2D, Contact2DType, Collider2D, Color, Sprite, ParticleSystem2D, EPhysics2DDrawFlags } from 'cc'; + const { ccclass } = _decorator; + + @ccclass('SpineCollider') + export class SpineCollider extends Component { + + touchingCountMap : Map < Node, number > = new Map; + + private debugDrawFlags : number = 0; + + start () { + // Your initialization goes here. + PhysicsSystem2D.instance.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); + PhysicsSystem2D.instance.on(Contact2DType.END_CONTACT, this.onEndContact, this); + this.debugDrawFlags = PhysicsSystem2D.instance.debugDrawFlags; + } + + onEnable () { + PhysicsSystem2D.instance.debugDrawFlags = this.debugDrawFlags | EPhysics2DDrawFlags.Shape; + } + + onDisable () { + PhysicsSystem2D.instance.debugDrawFlags = this.debugDrawFlags; + } + + addContact (c: Collider2D) { + let count = this.touchingCountMap.get(c.node) || 0; + this.touchingCountMap.set(c.node, ++count); + + let sprite = c.getComponent(Sprite); + if (sprite) { + sprite.color = Color.RED; + } + } + + removeContact (c: Collider2D) { + let count = this.touchingCountMap.get(c.node) || 0; + --count; + if (count <= 0) { + this.touchingCountMap.delete(c.node); + + let sprite = c.getComponent(Sprite); + if (sprite) { + sprite.color = Color.WHITE; + } + } else { + this.touchingCountMap.set(c.node, count); + } + } + + onBeginContact (a: Collider2D, b: Collider2D) { + this.addContact(a); + this.addContact(b); + } + + onEndContact (a: Collider2D, b: Collider2D) { + this.removeContact(a); + this.removeContact(b); + } + } + ``` + +6. Click the **Preview** button at the top of the editor to see the effect. For details, please refer to the [SpineCollider](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/spine) example. + + > **Note**: the collision detection based on attachment has a delay of one frame due to the implementation mechanism of the attachment. + + ![collider](./spine/collider.gif) diff --git a/versions/4.0/en/editor/components/spine/attach-ts.gif b/versions/4.0/en/editor/components/spine/attach-ts.gif new file mode 100644 index 0000000000..db5f5465d5 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/attach-ts.gif differ diff --git a/versions/4.0/en/editor/components/spine/attach0.png b/versions/4.0/en/editor/components/spine/attach0.png new file mode 100644 index 0000000000..8ac5c2e1be Binary files /dev/null and b/versions/4.0/en/editor/components/spine/attach0.png differ diff --git a/versions/4.0/en/editor/components/spine/attach1.png b/versions/4.0/en/editor/components/spine/attach1.png new file mode 100644 index 0000000000..f41869a0eb Binary files /dev/null and b/versions/4.0/en/editor/components/spine/attach1.png differ diff --git a/versions/4.0/en/editor/components/spine/attach2.png b/versions/4.0/en/editor/components/spine/attach2.png new file mode 100644 index 0000000000..39c82fd585 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/attach2.png differ diff --git a/versions/4.0/en/editor/components/spine/attach3.png b/versions/4.0/en/editor/components/spine/attach3.png new file mode 100644 index 0000000000..0c3d444319 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/attach3.png differ diff --git a/versions/4.0/en/editor/components/spine/click_event.png b/versions/4.0/en/editor/components/spine/click_event.png new file mode 100644 index 0000000000..9033d779e0 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/click_event.png differ diff --git a/versions/4.0/en/editor/components/spine/cloth0.png b/versions/4.0/en/editor/components/spine/cloth0.png new file mode 100644 index 0000000000..cee994f916 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/cloth0.png differ diff --git a/versions/4.0/en/editor/components/spine/cloth1.png b/versions/4.0/en/editor/components/spine/cloth1.png new file mode 100644 index 0000000000..dd6072b16e Binary files /dev/null and b/versions/4.0/en/editor/components/spine/cloth1.png differ diff --git a/versions/4.0/en/editor/components/spine/cloth2.png b/versions/4.0/en/editor/components/spine/cloth2.png new file mode 100644 index 0000000000..6d763ae0ae Binary files /dev/null and b/versions/4.0/en/editor/components/spine/cloth2.png differ diff --git a/versions/4.0/en/editor/components/spine/collider.gif b/versions/4.0/en/editor/components/spine/collider.gif new file mode 100644 index 0000000000..5e5f1c7781 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/collider.gif differ diff --git a/versions/4.0/en/editor/components/spine/collider0.png b/versions/4.0/en/editor/components/spine/collider0.png new file mode 100644 index 0000000000..c97db18065 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/collider0.png differ diff --git a/versions/4.0/en/editor/components/spine/collider1.png b/versions/4.0/en/editor/components/spine/collider1.png new file mode 100644 index 0000000000..b6f6d58007 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/collider1.png differ diff --git a/versions/4.0/en/editor/components/spine/collider2.png b/versions/4.0/en/editor/components/spine/collider2.png new file mode 100644 index 0000000000..a284c0a2e0 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/collider2.png differ diff --git a/versions/4.0/en/editor/components/spine/collider3.png b/versions/4.0/en/editor/components/spine/collider3.png new file mode 100644 index 0000000000..3afbd503c3 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/collider3.png differ diff --git a/versions/4.0/en/editor/components/spine/collider4.png b/versions/4.0/en/editor/components/spine/collider4.png new file mode 100644 index 0000000000..5dccdfc5c4 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/collider4.png differ diff --git a/versions/4.0/en/editor/components/spine/layer.png b/versions/4.0/en/editor/components/spine/layer.png new file mode 100644 index 0000000000..f77f10b251 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/layer.png differ diff --git a/versions/4.0/en/editor/components/spine/spine-jscom.png b/versions/4.0/en/editor/components/spine/spine-jscom.png new file mode 100644 index 0000000000..f8b6f69f89 Binary files /dev/null and b/versions/4.0/en/editor/components/spine/spine-jscom.png differ diff --git a/versions/4.0/en/editor/components/spine/spine-properties.png b/versions/4.0/en/editor/components/spine/spine-properties.png new file mode 100644 index 0000000000..b2e914ef6b Binary files /dev/null and b/versions/4.0/en/editor/components/spine/spine-properties.png differ diff --git a/versions/4.0/en/editor/components/tiledmap.md b/versions/4.0/en/editor/components/tiledmap.md new file mode 100644 index 0000000000..71b7d00726 --- /dev/null +++ b/versions/4.0/en/editor/components/tiledmap.md @@ -0,0 +1,85 @@ +# TiledMap Component References + +TiledMap is used to display a map in TMX format in the game. + +![tiledmap-component](tiledmap/tiledmap-component.png) + +Click the **Add Component -> Components -> TiledMap** button under the **Inspector** panel to add the TiledMap component to the node. + +![](./tiledmap/add_tiledmap.png) + +For the script interface of TiledMap, please refer to the [TiledMap API](%__APIDOC__%/en/class/TiledMap). + +## TiledMap Properties + +| Property | Description +| :---------------- | :----------------- | +| **Tmx Asset** | Specifies a map asset in `.tmx` format (Ensure the `.tmx` and `.tsx` are in the same folder)| +| **EnableCulling** | Enables culling, which needs to be turned off if the map needs to be rotated or placed in the 3D camera. If the map is not very large, e.g., less than 5000 blocks, then turning off culling reduces the CPU computational load and the GPU renders directly using the cache. + +## Detailed Description + +- After adding the TiledMap component, drag and drop a map asset in **.tmx** format from the **Assets** panel to the Tmx Asset property to see the map displayed in the scene. +- When the Tmx Asset property is added to the TiledMap component, nodes corresponding to the Layers in the map are automatically added to the nodes (floor, barrier, and players nodes in the image below). These nodes are added to the TiledLayer component. **Do not delete the TiledLayer component from these Layer nodes**. + + ![](./tiledmap/tiledlayer.png) + +- TiledMap component does not support the `mapLoaded` callback. It can be used normally in the `start` function. + +## TiledLayer and Node Occlusion + +The TiledLayer component converts the node coordinates added to the map layer into map block row coordinates. When rendering a map block in a map layer in row order, if there is a node in the row of the map block, then rendering of the map block will be interrupted in favor of rendering the node. After the nodes in the map block are rendered, the rendering of the map block will continue. In this way, the nodes and map layers are mutually occluded from each other. + +> **Note**: this occlusion relationship is only related to the coordinates of the node, not the size of the node. + +The following is an example of how the TiledLayer and the node can occlude each other. + +1. Create a new node in the scene and add the TiledMap component. After setting the properties of the TiledMap component, the node with the TiledLayer component (i.e. map layer) will be generated automatically. + +2. Create [Prefab](../../asset/prefab.md)s to instantiate multiple nodes in the scene. + +3. Create a new TypeScript script in **Assets** panel to write component scripts. The script code is as follows: + + ```ts + import { _decorator, Component, Node, TiledLayer, loader, Prefab, v2, instantiate, Vec3, EventTouch } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('ShieldNode') + export class ShieldNode extends Component { + + @property({ type: TiledLayer }) + public tiledLayer: TiledLayer | null = null; + + @property({ type: Prefab }) + public nodePrefab: Prefab | null = null; + + start () { + this.initScene(this.nodePrefab!); + } + + initScene (prefab: Prefab) { + const posArr = [v2(-249, 96), v2(-150, 76), v2(-60, 54), v2(-248, -144), v2(-89, -34)]; + const tmpP = new Vec3(); + for (let i = 0; i < posArr.length; i++) { + const shieldNode = instantiate(prefab); + shieldNode.setPosition(posArr[i].x, posArr[i].y); + this.tiledLayer!.addUserNode(shieldNode); + shieldNode.on(Node.EventType.TOUCH_MOVE, (event:EventTouch) => { + const deltaMove = event.getDelta(); + shieldNode.getPosition(tmpP); + tmpP.x += deltaMove.x; + tmpP.y += deltaMove.y; + shieldNode.setPosition(tmpP); + }); + } + } + } + ``` + +4. Attach the script component to the Canvas node, i.e. drag and drop the script into the **Inspector** panel of the Canvas node. Then drag and drop the automatically generated node with TiledLayer component from **Hierarchy** panel and the Prefabs from **Assets** panel to the corresponding property box of the script component, and then save the scene. + +5. Click the Preview button at the top of the editor to see the effect of the nodes and map layers occluding each other. For the code, please refer to the [ShieldNode](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/tiled-map) example. + + ![shieldNode](./tiledmap/shieldNode.png) + +To remove a node from a map layer, call the `removeUserNode` method of TiledLayer. diff --git a/versions/4.0/en/editor/components/tiledmap/add_tiledmap.png b/versions/4.0/en/editor/components/tiledmap/add_tiledmap.png new file mode 100644 index 0000000000..89b32df536 Binary files /dev/null and b/versions/4.0/en/editor/components/tiledmap/add_tiledmap.png differ diff --git a/versions/4.0/en/editor/components/tiledmap/shieldNode.png b/versions/4.0/en/editor/components/tiledmap/shieldNode.png new file mode 100644 index 0000000000..9ed5de9157 Binary files /dev/null and b/versions/4.0/en/editor/components/tiledmap/shieldNode.png differ diff --git a/versions/4.0/en/editor/components/tiledmap/tiledlayer.png b/versions/4.0/en/editor/components/tiledmap/tiledlayer.png new file mode 100644 index 0000000000..293588271c Binary files /dev/null and b/versions/4.0/en/editor/components/tiledmap/tiledlayer.png differ diff --git a/versions/4.0/en/editor/components/tiledmap/tiledmap-component.png b/versions/4.0/en/editor/components/tiledmap/tiledmap-component.png new file mode 100644 index 0000000000..2539041575 Binary files /dev/null and b/versions/4.0/en/editor/components/tiledmap/tiledmap-component.png differ diff --git a/versions/4.0/en/editor/components/tiledtile.md b/versions/4.0/en/editor/components/tiledtile.md new file mode 100644 index 0000000000..6ab931e2a4 --- /dev/null +++ b/versions/4.0/en/editor/components/tiledtile.md @@ -0,0 +1,62 @@ +# TiledTile Component References + +The TiledTile component can operate on a particular map block individually. + +![tiledtile-component](./tiledtile/tiledtile-component.png) + +## Creation Method + +### Create via Editor + +Create an empty node under the Layer node **automatically generated** during the creation of the [TiledMap component](tiledmap.md). Then select the empty node and click **Add component -> TiledMap -> TiledTile** under the **Inspector** panel to add the TiledTile component to the node. Then you can manipulate the map block by setting the properties on the TiledTile component. + +![add_tiledtile](./tiledtile/add_tiledtile.png) + +For related TiledTile script interface, please refer to the [TiledTile API](%__APIDOC__%/en/class/TiledTile). + +### Create by Code + +There are two ways to set up a map block in code. When setting the TiledTile in a Layer node, the TiledTile at the original location of that Layer node will be replaced. + +#### Create by adding a TiledTile Ccmponent to a node + +```ts +// Create a new node +const node = new Node(); +// then set the node's parent to any layer node +node.parent = this.layer.node; +// Finally add the TiledTile component to the node and return the TiledTile object, which allows you to perform a series of operations on the TiledTile object +const tiledTile = node.addComponent(TiledTile); +``` + +#### Get TiledTile by getTiledTileAt + +```ts +// Get the TiledTile object on the layer with horizontal coordinates of 0 and vertical coordinates of 0. You can then perform a series of operations on the TiledTile object +const tiledTile = this.layer.getTiledTileAt(0, 0); +``` + +For the Layer script interface, please refer to the [TiledLayer API](%__APIDOC__%/en/class/TiledLayer). + +## TiledTile Properties + +| Property | Description +| :-----| :---------- | +| **X** | Specifies the horizontal coordinates of the TiledTile, in map blocks +| **Y** | Specifies the vertical coordinates of the TiledTile, in map blocks +| **Gid** | Specifies the gid value of the TiledTile to toggle the style of the TiledTile + +TiledTile can control the specified map block and apply the displacement, rotation and scaling of nodes to the map block. The user can change the map block style by changing the gid property of the TiledTile. + +> **Note**: the gid can only be used with an existing map block in the map to toggle the style of the map block, it is not possible to toggle the style of the map block by customizing the Sprite Frame. + +## Node properties that can be applied to TiledTile + +| Property | Description +| :-----| :---------- | +| **Position** | Change the **Position** of the specified TiledTile +| **Rotation** | **Rotate** the specified TiledTile +| **Scale** | **Scale** the specified TiledTile +| **Color** | Change the **Color** of the specified TiledTile +| **Opacity** | Adjust the **Opacity** of the specified TiledTile +| **Skew** | Adjust the **Skew** of the specified TiledTile diff --git a/versions/4.0/en/editor/components/tiledtile/add_tiledtile.png b/versions/4.0/en/editor/components/tiledtile/add_tiledtile.png new file mode 100644 index 0000000000..c90a45a63e Binary files /dev/null and b/versions/4.0/en/editor/components/tiledtile/add_tiledtile.png differ diff --git a/versions/4.0/en/editor/components/tiledtile/tiledtile-component.png b/versions/4.0/en/editor/components/tiledtile/tiledtile-component.png new file mode 100644 index 0000000000..bab5fc2276 Binary files /dev/null and b/versions/4.0/en/editor/components/tiledtile/tiledtile-component.png differ diff --git a/versions/4.0/en/editor/console/index.md b/versions/4.0/en/editor/console/index.md new file mode 100644 index 0000000000..181bba73d1 --- /dev/null +++ b/versions/4.0/en/editor/console/index.md @@ -0,0 +1,84 @@ +# Console + +![index](index/index.png) + +**Console** outputs editor or engine messages, including **log**, **warn**, and **error**. Different types of messages are displayed in different colors. + +- `console.log()`: outputs log, gray text, usually used to show what action is in progress. + + ![log](index/log.png) + +- `console.warn()`: outputs warnings, yellow text, used to indicate exceptions that the developer would be wise to handle, but which will not affect operation if left unhandled. + + ![warn](index/warn.png) + +- `console.error()`: outputs error, red text, indicating a serious error that must be resolved before proceeding to the next step or running the game. + + ![error](index/error.png) + +## Panel operations + +The top toolbar functions, in order, are: + +- Clear all logs in the current console +- Enter text for fuzzy search +- Whether to convert the input text to regular for searching +- Select the type of logs to be displayed +- Open the log file backed up on disk, which will be reset each time the editor is started. + + ![open-log-file](index/open-log-file.png) + +## Parameter settings + +Some parameters of the console can be configured in **Preferences -> Console**, including whether to display the date and adjust the text size. + +![preferences](index/preferences.png) + +## Custom output messages + +To make it easier to locate files, nodes or assets, or to provide links to jump to help documentation, etc., Cocos Creator supports custom output to the **console** log in **Developer -> Developer Tools** in the editor's main menu, which currently supports output of the following: + +- Jump to links by URL +- Show images by URL +- Locate assets by URL or UUID +- Locate nodes by UUID +- Locate script files by disk file path +- Output the text in the corresponding language + +### Data format + +Depending on the output content, the input data formats include the following two: + +- `{type[text](url)}` +- `{type(text | url | uuid | path)}` + +The data formats are described as follows: + +- Matches the characters in `{}` as a whole. +- `[text]`: Text of the jump link, optional. +- `type`: The type of information to be output, including the following. The filling is case-insensitive, and if not fill in, the input is directly output without format. + + - `link`: external jump link + - `image`: display image + - `asset`: locate to a asset + - `node`: locate to a node + - `i18n`: multilingual translation + +### Example + +Open **Developer -> Developer Tools** in the editor's main menu, then type: + +```sh +console.log('Open {link[the help doc url](https://docs.cocos.com/creator/manual/en/editor/console/)}'); +console.log('Locate {link[ the file in library](D:/cocos-creator/a/library/36/36b55a90-1547-4695-8105-abd89f8a0e5f.js)}'); +console.log('Locate Node UUID {node(f6zHdGKiZDhqbDizUsp8mK)}'); +console.warn('Locate Asset UUID {asset(17185449-5194-4d6c-83dc-1e785375acdb)}'); +console.error('Locate Asset URL {asset(db://assets/animation.anim)}'); +console.log('The URL is {asset[{asset(db://assets/animation.anim)}](db://assets/animation.anim)}'); +console.log('Show image {image(https://forum.cocos.org/images/logo.png)}'); +console.log('Translate: {i18n(console.description)}'); +``` + +The output log can be seen in the **Console** panel as follows: + +![content](index/content.png) diff --git a/versions/4.0/en/editor/console/index/content.png b/versions/4.0/en/editor/console/index/content.png new file mode 100644 index 0000000000..49e97861a2 Binary files /dev/null and b/versions/4.0/en/editor/console/index/content.png differ diff --git a/versions/4.0/en/editor/console/index/error.png b/versions/4.0/en/editor/console/index/error.png new file mode 100644 index 0000000000..b6db81ad83 Binary files /dev/null and b/versions/4.0/en/editor/console/index/error.png differ diff --git a/versions/4.0/en/editor/console/index/index.png b/versions/4.0/en/editor/console/index/index.png new file mode 100644 index 0000000000..2a81b90704 Binary files /dev/null and b/versions/4.0/en/editor/console/index/index.png differ diff --git a/versions/4.0/en/editor/console/index/log.png b/versions/4.0/en/editor/console/index/log.png new file mode 100644 index 0000000000..20dd9ffa23 Binary files /dev/null and b/versions/4.0/en/editor/console/index/log.png differ diff --git a/versions/4.0/en/editor/console/index/open-log-file.png b/versions/4.0/en/editor/console/index/open-log-file.png new file mode 100644 index 0000000000..60f2516aa4 Binary files /dev/null and b/versions/4.0/en/editor/console/index/open-log-file.png differ diff --git a/versions/4.0/en/editor/console/index/preferences.png b/versions/4.0/en/editor/console/index/preferences.png new file mode 100644 index 0000000000..63b0b12cab Binary files /dev/null and b/versions/4.0/en/editor/console/index/preferences.png differ diff --git a/versions/4.0/en/editor/console/index/warn.png b/versions/4.0/en/editor/console/index/warn.png new file mode 100644 index 0000000000..e2d938337b Binary files /dev/null and b/versions/4.0/en/editor/console/index/warn.png differ diff --git a/versions/4.0/en/editor/editor-layout/index.md b/versions/4.0/en/editor/editor-layout/index.md new file mode 100644 index 0000000000..b9dabf5ef0 --- /dev/null +++ b/versions/4.0/en/editor/editor-layout/index.md @@ -0,0 +1,40 @@ +# Editor Layout + +Editor layout is the position, size and stacking of each panel in Cocos Creator. + +Select the **Cocos Creator/File -> Layout** menu in the main menu, currently only **Default Layout** is supported. Based on the default layout, it is allowed to continue to adjust the position and size of each panel. Changes to the layout are automatically saved in the global directory at: + +- **Windows**: `%USERPROFILE%\.CocosCreator\editor\window.json` +- **macOS**: `$HOME/.CocosCreator/editor/window.json` + +## Resizing panel + +Hover the mouse over the boundary line between the two panels and see the mouse pointer become ![mouse-cursor](index/mouse-cursor.jpg), then press the mouse, the boundary line will turn yellow, then drag the mouse to change the size of the two adjacent panels. + +![resize](index/resize.png) + +> **Note**: some panels are set to the minimum size, when dragging to the minimum size limit, shrinking the panel cannot continue. + +## Moving panel + +A panel can be moved to any position in the editor window by clicking the tab bar of the panel and dragging it. The red box in the following figure indicates the area of the tab bar that can be dragged and dropped, and the arrow indicates the dragging direction: + +![drag tab](index/drag_tab.png) + +The blue translucent box during moving the panel indicates the position where the moved panel will be placed after releasing the mouse. + +## Stacking Panel + +In addition to moving the panel position, it is allowed to move the mouse to the tab bar area of another panel when dragging and dropping the panel tab bar: + +![stack before](index/stack_before.png) + +Releasing the mouse when the red vertical line appears in the tab bar of the target panel allows stacking two panels together, while only one panel is displayed: + +![stack after](index/stack_after.png) + +Stacking panel is useful when the screen resolution is insufficient, or when laying out less-used panels. Panels in the stack can be dragged out at any time to restore displaying at the top level. + +Panels also support pop-up standalone panels or close panel operations by clicking the button at the top right of the panel. However, note that the **Scene** panel does not support pop-up/close. + +![popup](index/popup.png) diff --git a/versions/4.0/en/editor/editor-layout/index/drag_tab.png b/versions/4.0/en/editor/editor-layout/index/drag_tab.png new file mode 100644 index 0000000000..17d7800e77 Binary files /dev/null and b/versions/4.0/en/editor/editor-layout/index/drag_tab.png differ diff --git a/versions/4.0/en/editor/editor-layout/index/mouse-cursor.jpg b/versions/4.0/en/editor/editor-layout/index/mouse-cursor.jpg new file mode 100644 index 0000000000..df66301fc4 Binary files /dev/null and b/versions/4.0/en/editor/editor-layout/index/mouse-cursor.jpg differ diff --git a/versions/4.0/en/editor/editor-layout/index/popup.png b/versions/4.0/en/editor/editor-layout/index/popup.png new file mode 100644 index 0000000000..de83312559 Binary files /dev/null and b/versions/4.0/en/editor/editor-layout/index/popup.png differ diff --git a/versions/4.0/en/editor/editor-layout/index/resize.png b/versions/4.0/en/editor/editor-layout/index/resize.png new file mode 100644 index 0000000000..c8a7d9f1f4 Binary files /dev/null and b/versions/4.0/en/editor/editor-layout/index/resize.png differ diff --git a/versions/4.0/en/editor/editor-layout/index/stack_after.png b/versions/4.0/en/editor/editor-layout/index/stack_after.png new file mode 100644 index 0000000000..c631652c96 Binary files /dev/null and b/versions/4.0/en/editor/editor-layout/index/stack_after.png differ diff --git a/versions/4.0/en/editor/editor-layout/index/stack_before.png b/versions/4.0/en/editor/editor-layout/index/stack_before.png new file mode 100644 index 0000000000..de3d8e49d8 Binary files /dev/null and b/versions/4.0/en/editor/editor-layout/index/stack_before.png differ diff --git a/versions/4.0/en/editor/extension/basic.md b/versions/4.0/en/editor/extension/basic.md new file mode 100644 index 0000000000..f2ab9c42db --- /dev/null +++ b/versions/4.0/en/editor/extension/basic.md @@ -0,0 +1,11 @@ +# Basic + +Many functions are provided within the editor, here are some of the most commonly used features. + +- [Extension Infrastructure](./package.md) +- [Definition of Extension](./define.md) +- [Message System](./messages.md) +- [Multilingual System (i18n)](./i18n.md) +- [Configuration System](./profile.md) +- [Extension Panel](./panel.md) +- [UI Components](./ui.md) diff --git a/versions/4.0/en/editor/extension/contributions-database.md b/versions/4.0/en/editor/extension/contributions-database.md new file mode 100644 index 0000000000..871ddc9229 --- /dev/null +++ b/versions/4.0/en/editor/extension/contributions-database.md @@ -0,0 +1,86 @@ +# Extending the Database (DB) + +All asset files in the project are managed through the asset database, where the `assets` directory in the project stores the assets of the current project, and the `editor/assets` in the engine repository stores the built-in assets of the engine (e.g., common images, scripts, etc.). + +When we use the assets in the extension, we need to register the asset folder in the extension to the asset database, and publish the assets with the extension when it is released. + +Through this article we will learn how to register a asset folder and use the assets in scripts. + +## Registration Database Configuration + +Asset registration needs to be configured in `contributions` using the `asset-db` field, as follows. + +```json5 +{ + "name": "test-package", + "contributions": { + "asset-db": { + "mount": { + "path": "./assets", + "readonly": true + } + } + } +} +``` + +In the above example, we have registered the `assets` folder in the root directory of the extension `test-package` to the asset database. + +## Writing Script Resources in Extensions + +Create a script `foo.ts` in the `test-package/assets/` directory, with the following content: + +```typescript +/// foo.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Foo') +export class Foo extends Component { + start () { + console.log('foo'); + } +} +``` + +In order to use the `cc` definitions, we need to copy the definition files from `{project directory}\temp\declarations` to the extension root directory. + +Since `foo.ts` is only used as a asset and is not part of the extension source code, we need to exclude it by adding `exclude` configuration to `tsconfig.json`, otherwise we will get a compilation error. + +```json5 +{ + "compilerOptions": { + ... + }, + "exclude": ["./assets"] +} +``` + +> **Note**: The script assets in the extension can be written and tested in the Cocos Creator project and then copied to the extension's `assets` directory. + +## Other Assets + +Assets such as images, text, fonts, etc. can be placed directly in the `assets` directory. + +## Using Assets in Extensions + +Refresh the extension and you will see a new `test-package` assets package in the **Assets Manager** panel of the Cocos Creator editor, as shown below:s + +![extension-database](./image/extension-database.png) + +## Drag-and-drop Assets + +To reference an asset within a package as a drag-and-drop component, use it in the same way as the assets in `assets` and `internal`. + +## Import Script + +You can simply reference a script file from the AssetDatabase as follows: + +```typescript +/// bar.ts +import { Foo } from 'db://test-package/foo'; +``` + +You don't need to pay attention to whether a class comes from an extension package, the TypeScript development environment's auto-completion feature will prompt the **import** directory, no need to worry. + +> **Note**: The class name in the extension package should be kept globally unique, otherwise it will cause conflicts, try to add a suitable prefix in the actual development (e.g. `test-package` can be shortened to `TP`, and all classes in the package are unified with the `TP` prefix to become `TPFoo`). diff --git a/versions/4.0/en/editor/extension/contributions-menu.md b/versions/4.0/en/editor/extension/contributions-menu.md new file mode 100644 index 0000000000..1c1fe883b1 --- /dev/null +++ b/versions/4.0/en/editor/extension/contributions-menu.md @@ -0,0 +1,70 @@ +# Customize the Main Menu + +There is a main menu bar at the top of the editor, and it is easy to add your own menu in this menu bar within the extension. + +## Registering menus + +When an extension needs to add a menu, just fill in the `contributions.menu` object. For example, if we add a menu item to the "Extensions" menu, we can modify `package.json` with the following code example: + +```json5 +{ + // package.json + "name": "hello-world", + "contributions": { + "messages": { + "open-panel": { + "methods": ["openPanel"] + } + }, + "menu": [ + { + "path": "i18n:menu.extension", + "label": "Open Hello World", + "icon": "./static/icon.png", + "message": "open-panel" + } + ] + } +} +``` + +The above configuration information will add a new "Open Hello World" menu in the "Extensions" menu of the editor. Clicking this menu will send an `open-panel` message to the current extension according to the message configuration, which will be triggered if the current extension is configured to listen to this message and the corresponding `openPanel` handler. + +For the definition of messages, please refer to the documentation [Customized Messages](./contributions-messages.md). + +Let's look at the meaning of the fields in the `menu` object. + +### path + +type {string} required + +The format is: [top existing menu path][/path1][/path2], and the following is reasonable. +- `i18n:menu.extension` - with the extension menu as the parent menu +- `i18n:menu.extension/Hello World` - adds a `Hello World` menu item to the extension menu as a parent menu +- `MyMenu` - Adds a `MyMenu` menu to the top menu bar as a parent menu +- `MyMenu/Hello World` - adds a `MyMenu` to the top menu bar and adds a `Hello World` menu item as a parent + +In the top menu bar, the pre-defined menus are +- i18n:menu.project - the "Project" menu +- i18n:menu.node - the "Node" menu +- i18n:menu.panel - "Panel" menu +- i18n:menu.extension - "Extensions" menu +- i18n:menu.develop - "Developers" menu + +### label + +Type {string} Required + +The name of the menu item, supports i18n:key syntax. + +### icon + +Type {string} Optional + +Relative path to the icon of the menu, the material used by the extension is usually placed under a folder named `static`, if it doesn't exist, create a new one. + +### message + +Type {string} Optional + +The message that will be triggered when the menu is clicked, this message needs to be defined in `contributions.messsages` first. diff --git a/versions/4.0/en/editor/extension/contributions-messages.md b/versions/4.0/en/editor/extension/contributions-messages.md new file mode 100644 index 0000000000..dc8f1d8c41 --- /dev/null +++ b/versions/4.0/en/editor/extension/contributions-messages.md @@ -0,0 +1,83 @@ +# Customized Messages + +In the Cocos Creator editor architecture, all interactions are implemented through message communication, and this article will explain how to customize a message. + +## View the Public Message List + +Find **Developer** -> **Message Manager** in the top menu bar of the editor to open the message management panel, which shows the public messages of each system of the editor and their descriptions. + +![extension-message-mgr-menu](./image/extension-message-mgr-menu.png) + +![extension-message-mgr-panel](./image/extension-message-mgr-panel.png) + +## Define a message + +Only messages defined in the `contributions.messages` field of the `package.json` file can be used. The messages are defined as follows. + +```json +{ + "name": "hello-world", + "contributions": { + "messages": { + "test-messasge": { + "public": false, + "description": "", + "doc": "", + "methods": [] + } + } + } +} +``` + +`test-messasge` is the name of the message, and we will explain the meaning of each property one by one. + +### public + +Type `{string}` Optional + +Whether to display this message externally, if true, the basic information of this message will be displayed on the message list interface. + +### description + +Type `{string}` Optional + +If public is true, some simple descriptions will be displayed in the message list, supporting `i18n: key` syntax. + +### doc + +Type `{string}` Optional + +If public is true, some documents of this message will be displayed, supporting i18n:key syntax. + +This document is written and rendered in markdown format. + +### methods + +Type `{string[]}` Optional + +The method queue triggered by the message. + +This is an array of strings. The strings are methods on the extension or panel. + +If it is a method on the extension, directly define `methodName`, if you want to trigger a method on the panel, you must fill in `panelName.methodName`. For example, the `ready` method of the scene manager is `scene:ready`. + +## Define broadcast message + +When developing an extension, you need to send some notifications to other extension after completing an action. If these notifications also need to be displayed on the **Developer -> Message List** panel, you can define the message like this: + +```json +{ + "name": "hello-world", + "contributions": { + "messages": { + "hello-world:ready": { + "public": true, + "description": "hello-world ready notification" + } + } + } +} +``` + +For more information, please refer to the document [Message System](./messages.md). diff --git a/versions/4.0/en/editor/extension/contributions-preferences.md b/versions/4.0/en/editor/extension/contributions-preferences.md new file mode 100644 index 0000000000..aaf0703323 --- /dev/null +++ b/versions/4.0/en/editor/extension/contributions-preferences.md @@ -0,0 +1,128 @@ +# Extending the Preferences Panel + +## Preferences Panel Introduction + +The **Cocos Creator/File -> Preferences** menu can be found in the top menu bar, as shown in the following image: + +![preferences](./image/preferences-menu.png) + +Clicking on it opens the Preferences panel, as shown below. + +![preferences](./image/preferences-tool.png) + +The Preferences panel is divided into left and right sides. + +- The left side shows the names of the feature extensions that provide configuration items. +- On the right side is the operation panel rendered according to the configuration. + +Changes on the panel are immediately modified to the corresponding configuration items. For more information about the **Preferences** panel, please refer to the document [Preferences](../../editor/preferences/index.md). + +## Customization panel + +Cocos Creator allows each extension to register its own editor configuration, which is then displayed within the Preferences panel. + +The preferences control editor-related configurations and will act on all projects. If you want to add only project-specific configurations, please refer to the documentation [Custom Project Settings Panel](./contributions-project.md). + +### Two ways of preference setting + +Preference settings allow to display the configuration in two ways. + +1. general configuration +2. Lab configuration + +Generic settings are displayed directly as tabs, while lab switches are displayed centrally in a separate tab. + +- When the functionality provided by the plug-in is more stable it is recommended to place the configuration data within the generic functionality. +- When the functions provided by the plug-in are in the development stage it is recommended that the switch configuration data of the function be placed in the lab configuration. + +### Preferences definition + +Extending preferences depend on data configuration and need to be defined in `contributions.profile.editor` first. + +> **Note**: The configuration data in the preferences should be stored in the `profile.editor` field. + +Once the data fields are defined, you also need to define the data to be displayed and the UI components to be used to display it in the `contributions.preferences` field. This is shown below. + +```JSON5 +{ + //`package.json` + "name": "first-panel", + "contributions": { + "profile": { + "editor": { + "foo": { + "default": 1, + "label":"foo" + }, + "foo1": { + "default": 1, + "label":"foo1" + }, + "foo2": { + "default": false, + "label":"foo2" + }, + "foo3": { + "default": 0, + "label":"foo3" + } + } + }, + "preferences": { + "properties": { + "foo1": { + "ui": "ui-slider", + "attributes": { + "min": 0, + "max": 1, + "step": 0.1 + } + }, + "foo2": { + "ui": "ui-checkbox" + }, + "foo3": { + "ui": "ui-select", + "items": [ + { + "value": 0, + "label": "ITEM 0" + }, + { + "value": 1, + "label": "ITEM 1" + }, + { + "value": 2, + "label": "ITEM 2" + } + ] + } + }, + "laboratory": ["foo"] + } + } +} +``` + +In the above example, 4 data items are defined in the `contributions.profile.project` field: `foo`, `foo1`, `foo2`, `foo3`. + +For more information on how to define `profile` related configuration, please refer to [Configuration System](./profile.md). + +In the `contributions.preferences` field, we define `properties` and `laboratory`. + +### General configuration (properties) + +The fields defined in `properties` will be displayed independently in a new tab with the same name as the extension in the preferences panel, as follows: + +![preferences-tool-custom](./image/preferences-tool-custom.png) + +### Laboratory configuration (laboratory) + +The fields defined in `laboratory` will be displayed in the **Laboratory** tab in the preferences panel, as follows: + +![preferences-tool-custom-laboratory](./image/preferences-tool-custom-laboratory.png) + +## UI Component Configuration + +This example shows the usage of 4 common UI components in customizing the preferences panel, in theory all UI components with `value` attribute can be used in the preferences panel, please refer to the document [UI Components](./ui.md). diff --git a/versions/4.0/en/editor/extension/contributions-project.md b/versions/4.0/en/editor/extension/contributions-project.md new file mode 100644 index 0000000000..e4388f2040 --- /dev/null +++ b/versions/4.0/en/editor/extension/contributions-project.md @@ -0,0 +1,142 @@ +# Extending Project Settings Panel + +## Panel Introduction + +**Project Settings** holds the configuration related to the project operation, which is stored in the `settings` folder under the project directory and needs to be included in the version management, so that multiple people can share the configuration, otherwise it may lead to inconsistent operation on different machines. + +The **Projects** -> **Projects Settings** menu can be found in the top menu bar, as shown below: + +![project-settings-menu](./image/project-settings-menu.png) + +Click on it to open the settings panel, as shown in the following figure:s + +![project-settings-panel](./image/project-settings-panel.png) + +On the left side of the project settings panel is the function module tab, and on the right side is the configuration modification interface for the corresponding function. + +We can add custom configurations to the project by customizing the display data in this panel, and visualize and manage the project configuration with the help of the project settings panel. + +If you want to take effect for all projects, you need to customize the editor-related configuration, please refer to the document [Extending the Preferences Panel](./contributions-preferences.md). + +The **Custom Item Settings** function allows a we to register multiple tabs, so there will be a small line on the left tab indicating which function the tab belongs to. + +## Data Configuration and Display + +Custom project settings need to rely on data configuration, you need to define the relevant data fields in `contributions.profile.project` first. + +> **Note**: The configuration data in the project settings should be stored in the `profile.project` field. + +Once the data fields are defined, you also need to define the data to be displayed and the UI components to be used to display it in the `contributions.project` field. This is shown below. + +```json5 +// package.json +{ + "name": "project-test", + "contributions": { + "profile": { + "project": { + "foo": { + "default": 1 + }, + "foo1": { + "default": 0.4 + }, + "foo2": { + "default": false + }, + "foo3": { + "default": 0 + }, + "foo4": { + "default": "label" + } + } + }, + "project": { + "tab1": { + "label": "test", + "content": { + "foo": { + "ui": "ui-num-input" + }, + "foo1": { + "ui": "ui-slider", + "attributes": { + "min": 0, + "max": 1, + "step": 0.1 + } + }, + "foo2": { + "ui": "ui-checkbox" + }, + "foo3": { + "ui": "ui-select", + "items": [ + { + "value": 0, + "label": "ITEM 0" + }, + { + "value": 1, + "label": "ITEM 1" + }, + { + "value": 2, + "label": "ITEM 2" + } + ] + } + } + }, + "tab2": { + "label": "test2", + "content": { + "foo4": { + "ui": "ui-input" + } + }, + "foo2": { + "ui": "ui-checkbox" + }, + "foo3": { + "ui": "ui-select", + "items": [ + { + "value": 0, + "label": "ITEM 0" + }, + { + "value": 1, + "label": "ITEM 1" + }, + { + "value": 2, + "label": "ITEM 2" + } + ] + } + }, + "tab1": { + "label": "test" + } + } + } +} +``` + +In the above example, 4 data items are defined in the `contributions.profile.project` field: `foo`, `foo1`, `foo2`, `foo3`. + +For more information on how to define `profile` related configuration, please refer to [Configuration System](./profile.md). + +There are 2 tabs defined in the `contributions.project` field: `test`, `tes2`. + +In the `test` tab, each of the 4 data items is configured, see [UI Component Configuration](##UI%20Component%20Configuration) later for the specific configuration properties. + +After refreshing the extension in the Extension Manager list, open the **Project Settings Panel** again via the **Project -> Project Settings** menu to see the following screen. + +![project-settings-panel-custom](./image/project-settings-panel-custom.png) + +## UI Component Configuration + +This example shows the usage of 4 common UI components in customizing the project settings panel, in theory all UI components with `value` attribute can be used in customizing the project settings panel, please refer to the documentation [UI Components](./ui.md). diff --git a/versions/4.0/en/editor/extension/contributions-shortcuts.md b/versions/4.0/en/editor/extension/contributions-shortcuts.md new file mode 100644 index 0000000000..c321e0ca22 --- /dev/null +++ b/versions/4.0/en/editor/extension/contributions-shortcuts.md @@ -0,0 +1,71 @@ +# Extending Shortcut + +Shortcut keys within the editor are managed by the "Shortcut Key Manager". Each shortcut key can be bound to a message, and when the shortcut key is pressed, the bound message will be triggered. + +## Defining Shortcut + +Defining the shortcuts needs to be done in the `contributions.shortcuts` field of `package.json`, as follows: + +```json5 +// package.json +{ + "name": "hello-world", + "panels": { + "default": { + "main": "./panel.js" + } + }, + "contributions": { + "messages": { + "undo": { + "title": "i18n:hello.messages.undo.title", + "methods": ["say-undo"] + } + }, + "shortcuts": [ + { + "message": "undo", + "when": "panelName === 'hello-world'", + "win": "ctrl+z", + "mac": "cmd+z", + } + ] + } +} +``` + +In this example, we define a shortcut key for the **undo** operation, which is `CTRL + Z` on Windows and `CMD + Z` on macOS. + +When the corresponding shortcut key is pressed, the `undo` message is sent. + +> **Note**: This message needs to be defined in `contributions.messages`, please refer to the documentation [Customized Messages](./contributions-messages.md). + +## Parameter descriptions + +Below we will see the details of each parameter of `contributions.shortcuts`. + +### message + +Type {string} Required + +Shortcut-bound message that will be sent when this shortcut is triggered. Shortcut pressed messages can only be sent to the current extension. + +### when + +Type {string} Optional + +The shortcut will be triggered only under certain conditions. + +`"when": "PanelName === 'hello-world'"` means that the `message` message will be sent when the shortcut key is pressed when the panel name that gets focus is `hello-world`. + +### win + +type {string} required + +On Windows platform, the keystroke to listen to. + +### mac + +Type {string} Required + +On macOS, the keystroke to listen to. diff --git a/versions/4.0/en/editor/extension/contributions.md b/versions/4.0/en/editor/extension/contributions.md new file mode 100644 index 0000000000..c6d31c07ca --- /dev/null +++ b/versions/4.0/en/editor/extension/contributions.md @@ -0,0 +1,53 @@ +# Extend Existing Functionality + +**Cocos Creator** supports contributions between extensions. + +When writing an extension, it is possible to query whether the existing functions in the editor provide the ability of receiving `contributions` externally. + +If there are functions that provide the ability of receiving `contributions` externally, use these functions when writing extensions. + +## Contributions Data Definition + +The `contributions` function, uniformly defined in the `contributions` field in `package.json`, is as follows. + +```JSON5 +{ + "name": "hello-world", + "contributions": { + "builder":{ ... }, + "assets":{ ... }, + "profile": { ... }, + "scene": { ... }, + "menu": [ ... ], + "inspector":{ ... }, + "messages": { ... }, + "shortcuts": { ... }, + "preferences": { ... }, + "project": { ... } + }, +} +``` + +## Field Descriptions + +`contributions` provides the ability to interact with the editor's various functional systems, the main functions involved are as follows: + +- `builder` - custom build process, please refer to the documentation [Extending Build Process](../publish/custom-build-plugin.md) for details. + +- `assets` - Enhanced Explorer panel, please refer to the documentation for more details [Extending the Assets Panel](../assets/extension.md). + +- `profile` - defines the configuration needed for the extension, see the documentation [Configuration System](./profile.md). + +- `scene` - Write scripts in the extension that need to interact with the engine and project scripts, see the documentation [Calling the Engine API and Project Script](./scene-script.md). + +- `inspector` - custom **Inspector** panel, see the documentation [Custom Property Inspector Panel](./inspector.md). + +- `menu` - defines the menu information that the extension needs to add, see the documentation [Customize the Main Menu](./contributions-menu.md). + +- `messages` - defines the list of messages that the extension needs to use, please refer to the documentation [Custom Messages](./contributions-messages.md). + +- `shortcuts` - defines the shortcuts required by the extension, see the documentation [Extending Shortcut](./contributions-shortcuts.md). + +- `preferences` - customize the preferences, see the documentation [Extending the Preferences Panel](./contributions-preferences.md). + +- `project` - Custom project settings, see the documentation [Extending Project Settings Panel](./contributions-project.md). diff --git a/versions/4.0/en/editor/extension/create-extension.md b/versions/4.0/en/editor/extension/create-extension.md new file mode 100644 index 0000000000..b2f0911c7a --- /dev/null +++ b/versions/4.0/en/editor/extension/create-extension.md @@ -0,0 +1,59 @@ +# Extension Templates and Compile Builds + +This article will explain in detail how to create an extension with a panel and use it with the extension template provided by Creator. + +Select the **Extension -> Create Extension** menu on the main editor menu to open the New Panel. + +![create-extension-menu](./image/create-extension-menu.png) + +![create-extension-panel](./image/create-extension-panel.png) + +## Template Types + +Cocos Creator provides 4 types of extension templates for quickly creating a new extension project. +- **Blank**: empty extension. +- **HTML Panel**: HTML-based extension template with a popup panel. +- **Vue2.x Panel**: Vue2.x based extension template with a popup panel. +- **Vue3.x Panel**: Vue3.x based extension template with a popup panel. + +The extensions created in the above 4 ways differ only in workload and technical selection, and there is no difference in the achievable capabilities of the extensions, so developers can choose according to their needs and technical background. + +> **Note**: Please refer to the `README.md` file in the corresponding extension package directory after creation. + +## Option Description + +| Options | Description | +| :--- | :----- | +| **Extension's name** | The extension name created must not start with `_` or `.`, and cannot contain capital letters. Because extensions become part of the URL, they also cannot contain illegal characters from the URL, `.`, `'` and `,`. | +| **Author** | Author of extension | +| **Editor dependency version** | The Cocos Creator version required by the runtime of the created extension. | +| **Show in the manager** | If this item is checked, **Extension Manager** will automatically open and show the created extensions when the extension creation is finished.
If this is unchecked, you can click **Extensions -> Extension Manager** in the menu bar at the top of the editor to view the extensions when they are created.| +| **Show in folder** | If this item is checked, the extension will be automatically opened in the system file manager when the extension is created. | +| **Extension's location** | The directory where the extension package was created. Since v3.7, extension can only be created with the **Project** option. | + +## Extension's Location + +### Project + +Apply the extension package to the specified Cocos Creator project, **Project** with the path: + +`${your project address}/extensions` + +## Dependency Installation and Compilation Build + +Once created, open the directory where the extension package is located and execute the following command: + +```bash +## Install the dependency module +npm install +# Build +npm run build +``` + +The extension depends on **Node.js** third-party libraries, so you need to run `npm install` in the extension directory before enabling the extension to compile properly. + +TypeScript can be written with full code hints and is more engineering friendly, so Cocos Creator recommends using a TypeScript-based workflow. + +The extension templates provided by default are also based on TypeScript and need to be compiled with `npm run build` to run. + +> **Note**: The extension `tsconfig.json` configuration file has `resolveJsonModule` enabled to get the extension name from the imported extension's `package.json`, so TypeScript requires **v4.3** and above, otherwise, importing `json` outside the root directory, otherwise the compilation result will be wrong path. diff --git a/versions/4.0/en/editor/extension/define.md b/versions/4.0/en/editor/extension/define.md new file mode 100644 index 0000000000..0a8b317158 --- /dev/null +++ b/versions/4.0/en/editor/extension/define.md @@ -0,0 +1,91 @@ +# Definition of Extension + +The extension package needs to have all the features and some basic information predefined in the `package.json` file, as follows. + +```JSON5 +{ + "package_version": 2, + "version": "1.0.0", + "name": "first-panel", + "tilte": "i18n:first-panel.title", + "description": "i18n:first-panel.description", + "author": "Cocos Creator", + "editor": ">=3.4.2", + "main": "./dist/main.js", + "dependencies": { ... }, + "devDependencies": { ... }, + "panels": { ... }, + "contributions": { + }, + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + } +} +``` + +## package_version + +Type {number} Required + +The version number of the extension, which is used to submit the version verification of the extension, as well as some upgrades of the extension itself, and data migration as a basis for comparison. + +## version + +Type {string} Required + +The version number of the extension, mainly used for display, if you want to make logical judgments, `package_version` is recommended. + +## name + +Type {string} Required + +The name of the extension, this name should correspond to the extension folder. + +## title + +Type {string} Optional + +The title of the extension, when `title` is configured, `title` will be used when the extension needs to be displayed, supports [Multilingual System (i18n)](./i18n.md) configuration. + +## description + +Type {string} Optional + +The description of the extension, a brief overview of the extension's functionality. Supports [Multilingual System (i18n)](./i18n.md) for multilingual syntax. + +## author + +type {string} optional + +The name of the author of the extension, which will be shown in the "Extension Manager". + +## editor + +type {string} Optional + +Description of the editor version supported by the extension, conforming to the [`semver` semantic version control specification](https://semver.org/). + +## main + +type {string} Optional + +A relative path to a js file that defines the function entry file. When the extension starts, the js file pointed to by the `main` field will be executed and the corresponding method will be triggered or executed according to the flow. + +## panels + +Type {[name: string]: PanelInfo} Optional + +The panel information defined within the extension. You can use `Editor.Panel.open('hello-world.list');` to open the defined panel. For details, please refer to [Extension Panel](./panel.md). + +## contributions + +type {[name: string]: any} optional + +`contributions` provides the ability to interact with the editor's feature system, see the documentation [Extend existing functionality](./contributions.md). + +## scripts + +type {[name: string]: any} required + +Extends the executable command line. diff --git a/versions/4.0/en/editor/extension/extension-change-name.md b/versions/4.0/en/editor/extension/extension-change-name.md new file mode 100644 index 0000000000..80c40d950b --- /dev/null +++ b/versions/4.0/en/editor/extension/extension-change-name.md @@ -0,0 +1,25 @@ +# Change the Name of a Extension + +## Change the Display Name + +If you want to rename the extension, just change the `name` field in the `package.json` file. The code example is as follows: + +```JSON5 +// "name": "simple-1649426645745" +"name": "hello-world" +``` + +Change the `name` field to "hello-world" as above and refresh the extension in the extension manager panel to see the name of the extension changed to **hello-world**. + +![extension](first/extension-hello-world.png) + +## Change the Extension Folder Name + +If you want to change the folder name along with it, you need to restart Cocos Creator after the folder change in order to make the extension with the modified folder name take effect again. + +If you modify the folder name, **i18n** the path to the multilanguage related string needs to be modified as well, as follows. + +```json5 +// "description": "i18n:simple-1649426645745.description", +"description": "i18n:hello-world.description", +``` diff --git a/versions/4.0/en/editor/extension/extension-manager.md b/versions/4.0/en/editor/extension/extension-manager.md new file mode 100644 index 0000000000..d7701ef557 --- /dev/null +++ b/versions/4.0/en/editor/extension/extension-manager.md @@ -0,0 +1,37 @@ +# Extension Manager Panel + +The **Extension Manager** is used to manage extensions within the editor. Click **Extension -> Extension Manager** in the main menu bar of Cocos Creator to open. + +![extension-manager-menu](./image/extension-manager-menu.png) + +The Extension Manager panel is as follows. + +![extension-manager](./image/extension-manager.png) + +## Functions + +Its relevant functions are described as follows. + +1. Extension type, divided into **Cocos Official** and **Builtin**, selected by drop-down menu. +2. Installed, click it to show the currently installed extensions. +3. From left to right, **Search Extensions**, **Import Extension** and **Refresh Extension List** + - **Search Extensions**: When clicked, you can find the extensions within the current project by keywords in the input box shown below. + ![search](./image/search.png) + - **Import Extension**: Click to import a new extension via zip file + - **Refresh Exntesion List**: Refresh the current status of all extensions +4. Extension list. + + ![detail](./image/ext-detail.png) + + For each extension, the extension name, icon, version number and description are displayed on the left side. + The buttons on the right side are. + - **Open extension directory** + - **Delete** the extension + - **Enable/Disable** the extension + The built-in and official extensions cannot be deleted or disabled, some of the buttons need to be visible by moving the mouse over the entry. +5. Details of extensions + +## Import Methods + +- **Import Extension Folder**: If you choose this import method, you need to copy the extension to the "extensions" directory of the project. +- **Developer Import**: After choosing this import method, the extension will be referenced in the form of a soft link and doesn't need to be copied to the "extensions" directory, which facilitates developers in project management. diff --git a/versions/4.0/en/editor/extension/first-communication.md b/versions/4.0/en/editor/extension/first-communication.md new file mode 100644 index 0000000000..3139c741d4 --- /dev/null +++ b/versions/4.0/en/editor/extension/first-communication.md @@ -0,0 +1,157 @@ +# Getting Started Example - First Data Interaction + +In the previous two documents [Getting Started Example - Menu](./first.md) and [Getting Started Example - Panel](./first-panel.md), we introduced: +- How to create extensions +- How to define menus in extensions +- how to define messages in an extension +- How to define a panel in an extension + +This article demonstrates how two extensions can communicate with each other and will cover three topics: +- How to open a panel of another extension +- How to send a message to another extension +- How to send and listen to broadcast messages + +## Open Another Extension's Panel + +Sometimes we need to open another extension in an extension we wrote, so next we'll try to modify the extension example in **Getting Started Example - Menu** so that it opens the panel defined in **Getting Started Example - Panel**. + +The modified `package.json` looks like this: + +```JSON5 +{ + "package_version": 2, + "version": "1.0.0", + "name": "hello-world", + ... + "contributions": { + "menu": [ + { + "path": "Develop/HelloWorld", + "label": "test", + "message": "log" + }, + { + "path": "Develop/HelloWorld", + "label": "open other", + "message": "open-other" + } + ], + "messages": { + "log": { + "methods": [ + "log" + ] + }, + "open-other": { + "methods": [ + "openOther" + ] + } + } + } +} +``` + +We modified `contributions.menu`, added `open other` menu item, and put all the menus of this extension under Develop/HelloWorld. After refreshing the extension, you can find the menu items in the top menu bar as shown below. + +![extension-menu-hw.png](./first/extension-menu-hw.png) + +In `contributions.messages`, we add an `open-other` message and let the `openOther` function in `main.ts` handle this message. + +The extension in **Getting Started Example - Panel** is `first-panel`, so we use `Editor.Panel.open('extension')` to open its default panel, as follows: + +```typescript +openOther(){ + Editor.Panel.open('first-panel'); +} +``` + +After executing the `npm run build` command at the root of `hello-world`, go to **Extension Manager** to refresh the `hello-world` extension. + +Click on the **Develop** -> **HelloWorld** -> **open other** menu item and you will see the example panel open. + +## Communication with other extensions + +### Directional communication + +In the above example, we open the `first-panel` panel in `hello-world` with `Editor.Panel.open('extension')`. But if we are trying to do something else, this solution won't work. + +When an extension wants to call the function of another extension, this can be done by sending a message to one of the extensions with the following function: + +```typescript +Editor.Message.send(extensionName:string,messasge:string,...args:any[]) +``` + +The messages defined in `contributions.messages` of each extension are available to the public by default. In `first-panel` we find the `open-panel` message, which is used to open its own default panel. For simplicity, we replace the `openOther` function in `main.ts` in `hello-world` with the following: + +```typescript +openOther(){ + Editor.Message.send('first-panel','open-panel'); +} +``` + +After recompiling the `hello-world` extension and refreshing it, click **Develop** -> **HelloWorld** -> **open other** menu item again, you can see the default panel of `first-panel` is opened. + +### Broadcast communication + +When an extension wants to notify all extensions across the system of the completion of an event, it can do so by broadcasting a message with the following function. + +```typescript +Editor.Message.broadcast(message:string, ...args:any[])` +``` + +Next, we define a broadcast message called `first-panel:open`, which is broadcast by the `first-panel` extension and listened to by the `hello-world` extension. + +In `hello-world`, we add a new message listener and specify the handler function, with the following modified `contributions.messages`: + +```json5 +{ + "messages": { + "log": { + "methods": [ + "log" + ] + }, + "open-other": { + "methods": [ + "openOther" + ] + }, + "first-panel:open":{ + "methods": [ + "onFirstPanelOpen" + ] + } + } +} +``` + +Then add the following handler function to `main.ts` of `hello-world`: + +```typescript +onFirstPanelOpen(){ + console.log("hello-world knows first-panel is open"); +} +``` + +The transformation as a listener is done, next we modify the broadcast side `first-panel`. + +Add the following broadcast message code to the ``src/panels/default/index.ts :ready`` function in the ``first-panel` project. + +```typescript +Editor.Message.broadcast("first-panel:open"); +``` + +The `ready` function will be called when the default panel of `first-panel` is opened, at which point the `first-panel:open` message will be broadcast. + +> **Note**: Broadcasters can also listen for their own broadcast messages in messages, but this is usually not necessary. + +Compile and refresh the two extensions separately, click again on the **Develop** -> **HelloWorld** -> **open other** menu item, and you will see the sample panel opened, in addition to the following print in the Cocos Creator console window. + +``` +hello-world knows first-panel is open +``` + +This means that the ``hello-world`` extension has received a broadcast message from the ``first-panel`` extension. + +For more message-related details, please refer to the documentation [Message System](./messages.md). diff --git a/versions/4.0/en/editor/extension/first-panel.md b/versions/4.0/en/editor/extension/first-panel.md new file mode 100644 index 0000000000..02dd1cd687 --- /dev/null +++ b/versions/4.0/en/editor/extension/first-panel.md @@ -0,0 +1,148 @@ +# Getting Started Example - Panel + +In the document [Getting Started Example - Menus](./first.md) explains how to create a minimal extension, next we look at how to create a panel and communicate with it. + +## Creating via templates + +The quickest way to create a panel in Cocos Creator is through the **Extension template with a panel**, as shown in the following image: + +![extension-first-panel-create](./first/extension-first-panel-create.png) + +After clicking the **Create** button, you can find the extensions/first-panel extension in the project root directory. + +### Compile, Install + +In the command line tool, locate the extensions/first-panel directory and execute the following statement: + +```bash +npm install +npm run build +``` + +After the command is executed, go back to the **Extension Manager**, find the first-panel extension, enable it and refresh it, as shown in the following image: + +![extension-first-panel-enable](./first/extension-first-panel-enable.png) + +### View Panel + +After enabling and refreshing the extension, you can find the menu items in the **Panel** menu as shown in the following figure. + +![extension-first-panel-menu](./first/extension-first-panel-menu.png) + +Click on the **Default panel** menu item to bring up the panel shown below. + +![extension-first-panel](./first/extension-first-panel.png) + +This example also defines another menu item for communication in the **Developer** menu, as shown in the following image. + +![extension-first-panel-sendmsg](./first/extension-first-panel-sendmsg.png) + +After clicking the **Send message to Default Panel** button shown in the red box in the above image, you can see that the content displayed on the panel will change. + +> **Note**: If this menu is not displayed, please open the **Extension Manager** panel and make sure the extension is enabled. +> +> ![enable-first-panel](./image/enable-first-panel.png) + +## Panel Explanation + +In the following, we'll explain the panel directory structure, definition and communication mechanism one by one. + +### Panel Directory Structure + +In the project directory, find `./extensions/first-panel` folder, which is the directory where the entire extension can be viewed. + +![extension-first-panel-folder](./first/extension-first-panel-folder.png) + +As shown above, there are more `static` and `panels` directories than hello-world. + +`static` - used to store panel layout files, such as css\html, etc. + +`panels` - used to store panel-related source code, each panel has an `index.ts` entry source file. + +`index.ts`, `style`, `template` Please refer to the documentation [Write a Custom Panel](./panel-boot.md) + +### Description File package.json + +Before we understand the panel, let's look at the panel-related definitions in `package.json`, as follows: + +```json +{ + "package_version": 2, + "version": "1.0.0", + "name": "first-panel", + "description": "i18n:first-panel.description", + "main": "./dist/main.js", + "dependencies": { + "fs-extra": "^10.0.0" + }, + "devDependencies": { + "@types/node": "^16.0.1", + "@types/fs-extra": "^9.0.5", + "typescript": "^4.3.4" + }, + "panels": { + "default": { + "title": "first-panel Default Panel", + "type": "dockable", + "main": "dist/panels/default", + "size": { + "min-width": 400, + "min-height": 300, + "width": 1024, + "height": 600 + } + } + }, + "contributions": { + "menu": [ + { + "path": "i18n:menu.panel/first-panel", + "label": "i18n:first-panel.open_panel", + "message": "open-panel" + }, + { + "path": "i18n:menu.develop/first-panel", + "label": "i18n:first-panel.send_to_panel", + "message": "send-to-panel" + } + ], + "messages": { + "open-panel": { + "methods": [ + "openPanel" + ] + }, + "send-to-panel": { + "methods": [ + "default.hello" + ] + } + } + }, + "author": "Cocos Creator", + "editor": ">=3.4.2", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + } +} +``` + +`panels`: {} - The panels defined in this extension +- default: String - defines a panel named default + - title: String - the title of the panel + - type: String - the type of the panel + - main: String - the panel source directory + - size: {} - size information + - min-width: Number - the minimum width + - min-height: Number - the minimum height + - width: Number - the default width of the panel + - height: Number - the default height of the panel + +## Additional Reading + +`panel` For a detailed explanation of panels, please refer to the documentation [Extension Panel](./panel.md). + +`i18n` for multi-language configuration, please refer to the documentation [Multilingual System (i18n)](./i18n.md). + +`messages` For the complete message definition mechanism, please refer to the documentation [Customized Messages](./contributions-messages.md). diff --git a/versions/4.0/en/editor/extension/first.md b/versions/4.0/en/editor/extension/first.md new file mode 100644 index 0000000000..809e281036 --- /dev/null +++ b/versions/4.0/en/editor/extension/first.md @@ -0,0 +1,200 @@ +# Getting Started Example - Menu + +This article will demonstrate how to create a Cocos Creator extension, and it will cover the following points. +- Creating an extension +- Adding a menu +- Menu Messages + +## Create and Install Extensions + +Find the **Extension -> Create** menu in the editor's menu bar, as shown below: + +![create-extension-menu](image/create-extension-menu.png) + +After clicking **Create**, the creation panel will pop up as shown below: + +![create-extension-panel](image/create-extension-panel.png) + +Cocos Creator provides 4 extension templates as shown above for quickly creating a new extension project. + +To demonstrate the template creation process more simply, let's select the **Blank** template and click the **Create Extension** button at the bottom right of the panel to build an extension package. + +For more template creation related content, please refer to the document [Extension Templates and Compile Builds - Template Types](./create-extension.md). + +## Extension Manager + +After the extension is successfully created, click **Extensions-> Extension Manager -> Project/Global** in the top menu bar, you can see the extension you just created. The newly created extension is not enabled by default, click the Enable button to enable this extension. + +![extension](first/extension.png) + +For more information about extension management, please refer to the documentation [Extension Manager - Extension List](./extension-manager.md#Extensions%20list). + +## Extended Directory + +Click the ![folder](first/folder.png) button to open the directory where the extensions are located. Take the **Blank** template as an example, the directory structure is as follows: + +![extension-folder](image/extension-folder-blank.png) + +The function of each subfile (folder) is as follows. +- `@types` - TypeScript definition file. +- `dist` - TypeScript-generated javascript code. +- `i18n` - Multilanguage configuration. +- `src` - TypeScript source code. +- `package.json` - The extension description file. +- `README-CN/EN.md` - Chinese/English description file. +- `tsconfig.json` - TypeScript configuration file. + +## Extension Definition File `package.json` + +Each extension needs a `package.json` file to describe the purpose of the extension. + +Only after the complete definition of the description file `package.json`, the editor will be able to know the specific functionality, loading entry and other information defined in the extension. + +> **Note**: Although many of the fields in `package.json` are defined similarly to `package.json` of the `node.js` npm module, the npm module downloaded from the npm community cannot be used directly as a Cocos Creator extension. You can call the npm module from within the Cocos Creator extension to make the extension capable of doing so. + +Open the `package.json` file and you will see the following: + +```json +{ + "package_version": 2, + "version": "1.0.0", + "name": "simple-1649426645745", + "description": "i18n:simple-1649426645745.description", + "main": "./dist/main.js", + "devDependencies": { + "@types/node": "^16.0.1", + "typescript": "^4.3.4" + }, + "author": "Cocos Creator", + "editor": ">=3.4.2", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + } +} + +``` + +The description of each field is as follows. +- `package_version`: Number - the value of the version number. +- `version`: String - The version number string, it is recommended to use [semver](http://semver.org/) format to manage your package version. +- `name`: String - defines the name of the package, the package name is globally unique. Please refer to [option description] for naming rules (. /create-extension.md). +- `description`: Stirng - Extension description, used to briefly introduce the key features, usage and other information of the extension, supports **i18n** multi-language settings. +- `main`: String - The entry program file. +- `devDependencies`: {} - Extension dependencies. In this example, the extension depends on NodeJS version 16.0.1 and TypeScript version 4.3.4. +- `author`: String - Author information. +- `editor`: String - the supported Cocos Creator editor version. +- `scripts`: {} - Script compilation related commands. + +## Define Menus and Messages + +Replace `package.json` with the following: + +```json +{ + "package_version": 2, + "version": "1.0.0", + "name": "simple-1649426645745", + "description": "i18n:simple-1649426645745.description", + "main": "./dist/main.js", + "devDependencies": { + "@types/node": "^16.0.1", + "typescript": "^4.3.4" + }, + "author": "Cocos Creator", + "editor": ">=3.4.2", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + }, + //------------------------------ + "contributions": { + "menu": [{ + "path": "Develop", + "label": "test", + "message": "log" + }], + "messages": { + "log": { + "methods": ["log"] + } + } + } +} +``` + +The description of the new fields is as follows. +- `contributions`: Object (optional) - configuration related to the extension of the editor's existing functionality + - `menu`: [], register the menu and bind the message. For details, please refer to [Customize the Main Menu](./contributions-menu.md). + - `messages`:[] - Register editor messages, which can be bound to one or more methods defined within the extension. See [Customized Messages](./contributions-messages.md) for more definition data. + +For more definitions of the `package.json` format, please refer to [Definition of Extension](./define.md). + +## Installing Dependencies and Compiling + +After the extension is created, open the directory where the extension package is located and execute the following command. + +```bash +# install dependencies +npm install +# build +npm run build +``` + +For more information on building extensions, see the documentation [Extension Templates and Compile Builds](./create-extension.md). + +## Run Extensions + +Go back to the editor and click **Extension -> Extension Manager -> Project/Global** in the top menu bar and find the extension you created earlier. Click on the ![refresh](first/refresh.png) button on the right side of the extension to make the above changes take effect. + +If the extension has taken effect, a **Develop** menu will appear in the top menu bar area of Cocos Creator with a **test** menu item, as shown below. + +![menu-test](first/extension-menu-test.png) + +If you click on the **test** menu item, you will see that there is no response, because we have not yet written the code for the menu information. + +Next, we will see how to make the menu communicate with the extension. + +## Entry Program `main.ts` + +Each extension has a unique entry program `main.ts`, which is generated by default as follows: + +```typescript +/** + * @en Registration method for the main process of Extension + * @zh 为扩展的主进程的注册方法 + */ +export const methods: { [key: string]: (...any: any) => any } = { }; + +/** + * @en Hooks triggered after extension loading is complete + * @zh 扩展加载完成后触发的钩子 + */ +export const load = function() { }; + +/** + * @en Hooks triggered after extension uninstallation is complete + * @zh 扩展卸载完成后触发的钩子 + */ +export const unload = function() { }; +``` + +The methods defined in `export const methods` will be used as interfaces for operations that will be called across extensions via the [Message System](./messages.md) to be called across extensions or to communicate with panels. + +The entry program is the main process of the extension and will be loaded during the startup of Cocos Creator. + +## Menu Message Handling + +We modify the entry program slightly by adding a handler function that receives ``log`` messages, as follows. + +```typescript +export const methods: { [key: string]: (.. . any: any) => any } = { + log(){console.log('Hello World')}, +}; +``` + +Execute the `npm run build` command to compile the extension. + +Click on the ![refresh](first/refresh.png) button on the right side of the extension to make the above changes take effect. + +Click the `Develop/test` menu item again and you will see that "Hello World" is printed out in the Cocos Creator **console**. diff --git a/versions/4.0/en/editor/extension/first/delete.png b/versions/4.0/en/editor/extension/first/delete.png new file mode 100644 index 0000000000..a0bd4163e6 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/delete.png differ diff --git a/versions/4.0/en/editor/extension/first/enable-or-not.png b/versions/4.0/en/editor/extension/first/enable-or-not.png new file mode 100644 index 0000000000..d0c512bbd0 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/enable-or-not.png differ diff --git a/versions/4.0/en/editor/extension/first/enable.png b/versions/4.0/en/editor/extension/first/enable.png new file mode 100644 index 0000000000..c49a0d4eb5 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/enable.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-first-panel-create.png b/versions/4.0/en/editor/extension/first/extension-first-panel-create.png new file mode 100644 index 0000000000..a789d5eebd Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-first-panel-create.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-first-panel-enable.png b/versions/4.0/en/editor/extension/first/extension-first-panel-enable.png new file mode 100644 index 0000000000..b151b115be Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-first-panel-enable.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-first-panel-folder-origin.png b/versions/4.0/en/editor/extension/first/extension-first-panel-folder-origin.png new file mode 100644 index 0000000000..6ee820b818 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-first-panel-folder-origin.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-first-panel-folder.png b/versions/4.0/en/editor/extension/first/extension-first-panel-folder.png new file mode 100644 index 0000000000..fa9c130698 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-first-panel-folder.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-first-panel-install.png b/versions/4.0/en/editor/extension/first/extension-first-panel-install.png new file mode 100644 index 0000000000..fc95c5c9ae Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-first-panel-install.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-first-panel-menu.png b/versions/4.0/en/editor/extension/first/extension-first-panel-menu.png new file mode 100644 index 0000000000..44e76671a5 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-first-panel-menu.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-first-panel-sendmsg.png b/versions/4.0/en/editor/extension/first/extension-first-panel-sendmsg.png new file mode 100644 index 0000000000..1a35f1ac38 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-first-panel-sendmsg.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-first-panel.png b/versions/4.0/en/editor/extension/first/extension-first-panel.png new file mode 100644 index 0000000000..32985244cf Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-first-panel.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-hello-world.png b/versions/4.0/en/editor/extension/first/extension-hello-world.png new file mode 100644 index 0000000000..efb558bf9a Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-hello-world.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-menu-hw.png b/versions/4.0/en/editor/extension/first/extension-menu-hw.png new file mode 100644 index 0000000000..847403389c Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-menu-hw.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-menu-test.png b/versions/4.0/en/editor/extension/first/extension-menu-test.png new file mode 100644 index 0000000000..9205042cb1 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-menu-test.png differ diff --git a/versions/4.0/en/editor/extension/first/extension-package.png b/versions/4.0/en/editor/extension/first/extension-package.png new file mode 100644 index 0000000000..8977bf3054 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension-package.png differ diff --git a/versions/4.0/en/editor/extension/first/extension.png b/versions/4.0/en/editor/extension/first/extension.png new file mode 100644 index 0000000000..bc23d8c182 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/extension.png differ diff --git a/versions/4.0/en/editor/extension/first/folder.png b/versions/4.0/en/editor/extension/first/folder.png new file mode 100644 index 0000000000..ae8f1ec9f6 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/folder.png differ diff --git a/versions/4.0/en/editor/extension/first/refresh.png b/versions/4.0/en/editor/extension/first/refresh.png new file mode 100644 index 0000000000..b719f388b2 Binary files /dev/null and b/versions/4.0/en/editor/extension/first/refresh.png differ diff --git a/versions/4.0/en/editor/extension/i18n.md b/versions/4.0/en/editor/extension/i18n.md new file mode 100644 index 0000000000..1a31b0d171 --- /dev/null +++ b/versions/4.0/en/editor/extension/i18n.md @@ -0,0 +1,106 @@ +# Multilingual System (i18n) + +## What is i18n + +**i18n** (derived from the first and last characters i and n of the English word internationalization, with 18 being the number of characters in between) is short for "internationalization". + +In the field of information, i18n refers to the ability to adapt products (publications, software, hardware, etc.) to the needs of different languages and regions without much change. + +In the field of application development, i18n refers to the ability to display localized content for different languages and regions without modifying the internal code. + +The built-in i18n in the Cocos Creator extension system allows extensions to configure **key-value mapping** for multiple languages and use strings from different languages in the extension according to the editor's current language settings. + +## i18n Folder + +To enable the i18n, you need to create a new folder named i18n in the extension directory and add a corresponding `JavaScript` file for each language as key-value mapping data. + +The data file name should be consistent with the language code, e.g. `en.js` corresponds to English mapping data, `zh.js` corresponds to Chinese mapping data. As shown in the following figure. + +![i18n-folder](./image/i18n-folder.png) + +The mapping takes the key of the `JavaScript` object as the key value and is exported with the module information as follows: + +- `en.js` + + ```javascript + "use strict"; + module.exports = { + open_panel:"Default Panel", + send_to_panel:"Send message to Default Panel", + description:"Extension with a panel" + }; + ``` + +- `zh.js` + + ```javascript + module.exports = { + open_panel:"默认面板", + send_to_panel:"发送消息给面板", + description:"含有一个面板的扩展" + }; + ``` + +Take `open_panel` as an example, suppose the registered extension name is `first-panel`, then the corresponding text translation key is `first-panel.open_panel`. + +## Using in Scripts + +In TypeScript or JavaScript scripts, the translated text for the current language can be obtained through the ``Editor.I18n.t`'' interface. + +```typescript +let str = Editor.I18n.t('first-panel.open_panel'); +``` + +## Use in HTML Templates + +You can use the ui-label element for translation in HTML templates if you need to translate. + +```html + +``` + +> **Note**: `ui-label` is a normal in-line element, similar to span. + +### Use in json Files + +For example, when registering a menu path in the `package.json` of an extension package, the path can be translated into multiple languages using the form `i18n:${key}`, as long as the field supports i18n formatted paths. + +Example 1: Extension description + +```json5 +// "package_version": 2, +// "version": "1.0.0", +// "name": "first-panel", +"description": "i18n:first-panel.description", +// "main": "./dist/main.js", +``` + +Example 2: Panel Title + +```json5 +"panels": { + "default": { + "title": "first-panel Default Panel", + // "type": "dockable", + // "main": "dist/panels/default", + // "size": {...} + } +}, +``` + +Example 3: Menu path and display content + +```json +"menu":[ + { + "path": "i18n:menu.panel/first-panel", + "label": "i18n:first-panel.open_panel", + "message": "open-panel" + }, + { + "path": "i18n:menu.develop/first-panel", + "label": "i18n:first-panel.send_to_panel", + "message": "send-to-panel" + } +] +``` diff --git a/versions/4.0/en/editor/extension/image/category.png b/versions/4.0/en/editor/extension/image/category.png new file mode 100644 index 0000000000..f23a305b36 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/category.png differ diff --git a/versions/4.0/en/editor/extension/image/create-extension-menu.png b/versions/4.0/en/editor/extension/image/create-extension-menu.png new file mode 100644 index 0000000000..3c05598adb Binary files /dev/null and b/versions/4.0/en/editor/extension/image/create-extension-menu.png differ diff --git a/versions/4.0/en/editor/extension/image/create-extension-panel.png b/versions/4.0/en/editor/extension/image/create-extension-panel.png new file mode 100644 index 0000000000..6dbc663601 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/create-extension-panel.png differ diff --git a/versions/4.0/en/editor/extension/image/create.png b/versions/4.0/en/editor/extension/image/create.png new file mode 100644 index 0000000000..2c2970a8d3 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/create.png differ diff --git a/versions/4.0/en/editor/extension/image/dashboard-store.png b/versions/4.0/en/editor/extension/image/dashboard-store.png new file mode 100644 index 0000000000..aa2e597c21 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/dashboard-store.png differ diff --git a/versions/4.0/en/editor/extension/image/default-extension-panel.png b/versions/4.0/en/editor/extension/image/default-extension-panel.png new file mode 100644 index 0000000000..1d45b29cf4 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/default-extension-panel.png differ diff --git a/versions/4.0/en/editor/extension/image/electron-process.png b/versions/4.0/en/editor/extension/image/electron-process.png new file mode 100644 index 0000000000..642b4117a3 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/electron-process.png differ diff --git a/versions/4.0/en/editor/extension/image/enable-extension.png b/versions/4.0/en/editor/extension/image/enable-extension.png new file mode 100644 index 0000000000..b67e838923 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/enable-extension.png differ diff --git a/versions/4.0/en/editor/extension/image/enable-first-panel.png b/versions/4.0/en/editor/extension/image/enable-first-panel.png new file mode 100644 index 0000000000..38bf951ae6 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/enable-first-panel.png differ diff --git a/versions/4.0/en/editor/extension/image/ext-detail.png b/versions/4.0/en/editor/extension/image/ext-detail.png new file mode 100644 index 0000000000..95ae974526 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/ext-detail.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-database.png b/versions/4.0/en/editor/extension/image/extension-database.png new file mode 100644 index 0000000000..492c2585bf Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-database.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-folder-blank.png b/versions/4.0/en/editor/extension/image/extension-folder-blank.png new file mode 100644 index 0000000000..2701aad028 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-folder-blank.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-global.png b/versions/4.0/en/editor/extension/image/extension-global.png new file mode 100644 index 0000000000..384280592b Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-global.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-internal.png b/versions/4.0/en/editor/extension/image/extension-internal.png new file mode 100644 index 0000000000..9e1d1f90a7 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-internal.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-list.png b/versions/4.0/en/editor/extension/image/extension-list.png new file mode 100644 index 0000000000..52d06c7ab5 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-list.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-manager-menu.png b/versions/4.0/en/editor/extension/image/extension-manager-menu.png new file mode 100644 index 0000000000..863e70870e Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-manager-menu.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-manager.png b/versions/4.0/en/editor/extension/image/extension-manager.png new file mode 100644 index 0000000000..98c9ea92c0 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-manager.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-message-mgr-menu.png b/versions/4.0/en/editor/extension/image/extension-message-mgr-menu.png new file mode 100644 index 0000000000..0b13c7222c Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-message-mgr-menu.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-message-mgr-panel.png b/versions/4.0/en/editor/extension/image/extension-message-mgr-panel.png new file mode 100644 index 0000000000..2357044169 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-message-mgr-panel.png differ diff --git a/versions/4.0/en/editor/extension/image/extension-project.png b/versions/4.0/en/editor/extension/image/extension-project.png new file mode 100644 index 0000000000..2f66e88892 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/extension-project.png differ diff --git a/versions/4.0/en/editor/extension/image/i18n-folder.png b/versions/4.0/en/editor/extension/image/i18n-folder.png new file mode 100644 index 0000000000..b82d28db85 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/i18n-folder.png differ diff --git a/versions/4.0/en/editor/extension/image/introduction.png b/versions/4.0/en/editor/extension/image/introduction.png new file mode 100644 index 0000000000..3c6c37ec04 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/introduction.png differ diff --git a/versions/4.0/en/editor/extension/image/output.png b/versions/4.0/en/editor/extension/image/output.png new file mode 100644 index 0000000000..0dafdb8e8a Binary files /dev/null and b/versions/4.0/en/editor/extension/image/output.png differ diff --git a/versions/4.0/en/editor/extension/image/preferences-menu.png b/versions/4.0/en/editor/extension/image/preferences-menu.png new file mode 100644 index 0000000000..d2ebf81094 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/preferences-menu.png differ diff --git a/versions/4.0/en/editor/extension/image/preferences-tool-custom-laboratory.png b/versions/4.0/en/editor/extension/image/preferences-tool-custom-laboratory.png new file mode 100644 index 0000000000..76d3405fc2 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/preferences-tool-custom-laboratory.png differ diff --git a/versions/4.0/en/editor/extension/image/preferences-tool-custom.png b/versions/4.0/en/editor/extension/image/preferences-tool-custom.png new file mode 100644 index 0000000000..0daceecf34 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/preferences-tool-custom.png differ diff --git a/versions/4.0/en/editor/extension/image/preferences-tool.png b/versions/4.0/en/editor/extension/image/preferences-tool.png new file mode 100644 index 0000000000..55b0c1eee3 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/preferences-tool.png differ diff --git a/versions/4.0/en/editor/extension/image/pricing.png b/versions/4.0/en/editor/extension/image/pricing.png new file mode 100644 index 0000000000..fb469f478d Binary files /dev/null and b/versions/4.0/en/editor/extension/image/pricing.png differ diff --git a/versions/4.0/en/editor/extension/image/project-settings-menu.png b/versions/4.0/en/editor/extension/image/project-settings-menu.png new file mode 100644 index 0000000000..1826d6e213 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/project-settings-menu.png differ diff --git a/versions/4.0/en/editor/extension/image/project-settings-panel-custom.png b/versions/4.0/en/editor/extension/image/project-settings-panel-custom.png new file mode 100644 index 0000000000..71e531437b Binary files /dev/null and b/versions/4.0/en/editor/extension/image/project-settings-panel-custom.png differ diff --git a/versions/4.0/en/editor/extension/image/project-settings-panel.png b/versions/4.0/en/editor/extension/image/project-settings-panel.png new file mode 100644 index 0000000000..1a08df4740 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/project-settings-panel.png differ diff --git a/versions/4.0/en/editor/extension/image/search.png b/versions/4.0/en/editor/extension/image/search.png new file mode 100644 index 0000000000..e24a3964db Binary files /dev/null and b/versions/4.0/en/editor/extension/image/search.png differ diff --git a/versions/4.0/en/editor/extension/image/store.png b/versions/4.0/en/editor/extension/image/store.png new file mode 100644 index 0000000000..ec7b3db78b Binary files /dev/null and b/versions/4.0/en/editor/extension/image/store.png differ diff --git a/versions/4.0/en/editor/extension/image/submit-for-review.png b/versions/4.0/en/editor/extension/image/submit-for-review.png new file mode 100644 index 0000000000..139a8992f6 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/submit-for-review.png differ diff --git a/versions/4.0/en/editor/extension/image/ui-component-menu.png b/versions/4.0/en/editor/extension/image/ui-component-menu.png new file mode 100644 index 0000000000..d301482e8b Binary files /dev/null and b/versions/4.0/en/editor/extension/image/ui-component-menu.png differ diff --git a/versions/4.0/en/editor/extension/image/ui-component.png b/versions/4.0/en/editor/extension/image/ui-component.png new file mode 100644 index 0000000000..88a46280ab Binary files /dev/null and b/versions/4.0/en/editor/extension/image/ui-component.png differ diff --git a/versions/4.0/en/editor/extension/image/upload-store.png b/versions/4.0/en/editor/extension/image/upload-store.png new file mode 100644 index 0000000000..9f8bcfd8b6 Binary files /dev/null and b/versions/4.0/en/editor/extension/image/upload-store.png differ diff --git a/versions/4.0/en/editor/extension/inspector.md b/versions/4.0/en/editor/extension/inspector.md new file mode 100644 index 0000000000..c2738d7ffb --- /dev/null +++ b/versions/4.0/en/editor/extension/inspector.md @@ -0,0 +1,255 @@ +# Custom Inspector Panel + +Developers with custom inspector needs are advised to first refer to the documentation [Decorator](../../scripting/decorator.md), and if that document meets the requirements, it is recommended that the methods in that document be used first. + +The **Inspector** panel, the module in Cocos Creator that displays the currently selected state, provides some basic extension capabilities. + +In **Inspector** panel, two levels of data are defined: + +1. the main type of the selected object +2. the sub-data types contained in the content when rendering the content of the main type + +When a **node** or an **asset** is selected in the **Hierarchy**/**Assets Panel**, Cocos Creator broadcasts the message that the object is selected. When the **Inspector** panel receives the message, it checks the type of the selected object, e.g. if the selected object is a node, then the type is `node`. + +For both types, two types of renderers are allowed to be registered. +1. the main type renderer +2. When the main type renderer receives data to start rendering, it allows subtype renderers to be attached + +The selected object in the example is `node`, and `node` carries multiple `component`s, so the main type is `node` and the subtype is `component`. + +After the **Inspector** receives the broadcast message for the selected object, it first determines the type, and then the **Inspector** takes the received data (the uuid of the object) and passes it to the `node` renderer, handing over the rendering privileges completely. + +And in the `node` renderer, the rendering privileges for that region are handed over to the `subtype renderer` when rendering to each `component`, according to its own implementation. For the most part, we don't need to be concerned about this. Let's take a look at a few common ways to customize it. + +## Customizing Component Rendering + +The default component renderer sometimes doesn't meet our needs, so we need to customize a component's rendering method. + +First, create a new script component `CustomLabelComponent.ts` in the project and add a string property with the following content: + +```typescript + +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('CustomLabelComponent') +export class CustomLabelComponent extends Component { + @property + label = ''; + + start () { + + } +} + +``` + +Drag the component to the node and you will see an input box on the component. + +Create a new extension and register the following `contributions.inspector` information in the extension's `package.json`: + +```json +{ + "contributions": { + "inspector": { + "section": { + "node": { + "CustomLabelComponent": "./dist/contributions/inspector/comp-label.js" + } + } + } + } +} +``` + +### Automatic Rendering + +Write a `src/contributions/inspector/comp-label.ts` file with the following contents: + +```typescript +'use strict'; + +import { methods } from "../../main"; + +type Selector<$> = { $: Record } + +export const template = ` + +`; + +export const $ = { + test: '.test', +}; + +export function update(this: Selector, dump: any) { + // Use ui-prop to auto-render, set the type of prop to dump + // render pass in a dump data to be able to automatically render the corresponding interface + // Auto-rendered interface can automatically commit data after modification + this.$.test.render(dump.value.label); +} +export function ready(this: Selector) {} +``` + +After compiling and refreshing the extension, we can see that the rendering of the `CustomLabelComponent` component has been taken over. + +> **Note**: Each `ui-prop` corresponds to one property, to display multiple properties you need to define multiple `ui-props`. + +### Manual Rendering + +In the above auto-rendering example, we used a special `ui-prop` of type `dump` for rendering data submission, which allows us to quickly take over the rendering of the component, but if we face some extreme cases but it is difficult to handle some details, we can switch to manual rendering mode, the code is as follows: + +```typescript +'use strict'; + +type Selector<$> = { $: Record } + +export const template = ` + + + + + +`; + +export const $ = { + label:'.label', + test: '.test', + testInput: '.test-input', +}; + +type PanelThis = Selector & { dump: any }; + +export function update(this: PanelThis, dump: any) { + // Cache 'dump' data, please hang on 'this', otherwise there may be problems when multiple opening + this.dump = dump; + // Pass the 'dump' data to the prop element that helped submit the data + this.$.test.dump = dump.value.label; + // Update the data on the input element responsible for 'input' and display + this.$.testInput.value = dump.value.label.value; + this.$.label.value = dump.value.label.name; +} +export function ready(this: PanelThis) { + // Listen for commit events on the input, update the dump data when the input commits data, and use prop to send change-dump events + this.$.testInput.addEventListener('confirm', () => { + this.dump.value.label.value = this.$.testInput.value; + this.$.test.dispatch('change-dump'); + }); +} +``` + +Manual rendering mode still requires data to be submitted via a `ui-prop` element of type `dump`. However, the `html` layout in the `template` is a completely free-handed content that can be displayed in very complex ways depending on the requirements. +> **Note 1**: If you do not modify `this.$.test.dump = dump.value.label`, `this.$.test.dispatch('change-dump')` won't actually do anything. +> **Note 2**: If you wish to modify a more complex field, such as a `cc.JsonAsset`, yoiu need to do `this.$.testJsonInput.value = dump.value.testJson.value.uuid`. Since the value from the acutal asset won't fit the controller there. + + +## Customizing Asset rendering + +When a file is selected in the **Assets** panel, the **Inspector** panel will display the important properties of the currently selected file, or we can customize the rendering if the default display does not meet the requirements. + +Add the `contributions.section.asset` field to `package.json` and define a custom rendering script for the corresponding asset type as follows: + +```json +{ + "contributions": { + "inspector": { + "section": { + "asset": { + "effect": "./dist/contributions/inspector/asset-effect.js" + } + } + } + } +} +``` + +`effect` indicates that we want to customize the rendering of the asset inspector panel for the Cocos Effect (*.effect) file type. Common asset file types are as follows. +- `scene` - Scene files +- `typescript` - TypeScript script files +- `prefab` - prefab files +- `fbx` - FBX files +- `material` - material files +- `directory` - folder +- `image` - image files + +> **Note**: Currently, you can't add new assets. Those listed above are the only extensible assets for this time. + +You can get the type definition of the file by looking at the `importer` field in the `*.meta` corresponding to the file. + +Next, create a new `src/contributions/inspector/asset-effect.ts` script file in the extension directory and write the following code: + +```typescript +'use strict'; + +interface Asset { + displayName: string; + file: string; + imported: boolean; + importer: string; + invalid: boolean; + isDirectory: boolean; + library: { + [extname: string]: string; + }; + name: string; + url: string; + uuid: string; + visible: boolean; + subAssets: { + [id: string]: Asset; + }; +} + +interface Meta { + files: string[]; + imported: boolean; + importer: string; + subMetas: { + [id: string]: Meta; + }; + userData: { + [key: string]: any; + }; + uuid: string; + ver: string; +} + +type Selector<$> = { $: Record } & { dispatch(str: string): void, assetList: Asset[], metaList: Meta[] }; + +export const $ = { + 'test': '.test', +}; + +export const template = ` + + Test + + +`; + +type PanelThis = Selector; + +export function update(this: PanelThis, assetList: Asset[], metaList: Meta[]) { + this.assetList = assetList; + this.metaList = metaList; + this.$.test.value = metaList[0].userData.test || false; +}; + +export function ready(this: PanelThis) { + this.$.test.addEventListener('confirm', () => { + this.metaList.forEach((meta: any) => { + // Modify the data in the corresponding meta + meta.userData.test = !!this.$.test.value; + }); + // The Assets panel is modifying the meta file of the asset, not the dump data, so the event sent is not the same as the component property modification + this.dispatch('change'); + }); +}; + +export function close(his: PanelThis, ) { + // TODO something +}; +``` + +After compiling and refreshing the extension, go back to the Cocos Creator editor interface, select one of the `Cocos Effect` files, and you can find an additional **Test checkbox** at the bottom of the **Inspector** panel. + +> **Note**: Multiple extensions register data side by side. If a **Component**/**Asset** already has a custom renderer, any custom renderers registered again will be appended to it. If a **Component**/**Asset** does not have a custom renderer built-in and uses the default renderer, then when the extension registers a custom renderer, it takes over the rendered content completely. diff --git a/versions/4.0/en/editor/extension/install.md b/versions/4.0/en/editor/extension/install.md new file mode 100644 index 0000000000..389a318090 --- /dev/null +++ b/versions/4.0/en/editor/extension/install.md @@ -0,0 +1,82 @@ +# Install and Share + +## Install Location + +Cocos Creator will search and load extensions under **Global** and **Project** paths during project startup. + +### Global + +If you need to apply an extension to all Cocos Creator projects, you can choose to place the extension package in the **Global** extension package path at + +- **Windows**: `%USERPROFILE%\.CocosCreator\extensions` +- **macOS**: `$HOME/.CocosCreator/extensions` + +### Project + +If you only wish to apply the extension to the specified project, you can choose to place the extension package in the **Project** extension package path at. + +- `${your project address}/extensions` + +## Installing extensions + +Extensions can be obtained in three ways. +- Other developers packaged and shared, see [Packaged Extensions](#Package%20extensions) below. + +- Download from **Dashboard -> Store**. + + ![dashboard-store](./image/dashboard-store.png) + +- Downloaded from the [Cocos Store](http://store.cocos.com) page. + +After getting the extension zip file, click **Extension -> Extension Manager** in the top menu bar of the editor. + +![extension-manager-menu](image/extension-manager-menu.png) + +Clicking on it opens the **Extension Manager** panel as shown below. + +![extension-manager](image/extension-manager.png) + +In the **Extension Manager**, select the **Projects**/**Global** (1 above) tab and click the **+** (3 above) button. + +In the file selection box that pops up, select the extension zip you want to import and click the **Open** button to import it. + +The imported extension zip will be extracted and placed in the specified [installation location](#Install%20Location). + +Finally, find the extension in the **Project**/**Global** tab of the corresponding **Extension Manager** and click the **Enable** button on the right side, the extension you just imported will run normally. As shown in the figure below. + +![enable-extension](image/enable-extension.png) + +## Uninstall installed extensions + +Find the extension you want to delete in **Extension Manager** and click the **Delete button** ![delete](first/delete.png) to do so, and the folder where the extension is located will also be deleted. If you only need to disable it, you can just select "Close". + +## Reload extensions + +If the content of the extension has been modified, it will not be updated automatically, so you need to reload the extension once manually inside the editor. + +Find the corresponding extension in the **Extension Manager** and click the **Reload button** ![refresh](first/refresh.png), then the extension will be re-run with the latest code and files in the editor. + +## Package extensions + +After writing an extension, if you want to share it with other users, you need to package the extension as a zip archive. + +Let's take the `first-panel` extension as an example, its directory structure is as follows. + +![extension-first-panel-folder-origin](./first/extension-first-panel-folder-origin.png) + +Go to the extension root directory, select the appropriate files and zip all files as follows (screenshot for macOS, same for all other platforms) + +![extension-first-panel-install](./first/extension-first-panel-install.png) + +The files (folders) selected in the above diagram are mandatory, none of them are required, and they serve the following purposes. +- `dist` - Generated javascript code. +- `i18n` - Multilanguage configuration. +- `node_modules` - dependent Node.js modules. +- `package.json` - Extension description file. +- `static` - Static resource files. + +Name the zip package `first-panel.zip` (same name as the extensions folder is recommended) and share it with others or upload it to the Cocos Store to finish sharing. + +> **Note**: You have to do the file selection operation in the directory of the extension, otherwise it may lead to incorrect directory structure. + +If you want to upload the extension to the [Cocos Store](https://store.cocos.com), please refer to the document [Submitting Resources to Cocos Store](./store/upload-store.md). diff --git a/versions/4.0/en/editor/extension/messages.md b/versions/4.0/en/editor/extension/messages.md new file mode 100644 index 0000000000..e4da2d3d1c --- /dev/null +++ b/versions/4.0/en/editor/extension/messages.md @@ -0,0 +1,141 @@ +# Message System + +There are many independent processes running in Cocos Creator, and they are isolated from each other. When you need to interact with other functions within the editor, you need to do so through the "messaging mechanism". + +The "Message System" in the editor is a functional extension of the IPC (Inter-Process Communication) wrapper. This system carries the burden of communication and interaction within the editor. + +For more information about multi-process architecture and cross-process communication, please refer to the document [Extension Infrastructure](./package.md). + +## Message Types + +There are two types of messages within the Cocos Creator system. + +1. Normal message: a message is sent to a function (extension) on its own initiative +2. Broadcast messages: a function (extension) sends a notification to everyone that an operation has been completed + +### Normal messages + +It can be understood as an external interface, for example the engine's **Scene Editor** module has defined a `query-node` message for querying nodes, as follows: + +```json +{ + "name": "scene", + "contributions": { + "messages": { + "query-node": { + "methods": ["queryNode"] + } + } + } +} +``` + +For more information on how to customize messages and the meaning of the message fields, please refer to the document [Customized Messages](./contributions-messages.md). + +When we want to query a scene node in an extension we have written, we can use this message to do so, as follows: + +```typescript +const info = await Editor.Message.request('scene', 'query-node', uuid); +``` + +This message is similar to a remote process call (RPC), where the `info` object is part of the data on the actual node being queried. + +> **Note**: Since this is a remote call, `request` will not return immediately, so you need to use `await` to convert asynchronous to synchronous. + +#### Naming Convention for Normal Messages + +Please use **lowercase** words, and no special characters, with **-** concatenated between words. For example, `open-panel`, `text-changed`. + +### Broadcast Messages + +A broadcast message is a notification to the outside world after the completion of an operation within a function. + +#### Receive Broadcast Messages + +For example, if the **Scene Editor** needs to notify everyone that a scene has been started after it has been started, the **Scene Editor** sends a broadcast message using the following code. + +```typescript +Editor.Message.broadcast('scene:ready', sceneUUID); +``` + +If an extension wants to receive `scene:ready` messages, they need to be defined first in `package.json`, as follows: + +```json +{ + "name": "hello-world", + "contributions": { + "messages": { + "scene:ready": { + "methods": ["initData"] + } + } + } +} +``` + +The broadcast `scene:ready` message triggers the `initData` method in the "hello-world" extension whenever the scene is ready. + +#### Sending Broadcast Messages + +If an extension wants to send a broadcast message, it also needs to be defined in `package.json` first. + +For example, "hello-world" will broadcast a message to other extensions when it is ready for data. As shown below: + +```json +{ + "name": "hello-world", + "contributions": { + "messages": { + "scene:ready": { + "methods": ["initData"] + }, + "hello-world:ready": { + "public": true, + "description": "hello-world ready notification." + } + } + } +} +``` + +At the appropriate time, the following code is called within the "hello-world" extension to broadcast to everyone. + +```typescript +Editor.Message.broadcast('hello-world:ready'); +``` + +> **Note**: Broadcast messages can have no `methods`, which means they don't listen. As shown in the definition above, it means that "hello-world" does not need to listen for its own initialization completion message. + +#### Naming Convention for Broadcast Messages + +The format is `packageName:actionName`, and the following naming is legal. +- scene:ready +- scene:query-node +- hello-world:ready +- hello-world:data-loaded + +Adding `packageName` prevents naming conflicts and makes it more intuitive to see which extension is listening to which broadcast message (action) when defining messages in `package.json`. + +## View the List of Messages + +The list of messages that are available to the editor and extensions can be viewed in the **Developer -> Message Manager** panel. For detailed definition rules, please refer to the documentation [Custom Messages](./contributions-messages.md). + +## Sending Messages in Code + +The `send` method only sends the message and does not wait for a return. Use this method if you don't need to return data and don't care if execution completes. + +```typescript +Editor.Message.send(pkgName, message, . .args); +``` + +The `request` method returns a promise object that receives the data returned after the message has been processed. + +```typescript +await Editor.Message.request(pkgName, message, . . args); +``` + +The `broadcast` method only sends, and sends to all function extensions that listen for the corresponding message. + +```typescript +Editor.Message.broadcast(`${pkgName}:${actionName}`, . .args); +``` diff --git a/versions/4.0/en/editor/extension/package.md b/versions/4.0/en/editor/extension/package.md new file mode 100644 index 0000000000..bf9ab4e2a1 --- /dev/null +++ b/versions/4.0/en/editor/extension/package.md @@ -0,0 +1,41 @@ +# Extension Infrastructure + +Before writing an extension, we first need to understand the infrastructure of extensions within Cocos Creator. + +## Electron + +The Cocos Creator editor is based on the [Electron](https://github.com/atom/electron) kernel from GitHub. + +Electron is a cross-platform development framework that integrates with [Node.js](https://nodejs.org/) and [Google Chromium](https://github.com/chromium/chromium). + +## Multi-process Mechanism + +In Electron's architecture, an application consists of a main process and a rendering process, with the main process managing platform-related scheduling, such as window opening and closing, menu options, basic dialogs, and so on. Each newly opened window is a separate rendering process. Each process has its own JavaScript content and is not directly accessible to each other. When data needs to be passed between processes, the Inter-Process Communication (IPC) mechanism is used. + +You can read [Electron's introduction document](https://github.com/atom/electron/blob/master/docs/tutorial/quick-start.md) for a more in-depth understanding of the features The relationship between the main process and the rendering process in Electron. + +To put it simply, the main process in Electron is equivalent to a Node.js server application, while each window (rendering process) is equivalent to a client-side web application. + +The Cocos Creator editor follows the structure of Electron's main process and rendering process. So when the extension is started and run inside the editor, the main defined by the extension is actually started in the main process, while the panels defined by the panels are started in the rendering process. The process structure is briefly summarized as follows. + +![electron-process](./image/electron-process.png) + +## Inter-process Communication + +Inter-process communication is actually the process of sending a message in one process and listening to it in another process. + +Electron provides modules `ipcMain` and `ipcRenderer` for inter-process communication to help us with this task. + +Since these two modules only perform very basic communication functions and do not satisfy the communication needs between the editor, extension panel and the main process, Cocos Creator wraps this and extends the method of sending and receiving messages between processes to make it easier for extension developers and editor developers to create more complex scenarios. For more details, please see the documentation [Message System](./messages.md). + +## Capabilities of extensions + +With a full Node.js environment inside the extension, it is easy to use the large number of tools available on the npm marketplace for the functionality you want. + +If you need to interact with other features, you need to open up the corresponding operation messages for the corresponding features, and we do this within our own extension, via [Message System](./messages.md) to trigger, query and process functions or data in the editor. + +The list of opened messages can be viewed in the top menu **Developer -> Message Manager** panel, as shown below. + +![extension-message-mgr-menu](./image/extension-message-mgr-menu.png) + +![extension-message-mgr-panel](./image/extension-message-mgr-panel.png) diff --git a/versions/4.0/en/editor/extension/panel-boot.md b/versions/4.0/en/editor/extension/panel-boot.md new file mode 100644 index 0000000000..fc1cc75c1c --- /dev/null +++ b/versions/4.0/en/editor/extension/panel-boot.md @@ -0,0 +1,326 @@ +# Creating a Custom Panel + +We have already written the panel definition in [package.json](./panel.md), it's time to implement the panel's logical functionality. + +It is time to identify the main entry file in the panel definition and fill it with the following content. + +Javascript + +```javascript +'use strict'; + +// html text +exports.template =''; +// style text +exports.style =''; +// html selector after rendering +exports.$ = {}; +// method on the panel +exports.methods = {}; +// event triggered on the panel +exports.listeners = {}; + +// Triggered when the panel is rendered successfully +exports.ready = async function() {}; +// Triggered when trying to close the panel +exports.beforeClose = async function() {}; +// Triggered when the panel is actually closed +exports.close = async function() {}; +``` + +Typescript + +```typescript +'use strict'; + +// html text +export const template = ''; +// style text +export const style = ''; +// html selector after rendering +export const $ = {}; +// method on the panel +export const methods = {}; +// event triggered on the panel +export const listeners = {}; + +// Triggered when the panel is rendered successfully +export async function ready() {}; +// Triggered when trying to close the panel +export async function beforeClose() {}; +// Triggered when the panel is actually closed +export async function close() {}; +``` + +If we are using Typescript, when this is not correctly identified within functions such as ready, we can add the definition of this: + +```typescript +'use strict'; + +type Selector<$> = { $: Record } + +export const $ = { + test: '.test', +}; + +export const methods = { + update() {}, +}; + +export async function ready(this: Selector & typeof methods) { + this.update(); +}; +``` + +You can also use Editor.Pabel.define for common panel objects. + +```typescript +module.exports = Editor.Panel.define({ + methods: { + update() {}, + }, + ready() { + this.update(); + }, +}); +``` + +`Editor.Panel.define` is a new interface added in v3.3. + +## template + +html text, e.g. + +Javascript + +```javascript +exports.template = ` +
+ Header +
+
+ Section +
+`; +``` + +Typescript + +```typescript +export const template = ` +
+ Header +
+
+ Section +
+`; +``` + +It is also possible to read an HTML file directly. + +Javascript + +```javascript +const { readFileSync } = require('fs'); +const { join } = require('path'); +exports.template = readFileSync(join(__dirname, '../static/default.html'), 'utf8'); +``` + +Typescript + +```typescript +import { readFileSync } from 'fs'; +import { join } from 'path'; +export const template = readFileSync(join(__dirname, '../static/default.html'), 'utf8'); +``` + +When the `template` is defined and the panel is opened, the content of the template will be automatically rendered to the interface. + +In addition, the editor also provides some custom elements, which can be used in the [UI components](./ui.md). + +## style + +With HTML, you need to customize some styles and use style, which is a string like template. + +Javascript + +```javascript +exports.style = ` +header { padding: 10px; } +`; +``` + +Typescript + +```typescript +export const style = ` +header { padding: 10px; } +`; +``` + +Of course, it is also possible to read a css file: + +Javascript + +```javascript +const { readFileSync } = require('fs'); +const { join } = require('path'); +exports.style = readFileSync(join(__dirname, '../static/default.css'), 'utf8'); +``` + +Typescript + +```typescript +import { readFileSync } from 'fs'; +import { join } from 'path'; +export const style = readFileSync(join(__dirname, '../static/default.css'), 'utf8'); +``` + +## $ + +This is an HTML element selector that is used as a shortcut after calling `querySelector` directly to find the specified element. + +Javascript + +```javascript +exports.$ = { + header: 'header', + test: '.test', +}; +``` + +Typescript + +```typescript +export const $ = { + header: 'header', + test: '.test', +}; +``` + +First define the selector and the editor will automatically call `document.querySelector` after the template is rendered to find the corresponding element and hang it on `this.$`. + +Javascript + +```javascript +exports.ready = function() { + console.log(this.$.header); //
+ console.log(this.$.test); //
+} +``` + +Typescript + +```typescript +export function ready() { + console.log(this.$.header); //
+ console.log(this.$.test); //
+} +``` + +## methods + +Methods defined on the panel. The external functions of the panel need to be encapsulated as methods and made available to the public as functions. Messages can also trigger methods on the panel directly, see [custom messages](./contributions-messages.md). + +This object is full of functions, please don't mount other types of objects to it. + +Javascript + +```javascript +const packageJSON = require('./package.json'); +exports.methods = { + open() { + Editor.Panel.open(packageJSON.name); + }, +}; +``` + +Typescript + +```typescript +import { name } from './package.json'; +export const methods = { + open() { + Editor.Panel.open(packageJSON.name); + }, +}; +``` + +## listeners + +After the basic layout is completed, we sometimes need to update the state on some panels according to some circumstances, and this time we need to use the listeners function. + +Javascript + +```javascript +exports.listeners = { + /** + * Triggered when the panel is hidden + */ + hide() { + console.log(`hide: ${this.hidden}`); + }, + /** + * Triggered when the panel is displayed + */ + show() { + console.log(`hide: ${this.hidden}`); + }, + /** + * Triggered when the panel size is changed + */ + resize() { + console.log(`height: ${this.clientHeight}`); + console.log(`width: ${this.clientWidth}`); + }, +}; +``` + +Typescript + +```typescript +interface PanelInfo { + hidden: boolean; + clientHeight: number; + clientWidth: number; +} + +export const listeners = { + /** + * Triggered when the panel is hidden + */ + hide(this: PanelInfo) { + console.log(`hide: ${this.hidden}`); + }, + /** + * Triggered when the panel is displayed + */ + show(this: PanelInfo) { + console.log(`hide: ${this.hidden}`); + }, + /** + * Triggered when the panel size is changed + */ + resize(this: PanelInfo) { + console.log(`height: ${this.clientHeight}`); + console.log(`width: ${this.clientWidth}`); + }, +}; +``` + +## ready + +This life cycle function will be triggered when the panel is ready. + +## beforeClose + +This function will be triggered when the panel tries to be closed. beforeClose can be an async function, which can make asynchronous judgments, and if return false, it will terminate the current close operation. + +Please don't execute the actual destruction and close related logic code in this function, this step is just to ask, please put the actual destruction in the close function. + +**Please use with caution** If you make a mistake, the editor or panel window may not close properly. + +## close + +When all panels in the window are allowed to close, it will trigger the close of the panel. Once the close is triggered, the window will be forcibly closed after the end, so please save the data in the close, and if there is an abnormal close, please make a backup of the data so that the data can be restored as much as possible when restarting. diff --git a/versions/4.0/en/editor/extension/panel-messages.md b/versions/4.0/en/editor/extension/panel-messages.md new file mode 100644 index 0000000000..dde917d135 --- /dev/null +++ b/versions/4.0/en/editor/extension/panel-messages.md @@ -0,0 +1,159 @@ +# Panel and Extension Communication + +> **NOTE**: After v3.5, we updated the plugin documentation, so this documentation is deprecated, please move to [Message System](./messages.md) or [Customized Messages](./contributions-messages.md) for more information. If you see this document online, please post an issue on [github](https://github.com/cocos/cocos-docs/issues/new) and let the official staff know how to handle it. + +Some useful tools or simple functions can be written directly on the panel, but the panel is not a reliable data storage location. The window may be closed at any time, and the panel will also be closed. + +The most common example is that a panel is dragged and docked to the main window. At this time, the panel will be closed first and then reopened in the main window. If the data in the memory used on the panel is not stored and backed up, it will be lost with restart. + +At this time, a certain degree of data interaction is required with the extended main body. + +Before reading this chapter, please review the [Message System](./messages.md) documentation. + +## Define the method of extending the top and panel + +**First**, define a `package.json`: + +```json +{ + "name": "hello-world", + "main": "./browser.js", + "panels": { + "default": { + "title": "hw", + "main": "./panel.js" + } + }, + "contributions": { + "messages": { + "upload": { + "methods": ["saveData"] + }, + "query": { + "methods": ["queryData"] + } + } + } +} +``` + +**Second**, define the extended main file `browser.js`: + +```javascript +exports.methods = { + saveData(path, data) { + // Cache it after receiving the data + this.cache[path] = data; + }, + queryData(path) { + const result = this.cache[path]; + delete this.cache[path]; + return result; + }, +}; + +exports.load = function() {}; +exports.unload = function() {}; +``` + +Typescript + +```typescript +interface PackagelItem { + cache: { + [path: string]: any; + } +} +export const methods = { + saveData(this: PackagelItem, path: string, data: any) { + // 收到数据后缓存起来 + this.cache[path] = data; + }, + queryData(this: PackagelItem, path: string) { + const result = this.cache[path]; + delete this.cache[path]; + return result; + }, +}; + +export function load() {}; +export function unloal() {}; +``` + +**Last**, define the main file of the panel: + +```javascript +const packageJSON = require('./package.json'); +exports.ready = async () => { + const tab = await Editor.Message.request(packageJSON.name, 'query', 'tab'); + const subTab = await Editor.Message.request(packageJSON.name, 'query', 'subTab'); + + // Print the queried data + console.log(tab, subTab): + // TODO uses these two data to initialize +}; +exports.close() { + // Upload the data to the extension process after receiving the data + Editor.Message.send(packageJSON.name, 'upload', 'tab', 1); + Editor.Message.send(packageJSON.name, 'upload', 'subTab', 0); +}; +``` + +Typescript + +```typescript +import { name } from './package.json'; +exports.ready = async () => { + const tab = await Editor.Message.request(name, 'query', 'tab'); + const subTab = await Editor.Message.request(name, 'query', 'subTab'); + // 打印查询到的数据 + console.log(tab, subTab): + // TODO 使用这两个数据初始化 +}; +exports.close() { + // 收到数据后上传到扩展进程 + Editor.Message.send(name, 'upload', 'tab', 1); + Editor.Message.send(name, 'upload', 'subTab', 0); +}; +``` + +## Send a message + +After defining the extension and the panels in the extension, we can try to trigger these messages. + +Press **ctrl(cmd) + shift + i** to open the console. Open the panel in the console: + +```javascript +// Default can be omitted, if the panel name is not default, you need to fill in'hello-world.xxx' +Editor.Panel.open('hello-world'); +``` + +After opening the panel, the console will print out a sentence: + +```sh +undefined, undefined +``` + +This is because the data has not yet been submitted. Now, close this panel and open it again. At this time, the console prints out the data: + +```sh +1, 0 +``` + +Because when the panel is closed, two messages are sent: + +```javascript +Editor.Message.send(packageJSON.name, 'upload', 'tab', 1); +Editor.Message.send(packageJSON.name, 'upload', 'subTab', 0); +``` + +Through these two messages, the Message system first saves the data to the extension process according to the upload definition in messages `"methods": ["saveData"]`. + +When opening the panel again, use the following code to query for the data you just saved, initialize the interface, and print to the console. + +```javascript +const tab = await Editor.Message.send(packageJSON.name, 'query', 'tab'); +const subTab = await Editor.Message.send(packageJSON.name, 'query', 'subTab'); +``` + +At this point, we have completed an interaction between the panel and the extension process. diff --git a/versions/4.0/en/editor/extension/panel.md b/versions/4.0/en/editor/extension/panel.md new file mode 100644 index 0000000000..2255ebc8db --- /dev/null +++ b/versions/4.0/en/editor/extension/panel.md @@ -0,0 +1,194 @@ +# Extension Panel + +By default, extensions do not have an interface to display. If an extension needs to implement interface interaction, it needs to use the panel system related functions. + +## Panel Definition + +One or more panels can be defined in `package.json` in the `panels` field, as follows: + +```json +{ + "name": "hello-world", + "panels": { + "default": { + "title": "world panel", + "type": "dockable", + "main": "./dist/panels/default", + "icon": "./static/default.png" + }, + "list": { + "title": "world list", + "type": "simple", + "main": "./dist/panels/list", + "icon": "./static/list.png", + + "flags": {}, + "size": {} + } + } +} +``` + +We define two panels: `defualt` and `list`. `default` is the default panel, which is used as the default action object when no specific panel is named. + +The meaning of the fields in the panel is as follows. +- `title`: string - the title of the panel, supports i18n:key, required +- `main`: string - the relative directory of the panel source code, required +- `icon`: string - panel icon relative to directory, required +- `type`: string - Panel type (dockable | simple), optional +- `flags`: {} - flags, optional + - resizable - if or not the size can be changed, default true, optional + - save - if or not the panel needs to be saved, default false, optional + - alwaysOnTop - if or not to keep the top level displayed, default flase, optional +- `size`: {} - size information, optional + - min-width: Number - the minimum width, optional + - min-height: Number - the minimum height, optional + - width: Number - the default width of the panel, optional + - height: Number - the default height of the panel, optional + +## Writing Panels + +Create two files `src/panels/default/index.ts` and `src/panels/list/index.ts` in the extension root directory, and paste the following minimal panel template code into each of the two `index.ts` files. + +```typescript + +module.exports = Editor.Panel.define({ + listeners: { + show() { console.log('show'); }, + hide() { console.log('hide'); }, + }, + template: '
Hello
', + style: 'div { color: yellow; }', + $: { + elem: 'div', + }, + methods: { + + }, + ready() { + + }, + beforeClose() { }, + close() { }, +}); +``` + +`listeners` - some event listeners for the panel +`template` - the panel's HTML layout file +`style` - the panel's css file +`$` - global selector for quick access to some elements +`methods` - the external method interface for this panel +`ready` - called when the panel is opened +`beforeClose` - called before the panel is closed +`close` - called after the panel is closed + +## Displaying panels + +You can use the `Editor.Panel.open` method to open any panel (this extension's own panels and other extensions' panels). + +Assuming the extension is `hello-world`, the default panel can be opened in either of the following ways: + +```typescript +// Editor.Panel.open('hello-world.defualt'); +Editor.Panel.open('hello-world'); +``` + +Open other panels by: + +```typescript +// Editor.Panel.open('{extension-name}.panelName'); +Editor.Panel.open('hello-world.list'); +``` + +## Communication Interaction + +The Cocos Creator extension system is built based on Electron's multi-process approach. Each extension is a separate process, and each panel in the extension, is also a separate process. Therefore, the interaction between extensions and panels and between panels and panels can only be achieved through Inter-Process Communication (IPC). For details, please refer to the document [Message System](./messages.md). + +### Panels send outgoing messages + +Since the process also exits when the panel is closed, we usually use the extension as a carrier for in-memory data. The data and logical interfaces that are needed in the panel will usually be fetched from the extension's main process. + +If you want to **query** and **set** the data located in the main process of the extension, assuming the extension defines two messages ``queryData` and `saveData`, we can use them as follows + +```typescript +const data = await Editor.Message.request(packageName, 'queryData', dataName); +await Editor.Message.request(packageName, 'saveData', dataName,dataValue); +``` + +If you want to broadcast notifications to the entire extension system, you can do so using the **broadcast message** mechanism, see the documentation [Messaging System](./messages.md). + +### Panel Receives Messages + +```json5 +// package.json +{ +"contributions": { + "messages": { + "log": { + "methods": ["log"] + } + } + } +} +``` + +The above message defines a log message, which is handled by the log method in the extended master process. Next we make a slight modification so that the recipient of the message is a panel: + +```json5 +// package.json +{ +"contributions": { + "messages": { + "log": { + "methods": ["default.log"] + } + } + } +} +``` + +`default.log` makes the message recipient a `default` panel, just implement a `log` method in the panel and you can handle this message smoothly. + +## A better way to organize panel resources + +In the minimalist panel template above, we have two lines of panel display-related code: + +```typescript +module.exports = Editor.Panel.define({ + ... + template: '
Hello
', + style: 'div { color: yellow; }', + ... +}); +``` + +In most cases, panel layouts cannot be this simple. If you continue to write complex HTML layouts and css styles here, the code will become unmaintainable. You can refer to the project created in the document [Getting Started Example - Panel](./first-panel.md), we can separate the `html` and `css` code into separate files and put them in the `static` folder. + +The resulting panel template code is shown below: + +```typescript +import { readFileSync } from 'fs-extra'; +import { join } from 'path'; + +module.exports = Editor.Panel.define({ + listeners: { + show() { console.log('show'); }, + hide() { console.log('hide'); }, + }, + template: readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8'), + style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'), + $: { + app: '#app', + }, + methods: { + + }, + ready() { + + }, + beforeClose() { }, + close() { }, +}); +``` + +For more details on the panel utility, please refer to [Getting Started Example - Panel](./first-panel.md). diff --git a/versions/4.0/en/editor/extension/profile.md b/versions/4.0/en/editor/extension/profile.md new file mode 100644 index 0000000000..cfc67c21b7 --- /dev/null +++ b/versions/4.0/en/editor/extension/profile.md @@ -0,0 +1,149 @@ +# Configuration System + +The Cocos Creator extension provides a configuration management mechanism for saving user settings and data in the extension. + +## Configuration Types + +There are two types of configuration types. +1. editor configuration (editor) +2. project configuration (project) + +### Editor Configuration + +The editor configuration is used to store some editor-related user settings and data, and is divided into three priority levels, from high to low, as follows. + +``` +local -> global -> default +``` + +If there is no corresponding configuration in `local`, the configuration in `global` will be used, and if no corresponding configuration in `global` is found, the default `default` configuration will be used. + +### Project Configuration + +The project configuration is used to store some project-related user settings and data, and is divided into two priority levels, from highest to lowest. + +``` +local -> default +``` + +Similar to the rules of **Editor Configuration**, when fetching configuration data, the configuration item in `local` will be used first, and if there is no corresponding configuration item in `local`, the default `default` configuration will be used. + +## Registering Configuration + +To use the configuration system, you need to define the `profile` information in the `contributions` field of the extension definition file `package.json`, as follows. + +```json +{ + "name": "hello-world", + "contributions": { + "profile": { + "editor": { + "test.a": { + "default": 0, + "message": "editorTestAChanged", + "label": "Test Editor configuration" + } + }, + "project": { + "test.a": { + "default": 1, + "message": "projectTestAChanged", + "label": "Test Project Configuration" + } + } + } + }, +} +``` + +**contributions.profile** The related fields are interpreted as follows. +- `editor`:{} - editor configuration +- `project`:{} - project configuration +- `test.a`:{} - key for the configuration of test.a +- `default`:any - the default value of this configuration item, optional parameter +- `message`:string - the message that will be triggered when this configuration item is modified, optional +- `label`:string - where the configuration can be displayed, this description may be displayed. Supports i18n:key format, optional parameters + +The TypeScript interface associated with `profile` is defined as follows. + +```typescript +interface ProfileInfo { + editor: { [ key: string ]: ProfileItem }; + project: { [ key: string ]: ProfileItem }; +} + +interface ProfileItem { + // Default data for configuration + default: any; + // This message is automatically sent for notification of configuration changes + message: string; + // A simple description of the role of configuration information, supporting the i18n:key syntax + label: string; +} +``` + +## Read and Modify Configuration + +### Importing Extension Definitions + +```typescript +import packageJSON from '../package.json'; +``` + +If the last parameter of `Editor.Profile.getConfig` is empty, a [priority](#Editor%20Configuration) match will be performed. + +If the fetch location (one of `local`, `global`, `default`) is specified, the corresponding value will be returned. As shown below, `local` and `global` are `undefined` because they are not set. + +```typescript +await Editor.Profile.getConfig(packageJSON.name, 'test.a'); // 0 +await Editor.Profile.getConfig(packageJSON.name, 'test.a', 'local'); // undefined +await Editor.Profile.getConfig(packageJSON.name, 'test.a', 'global'); // undefined +``` + +### Modify the Editor Configuration + +Call `getConfig` after modifying the configuration with the following code to see the corresponding changes. + +```typescript +await Editor.Profile.setConfig(packageJSON.name, 'test.a', 1); +await Editor.Profile.setConfig(packageJSON.name, 'test.a', 'local', 2); +await Editor.Profile.setConfig(packageJSON.name, 'test.a', 'global', 3); +``` + +### Read Project Configuration + +If the last parameter of `Editor.Profile.getProject` is empty, a [priority](#Project%20Configuration) match will be performed. + +If the fetch location (either `local`, `default`) is specified, the corresponding value will be returned. As shown below, the `local` is `undefined` because it is not set. + +```typescript +await Editor.Profile.getProject(packageJSON.name, 'test.a'); // 1 +await Editor.Profile.getProject(packageJSON.name, 'test.a', 'local'); // undefined +``` + +### Modify the Project Configuration + +Call `getProject` after modifying the configuration with the following code to see the corresponding changes. + +```typescript +await Editor.Profile.setProject(packageJSON.name, 'test.a', 1); +await Editor.Profile.setProject(packageJSON.name, 'test.a', 'local', 2); +``` + +## Storage Paths + +### Editor Configuration Storage Path + +| Hierarchy | Path | +| ------- | ------------------------------------------------------------ | +| local | `{projectPath}/profiles/v2/extensions/{extensionName}.json` | +| global(mac) | `Users/{name}/.CocosCreator/profiles/v2/extensions/{extensionName}.json` | +| global(window) | `c/Users/{name}/.CocosCreator/profiles/v2/extensions/{extensionName}.json` | +| default | `{extensionPath}/package.json` | + +### Project Configuration Storage Path + +| Hierarchy | Path | +| ------- | ----------------------------------------------------------- | +| local | `{projectPath}/settings/v2/extensions/{extensionName}.json` | +| default | `{extensionPath}/package.json` | diff --git a/versions/4.0/en/editor/extension/readme.md b/versions/4.0/en/editor/extension/readme.md new file mode 100644 index 0000000000..ca3b1d9e21 --- /dev/null +++ b/versions/4.0/en/editor/extension/readme.md @@ -0,0 +1,13 @@ +# Editor Extension + +This section focuses on extending the editor, including the following: + +- [Extension Manager](./extension-manager.md) +- [The First Extension](./first.md) +- [First Panel](./first-panel.md) +- [First Data Interaction](./first-communication.md) +- [Change the Name of a Extension](./extension-change-name.md) +- [Install and Share](./install.md) +- [Extended Panel](./panel.md) +- [Contributions](./contributions.md) +- [Basic](./basic.md) diff --git a/versions/4.0/en/editor/extension/scene-script.md b/versions/4.0/en/editor/extension/scene-script.md new file mode 100644 index 0000000000..205e96a9a9 --- /dev/null +++ b/versions/4.0/en/editor/extension/scene-script.md @@ -0,0 +1,89 @@ +# Calling the Engine API and Project Script + +In the extension you can define a special **scene script** file, which will be in the same runtime as the scripts in the `assets\` directory of the project, with the same runtime environment. + +In the **Scene Script** you can call the engine `API` and other project scripts, and with this feature we can + +- Query and traverse the nodes in the scene to get or modify node data +- Call functions related to the engine components on the node to finish the job + +## Register Scene Script + +First, add a `scene` field to the `contributions` property of `package.json`, the value of which is the path to a script file, relative to the extension package directory. Example: + +```json +{ + "contributions": { + "scene": { + "script": "./dist/scene.js" + } + } +} +``` + +## Scene Script Template + +Create a new `scene.ts` in the `src` directory and write the following code. + +```typescript +export function load() {}; +export function unload() {}; +export const methods = { }; +``` + +`load` - the function that fires when the module is loaded + +`unload` - the function that fires when the module is unloaded + +`methods` - methods defined inside the module that can be used to respond to external messages + +## Calling the Engine API + +Next, we will demonstrate how the scene script calls the engine API by rotating the main camera. + +In order to call the engine API, we need to add the search path of the engine script at the beginning of `scene.ts` and write the corresponding code, which ends up looking like this + +```typescript +import { join } from 'path'; +module.paths.push(join(Editor.App.path, 'node_modules')); + +export function load() {}; + +export function unload() {}; + +export const methods = { + rotateCamera() { + const { director } = require('cc'); + let mainCamera = director.getScene().getChildByName("Main Camera"); + if(mainCamera){ + let euler = new Vec3(); + euler.set(mainCamera.eulerAngles.x, mainCamera.eulerAngles.y + num, mainCamera.eulerAngles.z); + mainCamera.setRotationFromEuler(euler); + return true; + } + return false; + }, +}; +``` + +In the above code, we have defined a `rotateCamera` method that rotates the main camera `10` degrees around the `Y` axis every time it is executed. + +In other extension scripts, we can call the `rotateCamera` function with the following code. + +```typescript +const options: ExecuteSceneScriptMethodOptions = { + name: packageJSON.name, + method: 'rotateCamera', + args: [] +}; + +// result: {} +const result = await Editor.Message.request('scene', 'execute-scene-script', options); +``` + +The properties of `ExecuteSceneScriptMethodOptions` are defined as follows: +- name - the package name of the extension where `scene.ts` is located, you can use `packageJSON.name` if it is in this extension +- method: the method defined in `scene.ts` +- args: arguments, optional + +As the communication between extensions is based on Electron's underlying cross-process IPC mechanism, the transferred data will be serialized as JSON. so the transferred data must not contain native objects, otherwise it may lead to process crashes or memory spikes. It is recommended to transfer only pure `JSON` objects, such as the `options.args` parameter in the above code and the return value of the scenario script method. diff --git a/versions/4.0/en/editor/extension/store/upload-store.md b/versions/4.0/en/editor/extension/store/upload-store.md new file mode 100644 index 0000000000..21a6917c09 --- /dev/null +++ b/versions/4.0/en/editor/extension/store/upload-store.md @@ -0,0 +1,77 @@ +# Submitting Resources to Cocos Store + +Cocos Creator has a built-in **Extension Store** for users to browse, download and automatically install official or third-party extensions and resources. Users can also submit their own extensions, art materials, music and sound effects to the extension store for sharing or selling. Here is an example of submitting an extension. + +![img](../image/store.png) + +## Packaging the Extension + +Suppose the developer completes a extension package with the following directory structure: + +``` +foobar + |--panel + |--index.js + |--package.json + |--main.js +``` + +Developers need to package the `foobar` folder into a `foobar.zip` file and submit it to the Cocos Developer Center. + +For additional information on extension packages, please review the [Creating Extension Packages](../first.md) documentation. + +### Third-party Libraries + +There is currently no workflow for installing an included management system such as NPM in the extension package installation system, extension packages that use third-party libraries should be packaged in a zip package with folders such as `node_modules`. + +## Submitting the Extension + +Visit the [Cocos Developer Center](https://auth.cocos.com/#/) and login. Next, visit the [Store](https://store-my.cocos.com/#/seller/resources/) section and click **Create New Resource** on the top right. + +![img](../image/create.png) + +- First, go to the **Category** page, fill in the **Name** and **Category**, and check the agreement checkbox. + + ![img](../image/category.png) + + - **Name**: the name of the extension to be displayed in the extension store. Please note that the name cannot be changed once it is confirmed, please fill it in carefully. + - **Category**: the category of the resource to be submitted, choose **Creator Extension -> Plugins** here. + + Click **Next** after the settings are done to enter the **Introduction** page. + +- Second, fill in the relevant information on the **Introduction** page. + + ![img](../image/introduction.png) + + - **Keyword**: facilitate users to search for your extension faster, support multiple keywords + - **Supported Platforms**: including Android, iOS, HTML5 + - **Icon**: icon size of **256 * 256**, size no more than **500KB**, **PNG** format. + - **Screenshots**: upload a maximum of **5** screenshots in **jpg**/**png** format. The size of each screenshot is limited to a minimum of **640px** and a maximum of **2048px**, and the size should not exceed **1000KB**. + - **Description**: fill in the basic functions and usage of the extension. It includes **Chinese** and **English** languages, and will only be displayed in the extension store of the corresponding language version after filling it in.
The Extension Store has certain formatting requirements for the descriptions of plugins, please refer to the [plugin description template [cn]](https://store.cocos.com/document/zh/cocos-store-template-extension.html) documentation for details. + + Click **Next** to enter the **Pricing** page after filling out the form. + +- Third, in the **Pricing** page, set the price of the extension, including **CNY** and **USD**, if it is free, please fill in **0**. + + ![img](../image/pricing.png) + + Click **Next** to enter the **Upload** page after completing the form. + +- Fourth, on the **Upload** page, upload the extensions and fill in the relevant information. + + ![img](../image/upload-store.png) + + - **Package**: zip format, max 100MB. + - **Extension Name**: the name of the extension package, defined in the `package.json` file of the extension package. + - **Version Number**: extension version number, defined in the `package.json` file of the extension package. Please follow the [semver specification](https://semver.org/) for the writing specification. + - **Creator Minimum Version Requirement**: the extension's requirement for Creator version. + + > **Note**: since Creator 2.x and 3.x extensions are not compatible with each other, if there is no corresponding version of the supported extension package, the work will not be displayed in the corresponding version of the extension store of Creator. + + Click **Next** to go to the **Submit for Review** page after completing the form. + +- Fifth, on the **Submit for Review** page, click the **Submit Review** button, or click the **View** button to re-edit the extension resource. + + ![img](../image/submit-for-review.png) + +- Lastly, after submitting for review, the extension store management will review the extension content and information within **3** business days. diff --git a/versions/4.0/en/editor/extension/to-panel-messages.md b/versions/4.0/en/editor/extension/to-panel-messages.md new file mode 100644 index 0000000000..a47fd0cda7 --- /dev/null +++ b/versions/4.0/en/editor/extension/to-panel-messages.md @@ -0,0 +1,90 @@ +# Communicate with the Panel + +> **NOTE**: After v3.5, we updated the extension documentation, so this documentation is deprecated, please move to [Message System](./messages.md) or [Customized Messages](./contributions-messages.md) for more information. If you see this document online, please post an issue on [github](https://github.com/cocos/cocos-docs/issues/new) and let the official staff know how to handle it. + +In general, the interaction model is dominated by **extension process** and **panel** for data presentation. Similarly to the traditional Web, the **plug-in** function is the server side, and the __panel__ function is the browser on the client's computer. + +In this case, there is usually no direct data sent to the panel, the majority is some state synchronization, just using **broadcast** to broadcast. + +But for simple extensions, or extensions to the browser environment, the actual functionality may be on the panel, and a request needs to be sent to the panel. + +Some level of understanding of the [Message System](./messages.md) is required before reading this section. + +## Define methods on extensions and panels + +First we define the file: `package.json` + +```json +{ + "name": "hello-world", + "panels": { + "default": { + "title": "hw", + "main": "./panel.js" + } + }, + "contributions": { + "messages": { + "console": { + "methods": ["default.console"] + } + } + } +} +``` + +The method name defined by methods in `messages.console` is `default.console`. Represents a console method issued to the `default` panel. +(to send to the plug-in process, fill in `methdName` directly) + +Then define the `panel.js` file of the panel: + +```javascript +exports.template = ''; +exports.style = ''; + +exports.methods = { + console(str) { + console.log(str); + }, +}; + +exports.ready = async function() {}; + +exports.close = function() {}; +``` + +Typescript + +```typescript +export const template = ''; +export const style = ''; + +export const methods = { + console(str: string) { + console.log(`console: ${str}`); + }, +}; + +export async function ready() {}; + +export function close() {}; +``` + +## Send a message + +Once we have defined the extension and the panels within the extension, we can try to trigger these messages. + +Press **CTRL (CMD) + Shift + I** to open the console. Open the panel in the console: + + ```javascript + // default can be omitted, if the panel name is non-default, then you need to fill in 'hello-world.xxx' + Editor.Panel.open('hello-world'); + // Send a console message to the hello-world plugin + Editor.Message.send('hello-world', 'console', 'log'); + ``` + +When the **Hello World** plug-in receives a message, it passes it to the `methods.console` in `panel.js` for processing. + +The result is printing a string to the **log** on the console. + +At this point, we have completed one interaction with the panel. diff --git a/versions/4.0/en/editor/extension/ui.md b/versions/4.0/en/editor/extension/ui.md new file mode 100644 index 0000000000..c90f0624e2 --- /dev/null +++ b/versions/4.0/en/editor/extension/ui.md @@ -0,0 +1,50 @@ +# UI Components + +## UI Components Panel + +To facilitate the layout, many pre-defined UI components are provided within the editor. + +1. Find **Developer -> UI Components** in the main menu at the top of the editor to view them. + +![ui-component-menu](image/ui-component-menu.png) + +2. Click it to open the following panel. + +! [ui component](image/ui-component.png) + +The panel consists of two parts, the left column lists the UI types currently supported by the engine, and the right column provides some examples that developers can use as needed. + +## Using in HTML + +Using UI components in HTML is very easy, just copy the corresponding code into your HTML file and you are ready to use. + +## Using in Extension Panel + +When extending the editor panel, it can be configured using json. In theory, all UI components with `value` attribute can be used to extend the editor panel, here are some common ones. + +### input-box + +- Component: `ui-num-input` +- No additional properties + +### ui-slider + +- Component: `ui-slider` +- `attributes` component attributes + - `min` minimum + - `max` maximum value + - `step` step length + +### ui-checkbox + +- Component: `ui-checkbox` +- No additional properties + +### ui-select + +- Component `ui-select` +- `items` list element + - `value` value + - `label` display label + +For usage examples, please refer to the documentation [Extending the Preferences Panel](./contributions-preferences.md) and [Extending Project Settings Panel](./contributions-project.md). diff --git a/versions/4.0/en/editor/hierarchy/img/after.png b/versions/4.0/en/editor/hierarchy/img/after.png new file mode 100644 index 0000000000..66204497b1 Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/after.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/context-menu.png b/versions/4.0/en/editor/hierarchy/img/context-menu.png new file mode 100644 index 0000000000..eda312e7cb Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/context-menu.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/create.png b/versions/4.0/en/editor/hierarchy/img/create.png new file mode 100644 index 0000000000..92ad456f25 Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/create.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/drop.png b/versions/4.0/en/editor/hierarchy/img/drop.png new file mode 100644 index 0000000000..969cfaeaea Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/drop.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/drop1.png b/versions/4.0/en/editor/hierarchy/img/drop1.png new file mode 100644 index 0000000000..dea4ec5af9 Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/drop1.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/drop2.png b/versions/4.0/en/editor/hierarchy/img/drop2.png new file mode 100644 index 0000000000..642a3372b2 Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/drop2.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/hierarchy.png b/versions/4.0/en/editor/hierarchy/img/hierarchy.png new file mode 100644 index 0000000000..e88acee20c Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/hierarchy.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/node-name.png b/versions/4.0/en/editor/hierarchy/img/node-name.png new file mode 100644 index 0000000000..6590dca28f Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/node-name.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/rename.png b/versions/4.0/en/editor/hierarchy/img/rename.png new file mode 100644 index 0000000000..9ba5d01e2a Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/rename.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/search-type.png b/versions/4.0/en/editor/hierarchy/img/search-type.png new file mode 100644 index 0000000000..ae80a102c6 Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/search-type.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/search.png b/versions/4.0/en/editor/hierarchy/img/search.png new file mode 100644 index 0000000000..bcf4f9573d Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/search.png differ diff --git a/versions/4.0/en/editor/hierarchy/img/thumb.gif b/versions/4.0/en/editor/hierarchy/img/thumb.gif new file mode 100644 index 0000000000..cb2dce658c Binary files /dev/null and b/versions/4.0/en/editor/hierarchy/img/thumb.gif differ diff --git a/versions/4.0/en/editor/hierarchy/index.md b/versions/4.0/en/editor/hierarchy/index.md new file mode 100644 index 0000000000..29c849c742 --- /dev/null +++ b/versions/4.0/en/editor/hierarchy/index.md @@ -0,0 +1,102 @@ +# Hierarchy Panel + +The **Hierarchy** panel consists of two main sections, the **Toolbar** and the **Node List**, which are used to show the relationships between the nodes that are editable in the current scene. There are still some private nodes in the scene that are not visible and will not be displayed here. + +You can single-select, multi-select, create, copy, move, delete and rename nodes. Any node can create a child node, whose coordinates are relative to the parent node and follow it. + +![Panel Overview](img/thumb.gif) + +- When a node is selected, it is highlighted on a blue background and its detailed properties are displayed in the **Inspector** panel. Click on the blank area of the panel to **unselect** it. +- Functions in the **Toolbar** include: **New Node(+)**, **Search Type**, **Search Box**, and **All Collapse**. +- The **Node list** mainly reflects the hierarchical relationship of nodes, the root node is **Scene**. When editing the Prefab file, its own node is used as the root node. Add or delete nodes is done here with the right-click menu or a drag-and-drop operation. +- The panel supports the following keyboard shortcuts: + - **Copy**: Ctrl/Cmd + C + - **Paste**: Ctrl/Cmd + V + - **Clone**: Ctrl/Cmd + D, Ctrl + drag nodes + - **Delete**: Delete + - **Select up and down**: Up and Down arrows + - **Collapse nodes**: Left arrow + - **Expand nodes**: Right arrow + - **Multi-select**: Ctrl or Cmd + click + - **Multi-select Continuously**: Shift + click + - **Rename**: Enter/F2 + - **Cancel input**: Esc + +## New Node + +Click the **New Node (+)** button in the upper left corner of the panel or right-click directly in the panel to create a node. + +![create-node](img/create.png) + +When creating a node, an **Input box** will appear asking for the name of the node, which is allowed to be empty and will be named by the default node name if it is empty. + +> The menus are a little different when you create a project through the Project(2D) template in the Cocos Dashboard. +> - Only 2D objects can be created. +> - All 3D objects, terrain, particle systems and lights are invalid. + +![node-name](img/node-name.png) + +- If there isn't a node selected in the tree list, the new node will be created under the current root node by default (`Scene`). +- If there are multiple nodes selected, the new node will be created under the first selected node. + +### UI nodes + +For a UI node to display properly, any of its parent nodes must have at least one **UITransform** component. When creating a UI node, if it does not meet the rules, a **Canvas** node will be automatically created as the root of the UI node, as described in the [UI Structure Description](../../2d-object/ui-system/index.md) documentation. + +### Prefab nodes + +For a Prefab node, drag a **Prefab** asset from the **Assets** panel directly into the **Hierarchy** panel to generate a Prefab node. Also, dragging a **Prefab** node from the **Hierarchy** panel into the **Assets** panel to generate a Prefab asset. + +## Search Nodes + +Search types include: **Search Name or UUID**, **Search UUID**, **Search Path**, **Search Component Name**, **Search Asset UUID**, and **Only Nodes With Missing Asset**. + +![search-type](img/search-type.png) + +**Search Component Name** will search out all nodes in the node list containing the specified component, which can be viewed in the **Inspector** panel, such as **MeshRenderer**. + +The **Search box** will update the search results instantly based on the input content. When a node is selected in the search results and the search is cleared, the selected node will still be located in the asset list. + +![search](img/search.png) + +## Change the display order of nodes + +The order of nodes in the list can be changed by dragging them up and down. Moving nodes are divided into **moved nodes** and **target placement nodes**. + +- Drag the **moved node** on top of the **target placement node**, both are **level**. + + As shown below, select the Cube node and drag it above the Sphere node. The Sphere node will be highlighted in yellow and have a blue line above it indicating where the Cube node will be inserted. + + ![drop](img/drop.png) + +- Drag the **moved node** onto the **target placement node**, and the **moved node** will be at the end as a child of the **target placement node**. + + In the following figure, select the Cube node and drag it to the Sphere node. The Sphere node will be highlighted in yellow on a light blue background, indicating that the Cube node will become a child of the Sphere. + + ![drop](img/drop1.png) + +- Drag the **moved node** below the **target placement node**, both are **level**. + + As shown below, select the Sphere node and drag it below the Cube node. The Cube node will appear highlighted in yellow and have a blue line below it indicating where the Sphere node will be inserted. + + ![drop](img/drop2.png) + +## Rename Nodes + +Select a node, then right click and select **Rename** to change the node name, or just use the shortcut keys **Enter** or **F2**. Click elsewhere in the panel or press the shortcut **Esc** to cancel the renaming. + +Different nodes can have the same name. + +![rename](img/rename.png) + +## Other Operations + +The menu that pops up when right-clicking on a node also includes the following actions. + +- **Copy/Paste**: Copy the node to the clipboard, then you can paste it to another location, or open another scene to paste the node that was just copied. +- **Duplicate**: Generate a copy of the node that is exactly the same as the selected node, with the generated node and the selected node in the same hierarchy. +- **Select All**: Select all nodes in the same hierarchy. +- **Copy and print UUID/PATH**: In complex scenarios, we sometimes need to get the UUID or full hierarchy path of a node in order to access it when the script is running. Click this option to see the UUID of the currently selected node and the node's hierarchical path in the **Console** panel. +- **Lock Node**: Mouse over the node and there will be a lock button on the left side, the node cannot be selected in the **Scene** panel once it is locked. + + Starting from v3.1.1, batch locking of nodes and their children is supported. Select any node, hold down the Alt and click the lock button, and the node and its children will all be locked. diff --git a/versions/4.0/en/editor/index.md b/versions/4.0/en/editor/index.md new file mode 100644 index 0000000000..a808a3b5ee --- /dev/null +++ b/versions/4.0/en/editor/index.md @@ -0,0 +1,40 @@ +# Editor Interface Introduction + +This chapter will introduce the editor interface and familiarize you with the panels, menus, and function buttons that make up the editor, which consists of multiple panels that can be freely moved and combined to suit the needs of different projects and developers. Taking the default editor layout as an example, notice the names and roles of each panel: + +![main](index/editor.png) + +- (**A**) [Hierarchy](./hierarchy/index.md): show all the nodes in the scene and their hierarchy in the form of a tree list, all the contents seen in the **Scene** panel can find the corresponding node entries in the **Hierarchy** panel, the contents of these two panels will be displayed simultaneously when editing the scene, and we usually use both panels to build scenes. +- (**B**) [Assets](./assets/index.md): shows all assets in the project assets folder (`assets`). Here the folders are displayed in a tree structure and automatically synchronized with changes made to the project asset folder contents in the operating system. Drag and drop files directly from outside the project, or import assets using the menu. +- (**C**) [Scene](./scene/index.md): workspace for displaying and editing the visual content of the scene. Building the scene in the **Scene** panel allows to get a WYSIWYG preview of the scene. +- (**D**) [Animation](./animation/index.md): used to edit and store animation data. +- (**E**) [Inspector](./inspector/index.md): used to view and edit the working area of the currently selected node and component properties. This panel displays the property data from the script definition in the most appropriate form and allows the user to edit them. +- (**F**) [Preview](./preview/index.md): after the scene is built, preview the game in action on the Web or native platform. + +Other important editor-based interfaces include: + +- **Console** + + ![console](index/console.png) + + The **Console** displays error reports, warnings, or other editor- and engine-generated log messages. For details, please review the [Console](console/index.md) documentation. + +- **Preferences** + + ![Preferences](index/preferences.png) + + **Preferences** provides various editor-specific global settings, including global settings for native development environment, game preview, other extensions, etc. For details, please read the section [Preferences](preferences/index.md). + +- **Project Settings** + + ![Settings](index/settings.png) + + **Project Settings** provides various project-specific personalization settings, including group management, feature cropping, project preview, custom engine, etc. For details, please review the [Project Settings](project/index.md) documentation. + +- **Service** + + ![service](index/service.png) + + Cocos Service is a **Service** panel integrated within Cocos Creator. We select high-quality technology solution providers to provide cost-effective service access, and are committed to giving users a one-click access experience and providing corresponding technical support. At the same time, we will also rely on the Cocos developer community to get a more favorable price for developers. + + For details, please refer to the [Cocos Service Introduction](https://service.cocos.com/document/en/) documentation. diff --git a/versions/4.0/en/editor/index/animation.png b/versions/4.0/en/editor/index/animation.png new file mode 100644 index 0000000000..9d4391514f Binary files /dev/null and b/versions/4.0/en/editor/index/animation.png differ diff --git a/versions/4.0/en/editor/index/assets.png b/versions/4.0/en/editor/index/assets.png new file mode 100644 index 0000000000..f29251e5aa Binary files /dev/null and b/versions/4.0/en/editor/index/assets.png differ diff --git a/versions/4.0/en/editor/index/console.png b/versions/4.0/en/editor/index/console.png new file mode 100644 index 0000000000..2d17e7da73 Binary files /dev/null and b/versions/4.0/en/editor/index/console.png differ diff --git a/versions/4.0/en/editor/index/editor.png b/versions/4.0/en/editor/index/editor.png new file mode 100644 index 0000000000..9bd13f27dc Binary files /dev/null and b/versions/4.0/en/editor/index/editor.png differ diff --git a/versions/4.0/en/editor/index/hierarchy.png b/versions/4.0/en/editor/index/hierarchy.png new file mode 100644 index 0000000000..e88acee20c Binary files /dev/null and b/versions/4.0/en/editor/index/hierarchy.png differ diff --git a/versions/4.0/en/editor/index/inspector.png b/versions/4.0/en/editor/index/inspector.png new file mode 100644 index 0000000000..afd9af3969 Binary files /dev/null and b/versions/4.0/en/editor/index/inspector.png differ diff --git a/versions/4.0/en/editor/index/main.jpg b/versions/4.0/en/editor/index/main.jpg new file mode 100644 index 0000000000..f31dd09e05 Binary files /dev/null and b/versions/4.0/en/editor/index/main.jpg differ diff --git a/versions/4.0/en/editor/index/preferences.png b/versions/4.0/en/editor/index/preferences.png new file mode 100644 index 0000000000..cc67b46004 Binary files /dev/null and b/versions/4.0/en/editor/index/preferences.png differ diff --git a/versions/4.0/en/editor/index/scene.png b/versions/4.0/en/editor/index/scene.png new file mode 100644 index 0000000000..fbb94090a3 Binary files /dev/null and b/versions/4.0/en/editor/index/scene.png differ diff --git a/versions/4.0/en/editor/index/service.png b/versions/4.0/en/editor/index/service.png new file mode 100644 index 0000000000..15561df35e Binary files /dev/null and b/versions/4.0/en/editor/index/service.png differ diff --git a/versions/4.0/en/editor/index/settings.png b/versions/4.0/en/editor/index/settings.png new file mode 100644 index 0000000000..65b71c64a0 Binary files /dev/null and b/versions/4.0/en/editor/index/settings.png differ diff --git a/versions/4.0/en/editor/inspector/index.md b/versions/4.0/en/editor/inspector/index.md new file mode 100644 index 0000000000..0b3e38b778 --- /dev/null +++ b/versions/4.0/en/editor/inspector/index.md @@ -0,0 +1,118 @@ +# Inspector Panel + +The **Inspector** panel is the work area for viewing and editing the currently selected nodes, node components, and assets. Properties can be displayed and edited in the **Inspector** panel by selecting a node in the **Scene** panel or **Hierarchy** panel, or by selecting an asset in the **Assets** panel. + +![introduce](index/introduce.gif) + +## Panel overview + +![inspector](index/inspector-panel.png) + +The **Inspector** panel can be divided into two parts: **Toolbar** and **Property Settings**. + +## Toolbar + +The **two arrows** in the top left corner are the edit history, click on them to advance/reverse the selected nodes/assets.
+The **lock** button in the upper right corner can lock the panel, fixing the currently edited object and not allowing the panel to change with new selections. + +![header](index/header.png) + +## Property Settings + +The **Property Settings** area allows setting node properties, component properties, asset properties, etc. + +### Node name and activation switch + +Node name, consistent with the node name displayed in **Hierarchy** panel. + +The Node checkbox indicates the node's enabled/disabled status. When unchecked, the node is disabled and rendering of the node will be suspended, and the node's children will be hidden (grayed out). + +### Node properties + +Click `Node` below the node name to collapse or expand the node's properties. To the right of `Node` are the **Help Documentations** and **Node Settings** buttons. + +- The Help button jumps to the official documentation about the node. + +- Click the Node Settings button to perform the following operations on the node: + + 1. Reset the node properties. + 2. Copy/paste the node values. + 3. Copy/paste world transform of the node, including `worldPosition`/`worldRotation`/`worldScale` properties. + 4. Reset the `Position`/`Rotation`/`Scale` properties of the node respectively. + + ![node-menu](index/node-menu.png) + +The transformation properties of nodes include **Position**, **Rotation** and **Scale**. Modifying the properties of a node will usually result in immediate changes in the appearance or position of the node in the **Scene** panel. For details, please review the [Coordinate Systems and Transformations](../../concepts/scene/coord.md#transformation-properties) documentation. + +To modify node properties in a batch, press Shift in the **Hierarchy** panel to select multiple nodes and then set them in batch in the **Inspector** panel. The batch setting of node properties is similar to that of assets, please refer to the Batch Setting of Assets Multiple Selection section at the end of this article for details. + +### Component property settings + +The Component checkbox indicates the enabled/disabled state of the component. When unchecked, the component is disabled and will not participate in rendering. + +Below the node properties, all the components attached on the node and the component properties are listed. As with the node properties, clicking on a component's name toggles the collapsed/expanded state of that component's properties. In the case of many components attached on the node, collapse infrequently modified component properties to get a larger working area. + +To the right of the component name are buttons for **Help Documentations** and **Component Settings**. +- The Help Documentations button jumps to the official documentation page for the component. +- The Component Settings button allows resetting, deleting, moving up, moving down, copying the component, paste the component's value, paste it as a new component, etc. for the component. + +The properties and settings of each component are different, please refer to the corresponding component description documentation for details. + +## Adding components + +Clicking the **Add Component** button brings up a list of components, including system-provided components and custom script components. The list of added components has a search box that supports toggling with the up and down keys and confirming the selection with **Enter**. + +![add-component](index/add-component.png) + +Developers' script in the **Assets** panel can be dragged and dropped directly into the **Inspector** panel to generate a script component, or added via **Add Component -> Custom Script**. The properties of a script component are declared by the script. Different types of properties have different control appearance and editing in the **Inspector** panel. We will add the properties in [Declare Properties](../../scripting/decorator.md) section for details on how properties are defined. + +## Property types + +**Property** is a publicly available variable declared in the component script that can be serialized and stored in the scene and animation data. The **Inspector** panel allows quickly modifying property settings for the purpose of adjusting game data and game play without modifying the script. + +**Property** can usually be divided into two categories, **value type** and **reference type**, depending on where the variable uses memory. + +### Value type properties + +**Value Types** include simple variable types that take up very little memory, such as numbers, strings, booleans, enumerations, etc. + +- `Number`: can be entered directly using the keyboard or incremented or decremented by pressing the up and down arrows next to the input box. +- `Vec2`: the control of a vector is a combination of two numeric inputs, and the input box is marked with x and y to identify the sub-property name corresponding to each value. +- `String`: entered directly into the text box using the keyboard. +- `Boolean`: edited in the form of a checkbox, the selected state means the property value is true, the non-selected state means false. +- `Enum`: edited in the form of a drop-down menu. Click the enumeration menu, and then select an item from the pop-up menu list to finish modifying the enumeration value. +- `Color`: click on the color property preview box, the **Color Picker** window will pop up, in this window use the mouse to directly click on the desired color, or directly enter the specified color in the RGBA color input box below. Clicking anywhere outside the **Color Picker** window will close the window and use the last selected color as the property value. For example, the color picker component. + + ![ui-color](index/ui-color.png) + +### Reference type properties + +**Reference Types** include `object` objects, such as nodes, components, or assets. They can be selected and assigned by **dragging the node or asset into the property bar** or by **popping up the asset panel**. + +![assets-panel](index/assets-panel.png) + +## Batch operations + +When needing to set asset properties of the **same type** in a batch, press Shift in the **Assets** panel and select multiple assets, the **Inspector** panel will show the number of assets selected and the editable asset properties. Click the **Apply** button at the top right when done. + +![multiple-edit](index/multiple-edit1.png) + +Batch modification of node properties is the same. However, if an property in the **Inspector** panel displays one of the following states, it means that the property has inconsistent property values across the multiple assets selected, and choose whether to continue to batch modify the property as needed: + +- The check box displays **Grey**. +- The input box displays **-**. +- The selection box displays **blank**. + +> **Notes**: +> 1. Batch setting operations are not currently supported for Material assets. +> 2. Assets of **different types** can be selected at the same time, but they do not support setting properties in batch. + +## Edit Prefab node properties + +The Prefab node functions in the top toolbar of the **Inspector** panel include: disassociate, locate asset, restore from asset, and update to asset. For details, please refer to the [Prefab](../../asset/prefab.md) documentation. + +![prefab-menu](index/prefab-menu.png) + +> **Note**: when editing the asset, please remember to save it by clicking the **green tick** button in the upper right corner. +> +> ![edit-assets](index/edit-assets.png) diff --git a/versions/4.0/en/editor/inspector/index/add-component.png b/versions/4.0/en/editor/inspector/index/add-component.png new file mode 100644 index 0000000000..20cb0408e4 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/add-component.png differ diff --git a/versions/4.0/en/editor/inspector/index/assets-panel.png b/versions/4.0/en/editor/inspector/index/assets-panel.png new file mode 100644 index 0000000000..26d8208e44 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/assets-panel.png differ diff --git a/versions/4.0/en/editor/inspector/index/component-menu.png b/versions/4.0/en/editor/inspector/index/component-menu.png new file mode 100644 index 0000000000..8d9ca875e2 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/component-menu.png differ diff --git a/versions/4.0/en/editor/inspector/index/drag-assets.png b/versions/4.0/en/editor/inspector/index/drag-assets.png new file mode 100644 index 0000000000..c055e45481 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/drag-assets.png differ diff --git a/versions/4.0/en/editor/inspector/index/edit-assets.png b/versions/4.0/en/editor/inspector/index/edit-assets.png new file mode 100644 index 0000000000..c10f9afb87 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/edit-assets.png differ diff --git a/versions/4.0/en/editor/inspector/index/header.png b/versions/4.0/en/editor/inspector/index/header.png new file mode 100644 index 0000000000..5a0b1d9a20 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/header.png differ diff --git a/versions/4.0/en/editor/inspector/index/inspector-panel.png b/versions/4.0/en/editor/inspector/index/inspector-panel.png new file mode 100644 index 0000000000..7bd27ae8bd Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/inspector-panel.png differ diff --git a/versions/4.0/en/editor/inspector/index/introduce.gif b/versions/4.0/en/editor/inspector/index/introduce.gif new file mode 100644 index 0000000000..76aa5e7cd5 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/introduce.gif differ diff --git a/versions/4.0/en/editor/inspector/index/material-in-node.png b/versions/4.0/en/editor/inspector/index/material-in-node.png new file mode 100644 index 0000000000..23d4f19d05 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/material-in-node.png differ diff --git a/versions/4.0/en/editor/inspector/index/multiple-edit.png b/versions/4.0/en/editor/inspector/index/multiple-edit.png new file mode 100644 index 0000000000..74feed07d1 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/multiple-edit.png differ diff --git a/versions/4.0/en/editor/inspector/index/multiple-edit1.png b/versions/4.0/en/editor/inspector/index/multiple-edit1.png new file mode 100644 index 0000000000..ff0c25147e Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/multiple-edit1.png differ diff --git a/versions/4.0/en/editor/inspector/index/node-attrs.png b/versions/4.0/en/editor/inspector/index/node-attrs.png new file mode 100644 index 0000000000..542cf06f8d Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/node-attrs.png differ diff --git a/versions/4.0/en/editor/inspector/index/node-menu.png b/versions/4.0/en/editor/inspector/index/node-menu.png new file mode 100644 index 0000000000..cb7ccd21c0 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/node-menu.png differ diff --git a/versions/4.0/en/editor/inspector/index/prefab-edit-menu.png b/versions/4.0/en/editor/inspector/index/prefab-edit-menu.png new file mode 100644 index 0000000000..c436178d80 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/prefab-edit-menu.png differ diff --git a/versions/4.0/en/editor/inspector/index/prefab-menu.png b/versions/4.0/en/editor/inspector/index/prefab-menu.png new file mode 100644 index 0000000000..2182571bcd Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/prefab-menu.png differ diff --git a/versions/4.0/en/editor/inspector/index/ui-color.png b/versions/4.0/en/editor/inspector/index/ui-color.png new file mode 100644 index 0000000000..95b0ee9294 Binary files /dev/null and b/versions/4.0/en/editor/inspector/index/ui-color.png differ diff --git a/versions/4.0/en/editor/l10n/collect-and-count.md b/versions/4.0/en/editor/l10n/collect-and-count.md new file mode 100644 index 0000000000..98e813a9ea --- /dev/null +++ b/versions/4.0/en/editor/l10n/collect-and-count.md @@ -0,0 +1,27 @@ +# Collect and Count + +The Collect and Count feature collects files such as text, Typescript scripts, scene resources, Prefab, videos and images from within the project and allows developers to localize and configure them. + +![overview](collect/overview.png) + +## Properties and Descriptions + +- **Local development language**: The local development language, the language used by the developer during development. The language selected here will be used as the source language and provided to the translation service provider for translation. Developers can select it according to their current preferences via the drop-down menu at. + + ![lang](collect/diff-language.png) + + This option is required. + +- **Collecting from resource files**: The localization editor feature allows you to collect text information from different resource directories for the desired translation, as well as the option to filter or exclude certain files/folders. + + ![group](collect/group.png) + + - **Collection groups**: Multiple can be added, different collection groups can be added by the **+** button, for the added items, **delete** button will be displayed when mouse slide over. + + ![add-remove](collect/add-remove.png) + + - **Search directory**: You can specify a specific directory within the current resource database for collecting resources to be translated, with a minimum of 1 i.e. `db://assets`. To reduce the collection time, we recommend developers to store the resources to be translated in a separate directory. Then sort them by this option. + - **Extension name**: Specify to collect by a specific file extension, or if none, collect all files. + - **Excluded path**: excluded paths will not be collected. + +Once the directory to be collected is configured, the data is recorded by clicking the **Collect and Count** button and can be compiled and organized by [Compile Language](compile-language.md). diff --git a/versions/4.0/en/editor/l10n/collect/add-remove.png b/versions/4.0/en/editor/l10n/collect/add-remove.png new file mode 100644 index 0000000000..5132d27015 Binary files /dev/null and b/versions/4.0/en/editor/l10n/collect/add-remove.png differ diff --git a/versions/4.0/en/editor/l10n/collect/diff-language.png b/versions/4.0/en/editor/l10n/collect/diff-language.png new file mode 100644 index 0000000000..bc9156621d Binary files /dev/null and b/versions/4.0/en/editor/l10n/collect/diff-language.png differ diff --git a/versions/4.0/en/editor/l10n/collect/group.png b/versions/4.0/en/editor/l10n/collect/group.png new file mode 100644 index 0000000000..808c8f8348 Binary files /dev/null and b/versions/4.0/en/editor/l10n/collect/group.png differ diff --git a/versions/4.0/en/editor/l10n/collect/overview.png b/versions/4.0/en/editor/l10n/collect/overview.png new file mode 100644 index 0000000000..30d1f8e706 Binary files /dev/null and b/versions/4.0/en/editor/l10n/collect/overview.png differ diff --git a/versions/4.0/en/editor/l10n/compile-language.md b/versions/4.0/en/editor/l10n/compile-language.md new file mode 100644 index 0000000000..6bbede5d28 --- /dev/null +++ b/versions/4.0/en/editor/l10n/compile-language.md @@ -0,0 +1,187 @@ +# Compile Language + +![overview](compile/overview.png) + +Once the local development language is selected in the [Collect and Count](collect-and-count.md) view, the compilation process can be configured. This will include features such as **automatic translation**, **manual translation**, **variants**, etc. + +## Language + +Developers can choose to add a new language here. + +![add](compile/add-lang.png) + +The language in the first row is **Local Development Language**, which records the local language used in the current development process, and can usually be chosen as a language familiar to the developer. Under this language, **Operation** column appears **Complete** function, which is used to record the original text of the Label component and should be used with [L10nLabel](l10n-label.md). Please refer to the following for details. + +Starting from the second line, the language used in the target country/region, developers need to select at least one language of the target country/region for the translation option to appear. + +## Languages recognized by translation service providers + +![provider](compile/lang-provider.png) + +The above chart is used to select the input/output language type of the translation service provider at the time of automatic translation. + +- For native development languages, the input language type is selected. + +- For the translation language, the selection is for the output language type. + +For example, in the above figure, the local development language is English (en), then the drop-down menu needs to select **English**. + +And the translation language is **Simplified Chinese (zh-Hans-CN)**, then in the drop-down menu, please select **zh-CHS**. + +> **Note**: If the drop-down menu is not available, please check if [AppKey/AppSecret](translation-service.md) is configured correctly. + +## Translation Progress + +![progress](compile/progress.png) + +Shows current translation progress. + +## Operations + +The actions field provides **Complement** functionality for native development languages and **Translation** functionality for target translation languages. Common functions for both include **Preview**, **Export** and **Delete**. + +### Complement + +**Complement** for only taking effect when the current language is set to **Local Development Language**. The purpose of the complement feature is to record the original text of the content to be translated in the current development language. All the original text data obtained by the **Collect and Count** function will be displayed here. When you click on it, you can view all the results. + +![complemenet](compile/complement.png) + +When adding a key via the [L10nLabel](l10n-label.md) component within a prefab or scene: + +![new key](compile/new-key.png) + +You can find the corresponding key in the **Complement** panel, enter the value of the key in the Original field, and click the **Save** button to save the original text, which will be automatically filled if the Label's String property has a value. + +![unfilled](compile/unfilled.png) + +Non-text resources such as images and audio are also complemented, see the **Non-Text Resources** section below for details. + +### Variants + +The variant function is specially used to solve the plural problem encountered in translation. The system will automatically adjust the translation content according to the rules of variants, and users can also freely set the rules of variants. + +In the **Complement** panel, click the **Variant** button to enter a new variant. + +![otehr](compile/other.png) + +After the **Complement** is done, remember that you need to click the **Save** button above to save it. + +The number of variants is language dependent and is one of the international rules, which interested users can check by moving to [https://cldr.unicode.org/](https://cldr.unicode.org/). + +For more information, see [L10nLabel Variant Count](l10n-label.md). + +### Translation + +Once the key value and the original text have been added to the **Complement** function, you can use the translation service provider's translation service to translate by clicking on the **Translat** button to bring up the translation interface. + +![translate](compile/translate.png) + +Click on the **Translate** button at the top right to translate. + +![translate-overview](compile/translate-overview.png) + +Once the translation is complete, the results can be viewed in the **Translation** sub-page. + +![complete](compile/translate-complete.png) + +For translated text, different translations can also be added by clicking on **Variant**:. + +![variant](compile/translated-variant.png) + +> **Note**: This feature only works if the **Translation Service Provider** is properly configured. + +### Preview + +Click Preview to get a quick preview of how the nodes/image resources etc. of the [L10nLabel](l10n-label.md) component are displayed in that language for the current scene: + +![preview](compile/preview-overview.png) + +Before translation: + +![pev](compile/original-preview.png) + +After translation: + +![prev](compile/translate-preview.png) + +### Export + +Click the **Export** button to export the original/translated content to a PO file. + +> PO file is a common text-based object file in software development, usually used to record the result of interface translation. + +![export](compile/export.png) + +The following are some supplementary translation tools, which developers can choose according to their needs. + +- Manipulating PO Files +- [https://poedit.net/](https://poedit.net/) +- [https://github.com/translate/translate](https://github.com/translate/translate) +- [OmegaT - The Free Translation Memory Tool - OmegaT 3](https://omegat.org/) + +### Delete + +![del](compile/delete.png) + +Delete the translated progress of the current language. The result will not be saved after confirmation, so please operate with caution. Local development languages cannot be deleted. + +## Non-text resources + +Localization may also be required when there is text drawn on images, video and audio dubbed in different languages in the project. In this case, the translation interface will provide the **Import** button, and here is an example of how to configure localization using images. + +![image](compile/import-image.png) + +When **Collection and Count** is complete, the detected resources will be in the form of paths as key values. + +![assets](compile/asset.png) + +Import the images required for the target language by clicking on the **Import** button to. + +![dest image](compile/import-other-lang-jpg.png) + +> **Note**. +> 1. When importing, you can only import the files in the project. +> 2. Since auto-atlas will modify the uuid of the resource, it will cause the redirection failure, so the images that need to be translated cannot be checked for auto-atlas function. + +### Intelligent Matching + +The Intelligent Matching button will bring up a secondary confirmation menu, clicking on it will enable intelligent matching. Intelligent matching will be modified by the file name and match the corresponding language. Example. + +The source file is: assets/aassb/cn-abc001.jpg and needs to be translated into English, its translation file is: assets/aassb/en-abc001.jpg, we will automatically replace cn in assets/aassb/cn-abc001.jpg with en. + +Operation steps. + +- For example, when our local development language is Chinese (zh) and the language to be translated is English (en), the following resources may be prepared as shown in the figure below: + + ![source](compile/source-matching.png) + +- After clicking **Intelligent Matching**, select Confirm in the secondary menu that pops up: + + ![before](compile/before-matching.png) + +- Developers do not need to import them manually, and role.jpg in different languages will be automatically matched with: + + ![result](compile/matching-result.png) + +## Import Files + +The **Import File** button allows you to import external data files, supporting PO, CSV and XLSX formats. + +The format requirements for CSV and XLSX are as follows. + +![xlsx](compile/xlsx.png) + +The file should be in UTF-8 encoding format. + +![csv](compile/import-csv.png) + +After importing, if you need to translate manually, the process is the same as the above document. + +### Conflict resolution + +If you import the same external file repeatedly, a conflict will occur, and you will need to resolve the conflict manually at this point: + +![conflict](compile/conflict.png) + +- **Skip**: skip the conflicting key value, no additional processing. +- **Cover**: Overwrite the existing key value, the old data will be discarded. diff --git a/versions/4.0/en/editor/l10n/compile/add-lang.png b/versions/4.0/en/editor/l10n/compile/add-lang.png new file mode 100644 index 0000000000..c9c5097972 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/add-lang.png differ diff --git a/versions/4.0/en/editor/l10n/compile/asset.png b/versions/4.0/en/editor/l10n/compile/asset.png new file mode 100644 index 0000000000..a56e032116 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/asset.png differ diff --git a/versions/4.0/en/editor/l10n/compile/before-matching.png b/versions/4.0/en/editor/l10n/compile/before-matching.png new file mode 100644 index 0000000000..4516fd69e7 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/before-matching.png differ diff --git a/versions/4.0/en/editor/l10n/compile/complement.png b/versions/4.0/en/editor/l10n/compile/complement.png new file mode 100644 index 0000000000..bc2a350f5e Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/complement.png differ diff --git a/versions/4.0/en/editor/l10n/compile/conflict.png b/versions/4.0/en/editor/l10n/compile/conflict.png new file mode 100644 index 0000000000..8c15ee19f1 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/conflict.png differ diff --git a/versions/4.0/en/editor/l10n/compile/delete.png b/versions/4.0/en/editor/l10n/compile/delete.png new file mode 100644 index 0000000000..e70db7a858 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/delete.png differ diff --git a/versions/4.0/en/editor/l10n/compile/export.png b/versions/4.0/en/editor/l10n/compile/export.png new file mode 100644 index 0000000000..4e9ae19bb8 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/export.png differ diff --git a/versions/4.0/en/editor/l10n/compile/import-csv.png b/versions/4.0/en/editor/l10n/compile/import-csv.png new file mode 100644 index 0000000000..f5c8caa606 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/import-csv.png differ diff --git a/versions/4.0/en/editor/l10n/compile/import-image.png b/versions/4.0/en/editor/l10n/compile/import-image.png new file mode 100644 index 0000000000..c632692494 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/import-image.png differ diff --git a/versions/4.0/en/editor/l10n/compile/import-other-lang-jpg.png b/versions/4.0/en/editor/l10n/compile/import-other-lang-jpg.png new file mode 100644 index 0000000000..4b1cce3969 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/import-other-lang-jpg.png differ diff --git a/versions/4.0/en/editor/l10n/compile/lang-provider.png b/versions/4.0/en/editor/l10n/compile/lang-provider.png new file mode 100644 index 0000000000..df244c0ced Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/lang-provider.png differ diff --git a/versions/4.0/en/editor/l10n/compile/matching-result.png b/versions/4.0/en/editor/l10n/compile/matching-result.png new file mode 100644 index 0000000000..fcd78aa1eb Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/matching-result.png differ diff --git a/versions/4.0/en/editor/l10n/compile/new-key.png b/versions/4.0/en/editor/l10n/compile/new-key.png new file mode 100644 index 0000000000..270eedd9b0 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/new-key.png differ diff --git a/versions/4.0/en/editor/l10n/compile/original-preview.png b/versions/4.0/en/editor/l10n/compile/original-preview.png new file mode 100644 index 0000000000..06549fee47 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/original-preview.png differ diff --git a/versions/4.0/en/editor/l10n/compile/other.png b/versions/4.0/en/editor/l10n/compile/other.png new file mode 100644 index 0000000000..c26eb121d5 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/other.png differ diff --git a/versions/4.0/en/editor/l10n/compile/overview.png b/versions/4.0/en/editor/l10n/compile/overview.png new file mode 100644 index 0000000000..8236703abc Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/overview.png differ diff --git a/versions/4.0/en/editor/l10n/compile/preview-overview.png b/versions/4.0/en/editor/l10n/compile/preview-overview.png new file mode 100644 index 0000000000..6df220616c Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/preview-overview.png differ diff --git a/versions/4.0/en/editor/l10n/compile/progress.png b/versions/4.0/en/editor/l10n/compile/progress.png new file mode 100644 index 0000000000..292484eba4 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/progress.png differ diff --git a/versions/4.0/en/editor/l10n/compile/source-matching.png b/versions/4.0/en/editor/l10n/compile/source-matching.png new file mode 100644 index 0000000000..d8f1ceb130 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/source-matching.png differ diff --git a/versions/4.0/en/editor/l10n/compile/translate-complete.png b/versions/4.0/en/editor/l10n/compile/translate-complete.png new file mode 100644 index 0000000000..6c828a5515 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/translate-complete.png differ diff --git a/versions/4.0/en/editor/l10n/compile/translate-overview.png b/versions/4.0/en/editor/l10n/compile/translate-overview.png new file mode 100644 index 0000000000..7a0ab9b0af Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/translate-overview.png differ diff --git a/versions/4.0/en/editor/l10n/compile/translate-preview.png b/versions/4.0/en/editor/l10n/compile/translate-preview.png new file mode 100644 index 0000000000..33c899b304 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/translate-preview.png differ diff --git a/versions/4.0/en/editor/l10n/compile/translate.png b/versions/4.0/en/editor/l10n/compile/translate.png new file mode 100644 index 0000000000..ebcd99dc18 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/translate.png differ diff --git a/versions/4.0/en/editor/l10n/compile/translated-variant.png b/versions/4.0/en/editor/l10n/compile/translated-variant.png new file mode 100644 index 0000000000..aeb1673331 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/translated-variant.png differ diff --git a/versions/4.0/en/editor/l10n/compile/unfilled.png b/versions/4.0/en/editor/l10n/compile/unfilled.png new file mode 100644 index 0000000000..d50cb44eb8 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/unfilled.png differ diff --git a/versions/4.0/en/editor/l10n/compile/variant.png b/versions/4.0/en/editor/l10n/compile/variant.png new file mode 100644 index 0000000000..fccfe7f465 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/variant.png differ diff --git a/versions/4.0/en/editor/l10n/compile/xlsx.png b/versions/4.0/en/editor/l10n/compile/xlsx.png new file mode 100644 index 0000000000..ff0e68e1c4 Binary files /dev/null and b/versions/4.0/en/editor/l10n/compile/xlsx.png differ diff --git a/versions/4.0/en/editor/l10n/l10n-label.md b/versions/4.0/en/editor/l10n/l10n-label.md new file mode 100644 index 0000000000..6a8afc97bd --- /dev/null +++ b/versions/4.0/en/editor/l10n/l10n-label.md @@ -0,0 +1,84 @@ +# L10nLabel + +L10nLabel is a component that can be customized to translate content. Used in conjunction with a text component, the content of the text component can be translated. + +![overview](l10nlabel/overview.png) + +## Adding Components + +Users can click **Add Component** on the **Inspector** panel and select L10nLabel to add. + +![add](l10nlabel/add.png) + +> **Note**: L10nLabel must be paired with a Label component to work, if there is no Label component on the node, one will be created automatically. + +## Properties and descriptions + +| Properties | Description | +| :-- | :-- | +| **String** | Text within Label component
not editable| +| **Count** | Number of variants
See below for details | +| **Key** | Localized key | + +### Count + +By entering a different number, the localization will switch between different variants. + +In some languages, different formats are used to represent singular and plural, for example, in English, one apple is expressed as one apple, while two apples is used in the plural form of apples: two apples. + +In order to be able to use the correct format after translation, we can fill in **Count** exactly how many objects are specified in the current sentence. + +As a rule, the number of variants in different languages is related to the local language, and developers can consult with native speakers. + +For example, English has a singular/plural distinction, so there are two variants in configuration `one` and `other`. Some languages like Arabic have 5 variants while Russian has 3 variants. The engine generates the number of variants by adapting international rules, which can be found at the following URL if you wish to know them. + +- ECMAScript Internationalization API:[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) +- Unicode CLDR Project:[https://cldr.unicode.org/](https://cldr.unicode.org/) + +We'll walk through a detailed example below, for those unfamiliar with the **Localized Edit** panel, you can start by referring to [Localization](./overview.md). + +- First enable **localization editing** and select Chinese as the development language:. + + ![dev-lang](l10nlabel/dev-lang.png) + +- Add a node with a Label component within the scene. + + ![apple](l10nlabel/pingguo.png) + +- Add a L10nLabel component to this node and change the Key to: apple-key: + + ![apple-key](l10nlabel/apple-key.png) + +- Add the language to be translated to English: + + ![english](l10nlabel/english.png) + +- Adding variants within the **translation** panel: + + ![l10nlabel/apple-variant.png](l10nlabel/apple-variant.png) + +- The translated result can be previewed by clicking on the **Preview** action in the **Localization Editor** panel, and different variants will be displayed when switching between singular/plural in the Count property. + + ![result](l10nlabel/variant-result.gif) + +### Key + +Globally unique keys, localization will get the result of that key in localization depending on the key. Developers can enter their own custom keys. New keys can also be added by the **reset** button. + +![key](l10nlabel/key-prop.png) + +#### Reset Key + +![reset](l10nlabel/reset-key.png) + +If you need new keys you can click on the reset button shown, at which point the L10N will generate new keys, which can then also be edited in the **Localization Editor** in the panel opened by the **Compile** operation: + +![compile](l10nlabel/compile.png) + +For details, please refer to [Compile Language](compile-language.md) + +#### Select Key + +The drop-down menu shown allows you to select existing keys: + +![select](l10nlabel/select-key.png) diff --git a/versions/4.0/en/editor/l10n/l10nlabel/add.png b/versions/4.0/en/editor/l10n/l10nlabel/add.png new file mode 100644 index 0000000000..034490af48 Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/add.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/apple-key.png b/versions/4.0/en/editor/l10n/l10nlabel/apple-key.png new file mode 100644 index 0000000000..c20d5600bb Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/apple-key.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/apple-variant.png b/versions/4.0/en/editor/l10n/l10nlabel/apple-variant.png new file mode 100644 index 0000000000..51637e7a23 Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/apple-variant.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/compile.png b/versions/4.0/en/editor/l10n/l10nlabel/compile.png new file mode 100644 index 0000000000..cb30c40949 Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/compile.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/dev-lang.png b/versions/4.0/en/editor/l10n/l10nlabel/dev-lang.png new file mode 100644 index 0000000000..51d364a6fd Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/dev-lang.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/english.png b/versions/4.0/en/editor/l10n/l10nlabel/english.png new file mode 100644 index 0000000000..bff0b02d2d Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/english.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/key-prop.png b/versions/4.0/en/editor/l10n/l10nlabel/key-prop.png new file mode 100644 index 0000000000..94960dd963 Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/key-prop.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/overview.png b/versions/4.0/en/editor/l10n/l10nlabel/overview.png new file mode 100644 index 0000000000..d62fcb2cff Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/overview.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/pingguo.png b/versions/4.0/en/editor/l10n/l10nlabel/pingguo.png new file mode 100644 index 0000000000..2b0c0bf78a Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/pingguo.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/reset-key.png b/versions/4.0/en/editor/l10n/l10nlabel/reset-key.png new file mode 100644 index 0000000000..8a509c3f43 Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/reset-key.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/select-key.png b/versions/4.0/en/editor/l10n/l10nlabel/select-key.png new file mode 100644 index 0000000000..ebeda568be Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/select-key.png differ diff --git a/versions/4.0/en/editor/l10n/l10nlabel/variant-result.gif b/versions/4.0/en/editor/l10n/l10nlabel/variant-result.gif new file mode 100644 index 0000000000..b320f60b32 Binary files /dev/null and b/versions/4.0/en/editor/l10n/l10nlabel/variant-result.gif differ diff --git a/versions/4.0/en/editor/l10n/overview.md b/versions/4.0/en/editor/l10n/overview.md new file mode 100644 index 0000000000..b2090494c5 --- /dev/null +++ b/versions/4.0/en/editor/l10n/overview.md @@ -0,0 +1,68 @@ +# Localization + +Localization (hereinafter referred to as L10N) is a feature introduced in Cocos Creator 3.6 that integrates translation services from third-party translation service providers, while running the localization of text, audio and image resources into the engine and supporting one-click publishing to different languages. + +> L10N is the abbreviation of the first and last letter of the word Localization, 10 means Localization with 10 letters in the middle. + +## L10N OverView + +The Localization Editor panel can be opened by selecting **Panel** -> **Localization Editor** in the top menu of the engine.
+ +At first start-up, the user has to manually enable the L10N function. + +![enable](overview/enable.png) + +You can edit it after starting it. + +![overview](overview/overview.png) + +In the upper right corner of the panel, the L10N provides two functions **Turn Off L10N** and **Unload Data**. + +![menu](overview/menu.png) + +- **Turn Off L10N**: Disable the L10N function. After selecting Confirm in the secondary menu that pops up, the data is still retained and the developer can enable the function again: + + ![alert](overview/close-alert.png) + +- **Unload data**: After selecting Confirm in the popup secondary menu, the L10N data will be cleared and the associated components in the scene will be deleted: + + ![alert](overview/uninstal-alert.png) + + > **Note**: The uninstalled data cannot be recovered, please operate with caution. + +## Translation Service Provider + +![overview](translation-service/overview.png) + +The engine integrates the translation services of several translation service providers, and developers can choose different translation service providers to provide more complete and diversified services. For details, please refer to [Translation Service Provider](translation-service.md). + +## Collect and Count + +![ovefview](collect/overview.png) + +Collect and count collects resources that may need to be translated in a project to facilitate translation, and developers can also filter them by specifying specific files, directories, or through configuration. For details, please refer to [Collect and Count](collect-and-count.md) + +## Compile Language + +![overview](compile/overview.png) + +Language translation allows you to view the currently configured and translated languages and view their progress or delete actions. Manual processing of certain non-text resources is also provided. For more details, please refer to [Compile Language](compile-language.md) + +## Programming Guide + +Please refer to [Sample](script-using.md) for an example of programmatic use of L10N. + +## Publish + +Once the translation has been completed according to the above steps, it can be published with one click on the build panel at + +![publish](overview/publish.png) + +- **Language used**: This will contain all the languages set in the **Localization Editor**, and developers can select them as needed depending on the version +- **Default language**: the language used when the project is started after the release +- **Alternate language**: the alternate language to be used if there is a problem with the default language + +## Q & A + +- Q: Is there a solution if I accidentally modify the data file and the data file becomes corrupted and I cannot open L10N? + A: You can modify it by searching the corresponding file in "{project directory}\localization-editor\translate-data". diff --git a/versions/4.0/en/editor/l10n/overview/close-alert.png b/versions/4.0/en/editor/l10n/overview/close-alert.png new file mode 100644 index 0000000000..870e0b2724 Binary files /dev/null and b/versions/4.0/en/editor/l10n/overview/close-alert.png differ diff --git a/versions/4.0/en/editor/l10n/overview/enable.png b/versions/4.0/en/editor/l10n/overview/enable.png new file mode 100644 index 0000000000..bb5db137f0 Binary files /dev/null and b/versions/4.0/en/editor/l10n/overview/enable.png differ diff --git a/versions/4.0/en/editor/l10n/overview/menu.png b/versions/4.0/en/editor/l10n/overview/menu.png new file mode 100644 index 0000000000..0febbf8ec2 Binary files /dev/null and b/versions/4.0/en/editor/l10n/overview/menu.png differ diff --git a/versions/4.0/en/editor/l10n/overview/overview.png b/versions/4.0/en/editor/l10n/overview/overview.png new file mode 100644 index 0000000000..470de1cae3 Binary files /dev/null and b/versions/4.0/en/editor/l10n/overview/overview.png differ diff --git a/versions/4.0/en/editor/l10n/overview/publish.png b/versions/4.0/en/editor/l10n/overview/publish.png new file mode 100644 index 0000000000..22fc414a15 Binary files /dev/null and b/versions/4.0/en/editor/l10n/overview/publish.png differ diff --git a/versions/4.0/en/editor/l10n/overview/uninstal-alert.png b/versions/4.0/en/editor/l10n/overview/uninstal-alert.png new file mode 100644 index 0000000000..58d8dcd293 Binary files /dev/null and b/versions/4.0/en/editor/l10n/overview/uninstal-alert.png differ diff --git a/versions/4.0/en/editor/l10n/script-using.md b/versions/4.0/en/editor/l10n/script-using.md new file mode 100644 index 0000000000..2a69b13bec --- /dev/null +++ b/versions/4.0/en/editor/l10n/script-using.md @@ -0,0 +1,73 @@ +# Sample + +## Import + +The code example is as follows: + +```ts +import l10n from 'db://localization-editor/core/L10nManager' +``` + +- Description: l10n provides the ability to translate text in code as an api + +## Switch Language at Runtime + +The code example is as follows: + +```ts +l10n.changeLanguage('zh-Hans-CN') +``` + +For more information, please refer to [BCP47 Language Tag](https://www.techonthenet.com/js/language_tags.php). + +> **Note**: After calling this method, the game will be restarted automatically, so please make sure to do the data persistence. + +- Interface definination: `t(key: L10nKey, options?: StandardOption): L10nValue` + +## Get the value of L10N according to the key + +```ts +console.log(l10n.t('this_is_apple')) +// This is an apple +``` + +Here you can get the value of the current language with `this_is_apple` as the key. + +## Query + +The code example is as follows: + +```ts +console.log(l10n.exists('test_key')) +``` + +### Get the current language + +The code example is as follows: + +```ts +console.log(l10n.currentLanguage) +// 'zh-Hans-CN' +``` + +Returns the current language's [BCP47 Language Tag](https://www.techonthenet.com/js/language_tags.php)。 + +### Get all available languages + +The code example is as follows: + +```ts +console.log(l10n.languages) +// ['zh-Hans-CN', 'en-US'] +``` + +Returns all available language's [BCP47 Language Tag](https://www.techonthenet.com/js/language_tags.php) as an array. + +## Get directions to the language + +Most languages follow a left-to-right reading convention, with the exception of some languages such as Arabic, where this method learns the `TextInfoDirection` of the incoming language + +```ts +console.log(l10n.direction('ar')) +// 'rtl' +``` diff --git a/versions/4.0/en/editor/l10n/translation-service.md b/versions/4.0/en/editor/l10n/translation-service.md new file mode 100644 index 0000000000..8c2a6d6a72 --- /dev/null +++ b/versions/4.0/en/editor/l10n/translation-service.md @@ -0,0 +1,22 @@ +# Translation Service Provider + +Translation service providers are third-party software vendors that the engine integrates with by smoothing out the differences in APIs between them. Usually the developer needs to register the service provider's account and enable the corresponding API in order to start the automatic translation function. + +Don't worry if you don't have a corresponding developer account, L10N supports manual translation. + +![service](translation-service/overview.png) + +- **Translation Service Provider**: This drop-down menu allows developers to select a different translation service provider. If **None** is selected, the automatic translation function is not available, and the manual translation function is not affected: + + ![select](translation-service/select.png) + + The URL of the currently supported service providers are as follows: + + - [有道智云平台](https://ai.youdao.com/gw.s#/) + - [Google Cloud](https://cloud.google.com) + +- **AppKey**/**AppSecret**: After selecting different service providers, developers need to enter AppKey and AppSecret to continue the subsequent operation. Usually these information need to be obtained on the website of the translation service provider: + + ![key](translation-service/youdao.png) + + Once you have finished entering, just click the **Save** button. diff --git a/versions/4.0/en/editor/l10n/translation-service/overview.png b/versions/4.0/en/editor/l10n/translation-service/overview.png new file mode 100644 index 0000000000..8cedb5221d Binary files /dev/null and b/versions/4.0/en/editor/l10n/translation-service/overview.png differ diff --git a/versions/4.0/en/editor/l10n/translation-service/select.png b/versions/4.0/en/editor/l10n/translation-service/select.png new file mode 100644 index 0000000000..7c323a7e46 Binary files /dev/null and b/versions/4.0/en/editor/l10n/translation-service/select.png differ diff --git a/versions/4.0/en/editor/l10n/translation-service/youdao.png b/versions/4.0/en/editor/l10n/translation-service/youdao.png new file mode 100644 index 0000000000..2ff5c9c549 Binary files /dev/null and b/versions/4.0/en/editor/l10n/translation-service/youdao.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/cocos.png b/versions/4.0/en/editor/mainMenu/img/cocos.png new file mode 100644 index 0000000000..9b38b3a004 Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/cocos.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/developer.png b/versions/4.0/en/editor/mainMenu/img/developer.png new file mode 100644 index 0000000000..75b9a2fe31 Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/developer.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/edit.png b/versions/4.0/en/editor/mainMenu/img/edit.png new file mode 100644 index 0000000000..e40f0b2007 Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/edit.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/extension.png b/versions/4.0/en/editor/mainMenu/img/extension.png new file mode 100644 index 0000000000..7bf30d8d8e Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/extension.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/file.png b/versions/4.0/en/editor/mainMenu/img/file.png new file mode 100644 index 0000000000..a027dd0b00 Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/file.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/help.png b/versions/4.0/en/editor/mainMenu/img/help.png new file mode 100644 index 0000000000..6eba952977 Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/help.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/menu.png b/versions/4.0/en/editor/mainMenu/img/menu.png new file mode 100644 index 0000000000..8c3c60f9dd Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/menu.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/node.png b/versions/4.0/en/editor/mainMenu/img/node.png new file mode 100644 index 0000000000..e643f686f9 Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/node.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/panel.png b/versions/4.0/en/editor/mainMenu/img/panel.png new file mode 100644 index 0000000000..e96537b2b6 Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/panel.png differ diff --git a/versions/4.0/en/editor/mainMenu/img/project.png b/versions/4.0/en/editor/mainMenu/img/project.png new file mode 100644 index 0000000000..1e5450af68 Binary files /dev/null and b/versions/4.0/en/editor/mainMenu/img/project.png differ diff --git a/versions/4.0/en/editor/mainMenu/index.md b/versions/4.0/en/editor/mainMenu/index.md new file mode 100644 index 0000000000..8e37e3c82c --- /dev/null +++ b/versions/4.0/en/editor/mainMenu/index.md @@ -0,0 +1,150 @@ +# Main Menu + +The main menu bar at the top of Cocos Creator contains 9 menu options, **Cocos Creator**, **File**, **Edit**, **Node**, **Project**, **Panel**, **Extension**, **Developer** and **Help**, which integrate most of Cocos Creator's functionalities. + +![menu](./img/menu.png) + +## File + +This option is mainly used to create, open, and save a project or scene, as well as import Cocos Creator 2.x projects. + +![file](./img/file.png) + +| Option | Description | +| :--- | :-- | +| New Project | Opens Dashboard's [Project](../../getting-started/dashboard/index.md#project) tab of Dashboard to create a new project. If you are using Dashboard v1.0.19, you will open Dashboard's [New Project](../../getting-started/dashboard/index.md#new-project) tab. | +| Open Project | Open Dashboard's [Project](../../getting-started/dashboard/index.md#project) tab. | +| New Scene (Ctrl/Cmd + N) | Closes the current scene and create a new scene, the newly created scene needs to be saved manually before it will be added to the project directory. | +| Save Scene (Ctrl/Cmd + S) | Save the scene you are currently editing. If the scene is created using **File -> New Scene**, a dialog box will pop up when you save it for the first time, you need to choose the location to save the scene file and fill in the file name, then click **Save** to save it. Scene files have `.scene` as extension. | +| Save As (Ctrl/Cmd +Shift+ S) | Generates a copy of the current scene file and save it in the project. | +| Layout | Sets the editor interface layout. Custom layout can be saved and re-uesed | +| Preferences | Opens [Preferences](../preferences/index.md) panel to customize the editor. +| Shortcuts | Opens the Shortcuts panel to view the default shortcuts used by each panel or function module of the editor, support customization. | +| Import Cocos Creator 2.x Project | V2.x asset import tool, supporting the perfect import of old project assets, as well as the assisted migration of code. Please refer to [v3.0 Upgrade Guide](../../release-notes/upgrade-guide-v3.0.md) for details. | +| Close Window (Ctrl/Cmd + W) | Closes current window. | +| Exit | Closes the current editor and automatically opens Dashboard. | + +## Edit + +This option mainly includes common editing functions such as undo, redo, copy and paste. + +![edit](./img/edit.png) + +| Option | Description | Default Shortcut | +| :--- | :-- | :--| +| Undo | Undoes the last change to the scene. | Ctrl/Cmd + Z +| Redo | Redoes the undo action from the previous step. | Ctrl/Cmd + Shift + Z +| Cut | Cuts the currently selected node or character to the clipboard. | Ctrl/Cmd + X +| Copy | Copies the currently selected node or character to the clipboard. | Ctrl/Cmd + C +| Paste | Paste the contents of the clipboard into the appropriate location. | Ctrl/Cmd + V +| Select All | Selects all nodes in the same hierarchy if the focus is within the **Hierarchy** panel, or all assets in the same hierarchy if the focus is in the **Assets** panel. | Ctrl/Cmd + A + +> **Note**: **Copy**, **Cut** and **Paste** of nodes should be done using shortcut keys. + +## Nodes + +This option contains functions to adjust the view, disconnect prefabricated nodes and create nodes. + +![node](./img/node.png) + +| Option | Description | +| :--- | :-- | +| Align With View (Ctrl/Cmd + Shift + F) | Moves the currently selected node to the center of the view of the **Scene** panel. | +| Align View With Node | Sets the view in the **Scene** panel to be centered on the currently selected node. +| Disconnect Node With Prefab Asset | For converting the selected prefab node to a normal node, please refer to [Prefab](../../asset/prefab.md) for details. | +| Disconnect Node With Prefab Asset Recursively | Recursively convert prefab nodes containing nested nodes in the scene to normal nodes, see [Prefab](../../asset/prefab.md) for details. +| Empty Node | Creates an empty node in the scene. If the node is already selected in the scene before the command is executed, the newly created node will become a child of the selected node. | +| 3D Objects | Creator provides some relatively basic static model controls for use, currently including **Cube**, **Cylinder**, **Sphere**, **Capsule**, **Cone**, **Torus**, **Flat** and **Quad**. To create other types of models, refer to the [MeshRenderer component](../../engine/renderable/model-component.md) documentation. | +| 2D Objects | Create Creator's pre-defined 2D nodes in the scene containing the base renderable components, which currently include **Graphics**, **Label**, **Mask**, **ParticleSystem2D**, **Sprite**, **SpriteSplash** (monochrome) and **TiledMap** (map), for details please refer to the [Introduction to 2D Renderable Components](../../ui-system/components/editor/render-component.md) documentation. | +| UI Components | Create Creator's pre-defined nodes containing basic UI components in the scene, which currently include Common UI controls such as **Button**, **Widget**, **Layout**, **ScrollView**, **EditBox** nodes, etc. For more UI components, please refer to the [UI Basic Components](../../ui-system/components/editor/base-component.md) documentation. | +| Lighting | Create Creator preset nodes in the scene containing base light components, currently including **Parallel Light**, **Spherical Light** and **Spot Light**, please refer to the [Lighting](../../concepts/scene/light.md) documentation. | +| Effects | Create a Creator preset in the scene containing [Particle System](../../particle-system/overview.md) component in the scene. More effects components can be added in the **Property Inspector** panel by clicking **Add Component -> Special Effects**. | +| Camera | Creates a node in the scene with the Camera component pre-defined by Creator. For details on how to use it, please refer to the [Camera component](../components/camera-component.md) documentation. | +| Terrain | Creates a node containing the terrain component pre-defined by Creator in the scene. Please refer to the [Terrain System](../terrain/index.md) documentation for more details on how to use it. | + +## Project + +This option is mainly used to perform preview run build projects, project configuration and custom builds, etc. + +![project](./img/project.png) + +| Option | Description | +| :--- | :--- | +| Project Settings | Opens [Project Settings](../project/index.md) panel to set the relevant configuration options for a specific project. | +| Play on Device (Ctrl/Cmd + P) | Click on this option to preview the project in the selected browser/simulator window, for details please refer to the [Project Preview & Debugging](../preview/index.md) documentation. | +| Refresh Device (Ctrl/Cmd + Shift + P) | Refreshes the opened browser preview window. | +| Create Preview Template | This option is used to customize the desired preview effect, please refer to the [Web Preview Customization Workflow](../preview/browser.md) documentation for details. | +| Build (Ctrl/Cmd + Shift + B) | Opens the [Build](../publish/index.md) panel to package the game to the target platform. | +| Create Build Template | This option is used to customize the Build panel of the project, please refer to the [Custom Project Build Process](../publish/custom-project-build-template.md) documentation for details. | +| Generate Build Extension | This option is used to extend the build process, please refer to the [Extending Build Process](../publish/custom-build-plugin.md) documentation for details. | + +## Panel + +This option is mainly used to open various panels in the editor. + +![panel](./img/panel.png) + +| Option | Description | Default Shortcut | +| :--- | :-- |:-- | +| Console | Opens [Console](../console/index.md). For viewing the output log messages. |Ctrl/Cmd + 0 +| Scene | Opens the [Scene](../scene/index.md) panel. For selecting and placing various game elements such as scene images, characters, effects, UI, etc. |Ctrl/Cmd + 1 +| Assets | Opens **Assets**/**Assets Preview** panel. For accessing/managing/viewing project assets, please refer to the [Assets](../assets/index.md) documentation for details. | The shortcut for the **Assets** panel is Ctrl/Cmd + 2 +| Inspector | Opens the [Inspector](../inspector/index.md) panel. For viewing and editing the working area of the currently selected node, node components and assets. |Ctrl/Cmd + 3 +| Hierarchy | Opens the [Hierarchy](../hierarchy/index.md) panel, which shows the hierarchical relationship between all nodes in the scene in the form of a tree-like list. |Ctrl/Cmd + 4 +| Animation | Opens the [Animation](../../animation/index.md) panel or [Joint Texture Layout](../../animation/joint-texture-layout.md) panel. For editing and viewing frame animation or skeleton animation, etc. |The shortcut for the **Animation** panel is Ctrl/Cmd + 6 +| Animation Graph | Opens the [Animation Graph](../../animation/marionette/index.md) panel. Animated Graphics and Program Animation is a visual animation editing panel. | The shortcut for the **Animation** panel is Ctrl/Cmd + 5 +| Light Baking | Opens the [Lightmapping](../../concepts/scene/light/lightmap.md) panel to configure the baking properties for generating light maps. | +| Node Library | Open the Node Prefab Library panel. This panel is a very simple and straightforward visual control repository, where developers can drag and drop the controls listed here into the **Scene** or the **Hierarchy** panel to quickly create pre-configured controls. |Ctrl/Cmd + 7 +| Service | Opens the [Service](https://service.cocos.com/document/en) panel, which provides a number of quality official and third-party services that allow developers to access more extensions beyond the engine and make game development easier. |Ctrl/Cmd + 8 +| Reference Image | Opens the [reference image](../../editor/scene/index.md#reference-image) panel when creating UI in the scene |Ctrl/Cmd + 8 +| Service | Opens the [Service](https://service.cocos.com/document/en) panel, which provides a number of quality official and third-party services that allow developers to access more extensions beyond the engine and make game development easier. |Ctrl/Cmd + 9 +| Localization Editor | Opens the [L10N](../l10n/overview.md) panel. | + +## Extension + +This option mainly includes Extension Manager, Store and Create Extension, please refer to the [Extending the Editor](../extension/readme.md) chapter. + +![extension](./img/extension.png) + +| Option | Description | +| :--- | :--- | +| Extension Manager | Opens the **Extension Manager** panel, which includes editor built-in extensions, extensions installed in project directory and global directory, please refer to the [Extending the Editor](../extension/readme.md) chapter for details. | +| Store | The [Cocos Store](https://store.cocos.com/app/) is built into Cocos Creator and allows users to browse, download and automatically install official or third-party extensions and assets. You can also submit your own extensions, art materials, music and sound effects to the extension store for sharing or selling. For more information, please refer to the [Submitting Resources to Store](../extension/store/upload-store.md) documentation. +| Create Extension | This option is used to generate an [extension package](../extension/first.md) in the project/global directory to extend the editor with features. | + +## Developer + +This option contains mainly development-related menu functions such as scripts, engine and DevTools. + +![developer](./img/developer.png) + +| Option | Description | +| :--- | :--- | +| Compile Engine (Ctrl/Cmd + F7) | Compile custom engine, please refer to the [Engine Customization Workflow](../../advanced-topics/engine-customization.md) documentation. | +| Rebuild Native Engine | Compile the custom native engine simulator, please refer to [Customizing the Native Engine Simulator](../../advanced-topics/engine-customization.md#modifying-native-engine-simulator). | +| Message DevTools | Opens the Message DevTool for debugging IPC interactions at runtime inside the editor. | +| Tester | The editor's built-in extended testing tool, not yet fully functional | +|Toggle Graphics Tool | Toggles the Graphics tool panel for debugging scene rendering. | +| Message Manager | Opens the [Message Manager](../extension/contributions-messages.md) panel to display public messages and their descriptions for each feature defined by the editor. | +| Export.d.ts | Exports editor-related APIs. | +| Reload (Ctrl/Cmd + R) | Reload the editor interface. | +| UI Components | Opens the UI Components panel, which lists how to use the pre-defined UI components provided within the editor, please refer to [UI Components](../extension/ui.md). | +| Cache | The **clear code cache** in this option is used to clear the cache generated when compiling scripts. | +| VS Code Workflow | The working environment-related features of the VS Code editor, currently supporting **Add Chrome Debug Setting** and **Add Compile Task**. Please refer to the [Coding Environment Setup](../../scripting/coding-setup.md) documentation for details. | +| Toggle DevTools (Ctrl/Cmd + Shift + I) | Opens DevTools window for editor interface extension development. It is also possible to customize the log output to **Console**, please refer to [Custom output messages](../console/index.md#custom-output-messages). | +| Open Assets DevTools | Opens the Assets DevTool panel for viewing log messages during modifications to the asset-db process. | +| Open Scene DevTools | Opens the Scene DevTool panel to view log messages during modifications to the scene. | +| Open Build DevTools | Opens the Build DevTool to view all log messages generated during the build process including the call stack. | + +## Help + +![help](./img/help.png) + +| Option | Description | +| :--- |:--------------------------------------------------------------------------------------------------------------| +| User Manual | Open [User Manual](../../index.md) in default browser. | +| API Reference | Open [API Reference Documentation](%__APIDOC__%/en/) in default browser. | +| Forum | Open [Cocos Creator Forum](https://discuss.cocos2d-x.org/c/33) in default browser. | +| Release Notes | Open the [release notes](https://www.cocos.com/creator-download) for each version of Cocos Creator in default browser. | +| Engine Repository | Open [TypeScript Engine Repository](https://github.com/cocos/cocos4/) in default browser. | +| About Cocos Creator | Shows Cocos Creator-related version number and copyright information. | diff --git a/versions/4.0/en/editor/preferences/index.md b/versions/4.0/en/editor/preferences/index.md new file mode 100644 index 0000000000..e10dab26f4 --- /dev/null +++ b/versions/4.0/en/editor/preferences/index.md @@ -0,0 +1,230 @@ +# Preferences + +The **Preferences** panel provides personalized settings for the editor, which can be opened by clicking **Cocos Creator/File -> Preferences** in the editor's main menu bar. + +**Preferences** consists of several different tabs, including **General**, **External Program**, **Device Manager**, **Engine Manager**, **AssetDB**, **Console**, **Inspector**, **Preview**, **Build** and **Laboratory**. The **Preferences** panel will automatically save the changes after you modify them. + +## General + +![general](index/general.png) + +The **General** tab is mainly for configuring some basic information related to the editor, including: + +**Language**: choose Chinese or English, the editor will automatically switch the language after modifying the language setting, if some texts are not switched, refresh the editor. + +- **Number Step**: used to set the step size when adjusting the numeric properties by step button in the **Inspector** panel. The default step size is 0.001. The step buttons in the **Inspector** panel include the following two types: + + - When the mouse is moved to the right of the numeric property input box, a set of up and down arrows will appear, which can continuously increase or decrease the value by a certain step magnitude. + + ![step button](index/step.png) + + - When the mouse is hovered near the name of a numeric property, the cursor will change to ![mouse cursor](index/mouse-cursor.jpg), and then drag the mouse left and right to increase or decrease the value continuously in a certain step. + +## External Program + +The **External Program** tab is used to set up the development environment required to build for publishing to the native platform, as well as to configure some third-party programs. When the mouse is moved over a specific configuration item, a gray circular question mark icon is displayed on the left side. Clicking on this icon allows setting the configuration item to be applied to the current project or to all projects globally. When set to apply to the current project, the gray icon will turn yellow. + +![external-program](./index/external-program.png) + +- **WeChat DevTools**: used to configure the Developer tools of WeChat Mini Game, please refer to the [Publishing to WeChat Mini Game](../publish/publish-wechatgame.md) documentation. + +- **Android NDK**: used to set up the Android NDK path, please refer to the [Setup Native Development Environment](../publish/setup-native-development.md) documentation. + +- **Android SDK**: used to set the Android SDK path, please refer to the [Setup Native Development Environment](../publish/setup-native-development.md) documentation. + +- **HarmonyOS NDK**: used to set the HarmonyOS NDK path, please refer to the [Publish for the Huawei HarmonyOS](../publish/publish-huawei-ohos.md) documentation. + +- **HarmonyOS SDK**: used to set the HarmonyOS SDK path, please refer to the [Publish for the Huawei HarmonyOS](../publish/publish-huawei-ohos.md) documentation. + +- **Default Script Editor**: choose any executable file from an external text editing tool (e.g.: [VS Code](../../scripting/coding-setup.md)) as the way to open the script file when you double-click it in the **Assets** panel. The executable file of the preferred text editor can be selected by clicking the **Search** button after the input box. The folder icon is used to open the path to the text editor that has been set up. + +- **Default Browser**: used to select the browser to be used when previewing the editor. A browser path can be specified by clicking the **Search icon** button behind the input box. + +## Device Manager + +The **Device Manager** tab is used to manage the device resolution when using the simulator or browser preview, and supports adding/modifying/deleting custom device resolutions manually on the right side of the panel. The editor's default device resolution does not support modification/deletion. + +![device-manager](./index/device-manager.png) + +## Engine Manager + +The **Engine Manager** tab is used to configure the engine path when customizing the engine. + +![engine-manager](./index/engine-manager.png) + +- **Use built-in TypeScript engine**: whether to use the engine path that comes with the Cocos Creator installation path as the TypeScript engine path. This engine is used for scene rendering in the scene editor, declaration of built-in components and other engine modules in the web environment. + +- **Custom TypeScript engine path**: in addition to using your own engine, an engine can also be cloned from the [engine repository](https://github.com/cocos/cocos4/) or forked to any local location for customization, uncheck **Use built-in TypeScript engine** and specify **Custom TypeScript engine path** as the customized engine path, then it's ready to use. + +- **Use built-in native engine**: whether to use the `engine-naive` path that comes with the Cocos Creator installation path as the native engine path. This engine is used to build and compile projects for all native platforms (iOS, Android, Mac, Windows) when building. + +- **Custom native engine path**: after unselecting the previous item **Use built-in native engine**, the native engine path can be specified manually. + +> **Note**: the native engine used here must be from [engine-native](https://github.com/cocos/cocos4/) or the fork of that repository. + +For details on customizing the engine, please review the [Engine customization workflow](../../advanced-topics/engine-customization.md) documentation. + +## Asset Database + +The **Asset database** tab is used to set the [Assets](../assets/index.md) panel with information about the asset database, including **Log Levels**, **Ignore (regular)** and **Default Meta**. + +![asset-db](./index/asset-db.png) + +- **Log Level**: used to set the type of information output to the **console** from the asset database in the **Assets** panel. This currently includes **Error Only**, **Error and Warning Only**, **Error, Warning, and Log**, and **Output All Information**. +- **Ignore Files (Glob)**: Use Glob patterns to match files that should be ignored. The `!` prefix in Glob patterns means to exclude matching resources from search results, and the editor will not import these resources. Editor restart is required after modification. + - Example: `!**/*.txt` ignores all `.txt` files. + - Example: `!test` ignores all files in the relative path assets/test folder. + - Example: `!**/Node.*` ignores all files named Node. +- **Update resources automatically**: automatically refresh resources when returning to the editor from outside. See below for details. +- **Automatically overwrite metadata on import**: when importing assets to replace existing assets, if the imported assets come with Meta, use this option to set whether to overwrite the Meta of the existing assets. +- **Default Meta**: used to set the default configuration when importing assets within a project. Please refer to the description below for details. + +### Update resources automatically + +If this option is enabled, all resources will be automatically checked when returning to Creator, regardless of whether resources have been manipulated outside of Creator. Then when the number of files in the project is too high, or the random read and write speed of the hard disk is too low, it will cause the resource system response lag. For example, if a resource is selected in **Assets** panel, the **Inspector** panel will take a while to show the resource-related properties. + +At this point, please disable the automatic refresh feature of Creator. After disabling this feature, if resources are manipulated, then manually click the **Refresh** button at the top right of the **Assets** panel to refresh the resources. + +> **Note**: it is not recommended to turn this option off if not experiencing resource system response problems. + +### Default Meta + +This option is used to set the default configuration when importing assets into the project. For example, if want the imported image to be `sprite-frame` by default, then click on the **Edit** button to the right of this option and fill in the following in the `json` file that opens: + +```json5 +{ + // 'image' indicates that the type of the asset is an image. + "image": { + "type": "sprite-frame" + } +} +``` + +After editing and saving, return to the editor and click the **Apply** button to take effect. + +The asset type is `key`, and the key value `value` needs to be an **object**, which is the default configuration used when the asset is imported.
+For example, in the above sample code, the `key` is `image`, the `value` is the content configured in `image`, and the configuration information of the asset database will be refreshed after clicking the **Apply** button, and the content in `image` will be configured into the `userData` field of the asset `meta` file one by one. For example, if `image.type` is set to `sprite-frame`, the default `userData.type` will be set to `sprite-frame` when importing image assets. So the default import configuration of various assets can be set dynamically according to project needs. + +If want to get the asset type, just right-click on the asset in **Assets** panel, select **Reveal in Explorer**, then find the meta file corresponding to the asset in the opened folder and open it, the asset type will be marked in the `importer` field. + +For example, the meta file for a material asset is as follows, and the `material` in the `importer` field is the asset type. + +```json +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "482a5162-dad9-446c-b548-8486c7598ee1", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} +``` + +## Console + +The **Console** tab is used to set the [Console](../console/index.md) panel output log, including **Display date** and **Font size**. + +![console](./index/console.png) + +- **Display date**: whether to display the date in front of the log output from the **Console** panel. +- **Font size**: used to set the text size of the log output from the **Console** panel. + +## Inspector + +The **Auto-save when leaving edit** option in the **Inspector** tab is used to set the [Inspector](../inspector/index.md) panel to automatically save changes after the property edit is complete. + +![inspector](./index/inspector.png) + +## Preview + +The **Preview** tab is mainly used for the various options that can be set when using the [Preview](../preview/index.md) button on the top of the editor, but is only available for the current project. + +![preview](./index/preview.png) + +- **Auto refresh preview when saving scene**: if this option is checked, the opened preview page will be automatically refreshed when saving the scene in the editor. Currently the preview using the simulator is not supported for now. +- **Auto clear cache when using simulator**: if this option is checked, the cache will be automatically cleared when using the simulator preview. +- **Open simulator debugger**: if this option is checked, the debugger will be opened automatically when previewing the project with the simulator. +- **Simulator wait for debugger to connect**: this option will take effect when **Open the simulator debugger** is checked, and it is used to pause the simulator startup process until the debugger is connected, which is used for debugging the loading process. +- **Preview server port number**: Port for the preview server, Cocos Creator will create a http/https server to preview game when you click the preview button. +- **Enable HTTPS**:Whether to enable HTTPS for debugging +- **HTTPS port**: Server port of HTTPS +- **Private key**: Path of the private key +- **Public cert**: Path of the public cert +- **CSR file for certificate singing**: The path of CSR file. + +## Animation + +**Animation** Paging is used to set up the [animation system](../../animation/index.md) some parameters and configurations during editing. + +![animation](./index/animation.png) + +- **Enable animation instant cache function**: when enabled, the animation in the editor will be automatically cached according to the **Caching interval time** +- **Cache interval time**: the time between caches, in milliseconds. +- **Maximum number of cached files**: the maximum number of animations that can be cached + +> **Note**: Currently the cached animation files are not automatically restored, if you want to restore the cached files, please refer to [Animation System](../../animation/index.md). + +## Build + +The **Build** tab is used to set up the execution of the [Build](../publish/build-panel.md), including **Log file opening method** and **Cache Serialized JSON of Assets**. + +![build](./index/build.png) + +- **Log file opening method**: This option is used to set whether to open the build log file directly or to open the directory where the log file is located, when clicking the **Open Log** button at the bottom left of the [platform build task](../publish/build-panel.md). The default is to open the log file directly. + +- **Cache Serialized JSON of Assets**: in order to speed up the build and reduce the repeated deserialization of unmodified assets, the serialized JSON of assets will be cached during the asset build process, which will be placed in the `temp/asset-db/assets/uuid/build` directory of the project and divided into `debug.json` and `release.json` according to **debug** and **release** mode. + + ![build](./index/json.png) + + When a cached asset exists the build will take it directly, and this part of the cached asset will be re-updated after each asset import. This option is checked by default, but if some special requirements are encountered and want to build without storing this serialized build cache asset, just uncheck it. + +- **Cache Build Engine**: Cache the compiled engine, this cache will take effect in the global directory. Developers can print this cache address by using the `Editor.App.temp` command in **Developer -> Switch Developer Tools**: + + ![build-temp](./index/build-temp.png) + + With this feature, it is possible to speed up the build. This feature can also be viewed in the log at: + + ![cached-log](./index/build-log-cached-engine.png) + +- **Cache Compressed Texture**: Cache the compressed texture assets, if the texture parameters are modified, the cache will be invalidated, the cache directory is: `temp/builder/CompressTexture`. +- **Cache Auto Atlas**:The cache of the auto-atlas can be cleared at build time, for details please refer to the [Build Tasks](../publish/build-panel.md#Build%20Tasks) +- **Keep UUID in the Node Component**:By default, component UUIDs are not serialized into Prefabs/Assets, but are automatically generated by the engine at runtime. When enabled, the component's UUID is serialized into the corresponding Prefab/Assets at build time and remains the same at runtime and editor time. + +## Laboratory + +The **Laboratory** tab will occasionally provide some new technical solutions or experimental features that can be selected via a switch option to be used or not, and in most cases are turned on by default. Currently these include **Scene Real-time Cache** and **Enable baking feature**. + +![laboratory](./index/laboratory.png) + +- **Enable Deferred Rendering Pipeline**: enables or disables the deferred rendering pipeline. By default, deferred rendering pipelines are disabled. For details, see [Deferred Render Pipeline](../../render-pipeline/builtin-pipeline.md#%E5%BB%B6%E8%BF%9F%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF). + +- **Automatically update script's import path after relocation**: when enabled, the import part of the script will be automatically modified and relocated to the new location when you move the script in the editor. + +- **Optimized scheduling strategy**: This policy will try to merge resources when importing them multiple times repeatedly to reduce the number of scheduling times. + +- **Scene real-time cache**: this item is enabled by default, it is mainly used to cache scene files to `temp/scene/[SCENE_UUID]/[TIME].json` file in project directory every once in a while during scene editing (the current time interval is 5s). In case of unexpected situation, such as scene crash, process crash, etc., when you open the editor again, a popup window will prompt you whether to apply the latest scene file in the cache. + + > **Note**: In daily use, as long as the scene is opened normally, all scene files cached before the current scene is opened will be cleared. If you have a special need to view the cache files of a specific scene, please close the corresponding scene in the editor first. + +- **Keep scene main loop running**: whether or not to allow the scene to be rendered in the same way as in the preview with a constant rendering loop. +- **Enable native engine loading scene editor**: when enabled, the rendering module inside the editor will use the native engine. +- **Debug native engine for scene editor**: whether or not the native engine scene can be enabled. When this option is enabled, it will prevent the native engine from starting and a dialog box will pop up, click to confirm before it will continue to start. It is mainly used for debugging the logic inside the scene after the native engine has been activated. + +- **Animation Embedded Player**: This feature supports the user to synchronize with other particles and animations while editing the animation +- **Animation Auxiliary Curve**: If or not the animation auxiliary curve function is enabled, please check [Auxiliary Curve Edit View](../../animation/animation-auxiliary-curve.md) for details. + +- **Enable Pose-Express Function**: If or not enable the animation gesture map function, please check [Programmatic Animation](../../animation/animation-auxiliary-curve.md) for more details. +- **Enable baking feature**: Use to enable the baking function, please refer to [Light Mapping](../../concepts/scene/light/lightmap.md) + +### Caution + +In future releases, these features in **Laboratory** may be merged, but there is also a chance that compatibility-breaking changes may occur, or may even be removed. If needing to use these features in a development environment, please be sure to test them rigorously and keep an eye out for update announcements for new releases. + +We welcome users to turn on the trial of these features and provide valuable feedback in our [Forum](https://discuss.cocos2d-x.org/c/33) to make these features more suitable for their own usage scenarios and provide more powerful help for projects. + +## Extending the Preferences Panel + +Creator supports adding custom functional pages on the right side of **Preferences**, please refer to the [Extended Preferences](../../editor/extension/contributions-preferences.md) documentation for details. diff --git a/versions/4.0/en/editor/preferences/index/add-component.png b/versions/4.0/en/editor/preferences/index/add-component.png new file mode 100644 index 0000000000..e4873386ac Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/add-component.png differ diff --git a/versions/4.0/en/editor/preferences/index/animation.png b/versions/4.0/en/editor/preferences/index/animation.png new file mode 100644 index 0000000000..06d918598d Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/animation.png differ diff --git a/versions/4.0/en/editor/preferences/index/asset-db.png b/versions/4.0/en/editor/preferences/index/asset-db.png new file mode 100644 index 0000000000..05c82ae66f Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/asset-db.png differ diff --git a/versions/4.0/en/editor/preferences/index/build-log-cached-engine.png b/versions/4.0/en/editor/preferences/index/build-log-cached-engine.png new file mode 100644 index 0000000000..79664a0d29 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/build-log-cached-engine.png differ diff --git a/versions/4.0/en/editor/preferences/index/build-temp.png b/versions/4.0/en/editor/preferences/index/build-temp.png new file mode 100644 index 0000000000..7de1fe5306 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/build-temp.png differ diff --git a/versions/4.0/en/editor/preferences/index/build.png b/versions/4.0/en/editor/preferences/index/build.png new file mode 100644 index 0000000000..89fbe3d5e9 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/build.png differ diff --git a/versions/4.0/en/editor/preferences/index/console.png b/versions/4.0/en/editor/preferences/index/console.png new file mode 100644 index 0000000000..a9c921f224 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/console.png differ diff --git a/versions/4.0/en/editor/preferences/index/device-manager.png b/versions/4.0/en/editor/preferences/index/device-manager.png new file mode 100644 index 0000000000..66a0e9ebba Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/device-manager.png differ diff --git a/versions/4.0/en/editor/preferences/index/engine-manager.png b/versions/4.0/en/editor/preferences/index/engine-manager.png new file mode 100644 index 0000000000..92bf8c1376 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/engine-manager.png differ diff --git a/versions/4.0/en/editor/preferences/index/external-program.png b/versions/4.0/en/editor/preferences/index/external-program.png new file mode 100644 index 0000000000..4d19ca130d Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/external-program.png differ diff --git a/versions/4.0/en/editor/preferences/index/general.png b/versions/4.0/en/editor/preferences/index/general.png new file mode 100644 index 0000000000..a7d901c7b4 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/general.png differ diff --git a/versions/4.0/en/editor/preferences/index/inspector.png b/versions/4.0/en/editor/preferences/index/inspector.png new file mode 100644 index 0000000000..eb093a66e5 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/inspector.png differ diff --git a/versions/4.0/en/editor/preferences/index/json.png b/versions/4.0/en/editor/preferences/index/json.png new file mode 100644 index 0000000000..cbd3490d40 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/json.png differ diff --git a/versions/4.0/en/editor/preferences/index/laboratory.png b/versions/4.0/en/editor/preferences/index/laboratory.png new file mode 100644 index 0000000000..e4fd48bc9b Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/laboratory.png differ diff --git a/versions/4.0/en/editor/preferences/index/mouse-cursor.jpg b/versions/4.0/en/editor/preferences/index/mouse-cursor.jpg new file mode 100644 index 0000000000..df66301fc4 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/mouse-cursor.jpg differ diff --git a/versions/4.0/en/editor/preferences/index/preview.png b/versions/4.0/en/editor/preferences/index/preview.png new file mode 100644 index 0000000000..1ffbc09f63 Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/preview.png differ diff --git a/versions/4.0/en/editor/preferences/index/step.png b/versions/4.0/en/editor/preferences/index/step.png new file mode 100644 index 0000000000..71626d43fe Binary files /dev/null and b/versions/4.0/en/editor/preferences/index/step.png differ diff --git a/versions/4.0/en/editor/preview/browser.md b/versions/4.0/en/editor/preview/browser.md new file mode 100644 index 0000000000..f660733db4 --- /dev/null +++ b/versions/4.0/en/editor/preview/browser.md @@ -0,0 +1,48 @@ +# Web Preview Customization Workflow + +## Custom Preview Templates + +Preview supports custom templates for users to customize the preview effect they need, and the custom preview template can be placed in the `preview-template` folder in the project directory. Or click **Project -> Create Preview Template** in the editor main menu to create an updated preview template in the project directory. The preview in the editor also uses the template to inject the latest project data, the preview will look for the index file in the directory and if it exists it will be used as the template for the preview. + +The `preview-template` folder has a structure similar to + +```js +project-folder + |--assets + |--build + |--preview-template + // Required entry file + |--index.ejs + // Other files can be added according to the preview effect you want to achieve +``` + +To start customizing the page preview, it should be noted that there are some preview menu items and preview debugging tools in the preview template. Be careful when adding or deleting some template syntax. Random changes may cause the preview template to be unavailable. It is recommended to keep all the content injected with ejs and then add the required content on top of it. Also, if `index.html` and `index.ejs` are coexisting, **`index.html` will replace `index.ejs`** as the preview page content. + +## Usage examples + +1. Click **Project -> Create Preview Template** in the editor main menu, the **Console** will output the message "Preview Template generated successfully" and show the path of the generated preview template. + +2. Add scripts like `test.js`, where `<%- include(cocosTemplate, {}) %>` contains the default start game logic, and the added scripts can be stored before/after the game logic is started as needed. The following `test.js` is loaded after the game is launched. + + - Open `index.ejs` and modify it as follows: + + ```html + + ... + + ... + <%- include(cocosTemplate, {}) %> // Game launch processing logic + // Add a new script + + + ``` + + - `test.js` is placed in the relative path of the logo within the page (only in the `preview-template` folder) + + ``` + |--preview-template + |--index.ejs + |--test.js + ``` + +For more details, please refer to the example [Preview Template](https://github.com/cocos/cocos-test-projects/tree/v3.8/preview-template). diff --git a/versions/4.0/en/editor/preview/index.md b/versions/4.0/en/editor/preview/index.md new file mode 100644 index 0000000000..a267bd4f60 --- /dev/null +++ b/versions/4.0/en/editor/preview/index.md @@ -0,0 +1,143 @@ +# Project Preview & Debugging + +After using the main editor panel to import assets, build scenes, configure components, and adjust properties, we can now preview and build to see how the game will look when running on the Web or Native platform. + +## Selecting the preview platform in the editor + +At any time during game development we can click the **Preview** button at the top of the editor window to see the game in action. + +![select-platform](index/select-platform.png) + +- The left drop-down menu allows selecting the platform for previewing, currently **Browser**, **Preview in Editor** and **Simulator** are supported. +- ![play](index/play.png): preview button, click it to run the preview +- On the right side of the **Preview** button you can select the scene to preview, the dropdown box will list all the scenes in the project, by default **Current scene** is used. + + ![select-scene](index/select-scene.png) + +- ![play](index/refresh.png): refresh button, click this button to refresh all opened preview pages. To refresh the preview page automatically after the save scene operation, select **Cocos Creator/File -> Preferences -> Preview** in the editor main menu, and then check **Refresh preview page when saving scene**. + +> **Note**: double-click is needed to open the scene to preview the game content. If there are not any scenes open, or if an empty scene is created, no content will show. + +### Browser + +After selecting **Browser** to preview, click the **Preview** button next to it to run the web version of the game directly in the default desktop browser. It is recommended to use Google Chrome as the browser for previewing and debugging during development, as Google Chrome has the most comprehensive and powerful DevTools. + +At the top of the browser preview screen there are a series of controls to control the preview effect: + +- The leftmost menu is used to select the scale size of the preview window to simulate the display effect on different mobile devices, and the device resolution can be added manually in **Cocos Creator/File -> Preferences -> Device Manager**. +- **Rotate**: decide whether to display landscape or portrait screen +- **Debug Mode**: choose which levels of log in the script will be output to the browser console +- **Show FPS**: allows choosing whether to display debug information such as frames per second and number of drawcalls in the bottom left corner +- **FPS**: allows limiting the maximum number of frames per second +- **Pause**: pause the game + +![browser](index/browser.png) + +Creator supports the feature of web preview customization, please refer to the [web preview customization workflow](browser.md) documentation for details. + +#### Browser compatibility + +The desktop browsers tested during Cocos Creator development include: **Chrome**, **Firefox** and **QQ Browser**, other browsers can be used normally as long as the kernel version is high enough. + +Browsers tested on mobile devices include: **Safari (iOS)**, **Chrome (Android)**, **QQ Browser (Android)** and **UC Browser (Android)**. + +#### Debugging with browser DevTools + +Taking Google Chrome as an example, click **Developer -> Toggle DevTools** in the main menu of the editor to open the DevTools interface. DevTools, allow viewing the runtime log, using breakpoints to debug, viewing the value of each variable at each step in the call stack, and even performing asset loading and performance analysis. + +To learn how to use DevTools for debugging, read the [Chrome DevTools User Guide](https://developers.google.com/web/tools/chrome-devtools), or the DevTools help for other browsers. + + + +### Preview In Editor + +![editor preview](index/editor-preview-props.png) + +To preview within the editor, select **Preview in Editor** (mark 1 below) from the drop-down menu and then click on the **Run Preview** (mark 2 at the bottom) button to preview. + +![bar](index/edit-bar.png) + +Click ![pause](index/pause.png) to pause the current preview, and after pause you can also single step by clicking ![step](index/step.png) to run a single step. + +When you don't want to continue the preview, you can click ![stop](index/stop.png) button to stop. + +When selecting the editor preview, the preview output can also be configured via the menu at the top of the preview window. + +- ![state](index/state.png) Click to select whether to display the following state + within the view. + + ![info](index/state-info.png) + +- ![FPS](index/FPS.png) Maximum frame rate for previews, or you can enter a custom frame rate. + + ![max](index/max-fps.png) + +- ![resolution](index/resolution.png) Resolution at preview, the drop-down menu allows you to select a different resolution, or if the current engine's built-in resolution cannot be met, you can pull the drop-down box to the bottom and select **Edit**. + + ![edit](index/edit.png) + + You can also add/modify/remove resolutions in [Device Manager](../preferences/index.md#Device%20Manager) in **Preferences** in the top menu bar to add/modify/remove resolutions. + + ![device manager](index/device-manager.png) + +- ![rotate](index/rotate-view.png) Toggle landscape/portrait preview +- ![scale](index/scale-game-view.png) Zoom view +- ![fit](index/fit.png) Adapting the game view to fit the current view + +The properties of the component can also be modified for debugging within the **Inspector** panel during preview, but note that the data entered at runtime is not saved. If you wish to save the adjusted component, you can click on the **Component Menu** on the right and select **Copy Component** in the drop-down list, then select **Paste Component Value** after the preview exited. + +![copy](index/copy-component.png) + +### Simulator + +When you select **Simulator** preview, the current game scene will be run using Cocos Simulator (desktop simulator). When running the game with the simulator, log messages (printed with `cc.log`) and error messages from the script are displayed in the **Console** panel. + +![simulator](index/simulator.png) + +When using **Simulator** preview, it supports automatically opening debug window for debugging, which can be set in **Cocos Creator/File -> Preferences -> Preview**, please refer to [Preferences - Preview](../preferences/index.md). + +To add support for the iOS simulator on Apple M1 (Silicon) architecture devices, click the **App** button in the upper right corner of the Creator, open the `CMakeLists.txt` file in the `resources\3d\engine\native\external` directory of the Editor, and uncomment the code related to the iOS simulator and comment the code related to the iOS physical device, as shown below: + +![simulator](index/ios-simulator-m1.png) + +## Mobile preview + +There are the following ways to debug the preview effect on mobile phone: + +- Use **Mobile Preview mode of Browser DevTools** + +- **Scan the preview QR code** + + Move the mouse over the IP preview address on the left side of the editor toolbar, a preview QR code will be displayed, just scan it with your phone. + + ![preview-url](index/preview-url.png) + +- Directly **Enter the preview address in your cell phone browser** + +> **Note**: the phone needs to be on the **same network segment** as the computer. Since there may be more than one network for the computer, if the IP address of the editor preview URL is not selected correctly, change it in the main menu bar in **Cocos Creator/File -> Preferences -> General -> Preview IP**. + +## Build + +After previewing and debugging, if you are satisfied with your game, you can open the **Build** panel via **Project -> Build** in the main menu to package and publish the game to the target platforms, including Web, iOS, Android, various "mini-games", PC clients and other platforms. For the detailed build and publish process, please refer to the [Cross-Platform Game Publishing](../publish/index.md) documentation. + +> **Note**: the effect of running the game with **Simulator** and the final release to the native platform may be somewhat different, for any important game features, please do the final test with the version after build and release. + +## Other references + +- [Introduction to the Preview Process and Common Error Handling](preview-guid.md) diff --git a/versions/4.0/en/editor/preview/index/FPS.png b/versions/4.0/en/editor/preview/index/FPS.png new file mode 100644 index 0000000000..e3d9429e13 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/FPS.png differ diff --git a/versions/4.0/en/editor/preview/index/browser.png b/versions/4.0/en/editor/preview/index/browser.png new file mode 100644 index 0000000000..5aa0d58d78 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/browser.png differ diff --git a/versions/4.0/en/editor/preview/index/copy-component.png b/versions/4.0/en/editor/preview/index/copy-component.png new file mode 100644 index 0000000000..745b270be6 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/copy-component.png differ diff --git a/versions/4.0/en/editor/preview/index/custom.png b/versions/4.0/en/editor/preview/index/custom.png new file mode 100644 index 0000000000..b761bbea4a Binary files /dev/null and b/versions/4.0/en/editor/preview/index/custom.png differ diff --git a/versions/4.0/en/editor/preview/index/device-manager.png b/versions/4.0/en/editor/preview/index/device-manager.png new file mode 100644 index 0000000000..50c86d166e Binary files /dev/null and b/versions/4.0/en/editor/preview/index/device-manager.png differ diff --git a/versions/4.0/en/editor/preview/index/edit-bar.png b/versions/4.0/en/editor/preview/index/edit-bar.png new file mode 100644 index 0000000000..7a9cc87757 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/edit-bar.png differ diff --git a/versions/4.0/en/editor/preview/index/edit.png b/versions/4.0/en/editor/preview/index/edit.png new file mode 100644 index 0000000000..1887a24d10 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/edit.png differ diff --git a/versions/4.0/en/editor/preview/index/editor-preview-props.png b/versions/4.0/en/editor/preview/index/editor-preview-props.png new file mode 100644 index 0000000000..97cc7fc077 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/editor-preview-props.png differ diff --git a/versions/4.0/en/editor/preview/index/fit.png b/versions/4.0/en/editor/preview/index/fit.png new file mode 100644 index 0000000000..d374e4d18d Binary files /dev/null and b/versions/4.0/en/editor/preview/index/fit.png differ diff --git a/versions/4.0/en/editor/preview/index/gameview.png b/versions/4.0/en/editor/preview/index/gameview.png new file mode 100644 index 0000000000..33c1603959 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/gameview.png differ diff --git a/versions/4.0/en/editor/preview/index/ios-simulator-m1.png b/versions/4.0/en/editor/preview/index/ios-simulator-m1.png new file mode 100644 index 0000000000..a249cccc66 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/ios-simulator-m1.png differ diff --git a/versions/4.0/en/editor/preview/index/max-fps.png b/versions/4.0/en/editor/preview/index/max-fps.png new file mode 100644 index 0000000000..828a6be9b0 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/max-fps.png differ diff --git a/versions/4.0/en/editor/preview/index/pause.png b/versions/4.0/en/editor/preview/index/pause.png new file mode 100644 index 0000000000..3da2bf5106 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/pause.png differ diff --git a/versions/4.0/en/editor/preview/index/play.png b/versions/4.0/en/editor/preview/index/play.png new file mode 100644 index 0000000000..884367029b Binary files /dev/null and b/versions/4.0/en/editor/preview/index/play.png differ diff --git a/versions/4.0/en/editor/preview/index/preferences.png b/versions/4.0/en/editor/preview/index/preferences.png new file mode 100644 index 0000000000..ce93c7606b Binary files /dev/null and b/versions/4.0/en/editor/preview/index/preferences.png differ diff --git a/versions/4.0/en/editor/preview/index/preview-type-drop-down.png b/versions/4.0/en/editor/preview/index/preview-type-drop-down.png new file mode 100644 index 0000000000..2974d3682b Binary files /dev/null and b/versions/4.0/en/editor/preview/index/preview-type-drop-down.png differ diff --git a/versions/4.0/en/editor/preview/index/preview-url.png b/versions/4.0/en/editor/preview/index/preview-url.png new file mode 100644 index 0000000000..8157c9de21 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/preview-url.png differ diff --git a/versions/4.0/en/editor/preview/index/refresh.png b/versions/4.0/en/editor/preview/index/refresh.png new file mode 100644 index 0000000000..159322e6c6 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/refresh.png differ diff --git a/versions/4.0/en/editor/preview/index/resolution.png b/versions/4.0/en/editor/preview/index/resolution.png new file mode 100644 index 0000000000..d4b6c5cc49 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/resolution.png differ diff --git a/versions/4.0/en/editor/preview/index/rotate-view.png b/versions/4.0/en/editor/preview/index/rotate-view.png new file mode 100644 index 0000000000..ae3e19045f Binary files /dev/null and b/versions/4.0/en/editor/preview/index/rotate-view.png differ diff --git a/versions/4.0/en/editor/preview/index/scale-game-view.png b/versions/4.0/en/editor/preview/index/scale-game-view.png new file mode 100644 index 0000000000..e98e8562c4 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/scale-game-view.png differ diff --git a/versions/4.0/en/editor/preview/index/select-platform.png b/versions/4.0/en/editor/preview/index/select-platform.png new file mode 100644 index 0000000000..e3beced184 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/select-platform.png differ diff --git a/versions/4.0/en/editor/preview/index/select-preview-type.png b/versions/4.0/en/editor/preview/index/select-preview-type.png new file mode 100644 index 0000000000..c1568919a6 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/select-preview-type.png differ diff --git a/versions/4.0/en/editor/preview/index/select-scene.png b/versions/4.0/en/editor/preview/index/select-scene.png new file mode 100644 index 0000000000..06255ae8e2 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/select-scene.png differ diff --git a/versions/4.0/en/editor/preview/index/simulator.png b/versions/4.0/en/editor/preview/index/simulator.png new file mode 100644 index 0000000000..0a8ff618a9 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/simulator.png differ diff --git a/versions/4.0/en/editor/preview/index/state-info.png b/versions/4.0/en/editor/preview/index/state-info.png new file mode 100644 index 0000000000..539b3b85dd Binary files /dev/null and b/versions/4.0/en/editor/preview/index/state-info.png differ diff --git a/versions/4.0/en/editor/preview/index/state.png b/versions/4.0/en/editor/preview/index/state.png new file mode 100644 index 0000000000..affad83f9b Binary files /dev/null and b/versions/4.0/en/editor/preview/index/state.png differ diff --git a/versions/4.0/en/editor/preview/index/step.png b/versions/4.0/en/editor/preview/index/step.png new file mode 100644 index 0000000000..be0ac0f620 Binary files /dev/null and b/versions/4.0/en/editor/preview/index/step.png differ diff --git a/versions/4.0/en/editor/preview/index/stop.png b/versions/4.0/en/editor/preview/index/stop.png new file mode 100644 index 0000000000..bf3441191e Binary files /dev/null and b/versions/4.0/en/editor/preview/index/stop.png differ diff --git a/versions/4.0/en/editor/preview/preview-guid.md b/versions/4.0/en/editor/preview/preview-guid.md new file mode 100644 index 0000000000..1a8fd96e03 --- /dev/null +++ b/versions/4.0/en/editor/preview/preview-guid.md @@ -0,0 +1,23 @@ +# Introduction to the Preview Process and Common Error Handling + +This section introduces the browser preview process. + +## Process Introduction + +The browser preview is essentially the editor opening an express server that opens the user's default browser to access the preview URL when the "Preview" button is clicked. the preview template has some simple logic written in it for loading the engine and initializing the scene, while the loading of user assets relies heavily on the generation of `settings.json`, as `settings.json` contains the The current project's assets, scripts, and project settings are documented in `settings.json`. The preview `settings.json` is generated by calling the interface of the build extension, so if `settings.json` is not generated properly, select **Developers -> Open Build DevTools** to see it. + +## Common Error Handling + +When encountering a preview that does not display properly, first, open NetWork in DevTools to see if there are assets or scripts that failed to load at this time. + +### `settings.json` failed to load + +Before looking for the cause of this issue, make sure there are no error messages in the editor before previewing. + +Under normal circumstances, `settings.json` load failure means `settings.json` generation fails, there will be error messages in console. Most common is a script error, because when generating `settings.json`, all scripts in the project are loaded in the build process. If any script contains illegal writing, an unexpected error will be thrown during the loading process and `settings.json` will fail to be generated. For specific error message information, you can refer to the hint in the error message. Usually the error content here is the UUID of the asset. The corresponding **UUID** can be copied to the __Assets__ panel to search and locate the script. + +For more information about the specific generation process of `settings.json`, please refer to the [Introduction to the Build process and FAQ](../publish/build-guide.md) documentation. The preview `settings.json` generation rules are basically the same as the generation rules with only debug mode checked, the difference is that the preview only sorts out asset information and does not package the assets. + +### Asset loading 404 + +Usually, this is caused by asset loss or import failure. Please **use the missing asset UUID to search in the editor's assets panel**. If no assets are found, usually the asset is lost. You need to modify the scene or other assets that use this lost asset. If assets are found, you can try to re-import. diff --git a/versions/4.0/en/editor/project/index.md b/versions/4.0/en/editor/project/index.md new file mode 100644 index 0000000000..d91cea5bcc --- /dev/null +++ b/versions/4.0/en/editor/project/index.md @@ -0,0 +1,403 @@ +# Project Settings + +The **Project Settings** panel can be opened by clicking on **Project -> Project Settings** in the editor's main menu bar and is mainly used to set project-specific configuration items. These settings are stored in the project's `settings/packages` folder. To synchronize project settings between developers, please add the `settings` directory to version control. + +**Project Settings** consists of several different sub-pages, including **Project Data**, **Layers**, **Physics**, **Scripting**, **Macro Config**, **Feature Cropping** and **Texture Compression**. The **Project Settings** panel will automatically save the changes after modifying the settings. + +## Project Data + +The **Project Data** tab is mainly used to set the default Canvas, render pipeline, etc. and only works for the current project. + +![project-data](./index/project-data.png) + +### Default Canvas Settings + +The default Canvas settings include **Design Resolution** and **Fit Screen Width/Height**, which specify the default design resolution values in Canvas when a new scene or Canvas component is created, as well as `Fit Height` and `Fit Width`. For more details, please refer to the [Multi-Resolution Adaptation Solution](../../ui-system/components/engine/multi-resolution.md) documentation. + +### High Quality + +When using Cocos Dashboard to create projects with the default template 3D HQ, shadows and lighting settings are turned on by default to achieve higher quality rendering, eliminating the need to manually turn on multiple shadows and lighting settings for scenes, objects, lights, etc. +This high quality mode is the toggle switch for the template function. It mainly affects the lighting effect of 3D projects, and has no effect on the game quality. + +### Render Pipeline + +The render pipeline is used to control the rendering process of the scene, currently supports **builtin-forward** (Forward Render Pipeline) and **builtin-deferred** (Deferred Render Pipeline), developers can also customize the render pipeline. Please refer to the [Render Pipeline Overview](../../render-pipeline/overview.md) documentations for details. + +## Layers + +![Layers](./index/layers.png) + +- Layers allow the camera to render parts of the scene and lights to illuminate parts of the scene. +- Layers can be customized from 0 to 19, and clearing the input box removes the original settings. +- The last 12 Layers are built-in to the engine and cannot be modified. +- The locations where Layers are currently used include: + + 1. The `Layer` property of the node `Node` in the **Inspector** panel, For more details, please refer to the [Node Component](../../concepts/scene/node-component.md#setting-the-visibility-of-nodes) documentation. + + ![Layers-node](./index/layers-node.png) + + 2. The `Visibility` property of the Camera node in the **Inspector** panel, the `Layer` property of the node matches the `Visibility` property of the Camera. A node can only be seen by the Camera if the `Layer` property set by the node is checked in the `Visibility` of the Camera. For more details, please refer to the [Camera Component](./../components/camera-component.md#camera-component) documentation. + + ![Layers-camera](./index/layers-camera.png) + + + +## Sorting Layers + +![sorting-layer](index/sorting-layer.png) + +Customize the rendering order for both 2D UI and 3D objects. + +* Operation Instructions + - Click the delete button on the right to remove the corresponding layer. + - Except for the `default` layer which cannot be edited, all other layers can have their names manually edited. + - Click the plus button to add different layers. + - Drag and drop the gray striped button on the left side of layers to manually rearrange them. + + ![drag](index/drag-to-sort.gif) + +* **(2D UI Rendering Order)** After adding render layers, attach the [Sorting2D](../../engine/rendering/sorting-2d.md) component to any 2D render component node and configure the sorting layer and in-layer order to customize the 2D UI rendering sequence. +![sorting-2d](../../../zh/engine/rendering/sorting-2d/sorting2D-component.png) + +* **(3D Object Rendering Order)** After adding render layers, attach the [Sorting](../../engine/rendering/sorting.md) component to any node with MeshRenderer or SpriteRenderer components and configure the sorting layer and in-layer order to customize the 3D object rendering sequence. +![sorting](index/sorting.png) + +## Physics + +![physics](./index/physics.png) + +Used to configure various parameters of physics, please refer to the [Physics Config](physics-configs.md) documentation for details. + +## Scripting + +![scripting](./index/scripting.png) + +- **Conforming class fields**: if enabled, class fields are implemented using `Define` semantics; otherwise, class fields are implemented using `Set` semantics. + +- **Allows declaration of class fields**: if enabled, the `declare` keyword may be used to declare class fields in TypeScript scripts. In such case, class fields without explicit initializer would be initialized to `undefined`, according to the specification. + +- **Enable loose mode**: if enabled, the script will be compiled in non-strict mode. + +- **Import Map**: used to control the import behavior of TypeScript/JavaScript, see the [Import Map](../../scripting/modules/import-map.md) documentation for details. + +- **Browser list used for preview**: sets the browser list config file for TypeScript/JavaScript compilation at preview time. + > **Example:** + > For instance, when previewing, async/await uses polyfill by default. If we don't want to use polyfill, + > we can create a previewbrowserlist.txt file in the project root directory with the content chrome 80. + > Then go to Project Settings -> Scripts -> Browser List for Preview -> select the previewbrowserlist.txt file. + > Restart the editor or restart the script compilation process. The polyfill for async/await will no longer be applied. + +- **Exports Conditions**: specify the parsing conditions for conditionalized export modules, see [Conditional exports](../../scripting/modules/spec.md) for details. + +### Sorting Plugin Scripts + +A partial introduction to plugin scripts can be found in the document [Plugin Scripts](../.../scripting/external-scripts.md). + +![Sorting Plugin Scripts](index/sorting-plugin-script.png) + +When some of the plugin scripts have a certain prioritized loading order between them, you can add the sorting here. (Just add the plugin scripts that have a prioritized order) + +**How to operate**: + +- Click the `+` button to add plug-in scripts that are recognized but not added to the project. +- Click the `-` button to delete the selected plugin scripts. +- Click the `┇` button to expand the menu and select `Add All Plugin Scripts` or `Edit as JSON`. + + ![context menu](index/plugin-script-menu.png) + Editing in JSON is for those who want to edit the data directly, this is only a simple conversion, please check the data and format by yourself, illegal JSON format will not be submitted and saved. + + ![Edit in JSON](index/json-editor.png) + +## Model + +- **Start Material Conversion**: Please refer to [FBX Smart Material Conversion +](../../importer/materials/fbx-materials.md) for more details. + +## Joint Texture Layout + +![joint texture layout](./index/joint-texture-layout.png) + +Configure the joint texture layout within the project. Please refer to [Joint Texture Layout Settings](../../animation/joint-texture-layout.md)。 + +## Macro Config + +**Macro Config** provides a shortcut to modify the macro configuration. The configured macros will take effect on preview and build, and will also follow the custom engine configuration to update the default values of the current macro configuration. + +![macro](./index/macro.png) + +- **ENABLE_TILEDMAP_CULLING**: whether or not to enable TiledMap auto-culling. It's enabled by default. Note that if `skew` and `rotation` are set in the TiledMap, it's necessary to turn it off manually, otherwise it will cause rendering errors. + +- **TOUCH_TIMEOUT**: the duration of the delay used to screen a contact object to see if it has failed and can be removed. This value can be modified to get the desired effect, the default value is 5000 ms. Please refer to the API [TOUCH_TIMEOUT](%__APIDOC__%/en/interface/Macro?id=TOUCH_TIMEOUT) for details. + +- **ENABLE_TRANSPARENT_CANVAS**: used to set whether the Canvas background supports alpha channels. + + - If enabled, the Canvas background will be transparent and show the other DOM elements behind it. + - If disabled, it will result in higher performance. + +- **ENABLE_WEBGL_ANTIALIAS**: whether to activate WebGL antialias setting in the engine, it's enabled by default. This setting only affects WebGL graphics back-end, it indicates whether to turn on the anti-aliasing option when creating WebGL Context (for forward pipeline only). + +- **ENABLE_ANTIALIAS_FXAA**: used to enable FXAA anti-aliasing (for deferred pipeline only). + +- **ENABLE_BLOOM**: used to enable the BLOOM post-processing. + +- **CLEANUP_IMAGE_CACHE**: whether to clear the image cache after uploading a texture to GPU. If the cache is cleared, [Dynamic Atlas](../../advanced-topics/dynamic-atlas.md) will not be supported. It's disabled by default. + +- **ENABLE_MULTI_TOUCH**: whether to enable multi-touch. + +- **MAX_LABEL_CANVAS_POOL_SIZE**: set the maximum number of Canvas object pools used by the Label, and adjust it according to the number of Labels in the same scene of the project. + +- **ENABLE_WEBGL_HIGHP_STRUCT_VALUES** (new in v3.4.1): on the Android platform with WebGL backend, the internal variables of the structures defined in the fragment shader use **mediump** precision, which may lead to incorrect results for some data that requires high precision (such as position information). To avoid this problem, check this option and enable WebGL to calculate variables with **highp** precision. + + - If this option is enabled, use the `HIGHP_VALUE_STRUCT_DEFINE` macro in the fragment shader code to define structural variables, and use `HIGHP_VALUE_TO_STRUCT_DEFINED` and `HIGHP_VALUE_FROM_STRUCT_DEFINED` to assign values between structural and non-structural variables. + + - For specific information and code on the above macro calls, please refer to [packing.chunk](https://github.com/cocos/cocos4/blob/v4.0.0/editor/assets/chunks/common/data/packing.chunk#L40) for details. + +- **BATCHER2D_MEM_INCREMENT** (new in v3.4.1): this option affects the maximum number of vertices in each MeshBuffer, the default value is 144KB, please refer to the [MeshBuffer Batch Description](../../ui-system/components/engine/ui-batch.md) documentation for the conversion relationship between the number and the value. + +- **Custom Macro**: used to customize macro configurations, providing a macro tagging feature for current project scripts for easy visual configuration. Click the **+** button below to add a new macro configuration, hover over the added macro configuration and the **Delete** and **Modify** buttons will be displayed on the left side for deleting/renaming the current macro configuration respectively. + + ![macro](./index/custom-macro.png) + +For more specific information and code about the engine macro module, please refer to the [Engine Macro source](https://github.com/cocos/cocos4/blob/3d/cocos/core/platform/macro.ts#L824). + +## Feature Cropping + +The **Feature Cropping** tab is mainly for modules used in the engine when releasing the game, to achieve the effect of reducing the package size of the released version of the engine. The unchecked modules in the list will be cropped out when packaging and previewing. It is recommended to do a complete test after packaging to avoid scenes and scripts that use modules that have been cropped out. + +![feature-core](./index/feature-crop.png) + +## Texture Compression + +![compress-texture](./texture-compress/compress-texture.png) + +The preset configuration of texture compression is moved to **Project Settings**, and developers can select how the image asset is preset in the **Inspector** panel. The editor will automatically scan all the texture compression configurations in the project and sort out a few presets. Since it is automatically scanned, the preset names may not match the project and can be modified here by developers. + +### Compress Presets + +This panel is mainly used to add presets for texture compression. Each texture compression configuration allows to tailor the configuration for different platforms. Once added, developers can quickly add a texture compression preset in the **Inspector** panel by selecting the image asset in the **Hierarchy** panel. It is also possible to modify the preset directly in this panel to achieve the need to update the compression texture configuration in bulk. + +The following platforms are currently supported for configuring texture compressions: + +1. Web: both Web-Mobile and Web-Desktop platforms +2. iOS +3. Mini Game: including all mini game platforms currently supported by Creator, such as WeChat Mini Game, Huawei Quick Game, etc. +4. Android + +Please refer to the [Texture Compression](../../asset/compress-texture.md) documentation for details of texture compression on each platform. + +### Properties + +- **Mipmaps for compressed textures**: Pre-generated Mipmap for compressed mapping, options: **Allow Pre-generation (default)**; **Disabled** Disabled + - If the project is upgraded from a version before v3.7, this option is disabled by default + - Generate multiple images when turned on, it will affect the package size, please choose according to your needs + - If the user is using texture with generate mip maps checked, and needs to generate mipmaps with texture compression checked and some formats that need to be compressed by third-party tools such as pvr etc, you need to turn this option on in the project settings. Once enabled, the build will generate compressed mipmaps of the corresponding format for use at runtime + +#### Adding/Removing Texture Compression Presets + +Enter a texture compression preset name in the upper input box and click Enter or the "+" button on the right to add a preset. The other two buttons are for importing/exporting texture compression presets, please refer to the description below for details. + +![add](./texture-compress/add.png) + +After adding a compressed texture, it can be deleted by hovering over the preset name and clicking the delete button on the right. + +![delete](./texture-compress/delete.png) + +> **Note**: the two presets **default** and **transparent** built into the panel cannot be modified/deleted. + +#### Adding/Deleting Texture Compression Format + +Select the platform, then click the **Add Format** button, select the desired texture format, and then configure the corresponding quality level. Currently the same type of image format can only be added once. + +![add-format](./texture-compress/add-format.png) + +To delete, move the mouse over the texture format and click the red delete button on the right. + +![delete-format](./texture-compress/delete-format.png) + +#### Modifying the Texture Compression Preset Name + +The name of the texture compression preset is for display purposes only. When a texture compression preset is added, a random UUID is generated as the ID of the preset, so changing the preset name directly does not affect the reference to the preset at the image asset. + +![edit](./texture-compress/edit.png) + +#### Exporting/Importing Texture Compression Presets + +The texture compression configuration page allows importing/exporting texture compression presets for better cross-project reuse of the configuration, or edit the texture compression presets externally and import them to the editor. + +To write a custom texture compression configuration, please refer to the following interface definitions and examples: + +**The interface is defined as follows**: + +```ts +type IConfigGroups = Record; +type ITextureCompressPlatform = 'miniGame' | 'web' | 'ios' | 'android' | 'pc'; +type ITextureCompressType = + | 'jpg' + | 'png' + | 'webp' + | 'pvrtc_4bits_rgb' + | 'pvrtc_4bits_rgba' + | 'pvrtc_4bits_rgb_a' + | 'pvrtc_2bits_rgb' + | 'pvrtc_2bits_rgba' + | 'pvrtc_2bits_rgb_a' + | 'etc1_rgb' + | 'etc1_rgb_a' + | 'etc2_rgb' + | 'etc2_rgba' + | 'astc_4x4' + | 'astc_5x5' + | 'astc_6x6' + | 'astc_8x8' + | 'astc_10x5' + | 'astc_10x10' + | 'astc_12x12'; +type IConfigGroupsInfo = Record +interface ICompressPresetItem { + name: string; + options: IConfigGroups; +} +``` + +**Examples**: + +```json +{ + "default": { + "name": "default", + "options": { + "miniGame": { + "etc1_rgb": "fast", + "pvrtc_4bits_rgb": "fast" + }, + "android": { + "astc_8x8": "-medium", + "etc1_rgb": "fast" + }, + "ios": { + "astc_8x8": "-medium", + "pvrtc_4bits_rgb": "fast" + }, + "web": { + "astc_8x8": "-medium", + "etc1_rgb": "fast", + "pvrtc_4bits_rgb": "fast" + }, + } + }, + "transparent": { + "name": "transparent", + "options": { + "miniGame": { + "etc1_rgb_a": "fast", + "pvrtc_4bits_rgb_a": "fast" + }, + "android": { + "astc_8x8": "-medium", + "etc1_rgb_a": "fast" + }, + "ios": { + "astc_8x8": "-medium", + "pvrtc_4bits_rgb_a": "fast" + }, + "web": { + "astc_8x8": "-medium", + "etc1_rgb_a": "fast", + "pvrtc_4bits_rgb_a": "fast" + }, + } + } +} +``` + +### Custom Compress Format + +Since v3.5.0, custom texture compression pages have been supported in order to make it easy for users to customize the texture compression tool and some custom parameters, encryption, etc. Interface interaction is similar to texture compression presets. + +![custom-compress](./texture-compress/custom-compress.png) + +### Configuration + +- **Compress format**: Config the processing format. After selecting a different format, quality options will keep the same type. If the customized compression mode fails, the system will automatically revert to the original compression scheme of the editor. + + ![overwrite-format](./texture-compress/custom-format.png) + +- **Overwrite original format**: If this option is selected, the original compression format in the existing texture compression preset will be automatically overwritten. The name of the custom compression mode will appear next to the original configuration name. One format can only be overwritten by one custom compression mode. + + ![overwrite-format](./texture-compress/overwrite-format.png) + +- **Compression tool**: + - **program**: compress tool path. + - **Command line parameters**: Sets the parameters to be passed by the calling program. The names of the parameters to be passed by default to build texture compression can be added quickly through the control on the right of the parameter input box. Currently, the 'src, dest, quality' fields are passed by default. + +#### Custom Compress Format In Build + +After the custom texture format is configured, the word `custom-compress` and the command line parameter information will be printed to console when build the project. + +![custom-compress-log](./texture-compress/custom-compress-log.png) + +## Splash Setting + +The splash screen setting is a feature that displays the engine logo or a developer-defined logo at the start of the game. + +The splash screen will only be shown after the release, not during the preview. + +![splash](./index/splash.png) + +- **TotalTime**:Total duration of splash screen(ms) +- **Image Size Multiplier**: The size multiplier of the splash image +- **Preview**: If you want to change the Splash Image, Hover the mouse on the preview window, then click the '+' button, then select a new image path. + +If you want to disable the splash screen, please refer to [Build Options](../publish/build-options.md) for details. + +> **Notes**. +> 1. After selecting different build platforms in the build options, adjust the splash screen rules again to achieve the diversity of splash screen on different platforms +> 2. the splash screen will only take effect after packaging, not when previewing +> 3. some countries and regions are not open for full splash screen, we apologize for any inconvenience caused to you. + +## Bundle Config + +![bundle-config.png](index/bundle-config.png) + +Bundle Config allows users can config different configurations for various platforms. For example, to distinguish normal textures and high-resolution textures, or for different shops, etc. For more, please refer to [Asset Bundle](../../asset/bundle.md). + +Currently, three main types of platforms are supported by Cocos Creator including the native platform, web, and mini-games. + +You can select the target platform by clicking on the platform buttons. + +![bundle-platforms.png](./index/bundle-platforms.png) + +Use the menus on the top right of the panel to create a new, import, or export config. + +![bundle-menus.png](./index/bundle-menus.png) + +- **New Config**: Create a new config. + + ![bundle-change-type.png](./index/bundle-change-type.png) + + After a config is created, you can select it by clicking on the drop-down box beside **Platform Settings** on the **Inspector** panel when a bundle is selected. + + > Different bundles can use different configs. + +- **Import Config**:To import a JSON file as the bundle config. +- **Export Config**:Export the config to reuse it in other projects. + +For the created config, you can **Copy Config**, **Copy ID**, **Rename** or **Delete** the config by clicking the menu button. + +![bundle-copy.png](./index/bundle-copy.png) + +### Platform Override + +Currently, Cocos Creator supports multiple platforms for publishing. When publishing on each platform, you can choose the option to add a new configuration to override the currently configured platform. + +Click the "+" sign on the right side of **Platform Override** to select the target platform. + +![bundle-override.png](./index/bundle-override.png) + +After selecting new platforms as needed, the configuration of these new platforms will override the previous generic configuration. + +This operation allows developers to optimize for certain platforms. + +## Extending the Project Settings Panel + +Creator supports adding custom feature pages on the right side of **Project Settings**, please refer to the [Extending the Project Settings Panel](../../editor/extension/contributions-project.md) documentation. diff --git a/versions/4.0/en/editor/project/index/bundle-change-type.png b/versions/4.0/en/editor/project/index/bundle-change-type.png new file mode 100644 index 0000000000..af0a09072e Binary files /dev/null and b/versions/4.0/en/editor/project/index/bundle-change-type.png differ diff --git a/versions/4.0/en/editor/project/index/bundle-config.png b/versions/4.0/en/editor/project/index/bundle-config.png new file mode 100644 index 0000000000..7e2d9e0377 Binary files /dev/null and b/versions/4.0/en/editor/project/index/bundle-config.png differ diff --git a/versions/4.0/en/editor/project/index/bundle-copy.png b/versions/4.0/en/editor/project/index/bundle-copy.png new file mode 100644 index 0000000000..5ae6af4067 Binary files /dev/null and b/versions/4.0/en/editor/project/index/bundle-copy.png differ diff --git a/versions/4.0/en/editor/project/index/bundle-menus.png b/versions/4.0/en/editor/project/index/bundle-menus.png new file mode 100644 index 0000000000..c357c129b3 Binary files /dev/null and b/versions/4.0/en/editor/project/index/bundle-menus.png differ diff --git a/versions/4.0/en/editor/project/index/bundle-override.png b/versions/4.0/en/editor/project/index/bundle-override.png new file mode 100644 index 0000000000..b43a417de7 Binary files /dev/null and b/versions/4.0/en/editor/project/index/bundle-override.png differ diff --git a/versions/4.0/en/editor/project/index/bundle-platforms.png b/versions/4.0/en/editor/project/index/bundle-platforms.png new file mode 100644 index 0000000000..e1b9baad94 Binary files /dev/null and b/versions/4.0/en/editor/project/index/bundle-platforms.png differ diff --git a/versions/4.0/en/editor/project/index/custom-macro.png b/versions/4.0/en/editor/project/index/custom-macro.png new file mode 100644 index 0000000000..fc70d145a4 Binary files /dev/null and b/versions/4.0/en/editor/project/index/custom-macro.png differ diff --git a/versions/4.0/en/editor/project/index/drag-to-sort.gif b/versions/4.0/en/editor/project/index/drag-to-sort.gif new file mode 100644 index 0000000000..2171cac39c Binary files /dev/null and b/versions/4.0/en/editor/project/index/drag-to-sort.gif differ diff --git a/versions/4.0/en/editor/project/index/feature-crop.png b/versions/4.0/en/editor/project/index/feature-crop.png new file mode 100644 index 0000000000..2e10729f25 Binary files /dev/null and b/versions/4.0/en/editor/project/index/feature-crop.png differ diff --git a/versions/4.0/en/editor/project/index/joint-texture-layout.png b/versions/4.0/en/editor/project/index/joint-texture-layout.png new file mode 100644 index 0000000000..14487d243a Binary files /dev/null and b/versions/4.0/en/editor/project/index/joint-texture-layout.png differ diff --git a/versions/4.0/en/editor/project/index/json-editor.png b/versions/4.0/en/editor/project/index/json-editor.png new file mode 100644 index 0000000000..fe55c435bc Binary files /dev/null and b/versions/4.0/en/editor/project/index/json-editor.png differ diff --git a/versions/4.0/en/editor/project/index/layers-camera.png b/versions/4.0/en/editor/project/index/layers-camera.png new file mode 100644 index 0000000000..53b43216c6 Binary files /dev/null and b/versions/4.0/en/editor/project/index/layers-camera.png differ diff --git a/versions/4.0/en/editor/project/index/layers-node.png b/versions/4.0/en/editor/project/index/layers-node.png new file mode 100644 index 0000000000..f74809a8b4 Binary files /dev/null and b/versions/4.0/en/editor/project/index/layers-node.png differ diff --git a/versions/4.0/en/editor/project/index/layers.png b/versions/4.0/en/editor/project/index/layers.png new file mode 100644 index 0000000000..0f2791b44d Binary files /dev/null and b/versions/4.0/en/editor/project/index/layers.png differ diff --git a/versions/4.0/en/editor/project/index/macro.png b/versions/4.0/en/editor/project/index/macro.png new file mode 100644 index 0000000000..38d7e70b55 Binary files /dev/null and b/versions/4.0/en/editor/project/index/macro.png differ diff --git a/versions/4.0/en/editor/project/index/model.png b/versions/4.0/en/editor/project/index/model.png new file mode 100644 index 0000000000..2d81a07a4b Binary files /dev/null and b/versions/4.0/en/editor/project/index/model.png differ diff --git a/versions/4.0/en/editor/project/index/path-protocol.png b/versions/4.0/en/editor/project/index/path-protocol.png new file mode 100644 index 0000000000..f8b36d4e78 Binary files /dev/null and b/versions/4.0/en/editor/project/index/path-protocol.png differ diff --git a/versions/4.0/en/editor/project/index/physics-collision-demo.png b/versions/4.0/en/editor/project/index/physics-collision-demo.png new file mode 100644 index 0000000000..c8c9e93945 Binary files /dev/null and b/versions/4.0/en/editor/project/index/physics-collision-demo.png differ diff --git a/versions/4.0/en/editor/project/index/physics-collision.png b/versions/4.0/en/editor/project/index/physics-collision.png new file mode 100644 index 0000000000..8a8e7896ec Binary files /dev/null and b/versions/4.0/en/editor/project/index/physics-collision.png differ diff --git a/versions/4.0/en/editor/project/index/physics-in-engine.png b/versions/4.0/en/editor/project/index/physics-in-engine.png new file mode 100644 index 0000000000..cb9ff8c3a3 Binary files /dev/null and b/versions/4.0/en/editor/project/index/physics-in-engine.png differ diff --git a/versions/4.0/en/editor/project/index/physics-index.png b/versions/4.0/en/editor/project/index/physics-index.png new file mode 100644 index 0000000000..e895e92815 Binary files /dev/null and b/versions/4.0/en/editor/project/index/physics-index.png differ diff --git a/versions/4.0/en/editor/project/index/physics.png b/versions/4.0/en/editor/project/index/physics.png new file mode 100644 index 0000000000..c1219e2a71 Binary files /dev/null and b/versions/4.0/en/editor/project/index/physics.png differ diff --git a/versions/4.0/en/editor/project/index/plugin-script-menu.png b/versions/4.0/en/editor/project/index/plugin-script-menu.png new file mode 100644 index 0000000000..c7fcfce9d5 Binary files /dev/null and b/versions/4.0/en/editor/project/index/plugin-script-menu.png differ diff --git a/versions/4.0/en/editor/project/index/project-data.png b/versions/4.0/en/editor/project/index/project-data.png new file mode 100644 index 0000000000..c6f0ef3415 Binary files /dev/null and b/versions/4.0/en/editor/project/index/project-data.png differ diff --git a/versions/4.0/en/editor/project/index/rigidbody-group.jpg b/versions/4.0/en/editor/project/index/rigidbody-group.jpg new file mode 100644 index 0000000000..9601623a75 Binary files /dev/null and b/versions/4.0/en/editor/project/index/rigidbody-group.jpg differ diff --git a/versions/4.0/en/editor/project/index/scripting.png b/versions/4.0/en/editor/project/index/scripting.png new file mode 100644 index 0000000000..3c49b05416 Binary files /dev/null and b/versions/4.0/en/editor/project/index/scripting.png differ diff --git a/versions/4.0/en/editor/project/index/sorting-layer.png b/versions/4.0/en/editor/project/index/sorting-layer.png new file mode 100644 index 0000000000..3dc77c949d Binary files /dev/null and b/versions/4.0/en/editor/project/index/sorting-layer.png differ diff --git a/versions/4.0/en/editor/project/index/sorting-plugin-script.png b/versions/4.0/en/editor/project/index/sorting-plugin-script.png new file mode 100644 index 0000000000..24fc1b265e Binary files /dev/null and b/versions/4.0/en/editor/project/index/sorting-plugin-script.png differ diff --git a/versions/4.0/en/editor/project/index/sorting.png b/versions/4.0/en/editor/project/index/sorting.png new file mode 100644 index 0000000000..82e81660c9 Binary files /dev/null and b/versions/4.0/en/editor/project/index/sorting.png differ diff --git a/versions/4.0/en/editor/project/index/splash.png b/versions/4.0/en/editor/project/index/splash.png new file mode 100644 index 0000000000..6f1f560e3f Binary files /dev/null and b/versions/4.0/en/editor/project/index/splash.png differ diff --git a/versions/4.0/en/editor/project/physics-configs.md b/versions/4.0/en/editor/project/physics-configs.md new file mode 100644 index 0000000000..de57247cba --- /dev/null +++ b/versions/4.0/en/editor/project/physics-configs.md @@ -0,0 +1,59 @@ +# Physics Configs + +Physical configuration is used to configure various commonly used properties. + +## Property description + +![Physics](./index/physics-index.png) + +- `gravity` Gravity direction vector, the sign means the positive or negative direction on the axis. **Default**: `{ x: 0, y: -10, z: 0 }`. +- `allowSleep` Whether to allow rigid bodies to enter sleep state. **Default**: `true`. +- `sleepThreshold` The maximum speed threshold for entering sleep. **Default**: `0.1`. **Min**: `0`. +- `autoSimulation` Whether to enable automatic simulation. +- `fixedTimeStep` Fixed time step between each simulation. **Default**: `1/60`. **Min**: `0`. +- `maxSubSteps` Maximum number of substeps per simulation step. **Default**: `1`. **Min**: `0`. +- `friction` Coefficient of friction. **Default**: `0.5`. +- `rollingFriction` Rolling friction coefficient. **Default**: `0.1`. +- `spinningFriction` Spin friction coefficient. **Default**: `0.1`. +- `restitution` Coefficient of elasticity. **Default**: `0.1`. +- `collisionMatrix` The collision matrix, used only for initialization. + + +## Collision Matrix + +The collision matrix is used to initialize groups and masks of physical elements. + +![Physics-collision](./index/physics-collision.png) + +### Grouping concept + +In the editor, the grouping format of the collision matrix is __{index, name}__, __index__ is the number of bits from __0__ to __31__, and __name__ is the name of the group. The new project will have a default grouping: __{index: 0, name: 'DEFAULT'}__. + +By clicking the __+__ button you can add a new group. + +> **Note**: both __index__ and __name__ cannot be empty and cannot be repeated with existing items; after adding, the group cannot be deleted, only the name of the group can be modified. + +### How to configure + +Take a new __water__ group as an example: + +![Physics-collision-demo](./index/physics-collision-demo.png) + +This table lists all the groups, and you can check it to determine which two groups will do the collision detection. + +**As shown in the figure above, whether `DEFAULT` and `water` will perform collision detection will be determined by whether the corresponding check box is checked**. + +According to the above rules, the collision pairs generated in this table are: + +- DEFAULT - water +- DEFAULT - DEFAULT + +And the grouping pairs that do not perform collision detection include: + +- water - water + +### Configure groups of physical components + +In addition, the __Group__ property on the __RigidBody__ component needs to be configured into the corresponding physical element: + +![rigidbody-group](./index/rigidbody-group.jpg) diff --git a/versions/4.0/en/editor/project/texture-compress/add-format.png b/versions/4.0/en/editor/project/texture-compress/add-format.png new file mode 100644 index 0000000000..7595797eff Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/add-format.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/add.png b/versions/4.0/en/editor/project/texture-compress/add.png new file mode 100644 index 0000000000..aa1ecf7ef2 Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/add.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/compress-presets.png b/versions/4.0/en/editor/project/texture-compress/compress-presets.png new file mode 100644 index 0000000000..76880dbcec Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/compress-presets.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/compress-texture.png b/versions/4.0/en/editor/project/texture-compress/compress-texture.png new file mode 100644 index 0000000000..ba2d602dfb Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/compress-texture.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/custom-compress-log.png b/versions/4.0/en/editor/project/texture-compress/custom-compress-log.png new file mode 100644 index 0000000000..c78a629c6a Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/custom-compress-log.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/custom-compress-options.png b/versions/4.0/en/editor/project/texture-compress/custom-compress-options.png new file mode 100644 index 0000000000..aa02fe0dfe Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/custom-compress-options.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/custom-compress.png b/versions/4.0/en/editor/project/texture-compress/custom-compress.png new file mode 100644 index 0000000000..c1752bb5e2 Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/custom-compress.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/custom-format.png b/versions/4.0/en/editor/project/texture-compress/custom-format.png new file mode 100644 index 0000000000..27343e7289 Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/custom-format.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/delete-format.png b/versions/4.0/en/editor/project/texture-compress/delete-format.png new file mode 100644 index 0000000000..fb2874a840 Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/delete-format.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/delete.png b/versions/4.0/en/editor/project/texture-compress/delete.png new file mode 100644 index 0000000000..debfc3a9f3 Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/delete.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/edit.png b/versions/4.0/en/editor/project/texture-compress/edit.png new file mode 100644 index 0000000000..cf1ad8e67f Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/edit.png differ diff --git a/versions/4.0/en/editor/project/texture-compress/overwrite-format.png b/versions/4.0/en/editor/project/texture-compress/overwrite-format.png new file mode 100644 index 0000000000..315e823aca Binary files /dev/null and b/versions/4.0/en/editor/project/texture-compress/overwrite-format.png differ diff --git a/versions/4.0/en/editor/publish/android/build-example-android.md b/versions/4.0/en/editor/publish/android/build-example-android.md new file mode 100644 index 0000000000..afdc103041 --- /dev/null +++ b/versions/4.0/en/editor/publish/android/build-example-android.md @@ -0,0 +1,168 @@ +# Android Publish Example + +In this section, we will introduce how to build Cocos Creator project with Android Studio. + +Please prepare a Cocos Creator project with at least one scene. + +![project.png](images/project.png) + +## Set Android Development Environment + +To publish an Android native application, you need to install the Android Studio development environment, as well as specific versions of JDK (or OpenSDK), Android SDK, NDK, and more. For more details, please refer to the [Android Native Development Environment Setup](../setup-native-development.md). + +## Publish Process + +The next step is to create a new build task and publish an Android APK on the **Build** panel in Cocos Creator. + +### Create Build Task + +1. By clicking the **Project** -> **Build** menu to open **Build** panel. + + ![cc-build-menu.png](images/cc-build-menu.png) + +2. At the top of **Build** panel, click on the **New Build Task** button + + ![new-build-task.png](images/new-build-task.png)P + +3. To select **Android** as the build target platform, click on the drop-down menu + + ![select-platform.png](images/select-platform.png) + +4. Make sure to have at least one scene designated as the Start Scene. If you have only one scene, it will be added by default + + ![start-scene.png](images/start-scene.png) + +5. Please refer to [Publishing to Native Platforms - Render BackEnd](../native-options.md#Render%20BackEnd) to select an appropriate render backend + + ![render-backend.png](images/render-backend.png) + +6. Enter the Game Package Name + + ![game-package-name.png](images/game-package-name.png) + + > Please refer to [Bundle Identifier](../native-options.md#Bundle%20Identifier) for guidance your app's bundle identifier + +7. Select Target API Level + + ![target-api-level.png](images/target-api-level.png) + + > In case no dropdown content, please check the SDK and NDK configuration in Cocos Creator + +Other build options please refer to [Publishing to Native Platforms](../native-options.md) + +### Build and Publish + +1. To begin a build process, click on the **Build** button for the selected build task. + + ![build.png](images/build.png) + +2. Please wait until the build process finished + + ![building.png](images/building.png) + +3. Click on the button below to open the folder containing the exported Android Project + + ![open](images/open.png) + +4. Locate the corresponding directory + + ![find-proj](images/find-proj.png) + +5. Click on the open menu on android studio + + ![android studio open project menu](images/as-open-menu.png) + +6. Navigate to the project path in the `{Your project}/build` directory, which is named 'android' by default, and open it using Android Studio. + + ![android studio open project](images/as-open-proj.png) + +7. Generate APK in Android Studio + + After the preparation has been completed by Android Studio, you can start building an Android APK. However, the sync process may take a long time to complete. If this happens, you can stop any ongoing background tasks by following the steps below: + + Click on the background task on the status bar: + ![background-task.png](./images/background-task.png)
+ + Close all background tasks by clicking on the close button + ![interrupt-sync.png](images/interrupt-sync.png) + +8. Click on **Build Bundle(s) / APK(s)** in the **Build** menu + + ![build-apk.png](images/build-apk.png) + +9. The debug APK could be found in the `{proj/build}` directory + + ![apk.png](images/apk.png) + +## Keystore Generation and Usage + +For the release of the final version, the debug key cannot be used. You need to create your own key. + +### Creating a Key + +You can generate a key using Android Studio: + +1. In Android Studio, click on the "Build" menu and select "Generate Signed Bundle / APK": +![gen-sign-apk.png](images/gen-sign-apk.png) + +2. In the pop-up window, select "APK" and click "Next": +![gen-sign-apk.png](images/gen-sign-apk.png) + +3. In the guided window, click on "Create new": +![create-sign.png](images/create-sign.png) + +4. Fill in the required information in the pop-up window: +![android-keystore-panel.png](images/android-keystore-panel.png) + +> It is recommended to use different keys for different projects. You can store each project's key in the project's root directory. + +### Using the Key + +After successfully creating the key, a key file will be generated in the selected directory. You can fill in the key information in the Android build panel. This way, both the debug and release versions will use the key you created. + +As shown in the following image, uncheck the "Use debug keystore" option and select your custom key file from the "Keystore Path" below. Fill in the relevant information: +![debug-keystore.png](images/debug-keystore.png) + +### Considerations + +1. It is recommended to create a dedicated key for each project from the first release. +2. Some SDK services require APK key signature verification. Using the default debug key may result in service call failures. + +## Advanced + +### Java and TypeScript Communication + +The engine provides various methods to solve the communication issues between TypeScript and the native layer. + +When integrating common SDKs, we often need to perform login operations through the SDK's login interface, and then pass the results to the TypeScript layer for further processing in the game. + +The engine provides three methods for communication between TypeScript and Android native layer. + +- [JavaScript and Java Communication using JsbBridge](../../../advanced-topics/js-java-bridge.md): This method is very convenient for integrating SDKs and handling actions such as registration, login, and displaying ads. It can quickly solve these types of problems. + +- [Tutorial: JSB 2.0](../../../advanced-topics/JSB2.0-learning.md): This method is recommended for frequent C++ API calls or batch exporting of C++ APIs.. + +- [JavaScript and Android Communication with Reflection](../../../advanced-topics/java-reflection.md): This method is highly effective for non-frequent calls. + +### Importing Third-Party Libraries + +To publish your application to the app market, you usually need to integrate certain third-party SDKs. These SDKs are typically provided in JAR or AAR format. You can refer to [Using Your Library in Other Projects](https://developer.android.com/studio/projects/android-library?hl=zh-cn#psd-add-library-dependency) to import the local library into your project. + +### Extending Build Process + +Please refer to [Extending Build Process](../custom-build-plugin.md) to extend the publishing mechanism by the extension system. + +## Q&A + +- Q: How to debug publishing errors + - A: You can debug publishing errors by opening the log.txt file using the log button + + ![show-log.png](images/show-log.png) + +- Q: Missing LIB_EGL + - A: Change your NDK to the version mentioned above. + +- Q: Asset name mechanism + - A: Since Android is based on Linux, some file specifications of Linux still apply in Android, such as + - 1. Android is case-sensitive, make sure the path is case-sensitive, otherwise, it will not load correctly + - 2. Do not include spaces in the folder or file name diff --git a/versions/4.0/en/editor/publish/android/build-options-android.md b/versions/4.0/en/editor/publish/android/build-options-android.md new file mode 100644 index 0000000000..7186eb9d21 --- /dev/null +++ b/versions/4.0/en/editor/publish/android/build-options-android.md @@ -0,0 +1,125 @@ +# Build Options - Android + +The build options for the Android platform are as follows: + +![build-options-android.png](./images/build-options-android.png) + +## Enable Swappy + +**Enable Swappy** is used to decide whether to enable the integrated Swappy feature in the engine. Currently supported for GLES and Vulkan. + +This option actively adjusts the rendering time to match the screen refresh rate, achieving stable frame rates and reducing unnecessary rendering. The build parameter is `swappy`. + +For more information, refer to the official documentation on [Frame Pacing Library Overview](https://source.android.com/devices/graphics/frame-pacing). + +## Render Backend + +Currently supported options are as follows: + +- [VULKAN](https://www.vulkan.org/) +- [GLES3](https://www.khronos.org/registry/OpenGL-Refpages/es3/) +- [GLES2](https://www.khronos.org/registry/OpenGL-Refpages/es2.0/) + +At least one option must be selected, with **GLES3** being the default. + +If GLES 2/3 is selected, GLES3 must be checked by default and it is not allowed to select GLES2 alone. + +When multiple options are selected, the runtime will choose the rendering backend based on the device's actual support. + +## Game Package Name + +The Game Package Name usually follows the reverse order of the product's website URL, such as `com.mycompany.myproduct`. + +> **Note**: The package name can only contain numbers, letters, and underscores. Additionally, the last part of the package name must start with a letter and cannot start with an underscore or a number. + +## Target API Level + +Set the Target API Level required for Android platform compilation. Clicking the Set Android SDK button next to it will quickly navigate to the configuration page. For specific configuration rules, refer to [Android Development Environment Setup](../setup-native-development.md). + +## APP ABI + +Set the CPU types that your Android app needs to support. You can choose one or more options, including `armeabi-v7a`, `arm64-v8a`, `x86`, and `x86_64`. + +**Notes**: + +1. When you select one ABI and build it without cleaning, both ABIs' shared objects (so) will be packaged into the APK. This is the default behavior of Android Studio. If you import the project into Android Studio, after building one ABI, perform **Build -> Clean Project** before building another ABI. This way, only the latter ABI will be packaged into the APK. + +2. After importing the project into Android Studio, it exists independently and is not dependent on the build and release panel. If you need to modify the ABI, directly modify the **PROP_APP_ABI** property in the `gradle.properties` file. +![modify abi](../publish-native/modify_abi.png) + +## Use Debug Keystore + +Android requires that all APKs be digitally signed with a certificate before they can be installed. Cocos Creator provides a default keystore, and checking **Use Debug Keystore** means using the default keystore. If developers need a custom keystore, uncheck **Use Debug Keystore**. For more details, refer to the [Android Developer - App Signing](https://developer.android.google.cn/studio/publish/app-signing). + +## Orientation + +The screen orientation currently includes three options: **Portrait**, **Landscape Left**, and **Landscape Right**. + +- **Portrait**: The screen is in an upright position with the Home button at the bottom. +- **Landscape Left**: The screen is in a landscape position with the Home button on the left side of the screen. +- **Landscape Right**: The screen is in a landscape position with the Home button on the right side of the screen. + +## Google Play Instant + +Checking this option allows you to package and publish your game to Google Play Instant. Google Play Instant is dependent on Google Play and is not a separate distribution channel but rather a game streaming solution. It enables players to try, share, and engage with your game instantly without the need to install it. + +**Note the following points when using Google Play Instant:** + +1. Android Studio version should be 4.0 or above. +2. Android phones running Android 6.0 or above. Devices with Android SDK versions between 6.0 and 7.0 require the installation of Google Play Services Framework, while devices with SDK versions 8.0 or above do not require it and can use the services directly. +3. For the first build, you need to open the built project with Android Studio to download the Google Play Instant Development SDK (Windows) or Instant Apps Development SDK (Mac) support package. If the download fails, it is recommended to set up an HTTP proxy for Android Studio. +![Google Play Instant](../publish-native/sdk-android-instant.png) + +## Generate App Bundle(Google Play) + +Checking this option allows you to package your game as an App Bundle format for uploading to the Google Play Store. For more information, refer to the [Android Developer - App Bundle](https://developer.android.google.cn/guide/app-bundle/). + +## Other Options + +Some SDK and NDK options need to be set in the preferences. In addition to setting them in the preferences, you can also pass specified parameters through the build command line. + +**sdkPath**: Specify the SDK path +**ndkPath**: Specify the NDK path + +You can export the build configuration and add the corresponding parameters in the Android options. + +## Build Parameter Interface Definition (used to modify parameters during command line builds) + +```ts +interface IOptions { + packageName: string; + resizeableActivity: boolean; + maxAspectRatio: string; + orientation: { + landscapeRight: boolean; + landscapeLeft: boolean; + portrait: boolean; + upsideDown: boolean; + }, + + apiLevel: number; + appABIs: IAppABI[]; + + useDebugKeystore: boolean; + keystorePath: string; + keystorePassword: string; + keystoreAlias: string; + keystoreAliasPassword: string; + + appBundle: boolean; + androidInstant: boolean; + inputSDK: boolean; + remoteUrl: string; + sdkPath: string; + ndkPath: string; + javaHome?: string; + javaPath?: string; + + swappy: boolean; + + renderBackEnd: { + vulkan: boolean; + gles3: boolean; + gles2: boolean; + } +} diff --git a/versions/4.0/en/editor/publish/android/build-setup-evn-android.md b/versions/4.0/en/editor/publish/android/build-setup-evn-android.md new file mode 100644 index 0000000000..b21d083a70 --- /dev/null +++ b/versions/4.0/en/editor/publish/android/build-setup-evn-android.md @@ -0,0 +1,106 @@ +# Android Development Environment Setup + +## Download Android Studio + +Developers should download the appropriate IDE from the [official Android Studio](https://developer.android.com/studio). + +Reference Setting up the [Native Development Environment](../setup-native-development.md#Android%20Platform%20Dependencies) to set up the development environment + +## Download and install JDK + +Reference [Native Development Environment](../setup-native-development.md#Downloading%20the%20Java%20SDK (JDK)) + +Type `java -version` in the terminal to verify if the installation is successful. + +![terminal.png](images/terminal.png) + +> Please refer to [How do I set or change the PATH system variable?](https://www.java.com/en/download/help/path.html) to set up your environment
+> +> **Note**:The author utilizes [OpenJDK](https://openjdk.org/), which has similar features as the JDK but follows another open-source protocol. + +Download and install the Android SDK + +### Auto Download + +Here is an example of how to download Android SDK through Android Studio + +1. Start Android Studio + +2. Open the Settings/Preferences panel from the main menu + ![as-setting.png](images/as-setting.png) + + > The name of the setting panel differs on the platform. It is called **Settings** on Windows but **Preferences** on MacOS. + +3. Navigate to the Android SDK page in the Settings/Preferences panel. + ![download-sdk.png](images/download-sdk.png) + +4. Select at least one suitable SDK to download. You can choose the recommended version mentioned in [Downloading the SDK and NDK required to publish to the Android platform](../setup-native-development.md#Downloading%20the20SDK20and20NDK20required20to20publish20to20the%20Android%20platform). + + - As an example, we will be using Android 11.0 (R). Check the box next to the name and then click either the OK or Apply button to proceed + + - Click the OK button in the pop-up dialog + ![confirm-change.png](images/confirm-change.png) + + - Please wait until the download is finished + ![component-installer.png](images/component-installer.png) + +### Manually downloading the Android SDK + +In case of network connectivity issues, you can manually download the Android SDK and put it in the directory configuring in **Android SDK Location**. + +You could click the **Edit** button to choose a different path. + +![sdk-location.png](images/sdk-location.png) + +You can change to use SDK mirrors(Optional) by setting HTTP Proxy in Android Studio. +**Auto-detect proxy settings:** In the Settings panel of Android Studio, navigate to the HTTP Proxy and select the **Auto-detect proxy settings** option, and then input the mirror links below. + +![http-proxy.png](images/http-proxy.png) + +**Choose specific mirrors:** Here are some alternative mirrors for the Android SDK and Gradle that can help developers resolve download issues when downloading failed from the official website. However, if these mirrors do not work, you may need to search for other mirrors. + +| Mirror | Address | +| :------- | :----------------------------------------------------- | +| Tencent(CN) | | +| AliCloud(CN) | | + +> In case these mirrors are not working, please search for other valid mirrors. + +## Download And Install Android NDK + +Please refer to [Downloading the SDK and NDK required to publish to the Android platform](../setup-native-development.md#Downloading%20the%20SDK%20and%20NDK%20required%20to%20publish%20to%20the%20Android%20platform) to download the appropriate version of Android NDK(**r18~21**). + +### Download the Android NDK from Android Studio + +Navigate to the **Android SDK** page in Android Studio **Settings** panel. + +> **Note**: check **Show Package Details** +> ![show-package-details.png](images/show-package-details.png) + +![download-ndk-by-as.png](images/download-ndk-by-as.png) + +Make sure to select the appropriate version to download. If the list fails to refresh, you can follow the steps in the next section to manually download the Android NDK. + +### Manually download + +You could find the historical versions of Android NDK [here](https://github.com/android/ndk/wiki/Unsupported-Downloads#r20b) + +For instance, let's consider the version r20b, which can be found in the link provided above. Make sure to select the appropriate version according to your operating system, and extract it to the directory previously set as the Android SDK Location. + +![download-ndk.png](images/download-ndk.png) + +## Configure Android SDK and NDK in Cocos Creator + +Navigate to the **Preferences** panel in Cocos Creator by clicking the **File** -> **Preferences** menu. From there go to the **Program Manager** page and configure the **Android SDK** and **Android NDK** properties with the following images. + +![program.png](images/program.png) + +The path should be like follows: + +**NDK**: + +![ndk-dir.png](images/ndk-dir.png) + +**SDK**: + +![sdk-dir.png](images/sdk-dir.png) diff --git a/versions/4.0/en/editor/publish/android/images/accept.png b/versions/4.0/en/editor/publish/android/images/accept.png new file mode 100644 index 0000000000..d11260cc42 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/accept.png differ diff --git a/versions/4.0/en/editor/publish/android/images/android-keystore-panel.png b/versions/4.0/en/editor/publish/android/images/android-keystore-panel.png new file mode 100644 index 0000000000..76f1b51ac9 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/android-keystore-panel.png differ diff --git a/versions/4.0/en/editor/publish/android/images/android-sdk-preference.png b/versions/4.0/en/editor/publish/android/images/android-sdk-preference.png new file mode 100644 index 0000000000..838577f99f Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/android-sdk-preference.png differ diff --git a/versions/4.0/en/editor/publish/android/images/apk.png b/versions/4.0/en/editor/publish/android/images/apk.png new file mode 100644 index 0000000000..349353c1c0 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/apk.png differ diff --git a/versions/4.0/en/editor/publish/android/images/as-open-menu.png b/versions/4.0/en/editor/publish/android/images/as-open-menu.png new file mode 100644 index 0000000000..b1f57b5d4c Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/as-open-menu.png differ diff --git a/versions/4.0/en/editor/publish/android/images/as-open-proj.png b/versions/4.0/en/editor/publish/android/images/as-open-proj.png new file mode 100644 index 0000000000..9eb58e9f8e Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/as-open-proj.png differ diff --git a/versions/4.0/en/editor/publish/android/images/as-setting.png b/versions/4.0/en/editor/publish/android/images/as-setting.png new file mode 100644 index 0000000000..cb72db0d6a Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/as-setting.png differ diff --git a/versions/4.0/en/editor/publish/android/images/background-task.png b/versions/4.0/en/editor/publish/android/images/background-task.png new file mode 100644 index 0000000000..0977698682 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/background-task.png differ diff --git a/versions/4.0/en/editor/publish/android/images/build-apk-success.png b/versions/4.0/en/editor/publish/android/images/build-apk-success.png new file mode 100644 index 0000000000..0fb0f4a4b9 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/build-apk-success.png differ diff --git a/versions/4.0/en/editor/publish/android/images/build-apk.png b/versions/4.0/en/editor/publish/android/images/build-apk.png new file mode 100644 index 0000000000..b5692a05ec Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/build-apk.png differ diff --git a/versions/4.0/en/editor/publish/android/images/build-menu.png b/versions/4.0/en/editor/publish/android/images/build-menu.png new file mode 100644 index 0000000000..3d4968bcbb Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/build-menu.png differ diff --git a/versions/4.0/en/editor/publish/android/images/build-options-android.png b/versions/4.0/en/editor/publish/android/images/build-options-android.png new file mode 100644 index 0000000000..894aabe3e4 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/build-options-android.png differ diff --git a/versions/4.0/en/editor/publish/android/images/build.png b/versions/4.0/en/editor/publish/android/images/build.png new file mode 100644 index 0000000000..a81485fb5b Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/build.png differ diff --git a/versions/4.0/en/editor/publish/android/images/building.png b/versions/4.0/en/editor/publish/android/images/building.png new file mode 100644 index 0000000000..427d0a9987 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/building.png differ diff --git a/versions/4.0/en/editor/publish/android/images/cc-build-menu.png b/versions/4.0/en/editor/publish/android/images/cc-build-menu.png new file mode 100644 index 0000000000..83935002d6 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/cc-build-menu.png differ diff --git a/versions/4.0/en/editor/publish/android/images/check-err.png b/versions/4.0/en/editor/publish/android/images/check-err.png new file mode 100644 index 0000000000..4978158a95 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/check-err.png differ diff --git a/versions/4.0/en/editor/publish/android/images/component-installer.png b/versions/4.0/en/editor/publish/android/images/component-installer.png new file mode 100644 index 0000000000..f57a8b2f40 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/component-installer.png differ diff --git a/versions/4.0/en/editor/publish/android/images/confirm-change.png b/versions/4.0/en/editor/publish/android/images/confirm-change.png new file mode 100644 index 0000000000..3e6d57aadb Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/confirm-change.png differ diff --git a/versions/4.0/en/editor/publish/android/images/create-scene.png b/versions/4.0/en/editor/publish/android/images/create-scene.png new file mode 100644 index 0000000000..7fa7f6560d Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/create-scene.png differ diff --git a/versions/4.0/en/editor/publish/android/images/create-sign.png b/versions/4.0/en/editor/publish/android/images/create-sign.png new file mode 100644 index 0000000000..5f633747ed Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/create-sign.png differ diff --git a/versions/4.0/en/editor/publish/android/images/debug-keystore.png b/versions/4.0/en/editor/publish/android/images/debug-keystore.png new file mode 100644 index 0000000000..c0f2c237bd Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/debug-keystore.png differ diff --git a/versions/4.0/en/editor/publish/android/images/download-ndk-by-as.png b/versions/4.0/en/editor/publish/android/images/download-ndk-by-as.png new file mode 100644 index 0000000000..40ddd3dc32 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/download-ndk-by-as.png differ diff --git a/versions/4.0/en/editor/publish/android/images/download-ndk.png b/versions/4.0/en/editor/publish/android/images/download-ndk.png new file mode 100644 index 0000000000..37b1acf917 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/download-ndk.png differ diff --git a/versions/4.0/en/editor/publish/android/images/download-sdk.png b/versions/4.0/en/editor/publish/android/images/download-sdk.png new file mode 100644 index 0000000000..07b8a8bcc7 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/download-sdk.png differ diff --git a/versions/4.0/en/editor/publish/android/images/downloading-complete.png b/versions/4.0/en/editor/publish/android/images/downloading-complete.png new file mode 100644 index 0000000000..70f6c65565 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/downloading-complete.png differ diff --git a/versions/4.0/en/editor/publish/android/images/downloading.png b/versions/4.0/en/editor/publish/android/images/downloading.png new file mode 100644 index 0000000000..398f61dcaf Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/downloading.png differ diff --git a/versions/4.0/en/editor/publish/android/images/find-proj.png b/versions/4.0/en/editor/publish/android/images/find-proj.png new file mode 100644 index 0000000000..ac741c9f4d Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/find-proj.png differ diff --git a/versions/4.0/en/editor/publish/android/images/find-required-version.png b/versions/4.0/en/editor/publish/android/images/find-required-version.png new file mode 100644 index 0000000000..f284735b3e Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/find-required-version.png differ diff --git a/versions/4.0/en/editor/publish/android/images/find-sdk-version.png b/versions/4.0/en/editor/publish/android/images/find-sdk-version.png new file mode 100644 index 0000000000..8fc2e931a9 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/find-sdk-version.png differ diff --git a/versions/4.0/en/editor/publish/android/images/game-package-name.png b/versions/4.0/en/editor/publish/android/images/game-package-name.png new file mode 100644 index 0000000000..f838d433cd Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/game-package-name.png differ diff --git a/versions/4.0/en/editor/publish/android/images/gen-apk.png b/versions/4.0/en/editor/publish/android/images/gen-apk.png new file mode 100644 index 0000000000..df8ae2fbab Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/gen-apk.png differ diff --git a/versions/4.0/en/editor/publish/android/images/gen-sign-apk.png b/versions/4.0/en/editor/publish/android/images/gen-sign-apk.png new file mode 100644 index 0000000000..e3249799ab Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/gen-sign-apk.png differ diff --git a/versions/4.0/en/editor/publish/android/images/http-proxy.png b/versions/4.0/en/editor/publish/android/images/http-proxy.png new file mode 100644 index 0000000000..ffa66da5ae Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/http-proxy.png differ diff --git a/versions/4.0/en/editor/publish/android/images/install-ndk-by-as.png b/versions/4.0/en/editor/publish/android/images/install-ndk-by-as.png new file mode 100644 index 0000000000..81e07c3974 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/install-ndk-by-as.png differ diff --git a/versions/4.0/en/editor/publish/android/images/interrupt-sync.png b/versions/4.0/en/editor/publish/android/images/interrupt-sync.png new file mode 100644 index 0000000000..f10fdbe28d Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/interrupt-sync.png differ diff --git a/versions/4.0/en/editor/publish/android/images/java-ts.png b/versions/4.0/en/editor/publish/android/images/java-ts.png new file mode 100644 index 0000000000..e6e0a28319 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/java-ts.png differ diff --git a/versions/4.0/en/editor/publish/android/images/missing-sdk.png b/versions/4.0/en/editor/publish/android/images/missing-sdk.png new file mode 100644 index 0000000000..4e3de76a2c Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/missing-sdk.png differ diff --git a/versions/4.0/en/editor/publish/android/images/ndk-dir.png b/versions/4.0/en/editor/publish/android/images/ndk-dir.png new file mode 100644 index 0000000000..adb9459b76 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/ndk-dir.png differ diff --git a/versions/4.0/en/editor/publish/android/images/new-build-task.png b/versions/4.0/en/editor/publish/android/images/new-build-task.png new file mode 100644 index 0000000000..6ac624a9b3 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/new-build-task.png differ diff --git a/versions/4.0/en/editor/publish/android/images/open-with-as.png b/versions/4.0/en/editor/publish/android/images/open-with-as.png new file mode 100644 index 0000000000..42cb349157 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/open-with-as.png differ diff --git a/versions/4.0/en/editor/publish/android/images/open.png b/versions/4.0/en/editor/publish/android/images/open.png new file mode 100644 index 0000000000..cbc97681ab Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/open.png differ diff --git a/versions/4.0/en/editor/publish/android/images/package-name.png b/versions/4.0/en/editor/publish/android/images/package-name.png new file mode 100644 index 0000000000..0fd4824c87 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/package-name.png differ diff --git a/versions/4.0/en/editor/publish/android/images/preference.png b/versions/4.0/en/editor/publish/android/images/preference.png new file mode 100644 index 0000000000..850cf3a115 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/preference.png differ diff --git a/versions/4.0/en/editor/publish/android/images/program.png b/versions/4.0/en/editor/publish/android/images/program.png new file mode 100644 index 0000000000..f734f91b7b Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/program.png differ diff --git a/versions/4.0/en/editor/publish/android/images/progress.png b/versions/4.0/en/editor/publish/android/images/progress.png new file mode 100644 index 0000000000..53048055b7 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/progress.png differ diff --git a/versions/4.0/en/editor/publish/android/images/project.png b/versions/4.0/en/editor/publish/android/images/project.png new file mode 100644 index 0000000000..2bd2bef86c Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/project.png differ diff --git a/versions/4.0/en/editor/publish/android/images/render-backend.png b/versions/4.0/en/editor/publish/android/images/render-backend.png new file mode 100644 index 0000000000..b5e88160a7 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/render-backend.png differ diff --git a/versions/4.0/en/editor/publish/android/images/sdk-component-setup.png b/versions/4.0/en/editor/publish/android/images/sdk-component-setup.png new file mode 100644 index 0000000000..b3dd272f98 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/sdk-component-setup.png differ diff --git a/versions/4.0/en/editor/publish/android/images/sdk-dir.png b/versions/4.0/en/editor/publish/android/images/sdk-dir.png new file mode 100644 index 0000000000..d127103c27 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/sdk-dir.png differ diff --git a/versions/4.0/en/editor/publish/android/images/sdk-location.png b/versions/4.0/en/editor/publish/android/images/sdk-location.png new file mode 100644 index 0000000000..86a8844d48 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/sdk-location.png differ diff --git a/versions/4.0/en/editor/publish/android/images/select-platform.png b/versions/4.0/en/editor/publish/android/images/select-platform.png new file mode 100644 index 0000000000..d160c93e08 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/select-platform.png differ diff --git a/versions/4.0/en/editor/publish/android/images/select-sdk.png b/versions/4.0/en/editor/publish/android/images/select-sdk.png new file mode 100644 index 0000000000..44b4ab4328 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/select-sdk.png differ diff --git a/versions/4.0/en/editor/publish/android/images/show-log.png b/versions/4.0/en/editor/publish/android/images/show-log.png new file mode 100644 index 0000000000..9e9cb4d6cb Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/show-log.png differ diff --git a/versions/4.0/en/editor/publish/android/images/show-package-details.png b/versions/4.0/en/editor/publish/android/images/show-package-details.png new file mode 100644 index 0000000000..832618e2b5 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/show-package-details.png differ diff --git a/versions/4.0/en/editor/publish/android/images/start-scene.png b/versions/4.0/en/editor/publish/android/images/start-scene.png new file mode 100644 index 0000000000..7ac450b1ad Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/start-scene.png differ diff --git a/versions/4.0/en/editor/publish/android/images/target-api-level.png b/versions/4.0/en/editor/publish/android/images/target-api-level.png new file mode 100644 index 0000000000..b6006ef0ee Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/target-api-level.png differ diff --git a/versions/4.0/en/editor/publish/android/images/terminal.png b/versions/4.0/en/editor/publish/android/images/terminal.png new file mode 100644 index 0000000000..2d152f9bb0 Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/terminal.png differ diff --git a/versions/4.0/en/editor/publish/android/images/verify-setting.png b/versions/4.0/en/editor/publish/android/images/verify-setting.png new file mode 100644 index 0000000000..cd75d0091a Binary files /dev/null and b/versions/4.0/en/editor/publish/android/images/verify-setting.png differ diff --git a/versions/4.0/en/editor/publish/android/index.md b/versions/4.0/en/editor/publish/android/index.md new file mode 100644 index 0000000000..54a2a55c35 --- /dev/null +++ b/versions/4.0/en/editor/publish/android/index.md @@ -0,0 +1,21 @@ +# Publishing Android Apps + +This article will introduce the process of publish a Cocos Creator project to Android apps. + +## Content + +- [Android Publishing Example](./build-example-android.md) +- [Build Options - Android](build-options-android.md) +- [JavaScript and iOS/macOS Communication with Reflection](../../../advanced-topics/oc-reflection.md) +- [JavaScript and Objective-C Communication using JsbBridge](../../../advanced-topics/js-oc-bridge.md) +- [Features and System Versions](../../../advanced-topics/supported-versions.md) + +## Read More + +- [Setting up Native Development Environment](../setup-native-development.md) +- [Build Panel](../build-panel.md) +- [General Build Panel](../build-options.md) +- [General Native Build Options](../native-options.md) +- [Debugging JavaScript on Native Platforms](../debug-jsb.md) +- [Build Process and FAQ](../build-guide.md) +- [Native Platform Secondary Development Guide](../../../advanced-topics/native-secondary-development.md) diff --git a/versions/4.0/en/editor/publish/app-clip/cocos-proj.png b/versions/4.0/en/editor/publish/app-clip/cocos-proj.png new file mode 100644 index 0000000000..81f1e52e74 Binary files /dev/null and b/versions/4.0/en/editor/publish/app-clip/cocos-proj.png differ diff --git a/versions/4.0/en/editor/publish/app-clip/other-proj.png b/versions/4.0/en/editor/publish/app-clip/other-proj.png new file mode 100644 index 0000000000..652e667ef6 Binary files /dev/null and b/versions/4.0/en/editor/publish/app-clip/other-proj.png differ diff --git a/versions/4.0/en/editor/publish/app-clip/ui-build.png b/versions/4.0/en/editor/publish/app-clip/ui-build.png new file mode 100644 index 0000000000..c7eefa7e6c Binary files /dev/null and b/versions/4.0/en/editor/publish/app-clip/ui-build.png differ diff --git a/versions/4.0/en/editor/publish/build-guide.md b/versions/4.0/en/editor/publish/build-guide.md new file mode 100644 index 0000000000..9c473b165e --- /dev/null +++ b/versions/4.0/en/editor/publish/build-guide.md @@ -0,0 +1,252 @@ +# Build Process and FAQ + +## Introduction to the Build Infrastructure + +The build process mainly includes the following two parts: + +- **General build processing** +- **Build processing for each platform** + +Due to the adjustment of the build mechanism in v3.0, the build process of different platforms is injected into the **Build** panel in the form of **build plugins**, each participating in the build process. The build options specific to each platform are displayed in the **Build** panel in the form of expanded options. Developers can make custom build options appear in the **Build** panel via the [build plugin](custom-build-plugin.md). + +![build-engine](./build-guide/web.png) + +## General Build Processing Flow + +The general Build process of Cocos Creator mainly includes the following contents: + +1. Initialization of build options +2. Build data organization +3. Write the built resources to the file system +4. Organize `settings.json`/`config.json` data +5. UUID compression and file writing in `config.json` + +### Initialization of Build options + +This step is mainly for initializing the **primal options** passed to the build during the build to **build internal options**, completing the format conversion of some of the build options, initialize project configuration, initialize the asset data of the build asset database, and check the latest asset information and classify it. + +### Build data organization + +When building, the editor will first sort out the current scenes involved in the build and assets in all [Bundles](../../asset/bundle.md) folders and then search for their dependent assets through engine deserialization, and recursively find a list of all the assets that need to be packaged. After the asset list is summarized, the assets will be classified according to the `Bundle` configuration, and script compiling tasks, image compression tasks, and json grouping information will be collected. + +> **Notes**: +> +> 1. The engine will load all user scripts before deserialization. Whether the script is successfully loaded will directly affect the deserialization. If the script is not correctly written, it will cause the build to fail. +> 2. If a dependency asset is found missing during the asset sorting process, an error will be reported but the build will normally continue. Although the build is allowed to continue successfully, it does not mean that the problem does not need to be solved. If the missing assets are not solved, it is difficult to guarantee that the built game package has no problems. + +### Writing the built assets to the file system + +The built assets will be packaged into `assets/[Bundle name]` under the corresponding platform release package directory according to the configuration of the [Asset Bundle](../../asset/bundle.md#configuration) after the build is completed. The directory structure is as follows: + +![build-engine](./build-guide/bundle.png) + +For more information about `Asset Bundle` configuration, building, file description, etc., please refer to the [Asset Bundle](../../asset/bundle.md) documentation. + +After organizing the basic build tasks, the build will loop through all the `Bundles`, executing the asset packaging tasks that each `Bundle` needs to handle in turn to generate these `Bundle` asset packages. + +Each `Bundle` goes through the following asset handling process: + +- **Script Building**: scripts within the editor are divided into [plug-in scripts](../../scripting/external-scripts.md) and **non-plugin scripts**, with different rules for handling different kinds. + + - **Plug-in scripts**: directly copy the source files according to the original directory structure to the `src` directory under the release package directory generated after the build, the plug-in script does not support any script form that needs to be compiled, such as `TypeScript` or `JavaScript (ES6)`. The asset information of the plug-in script will be written into the jsList array in `settings`. + + - **Non-plugin scripts**: packaged into `project.js` and placed in the corresponding `src` directory. Checking the `sourceMap` option will generate the corresponding `.map` file, and determine whether the script is compressed according to the `debug` option. + +- **Auto Atlas Processing**: queries the Auto Atlas asset list within the project, packaging the SpriteFrame small images under the atlas into a large image, generating serialized files, etc. according to the configuration of the Auto Atlas resource. This step will also modify the `json` grouping information, asset grouping information, and add texture compression tasks. + +- **Texture Compression**: according to the organized image compression task, the image assets are compressed and written into the build folder. + +- **Engine Script Compilation**: according to the configuration in **Main Menu** -> **Project Settings** -> **Feature Cropping**, remove the unused engine modules and pack them into the `cocos-js` directory. Checking the `sourceMap` option in the **Build** panel will generate the corresponding `.map` file, and determine whether the script is compressed according to the `debug` option. + + The main steps of engine compilation include: + + - Get **engine module information in project settings**. + + - **Check** whether the compilation options of the engine in the **cache** are consistent with the current options to be compiled, and if they are, use the cache directly. + + - If compilation is needed, execute the task of packaging the engine, then copy the compiled js file and save the engine's cache judgment information file. + + When compiling the engine, [output log information](./build-panel.md#build-log) can be viewed: + + ![build-engine](./build-guide/build-engine.jpg) + + Regarding the reuse rules of engine files, it is necessary to elaborate:
+ The packaged engine file will be placed in the editor's global temporary directory (use `Build.globalTempDir` to look up the cache address during the build process). The cache file is stored as the name according to the hash value generated by the parameters that will affect the engine compilation. + + ```bash + global-temp-folder + |--CocosCreator + |--x.xx(3.0.0) + |--builder + |--engine + |--1dc4a547f9...63a43bb8965.watch-files.json + |--1dc4a547f9...63a43bb8965 (engine folder) + |--1dc4a547f9...63a43bb8965.meta (folder) + ... + ``` + + As long as any relevant engine build parameters are changed, the engine will be recompiled. Parameters that specifically affect the use of the build engine cache are: + + - debug: whether to open the debug mode + - includeModules: engine module settings + - sourceMaps: whether to enable sourceMaps + - platform: build platform + - Engine modification time + - Whether to check the **Separate Engine** in the **Build** panel (only WeChat Mini Game) + - Use engine path, engine version number + +- **JSON build**: When serializing `JSON`, it will be merged and written into the file system (in the folder `assets/xxxBundle/import`) according to the `JSON` grouping and the `Bundle` to which it belongs. **If it is in release mode, it will also compress the UUID in the serialized JSON**. + +- **Common Asset Copy**: Some raw assets will be directly copied from the `library` to the built folder `assets/xxxBundle/native`. + +- **md5 processing**: Add the `.md5` suffix to all the assets, project scripts and engine files in the `assets` folder, and organize the data to be recorded in the `settings.json`. + +- **`application.js` template file generation**: dynamically generates the `application.js` file according to user options and generate it to the release package directory `src` generated after the build. + +### Organizing `settings.json`/`config.json` and configuring JSON data + +It is mainly to prepare the necessary configuration information for the game launch based on the asset data compiled previously. + +#### settings.json + +`settings.json` records the basic configuration information of the whole game package, which will directly affect the initialization of the game package. + +The description of the `settings.json` configuration is as follows: + +```js +{ + debug: boolean; // Whether it is in debug mode, depends on the settings in the Build panel. + designResolution: ISettingsDesignResolution; // Canvas resolution setting, depends on the settings of the Project Data page in the Project Settings panel. + jsList: string[]; + launchScene: string; // URL of the initial scene. + moduleIds: string[]; // Information about all user script components. + platform: string; + renderPipeline: string; // renderPipeline information, depends on the settings of the Project Data page in the Project Settings panel. + physics?: IPhysicsConfig; // Physics module settings (only generated when the physics engine module is checked). + BundleVers: Record; // Bundle's md5 file value. + subpackages: string[]; // subpackage information. + remoteBundles: string[]; // Record the collection of remote Bundle. + server: string; // Record the remote server address (note: before v3.4 this option was stored in the 'application.js' file). + hasResourcesBundle: boolean; // Does it contain the built-in Bundle "resources". + hasStartSceneBundle: boolean; // Does it contain the built-in Bundle "start-scene". + customJointTextureLayouts?: ICustomJointTextureLayout[]; + macros?: Record; // Engine macro configuration values in the Project Settings panel. +} +``` + +#### config.json + +Each Bundle asset package has a `config.json`, which records basic information such as assets and scripts for the entire `Bundle`, which will directly affect the loading of the `Bundle` package. + +The description of the `config.json` configuration is as follows: + +```js +{ + importBase: string; // The name of the import directory in the Bundle, usually 'import'. + nativeBase: string; // The name of the native directory in native, usually 'native'. + name: string; // The name of the Bundle, the Bundle can be loaded by the Bundle name + deps: string[]; // Other Bundle names that this Bundle depends on. + scenes: Array<{url: string, uuid: string}>; // The array of scene information contained in the Bundle. + rawAssets: { [index: string]: { [uuid: string]: string[] } }; + // Store the URL and type of the asset loaded under 'resources' + // Example: "bba00d3a-2f17-4511-b47c-0d584b21b763@6c48a": ["test/right/texture", "cc.Texture2D", "bba0...@6c48a"] + // "bba0...@6c48a": ["test/right/texture", 1, 1] + packs: Record; // json group information. + versions: { + import: Array; + native: Array; + }; // Only available after md5Cache is checked, the array part is stored in the format of [uuid_1, md5_1, uuid_2, md5_2, ...], where uuid_1 is a simple number indicating that the storage is the uuid index in the uuids array. + uuids: string[]; // uuid array, only in release mode. + types?: string[]; // Resource type array, only in release mode. + encrypted?: boolean; // Marks whether the script in the Bundle is encrypted, active on the native platform. + isZip?: boolean; // Is it in ZIP mode. + zipVersion?: string; // MD5 Hash value of ZIP package. +} +``` + +The structure here only lists the structure of `settings.json` and `config.json` in a general sense. In fact, these parameters will vary after different platform builds. + +### Compressing config.json + +Before generating the `config.json` file, the `UUID` information in the file will be compressed according to whether it is in release mode or not, understanding this rule will be helpful to find the location of the file after the asset is built. + +The `UUIDs` used in the `Bundle` will be sorted during the build, and those that appear **twice or more** will be stored in the `uuids` array, and the location of the previously used `UUID` will be replaced with the index. + +All `types` that appear **twice and more** are also stored in the `types` array, and the previously used position is replaced with the index. + +#### Build assets + +This step generates project asset files other than scripts, since scripts are compiled and processed separately as special files. The assets are re-serialized after deserialization is performed during the packaging process to reduce the package size after packaging. Multiple serialized JSONs are combined into a single file based on the JSON grouping information collated from the previous data, e.g. the serialized files of the `texture` asset are all packaged into a single JSON file. + +## Building of each platform + +The Build process provides hook functions that build the lifecycle, which is convenient for developers to participate in the building during different processing periods of the building and affect the building result. At the same time, the build also provides a way for developers to directly add some custom build options. The corresponding new parameters can be displayed directly in the **Build** panel through simple configuration. For details, please refer to the [Extending Build Process](custom-build-plugin.md) documentation. The build options injected by the build plugin will be stored in `options.packages[pkgName]`, the current way of writing option parameters built through the command line also needs to follow this rule. The rules for the corresponding parameters can be referenced by clicking **Export Build Config** at the top right of the **Build** panel. + +### Compilation/generation process of each platform + +The build process of all platforms that require support for separate compilation and generation have been split. Some developers may wonder why the current mini game platform has a new **Make** button. In fact this part of the logic has always existed before, but it is merged in the **build** process and cannot be controlled separately. + +The **build** of the editor is similar to the function of an **export game package for the corresponding platform**, which is mainly to complete the interface of the engine to each platform and the compatibility of the basic format of the game package, which does not mean that all the work is completed. Each platform usually has its own compilation process, such as the compilation and upload function of the developer tools that come with the WeChat Mini Game platform, and the compilation, running and debugging functions of the relevant IDE for each native platform. If the developer needs to customize the packaging process for a specific platform, the editor needs to support the process of splitting in order to better access. + +## FAQ Guide + +The entire build process is in a single worker, to view the log information of the build process or view the complete call stack when an error occurs, click on the main menu, then select **Developer -> Open Build DevTools**. In fact, a lot of log information will be output when building, but in order not to interfere with users, by default only error and important information will be printed to the editor's **Console** panel, and the log information in the devtools is the most complete. + +> **Note**: before building **please make sure that the scene participating in the building can be previewed normally**. Loss of assets in some scenes or other script problems can be exposed during the preview stage. Building under the condition that the preview is normal can save time and troubleshoot better. + +### Asset loading 404 errors + +In case of a 404 error, please copy the UUID in the log that reported the error resource loss and search for the corresponding resource in **Assets** panel to check whether the resources that the resource depends on are normal. Resource loading 404 errors usually occur under the following situations: + +1. **Resources that are not placed in the Bundle are dynamically loaded in the script**. + + - **Reason**: Through the above introduction, we know that only the resources and their dependent resources in the Bundle directory, as well as the resources and their dependent resources participating in the build scene will be packaged into the final build folder, and **only the resource URL directly put into the `Bundle` folder will be written to the `config.json`**. If a resource is used in the script but the resource is not placed in any Bundle directory, a 404 error will appear when it is loaded. + + - **Solution**: Move the used resources to the Bundle folder. + +2. **There is a problem importing the loaded resource, and the data cannot be generated normally to the library** + + - **Reason**: All the original data is obtained by reading the asset files in the `library` during the build. If the import fails, the correct corresponding asset information will not be obtained. + + - **Solution**: Find the corresponding asset through **Assets** panel, right-click, and select **Reimport Asset** in the menu. + +3. **Asset Loss** + + - **Reason**: As mentioned in the previous construction process, **asset construction will go through the reverse sequence to find dependent assets**, and the most frequent problem is that the dependent assets are accidentally during the project iteration process delete and cause asset loss. The loss of these assets may not usually be noticed, but once the build is performed, it will be exposed. + + - **Solution**: Use the code editor to find out which assets the `UUID` is referenced, and modify the corresponding assets. + +### Script asset loading error + +As mentioned in the previous section of **Building Data Sorting**, the script environment needs to be configured when building. If the error message is related to the script, please refer to the error content to modify the script. If it is unknown which script is reporting the error, find the `UUID` of the corresponding script in the error message call stack, and then find the location in the **Assets** panel. + +### How to find the big picture after the small picture is automatically combined + +The Auto Atlas will print out the UUID information of the original small image and the synthesized large image during the build process, which can be found in the build devtools, and then use the UUID of the composite large image found to generate the `XXXBundle/native` after packaging. You can view it in the directory. If there are too many combined images, open the build `log` file and search for the UUID. + +![build-atlas](./build-guide/build-atlas.jpg) + +### How to decompress UUIDs + +In **release** mode, the packaged asset JSON file and the `UUID` in `config.json` are compressed and need to be unzipped to find the corresponding assets in the original project. There are some built-in tools and methods in the build process. On the global variable Build, directly click **Developer -> Build DevTools** in the main menu, the original `UUID` can be queried in the console by entering the following command: + +```js +Build.Utils.decompressUuid('425o80X19KipOK7J1f5hsN'); +// 42e68f34-5f5f-4a8a-938a-ec9d5fe61b0d +``` + + + +### Engine compilation failed + +If the engine fails to compile, please check if the installation package is complete, if the modified built-in engine code is correct, and if you are using a custom engine, if the path is correct, etc. + +### After switching to a custom engine and rebuilding, Android Studio still uses the default engine files + +In Android Studio, open the `Build` menu and click the `Refresh Linked C++ Projects` button to use the code files from the new engine directory. + +### Other errors + +If errors are encountered that cannot be resolved, please send feedback to the [Cocos Forum](https://discuss.cocos2d-x.org/c/creator/33) with the version of Creator, build options configuration, build log file from the build task, and a demo that reproduces the issue. diff --git a/versions/4.0/en/editor/publish/build-guide/build-atlas.jpg b/versions/4.0/en/editor/publish/build-guide/build-atlas.jpg new file mode 100644 index 0000000000..204ab96a23 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-guide/build-atlas.jpg differ diff --git a/versions/4.0/en/editor/publish/build-guide/build-engine.jpg b/versions/4.0/en/editor/publish/build-guide/build-engine.jpg new file mode 100644 index 0000000000..f60f377f3b Binary files /dev/null and b/versions/4.0/en/editor/publish/build-guide/build-engine.jpg differ diff --git a/versions/4.0/en/editor/publish/build-guide/bundle.png b/versions/4.0/en/editor/publish/build-guide/bundle.png new file mode 100644 index 0000000000..a0591ef16e Binary files /dev/null and b/versions/4.0/en/editor/publish/build-guide/bundle.png differ diff --git a/versions/4.0/en/editor/publish/build-guide/web.png b/versions/4.0/en/editor/publish/build-guide/web.png new file mode 100644 index 0000000000..3e8389c71f Binary files /dev/null and b/versions/4.0/en/editor/publish/build-guide/web.png differ diff --git a/versions/4.0/en/editor/publish/build-open-data-context.md b/versions/4.0/en/editor/publish/build-open-data-context.md new file mode 100644 index 0000000000..f036711d4c --- /dev/null +++ b/versions/4.0/en/editor/publish/build-open-data-context.md @@ -0,0 +1,108 @@ +# Access to Open Data Context + +Currently, platforms such as **WeChat**, **Baidu**, and **Douyin Mini Game** have added the concept of **Open Data Context**, which is a separate game execution environment, in order to protect their social relationship chain data. The resources, engines, and applications in the **Open Data Context** are completely isolated from the main context, and only in the **Open Data Context** can developers access the relationship chain data through the open interface provided by the platform to implement some features such as leaderboards. + +In **Cocos Creator 3.0**, we deprecate the Canvas Renderer module and replaced it with a lightweight front-end Canvas engine based on **XML** + **CSS** designed by WeChat team. The engine is integrated into the **Cocos Creator 3.0**'s built-in **Open Data Context** project template, which allows developers to implement a leaderboard-like feature based on the template with a few basic front-end skills. + +## SubContextView Component Description + +Since the **Open Data Context** can only be rendered on the off-screen canvas called **sharedCanvas**, you need a node in your project to act as a container for rendering the **Open Data Context**, and add the `SubContextView` component to that node, which will render the **sharedCanvas** to the container node. + +The `SubContextView` component contains two main properties, **Design Resolution Size** and **FPS**. + +![sub-context-view](./build-open-data-context/sub-context-view.png) + +### Design Resolution Size + +If you set the **Design Resolution Size** of the `SubContextView` component to **640 * 960**, the size of the **sharedCanvas** will be set to **640 * 960** during the component's `onLoad` phase. This means that after the build, the **Open Data Context Project** is rendered on an off-screen canvas of **640 * 960**. Then, when customizing the **Open Data Context** (see below), the maximum size of the tag style in `style.js` is **640 * 960**, otherwise the rendered content will be off the canvas. Example: + +```js +// style.js +export default { + container: { + width: 640, // max width + height: 960, // max height + }, +} +``` + +To avoid this part of the data coupling, setting a percentage adaptation to the size is supported. Example: + +```js +// style.js +export default { + container: { + width: '100%', + height: '100%', + }, +} +``` + +In the actual rendering process, the engine will adopt the **SHOW ALL** adaptation policy to render the **sharedCanvas** to the `SubContextView` component node to avoid the UI distortion caused by stretching during rendering. For example, in the following two images, we are using `SubContextView` component nodes of different sizes, and the **Open Data Context** texture will not be stretched. + +![adaption-1](./build-open-data-context/adaption-1.png) + +![adaption-2](./build-open-data-context/adaption-2.png) + +### Setting FPS + +The **FPS** property is primarily used to set how often the main context will update the **sharedCanvas** on the `SubContextView` component to avoid performance loss due to frequent updates to the **Open Data Context** texture. + +## Release Process + +1. Open the project and double-click the scene, then add the `SubContextView` component to the node on which you need to render the **Open Data Context**. + +2. After the scene is set, save the scene, and then open the **Build** panel in **Menu -> Project**, select the **WeChat** / **Baidu** / **Douyin Mini Game** platform you want to release, check **Generate Open Data Context Template**, and then click **Build**. + + ![generate-template](./build-open-data-context/generate-template.png) + +3. After the build is complete, click the **Folder Icon** button at the end of **Buid Path**, you'll see an **openDataContext** folder (e.g.: `build/wechatgame/openDataContext`), which is an **Open Data Context** project template built into Cocos Creator, in the distribution folder of the corresponding game platform. + + ![build-output](./build-open-data-context/build-output.png) + + Developers can customize the required **Open Data Context** content based on this template, and the customization methods are described below. When built again, if the **openDataContext** folder exists in the `build` directory, it will be skipped directly and the developer does not have to worry about the customized **Open Data Context Project** being overwritten. + +4. Open the build distribution (e.g.: `build/wechatgame`) using the DevTools of the corresponding mini game platformer to open the mini-game project to view the **Open Data Context** content. + + ![show-in-devtool](./build-open-data-context/show-in-devtool.png) + + > **Note**: in the **Open Data Context** of **Baidu** platform, since the image can only load player avatars returned from Baidu, the local avatar image may not be loaded in the generated template project. + +## Customization on Open Data Context Project + +Before customizing an **Open Data Context** project, developers need to know some basic information: +- [minigame-canvas-engine quick start[cn]](https://wechat-miniprogram.github.io/minigame-canvas-engine/overview/guide.html) +- [doT template engine use](http://olado.github.io/doT/?spm=a2c6h.12873639.0.0.36f45227oKu0XO) + +With this basic information in mind, let's take a look at the **Open Data Context** template generated by default after the build, with the following directory structure: + +![folder-structure](./build-open-data-context/folder-structure.png) + +- **render/dataDemo.js**: Simulates some random data of the leaderboards, where the developer can request the relational chain data from the platform and pass it to the **doT template engine** to generate relevant XML text +- **render/style.js**: To record CSS style text information, refer to [Style documentation [cn]](https://wechat-miniprogram.github.io/minigame-canvas-engine/api/style.html#%E5%B8%83%E5%B1%80) +- **render/template.js**: To record XML text information, the project uses the template engine to generate XML text by default. Refer to [Tag documentation [cn]](https://wechat-miniprogram.github.io/minigame-canvas-engine/api/tags.html#%E6%A0%87%E7%AD%BE%E5%88%97%E8%A1%A8). +- **render/avatar.png**: Header images for display in **Open Data Context** project template, can be deleted. +- **engine.js**: source code of Canvas engine +- **index.js**: **Open Data Context Project** entry file where the **Open Data Context** is rendered by passing XML text and CSS styles to the Canvas engine + +## Recommended practices + +1. Since the build directory generated after the build of the project is excluded from version control by default by git, if you want to include your custom **Open Data Context** in version control, you can put the `openDataContext` folder (e.g.: `build/wechatgame/openDataContext`) into your project's `build-templates` directory. Please refer to [Custom Project Build Process](./custom-project-build-template.md) documentation. + +2. In an **Open Data Context Project**, if you need to listen to messages from the main context, you need to first determine whether the message comes from the main context engine, using the WeChat interface as an example: + + ```js + wx.onMessage(res => { + if (!(res && res.type === 'engine')) { + console.log('do something...'); + } + }); + ``` + + When the main context sends a message to the open data context, it is recommended to include a `type` message to avoid handling the wrong message source. For example, the `res.type === 'engine'` in the above code means that the message comes from the main context engine. + +## Reference documentation + +- [WeChat official document -- Canvas engine for mini games [cn]](https://wechat-miniprogram.github.io/minigame-canvas-engine/) +- [minigame-canvas-engine source code](https://github.com/wechat-miniprogram/minigame-canvas-engine) +- [doT template engine](http://olado.github.io/doT/?spm=a2c6h.12873639.0.0.36f45227oKu0XO) diff --git a/versions/4.0/en/editor/publish/build-open-data-context/adaption-1.png b/versions/4.0/en/editor/publish/build-open-data-context/adaption-1.png new file mode 100644 index 0000000000..1bb192df80 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-open-data-context/adaption-1.png differ diff --git a/versions/4.0/en/editor/publish/build-open-data-context/adaption-2.png b/versions/4.0/en/editor/publish/build-open-data-context/adaption-2.png new file mode 100644 index 0000000000..4883df8447 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-open-data-context/adaption-2.png differ diff --git a/versions/4.0/en/editor/publish/build-open-data-context/build-output.png b/versions/4.0/en/editor/publish/build-open-data-context/build-output.png new file mode 100644 index 0000000000..a227816372 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-open-data-context/build-output.png differ diff --git a/versions/4.0/en/editor/publish/build-open-data-context/folder-structure.png b/versions/4.0/en/editor/publish/build-open-data-context/folder-structure.png new file mode 100644 index 0000000000..5fb48ca3a2 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-open-data-context/folder-structure.png differ diff --git a/versions/4.0/en/editor/publish/build-open-data-context/generate-template.png b/versions/4.0/en/editor/publish/build-open-data-context/generate-template.png new file mode 100644 index 0000000000..8899376e1d Binary files /dev/null and b/versions/4.0/en/editor/publish/build-open-data-context/generate-template.png differ diff --git a/versions/4.0/en/editor/publish/build-open-data-context/show-in-devtool.png b/versions/4.0/en/editor/publish/build-open-data-context/show-in-devtool.png new file mode 100644 index 0000000000..930ff2a608 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-open-data-context/show-in-devtool.png differ diff --git a/versions/4.0/en/editor/publish/build-open-data-context/sub-context-view.png b/versions/4.0/en/editor/publish/build-open-data-context/sub-context-view.png new file mode 100644 index 0000000000..e08694bff2 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-open-data-context/sub-context-view.png differ diff --git a/versions/4.0/en/editor/publish/build-options.md b/versions/4.0/en/editor/publish/build-options.md new file mode 100644 index 0000000000..fdf36d189d --- /dev/null +++ b/versions/4.0/en/editor/publish/build-options.md @@ -0,0 +1,181 @@ +# General Build Options + +## General Build Options + +For the general build options in the **Build** panel are as follows: + +![build options](./build-options/options.png) + +### Build Path + +There are two input boxes in the Build Path: + +![build path](./build-options/build-path.png) + +- The first input box is used to specify the build path of the project. The path can be entered directly in the input box or selected via the magnifying glass button next to it. Starting from v3.1, support to switch the following two paths to use: + + ![path](./build-options/path.png) + + - **file**: The specified build path is an **absolute path**, which is the way used in previous versions. + - **project**: The specified build path is a **relative path**, and the selected path can only be in the project directory. When using this path, some path-related configurations (e.g. Icon) in the build options are recorded as relative paths, making it easier for team members to share configurations across devices. + + The default build path is the `build` folder in the project directory. If a version control system such as Git or SVN is used, the `build` folder can be ignored in version control. + + > **Note**: Spaces, illegal characters and Chinese are not allowed in the Build Path. + +- The second input box is used to specify the name of the build task when the project is built and the name of the release package generated after the build. The default is the name of the current build platform, and for each additional build of the same platform, the **-001** suffix will be added to the original one, and so on.
After the build is completed, click the folder icon behind the input box to open the project release package directory. + +### Start Scene + +It is necessary to set the first scene to enter after opening the game. One way is to search for the desired scene in the list of **Included Scenes**, move the mouse to the desired scene bar, and then click the button that appears on the right to set it as the start scene. + +![start scene](./build-options/set-start-scene.png) + +### Scenes that Participate in Build (Included Scenes) + +During the build process, all the assets and scripts in the bundle will be packaged except for the `resources` folder in the project directory and the assets and scripts in the bundle. Other assets are packaged on demand according to the scenes involved in the build and the asset referenced in the `bundle`. Therefore, removing the check box for scenes that do not need to be released can reduce the size of the project release package generated after the build. + +### Bundles + +As of v3.8, developers can decide whether a Bundle should participate in a build based on the needs of the project. + +![bundle-option](./build-options/bundle-option.png) + +- Select all: all Bundles will participate in the build. +- Uncheck it to select the Bundle you want to participate in the build from the dropdown box. + + ![bundle-select.png](./build-options/bundle-select.png) + + > Main and engine built-in internal bundles cannot be canceled. + +### Embed Shared Script in Bundle + +![embed-script-in-bundle.png](./build-options/embed-script-in-bundle.png) + +Whether to embed shared scripts in bundle when build. + +> This option is read only, and only switched with the build button changes from **Normal Build** or **Build Bundle** in the **Build** panel. + +- Unchecked: + When building a Bundle, if there are shared scripts inside the Bundle, they will not be built inside the Bundle, but the script inside the Bundle will be built separately inside bundle.js in src/chunk. Therefore, the bundles built in this way are integrated with the project, which means that such bundles are smaller in size, but cannot exist separately from the project. +- When checked: + The scripts inside the bundle are not built into the public JS library but inside the bundle. Such Bundle can be run independently from the project (because the required scripts are inside the Bundle, and Bundles referencing the same code may have duplicated parts), and can be loaded and run correctly even in other projects, but the drawback is that because the script resources are inside the Bundle, the final size of the Bundle will be larger. + +### MD5 Cache + +Adding MD5 information to all the asset file names after building can solve the problem of CDN or browser asset caching. + +After enabling, if the asset cannot be loaded, it means that the renamed new file cannot be found. This is usually caused by some third-party assets not being loaded through the `assetManager`. In this case, you can use the following method to convert the URL before loading, and the converted path can be loaded correctly. + +```typescript +const uuid = assetManager.utils.getUuidFromURL(url); +url = assetManager.utils.getUrlWithUuid(uuid); +``` + +> **Note**: after MD5 Cache is enabled on the native platform, if assets cannot be loaded, it is usually caused by some third-party assets used in C++ not being loaded through the `assetManager`. It can also be solved by converting the URL with the following code: +> +> ```cpp +> auto cx = ScriptingCore::getInstance()->getGlobalContext(); +> JS::RootedValue returnParam(cx); +> ScriptingCore::getInstance()->evalString("cc.assetManager.utils.getUrlWithUuid(cc.assetManager.utils.getUuidFromURL('url'))", &returnParam); +> +> string url; +> jsval_to_string(cx, returnParam, &url); +> ``` + +### Main Bundle Compression Type + +Set the compression type of the main bundle. For details, please refer to the [Asset Bundle — compression type](../../asset/bundle.md#compression-type) documentation. + +### Main Bundle is Remote + +This option is optional and needs to be used in conjunction with the **Resources Server Address** option. + +After checking, the main package will be configured as a remote package, and its related dependent assets will be built to the [built-in Asset Bundle — main](../../asset/bundle.md#the-built-in-asset-bundle) under the `remote` directory of the release package. The developer needs to upload the entire `remote` folder to the remote server. + +### Debug Mode + +If this option is unchecked, the build is running in release mode, compressing and obfuscating asset UUID, built engine scripts and project scripts, and subcontracting the JSON of similar assets to reduce the number of asset loadings. + +If this option is checked, the build is running in debug mode. At the same time, the **Source Maps** option can be checked, which is more convenient for locating problems. + +### Mangle Engine Internal Properties +This feature is supported since v3.8.6. When enabled, it mangles internal properties in the engine's TypeScript code, effectively reducing code size. After the project is built, an `engine-mangle-config.json` configuration file will be generated in the root directory, where users can customize and add properties to be mangled. This feature currently does not support native platforms. If the project has enabled the engine separation plugin feature, this configuration will be ignored during build. For detailed usage instructions, please refer to [Mangle Engine Internal Properties](../../advanced-topics/mangle-properties.md) + +### Inline Enums + +This feature is supported since v3.8.6. When enabled, it replaces enum values in the engine's TypeScript code with specific numerical values and disables enum reverse-mapping functionality, effectively reducing code size. If you need to generate reverse-mapping for certain enums, you can use the 'cc.Enum(your_enum)' function to decorate the corresponding enum to dynamically generate reverse-mapping. If the project has enabled the engine separation plugin feature, this configuration will be ignored during build. + +### Source Maps + +Check this option to generate sourcemap. The engine files and project scripts will be compressed by default during the build. + +As JavaScript is becoming more and more complex, most of the source code (development code) has to be compiled and converted before it can be put into the production environment, which makes the actual running code different from the source code. This makes it impossible to locate the source code during debugging. The Source Map can map the converted code to the source code, that is, the converted code corresponds to the location of the source code before the conversion. In this way, when a problem occurs, it is possible to directly view and debug the source code, making it easier to locate the problem. For details, please refer to the [Use source maps](https://developer.chrome.com/docs/devtools/javascript/source-maps/) documentation. + +### Replace Splash Screen + +When the mouse is moved to this option, the **Edit Icon** button will appear. Click this button to open the splash screen setting panel, and the data will be saved in real time after editing. + +![splash setting](build-options/splash-setting.png) + +- **TotalTime**: the total time (milliseconds) to display the inset screen, minimum 500 milliseconds +- **Image Size Multiplier**: the zoom ratio of the image, the minimum is 100% +- **Preview**: If you want to change the splash screen image, hover your mouse over the preview window and click the '+' button, then select a new image path. + +If you want to disable the splash screen, please refer to [Introduction to Build Options](./publish/build-options.md) for details. + +> **Note**: +> 1. After selecting different build platforms in the build options, adjusting the screen insertion rules again can achieve the diversity of screen insertion on different platforms +> 2. the screen insertion will only take effect after the package, not during the preview. +> 3. Some countries and regions are not open for full screen insertion, we apologize for any inconvenience caused to you. + +### Erase module structure (experimental) + +If this option is checked, the script import speed is faster, but module features such as `import.meta`, `import()`, etc. cannot be used. + +### Skip Compress Texture + +The default is 'false'. If this option is checked, the build skips the entire texture compression process to reduce build time. The command parameter is `skipCompressTexture`. + +### Engine Config + +As of 3.8.3, it is possible to configure some of the engine module configurations in the build panel, making it easier to select different physical backends for different platform or build tasks, etc. + +![Engine Config](build-options/engine-config.png) + +The following configuration changes are currently supported: + +| Configuration | Optional | Description | Default | +| --- | --- | --- | --- | +| CLEANUP_IMAGE_CACHE | `Inherit Project Setting`, `On`, `Off` | Whether to clear the original image cache after uploaded a texture to GPU | `Inherit Project Setting` | +| 2D Physics | `Inherit Project Setting`, `Box 2D`, `Built-In` | - | Inherit Project Setting | +| 3D Physics | `Inherit Project Setting`, `Bullet`, `cannon`, `PhysX`, `built-in` | - |Inherit Project Setting| +| WebGL 2.0 | `Inherit Project Setting`, `On`, `Off` | - | Inherit Project Setting| +| Bundle Mode Of Native Code | `Wasm + AsmJS`, `Wasm`, `AsmJS` | It mainly affects the native module packing format of Physics, Spine, etc., in which Wasm mode can improve the performance of games to a certain extent. Since the support degree of different game platforms is not consistent, please judge the choice according to the actual official support data. | Wasm + AsmJS| +| Enable Wasm Brotli compression | `On`, `Off` | Enabling this will reduce the size of the wasm package, which will take slightly longer to load due to the need to decompress it at runtime | Off | + +The above options will be shown or hidden according to the selection of the actual module in the project settings, e.g. `If the 2D Physical Module is not checked in the project settings, the corresponding option will not appear`. + +### Cocos Service Config Set + +This option is used to display all the services integrated in the [Service](https://service.cocos.com/document/en/) panel for the current project. + +## Related Build Options for Each Platform + +Due to the adjustment of the current build mechanism, the processing of different platforms is injected into the **Build** panel in the form of a plugin. After selecting the platform to build in the **Platform** option of the **Build** panel, notice the expanded options of the corresponding platform. The name of the expanded option is the platform plug-in name, in the editor main menu **Extensions -> Extension Manager -> Internal** to see various platform plug-ins. + +For the relevant build options of each platform, please refer to: + +- [Publish to Web Platform](publish-web.md) +- [Publish to Mini Game Platform](publish-mini-game.md) +- [Publish to Facebook Instant Games](publish-fb-instant-games.md) +- [Publish to Native Platform](native-options.md#%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) +- [Publish to Google Play](google-play/build-example-google-play.md) +- [Publish to Google Play On Games](google-play-games/index.md) +- [Publish to HarmonyOS Platform](publish-huawei-ohos.md) + +Cocos Creator supports custom extension build plugins, handled in the same way as platform plugins. For details, please refer to the [Extension Build Process](custom-build-plugin.md) documentation. + +## Configuration of Other Options Involved in the Build + +The configuration of **Project -> Project Settings** in the menu bar of the editor will affect the result of the project build. For details, please refer to the [Project Settings](../project/index.md) documentation. diff --git a/versions/4.0/en/editor/publish/build-options/build-path.png b/versions/4.0/en/editor/publish/build-options/build-path.png new file mode 100644 index 0000000000..c7af2406b8 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/build-path.png differ diff --git a/versions/4.0/en/editor/publish/build-options/bundle-option.png b/versions/4.0/en/editor/publish/build-options/bundle-option.png new file mode 100644 index 0000000000..1cc28e07c3 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/bundle-option.png differ diff --git a/versions/4.0/en/editor/publish/build-options/bundle-select.png b/versions/4.0/en/editor/publish/build-options/bundle-select.png new file mode 100644 index 0000000000..5ce18986fa Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/bundle-select.png differ diff --git a/versions/4.0/en/editor/publish/build-options/embed-script-in-bundle.png b/versions/4.0/en/editor/publish/build-options/embed-script-in-bundle.png new file mode 100644 index 0000000000..d2885d3322 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/embed-script-in-bundle.png differ diff --git a/versions/4.0/en/editor/publish/build-options/engine-config.png b/versions/4.0/en/editor/publish/build-options/engine-config.png new file mode 100644 index 0000000000..1199c7cd77 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/engine-config.png differ diff --git a/versions/4.0/en/editor/publish/build-options/options.png b/versions/4.0/en/editor/publish/build-options/options.png new file mode 100644 index 0000000000..9feedda14b Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/options.png differ diff --git a/versions/4.0/en/editor/publish/build-options/path.png b/versions/4.0/en/editor/publish/build-options/path.png new file mode 100644 index 0000000000..28dc222608 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/path.png differ diff --git a/versions/4.0/en/editor/publish/build-options/set-start-scene.png b/versions/4.0/en/editor/publish/build-options/set-start-scene.png new file mode 100644 index 0000000000..3749b39e45 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/set-start-scene.png differ diff --git a/versions/4.0/en/editor/publish/build-options/splash-setting.png b/versions/4.0/en/editor/publish/build-options/splash-setting.png new file mode 100644 index 0000000000..be24ce2a99 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/splash-setting.png differ diff --git a/versions/4.0/en/editor/publish/build-options/start_scene.png b/versions/4.0/en/editor/publish/build-options/start_scene.png new file mode 100644 index 0000000000..0d3583c299 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-options/start_scene.png differ diff --git a/versions/4.0/en/editor/publish/build-panel.md b/versions/4.0/en/editor/publish/build-panel.md new file mode 100644 index 0000000000..cdfb53af21 --- /dev/null +++ b/versions/4.0/en/editor/publish/build-panel.md @@ -0,0 +1,129 @@ +# About the Build Panel + +Click **Project -> Build** in the main menu of the editor or use the shortcut key `Ctrl/Cmd + Shift + B` to open the **Build** panel: + +![build-panel](./build-panel/build-panel.png) + +If a platform has already been built, open the **Build** panel to enter the **Build Task** page. The build of each platform for v3.0 is carried out in the form of a build task, similar to a download task: + +![panel](./build-panel/panel.png) + +## Build Panel + +In the **Build** panel, select the platform to be built, and then configure the [build options](build-options.md). After the configuration is complete, click the **Build** button in the lower right corner to jump to the **Build Task** page to execute the build process. Another way is to click the **Close (X)** button in the upper right corner to enter the **Build Task** page. + +There are three function buttons at the top of the panel: + +![build-options](./build-panel/build-options.png) + +- ![doc](./build-panel/doc.png): click this button to jump to the official manual documentation of the current platform. + +- **Import**: click this button to import the JSON file that saves the configuration of the build options. + +- **Export**: click this button to export the current platform's build option configuration as a JSON file for the [command line build](publish-in-command-line.md), or share it among project members. The exported configuration is differentiated according to the platform. When using the command line to build, specify the file path of the build parameter `configPath` as the exported JSON configuration file path. + +> **Notes**: +> +> 1. It is meaningless to build a project without a scene, if there is no scene in the currently opened project, a prompt will appear to create a scene first when opening the **Build** panel: +> +> ![save-scene](./build-panel/create-scene-first.png) +> +> 2. Before building, please make sure that the current scene has been saved, otherwise when click the **Build** button, and a pop-up prompt will be displayed. Choose **Save**, **Ignore** or **Cancel**. Select **Save** and **Ignore** to continue the build process. +> +> ![save-scene](./build-panel/save-scene.png) + +## Build Tasks + +On the **Build Tasks** page, developers can view the current platform's build progress and build results. + +- **Building**: the progress bar is displayed as **blue**. +- **Build success**: the progress bar reaches 100%, and the actual build time is output and displayed as **green**. +- **Build failed**: the progress bar reaches 100%, prompting the reason for the failure of the build or an error message, and it is displayed as **red**. + +![panel](./build-panel/build-page.png) + +There are three buttons at the top of the page, including **New Build Task**, **Open Build DevTools** and **Clear Build Cache**: + +- **New Build Task**: click this button to return to the **Build** panel, select a new platform to build. + +- ![debug](./build-panel/debug.png): click this button to open the Build DevTools, and view all the log information generated during the build process, including the call stack. + +- ![clean](./build-panel/clean.png): clear the build cache. In order to reuse the reusable build results, and in order to speed up the build and reduce memory usage when rebuilding, many processes in the build process have added cache management mechanisms, such as compressed textures, automatic atlas generation, engine compilation, and resources being serialized JSON, etc.
Under normal circumstances, this part of the cached data does not need to be manually cleaned, but if it is needed to avoid cache interference under special circumstances, click this button to clear the cached data. + + The project-related resource cache will be stored in the project directory, and the engine compilation-related cache will be stored in the global directory. Developers can choose to clear the project cache, the global cache, or all of them according to their needs. + + ![clean window](./build-panel/clean-window.png) + +### Platform Build Tasks + +The build of each platform is carried out in the form of a **build task**, similar to a download task. The name of the platform build task depends on the **Build Path** option in the **Build** panel. For details, please refer to the [Build Options](build-options.md) documentation. + +![build task](./build-panel/build-task.png) + +Each building task has corresponding function buttons for easy use. + +**The remove (X) button at the top right of the build task** is used to remove the current build task. Choose **Remove records only** or **Remove source files**. **Remove source files** means to delete the project release package generated in the `build` directory after the corresponding platform is built. + +![remove build task](./build-panel/remove-build-task.png) + +**The buttons at the bottom left of the build task include**: + +- ![folder](./build-panel/folder.png): click this button to open the project release package generated after the corresponding platform is built (the default is in the `build` directory). + +- ![editing](./build-panel/editing.png): click this button to return to the **Build** panel, modify the build options configured during the last build of the corresponding platform, and then click the **Build** button at the bottom right to rebuild. For details, please refer to the content in the **Modify Build Options** section below. + +- ![setting](./build-panel/setting.png): click this button to return to the **Build** panel to view the build options configured during the last build of the corresponding platform. + +- ![log-file](./build-panel/log-file.png): click this button to open the log file generated by the corresponding platform during the build process. For more information, please refer to the content in the **Building Log Information View** section below. + +**The button at the bottom right of the build task** is mainly used for each platform to execute the release process such as generation, operation, and upload according to the platform requirements after the completion of the build. The **Build** button is used to rebuild. + +After each platform is built, the build options configuration information related to the build will be saved in the `profiles/v2/packages/builder.json` file in the project directory, as long as the build of the corresponding platform is not deleted on the **Build Tasks** page or delete the project release package in the `build` directory. To view the build option configuration of the last build after reopening the editor, and continue to run the preview, etc. + +For the specific release process of each platform, please refer to: + +- [Publish to Native platform](native-options.md) +- [Publish to Mini Game platform](publish-mini-game.md) + +### Modifying Build Options + +Click the edit button at the bottom left of the build task to return to the **Build** panel to modify the build options configured during the last build for rebuilding. Since it is only possible to modify the build option configuration during the last build of the current platform, the **Platform** option on the page is grayed out and cannot be modified. + +![edit build option](./build-panel/edit-build-option.png) + +Click the ![setting](./build-panel/setting.png) button to the right of the edit button to view the configuration of the build options during the last build of the current platform. + +After the modification is completed, clicking the **Build** button will clear the project release package generated after the last build and rebuild. Or click the **X** button at the top right of the **Build** panel to return to the **Build Tasks** page, and then click the **Build** button at the bottom right of the platform build task to rebuild. + +> **Note**: in order to avoid accidentally deleting customized content, the native platform only updates project resources when rebuilding, and does not overwrite the original native project content. Therefore, when returning to the **Build** panel to modify the previously configured build options, the native platform-related build options are in the **disabled** state. To regenerate the project, please create a new build task. + +If the **Build** button is not clicked to rebuild after modifying the configuration, the modified configuration will also be saved. If the configuration in the current **Build** panel is inconsistent with the configuration in the project release package in the `build` directory generated after the last build, a yellow * key will be displayed at the top of the **Build** panel. + +![settings](build-panel/settings.png) + +### Build Log Information View + +Since the build process will generate a lot of log information, only error messages will be printed to the editor's **console** panel by default. + +To view all the log information, there are the following operations: + +- **Open the Build DevTools** + + To view, click **Developer -> Open Build DevTools** in the main menu or click the ![debug](./build-panel/debug.png) button at the top right of the **Build Task** page. All log information printed during the build process includes the call stack. + +- **Open the build logging file** + + The error message generated during each build process will be recorded and stored in the `temp/builder/log` folder under the project directory. Click the ![log-file](./build-panel/log-file.png) button at the bottom right of the build task to view it. When feedback on build related issues, directly paste the file to locate the problem. + +### Build Queue and Interrupt Build + +![interrupt](./build-panel/interrupt.gif) + +You can still add a new build task during the build process, and the new build task will be added to the build queue. +During the build process, you can click the X button on the right side of the build task to interrupt the current build task. + +### Build Bundle Only + +![build-dropdown.png](./build-panel/build-dropdown.png) + +Cocos Creator supports **Build Bundles Only** since v3.8. All selected Bundles can be built at once when building the project. Unlike building individual Bundles, the **Build Bundles Only** option will build all Bundles and store public assets in high-priority Bundles. diff --git a/versions/4.0/en/editor/publish/build-panel/build-dropdown.png b/versions/4.0/en/editor/publish/build-panel/build-dropdown.png new file mode 100644 index 0000000000..d281ef19b0 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/build-dropdown.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/build-options.png b/versions/4.0/en/editor/publish/build-panel/build-options.png new file mode 100644 index 0000000000..2b6d7d4032 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/build-options.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/build-page.png b/versions/4.0/en/editor/publish/build-panel/build-page.png new file mode 100644 index 0000000000..27601c7e69 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/build-page.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/build-panel.png b/versions/4.0/en/editor/publish/build-panel/build-panel.png new file mode 100644 index 0000000000..bdcdef7e19 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/build-panel.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/build-task.png b/versions/4.0/en/editor/publish/build-panel/build-task.png new file mode 100644 index 0000000000..b4f9c55041 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/build-task.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/clean-window.png b/versions/4.0/en/editor/publish/build-panel/clean-window.png new file mode 100644 index 0000000000..c5c1f14847 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/clean-window.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/clean.png b/versions/4.0/en/editor/publish/build-panel/clean.png new file mode 100644 index 0000000000..dbb3eb80e5 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/clean.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/clear-build-data.png b/versions/4.0/en/editor/publish/build-panel/clear-build-data.png new file mode 100644 index 0000000000..18787d3ee9 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/clear-build-data.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/create-scene-first.png b/versions/4.0/en/editor/publish/build-panel/create-scene-first.png new file mode 100644 index 0000000000..00fb132de7 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/create-scene-first.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/debug.png b/versions/4.0/en/editor/publish/build-panel/debug.png new file mode 100644 index 0000000000..a79e2c802e Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/debug.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/doc.png b/versions/4.0/en/editor/publish/build-panel/doc.png new file mode 100644 index 0000000000..93174e837e Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/doc.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/edit-build-option.png b/versions/4.0/en/editor/publish/build-panel/edit-build-option.png new file mode 100644 index 0000000000..fd2a7a2eb9 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/edit-build-option.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/editing.png b/versions/4.0/en/editor/publish/build-panel/editing.png new file mode 100644 index 0000000000..d1f1672fd5 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/editing.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/folder.png b/versions/4.0/en/editor/publish/build-panel/folder.png new file mode 100644 index 0000000000..ebd78f5c37 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/folder.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/interrupt.gif b/versions/4.0/en/editor/publish/build-panel/interrupt.gif new file mode 100644 index 0000000000..53fd0ba02c Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/interrupt.gif differ diff --git a/versions/4.0/en/editor/publish/build-panel/log-file.png b/versions/4.0/en/editor/publish/build-panel/log-file.png new file mode 100644 index 0000000000..114b419cfa Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/log-file.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/log.jpg b/versions/4.0/en/editor/publish/build-panel/log.jpg new file mode 100644 index 0000000000..4a08f8346c Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/log.jpg differ diff --git a/versions/4.0/en/editor/publish/build-panel/options.png b/versions/4.0/en/editor/publish/build-panel/options.png new file mode 100644 index 0000000000..4142fb45ba Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/options.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/panel.png b/versions/4.0/en/editor/publish/build-panel/panel.png new file mode 100644 index 0000000000..d30372f059 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/panel.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/remove-build-task.png b/versions/4.0/en/editor/publish/build-panel/remove-build-task.png new file mode 100644 index 0000000000..2df63897c9 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/remove-build-task.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/save-scene.png b/versions/4.0/en/editor/publish/build-panel/save-scene.png new file mode 100644 index 0000000000..b9b16afaeb Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/save-scene.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/setting.png b/versions/4.0/en/editor/publish/build-panel/setting.png new file mode 100644 index 0000000000..3f1796e726 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/setting.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/settings.png b/versions/4.0/en/editor/publish/build-panel/settings.png new file mode 100644 index 0000000000..9b358ab52a Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/settings.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/subpackage/subpackage-config.png b/versions/4.0/en/editor/publish/build-panel/subpackage/subpackage-config.png new file mode 100644 index 0000000000..4e4f2ee0cd Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/subpackage/subpackage-config.png differ diff --git a/versions/4.0/en/editor/publish/build-panel/view_build_parameter.png b/versions/4.0/en/editor/publish/build-panel/view_build_parameter.png new file mode 100644 index 0000000000..db83c1cf16 Binary files /dev/null and b/versions/4.0/en/editor/publish/build-panel/view_build_parameter.png differ diff --git a/versions/4.0/en/editor/publish/custom-build-plugin.md b/versions/4.0/en/editor/publish/custom-build-plugin.md new file mode 100644 index 0000000000..d42c5305c8 --- /dev/null +++ b/versions/4.0/en/editor/publish/custom-build-plugin.md @@ -0,0 +1,497 @@ +# Extending Build Process + +To build a platform plug-in a common editor plug-in format is required. For the basic structure of the plug-in, please refer to the [First Extension](../extension/first.md) documentation . To extend the build function, it is necessary to understand the overall process of the build. Please read the [Introduction to the build process and FAQ guide](./build-guide.md) documentation. + +## Quick start + +1. Click **Project -> New Build Extension** in the menu bar of the editor, and select **Global**/**Project** to create a build extension package. + + - If selecting **Global**, the build extension will be applied to all Cocos Creator projects. The path of **Global** is: + + - **Windows**: `%USERPROFILE%\.CocosCreator\extensions` + + - **macOS**: `$HOME/.CocosCreator/extensions` + + - If selecting **Project**, this will apply the build extension to the specified Cocos Creator project. The path of **Project** is: + + - `$Your project address/extensions` + +2. After the build extension is created, notice the generation path of the plugin in the **Console**. Click on the path to open the build extension package in the file manager of the operating system. + + ![console](./custom-project-build-template/console.png) + +3. Before enabling the build extension, execute `npm install` in the directory to install some dependent **@types** modules to compile normally. The interface definition that comes with the editor has been generated under the **@types** folder in the root directory. **Developer -> Export.d.ts** from the menu bar of the editor shows the latest interface definitions. + +4. Click **Extension -> Extension Manager** in the menu bar of the editor to open the **Extension Manager** panel. Then select the **Project**/**Global** tab in the **Extension Manager**, and click the **Refresh Icon** button to see the build extension just added. Then click the **Enable** button on the right to run the plug-in normally. + + ![enable-plugin](./custom-project-build-template/enable-plugin.png) + +5. After the build extension is enabled, open the **Build** panel, notice the expansion bar of the build extension plugin. Click **Build** to join the build process. + + ![plugin-template](./custom-project-build-template/plugin-template.png) + +6. To modify the content of the built extension, directly modify the build extension package under the `extensions` directory, review the `readme.md` file in the build extension package directory for details. Find the corresponding build extension in the **Extension Manager**, and click the **Reload** icon button. The extension in the editor will re-run with the latest code and files. + + ![reload](./custom-project-build-template/reload.png) + +## Basic configuration process + +To extend the build function of the plug-in, add the `builder` field to the `contributions` in `package.json`, and the relative path configuration of the corresponding module can be passed to the specified platform in the field. + +Example `package.json`: + +```json +// package.json + +{ + "contributions": { + "builder": "./dist/builder" + } +} +``` + +> **Note**: the `builder` field specifies the `./dist/builder.js` start script is the compiled script, and the source file of the start script is located in `./source/builder.ts`. To configure the start script, change it in the source file. + +### Start script configuration + +The plugin entry configuration code example is shown below: + +```ts +// builder.ts + +// Allow external developers to replace parts of the build asset handler module. Please refer to the "Custom Texture Compression Processing" section below for details. +export const assetHandlers: string = './asset-handlers'; + +export const configs: IConfigs = { + 'web-mobile': { + hooks: './hooks', + options: { + remoteAddress: { + label: 'i18n:xxx', + render: { + ui: 'ui-input', + attributes: { + placeholder: 'Enter remote address...', + }, + }, + // Validation rules, there are currently several commonly used validation rules built in, and the rules that need to be customized can be configured in the "verifyRuleMap" field + verifyRules: ['require', 'http'], + }, + enterCocos: { + label: 'i18n:cocos-build-template.options.enterCocos', + description: 'i18n:cocos-build-template.options.enterCocos', + default: '', + render: { + // Please click "Developer -> UI Components" in the menu bar of the editor to view a list of all supported UI components. + ui: 'ui-input', + attributes: { + placeholder: 'i18n:cocos-build-template.options.enterCocos', + }, + }, + verifyRules: ['ruleTest'] + } + }, + verifyRuleMap: { + ruleTest: { + message: 'i18n:cocos-build-template.ruleTest_msg', + func(val, option) { + if (val === 'cocos') { + return true; + } + return false; + } + } + } + }, +}; +``` + +Please pay extra attention to the following points when writing start scripts: + +1. The environment variables in different processes will be different. The start script will be loaded by the rendering process and the main process at the same time, do not use the editor interface that only exists in a single process in the start script. + +2. There are two ways to configure the key of `config`: + + - One is for a single platform configuration, and the key is filled in as **platform plugin name** (available in the editor menu bar **Extensions -> Extension Manager -> Internal** to view the platform plug-in name). + + - One is the configuration for all platforms, the key is filled in as `*`. These two configuration methods are mutually exclusive, please do not use them in the same build extension package. + + > **Note**: these two configuration methods are mutually exclusive, please do not use both in the same build extension package. Otherwise the configuration for a single platform (key value `platform build plugin name`) will overwrite the configuration for all platforms (key value `*`). + +## Customizing Build Panel Options + +There are currently two ways to add build options to the interface: **options automatic rendering configuration method** and **panel custom panel configuration method** (>=3.8.2). The former automatically renders the content of the options configuration in the build panel and performs data update operations after some operations, while the latter adds a panel configuration to place the content of a custom panel, and the build will use this configuration to render with `ui-panel`. +To facilitate testing, this article will use `web-mobile` as an example to demonstrate how to add new options to the build panel and display them. + +### Options Automatic Rendering Configuration Method + +Create a new `src/builder.ts` script file and write the following code in `builder.ts`: + +```ts +import { BuildPlugin, IBuildTaskOption } from "../@types/packages/builder/@types"; + +export const load: BuildPlugin.load = function() { + console.debug('custom-build-example load'); +}; + +export const unload: BuildPlugin.load = function() { + console.debug('custom-build-example unload'); +}; + +export const configs:BuildPlugin.Configs = { + 'web-mobile': { + options: { + testInput: { + label: 'testVar', + description: 'this is a test input.', + default: '', + render: { + ui: 'ui-input', + attributes: { + placeholder: 'Enter numbers', + }, + }, + verifyRules: ['required','ruleTest'] + }, + testCheckbox: { + label: 'testCheckbox', + description: 'this is a test checkbox.', + default: false, + render: { + ui: 'ui-checkbox', + }, + }, + }, + verifyRuleMap: { + ruleTest: { + message: 'length of content should be less than 6.', + func(val: any, option: IBuildTaskOption) { + if (val.length < 6) { + return true; + } + return false; + } + } + } + }, +}; +``` + +In the above `configs`, we define two parameters: + +- `testInput`: string - String variable, modified by input box +- `testCheckbox`: boolean - Boolean variable, modified by checkbox + +The meanings of each field in the configuration of build options are as follows: + +- `label`: string - Required, the name of this parameter displayed on the interface, supports `i18n:key` configuration +- `description`: string - Optional, brief description information, used for displaying hints when the mouse hovers over the label, supports `i18n:key` configuration +- `default`: any - Optional, the default value of the parameter +- `render`: {} - Required, configure information about the rendered component + - `ui`: string - Required, UI component name, please refer to the documentation [UI components](../extension/ui.md) + - `attributes`: {} - Optional, properties required by the UI component, please refer to the documentation [UI components](../extension/ui.md) + +- `verifyRules`: [] - Optional, parameter verification rules +- `verifyRuleMap`: [] - Optional, custom parameter verification rule functions. Refer to the section below on [parameter validation rules](#rules-for-customizing-build-panels) + +After executing `npm run build` to compile this extension and refresh it, open the build panel and you can see two additional build parameters at the end of the web-mobile build task, as shown in the following figure: + +![custom-build-example-options](./custom-build-plugin/custom-build-example-options.png) + +### Panel customization method (>=3.8.2) + +Create a new `src/builder.ts` script file and write the following code in `builder.ts`: + +```ts +import { BuildPlugin, IBuildTaskOption } from "../@types/packages/builder/@types"; + +export const configs:BuildPlugin.Configs = { + 'web-mobile': { + panel: './panel', + } +}; + ``` + +Create a new `src/panel.ts` script file and write the following code in `panel.ts`: + +```ts +import { ICustomPanelThis, ITaskOptions } from '../@types'; +import { PACKAGE_NAME } from './global'; +let panel: ICustomPanelThis; + +export const style = ``; + +export const template = ` +
+ + + + +
+`; + +export const $ = { + root: '.build-plugin', + hideLink: 'ui-checkbox', + link: '#link', +}; + +/** + * all change of options dispatched will enter here + * @param options + * @param key + * @returns + */ +export async function update(options: ITaskOptions, key: string) { + if (key) { + return; + } + // when import build options, key will bey '' + init(); +} + +export function ready(options: ITaskOptions) { + // @ts-ignore + panel = this as ICustomPanelThis; + panel.options = options; + init(); +} + +export function close() { + panel.$.hideLink.removeEventListener('change', onHideLinkChange); +} + +function init() { + panel.$.hideLink.value = panel.options.hideLink; + updateLink(); + panel.$.hideLink.addEventListener('change', onHideLinkChange); +} + +function onHideLinkChange(event: any) { + panel.options.hideLink = event.target.value; + // Note: dispatch the change to build panel + panel.dispatch('update', `packages.${PACKAGE_NAME}.hideLink`, panel.options.hideLink); + updateLink(); +} + +function updateLink() { + if (panel.options.hideLink) { + panel.$.link.style.display = 'none'; + } else { + panel.$.link.style.display = 'block'; + } +} +``` + +#### Rules for customizing build panels + +The above code comes from a simple example code in the template of a custom build plugin. You can choose other front-end frameworks to develop the interface more conveniently according to your preferences. Just pay attention to the following rules: + + 1. The file content rules of the panel are rendered by ui-panel, and need to follow the relevant component rules, which basically refer to the variables or lifecycle hooks exposed in the above example code. + 2. The `ready` function of the panel will be called after the panel has been loaded. According to needs, you can do some dom element initialization, event binding, or some front-end framework initialization binding in this function. + 3. The `close` function of the panel will be called when the panel is closed. According to needs, you can do some event unregistration, object destruction, etc. in this function. + + 4. The `update` function of the panel will be called whenever any build option changes (including those sent by itself). If necessary, you can add this hook here. At the same time, when the build panel has an action of importing configuration, you can also do some updating of the customization panel in this function. At this time, `key` is an empty string. + + 5. If the interaction in the customized panel involves updating the build option, including the data update that may be done during the initialization process, you must use `panel.dispatch('update', key, value, error)` to perform the update, where `key` needs to follow the format of `packages.${PACKAGE_NAME}.key`. Only after calling it will the data be synchronized to the final build option when building. Sending an error mainly affects the disablement status of the build button, and error is an optional parameter. + + 6. If the interaction in the customized panel involves reading the build option, you need to use `panel.options.key` + +### Start script interface definition + +The detailed interface definition is described as follows: + +```ts +declare type IConfigs = Record; +declare interface IBuildPlugin { + hooks?: string; // Storage path of hook function + options?: IDisplayOptions; // Platform parameter configuration that needs to be injected + verifyRuleMap?: IVerificationRuleMap; // Register parameter verification rule function +} +declare type IDisplayOptions = Record; +declare interface IConfigItem { + // The default value, the registered default value will be in the "options.[platform].xxx" field in the plugin configuration + default?: any; + + render: ?{ + // The rules for rendering UI components are consistent with the unified rules at "ui-prop". Only configurations with UI properties specified will be displayed on the Build panel + ui?: string; + // The configuration parameters passed to the UI component + attributes?: IUiOptions; + }; + + // Configure the displayed name, if you need to translate, then pass in "i18n:${key}" + label?: string; + + // A brief description of the setting, which will be displayed on the title when the mouse hovers over the configuration name. + description?: string; + + // Type of configuration + type?: 'array' | 'object'; + + // If type is an array, the data will be rendered according to the specified data type and "itemConfigs" + itemConfigs?: Record | IConfigItem[]; +} + +declare interface IUiOptions extends IOptionsBase { + // Validation rules array, build provides some basic rules, and you can also specify new validation rules through “verifyRuleMap”. Only when pass in “require” will be a valueless checksum, otherwise only when there is a value. + verifyRules?: string[]; +} + +declare interface IUiOptions extends IOptionsBase { + class?: string | string[]; // The name of the style that needs to be set on the current "ui-prop" +} +``` + +For the interface definition of `IOptionsBase` please refer to [ui-prop automatic rendering rule definition](../extension/ui.md). + +## Custom build hook function code configuration + +In the script module defined by the hooks field in the entry configuration, hook functions can be written that build the life cycle. In different hook functions, the data received will be different. All hook functions run in the build process, and the engine method can be used directly in the build process. + +The relationship between the public hook function and the life cycle of the build can be seen in the following figure: + +![build-process](./custom-project-build-template/build-process.jpg) + +The rough interface definition of hook function is as follows: + +```ts +declare interface IHook { + throwError?: boolean; // The hook function injected by the plugin, whether to exit the build process directly and show the build failure when the execution fails. + // ------------------ hook function -------------------------- + onBeforeBuild?: IBaseHooks; + onBeforeCompressSettings?: IBaseHooks; + onAfterCompressSettings?: IBaseHooks; + onAfterBuild?: IBaseHooks; + + // Compile the generated hook function (only valid if the platform's build process has a "Make" step) + onBeforeMake?: (root: string, options: IBuildTaskOptions) => void | Promise; + onAfterMake?: (root: string, options: IBuildTaskOptions) => void | Promise; +} +type IBaseHooks = (options: IBuildTaskOptions, result?: IBuildResult) => void | Promise; +``` + +> **Notes**: +> 1. the `result` parameter can be accessed only at the beginning of `onBeforeCompressSettings`, and the `options` passed to the hook function is a copy of the `options` used in the actual build process, and only used as a reference for information acquisition, so directly modifying it does not really affect the build process, although it can be modified successfully. To modify the build parameters, please set in the `options` field of the entry configuration code. Due to the numerous interface definitions, you can refer to the `@types/packages/builder` folder in the build extension package for detailed interface definitions. +> 2. The hook function is allowed to be an asynchronous function, and the build will await the completion of the hook function execution before executing the next process by default. + +A simple example: + +```ts +export function onBeforeBuild(options) { + // Todo some thing... +} +export async function onBeforeCompressSettings(options, result) { + // Todo some thing... +} +``` + +### Custom texture compression processing + +The `assetHandler` path configuration specified in the **start script configuration** above allows external developers to register some asset handling functions to replace the engine's handler module when building partial assets. Currently only **texture compression** handler registration is available. + +Creator provides its own compression tools to handle compressed texture assets at build time, but does not focus on image compression because it needs to be compatible with different user environments and usually the compression tools are chosen to work on most computers rather than the most efficient ones. Therefore, Creator has opened up a plug-in mechanism in v3.4, **which allows users to directly register compression processing functions for the corresponding texture assets, which will be called at the appropriate processing time when building**. + +The specific steps are as follows: + +1. In the start script, write the relative path of the `assetHandlers` module script: + + ```ts + export const assetHandlers = './asset-handlers'; + ``` + +2. In the `assetHandlers` script module, the `compressTextures` function has been opened up for developers to write the corresponding handler function directly in `compressTextures`, which will be called during the texture compression phase of the build. + + The handler function takes the current array of remaining unprocessed texture compression tasks and removes them from the original array when processing is complete. Texture compression tasks that are not removed are considered unprocessed and are placed in the next corresponding processing function until all processing functions have been processed, and if there are still unprocessed texture compression tasks, they are placed back in the Creator's original texture compression process. + + When there are multiple plugins registered with texture compression handler functions, they are executed in the order in which the plugins are started. If the previous plugin processes all the texture compression tasks, the subsequent plugins registered with the handler functions will not receive the tasks. + + The code example is as follows: + + ```ts + type ITextureCompressType = + | 'jpg' + | 'png' + | 'webp' + | 'pvrtc_4bits_rgb' + | 'astc_12x12'; // See interface definition for detailed format + interface ICompressTasks { + src: string; // Source file address + dest: string; // Address of the generated target file (default suffix is PNG, other types need to be changed manually) + quality: number | IPVRQuality | IASTCQuality | IETCQuality; // Compression quality 0 - 100 or other compression levels + format: ITextureCompressType; // Compression type + } + export async function compressTextures(tasks: ICompressTasks[]) { + for (let i = 0; i < Array.from(tasks).length; i++) { + const task = Array.from(tasks)[i]; + if (task.format ! == 'jpg') { + // Texture compression tasks that are skipped are passed to the next processing function until they finally enter the Creator's original build-time texture compression process + continue; + } + task.dest = task.dest.replace('.png', '.jpg'); + await pngToJPG(task.src, task.dest, task.quality); + // Remove the finished texture compression task from tasks, so that it will not be processed again when building + tasks.split(i, 1); + } + } + ``` + +## Debugging build extension plugins + +When the build extension plugin is involved in the build process, the associated code runs in the following three processes: + +- **Main Process**: executes the start script and its dependent assets. +- **Rendering Process**: executes some of the fields registered in the start script to the **Build** panel. +- **Build Process**: executes the script defined in the `hooks` field of the start script. + +### Main Process (Start Script) + +The main process mainly executes the start script used in the build extension plugin to participate in the build process (the script specified in the `builder` field), and the plugin's own start script (the script specified in the `main` field). + +When the code running in the main process is modified, the plugin must be restarted and then the process to be updated must be refreshed (this will be optimized later to try to solve the code update problem with a single restart, but refreshing is still the most thorough reloading method). The main process currently does not have a more appropriate debugging method, you can use the command line to open the editor to view the main process code log to assist debugging: + +```bash +// Mac +/Applications/CocosCreator/Creator/3.0.0/CocosCreator.app/Contents/MacOS/CocosCreator --project projectPath + +// Windows +... \CocosCreator.exe --project projectPath +``` + +### Rendering Process (Build Panel) + +The start script of the build extension plugin has some fields that are registered to the **Build** panel, such as the display configuration of `options`, the `panel` field, and the `panel` script itself, which is loaded and executed in the render process. The rendering process is actually the window's own execution process. Open the DevTools to debug the `dom` elements, styles, scripts, etc. on the **Build** panel. + +If the code registered to the **Build** panel is modified, refresh the panel without restarting the plugin. + +- **Open the DevTools for the rendering process of the Build panel** + + Click on the **Build** panel and press the shortcut **Ctrl + Shift + I** (Windows) or **Command + Option + I** (Mac) to open the DevTools for the **Build** panel. + +- **How to reload (refresh) the panel** + + Press **Ctrl/Command + R** after clicking on the **Build** panel or the DevTool in the **Build** panel. + +### Build Process (`hooks` Script) + +The actual execution phase of the build is a separate worker process, ensuring that even if an abnormal crash occurs, it will not affect the normal use of other windows. The scripts defined in the `hooks` field of the start script are also loaded and executed in this separate worker process. + +If only the script defined in the `hook` field is modified, the build process can be refreshed without restarting the plugin. To do this, press **Ctrl/Command + R** after opening the build DevTools, as in the **Build** panel above. + +#### Opening the DevTools for the build process + +This includes the following three ways: + +1. Click the **Open Build DevTools** button at the top right of the build task window in the **Build** panel. + + ![dev_tools](./custom-project-build-template/dev_tools.png) + +2. Click **Developers -> Open Build DevTools** in the main editor menu to open it. + +3. In any plugin code or in the console, execute the following code: + + ```ts + Editor.Message.send('builder', 'open-devtools'); + ``` + + `Editor.Message` can be extended to suit additional needs. For example, developers can catch errors in the code written to build the plugin, and automatically open DevTools once there is an exception or something like that. diff --git a/versions/4.0/en/editor/publish/custom-build-plugin/cocos-build-template-menu.png b/versions/4.0/en/editor/publish/custom-build-plugin/cocos-build-template-menu.png new file mode 100644 index 0000000000..879cbd9f23 Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-build-plugin/cocos-build-template-menu.png differ diff --git a/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-compress-tex-option.png b/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-compress-tex-option.png new file mode 100644 index 0000000000..bcfb711f1d Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-compress-tex-option.png differ diff --git a/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-example-hooks-log.png b/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-example-hooks-log.png new file mode 100644 index 0000000000..0adba31ae8 Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-example-hooks-log.png differ diff --git a/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-example-options.png b/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-example-options.png new file mode 100644 index 0000000000..6679e0a73e Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-example-options.png differ diff --git a/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-example-validator-errormsg.png b/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-example-validator-errormsg.png new file mode 100644 index 0000000000..da777f98ec Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-build-plugin/custom-build-example-validator-errormsg.png differ diff --git a/versions/4.0/en/editor/publish/custom-project-build-template.md b/versions/4.0/en/editor/publish/custom-project-build-template.md new file mode 100644 index 0000000000..bcc9c87102 --- /dev/null +++ b/versions/4.0/en/editor/publish/custom-project-build-template.md @@ -0,0 +1,74 @@ +# Custom Project Build Template + +**Cocos Creator** supports custom build templates for each project. Add a `build-templates/[platform]` folder to the project path, and divide the sub-folder according to the **platform name**. Then all the files in this folder will be automatically **copied** to the build generated project according to the corresponding folder structure after the build. Currently, all platforms except the native platform support this function, the specific **platform name** can be referred to as the following **custom build template supported platform list**. + +Folder Structure: + +```bash +project-folder + |--assets + |--build + |--build-templates + |--web-mobile + // The file to be added, such as index.html + |--index.html +``` + +If the current platform is `Web-Mobile`, then `build-templates/web-mobile/index.html` will be copied to `build/web-mobile/index.html`. + +In addition, the file types currently supported by the build template include **ejs type** and **json type**. These two types will not copied directly but rather parsed. Please refer to the **Special Custom Build Template Platform support list** below for details on the support for these two template types by platform. + +## `ejs` type + +Since the content of the package is not guaranteed to be exactly the same in every version, when the build template within the editor is updated, the developer also needs to update the build template within their project. For example, if the MD5 Cache option is checked at build time, taking `index.html` on the web platform as an example, the `css` file referenced in it will have an MD5 Hash suffix, which may not match the one in the original template and may not work.
+To optimize this problem, a new way is added to use the template. Click on **Project -> Create Preview Template** in the main menu, and an `ejs` template file will be generated for the corresponding platform. + +![build template](custom-project-build-template/build-template.png) + +Developers only need to customize the generated build template in the `.ejs`, the build will automatically synchronize the updates of the editor build template to the custom build template, and the frequently changed content will be synchronized to the sub-template (`.ejs`) referenced by the template, so that the custom build template can be updated manually without frequent updates. + +Taking the creation of a Web Mobile build template as an example, the generated build template directory structure is as follows: + +![web-mobile](custom-project-build-template/web-mobile.png) + +> **Note**: the copy template occurs after the rendered template. For example, if both `index.ejs` and `index.html` exist in this directory, the final packaged package will be the `index.html` file instead of the `index.ejs` rendered file. + +### `json` Type + +Many mini games have their own configuration `JSON` files, like `game.json` to WeChat Mini Games. Files in the build templates folder will just copy in **default**, but this configuration JSON will be merged instead of overwrite. Of course, it doesn't mean that all `JSON` file will be merged, you can check it in the tables below. + +## Special Custom Build Template supported Platform list + +Most files placed in the `build-templates/[platform]` directory will be copied directly to the corresponding directory. In addition, many platforms support some template files with special name formats. The supported file with special name formats for build templates by the platform are as follows: + +| Platform | Actual Name | Supported File Type | +| :-------- | :---------- | :----------- | +| **Huawei AGC** | huawei-agc | `index.ejs` | +| **Alipay Mini Game** | alipay-mini-game | `game.json` | +| **Taobao Mini Game** | taobao-mini-game | `game.json`、`mini.project.json` | +| **Douyin Mini Game** | bytedance-mini-game | `game.ejs`、`game.json`、`project.config.json` | +| **OPPO Mini Game** | oppo-mini-game | `manifest.json` | +| **Huawei Quick Game** | huawei-quick-game | `game.ejs` | +| **Cocos Play** | cocos-play | `game.config.json` | +| **vivo Mini Game** | vivo-mini-game | `project.config.json` | +| **Baidu Mini Game** | baidu-mini-game | `game.json`, `project.swan.json` | +| **WeChat Mini Game** | wechatgame | `game.ejs`, `game.json`, `project.config.json` | +| **Web Desktop** | web-desktop | `index.ejs` | +| **Web Mobile** | web-mobile | `index.ejs` | +| **Native** | native | `index.ejs` | + + \ No newline at end of file diff --git a/versions/4.0/en/editor/publish/custom-project-build-template/build-process-public.drawio b/versions/4.0/en/editor/publish/custom-project-build-template/build-process-public.drawio new file mode 100644 index 0000000000..475c2ed70c --- /dev/null +++ b/versions/4.0/en/editor/publish/custom-project-build-template/build-process-public.drawio @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/versions/4.0/en/editor/publish/custom-project-build-template/build-process.jpg b/versions/4.0/en/editor/publish/custom-project-build-template/build-process.jpg new file mode 100644 index 0000000000..eb6311ab8c Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-project-build-template/build-process.jpg differ diff --git a/versions/4.0/en/editor/publish/custom-project-build-template/build-template.png b/versions/4.0/en/editor/publish/custom-project-build-template/build-template.png new file mode 100644 index 0000000000..25501118a4 Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-project-build-template/build-template.png differ diff --git a/versions/4.0/en/editor/publish/custom-project-build-template/console.png b/versions/4.0/en/editor/publish/custom-project-build-template/console.png new file mode 100644 index 0000000000..fcd6c19463 Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-project-build-template/console.png differ diff --git a/versions/4.0/en/editor/publish/custom-project-build-template/dev_tools.png b/versions/4.0/en/editor/publish/custom-project-build-template/dev_tools.png new file mode 100644 index 0000000000..5b1f4122e6 Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-project-build-template/dev_tools.png differ diff --git a/versions/4.0/en/editor/publish/custom-project-build-template/enable-plugin.png b/versions/4.0/en/editor/publish/custom-project-build-template/enable-plugin.png new file mode 100644 index 0000000000..60735e197b Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-project-build-template/enable-plugin.png differ diff --git a/versions/4.0/en/editor/publish/custom-project-build-template/plugin-template.png b/versions/4.0/en/editor/publish/custom-project-build-template/plugin-template.png new file mode 100644 index 0000000000..03a30f79f0 Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-project-build-template/plugin-template.png differ diff --git a/versions/4.0/en/editor/publish/custom-project-build-template/reload.png b/versions/4.0/en/editor/publish/custom-project-build-template/reload.png new file mode 100644 index 0000000000..cae0cc7dd7 Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-project-build-template/reload.png differ diff --git a/versions/4.0/en/editor/publish/custom-project-build-template/web-mobile.png b/versions/4.0/en/editor/publish/custom-project-build-template/web-mobile.png new file mode 100644 index 0000000000..24d68d3a46 Binary files /dev/null and b/versions/4.0/en/editor/publish/custom-project-build-template/web-mobile.png differ diff --git a/versions/4.0/en/editor/publish/debug-jsb.md b/versions/4.0/en/editor/publish/debug-jsb.md new file mode 100644 index 0000000000..46f89df8d4 --- /dev/null +++ b/versions/4.0/en/editor/publish/debug-jsb.md @@ -0,0 +1,91 @@ +# Debugging JavaScript on Native Platforms + +After a game is released on the native platform, because the runtime environment is different, there may be some bugs that cannot be reproduced in the browser preview. This means it is necessary to debug it directly on the native platform. **Cocos Creator** makes it easy to debug JavaScript remotely in the native platforms. + +## Debugging on Android / iOS + +If a game can only run on a physical device, then the packaged game must be debugged on a physical device. Debugging steps are as follows: + +- Make sure that the Android / iOS device is on the same LAN as Windows or Mac. +- Select the **Android/iOS** platform and **Debug** mode in the **Build** panel to build, compile and run a project (for the iOS platform, it is recommended to connect to the physical device via Xcode to compile and run). +- Open this address with Chrome browser: `devtools://devtools/bundled/js_app.html?v8only=true&ws=Local IP of the device:43086/00010002-0003-4004-8005-000600070008`. + + ![v8-android-debug](debug-jsb/v8-android-debug.png) + +>If the port is occupied, the port will auto-increment by +1. If you cannot connect, please check the port number printed in the console when the App starts. +>It is recommended to check the "Debug" and "Source Maps" options when buiding. This ensures that the corresponding JavaScript code is displayed in an expanded format. + +## Debugging on Windows / Mac + +The steps for debugging a game on the Windows / Mac platform are similar to the Android / iOS, just compile the project and run it in the IDE. + +- Compile and run the packaged project with the IDE (Visual Studio for Windows and Xcode for Mac). +- Open Chrome while the game is running and enter the address: `devtools://devtools/bundled/js_app.html?v8only=true&ws=127.0.0.1:6086/00010002-0003-4004-8005-000600070008` to debug it. + + ![v8-win32-debug](debug-jsb/v8-win32-debug.png) + +## Using `lldb` to view the current JS call stack + +By using breakpoints in C++, we can easily see the C++ call stack, but not the JS call stack at the same time, which often breaks the debugging experience. The features provided by `lldb` allow doing a lot of things during debugging, including viewing the call stack. + +Both **Xcode** and **Android Studio** use `lldb` as debugger by default. For more details, please refer to the [LLDB Guide](https://lldb.llvm.org/use/tutorial.html) documentation. + +### Global configuration of `lldb` + +`lldb` will load `~/.lldbinit` at startup, for example the following configuration: + +`~ % cat ~/.lldbinit` + +``` +target stop-hook add +po se::ScriptEngine::getInstance()->getCurrentStackTrace() +DONE +``` + +The behavior after setting **every breakpoint** and executing the following code to output the JS call stack: + +```txt +po se::ScriptEngine::getInstance()->getCurrentStackTrace() +``` + +For more information on the usage of `target stop-hook`, please refer to the [Examining Variables](https://lldb.llvm.org/use/map.html#examining-variables) documentation. + +However, this approach has an obvious drawback: it works for **all projects** and will result in an error if the corresponding symbol does not exist for other projects. + +### Configuring `lldb` in Xcode + +#### Editing `Action` in breakpoints in Xcode (only triggered for specific breakpoints) + +![xcode-brk-point-action](debug-jsb/xcode-brk-point-action.png) + +Enter the following command in the **Debugger Command**: + +```txt +po se::ScriptEngine::getInstance()->getCurrentStackTrace() +``` + +For more information about the usage of `target stop-hook`, please refer to the [Evaluating Expressions](https://lldb.llvm.org/use/map.html#evaluating-expressions) documentation. + +#### Setting `stop hook` + +After the breakpoint is triggered, a callback needs to be added to the lldb console. More calls can be made for specific breakpoints: the + +![xcode-brk-point-lldb](debug-jsb/xcode-brk-point-lldb.png) + +As above, the call stack can also be viewed by executing the following code: + +```txt +po se::ScriptEngine::getInstance()->getCurrentStackTrace() +``` + +### Configuring `lldb` in Android Studio + +Configure in the **Run -> Debug Configuration -> Debugger** interface of **Android Studio**: + +![as-brk-point-action](debug-jsb/as-brk-point-action.png) + +Android Studio also provides a `lldb console` similar to Xcode. + +## Advanced Debugging Guide + +To debug in Release mode, or need to debug a customized native engine, please refer to the more detailed [JSB 2.0 Usage Guide: Remote Debugging and Profile](../../advanced-topics/JSB2.0-learning.md) documentation. diff --git a/versions/4.0/en/editor/publish/debug-jsb/as-brk-point-action.png b/versions/4.0/en/editor/publish/debug-jsb/as-brk-point-action.png new file mode 100644 index 0000000000..ffa52225f1 Binary files /dev/null and b/versions/4.0/en/editor/publish/debug-jsb/as-brk-point-action.png differ diff --git a/versions/4.0/en/editor/publish/debug-jsb/simulator-run.png b/versions/4.0/en/editor/publish/debug-jsb/simulator-run.png new file mode 100644 index 0000000000..b365e75e36 Binary files /dev/null and b/versions/4.0/en/editor/publish/debug-jsb/simulator-run.png differ diff --git a/versions/4.0/en/editor/publish/debug-jsb/v8-android-debug.png b/versions/4.0/en/editor/publish/debug-jsb/v8-android-debug.png new file mode 100644 index 0000000000..c866cd1813 Binary files /dev/null and b/versions/4.0/en/editor/publish/debug-jsb/v8-android-debug.png differ diff --git a/versions/4.0/en/editor/publish/debug-jsb/v8-win32-debug.png b/versions/4.0/en/editor/publish/debug-jsb/v8-win32-debug.png new file mode 100644 index 0000000000..7061257b84 Binary files /dev/null and b/versions/4.0/en/editor/publish/debug-jsb/v8-win32-debug.png differ diff --git a/versions/4.0/en/editor/publish/debug-jsb/xcode-brk-point-action.png b/versions/4.0/en/editor/publish/debug-jsb/xcode-brk-point-action.png new file mode 100644 index 0000000000..bdf435936d Binary files /dev/null and b/versions/4.0/en/editor/publish/debug-jsb/xcode-brk-point-action.png differ diff --git a/versions/4.0/en/editor/publish/debug-jsb/xcode-brk-point-lldb.png b/versions/4.0/en/editor/publish/debug-jsb/xcode-brk-point-lldb.png new file mode 100644 index 0000000000..43c623776c Binary files /dev/null and b/versions/4.0/en/editor/publish/debug-jsb/xcode-brk-point-lldb.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/build-and-run.md b/versions/4.0/en/editor/publish/google-play-games/build-and-run.md new file mode 100644 index 0000000000..07eee343aa --- /dev/null +++ b/versions/4.0/en/editor/publish/google-play-games/build-and-run.md @@ -0,0 +1,60 @@ +# Build and Run + +![build-and-run/run-on-hpe.png](build-and-run/run-on-hpe.png) + +In this section we will describe how to develop and test an existing APK with the HPE simulator. + +## Preparation + +First you need to prepare an Android Studio project published by Cocos Creator, if you are not familiar with this, we recommend you to refer to [Android Build Example](./android/build-example.md) + +## Start the emulator + +First you need to launch the emulator by finding the emulator icon in the OS toolbar and selecting **Launch Emulator**: + +![build-and-run/start-simulator.png](build-and-run/start-simulator.png) + +After starting: + +![build-and-run/start-simulator.png](build-and-run/after-start-simulator.png) + +## Installing the app + +Let's review a little how to build the APK via Android Studio, just click on the following menu: + +![build-and-run/build-apk.png](build-and-run/build-apk.png) + +After a successful build, you can find the corresponding APK file in the project directory. + +### Start ADB + +Next you need to install the APK to the emulator, find the adb command and enter: + +> adb is usually found in the SDK/plat-form directory of Android. You may consider adding this directory to the environment variables so that it is available anywhere. + +```bash +adb devices +``` + +Used to check if the emulator is properly connected. + +![build-and-run/adb-devices.png](build-and-run/adb-devices.png) + +If `localhost:6520 offline` is displayed or the devices are not displayed, restart the emulator or type: `adb connect localhost:6520` from within the command line. + +### Install the application + +Find the directory where the apk was previously stored and type in the command line. + +```bash +adb install C:/yourpath/yourgame.apk + +``` + +![build-and-run/install-apk.png](build-and-run/install-apk.png) + +## Start the application + +If you want to find the installed application, find the application icon in the screen by swiping up and then click on it to launch the application: + +![build-and-run/show-apps.png](build-and-run/show-apps.png) diff --git a/versions/4.0/en/editor/publish/google-play-games/build-and-run/adb-devices.png b/versions/4.0/en/editor/publish/google-play-games/build-and-run/adb-devices.png new file mode 100644 index 0000000000..96ae1ac74d Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/build-and-run/adb-devices.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/build-and-run/after-start-simulator.png b/versions/4.0/en/editor/publish/google-play-games/build-and-run/after-start-simulator.png new file mode 100644 index 0000000000..bd3724b294 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/build-and-run/after-start-simulator.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/build-and-run/build-apk.png b/versions/4.0/en/editor/publish/google-play-games/build-and-run/build-apk.png new file mode 100644 index 0000000000..2eb37c9faf Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/build-and-run/build-apk.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/build-and-run/find-adb.png b/versions/4.0/en/editor/publish/google-play-games/build-and-run/find-adb.png new file mode 100644 index 0000000000..08cb108dd2 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/build-and-run/find-adb.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/build-and-run/install-apk.png b/versions/4.0/en/editor/publish/google-play-games/build-and-run/install-apk.png new file mode 100644 index 0000000000..61e2bf1876 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/build-and-run/install-apk.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/build-and-run/run-on-hpe.png b/versions/4.0/en/editor/publish/google-play-games/build-and-run/run-on-hpe.png new file mode 100644 index 0000000000..56ff530221 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/build-and-run/run-on-hpe.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/build-and-run/show-apps.png b/versions/4.0/en/editor/publish/google-play-games/build-and-run/show-apps.png new file mode 100644 index 0000000000..78220a4587 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/build-and-run/show-apps.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/build-and-run/start-simulator.png b/versions/4.0/en/editor/publish/google-play-games/build-and-run/start-simulator.png new file mode 100644 index 0000000000..1f7cd19b8b Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/build-and-run/start-simulator.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/external-storage/external-permissions.png b/versions/4.0/en/editor/publish/google-play-games/external-storage/external-permissions.png new file mode 100644 index 0000000000..f1b69f96ac Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/external-storage/external-permissions.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/external-storage/permission.png b/versions/4.0/en/editor/publish/google-play-games/external-storage/permission.png new file mode 100644 index 0000000000..16f75be236 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/external-storage/permission.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/index.md b/versions/4.0/en/editor/publish/google-play-games/index.md new file mode 100644 index 0000000000..e732a1894c --- /dev/null +++ b/versions/4.0/en/editor/publish/google-play-games/index.md @@ -0,0 +1,86 @@ +# Google Play Games on PC + +[Google Play Games (GPG)](https://play.google.com/googleplaygames#section-system-requirements) is a related technology developed by Google that allows you to publish and play your mobile APK on PC. + +Starting from v3.8, Cocos Creator will provide support for publishing to GPG. This will help your Android version of the game to be published on PC platform for more growth. + +In order to publish on GPG smoothly, we recommend you to read the [GPG official website](https://developer.android.com/games/playgames/overview?hl=zh-cn) first to get involved with GPG SDK quickly. + +## Integration Guide + +In order to get your app on the shelf smoothly, please check that the following steps are all set. + +1. In order for the game to run on Windows (both intel and AMD chips), you need to build it with x86 architecture. When building in Cocos Creator, check [APP ABI](./native-options.md#app-abi) and check the box x86_64: + + ![ap](./index/app-abi.png) + + > x86_64 supports both x86 and x64 builds, so we recommend that you check x86_64 for future changes in requirements. + +2. The highest version of OpenGL ES supported by Cocos Creator is 3.1, please check the GLES options in the following figure if you want your application supports OpenGL ES. +3. Vulkan version is not higher than 1.1. For Cocos Creator to support Vulkan, you need to check Vulkan in the build options, please refer to the figure below. + + ![index/render-backend.png](index/render-backend.png) + +4. The relevant mobile platform features and permissions need to be removed, according to the [Functional testing requirements](https://developer.android.com/games/playgames/pc-compatibility?hl=zh-cn#unsupported-features-1) and [Quality testing requirements](https://developer.android.com/games/playgames/pc-compatibility?hl=zh-cn#unsupported-features-2). Currently, the default build of Cocos Creator does not cover these features or permissions. +5. Remove the Permissions dialog for Android apps, [Details](https://developer.android.com/games/playgames/pc-compatibility#permissions-dialogs). +6. Remove the unsupported Google Play API, [Details](https://developer.android.com/games/playgames/pc-compatibility#unsupported-google-apis). +7. Enable scoped storage when the application needs to read and write to external storage, example is as follows: + + - Find AndroidManifest.xml in the project directory. + + ![external-storage/permission.png](external-storage/permission.png) + + - After adding the permissions: + + ![external-storage/permission.png](external-storage/external-permissions.png) + + - Fore more details, please refer to [Enable scoped storage](https://developer.android.com/games/playgames/pc-compatibility#scoped-storage). + +8. Scaling UI + + Cocos Creator supports adaptive UI. For most mobile games, the resolution is determined when the app is launched, so there is no need to consider the adaptation, but on GPG, since the user can resize the window through the outside of the window, you need to adapt the resolution separately. + + We recommend you to select **Widget** component and make sure its **Align Mode** is **On_WINDOW_RESIZE** or **ALWAYS**. + + ![scale-ui/scale-ui.png](scale-ui/scale-ui.png) + + - [GPG Interface Scaling](https://developer.android.com/games/playgames/graphics?hl=zh-cn#ui-scaling) + - [Multi-resolution Adaptation Scheme](../../../ui-system/components/engine/multi-resolution.md) + - [Widget Component Reference](../../../ui-system/components/editor/widget.md) Adaptation of child UI + + For more details, see [UI Practice Guide](../../../ui-system/components/engine/usage-ui.md) + + For adapting devices with large screens, you can refer to Google [Responsive Layout for Large Screen Device Development](https://developer.android.com/large-screens). + + + +9. GPG requires support for a 16:9 aspect ratio. For an ideal player experience, the game should also support 21:9, 16:10 and 3:2. +Portrait mode games only need to support a 9:16 aspect ratio. If your game lacks landscape support, Google Play Games will render black bars in full screen mode. Again, see the **Widget** component section above. +1. Adapting to window transformations, the resolution rendered by GPG games will change the game's rendering resolution at game startup, at window resize, and when switching between full-screen and window models, [details](https://developer.android.com/games/playgames/graphics#dynamic-display). +2. To ensure that users do not lose progress when switching between mobile devices and PCs, please check that your game meets Google's requirements for continuity, see [About continuity and cross-device play](https://developer.android.com/games/playgames/identity?hl=zh-cn) + +## Release process + +The GPG release process is similar to the Android release process, you can refer to the following documents for release support. + +- [install-and-run](./build-and-run.md) +- [Android build example](./android/build-example.md) +- [native-release](../native-options.md) + +## Content + +- [Integration with Input SDK](../gpg-input-sdk.md) + +## Related links + +- [GPG official website](https://developer.android.com/games/playgames/overview?hl=zh-cn) + +## Interface descriptions + +To determine whether the runtime is HPE, use the following code: + +```ts +if( sys.hasFeature(sys.Feature.HPE) ) { + ... +} +``` diff --git a/versions/4.0/en/editor/publish/google-play-games/index/app-abi.png b/versions/4.0/en/editor/publish/google-play-games/index/app-abi.png new file mode 100644 index 0000000000..806c016d67 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/index/app-abi.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/index/hyper-resolution.png b/versions/4.0/en/editor/publish/google-play-games/index/hyper-resolution.png new file mode 100644 index 0000000000..5d733ecc7a Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/index/hyper-resolution.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/index/render-backend.png b/versions/4.0/en/editor/publish/google-play-games/index/render-backend.png new file mode 100644 index 0000000000..84eee6f6be Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/index/render-backend.png differ diff --git a/versions/4.0/en/editor/publish/google-play-games/scale-ui/scale-ui.png b/versions/4.0/en/editor/publish/google-play-games/scale-ui/scale-ui.png new file mode 100644 index 0000000000..2d22bb6147 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play-games/scale-ui/scale-ui.png differ diff --git a/versions/4.0/en/editor/publish/google-play/build-example-google-play.md b/versions/4.0/en/editor/publish/google-play/build-example-google-play.md new file mode 100644 index 0000000000..2aa6da03a2 --- /dev/null +++ b/versions/4.0/en/editor/publish/google-play/build-example-google-play.md @@ -0,0 +1,142 @@ +# Google Play Build Example + +This article demonstrates the process of publishing a Cocos Creator project as a Google Play application. + +Please prepare a Cocos Creator project with at least one scene. + +![project.png](./images/project.png) + +## Setting Up the Publishing Environment + +To publish a Google Play native application, you need to install the Android Studio development environment, along with specific versions of JDK (or OpenSDK), Android SDK, and NDK. For details, please refer to [Configure Android Native Development Environment](../setup-native-development.md). + +## Publishing Process + +Next, in Cocos Creator, find the **Project** menu, click the **Build** button to open the **Build** panel. + +### Creating a Build Task + +1. Select **Project** -> **Build** menu to open the build panel + + ![cc-build-menu.png](./images/cc-build-menu.png) + +2. Click the **New Build Task** option in the panel: + + ![new-build-task.png](./images/new-build-task.png) + +3. Select Google Play as the build platform: + + ![select-platform.png](./images/select-platform.png) + +4. Select at least one scene as the initial loading scene. When there is only one scene, it will be added by default: + + ![start-scene.png](./images/start-scene.png) + +5. Enable ADPF (Optional) + + ![enable-adpf.png](./images/enable-adpf.png) + +6. Refer to [Android Platform Build Options - Render Backend](../../../../en/editor/publish/native-options.md#%E6%B8%B2%E6%9F%93%E5%90%8E%E7%AB%AF) to select the render backend + + ![render-backend.png](./images/render-backend.png) + +7. Enter the package name + + ![game-package-name.png](./images/game-package-name.png) + + > For naming conventions, please refer to [Application ID Name](../../../../en/editor/publish/native-options.md#%E5%BA%94%E7%94%A8-id-%E5%90%8D%E7%A7%B0) + +8. Change Application Icon (Optional) + +![custom-icon.png](./images/custom-icon.png) + +9. Select Target API Level + + ![target-api-level.png](./images/target-api-level.png) + + > If there's no dropdown box, please check if the **SDK and NDK Configuration** above is correct. + +10. Enable Google Play Instant (Optional) + + ![enable-google-play-instant.png](./images/enable-google-play-instant.png) + +11. Enable Google Play Billing Feature + + ![enable-google-play-billing.png](./images/enable-google-play-billing.png) + + > Without checking this option, you cannot use the Google Play Billing interface + +For other options, please refer to [Android Platform Build Options](../../../../en/editor/publish/native-options.md#android-%E5%B9%B3%E5%8F%B0%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) for configuration. + +### Build and Publish + +1. Build: Click the **Build** button below to create a new build task and start building + + ![build.png](./images/build.png) + +2. Wait for the build to complete + + ![building.png](./images/building.png) + +#### Generate AAB Package via Creator +1. Click **Generate** + + ![make](./images/make.png) + +2. Wait for the generation to complete + + ![making.png](./images/making.png) + +3. Click the button below to open the generated project: + + ![open](./images/open.png) + +4. Find the publish directory + + ![find-publish-dir](./images/find-publish-dir.png) + +5. Find the generated AAB package + + ![find-release-aab](./images/find-release-aab.png) + +#### Generate AAB Package via Android Studio +1. 找到工程对应的目录 + + ![find-proj](./images/find-proj.png) + +2. Open Android Studio menu: + + ![android studio open project menu](./images/as-open-menu.png) + +3. Open the built project at `{project path}/build/google-play/proj`: + + ![android studio open project](./images/as-open-proj.png) + +4. Build APK using Android Studio + + After opening Android Studio, it will take some time for preparation. Once Android Studio has finished preparing the project, you can package the APK. The preparation process might take a while. If there's no response for a long time, please check your network or switch to another mirror. If you need to interrupt the current background task, you can refer to the following closing method: + + > When Android Studio has background tasks, you can click the background task bar at the bottom of the window: + > + > ![background-task.png](./images/background-task.png) + > + > Click the × on the right in the popup window to end the background task: + > + > ![interrupt-sync.png](./images/interrupt-sync.png) + +5. Open the **Build** menu and select **Build Bundle(s) / APK(s)**: + + ![build-apk.png](./images/build-apk.png) + +6. After successful publication, you can find the Debug version of the APK in the proj/build directory: + + ![apk.png](./images/apk.png) + +## Others +Other aspects are basically the same as Android. You can refer to [Android Build Example](../../../../en/editor/publish/android/build-example-android.md) + +## Google Play Billing +[Google Play Billing Documentation](./google-play-billing-example.md) + +## Google Play Game Services +[Google Play Game Services Documentation](./google-play-game-services.md) \ No newline at end of file diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/achievement-infos.png b/versions/4.0/en/editor/publish/google-play/game-services-images/achievement-infos.png new file mode 100644 index 0000000000..7acf5263ba Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/achievement-infos.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/application-id.png b/versions/4.0/en/editor/publish/google-play/game-services-images/application-id.png new file mode 100644 index 0000000000..dbefa9be71 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/application-id.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/config-application-id-en.png b/versions/4.0/en/editor/publish/google-play/game-services-images/config-application-id-en.png new file mode 100644 index 0000000000..e3990558ad Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/config-application-id-en.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/config-application-id-zh.png b/versions/4.0/en/editor/publish/google-play/game-services-images/config-application-id-zh.png new file mode 100644 index 0000000000..20c3732c14 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/config-application-id-zh.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/create-achievements.png b/versions/4.0/en/editor/publish/google-play/game-services-images/create-achievements.png new file mode 100644 index 0000000000..9e23c4f454 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/create-achievements.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/credentials-infos.png b/versions/4.0/en/editor/publish/google-play/game-services-images/credentials-infos.png new file mode 100644 index 0000000000..6cfddec84a Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/credentials-infos.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/default-achievements.png b/versions/4.0/en/editor/publish/google-play/game-services-images/default-achievements.png new file mode 100644 index 0000000000..a71db97f2c Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/default-achievements.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/google-cloud-new-oauth-client-id.png b/versions/4.0/en/editor/publish/google-play/game-services-images/google-cloud-new-oauth-client-id.png new file mode 100644 index 0000000000..011d673f08 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/google-cloud-new-oauth-client-id.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/new-credentials.png b/versions/4.0/en/editor/publish/google-play/game-services-images/new-credentials.png new file mode 100644 index 0000000000..018e58075a Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/new-credentials.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/no-config-application-id-error.png b/versions/4.0/en/editor/publish/google-play/game-services-images/no-config-application-id-error.png new file mode 100644 index 0000000000..f265a4f699 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/no-config-application-id-error.png differ diff --git a/versions/4.0/en/editor/publish/google-play/game-services-images/publish-achievement.png b/versions/4.0/en/editor/publish/google-play/game-services-images/publish-achievement.png new file mode 100644 index 0000000000..de52b1d02f Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/game-services-images/publish-achievement.png differ diff --git a/versions/4.0/en/editor/publish/google-play/google-play-billing-example.md b/versions/4.0/en/editor/publish/google-play/google-play-billing-example.md new file mode 100644 index 0000000000..32ee528a60 --- /dev/null +++ b/versions/4.0/en/editor/publish/google-play/google-play-billing-example.md @@ -0,0 +1,90 @@ +# Using the Google Play Billing + +## Import the API +```ts +import { google } from 'cc' +``` + +## Initialize BillingClient +```ts +import { google } from 'cc' +this._client = google.billing.BillingClient.newBuilder().enablePendingPurchases( + google.billing.PendingPurchasesParams.newBuilder().enableOneTimeProducts().build() + ).setListener({ + onPurchasesUpdated: (billingResult: google.billing.BillingResult, purchases: google.billing.Purchase[]): void => { + // TODO: Handle purchase updates + } + }).build(); +``` + +## Connect to Google Play +```ts +this._client.startConnection({ + onBillingServiceDisconnected: (): void => { + // TODO: Handle connection failure + }, + onBillingSetupFinished: (billingResult: google.billing.BillingResult): void => { + // TODO: Handle successful connection + } +}); +``` + +## Show Available Products +```ts +const product = google.billing.QueryProductDetailsParams.Product.newBuilder() + .setProductId("productId") + .setProductType(google.billing.BillingClient.ProductType.INAPP) + .build(); +const params = google.billing.QueryProductDetailsParams.newBuilder().setProductList([product]).build(); +this._client.queryProductDetailsAsync(params, { + onProductDetailsResponse: (billingResult: google.billing.BillingResult, productDetailsList: google.billing.ProductDetails[]): void => { + // TODO: Product query callback + } +}); +``` + +## Launch Purchase Flow +```ts +const params = google.billing.BillingFlowParams.newBuilder().setProductDetailsParamsList( + [google.billing.BillingFlowParams.ProductDetailsParams.newBuilder().setProductDetails(productDetails).build()] + ).build(); +this._client.launchBillingFlow(params); +``` + +## Consumable Products +```ts +this._client.consumeAsync(google.billing.ConsumeParams.newBuilder().setPurchaseToken(this._purchases[0].getPurchaseToken()).build(), { + onConsumeResponse: (billingResult: google.billing.BillingResult, token: string): void => { + // TODO: Consumption callback + } +}); +``` + +## Non-Consumable Products +```ts +this._client.acknowledgePurchase( + google.billing.AcknowledgePurchaseParams.newBuilder().setPurchaseToken(this._purchases[0].getPurchaseToken()).build(), { + onAcknowledgePurchaseResponse: (billingResult: google.billing.BillingResult): void => { + // TODO + } + }); +``` + +## Query Purchased Products +```ts +this._client.queryPurchasesAsync( + google.billing.QueryPurchasesParams.newBuilder().setProductType(google.billing.BillingClient.ProductType.INAPP).build(), { + onQueryPurchasesResponse: (billingResult: google.billing.BillingResult, purchaseList: google.billing.Purchase[]) : void => { + // TODO: Purchased products callback + } +}); +``` + +## Query User Billing Configuration +```ts +this._client.getBillingConfigAsync(google.billing.GetBillingConfigParams.newBuilder().build(), { + onBillingConfigResponse: (billingResult: google.billing.BillingResult, billingConfig: google.billing.BillingConfig) : void => { + // TODO: Get user config callback + } +}); +``` \ No newline at end of file diff --git a/versions/4.0/en/editor/publish/google-play/google-play-game-services.md b/versions/4.0/en/editor/publish/google-play/google-play-game-services.md new file mode 100644 index 0000000000..cd6ebce8f4 --- /dev/null +++ b/versions/4.0/en/editor/publish/google-play/google-play-game-services.md @@ -0,0 +1,143 @@ +# Google Play Build Example + +This feature is only available on the Google Play platform. Please refer to [Publish Google Play Example](./build-example-google-play.md) + +### Auto Sign-in +When players launch a game with auto sign-in enabled, they can sign in without interacting with the sign-in prompt. Players can enable auto sign-in either in the Google Play Games app or through the initial sign-in prompt displayed in the game. + +#### Google Cloud Backend Configuration + +- Create Credentials + +![new credentials](./game-services-images/google-cloud-new-oauth-client-id.png) + +You need to create an OAuth client ID in Google Cloud before you can see it in the credentials section of the Google Console. + +### Google Play Console Backend Configuration +[Reference Documentation](https://developer.android.com/games/pgs/console/setup?hl=en#add_your_game_to_the) + +1. Add New Credentials +![new credentials](./game-services-images/new-credentials.png) +2. Fill in Credential Information +![credentials infomations](./game-services-images/credentials-infos.png) +3. Creation Completed +![application id](./game-services-images/application-id.png) +The Application ID highlighted in the red box is what you'll need to fill in below. +4. After building for the Google Play platform, users need to configure the application ID. + +For English applications, configure in English: +![config application id](./game-services-images/config-application-id-en.png) +For Chinese applications, configure in Chinese: +![config application id](./game-services-images/config-application-id-zh.png) + +You can configure both English and Chinese simultaneously. + +If not configured, you'll see the following error: +![no-config-application-id-error](./game-services-images/no-config-application-id-error.png) + +#### Usage Example +1. Initialize SDK: +```typescript +import { google } from 'cc'; +// Initialize Play Games SDK +google.play.PlayGamesSdk.initialize(); +``` + +2. Get Login Result +```typescript + gameSignInClient.isAuthenticated().addOnCompleteListener({ + onComplete: (isAuthenticatedTask: google.play.Task) => { + const isAuthenticated = + (isAuthenticatedTask.isSuccessful() && + isAuthenticatedTask.getResult().isAuthenticated()); + if (isAuthenticated) { + // Continue with Play Games Services + } else { + // Disable your integration with Play Games Services or show a + // login button to ask players to sign-in. Clicking it should + // call GamesSignInClient.signIn(). + } + }, + }); +``` + +### Achievements +Achievements are an excellent way to increase user engagement in your game. You can implement achievements to encourage players to try features they might not normally use or to play the game in completely different ways. Achievements also allow players to compare their game progress and easily engage in friendly competition. + +#### Google Backend Configuration +1. Create Achievement +![create-achievements](./game-services-images/create-achievements.png) +2. Fill in Achievement Information +![achievement-infos](./game-services-images/achievement-infos.png) +3. Publish Achievement Information +![publish-achievement](./game-services-images/publish-achievement.png) + +Usage Examples: +1. Display Achievements: +```typescript +import { google } from 'cc'; +// Show default achievements interface +google.play.PlayGames.getAchievementsClient().showAchievements(); +``` +The display effect is shown below: + +![default achievements](./game-services-images/default-achievements.png) + +2. Unlock Achievement: +```typescript +import { google } from 'cc'; +google.play.PlayGames.getAchievementsClient().unlock("achievementId"); +``` + +If the achievement is incremental (requires several steps to unlock), call AchievementsClient.increment() instead: +```typescript +import { google } from 'cc'; +google.play.PlayGames.getAchievementsClient().increment("achievementId", 1); +``` +Google Play Games Services will automatically unlock the achievement when the completed steps meet the requirement. + +3. Load All Achievement Information: +```typescript + import { google } from 'cc'; + const achievementsClient = google.play.PlayGames.getAchievementsClient(); + achievementsClient.load(false).addOnSuccessListener({ + onSuccess: (data: google.play.AnnotatedData) => { + this.addLog("isStale : " + data.isStale()); + const achievements = data.get(); + for(let i = 0; i < achievements.getCount(); ++i) { + str += `getCurrentSteps : ${achievements.get(i).getCurrentSteps()}\n`; + str += `getState : ${achievements.get(i).getState()}\n`; + str += `getTotalSteps : ${achievements.get(i).getTotalSteps()}\n`; + str += `getType : ${achievements.get(i).getType()}\n`; + str += `getLastUpdatedTimestamp : ${achievements.get(i).getLastUpdatedTimestamp()}\n`; + str += `getXpValue : ${achievements.get(i).getXpValue()}\n`; + str += `getAchievementId : ${achievements.get(i).getAchievementId()}\n`; + str += `getDescription : ${achievements.get(i).getDescription()}\n`; + str += `getFormattedCurrentSteps : ${achievements.get(i).getFormattedCurrentSteps()}\n`; + str += `getFormattedTotalSteps : ${achievements.get(i).getFormattedTotalSteps()}\n`; + str += `getName : ${achievements.get(i).getName()}\n`; + str += `getRevealedImageUrl : ${achievements.get(i).getRevealedImageUrl()}\n`; + str += `getUnlockedImageUrl : ${achievements.get(i).getUnlockedImageUrl()}\n\n`; + } + console.log(str); + } + }); +``` + +4. Reveal Hidden Achievement: +```typescript +import { google } from 'cc'; +google.play.PlayGames.getAchievementsClient().reveal(); +``` + +### Recall API +To communicate with Google's servers using the correct information, you need to request a Recall session ID from the client SDK and send it to your game server. +```typescript +const recallClient = google.play.PlayGames.getRecallClient(); +recallClient.requestRecallAccess().addOnSuccessListener({ + onSuccess: (recallAccess: google.play.RecallAccess) => { + const recallSessionId = recallAccess.getSessionId(); + // Send the recallSessionId to your game server + } +}) +``` \ No newline at end of file diff --git a/versions/4.0/en/editor/publish/google-play/images/apk.png b/versions/4.0/en/editor/publish/google-play/images/apk.png new file mode 100644 index 0000000000..759cfb4137 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/apk.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/as-open-menu.png b/versions/4.0/en/editor/publish/google-play/images/as-open-menu.png new file mode 100644 index 0000000000..b1f57b5d4c Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/as-open-menu.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/as-open-proj.png b/versions/4.0/en/editor/publish/google-play/images/as-open-proj.png new file mode 100644 index 0000000000..80996bac67 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/as-open-proj.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/background-task.png b/versions/4.0/en/editor/publish/google-play/images/background-task.png new file mode 100644 index 0000000000..0977698682 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/background-task.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/build-apk.png b/versions/4.0/en/editor/publish/google-play/images/build-apk.png new file mode 100644 index 0000000000..393c5647a9 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/build-apk.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/build.png b/versions/4.0/en/editor/publish/google-play/images/build.png new file mode 100644 index 0000000000..ddcd705d67 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/build.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/building.png b/versions/4.0/en/editor/publish/google-play/images/building.png new file mode 100644 index 0000000000..7a93b2c8a5 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/building.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/cc-build-menu.png b/versions/4.0/en/editor/publish/google-play/images/cc-build-menu.png new file mode 100644 index 0000000000..3fc866d77d Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/cc-build-menu.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/custom-icon.png b/versions/4.0/en/editor/publish/google-play/images/custom-icon.png new file mode 100644 index 0000000000..75a2c2684b Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/custom-icon.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/enable-adpf.png b/versions/4.0/en/editor/publish/google-play/images/enable-adpf.png new file mode 100644 index 0000000000..9831d84afe Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/enable-adpf.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/enable-google-play-billing.png b/versions/4.0/en/editor/publish/google-play/images/enable-google-play-billing.png new file mode 100644 index 0000000000..63e6e4bb35 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/enable-google-play-billing.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/enable-google-play-instant.png b/versions/4.0/en/editor/publish/google-play/images/enable-google-play-instant.png new file mode 100644 index 0000000000..42d5fd2232 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/enable-google-play-instant.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/find-proj.png b/versions/4.0/en/editor/publish/google-play/images/find-proj.png new file mode 100644 index 0000000000..afd2da6796 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/find-proj.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/find-publish-dir.png b/versions/4.0/en/editor/publish/google-play/images/find-publish-dir.png new file mode 100644 index 0000000000..9f22dd0b5c Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/find-publish-dir.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/find-release-aab.png b/versions/4.0/en/editor/publish/google-play/images/find-release-aab.png new file mode 100644 index 0000000000..c3b0bfb178 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/find-release-aab.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/game-package-name.png b/versions/4.0/en/editor/publish/google-play/images/game-package-name.png new file mode 100644 index 0000000000..bb088b8f61 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/game-package-name.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/interrupt-sync.png b/versions/4.0/en/editor/publish/google-play/images/interrupt-sync.png new file mode 100644 index 0000000000..f10fdbe28d Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/interrupt-sync.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/make.png b/versions/4.0/en/editor/publish/google-play/images/make.png new file mode 100644 index 0000000000..02085c14d1 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/make.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/making.png b/versions/4.0/en/editor/publish/google-play/images/making.png new file mode 100644 index 0000000000..aeaf651840 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/making.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/new-build-task.png b/versions/4.0/en/editor/publish/google-play/images/new-build-task.png new file mode 100644 index 0000000000..71707a9373 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/new-build-task.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/open.png b/versions/4.0/en/editor/publish/google-play/images/open.png new file mode 100644 index 0000000000..8c4841ae51 Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/open.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/project.png b/versions/4.0/en/editor/publish/google-play/images/project.png new file mode 100644 index 0000000000..2bd2bef86c Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/project.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/render-backend.png b/versions/4.0/en/editor/publish/google-play/images/render-backend.png new file mode 100644 index 0000000000..83a171bddd Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/render-backend.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/select-platform.png b/versions/4.0/en/editor/publish/google-play/images/select-platform.png new file mode 100644 index 0000000000..c8eca190bb Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/select-platform.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/start-scene.png b/versions/4.0/en/editor/publish/google-play/images/start-scene.png new file mode 100644 index 0000000000..5eb6e1b57e Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/start-scene.png differ diff --git a/versions/4.0/en/editor/publish/google-play/images/target-api-level.png b/versions/4.0/en/editor/publish/google-play/images/target-api-level.png new file mode 100644 index 0000000000..b6006ef0ee Binary files /dev/null and b/versions/4.0/en/editor/publish/google-play/images/target-api-level.png differ diff --git a/versions/4.0/en/editor/publish/gpg-input-sdk.md b/versions/4.0/en/editor/publish/gpg-input-sdk.md new file mode 100644 index 0000000000..26ecdf3dc7 --- /dev/null +++ b/versions/4.0/en/editor/publish/gpg-input-sdk.md @@ -0,0 +1,150 @@ +# Integrating Input SDK + +The Input SDK provides a unified interface that allows players to easily find the mouse and keyboard bindings for your game on Google Play Games for PC, thereby enhancing their gaming experience. + +## Prerequisites + +If you are using Input SDK on a new Android build, simply check the Input SDK option in the build panel. + +If the Android project was built previously, you will need to manually add the following code to the `native/engine/android/app/build.gradle` file. + +```txt +dependencies { + implementation 'com.google.android.libraries.play.games:inputmapping:1.0.0-beta' + ... +} +``` + +## Create a the class named `MyInputMapProvider` + +Create a new file `MyInputMapProvider.java` which contains the following code: + +```java + public class MyInputMapProvider implements InputMappingProvider { + public enum InputEventIds { + JUMP, + LEFT, + RIGHT, + USE, + SPECIAL_JUMP, + SPECIAL_DUCK, + CMB_MOVE, + CMB_DASH, + CMB_WAYPOINT + } + + @Override + public InputMap onProvideInputMap() { + InputAction jumpInputAction = InputAction.create( + getString(R.string.key_jump), + InputEventIds.JUMP.ordinal(), + InputControls.create( + Collections.singletonList(KeyEvent.KEYCODE_SPACE), + Collections.emptyList() + ) + ); + InputAction leftAction = InputAction.create( + getString(R.string.key_Left), + InputEventIds.LEFT.ordinal(), + InputControls.create( + Collections.singletonList(KeyEvent.KEYCODE_DPAD_LEFT), + Collections.emptyList() + ) + ); + InputAction rightAction = InputAction.create( + getString(R.string.key_Right), + InputEventIds.RIGHT.ordinal(), + InputControls.create( + Collections.singletonList(KeyEvent.KEYCODE_DPAD_RIGHT), + Collections.emptyList() + ) + ); + + InputAction cmbMove = InputAction.create( + getString(R.string.key_Move), + InputEventIds.CMB_MOVE.ordinal(), + InputControls.create( + Collections.emptyList(), + Collections.singletonList(InputControls.MOUSE_RIGHT_CLICK) + ) + ); + InputAction cmbDash = InputAction.create( + getString(R.string.key_Dash), + InputEventIds.CMB_DASH.ordinal(), + InputControls.create( + Arrays.asList(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SPACE), + Collections.emptyList() + ) + ); + InputAction cmbWaypoint = InputAction.create( + getString(R.string.key_Waypoint), + InputEventIds.CMB_WAYPOINT.ordinal(), + InputControls.create( + Collections.singletonList(KeyEvent.KEYCODE_SHIFT_LEFT), + Collections.singletonList(InputControls.MOUSE_RIGHT_CLICK) + ) + ); + InputGroup movementInputGroup = InputGroup.create( + getString(R.string.key_BasicMove), + Arrays.asList(jumpInputAction, leftAction, rightAction, cmbMove, cmbDash, cmbWaypoint) + ); + return InputMap.create( + Arrays.asList(movementInputGroup), + MouseSettings.create(true, true) + ); + } + } + +``` + +## Add binding names for keys + +Define key code names in the `res/values/strings.xml` file to enable internationalization of the app. These descriptions will be used in the previous steps to provide context and clarity for the user when mapping input events to corresponding actions. + +```xml + + ... + Jump + Left + Right + Move + Dash + Add Waypoint + Basic Movement + +``` + +## Register `InputMapping` + +To register the `InputMapping`, modify `AppActivity.java` located in `native\engine\android\app\src\main\cocos\game` by adding the necessary code. + +```java + ... + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ... + + InputMappingClient inputMappingClient = Input.getInputMappingClient(this); + inputMappingClient.setInputMappingProvider(new MyInputMapProvider()); + } + ... + + @Override + protected void onDestroy() { + InputMappingClient inputMappingClient = Input.getInputMappingClient(this); + inputMappingClient.clearInputMappingProvider(); + + super.onDestroy(); + ... + } + +``` + +## Test Result + +To bring up the page, use Shift + Tab while running on the HPE emulator provided by Google Play Games. + +![key codes](./publish-native/gpg-input-sdk-keys.png) + +Refer to the [official documentation](https://developer.android.com/games/playgames/input-sdk-start) of the Input SDK for more details. diff --git a/versions/4.0/en/editor/publish/index.md b/versions/4.0/en/editor/publish/index.md new file mode 100644 index 0000000000..63e8f00f25 --- /dev/null +++ b/versions/4.0/en/editor/publish/index.md @@ -0,0 +1,46 @@ +# Publish to Multiple Platforms + +Thanks to the Cocos Creator's dual-core engine architecture (the C++ core is for native platforms and the TS core is for web and mini-game platform), projects created with Cocos Creator can run smoothly on native, web, and mini-game platforms. This truly achieves the goal of developing once, run everywhere. + +## Supported Platforms + +- [iOS](./ios/index.md) +- [Android](./android/index.md) +- [HuaWei AppGallery Connect](./publish-huawei-agc.md) +- [Huawei HarmonyOS](./publish-huawei-ohos.md) +- [Google Play](google-play/build-example-google-play.md) +- [Google Play On Games](google-play-games/index.md) +- [macOS](./ios/index.md) +- [Windows](./windows/index.md) +- [Web Mobile - H5](./publish-web.md) +- [Web Web Desktop](publish-web.md) +- [Facebook Instant Games](./publish-fb-instant-games.md) +- [WeChat Mini Games](./publish-wechatgame.md) +- [Douyin Mini Games](./publish-bytedance-mini-game.md) +- [Facebook Instant Games](./publish-fb-instant-games.md) +- [OPPO Mini Games](./publish-oppo-mini-game.md) +- [Huawei Quick Games](./publish-huawei-quick-game.md) +- [vivo Mini Games](./publish-vivo-mini-game.md) +- [Honor Mini Games](./publish-honor-mini-game.md) + +## Preparation + +The preparation that needs to be done before publishing includes getting familiar with following concepts: + +- [Build Panel](build-panel.md) +- [Build Options](build-options.md) + +Developers can also publish projects via the command line. For details, please refer to the [Publish from the Command Line](publish-in-command-line.md) documentation. + +## Advanced + +If you have a certain degree of familiarity and understanding of the build process, it is possible customize the build template and extend the build process. For details, please refer to the following documents: + +- [Introduction to the Build Process and FAQ](build-guide.md) +- [Custom Project Build Process](custom-project-build-template.md) +- [Extended Build Process](custom-build-plugin.md) + +## Additional Resources + +- [Cocos Technical Support](https://www.cocos.com/en/assistant) +- [Cocos Official Forum](https://discuss.cocos2d-x.org/) diff --git a/versions/4.0/en/editor/publish/ios/build-example-ios.md b/versions/4.0/en/editor/publish/ios/build-example-ios.md new file mode 100644 index 0000000000..cf76a6453b --- /dev/null +++ b/versions/4.0/en/editor/publish/ios/build-example-ios.md @@ -0,0 +1,88 @@ +# iOS Publishing Example + +This article will demonstrate the process of publishing a Cocos Creator project as an iOS application. Before getting started, you will need the following preparations: + +- A macOS device with Xcode installed +- An Apple Developer account + +## Publishing Process + +### Register an Apple Developer account + +Firstly, you need to have an Apple Developer account. If you don't have one, please go to the [registration page](https://appleid.apple.com/account) to create an account. + +### Verify the macOS System and Xcode Version + +Publishing iOS apps via Cocos Creator environment requirements: + +- Xcode version **11.5** or above. +- macOS version **10.14** or above. + +> **Note**: By default, the corresponding Xcode version in the App Store matches the system. If you want to use a specific version of Xcode, you can download it from the [Xcode download page](https://developer.apple.com/xcode/download/). + +### Prepare a Test Project + +Open an existing project or create a new one. + +### Build + +![project-build-menu](./images/project-build-menu.png) + +As shown in the image above, select **Project -> Build** from the top menu in Cocos Creator to open the build panel. + +![ios-build-panel](./images/ios-build-panel.png) + +#### General Options + +In the blue section on the left side of the image, you can find the common parameters that need to be configured for all platforms supported by Cocos Creator. For more details, you can refer to the [General Build Options](./../build-options.md) page. + +#### iOS Specific Options + +Choose the platform as **iOS**, and as you scroll down the panel, you will see the red section on the right side. These are the configurations specific to the `native` and `iOS` platforms. For more details, please refer to the [General Native Build Options](./../native-options.md) and [Build Options - iOS](./build-options-ios.md) pages. + +#### Bundle Identifier + +The Bundle Identifier is a required configuration. It is generally in the format of `com.mycompany.myproduct`. For example, in this example, it is set to `com.cocos.testios`. + +#### Perform the Build + +After completing the configurations, click the **Build** button to generate the Xcode project. + +Once the build is successful, you can click the open file button as shown below to open the generated project folder. + +![ios-build-open-path](./images/ios-build-open-path.png) + +If you haven't changed the build path, you can find the `build/ios/proj` directory in the project root, which contains the following files: + +![ios-xcode-folder](./images/ios-xcode-folder.png) + +### Compile and Run in Xcode + +Double-click on `build/ios/proj/.xcodeproj` to open the Xcode project. + +![ios-xcode-showcase](./images/ios-xcode-showcase.png) + +Select `-mobile` as the build target, choose a suitable simulator or a connected iOS device, and click the compile and run button to start the project. + +![ios-run](./images/ios-run.png) + +## Read More + +### Script-Native Communication + +Sometimes, projects need to call iOS system functions from the script layer, or when integrating a third-party iOS SDK, it is necessary to call its API through script code. In such cases, the communication mechanism between the script layer and the native layer is required. + +For communication mechanisms, please refer to: + +- [JavaScript and iOS/macOS Communication with Reflection](../../../advanced-topics/oc-reflection.md) +- [JavaScript and Objective-C Communication using JsbBridge](./../../../advanced-topics/js-oc-bridge.md) + +### Debugging JavaScript on Native Platforms + +Some issues only occur on specific devices and environments. If you can debug the code on the corresponding situation, you can quickly identify the problem and find a solution. + +Cocos Creator provides a native debugging mechanism that allows you to easily debug game code on a device. For more details, please refer to [Debugging JavaScript on Native Platforms](./../debug-jsb.md). + +## Features and System Versions + +Different features rely on specific system versions. Please refer to [Features and System Versions](./../../../advanced-topics/supported-versions.md) for more details. diff --git a/versions/4.0/en/editor/publish/ios/build-options-ios.md b/versions/4.0/en/editor/publish/ios/build-options-ios.md new file mode 100644 index 0000000000..1d7984592c --- /dev/null +++ b/versions/4.0/en/editor/publish/ios/build-options-ios.md @@ -0,0 +1,50 @@ +# Build Options - iOS + +![ios-build-options](./images/ios-build-options.png) + +## Executable Name + +This a field used to specify the name of the main executable file of an application, which is stored in the app's Info.plist file. If not provided, the system will generate a default value based on the app name field. The value for this field must adhere to a specific format, containing only numbers, letters, underscores (_) and hyphens (-). + +## Bundle Identifier + +The package name, usually in reverse order of the product website URL, such as `com.mycompany.myproduct`. + +> **Note**: The package name can only contain numbers (0-9), letters (A-Z, a-z), hyphens (-), and dots (.), and the last part of the package name must start with a letter and cannot start with an underscore or a number. For more details, please refer to [Bundle Identifier](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier). + +## Skip the update of Xcode project + +By default, each build will execute the CMake command to generate an Xcode project. However, if modifications or configurations are made to the generated Xcode project, such as integrating an SDK with CocoaPods, this could cause issues, as these modifications will be reverted in the next build. + +However, if this option is checked, subsequent builds will no longer update or overwrite the configuration of the Xcode project. + +It should be noted that other changes related to CMake, such as adding C++ source code, will no longer trigger the regeneration of the Xcode project. + +## Orientation + +The screen orientation currently includes three options: **Portrait**, **Landscape Left**, and **Landscape Right**. + +- **Portrait:** The screen is in an upright position with the Home button at the bottom. +- **Landscape Left:** The screen is in a landscape position with the Home button on the left side of the screen. +- **Landscape Right:** The screen is in a landscape position with the Home button on the right side of the screen. + +## OS Target + +This defines the default launch system, types can be used are as follows: + +- iPhone OS: Run on real iOS devices, e.g. an iPhone, and iPad. +- iOS Simulator: Run on the iOS simulator. + +These is just the default value and can be modified at any time in Xcode. + +## Render Backend + +The Render Backend currently supports **METAL**. For more details, refer to the official documentation on [Metal](https://developer.apple.com/cn/metal/). + +## Developer Team + +This option is used to configure the Apple Developer account used for signing the app when building and compiling the iOS project. If you manually configure this information in Xcode during compilation, the configuration in Xcode takes precedence. When performing a rebuild, the value of this option will override the value configured in Xcode. + +## Target iOS Version + +This option is used to specify the iOS software version when publishing to the iOS platform, with a default value of 12.0. The version number after the build will be recorded in the `TARGET_IOS_VERSION` field in the `build/ios/proj/cfg.cmake` file. diff --git a/versions/4.0/en/editor/publish/ios/images/ios-build-open-path.png b/versions/4.0/en/editor/publish/ios/images/ios-build-open-path.png new file mode 100644 index 0000000000..5c8fb969ce Binary files /dev/null and b/versions/4.0/en/editor/publish/ios/images/ios-build-open-path.png differ diff --git a/versions/4.0/en/editor/publish/ios/images/ios-build-options.png b/versions/4.0/en/editor/publish/ios/images/ios-build-options.png new file mode 100644 index 0000000000..1a3368fa05 Binary files /dev/null and b/versions/4.0/en/editor/publish/ios/images/ios-build-options.png differ diff --git a/versions/4.0/en/editor/publish/ios/images/ios-build-panel-a.png b/versions/4.0/en/editor/publish/ios/images/ios-build-panel-a.png new file mode 100644 index 0000000000..0054002269 Binary files /dev/null and b/versions/4.0/en/editor/publish/ios/images/ios-build-panel-a.png differ diff --git a/versions/4.0/en/editor/publish/ios/images/ios-build-panel-b.png b/versions/4.0/en/editor/publish/ios/images/ios-build-panel-b.png new file mode 100644 index 0000000000..e4148b4fdc Binary files /dev/null and b/versions/4.0/en/editor/publish/ios/images/ios-build-panel-b.png differ diff --git a/versions/4.0/en/editor/publish/ios/images/ios-build-panel.png b/versions/4.0/en/editor/publish/ios/images/ios-build-panel.png new file mode 100644 index 0000000000..bb063c6fd6 Binary files /dev/null and b/versions/4.0/en/editor/publish/ios/images/ios-build-panel.png differ diff --git a/versions/4.0/en/editor/publish/ios/images/ios-run.png b/versions/4.0/en/editor/publish/ios/images/ios-run.png new file mode 100644 index 0000000000..f995fcb15e Binary files /dev/null and b/versions/4.0/en/editor/publish/ios/images/ios-run.png differ diff --git a/versions/4.0/en/editor/publish/ios/images/ios-xcode-folder.png b/versions/4.0/en/editor/publish/ios/images/ios-xcode-folder.png new file mode 100644 index 0000000000..e1a7dd0e68 Binary files /dev/null and b/versions/4.0/en/editor/publish/ios/images/ios-xcode-folder.png differ diff --git a/versions/4.0/en/editor/publish/ios/images/ios-xcode-showcase.png b/versions/4.0/en/editor/publish/ios/images/ios-xcode-showcase.png new file mode 100644 index 0000000000..ae439ebaae Binary files /dev/null and b/versions/4.0/en/editor/publish/ios/images/ios-xcode-showcase.png differ diff --git a/versions/4.0/en/editor/publish/ios/images/project-build-menu.png b/versions/4.0/en/editor/publish/ios/images/project-build-menu.png new file mode 100644 index 0000000000..d5cec7521f Binary files /dev/null and b/versions/4.0/en/editor/publish/ios/images/project-build-menu.png differ diff --git a/versions/4.0/en/editor/publish/ios/index.md b/versions/4.0/en/editor/publish/ios/index.md new file mode 100644 index 0000000000..33fcf8373f --- /dev/null +++ b/versions/4.0/en/editor/publish/ios/index.md @@ -0,0 +1,21 @@ +# Publishing iOS Apps + +Cocos Creator supports publishing iOS Apps, but it requires at least one macOS device and one Apple developer account. + +## Content + +- [iOS Publishing Example](./build-example-ios.md) +- [Build Options - iOS](build-options-ios.md) +- [JavaScript and iOS/macOS Communication with Reflection](../../../advanced-topics/oc-reflection.md) +- [JavaScript and Objective-C Communication using JsbBridge](../../../advanced-topics/js-oc-bridge.md) +- [Features and System Versions](../../../advanced-topics/supported-versions.md) + +## Read More + +- [Setting up Native Development Environment](../setup-native-development.md) +- [Build Panel](../build-panel.md) +- [General Build Panel](../build-options.md) +- [General Native Build Options](../native-options.md) +- [Debugging JavaScript on Native Platforms](../debug-jsb.md) +- [Build Process and FAQ](../build-guide.md) +- [Native Platform Secondary Development Guide](../../../advanced-topics/native-secondary-development.md) diff --git a/versions/4.0/en/editor/publish/mac/build-example-mac.md b/versions/4.0/en/editor/publish/mac/build-example-mac.md new file mode 100644 index 0000000000..2d9365ff66 --- /dev/null +++ b/versions/4.0/en/editor/publish/mac/build-example-mac.md @@ -0,0 +1,103 @@ +# macOS Publishing Example + +This article will demonstrate the process of publishing a Cocos Creator project as a macOS application. Before getting started, you will need the following preparations: + +- A macOS device +- Xcode installed + +## Publishing Process + +### Verify the macOS System and Xcode Version + +Publishing macOS desktop apps via Cocos Creator environment requirements: + +- Xcode version **11.5** or above. +- macOS version **10.14** or above. + +> **Note**: By default, the corresponding Xcode version in the App Store matches the system. If you want to use a specific version of Xcode, you can download it from the [Xcode download page](https://developer.apple.com/xcode/download/). + +### Prepare a Test Project + +Open an existing project or create a new one. + +### Build + +![project-build-menu](./images/project-build-menu.png) + +As shown in the image above, select **Project -> Build** from the top menu in Cocos Creator to open the build panel. + +![build-panel-mac](./images/build-panel-mac.png) + +#### General Options + +In the blue section on the left side of the image, you can find the common parameters that need to be configured for all platforms supported by Cocos Creator. For more details, you can refer to the [General Build Options](./../build-options.md) page. + +#### macOS Specific Options + +Choose the platform as **Mac**, and as you scroll down the panel, you will see the red section on the right side. These are the configurations specific to the `native` and `Mac` platforms. For more details, please refer to the [General Native Build Options](./../native-options.md) and [Build Options - macOS](./build-options-mac.md) pages. + +#### Bundle Identifier + +The Bundle Identifier is a required configuration. It is generally in the format of `com.mycompany.myproduct`. For example, in this example, it is set to `com.cocos.mac`. + +#### Perform the Build + +After completing the configurations, click the **Build** button to generate the Xcode project. + +Once the build is successful, you can click the open file button as shown below to open the generated project folder. + +![build-open-path-mac](./images/build-open-path-mac.png) + +If you haven't changed the build path, you can find the `build/mac/proj` directory in the project root, which contains the following files: + +![xcode-folder-mac](./images/xcode-folder-mac.png) + +### Compile and Run in Xcode + +Double-click on `build/mac/proj/.xcodeproj` to open the Xcode project. + +![xcode-showcase-mac](./images/xcode-showcase-mac.png) + +Select `-desktop` as the build target, choose a suitable simulator or a connected Mac device, and click the compile and run button to start the project. + +![run-mac](./images/run-mac.png) + +### Modifying Resolution + +You can modify the resolution by editing `_windowInfo` in `native/engine/common/classes/Game.cpp`: + +The default resolution is 800 x 600. Let's take the example of changing the resolution to 800 x 400. + +```C++ +int Game::init() { + _windowInfo.title = GAME_NAME; + // configure window size + _windowInfo.width = 800; + _windowInfo.height = 400; +} +``` + +After making the modifications, compile and run the project again in Xcode. The effect should be as shown in the following image: + +![run-mac-800to400](./images/run-mac-800to400.png) + +## Read More + +### Script-Native Communication + +Sometimes, projects need to call macOS system functions from the script layer, or when integrating a third-party macOS SDK, it is necessary to call its API through script code. In such cases, the communication mechanism between the script layer and the native layer is required. + +For communication mechanisms, please refer to: + +- [JavaScript and iOS/macOS Communication with Reflection](../../../advanced-topics/oc-reflection.md) +- [JavaScript and Objective-C Communication using JsbBridge](./../../../advanced-topics/js-oc-bridge.md) + +### Debugging JavaScript on Native Platforms + +Some issues only occur on specific devices and environments. If you can debug the code on the corresponding situation, you can quickly identify the problem and find a solution. + +Cocos Creator provides a native debugging mechanism that allows you to easily debug game code on a device. For more details, please refer to [Debugging JavaScript on Native Platforms](./../debug-jsb.md). + +## Features and System Versions + +Different features rely on specific system versions. Please refer to [Features and System Versions](./../../../advanced-topics/supported-versions.md) for more details. diff --git a/versions/4.0/en/editor/publish/mac/build-options-mac.md b/versions/4.0/en/editor/publish/mac/build-options-mac.md new file mode 100644 index 0000000000..e0fbe2643c --- /dev/null +++ b/versions/4.0/en/editor/publish/mac/build-options-mac.md @@ -0,0 +1,33 @@ +# Build Options - macOS + +![build-options-mac](./images/build-options-mac.png) + +## Executable Name + +This a field used to specify the name of the main executable file of an application, which is stored in the app's Info.plist file. If not provided, the system will generate a default value based on the app name field. The value for this field must adhere to a specific format, containing only numbers, letters, underscores (_) and hyphens (-). + +## Bundle Identifier + +The package name, usually in reverse order of the product website URL, such as `com.mycompany.myproduct`. + +> **Note**: The package name can only contain numbers (0-9), letters (A-Z, a-z), hyphens (-), and dots (.), and the last part of the package name must start with a letter and cannot start with an underscore or a number. For more details, please refer to [Bundle Identifier](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier). + +## Target Version + +This option is primarily used to specify the target system version when publishing for the Mac platform, with a default value of 10.14. The version number after the build will be recorded in the `TARGET_OSX_VERSION` field of the `build/mac/proj/cfg.cmake` file in the project directory. + +## Support Apple Silicon + +This option is used to provide better prompts for known engine module support issues on Apple M1 (Silicon) architecture devices. + +## Skip the update of Xcode project + +By default, each build will execute the CMake command to generate an Xcode project. However, if modifications or configurations are made to the generated Xcode project, such as integrating an SDK with CocoaPods, this could cause issues, as these modifications will be reverted in the next build. + +However, if this option is checked, subsequent builds will no longer update or overwrite the configuration of the Xcode project. + +It should be noted that other changes related to CMake, such as adding C++ source code, will no longer trigger the regeneration of the Xcode project. + +### Render Backend + +The Render Backend currently supports **METAL**. For more details, refer to the official documentation on [Metal](https://developer.apple.com/cn/metal/). diff --git a/versions/4.0/en/editor/publish/mac/images/build-open-path-mac.png b/versions/4.0/en/editor/publish/mac/images/build-open-path-mac.png new file mode 100644 index 0000000000..d9c17541cf Binary files /dev/null and b/versions/4.0/en/editor/publish/mac/images/build-open-path-mac.png differ diff --git a/versions/4.0/en/editor/publish/mac/images/build-options-mac.png b/versions/4.0/en/editor/publish/mac/images/build-options-mac.png new file mode 100644 index 0000000000..751908d1eb Binary files /dev/null and b/versions/4.0/en/editor/publish/mac/images/build-options-mac.png differ diff --git a/versions/4.0/en/editor/publish/mac/images/build-panel-mac.png b/versions/4.0/en/editor/publish/mac/images/build-panel-mac.png new file mode 100644 index 0000000000..85e69f6810 Binary files /dev/null and b/versions/4.0/en/editor/publish/mac/images/build-panel-mac.png differ diff --git a/versions/4.0/en/editor/publish/mac/images/project-build-menu.png b/versions/4.0/en/editor/publish/mac/images/project-build-menu.png new file mode 100644 index 0000000000..d5cec7521f Binary files /dev/null and b/versions/4.0/en/editor/publish/mac/images/project-build-menu.png differ diff --git a/versions/4.0/en/editor/publish/mac/images/run-mac-800to400.png b/versions/4.0/en/editor/publish/mac/images/run-mac-800to400.png new file mode 100644 index 0000000000..1c047e2bb6 Binary files /dev/null and b/versions/4.0/en/editor/publish/mac/images/run-mac-800to400.png differ diff --git a/versions/4.0/en/editor/publish/mac/images/run-mac.png b/versions/4.0/en/editor/publish/mac/images/run-mac.png new file mode 100644 index 0000000000..831e2e222e Binary files /dev/null and b/versions/4.0/en/editor/publish/mac/images/run-mac.png differ diff --git a/versions/4.0/en/editor/publish/mac/images/xcode-folder-mac.png b/versions/4.0/en/editor/publish/mac/images/xcode-folder-mac.png new file mode 100644 index 0000000000..0afdda5796 Binary files /dev/null and b/versions/4.0/en/editor/publish/mac/images/xcode-folder-mac.png differ diff --git a/versions/4.0/en/editor/publish/mac/images/xcode-showcase-mac.png b/versions/4.0/en/editor/publish/mac/images/xcode-showcase-mac.png new file mode 100644 index 0000000000..8f2b05b756 Binary files /dev/null and b/versions/4.0/en/editor/publish/mac/images/xcode-showcase-mac.png differ diff --git a/versions/4.0/en/editor/publish/mac/index.md b/versions/4.0/en/editor/publish/mac/index.md new file mode 100644 index 0000000000..d5b60f09a2 --- /dev/null +++ b/versions/4.0/en/editor/publish/mac/index.md @@ -0,0 +1,21 @@ +# Publishing macOS Apps + +Cocos Creator supports publishing macOS Apps, but it requires at least one macOS device and one Apple developer account. + +## Content + +- [macOS Publishing Example](./build-example-mac.md) +- [Build Options - macOS](build-options-mac.md) +- [JavaScript and iOS/macOS Communication with Reflection](../../../advanced-topics/oc-reflection.md) +- [JavaScript and Objective-C Communication using JsbBridge](../../../advanced-topics/js-oc-bridge.md) +- [Features and System Versions](../../../advanced-topics/supported-versions.md) + +## Read More + +- [Setting up Native Development Environment](../setup-native-development.md) +- [Build Panel](../build-panel.md) +- [General Build Panel](../build-options.md) +- [General Native Build Options](../native-options.md) +- [Debugging JavaScript on Native Platforms](../debug-jsb.md) +- [Build Process and FAQ](../build-guide.md) +- [Native Platform Secondary Development Guide](../../../advanced-topics/native-secondary-development.md) diff --git a/versions/4.0/en/editor/publish/native-options.md b/versions/4.0/en/editor/publish/native-options.md new file mode 100644 index 0000000000..f4140f17f0 --- /dev/null +++ b/versions/4.0/en/editor/publish/native-options.md @@ -0,0 +1,125 @@ +# Publishing to Native Platforms + +To access the **Build** panel, go to the top menu bar and select **Project -> Build**. + +Cocos Creator currently supports deployment to native platforms including **Android**, **iOS**, **Mac** and **Windows**. Options for publishing to **iOS**, **Mac**, and **Windows** are only accessible when using their corresponding operating systems. + +![native platform](publish-native/native-platform.png) + +## General Native Build Options + +![Native Options](publish-native/native-options.png) + +### Encrypt JS + +Once enabled, scripts in the project will be encrypted and encapsulated in a .jsc file which can be found in the `assets` folder of the project directory. The original script files will be stored in the `script-backup` folder for debugging purposes and will not be added to the release package. + +**JS Encryption Key**: Value will be used as the encryption key for script encryption and will be generated randomly at the project’s creation. + +**Zip compression**: If enabled, scripts will be compressed, reducing package size. + +![encrypt js](publish-native/encrypt-js.png) + +### Native Engine + +Users may choose Cocos Creator’s built-in native engine or their custom typescript engine to deploy to native platforms. Click **Edit** to open the **Preferences** panel and enter the custom engine directory under the **[Engine Manager](../preferences/index.md#engine-manager)** tab. + +### Job System + +Job System is managed by the internal mechanisms and does not require manual modifications. Should such need emerges, please take note: + +1. TBB / TaskFlow gives varying performances depending on the execution environment and project specifications, thus should be selected accordingly. +2. Cocos Creator supports specific versions of TBB / TaskFlow for deployment. For more information, please see below in section **Compatibility**. + +### Automatically Execute Next Step + +At the bottom right of the **Build** panel, there are three buttons: **Build**, **Make**, and **Run**. By default, you need to click the corresponding button to perform the respective action. + +If you want, you can click the **Link** button between two buttons, and it will turn into an **Arrow**. This means that the next step pointed to by the arrow will be automatically executed after the completion of the previous step. Clicking the arrow again will restore it to its previous state. + +## Other Build Options + +To publish to native platforms, you need to install and configure some necessary environments. For more details, please refer to [Setting up Native Development Environment](setup-native-development.md). + +To view the common options for all platforms, please refer to [General Build Options](build-options.md). + +To view platform-specific build options, please refer to: + +[Build Options - Android](./android/build-options-android.md) +[Build Options - iOS](./ios/build-options-ios.md) +[Build Options - Windows](./windows/build-options-windows.md) +[Build Options - macOS](./mac/build-options-mac.md) + +## Start the Build Process + +With the builder properties set, click the **Build** button at the bottom right corner of the **Build** panel to start the build process. + +While building is in progress, a blue progress bar will be displayed on the Build Tasks page in the Build panel. If the build is successfully done, the progress bar will reach 100% and turn green. + +![build progress](publish-native/build-progress-windows.png) + +### Native Project Directory + +The native project built by Cocos Creator includes the native common directory, native platform directory, and native project directory. For more details, please refer to the [Native Platform Secondary Development Guide](../../advanced-topics/native-secondary-development.md). + +## Compile and Run + +In Cocos Creator, you can compile and run your project through the buttons on the **Build** panel. You can also open the project in the corresponding IDE for each platform, such as Xcode for iOS and macOS, Android Studio for Android, or Visual Studio for Windows. This allows you to perform further compilation, running, debugging, and publishing tasks. + +### Via Cocos Creator + +After the build process is completed, click the **Make** button to start compiling, which will prompt the following message: + +`make package YourProjectBuildPath success!` + +> **Note**: When building targeting Android platform, it is needed to open the built project in Android Studio, download and install the missing modules and plug-ins as prompted first, then return to Cocos Creator and repeat the operations above. + +After the "Make" process is complete, click the **Run** button next to it. Some unfinished compilation may still be ongoing at this point. Please wait patiently or check the progress through the log file. After compilation is done, users will get: + +- Mac / Windows: Preview will be executed on desktop. +- Android: Preview can be executed on a USB-connected device provided the device has USB debugging enabled. +- iOS: Preview will be executed via a simulator, but it is recommended to execute on a device via Xcode. + +### Via IDE + +Users can access the build directory with the **folder** button at the lower left corner of each **build task** listed in the **Build** panel. Under `proj` folder in the build directory is a project file that can be opened with IDEs. + +Open the project in the IDEs of their respective platform (Xcode for iOS, Android Studio for Android, Visual Studio for Windows, etc.) where users may continue with regular operations such as compilation, preview and release. + +- **Android** + + ![android xcode](publish-native/android-studio.png) + +- **Windows** + + ![windows xcode](publish-native/windows-vs.png) + +- **iOS** and **Mac** + + ![ios xcode](publish-native/ios-xcode.png) + +> **Note**: It is not advisable to do secondary development in IDEs at this point as they will be overridden if the project is rebuilt by Cocos Creator. + +To learn more about debugging in IDEs, please refer to [Debugging JavaScript on Native Platforms](debug-jsb.md). + +## Considerations + +1. When running a debug build on MIUI 10, system may prompt the message "Detected problems with API compatibility." This issue will not emerge with release builds. + +2. When deploying a project to iOS without WebView functionalities incorporated, please be sure to disable the WebView module in Cocos Creator at **Project -> Project Settings -> Feature Cropping**. This will noticeably increase the success rate of the project passing iOS App Store’s automatic submission review. If the project requires WebView or related third-party SDKs incorporated to function properly, it is likely to be flagged by the submission review system, in which case it is advisable to contact Apple for an appeal. + +3. When building for Android, compilation output of Cocos Creator and Android Studio may differ in the following ways: + + - After the **Make** process is executed in Cocos Creator, the `build` folder will be generated in the release directory, and the .apk package will be generated in the `build\app\publish\` folder of the build directory. + + - After the project is compiled in Android Studio, the .apk package will be generated in the `proj\app\build\outputs\apk` folder. + +4. Projects targeting Android and Android Instant are built with the same template. Build output can be found in the `build\android\proj` folder. Regarding this folder, please take note: + + - Code intended for the Android platform should be stored in the `app\src` folder. Third-party libraries should be stored separately in the `app\libs` folder. If these folders do not exist in the build directory, users can create them manually. + + - Code intended for the Android Instant should be stored in the `instantapp\src` folder. Third-party libraries should be stored separately in the `instantapp\libs` folder. If these folders do not exist in the build directory, users can create them manually. + + - Code and libraries shared by Android and Android Instant should be stored in the `src` and `libs` folders respectively. If these folders do not exist in the build directory, users can create them manually. + + When compiling for Android by clicking the **Make** button in the **Build** panel, `assembleRelease/Debug` will be executed by default. Similarly, `instantapp:assembleRelease/Debug` will be executed by default when compiling for Android Instant. diff --git a/versions/4.0/en/editor/publish/publish-alipay-mini-game.md b/versions/4.0/en/editor/publish/publish-alipay-mini-game.md new file mode 100644 index 0000000000..f05f3ff8de --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-alipay-mini-game.md @@ -0,0 +1,49 @@ +# Publish to Alipay Mini Game + +## Environment Configuration + +- Download [Alipay Mini Program Studio](https://render.alipay.com/p/f/fd-jwq8nu2a/pages/home/index.html) to the desktop and install it. + +- Download [Alipay](https://mobile.alipay.com/index.htm) and install it on your mobile device. + +- The minimum version supported of the Alipay client on Android is 10.3.70 and on iOS is 10.3.70. + +## Publishing Process + +Use Cocos Creator to open the project to be published, select **Alipay Mini Game** in **Platform** of the **Build** panel, and then click **Build**. + +![build option](./publish-alipay-mini-game/build_option.png) + +Please refer to the [build options](build-options.md) documentation for general build options settings. The rules for filling out build options related to Alipay Mini Game are as follows: + +| Build Option | Description | Field Name (for command-line publishing) | +| :-- | :-- | :-- | +| Start Scene Asset Bundle | When checked, the first scene and its related dependencies will be built into the built-in Asset Bundle - [start-scene](../../asset/bundle.md#the-built-in-asset-bundle) under the release package directory `assets` to improve the speed of loading assets for the start scene. | `startSceneAssetBundle` | +| Orientation | Optional values include `landscape` and `portrait`. | `deviceOrientation` | +| Resource Server Address | Used to fill in the address of the remote server where the resource is stored. Developers need to manually upload the remote folder in the release package directory to the filled in resource server address after the build. For details, please refer to the [Uploading Resources to Remote Servers](../../asset/cache-manager.md) documentation | `remoteUrl` | +| Polyfills | Build polyfills that support some new features, mainly when packaging scripts. Currently only **Async Functions** are supported, more features will be opened later. | `polyfills` | + +After the build is finished, click the folder icon button in the bottom left corner of **Build Task**, you can see that the project folder `alipay-mini-game` is created in the `build` directory of the project, which already contains the configuration file `game.json` for the Alipay Mini Game environment. + +![build](./publish-alipay-mini-game/build.png) + +Use **Alipay Mini Program Studio** to open the `alipay-mini-game` folder generated by the build to open the Alipay Mini Game project and preview/debug the game content. + +![preview](./publish-alipay-mini-game/preview.png) + +## Resource management for Alipay Mini Game environment + +Alipay Mini Game are similar to WeChat Mini Game in that there is a package size limit, and additional resources over **4MB** must be downloaded via network requests.
When the package size is too large, configure the **Resource Server Address** option in the **Build** panel to upload the resources to a remote server, please refer to the [Upload Resources to Remote Server](../../asset/cache-manager.md) documentation. + +After the game starts, the engine will automatically download resources from the remote server address. Once the resources are downloaded, the engine's cache manager will record the save paths of these resources. This information is used to automatically delete some cached game resources when the cache space is insufficient. Please refer to [Cache Manager](../../asset/cache-manager.md) for more details. + +## Restrictions for Alipay Mini Game + +The following modules are still not supported: + +- WebView +- VideoPlayer + +## Documentation + +Since Alipay Mini Game related documentation is currently only available internally, developers can directly contact the members of Alipay integration group if needed. diff --git a/versions/4.0/en/editor/publish/publish-alipay-mini-game/build.png b/versions/4.0/en/editor/publish/publish-alipay-mini-game/build.png new file mode 100644 index 0000000000..f049bc5d2b Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-alipay-mini-game/build.png differ diff --git a/versions/4.0/en/editor/publish/publish-alipay-mini-game/build_option.png b/versions/4.0/en/editor/publish/publish-alipay-mini-game/build_option.png new file mode 100644 index 0000000000..c8446c9d70 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-alipay-mini-game/build_option.png differ diff --git a/versions/4.0/en/editor/publish/publish-alipay-mini-game/preview.png b/versions/4.0/en/editor/publish/publish-alipay-mini-game/preview.png new file mode 100644 index 0000000000..993bd1334b Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-alipay-mini-game/preview.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidu-mini-game.md b/versions/4.0/en/editor/publish/publish-baidu-mini-game.md new file mode 100644 index 0000000000..7478bd5531 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-baidu-mini-game.md @@ -0,0 +1,97 @@ +# Publish to Baidu Mini Games + +> **Note**: +> 1. As of November 30, 2022, the publishing and building of Baidu mini-games will no longer be supported as the relevant contract has expired. +> +> 2. some platforms only have Chinese documentation available when visiting the platform's website. It may be necessary to use Google Translate in-order to review the documentation. + +**Cocos Creator** officially supports the release of games to the **Baidu Mini Games**. + +The runtime environment of the Baidu Mini Game is an extension of the **Baidu Smart Mini Program**. This provides a WebGL interface encapsulation based on the mini program environment. This greatly improves the rendering capabilities and performance. However, since these interfaces are encapsulated by the Baidu team, they are not equivalent to the browser environment. + +On the engine side, in order to make the developers' workload as easy as possible, our main tasks for developers include the following: + +- The engine framework adapts to the Baidu Mini Game API, pure game logic level, developers do not need any additional modifications. +- The **Cocos Creator** editor provides a fast packaging process, released directly as a **Baidu Mini Game**, and automatically evokes the Baidu DevTools. +- Automatically load remote assets, cache assets, and cache asset version control. + +Please refer to the [Baidu Mini Game Developer Documentation [cn]](https://smartprogram.baidu.com/docs/game/) documentation to review the game submission, the review, and the release process for a Baidu Mini Game. + +## Publish Baidu Mini Games with Cocos Creator + +### Prerequisites + +- Download and install **Baidu DevTools** in the [Baidu DevTools [cn]](https://smartprogram.baidu.com/docs/game/tutorials/howto/dev/) documentation. +- Download and install the **Baidu App** in the app store of your phone. +- Log in to the [Baidu Smart Mini Program Platform [cn]](https://smartprogram.baidu.com/developer/index.html) and find **App ID**. + + ![appid](./publish-baidugame/appid.png) + +### Release process + +1. Select the **Baidu Mini Game** in the **Platform** of the **Build** panel, fill in the **appid**, and then click **Build**. + + ![build](./publish-baidugame/build.png) + +2. After a build is completed, a `baidu-mini-game` folder will be generated in the project's **build** directory (the name of the folder is based on the **Build Task Name**), which already contains the configuration files `game.json` and `project.swan.json` of the Baidu Mini Games environment. + + ![package](./publish-baidugame/package.png) + +3. Use the **Baidu DevTools** to open the `baidu-mini-game` folder to preview and debug the game. Please refer to the Baidu DevTools documentation for details. About how ​​to use Baidu DevTools, please refer to [Baidu DevTools Documentation](https://smartprogram.baidu.com/docs/game/tutorials/howto/dev/) for details. + + ![preview](./publish-baidugame/preview.png) + + > **Note**: when previewing and debugging, if a prompt appears stating: `The current version of the developer tool can't publish mini program, please update to the latest devtools`. This means the **appid** filled in the **Build** panel is the **appid** of the **Baidu Smart Mini Program**, not the **appid** of the **Baidu Mini Game**, please re-apply for the **appid** of the **Baidu Mini Game**. + +## asset Management for Baidu Mini Game Environment + +**Baidu Mini Game** is similar to **WeChat Mini Game**. There are restrictions on the package size. Assets more than **4MB** must be downloaded via a network request. + +It is recommended to only save script files in the mini-game packages, while other assets are uploaded to the remote server, and downloaded from the remote server as needed. The download, cache, and version management of remote assets, **Cocos Creator** has already done it for you. The specific implementation logic is similar to the WeChat Mini Game. Please refer to the [asset Management for WeChat Mini Game Environment](./publish-wechatgame.md) documentation for details. + +When the **MD5 Cache** feature of the engine is enabled, the URL of the file will change as the content of the file changes. When the game releases a new version, the assets of the old version will naturally become invalid in the cache, and only the new assets can be requested from the server, which achieves the effect of version control. + +Specifically, developers need to do the following: + +1. When building, check the **MD5 Cache** in the **Build** panel. +2. Set **Remote server address** in the **Build** panel and then click **Build**. +3. After the build is complete, upload the `build/baidu-mini-game/res` folder to the server. +4. Delete the `res` folder under the local release package directory. + +> **Note**: when Baidu loads the assets on the remote server on the physical device, it only supports access via HTTPS, so the asset file must be placed on HTTPS, otherwise the loading of the asset will fail. +> **Note**: if the cache asset exceeds the environment limit of Baidu, you need to manually clear the asset. Use the `remoteDownloader.cleanAllCaches()` and `remoteDownloader.cleanOldCaches()` interfaces to clear the cache in Baidu mini game. The former will clear all cache assets in the cache directory, please use it with caution. The latter will clear the cache assets that are not used in the current application in the cache directory. + +## Baidu Mini Game Subpackage Loading + +The subpackage loading method of **Baidu Mini Game** is similar to WeChat, with the following package restrictions: + +- The size of all subpackages of the entire Mini Game can not exceed **8MB**. +- The size of a single subpackage/main package can not exceed **4MB**. + +Please refer to the [Mini Game Subpackage](subpackage.md) documentation for details. + +## Platform SDK Access + +In addition to pure game content, the **Baidu Mini Game** environment also provides a very powerful native SDK interface. These interfaces only exist in **Baidu Mini Game** environment, equivalent to the third-party SDK interface of other platforms. The porting of such SDK interfaces still needs to be handled by developers at this stage. Here are some of the powerful SDK capabilities offered by **Baidu Mini Game**: + +1. User interface: login, authorization, user information, etc. +2. Baidu cashier payment +3. Forwarding information +4. File upload and download +5. Other: images, locations, ads, device information, etc. + +## Baidu Mini Games known issues + +Currently, the adaptation work of Baidu Mini Game is not completely finished, and the following components are not supported for the time being: + +- VideoPlayer +- WebView + +If needed, you can directly call Baidu's [API [cn]](https://smartprogram.baidu.com/docs/game/api/openApi/authorize/) as needed. + +## Reference documentation + +- [Baidu Mini Game Registration Guide [cn]](https://smartprogram.baidu.com/docs/game/) +- [Baidu DevTools documentation [cn]](https://smartprogram.baidu.com/docs/game/tutorials/howto/dev/) +- [Baidu Mini Game API documentation [cn]](https://smartprogram.baidu.com/docs/game/api/openApi/authorize/) +- [Baidu Mini Game Subpackage Loading [cn]](https://smartprogram.baidu.com/docs/game/tutorials/subpackages/sub/) diff --git a/versions/4.0/en/editor/publish/publish-baidugame/appid.png b/versions/4.0/en/editor/publish/publish-baidugame/appid.png new file mode 100644 index 0000000000..513e32cf6f Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/appid.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidugame/build.png b/versions/4.0/en/editor/publish/publish-baidugame/build.png new file mode 100644 index 0000000000..027d437075 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/build.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidugame/game-json.png b/versions/4.0/en/editor/publish/publish-baidugame/game-json.png new file mode 100644 index 0000000000..8581558bb6 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/game-json.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidugame/maintest-build.png b/versions/4.0/en/editor/publish/publish-baidugame/maintest-build.png new file mode 100644 index 0000000000..8f54aabd5a Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/maintest-build.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidugame/open-data-project-build.png b/versions/4.0/en/editor/publish/publish-baidugame/open-data-project-build.png new file mode 100644 index 0000000000..732fc1895c Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/open-data-project-build.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidugame/open-data-project-package.png b/versions/4.0/en/editor/publish/publish-baidugame/open-data-project-package.png new file mode 100644 index 0000000000..1a8d821791 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/open-data-project-package.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidugame/open-data-project-preview.png b/versions/4.0/en/editor/publish/publish-baidugame/open-data-project-preview.png new file mode 100644 index 0000000000..c5d8590dba Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/open-data-project-preview.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidugame/package.png b/versions/4.0/en/editor/publish/publish-baidugame/package.png new file mode 100644 index 0000000000..b1055fe429 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/package.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidugame/preview.png b/versions/4.0/en/editor/publish/publish-baidugame/preview.png new file mode 100644 index 0000000000..89a6a30cd7 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/preview.png differ diff --git a/versions/4.0/en/editor/publish/publish-baidugame/subcontext.png b/versions/4.0/en/editor/publish/publish-baidugame/subcontext.png new file mode 100644 index 0000000000..0c2b1bc11b Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-baidugame/subcontext.png differ diff --git a/versions/4.0/en/editor/publish/publish-bytedance-mini-game.md b/versions/4.0/en/editor/publish/publish-bytedance-mini-game.md new file mode 100644 index 0000000000..7168163675 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-bytedance-mini-game.md @@ -0,0 +1,134 @@ +# Publish to Douyin Mini Games + +> **Note**: some platforms only have Chinese documentation available when visiting the platform's website. It may be necessary to use Google Translate in-order to review the documentation. + +**Douyin Mini Games** are developed based on Douyin full products, which do not require users to download. This is a brand-new game type that can be played on tap. + +The game submission, review and release process of a mini-game needs to comply with the requirements and standard processes of the Douyin official specification. For specific information, please refer to the links at the end of this document. + +## Preparation + +1. Download the [Douyin DevTools [cn]](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/developer-instrument/developer-instrument-update-and-download) and install it.. + +## Reference Links + +- [ByteDance Mini Game Integration Guide](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/guide/minigame/introduction/) +- [ByteDance Mini Game Developer Documentation](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/bytedance-mini-game) +- [ByteDance Mini Game API Documentation](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/api/overview) +- [ByteDance Developer Tools Download](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/developer-instrument/developer-instrument-update-and-download) +- [ByteDance Developer Tools Documentation](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/dev-tools/developer-instrument/development-assistance/mini-app-developer-instrument) + +2. Refer to the [Douyin Mini Game Access Guide [cn]](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/guide/minigame/introduction) to register, login and apply for mini games on the [Douyin Developer Platform [cn]](https://microapp.bytedance.com). + +3. After the application is approved, find the appid in **Development Management -> Development Settings** of the developer platform. + + ![appid](./publish-bytedance-mini-game/appid.png) + +## Publishing Process + +1. Use Cocos Creator to open the project to be published, open the **Build** panel from **Menu Bar -> Project**, select **Douyin Mini Game** in **Platform**, fill in the **appid**, and click **Build**. + + ![build](./publish-bytedance-mini-game/build.png) + + Click on the **Douyin Mini Game** below to expand the build options configuration of Douyin Mini Game: + + ![Douyin options](./publish-bytedance-mini-game/build-options.png) + + For the general build options settings, please refer to [General Build Options](build-options.md) for details. The Douyin Mini Games specific build options are shown below, please refer to the **Build Options** section below for the filling rules. + +2. After the relevant parameters of the **Build** panel are set, click **Build** button.
When the build is complete, click the folder icon button below the corresponding build task to open the build release path, notice that a directory `bytedance-mini-game` (with the same name as the Build Task Name) is generated in the default release path `build` directory, which already contains the configuration files `game.json` and `project.config.json` of the ByteDance Mini Games environment. + + ![package](./publish-bytedance-mini-game/package.png) + +3. Use the **Douyin DevTools** to open the `bytedance-mini-game` folder generated by the build, open the Douyin game project and preview the debugging game content. Please refer to the [Introduction to Douyin DevTools [cn]](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/developer-instrument/development-assistance/mini-app-developer-instrument) for details on how to use the developer tools. + + ![tool](./publish-bytedance-mini-game/tool.jpg) + +### Build Options + +| Options | Description | Field name (for publishing with command line) | +| :-- | :-- | :-- | +| **Start Scene Asset Bundle** | If set, the start scene and its related dependent resources are built into the built-in Asset Bundle — [start-scene](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) to speed up the resource loading of the start scene. | `startSceneAssetBundle` | +| **Orientation** | Device orientation, it will be written to `game.json` file. | `orientation` | +| **AppID** | The appid of the Douyin Mini Games, it will be written to `project.config.json` file. | `appid` | +| **Remote server address** | This option is used to fill in the address of the remote server where the resources are stored. Developers need to manually upload the `remote` folder from the release package directory to the filled resource server after build. | `remoteServerAddress` | +| **Open data context root** | For access to Open Data Context, please refer to the [Open Data Context](./build-open-data-context.md) documentation for details. | `buildOpenDataContextTemplate` | +| **Native PhysX Physics System**| This option is used to set whether to use the PhysX physics system, please see the **Native Physics** section below for more details. | - | + +## Native Physics + +The Douyin platform has always been committed to providing developers with the more powerful performance and basic capabilities. TikTok will provide PhysX native physics interfaces for Douyin mini games in v16.3 and later versions. + +Cocos Creator and the Douyin platform have conducted in-depth collaboration, with the experimental feature in **v3.2** to support the use of PhysX physics capabilities provided by the Douyin platform in Douyin Mini Games, optimizing the performance of physics calculations, and having a nearly 100% performance improvement compared to Bullet physics: + +![compare performance](./publish-bytedance-mini-game/performance.png) + +The prerequisite for using native physics is to set **Physics System** to **PhysX** in **Project -> Project Settings -> Feature Cropping** in the Creator main menu bar. Then open the **Build** panel with the **Platform** option set to **Douyin Mini Game**, and see the native physics related configuration options as follows: + +![PhysX options](./publish-bytedance-mini-game/physx-options.png) + +| Options | Description | Field name (for publishing with command line) | +| :-- | :-- | :-- | +| **Not to pack the built-in PhysX library**| If checked, the built-in PhysX library files will not be packaged when building, which helps to reduce the package size, but does not support PhysX running on some application platforms of Douyin, such as TikTok (iOS version) and DevTools.
If unchecked, Creator supports PhysX running on the full application platform of Douyin. | `physX.notPackPhysXLibs` | +| **Multithreading mode**| If checked, multithreading mode is enabled. | `physX.multiThread` | +| **Number of sub-threads** | The number of sub-threads when multithreading mode is enabled. | `physX.subThreadCount` | +| **Tolerance of errors** | If this option is enabled, the precision will be lower when using multithreading than single threading. | `physX.epsilon` | + +## Subpackage Loading + +The Douyin Mini Game requires a specific version to support the subpackage feature, and the version requirements for the Douyin product are as follows: + +| Product | Android | iOS | +| :-- | :--- | :--- | +| TikTok | v13.6.0 | v13.7.0 | +| TouTiao | v7.9.9 | v7.9.8 | + +For Douyin DevTools, please use **v2.0.6** or above, but below **v3.0.0**. After updating the DevTools, don't forget to modify the **Debug Base library** to **1.88.0** or above in the Douyin DevTools. + +> **Note**: if the product version does not support subpackage loading, the engine will load the subpackage as a normal asset bundle. + +### Ordinary game packages + +In the scenario of not configuring sub-packaging, the total size of code packages allowed to be uploaded for each mini-game is capped at 20MB. + +### Packages after packetization + +For games with subpackaging configured, the default limit is: + +- The size of the whole game package (the entire directory of the game package) should not exceed 20MB. +- The size of a single main package should not exceed 4MB. +- The size of a single sub-package should not exceed 20MB. + +For details, please refer to the [Douyin Subpackage Loading Official Documentation [cn]](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/framework/subpackages/introduction/) + +## Asset Management for Douyin Mini Game Environment + +In a **Douyin Mini Game** environment, asset management is the most special part. It differs from the browser in the following four points: + +1. The size of the **Douyin Mini Game** package cannot exceed **4MB**, including all the code and assets. Additional assets must be downloaded via web request. + +2. For files downloaded from a remote server, the **Douyin Mini Game** environment does not have the browser's caching and outdated update mechanism. + +3. For the assets in the **Douyin Mini Game** package, they are not loaded on demand in the mini game environment, but rather all the assets in the package are loaded at once, and then the game page is launched. + +4. You cannot download script files from a remote server. + +This brings up two key issues, home page loading speed and remote asset caching and version management. For the home page loading speed, we recommend that developers only save the script file in the **Douyin Mini Game** package, and all other assets are downloaded from the remote server. As for downloading, caching and version management of remote assets, Cocos Creator has done the job for developers. + +Specifically, developers need to do the following: + +1. When building, enable the **MD5 Cache** in the **Build** config panel. +2. Set the **Remote service address**, and then click **Build**. +3. When the build is complete, upload the **res** folder in the mini game release package to the server. +4. Delete the **res** folder inside the local release package. +5. For the test phase, you may not be able to deploy to the official server, you need to use the local server to test, then open the details page in the WeChat DevTools, check the `Does not verify valid domain names, web-view (business domain names), TLS versions and HTTPS certificates` option in the **Local Settings**. + +## Reference documentation + +> **Note**: some platforms only have Chinese documentation available when visiting the platforms website. It may be necessary to use Google Translate in-order to review the documentation. + +- [ByteDance Mini Game Integration Guide](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/guide/minigame/introduction/) +- [ByteDance Mini Game Developer Documentation](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/bytedance-mini-game) +- [ByteDance Mini Game API Documentation](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/api/overview) +- [ByteDance Developer Tools Download](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/developer-instrument/developer-instrument-update-and-download) +- [ByteDance Developer Tools Documentation](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/dev-tools/developer-instrument/development-assistance/mini-app-developer-instrument) diff --git a/versions/4.0/en/editor/publish/publish-bytedance-mini-game/appid.png b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/appid.png new file mode 100644 index 0000000000..fce04c9524 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/appid.png differ diff --git a/versions/4.0/en/editor/publish/publish-bytedance-mini-game/build-options.png b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/build-options.png new file mode 100644 index 0000000000..10449da610 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/build-options.png differ diff --git a/versions/4.0/en/editor/publish/publish-bytedance-mini-game/build.png b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/build.png new file mode 100644 index 0000000000..4076798011 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/build.png differ diff --git a/versions/4.0/en/editor/publish/publish-bytedance-mini-game/package.png b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/package.png new file mode 100644 index 0000000000..20c2f02073 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/package.png differ diff --git a/versions/4.0/en/editor/publish/publish-bytedance-mini-game/performance.png b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/performance.png new file mode 100644 index 0000000000..19bcbba796 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/performance.png differ diff --git a/versions/4.0/en/editor/publish/publish-bytedance-mini-game/physx-options.png b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/physx-options.png new file mode 100644 index 0000000000..8e1770e9cf Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/physx-options.png differ diff --git a/versions/4.0/en/editor/publish/publish-bytedance-mini-game/tool.jpg b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/tool.jpg new file mode 100644 index 0000000000..021c50c3f4 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-bytedance-mini-game/tool.jpg differ diff --git a/versions/4.0/en/editor/publish/publish-fb-instant-games.md b/versions/4.0/en/editor/publish/publish-fb-instant-games.md new file mode 100644 index 0000000000..426ae6a349 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-fb-instant-games.md @@ -0,0 +1,161 @@ +# Publish to Facebook Instant Games + +The essential difference between Facebook Instant Games and WeChat mini-games is that Facebook Instant Games runs on a pure HTML5 environment. So it can run not only on cell phones but also on desktop browsers, making development and debugging much easier. + +**Currently Cocos Creator does the work for users including:** + +- Integration with Facebook Instant Games SDK and automatic initialization, allowing users to call the relevant APIs directly +- One-click packaging process in the Cocos Creator build panel to package games directly to Facebook Instant Games specifications + +**What the user needs to do:** + +- Call the Facebook Instant Games SDK to access platform-related features +- Upload the packaged version of Cocos Creator to Facebook + +## Publishing process + +- Build the game using Cocos Creator +- Upload to Facebook backend +- Test the game +- Share your game in Facebook + +## Build the game with Cocos Creator + +1. Open the project to be published using Cocos Creator and open the **Build** panel from the **Menu bar -> Projects**. Select **Facebook Instant Games** from the **Platform** in the **Build** panel. + + ![build](./publish-fb-instant-games/build.png) + + Please refer to [Build Options](build-options.md) for general build options, Facebook Instant Games-specific build options are listed below, please refer to the **Build Options** section below for more information. + + ![fb-instant-games-options](./publish-fb-instant-games/build-options.png) + +2. Once the build options in the **Build** panel are set, click **Build** button. + When the build is complete, click the folder icon button in the bottom left corner of **build task** to open the project release package, you can see that the `fb-instant-games` (whichever is the name of the specific build task) folder has been created in the default release path `build` directory, which already contains the zip file of the Facebook Instant Games environment `fb -instant-games.zip`. + + ![package](./publish-fb-instant-games/package.png) + +### Build options + +Build options are common across platforms, please refer to [Build Options](build-options.md) for details. Next, let's look at the build options specific to the Facebook Instant Games platform. + +| Build options | Description | Field name (for command line publishing) | +| :--- | :--- | :--- | +| Device orientation | Options include **Landscape**, **Portrait** | `orientation` | +| vConsole | Insert the vConsole debugging plugin. vConsole is similar to a mini version of DevTools and is used to assist in debugging. | `embedWebDebugger` | + +### Upload to Facebook + +Create a new app in Facebook backend, add **small games** in **Add products**, set the game category, and save the changes. (For details, please refer to [Set up the app](https://developers.facebook.com/docs/games/instant-games/getting-started/quickstart?locale=zh_CN#app-setup)) + +Click the **Small Games -> Web Hosting** tab on the left side of the app panel, click **Upload Versions**, and upload the `.zip` file from the `fb-instant-games` directory to the Facebook hosting service. + +![upload](./publish-fb-instant-games/upload.png) + +When the version status changes to "Standby", click the "★" button to push the build to the "Production" environment. + +![push](./publish-fb-instant-games/push.png) + +### III. Testing the game + +#### Enable the https-enabled web server locally + +- First, open a command line window, go to the `fb-instant-games` directory, and install the http-server package via npm: ### + + ```bash + cd fb-instant-games + npm install -g http-server + ``` + +- Next, create the private key and certificate via openssl. + + ```bash + openssl genrsa 2048 > key.pem + openssl req -x509 -days 1000 -new -key key.pem -out cert.pem + ``` + +- When the private key and certificate are ready, the Web service can be started locally via SSL: the + + ```bash + http-server --ssl -c-1 -p 8080 -a 127.0.0.1 + ``` + +- Open [https://localhost:8080](https://localhost:8080) with a browser and skip the security warning displayed by the browser. This step is only for the browser to whitelist the above private key and certificate. If the private key and certificate are subsequently regenerated, you need to open it again to confirm it once. It is not yet possible to preview the game directly at this step, because previewing the game requires initializing the Facebook Instant Games SDK, which needs to be done in the following way. + +#### Preview games under Facebook domain + +To use all the features of the Facebook Instant Games SDK, you need to open [https://www.facebook.com/embed/instantgames/YOUR_GAME_ID/player?game_url=https:// localhost:8080](https://www.facebook.com/embed/instantgames/YOUR_GAME_ID/player?game_url=https://localhost:8080) and be careful to replace the link `YOUR_ GAME_ID` in the link and replace it with the application number you created in the Facebook backend. + +Then you can see the game running successfully. + +![game](./publish-fb-instant-games/game.png) + +### IV. Share your game in Facebook + +Click the **Games** tab in the application panel, select **Details**, and pull down the **Details** page to the bottom to see the section shown below, select **Share Game** to share the game directly to the Facebook dynamic message. + +![share](./publish-fb-instant-games/share.png) + +For details, please refer to [Test, Publish and Share Trivia Games](https://developers.facebook.com/docs/games/instant-games/test-publish-share?locale=zh_CN). + +> **NOTE**: There are several restrictions on Facebook hosting, the most important of which are. +> +> 1. server-side logic (e.g. php) is not supported +> 2. no more than 500 files per application upload + +## Customizing Instant Games + +Developers can create the [build-templates/fb-instant-games](custom-project-build-template.md) directory under the Creator project folder to customize them according to their needs, and then copy the published files into that directory to customize them: ## + +![file](./publish-fb-instant-games/file.png) + +- `fbapp-config.json`: this is the configuration for the whole package, go to [official introduction](https://developers.facebook.com/docs/games/instant-games/bundle-config) +- `index.html`: Here you can modify the introduced Facebook Instant Games SDK version +- `index.js`: Here you can modify the SDK initialization and progress bar + +## Creating DOM Elements in the Game +Please refer to the example code below to create Dom elements in a Facebook Instant Game and display them on the game screen. + +```javascript +var buttonDiv = document.createElement("div"); +var button = document.createElement("button"); + +const frameSize = screen.windowSize; +const imageWidth = 195, imageHeight = 57; + +button.style.position = "absolute"; +button.style.left = `${frameSize.width / 2 - imageWidth / 2}px`; +button.style.top = `${frameSize.height / 2 - imageHeight / 2}px`; + +//load image from resources bundle +resources.load("btn_play", Texture2D, (error, res)=>{ + if (!error) { + button.style.backgroundImage = `url(${res.nativeUrl})`; + button.style.backgroundRepeat = "no-repeat"; + button.style.backgroundPosition = "center"; + button.style.width = res.width + "px"; + button.style.height = res.height + "px"; + button.style.backgroundColor = "transparent"; + button.style.borderColor = "transparent"; + button.onclick = (()=>{ + //DO SOMETHING + }); + buttonDiv.appendChild(button); + var body = document.body; + body.insertBefore(buttonDiv, body.lastChild); + } +}); +``` + +Result when running: + +![](../../../zh/editor/publish/publish-fb-instant-games/use-dom-element.png) + +## SDK description + +The Creator is integrated with the Instant Games SDK provided by Facebook and is automatically initialized when the game is loaded (`initializeAsync` and `startGameAsync`). Users can access it directly through the `FBInstant` module, see [Instant Games SDK](https://developers.facebook.com/docs/games/instant-games/sdk) for instructions. +In addition, Facebook also provides a Facebook SDK for JavaScript to access Facebook's social features, which can be accessed through the `FB` module. However, this SDK Creator is not integrated and needs to be introduced manually by the user, see [official documentation](https://developers.facebook.com/docs/javascript). + +## Reference links + +- [Facebook Backend](https://developers.facebook.com/) +- [Facebook Instant Games documentation](https://developers.facebook.com/docs/games/instant-games) diff --git a/versions/4.0/en/editor/publish/publish-fb-instant-games/build-options.png b/versions/4.0/en/editor/publish/publish-fb-instant-games/build-options.png new file mode 100644 index 0000000000..7f7a9fabf8 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-fb-instant-games/build-options.png differ diff --git a/versions/4.0/en/editor/publish/publish-fb-instant-games/build.png b/versions/4.0/en/editor/publish/publish-fb-instant-games/build.png new file mode 100644 index 0000000000..7a6cc23c07 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-fb-instant-games/build.png differ diff --git a/versions/4.0/en/editor/publish/publish-fb-instant-games/file.png b/versions/4.0/en/editor/publish/publish-fb-instant-games/file.png new file mode 100644 index 0000000000..81880f764a Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-fb-instant-games/file.png differ diff --git a/versions/4.0/en/editor/publish/publish-fb-instant-games/game.png b/versions/4.0/en/editor/publish/publish-fb-instant-games/game.png new file mode 100644 index 0000000000..ed6def3ea7 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-fb-instant-games/game.png differ diff --git a/versions/4.0/en/editor/publish/publish-fb-instant-games/package.png b/versions/4.0/en/editor/publish/publish-fb-instant-games/package.png new file mode 100644 index 0000000000..ffd5762aea Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-fb-instant-games/package.png differ diff --git a/versions/4.0/en/editor/publish/publish-fb-instant-games/push.png b/versions/4.0/en/editor/publish/publish-fb-instant-games/push.png new file mode 100644 index 0000000000..1af0b924dd Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-fb-instant-games/push.png differ diff --git a/versions/4.0/en/editor/publish/publish-fb-instant-games/share.png b/versions/4.0/en/editor/publish/publish-fb-instant-games/share.png new file mode 100644 index 0000000000..682b8f73b2 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-fb-instant-games/share.png differ diff --git a/versions/4.0/en/editor/publish/publish-fb-instant-games/upload.png b/versions/4.0/en/editor/publish/publish-fb-instant-games/upload.png new file mode 100644 index 0000000000..9b5ac56d75 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-fb-instant-games/upload.png differ diff --git a/versions/4.0/en/editor/publish/publish-honor-mini-game.md b/versions/4.0/en/editor/publish/publish-honor-mini-game.md new file mode 100644 index 0000000000..246fd760be --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-honor-mini-game.md @@ -0,0 +1,92 @@ +# Publish to Honor Mini Games + +Starting from Cocos Creator v3.8.6, games can be published to **Honor Mini Games**. + +## Preparation Work + +- Refer [Honor Mini Games Integration Guide](https://developer.honor.com/cn/docs/game_center/guides/miniGame/miniGameGuidelines), complete the developer registration process and app creation on the Honor Open Platform. + +- Download [Honor Quick Game APP](https://developer.honor.com/cn/docs/game_center/guides/miniGame/miniGameGuidelines#开发+调试指引) and install it on your honor mobile device. + +## Build Options + +Open the project you want to publish with Cocos Creator, and from the Menu Bar -> Project, open the Build and Publish panel, select Honor Mini Games as the Publish Platform. + +![build](./publish-honor-mini-game/build.png) + +General build options can be set according to [General Build Options](build-options.md). The specific build options for Honor Mini Games are as follows: + +![build option](./publish-honor-mini-game/build-option.png) + +| Name | Optional | Description | Field name | +| :-- | :-- | :-- | :-- | +| **Game Package Name** | required | such as `com.example.demo` | `package` | +| **Desktop Icon** | required | Click the **search icon** button at the back of the input box to select the icon you want. When building, the **Desktop Icon** will be built into the Honor Mini Game project. It is suggested to use `PNG` images for the **Desktop Icon**. | `icon` | +| **Game Version Name** | required | **Game Version Name** is the real version, such as: **1.0.0**. | `versionName` | +| **Game Version Number** | required | **Game Version Number** is different from the **Game Version Name**, and the **Game Version Number** is mainly used to distinguish the version update. Each time when you submit audit, the game version number is at least 1 higher than the value of the last submitted audit. It must not be equal to or less than the value of the last submitted audit, and it is recommended that the **Game Version Number** be recursively incremented by 1 each time when the audit is submitted. | `versionCode` | +| **Supported Minimum Platform Version Number** | required | For compatibility checks to prevent incompatibilities from arising when running on lower version platforms after going live. | `minPlatformVersion` | +| **Screen orientation** | - | Device direction, it will be written in `manifest.json`.| `orientation` | +| **Use debug keystore** | - | true | When you check **Use Debug Keystore**, it means that the rpk package built with the certificate that comes with Creator is used by default, and it is only used for **debugging**. when the rpk package is to be used to submit an audit, do not check the **Use Debug Keystore** to build it.| `useDebugKey` | +| **Key certification path** | - | - | The key store certificate, the quick game on the Huawei App Market, must be signed with the release version certificate, and the certificate fingerprint must be configured in the background of the Huawei Developers Alliance. For details, please refer to the following **Generate Signature File** | `privatePemPath`, `certificatePemPath` | + +### Generate signature file + +If you don't check the **Keystore**, you need to configure the signature files **certificate.pem path** and **private.pem path**, where you build a rpk package that you can **publish directly**. The developer can configure two signature files by using the **search icon** button to the right of the input box. + +There are two ways to generate a signature files: + +- Generated by the **New** button after the **certificate.pem path** in the **Build** panel. + +- Generated by the command line. + + The developer needs to generate the signature file **private.pem**, **certificate.pem** through tools such as **openssl**. + + ```bash + # Generate a signature file with the openssl command tool + openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out certificate.pem + ``` + + > **Note**: **openssl** can be used directly in the terminal in Linux or Mac environment, and in the Windows environment you need to install `openssl` and configure system environment variables. Restart Creator after the configuration is complete. + +### Building + +After setting the build options in the **Build and Publish** panel, click the **Build and Generate** button. + +After completion, click the folder icon button at the bottom left of **Build Task** to open the project's published package. You can see that a `honor-mini-game` folder (subject to the specific build task name) is generated in the default publish path build directory. This folder is the exported Honor Quick Game project directory and rpk, and the rpk package is in the `build/honor-mini-game/dist` directory. + +![package](./publish-honor-mini-game/package.png) + +If you need to modify the generated rpk package, after making changes, click the **Generate** button at the bottom right of **Build Task** to regenerate the rpk package without rebuilding. + +## Run the rpk + +There are two ways to run rpk on your phone: + +- **Method One** + + Click the **Run** button in the **Build and Publish** panel and wait for the QR code interface to generate: + + ![](./publish-honor-mini-game/qr_code.png) + + Then open the **Quick App** that was installed before on your Android device. Click the **Scan code install** button to scan the QR Code to open the **rpk**. + + ![instant_scan_install](./publish-honor-mini-game/honor-instant_scan_install1.png) + +- **Method Two** + + Copy the generated mini game **rpk** file (located in the `dist` directory) to the internal storage directory of the mobile phone. + + Open the **Quick App** that has been installed before on the Android device, click **Local Install**, then find the **rpk** file from the internal storage directory of your mobile phone and select **Open**. + + ![instant_native_install](./publish-honor-mini-game/honor-instant_native_install2.png) + +## Honor Mini Game Environment Resource Management + +Similar to WeChat Mini Games, Honor Mini Games also have package size limitations. The main package size limit for Honor Mini Games is **5MB**, and any content exceeding this limit must be downloaded via network requests.
When the package size is too large, you can configure the **Remote Server Address** option in the **Build and Publish** panel to upload low-priority resources to a remote server. For more details, please refer to [Uploading Resources to Remote Server](../../asset/cache-manager.md). + +After the game starts, the engine will automatically download resources from the remote server address. Once the resources are downloaded, the engine's cache manager will record the save paths of these resources. This information is used to automatically delete some cached game resources when the cache space is insufficient. Please refer to [Cache Manager](../../asset/cache-manager.md) for more details. + +## Reference documentation + +- [Honor Mini Games Integration Guide](https://developer.honor.com/cn/docs/game_center/guides/miniGame/miniGameGuidelines) +- [Honor Quick Game APP](https://developer.honor.com/cn/docs/game_center/guides/miniGame/miniGameGuidelines#开发+调试指引) diff --git a/versions/4.0/en/editor/publish/publish-honor-mini-game/build-option.png b/versions/4.0/en/editor/publish/publish-honor-mini-game/build-option.png new file mode 100644 index 0000000000..b9fbb3da37 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-honor-mini-game/build-option.png differ diff --git a/versions/4.0/en/editor/publish/publish-honor-mini-game/build.png b/versions/4.0/en/editor/publish/publish-honor-mini-game/build.png new file mode 100644 index 0000000000..29314f9428 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-honor-mini-game/build.png differ diff --git a/versions/4.0/en/editor/publish/publish-honor-mini-game/honor-instant_native_install2.png b/versions/4.0/en/editor/publish/publish-honor-mini-game/honor-instant_native_install2.png new file mode 100644 index 0000000000..d36a540955 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-honor-mini-game/honor-instant_native_install2.png differ diff --git a/versions/4.0/en/editor/publish/publish-honor-mini-game/honor-instant_scan_install1.png b/versions/4.0/en/editor/publish/publish-honor-mini-game/honor-instant_scan_install1.png new file mode 100644 index 0000000000..138de3dd5e Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-honor-mini-game/honor-instant_scan_install1.png differ diff --git a/versions/4.0/en/editor/publish/publish-honor-mini-game/package.png b/versions/4.0/en/editor/publish/publish-honor-mini-game/package.png new file mode 100644 index 0000000000..52b7f9d16e Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-honor-mini-game/package.png differ diff --git a/versions/4.0/en/editor/publish/publish-honor-mini-game/qr_code.png b/versions/4.0/en/editor/publish/publish-honor-mini-game/qr_code.png new file mode 100644 index 0000000000..c8e9af64bc Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-honor-mini-game/qr_code.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc.md b/versions/4.0/en/editor/publish/publish-huawei-agc.md new file mode 100644 index 0000000000..64e8823d44 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-huawei-agc.md @@ -0,0 +1,92 @@ +# Publish to HUAWEI AppGallery Connect + +Cocos Creator supports publishing games to HUAWEI AppGallery Connect, which helps developers access to HUAWEI AppGallery. + +## Preparation + +- Login to the [AppGallery Connect](https://developer.huawei.com/consumer/en/service/josp/agc/index.html), it is necessary to complete the [Developer Registration](https://developer.huawei.com/consumer/en/doc/start/10104) before [creating an app](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-create_app). When creating an app, select **APK** for the **Package type**. + + ![](./publish-huawei-agc/app-info.png) + +- Access the required HUAWEI AppGallery Connect related services via the Cocos Service panel. Currently the SDK only supports the Android platform, please refer to the [HUAWEI HMS Core](https://service.cocos.com/document/en/sdkhub-plugins/sdkhub-hms.html) documentation for the detailed operation steps. + +## Publishing Process + +Use Cocos Creator to open the project that needs to be published. Open the **Build** panel from the **Menu bar -> Project**, select **HUAWEI AppGallery Connect** in the **Platform** dropdown of the **Build** panel. + +![agc-build](./publish-huawei-agc/agc-builder.png) + +For the settings of the general options, please refer to the [Build Options](build-options.md) documentation. + +- **agconnect-services Config**: used to configure Huawei parameter file `agconnect-services.json`. Please refer to the [Configs HUAWEI Parameter File](https://service.cocos.com/document/en/sdkhub-plugins/sdkhub-hms.html#configs-huawei-config-file) for the specific configuration method. + +- **Cocos SDKHub Config Set**: this option is used to help the game quickly integrate the channel SDK. Please select the SDKHub service that you have previously enabled in the Cocos Services panel. + + ![sdkhub](./publish-huawei-agc/sdkhub.png) + +### Build and Compile + +After the relevant options of the **Build** panel are set, click **Build**.
+After the build is complete, click the folder icon button in the bottom left corner of the **Build Task** to open the release package directory. You can see that the `huawei-agc` folder is generated under the default release path `build` directory, which has been automatically integrated with HUAWEI AppGallery Connect related services. + +Then click **Make**, or use [Android Studio](native-options.md#make-and-run) to open the project to compile. After compiling, HUAWEI AppGallery Connect APK will be generated in the release package directory. + +![apk](./publish-huawei-agc/apk.png) + +### Upload the APK to AppGallery Connect + +There are two ways to upload the APK to AppGallery Connect. + +#### 1. Upload by the Build panel + +Creator supports uploading the APK generated by building and compiling directly to the AppGallery Connect backend.
+Click the **Upload** button in the bottom right of the **Build Task** panel to open the **Upload** panel, then fill in the relevant information. + +![agc-upload-panel](./publish-huawei-agc/agc-upload-panel.png) + +- APP ID: fill in the APP ID for your app. Login to the AppGallery Connect and click **My apps -> App information** to get the APP ID. + +- Version: fill in as required. + +- APK Path: select the APK generated after compilation. + +- Login Type: includes both **OAuth** and **API Client**. + + - **OAuth** + + The OAuth login type only requires you follow the prompts to login to your HUAWEI account (Need to have sufficient [permissions](https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agcapi-getstarted-0000001111845114#section797720532313)) when you click **Confirm**, then check the box of the required permissions, the window will automatically close and the APK will be uploaded automatically. + + ![upload-oauth](./publish-huawei-agc/upload-oauth.png) + + - **API Client** + + - If the API Client login type is used for the first time, you need to login to AppGallery Connect to get the relevant configuration information. + + ![upload-api](./publish-huawei-agc/upload-api.png) + + - Select **Users and permission -> Connect API -> Create** to create an API client, and select [Roles and Permissions](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-help-rolepermission-0000001155345429) as needed, then click **Confirm**. + + ![create-api-key](./publish-huawei-agc/create-api-key.png) + + - Fill the **Client ID** and **Key** of the API client into the corresponding input boxes in the **Upload** panel of Creator. + + - Click **Confirm** when you are done configuring. + + Detailed descriptions of the two login types can be found in the [AppGallery Connect API Getting Started](https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agcapi-getstarted) documentation. + +#### 2. Upload by AppGallery Connect + +Sign in to AppGallery Connect, click **My apps** and select the app. Then switch to the **Distribute** column at the top left, click on **Version information -> Draft** in the left column, find the **Upload APK package**, click on **Software packages**, and then click on **Upload**. + +### Submit Your App for Review + +1. Sign in to AppGallery Connect, click **My apps** and select the app. Then switch to the **Distribute** column at the top left, click on **Version information -> Draft** in the left column, find the **Upload APK package**, click on **Software packages**, and then click on **Select**. + + ![apk-list](./publish-huawei-agc/apk-list.png) + +2. For other configuration information, please refer to the [Releasing an App](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-release_app) documentation. After completing and confirming all of the information is in order, click the **Submit** button at the top right of the page. HUAWEI AppGallery will complete the review in 3~5 working days. + +## Related Reference Links + +- [AppGallery Connect](https://developer.huawei.com/consumer/en/service/josp/agc/index.html) +- [AppGallery Connect Operation Guide](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-create_app) diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/agc-builder.png b/versions/4.0/en/editor/publish/publish-huawei-agc/agc-builder.png new file mode 100644 index 0000000000..823ba820b0 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/agc-builder.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/agc-upload-panel.png b/versions/4.0/en/editor/publish/publish-huawei-agc/agc-upload-panel.png new file mode 100644 index 0000000000..da5cc56f4e Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/agc-upload-panel.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/agc-upload.png b/versions/4.0/en/editor/publish/publish-huawei-agc/agc-upload.png new file mode 100644 index 0000000000..9bf5a6d6b9 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/agc-upload.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/apk-list.png b/versions/4.0/en/editor/publish/publish-huawei-agc/apk-list.png new file mode 100644 index 0000000000..5718919a17 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/apk-list.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/apk.png b/versions/4.0/en/editor/publish/publish-huawei-agc/apk.png new file mode 100644 index 0000000000..1ea9419243 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/apk.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/app-info.png b/versions/4.0/en/editor/publish/publish-huawei-agc/app-info.png new file mode 100644 index 0000000000..3a6eb809a6 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/app-info.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/create-api-key.png b/versions/4.0/en/editor/publish/publish-huawei-agc/create-api-key.png new file mode 100644 index 0000000000..fa51c93448 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/create-api-key.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/sdkhub.png b/versions/4.0/en/editor/publish/publish-huawei-agc/sdkhub.png new file mode 100644 index 0000000000..91f41db797 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/sdkhub.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/upload-api.png b/versions/4.0/en/editor/publish/publish-huawei-agc/upload-api.png new file mode 100644 index 0000000000..8399dacf27 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/upload-api.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-agc/upload-oauth.png b/versions/4.0/en/editor/publish/publish-huawei-agc/upload-oauth.png new file mode 100644 index 0000000000..7cbacf2b99 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-agc/upload-oauth.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos.md b/versions/4.0/en/editor/publish/publish-huawei-ohos.md new file mode 100644 index 0000000000..aa60d44289 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-huawei-ohos.md @@ -0,0 +1,145 @@ +# Publish for the Huawei HarmonyOS + +Starting with v3.2, Cocos Creator supports packaging games as Huawei **HarmonyOS** (`.hap`) applications. + +## Preparations + +- Enter [AppGallery Connect website](https://developer.huawei.com/consumer/en/service/josp/agc/index.html#/) to register for a [HUAWEI ID](https://developer.huawei.com/consumer/en/doc/start/registration-and-verification-0000001053628148). + +- After logging in, select [Create Project](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-harmonyapp-createproject) and [Add HarmonyOS App](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-harmonyapp-createharmonyapp), select the **Platform** as **HarmonyOS**. + + ![app info](./publish-huawei-ohos/app-info.png) + + > **Note**: the **Package name** needs to be consistent with the **Game Package Name** in the **Build** panel of Cocos Creator. + +- Download and install [HUAWEI DevEco Studio](https://developer.harmonyos.com/en/develop/deveco-studio#download). After the installation is complete [Configure Development Environment](https://developer.harmonyos.com/en/docs/documentation/doc-guides/environment_config-0000001052902427), download the HarmonyOS SDK, please check **Java** and **Native** for the SDK packages in the **SDK Platforms** page. + + ![dev setting](./publish-huawei-ohos/dev-setting.png) + + Remember the directory indicated by **HarmonyOS SDK Location** shown at the top of the window, fill in the path of this SDK in the **Preferences** panel of Cocos Creator later. + +## Release process + +### Building with Cocos Creator + +1. Use Cocos Creator to open the project, click **Cocos Creator/File -> Preferences -> External Programs** in the upper menu bar, configure **HarmonyOS NDK** and **HarmonyOS SDK** path: + + ![preferences](./publish-huawei-ohos/preferences.png) + +2. Open the **Build** panel from **Project -> Build** in the menu bar. Select **HarmonyOS** in **Platform** in the **Build** panel, and configure [Build Options](./native-options.md#build-options) as needed. Next, click the **Build** button at the bottom right. + + ![build](./publish-huawei-ohos/build.png) + + > **Notes**: + > + > 1. **Game Package Name** in the expanded option of **HarmonyOS** needs to be consistent with the **Package name** when adding the HarmonyOS application in the AppGallery Connect. + > 2. We deprecated the **Render BackEnd** in the expanded option of **HarmonyOS** since v3.4.2 due to **HarmonyOS** offering full support to GLES3. + +3. Open the project directory after the build is complete, notice that the `ohos` folder is generated under the `native\engine` directory, which contains the HarmonyOS project generated by the build. + + ![package](./publish-huawei-ohos/package-ohos.png) + + > **Note**: currently, HarmonyOS does not support multi-directory builds, so the generated HarmonyOS project is in the `native\engine` directory, unlike other platforms that generate in the `build` directory of the project. + + Next, open the `ohos` folder with HUAWEI DevEco Studio to perform further compilation, as described below. + +### Compile and run through HUAWEI DevEco Studio + +1. Open HUAWEI DevEco Studio, click **Open Project** and select the HarmonyOS project (`ohos` folder) generated after the build in the previous step. + + ![open-project](./publish-huawei-ohos/open-project.png) + +2. Prepare the signature file. For the method of obtaining the signature file, please refer to the contents of the **Signature File** section below. + +3. Configure the signature file. Click **File -> Project Structure** in the DevEco Studio menu bar, then select **Modules -> entry -> Signing Configs** and configure the signature information. For details, please refer to the [Configure Signature Information](https://developer.harmonyos.com/en/docs/documentation/doc-guides/ide_debug_device-0000001053822404#EN-US_TOPIC_0000001154985555__section19238119191816) documentation. + + ![sign configs](./publish-huawei-ohos/sign-configs-debug.png) + + Then proceed to configure the signature information in **Project -> Signing Configs**. + + After setting and saving, the configured signature information can be viewed in the project directory in the `native\engine\ohos\entry\build.gradle` file in the project directory. + + Depending on the build type (Debug/Release) and whether the generated `.hap` contains signature information, the developer can configure in combination as needed, please refer to the [Building and Creating HAPs](https://developer.harmonyos.com/en/docs/documentation/doc-guides/build_hap-0000001053342418) documentation for details. + + ![build variants](./publish-huawei-ohos/build-variants.png) + +4. Then click on the **Build -> Build Hap(s)/APP(s) -> Build Hap(s)** in the menu bar to execute the compilation process and generate the `.hap` file. + + ![build hap](./publish-huawei-ohos/build-hap.png) + + After compiling, the `.hap` file with signature information is generated in the `native\engine\ohos\build\outputs\hap` directory of the project directory. + + ![output](./publish-huawei-ohos/output.png) + + All `.hap` files with/without signature information are generated in the `native\engine\ohos\entry\build\outputs\hap` directory of the project directory. + + ![output](./publish-huawei-ohos/debug-output.png) + +5. Connect Huawei devices with HarmonyOS system using USB, then click **Run -> Run ‘entry’** in the menu bar or click the Run button above to run the HarmonyOS app to your device. Please refer to the [Running Your App](https://developer.harmonyos.com/en/docs/documentation/doc-guides/run_phone_tablat-0000001064774652) documentation for details. + + ![run project](./publish-huawei-ohos/run-project.png) + + > **Note**: if the app icon is not found when running on the device, check if there is an `installationFree` field with `true` in the `native\engine\ohos\entry\src\main\config.json` file in the project directory, change it to `false`. + +6. If need to upload and publish HarmonyOS apps to Huawei AppGallery Connect, please refer to the official [Release HarmonyOS Application Guide](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-harmonyapp-releaseharmonyapp) documentation for the specific process. + +### Signature file + +HarmonyOS applications use digital certificates (`.cer` files) and HarmonyAppProvision files (`.p7b` files) to ensure the integrity of the application. First, use **DevEco Studio** to generate a key and certificate request file, and then use the certificate request file to apply for a digital certificate and profile file for publishing/debugging in AppGallery Connect. + +#### Generate key and certificate request file + +In **DevEco Studio**, click **Build -> Generate Key and CSR** in the upper menu bar to generate a key (`.p12` file), and then generate a certificate request file (`.csr`). For details, please refer to the [Generating a Key Store and CSR](https://developer.harmonyos.com/en/docs/documentation/doc-guides/publish_app-0000001053223745#EN-US_TOPIC_0000001154985553__section7209054153620) documentation. + +#### Generate and download a digital certificate (`.cer` file) + +- Visit the [AppGallery Connect website](https://developer.huawei.com/consumer/en/service/josp/agc/index.html), select **Users and permissions -> Certificate management**, click the **New certificate** button in the upper right corner. + + ![cer profile](./publish-huawei-ohos/cer-file.png) + +- Enter the required certificate information in the New certificate dialog box that is displayed. **Certificate type** Choose **Release certificate**/**Debug certificate** as required. The certificate request file is the `.csr` file generated in **DevEco Studio** in the previous step. + + ![cer profile](./publish-huawei-ohos/new-cer-file.png) + + - **Certificate name**: Enter up to 100 characters. + - **Certificate type**: Select **Release certificate**/**Debug certificate** as required. + - **CSR certificate request file**: Select the `.csr` file generated in **DevEco Studio** in the previous step. + +- After filling in the information, click **Submit**, a release/debugging certificate (`.cer` file) will be generated, and click the **Download** button behind the certificate to download it to the local. + +For more information, please refer to the [Apply for Digital Certificate](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-harmonyapp-debugharmonyapp#h1-1598336089667) documentation. + +#### Generate and download HarmonyAppProvision Profile file (`.p7b` file) + +- Enter [AppGallery Connect website](https://developer.huawei.com/consumer/en/service/josp/agc/index.html), select **My Project**, select the HarmonyOS project and application created before. + +- Click the **HarmonyOS app services -> HAP Provision Profile management** item on the left, and then click the **Add** button at the top right of the page to create a `.p7b` file. + + ![provision profile](./publish-huawei-ohos/provision-profile.png) + +- Enter the required information in the HarmonyAppProvision information dialog box that is displayed. + + ![new provision profile](./publish-huawei-ohos/new-provision-profile.png) + + - **HarmonyAppProvision file**: Enter the file name, which contains up to 100 characters. + - **Type**: Select **Release** or **certificate** as required. + - **Certificate**: Select the `.cer` file generated in the previous step. + - **Device**: Please refer to the [Registering Debugging Devices](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-harmonyapp-debugharmonyapp#h1-1598520099105) documentation for configuration. It is not necessary to configure this item when the **Type** is set to **Release**. + +- After setting, click **Submit** to download the generated `.p7b` file to the local. + +For more information, please refer to the [Application Profile](https://developer.huawei.com/consumer/en/doc/distribution/app/agc-harmonyapp-debugharmonyapp#h1-1598336409517) documentation. + +## Known issues + +- WebView does not support `touch` events, touch is not responsive. +- VideoPlayer does not support dragging video when playing remote video. +- EditBox is not zoomed out when full screen, and does not re-layout after keyboard retraction. +- Gravity/acceleration sensor delay around 500ms, poor followability. + +## Reference documentation + +- [AppGallery Connect Website](https://developer.huawei.com/consumer/en/service/josp/agc/index.html#/) +- [HUAWEI DevEco Studio User Guide](https://developer.harmonyos.com/en/docs/documentation/doc-guides/tools_overview-0000001053582387) +- [Debugging the HarmonyOS App](https://developer.harmonyos.com/en/docs/documentation/doc-guides/ide_debug_device-0000001053822404) +- [Releasing the HarmonyOS App](https://developer.harmonyos.com/en/docs/documentation/doc-guides/publish_app-0000001053223745) diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/app-info.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/app-info.png new file mode 100644 index 0000000000..acf87c90c5 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/app-info.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/build-hap.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/build-hap.png new file mode 100644 index 0000000000..d052e7a13e Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/build-hap.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/build-variants.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/build-variants.png new file mode 100644 index 0000000000..d061788cbd Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/build-variants.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/build.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/build.png new file mode 100644 index 0000000000..de2251f352 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/build.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/cer-file.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/cer-file.png new file mode 100644 index 0000000000..1799a245d0 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/cer-file.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/debug-output.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/debug-output.png new file mode 100644 index 0000000000..bf8a94b6a8 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/debug-output.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/dev-setting.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/dev-setting.png new file mode 100644 index 0000000000..8f1e26a010 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/dev-setting.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/new-cer-file.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/new-cer-file.png new file mode 100644 index 0000000000..94aa6b632c Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/new-cer-file.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/new-provision-profile.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/new-provision-profile.png new file mode 100644 index 0000000000..a507302b25 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/new-provision-profile.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/open-project.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/open-project.png new file mode 100644 index 0000000000..32625f32f3 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/open-project.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/output.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/output.png new file mode 100644 index 0000000000..032c121905 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/output.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/package-ohos.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/package-ohos.png new file mode 100644 index 0000000000..99a0833314 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/package-ohos.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/preferences.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/preferences.png new file mode 100644 index 0000000000..7ff73f0f8a Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/preferences.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/provision-profile.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/provision-profile.png new file mode 100644 index 0000000000..ce496aaf1e Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/provision-profile.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/run-project.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/run-project.png new file mode 100644 index 0000000000..f7904309d7 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/run-project.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/sign-configs-debug.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/sign-configs-debug.png new file mode 100644 index 0000000000..fb6f817556 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/sign-configs-debug.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-ohos/sign-configs.png b/versions/4.0/en/editor/publish/publish-huawei-ohos/sign-configs.png new file mode 100644 index 0000000000..dfd3f7fe15 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-ohos/sign-configs.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-quick-game.md b/versions/4.0/en/editor/publish/publish-huawei-quick-game.md new file mode 100644 index 0000000000..ebe92bd05b --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-huawei-quick-game.md @@ -0,0 +1,116 @@ +# Publish to Huawei Quick Games + +> **Note**: some platforms only have Chinese documentation available when visiting the platform's website. It may be necessary to use Google Translate in-order to review the documentation. + +__Cocos Creator__ officially supports the release of games to the **Huawei Quick Games**. + +## Environment Configuration + +- Download the [Huawei Quick APP Loader](https://developer.huawei.com/consumer/en/doc/Tools-Library/quickapp-ide-download-0000001101172926#section9347192715112) and install it on your Android device (Android Phone 6.0 or above is recommended) + +- Install [nodejs-8.1.4](https://nodejs.org/en/download/) or above, globally. + +## Release Process + +1. Use **Cocos Creator 3.0** to open the project that needs to be released. Select **Huawei Quick Game** in the **Platform** dropdown of the **Build** panel. + + ![build options](./publish-huawei-quick-game/build_options.png) + + Click on the **huawei-quick-game** below to expand the parameter configuration of **Huawei Quick Game**. + + ![huawei options](./publish-huawei-quick-game/huawei_options.jpg) + +The specific filling rules for the relevant parameter configuration are as follows: + +- **Game Package Name** + + **Game Package Name** is filled in according to the developer's needs. It's required. + +- **Desktop Icon** + + **Desktop Icon** is required. Click the **search icon** button at the back of the input box to select the icon you want. When building, the **Desktop Icon** will be built into the **Huawei Quick Game** project. It is suggested that the **Desktop Icon** is a `.png` image. + +- **Game Version Name** + + This item is required. **Game Version Name** is the real version, such as: 1.0.0. + +- **Game Version Number** + + This item is required. **Game Version Number** is different from the **Game Version Name**, and the **Game Version Number** is mainly used to distinguish the version update. Each time when you submit audit, the game version number is at least 1 higher than the value of the last submitted audit. It must not be equal to or less than the value of the last submitted audit, and it is recommended that the **Game Version Number** be recursively incremented by 1 each time when the audit is submitted + + > **Note**: the **Game Version Number** must be a positive integer. + +- **Supported Minimum Platform Version Number** + + This item is required. According to the requirements of Huawei Quick Games, this value must be greater than or equal to **1035**. + +- **Custom manifest file path (optional)** + + This is an optional item, which is the expansion function of Huawei Quick Game. When used, you need to select a `JSON` file, and the data type in the file is required to be in `JSON` format. + + > **Note**: the `JSON` data is not available when the key value are `package`, `appType`, `name`, `versionName`, `versionCode`, `icon`, `minPlatformVersion`, `config`, `display`, otherwise it will be overwritten by data such as **Game Package Name**, **Game Name**, **Desktop Icon**, **Game Version Name**, **Game Version Number** during the build. + +- **Small Packet Mode** + + This item is optional. The in-package volume of the mini-game contains code and assets that cannot exceed 10M, and assets can be loaded via network requests. **Small Packet Mode** is to help developers keep the script files in the mini game package, other assets are uploaded to the remote server, and downloaded from the remote server as needed. Cocos Creator has already helped the developer with the download, cache, and version management of remote assets. The developer needs to do is the following steps: + + 1. When building, check the **Small Packet Mode** and fill in the **Small Packet Mode Server Path**. + + 2. **First game asset package into the game package**, this item is optional. + + In the Small Packet Mode, due to too many assets on the launch scene, downloading and loading assets for a long time may result in a short black screen when entering the game for the first time. If **First game asset package into the game package** is checked, you can reduce the black screen time when you first enter the game. However, it should be noted that the `res/import` asset does not support split asset downloading at this time, and the entire `import` directory is also packaged into the first package. + + Developers can choose whether to check this item according to their needs. Then click on **Build**. + + 3. After the build is complete, click the **Open** button after the **Build Path** to upload the `res` directory under the release path to the small packet mode server. For example, if the default release path is `build`, the **Build Task Name** is `huawei-quick-game`, you need to upload the `/build/huawei-quick-game/res` directory. + + At this point, the `res` directory will no longer be included in the built-up **rpk**, and the assets in the `res` directory will be downloaded from the filled **Small Packet Mode Server Path** through the network request. + +- **Keystore** + + When you check the **Keystore**, the default is to build the **rpk** package with a certificate that comes with Creator. This certificate is used only for **debugging**. + + > **Note**: when the **rpk** package is to be used to submit an audit, do not check the **Keystore** to build it. + + If you don't check the **Keystore**, you need to configure the signature files **certificate.pem path** and **private.pem path**, where you build a **rpk** package that you can **publish directly**. The user can configure two signature files by using the **search icon** button to the right of the input box. + + There are two ways to generate a signature files: + + - Generated by the **New** button after the **certificate.pem path** in the **Build** panel. + + - Generated by the command line. + + The user needs to generate the signature file **private.pem**, **certificate.pem** through tools such as **openssl**. + + ```bash + # Generate a signature file with the openssl command tool + openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out certificate.pem + ``` + + > **Note**: **openssl** can be used directly in the terminal in Linux or Mac environment, and in the Windows environment you need to install `openssl` and configure system environment variables. Restart **Cocos Creator** after the configuration is complete. + +**2. Build** + +After the relevant parameters of the **Build** panel are set, click **Build**. When the build is complete, click the **folder icon** button below the corresponding build task to open the build release path, you can see that a directory with the same name as the **Build Task Name** is generated in the default release path `build` directory, which is the exported Huawei Quick Game project directory and **rpk**, **rpk** package are in the `dist` directory. + +![rpk](./publish-huawei-quick-game/package.png) + +**3. Run the built rpk to the phone** + +Copy the **rpk** package generated by the build to the internal storage directory of the Android device. Open the **Huawei Quick APP Loader** that has been installed before, clicking the back button on the Android device will bring up a list, select the **Local Install**, select the path of place **rpk**, and then run the **rpk** on the Android device. + +**4. Subpackage rpk** + +Subpackage **rpk** can be used according to your needs. + +Subpackage loading, which is, splitting the game content into several packages according to certain rules, only downloading the necessary packages when starting up for the first time. This necessary package is called **main package**. The developer can trigger in the main package to download other sub-packages, which can effectively reduce the time spent on the first boot. + +To use this function, set the [Bundle Configuration](subpackage.md) in **Cocos Creator**, and the package will be automatically subpackaged when the setting is completed. + +After the build is complete, the generated subpackages and main package are merged into one **rpk**, which is in the `build/huawei-quick-game/dist` directory. + +> **Note**: currently, Huawei Quick Game does not support downloading multiple subpackages at the same time, please download them in order if you need to download multiple subpackages. + +## Reference documentation + +[Huawei Quick Game Development Guide](https://developer.huawei.com/consumer/en/doc/quickApp-Guides/quickgame-dev-runtimegame-guide-0000001159778255) diff --git a/versions/4.0/en/editor/publish/publish-huawei-quick-game/build_options.png b/versions/4.0/en/editor/publish/publish-huawei-quick-game/build_options.png new file mode 100644 index 0000000000..65a08f4d71 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-quick-game/build_options.png differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-quick-game/huawei_options.jpg b/versions/4.0/en/editor/publish/publish-huawei-quick-game/huawei_options.jpg new file mode 100644 index 0000000000..5a9c45b64c Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-quick-game/huawei_options.jpg differ diff --git a/versions/4.0/en/editor/publish/publish-huawei-quick-game/package.png b/versions/4.0/en/editor/publish/publish-huawei-quick-game/package.png new file mode 100644 index 0000000000..4b0014268b Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-huawei-quick-game/package.png differ diff --git a/versions/4.0/en/editor/publish/publish-in-command-line.md b/versions/4.0/en/editor/publish/publish-in-command-line.md new file mode 100644 index 0000000000..94a63f7f73 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-in-command-line.md @@ -0,0 +1,121 @@ +# Publish from the Command Line + +Publish a project from the command line can help us build an auto-publish routine that allows modifying command line parameters to achieve different goals. + +## Command Reference + +**For example**: Building Web Desktop with debug mode enabled: + +- Mac + + ```bash + /Applications/CocosCreator/Creator/3.0.0/CocosCreator.app/Contents/MacOS/CocosCreator --project projectPath --build "platform=web-desktop;debug=true" + ``` + +- Windows + + ```bash + ...\CocosCreator.exe --project projectPath --build "platform=web-desktop;debug=true" + ``` + +Currently, when using the command line to build, except for the required build options, if no parameter values are uploaded, the default values are used to build. Please refer to the description below and the platform's build options description for specific default values. + +## Exit Codes + +- **32** Build failed —— Invalid build parameters. +- **34** Build failed —— Some unexpected errors occurred during the build process, please refer to the build log for details. +- **36** Build success. + +## Publish Parameters + +- `--project`: Required, specify the project path. +- `--engine`: Optional, specify custom engine path. + +- `--build`: Specify the parameters to be used when building the project. + + When parameters are not specified after `--build`, then the parameters used in the **Build** panel, such as platforms, templates, and so on, will be used as default parameters. When additional parameter settings are specified, the default parameters will be overwritten with the specified parameters. The available parameters are: + + - `configPath`: Parameter file path. If define `configPath`, then Cocos Creator will load this file as a build parameter in the `JSON` file format. This parameter can be modified by yourself or exported directly from the **Build** panel. + - `stage` - Specify the build mode, default is 'build', options are 'make' | 'build'. + - `logDest`: Specify the log output path. + - `includedModules`: Customize the engine packaged modules, only the required modules are packaged. The corresponding field of each module can be found in the `features` field in [this file](https://github.com/cocos/cocos4/blob/3d/cc.config.json) of engine repository. + - `outputName`: The name of the release folder generated after the build. + - `name`: Game name. + - `platform`: Required, the platform needs to be built. + - `buildPath`: Specify the directory where the build release package is generated, the default is the `build` directory under the project directory. Either an absolute path or a path relative to the project (e.g.: `project://release`) can be used. Starting with v3.4.2, relative paths like `../` are supported. + - `startScene`: The UUID of the main scene (the participating scene will use the build option parameters in the **Build** panel from the last build), and the first scene from the **Included Scenes** will be used if not specified. + - `scenes`: Information about the scenes involved in the build, which defaults to all scenes when not specified. + - `debug`: Whether or not debug mode, the default is `false`. + - `replaceSplashScreen`: Whether to replace the splash screen, the default is `false`. + - `md5Cache`: Enabled or disabled the MD5 Cache, the default is `false`. + - `mainBundleCompressionType`: Main bundle compression type. For specific option values, please refer to the document [Asset Bundle — compression type](../../asset/bundle.md##compression-type). + - `mainBundleIsRemote`: Configure the main package as a remote package. + - `packages`: The build configuration parameters supported by each plugin. What needs to be stored is the serialized string for the data object. For details, please refer to the following. + +Each platform's build will be embedded in the **Build** panel as a separate plugin, so each platform's build options are in different locations. The build parameters are configured in the `packages` field, for example, to specify the build options for WeChat Mini Game, the configuration is as follows: + +```bash +{ + taskName: 'wechatgame', + packages: { + wechatgame: { + appid: '*****', + } + } +} +``` + +After the build plugin system is opened to the public, the configuration parameters of other plugins are embedded in the **Build** panel in the same way. **Please refer to the documentation of each platform for the specific parameter fields of each platform**, it is better to use the **Export** function of the **Build** panel to get the configuration parameters. Currently it is still compatible with the old version of the parameters to build, but the compatibility process will be gradually removed later, so please upgrade the configuration parameters as soon as possible. + +## Bundle Build via Command Line + +1. Open the Bundle Build panel and configure the options. After configuring, export the configuration (since 3.8.2). + ![export-config](./../../asset/bundle/export-config.png) + The exported configuration is as follows: + + ```json + { + "buildTaskIds": [ + "1699344873959" + ], + "dest": "project://build/build-bundle", + "id": "buildBundle", + "bundleConfigs": [ + { + "root": "db://assets/resources", + "output": true + } + ], + "taskName": "build bundle db://assets/resources" + } + + ``` + +2. Execute the following command in the command line: + +- Mac + + ```bash + /Applications/CocosCreator/Creator/3.0.0/CocosCreator.app/Contents/MacOS/CocosCreator --project projectPath + --build "stage=bundle;configPath=./bundle-build-config.json;" + ``` + +- Windows + + ```bash + ...\CocosCreator.exe --project projectPath --build "stage=bundle;configPath=./bundle-build-config.json;" + ``` + +The command line publish of the Bundle is similar to the ordinary command line build, but the `stage` parameter needs to be specified as `bundle`, and the configuration exported on the Bundle Build Panel should be specified as the `configPath` parameter. + +3. Execute "Build script only" on the command line: +- Mac && Windows + ```bash + --build "buildScriptsOnly=true" + ``` + +## Publish using Jenkins + +**Cocos Creator** still needs the GUI environment when running from the command line. If the Jenkins server can not run **Cocos Creator** from the command line, a solution is running Jenkins in agent mode, so it can interact with the operating systems window server. For more details please review this [Stack Overflow post](https://stackoverflow.com/questions/13966595/build-unity-project-with-jenkins-failed). + +If the Jenkins server can not compile under Windows, specify a local user for the Jenkins service in the Windows **Control Panel -> Administrative Tools -> Services**, and then restart the computer. You don't need to set up a master-slave mode separately. diff --git a/versions/4.0/en/editor/publish/publish-mini-game.md b/versions/4.0/en/editor/publish/publish-mini-game.md new file mode 100644 index 0000000000..97f8c582f8 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-mini-game.md @@ -0,0 +1,16 @@ +# Publish to Mini Game Platforms + +- [Publish to HUAWEI AppGallery Connect](publish-huawei-agc.md) +- [Publish to Alipay Mini Game](publish-alipay-mini-game.md) +- [Publish to Taobao Mini Game](publish-taobao-mini-game.md) +- [Publish to Douyin Mini Game](publish-bytedance-mini-game.md) +- [Publish to Huawei Quick Game](publish-huawei-quick-game.md) +- [Publish to OPPO Mini Game](publish-oppo-mini-game.md) +- [Publish to vivo Mini Game](publish-vivo-mini-game.md) +- [Publish to Honor Mini Game](publish-honor-mini-game.md) +- [Publish to Baidu Mini Game](publish-baidu-mini-game.md) +- [Publish to WeChat Mini Game](publish-wechatgame.md) + - [WeChat Engine Plugin](wechatgame-plugin.md) + - [Access to WeChat PC Mini Game](publish-pc-wechatgame.md) +- [Access to Open Data Context](build-open-data-context.md) +- [Mini Game Subpackage](subpackage.md) diff --git a/versions/4.0/en/editor/publish/publish-native-index.md b/versions/4.0/en/editor/publish/publish-native-index.md new file mode 100644 index 0000000000..8119237581 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-native-index.md @@ -0,0 +1,21 @@ +# Fundamentals for Publishing to Native Platforms + +Cocos Creator supports publishing to native applications on multiple platforms: +- [Publishing iOS Apps](./ios/index.md) +- [Publishing Android Apps](./android/index.md) +- [Publishing HUAWEI Harmony Apps](./publish-huawei-ohos.md) +- [Publishing macOS Desktop Apps](./mac/index.md) +- [Publishing Windows Desktop Apps](./windows/index.md) + +The following are some common knowledge points related to publishing to native platforms: +- [About the Build Panel](build-panel.md) +- [General Build Options](build-options.md) +- [General Native Build Options](native-options.md) +- [Debugging JavaScript on Native Platforms](debug-jsb.md) +- [Build Process and FAQ](build-guide.md) + +If you want to publish to [Mini Game](./publish-mini-game.md) and [Web](./publish-web.md) platforms, please refer to the corresponding documentation. + +If you want to perform custom development on native platforms, please refer to the [Native Development](../../native/overview.md). + +If you want to customize the engine, please refer to the [Engine Customization Workflow](../../../zh/advanced-topics/engine-customization.md). diff --git a/versions/4.0/en/editor/publish/publish-native/android-options.png b/versions/4.0/en/editor/publish/publish-native/android-options.png new file mode 100644 index 0000000000..a429379f98 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/android-options.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/android-studio.png b/versions/4.0/en/editor/publish/publish-native/android-studio.png new file mode 100644 index 0000000000..1916313261 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/android-studio.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/build-progress-windows.png b/versions/4.0/en/editor/publish/publish-native/build-progress-windows.png new file mode 100644 index 0000000000..cbe07db4b5 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/build-progress-windows.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/cocos-console-log.png b/versions/4.0/en/editor/publish/publish-native/cocos-console-log.png new file mode 100644 index 0000000000..0aaa10ec77 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/cocos-console-log.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/encrypt-js.png b/versions/4.0/en/editor/publish/publish-native/encrypt-js.png new file mode 100644 index 0000000000..ef3cfbea4a Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/encrypt-js.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/gpg-input-sdk-keys.png b/versions/4.0/en/editor/publish/publish-native/gpg-input-sdk-keys.png new file mode 100644 index 0000000000..14a0370fef Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/gpg-input-sdk-keys.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/ios-options.png b/versions/4.0/en/editor/publish/publish-native/ios-options.png new file mode 100644 index 0000000000..1a3368fa05 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/ios-options.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/ios-xcode.png b/versions/4.0/en/editor/publish/publish-native/ios-xcode.png new file mode 100644 index 0000000000..b447d27cbc Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/ios-xcode.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/mac-options.png b/versions/4.0/en/editor/publish/publish-native/mac-options.png new file mode 100644 index 0000000000..faa8dd4386 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/mac-options.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/modify_abi.png b/versions/4.0/en/editor/publish/publish-native/modify_abi.png new file mode 100644 index 0000000000..3cbe4c3419 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/modify_abi.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/native-build-buttons.png b/versions/4.0/en/editor/publish/publish-native/native-build-buttons.png new file mode 100644 index 0000000000..fbdea7ef80 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/native-build-buttons.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/native-common.png b/versions/4.0/en/editor/publish/publish-native/native-common.png new file mode 100644 index 0000000000..8da739db02 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/native-common.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/native-directory.png b/versions/4.0/en/editor/publish/publish-native/native-directory.png new file mode 100644 index 0000000000..3e0ac88bdb Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/native-directory.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/native-options.png b/versions/4.0/en/editor/publish/publish-native/native-options.png new file mode 100644 index 0000000000..44d552ff0a Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/native-options.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/native-platform.png b/versions/4.0/en/editor/publish/publish-native/native-platform.png new file mode 100644 index 0000000000..80c20c5ec5 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/native-platform.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/native.png b/versions/4.0/en/editor/publish/publish-native/native.png new file mode 100644 index 0000000000..1798ef85e5 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/native.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/sdk-android-instant.png b/versions/4.0/en/editor/publish/publish-native/sdk-android-instant.png new file mode 100644 index 0000000000..50b1fb5d62 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/sdk-android-instant.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/windows-options.png b/versions/4.0/en/editor/publish/publish-native/windows-options.png new file mode 100644 index 0000000000..667fad34d7 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/windows-options.png differ diff --git a/versions/4.0/en/editor/publish/publish-native/windows-vs.png b/versions/4.0/en/editor/publish/publish-native/windows-vs.png new file mode 100644 index 0000000000..8d6f2fce38 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-native/windows-vs.png differ diff --git a/versions/4.0/en/editor/publish/publish-oppo-mini-game.md b/versions/4.0/en/editor/publish/publish-oppo-mini-game.md new file mode 100644 index 0000000000..f0ba8e75ef --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-oppo-mini-game.md @@ -0,0 +1,123 @@ +# Publish to OPPO Mini Games + +> **Note**: some platforms only have Chinese documentation available when visiting the platform's website. It may be necessary to use Google Translate in-order to review the documentation. + +__Cocos Creator__ officially supports the release of games to the **OPPO Mini Games**. + +## Environment Configuration + +- Download [OPPO Mini Game Debugger [cn]](https://cdofs.oppomobile.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use?id=_2-%e5%ae%89%e8%a3%85-runtimeapk-%e5%8c%85%e5%88%b0-oppo-%e6%89%8b%e6%9c%ba%e4%b8%8a) and install it on your OPPO phone (Android 6.0 or above is recommended) + +- Install [nodejs-8.1.4](https://nodejs.org/en/download/) or above, globally + +## Release Process + +1. Use **Cocos Creator 3.0** to open the project that needs to be released. Select **OPPO Mini Game** in the **Platform** dropdown of the **Build** panel. + + ![oppo build](./publish-oppo-mini-games/oppo-build.png) + + Click on the **oppo-mini-game** below to expand the build options configuration of OPPO Mini Game. + + ![build option](./publish-oppo-mini-games/build-option.png) + +For the general build options for each platform, please refer to [General Build Options](build-options.md) for details. OPPO Mini Game related build options filling rules are as follows: + +- **Start Scene Asset Bundle** + + This option is optional.
+ If set, the start scene and its related dependent resources are built into the built-in Asset Bundle - [start-scene](../../asset/bundle.md#the-built-in-asset-bundle) to speed up the resource loading of the start scene. + +- **Resource Server Address** + + This option is optional and used to fill in the address of the remote server where the resources are stored. + + - If this option is left blank, the `remote` folder in the release package directory will be packaged into the **rpk** package. + + - If this option is filled in, the `remote` folder will not be packaged into the built **rpk** package. You need to manually upload the `remote` folder to the filled in Resource Server Address after build. + + Refer to the Resource Management section at the bottom of the document for more details. + +- **Game Package Name**: is filled in according to the user's needs. It's required. + +- **Desktop Icon**: is required. Click the **search icon** button at the back of the input box to select the icon you want. When building, the Desktop Icon will be built into the **OPPO Mini Game** project. It is suggested to use **PNG** images for the **Desktop Icon**. + +- **Game Version Name**: is required. **Game Version Name** is the real version, such as: 1.0.0. + +- **Game Version Number**: is required. **Game Version Number** is different from the **Game Version Name**, and the **Game Version Number** is mainly used to distinguish the version update. Each time when you submit audit, the game version number is at least 1 higher than the value of the last submitted audit. It must not be equal to or less than the value of the last submitted audit, and it is recommended that the **Game Version Number** be recursively incremented by 1 each time when the audit is submitted. + + > **Note**: the **Game Version Number** must be a positive integer. + +- **Supported Minimum Platform Version Number**: is required. According to the requirements for OPPO Mini Games, this value must be greater than or equal to **1031**, and **1060** is recommended. Refer to the [Instructions [cn]](https://cdofs.oppomobile.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use) for details. + +- **Screen Orientation**: The screen orientation currently includes **Portrait** and **Landscape**. + +- **Seperate Engine**: This feature reduces the size of the first package for each mini-game by sharing the global engine. When enabled, if the engine already has a cache in the phone, the first package download will automatically remove the engine file and load the full version of the engine cached in the phone. If there is no cache in the phone, the full first package will be loaded, and the complete first package will contain the entire engine. + +- **Keystore**: when you check the **Keystore**, the default is to build the rpk package with a certificate that comes with Creator, which is used only for **debugging**. + + > **Note**: when the rpk package is to be used to submit an audit, do not check the **Keystore** to build it. + + If you don't check the **Keystore**, you need to configure the signature files **certificate.pem path** and **private.pem path**, where you build a rpk package that you can **publish directly**. The user can configure two signature files by using the **search icon** button to the right of the input box. + + There are two ways to generate a signature files: + + - Generated by the **New** button after the **certificate.pem path** in the **Build** panel. + + - Generated by the command line. + + The user needs to generate the signature file **private.pem**, **certificate.pem** through tools such as **openssl**. + + ```bash + # Generate a signature file with the openssl command tool + openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out certificate.pem + ``` + + > **Note**: **openssl** can be used directly in the terminal in **Linux** or **Mac** environment, and in the **Windows** environment you need to install **openssl** and configure system environment variables. Restart **Cocos Creator** after the configuration is complete. + +**2. Build** + +After the relevant parameters of the **Build** panel are set, click **Build**. When the build is complete, click the **folder icon** button below the corresponding build task to open the build release path, you can see that a directory with the same name as the **Build Task Name** is generated in the default release path `build` directory, which is the exported OPPO Mini Game project directory and **rpk**, **rpk** package is in the `dist` directory. + +![package](./publish-oppo-mini-games/package.png) + +**3. Run the built rpk to the phone** + +Copy the generated mini-game **rpk** file to the `games` directory on the phone's internal storage. Then open the **Mini Game Debugger** that has been installed before on the OPPO phone, click the **OPPO Mini Game** section, and then find the icon corresponding to the game name. If not found, click on the **More -> Refresh** button in the upper right corner to refresh. + + > **Note**: if the OPPO Mini Game Debugger version is `v3.2.0` and above, copy the mini-game **rpk** file to the `Android/data/com.nearme.instant.platform/files/games` directory on the OPPO phone's internal storage. If there is no `games` directory, create a new one. Please refer to the [Instructions -- New Directory [cn]](https://cdofs.oppomobile.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use?id=_3-%e6%96%b0%e5%bb%ba%e7%9b%ae%e5%bd%95) for details. + +![rpk games](./publish-oppo-mini-games/rpk_games.jpg) + +## Subpackage rpk + +Subpackage **rpk** can be used according to your needs. + +Subpackage loading, which is, splitting the game content into several packages according to certain rules, only downloading the necessary packages when starting up for the first time. This necessary package is called **main package**, The developer can trigger in the main package to download other sub-packages, which can effectively reduce the time spent on the first boot. + +To use this function, set the [Bundle Configuration](subpackage.md) in **Cocos Creator**, and the package will be automatically subpackaged when the setting is completed. + +After the build is complete, the subpackage directory is in the `dist` directory. In this case, create a new `subPkg` directory in the internal storage directory of the OPPO phone, and then copy the **.rpk** file in the `dist` directory to the `subPkg` directory. + +Then switch to the **Package Load** section of OPPO **Mini Game Debugger**, click **Refresh** at the top right to see the game name of the subpackage, click **Second Open** to use the same as the normal packaged **rpk**. + +![run_subpackage](./publish-oppo-mini-games/run_subpackage.jpg) + +Subpackage rpk needs to be copied to the `subPkg` directory of OPPO phone's internal storage, and non-subpackaged rpk needs to be copied to the `games` directory of OPPO phone's internal storage, both of which cannot be mixed. + +> **Note**: if the OPPO Mini Game Debugger version is **v3.2.0** and above, copy the mini game subpackaged **rpk** file to the `Android/data/com.nearme.instant.platform/files/subPkg` directory on the OPPO phone's internal storage, or create a new one if there is no `subPkg` directory. The non-subpackaged rpk is copied to the `Android/data/com.nearme.instant.platform/files/games` directory on the OPPO phone's internal storage, and the two cannot be mixed. + +For more information, please refer to the [OPPO Mini Game - Subpackage [cn]](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/subpackage/subpackage) documentation. + +## Resource Management for OPPO Mini Game Environment + +**OPPO Mini Game** is similar to **WeChat Mini Game**. There are restrictions on the package size. The main package size limit for OPPO Mini Game is **4MB**, more than that must be downloaded via a network request. + +After the game starts, the engine will automatically download resources from the remote server address. Once the resources are downloaded, the engine's cache manager will record the save paths of these resources. This information is used to automatically delete some cached game resources when the cache space is insufficient. Please refer to [Cache Manager](../../asset/cache-manager.md) for more details. + +## Reference documentation + +- [OPPO Developer Guides](https://developers.oppomobile.com/wiki/doc/index#id=88) +- [OPPO Mini Game Tutorial [cn]](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/quickgame) +- [OPPO Mini Game API Documentation [cn]](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/feature/account) +- [OPPO Mini Game Tool Download [cn]](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use) +- [OPPO Mini Game Instructions -- New Directory [cn]](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use?id=_3-%e6%96%b0%e5%bb%ba%e7%9b%ae%e5%bd%95) diff --git a/versions/4.0/en/editor/publish/publish-oppo-mini-games/build-option.png b/versions/4.0/en/editor/publish/publish-oppo-mini-games/build-option.png new file mode 100644 index 0000000000..5a8de90dab Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-oppo-mini-games/build-option.png differ diff --git a/versions/4.0/en/editor/publish/publish-oppo-mini-games/oppo-build.png b/versions/4.0/en/editor/publish/publish-oppo-mini-games/oppo-build.png new file mode 100644 index 0000000000..71da63ee91 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-oppo-mini-games/oppo-build.png differ diff --git a/versions/4.0/en/editor/publish/publish-oppo-mini-games/package.png b/versions/4.0/en/editor/publish/publish-oppo-mini-games/package.png new file mode 100644 index 0000000000..c12bf91e23 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-oppo-mini-games/package.png differ diff --git a/versions/4.0/en/editor/publish/publish-oppo-mini-games/rpk_games.jpg b/versions/4.0/en/editor/publish/publish-oppo-mini-games/rpk_games.jpg new file mode 100644 index 0000000000..b7e9ddb43c Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-oppo-mini-games/rpk_games.jpg differ diff --git a/versions/4.0/en/editor/publish/publish-oppo-mini-games/run_subpackage.jpg b/versions/4.0/en/editor/publish/publish-oppo-mini-games/run_subpackage.jpg new file mode 100644 index 0000000000..57888b1756 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-oppo-mini-games/run_subpackage.jpg differ diff --git a/versions/4.0/en/editor/publish/publish-pc-bytedance.md b/versions/4.0/en/editor/publish/publish-pc-bytedance.md new file mode 100644 index 0000000000..25363f57ac --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-pc-bytedance.md @@ -0,0 +1,28 @@ +# Access to Douyin PC Mini Games + +Douyin PC Mini Games allow you to play Douyin mini-games directly through the Streaming Companion. PC mini-games retain most features available on mobile but currently do not support the following: `keyDown`/`keyUp` keyboard events, screen recording, recording, ads, sharing, favorites, group chat, sidebar, payments, or capabilities that depend heavily on mobile device hardware, such as compass, accelerometer, camera, and microphone. + +Cocos Creator supports publishing games to Douyin PC Mini Games. Let’s look at how to publish your game to the Douyin PC mini-game platform through Cocos Creator. + +## Using Cocos Creator to Integrate Douyin PC Mini Games + +### Preparation + +Download and install the latest version of the [Douyin Streaming Companion for PC](https://streamingtool.douyin.com/). + +### Publishing Process + +1. Follow the process in [Publishing to Douyin Mini Games](./publish-bytedance-mini-game.md) to publish the project to Douyin Mini Games. +2. Douyin PC Mini Games require code to run in strict mode to ensure proper functionality. Compile the code in strict mode and resolve any issues arising from strict mode to avoid runtime errors. +3. Ensure the game runs smoothly in the IDE before debugging on the Streaming Companion. + +## FAQs + +**Q**: Does Douyin PC Mini Games support Mac systems? +**A**: Not yet. + +## Related Links + +- [Douyin PC Mini Games Integration Guide](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/open-ability/pc-game/tutorial) +- [Douyin PC Download](https://streamingtool.douyin.com/) +- [Douyin Developer Tools - IDE Download](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/developer-instrument/developer-instrument-update-and-download) \ No newline at end of file diff --git a/versions/4.0/en/editor/publish/publish-pc-wechatgame.md b/versions/4.0/en/editor/publish/publish-pc-wechatgame.md new file mode 100644 index 0000000000..50a2a32569 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-pc-wechatgame.md @@ -0,0 +1,35 @@ +# Access to WeChat PC Mini Games + +> **Note**: some platforms only have Chinese documentation available when visiting the platform's website. It may be necessary to use Google Translate in-order to review the documentation. + +WeChat PC Mini Game supports running WeChat mini games in WeChat for PC. The WeChat PC Mini Game will have most of the Mobile capabilities, including but not limited to virtual payment, open data context, touch events, etc. (ads are not currently supported). It also provides keyboard events, mouse events, window customization and other features. + +Cocos Creator supports publishing games to WeChat PC Mini Games, and completes the adaptation of mouse and keyboard related interfaces. Let's see how to publish the game to WeChat PC Mini Games by Cocos Creator. + +## Use Cocos Creator to Access to WeChat PC Mini Games + +### Preparation + +Download and install the latest version of [WeChat for PC](https://windows.weixin.qq.com/?lang=en_US) and login to it with the WeChat account bound to the WeChat DevTools. + +### Process of Publishing + +1. Refer to the process of [Publish to WeChat Mini Games](./publish-wechatgame.md) and publish the game project to the WeChat Mini Game. + +2. In the **WeChat DevTools**, click the **Preview** button in the upper toolbar, select the **Automatic Preview** tab, check the **Launch PC for auto preview** option, and then click the **Compile and Remote Debug** button to preview and debug the mini game on the WeChat for PC. + + ![WeChat PC preview](./publish-wechatgame/wechat-pc.png) + +## FAQ + +Q: How to distinguish the **Mobile** and **PC** of WeChat through the engine interface?
+A: You can determine by `sys.isMobile` that the PC side returns `false` and the mobile side returns `true`.
+> **Note**: the simulator on the WeChat DevTools simulates a environment on the mobile, so it returns `true`. + +Q: Does the WeChat PC Mini Game support Mac?
+A: Yes. WeChat PC Mini Game already support Mac. The publishing process is the same as above. + +## Related Reference Links + +- [WeChat PC Mini Game Access Guide [cn]](https://developers.weixin.qq.com/minigame/dev/guide/open-ability/pc-game.html) +- [Download WeChat for PC](https://windows.weixin.qq.com/?lang=en_US) diff --git a/versions/4.0/en/editor/publish/publish-taobao-mini-game.md b/versions/4.0/en/editor/publish/publish-taobao-mini-game.md new file mode 100644 index 0000000000..5d6f7a2075 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-taobao-mini-game.md @@ -0,0 +1,81 @@ +# Publish to Taobao Mini Game + +## Environment Configuration + +- Download [Taobao Mini Game Studio](https://developer.taobao.com/?spm=a219a.15212435.0.0.6a14669aEQ2g6k) to the desktop and install it. + +- Download [Taobao](https://market.m.taobao.com/app/fdilab/download-page/main/index.html) and install it on your mobile device. + +- The minimum version supported of the Taobao client on Android is 10.22.30 and on iOS is 10.22.30. + +## Publishing Process + +Use Cocos Creator to open the project to be published, select **Taobao Mini Game** in **Platform** of the **Build** panel, and then click **Build**. + +![build option](./publish-taobao-mini-game/build_option.png) + +### Parameter Configuration + +The specific filling rules for related parameter configuration are as follows: + +- **Main Package Compression Type** + + Set the compression type of the main package. For specific content, please refer to the document [Asset Bundle — Compression Type](../../asset/bundle.md#compression-type). + +- **Configure Main Package as Remote Package** + + This is an optional item and needs to be used in conjunction with the option **Resource Server Address**.
+ After checking, the main package will be configured as a remote package and built with its related dependent resources into the built-in Asset Bundle — [main](../../asset/bundle.md#the-built-in-Asset-Bundle) under the release package directory remote.Developers need to upload the entire remote folder to the remote server. + +- **Start Scene Asset Bundle** + + This option is optional.
+ If set, the start scene and its related dependent resources are built into the built-in Asset Bundle - [start-scene](../../asset/bundle.md#the-built-in-asset-bundle) to speed up the resource loading of the start scene. + +- **Resource Server Address** + + This is an optional item used to fill in the address where resources are stored on a remote server. After building, developers need to manually upload the remote folder under the release package directory to the filled-in resource server address. + +- **Separate Engine** + This option is optional.
+ After checking, the initial package size of the game can be reduced. You can refer to the document [Usage Instructions](./taobaominigame-plugin.md) for instructions on how to use it. Please select Taobao Mini Game as the building platform. + +### Running Preview + +- After the build is finished, click the folder icon button in the bottom left corner of **Build Task**, you can see that the project folder `taobao-mini-game` is created in the `build` directory of the project, which already contains the configuration file `game.json` for the Taobao Mini Game environment. + +![build](./publish-taobao-mini-game/build.png) + +- Use **Taobao Mini Game Studio** to open the `taobao-mini-game` folder generated by the build to open the Taobao Mini Game project and preview/debug the game content. + +![preview](./publish-taobao-mini-game/preview.png) + +## Resource management for Taobao Mini Game environment + +Taobao Mini Game is similar to WeChat Mini Game in that there is a package size limit, and additional resources over **4MB** must be downloaded via network requests.
When the package size is too large, configure the **Resource Server Address** option in the **Build** panel to upload the resources to a remote server, please refer to the [Upload Resources to Remote Server](../../asset/cache-manager.md) documentation. + +After the game starts, the engine will automatically download resources from the remote server address. Once the resources are downloaded, the engine's cache manager will record the save paths of these resources. This information is used to automatically delete some cached game resources when the cache space is insufficient. Please refer to [Cache Manager](../../asset/cache-manager.md) for more details. + +## Restrictions for Taobao Mini Game + +Work on adapting the Taobao Mini Game is not completely finished yet, and it is known that the text rendering may not work well on low-end machines. And the following modules are still not supported: + +- WebView +- VideoPlayer +- Custom Fonts + +## Differences Between Taobao and Other Mini Game Platforms + +- Access to Global Variables: Accessing global variables in Taobao requires mounting them onto the global object. For example: `$global.my = my;` + +- Explanation of 'global-variables.js': The window variable is a reference to the global object, so it is necessary to ensure that variables on the global object already exist for temporary variables to have values. If you are using custom scripts or third-party plugins and encounter situations where global variables are missing, it is typically due to loading timing issues, where the script is used before it is fully loaded. + +- The Taobao Mini Game platform currently uses a self-developed JS virtual machine (which may be replaced with v8 in the future). The JS performance of the current platform version is slightly inferior to the mainstream v8 engine, so when publishing games on this platform, special attention needs to be paid to the impact of the game's DrawCall count on performance. The Cocos engine provides a dynamic atlas feature to reduce draw calls generated by the game interface. You can enable the CLEANUP_IMAGE_CACHE option when building for the Taobao Mini Game platform. + +- Inconsistencies Among Taobao IDE, Simulator, and Real Device JS Environments: The JavaScript environments in Taobao's IDE, simulator, and real devices are not consistent. If there are no issues on real devices, it is recommended to report the problem directly to the Taobao platform. + +- Due to the additional performance overhead in real device debugging mode, it is advisable to use real device preview mode, rather than real device debugging mode, when verifying frame rates. + +## References + +- [Taobao Document Center - CN](https://miniapp.open.taobao.com/doc.htm?docId=121719&docType=1&tag=game-dev) \ No newline at end of file diff --git a/versions/4.0/en/editor/publish/publish-taobao-mini-game/build.png b/versions/4.0/en/editor/publish/publish-taobao-mini-game/build.png new file mode 100644 index 0000000000..abc43ec3b3 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-taobao-mini-game/build.png differ diff --git a/versions/4.0/en/editor/publish/publish-taobao-mini-game/build_option.png b/versions/4.0/en/editor/publish/publish-taobao-mini-game/build_option.png new file mode 100644 index 0000000000..f0474c9a6b Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-taobao-mini-game/build_option.png differ diff --git a/versions/4.0/en/editor/publish/publish-taobao-mini-game/preview.png b/versions/4.0/en/editor/publish/publish-taobao-mini-game/preview.png new file mode 100644 index 0000000000..bc3f7aae11 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-taobao-mini-game/preview.png differ diff --git a/versions/4.0/en/editor/publish/publish-vivo-mini-game.md b/versions/4.0/en/editor/publish/publish-vivo-mini-game.md new file mode 100644 index 0000000000..875830b4c7 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-vivo-mini-game.md @@ -0,0 +1,120 @@ +# Publish to vivo Mini Games + +> **Note**: some platforms only have Chinese documentation available when visiting the platform's website. It may be necessary to use Google Translate in-order to review the documentation. + +## Environment Configuration + +- Download the [Quick App & vivo Mini Game Debugger](https://minigame.vivo.com.cn/documents/#/lesson/base/environment?id=%E5%AE%89%E8%A3%85vivo%E5%B0%8F%E6%B8%B8%E6%88%8F%E8%B0%83%E8%AF%95%E5%99%A8) and [vivo Mini Game Engine](https://minigame.vivo.com.cn/documents/#/lesson/base/environment?id=%E5%AE%89%E8%A3%85vivo%E5%B0%8F%E6%B8%B8%E6%88%8F%E5%BC%95%E6%93%8E) and install it on your Android device (recommended Android Phone 6.0 or above) + +- Install [nodejs-8.9.0](https://nodejs.org/en/download/) or above, globally: + + > **Note**: after installing nodejs, you need to note whether the npm source address is + + ```bash + # View current npm source address + npm config get registry + + # If not, reset the npm source address + npm config set registry https://registry.npmjs.org/ + ``` + +- Install `vivo-minigame/cli` globally: + + ```bash + npm install -g @vivo-minigame/cli + ``` + + If `vivo-minigame/cli` installation fails, it may be caused by too low version of **nodejs**. Please check the version of **node** and upgrade. + +## Build Options + +For some general build options of platforms, please refer to the [General Build Options](build-options.md) documentation for details. + +| Name | Optional | Default value | Description | Field name | +| :-- | :-- | :-- | :-- | :-- | +| **Start Scene Asset Bundle** | Optional | false | If set, the start scene and its related dependent resources are built into the built-in Asset Bundle — [start-scene](../../asset/bundle.md#the-built-in-asset-bundle) to speed up the resource loading of the start scene. | `startSceneAssetBundle` | +| **Remote server address** | Optional | Empty | This option is optional and used to fill in the address of the remote server where the resources are stored.If this option is left blank, the `build/vivo-mini-game/remote` folder in the release package directory will be packaged into the rpk package.Refer to the Resource Management section for more details. | `remoteServerAddress` | +| **Game Package Name** | required | (Project Name) | such as `com.example.demo` | `package` | +| **Desktop Icon** | required | (Cocos Logo) | Click the **search icon** button at the back of the input box to select the icon you want. When building, the **Desktop Icon** will be built into the vivo Mini Game project. It is suggested to use `PNG` images for the **Desktop Icon**. | `icon` | +| **Game Version Name** | required | (Cocos version) | **Game Version Name** is the real version, such as: **1.0.0**. | `versionName` | +| **Game Version Number** | required | 1201 | **Game Version Number** is different from the **Game Version Name**, and the **Game Version Number** is mainly used to distinguish the version update. Each time when you submit audit, the game version number is at least 1 higher than the value of the last submitted audit. It must not be equal to or less than the value of the last submitted audit, and it is recommended that the **Game Version Number** be recursively incremented by 1 each time when the audit is submitted. | `versionCode` | +| **Supported Minimum Platform Version Number** | required | 1035 | Please refer to [Official Documentation [cn]](https://minigame.vivo.com.cn/documents/#/download/engine?id=%E6%9B%B4%E6%96%B0%E8%AE%B0%E5%BD%95%EF%BC%9A) to check the latest version number of vivo engine. | `minPlatformVersion` | +| **Orientation** | - | landscape | Device direction, it will be written in `manifest.json`.| `deviceOrientation` | +| **Separate Engine** | Optional | - | Vivo has added **game engine plugin** feature since platform version number **1063**. This plugin has the official version of the Cocos Creator engine built-in. If the plugin is enabled in the first game the player experiences, all games that also have the plugin enabled do not need to download the Cocos Creator engine again, just use the same version of the engine directly from the public plugin library, or incremental update the engine.
Check **Separate Engine** when using, and then build and release normally in the **Build** panel, without additional manual operation. Please refer to the [WeChat Engine Plugin Instructions](./wechatgame-plugin.md) for details. | `separateEngine` | +| **Use debug keystore** | - | true | When you check **Use Debug Keystore**, it means that the rpk package built with the certificate that comes with Creator is used by default, and it is only used for **debugging**. when the rpk package is to be used to submit an audit, do not check the **Use Debug Keystore** to build it.| `useDebugKey` | +| **Key certification path** | - | - | The key store certificate, the quick game on the Huawei App Market, must be signed with the release version certificate, and the certificate fingerprint must be configured in the background of the Huawei Developers Alliance. For details, please refer to the following **Generate Signature File** | `privatePemPath`, `certificatePemPath` | + +### Generate signature file + +If you don't check the **Keystore**, you need to configure the signature files **certificate.pem path** and **private.pem path**, where you build a rpk package that you can **publish directly**. The developer can configure two signature files by using the **search icon** button to the right of the input box. + +There are two ways to generate a signature files: + +- Generated by the **New** button after the **certificate.pem path** in the **Build** panel. + +- Generated by the command line. + + The developer needs to generate the signature file **private.pem**, **certificate.pem** through tools such as **openssl**. + + ```bash + # Generate a signature file with the openssl command tool + openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out certificate.pem + ``` + + > **Note**: **openssl** can be used directly in the terminal in Linux or Mac environment, and in the Windows environment you need to install `openssl` and configure system environment variables. Restart Creator after the configuration is complete. + +## Run the rpk + +![rpk](./vivo-mini-game/rpk.png) + +There are three ways to run rpk on your phone: + +- **Method One** + + Click the **Run** button at the bottom right of the `vivo-mini-game` build task in the **Build** panel and wait for the QR Code interface to be generated: + + ![play](./vivo-mini-game/play.jpg) + + Then open the **Quick App & vivo Mini Game Debugger** that was installed before on your Android device. Click the **Scan code install** button to scan the QR Code to open the **rpk**. + + ![vivo-instant_scan_install](./vivo-mini-game/vivo-instant_scan_install.jpg) + +- **Method Two** + + Copy the generated mini game **rpk** file (located in the `dist` directory) to the internal storage directory of the mobile phone. + + Open the **Quick App & vivo Mini Game Debugger** that has been installed before on the Android device, click **Local Install**, then find the **rpk** file from the internal storage directory of your mobile phone and select **Open**. + + ![vivo-instant_native_install](./vivo-mini-game/vivo-instant_native_install.jpg) + +- **Method Three** + + Specify to the editor installation directory `resources/tools/vivo-pack-tools` in the command line, and execute the command `npm run server` to generate URL and QR code using the vivo Mini Game Packer Commands. + + ```bash + # Specify to the editor installation directory. + cd F:/CocosCreator/resources/tools/vivo-pack-tools + + # Generate URL and QR code + npm run server + ``` + + Then open the **Quick App & vivo Mini Game Debugger** that was installed before on your Android device. + + Finally, click the **Scan code install** button to copy the URL generated in the first step to the browser, and then directly scan the QR code on the web page to open the **rpk**. + +## Subpackage Loading + +The subpackage loading of vivo Mini Games is similar to WeChat Mini Games. Please refer to the [Mini Game Subpackage](subpackage.md) documentation for details. + +## vivo Mini Game Environment Resource Management + +Similar to WeChat Mini Games, Vivo Mini Games also have package size limitations. The main package size limit for Honor Mini Games is **4MB**, and any content exceeding this limit must be downloaded via network requests.
When the package size is too large, you can configure the **Remote Server Address** option in the **Build and Publish** panel to upload low-priority resources to a remote server. For more details, please refer to [Uploading Resources to Remote Server](../../asset/cache-manager.md). + +After the game starts, the engine will automatically download resources from the remote server address. Once the resources are downloaded, the engine's cache manager will record the save paths of these resources. This information is used to automatically delete some cached game resources when the cache space is insufficient. Please refer to [Cache Manager](../../asset/cache-manager.md) for more details. + +## Reference documentation + +- [vivo Mini Games Development Documentation [cn]](https://minigame.vivo.com.cn/documents/#/lesson/base/start) +- [vivo Mini Games API Documentation [cn]](https://minigame.vivo.com.cn/documents/#/api/system/life-cycle) +- [Quick App & vivo Mini Game Debugger Download [cn]](https://minigame.vivo.com.cn/documents/#/download/debugger) diff --git a/versions/4.0/en/editor/publish/publish-web.md b/versions/4.0/en/editor/publish/publish-web.md new file mode 100644 index 0000000000..38d637772d --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-web.md @@ -0,0 +1,85 @@ +# Publish to Web Platforms + +Select **Project -> Build** from the main menu, and open the [Build](build-panel.md) panel. + +![web](publish-web/web.png) + +Cocos Creator provides two web platform page templates. Select **Web Mobile** or **Web Desktop** through the drop-down menu of **Platform**. The main differences between them are: + +- **Web Mobile** will fill the entire browser window with the game view by default. +- **Web Desktop** allows to specify the resolution of a game view when publishing, and the game view will not change with the browser window size afterwards. + +## Introduction to Build Options + +For the general build options of each platform, please refer to the [General Build Options Introduction](build-options.md) documentation for details. + +### Web Desktop + +| Build options | Description | Field name (for command line publishing) | +| :--- | :--- | :--- | +| Resource Server Address | The server address used to download remote resources. For details, please refer to the content of the **Resource Server Address** section below. | `remoteServerAddress` | +| WEBGPU | Whether use WebGPU as rendering backend | `WEBGPU` | +| Preview resolution | Game view resolution, the default is **(1280, 960)** | `resolution` | +| Polyfills | Build supports polyfills of some new features of scripts. Corresponding processing will be done when scripts are packaged. Developers can choose the polyfills to use according to actual needs. For the time being, only **Async Functions** are supported, and more functions will be opened in the future. | `polyfills` | + +### Web Mobile + +| Build options | Description | Field name (for command line publishing) | +| :--- | :--- | :--- | +| Resource Server Address | The server address used to download remote resources. For details, please refer to the content of the **Resource Server Address** section below. | `remoteServerAddress` | +| Orientation | Optional values ​​include **Auto**, **Landscape**, **Portrait** | `orientation` | +| Polyfills | Build supports polyfills of some new features of scripts. Corresponding processing will be done when scripts are packaged. Currently, there are **async Functions** and **coreJs**. Developers can choose the polyfills to use according to actual needs. | `polyfills` | +| vConsole | Insert the vConsole debugging plug-in. vConsole is similar to the mini version of DevTools for assisting debugging. | `embedWebDebugger` | +| Preview the QR code | Used to scan and preview, see the introduction below for details |-| +| Preview URL | Link for preview, see the introduction below for details |-| + +- **Resource server address** + + This option is optional and used to fill in the address where the resource is stored on the server. + + - If **do not fill in**, the `remote` folder in the release package directory will be packaged into the built game package. + - If **fill in**, it will not be packaged into the game package. Developers need to manually upload the `remote` folder in the release package directory to the resource server address filled in after the build. For details, please refer to [Upload resources to a remote server](../../asset/cache-manager.md). + +- **Preview URL** + + The build supports simultaneous previewing of multiple Web projects, so the preview URL to be built is no longer uniform, but each build task will have a separate preview URL, which does not interfere with each other. Click the URL to automatically open the browser for preview. The specific preview URL splicing rule is **${Preview IP address in Preferences panel}:${Editor preview port number}/${Build platform}/${Build task name}/index.html**. + + ![preview-url](publish-web/preview-url.png) + +## Build and Preview + +After configuring the build options, click the **Build** button to start the web platform version build. A progress bar will appear on the panel. When the progress bar shows **Build success**, the build is complete. + +Next, click the **Run** button to open the built game version in the browser for preview and debugging. + +![web mobile](publish-web/web-mobile.png) + +The picture above is the preview of the Web Mobile platform. Notice that the game view occupies the entire browser window, while the game view of Web Desktop has a fixed resolution and will not fill the screen. + +### WebGPU Support (Experimental) + +[WebGPU](https://www.w3.org/TR/webgpu/) is the next generation GPU API for the web. Cocos Creator supports WebGPU as rendering backend for building Web-Desktop since 3.6.2, just enable `WebGPU` option in the build panel when building Web-Desktop. +The build process will be a little bit different with `WebGPU` option enabled. Normal builtin server won't work for `WebGPU` package. When building done, you need to locate the package folder by clicking the folder icon in the build panel. Then you can either start an http-server or nginx server to make an accessible address for the supported browsers. +As WebGPU standard is still a working draft, it's not widely supported on all browsers yet. So please remember to check the compatibility of WebGPU on your browser, see chapter **Browser Compatibility** below for details. + +### Browser Compatibility + +The desktop browsers tested during the development of Cocos Creator include: **Chrome**, **Firefox** and **QQ Browser**. Other browsers can be used normally as long as the kernel version is high enough. For the browser, please do not enable IE compatibility mode. + +Browsers tested on mobile devices include: **Safari (iOS)**, **Chrome (Android)**, **QQ Browser (Android)** and **UC Browser (Android)**. + +When you build Web-Desktop platform with `WEBGPU` option enabled, only certain versions of chromium is supported currently. +Chromium history version is available [here](https://vikyd.github.io/download-chromium-history-version/#/), version 105 and 106 should be well supported by Cocos Creator's WebGPU build, support for later version is comming soon. +After chromium is opened, visit `chrome://flags` in the address bar, then enable `WebGPU Developer Features` and reopen chromium, then everything is set for you to try your project rendered with WebGPU. + +## Retina Settings + +Set whether to use high resolution through `view.enableRetina(true)` in the script. Retina display will be turned on by default when it is built on the Web platform. For details, please refer to the [enableRetina](%__APIDOC__%/en/class/View?id=enableRetina) API. + +## Publish to Web Server + +To publish or share the game on the Internet, just click the **folder icon** button at the bottom left of the **Build Task**, and after opening the release path, follow the name of the current build task to build the corresponding folder in the the entire content is copied to the Web server and can be accessed through the corresponding address. + +![web mobile](publish-web/web-folder.png) + +Regarding the establishment of the Web server, search for related solutions such as Apache, Nginx, IIS, Express, etc. diff --git a/versions/4.0/en/editor/publish/publish-web/preview-url.png b/versions/4.0/en/editor/publish/publish-web/preview-url.png new file mode 100644 index 0000000000..6dbaf408c2 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-web/preview-url.png differ diff --git a/versions/4.0/en/editor/publish/publish-web/web-folder.png b/versions/4.0/en/editor/publish/publish-web/web-folder.png new file mode 100644 index 0000000000..3486a1b160 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-web/web-folder.png differ diff --git a/versions/4.0/en/editor/publish/publish-web/web-mobile.png b/versions/4.0/en/editor/publish/publish-web/web-mobile.png new file mode 100644 index 0000000000..a67bd441b2 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-web/web-mobile.png differ diff --git a/versions/4.0/en/editor/publish/publish-web/web.png b/versions/4.0/en/editor/publish/publish-web/web.png new file mode 100644 index 0000000000..b1ee8469f3 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-web/web.png differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame.md b/versions/4.0/en/editor/publish/publish-wechatgame.md new file mode 100644 index 0000000000..a1e6c09582 --- /dev/null +++ b/versions/4.0/en/editor/publish/publish-wechatgame.md @@ -0,0 +1,141 @@ +# Publish to WeChat Mini Games + +> **Note**: some platforms only have Chinese documentation available when visiting the platform's website. It may be necessary to use Google Translate in-order to review the documentation. + +The runtime environment of the **WeChat Mini Game** is an extension of the **WeChat Mini Program**, providing a WebGL interface encapsulation based on the mini program environment, greatly improving rendering capabilities and performance. However, since these interfaces are encapsulated by the WeChat team, they are not equivalent to the browser environment. + +On the engine side, in order to make the developers' workload as easy as possible, our main tasks for developers include the following: + +- The engine framework adapts to the WeChat Mini Game API, pure game logic level, developers do not need any additional modifications. +- The **Cocos Creator** editor provides a fast packaging process, released directly as a **WeChat Mini Game**, and automatically evokes the **WeChat DevTools**. +- Automatically load remote assets, cache assets, and cache asset version control. + +In addition, the game submission, review and release process of the **WeChat Mini Game** is no different from the **WeChat Mini Program**. Please refer to the [WeChat Mini Game Developer](https://developers.weixin.qq.com/minigame/en/dev/guide/) documentation. + +## Publish WeChat Mini Games with Cocos Creator + +1. Download the **WeChat DevTools** on [WeChat Official Document](https://developers.weixin.qq.com/miniprogram/en/dev/devtools/download.html) + +2. Set the **WeChatGame App Path** in **Cocos Creator/File -> Preferences -> [Native Develop](../../editor/preferences/index.md)**. + + ![preference](./publish-wechatgame/preference.png) + +3. Log in to the WeChat public platform and find the appid + + ![appid](./publish-wechatgame/appid.jpeg) + +4. Select the **WeChat Game** in the **Platform** of the **Build** panel, fill in the `mini game appid`, and then click **Build** + + ![build](./publish-wechatgame/build.png) + +5. Click **Play** to open the **WeChat DevTools** + + ![tool](./publish-wechatgame/tool.jpeg) + + > **Note**: the **WeChat DevTools**, if it has not been run on a Mac before, will show an error that states: `Please ensure that the IDE has been properly installed`. You need to manually open the **WeChat DevTools** once, before you can click **Run**. + +6. Preview deployment + + Following this process, a `wechatgame` folder will be generated in the project's **build** directory (the name of the folder is based on the **Build Task Name**), which already contains the configuration files `game.json` and `project.config.json` of the WeChat Mini Games environment. + + ![package](./publish-wechatgame/package.jpg) + +### Build Options + +![build](./publish-wechatgame/wechat-build.png) + +| Options | Optional or not | Default | Explanation | +| :-- | :-- | :-- | :-- | +| **appid** | Required | `wx6ac3f5090a6b99c5` | The appid of the WeChat Mini Games, it will be written to `project.config.json` file.| +| **Start Scene Asset Bundle** | Optional | false | If set, the start scene and its related dependent resources are built into the built-in Asset Bundle — [start-scene](../../asset/bundle.md#the-built-in-asset-bundle) to speed up the resource loading of the start scene.| +| **Open data context root** | Optional | Empty | If an Open Data Context exists, use this root to specify the relative path of the Open Data Context folder in the build directory so that the directory is not overwritten or modified during the build.| +| **Orientation** | Required | `landscape` | Device orientation, it will be written to `game.json` file.| +| **Separate Engine** | Optional | Empty | Whether to use WeChat Mini Games engine plugin, please refer to [WeChat Mini Games Engine Plugin Instructions](./wechatgame-plugin.md) for details. | +| **High Performance Mode** | Whether to use hight performance mode
Please refer to [High Performance Mode](https://developers.weixin.qq.com/minigame/dev/guide/performance/perf-high-performance.html) for more details. | +| **Wasm 3D physics system (based on `ammo.js`)** | Optional | Enabled | This option is used to select whether to enable **Wasm**, which takes effect when using **bullet(ammo.js)** physics. Please refer to the **WebAssembly Support** section below for more details. | +| **Whether to enabled WebGL2** (Experimental feature) | - | Forced Off | In order to allow WebGL 2.0 to be enabled on WeChat Mini Games in the future, we have added this option in **v3.4.1** to support to enable WebGL 2.0 following the configuration in the **Project Settings** panel, and to turn off to reduce the package by default.
If this option is set to **Consistent with project settings** and **WebGL 2.0** is checked in **Project Settings -> Feature Cropping**, it will be successfully enabled in the future if the WeChat environment supports WebGL 2.0. | + +## Asset Management for WeChat Mini Game Environment + +In a **WeChat Mini Game** environment, asset management is the most special part. It differs from the browser in the following four points: + +1. The size of the **WeChat Mini Game** package cannot exceed **4MB**, including all the code and assets. Additional assets must be downloaded via web request. + +2. For files downloaded from a remote server, the **WeChat Mini Game** environment does not have the browser's caching and outdated update mechanism. + +3. For the assets in the **WeChat Mini Game** package, they are not loaded on demand in the mini game environment, but rather all the assets in the package are loaded at once, and then the game page is launched. + +4. You cannot download script files from a remote server. + +This brings up two key issues, home page loading speed and remote asset caching and version management. For the home page loading speed, we recommend that developers only save the script file in the **WeChat Mini Game** package, and all other assets are downloaded from the remote server. As for downloading, caching and version management of remote assets, **Cocos Creator** has done the job for developers. + +In the **WeChat Mini Game** environment, we provide a `wxDownloader` object, and after setting the `REMOTE_SERVER_ROOT` property to it, the logic of the engine to download assets becomes: + +1. Check that assets are in the mini game package. +2. If not present, query local cache assets. +3. If no local cache assets are available, download from a remote server. +4. Download and save them to the mini game application cache in backstage for re-access. +5. Local cache storage has space limitation, if total space of cache exceeds the limit, there will be no more caching without disturbing game process. + +It should be noted that once the cache space is full, all the assets that need to be downloaded cannot be saved, only the temporary files for save download assets can be used, and WeChat will automatically clean up all temporary files after the mini game is exited. So the next time you run the mini game again, those assets are downloaded again and the process keeps looping.
+In addition, the problem of file saving failure due to cache space exceeding the limit does not occur on the **WeChat DevTools**, because the **WeChat DevTools** does not limit the cache size, so testing the cache needs to be done in a real WeChat environment. + +At the same time, when the **MD5 Cache** feature of the engine is enabled, the URL of the file will change as the content of the file changes, so that when a new version of the game is released, the assets of the old version will naturally become invalid in the cache, and only the new assets can be requested from the server, which achieves the effect of version control. + +Specifically, developers need to do: + +1. When building, check the **MD5 Cache** in the **Build** panel. +2. Set the **Remote service address**, and then click **Build**. +3. When the build is complete, upload the **res** folder in the mini game release package to the server. +4. Delete the **res** folder inside the local release package. +5. For the test phase, you may not be able to deploy to the official server, you need to use the local server to test, then open the details page in the WeChat DevTools, check the `Does not verify valid domain names, web-view (business domain names), TLS versions and HTTPS certificates` option in the **Local Settings**. + + ![detail](./publish-wechatgame/detail.jpeg) + +> **Note**: if the cache asset exceeds the WeChat environment limit, you need to manually clear the asset. Use `wx.downloader.cleanAllAssets()` and `wx.downloader.cleanOldAssets()` to clear the cache in **WeChat Mini Games**. The former clears all the cache assets in the cache directory, please use it carefully. While the latter clears cache assets that are currently unused in the cache directory in the application. + +## WeChat Mini Game Subpackage Loading + +To achieve subpackage loading with **WeChat Mini Game**, please refer to [Mini Game Subpackage](subpackage.md) documentation. + +## Platform SDK Access + +In addition to pure game content, the **WeChat Mini Game** environment actually provides a very powerful native SDK interface, the most important of which are user, social, payment, etc. These interfaces are only available in the **WeChat Mini Game** environment, equivalent to third-party SDK interfaces for other platforms. The porting of such SDK interfaces still needs to be handled by developers at this stage. Here are some of the powerful SDK capabilities provided by WeChat Mini Games: + +1. User interface: login, authorization, user information, etc. +2. WeChat payment +3. Forward and get forwarding information +4. File upload and download +5. Media: pictures, recordings, cameras, etc. +6. Other: location, device information, scan code, NFC, etc. + +## WeChat Mini Games Known issues + +Cocos Creator's adaptation of **WeChat Mini Games** has not been completely implemented. The following modules are still not supported: + +- VideoPlayer +- WebView + +It is possible to use the missing functionality by calling the **WeChat's** API directly. + +## WebAssembly Support + +> **Note**: this section has been significantly changed in v3.3.1. For v3.3.0, please switch to the previous version documentation (e.g., v3.2) in the upper right corner of the page to view the content. + +As of 3.0, the **Wasm 3D physics system (based on `ammo.js`)** option has been added to the build options of WeChat Mini Game, which takes effect when **Projects -> Project Settings -> Feature Cropping -> 3D -> Physics System** in the editor's main menu is set to **bullet(ammo.js)**. + +The **Wasm 3D physics system** option is enabled by default, and the engine will automatically package codes in `wasm` mode when building. If disabled, `js` mode is used. + +> **Notes**: +> 1. The WeChat Mini Games Separation Engine Plugin currently only supports **js** mode. +> 2. WebAssembly required WeChat v7.0.17 and above. +> 3. The WeChat WebAssembly debugging base library needs to be v2.12.0 and above. + +## Reference documentation + +- [WeChat Mini Game Developer Document](https://developers.weixin.qq.com/minigame/en/dev/guide/) +- [WeChat Public Platform](https://mp.weixin.qq.com/?lang=en_US) +- [WeChat Mini Game API Documentation](https://developers.weixin.qq.com/minigame/en/dev/api/) +- [WeChat DevTools](https://developers.weixin.qq.com/miniprogram/en/dev/devtools/devtools.html) +- [WeChat DevTools Download](https://mp.weixin.qq.com/debug/wxagame/en/dev/devtools/download.html) +- [WeChat Cache Space Overflow Case](https://github.com/cocos-creator/test-wechat-mini-game) diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/appid.jpeg b/versions/4.0/en/editor/publish/publish-wechatgame/appid.jpeg new file mode 100644 index 0000000000..1bd570ccb2 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/appid.jpeg differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/build.png b/versions/4.0/en/editor/publish/publish-wechatgame/build.png new file mode 100644 index 0000000000..9d7d3fcc98 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/build.png differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/detail.jpeg b/versions/4.0/en/editor/publish/publish-wechatgame/detail.jpeg new file mode 100644 index 0000000000..6c2584a505 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/detail.jpeg differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/game-json.png b/versions/4.0/en/editor/publish/publish-wechatgame/game-json.png new file mode 100644 index 0000000000..9e7ed4fa0f Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/game-json.png differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/maintest-build.png b/versions/4.0/en/editor/publish/publish-wechatgame/maintest-build.png new file mode 100644 index 0000000000..e45b3a3c7c Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/maintest-build.png differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/open-data-project-build.png b/versions/4.0/en/editor/publish/publish-wechatgame/open-data-project-build.png new file mode 100644 index 0000000000..8b2dd5919b Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/open-data-project-build.png differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/package.jpg b/versions/4.0/en/editor/publish/publish-wechatgame/package.jpg new file mode 100644 index 0000000000..453c49d44b Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/package.jpg differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/package.png b/versions/4.0/en/editor/publish/publish-wechatgame/package.png new file mode 100644 index 0000000000..f00dc1df73 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/package.png differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/preference.png b/versions/4.0/en/editor/publish/publish-wechatgame/preference.png new file mode 100644 index 0000000000..34de48bd65 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/preference.png differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/preview.jpeg b/versions/4.0/en/editor/publish/publish-wechatgame/preview.jpeg new file mode 100644 index 0000000000..e0875e4e2a Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/preview.jpeg differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/preview.png b/versions/4.0/en/editor/publish/publish-wechatgame/preview.png new file mode 100644 index 0000000000..7849d2ee83 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/preview.png differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/tool.jpeg b/versions/4.0/en/editor/publish/publish-wechatgame/tool.jpeg new file mode 100644 index 0000000000..2c710168ee Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/tool.jpeg differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/wechat-build.png b/versions/4.0/en/editor/publish/publish-wechatgame/wechat-build.png new file mode 100644 index 0000000000..703948657f Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/wechat-build.png differ diff --git a/versions/4.0/en/editor/publish/publish-wechatgame/wechat-pc.png b/versions/4.0/en/editor/publish/publish-wechatgame/wechat-pc.png new file mode 100644 index 0000000000..fbe5f592b6 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-wechatgame/wechat-pc.png differ diff --git a/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/build.jpg b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/build.jpg new file mode 100644 index 0000000000..fcbb794a8c Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/build.jpg differ diff --git a/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/certificate.png b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/certificate.png new file mode 100644 index 0000000000..ce93306c86 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/certificate.png differ diff --git a/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/debug.png b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/debug.png new file mode 100644 index 0000000000..7510fe7da3 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/debug.png differ diff --git a/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/play.png b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/play.png new file mode 100644 index 0000000000..bbfdc3a1e5 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/play.png differ diff --git a/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/play2.png b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/play2.png new file mode 100644 index 0000000000..67e219f335 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/play2.png differ diff --git a/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/rpk.png b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/rpk.png new file mode 100644 index 0000000000..0756594ccb Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/rpk.png differ diff --git a/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/run.jpg b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/run.jpg new file mode 100644 index 0000000000..b072797a6d Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/run.jpg differ diff --git a/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/xiaomi_options.jpg b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/xiaomi_options.jpg new file mode 100644 index 0000000000..52b34e9ec6 Binary files /dev/null and b/versions/4.0/en/editor/publish/publish-xiaomi-quick-game/xiaomi_options.jpg differ diff --git a/versions/4.0/en/editor/publish/setup-native-development.md b/versions/4.0/en/editor/publish/setup-native-development.md new file mode 100644 index 0000000000..3c1c1a51c7 --- /dev/null +++ b/versions/4.0/en/editor/publish/setup-native-development.md @@ -0,0 +1,94 @@ +# Setting Up Native Development Environment + +Cocos Creator supports building projects into native application projects that comply with different platform specifications. Before building and publishing native applications, developers need to configure the relevant native development environment. + +### Download Java SDK (JDK) + +Compiling Android projects requires a complete Java SDK toolkit on your development machine. Please download the appropriate installation package matching your operating system and architecture from: + +[JDK Development Kit 17.0.7 downloads](https://www.oracle.com/java/technologies/downloads/#java17) + +For Windows systems, after installing JDK, you need to add the `JAVA_HOME` system variable to environment variables. Follow these steps: Right-click My Computer -> Properties -> Advanced system settings -> Environment Variables. Add the variable as shown in the following image (variable value is the JDK installation path), then restart your computer for changes to take effect. + +![windows-java-home](./setup-native-development/windows-java-home.jpg) + +To verify Java environment configuration, execute the following command in Mac Terminal or Windows Command Prompt. Successful installation will display version information: + +``` +java -version +``` + +If your system uses JRE, install [JAVA SE Runtime Environment](http://www.oracle.com/technetwork/java/javase/downloads/index.html). + +> **Note**: [OpenJDK](https://openjdk.org/) and JDK differ only in open-source licenses, with no fundamental differences in functionality or configuration methods. + +## Download Android Studio + +Different versions of Cocos Creator engine typically support different Android Studio versions. Users can refer to the following version recommendations and download the corresponding IDE from [Android Studio Archive](https://developer.android.com/studio/archive?hl=zh-cn). + +| Cocos Creator Version Range | Recommended Android Studio Version | JDK Version | +| :-------------------------- | :--------------------------------- | :---------- | +| v2.4.12+ or v3.8.0+ | 2022.2.1 or 2022.3.1 | JDK 17 | +| v2.4.11- or v3.0.0-3.7.4 | 2020.3.1 or earlier versions | JDK 11 | + +## Download Android SDK and NDK + +After installing Android Studio, open SDK Manager following the official documentation: [SDK Manager Guide](https://developer.android.google.cn/studio/intro/update.html#sdk-manager). + +1. In SDK Platforms tab, select your desired API Levels. Recommended choices include mainstream API Level 26 (8.0), API Level 28 (9.0), etc. + * [Check Android versions corresponding to API Levels](https://developer.android.google.cn/tools/releases/platforms) + * [View minimum system versions supported by engine](../../advanced-topics/supported-versions.md) +2. Remember the **Android SDK Location** path shown in the window, which will be used for configuring SDK and NDK paths in Cocos Creator editor +3. In SDK Tools tab, check **Show Package Details** to display different tool versions before installation +4. Under **Android SDK Build-Tools**, select the latest build tools version +5. Check **Android SDK Platform-Tools** and **CMake**. For Android Support Library installation, refer to [Official Documentation - Support Library Setup](https://developer.android.google.cn/topic/libraries/support-library/setup) +6. Select desired **NDK** version. Recommended versions: **r21 ~ r23** to avoid compatibility issues. Users with Apple M-series chips are recommended to use the **r24** version. +7. Click **Apply** to install. You can also manage installed tools here later + + ![sdk manager](setup-native-development/sdk-manager.png) + +## Install Windows & Mac/iOS Development Environments + +- Windows requires [Visual Studio 2019/2022 Community Edition](https://www.visualstudio.com/downloads/download-visual-studio-vs). During installation, select **Desktop development with C++** and **Game development with C++** modules. +- After Visual Studio installation, you can build and publish Windows platform games. + + > **Important**: Do NOT check the **Cocos** option under **Game development with C++** module. + +- macOS requires Xcode 14 or later, [Download here](https://apps.apple.com/us/app/xcode/id497799835). +- After Xcode installation, you can build and publish Mac & iOS platform games. + +## Configure Android Build Environment Paths + +After setting up native build environment, configure development paths in Cocos Creator editor (skip if not building for Android).
+From main menu: **Cocos Creator -> Preferences**, open **Program Manager** page to configure: + +- **Android SDK**: Enter the `Android SDK Location` path (should contain `build-tools`, `platforms` folders) +- **Android NDK**: Find ndk folder under `Android SDK Location` path. For newer Android Studio versions, this folder contains multiple NDK versions. Enter your downloaded NDK version path in **Android NDK** field + + > **Note**: New Android Studio versions support multiple NDK versions in ndk folder, while older versions store single version in `ndk-bundle`. + + ![preference](setup-native-development/sdk.png) + +> **Note**: These configurations apply during native project building. If configuration fails, manually set these environment variables: `NDK_ROOT`, `ANDROID_SDK_ROOT`. + +## Troubleshooting Notes + +Based on public beta feedback, here are additional potential issues: + +1. Package Name Issues + + Verify **Game Package Name** in Build panel. Refer to [Build Options Documentation](./native-options.md#build-options) for platform-specific naming conventions. + +2. Android Runtime Error: `dlopen failed: cannot locate symbol "xxxx" referenced by "libcocos.so"...` + + Verify NDK and Android SDK versions match the target device's Android system architecture. Test with NDK/SDK versions specified in this documentation. + +3. JDK Version Compatibility + + When upgrading Android Studio and Gradle, ensure JDK version is upgraded to 17 or specified version to maintain compatibility. + +If build failures persist, please submit feedback via [Cocos Forum](https://discuss.cocos2d-x.org/c/33) including: +- Cocos Creator version +- Detailed reproduction steps +- Build log from Build panel +- Sample project reproducing the issue diff --git a/versions/4.0/en/editor/publish/setup-native-development/sdk-manager.png b/versions/4.0/en/editor/publish/setup-native-development/sdk-manager.png new file mode 100644 index 0000000000..4d36367000 Binary files /dev/null and b/versions/4.0/en/editor/publish/setup-native-development/sdk-manager.png differ diff --git a/versions/4.0/en/editor/publish/setup-native-development/sdk.png b/versions/4.0/en/editor/publish/setup-native-development/sdk.png new file mode 100644 index 0000000000..cf6e7a3b9d Binary files /dev/null and b/versions/4.0/en/editor/publish/setup-native-development/sdk.png differ diff --git a/versions/4.0/en/editor/publish/setup-native-development/windows-java-home.jpg b/versions/4.0/en/editor/publish/setup-native-development/windows-java-home.jpg new file mode 100644 index 0000000000..0cfdde10d4 Binary files /dev/null and b/versions/4.0/en/editor/publish/setup-native-development/windows-java-home.jpg differ diff --git a/versions/4.0/en/editor/publish/subpackage.md b/versions/4.0/en/editor/publish/subpackage.md new file mode 100644 index 0000000000..a7248474a6 --- /dev/null +++ b/versions/4.0/en/editor/publish/subpackage.md @@ -0,0 +1,85 @@ +# Mini Game Subpackage + +Some mini game platforms support subpackaging to divide resources, scripts and scenes. Including WeChat Mini Game, Baidu Mini Game, Douyin Mini Game, Huawei Quick Game, OPPO Mini Game and vivo Mini Game. + +Cocos Creator supports [Asset Bundle](../../asset/bundle.md) starting in v2.4, which allows developers to divide contents that need to be subpackaged into multiple Asset Bundles, and these Asset Bundles will be built into subpackages of the mini game. Only the necessary main packages will be downloaded when you startup the game, and these subpackages will not be loaded, but will be manually loaded by the developer during the game. This effectively reduces the time for the game startup. + +## Configuration + +The Asset Bundle is configured in **folders**. When we select a folder in the **Assets** panel, the **Inspector** panel will show a **Is Bundle** option, if set, the folder-related configuration options will appear: + +![subpackage configuration](subpackage/subpackage-config.png) + +In addition to the general [Asset Bundle Configuration](../../asset/bundle.md#configuration), the main settings to focus on for the mini game subpackage are: +- Set the **Target Platform** to the mini game platform that you want to subpackage, and set the **Compression Type** to the **Mini Game Subpackage**. +- The mini game subpackage can only be placed locally and cannot be configured as remote packages, so the **Is Remote Bundle** option cannot be checked. + +Once configured, click the **Check** button on the top right and the folder will be configured as a Asset Bundle. + +## Build + +When building, the **Main Bundle Compression Type** in the **Build** panel should be set to **Mini Game Subpackage**. For detailed instructions, please refer to the [Asset Bundle - Compression Type](../../asset/bundle.md#compression-type) documentation. + +After the project is built, this Asset Bundle folder is packaged into the `subpackages` folder in the release package directory of the mini game platform. Each folder contained in this folder is an Asset Bundle. + +**For example**, if the `assets/scene` folder in the Hello World project is configured as an Asset Bundle on the WeChat Mini Game, then after the project is built, a `scene` folder is generated in the `subpackages` folder in the release package directory, and the `scene` folder is an Asset Bundle. + +![subpackage](subpackage/subpackage.png) + +## WeChat Mini Games + +When building for the WeChat Mini Game, the configuration of the Asset Bundle will be automatically generated into the `game.json` configuration file of the WeChat Mini Games release package directory according to the rules. + +![profile](subpackage/profile.png) + +> **Note**: WeChat Mini Games require a specific version to support the Subpackage feature. WeChat 6.6.7 client, 2.1.0 and above base library support, please update to the latest client version. Developer tools please use version **1.02.1806120** and above. After updating the developer tools, don't forget to modify the version of **Details -> Local Settings -> Debug Base library** to 2.1.0 and above in the WeChat DevTools: +> +> ![devtools setting](./subpackage/devtools-setting.png) + +### Subpackage Load Packet Size Limit + +Currently, the size of the WeChat Mini Game subpackage has following restrictions: + +- The size of all subpackage of the entire Mini Game can not exceed **30M**. +- The size of a single subpackage is not limited. +- The main package size can not exceed **4M**. + +Please refer to the [WeChat Mini Game Subpackage Loading](https://developers.weixin.qq.com/minigame/en/dev/guide/base-ability/subPackage/useSubPackage.html) documentation for details. + +## ByteDance Mini Game + +When building for the ByteDance Mini Game, the configuration of the Asset Bundle will be automatically generated into the `game.json` configuration file of the ByteDance Mini Games release package directory according to the rules. + +![profile](subpackage/profile.png) + +For the specific property meanings in game.json, you can refer to the documentation:[ByteDane Mini Game Configurition](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/framework/mini-game-configuration/). + +### Subpackage Limit + +Currently, the size of the WeChat Mini Game subpackage has following restrictions: + +- The size of all subpackage of the entire Mini Game can not exceed 20MB​ +- The main package size can not exceed 4MB +- The size of a single subpackage can not exceed 20MB + +> The ByteDance Mini Game Open Data Context is similar to the subpackage, which can not exceed a size of 4MB. + +For more details, pelease refer to:[ByteDance Mini Game Subpackage](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/framework/subpackages/introduction). + +## vivo Mini Games + +When building for the vivo Mini Game, the configuration of the Asset Bundle will be automatically generated into the `manifest.json` configuration file in the `vivo-mini-game/src` directory of the vivo Mini Game release package according to the rules. + +![profile](./subpackage/vivo-profile.png) + +> **Notes**: +> 1. Starting with **1051** version, **Quick App & vivo Mini Game Debugger** supports the subpackage loading of vivo Mini Game. Versions lower than 1051 do not support subpackage loading, but they are also compatible. If a subpackage is configured in the editor's **Properties** panel, it will not affect the normal operation of the game. Please refer to the [vivo Mini Game Subpackage Loading -- Runtime Compatibility [cn]](https://minigame.vivo.com.cn/documents/#/lesson/base/subpackage?id=%e8%bf%90%e8%a1%8c%e6%97%b6%e5%85%bc%e5%ae%b9) documentation for details. +> 2. Unlike other mini game platforms, the Asset Bundle folder for the vivo Mini Game will be generated in the `src` directory of release package `vivo-mini-game` directory after the project is built. +> +> ![vivo-subpackages](./subpackage/vivo-subpackages.png) + +### Subpackage Load Packet Size Limit + +Currently, the size of the vivo Mini Game subpackage is limited to 20M (4M for main package and 16M for subpackages). + +Please refer to the [vivo Mini Game Subpackage Loading [cn]](https://minigame.vivo.com.cn/documents/#/lesson/base/subpackage) documentation for details. diff --git a/versions/4.0/en/editor/publish/subpackage/devtools-setting.png b/versions/4.0/en/editor/publish/subpackage/devtools-setting.png new file mode 100644 index 0000000000..cdc27d98d2 Binary files /dev/null and b/versions/4.0/en/editor/publish/subpackage/devtools-setting.png differ diff --git a/versions/4.0/en/editor/publish/subpackage/profile.png b/versions/4.0/en/editor/publish/subpackage/profile.png new file mode 100644 index 0000000000..5aea3b02d1 Binary files /dev/null and b/versions/4.0/en/editor/publish/subpackage/profile.png differ diff --git a/versions/4.0/en/editor/publish/subpackage/subpackage-config.png b/versions/4.0/en/editor/publish/subpackage/subpackage-config.png new file mode 100644 index 0000000000..d36609025a Binary files /dev/null and b/versions/4.0/en/editor/publish/subpackage/subpackage-config.png differ diff --git a/versions/4.0/en/editor/publish/subpackage/subpackage.png b/versions/4.0/en/editor/publish/subpackage/subpackage.png new file mode 100644 index 0000000000..f86af19eda Binary files /dev/null and b/versions/4.0/en/editor/publish/subpackage/subpackage.png differ diff --git a/versions/4.0/en/editor/publish/subpackage/vivo-profile.png b/versions/4.0/en/editor/publish/subpackage/vivo-profile.png new file mode 100644 index 0000000000..58b5b92a34 Binary files /dev/null and b/versions/4.0/en/editor/publish/subpackage/vivo-profile.png differ diff --git a/versions/4.0/en/editor/publish/subpackage/vivo-subpackages.png b/versions/4.0/en/editor/publish/subpackage/vivo-subpackages.png new file mode 100644 index 0000000000..effe4ea371 Binary files /dev/null and b/versions/4.0/en/editor/publish/subpackage/vivo-subpackages.png differ diff --git a/versions/4.0/en/editor/publish/vivo-mini-game/build_options.jpg b/versions/4.0/en/editor/publish/vivo-mini-game/build_options.jpg new file mode 100644 index 0000000000..5078eebf95 Binary files /dev/null and b/versions/4.0/en/editor/publish/vivo-mini-game/build_options.jpg differ diff --git a/versions/4.0/en/editor/publish/vivo-mini-game/play.jpg b/versions/4.0/en/editor/publish/vivo-mini-game/play.jpg new file mode 100644 index 0000000000..2bf3a964cb Binary files /dev/null and b/versions/4.0/en/editor/publish/vivo-mini-game/play.jpg differ diff --git a/versions/4.0/en/editor/publish/vivo-mini-game/rpk.png b/versions/4.0/en/editor/publish/vivo-mini-game/rpk.png new file mode 100644 index 0000000000..98aafe1ba8 Binary files /dev/null and b/versions/4.0/en/editor/publish/vivo-mini-game/rpk.png differ diff --git a/versions/4.0/en/editor/publish/vivo-mini-game/vivo-instant_native_install.jpg b/versions/4.0/en/editor/publish/vivo-mini-game/vivo-instant_native_install.jpg new file mode 100644 index 0000000000..f0a0175bdb Binary files /dev/null and b/versions/4.0/en/editor/publish/vivo-mini-game/vivo-instant_native_install.jpg differ diff --git a/versions/4.0/en/editor/publish/vivo-mini-game/vivo-instant_scan_install.jpg b/versions/4.0/en/editor/publish/vivo-mini-game/vivo-instant_scan_install.jpg new file mode 100644 index 0000000000..729e92eb9c Binary files /dev/null and b/versions/4.0/en/editor/publish/vivo-mini-game/vivo-instant_scan_install.jpg differ diff --git a/versions/4.0/en/editor/publish/vivo-mini-game/vivo_options.jpg b/versions/4.0/en/editor/publish/vivo-mini-game/vivo_options.jpg new file mode 100644 index 0000000000..84728fcacf Binary files /dev/null and b/versions/4.0/en/editor/publish/vivo-mini-game/vivo_options.jpg differ diff --git a/versions/4.0/en/editor/publish/wechatgame-plugin.md b/versions/4.0/en/editor/publish/wechatgame-plugin.md new file mode 100644 index 0000000000..8cb8df12c8 --- /dev/null +++ b/versions/4.0/en/editor/publish/wechatgame-plugin.md @@ -0,0 +1,39 @@ +# WeChat Mini Games Engine Plugin Instructions + +> **Note**: some platforms only have Chinese documentation available when visiting the platform's website. It may be necessary to use Google Translate in-order to review the documentation. + +The **Game Engine Plugin** is a new feature added to **WeChat v7.0.7**, which has the official version of the __Cocos Creator__ engine built in. If the plugin is enabled in the first game the player experiences, all games that also have the plugin enabled do not need to download the __Cocos Creator__ engine again, just use the same version of the engine directly from the public plugin library, or incremental update the engine. + +For example, when a player has played an A game developed using __Cocos Creator__ v2.2.0, and the A game already enabled this plugin. Then he played the B Game, also developed by v2.2.0, and would not have needed to re-download the __Cocos Creator__ engine if the B game had also enabled this plugin. Even if the B Game is developed using __Cocos Creator__ v2.2.1, WeChat only needs to incremental update the difference between the two engine versions. This will drastically reduce the download counts of mini games, and improve the startup speed of mini games by 0.5~2s for a better user experience. + +## How to use + +Simply check the **Separate Engine** option in the **Build** panel, and then build and release as normal, without additional manual operation. (This feature is only available when the built-in engine is used and the build is in non-debug mode.) + +![build-options](./wechatgame-plugin/build-options.png) + +## FAQ + +__Q:__ Does the engine plugin feature support engine customization?
+__A:__ Not supported. If the version does not match or the engine customization is enabled during the build, the built package will not actually use the engine plugin feature properly, although the editor will continue to build after an error occurs. + +__Q:__ The project enable the engine module clipping, should I need to disable it when using the engine plugin?
+__A:__ No, the project can continue to use the engine module clipping as before. The engine plugin provides a complete engine that is compatible with all clipping settings without affecting the original project package. + +__Q:__ After the engine plugin is enabled, will the engine code still be counted into the first package?
+__A:__ According to WeChat's rules, it will still be counted. + +__Q:__ After the engine plugin is enabled, can I remove all modules in **Project Setting -> Modules** of editor to reduce the package size?
+__A:__ No, because WeChat only supports engine plugin since v7.0.7, if the engine is clipped randomly, the game may not be able to run on a lower version of WeChat. + +__Q:__ When the engine plugin is enabled, prompt "Code package unpacking failed" or "Login user is not the developer of the Mini Program" in the WeChat DevTools, while the physical device previews correctly?
+__A:__ The default appid in the **Build** panel is a common test id, and according to WeChat's rules, you need to fill in the **appid** applied for yourself to test the engine plugin. + +__Q:__ When the engine plugin is enabled, prompt "Unauthorized plugin, `Add plugin`" in the WeChat DevTools?
+__A:__ Click the `Add plugin` in the prompt, then select add **CocosCreator** plugin and recompile. If prompt "There are no plugins to add" when you add the plugin, you can select the **Clear Cache -> Clear All** option in the WeChat DevTools and try again. + +## Reference documentation + +> **Note**: some platforms only have Chinese documentation available when visiting the platforms website. It may be necessary to use Google Translate in-order to review the documentation. + +- [WeChat Mini Games Engine Plugin Development Documentation](https://developers.weixin.qq.com/minigame/dev/guide/base-ability/game-engine-plugin.html) diff --git a/versions/4.0/en/editor/publish/wechatgame-plugin/build-options.png b/versions/4.0/en/editor/publish/wechatgame-plugin/build-options.png new file mode 100644 index 0000000000..d80679187a Binary files /dev/null and b/versions/4.0/en/editor/publish/wechatgame-plugin/build-options.png differ diff --git a/versions/4.0/en/editor/publish/windows/build-example-windows.md b/versions/4.0/en/editor/publish/windows/build-example-windows.md new file mode 100644 index 0000000000..08d20a3a48 --- /dev/null +++ b/versions/4.0/en/editor/publish/windows/build-example-windows.md @@ -0,0 +1,93 @@ +# Windows Publishing Example + +This guide demonstrates how to publish a project created with Cocos Creator as a Windows application. Before proceeding, make sure you have the following prerequisites: + +- A Windows computer +- C++ development environment + +## Installing C++ Development Environment + +First, you need to install [Visual Studio 2019/2022](https://www.visualstudio.com/downloads/download-visual-studio-vs). + +During the installation of Visual Studio, make sure to select the modules `Desktop development with C++` and `Game development with C++`. + +> **Note**: Within the Game development with C++ module, there is an option for **Cocos**. Please **do not** select this option. + +## Publishing Process + +### Prepare a Project for Testing + +Open an existing project or create a new test project. + +### Build + +![project-build-menu](./images/project-build-menu.png) + +As shown in the above image, select **Project -> Build** from the top menu of Cocos Creator to open the **Build** panel. + +![build-panel-windows](./images/build-panel-windows.png) + +#### General Options + +In the left section of the image above, you can find the common parameters that need to be configured for all platforms supported by Cocos Creator. For more details, refer to [General Build Options](./../build-options.md). + +#### Windows-specific Options + +Select the platform as **Windows**, and scroll down the panel to see the native and Windows-specific options as shown in the image on the right. For more information, refer to [General Native Build Options](./../native-options.md) and [Build Options - Windows](./build-options-windows.md). + +#### Start to Build + +Once the configuration is complete, click the **Build** button to generate the Visual Studio `*.sln` project. + +After a successful build, you can click the open file button as shown below to open the generated project folder. + +![build-open-path-windows](./images/build-open-path-windows.png) + +If you haven't changed the build path, you can find the `build/windows/proj` directory located in the project root, which contains the following files: + +![project-folder-windows](./images/project-folder-windows.png) + +### Compile and Run in Visual Studio + +Double-click on `build/windows/proj/.sln` to open the project with Visual Studio. + +![vs-showcase-windows](./images/vs-showcase-windows.png) + +As shown in the image above, the project includes the `cocos-engine` library and **project code**. Click the compile and run button to start the project. + +![run-windows](./images/run-windows.png) + +> `*.sln` is a Visual Studio project solution file, and it is recommended to open it with Visual Studio. You can also use IDEs such as **Rider** that support `*.sln` files. + +### Modifying the Resolution + +You can modify the resolution in `native/engine/common/classes/Game.cpp` using the `_windowInfo` property. + +The default resolution is set to **800 x 600**. Let's take an example of changing it to **800 x 400**. + +```C++ +int Game::init() { + _windowInfo.title = GAME_NAME; + // configure window size + _windowInfo.width = 800; + _windowInfo.height = 400; +} +``` + +After making the modification, compile and run again in Visual Studio. The effect will be as shown in the image below: + +![run-windows-800to400](./images/run-windows-800to400.png) + +## Read More + +### Script-Native Communication + +Sometimes, projects need to call Windows system functions or, when integrating a third-party SDK, use script code to call its APIs. In such cases, the communication mechanism between the script layer and the native layer is required. + +For information on the JS script and C++ communication mechanism, please refer to the [Tutorial: JSB 2.0](../../../advanced-topics/JSB2.0-learning.md)。 + +### Debugging JavaScript on Native Platforms + +Some issues only occur on specific devices and environments. If you can debug the code on the corresponding situation, you can quickly identify the problem and find a solution. + +Cocos Creator provides a native debugging mechanism that allows you to easily debug game code on a device. For more details, please refer to [Debugging JavaScript on Native Platforms](./../debug-jsb.md). diff --git a/versions/4.0/en/editor/publish/windows/build-options-windows.md b/versions/4.0/en/editor/publish/windows/build-options-windows.md new file mode 100644 index 0000000000..1304e7d296 --- /dev/null +++ b/versions/4.0/en/editor/publish/windows/build-options-windows.md @@ -0,0 +1,17 @@ +# Build Options - Windows + +The build options for the Windows platform include the **Render BackEnd** and **Target Platform**. + +![build-options-windows](./images/build-options-windows.png) + +## Executable Name + +This a field used to specify the name of the main executable file of an application. If not provided, the system will generate a default value based on the app name field. The value for this field must adhere to a specific format, containing only numbers, letters, underscores (_) and hyphens (-). + +## Render BackEnd + +On the Windows platform, Cocos Creator currently supports three render backends: **VULKAN**, **GLES3**, and **GLES2**. By default, **GLES3** is selected. If multiple options are selected, the runtime will choose the appropriate render backend based on the device's capabilities. + +## Target Platform + +Set the compilation architecture, currently only supporting **x64**, which means the application can only run on **x64** systems. diff --git a/versions/4.0/en/editor/publish/windows/images/build-open-path-windows.png b/versions/4.0/en/editor/publish/windows/images/build-open-path-windows.png new file mode 100644 index 0000000000..519ddb02a4 Binary files /dev/null and b/versions/4.0/en/editor/publish/windows/images/build-open-path-windows.png differ diff --git a/versions/4.0/en/editor/publish/windows/images/build-options-windows.png b/versions/4.0/en/editor/publish/windows/images/build-options-windows.png new file mode 100644 index 0000000000..06253e18ed Binary files /dev/null and b/versions/4.0/en/editor/publish/windows/images/build-options-windows.png differ diff --git a/versions/4.0/en/editor/publish/windows/images/build-panel-windows.png b/versions/4.0/en/editor/publish/windows/images/build-panel-windows.png new file mode 100644 index 0000000000..aa63b2eda9 Binary files /dev/null and b/versions/4.0/en/editor/publish/windows/images/build-panel-windows.png differ diff --git a/versions/4.0/en/editor/publish/windows/images/project-build-menu.png b/versions/4.0/en/editor/publish/windows/images/project-build-menu.png new file mode 100644 index 0000000000..225f5c1c1a Binary files /dev/null and b/versions/4.0/en/editor/publish/windows/images/project-build-menu.png differ diff --git a/versions/4.0/en/editor/publish/windows/images/project-folder-windows.png b/versions/4.0/en/editor/publish/windows/images/project-folder-windows.png new file mode 100644 index 0000000000..d1cdaa85f2 Binary files /dev/null and b/versions/4.0/en/editor/publish/windows/images/project-folder-windows.png differ diff --git a/versions/4.0/en/editor/publish/windows/images/run-windows-800to400.png b/versions/4.0/en/editor/publish/windows/images/run-windows-800to400.png new file mode 100644 index 0000000000..352915cb54 Binary files /dev/null and b/versions/4.0/en/editor/publish/windows/images/run-windows-800to400.png differ diff --git a/versions/4.0/en/editor/publish/windows/images/run-windows.png b/versions/4.0/en/editor/publish/windows/images/run-windows.png new file mode 100644 index 0000000000..acb3fc3312 Binary files /dev/null and b/versions/4.0/en/editor/publish/windows/images/run-windows.png differ diff --git a/versions/4.0/en/editor/publish/windows/images/vs-showcase-windows.png b/versions/4.0/en/editor/publish/windows/images/vs-showcase-windows.png new file mode 100644 index 0000000000..55f9fe6850 Binary files /dev/null and b/versions/4.0/en/editor/publish/windows/images/vs-showcase-windows.png differ diff --git a/versions/4.0/en/editor/publish/windows/index.md b/versions/4.0/en/editor/publish/windows/index.md new file mode 100644 index 0000000000..721f66b6d3 --- /dev/null +++ b/versions/4.0/en/editor/publish/windows/index.md @@ -0,0 +1,18 @@ +# Publishing Windows Desktop Apps + +Cocos Creator supports publishing Windows desktop applications, but it requires at least one device that supports Windows. + +## Content + +- [Windows Publishing Example](./build-example-windows.md) +- [Build Options - Windows](build-options-windows.md) + +## Read More + +- [Setting up Native Development Environment](../setup-native-development.md) +- [Build Panel](../build-panel.md) +- [General Build Panel](../build-options.md) +- [General Native Build Options](../native-options.md) +- [Debugging JavaScript on Native Platforms](../debug-jsb.md) +- [Build Process and FAQ](../build-guide.md) +- [Native Platform Secondary Development Guide](../../../advanced-topics/native-secondary-development.md) diff --git a/versions/4.0/en/editor/rendering/lod.md b/versions/4.0/en/editor/rendering/lod.md new file mode 100644 index 0000000000..9b9182da93 --- /dev/null +++ b/versions/4.0/en/editor/rendering/lod.md @@ -0,0 +1,139 @@ +# Level Of Details + +Level Of Details (LOD) is a common optimization method used in large-scene development, where the core of LOD is to reduce the display details of distant or unimportant objects to improve rendering efficiency. + +The common practice of LOD is to use low-poly for some objects that are far away from the screen or unimportant instead. + +To enable LOD in the engine, select the **Add Component** button in the **Inspector** panel and select the **LOD Group** component. + +![add lod group](lod/add-comp.png) + +When added, the words **Culled** or **LOD** will be displayed in the scene. + +![enabled](lod/enabled.png) + +## Properties + +![property](lod/property.png) + +The properties of the LOD Group component are described as follows. + +| Properties | Description | +| :-- | :-- | +| **Recalculate Bounds** | Recalculate Bounds
When this button is clicked, the model bounds for the entire group are recalculated
This button calls the `recalculateBounds` interface to recalculate the bounds, which will also effects the **Object Size** property | +| **Object Size** | Bounding box size
Calculate the bounding box of the object and take the axis with the largest value in X, Y, Z
Clicking the **Reset Object Size** button on the right will reset this property to 1 +| **LOD(0 ~ 3)** | LOD. Click the **>** symbol on the left side to expand for configuration, please refer to the figure below for the expanded content.
LOD0 indicates the richest display detail
LOD2 shows low-poly
When the object's screen ratio is less than the **Screen Ratio** value of LOD2, the object will be culled | + +Click on any **> LOD(0 ~ 3)** on the **Inspector** panel to expand it and the following image will be displayed. + +![lod0](lod/lod0.png) + +Its properties and description are as follows. + +| Properties | Description | +| :-- | :-- | +| **Screen Ratio(%)** | Screen Ratio [0, 1]。
Developers can enter a custom screen ratio in the back input box, or the screen ratio can be calculated automatically with **Apply Current Screen Ratio**
When the screen ratio is lower than the currently configured value, LOD will automatically switch to the next level; if there are no more levels, the object will be culled | +| **Apply Current Screen Ratio** | Applying the Current Screen Ratio
When pressed, the LOD Group component takes the screen ratio of the current display state as the value of **Screen Ratio (%)** | +| **Mesh Renderers** | Mesh Renderer
Please refer to the documentation below for more detailed information | +| **0 Triangle - 0 Sub Mesh(es)** | This property shows the number of triangulated surfaces configured at the current level and the number of their submeshes + +### Mesh Renderer Properties + +The mesh renderer can be changed by clicking on the ![+](lod/+.png) and ![-](lod/-.png) buttons below to add or remove actions. + +Once added, the node with the mesh renderer within the hierarchy manager can be configured by dragging and dropping the following. + +![drag mesh](load/../lod/drag-mesh.gif) + +## LOD + +Mouse over the right side of the LOD to display the mesh information to show the Add or Remove Levels button. + +![add-remove-level](lod/add-remove-level.png) + +Clicking on ![+](lod/+.png) will add a new LOD to the end of the array. + +Clicking on ![-](lod/-.png) will delete the LOD currently pointed to by the mouse; after deletion, subsequent LODs are automatically shifted forward. + +## Preview + +Once you have configured the LOD, you can preview the different levels with the mouse wheel in the scene manager. + +![preview](lod/preview.gif) + +As the mouse scrolling changes, the scene camera will gradually pull away, and the level of the LOD will change, if the screen ratio of the current object is lower than the configured screen ratio of LOD2, the scene management system will cull it. The screen will show the current LOD Group's hierarchy status LOD(0~3) or Culled. + +## Use of scripts + +The developer can obtain the component via the `getComponent` method. + +```ts +import { _decorator, Component, Node, LODGroup } from 'cc'; +const { ccclass, property, requireComponent } = _decorator; + +@ccclass('LOD') +@requireComponent(LODGroup) +export class LOD extends Component { + + lodGroup : LODGroup | null = null; + + start() { + this.lodGroup = this.node.getComponent(LODGroup); + } + + update(deltaTime: number) { + + } +} +``` + +API reference could be found at [LOD Group API](%__APIDOC__%/en/class/LODGroup) + +## Art Workflow + +In real project development, artists can perform faceting operations in DCC software (Maya, 3D Max, Blender) and export the model. + +It is often possible to export multiple models at different levels or add different sub-meshes to the same model to represent different LOD levels. + +![export](lod/dcc-export.png) + +### Automatic Matching + +The LOD Group component automatically matches child meshes or mesh renderers of models exported via DCC software that have multiple child nodes ending in **_lod+[0,N]**. + +#### Example of automatic matching + +Make sure you have exported the appropriate model according to the rules above. + +Import the prepared FBX or glTF file into the engine's **Asset Manager** at + +![import-asset](lod/import-asset.png) + +In this case, the asset import system automatically recognizes these nodes and enables LOD Groups for automatic matching. + +![auto-match](lod/auto-match.png) + +#### Auto LOD + +Since v3.8, we have optimized the LOD when importing models, if the model does not have LOD information, you can find the Model pagination on the **Inspector** panel of the model, find and edit the LOD of the model, check the LOD check box to enable auto LOD. + +![model.png](./lod/model.png) + +Mouse over the hierarchy to click ![-](./lod/+.png)/![-](./lod/-.png) to increase and decrease the level. + +![hover.png](./lod/hover.png) + +- Face count(%): the face count ratio, the ratio of the number of triangular faces of the optimized mesh divided by the number of faces of the original mesh. For example, 25% means 75% original faces will be optimized. +- Screen Ratio: the minimum screen ratio, same as the Screen Ratio property of the LOD component, see above. + +Each tier is set up similarly to the LOD component, allowing you to modify the percentage of faces in different tiers. + +## Q&A + +- Can I add more than one LOD Group? + + Yes. They will work well with each other. + +- Can I use LODs within a 2D scene? + + Since LOD Group components only recognize MeshRenderers and 2D or UI components usually don't have MeshRenderers, it doesn't make much sense to do so. diff --git a/versions/4.0/en/editor/rendering/lod/+.png b/versions/4.0/en/editor/rendering/lod/+.png new file mode 100644 index 0000000000..eb5321a9ba Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/+.png differ diff --git a/versions/4.0/en/editor/rendering/lod/-.png b/versions/4.0/en/editor/rendering/lod/-.png new file mode 100644 index 0000000000..197fef21f9 Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/-.png differ diff --git a/versions/4.0/en/editor/rendering/lod/add-comp.png b/versions/4.0/en/editor/rendering/lod/add-comp.png new file mode 100644 index 0000000000..564267d0ad Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/add-comp.png differ diff --git a/versions/4.0/en/editor/rendering/lod/add-remove-level.png b/versions/4.0/en/editor/rendering/lod/add-remove-level.png new file mode 100644 index 0000000000..bc75de91e0 Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/add-remove-level.png differ diff --git a/versions/4.0/en/editor/rendering/lod/auto-match.png b/versions/4.0/en/editor/rendering/lod/auto-match.png new file mode 100644 index 0000000000..2fc09c02c8 Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/auto-match.png differ diff --git a/versions/4.0/en/editor/rendering/lod/dcc-export.png b/versions/4.0/en/editor/rendering/lod/dcc-export.png new file mode 100644 index 0000000000..ab6024b2f2 Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/dcc-export.png differ diff --git a/versions/4.0/en/editor/rendering/lod/drag-mesh.gif b/versions/4.0/en/editor/rendering/lod/drag-mesh.gif new file mode 100644 index 0000000000..a49516d4ac Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/drag-mesh.gif differ diff --git a/versions/4.0/en/editor/rendering/lod/enabled.png b/versions/4.0/en/editor/rendering/lod/enabled.png new file mode 100644 index 0000000000..82515eaab9 Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/enabled.png differ diff --git a/versions/4.0/en/editor/rendering/lod/hover.png b/versions/4.0/en/editor/rendering/lod/hover.png new file mode 100644 index 0000000000..66272d83da Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/hover.png differ diff --git a/versions/4.0/en/editor/rendering/lod/import-asset.png b/versions/4.0/en/editor/rendering/lod/import-asset.png new file mode 100644 index 0000000000..bf1b14fbc0 Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/import-asset.png differ diff --git a/versions/4.0/en/editor/rendering/lod/lod0.png b/versions/4.0/en/editor/rendering/lod/lod0.png new file mode 100644 index 0000000000..135bb35848 Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/lod0.png differ diff --git a/versions/4.0/en/editor/rendering/lod/mesh-renderers.png b/versions/4.0/en/editor/rendering/lod/mesh-renderers.png new file mode 100644 index 0000000000..838b1ce296 Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/mesh-renderers.png differ diff --git a/versions/4.0/en/editor/rendering/lod/model.png b/versions/4.0/en/editor/rendering/lod/model.png new file mode 100644 index 0000000000..06e7a51b1b Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/model.png differ diff --git a/versions/4.0/en/editor/rendering/lod/preview.gif b/versions/4.0/en/editor/rendering/lod/preview.gif new file mode 100644 index 0000000000..054a1e8d7f Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/preview.gif differ diff --git a/versions/4.0/en/editor/rendering/lod/property.png b/versions/4.0/en/editor/rendering/lod/property.png new file mode 100644 index 0000000000..0a88e716af Binary files /dev/null and b/versions/4.0/en/editor/rendering/lod/property.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/2d.png b/versions/4.0/en/editor/scene/bar_img/2d.png new file mode 100644 index 0000000000..84cc636e1b Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/2d.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/3d.png b/versions/4.0/en/editor/scene/bar_img/3d.png new file mode 100644 index 0000000000..ee60de495a Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/3d.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/align-bar.png b/versions/4.0/en/editor/scene/bar_img/align-bar.png new file mode 100644 index 0000000000..66ccd1a4ff Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/align-bar.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/align.png b/versions/4.0/en/editor/scene/bar_img/align.png new file mode 100644 index 0000000000..b197746bfd Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/align.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/all-use-x.png b/versions/4.0/en/editor/scene/bar_img/all-use-x.png new file mode 100644 index 0000000000..46094417cf Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/all-use-x.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/anchor-or-center.png b/versions/4.0/en/editor/scene/bar_img/anchor-or-center.png new file mode 100644 index 0000000000..9903360e0e Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/anchor-or-center.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/center.png b/versions/4.0/en/editor/scene/bar_img/center.png new file mode 100644 index 0000000000..dd020cc8ec Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/center.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/desig-resolution.png b/versions/4.0/en/editor/scene/bar_img/desig-resolution.png new file mode 100644 index 0000000000..7ff551fe0e Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/desig-resolution.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/distribute.png b/versions/4.0/en/editor/scene/bar_img/distribute.png new file mode 100644 index 0000000000..3b3e9fed63 Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/distribute.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/increase-snap-config.png b/versions/4.0/en/editor/scene/bar_img/increase-snap-config.png new file mode 100644 index 0000000000..1b8ce6eb2f Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/increase-snap-config.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/light.png b/versions/4.0/en/editor/scene/bar_img/light.png new file mode 100644 index 0000000000..b4185acb22 Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/light.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/local.png b/versions/4.0/en/editor/scene/bar_img/local.png new file mode 100644 index 0000000000..f7bb11eb98 Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/local.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/moveing.png b/versions/4.0/en/editor/scene/bar_img/moveing.png new file mode 100644 index 0000000000..02d58a2a35 Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/moveing.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/rect.png b/versions/4.0/en/editor/scene/bar_img/rect.png new file mode 100644 index 0000000000..b9ffec6a63 Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/rect.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/rotating.png b/versions/4.0/en/editor/scene/bar_img/rotating.png new file mode 100644 index 0000000000..0d12b188a9 Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/rotating.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/rotation.png b/versions/4.0/en/editor/scene/bar_img/rotation.png new file mode 100644 index 0000000000..bfb625cf85 Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/rotation.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/scale.png b/versions/4.0/en/editor/scene/bar_img/scale.png new file mode 100644 index 0000000000..779187653b Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/scale.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/scaling.png b/versions/4.0/en/editor/scene/bar_img/scaling.png new file mode 100644 index 0000000000..658e8f9d7b Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/scaling.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/shaded.png b/versions/4.0/en/editor/scene/bar_img/shaded.png new file mode 100644 index 0000000000..66248a09fb Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/shaded.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/translate.png b/versions/4.0/en/editor/scene/bar_img/translate.png new file mode 100644 index 0000000000..d20ca60753 Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/translate.png differ diff --git a/versions/4.0/en/editor/scene/bar_img/world-or-local.png b/versions/4.0/en/editor/scene/bar_img/world-or-local.png new file mode 100644 index 0000000000..6ebe2b1952 Binary files /dev/null and b/versions/4.0/en/editor/scene/bar_img/world-or-local.png differ diff --git a/versions/4.0/en/editor/scene/camera-gizmo.md b/versions/4.0/en/editor/scene/camera-gizmo.md new file mode 100644 index 0000000000..dff9d3c13f --- /dev/null +++ b/versions/4.0/en/editor/scene/camera-gizmo.md @@ -0,0 +1,15 @@ +# Camera Gizmo + +**Camera Gizmo** is used to show the clip area of a camera, you can read the [Camera Introduction](../components/camera-component.md) documentation for more information about camera. + +## Perspective Camera Gizmo + +**Perspective Camera Gizmo** shows the shape of frustum, which is calculate by the distance of the near clip plane, far clip plane and fov. You can edit them by dragging the control quad. + +![camera perspective gizmo](images/camera-perspective-gizmo.png) + +## Ortho Camera Gizmo + +**Ortho Camera Gizmo** shows the shape of box, which is calculated by the distance of the near clip plane, far clip plane and the height of ortho camera. You can edit them by dragging the control quad. + +![camera ortho gizmo](images/camera-ortho-gizmo.png) diff --git a/versions/4.0/en/editor/scene/collider-gizmo.md b/versions/4.0/en/editor/scene/collider-gizmo.md new file mode 100644 index 0000000000..d90b3939cf --- /dev/null +++ b/versions/4.0/en/editor/scene/collider-gizmo.md @@ -0,0 +1,11 @@ +# Collider Gizmo + +Information about colliders for Cocos Creator can be found at: [Colliders](../../physics/physics-collider.md) + +The collider gizmo is used to display the position and size of the collider in the editor. Select any node with a collider component in the **Hierarchy** panel to see its gizmo in the scene window: + +![box collider gizmo](images/box-collider-gizmo.png) + +For all colliders, the collider can be resized by selecting and dragging these squares with the mouse. The following figure shows how to resize colliders, using the **box collider gizmo** as an example. + +![edit](images/edit-collider.gif) diff --git a/versions/4.0/en/editor/scene/images/auto-snapping.gif b/versions/4.0/en/editor/scene/images/auto-snapping.gif new file mode 100644 index 0000000000..60bc7c28be Binary files /dev/null and b/versions/4.0/en/editor/scene/images/auto-snapping.gif differ diff --git a/versions/4.0/en/editor/scene/images/bar.png b/versions/4.0/en/editor/scene/images/bar.png new file mode 100644 index 0000000000..3b089be1ef Binary files /dev/null and b/versions/4.0/en/editor/scene/images/bar.png differ diff --git a/versions/4.0/en/editor/scene/images/bar2d.png b/versions/4.0/en/editor/scene/images/bar2d.png new file mode 100644 index 0000000000..572360db6e Binary files /dev/null and b/versions/4.0/en/editor/scene/images/bar2d.png differ diff --git a/versions/4.0/en/editor/scene/images/box-collider-gizmo.png b/versions/4.0/en/editor/scene/images/box-collider-gizmo.png new file mode 100644 index 0000000000..f0e3fc021f Binary files /dev/null and b/versions/4.0/en/editor/scene/images/box-collider-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/camera-ortho-gizmo.png b/versions/4.0/en/editor/scene/images/camera-ortho-gizmo.png new file mode 100644 index 0000000000..7b45da27de Binary files /dev/null and b/versions/4.0/en/editor/scene/images/camera-ortho-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/camera-perspective-gizmo.png b/versions/4.0/en/editor/scene/images/camera-perspective-gizmo.png new file mode 100644 index 0000000000..3cfed133f5 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/camera-perspective-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/debug-preview.png b/versions/4.0/en/editor/scene/images/debug-preview.png new file mode 100644 index 0000000000..252f6f32a3 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/debug-preview.png differ diff --git a/versions/4.0/en/editor/scene/images/edit-collider.gif b/versions/4.0/en/editor/scene/images/edit-collider.gif new file mode 100644 index 0000000000..4bac5bb1fd Binary files /dev/null and b/versions/4.0/en/editor/scene/images/edit-collider.gif differ diff --git a/versions/4.0/en/editor/scene/images/edit-resolution.png b/versions/4.0/en/editor/scene/images/edit-resolution.png new file mode 100644 index 0000000000..21bfc1daca Binary files /dev/null and b/versions/4.0/en/editor/scene/images/edit-resolution.png differ diff --git a/versions/4.0/en/editor/scene/images/enable-snaping-rotating.gif b/versions/4.0/en/editor/scene/images/enable-snaping-rotating.gif new file mode 100644 index 0000000000..a4580d5321 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/enable-snaping-rotating.gif differ diff --git a/versions/4.0/en/editor/scene/images/global-rotation.png b/versions/4.0/en/editor/scene/images/global-rotation.png new file mode 100644 index 0000000000..576a99543d Binary files /dev/null and b/versions/4.0/en/editor/scene/images/global-rotation.png differ diff --git a/versions/4.0/en/editor/scene/images/increase-snap-conf.png b/versions/4.0/en/editor/scene/images/increase-snap-conf.png new file mode 100644 index 0000000000..bed3de572d Binary files /dev/null and b/versions/4.0/en/editor/scene/images/increase-snap-conf.png differ diff --git a/versions/4.0/en/editor/scene/images/light-off.png b/versions/4.0/en/editor/scene/images/light-off.png new file mode 100644 index 0000000000..e6e5f93211 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/light-off.png differ diff --git a/versions/4.0/en/editor/scene/images/light-on.png b/versions/4.0/en/editor/scene/images/light-on.png new file mode 100644 index 0000000000..f87cfb06e4 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/light-on.png differ diff --git a/versions/4.0/en/editor/scene/images/local-rotation.png b/versions/4.0/en/editor/scene/images/local-rotation.png new file mode 100644 index 0000000000..66d69c05f0 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/local-rotation.png differ diff --git a/versions/4.0/en/editor/scene/images/multiple-select.gif b/versions/4.0/en/editor/scene/images/multiple-select.gif new file mode 100644 index 0000000000..7f9642f752 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/multiple-select.gif differ diff --git a/versions/4.0/en/editor/scene/images/particle-box-gizmo.png b/versions/4.0/en/editor/scene/images/particle-box-gizmo.png new file mode 100644 index 0000000000..94832637c9 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/particle-box-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/particle-circle-gizmo.png b/versions/4.0/en/editor/scene/images/particle-circle-gizmo.png new file mode 100644 index 0000000000..3a39cd2ab0 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/particle-circle-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/particle-cone-gizmo.png b/versions/4.0/en/editor/scene/images/particle-cone-gizmo.png new file mode 100644 index 0000000000..86fa6f8650 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/particle-cone-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/particle-hemisphere-gizmo.png b/versions/4.0/en/editor/scene/images/particle-hemisphere-gizmo.png new file mode 100644 index 0000000000..bf106f3dd4 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/particle-hemisphere-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/particle-sphere-gizmo.png b/versions/4.0/en/editor/scene/images/particle-sphere-gizmo.png new file mode 100644 index 0000000000..7f86d8ed2e Binary files /dev/null and b/versions/4.0/en/editor/scene/images/particle-sphere-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/position-gizmo.png b/versions/4.0/en/editor/scene/images/position-gizmo.png new file mode 100644 index 0000000000..c9f0b9d98f Binary files /dev/null and b/versions/4.0/en/editor/scene/images/position-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/rect-gizmo.png b/versions/4.0/en/editor/scene/images/rect-gizmo.png new file mode 100644 index 0000000000..8b23b7c45f Binary files /dev/null and b/versions/4.0/en/editor/scene/images/rect-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/rect-tool-snap-config.png b/versions/4.0/en/editor/scene/images/rect-tool-snap-config.png new file mode 100644 index 0000000000..83d1192eae Binary files /dev/null and b/versions/4.0/en/editor/scene/images/rect-tool-snap-config.png differ diff --git a/versions/4.0/en/editor/scene/images/reference.png b/versions/4.0/en/editor/scene/images/reference.png new file mode 100644 index 0000000000..b0da445ca9 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/reference.png differ diff --git a/versions/4.0/en/editor/scene/images/remove-light.png b/versions/4.0/en/editor/scene/images/remove-light.png new file mode 100644 index 0000000000..e75dc8473c Binary files /dev/null and b/versions/4.0/en/editor/scene/images/remove-light.png differ diff --git a/versions/4.0/en/editor/scene/images/rotation-gizmo.png b/versions/4.0/en/editor/scene/images/rotation-gizmo.png new file mode 100644 index 0000000000..87ccdda345 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/rotation-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/rotation.png b/versions/4.0/en/editor/scene/images/rotation.png new file mode 100644 index 0000000000..7573ea4848 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/rotation.png differ diff --git a/versions/4.0/en/editor/scene/images/scale-gizmo.png b/versions/4.0/en/editor/scene/images/scale-gizmo.png new file mode 100644 index 0000000000..3e3086d9c0 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/scale-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/scene-camera.png b/versions/4.0/en/editor/scene/images/scene-camera.png new file mode 100644 index 0000000000..aec1780690 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/scene-camera.png differ diff --git a/versions/4.0/en/editor/scene/images/scene-gizmo.png b/versions/4.0/en/editor/scene/images/scene-gizmo.png new file mode 100644 index 0000000000..a02467b623 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/scene-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/scene-grid.png b/versions/4.0/en/editor/scene/images/scene-grid.png new file mode 100644 index 0000000000..96a5af68d9 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/scene-grid.png differ diff --git a/versions/4.0/en/editor/scene/images/scene-light.png b/versions/4.0/en/editor/scene/images/scene-light.png new file mode 100644 index 0000000000..c88b9f2b5b Binary files /dev/null and b/versions/4.0/en/editor/scene/images/scene-light.png differ diff --git a/versions/4.0/en/editor/scene/images/scene-reference.png b/versions/4.0/en/editor/scene/images/scene-reference.png new file mode 100644 index 0000000000..01d84a02bf Binary files /dev/null and b/versions/4.0/en/editor/scene/images/scene-reference.png differ diff --git a/versions/4.0/en/editor/scene/images/scene-resolution.png b/versions/4.0/en/editor/scene/images/scene-resolution.png new file mode 100644 index 0000000000..b09b353749 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/scene-resolution.png differ diff --git a/versions/4.0/en/editor/scene/images/scene.png b/versions/4.0/en/editor/scene/images/scene.png new file mode 100644 index 0000000000..46d8f00f7e Binary files /dev/null and b/versions/4.0/en/editor/scene/images/scene.png differ diff --git a/versions/4.0/en/editor/scene/images/set-camera.png b/versions/4.0/en/editor/scene/images/set-camera.png new file mode 100644 index 0000000000..9c1badaa89 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/set-camera.png differ diff --git a/versions/4.0/en/editor/scene/images/shaded-config.png b/versions/4.0/en/editor/scene/images/shaded-config.png new file mode 100644 index 0000000000..2d9d9abe61 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/shaded-config.png differ diff --git a/versions/4.0/en/editor/scene/images/shaded-debug1.png b/versions/4.0/en/editor/scene/images/shaded-debug1.png new file mode 100644 index 0000000000..4df41fbf90 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/shaded-debug1.png differ diff --git a/versions/4.0/en/editor/scene/images/shaded-debug2.png b/versions/4.0/en/editor/scene/images/shaded-debug2.png new file mode 100644 index 0000000000..752d63b8a4 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/shaded-debug2.png differ diff --git a/versions/4.0/en/editor/scene/images/shaded-debug3.png b/versions/4.0/en/editor/scene/images/shaded-debug3.png new file mode 100644 index 0000000000..e725095e97 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/shaded-debug3.png differ diff --git a/versions/4.0/en/editor/scene/images/snaping-surface.gif b/versions/4.0/en/editor/scene/images/snaping-surface.gif new file mode 100644 index 0000000000..4cc2d61a29 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/snaping-surface.gif differ diff --git a/versions/4.0/en/editor/scene/images/sphere-collider-gizmo.png b/versions/4.0/en/editor/scene/images/sphere-collider-gizmo.png new file mode 100644 index 0000000000..5c86b3a974 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/sphere-collider-gizmo.png differ diff --git a/versions/4.0/en/editor/scene/images/transform-bar.png b/versions/4.0/en/editor/scene/images/transform-bar.png new file mode 100644 index 0000000000..52ad594a66 Binary files /dev/null and b/versions/4.0/en/editor/scene/images/transform-bar.png differ diff --git a/versions/4.0/en/editor/scene/index.md b/versions/4.0/en/editor/scene/index.md new file mode 100644 index 0000000000..04e35727d3 --- /dev/null +++ b/versions/4.0/en/editor/scene/index.md @@ -0,0 +1,314 @@ +# Scene Panel + +![scene](images/scene.png) + +The **Scene** panel is the central work area for content creation, used for selecting and placing scene images, characters, effects, UI, and other game elements. This workspace allows to select and modify the position, rotation, and scaling of nodes with the **Transform Gizmos**, and get a WYSIWYG scene preview. + +## Menu Bar Introduction + +The menu bar at the top of the scene panel is slightly different after switching between 3D and 2D views, see below. + +- 3D: + + ![bar](images/bar.png) + +- 2D: + + ![bar](images/bar2d.png) + +## View Introduction + +The **Scene** panel includes two views, **3D** and **2D**. The 3D view is used for 3D scene editing, while the 2D view is mainly used for editing 2D elements such as UI nodes, etc. The scene view can be switched via the **3D/2D** button in the toolbar at the top left of the editor. + +- ![3d](bar_img/3d.png) Indicates that the current view is **3D** view mode, when the view's camera is of perspective type. Click on it to go to **2D** view. +- ![2d](bar_img/2d.png) Indicates that the current view mode is **2D**, and the view camera is of orthogonal type at this time, which will return to **3D** view when clicked again. + +### 3D view + +In the 3D view, you can move and position the view of the **Scene** panel by the following actions: +- Left mouse button + Alt: rotates the view centered on its center point. +- Middle mouse button: pans the view. +- Spacebar + mouse/touchpad drag: pans the view. +- Mouse wheel: zoom the view centered on the view center point. +- Right mouse button + WASD: camera roaming. +- **F** Shortcut: focus the camera on the currently selected node. + +### 2D view + +In 2D view, the view of the **Scene** panel can be moved and positioned by the following actions: +- Middle mouse button: pans the view. +- Mouse wheel: zooms the view centered on the current mouse hover position. +- Right mouse button: pans the view. +- **F** Shortcut: focus the camera on the currently selected node. + +## Transform Tools + +![transform](images/transform-bar.png) + +The above tools are the transform tools group, responsible for node **moving, scaling, rotating, attaching, anchoring/centering, local/world coordinate system**, etc. + +- ![3d](bar_img/translate.png) Switch the moving gizmo in the current scene to **Move Gizmo**, the shortcut key is W + + When selecting any node, notice a movement control handle consisting of red, green, and blue arrows and red, green, and blue squares in the center of the node in the **Scene** panel. + + The **control handles** are controllers that can be interacted with by the mouse in a particular editing state in the **Scene** panel. These controllers are only used to assist in editing and will not be displayed while the game is running. + + ![position gizmo](./images/position-gizmo.png) + + When the Move Gizmo is active: + - Holding the red/green/blue arrows and dragging the mouse will move the node in the X, Y and Z directions respectively. + - Holding the red/green/blue square and dragging the mouse will move the node in the Y-Z plane, X-Z plane, X-Y plane, respectively. + +- ![3d](bar_img/rotation.png) Switch the gizmo tool in the current scene to **Rotate Gizmo**, the shortcut key is E + + The handle of the Rotate Gizmo consists of three orthogonal red, green and blue circles (in 2D view it consists of an arrow and a circle). When you drag the mouse on any of the red/green/blue circles, the nodes will rotate around the X, Y and Z axes respectively. + + ![rotation gizmo](./images/rotation-gizmo.png) + + When the mouse hovers over any circle, the circle will be displayed in yellow, click on it to select it, and a yellow arrow will be displayed to indicate which axis the current node is rotated by. Drag any point on the circle to rotate the node, and before releasing the mouse, notice the rotation angle on the control handle. + + ![rotation](./images/rotation.png) + +- ![3d](bar_img/scale.png) Switch the transform gizmo in the current scene to **Scale Gizmo**, the shortcut key is R + + The scale gizmo consists of three axes with red, green and blue squares at the head and a gray square at the center. When the mouse is hovered over any of the squares, it appears yellow and can be selected and dragged by clicking on. + + - Holding the red/green/blue square and dragging the mouse will scale the node in the X, Y and Z axis directions respectively. + - Dragging the mouse on a gray square will scale the node in X, Y, and Z directions at the same time. + + ![scale gizmo](./images/scale-gizmo.png) + +- ![3d](bar_img/rect.png) Switch the transform tool in the current scene to **Rect Gizmo** with the shortcut key T. + + The Rect Gizmo consists of four vertex control points, four edge control points, and one center control point which is only valid for UI Components. + + ![rect gizmo](./images/rect-gizmo.png) + + When the Rect Gizmo is active: + - Dragging either vertex control point of the control handle can modify both the `Position` property of the UI node and the `ContentSize` property in the UITransform component while keeping the position of the diagonal vertices unchanged. + - Dragging either side of the control handle modifies both the `Position` (`X` or `Y` property) of the UI node and the `ContentSize` property (`width` or `height` property) of the UITransform component, while keeping the position of the opposite side unchanged. + - Dragging the center control point of a control handle allows to modify both the `Position` property of the UI node and the `AnchorPoint` property of the UITransform component while keeping the size of the UI node unchanged. + + In the layout of UI elements, it is often necessary to use the **Rect Gizmo** to directly control the position and length of the node's four edges precisely. For image elements where the original image aspect ratio must be maintained, the Rect Gizmo is not usually used to resize. + +- ![3d](bar_img/increase-snap-config.png) **Incremental snap tool**, click this button and **Incremental snap configuration** will appear, please refer to **Incrase Snap Configuration** section below for details. +- ![3d](bar_img/anchor-or-center.png) The transform gizmo control point is used to set the position of the gizmo and to control the orientation of the handles. + + - ![3d](bar_img/anchor-or-center.png) Pivot: the transform gizmo will be displayed in the location of the 2D object **AnchorPoint** or in the 3D object's **world coordinate system**. + - ![anchor](bar_img/center.png) Center: the transform gizmo will be displayed at the center of the node. If multiple nodes are selected at the same time, the center of all nodes will be displayed. +- ![3d](bar_img/world-or-local.png) Toggle transformation tool gizmo is **local** coordinate system or **world** coordinate system. + + - ![local](bar_img/local.png) Local: the control handle of the transform gizmo is based on the direction of rotation of the node, as follows: + + ![local-rotation](./images/local-rotation.png) + + - ![global](bar_img/world-or-local.png) Global: the direction of the control handles in the transform gizmo is based on the world coordinate system, and is not affected by the node rotation, as follows: + + ![global-rotation](./images/global-rotation.png) + +## Increment Snap Configuration + +The incremental snap tool mainly consists of **Increment Snap Configuration** and **Rect Tool Snap Configuration**. + +### Increment Snap Configuration + +The increment snapping feature can be used to manipulate nodes in a set step size when using the move/rotate/scale transform tools in the scene editor. The increment snapping feature can be triggered in the following two ways: + +1. Press and hold the Ctrl/Command key while using transform tools to trigger the increment snapping feature. +2. In the increment snapping configuration panel, use the button to enable the automatic snapping feature of corresponding transform tools. See below for details. + +Click on the fifth **Increment Snapping configuration** button in the toolbar in the upper left corner of the editor's main window, the increment snapping configuration panel will pop up, which can be used to set the step size of corresponding transform tools, and enable the auto snapping feature: + +![increment snap config](images/increase-snap-conf.png) + +| Button | Description | +| :-- | :-- | +| ![position snap](bar_img/moveing.png)| This button is used to set whether to enable the automatic snapping when using the **Move Transform Tool**. X, Y, and Z are used to set the move steps on the X, Y, and Z axes respectively, and the default X, Y, and Z values are the same for X, or you can click the ![position snap](bar_img/moveing.png) button to set the step size for each axis separately. | +| ![rotation snap](bar_img/rotating.png)| This button is used to set whether to enable automatic snapping when using the **Rotation Transform Tool**. The box on the right side is used to set the rotation step, the default is 1. | +| ![scale snap](bar_img/scaling.png)| This button is used to set whether to enable automatic snapping when using the **Scale Transform Tool**. The box on the right side is used to set the rotation step, the default is 1.
Please note that the scale snap is a factor not an absolute value | + +![snap rotating](images/enable-snaping-rotating.gif) + +### Rect Tool Snap Configuration + +![rect tool config](images/rect-tool-snap-config.png) + +When using the **Rect Tool Snap Configuration**, the Smart Alignment feature is enabled by default. When dragging UI elements around the scene and encountering elements that can be aligned, the alignment reference line will be displayed and automatically attached to the reference line position. + +![auto snapping](images/auto-snapping.gif) + +| Option | Description | +| :-- | :-- | +| Enable auto snap | Check this option to enable the Smart Alignment feature, which is enabled by default. | +| Snap threshold | Set the threshold value for adsorption detection (world space unit). | + +## Alignment and Distribution + +This tool is unique to the 2D view and is used for 2D/UI multi-node alignment and distribution functions. + +![align](bar_img/align-bar.png) + +- Alignment: Align selected nodes, at least two nodes need to be selected for the alignment to take effect + + ![align](bar_img/align.png) + + In order from left to right, the menus are as follows: + + - Align Top + - Align Vertical Center + - Align Bottom + - Align Left + - Align Horizontal Center + - Align Right + + The handling process of alignment is as follows: After selecting one or more nodes, the AABB for all selected nodes will be calculated via current **un-parent** nodes(This means if there is a parent-child relationship in the selected nodes, then only the parent node will be taken into consideration), and when clicking the Align Bottom button, all nodes will be put at the bottom of the AABB, and when clicking the Align Left button, all nodes will be put to the left side of the AABB. + +- Distribution: Used to evenly distribute multiple selected nodes, at least three nodes are required for this to take effect + + ![distribute](bar_img/distribute.png) + + The buttons in order from left to right are as follows: + + - Distribute Top + - Distribute Vertical Center + - Distribute Bottom + - Distribute Left + - Distribute Horizontal Center + - Distribute Right + +## Adsorption operation + +In the 3D view, objects in the scene can be edited more accurately through adsorption. Two new adsorption modes have been added to the engine **Vertex Adsorption** and **Surface Adsorption**. + +- **Vertex Adsorption**: The main purpose of vertex adsorption is to align more precisely through a vertex on one object to a certain vertex on another object. The shortcut is v +- **Surface Adsorption**: for objects with colliding bodies, surface adsorption will adsorb to the surface of the colliding body; and for objects without colliding bodies, it will adsorb to the surface of the object. The shortcut isCtrl/Command + Shift + +Operation process. +- Select the model model you want to move in **Hierarchy** panel +- Press the corresponding shortcut key in the scene, a white control block will appear below the model +- Hold down the left mouse button and drag the control block to move the model +- Move the mouse, during the movement, the object will move depending on the type of adsorption operation +- Release the left mouse button to determine the position of the object + +![snaping-surface](images/snaping-surface.gif) + +## Shaded Mode + +![shaded](bar_img/shaded.png) It is mainly used to configure the way the scene is drawn. Clicking on it will display the shading configuration window. + +![shaded-config](images/shaded-config.png) + +Supports both basic shading mode and **Rendering Debug**. + +By default, the base shading mode mode is used. To debug the scene, slide the mouse over the **Rendering Debug** menu (currently only the [Surface Shader](../../shader/surface-shader.md#Debug%20View)) is supported) to select a different debugging mode. + +![images](images/shaded-debug1.png)![images](images/shaded-debug2.png)![images](images/shaded-debug3.png) + +To demonstrate the use of **Rendering Debug** using **World Normal** as an example. + +![debug preview](images/debug-preview.png) + +- Select the engine's built-in Surfaces/standard shader in the **Shader** property of the Material **Property Inspector**. +- Select the item to be debugged in the **Rendering Debug** drop-down menu +- Observe whether the rendering results in the view are as expected + +## Render output target resolution setting + +![resolution](bar_img/desig-resolution.png) The rendering output target resolution of the scene camera can be selected as needed, which will affect the viewing range of the scene camera and facilitate a similar display to the resolution selected in the final preview. + +![scene resolution](images/scene-resolution.png) + +The resolution can be added/modified/deleted in the [Device Manager](../preferences/index.md#Device%20Manager) of **Preferences** in the top menu bar to add/modify/delete resolutions. + +The resolution can also be edited by scrolling down to the bottom: + +![edit resolution](images/edit-resolution.png) + +## Scene lighting settings + +![light](bar_img/light.png) The button is mainly used to set whether to use the scene lights when doing scene editing, the default is to use. + +If the button is shown in blue, it means that the scene lights are used and the scene will be illuminated with the lights added in the scene, as follows: + +![scene light on](images/light-on.png) + +When there are no lights added to the scene, the scene is completely black, making it inconvenient to edit. + +![scene light on](images/remove-light.png) + +At this point, the button can be switched to black, indicating that no scene lights are used and the editor will automatically create a hidden directional light aligned with the scene camera view to illuminate the scene, as shown in the following figure. + +![light](images/light-off.png) + +## View Camera + +Click on the ![set camera](images/set-camera.png) button in the right corner to set the properties of the scene camera (not the user-created camera). + +![scene camera](images/scene-camera.png) + +| Options | Description | +| :-- | :-- | +| Fov | Field of view of the camera | +| Far | The far clipping distance of the camera, should be as small as possible within acceptable range | +| Near | The near clipping distance of the camera, should be as large as possible within acceptable range | +| Color | The Clear Color of the camera | +| Wheel Speed | Set the speed at which the scene camera moves back and forth when the mouse wheel is scrolled | +| Wander Speed | Set the movement speed of the scene camera when wandering | +| Wander Acceleration |Set whether to enable acceleration when the camera wanders (new in v3.3)
If this is checked, acceleration is enabled and the camera will move faster and faster
If this is unchecked, the camera will move at a constant speed. | + +For the description of camera viewing range, please refer to [Camera component](../components/camera-component.md)。 + +## Reference Image + +Click the ![scene reference](images/scene-reference.png) button in the top right corner of the **Scene Editor** to open the **Reference Image** panel, which is mainly used as a cross-reference for developers when stitching together UI in the **Scene Editor**. + +![reference](images/reference.png) + +| Options | Description | +| :-- | :-- | +| Add | Add a UI reference image, multiple reference images can be added. | +| Delete | Deletes the UI reference image in the currently selected box. | +| Position X | The selected UI reference image is displayed in the X-axis position of the scene. | +| Position Y | The selected UI reference image is displayed on the Y-axis of the scene | +| Scale X | Scale on X-axis of the selected UI reference image | +| Scale Y | Scale on Y-axis of the selected UI reference image | +| Opacity | The transparency of the selected UI reference image in the scene | + +### Grid Configuration + +The grid in the scene is an important reference for the position of the scene elements when we place them, and can be set via the button at the top right of the **Scene Editor** at. + +![Gizmo](images/scene-grid.png) + +| Options | Function Description | +| :-- | :-- | +| 3D Icons | Whether the scene is 3D icon enabled or not, the input box after it is used to set the size of the icon Gizmo, the value range is 0 ~ 8.
If you check this box to enable 3D icons, the icon Gizmo in the **Scene Editor** will be a 3D slice with a near-large and far-small effect.
If this item is unchecked, the icon Gizmo will be displayed as a fixed size image. | +| Show Grid | Whether to show the grid in **Scene Editor**, the color setting box behind is used to set the color of the grid. | + +## Scene Gizmo + +Scene Gizmo is in the upper right corner of the scene view, it shows the viewing direction of the current scene camera, and you can quickly switch between different viewing angles by clicking it. + +![Scene Gizmo](images/scene-gizmo.png) + +- Click on the 6 directional axes to quickly switch to the top, bottom, left, right, front, and back angles to view the scene. +- Click the center cube to switch between orthogonal view and perspective view. + +### Select node + +Click the left mouse button in the scene view to select the node where the object is located. Selecting the node is a prerequisite for using transform gizmos to set the node position, rotation, scaling, etc. + +### Multiple selection operation + +Pressing the left mouse button and dragging the mouse within the scene will simultaneously review all the nodes within the current box selection range, and through the transform gizmos, you can batch transform the selected nodes. + +![multiple-select](images/multiple-select.gif) + +### Gizmo operation introduction + +The core function of **Scene** panel is to edit and arrange the visible elements in the scene in a WYSIWYG way. **Gizmos** are mainly used to assist in the visual editing of the scene. + +- [Toolbar](../toolbar/index.md) +- [Camera Gizmo](./camera-gizmo.md) +- [Collider Gizmo](./collider-gizmo.md) +- [Particle System Gizmo](./particle-system-gizmo.md) diff --git a/versions/4.0/en/editor/scene/particle-system-gizmo.md b/versions/4.0/en/editor/scene/particle-system-gizmo.md new file mode 100644 index 0000000000..af9a7bd6ac --- /dev/null +++ b/versions/4.0/en/editor/scene/particle-system-gizmo.md @@ -0,0 +1,33 @@ +# Particle System Gizmo + +Particle System Gizmo is used for the visualization of the [ShapeModule](../../particle-system/emitter.md) of particle system. + +## Box + +`Box Gizmo` shows the length, width, height of box, you can edit them by dragging the control quad. + +![particle box gizmo](images/particle-box-gizmo.png) + +## Sphere + +`Sphere Gizmo` shows the size of sphere, you can edit the radius of sphere by dragging the control quad. + +![particle sphere gizmo](images/particle-sphere-gizmo.png) + +## Hemisphere + +`Hemisphere Gizmo` shows the size of hemisphere, you can edit the radius of sphere by dragging the control quad. + +![particle hemisphere gizmo](images/particle-hemisphere-gizmo.png) + +## Circle + +`Circle Gizmo` shows the size of circle, you can edit the radius of circle by dragging the control quad. + +![particle circle gizmo](images/particle-circle-gizmo.png) + +## Cone + +`Cone Gizmo` shows the shape of cone, you can edit the radius,angle,height of cone by dragging the control quad. + +![particle cone gizmo](images/particle-cone-gizmo.png) diff --git a/versions/4.0/en/editor/terrain/images/create-terrain-asset.png b/versions/4.0/en/editor/terrain/images/create-terrain-asset.png new file mode 100644 index 0000000000..376f68b679 Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/create-terrain-asset.png differ diff --git a/versions/4.0/en/editor/terrain/images/create-terrain.png b/versions/4.0/en/editor/terrain/images/create-terrain.png new file mode 100644 index 0000000000..4f699e65ce Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/create-terrain.png differ diff --git a/versions/4.0/en/editor/terrain/images/terrain-inspector.png b/versions/4.0/en/editor/terrain/images/terrain-inspector.png new file mode 100644 index 0000000000..c89c89be7a Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/terrain-inspector.png differ diff --git a/versions/4.0/en/editor/terrain/images/terrain-layer.png b/versions/4.0/en/editor/terrain/images/terrain-layer.png new file mode 100644 index 0000000000..b156f9e1fc Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/terrain-layer.png differ diff --git a/versions/4.0/en/editor/terrain/images/terrain-manage.png b/versions/4.0/en/editor/terrain/images/terrain-manage.png new file mode 100644 index 0000000000..afda42b5d4 Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/terrain-manage.png differ diff --git a/versions/4.0/en/editor/terrain/images/terrain-paint.png b/versions/4.0/en/editor/terrain/images/terrain-paint.png new file mode 100644 index 0000000000..a180a19ecf Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/terrain-paint.png differ diff --git a/versions/4.0/en/editor/terrain/images/terrain-panel.png b/versions/4.0/en/editor/terrain/images/terrain-panel.png new file mode 100644 index 0000000000..671051da6c Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/terrain-panel.png differ diff --git a/versions/4.0/en/editor/terrain/images/terrain-sculpt.png b/versions/4.0/en/editor/terrain/images/terrain-sculpt.png new file mode 100644 index 0000000000..8943a65e92 Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/terrain-sculpt.png differ diff --git a/versions/4.0/en/editor/terrain/images/terrain-select.png b/versions/4.0/en/editor/terrain/images/terrain-select.png new file mode 100644 index 0000000000..d68bdbf9ab Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/terrain-select.png differ diff --git a/versions/4.0/en/editor/terrain/images/terrain.png b/versions/4.0/en/editor/terrain/images/terrain.png new file mode 100644 index 0000000000..2ad48159d0 Binary files /dev/null and b/versions/4.0/en/editor/terrain/images/terrain.png differ diff --git a/versions/4.0/en/editor/terrain/index.md b/versions/4.0/en/editor/terrain/index.md new file mode 100644 index 0000000000..c2f9a68074 --- /dev/null +++ b/versions/4.0/en/editor/terrain/index.md @@ -0,0 +1,129 @@ +# Terrain System + +The **terrain system** is an efficient tool for creating mountainous landscapes. Users can use various tools to sculpt out basins, mountains, valleys, plains and other landforms. + +![terrain](./images/terrain.png) + +## Creating Terrain Objects and Assets + +To create a new terrain in the project, users need to create a terrain object in the scene in conjunction with a terrain asset in the project directory: + +1. Right click anywhere in the **Hierarchy** panel and select **Create -> Terrain**. This will create a new terrain object in the current scene. Terrain objects can be moved but is prohibited from rotating and scaling. + + ![create terrain](./images/create-terrain.png) + +2. Right click anywhere in the **Assets** panel and select **Create -> Terrain**. This will create a new terrain asset in the project directory. Terrain objects require terrain assets in conjunction to work properly. + + ![create terrain asset](./images/create-terrain-asset.png) + +## Configuring Terrain Properties + +Select the terrain object in the **Hierarchy** panel. Drag and drop the terrain asset from the **Assets** panel in the `Asset` property of the terrain component (`cc.Terrain`) or click the arrow button behind the `Asset` property and select the terrain asset from the drop-down menu. + +The terrain component includes properties as follows: + +![terrain inspector](./images/terrain-inspector.png) + +| Property | Description | +| :----- | :---- | +| Asset | Specify terrain asset | +| Effect Asset | Specify a shader to render terrain, e.g. `builtin-standard.effect` | +| Receive Shadow | Allow shadows to be casted on the terrain object | +| Use Normal Map | Allow terrain to be rendered with normal maps | +| Use PBR | Allow terrain to be rendered using PBR maps | +| Lod Enable | Enable LODs for the terrain mesh to improve rendering performance | +| Lod Bias | Specify the distance at which the LOD level is switched | + +## Terrain Editing Tools + +With the terrain asset specified in the `Asset` property, users now have access to the terrain editing panel in lower right corner of the **Scene** panel, as well as terrain editing tools on the upper left corner of the **Scene** panel. + +Terrain editing panel includes properties brush size and intensity, terrain material and layers, etc. which can be accessed via **Manage**, **Sculpt**, **Paint** and **Select** tabs. + +![terrain panel](./images/terrain-panel.png) + +Terrain editing tools can be found at the upper left corner of the **Scene** panel: +- **Manage** tool: Switch to Manage tab in terrain editing panel. +- **Paint Bulge** tool: Switch to the bulge brush. This is equivalent to selecting **Bulge** in the `BrushMode` property under Sculpt tab in the terrain editing panel. With the bulge brush, users may sculpt bumps in the terrain mesh. +- **Paint Sunken** tool: Switch to the sunken brush. This is equivalent to selecting **Sunken** in the `BrushMode` property under Sculpt tab in the terrain editing panel. With the bulge brush, users may sculpt recesses in the terrain mesh. +- **Paint Smooth** tool: Switch to the smooth brush. This is equivalent to selecting **Smooth** in the `BrushMode` property under Sculpt tab in the terrain editing panel. With the smooth brush, users may smooth out the excessive bumpiness of the terrain mesh by averaging the vertices heights. +- **Paint** tool: Switch to the material painting brush. This is equivalent to selecting the **Paint** tab in the terrain editing panel. With the paint brush, users may no longer sculpt structures on the terrain mesh. Instead the brush allows users to paint a layer of texture on top of the existing texture layer of the terrain mesh. +- **Select** tool: Switch to the selection tool. This is equivalent to selecting the **Select** tab in the terrain editing panel. With the selection tool, users may gain more information including texture layers and height alphas by selecting a tile in the terrain object. + +### Working with Terrain: Manage + +The **Manage** tab includes properties to adjust the overall size, density and texel density of the terrain object. A terrain object consists of multiple instances of an editable surface known as a tile block, which is 32 by 32 meters by default (with **TileSize** being equal to 1.) A terrain object requires at least one tile block. + +![edit manage](./images/terrain-manage.png) + +| Parameter | Description | +| :--- | :-- | +| Tile Size | Scalar for a single tile. By default, a tile block is 32 by 32 meters. This property scales the tile block linearly, which means the final size of a tile block is **32 * Tile Size** square meters. | +| Weight Map Size | Resolution of the weight map which produces the bumps and recesses in the terrain mesh. Higher resolution yields more detailed structures in the terrain mesh. | +| Light Map Size | Resolution of the light map | +| Block Count | Number of tile blocks in the current terrain object | + +### Working with Terrain: Sculpt + +Users may create the structure of the terrain object by sculpting bumps and recesses in the terrain mesh. + +![edit sculpt](./images/terrain-sculpt.png) + +| Parameter | Description | +| :--- | :--- | +| Brush Size | Size of the active brush | +| Brush Strength | Intensity of the active brush | +| Brush Mode | Active mode of the brush, including **Bulge**, **Sunken**, **Smooth**, **Flatten**, **Set Height** | +| Brush Height | Height of the brush, only availabe with **Set Height** mode | +| Brush | Stencil of the brush, requires a texture map with alpha channel as stencil | +| Brush Rotation | The brush rotation | + +| Brush mode | Description | +| :--- | :--- | +| Bulge | Sculpt bumps in the terrain mesh | +| Sunken | Sculpt recesses in the terrain mesh | +| Smooth | Smooth out terrain bumpiness by averaging vertices heights | +| Flatten | Flatten terrain by resetting vertices heights to the same value | +| Set Height | Used in conjunction with the **Brush Height** property to reset vertices heights to a certain value | + +While a brush tool is activated, use **LMB** to sculpt or paint on the terrain mesh. Hold **Shift** to temporarily switch from **Bulge** mode to **Sunken** mode when **Bulge** brush is activated, or vice versa when **Sunken** brush is activated. + +### Working with Terrain: Paint + +With the **Paint** tool, users may paint textures on the terrain mesh. **Paint** tool includes properties as follows: + +![edit paint](./images/terrain-paint.png) + +| Parameter | Description | +| :--- | :--- | +| Terrain Layer | Texture layers for the current terrain object, see below in section **Layer editing**. | +| Brush Size | Size of the active brush | +| Brush Strength | Intensity of the active brush | +| Brush Falloff | Hardness of the active brush
**0.0** indicates full hardness.
**1.0** indicates full feathering. | +| Brush | Stencil of the brush, requires a texture map with alpha channel as stencil | + +#### Painting with Layers + +![terrain layer](./images/terrain-layer.png) + +Users may paint multiple layers of texture on the terrain mesh. Click the **+** or **-** button at the top right of the terrain editing panel to **add** or **delete** layers (up to **4** layers are supported.) Once a Layer is selected, use the **Paint** tool to paint on the corresponding layer. + +| Parameter | Description | +| :--- | :--- | +| Terrain Layer | Texture layers for the current terrain object | +| Normal Map | Assign a normal texture to the selected layer. For this property to take effect, the **Use Normal Map** property in the terrain component must be enabled. | +| Metallic | Set the metallic value of the selected layer | +| Roughness | Set the roughness value of the selected layer | +| Tile Size | Scalar for a single tile. This will only take effect regarding texture projection. Users may use a smaller tile scale compared to that in terms of the terrain construction (**Tile Size** under **Manage** tab) to achieve higher texel density. | + +### Working with Terrain: Select + +When switching to the **Select** tab and selecting a terrain block in the **Scene** panel, information about the current terrain block will be displayed. + +![terrain select](./images/terrain-select.png) + +| Parameter | Description | +| :--- | :--- | +| Index | Index of the selected block (in x and y axis.) | +| Layers | Texture layers being applied to the selected block | +| Weight | Weight maps of the selected block | diff --git a/versions/4.0/en/editor/toolbar/img/2d3d.png b/versions/4.0/en/editor/toolbar/img/2d3d.png new file mode 100644 index 0000000000..d98908f33d Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/2d3d.png differ diff --git a/versions/4.0/en/editor/toolbar/img/browse.png b/versions/4.0/en/editor/toolbar/img/browse.png new file mode 100644 index 0000000000..dfe05fd423 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/browse.png differ diff --git a/versions/4.0/en/editor/toolbar/img/build.png b/versions/4.0/en/editor/toolbar/img/build.png new file mode 100644 index 0000000000..d15ed5a884 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/build.png differ diff --git a/versions/4.0/en/editor/toolbar/img/gizmo_position.png b/versions/4.0/en/editor/toolbar/img/gizmo_position.png new file mode 100644 index 0000000000..ec87099ade Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/gizmo_position.png differ diff --git a/versions/4.0/en/editor/toolbar/img/open_project.png b/versions/4.0/en/editor/toolbar/img/open_project.png new file mode 100644 index 0000000000..e7b10a9c2b Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/open_project.png differ diff --git a/versions/4.0/en/editor/toolbar/img/position-snap.png b/versions/4.0/en/editor/toolbar/img/position-snap.png new file mode 100644 index 0000000000..2c4706faae Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/position-snap.png differ diff --git a/versions/4.0/en/editor/toolbar/img/position-snap1.png b/versions/4.0/en/editor/toolbar/img/position-snap1.png new file mode 100644 index 0000000000..874772802d Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/position-snap1.png differ diff --git a/versions/4.0/en/editor/toolbar/img/position-tool.png b/versions/4.0/en/editor/toolbar/img/position-tool.png new file mode 100644 index 0000000000..d7fa645952 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/position-tool.png differ diff --git a/versions/4.0/en/editor/toolbar/img/preview-bar.png b/versions/4.0/en/editor/toolbar/img/preview-bar.png new file mode 100644 index 0000000000..601f124d7b Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/preview-bar.png differ diff --git a/versions/4.0/en/editor/toolbar/img/preview.png b/versions/4.0/en/editor/toolbar/img/preview.png new file mode 100644 index 0000000000..30d7b78dcb Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/preview.png differ diff --git a/versions/4.0/en/editor/toolbar/img/preview_url.png b/versions/4.0/en/editor/toolbar/img/preview_url.png new file mode 100644 index 0000000000..50d9aa5b56 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/preview_url.png differ diff --git a/versions/4.0/en/editor/toolbar/img/rect-tool-config.png b/versions/4.0/en/editor/toolbar/img/rect-tool-config.png new file mode 100644 index 0000000000..367c4391a5 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/rect-tool-config.png differ diff --git a/versions/4.0/en/editor/toolbar/img/rect-tool.png b/versions/4.0/en/editor/toolbar/img/rect-tool.png new file mode 100644 index 0000000000..4031c6a011 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/rect-tool.png differ diff --git a/versions/4.0/en/editor/toolbar/img/rotation-snap.png b/versions/4.0/en/editor/toolbar/img/rotation-snap.png new file mode 100644 index 0000000000..223f24c4c8 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/rotation-snap.png differ diff --git a/versions/4.0/en/editor/toolbar/img/rotation-tool.png b/versions/4.0/en/editor/toolbar/img/rotation-tool.png new file mode 100644 index 0000000000..d7451f3e70 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/rotation-tool.png differ diff --git a/versions/4.0/en/editor/toolbar/img/scale-snap.png b/versions/4.0/en/editor/toolbar/img/scale-snap.png new file mode 100644 index 0000000000..ac32b83238 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/scale-snap.png differ diff --git a/versions/4.0/en/editor/toolbar/img/scale-tool.png b/versions/4.0/en/editor/toolbar/img/scale-tool.png new file mode 100644 index 0000000000..7ad09bed53 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/scale-tool.png differ diff --git a/versions/4.0/en/editor/toolbar/img/small.png b/versions/4.0/en/editor/toolbar/img/small.png new file mode 100644 index 0000000000..787e94d176 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/small.png differ diff --git a/versions/4.0/en/editor/toolbar/img/smallSecretary.png b/versions/4.0/en/editor/toolbar/img/smallSecretary.png new file mode 100644 index 0000000000..567fd61c35 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/smallSecretary.png differ diff --git a/versions/4.0/en/editor/toolbar/img/toolbarEdit.png b/versions/4.0/en/editor/toolbar/img/toolbarEdit.png new file mode 100644 index 0000000000..705e65a603 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/toolbarEdit.png differ diff --git a/versions/4.0/en/editor/toolbar/img/transform-snap-config.png b/versions/4.0/en/editor/toolbar/img/transform-snap-config.png new file mode 100644 index 0000000000..112131acc6 Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/transform-snap-config.png differ diff --git a/versions/4.0/en/editor/toolbar/img/transform_tool.png b/versions/4.0/en/editor/toolbar/img/transform_tool.png new file mode 100644 index 0000000000..2ef8d1f61b Binary files /dev/null and b/versions/4.0/en/editor/toolbar/img/transform_tool.png differ diff --git a/versions/4.0/en/editor/toolbar/index.md b/versions/4.0/en/editor/toolbar/index.md new file mode 100644 index 0000000000..61e90fd92c --- /dev/null +++ b/versions/4.0/en/editor/toolbar/index.md @@ -0,0 +1,28 @@ +# Toolbar + +The **Toolbar** is located directly above the main editor window and contains seven groups of control buttons or messages that are used to provide editing functionality for specific panels or to facilitate our implementation of development workflows. + +![toolbar](./img/toolbarEdit.png) + +## Preview + +![preview](img/preview-bar.png) + +Multiple preview types are allowed before the preview section, including **in-editor**, **browser** and **emulator**, please refer to [Project Preview & Debugging](../preview/index.md). + +## Build + +![build](img/build.png) Click the build button to build the project, please refer to [Cross-Platform Game Publishing](../publish/index.md) for more information. + +## Mobile Preview Address + +![preview url](./img/preview_url.png) + +This shows the LAN address of the desktop computer running Cocos Creator. Mobile devices connected to the same LAN can access this address to preview and debug the game. Hovering over the LAN address will bring up a QR code, which can also be accessed by scanning the QR code to preview and debug the game. + +## Open the Project/App Directory + +![open project](./img/open_project.png) + +- **Project**: opens the folder where the project is located +- **App**: opens the installation path of the program diff --git a/versions/4.0/en/engine/event/bubble-event.png b/versions/4.0/en/engine/event/bubble-event.png new file mode 100644 index 0000000000..66c998144a Binary files /dev/null and b/versions/4.0/en/engine/event/bubble-event.png differ diff --git a/versions/4.0/en/engine/event/event-api.md b/versions/4.0/en/engine/event/event-api.md new file mode 100644 index 0000000000..ae87765116 --- /dev/null +++ b/versions/4.0/en/engine/event/event-api.md @@ -0,0 +1,61 @@ +# Global and Node Touch and Mouse Events API + +## Mouse Event API + +| Function Name | Return value type | Meaning | +|:--------------|:------------------|:----------------------------------------------------------------------------------------| +| __getScrollY__ | Number | Get the scrolling distance of the mouse wheel on the y-axis, valid only when scrolling. | +| __getButton__ | Number | __EventMouse.BUTTON_LEFT__ or __EventMouse.BUTTON_RIGHT__ or __EventMouse.BUTTON_MIDDLE__. | + +### Global Mouse Events API + +| Function Name | Return value type | Meaning | +|:--------------------|:------------------|:------------------------------------------------------------------------------------------------------------------------------------| +| __getLocation__ | Vec2 | Get the mouse position, which contains the x and y properties. | +| __getLocationX__ | Number | Get the mouse position on x-axis. | +| __getLocationY__ | Number | Get the mouse position on y-axis. | +| __getPreviousLocation__ | Vec2 | Get the position of the last triggered mouse event, which contains the x and y properties. | +| __getDelta__ | Vec2 | Get the distance the mouse has moved relative to the lower-left corner since the last event, which contains the x and y properties. | +| __getDeltaX__ | Number | Get the x-axis distance the mouse has moved relative to the lower-left corner since the last event. | +| __getDeltaY__ | Number | Get the y-axis distance the mouse has moved relative to the lower-left corner since the last event. | + +### Node Mouse Events API + +| Function Name | Return value type | Meaning | +|:-----------------------|:-------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------| +| __getUILocation__ | Vec2 | Get the current mouse position relative to the lower-left corner of the UI window, which contains the x and y properties. | +| __getUILocationX__ | Number | Get the position of the touch point on the x-axis relative to the lower-left corner of the UI window. | +| __getUILocationY__ | Number | Get the current mouse position on y-axis relative to the lower-left corner of the UI window. | +| __getUIPreviousLocation__ | Vec2 | Get the previous mouse position relative to the lower-left corner of the UI window, which contains the x and y properties. | +| __getUIDelta__ | Vec2 | Get the distance the mouse has moved relative to the lower-left corner of the UI window since the last event, which contains the x and y properties. | +| __getUIDeltaX__ | Number | Get the x-axis distance the mouse has moved relative to the lower-left corner of the UI window since the last event. | +| __getUIDeltaY__ | Number | Get the y-axis distance the mouse has moved relative to the lower-left corner of the UI window since the last event. | + +## Touch Events API + +| API Name | Type | Meaning | +|:----------|:--------|:---------------------------------------------------------------------| +| __touch__ | Touch | The touch point related to the current event. | +| __getID__ | Number | Get the ID of the touch point, which is used for multi-touch logic. | + +### Global Touch Events API + +| Function Name | Return value type | Meaning | +|:---------------------|:-------------------|:-------------------------------------------------------------------------------------------------------------------------------------| +| __getLocation__ | Vec2 | Get the mouse position, which contains the x and y properties. | +| __getLocationX__ | Number | Get the position of the touch point on the x-axis. | +| __getLocationY__ | Number | Get the position of the touch point on the y-axis. | +| __getStartLocation__ | Vec2 | Get the initial position of the touch point, which contains the x and y properties. | +| __getPreviousLocation__ | Vec2 | Get the position of the last triggered touch point event, which contains the x and y properties. | +| __getDelta__ | Vec2 | Get the distance the mouse has moved relative to the lower-left corner since the last event, which contains the x and y properties. | + +### Node Touch Events API + +| Function Name | Return value type | Meaning | +|:-----------------------|:-------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------| +| __getUILocation__ | Vec2 | Get the current mouse position relative to the lower-left corner of the UI window, which contains the x and y properties. | +| __getUILocationX__ | Number | Get the current mouse position on x-axis relative to the lower-left corner of the UI window. | +| __getUILocationY__ | Number | Get the current mouse position on y-axis relative to the lower-left corner of the UI window. | +| __getUIStartLocation__ | Vec2 | Get the position of the initial touch point relative to the lower-left corner of the UI window, which contains the x and y properties. | +| __getUIPreviousLocation__ | Vec2 | Get the position of the last touch point relative to the lower-left corner of the UI window, which contains the x and y properties. | +| __getUIDelta__ | Vec2 | Get the distance the mouse has moved relative to the lower-left corner of the UI window since the last event, which contains the x and y properties. | diff --git a/versions/4.0/en/engine/event/event-emit.md b/versions/4.0/en/engine/event/event-emit.md new file mode 100644 index 0000000000..92c6ec0fea --- /dev/null +++ b/versions/4.0/en/engine/event/event-emit.md @@ -0,0 +1,119 @@ +# Event Listening and Emitting + +Cocos Creator provides the `EventTarget` class to suport the listening and emitting of custom event. Before using it, it needs to be imported from the `'cc'` module and an `EventTarget` object needs to be instantiated. + +```ts +import { EventTarget } from 'cc'; +const eventTarget = new EventTarget(); +``` + +> __Note__: although the `Node` object also implements some `EventTarget` interfaces, it is no longer recommended to continue using the `Node` object to listen to and emit custom events. Because this is not efficient enough, and we also hope that `Node` object only listens to events related to `Node`. + +## Event Listening + +Listen to events can be registered by the interface `this.node.on()`. The methods are as follows: + +```ts +// This event listener is triggered every time and needs to be unregistered manually. +eventTarget.on(type, func, target?); +``` + +The `type` is the event registration string. `func` is the callback to listen to when the event is executed. And `target` is the event receive object. If `target` is not set, then `this` in the callback refers to the object that is currently executing the callback. + +The event listener interface `on` can pass to the third parameter target to bind the caller of the response function. Calling the following two methods would have the same effect: + +```ts +// Using Function Binding +eventTarget.on(Node.EventType.MOUSE_DOWN, function ( event ) { + this.enabled = false; +}.bind(this)); + +// Using the third parameter +eventTarget.on(Node.EventType.MOUSE_DOWN, (event) => { + this.enabled = false; +}, this); +``` + +Besides listening with `on`, the `once` interface can also be used. The `once` listener will shut the event being listened to after the listener function responds. + +## Canceling listeners + +We can shut the corresponding event listener using `off` when we don't care about a certain event anymore. + +The `off` method can be used in two ways + +```ts +// Cancel all registered events of this type on the object. +eventTarget.off(type); +// Cancels events on objects with this type of callback designation target. +eventTarget.off(type, func, target); +``` + +One thing to note is that the parameter of `off` must be in one-to-one correspondence with the parameter of `on` in order to cancel it. + +Example: + +```ts +import { _decorator, Component, EventTarget } from 'cc'; +const { ccclass } = _decorator; +const eventTarget = new EventTarget(); + +@ccclass("Example") +export class Example extends Component { + onEnable () { + eventTarget.on('foobar', this._sayHello, this); + } + + onDisable () { + eventTarget.off('foobar', this._sayHello, this); + } + + _sayHello () { + console.log('Hello World'); + } +} +``` + +## Event Emitting + +Events are emitted through the `eventTarget.emit()` interface, as follows: + +```ts +// At most 5 args could be emit. +eventTarget.emit(type, ...args); +``` + +## Explanation for event parameters + +When emitting events, we can pass our event parameters starting with the second argument of the `emit` function. Also, the corresponding event parameters can be fetched in the callback registered in `on`. + +```ts +import { _decorator, Component, EventTarget } from 'cc'; +const { ccclass } = _decorator; +const eventTarget = new EventTarget(); + +@ccclass("Example") +export class Example extends Component { + onLoad () { + eventTarget.on('foo', (arg1, arg2, arg3) => { + console.log(arg1, arg2, arg3); // print 1, 2, 3 + }); + } + + start () { + let arg1 = 1, arg2 = 2, arg3 = 3; + // At most 5 args could be emit. + eventTarget.emit('foo', arg1, arg2, arg3); + } +} +``` + +> __Note__: only up to 5 event parameters can be passed here for the performance of the underlying event distribution. Therefore, care should be taken to control the number of parameters passed when passing a parameter. + +## System built-in events + +Above are the general rules for listening to and emitting events. __Cocos Creator__ has some built-in system events. Please refer to the following documents: + +- [Input Event System](event-input.md) + +- [Node Event System](event-node.md) diff --git a/versions/4.0/en/engine/event/event-input.md b/versions/4.0/en/engine/event/event-input.md new file mode 100644 index 0000000000..b5b3dbf46c --- /dev/null +++ b/versions/4.0/en/engine/event/event-input.md @@ -0,0 +1,242 @@ +# Input Event System + +As mentioned in the previous document, `EventTarget` supports a complete set of event listening and emitting mechanisms. In Cocos Creator v3.4.0, `input` object is supported, which implements the event registering interface of `EventTarget`, and can register global system input events through this object. The original `systemEvent` object has been deprecated since v3.4.0 and will be gradually removed in the future, we recommend using the `input` object as a replacement. + +The differences between `systemEvent` and `input` are as follows: + +- **Differences in type definitions** + + - The declaration of touch event callback in `systemEvent` is `(touch: Touch, event: EventTouch) => void` + + - The declaration of touch event callback in `input` is `(event: EventTouch) => void` + +- **Differences in priority** + + - The event listener of `systemEvent` will be intercepted by the event listener of the node. + + - `input` objects have higher priority than nodes and will not be intercepted. + + > __Note__: we lowered the priority of `input` in **v3.4.1**, so there is no difference in priority between the two objects since v3.4.1. + +--- + +In this section, the handling of global input events in Cocos Creator will be introduced. + +**Global input events** are irrelevant with the node hierarchy, so they are dispatched globally by `input`, currently supported: + +- Mouse +- Touch +- Keyboard +- DeviceMotion + +## How to define the input events + +Use `input.on(type, callback, target)` to register global input event listeners. Event types included: + +| Input Event | `type` | +| :----- | :---------- | +| Mouse Event | `Input.EventType.MOUSE_DOWN`
`Input.EventType.MOUSE_MOVE`
`Input.EventType.MOUSE_UP`
`Input.EventType.MOUSE_WHEEL` | +| Touch Event | `Input.EventType.TOUCH_START`
`Input.EventType.TOUCH_MOVE`
`Input.EventType.TOUCH_END`
`Input.EventType.TOUCH_CANCEL` | +| Keyboard Event | `Input.EventType.KEY_DOWN` (keyboard pressed)
`Input.EventType.KEY_PRESSING` (keyboard continuously pressed)
`Input.EventType.KEY_UP` (keyboard release) | +| DeviceMotion Event | `Input.EventType.DEVICEMOTION` | + +### Pointer Events + +Pointer events include mouse and touch events. + +- Event listener types + + - Mouse event listening + + - `Input.EventType.MOUSE_DOWN` + + - `Input.EventType.MOUSE_MOVE` + + - `Input.EventType.MOUSE_UP` + + - `Input.EventType.MOUSE_WHEEL` + + - Touch event listening + + - `Input.EventType.TOUCH_START` + + - `Input.EventType.TOUCH_MOVE` + + - `Input.EventType.TOUCH_CANCEL` + + - `Input.EventType.TOUCH_END` + +- Callback function after event triggering + + - Custom callback function: `callback(event);` + +- Callback parameter + + - [EventMouse](%__APIDOC__%/en/class/EventMouse) or [EventTouch](__APIDOC__/en/class/EventTouch) + +Examples of the use of pointer events are as follows: + +```ts +import { _decorator, Component, input, Input, EventTouch } from 'cc'; +const { ccclass } = _decorator; + +@ccclass("Example") +export class Example extends Component { + onLoad () { + input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + + onDestroy () { + input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + + onTouchStart(event: EventTouch) { + console.log(event.getLocation()); // location on screen space + console.log(event.getUILocation()); // location on UI space + } +} +``` + +### Keyboard events + +- Event listener types + + - `Input.EventType.KEY_DOWN` + + - `Input.EventType.KEY_PRESSING` + + - `Input.EventType.KEY_UP` + +- Callback function after event triggering + + - Custom Function: `callback(event);` + +- Callback parameter + + - [EventKeyboard](%__APIDOC__%/en/class/EventKeyboard) + +Examples of the use of keyboard events are as follows: + +```ts +import { _decorator, Component, input, Input, EventKeyboard, KeyCode } from 'cc'; +const { ccclass } = _decorator; + +@ccclass("Example") +export class Example extends Component { + onLoad () { + input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this); + input.on(Input.EventType.KEY_UP, this.onKeyUp, this); + } + + onDestroy () { + input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this); + input.off(Input.EventType.KEY_UP, this.onKeyUp, this); + } + + onKeyDown (event: EventKeyboard) { + switch(event.keyCode) { + case KeyCode.KEY_A: + console.log('Press a key'); + break; + } + } + + onKeyUp (event: EventKeyboard) { + switch(event.keyCode) { + case KeyCode.KEY_A: + console.log('Release a key'); + break; + } + } +} +``` + +### Device Motion + +- Event listener type + + - `Input.EventType.DEVICEMOTION` + +- Callback function after event triggering + + - Custom callback function: `callback(event);` + +- Callback parameter + + - [EventAcceleration](%__APIDOC__%/en/class/EventAcceleration) + +Examples of the use of DeviceMotion events are as follows: + +```ts +import { _decorator, Component, input, Input, log } from 'cc'; +const { ccclass } = _decorator; + +@ccclass("Example") +export class Example extends Component { + onLoad () { + input.setAccelerometerEnabled(true); + input.on(Input.EventType.DEVICEMOTION, this.onDeviceMotionEvent, this); + } + + onDestroy () { + input.off(Input.EventType.DEVICEMOTION, this.onDeviceMotionEvent, this); + } + + onDeviceMotionEvent (event: EventAcceleration) { + log(event.acc.x + " " + event.acc.y); + } +} +``` + +The specific usage can be found in the example [event](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/event), which contains the implementation of keyboard, accelerometer, single-touch, multi-touch and other functions. + +## Touch detection for 3D objects + +The touch detection for 3D objects and 2D UI nodes is different: + +- 2D UI nodes only need the size information provided by the `UITransform` component and the position information of the node to do the touch detection. For details, please refer to [Node Event System](event-node.md). + +- The touch detection for 3D objects needs to be implemented by ray cast. The specific method is to generate a ray from the rendering camera of the 3D object to the screen coordinates of the touch point to determine whether the ray hits the object that was detected. The specific code implementation is as follows: + + ```ts + import { _decorator, Component, Node, Camera, geometry, input, Input, EventTouch, PhysicsSystem } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass("Example") + export class Example extends Component { + + // Specify the camera rendering the target node. + @property(Camera) + readonly cameraCom!: Camera; + + @property(Node) + public targetNode!: Node + + private _ray: geometry.Ray = new geometry.Ray(); + + onEnable () { + input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + + onDisable () { + input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + + onTouchStart(event: EventTouch) { + const touch = event.touch!; + this.cameraCom.screenPointToRay(touch.getLocationX(), touch.getLocationY(), this._ray); + if (PhysicsSystem.instance.raycast(this._ray)) { + const raycastResults = PhysicsSystem.instance.raycastResults; + for (let i = 0; i < raycastResults.length; i++) { + const item = raycastResults[i]; + if (item.collider.node == this.targetNode) { + console.log('raycast hit the target node !'); + break; + } + } + } else { + console.log('raycast does not hit the target node !'); + } + } + } + ``` diff --git a/versions/4.0/en/engine/event/event-node.md b/versions/4.0/en/engine/event/event-node.md new file mode 100644 index 0000000000..44c34d7907 --- /dev/null +++ b/versions/4.0/en/engine/event/event-node.md @@ -0,0 +1,245 @@ +# Node Event System + +As mentioned in the previous document, the `input` object supports the global [input event system](event-input.md), and the `Node` also implements the event listening interface of the `EventTarget`. On this basis, the basic node-related system events are provided. This document will introduce how to use these events. + +This document focuses on the mouse and touch events associated with the UI node tree, which are triggered directly on the UI-related nodes and are therefore called node events, and are used as follows: + +```ts +node.on(Node.EventType.MOUSE_DOWN, (event) => { + console.log('Mouse down'); +}, this); +``` + +> __Note__: it is no longer recommended to directly use the event name string to register event listeners. Like the example above, please do not use `node.on('mouse-down', callback, target)` to register event listeners. + +The touch event listener on the node depends on the `UITransform` component, which is only applicable to 2D UI nodes. To implement the touch detection of 3D objects, refer to the [Touch Detection of 3D Objects](event-input.md#touch-detection-for-3D-objects) documentation. + +## Mouse event types and event objects + +__Mouse events__ will only be triggered on desktop platforms, the event types the system provides are as follows: + +| Enum Value Definition | Timing of Event Triggering | +|:-------------------------------|:-----------------------------| +| __Node.EventType.MOUSE_DOWN__ | When a button on the mouse is pressed in the target node area | +| __Node.EventType.MOUSE_ENTER__ | When the cursor enters the target node area, whether or not the button is pressed | +| __Node.EventType.MOUSE_MOVE__ | When the cursor is moved in the target node area, whether or not the button is pressed | +| __Node.EventType.MOUSE_LEAVE__ | When the cursor is moved out of the target node area, whether or not the button is pressed | +| __Node.EventType.MOUSE_UP__ | When a button on the mouse is released | +| __Node.EventType.MOUSE_WHEEL__ | When the mouse wheel is scrolled | + +The important APIs of mouse events (`Event.EventMouse`) are described in the [Mouse Events API](event-api.md#Mouse-Event-API) (`Event` standard events API excluded). + +## Touch event types and event objects + +__Touch events__ can be triggered on both mobile platforms and desktop platforms. Developers can better debug on the desktop platform, by simply listening to touch events and responding to both mobile touch events and desktop mouse events at the same time. The types of touch events provided by the system are as follows: + +| Enum Value Definition | Timing of Event Triggering | +|:--------------------------------|:---------------------------| +| __Node.EventType.TOUCH_START__ | When one or more touch points are placed in the target node area | +| __Node.EventType.TOUCH_MOVE__ | When one or more touch points are moved along the screen | +| __Node.EventType.TOUCH_END__ | When one or more touch points are removed from the screen in the target node area | +| __Node.EventType.TOUCH_CANCEL__ | When one or more touch points are removed from the screen outside the target node area | + +The important APIs of a touch event (`Event.EventTouch`) are described in the [Mouse Events API](event-api.md#Touch-Event-API) (`Event` standard event API excluded). + +> __Note__: touch events support multi-touch, each touch spot will send one event to the event listener. + +## Node Event Dispatching + +The `dispatchEvent` interface is supported on `Node`. Events dispatched by this interface would enter the event delivery stage. The event dispatching system of Cocos Creator is based on the implementation of [event bubbling and capture on Web standard](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_bubbling_and_capture). After the event is dispatched, it will go through the following three stages: +- __Capturing phase__: the event is passed from the scene root node to the child nodes step by step, until it reaches the target node or the event propagation is stopped in the event callback +- __Target phase__: the event is triggered on the target node +- __Bubbling phase__: the event is bubbled from the target node to the parent node level by level, until the root node is reached or the event propagation is stopped in the event callback + +When calling `node.dispatchEvent()`, it means that `node` is the target node mentioned above. In the process of event delivery, call `event.propagationStopped = true` to stop the event propagation. + +In v3.0, the `Event.EventCustom` class was removed. To dispatch custom events, a custom event class that inherits from the `Event` class needs to be implemented first. For example: + +```ts +// Import "Event" from 'cc' module +import { Event } from 'cc'; + +class MyEvent extends Event { + constructor(name: string, bubbles?: boolean, detail?: any){ + super(name, bubbles); + this.detail = detail; + } + public detail: any = null; // Custom property +} +``` + +![bubble-event](bubble-event.png) + +Take the above picture as an example, this picture shows the propagation sequence of events in the **target** and **bubbling** phases. When we dispatch the event `“foobar”` from node c, if both node a and b listen to the event `“foobar”`, the event will pass to node b and a from c. For example: + +```ts +// In the component script of node c +this.node.dispatchEvent( new MyEvent('foobar', true, 'detail info') ); +``` + +To stop the event propagation after node b intercepts the event, call `event.propagationStopped = true` to do this. Detailed methods are as follows: + +```ts +// In the component script of node b +this.node.on('foobar', (event: MyEvent) => { + event.propagationStopped = true; +}); +``` + +> __Note__: to dispatch a custom event, do not use `Event` directly because it's an abstract class. + +If you wish to register the event in the capturing phase, you can pass a fourth parameter to the `on` interface as follows: + +```ts +// in the component script of node a +this.node.on('foobar', callback, target, true); +``` + +## Event Objects + +In the callback of the event listener, the developer will receive an event object event of the `Event` type. `propagationStopped` is the standard API of Event, other important API include: + +| API Name | Type | Meaning | +|:-----------|:-------|:---------| +| __type__ | String | The event type (event name). | +| __target__ | Node | The original target that received the event. | +| __currentTarget__ | Node | The current object that received the event. The current target of the event during the bubbling phase may be different from the original target. | +| __getType__ | Function | Get the event type. | +| __propagationStopped__ | Boolean | Whether or not stop the bubbling phase. The parent node of the current target no longer receives the corresponding event. | +| __propagationImmediateStopped__ | Boolean | Whether or not stop passing the current event immediately. The current target no longer receives the event either. | + +## Touch event propagation + +As mentioned above, touch events registered on the `Node` are dispatched internally by the engine through the `dispatchEvent` interface. Below introduces the propagation sequence of touch events in the **target** and **bubbling** phases. + +### Touch event bubbling + +Touch events support the event bubbling on the node tree, take the pictures below as an example: + +![propagation](propagation.png) + +In the scene shown in the picture, suppose node A has a child node B which has a child node C. The developer sets the touch event listeners for all these three nodes (each node has a touch event listener in examples below by default). + +When the mouse or finger was applied in the node C region, the event will be triggered at node C first and the node C listener will receive the event (this is the target phase). Then the node C will pass this event to its parent node, so the node B listener will receive this event. Similarly the node B will also pass the event to its parent node A. This is a basic event bubbling phase. + +> __Note__: it needs to be emphasized that there is no hit test in parent nodes in the bubbling phase, which means that nodes A and B can receive touch events even though the touch location is out of their node area. + +The bubbling phase of touch events is no different than the general events. Calling `event.propagationStopped = true` can force stopping the bubbling phase. + +### Ownership of touch points among brother nodes + +Suppose the node B and C in the picture above are brother nodes, while C partly covers over B. Now if C receives a touch event, it is announced that the touch point belongs to C, which means that the brother node B won't receive the touch event any more, even though the touch location is also inside its node area. The touch point belongs to the top one among brother nodes. + +At the same time, if C has a parent node, it will also pass the touch event to its parent node through the event bubble mechanism. + +In v3.4.0, the ability of event penetrating dispatch is supported. In this example, if the event needs to be dispatched to node B, the event can be prevented from being swallowed by node C by calling `event.preventSwallow = true`. + +> __Note__: Can prevent swallowing touch end event only if prevent swallowing corresponding touch start event. Penetrating dispatch reduces the efficiency of event dispatch, please use it with caution. + +### Point of Contact Attribution for Different Canvas + +Contact interception between different Canvas is determined by priority. In the scene in the figure below, Canvas 1-5 in the node tree corresponds to priority 1-5 of the image display. It can be seen that even though Canvas nodes 3, 4, and 5 are arranged in scrambled order, the order of response of the contacts is still __Canvas5 -> Canvas4 -> Canvas3 -> Canvas2 -> Canvas1__, according to the priority relationship on the Canvas. The sorting between Canvas is done in the order of the node tree only if the priority is the same. + +![multi-canvas](multi-canvas.png) + +### Register touch or mouse events in the capturing phase + +Sometimes, it is necessary to dispatch touch or mouse events to parent node event listeners before dispatching to any child nodes beneath it in hierarchy, like the design of __ScrollView__ component. + +Event bubbling cannot meet all demands. When this happens, register the parent node event listeners in the capturing phase. To achieve this goal, passing in `true` as the `useCapture` parameter (a fourth parameter) when registering touch or mouse event on the node. For example: + +```ts +this.node.on(Node.EventType.TOUCH_START, this.onTouchStartCallback, this, true); +``` + +When the node fires the `Node.EventType.TOUCH_START` event, the `Node.EventType.TOUCH_START` event will be first dispatched to all the parent node event listeners registered in the capturing phase, then dispatched to the node itself, and finally comes the event bubbling phase. + +Only touch or mouse events can be registered in the capturing phase, while the other events can't be. + +### Event Interception + +Normal events are dispensed as described above. However, if the node has components such as `Button`, `Toggle` or `BlockInputEvents` on it, it will stop event bubbling. + +Look at the picture below. There are two buttons, priority 1 for Canvas0 and priority 2 for Canvas1. Click on the intersection of the two buttons, which is the blue area in the picture, it appears that button priority 2 received the contact event successfully, while button priority 1 did not.
That's because according to the event reception rules above, button priority 2 receives contact events first and intercepts them (`event.propagationStopped = true`) to prevent event penetration. If the node is a non-button node, events can also be intercepted by adding the `BlockInputEvents` component to prevent penetration. + +> **Note**: buttons priority 1 and priority 2 are under Canvas 0 and Canvas 1 nodes respectively, the two buttons are not brother nodes. + +![events-block](events-block.png) + +## Example for touch events + +Using the example below, summarizing touch events is easy. There are four nodes A, B, C and D in the picture above, where A and B are brother nodes. The specific hierarchical relationship should be like this: + +![example](example.png) + +1. If one touch is applied in the overlapping area between A and B, now B won't receive the touch event, so that propagating order of the touch event should be __A -> C -> D__. +2. If the touch location is in node B (the visible green area), the order should be __B -> C -> D__. +3. If the touch location is in node C, the order should be __C -> D__. +4. As a precondition to the second case, register touch events on C D node in the capturing phase, then the order should be __D -> C -> B__. + +## Other events of Node + +All built-in `Node` events can get event names from `Node.EventType`. + +### 3D Node Events + +| Enum Value Definition | Timing of Event Triggering | +| :------------- | :---------- | +| __TRANSFORM_CHANGED__ | When a transform property is modified, an enum value `TransformBit` is assigned that defines the modified transform based on the enum value. | + +Definition of Transformation Enum Values: + +| Enum Value Definition | Transformations | +|:---------------------------|:-----------------------------------------------| +| __TransformBit.NONE__ | The properties remain unchanged. | +| __TransformBit.POSITION__ | The node position changes. | +| __TransformBit.ROTATION__ | The node rotation changes. | +| __TransformBit.SCALE__ | The node scale changes. | +| __TransformBit.RS__ | The node rotation and scale change. | +| __TransformBit.TRS__ | The node position, rotation and scale change. | + +### 2D Node Events + +| Enum Value Definition | Timing of Event Triggering | +|:---------------------------|:-------------------------------------------------------------------------------------------------------------------------| +| __SIZE_CHANGED__ | When the width/height property is modified. The width/height property is located on the `UITransform` component. | +| __ANCHOR_CHANGED__ | When the X/Y properties of the anchor is modified. The width/height property is located on the `UITransform` component. | +| __COLOR_CHANGED__ | When the color property is modified. The width/height property is located on the `UITransform` component. | +| **CHILD_ADDED** | When adding child node | +| **CHILD_REMOVED** | When removing child node | +| **PARENT_CHANGED** | When the parent node changes | +| **SIBLING_ORDER_CHANGED** | When the sibling order changes | +| **SCENE_CHANGED_FOR_PERSISTS** | When the scene changes where the persist node is | +| **NODE_DESTROYED** | When destroying node | +| **LAYER_CHANGED** | When `layer` property changes | +| **ACTIVE_IN_HIERARCHY_CHANGED** | When `activeInHierarchy` property changes | + +## Multi-touch event + +The engine has a multi-touch event blocking switch. Multi-touch events are enabled by default. For projects that do not require multi-touch, you can disable allowing multi-touch with the following code: + +```ts +macro.ENABLE_MULTI_TOUCH = false; +``` + +Alternatively, it can be configured via __Project -> Project Settings -> Macro Config__. Just uncheck the **ENABLE_MULTI_TOUCH** property. + +## Pause or resume node system events + +Pause node system events + +```ts +// Pause all node system events registered on the current node. Node system events include Touch and Mouse Events. +// If a parameter true is passed, the API will pause node system events on this node and all its children. +// Example +this.node.pauseSystemEvents(); +``` + +Resume node system events + +```ts +// Resume all node system events registered on the current node. Node system events include Touch and Mouse Events. +// If a parameter true is passed, the API will resume node system events on this node and all its children. +// Example +this.node.resumeSystemEvents(); +``` diff --git a/versions/4.0/en/engine/event/event-screen.md b/versions/4.0/en/engine/event/event-screen.md new file mode 100644 index 0000000000..89eae921f2 --- /dev/null +++ b/versions/4.0/en/engine/event/event-screen.md @@ -0,0 +1,55 @@ +# Screen Event System in Cocos Creator + +## Introduction + +As discussed earlier, `EventTarget` provides functionalities for event listening and emitting. Cocos Creator v3.8.0 introduces the `screen` object, which implements the `EventTarget` interface. This object allows registering global system screen events. + +## Supported Events + +Here's a table outlining the currently supported global screen events: + +| Event Name | Description | Supported Platforms | Supported Version | +|----------------------------|-------------------------------------------------|--------------------------|-------------------| +| `window-resize` | Listens for window size changes. | Web, Native, MiniGame | 3.8.0 | +| `orientation-change` | Listens for screen orientation changes. | Web, Native | 3.8.3 | +| `fullscreen-change` | Listens for full screen changes. | Web | 3.8.0 | + +## Event Usage Example + +```typescript +import { _decorator, Component, screen, macro } from 'cc'; +const { ccclass } = _decorator; + +@ccclass("Example") +export class Example extends Component { + onLoad() { + // Register event listeners with the screen object + screen.on('window-resize', this.onWindowResize, this); + screen.on('orientation-change', this.onOrientationChange, this); + screen.on('fullscreen-change', this.onFullScreenChange, this); + } + + onDestroy() { + // Unregister event listeners when the component is destroyed + screen.off('window-resize', this.onWindowResize, this); + screen.off('orientation-change', this.onOrientationChange, this); + screen.off('fullscreen-change', this.onFullScreenChange, this); + } + + onWindowResize(width: number, height: number) { + console.log("Window resized:", width, height); + } + + onOrientationChange(orientation: number) { + if (orientation === macro.ORIENTATION_LANDSCAPE_LEFT || orientation === macro.ORIENTATION_LANDSCAPE_RIGHT) { + console.log("Orientation changed to landscape:", orientation); + } else { + console.log("Orientation changed to portrait:", orientation); + } + } + + onFullScreenChange(width: number, height: number) { + console.log("Fullscreen change:", width, height); + } +} +``` diff --git a/versions/4.0/en/engine/event/events-block.png b/versions/4.0/en/engine/event/events-block.png new file mode 100644 index 0000000000..9fb6664e4e Binary files /dev/null and b/versions/4.0/en/engine/event/events-block.png differ diff --git a/versions/4.0/en/engine/event/example.png b/versions/4.0/en/engine/event/example.png new file mode 100644 index 0000000000..d38c8f2985 Binary files /dev/null and b/versions/4.0/en/engine/event/example.png differ diff --git a/versions/4.0/en/engine/event/index.md b/versions/4.0/en/engine/event/index.md new file mode 100644 index 0000000000..fa3d7c8c24 --- /dev/null +++ b/versions/4.0/en/engine/event/index.md @@ -0,0 +1,13 @@ +# Event System + +The event system is a common feature that needs to be involved in interaction during game development. The event system can be used not only to send input actions (e.g. keyboard, mouse, touch) to the application in the form of events, but also to respond to things that happen during the game that require the attention of other objects in the form of events. For example, opening the checkout or reward screen after a game victory. + +## Use of Events + +Events need to be registered to get listened to. Learn about event listening and emitting through the [Event Listening and Emitting](event-emit.md) documentation. + +On the basis of event listening and emitting, Cocos Creator supports many built-in event systems, including: + +- [Input Event System](event-input.md) +- [Node Event System](event-node.md) +- [Screen Event System](event-screen.md) diff --git a/versions/4.0/en/engine/event/multi-canvas.png b/versions/4.0/en/engine/event/multi-canvas.png new file mode 100644 index 0000000000..3004dd4647 Binary files /dev/null and b/versions/4.0/en/engine/event/multi-canvas.png differ diff --git a/versions/4.0/en/engine/event/propagation.png b/versions/4.0/en/engine/event/propagation.png new file mode 100644 index 0000000000..4c3cd1170f Binary files /dev/null and b/versions/4.0/en/engine/event/propagation.png differ diff --git a/versions/4.0/en/engine/renderable/create-model.png b/versions/4.0/en/engine/renderable/create-model.png new file mode 100644 index 0000000000..86412ef46a Binary files /dev/null and b/versions/4.0/en/engine/renderable/create-model.png differ diff --git a/versions/4.0/en/engine/renderable/mesh-renderer/view-model.gif b/versions/4.0/en/engine/renderable/mesh-renderer/view-model.gif new file mode 100644 index 0000000000..75bb20c8a8 Binary files /dev/null and b/versions/4.0/en/engine/renderable/mesh-renderer/view-model.gif differ diff --git a/versions/4.0/en/engine/renderable/meshrenderer-properties.png b/versions/4.0/en/engine/renderable/meshrenderer-properties.png new file mode 100644 index 0000000000..c62a5ad74f Binary files /dev/null and b/versions/4.0/en/engine/renderable/meshrenderer-properties.png differ diff --git a/versions/4.0/en/engine/renderable/model-component.md b/versions/4.0/en/engine/renderable/model-component.md new file mode 100644 index 0000000000..e8c561f745 --- /dev/null +++ b/versions/4.0/en/engine/renderable/model-component.md @@ -0,0 +1,89 @@ +# MeshRenderer Component Reference + +The __MeshRenderer__ component is used to display a static 3D model. Set the model mesh with the `Mesh` property, and change the appearance of the model with the `Materials` property. + +Click **Add Component -> Mesh -> MeshRenderer** in the **Inspector** panel to add the MeshRenderer component. + +![MeshRenderer properties](meshrenderer-properties.png) + +## MeshRenderer Properties + +| Property | Description +| :--- | :--- +| **Materials** | The material used to render the model, one material corresponds to one sub-mesh in the mesh. +| **LightmapSettings** | For baking Lightmap, please refer to [Lightmapping](../../concepts/scene/light/lightmap.md) for details. +| **ShadowCastingMode** | Specifies whether the current model will cast shadows, which needs to [enable shadow effect](../../concepts/scene/light/shadow.md#enable-shadow-effect) in the scene first +| **ReceiveShadow** | Specifies whether the current model will receive and display shadow effects generated by other objects, which needs to [enable shadow effect](../../concepts/scene/light/shadow.md#enable-shadow-effect) in the scene first. This property takes effect only when the shadow type is __ShadowMap__. +| **Mesh** | 3D model assets for rendering. + +The component interfaces for the common model are described in [MeshRenderer API](%__APIDOC__%/en/class/MeshRenderer). + +The component interfaces for the skinning model are described in [SkinnedMeshRenderer API](%__APIDOC__%/en/class/SkinnedMeshRenderer). + +### Mesh Assets + +Mesh assets are necessary for rendering meshes. Currently, meshes are mainly generated automatically by Creator when [importing models](../../asset/model/mesh.md) into Creator. + +A Mesh asset contains a set of vertices and multiple sets of indices. The index points to the vertex in the vertex array, and every three sets of indices form a triangle. A mesh is composed of multiple triangles and is the most basic graphical element in the 3D world. Multiple triangles are stitched together to form a complex polygon, and multiple polygons are stitched together to form a 3D model. + +Creator provides several simple static 3D models with basic models such as the cube, cylinder, etc. Developers can create a few in the **Hierarchy** panel as needed to get a first look. + +![create model](create-model.png) + +## Model Group Rendering + +The group rendering function is determined by the [Visibility property](../../editor/components/camera-component.md#set-the-visibility-property) of the camera component and the [Layer property](../../concepts/scene/node-component.md#set-the-layer-property-of-the-node) of the node. Users can set the `Visibility` value through code to complete the group rendering. All nodes belong to the `DEFAULT` layer by default and are visible on all cameras. + +## Static Batching + +The current static batching scheme is static batching at run time. Static batching can be performed by calling `BatchingUtility.batchStaticModel`. This function receives a node, and then merges all `Mesh` in `MeshRenderer` under that node into one, and hangs it under another node. + +After batching, the original transform of `MeshRenderer` cannot be changed, but the transform of the root node after batching can be changed. Only nodes that meet the following conditions can be statically batched: + +- The child node can only contain `MeshRenderer`. +- The vertex data structure of `Mesh` of `MeshRenderer` under child nodes must be consistent. +- The material of `MeshRenderer` under child nodes must be the same. + +## Dynamic Batching + +The engine currently provides **instancing batching** . + +To turn on batching, simply check the corresponding `USE_INSTANCING` switch in the material used by the model. + +> **Note**: the current batching process introduces several limitations: +> +> 1. The transparent models in the same batch are not sorted, this may lead to incorrect blending results. +> 2. The inverse-transpose world matrix is not uploaded into batches, models with non-uniform scale will have inaccurate normals. +> 3. Only plain models and skinning models under pre-baked skeletal animation are supported. (i.e. real-time skeletal animations, 2D objects, UIs and particles do not support dynamic batching) + +### Instancing Batching + +The batch through **instancing** is suitable for drawing a large number of dynamic models with the same vertex data. When enabled, drawing will be grouped according to the material and vertex data, and the instanced attributes information will be organized in each group, and then complete the drawing at one time. + +> **Note**: for the support and related settings of the skinning model, refer to the [Skeletal Animation Component](../../animation/skeletal-animation.md#AboutDynamic-Instancing) documentation. + +In addition, inside each group, the instanced attributes supports custom additional instanced attributes, which can pass more per-instance data between different instances (such as the difference in appearance of a diffuse color between different characters, or the influence of wind in a large grass field). This requires the support of custom effects. For more detailed instructions, please refer to the [Syntax Guide](../../material-system/effect-syntax.md#Custom-Instanced-Properties) documentation. + +### VB-merging Batching + +VB-merging batching will do some operations such as merging vertices per frame introduce a portion of CPU overhead, which is particularly expensive in JavaScript. Abuse without rigorous testing can cause performance degradation, **This feature has been removed in 3.6.2, so please use Instancing or static batching as a preference instead.** + +In addition, it is necessary to remind that the number of draw calls is not is not the only performance metric[^2]. Optimal performance is often the result of CPU and GPU load balancing, so when using batch functions, be sure to do more tests to identify performance bottlenecks and do targeted optimization. + +## Batch Best Practices + +Generally speaking, the priority of the batch system is: **static batching** -> **instancing batching**. + +The material must be insured that it is consistent, under this premise: + +- If you are certain that certain models will remain completely static during the game cycle, use **static batching**. +- If there are a large number of the same model repeated drawing, there is only a relatively controllable small difference between each other, use **instancing batching**. + +## Preview Model + +The model can be observed in the scene view when the mouse hovers over the drop-down mesh in the **Mesh** property. + +![view model](mesh-renderer/view-model.gif) + +[^1]: Currently use uniforms to upload the batched world transformation matrix, taking into account the WebGL standard uniform quantity limit, the current batch draws up to 10 models, so for a large number of same For the material model, the number of drawcalls is expected to be reduced by up to 10 times after enabling __VB-merging batching__. +[^2]: There have been many discussions in the industry on the topic of batching and performance, you can refer to this [nVidia slide](https://www.nvidia.com/docs/IO/8228/BatchBatchBatch.pdf). diff --git a/versions/4.0/en/engine/rendering/sorting-2d.md b/versions/4.0/en/engine/rendering/sorting-2d.md new file mode 100644 index 0000000000..fd793f8c23 --- /dev/null +++ b/versions/4.0/en/engine/rendering/sorting-2d.md @@ -0,0 +1,76 @@ +# 2D Rendering Sorting Component + +The Sorting2D component allows customizing the rendering order of 2D objects without affecting the original node hierarchy. It can help solve the issue of increased DrawCalls caused by adjacent render objects using different materials. + +![sorting2D-component](../../../zh/engine/rendering/sorting-2d/sorting2D-component.png) + +## Sorting2D Properties + +| Property | Description | +| :-------------- | :----------- | +| sortingLayer | Sets the sorting layer for render objects. [**Modify project's sorting layer configuration**](../../editor/project/index.md#sorting-layers). Lower layers are rendered first. Default layer is "Default". | +| sortingOrder | Sets the order of render objects within the same sorting layer. Objects with lower values are rendered first. If values are equal, nodes with lower hierarchy are rendered first. | + +**Note**: When recursively collecting render elements from a 2D node tree encounters a Mask renderer, it will use custom sorting rules to render the recorded 2D UI, reset the render state, recollect render elements, and continue using custom sorting rules for the remaining 2D UI after collection is complete. + +## Optimization Example + +1. Configure sorting layers in your project + + ![](./sorting-2d/sorting2D-layers.jpg) + +2. Enable the `2D Rendering Sorting` module in `Project Settings -> Feature Cropping` + + ![](./sorting-2d/sorting2D-open.jpg) + +3. Add Sorting2D component to 2D UI nodes and set sorting layers and orders. + + ![](./sorting-2d/sorting2D-setProperty.jpg) + +4. Build the project, run and observe optimization results + + * Before optimization + ![](../../../zh/engine/rendering/sorting-2d/sorting2D-no-optimize.jpg) + + * After optimization + ![](../../../zh/engine/rendering/sorting-2d/sorting2D-optimized.jpg) + +## Modifying Sorting2D Component's Layer and Order in Code + +```typescript +import { _decorator, Component, find, Node, settings, Sorting2D } from 'cc'; +const { ccclass, property } = _decorator; + +const sortingLayers = settings.querySettings("engine", "sortingLayers"); +const default_layer = sortingLayers[0].value; +const autoAtlas_1_layer = sortingLayers[1].value; +const autoAtlas_2_layer = sortingLayers[2].value; +const autoAtlas_1_1_layer = sortingLayers[3].value; +const label_layer = sortingLayers[4].value; + +@ccclass('NewComponent') +export class NewComponent extends Component { + + start() { + var testNode = find("Canvas/test"); + if (testNode) { + this.changeUISortingLayer(testNode, autoAtlas_1_layer, 0); + } + } + + changeUISortingLayer(sortingNode: Node, sortingLayer: number, sortingOrder?: number) { + if (sortingNode.getComponent(Sorting2D)) { + sortingNode.getComponent(Sorting2D).sortingLayer = sortingLayer; + if (sortingOrder !== undefined) { + sortingNode.getComponent(Sorting2D).sortingOrder = sortingOrder; + } + } + } +} +``` + +## API Reference and Examples + +[Sorting2D API](https://docs.cocos.com/creator/4.0/api/en/class/Sorting2D) + +[Sorting2D Demo](https://github.com/cocos/cocos-test-projects/tree/v3.8.7/assets/cases/ui/other/sorting2D) diff --git a/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-addComponent.jpg b/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-addComponent.jpg new file mode 100644 index 0000000000..ef526b5489 Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-addComponent.jpg differ diff --git a/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-layers.jpg b/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-layers.jpg new file mode 100644 index 0000000000..ab084987b0 Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-layers.jpg differ diff --git a/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-open.jpg b/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-open.jpg new file mode 100644 index 0000000000..d9749f485e Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-open.jpg differ diff --git a/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-setProperty.jpg b/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-setProperty.jpg new file mode 100644 index 0000000000..9413e9f38a Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting-2d/sorting2D-setProperty.jpg differ diff --git a/versions/4.0/en/engine/rendering/sorting.md b/versions/4.0/en/engine/rendering/sorting.md new file mode 100644 index 0000000000..57d6314edb --- /dev/null +++ b/versions/4.0/en/engine/rendering/sorting.md @@ -0,0 +1,87 @@ +# 3D Sorting + +For most rendering cases, the default sorting is sufficient. But in practice, due to the special nature of translucent objects, we may need to sort these objects manually. For this case, the **Sorting** component can be used. + +Click the **Add Component** button inside the **Inspector** panel and select **Sorting** to add it. + +![sorting](sorting/sorting.png) + +> **Note**: This component is only available for nodes holding **MeshRenderer** and **SpriteRenderer** components. + +## Properties + +| Properties | Decription | +| :-- | :-- | +| **Sorting Layer** | Select **Sort Layers**
Select different sort layers by using the drop down box, these layers need to be set within **Project Settings** -> **Sort Layers** before they can be selected
![sortring-layer](sorting/sorting-layer.png) | +| **Sorting Order** | Sorting priority within the same layer | + +## Example + +- Add Sorting Layer 1 to **Project Settings** -> **Sorting Layer**. + + ![setting](sorting/project-settings.png) + +- Build the following scene as shown in the example. + + ![default](sorting/default-sort.png) + +- The cube and sphere materials require the option of using the translucent render queue. + + ![technique](sorting/tech.png) + + Adds the Sorting component and leaves the properties as default. + +- The image needs to use the **SpriteRenderer** component (note that it is not a Sprite component), the shader needs to use the **builtin-sprite-renderer**, and the Sorting component needs to be added, keeping the properties defaulted to. + + ![layer1](sorting/sprite-renderer-layer.png) + +At this point it can be observed that the nodes are sorted in the default order. + +### Sorting Layer Example + +The Sorting Layer of the SpriteRenderer node is adjusted as follows. + +![layer 2](sorting/sorting-layer1.png) + +Since the custom level is higher than the level default for cubes and spheres, the result can be observed in the following figure. + +![result](sorting/result-sorting-layer.png) + +### Sorting Order Example + +The Sorting Order property allows you to adjust the priority of rendering objects within the same sorting layer. + +The scene is similar to the example above, but with the Sorting Order property within the Sorting component of the cube adjusted to 1. + +![order 1](sorting/sorting-order1.png) + +At this point, the cube is prioritized over the sphere within the rendering queue, so you can observe the result in the following figure. + +![result](sorting/sorting-order1-result.png) + +## Code Example + +The following code demonstrates how to modify the Sorting Order and Sorting Layer at runtime. + +```ts +import { _decorator, Component, Sorting, SortingLayers } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('SortingSample') +export class SortingSample extends Component { + + @property(Sorting) + sorting:Sorting | null = null; + + start() { + this.sorting.sortingLayer = SortingLayers.getLayerIndexByName("Sorting Layer 1"); + this.sorting.sortingOrder = 1; + } +} +``` + +**SortingLayers** The names and indexes of sorting layers can be obtained at runtime, but cannot be modified at runtime. + +## API + +The API for sorting components can be found in [Sorting](%__APIDOC__%/en/class/Sorting) and [SortingLayers](%__APIDOC__%/en/class/SortingLayers) 。 diff --git a/versions/4.0/en/engine/rendering/sorting/default-sort.png b/versions/4.0/en/engine/rendering/sorting/default-sort.png new file mode 100644 index 0000000000..affb0e08e2 Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/default-sort.png differ diff --git a/versions/4.0/en/engine/rendering/sorting/project-settings.png b/versions/4.0/en/engine/rendering/sorting/project-settings.png new file mode 100644 index 0000000000..d1d0734fa1 Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/project-settings.png differ diff --git a/versions/4.0/en/engine/rendering/sorting/result-sorting-layer.png b/versions/4.0/en/engine/rendering/sorting/result-sorting-layer.png new file mode 100644 index 0000000000..747cd800a1 Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/result-sorting-layer.png differ diff --git a/versions/4.0/en/engine/rendering/sorting/sorting-layer.png b/versions/4.0/en/engine/rendering/sorting/sorting-layer.png new file mode 100644 index 0000000000..cd9572b379 Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/sorting-layer.png differ diff --git a/versions/4.0/en/engine/rendering/sorting/sorting-layer1.png b/versions/4.0/en/engine/rendering/sorting/sorting-layer1.png new file mode 100644 index 0000000000..b353823258 Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/sorting-layer1.png differ diff --git a/versions/4.0/en/engine/rendering/sorting/sorting-order1-result.png b/versions/4.0/en/engine/rendering/sorting/sorting-order1-result.png new file mode 100644 index 0000000000..df467f408c Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/sorting-order1-result.png differ diff --git a/versions/4.0/en/engine/rendering/sorting/sorting-order1.png b/versions/4.0/en/engine/rendering/sorting/sorting-order1.png new file mode 100644 index 0000000000..d4b871182b Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/sorting-order1.png differ diff --git a/versions/4.0/en/engine/rendering/sorting/sorting.png b/versions/4.0/en/engine/rendering/sorting/sorting.png new file mode 100644 index 0000000000..44415270cd Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/sorting.png differ diff --git a/versions/4.0/en/engine/rendering/sorting/sprite-renderer-layer.png b/versions/4.0/en/engine/rendering/sorting/sprite-renderer-layer.png new file mode 100644 index 0000000000..0db65490b7 Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/sprite-renderer-layer.png differ diff --git a/versions/4.0/en/engine/rendering/sorting/tech.png b/versions/4.0/en/engine/rendering/sorting/tech.png new file mode 100644 index 0000000000..dedcfba542 Binary files /dev/null and b/versions/4.0/en/engine/rendering/sorting/tech.png differ diff --git a/versions/4.0/en/engine/template/native-upgrade-to-v3.5.md b/versions/4.0/en/engine/template/native-upgrade-to-v3.5.md new file mode 100644 index 0000000000..a7116cac4c --- /dev/null +++ b/versions/4.0/en/engine/template/native-upgrade-to-v3.5.md @@ -0,0 +1,156 @@ +# v3.5 Built project upgrade Guide + +Since v3.5, the 'appdelegate' of MAC and windows platforms has been moved into the internal implementation of the engine, and can be compatible with the usage of previous versions by overloading 'appdelegate' `game.cpp` has also been adjusted, and existing projects need to be rebuilt and upgraded. + +## Engineering Upgrade + +Check whether the native/engine directory exists in the project directory. If it exists, you need to delete the folder and make a backup before deleting it (if this directory exists, it will not be updated automatically when you rebuild); if it does not exist, you can build it directly. + +### Custom code migration methods + +The code previously added in AppDelegate can be upgraded by customizing the platform and AppDelegate below; the custom game.cpp can be upgraded by replacing the interface. + +#### Platform and AppDelegate customization methods + +Take **Mac** as an example: + +1. Custom AppDelegate(Reference file name: MyAppdelegate.h, MyAppdelegate.mm) + +``` +@interface MyAppDelegate : NSObject + // Define the methods that need to be rewritten + - (void)applicationWillResignActive:(UIApplication *)application; +@end + +@implementation MyAppDelegate +- (void)applicationWillResignActive:(UIApplication *)application { + // Note: Calling methods of the parent class + [super applicationWillResignActive:application] +} +@end +``` + +2. Customized platforms(Reference file name: CustomMacPlatform.h) + +``` +#include "platform/BasePlatform.h" +#include "MyAppDelegate.h" + +class CustomMacPlatform : public MacPlatform { +public: + // Rewrite the initialization method of the platform + int32_t init() override { + // Calling the methods of the parent class + return MacPlatform::init(); + } + // Here you enter the message loop of oc until the program exits + int32_t run(int argc, const char** argv) { + id delegate = [[MyAppDelegate alloc] init]; + NSApplication.sharedApplication.delegate = delegate; + return NSApplicationMain(argc, argv); + } +} +``` + +3. Loading custom platforms(Reference file name: main.mm) + +``` +#include "CustomMacPlatform.h" + +int main(int argc, const char * argv[]) { + CustomMacPlatform platform; + if (platform.init()) { + return -1; + } + return platform.run(argc, (const char**)argv); +} +``` + +#### game.cpp Migration mode + +- Set js encryption secret key: jsb_set_xxtea_key -> Set `_xxteaKey` member variable; ; or call `setXXTeaKey` +- Setup debugging: jsb_enable_debugger -> Change the value of `_debuggerInfo`, or call `setDebugIpAndPort` +- Setting exception callbacks: setExceptionCallback -> Override the `handleException` interface +- Run custom scripts: jsb_run_script -> call `runScript` +- You can add events to be listened to by using `engine`, -> `getEngine()->addEventCallback(WINDOW_OSEVENT, eventCb);` +- Customized games `CustomGame`, Need to register to engine `CC_REGISTER_APPLICATION(CustomGame)` for loading +- `game` Inherited from `cc::BaseGame`, and `cc::BaseGame` inherits from `CocosApplication`,so that partial implementations can be rewritten to add custom logic. + +### The modification of Native Files + +- Replace the header file path: #include "cocos/platform/Application.h" —> #include "application/ApplicationManager.h" +- Change of usage: cc::Application::getInstance()->getScheduler() -> CC_CURRENT_ENGINE()->getScheduler() +- If the code uses custom jsb: `native_ptr_to_seval` changed to `nativevalue_to_se` + +### Android + +#### JAVA + +- Delete `onCreate` in the following files: game/AppActivity.java, game/InstantActivity.java + + ```java + // Workaround in https://stackoverflow.com/questions/16283079/re-launch-of-activity-on-home-button-but-only-the-first-time/16447508 + if (!isTaskRoot()) { + // Android launched another instance of the root activity into an existing task + // so just quietly finish and go away, dropping the user back into the activity + // at the top of the stack (ie: the last state of this task) + // Don't need to finish it again since it's finished in super.onCreate . + return; + } + ``` + +- app/AndroidManifest.xml + - delete code in the `application` tag: `android:taskAffinity=""` + - add code in the `application` tag: `android:exported="true"` + +- app/build.gradle + - modify code: + + ```html + "${RES_PATH}/assets" -> "${RES_PATH}/data" + ``` + +#### CMakeLists.txt + +- android/CMakeLists.txt + - LIB_NAME changed to CC_LIB_NAME + - PROJ_SOURCES changed to CC_PROJ_SOURCES + - add code: set(CC_PROJECT_DIR ${CMAKE_CURRENT_LIST_DIR}) + - add code: set(CC_COMMON_SOURCES) + - add code: set(CC_ALL_SOURCES) + - delete code: + + ```cmake + ${CMAKE_CURRENT_LIST_DIR}/../common/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/../common/Classes/Game.cpp + + add_library(${LIB_NAME} SHARED ${PROJ_SOURCES}) + target_link_libraries(${LIB_NAME} + "-Wl,--whole-archive" cocos2d_jni "-Wl,--no-whole-archive" + cocos2d + ) + target_include_directories(${LIB_NAME} PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/../common/Classes + ) + ``` + + - add code: + + ```cmake + cc_android_before_target(${CC_LIB_NAME}) + add_library(${CC_LIB_NAME} SHARED ${CC_ALL_SOURCES}) + # Add user dependent library AAA here. target_link_libraries(${CC_LIB_NAME} AAA) + # Add user defined file xxx/include here. target_include_directories(${CC_LIB_NAME} PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../common/Classes/xxx/include) + cc_android_after_target(${CC_LIB_NAME}) + ``` + +- common/CMakeLists.txt + - cocos2d-x-lite/ changed to engine/native/ + - Add code at the end of the file + + ```cmake + list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp + ) + ``` diff --git a/versions/4.0/en/engine/template/native-upgrade-to-v3.6.md b/versions/4.0/en/engine/template/native-upgrade-to-v3.6.md new file mode 100644 index 0000000000..ead16cf613 --- /dev/null +++ b/versions/4.0/en/engine/template/native-upgrade-to-v3.6.md @@ -0,0 +1,69 @@ +# v3.5 Built Project Upgrade Guide to v3.6 + +This article details the considerations for upgrading Cocos Creator native builds from 3.5.0 ~ 3.5.x to 3.6. The changes are only for the native directory under the project. + +## Build + +Open the old version of the project with the new version of the engine, and then build the target platform after the upgrade is completed. To avoid upgrade failure, please backup the project first, and then follow the steps below to upgrade. + +## Android + +### File modification + +- Delete file: jni/main.cpp +- android/CMakeLists.txt: delete `${CMAKE_CURRENT_LIST_DIR}/jni/main.cpp` + +### Compile changes + +To reduce the package size, the default value of `CMAKE_C_FLAGS_RELEASE` and `CMAKE_CXX_FLAGS_RELEASE` compiler parameter `visibility` has been changed from `default` to `hidden`. For this change, if the interface is not found in the release version of `jni`, please check if the interface has the declaration `JNIEXPORT` added. For example. + +- Old Code + + ```c++ + void Java_com_google_android_games_paddleboat_GameControllerManager_onMouseConnected + ``` + +- Modified + + ````c++ + JNIEXPORT void JNICALL Java_com_google_android_games_paddleboat_GameControllerManager_onMouseConnected + ```` + +## Code modification + +- Projects with custom jsb interfaces: code related to `NonRefNativePtrCreatedByCtorMap` must be removed + +- Manually written JSB `_finalize` functions should be set to empty, please refer the [Tutorial: JSB 2.0](../../advanced-topics/JSB2.0-learning.md#c++-object-lifecycle-management). + + For example: + + ```c++ + static bool js_cc_gfx_Size_finalize(se::State& s) // NOLINT(readability-identifier-naming) + { + auto iter = se::NonRefNativePtrCreatedByCtorMap::find(SE_THIS_OBJECT(s)); + if (iter != se::NonRefNativePtrCreatedByCtorMap::end()) + { + se::NonRefNativePtrCreatedByCtorMap::erase(iter); + auto* cobj = SE_THIS_OBJECT(s); + JSB_FREE(cobj); + } + return true; + } + SE_BIND_FINALIZE_FUNC(js_cc_gfx_Size_finalize) + ``` + Remove all operations. + ```c++ + static bool js_cc_gfx_Size_finalize(se::State& s) // NOLINT(readability-identifier-naming) + { + return true; + } + SE_BIND_FINALIZE_FUNC(js_cc_gfx_Size_finalize) + ``` + +## Scripting Considerations + +Since the implementation of Native engines differs slightly from that of non-Native engines, developers must be aware of these differences, which are organized as follows. + +- `Readonly`: On Native platforms, properties are fetched as newly created objects in order to reduce memory usage. + + The description of `Readonly` can be found in [Development Notes - ReaOnly](../../scripting/readonly.md#readonly) diff --git a/versions/4.0/en/geometry-renderer/enable-geometry-renderer.png b/versions/4.0/en/geometry-renderer/enable-geometry-renderer.png new file mode 100644 index 0000000000..30942212ec Binary files /dev/null and b/versions/4.0/en/geometry-renderer/enable-geometry-renderer.png differ diff --git a/versions/4.0/en/geometry-renderer/geometry-renderer-demo.png b/versions/4.0/en/geometry-renderer/geometry-renderer-demo.png new file mode 100644 index 0000000000..75b9530656 Binary files /dev/null and b/versions/4.0/en/geometry-renderer/geometry-renderer-demo.png differ diff --git a/versions/4.0/en/geometry-renderer/geometry-renderer-features.png b/versions/4.0/en/geometry-renderer/geometry-renderer-features.png new file mode 100644 index 0000000000..fc5c233abd Binary files /dev/null and b/versions/4.0/en/geometry-renderer/geometry-renderer-features.png differ diff --git a/versions/4.0/en/geometry-renderer/index.md b/versions/4.0/en/geometry-renderer/index.md new file mode 100644 index 0000000000..b4a6307739 --- /dev/null +++ b/versions/4.0/en/geometry-renderer/index.md @@ -0,0 +1,86 @@ +# Geometry Renderer + +Geometry renderer is a functional interface provided by the engine for batch rendering of various geometries, mainly used for debugging (such as displaying the enclosing boxes of objects) and the gizmo batch display of Cocos Creator. + +The effect of the geometry renderer is shown in the following figure. + +![geometry-renderer-demo](./geometry-renderer-demo.png) + +The features of Geometry Renderer is as follows: + +![geometry-renderer-features](./geometry-renderer-features.png) + +Where. +- solid: whether to support solid mode, if not, then display wireframe mode +- depth test: whether to support depth test, if support then the blocked part of the translucent display, not blocked part of the opaque display, if not support then all opaque display +- lighting: whether to support simple lighting, if not, use no light mode +- transform: whether support transform, if support, developer can pass in a transform matrix, the transform matrix will act on the geometry of the vertices, convenient to display the geometry of any coordinate space + +## API + +- Typescript: [geometry-render.ts](https://github.com/cocos/cocos4/blob/v4.0.0/cocos/core/pipeline/geometry-renderer.ts). +- Native: [GeometryRenderer.h](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/renderer/pipeline/GeometryRenderer.h). + +### Supported Geometry Types + +| Geometry Type | Interface | +|:--|:--| +| Dashed Line | addDashedLine | +| Line | addLine | +| Triangle | addTriangle | +| Quad | addQuad | +| BoundingBox | addBoundingBox | +| Cross | addCross | +| Frustum | addFrustum | +| Capsule | addCapsule | +| Cylinder | addCylinder | +| Cone | addCone | +| Circle | addCircle | +| Arc | addArc | +| Polygon | addPolygon | +| Disc | addDisc | +| Sector | addSector | +| Sphere | addSphere | +| Torus | addTorus | +| Octahedron | addOctahedron | +| Bezier curves | addBezier | +| Spline curves with three modes: Folded segments, Multi-segment Bessel curves, Catmull-Rom curves | addSpline | +| Mesh | addMesh | +| Index Based Mesh | addIndexedMesh | + +### Example + +Since the vertex cache is cleared after rendering these geometries per frame, it is necessary to add geometries to the geometry renderer object (located in the camera) per frame in functions like update. + +To use this feature you need to enable **Geometry Renderer** in **Project Settings** -> **Feature Cropping**. + +![enable geometry renderer](enable-geometry-renderer.png) + +> **Note**: Please make sure your project is 3D based. + +The code example is as follows: + +```ts +import { _decorator, Component, Camera, Color } from 'cc'; +const { ccclass, property, executeInEditMode} = _decorator; + +@ccclass('Geometry') +@executeInEditMode(true) +export class Geometry extends Component { + + @property(Camera) + mainCamera:Camera = null; + + start() { + this.mainCamera?.camera.initGeometryRenderer(); + } + + update(deltaTime: number) { + this.mainCamera?.camera?.geometryRenderer?.addCircle(this.node.worldPosition, 1, Color.GREEN, 20); + } +} +``` + +The result is as follows: + +![result](result.png) diff --git a/versions/4.0/en/geometry-renderer/result.png b/versions/4.0/en/geometry-renderer/result.png new file mode 100644 index 0000000000..9e4deb3dd0 Binary files /dev/null and b/versions/4.0/en/geometry-renderer/result.png differ diff --git a/versions/4.0/en/getting-started/attention/index.md b/versions/4.0/en/getting-started/attention/index.md new file mode 100644 index 0000000000..790b1b8899 --- /dev/null +++ b/versions/4.0/en/getting-started/attention/index.md @@ -0,0 +1,11 @@ +## Precautions + +### Syntax format + +__Cocos Creator 3.0__ and __Cocos Creator v2.x__ use a different syntax format. + +__Cocos Creator 3.0__ has full support for __ES6__ and __TypeScript__. Therefore, __Cocos Creator 3.0__ only supports __ES6__ and __TypeScript__ `classes`. In addition, we also support __TypeScript__ syntax hints. + +__Cocos Creator__ supports only __ES5__ `classes`. + +__Please keep this precaution in mind.__ diff --git a/versions/4.0/en/getting-started/dashboard/index.md b/versions/4.0/en/getting-started/dashboard/index.md new file mode 100644 index 0000000000..124fb43eb1 --- /dev/null +++ b/versions/4.0/en/getting-started/dashboard/index.md @@ -0,0 +1,94 @@ +# Using Cocos Dashboard + +Upon launching the Cocos Dashboard and logging in with your Cocos Developer account, the Dashboard interface opens. Dashboard allows downloading the engine, creating a new project, opening an existing project, or getting help information. + +![Dashboard](index/home.png) + +The left navigation menu bar includes: **Home**, **Projects**, **Installs**, **Store**, **Community**, **Learn**, **Settings**, and **Account**. See below for specific instructions. + +## Home + +The home page contains the Cocos product news, editor version news, and user's recently opened projects. Here you can quickly open projects, create new projects, install editors, get the latest editor version, etc. + +![Dashboard](index/home1.png) + +It mainly consists of the following three parts: + +- Carousels: shows the latest Cocos product news. +- Recently Projects: Shows recently opened projects, double-click to open a project; the **New Project** button on the top right quickly opens the New Project page to create a project, and the **View More** button takes you to the project list page. +- Editors: Shows released products of Cocos, where you can quickly install editor versions, create new projects, and get the latest released editor versions. + +## Projects + +The Projects page shows all created/imported projects, **double click** opens the project. The list is empty when you run Cocos Dashboard for the first time, and you can create/import a new project in the top right. + +![Dashboard](index/project.png) + +- **1**: Search for projects directly by project name. +- **2**: Sorts projects in forward/reverse order according to the most recent open time. +- **3**: Select another version of the editor that has been successfully installed to open the project. +- **4**: Operation menu, which can also be opened by right-clicking. Support **Open (project)**, **Show in Finder/Explorer**, **Remove from list**, **Select the icon of the project**, **Rename the project**, **Set the description of the project**: + - **Select the icon of the project**: Customize the project cover image, currently supports four formats: BMP, PNG, GIF and JPG. + - **Rename the project**: Renaming the project name will also rename the project folder. + - **Set the description of project**: Can be used to note descriptive information related to the project. + +### New Project + +Creator provides a number of project templates, including basic architectures for different types of games, as well as sample resources and scripts for learning to help developers get into creative work faster. As Cocos Creator becomes more complete, we will continue to add more project templates. + +Click **Templates** and **Examples** in the upper left to switch the project type. **Editor Version** is used to select the editor version for creating the project, and the drop-down box includes all successfully installed versions. + +![Dashboard](index/add-project.png) + +Check any of the project templates and at the bottom of the page you can see **Project Name** and **Location**: + +- **Name**: The project name can only contain **a-z**, **A-Z**, **0-9** as well as **_** and **-**. +- **Location**: Click the icon behind the Project Path input box to select the project's storage path, or you can manually enter it in the input box. + +Once everything is set up, click the **Create and Open** button to finish creating the project; the Dashboard window will be hidden and the newly created project will open in the main Cocos Creator editor window. + +## Installs + +The Installs page displays the successfully installed product editor, and you can import/install the editor version by clicking the button on the top right. + +![Dashboard](index/installs.png) + +> **Note**: When Cocos Dashboard is first run, this list is empty. Click the two buttons in the top right corner to import existing Creator editors locally or download and install them directly. + +- **Locate**: Used to add locally existing editors, supports dragging and dropping locally existing Creator editors directly from the operating system's file manager into the version list. +- **Install Editor**: Clicking on this button opens the Install Editor page, showing all installed and uninstalled versions of the editor, which can be downloaded and installed as needed. + + ![Download](index/download.png) + +## Store + +Cocos game development resource center, you can browse, download and install official or third-party extensions, source code, resources and so on, specific instructions can refer to the [Cocos Store](https://store.cocos.com/document/en/). + +![store](index/store.png) + +## Community + +This tab is used to post official announcements or events of Cocos Creator, including the **News** and **Update** modules. + +![community](index/community.png) + +## Learn + +The Learn page collects official tutorial resources from Cocos, as well as editor's choice and forum Pinned Topics. + +![learn](index/learn.png) + +## Account + +Displays information about the currently logged in account, the ability to go to the account center, and logging out. + +![account](index/account.png) + +Developers can also visit the official Cocos website for tutorials and other information by clicking the icon below: + +- [Twitter](https://twitter.com/CocosEngine) +- [Facebook](https://www.facebook.com/CocosEngine) +- [Forum](https://discuss.cocos2d-x.org/c/33) +- [Discord](https://discord.com/invite/pVqab4K) +- [YouTube](https://www.youtube.com/cocosengine) +- [GitHub](https://github.com/cocos/cocos4/) diff --git a/versions/4.0/en/getting-started/dashboard/index/account.png b/versions/4.0/en/getting-started/dashboard/index/account.png new file mode 100644 index 0000000000..50f92e2c03 Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/account.png differ diff --git a/versions/4.0/en/getting-started/dashboard/index/add-project.png b/versions/4.0/en/getting-started/dashboard/index/add-project.png new file mode 100644 index 0000000000..8ab0e3edde Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/add-project.png differ diff --git a/versions/4.0/en/getting-started/dashboard/index/community.png b/versions/4.0/en/getting-started/dashboard/index/community.png new file mode 100644 index 0000000000..ad9686643d Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/community.png differ diff --git a/versions/4.0/en/getting-started/dashboard/index/download.png b/versions/4.0/en/getting-started/dashboard/index/download.png new file mode 100644 index 0000000000..19849483d4 Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/download.png differ diff --git a/versions/4.0/en/getting-started/dashboard/index/home.png b/versions/4.0/en/getting-started/dashboard/index/home.png new file mode 100644 index 0000000000..47f9fd587d Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/home.png differ diff --git a/versions/4.0/en/getting-started/dashboard/index/home1.png b/versions/4.0/en/getting-started/dashboard/index/home1.png new file mode 100644 index 0000000000..26c8c3b62a Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/home1.png differ diff --git a/versions/4.0/en/getting-started/dashboard/index/installs.png b/versions/4.0/en/getting-started/dashboard/index/installs.png new file mode 100644 index 0000000000..9fa220c6d6 Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/installs.png differ diff --git a/versions/4.0/en/getting-started/dashboard/index/learn.png b/versions/4.0/en/getting-started/dashboard/index/learn.png new file mode 100644 index 0000000000..10c1baa49b Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/learn.png differ diff --git a/versions/4.0/en/getting-started/dashboard/index/project.png b/versions/4.0/en/getting-started/dashboard/index/project.png new file mode 100644 index 0000000000..2beeae0f8a Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/project.png differ diff --git a/versions/4.0/en/getting-started/dashboard/index/store.png b/versions/4.0/en/getting-started/dashboard/index/store.png new file mode 100644 index 0000000000..2fb9e56eb1 Binary files /dev/null and b/versions/4.0/en/getting-started/dashboard/index/store.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/40-on-y.png b/versions/4.0/en/getting-started/first-game-2d/images/40-on-y.png new file mode 100644 index 0000000000..1527286f03 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/40-on-y.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/add-animation.png b/versions/4.0/en/getting-started/first-game-2d/images/add-animation.png new file mode 100644 index 0000000000..b9a85fb04e Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/add-animation.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/add-keyframes.gif b/versions/4.0/en/getting-started/first-game-2d/images/add-keyframes.gif new file mode 100644 index 0000000000..959649c47d Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/add-keyframes.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/add-player-controller.gif b/versions/4.0/en/getting-started/first-game-2d/images/add-player-controller.gif new file mode 100644 index 0000000000..44566e934b Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/add-player-controller.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/add-position-track.png b/versions/4.0/en/getting-started/first-game-2d/images/add-position-track.png new file mode 100644 index 0000000000..9d3c20c6dc Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/add-position-track.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/after-layer-setting.gif b/versions/4.0/en/getting-started/first-game-2d/images/after-layer-setting.gif new file mode 100644 index 0000000000..04814334f5 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/after-layer-setting.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/assign-body-anim.gif b/versions/4.0/en/getting-started/first-game-2d/images/assign-body-anim.gif new file mode 100644 index 0000000000..04e0bc4040 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/assign-body-anim.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/assign-box-prefab.gif b/versions/4.0/en/getting-started/first-game-2d/images/assign-box-prefab.gif new file mode 100644 index 0000000000..89467804fb Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/assign-box-prefab.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/assign-clip.gif b/versions/4.0/en/getting-started/first-game-2d/images/assign-clip.gif new file mode 100644 index 0000000000..ec4eaf5721 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/assign-clip.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/bind-manager.png b/versions/4.0/en/getting-started/first-game-2d/images/bind-manager.png new file mode 100644 index 0000000000..c1346f519e Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/bind-manager.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/box-layer.png b/versions/4.0/en/getting-started/first-game-2d/images/box-layer.png new file mode 100644 index 0000000000..4e33b59b69 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/box-layer.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/cavans-camera.png b/versions/4.0/en/getting-started/first-game-2d/images/cavans-camera.png new file mode 100644 index 0000000000..38c451d8b8 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/cavans-camera.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/click-event.gif b/versions/4.0/en/getting-started/first-game-2d/images/click-event.gif new file mode 100644 index 0000000000..dfa7beb7d9 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/click-event.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/click-event.gif.gif b/versions/4.0/en/getting-started/first-game-2d/images/click-event.gif.gif new file mode 100644 index 0000000000..f2dbd6faae Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/click-event.gif.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-2d-empty.png b/versions/4.0/en/getting-started/first-game-2d/images/create-2d-empty.png new file mode 100644 index 0000000000..5fd75021d1 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-2d-empty.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-bg.gif b/versions/4.0/en/getting-started/first-game-2d/images/create-bg.gif new file mode 100644 index 0000000000..224aeeda9e Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-bg.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-body.gif b/versions/4.0/en/getting-started/first-game-2d/images/create-body.gif new file mode 100644 index 0000000000..ef9fcbd914 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-body.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-box-prefab.gif b/versions/4.0/en/getting-started/first-game-2d/images/create-box-prefab.gif new file mode 100644 index 0000000000..b9219f67c4 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-box-prefab.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-box.png b/versions/4.0/en/getting-started/first-game-2d/images/create-box.png new file mode 100644 index 0000000000..e5c77f69a1 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-box.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-clip-onestep.gif b/versions/4.0/en/getting-started/first-game-2d/images/create-clip-onestep.gif new file mode 100644 index 0000000000..3dab75afff Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-clip-onestep.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-fist-script.png b/versions/4.0/en/getting-started/first-game-2d/images/create-fist-script.png new file mode 100644 index 0000000000..017cad1339 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-fist-script.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-game-manager.png b/versions/4.0/en/getting-started/first-game-2d/images/create-game-manager.png new file mode 100644 index 0000000000..cf4bc1bab1 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-game-manager.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-player.gif b/versions/4.0/en/getting-started/first-game-2d/images/create-player.gif new file mode 100644 index 0000000000..1e093cf010 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-player.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-scripts.gif b/versions/4.0/en/getting-started/first-game-2d/images/create-scripts.gif new file mode 100644 index 0000000000..bc4388c7c0 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-scripts.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-sprite.gif b/versions/4.0/en/getting-started/first-game-2d/images/create-sprite.gif new file mode 100644 index 0000000000..d2a1169d39 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-sprite.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-sprite.png b/versions/4.0/en/getting-started/first-game-2d/images/create-sprite.png new file mode 100644 index 0000000000..556d5e9b98 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-sprite.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-start-menu.png b/versions/4.0/en/getting-started/first-game-2d/images/create-start-menu.png new file mode 100644 index 0000000000..223d097f78 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-start-menu.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-tip.png b/versions/4.0/en/getting-started/first-game-2d/images/create-tip.png new file mode 100644 index 0000000000..6fe17d3f8d Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-tip.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-title.png b/versions/4.0/en/getting-started/first-game-2d/images/create-title.png new file mode 100644 index 0000000000..f92e851d1e Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-title.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-twostep.gif b/versions/4.0/en/getting-started/first-game-2d/images/create-twostep.gif new file mode 100644 index 0000000000..8ef64b1d2b Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-twostep.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/create-ui-canvas.png b/versions/4.0/en/getting-started/first-game-2d/images/create-ui-canvas.png new file mode 100644 index 0000000000..253fdb4094 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/create-ui-canvas.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/download-editor.png b/versions/4.0/en/getting-started/first-game-2d/images/download-editor.png new file mode 100644 index 0000000000..da4b9b54ac Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/download-editor.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/enter-anim-editing-mode.png b/versions/4.0/en/getting-started/first-game-2d/images/enter-anim-editing-mode.png new file mode 100644 index 0000000000..d883e38f51 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/enter-anim-editing-mode.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/gen-road.png b/versions/4.0/en/getting-started/first-game-2d/images/gen-road.png new file mode 100644 index 0000000000..ffb0698839 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/gen-road.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/jumptime-with-duration.gif b/versions/4.0/en/getting-started/first-game-2d/images/jumptime-with-duration.gif new file mode 100644 index 0000000000..daac617871 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/jumptime-with-duration.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/layer-default.png b/versions/4.0/en/getting-started/first-game-2d/images/layer-default.png new file mode 100644 index 0000000000..3309f5e20e Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/layer-default.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/layer-error.png b/versions/4.0/en/getting-started/first-game-2d/images/layer-error.png new file mode 100644 index 0000000000..030f6d4113 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/layer-error.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/preview-anim.gif b/versions/4.0/en/getting-started/first-game-2d/images/preview-anim.gif new file mode 100644 index 0000000000..638dfae475 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/preview-anim.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/preview-menu.png b/versions/4.0/en/getting-started/first-game-2d/images/preview-menu.png new file mode 100644 index 0000000000..720237c7ea Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/preview-menu.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/preview-oneStep.gif b/versions/4.0/en/getting-started/first-game-2d/images/preview-oneStep.gif new file mode 100644 index 0000000000..7b0d6242f6 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/preview-oneStep.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/projects.png b/versions/4.0/en/getting-started/first-game-2d/images/projects.png new file mode 100644 index 0000000000..67d24d345e Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/projects.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/save-prefab.png b/versions/4.0/en/getting-started/first-game-2d/images/save-prefab.png new file mode 100644 index 0000000000..96a1aa8e7b Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/save-prefab.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/save-scene.png b/versions/4.0/en/getting-started/first-game-2d/images/save-scene.png new file mode 100644 index 0000000000..13cc9541a1 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/save-scene.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/saved-scene.png b/versions/4.0/en/getting-started/first-game-2d/images/saved-scene.png new file mode 100644 index 0000000000..046f908f42 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/saved-scene.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/scene-dir.png b/versions/4.0/en/getting-started/first-game-2d/images/scene-dir.png new file mode 100644 index 0000000000..2adf590e57 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/scene-dir.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/scene.png b/versions/4.0/en/getting-started/first-game-2d/images/scene.png new file mode 100644 index 0000000000..25ae7f80b0 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/scene.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/scroll.gif b/versions/4.0/en/getting-started/first-game-2d/images/scroll.gif new file mode 100644 index 0000000000..4fae1fe790 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/scroll.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/setup-scroll.gif b/versions/4.0/en/getting-started/first-game-2d/images/setup-scroll.gif new file mode 100644 index 0000000000..37db457cca Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/setup-scroll.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/start-game-without-result.gif b/versions/4.0/en/getting-started/first-game-2d/images/start-game-without-result.gif new file mode 100644 index 0000000000..4d504eb771 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/start-game-without-result.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/step.png b/versions/4.0/en/getting-started/first-game-2d/images/step.png new file mode 100644 index 0000000000..e027d29b62 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/step.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/to-red.png b/versions/4.0/en/getting-started/first-game-2d/images/to-red.png new file mode 100644 index 0000000000..fd01b478b1 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/to-red.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/ui-canvas.png b/versions/4.0/en/getting-started/first-game-2d/images/ui-canvas.png new file mode 100644 index 0000000000..c58ab38791 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/ui-canvas.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/uicanvas-camera.png b/versions/4.0/en/getting-started/first-game-2d/images/uicanvas-camera.png new file mode 100644 index 0000000000..2fdb71eff7 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/uicanvas-camera.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/with-scale.gif b/versions/4.0/en/getting-started/first-game-2d/images/with-scale.gif new file mode 100644 index 0000000000..ed7408564d Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/with-scale.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/images/without-scale.gif b/versions/4.0/en/getting-started/first-game-2d/images/without-scale.gif new file mode 100644 index 0000000000..e0935b59f1 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/images/without-scale.gif differ diff --git a/versions/4.0/en/getting-started/first-game-2d/index.md b/versions/4.0/en/getting-started/first-game-2d/index.md new file mode 100644 index 0000000000..b5be8f9301 --- /dev/null +++ b/versions/4.0/en/getting-started/first-game-2d/index.md @@ -0,0 +1,1679 @@ +# Quick Start: Making Your First 2D Game + +Platform jumping games are a very common and popular game genre, ranging from simple games on the original NES console to large-scale games made with complex 3D technology on modern gaming platforms. You can always find platform jumping games. + +In this section, we will demonstrate how to use the 2D features provided by Cocos Creator to create a simple platform jumping game. + +## Environment Setup + +### Download Cocos Dashboard + +Visit the [Cocos Creator official website](https://www.cocos.com/creator-download) to download the latest version of Cocos Dashboard, which allows for unified management of the Cocos Creator versions and your projects. After installing, open the Cocos Dashboard. + +![dashboard](images/projects.png) + +### Install Cocos Creator + +![download-editor](images/download-editor.png) + +In the **Editor** tab, click on the Install button for the desired version to install the editor, Cocos Creator. + +> We usually recommend using the latest version of Cocos Creator to get started. it will obtain more features and support. + +### Create Project + +In the **Project** tab, find the **Create** button, select **Empty(2D)**. + +![create-2d-project](images/create-2d-empty.png) + +Next, just enter your project name in the highlighted field shown in the image above. + +For example, you could input: cocos-tutorial-mind-your-step-2d. + +Let's make a game that is similar to the one in [Quick Start: Making Your First 3D Game](../first-game/index.md). + +If you haven't read [Quick Start: Making Your First 3D Game](../first-game/index.md) yet, that's okay. In this section, we'll assume you haven't used Cocos Creator before, and we'll start from scratch! + +Without further ado, let's get started. + +## Create the Character + +In a 2D game, all visible objects are made of images, including the character. + +To keep things simple, we'll be using the images that come bundled with Cocos Creator to create our game. These images can be found in `internal/default_ui/`. + +In Cocos Creator, we use a `Sprite` node ( a node with a Sprite component ) to show an image. + +To create a new node of type Sprite, right-click in the Hierarchy panel, select "Create", and then choose "2D -> Sprite" from the pop-up menu. + +Right-click in the Hierarchy and select "Create" from the pop-up menu, and we can see different types of nodes. Here, we select "2D -> Sprite" to create a new `Sprite` node. + +![create-sprite.png](images/create-sprite.png) + +Next, we find the `internal/default_ui/default_btn_normal` and assign it to the `Sprite Frame` property of the `Sprite` node we just created. + +![create-sprite.gif](images/create-sprite.gif) + +Next, create an `Empty` node and name it "Player": + +![create-player.gif](images/create-player.gif) + +If you didn't name the node when you created it, there are two ways to change its name: + +- In the **Inspector** panel, find the name and rename it +- In the **Hierarchy**, select the node then press `F2` + +We can adjust the parent-child relationship of nodes by dragging and dropping them in the Hierarchy. Here, drag the `Sprite` node onto the `Player` node to make it a child, then rename the `Sprite` node to "Body". + +![create-body.gif](./images/create-body.gif) + +> **Note**: +> - The hierarchy relationship determines the rendering order. if may cause the node to not be visible when the order is wrong. +> - 2D/UI elements must be under a `Canvas` node in order to be visible. +> - The layer of 2D/UI elements must be set to `UI_2D` + +Next, Let's adjust the position's Y of the `Body` node to 40: + +![40-on-y.png](images/40-on-y.png) + +Finally, let's adjust the color of the `Body`. + +In the Inspector panel, find the `Color` property and expand it, then set the color to red. + +![to-red.png](images/to-red.png) + +## The First Script + +Scripts, also known as code, are used to implement game logic, such as character movement, jumping, and other gameplay mechanics. + +Cocos Creator uses TypeScript as its scripting programming language. It has simple and easy-to-learn syntax, a large user base, and widespread applications. You can encounter it in web development, app development, game development, etc. + +Creating a script component in Cocos Creator is very simple. All you need to do is right-click in the Assets Manager window, and select "Create -> TypeScript -> NewComponent" option. + + + +For ease of management, it is often recommended to create a folder named `Script` to put all of your scripts in. + +Next, right-click on the `Scripts` folder, and create a new script component named `PlayerController` to control the player. + +![create-scripts.gif](images/create-scripts.gif) + +The engine will generate the following code for the script component that we just created. + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('PlayerController') +export class PlayerController extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} +``` + +**Note** Cocos Creator uses a Node + Component architecture, meaning that a component must be attached to a node in order to function. Scripts in Cocos Creator are also designed as components. + +So, let's drag the `PlayerController` script onto the Inspector of Player node. + +![add-player-controller.gif](images/add-player-controller.gif) + +You should see that a `PlayerController` component has been added to the Player node. + +> **Note**: You can also click on the **Add Component** button to add different types of components. + +## Map + +A map in a game is an area where your character can move around and interact within a game. + +As mentioned before, all visible objects in 2D games are made up of images. The map is no exception. + +Just like the steps we used to create the `Body` node, we will now create an object called `Box` which will be used to construct the map. + +- Right-click in the Hierarchy +- Create a `Sprite` node by selecting "Create -> 2D Objects -> Sprite" through the pop-up menu. +- Name it as "Box" +- Select the "Box" node, set its **Sprite Frame** property by using `internal/default_ui/default_btn_normal` + +![create-box.png](images/create-box.png) + +### Prefab + +A prefab is a special type of resource that can save the information of a node as a file. so that it can be reused in other situations. + +In Cocos Creator, creating a prefab is quite simple. We just need to drag the node into the Assets Manager window, a *.prefab file will be automatically generated. + +Now, let's create a folder named Prefab in the Assets Manager window, which will be used to organize all prefabs together. + +Then, find the Box node and drag it to the Prefab folder, a prefab file named "Box" will be generated. + +The box node in the hierarchy can be deleted, because it won't be used when the game is running. Instead, we will create nodes in the script using Box.prefab to build the game map during gameplay. + +![create-box-prefab.gif](images/create-box-prefab.gif) + +> **Tips:** Generally, We will use different folders to manage different types of resources. It's a good habit to keep your project well-organized. + +### Scene + +In the game engine, a scene is used to manage all game objects. It contains characters, maps, gameplay, UI. you name it in a game. + +A game can be divided into different scenes based on its functionalities. Such as the loading-scene, start-menu-scene, gameplay-scene, etc. + +A game requires at least one scene to start. + +So, in Cocos Creator, an unsaved empty scene will be opened by default, just like the one we are currently editing. + +To ensure that we can find this scene the next time we open Cocos Creator, we need to save it. + +First, let's create a folder named "Scene" to save our scenes in the Assets Manager window. + +![scene-dir.png](images/scene-dir.png) + +Then, press the Ctrl + S shortcut key. + +Since it is the first time we are saving this scene, the scene-saving window will pop up. + +We choose the "Scene" folder that we just created as the location, and name it "game.scene". Click save. + +![save-scene.png](images/save-scene.png) + +Now the scene is saved. We can see a scene resource file named "game" under the assets/Scene folder in the Assets Manager window. + +![saved-scene.png](images/saved-scene.png) + +The scene now can be observed as below, the red block represents the player, and the white represents the ground. + +![scene.png](images/scene.png) + +> Don't forget to press Ctrl + S shortcut key to save your scene when there are changes to the scene. Avoid losing work progress due to unexpected events such as power outages. + +## Make the character move + +We have created the "Player" node before, but it can not move. + +Next, we will add code and animations to control its movement and make it move. + +### PlayerController + +The player should have the following behaviors: + +- it starts jumping when the mouse is clicked. +- When it has been jumping for a certain amount of time, the jumping ends. + +To achieve the above goals, we need to add some methods in the `PlayerController` component. + +- Listen for mouse-click events + + ```ts + onMouseUp(event: EventMouse) {} + ``` + +- Jump according to the given steps + + ```ts + jumpByStep(step: number) {} + ``` + +- Calculate the position of the player + + ```ts + update (deltaTime: number) {} + ``` + +Next, let's complete these methods. + +#### Listen for mouse-click events + +Cocos Creator supports various common control devices, such as the mouse, keyboard, touchpad, and gamepad. You can easily access the relevant content through `Input` class. + +For ease of use, Cocos Creator provides a global instance object `input` for the `Input` class. + +> **Note** It's easy to confuse, `input` is the instance, and `Input` is the class. + +To make the `onMouseUp` method be called by the engine when the mouse is clicked, we need to add the following code to the `start` method. + +```ts +start () { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); +} +``` + +The `onMouseUp` method has an `event` parameter of type `EventMouse`. + +Through the `event.getButton()` method, we can get which button of the mouse is clicked. + +Add the following code to the `onMouseUp` method: + +```ts +onMouseUp(event: EventMouse) { + if (event.getButton() === EventMouse.BUTTON_LEFT) { + this.jumpByStep(1); + } else if (event.getButton() === EventMouse.BUTTON_RIGHT) { + this.jumpByStep(2); + } +} +``` + +In the `EventMouse` class, there are three values have been defined: +- public static BUTTON_LEFT = 0; +- public static BUTTON_MIDDLE = 1; +- public static BUTTON_RIGHT = 2; + +The code has implemented that: +- When the left mouse button is clicked, the player jumps forward one step. +- When the right mouse button is clicked, the player jumps forward two steps. + +#### Move the player + +In our game, the player moves horizontally to the right, so we need to use a simple physics formula as below: + +```txt +P_1 = P_0 + v*t +``` + +Where `P_1` is the final position, `P_0` is the original position, v is the speed of the object, and t is the unit time. + +> Final Position = Original Position + Speed * deltaTime + +The `update` function in the PlayerController component will be automatically called by the game engine. And also pass in a `deltaTime` parameter. + +```ts +update (deltaTime: number) {} +``` + +The times that `update` will be called per second is determined by the frame rate (also known as FPS) when the game running. + +For example, if a game is running at 30 FPS, the `deltaTime` will be 1.0 / 30.0 = 0.03333333... second. + +In game development, we use `deltaTime` as the `t` in the physics formula to ensure consistent movement results at any frame rate. + +Here, let's add some properties needed for calculating player movement `PlayerController` component. + +```ts +//used to judge if the player is jumping. +private _startJump: boolean = false; + +//the number of steps will the player jump, should be 1 or 2. determined by which mouse button is clicked. +private _jumpStep: number = 0; + +//the time it takes for the player to jump once. +private _jumpTime: number = 0.1; + +//the time that the player's current jump action has taken, should be set to 0 each time the player jumps, when it reaches the value of `_jumpTime`, the jump action is completed. +private _curJumpTime: number = 0; + +// The player's current vertical speed, used to calculate the Y value of position when jumping. +private _curJumpSpeed: number = 0; + +// The current position of the player, used as the original position in the physics formula. +private _curPos: Vec3 = new Vec3(); + +//movement calculated by deltaTime. +private _deltaPos: Vec3 = new Vec3(0, 0, 0); + +// store the final position of the player, when the player's jumping action ends, it will be used directly to avoid cumulative errors. +private _targetPos: Vec3 = new Vec3(); +``` + +Now, what we need to do next is very simple: + +- Calculating the data needed for player movement in the `jumpByStep` method. +- Processing player movement in the `update` method. + +In the `jumpByStep` method, we add the following code: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + //if the player is jumping, do nothing. + return; + } + //mark player is jumping. + this._startJump = true; + //record the number of steps the jumping action will take. + this._jumpStep = step; + //set to 0 when a new jumping action starts + this._curJumpTime = 0; + //because the player will finish the jumping action in the fixed duration(_jumpTime), so it needs to calculate jump speed here. + this._curJumpSpeed = this._jumpStep / this._jumpTime; + //copy the current position of the node which will be used when calculating the movement. + this.node.getPosition(this._curPos); + //calculate the final position of the node which will be used when the jumping action ends. + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); +} +``` + +`Vec3` is the vector class in Cocos Creator, the name is short for `Vector3`, which has 3 components, x,y,z. all vector operations are placed in `Vec3` class. Such as `Vec3.add`, `Vec3.subtract` etc. + +In Cocos Creator, 2D games also use `Vec3` as the property type for position, scale, and rotation. Just ignore the irrelevant components, e.g. the z component in position. + +Next, let's calculate the movement of the player while jumping. + +In this game, the player only moves when jumping, and keeps still when not jumping. + +Let's add the following code to the `update` method in `PlayerController`. + +```ts +update (deltaTime: number) { + //we only do something when the player is jumping. + if (this._startJump) { + //accumulate the jumping time. + this._curJumpTime += deltaTime; + //check if it reaches the jump time. + if (this._curJumpTime > this._jumpTime) { + // When the jump ends, set the player's position to the target position. + this.node.setPosition(this._targetPos); + //clear jump state + this._startJump = false; + } else { + //if it still needs to move. + // copy the position of the node. + this.node.getPosition(this._curPos); + //calculate the offset x by using deltaTime and jumping speed. + this._deltaPos.x = this._curJumpSpeed * deltaTime; + //calculate the final pos by adding deltaPos to the original position + Vec3.add(this._curPos, this._curPos, this._deltaPos); + //update the position of the player. + this.node.setPosition(this._curPos); + } + } +} +``` + +Now, click the **Preview** button at the top of Cocos Creator. + +![preview-menu.png](images/preview-menu.png) + +The **Player** will move by clicking the mouse buttons. + +![without-scale.gif](images/without-scale.gif) + +As you can see, the player only moves a little each time you click the mouse button. + +This is because we use `pixels/s` as the unit of speed for the Player. + +```ts +this._curJumpSpeed = this._jumpStep / this._jumpTime; +``` + +The above code indicates that the player will only move one pixel per step. + +In fact, we want the Player to move a certain distance per step. + +To fix this, we need to add a constant to express the step size. + +Below, the `BLOCK_SIZE` is used for this purpose. + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +// +export const BLOCK_SIZE = 40; + +@ccclass('PlayerController') +export class PlayerController extends Component { + //... +} +``` + +As you can see, in TypeScript: +- A constant can be defined outside of the class and exported separately. +- Values declared as const cannot be modified and are often used for fixed configurations. + +Next, find the code line in `jumpByStep` method: + +```ts +this._curJumpSpeed = this._jumpStep / this._jumpTime; +``` + +Change it to: + +```ts +this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; +``` + +Here is the updated `jumpByStep`: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); +} +``` + +Restart the game, and you can see that the distance of the player's movement is as expected now. + +![with-scale.gif](images/with-scale.gif) + +At this moment, the code of `PlayerController` is as follows. + +```ts +import { _decorator, Component, Vec3, EventMouse, input, Input } from "cc"; +const { ccclass, property } = _decorator; + +export const BLOCK_SIZE = 40; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + private _startJump: boolean = false; + private _jumpStep: number = 0; + private _curJumpTime: number = 0; + private _jumpTime: number = 0.3; + private _curJumpSpeed: number = 0; + private _curPos: Vec3 = new Vec3(); + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + private _targetPos: Vec3 = new Vec3(); + + start () { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + reset() { + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); + } + + update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` + +### Player Animation + +For 2D game development, Cocos Creator supports various types of animation, including keyframe animation, Spine, DragonBones, and Live2D. + +In this tutorial, the jump animation of the Player is very simple and it is enough to use keyframe animation. + +Using Cocos Creator's built-in animation editor, it's easy to make it. + +Let's take a step-by-step approach to creating it. + +First, let's add the Animation component to the Body node of the Player. + +![add-animation.png](images/add-animation.png) + +In the Assets Manager window, create a new folder named "Animation", Inside that folder, create a new AnimationClip named "oneStep". + +![create-clip-onestep.gif](images/create-clip-onestep.gif) + +In the Hierarchy, select the "Body" node and drag "oneStep" from the Animation folder onto the "Clips" property in the Inspector panel. + +![assign-clip.gif](images/assign-clip.gif) + +In the editor console area, switch to the "Animation" tab and click the "Enter animation editing mode" button: + +![enter-anim-editing-mode.png](images/enter-anim-editing-mode.png) + +In the animation editor, we add a track for the node's position property. + +![add-position-track.png](images/add-position-track.png) + +After adding the track, we can set the indicator of the current frame to a certain frame and then change the position of the node, the current frame will be set to be a keyframe automatically. + +> Both modify the value on the Inspector panel and dragging the node in the scene can change the position of a node. + +![add-keyframes.gif](images/add-keyframes.gif) + +Finally, we have the following keyframes: + +- 0 frame:set position to x = 0, y = 40 +- 10 frame: set position to x = 0, y = 120 +- 20 frame: set position to x = 0, y = 40 + +> Don't forget to click the **Save** button to save it. + +You can click the **Play** button to preview the animation clip. + +![preview-oneStep.gif](images/preview-oneStep.gif) + +Follow the steps of making `oneStep` animation, and make another one: `twoStep`. + +![create-twostep.gif](images/create-twostep.gif) + +After completing the animation creation, click the **Close** button to exit the Animation editing mode. + +### Play animations in code + +Next, let's add some code lines into PlayerController to play the animation we've just made. + +Playing an animation using TypeScript in Cocos Creator is quite simple: + +```ts +animation.play(animName); +``` + +- animation is the Animation component on Body node. +- play is the method of Animation component to play animation +- animName is the name of an animation file that you want to play + +> In Cocos Creator, we must ensure the animation which will be played is included in the clips of the node's Animation component, + +Add the following code at the beginning of the PlayerController class: + +```ts +@ccclass("PlayerController") +export class PlayerController extends Component { + @property(Animation) + BodyAnim:Animation = null; + //... +} +``` + +> **Note**:The TypeScript and Cocos Creator both have an Animation class, please make sure the `Animation` is included in the code line `import { ..., Animation,... } from "cc" `. Otherwise, the code will use the `Animation` from TypeScript, and unpredictable errors may occur. + +Here we added a property named `BodyAnim` and added `@property` above it. This syntax is called: [Decorator](../../scripting/decorator.md). The `@property` decorator allows the editor to be aware of the type of `BodyAnim` and display the exported properties of the Animation component on the Inspector panel. + +To make sure there is a code line in your PlayerController file as below, or the code will fail to compile. + +```ts +const { ccclass, property } = _decorator; +``` + +Here `_decorator` is a class containing all of the decorators that can be used in Cocos Creator, it should be imported from namespace cc before using it. + +The related code lines are as follows: + +```ts +import { _decorator, Component, Vec3, EventMouse, input, Input, Animation } from "cc"; +const { ccclass, property } = _decorator; + +``` + +In `jumpByStep` method, we add to the following code lines: + +```ts +if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } +} +``` + +Now, the `jumpByStep` method is like this: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); + + //the code can explain itself + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } +} +``` + +Back to the Cocos Creator, select **Player** node, and then drag the **Body** node on the `BodyAnim` property. + +![assign-body-anim.gif](images/assign-body-anim.gif) + +The engine will automatically get the Animation component on the Body node and assign it to `BodyAnim`. As a result, the `PlayerController`'s `BodyAnim` property references the `Animation` component of **Body** node. + +Hit **Play** button at the top of Cocos Creator to preview, you can see the **Player** jumps while clicking the mouse buttons. + +![preview-anim.gif](images/preview-anim.gif) + +Because of using the unified. + +Here we use a unified jumpTime value, `jumpTime = 0.1`, But since the duration of the two animations is not the same, you can find it a little weird when animations are played. + +To solve this, it's better to use the real duration of the animations as the value of `jumpTime`. + +```ts +// Get jump time from animation duration. +const clipName = step == 1? 'oneStep' : 'twoStep'; +const state = this.BodyAnim.getState(clipName); +this._jumpTime = state.duration; +``` + +![jumptime-with-duration.gif](images/jumptime-with-duration.gif) + +## GameManager + +In game development, we can manually place nodes using Box.prefab to build the map, but the map will be fixed. To make the map change whenever the game starts and provide some surprises for the players, we can randomly build the map in code. + +Now, let's create a new TypeScript component called `GameManger` in the Assets Manager window to achieve this. + +> **Note**:If you forget to rename the script or input the wrong name you don't want to use when creating a script component, The best way to fix it is to delete it and create a new one. +> **Note**:If you modify the name of a script, the content in the script file will not change accordingly. + +After creating the `GameManger` script component, let's create a new node named **GameManager**, then attach `GameManager` to it. + +> **Note** Generally, we can attach the `GameManager` script component to any node in the scene, but for keeping the project structure well-organized, we usually create a node with the same name and attach the `GameManager` to it. This rule applies to all XXXManager script components. + +![create-game-manager.png](images/create-game-manager.png) + +To build the map, we will use the `Box.prefab` to create the nodes. + +So, the first thing we need to do is to add a property to the `GameManager` class for referencing the `Box.prefab`. + +Now, the content of the `GameManager` class is as follows: + +```ts +import { _decorator, Component, Prefab } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('GameManager') +export class GameManager extends Component { + + @property({type: Prefab}) + public boxPrefab: Prefab|null = null; + + start(){} + + update(dt: number): void { + + } +} +``` + +Go back to the Cocos Creator, select the **GameManager** node, and drag `Box` prefab onto the `boxPrefab` property of **GameManager** node. + +![assign-box-prefab.gif](images/assign-box-prefab.gif) + +The map in this game is made up of two types of blocks. the two types of blocks alternate to form the map. + +- None: an empty block, if the Player steps on a block of this type, the game is over. +- Stone: The Player can stand on. + +To make the code more understandable, we often use `enum` to define the types of objects. + +We define an enum named `BlockType` which has two elements as below. + +```ts +enum BlockType{ + BT_NONE, + BT_STONE, +}; +``` + + > In TypeScript, if the first element of an enum hasn't been given a value, it will take 0 as default. Here, `BT_NONE = 0`, `BT_STONE = 1`. + +In the following code, you can see how we use it. + +We put it above the definition of GameManager class, and without giving it an `export`. As a result, it only can be used in this single file. + +Next, it is needed to determine where to place a new block. We add a property named `roadLength` to record the length of the road made up of the blocks. + +To manage all the types of blocks we have created, we add the private property `_road` of type Array to store the generated block types. + +Now, the code of the `GameManager` is as follows: + +```ts +import { _decorator, CCInteger, Component, Prefab } from 'cc'; +const { ccclass, property } = _decorator; + +enum BlockType{ + BT_NONE, + BT_STONE, +}; + +@ccclass('GameManager') +export class GameManager extends Component { + + @property({type: Prefab}) + public boxPrefab: Prefab|null = null; + @property({type: CCInteger}) + public roadLength: number = 50; + private _road: BlockType[] = []; + + start() { + + } +} +``` + +The flow of constructing the map is as follows: + +- Clear all data when the game starts +- The type of the first block is always `BlockType.BT_STONE` to prevent the Player from falling off. +- The type of a block after a block with the type of `BlockType.BT_NONE` should always be `BlockType.BT_STONE`. + +Next, let's add the following method to `GameManger`. + +- Method to generate the map: + + ```ts + generateRoad() { + + this.node.removeAllChildren(); + + this._road = []; + // startPos + this._road.push(BlockType.BT_STONE); + + for (let i = 1; i < this.roadLength; i++) { + if (this._road[i - 1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + for (let j = 0; j < this._road.length; j++) { + let block: Node | null = this.spawnBlockByType(this._road[j]); + if (block) { + this.node.addChild(block); + block.setPosition(j * BLOCK_SIZE, 0, 0); + } + } + } + ``` + + >`Math.floor`: rounds down and returns the largest integer less than or equal to a given number. refer to [Math.floor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor) for more detail. + >`Math.random`:returns a floating-point in a range of [0.0,1.0), refer to [Math.random](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) for more detail. + + Obviously, the code `Math.floor(Math.random() * 2)` will only produce two integer numbers, 0 or 1, which are exactly correspond to the value of `BT_NONE` and `BT_STONE` declared in the `BlockType` enum. + +- Create a new block by the given type: + + ```ts + spawnBlockByType(type: BlockType) { + if (!this.boxPrefab) { + return null; + } + + let block: Node|null = null; + switch(type) { + case BlockType.BT_STONE: + block = instantiate(this.boxPrefab); + break; + } + + return block; + } + ``` + + If the given type is `BT_STONE`, we create a new block from `boxPrefab` using `instantiate` method. + + If the given type is `BT_NONE`, we just do nothing. + + > `instantiate`: is a built-in method provided by Cocos Creator, it is used for making a copy of an existing node and creating a new instance for a prefab. + +Let's call `generateRoad` in the `start` method of `GameManager`: + +```ts +start() { + this.generateRoad() +} +``` + +You can see the generated map when running the game. + +![gen-road.png](images/gen-road.png) + +## Camera Follow + +In a game that has a movable player, we often let the camera follow the player. As a result, you can see the screen scrolling when the player is moving. + +It's very simple to achieve it in Cocos Creator. Just make the following changes. + +1. Select the Canvas node, and uncheck the **Align Canvas With Screen** property of cc.Canvas component on the Inspector panel. + +2. Drag the Camera node on the Player node, and make it a child node. + +![setup-scroll.gif](./images/setup-scroll.gif) + +Now, run the game, and you can see the camera is following the player. + +![scroll.gif](images/scroll.gif) + +## UI layout + +UI ( User Interface ) is a very important part of most games. it displays information about the game and allows users to interact with the game systems. + +As we mentioned before, In Cocos Creator, all 2D elements should be put directly or indirectly under the Canvas node, or they will not be rendered. + +In Cocos Creator, UI is a special collection of 2D elements, they are text, buttons, toggles, etc. + +As 2D elements, they also need to be put under the Canvas node. + +As we know, UI elements are always fixed on the screen, so we need a fixed camera to render them. + +In the previous section, the camera of the Canvas has been changed to follow our Player, it is no longer suitable for UI rendering. + +Thus, we need to create a new Canvas for UI. + +### UICanvas + +In the Hierarchy, right lick the scene root and select "Create -> UI Component -> Canvas" in the pop-up menu. + +![create-ui-canvas.png](images/create-ui-canvas.png) + +Name it "UICanvas". + +![ui-canvas.png](images/ui-canvas.png) + +Create an empty node named **StartMenu** under the UICanvas. + +Then, create a button node under the **StartMenu** node, you can find there is a node named 'Label' under the button node. Select it and set the String property to 'Play'. + +Now, we have made a 'Play' button. + +![create-start-menu.png](images/create-start-menu.png) + +### Background & Text + +Next, let's add a background and text to tell users how to play this game. + +Create a Sprite node under the 'StartMenu' node and name it 'Bg'. + +Assign `internal/default_ui/default_panel` to the `Sprite Frame` property of 'Bg' node. + +Set the value of `Type` property to `SLICED`. + +Set the `Content Size` of `UITransform` to a certain value (e.g. 400,250) . + +![create-bg.gif](images/create-bg.gif) + +Create a new Label node named 'Title' under the 'StartMenu' node, and set the properties as below: +- position: 0,80 +- cc.Label Color: black +- cc.Label String: Mind Your Step 2D +- cc.Label Font Size:40 + +![create-title.png](images/create-title.png) + +Continue creating some `Label` nodes to describe the gameplay. Name them 'Tip'. + +![create-tip.png](images/create-tip.png) + +Create a `Label` node **under UICanvas**, and name it 'Step', to show how many steps the player has taken. + +![step.png](images/step.png) + +Now, we have completed the UI layout, let's write some code to finish the game logic. + +## Game state + +There are 3 states in most games. +- **INIT**: game is ready to start +- **PLAYING**: game is playing +- **END**: game is over, will restart or exit + +We can define these states using an enum type as below: + +```ts +enum GameState{ + GS_INIT, + GS_PLAYING, + GS_END, +}; +``` + +For better readability, let's put it after the `BlockType` enum. + +Let's add a `setCurState` method to `GameManger`, which will be used to control the state of the game. + +The code is as follows. + +```ts +setCurState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + break; + case GameState.GS_PLAYING: + break; + case GameState.GS_END: + break; + } +} +``` + +Add a new method named `init` to initialize game data. + +```ts +init() { + //to do something +} +``` + +Then, call it in `setCurState` when the game state is set to `GameState.GS_INIT`. + +```ts +setCurState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + break; + case GameState.GS_END: + break; + } +} +``` + +As designed, the **Player** only can be controlled by users when the game is running. + +So, we make a small change to the input event listener in the `PlayerController`. + +The input event is no longer listening in the `start` method, instead, we create a new method named `setInputActive` to handle it. the `setInputActive` method will be called when needed. + +```ts +start () { + +} + +setInputActive(active: boolean) { + if (active) { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } else { + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } +} +``` + +Here, the code of `GameManager` is like this: + +```ts +import { _decorator, CCInteger, Component, instantiate, Node, Prefab } from 'cc'; +import { BLOCK_SIZE, PlayerController } from './PlayerController'; +const { ccclass, property } = _decorator; + +enum BlockType{ + BT_NONE, + BT_STONE, +}; + +enum GameState{ + GS_INIT, + GS_PLAYING, + GS_END, +}; + +@ccclass('GameManager') +export class GameManager extends Component { + + @property({type: Prefab}) + public boxPrefab: Prefab|null = null; + @property({type: CCInteger}) + public roadLength: number = 50; + private _road: BlockType[] = []; + + start() { + } + + init() { + } + + setCurState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + + break; + case GameState.GS_END: + break; + } + } + + generateRoad() { + + this.node.removeAllChildren(); + + this._road = []; + // startPos + this._road.push(BlockType.BT_STONE); + + for (let i = 1; i < this.roadLength; i++) { + if (this._road[i - 1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + for (let j = 0; j < this._road.length; j++) { + let block: Node | null = this.spawnBlockByType(this._road[j]); + if (block) { + this.node.addChild(block); + block.setPosition(j * BLOCK_SIZE, 0, 0); + } + } + } + + spawnBlockByType(type: BlockType) { + if (!this.boxPrefab) { + return null; + } + + let block: Node|null = null; + switch(type) { + case BlockType.BT_STONE: + block = instantiate(this.boxPrefab); + break; + } + + return block; + } +} +``` + +Next, let's add the logic code. + +### Game Start + +This is not a state, but we must start from here. When the game is launched, the `start` method of `GameManager` will be called. + +We call `setCurState` here to initialize the game. + +```ts + start(){ + this.setCurState(GameState.GS_INIT); + } +``` + +### GS_INIT + +In this game state, we should initialize the map, reset the position of the player, show the game UI, etc. + +So, we need to add the needed properties to `GameManager. + +```ts +// References to the startMenu node. +@property({ type: Node }) +public startMenu: Node | null = null; + +//references to player +@property({ type: PlayerController }) +public playerCtrl: PlayerController | null = null; + +//references to UICanvas/Steps node. +@property({type: Label}) +public stepsLabel: Label|null = null; +``` + +In the `init` method, we add code lines as below: + +```ts +init() { + //show the start menu + if (this.startMenu) { + this.startMenu.active = true; + } + + //generate the map + this.generateRoad(); + + + if (this.playerCtrl) { + + //disable input + this.playerCtrl.setInputActive(false); + + //reset player data. + this.playerCtrl.node.setPosition(Vec3.ZERO); + this.playerCtrl.reset(); + } +} +``` + +### Handle Button Click Event + +Next, let's implement when users click the 'Play' button on the UI, the game starts playing. + +Add a new method named `onStartButtonClicked` to the `GameManager` class, which is used to handle the click event of 'Play' button on the 'startMenu` node. + +In `onStartButtonClicked`, we just call `setCurState` to set the game state to `GameState.GS_PLAYING`. + +```ts +onStartButtonClicked() { + this.setCurState(GameState.GS_PLAYING); +} +``` + +Go back to Cocos Creator, and select the `UICanvas/StartMenu/Button` node. + +On the Inspector panel, type `1` into the input box after `Click Events` property. + +Then drag the `GameManager` node to the first slot, select `GameManager` for the second slot, and choose `onStartButtonClicked` for the third slot. + +![click-event.gif](images/click-event.gif) + +### GS_PLAYING + +After users click the 'Play' button, the game is going to this state. We need to: + +- Hide the StartMenu +- Reset the number of steps +- Enable user input + +The related code in `setCurState` method is as below: + +```ts +setCurState(value: GameState) { + switch (value) { + //... + case GameState.GS_PLAYING: + if (this.startMenu) { + this.startMenu.active = false; + } + + //reset steps counter to 0 + if (this.stepsLabel) { + this.stepsLabel.string = '0'; + } + + //enable user input after 0.1 second. + setTimeout(() => { + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + break; + //... + } +} +``` + +### GS_END + +We do nothing for now. you can add anything you want to make the game perfect. + +### Bind properties + +Go back to Cocos Creator, and drag the corresponding node to each property for `GameManager`. + +![bind-manager.png](images/bind-manager.png) + +Look! We can play it now. +> #### Note: If you find that the UI appears duplicated after running the game, please refer to the `Layers & Visibility` section of this article to resolve the issue. + +![start-game-without-result.gif](./images/start-game-without-result.gif) + +## Game Over + +Next, let's handle the situation when the player steps on an empty block. + +### Handle jump end + +Add a new property called `_curMoveIndex` to `PlayerController`, which is used to record how many steps the player has taken. + +```ts +private _curMoveIndex: number = 0; +``` + +Set it to 0 in the `reset` method. + +```ts +reset() { + this._curMoveIndex = 0; + this.node.getPosition(this._curPos); + this._targetPos.set(0,0,0); +} +``` + +In the `jumpByStep` method, increase it by `step`. + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); + + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } + + this._curMoveIndex += step; +} +``` + +Add `onOnceJumpEnd` to `PlayerController` to emit an 'JumpEnd' event and pass in `_curMoveIndex` as a parameter. + +```ts +onOnceJumpEnd() { + this.node.emit('JumpEnd', this._curMoveIndex); +} +``` + +Call `onOnceJumpEnd` in the `update` of `PlayerController` when the jump action is over. + +```ts +update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + this.onOnceJumpEnd(); + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } +} +``` + +Go back to `GameManager` and add the following code. + +- Add `onPlayerJumpEnd` method to handle the jump end event. + + ```ts + onPlayerJumpEnd(moveIndex: number) { + + } + ``` + +- Listen 'JumpEnd' event in the `start` method. + + ```ts + start() { + this.setCurState(GameState.GS_INIT); + this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this); + } + ``` + + > In Cocos Creator, an event dispatched through the `emit` of a node can only be listened to by using its `on`. + +- Add `checkResult` to check the type of block the player steps on. + + ```ts + checkResult(moveIndex: number) { + if (moveIndex < this.roadLength) { + if (this._road[moveIndex] == BlockType.BT_NONE) { //steps on empty block, reset to init. + this.setCurState(GameState.GS_INIT); + } + } else { //out of map, reset to init. + this.setCurState(GameState.GS_INIT); + } + } + ``` + +- Finish the `onPlayerJumpEnd` method. + + ```ts + onPlayerJumpEnd(moveIndex: number) { + //update steps label. + if (this.stepsLabel) { + this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex); + } + this.checkResult(moveIndex); + } + ``` + +## Layers & Visibility + +When playing the game, you may notice the overlapping graphics, this is because the both cameras ( Canvas/Camera, UICanvas/Camera) are rendering all objects. + +![layer-error.png](images/layer-error.png) + +In Cocos Creator, a node can only be put in one of the layers, a camera can choose which layers will be rendered by itself. + +To solve this problem, we need to allocate the role of the layers and the visibility of cameras. + +In this game, we have two types of objects. +- Scene Object: player, blocks +- UI Object: windows, buttons, labels + +So, we just need to put all of the scene objects to `DEFAULT` layer and put all of the UI objects to `UI_2D` layer. + +Then, we need to change a little about the visibility of cameras to let the `Canvas/Camera` only render the objects in `DEFAULT` layer, the `UICanvas/Camera` only render the object in `UI_2D` layer, and everything will be ok. + +It's so clear, now, let's do it. + +## DEFAULT + +- Set the layer of Canvas **and all its children** to `DEFAULT`: + + ![layer-default.png](images/layer-default.png) + +- Set the layer of `Box.prefab` to `DEFAULT`: + + ![box-layer.png](images/box-layer.png) + + Double-click the left mouse button on the prefab file to enter the prefab editing mode, don't forget to hit the 'Save' button after finishing the modification. + + ![save-prefab.png](images/save-prefab.png) + +- Set the **Visibility** of `Canvas/Player/Camera` as follows: + + ![canvas-camera.png](images/cavans-camera.png) + +## UI_2D + +- Set the **Visibility** of `UICanvas/Camera` as follows: + + ![images/uicanvas-camera.png](images/uicanvas-camera.png) + + Since the default layer for 2D nodes is `UI_2D`, we don't need to set the layer for nodes under `UICanvas`. + +Play the game again, everything is ok now. + +![after-layer-setting.gif](images/after-layer-setting.gif) + +## Summary + +Here we come to the end of this tutorial, hope it has been helpful to you. + +In the future, you can add more gameplay and features based on this game, such as replacing the Player with an animation role, adding a beautiful background image, adding rhythmic background music and sounds, etc. + +If you have any questions, please refer to [Get Help and Support](../support.md). + +## Full Source Code + +PlayerController.ts: + +```ts +import { _decorator, Component, Vec3, EventMouse, input, Input, Animation } from "cc"; +const { ccclass, property } = _decorator; + +export const BLOCK_SIZE = 40; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + @property(Animation) + BodyAnim:Animation = null; + + private _startJump: boolean = false; + private _jumpStep: number = 0; + private _curJumpTime: number = 0; + private _jumpTime: number = 0.1; + private _curJumpSpeed: number = 0; + private _curPos: Vec3 = new Vec3(); + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + private _targetPos: Vec3 = new Vec3(); + private _curMoveIndex: number = 0; + start () { + //input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + setInputActive(active: boolean) { + if (active) { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } else { + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + } + + reset() { + this._curMoveIndex = 0; + this.node.getPosition(this._curPos); + this._targetPos.set(0,0,0); + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + + // get jump time from animation duration. + const clipName = step == 1? 'oneStep' : 'twoStep'; + const state = this.BodyAnim.getState(clipName); + this._jumpTime = state.duration; + + + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); + + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } + + this._curMoveIndex += step; + } + + + onOnceJumpEnd() { + this.node.emit('JumpEnd', this._curMoveIndex); + } + + update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + this.onOnceJumpEnd(); + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` + +GameManager.ts: + +```ts +import { _decorator, CCInteger, Component, instantiate, Label, Node, Prefab, Vec3 } from 'cc'; +import { BLOCK_SIZE, PlayerController } from './PlayerController'; +const { ccclass, property } = _decorator; + +enum BlockType { + BT_NONE, + BT_STONE, +}; + +enum GameState { + GS_INIT, + GS_PLAYING, + GS_END, +}; + +@ccclass('GameManager') +export class GameManager extends Component { + + @property({ type: Prefab }) + public boxPrefab: Prefab | null = null; + @property({ type: CCInteger }) + public roadLength: number = 50; + private _road: BlockType[] = []; + + @property({ type: Node }) + public startMenu: Node | null = null; + @property({ type: PlayerController }) + public playerCtrl: PlayerController | null = null; + @property({type: Label}) + public stepsLabel: Label|null = null; + + start() { + this.setCurState(GameState.GS_INIT); + this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this); + } + + init() { + if (this.startMenu) { + this.startMenu.active = true; + } + + this.generateRoad(); + + if (this.playerCtrl) { + this.playerCtrl.setInputActive(false); + this.playerCtrl.node.setPosition(Vec3.ZERO); + this.playerCtrl.reset(); + } + } + + setCurState(value: GameState) { + switch (value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + if (this.startMenu) { + this.startMenu.active = false; + } + + if (this.stepsLabel) { + this.stepsLabel.string = '0'; + } + + setTimeout(() => { + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + break; + case GameState.GS_END: + break; + } + } + + generateRoad() { + + this.node.removeAllChildren(); + + this._road = []; + // startPos + this._road.push(BlockType.BT_STONE); + + for (let i = 1; i < this.roadLength; i++) { + if (this._road[i - 1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + for (let j = 0; j < this._road.length; j++) { + let block: Node | null = this.spawnBlockByType(this._road[j]); + if (block) { + this.node.addChild(block); + block.setPosition(j * BLOCK_SIZE, 0, 0); + } + } + } + + spawnBlockByType(type: BlockType) { + if (!this.boxPrefab) { + return null; + } + + let block: Node | null = null; + switch (type) { + case BlockType.BT_STONE: + block = instantiate(this.boxPrefab); + break; + } + + return block; + } + + onStartButtonClicked() { + this.setCurState(GameState.GS_PLAYING); + } + + checkResult(moveIndex: number) { + if (moveIndex < this.roadLength) { + if (this._road[moveIndex] == BlockType.BT_NONE) { + this.setCurState(GameState.GS_INIT); + } + } else { + this.setCurState(GameState.GS_INIT); + } + } + + onPlayerJumpEnd(moveIndex: number) { + if (this.stepsLabel) { + this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex); + } + this.checkResult(moveIndex); + } + +} + +``` diff --git a/versions/4.0/en/getting-started/first-game-2d/touch.md b/versions/4.0/en/getting-started/first-game-2d/touch.md new file mode 100644 index 0000000000..f3117b260a --- /dev/null +++ b/versions/4.0/en/getting-started/first-game-2d/touch.md @@ -0,0 +1,203 @@ +# Handle Touch Events + +If we want our game to run on mobile devices, we need to handle touch events instead of mouse-clicked events, since the latter are only fired on PCs. + +In Cocos Creator, we can handle touch events from the screen by listening to `Input.EventType.TOUCH_START` event. + +There are two ways can be used. + +- handle all touch events from the screen using`input.on` +- listen to touch events that occur within the content size of a node by using the `node.on` method + +We partition the screen into two sides, when users touch the left side, the player moves one step, when they touch the right side, the player moves two steps. It's better to use `node.on` in this situation. + +Next, let's take a look at how to accomplish it. + +Under the `UICanvas` node, create two nodes and name them `LeftTouch` and `RightTouch`. + +![create-touch.png](touch/create-touch.png) + +Set their positions and content size as shown below. + +![left-touch.png](touch/left-touch.png) + +![right-touch.png](touch/right-touch.png) + +Back to `PlayerController`, add the following properties for referencing the touch nodes. + +```ts +@property(Node) +leftTouch: Node = null; + +@property(Node) +rightTouch: Node = null; +``` + +Select the `Player` node and then drag the nodes created before onto the corresponding properties added above. + +![config-touch-nodes.png](touch/config-touch-nodes.png) + +In `PlayerController`, add the following code. + +- Listen touch events: + + ```ts + setInputActive(active: boolean) { + if (active) { + //for pc + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + //for mobile + this.leftTouch.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + this.rightTouch.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + } else { + //for pc + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + //for mobile + this.leftTouch.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + this.rightTouch.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + } + ``` + +- Add the method for handling touch events + + ```ts + onTouchStart(event: EventTouch) { + const target = event.target as Node; + if (target?.name == 'LeftTouch') { + this.jumpByStep(1); + } else { + this.jumpByStep(2); + } + } + ``` + + `target` is of type `any`, so we need to use `as` to convert it into the `Node` type. + + After obtaining the touched target node, we can determine whether it is touching on the left or right side based on its name. + +Now, you can run the game on your phone by using the browser to scan the QR code on the Cocos Creator operation bar shown as below. + +> **Note**: your computer running Cocos Creator and your mobile devices must be connected to the same LAN. + +![qr-code.png](./touch/qr-code.png) + +The complete code of `PlayerController` is shown below. + +```ts +import { _decorator, Component, Vec3, EventMouse, input, Input, Animation, EventTouch, Node } from "cc"; +const { ccclass, property } = _decorator; + +export const BLOCK_SIZE = 40; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + @property(Animation) + BodyAnim: Animation = null; + + private _startJump: boolean = false; + private _jumpStep: number = 0; + private _curJumpTime: number = 0; + private _jumpTime: number = 0.3; + private _curJumpSpeed: number = 0; + private _curPos: Vec3 = new Vec3(); + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + private _targetPos: Vec3 = new Vec3(); + private _curMoveIndex = 0; + + @property(Node) + leftTouch: Node = null; + + @property(Node) + rightTouch: Node = null; + + start() { + //input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + setInputActive(active: boolean) { + if (active) { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + this.leftTouch.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + this.rightTouch.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + } else { + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + this.leftTouch.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + this.rightTouch.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + } + + reset() { + this._curMoveIndex = 0; + } + + onTouchStart(event: EventTouch) { + const target = event.target as Node; + if (target?.name == 'LeftTouch') { + this.jumpByStep(1); + } else { + this.jumpByStep(2); + } + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + + const clipName = step == 1 ? 'oneStep' : 'twoStep'; + const state = this.BodyAnim.getState(clipName); + this._jumpTime = state.duration; + + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep * BLOCK_SIZE, 0, 0)); + + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } + + this._curMoveIndex += step; + } + + + onOnceJumpEnd() { + this.node.emit('JumpEnd', this._curMoveIndex); + } + + update(deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + this.onOnceJumpEnd(); + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` diff --git a/versions/4.0/en/getting-started/first-game-2d/touch/config-touch-nodes.png b/versions/4.0/en/getting-started/first-game-2d/touch/config-touch-nodes.png new file mode 100644 index 0000000000..e17e6da77b Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/touch/config-touch-nodes.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/touch/create-touch.png b/versions/4.0/en/getting-started/first-game-2d/touch/create-touch.png new file mode 100644 index 0000000000..00ea30da62 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/touch/create-touch.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/touch/left-touch.png b/versions/4.0/en/getting-started/first-game-2d/touch/left-touch.png new file mode 100644 index 0000000000..95eb7743dc Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/touch/left-touch.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/touch/qr-code.png b/versions/4.0/en/getting-started/first-game-2d/touch/qr-code.png new file mode 100644 index 0000000000..94ea7d80f3 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/touch/qr-code.png differ diff --git a/versions/4.0/en/getting-started/first-game-2d/touch/right-touch.png b/versions/4.0/en/getting-started/first-game-2d/touch/right-touch.png new file mode 100644 index 0000000000..88c7515a77 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game-2d/touch/right-touch.png differ diff --git a/versions/4.0/en/getting-started/first-game/advance.md b/versions/4.0/en/getting-started/first-game/advance.md new file mode 100644 index 0000000000..18f38f934c --- /dev/null +++ b/versions/4.0/en/getting-started/first-game/advance.md @@ -0,0 +1,448 @@ +# Advanced: Add Light, Shadow and Skeleton Animation + +In this section, we will show you how to use third-part assets such as animation, etc to complete the game prototype we have made in [Quick Start: Make Your First 3D Game](index.md) + +## Lighting and Shadows + +Lighting and shadows are important rendering features that describe the game. With light sources and shadows, we can simulate a more realistic game world, providing better immersion and empathy. + +Next, we add a simple shadow to the character. + +### Turn on shadows + +1. Click the top `Scene` node in the **Hierarchy** panel, then in the **Inspector** panel check Enabled in `shadows` and modify the **Distance** and **Normal** properties to: + + ![planar shadows](./images/planarShadows.png) + +2. Click on the Body node under the Player node and set **ShadowCastingMode** to **ON** in the `cc.MeshRenderer` component. + + ![model shadow](./images/model-shadow.png) + +At this point, you will see a shadow sheet in the **Scene Editor**. The preview will show that the shadow is not visible because it is directly behind the model, covered by the capsule. + +![player shadow](./images/player-shadow-scene.png) + +### Adjusting lighting + +By default, a **Main Light** node with `cc.DirectionalLight` component is added when creating a new scene. So to make the shadows appear in a different position, we can adjust the direction of this light. Click in the **Hierarchy** panel to select the **Main Light** node and adjust the `Rotation` property to (-10, 17, 0). + +![main light](./images/main-light.png) + +The shadow effect can be seen by clicking on the preview at + +![player shadow preview](./images/player-shadow-preview.png) + +## Adding a protagonist model + +### Importing model resources + +Importing models, materials, animations and other resources from the original sources is not the focus of this basic tutorial, so we will directly use the resources that have been imported into the project. Put **project** ([GitHub](https://github.com/cocos-creator/tutorial-mind-your-step-3d) in the assets directory and copy the cocos folder to the assets directory of your own project. + +### Adding to the scene + +A Prefab named Cocos is already included in the Cocos file. Drag it to the Body node under the Player node in the **Hierarchy** panel as a child node of the Body node. + +![add cocos prefab](./images/add-cocos-prefab.png) + +Also, remove the original capsule model from the **Inspector** panel: + +![remove capsule](./images/remove-capsule.png) + +You can add a Spotlight under the Cocos node to highlight its shiny head. + +![add cocos light](./images/cocos-add-light.png) + +### Add jump animation + +Now the preview shows that the main character will have a standby animation initially, but it will look very inconsistent to use this standby animation when jumping, so we can change it to a jump animation during jumping. Add a variable to the `PlayerController.ts` class that references the model animation: + +```ts +@property({type: SkeletalAnimation}) +public CocosAnim: SkeletalAnimation|null = null; +``` + +Also, since we have changed the protagonist from a capsule body to a character model, we can discard the animation previously created for the capsule body and comment on the relevant code as follows: + +```ts +// @property({type: Animation}) +// public BodyAnim: Animation|null = null; + +jumpByStep(step: number) { + // ... + // if (this.BodyAnim) { + // if (step === 1) { + // this.BodyAnim.play('oneStep'); + // } else if (step === 2) { + // this.BodyAnim.play('twoStep'); + // } + // } +} +``` + +Then in the **Hierarchy** panel drag and drop the Cocos node into the `CocosAnim` property box of the Player node: + +![assign cocos prefab](./images/assign-cocos-prefab.png) + +Play the jump animation in the `jumpByStep` function of the `PlayerController` script: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); + + if (this.CocosAnim) { + this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; // Jump animation time is relatively long, here to speed up the playback + this.CocosAnim.play('cocos_anim_jump'); // Play jump animation + } + + // if (this.BodyAnim) { + // if (step === 1) { + // this.BodyAnim.play('oneStep'); + // } else if (step === 2) { + // this.BodyAnim.play('twoStep'); + // } + // } + + this._curMoveIndex += step; +} +``` + +Here `_jumpStep` time is 0.3 seconds, if the animation length and `_jumpStep` don't match, it may cause the following problem: + +- The animation transition is not smooth when the animation is not finished yet +- or the animation is finished but the jump time is not yet up, resulting in slippage + +One way to handle this is to recalculate the speed of the animation directly from the length of the animation clip and `_jumpStep` instead of using a constant: `_jumpStep`: + +```ts +var state = this.CocosAnim.getState('cocos_anim_jump'); +state.speed = state.duration/this._jumpTime; +``` + +Developers can try it themselves, or manually modify `_jumpStep` and `speed` to the right values to control the game's pace. + +In the `OnceJumpEnd` function of the `PlayerController` script, make the main character standby and play the standby animation. + +```ts +onOnceJumpEnd() { + if (this.CocosAnim) { + this.CocosAnim.play('cocos_anim_idle'); + } + this.node.emit('JumpEnd', this._curMoveIndex); +} +``` + +> **Note**: `onOnceJumpEnd` will be triggered when the jump is completed, see the `update` function in `PlayerController.ts` for details. +> +The preview effect is as follows: + +![cocos play](./images/cocos-play.gif) + +## Final Code + +**PlayerController.ts** + +```ts +import { _decorator, Component, Vec3, input, Input, EventMouse, Animation, SkeletalAnimation } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + @property({type: Animation}) + public BodyAnim: Animation|null = null; + @property({type: SkeletalAnimation}) + public CocosAnim: SkeletalAnimation|null = null; + + // for fake tween + private _startJump: boolean = false; + private _jumpStep: number = 0; + private _curJumpTime: number = 0; + private _jumpTime: number = 0.3; + private _curJumpSpeed: number = 0; + private _curPos: Vec3 = new Vec3(); + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + private _targetPos: Vec3 = new Vec3(); + private _curMoveIndex = 0; + + start () { + } + + reset() { + this._curMoveIndex = 0; + } + + setInputActive(active: boolean) { + if (active) { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } else { + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); + + if (this.CocosAnim) { + this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; // Jump animation time is relatively long, here to speed up the playback + this.CocosAnim.play('cocos_anim_jump'); // Play jump animation + } + + // if (this.BodyAnim) { + // if (step === 1) { + // this.BodyAnim.play('oneStep'); + // } else if (step === 2) { + // this.BodyAnim.play('twoStep'); + // } + // } + + this._curMoveIndex += step; + } + + onOnceJumpEnd() { + if (this.CocosAnim) { + this.CocosAnim.play('cocos_anim_idle'); + } + + this.node.emit('JumpEnd', this._curMoveIndex); + } + + update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + this.onOnceJumpEnd(); + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` + +**GameManager.ts** + +```ts +import { _decorator, Component, Prefab, instantiate, Node, Label, CCInteger, Vec3 } from 'cc'; +import { PlayerController } from "./PlayerController"; +const { ccclass, property } = _decorator; + +// The runway type, pit (BT_NONE) or solid road (BT_STONE) +enum BlockType{ + BT_NONE, + BT_STONE, +}; + +enum GameState{ + GS_INIT, + GS_PLAYING, + GS_END, +}; + +@ccclass("GameManager") +export class GameManager extends Component { + + // The runway prefab + @property({type: Prefab}) + public cubePrfb: Prefab | null = null; + // Length of the road + @property({type: CCInteger}) + public roadLength: Number = 50; + private _road: BlockType[] = []; + // Node of the start menu + @property({type: Node}) + public startMenu: Node | null = null; + // The reference of the PlayerController instance on the Player node + @property({type: PlayerController}) + public playerCtrl: PlayerController | null = null; + // Label to display the step + @property({type: Label}) + public stepsLabel: Label | null = null!; + + start () { + this.curState = GameState.GS_INIT; + this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this); + } + + init() { + // Active the start menu + if (this.startMenu) { + this.startMenu.active = true; + } + // Generate the runway + this.generateRoad(); + if(this.playerCtrl){ + // Disable user input + this.playerCtrl.setInputActive(false); + // Reset the player's position + this.playerCtrl.node.setPosition(Vec3.ZERO); + // Reset the steps + this.playerCtrl.reset(); + } + } + + set curState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + if (this.startMenu) { + this.startMenu.active = false; + } + + if (this.stepsLabel) { + this.stepsLabel.string = '0'; // Reset the number of steps to 0 + } + // What happens is that the character already starts moving at the moment the game starts + // Therefore, a delay is needed here + setTimeout(() => { + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + break; + case GameState.GS_END: + break; + } + } + + generateRoad() { + // Prevent the track from being the old track when the game is restarted + // Therefore, the old track needs to be removed and the old track data cleared + this.node.removeAllChildren(); + this._road = []; + // Make sure that the character is standing on the real road when the game is running + this._road.push(BlockType.BT_STONE); + + // Determine the type of track for each frame + for (let i = 1; i < this.roadLength; i++) { + // If the last track is a pit, then this frame must not be a pit + if (this._road[i-1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + // Generate tracks based on track type + let linkedBlocks = 0; + for (let j = 0; j < this._road.length; j++) { + if(this._road[j]) { + ++linkedBlocks; + } + if(this._road[j] == 0) { + if(linkedBlocks > 0) { + this.spawnBlockByCount(j - 1, linkedBlocks); + linkedBlocks = 0; + } + } + if(this._road.length == j + 1) { + if(linkedBlocks > 0) { + this.spawnBlockByCount(j, linkedBlocks); + linkedBlocks = 0; + } + } + } + } + + spawnBlockByCount(lastPos: number, count: number) { + let block: Node|null = this.spawnBlockByType(BlockType.BT_STONE); + if(block) { + this.node.addChild(block); + block?.setScale(count, 1, 1); + block?.setPosition(lastPos - (count - 1) * 0.5, -1.5, 0); + } + } + spawnBlockByType(type: BlockType) { + if (!this.cubePrfb) { + return null; + } + + let block: Node|null = null; + switch(type) { + case BlockType.BT_STONE: + block = instantiate(this.cubePrfb); + break; + } + + return block; + } + + onStartButtonClicked() { + // To start the game by clicking the Play button + this.curState = GameState.GS_PLAYING; + } + + checkResult(moveIndex: number) { + if (moveIndex < this.roadLength) { + // Jumped on the pit + if (this._road[moveIndex] == BlockType.BT_NONE) { + this.curState = GameState.GS_INIT; + } + } else { // Jumped over the maximum length + this.curState = GameState.GS_INIT; + } + } + + onPlayerJumpEnd(moveIndex: number) { + if (this.stepsLabel) { + // Because in the last step there may be a jump with a large pace, but at this time, whether the jump is a large pace or a small pace should not increase the score more + this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex); + } + // Check the type of the currently falling road and get the result + this.checkResult(moveIndex); + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } +} +``` + +## Conclusion + +Congratulations on completing your first game made with Cocos Creator! You can download the full project at [GitHub - Mind-Your-Step-3D](https://github.com/cocos-creator/tutorial-mind-your-step-3d). We hope this quick start tutorial will help you understand the basic concepts and workflow of the Cocos Creator game development process. If you are not interested in writing and learning to script, you can also copy the script directly from the finished project. + +Next, you can continue to improve all aspects of the game, here are some recommended improvements: +- Add difficulty to the game, when the character stays in the same place for 1 second it is considered a failure +- Change the runway to infinite, dynamically delete the runway that has been run and extend the runway behind. +- Add game sound effects +- Add an end menu for the game, count the number of jumps and the time spent by the player +- Replace characters and runways with more beautiful resources +- Add some pickup items to guide the player to "make mistakes" +- Add some particle effects, such as trailing when the character is moving, dust when it hits the ground +- Add two buttons for touchscreen devices instead of left and right mouse buttons + +In addition, if you want to publish your finished game to the server and share it with your friends, you can read the [Cross-Platform Game Publishing](../../editor/publish/index.md) section. diff --git a/versions/4.0/en/getting-started/first-game/images/2d-view.png b/versions/4.0/en/getting-started/first-game/images/2d-view.png new file mode 100644 index 0000000000..d07cb8ce34 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/2d-view.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/add-animation-from-assets.gif b/versions/4.0/en/getting-started/first-game/images/add-animation-from-assets.gif new file mode 100644 index 0000000000..bc1cd0836e Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/add-animation-from-assets.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/add-animation.gif b/versions/4.0/en/getting-started/first-game/images/add-animation.gif new file mode 100644 index 0000000000..b360b5b13e Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/add-animation.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/add-cocos-prefab.png b/versions/4.0/en/getting-started/first-game/images/add-cocos-prefab.png new file mode 100644 index 0000000000..87619e8ff1 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/add-cocos-prefab.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/add-ground-base.png b/versions/4.0/en/getting-started/first-game/images/add-ground-base.png new file mode 100644 index 0000000000..92e74efdf9 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/add-ground-base.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/add-keyframe.gif b/versions/4.0/en/getting-started/first-game/images/add-keyframe.gif new file mode 100644 index 0000000000..5c2ed5d241 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/add-keyframe.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/add-label-title.gif b/versions/4.0/en/getting-started/first-game/images/add-label-title.gif new file mode 100644 index 0000000000..c13788d4c2 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/add-label-title.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/add-player-controller.png b/versions/4.0/en/getting-started/first-game/images/add-player-controller.png new file mode 100644 index 0000000000..ea9759e4be Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/add-player-controller.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/add-steps-to-game-manager.png b/versions/4.0/en/getting-started/first-game/images/add-steps-to-game-manager.png new file mode 100644 index 0000000000..57e864dfd9 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/add-steps-to-game-manager.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/assign-cocos-prefab.png b/versions/4.0/en/getting-started/first-game/images/assign-cocos-prefab.png new file mode 100644 index 0000000000..85c103828f Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/assign-cocos-prefab.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/assign-cube-prefab.png b/versions/4.0/en/getting-started/first-game/images/assign-cube-prefab.png new file mode 100644 index 0000000000..72068ab151 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/assign-cube-prefab.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/camera-setting.png b/versions/4.0/en/getting-started/first-game/images/camera-setting.png new file mode 100644 index 0000000000..0f5fc9a5ea Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/camera-setting.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/change-spriteFrame.png b/versions/4.0/en/getting-started/first-game/images/change-spriteFrame.png new file mode 100644 index 0000000000..4053ba21a0 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/change-spriteFrame.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/cocos-add-light.png b/versions/4.0/en/getting-started/first-game/images/cocos-add-light.png new file mode 100644 index 0000000000..f0146933bd Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/cocos-add-light.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/cocos-play.gif b/versions/4.0/en/getting-started/first-game/images/cocos-play.gif new file mode 100644 index 0000000000..0d829a51ca Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/cocos-play.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-bg-sprite.gif b/versions/4.0/en/getting-started/first-game/images/create-bg-sprite.gif new file mode 100644 index 0000000000..fcefda381c Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-bg-sprite.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-button.gif b/versions/4.0/en/getting-started/first-game/images/create-button.gif new file mode 100644 index 0000000000..e41a65d52b Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-button.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-cube-prefab.gif b/versions/4.0/en/getting-started/first-game/images/create-cube-prefab.gif new file mode 100644 index 0000000000..7c35aa6f71 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-cube-prefab.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-cube.gif b/versions/4.0/en/getting-started/first-game/images/create-cube.gif new file mode 100644 index 0000000000..f163df010f Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-cube.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-folder.png b/versions/4.0/en/getting-started/first-game/images/create-folder.png new file mode 100644 index 0000000000..fdfd12e71a Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-folder.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-player-script.gif b/versions/4.0/en/getting-started/first-game/images/create-player-script.gif new file mode 100644 index 0000000000..e75ad1abda Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-player-script.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-player-script.png b/versions/4.0/en/getting-started/first-game/images/create-player-script.png new file mode 100644 index 0000000000..63e9ea4788 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-player-script.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-player.gif b/versions/4.0/en/getting-started/first-game/images/create-player.gif new file mode 100644 index 0000000000..0754205ded Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-player.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-player.png b/versions/4.0/en/getting-started/first-game/images/create-player.png new file mode 100644 index 0000000000..7a4941aacf Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-player.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-project.png b/versions/4.0/en/getting-started/first-game/images/create-project.png new file mode 100644 index 0000000000..914b6f9613 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-project.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/create-scene.png b/versions/4.0/en/getting-started/first-game/images/create-scene.png new file mode 100644 index 0000000000..e307de90fc Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/create-scene.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/drag-camera-to-player.gif b/versions/4.0/en/getting-started/first-game/images/drag-camera-to-player.gif new file mode 100644 index 0000000000..0da4c53eda Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/drag-camera-to-player.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/drag-to-animComp.gif b/versions/4.0/en/getting-started/first-game/images/drag-to-animComp.gif new file mode 100644 index 0000000000..e9696b5f8a Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/drag-to-animComp.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/edit-second-clip.png b/versions/4.0/en/getting-started/first-game/images/edit-second-clip.png new file mode 100644 index 0000000000..19ae8d7339 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/edit-second-clip.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/game-manager-player.png b/versions/4.0/en/getting-started/first-game/images/game-manager-player.png new file mode 100644 index 0000000000..63390d17fe Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/game-manager-player.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/hierarchy.png b/versions/4.0/en/getting-started/first-game/images/hierarchy.png new file mode 100644 index 0000000000..8bb03eba5a Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/hierarchy.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/main-light.png b/versions/4.0/en/getting-started/first-game/images/main-light.png new file mode 100644 index 0000000000..83f594862d Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/main-light.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/main-window.png b/versions/4.0/en/getting-started/first-game/images/main-window.png new file mode 100644 index 0000000000..c1325dae0c Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/main-window.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/model-shadow.png b/versions/4.0/en/getting-started/first-game/images/model-shadow.png new file mode 100644 index 0000000000..5810745dfa Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/model-shadow.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/planarShadows.png b/versions/4.0/en/getting-started/first-game/images/planarShadows.png new file mode 100644 index 0000000000..6e9ee1972a Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/planarShadows.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/play-button-inspector.png b/versions/4.0/en/getting-started/first-game/images/play-button-inspector.png new file mode 100644 index 0000000000..03028061b3 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/play-button-inspector.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/play.png b/versions/4.0/en/getting-started/first-game/images/play.png new file mode 100644 index 0000000000..35a468cf9f Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/play.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/player-move.gif b/versions/4.0/en/getting-started/first-game/images/player-move.gif new file mode 100644 index 0000000000..5d235463ae Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/player-move.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/player-shadow-preview.png b/versions/4.0/en/getting-started/first-game/images/player-shadow-preview.png new file mode 100644 index 0000000000..c7cf7b9491 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/player-shadow-preview.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/player-shadow-scene.png b/versions/4.0/en/getting-started/first-game/images/player-shadow-scene.png new file mode 100644 index 0000000000..455545762d Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/player-shadow-scene.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/preview-with-jump.gif b/versions/4.0/en/getting-started/first-game/images/preview-with-jump.gif new file mode 100644 index 0000000000..bc92c7659e Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/preview-with-jump.gif differ diff --git a/versions/4.0/en/getting-started/first-game/images/remove-capsule.png b/versions/4.0/en/getting-started/first-game/images/remove-capsule.png new file mode 100644 index 0000000000..659fac63e9 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/remove-capsule.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/start-menu.png b/versions/4.0/en/getting-started/first-game/images/start-menu.png new file mode 100644 index 0000000000..a010bc532a Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/start-menu.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/steps-label.png b/versions/4.0/en/getting-started/first-game/images/steps-label.png new file mode 100644 index 0000000000..b629572b20 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/steps-label.png differ diff --git a/versions/4.0/en/getting-started/first-game/images/title-inspector.png b/versions/4.0/en/getting-started/first-game/images/title-inspector.png new file mode 100644 index 0000000000..e1f71adac9 Binary files /dev/null and b/versions/4.0/en/getting-started/first-game/images/title-inspector.png differ diff --git a/versions/4.0/en/getting-started/first-game/index.md b/versions/4.0/en/getting-started/first-game/index.md new file mode 100644 index 0000000000..c8c63cfdfa --- /dev/null +++ b/versions/4.0/en/getting-started/first-game/index.md @@ -0,0 +1,1163 @@ +# Quick Start: Make Your First 3D Game + +In this section, we will introduce you to some of the 3D features in Cocos Creator, and how to use them to make a simple but complete platform jump game. + +The following tutorial shows a mini-game named "Mind Your Step". This game tests the user's reaction speed, choosing whether to jump one step or two steps according to the road. + +You can experience the finished form of the game at [here](https://gameall3d.github.io/MindYourStep_Tutorial/index.html). + +## Add Player + +A controllable character is always needed for most games. Here we will make our main character in the game, Miss/Mr. Capsule. + +For the sake of convenience in production, let's review a little here how nodes are created within the editor. + +![hierarchy.png](images/hierarchy.png) + +The **Hierarchy** panel will demonstrate all nodes in the scene, and you can create a new node right-clicking on the popup menu of the **Hierarchy** panel. + +### Create Player Node + +First, you need to create an empty Node named 'Player', and then create another Node named 'Body' to represent the character's model node. To make it simple, we use the built-in capsule model as our main character. + +![create player node](./images/create-player.gif) + +You may notice that our player is divided into two nodes, the advantage of the division is that when we move the Player node horizontally, this movement will not affect the vertical animation on the Body node(such as jump up and fall). The entire jump animation is combined with horizontal movement and vertical jump-fall animation. + +Then place the Player node at (0, 0, 0), so that it can stand on the first block. The effect is as follows: + +![create player](./images/create-player.png) + +### Scripting the Main Character + +To make a character move by mouse-events, there is a need to write some custom scripts. Don't worry even if you don't have any programming experience, we will provide all code and all you need to do is copy and paste all those codes to the right place. And of course, we will explain all the code provided to help you get started with Cocos Creator as quick as possible. + +#### How to Create Script Components + +1. If you haven't created the Scripts folder yet, first right-click on the **assets** folder in **Assets**, select the **Create -> Folder** menu to create a new folder, and rename it with 'Scripts'; +2. Right-click on the Scripts folder, and select **Create -> TypeScript -> NewComponent** on the popup menu to create a new TypeScript component. More references can be found on [official TypeScript](https://www.typescriptlang.org/). +3. Rename the created component to `PlayerController` and double-click on the script file to open any code editor(such as VSCode). + + ![create player script](./images/create-player-script.gif) + +> **Note**: The script name in Cocos Creator is case-sensitive! If the capitalization is incorrect, you can't use the component based on its name. +> +> About renaming: If you enter the name of a component by mistake, you need to change the file name, class name, and decorator name to correct it. Therefore if you do not familiar with the whole operation, consider deleting the file and re-creating it. + +#### Writing Script Code + +Within the opened `PlayerController` script there is already some pre-set blocks for code, as follows: + +```ts +import { _decorator, Component } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + /* class member could be defined like this */ + // dummy = ''; + + /* use the `property` decorator if you want the member to be serializable */ + // @property + // serializableDummy = 0; + + start () { + // Your initialization goes here. + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } +} +``` + +The above code demonstrates the necessary structure of a component(script). Any class that inherits from a `cc.Component` is called a Component and can be attached to any node in a scene to control the behavior of the node, for more details please refer to [Scripting](../../scripting/index.md). + +Next, let's refine the `PlayerController` code so that the character can actually move. + +#### Listening to the Input + +We need to listen to the computer input(mouse, keyboard, or joystick, etc.) to manipulate the character in games, and in Cocos Creator, you can do this by listening to the `input` events. + +```ts +input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); +``` + +The above code demonstrates use how we are listening to the mouse-up event(`MOUSE_UP`) of the Input-System(`input`), and when the mouse-up event is emitted, the `onMouseUp` in this script will be invoked. + +Usually, we will put all the initial code in the `start` method of a Component to make sure the input events can listen correctly right after the initialization. + +> The invocation of the `start` method indicates that the component has been properly initialized and you can use it with confidence. + +So the following code could be seen. + +```ts +import { _decorator, Component, input, Input, EventMouse } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + + + } +} +``` + +#### Make Character Moves + +To make our Player move, we have to add some additional properties to the Player to describe it. Think of some questions in the high school physics class about cubes or balls. Yes, indeed, we change the position, speed, and other factors to achieve a movable character in game development. + +In the current game, we want to accomplish this behavior: the user presses the left/right mouse button -> determines that it is a displacement of one/two steps -> moves the character forward according to the input until it reaches the destination point. + +So we could add a property to the script like the following: + +```typescript +// Whether to receive the jump command +private _startJump: boolean = false; +``` + +With a boolean variable like `_startJump`, we can mark whether the current character is in the jump or not, to help us distinguish between different branching logic in the `update` method. Because obviously, we don't need to move the character when no input is received. + +> Q: Why do we handle it in the `update` method? +> +> A: The `update` method will be invoked by the engine in a specific time interval. If the FPS of our game is 60 (60 render frames per second), then the `update` method will be called 60 times per second. We can simulate realistic continuous behaviors by using `update` as much as possible. + +So we can change the code as follows: + +```typescript +import { _decorator, Component, input, Input, EventMouse } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + // Whether to receive the jump command + private _startJump: boolean = false; + // The jump step count + private _jumpStep: number = 0; + + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + + + } + + update(dt: number): void { + if( this._startJump){ // Handle branching logic of jump + + } + } +} +``` + +Good, here we have completed the main framework of our Player controller, so we just need to think about how to move the Player in the `update` method. + +In Newtonian mechanics, we have learned that if an object is to move with uniform velocity, then its position must be like the following: + +```txt +P_1 = P_0 + v*t +``` + +In other words, we can add the current position of the object plus the velocity multiplied by the time, then we can get the new position. + +In our game, when the mouse button is pressed, the character will move directly 1 unit if the jump step is 1, and 2 units if the jump step is 2. So we can calculate the target position of the character (_targetPos) by using the following formula: + +``` +_targetPos = _curPos + step +``` + +It can be seen that the above formula contains three pieces of information: _targetPos、_curPos and step, so we record these properties in the script to use them in the `update` method. + +Add those properties in the `PlayerController` component like the following: + +```ts +// The jump step +private _jumpStep: number = 0; +// Current position of the character +private _curPos: Vec3 = new Vec3(); +// The target position of the character +private _targetPos: Vec3 = new Vec3(); +``` + +The code of `PlayerController` should look as follows: + +```ts +import { _decorator, Component, input, Input, EventMouse, Vec3 } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + // Whether to receive the jump command + private _startJump: boolean = false; + // The jump step + private _jumpStep: number = 0; + // Current position of the character + private _curPos: Vec3 = new Vec3(); + // The target position of the character + private _targetPos: Vec3 = new Vec3(); + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + + + } + + update(dt: number): void { + if( this._startJump){ + + } + } +} +``` + +Our next mission is to accomplish how could we get that information by computerizing. + +First of all, we should check whether the user has clicked the right mouse button or the left mouse button. + +Those determinations can be made by getting the mouse button information in the `onMouseUp` method by using the `getButton` of `EventMouse`. In the `EventMouse` event, the `getButton` will return 0 if the left button is clicked, or 2 if the right button is clicked. + +So let's extend the `onMouseUp` method as follows: + +```ts +onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + + } + else if (event.getButton() === 2) { + + } +} +``` + +Add a new method `jumpByStep` to the `PlayerController` component to help us calculate the destination position and speed of the character. Since the input step maybe 1 or 2 steps, so we add a numeric parameter `step` to the `jumpByStep` method to achieve better reusability. + +> Reusability? Such a concept sounds complicated! +> Don't stress, it's just some engineering jargon that we can ignore when we first learn it. +> This is just to explain more clearly why we added a method called `jumpByStep`. + +So, the method looks like the follows: + +```ts +jumpByStep(step: number) { + +} +``` + +We use the `step` parameter to indicate the jump steps. + +In our game, jumping(raise and fall) must be a complete step and we don't accept any input unless the jumping process is finished, so we use `_startJump` to ignore the character's input while jumping. + +After that, we shall calculate how much is the character's velocity in the given time when it moves from its current position to the destination. + +The calculating process looks like the follows: + +1. Calculate current speed(_curJumpSpeed) +2. Calculate target position(_targetPos) + +In order to exit the `if( this._startJump)` branch logic correctly, we have to remember the time to start the jump (_curJumpTime), because when this time has elapsed, we consider the whole jump process to be finished. + +Now the `jumpByStep` method looks like follows: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; // Whether to start to jump + this._jumpStep = step; // Jump steps for this time + this._curJumpTime = 0; // Reset the jump time + this._curJumpSpeed = this._jumpStep / this._jumpTime; // Current jump step + this.node.getPosition(this._curPos); // use 'getPosition` to get the current position of the character + // target position = current position + steps + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); +} +``` + +- Q: What is `Vec3.add`? + - `Vec3` is a 3D vector class in Cocos Creator, used to record and calculate 3D vectors. + - `add` is a method in `Vec3` to add two 3D vectors, we can notice that there is no `this`, because `add` is a **static method**. + - `Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0));` This code block indicate that we use the target position(`_targetPos`) to store the add result of current position(`_curPos`) and a new vector(`new Vec3(this._jumpStep, 0, 0)`) which represent the jump step on x axis. + +Sure, we can simply move the character on the X-axis, but in fact, in 3D game development, objects may move along 3 different axes, and the more familiar you are with the usage of 3D vectors, the faster we can master the 3D engine. + +At this moment, our code may look as follows: + +```ts +import { _decorator, Component, input, Input, EventMouse, Vec3 } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + // Whether received jump command + private _startJump: boolean = false; + // The jump step count + private _jumpStep: number = 0; + // Current jump time + private _curJumpTime: number = 0; + // Total jump time + private _jumpTime: number = 0.1; + // current jump speed + private _curJumpSpeed: number = 0; + // current position of the + private _curPos: Vec3 = new Vec3(); + // The difference of the current frame movement position during each jump + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + // Target position of the character + private _targetPos: Vec3 = new Vec3(); + + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + + } + else if (event.getButton() === 2 ) { + + } + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); + } + + + update(dt: number): void { + if( this._startJump){ + + } + } +} +``` + +Fascinating, now we have all the data required to calculate the character's manipulation, then we can move it in the `update` method. + +Remember the Newton problem we talked about above? It's time to really solve this problem. + +First up, we need to check the entire jump duration (_curJumpTime) has exceeded our predefined time (_jumpTime). + +The test here is simple: we have set _curJumpTime to 0 at input time, so `update` only needs to: + +``` +Jump time = last jump time + frame interval +``` + +The `update` method happens to provide a parameter like `deltaTime` as the frame interval. + +So we just add `_curJumpTime` to `deltaTime` and that's it: + +```ts +update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + } +} +``` + +> You can see that we ues the '+=' operator, which is equivalent to `this._curJumpTime = this._curJumpTime + deltaTime`. + +When `_curJumpTime` is greater than our pre-defined `_jumpTime`, this means that the jump is over, so we need two branches to handle the end-of-jump and in-jump states, respectively: + +```ts +update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { // Jump ends + + + } else { // Jumping + + } + } +} +``` + +Finally, we refine the logic for the different branches: + +- End of jump: ends the jumping process, forcing the character to be displaced to the target position +- During the jump: moves the character forward according to the velocity + +So our `update` method will look like this: + +```ts +update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime. + if (this._curJumpTime > this._jumpTime) { // end of jump + // end + this.node.setPosition(this._targetPos); // force move to target position + this._startJump = false; // mark the end of the jump + } else { // jump in progress + // tween + this.node.getPosition(this._curPos); // Get the current position + this._deltaPos.x = this._curJumpSpeed * deltaTime; // calculate the length of this frame that should be displaced + Vec3.add(this._curPos, this._curPos, this._deltaPos); // add the current position to the length of the displacement + this.node.setPosition(this._curPos); // set the position after displacement + } + } +} +``` + +Great! You have completed the core code of this game - character control. + +If you still find this explanation a bit difficult, please let us know by clicking on [Get help and support](../support.md). + +#### Complete PlayerController + +We already have the complete PlayerController code, so we just need to attach it to a node to make it work. + +If you still find it difficult, try copying and pasting the following code into the PlayerController.ts file in your project. + +```ts +import { _decorator, Component, Vec3, input, Input, EventMouse, Animation } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + /* class member could be defined like this */ + // dummy = ''; + + /* use the `property` decorator if you want the member to be serializable */ + // @property + // serializableDummy = 0; + + // for fake tween + // Whether the jump command is received or not + private _startJump: boolean = false; + // Jump step length + private _jumpStep: number = 0.0; + // current jump time + private _curJumpTime: number = 0.0; + // length of each jump + private _jumpTime: number = 0.1; + // current jump speed + private _curJumpSpeed: number = 0.0; + // current character position + private _curPos: Vec3 = new Vec3(); + // the difference in position of the current frame movement during each jump + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + // target position of the character + private _targetPos: Vec3 = new Vec3(); + + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } + else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); + } + + update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` + +Now we can add the `PlayerController` component to the character node `Player`. Select the `Player` node on the **Hierarchy** panel, then click the **Add Component** button on the **Inspector** panel, and choose **Custom Script -> PlayerController** to add it to the `Player` node. + +![add player controller comp](./images/add-player-controller.png) + +To display the content on screen properly during runtime, We need to adjust some parameters of the camera, select the **Main Camera** node, and change its `Position` to (0, 0, 13), and `Color` to (50,90,255,255): + +![camera setting](./images/camera-setting.png) + +Then click the **Preview** button on the center of the top toolbar. + +![play button](./images/play.png) + +By clicking the left mouse button or right mouse button in the open browser, you may see the following screen: + +![player move](./images/player-move.gif) + +For more preview functions, please refer to [Project Preview & Debugging](../../editor/preview/index.md). + +## Add Character Animations + +We could notice that the horizontal movement of the character is tedious from the result of the above execution. To make out character feel like jumping you could add a vertical animation. For more information about **Animation Editor**, please refer to [Animation System](../../animation/index.md). + +1. Select the `Body` node in the **Hierarchy** panel, and then click the **Add Animation Clip** button in the **Animation Editor** at the bottom of the editor to create an animation clip and name it `onStep`. + + ![player move](./images/add-animation.gif) + +2. Enter the animation editing mode, add a `position` property track to the created clip, and then add three keyframes, the position value of each should be (0, 0, 0), (0, 0.5, 0), (0, 0, 0); + + ![add keyframe](./images/add-keyframe.gif) + + > **Note**: Alway remember to save animation clips when exiting animation editing mode, otherwise you work will be lost. + +3. We can use **Assets Manager** to create clips. Right-click on the **Assets** panel to create a new animation clip named `twoStep`, and add it to the `Animation` property of the `Body` node. To make the recording process easier, we can adjust the layout of the editor. + + > **Note**: Please note that when the animation clip can not be dragged to the `Animation` property, please check the import section `import {...} from "cc"` in `PlayerController` contains `Animation`. + + ![add animation from assets](./images/add-animation-from-assets.gif) + +4. Enter the animation edit mode, select `twoStep` clip in the dropdown menu like step 2, then add three key-frames of the `Position` property which should be (0, 0, 0), (0, 1, 0), (0, 0, 0). + + ![edit second clip](./images/edit-second-clip.png) + +5. Import and reference the `Animation` component in the `PlayerController`, because we need to play different animation clips according to different steps. + + First, reference `Animation` component of the `Body` node to `PlayerController`. + + ```ts + @property({type: Animation}) + public BodyAnim: Animation | null = null; + ``` + + Drag the `Body` node to the `Animation` property of the **Inspector** panel. + + ![drag to animComp](./images/drag-to-animComp.gif) + + Add the following code to the jump method `jumpByStep` to play animations. + + ```ts + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } + ``` + +6. Click the **Preview** button to see the new jump animation in the open browse by clicking the left or right mouse button. + + ![preview with jump](./images/preview-with-jump.gif) + +## Runway upgrade + +To make our game more interesting, we need a long runway for the Player to fleet on. Duplicating a lot of Cube nodes is obviously not a good idea, we can automatically create the runway by script. + +### Game Manager + +Typically, a scene contains various functions and different types of nodes, All those nodes stored in the scene could be taken as the most important data of our game. Occasionally we need to dynamically create, visit, and delete those nodes. Although we can find a node by `find` method in Cocos Creator, actually the `find` method has to enumerate all nodes in the scenes which will cause a low accuracy. In commercial development, we use some single components to manage **some kind of** nodes or **some** nodes data which we call **Manager**. + +For example, in a game, we may have many characters, and we may have to create a new character, delete characters who are dead, or query the status of a character, in that case, we can create a class named `ActorManager` to manage all the characters and support the functions mentioned above. + +In this game, we declare a new node component named GameManager to manage all relevant nodes or data. + +In this case, we put all functions, such as runway generation and deletion, in GameManager to make data collection more convenient. + +This style of cohesion is very common in game development, do not be surprised when you see some other components such as ConfigManager or NetworkManager. + +Of course, there are more complex structures in complex games. Those complex, tremendous conceptions of design can improve the stability and maintainability of our applications. + +So let's go ahead and create such a manager. + +### Create GamManager + +1. Right-click on the **Hierarchy** panel to create a new node named `GameManager`. +2. Right-click in `assets/Scripts` folder on the **Assets** panel to create a TypeScript component named `GameManager`. +3. Add the `GameManager` component to the GameManager node. + +### Create Prefabs + +For nodes that need to be duplicated, we can save them to a [Prefab](../../asset/prefab.md) asset as a template of dynamic assets. + +> Prefabs are a built-in asset type of the engine, the main purpose of which is to provide the possibility to instantiate some kind of node. Imagine if we had 1000 enemies in our game, creating them individually would take a lot of time, to solve this problem we could dynamically clone the prefabs 1000 times. + +Drag the elementary Cube node to the **Assets** panel to create a Prefab. + +![create cube prefab](./images/create-cube-prefab.gif) + +### Add Automatic Runway Creation Code + +The player needs a long runway, and the ideal way is to dynamically increase the length of the runway so that it can run forever. Here for convenience, firstly we generate a fixed-length runway, the runway length can be defined by yourself. In addition, we can generate some pits on the runway and GameOver when the Player jumps to the pits. + +First, we need to define some constants to indicate whether the current coordinates represent a pit or a rock. In Typescript, this can be done with enumerations. + +By using the keyword `enum` + the name of the enumeration we define the following enumeration, where `BT_NONE` represents a pit and `BT_STONE` represents a walkable surface. + +```typescript +enum BlockType{ + BT_NONE, + BT_STONE, +}; +``` + +Again, we need to define which Prefab or bodies the game manager will use to create the map. + +```ts +// The runway prefab +@property({type: Prefab}) +public cubePrfb: Prefab | null = null; +``` + +The editor will recognize `@property({type: Prefab})` as a prefab so that we can assign the Cube.prefab to this property in the **Inspector** panel. + +> A syntax like `@property` is called a [Decorator](../../scripting/decorator.md). There is various type of decorators in Cocos Creator that can use it to customize various effect to decorate custom components. + +To make it easier to quickly find out whether the current location is a Cube or a pit, we describe the entire track using an array + +```ts +_road: BlockType[] = []; +``` + +Thus a basic GameManager is defined, which should look like this: + +```ts +import { _decorator, Component, Prefab } from 'cc'; +const { ccclass, property } = _decorator; + +// Track grid type, pit (BT_NONE) or solid road (BT_STONE) +enum BlockType { + BT_NONE, + BT_STONE, +}; + +@ccclass("GameManager") +export class GameManager extends Component { + + // The runway prefab + @property({type: Prefab}) + public cubePrfb: Prefab | null = null; + // Total road length + @property + public roadLength = 50; + private _road: BlockType[] = []; + + start () { + this.generateRoad(); + } + + generateRoad() { + + } +} +``` + +Then we generate the entire runway by the following principles. + +- The first location on the runway must be BT_STONE to make sure the player won't drop at the beginning of the game. +- After the first position, we will generate a cube or a pit at random. Considering our character's poor jumping ability (up to 2 steps), we can't generate 2 or move pits continuously, so when we generate a new location, we should examine whether the previous location is a rock or a pit, and if it is a pit, we must put a rock in the current location. +- Based on the above two principles, we save the generated block to the `_road` array to quickly query whether a location is a rock or a pit. +- Finally, we instantiate the whole road into the scene based on the road information `_road` to accomplish the whole map. + +Thus the method `generateRoad` for generating the map is as follows: + +```ts +generateRoad() { + // Prevent the track from being the old track when the game is restarted + // Therefore, the old track needs to be removed and the old track data cleared + this.node.removeAllChildren(); + this._road = []; + // Make sure that the character is standing on the real road when the game is running + this._road.push(BlockType.BT_STONE); + + // Determine the type of track for each frame + for (let i = 1; i < this.roadLength; i++) { + // If the previous track is a pit, then this one must not be a pit + if (this._road[i-1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + // Generate tracks based on runway + for (let j = 0; j < this._road.length; j++) { + let block: Node = this.spawnBlockByType(this._road[j]); + // Determine if a road was generated, as spawnBlockByType may return a pit (with a value of null) + if (block) { + this.node.addChild(block); + block.setPosition(j, -1.5, 0); + } + } +} +``` + +> `Math.floor`: This method is one of the methods of the Typescript math library: we know that floor means floor, and this means taking the "floor" of this method argument, i.e. rounding down. +> `Math.random`: Again random is one of the methods of the standard math library, and is used to randomize a decimal number between 0 and 1, note that the range of values is [0, 1). +> So `Math.floor(Math.random() * 2)` means simply that it takes a random number from [0, 2) and rounds down to 0 or 1, which corresponds to the `BT_NONE` and `BT_STONE` declared in the enumeration `BlockType`. +> By the way, in Typescript enumerations, if you don't assign a value to the enumeration, then the enumeration values are assigned sequentially starting from 0. + +The method `spawnBlockByType` for generating a stone is as follows: + +```ts +spawnBlockByType(type: BlockType) { + if (!this.cubePrfb) { + return null; + } + + let block: Node | null = null; + // The runway is generated only for real roads + switch(type) { + case BlockType.BT_STONE: + block = instantiate(this.cubePrfb); + break; + } + + return block; +} +``` + +We can notice that when the block type is Stone, we will use the `instantiate` method to clone a new Prefab, on the other hand, if the block type is not Stone, we don't do anything. + +> `instantiate` is the method for cloning a Prefab in Cocos Creator. You can use it to clone not only Prefab but also clone other types of objects! + +The entire code of `GameManager` is as follows: + +```ts +import { _decorator, Component, Prefab, instantiate, Node, CCInteger } from 'cc'; +const { ccclass, property } = _decorator; + +// The runway type, pit (BT_NONE) or solid road (BT_STONE) +enum BlockType { + BT_NONE, + BT_STONE, +}; + +@ccclass("GameManager") +export class GameManager extends Component { + + // The stone prefab + @property({type: Prefab}) + public cubePrfb: Prefab | null = null; + // Length of the runway + @property + public roadLength = 50; + private _road: BlockType[] = []; + + start () { + this.generateRoad(); + } + + generateRoad() { + // Prevent the track from being the old track when the game is restarted + // Therefore, the old track needs to be removed and the old track data cleared + this.node.removeAllChildren(); + this._road = []; + // Make sure that the character is standing on the real road when the game is running + this._road.push(BlockType.BT_STONE); + + // Determine the type of track for each frame + for (let i = 1; i < this.roadLength; i++) { + // If the previous track is a pit, then this one must not be a pit + if (this._road[i-1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + // Generate tracks based on runway + for (let j = 0; j < this._road.length; j++) { + let block: Node = this.spawnBlockByType(this._road[j]); + // Determine if a road was generated, as spawnBlockByType may return a pit (with a value of null) + if (block) { + this.node.addChild(block); + block.setPosition(j, -1.5, 0); + } + } + } + + spawnBlockByType(type: BlockType) { + if (!this.cubePrfb) { + return null; + } + + let block: Node | null = null; + // The runway is generated only for real roads + switch(type) { + case BlockType.BT_STONE: + block = instantiate(this.cubePrfb); + break; + } + + return block; + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } +} +``` + +Drag the pre-made Cube.prefab on to **CubePrfb** property of the **Inspector** panel. + +![assign cube prefab](./images/assign-cube-prefab.png) + +You can change the **roadLength** property of the **Inspector** panel to control the length of the runway. + +Now, click the **Preview** button to see the automatically created runway in the open browser. + +## The Player-Centered Camera + +We can notice that the Camera does not follow the Player node, hence we can not see the runway behind it. We can avoid this situation by making the camera a child node of Player. + +![drag camera to player](./images/drag-camera-to-player.gif) + +Thus the Camera will follow the Player's movement, now by clicking the **Preview** button, we can watch the whole runway. + +## Add a Start Menu + +The User Interface(UI) is an essential part of a game, developers can inform users of some significant information such as data, status, etc so that the user can have their strategy. We could also add the game name, an introduction, the production staff, and other information. + +Please refer to relevant documents such as [2D/UI](. /. /2d-object/index.md) to help understand the mechanics of the UI as a 2D part. + +Then we will describe a UI workflow taking the Start Menu as an example. + +1. Create a new Button node in the **Hierarchy** panel and rename it PlayButton + + ![create button](./images/create-button.gif) + + It can be noticed that the engine automatically creates a Canvas node in the **Hierarchy** panel, a Player button node, and a Label node. That is because any node that contains a UI component only be seen when it is under the `Canvas` node, so the editor will create a Canvas in case there is none. + + Change the **String** property of the `cc.Label` component in the Label Node from "Button" to "Play". + + > You may notice that Cocos Creator automatically adds a new Camera node under the "Canvas" when you create a UI node. + > This is a new feature in Cocos Creator. The new Camera will be in charge of the UI/2D parts, therefore the engine's render pipeline can process the UI/2D content more efficiently. + +2. Create an empty node named StartMenu under the Canvas node, and drag the PlayButton under it. We can switch to a 2D editing view for editing UI by clicking the 2D/3D button on the scene's toolbar. For more details on scene editing, please refer to [Scene Panel](../../editor/scene/index.md)。 + + ![2d-view](./images/2d-view.png) + +3. Create a new Sprite node named `BG` under the `StartMenu` as a background image, and drag it on the PlayButton. + + ![create bg sprite](./images/create-bg-sprite.gif) + + Then set the `ContentSize` property of `cc.UITransform` component to (200, 200) in the **Inspector** panel, and drag the `internal/default_ui/default_sprite_splash` asset to its **SpriteFrame** property. + + ![change spriteFrame](./images/change-spriteFrame.png) + +4. Create a Label node named Title beneath the StartMenu node on the **Hierarchy** panel, and use it as the title of the start menu. + + ![add title label](./images/add-label-title.gif) + +5. Adjust other properties of the Title node such as on `Position`, `Color`, `String`, `FontSize`, etc on the **Inspector** panel by the following image: + + ![modify title](./images/title-inspector.png) + +6. Once the required Tips nodes are added and the PlayButton is adjusted, a simple start menu is completed. + + ![modify title](./images/start-menu.png) + +## Add Game States + +For most games, we can roughly break it down into 3 different states: initialize, play, and settle. Just like in any board game, the process of placing the pieces is called initialization; the process of playing is called playing; and the final state when the two players finish the game to settle the victory/failure is called the settlement state. + +In the same way, we can define the game state by using an enumeration. + +```ts +enum GameState{ + GS_INIT, + GS_PLAYING, + GS_END, +}; +``` + +- GS_INIT(initialize): Show the game menu and initialize some resources. +- GS_PLAYING(play): Hide the game menu, and the user can start to control the character. +- GS_END(end): End the game and show the start menu. + +The current game state can be easily informed by reading the `GameState` enumeration, and we can handle some logic when the state changes. + +In order to allow the user to control the character in the play state rather than the game initialize, we have to enable/disable listening to the mouse event automatically. So modify the `PlayerController` script as follows: + +```ts +start () { + // Your initialization goes here. + // input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); +} + +setInputActive(active: boolean) { + if (active) { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } else { + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } +} +``` + +And reference the `PlayerController` script in the `GameManager` script. + +```ts +@property({type: PlayerController}) +public playerCtrl: PlayerController | null = null; +``` + +Back to Cocos Creator after saving the scripts, and drag the Player node that attached a `PlayerController` script to the `playerCtrl` property of the GameManager Node in the **Inspector** panel. + +Meanwhile, we need to reference the StartMenu node in the `GameManager` script to dynamically enable/disable the start menu. + +```ts +@property({type: Node}) +public startMenu: Node | null = null; +``` + +Return to the editor after saving all scripts and drag the StartMenu node to the `startMenu` property of the GameManager node on the **Inspector** panel. + +![add player to game manager](./images/game-manager-player.png) + +### Add State Change Code + +Add the switch state code to the GameManager script, and add the initialization code to the `start` method. + +```ts +start () { + this.curState = GameState.GS_INIT; +} + +init() { + // Active start menu + if (this.startMenu) { + this.startMenu.active = true; + } + // Generate the runway + this.generateRoad(); + if(this.playerCtrl){ + // Disable user input + this.playerCtrl.setInputActive(false); + // Reset the player's position + this.playerCtrl.node.setPosition(Vec3.ZERO); + } +} + +set curState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + if (this.startMenu) { + this.startMenu.active = false; + } + // When active is set to true, it will start listening for mouse events directly, while mouse up events are not yet dispatched + // What will happen is that the character will start moving the moment the game starts + // Therefore, a delay is needed here + setTimeout(() => { + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + break; + case GameState.GS_END: + break; + } +} +``` + +Here we can see in the newly added `init` method, it initializes the runway, activates the start menu, and disable user input at the same time. In any case, we will invoke this method when the game state changes to `GS_INIT`. + +And for the first time, accessors like `set curState` allow us to be more secure when setting the game state. + +### Add an Event Listener to the Play Button + +In order to start the game immediately after clicking the Play button, we need to listen to its click event. Add the following response code in the `GameManager` script as a means to enter the Playing state after the user clicked the button. + +```ts +onStartButtonClicked() { + this.curState = GameState.GS_PLAYING; +} +``` + +Then select the PlayButton node in the **Hierarchy** panel, add a response method to the `cc.Button` component in the **Inspector** panel, and drag the GameManager node on the `cc.Node` property. + +![play button inspector](./images/play-button-inspector.png) + +Here we can notice the element [0] of Click Events is divided into three parts, which are: +1. Node: Here we drag and drop the GameManager node, which means that the button event will be received by the GameManager node +2. Component: after dragging the GameManager node, you can select the node on the GameManager through the drop-down, here select the component with the same name `GameManager`. +3. Event Handler: through the drop-down menu, select the response event of the button, here you have to select the `onStartButtonClicked` method added above as the response of **button clicked** + +> **Note**: The above three steps must be performed in strict order of 1->2->3. + +Now, by clicking the **Preview** button, we can start the game. + +## Add Game Over Logic + +Currently, the game character just runs forward dully, we need to add game rules to make it more challenging to run. + +1. The character needs to send a message at the end of each jump and send a message with its current position as a parameter, and record how many steps it has jumped in the `PlayerController` script: + + ```ts + private _curMoveIndex = 0; + // ... + jumpByStep(step: number) { + // ... + + this._curMoveIndex += step; + } + ``` + + And at the end of each jump sends the message: + + ```ts + onOnceJumpEnd() { + this.node.emit('JumpEnd', this._curMoveIndex); + } + ``` + +2. Listen for end-of-character jump events in the `GameManager` script, and determine the winner according to the rules, adding a failure and end judgment, and ending if the jump reaches an empty block or exceeds the maximum length value: + + ```ts + checkResult(moveIndex: number) { + if (moveIndex < this.roadLength) { + // Jumped on the pit + if (this._road[moveIndex] == BlockType.BT_NONE) { + this.curState = GameState.GS_INIT; + } + } else { // Skipped maximum length + this.curState = GameState.GS_INIT; + } + } + ``` + + Listens for role jump messages and calls the judgment function: + + ```ts + start () { + this.curState = GameState.GS_INIT; + // '?.' is Typescript's optional chain writing + // The equivalent of: + // if(this.playerCtrl ! = null) this.playerCtrl.node.on('JumpEnd', this.onPlayerJumpEnd, this). + // Optional chains are written in a more concise way + this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this); + } + + // ... + onPlayerJumpEnd(moveIndex: number) { + this.checkResult(moveIndex); + } + ``` + + If you preview the game at this point, you will find that there is an error of judgment when restarting the game, which is caused by not resetting the value of the `_curMoveIndex` property in `PlayerController.ts` when restarting. So we need to add a `reset` function to the `PlayerController` script: + + ```ts + reset() { + this._curMoveIndex = 0; + } + ``` + + Then call `reset` in the `init` function of the `GameManager` script to reset the `_curMoveIndex` property in the `PlayerController.ts`. + + ```ts + init() { + // ... + this.playerCtrl.reset(); + } + ``` + +## Step Count Display + +We can display the current number of steps on the UI so that it will be a great sense of accomplishment to watch the number of steps increasing during the jump. + +1. Create a new Label node named Steps under Canvas and adjust the position, font size and other properties. + + ![steps label](./images/steps-label.png) + +2. Reference this Label in the `GameManager` script: + + ```ts + @property({type: Label}) + public stepsLabel: Label | null = null; + ``` + + After saving the script and going back to the editor, drag the Steps node into the GameManager's stepsLabel property box in the properties inspector at + + ![steps label to game manager](./images/add-steps-to-game-manager.png) + +3. Update the current step count data to the Steps node. Since we don't have a GameOver UI n now, we will jump back to the start menu when the game is over, so we need to see the last jump in the start screen, so we need to reset the step count to 0 when we enter the Playing state. + + ```ts + // GameManager.ts + set curState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + if (this.startMenu) { + this.startMenu.active = false; + } + + if (this.stepsLabel) { + this.stepsLabel.string = '0'; // Reset the number of steps to 0 + } + setTimeout(() => { // Directly set active will start listening to mouse events directly, here the delay is done + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + break; + case GameState.GS_END: + break; + } + } + ``` + + Then, in the function `onPlayerJumpEnd`, which response to the character jump, the number of steps is updated to the Label control + + ```ts + onPlayerJumpEnd(moveIndex: number) { + if (this.stepsLabel) { + // Because in the last step there may be a jump with a large pace, but at this time, whether the jump is a large pace or a small pace should not increase the score more + this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex); + } + this.checkResult(moveIndex); + } + ``` + +## Conclusion + +Up to this point, you have basically mastered most of the contents of this chapter, next you can improve the game by improving the art and gameplay, for which we have also prepared an [advanced](./advance.md) chapter as an option. If you are interested in other features of Cocos Creator, you can click on the summary on the left. + +If you have any feedback or suggestions, you can visit our [forum](https://discuss.cocos2d-x.org/) or request a question on [GIT](https://github.com/cocos/cocos-docs). diff --git a/versions/4.0/en/getting-started/helloworld/index.md b/versions/4.0/en/getting-started/helloworld/index.md new file mode 100644 index 0000000000..2d5a0e2bbb --- /dev/null +++ b/versions/4.0/en/getting-started/helloworld/index.md @@ -0,0 +1,118 @@ +# Hello World Project + +It's time to create and open the **Hello World** project. + +Before we starts, please make sure you have read the [Install and Launch](../install/index.md), installed the Cocos Dashboard, and the newest Cocos Creator editor. + +## Creating a new project + +In the **Project** tab of Cocos Dashboard, click the **New** button in the bottom right corner to enter the **New Project** page. Select the **empty** project template, set the project name and project path + +![empty](index/new-empty.png) + +Then click the **Create and open** button at the bottom right to automatically create a project with an empty project template and open. + +![editor](index/editor-panel.png) + +- **Assets**: shows all the assets in the project's assets folder (`assets`). +- **Scene**: working area for displaying and editing the visual content in the scene. +- **Hierarchy**: shows all nodes in the scene and their hierarchical relationships in a tree list, all content seen in the **Scene** panel can be found in the **Hierarchy** panel with corresponding node entries +- **Inspector**: working area for viewing and editing the properties of the currently selected node and its components +- **Assets Preview**: Selecting an asset in the **Assets** panel displays a thumbnail of the asset in the **Assets Preview** panel. If the folder where the asset is located is selected, the thumbnails of all assets under the folder can be displayed for easy viewing. +- **Animation**: used to create some less complex animations that need to be linked with logic, such as UI animations. +- **Console**: used to display error reports, warnings or other log messages generated by the editor and engine. + +For more information about the various editor panels, please refer to [Editor Panels](../../editor/index.md). + +## New Scene Creation + +Right-click in the **Assets** panel at the bottom left and select **Create -> Scene**. + +![scene](index/create-scene.png) + +Alternatively, you can create a new scene in the `asset` directory in the **Assets** panel by clicking the **+** button in the upper left corner and selecting **Scene**. + +![scene](index/scene.png) + +## Creating Objects + +In the **Hierarchy** panel on the top left, right-click and select **Create -> 3D Object -> Cube**. Or you can just click the **+** button in the top left corner and select **3D Object -> Cube Cube**. + +![create-cube](index/create-cube.png) + +A cube will be created and displayed in the **Scene** panel: + +![cube](index/cube.png) + +## Adding Scripts + +- Create a new script + + Right click in the **Assets** panel, select **Create -> TypeScript**, and name it "HelloWorld" to create a new script in the `asset` directory of **Assets** panel. + + ![create-typescript](index/create-typescript.png) + +- Adding code + + Double-click the new script and it will be automatically opened in the script editor, provided that you specify the script editor in the editor menu bar under **Cocos Creator/File -> Preferences -> Program Manager -> Default Script Editor**. + + Then add the `start()` function to the script. The `start()` function will be called when the component is activated for the first time and will output "Hello world". + + ```ts + import { _decorator, Component, Node } from 'cc'; const { ccclass, property } = _decorator; + + @ccclass('HelloWorld') + export class HelloWorld extends Component { + /* class member could be defined like this */ + // dummy = ''; + + /* use `property` decorator if you want the member to be serializable */ + // @property + // serializableDummy = 0; + + start () { + // Your initialization goes here. + console.info('Hello world'); + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } + } + ``` + +- Binding Scripts + + Select the created Cube node in the **Hierarchy** panel and click **Add Component -> Custom Script -> HelloWorld** at the bottom of the **Inspector** panel to mount the script to the Cube node. Alternatively, you can drag and drop the script directly into the **Inspector** panel. + + ![script](index/script.png) + + > **Note**: remember to save the scene after setting it up. + +## Previewing the Scene + +Once the simple scene setup is done, you can click the **Play on Device** button on top of the editor to preview the game. Currently, previewing is supported using **Browser/Simulator**. + +![preview](index/preview.png) + +Using the **Browser** preview as an example, Cocos Creator will run the current game scene with your default browser, as shown in the following image. + +![console](index/console.png) + +Notice the output "Hello World" in the log message in the developer tools of the browser. + +## Modifying the Camera in the Scene + +In the preview we can see that the cube seems a bit too small, so we can adjust the area of the scene by adjusting the Camera in the scene, which represents the player's perspective in the game. + +- First, select the **Main Camera** node in the **Hierarchy** panel, and the **Scene** panel will display the transformation tool Gizmo and a small window with the player's perspective. + + ![camera](index/camera.png) + +- Then drag the Gizmo in the **Scene** panel or modify the **Position** property in the **Inspector** panel to make the cube in the player's view window more visible. + + ![camera-position](index/camera-position.png) + +- Then preview it again in the browser and you can see that the cube in the scene is now obvious. + + ![preview](index/preview1.png) diff --git a/versions/4.0/en/getting-started/helloworld/index/camera-position.png b/versions/4.0/en/getting-started/helloworld/index/camera-position.png new file mode 100644 index 0000000000..71290c3aea Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/camera-position.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/camera.png b/versions/4.0/en/getting-started/helloworld/index/camera.png new file mode 100644 index 0000000000..e7c1bb6662 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/camera.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/component.png b/versions/4.0/en/getting-started/helloworld/index/component.png new file mode 100644 index 0000000000..29d9f6c39e Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/component.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/console.png b/versions/4.0/en/getting-started/helloworld/index/console.png new file mode 100644 index 0000000000..20c5070816 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/console.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/console1.png b/versions/4.0/en/getting-started/helloworld/index/console1.png new file mode 100644 index 0000000000..7f93df290c Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/console1.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/create-cube.png b/versions/4.0/en/getting-started/helloworld/index/create-cube.png new file mode 100644 index 0000000000..eab97424f2 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/create-cube.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/create-scene.png b/versions/4.0/en/getting-started/helloworld/index/create-scene.png new file mode 100644 index 0000000000..39ec8f1ec7 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/create-scene.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/create-typescript.png b/versions/4.0/en/getting-started/helloworld/index/create-typescript.png new file mode 100644 index 0000000000..f0349c6e47 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/create-typescript.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/cube.png b/versions/4.0/en/getting-started/helloworld/index/cube.png new file mode 100644 index 0000000000..bde3f255f6 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/cube.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/dashboard.png b/versions/4.0/en/getting-started/helloworld/index/dashboard.png new file mode 100644 index 0000000000..bf08d6f37c Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/dashboard.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/debug.png b/versions/4.0/en/getting-started/helloworld/index/debug.png new file mode 100644 index 0000000000..d2b9cc53d0 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/debug.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/editor-panel.png b/versions/4.0/en/getting-started/helloworld/index/editor-panel.png new file mode 100644 index 0000000000..fed3beaa03 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/editor-panel.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/engine.png b/versions/4.0/en/getting-started/helloworld/index/engine.png new file mode 100644 index 0000000000..1d53cbaa7e Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/engine.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/move.png b/versions/4.0/en/getting-started/helloworld/index/move.png new file mode 100644 index 0000000000..223a276de5 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/move.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/new-empty.png b/versions/4.0/en/getting-started/helloworld/index/new-empty.png new file mode 100644 index 0000000000..d425096f97 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/new-empty.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/new.png b/versions/4.0/en/getting-started/helloworld/index/new.png new file mode 100644 index 0000000000..34a9d86506 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/new.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/preview.png b/versions/4.0/en/getting-started/helloworld/index/preview.png new file mode 100644 index 0000000000..696d583850 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/preview.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/preview1.png b/versions/4.0/en/getting-started/helloworld/index/preview1.png new file mode 100644 index 0000000000..f93ff322a1 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/preview1.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/run.png b/versions/4.0/en/getting-started/helloworld/index/run.png new file mode 100644 index 0000000000..90de96d712 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/run.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/scene.png b/versions/4.0/en/getting-started/helloworld/index/scene.png new file mode 100644 index 0000000000..1299117f00 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/scene.png differ diff --git a/versions/4.0/en/getting-started/helloworld/index/script.png b/versions/4.0/en/getting-started/helloworld/index/script.png new file mode 100644 index 0000000000..7d368327e5 Binary files /dev/null and b/versions/4.0/en/getting-started/helloworld/index/script.png differ diff --git a/versions/4.0/en/getting-started/index.md b/versions/4.0/en/getting-started/index.md new file mode 100644 index 0000000000..3aec7c6c0a --- /dev/null +++ b/versions/4.0/en/getting-started/index.md @@ -0,0 +1,19 @@ +# Beginner's Guide + +Welcome to Cocos Creator. Before learning how to use it, please refer to the [Installing and Launching](install/index.md) documentation to install Cocos Creator. + +After installing the editor, it is important to become familiar with it, including how to create a project, the project structure, and the editor panels, by going through the following contents: + +- [Using Dashboard](dashboard/index.md) +- [Project Structure](project-structure/index.md) +- [Editor Panels](../editor/index.md) + +With some familiarity and understanding with the editor, it is easy to get familiar with the Cocos Creator development process with simple examples: + +- [Hello world!](helloworld/index.md) +- [Quick start: Making your first game](first-game/index.md) + +Cocos Creator also provides a lot of examples and tutorials and supports other third-party tools and resources. Developers can also give feedback to the Cocos Creator development team directly by utilizing the following options: + +- [Getting Help and Support](support.md) +- [Notes](attention/index.md) diff --git a/versions/4.0/en/getting-started/install/index.md b/versions/4.0/en/getting-started/install/index.md new file mode 100644 index 0000000000..5f2849c455 --- /dev/null +++ b/versions/4.0/en/getting-started/install/index.md @@ -0,0 +1,68 @@ +# Install and Launch + +Cocos Creator is equipped with a new Dashboard system which allows developers to upgrade and manage multiple versions of the engine and projects at the same time! Cocos Dashboard will serve as a unified downloader and launch portal for each Creator engine, making it easy to upgrade and manage multiple versions of Creator, as well as integrating a unified project management and creation dashboard, making it easy to work on projects with different versions of the engine at the same time. + +![Dashboard](index/dashboard-editor.png) + +## Downloading Dashboard + +Download the Dashboard installation package by visiting the download link on the [Cocos Creator Product Page](https://www.cocos.com/en/creator-download). + +Double-click the installation package after downloading. + +### Installation Instructions for Windows + +The installer for Windows is an `.exe` executable file, usually named **CocosDashboard-vX.X.X-win32-20XXXXXX.exe**, where **vX.X.X** is the version number of Cocos Dashboard, such as v1.0.11. The string of numbers after it is the version date number. + +> **Notes**: +> 1. If the version number installed on the current PC is the same as the version number of the installation package, the installation package with the same version number cannot be overwritten automatically, and it will be necessary to uninstall the previous version before continuing the installation. +> +> The default installation path of the application is `C:\CocosDashboard` and can be changed during the installation. +> +> 2. If there is an error about the digital signature of CAB file, please try to install with administrator privileges. +> +> 3. For some Windows PCs that are very old and have been on the intranet for a long time or have not been updated with OS patches for a long time, there may be some errors caused by missing dlls when running, try to install the system patch to solve: . +> +> 4. If the "Access Denied" pop-up window appears during installation, please make sure that the operating system installed on your machine is the official version of Microsoft, not a customized or streamlined third-party version. + +### Installation Instructions for MacOS + +The installer for Cocos Dashboard for MacOS is a `.dmg` file. Double-click the `.dmg` file, and drag and drop **CocosDashboard.app** into the **Applications** folder shortcut, or any other location. Then double-click the dragged **CocosDashboard.app** to get started. + +> **Notes**: +> 1. If Dashboard doesn't open after downloading, it indicates that the `.dmg` or app file is corrupted, from an unknown developer, or contains malware, etc. Please right-click the `.dmg` or app file in the Finder and select **Open**, then click **Open** again in the pop-up dialog box. Then please go to **System Preferences -> Security & Privacy** and click **Open Anyway** so that it can be launched normally later. +> +> 2. If you get a "corrupted, can't open" message during the installation process, you need to check if there is any software like Xcode occupying the files in the Dashboard installation directory. If so, exit, then uninstall Dashboard and reinstall it. + +### Operating System Requirements + +The supported system environments for Cocos Dashboard are: + +- The minimum supported version of Mac OS X is OS X 10.9. +- The minimum supported version of Windows is Windows 7 64-bit. + +## Running Cocos Dashboard + +On Windows, double-click the **CocosDashboard.exe** file in the `CocosDashboard` folder after unzipping to start Cocos Dashboard. + +On MacOS, double-click the dragged out **CocosDashboard.app** application icon to launch Cocos Dashboard. + +You can set a Quick Launch, Dock, or shortcut to the **CocosDashboard** app as desired. + +### Checking the Graphics Card Driver + +For some Windows operating systems and graphics card models, an error message such as the following may be encountered: + +``` +This browser does not support WebGL... +``` + +This is due to the editor relying on GPU rendering, which is not supported by the graphics card driver. If this happens, it can usually be fixed by making sure that the official driver for your graphics card is successfully installed. + +## Login with Cocos Developer Account + +When Cocos Dashboard is launched, the Cocos Developer Account login screen will appear. Once logged in, online services, product update notifications, and developer benefits will be available to use. + +To create a Cocos Developer account, sign up using the **Register** button on the login screen to go to the **Cocos Developer Center** or go directly to [this link](https://auth.cocos.com/#/sign_up/register?language=en) to register. + +Once registered, return to the Cocos Dashboard login screen and complete your login! After verifying your identity, we'll be in the Dashboard interface. In all cases, except for manual log out or expired login information, future logins will automatically occur with the saved information in the local session. diff --git a/versions/4.0/en/getting-started/install/index/dashboard-editor.png b/versions/4.0/en/getting-started/install/index/dashboard-editor.png new file mode 100644 index 0000000000..d5b0266a1e Binary files /dev/null and b/versions/4.0/en/getting-started/install/index/dashboard-editor.png differ diff --git a/versions/4.0/en/getting-started/install/index/language.png b/versions/4.0/en/getting-started/install/index/language.png new file mode 100644 index 0000000000..94587fe7fe Binary files /dev/null and b/versions/4.0/en/getting-started/install/index/language.png differ diff --git a/versions/4.0/en/getting-started/install/index/path.png b/versions/4.0/en/getting-started/install/index/path.png new file mode 100644 index 0000000000..4f3d854e81 Binary files /dev/null and b/versions/4.0/en/getting-started/install/index/path.png differ diff --git a/versions/4.0/en/getting-started/install/index/startup.png b/versions/4.0/en/getting-started/install/index/startup.png new file mode 100644 index 0000000000..83abb9fae5 Binary files /dev/null and b/versions/4.0/en/getting-started/install/index/startup.png differ diff --git a/versions/4.0/en/getting-started/install/index/waiting.png b/versions/4.0/en/getting-started/install/index/waiting.png new file mode 100644 index 0000000000..4b4c9c1cca Binary files /dev/null and b/versions/4.0/en/getting-started/install/index/waiting.png differ diff --git a/versions/4.0/en/getting-started/introduction/index.md b/versions/4.0/en/getting-started/introduction/index.md new file mode 100644 index 0000000000..9365eced9e --- /dev/null +++ b/versions/4.0/en/getting-started/introduction/index.md @@ -0,0 +1,93 @@ +# About Cocos Creator + +- **Q**: Is Cocos Creator a game engine?
+ **A**: It's a complete game development solution that includes a lightweight and efficient cross-platform game engine, and various UI tools that can help you develop games more quickly. + +- **Q**: What is the editor of Cocos Creator?
+ **A**: It is an all-in-one, extensible editor that simplifies resource management, game debugging and previewing, and multi-platform publishing. Support for Windows and Mac systems. + +- **Q**: I can't write programs, can I use Cocos Creator?
+ **A**: Of course! The Cocos Creator editor provides two workflows for design and development, allows designers to deeply participate in the game development process, making quick edits and iterations during the game development cycle, providing a simple and smooth way of working together. + +- **Q**: What platform can Cocos Creator let me develop for?
+ **A**: Cocos Creator supports all major platforms allowing games to be quickly released for the web, iOS, Android, Windows, Mac, and various mini-game platforms. A pure JavaScript-developed engine runtime is available on the web and mini-game platforms for better performance and smaller packages. On other native platforms, C++ is used to implement the underlying framework, providing greater operational efficiency. + +## What is Cocos Creator? + +Cocos Creator is a content creation-focused, scripted, component-based and data-driven game development tool. It features an easy-to-use content production workflow and a powerful developer tool suite for implementing game logic and high-performance game effects. + +## Workflow Description + +In the development stage, Cocos Creator has been able to bring great efficiency and creativity to users, but the workflow we offer goes far beyond the development. For a successful game, the entire workflow of development and debugging, commercial SDK integration, multi-platform release, testing, and launching is not only indispensable, but iterates through multiple iterations. + +![cocos workflow user](./work-flow.png) + +Cocos Creator integrates the entire mobile browser game solution into the editor tool, eliminating the need to shuttle between multiple software applications. With the Cocos Creator editor open, a one-click automated process takes the least amount of time and effort to solve all the above questions. Developers can focus on the development phase and increase product competitiveness and creativity! + +### Creating or importing resources + +You can complete resource importing by dragging and dropping resources such as images, sounds, etc. into the editor's **Assets** panel. + +In addition, you can create scenes, prefabs, animations, scripts, particles and other resources directly within the editor. + +### Building scene + +After having some basic resources in the project, we can start to set up the scene. The scene is the most basic organization of the game content, and it is also the basic form of showing the game to the player. + +### Adding component scripts to implement interactive functions + +We can mount various built-in components and custom script components for the nodes in the scene to enable the game logic to run and interact. This includes from the most basic animation playback and button responses, to the main loop script that drives the entire game logic and the control of the player's character. Almost all game logic functions are implemented by mounting scripts to nodes in the scene. + +### One-click preview and release + +In the process of setting up the scene and developing functions, you can click to preview your work, at any time, to see the current scene running. Use your phone to scan the QR code and instantly preview the game on your phone. When the development comes to an end, the **Build** panel allows you to publish games to multiple platforms, including desktops, mobile phones, and the Web. + +## Features + +Highlights of Cocos Creator's features include: + +- Data properties can be adjusted at any time in the editor and can be easily declared in scripts. Designers can even adjust parameters without interfering with code. +- UI system that support smart canvas adaptation and programming-free element alignment are perfectly adapted to device screens of any resolution. +- An animation system that supports animated track preview and complex curve editing. +- Scripting development using dynamic language support, you can use JavaScript to develop games, quickly preview and debug on physical machines and devices, and update your published games. TypeScript is also supported and can be mixed with your JavaScript code at the same time. +- The underlying layer evolved from Cocos2d-x, and maintain lightweight and high performance of native level while enjoying the convenience of scripted development +- Script componentization and an open plugin system provide developers with methods to customize workflows at different depths. The editor can be adjusted on a large scale to fit the needs of different teams and projects. +- It comes with an easy-to-follow content production workflow and a powerful suite of developer tools for high-performance game creation. + +## Framework Structure + +Cocos Creator includes the full set of features required for game development such as the game engine, resource management, scene editing, game preview, game publishing, and integrates all functions and toolchain into one unified application. + +While providing a powerful and complete toolchain, the editor provides an open plug-in architecture that allows developers to easily extend the editor's functionality and allows customizations to personalize workflows using front-end common technologies such as HTML + JavaScript. + +Below is the technical architecture diagram of Cocos Creator + +Cocos Creator structure editor +

Figure 1

+Cocos Creator structure engine +

Figure 2

+ +As you can see from the figure, the editor is a development environment built on top of the Electron framework. The engine is responsible for providing many easy-to-use components and a unified interface for each platform. + +The combination of the engine and the editor brings the data-driven and componentized approach to functional development, as well as the perfect division of labor between the design and the program: + +- Designers build the visual presentation of game scenes in the editor +- Programmers develop functional components that can be mounted to any object in the scene +- The designer is responsible for mounting the components for objects that need to exhibit specific behavior and improving the parameters through debugging +- The data structures and resources that programmers need to develop games +- Designers configure various data and resources through a graphical interface +- (From simple to complex, all kinds of workflows you can imagine can be implemented) + +Workflow-centric development concepts allow developers of different functions to quickly find work entry points that maximize their role, and fluently coordinate with other team members. + +## Instructions for use + +On the basis of data-driven workflow, the creation and editing of the scene becomes the focus of game development. The design work and functional development can be synchronized for seamless collaboration. Whether you're an artist, a designer, or a programmer, you can all click the **Preview** button at any point in the production process to test the latest state of the game in browser, mobile device simulators or physical device. + +Programmers and designers can now implement a wide range of division of labor, whether it is to build the scene first, then add functions, or to first produce functional modules and then combined debugging by designers, Cocos Creator can meet the needs of the development team. The properties defined in the script can be presented in the editor with the most appropriate visual experience to facilitate the content producer. + +Content resources outside the scene can be imported from outside, such as images, sounds, atlases, skeletal animations, etc. In addition, we are constantly improving the editor's production resources, including the currently completed animation editor, art people can use this tool to create very delicate and expressive animation resources, and can see the preview of the animation in the scene at any time. + +Supports both 2D and 3D game development with features that meet the specific needs of your various game types. Deep optimization of the editor experience and engine performance, and built-in support for middleware such as Spine, DragonBones, TiledMap, Box2D, and Texture Packer. + +Finally, the developed game can be published to each platform by one-click through a graphical tool, from design development to test release, Cocos Creator can handle everything for you. diff --git a/versions/4.0/en/getting-started/introduction/structure-editor.png b/versions/4.0/en/getting-started/introduction/structure-editor.png new file mode 100644 index 0000000000..c2356e5520 Binary files /dev/null and b/versions/4.0/en/getting-started/introduction/structure-editor.png differ diff --git a/versions/4.0/en/getting-started/introduction/structure-engine.png b/versions/4.0/en/getting-started/introduction/structure-engine.png new file mode 100644 index 0000000000..9d84d78813 Binary files /dev/null and b/versions/4.0/en/getting-started/introduction/structure-engine.png differ diff --git a/versions/4.0/en/getting-started/introduction/work-flow.png b/versions/4.0/en/getting-started/introduction/work-flow.png new file mode 100644 index 0000000000..8766798527 Binary files /dev/null and b/versions/4.0/en/getting-started/introduction/work-flow.png differ diff --git a/versions/4.0/en/getting-started/project-structure/index.md b/versions/4.0/en/getting-started/project-structure/index.md new file mode 100644 index 0000000000..780cf0a940 --- /dev/null +++ b/versions/4.0/en/getting-started/project-structure/index.md @@ -0,0 +1,67 @@ +# Project Structure + +Using the Dashboard, a new project can be created. `HelloWorld` is the classic first example. Once created, the project has a specific folder structure, and this structure can be used to get familiar with the Cocos Creator project folder structure. + +## Project Folder Structure + +After creating and opening a Cocos Creator project for the first time, the developer project folder will have the following structure. + +![project-file](index/project-file.png) + +- `assets`: the assets directory + +- `build`: build directory (generated after building a platform) + +- `library`: directory of imported assets + +- `local`: log files directory + +- `profiles`: editor configuration + +- `temp`: temporary files directory + +- `package.json`: project configuration + +### `assets` + +`assets` is used to hold all local assets, scripts, and third-party library files in the game. Only the contents of the `assets` directory will be displayed in the **Assets** panel. Each file in `assets` generates a `.meta` file with the same name after importing the project, which is used to store the corresponding asset configuration and index information. The `.meta` file needs to be submitted to the version control system as well, please review the [Asset Management Notes --- meta Files](../../asset/meta.md) documentation. + +Project or design source files generated by some third-party tools, such as `.tps` files for TexturePacker, or `.psd` files for Photoshop, can optionally be managed outside of `assets`. + +### `build` + +After publishing a project using the default publish path through **Project -> Build** in the editor main menu, the editor will create a `build` directory under the project path and store all builds for the target platform. + +### `library` + +The `library` is generated by importing the assets from `assets`, where the structure of the files and the format of the assets will be processed into the form needed for the final game release. + +When `library` is lost or corrupted, just delete the whole `library` folder and open the project again, and the repository will be regenerated. + +### `local` + +The `local` folder contains configuration information for the project on the local machine, including the editor panel layout, window size, location, and other information. Developers do not need to care about the contents here. + +### `profiles` + +The `profiles` folder contains configuration information for the editor, including build configuration information for each target platform, scene configuration information, etc. + +### `extensions` + +The `extensions` folder is used to place custom extensions for this project. To install extensions manually, create this folder manually. To uninstall the extensions, just delete the corresponding folder in `extensions`. + +### `settings` + +`settings` contains project-specific settings, such as configuration information in the **Project Settings** panel. To synchronize project settings between developers, please add the settings directory to version control. + +### `temp` + +`temp` is a temporary folder for caching some of Cocos Creator's temporary files locally. This folder can be deleted manually after closing Cocos Creator, and developers don't need to care about the contents. + +### `package.json` + +The `package.json` file, together with the `assets` folder, serves as a flag to verify the legitimacy of a Cocos Creator project, and only folders that include these two contents can be opened as Cocos Creator projects. Developers don't need to care about the contents inside. + +## Version control + +Cocos Creator automatically generates `.gitignore` files when a new project is created to exclude files that should not be committed to the git repository. If using another version control system, or if needing to commit the project elsewhere, be careful to only commit `assets`, `extensions`, `settings`, `package.json`, and any other manually added files associated with the project. diff --git a/versions/4.0/en/getting-started/project-structure/index/project-file.png b/versions/4.0/en/getting-started/project-structure/index/project-file.png new file mode 100644 index 0000000000..4a1db43820 Binary files /dev/null and b/versions/4.0/en/getting-started/project-structure/index/project-file.png differ diff --git a/versions/4.0/en/getting-started/support.md b/versions/4.0/en/getting-started/support.md new file mode 100644 index 0000000000..22aa35b966 --- /dev/null +++ b/versions/4.0/en/getting-started/support.md @@ -0,0 +1,43 @@ +# Get Help and Support + +## Official Medias + +More engine news, quality tutorials, and interviews can be viewed at [![twitter](support/twitter.png)](https://twitter.com/cocos2dx), [![facebook](support/facebook.png)](https://www.facebook.com/cocos2dx/) or [![youtube](support/youtube.png)](https://www.youtube.com/c/CocosEngine/videos). + +## Send Issues and Feedbacks + +Apart from the information provided in this guide, you can also obtain information from or send feedback to the Cocos team through the following channels at any time: + +- [Cocos Forum](https://discuss.cocos2d-x.org) +- [Cocos Support](https://www.cocos.com/en/assistant) + +- [Submit PR for Cocos Engine on GitHub](../submit-pr/submit-pr.md) +- [Submit Issues for Cocos Engine on GitHub](https://github.com/cocos/cocos4/issues/new/choose) + +## Third-party tools you may need + +### Code IDE + +- [VS Code](https://code.visualstudio.com/) Recommended coding environment for Cocos Creator. +- [WebStorm](https://www.jetbrains.com/webstorm/) + +### Tools for texture atlas + +- [TexturePacker](https://www.codeandweb.com/texturepacker) + +### Tools for Bitmap font + +- [Glyph Designer](https://71squared.com/glyphdesigner) +- [Hiero](https://github.com/libgdx/libgdx/wiki/Hiero) +- [BMFont (Windows)](http://www.angelcode.com/products/bmfont/) + +### Tools for 2D skeleton animation + +- [Spine](http://www.esotericsoftware.com) +- [Spriter](http://brashmonkey.com/) +- [DragonBones](http://dragonbones.github.io/) + +### Tools for 2D particles + +- [Particle Designer](http://particledesigner.71squared.com/) +- [Particle2dx](http://www.effecthub.com/particle2dx): Free online tool diff --git a/versions/4.0/en/getting-started/support/facebook.png b/versions/4.0/en/getting-started/support/facebook.png new file mode 100644 index 0000000000..180e1ab928 Binary files /dev/null and b/versions/4.0/en/getting-started/support/facebook.png differ diff --git a/versions/4.0/en/getting-started/support/twitter.png b/versions/4.0/en/getting-started/support/twitter.png new file mode 100644 index 0000000000..041a887987 Binary files /dev/null and b/versions/4.0/en/getting-started/support/twitter.png differ diff --git a/versions/4.0/en/getting-started/support/youtube.png b/versions/4.0/en/getting-started/support/youtube.png new file mode 100644 index 0000000000..180d6fa823 Binary files /dev/null and b/versions/4.0/en/getting-started/support/youtube.png differ diff --git a/versions/4.0/en/glossary/index.md b/versions/4.0/en/glossary/index.md new file mode 100644 index 0000000000..c651ef8084 --- /dev/null +++ b/versions/4.0/en/glossary/index.md @@ -0,0 +1,108 @@ +# Glossary + +In this chapter, we will collect and explain some glossaries and abbreviations in or related in Cocos Creator. + +## Contact us + +If you find any unexplained or poorly described glossaries, please send us feedback by clicking the "Have Feedback?" button on the right side of this page or [Get Help and Support](../getting-started/support.md). + +## Editor + +| Glossary | Description | +| :--- | :--- | +| Editor | Usually refers to the editor interface of Cocos Creator | +| Gizmo | Some controllers in the scene editor, such as for adjusting the position, rotation, scaling, or lighting of nodes, etc. | + +## Graphs + +| Glossary | Description | +| :--- | :--- | +| VB, Vertex Buffer | A vertex buffer is a set of vertices in a model, often described as an array, where each element of the array is a composite structure, including properties such as Position, Texture-coordinate, Color or Normal. | +| IB, Index Buffer | Index Buffer, which stores the indexes of the vertices in the above VB, through which the specific geometry is combined, example: If VB is \{ (0, 0, 0), (1, 0, 0), (1, 1, 0) \}, IB is [0, 2, 1], then the vertices of the triangle are drawn as follows: \{ VB[0], VB[2], VB[1] \} | +| FPS | Frame rate, the number of renderings per second. | +| Layer | refers to the grouping of nodes when rendering, and the [Visibility property of the camera](../editor/components/camera-component.md#camera-group-rendering) can be used to handle whether certain nodes are rendered by the camera. | +| LOD | Level of details,which improves rendering efficiency by having models with different levels of details at different levels. | + +### Illumination + +| Glossary | Description | +| :--- | :--- | +| GI, Global Illumination | Global illumination, or local illumination, refers to a set of algorithms used in computer graphics to calculate and simulate realistic light sources | +| LDR | Low Dynamic Range Light, usually refers to the color range of a single color between [0,255] (integer) or [0,1.0] (floating point), which is the color space that most display devices can display.| +| HDR | High Dynamic Range Light refers to colors whose color gamut is outside of the color space described in the above article. The colors in the display are likely to be outside of this color range, so they need to be converted to a region recognizable by the display using **Tone Mapping** when displayed on the display device.| + +## 2D/UI + +| Glossary | Description | +| :--- | :--- | +| 2D Object | A 2D object created by a right-click menu within the **Hierarchy Manager**. +| UI Object | A UI object created by the **Hierarchy Manager** right-click menu, usually encapsulating some UI element for interaction | +| Sprite Atlas | [Atlas](../asset/atlas.md), which is used to improve rendering efficiency by combining some pieces into a single large image, and the engine provides [auto-atlas](../asset/auto-atlas.md) function | + +## Animation + +| Glossary | Description | +| :--- | :--- | +| Spine | A 2D animation software whose output animation files can be recognized by Cocos Creator | +| DragonBone | A 2D animation software whose output animation files can be recognized by Cocos Creator | +| DragonBone | A 2D animation software whose output animation files can be recognized by Cocos Creator. +| Animation | Animation, the engine's built-in animation, no skinning, support for different nodes can be created through the editor animation function, [more](../animation/index.md) +| SkeletalAnimation | SkeletalAnimation, animation component with skinning, [more](../animation/skeletal-animation.md) | +| Marionette | An animation system that supports user-defined state maps, and SkeletalAnimation for rich functionality, [more](../animation/marionette/index.md) | + +## Physical + +| Glossary | Description | +| :--- | :--- | +| Force | The force that allows an object to move linearly | +| Torque | the force that makes an object rotate | + +### 2D Physics + +| Glossary | Description | +| :--- | :--- | +| Physics backends | The type of physics engine that the engine is wrapped in, the two types of physics backends provided by Cocos Creator are **Built-In** and **Box2D** | +| Box2D | Box2D is a well-known and efficient 2D physics engine | +| Rigid bodies | Define objects with mass that can be affected by forces and velocities, and are usually of four types: kinematic, dynamic, static and animated.| +| Joint2D | Physics joints are used to describe the physical connections between objects.| +| Physical Materials | Physical materials for adjusting the handling of friction and elasticity when two physical colliding bodies collide.| + +### 3D Physics + +| Glossary | Description | +| :--- | :--- | +| Physics Backend | The type of physics engine that the engine encapsulates. Cocos Creator provides several physics engines including: **Built-In**, **Bullet**, **PhysX** and **cannon.js** etc., which can be modified in the project settings to adapt to different platforms and situations, **Note**: The engine unifies most of the physics engine differences. However, some physics specific are only supported in specific physics backends. | +| Rigid body | Defines an object with mass that can be affected by forces and velocities, usually of three types: kinematic, dynamic, and static. | +| Constraint | A physical constraint that describes the physical connection between objects. +| Physical materials | Adjusts the handling of frictional and elastic forces when two physical colliding objects collide. | +| Force | The force that allows the object to move linearly. | +| Torque | Torque, the force that makes the object rotate | + +## Asset System + +| Glossary | Description | +| :--- | :--- | +| Bundle, Asset Bundle | A way of handling resources, where the engine algorithmically combines one or more resources into a single file (usually some kind of compressed file) for efficient storage and downloading | +| Resources | A special directory that the engine packages into a special bundle, which is loaded via methods such as `Resources.load` | +| Cocos Store | Cocos' resource store, where developers can sell and buy assets, source code, or complete commercial games | + +## Scripting and Programming + +| Glossary | Description | +| :--- | :--- | +| Component | A script class than inherits from `cc.Component`, and can be attach to a `cc.Node`. | +| Events | Cocos Creator based on [Web Event Standard](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events) implemented as an event system for responding to hardware input or communication between nodes, [more](../engine/event/index.md). | + +## Native Development + +| Glossary | Description | +| :--- | :--- | +| Native | Refers to the ability provided by Cocos Creator to run on any desktop native platform or mobile native platform without relying on a browser | +| CMake | A file format or tool for organizing C++ code | + +## Other Features + +| Glossary | Description | +| :--- | :--- | +| XR | Extended Reality. Cocos Creator extensions adapted to OpenXR can assist you in creating added reality features, [more](../xr/index.md). | +| AR | Augmented reality. Technology that connects the virtual world with reality, you can use Cocos Creator to implement your own AR applications. [more](../xr/index.md).| diff --git a/versions/4.0/en/graphics-backend/overview.md b/versions/4.0/en/graphics-backend/overview.md new file mode 100644 index 0000000000..f0cd7ff83a --- /dev/null +++ b/versions/4.0/en/graphics-backend/overview.md @@ -0,0 +1,15 @@ +# Overview + +Graphics backend is the native graphics API implementation of interface of GFX. Cocos creator supports: + +- Native + - GLES2 + - GLES3 + - Vulkan + - Metal + - NVN + - WebGPU on WASM + +- Web/MiniGames + - WebGL + - WebGL2.0 diff --git a/versions/4.0/en/guide/unity/index.md b/versions/4.0/en/guide/unity/index.md new file mode 100644 index 0000000000..d3ff5c1453 --- /dev/null +++ b/versions/4.0/en/guide/unity/index.md @@ -0,0 +1,354 @@ +# Cocos Creator Quick Guide for Unity Developers + +With the increasing variety of game platforms and portals, developers want to be able to write their games once and publish them multiple times to different platforms and portals, and Cocos Creator is a great way to fulfill this need. + +In this article, we will compare the following perspectives from a Unity developer's point of view to help Unity developers quickly get started with the Cocos Creator engine. + +- Installation and Versioning +- Editors +- Editor Workflow +- Scripting and Debugging +- Shaders + +## Installation and Versioning + +The Unity Hub can be used to manage Unity's editor versions, projects, and templates. In Cocos Creator, you can also manage the engine, projects, and templates via the [Cocos Dash Board](https://www.cocos.com/creator-download). + +| Unity Hub | Cocos Dashboard | +| :-- | :-- | +| ![unity-hub](./index/unity-hub.png) | ![cocos-dash-board](./index/cocos-dash-board.png) | + +You can also find a large selection of extensions, resources and source code in the Store page, as well as more material to learn in the Learn page. + +## Editor + +As a Unity developer, you can seamlessly use Cocos Creator's editor in most cases, and they have a similar editor layout and usage. + +| Unity Editor | Cocos Creator Editor | +| :-- | :-- | +| ![overview](./index/unity-editor-overview.png) | ![overview](./index/cocos-editor-overview.png) + +A slight difference is that Cocos Creator, because it is developed in Electronic+Chromium, allows you to both preview the game in a browser and run it directly in the editor. + +## Workflows + +Cocos Creator's 2D and 3D workflows are similar to Unity's, as you can read [Scene Creation Workflow](../../concepts/scene/index.md) to see Cocos Creator's workflow. + +### Texture Assets + +Importing texture assets is similar to Unity. + +| Unity | Cocos | +| :-- | :-- | +| | | + +Global texture compression can also be configured in the project settings + +![cocos-texture-compress.png](./index/cocos-texture-compress.png) + +### Models and animations + +Importing FBX in Cocos Creator is the same as Unity, just drag and drop or copy the files into the Assets directory of your project. + +| Unity | Cocos | +| :-- | :-- | +| | | + +Cocos Creator also supports glTF format files, as well as standard materials for DCC tools such as Maya and 3DMax. + +### Spine Animations + +Cocos Creator has a built-in Spine animation component, which you can use directly through the `spine.Skeleton` component. + +### Animation and State machine + +Cocos Creator supports keyframe animations, skeletal animations.You can edit and preview these animations directly in the editor. + +![cocos-keyframe-anim.png](./index/cocos-keyframe-anim.png) + +Similar to Unity's Animator, Cocos Creator supports the editing of animation state machines, which you can find in the [Marionette Animation System](../../animation/marionette/index.md). + +![./index/cocos-anim-graph.png](./index/cocos-anim-graph.png) + +### Music and sound effects + +Cocos Creator also supports Audio Source components for playing music and sound effects. + +![cocos-audio-source.png](./index/cocos-audio-source.png) + +### Assets Package + +Similar to Unity, Cocos Creator supports co-development by importing assets packages from the outside of the editor. + +| Unity | Cocos | +| :-- | :-- | +| | | + +### Build and Publish + +In addition to being published on various native platforms like Unity, Cocos Creator also supports publishing on small game platforms such as WeChat Small Games and TikTok Small Games. + +| Unity | Cocos | +| :-- | :-- | +| | | + +## Scripting and Debugging + +Unlike Unity's GameObject, in Cocos Creator the entities in the scene are named Node, but similar to Unity, Cocos Creator is an ECS (Entity-Component-System) architecture, and you can add different components to a Node to realize the functionality of the game. + +![cocos-add-component.png](./index/cocos-add-component.png) + +### Component Lifecycle + +Similar to Unity, Cocos Creator's components have their own lifecycle. The system facilitates the developer's business logic by calling back registered methods within the component. + +![组件生命周期](./index/script-lifecycle.png) + +### Writing Custom Components + +In Unity, we inherit from Monobehavior to implement our own game scripts. + +```csharp +public class Player : NetworkBehaviour +{ + Animation _animation; + + Start(){ + + _animation = gameObject.GetComponent(); + } +} +``` + +Cocos Creator uses Typescript to write scripts. + +The following example shows how to implement a custom component using Typescript. + +```ts +@ccclass('MotionController') +export class MotionController extends Component { + + animation: SkeletalAnimation; + + start() { + + this.animation = this.getComponent(SkeletalAnimation); + } +} +``` + +C# and Typescript are both programming languages developed by Microsoft and their ease of use is similar. + +### Debugging and Logging + +#### Logging Debugging + +To use logging in Unity we can use the `Debug.Log` method. + +To use logging in Cocos Creator, you can either use js's log printing `console.log()` or Cocos Creator's logging methods: + +```ts +cc.log() +cc.debug() +cc.error() +``` + +#### Breakpoint Debugging + +Unity can be debugged at breakpoints using Visual Studio or VSCode. + +Cocos Creator uses VSCode or developer tools directly in the browser. + +![cocos-debug.png](./index/cocos-debug.png) + +## Material and shader writing + +### Materials + +Cocos Creator materials have a similar preview and properties panel to Unity materials. + +| Unity | Cocos | +| :-- | :-- | +| | | + +Unlike Unity, Cocos Creator makes it easier to view and define the state of the rendering in the pipeline in materials. + +![cocos-pipeline-state.png](./index/cocos-pipeline-state.png) + +### Shaders + +Unlike Unity, which supports CG, GLSL, and HLSL, Cocos Creator only supports GLSL as a shader programming language. + +The table below compares the file formats they use and the differences in DSL. + +| | Unity | Cocos | +| :--- | :--- | :--- | +| File Format | *.shader | *.effect | +| DSL | Cg/HLSL/GLSL + Unity Shader Format | GLSL + Yaml | + +Unity uses a custom shader file as the DSL, while Cocos creator uses Yaml as the DSL file format. + +#### Shader syntax rules + +Unity Shader syntax rules. + +```shader +Shader "Transparent/Cutout/DiffuseDoubleside" { +Properties { + _Color ("Main Color", Color) = (1,1,1,1) + _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {} + _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5 +} + +SubShader { + Tags {"IgnoreProjector"="True" "RenderType"="TransparentCutout"} + LOD 200 + Cull Off + +CGPROGRAM +#pragma surface surf Lambert alphatest:_Cutoff + +sampler2D _MainTex; +float4 _Color; + +struct Input { + float2 uv_MainTex; +}; + +void surf (Input IN, inout SurfaceOutput o) { + half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; + o.Albedo = c.rgb; + o.Alpha = c.a; +} +ENDCG +} + +Fallback "Transparent/Cutout/VertexLit" +} + +``` + +Cocos Creator Shader syntax rules: + +``` +// Effect Syntax Guide: https://github.com/cocos-creator/docs-3d/blob/master/zh/material-system/effect-syntax.md + +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: general-vs:vert # builtin header + frag: unlit-fs:frag + properties: &props + mainTexture: { value: white } + mainColor: { value: [1, 1, 1, 1], editor: { type: color } } + - name: transparent + passes: + - vert: general-vs:vert # builtin header + frag: unlit-fs:frag + blendState: + targets: + - blend: true + blendSrc: src_alpha + blendDst: one_minus_src_alpha + blendSrcAlpha: src_alpha + blendDstAlpha: one_minus_src_alpha + properties: *props +}% + +CCProgram unlit-fs %{` + precision highp float; + #include + #include + + in vec2 v_uv; + uniform sampler2D mainTexture; + + uniform Constant { + vec4 mainColor; + }; + + vec4 frag () { + vec4 col = mainColor * texture(mainTexture, v_uv); + CC_APPLY_FOG(col); + return CCFragOutput(col); + } +}% + +``` + +### Shader Syntax Comparison + +This subsection compares the file structure of UnityShader and Cocos Effect. + +### Structural Comparisons + +Defining Shader Objects. + +- Unity shader: + + ```shader + Shader "" + { + + + + + } + ``` + +- Cocos Shader: + + ```yaml + CCEffect %{ + + + + + + + } + + ``` + +### Pass Structure + +- Unity Shader: + + ```shader + SubShader{ + + Tag {} + + Pass + } + + ``` + + Pass: + + ```shader + Pass{ + + + + + + } + ``` + +- CocosCreator Shader: + + ``` + CCProgram %{ + + + + + + + function vert(); + + function frag(); + }% + ``` diff --git a/versions/4.0/en/guide/unity/index/cocos-add-component.png b/versions/4.0/en/guide/unity/index/cocos-add-component.png new file mode 100644 index 0000000000..f170c79d1c Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-add-component.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-anim-graph.png b/versions/4.0/en/guide/unity/index/cocos-anim-graph.png new file mode 100644 index 0000000000..ff794b9a1c Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-anim-graph.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-audio-source.png b/versions/4.0/en/guide/unity/index/cocos-audio-source.png new file mode 100644 index 0000000000..3ddb1ea0fe Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-audio-source.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-build.png b/versions/4.0/en/guide/unity/index/cocos-build.png new file mode 100644 index 0000000000..ecc75fef5e Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-build.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-dash-board.png b/versions/4.0/en/guide/unity/index/cocos-dash-board.png new file mode 100644 index 0000000000..d5b0266a1e Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-dash-board.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-debug.png b/versions/4.0/en/guide/unity/index/cocos-debug.png new file mode 100644 index 0000000000..80d85fdd75 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-debug.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-editor-overview.png b/versions/4.0/en/guide/unity/index/cocos-editor-overview.png new file mode 100644 index 0000000000..80e8d330ab Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-editor-overview.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-import-package.png b/versions/4.0/en/guide/unity/index/cocos-import-package.png new file mode 100644 index 0000000000..f1a2f52fe6 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-import-package.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-keyframe-anim.png b/versions/4.0/en/guide/unity/index/cocos-keyframe-anim.png new file mode 100644 index 0000000000..58a4bd2573 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-keyframe-anim.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-material.png b/versions/4.0/en/guide/unity/index/cocos-material.png new file mode 100644 index 0000000000..40b1eed64f Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-material.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-model.png b/versions/4.0/en/guide/unity/index/cocos-model.png new file mode 100644 index 0000000000..ddacf4ea1e Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-model.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-pipeline-state.png b/versions/4.0/en/guide/unity/index/cocos-pipeline-state.png new file mode 100644 index 0000000000..bdff7da249 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-pipeline-state.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-texture-compress.png b/versions/4.0/en/guide/unity/index/cocos-texture-compress.png new file mode 100644 index 0000000000..fdb27975c5 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-texture-compress.png differ diff --git a/versions/4.0/en/guide/unity/index/cocos-texture-inspector.png b/versions/4.0/en/guide/unity/index/cocos-texture-inspector.png new file mode 100644 index 0000000000..1345ec3503 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/cocos-texture-inspector.png differ diff --git a/versions/4.0/en/guide/unity/index/script-lifecycle.png b/versions/4.0/en/guide/unity/index/script-lifecycle.png new file mode 100644 index 0000000000..84ea880faa Binary files /dev/null and b/versions/4.0/en/guide/unity/index/script-lifecycle.png differ diff --git a/versions/4.0/en/guide/unity/index/unity-build.png b/versions/4.0/en/guide/unity/index/unity-build.png new file mode 100644 index 0000000000..2090f596a9 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/unity-build.png differ diff --git a/versions/4.0/en/guide/unity/index/unity-editor-overview.png b/versions/4.0/en/guide/unity/index/unity-editor-overview.png new file mode 100644 index 0000000000..e53330762a Binary files /dev/null and b/versions/4.0/en/guide/unity/index/unity-editor-overview.png differ diff --git a/versions/4.0/en/guide/unity/index/unity-hub.png b/versions/4.0/en/guide/unity/index/unity-hub.png new file mode 100644 index 0000000000..cd1c6e61d4 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/unity-hub.png differ diff --git a/versions/4.0/en/guide/unity/index/unity-import-package.png b/versions/4.0/en/guide/unity/index/unity-import-package.png new file mode 100644 index 0000000000..abafe53bb8 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/unity-import-package.png differ diff --git a/versions/4.0/en/guide/unity/index/unity-material.png b/versions/4.0/en/guide/unity/index/unity-material.png new file mode 100644 index 0000000000..3610223813 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/unity-material.png differ diff --git a/versions/4.0/en/guide/unity/index/unity-model.png b/versions/4.0/en/guide/unity/index/unity-model.png new file mode 100644 index 0000000000..10b64c569f Binary files /dev/null and b/versions/4.0/en/guide/unity/index/unity-model.png differ diff --git a/versions/4.0/en/guide/unity/index/unity-texture-inspector.png b/versions/4.0/en/guide/unity/index/unity-texture-inspector.png new file mode 100644 index 0000000000..2c625dc303 Binary files /dev/null and b/versions/4.0/en/guide/unity/index/unity-texture-inspector.png differ diff --git a/versions/4.0/en/importer/materials/cocos-viewport.png b/versions/4.0/en/importer/materials/cocos-viewport.png new file mode 100644 index 0000000000..cb7e2ec5f2 Binary files /dev/null and b/versions/4.0/en/importer/materials/cocos-viewport.png differ diff --git a/versions/4.0/en/importer/materials/enable-smart-conversion.png b/versions/4.0/en/importer/materials/enable-smart-conversion.png new file mode 100644 index 0000000000..03eb767d4c Binary files /dev/null and b/versions/4.0/en/importer/materials/enable-smart-conversion.png differ diff --git a/versions/4.0/en/importer/materials/fbx-materials.md b/versions/4.0/en/importer/materials/fbx-materials.md new file mode 100644 index 0000000000..b4f921db18 --- /dev/null +++ b/versions/4.0/en/importer/materials/fbx-materials.md @@ -0,0 +1,125 @@ +# FBX Smart Material Conversion + +FBX Smart Material Conversion is a feature of model importer, it can map some standard materials in DCC(Digital Content Creation) tools to Cocos Creator's built-in materials, and restore the visual effects of materials in DCC tools. This feature was added in v3.5.1. + +The following materials are supported: + +| | Phong | PBR | +|---------|---------------------|-------------------| +| 3ds Max | Standard(legacy) | Physical Material | +| Blender | N/A | Principled BSDF | +| C4D | Standard | N/A | +| Maya | Lambert/Blinn/Phong | Standard Surface | + +Here is the comparison of a model using maya standard surface viewed in Maya and viewed in Cocos Creator after import: + +| Maya Viewport | Cocos Creator Viewport | +|----------------------------|------------------------------| +| ![Maya](maya-viewport.png) | ![cocos](cocos-viewport.png) | + +You can refer to the following project file to see how the example model with Maya Standard Surface is imported. [Maya Car Demo](maya_car.zip) + +## Smart Material Conversion configuration and import process for FBX assets + +1. In the Cocos Creator main menu, go to Project -> Project Settings -> Model -> Smart Material Conversion and make sure the option is enabled. +2. Select the FBX file in Cocos Creator, and make sure the Smart Material Conversion option is enabled in the Inspector panel of the FBX file. + +The following is the final settings: + +![img_6.png](enable-smart-conversion.png) + +## Different DCC materials support in detail + +### Autodesk 3ds Max + +- Material:The following material types are supported: + - Standard(Legacy) + - Physical Material (Recommended) + - Multi/SubObject + - Multi/SubObject's sub-material type can only be Standard or Physical Material, and can't be Multi/SubObject, otherwise, the material will be lost. +- Texture: + - The following texture channels are supported for Standard(Legacy) material: + - Diffuse Color + - Specular Color + - Glossiness + - Opacity + - Bump + - The following texture channels are supported for Physical Material: + - Base Color + - Roughness + - Metalness + - Bump + - Opacity + - Texture simplification requirements: + - Before exporting FBX, please make sure the texture nodes on the right side of the material are all Bitmap nodes. + - Simplification method: [Convert a Procedural texture into a bitmap image texture in 3ds Max](https://knowledge.autodesk.com/support/3ds-Max/learn-explore/caas/sfdcarticles/sfdcarticles/How-to-convert-a-Procedural-texture-into-a-bitmap-image-texture-in-3ds-Max-for-fbx-export.html) + +| Before Simplification | After Simplification | +|-----------------------|-------------------------| +| ![img.png](img.png) | ![img_1.png](img_1.png) | + +Rendering Note: For Max Physical Material, you need to enable High Quality Rendering in Max Viewport to get a relatively accurate preview. For a more accurate preview, you can pair Max and Cocos Creator rendering environment. +- For more information on Max Viewport Rendering Settings, you can refer to [tutorial](https://www.youtube.com/watch?v=82hhg8Q1nus&list=PL9xXzsdQ6pbZGBnVSKMBO_BCYjzmFTj0R&index=2) +- For more information on Cocos Creator Viewport Rendering Settings, you can refer to [Cocos Creator Document](https://docs.cocos.com/creator/manual/zh/module-map/graphics.html) + +### Autodesk Maya + +- Material:The following material types are supported: + - Lambert,Blinn,Phong,Phong-E + - Standard Surface (Recommended) +- Texture: + - The following texture channels are supported for lambert,blinn,phong: + - Color + - Normal + - Transparency + - SpecularColor + - Cosine Power + - The following texture channels are supported for Standard Surface: + - Base Color + - Specular Roughness + - Metalness + - Normal + - Alpha + - Texture simplification requirements: + - Before exporting FBX, please make sure the texture nodes on the right side of the material are all File nodes. + - Simplification method: [Convert a Procedural texture into a File Texture in Maya](https://knowledge.autodesk.com/support/Maya/learn-explore/caas/CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-0F504570-CB7A-49D3-A7A2-83438C353A9C-htm.html) + +| Before Simplification | After Simplification | +|-------------------------|--------------------------| +| ![img_2.png](img_2.png) | ![img_3.png](img_3.png) | + +Rendering Note: For transparent material, you need to enable Depth peeling and Alpha Cut Prepass in maya. In order to get a more accurate preview, you can pair Maya and Cocos Creator rendering environment. +- For more information on Maya Viewport Rendering Settings, you can refer to [Maya Document](https://help.autodesk.com/view/MAYAUL/2022/ENU/) +- For more information on Cocos Creator Viewport Rendering Settings, you can refer to [Cocos Creator Document](https://docs.cocos.com/creator/manual/zh/module-map/graphics.html) + +### Cinema 4D + +- Material:The following material type is supported: + - Standard Material +- Texture: + - The following texture channels are supported : + - Diffuse Color + - Specular Color + - Glossiness + - Opacity + - Bump + - Texture export requirements: Before exporting to FBX, if a model has more than one material, you need to ensure that the faces and materials in the UV set have a unique correspondence. + - [Example](https://github.com/cocos-creator/3d-tasks/issues/11267) + +### Blender + +- Material:The following material type is supported: + - Principled bsdf +- Texture: + - The following texture channels are supported : + - Base Color + - Roughness + - Metallic + - Normal + - Alpha + - Texture simplification requirements: Only file texture node and normal map node are supported. + - Simplification method: [Baking Procedural Materials to Image Textures in Blender](https://www.youtube.com/watch?v=AB24ITZHtuE) + +| Before Simplification | After Simplification | +|-------------------------|-------------------------| +| ![img_4.png](img_4.png) | ![img_5.png](img_5.png) | diff --git a/versions/4.0/en/importer/materials/img.png b/versions/4.0/en/importer/materials/img.png new file mode 100644 index 0000000000..f100c6ec7e Binary files /dev/null and b/versions/4.0/en/importer/materials/img.png differ diff --git a/versions/4.0/en/importer/materials/img_1.png b/versions/4.0/en/importer/materials/img_1.png new file mode 100644 index 0000000000..cbb4486c3c Binary files /dev/null and b/versions/4.0/en/importer/materials/img_1.png differ diff --git a/versions/4.0/en/importer/materials/img_2.png b/versions/4.0/en/importer/materials/img_2.png new file mode 100644 index 0000000000..ae4592d89f Binary files /dev/null and b/versions/4.0/en/importer/materials/img_2.png differ diff --git a/versions/4.0/en/importer/materials/img_3.png b/versions/4.0/en/importer/materials/img_3.png new file mode 100644 index 0000000000..3790b3180e Binary files /dev/null and b/versions/4.0/en/importer/materials/img_3.png differ diff --git a/versions/4.0/en/importer/materials/img_4.png b/versions/4.0/en/importer/materials/img_4.png new file mode 100644 index 0000000000..b5ede6560e Binary files /dev/null and b/versions/4.0/en/importer/materials/img_4.png differ diff --git a/versions/4.0/en/importer/materials/img_5.png b/versions/4.0/en/importer/materials/img_5.png new file mode 100644 index 0000000000..478f2aeb00 Binary files /dev/null and b/versions/4.0/en/importer/materials/img_5.png differ diff --git a/versions/4.0/en/importer/materials/maya-viewport.png b/versions/4.0/en/importer/materials/maya-viewport.png new file mode 100644 index 0000000000..ce49d01b8e Binary files /dev/null and b/versions/4.0/en/importer/materials/maya-viewport.png differ diff --git a/versions/4.0/en/importer/materials/maya_car.zip b/versions/4.0/en/importer/materials/maya_car.zip new file mode 100644 index 0000000000..56b24fb8c7 Binary files /dev/null and b/versions/4.0/en/importer/materials/maya_car.zip differ diff --git a/versions/4.0/en/index.md b/versions/4.0/en/index.md new file mode 100644 index 0000000000..f4a10c10e9 --- /dev/null +++ b/versions/4.0/en/index.md @@ -0,0 +1,52 @@ +# Cocos Creator User Manual 4.0 + +Welcome to use Cocos Creator 4.0! + +Cocos Creator is both an efficient, lightweight, free and open source cross-platform 2D & 3D graphics engine and a real-time interactive 2D & 3D digital content creation platform. With many advantages such as **High Performance**, **Low Power Consumption**, **Streaming Loading** and **Cross-Platform Publishing**, you can use it to create projects in fields such as **Games**, **Cars**, **XR**, **MetaVerse** etc. + +This manual contains detailed instructions, workflows for different positions and tutorials for beginners. +It helps you learn how to use the Cocos Creator and its associated services. + +You can read it from begin to end, or just use it as a reference. + +If you are the first time to use Cocos Creator, take a look at the [Getting Started](getting-started/index.md), and see the [Examples & Tutorials](./cases-and-tutorials/index.md). + +If you're already familiar with other engines like Unity, you can check out the [Unity Developer's Quick Guide to Cocos Creator](./guide/unity/index.md) to get started with Cocos Creator. + +## User Manual sections + +- [Cocos Creator Basic Usage](getting-started/index.md) +- [Scene Creation](concepts/scene/index.md) +- [Assets System](asset/index.md) +- [Scripting Guide and Event System](scripting/index.md) +- [Cross-Platform Publishing](editor/publish/index.md) +- [Graphics Rendering](module-map/graphics.md) +- [2D Rendering](2d-object/2d-render/index.md) +- [UI System](2d-object/ui-system/index.md) +- [Animation System](animation/index.md) +- [Audio System](audio-system/overview.md) +- [Physics System](physics/index.md) +- [Particle System](particle-system/index.md) +- [Tween System](tween/index.md) +- [Terrain System](editor/terrain/index.md) +- [Asset Manager](asset/asset-manager.md) +- [Localization](editor/l10n/overview.md) +- [Editor Extension](editor/extension/readme.md) +- [Advanced Topics](advanced-topics/index.md) + +## More + +- [Cocos Forum](https://discuss.cocos2d-x.org/):ask questions, find anwsers and communicate with other developers +- [Examples & Tutorials](./cases-and-tutorials/index.md):learning resources of Cocos Creator +- [Cocos Store](https://store.cocos.com):access to get more art assets, learning demos and source codes + +## Product Line Overview + +Over the years, Cocos (Xiamen Yaji Software) has been continuously developing and has released several product lines closely related to Cocos Creator. To avoid confusion, here is a brief introduction to these products: +- **Cocos Creator 3.x**: Released in early 2021, it is the latest version of Cocos Creator, and has been validated through numerous commercial projects. 3.x completely abandons the Cocos2d-x base, adopting a brand new high-performance cross-platform 3D core. This marks the official development of Cocos Creator into a comprehensive pan-mobile 3D game engine. Since the 3.x base has been completely rewritten, Cocos Creator is no longer considered a direct extension and upgrade of Cocos2d-x. +- **Cocos Creator 2.x**: Released in 2018, updates ceased in 2023. All capabilities have been inherited by Cocos Creator 3.x, so it is recommended that new projects use the [latest Cocos Creator 3.x](https://www.cocos.com/creator-download). +- **Cocos Creator 3D**: Initiated in 2017, it underwent small-scale testing in China for over a year as Cocos Creator 3D at the end of 2019, and later officially merged into Cocos Creator 3.0. Since it has been replaced by Cocos Creator 3.x and is no longer updated separately, Cocos Creator 3D usually refers to Cocos Creator's own 3D capabilities rather than this specific version. +- **[Cocos2d-x](https://www.cocos.com/cocos2d-x)**: Released in 2010, it ceased updates in 2019. This is the most active branch of the Cocos2d community, and the underlying runtime initially adopted by Cocos Creator 2.x was the upgraded Cocos2d-x. +- **Cocos**: When Cocos appears as the engine name alone, it usually represents Cocos Creator 3.x, rather than Cocos2d-x. + +After years of rapid development, there are significant differences in usage between Cocos Creator 3.x and Cocos Creator 2.x, and their APIs are not fully compatible. Therefore, when developers consult documentation, APIs, and tutorials, please pay attention to distinguish whether the target version is 2.x or 3.x to avoid errors due to version inconsistency. diff --git a/versions/4.0/en/material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md b/versions/4.0/en/material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md new file mode 100644 index 0000000000..0d5c747c64 --- /dev/null +++ b/versions/4.0/en/material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md @@ -0,0 +1,158 @@ +# Cocos Creator 3.1 Material Upgrade Guide + +> This article will detail the considerations for upgrading Cocos Creator 3.0 materials to v3.1. + +## 1. Shader upgrades and changes + +### 1.1 Built-in header file changes + +The standard shader header `shading-standard` from v3.0 has become `standard-surface-entry` from v3.1, making the effect compatible with both the forward render pipeline and the deferred render pipeline. + +The `cc-fog` header file from v3.0 is now `cc-fog-vs/fs` from v3.1, split into vertex shader and fragment shader versions. + +### 1.2 Vertex shaders + +- `gl_Position` + + The main function name of `VS` in v3.1 has been changed from `vert` to `main`, and a new macro `gl_Position` has been added to assign a value to the return value. + + ```c + CCProgram standard-vs %{ + precision highp float; + + // Include your headfile + + #include // Note the change in the header file name here + + // Fill in your data here + + void main () { + + // Fill in your data here + + gl_Position = fill in your data result; + } + }% + ``` + +### 1.3 Fragment shaders + +- `CC_STANDARD_SURFACE_ENTRY()` + + Load the standard shader header file `standard-surface-entry` and use the v3.1 standard shader output function `CC_STANDARD_SURFACE_ENTRY()` to replace the original v3.0 shader output function `frag()`. + + ```c + CCProgram standard-fs %{ + + // Include your headfile + + #include // Note the change in the header file name here + #include // Note the change in the name of the standard shader header file here + + // Fill in your data here + + void surf (out StandardSurface s) { + + // Fill in your data here + + } + CC_STANDARD_SURFACE_ENTRY() // Standard shader output function + }% + ``` + +## 2. Deferred Render Pipeline + +### 2.1 Deferred Render Pipeline + +The biggest difference between v3.1 and v3.0 is that v3.1 supports the [deferred render pipeline](../render-pipeline/builtin-pipeline.md). The engine comes with a standard `standard-surface-entry` header file that supports both the forward render pipeline and the deferred render pipeline, which is used as follows: + +```c +CCEffect %{ + techniques: + + // Fill in your data here + + - &deferred + vert: // your Vertex shader + frag: // your Fragment shader + phase: deferred + propertyIndex: 0 + blendState: + targets: // turn off blending + - blend: false + - blend: false + - blend: false + - blend: false + properties: // your properties name + + // Fill in your data here + +}% + +// fill in your data here + +CCProgram standard-fs %{ + precision highp float; + #include + #include + #include // Note the change in the header file name here. + #include // Note the change in the name of the standard shader header file here + + // Fill in your data here + void surf (out StandardSurface s) { + + // Fill in your data here + + } + CC_STANDARD_SURFACE_ENTRY() // Standard shader output function +}% + +// fill in your data here + +``` + +### 2.2 Render pipeline determination + +The header file `standard-surface-entry` determines which render pipeline is selected, and the lighting calculation is in the file `shading-standard-additive`. + +If it is a deferred render pipeline, the `deferred-lighting` effect file is called first, followed by the light calculation file `shading-standard-additive`. + +```c +#define CC_STANDARD_SURFACE_ENTRY() +#if CC_FORWARD_ADD + #include + + // Fill in your data here + +#elif CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_FORWARD // Determine if it is the forward render pipeline + + // Fill in your data here + +#elif CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_DEFERRED // Determine if it is the deferred render pipeline + + // Fill in your data here + +#endif +``` + +## 3. Parameter transfer upgrade + +The macro for passing shadow parameters from vertex shaders to fragment shaders was originally `CCPassShadowParams` in v3.0, but was changed to `CC_TRANSFER_SHADOW` in v3.1. + +The v3.1 vertex shader transfers `FOG` parameters to the fragment shader, using the `CC_TRANSFER_FOG` macro directly. + +Version comparison: + +- v3.0 + + ```c + v_fog_factor = CC_TRANSFER_FOG(pos); + CCPassShadowParams(pos); + ``` + +- v3.1 + + ```c + CC_TRANSFER_FOG(pos); + CC_TRANSFER_SHADOW(pos); + ``` diff --git a/versions/4.0/en/material-system/builtin-material.md b/versions/4.0/en/material-system/builtin-material.md new file mode 100644 index 0000000000..29eddc402f --- /dev/null +++ b/versions/4.0/en/material-system/builtin-material.md @@ -0,0 +1,12 @@ +# Builtin Materials + +Creator has built-in several common materials in the **internal -> default_materials** directory of the **Assets** panel. The Effect used is [built-in shader](../shader/effect-builtin.md), the properties of the built-in materials are not allowed to be modified. + +![builtin material](img/builtin-material.png) + +| Builtin material | description | +|:--|:--| +|default-material| [Standard Material](../shader/effect-builtin-pbr.md) for opaque objects | +|default-material-transparent| [Standard Material](../shader/effect-builtin-pbr.md) for transparent objects | +|particle-add| Standard material for particle system | +|ui-sprite-material| Standard material for sprite | diff --git a/versions/4.0/en/material-system/effect-2.x-to-3.0.md b/versions/4.0/en/material-system/effect-2.x-to-3.0.md new file mode 100644 index 0000000000..134025cf80 --- /dev/null +++ b/versions/4.0/en/material-system/effect-2.x-to-3.0.md @@ -0,0 +1,254 @@ +# Cocos Creator 3.0 Material Upgrade Guide + +> This article will detail the considerations for upgrading Cocos Creator 2.x materials to v3.0. + +## 1. Introduction to the base design of the material system + +### 1.1 The material system framework of Cocos Creator + +The material system consists of four core classes from top to bottom: **Material**, **Effect**, **Technique** and **Pass**, whose relationship can be understood by the following class diagram: + +![effect](material.png) + +#### Material + +The Material asset can be seen as an asset instance of the EffectAsset in the scene, and its own configurable parameters are effectAsset, technique, defines, states. + +#### Effect + +An Effect asset represents a material type and is the most important core asset in the material system. To implement a custom shading effect in the engine, it is necessary to write a custom Effect. + +#### Technique + +The solution to accomplish a final effect is referred to as a rendering technique , and a technique can be fused by one or more passes. + +#### Pass + +A Pass is a single GPU draw, typically including a vertex shader and a fragment shader, and there are many optional configuration parameters for Pass in Creator. + +### 1.2 Material Instance Panel + +The Material instance panel is the most intuitive material editing window for all developers, and all actual material instances are configured through it. + +The Material instance panel in Cocos Creator 3.0 is as follows: + +![effect](material3.png) + +The Material instance panel of Cocos Creator 2.x looks like this: + +![effect](material10.jpg) + +Notice from the above two figures, the instance panel in v3.0 is quite complex compared to v2.4, partly because of the increased complexity of the material configuration, and partly because of the enhanced functionality of the panel. + +#### The configurable items of the material panel are divided into five main types + +1. Effect asset: the drop-down box will list all the Effect assets in the current project, and developers can select the Effect asset used by the current material. Other properties will be reset as default when the Effect is switched. +2. Technique rendering technique selection: the drop-down box will list all the Technique in the Effect asset currently in use, and there may be multiple Technique in the Effect asset, each Technique is suitable for different situations. For example, the Technique with less effect but better performance is more suitable for mobile platform. +3. Macro options defined in Effect: these macros control whether code branches are enabled in shader programs. +4. The list of properties defined in the Effect, dynamically chose according to the macro definition. They are shown in different input types as defined type in the effect, e.g.: number, color. The editable properties are generally the mapping of uniforms used by the shader, from v3.0, it's possible to specify the mapping of a property to a component in vector uniform by using the target parameter in Effect. +5. v3.0 also added the PipelineStates option, which is mainly used to define the pipeline states of one pass, such as DepthStencilState, BlendState, CullMode, etc. + +### 1.3 Editor Experience + +There are also some differences between v2.x and v3.0 in the editor experience of the material system. + +In v3.0, after selecting the node containing the model and material in the **Hierarchy** panel, the **Inspector** panel will display the component properties and the detailed Material configuration panel as follows. + +![effect](material-v3.png) + +In v2.x, when the node containing the model and material is selected in the **Node Tree** panel, the **Properties** panel will only show the component properties, not the detailed Material configuration panel: + +![effect](material-v2x.png) + +Instead, you need to jump to the **Assets** panel and select the Material asset before you can edit it in the **Properties** panel: + +![effect](material-panel-v2x.png) + +## 2. Effect Assets + +This section describes the commonalities and differences between assets in v2.x and v3.0. + +### 2.1 Effect format and content + +For the Effect asset, both v2.x and v3.0 use the YAML1.2 standard syntax and parser, and there are few differences between the two versions. The Effect asset defines a material type, and by following the [syntax format](yaml-101.md), the following information can be defined: + +- Technique list of rendering techniques +- A list of Pass for each Technique +- A list of editable properties exposed to the editor in each Pass (including data type designation within the editor, component mapping relationships, etc.) +- Shader programs for each Pass, including vertex and fragment shader programs + +In terms of syntax details, such as Property declarations and macro definitions, the approach is the same: + +![effect](material4.jpg) + +![effect](material4.1.jpg) + +![effect](material5.jpg) + +### 2.2 Built-in Effect Types + +There is a big difference between v2.x and v3.0 in terms of preset materials. + +- The preset materials in v2.x include 2D Sprite, classic `blinn-phong` lighting materials, `unlit` materials, default `toon` materials, particle materials, etc. + +- The preset materials in v3.0 are based on physically based rendering, including standard PBR material, Skybox, cartoon style material, 3D particle materials (CPU & GPU), particle trailing materials, traditional 2D Sprite materials, etc. + +The v3.0 default standard material supports the standard Physically Based Rendering (PBR) process, which contains a lot of mapping information to enhance the quality and realism of the material, such as diffuse map, normal map, metallic texture, roughness texture, ambient light occlusion texture, and so on. The whole algorithm is based on the standard BRDF lighting model, which is not available in v2.x. The overall rendering quality of v3.0 is also much more realistic than v2.x. + +### 2.3 Effect Writing Details Differences + +Although the syntax rules of Effect in Cocos Creator are basically the same in v2.x and v3.0, there are still differences in many built-in header files, variable names and function names. + +For example, in v3.0, it was `cc_mainLitDir` and the header file `cc-global` was included. In v2.x, you need to use `cc_lightDirection[i]` to get the direction of the light source, and you need to include the header `cc-lights`. See the third point below in the **API Upgrade Guide** section for more details on the differences. + +Some default shader functions are unique to v3.0, such as `CCStandardShading`, `CCToonShading`, etc., which are not available on v2.x. Again, see the third point below in the **API Upgrade Guide** section. + +Regarding uniform declarations, v3.0 forces the use of UBO for organization, and the minimum unit for memory layout is vec4, which no longer supports separate declarations of float or vec3 types of uniform. + +In terms of header files, v3.0 has built-in editor header assets in the `assets/chunks` directory of **Internal DB**. They can be referenced directly with file name instead of file path, mainly for including some common **tool functions** and **standard shading functions**. The header files of v2.x are built into the editor, it is not possible see what they are. + +### 2.4 New Pass Options + +v3.0 adds some new Pass options: + +- `PropertyIndex`: specify which `pass` this `pass`'s runtime uniform property data should be consistent with, e.g.: a `pass` such as `forward add` needs to be consistent with the `base pass` to ensure proper rendering. Once this parameter is specified, the material panel will no longer display any properties of this `pass`. + +- `embeddedMacros`: specifies additional constant macros to be defined on top of the `shader` for this `pass`. This parameter can be used to reuse `shader` assets when only the macro definition differs in multiple `pass` `shaders`. + +For more details on the pass parameters, please refer to the [parameter list](./pass-parameter-list.md) documentation. + +## 3. API Upgrade Guide + +### 3.1 Built-in Uniform Difference List + +If you want to use built-in variables in shader, you need to include the corresponding header file. The following table is a summary of commonly used functional uniforms, many of which are the same as v2.x and v3.0, and some of which are different. + +| v2.x Header & Name | v3.0 Header & Name| Type | Usage | Version Difference | +| :------ | :------ | :----- | :------ | :------ | +| `cc-local.chunk` & `cc_matWorld` | `cc-local.chunk` & `cc_matWorld` | mat4 | Model space to world space matrix | no difference | +| `cc-local.chunk` & `cc_matWorldIT` | `cc-local.chunk` & `cc_matWorldIT` | mat4 | Model space to world space inverse substitution matrix | no difference | +| `cc-global.chunk` & `cc_time` | `cc-global.chunk` & `cc_time` |vec4 | x: global time in seconds since start
y: incremental time of current frame
z: total number of frames since start |no difference | +| `cc-global.chunk` & `cc_screenSize` | `cc-global.chunk` & `cc_screenSize` | vec4 | xy: screen size
zw: inverse of the screen size | no difference | +| `cc-global.chunk` & `cc_screenScale` | `cc-global.chunk` & `cc_screenScale` | vec4 | xy: screen scale
zw: inverse screen scale | no difference | +| none | `cc-global.chunk` & `cc_nativeSize` | vec4 | xy: the size of the actual shading buffer
zw: the inverse of the size of the actual shading buffer | new in v3.0, not in v2.x | +| `cc-global.chunk` & `cc_matView` | `cc-global.chunk` & `cc_matView` | mat4 | view matrix | no difference | +| `cc-global.chunk` & `cc_matViewInv` | `cc-global.chunk` & `cc_matViewInv` | mat4 | view inverse matrix | no difference | +| `cc-global.chunk` & `cc_matProj` | `cc-global.chunk` & `cc_matProj` | mat4 | projection matrix | no difference | +| `cc-global.chunk` & `cc_matProjInv` | `cc-global.chunk` & `cc_matProjInv` | mat4 | projection inverse matrix | no difference | +| `cc-global.chunk` & `cc_matViewProj` | `cc-global.chunk` & `cc_matViewProj` | mat4 | view projection matrix | no difference | +| `cc-global.chunk` & `cc_matViewProjInv` | `cc-global.chunk` & `cc_matViewProjInv` | mat4 | view projection inverse matrix | no difference | +| `cc-global.chunk` & `cc_cameraPos` | `cc-global.chunk` & `cc_cameraPos` | vec4 | xyz: camera position | no difference | +| none | `cc-global.chunk` & `cc_exposure` | vec4 | x: camera exposure
y: camera exposure countdown
z: whether HDR is enabled
w: HDR to LDR scaling parameter | new feature in v3.0, not in v2.x | + +**Also, v2.x and v3.0 are very different in terms of light sources and shadows, and v3.0 is a big improvement over v2.x. The following table lists some common features of uniform.** + +| v2.x Header & Name| v3.0 Header & Name| Type | Usage | Version Differences | +| :------ | :------ | :----- | :------ | :------ | +| `cc-lights.chunk` & `cc_lightDirection[CC_MAX_LIGHTS]` | `cc-global.chunk` & `cc_mainLitDir` | vec4 | Get light direction | v2.x: How many lights are affected by a single model drawing at a time in the shader The default maximum value is 1.0. To get the position information, fill in 0. For example, `cc_lightDirection[0]`
v3.0: xyz: main direction light direction +| `cc-lights.chunk` & `cc_lightColor[CC_MAX_LIGHTS]` | `cc-global.chunk` & `cc_mainLitColor` | vec4 | Control the color intensity of the light | v2.x: exp of the light, which is the light's pow intensity.
v3.0: xyz - the main directional light color; w - the main directional light intensity | +| `cc-lights.chunk` & `CC_CALC_LIGHTS` | `cc-global.chunk` & `cc_ambientSky` |v2.x: macro definition
v3.0: vec4| controls the sky color intensity | v2.x: this is a macro that is computed by threading in the ambient parameter for calculation. And there are function overloads that can be passed in with different parameters.
v3.0: xyz - sky color; w - brightness | +| none | `cc-global.chunk` & `cc_ambientGround` |vec4| xyz: ground reflected light color | new feature in v3.0, not in v2.x | +| none | `cc-environment.chunk` & `cc_environment` | samplerCube | xyz: IBL environment mapping | new feature in v3.0, not in v2.x | + +### 3.2 Shader Built-In Functions and Variables + +In v3.0, to interface with the engine's dynamic batching and instancing processes, include the `cc-local-batch` header file and get the world matrix via the `CCGetWorldMatrix` utility function. + +#### New shading functions in v3.0 + +- `CCStandardShading` + + The function name `CCStandardShading` needs to contain the header file `shading-standard.chunk`, which is used to perform the lighting calculations that make up the surface shader process. + + ```c + #include + void surf (out StandardSurface s) { + // fill in your data here + } + vec4 frag () { + StandardSurface s; surf(s); + vec4 color = CCStandardShading(s); + return CCFragOutput(color); + } + ``` + + It is easy to implement custom surface input, or other shading algorithms in this framework. + + > **Note**: the `CCFragOutput` function does not generally need to be implemented by itself, it only serves the purpose of interfacing with the render pipeline. And for outputs containing lighting calculations, the `output-standard` header should be included instead of `output` since the results are already in HDR range. + +- `CCToonShading` + + Function name `CCToonShading`, which needs to contain the header file `shading-toon.chunk` for the lighting calculation for cartoon rendering. + + ```c + #include + void surf (out ToonSurface s) { + // fill in your data here + } + vec4 frag () { + ToonSurface s; surf(s); + vec4 color = CCToonShading(s); + return CCFragOutput(color); + } + ``` + +### 3.3 Functions related to light and shadow calculation + +v2.x is very different from v3.0 in terms of light and shadow calculation, mainly including the following two parts: + +#### spherical light + +The **point light source** in v2.x is adjusted to **spherical light** in v3.0, and there are many ready-made functions, which need to be added to the header file `cc-forward-light.chunk` when using them. + +| Name | Type | Info | +|:------ | :------ | :----- | +| `cc_sphereLitPos[MAX_LIGHTS]` | vec4 |xyz: sphere light position | +| `cc_sphereLitSizeRange[MAX_LIGHTS]` | vec4 | x: sphere light size
y: sphere light range | +| `cc_sphereLitColor[MAX_LIGHTS]` | vec4 | xyz: sphere light color
w: sphere light intensity | + +For additional details, please refer to the [Common built-in shader Uniform](builtin-shader-uniforms.md) documentation. + +#### Spotlights + +The spotlight in v3.0 has a lot of ready-made features, which need to be added to the header file `cc-forward-light.chunk` when using it. + +| Name | Type | Info | +| :----- | :----- | :----- | +| `cc_spotLitPos[MAX_LIGHTS]` | vec4 | xyz: spotlight position | +| `cc_spotLitSizeRangeAngle[MAX_LIGHTS]` | vec4 | x: spotlight size
y: spotlight range
z: spotlight angle | +| vec4 | xyz: Spotlight direction | +| `cc_spotLitColor[MAX_LIGHTS]` | vec4 | xyz: spotlight color
w: spotlight intensity | + +Please refer to the [Common shader built-in Uniform](builtin-shader-uniforms.md) documentation for additional details. + +### 3.4 Shadows + +There is a difference between v2.x and v3.0 in shadow calculations. In v2.0 you need to add the header file `shadow.chunk`, while in v3.0 the header file `cc-shadow.chunk` should be used instead. + +**v2.0 header file `shadow.chunk` has the following common functional uniform and functions**: + +| Name | Type | Info | +| :----- | :----- | :---- | +| `cc_shadow_lightViewProjMatrix[CC_MAX_SHADOW_LIGHTS]` | mat4 | Draws a shadow map in light coordinates | +| `cc_shadow_info[CC_MAX_SHADOW_LIGHTS]` | vec4 | Calculate shadow offset | + +| Name (function) | Type | Info | +| :------ | :----- | :----- | +| `getDepth` | float | Returns the depth value | +| `shadowSimple` | float | Hard sampling of shadows can have jaggedness issues | + +**v3.0 header file `cc-shadow.chunk` has the following common functions uniformly**: + +| Name | Type | Info | +| :------ | :----- | :----- | +| `cc_matLightPlaneProj` | mat4| Transform matrix for plane shadows | +| `cc_shadowColor` | vec4 | Shadow color | + +#### ShadowPCF Soft Shadows + +| header file | function | +| :------ | :----- | +| v2.x: `shadow.chunk`| `shadowPCF3X3` (**3 * 3** samples)
`shadowPCF5X5` (**5 * 5** samples)| +| v3.0: `cc-shadow-map-fs.chunk` | `CC_DIR_SHADOW_FACTOR`: modify the value of the shadow color in memory directly | diff --git a/versions/4.0/en/material-system/effect-syntax.md b/versions/4.0/en/material-system/effect-syntax.md new file mode 100644 index 0000000000..f9e66181fa --- /dev/null +++ b/versions/4.0/en/material-system/effect-syntax.md @@ -0,0 +1,3 @@ +# Effect syntax + +We have systematically improved and updated the documentation of the material system module since v3.4. Please move to the [latest version](../shader/index.md) for the content related to the effect syntax. diff --git a/versions/4.0/en/material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md b/versions/4.0/en/material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md new file mode 100644 index 0000000000..7a24db0ae5 --- /dev/null +++ b/versions/4.0/en/material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md @@ -0,0 +1,120 @@ +# Upgrade Guide: Effect from v3.4.x to v3.5.0 + +## Macro tags and functional macros + +The effect syntax for Macro Tags and Functional Macros have been upgraded to avoid the occupation of standard glsl define, old effects in project will be upgrade automatically, but if you are using external effects without meta or writing a new one, you have to pay attention. + +- New syntax for Macro Tag: `#pragma define-meta` +- New syntax for Funtional Macro: `#pragma define` + +You can refer to [Effect Syntax - macro-tags](../shader/macros.md#macro-tags) for detailed information. + +## Model level shadow bias + +In v3.5, we supported individual shadow bias configuration for models, this allows detailed control of shadow effect on simple or complex surfaces. If you have any customized effect, you may need to upgrade them for shadow bias configuration to take effect. + +> **Note**: If shadow map of lights are disabled, or if `CC_TRANSFER_SHADOW(pos)` is not invoked in your vertex shader, then you won't need to upgrade it. + +### Upgrade instructions + +There are **four elements** to add to your effect file, they are listed below: + +1. Output varying define in the vertex shader + + ``` + #if CC_RECEIVE_SHADOW + out mediump vec2 v_shadowBias; + #endif + ``` + +2. Calculation of shadow bias in the vertex shader + + ``` + #if CC_RECEIVE_SHADOW + v_shadowBias = CCGetShadowBias(); + #endif + ``` + +3. Input varying define in the fragment shader + + ``` + #if CC_RECEIVE_SHADOW + in mediump vec2 v_shadowBias; + #endif + ``` + +4. Shadow bias assignment in the fragment shader + + ``` + #if CC_RECEIVE_SHADOW + s.shadowBias = v_shadowBias; + #endif + ``` + +### Example (code snippets) + +```c +// Vertex shader +CCProgram xxx-vs %{ + // Header file area + #include + ... + #include + + // Vs output area + out vec3 v_xxx; + ... + + #if CC_RECEIVE_SHADOW + out mediump vec2 v_shadowBias; + #endif + + ... + out vec3 v_xxxx; + + // Vs execution area + void main () { + xxx; + ... + + #if CC_RECEIVE_SHADOW + v_shadowBias = CCGetShadowBias(); + #endif + + ... + xxxx; + } +}% + +// Pixel shader +CCProgram xxx-fs %{ + // Header file area + #include + ... + #include + + // Vs output area + in vec3 v_xxx; + ... + + #if CC_RECEIVE_SHADOW + in mediump vec2 v_shadowBias; + #endif + + ... + in vec3 v_xxxx; + + // Ps execution area + void surf (out StandardSurface s) { + xxx; + ... + + #if CC_RECEIVE_SHADOW + s.shadowBias = v_shadowBias; + #endif + + ... + xxxx; + } +}% +``` diff --git a/versions/4.0/en/material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md b/versions/4.0/en/material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md new file mode 100644 index 0000000000..b5983b6eed --- /dev/null +++ b/versions/4.0/en/material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md @@ -0,0 +1,80 @@ +# Upgrade Guide: Effect from v3.5.x to v3.6.0 + +## Chunks migration + +v3.6.0 stores the chunk files from the chunks folder of the previous version into subfolders, please refer to the following table when writing #include for chunk. + +### 1. Public function libraries + +| Origin Path | New Path | +| -------------------- | -------------------------------- | +| common | common/common-define | +| texture-lod | common/texture/texture-lod | +| packing | common/data/packing | +| unpack | common/data/unpack | +| aces | common/color/aces | +| gamma | common/color/gamma | +| octahedron-transform | common/math/octahedron-transform | +| transform | common/math/transform | +| rect-area-light | common/lighting/rect-area-light | + +### 2. Uniform define + +| Origin Path | New Path | +| ---------------- | --------------------------------- | +| cc-global | builtin/uniforms/cc-global | +| cc-local | builtin/uniforms/cc-local | +| cc-forward-light | builtin/uniforms/cc-forward-light | +| cc-environment | builtin/uniforms/cc-environment | +| cc-diffusemap | builtin/uniforms/cc-diffusemap | +| cc-shadow | builtin/uniforms/cc-shadow | +| cc-world-bound | builtin/uniforms/cc-world-bound | + +### 3. Common shader main-functions for legacy shader + +| Origin Path | New Path | +| ----------- | -------------------------------- | +| outline-vs | legacy/main-functions/outline-vs | +| outline-fs | legacy/main-functions/outline-fs | +| general-vs | legacy/main-functions/general-vs | + +### 4. Engine functionality and miscellaneous for legacy shader + +| Origin Path | New Path | +| ------------------------- | -------------------------------- | +| cc-fog-base | legacy/fog-base | +| cc-shadow-map-base | legacy/shadow-map-base | +| morph | legacy/morph | +| cc-skinning | legacy/skinning | +| cc-local-batch | legacy/local-batch | +| lighting | legacy/lighting | +| lightingmap-fs | legacy/lightingmap-fs | +| cc-shadow-map-vs | legacy/shadow-map-vs | +| cc-shadow-map-fs | legacy/shadow-map-fs | +| cc-fog-vs | legacy/fog-vs | +| cc-fog-fs | legacy/fog-fs | +| lightingmap-vs | legacy/lightingmap-vs | +| decode | legacy/decode | +| decode-base | legacy/decode-base | +| decode-standard | legacy/decode-standard | +| input | legacy/input | +| input-standard | legacy/input-standard | +| output | legacy/output | +| output-standard | legacy/output-standard | +| shading-standard | legacy/shading-standard | +| shading-standard-base | legacy/shading-standard-base | +| shading-standard-additive | legacy/shading-standard-additive | +| shading-cluster-additive | legacy/shading-cluster-additive | +| shading-toon | legacy/shading-toon | +| standard-surface-entry | legacy/standard-surface-entry | + +### 5. For internal use only + +| Origin Path | New Path | +| ----------------- | -------------------------------- | +| alpha-test | builtin/internal/alpha-test | +| cc-sprite-common | builtin/internal/sprite-common | +| cc-sprite-texture | builtin/internal/sprite-texture | +| embedded-alpha | builtin/internal/embedded-alpha | +| particle-common | builtin/internal/particle-common | + diff --git a/versions/4.0/en/material-system/effect.png b/versions/4.0/en/material-system/effect.png new file mode 100644 index 0000000000..c8ebd9ba25 Binary files /dev/null and b/versions/4.0/en/material-system/effect.png differ diff --git a/versions/4.0/en/material-system/img/add-material.png b/versions/4.0/en/material-system/img/add-material.png new file mode 100644 index 0000000000..8ac156f4bd Binary files /dev/null and b/versions/4.0/en/material-system/img/add-material.png differ diff --git a/versions/4.0/en/material-system/img/builtin-material.png b/versions/4.0/en/material-system/img/builtin-material.png new file mode 100644 index 0000000000..6f7d312ffa Binary files /dev/null and b/versions/4.0/en/material-system/img/builtin-material.png differ diff --git a/versions/4.0/en/material-system/img/dump-material.png b/versions/4.0/en/material-system/img/dump-material.png new file mode 100644 index 0000000000..95e8208a02 Binary files /dev/null and b/versions/4.0/en/material-system/img/dump-material.png differ diff --git a/versions/4.0/en/material-system/img/dump-result.png b/versions/4.0/en/material-system/img/dump-result.png new file mode 100644 index 0000000000..327e58ec95 Binary files /dev/null and b/versions/4.0/en/material-system/img/dump-result.png differ diff --git a/versions/4.0/en/material-system/img/locate.png b/versions/4.0/en/material-system/img/locate.png new file mode 100644 index 0000000000..9328146a2f Binary files /dev/null and b/versions/4.0/en/material-system/img/locate.png differ diff --git a/versions/4.0/en/material-system/img/mat-show.png b/versions/4.0/en/material-system/img/mat-show.png new file mode 100644 index 0000000000..c251d0d6f8 Binary files /dev/null and b/versions/4.0/en/material-system/img/mat-show.png differ diff --git a/versions/4.0/en/material-system/img/material.png b/versions/4.0/en/material-system/img/material.png new file mode 100644 index 0000000000..10993f7481 Binary files /dev/null and b/versions/4.0/en/material-system/img/material.png differ diff --git a/versions/4.0/en/material-system/img/particle-material.png b/versions/4.0/en/material-system/img/particle-material.png new file mode 100644 index 0000000000..3d39c5bba3 Binary files /dev/null and b/versions/4.0/en/material-system/img/particle-material.png differ diff --git a/versions/4.0/en/material-system/img/post-dump.png b/versions/4.0/en/material-system/img/post-dump.png new file mode 100644 index 0000000000..14b7bc14cb Binary files /dev/null and b/versions/4.0/en/material-system/img/post-dump.png differ diff --git a/versions/4.0/en/material-system/img/preview-model-select.png b/versions/4.0/en/material-system/img/preview-model-select.png new file mode 100644 index 0000000000..379e5b5b5f Binary files /dev/null and b/versions/4.0/en/material-system/img/preview-model-select.png differ diff --git a/versions/4.0/en/material-system/img/readonly-material.png b/versions/4.0/en/material-system/img/readonly-material.png new file mode 100644 index 0000000000..e4edf0af44 Binary files /dev/null and b/versions/4.0/en/material-system/img/readonly-material.png differ diff --git a/versions/4.0/en/material-system/img/revert-material.png b/versions/4.0/en/material-system/img/revert-material.png new file mode 100644 index 0000000000..0e245e1a05 Binary files /dev/null and b/versions/4.0/en/material-system/img/revert-material.png differ diff --git a/versions/4.0/en/material-system/img/save-material.png b/versions/4.0/en/material-system/img/save-material.png new file mode 100644 index 0000000000..757ea1d565 Binary files /dev/null and b/versions/4.0/en/material-system/img/save-material.png differ diff --git a/versions/4.0/en/material-system/img/ui-select.png b/versions/4.0/en/material-system/img/ui-select.png new file mode 100644 index 0000000000..4013be7a8a Binary files /dev/null and b/versions/4.0/en/material-system/img/ui-select.png differ diff --git a/versions/4.0/en/material-system/material-panel-v2x.png b/versions/4.0/en/material-system/material-panel-v2x.png new file mode 100644 index 0000000000..0d7dd7a7e8 Binary files /dev/null and b/versions/4.0/en/material-system/material-panel-v2x.png differ diff --git a/versions/4.0/en/material-system/material-script.md b/versions/4.0/en/material-system/material-script.md new file mode 100644 index 0000000000..b03be5b448 --- /dev/null +++ b/versions/4.0/en/material-system/material-script.md @@ -0,0 +1,124 @@ +# Programmatic use of materials + +## Materials creation + +Material resources can be regarded as resource instances of shader resources (EffectAsset) in the scene. + +Creator supports manually [creating material resources](../asset/material.md) in **Assets**, and also supports [IMaterialInfo](%__APIDOC__%/zh/interface/IMaterialInfo) interface to create materials programmatically in a script module. Configurable parameters for `IMaterialInfo` include: + +- `effectAsset`/`effectName`:An effect asset reference that specifies which EffectAsset describes the process to use for rendering. (either `effectAsset` or `effectName` must be selected) +- `technique`:Specifies which technique in the EffectAsset to use, defaults to 0. +- `defines`:List of macro definitions, specifying which [preprocessing macro definitions](../shader/macros.md) to enable, all are disabled by default. +- `states`:List of pipeline state overloads, specifying which overloads are available for rendering pipeline states (depth, stencil, transparent blending, etc.), the default is consistent with the effect declaration. + +Code example to create a material is as follows: + +```ts +const mat = new Material(); +mat.initialize({ + // Specifies the shader resource used by the material by the effect name + effectName: 'pipeline/skybox', + defines: { + USE_RGBE_CUBEMAP: true + } +}); +``` + +## Use material + +![add material](img/add-material.png) + +Any renderer component can be accessed programmatically in the script module. The code example is as follows: + +```ts +// Materials for 3D objects are accessible through mesh renderer components (MeshRenderer, SkinnedMeshRenderer, SkinnedMeshBatchRenderer) +let renderable = this.getComponent(MeshRenderer); + +// Get the material with index 0 +let material = renderable.getMaterial(0) + +// Set the material with index 0 +renderable.setMaterial(mat, 0); + +let sprite = this.node.getComponent(Sprite) + +// Get a custom material for a 2D renderer component +let customMaterial = sprite.customMaterial; + +// Set custom materials for 2D renderer components +sprite.customMaterial = mat; + +// Get and set the material of the particle renderer +let particleSystem = this.getComponent(ParticleSystem); +const material = particleSystem.material; +particleSystem.material = material; + +// Set and get particle trailing material +const trailMaterial = particleSystem.renderer.trailMaterial; +particleSystem.renderer.trailMaterial = trailMaterial; +``` + +> **Notes**: +> 1. What is accessed here is the shared material. +> 2. There are two situations in the material: shared material and material instance. Shared material cannot be batched with material instance. + +## Set material properties + +After the material is initialized through the `IMaterialInfo` interface, the `Uniform` variable of the material can only be set through `Material.setProperty`. The code example is as follows: + +```ts +mat.setProperty("uniform name", uniformValue) +``` + +`Uniform` corresponds to `Uniform`-qualified variables declared within `Shader`. To learn more about `Uniform` please refer to: + +- [Cocos Effect builtin Uniform](../shader/uniform.md) +- [GLSL storage qualifier](../shader/glsl.md#存储限定符) + +If you need to set the value of `Uniform` frequently, use `Pass.setUniform` for better performance. + +## Shared material & material instance + +In the renderer component, materials exist as **shared materials** and **material instances**. + +- **Shared materials** + + A shared material is used by multiple renderer components, and modifying the shared material affects all renderer components that use it. By default, the same material is shared among multiple renderer components. + + The code example to get the shared material is as follows: + + ```ts + // Get the renderer component + let renderableComponent = this.node.getComponent(MeshRenderer) as RenderableComponent + // Get the element at index 0 in the shared material array + let sharedMaterial = renderableComponent.sharedMaterial + // Get an array of shared materials + let sharedMaterials = renderableComponent.sharedMaterials + // Get the element at index 0 in the shared material array + let sharedMaterial = renderableComponent.getMaterial(0) + ``` + +- **Material instance** + + Material instances are used solely by a single renderer component, and modifying a material instance affects only the renderer components that use it. The material defaults to a shared material. When the shared material is modified, the engine will create a material instance according to the material, for example: + + - When the `getter` of `RenderableComponent.getMaterialInstance` or `RenderableComponent.material` is called, the engine will create a material instance based on the current material + - When the `setter` of `RenderableComponent.material` is called, the engine will create a material instance based on the passed in material + + The code example is as follows: + + ```ts + // Get the renderer component + let renderableComponent = this.node.getComponent(MeshRenderer) as RenderableComponent + // Get the array of material instances, if not, create it according to the current material array + let materialInstances = renderableComponent.materials + // Get the element with index 0 in the material instance array, if not, create it according to the current material + let materialInstance = renderableComponent.material + // Get the material instance, if not, create it according to the current material + let materialInstance = renderableComponent.getMaterialInstance(materialIndex); + ``` + +### FAQ + +**Q**:Why the DrawCall is increased after modifying the properties of the material?
+**A**:It may be because the `getMaterialInstance` of the renderer component or the `getter` method of `RenderableComponent.material` is used, resulting in the generation of a new material instance, which affects the batching process. diff --git a/versions/4.0/en/material-system/material-structure.md b/versions/4.0/en/material-system/material-structure.md new file mode 100644 index 0000000000..ea65c4ea03 --- /dev/null +++ b/versions/4.0/en/material-system/material-structure.md @@ -0,0 +1,11 @@ +# Material System Classes Diagram + +The material system controls the final shading process and order of each model, and the related inter-class structures in the engine are as follows: + +[![Assets](img/material.png "Click to view diagram source")](material.dot) + +The material system controls the final coloring process and order of each model. The related inter-class structures in the engine are as follows: a **Material** ([Material](../asset/material.md)) in the above figure and **EffectAsset** ([Effect](../shader/index.md)) are all assets. + +- **Material** is responsible for the `Uniform` declared by EffectAsset, macro data storage, and Shader usage and management, which are displayed in the **Inspector** panel in the form of visual properties of material assets. Material is usually used by renderer components. All components that inherit from RenderableComponent are renderer components, such as MeshRenderer, Sprite, etc. For more information, please refer to [Material Assets](../asset/material.md). + +- **EffectAsset** is responsible for providing property, macro, shader list definitions. Each EffectAsset is eventually compiled into the format used in the engine, and the engine parses and applies it according to the format. All parsed EffectAsset information will be registered in the ProgramLib library in the engine, so that users can directly obtain the EffectAsset resources used by the actual engine through the code. See [shaders](../shader/index.md) for more details. diff --git a/versions/4.0/en/material-system/material-v2x.png b/versions/4.0/en/material-system/material-v2x.png new file mode 100644 index 0000000000..7b44fdd8be Binary files /dev/null and b/versions/4.0/en/material-system/material-v2x.png differ diff --git a/versions/4.0/en/material-system/material-v3.png b/versions/4.0/en/material-system/material-v3.png new file mode 100644 index 0000000000..37abfacdbe Binary files /dev/null and b/versions/4.0/en/material-system/material-v3.png differ diff --git a/versions/4.0/en/material-system/material.dot b/versions/4.0/en/material-system/material.dot new file mode 100644 index 0000000000..25097a8a55 --- /dev/null +++ b/versions/4.0/en/material-system/material.dot @@ -0,0 +1,31 @@ +digraph G { + layout=dot splines=true compound=true overlap=false fontname="Noto Sans CJK SC" + node [shape=Mrecord fontname="Source Code Pro"] + edge [fontname="Noto Sans CJK SC"] + + subgraph cluster_framework { + style=invis + ast [label="Asset"] + cmp [label="Component"] + lib [label="ProgramLib | { shader instances | shader templates }"] + } + + mat [label="Material | { defines | properties | effect}"] + + subgraph cluster_effect { + style=dotted label="auto-generated from *.effect file" labelloc=b + shd [label="ShaderInfo | { name | glsl3 | glsl1 | defines | blocks | samplers | dependencies }"] + efx [label="EffectAsset | { techniques | shader list }"] + } + + ast -> mat [dir=back arrowtail=empty] + ast -> efx [dir=back arrowtail=empty] + lib:temp -> efx:shd [dir=back arrowtail=vee style=dashed label=" register to"] + efx:shd -> shd [dir=back arrowtail=ediamond] + mat:efx -> efx [dir=back arrowtail=ediamond] + + rnd [label=" RenderableComponent | materials"] + + cmp -> rnd:hd [dir=back arrowtail=empty] + rnd:mats -> mat [dir=back arrowtail=ediamond] +} diff --git a/versions/4.0/en/material-system/material.png b/versions/4.0/en/material-system/material.png new file mode 100644 index 0000000000..10993f7481 Binary files /dev/null and b/versions/4.0/en/material-system/material.png differ diff --git a/versions/4.0/en/material-system/material10.jpg b/versions/4.0/en/material-system/material10.jpg new file mode 100644 index 0000000000..c481c4628b Binary files /dev/null and b/versions/4.0/en/material-system/material10.jpg differ diff --git a/versions/4.0/en/material-system/material2.png b/versions/4.0/en/material-system/material2.png new file mode 100644 index 0000000000..2dfd9d9ee1 Binary files /dev/null and b/versions/4.0/en/material-system/material2.png differ diff --git a/versions/4.0/en/material-system/material3.png b/versions/4.0/en/material-system/material3.png new file mode 100644 index 0000000000..c6089edc81 Binary files /dev/null and b/versions/4.0/en/material-system/material3.png differ diff --git a/versions/4.0/en/material-system/material4.1.jpg b/versions/4.0/en/material-system/material4.1.jpg new file mode 100644 index 0000000000..9e5b4d2d33 Binary files /dev/null and b/versions/4.0/en/material-system/material4.1.jpg differ diff --git a/versions/4.0/en/material-system/material4.jpg b/versions/4.0/en/material-system/material4.jpg new file mode 100644 index 0000000000..02e7ba4089 Binary files /dev/null and b/versions/4.0/en/material-system/material4.jpg differ diff --git a/versions/4.0/en/material-system/material5.jpg b/versions/4.0/en/material-system/material5.jpg new file mode 100644 index 0000000000..1264faf293 Binary files /dev/null and b/versions/4.0/en/material-system/material5.jpg differ diff --git a/versions/4.0/en/material-system/material8.jpg b/versions/4.0/en/material-system/material8.jpg new file mode 100644 index 0000000000..c40362842e Binary files /dev/null and b/versions/4.0/en/material-system/material8.jpg differ diff --git a/versions/4.0/en/material-system/material9.jpg b/versions/4.0/en/material-system/material9.jpg new file mode 100644 index 0000000000..4c81d019eb Binary files /dev/null and b/versions/4.0/en/material-system/material9.jpg differ diff --git a/versions/4.0/en/material-system/overview.md b/versions/4.0/en/material-system/overview.md new file mode 100644 index 0000000000..4261c510e3 --- /dev/null +++ b/versions/4.0/en/material-system/overview.md @@ -0,0 +1,21 @@ +# Material System Overview + +![mat-inspector](img/mat-show.png) + +In the real world, all objects interact with light, and depending on the appearance of the surface of the object, the effect of the light is also different. + +Cocos Creator uses **material** to describe the appearance of objects, such as whether a small ball is a glass ball or a plastic ball, and a box is a wooden box or a metal box. The light and shade, light points, light reflections, light scattering and other effects they present under lighting conditions are all achieved by [Effect](../shader/index.md). The material is the data set of the shader (including texture maps, lighting algorithms, etc.), which is convenient for visual adjustment. + +The material system mainly includes the following: + +- [Material assets](../asset/material.md) +- [Procedural use of materials](material-script.md) +- [Builtin materials](builtin-material.md) +- [Material System Class Diagram](material-structure.md) +- [Cocos Creator 3.1 Material Upgrade Guide](Material-upgrade-documentation-for-v3.0-to-v3.1.md) +- [Upgrade Guide: Effect from v3.4.x to v3.5.0](effect-upgrade-documentation-for-v3.4.2-to-v3.5.md) +- [Upgrade Guide: Effect from v3.5.x to v3.6.0](effect-upgrade-documentation-for-v3.5-to-v3.6.md) + +## Examples + +Creator provides **material** examples related to materials [GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/material), users can refer to it as needed. diff --git a/versions/4.0/en/material-system/standard-material-graph.png b/versions/4.0/en/material-system/standard-material-graph.png new file mode 100644 index 0000000000..1e4afdd8f1 Binary files /dev/null and b/versions/4.0/en/material-system/standard-material-graph.png differ diff --git a/versions/4.0/en/module-map/animation.png b/versions/4.0/en/module-map/animation.png new file mode 100644 index 0000000000..d829c82ba8 Binary files /dev/null and b/versions/4.0/en/module-map/animation.png differ diff --git a/versions/4.0/en/module-map/asset.png b/versions/4.0/en/module-map/asset.png new file mode 100644 index 0000000000..9982559ad5 Binary files /dev/null and b/versions/4.0/en/module-map/asset.png differ diff --git a/versions/4.0/en/module-map/audio.png b/versions/4.0/en/module-map/audio.png new file mode 100644 index 0000000000..89315008f0 Binary files /dev/null and b/versions/4.0/en/module-map/audio.png differ diff --git a/versions/4.0/en/module-map/component.png b/versions/4.0/en/module-map/component.png new file mode 100644 index 0000000000..239ce54df2 Binary files /dev/null and b/versions/4.0/en/module-map/component.png differ diff --git a/versions/4.0/en/module-map/effects/index.md b/versions/4.0/en/module-map/effects/index.md new file mode 100644 index 0000000000..8eb7f79616 --- /dev/null +++ b/versions/4.0/en/module-map/effects/index.md @@ -0,0 +1,8 @@ +# Effects + +Effects for Cocos Creator include the **Billboard** and **Line** components, which can be added by clicking on **Add Component -> Effects** in the **Inspector** panel. + +For specific descriptions and usage, please refer to the following: + +- [Billboard](../../particle-system/billboard-component.md) +- [Line](../../particle-system/line-component.md) diff --git a/versions/4.0/en/module-map/graphics.md b/versions/4.0/en/module-map/graphics.md new file mode 100644 index 0000000000..f8a1cf9d9d --- /dev/null +++ b/versions/4.0/en/module-map/graphics.md @@ -0,0 +1,16 @@ +# Graphics Rendering + +Cocos Creator provides the following graphics features to enrich the graphics and enhance the realism of the graphics, etc.: + +- [Render Pipeline](../render-pipeline/overview.md) +- [Camera](../editor/components/camera-component.md) +- [Lighting](../concepts/scene/light.md) +- [Meshes](mesh/index.md) +- [Textures](texture/index.md) +- [Material](../material-system/overview.md) +- [Sorting2D](../engine/rendering/sorting-2d.md) +- [Sorting3D](../engine/rendering/sorting.md) +- [Particle](../particle-system/overview.md) +- [Effects](effects/index.md) +- [Skybox](../concepts/scene/skybox.md) +- [Global Fog](../concepts/scene/fog.md) diff --git a/versions/4.0/en/module-map/graphics.png b/versions/4.0/en/module-map/graphics.png new file mode 100644 index 0000000000..10edf1c614 Binary files /dev/null and b/versions/4.0/en/module-map/graphics.png differ diff --git a/versions/4.0/en/module-map/light.png b/versions/4.0/en/module-map/light.png new file mode 100644 index 0000000000..4f8eadde07 Binary files /dev/null and b/versions/4.0/en/module-map/light.png differ diff --git a/versions/4.0/en/module-map/material.png b/versions/4.0/en/module-map/material.png new file mode 100644 index 0000000000..908dbe9eb0 Binary files /dev/null and b/versions/4.0/en/module-map/material.png differ diff --git a/versions/4.0/en/module-map/mesh/img/batched-skinning-model-component.png b/versions/4.0/en/module-map/mesh/img/batched-skinning-model-component.png new file mode 100644 index 0000000000..59bf78284d Binary files /dev/null and b/versions/4.0/en/module-map/mesh/img/batched-skinning-model-component.png differ diff --git a/versions/4.0/en/module-map/mesh/img/skinned-mesh-renderer.png b/versions/4.0/en/module-map/mesh/img/skinned-mesh-renderer.png new file mode 100644 index 0000000000..154488ca0c Binary files /dev/null and b/versions/4.0/en/module-map/mesh/img/skinned-mesh-renderer.png differ diff --git a/versions/4.0/en/module-map/mesh/index.md b/versions/4.0/en/module-map/mesh/index.md new file mode 100644 index 0000000000..615b6b64f9 --- /dev/null +++ b/versions/4.0/en/module-map/mesh/index.md @@ -0,0 +1,9 @@ +# Meshes + +Meshes are generally used to draw 3D images. Cocos Creator provides the following mesh renderer components to render base meshes, skinned meshes, etc., so that the model can be drawn and displayed: + +- [MeshRenderer](../../engine/renderable/model-component.md): used to render static 3D models. +- [SkinnedMeshRenderer](../../animation/skeletal-animation.md): used to render skeletal animations. +- [SkinnedMeshBatchRenderer](../../animation/skeletal-animation.md): used to combine the rendering of all sub-skinned models controlled by the same skeletal animation component. + +Also, if the model needs to be applied to actual physical collisions to achieve an effect similar to a bumpy road surface, use the Mesh Collision component, which will generate a collision mesh based on the model shape. Please refer to [using mesh collisions](../../physics/physics-collider.md#realize-the-shape-of-goose-soft-stone) documentation for details. diff --git a/versions/4.0/en/module-map/mesh/skinnedMeshBatchRenderer.md b/versions/4.0/en/module-map/mesh/skinnedMeshBatchRenderer.md new file mode 100644 index 0000000000..5164aa8f8b --- /dev/null +++ b/versions/4.0/en/module-map/mesh/skinnedMeshBatchRenderer.md @@ -0,0 +1,27 @@ +# SkinnedMeshBatchRenderer + +The SkinnedMeshBatchRenderer component is used to combine the rendering of all meshes controlled by the same skeletal animation component. + +![mesh batch](./img/batched-skinning-model-component.png) + +| Properties | Description | +| :--- | :--- | +| **Operation** | Any changes to the property will not take effect immediately and will need to be recalculated by tapping the **Cook** button before they can be applied. | +| **Materials** | The "master material" used after the merge requires the use of your own customized merge effect. +| **LightmapSettings** | For baking Lightmap, please refer to [Lightmapping](../../concepts/scene/light/lightmap.md) for details. | +| **ShadowCastingMode** | Specify whether the current model will cast shadows, you need to [Enable Shadow Effect](../../concepts/scene/light/shadow.md#Enable%20Shadow%20Effect) in the scene first。 | +| **ReceiveShadow** | Specify whether the current model will receive and display shadow effects generated by other objects, you need to [Enable Shadow Effect](../../concepts/scene/light/shadow.md#Enable%20Shadow%20Effect) in the scene first. This property only takes effect when the shadow type is **ShadowMap**. | +| **SkinningRoot** | The root node of the skeletal skin, usually the node where the SkeletalAnimation component is located. | +| **AtlasSize** | The edge length of the final set generated by the combined atlas. | +| **BatchableTextureNames** | The texture properties of the material that are actually involved in the merge, and the properties that are not involved in the merge, use the first unit's texture uniformly. | +| **Units** | The sub-model data before the combined batch, is the main data source. | +| **Mesh** | The model data of current sub-mesh, usually directly from glTF or FBX model file. | +| **Skeleton** | The skeleton data of the current model, usually directly from the glTF or FBX model file. | +| **Materials** | The "sub-materials" used by the current sub-model, which is a non-batching effect, the effect used by different sub-meshes should be consistent. | +| **Offset** | The offset of the current sub-model when it is merged, with the top-left corner of the atlas as the origin, in the range `[0, 1]`. For example, the data in the figure represents that the sub-map is overlapped with the top-left corner of the atlas. | +| **Size** | The size of the current sub-model, in the range `[0, 1]`. For example, the data in the figure represents 1/2 of the entire set. | +| **CopyFrom** | The target properties (except `offset` and `size`) can be copied automatically by dragging in **SkinningModelComponent** for easy operation. | + +Please refer to [BatchedSkinnedMeshRenderer](../../animation/skeletal-animation.md) for more details. + +For API reference, please refer to [SkinnedMeshBatchRenderer API](%__APIDOC__%/en/class/SkinnedMeshBatchRenderer). diff --git a/versions/4.0/en/module-map/mesh/skinnedMeshRenderer.md b/versions/4.0/en/module-map/mesh/skinnedMeshRenderer.md new file mode 100644 index 0000000000..b4aeb9a5d0 --- /dev/null +++ b/versions/4.0/en/module-map/mesh/skinnedMeshRenderer.md @@ -0,0 +1,26 @@ +# SkinnedMeshRenderer + +The SkinnedMeshRenderer component is mainly used for rendering skinned model meshes. + +After [Import Model Assets](../../asset/model/mesh.md), the SkinnedMeshRenderer component will be automatically added to the model nodes when the model is used if the model mesh has skinning information. + +![SkinnedMeshRenderer](./img/skinned-mesh-renderer.png) + +## Properties + +| Properties | Description | +| :--- | :--- | +| **Materials** | Mesh assets allow multiple materials, all of which are in the `materials` array.
If the mesh asset has more than one sub-mesh, then the Mesh Renderer will get the corresponding material from the `materials` array to render this sub-mesh. | +| **LightmapSettings** | For baking Lightmap, please refer to [Lightmap](../../concepts/scene/light/lightmap.md) for details | +| **ShadowCastingMode** | Specify whether the current model will cast shadows, you need to [Enable Shadow Effect](../../concepts/scene/light/shadow.md#Enable%20Shadow%20Effect) in the scene first | +| **ReceiveShadow** | Specify whether the current model will receive and display shadow effects generated by other objects, you need to [Enable Shadow Effect](../../concepts/scene/light/shadow.md#Enable%20Shadow%20Effect) in the scene first. This property only takes effect when the shadow type is **ShadowMap**. | +| **Mesh** |Specify the mesh asset used for rendering, [Mesh Assets](../../engine/renderable/model-component.md#Mesh%20Assets) in the mesh renderer component section.| +| **Skeleton** | The skeletal data of the current model, usually directly from the imported glTF or FBX model file | +| **SkinningRoot** | Reference to the skeleton root node, corresponding to the node that controls the animation component of the model | + +Please refer to [SkinnedMeshRenderer API](%__APIDOC__%/en/class/SkinnedMeshRenderer) for API reference. + +The difference between SkinnedMeshRenderer component and MeshRenderer component are as follows: + +- The MeshRenderer component renders a static model, consisting of 3D model data +- The SkinnedMeshRenderer component renders not only the model but also the bones, so in addition to the 3D model data, it also includes data such as bone data and vertex weights.
However, if there is no skeleton data mounted on the SkinnedMeshRenderer, it is no different from a normal MeshRenderer component. diff --git a/versions/4.0/en/module-map/particle.png b/versions/4.0/en/module-map/particle.png new file mode 100644 index 0000000000..65f350abd1 Binary files /dev/null and b/versions/4.0/en/module-map/particle.png differ diff --git a/versions/4.0/en/module-map/physics.png b/versions/4.0/en/module-map/physics.png new file mode 100644 index 0000000000..ba59c5370d Binary files /dev/null and b/versions/4.0/en/module-map/physics.png differ diff --git a/versions/4.0/en/module-map/scene.png b/versions/4.0/en/module-map/scene.png new file mode 100644 index 0000000000..100ffce3c6 Binary files /dev/null and b/versions/4.0/en/module-map/scene.png differ diff --git a/versions/4.0/en/module-map/script.png b/versions/4.0/en/module-map/script.png new file mode 100644 index 0000000000..9f5c8d9138 Binary files /dev/null and b/versions/4.0/en/module-map/script.png differ diff --git a/versions/4.0/en/module-map/texture/index.md b/versions/4.0/en/module-map/texture/index.md new file mode 100644 index 0000000000..22118dd25a --- /dev/null +++ b/versions/4.0/en/module-map/texture/index.md @@ -0,0 +1,16 @@ +# Textures + +A texture is a displayable image, or a piece of intermediate data used for computation, which is mapped to the surface of a rendered object via UV coordinates to give it a richer and more realistic effect. The following are some of the applications of textures in Cocos Creator: + +- Used for 2D UI rendering, see the [SpriteFrame](../../asset/sprite-frame.md) documentation for details. + +- Used for 3D model rendering. A texture asset needs to be specified in the material, to render and map it to the mesh surface.Textures also support switching to **cube map** or **normal map** when [importing image assets](../../asset/image.md). + +- Used for the particle system to make particles more expressive. As with 3D models, the use of textures in particle system also depends on materials. + +- Used for terrain rendering, see the [Terrain system](../../editor/terrain/index.md) documentation for details. + +## More Reference + +- [RenderTexture](../../asset/render-texture.md) +- [Texture Compression](../../asset/compress-texture.md) diff --git a/versions/4.0/en/module-map/world01.jpg b/versions/4.0/en/module-map/world01.jpg new file mode 100644 index 0000000000..3a7f31548e Binary files /dev/null and b/versions/4.0/en/module-map/world01.jpg differ diff --git a/versions/4.0/en/native/overview.md b/versions/4.0/en/native/overview.md new file mode 100644 index 0000000000..4396257dc7 --- /dev/null +++ b/versions/4.0/en/native/overview.md @@ -0,0 +1,25 @@ +# Native Development + +Cocos Creator supports publishing to various platforms, including mobile native apps, mobile web apps, mobile mini-games, desktop native apps, and desktop web apps. + +This chapter will cover some topics that you may involve in native app development using Cocos Creator. + +## Content + +- [Native Platform Secondary Development Guide](../advanced-topics/native-secondary-development.md) +- [JavaScript and Android Communication with Reflection](../advanced-topics/java-reflection.md) +- [JavaScript and iOS/macOS Communication with Reflection](../advanced-topics/oc-reflection.md) +- [JavaScript and Java Communication using JsbBridge](../advanced-topics/js-java-bridge.md) +- [JavaScript and Objective-C Communication using JsbBridge](../advanced-topics/js-oc-bridge.md) +- [JsbBridgeWrapper - An Event Mechanism based on JsbBridge](../advanced-topics/jsb-bridge-wrapper.md) +- [Tutorial: JSB 2.0](../advanced-topics/JSB2.0-learning.md) + - [JSB Manual Binding](../advanced-topics/jsb-manual-binding.md) + - [JSB Auto Binding](../advanced-topics/jsb-auto-binding.md) + - [Swig](../advanced-topics/jsb-swig.md) + - [Swig Tutorial](../advanced-topics/jsb/swig/tutorial/index.md) +- [Introduction to CMake Usage](../advanced-topics/cmake-learning.md) +- [Native Engine Memory Leak Detection System](../advanced-topics/memory-leak-detector.md) +- [Native Scene Culling](../advanced-topics/native-scene-culling.md) +- [Native Performance Profiler](../advanced-topics/profiler.md) +- [Native Plugins](../advanced-topics/native-plugins/brief.md) + - [Native Plugin Quick Tutorial](../advanced-topics/native-plugins/tutorial.md) diff --git a/versions/4.0/en/particle-system/2d-particle/2d-particle.md b/versions/4.0/en/particle-system/2d-particle/2d-particle.md new file mode 100644 index 0000000000..548d8f2377 --- /dev/null +++ b/versions/4.0/en/particle-system/2d-particle/2d-particle.md @@ -0,0 +1,46 @@ +# ParticleSystem2D Component Reference + +The ParticleSystem2D component is used to read the particle asset data and perform a series of operations such as play, pause, destroy, etc. Particle assets support `plist` files and images, both of which are recommended to be placed in the same folder. + +![ParticleSystem2D](./2d-particle.png) + +Click the **Add Component** button at the bottom of the **Inspector** panel and select **ParticleSystem2D** from **Effects** to add the ParticleSystem2D component to the node. + +For the script interface of ParticleSystem2D, please refer to [ParticleSystem API](%__APIDOC__%/en/class/ParticleSystem2D). + +## ParticleSystem2D Properties + +| Property | Description +| :-------------- | :----------- | +| CustomMaterial | Custom material, please refer to the [Custom Material](../../ui-system/components/engine/ui-material.md) documentation. +| Color | Color of particles. +| Preview | Previews particles in editor mode. When enabled, particles will be played automatically in the **Scene** panel when they are selected. +| PlayOnLoad | If this option is checked, the particles will be emitted automatically at runtime. +| AutoRemoveOnFinish | Automatically destroys the node where the particle is located when it finishes playing. +| File | The particle configuration file in Plist format. +| Custom | Whether to allow custom particle properties. When this property is enabled, the particle properties of the following sections can be customized. +| SpriteFrame | Customizes the texture of particles. +| Duration | The duration of the particle system in **seconds**, -1 means continuous emission. +| EmissionRate | The number of particles emitted per second. +| Life | The running time and range of particles. +| TotalParticle | The maximum number of particles. +| StartColor | The initial color of particles. +| EndColor | The end color of particles. +| Angle | The angle and range of particles. +| StartSize | The initial size and range of particles. +| EndSize | The end size and range of particles. +| StartSpin | The initial spin angle and range of particles. +| EndSpin | The end spin angle and size of particles. +| PosVar | The range of the emitter position (horizontal and vertical). +| PositionType | The type of particle position, including **FREE**, **RELATIVE**, **GROUPED**. For details, please refer to [PositionType API](%__APIDOC__%/en/class/ParticleSystem2D?id=positionType). +| EmitterMode | The type of the emitter, including **GRAVITY**, **RADIUS**. For details, please refer to [EmitterMode API](%__APIDOC__%/en/class/ParticleSystem2D?id=EmitterMode). +| Gravity | Gravity. Only works when Emitter Mode is set to **GRAVITY**. +| Speed | The speed and range. Only effective when Emitter Mode is set to **GRAVITY**. +| TangentialAccel | The tangential acceleration and range of each particle i.e. the acceleration perpendicular to the direction of gravity. Effective only when Emitter Mode is set to **GRAVITY**. +| RadialAccel | The radial acceleration and range of particles, i.e. the acceleration parallel to the gravity direction. Effective only when Emitter Mode is set to **GRAVITY**. +| RotationIsDir | Whether the rotation of each particle is equal to its direction. Effective only when Emitter Mode is set to **GRAVITY**. +| StartRadius | The initial radius and range, which indicates the distance of particles relative to the emitter when it is launched. Effective only when Emitter Mode is set to **RADIUS**. +| EndRadius | The end radius and range. Valid only when Emitter Mode is set to **RADIUS**. +| RotatePerS | The rotation angle and range of particles around the initial point per second. Only effective when Emitter Mode is set to **RADIUS**. + +For more specific usage, please refer to the official [ui/25.particle](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/25.particle) example. diff --git a/versions/4.0/en/particle-system/2d-particle/2d-particle.png b/versions/4.0/en/particle-system/2d-particle/2d-particle.png new file mode 100644 index 0000000000..dde6c50d65 Binary files /dev/null and b/versions/4.0/en/particle-system/2d-particle/2d-particle.png differ diff --git a/versions/4.0/en/particle-system/billboard-component.md b/versions/4.0/en/particle-system/billboard-component.md new file mode 100644 index 0000000000..8686ca15f8 --- /dev/null +++ b/versions/4.0/en/particle-system/billboard-component.md @@ -0,0 +1,14 @@ +# Billboard Component + +The Billboard component is used to render a box that always faces the camera. + +![billboard](particle-system/billboard.png) + +| Property | Description | +| :---| :--- | +| **Texture** | The texture displayed on the billboard. | +| **Height** | The height of billboard. | +| **Width** | The width of billboard. | +| **Rotation** | The angle of rotation of the billboard around the center point. | + +To use `Billboard`, please refer to the [Billboard API](%__APIDOC__%/en/en/class/Billboard). diff --git a/versions/4.0/en/particle-system/color-module.md b/versions/4.0/en/particle-system/color-module.md new file mode 100644 index 0000000000..5560ff849d --- /dev/null +++ b/versions/4.0/en/particle-system/color-module.md @@ -0,0 +1,13 @@ +# Color Overtime Module + +The color module is used to set the particle color and the color change of the particle during its life cycle. + +![color_overtime](particle-system/color_overtime.gif) + +## Properties + +![color_module](particle-system/color_module.png) + +Properties | Features +---|--- +**color** | Set the particle color to fade over time. Click the right ![menu button](main-module/menu-button.png) button on the right side, you can choose to use different color change modes, please refer to [gradient color editor](./editor/gradient-editor.md). | diff --git a/versions/4.0/en/particle-system/editor/curve-editor.md b/versions/4.0/en/particle-system/editor/curve-editor.md new file mode 100644 index 0000000000..11562bc567 --- /dev/null +++ b/versions/4.0/en/particle-system/editor/curve-editor.md @@ -0,0 +1,17 @@ +# Curve Editor + +The curve editor can set the curve of a certain property in the particle system with time. The abscissa represents the unitized life cycle of a particle. For example, the life cycle of a particle is 5s, then 0.5 represents 2.5s. The ordinate represents the attribute value, and the ordinate interval can be adjusted through the upper edit bar. The default interval is **[0, 1]** or **[-1, 1]**. + +The interface of the curve editor is as follows: + +![curve_editor](img/curve_editor.png) + +The curve editor can perform the following operations: + +- The ordinate interval can be adjusted through the upper edit bar. +- Directly in the blank space, right-click on **Add Key Frame** to add a new key frame point and connect the curve. +- Right-click the key frame and select **Delete Key Frame** in the menu to delete it. +- Right-click a point on the curve or the blank space to add a keyframe. +- Drag the key frame to change its coordinates. +- You can change the slope of this point by turning the line segment next to the key frame. +- There is a built-in curve template on the right side of the editor. Click a template to apply it directly to the current curve. diff --git a/versions/4.0/en/particle-system/editor/gradient-editor.md b/versions/4.0/en/particle-system/editor/gradient-editor.md new file mode 100644 index 0000000000..6a50a4b076 --- /dev/null +++ b/versions/4.0/en/particle-system/editor/gradient-editor.md @@ -0,0 +1,12 @@ +## Gradient Editor +The gradient editor can set the color of a certain property in the particle system that changes with time. The interface of the gradient editor is as follows: + +![gradient_editor](img/gradient_editor.png) + +The gradient editor can perform the following operations: + +- Mode has two possible choices: Blend mode will be interpolated according to the two adjacent keyframes at the current time to get the color of the current frame. Fixed mode will directly use the color of the previous keyframe at the current time. +- Click an empty space above the ribbon to insert an alpha key frame, click an empty space below the ribbon to insert an rgb key frame. +- Drag the key frame to move left and right to adjust the key frame position, and drag the key frame to move up and down to delete the key frame. +- The corresponding rgb or alpha value can be edited in the Color / Alpha edit box. +- **Location** can edit the position of the selected key frame. \ No newline at end of file diff --git a/versions/4.0/en/particle-system/editor/img/curve_editor.png b/versions/4.0/en/particle-system/editor/img/curve_editor.png new file mode 100644 index 0000000000..0fc4dca1ea Binary files /dev/null and b/versions/4.0/en/particle-system/editor/img/curve_editor.png differ diff --git a/versions/4.0/en/particle-system/editor/img/gradient_editor.png b/versions/4.0/en/particle-system/editor/img/gradient_editor.png new file mode 100644 index 0000000000..a024af24e8 Binary files /dev/null and b/versions/4.0/en/particle-system/editor/img/gradient_editor.png differ diff --git a/versions/4.0/en/particle-system/editor/img/particle_panel.png b/versions/4.0/en/particle-system/editor/img/particle_panel.png new file mode 100644 index 0000000000..04ab5b5615 Binary files /dev/null and b/versions/4.0/en/particle-system/editor/img/particle_panel.png differ diff --git a/versions/4.0/en/particle-system/editor/img/particle_play.png b/versions/4.0/en/particle-system/editor/img/particle_play.png new file mode 100644 index 0000000000..70e0b8e28c Binary files /dev/null and b/versions/4.0/en/particle-system/editor/img/particle_play.png differ diff --git a/versions/4.0/en/particle-system/editor/img/particle_replay.png b/versions/4.0/en/particle-system/editor/img/particle_replay.png new file mode 100644 index 0000000000..0c6be96289 Binary files /dev/null and b/versions/4.0/en/particle-system/editor/img/particle_replay.png differ diff --git a/versions/4.0/en/particle-system/editor/img/particle_stop.png b/versions/4.0/en/particle-system/editor/img/particle_stop.png new file mode 100644 index 0000000000..a147a502e0 Binary files /dev/null and b/versions/4.0/en/particle-system/editor/img/particle_stop.png differ diff --git a/versions/4.0/en/particle-system/editor/index.md b/versions/4.0/en/particle-system/editor/index.md new file mode 100644 index 0000000000..ee99af7dc8 --- /dev/null +++ b/versions/4.0/en/particle-system/editor/index.md @@ -0,0 +1,7 @@ +# Particle Property Editor + +The particle property editor mainly includes an introduction to the interface for editing particle properties and how to view and edit the status of selected particles. It mainly consists of the following parts: + +- [Curve Editor](curve-editor.md) +- [Gradient Editor](gradient-editor.md) +- [Particle Control Panel](particle-effect-panel.md) diff --git a/versions/4.0/en/particle-system/editor/particle-effect-panel.md b/versions/4.0/en/particle-system/editor/particle-effect-panel.md new file mode 100644 index 0000000000..7019b473ee --- /dev/null +++ b/versions/4.0/en/particle-system/editor/particle-effect-panel.md @@ -0,0 +1,17 @@ +## Particle Control Panel + +The particle control panel can perform some operations on the particles selected in the editor. The interface is as follows: + +![particle_panel](img/particle_panel.png) + +The following operations are possible: + +![particle_play](img/particle_play.png): Play / Pause particle + +![particle_replay](img/particle_replay.png): Replay particle + +![particle_stop](img/particle_stop.png): Stop playing particle + +- Playback Speed: Adjust particle playback speed +- Playback Time: Show particle duration +- Particles: Display the current number of particles diff --git a/versions/4.0/en/particle-system/emitter.md b/versions/4.0/en/particle-system/emitter.md new file mode 100644 index 0000000000..79d2c91368 --- /dev/null +++ b/versions/4.0/en/particle-system/emitter.md @@ -0,0 +1,118 @@ +# ShapeModule + +The shape module is mainly used to set the particle emitter shape, particle emission direction and velocity. + +![shape-module](emitter/shape-module.png) + +## Public properties + +There are a number of properties that are common to all types of emitters, including: + +Property | Features +:---|:--- +| **ShapeType** | Emitter shapes. The types **Box**, **Circle**, **Cone**, **Sphere**, **Hemisphere** are currently supported. | +| **Position** | The position of the emitter relative to the node where it is located | +| **Rotation** | Rotation of the emitter relative to its node, which can be used to adjust the particle emission direction | +| **Scale** | Scaling of the emitter relative to the node it is on can be used to adjust the size of the emitter shape, i.e. the particle emission range | +| **AlignToDirection** | The direction of particle movement is determined by the initial direction of particle emission, which can be used to simulate an effect similar to water spilling out of a bucket | +| **RandomDirectionAmount** | Particle generation direction random mix setting.
No effect when set to 0;
Random direction when set to 1 | +| **SphericalDirectionAmount** | Indicates the interpolation between the current emission direction and the direction of the line from the current position to the center of the node.
When this value is set to 1, particles are emitted from the center to the outside (the same behavior as when **ShapeType** is set to **Sphere**) | +| **RandomPositionAmount** | The particle generation position is set randomly, when this value is set **not 0**, the particle generation position will be out of the size range of the generator | + +## Box + +When the **ShapeType** property is set to **Box**, it is a box emitter. + +![box_emitter_property](emitter/box_emitter_property.png) + +The special property items if box emitter are described as follows. + +| Properties | Description | +| :---|:--- | +| **EmitFrom** | The part of the box from which the emitter emits particles, including:
**Volume**: Emitted from inside the box
**Shell**: Emitted from the box surface
**Edge**: Emitted from the box edge | +| **BoxThickness** | The thickness of the box in each emitting direction, effective only when **EmitFrom** is selected for **Shell** mode | + +Also in the **Scene Editor** there will be a box Gizmo that shows the shape size of the box emitter. + +![box_emitter](emitter/box_emitter.png) + +The **Scale** property of the box emitter can be adjusted by dragging the box Gizmo's square control point directly: + +![box_gizmo](emitter/box-gizmo.gif) + +## Circle + +When the **ShapeType** property is set to **Circle**, it is a circle emitter. + +![circle_emitter_property](emitter/circle_emitter_property.png) + +The special property items are described as follows: + +| Properties | Description | +| :--- | :--- | +| **Radius** | Set the radius of the circle emitter | +| **RadiusThickness** | The position of the round emitter emitting particles.
When set to **0**, it means firing from the circumference of the circle
When set to **1**, it means firing from the center of the circle
When set to between **0~1**, it means firing from the center of the circle to the circumference of the circle, within a certain proportional range | +| **Arc** | Set the fan area where the circle emitter emits particles +| **ArcMode** | Sets how the particles are emitted in the sector, including.
**Random**: random firing
**Loop**: cyclic firing in a certain direction, each cycle in the same direction
**PingPong**: cyclic firing, each cycle in the opposite direction of the previous one | +| **ArcSpread** | Sets the discrete interval around the arcs where particles may be generated. Example:
When set to 0, particles can be generated anywhere in the arc;
when set to 0.2, particles are generated only at 20% intervals around the arc. | +| **ArcSpeed** | Indicates the velocity of the particle emitted along the arc. Click the down icon button on the right to open the curve editor to edit the curve for this property. + +Also in the **Scene Editor** there will be a circular Gizmo showing the shape size of the emitter. + +![circle_emitter](emitter/circle_emitter.png) + +The **Radius** property of the circle emitter can be adjusted by directly dragging the square control point of the circular Gizmo. + +![circle_gizmo](emitter/circle-gizmo.gif) + +## Cone + +When the **ShapeType** property is set to **Cone**, we call it a cone emitter. In the **Scene Editor** there will be a Cone Gizmo that shows the shape size of the emitter: + +![cone_emitter](emitter/cone_emitter.png) + +> The cross section below the cone with the smaller radius is called the bottom of the cone, as shown in the above illustration. + +The properties of the cone emitter are shown in the **Inspector** panel as follows. + +![cone_emitter_property](emitter/cone_emitter_property.png) + +The special property items are described as follows. + +| Properties | Description | +| :---|:--- | +| **EmitFrom** | From which part of the cone the emitter emits particles, including:
**Base**: Emitted from the bottom of the cone
**Shell**: Emitted from the circumference of the bottom of the cone
**Volume**: Emitted from the inside of the cone | +| **Radius** | Set the radius of the bottom cross section of the cone | +| **RadiusThickness** | The position of the emitted particles from the cone emitter.
When set to **0**, it means emitting from the surface of the cone
When set to **1**, it means emitting from the center of the cone
When set to between **0~1**, it means emitting from the center of the cone to the surface, within a certain proportional range | +| **Angle** | The angle between the axis of the cone and the bus, the larger the value the greater the angle, the greater the degree of opening and closing of the cone emitter | +| **Arc** | Set the fan area where the cone emitter emits particles | +| **ArcMode** | indicates how the particles are emitted in the fan area.
**Random**: random position
**Loop**: Emitted cyclically in a certain direction, each cycle in the same direction
**PingPong**: Emitted cyclically, each cycle in the opposite direction
**spread**: indicates that the particle is emitted at a certain interval, for example, 0 means it can be emitted at any position; 0.1 means it is emitted every tenth of the circumference of the circle | +| **ArcSpeed** | The speed of the particle emitted along the arc +| **ArcSpread** | indicates the velocity of the particle along the arc. Click the down icon button on the right to open the curve editor to edit the curve | +| **Length** | The axis length of the top section of the cone from the bottom | + +The **Angle**, **Length**, and **Radius** properties of the cone emitter can also be set by directly dragging the cone Gizmo's square control point in the scene. + +## Sphere / Hemisphere + +When the **ShapeType** property is set to **Sphere**/**Hemisphere**, we call it a sphere/hemisphere emitter. + +The properties of the sphere emitter and the hemisphere emitter are the same, and here we use the sphere emitter as an example. + +![Sphere_emitter_property](emitter/sphere-emitter-property.png) + +The special property items are described as follows. + +| Properties | Description | +| :---|:--- | +| **EmitFrom** | From which part of the sphere the emitter emits particles, including:
**Volume**: Emits from inside the sphere
**Shell**: Emits from the surface of the sphere (not in effect yet, can be set by **RadiusThickness**) | +| **Radius** | The radius of the sphere. You can also set it by dragging the control point of the sphere Gizmo in the **Scene Editor** directly | +| **RadiusThickness** | The position of the particle emitted by the sphere emitter.
When set to **0**, it means emitting from the surface of the ball
When set to **1**, it means emitting from the center of the sphere
When set to between **0~1**, it means emitting from the surface to the center of the sphere, within a certain proportional range | + +The sphere emitter will have a sphere Gizmo in the **Scene Editor** that shows the shape size of the emitter. + +![Sphere_emitter](emitter/sphere-emitter.png) + +Similarly, the hemisphere emitter has a hemisphere Gizmo in the **Scene Editor** that shows the shape size of the emitter: + +![Hemisphere_emitter](emitter/hemisphere-emitter.png) diff --git a/versions/4.0/en/particle-system/emitter/box-gizmo.gif b/versions/4.0/en/particle-system/emitter/box-gizmo.gif new file mode 100644 index 0000000000..2a1d8a3cf1 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/box-gizmo.gif differ diff --git a/versions/4.0/en/particle-system/emitter/box_emitter.png b/versions/4.0/en/particle-system/emitter/box_emitter.png new file mode 100644 index 0000000000..3ac3bca03e Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/box_emitter.png differ diff --git a/versions/4.0/en/particle-system/emitter/box_emitter_property.png b/versions/4.0/en/particle-system/emitter/box_emitter_property.png new file mode 100644 index 0000000000..342a6946c8 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/box_emitter_property.png differ diff --git a/versions/4.0/en/particle-system/emitter/circle-gizmo.gif b/versions/4.0/en/particle-system/emitter/circle-gizmo.gif new file mode 100644 index 0000000000..3b1569d7d2 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/circle-gizmo.gif differ diff --git a/versions/4.0/en/particle-system/emitter/circle_emitter.png b/versions/4.0/en/particle-system/emitter/circle_emitter.png new file mode 100644 index 0000000000..bfcd35e2f2 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/circle_emitter.png differ diff --git a/versions/4.0/en/particle-system/emitter/circle_emitter_property.png b/versions/4.0/en/particle-system/emitter/circle_emitter_property.png new file mode 100644 index 0000000000..c22282e1f1 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/circle_emitter_property.png differ diff --git a/versions/4.0/en/particle-system/emitter/cone_emitter.png b/versions/4.0/en/particle-system/emitter/cone_emitter.png new file mode 100644 index 0000000000..b118b613b7 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/cone_emitter.png differ diff --git a/versions/4.0/en/particle-system/emitter/cone_emitter_property.png b/versions/4.0/en/particle-system/emitter/cone_emitter_property.png new file mode 100644 index 0000000000..c5ce468b91 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/cone_emitter_property.png differ diff --git a/versions/4.0/en/particle-system/emitter/hemisphere-emitter.png b/versions/4.0/en/particle-system/emitter/hemisphere-emitter.png new file mode 100644 index 0000000000..56b36588f6 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/hemisphere-emitter.png differ diff --git a/versions/4.0/en/particle-system/emitter/shape-module.png b/versions/4.0/en/particle-system/emitter/shape-module.png new file mode 100644 index 0000000000..c84aca768c Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/shape-module.png differ diff --git a/versions/4.0/en/particle-system/emitter/sphere-emitter-property.png b/versions/4.0/en/particle-system/emitter/sphere-emitter-property.png new file mode 100644 index 0000000000..abefa558b8 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/sphere-emitter-property.png differ diff --git a/versions/4.0/en/particle-system/emitter/sphere-emitter.png b/versions/4.0/en/particle-system/emitter/sphere-emitter.png new file mode 100644 index 0000000000..dea03417a0 Binary files /dev/null and b/versions/4.0/en/particle-system/emitter/sphere-emitter.png differ diff --git a/versions/4.0/en/particle-system/force-module.md b/versions/4.0/en/particle-system/force-module.md new file mode 100644 index 0000000000..9b6ba43a24 --- /dev/null +++ b/versions/4.0/en/particle-system/force-module.md @@ -0,0 +1,18 @@ +# Force Overtime Module + +The module allows acceleration of particles to simulate a wind-like effect. + +![force_overtime](module/force_overtime.gif) + +## Properties + +![force_module](module/force_module.png) + +| Properties | Description | +| :--- | :--- | +| **Space** | The acceleration calculation is based on **World Coordinate System** or **Local Coordinate System**
(**Custom** is not supported at this time) | +| **X** | Acceleration in the X-axis | +| **Y** | Acceleration in the Y-axis | +| **Z** | Acceleration in the Z-axis | + +Click the ![menu button](main-module/menu-button.png) button to the right of the property input box, you can choose to edit the curves of the property, please refer to [curve editor](./editor/curve-editor.md) for details. diff --git a/versions/4.0/en/particle-system/index.md b/versions/4.0/en/particle-system/index.md new file mode 100644 index 0000000000..46cbaba911 --- /dev/null +++ b/versions/4.0/en/particle-system/index.md @@ -0,0 +1,10 @@ +# Particle System + +Particle system is the basis of game engine effects performance, it can be used to simulate natural phenomena such as fire, smoke, water, clouds, snow, falling leaves, etc. It can also be used to simulate abstract visual effects such as luminous trails, speed lines, etc. + +![particle](particle.gif) + +Creator supports 2D/3D particle system, please refer to the following for details: + +- [2D Particle System](2d-particle/2d-particle.md) +- [3D Particle System](overview.md) diff --git a/versions/4.0/en/particle-system/limit-velocity-module.md b/versions/4.0/en/particle-system/limit-velocity-module.md new file mode 100644 index 0000000000..c05e5229d6 --- /dev/null +++ b/versions/4.0/en/particle-system/limit-velocity-module.md @@ -0,0 +1,13 @@ +# Limit Velocity Overtime Module + +The speed limit module is used to set the speed of the particles to gradually slow down over the life cycle. + +![limit_module](particle-system/limit_module.png) + +Property | Features +:---|:--- +**space** | In which coordinate system the speed is calculated. +**limit** | Lower speed limit. When the speed exceeds this value, the current speed is linearly interpolated with this speed. It is valid when separateAxes is false. +**dampen** | Interpolation of current speed and lower speed limit. +**separateAxes** | Whether the three axes are restricted separately. +**limit X,Y,Z** | The lower speed limits of the three axes are valid when separateAxes is true. diff --git a/versions/4.0/en/particle-system/line-component.md b/versions/4.0/en/particle-system/line-component.md new file mode 100644 index 0000000000..ef84687bc9 --- /dev/null +++ b/versions/4.0/en/particle-system/line-component.md @@ -0,0 +1,15 @@ +# Line Component + +The Line component is used to render a line segment connected by a given point in a 3D scene. The line segment rendered by the Line component is wide and always faces the camera, which is similar to the billboard component. + +Properties | Features +---|--- +**texture** | The map displayed in the line segment. +**worldSpace** | Which coordinate system is used for the coordinates of each point in the line segment, check Use world coordinate system, and deselect use local coordinate system. +**positions** | The coordinates of the end points of each line segment. +**wdith** | The width of the line segment, if a curve is used, it means the change of the curve along the direction of the line segment. +**tile** | Number of texture tiles. +**offset** | The offset of the texture coordinates. +**color** | The color of the line segment. If a gradient color is used, it indicates the color gradient along the direction of the line segment. + +To use `Line`, please refer to the [Line API](%__APIDOC__%/en/class/Line). diff --git a/versions/4.0/en/particle-system/main-module.md b/versions/4.0/en/particle-system/main-module.md new file mode 100644 index 0000000000..9c62172fc1 --- /dev/null +++ b/versions/4.0/en/particle-system/main-module.md @@ -0,0 +1,63 @@ +# Main Module (Particle System) + +The particle system main module is used to store all data displayed in the **Inspector** panel, manage particle generation, playback, updates, and destroy related modules. + +![main-module](main-module/main-module.png) + +| Property | Description | +| :-- | :-- | +| Duration | Total running time of the particle system. | +| Capacity | The maximum number of particles that the particle system can generate. | +| Loop | Whether the particle system loops. | +| PlayOnAwake | Whether the particle system automatically starts playing after loading. | +| Prewarm | After being selected, the particle system will start playing after one round has been played (only valid when loop playback is enabled). | +| SimulationSpace | The coordinate system where the particle coordinates are calculated. | +| SimulationSpeed | The update rate of the entire particle system. | +| StartDelay | The delayed emission time of the particles after the particle system starts running. (StartDelay will be set to 0 if Prewarm is selected)| +| StartLifetime | The life cycle of particles. | +| StartColor | The initial color of particles. | +| ScaleSpace | The coordinate system in which the particle is scaled
**Local**: scaling based on the local coordinate system
**World**: scaling based on the world coordinate system
**Custom**: custom scaling, not affected by the node's **scale** property. | +| StartSize3D | The initial size of the X, Y, and Z axes of particles. | +| StartSize | The initial size of the X-axis of particles. Cannot be used with as `StartSize3D`. | +| StartSpeed | The initial velocity of particle. | +| StartRotation3D | The initial rotation angles of the X, Y, and Z axes of particles. | +| StartRotation | The initial rotation angle of particles. | +| GravityModifier | Gravity coefficient for particles affected by gravity (only CPU particles are supported). | +| RateOverTime | Number of particles emitted per second. | +| RateOverDistance | Number of particles emitted per moving unit distance. | +| Bursts | The number of Bursts that will emit the specified number of particles at the specified time. It can be adjusted by the following properties:
**Time**: how long the particles play before the Burst starts to emit
**RepeatCount**: number of Burst triggers
**RepeatInterval**: time interval for each trigger
**Count**: number of particles fired. | +| DataCulling | Particle system asset culling, please refer to the description below for details. | +| RenderCulling | Particle culling, please refer to the description below for details. | + +Click the ![menu button](main-module/menu-button.png) button to the right of the above property input box to open the particle curve/gradient editor and edit the particle properties, please refer to the [Particle Property Editor](./editor/index.md) documentation. + +![set-pro](main-module/set-pro.png) + +To use Particle System, please refer to the [Particle System API](%__APIDOC__%/en/class/ParticleSystem). + +## Particle System Asset Culling + +The **DataCulling** option is used to cull the asset data of useless modules in the particle system. + +Each module in the particle system exists as an independent object, and each module stores some module-related data, so the data recorded for modules that are not checked for use are useless data. When the developer does not need to dynamically open these unused modules at runtime, he can check the **DataCulling** option to cull these useless data and thus reduce asset usage. + +> **Note**: before v3.4 this option was **EnableCulling**, in v3.4 it was renamed to **DataCulling** to distinguish it from **RenderCulling** below. This adjustment has been done for compatibility, and it does not affect users in any way. + +## Particle Culling + +Starting with v3.4, a new **RenderCulling** option has been added to the particle system to enable particle culling. + +If this option is enabled, the particle emitter will automatically calculate a culling box, that will be used to cull the particle emitter at runtime depending on whether the culling box is within the visible range of the camera, or not. The culling operation will be performed every frame, which is suitable for some time-consuming effects, it is not recommended to turn this feature on if the number of particles is small. + +The size of the bounding box can be adjusted by using the **AabbHalf** button in the picture below. Once the adjustment is complete, click the **Regenerate bounding box** button to recalculate the bounding box. + +![render culling](main-module/render-culling.png) + +| Property | Description | +| :--- | :--- | +| CullingMode | The behavior of the particle emitter after it has been culled. Possible options include **Pause**, **Pause and Catchup**, **Always Simulate**.
**Pause**: if the particle emitter bounding box is not in the visible range of the camera, particles will pause the simulation. If it resumes visibility, particles will continue the simulation at the same time as the last pause;
**Pause and Catchup**: if the particle emitter bounding box is not in the visible range of the camera, particles will pause the simulation. If it resumes visibility, particles will start simulating at the current time;
**Always Simulate**: particles will always simulate whether or not the particle emitter box is in the camera's visible range, but it will not render when it is not in the camera's visible range. | +| AabbHalfX | The half-width of the particle emitter bounding box. | +| AabbHalfY | The half-height of the particle emitter bounding box. | +| AabbHalfZ | The half-length of the particle emitter bounding box. | +| Show Bounds | Shows the particle emitter bounding box in the **Scene** panel. | +| Regenerate bounding box| Click this button to recalculate the bounding box after it has been resized. | diff --git a/versions/4.0/en/particle-system/main-module/main-module.png b/versions/4.0/en/particle-system/main-module/main-module.png new file mode 100644 index 0000000000..3df45e5c30 Binary files /dev/null and b/versions/4.0/en/particle-system/main-module/main-module.png differ diff --git a/versions/4.0/en/particle-system/main-module/menu-button.png b/versions/4.0/en/particle-system/main-module/menu-button.png new file mode 100644 index 0000000000..e2d836084f Binary files /dev/null and b/versions/4.0/en/particle-system/main-module/menu-button.png differ diff --git a/versions/4.0/en/particle-system/main-module/render-culling.png b/versions/4.0/en/particle-system/main-module/render-culling.png new file mode 100644 index 0000000000..a72f725942 Binary files /dev/null and b/versions/4.0/en/particle-system/main-module/render-culling.png differ diff --git a/versions/4.0/en/particle-system/main-module/set-pro.png b/versions/4.0/en/particle-system/main-module/set-pro.png new file mode 100644 index 0000000000..7ae63a1de0 Binary files /dev/null and b/versions/4.0/en/particle-system/main-module/set-pro.png differ diff --git a/versions/4.0/en/particle-system/module.md b/versions/4.0/en/particle-system/module.md new file mode 100644 index 0000000000..4044d99a3e --- /dev/null +++ b/versions/4.0/en/particle-system/module.md @@ -0,0 +1,20 @@ +# Particle System Function Introduction + +Particle System stores the initial state of particle emission and the state update submodule after particle emission. + +## Particle System Module + +The Cocos Creator particle system operation panel is as follows: + +![inspector_1](module/inspector_1.png)
+![inspector_2](module/inspector_2.png) + +The particle system uses modules to organize functionality, including the following modules: + +| Module | Description | +| :--- | :--- | +| Node | Particle node, used to set the position, direction, size, rendering level and other properties of the particle emitter. | +| [ParticleSystem](main-module.md) (main module) | Used to store all the data displayed in the **Inspector** panel, manage particle generation, update, destroy related modules, and control particle playback | +| [ShapeModule](emitter.md) (emitter module) | Used to control particle emitting, including emitting direction and speed, support predefined emitting direction, including square, circle, cone, ball, hemisphere. | +| AnimatorModule | Used to control the state update after the particle is launched. Supported features include:
[NoiseModule](noise-module.md)
[VelocityOvertimeModule](velocity-module.md)
[ForceOvertimeModule](force-module.md)
[SizeOvertimeModule](size-module.md)
[RotationOvertimeModule](rotation-module.md)
[ColorOvertimeModule](color-module.md)
[TextureAnimationModule](texture-animation-module.md)
[LimitVelocityOvertimeModule](limit-velocity-module.md)
[TrailModule](trail-module.md) | [Renderer]
Effects-> Particle System__ to create a node with the particle system component, as shown below: + + ![new_ParticleSystemComponent_node](overview/new_ParticleSystemComponent_node.png) + +## Contents + +The particle system consists of the following two main parts: + +- [Particle System Module](./module.md) + +- [Particle Property Editor](./editor/index.md) diff --git a/versions/4.0/en/particle-system/overview/new_ParticleSystemComponent.png b/versions/4.0/en/particle-system/overview/new_ParticleSystemComponent.png new file mode 100644 index 0000000000..8f0a8f07b5 Binary files /dev/null and b/versions/4.0/en/particle-system/overview/new_ParticleSystemComponent.png differ diff --git a/versions/4.0/en/particle-system/overview/new_ParticleSystemComponent_node.png b/versions/4.0/en/particle-system/overview/new_ParticleSystemComponent_node.png new file mode 100644 index 0000000000..c8fa3ec96f Binary files /dev/null and b/versions/4.0/en/particle-system/overview/new_ParticleSystemComponent_node.png differ diff --git a/versions/4.0/en/particle-system/particle-system/billboard.png b/versions/4.0/en/particle-system/particle-system/billboard.png new file mode 100644 index 0000000000..8895d0d367 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/billboard.png differ diff --git a/versions/4.0/en/particle-system/particle-system/box_emitter.png b/versions/4.0/en/particle-system/particle-system/box_emitter.png new file mode 100644 index 0000000000..17d2f83eb6 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/box_emitter.png differ diff --git a/versions/4.0/en/particle-system/particle-system/circle_emitter.png b/versions/4.0/en/particle-system/particle-system/circle_emitter.png new file mode 100644 index 0000000000..f95c134ebb Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/circle_emitter.png differ diff --git a/versions/4.0/en/particle-system/particle-system/color_module.png b/versions/4.0/en/particle-system/particle-system/color_module.png new file mode 100644 index 0000000000..859b7bc877 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/color_module.png differ diff --git a/versions/4.0/en/particle-system/particle-system/color_overtime.gif b/versions/4.0/en/particle-system/particle-system/color_overtime.gif new file mode 100644 index 0000000000..7f6e204423 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/color_overtime.gif differ diff --git a/versions/4.0/en/particle-system/particle-system/cone_emitter.png b/versions/4.0/en/particle-system/particle-system/cone_emitter.png new file mode 100644 index 0000000000..449ac2a66d Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/cone_emitter.png differ diff --git a/versions/4.0/en/particle-system/particle-system/force_module.png b/versions/4.0/en/particle-system/particle-system/force_module.png new file mode 100644 index 0000000000..feeb2ff254 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/force_module.png differ diff --git a/versions/4.0/en/particle-system/particle-system/force_overtime.gif b/versions/4.0/en/particle-system/particle-system/force_overtime.gif new file mode 100644 index 0000000000..8734abbbcf Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/force_overtime.gif differ diff --git a/versions/4.0/en/particle-system/particle-system/inspector_1.png b/versions/4.0/en/particle-system/particle-system/inspector_1.png new file mode 100644 index 0000000000..14659b9335 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/inspector_1.png differ diff --git a/versions/4.0/en/particle-system/particle-system/inspector_2.png b/versions/4.0/en/particle-system/particle-system/inspector_2.png new file mode 100644 index 0000000000..0fdf1ad286 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/inspector_2.png differ diff --git a/versions/4.0/en/particle-system/particle-system/limit_module.png b/versions/4.0/en/particle-system/particle-system/limit_module.png new file mode 100644 index 0000000000..8eacc85cfc Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/limit_module.png differ diff --git a/versions/4.0/en/particle-system/particle-system/main.png b/versions/4.0/en/particle-system/particle-system/main.png new file mode 100644 index 0000000000..edc12859a5 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/main.png differ diff --git a/versions/4.0/en/particle-system/particle-system/new_ParticleSystemComponent.png b/versions/4.0/en/particle-system/particle-system/new_ParticleSystemComponent.png new file mode 100644 index 0000000000..82ece744ef Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/new_ParticleSystemComponent.png differ diff --git a/versions/4.0/en/particle-system/particle-system/new_ParticleSystemComponent_node.png b/versions/4.0/en/particle-system/particle-system/new_ParticleSystemComponent_node.png new file mode 100644 index 0000000000..05427ee2b4 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/new_ParticleSystemComponent_node.png differ diff --git a/versions/4.0/en/particle-system/particle-system/particle-system-class.png b/versions/4.0/en/particle-system/particle-system/particle-system-class.png new file mode 100644 index 0000000000..6c86644ca6 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/particle-system-class.png differ diff --git a/versions/4.0/en/particle-system/particle-system/renderer.png b/versions/4.0/en/particle-system/particle-system/renderer.png new file mode 100644 index 0000000000..35a2d30735 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/renderer.png differ diff --git a/versions/4.0/en/particle-system/particle-system/rotate_overtime.gif b/versions/4.0/en/particle-system/particle-system/rotate_overtime.gif new file mode 100644 index 0000000000..67fb69bb3b Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/rotate_overtime.gif differ diff --git a/versions/4.0/en/particle-system/particle-system/rotation_module.png b/versions/4.0/en/particle-system/particle-system/rotation_module.png new file mode 100644 index 0000000000..958c7f9248 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/rotation_module.png differ diff --git a/versions/4.0/en/particle-system/particle-system/size_module.png b/versions/4.0/en/particle-system/particle-system/size_module.png new file mode 100644 index 0000000000..1bebfb3db5 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/size_module.png differ diff --git a/versions/4.0/en/particle-system/particle-system/size_overtime.gif b/versions/4.0/en/particle-system/particle-system/size_overtime.gif new file mode 100644 index 0000000000..14f5b58305 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/size_overtime.gif differ diff --git a/versions/4.0/en/particle-system/particle-system/sphere_emitter.png b/versions/4.0/en/particle-system/particle-system/sphere_emitter.png new file mode 100644 index 0000000000..9090851554 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/sphere_emitter.png differ diff --git a/versions/4.0/en/particle-system/particle-system/texture_animation.gif b/versions/4.0/en/particle-system/particle-system/texture_animation.gif new file mode 100644 index 0000000000..5bf3bd664a Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/texture_animation.gif differ diff --git a/versions/4.0/en/particle-system/particle-system/texture_animation.png b/versions/4.0/en/particle-system/particle-system/texture_animation.png new file mode 100644 index 0000000000..ef5f207b92 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/texture_animation.png differ diff --git a/versions/4.0/en/particle-system/particle-system/trail.gif b/versions/4.0/en/particle-system/particle-system/trail.gif new file mode 100644 index 0000000000..285ba2161c Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/trail.gif differ diff --git a/versions/4.0/en/particle-system/particle-system/trail_module.png b/versions/4.0/en/particle-system/particle-system/trail_module.png new file mode 100644 index 0000000000..39b1844d96 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/trail_module.png differ diff --git a/versions/4.0/en/particle-system/particle-system/velocity_module.png b/versions/4.0/en/particle-system/particle-system/velocity_module.png new file mode 100644 index 0000000000..95503932a3 Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/velocity_module.png differ diff --git a/versions/4.0/en/particle-system/particle-system/velocity_overtime.gif b/versions/4.0/en/particle-system/particle-system/velocity_overtime.gif new file mode 100644 index 0000000000..ed5ca8286c Binary files /dev/null and b/versions/4.0/en/particle-system/particle-system/velocity_overtime.gif differ diff --git a/versions/4.0/en/particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md b/versions/4.0/en/particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md new file mode 100644 index 0000000000..980c92a622 --- /dev/null +++ b/versions/4.0/en/particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md @@ -0,0 +1,102 @@ +# Upgrade Guide: Particle from v3.5.x to v3.6.0 + +## CPU Particle + +The particle shader particle-vs-legacy.chunk should be modified to support instance particles added in v3.6.0. + +layout before +```` +in vec3 a_position; // center position +in vec3 a_texCoord; // xy:vertex index,z:frame index +in vec3 a_texCoord1; // size +in vec3 a_texCoord2; // rotation +in vec4 a_color; + +#if CC_RENDER_MODE == RENDER_MODE_STRETCHED_BILLBOARD + in vec3 a_color1; // velocity.x, velocity.y, velocity.z, scale +#endif + +#if CC_RENDER_MODE == RENDER_MODE_MESH + in vec3 a_texCoord3; // mesh vertices + in vec3 a_normal; // mesh normal + in vec4 a_color1; // mesh color +#endif +```` + +layout now + +```` +in vec3 a_texCoord1; // size +in vec3 a_texCoord2; // rotation +in vec4 a_color; + +in vec3 a_texCoord; // xy:vertex index,z:frame index +#if !CC_INSTANCE_PARTICLE + in vec3 a_position; // center position +#endif +#if CC_INSTANCE_PARTICLE + in vec4 a_texCoord4; // xyz:position,z:frame index +#endif + +#if CC_RENDER_MODE == RENDER_MODE_STRETCHED_BILLBOARD + in vec3 a_color1; // velocity.x, velocity.y, velocity.z, scale +#endif + +#if CC_RENDER_MODE == RENDER_MODE_MESH + in vec3 a_texCoord3; // mesh vertices + in vec3 a_normal; // mesh normal + in vec4 a_color1; // mesh color +#endif +```` + +Other shader code should be modified refer to particle-vs-legacy.chunk of v3.6.0 + +## GPU Particle + +GPU particle shader particle-vs-gpu.chunk should be modified. + +layout before + +```` +in vec4 a_position_starttime; // center position,particle start time +in vec4 a_size_uv; // xyz:size, w:uv_0 +in vec4 a_rotation_uv; // xyz:rotation, w:uv_1 +in vec4 a_color; +in vec4 a_dir_life; // xyz:particle start velocity,w:particle lifetime +in float a_rndSeed; + +#if CC_RENDER_MODE == RENDER_MODE_MESH + in vec3 a_texCoord; // mesh uv + in vec3 a_texCoord3; // mesh vertices + in vec3 a_normal; // mesh normal + in vec4 a_color1; // mesh color +#endif +```` + +layout after + +```` +in vec4 a_position_starttime; // center position,particle start time +in vec4 a_color; +in vec4 a_dir_life; // xyz:particle start velocity,w:particle lifetime +in float a_rndSeed; + +#if !CC_INSTANCE_PARTICLE + in vec4 a_size_uv; // xyz:size, w:uv_0 + in vec4 a_rotation_uv; // xyz:rotation, w:uv_1 +#endif +#if CC_INSTANCE_PARTICLE + in vec4 a_size_fid; // xyz:size, w:fid + in vec3 a_rotation; // xyz:rotation + in vec3 a_uv; +#endif + +#if CC_RENDER_MODE == RENDER_MODE_MESH + in vec3 a_texCoord; // mesh uv + in vec3 a_texCoord3; // mesh vertices + in vec3 a_normal; // mesh normal + in vec4 a_color1; // mesh color +#endif +```` + +Other shader code should be modified refer to particle-vs-gpu.chunk of v3.6.0. diff --git a/versions/4.0/en/particle-system/particle.gif b/versions/4.0/en/particle-system/particle.gif new file mode 100644 index 0000000000..21ff8003ab Binary files /dev/null and b/versions/4.0/en/particle-system/particle.gif differ diff --git a/versions/4.0/en/particle-system/renderer.md b/versions/4.0/en/particle-system/renderer.md new file mode 100644 index 0000000000..b9b465813d --- /dev/null +++ b/versions/4.0/en/particle-system/renderer.md @@ -0,0 +1,24 @@ +# Renderer + +The renderer module is used to generate the data needed for particle rendering. + +![renderer](particle-system/renderer.png) + +| Property | Description | +| :--- | :--- | +| **RenderMode** | Set a particle patch generation method.
**Billboard** particles always face the camera
**StretchedBillboard** particles always face the camera, but will be stretched according to the relevant parameters
**HorizontalBillboard** particles patches are always in the x-z plane parallel
**VerticalBillboard** particles patch is always parallel to the Y axis, but will face the camera
**Mesh** particles are a model. | +| **VelocityScale** | In **stretchedBillboard** mode, the particles are stretched according to the speed in the direction of motion. | +| **LengthScale** | In **StretchedBillboard** mode, the particles are stretched by the size of the particles in the direction of motion. | +| **Mesh** | When **RenderMode** is set to **Mesh**, specify the model of particles to be rendered. | +| **ParticleMaterial** | The material used for particle rendering.
When use the CPU renderer, that is, when **UseGPU** option is not selected, the effect used by the material can only be `builtin-particle`, and other effects are not supported.
When use the GPU renderer, that is, when **UseGPU** option is checked, the effect used by the material can only be `builtin-particle-gpu`, and other effects are not supported. | +| **TrailMaterial** | The material used for trail rendering. The effect of the material only supports `builtin-particle-trail`, not other effects. | +| **UseGPU** | Whether to use the GPU renderer for particle rendering, it is not selected by default.
When this option is not checked, use the CPU renderer **ParticleSystemRendererCPU** to render particles.
When this option is checked, use the GPU renderer **ParticleSystemRendererGPU** to render particles. | +| **AlignSpace** | Particle alignment space, options include **View** (default), **World** and **Local**, selecting different spaces will determine the initial particle orientation:
When **View** is selected, the rotation direction of the particle mesh will follow the camera's view direction.
When **World** is selected, the direction of the particle mesh will use the world space rotation direction of the emitter node.
When **Local** is selected, the particle mesh will use the local space rotation direction of the emitter node. | + +## Particle Renderer + +The particle rendering part is controlled by **ParticleSystemRenderer**, which is divided into **CPU renderer** and **GPU renderer**. + +The CPU renderer maintains all particles through an object pool, generates corresponding vb and ib data according to the current state of the particles, holds the materials to be rendered by the particles, and saves the relevant rendering state. + +The GPU renderer generates particles on the CPU side, and only submits the vb and ib data of initial parameter, but the calculations related to the module are in the form of pre-sampled data. The data is submitted once during initialization, and the subsequent module system extracts and simulates the data on the GPU side to reduce the computing pressure on the CPU side. The subsequent versions will continue to optimize and improve the particle system. The current GPU version does not support `TrailModule` and `LimitVelocityOvertimeModule`. diff --git a/versions/4.0/en/particle-system/rotation-module.md b/versions/4.0/en/particle-system/rotation-module.md new file mode 100644 index 0000000000..bdbcd08271 --- /dev/null +++ b/versions/4.0/en/particle-system/rotation-module.md @@ -0,0 +1,16 @@ +# Rotation Overtime Module + +The rotate module is used to set the particles to rotate on the fly when running, and can be used to simulate random rotating effects like falling snowflakes. + +![rotation_module](module/rotate_overtime.gif) + +## Properties + +![rotation_module](module/rotation_module.png) + +| Properties | Description | +| :--- | :--- | +| **SeparateAxes** | Whether to set the particle rotation of the three axes separately | +| **X、Y、Z** | Sets the angular velocity of rotation around the X, Y, and Z axes. Where **X**, **Y** are only displayed when the **SeparateAxes** property is checked. | + +Click the ![menu button](main-module/menu-button.png) button to the right of the property input box, you can choose to edit the curves of the property, please refer to [curve editor](./editor/curve-editor.md) for details. diff --git a/versions/4.0/en/particle-system/size-module.md b/versions/4.0/en/particle-system/size-module.md new file mode 100644 index 0000000000..1fad2ef734 --- /dev/null +++ b/versions/4.0/en/particle-system/size-module.md @@ -0,0 +1,22 @@ +# Size Overtime Module + +The Size module is used to set the size of the particles during their lifetime, thus achieving particle effects like flames and snowflakes of varying sizes. + +![size_overtime](module/size_overtime.gif) + +## Properties + +![size_overtime](module/size_module.png) + +| Properties | Description | +| :--- | :--- | +| **SeparateAxes** | Whether to set particle size separately on X, Y, Z axis.
When you click the ![menu button](main-module/menu-button.png) button on the right side of the input box and switch to use curve editing, it indicates whether the three axes are scaled separately. Please refer to the following for details. | +| **Size** | Set the particle size.
When switching to use curve editing, you can set the curve of particle size versus time.
This item and the **separateAxes** attribute, only one of the two can be selected. | + +## Separate Axes + +Check the **SeparateAxes** property, click the ![menu button](main-module/menu-button.png) button and select **Curve** to switch to curve editing mode. You can then define the curve to specify the size change of the particle in the X, Y and Z directions during its lifetime (Z is only used for mesh particles). + +![size_module_curve](module/size_module_curve.png) + +For details, please refer to [Curve Editor](./editor/curve-editor.md). diff --git a/versions/4.0/en/particle-system/texture-animation-module.md b/versions/4.0/en/particle-system/texture-animation-module.md new file mode 100644 index 0000000000..6c72ff0d63 --- /dev/null +++ b/versions/4.0/en/particle-system/texture-animation-module.md @@ -0,0 +1,21 @@ +# Texture Animation Module + +The texture animation module is used to animate the texture specified by the **ParticleMaterial** property in the [Particle Renderer](./renderer.md) with the texture specified by the **ParticleMaterial** property as an animation frame to achieve an effect similar to the one in the following figure. + +![texture_animation](particle-system/texture_animation.gif) + +## Properties + +![texture_animation](particle-system/texture_animation.png) + +| Properties | Description | +| :--- | :--- | +| **Mode** | Set the type of particle animation mapping, currently only **Grid** (grid) mode is supported. A texture contains one animation frame for particle playback. +| **NumTilesX** | The number of maps divided by the texture in the horizontal (X) direction. +| **NumTilesY** | The number of maps in the vertical (Y) direction of the texture. +| **Animation** | Animation playback methods, including:
**WholeSheet**: play all frames in the mapping;
**SingleRow**: play only one of the rows, the first row by default. Can be used with **RandomRow** and **RowIndex** properties. +| **RandomRow** | Select a random row from the animation map to play the animation frames.
This item only takes effect when **Animation** is set to **SingleRow**. +| **RowIndex** | Select a specific row from the animation map to play the animation frames.
This item only takes effect when **Animation** is set to **SingleRow** and **RandomRow** is disabled. +| **FrameOverTime** | Set the animation playback speed.
When clicking the ![menu button](main-module/menu-button.png) button to the right of the input box to switch to using curve editing, it indicates the frame vs. time change curve of the animation playing in one cycle. +| **StartFrame** | Specifies the frame at which the animation starts to play throughout the life of the particle system. +| **CycleCount** | The number of times the animation frame will be repeated during the particle lifetime. \ No newline at end of file diff --git a/versions/4.0/en/particle-system/trail-module.md b/versions/4.0/en/particle-system/trail-module.md new file mode 100644 index 0000000000..c0d4db7b6a --- /dev/null +++ b/versions/4.0/en/particle-system/trail-module.md @@ -0,0 +1,24 @@ +# Trail Module0 + +The trail module is used to add a trail effect to the tail of the particle to achieve a trailing effect similar to the one in the following figure. + +![trail](particle-system/trail.gif) + +## Properties + +![trail_module](particle-system/trail_module.png) + +| Properties | Description | +| ---|--- | +| **Mode** s| The way the particle system generates trailing, currently only supports **Particles**, which means that a trailing effect is formed on the motion track of each particle. | +| **LifeTime** | The lifetime of the generated trailing. | +| **MinParticleDistance** | The shortest distance that particles travel each time they generate a trailing node. +| **Space** | Choose to run trailing based on **World coordinate system** (World) or **Local coordinate system** (Local)
(**Custom** is currently not supported) | +| **TextureMode** | When a **TrialMaterial** texture is specified in the [renderer module](./renderer.md), the unwrapped form of the texture on the trail. Currently only **Stretch** is supported, which means to overlay the texture on the entire trail. | +| **WidthFromParticle** | Trail width follows particle size | +| **WidthRatio** | Trailing width. If **WidthFromParticle** is checked, the trailing width is the sample size multiplied by the ratio. | +| **ColorFromParticle** | Whether the trailing color follows the initial particle color | +| **ColorOverTrail** | The trail color changes with the length of the trail itself | +| **ColorOvertime** | trailing color changes over time | + +Click the ![menu button](main-module/menu-button.png) button to the right of the attribute input box, you can choose to edit the curve/gradient color of the attribute, please refer to [Particle Property Editor](./editor/index.md). diff --git a/versions/4.0/en/particle-system/velocity-module.md b/versions/4.0/en/particle-system/velocity-module.md new file mode 100644 index 0000000000..a95720da37 --- /dev/null +++ b/versions/4.0/en/particle-system/velocity-module.md @@ -0,0 +1,19 @@ +# Velocity Overtime Module + +The velocity module is used to control the velocity of the particles during their life cycle. + +![velocity_overtime](particle-system/velocity_overtime.gif) + +## Properties + +![velocity_module](particle-system/velocity_module.png) + +| Property | Description | +| :--- | :--- | +| **Space** | When selecting particles for speed calculation, it is based on **World coordinate system** (World) or **Local coordinate system** (Local)
(**Custom** is currently not supported) | +| **X** | Velocity component in the X-axis direction | +| **Y** | Velocity component in the Y-axis direction | +| **Z** | Velocity component in the Z-axis direction | +| **SpeedModifier** | Speed modifier, only supports CPU particles. It does not take effect when the **UseGPU** property is checked in [Particle Renderer](./renderer.md) | + +Some properties have a ![menu button](main-module/menu-button.png) button, you can click it to edit the curve of the property, please refer to [Curve Editor](./editor/curve-editor.md). diff --git a/versions/4.0/en/physics-2d/image/add-joint.png b/versions/4.0/en/physics-2d/image/add-joint.png new file mode 100644 index 0000000000..b3cd00fba5 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/add-joint.png differ diff --git a/versions/4.0/en/physics-2d/image/add-rigid.png b/versions/4.0/en/physics-2d/image/add-rigid.png new file mode 100644 index 0000000000..562c845407 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/add-rigid.png differ diff --git a/versions/4.0/en/physics-2d/image/adjust-points.png b/versions/4.0/en/physics-2d/image/adjust-points.png new file mode 100644 index 0000000000..f5645eb777 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/adjust-points.png differ diff --git a/versions/4.0/en/physics-2d/image/anatomy-aabbs.png b/versions/4.0/en/physics-2d/image/anatomy-aabbs.png new file mode 100644 index 0000000000..1a656405de Binary files /dev/null and b/versions/4.0/en/physics-2d/image/anatomy-aabbs.png differ diff --git a/versions/4.0/en/physics-2d/image/box-colllider-2d.png b/versions/4.0/en/physics-2d/image/box-colllider-2d.png new file mode 100644 index 0000000000..e20620140e Binary files /dev/null and b/versions/4.0/en/physics-2d/image/box-colllider-2d.png differ diff --git a/versions/4.0/en/physics-2d/image/btn-regenerate-points.png b/versions/4.0/en/physics-2d/image/btn-regenerate-points.png new file mode 100644 index 0000000000..47f7b0c8bc Binary files /dev/null and b/versions/4.0/en/physics-2d/image/btn-regenerate-points.png differ diff --git a/versions/4.0/en/physics-2d/image/circle-collider.png b/versions/4.0/en/physics-2d/image/circle-collider.png new file mode 100644 index 0000000000..eab06deffa Binary files /dev/null and b/versions/4.0/en/physics-2d/image/circle-collider.png differ diff --git a/versions/4.0/en/physics-2d/image/collider-inspector.png b/versions/4.0/en/physics-2d/image/collider-inspector.png new file mode 100644 index 0000000000..ab870d84c2 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/collider-inspector.png differ diff --git a/versions/4.0/en/physics-2d/image/collider-types.png b/versions/4.0/en/physics-2d/image/collider-types.png new file mode 100644 index 0000000000..24ffebe59d Binary files /dev/null and b/versions/4.0/en/physics-2d/image/collider-types.png differ diff --git a/versions/4.0/en/physics-2d/image/collision-callback-order-1.png b/versions/4.0/en/physics-2d/image/collision-callback-order-1.png new file mode 100644 index 0000000000..cd4f019e73 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/collision-callback-order-1.png differ diff --git a/versions/4.0/en/physics-2d/image/collision-callback-order-2.png b/versions/4.0/en/physics-2d/image/collision-callback-order-2.png new file mode 100644 index 0000000000..cba1ff11d7 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/collision-callback-order-2.png differ diff --git a/versions/4.0/en/physics-2d/image/collision-callback-order-3.png b/versions/4.0/en/physics-2d/image/collision-callback-order-3.png new file mode 100644 index 0000000000..858ad64384 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/collision-callback-order-3.png differ diff --git a/versions/4.0/en/physics-2d/image/collision-points-1.png b/versions/4.0/en/physics-2d/image/collision-points-1.png new file mode 100644 index 0000000000..95c7a84ec1 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/collision-points-1.png differ diff --git a/versions/4.0/en/physics-2d/image/collision-points-2.png b/versions/4.0/en/physics-2d/image/collision-points-2.png new file mode 100644 index 0000000000..b8f1c12d18 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/collision-points-2.png differ diff --git a/versions/4.0/en/physics-2d/image/collision-points-3.png b/versions/4.0/en/physics-2d/image/collision-points-3.png new file mode 100644 index 0000000000..9113acbb0e Binary files /dev/null and b/versions/4.0/en/physics-2d/image/collision-points-3.png differ diff --git a/versions/4.0/en/physics-2d/image/distance-joint-dis.gif b/versions/4.0/en/physics-2d/image/distance-joint-dis.gif new file mode 100644 index 0000000000..846d1fcd77 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/distance-joint-dis.gif differ diff --git a/versions/4.0/en/physics-2d/image/distance-joint-dis.png b/versions/4.0/en/physics-2d/image/distance-joint-dis.png new file mode 100644 index 0000000000..091dfabb40 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/distance-joint-dis.png differ diff --git a/versions/4.0/en/physics-2d/image/distance-joint-gizmo.png b/versions/4.0/en/physics-2d/image/distance-joint-gizmo.png new file mode 100644 index 0000000000..968603f3fb Binary files /dev/null and b/versions/4.0/en/physics-2d/image/distance-joint-gizmo.png differ diff --git a/versions/4.0/en/physics-2d/image/distance-joint.gif b/versions/4.0/en/physics-2d/image/distance-joint.gif new file mode 100644 index 0000000000..5a661b1385 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/distance-joint.gif differ diff --git a/versions/4.0/en/physics-2d/image/edit-box-alt.gif b/versions/4.0/en/physics-2d/image/edit-box-alt.gif new file mode 100644 index 0000000000..6aba5358ce Binary files /dev/null and b/versions/4.0/en/physics-2d/image/edit-box-alt.gif differ diff --git a/versions/4.0/en/physics-2d/image/edit-box-collider.png b/versions/4.0/en/physics-2d/image/edit-box-collider.png new file mode 100644 index 0000000000..5ad869fb74 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/edit-box-collider.png differ diff --git a/versions/4.0/en/physics-2d/image/edit-box.gif b/versions/4.0/en/physics-2d/image/edit-box.gif new file mode 100644 index 0000000000..ab775230bd Binary files /dev/null and b/versions/4.0/en/physics-2d/image/edit-box.gif differ diff --git a/versions/4.0/en/physics-2d/image/edit-circle-collider.png b/versions/4.0/en/physics-2d/image/edit-circle-collider.png new file mode 100644 index 0000000000..6f7cd990a7 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/edit-circle-collider.png differ diff --git a/versions/4.0/en/physics-2d/image/edit-circle.gif b/versions/4.0/en/physics-2d/image/edit-circle.gif new file mode 100644 index 0000000000..6e200bef28 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/edit-circle.gif differ diff --git a/versions/4.0/en/physics-2d/image/edit-input.gif b/versions/4.0/en/physics-2d/image/edit-input.gif new file mode 100644 index 0000000000..755630b318 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/edit-input.gif differ diff --git a/versions/4.0/en/physics-2d/image/edit-polygon-collider.png b/versions/4.0/en/physics-2d/image/edit-polygon-collider.png new file mode 100644 index 0000000000..1c7248d354 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/edit-polygon-collider.png differ diff --git a/versions/4.0/en/physics-2d/image/edit-polygon.gif b/versions/4.0/en/physics-2d/image/edit-polygon.gif new file mode 100644 index 0000000000..c4fcd8f563 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/edit-polygon.gif differ diff --git a/versions/4.0/en/physics-2d/image/editing.png b/versions/4.0/en/physics-2d/image/editing.png new file mode 100644 index 0000000000..eb5a9fe28b Binary files /dev/null and b/versions/4.0/en/physics-2d/image/editing.png differ diff --git a/versions/4.0/en/physics-2d/image/enable-contact.png b/versions/4.0/en/physics-2d/image/enable-contact.png new file mode 100644 index 0000000000..6cb8033f98 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/enable-contact.png differ diff --git a/versions/4.0/en/physics-2d/image/fix-rotation.png b/versions/4.0/en/physics-2d/image/fix-rotation.png new file mode 100644 index 0000000000..d65bb4cda4 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/fix-rotation.png differ diff --git a/versions/4.0/en/physics-2d/image/fixed-joint.gif b/versions/4.0/en/physics-2d/image/fixed-joint.gif new file mode 100644 index 0000000000..5158fef4ff Binary files /dev/null and b/versions/4.0/en/physics-2d/image/fixed-joint.gif differ diff --git a/versions/4.0/en/physics-2d/image/fixed-joint.png b/versions/4.0/en/physics-2d/image/fixed-joint.png new file mode 100644 index 0000000000..3144d1cddb Binary files /dev/null and b/versions/4.0/en/physics-2d/image/fixed-joint.png differ diff --git a/versions/4.0/en/physics-2d/image/gizmo-a.png b/versions/4.0/en/physics-2d/image/gizmo-a.png new file mode 100644 index 0000000000..a8fb81bf3a Binary files /dev/null and b/versions/4.0/en/physics-2d/image/gizmo-a.png differ diff --git a/versions/4.0/en/physics-2d/image/gizmo-b.png b/versions/4.0/en/physics-2d/image/gizmo-b.png new file mode 100644 index 0000000000..3e55f21dcf Binary files /dev/null and b/versions/4.0/en/physics-2d/image/gizmo-b.png differ diff --git a/versions/4.0/en/physics-2d/image/gizmo.png b/versions/4.0/en/physics-2d/image/gizmo.png new file mode 100644 index 0000000000..ee4612373a Binary files /dev/null and b/versions/4.0/en/physics-2d/image/gizmo.png differ diff --git a/versions/4.0/en/physics-2d/image/hinge-joint-motor.gif b/versions/4.0/en/physics-2d/image/hinge-joint-motor.gif new file mode 100644 index 0000000000..5297cf2bb1 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/hinge-joint-motor.gif differ diff --git a/versions/4.0/en/physics-2d/image/hinge-joint.gif b/versions/4.0/en/physics-2d/image/hinge-joint.gif new file mode 100644 index 0000000000..f66ad60ce7 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/hinge-joint.gif differ diff --git a/versions/4.0/en/physics-2d/image/hinge-joint.png b/versions/4.0/en/physics-2d/image/hinge-joint.png new file mode 100644 index 0000000000..a75659525f Binary files /dev/null and b/versions/4.0/en/physics-2d/image/hinge-joint.png differ diff --git a/versions/4.0/en/physics-2d/image/joint-with-body-collider.png b/versions/4.0/en/physics-2d/image/joint-with-body-collider.png new file mode 100644 index 0000000000..a3003be1b7 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/joint-with-body-collider.png differ diff --git a/versions/4.0/en/physics-2d/image/module.png b/versions/4.0/en/physics-2d/image/module.png new file mode 100644 index 0000000000..9213ee016e Binary files /dev/null and b/versions/4.0/en/physics-2d/image/module.png differ diff --git a/versions/4.0/en/physics-2d/image/motor-speed.png b/versions/4.0/en/physics-2d/image/motor-speed.png new file mode 100644 index 0000000000..97af421619 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/motor-speed.png differ diff --git a/versions/4.0/en/physics-2d/image/polygon-add-point.gif b/versions/4.0/en/physics-2d/image/polygon-add-point.gif new file mode 100644 index 0000000000..a167cbf959 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/polygon-add-point.gif differ diff --git a/versions/4.0/en/physics-2d/image/polygon-collider.png b/versions/4.0/en/physics-2d/image/polygon-collider.png new file mode 100644 index 0000000000..921f057495 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/polygon-collider.png differ diff --git a/versions/4.0/en/physics-2d/image/polygon-default.png b/versions/4.0/en/physics-2d/image/polygon-default.png new file mode 100644 index 0000000000..bb13ce0aaf Binary files /dev/null and b/versions/4.0/en/physics-2d/image/polygon-default.png differ diff --git a/versions/4.0/en/physics-2d/image/raycasting-output.ai b/versions/4.0/en/physics-2d/image/raycasting-output.ai new file mode 100644 index 0000000000..9a89e8d5b4 --- /dev/null +++ b/versions/4.0/en/physics-2d/image/raycasting-output.ai @@ -0,0 +1,6376 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[6 0 R 7 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + raycasting-output + + + 2017-05-11T17:14:31+08:00 + 2017-05-11T17:14:31+08:00 + 2017-05-11T17:14:31+08:00 + Adobe Illustrator CC 2015 (Macintosh) + + + + 256 + 108 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAbAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FVskkccbSSMEjQFndjQAAVJJxVicfnbVru3iudP0dfQmUSRm9uf q7lGAKkpHFcUJB6H6c5XWe2GjwTMPVKUTR4R1HvIcmGkmRa2XXvNsqgxCws27hkmux94e0/V/Zqs 3t7iH93ikfeRH7uJtGiPUqMl15onH7zWPq5PeztoUH0fWBdZr83t7mP0Y4x95Mv+JZjRDqVJ7W+m Wl3q1/PXqVm+rHrX/j0Fv/n9Oa7N7aa+fIxh7o/8VbYNJAKDaDpci8bmN71T1F7NLd1+f1h5PDNd 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 AP/Z + + + + uuid:91432569-e28b-5847-9173-00da6ce9256d + xmp.did:9c6f38b4-6510-4b2b-9f49-88e1e65bada3 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + uuid:ac95c21a-360a-4ecf-b50d-18deafd85636 + xmp.did:3c521b3c-8e17-6442-b5cb-0001c5ab3e1b + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:9c6f38b4-6510-4b2b-9f49-88e1e65bada3 + 2017-05-11T17:14:26+08:00 + Adobe Illustrator CC 2015 (Macintosh) + / + + + + Document + Print + False + False + 1 + + 960.000000 + 640.000000 + Pixels + + + + + STHeitiSC-Light + 黑体-简 + 细体 + TrueType + 10.0d5e1 + False + STHeiti Light.ttc + + + + + + Cyan + Magenta + Yellow + Black + + + + + + 默认色板组 + 0 + + + + 白色 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 0.000000 + + + 黑色 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 100.000000 + + + CMYK 红 + CMYK + PROCESS + 0.000000 + 100.000000 + 100.000000 + 0.000000 + + + CMYK 黄 + CMYK + PROCESS + 0.000000 + 0.000000 + 100.000000 + 0.000000 + + + CMYK 绿 + CMYK + PROCESS + 100.000000 + 0.000000 + 100.000000 + 0.000000 + + + CMYK 青 + CMYK + PROCESS + 100.000000 + 0.000000 + 0.000000 + 0.000000 + + + CMYK 蓝 + CMYK + PROCESS + 100.000000 + 100.000000 + 0.000000 + 0.000000 + + + CMYK 洋红 + CMYK + PROCESS + 0.000000 + 100.000000 + 0.000000 + 0.000000 + + + C=15 M=100 Y=90 K=10 + CMYK + PROCESS + 15.000000 + 100.000000 + 90.000000 + 10.000000 + + + C=0 M=90 Y=85 K=0 + CMYK + PROCESS + 0.000000 + 90.000000 + 85.000000 + 0.000000 + + + C=0 M=80 Y=95 K=0 + CMYK + PROCESS + 0.000000 + 80.000000 + 95.000000 + 0.000000 + + + C=0 M=50 Y=100 K=0 + CMYK + PROCESS + 0.000000 + 50.000000 + 100.000000 + 0.000000 + + + C=0 M=35 Y=85 K=0 + CMYK + PROCESS + 0.000000 + 35.000000 + 85.000000 + 0.000000 + + + C=5 M=0 Y=90 K=0 + CMYK + PROCESS + 5.000000 + 0.000000 + 90.000000 + 0.000000 + + + C=20 M=0 Y=100 K=0 + CMYK + PROCESS + 20.000000 + 0.000000 + 100.000000 + 0.000000 + + + C=50 M=0 Y=100 K=0 + CMYK + PROCESS + 50.000000 + 0.000000 + 100.000000 + 0.000000 + + + C=75 M=0 Y=100 K=0 + CMYK + PROCESS + 75.000000 + 0.000000 + 100.000000 + 0.000000 + + + C=85 M=10 Y=100 K=10 + CMYK + PROCESS + 85.000000 + 10.000000 + 100.000000 + 10.000000 + + + C=90 M=30 Y=95 K=30 + CMYK + PROCESS + 90.000000 + 30.000000 + 95.000000 + 30.000000 + + + C=75 M=0 Y=75 K=0 + CMYK + PROCESS + 75.000000 + 0.000000 + 75.000000 + 0.000000 + + + C=80 M=10 Y=45 K=0 + CMYK + PROCESS + 80.000000 + 10.000000 + 45.000000 + 0.000000 + + + C=70 M=15 Y=0 K=0 + CMYK + PROCESS + 70.000000 + 15.000000 + 0.000000 + 0.000000 + + + C=85 M=50 Y=0 K=0 + CMYK + PROCESS + 85.000000 + 50.000000 + 0.000000 + 0.000000 + + + C=100 M=95 Y=5 K=0 + CMYK + PROCESS + 100.000000 + 95.000000 + 5.000000 + 0.000000 + + + C=100 M=100 Y=25 K=25 + CMYK + PROCESS + 100.000000 + 100.000000 + 25.000000 + 25.000000 + + + C=75 M=100 Y=0 K=0 + CMYK + PROCESS + 75.000000 + 100.000000 + 0.000000 + 0.000000 + + + C=50 M=100 Y=0 K=0 + CMYK + PROCESS + 50.000000 + 100.000000 + 0.000000 + 0.000000 + + + C=35 M=100 Y=35 K=10 + CMYK + PROCESS + 35.000000 + 100.000000 + 35.000000 + 10.000000 + + + C=10 M=100 Y=50 K=0 + CMYK + PROCESS + 10.000000 + 100.000000 + 50.000000 + 0.000000 + + + C=0 M=95 Y=20 K=0 + CMYK + PROCESS + 0.000000 + 95.000000 + 20.000000 + 0.000000 + + + C=25 M=25 Y=40 K=0 + CMYK + PROCESS + 25.000000 + 25.000000 + 40.000000 + 0.000000 + + + C=40 M=45 Y=50 K=5 + CMYK + PROCESS + 40.000000 + 45.000000 + 50.000000 + 5.000000 + + + C=50 M=50 Y=60 K=25 + CMYK + PROCESS + 50.000000 + 50.000000 + 60.000000 + 25.000000 + + + C=55 M=60 Y=65 K=40 + CMYK + PROCESS + 55.000000 + 60.000000 + 65.000000 + 40.000000 + + + C=25 M=40 Y=65 K=0 + CMYK + PROCESS + 25.000000 + 40.000000 + 65.000000 + 0.000000 + + + C=30 M=50 Y=75 K=10 + CMYK + PROCESS + 30.000000 + 50.000000 + 75.000000 + 10.000000 + + + C=35 M=60 Y=80 K=25 + CMYK + PROCESS + 35.000000 + 60.000000 + 80.000000 + 25.000000 + + + C=40 M=65 Y=90 K=35 + CMYK + PROCESS + 40.000000 + 65.000000 + 90.000000 + 35.000000 + + + C=40 M=70 Y=100 K=50 + CMYK + PROCESS + 40.000000 + 70.000000 + 100.000000 + 50.000000 + + + C=50 M=70 Y=80 K=70 + CMYK + PROCESS + 50.000000 + 70.000000 + 80.000000 + 70.000000 + + + + + + 灰色 + 1 + + + + C=0 M=0 Y=0 K=100 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 100.000000 + + + C=0 M=0 Y=0 K=90 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 89.999400 + + + C=0 M=0 Y=0 K=80 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 79.998800 + + + C=0 M=0 Y=0 K=70 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 69.999700 + + + C=0 M=0 Y=0 K=60 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 59.999100 + + + C=0 M=0 Y=0 K=50 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 50.000000 + + + C=0 M=0 Y=0 K=40 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 39.999400 + + + C=0 M=0 Y=0 K=30 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 29.998800 + + + C=0 M=0 Y=0 K=20 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 19.999700 + + + C=0 M=0 Y=0 K=10 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 9.999100 + + + C=0 M=0 Y=0 K=5 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 4.998800 + + + + + + 明亮 + 1 + + + + C=0 M=100 Y=100 K=0 + CMYK + PROCESS + 0.000000 + 100.000000 + 100.000000 + 0.000000 + + + C=0 M=75 Y=100 K=0 + CMYK + PROCESS + 0.000000 + 75.000000 + 100.000000 + 0.000000 + + + C=0 M=10 Y=95 K=0 + CMYK + PROCESS + 0.000000 + 10.000000 + 95.000000 + 0.000000 + + + C=85 M=10 Y=100 K=0 + CMYK + PROCESS + 85.000000 + 10.000000 + 100.000000 + 0.000000 + + + C=100 M=90 Y=0 K=0 + CMYK + PROCESS + 100.000000 + 90.000000 + 0.000000 + 0.000000 + + + C=60 M=90 Y=0 K=0 + CMYK + PROCESS + 60.000000 + 90.000000 + 0.003100 + 0.003100 + + + + + + + Adobe PDF library 15.00 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 9 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 960.0 640.0]/Type/Page>> endobj 10 0 obj <>stream +HTn@Wx3;3J)U,q@4- gƻvL Pw͛c^֗=UE؝NGz?_}nR1'g mg0<L90"!4Q|\Q0EBm +KSc 5Iܔz_UFkb*,129&p?bT£*|NLgfBÚ=\l\CnZ-}Aup+:}a%_ +Uֳ_h&kIyBH(LKD`]&0@8OZpc*>:\\pMCI?TZ@4gFoN'>&bsaU-"Bb"q +[uR/&'~> endstream endobj 15 0 obj [/Indexed/DeviceRGB 255 16 0 R] endobj 16 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 6 0 obj <> endobj 7 0 obj <> endobj 19 0 obj [/View/Design] endobj 20 0 obj <>>> endobj 17 0 obj [/View/Design] endobj 18 0 obj <>>> endobj 5 0 obj <> endobj 21 0 obj [23 0 R] endobj 22 0 obj <>stream +H\j0ໞb!x>tNƩ*CI3`_*M|AlS)7M8φ)p9(M#6ʢ(8Q +|Cإߍ]G0W -A COPW qI +m JJ!aAGh !Ci d#9!]ė^h舼YRN㍘Z>+Z@ endstream endobj 23 0 obj <> endobj 24 0 obj <> endobj 25 0 obj <> endobj 26 0 obj <>stream +Hj`_`N endstream endobj 27 0 obj <>stream +HWkP>-eeEA "Adw#J] +^ +-:EemNMюMbB23Ldұhd줙vbf} qN'?:Ξ I%R>@ڷ7)SԮ={ſ!R-"04EكC%;׼=H*ѥ$k11'oǷَtoOgdO!.c6/q{ddgϺ|lCy, l-n<5bfJJXܮ((a+j8YနFm-5+X+[Rx6/dS2'('y#u!-, '+h0JkbW53 IsO3֫_ r%̃Uح#.1Boz \?|aTJ5{͜eCWƆ8:aw8%DXie6S8j#h`NGR!EsMYӫ?>uHp̣[™3~t}/6yAMȵv + m*tԴ(:ڮ%L]]4x|! h ]̜-{{}>$i=[YY8}T櫖clr:%J*A݈f|S#ˁϕ^Q`H$D +E*Yhcʥ.BNRe)h̍JLJfd!f (w3H +EFս+Zt#Չ[LĊ^^߼TE?f5>{Ui۸'O)B1' V*e2cS8`0;[\۰}Wo-{ڕm=vvm*L\:l1'OF\A;O?Ѕ}WPgz}x }ƹh+jD5-&ɨay׏ zٽ_췁##E|W/,h HQrJUݒEռnK,8MkE|6J)?MkZv]|ފ +`d_ r\R@ }A8wRy*3ݣM*cwP/S)N4`6EWdw~-a4?[{ ZAؼD.~c_&7/p2ᧁxM +/ .RX'A\9Zg;yyT$OY7&/eF*`[ht@^Zy.0si0_"x?)KgCױ~(j`Ppޤh5|ED¶T y䀤KK(S x9E3t܉-pa?o7uaٓl])QHHI]eɖc;TS Rh)UDQf߮ +]wsf sΜ9vdsO%/-ؿAv9IV/PL.>7d{Ss?N KzNӯ>Gs$yY/GXm&,ehanHxa|q4-[pZ:=|{`.e}M~$n ?ȳj\]=T),E>§}Z5q1 +wuȞ[~A  x^߄NJ_ K!;՜"~Mc2p߰ȝk.G2\6ks!|Y:WSW kX<:ʬq{׎§qw-a܏qo`cJљEZԢ:ʵ1_M7,zzǻid>}dܴ y/}޹~A,u6 "|meߝ?c✔6bo 2΅m8v>b>6k^G(<{>|of|+-Z"ӻ<ϳF=T}Uusu4Ym|ܺo=dΐt_o[eӵu⻊sg6߳:~$'}`(uZ>Y{JbZ>q^[3ιg&|hg}617iC;r[[e3SW9P<-k uw]D,fA"c [k'-fRfS'̰V5X/;EWݡue\/-jU\ 1qS~W,f vjR.=s}\<5~}U5lX"Cu{o[ Xyw=0-W@bܵmtteX鮧>>b1X>x\gcKuabX¯̧=R5>?o׳,f k˺;ʥ,md8'6ރKͥr`Qy>yjs>kf+d`Qw]T5IdX/vPCMg_3i۴16_xY֏#xaw~,e&sw]UV]hf6:'pa6O7- +r&{߯{8@a^g;Cj-;U>:aG}6KAXw&6̱n}{w-^'ż켎ZضENbd-i"~WIag=?'iqgp[-ݵM-fPS _]͢uࢊl5wli/w[Q?(Ww ](U4whphqn2'vV//Yw귺S,%Ւp9~n5'󸟥C^16\*(} _fSިz}jqorʡ,߱'ί̛94~C- + &{e1\Ǭoy\Xۼeћ_رe1ˮ(\γ_=Rػ10uY!-k$<֬~YԨEm[w4Ϛw,1|m#9bqG\fB\A^-mgs)[.Yys7"6 [e4 V3~k1~MbQ&SXycߕcKw%^{f:]qEJ٢_xZotT טk o^QREM5*v-`;;gB>2F+|XZRM]|gEj .:.Z ޶s]wbX}A>x>za=lبyBf}=ĮyWnX%-"PZ ųzCt7|njZԼ]F3e͕,'/> ,zfߩ,^p']xrU]O٢{Vբg^MVO+ @;m-aU_aL ;5:mQ4K8v^_˭ +贱9 BH !HY1ƍNq'mLp^w[w{>^w5!166 sniwsrQAw%'u=Krwmkwޖ?==aƢYq)uɓg"Gf䢿SE%=:%';D6Ⱦ%d~נ׼(oxۈ70Vȕy#D %O}|]2_|?Q\W.cM@|-cT6|wkb|&ryªb +Oo:1D +}"?S{u;XUE֤6_뮶*Mo+{WcZ_R7igYWl0u ۯZsy +.gEv&}~ ȧ1922Ze8Ʒ +s):ߠE8zyܱ)v=owA9Sym]O&:|7>1F^}09>tйZ(^e_YϪW3 %75]>~E~\#_QK5.x֔=3ܴ"~k`g{ x+{т"KZǶ&v*uRimy)9yD<.8&ʖa\ù*q<2; Ʃ>{w]Wt ?">E8ﻵT5% ?%fI_{%6oG߿Tou5X9tgx{ÎOs yus?cKۼq){"q>Ǻ Xl|.dw}_#.vQY |\:j{徴eVȕo?T\nme3]+Wi }k#y!W0%g򣆢{N\֋YwQQQ{rO # + Ƅy享^gYc{rQ_7-mggjcl;<@9-wy_>o!)yJwDN/Y:ls#匱WĽ&]qʹEnw]9b͐3Ŗ2a˹Z7@9w6y/?+l4]UΝrg Nqc?~4W!%xߵx+XGO1P1A|f#y=˺uLP9^WBK=BykiC%{)aB[rt@B ωr1+iSs9g} Y%7s󽩬Ǘ}\yHE[J,X)bz-`NΈ@]OW\# ??TbJu|L#vSO&8 fWJL.ܳB׎{JWa|ߛ8^?r|*gacV\[ b1M<申:!O7x>c1WO=ts^8ǽ*l_PSܚgEm?g_ۛ9F5Omârn"8fml9D1+dZ1o߇["cϗ}II/(Ĉc&+m-xV

Ul 1G\ICɵq[^({T_qJy v?euŷm:1d %ERDQ%QyiIL.hn]Ձ~Υ|3 ]==yV7_ӽ;GjwC~9"!J'x;<綐m,:F?)ga 6y ?R0+[e[_'UΑDYx^V|.(jqtE'=[4R浱wwy90Ce_ `ȘyE[=ttm1[KYe48obCXme^8V"r쯖GǮ| +-+gkuu4 +:ωr:V4.>z)mqݽqscq<=/zʞXbFg5yuvOCQUe4U71^eEl\՞ΉU@+5ǽ~ggLWz>YWSenx)w:"7ڍ|r_Q*9J]J쾮J0PJ<vOUOZ^}e^p/dkp^&8î 75?繈GJ1K~RʞζrOV +63d։gVE--xn/tWʺ%SΘgRqgۖ8{sh=C9?*мKų1w}}%D2~ϹYx¹%k)K8x?†!Z@]].Tb1(PO9ks{0u:7Uۺx9FC7ػYm+y rXϐ\c{ƿPC10&:辉.| O} 9/u@>8n?߸MƳ5x9zQW?79nbsxkIJiz.{ ]z\ػn>⃧a r z3;p!<7 +28G2&2q(qONÞAJ<w;PW9kyPJ`ҿk 糡WE_bPbKe.)kOL"[t^.rvAmcׯy2etysvmgs?Q Yy|?/8Saw}׿s$zY֊g}εĽ&6z/w= e{;QŲvy| 7ܙw3s}gYCcy|6XV֭=ZFhL~Qml^z~5Ŧ}ljr;n V5Aw1<997:V5g\G 79V'vOFfw_%]q]ٿ:>u:V ++ +z 礈{s=߼a2qgh;D|u)qA ϐ5wFzM X\7\cʞqNfrms[za&E~Ah(@Gcd?cbྼqs5nM% Fw}o[ʚ.>YEGπGk}]7?,x5iz_- Zjc>K&Pwݾr .[Gu\uot)!vA*:>1t/qI#l`{ĹEBgf +_n*ww"fׅ}o+|pܹ<y~!cHaq޵W8xbg}^Ct`'Jq=)lWbH9qw;loW9k\9ypsYΉ#;}B3֑u/1e=(vUSMm7v~Q7p/)P:x ΡyvK9_Yils5|;4>i.r\\@%#zS榱>~+q{Ro!/zIЩ&N;ö?,!)qr[ ȺP֗Zqn ~a]#ͳӸ^p:=B_ +;{LzOc}|)>!X5.8X<-zl` {]Ů<^dG },*󲋝5|s=ϜłFĨ/Aw~9j,Y܉J U|8_U!OOSl+Q:Qy ]^p3#l Ĝ]Bwk%nm}M;s-@1E|;DZ{>dyw叕{;B˭ѫ +B0$>N۱;vdL5Rڊ +B-1Ky^֞X{:wC=@:|+P7BȁW=UЧ>אo#Ϡo<2oϦ-|n}|O97v)?U +|`:YÝw)\r]za"wVxХǸցFcg㲮԰uLsODS9QQߟC %hohu}ј%ߏw ͑ dOĻBb97=cgNKE/. +ʽQPuFlGJ:GO.T}+*{}\ş}С; Ϟ#=.LN6[T{AwP|99'=%N(O8ozƗkc%Ƚ^X;qw{1Ο~Y62w*ʹ9zp>\#]K93ו0{G?y8ƦB粇5y;?ʞ6Л}i>a/%xnJǶN6 %[߹2=;Q1C#'2s@ \%!p0$]?@ιu{ |]FW='-O!]'a\Nhl_d;7~6uyy*{C/8s[Wccp?=Vseǿ;맺[J 2y<6qv*T ?9[фrO#utʝ{8GEvaw[E:gY>zu r/r/t}λȁo<.'? e>̋8; 9EoCG\Z%2wmz߯?lYoSmPf)}>.}=bCdw|t/o|"6?={SDžͮWxd l9P}%frCzNYg}rb–z*ۅuO^Sb nu=bwY[[y-#O~UA~3޵} +kxB"FH9O7s{eߴ~a9r;<;g}cxL v~GNUW<{J0TΔƤ#)?V%ζ[+dSk뮏ƨ'YB.u==\A=NaM8}KbL\QgFَeARkEN3T9(rE=xU3">Dϱ2W:ʼ.O_ב=W 6sĠiW8\#|Us[(zT:-dtm??seޗx9%^^9ryG96S1%=<[r-t;$vӵ7%=Ɨ7woC!r~}O\({" B^oeB`sw=8nϹ_y}ϏzClO)3}*D=| +rw05gDqVCo\@*Oˎm|H#dqL9SE{>2UϹƎP%pݻ_z2Gg +>7hGq-1>6x0^l=uxVފצSSd~s`ާ7|UwٶK%Ứ5(UPYJ,">oyČ"&5v=:Tւvq[<xgrscd!ޝ#׵ys'|9*zlSh7y=XYqvʯgiN{xg[Y>)^ s ÿK`r-öǜNk{nwDO@}s_]ue޿xM_nYhw cSδҵ`[p +ƫ3eo>gqwQ |_Sb.7JLv -w*k)2K*pt$/nVI$Ui(UJԃz c@`/`e#2<'4UꞾٜ/c!4j379~w#k/uO豊_Æf3ϼb-I̙Mb;gQ)vA> =7Q䙹rOxvKY/9wv5~p8 +أ;ٴQvL}˹zxGF&s~:&ytN> 73)"jXꟼȻ;{c *wcyo&xߍZIQ8WQޞ7cb{|_mٟFQ(<҉ړXen]# +oz~(Lg _ZVyxNG +QZoist*jvLnW_/w=u7=)e<+yf'}^nk ^Mw6_4]|6E,wwoM6?,1ZDqw{[ +w<9S s3p)gR}d[}R(iƯLT͈ o]tDq~ڽU6||Ũ~ ؅9#x܍){X6z̠_+g5FϾS|/9v̥Q;$^zWwgI#chs֦uۍŸꞓ^&(Ç|÷|_::r'qj6 eCGaFo9c%$'-;~& Sn/ +K|}ML|)r&jl]g[(gὨysKI7 +(իw-^^ Q^s!}|:a9#ao[CGg1i>𥾘RQ6ϡϐ5sǚ~Y/wbWrwt_D=tquoKut}O\`aIbmo7Yd- ^M-ޕ9,.Da3C5wDaꭨ2؅zbu&nb2EՀiapgo˻Q9ߦu^3NTڈW']u5pe{9ߥ ߜGٿ5ā}Ð;wpv3kQu疐N|KY1~T cME:-Eb+:g^{O{Xgo} }Aq} dXQX=0n-j;s6;vѤ@ypoQxa> +gr\SdK0ӜذeDaR̷qDեoMFչ{Tsn/<[ffz=jlN~Akͳ8\5HrUkc=jcMV7jӧ݊:d92/]|K2?wϠC/'Ga1{csk1*+w§Q]?GQ=U7x=Dm? +';kSэu|&.0:~2Jٙo@gן>>wlE2"ϼى®{ ?e.ߝ-è> V>ױuG5K=흆OS>~lww<Z%ֲwFQuijj߷O?NN; V_T{5lϬg.wOUSw0' }{2v2ܱ~px=G,3ĶEE {܍ߏlEQsFݮ0#(AT^1k y~TX l< +k?E xm(H;O~SQyemN̙+CgND>kv4kj{/eT-C|.>wOy̻ 2.sUdgqڔ5H?YKÛ;?|θݝţ1>GK`x34*7(L}Cwr;\1rO= t>@osM5FGQ}-b&^ˉ.:/NmhYNyGX!1+@/{?x#Cu wNG'v +Nok +eu:w.{WO>=p<7sx6z=gEMx|&ZXQH5-fLӬ۵d_[1XOSx3^NwS,9Pbr5w99żvN_&n;YI_[/3O+|&l6o5`ϻ'ܕ mGf}t>3ɮ+K8.9sfBi;!< :;g1nq_O!Α_<2rn ɺ}p|t./}?8OqUX*G6v}o n݃>1x-fUcBn ]lodp_z6wSE(G>WחOg?͗9c}ɾ7o2'|r +t8SnStu +G}oǼ-X֙Eu}\ sxT9fmiz/#E/w + >Yuâp 'I{inZ__՘S~iWcVmٝR~qlɦw8%z~dYԪ]< +/#t-j'ȹiQS#ClY#O{{=ݳkzufWitS/9dE/z~fQs#?9L<-z-rKF y[C5￰'s'{Aҫ}c;KGL}dwϓw>N \C//KV(,z%Wոtr&:9AC8lrd'>;-kmb6Fwz l#zXwr~vl1{ʷf>yJ9LYb6S-݄l>٩ES_vhO,ȢFcud\IE}zD RYX3 m^{w9w'1/^eߎ]㉅[[L8TՇV oz&w=C2,$.W;xܧ N-rrk8zaSpגoRMt߸/)O;)Lhg3ciיrEkO9E9\9 Cl\HݦENY/y[J ,Q/!o[䉖ET6Eqw~ՍdsV9@o?Ƚ'{@MՋ+XHv547 cеScŞpIyƻ&q'~ˊ0V=:h-c'ޝ;cgu=403/ !$ Rk%?y(sԩ嫯?gH>- +I#sIa+'{Ss|z} F:_:kpE>&?3 ^D=c|w=iq:k7,8mGKopﲿ.=5JZ#|>QӺIk69Gݢ/|u{"nO,#RY~e]I˺<; lْmiu_k,IU\dB^:>N3xD~Z)z|2^lpylYm<'u4K9O|N3GqM=s\нoށ%̗ l==~ׂGԲ[X s"\snϢW`2;ə-rme7pu]oVm (Y4 +>ZGɱVёm&ck,JZ+(I+Tva% N$b.8~I#mWd뻗O% >!q?;ʂ_̂kTB$#B˯.^?p;k<_d yCv/,5 ]Թsm^zJ]bwZ?虜~,'KQN,ԷWBRq[׺܂-|[/&s'3zbF|~=Z>N`]I7V"w:Ẁ2W#Z;=˶b׊/</Wt nwl\u9yoyF|)_B{ȻY?1NG҉ψ'Al'NR "םǺύ_ʟ{9H3x;&r 3i[8bݙK\e Ƣ&;-4v׊ܵH~lJ\{.} J,?s?2_#'?iHn:zKɵ Yk#ZJ̱-fE-51fvBz>|yhЂR|#9$cl/kŚ|Nk6VkߪdYYplhm"E_+,I]v`܂SKIqjdm1Sv-xDYwv?Kck]/ſw .qk_Ɇ,,f(+xXێ,֩0Yc ;gRbu߾m\v=p޸nf[_q{&,r@KQ !fiyY}j[9ڱW{Sū=ji]j^B i-亼4J=({ՑIE: + _j7G`|E }n6kD8ϸBw&ߥ{8Jng+WZܱcy(? +}wȦ"''zf6k/km|F'߇y<}o|=CX1Su0K߃Wn3NǢ7,pٔ ݫgGrlYlJ*[eql"3ёß] |+_tStN=Y܅ls|K[AվАmm}ocN,^XVբKlO0[注x>سG|gd9Oej7M^p7ӽehZ^[|?5SmstG|iZR$qu)Y1i:1]qE>W"=Hߩ# g3ZwMM/-Ǎ|tݎMOS 2 gKyW~y_%ߕ5]Po`.yjͤ=w|fܳb%>_77k=H,c3bVLUKۜѰyapvS3{o{80+PֶS(:Gn,l~c. =z97jzG5w7p&[W><|ǒEVRlzlZ`aE{8I=Ժn97xk+kTz]3Q#}:b#roeXvDpOͳЈЄM׈vwUw{co[o 7w}ys??#=mZ!nS򊜩YBEN,p:xKnyGI?Pgx0?l|k1s*s\+:(7DY=";o%t-z(]tgQgE'ɯ},XNWqs\b^,yj[<Š&_~y[뚲ΕtW%I-}iK~V_U-旺[H˂KRE0s^{̆Y.jK&]i9֮[6\3KfvW,pc@w8E0$Pw3#ҧ1~o7cua,fVꭕs .jy-꺟}aߥE[NZ(ܤεR'1[*=R?Mԕ |/R Ao51}u%^u L=jݝb56 bn.ٹP &~ejk;G)0TpsZ6F~58II{='c#Ft/"XP~RR> ,/=}g1;ÆƎ,ͮ9ws\z``6 z8bA0wEYp:XB_k-jүIqnk[ϙ׉sKy}=T^fxZ$o-][ <{3Z׺gc _n,7Fou^{Qk?sߒq+kXF6"J|ݮGE}_RؽM񽗜~^db6OqzԻ|oAg nyj{Me!#Փ_%?+ͭt|l?i}K6X=gYvӏt֍E.foVwzHlX'6]mտ|7ua/u޳ͫtxE#1Wʂ+=j' `X}fͦE]Zd{A -6-8eɎ̜90`'_0{AO/Xm3M%m.Y*KH{T,fOfE+깡4 ++k 49 m%z`[ L,fb޶Z=m%'e33aۂϔ"QUL+ҧ{Q#YwߢZoa3wj5U>ul?M,8Es.kn;eI'rHoAkgVM{AzO㫥G\[bN"m lx ,f;>pשbGd`rXz{k{O ؝b~%nojd!iaЅTԝ\׹d7-0U Q QC+>+'9̍|x +&2u,꧛r粷3ym=&{ثY5,r$zƾE~ 䓎V=J&X4ӹe6NKb_:Wu0FZNW~ȹdO~C4$s!+]3igg }hd[U }ksTz/:Wt?8ӱۘ!$rHv <$"OH1OXtSִAߤЃ{ӚGE_R)הkz3sߪ$c E}^.0\䮜 -sݷT `܉E:)>Kҏ,8;lxB.?~Z+R仏``xcrm"NOlu?.jwM Fu/Nv_/l_sZQgۭɢN%z/pI~)ka_pv.gq|;OW; Gk;̶g}ܾ~H1~| ZIUodgo5՝1uuclIscY,A-ɖ,y(.]UE7A6lذeo /2%z!y8x̬뇢Oe 'Y4g~nY˽3l`%6:mW۶\0S\:S9+cj<C4RZ۟ tVO˻t̻Vp3ozǰώ C͑WϹu|,k;+~Sߥ^uwXfDZ(1|=isVʽarc|s9w sͷR9;7&G'39<;Q$-H#-:g'J |1ezT<ߙr7&n[V#$z\7ʾ;sܛOmӭy~!w. ce Y 3S򁲖; CN:MoMUCbN-em9ukw:ͭWĬ3`Kro̪ce-X#o\E1#% +y_Q,0K> b3ɃyXmS3[31q/lq|m^(gڹ->&Eֽ+t_XFSMe}͟}ǝ }Q9}=PY##DYWZ} .;fE*s:D~;jܠ/"ܿE#*c?rj{J\sayJBu:7z^~Ic-kl :ϩ'InoOLGJ:'y*MGYO;;A39N; X?VsĜ*29C3#LwRpΞr~QWGCor?# sW}yqߎm+(zo;w۱{lw +LFʾ2er}A|^Wtėk֯eY˼%x\꾯]RmZf6 +۵<>#%nڞrWeossn6]:R9~sQW&ֹCeo?W%|nge]'ȓmyٳnx̿mi=:~G}){ۍ'aB'Ză`LU%&ZNG[`N2R aC7`k.q! +dfוy{m^){2=sU1̚i@b +>C|௕_CߑoQIBR|p <3ȏ]%پ2u<ȯ w~w45Rκ_(1x~a=׍֋ܡ2XQbްai,<֕}(|ό^ о~Ŷ2m67?z(Ϲayw|tL C7JB^3Q0.ٲ֝\Gx<& r :ږ}O`wc-ΝLM;zs۷f~`7=޽oa}y#7;J41 7$?rv=}xnU[K>@/f]d[ٯ2oا"7)gc=g>zRl(w9ݐTqVlgfD0w3, yQWolпrb{ȝf@zG}?o?cyAȨvwŇkz"!:;ݩð!sl*e<cagSx2ˮyr踅Q/+Բy3XyĘh؄H7Л>2UXd|]1ze^fya19k9*8ߓbe?辮0v'ZqEgj:SkeĹ17w8*vCM%a&3؛3G}:mǜf>~%f%fP9WוDo?S(@u 1Z7჊*[Q//l9]9(fc=󙖲 g}Y+*1 2q2[8WbS`{m_֕=huGZ5Lhl?G5Vs;+ki3>ϗqO?/~8}WɿcLw:0 c[h-)N;).(Y-ɖ!v4E 4hk5`D y; +{O!_,{d=$V_^9Fܥ}fU7NcFy/=,r^7\X}|g1G&)>$wlQw_KO~,;%{ ,ݵ䓾d"{Y܈׃?  \NfzO-u7zFO/CDk K~3nu;?H6vOGѧCrƉ݋>o<У8y\f[m]I`&3K*z3seķ P=;@gJgkbg;c ̕1zA&U .WhƭoB^$ZJ>Z` ė LѕnX+l?#y!)ɯHHo?K?1lJ:`gU< =wCp7s)#?;N ,?:]{$۹|P$`!z 忊ûnQz^DuɀOiJ߾>I {(9y% LN7,d8yǢ.uzfEWwq{MآWy^& }?5P~EoRRْt$kHWKoɗ7ҡbA#v2W}w ڣ6ֲ<{mNp3y@>4~́nd"a$>9s-ꓸ/}^٢-lKz?L0@r->Jž;uuj!ɝq|`KzOGTlm:JoQ6ki7y/-QH"6&8{!{9N0ř~7,y%ۙMZ71wէ;ŶteΏyWBWz:d{}YÌi;+>G8Ap8<0zM^4Kb˲ѭOfɓbF I.=Bl䣩1v:tZ`b:ۖ]HGqt{3\<)?īi{v ]'3== }yE/{((xehQo J~SKڙI<+2Nq&ߐ|vqzߵֲy&#]J,6;&)(s LzMn-zm[2_C5tiSW1G~\HfNwqP =.,r{עFZ-p|g uf(>t޵-v"g5S7ɇX=~|=yco`\.f'}:]ƎTŬmH&՞_sp[W7-ǂ+ ;@\Y̚R%KX}dgb~,ꊹ|;;G&ҁu 3aQZE6,ꥣ?[@yĖ1 iK\qlY`Ec$fBsĉ࿜Go;W<_suy[N:`NzWm- ܋>GTs!uNq]M2dc=~4oz ؇݄R;-?'bV[_obMX˻~M޷;1 ِklL[F|=٫e`+?udPg\J?Y`Z+xΑ̠I=9O~VO|u۝Y=ضbc$=³ē] bo^F2=_n.Smw@9IX6D[!߉i|D}Lݩθo>8N3}+}D bLJ\[Ҫ|le`6zw\{Ȣ9<-zU}m73eg  rȼ%9OσOKb+ʿc$xo fv[oF;97zV Jw] u#bv.dޭ-\bF]T$``[FP=}!A~7,QO<u Bllw7ٌ; p bf=*H;? p6g=mj5۝u}<,fH2O?v-叜+'zGIBgA>^0 +2?4Mc!}+DϜ Ŝ]E>Z`鮿LWϲN߈'X(q^[L7&_l+h?`^Ulw:[9̌Wb[,pvoR]vwsŒԽ?Gv5Q\'6 I@Budny~}[atw_W>V汶\{S< 2_OprY`G_v[]|vӫk%R>UrD_ N1[o Oj ck-&û_W:VËK-eM>jڎQszRӺ2SK|!> /÷X/v|>[b +]>{mv!w–'/|1WrkzDɡ'_eMq_bY){вʹvsVY|Xlj9|mpJö/Nbsϕum˵8o em;X]uQEVqNΕuUbUSYoӢt,P8V;=y^gJxiƶsc=/#:.+yrވnbބ,,,`w.Sþ14>fcע7><8>:FrΊ̟r XdfqR ,-׳̬^B+enT?W 'MovgDWj˾A06΃'\pvgyF0){N^⟋rwG_.R[?WC^!?s+R.~yxx1/zQ2E. yσG:J2Mgc1|ҵn)Q`P,0DGBSeϔ)"*54-uck7t'Le"OW,+yD'br{Aw|j ϥ2GGJ\]|o(1R؁cO[DYSĒywS%d#9ˬvcegRsܢ<~zb< ?9cr83Lop{93ۅ߯ Ν*1>z"sdm÷m/涞luBUщ8 7-W&3Dvgx}sc}VX w?:'Ur=pܶsc;(w.|g.q[mQJpg>̰crh\ks?Gmo1lOeu#%FW}h?x=61}A?( +t_;mz[6p}|g{=Z~gj=(닸\LY7o0/yE%f e])k {'A~ҷϭۓvkM3#egJNHɽ~f9Q %+s# [o=lX׺2vĚx5H)96M|?} O6.b̅ZrZ;Q~i;C׺י>;Cy9g̍En?g%.3zs}sS}?gώK?+%ng;#eHi_9kά1jw=*{~+발Wf!ʍ3~J3G?\rm=|w C~,tb:nr;.㐣>ȵo{m[WdFg7TW΄gEAuh){2:ʚϔ}n+{5{Wk)H8ԴU!=ѶۦÌ|XnmoX*Np ĭڕc7 X=,to3r!jmO{ŀE鉒'їKeM:_m O+y$W^g[ɥk 6s满Y999>RAL*s@ϦG7(N}7sj@AG rsF:B9=I|O;+37f=]~ёVD_V<-{Ķ0ց~A2^ɽ{'/8g~D,?X$xRN:|wraS([rm*q4=en=qK̭^anq_;W~.dngK߽޾W:0ρm+%G?[9Pmc})vE_zu_+9 3-\^YZt /ʜZ)y:>cr[?*g/|ߙ)ʞ.www9h|C9߂e%\O+uZO?Pً&~>q̛3 +ݞK̖:0\ c@lC, b"#q\q.rG3=R.23ݶC2~}V־zMr.fmXʼZ1ws7oL|/}Iԁ,GLL>\{kaY/$f +zIoeۋ kZd^5O"%;E_"9#b;7sדh>+ q.y|wIWpY{`ɅM@r}4?ǖUlPT;َZQtug"SE~ =$ϱAյiwZO#v&i |ݚa>8 p?)f;E^Ӥ_q{K+gfn̯aV+s;:xoj[Kiuՙ"?*0 %o,#2׉ILCe+o9Sw%7&ܙ1@]̝m^o~/9VzCev=M7}?W]E g3iGkvc[yr ):dyc#E΁ؔK=*Jy]ZQ1=4ߡ9V4YJULy?}`EOֻ2O*m3g۶o{QvU#$]V9D1w}Rq/xjM,?۲Mț{b9-VΗ\n>P̊gчަ=+ӧS1,gǼ^-5Uh^Ybf%f|N.4َu̯U~7ߜoXmcdg=Iz>J:v=mm8|J~[(6ӥa,ae1TƦ1نy 9~)C4B >gIQZ}/TD8[P)b N`x FB/0bR?Rw`Y8Ϗ_)bkwS)jL]Y(5޳75ObqcY7lKo5^QשI~PzECk#ݿ"GZQOϕ"!="~N_W3^۾~&KQP} >2;=Γ"2ش/fk-CE)0L` C۰ePd|pE'zbNK̃Y̎uZؿ<zpY2l{苉Yj-B/RBѧ/ې\ 陡Ao{"ǡyLk9о(dbFʸ>j&d^+4{gW˃cE܌d=Dt[ +]'kE^k?ɖtoM,ZQ'vCtx{g~zjuayíW+|R`L1NM1DkE/>r+ ]9;LwVI?M+tC^ZEl\'Q3U[;-XRt(ftYŏo Ο%?PoG]gEe*]j(z~6(bzKݠg'Nl^BtyLz" ǩ{A=Bۜya +CE>[ΓtoTe$cơޓ?sL `.޾>E⳦.2n'0$ۦql}}\QNo7LKߴZ~?r|w|<d8qh +lֶlM!Fɵzy:4F'{SBG}v=~v^]Wt]Y9fK5iS }Pe9#lUz74\̞?S=Ub穋#K'&^m8j?5zb[6#Uxo]{;X14oC6l*r<,G)~8P(ڲGzՁ΃o`!XR2#'L8ɏѥBQ7Ѝm"[? lOZQ惯 Wڑ󩫨UZGX#Z^8?:jۛs: 10EonfeF^I }gW6Nk"7Zֳ{P)p~CEJ=H Ff[g]|ox}/euasWh/ٖD-hE$EQ%ٴlNmvqA{?_Q>!ыsfNn̴N{3};wd%SSb~uGb,>uCHoYIzIox^攮H?oJnSaI,%MCz~$D˒-郾.=?%kIk:y&]~I>wvas}~U>'b;}6:w[U<^{ݵͮ><[ҹ<һKzӛ%݈7{,>/$Tk/Y\{O X̡t|+>IBw8gG̤-Bt~;#=r&Hkq.s=a.3=]~Leϧ7﫤\:On u,>35J1tu9yZfc*iKz勗}Ǯ3XG78y0p3sdl47Oq2YX#xt.wz+z.co\vw]o.щd_LwbZ<=]}.3xO,xH[;оnzL~LvޏS٭f :Г<`}&y`)0K٢?>ZI뗒GkW:%<^0X,mXq |rn5mi?/XVٖMOo۳\bZ̅õvŧ&%<v՗fi?x\ +n;L/ȆPomZZvdC/J .>/S''o`{^UQC-b[Ry7u}Mx.ێ$kWw@fFfHp&Xsۭl͘г?;8uy~-X䵯UYfV?; .,r~܃<6 QGU83:5yey,eu}u=fPcJm"w/?e1[̝^ķiQŧ~f[v.'Z/%9y+$Nj ev\ʺ+a%~*I._"n53Bҗw5]e3+|}ӞxW7cJI~޴JWG758/{sM}{ؓ,ߖ -j ClGdϼo?9'u8-I>ܻwuMrXG@{YI &]l;oiώx8q~q ^9Gv^A7 E×K^"Ww$sP{ڟq*e]CCJ_݆xqkCSԢ:QC*5ȭm[.ZaBZwcdX֒l_ܗ[\ї޾ 5dlo1۔-E~t)}A-PğA{h5I}0scrE~ҋϒMX>Q?g{:ؤlTOvϱXu`̉5/INboq;г'&q-^u?'XQ7٬Ԃ9I<>ᛚB| SDYώEӚ?\fӾdk5~H',搾E8Kgw"ɆδzI_^}-fI^wvl'^$_ߊoNyp1p{`s-~rNm'G:*17ZγǁEܒ7C_R<:5;Υ[[!ʹ|{3%3"Ỵ}},曢=љEoߔ.uzջNjc{gz>|8{X5,;v;^SUlg1gb fW`j@âѳy9}\+:O XtWg~ %E9ȳ5-r +;LJڏ\gbt`EL_ [gz q3U쵝m/53&5sL4ܚ։ v?@?"'ˊ` l"lI{wEk%|Ouv!mHV!eJS[xw`ZؘH-{bQ_ 5fCddM[9Emڑ$39^=}{ǀ?ntsZM[#n~KmjIεEL**9:\ofj}$Zw7lZަ|_]/w⫯s`12+od;z7l;ݙzїds6|ѻ:wd ocw=o.fVcU#Y@>PNZn Pү<>8x̼ĪoWvv`Eʆ\r0;o,rִ@݂xϝNt= O Z$dn! Nf'@_+'wd^S&׸[O2DRe#}_ ) zhQgFnm9r"t"҃^tlא̞|a%ܕE/}P3ŷֱ&y>jQ-N嗦EYVO2[nzNldk%[myܶsJ((nyx7ߪ}:ũ?Ӟq,gʻ]kˏ-贠zϵZnge/^ K`}`*}k\ >TJ;<Δf_噠ݵWp](c< (1>wc컥uv1"^X7 }S청]OGgO|#~FKeOZއYi̲&ƽXPMgLL킖zw Ε[48Tb-y:&zuv̛f1A»pw[{ &۲XJv» Ϛ2C'a.46^= ƞ_1COT ?/X!k|!㹲87D\G\>>1Y5w8h/(NWrm}̥J,kϔ8o1t/s_ycqݹ3etz߮wgbW C߭?Auߘsfr8U+D YGcxkcl>vCob߰cЗӂ_A󭲷 [3lȝ-Ywn~zue\b[e(kʵ;vDcV9UbƩ6\BwWX{GЏx;\ 뜻A?|{Υ2vٟ hgݳ68q5Q5Mdz]YSMww~ݎ73.}A-lf mȹQ輥̅vM%F=F>]Dx#%kBKWyG9#Gz\y,_^m޸.þgsMwaOۨ +yR݂. eowg`s:Wb*wvY|}]5͔u[j/2F/5cÅ 12B5c#ty +sDNƹR#1FJv?q|zل_Ĭc~nR۳SmQwt^2)s&x~\*{%v ?U\KWМ.G㊵ҷNx+x˺ސsKtnz!29&B㜳wJ]~Q;W[c92}ΔwE%wΝϔŽϞyz)E5kPv]yGȳ^@w +ñ2n,2F>#*][l̍F} +)_Kc}%^=BBNhKx<ѫ֑'θ{}f8l-{ʾ]hX®{忍LN8?6x]-_@s/TJ-J\e)k?d8?xe^!{\)|WΡeYCWJ1*k|yHُ,a_Sk!F8 ?2-sGc&}~b3e͵xУ]ueTb|{=S9] )׀-ly{Sl\弰9\;)sc5>5*l27Wж]OЮ1_)Io\Ͳ'LXұsG J,oP 8oذ l4`^U'6U]ݫQ̥yU +Z>D.6i)V~]wVSCe ?{0 +ηsw}a{su 8ǽs|>wN8ný}!hOׇ֏[ JrOYs?Ǧ!OĦ{ kWi+{ϰ}` =NXudunv8DO[=;%ַǜuXyx5˾`2O,3L yRc1+m-xq \!ϙ3as;=2YDѪA.~7QX=GmO=d>wu\,/ +OIyVT_{Ϋq#Kv89ʟ3s1b|3!qaT_GWFub%{~b?czؒ5z9;⑜,ůUut'؏+dZo;-GjuuUGQj/n3nD/( JٯE}rJ<'&=\Ms!vW>u{λ FM\MGY/7$~er7 + cr)䁳(r|}]2F'| &sňqǗǽ?miy72 +sؗuԝ7/[(N +ݷY{E{ɥNmCۻrOXA֋_I/7kn~ >/ƿ}l9wChی9}|5Ss[#dOo}ӨZ혠 YJ&-)!k_G=FZ5dʄ3䜗O:/YskkĤ9箙/?9m΢jqN{N\7OEG(i͙gԬO6CnTw7ˬ {DTu>P&/?¯)M s_Gzd\|/ 1:uQCc|o!cAߓFvNw8AGۼ?JJl;eM?Ʒ=R>. 9evۭ]~ll_D(T?>Kk<Ƭ=şG>(<{NrCgOTwOӷ#IN>QX^cEUQ~6GDEok~nܠ^?}=|)7OwYB Z#Gm',A% KOz_ΓWe8}lϟFݗ;F\( +siƺ^zэg8闝(*݊;컈E/{(. ~C~_]]xK\s~Zen%W}dߊGd5 +#8sU{=CUl^5NT-\!ǼVK("F縜gU_i3gQH-^Go_ ϻ*/2_Ekݬr({usμ)^51o>oG̶b->j_wˆsjs-]Bק(Y1onױm}Nczy5B&ȽalÞkl0_mc/~_'o:ޙƖ3k>e}6NQ[X&l+J'y˹wObMG_9ŢKYQu3nT slǬ/L^5ysj"˵mFwe9CsoU3GW_ \Da9 Qy3FNTݓ؛gΜg2>~1ށ7A͹(֏[=ԎF몶Fq2k%Kj!1|l;ϩnFI15Wij{Ӽ8&-74VG۬ߠ׍>7Y;v[oӟħ|u5{G<]!N);q#͐uYD|<^8S?_ld<-u}cmw5:y?D :Cby}nfoGՁO:Ql}γny!뾱~hUQ}X!y;8auua)bI HٲerbFҸu6rQH/P@V-'~̒/kϚ5攵~5e_uO}%wn P]ޫǷICoG vߵZ2\o\Smeo|.Vgi|sBcUY]ʙ TZϕ%|q>tre>rʙ/cטų֕3qa3tt|au*g1Oxb%cM}Zy#Q#]K]%OvzqmW.|=C,BO\wJl|랱ͼie}(s9t~yz^}|u%4.dܵVt=f~v*ξ낗}S| ye;oXq{~Y#G8|ED/egԢ'u;aPk̮Zğu|:XvB1>wz t3?sùGZΙ#x8׃&+8fC +z}+k9?q}8{Q(;טM"{M]ϔ)ZJ +E3k>YyyQ n*gvqUduYo6U-qP킶죡k{am){҅r5q- ?۬ (q{2U vŽΕxuHcr`ߘzث4e?(U8oB^ rW} Fq&]~*x7~2J\ogׄR[iĞ1B9ψmGq5󼏍+y%b +C=%\Y& tE>>6ǖi\[leJox7m{X®&ykC9W,(s'_)tEgs>F=B{?WW+ʾ5R1{9.IiGcbcpZfg])c;o*Y~٣g_6<пȉsmAM 6*{gX3z X.V_9?+F{)k>y\s1݈ 8xfSU6ww<ض [Vj;G8b!a=U{Ue;\YvM69YgUBV1s3zq[Am3;ͽl^#zv3}h|/c8=joc*w+|M*!{ e}?cc>t`}Ytsct֒;Q熵tDwctw^L_9v>P:{85>X7f4F+cr ]Ƨ;E |oG~X߅;ʘv) r [u þ/I8wue_r͝`Wm=>|8oKGz%xO")kbḰseE>#&]>A]%߅>Y+ =w ,lB.@6 +wW]*c(V)]US?t9y}v۲+2k>r3b]}6p"|''iiܷWmC%<'"?~ ]:.+m;g1r{}Cex֭2Ou?3n(ɳ_ž "81}-_L-%{?q=ų6ZYݟޮUa[ž'+cuﶲoOwqr}X w|p:]S +<Rְx5='lro{eëripK]9ٷ)k);Jc e?>s]xmD9Ǵ(tV|`clg=>zP.=쯆29Soz&K]θ#M_b9{{[Pb:x5v~ۡ3t?yEak-(J")x(JdӺ8رcAE"A&E(:>u@>\:;_vYWέAν o1`7,|WJ~ĕA5\! gϔ5d +ҹZŗ[s wcs籁rr_-;ܫ^"Wwan8RM cgO/l2ف*)qk(s3k|Cχ־?A&ڦ>gn(1Z%^R`_}p(DGYCJ\s͸X.#΄,+A8 4ou_wc +2<'>BsڪԶ?~ƺqAᗙv=hFs[9;ö 5c%>:fWs^Uus7ycho<ƞ<㚞O^6m]>_E^;|{Ƌ9?DŽϞ 2Vy}F N8}n;ue xϕ2g PA/wbsSqc y|| Ο#e_wMvt+b1{TJ]m)Ё/5~AϾ}sO_a~ ^}^Q/`ۆq%>0^[ ק7fs-""~]3eBYN|.~Ž!c~U}ً>ȥOMboH?.X{#h8sl5|96 /XsL8o^%}l)s+kmn9={c ?Pt1"O7v=snG A! s&2k+1\ 56O)gK+9}֎}.y=n+qZ#e-@u퐽W=,l9=~33TMe~ +}7>Vr]{ܺn`mq->Pd̙bmoc6 Suw>*3.W7>GSGʚ1V c 谪Ĥ+8 <@35SY<{ 8Uf^8)|q\1YY7n*1rzŗ|71oL2aϽ8)hn; n^9Wƚ{Vsfo]h?nSY\93(h Kvَr>+&)gGrc%vuO=ܹz5Vhqk +_א1w\c_vMU9>>ukrqI@rBYO'zw}5uģ_|]g\Owg?1نb̎DmO)tgϹ7*xosTn8;-80C)4;gc^{Qq:(G>S9@#to (NxXnwo@{m}yTⴑ2z:¶, +'|K"ӼйOYŞckwq:ZfjJ rWs_p=mlQֹ/XC-Vir +8I +g+)+gv||~|Y>msTP{t:q +l@g]v_rc{?ۺ]`¾gs;=Xə1w?7nw_+g3ٳgۦxM)WbnepoqMl0V$NO!'=~`?8K'H;?_~>YΏnϗh;m_܇Թk㇌hyIGsϵ_̿5g~Fnߧ/o<c5uĻ nei/xҖ{=soG{4~G1f:ɻ}8۬8>7ٱuw;1'V}>8\uK!sAg:Ej=o1QGGcӦ{F,8փ3`gsOwxboqtuOY/8I z#XVZU?wUq5>/]:4w,H$=؄>58`#j|7amp 39ByαvVӃt=)p,Y^|f Mhwf}Nꅜw_ԛ(]WMTl1i߭g+R?&rjvw)~CwQ,LmxZ3+~#WUf=! .=ytXȹgsm]^sY{rcH0s4c%umNoj c8b2e^Z/܃#k>f1 zZe;oS9 .S3yj3w5C}cUzb(~r1qзgj\+kAs_rH;qS%5\7ck=`5iǃ _[5kXy_s_ 38>*JߍmeǼZ{$Aֱ3x甚;PSNl{]La,7zb\֍oLKy}b|nyg뭱t^rl~9`Uq]/otS,GU~k"x}n߸c=jwѸE_<۠bx*\Gsb#f˨U/{ڇ=ϼz~*k\s[s_']~CM[]d9WU~!\%ggsoufnϯ=g})d^xr~stqMg>.̝\'4wp޽xQ9s½9yyϢ}TBGs`{q7XWsϼk=ɵ/־ou3dgUM9wd\d8\ʹ~ַy{owcǚA'q{5/\-=zLݬݤ>IT05pŵ 'ٻɧ;}ƽ>ɯzJ8|a66[WwhJ^د|c^z x~jn9by|A>b9On7{|w]n SgTyۀ'z֌'߫Mwq?X9BmG!FLz{g9d _ /޻h9}ߣ]_>~n;7uٱZ;0Ϙg7#o r>d'.ESz >a=Z{+X&OL,uSW#}:N]q6/әkyt ߣաgYW%&]ygOFf9>Ho+x/~6'N!qs&w_ #Z=1jo갓px"M3YmMunq5߮ z<-(3*\8HUk}#NژamWGp |}jϸM%&9pNx6>ŦMzlCzݳGq%2rh9 >G)S4~OM҃9c~T yNYN[D}>5B ҋMvײk;H&^]4bd!`~'i/ !*s?4Q ".]QY,,,,$S( 8RQ|d)ii: +ahh:ieTj|\c&>=sϵmۯNqfd{wPa?itދqH¿G<8If{W3 Igi)9oA }+EM5ݗio6;KO)ާ֯0g''j8N0/o+[TAm֨S`\ _Fjk=O)cp,p+mȫXW G*ДrIqbU,0zmcht8ur(?w>uF[g&Ap~kXN|5^wzʚrqcgX +KtݘOtv_ +ڱ΄/u5aѿ[`3<іnC;'O`s7?fw)g~ o~nt WًK,M/ӃRIuٱ>ftw#%ziIu<\&폘fIGjO( at?Q-2B7˱dVORN0b"~(˫rEҋѾ8e'EZ/-mVi^38M$2)o%,1Y?-̕AL3.=oLkT_> je9d42[E^/'t>_y z6Bfe-3g^~ҫniuȶwٕ9Nsdԧ-ĞJia6Gttѻؓ c/iրWH#W!ߔȌP/!^:I mWADr:*6)e78ԡ3>)T:C'Ff>?U/滙#egcoNDֵvS-k 6b {&)at汪w+=™Fsc+4iKܜȿ3-2eܫdvIn^&ryhǺS?(=׋}繇.` Ji %`I3?>?ڒU+Vt;i⳶Z!u 3* Ў?s-43|kF%}lZrґ0cΪ^f{i_-5sR<#]I}&^LrǶgh=w12gR9z9}~ +;\ȿ+nx/勬q}h o'Xd[\:X]("TF2"t+۟u>W4Q*G]"~ϧs9NyzLXΛ=Wjd qT32Vi+(R^#u229̞o +y'h?=E&c۝6wYd] +dڎ? Yûc J7}.W9Ƞ0%o;a㡽SϿ8`T$0К1F||yo467j6,Y1 x6ް18H*]U)RMEFݔJmB%h([M"*Km6isQ~:ケ|_k 9Z$)Oo`>_٠"b0'AW܊Ơʱ_=u1 :5}J3Cߙ1xNw}Gm/v:Zj4{)ƲOmʚ#n9 <&+plփujo5(aE{QeR^pRjL[lЏ2Wf1!)6[) +G_8VzKo=->9% : + +lRWIW/$,w8^{㇔^o6?O.糘5Zd\\B^KCcY|7w[׃>,A*@2%$@>WaO}Ӏv#/3#εaZ|FRr'*@Jc"!hކo3 V]J99'Uzi:_ja''n\O%Tsm{rReM@?19"YrFC{#ꏴtRIK&q關7٨vHiMJD h)hyR\~fgP vId%͚7 ec9Ɣ{(g};:zl\-ںƎu8{)];qs}Me޻2EG;ww>cz=ùSoIwqHzvWGt=y3l=ݶ[W7.5ܽǝ+jG2HYuPǺ^;TyZY{95J`8v(Y]s.QF/w^z8~\߮X_w3 U+Uv?}~ϖP}^l +joVL8}pvk/RJKW{C/6oZj^p]MWz:6o"լF\⾿Xct@aj\+S8Vy>+ֵu8Rv;Iq! d{vR@qOyz,q\Sa}Z.X-޷/O==lDqs>R ./,62|vW(Lqy6-Sd4,ғ;\ vhL9D6']O6{v!1M>y/;߫\ޗ!{(b7\ ܁gb݋[\HNMQx?wӼqzq_Qyu(W*.G]s>4v7ǽf[⻧ ޛܫo=>灻~|oRڨYܦλ4FEawve=;xKQ#?J߶H<}A׮U8kk"w," ^/{/$ۏ9aTϋ +vxCΕ{>;y .>\9|i >aq^ #'%v37: +F/.N8D.OSd,^1Csg\^ ^5+ Q 'q_6zRw&b97+_)s1Nϻ!&9A%BU\o&=[p ?_XuNЂOG^Kclҹ->JA6pb%9PcN q}}U÷2Ƹoq|ѻ ޟ$1je9yޮcX[&xCb΀? CVA> Oj ^W5h.ÖSy8G ۰TC{|fNصM1r|8 X.V"΄yYa)8΍5 !}$G\'!8۔ %@{k[ufTB8 č;JPN\8~kHTͼy3ߙyyw>N?VPǶ +h^Dr+c6TrPgr*nS7ruX_9|=LdLs:\v*m/e匭N5{RYR;1Njlf5|:9Z5mf:,d?7ӵ,lx1㼐d\zr/Z_\_~<s]sg}~ɵznf;g=e.s/g\wZ4K7fywmM?֕F90mh,uxj{1}\5o%uSs4[[7wҩyfsybۥ9x{~&9el{W#sm8x5Zo#A.~_kܷsP}6{:nW e̗[-f vK%ag=҅hs'JVt-%~uFNW}Jc\hG)~5·޿6t)%5¯KsOU) ^F))kt,e|Ow4Ky68#3^ϖϢecq7TǙ*k&(]#̟)SC3|I53QNT}~aypztܪZ;Gޣj=3UmTaAi=hulμ[Z'uR N:ljoDYYKGS^/'*eLD:V3wLEz.Gȵ(ی"=Vlq'ð5Uwn/1nFH1<螵"mEf붯DZ[s܋hg=|9j"È_ڪts]'[zw].<.lP97x7rRlx>1Yލoo.z'ǥQ=݃_|\V= endstream endobj 13 0 obj <> endobj 12 0 obj [/ICCBased 28 0 R] endobj 28 0 obj <>stream +H 4]ƾdɖ-Èc.j}[v*EF5-%Kxl"Jxt>}=nw.DQ*XThju 97M-6,f&:FH#]ËFzf6Fv&NfXKU5yv&]6\(VλT;Ʃ-~%A{!ܡ+Fa^1-Q۱qWa&ޘJLfJi(hZ6u`RȂ六E#%9J%hYVTTV>\٫;HNsC`ͦ'Zq?7jjdv}.M_/ }W0|};8>.px}Rʇ菄eԊ'U/Ӿ6P[0|{.{? E!##+* 4E]@sVn!ѓI$8jNpqAWxysN +, +vF"b+~gŃ$\"%ed&eȧ@`peE2ʒڀzF\$|(m{KE==!j,o"o +5+[hX"-lm=|NW\R]sJQNaU̶%._?ZXS7G&VtփɡlWDÔ #PAQe1q{8KƟOT~XPpZt0 l) !k8%J!).,UzDI\7TA& g(+ *7j83TӒ>/tA}y[UѠְ¨ԸؤдyEeUuMm]}=2jG.MOQ=AQiE̚׎/X&NoM@a9Ae!}W>#8"!QJz1NWs]#=I*v3+6L,XIO;m3~a]WY5}M.G^~ϡOT +2 Mޤ)TVzc& 3-s;K u +64v\y.<`%S`|{MxO " "E3SZ-,,}CYV^$7)_VIAi:e*Z=s*Z\ZG.!Ҵݻ8ס_jdH0r0FHr2[5贬ʷN s7sqTqutcFѠ}; K[>|`C< NT 6 q\!E'GEE5FwŌ.m\% /@Rdwőڒ2Iazɝ>^Ħ4\֯.]&\̻CA„ Tǜ2z_ЮQԤLءKܞA.:ELNPV2P> y_P]Ȁi>9[ل%}ߌ@}~V~Pz~w[S=׆pۉ/7|&L|Trő|˫K|}vv.~Z~֌=] zϦ{×{i@u{яe|t}5+Y~<_FzqA{ WH|;}EDۉ/~ )=}f]~|~3H~o~T7@vw:P' +mm'{ƅmSB79[ERքTZTkロl`RT6I8 sσўlJWNqĕk“pQ6FّVgRLтs{$|2jQ:]L5B⽂ϻDʪȈ{] vLu; !ZLYʗ&$Gf|a4',F6NoyI ]لf$ZZp~˔oISUD Л}KnjC(?l?[Nv>E^ =HK\kA.?&;bWF4Dn ,#7~IR3 (,euj`Fn%,3N&(J.O@E\>৤'漉ꂒ>̏p&K.ǪuL$]BV͟W(圴xBR̺* a +YfyN*>!-j潥{8Cpr +NĘRvne< Icwߩ!.tpmS' c,FM@~$iHfEt}f-hNd??{X y_>Fmq+cK{$K؇`&qT bnr,{ qU擄|ܱBb7pfsi#\%Q/Ί|_Ip}@F BWD#&O"{\gZ(RSN?$Q¿MG,rAUpX ʓ9Wixuz>/TƷ;kd 'e1Vńb4n_DX4. _/p0x'kfĶ*w-Nk.s +s7)iE6a?KߧK4;rg)x`pig4N&͊DZɴ$7a + +}VI.>w:0˖M@7o?nnU|/OB7M'`iMyvd&r#Dp+Q1fh2_Œ$gGл6'+ɩbX&Ca6-SqZWuD}`H:) +_rHh7zIMߊڍV-SC5}M;ŊIKp\GOCDg#H:Sί ߧP;^D*ĭKClDS"GD *} T<N๬ٞ/ :}=>HƦ]_}O<duw nfo-E?xG}1j3GEԅ:[X| :k{{ "?@(>C2=Aw|}eb#ڐz^Ofafs0O5&Z;M^9("JeP$.sÙ׸ +Z‹CP+6KO]S\Y[&-s2;6HpK-";L\p\8zب[m_;iB&}睾<3^`(b O e^SՈ2 ue3IM9OR3OjGWbSQ"jP 8Tv'*v_.` {g5Tj.dפ~@3PzF{v¨s~^ውsGl+T$0luk;Q1:D@@Ŵ0ZޏB 7DEREisfΙs4iwfIisǃq WO<|^z[5fުo; W M]>ƠNԫ)~=3ޓу2_ڴukk2sSw@=<+`T"F.vDz]#Z%J7Մ3zz::7/Ԇ/m/&mp]m4l:S8T?_P6[ f/U*yn_m5" KS:30Ny\:ƊEœna۝7: u:գSG+28qs U~.}nem)7 Щ׺#>[px͚?lTBZnpS7AeGl+z 7#GVz(\#ˉN>Y=GN~ËYPiA4z*Mg@Bb$[KxI|N:804SAkмÐE#>)6ח5ė d;ndi9gSwro5),eowgb"6R) "B $=M6 s zQ;@~] 3wezֆ] \YQi$ITpAG(@on[^3ӯ$~[:\,!a9g]JL VņOf/>Wɲ6- |1Mml!VSlϨP2-9?EoUD+$$žr&LmKčN;` +uco@4O-ҭDQS$Pȣ>sԷB,u)<-mMDAn0U ^B:O+=wȼjS+j0)#^1%>/ŒvrLxBT]ְ'm4z+Lsr5ct`1;n -Ӟ`'nC;A/ +O5:_*ЅGٹB Ȋa>δEgMgY>=OE=ʂކp|<;(lahp`G%ß,~H-h5ʑ2q*iEJ?Vŭ?<>$=p ?oV]@P|銣4˧mB;Y=In+6nS~$nѐ6`I2flOURS6 *ݰVD(/dS;]H4I;i5 lUR:e-ǥ,u&RWR7jFߩ!QAJq'XB3`GY-nd1EHVPw5攱Ly6'\~] q1-xs (1Emb̍2\AZdE~Q@(\T1"]JS24l )p)z[o}C=+DXɰYëwP}mY=(?T0Nmǜ{Ycg@;[&G@GeYkȣK2]*7 D6cv,& ,u-C}~R*4}G#֩߇\L0ԉ\fGubQ,A 6\Bk I/+>?ɩd=ۉ6cƺ( h˳ʩ&@+f :/}6y2 +Ԭ/˴jO}\iv vn'B^C:ccKno^~/63!"F]i;T߄3\M$GWDV16trKq`",]$!I^IH!-J)zXPVrz>=m4Ś2ᶍrtqUJ:fYS{ץx[+oeS`n&2jȟUzǂ?tfFZGj3]kwd-2 mKYFzޞ^J't"u~4>ʮ[Vל .Uqmxu +PX7TF{reɔ7t2x\ntNm![rt7O2d!-YZRC-=!97@@v1h$g5+EsUŪUBG\xE}1|kqgMŖʀl>y<?!xvJ8?v~Gfŷ۳]QtPnR: ŞUWM9 / rg *7[5{e.;$t~h3ts3i86@cQPy˅i +=1_: E*y͂-גhb1نGM~vL%j7ӊqo+gΞ'HRXrkSbiuX0Qz>i4t!.^ifK2k^F:/b[ `߱jq +9k&:p76WgK`~vit* oaY2»W(-}=7AրY'ikplY_Wz*Ka_^xGZi|mo2;Sҹ6YDnt 1V~_2;Pj?.רpijyr pxB׈n᭼fj|czNd-@<,T+jIrUOV@{x)m+4z )7Q +y3V!Yk,U[,T$ IsD#p *4*(Q;YZpm6&6,rEiEŏ ?uQرKmXo̳al36Z?A)V8+}Qj#yj-O+z#H44ۼtbjP V曖H@Q+;qy SS$>ڥnfTݠYl_H7HBދ*ӂ*(v\Q.(T»NCT-o8\ +|zzQTc]3sV.5NyNt*LĦ]a`31b|fU0ٷӇkѼsFaDYTToIlʨ~~*|P|]}w.WjmnBuR rFޔnb.N@lO30?SŨpbca$gX` W=C .{eTUhqEaIv_1oKw-BwYuBim b ҡF=3v򨬔hb rG׉HΖ6+beUb5,JVi>2M252]͐}ܵ^ AAhỈo;TA[?#Y4UOm%o_f( Ϫm: S+tq4W;Z9n?2PBpw |m=H 3O&h~LJx9g.%RjH"2M{׀L+С 9 tC4/<)H8ֈޘdКgj9 TG6UBX#3hYܜ(Va'8CnV˥] W)`0(Shwh dW~J ϋ7"jrQo@5wRX0@҅oTt9Do!^QYjZ#khq43y^x"SjϠ\ GVؙ5k6qEDnZmCQ IMWyz%:_x,ϳ%ƶ >&ˤnc YЅ7 hʬX!x<w&}JhOIA#dG57 _`Pۼ\,Qh6js&Fr E,{2~>{5±."!1% ;=J70 `90H ul(e4ރ/ӯ$ L,I>KduA!IhӠ=4,G*3_@W ӊabFb&3]sX &,:3}bTge5}5QH:mGe^;3'2bO߿NyѱG2^u!2@IO*t4d |3#'V" ii|!KO" %z +oW/\;<pQw;')gknP(w`aX76IjX\ txB8Ӓz)m5/F=pD>bQñT*aD Wie}I4*9VGm8S8"U 9Jud 7Gq5ua *E@!VhR* .ZG- B,@HnBr}7 "ت03U+ֺ{"Vp>=W\%E(‡RiO ~_cڪ]1BF(uă2a +1;ljE] _'³?gaذ~g;6d <{PY3ɷG|_X2YP 70Y\?up o?S4?5t7xy=q";>ޔk '8B1Ǽ.C(#(ǭx/ݖ/5Q~0Wd"pCi#`LWZ1ˋrt"Vީ6=$Ԃ7ćփBΐcl pU-p#Zm$=Mǿ:8g]N~rNY)U\gysnWq_+cj|XCb9bZb;o$R U(=]TI#% veI6^]ͯ/2'?KUJ&Ky Zjz,F VJCT:dIU?b(4g?<%1[/ J73n\I}ÈB6AZVh`탬TZ?+$VV&X7Dsr@pYy--L^/<Íh⤔{B'z@,E +YAՂE2i,keaJB*^F[R_@C|u>!tQ^H"qa)0%\fȏ>w订qIG? 87ȳ{;S҆ +JA꺓KsEG-"Q 0tW.MS|Ĕ/E  Է S/I +: *XomUnͲa#{J;H"5m5nDPΒ5:iLL@Ȕ&aZ$IIyNSgR~Ot>D(m!ߜHcCpX5I/]ﵝxI.ܢ|)Mmj~__$!-̩L1Ul\c_~K"‚<#[(a/j\2]׌ {vf1g{)aV#߸y@Xka=wq"H7ߧ~5F<#·kb3(=mm!}<(sC;ta>} `/jLKʵ~DM" +'ܶk;>+)2}+z%m-lS.4uٸ"\Y~ʹ|FVzg{Wդ|X;X# y8,C8,Uh"ww*k[w8M;+ț (/Q==!O2' :˂:&2V".}rR(XpCH2?ԡq< PqhLeP2u(" +B'9@a !jH]཈ZX}?y7jmܚqNkWSax*Y5]y0}bꘫ~yuԈJdфJ3C ͼzXʪoUVn|\)}pv5Lr.j9៥F$lS W~R|-*DPYh M_/"[/\kvY(<~JӠECw{kOC=N+婨G uK2KN T&Ѧ0;X:ITfw?N$?Rb9`%i~' + " +уuXڏLxyt 53gAgMU[aBKۍ@ހTe&s`:i+k,bZbgB]ܡۗQ@ YYV&[coܤ^mcӶdB+|܅w#Ȝ%_cS! ^k;oç.o"7VbXMW~9;XM/ ~m +R=W ލZ9 jD_2 mL*PJ}}{SPez +_n*Hq-=sq<4qD/(.D+ k&EH7 q̚G\8}Qoj+HG1jConHWq8`j`I)Kԓ"כ`ii![yQ q =СDÅ +hu+P?VMQm%Xc#ބ s TyWAhk7[F<L8A+a2|22}UȴQ>ͼkT,u(6^Q]d* +vBEw~K3cO+l?w>9%+ 5daB+\ Q G1iG˚O蹥 `sQpX.66OxyOꯐ $SF.J4pBqsq /g[jŒJ[' QdM|| `X_cs#h݂*o;=kKPoB9WI?ɡ\F7N3)6,vi͈3( s0DQ9 fJt^DO dy Ţnp_pCkh:0I6:Nw;Eݫy/dlDA z =FU%}C/~|­/>1/Qb,6lhTșE6R~|{ +пI~ "@Kk c2p-iJk_,p):+7; E; /̳++=UV0C#3[X7YfLf5 unϒ|Uux0SIl*p5@ZCGyܮejKS)@MA7%Lw>^ /sjԖA:-qf0X JQPtE@ %) 4x/yI^K}Ћ;.vZ`@qˎeDy9s~oY8%o~Kpؖb.Rq^պ)5GLS5/zY;va9Ŏky=DZ¹C8Օ@fW|dJ9sMy.a%fD&Ʈ Cz2Q9^x<ć' )*$v$/;XR%.>JowJ؞/1.v*J 97wNx{0/qy/YrJo4hbZ&z@(4g(aw-!Lv~XRہ; ј!_R[|0T,dNd8C B8++ *Zl76 A 5D+r [)}`,n`,.传ZڎƤmNBeV<)8`͛ǟ0Jiuڱ p !7I$$' |qs)눤=eZQĔB(xtQ/猁@DHlNk>5m;"C\R^t(R4 ^mhipUrYFlRT6 ZmԤVR߼㏎h/T6D21qUJZL1ŠUjV 0E-K'*a*cBp\(W/pVss},x)y؞"\t*G0'G)`vR~ +s? +OAL-pUJ+_Wf+rd8/I_II1L E*a{z`&\&\!0͏ *~rFIbp2UeyV8u| +:dM WVI-;w/Lscix6J񖜕):%: \-*h]~px\A}Y\WD%>טǔltc7`- v$ۣb5j1JYC EŬV$u%uVґĥR mG6lD܊ +l֊>VZ+FM{C3W1YkM|LXd6`K_or bN~H!s?^D63푮5aj>Xe\,Wx/ =ԅwn>)[~ GvKm!0S>Wud+vjgK^=QN$":]}N)`φf,ޣM;ŧhqJ6Zygaj6cO:Ԃo͏R FK\$r)b +L+kxj Fьv^UNRxQJPݮk_Ph=G* {<{`l`*]xn뱘&Mfڔ!f١Dp-G-7݊~C)elԟ}hAic[eqǁo%Kw/vJlW>/ʾe=ΝrloWۦʯ̪Ϟi`!͗l8gbLZ SYߴzN6m`].i`Fcl3l͹լglԅk*zcXy5|G q?7 McN6"/41S.ӆnL%MB`Olx%0-d+)\h=}Ť944ڏ,L%5kQE?*(a99mܡxIڋUsn |WkM=3/Z ſ0Х1*NJD6t(jllBuot2~I|Ƕ(fLTl-#4]CWm3l/r~={!j ]0U h+6rO=jΧ%CEiViu/4*D 8hV.wN5t\*ƥlS H6\" RJ6]mY3eǩۤ5g=[{j9s^(j g +ox#b_Gg8ltCדCr ZMQ{19~] Vc5O0c{B3O2,ؽn/Ev~.MΚͿ5n>< o[`H'S4%蚖 ;Xɯ,Oga-bV8oE휨[LmgpӯOx%0nͮZ^ْ/{撢:%9?*Qu :N + IrimŰݑ smS9`krOH'4=MMZՆ}}7ޭZeuƐ2W +-al̏?tc!#Pt]עk5/T'T +V9ՆRRpvhN=8ҵ/ί]ZݛS@>o#<ķC1JN>3} +_=Lu@&=+^#Bh'7Ѓ!=tTk_۩ow2/I{ <1ۇA㺇"lw:ʹʎ=@*w BO'^SW(7@>h aSmpxpe T\bGVXfHl$;i@#prQ-ukdõӫ!fUMfoi"44}#&q(9 (.aBbD8k5ѰuTp`[T *r`L0E0!|D1bVFhs_HjLczQrse0}=9kd m{GrraI}^}4 9WA#O)-#\H+=NRn5"!zZ.%h +< ,J@=GC&zP :\{?Lk#x4 ]1r+ɼJ%X)c XlS H^x[KuI%ZH" "eF0הOU2@gY7|9e`5|Ke@FKJѢӴ;#6Y?|amd0m\oMjYUKQ$˰H/ b#$a}rNIr' { +UDZp{T:kW\ZO>}߯1.B!96Mr2)= F}c^қGO_~WKqC['98֔]#y8rrQon&mDZ. +>k3%$t<иTDvk-HA i`Q,>ӜKX& y77 `Ѡ]I~cd7"y|rDR댏Vw:¬H :R [EqJD8;uMYu5wt'> I"h1 ?DQ`)a5itL]œ Q%qRN9xu]GvZPB?AxKwЎUt>Jxex]r"w`c;>r I(%_v sEg=rx.Mu-km@}pV׊r8?i]c\y3?)|t5,oQ$C%U +ah"M/tP/:#mJCVo/ +V4d˶c*Tt5T:\.!OK, +xu~ ̺C۰Ղu?W0zwpZ)h +,ZEۋp–oew.3@i?SqÂhIQfm}q:'%o@:0Ӟ%"Z&U ,&LK3*Cde?spIIޚ\Z svQIC:Qѝ'/۬MYyQ}66OuV ֆU4)sڕ&r~izy*Qy#Y1LFI݇S*]B-[B 'jRe{hw>J"DFb@I)&V4anv੄H\JmG?]L$ Qz+f_.?,Oޕ+/}} Ó8z7.( UxaB +&F*TaJ:EUTߑ/tb,زf~nFK~7CJ 䭦f)(Ʌmqe%]GSq;0LmQIP}Qr@C^AWqjYy)ugA,˪1vUAX_;">)Ϊz6B5⇖x܇V?T\SdM@:S_m49օW|)Q6.KJpn594K5y5E7f -j&ۿXhGmo-a}VՔc1mfΤ ۘS| ]L.9bCѦj.7˺Mr S|uBm1Z3 xj$kU")K5He_w?hJt_hē 5S\AD_+ҀP"4վ/[a0i +d憲*ꍺDQ٦(0yj,QTE3K1{tƾ,M#{aCX{3]ut *θCWڭ?֒줾oRm &gM*/d]rjS#+UH7%~yȷ"ο>8A"Z +DD1,jY+RP{$`H$de (VG.UEQ8ʴeEx=y~G +i-ahiSxӯ"&+tn!h]H9~yJi +kYڜPW)2;q3vۙ3~"S'+by'H D.u{ה@k="iP > t[F DbKKW`Cbh3=!W]D}oxmET{H+ibYƭ=S\=JuA>)S8b1Tl7U!j*N8mCu)ny^#,Oj-JC0?Hl$5[jU/_x*'a`F zN6o5+>\YZyPJq*eEv1"-!:u,Km9 { B{(m +O8z^V0LTӍE+#0\NjͱV[(/-2dL25~G]N#JBgPY/Vk?w\Cƛ'R_p,{q@]t*=.J.`Yae` l!`Աk;@{k&9[۠VB)'>!;1M`J+2!=]/\?=g%蒓\p'f$:)'B.usӍQL7~_syvX#Xբ vX!B~ SMH =@"s`h~Y)[Lç=׳+d#4{{2jW>dFibıf 0ޮ\5IlLO4)GR}US.W%w, +ٜtFo_I8@PydTߌENne˕f*\iTro^&a,ό8FwK?=oźFT`@}~d<зjSW].EhU*=jKl#2#p,i .Cܗ'b9I[ȂA5J_΀3 #E``G"D/#$nsw d%b7Hݳ&B{@at1R3eY EAl~EKQk&vt0 'o0ɁXO^Z$J LOK{O0H&0zɱ.I#l$ -2jFHQ`Ry$WEcԪ\^M{ b( *%CT6Y +@BofH"+ b^(rᪧzZDpPgmm<]hȔ)F.dڀ|U-o8՚=!idd@ +[@޲R$po@ MRZw&=v~\,n̿GE:{)"fm 7a6z>5siׁm=)$Xsi0Ph$`Rz,BJUvW۫dvGkǤt4Ez7;C0*'$H*`:j~F8! D'${C/JtZ.HdV T5* _3sw5Y9VQ/d%rZu.^(JcW;N@8=(}D)kDEt70&r"x#'Ph9톲wvl4"';h|@w+Vq`A?A< oqNi\*0Ҭ^\}FJp+5/c-o$Xc;v7xsM0kTonʡOļ _겓:3VGlG|'>w5'~%|ҭmȃx[>b(Kg22 :1!9~x>S`UګROھJ}3kwM{,㪖ե1EJaH "ɷe+?BtS-jPUOhs%'Bia! Z[YyfC:xeʀaP`5DɀIvOʆx BRet)DJ#O8(<y[#S9Xba Y餏P }V"0@qZ17pؖ9_b'U+%*%V`JxJD =Uu޼A ڒ'+*IOj'"ϫ֋6:UZ|S^M4,zg~e/ky()QJ_0Az* {:x/<,Ggv\ԬH[ϴxPy­Xl5rl{Of֐E5n +߄"$3v˜I*a kꩍ1^&,/1\&ďw琣1(}Dwz~n+yW_(EGTjC].SoH ȀRflke\< +^͸I펻}Q#F0׭h4t闉xHFJ$ĉŠT$og*^>֩D1gߪ*o(=¸8Aq&p(0[ݞrlҞUfey}2L0ӡG %mLvxRQ +T7*JZ6}Ԝ +-k-1ayמ?Cx0ۭ(V^g|bvEPEkriSkOi-`e| nE /]rU#uoƂ̺~ںlU@(!ũ¯,XX;SL j(i;k IZIܙ$_RZ` LHL>tYC?d{|2g#S%%^G&7CgE_. +sқ*gPc m+6&6\#W<\_'s\RsvEf0qHӘ49PPy4xTNp[MU35.RXè.bҡtnf--dSU\>y`nZ9Y`[wyZXj߈l|4j`ڻkGz:[?WA:)ܩ'y6aՂ?-֭nT,ZS`ws9׼Jy +(6Ċ `77Yiq/(4,ՉGTG(Wb@h~ҳ罃"TცEQOƈǰxrT*4'[h?b^,`m}r.G\ͯ]/d +f +љe$P&l2H"\)!S:2NO[uH;U$忕 ^ ?cD*ڒs8|$ J8m,n9]y%{柌 SxreT24  4+ԠbZkV +qTFbt\C =  B;\x#2 LΠٚ@KZ|oB4_eJ m{g<|3\i% 1b\q9g8R+CVPҁ![ѾWn7:۫CklE+AD:5-q\4ں2PK ɛ!͌qTW|Q vl plLO)2ـOO&ːd8AE1Ġ2c\y1OvWTqN7)=ߎ?\[H"jgY{Hfr Y{GXp56'}7Vo1SȄ!Py2uUKM~cjQ|wv)%)x&4Ɨܴ=[=:j =E!*e 0)M\AB$/#˞@ @uQ:V=;ъ⊢T֡'~;5|1+tUmhM(h֓zW߰J vi*Otwb +/6%;%SE+i#-FOo+ВȀ`X^m/"VIONP>U,?)O~Q~J &U1]RGX@." ]g>ct1=9譖 +tEWc {ſs~䯜tyrMުNIU' /g\"RCt/p.36q&XJ5'VmÄ /^&[4-/J6̘~^= di4DdNzI$wgFZ +Eqv=EXu+Hd1M`%(.~=pOU3m26ҋ'2sǪJb4`9Jkڕ$,w Sz[Ջw1ì-ְ9nB]#TĂ~[P")Ȇʪ5dx&;(7>)@x[_Fʚ2ؽ垼ܿN E_KPe_1gic9&OR@&e&WzGZ˹97TQ9V7nH<h/lv,Nz) + 9 q>B\ZM?aP-'Pn*p!۔&(k')wHE&,-Tw"}?'P^ztU yKj?mb^z^xW?'HNRYnW&bQ^b:d;$.x= +&꺽̬yqd$ڮW91ۯWer'>ηN׶P.͑oZ-%K#kxcEy1cT\3"l 1EWUx&12P<@Γx /dF"HH+ǟ, 4nWi٨.LT)u +~&X@E~ԚPyg&똫U$@-2o?eR=(ۦ +th=KkՏ]$#%Kr[b|k#/A最m?6زRZلKaZ4|{F"=k̊_Ǝd) +חVfo-sIh,ֆAjr&/8TPi4 'U`VwT̡z\wK)GK{@}ˢ)(\3SSX7H.26V, +嘓F\0kL[W +߰lXS6nj"}Mni*C; Xn\Ļ!fWiCM^>OXKֵCu0[ эSpyA`:N[ u܇!g5a6V )=s4!GK#jnܐ|T#}6>QUM~V sFn:DH1kGF琝| awWXkS.jj).E5hV,b)վZk6V]zeݫK&862u_$)NqFHůk_V] +3iϹLZ gd2ĹlFW15xz:A-TA~Օ^~Ḓ Vt'ݷ ° +hH&a:$! n:(A "tpE8p38&Eo%mZENIhP݃_G ;_IYug 3@o!-l?%=8y# 6l0F$`2rP9h?$/((jxgx|C,88^P}x,f?-Y.pOi',eϕEj|(ѐ@Ňo>4~nTִ uȶzFY@A-)]>Sg"hج;<]w__TŻh[M˔X3y? G(IqyEբi1JVg&&Z 2{y473l|"w +y PNbw dB6L-}߷< _M2X8=I"/ȯRM LP!d j0 Bs*/gX'w[)E +d.TSTl<.TB M]AI6EN{/EGQe0 y]B-+ ԓ)!HDJeۜhҡuՔF# N%܊P/JT IXzE_OzLROT:(dU<0S)6W;=q5#mҽNt+z*KqIzvkNzQZObr^U҇5;53낈|o|J szT,,j^vnEClk ` V9>JQ=ou* cXLmtu8o$t)ۭDu'NH[ +h-ڐytx(q !>b"jtCF6ogkP q ym|-+܊wH;8WUmz(Y;Q?;Ӯ&!`C^7g +~T% G.PAU=$ج.n#cL2z_iv&6? iZ}_dX>scH%G} qrK4xC~w6a^G2,٤B2%ų1sWeK+"@3eL62ƣ DܥUc_EbfstF]\:Br-2]iJ/r+tk"$rŮ,h6_N}gE&̵&Ms+[:kGqC})g–4(WX+ߝC܊fvh|edKKgonWDs'uVȭ[Wc5+j<يXo<|M+78ó[Fzƭ~O bDS׼<+䩱5^Ztڭ9ƄPٹG~^O0b+T-ɒj8/#'Umŷ$ T;]v+UYj+,>'5ee1岽ᾪP9w]xpZ-CE_$)e˦-߰%j5ǧRlh\dguHH`rCMHOʔ^ް9fTp.BgbEKl/̒ +{4,]gbs[U>M0u_Mwh`mC꜂%ȟQ3Uug[/| W9Oof4ջn_TI[X="ɺDѳu5oV֞-w4|e_D56Ά#x +sy'յιRyK+ (Uֲw)Ytڞ{u{0c–qͅ'Dx&d'WsbǸnWlxYfՁjH7*5",W}8:9OM|3g(t k'][aJϒ5J%-WI:u#Ul8E72g+SQkJ N=$֋"5sZsN//}|ŇTH֜AR:RqSm2K4If*L *+3[99o'TE#6dz-żj*}9"$p4^wK?Rۍ]=𦕸snNxB]j[M:Y80 q!NdϹBrm!w+ۇ^kI]i@fY*CRپ9TZҲEr{ Kv9s;g:s?G0u: &oVKd>A)Qn06$A}qw +A*QUlƢm֧r a}!hU<2wSh&;ef LB[uY( +7Nvc)I腦HC& 6l Zgj1<6`K`-#$k.='p9S8w%wY,Vo X\uhM都5QJUZ@A#Ȑޛ_&ل,!cdZVi핫QGETZzԪxպ^ ? 3]̽ 9f2j Y7bLb' + Oy=A\/LYe yZaO 3Ǜ3k5#Mم_u[PzX k_^!{Ɣi5Fvڄe!3 g 98]kui?o 5sUn@Z!FzU4mfelr@GD]ŚŞHظ^ċ{2IRUn7Tj+N≊^fʈ`f0G1BK `p̷Eqk-qpko1?HkHpB|]d”6z@8ڟ&UGީM*clAOYUХAטy7 +Zo*&Kx]Ls b_fN[=f!Fh\6D6GSIYnpN6;Fʳ +Ɏ,|$RN8l>)%#G|)e.PG[sLwlE nWͮ]d׬ȍا2p8l!% iFfw{VV9Tx;RΖ\Y3IĂ<̠@3B`i< +̶ [`/FZ/*͂.rH9򹽾o0&` F#]2*B,XX+!|+NJ1iyqOڤP])rVW%Ա^<*t<{Ds-%mzG٥ERISP@CYVW^S@|^Jl% +{MR9LRJt5;Pg )ZVr[A }J {auSJaȽwpxԇx\99Muf$pw 5UgJ`tqJ]=К̓l6\ŋ׭DT}iG^;#ն CR{e)%SW!sePL"LQ6($WʵqUۘ h?!ŤR뤡@H&ik:g*eq JгtmQA/?IH|vR^yn%nz^$BX- $sD,RY-:L-_bcЅX8뤸 n\ISpKQّP`\}R+r23QEpѩdU9` +`itn(臺|v}:+)?Taa )xU٘/6bȚBS`,oPd|Fޅ]QMYD MPYBX,DPv-,=,dO$$T0EǺQ8k)S=QTdE|0|{N`,j!ے,#,sS +kv\g3YsZsS{d[F6$f8f Y[̶=cٚN`)UMWs;GwƴUƃTF?s\q.9kH+RHjk` JLМe`3o.ĄYCK˻0ʟm^}!}k:|&П^$) T]9/`ir0l.0gg +ʙt&ócR.d/Pt/W{{Ҩw*n<M0%[e_IQpr]\;p(Ǖ]c jt-5S+mԄA(ʮql~j1!!%@$mt8 @k!%ؕ"\%edˍy=yu6a2* %TiP?.V3@>+3BOЕE̒=K[i!-UԾ S=Pv7<̱傑2 |g92C%ngR@ iYѷp/0?]̃bUFG +މS8]gXBRmSh @yjZo93ŕ)tJ+ܬBT`Z6\F0i0֌.-jSyn/ot>ծ2!riwb̄-ws,2(#Dqgͳ'#漥T^5ٱ?u5UX\Id&m8(y'gvHz܊_JelI +~GdU16o>WAq{CEgzIHQ(_aG\i'Qi$;}^UWp62z57׶Sb+t4]n^9ZɌ-,Ink}U[ +뇘%֠Z xMk{7 `[+xp wmz#/ 8)oȅy*Wڥ@%zѴ=zL %dIX[Ϊܖ05w* oRG +$ +MRXc%-EwJZ.5cKh18B/ .]{J#s)2βZZ0leS{.ς eA˔{ ѲgR +y[DscV7͟RW;'4sڪ׋8+oWo}V LXi/vt.i{AXzi J32Il?XOQ5_ӶR7QnN# 'YiQ:c#6dbh +*+4c}]g9Pp»*1Va-ń=ax;g~T\8Nx0'z +7Xtb,VU+Ac*ç4ʧP^ +)b< ^X6*M E1U vvs)e\\F'9A9i_{NSֻ ޝ~/K/\!6g8t4,;7-;Îs[N{eG]jyFM%+,~mOa~CבƼH_!cX?U@QAOBMCKDKEJFKGLHOIRJUKWLYM[N^ObPgQmRtS|TUVWXYZ[\]^`ab1cDdXemfghijkmno7pQqkrstuvxy0zK{f|}~Հ+He׊ +#  _3W0 rL(` A!$""#$%&v'](D),**+,-./0v1_2J364#566789:;<=>?s@iA`BXCRDMEIFGGDH=I8J3K0L/M/N0O2P6Q;RASHTQUXV[W_XdYjZr[z\]^_`abcdefhij!k/l>mNn_oqpqrstuvwyz"{5|J}`~v܃)?Vnَ3Ssۗ/Jeס1Pn˪ +'D`|д$@\x˾:VqńƗǫȿ#7LavӋԡշ0Jd~ߙ.ATfw_0Vi$^3  +g 8 + V*xQ)iD t R!1""#$%&y'](A)&* *+,-./g0N1623 3456789|:j;Y+?@AABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgijkl(m2n=oHpTq`rms{tuvwxyz{}~#5G[oLJވ*D_{Ȓ2Mi۝4QoǦ9VrŰ2Mgл-CYoÅĜŲ #:RiρИѱ6Spڋܴ۟ "7K^p7_"Jqk7SLASq' +M  x 3 aW] l2Nr =!!"#m$;%%&'u(D))*+,\-/../01U2+33456a7:889:;~>?@ABpCRD3EEFGHIJqKYLAM)NNOPQRSTU{VkW]XOYCZ7[-\#]^_ ``abcdefghijkmno +pqrs&t/u9vCwMxYyezq{~|}~Áт/@Rdwőڒ2Iazɝ>^Ħ4\֯.]&\̻CA„ Tǜ2z_ЮQԤLءKܞA.:ELNPV2_qUKwT\( =jˮr +R2<3ɓI!!D ld +"Dq "rTrw?7952LWּGx} nxN&Ą} ^~ʶ5&u\)j)P%gB=xȅN%-3 +[-X?癲.KGu%$${Ma[!πT aJ}sqҗ9ݬ|w;$ N| +~ Ь4t0t5f~%M`R\Cv+kCٴ85V0~5˝v5 +pCG>^}(S5H&-k[dLjKG.VoehjQu}C& i3-b Ujh=b[[3`A#X܅eX3H77#'sbP[5nG~} ](e;4 D-=vh&S)䛪ޠ Ia!*#$I,nM4!$vyvZt?wJ;j]wR^O{T X>f ƥ^ۍ n!-WאBk ܗù]i氩SO,^Nscn`x$*xynQ ەm)g@uG3PbCb(_u~ +x ޾Y.4<}ڕМⵘ!}&cJhpv#zY ) 6-H=20krOƁSy}Lo< uL@KzJN+MxV(/pr#x+32Qn$j3G,9I)pՑ* +ؔ`q<Cog,5\twa"[&  hMF!ш|J<Ï!=}Gěf4/԰7AKD +<:>_o/wK l% \ͽ)d]r(-o N.B~@%aqw/4Wpe>ˉ8dZr6~цoVx>⟕c#3s9;*,Ϭ(+$rR)L}M&^3 `ʅ..dqKSufCc84Vr[ӏ:b"6yM$H Ri#k#ZYDW/,f476t.1.YEEߛ_N\eAZU"<օɇJ #^NI'[dأ:k,qzU] %jP0@=ðX'y!P2ZDQ.,x=ᵐpn5uX%3fUV8Q +^?,ā9*l[>P6AzS`P{n ZJ|xcV $8|x9ݮT}񮑧)(U;dbyIݱPPlxEՄ7 GꥼwV'|,ثm;Fe H^( ^M}º5f !o^"#PR1KU%Wy$|ŋv 8/AYkygwޠ(XxW6ND |?q +y҇fBJytVbN 뾉TTɲ .+kVl>2~|Ǎ_QcM ]vg1L(eQf!/ЇUddUai,$ +I+C2ÖIoȸ6-װSNcZɺSd2y4ΌY|`Ǩ~CCj^=SD fMxWטrhۨ?J&Uo|NH4"5 ->܁-1?;vZQ蝍&{m@[7|(~Brnn z⿨.5\v2˱Neu7jnmMR @ %uP ŢfĉdlRT6agUo|IҪ;칊Cv< +KXԕv:Tr~Ƌ7S Ԁ" ++B|vSt"70RЂr0v ;,T̬Y ׉? `Vk˧$T6*tˑvK$s}3c0縉ȕ܄c+m%?=ųɦ=$:wJ@(RA7"4SAU@kB)v2uH5*&dWxf Fhɓ?\kP'LE.~5ch*Ayw 99JZƲWD}N q2nҒ+":%juxspDeI,`{x2O~*ݥO²ksBcOiAu#No?c6y=$z =:H a"1QhGwJA*0tݟO`,%1ư$ ]Q ؇?y)*/o?DF|NCkF!#͇'C; <+BZI[R^O Q;O\6x4̀͠YjtԸE[ϔB|^\0G6Z'Ez;PU\Ξg 6x %x 8zU}s4EN iQ\!k, +K8WQp&>%osGM"|rq Z^8gyWy]ym3ήJS\9dsfX,7=x>/t^p?H!i{iv7dgW}0SUi Pq 2yQ w͐c|U3/NH-dxm-dʗ F{Y3Afc%7_s;Wr(y-#&abS/Ju0T80cr*.F*zc⾚>/6Aę;O<<[@TsO:M%x0p])vDuOTqtPC:b8G?˷s]/.\nB4hQa=]]B NWM}\K輽#crSő'M ?=?v7̳93&uoeiJ70^/*$P\(wES&+V(ϋ\X<" +I~IYߨ^g/Aԁؖ+*@= o :N;#LhZM㬹mrnM;.d@SOO'P4VVb`Kӫ{6?GjUB h;[| ZWFf*r9~D@Wd7"~[ [!ꭟY5_x1C'uZ_>ਊx+eW}RӗI^p[EfYtWmSf{QeWYx'C +K#yYQmZ!4D>`)ɾUBb8 +7y\sðZk7*C"-n(sds94>`K0E)MSr)Ր^qI?Ss>DCA,V 8G&{@UdvqGe_]_%JJx9*="7S\=AaCZB*Fd62ab nIK8.P4xM4MU|>ղzߠ\'^*&^]wI&Jm]u2=ט}-6HhrҎ +кwC1T'nl}ԫ!wPo?ﴺ~pfK899շHJvj\ ƱLQVՠE!g1nJ9U:y7nD;sA&N4FQyxozҥڭ9*OW:+Q{,rGA!눅.7zk:&A՚ݢ,"}+̏A6AcAҠr3ks6Sj9a$QL5LjAqv<#omMr?{3v똆Rb*G=v `,4sg)AuIӴSSjQHU9ua~aN4o`I4;慗hJ9zdȈ2>wy[?s1yEև@ũaI d2-d>Ҷ1O(lXQaZ9n=O ;bZ'M VU|"kzBr2gYƳNj)37rI=ٰ~.XLH+\-u4͑w2R!;7Ⱊ fM!NJldL,wtGɁ[N:y +:5R!0 lQզ?ٶ¿]1&>#ϫ3\Y(*HV4TeuVgìY񦶟Lu?A3 (ߌkQwg3uju^4RvTQwEEex9)yT-z#.V|[}wo}kZ |~^|TQֳe|~EW|9@y}m-~ +#~+xk`\^SyQ޲ +Ej9jTm_oOq~rit訯Tw@Hy3-|klqY rŨt+ud}vh*Cx0RҀy=Y{j(}pс}S}+h}}<}}N}}v|7~R}f~~$Qx~=$b;)Bm{3κs{H{|Xz|e}8Q,~k<){yQz z}⎬{y{e|;P}v<~ӆ)€x3 xҜR?y_yyxzdD{!P |Ս1EZ+Zgݺeg';X܀ѵ z&r|Z}!c<ڃ Gې@":f8^ퟂssXx~(gq7ڐ`?+ܟj}fcHRDg9pkk|l\w,`NF}(p2*mם5ypp jguӃS 㢚щ!9|mNF G'A0~յH=n\V1^!(@LmY{hDMOdTBS|`= +fډA[zW4E`2DZ8*of?Vԭ%RmX|~ +Sj/)-e4ה7w)oE?)]q.* 9Cǟ(X*:4 )I@ gjtEZnI9YSrK]C[XD\|Ž}n4 EfלfN'3m#^|$z`pn@.R/qs%<Ϻ\ĶNxvq3vTC:Ho:ӣ;w ϲn  W'E̎p-}PԵ+ sW5g7GsINV?2-Qw<)h,QluG!ԬN m)re0(=,dMXXU5\Qtڷ [L+՜/L00NJ| rTbhr<}phej3fWڠSH9FM40W[@l/Uە9ʦϲ0S;ka+kmƒO7)q~m8 9WE˾g 06ՙ¤p֩¯?슊upt+BfvE*a8Nydf&>%dH}O\=ңlVmd(ۻЈ5./]p}ejO93p홈$l*(DP]:4Hm,}WxrUĚ{nz_Q:Ȗg \p@P{n#Ga7T`ɃG"K;^m/ӖN%Oizʱߎm7a ~TDljhG!;y~0 ҅[%$)zm֬j?`𧺐T(GaQ;-0=!%퐄 z(XzZmȫ͙O`ܧ#3Tu6GѐJ;/a0d&(ŋdzp΢mǫM`Ҧ"S}G_;Ɛ:0~,&Y6Z?zB&m#`}eSˡ,Gb ;~l0wӎS&gN\/n$o`bapNW:qhKr@-tS5v*Ww y{oJ*vcsvWwYKx@S +y5,z*ќ{H!z|}ob~ c}W̳}qL]}@}5l~!++~!o^/c~hWѱL+@:P5_+v"{e4:opcRW개3LJ݇@"6\v+왅g"蓯r'cĖX"BLpmA$6Z},n.#yi+o|dOX,LuKA'E6b,#˅omdX+y3Lj{A6`4,jb#Α ی@LrksmHtoꕪvqwPsj xupTwz|yb)~f{(oxaqx煮r{ysz(~pu`zhw +{Szx|s>X{}_*$}~smnʧpWsq}s:guRw>z W*^| Ϸjܐlˎnp^{rMftdR!v=yB.*|Hi1E2kE'om2;o{zq.fsq4Qu݋=x*{ƅ*gj lan%ypRe=rQu=C=Wx9*{Zfvi'ɡk?Rm_xodrPt=+wR*{ɟf7ĵrhrjllƫ~xo!fcq2PIt_ՁOX=i=biݖ>2uwe V9XSmɡ:h湿\#FxbPpD{MN]KU:񷅅.7<n|JTq{IR^h*}j1Hsr0 襮1ھ /z~ۢ͢ +OToq<:p| y[I%V +⍺mТP;!RmS U,!%ҷt\uz)p܉8C iۍn@L %wK3h,=LTlIA- _wh@H4ܷjߺ+y+mrh6J "f84s\gyjpx%1lw*iݔSGӹ<'n2M[Q¾`@O_su9;%\aY>mB?T/ME ]^%Lf\wQ-eݵBzlI E EYu=@x&(8sXU3/5=ߜ1?2(='- G ҡžhp"zx&h+l /bKⓘې56_?=_гE!D4:&%] _D<۬aM +cO+7ܓ>>~awdlМr1 yg])0D/0pXg3h̒uKUI$RN/B*R7--RD$p ql[eǤYӠMB7}-m҉$;@‰q@eyTYMʜd]B_N7wT&-lo$Lg=f ijPltEܬEnd;Op1#r'vu>woz;fʹpU[qBQxr^F%>s;u31Idv'xuLzz,|Ef׷w\xQ$xpFQry;,y1z(Fh{} q~"fĶ6#[4~Q*~sF~~U;~q1s~(-~ a.fɴɆ\݅Q>ȄFW<624Y6), yn=f߳\!يQT׉RFXզ +z+1\ qSQ#>VrdP'"䝭r(g7\RP)Pk;ZKvH1MQZߩ0)m#;rsbjB) "oKsMYĦ{=Yutl%)uj1 ^7jzvc8~$$E!_xOAAGf)>[FKV@MYXo=q/QfMlG/ݟĒ|.:䥞 ^V\ g_*CZňax%dΰ:$i/gPA Ƌ۵`Jí OSs_xZ蛤[KOhP9K}*}@$4o%n=TFA֊'ט[,x@AoE)]HŴKx  7a*).ybEXV=f1(EGdal^+bq%$UL~`s|u~(v`c^Fyc牋l)i> WQ?vh^'Xmdb +{~6vb\띊Qd9@2:cQu ɛчU7w=}ah#8:Vb6R2BXoߴMqAzo6QwMSea~B1|ݭY?8sPR}֒22뽥\2'|F:$3U^d5_Y|j0(K@<!4~P'ymgZ]Ęl0f/?iLtd:NTj.q'ðp92Nb7N] ]EΔƟfO"t龍̷Rq҈+WMZ#6]SCgs 5cgiҗ0D eœF&Wg)>7m-8q4 |ɗJOtɣmbnƺݲQdChكˠt E0`ɋQn gX`z>UU*{8RHpe)iefA$AܜUje +EE:޽_w +Nʒzx`CR5F=o*d$Շ}qb}Õ>l%2)N,ˎ0cߌ sy>0p(]Z?rMǘo)xc +bTuGc\ +YP *U;i?Kc1VIyk[=M*qbAChqI;/ʍzv"o)uFTX+?M- +WJ>F iCU[mz1^ 1d+p>Js4븪fĸ&QW!Bi׾rC-;0( [yneL#%]M=n&cʏgJ7=bYjin5i;-[F!St`j!B =EnuOJt|\ou:" jwOG@,ԮGZ({q`w[:A +9 ߋYäLvND2c3(v& 6ܨ$un9Dkkȁ:?aoaNRj#s6pLz ~x.p @A;Aqn.!5:4~ڋ #[G" qAҼNUĹ|/ZDoN _"oY>$FFn +da)p{yr2sUї_-"s2yeQu$A4I>FYl[>,3@KfA=6ǟD) >cl$I{Mv8xjZS ƪQGMG^֤S]H|٩k4HP{X NR #8$ؼ{=Llٍxy֡i,_g6 +T?:X\unTDw!F7 6F՛}#vL`LyͰ'A9Nt}/<ֹLHhmK"gȦZjʹںw٪r#ET.}&Ljw:wYav''? m4X${ 8m{'|l"r©u hO |Wjv8Yu-XG]hT$4| ?Q V@뫿)5T +Q\^TUp-PuAlyɉaU~F) i 0Nn"׼CӮY +uQ,FQp\>eP?.j}%ޑ^ ;~BOtwQ_/;R"g +'%[XѸx0R:V\3eTӜAL?i^>`W"p{NyW\0V)ek&Ɩb%M؊ M|-G0(<)H,O}FW/_ێÌސfĩ*+ҙߍ|8Qp/0wڨ%ܼ#29gr:Bh, 9.9<H DFuIHXxVx\B(59!!S!vՊ 5kΫ KσQ+ R{,!]>(>9SoOno׹ +#<3YLMHAUdq|XHsSCXU,^]),ϳs2#+FԮȩ3*tx%@y`{7 viϋ5Բ5d lUㅘb*N&ڠz>3g/A}ӵ[6,%6JakD[7ûWAnyʏNI}U8ҷ,s\8C h${+Mi% &%!ՕP6BûFs*房˨Y䡲 'oΥ2e!QAx$#‘N\L%y0'YͥM[l:9!¤qG`P$؃ ?]ԁث OlAqTdÏ%D84S Cd佼^ @V֪砞WxP` vmʠJ!/A(MAӻEFҠ{Lb/ѨbZG$c8 ώeзb,dW"#_(pl=nhI{ _yfڞW"5/Jr艢Bgdy +ayk':Ku]Jf'E22E-ZWشZ pFDq$2T +Q!CQQ9(4v {]~i/3r8BBRjGX ;K^d>VZxҹ9qN+:.JeQ\}ua]/oVT$~cQ %"iI|Y[~x/;o+S"-$P>*ZVTlrnǾ( 1D99b*xOaS5[$YJPI~H-9B`ct +P<*N\KJ0#vF$X7!iHsB! +ף.RTjʻ5>xأn}򩙢F**@b)Ir1"= F3)dK0w6ƽn.$ ý]ÁR\mތ#kz+ۺZal[%< V%I ++KtqdtPHΣ}#˦^G`5,nz6 +))13K/"|A7` +(W-T)wRZSU00l؈EeaoHSp}num"g*FGWlOD +_fpBk. '|NW3UjSK5 Eu{^r\T7GԴOI}'~q \rvL5Fc to|ܥjGHѬx 'Nap~"HϘMDrG=ﺜ]&p-yd~9 '^86Q5Z٤U:uKveTժtθYz;hXQv+6a<-LcGHVq`x3??U-$%9l*) UYb7ykRb L9^f2i[-%kHVDuyhMznttƎZ/7Geu/ҷc$WyiBf_Zt*\`&b{L.~xmJiW5  +jOUisOF3epey;zPeUG!dĥ5X|09 wM3B8/jx)͘J#6rT{J^pC̀w;_\GrGn~ +ؠn Z8T ~,MoU.eјソx[1lTwUy6 i'~:#կ5oquj,\AgNÒLw5kd)^i`aB&H)Zdax ( +"""9cvLjevtlSYMnf53eۣNfs'հ3; @H'n;!k e}#c%o\zc$!&Vȵou偉 zeDxI] eO"M?+1[~<ZD2 @=<NoDPu7 J'7{J 7v1c4('g`9MJaY'Xl9]+-ϫ5,nA{\Լ:f}sXZu]D3_YM7Kha]8-C'C~\(b/"gjI:9J[,t'mаU<;T~GkYی|N{;p|庘o&a}Diڻo*U?jlP?1MU;eLa̷>\|Aj2 T7)<* +21z>{ Hm~ 9QQbneW&t/Xn\sدԎ3uٚFsDNgwa!Ku DnhH3l1`L +:n`NjwQz'JHSE$c0:RxYa'uyIs[5 CIkH=j3DpYliJIqԖg9bP<:kuYtFŠuߎI:YkIly(؅͗~"5I=4IGǚW!<>mX:fack +Yxݘ/WVmE"Y51VEĦ:\7ѓUq]XOa'^FYQ7)xt`i>zؼQe{45j'j xEƑ񹞅4ԭ-3*GtS Po(Nk7mC6 CJ匇6 +:G"BBGWc;kC z酗yTn5l&qy9:^F|wi/ /fjϨN^x^A#ۙHUwx uc{cޕ8osßd M>e VMwK^~}s^xm[gaƱK^%WP|nQtȷ2B7MS4D55 +ÞڱRj*\G0uzl|MY'+xeAGrh)vLbyZ_ׄmq]py ej; y2r$ *uƯ |O2K, pVb>VtH>?#VE,6)hs­d.k6o 8ChN(AI=LfDͨlиU k60,1@|wi^8Z?d IiFuS P`Q'}5 +ש^ |s7-Ow!3JFGIWJ.opN*)$f]-P̏s/柕|MGhRW_%AS&eR,jSlF|>;wJϬh@-5Uo $LkQ柪າp4#4R^  -. 4VҭxBܥvہ Y evԔs4龍aߪZ`\4:z\hPY2?TW+S̎0eOg1׽'RύkOabSW$17O|5u V:%KN@e){ {fMH $`@~E,ny"WXρ(J-ρv?wߗKQ*=Kc <@] +eGt$3yo?jVڶCT3,~n OUGa}DPHm] r8 +ybH`P(|}9N˛dg5 T8R~Y)n( 1(.`cvd5KZ'滁DŽrKU޾3܋P">XX/3c5 OrxomJTt\j%e-4Cc6˴:%K1د&-ttGa`I,sL,^Pé^R~["OK$ !oyNU"9ltHwun%*NB 3*N2 y3@ΗۚKcɁungL_mC1vQUr4>@N%SK~<V92TL6I罫d>ٜ%w6%]( +V,Yn&R)Z3i?cwArE(!*yqB}gR6n .:.\GZ ?.^U>Ÿ'6 L)( nm (G#*\9[1[=j.PW-xv3D7$Rl&4w"1 KxP:TYMl7"R5e[~J,ʫkRdkIZIP!xDkJcՙJÜ+MVO3>7L'H#A_)7ҦZ }TV朽Pbl<a~pyfa4)%ӻ{d!+Z'KM_JgP5Oe;E -\iB"Q@&Ylrk +m Ium?.֤La1<ԘSծ*˥[~@`Yj*MQj+(.L!= FG[ZOqbi8LhehCJGr0\EE{ eT.@ˎǻȏoT ^1_# HEREF虸^> tU6+ ծ[ȮK8܋q&0IʧUXl TqZ-Veh^|"$0\LzS?1 +ӯt԰h'p-I$hh>myS'HtǪ/.T`NbBLZ0 g3 "Q1B(DjsոcTYh^ zIDs>{)r(&ޯjy*NzNv֍@:e\941 !U%ɧPMMS N1jI/+}m̜MpAMn29j|eQ6)V(ː1?Zxb09]&oP@)Hp!tEY4sÔWw7U[O5uբ+Tt!!r$`,$(GJ8  ?D) "ZZu<]Qx8"^TwǾwX) +SoS/d&yH“Nȕnui458\3DңEXZ߭[wUʫwĽRz:(A;pq1F,TuR5twMHyJz:9&t&8aW +^i~N 17 [^vHG ~>7a8K8S0s< xZ :l.S\!:HCP6+nP[Ĩ'MQĻL2ا ]GP^@8Lu <Ő]be?UoE$, 9h`c@ߠPV6_r'i;/$7ƟmӦ'N$ p`~IK8'W#9O}xYEt+l/ X#++uɎE5i7# +{dErrnfKRr +V]  Ҵ:B+*rlEFZMUtePɏzA)j^WWD?ImG\dVcmeTFɆϨ3VuLJ4Q-ݪ iXA%o$IN&FtQ~lF_ dtʸ?@Lډ@>,Nw,Ҡhc0%W tV0!ډB%3( 9 n$ YgGEhU&&Kdj>?dw +8 X{vA+ V3#,)BNhHBCckYNP. a!0-g߬h!=dǞݑHێKdϚ哬[/ɶ,ǽPnmd"mkkyqJ-[mUyavoB{U[ԾO xʫdL:?VGbv-K-SBm[}>xCJy&;j$1yȵV?0EV/gɢN/>chxӍ?TZIӺe's,2M$@_Ѕ5'DN޵;̿'.B1 Yq|efqnvyP&cIZ=z(%Ę8iM7P'Ĉ r0{?1DE<%[߶.򞺾k&D?As A6>gm뇿eJ D8E:9:\AuLT?d(2A{8,jٳ~ṢOإAC5e7/gHg2%RT֜ jT%rƦIUnx_!|dig^Ws-=,iBZ+Z7*ET <Jv|9aO1o\캧.HUuѴm͓שBkѐ GG/-|:M_W֊g ^\kCXꧤ!Wa2dJ3$?9ń~=3u"ILDQ)w+&.9r]ʍܔI'lp8@h9Okb:C)S,|kT)ԗ`uW 4ih#^ }OP6:tpD?#eU:OּĨz~EdN(ѥI }/o9uga?YQ"E!Ilb9) L j愍!2\|ĢTfqt0'ˋ/ZQ6 $r{Q yِk`6fUINq-LO~"K: iOV8爙j]imF&5@^T36+^ibHs;Sֹjꁍ*$h ʼnBZJdS@4WL"eB:{^6 p,f| Agtmh-܆E㕆Y6xH%\aN<, rŚB .![ :Xaw к.umZ5(7sQNuZYY +^i*yژ6 +~AƢ+V Շ_S`a4@,0$d^"a"D۫VړZC=O|_$<鬡N˓J=?9:`z{1*M]@^콠i)nqH'@'4BƬ@b9zH/R~>[Q&U˨n|C4?B̶Ld{}Voѯ酊5Os E?̉9\I6|q>=hZ듦:}<^K+MU5 +B4_^/^lP2cxFt>;UD,ľDgvpa"/wQ-a]"Yj ?+gS9U>=ׇG9QMFRGiIRCEJ) +C2\3^A*G8~LUhB0i U`0TTSv*sJ!<sK5&[NˁcV[|:h2Ks?<  W]Pd0k44!^kZ]zdȳt䌺.+X&5q SͿ)APʹ6Yhg-t z2ϖH(3 9L9vR)ar/_ZzJh?jyЛR.žua5@(Aez*Z:y=蠫zBzb/_P=G$,RmbQ=5\up+Nr 02r\]s+Q)u_ 7m,y>-u|/uYrg5mIQ+J'iDy //H^8l\ +!*aO ?CPP;lij=Y"uaV|REޕZ>}+[ݘT ŢĠ +tYΠn9 Xbm{j xi{"܂oI-o{AIZ>=>FUihs0^k 墴XXɩ#UAyA)](1܄plΈ פ.`%G6G4}Q#w+/( ڟǐY-#U$֥*ɸWvs's'(h +uMĂXrUDgoU1EZ[ OC"5'1{6K2&h}ŏnan,2T%QǓQ7_p9lM3d{v%4^n,"X,wK?2ΙyG6bzo46 +&pɨc[I⎄Kbٖ_ͤQU]afc$]>ui:xBs*X1ЅTu}XJ %ʈ9y[,qZd #iOУ₨@X( YKDlzScn]x ~R9B$bmxʸS)5)vL#uF =Dsz51\΢bP8_Z!} ='0f:*HEx3h GF!ށ(K;CA?02*W;sȏ]_O#2L{01/o!ҁPt3 ؉OGm9!q mϩ7ϵ>/xO4G#5$gIˢ7kLNց5Qޏy!݅E)~M{x|j|{'asdxr(2\Jص0l+oğ7w (ަs72u#?,JOS,w{̈wsgj1;TU;V`A*d}<sy5w&tl)׸'uM$ ǖ(;]ݬ+\u +MFzl1aۿsKBoG;83gF7|'5y_G'\ˮ.*f4UnJZ@]/u}I`cy-{tnF-fG(UI9>S#$$ON'D-(taJ̻Cb_נ=YͭxkE}MlR̼#(: -̐ !ܥKG4N3Xl]8<"$azUU ̤q;սu{D:}L7oɔ1׆ U-RwxxѢ_)=xQȆ@wW@j4A+zǓm-[ITYƭfO[9]ǣoyڠzO1Ka;ΏUטif9$Y^'DbL"TȰqEE“.|yGhB[ j-04D}럪`|SED5JwMj/{C +eκ-N2_PvU`o gA::ߕz\}ݼioCUГEv B~lk rKU6QT/,〲@2|&HEHFM* ETH)V5b+{PBsIh>abG=9T3}&` ;('WSʀ +t28YO-ȗɪ $7>nM[w}hPU|ѽ_VORI#E6Fo{:P~RȍZ~`$7pH6OX04HThV&I7T4Ļf8?Z5 ngߵ^PBf$C)nIqBC$rsy&)=؟'a:5wJ4-V + ߫˒5Zv^ܛ9o7lTخ[liy[C5 C_*Lu<н2}EgZ6~kv0xҨ1JLkX̹hfR&zi1lUXY W[]%1QRK7 Nf3fV8z䦑Wt¹Urri34rΒ⋡S8=_8{V>ت'4_ʹ# RvRt7\C2[pTq>nY-*55Y4:p00olbx)Z'( |9eIߊYF^^H Pj.:/C0治!`Wkr²ɺ#C gl3%bY$fm|O=)) /0(!CָՖ,> aLIs:ΔC(w8NpƊOxFVUu1zĥvlh{\"n Jv8%3*#W(j$8pKjSZ8.9.k=kDjY}uʖ-V?f#>&)_&Fc s/;u?q}P{PE9}ikK9DnT59[~BPNJ(vұce jhMhuNd{]F }DY?6xur֧\ZX\xږovp/dX`Ն &/s[rIi*v 7RSgi7QP(p5w[ cp}Q~.ݘwDDb˟lhvP7}s2g^X ,7 9|H1{`\2\ӊq^+g9;b:u[ B %䙪#SHAElV$^2j{'i=邭SxfB1onٝJbf\A, @'V_ AT^)*IDŏ;MO`7^mv@&þ.y"Zޔ.ZW8Z]խ#FWmm9&F\۲9/ҙ+7V6UV9 !tW2*{W}K lXkP:\si^uCAP畇Y `:G[䬄bDu]qw sLz+'׺Xc0aUQ{Qӄ-*$@7R(ɽ?q"'FOV' 2'~ +V@QHkFj"4 s{ܭB\Yi}ɘzF;jY֧N@GB(غA)> {N `'{ȾmT Ȓ p'r+!m@vb#19';>|rg`"^4HIfC$p1aIt~ljKuⁿ }$AFnכ(^/9n)ӆko] +k\p@xG-U+krǢUjغ @02 F!H$7&9}-4=CRHGD{ʾvw㐫 +L1=j?8E*r9,֛'3t| m끍VA6Le*bSm3F-gCx[K۽,u5o Xi xCtT}8!7r!}zs Y2ɱ4sfct=~@^l<49 ^BYzɔr?bC3'y{H́M˜ :bAjW!]!Fא>~v};A5计vP$3{1<1q+D,2fwrvqV"b\*7;LOº`hc(~Ha/7G  Q{z`AdRg& lz_w -[}]k>ݹL[g~??tG _gLiz.^2/_굟v;wC?,m={bN/]n^ >~PD1X@$CB)aᴈ(:#:&6J`'r<~RrJ -=#3+;'7/TXV^!KuycSsKk[{GgWwϛ^m_̻ٹ K+k*-[ >پ]Kkç;wؽg^` m`]=`&(*,.0٘6gc`޼uylg셣3BA!*-d9\~r -#hKƍ@& @*:0c@4nvv~o,5׊_ꬕzUlfj&cbJ萔'Qzby,A(N-UTKjeaaaaaaar(e\8T㭧 48xHFnRhGڋv0JWE^K ' x9;pSeZcPd-`O=[ o`[$SK"cĴ8nJs^w.-{0Ky75墒۩NXaD:ئTi 0[V8dζoh>_\6,zq^rCjP[Rn;pvߢPیj';iВm;C`?m+>ĊC-1es~<|Oh 5c +]|Y.ՆLWg}N"`6P/s{uWe‚u{.. MرfR"J.Olks~臟 !oLw6b*('j#5M*ߞm @W|п.`ggÈkQTa]2W(qSbU`^Zn]eF{>sAgSt y+G%]FL`pl1+5aNP$~Udݎt퇠?,͆NOD9AQLA4 8WLs 0x^D͓+"bMkXMF~eʶo/Ǧ;?N&9={qL'krS~:Ozx2iH)(4go +\DP$J;;Sf0T},,mt䂨LɍhYm_r|_}hdoG4Y>AZ>N\#@ cV^TV˙Q"@)ȶ տ> +L6AUئpH}Y5Bn X&O# +D^S{lNCr W2Bug`y +LQ"Y4BL q"Tҧfh8 ٲo_xҟǪ}CQʙO$ &6¡tʰIfh: t7.74xt/U9a+yD5'`Nᑴ ":NRhxRDLqI~3!)"fs!+僚Bɻ?O~߸Z{NQ+z-b/, +scBaT4CmOi&;Ab&yl۴̄n@b#ϕfZ+Y!XyUso^ů`Wp˄%R)jR\l)G,0* l[88 q*?*O0`eن]Gmn an7qa b~Sr)uc3?fo}ϼZr;W0 x, }A?@PuDԂ؋5 jeHT/g(z*TM=V~fkaEq fTq(c2=-B-wo'N'z$QF_k'm{=Þamk:c%޲Up4\~jGpAOOj1̷Q#|xhgdafLdJwێA /MGͧOإuĖ- v@#iRIHU(A~XV= +;&mA{4zn+=d~qŅ[,c3e LzR+ĕ +рT/ʨ0 nQoE^wrōY`n-zjIi2Z |NL#g^ yHjVnKij+[v Íkw\K^E/yP5%“n2n`Pz\NM,rܠnmI$j¨zm`ꆳUwE/ܼ83#B鴱9>PNNcS򰡭KD5 O#˞|ȳsƫ3!LD'YPcNۭN:vkV+QV:t]9tA+B/BB 9 !BB,7$ DDAu;}Њ;i EE]m7 zŬ[ .S&` C͎ ~d[?\;>)ILFC`7g0!'Ey0>TQa +2O(e~V3^0ݐmk5Wξxk# 1ܱ(9;MkYT +ALأRF-;ұzt64R~Ҝ|[P`s3&s͒߬PiQ&K6Eb^H!ʪ^.96&{OZ1ہ! (;:Mjgٷ_0Mĩ'T)rCBg@.bk%YV< +|-a!mWc{`'A~Fsa_ duEdQAq5,2T|^Hhզ{?k/h{mS%7ϡ/:d&[-S<7ZIfE4fO3cr&mTâٔ3j@ O_}/0!8<%KeTf)}\^ǕfhfWLq I +*1RM#FtDsǍoϵg~k`(q,@cy3d }~XUIp< A~@XY.0+,Hʱ1%SảeX XW$ޙ7ͫ'4[Š:gqrF]'-O)%CF.fW/{,(@%pT\ +9Pm;曾3Dz}P Ai6ȻHEWI*'oR֩wggU5Nb*MG`=`eXc,S7Ԣw)!~1dLaWb^1:)Uy&Y̡tJ|j8BJ_1|^WxПĽ^͘:>PN+39Zȩ;u*̣ɂZOeK6/79YvaEoxRua;vp7~_` ^o%VvŭfAPIU5*E^Q-,T4slzii6^̗~"fㆴ?Mf}4ҁr tIjr *ɢE*7˥ zX- +ZQCH!ҚuvC=m..wvpt(jTbTe7ۙ#QHvf*mJ^U 0NkooTK Pw >~H9S=A(&=jIu~ ߧr$^[K} F5w]Rt6=ݍGoyg˦QIRj=#\-fq,Ow,wөUloDp&n4 $(/Y|l)c\gDDeyra%ֳ)C.yGl (P ӫr-17rmXk'1l>\y>f;KH)`Bm*gL $(Bt+#R@z տQ6i;*_WqN%B|0ѐe&`T M1$ o}<x'@x2$Rs/MB0JνdžKVD~;!qw*AB$["⁰/JM>/ 8"@5rX @!?S? +-19I>jFh>[ +2jy] a2Ynf{ ye ×/ zAMd"Ak NsmIN-.cb~]0uA7Cv0E=5#y/,hӢB$'8mqv/vqC.c纹 CΊA}y=%a7XS>N,[pg̤O jN#=%SS>ƿr<^ xC&ޠuފ!/t5hsV(NYiځoJps;g!'"")nVrM>]A%]FKT@/ .|EӃmYǞ̔.OY)fF^Ry吔A]7 +ZS\@ +7Xb<+KbEʬW⿟.Q"Gġ~FNJp(N.-JjD͢I7V4S&rJ^7 1`S[Ҏ'5|5o5h%%]::TqZb:Zm=Z a KB (M#E@Dy/xN{|IS +6dAU)WTNEJU;}*_o ` vI;jIfG&Y'qG*~L=3Plh.YT/h*uBժq({%JX٧_ +l2guϦn2'xg.0aJMt\X -ח*˴RMXӣ+R{Յ>5ho(̖7)&]̃cnw~7bW=R) *f8<ؑcl*JSw +=]&_ۧWcƕ7_Ǽ?m@;f~kb; m]lo`E{j 1XwXⒼYQnj\:[kj{e0ە?-Ul8y{A+vF`w+Br5".4BIoUi͖zr[oњL^5hQ9h mX}w&aۄkI;E<Кz3E5ټ +eL/їZbVSZtʢګ7j{غ&ݤ;_P#H9!uv"-d;S5;B]6[]#WPII+je^2ǾiNꞖ!ц~rP=ՒNVbA+bnudMXVgf; rUfGYU˽*9jУUKkZR6 tu P7 28S.09]!MO|U>OţP +F+B䞄%V L;Lr1;}~y0@8xŜ3b}wcuǯmnujYG̟RC >&F$ fg҇TLސ,HctW;P3j-n_6ҎAc7׻|jS?/LSOhWFeM鸴 131 Ƣؔʃ~ j=,UA=֡ؕ|㔀iW 1H@5O&gafiQkJc!'MDD>2QB!Ri:`=՞D;9N!.™, ]"h$TGb3tARʔ=0U70A 2vz? qN`P6b}mJH<&Sr Գ,`]LeˌP qiIN|f-XJ rw#H&1fruKQ[S1D:X@8 +S4`H <@z.@{!̗H_cH_pu{&\à t*t`}IL  8 1`W/G:V ;¥#H.ή¥N!@>' @<ΗU ĀhÑn-C.G,×e! &\Hw5a!8H[ v`ԝׁI$0w]{Ai?F@5ӯІmCtpSd4=ҫ/2 EAA4q>7urRfؽiyQzx7ï ԐP5S`F &· 3ْkK"]̢0 (IW\)64K~qR>j;mO ٨Au~=7C.fX> 0S~xa`-y! r9*8'qFx?- zj>8:Zj{֮xڵ=Nݴ/r ry@ !$$r@p r$.`Qx8;|<<&>tw3u=Ei' 5W E4v0$Fux@:a] + y&ؘ=+.JA r8˝s<?QDAz%տ +bSPJEK&Ec򼚟0O2zB@l!O4qߟ71n]d"C>jPcC@Eҟ@U,هT=65oȻ8Sʋ%)B%//%I+(ĵ9$ax=IG8q&w!4!SOMYs3^7pE*bw~KVHV"4dBXL% F rP qo[fv9HCu5cV֕aFNT+IJ-Eab5ɪy.i^-rHpt\dGJDžřu@*.Wq=?|3#y+x7: M[@jVi.,k jYlϿ#J"Cj(C Uq?Mykacffo#9zq½nqerR}I1NUƶ++BJ&P f1)bG  +Qk`Ӻî]snV6#ӽN%eԗjuJD˩Rبt#eDI>/$[5(Df#ſMY̶Nzh:[\KDUQkmFN6 S7"nVu_-!b8^$51/_}ҐÖ췦=]]}q<٧<͔MNOj~ +!V A.+ XmZTIj?Ij(F &5,4>׆$ 3V2ޕ{^esU2\.1*C%;BGZ'-$J)jPGfgלLak'M vw`_==lbj݌KkʪiUTTDrs-ojHMÒr"ő jļ%ms@֖{ɻGR89onVjYqJEuLEo7 +[5 [݈:U~1bѐѿ̺' c7L6ܹJ//w2;)6~zdy!iPa'pڄ )hš-נDhp:>I$T3AxٛC䷝-Ӫ|.dPs14^$Pn֡COc- vG>H-g΢<R܏::z\Ԏ:T[VTl X& YH!$@а/B**R @Q3qTAK\ :ܠ} ܐR'D0I=981ι0f5Dz{9#..o4[AҏυL[E\ ٤?A6#1? 7<ĄTLTXBC+"ZH#܎X2r=.06${xAAGxr+HF_^wOըS0*5H +#<"GVKD]87A|/oz_xhm0eAy|dB.q9ILp27a׸u`L'8:(TrC#%-)#(K=hwt|a4caAԸ@xe`!3enOĔwdP+9ѯV)arst8%jw +[w΄X_Bp(Y4ly a4pCd#uBiBꦱ_G,ɟ ѷ=tuf?Ƙ¸R톤qMܜ&Hj&7CuP Z`" +KHkS73 ڒY8_jtdAw*MU6]|KZ%9)%ig]<`5k*lt字7p>`RҪ2Oc4iWO@#Wt=+sܬ4Q/+l MI zV}Z Vz[ϭH{u?8np + PL|kNZ>꠾;PHqߞZ85'kMZʒX(KPͩx?o|_/%?t` ֠yPJ\tI}_Q7ZU2ɈVEҘNU1ˑ%bEN#!nPo_( ;e'>$-|XX& {Gذ4bMn"[K/2홅W*[efUQ-W{ m c` 3 .x\I^!aMgu{*gi*-H%V] ;ziY e<ѿ)wGyp;k] s?[ҟ/K!r}df_u(e{$zdB7dծC ]P=ݘ`5k'ma4>;ƥ.b}__ıYLh-<] 7Qz&$̐gW&sT1O|W̑g( Q! >c=1&83\Z71=x\ŒqoSuhx)j4Cλ]S{/<z M,Pm8 GX g@tH +i'!1 IN!p jIbsxw-F/GmQ>D}I:H7ɁU;x-6@TP ա0Pۑ!yGP`b* tbT89H.fIL H6Hf Tk:0E +oB@!iWh@k9~Pn3U=n!TЊ7T؉;B +|ߕQ@2> z2DoF"6 H@6B7hxAVOf* +J0GI` -atl?<C2"(BV*"?O|i  /ѧ@fJK7HZsR֚ +h,GA<;]@n$P+H *-(v Uك ~3 :Zy#eqӌ8s76p}܅ t R̠}Ɂk 3i; m q";;TsDNW]ݍjk݆a{O|&Y>9Ak +! +31e $aT%h_Pl!Q`qSFis_i~k9#o9SYޘ99+A6GEao3ĺk9>K!bv0m|;ca..qF<%u{') u>zAYۘ.P0~ f˟%tg_fC4fa3Lz-Kՠ ȯ/14C3jTBD\xlOc+^oCf€3W0-̜!!6fI&h |̐>LH)Ua`a1fP;-RCil~dLtMK:ͣ-1˷5Lyآ( iW=PeᵲdSӑ&6O1E' Xa,$YN]e +mPӦ?gו%=]&ճEoJ jP QVDT+#ANV;EX5ȏbӋ 9/FN|:m˛"+m9g(].%{_P>w34 vY:DZzlQ Qz 1&%@)!$$R 86+H 㞳~osOĽ.Z};RC5]̲Q:bQ9P_dC(ِt0rg a%ms%'Ď<#١g֍DJ0ݹ:>ǣ2VN-]/gX%Q}le["z%~ǒEc#Mɻ9yǷݴq7C*y>{T +/m~r4dFYפS4lOuBOq\ CsU‘ 14f#cX!b?9s  ?lJU>Eur5Ō2gj֕TKIr")&*x [7͍%r8$>%dC$ِz3, &LV;Lt[1[㾺UM߮UJOk.D4/iJM"3s9\-|Mf G GoC>c`BqpjiOkvT[~}kC qJ~TYAh{8Y♗.J2SDfr=/]O ϣDz~#|ِ>ugZוZ ͤkJlU$/ȍs'{hVZz VKNkH#nr(ϗu, +4M^ಢϖ +ZgPYJevyN$ό Z8)VYռ~|V?!k' +G }O6 q{G).5V˳Z5uޛʫY+' +Ձab1PR擘Èϓsbs+& )i#%b{wL7tܳpqFUcw,2,r*!Y&,J񎓋BVda9!råoDd(G%gޛw+j3xԼڏzV5{Wh% ٭sU6ɕayKlig*'D).e7r9ANp;D !ȳզ; [ ie\wIvJn Lu-r +j .F8#@ ?r+|(nhXOW=PmMn϶virxBKyAsuDS]I7.7H<|屮x.*aʫQy5:}0`9y6@~r=Y:g@ӭ9 }#E^Y|6xs>箂z׈\T>W@8U1V?uIk:+6 KfXV2ޖߖ{}0x)-,Z3[j(8A+(x{mxm^ײuo&C]7nFF &N3siCU>C=ԁQKjN/^Ц2FC({(7t(佳2~O1Np04j6DG#vtA{8L]NtGu{פdnW&¤!h2fC ͇qȞ4_4}507] +#{=ylba C0j7Ĕ(늲Xf #>a6@E#K VH# fz(- ^BJ:iП1gW)PPt6l&d? ȵA nHB@L:͹H ߹#g+.qƀF`J;՗1 *(d?| \wN!`I:ZLWh? ]'逴YW99; +C[c+[ho-0kѫa8\{{p|5wqmuPKQFƥ!"d]BB$B!TPAE֕".kNx?w^ssEM#Z o=@ǗA7@ +[]@K"C$;ps3vTGtW`Or`KmR߭@ɺg979gŬSBq~WS| e8m>ȼ*yYk'ɓޏ> .&0 ¦%@qxac>r-@[2yEԟg &GFO \ieg_GK;֩f۶+VحI] +3ëD8 _. o eyBhFBpjixPR 2\4r-Umƈ?vR(fٶrэ +[\m)*sC*$>!T|P"'jůIYbDЈW G Y(uFƫ<:M?鮎D:FMcJVf{E!:ͩkۉj=Rd\=M*6U+xsҏ>;!F 2 4lkcW Fef=t] U`* +˔%M^)0׈/]')edUTl/[&)&9>8$]'UYd:!/Z+ۺCp]1H}Pw-dziղ"2Db*|^~ )7,{%˛eRD0D.#_2=Ey9EpZ-4sT.rVӨwdI)[a"}3i/Upv SȘ4ar=xwK+Uuԓz#^[Te%EH(& 5da(dYh""x?\ HsHWeۭ5b})DI}9u Ii˫|r AYa"}NPVKvIW7OkMsxo~>w{dy/~[͢N'5_nFе%2Ml|i\iVKai^5h} C i6-gkNRIEfi&{4Ȕ$ iAE}{}=}]?]v"8ZaXZRJ$]C_i/wT cӁ< 7#9zC-תF_-,#*8T{XbHͪ(ͦh鍮huet|Dt`/~%&Yng½3hJ[ εD,=D]Qޘ_/yU#~wJmީ`҃U»Ma0e6O +BlMzPH*[Õ?3{94kuSUxǴ Ħ\Oi)_qcAC?*}ȫWpjednY +@K.ܿ p.u^Pw3r^~'嫜,7u;⭒^';$^*Q/ROv{7هˇb'd_OkÃ[PYW[|JɾCssK2zLS{^xEXn#Ps)~v(\YKL_?0zm+^o lwML f́_]}Zt| E}렰~g+#+c%+_˔\JXS㨲,TǭBkĖ.UW;ƨ?OK swn<сpReyCplW+s4@X|fN֠}?z8LY48Bor<Q1BŇWj9'֏s-8F7GXU6ì:f{5󵃒ptg:C>&:)Ds J:F_Sfcr|LbLl:!M1gaXPiwZ-Uq*앜ZAv'vS?ǹYq.=,t}BO, "ؤwŴ1-KMj$H|HV-9a<(,((;?]svvs\q]r4o8[N $C0HgH$L۫!03ꋱ#%i<U|PcBS%/:)D]z]ܺO;|r3z5;0қ]QM^k~Cky@/Vb*Z8PpBDfHDD@@$ ! !sB L02ƨ(@l*Cժ,Z߿os~߳QIAsz0Ym8csgQK x<=JL}/ItFz:KmqN+D{ʈ6eܭ-eIp-+`H)bPICs>[x1a9޹9 +orw,C]dvR6D[IB|є£!G}FԦ;4f L&/D~O3g Wb'Z_ge1msKf&r !գR{אyHo30oeUm^<'3s`\xILJHC}[xRK&nZ-ؒ[ԘwϖT>U̧~|YOWBp2e's/;/.`(= X$rE nբ X2_'sD9m/aBg ,iB}32{+|, }G~ zEп:×b~h%n٩(r--(2/qW׌uGw^G㋆ o"_Ҷ$g|/ܯ=ʀ9WgE%lU*rͩtp"ċ_1\FW 1fA)P̖ܘb7viH@>Vè +W/ކlWZ]LZ}f6_2ō%qOQe +QF98S"ȔVYz= C:! K>3%Q(hVfI%<+[Aײm@(^ӕm[-M ۸uv |USBeV=3>i~ +=>UO?"0?c`Q&YЗ4,f7~e%4Pa! +LpZ&n %6H=@i;J㸦ET#;VX3ITM$Vc!?.`! K,ZLc){~q2=~@eUkבdӞ+KK'] $k.55VKq-c'㚧/5aF#/,D{r&AZnك︩o&0{#Vo^\KqGuM>kgܽI +tƒtG.49GXV/Ɔe^ع\+jKo( \aX t:v 8ܙ6#g{c/Ls=0gΧa*-tZ\ p @#880Q3 +qx^B8Ix/1!.ț(VsP߃X TRqL>Y3j/4BflG D^tH]Dz-6<Ȃ[06,1,0E%H%N[Xzyt= Li"G!;u r# + ,_Uȸ`uS^ }BoGё + L4P"}H L4'BnbȗbYeyi 2~ l\7ئ%$eCe RO ?@Á?݈oI%}3O0 cj"11D=r3W`pנ_UkصIV euO٩?60d|d&3} \OId;l#( pEi#?q4/ǩUܢ +Xph? F O> ?Gcq/MzXh +t'jmI: ؤo᫁Q(j}FhO/S -X7+|d0ĭ3m4Z?0t 7ጘL q 4'XA4Ј_sH Mc|1Qڣ"PxAA@9 F߇nb{h5o+!_̟e'7=%*p?ɳ_I E+tRa I:u}J^aQwx[Aٛkš73n|(=I=@:q-ė>I=67k89p@BȢ~hY$^;F::(Edٳ*N˧፛%Cn{w6M[ -*@/t@Shr ?Sk ɃN.J<:c3s: -7ߏYm l[">6Gh@_^$S&}M!_&Q ӽҏyZRS810^a|/.{3oŔXFW܌mjkl=o&]/W uQDy+9dRkJC>s#3_d#E]yvܤ5!âYw֦Qr޶!zo1ĭ{"wYWW1Q z&3y#BH"H1zNeS=<=Cz"ҜdzCv²!)۪>H/]ɡ:.A*ɔ :VOCD'x"#m}=tv3jmWif V6eDSMSS63ksmk +wXb|CU|SݥBkYKY<:ƣ*Їh`OHô|#| +=J[x:z̸[ٚZܤؐ-\ˌ3MmQYc[%Ϸ+OXTRF=I+N%QЕ +!InCHS:C{ ZXm}wEmPZ)6I8b]$]i_VpVQᜟzv:{nJ2u]4@_`($=ɠ@iC,66v|f^BJgyM`żh\E)uqvmaV}~fC^Fsnzti8h92TzT[ӞVB8L9CN(dɾ!'K$]4zTts]W>>zNF|]ruHS㽠$Ar%bf&'gNM7IJ0y {Vw1^'ؾ|2Q:xg5Un=h(? WSjTQ|Lk~anUYYgdFoI'Kv'ħJbSx)SZF|eg2 gכX8OwKVZ4Vbl%2nQNn+3rCץD%e'LJ3ͼhQjQg.i&yj&ym. dWc8 xjr5 P}ي#+W:IݵN.YZ&0bs|~ܬ=a9Ŧ_ ~ ~",2rdoWP2zpD׫梾f%kwBZǓ)=wn$U S*N/Kϭ+]5$ygh"fP0hPl> = O8ATe?.뗡V_3h=5ARwbߒؚ"/_V)2q۹ #K{.^12+# 56 1n*3eQ4ת5-Aa&#~FrZBVLөtCCV]ZsAѿ^ů>ɺ]>u-u=޵W>zאd +{p'h.jjio_kqi$O<3ꖻFMC:, {F +էyojhkֳͣepmʹ}2_N Z6Ƚ!ڹ];m6rݎC:u+h}zu[gjQgQokE7Ft1ObՠPQ;$.BX?p{q~iwg{U Dq]5.=:X0Xpp𙎰8&A"p=Òk >~mZ7v7NG9׏p:40q(DI8,NVdڎ>*]ug#5'= l]+V'?sd9F7g}|1ѯVpøp7wY7o#/oMSQԴl%ϰ%=G'@AN x8x~Vh\N ! ;҃ 0pb?’\#w3 +12K鲻@ֈ*䌨*?& 8L[t -m>2^2n!.:r6:-=:Mu(Bځn@X*\V3Lg فd40 SeYE6XNf) +b:E1!]@@V,XG@B9x˓0pGN'Ae*] +ٕ"~*g]8+kw?'Ζ4'>y0d˜,o+azɓc9 w$̘IyȾLzAmL*EQ#v4> ki9hD.a;W{@wavmk9)O +t~W昮2:PաEXYx]cTxK㓰[sX\pPpHpL +i=iۓDw2ld7Mc~ +\C\bf~qNQ씥7eWP*è("k@@P@D'iT;;!$@H  KRA7T\HUԊ`A +p\;t;g!d>]Ddžfo""nφO2 3|<Y Gc0v دc;gv(>S7{}LZm"hf&`tt\e6yVelD\x\˸q#gq/ Oflž096ݏAwcf]5q{XD|7RGzs˧ShFSƯfcr(5V OmG{7>&_{DiaM22t9_?.@{p}x,p'q'$ //D$ QG C }\[BVQG"¨lR]l%ʲ\WDC$*(HyK\у\a\BWH.q|'p ,pWh %pIa5۠6ZT߂RUjeZ"BVE(36s=eE2?60G1[– eL`CK C:'15; x1\ЇޚСrm?h{Ae镕1Ʋ:&զHXʯmϫy˾̮P[}$FpC6yGb)C@ƸkW@zxKe Zf4} =l9_|byYSMij-ϰWu\WNmVtC+ҫ{Ӫoc +^0XpUc=Զz- ڏ-+i0 4ۜf&vS Q薩xkvԍ)nfj +D^`\`Tt`h  ;O.Ө,?vEk]8wGE & I bAmq#uSuCQDP}׊,."ܹ~|{='OnvlF ^Ժm1zN^s!뇧:*qIY'Řu1ptlÙ)=S ,h5@! p䅽W`Wt(7hDk&]]陚z@ʕ+l”Ϙ脼ruyt&.DEPmn;89=*tǶ/XK`[*[ʒUM%k7{wg)..S߬x)NQpw ;8`. !@bd;07⻂. AF.nw +Z$h#A$2 gN4_ -IaMm$kG=ݒ$$y.uGa买]F*i6K:R6Hɱ^Jwr6Mg- +`,/bR>J\gAVֆ[m?3d +>N;eYYYcԱMT"5EdrɹVF.52r5M9mh]sLkv +Pd-K^nٮ3r~A␰Mq⴨U#jQ\ujV;5E9GT:)(>8U+ȥ2*"uy 3FsƀB9@P|=!;艉1ZwĬm`]ױ)꠨1Swwoy.u.ʇ׮Q.Qn({<^(e24}nw"LDp.KU0zRtj,4zMmfCzm.Qm.ժUn\ +ur=rgec=P1u'?f(윉Y[RGZ4YOѥ u+;_sy/ˏz<לL˾w5J5^%Onj熚|4hgїw0?S?'QcL4ږS/ Nx}aϻ*^P]اH[sM2@58_KCh7l;'o ߇v S4IAx)і/m˒V;1mr/MާĸMx_O89P=@_0$_7O_>aEazvAO_?<9$a,:VLBcr ެZWx:AjU&ǒT%m[bg|~fzuKnMs~Sh`n`_ճoճ'ؿS!7'6G:ŠK,wNK)*8tX څE¥&tEk(l\[iqCk9gk8cj#pm!FNt==r=pn u2}+ߊ~Y]OY g$$.l*.ZjJVrߺ"-_Xk 5z%jxAW(OMFf#o>EqI KyQ*vuͲtgSXh+je"PT)0} +J~\K\@щpɽu}v)sVaUmB*fbI'3n`|.C8tiGh)Vd+1*ϪZrܸ']g}4/~k%j~EmQћ 7 dgY%HW g$}Wbߵϑp={`XM8{Zx7ʦᄀs[V;1zri9u?0@/"@ҷH~y1w7 VCgߧb~?aͧkl` ͏&mz_ Ԑh! ůϟ̿$^b9x vLcۏ`˓uc|t+6>ƆgIX,stiXy)%#L}kiӥcTG2v$d Q¸Er?\;1K+ôj Ӿ}߁B XsHnfx WgmYm0}:/|߱Hh &P\e/%a,e`4ma0N`$gq$y 9&0FM1` +9ppL1O0&st9AN>Jȳ8P()v~})~#4H D]Z x '8WW2ׇLNFpL.HqΙFOZ<9bʳQ2 x/$yC:/}HEșrtcY]yI@>Jt,DdV +e녟dۄM=FYH^V&n]-]V@R['yQܷU/>%G :={!|&A dD@|Y'r4W?׈E;o$#7:W +j{:Ot}нU v[NƝh'9<;<ϋB"VV&|X%SlTlPd+ږ§}JE +%iŏ<ܢ{/pw(4PfTpz-g&U &W]nb YTt&{ЗϞ弓%#4SѠt_< cU~hPz! o7 6>hR7MYlvEy[zf˳ԣUIhR~z.y'A2U[: OåP uF{-\kp5lQe&KL/.vAu⼪򜪬Y?Ψ^,V5Y""VT.B7I܍Dm5Jo$Q z#5"VG7;lyze@=O~Ѷ(WA'zSӿvfpוZzfGLR_lT5=TFˢVQ{OF͏,}<ﱈ~y|Pdk(+cKx^h^0]P4d,9Tr&)RE  {b}4vm^Ln1Ŕ\p z/Ҁy{r߲Fc5N(OTXP.?gbv,Ik6zHoQb퉿;aWg] 4hg +sהsU,hn(_1%Z/Q X,=HZ١tܔUVI';)e%V ޑxq{Km%`5uF@-@!h4C}!&⚦*(]92{n\-'ﱌ3N'` QSb}9@]*PRbRy!|55teazGKƦ$&OL,LI(7+Wyb> hYL 2)~G"ePTZ@^5҃W{CX^#zvZuJJO⏋̛]QfazTyȊ_L#˟Fu4t0(:@-r~#!;<Z꜑^ _4ho>4>fDlQ{S넆Qu%w՞Zϧt$`n;Sr[WU +ꀒzNq&R9 5BjT[5BĨvVE6%$֔TlT;~{әAM?o3 +:i&38/)Ҩ?i=31g]}n9vED5Za=CBԉCizAjmj-1Sc/\Txec.*j#@fH=40D^4F쀐ˋ|ۛ!9ۮ֫Qih"Ԓ*=V=z믾o0 +1\2CQ r?`~a^7c@{3koamۥ{Y-ۯ|njdQ<:7sf h}|_?Xe,}%q<?I,zVgMlkGx<}'P+ɟF؋}o+`Yf/fóp /WZ8ulc ױX0rؾ=F\ƼwA4T#-,z xX5vs{{}17 o78aA sV3VSvhD5(kKp6_i8f}Kf ƣ0- 1$XƱX|D6 X1qxE*A$G?~/;>llȩKα@{2& Lw¬a#J2h`eF&2lLsl%`D(6l~&]6Q?>xoߊ^[wƾ:2t4̘l&C]ӖGk{ګM"! la!|;W:t +Nߡ˩N7 ^;uSڝ^00Y`V`Or,y9s@l'\{W1z\sD[:js?jqokp/ܣ <.bxB<^4 YXP0Gu20O?b <<37'9HitDZ;vۮkXDA4AMjD4 !$!$CVYMv뭕 +U뱶Lg~gyg%^,(xЁ' 8vancL|N$5|d 効x _;kq{enR*-.Lθ(dճFD-sQ7sRtsb%<$N"Iī7B4FG,X9?$}MÍd\Z#Eq&FjM)dNaq5縸;$n8*JwD HuKn>)%HI@>DOkYScT:#81SS0,f}(U=׻̫g˻[wl߿d]]{dwˮL쐍jHZLJC\" &Mqb<y27`\+9[{=6[;7;}wJkؙؾ'm.lOߖߜFMom&!gtux!S9'gb0c.3cp kz%ؗTve:2}鮀 -m}&Fhhih}: K'In<7:Z:6&/џw*ѭ\.ZQ:vE.5G=גmМؘUܐ_9.oJM[\Y)2 +uGҼ֑⍗Ԗ&w -C%"ǩE!̛3)i;v Is|WGp:?G3+aHaW8F3[_ӕh%jG$ceNANavٙшlOR9nH2p[F@wqP7nsUU` Ku45 0֬aj,}MWW婩VUV'*!ٮə)^AT{ixzAzy+u:y@?>z~he歟dy\OL s] 1.!m-:RZiHN}\2քZT2BȌ-kywu_s~;O;XΒhEt$Nk>o\9;s %lm75!wNqp'oq6hyfY82' +º=V2bS%ٱ8y }ܔ=k3RfMKYcSRֶ Imc| &:{ V_x(Cg~WfYf6It_;0fAfrff'SN\Z4S&iM9<]gy&-kZ?&mSݭƤmuMTZy6Q:pCb<k!U?)Av?i'As3}GqsgRO'd3FøDgX'*sz; +J2뇝ll\S}OH ^й`Jf01cO[uf0s4Dq6 ˍ%47DPZ۴ujtrtΪfˋ pP;E!Xu@x~sBTdˆ ~|-Ƨx4Kb.|]^ރ$ͥWhJLߚ*@}\lVh^ ?@~Mo\nV$Nc|'Aw0|%w7pom>þA4}⨜}2_we2\8'\: z`G]q0î†ҿU?OB_SE +k6cY8 /Q\f?[_~ +n*m=6[g 3_ZcWN|koIQtH緳n9F 1lHcI:4O&鯐\_$v o},߀YS L޷LJNtWzEYI`i1PU>R!P4WSi֠'HH#?/2}@0VHS>F-Tu޿hz2+e%irBW/"(SVC('T۾(#e,fO/"TbkFTmbL$&4d`ϻ%+'ԙYF*j_RZ:϶B:݄Cňib7W|=[h;dZo,xm+b^Xe%ՖuTiI_Eʮk,,dkv"XEYMz%xc6ye=Lmmsxf{T>_7`➝⮽3Ǿ캦='3e$<>؏n"v3yeٯ9l!jrL1JK{nҔ 3(ir9N1:y˨rYE:*\~$q \Szۮr-l#Jj@Fq^}F*e'ù'j%}M.=j1TO,x,&;5\kCqeJ=Y@ɰR.x=9ߑ7\룴ΊYgMwGy˚yN?#(i e\YEun(;9t +sȗس |)2F(ϩ>Qߢ Q^2 [Կ?~ΔxG@0c)1 #gS8r- ?'7x 9Zgi +>RrC'=JxcّP(v2MS;'#- š |ɉedG-̈u#tOD=hdޑȫzifܣ>8Hȍu[.=u縋ݺyJ EA.$$$&!! @ {""7 ZugtyVמvέ9:ߟ}}턣H(VKO@JzjeogwrVF x7g"ٌ q0E%- oHʹR;}RȘcEqNJ"EфLbLz-qXz=X|=P(J{ +/I}CURK6w/{tΕ|N̔f Yd(Sc@YGeN7>&kMud~\r-ND'\ t=jɼ;H{x`O?se/cLoÔ|/*dQ(i1>03yCBꕇyoR|-Y%)_s;_p +*iPI$ud'|& x8 )ŋ&[1܃#M{^?7ϯR5yI}K_${%4U +!fT/`B͈i21ُ"VUnM Ks(S`uAu MݵU='y>uA #.J'WL$-Jk]P0aJF?AT5;W kd5PƷlV8>m(Y۟ю񛴧 +E υn% PtP3~)p^5|1* ֡טK4*mF 3`4 V9M`J֏ +\9SMT[ajY#7 H\kH%q%L?BW6ӼCh;$E\NUfsMlf52q0aM:4fz,( r&%35!Q7gy WnA- +l+{lJFU`euzk#n p,<%&XNY-7f%"YZ^?NQgS}쯠ͱ ~N8ୗP_p94qcacdݗr~g j"RAG"C%$CۤyxpO |m{;bl(7v:'XܖDۙ\jI1Bz _:.ԺDZ׍4SPuR'H/y$H<&u@8JE[Ҵ +Mp{^ӻoEz-^z,בTr S{&UBgQjDX@PH-_Cҏ^5E0IN;doby%}/{v`HP( } &hu@SrUm˭||c$36\Y;$wn:?5掍CHPt!)Cr&TŨ +!kBe!Q +$œh,t+ ϕu|ĕکޮN<wdNL]н +C U8H*#2TD*i^13"҈UbK##IEž(NaWY\;@+I=$ZC/BQ9s(6|hQ6a)J(!iEQ; euǢ yل;臬!;H%.%,ăFKIUEiP6p/ʰ +/b%7_b^95WlyToTrPț2U CQH T(%X^6m2BƔ2dȐ"ZIe >Yzw<\yո]E1i;S;vɳ_4a)"NAP6\˺ ou \ +3О.Aߛ8DZ((o ]O;۱{T\Nv5ݮo 5A@rY0DڃIEɿ +΅`[lӳԚza?=D2/zHϠS:>YIX<=i7 yȾ/:? 9&uL܂[c–/hWBۗիZ8Q34ݎqar1z[v_H\ɿ CC[`*UyG&КiQߙfaON|vEO ?!_ïū6=" rY"fA>Dj#,w*m9A ͔&C}e.ItvI #@K@a%6 mQ&=PMZF @*d"P3&Ub#hz}|0KQޙE +ycj<7<5oHD^5J],wIO5|Ьf3o5IivS9H0o,xmK^hPyFE:;*wR<(ҐjM$ e=Hqe,xo9Skռ+˭E~J YPiu +YWk-+Jz(꩸F5v!{bk/[*dlsW{rGEwhHi:d.P}d?{-_w!Sxn*S;a5ﰍdr<ǣv̦*}KWɕ9梋"W\^n&r,pu\yΓxMLJpe!.K(r: +]7smn:0O7&gκgN{(N}1!e%v$-Mj1Ěvҗ)ý!(Mw-6tWFХ\a๕ɜF~∷FygG^{ugoJ=](`?WGN";\+gG- {YNN&p7860GGgs/ ԱU:_m.ޚm@Ȯ8qmFs~L0c#Y7c8꿄#d'= CI L`):ǗLwO{ Ů>F(/˨2Vȕq} șޜ +$3(#ɘ0 I OjbRq d5Ct;4S/94Wogh~JMT[EwS#û1FPg1\ 4''؆Pg2'zqxXM +%uR$)3&eS'9,^ggzao ;hkF7 6?1HRci2z(O˓1oR؈3&wXx_ҧ %uhGL`oD8#b9I:"&FE?aj7N=xuQ F1\ZK|=Zj }uyd$3ŠD fQ$O dI$EG-:Yl9z%zW鯏d.fG51cNf2q|+V*[j]BjP W'CN8lFFt'Rb'֍]G4}[EA6MڙlY]VF Vj0ZwxY&KJMWdIGѷW=R]aQi͉pՖںRnkV i)J<*T30'`f8" b-EUnWVjgJv5#}K ~潮y}x/=_كˡmE;ȶ'iHGi25T^c)uͩTm5,j[3b0]_d>n(4_60,(z!'dNKG%vM +ԟF0jӟ#"U3YgZbXSnIg,Ky,E@hMjȷ3Y?ϵ~o̵5Z{{ 7kK]?U~Y `zh*1/̾*fP9\BGv;Cs[.>chxbx;'cC1( }p U|8dCVL]Y7O u/^F;,w*ӝp bq׆[ iwfyќGP 'dHp\lsw慰?ŅC),K~\h=sĒYBw%vYe:VK&o>{=+\5$y~}RQo. 9hwېUBy/%&t٥dMQ6[8,|)}VɗIj}A|)}˰ۂXExPz`$*V[A y9vh̓fѯ+U!ZMƇXY u#XV7%'y:q1,eQ֯$!xkSmܭi|W=::f:Eӓ.E.@T* uxzXEOܛXm(۟o̼mKL"^m^+ac֎QG2h#|÷٩bOn>Bvv^9;0明Q't@Ft Fq ?9a'1^ygx"?} 1~rlP&h;=(GD8> £B4wx}ݗ_AtE~rߐC i뻅%ol+%= K$yc.u\?}/! h;a0R/LDp\r t +rػ%˧M~&HHS%h%hՏu$EEyT0V(ȅȅ T"PɉR +CUu+(@.gK.{ +n5*^W13D{ ߞxr/ a-xLe6/,Ͳ<\rߺ{˸cֵbVnuV3\jbLJ +Nw*8 C[Dw%Nۍ&66<}#wmRm-|nsݦV%TZs;.6q4]w|*8,9dwDJʵIoY_3zz$څp.vqؙje.?leQɞiqW3zLw ~4_.#zuDky׉{Nj>\>39cIfroq_)/`j XǏFʋsol$hpl{ ѭk?F;g'}BG$[=l3:L6za\Ǖn\kZ͚Ɵ X^Y%z 1HpQUEH5Gؘ95ư˛z@&Nchj'Ʊvb"kX*V~]·_WR>u,^դ,bTb6~K7h?쮢i|Gv{ǑIl:_?RJ͊,ɹTĿ+ XϢf假/- ( lAA"/a3> S̚@VPpY4SFŔlʃ ( .4laZ! &7C( ^H L("d 2ܥ!_+1]ꃺSlOMPCǰ,t"KC "LKyX eaY85,:i ̊-V̟V( ߢߧ~V*7D(sM.KyBj~c~[c}hWj~ƊiXFexRˆDDMDE?}sȏ,2Yȋ\֫jYڛꬨg( wyo8 =g[ Ӱ&3{4ҁ +ʴh}(R5¨( +̋27#(TdG+W273tt5st74i4x-b$32a49vF(X@ՌO@n8 tc)M@<](yf2wIdde+cJsUi nMy+9R)q%*)VhN`8f`fn4<^K-d Wd/"ۤ\7ĴlV7㾠Dqd|020ZRgI7)>Gh,V&Ƶ"U~ⅺ-e/\c!kr֗,8+NAFsIM'91HL1$I0om*R*UqIձI5z#Dr[YY&;1H#)1Sؙܤd% =y8cIN”GBJ0r8[axiCT(͐u3A"*ZhE -SJYZZ H"lYlC3g9J\{>o@Wzˢe, +o 8V'xwpWpgQᆶgPN3H Dx8-u( H)"[~$a2 "u 6&x5b_X8:.,OgIyỴÏh_r {VP4s~odL x!2Xz6ῢKVw- #F1xE3̋]5*AcNKT欨R?4gEϸ*2y +OziC/h@Ds X9>1}GŎ=f*O\V2kU3Vό&5pR9zSgd/>wxĻv,ǪZxcځ^; 0焩Hw}O?SCd~!i?IgO}دU>B2(ĻAƲXqܵm]K\wyӓ-SS옒2)8lt~l&m`F_&1>5qIMbL>Fd:FdL˜O> Ŀ9d w}d4##4S6akW&mdBz?ƧǸǘ GFeLLWϜLgmaX.laYmsl JER<>KkYw f}9?lgvX1kA˳}hq[87'3Ѫ +BjT2hrP8 k?*zGK-ngYDqu.NsZˠQ `*ܔ߇45 ܑ]yK Ֆ H fI?(~q[D܆n[ͯ:j;Zr5=#x`J*MꓱP/MS9 q\B.y~}7_YY>T,O@7q뉻U5hm@(yzmqkKci%׃P M"/b%JHP0}W*& .X!?Y+yA2D=r+yNQCJO%VNSFs+[{^p򞅼eC!XF$Tqnh+\+Qq]zM\\PՈ(QJ&ͬ(v(Z?^ W^B3Upy*{djU*|P +Tu~ɩf +'+T~puPԆ($=P޶ëV>%.J'ɤ|Zp0 FHouzӸM=o-^8YU_) Tl6* (XHY2w@Q-BP`ʗQt[tibbڕצ=7^ס2.Tuəns9i&?QaDi8eQ1{"pl+mgy>E%XfovzoigCSf(fjyӀԚ[rl,9c(M'%V?{Sl/4 +ÏT0]XPC:NNpرX^M^dKdɒlɒlY,ɶ˻8,b,8544 @)L% Mi)0PvCgHI~<|{9/&fkX^3˩88םaisuc! W8tM*;BY-oWW Ikyzݧlr ^_+7xdS9q)1R92э,n¦ٴ¡f {7=W&kajk'“)9)GSX\-b-vYLkf!#Qr0c)ga߃ e.'Ɂ&؛\N7ٽm Ӛ~۟ ڋk?bL0z_ϲL,Βf S9ͼ}rFf6v5+t^Q{I0Y.1{ ċbC@Iol%s=%l\Rޭ,f0}LfuZfJ.2BdƊ;-fdxiҽ e1ӫ+=N3{Bx5,|̕&3S2 2 +FF6 } 1PI}TZj.ót jx?B[(ROH)S?_3Wq'*1UDU6U W1Te`BAC!@:Lwu/]5Dkf6*dzMnzO6r9_̳$.W쪾DMc56f2ho,XIhhHL6D6FyyvAڬZO +QZ_ +-koD{YCXuv|)ӭW7o`NEC̢Ӣ'j!laqziVE6Dm-fI~^UK\+_7 .h=)0gc^Ÿ'~A>btٲ鰕nZZ=Էw s&:\'T9zGv~r;y2RGEa HTa,^7︋n:pftpV2w6؏=A{vy^V;>U9˸(EVjpZ.W#wiL"N! zjxp7hlۣS}g66'*W7e/Nb?g  6\G駴yWK/ol쥾 {K [`nTU֗U斏EenVd'\2I)9aߡzn,djc1l'CO1U=TR@룬7Di_%}@׷,m GDBO"wMlr]a9kd+Eh%")R1P,1cc%kJP)3H̆afc1w.t};坶6j/(rU+bhAmЬ A ĔOhLd@dsg+kٌe-pwmzL57)ownwl.)TF߉;byT3y!Zڄ/7: +uq-H1^L3x\_XϬBS:?H{yH:}rBuDW;NSeJT8P#[9BS&S\J +\@s}ޮh;odžΊs 箓=03.b:2i6nPt_Bqh +{Rc=93r۫1r/;2= zpYFf&mr};As!-"crǐ6d#CI`~&ѿ힀G`szURGoHO{v#ϛɮI iH )AKH f[p ![2,a6 ?'zua {0zqr_\C[%ă:P3)aI{İw6>l#bX?r3FZ6u5XeO+`q5:DwPu @^Cʨ@G$$6'"g]\">b%Č^Ś1X=&豻YYȊc,K$0X;0T[&`3tZ4ۑ:cI;l`}"$6b:j6=n+ƭd8O`Ʉ o=*:㟵jzڪin%5CME+*00 3 0 w7@ED0 RMeXV㩭.~?>}3~"E J-4\$AScy1:EknjFKL1~.>Zj ;ZE?"L*U'=]]=`ԤL*e6)(5Gl$hP`vxȷVkm ٶ'='-%q~~R//)g&@'bVjRRiLu&EZ(ƐoM ϚϖFn4!Uxr8C;qL{3sfyPOr !u5HCIB(Go_B=\{<^ OwFYA\8tڃՍt׷;0陿kãT谪D~UڇR1̻wOs9Ex]+rqRqfe%.YGgiؼz_%9ˤ} CbN[ +׳*J#5.r=Szg."3;fҳ帱斐[ׄN)}3s%9'˪a΃T8Y};[νoμ2"7a0H(!)cAH(8S 1XrpH1w{#'9'h{H NZ8KB̅I2J (CQ!EU)n 1V'ebOStO"LL᯼^<tI{W~'[}+ᓾ,;`l%##l2PC*֔Wtb+ܬ]Q N*_`Y)U\`Y,+ s\98Uۥ[W mwKR1S$VA|Ollbj"6,5Uʲ:'K|,+aQZmfLG_7מc~%ۥT U>Oү:yu7a+6#j$G~.a% 0!y m21jf5nafc3h|DO+"G)GCP KP^T`߲i$rxl3%h[ߒԖ<3I;$Z_cB&4˄m/}Tw2@6LyYq[`Y33v\nfQLi3cR[wEr%ܳkwJd|=mqgGc:Zc;35~*IՊP^iI;Q+;afL^'nOOdL8FwNbTtF3BCwxۺ ۿpc .}?x\Nڨ>hT; v,~+ܷO`! }f7v1 {r]σ Y11.#*Q"ݯ qZء7mVlYvj;E5N}RAvѠ"n%7Zh)>츆'5tzu{ + ^yJ?hQ,(Ŝ\|3җ;E84d$ZJ'jG{Dh!P.T7 |?%8}ʁ]6I{-pp0_MxO|(>j>EK2 giY}8y%D/(V9/rn筒/4 V >Ag^t}}K^l%(2mb+V\嫔o|.ߛr=f xKO`Vh4F﫧)vWu5) 9#UlJ+K\ +V_ΙUb;oljop[]X9G?p{Cv6ZxJ؅]Ӻ?[Z\mGmx]:X981w:-9y%'^.h|cpnm9 +!\ v>cЧmtknmۥvx: /OR7wK'i|ɱNClPU]́eS>u{]NR2uvmj>`@Abt_G}7ӧߣ%ve^=S&r|L Kàl11}/.fϐ%î!]Mv v6jcF^qרuQMl~`(]s1{K[sv`N _HwޗkёԍejǤP3&cQ=ױ2*US1n7\ߠ|96 ?P wu~5Qq4kdQ? 3>av 㵉QlՉIYOʣlj6N.tʫLxaUָ*_)0`# zJ7ߞzs0Oue4gSf`=,ydֿ9M_ŬU3k)T,iOJu9C5gWxcH6w:E>1ʴBS4+LqIf r3w56ԯLd(.LIT8Y@>pWmw|;iN/Jh+kQE\X2+9dO$k~ rX%%VFOr?I +#Eo*;t}ml׎R-̪1L&/`d40@ 6҃0,.&9U;I4#|o-48Cq}J $B212L"Z(((R:F * + +"+ &*׍E\ .6dfiSSMNN&Tv;t{8p{YGgZLa1{sИF z&,VjNuW΁Zt ы‚hOgΒZ۱37h%x'7H<#(bZ"(q^ysK cH&"78I&wN~&&gτRƧT3.)|w-V.hV*7+A(|ju_$5gz3˺ELY>഑LJĴ`Ӧ1>}ǘXͨȌ#g\?6i?WarZsp@+ɻf #qr/JRϡv1< B2#Wy0!ӛd cLhFgM`TV##ϞLjhNfXNCr628A9fr.} e>+GUZɽ.䎖{+`ڣ vY̨ndD~1`8C 2`tװ Ka kg0c𾸉wO3+fw]"w^ rG=S\aZ6B7^/b`Q |.oq8ū8ϒTzd3%{QrV|I{,zB߬OZ{}*d˝*L'wܓԖ(wkk:YF>xnqnZ> TkE&+ +qM8U\ǹ.팏hWu.fLZ +;AHg=n74«ۡN\k Rjf8GSMhei9vؚ`cMMlk?[1*lI䎑{nj +jk>piM. 6 f]̺1kÙs +d CqSܱ)OgA5X@teQU'~oWC =FMhХ.ħt9 ]"rV69M9~n. +qJ:X5ПḱA#z ڜfg?/[;B]/>pe\qM/?b^ ʄ~8oŠ^Q L2W桇=qڟMBBqCҧgNj.7uAf|q;~wh5j +Xo%wQ ZC].KFߊDrWM=]xM.(4z)>8O؟XpD{& +?1JafV3]+qRj:^r+e UUg5Ow9q&6mW`fy7@]R5` !jZ vk:IfٙdF&cjF2t +TZP:WzћFOVYteO+g`I:sB*?ҦV"-g~UZ n,Gv\Reb*;seOn!}zB&UVv\t ]=Jzւ5hּJS]FM>_)ɞsᨬsٰ/g1cPOZA 5]Z4f:4v5 +h)_8Lvcxui]mJާN]>CಸIeJDMF,$3J<(cشGd?6!t(M4V}6~}Mzrz 'uexZ-bw4.aƳ8L/c7=lO{X(=\Uh)Œutr¸Vf%2,xSU(VQ_QBOɃԌ2üy-g,oǘH~U oA'K%s%o aɌUc⫌kNΜN9YRbnbZ*k8f<aRa +HrHyLr2n;̋[X Zp[pڲXmZT٫]T:!*c(s9z]wޡyRG$r/_b ŹW2A~+xmq\͕BKŝ]mXcAYm=VnJ=#x;"uoPE#j"Qۻ(⟗wJaB]N]ճ8*aO>  FJ輵h5vQ8q?}?#!oK"Q^w|\jGY_h]M)mVPҜ֟OB~7->ZZi!YϓzMIf?Afs$/IGl!CF>q6͒{ZEiJ(،#ug:9uj 0ph@lgg$;I ~"xECR;?'#E?+K4wDNqKp +zjEozk%;̞=Jv*&-TFjE!?ɽl#w޳$^!C _E~\|% +햺[-ϊjq%H<eR6F%<ʶ,$?lb˰F6 "6|g ̺3>1~^>$Iq! n6['=$&."a;lXɧ4[JSLiX'*fǧY=3ʙ,zwQN5[w &r6 @A2 Eb▽~:]\sOzn#X1l~0_Y`瑅F^҅1, ,&xh( 'LyX?"َ"CeR +MeKM$,EE$%ı29c8csY4z{~=}c垣c# ߤSu1X]ӄ? - 03OtUh+\,vZՠ& +i;Rrp +ZB]Ъ +L?NtrVVFp.mB֬Uj q bFr wܳT5ئ=P +C `[N49=q.\ǻ '᤽:kҟւVOk:R.OXN]mhūr~Zz츼D8+Wq~.*UTq qEUxWٯ)kZ{~Lu{V}@{@?*g;'"eqE\ŇZ s;U8BQ)5*wTW%<&oIV_6M|bJ|~_T?Sz{䉦B06q#F3^t}!Ɲ1# 9勑+A$B+K6rA1R.^VU%K^} ?`TkD]䷒{pKOo*PzFS\ZeP<2K\m< +UJ m8,iZEuJSlĨ%7KzmUh+L(yȓ(2B4]lmuz]*W߫8GPYCZF_}&ZZrVvI/9,L.O< 8ym_-h}q_2^rR\+ZxZ&W*d^b*A3-SJEYȶ7׫*nLqwP=b`TLcmAUhƳt1˩hFy,Sڢ;r?ŭNeZߡljFyqwC%5L̨mbmt+z} &aByPvvS4%;<" +lbsV16tg]xYca$n騭ױZw۶S0ʻ/-QlgXNrlC~z%W*뭲Ŭy>GX}FClg7(^z-QkpoSvYSfՇ>uړL``m9X/~dO&:rY5ItIr$',ma_Աzb(;lڳ%?h(FkZ@'~*Hw*(V'uRR$q 74t'KUةDX5 _x'#_u;c[Q& +.oIXhMPAiNHuDsΡ,w$ix,K'%D$pD ns%:1v.~"]+]EtT)FW6QyLuLI1$y-$ ,qB %ad#`rFg3zۈbI"\%b=1?ilrP=U#M#VgH Ve$yÃE~$x98b=CIZ1%ΊK'cŤTTxL@g)4#F32 +LeU`VV?A~{6LYy=oF&aW-IrQF|9]Y=HF(4gBr&jz! +G*nM~Y[)y49Z6jB^|O{M>oCMw|p%2hBmf%EF3fh +T@HoM-0M*B| +Jl+rhl +)Z/UH +jT]y~~jyni}@Iۘ 6P뤩\5i M?\>%5dƕiliFFʻ4NzacF8hNC쐧MCg5Mc/&.&.vC6/Ma +Up+s';v +M`y\ER-3d$b:v{@KZwQ5w&lkFg Kp_lnV{>u΋zd ;@tx?8p|C om<ɏ<9-ay%˩N˶ye ?D Ra@ 1G:1p`t8 4= fϰoCv~BeY! MR8}w҃$zFgKos&ٗ\_hD/΅; z=0i'/ ta\eo3ޤx%xACa_4zg3rGC8O 2-H%E*uMoU+ՓW7j&aIX1>/>,>μ]P'z(o}r` K5 riIν2Rl%j`V#N󜡀W:弧?Ay)y;  >x7 Mpq K` +V- puB-:ô ZRmT(>NxwmZO=M_0hf 'N.8;x 3̄e8YɇRL:`kVF +xFvp:랬Qgg;8+V,Y.;3'P +ϑrBnrh-l{; }'[%r2rQ pIr-LPq̅0t3ZŠ8VdX5q`k3mض~m~Y^QË^}%.4_֔*Uk1ԆFK$NI^]6\i!2H$v*)ߑxGKJ} >^IRGf⮓FUvגoۂi;8rnN'Ӯ"î#n" +eK#)'Ɏ~v:Q8z;~#&KߑhUڧߣ헚]mdu-*mh%=Tg")N%tpHrI#b;G/}S%:[aK|.|C!UC֐fq[Z ĭemUAJ6 fg;8†  dXCH!GԳqIw IyOo5{/eRTMdGAMEQMTJ)fJ6fX>jRqN98ItN~{w%%ةcaGHa`lk5RֵdYAuX磗MRߺ2OTL_*3W3D11~˨Z`.](gK; +eR(lJcĈfJh4YSdba <|.}n $y"&1H.!ǦaWHK\ةqje,IfX^o# ȦșKbo("Mh/kmnnb$|gNL0>>FKW5z s +f&E"oU!tCA_0ܦ^bo, `1= ooNSd`䄼Q2h=b}le +ԩ"f2*f&}͌m&/)ڛx7$!Por>~y49_̠p'qdXrw//o1 ψV56c&@?p/ٴDrVwI#DңwgضF>x;zf4.42N128UC +Ryu"~No1\uG{K C p9xHSp'? 񒇇Ŭ^ WqDja:XXeGNp.dG@{tOư_0v郯Pxä́g~`,#q8HcqU# 0Pddp;Mߑk3u"cc}zrqRş3W&šhV' N*LZ j9TX-0^N sItrDEV8=0u, x`Mx8IP`ejQ +d\UZBVSՙQM%U*NU>?.rk8O~ǔgȌeƎj33駖|#'FkkM$~YHu/f@V SATnʬ꒖YT4 Rt{^Ormَs[z[8`F@S)\0;l}ve3S5 j[ٍRp_Uۇ>BSaJcU☨b*%NTTw^<帼lr;Lp7ztg粒IKA ޅWrNΞ*u2*rMS[ +>OyilBey,W:eYەuP^guU)R#67nk]_ו\f壬UᨢѣT8Gc3\yS,-JPwҽ366_>[df%۪~?Q]_T~]d> Փ[j@+P},T8Nc]l_?- UƸ-7Uifi_R쟮yyPjҜ͊ ޫ!PLE=DOܥs(+>R#mi @%|-\ +-Qj`RÔ4Y"DŇ).4GK5{|'4**MS'EA)2'୧hQ)btQ>_̔l!ꪤP/ SP S;!J1aOQTxfM,Ԍ +ENZ6[PFD P6;ɐ0##8GPi-uju 16:hӤcژVm-}is#}}o%U d4|*c#ԭn?0i EUUpN?E*']Q^ʈUZtl!JTRLcd1(!Hje49gjSc1ZOW1=D:J^f69C Xk,0Zʍ0Rj(8SVc,śbe6'd&T*ҨhjEY7)ҺCs("­7=EXnbQMXkQXqxn [p%%%ad%%Lkbf$&+*i"5'\ +KiRhJBll; Yt]A)_*8+of +h y*i|,I2'1SŤ+H[">7Qa +MUpZ0K+5+Eu!w5E@Pej + (ۊL6'ťRLHEל ogPhl WмXfZ4;srU,E\ӳkZv|'f_O|3~39/RQ.t,c2ee5rQXBr4K(`h(5izn|2W)y՚L/ib@wG^hbcmp+ɹp.=mmɔ נ;vէYGjt0s׈:/ [\C5>V[5~6ȥFV_C˩ԟS 9QߺH+ɻ. ߛ#$$؆),ؾ/H/JÖ8iв5>\.vOT?dR3v[>h-g.81n{j?Ε{T6ÎĖž^/M0lM5,:k^|^|.}G7\3^al;$:nZIu "Gؙ;f3 X4vfX6m`8G|Zx[x6F-ږr17mQtaN[oj ܚ2G `;7ᏅwFmm&N$b!-ıxxHZt~It]U=jj@`Gdy2-Knp](#K_~UcCe\{:<w=t 7n\Lk*STéupINVQqahOS4>w1H5@'yN'`1,RdO>O9-q8N N3 0h*qJ C׫/՗|]uUcaN"H>IN>90a(1rDp&8߃:rPe:C䱾cEH-G7")FrɇQUǮZ gJtq3lj6umv1Cߎ}_0=k2,?X'N + 9A)Sk +-LF'W.Co3-2VߠǨo<:SAb/s[,Xp p,pRdK6<-SQ- +Mѕveg}!!1$3$%˹.˱\%5jN<1^j3U[XرvNiNj~w y^椲w\8W>y#Iޑ$Hr+)VbWLqzˀX#bM5#VXNsIOڱ aE}8#\ב&$= ݦ;%^x%bUr*5^G9^n];ȑuAMu˄ןd~9e&UBo2b>@W UGGkoRSOo ܱ68!c-F;XbysxY-|0sn;Ewg*.&f<%ф7I|Oٟt]oj~V]ý:ߺmIّR\ 6'q(z&Ijc:~&f,0]uû`0; Log}knXL~ݻ4i4ZIѨ8SRebfwӻKJu2ZxZ#ci F f(}93Зy=Y6>I%:L&`t*ĿGZQiYZyC$ғ9xL ,*r3hoEi1| SGk~@Kޯh/?P LpbM),ek4w62n'^#|EћSEwN=>:rі?Bk4-4[Nd9zElKoQot ~|?x/{flҜ4uB F13)0U`N{a-Z=4ن8LCqJ?JyjW_T?b=/)G +:!HHYc%a3n3ZOKQ2UxJh,iIcweTn\KT:O5οH,\~ǭ5+Z6wgFq92:rh,PWj]V)rU*ƩZu'eUqT]^ qr~V+Ǥ!Z;snLblj*,*UCEgu} 9jg׮P>KaluZs,u~_kER+$wHDU/)#O]lO3Ԉ]+}-soƾX2%/ Cj'vŖ1D r4Ɨ?˺K{ȱ׵źONr~PZ]-vYX G)3() ӛ#:M 6D %"3`&,[88dBߕ>rֿP-{>N;kBs +f9:p.V ᄳ$om. y)I&1 *+b*Qq{tUΫt}'8 *,/ʊ|劆[wHjc̣0{;"d?c_APDEK"n1&c%5&mZ$UklFx?̝;w@k6r9`$T#ldFZ16GMtG{D;v8WJnlO(IJCBkJf[l˶B6maі&Ĭ0ُ+̓[2\I y(؁<ϣpv Z^CcC/D<6҈l"uBiD$[+neză[."nq 1/caqyrR?f` KF]NSGM~!?iڹ9x$\{",fȳ<vl|vl&9lC:Eo%151qIw3.yqBoӚ߹<v ~`&7qwus;Yox@@H>*tӰ'&h>ĎKe*jtm:{2x!=6SKVl nt{@5JًMrK9WI8~K7d-r#wJ`*KcJf&=橻SU1brR ՟~4tĹN\^]eƻL>`;Li'YuQ3aprGS([츊W.aw3#aϒ4i֞dL:֭_,w~tןuy]x"dT|)bfR/Yv-kM+۸H!eϱ7k$9'3}0m `X1# _t;aQx;N5Z8K,gU 7yhdݡosd=/!'ˠw'FG1 ?ԎlQ9j8pvy ާMs"O#렯Er{cfuX#9 80`L40raA g6r8p4~3ƻ7ɔdXwrh=E;cgt,0SFs!}eQ0Ʊi 'ND6PN=BvŚmD`gԓUVI{W9]vrNU8!pÙ' J&NDJaUi1{RǷ<(j2u*\N\GOȿfoUJkӆ:e%Q;jٗif*Ug*͚UnYsQqQzT}[>Dx+p%W!q v%AY/tbsož g*Ik*zMSyB)SijWq*I/S1PyAhG)gU +jL7@GuiA_{UsUU_U`TU U٠=hJOV )P 4ݺIS7(gneۼ)6isS#~Pg: \ukvh:k+Uќa*k*SMGj,MQEU)/-TrM_mJs8TRt j?Z:ڐs9W_mS(;;oh}r3:^SF'+!]98Cʔ:FF\w(ƻZ nS!m6 mGwϧ=l }#sf8 RHMurTwMLg&;G(%Vi.JuMU[>[J|Y^Moj9@Cx?ѡv]k>\|j6\vJwwV&z QJx%xgh4R_b(" [a*,w +3<@u=B})r4 b5ӫҽ(V>Jqx__i_8XOR!]Q ,RD`‚+ԸR!F\2.z c íJi tlwi=lxQk%+.Ec`PT`"bn4g+$Pr)0l_xߓo'ᅰ_O~'ª E rxR跍fJ0U\PńRdC<꧐P"e WPx"d,E'Iћ.Ϙ3򈹆n3GĻ~ 2Ƥ`^RQR4.~L P-py* ( R@eXPv=EDA%QZ&Z$i;5itI8vcI5h&<,~{|{R<:_sSYiњJLOQB|MU|Iqk)ư^S):O+*5Ef>R&R?|÷ë ^׿B=O2Iط 7(0A HMϊմDcٚH1Q+N9W9/*,U\A³j?5؁ :K&&yp xl3M?Z@(&gNQttE-E3jb^_v+PHnSP52{ +}^'_%l 2p H s5{w&/cQ ++ՄD/ +.^R1UkTIKP+-9%ч3}*frH+`W5s1gN=;ER$1lƗ(,XAeS +,s 5\U(ϊgQ/Srcyo$հkna6}M̀=vXǠJW,eFX2I,1NG\WZ/r f?QyԼgjn Subw=*b.! ;L"W῰(uwܬ~r +CJsY9Vj%+I3mx+ߵqKa!+7(]صpp3a'Neƣ>ecp aȰ3hr;NCUvPfi'?AE;莚a[V--f6NDSk_Ik$FɥKG ^n7򣘼t6ku \Z+䶑6hkBԣ:0E7n6΁{&X>%l݊6 N>fvh݆͢0YD=v/mc+K aO=.6IVURKZ.ݣc/>NGE C4:j9*{! 9mud\y:/\ ;.$Qr} 0xI 3 ћUK΃;ig]El;N-`}8.. .'}S\Ox?:H@8.όĝ ;v(_s%zq^0.osy-IEXK*ދ(Dˌ&fCNO ti"ƌ~9MqFIBFk{̍C |ح!X=|{{/usV\u{8xѴ;z]g^lUy qm㫾@BЩ0RuK `)Qc9Xi#-pt$m/RӔ7wQwo̻ 3GI\J.(xu_Jx}U QLv8-ptNڦ9B&^񞥬Wh;z|"ktDEG 7xx:F.~)W褖`sDZlf 59J.@˛Co9+C (X 'li0?0*aY>viVvХ|Xq+A}9r~ Xpb$˜Ê0?FLG.jlb\ 6SfN:N~sO?1^ cH8qp%tV7C' N9Ufj_ZV[C=VCYũYI=lS`WCe α um!1Bi2+Ùg. 8F8 +``aY`gCt Uw>~'OƠce>k ma˝|Y)^8)LX)Ĕ+x +¥T]ZոŵYUntQG̞'Tx^UmxGƯ#bwZ|<\ <ָh[l1HrYLU{-;OUÊU9̬ +jTوu*"_UpBS~ QA_z~Xën;ײ6^3Q#c~M[>T7|jFF*DU'" M.3?7 ,r, ..7산hxEH<ɨ*cښњv miҚImƶ416$MI?Ȧ?q{^uѩjZ-jVC죪="OID5o}kY K 5` Wp_lF,ѪRmRgt1j-R-qjkTcO =&Q]Iޥr<* 9 /a(GO#=z5u/!0u&Dȗ4$e))O EO.'ťj rvɕ֧A9;T3t\UbMY,>vrlJ'#h߻i7QaRcU"OZjӲU^ wzNUfajRSe^37$' +-T`yA7П&(yk;&IU6ƌYjsir3U"et[%gd^œ *.kވ\3ʶ GRvm=E Ra l`%5J, u|w[Ҳ\KʲSTEE9UW|WVk{S^مd)ܫ,Wi{Z2]zOfۿtgىe[+uVߵ|Xy*.R5JE*OW~A +[XSٶ:(jQVq2KTEe*-ڟVQnŇQ-*R])8\yq)1Rb֊edTV-cYW*_MJq +qH %8x︪ߕ +`ƯկynŬvvβ9/UVE̎$e82:V(͙T]).%W+XLp*ubݻ>hQEUO*SEME>!fpT݌ZrӇg#5ȁo1ؖT~U!p-P;Bx%W*&S 5)VlS1uEյ+ҳZឍZ١Z6BzhoR݇^gKBsAجj)8;WK$E5hQxc5kIS5hAS@a5yHsRh|R!/yIn(o xmķ9@bPZwm>ƶ(-Lõ=N }̚\RA<0~ϭvaMۋ料fr_w7zمwسRZv)G/蚫9KҬ@@ P P@-3i0 B@Z( +\@_z}7RFs'y㝋U2N)[Z +墽g! bcyij?h]~~n~xwJVτFۍ7Rx( +jl~/F.ڍ/ / $AGcE@u ݋ogLx#%NFCXr6S-,[rG#z ѓ!^!C$0|7t 1/wߞf$z݈KH qFKg + XN] q s&=\~{9G8pF;CGHb<ٽl{$\V/3Fuxm۴NJ$MmA]h7,哾/Aqrҍѓ1;F-FIdFAlӣohY#ޕ܆w怔DCҼ}=Q4\xkf!:s q`6&E=uW-du]J +Y\E-q {\~5JAQކi?+?#A_SGZgFyW%*].ՁG^R^=nq^!яhiy}s)O)g-tC6J.ºØ7%Ic>8>4`a lNs4HB!a%MD9tm4Zj[6iVEIݥ-i6u[RemS폭pL諟m{>>σsp25O8I'm.KN&wuH7_>zUD-m6ªėn Y#؟!Uf!Xx,XA +".WY WG} M_ѯvR;[)ybS]u0'8200a,XLg?.b{/ϓY9;ΑE,n*N +,`4pQލ0>cJhGa,8LF'u_⭓xp{ :FVo?rZDh޹o])b8U0 `"cp&9Q8p`{UakxJI:_VεxX猜Q>2 3X# +0X"gُ}_ 7ż9ڎute$)''ٰVN v8>8p)°ɈX{a*Npz.ϰ[N '{ZoG[1Εy=,ɈqXπWŰ*`YV >u k7aXXѭL$^D84if ucb>`cpk e(ZI,&f:uUlDUth:[S}Dܘg4e64|N)W5S/V`׻íRp t"˔Gm+| 䕪?R|z )hUwG"ZNKܖTZr.LjjU_!կZېj"sϨY[d@)^=`L #/X;VlKR-S uՕZ_zݪlVE̎q5|H%T켨" *t~U)Sa,18AW/FAZv¸K :T0dTeY52;29[UUIk*j]O( qEW].'aa3tN|dofiQUk۲dreRJE:;ީ at(˳LϺ2潦]/+]x~T[w%։(qx0V3AFo ++WZew9VFwҺ3%)Z翥X3)`>|= 38 ní`H&T#Co2W>R,JWR_*!.8P K&b%Du_#p``7y% myYTĨbJ JJW|(Wq\ 1!:D҆p$ċ^6A6̌3,aSn%LuKGy5I/5K>fعRF3N1LpOXu ^`ΌL}5€CP)'lj<>O krc +|@*fDOxS4͜;C{a\Q;JREqf͎"cv%Ewt{~A|9@ veH*a\ϸHh)l^fA. b˾r;pn,~qqDN,ұgX*ゥm +f%F.vոP?(@..˱ˡ rWP)rT^XMLĦ4m5dbD iI3d&m2j2L1IN𵓎}~<Ú#fɛQ{bSGW?6nGNzd,ý,w%"=5&D0Zol@~>DN4vFM<c ~lÏ~ Ģ ?;t覷]R^ÝK\ة#Ys\op7*MDӹ v7^c?:Hm$'CzB ] ] {M-ay]{&l'Rk<6h|ҺO4ć=hLQc?~Jm0k>*A3v1u6=Mm}'F{Bf=܃urܜ;etzacnC#yAyc`Oh0ό&=@#*Q1FM + ^hGٷWٳW+[W\W,2/?5GI̭9icnIu78&E]C;#'t|H }Ü7f/n1|lf*׻1`Jy ?q?^g~) +e%y$%qys + '}L16_3̷c6=3U)f.R-X&ahAitQ"06S2;)9zx6?t qzÔ._%/11S%'NدL`}f+atnl<<9D R<߭0bx{ +&Nf֑# FJ_دga,F׳62zo]X inqEۉ21寧ί"0YQ _J˱_4[jhkwk9›rH>*J;v>a5G7 0,X}'kȁQq]F5y숅pBa˥O KDn-UFNmv|pq 6%XG*F kr.( ++SuщkK{;V_a}mʚ&vy#n|8bNzp_O}Ģb^pɀ N12*Vd10Vb݈WES2w o?fw}?n׏I>~ENbF~`Ea55dMp + *'54i~Qx;*G@kGZNԎܭ/^4, dX`屦BXϾV˛"QZUƪ/Q~MvjJ T2TEU@ӖiFpB6(7t\ +Vfi9F=Г{X r5֣1j]<ş`UNS`͙Ҡx4+8U!YbU('NeE*#KJ3Pͯ+t}!yw-zC kG|21|oi$0"\3#ʏ*7.WSQ9ʌ.Ti͵JY9:e,[>%YVyY-d\Gwd!b=t%^xQJ8%RA(=ؚ)SdEBV+^!9 +qjjnrWy"p]AQg?!FDQ"uaaaY!VЈUT`4Eڑƨ$M ֻiL5u8ZffL;1ԉiL?v\9~Y~$S +`1pCXT,) m} s.z Y +Af#[b+B]9EU+Қpkp:Z*(]}Unq#Ip ܦ؟k= 2b] >!XMMpŏux}xd!0ڏ_GO= r=}lIjApgDѤ1bys,?c?60ʠo%[p;`>a6͈MTm;/W}ߐl3%Ȼ r NzcS4[( ~{Q|5pÝ w +i1sy MYE9c{+ ՗鑽~e*C*C5AQ'>%&3'rO3!3 =;f,\Ǚc Q8CQ)x{|γ§=c\}k6q_}+kkjwхUda7v#Oo _s~q50b~*3#3a5p8\4sӁ^M60ū]d\wt[ࡇ8n\#ifdPSOipdau(:? fxX'ude'/tIn/?3WzKH#lbRx*1xԩEp oV t3t8N0QG<lW「WoEXA..M'dxf“ O><J&.',k1Ky1Et(ӃL |nvWZx,1a=fW>\* +iy);V:e}[̨ʢOTN.v{W>V._|`/h_*\Ė Wqe!9FTЭ^TbQq. : s)]Q>]kj#|k"VŠ YYYB!G@ LUfTY* 4Aš* +9Ws#9ݚ5ʎȿ(+ꁯ[uhV`VVV&b-sl54Va*KWixJ"UQy*).OmU޴.*{(#vfN*=b{ƨf\<ֿ/J5]Ǟ?)UST45NT͝i5'Py˔kSv\[@'VzҒ+%i(1]%$]U|'>+ 1cō`N֟^Jq&EhN\f'*'~bL*=٩fv(9m.;$ PDRBC !!@@Q*ŖY=2+ZW[[ֶjs]N^o9kcvmvK~@}aT2 $x@Ҍ'$~%Ɇgن\{a"b-L3}͜+bҧQCVdk-24,vY}G$ټUZ|3reњߖ$Ǣ;-[^|muyP@1pX1,Պޔ*t6ʒ\I7;%ԜJImo]'ZIMHm̵ۋ͈z]_[`FX˽ mf~7 kʺDI.4IeI, 2?HvsJE$>P✛e=Tvr;y&b0@Op"6`FH&+r]$/w;r3M2`-J%Wq˻ ,ͅ,""BBB4>r +&xB.\t˂[tZ\ ȭyIP"qJz0xѻ^vvC Ⱄ)\ʍXT,9 dXW\ep'[킟nd֫2aVNSD0!"0H +p;X]% P܀  +Lp1{%𗣁詊𪢨U5&ռb6ޭ&ASPBl j ZW)yLK RpMK![-å^ƀki0Nq(̋atf|;5V +#, 6҃+^VB A>ȁvJ)x˥-Фa. V"BrۈEF#6Dl^@ TVk|!|C&D4Zl6bd;ZrP&Gwܐk.zc ݐVxs%9. f5s&ϻJbҕQYrD.ա?HN(!kgwXϦ4@MA#Oy. Hc>'PyN yuQ.ܓajtsr1dqbi9H>6X/ ?mxaGآ,\c`(KmNS8jc `?F]™z'3,0A)m;!wLڢ.=%6[qpL/9Q?ES&Ob&ya5s%pu*sJ%Y8xc)X G;^vS<} )>J{ӼrCg)YiQS{mcbq=sy +-s@G#GaT!TQd&P94^4nv. Gpr{kN%XLuDcNJ/A|%ڍp,)j9Gh[ b5JՍ¯F?^bG^,ln{:_ChUp~Fwe' +Dj=:w0ūSyL ߋl/2ov0Q GO+D 'JtO>͵sok0-G1CMʈ ^t9ppၣ'D OYk4?B's?7?ҟC#k%=ge#9N]SkwI%5t[ &$9PF?卭\]X#_‘’Q OQa8"tZ*wOP'KdZf%*khx& xd+?p+O=TRJ>TJ.&ED!ܫ@pFXϖF֣e!zTQh>Q. +*અ 7,T<QV܎&%p~s,窫5ը8m>|ipv|N)/'OsZŵUNr7%~B {Ŕ2m;qbljs:NHJ҆WI途 +ʠ:ֆѮl +hJKSwh +iM*vx7~Ol=ߒ~ o'|>M:N,) :@+E왑LS>ٮdjR| +TR0A +dt,OaXk\>*ʾ& +%@W?ރ,3Y]"gScIUuf2,R ˭,JZˎʛ)wn\r,WQ춧eP~uen))>/3 :$YTbWE~ϵ$@<<>MϯV[DŅ1se+RA29*DZ]VsrLe8Wcp_ۈz ˚ҁhq?u ij +PMkO۞!}vUX\&#g\m暣郲+ӳZ鞭JSaoi31ϓF_7AkԃjP2.7p"Wl.۔O,:eQ2Ki%R:d*ɿ 0 P?UzWJbh(S塙[0Re2GTK鞖?<O-L'E +kY!,Qh%8NQ/G%.FMĸ bJ1vv66ֻZ || O >APg>|RltVǤ@A,f1Dp$X !=V=|=rm݅rϴI2kՑ|.gk3o5xK,qrq) u1H,?d6@ 0wQOXhnZ ]#Z>JvKܸ9ϼ%' c!FcQc82c;eN,B)-B5П S7oGc =Ҏ :đS'!6,ǎ89Y0y ?N,;U|HcvkQo)v 1n/CŎ cvlOP[(b34?:qUx&p;!%&5%xo)¶D;s^rgHnT-Wη#ϭ.1UO\Lm]J,{X +KSCee06I=S(Ibiu0N#{;nOBXrհhhJhV,UbSb!1)e>XH5 =s;1oS8yFcG4suHK1V>o1}u|Gu\Eu@u\\ ט?_ +uqޝT#y1Zgrs$z&IpoFJ +%kQ7q9wm-Er?ǖ)|oF xq&  +?`JQW)K;"Iz 3ngˈ%`kW;#~v5#tu.v\Žwz.8hة8g u +C@sCBTIi)߸ܷ7g^&#fvz??ð>'c^cXgz6=xE(줄a,9AI]4N +=@]ON#7a|h&~Dx6[l` 77h~jg.,;p?O>abAYu!gTs~h9dw4hXA{KSXv('cqw~/Ϛ-Kis_ V ]p̅c>Cpq +F"*##`%z gwQASy:.JPt8ZA<*N*hTc-8"XqdHO86p`(X iU+8?Eb2*;LGfz4j9 DK)#M^d$l6K*E< =RhK)$>SL*d5;M\ g.v"mXl$vX̶byNL7$.|r~x);gM*r y;lz)1J%K-Nɷe Z&J(6{J.vJs\,zѹ"ND=+\:;h`EH"X}煘/O'nQ\vfKnnnlW\kng=R>|taGg>xsfdjFy"Xg~}zqbw$'?K,nd]b[,_LtI4B^@nDw8svBuY 3rS5"ǫq a07'+'=/)m_XT6J+8"&9YJjc= ($|p` #~T\gkaUXE.ہ6fhbc&|(;kF-!OsbZpdhx,'v"מz%u~y05<%vI/a+418ǧFje `1J77"Qf737 툸΃2sL w&25,5kXJm¶HFf 7e/|w'ȋ=8նy4Nj>hj]am"Bee,Ej9ًQ%+r{r5\>|M7l޺m7y筻v{۾>w|S==rc?C_<~O~WN?䙧~7֋}{/\_}Go?/~_w|}Ye>ەxSn)#8U'^o `bGQ  aa"b-LWTl-(6n*>>FTܭ8 |V9hyD"0UE3C =`NJ7G?(h[oXN1K˓ +3|ew'(/]KN7i-Аc vt '+4/b.K4/2[4kNE,[jͺoڼfbۺ$$$IH<vcvlv~ujU X?FƮ0OZsZL3 wFܞsIavm܂o߹sf޽||>Ow/h<*`:9_ݘ-Mg4(9*M,h)1'R%Nkk có = 'g7k7j%0H` hHr\N[ =徾ʹFmfdq` A@ +S,||m ߜ̉{W`P 0p`` !&#GH?4,a JFd6h)Y:0`Y9?0#t>t* 44gώVkC4Ն`yO͠YvAB dMUdI?̗|QUsV?=ZQT8R$L ,dJP!"s3׎^=_m+׆'%#cM/aL bt | ſ ߞZ?|8sͨ^ aJtE EIx'16P6DLjN[~Ш<73#Y^*kVd`Ip8_T L4iϐym6{[3ÿ5G.je*Ѧye:]$Q\1M8,"N[R4o!o\V'5k]>˨V4Lc +I.<(z"~ +[㝶CA?6(/*mT3[x LQʼndO#.);pgv}8wjžśɚ-O-niGs.Ս:T_`Z mJʳ/"FsIK2gO.jDe.߰%kL\ƴQtxnUztgѱӛ?E$/Ů|`vrtXs䯍[6n;c 7' NY=U(czKiI<%(< #00,Z<0Vo(OH 2J't $, + Kۃ6 Gabrg9:0tM)T(%Ib6Vah] +T0(g\. / M2~0]" NQL i +|@ -0=OaM|ahE>Wz\8eՆr#Lhe +qΟt gLp!hGd>T,.+_}mzYe?YZs4 U΄ʛ0"gLX b$!(K~`SՋ.\=Tt)OXA^0YтE̡9GIa)'߁J_[ZgvXLiTy҂db. VS +8/.oLE`8S+Mw)}}ZBˬFcMFj6LNƉ +bVPLˤř6|瑇47:&?:523\ZY6 .YEN"1t'Y ~{W]6N>iykˎscݗ7 lD{ktykyk;t{)xplWl{yz7we-S h5K^nc2ͱ8K4eê61dekq%HRO7(L=7<7<7<7? s)L1_SCq7wu-ڥNS ah&NIQ"8E4![`I;7slg v6ըfQ !~>`4,X,3p:$*BL⼨쮷}gaGʐѸԼAzp7cd/06! T Q&SBc87.m}ft`yj%5ŔjU`($1w6A3"9> KAw<KNR}CGvR5$Gj˜fQV TQx*(Ƙp@b +f蔏K\B xӯ =qThsXӊs"O8Fx p,+L=IѸQ ̸~0 sfz $p8= b|o4RMG6SR{a˚нL\ep7k +,b1'^6Ii R<"h~,ۿ?90lӛb׵Ukp=T2'xteUEH|h2 +I%H!]}aCn}eS~AS-^oupvQR e̔7 9\ +2R"RM9=y 1SB=~ |m+/+/oL3۹q2l:$7`ޜX(: IJ +Π2Dd\Ef)Rw_xOc ʷ[O>H_|ݷrs-][|e)MB捘CM2 BZ3( ;40)ڨ3Dp_ijBsgSK=HEkݝْv V[by f8`#Y͔ Y'Ϻj7宝(4./Kw3;쫎يl/Ozh#E3@[+j!oty]wP^L% +wpk}QtvL Oku`2P2lѕd 'J6H[zH|XQ,م ?76tH !vb ހM-vaŶ!mMs9ˎ9=GvN4}.羛}}裟3?Y{>34 }ǃ=ҵɃa}hL NeSKE? ˨`ڽR@|Ty5kC+WEejsubccl}s k䱨{g葬CEC`M30Z60>@W>=۬;C{Qσ2fkǟw^9w&{w{vD `c׶siD9}x6,Gg.}_G s>SQ鷃L{ȅ[ŎO_/\n{.zK19> 0'?6N]Y6Ɂ +)< d +\#%*."6K*RR4E_SEТ4iqkkk%iPK )\.1ޫO| cWW}}N&>q|QʨA]BHQmETn݂2ج,Fy X5yGHӺsj +Vm +).b@%=QB;XsjaNgIcF6Ҳ!>\b4xxXf58LDeכsZ+>xL3pC)s\"ό_ø_w(;yFֽኗ5EThSJ$,:n4SckTJX#ůH݊,%F+ÉBa5Yvኟ5De(_t*M', +8e=vDa[69YK ̬}5ya'pi=05\ Pe\]tA9;P9G quQwPC;՟vC}5[O#wbS h9- 2YZCa#bsf266!it##ְ~n?½՛5ztAzB\28V4B2zm*hFGc iޔ$uK $8{6Ngс'`Zrh~~TPヹ Tx8ިnc.sŃnxtڡ%J.12z?>qv/ێv}A=Jfs|y7#| ɩpإvđ`*eg k]<i8?'Vt**ie4'ELAF2ppcWG6ȗ r+8_<m<k=KLٛrXxGc@Ըr~ڈ%Pc8n&!N)&- +c5|j+|fn{I733\ legAd:, ahJ)JM$EnB`,mxa{OI7h\gKV[#+]q{VΈ!"XNj*-PF),M }80p1~ 8q .Mo=J`~'镖hV]rbVf@-`8DZiPYNfq$kщ0cpz؊\&\i<]nj]1cyrZ0 sb|OR( ER + PX#I 5T27_;O5,fi5rc{4؋<VKIP )ӢL$,*ES :priLjITiʤIӪm&-RMٚkӕ4dIڜK\ ͍c/ 4@!Bj+n/GoF -vfGm. +vܞJY\Λ4q Zጠ ׇqB;ChB[÷;g?7 `xn 4Ԣ'F圊4o Z0 lAH:x}7,Js8{gP}cP0lzҠ' E=jVˤO̜BĝJ1ԂQG<#>MaNR8iА[4i0zl XY5ʝPHu(f N<-쑤!?.%~ 4nFʞjъ4X3ff뤑 +frtT͒4݆İp@~.^Ro@uݠ% / i0S٨Eͥv`# .+kyZpL)F5d"<*pDD>ALO\$3 $ gu4X)WE'idM9E7`F*J+UaQ]ȯ #am/Qw c +0.s=Ͼ1H噡x;Qv8iIzEAF FIFUR%$!Cگ U:8aa2pm₧ʮn۫fH-2a\qqZ;UZ8aD%dx>4H4X kNʻ]Y͖If2h*a#G9\.̢ʈ =:({Io=5i:3m2Sf4xyhڃHRMԮ.i#4K"j-kD $$ ~nnXEo?|g{ۚ7sA&7FA,^;`US4ĀZ%)xBoɺ(-x]󁏶䜿N^?jiFd`0{`6&dT* +yT)eC@`' =Wr=Ppu"ؒN0bL~0K=b{ .V62taLQIq"uz8eyn_;~jl@]x<5RWm PƀԨd>CI!np}Dc|!xf9NnYu_x~o[sa㉚C#KLm?&9+&P&$IY0ߌu#,5*0},`=h}zB_/+~N]-f*rէzk+\9:6ziH$#B\IѨDr^ĤD\ +vį`br'~=uXIBIt2&tMk׉]'6x75^ubclv$ Zru $d؄Yn $#6ـ `N~I_? 9s@,y!d.ԗR^N>񏦙ˮtT61Gp_i5>Nkazao^ۮt{CU +AS]I?Kxc"ݦI=Zslc0Y,8H9"CbL%$" +Gu2ܐ$J뀟ʳ < Y # ܋l+"ȁTߟBơd>_ PUAp,`Gck4(cW)F Ÿ{'Gs/d…W xHMP@ξ(`uGBсKE@FpP! 17 PEP*(v:%He.g+ạe9`_˞5rfminoq^7 o n B # jҠ! Mmi0¶Tqk +a}Bk3kNdc`ia-oJ(ۄ~^/:b>/: 8% iА-iВCR0I+_Uӗ9'3H)qՠhhB.ܕ =~O%>NM9iО i0D3i(snĦ4K R?#7rdIC4!qnԋw> IQ g@@AtA*%1JI%7b$?aĥ,E9pZlB搌Kj1Ql CxAꕵ5zH# '=i0(-|JX)PM(cVO4ZDi4bނ򤏶-'-t`S3iRb:tTU&תIحjŃ*@HnUQOlTgZX,g(gB`&>*}Z1I{ ~]ԭ*%ċ~4I4Gޭ+šZ[T͝u +v 6fQˆG2u˻- DF֧w> 4b'T\ݳQ[W7G-7nqn̹*T8-f]EYVSR!3w56b@UJxWH !D̟?֋?ۮ+JC'My\lfE.4^kPM!]K:}a5;nMk,UkKbŠ,kbPv7)-7>^law6ۖ;Nd tp ;zZ_i얖rZ1:M> +Cϓ4hn^zyw[hZ!3y'YF݅/3]؊I*zfvԗ6}qYonN_43cuYήUۭ[W֝]3nvvu]֣* /BB!ʡX@P W4r>3wE_gtV=,o+Z-VUU{L]qu>Wx^aC6\<^y)`>aiG7aYcec랗|'`Kp>ѬCi@!NMUn^U&0ZnAIQ+>{o\^j]𨇸C螗G}a'$M)䱻 DX߆2kJT3!*]ȟ\Mfڞ ++W 0G;AI*O' G͡ sp«Wf.^-[w6BfT45{uv85"!, Jn*$KсxC+?t × 78DM^Elټ]ۄK25,{(yh〹#\ [(?΋~/S-krL,A`R`/fá48|AĚX ( ?^ƟgS> +ρjYHr҃NCǧ᷒ +rJ%H֣Bɰ`B"cI,[޼aC0ĿsS|$ք'@hi}0dm߀w^:_/erPͥ $5}(x UIZls«1E_J 3iN]Kg RFÔAtsGjHa 5V `ؠm:>a{3 ן-FJq3ⴠ$q<ɫM'qO wuf_.6HA ꐷ@ FH=z1Ј6R NKn&ŷ&qA-}ߑ 0nf`-b6 YB߀^ )6A Fl0cCaus?jUjSܴ2+aR~=iBZL<1}N/ts< ^<ĭ6hA @Fo6dR@8藺4”FPQϥlTO[P/*_O-6y`=f dwB6R$1O,Iܔʛg2ި+vz%}6ـ^T*nI-#E 6C~ I1sB,:L0 y2W qK]NyA/<;O-V(nGUucu;j붵ޭQ +*r&r<  + +T !EA>g~m'j&jrw# b " +@I'zPJ;=ci2=WʨR sBd4pzy \ Q=MF8s ' @z> "7ک2bvs-'N(ԩ78vCԄ9:)2aNs D. }O!%a|ߕP>m}Skrp֘rru~ʐ:QXQU.E1*6 n;1IdEvqȁENw K.`PA*r\MSҁiT-w:=h2+jP/شryiG73ZqjGkQuFIگZRT/KQԉK]BwA rP*׼2]0]Ot)eϓ=nal3(`Ֆɸ"ШQЭ֪U\\K4*Xt4 +'.\0(.Tq!{%.Ʈ^W?wu[&3epu|IaAPis +(6Zm9\]o r.ߒPgGh[\w}wI?$;*-Fsqe^)5 +Rk +m8WYN0,rwga zԙGoK~᠜lOlÊ)J)lf)~D.0YT~BZbˊlR.Q:pwArMl$a(8*<–uV !y(+{)u4я~f7 +>E'478M6&Vؤ"]".vNAB4Ѽoє%c+]^gҪAGéhxv8:`F3Ȓ~Wի4b鷝XŁsn:p!;1aK.,#AJWD[WoG{t Rִ42L׏1. ;g4YP@ҶwoO_=J FgO$IhfHuZpd3fn gYfRb@ krYFݮ`6q4IQ>M9MQ9t^4C `EsbXO5%y\4֐8zN'?xDOIoe K7/Pq~Ʊ?>%m*S&$lP/f̱Y3aصߔ)nĒ-I+)a99NJu"]-&Z[Ѥe=q2r 5xLE.ڤڡ_ P$dC0R3AҰؗ1/#ע#F&EFA3FOG}i-`qՅQ1́ [PЀ~" اȐs&DgcA _B|{@kۥO. 1Bt˼f!pa .y psngANDI>2$:E~8TGA{< 'B|2:~ +)w 2 T׺_԰)",j6{ +nIm=?+2w`/f%(q]=7C|D؈7(z02*J H +A` o 8ҷ\ d@9(,nwW!CB{`ʄu[p F p8$B%Z|F5uq.(SGku:.g,3z:eڙZK]Ԫ *(}Yna a7EYD(օE<՞9"˾>!eeП?_|> ^@H_ kvAầU݄a. K006)#v6b$xFg ;C08 DҢ8!Zz'`|v+/Zf*ڛԍ` ё@Į"U3p4[g~?0.zaFmPG"gQӴɨQt9b=~? EO ;!ؕ{YwO S S? T7EAH H#V@N{ $ R| +xNv85aH`~ sc3qscMi]46w:_@C d!ȠYdPC Xy@P<6E1`Θ3GOFc.D=^uID>2{A :dHOx tw@ xi{@l>’TG܎9έY {g4L_Cu~{l` k G` 2@# +U(H@R "; 'gy=qqG)X5XRp<]47` m^62Ƞڳ A%0kJy2XAwFpY#RCAO4`0NtFS.GSc-s:d 4!ugMy ^O{E̪rH'fG´%iRx=e wS]A[2L0ZQF31Foﱁ \d#.xd? P2$ G #iYquaܨLҰ!uHE}dK-av#|U 7y@ !8r×%o16X&D{GM#FCO֜Nq*e>=72/]]+u!W!Y0_~ ^PL~trK9!|Y+[s?LNWBU;7;x‹7+s*FϪ}xzF=cȠ@ _' Wpxa}-';mҿٹFaꚸYi#zvWT6O/ m=TA荿{~l1+g^:ʈwYUm$n]yK\Fߘ^+Oj4̮sI+tn#.yEe:/.b"C rv͇Á`Yf6zg=ݾ'7.۬lfw*}®靊rSVwIKnI#.{zlh"CޮyP~x\u̲Zʪ&+9oKI\ ^ag6+K2o3ENEaKV&Fje. ܌e<:b5SWw5fu}MRMXUb` +r%y2ky|SoWg(MNy~K鑠Dzb@{S 0(ʰjcG(Kmw/ٚZ_+:V]l3n), jU&U)_;489f4'-1yRtA M@s%Or䆶䠆V%W+RcC^ҩE)sIr<#A 2dA6x}q؋O|%isM;#,m ,uɑ3TH*]PjK"LouGlȰ)2AgïB h{G:Nx;W],Od7 F" MFԸd3.괛Дz$iE^ pd!/h󽋁cl~'ݺ]֟{)/;$gn+>h%=]ޚTx/ϰ(,  D6ظk$kvW & RD0Lcf0C nplA`")<^O~<}}y.d4ggi;ْyVaykfQYzacA':^Tӣ[.T(>Y4+>oXŕڀ]")]}pBqEog\n0^h3dUrkȫh[9cY/0ҏX?.Ӌɵ]yB5RsOgoUpff-)+qALnvaXP!4\V5)4PjuN2fT֏?t]nrmٝM>%HiOreiƧ!0=?^mۗj+ONk!E@$)T>nuK5u/Sk{d@USX/LޓTՖj:йUTn]|I8I<{)uvbyazMN:c^k7OuT#6ljz[㥭/{n%Y.uUT H'{VSEu9v¡i{ EHu΁rZx)WƖ DP2jx4* ?6 Z6L]oPٰ;Q>KN*AfgCHKӐ钊nb wMqQ:X4Ƣk]?dZGj*F[:9Vnpw`Ә"!fOGP͓@H&pq/WKPy, ǵm]< YKD/ V/h>-Q&XI$ `.%b I֡( ?!{cTv>Џap'y88a%ߴD?BJ읉NpO7 g. w< AoO .)ֶ,og79$0D n!Sz|o^ ϐv`WAVe$!nBӺ0$v&Yi,o&cO\kC푓l# m2hAY)BVG\Ho,5Ȇ#~X*jC2s3dB7e|K?@z!2PH?ʁH=a@gclpcbh3pPjx}pmHM e^sHK-c +yaKD%MC58#Ɲ8*!@vIܛҕ6Ң64ja҇xCu9Y0TO9lƶP8%bGlM[-zb;mB vǞI#{3w|ֹЫ 6=4tFğ}K,\*{0l;e=1sHքWG=g[G+jm-SAQ @vHF+#!,a7@ +©guBzo~s;ٯɭb +?Ԣd|;U]3D +E^wb({O`}(% 1fZ;\;T6dtJ +)FAU/ *4d:ݓ&*;/.%8]ث1h&zS^gՈFiP U+ +҉8ЈC@٫L‚5g?6~i1y}DiǕJ8=65^hf5Ŀfď*LmQ`&~d&@ߐs3Ʌ0Wy4j?Pz4luUn.yGTӜZ ׊[Չ׹Iq/&8ebD<*^$YLM&qP(v1iLQ?qmgwkBVh[.mbO\[+ͣV1/4r$x%'|d8H2UKLldU(13 +%5?jrom$[`$cz?`tBgThjr\ uһo ^NK1?~ܟ20Q7o Aħij +ـk2nNG!(~zJs%gՒM O~x`A5 nLõ/NMx0ߞbu;ѿ? +aX[6 M*/v@#2;37s(pd(tZ/4 v cӘwf8b6s y.(m5ʩ0Y 3o + + +Mg9 ٣Ӥo$-/g>7[ΘHP5h!m^|^;S+t +z~ѶpGp@D޿Px5#HT\}r֋U-$\p==y2]mxjz Rfc,F\/ܯw]dešI>OȦ#|b?qFt'd~ꈂ(eٛq3k\I T> pLeRa|,dX8XöØ9#083{3[x2dl"4̑6LP2y&y:5Qʻ}ާcL|0y@9kul#Gfl!2}0=x00mJHre-BZ3aL7N_vPi@Մ,p&d1_"0uy .º[O+ZޯACAwD"N;'1l'mMB쪁`?MJf!4jBRF[IR6WCxk u!x۵MCTa1%0PM : 7/d.Dx"""BŐU-G`6m:\ЅZׇACǍ 8Fo0ĺ^֫|)!YQ>FtDD+\UnЁU}^1Af=ģ0(w l iw 812Ne8oM$g#!D qq &D/GDj!B#z6zaLt$p ̆d0yo,Ca{zO G2N8HFJc ab)V"DQh6o3`:v|$p+ˡ!|M=az u|x,:D:odZɉK=\;Wi<SC1᨟,G^]}^w6/͞Q$lFF4Ĩ2}K ?+E'e CTdDT Y |LM_=o0䭼| 7qc/nc"Aqpb+[; shR-V2RƉL>y9#߼ω]6sΛtaj體/mz䭶oyqSl7V\.õ3p=+:G %S?BNiː6GDR(^ԗ7'Brǩu:k?q>gў pW'9 >pڍdOhzOR >{{YJwmUj*l~5⎮仜/9?xu=^'vx*8-tՉ1|C;Iy323׊|.L|Y)"l#UEs,+W+~ΨsLk<û#3SsO.M44K')eXtCvlT5/UϿ[*JQ奂| *玜j3Yާ2N_8ג>;1k@@c& τ:A(ʼA(gYЧ>ldn=}>iϼW'FutsE +{\*=[-9;YMY ゃ쉀ldO AH ti&rtݾe69C3n5q.|YgUaGEe*Dז2ΡZܣz9Q;+ŘZ1.RLؿ\uD7f$L(tX:6P4سGG\[8^;,w1tIGmA{UEkEM%zev!Au_ʂQIe"\P7ȃ:AUE*[$?JEɧ'AC8sqG.fsSS5ZM:qgFeX-Ģ%69m iǽN+jkDD#ėX +u d:YHbAhҭj]m U!.v9Reמg::n-\6-~sJZr\7zl:s/rwj<~@([i3Ir Z?TmK?m_<Ϳ#3̹21%w}ЙYq37.7κGh~3+vxk_kI35vQ;랪F 3J};R>{y~?$~ߎRիs1ch7:#-pS"sPo.QǏDuD5WXPgĞr4ZiҹIqQ&1I63m /*_!Ơ7c6`KpՁX=I'u͠͠t(Ք̞2^H}ɝ80K14`ĢX㈡4ֲW5$ۊXbs3?cU ]'hcD9҄R^7vΕ;׊Ό 6<Г'H3& +Lf<wwV~a{{lNTMtunamYBWR pHNSi"L[YB(4>PAyg^ =#u( 蜊w%6!XMDbC7`$x\H4}<S; ZȘmgT؎˳p{#8`l:ˈ.3(neE' +4XJXA>vDpMD9mCmE$;ٚP5.r6ezfl{dψik/'̘5ӱ%{%;6o6l N{ -Ĵ +!7D d|DZw a]"""{)Lwm"gJaAkaôli5dZi%cX1?Lf̏ |2@ #ZrO[X[Opdםu=oo{$ELGOn<χ9]Wsk||30|7qa+y똧oۙ a4iC:J=ۼbc#p2[q;4sa2̃Ñ=%u^o7Yq ӂ8vBBBAPc~!׌^#?L#yX:̇٠υX!yr65n:/ׁemOmȃ yfBrR38oQL!Z +} `oD0bK, V,C93߱Lv›u^!r?yʧ#4j6B#(]\N#)uf6OHn.Xx+|-rPbX?`)laβ 7N+gWjN ثAx"~o44 .3 }'o֯eX\6/9Cw5aeahXK]LJUQAEDPAD ;hՖqA!B!’ 6A@eeԱU\Ǎ[}:,|۞'Ahyy'w +]@n['BIdN/pߙ,Ïт5FD*gM 2b6w/C> }i$WP00 ǽ#=!5 <rC ~gK\;+^)*Z?ϣ̞Evz1{:"[Lh=W#Ѻ3%i$c4!dHѝZ8Zi*GSvǬbKw̬5nqѿoq&(B~Dm'&4@4P:=t}qIa†m㼬 q]a_:7͚:RoNmqxN l'2f\a85y@J+="DC٩7ܵ3ZAJ-]I #7pICf%e#L`*g FeՃWғLm-#I]y]nucԚǴ_WЭeIBpKYԃ ~n'셧 +;_ T +d07lu蕢Di9H ,Rx*=AwZhk2#PEeKKϯQGnTֲLeՉmZE\I_(ʚC>>\WB zTH "p;Ё!.@[Jhh7_qKUVzYsk.Oޟr1>Vĭ)VմK3GDjtэ[~3酅pG +p-dv;/}UM}T]Uq}eϙavEouě&_X$89GfK3yh0?P Ha"%V. +t aF0!TCoՍ! +( +4H ^e$]•$SvxOl:&ޞ3@~@sDGz;:F6cTvx#$ٳdFAe'ɟ@ym䌙C#d\'~J?_AMXgō'ndLdfbːAkOBF>X<҃6]h +X@w @s-@U@1ɟ9#z 5ooԻvr +賧—N2DLMe2yOS*:M"%*Ko^}SiOC1q`dcd-K9 )R:Eڗ<׹fy󽿿c[D7]lz`&MD懿}D="5^#3ZAt2 yHT\/G{ iyN9QǭA +)*I)Y1Q%7T134Yp{Y}I,[`)D87N]Dt? Q aL]6Jɟ){ecz䣾)Fij 4y)~}c~__Q1JDGcgDi*RR1 ZQԐ#xPZ +Rи?ICe;A~VHq3'T|Q&c hX q=BT#+Dr%"UpQR6,"@*W9?눒~! +m"@uh3f%kggZwZ5,rE GiJi>N2F  GWKwHM|Vb]U>y0'7ذ=9L&k#+bX2a/48TH3Pp#>=7m~G3J|Vb]YZ 5r>Y7&s,p S3Lb5M=i!TMJH!HKx]{=+ggo +ks˒HR*BpZ KP*?3jC0w|6 uFcҀ;o.a!{XHXGfLByN%˕! +55o36h@t3ÚBh +1w4 KbKw% Y;֚yt9l-PD!*SD5 #-Z_'m4,¤AfA t{EГN\y,`}/_On v! o"+*!ЄR ⛓ ц^gLܬ3Kwpۀn5EɟA1cJ{x66p&6{|x#"8IJګR 5X=x^gTrUwDrGoXR7yPҨ'"@KN >mԎ*4;93c ;k["2^cGK¾ ;zF/khGC~ `k1t|j[k``eBa* ,g˸gbXs;&2F;Dv,.Za(.Ue 6_k~/>juʰ+:"G>:]͑&o"0!_cγ̭VNq.9X3(فpޔ$lͮ"ݎăŗƝw]اfc[f,fi ,X=`J,+ϳ\M\J#i>ԗU'#D3#^ΝZizR S7&3ytz֋ʹ-j-$4Y=Nz0d}?&S\ON+kGv/zgBk=,9^Aέ=du#2+眪2/x&=ng3v&?fbLgYX|yX_p9uP^'j,gԖ*hٗjX]crpU/wBn˹*3w+_-;ݵ<{ls~2ne9p=I>3L{YAyfQdAŶ;Օ +tn$^ݟiRmť}6 KvyE}eae(X!k"Ql(RGbŠ`AlJ)Cg3 0tADE@hu%MbuƠ9{·ss}7.qr +iKYL[iL$q_T$%ע$TKɹPJڠv {܏ x;Oq[8+o%:6j5tVƌiRc)I62]iƹ$ε(U\{ͣ _qUq$Rœ+Be93NkXYӟ3o 2_ν $^z=t'f>P[[kX=ZSZ}" fVJ6?[npNш=/,M3!DEl'f[\A^G?ν!#ux/@MsP_)±5ʪCJ+vMPi¦GYeK/LM)uL/vWչ*%RmTrQ3\r!! յ;FjVgVDXZ'$hcK˜ZQdQ{D-u{đU@1JjsgE]B v Cf}TxL|=;1,=Pt9 2p{`-]ydOfbϋO߶bM<Зh/wC־l1~y{w޿F_?aڸA3A{@Kv<҅_)6Ͽ`NXoVb͛uz؅U]8)?OJwszI޿֗#}[)@{0HWۀ}|?_4 +h!"'H bZ7 +g +#žXLr2ϰH+d*yك*`Dֳ+L @#p-k3)t4:ƜTj`A3zbk[{g`]@HYs4e"M\V\%-f-gfI䅿q_LhЍH T ##f D؃[n^wǀS?X̦A?c ds-iY5F a(f0->D/ +gx)#h.8Xڀfq֜-n@N tΦJadZ$e~.>蒘~1vyF5fqBDE H1q36FD%V@:=PB&" bWtAVg.EdE9sowy@`F=ԇz@mTp' zq[ +y);z +1H<`sbLaiJ2&N@0'Pq6ЊВ<Ӛ\s:_~#\,gNGc t."q.0OS..ߘ_'>sJ2E噠 `ihE}<6knLu]. 7<( 5[]% tT]/LuJqzyn7&ݞg}rd}܍Q!T޺l-:rL;S:?pHuoPz ]Șv/Tr/g~)wס4Q»<λ7vʯG|P?I-@5(S >jqCO\g{#ʟ2q6m aOu"IC0)?z+V~[ʧ^O)>}O}ni<|Jsw|?}Pjmoit7k2sS^F~{a,2^V P~$T5}>ؿW =?=Y?%pU +qip=p&tYx +KV +<"18T%Lu,g[ɵ̃o1su:W[jP]m$+4--YIDmEsp,~E|/ aEp +=t_v ΠP2XQ]“TZ_6HInUq +qaiQiqԢUA~m^^CNޠ(%1e5ELKilKrpF:BG8^Ņ +Ujd*+btIK3VKrM +-$6K|AXwGa(^Xufr2 =tUoIr> r<^ yWhZ5h3z0jj}e5eQUE"rYVQdVe""}FLڥH6 NZ1gn4r0N{IA_yW)yO4BC=6:AѽX ºxݼ#YšBӴrFNbIA ;AS;쫭@ 2J@vb9 r9wU2P(92%thoRB206Ԥ&JSBtQҠLIfq<%ňNvG/Q9ta×F9BkWv&h /2 +1(4fѬX#篿끧<;{{3(@LJ58^J҂Tw_ޤyyfD.m脧9%nƔɅ; /ApHH"MMłC@V4 Hu:8fY؝% +P +k#C3Fo8>nCF3.O +(QxC/ +M Lg> 8gs K:a5ݳ`3,ļ^ksĜ>}`1b:EcB:L^>#@B:?wfs`a`F*0%M% a@s1,dDr8`E+: +MJH<>K.^7qwd ;E|7UH.MM6` d U>2@B=pO=]`2?,-o~:|Ob,)c44AZPP!](>D cX3Ό/??Ĵ9n,6c04I sdV0L( z4hz"hF* 2,b3@36|Ieg}k$κZ39SϡYsgb0\?k͎a΃AW9܆*Lޣ:Ms #h2a9gN寁 ׷v\ۃgv4{7&dz4?Ι5)C7Oc +fh3C9B lg;i|g176ǵ@f[x̻@ dΡoQ"z-Scnq.w)~6qމ[vq/^[ъ LCSts k»aX8dq,;!X}V;c.c=vx-MWj"A]aY}֞D5$WmO/ҟȳfu=u~Բ@*Y[h/\6/=e?0 !\{zu9bHAeDϑTUݡUܤJvk8rȣNmVd2+KL!ƴmZrx"i,!D=l1݅u伻F8$9t +_OaլAv+Q\It͒x2Ӽ+ժ媜DYjP4 #o)4",WB`9xBOp֍siPMB&mٟE +9[8u9{̺1)ToFS 0dr 3C~kNu~F uV VxD=l9֙}aiߝns’uPS=*_Z'r/͕yr2||n5Ys3ԚRkְpO25,#c p; (]thR8f2Tm@a稲x}BgAAL]^n&$$3WX+sf(rn}бpO4xr͠O te十EQUkQj`(/M_"S,5|3t*aBo +K/<./<Zx3";|yY=, J?ݨJ- +IK%%_߆K3Baww#35򛋀Z*&z_Ȯ1ǏV*kMrSdYuAR":V]V +rBLOtzrȻ(셼z>t94,Ef:qM1N\)% jKRdi<`&X_pXpB-&#Ȟ4C4Q~ (%it Yh4jo=AھI.t9.sK*9 >X_\ Vn=P H9R~m `$im@juMCY +Q:{!IpLtx-fV;~ggt7=kq6aW6@ 4V@HI'`Nľy;svÞsɜs2Q; \p:yL1Qgu2rI3{xkQ@AOv^ߩ-S",$$IH +Ɍ]jZTҡזJJQRGԱ\ÛK)$k{/s?Zc[a0o޾&cG}9! :a~p8Vˀ-`C0iLIad!d4d 1Bf@B7~ +)*^NBC^ T᜿+ +R#L`)AԡG:gyC84!M&[3>tdg?/M1g?wq""gLu(#f06Y8ȄH ˷Hk;u4CY @:{n>;;*+=bM"nGwC}f@hb-qhnӜ1I@A,mddC? +vdѨ mFV^֡ pty. L72'Ζ׿ DzA#|1=Nlv%ZHks_F5H^#sbR6s6|O?eOy`QvSh{KhזdيV O  CylFau1W Ad hي֟wvbL,Ix5,'[x:ضT={v  2ȷ3c"8ǔYvj/jf4Mێ{N/@G}_)VK} }I+Owd׻TTu-F㲁=Vؠ &KQ)>"S+[$=^(^ m'k+9,[F_wh<j2:B*{u_cJP P2PŒG+LR- +P@Zl9 h @ls&y +Kw+^:!,p&'Cx2]'( P*X+?tc}sBvgdj)>%\/nZ[=!$J !m4wFw4$ ʵ}~ pE*ob aSPO}ra2pktI/g$fL#{7/\Cr>rLo80^ؗ觘V%%!FmDmh&ǧe%o0w}І: H2A +\{:U7ՕGCW (0t""FW7'`I &"D> uiR uU1vd׍"DP,9g?s{{ޯ{AE(&}(LÖInJ*yAVrazR2QaHT''lJHPm?mN bE,śohc/ [G>:NI2xH!"W4fVJ }2"5(YlȒƥYŦlI+N>e|E% b$,śUsC=&N*$(IFA''@1Y󑑵DC M ^\F ȴ#mH;) O) W> W + Ja!UX7yݏ_X@4s@jT ?9YnTH͝EB$HrurFE椙D +uz5mkڄe LaI,u?^:1)WAk`uߪWh$ԑ+ zA w\}HJ˿#ri栉V>9~)U@6zz0tD./#vk.u!XV!,Wj//]X_&}+u]~E?o׻(ISQ>ڛԧ IJCK`IX0 w`:C1 s46Ujij6]9X2mھᄒf۩Ȫw+7_4aaqGGcV$|<O/じѲ[1SOnŔL9S['DŽd3@A kPҋ>>IW|pv& }1?] 01r6FR 4v/ +@e?{`W!;cb!y#P0FSïZOל:0po z#.70V΃s{ .8p.*Q?a졌]~``C? +!{4`1 },Yj>à| 쩃 F]:fKXL>v a3Oޗ_6@<ׇ SH__#&C"fs&( .:`'>3~/c_s mloSBzZBylq'dP.> ` .a X~O; c잌}0cl/8MCSbM.>uN7PAHq=90^sBK/T(Ǎ$(<#Td9DNK Ё^&Ἆ% +>"T]di$-<^\SxbЋ~:XĎL"< s HwǑ)R_x%=\RWfL:<6Wڌg-mE?=Kܱ%v} ‘tc_ ?Wv$E,e +ʲTRtWˡ09p>n9=Í!mCZ] +\} amavpc]GqHf@+uYuxN$t[.{ہ;ny _q ^G +8֌ +b‹zx#'s}߂>ĝ>eS<.G_0Kf0|F_~Ju~ݏYә+?³{`J1*wV|AV.Ȏø~7`vl| +fpL`ޠSR/ҙֽg帿n#Zpusqnk~3!-q é3860k|N%#+ػ`K(s.>`^Ƿ?Ɠwpoqs\ َK8#3;p6g~SS嘊4D~LF1Ջ#O8c91wԃH +e=wWw|0C Fovp%pr4<[%֭Ic,6ʏMgZ9șxυr:ާnҞOϦОKg,CO܍p䅣C{~ +Ze|)_ͳ*Fٳ^&0)Dc>ՊsU} +&"B"c^ )]g꟢9B_"(>DgzؕaƢY%F*_5T. *n*) ʣ?_eJMDDW2B/WhRޢgVl`0 =?AGR֢EhX`H9 9N+M^fAWuZ?j-b""$?4 wh/RP; ѮCM/Af,%[ѨD.&]*4[U2wK +uZOFZ{_]O]|75L<Dzԃ)9prπRqЪ!`.]zfQcC!FCS!`(rJt*ϒ2ûlȷ촿캿R7?(K7B Jsz0Az@Ϯh0 Sy+6Ҹ`܋4NiY[Y袩ՕFUe@i>yƓ~kon!r&${NHN;%(5 +Z_.BM"?@yM!FAkJƔbSS)߹Фq-0)LuV;v' +yYü3`hp.8QPe@eІ +]|Lo~ JV1EPwCiNF%r,E5Rii0 +ByN$5E(m|!60yq84ﮤTS}ʁ'h,Ce] eS#9-ѐ$"ۖ,[6GfS:eti*gqO.{[y71wt,zWEs`(j6r?CVR:V!#ivB#Ց}<$ws]ܤ.sb%k|xqNf(~S$n-@жPgHz@ʁ~o"gb@bJ3Xlj;E v<_2$tLbQ4qDAQ T 2@ l:6i8Yi"anJd% )=+;'qfÅ{s{M<<*K!KOU?2lNxKٓKa[BoZNtO@ڝhL8[K5q8_Jۚz6SuݧəU_ra>y!x?Ck^m u<_ߑgt\ ťQ4W:kbu-˵p*\1p-zLSTS2:*g5 mjU-mg"kokhAԠEHn墐ǃzyz+nU{j_wg/mXn?؋tFhM&t}Y>MqPGt'jWvKesIn<$\:?^MFY z& =tw-$D"%^ޖwe,HdINGG)Qn맑[:Cϥhp %d,$Y+PvebeW(}\ё?K7tzX{:*Qf\@-ѲGxhIAxVYrc)T͇2kjPpoZ4S: YqyzQ(}a4}ܵ[$nٯ}*3f+\ϏǨ\FN5z/G 9c_tƴiqnqոipNS 8[N\:}eE\t^ΏΫsI..{id |՝9pD*;V;Q^kLg'LWG7GWm(wz\<.32Qug.t$Nu[ˉn{zQݣ***=Suލ26KSnbP/zxM/?.y3I93̧')>8/~|eIYÔpU +_IS󘗵hrç>} D|;R7Cfq`{!)a[)Ϟ'7";yA֧N-w=*`<ז93cP9d#( Lqt +dwb3r!C7jR \.%{1ԑ1>c %M6iʸ:_lŝ[.n|53>R:/D?vfk,vam+̈42& }B#yȻQƒi,G={Ǐ;C|ָAVTڒC!EAXyLb{Tۢ$뵙dNÖ')iѫjIfYS`Si.[ɱzuZߌ?bj:1 +/8̉=z͏ +&X'%6m6MEꔿaG[b<.j]\L|lWşMa2جxzy\S08jMu* /-916?S?S0m*)7asY0ߒꄕ֫$%=1ي͗'~|YKM%vq¯ldz^SI;+<3agw]Ū*=`Vq0ֵCQ;UڏN7KQBacQ#OogH\\,}vE~"]Ri_SZDQF"$@u/P`/E3&u!ĩ(Aa"$@a5r j 6xfm1n,ҍ52Ma1R'&" REH_F68>8>8-g(K"(0BnBR!۴ +(8M:8 Eb3gz[ͅBJf2(RL;FU@aHɩ,9E^Y `$s/;F^Չ Q: +yIpg +-NKPdX#nՖa%f+V 5Z+dk"ɺ]]hLW$XDyB(#8`mIdm\/lHsaX WlW#ՑG;ʅG\VhS9Ue2"ĥ?Ew pvnN&94&it۰:œ R HΎBRz#eĻ\^ZW4U/][1^y<:,:K3"vGɳ0|YbfW>ftb8uELH^砛{VH +f?֝RGf l~'0sӺ_GFm~`TL @L 0#vN'U᷻~~ߎ'#bdv&z^Śs׳  tvf]^ 71}pFZ0GLޛa ѽ{?C"f] 6`܃MBjP{^``O<0z |>'w_O+fcEPLgl-pnNB8{3"ugjP[C}jP}jM׏CN>y ^rÁ c9x^q(s@^a1ߝ#w؏&Wrfz? =ÿ$߀Z2GuE7\q ܩ!,. {~N /[Yv_R00cXPj".PD0s`} 6#0˨`#'yx9 ^E?8 P5n3a6\#ӥ]Jpiduqqt+\hn &PoD@BH}s7nc|t<\=q? •Q-MԢcR.NmJ6OCB|=-kpf148S3ljYwg86~.0A6 p8ܙ0]5)B0ΕV^ sд(*4|Rc?@m.yKjjb_7K\8qt*ml6x<ƕY1!/B˧pzIN.DG L8܆8+ +q賽8y<QՆjP *VF* \I{21J<*m?.9ퟌY98|=} PZCJzq`צzmKTFźCu .3ΧG%ͥe@R!ZVit4qGjEa߆8TmРr6g<~^=U(nQ +E(=@Ol1u@wVh14cӾqa5TOqh"ľ?T`&16KQ9{QłtŮ](@~anF^e8B#7r bEj-}o jPi*,Gy:P/QE6H;az9"t$J3;QCyсpĄ#7FR=ddґ.spib.ȏ B"&H1,cY8S/C.A&9C(aoTƣ$b8 +%SyROl"G ٱȌFF\*ԫ[tNZSVk=pxMppr,\BF4PAЬ-mVEijoa!Y#E+EV .&]2 ,.Q +#Bk'Sa f;Q.^G4doYJ)vEv&:w$W¢CRf`4D`ިh֘yL4*L-8S0Oa b߃qxBpFIƾJPT Ӑd\ 3h.)ڤHhb`1CiMCYKrk@fPj&<$!m&L{C/*9A#QLM>Zw ,0Z&!:eؼmDBJ )O"ΞX2{ +I-%ӂNA "5/n8q{P.пpFJqPQ.hߠa }#ڻlCB,(.5^ 3 ٲ%!PX/3c=TRFiޒ-vƌecf1"|zw?ϽƓk|f S'111`_`iL^ӦM׼0ZmQqM=X@@{Qވʁ܋uRsǚ*|Аik1%ȞIAn eܺ/ i èE Y͈0n7t>%xwsx8xHpm l^5wʽ@tBhmַ`̆Ό +s`dwgxF'b"G`X|F+2O#_qx{)QU,Hkh{nL ã*㽩âcg1xnvc'>ŎaHfP\?sMc?F79o]X?T^)(5yjlm$'u9ّ~.8=cG8̣gJzajBRbg~DdIb;wQޠL=5F\>:vfkfWds@:ey1ۗi3ŴDCyH,Mtʑڧɟ|E*Wৱ;lK@<*Mգ44wA; |[89)ZI:GKU7S =Yʁb^abn v{f?4-ъXEFT,nKNt5]5S\%yj?7#E9OPOwUzAjG(f]Uhdqz +L߂&8SSM.@/2tIe>Wh*W%zPx-G)B=]r{hN`9 +'f)X|'9qQ\WMpnnAu wu]K^{ +x[ub_3ъSq;UrW( e⦸%~ŝTVG?sRx|RC@^Bnʽȵ2_B!hS<*33ǼtQ4tQ5ڀ6Bs.>5 \01VLsbRP^ bxN"H)vk=$@0 HLJI\<Io= g=:Wr0~7GwJIB׶dvnˢ3$mI.ZiILn9@_ƿRc^Q&$nh)VSy1 z0IXHOR$]kLD1tZRMӊhNsД136s_x3/Qydp?Z9"uiO$8 :ѿ;tÎe?IKF4eʡ!ۂ?N}N956Aun?UxLp.RnW) 3;̷ūwbUiOVCZ owtf̥5k rӐ),j +滨RUЈy/a\p|҂~$/̗ԃoȃKՎGtfM%wMy4䯤`-f>s"^K:UEx +(/*jiݍ, Joc^[!EK̈́Y:Cn'P+EF-KeqT*s(Y(,(^BV-hE7)̶ba sCo:s`\9[&h0OhT< JRVÑEÆYN3HaY7g0'u>#4D=u=u|YXYCx(w.Y ++ˀPG/ݝvy j'*sWކz| +jW8ȩLv]"ud哮Z_I}$GI%'"Vry_yp\K{]Zm]5`l =($7őd$)mͅ7;k5> abLLӗ60ou:1i)vArKmW({2mo]J\*=X ĶӑƎwxX sOwbM"U9{0.GT^EvR@|'\Q@-5]jT/Ve_ޭtJQ2`"vp888( rQD7Լ!^JHԬk|f&sK̬ɯWgwkoWPLNTå#m7i= ݏ)+'tψPh5JC|tPs& ?kLJmm{cg-J]+n~Bi;q] ~J5wZov54CMP2nl snWMmi޴94}-{j?:N`_ v浿XΧQmۻPۣ=噥lzv ]6e5=Yƪ^wY۠ACͤ'a:޾ۏvxSWge,;Om0>gf '/o5}RݯQl`ՀݬxgX6 +]u,e1t|>QϣMґb{l0W5X;8!d V4aT +e,^b: /S2En0-yc]̀;=Vg8t FP|9J=,`ɨ,IPܯ?'U]Z޽+U/j0*<ǰˏ%c(Eٸ8z'Rd JPSH" +|OL>r}O%پ?1 kAO#bpYiFjP R'6ƣk<;Q97KS32_JND3E̛L:gnBrV0GMdϬ/Ō{7s7]?6h=o5z^#*w` e( DD7ܠ$rҘIvpY! `f2B_'=8C/rgn` CO8kfNQJxb[J{P2AC^S ;l*md2+b&3#rQBz2E֐ԨcD'9p\YG׼#J8+a} JN(#}Ȋ +$3*Y&2̈I&=&i9H5-!ży'v簙ncȷ3Q;{&)Af P ?3QɎy,dIL;8a+'RRrxY,qN`jU-ZcyY'T=nՄSGdf@fĹ3EZ R#INaMĞ$1![B>V[jO 1Y bⅥKOW-sfHXTW1׌knjLK6h'-DB=}&q|0- sw>] +p/-X~׼0yD&hNv͎, = ruj]o)&f'LZaEQ}p:Bc5/z[5>[rjۖ.ƀ`f͒osV3ěiVf$jz%Ԥ)<[hbR )%4iLjS/9停^9!Zy;P"qGN?ɤ4cS35cfKxΚ*笹%,9fCfg'jxNT.l.2 M7qWsQ(#HrEZK qچ?Srɑr;~g/ 9Hd?R6e]0UC\ +V* *ܤѲ,V2+:EeQTt;!Zbߏ7o2ިX){Ilg)(35VSb).uTRW(lYFosYLwcqݒԩԠ%-T.2w|"9H,`4=_UKA˥$J#2S* [EQ= xثsi jPJd(ǹ3Z!m̖VJ^D3Fss;C7Rra8_@C1{Fl]/e۝9F&k/7>{ Cr1x%zg +)~V0s.5J"v=?[91w pJAH~ 0 f"X "JzɌWf̺.=Wmg1I'O`P9נ=wXqfBLQx$3ѿk7r֭&='y?f;p1Q^-;No|+k@z)uL]M c?D%̃.0#"J4cEp0!dlv7d7c96dM. IP`h9Ro[pTFthvhVڻG2cL63}~4oy?Y4/r6]`b]vH?fcn]Cws$v=px"42e?.쮠'F:tбQbp&h4|pzWb7۳_ш{G/i쟒9)1߉W1C2'>qeo32iKl%^ٛEuKTf~GPj ސ従Sb"Nb0V$ѝ !eDbL)3ƬY4LnLf7QEuA*sR3;rߣ4dW&Id-]:;'%I_H]JgݴgnŴp9sS嚨5[񚋨+*J^*,-YPj=<8,QdNa&yڇw {:U}=P^ѩ,ؖܵ6ךD-*OU4PZЎO}}B8Oqi@ꮗv:%"q MIn +!9fq)ZLĶ؈iqf"[{ o`W;[_% +a-!)@X")=ii.nE&i{U*{@|2b&c=DuExW<w+Bv!!~wwg-=3Ȧ3;ľ^Ps>Q/* +S Hڹ͐ =_+ ⡾ lP D<#?ࠋ?CzY3kq_}wZ'TWQ P+RT[/>+Y?ڑ 7̚P~0=fVkj㗹c UÁoxF{}L=$ivh$!luʀG嬜\Skmj˦vpT4L%qtK|kZMA#bV(@N)N)NaFcn>v퇼~APMG? Ê'`2 ؓ|O|jj̩tTܜ67%΋u4)vK (8=Ы~ AtםI}<#8-ܷj ]32`gӄy5j^׏_/Lm0G#ict?%s~^(^//%^コte_!{SqA.ڋj* +tV]qU|y^ٯ}.Wlif~ +6˾}Y&%x[s \!/HpL6L]@_]e ۮ١1icO ?L TTG\" C)Q (DdSYaaf`e@vYPh$.Ւf7ZXk1դnhbL39z8a}b6c 9O€c8&&sr{2q !A>:ANQϏs*#`  xiiw"_;4 6(EPuLph d&AfďXL$p,̹3wivE7'<9¢ ri'w &_͛)9ZD~>3KO16G=,|fx=u\:Σ&zvгn|A|~ 'pԏWb\IjPѓʻZOc(rF\XoFpO1CAxwcV{h{]Ã{챟Bz?鋡KƘ4ɠ'UF&3ڻ}ܕ.VS8tdg]ω1OgNbHg}'ƛ7!סgR1^{Άɕ{[5 SS5*ZgC󬟰}mc3_Imsks6p@˼>(?@%j=~D0F5_g&xipqC >xm%ehXhAg= hDW.څ:nxBϻ͋Bab6<}xQs̓n^wW=ǞhZޡhB *CRenYjJP|,CVl^a?4JGq">GܓK8s+[#tyhEӊ@l@]@,j^z +[SQhFŪl +*By jlގҐ=( 9( Cob]w ,  +>#8قtNh)hz [Q0TFasظZd4<%e(؂ˆ&v#Oԏ\o`E d##Bn\"c~#={0{\^WAQꊪ՞CyD0l"ơ42 %jGPeAAt.K.f3rc`#K܋ qXg.40~M0C=ywmᵁԬJ,l[/JcQ"GQl +D^ȉ*)BzX.)->A:R 9%J}alQÁW8\Z^+"LGY;J}P( @4 (5QD-2e&dȲ`I*@z Z;*^~:)h:!T2Nk!|3{>˙G2E s/BnrXȒ%YsJL yHU!EU >ՇTȓ$sm6{I9Y϶ i)\aM^LRXT0 M&m2RzftCn;A @ ܇}tmh}.lrN̂E4bHM CJJ$t)ЦʡIBeH`P$cd&$ 1!>bW 6܇8U8> 3ލɜHG {_`qИJ2M Ez"* H4gBj.ļ q- ".Z.AdQQ19aGQ[_ XRЧM +eH\0$dEA%A|qz-CՆzDXn=aZtG|q+APcID, f(ڃDVY.#Mr#lAUb Z +S֩vRѡh;&i'|NMrysI~B~+[jPKݷs G?K˘=5}Q@ M 䰑>^'kBt>W5."Dc" Qz9ZGyQw|Br!M䝋;ȰX +ý f) +^W?]cc5&Oc&hTlFԈ]!oCR4`PV 1|(wrQ19Xb; +w +iJ0ij41FJ#:kD| L8Ms䖴T咔YNɵrL>/Ǥ"9zoŝH/2 +&p܁~#K>'fA>P+qr̘c/cz iTRu#̳7Vg4H rrƛ*ESF=b- 9Kriv렮yβ]:u7v%jk)ELjizOVpM-UYr]HxFw q_6_/ dduȗ +%6.SR6W.WʋV:h2Zrٻp32<sxsX{0yOҨli beM-̔[mµ 6FUU4Ul4x5N"`>7cx/TD»NR9Fx6KH;aqn{{%}'~6E-8p_-Wo^ڿD"_怷R{|\י 1.ǹ|¦s*X'z|Osi{@:u`S7˥W+Axƿ_>N)8 Ol \[5+##47+$q/]n([ًzձ&&y h/@P5k.[\pI8rߣ7xIxi*gR8::ͼ79/ԍ;XbF)`,]z"N/8K7Sx3b|aB_`̀w y4i1Z)Nϔ0u&=UR0=b{@gZQ"=O'h7-V^ >0s6EVuJrM4DmktַF^~k7g8q#QTF &,~vmֽqle#:Dn^8kY<ʥh-Q; KFqTM2'( 3GdYrre]&$Mm$ks^'v H9Ez_;Eݲ-eP9{N(6{,viXs擞W9Hr G5cC7Qr<^KsY=PLj( grM~> {6|XDV B+ H**TTCBQwS+_%"EQ "Gy[s?&=.^!݆,+ٕ۲ ƓQ4٘KI)]AriXL$c,P^Dlyf"=;!9LA,z*w-nKzhR,įy:aվKuiKwC]$Li)((k`~dev\~LǯLjOO*?aZOxgRi oo&7I}<ҷ }K`ܖ˥nqE'U\f9rP)UVvIVPڙNҟGn?%M*1QTdӯ߀^Q"itTܠ8*ֿ)<|$aP{ԭ MꁴNvlm~0e~ I'z{t=:ǐ>~C kهVʘ{uud\L<-6ҭ]`x<sG/JwHQ2P;\8s9D/٣QyT8Kחu}OkB.6ikVKu[f@5Gҝ_>Oz!qW/XLIoAS2oȄV8>Z*zqN:wFOi1Мͪ9yXzv_z|E&NxSNh̼vCc$Sq)^FM??k?/kG#zT[s.e愽̐z̘C ![P蔆,HR_Dq]/򻡙|oJK=uv͹zzhy} +&sqL-Xh|:nۚ2wdAVq_ @MN93E<"ƋYb"]E p)G+UνKFKk%*y}ezSy]wCO&>b7r!Q]&QgUA #!dIksl6n6n6ɒ q!$` @ [TePj=Nmkk^a>!3{{tY'Ovqʼnϟ54?h20㸈(<+LOK]gO)!zU;MW G/F%(FX>"Zx_xO9)%8›@6+JE|Q|A]"<'={(sՠXtVҡYE8,1yy\a~ [s jҔYbcåQ1:ֱ~q ;٧p{JV.EVwJS_rMl yS3Is"1czyiE!NX'52,֨8ŹZCʊJ+uIfSs|jWt^9gD4J|e"O'NtuX*NnWy%"3*gQ#;Eаc)0M^oe fF̗29 'i`~Ʀozo]eyóαmz[nύeBsu*u=5KWlzޭ53~  Ekfύ6Fձ{753 z^ǶEZt``lY(>Od.5=4q#31]8D^gf|{atq1b0[n]`r7[W\1bȪ:AO27Bq':/N*.tս@סA%S:ǥUv{BFV2kd(.BVWпJ_ "ܜƵI;i1:"h/4f'S2t eL}9touKd,%Oo:z2^_D(,+uК97?w=xݏ;{F'{K>aNj t͎fJz)sY gM3/`~>TR P؀(@SQ'^<ŃGqYr')y[o)E.[qʅ* ? c٪s_-RDɽP|: +^@kq%M%J4-sP]֎g]TZq/UO`xs'˿\Ti״OL{*]ŪKRj P|Yx̅4TS_Y҉ai^Ս,U0WVSo0Y*%sZsi=$nCbnV9ڣgI}Jy&MTJUA5Z B́@uM3>*)d?E?آX'uAkp6K ~1GTK[ծ-^Gmu5+'`wbs`-֌i쬧OY]'Mn5F$GvD#ASZL|iZ}*鳪穁:5؝VgވٝI2wjJy)"ۻo=KER=wRIGy:WgTQ1,nnj۬n̤ܻƕ4%RܔF/_1yJrv[I ú1em>UMRc'ĿG⏈/nءt*b;Ŷz\-lk1GVk22h'v+).! &>8Ϊbϲ2 b?'-JlO(67fŮ۬XkNΙv-d] +5$RX! E`&~Uݍtw"ew$Y.6šYܥ +ZzHqy[M/eyEBP.D<UTDDD[`Y`Qf}Yq mJ㵍dvlǎʡJ[ n+ \Zѻp?UVIlP9`VJyl:noF A{QhA| /IwN܌xn!N'3w,>n|y3~DZ߃萵'&{s {f8 l8zNBt'9UKˈwN=1y)`;Vn6z@f֔hNsh /b%.l+jtktn + +w:_M'9_l q>.a +j>A$?#n9p}I᯶2Pe7>jq?q/X[q3 +Yw4i}B?gֳN1ǟͽ=G(%9h+WO/0;GLMըLUdO~d5=v ͷc[/3t]%=P +3$Nt?6j`/v%=p3W,xX3ɰO&ETB!(ڨoWqU_;*wVeu؎Xqx%n\?s7rכX.DdvUݦjED-wJQm2RM|U*Us^)+TgJ]UzBEWT>V^_K,/<E`&XݝƩ̽i+s -Fũ=AIZ?U dUGJVijNEkUUy^o(r,;Z82Yݥ\wަ AK 5zuiLU=_^cT)à*!3U2tW,(ϧ\M],e)]Pm{T?K|- sfNZF +юYZ:C/\E1*iZ<"I#S32S٣kR-VzYiAE􎒃?[J +~YA.rgc!m6_Z]ZOE# pB=:^ Tfek~"+5R)a֤J ?g~ShZK'uBk5<>2R/Sh-RN‚S7[sǥ)%2[ɑJPb +͈ڨ]}PguKS,Yt6 9>?iKy-Byω襅c= GQh91S5;&QI1Jk:Ǯ.3?ƃ(Ifh)*xCweWceU`mAZRz&zI4U3cSM:MMMC9F#j1{C&%'UbJHR|Gq.{&-f5od-JS5fߎRQ#5-aCe%dL)6ThB%V*fأXqE./򝢓]uxL~;fxb񽬁v~ӆm)H#KKeCd6Fʔ:U)JMː1-O)EJJRBC&ŚV(ڴASLa:py_WxRx*of771)8}g9}L@e1#Ls͉JLSBf +UZEeWdv²;&gQH +ɺ +1)`{ 6c6P4 u3g>*c%g+1'X q!ȨLMɟRVA,K5ѲN-;5rXAs +\yXlp MRFdQeb WQ2}"a +(̚&)Ц v+Wp.ZNg5[{qK WP&Oî&4ؙصT,ltQ_?/P&ق4$DJ"54NFfktY˪P6Ge-QoA'ҫZ|K\򵹴ߚ߂]KR;v:$1ibEoW>1U]c0#T#gDɯ:IOj|ej}W VyhT_[zʥ6ɽRo{5PؙV)TXא*]a[;\>u\7^4.VO8 vdH^hPoGz90n̡RZj6rOup+w4lb/1Ttsu_%?B ~2=Z3sh7Cm@9RȻ j"GEv;QmBʭPjBGuQ 'r cWȟ9Ek@nMoeRZfHz[zu:ڤSA*-߇4'Q`<wq3d9}D-Ú{?gs\c4r % P=#;D(s֣)Qg(Cސ$] 8: uiH_#t%!$ạb #[EVIt%_OAa #[9&d{[pاGϊt|~!ɷј~ x8g .edO=L9 KEv V1o x"@6H CRPqA  ub'F>85QƼ#aڠqr݌vDKӦ$!.F!bs X!&6ĤHI gi&-m2 `8Ϝ/=EY@v*|@5٥|UɰRfl+O,5VzE<#n *@"#D)NF(/8C,'k~z'mdƁ 0qH\ bzq^n6l:+ln+{.7Z,ʅQ~xό&?}QS!wQau@l9ٵd2 $ܳy^/25u6 \ .sֹ;rr^F7vsdNr?`Z0s@Qx+8\-ߢ6+)9ݼ;56F2*wsoश|0:kMDY]. ~2|wy-C^y|7SEoUc޵}gBc[\/wٌ60wԻW8H2#LEdXEE.}_1jGb2Fm4hN8m15M4ccF{iJ]$}v@ov@U<ة׽2Yd0eè]AW-6+=qGMx!{!}tߞ{FCQDVD4g4 ؉?Gq5p9wpXi 6nsAҮ+8ڏ5yӇWy[po3Nwu_j2ެSbXMu5?OpFih.,pQI9V=Bg>V+Y_iqYk,zM0&1?IL)b%p6.> ,+~PN 8{_էxݏKx̋x1:`t1bxzR!qH~HV@ j?k81-0SțatNQ;i0fc ^W8HR`;@^4E7%c7O1 3+R.>aY. +cy)FS-|H6Pg +h6z~xF.(V"dmT F% z2c"1 N7_@W"Ud|%]YF;i)%'ƑsQvbU)ē B.u;eWS[88ޖLLJ O ykpV] fW==l5M54XPQĖ/aYٍvXUxfxYY]tS =ouKz?PS@i:(+x-ZŵKB|m>ѷL]O4_WgH&I9tBqkz[AU۪ {ڰjzA: w+:F\y|_fWWh2kJX:mhZë^I٢ȩj6[E=>T}I;:S#wĵQh1c\ F֮^hG%krHjp& /US5y!Y MŪN\+wҋr%˙k9n,1 +O I/ZǨȕ?cڝvH5'$)1]IfK)V}jjSQ'ψ&w "}Tj:i.9z@N1 +dYwq*#w|ԁZ<)GSȟ+_zLfʓaS2UeTY#רq"gVʲ$)ͼOS3',YPGǩ9Ѹt2ױsj>3TQQd%i$r.ИgWȑSs'8w +-sU`Y<Z^Q̖ ʲPV2d (9ψ 99P3߾,suv9rƫ2BμQr[T_ TXP/u򭝲f+ǶBf&H lnt}etlcYGw!_ՠ*,ØrZ6\v[ G0G"ť/vRUNx.iW[e27*;[7lWIϔTrO } xm&ƯTi z$Ta&(4]yef)Qю +e9=tSU3ZDXK[~N14yW~9j\ uق-%̻xO܊peW<&EYcF*ӕ MJ*ZS{^U]Cê*K u_UxES+Fػ>vX^_!Y\[c46Ji QYI|%z슯q)V1fEy*»@CkݦP Lk>ր/5OV~[G}VpMzU;d{0ԅ(~|1%+ƗE6*~ +whWz_g(2xßHbL>0"r ܛXD%ފ& 1F֤8Subk25UjML4V$xd[Okg:'r{{{nb7"EV-C `gÞY?~b5>򫝠gcDj=[By+8XX;Ύyc;Že=R/nʅ;vykXFoc5Bu"6b|Ёt L8W^ʦ} +HȻT_LwY5@YVzt6ؔ-V.66L>XNdA';Kfɔdbicricl}Ḯ]4n yN#$Q4/|Z`y薌@wϜ0`#l+' ɰI,3z e2<1\xIc5q%u~Þ=,ӌV~OOǴnf]5-vV4Ű!G)=LCz|g3VFۉc=es{Ym@==úB}Eϳ׳aR8>%#_bQZpiq D'C\8lY <:bz:@OL-Ժ_WmmvIa=$~ca<Q<<'Nt'( ĿL%ew2G99J#{zCG<:N6ӟ3Y\xHɝp.+rוaŽ"P.Ӟ!1'cm:aJhqLr_7a?.JA.A 3J-dȔ$o3OQcF?1 ֯g E ]AA5lt8>?n(θK[esSoW(Ţ(zy +%Ns&Lpw9,q%ݤI?Y>$k'/һn[o5 +H}ztU8,<]Q0j ` *Z^s6= +Ml- HO蓡!IzO+M9r)$ yTj G`>f0:QYc kT1}en1߈'ƀ+x#5N d8鬔Co! +i~zFpG?qKt{v׻Xm'AI=4rl318I-G'tȀ1 +aQ`nm:6KdG_# z۸bLƑK(ᤒ˳0ra(Ӆ68udDBkWrV~vCsm.]>4qF뱏ZlD8™ +g&L*S j=f8NvĮ%-Tf/tIG{Hiʢ~&xc)V'dAɃSK'A.8V'DT:h"OrS=Еwڸ"{I^a&BEtX鰲a*jV3-6r01KNu"K!7;4\~ ?/AQRHwb=iaO' YiPR}DjTԼݪ4'黲xR>PѨ;!r=p/-\ky:I[Y[ f 5"Q)?YɓͧXUO[U[1mn۠q}*;|7r{1rWyjwȳQ3q-k#I8f=ߧT;A?QUgbB':@J'Ud|hN2^eRf)e_Ьr+==,vz1: -`*U!kp,!* ISIhTVʛlWΔʞҩ5ߦJ8o+%}D|i!3qs3Up;'SprgT#_W00Y&OTIx"bT93ܨʉ.RvU15ʈqhVlfƮحJ;q2ǝWBUtWodq#nj"9/ɜ(XfCۋU`E*'.Aŧ(+aMVyf-hӌFMKlԤJLڬ}M:褳LTTE'>V٭S\=SA+v1ڣ + *1N̤)ʘIJKNLHӴR%2<7†! Bpn%,.$lH@́JU`pT +JH*-NOZ@ԱzӠX&?|'9f].Q a֣iJWm*/% |?oYÚgE\,৓q=j,[23̲eZ4'ӪYs/KG43FwW*žN`߫)^)~Yq>toP*a1z-b?g}͐O#dJ;MI]9JҌ\̎B8JWļ)޹U݊q> yg4.;51!]=Fw\XȖ0S\)!sRMҌy 25=ߢi6%*W񮥊u7Mp?1hLSr2RtAHɷ-ĺ =\hgֆ;ʔh꒦ϏRJ,dM.LU|lSb5ShO<٩H/yE*b-/9~ v^n΄{|+ F+$FK%5ĢQ%2-p_"}5c4X|,#>^oрɷ{9 + 9m.fJGh4' ?3,KVAβ T^ ~aW,s6 [ʀmm;>JT%L5 採fZCC!> K\1T`J)#sڌߛa\"`πqVkydhhjC=H) Ę  Bd 2g[Slmϧ7îa*Yns`Ϭpil4f9; +# 5Whk7[hw+ƵbH haRj92=5j] ZAN6س'&JkJVtjCz~tG'IL:q&!37Tv0)v,?@#DI2R`gö:b07V1lfm!.[G2.tn} FM5*|]*Ei;'5I94l0za!?sh|_ǁx}4+~_Xög3nLanm 1c OagMot{Q}q}D:292Z:[AߩN<6Go1}R,81㈒~`,H/DIgۗx^xb;O[p9:K}jb텝; vVi÷=OC s+]XZ~:)׈%qLL/.܋X,9"s63ی'2lit ƃK92#ھ~~‹JxaW]#_K?9O'㋏reBȌQ`@3&P0`g#y=-x\1k0!B2n; CP",0 +|* x{{=~=^`/ӼJF{p0DžW>&|!Bϣ>5w>(g+4=qox`%llՄ +PZmNsSlj?pHP'O>nu ߤs QJ!w@^T*90\>NUz-vҼȎ#OqI>R%Snq[?}~/N*N + pQ"VJ(0V6Z(-{)Cx=<~5D } +]>i{FD옊wH[ + h2Fz_٭NDߦ} J/on?men{ 'S`̄1;`8``A,% Zy\C]˭" +"n`Fهnbcd)2 8?gNeprvMVqJN dQYVOՑu^mq!!@r$&l6ٜl]@"A x!"@ZNǖb; TTGHhCR;N'fw{}l~d}w\ExdIghG=$AgX l51LG6hLܳLRUFQy&ƌ8f +bsA1s G  B-I޷q;+T-Q1jN'&U͋˓;X5񕪚]'WBԫbS +4oSyG0WyLvu^oy܅4 :8Wbz]xA3k$KZSjU%JVERʒUVќ&Zt+׺JrXn-Rge >_)Av EDfY ցFԼd|UYĩܚҔ*NTQSsK[\[m󕙶Xi+fD۷jߣ9CJLʜ64۩ч]B̷{ApT|EnR~UyQvF2.e8dʖyR˒VYe~J (.b.kRq~y=+醧{ВN>nȁ R^Vr╕,GM9˖[ԼZYZ4٩D]29U|Êߥ*yJWHy_(2eIg1Z Y +\TĢj5C}.Y%*dBK(yM/>Ppg +)ZO g+|9h}8A)G!޳XJ-.KiX,SM˳_^ +*jS[ +q )ȵ]\7Sů{<pK3kC5gq%LHr)Ʌ +UlubjLIQdMk]Z[uFGײyk܈nn U筞Vf`W"x}pYM+p;N; 9-EMWXCf4+!YA 6Mk ?%ʃ`WLsAC4!ff>B~ g-.r.X +t?+ՀjRP+X^%/x ؇|G_P>S~r >?w6ipρ;3X \* 8X 9wj:I.y= oag+JP +N7+XQ>Bnn^9je$9ĪI:H WZ.u1H뙍,!z2`mI`?ܰ 0Aw>\4i]=;n[''m +bgBk!z7=IJc>/[c7°0Tۨ6fs+>sx;8K[8|Zz]ҙ9Q姈)|#; 1Tg8xsܽ$2F{NJQZzz] +>'!fRi|7x nC5 1e +!59I!g|ӓc4( =¾:C{]U-|T w.yڃc}D(M9 5e8~:0z8^d \/@rntg^U2.FqIӷc~?1x48 X9^ĨM^;?0' #1x5]赙Cw~#@]9r;.7|2?,SqV8wP]Q\_08@0__Be9nO8,x8k Qߥio+⤧8ߎ!p`ҿx +դ/'x8, k +"W8tVecx(gO#? o%@YS,Т+_Wc8׈w[%%\9)9$|>GC(Q&C㌎iqlI'1("yc$x.cK9&p3堶pOwL^<ި`Tϧ~0b'Q8\_,ɡpp䅣;cubyOJu'B?9>1槍#pXfjG9QZh n8?`x(6!2ـ]OgA/e]G4v$ùv;؉pj&6GiXWZvk1: 1nch& +EPU$u0!DI\*ELZ);P;$@k$}{KUGG9s=aSJ#u qx@ pDrUtxj/'Mwѕ < "XQ +0#J +QJ,^xG6t?|A"ο>8G";-*GH8CS+͠чGb6q>wW?ѪzJ,VǪ2'U-N!5)G5M*6:ˢ#g= +yYc&]al irxw &m!ЇDg]HNk:V+jR@.5*ƯƻԐѢnjN6#}Q#rWTjZS[ࣘ<8A U@#{A'G3ElijUP:Ye˪Rmv@5Utȗ;܍8GŎU8|9xEَ}+ %B!9, 49q`nsר֑-^][P]GEZ(=P%K @0kA9C-¿㏶@;W$k,>0`Ї=Jc̣pt6i/GP)#(ag@{} cbp۰HiU?` K~#`MRij2M`St )m< ؊֛^7y;60}%R +)x.ϰQJ?KPG+tClZ9 49YFc>Em,aIX>N쎣l84KxdwN &!|[CZv5O&b8MoeU!8xAG;c8=,: z.OnDޞZ֖Z^<(_pLJ7" +OXO?<#snj}ʹ|· I3$5"ѨmbFVQȩfƧ j|O 1F^(?#<AN [}0#=4-\$i?ѸqMul?B,ƹ :bp3x22]D/F;G;J>Z-Ӗdbc*V$u]bⓎSprˋ/F6>9=FʓDyWOH$I-*ui5`p)VW)Ys>|ME%“0օR(?~xZ"ùnjMF58#w߫|o43G,`gl3̿OnX o~OSNĮ{SlĚvO֢+㬄C +{Mܠ{-bUҜ^  fTHcF3=34H!$uYmaw q(bg!PŢR+7(1e${a0\qB?`7hQM/0tiTiPLRS&\P7Uk^8$ gt@Lb4|w$cQJOAb(f)"H(ho< 1T"ERU:ZKiK<.&I(eOKd$_IFm,]'}y71L6_0+z@{UiNY{Il2T-)KQEoVɶNlܕ/Hi_'-X@pjP lHr[ ^"ŕV19&ɭjW%ùINn%I#`YȪR-]Y ;&AG i' T/bɑ Q ^l'?pW@ "9>( +6@D.ߤ +-)D +l(6FqEPxZq .j>; Ys"`B40j5ɌpPF8 VDkt0yq3"fs ߗ9y n4/p'] +wa#HNCI ڡjqtP1Xh#FQ1UЉ@xCFvBԂ` #00 +M,\{(~58G4>_ !g} }5jx-|Z*ޮd +c)5;zr)88/G51p7ֳ1;3#9W'1krM5CxN q11Eoȧ)iS4PIF6؉db +_3`:[Ļ(?>5QsB[neC3=;]. +y'ǻY[feG]8vi˗jNcQja0}b ܁wyk'h!~p3nDՖ)wdՖym.hßkǚZ +2uzw?COs2Μ'/]lcĻsZ-,wBƻO[h\Ey% USuI/ +2~Dm2m;5]Ot3ʿ+;V|!m(׆}7Z.5mbrgvϡ#80:.} w-#}o)Rm19HxО,M, p?D'J;񮧥V*JmsEʝ<~(2.+ԎK t1/>ȿd-=0J rM +?Ik_\"1 ÷l u7KQ&6h#60N^1 +0eX + /gKsQ +qZD ju7BNS>7rZ@Wnop "lVo}#kcNztfiЕl JXW'F [_-51p8 7KUCb7r^SH^xv'kbcdH (-滺3$l\ !cM.|+ +(&┸KQ2( 6z~M,ۤ,9)4Ob(ӂ6An֕V+36-?A X ~KXY"B`&^%V۠,پNbo} $6*]~!Wf46=_'yԃ0]]/.QvTUTHE[lVª%RhbG#bpNI] HN"Levp*PuQСb>8z(ʖ >.].C5R\'&,e^2K20{TC`5wfr_o/SIqb3.\c""1+9>P_A U.H A* gpYCwq3p,#ʚnVSs@/Z0 aE0c/Fuu]9Lf 󆾔hp8`c/p4>.P 5 R*YIA [#bXdNjSYT8(NGc=wk &1awÑ5<^޻A94,bb,"8bnDL |AaI% d)N%I&Wq6Cy?4];-؞DCRMH) ImAPmQqh@y0#;Fit+ut,31qlžxPjy3mȬ5 w xZH!&Ĥae0*/2\8NaʌհcY}E.\y9|ixo:ռ 6ۭYjl˹X=ƈ*=c8lQSS0Oy {/Wbl.lUk=%oLp)s^8xrZn8&!@5gxhQ] =ZCUG4֪aKR +-0 +U!18,vTvp8646ݩv< ~{}1o?M}?BkWCG-J($6H_m(-(-oKNЎkncL;-).LB[B#]-Q> ur`̮yDhy7h-ڿJ֜Wms! Qp < /߃7g衯dPU^9uk>|n۬v~Gvp7%;DVA2ɇ$9 ?/g~U+Wƿ% V4P6h9\A_B=OžEfwH7I?W؞^i촯/N9 YSW?4.$Oղ!^#=Ѡ^p^Ii0Q/b'R̯+ੇ [8=I>^;hROpǣl &by9iePJf0C& ~6RGimEs[̽4?U9Ӱоi1q8>KofvíbDYm~B,Uy6֨ɗ" v-ptMFfhRV1&2v+mOfe_TyP4jͪESf^Ϟy1ܘ"G:q-n'Dpq +a9X p0"Ed"u[MWl5蠅s +@2xᩃYF0A-l?ūdN$ &T V@Ē(PlA5tV3“jxi!3:gLZh#|+H(aK[D/׼J0VMdTMLȍ. UWUW+\pe8{OƹzI$,5TUWE$WɛW%LNKB* 5rmfau.eި oq˛6o+0@J)d3F?P/6p*bl)J6/RSb#JF09"9$:w~iI#%4]4E-x_cG!+mvls_W$Mp ȍҁt >CV.2e pv;#% y<;KbN8ϗb{@"b 9J%訒"GsIsDHnqӵSs|RlXΰ03A lAob}K&@D\srgK0#!cRonyĝ+aqzy7xE`yϯDrfO16I\  6 wϛ-z_$Qq&K,vYT^!gC!-sfMN/b-XO324źTJAA}YRs|_x. x`XB,!v0KB +"T\ È)H,[ ~$4?úW$@ lRK~!"B∸Ŧ t|I$ƨX?8Yc=b4y#`8/xZ95?̒| e+bGa@), tk͘Hh +:LݮWrRAL2PZ_ ` g԰%@ >auI2 ('*sD<#O8):PiD ̫UܡL(USZb6gE{=A+jIQK6F|Mģx45W0 6XnC6\R A syJk@89p6U47|iJ4&Ki&d!g-Wd!~ }1^a5)+yq3"wrnrdH >C<@@? T=sfH,cmJ^˰5gAb m  0f{ KQ;Ge`,c ӧXY]axyS˸KBj2yPv+k$>IOе5tUЧVlؙ䵊Q^iujW~*_QQ_i$6t%dUPوŀ% *DPD"u6TpRC4;x?g?d>f899}}/lq &uٞ u!Ї>ч|@U> |П. o`ڲ[]uuFd^Ĉ`{*l+Kz0,mmS1b Nj*5bf8jA  ! אp(+aQ@}8y`=lἼQv@0,mˠ>*&U uPeE"#U c/"s|XPX;B0}CH"^ @m Mj]HDl l7SLPJ.`Hp />C/umB%UC嶠8 U7ʗH#lT6y8VyvA C{Ihqha{"'Ξ3y|E*,[⫕|vEmݦcw{^8tX~:~Suhg70426hjvk׭ott͝ear#Qx1 SR33e>~_PXTyeՋ54mmkx79/q?y~Y$ ,I/!/$j$L ٷ*$? $>p8Bh¯YO8\ . +X <  L&%P,IKd4))!t*WjB-Q'aqi=g-YwI)$> ?9/$!1k0DFFVS0GNn.'/?h|X,Y +L +P,23hl>3ys*X1RNRjQtFbGꃇx%5O7Pi%KN;㚮۶Z&^c!QeQ﯆ij)ۆJB^,Boi6MvpC r{h`^y=0KU8mCeMHj&b݅*6ה]>y'KǞܱ~Wp;0uQ5PUU AQG k˳٥pvi vSr e'SqA9`.Q"_ +/AUQ>" Ĵt-w+ÚZ{7b6o YTP8*#w1I-_R|';&peDa2Ϧ7;ĭa$m*/1)1..WMxU\VG% <x1!]{90g8Q`wPi}wޚďԧ߫NJKM+.7upݭݵuv3ֵ:n gVEwE⾑@G8B};!H H90\}%k;}I^/gM e_x9T^}o&d|}8i[ `V܊GQ ]z6C#:r]\5T9Va[Tャ roߴ=ĄtFWc!ۍ CǕZ+%u*]^[4@ L u[flM8bN>N:Ԟ1ؙiT-1izB +YiA?joqnD;7I{فZ,Ē |Z#)T5ܬ 2a9KGw?#w')/# *ZKFRM\PTYi ҳ^zwgK Ljδ?Cc:J('irNufk^KuS?ty;!PZÍ|T/wbHxA +ETnhX 1a6z-\Y7'ww`ı#i!dzuoZ/k+$ +UX~Ћ+kGZ{(hXxHV?/WSuH6"sh\>I&&4VI<핁ʏ;h;M=:F:b/Տe&)ܹHQoac>)#R۩TR+wJ8ycв44fW >mxp6s"%T5'IjC">aJqi 8AtZП;. c8luaBybJH "7.HY|Ni)bp1nq߉Д$yjy,lWܽꖣ ;6s&'kBmOA%ꉠ7% DfI@]*Pp^Cr7ٷ POO !azj|B n<-'A\Psـ,@= XwӀch̴n r[L7E aH?H$2~=eG2h:`O從ٛ C|pv\X@Ї l+z#]; d5}K%$@<xZq5d^ C\pC8pn \^遼AD%~9 ƂOb@_b@hJ=Ͳl{ /wwT~JzN:+ÄHIq"v(_=Q݌*l't;hE6†2@ 4 (aEm̡ym7+MʉRfP?ȕcY2HV 4c;+ 6`ڦ;; чf )YQ5ᰒaRBOŪfw&B1Vh8"l`ؠis3]|z˜y}5e hh /~EEyTI'i"DIlcpkiҘ=-m4. +"00l̾+3ð̀0AM@@@}i^{7>;jS)YO;|k@f.i<]9_m6+LMF9oA޿ duÖþ:w=͝SבIlJjYjhu<$TH +bssހYA1A ܽw 3юi6ZkyCRVt2(. Jko1w~YϢɛˆW86P{?)CLtM\+R֋ZF4yr9SѢp "d0Þ,k_\ +1]ȵV %FX =Yٺ4-DRARm6jXM:Fc6M2HEa/6SytP@Αw_=]6p/7s l؊5Ŝ#Kʦ>5@ȟG 8v"H)9󌠬ODYy(aF:m2]|o ]f:lb-^9&^+i?C 7v ;2d#8F <r%C$((Vb n.[҉XKVsZDl@$HK*w{w2$t_"' (h=Řo1WqB8 ?K$Q:ĮM$d lL7Y<з?łXᮣh,k09xEt8Z-'@ۉQ ~u,$p kb w1Q4P~G!ĸXY]0'>ذ><K<~Qo]/A; 0 +.B֊P {+rqd4(h>PMb/ /95c%7pF.qć1NnFIݚџWWWS@Ci>[s½Lq<1Mk) +}܄8i\d'eI:(3`~eʉ} +=GRt lnn󳴞!SL< #ԛJW>. [c,h 1hB/$ +oYM*YI`+]Bkv9:2"`/g|͹fOVFL_F[$tj9Qܔ%5"ZXs]* Slf h0A EKϼ7]u{Ʊ|oyvl%75GI5*JnVHJ6SU"lSyR#n/ڃ{A'g9Ck#w]?_zN%)UJOo0sN]Q +mJD^HE6QL! F [}x\5u3{!b`S޺NGfl^[̦Tl^.jt"YYkLfN.׈:Exzi AJ|ߘuxNԇ|9b}WqwcR6#B.aRlf>j + uI4mA+N1 t7C N>Sxs}׎w&]h'^&6Zc&JZ]FU.T!  Cӡs__aQ_id1$1Dͣ}S]5hLKDD4i 3 {/ 2WK "tH|lw/8;߹juh׶SW]ag~U[O +/O'rbyAJd&e<%ꛓR[- kB`8a5WraD!E/xx4i%XW)H>DbfIOIa&ө|v\g-nQ |; A -@:{u՘){i?0mtz*E1IO2yxq >hq77Y7aga;4Yv3l5͙^;VχSǜ ]ᾠ^p=>)} a2~4$f:Ԓ@{'_;Tۻ2N9d,mJ*\a!N2H92.@qjg/iŬ1 ע- b;-/_6{7h}w-KfA~*z(X 8 xH<)ѐy, TT_ lE9Q)k5[%+kpu -|Bx ϢFlmP폀/q`*_[Ңn5/},CY3"#9pa \x͑m?A8쭡!a-~zjFf֣)`!WH<.6f0 ܟ!j} ľw7{MoɦxϒJ24!H"Bd8p݆֡,k+}g !k<Db\;^w׀- +YM${#n63xԽ;&Ic:pϜ+t%,`W+@wn/rsnȎ瘁4>Q+:4mtf($(4u' +f42(K oW 0V,85zM7F|;K8?EsڵPcIx"}aJ*)洋8ݜlv?m41 ; i|ڙBOۉ]EBŞO{׬6je2J/EJi7ɲx]{^R@ cؿiߜ*/ 5T]_I̥cY,\^ YNHRKzA߯T(-wȊr~@:LYANhŬ W ٭MpRf>(ͤh´B9@)(/HrH!S ;q +w1`糗#Ot@]]zl}Rs+;Hʽ]X PPURU}2Mw 󤽙% 7J{6~qoMnzmr1urEYV1(_RnKyU.Is{ydpqDfS<_x --DW]#9=FtU|Z\&fU¼RH#5No*;;{OXl6#y+دmq7:zTY1 +r9_^JnݒJje9%z@)z%y? uh,ӻ[ ty}O'Y9Mk_>%DjlVWdgW"ʪ*e"nITfFC8hͲ!GcVw$n5-A0#E +]xO+xΝ*C1KȬ +;dZ< yUÄ!Δu_.zF )hce-l,nc>t m~k*;/;v#(ᦟӞs˚|VFjFBӮX ;P=z#Ҏi Wl+m)ܯ +?.$M /8۟iġ4"jPGQ U1$ÃĄC<;| M4lFϷ 1YTU|χse]ҍ,WwfyRc/Xdl $[ +d}9z[)j{d/BuGT>sޢU +^/9;gHtB@ w:'­ Pw.f*5Iҵ[Z>T:n.X䁻. -bw!i78%AƁD!ć!nebx 40ڢeHhj?E%s %8 96ˆsVXkR h"7&Al"?2_E(ȳծ(wJ +!fn1CMH 8-c ZM+Ol̬r4xd=gXWiɠ>TPy@tܳZ -Fn]%흓f:dKSRI߉ m8媁:>1ٓ:)e"5y:8ÐqM{vP\BA6c~9贔sjR$tVxR24DdGψCiC{h#W)Q)ad(K_da9(=ߩTa(㿙RPsYcRQD +y+I}鏓^4FI:x}T)eɁ!A9.X/ͨCO}?4 + mNǺuڣc]0Z[7"u*"[Q@%!}O a !HE e_S7}{rҚ˹<(2Csc%H>~Sc^'`2[YF4"9P?8qƑBx7~^Kq+`]+3U2-,1:Eu i s8cj\!P?8PgI-r_1geYȮXqC됎ad̬y>]J*Y-n̫#2$&;ii8㈰C&vgoVZ0Sk2it_Os1*[jJSQ/+JxRl +"T-@b^u~õT~.hX]@Li.k1rRCZ0%~MhL{"K:C+6w`%=3~{~\۲>&w 7E:Yj gz*(bVBatC^::[Ic*%w*ѽGBـH6K;Ti!޹n׎>x?P_c])mF"~tuA;w!ɽ*Ve)'ʠ'fP,f^^>OS,Tk$YReE;̽{A~{4pnG=~[z!Mc][HjhHQHe\]&3[ Uby~4#4=g'cб;pU`rW4nU͏uƜoKthײ"Bn4EU`d\=@+H$O$!8oa @佃`%U lp^gA_7?r6ZStM:^ZBά3JUlQJ’F1#x|A%sF~7{_Z-A_=eLځ{?pM|j9{]y}iȍO8^w''0dBMFϢ[6CkFY -k# {Шrx5M9Tv4 =$^Wu&7QRdz`<"۫KUn&td)8ArwNALݩx(?ͮL A3%T5t-FD߾EpNೖ  +ɐD`Ax7?DCHhsöVi: 5,DHߐ +#98# [ą+tXEI!qS,| ­y8dn P(廂i-55 dX4}7J}G\P +\XnKkY?AG!@X mM0wm2P:C?(h F#B +XD 1K|!D(I\\n/񇠥~nbB²RVx}Wz`';6QQPAB"ć~5yl{t[fvqص;:vkZoA9Pnxs'"!!$pA.  Cgq~~ͼy? 6o`N$p  A pU ܮw?~Ļe[/jo+@'p};$iԧ@7p+v{-yG8'OO-μA_{x0 cd=B^C{"M&t^r 5Ȱ_)+ o+R|߿$,8ȷ_5綞g?|rxf]F}W+Ô˃pw9tB +MMEwX <@ZyR*gTo^q|rNXz5{w 4? G7!C?WIЁ[[OTkAzNJ%oisrYČ9RT\5Mpۮ2n >\cbHm+9$][O[O~ _]tK Z75w3ʔNy iϸ8/`Lh>ʫ 4Gc^TCt!]qCj&|a`{M UÞ3 bKR[J%&7( r-o:7g-Dd89d9sk8oo$lQ@V+W h6`&ĸ*,!,g<fJ~7Q~kHY pOݫ lϔ+cLL܌$մ +q &~ZSH4-Ż :ag;>+K? +Pe~+"gl7-Ji$k^".fdU Ipfl+:ai#+٧9˥ՓyMgIP<[wYjl&PʣYzfmNusctSoL`_ +[OeIdx_o~a5`[_+0Icz%֒#PsŌU ˤl8(O$XW{tA8 ' nW5|c]=us %U/jZi|`֙&m^k9:3N3Ԫ'IZ4(;kd8Z5ro5["5&W֔ӯsCE1f9&Ǥ!|4#N5q4qպ,vJJvwU=cxyE}uKSu34+fES r]nEU2V0tqa\?Ng:x@m\;´d 2;흷6[VM>e^)d0 D]Qrrs)KjH̝q[bHN+qngb'2U~n:?rzU}{[sI]:4PŋwVNQ%,&Ȃ +jYv6p-rxWtU[`@ vh'̪^OO6Uu|[ڎ=Bhl_5pnkEqj9VRNH)":e1Ye]:AfO%С:Hߊ̕ y,rqm_Ʋѻ +&~@ҵ١{(]vGШI6B2Jk4z$^9s`o&8|foa3cٿ7 6psC#jGN_¡̧gՏ.V !fmYZk!Z'HSI$l+ +x{6"#됮ϐ{C{ob7?7GnU~Lu/Q?^?+ "(fL|hG0ZDZڱEMȒ@B%JB ,您Xh '&y>緼}^Q_]%z`='ꢦċ h||o{A9 לjH\%5ۓSSx\w5Lfv.³M~Zo-Dto-<.i=%#~Z#ES⿓8[$Z@pi?E. ڊںCqFOWa贀|,ѓjjgYjP٘#:7q73Tt}BO]!3׈|p#GblH>pr ː-pa;Pkkhclv;>E BP}-F%ztָ D%±y_ą%ɐ,&2 w] C\&lBT谙3=FB~1Ci؈4`T࠙l͂P+&P2 v>Ѐ($5җP 4JC xb:/"t& +ʁe( g %^ĂCf GC +䙑@1s€eIq| +C `P t^TϟUC1  FIY0a #>8B A،(@"$Y@7fzAԬ騽9 ^ b4Crڠ؊3W0#8A(:a (hb EɄ 8l:"aގƽ{A? 5_(g5ׁ}k߱5k<2:ڍ'aH &)r@]C8vQ #<R_ 9(.`UW77Om=cn.-{a ԟ a)>:PCM&n^u'Td+P2@*gQ![O\?%˷\n=nx1Qo+ [BÄ׉2zY3Gbl +`~ra%9[d?ׄWZŁRPj߽Z)n- +7@L FdJ@ +UѢ@p|?@.q$ {![Ca5}cwwA罹߳_Pi ؖw/KtDv:5BBeblkE!ת†!NMh|+`"OY]Ӡrh ONR*-y*iVs"'D|wMt%4PCQ-訦;n:CqO~g7m`!'Ho-Y#_C_͏ 9TZ .xLX#o_Ceԃȓ'Xy +gN_+O{μ3ƺ= ^@Gsh +5p~(~c7.į}$o{5|)4#p0`0w0ˊ5XhxQu6pC[ڳ![SMIkm`|VPx +ꔮtvFӼ^bU4$T%"TElSf21UUV 2@혥ФV0ZeMU=(~>ndNҘʹ8H0߷"5kE:x5Osq-Gc$n`hM-4MV;?AgF|Ҋ& V>@K @u@Q?rFVAVH xЊ,za#}>*E4ɥEJ*x^h7'k=Vf4(~h@joH x8pњ@)4tԙHE= 93vK:aBGT~=QP[P$'ȺĆ?/o@;]7tW?S;.`;fg1w YcV+J[d=~ %@CCSB3pý`[6 +gN/H9,ݓ(=!0r(1FN^G?Og\Kjzf8cl/pgc:ta# vvaVIОIOJY_22Nrc*1P:'Yܥ-[ڕi6@NdN~z5~6a'&d3qp.2{$= qbT20X4b [_ +yի' 'ʵ;?siدLXFXc?p&n&g6y#q'}N2kDt1efaH<E/Fad`"q~c׳i;yO[?GgOۇظ/j]Ӻ]aM_i3ֶV[qV;Պ[}M@B! BHB!%@!.{RAQE|} (Sly {=1&02VҺtƷ47)w/{c fH^ywSQb2~4Lf ;.&c +!k'zdlzmW=/5fsCzmIGgbƏOOLF<0 "-bac@,reèG9M }r%nv1{^0r&7<&u=7A:?q( !#qP<L[ԹL`RZԈoR>&{?͔XLQ5R:. _y7ҢSTx4K@>yF]\6mռvVx}Ì4/adRDzsM0J-qR2Li[ܠk[7{y3:6:14`0yI ȟ\ Xp]5'_;# 8%&0!&I2zmZG'W[-q{&thHgn@2u8q7x'[=xY,g͔qB㞈rpDl{b.r;K,hfx7|C~uԇ5Y;j&tҳyc|pi@}3 [ux!w^1Z=o'9,%'Xu m鎷.MI +zƫUW4zpq cұ!4DIC"ol[0|coFwH:%tlXIyjrNWX *bM}\Kƀi@]/ A6W0'? =к_}<FIm(^ Ĕ^ΰ-8j%|LXj\^Z^o d1>hEО⠹ ^那` r^]鷮(b_:W~F+g;d0*S'+q46D! HID4\ۂ#\4?ټ`R{Qu :PϚ{+41jBA%OvIeI$ +\ff^Q"G3̷Ln H@+u^*˔dx #uGAOuޟ7Tu{JS*5B^>%'/C* iYTik(Oz''}Bg-aӲ1`I4`){'xR YMmf"]$d^>sY⻋ž {3(% +ȇC9nklzw ?[|xT4lAK[k*n* u*򰢂lSFDE<״B7fv@ZVY* IPNXE,[ =Ð"1l`V W~mm@cݲvOZBvGՐ$UT[aˑK${pJb%- iTm0EO+Z`h'ckH-txR70P +t]f躯|R^]rc@6RlR繵\*'MKї+pWIp rp0Kt'О `Jٰ4u}~t6Umt_->g8jIJ, +CW'i'q'a3boC;CJY45/O@5P:L҇v3gl[8zH==KݥMCWo?rk?9LQ)5ˀ}}9wSqJ6K24LDDȾDBq8qp[I)iqjJih5wZmtB8u_|G(ou*m!oeme4W6KHr֋l4u7;s+o_u*\o[`&(Tɯw.\pA ~0>cpD]HQ ʖ˸)D$tHm.J,9SkYVLY5Cְ>Nf)s&_gj@ 58!XBQCNz|fD)D9 |p+x'x/S7 WgdfSa#ǔr:zN7@ B8Πs/.c:먆pc>``!#` F%Y\@!<}E9V5`8EwAWGTYSU2 YyNU3yc2g#dAFKw17fP?PW XA}REo"rR'#';d_}v&/FVd O]dc01 r&1аs }e?e XV;Tߍv+@^*Ƴ'bD9ӐHy'g!Bƹ5c;džWFÜVAyr|~k+ MznwHOlr _p=&9 )(BAѬqqG GWy7 ^,඘sd {99q ?sN.WP_@~aGJ}hߩIq$uFDG O&5\^P_ԬCsn߲N'..v.n㣩20v(?ew[L7^PE{n*Jh +HgJIOTWXkBͲWt[t?Zq~jEK<.o4SQt6tT0=Xu,3jP&OK'1rд[r̬K\n.Kp9I)neY)(v;v9b>YWg <"Lz{~3~&CGe`eE)>G*tiXW_vܭ֎Knh*Q@$ʾFPAAdd  @" C@`DקhmQc;Ӫ@9<3K;l;܏*?핝\%];g,֐cKj2NUΕz7UZgyWiʰ2uĽΩ42F<b"Λh*mp5U:^WN -?VYhQFm[%w:)痹fzdh>rCߢg̷Y&[}w돬OELccb3XV]h8S~d^2yy4FMߩJU]q>[ƿ[ȿW4L@2!AOfHa,ËUx\{^`wkRܶKJj7? +2v*9< Hp +՞9M>b%|77?9\"+%;}!3f/eUwj7`=O 4U.֔E*ݨ'mUm$S(.rX=U"0ŃjS ֭C_nt6Ol̮YT]yr2JHeKdyFdsk$ 63thwY2^ʝn +ZPV\QVZENέ%(q*K>I%W}&z$оT%+h qz<.Uh/N-~Vr|锹>vHCv &>J씬V$VV'TzUxV {U%T'g71MNĭR#k> -QcK{ǩzw+(xtL0$5@! e Mb-P>`P|D0K2~is^w†˳qvtE#[Np"8m{#T&dT=llmj{  e -7Ssn˼fz-jExg7G__dѯ o 뿶'نҎK]tjާ- F(3 o ek pFgG.wέ:u/sMPMCU[ i-C[ +3fr" }P!drA=T8,Zd=F ):OD]x]]'QNxKˎ_T=m^}iyaǣ恏,pVc,@ t&8?`]#_ ܟpbF"$bSBGg9:57hTihj88π z"Pw,@Pwˀ&{6 IK["_kBx9`a~5IR&^|~7u[37Ƙ^,U@u+)w3'h5Biv.qKtЋ"yP;%R wх H;C^,5@ a i8LOkx5h\h/ Ntt{(;.:[ؐ`HԦ: HdC!? O2e+a>ɂle6L;kڏm䎭K + +aEcz;b-Ez8}8<Ȑs*;|iV0-3X:|Oye=d߱r |b+ +ǔB ;Lg#y;7ټ7܀ď#.x5+-BQڥrSG頢Iu)0e,c0f0Ìa:%IZꞺ-E"sN~>@Lb*PʧF( ӁҤ3mth SFu( @R@R8as@ {1ӻc9Ig}? xMjPS1 rM*J ޝԧ3ƨgPuSRGtRA @W88dIĬI˙ +4&rVs3iu3hthz>}3{ȧk7~~/>07z5Ǟw܎l~'1Mz'j=Mk?{Ƙވa+n{8w|,^C|a~/Qþ q8cӁ X> 8l8=}1=(_0l| hZOoGo3߹ߗ~-p<a܌gam0cӆڃa\Ќ9!9 0T]7 S_+ޮ {U җ X7b>`: +{o#W2}Zc(:Ps5/2I7"ma3c¨;l凐5;6 j5|=㦶gqqh GLp35v,`^X!2>)=3RmxfɫP:!m!-?nesK{a.5*d:5x`Ď叻jΖ@#Jp'}=tq׵LE)̞D[X> blv/_ַ͡CXdX6EYc$ވcd: v#xgcNDNBFv\W?6: Ή2|1ax\E[YmlnF6noxz++=v#4DKѰR "&6p'{;= &$'Atxb"-@)?7~$?fYqۮ*mT}yw={.Du9Բٝq`Yf&u!pŷ`^Ě$ Ϸ!"&iO2f6 bV%\̯&6$X׳vWmrmw.>UL*~| T `a:}o{^OyfWhE:%= ZYf/nV]5iH_䊬j9Y5IvU eϝq`_s*g8+_:ݥL[elQrRp5xb dk7)aA:ms y ?>Y=Q8rU(Bjy ?ժ'Uqs9ʤ}% .ʼnw\ ^)>$ ;&ÎB.l+䁕:j8=/o&g5O3&KVҪp¼{lMUYA4|e8֤*=\Ʒ*oWe%Bn"[nr9I]rC{<5:o-#sjVfL),HK^r3EU *IyY:ϪD$^&W;˜sSj]e)yOܥR9e `,lei`5xce%d|WyIkS@JL\tzfoakCR;X8?k>_@knǤ,K"[M #?@}{yup#q'][%:\@AٞNGM?`5?e[i9{mn.q9\cd7΋&lrFghȻ Zs8S2MFN# \4`ۊT yR^sz}Yѧ +U*WzuIukfIXkbof{pȻdW&"t2ʥ;]RԈƬ m=tҽWdD(Q|fxQCDQMd!Y10!Ӏ@YPPe}Y97g"DOύ7'Wq퍘7G\8Nu-}b䵽"*VXT\ +xi|*ʷiWf&hNvRοw[#z + AxTD`QmThOPVWZS[m=AYEq5T$8U dTj!Z O#oH|iW/ՉQT`HuQ:_:4HGzE~g}kGz1ҧy.s4{$7\Μy{UZHz*Dsľ?" "V'WDz!ЯaC/߆tލy75UȚ6~$qgppg3W=b4-_@Ѳi]yBۻ-TG֮ҕ'vlJ:yu{v*3pü1XjS#W =V $a}9cMr2rZOI|O +E`!`>%iR ӞKtDuV#Ź@u>?u*d4y5 4.4Gle&bϵr-6 kJOK:Δ25r,pHDY;Eb,"yc0&&Î8ӹ?i.r_AI$o䍉_/PP*F&S#Gyv=L  Uhș|\ϘBrgM8;z~z̹smA֔ku[R_ ?flRf5*7( (1;ŋ^)>Pt> Oc%y$#Mlw\T^;{-BgT5"PsJ)<Ѱ>ls;4y|7A' +*TxihMa/O?UHih`2w0~Y wƛ;;1 Q U%j^ԩV߯&r}ڕLk3,ǡg^AȍC +~=VPӘʐ7Biص0\NT4kpeǓo wbTtウx| 3x|jbE(},GG~Ú@kl;6]٧*\PuTڤ`X,E7 +1הjv6p3u,-qV=ut|W88v*?x?Wl]8Lj3aDw_͊՝*$ZbeQK iZA{F`?=QT KQPF\w:xRSim(vÀq?ztK|mjFe6j 2EVZR'1ŭJq_B4#F= +] +)JhWC8;{/z;Xv) .=t7|xCF-idRBJAT}mMZIQMi{f7K*#K-7U*G29u: V"?M2{hC0wú:,Yf7jj(Zg6m2꫶̐:~J#SU5JcDy!CT*s{yWقwiB%JP+_x]pja@p4%CG|xisSfkZQP1w( N+& ;ak/m%Q+1O&V +x tn{Bost㡥Shn9n2^(Xm>i$j-3;L п/P;x]6p|ios8}%6J4GAi)i?J,qL*<{1\7&vWbg4܁`w`XяA>҉ rW-L{2'U+8[H@*d!H[ۑ/# ޅD@9S]-O_i?τcLw@ʍ62[<[!I!:Y닝""rSD(p;Gwytۭ(CpB,`D;c4aS݀'T +w|/HsQ7|byjywa]ރ7p/w_{hC]{\ .Y:QUY7(@(~ E?f/ۅC8[OKV,=tyXW5ʩ8MR 1&)j+[Miv2.2eH7!Gǹ]WDG%|>k]yV ۔%MU4wVW[Smu#}]@I.T1pٟ@Po6i΅Ő|tW;rc +^Qs+|q'u{$\$pk|t"Fٕ-@6wm9 ٬u@nyVEܓ- +_=v+yz=N(~W_T$.v +UNqMF"n$|4zbw ksYZ|zW($((TvJrE3vI65VmJ3wFFIkgz )R,`\yxp/~^zs'% D4F!MSlfZ{Z'UIϫ6I ~+1ѧBfhk:VZO}hȿ4Txhg`\p'|s'|] /SlO5oV~!KaA|[4ηD=g5:[t~Ҫ#J?RGjh3?4=\nA3j%x/[(o +FFC# {]v`DehnE BJ* VpIipqIW롤{-Chߑj΄Scu^{&_8ND #F+ޒu#"ߤ4iI1] {iv9٥^4HFz"i3?h$=1荚8K4$x⁇)Ea%j%ȵoog.1٣7rTŨE'F>X {gq>Y~4s1w.tCy[+ MIP''iQ>;i7S5KRt3$fǤ(И¸cZ3lSx*ƞ' d=\YpцhK4kX'c*6-@fWmn +P!Ӽs15UPŨ elGƟNҮDvF}tQIg&R-%2Lecz~9)6kɊGdkAJ>92|I/wIq<]NF]Uxr9J++BF)ʢe)dR"嬔"aA-A{=AJ&$52(Hdz{{G稬 ŵˠ]2\^sLNUɡ$SQ<<2\aXVs՞vFkz]k^hhVlbd5m.C8ZJJGfB]em nY1.HqԩuYyV15%v55{kN:Et9~༧cT"+f9Ǔh$ZJQp y'iAv +dlMof|"j\҉8MbMɧG5͌lh:iehݮWd@ d>?'|ZBDkp36ig"} oKl1^D?d +T7:z0! ԭ l=bIL䠅jVjqQ2jM7e'#iα33T@ D5B !];rl*bUPNt>/a:p^B&?:fyo՟0c%@!ۀۍ 9b41>lp']0 2d +^O&GYN wY; 0`n>l.nK9ݢ70NyCyz26D N}v8l0=~r%.K_'q؉؉a><>mlx# +e2QE}ѠѦѭѯ?8kq>cp6O ? zא _3{ nF="W ov?s&$JD=dVg31a54ɼlz9f6|fvbԼp1> Lbp +=F Zs>"6̯W]][y4&X `=|;"Û2*QVHtC .G_x)zB ˡ Ӗs NI1`,b :&]# +|bOBA|$M'^M_]/.k:׾L\`iSWlbL~>G,đļ'Bg~/LB\.dG;G7g +'AwЮX Wɠ6( k%셕rbJ&)e%EjbiwOZx.)/.|'OIqEBPEm蝅~= +PCkw\ ) U&ʬe +]iI^"w{~>zAA=~e9*7d.d+ұ{S~͹N9JmM 5u (}&XNPTpJleBGJ,.S ڥeo<Fq?_cPW TkAS +;ʴ{Jk̊3,r%Uat@tT{p=~Y1:_VżWqYS<%itϣQ Ь5zjXkq(")E &Ezm~1ԙ[[鞥makk׼a{kkFٮ?ijB~UkBj$54SjM0&g:Z?ZU" iruCMH%xﻇc59yS%gpG?y"s{(!ǘ"%J7ȱX~xj-C~[moO}Ȥ@_A=1@d^sO'",m:6||Y*X լljmv[Uo=%Z2 hM.֘ @=q{k<d sx`2+K j_X,/Ze^]I,gg'eο,9f8'N"GkȮ5RdR7EuˠE<'G{Ӏ-uYw7 w! ʼX~/XEA0 +C0\gaѿɋEw3D>w} +E>"||Wx Rxr__B[B!7Po<=,}2^ŋ]m0ܟg,ϣ0珋p.+\Zٶy"ҁm׀sm/z-sf͂/8YJL+CE(`6voc0*S݇mU5l+3|C|oΟp +~uV z|p&}נe:m ǘO0s4F7i`TqsM;Hs|\0lZ{`8Mha0<07S +F_ +CڃaB^r=N\Fpcɀzq) 3K?6I0tt盏C! ^tOqM2{p Ηcyg +\17Aѝs9tɊS¤LƼߗ0~ %Ȍ!cAC@#l@bG'15hTɢYKa|˞U\&4[YWP?1=42f2bҝ-d+l,% &Zl-+`Y:"ԲVmqA (sh+BuhH!\ -_O=h rxO8Q&uhtڄN;QgF㨞y +U x뜊Jk(^^DVg&W9?~Ȇ|-㵙-L>Msh9PU?m/~F{$^G{^xGAmwdKו 7^O^/;|k.Q^{ts|c}uuɯ wjݛOrfY$%G~[uz͸@ɨ?onx`!Jvxd)+ lѹKr^(KyL?Cy5eR+ti]S9EIJ%)I=9=Cn=) Aʉxl.BJ[@dt]Qѿ蜮O4 TU\ FTST$(&aEM?.Ө3?a [A@LQ멵3*ZqBXC $!!!!%eqgә:.Emg{Lo_OP;~{ygw:֕θ.E08{wsbe<܀٩ɱ.r:-u9rһfxpǹ=1N7- ̯۹/}ڸ Xnb\D{X;6^<a:]ۆX%0+p/aݎ{r>~=m9#r.Ys<nΝoe|6A/FYtG‹tdp7< [0RÅ (p-wY]{'_{+0Z [xMF޹ލ03_ASWx6f }'Luģ ;v|7\,Äp5FDbx7Sfo+q XmBUXn)4 -¶`?N0n\]R#xZ]hu!`,b| 0[H{|F}u=f8+ɒlDd'q蔦طIVXX"c[>ŕ:Q]IR-:f-^~#\/O>BK$MڵaWm@|;:qLd"tj,Ef]+UHtj) nw'#*QZKQ.1\BmOԹh?KoDѥXv6X1hVkPuWԖ j%FyO\˩Ud!pMHud􋥊o#?(eLG.U^.yP\S*}_ѮZ5Gf/,vfMIRTŞ*Nh@R+ʆ#e/--&Jx*S0[~`өb<(}[OZͨF jt) +seUzjK5*?2P1mH|X}wioKWab $00{4JH+ߘjxlQN'93^{!s=̾@E(o%6z¾Po`-6!wp#k0ܡ d : ɝSt![PG)CWه=>}1_ҽ8Mwe` t ]! {ѕH]ԱHۃ8$' |8#;*F p!| o7*!寓H_OgOH?iaXX }>\aX8yOjNsX+gΓ h4]q1{?M35ME("UaXXRmaٌm4鶧%rRH(Br'K{Z"aKE~wQ~L}~sduF1K6Ma C?)ҵ 3#5MKr9|! 7%c|;Kg9 +b')vO֨ ]F=xÃo7&˫ǜ=524ڙ<dd˟ lKw\vrvͪάfaւצh5#^d̹X&g072.*(7d^˽xgVGcUn3贺GK4Yu5kdOG#@cA4q dKA,qxkݶIKC]60^G)_K[hMh؉GnA>9ƍM8pr`&=?9@C:ba71͎hr܋Nxt Ϝ<D:<M?%[Q [Tnj9aTz]tB@T,mHIPJÁ!kڜo!hF? .e8e,_8)Alw,Q,WtLVqTeq!s|YAd$d2E\"%xf fɜp+7 Q&W\yZ|QyH]T(ߥyH#.hOHio/oW.ɑx4$b¾h{ Z"-Η: ++( qR HH +C"B!ECh)RĹ,~E^1يj݊G63] +E3gýy_7p1&t CHX`*aX{9۵Cӵw --zM?-RHFb9s',7{ k_ jEqL*QDȐ/"\='"JLg*JҕҔ{襄$+/ڡg<(Iv6%i3-&fsb˟{N5j8 %Aj:#ݐ}Q˰'j%vG +2To4vEFnuUYUڪ0xMGoS$0Ib$iEsx poP<ͯ!(F~rc!' ٱRdƮ@z*AZR36F#91%I1udm5]l]9dCC MOL{} Isیr xϽg +8=ߌE"'n!#>i8w3*3 e #8XVc[LDDfFEAcA1ƨd7 JFQΛG w9wܛi+NLRXg(e:e]r⽵]|+Vt#$dQ0ȿA-_lܽ&?Ipߡ,ƒX\p|GTX/[P+LV&7L~b2J9Dv{q`?Vvvɕ@ wCj OC1X95Ƭ 0f)k1&lgRIm!>݅ig:^5Fx:ՙ栊`zƝF#1bEL8.MKQw \ƢOjFBpQI9-}(:{̻ }UG` s+ls7_Cbd/W`j_ρɛe0gF +FcӉR멟B}P-3ꀉ?nW\2a K ?_w2xIdC&1OtAAqEQ?uW }cwg=y]9 ǔXĉ&nnHC8pF(DQ'Daփb%wM|g!$$hI +,!|MfߑzMڤ& +#ZM-m { Qwg܇!KDH^L]ڜkeb F<MF +4w댐SBѝ5yhHRI6 +=(JZQWxj)jLqO=@yO}S嬥x'3163dp?w ֬jܜUfm_;q-W>OK AqnKCprr-@&C 4doALƋx:EܵԸ1?Wr\\hǗj0Ë[q~q\1Y2SK1 yz(oXshiپ ~MGz^NӐxd6Kq%4cda ++^p7N7؊vYы D puGH= S}@tIA~,gvt\ _/#"q>rF,_ÑE\m@ S1۱? b/z.'bW,A's 1}Hd^w19W&a8zƆx\$t38ox{וw{\IhBwBK)?mW9 3?<*!n_`xD^7 G!8~5$ʱ7Q jlТ;EW;TTVAbpb@ؒ9kQc +¯W^+kbW,HlGwJ2vБʠ=ՀZv6[Ьl6*w3:u/ĥFI[_$7*rsPϫfUY`6[RɴHmno 3(`Fd]_3OoC$!BOH0iygc4wSdN#Df1j"QYMiҪjyvm!R[&im"vB$5kweF羥Q?NNG8-h"?$xFm-jqگ?QxM!g>!pl©O]J}&lzgNXnM* -5tp~bg~E[~ቌ5"aD Ƣi:VQڇ4Zul3ipCual4*Xٍ*0|#[,,cE디[El-(:g(gk%xB߁ڿ.h<\* PXKP)`6F)pEb$b9>>_wO}~iWR G$"%y:< },ApU:βߣ1fS$5(5m@IbSL^PP`.͕-՚[}2|7|7Hcz)1I=yN-:#Tu4sil5e4s [G(|e!8K8-1`rZ7`UAoexV_ky6Hr-R'vRi.˰~a}a!ȓƛw=,Zwizn7L?g"\8B# &9rBg8ҝ=MiUWο{+/$J;SD<{2)ڃC">`6)Dі0L&W0 +b^,w +2ݙpAU6՚iNajm([\{TD~(Qj"dTAQz98޵Y>)S䖶.BW-4g 2 g1q; ]U[ttu;pe|3kM17#vk]oX9L;|˛oZI7H}S| 9zLOI~ >}/u5Xo-Ub^ُ?W #O?㩘U/M +51IM(ՊQc$Ծ:ܣ|J8_k`}:U@оѾ茦4:g&dc(Fy7M(1VemG>,rp +v`~*x#j0]pI}~c"3%%a%_ɍdVvJL8I>R)}WjU]-t^ 6#!mQvbͲ PɃ@m{[QEiUΙ(1^LbJ6[ HY#O4M%4Ps^~Q³N^m媷Gλ7y#BlP,ߊSDbtIG2ɾ*%O_Ոߡ'}T;iGr*c@(mX*Vx7oxfm,~ӦR{mMyfz'&yJ6m)~qwe)t4A>E|ui}KztyA.:^juFUK$/*Zf~ꬸn*-Βߥd[lQelQFz:&4tKZ5T[V[CX%q ևc}ޣK6OgeTP%/([sT.~Pe<Nj>*Eܷ_F7uXmMtε>'K};frޱ(w5pj☓h@@l6((g!V;SF@;pwLn8ƕAK8\CsffJxX%r ]r8:$E/Z}C"w-(v( 3phU.U׉9A/8%'WrlFmc g293]Dy261o90F_-(-I +WS<:R?ʂʑVEyrjL(Τp\$yr|Cwdn$w~{HKe)F1?%5 ōU W ^ZFqCDžq)cȟ@iAƤ,%5+RVs {IɾX$grkĆTĞ`YďP$_/dy:*uτrvГ@gr< +'-4YI–읲)ۈ?= j7=UkoXc+~W! $!wr%W $- Qn +)-:n^Vj3OO=ںvS/O~MԍV1Y<$ŷs+pWs3S޸LfbgpLF݅{%(!1]bE[%ؗ[fiM$NAvaلC^\9sq;a w(ȝCy'ųpTKqP]hК}y4癐w1 A)Pgl\ƔyժQJ^X VrP<Aa =o$ݷ${i̍P;.V,ArZ[O**)T+07RTLTVjzx0B;!(׾'i?5a)pν:`Λrʽ +`rk`|t_@fРFV1:3:'*~DLXg+ ͼ4çk>ínP(VSWN\Ӑ4dθ5,Č"TPiT#l,DELT͖Rso>&DŽŖSB|c&ηH+Yw'.(=:FZc!}QZ o=+@֒c.L%-|cIP:"ԗ^J>&OXcsɇ -`%@O1~ A߈?G/{޲ klEQ րA7cUP3OO +/ T[UK*ȱL]80iw(( X+a o)cDCD}D ]mRD*dn^~$?/z//'p,1ӹI>Y`vGhBҷ!X0< eBu<x.5J(j ȯ#Y" i.FR;Ȋk/ć柌(A}M;0!lC;aK~ +q WB޸,H{ Iʱ78iɍM!ڔ@Ns;aWt;#CVÃLR FԋTl'Zg"m>ӰgvFNfd®ٮ=wa[G)vVcsg 6uql| )5qX?tGR;TKH-@yjt@nn!ڛͽ˰w 6ml>߃WXٟįz|2;Xayi@?j@QuCg?rc3/B,LG௰lp52`P^a p^8@X|jißa 't$?HnWst&ݽGmIWIEڹxn <3/S_X6ĸw KL.- ܠfH~Er]5itqg8طjlEkQZ2" $^In6$Bd! B,ڥLTfDZ Z[Qwi{9{>}}~$D{b>gDH_~K{KK^o/ VOPc5pAM7@ +mqGE^ +B V)9,ߣ!J_?u<]ݦ +tFW7O▝4p)WLjP~4~d\J&߃qF(dY~vKZ(Bax"[~m6׀pҌBMP2^~c QtD1X"PDHbX*V<')dqvZ&^ Ywmqhm]2@|$ ?aDH]"\-b~a4?><]NJ4e/U+&Zvt}p"E3,ӥ-O~#@>)Q8O~.f|̗RZ'6qU~|\s}y:T^bC **͔Gc[dqe- wh/[xkSɑvW8:FyG °'t&Ff(dx)k!N(@x{QnWkbp; Evrt>cG6}d㓑)9QɬQX3zEװjV +R˝/3K rŒZ^Ri^rXhAn|:z[Gnf@ֺX)rbK,X9n,we*6^F,EwvF;n/1r _=ΫW:kG(s~.lvG](Ix? +&|B8&ĐOG,!g +M,!k'^Șt~bSҼ &ھB9x\Yv[&}ԭ&tس7ER5^ɟ4I,dew$cN kr*Xu 0~UI} |JMn1Z;;SCsImY=>I+}B pNFxOI!!y ("%pɁI +:mJ 6%>fvZp8'4zlW0.E~-YߍX0쀱dz ЗAZI bnP)$ĐU̱lfeiSxuL#B bkQc * γSf]FA=K;Hf,qfe<oZIL%b#q LM 4f[o]ЍrSlI즇>d _TU;_{j眢s]âh} 9M7!ZGbu%F|X 3f>,fV0ݶ4-.&+舫vQHAD_jp[yl {V~qhC23?'smH} m|"6ʌ(GdZT +1QDG/#*."s.<), ȗY80UyޫHh3#̤E&%;<,|7Ke8ZN30$dK(KHv{)QHAXJ9hdP`.s9c,cɱΈ|qɜ?>o==_&7Y뿔eA, fI@"fQ`iA>r3|xſu.Of;"YW<@BL@HƱ,ȝ3X, + `AH$C ]Ǽmx1 ;av4; ^!o::Tǔ?7PFlzlFxh úoY>cY1̏O#Μ̎J+j3 }4=xF3nhNςR}oAHCąCZ(+|;dyO13 +ؙ̉x0+v3V0}E,qxmbj\.SV ++19Wn9:ʕ"? gGC +բMŒ8 毲c^s{̄aHg LKE+yq[iLXIKkC\jk|4Wwi ٶR됨+IsjX&LZӌHԵ}q_;#2)Oɸ\S251N]˺F+fĺ HfZa0<*DkPҙd] +ħʊBG/-SƦgt\\6i>a̐8g✑C8e)>NiphഡZg&LoFØ4Arh&Fl_lkːLg9s5YIyۣlzgoW>O./qfPG?GUr/t ,(6#Kg6 ט8`'|ۊ>'C?cLz,ġ nt-Lsa ++Tp5td[~.3{IFm͟ɻ`T8@ t>ܘEv/}'+KAؕ`[F/ZYJ#4/MŦtGaUz4+#oFVX+w [)9, tZ& hE=4MJ'W)7L9BRUPշxx*Ja(fbK/Т>: Ρz)蒚# !xEzhs]S׊ߓ \+>jPn1~ڞ3`qM\ ԐjH1QcvW =5B~@ZUօxO/{RDqOWTrk؞A(~Ba!^>^>jZ[[rgiAʹ"wo}- +-+x*OmsxРfPj10D42c^tw'bX DI!lWJ-Yb'/)CpZgX~_K<𛻍w!>Cb-/^5r+Mo/+>pDr.@ vOw~u[W%n>7wK(\{E(r%ʕ,׊-Clo|w[2V|_]钸(F4߭MO 2"W\+jRZx6TۥݧU(繪;Ϸ*]RTFkzzmHz4p0c^-T +kb[%W\g|[8Gg*4Snv׻KY57(*eê!Fs+6x٤'O-|s܃+sh󖁜kΙb88SP+ʛfp*ֹYI48Ъ} F3 mkքNhgM]ʝf\.kFUTpn)Q6b9.dsf{ +;#mvwyή:E[gt6Q۹![q8۾/xWvJI7/w_A_/eyOIfN64Q/Tr ."..,r- rʱB V$N66LӦMMLڤimlg0|rzS!>d 'BG9G/07^cn{lA&0qꯑF@x֊oqm +^ 8mJtŰ"Nq"ƱnpdK3v0m#l 09Q0WDdDCBїkf&X Z؎}@N12É?c0OMEpwKpAo/DZq%{~E?bjopBL'$kO62\tOib$Pj~f`}K*~BwJNw%xgȳ[ѿ!몌sm\\fv2He4MF L7z2['k=Yљ>+?U- C0TFxɜ2_/RO1Ѭ(I )3اT1җc'Bwn:w{=w/m1ZUGhQś:MyӠ;񨖸RޯfJ֑sFlzɜ9{êSӯVУa<=<y67Мߎ&4sS#괿\24Apkod1{?Տ2pz5tiRd^B#B Z ZExz+¥;Sk/[X$,:V ERɠ㲖a hW]hڊi)˥Q^ǣ3S­UҌӰ0i籗BU;T~x!%>^>x]jpItO$Nwet+YO!!AېPXFMR'&eT4Ob3/`-LEXt)lXt;/H[;|D,`@C>AKZM[pRp1qF+,fijQQu&ۋޢgJ+7>ܐ{UjpNBCY4?Fe01T[[3bjJc1Wz0UQVO} EJ7?BWo!Hq}.dc6ɽUrB*jVRm߈ݾ=CšܑɡDiu%7>t΃)r]@)?qBSDS.}RSZ2SzQOh&v2l,k0զPZĭE6s[)sRFO0yQ_G幉_։Su/ I/X1ɠV*뾆qL 16Q14M96i*!B\oGiE|7ߓ'MA2xSegr7$Erg+4y f},?G@G+S **~ZZhǖs쐸=o6kSrUw=[oIv8qK!؍`wq:lIfyd[DzelHmƆoXFHnUM 2Wycna;XrΎZF.hMV2 %HҚdR[LcC٬oMJxֶJ"FVNR?O]}ɒbq \{R  9-u{2Hkۗ`ֵkۏ&Vw%C+;FB;-ci:ge/q]Nωq0`H}Rn]wRHGٺnkEfugc8!pBp9%$t cqHGb{&)螹yיDGzbVi2$njtC~G],hǺ-Xԉ={ܗg{ "PIl`bzO!%.Q>+,D2 +p7oQyzlW{E~dNJ$4!O;v'͕X7/b2=yÈ;9}'2#HY췀~LW8k^G-^>,-ڟw9Uw-*u\MHrfG'Ӂh^`5#fzP$l@4S$d,B|1ɯ~F*i~HO@zW_quВy:1ljH7fz3ןp |7)~!L8sĀddxq c`t7j~8ޥ{Wzr0P~$ϖ͈oO7Ih/0I8 Rb"tcW)(ř=kV_L,!e*2.C|E*,vB +l)[*(>8%I}/ſ!Vz'^%̢%%I^XYdN:Yqe VxAa1 +ORHB(ȶ\uX'ದizV.zrފy`$(lFA1 =da2$)"i’|4UdfmiO "kZʻ:VsK+?C~ɭ\R:FT^;Ș"S\gNTwzjffP@ZEijZ/=[mjdlw$;J56s3>$^bx'CZ^`D˘ Q|2B3 Pᚖ)əS(Mʊw] 35qa&,,}Hٟ=6<{ǨA@*JŌf94>oK0v_|ϒE^9c3QojBt"\467CrWQy䚷_#.jd42F.q<=WyW)#Y|zO3L7 zȣјBW.tE^U/עYr)Ј"oHfh +9Wk=T'\xnz`&xV1{.<@rf/Ҙ?H.%Nzв!ReT歁S4;j@EvQϾj$7C:70#p8q.' .&q_c p9kds8lb ^Q{\ܖҬZiGCRǣxOh?\FP>)]b)H_Ǘ]9}^eOϛ(9tܱهGaR?çqW`X[\nǿC;4= +p|jV/y!LgjݛzVjs?n^+@)uJ8 G ~pexO\ 8D^n [0BYD?(_OLOTZ0>F=Tcúfїv?y&|7-8 -B J5W6\=P^knk=͸p5)Cu}Me"r}. !0\ c ypZ@f)m)5-qmSOa>WO'sgL ue?~M\'G?A"qŒS29ij<7 q|r\kqmµi'plZhK<ѫPC-qP}&.'"q8i8B\+qUk z4t=܁~B=ybxVLw$p\fS9`c 8,qp],>/ch${BښIΗg`cܜ+Wh-N_/)28p`#\adww!8ʗ:;>`_+OI/RynxMr>0)[j )f61f3״3al_x/q_ѽ3"'%Oe +gVB[hzD$w]&CvE# +G;[Bz6x0LWQ<ϱEG;)[ - +-uZyv_wfhiYq Ʀ1PUӫ0-8+]qVj2|?3ļ&9PYY ћִh:7ёF[ +wF>F6+4*2.G>jTU/QM}*WX +`"7^M:ϙ#9})O@ z d#+X٬ĥTWSS&{Nչ}4{27(~%K|1k&X/$?,ϊ=#wixTЖ06n!7zM:M>v[^UZV6* +z(/L7YR?Ĥ:$ܗsxKj +.IE'pK394i>?ڂ8 +hR+\oRXM.Lƣ]X*0=Z|Sdu Ta=}2oyܤSGavCUX* IX9:JLU`*"1N +K/=6_9ڒ57"J s-ki),AMlT°6`.I$S|J0-6+$2rhoSkX9ΕZXg$ +V[C=h5iX``DQT<,y* WVȳȵvS5jdni%ʪ~+PVNrOjGao]ScWa +Bo GgH~u" 49u%VkɬiCY%v?i R7HqKrI#>뒃 bF`HrK-jgs,@[ M](9ѨɮO%әҩ%YD n\=$uk(FE7(xMrpE}V)wIjE%Eg$ +b:S"ڣ%S:Ok:YӵЮ1^fu[Y5>LrMlysQ= ^B]6BBtD-"o9}yY'ԛFW~=Aewd"~w?;e9Ӳck@aŢd4 +*!Q;!d׏X)=Krװd8xyTwXpH0X*#"[!PDPBP8D((nEM4Q%eZ8[51q q r}{g<0D݋&[Q%S2u(.Eq̊OOE7e^,Vc'_#p5:6SpY# *I=7SR3u(.sS_IL#֦ji853id$70¿)U\G' +]snlqEY!Y/ڂf(>f dBO0 |Ax|b>ŋͦ#u<3@SH|X񶧔M~wPҗ\Q:cppxax*5 Ȯ_vB^{><<{g'>fsXFm|.w+8_e +׹$ptg/=`ߥ1K>'ɢΩ= }q;kbGZ׽=R .w-߼K2v:Bo ` +(\q,g9z}\&\xv9ngOΓe4n z-8w 0+O< !R,mQBZ]ej+=# /_!%}Jf ʠ_`iýWMm}=/N|8L4xOoXqNYDGZvڸL|«t+Eb8 9XD lFԮ?JG/'iœ'EXǧ1U-}[}|cEwPiKDpw~Vl0_|cq4σk"]?i.D\)JKE9 +U{RXzlhzQWI ޢV2T~N2`D+=R' ܽutv4ҶUj&VmU6Uۥ}6׺WתĬR +;\T~L+ʅF}UNlَ9nږ]kkwUtѦv]#,tI*ꑦZ3G+-gYjQe:ejI'l}8z8(tM-dz}TI QaPV3g=[闬~ ieoka[?EOkf޳3 +)QsYv~nةRiM?WA7|sr킔=`Sp-1Z<(ARhp Y!%JT%;|7 s|#r͚oG1G~)eXcAz+oeS:icҝ&h$-p4h:*eh-aYJ^rŻUIź]f(Fs^ @ݸsrΌ킟;W2*c]ܴ[*5Dv |)JrsOT[iLsG(ƣL=wi Ey]Le3óg[S3xs-`-.ĭ߲ւ1\JU{WD9^ъYީ^Q) EtH5h [F^_ƾKXyeG3-ԑmDo'ŏrWjOf*wfF*of'+PxzM ڦAi),PX`>nF-P٬%+5Ub]4+SQ~ 8Iӂ">:FSG'iʘtM + +[А#z'+MV>C0·񯂑gxf췥e%-PJ +lR]&PUi MS̤X` +"e" ,^A TtLM5s\ʥqfڜ42͎w~=e;?=~EeQZR 8Cb IWthBg)"l^0>U aO0k(=k67p%_:DJ%<:8Z!J %sh%Q|hŅy+&OEG(/1USYȹ2DTpFEU@UO;G/{35\Wgu" &;*1L(6r#G*2Sƨq + +RXQq2LTpLbW.`x4tZ+V>$p4{k3߅-#I]$c).G +U@|KT*k /nf> 3gy&Z^z`;ZuhWs]N/ыlf"BtB':*4hvU@(MH_M4wb|&&;%C^):G5&FviT }#{rKƑ@z؊Zl2Lsf&9)0Ӻj|z_f'c3+@ydeTehb Ta4$A\'$,\3p4RzDwh4i6Jim5<~=U-K#r4<'DCsc4$7Ys'kP^\h@^crZιw/Sx?eB{i?\.sS&Ě6siG$tMUxk1MDoڎazO 18Qd'4Ocy^%)?ʥ(E.]K/fJљ"}u{AtsZB;NŎ1Q8M6~o%T'> o> +SnFe%e1:ߙGCEcJV:}Tvû ۢ/!x +񓎟l<])FT5Щa'{Y̷2Vu3JJ+jqZ|l^&ttEg&gUSoӠZo%]jUOmk}gL=8:/]Xm3V.Z3*ʩ@Z+ѪAc;fO;Zblshi>z>T uŧ3CoYeZ2ˌVZ٪j܃,0K4 hE  ;MXXXXX {5rYr#Y1F52oVڪuSg۱c1nv;=>^>ɩXы81|'x|:3bG)?lo~|7k yif +^Btc Wjrf뜜ş5o7pQx6af%PL1w|h9(~~m5m/]Qxi}m瞍}l(XP G*TgS3wv063+=Pt#,V[@ʋpw`'PdOVIA|G!yr,u䖶a.%|cd:'s?ctc'^=WY/*IyY:͢U.QG˂G_ՙi$ŒRL5dVQՃzG0Ԝ% +5&#}j)%^%zZ=x\뿠ا%;5k/)ܡguʫj9UU[ sM4Yj dԚHbC}Hm8HkD\o|s}I\G|5WG{]~GdB.yQ|k5&W!"KZS*Im!' - ķv:LL,mQm"Q-뼠~_ T=^͂ .vDҾ$I|gqvuq[v]KdW;ۻbk"[!%@H [:ԃUŝ58x@0$%[W Mݻ} '?d a@!4s~' j!&R3aN[͝/ߑzi6BC^2k+չ10}D<tsSĢCG!u׹p`q:~n s ZvN ա :?qrs#iAs,?Grs9dc/}6Zo`u> Wѷoˆ]/^='C3V10ًuWvĎlj=mBuIa֟<'Cgt;3bцovO (x0H}즅0ar$-s,"ETs&Je;L8:XgW|~XX ʦG!b(e__%+5ev̧ G)%]71 ÉIO?D=X%a*jc-Ţ%,-_zB֬ mᶝ_ujzIx&XXIR!dW6>O"5'\9X8̿I4go&U`4wv8k `;S7"/kyآM!U ~yٚ㘫M 4$U6fiz%*oQ-ה tKZ?Qqkk&"[ .+ oLk|9۱V;8kyZK [׼V!Tzf.UdM9SsJۖ n_Y C +;S w}|8 +- +`Ὡ>^-Tԫ}j\o/A},de S__EJѠJ +D _ ;wfY3V`KIf ߦu֘~ӯF,e(?BCb!iJ1 `X +74p%T).d,ƣ5~=dUL3f &v*ƍhO9` +lQNapQ]AJ Ӑ %ipY$(1$EJ0Q\h,a>Okd6mS"".+tY)ܪCsc 7|-E? it+J3vQJh %(>,RqYLkJWt(# Y92W)̼UчehYe89fj1xU"{L R]gRLI%b".9FQ$ED)&[1 +-2K,+EAq+0Kݖ! kP{{`lW0~Oxy2[RZ1.X/E*" %TqfDv@U]q(h"" &)ŌȎ pdWɅ +*S9%Bd'rjs;9f:ΝGө.{{ ҴDMP +ДZME}Pџk +_DYރ-djߋ;[ReVd_Q^ +UM'LUHS +ДxMMS`l&ŕibJ=_/6O8 g4.tW,Y'70ї2ؓiFƓl%clg8MQz"a&'((!X8Kb4!)Ess4nn$/y|SZS>蔯5:|ZK-zx8Zbʔx Wi)+` q4&-\~iQM+,yg52cFdl_QyY7ZFƐu #*sFpl +Ʀ "z0R>16LԨ`ȞfG3'^9[!Kk֠r+50\sn-"lvĸ ?©g[$ՔJfy|NYH0l +,`*ш>*pGzkh8 . +{T˵(Z.\~J5Yݲ7}E9ߔcENJ_PN-,H%(P]Аr)DnWG+|ԧ_NSPHRJzT2T2[U6 wlDʹ[Ak˩B*wjU-M$䋆 r-[S6;7pMvD1LxѴ!ƚX-tO^* X +4y+s\+٘l+\~CL7"9 d3yɼ11] |7|ezj} 419ɩ._Ԉ)xtEn"M;RGj~Y߸#M'ymTΈڧ6"z'qm%ģu;ڸqh''4vN?nm +N JX `J ny5ޠ>}\B4.ꢋXt慮c캪>DC/GC :a."J^Wѵu.7D6Grrb n'sVOdc;`&(%ty]mU&[T7g)7$k/ǬuU.,:>PvD(f8hXdL00t ;pVW2%6&s)ϙ\?#y9>:ÿ3EO!թ^b?.sׅz 2 +p)$i0 0_pJTY:8[X:Ʒ[B,C܆Z ׭|A;`pCͅYp]%Tb6 +o USmlw>-u\F *" +bm9n8^C|n瘵WgUgU즫wGjC;&1UNY*@%}K2[Uܪ|p\9.= eY~cwEk&uWjziY??-vP`t5R ]cU6Wn*AuR2 ٤̡-?SÎ(u9,JsZ~tsчߗ{hULytņTE 2I2Y&d6wHd5(T5J(XզI*7$yʖrSLf('5W"e9dIoTFz3VɜR-}J<#S֯gB.獼{ ͧ?wa 1cۘqF 3m-!ϑ5}3dHRfYJd*WjVRʔR hݡܧe=+COrVαh-d+g0]IJꉥ4G*5WYفWzN9 J(9"Sn*1V Kd,蒡1*6hK] =w u\:=ƾ|.z 1]1뒝?Id.P +#ThPͤ[reXTj*xnͷoWD!ܒo +pGe Q c=P#r +3e*VO J"$V)5+Ŋ,[yE7+Sت`AV)7Z/Zq1؊4cĔM1>dT(r"B5*RUUZה)ANͪ[6kf~jzeMu[ GyH[:ǝF6` J)L8^a +iUpcM o2kfU~vhOR=غJnҏ[hJI\Wy7{t6jaih ;`{} xb tf`Mkq$:lR= 81PN i k\GC;}%g{b-w}5 9]5k&5HESuQ.HEb.^>1s. 3u;zۏ޶n|L}5^z7nwČ6x7Ѹ)27 &1vcນ0{y[~Bo!MƍRز$\O$W8`yf}~ςæC顰{XЃĔ }3A})C6z@4qg4gqGq~?ͷOg-)~ZwR;?k/uKų;H8Ji.i:)Lރ>`I4G94d}9H!jcCx(ya?GvS*hlMӂN^Rњt-J +02T8[|̰VѺqLKmó3ѧ8[z ryMs}S{r@0DdyPǿ4h@Z]sY`YaDAQPh 5;Qc4Z M8c&11ii;M;mMmI&M?. y}7s#f/>W .0V& i=psTw/3Qi?A }#O iiCéѦ!>>|,w(,9ݦ A&G3Œ $xY,N%eHXBVXM& z8}c(9gN FvB+1u|qQ|njpA"J 4K\VW5FXI}8.뱒u +`w/wye3S=\7W#׳+zc hxFX`YaٵΥZ3V<֥a~J+fZ^Sq%u99ǩ_Ҋ܁=ǕȜqM1sX3~Ana< yXӮ95xI˼ZݣyꙿV Yuc+j]Z~f58ԏZnJr?-HzK>fu׈kO|Sla.,P_z+_΀utʱ_8hw1CΩ.jB9U]_\+g->Gh35?_%AFu#GEm6-RsXjU}JFWuNUEWEK@ee3|,[#:Ujp ցbt0m8-NMYA橮pG-2N$A=Z2IapSGͱjV]\jMREBJUX%I%I*NZ䵲lSAMk=eBNeT#mh!%u?A=|誆DoTX%'49]%)XUZ.^fӖ*/mD9[W'yUiYwe̙_(-9 Ip?f-vͼ_tFrRePilUdNT٤l[QZeg*3WJ,sWJ%彋JuM\$#}y; +:rRd*?+RYqNQVv2s[jZd_UJ.(e,_tYEߣgqNi"sx_d<;TI]Ù@;ZK/^ZRdwU~r +2)0W)*e6)ڥ+ C.EQUE(;E]NEKyr:걑z_ +>/S U% ^2J+8H)%QJ*IԤl,e(kPCESX +8ʋ +UVA!˝BΒoцQ -R7kO9ϒ|*W&CA1IPdU«l +UHMjvT;Ww@uȧO[%b{z쒶Ђe4ZI=v0w'E1uUx}Bܐ5(X~UmZͽ<"'5es}}se̋\3:bf6fB6%=[!R(dkS .pw:T<{ji:/0 8 . CB.,4{m6 -}YXsGZ~l6½mb.ԓ6ҏ$z' ЋY˿ EorX|4bR@VJP֝Q"lqRU @)iR@6h~%kq<6ral$چvj"mIk@@"n]¹Ks4s.vE3| { v uEM4M{%~(2d^lO/;\rH)w~1r +uzŰ8A2lc1p mGE_ zVUWv35߸Ò/+K%N5@!JX9y~\Res!p&A@MtZ[YpiM?q|!@΃ 2:һ䆻X~E݆F?:x+]oq=ZIzpBCp|%^/03ЫiGcq5oaD dEM[c#}7h*L_l`qX>ZK\G&x\1xg誧hJ'<,a دF=Z u?Hޤy~Zѐo^'.LJg"3Ђp“[ܛ'/~%TQ ZJQ5P;ѮuAќC![a9^]?>| < 1- p%q3# 9~.yUjt?iTڭXhV<8Guӏah?tO F)U>Zm"@"$xNkdGOEp,1Jg)-!};5j/q?u2hit6cvr&NW g|atֲ Z'IK䜮Y*rQK +߬,I9h#;䜲\Sϕtv-~ݛp5\3 ^8c~E.ZKU<РŃZ IRk \q[yE;\ QܥY-J8 4)U𷲒65u_mT0-9^F&*c5C29_G(ݫLi+4{R4*eM=)RߧJgg7YQ}\d eėR䀧r=5he{(;L4gLfLROftJSIS4%p&ݡAtR CN;u'(=s` l}1Y ٱ4w2Fj4# B1J ichjL%ejrp&+1Vm54+.cUEG| nI}C;'QnToed"GY!cjUQ}BcŒwn|s ѝ:܂$8:UVKz9ap4  AبDŌ&cte,Pd2*(AP H,* * E\RB+ +b$EHg:I'MRcb54xjcJd,^ mx`c4btbdi%hhb)*N"R~ԋr."px~dݪs,i/cdHP\BF$F*6qbFhXh MNUtIQy:UcaRx:OۡK? |Th +MAr> hdߌT̞ͧ'g{b>aƾb W1JO 4"ӓ5px3"N~ZdjWz>M.C|{wwTs.ːIS' Re$DgxkPF"L +7Eji2tVS +̞>OWNrc+oQy+|r7]\a~l*LLg6Y*^D'p|uShnOU +3q'//S>,T&O|[VR/ KdnF +߿UI߫NB둆+\s9=U܌OF1/d/Rς>-'HyGr+LR +" +)Pb6"کo%y}|BiOI?*ZjCrYVjeX9dVⰕ`fT:FYWVt7)}j IFwP$oich Eyy\64orq6ƱL6aV 4"V po@ԬL9+RhB&O A,c)*V5pT1$W|F+VlHKؖ=.ĸz^e?rk8 `σ,Q$I.jK?r&5ԤW&4s5z3hְ>V}fbmrm썜 E~Q  $C ^?a ;N=k=Uh&]ƭ"zHCl4N4= +wBD]˶3B_-Cl=tMly`{(fLİThAxubiOhMC$$#sO-<$Ms_^mur:xKv!x(pC# +cQb>99μ88A':y +|:8Wȇs#gp^uرcNAE ai;N>9ef|LJpJ+[L&x F=w@q,s |0CRk, ߘM-6w^֛?00rPEurps tqeUC}Ihuw>D}Xg-p̩y> Dߡ)ݖ <]E~2ƾ0~?QCp`Ϣ +OOG{8|μ 8#+hOsba6md!pLմzc eOD=&8* ;p\_'a7,xdc#(q?^Ïq@upl z>pE>mſ&oDC,[*E3'ccZʱ}ͽh6xa<uCmLͼK۩ԳAte!g˟D?- + 38 +z k<̇g p D2`&< < ZOi{xrHet})'icޕZ&Ɖ`@lQb=$2•W6\yT-Kgp-Z2n{i:tJ>]t @o F؇Q_ճRDZ:h~BlĖDlFbˀ/> |E*rGVVBUN5q@/wy:r2=GSTXUUiPEpBTRPA\G,UneGu++j2_R5bB`Xg/? ݛbvr>N>ev,"J RudU,ܨ"RU%[MyQeʍUQVӣʌYUmTj.%Qb`>S)QT>6yiԤ4JTd *2i*}yS[xjU9ZsRhUrR*!|k1vs,r;؎I8S4a^9f&!v6c .c]aTO$c$pulG4`;6@C;ܓ9 u4_s&mmu +r>iTآB> 8x |>wpR 'p4&s2N8Ṋ:҂!j}~]y+W&|jj4wfj#9po89x܇jhm4qoE⭖7m]ƾS\O2yE?0#\;ɣ9dN2'klk鋵Ԣ|tx@_9Fӡ\V_g]ij ;o#%I3D?"ztSnffS沉=$i'1ˍ( /K6j+iX=Ă@~⒄ ?z'D<\.ֈpԽE@Kb ;R5 6<^.}Y#~RCAd`Mhu:Ho2уd>Ph@;Kwm>=|<ŸRb|8qcP#RO^q<>M-^ Wmk%\gƹ9#>7njBaBHg [!"Y+ ޥ?//pI/u{Mp^nJ"y:z&=M;u,yx?|9 p}(3|J7Q5%LO+iZ ʹvxhhh2g5l/p9|%8N랣o(bQM7shp,fLxg=<[h^x o#g>|f{56n . R6x3^iYЩ8 O#<ӡ11E|~~[cܲFn؆!<0 x/k%2/Ѣ3tR&reG/'z p .K h]$j.c;N~T37ϖZ ]V}Ra3ے̌ s݊^vpj:C]E'V܎G +zU?Ռ D(;m8LS/V#̀/+͉n.ko| 7x=/ mAc7.hU?Ŗ2OeODES/V#'&5)[ !:&wΨfu9o2=G<#9j6#]8_O'h^ZYƏ㌆S3,C]hu',5i]˵%F-lQmTwj^UL{ZTSQC>_jXջp|v#3,q'kDktbTVmlcKURq.7aЫ҄T8ylו}Y)r.#[/3_/&)bEad.kJ CH0ˑ+{UeIvnVqrF +=OVnYN*+ݫLӫ2U?D&Ӫqms\`=8d3hҖ|arcTfLTIJ)Y*JWAj*VETFLYǕ[Jڎ|[ˤZd6Y'%V$sdVZnRl2W+QIJ,쓡hTqV*LE/^RT O*0C`["cb=ug]ܽXy4"K5v)E5X(d,2*(CI<%P\--hCYm{iQT㮂?Q +Mmt\Fp^n&'XD+ 3(ېyBJ+Q$khêhG" +hUXeB*w+ά N: 9x` k-&ԦC.#S=U g~2Cj"S:K5 ++VPNbuX,ne׏ӂ3<W;se3ϰ!Fy:9#ri8Òa hczj{e&/GѬxylȒ,`[w4vbEuh]sA {apimbY2gH/s58.wzd:@=̊R:P hxC[ bA +  zvs騟em#9D,sOirry)'%"S8|ύ cQndУyFOQȍ,{XF>R7pՕ)98K= >yrgCd Q13 34|2MM4Fi.>Ą682Gƽ," 3-S$=(<ɡQo xs +uOu<~8dO^a]R^/w&7.p]daR%\ +Yհ~#y4sTgmgG`lft6 +aAR^##Ck{^Sxgad0jh N'@^\&AX//wWSعd:Mw7 +^`0cK+E>SM>28md VXzU`lnXU{_';;Qp7GF.6ȀK8TSLr +V'X ZV?k0;@V7hnXo߃ka;1@-+ h0X1 ,'ɮ@) V-X8-i>Hs'a+U8;w>Qh\A_#& ?3VXN\`&:7vzء%`k6h*@w3)j)LgrpqFmØDAk0YgU &QG!9.Pȭko6xݰk?f yJ9%fEN! =A w| ~ +V%?+xv1|\WM^ZLJbۮBC*0S C ZuO}~B}I >ȑ.4/j6z&gW_* 4P2CJjUؤIAmܣe r¶)+2)=lO qxs<܍ڒA$egpՇ*(\A*SIHB5)4Ga*+3ZS=n)#jV+Re~Kה`RiXIt)|װH;s9x ^W g.RiR |KjҸM|ҤOUNT&80)#RebS,ZeNM)-WF[nEOUDF3(Ա\ +#CzTYwdp6!wH KWJ!`9$$I rN#STEm=:6ֱRvL? a>}^,!%bd[I"n*+G ĺ[":>z^.^.^ꢗ"{DnfiFZR` X-n`!|O } `aҁt\Wn٠ +dF{H?2Nq"OFc愒!ggxvb<ˢgρkjg_M쭌Ks82]|3S G4·q'1sFRwa2΁~woη oۯI-qEo88WeE<ͯq/P)_qh;0g?6Os>rళPQLe?GFGFgqȧi1 +(Qf=`ÿW\JӠ:ُEQcX"WTJeGֳW3^.8]\bı8تnDTF}Btw9!փ[],WzaEBybGpVN{O8qp JuDVNI +N;iQg8|pj+`?p9qTS'kısi_Vn5RV\LfiJrƗqAȘ̼ 3Õʞl\u9rT^&UpΥƅ|;_%SpٞUo9!^5c5)5oѬh+LgÓHd_0ny J#k TһM#Yryc35FV 0:_"\ipeÕW \*bxZX:b 'rv(>0@-q  +$f8c`HdtgG-f2'8Kc8rVEER%tw/? +'H39.G,'dYt S"|eWHM:ȷnळ9,z4So:1xRr\|  J,yzW?xI>cK/Cḛ쬘 +&s92gXL5JzR)=J2LW|u/ҫӅMYK,u䧆Oe!1RqLVgFx*T,o2|&*wRf+V+.p,tZA/)Q /q|&ʔ/alt`Ce}wtuX$ljs=1ERSI<2h^/ʦsr`\c.>/-Q~룼-LLTsS,w-y K"x %3A=8`JJ!PtGy8;җNk%js{M~ \K"7dgeFӴiV@9r/g!Bc܊N`8?h)-E(" GN*" ȡLIu3tnn:\tfed˶}e!˖Is` eЧ +T}7[=ٌb6Hʊ [|:ztD +H5 +CB>*pWJn/V`vJl% 520b;0\Zsؔzb:\(5úZJ pC>Ί+UbQUJW XUSfPKؕTD> νWRࠕf=&=Z,\ק2;D,&Ť|LT<D8 xJa1 w螀BJ|6+QpO(x709%'lLuQ-L|T ^g2;t3b8|[=lW-KPEǥY$l9vBX"`7~o|pa5 :ל] g5RUG<}6Qkg˓ssN}Ku.zd&^)e 2uY7SK{VǵzB.:3|KH|2nMR`ziXonw֫zW7)u?RZ+EK9l19̳>L˻Ee/k0;oP KlX8ͼlx޻'b2cdF~["eItJ. ۝gmo\3M7C}P,HAQ^>.dSv Ȳ~$8Wk7. !{:i q,s4/z(CapLSb1jKP;#=3x4-_ ggMKhZ:! kuo{>/ ǩMqķd#)hO쏩_v޷mm[ + [U;.0/{r4D=O3K08_[v*24v)1U\Ο0ne´6}vI@=!Z@Qc_4O혯Pl //P?9xֆ )] <$mj4^żۧ]/vvls_ؗd^kLq&;36l*O&}k-oÆCxm7b[gH͛!ױ5s7GK=Om,/|';2sH-{%_xHÙn|8Yi"fi;l),d+=lD?-UU.pz(K9_P0cጋjd%K],S clry~7b^!¾9i$$8՚'a)O3?C3*2q,'VnڜnPS'쵤LhN5v^>npJ)s)!|3FU>NLb;UM"c{U n\߈88bgcFz3ns'-$z ++3UpkåuYԫuJ̈ѩE^t,m;fVEvhh75sk"5hiO <{V{Ib]?=i]]\Nv\9}p)0/5ڴn!uܒh'iFDJG:E(~S@<pT:g埌=Vmzd7<ճpP\KO9,GmӣEh1/:FJ~hV¡]TrW:HnDr7Ⰸ+2>(?}5$r6gzL=Z]GY.% O=."]8nY1BVbА%c !_.ns;FVl)>e8Ŧr7F)ռT~yUu%q (@FV v+'6%BTPM| %b7ꂚhXqu&k[b +;=޽/n|;7w;?3sfjRP 60; 2u Q.ݸZ3xab}K,&?oN.Phj0ȧ5I!878jOfU֝i@s@;2zz(y2{rC{B t +};`_|j?x)B9>xdqM92y!6H/^2{yq+6cAh;4[#dRg?9˚Oy~V?wahK#luGrBQ:rcԱXpKfhu@f&8m1_8ړ;Л>UJ^!.Eg4Ç,=NK=3eaĤԑi!gwI$3o?E.Hgo7 +#Ϣ5`ҷHnb Sp?_8:G$vgIzqdY>on"9H9Ȃٟ>ɡ_qm2ߝ@!mq1:^P5Ҭc:ڬj#c("b# 4N^6+^I m(۷h0Cd(wSZH@$N@JN /(nCoGgFL Mx2LY]|l]9p~ M)IJ=S#L>Rۀfɼs4ŵC*hQ@'nZq_O˧KhӸOȍt$ƒil:/OO!y 6bpE`4[S.r8YMךuʴ0li35Jɀ z1o_Wcg/Rbڟ؍r%´LV?ҿ7.II88,T+Ŋ(Y̫.e˕XrV ZԭݣaMy5o٪u:tӹK7|gpޡa}G8(&vaF=6~w&&&N쀙=L9*9!~I`貤6$ݗ,=4=5Ssu2(ėaV)Q'_e*)/PcA#E};9RK!{]`l9PF ȋd2-ޔD%1 *:fȰQcƽcH},G_M4DM4DMHo. /W]h&<Į[ٳ|@Csްs~ qu~b׮&yr;#R2 _={YHSU6ugc]|@JϨάڱkN^6?au;m}'4)WsYo5riA GTۖν?R,v±ӂ* ;*u]4O8>\hFX%רsĭn* %:@yd׮&}r=Mɱ7*ڡf~DMJL=,^h{En@]lUw2U܌Jf@mʀ"ͯh0#s + Ƞ&%߯=GX8CjLjDnnصd\LcUE)OUS_v\ltǂPA93de1J)V$EJ/.*)I{mTf+}5ު{Ў%>ߪ7/_RQM~dXL͘ӣ:b?6sʢAyH³'yT,-h|^TjvQJI$[v1P0ƾ.mEϿ{eyTZ#lM?n]97)1ҋ5F|}=UU4Zb>>q(bV3'q$Y``j弲(3W"_e+ K/"~F~M@E9kp48dm?dY6˚%e5 PCkJKqݰ8B4MЭJYj{l:u/}ybvzcʐ gXVneo훣_M!/i +=euBT$@g֪⍝6~SҖ5ڞniրV8k@_+@5 +d i<ti]Ni 5ᱱ#z :{3Mi~q-cE(TtR !ʾdO%)K%ɖ*I!"K46K3? 潿\\s\ws_&Sݩ֍ +7=`% Og+10v5ۃ~u`5,[o:?P G$6 `>`jI )c`wMa._!4<"za3GSsrע~ZUmiB~eyZŀ]qlu-iU Z]VlklB)Sd#"6c`nX z/F%kmR|ׇE8t3o^M|ߔ~ㇳA:%%wm#?``{4ȓЎ++IF@` A@C[Z7P$g_{D D8܃6"HF +b)S.LE;^N 7To7ꁵ +cؽ̩e<_!ׯޗwi5/A" vJqt`9l<Ђx+)D`e=e`GC#HS#81D6;ijK$E>|&޳hUׂ +VhgR`8#W6/CpHp6C.:OAoR  I1`Kr:iW`x!(/4 +n23J[LC l*SXDb\7Q]~iX{.EZ#\/j@Fg5@>QK3@s>H1zDP #9 +FH~}ӗ/؛7X4oig̚dgn`cam0/4zH̦ӏw? wəq: -~ms:x˓W7|#; +F( |gO>eSO5wS)Y<+Vla97˷OjXt)NG߲K|xrg2 +?``0`&!; endstream endobj 11 0 obj <> endobj 29 0 obj <> endobj 30 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.0.0 %%For: (youyou) () %%Title: (未标题-1) %%CreationDate: 17/5/11 下午5:14 %%Canvassize: 16383 %%BoundingBox: 215 -357 587 -200 %%HiResBoundingBox: 215.5 -356.9794921875 586.83203125 -200.873972506743 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 44 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%CMYKProcessColor: 1 1 1 1 ([套版色]) %AI3_Cropmarks: 0 -640 960 0 %AI3_TemplateBox: 480.5 -320.5 480.5 -320.5 %AI3_TileBox: 84 -626 876 -14 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 2 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 2 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -415.364781760912 206.383230838457 0.6667 1114 681 18 0 0 78 133 0 0 0 1 1 0 1 1 0 0 %AI17_Alternate_Content %AI9_OpenToView: -415.364781760912 206.383230838457 0.6667 1114 681 18 0 0 78 133 0 0 0 1 1 0 1 1 0 0 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 76 %%PageOrigin:174 -716 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 31 0 obj <>stream +%%BoundingBox: 215 -357 587 -200 %%HiResBoundingBox: 215.5 -356.9794921875 586.83203125 -200.873972506743 %AI7_Thumbnail: 128 56 8 %%BeginData: 5144 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD57FFA8FFA8A8A7A8A7A77CA0A8FD6CFFA8FFA8AEA7A87DA77CA6 %7CA6A4C6A4FEC6FEC6A6A8FD69FFA87CA7A0A6A5CCA4FEC6FEFEFEC6FEFE %FEC6FEFEFEC6A1FD69FF7BFEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEA47DFD67FF7DFD17FEA5A8FD65FFA8A6C6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FE7CFD65FFA8A4FEFEFEC6FEFEFEC6FEFEFEC6 %FEFEFEC6FEFEFEC6FEFEFEC6FE7DFD64FF7CFEC6FEC6FEC6FEC6FEC6FEC6 %FEC6FEC6FEC6FEC6FEC6FEC6FEC6A57DFD62FFA8A6FD1BFEA6A8FD61FFA7 %A4FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC67C %A8FD60FF7CFEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FE %FEFEC6FEA4A7FD15FFA8A8FD48FF7DA4C6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE7BA8FD10FFA87D7DA8A87DFD48FF %A7FD20FE7CFD11FF7DA87DFF7DFD48FFA5FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE7CFD0FFFA87D7DA852A8 %FD40FFA87DFFA8A8A88452C752C7C6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFE %C6FEFEFEC6FEFEFEC6FEFEFEC6FEFEC77DFD0FFFA8FD44FF7DA8527DA87D %4B527B7CA4FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6A0A8FD53FF7D7DA87DA8847DA4A67CCCFD21FEA7FD53FF %A8FFFFFFA8FF7CFEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FEC6FE9FA8FD57FFA8CCFEFEC6FEFEFEC6FEFE %FEC6CCA5FEC6CCC6FEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFEA7FD %0FFFC99AFD42FF527DA8A87D7D7BC77B7CA4A6A4A57BFEA5A57B7CA4FE75 %FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6C67DFD0BFFA8A87D7D8C %FC99FD41FF7D527DA87D27C8A5A0527CA57C52A67C7C51A6A5C8A57CA5FD %13FEA6FD05FFA8A87D7D527D527D527D7DC28CCAFD41FF7D7DA8A87C7C7B %C77B7C7BA076A6A5A6A5A57B7C9F7C517CC6FEC6FEC6FEC6FEC6FEC6FEC6 %FEC6FEA4A57B527D7D527D527D527D7DA8A8FD4CFFA87DA4A6A4CCA5FEA5 %CCFEFEC6FEFECCA5CCC6A6C6FEFEFEC6FEFEFEC6CCA5A69FA67C7C517C52 %5252A8A8FFA8FD54FFA87CC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEA4 %FEA5A57B7C51525152517C7BA69FC7A4FE7CFD5AFFA8A6FD0BFECCA5A67C %7C527C527C527C7CA6A5CCFD08FEA5A8FD5BFFA0BAC0FEC6C79FA67C7C51 %765152517C7BA6A5FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6A6A8FD5AFF7D %9A6F99517C527C7CA6A0A6A4FEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFE %FEC6FEFEFE7DFD19FFA8A8FD35FFA8FFA8A87D7D527D5252527D779475A5 %A4FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %7BA8FD16FFA87DFFA8A8FD30FFA8A87D7D527D527D527D7DA8A8FFA878A9 %FFFFA8A5FD1EFEA7FD17FF7DA87DA87DFD29FFA8A87D7D5252527D527D7D %A8A8FD07FFA8537EFD04FF7DA4FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6A5A8FD17FF7D7DA8A8A8FD21FFA87DA87D7D52 %7D527D7DA8A8FFA8FD0DFFA9FFA9FD06FF7DC6FEFEFEC6FEFEFEC6FEFEFE %C6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFE7CFD18FF7DFFFFFFA8FD19FFA8A8 %7DA859FD04527D527DA8A8A8FD12FFA828A2FD08FFA8A0C6FEC6FEC6FEC6 %FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEA4A8FD2FFFA8A8A87D527D %527D527D7DA8A8FD1BFF7EA9FD0AFF7DC7FD19FEA6A8FD28FFA8A87D7D52 %7D5252527D7DA8A8FD1FFFA87EFD0EFF7CFEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FE7DFD1BFFCFFD06FFA8A87D7D527D527D52FD %04A8FD25FFA9537EFD0FFF7CFEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFE %FEC6FEFEFE9FFD1BFFC28CBC5252527D527D52FD04A8FD2BFFA8FFA8FD10 %FFA87BFEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC67DA8FD1AFF %BCB093A8A8FD32FFA929A9FD13FFA8A4FD0FFEC6CCA4C7A5A6A8FD1CFFA0 %CFFD34FF7EA8FD14FFA87DC6FEC6FEC6FEC6FEA4A5A4A67CA77CA7A7A8A8 %A9A8FD50FFA87EFD18FFA8A6A4C7A5A77CA7A1FD04A8FD58FFA84D7EFD19 %FFFD05A8FFA8FD5EFFA9FD7BFFA829A9FD7DFF53A9FD20FFA8FFA8FF7D7D %A8FF7DFD52FF7E7EFD22FF7DA87D7D527D52A87D7D7D52527DA8FD4CFFA2 %53A2FD21FFA8A8FF7DA87D527D7DAF7D52527D7DFD4DFFA9A8FD23FF7DA8 %A87DA8FFA8FF7D84A8A8A8FD4BFFA229FD7EFF53A8FD7BFFA2A9FD7DFF7E %297EFD7EFFA9FD7CFF4DFFFD05A8FFFD04A8FFFD04A8FD71FFA87D7D7DA8 %52FD057D84A852FD72FF7DFFFD047DA8A87DA852A87D7DA8FD74FFA8A8A8 %FD05FFA8A8A8FDCBFFFF %%EndData endstream endobj 32 0 obj <>stream +=հnͫ1N.aB_V!De׽l=&.G)4 {-=/9UW>aَٝ}z[Cr4'[Y>ݖ}!?cc6 3:}Io2ApEG+ rj|;jdAX/FbE'X[->`zNԿTF,E!j|܃ʾ2a{.Q`AQfńLUAv<=4˦U#,=4Dߚ @'dU nkY[6N}Rޛ_9Av= BV\겕YТFY<ѾC qEA;.t"׬)!zIZSMZ1݆.BcP:f] ?ٱAR MyCDbPm} OCl:ZkW?臤Oe t24튺5Gc蓨WL+Aevd{JkGLJ#vȒ,چ +rLoSM7ljE؈n|.GF+yYO\y$hwɲʹ!bJ˪Uk*nr>zs*|^˃uY%+c328LoC=IH zn@e4[3M+`ňN]6brMU;?;و=u0WiiS6'!qk/D>O}uq1GxYG7TӼS +x_VV TS U:3O=n9iӟzk_l@'1r n+4;&q2컗 3 +7SҲvQmlAܟ i܋:T_|qA=#<NVFB|sbDT*|Lqʨ䴿haI+/tQw +wv\C99$rfO QT}М覩SW 1e]s0pg @e9fqDQz4|]_Pƚ}WtctaZӦ]|ৠ~̲aLX 7hė X/ B"w/^Ʈ5A]yiP<3@I腽Ցs0ge|JlrBo.*a͎ P9s2PĖX3Oӱ{~.mB;( NNɝAt9+ŒѶ$Lc ;lـIraםYuӇf,ƶg}YĎϓ VFjZ1tb~Ǣs?4?&E=?9/c,Ҏ\АCG >'J*ůcc^]o|1?KGF4=LK;j{)*\RX[|CYAbհ;ʣo{5;hg x{ޝQmB-_5R䉎gNAu51SyVwL/%ixB>Vq[pEêTW ISKLN-{2z?ۍw!Ps?2US%{AN59i+gV܌~8.^a]3o *5ɒa.UyWw2+qHS>vT=^zb ++P_^5%iQjbm[yԁwQRcD)~8@D_s$ OƦi&J:c}8g kgF$ ϧ{ߨpi7tnbzM8\q/ 'ɥES2>&9I|s + (|?:2',<權qY76w=! r+~#zqG?~s+M:/ɗARA]˳ޮϣ;:_xߝԽـ=37HȡV%\{dˀ )FHܖ꓎}K\:Eѿ_d?3 ?` NpnצMȄ j %R?Lտri{~:`ȝ6}NxEw5ȴq)Rjxd'^Yxm߃/[RC>|(~ISOP%=wc/M | p]Y)v\18$S,Gc:fr+gI{2_5]R5E}#6Ԝ%5rwXXձkj|dO@gɮrd쬒ľ|ܴPS^Ake 6?_46JPn>/m೏^w36αlCXxW9!kéfIKk~]YTQK׬b6J71[][b043}~.!.kV? ~8;q>!H)[qN}D[21GE>J1dL#HCh#_8wS]i1*/?1Q;JJmfjxG6$-eA36gSlh6fyQϵYu뀼⨧1æT5f9mBPɕf3/+mɻ(ƞi;B$>U>>H-nʍ?ڪ?]’[ɗLקi9u>JIu J#фgD[e;+取=pW(,-'K|vBO9ON +s<ײ_7|}JO~C_}{q5y '3soS4ҮQjtU>"ՠ3/gTcj=)+.=nxvJPxok_ ?J}yRe =/BmX#wJ6C*^_ ߦ:_g:lgUdV8X`Ém솧=$=[RCSQ9U+Lƚ43OVn ЈF"A%wMkrx!}:S~}R_4e`獴 59g*.9:['4"P)q fB }!Tm9;6LxX0Bo_T gls9\\j(Qq;Nr _=@C}w 1^(m>Ыu+i \Ćiu!F#03 +pY,fFL] +G)0~t5"/8UkI܌!RL/.IzR2LH?{ *fi%1mD. +:#vĨ吿[FH;* MA aIk1P3zBZlW`sbzjYC+ZS +gOp̭~dĕ+vFQ1t?)kJxS*r恇6b.Ysd_bׯ9|xO㛕ƨocDToՃ۳RU% $=Z3c*fetιؙ-၎:lID-n ĨCVlW4:z}OGzБ!+WB#Z|ƩxKƆ}^=vnY]5b2%lo %)s#C*񬾗%XZ,4TjO1+ڛS΍^/ ?Mu?=gm9~rR6F95'bՅ+Z2&XҞUDPlFPROomhjx1u}Dҫ-c,ʗ WhRb%,뉹KZESC:lxݜ.}<}m^~k,;# (頃=geS߰dtxe€{`ҷG(m9j6ܽQb- s +aCeШT  yԃ{[sLlвٰ`Uh=/m f.h}}Oy9+2dt-;&#yZ/GE5 d`Tʈ)r!mTFNE) Ձz`h.%p!ZՌ +4eɾC ХRQϺTgSC.?)5]tu˯q)dĖNRUJ(^0sJRڂZu ;ցzA+Qo9AhKP'ί6aI(n=C-)fTᚅP;; +u!6h?ܞM )X絘ZL-<-5F>z$DXbBt̪ARF"u1#oV0eG|AK[51Ӱ9"\qviIq˻}2_r|e96kB]3GU1 +6w7!/'Տ[R.M=\+mB²6"zXȐ ZfVlxJZaCzPtЊXҚFYLxUlVĉM#aǬY ZZ3lкK(lY8 Px{:lv3aK&BD2rqQhm]U/bVY47\z_s7IՁWҬU?7I w&iXՓ/3-eo]}vMC? l9 eM bՌ/E;hM;es1>p\$-;WINNY4׽QEs*ze ^qn8dW95VႁY5.ezJٜ i=F4*E  WYH,] >.pw/m8Ͷiv'5 gWGa7lڊ00ʯQy+.yL*xÊTl&/M[XOԶ`4nۤ{-?"rbUV뷙V)lo;2"A^ł^tM5A#*c~ĎI#T2vG[GYkWKćYi DfVӲjlyyM/4}A<搀m;}Bᦧo-Q\ʀӴBFXSjA2+m9i-H+j~dDPQB9>OUc! 6<\򚛃⳧A@į[VvhXD[f#6 %5(ɆH !>FQ#6<0e"Y*m^&l{c{x]j'.j"gUmlrj œOh\bA\fIyAGTQ)*p+Xvr1 b[XEs)~OLښr1QV|PS$6.jo hMWBhjɣ1 eyrbʼ\wBޖR}!8i +Q6n@)Xvp[s 8% tu/1oc'L6kB*p4886X6{ـ0>g%lhi`cLв9ݪܚk̉RC$&@mW=o j톗Y Ԛ M5 7 7oj>ACr{\3ez#|8eu`kUb笄e~uC\ G@`S`4Ԍ!a_P v`ֶ]5r~aE;ڝ +Vr2{0! %S@ыrVGNU\Pֺ}'QvtOxȮR +g%^lH_6˗;0Nz(~M;4,[pF gΞ5qFAzÜ^k~ޒ4؉0H!]pɜ֡ULj=/uwxj^".jh]~E 5w/[hrRΦV6F9u3'Hc7rDG9m?qmDޙn [w&M:n!){~ mB*[d* +帤6GtK 핻S4= iyC"{oKA}S:t;bيOiy7ǮjL(cXe%ntsF~]@Qeei~_x-)iz-pV}1Ìi_jKZ^#zDR{-&i*elEtTxYx'D*WgOZ3 +<*{|SQn͛31+u%3b%~׈V3(4 !ISJxЂ31a!{?Ц1n| 1,:uѲuD,ы0eD-n^l,OHsJzI6d6=BNpX@ Ӻ6JT;gU-9%YFGhR16Y%[z{,2 ܫg-NR'1 tЂ.f#,vIoX1P뀖y%7՜ܨN?OY?k՝Io\]yPz(kFMXS +VxL +V.`xHV!NnAW8i(3'x!$I yD,p y;=BqJM#2td{r %'fF7`.0:rM# +(h^/l;9;an(4X+E%jK0dv@b۟]BN)Wٝy@X!{^74) ІySq_s3gJ<| v'է3Qg8h=mG{渦 3iQ( 6jhD" |hHA|ͺ.iR_+YVq\^wr(?C]mcFl% ƴ-0NW̞J{.AyQl]kaIe {OnNX{̱Ha7Tzyۈӥ8H-m$u&λx\ޡ.8sL{42!XdS<UIPg Fa7"Y[I&.n0%G daa<]G5n55RKwt7u$KV=]0D)H^U//ҽnԂTwQԉ ᯪ?^r2D[NY'/.]M r#+]ԜV= :)sl72~mJFQvs֬uY$c6fp9P5US\ChDެi4+9q"kaO«? +x=(qByhADm(kkJ)_|?=X|xR7Ѩ.?2 +yZD,%ڃ򰧱cUYu@TwlX_VfN?C9P^X!w>^xCؽa/>OH`ʖȪ5_ǞnM ̌j _W&E e_V~Ke|xt<ثHB' awX Oݎ7 y_2JyhB.E.E{MP6ZOiፘ;`쎟=tܥ<;1},sۣD!YoTf#d\ +1^ 5:t2" N*΋:dzydS##[AJƨd媕ʦ>?JՏhJfTEgeNe~|C88_['CI9ƔY1=hU/XI#(cȤ29VY^c(PZՅ _kcDBDj!ςbcC>trs-4QX .-Ab)r&6 o|mM"Գ(챧j]X:{u";곆K@z( /u*Y޲hlS]r+]R^oݻQ\O}^dPި#ۅaW=| *aҲE' +gxw}%szFŒZoǾ5 >Ѽe;V٘ +IaZMSZ{+HXر<Ʉ UHCB{r=;X c\TK88v(TWFeS +KN?(ixYz'y7uưF)'Rh*Jvδ7g$4!F]"w*٢HV II(_rJkzck+듁"FtJFtgQ ;x(2[4LqE5Jne4C[;P&s]Z@OZ%*6E.mIpKVHdTHڭ)4sH,1 -<oBI[򱣧?I)q*Se%"02oJ[]FMŨiVs4m{Ryc +DJAoQ'#1?*j9PGv< +QAvlYF)De0+U1cv-aYH8Dt$Aex{|r9 I :FբMJ mQ)v kRMyX JuF."}X{ju_M X;0{,y:ev9pLKͳzEoTӆzU]iޞt逾FЁ{%}QI3jmofJO/ vfƅ:?t]~Kftf:t^*8- F\ ^h[@HB<8Y{ow'><$}!dw Ɩ5U{f=rkPX/N1G kde$,/&O`M=]#Sj54wmԻ֣5~PɆCՃ#c^QaSn&c\ͯ츑XX0k~-(g{PYF4Ռ_wU&۠ѮJ5>5!.kWFN }ձeVuͷu855qf]4;UIMvd*d'ȧG&z3]-Isc-%nfaM>м}SlwlBsÑѣ.^/}`ݷH9kBVt4󞢟)X2g;y:QSDh dP[bc,L2g:YPW]R Ip"bƌf Hw4ei+0YWˇJ? чY(=n[S!xVG@Pe{nd֦liؕ+ mO:'6k\/1T>:D#w>48  Ģpo5 Bh3%(v. |lQ5&H<7T /9_%Ů&f1ϊ"gIzi |ja=MO7kG qdb\:z4#oMiu׻[6m Ϯk]3)ݐU6ui +[3}bQPS>=|!uk9-9$$oK1Jv\; tX'?R87UeDEMFe'sMՀݜG:L-ǛcNҦWk';g{[YW4*Gj +>4 ]Xx7xJ,jՌJ"(I.j)ϲ+ͭޥm!,r5|OiܦZ&(9dlM*6o%*%XM)q49t `ňQ3*%$Ҳ6EIN^Dy6֯&d\7P6-5W ׶rZ&%.@AJ^-g"%,\-tQ@=_NDV>F]$E }Q"V4-wKDIv(YV !LZ7ǪӽJJ?pFÁ5-kVi @̕Z9aO22ya'8Ҙ6Ͷ)-sv ez3{-#M5'm}g)-6<r+k)>CSoѦ1:&Z;%gq6+TWK+ik ^Aw ƮG%J#8\ =Ԅ+>YĂrLKM\9_8eHSQGE. g4C?CWDZq6teuhٵTsnuu2n `cݫM-޵w]{,lk8h3KjĈi\RE+*NKꖰAc(e7AXBXy/4shˆe8 f|M}k==͢bID`UJ_NWGZ6HAYۃvG!nyuOM+Sm"T{h8>*"n#_z-bWK}'1?c_ghM~М{Cה"V&_9n)>)~fjOOblNqK,rgKбYbufCXɚ}؈ﶇ盛OL'm V?\)':RRS3:C^G<ϧ^ό^9HW]5 Z< +9]y$d$ѪW[Uρ~)ͳQ#cq>_Vgex 8*, O=uqB8:l_K0V;^1PzoJ_ݳ6"EOm#-DC-PEմ@8(!w_3v5q RV"=)ޟ$&u>ߟF.=4_G+xWND 5>B >`wE5z}:\Pטߔk!h+9:je܍x| $` QqtZrW+ {Dw*^ow-O=48\`i ]HHXxwu-钦&钱>0=_{ JqJҷRv +AC*^e<8kcRR'}}=7RS,'eZa8/v4@ѭmZ!ac;'K3؟t{쵮%QdO0| D Cۅ//T1*WG`aSܬRjhۣiۄ Ds2lӴ8&6)*"rkk*kK@؄0-+񽖟xM&|pm~$ݪiʫNK;ӯo ?]4^ki`>ӫ^§!^(S=`g$hΑKL?4 -߫ gl9.%%)#%;İPDe5 -_JGLb[m4L]ЇC9zw/e:n>XlOy5# p`;ňh, +o|}iȸ!տwX 痁9Ksn-I>2:Y14dֱPgW!Е]$(AdڧNͱ%.0VU;uS%&^Pb;Ѳ*}3~ 3\pKm/„~6d}/&m"aTŷuWU[+MRԭty\2r(9:Jxad93,*b'n/ω[Fc j|{Q=wo@/&d)--AG,vS֤b]RxsGXiA v&9ELsI6>!ǘGJZ.zs;E˽݇L9P勝ҧ RU|i]߶@/6ob]ZlH#E\hGjfF?{CLvP骐ۦC09K u]!6_lb\J1+=-೥6a4MjĔS5+%g˛m ٪ڂ$$MQ׏u˸+9E|kxwFw-tVݱ-)F䫥v~ !nsD<_E94 l3$65;'AtV+ߎ;r+ÄwCP~o &zs<"[F26Ԗxi'纩9Pzu6x9Aiȗn]+[}UO{!/ewR tZv.B >P#.]*FGC%]+7Ec =I^x%%/u?Ob_wy7=*6db.!޹%%Y@.U<%}ۍJ: 1}uf"ͣ#<"C{qt0N?dRL]U;駕E݁Vfi.ŏbuj{ւ=%[!~,ksI7T;g ⧫qwW;38d^]H̻dǦ~^=s1:i%}~8"o䱷En56ihƮ˖ix\s~37ȭy<*T 80Ttۧ'd퍡",ķ)5PǁH()yu4qBث9b걊^aCk '.wT^Xuok@ߚ>ssWO?_xv0kb1O@_mZE$ֶ^0W<1^F)FKD5u宲ۛBܛ.r/a;ĄYx8KH>l E~*֯#f>[x!&gk0iG*tB5|ȩwE Luy>Tb(= uH>]bկO 9IW-Rd{5_94jJЙ6]Ȁ0 C#`IO?l K\} esOc=hcC4HTGͅ|6{/M)Ä +o+2o}(C >CΔ5R{yB}Eɪ+8ެ~#]շ89-9.x^Z/*,D-w8`uM)?-g\:0=ce8;#WvOT0KA%"lA` y>#>G6.%-?^|_r&lStͅnT=z'$tn?2+ qA`C.@kI'j&#瀔@_K<3Y3e"| +JSJL2x.exfBE7"T$J_ckcjODNٙ$7}ZZu# 9;ǩOR.he:r+[<֦C#VMͱg_o[5)>(({&ypTucRSxaj)kNXj/&n =PEe U,A>B49PKVl("g %^BҏN9VSKvH 5)/u0\' aNC#p#Ԭ2,%}{6z3EM[ܝ$5()Id )P;/t*6tBϷ)O wQ_hp_b!,"dh o 躶6O9߭ /1E.ug_1ɩed-}Ľҷ@X5/LL[Okl]OW +Z>mlZ@|5'j)E+ckn=%BٚF +/wlmouբ}1#5 ${wƥ^ yI nmyppѭ{ l\GCbsJs50DϷ\5flY! $ +6=MS+Uz1K*s*40 Q}:b9hG nG(%gZѣd =;hkg864qW.ГrGsՅ7XL02j} ~6_ǶVasH;>%L<Ѽ2}r6R<7!~ 6mkwu1e~ui!-11ZlCZ!#҇;_7Mveu. `Y#=ťX'+B3x14ʛ ý*ү"E&|ܗRV,֮u\uKFnci~= %ե嶢 6xMi3*z顉uj9N9&:Q<8Oր+vi |m3IͱIbL `cmSUM}N@ם=%{#qJC_M+"b䖜g u\6c}(4ͮ2! }d5 ^e-u +얡IY>5*cCͩJe䵩?+Snob]}is~mVq-W~-ʡC8dTQOMu{zg/Vh/l3Y%MQ+~}TP;<[‵'?omxv hWBp)nUC+we:l\iv'aIybaR>إyqaO36p&FM + ۝(y!"Ot# >>{c "V ,we=DfC#Qъs-gv"RmleŬnIjZ/BK/3o+'J|B`W܌]gӶI jenu7-;k PE7O'h.e }kN|@?g+C'b#-BT[^pX YNmkW `5IٙDM2-S \uC?g쓈hck>aHYdZrxYrgڭWFYBj +~ubFB]S$2n6WcGm;๖ѱ۶)HT34CI6 qΊG mPsW,>XPjg*ҫ\,1 km)>έ}WѰGZb= +zT!-X}wDNcNW4RäYʃ?]@J;66fe«)|OE-0Ru6pO h{KD^ElW*CRBީQ76h듄XQ@ϳBhU?2|::t^yCP{м h9&V/U\CSÁ4DI\?lO5N]lK42xcuhÂ.5ڎ5k,KuK|K|ZOmG&.hpzly<*r_1itg좪Wn1PG=6Ǡ3ic/Y O< ph&m[(3N !'"֭moۜm} toŽ86yC"b\殨-jÙ%}12AcM={Y/l;} ʥA9DS0%Xu䕽ǧ*3lt+U[c,}⊰yze`f⦈xWV{bVUh@ECV]7ӷ!H:6!&إȈkm7mp 2te7iǚM~oHYהq:Ov8E'䅺"DfK]Ʃ ?~@E\.}x1 Cc̟}j>d_}#m!,vV ܥok1 aMwcrO$4mz4DjZsN@#mS8>lO;[푥}_~%glŎ;P~H-7;^ml5˸[b>,W,|?3B߸rf ) l͆,]TWI`n6٭.9ȭIl_O.X:zՉ<_`c{ё] ೌݠc7pMGO,?Yםo +NL,OGQy:<ֻfiQ3bxjѭar7G(2P2z9~_LJradIo8]9#̤;ʠC ,$nJ!aW.y=*ku +lLyW? +`qq\C5]1=]R~O'` ⟭<"3X\Xma̓6<ԯaވO:260rj£ĥ BZ*nhWZ}Ocz־]xsN1G+OVu0#&w/,-%R_eu{oU>e#>Sޟ$:erb~{ B.4=z4;.AG gݞi~!Fi}`?0v"(yѪg{fʽ3y۫d.p6)),Ɲڨk>6A:|MTb{8kh٩ynU31WOUF@Jp(9rGǂeC=0>%)H*D>iM=Ցޢ +(U-lعc"-.SwD,~/,ylbk|>[h[ybjlY`:"AF`!>fMퟖj "@>o4oKm*7Sחxؐv{}|oT%l8u[2pd ^N=8@.WҘq^Ç?Zl}*d(Oφ:60]jj:2M|l#xgpI'*Jᅱp +gᛥ~^r 'T8ѐ +?ӱNpѿX` ,u!E$\qN%%HO+쓣blW~%4}lhFX$b ` +_ Yk:YН +r:Ћe^%@vRQ<]-}v[4cWb#G:as lTM' ӅĥA3 'LẀ9U,ЎV1sj9^ UKRTcp<π8SJ]c]\[}_CȴNW.Wį?o4u k?N# . hGF\k8H|GLCK fyR h7> 攕94=^47:ԭ o,RJyDgk"PQ5<EŸgoO s;[͍[bvZƾOl86~ړ",kJ߃ɯcN蕁zΡ.˭hrNZ= -lKh ?7k _K*pȐoI 4@B +[ BZS۟ScrN-l o"Xx+O"ȅzƱ>[vne`oo < +.GEn0 +G ͍gKFJOI+{lN)=3Q!6jk |i?gnAn*ګD'OUڛy5Zk*RWKJ*"K9XihXrSN.ٝƙǫ^Ya/9t;? ޙa! >Sb-d|_\L"fn2NWYVjjET{ |٬;_45;BDf]SC))E>=9ߥ?"p c]lQpN.l!3G:Z} i'lz>Yq=u{L<Vl"i 4f~7?N&ǧ5=7 +>aS թr[/fm S,brhkfk**F~ s+y6* \=6r+DU]ꬸ }b$V|j0@Q\DEt+5A2ZƲoZ'rnڦ{$ȭT>=p[:]!9faYL‰O>h 5K)'K]Wq.d)`oXg#bJW%-55KɀP6z0+r<ѩ@fcmcsvERLZC +PO->+jB_C)=1ֱ@e +_ǩ 7*TW2/ʯ|<Toii@ĺԘ" {&h<Ǟժf{LT K=l#EF8~ܨ68KM#M]_xc.ߧ|99K:7ѱ_]=RvxUժaA6D$#5-2aG \}A>|;PcwxX}}X GM,8ꪋuڿ3MEZ=կig@+_ _VNTl"^O\Jاj)r gЀ,',U\*RGEdShutk%@ ԍR %3GL*);ds=eXKm`V4$ƩC䔬9>i{ *ƽu+ywFA xGIyuhGoT=Y ZM^i6cgfqu 4yreWUP3\*J$6ܡ.+L@֩ǎhyڧ!f:?3,0 OhU!(;?{ƹ >L͜3sO>w{۲e˶d+)"ER9$r,Y93'DJ{S_̬֝{fT  EYjo_h42D<{pkU"tՒu3ffu+g<ݽc79g{SJLTo7F/;,Bk")?b`'FRPua{3wI}pd{v=-Ϩ-'?y,:Ԕb|^0OZ6uOK!Eف WƪXy]q0\R3'nҰMHu3T}-ׄˆ 7QOإ)H"NHU ?@gee=19cVcׯϊ1y\u/IDi#j׭O^s I$m;jA@'. Lz¨ݴ>I[^ܜa‡ Q25+x:sc:By'fYʵRvOO6sTG)~y{s}m3WjyIͺ +fwNH9$ZeoLP |z ^9i?5vQμ;#>읡Df%8ωo}TMpSW:%6?d%oc* fyfKMoؼ4+_y o%ЃPƕwYN_/%i^;ҙIF{WZz)bWi +Ͳ+k1 ~xUpʇ6MZ6Ǚ+oIga v83ꛥ?ND]7qFlbJu?t&ad5fkoH&}gD"`.mysT֋H [25ځk4Wl +%WG(InmpH'=tQK>MQG:!z>5) v%Yv Z^1amĮXa-qa!s[˪:ISK+/[mkRzonL'D%lѷF̈́q tk!tW' y<7"mZ6FԼqC/ێR44ҧ}< \݊}qtKE|in- [Bsj8V xaFFMbVeptue{+o9a1Q6Q/{k8񧈆X.ԝ[SɗaԫX \>)*0E- Lq*GH?݅A:ɼ(!mVتnL lt? ۯ#+o}Ӳ.40n [ƹO~Ye } +:jDn[{Ӟ$^ eAܧeUt +~ԦTM0ȗu$110;:jeXˬx{c8uwd]q)D®QDD`_Cb‹C=!&Ou]BV xgJ*FlwF">3lV\,/ܿ72^&߂YY5bR6&i%~=zms^PM.Zplh) !N/<  +Rf6&?rR{C&ψ[8plL9z< u5J>{|ߗ8v~!l|3A5ִK0/f[' VFڏE &L?u# z+f];5 z @!8e¤MD^ -lvYۯ[O |I'xe9u!кɖca|\ǪDM|79)rHq!%\ u }3̈V܁D `vPͩ9uߎZ%0aɗobF`<%^4z{L԰9Lw-b 7LȍΰSuJ8qTֱcVRi@S{"0Syh}P j%7nU'{^D˹~ ZOH#h?h =zkfo)UZԺ6J<[1_NL."bjz_}h9:1s2⢀eYx+)8՘U++'Y1[=7;}/q LFʸABޜb5r_ h{y e*vg˵)Gw/bSu߆Yq^6n}~%wƭJճ[rDkK UHF]ʾ(`3gFf%\TǕձ+%WBrKy=ϚSGSsYVYmKٮ _}_oLLϨ ꥤA0z~[9݊ou/AsBBHǭj>|Pz0iOHeR<6rRX?1;&:9iRpwNb?3sCzf3Ǣ1^IoDQĂ7KV1-ؓru+7FmR}h-އ]*H9!'鹁옍ӚpY3h׷&y-u)ck*Ve@˨] 35ҕg_}x($zDŽ|ӻ}O&!#1/1w[EԄcפC9Ғr6[OlM΅usi7>!mkr'Ndb1`wR*C.%Ş~:-=SװCgmC?C$йwP矵ݚ]ڥQRk;y))eA#ga?}XRnW'M挭˙Y5㺃/p'Ӕx%ce7R"Dm"* +/#f/"⤍Q=)CAG)/ۏj:e>xY3 ڱ&;~^X>xu}k_Uw3ϼ95|V(6 zs_(jc_1HYq'GG^?>~>n]15-mq:QWTˬo==;P$b:vmXE%毾ÝkutKp rcBX:*XzN8 Q?N>]xXCL)K9mA $<_STG(OڛSVEb a9/Z"*raBO+nYşjx-︕֯|ux{ҵ UTH1Z.1SN>-'UČ +&a_ͪ#Yx\s~;-Aj*h3󊾤YLB 9?ctĜKZy>-ԯIPW܄\M.ϺYiEU욨^Ws*qGdZ?߽v 'BJխLym+cG]O +=0 +2VNK."E Oho^rjV9<27avԌ)EHD-N×˃Ыwr|)a}ܠƍ1u[,J{ܔG@] 8mWcQQ+aO+Ϻ'0W<ƏI1B9\A-.(ļoۦ#Q.@߀/2+AMh~ušѶca=.bT7GINC\+"5gF5zvr 'Hw*EI+>de]퉶K_=ĭ2!p%#Fg8O&m=}[3ה H!RL~?xZ7tSA-*d`4urt%jfT#&N#҆xvn{LΧd+*ڵS +9qu=*"*x[6h`+Sfvbd7;Fkr6ƺKxzo@l4䲤Mh>iq;3žF֐R A+ʲs \MoKIHx{%b0}sr7˸ގ8 ˖1 nOĝ; F╸xӏq'S=*s\⿯yΤ^>~jEOivutV] /;_tQvp&aWP'j8u)+*m<Vm?e]$- +buGmo7 cDiku)bfLK*Z~]^ O*a8l#N1z+FO&IyYA\o9$QiN9-(_\\dV|ÜTn>9"v]XoHZWkE+jEtN5Xoq9 cc[~K9[P)ȋcsoH9IZ`ɧm^,q ! 4QKrZYQ~ɧ%'݂S!Glb)X N^ouۦ4>Ҟ*}U&Tb G^kxk>)s1 +jSъZz9b3j$J$-Yˏ1eiχ9.%$/εΜ1%v zIBuЪcK,b‚>X("3aGo5?!FnEÒXX8-w +BڎGov雿3vCdʇEpgY׼/kAB-ÇQ#N'>jOװ4̲{zkUvY;^>4u\wa٥1Z2VnkĮOYHEb<1떇Ik5A"rv$t\׷Z-%?mTp/%yH1^aEx;}iS4K]Ӓ_\WK}cmAŮM1 +ޑάwp;E) U5UWFpDrRko"*VE$cr_ϩA,Rz1p;1;G-:"\LbO9x]i'Sn4̎WX^q?H{N(^;abo64=w[?Oy|w+k=qtFێMuH>58>'bV15afEtҥ7M_:w,Un3vm=z-4!:ak m]9YLu ijAH'hG,"R%'C&a[M+{ +3Z.>|E)'(I)zN1;jeI }y^/us[ ^UK={\dkڻ#c'bWE;; +2vs2E<\DߙoX'Mi91vn+GqaU]4/#c?=,?H>b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od.?H$~w`蓃yNWu'el9ϧt),fWr OOA?py:PΥ(L:гc~Aɑ_rNt_>y_9/GO9~LΉc:өSGO:q'бߝAH"\XrG>HNαc-'8~؁cGN ;|O>9x)b{s>iEf$G2JPůpj}VcjL9up-"F ʨۊ%쐎Ӵ1EҮfXeq5IhQƌjEQ=}+ƴq}YX1LϐDGQwMŔAS?+jM0ms4Qۓ&9gx"fU]6p 7QKP;6"^ +ڤ츁UKzDnuvw'-BLk%̤SʋY[c1$,a2.<##o u(8 :FZO%4ZT'!&MHu^Y{rbUwɎ\ȡ2jVEDíG"bʪ&ʞW)[$t̆M@9ittꈆVrjS03jFj~Xk84*3e)2>{>N9A-|Ī\5J:W"YEM5g "D|b@GHy$̼v%N>1a瓒^W@.iW&J bbjw!6v[i h#FF%fm& +Y"}u$Br*-5jmG~Ny{^7Q(Qw2hS}:~)A5/zV )i7MXM ;#PxNDêBL"J'r83M8]1 fRIHC.ؚ&oU|H 6qPp7AZr'l꘎T0k@zܠU6A<6Eɏ؄1 219q3#!|Vв0¿ ހow5 +|~hUA-fUJ":Q(ﷆqB3 54uF +oz+nvfsW^X巈|-iPXt"c&PH(qlʤZqZ6n[)fdM·bu,&a#P)7U|NF,$曖iex9\ '/~ dߑɸiܙ'_yUUBGБQ&c#V)M8q[U"F*B-Ҟޛi?a~6k#jvU\ŮNM)+!mg5,ꘚא48 *%_K۠,BB!'mF.$qԉpX|-'<"V!f$α^;mNʻɅI\S\2qmKZqrI,MaKۅw چD8`NȬIUi&dE [S31|u|pHAt[C&qg$PoOnAC65jELҰKcS\:!1TvK7e7SQ456\)cV)S휎馟6؜n<Sy4(`7EæW/>j:]K;%W*L8x]Hgrq%a6~-osq5h橁RL +n63WU1\]Ho|$'YDޘM+1a fp9+ZoPfsm^ wd5bh\XU1ǵ,h/[X}Gы:vDƓ1 $d!FV #e￙t#f93n$ Jam䲘ߑ0+E!?%nDRbBko=zp7Fב19)0-lm^ \@T­îG%Ͽlx+ϣ(fF&V_5츜2SJSKRM*A-ueY\㕈Vf]uYV<ך0۳V)MLukv\Ē00Y46iLzZrIpq^oKK̎[ĸE΋^YYFτ NޛJuR~-ȅ>葛.,(o}wXYr3\qɯk<׵b/° Xm)Sv2`tѺ"jVS~iw3jBzNS[9q#+P\. H{^=pPRjeJ9XY +G_$-siq+>fe;Z3da_H)C)jB&\òoV +{cҕ7]Ki+1mפiJ墤G*xq:a[c"ͭ t2Cj|>jd4dm: blY\H@815&e%DaZbu"`17/h7"ot.0I͏jYUq[g_4`t^Z-i`UH@] &`z}w !mGnDOʏie ST} +$~?yv]aH/[>;`3Fx/3goei8)kzs{+Su_~Shws~5ZĈMc^S&rI G. ͜F"XŔ eF+ϸE [9t(xۉTeP+1娑Hqjsҋ;Dz"ԣ腾J8j+,Ȕ; IzNOBR7 @W6V}ǧ]Nڥ"I}7PQӰ^23*NnG+d%^iQzlGIύ7Q\4Ap)6/SMJ[_ R&^kFk[ϮT}:Vsp {eZaA:dD,XCFGso[/K%.Z)pT3úS˯+XysT#!v1kct>eՆu9[9K/ێX~ytTb3\pQ$ Mu Rf)ɮ3q4%\hIҊ:ga/WŭI᳝_lY{kDL4B,JHE)͒VO؟>F=&!)cǥ/縆ױ㒊f2&97cQS6 3dMfbμgb5,sZЁΞE +ܣn͉ũe6O8{y`3ZG0N2[@:MP%vr%6 |#|Bk|q*iWN򀽮 ntx`Iˣdb bc!vf+v$BRnç\]":s۳Bnz^>XJ{%$hňVR >)/l_al0">RJ)owo#..agI*%=헙HQ<摺{N$ z6^ֳ0o:}J xkX)W!bjVv+3Z'=4X8  ̚~-?E)-I +:׵RǓkoP_2#oRܴFT>h;z4t]á1R!-dm(0i蕅U]5NLJBTì@w'wk)&DC-B@'|Bmm1tۓI:5!!/8x$m=6 +Rf$m OPE/jMEHIejɨh5Y!M1?#N;ԡ:V% +ӫ6.7F ":j0 4]HL~N +XhecܘQBOy~C|bP +6nRSb1:'CmҒ$I"jh<5F⸓^΃5YTgCfڭŮYb&kyik6F.u#d VNKH- g X =p7P5a'.qtD_uݙB$J tQ#2fⵡnOk:/c?]zMFbZ{rgu4w96 u_zǦgd +V M՗Yc%G*LؤLlU/#Euۙ9ݍ:Y-S++#*l03x1!k~U$DV(AJFJ1bS&, +'q-)SMۚ&151Ӥ+i^=*iYI4bjimqB^$c4bl(>rtu^2+DIs_ mL#jGraו"*ŕem:"!ZQH/o[O&bj$&Fኯ!4_ͩIY$ԸF-lK.EgjN zo i+ׇ;Υrqơo-B|"`ƌ소 iCt"B'\xTLM/CZi΀ٞ^ /̘$]/M!`?v-65< _ Jof)3ae3d"6f.fcfݑvz*AAMw^꽵;IE̤]ʂ^ڝWL:{LN<*">7F Ѓ +ԩ%uP8.bbnq1pouFԺ>żXy;s.e@zNȊ9@ػ1ۯh h2)d<".݊C!He3Ivx+!=. ,n T0?E,S.fE`OR}wkmRY)^F̬ a=$6t@D'lͲ2Aژ 7B_G}!ݑS*Ӕme햡 78?m/UrjQsʦ2:8M^ٍiHz5<î{ 5MҪe`(6m71 y%!Q +)w68vß٘`-=:baTGTv'M֥5,> g,=zmOi|~4î [%إf䐵*i7}{O)µQعx:㣆^yP +3Zm%Io dPۭ(!@֑&AWLE5w 2j.5擳 ÊP8iAAĪC+DYoM 0~Ay;S#Lۢnn\'!n!V[;[ǚ +%]!ԉH7K1]?^r[Fe'4%!G~#5dpw 7x%{|B"Br(gEU)M:zP{O_ݷ(e}iQ r5܈g = 0& Z ޞ]~v˄]EwI?=@Fݷc{ GG*~g] +ٮ[iY5. N=[n褤zt.ĭbVpzK.q+Z椠NczT 뽋c54W9VDLQ?Y|^ڋᎼG zu:o/D|2-cU3q#4ncXm +%bImkis1aQzfĸY>@k808í4*hevm۸pd}ukx%b`VNuh(`F@# +르H1[;Ÿ*$ԜZz%+AKOoˊAvE .l qj)<1&9tHyb\1.2 !ǵ +&jAvA1B?61syY:y!msAP!5iE 7qp!7 +zfE&BJ>݁sEd#As +z +z |9vfc3W&#CM~My{n$=E/+f-|B)"= R.) +dU}(.3-dVܙ}Za-2u"f 5bf"N +ғ Vӂ7na5ve0Vm8[vݺ^OlWFbf4IQ3FȺQr 31p4!Y疧rA\LQxnM.UI;\(N< +Es#z?@<"uIxh"x'%+#fAG]L{ł%grVģeIB2*FZ/ŴF#b S-G#F )fUlO @gYk/'j~C8-9- BӜN üXh@QM)*NuHî +L/E#jn](eb~sƃON&ԠmR.vohG~۝(VMT]w Y;bk(xI[ߝG7J13)؟cwMص*ܫH@LG9v|UċSHPcab{oQ{oiG V 1)bfTY,x:ptûv(!neZVEv~{Mx2[~Sb(q.;FbQ-$d=&JIvB}%t!3?I·-茣PQN{ W izAv}8_B8g~eOf3mFtFi|=P.쉖C9g9)u rԃv0Iu#0i_磊s^|SM?UbCz­C6%71FctɧOr:TEO t$ȇ]S0%pBg%l QLgPה8s_B#$@K8H@#vW҇COTL䑜w0F9`"3G2ȹ }?1Lĝ0RʀMp jNg'SIϾ w$AOUB8, 7uk#9G +bj1b @|#q,K4 :=ybcMR DD;\ D*IF%ȴ7)lޙۛ/ߓ?RWOE8W'5Q)>R甞?7~bbNĘC6 5M*0aн !DԴg;RT؁{dc_IuW vU<^%dy&XEi꜒mzz0IK:Rc ㄰A0`Z8ˡO8TȁNl*d&l& i-X+L1Q?D T0ٿsM?_[\J*#Gb5PߟQt2#H?01#qsug95@\k. h}i48s@w&?@o1dx:wg&q!i2:4M 5nyoHjTAMڗ]Kmt=)ncЖ +s+]/nH R؉`V$v?HE@x^WmuJk8vt}p(ԣӷ:#_:c>N\⁌M1xbt#,Z)87p=3yI&0FK4Iel )hgzʯj/- kM>"'Ё{㤈C3ȅCCe |d{7$}$+?LB艜G=2J~՟h2/㏔,́x$h)fy_]Y t22Lk?u>%8U8Ld=8ri?\?s,?sLa"}k8jk?J7 ۑpǓgSepw"7Ci%s64 ;Y5{*9(&:ťdԘw6#O?R }8K?*;ԖIf/} ?X/w1PEJ8RRODeKH(18H[fNf[fҳ\H53|P#;98|I0e:>T#gB>R1y>>H%|V Qrv>#U_au02Εy?~3oN[nG8(qtORӹF)~m6%>bqrg$;ăoТ Sl `wQ.g*B/ F)eGceV k}PlSg{2.O G +6v_Bm\Al4陦5Vs'[`NT 21@ 9Q02Rb=i#qK5/ϼ09K't2*t嬛vT $ 4+TZp.'o}]L9*ԍAN;h_3Y? +ఊzdM뽪=/<ӈ'&mM ݙ9 -+Fey0I?P1QJ6lA? l6Дb =&<7; x&g{R@Y, # +[@ES}UV&x:ׄR^Wp93L!zOIߕiSXQRnlf۟K9ͧ|)x=Ӛ(c c@zNS5q>&g? <ה{RR0$#ŴH#hNfsjOfxtK8N\<gg˸]9 pB;3sϧ2ϵ陿|o >8Yt6l^Q=kyLQŝOgM7 +ΕYǓ"iԿt8I !X)6ʊjČ\#:tR~)+Nn5D +^ hkф3,)b拏bƈZ(jra D6D>_}=ŧ>[jH.8ʭ:xʫ8Q8`)%ꑊx +0Ytw}ܖ0QB^hW5*,kPp[RZ$+uc [F 艒9V3\)7Hk`G$S 78 h Xg[;ʯ;ɩQ}f4הp>{4&Z`XL{>++˨*l>*dLZ>kGMFW3OǨ+=y~ôvy>U~xEgN\x QU*?+4Ne[2XD&'m|~O_PWrFQNދxii>͏=i\pnZ7̈ĦedBa78B@9TH'pIc4;Fږ38!&/ jy@ƀNT`|2DSv6QsؕbM=@MeTfӍo'P#>(!O5E*S~w4ӡK6o9a's5 issv"]9t>X=Sg]dq瞂sILʹ&?=Of8Rv H2E6TĞj8ԟEŌӹ#Y?2~-e <(%qΑ&tO%d* L he:Du +|׾*3נ-3Kz5G̒}aMx<â$ڮ ]etGYtwpܳ@iMFd@W}4̈ Jԯ)6ԗsΫ/6AQ":mqr 8jJN4f.@؛ ) "~iIfʑ<,i]lN(879ws,OgmX+c0J%Og؄?\֮ZzCNYeFEf=nX!l]İ-I<⮄Ì8:蘓I6zgw IRcz:Uz4UbZ<ͩ?1?d<%r;R;bA?6/UB9y\{h.a}$VNkgցXhH.~R}36Y׏ IMC)5zoM6WCV[;ÌnR=cZڂEجARbjrag74t}$NFM<GPn{/۽Q.G 5zP3 a XqZacI]Da|?9н!l ~m-vig;6LkWIrLOYa21ϔB桌8S3Q&6qqb!g +c8qQƱtQ 3l+ew3=HH!8%ԈAJ \lt"6;zTz9'\qd&p.km({C.lr0E%Fyof 6O*>}h)/c >#wS4}`8ȦIZ>h7AGgN\sC>TPOY)rOFYj#V>-ԑJkڗ3g~%h B5DNmù @>I' +. +(3ޤvE$DxW]hb 뽼^Z([}Hǭ^'f7T /@kX; k-OHۍ6j3#`DҔ_M,[ ߅6ZX^ÍQŒ':YiA:buf/_]l#ˇ>Xj6)!q4U?"B$Tw܄ކs>0NtO ݍ +}ڔd x`#r!`0r\ VLiMx 0M>a&ۃ鸍~VtU4b6&ZTL=>Թ0%x ?Vp0{[ӚҙZ9Rg|mi;z~Ԟl8NXl~EFh!^юv\kA~O}\b衅o +xmpo%X׋4#yK=/yQOf~ӵ]RR-o?fqN:2zm'8F76GtMUbщZjxZ@F3jiw +j/!L{M@},{*{ۇ 0a$gQG9+mԗ"S8ڼxu$.])(!oz< }U^X[j{v]gFA~lMG~HC-PDž#)5a{aÇ+(VԗZ6Ba`cw uoˍ[MiKyn˥zby +yye@rDٮ7㝷>[W2"Dƴӵ3n niS/7"AM>[j&8/5g_6D=ה_nLv+옽>B?Ȍ[n!4JC9[lmI ֚mT}/T@z͕4rŔK +-[(ԵG +ZkC~5 f(m5WCq|*Y,ԦZ:>{vw/ՐwX֧@! n|n,tIkmym9b-&V'0Ȉ[0_+}.>[m&E 7ut貝!jn.LUdQnUaF3}&r)8ٸ00ꎦ(Ri/=ݙȆ><'ov`]tDO0oKok]1nK&ۆ0nV'OwRđ҆Jt8Nh{mw3Sc[}\|`3i bDtLIc؜s՝a#v#Vܫ;Ko%Sw<̖NH^KNz;j*yڌ@;vu=I]-}))jgڒh9WHYl֞0YhG>V +ۃԈզd@V?֗Y!fhF8兦(b +AUlot#_&cg7ݯ"^0 $j'Mqҳ՚ ?b,kS="a$WŊSQeV焛ja擅HZ,rCjSRCbL}W]rXrRvzٰnZj;ٗ%' BA}"VQr3kJYPYXWoi ҊrXvfJ,$ |0Si=;Q{#.WK&GۃZyֈum\o:4 O2^NÙɑ[ ܘcAwU8_3v@m=&sЂ66NWI}]ktcYaz~f>~/֧W=Dޒd=Dzˉúq6(up +>cjށ繿̈́d0UDzXNj#R^{'4׿nO! kqnNE>1w@$G"'~zaLHP+_ U1Ojm pIA‡Xicebtҽ/rk뵖WWZt9s j'm.j(\^xw4=xvbWV1SI׻AWFD1>[zA0O.JD[ x{yAp3Ifn!QZrȓWZG@r\UG֦ /+2}ojl|%oJ=z j1[ZPm-}_rRMucJ}V ?j.J"oVS?6-t6j+RSbC"D^xvrDrFڪaR+i~AqJϕfͥ&JGY}2m 9@P=ZGڗy+|5FM>xw 0`!zr}b-dۙwk-g^D0]?%dpiP|O};[v,7&4Wq*CXMB+KR^6ֺYQ&5|k*[MݎwW|ZrH}g0qFUڧ!aKi^b#-h27buc=$5S$=.|H|AV"0П%p H@k"?u[E,5=5C !h 5W|kޝ@YJ ZoΙD(K_D?=O[4^Vcs3ut_iqCYq44b q427J;*l4/dNЗQ_ U) ϙkO) gr1w=lu|5賴wCUll +x~Z=ȯ/v= + /uwkzV?1{,;ݱ\@>sDɼHsUqE .4PV(Չ \"lt\' +^$zͷ`7h;4dk'ߣ)F:V;nXlH6Z/Xh(q{Qu8?'nwQOd鬃,z&5 Q.fZ%m ~:|P8\j#_<&g@ R~Ɏ*[ʳc #t3͔WZ32,/,+XFmyvݵǭ~FDU^mw^lŻj aBی6*;`n,+,7J 3TY9=Z.MxVFaC s2Ҿ_z.ůϻG +v{԰nf4V+'j!;]dxXl|'k<=l +8 xPR-.H&3#ĠnBu؋ɬoWg*c^ @'L&XlLC Boq!}QCq3QD]`1@ɅigG ;݌p[hHw{ZQIZZy{luT+yAcEUv]iZ[wZl"zo̶su ߃:L6Xq!>b| r +Uy"xE\;!Bx&Z M߇ 4PF 9r+k;l0I"x.7>dG# +}-5x'|Ht6BE!T@W$ۂvsGVojWJB0'!sweF h'Vr;O7(B֛X ynCuG2>ngIWUQw3tֶf;Os\h߫dy>) _\PҤ75H{mݯ ٦t]jLYo#{-`a[ qEQkMЕa +#ȧuX.t!+\oK'HJ_ː+g=XjF}d' k#K9uvn<T`h`*ŘVľZkq[fIxJ*=)3ZTdJE|Ί9~w<$HtlC=|#ڴOz #xПvKf6gJ@9mQALu~aßuH[^V8M3~8,GċnCxs +@zzg&|[|sڧ.j%˛:z$WÊєTӽnucjQtCb֠AVC?.'_hL],yX7}7>xАy(#мw7胂ga09\Eotӂ竑 _~SuDcuzz7)|A]^j%zm6aq>Ze/Jtk0K7򲖇x6TNjE!t"=, yo~I[0Uj1慪2쩼$JcS5L0rw$L㧬B>:RQ.森 N.#-'[tC"Ƙ/}|f'Å q~jr+mƏAKf@lĭ Li.l5w'i%?ʁ$muGcNBm<*彾?؀s,|xˉz6,~R`g% Po`7%3ENih=n9vM?g S"/9}Ţzn$# { $;̀{7REu'?D@OXw_oI-LJ<"vjK +. 6=usy/Ѐ_;CBj7%p:HMpP[ʂIq@:v1ۀvZjX)w /曐_p^b0JUd-I['$l0D>[b99FyVZ7!pYY{ue{ R}in"*$3J=z;>z{X@Oxɠޓv)u)v#<[BGw;LyΈ/o zbk+VTb[3u?S/V7?uTώo"/UDf;i\q,Ġ̯X3p&wyݑ ؇\äXm#R]w¢v~,<X߁ \Fc7IJ@7Bɪޮ WCy̥6QH'} 1rL@c-kRotnyi@~\fymg:2Rje-yP?dĻ_lܞȠڬԤGC'NTFߑOH*}Aq*,kA ˿C-Yw*ڹ1UY"#=Bט\Ȍ%3kĥ&vf_L׍ry[l4jH@[!i,!iKخ 2qt cJcmc5S+t̴͖z;)zw2( u11CZ^nBnHTmEIk6k(׭6rN7ޔsg2t}k.a2pB$Q/ff }AAh:8*f|e`bVDe𫵘[^[y^o&mY[I'+ e{n3ao"#c{O14䯚 +L=vP7+GF>y{}mY2J w#4SOI\i&l(7u{8vS[3RmE`T?3UE[sTm%{Dvf!/)~@ʾGUU&Tnr&n+J *K@{žWĽX+3ʜ^b|"-9^{(-(A*~PF^1Zx'ĴYxpOa߅߅~WοQ#h|y`0m3 Y5T/m5eIemjD +r7$9iq#nTH\F=9ŒSV g$=#؏琿d'\:|wt@r'3 +%7"쬯΋H6G ę +0ۆcTu iso&z%drfF|VU׿_r\fׯ@,܀$|Sz~:ԍ5 1vceVF-^XNZ nK)Cb7qG{d?ExF$_NN~QSm!X+*g=I% IN}ye~ ț{w!݂\;A,- Nvʐ7%YO c-).o!:ɚblU1!8% +7. W@BmA!NB,Zؿy ~2 oo/&\>Hz)FoT|Bc%  jay~/ȧ!ϟ@>>2ܾ7r`!!`ž+M@m{.~BЙò-#b ̿7a^IZ[AlC^xܽqoA (B|kU 4|b'/ I \ʻb|ȠKho h7G)C3=ȭ+W!7yq:1x."=&J.S*җ,N>zp)obc~-[nBn_r8 f&~ݓɬl'@#)%8]D/ߋvw!o܆8=} +| +鍛{!f׮A.] ^'綤y%U9>AiF 4 +rБ7) &nYBA>=~q~˗!Oou*"y7p6 + ߿ <.CRT#W^11#&]e&.#.B~szލ[; ܅~m9V|{즪 ~ӿ5 kYH Dž/!AvvG!] yp*}G KxqiBmXANڮ\Q2!eKCu +/(wa">~XZ6|+߃ܹ>O O>X>zIpuk6ZH: \*U2|'kX4rfL%7gG w&%^<~|r1[-&|#H-3s}L|"?k[;͓k<7B,\|ry3ZQrhY֖ϰiڷgzD*u. ;+>~+˯CMV2;mN箎LdE\<>6}&>NY)RSoS"oCnpUqv#Hm5pa;L\.rTewXS!: z 72Ke#PAq<&7 O=?9] N"D>ob߫*讛٬!lk@ۓdd9| {`&$9:o !ܺv26iB,kϝb_\8)tjo:j= 9\DlcOI)if >!ÜG_ֵCmOB44i8㘙-bfKfffb&KYdHڞw|C0lii9{9>~}?{>=эw?*L(e]蠺VXqQKjlK01tj*fNИoqe_KJ~=q>|q{ꊊ>%5]'̱na'-j:Ōejfz0I^ĉGXO_<]ҿ %9g%|zڕ|ٕf~YbMX }*b3^mzb&ЇTwwd0M'_ϻ?|QuZ_k>{cXeȖd,b!16! `{H۫h9M3~6dύ1 NW*9\]l)L~X5t9LM9\ޖ|/X=JKю1sVxKqybi9!eF%Lp/ꑒkBz/Xv{\T@=1IZ$2ӤoP-pSSe^O8|T ze_ЇI:ҾR2QariAe;}{Rhå.0jW`^6Y(ﯖ fysDOaOpZO٘J:.)mkq# +7KJX[ﶅ\pr=uqh|ex4By9͘s(iIN[.C;ߧj{G=鰱k-ѱ/YC[i--'}Cl֋Nޫ!NKp}h]五Sft [V"[A'~o«m2r>S x*g5v4lLth:d`}0 t|>ܞcZ7-&ŏЃex!9%h ˽y|]e)geť:/S1-Z';$}|ݟgLL!'yd*Cؓ6ma^`WhV:*[cWQ+\".4I2\CĆ[& OK]k!݈8:vUlQOӛ/"&"dkn_Ez@JA%6:rFk@vX{F.K Єd + 'vnH zإ5F5d2jSP;Y\CƆoQ-Ӽkb<.`n{q6:jj۸;C +٭{wDGީ Z2lKڐR^9Puv$\g3wW0i{+4ȾuI-ife_Lm|Z2PknS3OS1.)wy=S}|trOMۜ'5;NMJ(#=qb[#U}j:0?^Gt&4(jĝ̛s\HՎl|Ѱj*"Jݎu{&AGڇca]9撓EݥVIk+9UbuGJm jgo C5lρusȴ fWp(66)n^@NZQ94+,jI +[,B|"byTecѶg?zKW k݆E6EӔhR1ѭYLT(a=G .;OzM]5 N1-^kEtl3{+mI_7j7YLV OĆ\v@GĆSꗠlgM#ꁙtl6 &Jj_/t`37ŘxS@m.˵ +('FUX.f+80JB]ɱ n B6P~{qtc4TXjz\i}3 KEVc4 A ?Yw6ye[#;YnDGNrzd`=bZtk!as4B@:J.I3ya!U;+(کA`i%nj\nX9-ůpVK5zԝv)%zh-գN͈Wb*b]lE(x]n_V ˭৆n!- :Fl +RmtLg%80!=u{nȀi}17bh[;gbF)bUQT݃'9зO^lO5ݕJ56q, ~068O;xwhhv +WʂJR +(8Q>1.$ N븬 @ֹrTqH6I.0К%dR+ze4 &9V^%=!ts +%Pbxv\+m U7U|JLfe Uv5`HyzTXIQy{[@nĴlΑ<2`jN c=3|/O7 endstream endobj 33 0 obj <>stream +{ +¶h\BrH >{OlZG=RdN?AgW- +1Q5D9xƭ@5944.pK5z3|V&3=@;=Ud:$%>Xt0Hr'׋jEm/Js;zo Zҝ9pxÈ +n 4c_m;?xC Etʽ'ևel2*9dNK?!䭶269\5UVrA~dXX 8ܠ6C)2Z}}{ i&k{c^D:7ݒ}l$!2pc-¼Kxİ x?܍s8"4J;Զ;LjDo%3L[-q&Cu/ZHn@+86b~n iæ~Q@!*ఊS=Km ly5OypZSX/FnXwg!oF)gXyw| A^޶ޜpxF.g_d_ޙ9{fyrdwW@VͫKXwekiꪼl(A/5ĻF(U 44WHiH +HuupTMZsBDKD]藡JXfL RkN6> ؠo-wĖGd ]P`"TmioX'_dZ +} 6XEjiq}<6p{^<x \a5*) m/kcF&! {Ę y9_g%P|MhDF #%;ؼ)3=2T0)"xTer&"bDC,Hu~_[f=j 6Um).Q+rZk4+De6eUtOB'W;VYٶw! `Y!`˟L[{֎6A╡"|Gu~l[kZ0vC#CG `}4ӻ؞JVn61`*DKZ)~qmF(U!S{W'V]pԈUنƑ!-F&H%hxo%a`_k[q%!fW<<RtG5BLN 5lup!S}K˜Z?x1:ZsG<+a(+<=B:ȽG|M+h 1墎_'XHheX_|{s񉪿.98*1Ep/*E_[C:H3;,x~!(fabr +$ӱz5Yq(g%igV!(9(A7螰[{+3qA-xsfwfXDnpm0۶OLԱK %Dm +̃QGm\cG~Js8^wmGlkXAy B}4YI(]~5kz3ώꙘ_8J"ɽJw-A^4 +~ (@u]# z40B ʞHQ VK}"xD5/݉G)p}m ȇێ٦g^hο9WO]~92?.AfAZGcTR6[c onWuS}ӱȨ'gș5ǟd8f`'ZHۋϭ _sg WZ`fX'$ r{vLFB{VPrӿ3X+ozWYG+uDmڻu/rl i˃I3g~0 THmlzj[eżBBʧNUsr Rc©eBĮL>ѳac}!es2q(ug[zݙƯfΡ(pvo/Sҿo_ M6NV|SSY꺒/q Pk1|OLC7!1,チ>SQ-z0o&b:|ÑUJ.4?)h-"rB&hg +l_@xY5Tmz0}(ai~q<k}lnųH{{봺QȓEtiO>>s1MCҿCϡl쮽,Z~mH,9(8ґ6Nꗂ3m+M 椨 +XUuC.հGz:p`1H?U!)L̿,gVS_zD4*^^!@pB +xoឆ`ӽQ- 2/n-+Btoz7#շzYmu"@O9Nok]BRqXNmkz_PҳZ7+8C_,o4 'F|z[3yVDG5LKB;X4pTkaZ"D$2: [8=n5-n@_;EȂ]ul|j:%E:HcELwV\fZGoxWK:"M۞ԙC~q5Ȼ#=i3v Q~b$?ld&pcVё+"`9rQ9s\q.5|gVYfm m]'iKȹYG;kTP xCH@oŔr&۾H*9E0RQM#5?o_[uBmlv8~К티Ľ)s Ɣ8/>es8u +}T'+MK#fg5l) bXI>:r@Gzbllm?pd`*|eHDp3ZsU}ug D~y}agƵLr-saUlv` s8SWkx/MXaF%7k}W̓&"H6@%F՞Ds 4,c&W߳}r-z{H+;n>̻(`Qiis߷zW4sT{-␜kҀ cmirDO)_;V~p:ZR^\Hܛm|ѢJ="pзrʾֻ;Ko5=m$].d*)j+ kq ˠ~-!ӵ~c*ܯ"GeJO&.XkظZAOpM7?ɩ-E;rMз+4PN.S*&ҧĖt9)~mYQHEz(--&imz k(`~Ma3iuO?\eLwѯ?['|d'~NJ9s8є͵=-:+fCR|YXιZT[p>H`85x^ Ŧ^Wy!'tXoElf|d +`gT(^ЊJS9L%mP1 +Z{t4xLKGf}0N+'%@[Xtq%A%tluxaGkoHhk9l޲`:Zr.u%uWBw.7{0ݖ|>w(%oN_FnP>ӈ6lRGBx^l~bkN~0bjC +H63-<6S'5R + +*qG7Ÿ]GMP=t4|s3T}g\k_EDVH@Ƕ<S_AnGF L])qA߭S@O/#Jtky*<+T4:8z)]5OV)yg%?YomsJ2.,#.^OBqUMNuګOCLiS=(e qFV5B81KʟV)MbT|s@˿'wdmF}ۚ܀- ̷ɉgڇ+2dTG% m1q앢 +ij{?doֳ.Ip%+ uwÍ5d֗%pvLOlU_Qt~*1M<wLcb\ػ[־s>cDjZ28%6гxT*<:^y:\{GZ;gA֙k؆ٛizn4Ru2^v'n|a͊x,RTPN6yݿozbg%f_5:#f4aOB +h큺N"'Kʿ +CCi1.``dPU'zhcq0i(܇i`-BRI0rc蘒Δuֿm""jl80Ha"xsB[ "ާ5z+IM~95O'%TӃk#?W{8 r 2 +s,B6gf>Ѓsmsw:BuJGLk cr|2d6_3kxՠ ( +o5ub"9HixczmS,}4S;T!6UԵL*6=2tk|5\q[p1_~+0L֑KM&D'!U7ҶtX]cD#G wV\土?fo +5J|3H"D,BPIprG+=8<ָ>1)# +c7+7!< 1Bk4k#\g\ YTWt33'V&}J+L:.&5x%ݩi]5Z޵9XޝEfUhhEHTc\%n,^(QJfQ@ȅ' i~YpbP:5OxPBD\vTnj#w9yÍ(pc+]|fv_pѽH$s}5?7 !kxUB~3H(iz~1abzL[ID}qI:3l.۱+ *~"~9'F#9sb#3j` +tk1ޔ+"V@j:,y-xKmmLrq$(ѿB.]{؏8;ڱt2y@ +z k! SG[=5׆4USӰ66:Ƕ?7˼z|<::9Kܞ'׹/ܛ,`ҭ]kYXyR #N;VSu |·xH -MqA2]By +Ŋ9lSl)Y=+|5yq8Pt''wFͯBw 9 yJ*q-r wzTM JJc_"s~#HUV/Kڛ*.U~[8檟u(u6!,w@r,l{4;ĝ;smxG&6+eBZ.aOLҟ +="Ằ^l5݃AM;'wEMjD Х~4ǹҹ{ xZn['Ȕu^ L$fBk̀*:/ǖ8#fߛ'N\֩C?tSImҍa-Sjo묪_g!Y_Ji̲n@.6?  YN^~㪏5GJ,!o\蜃%D[+Y*@wmk氎21ѻƋ>ֱQ! wl ^o}D h%B6`V^KkgYdIDf_ݭ[$K܂F 9W< zvःEӀ~sj혌YC~%~OxZGIpŭ |Upy +uHERcA$nVh(awsҰ̮*TZW_WaJ\[+ꟄͼɘUʝ"\GFj;b~xV:6wk*ޒ w!9ƾg;-I y֔C!:o] 9crR}X:%gBm A^o/s`rrc r0bwtwe`;aĊ ۓ/=oL#2.mԿRW&h6`ڷW9ƻ. vb>1Gƶ""qO>;O`e-brLS4M}oik͊[ +q[+'֞c#۵ζ_;2~e,;Vy=8tkk,0' !_٥7YOp*n}('х|Wi5;">1ulaDI$yX2יu?uԽdUz1mOWug-?,mOmGj:̻ +9 2"hs%Qޙ}{3RA ,#n}㗂bZ?fR6m=tGL瀒K;R.4 MB}= N?+]E]K܋:X n><Cd[^m-w WpV#|JjkLoе!KTOCmi鰈c 8o%b;LXХBVϾezs +Z>+c|m-=Ւ0:-wtK&&%cnNH="<4`0x툁81*ϱܒ5Vz_-xiGZAQK뚗aŷՇk5rdoNf ok%@k~/L17>Wh,m hٝL{ݺ3xQpQQ}2{)<ݜ"Ux|P@tTLǢnrxG&<6AC*lO)+mG[ M~_ǥ,@%ordv:˽'UȔo^~#Ľa4>jHPsVœ/stPTP31J|uP>ڡ}FBU̜ystk>j߈7YNIŀ6B:j"SnM4'ڦp[/uSNkLq! VM۳pAup iĽEb_ͦĔZYGqXid%I )Իs pq@AޤQf29B58<+a`!1j!%Uۤ4! }!voX5BM2om;C_ݠlt4 v&A)ZƦ;f&nmV]ӿrK.xsۏ5p.#3桯7#MoVـ?@ljFbl_3씏;e=A ulwE \e=KU㫝ԝaR\D'}A}qFٜYFfmh,~)^&Zt}m飕!):ņ%Xy%X pe># l(;Yywo=@)eo=SzYKnUO/yE7? / Y7YU?jJٞ< ׸{ 𷈐Q!%&xXܾ&tN艭 ,,\s-K߃2Rc=n!\^Gٖ)Ls䜟6x^VvEīFȀI(%BU<#vǔK!:7PM#k<lKwY(2˺3)[Y1Iěd]H,ޝB;%G˜g^EGW@+芯mp-'W| [|׎RWne;k{ZB>WhM+#oJgyZlEtcc>9z.<  +5B_y|~ +[9NR8CL zW#Xw[C,T/^'L/э^}T5je~}bwfO͏ڞ֛o}׾H^0󾱎4؟'l~}C*A5 l˾/k_SAP=mQS̱31qݱF)jozȪzf[~XJ,3 ⩾rGDi(g4pL}ΒWS k;YFN-CHګoJ\;L#% %,-jK~obccH<_" +4V[^NAZf0%_+yRkKPoO#WP)fD6|&_Ia +7ilVP`f_lߝӶ\ۛly* +RW}8~ WyEBx#C;3 [\RbGN5\Ϋ0Z.-rDEǬ 5j agi| ":ˑWg{\=JUƼTD}S^zR|W/5O^j&֔K95^W+Z',PM26ݶ-’oJn|JJ~zD#0U扆s +Z<+mgUoSǔ[@栯7Z~vn /a -3ǁ2bF%eܺ??=vUu҃2|}P-:R TTǯat ;7ЅֹG.=ȄikJKXbp: `xĔ@U]G% ?rP ԾnN~ճ/"::H3rGq~˨~9 ؒM)l䠁A}}8F.Mӊf3MM5%gB|+}wiثm@_ sn 2Duh̀oUz?Cn))•.`c#j6t}{oŽߟU]xY3rTr}YoիT. +ޟGnk +ҽ"Yj mQIQ3dRJ*L6\s2xW {,[ãMxM". s V=}xcL݈U7 NgXa˨O䴌3LJTs_]m{y_5]&eAbnw Ӈ}fWZ`2Ψ?+ץ +~[c *i-ud} ^'א`ٵIezkr,=K.1zgpVb\MT*{Ө#](Kc-%(#T#p{{,F>W~Bn1YYuuᶧ&~/B?l6?U%VhIA/g?jNC!H>X%tV]lk}n]i{9xOmvMD-'=-(B"4<ŵJM^ڧ'k_V>+XiMhEQ_.S3c{d}D}^O|GeW!)j~5}g b]~̀>dO, CWj䈂 z(冞[{-i6`_W= Dܶ;\eyt_Nh.aY{޹ufכCտONoa5_Gfz~1zdta*6ѱJ +٤O/ ϭi'7nL"^,ѫDKkԢ[Cul5ښ%/&3If&Lڤ^LLb4{CA)J」`{7}~?yps*Z9toku}r8->2z|ipX3ࡂxY'휚/a49 -} +cX6?W99(| ,vRo? 2@^q5ڧ S ! .f#]~h3C /657 yqL~9p^EړB^Z֥nW˞#mulMN*i^3*//ܘqj 9FI*iʊj_D4MkO>^ I% $}͊*\5 J +dᆕ6xo*P9FlYAnJ1^$+LEHTۻG_tt,ꞮXswGeb>35Y"g 1~͈_f}ii"CYexWm{Pm5y3|Ԓ Dž5XPYNեaHgG53?(AgL} 7?1Y~L;KkHɿ1}EYRu5>JoEOOa@57zAjv3Q{~6aÆ\ӗ5_EE?^dwZo;}K[.F%!ϱVr˚ՀnjZ_/jik. ?xEG6R<9.i~yjxcvIhQjӓ}U-eEM4AmM,;Θ }D~b` hyZJqL墪3)"2\;ʮN-;YwKS`_bgad?8BE~Wx*/!ſfXtsoy:)ed2r-1 n{=?-SrMHQ~;A3>tòZp)G?ys jkʮl:;RN.@)cx+:x~vpÈ-}uĘv,.h5/=~>DH;fjMp@rW{>JLoe_{]5S~$cy2_&4v,[q42M"ʿMT7/ő7fu=-3di'a@d| Sy{ |OOh0oʾ5h{髻+>98k[2Sg'V=v0(YG+2Al{-eIMU_!OOao3t_Af`K+{Y:6+j}o4l0d+L .~2{Qu^Sv$Hnoc0XWwmLI֮ + ^|㚕аl"4,+FbdʽЬ<&,f~^O,mM} 6E!; Y~xqcPa6b`bhk [_-k/V4mb(m|xVi%z -5SOhgAIK!~6\,St~2o`kU;4gŁ^e2d?|?MRg~W.ZO+j[& ÷:Z7O,|MAE;7.l;#qq/gh-qd64aT^S~NHl[aT̤1>ܞV/Y[^Dɺ3xlEhu+ e;Q~厏ZԁXrP  Bq+@#oS|ݷI^i `pޡt'ZXI kjՙ1n١GZT4arZ*j -Paqq~~J׎15a6{wEBל1z/a;c,ҪXj&D䈌 Q[|ZT\|J"i}}L Iڳw\YiEAvtDP,_!c 1j:}G7`W̐\^ЀswDC'a]HJ`¢jATUc,hMhIHiٌaۨfa¾s&rO!Fq83o;6| 9Hs+Zdpӣ+ږ׫xά [cU+|N y{0N~ٽY>-&i#Xy{#Xa:fs5n\A׵Di $!vL~Fb0Mx?aw\؊U*g-OS?ʮU`Rp_z1/k}n~4+o}k=vwVάIٕSe=z`RgN-OǙ͘O(M aB=&6FQKWƺаAvf>p.J˵Arɘ^?3눷1:^{Q:wBׂ qb3!FzZ c16kMڴc4=Dt? +AN*l@s*XqӇM3aRꬽ5G o 3A=?J{}]"0& +{Zg2#& y˪bT w'dW :^N +r3(aj\isS#撏cX5<3 u<%i~ |rN)$֧^wW=&ޖ ^lO5mW wO8Y8 +] +igBgynZ۳mKdڅiE!lC94uF(y8ԕ@V.k is"^,ذ2 `A_Ta!PES;-<6%40pzŖ$c 3J'9`B<ݍT=sõ51JY5V-ʉ~qQ% (mӆP_ Yk6HUqr5~f>OɟisGX[t?S g==؆!پ{sO"WDZmApQCus)zSTΞo0)$&* #ȗ3ljbSx:S|)8b;v|LNWR2X[~/fr>HuOWF9BꪞT1Ow3.G;/Ps *b՚]@YՆ3)W~eB\rqAd˅YwS3à+1hdRc_lAʺ^O?^PVh&T9mf!5/Z-jfc VJ2b t\3`y8SbDڴ*kvjOp) UWE/~te~o +DzDZY9@&@9;5O{jZ =o6myc#pV,M[[ҼHw9(DۻT_&OBjbEGQ//oxnCu_ 23@@*qvkLBIoN}vVs kB*kN:iv=U=s">9-@?zˆ_%sT>*zH(x7Bm]9(97x==&P3\?l |}"vU;8{T\wK95+ޞ{t9b6*0\~ N.FʍQt=IXMo!6KI! {K._f,Lݫoٱ/S3"L.nmxm}]ԘPւdn>Xῂ"wAZ!u;sӈflL- 򮬓ӽ-ϖ-vTswG(y3jB/\sޞ urw\Sc|c\hBL8x\&hy3ۤ֞I%ݎߴEW} `_'I 3j k,^tmptm:/p|oZw}dzLAuMAiVNC1(& C_JDT5pnƒ?3Ja_Ya&ob{kcV;WL+ <fR;Fq+NX^L3tDmuu/@16b\8,ZƊK:RP6De VfÝU"? mesRJo /pS +j 7*R$Iuт̟A+e@x۞^}oM]w ++r~kD9cT]s#*B-CⶇӃd )*sAh5ЬU 4}Xs̩O?k`•RLfEU\ӲΌt> -3Z-r,GU~ݼ=~ݎ,]g]Y1 #JPɚ緜X,2D3ܔ긨u[d } +斔0tr?@rh?+f|YlXqVkU6Vã=/|nJ^5#زE;,$)k?/*qǗKb麩+}@&vf6.< COܟso9 eRK(]Hc WE/9`ʶU 8}XKfLi2Gǩy@w7, a{q1q}CГ- 4§ pдbǖ.E:RŒ + cBMXS:BW{1 0b,DQ!501R?g݅k ҉^,eTϚ^rk.ҲYjAlzpvLEĄ. ++q; rv=Ç}v|޾sqm/*$ +vxn<ʷmVXٚ 4i-뭋*gٿqsZzD)="Vl?c`4.y %+6&p@Wrj/WQdžHz +`{,ژ&1ūJs,Ǵnvs#f׼|rq[@-#9=9ӕ$GoMNj瘊X(@Vc6eGᎻ;۫v|킃Y0FyU? Vt؊e# 1j䮌:F|jWyd~M3uF٨~R窗XpRZ#v:`ls74T,xxm{~2xQL 4aAK_01}|-wݐQ*&pΆL QN\CxiHX돡֬  ;$aRM4-4~V|S[>p#} +`} iè!k#+qōo_ >\]"VlDMp#&jT`$W sf:hΧϻzyFfOx7iV M=_Q3jI{aoi۝d :Fϸ(~*|#H'xiV78p KVlܾ͒kEi{ޭ 5rlU?YLWdgrafRͪM@eJajPrx<.錟SApk..ewo{Tꙵ6pAL^I9*5ju,YUGdkE嬹u\D$eo2la -gJi )Rr֓;^ zE at&>6|˧g| ]0EEc"~^З<ҪPP{TmT0uX),)5{2d/]k#T?`G{Dۣ}/ģUk{ڂ5aVt:llԈX&Tx" 3Ý :pRh+lϷ=X|Hu3jB}Ԍh?=2E}`rbet)-5)N{#ڶ\[f/]<[؎jKEI-`<[?AA!Knt劗 +]{"nC4G4BS,X+6(,'XQU[~:.:؍BfvjKԆ-7u +rd8gezN졭Ǯ=ivZ>[=VWפ٘6b1wVhݝQVM)uc$B7̻ ۠8ҭ SAEWRr%:; hbZ o{;!$7:BoIfgJۍz&)+w /0pVzw$U/xĆo${b֔мˢ#άSB¾_oq>t(b7DBN4auJ)7ڶCBvۻ"VRø9lOph6ʡLwvǹ=;؎0 wHYnYz T2򨔙c&=rҧ%j-ϯe&ەa\d d+~6>#BR7h2$E܅fK;^AϦ7rQ]0X`dd؃<;5٦OPz :%Ҷ^m j{E3|TնaFStu2"ۆ魡< 2,$ϛsV6"|ŷY%c 7p|Q%C7^z뚷HQ/ b !(XS}1VpPc[kLC`_e+)x}T,{>wtJ4c.riuw8WSfF]ebްG@f6LZ6)#yHTqĨq5A5"ko6BCc3 |o+4!ʐ;Q~N1!&e+tۅIs.l!he3n}gXt#e@췭=l؄[rt)CL_1dx-@HcUi*JXxU_` Ɔ9cl-0[%R%bVIΝޖaVw7+@4^5iS +ZiDKvFqkPfj52M^SXl m6N)h٫R\՜s{\畩o-zp3D^{2q،q5,CqDuХX]?UOq=^YIOsŀ Ը:}.Byi;WPSYյK?y >¸'G]өq/[~`^ %Y3r5rRѩD,Yʣ2bѼMRY1>Pse:"2çKY[~fٯ +-V݀4K]P'[p9\)M'`p'KPgsӻ~xp;YrC9%45ɲCa'f`aKBHӆKMuq"U{ӆV/\X=ýG qό;w)?e +;q^!!C8E7??dB|)K0$L.渒?Qw˫'sJ߿~O:&Ov0InWoRfPek,2g*6߄0xS(B_3[M+.F<Լ18Ѡ".BZq[MSGr ߽xwQy܏>}8U +1s`ܹqYB{;}r>ȯ~E7n'T}ԯahw֪w$q+S;s1ȋW$u)p _qP~^aX[Kܥwk>ͬ>q5HJhz.牰^r+ qnOFɨoE +[ݚ1juo0(C38p1o4OTz؅GС1ǵ9+/ǤʹGl);G,(E_eя|hb +L{q6b2I7j<_ڿy4Xcnm WQx@(镘BԗjλhLTШUUk]\2LID O)wa*[o\ˮ^ϯ]Sک%S%5z~4tͻixΌ)9ZJB͔+Lut:MUS +/%ㅻMGCO)R=G9br5aN)L}j-W}6aJ͟V7\#G9UDM|OHGw`&Z:79Gf7bhBv ^-}\8p֍tb$PA^(vU\ +ۚj{M 2/oӑgrKW0(IEt(c ~3x(^CgɒN7Mr9;T< *.#кD6S],5;foۼ?p  E#:'E&erAMhUa\ڧZ+L}?x ܄" 1$E'y+JWJ 65lf WIfpǝo3i2K{~z;>}}y_ cO(3boHYu`t„[UrbqVbEG4Bu}GG.^+{uMdy o0HvEпIj&IeԊ( <`)%Cf񀫡Wb+[R]M@kI"_~;F ̾'H%~LuaΓZ?1zb0D4FP{}鰱Q0 k$cy҃4Y@N{>e$;8'`vy{ϗl6ee˘v^COn:l[… KH=NfZR%Lb1L%$jio:Vu/mo[7-,iVYZ; +RPH p<_:0WRKɷ +ODvbZ +wĘ +[ o-%/q{^}GLȕ>"&eW(5탒Pr+<^W+OKBî8 1bzaʛaq͍e`Z͚10Ö\2e'lB_Ϫ:+mpwS1f贈Etl{>6VlK:Z(͋AwP?aAF2RluWˤ,MV"h.ǿFK޲zTya`ªmk..cɄ8%$t{.q GW]1 WHPQVގQ01 +Nf?S +5.$.(Lݶi +j|f_ FF=ߘy:`E6!Xd&-0&wgXO1F~ c:Yh/ڥiG/}gu?'.:qU |嬝qKnjYQp|يи섗-s#x9yviN!#֤%~Gr:@)$}ȭ@Yc.!f9ma.OG׭STޱsb$7⢂\rFKN.1iQZJ W+ v=zĆ[dB vD*?WS7=R\x@Sߌ +zo/i<$W*G[Vc@8%H[  V 2oNo;x9;NDQ|0pj8Qbb_l|d`j|QBkĄ[uPۿSBDȗqRצY1$OA^M)׳zlټY8ou=WMΔ; 9%,{/;8>b]ùU#d誓ҶL)PrdŜ2.m{6.+Щf>$vəEZBs@XpkWy{k/|g]4qQ,~\siK^-.7Zm5+aYA>20v=03Ħ4eLT|{В%%7ɡg&I̽ l 4k^Hkw`Wl!",f + 0еuAK3GLf|iHM,2^3NknxWg9G]fL]T&4ϩZM U +\0PPyK5^ KFuO>N_$ȧiWotrAU0+/Q<D53 #|ժر?ϊ3ܡq:eŅkCS'1_$w3NZ{Fi%>㑟F]ϣگQsaHWelJDڂ+iZxa%]33v*tE̟w1 ?zMK77<17oxp Cm{W殇ԺcW-ƨv}/`P|AQp%G ZE=<ӸmX SmT7[vTP1a͕E=4cZ\7BԀ)A_sefjoŞEd9h?/iZR$"{]Zn{L:C2lY~jˌ.6Τ}!-fǎ(6Cڀ=@-yGdn9cCۜHs4_3%|5P>B/f6\TpDI_5Tvd[c4d~fQڶ7aAq~K]74U$kÆZL0eGnQΑi5.t0oG5u+8}ҙofu/!`Qs:[r>΂v8L09_܈UTߕc~ 2t;f`Ƣ->%oXQ;.l m[ٕ3'),x:_z{AVzIoSxQv{m#/u{GZ|seO U +K˒wݐ/! +8}~?[P آDx vF%v^;2mlM0t&ZOY,hφ)l{0?xgQtY|ՠrEm&%Ot?^&[,oZގp<ɿ۱kF`Ⲳ޾ u8w5iv>ar6NO>px)>2{nI^|mr w5=厍c8RǪ=Ց[}W--]|Js]x}^ך7`'6ѥڎ䨼~}GO;3mBPquیXRw,yԞc\UXW/ۓSLt_@wʹs~HGj/?umiH¸QluB>aƇ3wW3 mj_Q? /Gl?[iϳn{|N;3B)@wZZJ!$Zk{|`B:c%Q3P*¯墎,5!$8f`'Ibu>tc=em3yr0Or>-Uvi +l#&!橪>$3X>5W~2=248 Bi2{`cb~Wy_ᕓ21+IpI[̣Uq{}lՑ ? \.׈5Ւ +*ty= ϑ2=Zz[ͨ? ,P'uBEG-nAĺuU.1@5WH_{[|#'`;C-Ӹ8cϢ'1MHuiU^9Q0JC3!qi#sW0B(9yђc.CR6W%BМb /W&k?OL[9bm[" Sqk47yQw}sH(fx)2ڦĜ?j"~ե$g٤첍r^OMҟ+hW`on_\`#&n+_yzKӘ ;Goo?Tf7tsFPvaoZF耉X'Ls*Yۃp Hm{sd`"7ޕ|4I~5)ZG,ݕ}2Vʈ  e*ͣ_f_Ɔ<]S 8ܿ g|X`VG8IgIߪIBk !C"NVؔ[M.)5߯ڀ.ejpNtꘄqR]&"T큅̏:&2˻"̟;ͽ淇sOì/R.5VNh\WYO˧LiBQdnt\t)(v-&!XňǦ0M[Fw4+~S6U[_#EPbɱ];1Bu_vm=v 7B\uNswjČ 41p:Q~% B3-.@6zwG隨!ݝ[Avޙnlכ]H4*gn)sOI1_uC7_ Wa#ڳ(⿟%|n.|8ů85S,4-9%GA)<\tV#ږ3`#UK揇hY摪;ƴwF7˯> +^uX&^5,"-ȵG!:ˮw~x飡AͱE>mzrvsΙ4fk{ױ`iL83[Z)5ohN+ ӓk=;KrQOmh6 uS9u0HI0 gq6#-cGwo-(:{(36Dt5G'g>~T{RWǠf+.*0Ly_@CqyҴ&rUYmWyԭEG)߼CSӄ Ɵ ?I)Vyi]Y.fs7ȻݍN[v<\f/M~M߶z!6ޔ\%܏|bcN1"D()ؕ=gFX;gbINV=2_Zj/ׇ==14[gy{#>Ze/n/]reM̈j4lr&`@/ +QzSU YelvC+:riŏmnt½Z 4{5O-!w:?LjiZ0#TC]o@VaOw'1xEoP˂9K_d´SJa)XįxC5桌Uib)kψ scS7'}+F~e(!Y$SUywVgO /󯮽=>V+VʯXgЉy˗ QOG i#l#u|ڋ! 1v 25ɰK1ɀi1'_:*y?XšyR߳' ew4npWS4#Fo  Kѐz-03e2?O]G}зg^5<"R{+!WvFp77jwW Ϯ?E޳Nto +.M2Z{{l╨[ +O-*}kkus+o`H_9y퍒4лȨt09_i +.u3P?GF`f} cX'p=O +.8Xv."oEC̹n/?E5׎qr8%gs9q⪹cݕ`=᪪1gShU?@P#WF;(ELs +s8y*[QpIV+laj% ?>K\3ҍ.D4;)J$ oo)r]՘~M ?CN))"8֗=24LLXi/hh:Ԗ>Ŷӻm'M$^ t>ye-yC啷SORA$=gc5KXf&b.k׎WElyFoߴ YLDSV3BIP5\P4wEoWD|\ U~Ywljqy hX? ZnMߔsКM\m&vk 0)9/dLҥ&%qKBLbJZBp*P̾[|ţbZCJY bPCh|j+㔃l7 +_몼_f!'DȝIf>vb;.4E],d|?2vzDٜ9~d]L*iJ%F?>e#+q3!J7㴈?`~٬0Lh"[m ߍÔD0:C9J0߄~`hA37ר+]g~ezKW[o+)y9JJcH;㐐u-i?^$}љRKic_-"-P=5O^ =dhy'd)me.ۋ]%mUwJzDC/YxNo)zݰkg?$W?曊h0{k>175g9VÞG))>984]G&13Øȭ~TNx29q>7Ǖw7s+3tm;C )_v}=_7:K/QJ:[]XaCccF8¥SId\KAu^CCvTʢ-Qa@/5H0):My)ZSJ<7x#R xwpQJK类l @iڒNkqy!f~{A@.@9qBeY(f?il `ֲsʆ_ڡv88>A[(eh/䚣إ|(6Z S0&9E+#MQa)j{`E+o[?٦))>5` `61ֻy+k_&Y?~Vyme*-9me@.51ʦk+K3N3sT8x3xH%.uV#0O? s̳ ОIfW9W\E*^Kï#~-o,L0.=X|(@}LMZ4\,=3jRӓBc/үn>y&g /yQp5)%$sS#*o)jݗT݄&^f|=em)wmo;]F--{;u(4|;67F:( _"¿n ;KL9n>LTFz5q5)G Wb}rLq'(iY3I|z_ս1V…AfC 6;Ƞ QT0/q]~mӧm4 7'Yxu< }2ᑈ@"nl6izȸ+E6FAao`d8v=HShcV\Z-g9{A3P_)9r hқN7oafMJJY'Ɇ[l<.e18?LNuHx[ak~׷d<Ϗ&_3̒zz&, $5,SN-I0I.eeg`nZ99=rJp l yMLJ=`fw2~ jbdFk`w +>doݛcxYZf 1m?r ٥G+bI;Q #' Ok|w3\mߞ` =ZlqɅ(cpO ,6 +C{ $h2D*ɥ-_# :2RJo_$<3-̹d$pԭdħ^fߓ$\3t_~ x]>xO!ۊ Oyle%iο({Qw ѻ+r/Y?/ʹr#Wr}iaѥZMO5vJ"/DVݒ5eYyyu*)tXHe{)'f{4jSs&[5 +2j "AT1Vt_B{ÕO׻(ֳj[nm&. |/k&lwdMc!Cusꏆ3{|j֩`M3Lrg(EOLPIG "shޘ +,N1",Ӕ巰/Kϛ'[TuKUw@=\]xycY|0B}Xag)#dMP!N)"{Q;,uZYeADiiR.s  w5WDMJ7_ͮڟB7*Fi |# %5ǃZ&i>dKrZ怶Iy֯!F`{ 7MӔµi>S*@ZƩY1Bsj&'j"3x6H]xxڅw{+O䪀Ѿ):ʧlJ8^zDd5)ъp,:( Mʨ[K<ґRӷaw{E׻v)1#"*n_A/Nab,uV'1GT,CҲ]2j_ũN?<u2C-8Yij́8>F12VJDr8m'alG1vroᛣy70}?:j}qZ&ʻƁ2ۖd^D{yYem )Bk%.}kH>DgL:{jf l_˰NC.ͺ~m7E*k-֋kECiX8,>4}$8%4Q0XpYR5U1+l}0MN]Gf):B}sx0f@,3Pl~ovףL.z⫻Ca9\K +,76ZYƅݣrbeqfQPvekg[:.8 EnOGYAm30 :ޮ"_Vj봝p\| :_XKK*= ԲN]godڴ?NK[&i+@*(ȣcAB1H;^)2=GLs˅h3˼K./de;RnԤϻXnd溔,j^n%gy}O9m[rQᚧo~xu =p*Pq K]G+Mm{SԌ4b춲1t[NRc쭎6ٕdV{O=*H:ͫnkV7Eϴ7 6we&hRi#=r]۸r޴f׼|nzu[+2&qy.Mh&VydoCzP3:Nu8>1L"W{ +o-wd_~?fs]3CZ x7J3Or!7oZF=e y{5 3k?L@F;Zh[p˃œAn+8JO2c͙?"C>iĘQF]V^i>}_o17nz +bI냸ƑH<4"1[&. ukk3}?F2S2=*J 3-c][/[wA>ٰp e27\:FWU+յAv=ya/)zs{8X>O4ֽ^2ĩ.ǫ߱N[ؔ\5lc/fzjOβko+ V=u +O lɚHd=Mw9\OE**Iۃ{69*8~IlbpEhi9lq"lmeCGiz_(8e˛Κ n9-Ǧ|YC1\fo=g1HNZ|d&u"GԼ6[#yt"GAm R|[̈́7DuG+- JB}A-)yylCD/qj=K}^]1M}جm:ڷ&GK_^"œv/Sݫ۵ 5ۮRA0 "UvymuQ}pn~_" i jkqizo;E }3+C{&nNВW^;duD>q%:!'g6zOj$'_}pUUsQV .2Կ>"y2¥#7G=toA+E'qhR7gj$#e?5td'&3[MZ]#D~ԼV)zdP +J"Iɪk^.as~fb6!pRǫ@fphUGMu[(y#- +G".٪ۥ,y*wXc7NSGˍMm~ŖXٝBŸ٥ +7`YbK"GDؕԪ)DCKq(N 5Kh(e'k|Gͬ +h8.PA֦",3l!3]f]ZjwtjH5a1!t8:ʣgVֳpn=ZZfEBϴ=)'j^݁f0s) brM|✨8Vqo]/|ah;yk]杂=M6is#RWoUiG?5Y~%}wI;_Na̳a|Uʁ8u=13CZqE-9}8~䙥f5}Iٶ\M*B 7p!%5-RzD= +6)5{v_U,$:(pk^ +;Zܡ&\`ݱ)r1jccc7 wr>-GZu)5o?.DžVOOQ\}dj`] =_8Wè +.m ⢖;aY&Նs^64Hf%;/3YsQ':y#5;e>ͻzu.3рḟw>n6h SvǍFC>Z,&UBHs*IxPT ?312|Pŭ%C%Ա'[: +^h>֩H-vx@BVxJ>"XK絛[**..?t'UbMS)f⛢sTM?,_,pF3 $HEl S6eMLL-`dJGJ-T vCµ5 +n_Aj2tuTsCX$Ds*Y0}#~0d8#xso: [* s&9[%M j4r-,|]MSrڭmv}60&2p`N8ܟ@H=hͽUvvX:C(@,1џ\IX<rZKbsS`\ehdY\eM 91ةm}nS\J!ʥ`Yk5~=x˫g )~u蹶:ezܙj7G'}h~]'zN)"z "OYG(_uμ`Z㱼LUNǤڮkz\*Bc&1hY;M}KӖ[r0E/k:? + 3v<4Ea}m4[%{x¥Ľr]3W2 +!$ᕃKyhrJªCY][WGsEߟ"<u;1NvyYFLp |X`SP.Ǚ&)I12#(3f192~S +\Z .vo&/n_M/Q$++T[5:3GzFA 'N 7!<)HEwJ{9-;V1*2FIiYBG͂zc + Z~r+#';U"!M Q t\\`lxGE;$Z֚_:k,cS Ze`蓧9JѱW +-:;t f`FiQ)r>zkU>HMi[ M Aܭ&o ߕ$T<>ZWب,IB51.&``W' 7DlPK+YPLSᲰu/+'+ulRJ<~w.d4*)y~d Xhjr/ 9 ]jyH<>B}XQ8[!ӽA/ 9B5c #h}'B +K?v"X* +MgiƟ ?VP`\&S)zÆ4H`I +,m +>pWaCĜjQL8co m V&sy.RکhTV9~eB&QU WoȡUrTspI- +I^ !9Xe5g8XշYgC9M7R\cYPs"F:)n,aŸXgOĀVH9h 5;MK di)V"Aac@ĐG ʽv& +8$yq \ +7VyoԣshOzy-zo73Dxj\rr0EJV0N9 ֤p8+¦jΊ9YxtlPm؟d7Q4M:Ykl +^ ħbVzVs> Dz{/oU%Kd_= _NtBzP;"-e` d8XD_v[q9N&4Kf"Z\44:rs^ g^ כ;>m x~5>GɲᓏO W+n5}aӧvB*nRr7&h69Zo->㎀@qG(8^e ؤi\Mɬ9Y}hp JId,q\Ls]sE(" +\UuAi@i1hn L2-obzBCs gEP|_x~KON9=ÒeƖ[Q1KA M4hPN9GJsh9@3ZƱF@вve٥l RZ=~`2z>AXq)#g>/7Mvk"6niRLӤ sjA}+f n`NC\/+[吳Klirpyzn 1yL73'cn%)xjg]GuUHkg+Z=b@go--ֱj>y ,J>or)6JM,fV?1OܰLAFЙkZW@-AVr)jmRt^b{ϛ!lrS)$,`Oesɘ~1#%x@=o#9xW[z-d^XS>PA 6:]r8ZxFC۞Z"yUI~`\XLL_S4gfxG,:. +h"h\Od _lr֧]QQTJBhUp*2~Uh._Mo/].z!mȡ2,ED{YX$}'KBqTqlx>=>G|[\U.0&ƣU9efq+RU'z>o>Q˶z+YI~=3p^}KM,mL2LL03q m]s3 A.^.>rK0 k˩yyj龘j*o 6ڛegkc"θz*jddLCS46إ[gYj{(>%7S1 +2 xXF/FC)> v8C5O.q/riR|G-557,s_6/BѡQ=DN +}~5qѮ~5ʣba? \ME/󭴼=^m~yg#bON4WfeƺvD=wV t:EEBuc Ֆw w=q7u|C) +-&?%¿3e{[S㎨d%j8^qm2lRWͥKbM!w5du2:*{  jؠV~l4TDu<:ΧYp:f]NVE}^W,fSͷRJ5v_[UܳA|q ħb4̥dCs$lH<` zP\X~Xcz6<ء\whHVE2 D+ϙJ 4)æUj[J*0 .~0!x-KWK>e̱6'ʗM]=<6)`Z94xU7=s)L@ua +ja0=J.a"<>`?~l>ȹЀZDL7{ vJ\OMv+iViUyP7.խ Ao954aFx5Yn-ؙ7z:k/G+@*-nv\eG[הzF3蛓栾y 8~>-dKX'}=Yod\K\*0p||P')by}8C)k?7w923!r%ޣo8Ͳ]JF5^_|{Qb;5^-2=;ȽQxhoU55|2<b'Wx_VE%>$' W(b~Tbr>qp_6E/["*Y5 wKQ96.9 瀎]aE@"L[MqYNV7q4J +XlY0˃z ${@s.pДrʯ#g¼j.֩dW%i>p& |V$67(N=YRW +ܣ \syAt&^,mbO J[S0$jMn%z rs+ʑF**msqbM@q)զ)||siA61%p@@|k0,6ჁHR`-?oro_~uI7'!ۻSH+LSBe]ڦ٢h6q츁#@ zo@;yNYЌvg3\̽9)wog7t8wٝԶV80$#W[wtOk[g;/w;凁=zë+O wo^qG/k; ;;yq߲?׿|oVg-?Z5gޟM5fʹ<4Ӌ;Z?޽|+ݔgϓ7>tOoO^otҥ_ܮ_>~3;]3CCkF37}oɢ ڊ:^1/Zw~ ?:(Wvjüq'wN>n~tO}|Ә;gx^Ư' ;^\0q/:[~ۿZvOL܎sxԮG;yYW[Ƽ4q䕦;pánoUcG뗶ٴ /]ҋ9l@w>i載nЫo:sW秷}OC/ +Om%_o{nļwk>rO+nms_1|lc~Q?]sjvN8J?떵GNl{7r3]aSmmvK33|;X44 =솇N\sofm?2Ks߇>[ml⾁_gv-9s̳uشp ^=ZFt˹}=pk:g|qL[V6ytcZ_6}<K.0tɧw,wv?={ţ6qp,gʃx|/wo.hh,˳ .gcvwul^__|1ޚL=ggؘƓw]᫭lylQ_3]K\9ZAVrlqa ;W,=e{j;~1]?z獿{3K{ZOoϸ۴̆_79O͑״=Ҏv?΅΁wbcm^3[8ɥ@?iaG}zvwOw-X]6xjkl?|yn=wV|EЋ_i'=Ѕ5o{}?[~_~C?~}ţ'=}wJߴt^q{/S{7v=ÛOm[4Es>\` Ong:k? wjck[n>js stA_냗ۿn{ziؿxoٔo>w[n_Az{ywl}#5۶uS3GYvM'录4?x>זơF]r͗,h;a-Gֶ?6O>zuמhzf^nO}w՜}7gA|qnxqvww>}sG;0}:|v替>vSo:MK\m?7_hĖ%=\9o쑗L?aaseCB_t7==oԻ6>Rͻo\v]\=4~Ҭw<>~ޱtkgvrcm7|8>|ϴlckn>i͇_7؍_ }tmtE7vNyNow<}y]3~oOXІv/]v뿞 γz?=u}㗓'[۲W)ug%+~eg/{ +\30m'[϶<0oNi[ >ygCkn|f'~w|inɘK&;78R-䍾6qǟ6;o=y?z^?/ϼ [o;3xvg״w~zO9_[7M:iI_?\؋[/x^\wqws;Zo;#k'Z~S~T7o {vܝo[ Z/n 9}\س+c?11w_sfm.{YM پG7{}sGV7eOX3h\x|Mw]׬[kvutOv/_ǶvLx]=[֮{>\y_oј{q[/u v̿̔襆Y\%CM͋ZX4춞i'6v{}i|m'{V3з~;q`mrs &{{.: r5n9 [?~m٢?uuGwzr}{ӷ=xqYs㭛?sWB*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*3Lȍϟ_s-]McjVsl}wCo_sfB5Ր_@NPnn]W|{k{'76t4_6\: +qrnΡ/=OI ?iH-ċ|U'@)'/B0 _y$ +C狊WEEk殚0wuLM|} p{&5tE9{Bwפ|{W_{Wkm-4V%P3:n^{W맏7s0*wΎ.S-L+>5PpEc[{GS .r ӷZq.wԮFOv/hkۚ[ꮆNb/v%ia{S_[K2Wknӽ}%/25fȗ:٣;zf ̚!_JXxL,dQEcqXlӭ6ן\Vw>gVUzͽ3{#kK]܆|uwNlkh.' º'5^F륫wԘR8, .ByJ$|cƒetW'Uᜰ2Z\nt4%/(0lSW\y\ܒTPe& Laa-Ќ;&tfF`%{,3f6`ff6`ff6`f~h',ฉtU ~_G~m!͝%kpprǰr7tޞ1 NZ24,)}-s1Mt˃4t,lX\kȗe)CYh%.`Ԑ/d8 %//0+Pz\r!yrрa0K?UԐg9wpr:9w|?J]4}+];YTi<ހ*4Ⱦ +8O܎˩G-gMdΔx>%Z5WŋqkCoo{CטˮBY'Rs 8UuM \88+/u/K>tMr%']BJgh󇃟~" &U -St ڐokl+]+VIZ2* \іbH7[q37BY: ANb,`x6Q!h @71BYz(4N B gTYgvⲰsvWp";:z&X m('[8rC6LQ[Y5wԥ56wwfU:(7Eɒ?cyǗT,EA&\U\"-2\չt\pC*]Mp.}ݨ|ss(.Gko57*4;uP{jQ2mX_F!F55w|NVr]l3· + tCkE=pfx,eS/*u +:A۷zWT Gj4J` YR^,S +rK*2=K>Vl9K ]HЊU-eT܉dY.Y).9i.;|ONsCIs$YQMeg};HܩQ$г$JYH$2},Vm\KK!|;J9qUϹdF\qG*|+ij_yN.+=/sizKtfT#)?B6.?2G%?G5e +t<{vLjiRJF+ deYT-/nWNɒr*گ] xZ6qin2m?v44cPwOCc{2jR[ ZT;+*qd| {aHdyUܠaS8q6Æ\-E,5rfzTvqCĬ \heXZW V WLۑq2v򒓫@ժ#ePV.[%LE^|]16ӞL{ʔU*og킫&^PY"6O2N2ZgɬC+x陵 Dhu?_։d"2WK+23 IꐠκTf5߿2T +Z:&tn>ҕLhj,%W)99{*z#d㡫s"@s=C|e]ݓ/tN֍S;X`j̰ Rɯ|gܪ"ݜc[)utW tv+ |U5ۂ%`%2@c#~BVwKSdқ j41[ᜠlg\cFf> )Ь졚J/^BJy`rM%QVIU>.Esk[WSJ#ڧLF`]wWSqVWnm01ÜBvUom)UWTw-d&|`sgQ6莎 +8Jْ+, +1s9*UU2^h]U?){VF̻#Un]顆^f}U ۫&shu/窤|O[wGwk4:]IrE_RW]-MK Jg%ja}UoUagV|dCW4Tےon^Rrĵa8֒IKŕ.]\FiKDJ/k^-y-s[OXذ5kȗ!_W +Ch%`Ԑ䷢66t4.n 1`Wo&kjjk(*}]O%',̆(}e$eQJ, (AfV:F8RG Xr:M EV^KhxgWSͯݗ@nv%[I*e쭚[G2-coކwi-S2񷌿, +ϳO}C_eF>R_)9Y?[UK|sgNWk9"/YZWڻ[ڻ.@Oʹ憾eX!1b7^1R_P[ []ٻ YoYu~, MgOll Z0k)g[FDeQ&H$p5owٷt5EĄ &-. WLfBC&/Qsz^ɍ//52/ [S3\ZN )UVuA\g |s z:З.\un \PKW+k\{@u<wqp_0+> N\SO:u$ޑ%> @"r!px~f*y XyM0=;O9蕓ڦڇ|E5z<ĕcƌFCO^3~v['CZGS{AΫ 8]y:᷀]6) M8(L@}"?Y$/W g !I /d\ xPt\+<#H~$'\-7]xUp +* +79 L6{Æ(ih> /nlU&Fms= +CH[xHk:& X!å>cȈn L q@_Ny$QSq4j>AD@3, 6">MNq?4qSHXf|UPs ~=d@ĮdaED'3 a` \&h=  +/У$FbJgLHࡈ@v }Iw <8Upp]@ " +0qd@@8a4c1>wQT ]b!p_ + SWuyۦ]~6 +<5XCB $A`"> Dp>`Hȿ AaBel(ԐQ7^dA0F (@*")M,}/$=CL@;2Ќ|ZB{]R Kt[# -y"0q\A qJ&xwSP.@;E@&)|F<凨cqHs@r\@,A `3F`qQ#/ .sUL>%t4q@SLJՀt& ph'Q΂!{HȧI!- &˄rI] E>!#9EKqQ=D'"׮ 6N| `H9(ɉFAGp"e/gy w.@  /`#".K"CdVt4,#}=8_3oThH@Ԉ# +o ,62 a' 詊QN @9[,I8P +D:`\DebLYBh`mF PicEU!RIn@J 9 +6VHⴑ7 V8p|c]LvLV .&<8x +x'ʻ6{#K Q:' *CR b@i K@HᮑNjR(ס"t|SI0[7 Jlw!*T q %0Nqxp> +l0%E !` +XPH٧+ D HFZ ("銒 $i)XP!Ey璪| + 7)4C˄=J<& QyxuPDwF[63 PdQ"WBŃ ;?ds=3"A㮂r X EXI{>A@kP+>m(ĐfmPGpϦ1k,EH~`JH .@ ca_AVR"L)22xsڬB2pQFB8"lCD/ 33d  +S[⩁ ֈbI4 zGwY} %+IFi(yd1Ff6J!!"e.4( fs̢'brQ Q  )$ @pZ$25\{ R,=w]&W&e!. +4X?k'J?@-*YK<ߜ ̫jqFKq2݄Ps8"D A)2b# 'V|Þ^Ydkt? IQ S "PcTV$"}lTL<þd uB1X[-R- % fA`+!BzJ&4d؍Y2(^lk ,[c%X{"`O;ӌ4<$*Y 3""0@`4@ŀq1/# \HJ&jC/d,V#u=F0URIΑ~*A\UCEL]R l;+Hr o&lNQ&م,ifi {-',SZb ch!ȇ[e@MA$g6oLE.[fC3Œ,>"OH'd{bZk%E +gE&YÎy 0y\g HqaV:^h͇vfZ&73K@ڝ(tj_Aь46b>:i]hKqِy˞ڬtA>z-4ND^hc"M;Qj#rE d!q̀IDž f84rnB0rn,k6@Oyi'C7DqKR>$!9.9i-h ȱ޺5s6*{2y+K5>y45K"̻b +}y<`%槚117eӛjܵW 2kGGW~/M 4ocjP ZtKFtB(gX)ڗÏi\E/O%l9%꧁!` +jهf! ]‘Px8B?;/QujK妵E촶4 +-h]Lq">Pc^tB]+ `,`lJ ĘIYnҌT]S249 e!&mP2GwrŠOC~aXjf?y4PG4?m%i&ѓh[(>j=60h$@ M{g!; , )J@#HzJ06o+C/\꓋%S5FV-*op) InE{I}&cTc;Z`7X^3͍9p.e>L[ŠTj~h|DŽu&C)_β +zXBp b,W\\-mPhF)!/P}I+xBzvcӒ|8(r%jwzW*wNFW*ܐ0}Ewy-2)" FBu#3GAVZKhbD%Q(+y!n0 (QX +!@P~ߗє?`v +E? ]L-z:0@Lb,8188'ip1TMR< Y+_PG:A!S\c07fL ~@2&9QX^R2߅V_S[ѵ=h"EsMX YS̐aZL&)L̉פ8ka=hE16uD flzJ?l 0v>. SPl)(ˍ"6ڞ,<^~`_a\˿fm@PW~-|3\h|C\-R.QSa y1e\)_p y&y)\a$ ԨP8B4KQ&F0D8Eu\ITZ!.$>eD 4%ʬbU5Tb"wǨ=iTF$WT>pAs5,&+5@$ Lוx'k0ui*LQQ64<ʪ. -Q<=D8+N"IyX}9_9#SFk|y!$= TQՁqm9OĸXIȶX4A)Kcp`BE\| +f)#K{hƒv~H(|QLt5~H$0.yXK(OAAЍ(﹈2jV 5 J;\@GY .21a6k NXT[㊞G !$P'V$ Ҩa1N( ˧C,m|>e@Ҡ ãB h-YtJ SCҧr+B +Me!X\i1vrsH-!Fˤ +jmSp)viCMMzrB A}M}+ (-`mkIjyʬF VFCExO[@_Cw[6U]8kb0 JG)buR 4 (rDZK. h'4!&YלHN" YX bblc@r q)rYBD5sCpX>]"#7Gs`@rCz;G%Jj@Gq ik&F>& + RPq'G5MQh +x%m%ESt;)Q(\Fqճ:Q>9C#=b OAIq90j22ib!AH'T߉A9An#K dj*vWGMd t X܄u]S%w4JۏMa/:x1hdj}kwC|.gؓdM[ E~ .؛;#\78[%.{Q@8]NpC"JsƮL<k9o!6>ZIhx(=V #Eߧ듯\;COZ G'O~Sa +│t\پq{XKz>톇mvGÀ7 +q뙌>nggRTܣ~ .\HPQ_!.:m^3B3H ԰$0;iA?}1wAqš FrщSg(jN^ؿGCVꄩKcR\̐c 8 `wbnt6Hb$Y|R[ ~FSG.T? *7*%y wu:V1a`Hg> ӊC)(Vݩ+:[i{=z=hbƈ"ZEKl(N#:C6 ʍHU,go;hR5O"W!p:>bp5֢ȭrY5&0)\HV<pq0wҜBͺ:=>stream +ɯ>\R;ft#vŒQDI>'`(!S(1Q!z '.QϧޚJ F$D z& žd!&wlD 5 ImqH#֩´'V|7M1grr% OdgR{ig!j} +Q ,C(] +~N_'68$]u2Ed_jԏYTﳺN!\A)]XWmȲq0bڵT1;uzr0R1>=67/sURB+r2 -0 +5?e8BcbHP>5.l=~LqcS4ɎtІ:c37TSMh)| I#L_@l[t3+""aKF9ԘkX"]Jʎ澛ԄB>^ةQ>/gZBTѻdp+-RH(9gd& W",B6@4$JG: d.#1d01Nswc6Am) ZdKNLՔ^pTs&`I틚;&5[8Qs7/; . ;A(=\,: =0]anɵ1/߾J'w76Onlhj5t}./OmR/?o r_z8-VVnzzMwS.sKoT|N]tehDpԷ.ۉhhvmDfnH lH%TW\ +}|r\Jޛ(^BwhUQ'yJMȝD[)K ONhR) dQpX5###M|(UD@1@sZ6c4n&0ޖ!pgref ^v u L-gAi:3iC 8UP^ۅ@z&# +xmb#A= DKyOTsar6 r.p d &ӗI P&Z@{ԌNcffh}% ȼHeߛh&xy c +AƼ&)%ND rB@v0Gʅ:4iTV {gXJ$@KB7π7xC`Zb ^r"Zz$0_(I,qҖxJ{ʾAj +!mRkeɈ fCج2&TC݈y~&fɧ[vxZJݢW[@Go (nA ] +dW0C PL2KPe7BCR0B# R4#U2ɮE|\5,910i|27+lYߪ +~ s#]*i$x*}rUeDr;K+0@JD`$x.R +\[VC("bz{HveT%5߾E%\TL4}.?,0b"LX1&1ϟ@a rQ|SHeW@1?KVsYC9 k'ֈJ" Ao3馆4 +DR1 +@"NZE;㥐Sl[uMR41)~ITL' SLʼ5_Z-$ 7'xFtBޥeIbIJgˠ+Il&b]"BQ$<7IP$@#%%VZ*MdpdrD9I;5W*PB%cRP6*2hY#-K#).(SG+s(ӣbߪhRY MuNē$2(!o +_-ɜ4> \hz1Zh|۲]Y<PB$@c [QaRrtYm;F3%39K2GV^ j,sbt!PRwZYȅ4'(a:4L9%6%F'vV ،v <,ZGHRd$T_.Wz}=,Z6Lc0QlD eM#PQ +)ί&ۭ@ +U;7gN2 `2 +xLd@oZX߄#eLT +-&.c!(EH+6Q1qe0؀EyfsmXF.* +`]òaeTTVH7P;4~ XA&,"T8,C}|!([F- 91}`#8ln^qdFS4sn62Dd&A7) P1o>:X1Q1f:Yk2L_8@Qh) DBW<QTe!홒zD\j- +b@:VxHE (c"ǒ&؂"Tûm2sel?)"HImXFNDa pM +2&20':D9PQXFq; 2Om<3D$2+,Ȓ KSq=h|LUs)j`З&.qڸ6(Fqm%"0/0VL%*I^!mGĸm,%Acxe&Q`2/i2\V"UNh'i`HfDPxk¼7ǂh)P(<`VeS.2AArH+zʘD8V}oD+lgظula91/3H4-.=0 bϛʽB D<6̑y2kU)^.LV2rA\H +D'ڨ͟(!["0'Q*DT[oA"5(S39Vr٣d\4o  eFJ6buqw @rΤɵ6l1VmW2R\8uҤIj[ZH7r2HِN >bG6HD#$" kFY_Gm_w:l$b2dq-3Krhfd`5pȂ3& + +QR(!M‚(y⧍m$A 11M'4AYb*8(eIasql#&(Gж2zB7,$܈`V<kRLZ> ®)n0'GjSL[3V\K]G{G{dW(+ihDp#%0V> 2S"Qq 4NxUR9"0p yeJs5k_RiT:mijj̸8%P2`_7*pA^D16 5 bOHYSeQ!*hbQTBLܱQ('ʼ,BxĶ,(2S<mHzDKr"#(1M%FMj`S( hacP b`D8A \P&ITNHVAEsD؂"P0zW%G+R cn1 A y`#,ی IjVY{ncyGFϥx#ot0dы< =U0YӏJ"\B<HԢ3@y1F1*BӖA|H1F@iPs(T#ӦV[&uF%Jי]J?W1VJU $:ߵ%vKJ m^˔hcE* 46N;mˤĢ>ImXj$%6YH(9ʂ6& Bs;桍`i]f )W$3IJ7$F^ʩajS#l#rjĐS#xEf?P ;9jb`X3: i~HtMjRr*+6K,,JkF@,{)U.5+ +(ژ)a3.˨LH. $.۰+ D4ElJ!NG:V qNJ l*&) \X[J6]reT)6$K#S/=I҈xZM5AѦ$BqMWbmL(q0&zW+c}@A:gk]g)<܉V$z0 2a ! W+Ȣ0ZLt:2q&ʲk˾&t]!tQBE B!taBE BБαsK't=HBE B%t=HBЅ B&t !B[!tQBЃ$t]!taB{TeJ(C袄IH]-L蠌]!tQBE B!tQBE B!tQBE BБ:.Jz:.Jj +:*Lj:*J  ӗل:D$tU!TQBP$tD^'F$-U!TqBPE B%tXP^Pp'tDؑ'TqBPE B&t5HBPE B%tt)'tUЁ*LSʆP$tU!TQBP$tU!dQB$tPE ڄ:*Jj :.Jz:.Jz:.Jz:.JV3mclaJS\GVPV3,VשZB b5#jjl)95#Lw5՜ Kx7+S{nX9ul5[Pj4p'i5[Nj.V~XBYoX`5*5Uh5#j&L[*IK[DZiقӛRl5Y"izV3-#m5bVsjS9jNGfZقVs +'Ռ,j.B6*. AVsjl $YʹiN"m5[$e5N6ZB\Pl5ڍ@輅VsւX͑(>%AEVsbZͩsH̭jN%p\l [Xd5[X "i5q+7nh4p˴˼w!~O"3y55!Dr a$/"wR'>"a3CPd/k;*Z6T4iΔ(2pI[ڔ"(2m" A #YYGqee.a f 7PHY x/^<"eCrK+:e/rc+%edR6r"{y2;3ຑ k7a+{)z7;(Nk+{>Y[9m('4Ho4'ьTP={2!eASfx^IJdVƝV&Օ\(Jmee+\ʧ+StL2)Rd˻%ݲ!猛]dSGZKjE)tG'Sbb6%,c`ԉ_&:M7uu NlAv M| 9R-M&;y5Ɔw:vMU4,0d$Pg@NkGFFKfTJ:Mw%cRPM7 +7< %dL6m%W$R8:ײ8Ή\dSJ+RN`QCCɌ:n[;YhįäYh$[;IvL3r*n٤|nSq?7)dqxMvwvNռ­$܂f>Z" $:&;NdmDʳyiP"`zM e)DEYS4Rt?{oqwIw_}_\X%L6;tO&L={&$_ww:_ƒo1uNս Gɒ%O +SNN2[y<|t[wlmh*x:O7/4$<זx:]"UAm)plj-%G[JfOMc9x쀛Gș2{ v\T1.Z"Eֈ2Ќe~Tij0R<a[= O>2Y>gETA +Aۦfu\`6]s^ZB`\,dÞ`h*X\9ZK,p0Nx bi-R`%m[p!D{ϝu#(#ph.e,5eRZici=# +7$d^Legy̡n4g >+-%YPr7ѩ]G 2 XoB@$[ 1 +-Dfy͌5QvvCq(;_8ytJx0 +-3ZNw_iv>XOz,3Ѐ: $=}F!$4æ7c~̝&葇9D h6G [+!dwQZ"-D#35J퉬Ȑygl%@[e&v!/Q4&,VǛ|"y%d{jFflNB˼VԴrlu5:Tdy(G^#-0?  oy$(3YH5 im.oeuF o`e+n{FSij)Be~9يkJ R4)oy|󲹠MeșoS;A Sr"+]QQ9WR8Y744)6sEn vi,74법xZFı#$D_Q&T3$rrKFO!fsXYgz,f~,+#TqԫHkY0H#  |. %*>&G4|ذ=l%O +\NlT$*O +^^Mk>NaSS#yd;shvpWFhS~+yS,O% +,"3zJdX_X$?O,vx8xKS<<DA԰ 6l# ݿBD(x =1JKn8\4Yb@!O7`+A4`|6ERhx-J`aḫZ4ύ9Sy$j*p?O~bV[}Uz벂oT m| +cm mtѷӊ2n23rZ\ǾluGQ9VB' I>Rn]IY=;nR`"eEe+%]cѽ`*(L7I\.ʤw u,lem|v~Eଡ଼ r&\-d U!R*llt%0Hx[8 '.sv>9Jd`?U%QJӢ*><"&vi[jŐ U( G1F^ER롷 +,]B#u&"v!j*Ә^]j=2UGCl@ bda?|$̗qR;Gg,tAOM[jt $j ꩈ"[螈mEw q/؎/`&pI0Eadl셔H.vXЅP_x &嬸h/ۅ᛹ި_|1_ڎ/SWë4!*:J9:H<5|,"~d= ba1 or THB% ˊ"a9?Bۓ^Ɣ'I rH1 +/Ь ᾼWèW$Λp"psSc"b.iCq=$څu lx/ ӘOTTr}4c 7C<W!%eX\ j@FB(jyc@d&D+ O E@ J?,. b5wS5 B+dv++dH4-" OxRȌu\}mմ$t:u1Hy1Z`_UI|l!4Ni?'\i@",ci"r$[!9c@)<]Nc 01KG + ^C!Q± +JE^Yv4Fص畼;l3JƳǃ30B0KtQmLbdoĐS(C *a\VOL׫z 5ksL"!* ̤VWOA(^@*@ OeRu]]smz\pa̅UppLn3͐Dȓ;k9| -c\Vweeu[b龗(hNIԧ߁r}gB->x}>faah\>mЧN1&D >Ró/D<.K.C yhi + O8#;Ö", Lt4Xhb`|p + t`zC{TET•u\dv v;8X8Ĩ&Ф` YRTG aQe\E+# )[_7}'..,` bE{(py8TDRe5(j'AA4*|\3,`Y .ϩi<^CU`cR~*4I8#%\| 1 |m@^DZ{%*3lHL,R%)4 (Ea Ɍ@BLL$=dk`Rfx 8d 4,+зldjFTNWtTÎȠh -l`ACTP: {Yys^iJn +q5X>y>по,t亜N0ŊvX/rkkLT0"O2K"+d4 +P,PY%4/(q]يI95с񇶰`##bAx.4@Bݚ0"LBԿ%tseL>f!0nBMc=*Ͼd鎅pkE ΐOǻ7pSg͇J.aAHI"fxS˸[dx&tqC %ԋjn+*9[ٳQ+#-H\" UfJ= @6chٽ L {:!J ljzRDVxf9Jyq. œ2K +1A;sby)A` g=3 Gb̕C3Ⱦ츠U! U0.%'6ާL:EHDZ剖 ݈Լ"ʎ$"*;EcVTDgWe#>.! lQ +8q]Ppo8<!3$ 6 W]4[!`8;Xzv̓L4PIc>nҹ*TAA0FL@ * ,rȢhW#RKኵpFZ`N4Qe 1FO5:[J=3h)PA}7aICcGar.,i))#S?񤒍$ +z!@p/jrc),9M<=VCK>` ~n@@*} E^kS[e:ʹH2e$@7%YgD rGԋ+.:CLwf/Pp*SqBxoR57?o@MWɘA< [F@Zv`8j +5=F휗}{:!8rhIs`pARF*o#e {!jqَGÍJ@uP FX4*]&3GŇ@< 3pZ̰SP9ʜAgΰ&` H^MϢP0g5, +7skrmo]ecz EXtٹΕ0s4ѰBax>Mkzk6{[BH4*} +Q8ϋ"""idb0&fsĻ:1 # 4n] +lGF  `D+#΅{Z3Z X/_31=[9$ⱔt%4q=,w9&hF1~ERz[aj#fsE\Hz%QŃ,>zq$E NCE \~A3h"fx齲.Fb^<iwtѳ'Dp#w8Zr2g.D J''_mlntɡڡi( Zq[.WWOd-8q+f`{ +TE^3< 5ؤ"vt(b(ɒe WKNBU>w(,yŋQLCdS\xPwO}_@T̑dbj ^cb~l%pl5aK18fĢZ5z[ +FSs}h"(slI48" Hd{h,#(D4H[ vFb2{#6r̩I{IFq5x"Uq_PNPMyx2/RfhB' t,m$-i\%%1?żDfԼ#ξIi<7T񺼈 Ԓ ̂Ø7Kl`l4t݃x{ty +Map;P;+Ge(s8¾qL0oB;Ϡ ap`5wr/YZ3zH.5IeA&Aκ2愤L`.A<2Ҋnk±.^PD+iPh3DqxPÈRPbHdT49=an=XœZ (,-Ejm)|#|ԞHoXa9B5q2^fR2ZO;Q; B>PI%g'̬i$}R6PȤIO\t!m9޸'a+Su*ߚu 0nZD gt$M @*ňLP#_ 'A ݀hIu :/ nfT4 R,K*pwbLj/%ۛgԳq6x6yz=fƭo^cu;d! 5AgF?]UGBfހg#b3؁-%mn575,ʨݢ[5B1 `NE@V$6vCd-d +8Qfdky +fjv@SDZ;X9mr)H6? pʭ۳5̄l~>_~Ap;OזΉ%0!q,޵sҲ>eWO3tՆh:a4n2b_b}!';??|!oi0-D|kK X8+:‰[^p y}dD7~A/og~OpLDq #\N6O_\"AbC C,Z6KŇN!+Z~kO<`~!R`HNP ݻ ]5Gpiu{uyw^ +@K!!,w'FtP֛Op}mZ0f[zl}<]Ja:>> +eƬ"i*vsHH8ijp"%dx Xo_5!6tҾz= KG®N_DvbOS?l2XQ&mFzD0Q&BaQ FS@PUd7,Jq*FY- 8cI80>ª8NFfY]ڄ{As,T'EOoy'0o +D%|¥4ik2.FD| ~n` ]$yy]u>H5oݸ/H55:$ckD6 7 |A@$"14bz^"'EAdaUWUWm|,\8O[~Z ǭwy#x1,q,ܮe(,Y? +AگAZ*L~9_:sy3cq 󇫟7?B?qgn_4.7O;[w0}y0/W 5^hy#5^}E$ +4JS-$!Wp&owc|ÙK>I@ξ>G/zЯAGVq\LǕ W#fm}~F,O/qnN5wfNI3n'L?M^;z{VoNcloq(Qwvi<9hXO;hhjm{@eˀ̹/1ΨuqY?2u/J}xG#o~u. ͂ VAش½M +c'tHyO? n{-6 1㥔G5z3,0Uw1r1dc:pTo:MB2la8d|EB! yO??k_/%^ϋ2m OEwA7e_7w7U<~8 ۯٝĚpTlwMޝK:.::( vV:LJO)83yOLʻdE-Ht|_3G߬(Ƈ8 Ш Bbj%$k+,H`B?O>soF:r!C,\ձ+a ֕0ɘ+K?OxuKi_vȴvYi&YE֜ؗҗu徠?]|oWx{^ +ӒcXe y |y.>oq)~Ŗ_㳒WS«,idWDUbWZK,|MX}/<{sg W%?ͼw?{ xlElӲdɿbK)QOLڈ g&?-,_r +y[Z?7';fæbЩ7A>D(x>)Jϡ gnҰ1;l Ad^0; r(u +o,8b/([a0 HN5S)bTd"Xy(m}ݫV|`tCsFPX@suMS՚C 8Og +&Pݗ-p<͔QrR-S},02F$.等u=-0d;itV +6J&A@JVHW!a%hU9Y>BΊCT((0AA`dĀ-ÕLPW; CPm!h4x龰A--ZU^s@udFj9Ts*o{|[oi>og?Ϥ?O~~m ZK?Ma'^X :x-,gb/CyggޛX`? @hztC.t;J4 hMuv0 Nq!d;%;E;;ySXO~c.\<ڟ]L:U^dH"^|F[Î50őD~( dæXjHMdw2s2*R8n~) ]R5_cg/~?~o%($%?~OoO{^ϤnO~?o~t~2lD>OНݻ,z|H%V{ᏻލ9PNlJ3|޸iKh|Z_ӏGm`r>Z0[A{3'/Ll߾ 7 7l>[oΫ@7̍G=M$??+ &W1w^-U6mwo5S4L9L}ut3CnH8{+9~o|cK'_6o;/V$M4[6 _]ymAnψ~2ƽ4pJd~wN򖙹||}&uO# [f7GWv_QO{"/:JBN{BT [L'tkC (eD}"3JZn&Qx6zɴ=f;{{DaΦx8 +ڝ {iO1{{GQ{t32i icXñ}4l~Lm֌)Ȃ$h/foxadžBȲ( [^P kWDM4! X>xWL<ˎۓ][l4zN/D;=16Ia֘gtd+nPy{߃Y1[]«z>h.JIdP#c2wLj΢0 6Fe[焜np.!a3wu5IzSY29»w6꟣pdolz'x~k@uǓYnT]&3yÌ{ӏ&m΃ a[=oÎoM߆yNLuۭ1: _m>dc26wmâ%3Dnj'mLm>dnm2so@!l԰A̞AdR6vc!G`;h{'QAxJ8*,萛աkoc/ g/ȨfYX+Cڂ#e2JBOOxU&"]Ԅ%=DZ+"XJ9~^?NiНTnީXxg}0z W%=Cn DЛ ןgQlOrMq= LCw;9qN'[›oEg6W_DV#3z粛N:[45^0A*%f-7Gk3wpd,'5y,Ǵ7&&ސHh֛XaKik8x8:&ç\"bbsҾz#"^N|鄊>ß|c! 5A!“i|ŽTit '] +8^Ť*l;d v  nqt`8ו[нRVwַPE6]=ÏaFԅE@0/懠&l^lPES$BwlL԰CAf{O ߃'Eaߋ);bTQኵ@{sz6#Ƅ9:rA_^ϾPA:ؘHI7~9LjGsȨb5n|؈ D[|e06cㄮN`f1-uF>7xF77N |43{;x3R>%nl`oG;e~_'OOO{};;k:2{FZWsP:h'" sQ)C4~}fe[{`_hn 8)^@R MƥUa!v xOLm 5bkvaQ[ +oms4G/ +iVb'̴۳q^Y^}~<0W>(q^7KN9|c!)ܚg#28&"-xt4{@@=`4V^ݨ'|ONeX9'>7ZhVuGaF5~`Y؏5VⅫ8 6 +2hws"9nhfwKp:ۯ` S0 :.6/ܫ-8!_c?p:d + `W۔+\&8f/ 4`2!y};m O>ThU `fWg p>řJ v vn_M'&Z2FEmsKlQm!']C+?.Db1M>iKK͡<]hH)zέ$+gh's4g;ww?KK__?PsXȇ sp4xɷ&Gs˒ er=_WTF"|wc1l7sjq)vw.ܧH=-Iwe0:?{{Qƿ],NW ++ˡZbTXnW+Xھq7X˴ˋXxmcm)Th7~!Z/}qs\5I'|4[}Cenp#i7gմ$pLl\K܅tQ!79g\X);:Z f4iPMvW9^I^PBvnDO[dkiQ6uT;VN3wpD"xn"'R쩼θ8-n&jKOJ'e1YXhBzyH_eNn=U[If;M..r,}+wft˝˧ɇRek{[$`l*HJyL}ښ|'f::~f +oK~S_w٫[q4['͇C+uk LW)NGotz7t_ow; zu{{ٞ|8΃z?Vã7lpzh$UQ㶕yޑ疮jq|pZV@kKST>>,lwEG+wUvwƚǷvr5^]{N^OIX=(Kr`]OO۝| ޗW!Yd >xPcW8<>zd :'5wy9o(K+J%Uɾ-j_olݿoNo"qJWZ,QnTYZnSv]i$:wzJ~H6Y$r@K>+KWG[M9Õzm>OgpGi-*kU_}=RnFyKtSovb.ᐫX5R %:C{.uqP7*_5AZSWޚ+]fkeYV>L*Iucneᴣ>ᖳjJ/ʧvnWc{\'V/kwOAK-D#enzT5Oĕv`pĿ=4g{ -7ͳV0uK=E4mғtŇ(QefɽVjK<^g~&P n,\4NZtY8<[bח]+ɲ6 ʳJ{r}U88_njծԸgWڲ?|5ݾ]%p~oemup~Kٻ^N[Yט coˁpzѾ SX#\tF烫rkߺ>oH[mE"q}78ġmm}5z|\6\zN`' +˓}vtICK}fy.Ql%CH_\m7G>Zh&kmq};uy|{vgFQ~ަ_7jYv6RX=ەE XO.GAaw!qE$2*\zzɇM-G݁+d6ξ˨eiKy.Tzn-1++;]eTjwv˴쬝͵fI$|O +a^RrY+vR+EBƨvtV]6N;;WPS;Ǯ7g`+\ +;ݾiՃ"Ã=9=_x2AgyDe=t c?~ [jy RhOZryǹiV(o.帿:56+1|eۻZ|#WOעWm+tܹRGuK> Fr_?>F; Bmu/q,Ⱥ|Ri%,NwMcV73y4|Nv:X?\}ZSi4|/EJ4c|-Yt>;T'Igݗׅxwɣ+>sEtkvQlVT_i?9̥brډ-Bnw9/JOdL_H uu;WrM.nKn"|.OT>X˻(Y}_&́jj+4|Jmw[m~U؉KdrP*].N&QN~!Jn~2"sJ\=6IEeR8kd.=%Zr1ɵ^#Q,Mw{i&9غ:n41oD~+t=[\KzKrQ(^NoqDV&ٌOGY$l]f %և˫PmEĊxZʴ}hv|=ҹp9&kB7ZH|jjЎx<|("Y6g]_ x䓊X>57?wGz&Ǒxng|h:'NpYE.Hdr~O2_olr7a,gO'Kg^%xKOb/5(|h˝Zt/q7G\JFcFO +^KL&==̉tj92Od3;&gwjzsc +Vw:*]9^KqF'X),?SqU+ Bc,6=ʩNZ/8u/[J< +3w3s{W[I ഞ`+0oa/UܔR_bdwLF@'yظijkW*QRݻfj|ljBQ_)zfdTh'ѐhIAmm֓Ń/h\OR'{FՈdKE]oKRWq-[W'RւfUyP|^I)ZBeV"c4ڙcYP+wјpS{8=B/&z}~n&.sB$$RI6=fqBޯǧԒ)9!W)#nuWh<ߪVѻNzg\qj09f +g7^9^n-;ǽ -NSj*s6ZAjU$^eOw+rftO?JWueglmquS$cԫ~)7b.wPR>\?? +!Ku->P+j,l3A=>NW/l:{=QsZko%2pD^ZôϷ'rO +Jw۷i%n[1ۭ<y1Y3-J]r~-'zXpxYϏoz+zᄜܹ\gz #O^uUW-28nS~ T/i~9.fcTe+GvH熁ӽG5Y[)7oRڲ<pNVciJtE//gGhBhn/ڼzFґlLf$/MTa$7KA<9ȅl+m>U5dw/FzuYXMLrqJ:2J1QO?{GSPc0M`9x-Gw\bkZדb9wRFtyPKK+t~^dV{l,pt5HV3rdVvЫ ! +BvK,ʱ!RY.f֓s+tvg̉˝z$8ެzQ:Ke+!!oD`Y-)a.?ۉ>jn`;Rl j.Uz⳾㻔2Zv)'0&S\v-p+i_%2CW6F"Sthp]xPW7Kn>{Y:n=\_vvdi||q3p5T[T|7 ʬpSӫ\(nFJihi<)Tڕ6';epv1]^^ˑRNZ@4RmJMni;߼GdP>?O3ݻ@tzXZv? +yO]I|mƯ. Fv35U +l8:%")xẵV?y%p9_fWT(J#a9)(qU$=Q[ݟ T/C2R3oYO2ۙVWO3]<>&}pVV/7+Rcp;םS0۾v^wsZ.^*vwݻ}D.j_]&.&|xx߮.|\0k}DQ9}u1tI_nr }Y\*ߪj2[W+\u +K")er6=ʇ&ahWʅ¹ZǛ٭_&lTb&wXvuPs|i5'Jk=o]U!Hǧ뗑xQ*+Kˌ,ŋ{XcKt؊/ޙki-ws5Bg]>qv% JDuvVZ+bu_ngOAҏ\Doy4]wt㿗 +S-V;}]XOs@1NdH]|pT)w%/kLVbtvtS<o2ʴZ,ME.LrpFTڭTۓݫ29Q>ؾ'GˣU7*9HW,>nFi{ڨ6KgSA.6O;ݻĖSL `q+ĻbH6ϳb\ԮGwI4ҌwFNA?XLt}{(ϒ+ыQ"Iv7:$@}&;";i +rWjE;gӇI>}x$v^&ۚ!897ZtwNU}-NU 8y_ GW+u'\vRZva'9݊\y[S'>>*\L*+2e{mx]^mY@۰ɓ~sq1d2/P&ye@qK͍ڇ|W+,/N qCm%gCk0ŬZcM\V@7~_ E9-[vt\Y@FkBIzhas}w_i])pgo_QH/ϾcR;`=sb]|G;2Q]DψΛz)^CcK'2H8/@'}wKXxK,-qҷ68&2<nE,Yu[sѳmm<b|}QzU!0eO0ॿ˖t98tRdSx$VAOjS_J*֤5KV_t|4'S0 1ܹU"ǻ?2]+31E:?8o*gް眣.tՈ +׼f鿨۬ͣߡkVA?hfb(gOTNܫ5ˇ/vicPeR$k&~yb5mcZur|i]K$85զ}ns奲&çWmtW)253%?"!AC0i[,Emi-rfPV3wA;Fne4  薍g +1iEk0\ B(kh5 Vr[rrfoÉfFw={w;+/ Xtp[_߹n=H'f>y0,OڝE߷a~Y5%%|n<gs6g:׷:\of"ɨ3uk(ADz-jkT#Xw:7UTn^F Dgv40C$; [>\ ^`YeOuܾ><`0}5ۋ(HXJ+׼#͠Ws۬!xe}}~ {ӘR.J7ޞ vCIJxŠ!6.ۧ\HKy6VkBXj_kgJT@|m#"!{cg/cWɲcp[Vy@Ĺ"f +E6I&,{WRņ|ͅ}Ϧze)3UU8gݪ+uJ'v00z^GԔj,]^:}r¬-JAwV1 I} YD)/6=CmNxed rXWIVM+N1j=ppZ ^y1N:bZ.Y4Dr6,~z]gR _<߼\}~ØM0*:_r &I-K.k#KP[mUSioxJ{PXwnT5d&h4"|.6~?ډŏHܮ{nM:[*4{e%2߯WЩSU9"@Oqva?1~Ge/tv)]>?X9 .k6@?fuو{:{keF_{aMsInmk-3Gv!֞{F&f8irR}y,uMȚ,N|,b Hos,{ c +a(0!DhؕZڠQaB^WlΉ.46|~U5;$y$4`dpezpJ zL{(ݎ|+;8}nXQǕUݧ+a1rs,/E;Jk44Z~"[]W> ˷cFc{Xy]8l_{Kŷ#`n$lwO5&6TE" iآvDh0}\rTx|EnX">6G^LIDEq$ĊM>{\݈u*)&!;+^ֆI6 x}uO9* lt"J?Ū-2Nf1yUkom.=_=o\^ϐ2vea +M}TewbNI}r:-iwjCJ84dPl/*|3TߣhӦFFg_] Dmf4Bc=PFtYձd{QU +Ύk@]W},n2`J*{zU8,82o3)7sNY,*t9-߿ni!WRB )+rV}4I/:miܸ޸hV2 +3⾔]վ&!֯wE+H]p9 &/d|Np0>.=QVvb,/Np7tИĊmU5%lJ6y.2|&W֏R7_uinMY+c,_!Q2۫2.Y?`lS=荍f Qz7m4ޜ8{S*%wxeZktx' tMO~Dކ٢6|זG3@q+02Zr .cPbcA,sRjͥ%W=8mm_C@n,rWl"#+)(\ɠRچOGY/Љ3QM]{E[k?Qsn9mQ>vYɘ[T;G7"1I,g\QI5t?ݼnֺ.RMpa-U{NLH:PIZԍ5:΢ފ\p$3ъSPy,դuVK+VE:w6[Cu Vͱ.nU4V!srڐruGw=AC f;,y]ͧv*_./Q&U h4h'>Lt Մ!($LM|q*&umA>s]1[+YKgT_/B]H<4TysXb?>L20D||(Y=(+|=Xzt 5J ;Y^ 㮵BZWaySK?OΛ| t}a[鯎 Z]ZϬ~/[NĴf-R˄[(=+>UbxmdM7|4PwrfN4cx!l 0Xs/9W/ba(Ւ3֟Uyjc8Fª?|1Yc½ w^ ++D;^?M`d*vjJQH#ml}p_};9lkH)9t|+Z ]}#`ccX:yGjx55>x ZH[mSi?,9ĜR x V@жD_w 7=b<".ȕl)sxyƖԫG؋778ߧUI,g@ѕbn QwQ48"ʇBcPcV,3 k~s2*[tJ +Rs騋)S"@uy)}c0t)j^5Xwb`(I\z-u/g?^7Nɛn4Z87eܬJͻ9s8跱Ɨ^כֻaӇ@>v\pWȫ16=tAw7B%7v(RŜȉohc^PrqQ?.7NXI!>J>gppM nX#՞Kv3eH(/ TXcckTA1J)yځd AC*t%LyA|F#ξnOrYnm`ȓ8bQD0`Ks,k\-6(vcx2//h/&+a嚍YQLr*>1+Ψ}tIk^ +tzJ!s5 ؋u*ׇRۍ)o8=djZD6;f?[Oc/yPHnؐàK"{즁 !>v]R|z8gS1~tAlN&+Lpz-q+XXt@餺V{T2Gj!v;_+ Q ԭgwgk>Gt\Y![x>s.;tdQy`Z;fƪSE^ +MoV8MvEۓN]'Mj"ߋ?Cxg7Ӏ_G{>wO#3u'4ts9M^}S)cn rec?)ץ[Ѣo7^|;2I_ԇRu6iiS& i'+{*%V:j0Ɩ7P!8Pڝ )޺/l&ղ05uQ]μA;3{]1e4g~OJlbyd3ETٙʤ1Y{;|Q|ڄ 4.虜 G2d I)[\97K&U4$xuzqfvΠ'߉u>yuLnr!^2'`vo)ےUÇ'F*@ע'=py}b +QjW5u{Vht*Iay$66s~Pbb6UaX''5r/V$rG/|CQ8uqE`UJ}wZggܿ γgϞZuEKK;s]b]i'1b߅K:K}0FImIXԵI']XH|3F.2ەiN5ORCJ1e g&RP[1"*ʑ1ʸiG+)l'>I b:BZcZt`ߒ!?s~:6O'Fju*qs#} +5׵Ryrߓ\* =,yAQժ/gHB|լB,;ÞU:dA(A3ѦJNI+ lKu?49n-^mS{=8W^ G{WdS[WB (%Vƪ~ [}Ԍƛ(j_Q vڮRhd}JlͮVA;EI4rI֞Ju{! 376ӛL^Z.ӂOk~.nhъh D:((0n;MO@<㰆Cj fn{0$0[BƇͥ|}fkH!T%ie0lŢҢeGe'obAe;v7at+< Zj7Vx FTȱI?.]Op %K-ol~)Tr-25 4YǗbBznG` [Sޫ\SBapTZmAH@n*MfnO]gݿ3Xz)P5fnmk}æPc :;wnykߔɱ4@"{ˣ͏Q37aUc1]QKڎiP`1g@[bD;.$;otFR)y!)5qdJsru9^vf€?\pk>K|ar ++Y7ZeP-ܸΖ]YᄒxH|0X&<\>gQYAd7XMuz+f;,םRČ.-aw8e@Hb4]ݠ]u0 +*.SbUW$z'yϯ-WmʟSU +*f<YUbyL {¬~bz{3|D 8*P:M"oMZ.Rӽmp%jSZW_y$B1 *oSx~}=ޘY5;`I驸d _-޴b/ύeWGHK G r϶fƞq}L +^ +chbe +^@EޟBB=7dR $W7 +euJ/@Ҡ w +1;ԯc58s3ViKFvfgO/bT Y.|0R̭)5N+oQ +U'OM$ZXO +)P_sMװ|N}ͺdw"/GW&z"5{%y.XE/%d~Tӆ`6@4nƥxdCmtxn5Y`Z$,9 \31ȩܟ~bИ>V@ۉ={zV&Z+w,+Asc‰W叭\=(q`/UKI-k 6Ǽݰ7|C6}xߣ +&+q;Ol8~4F] r+|}pm POH&m>0Q>걨ͰےEX l9T #u+z|T-!ݻwα*AmrܪR~X2mg"GYh J9y;HXm +>gXFڣ4AѠ-a$7Yjr*:⳺BEB(~ 8-hFJɬǖ; T_FGhoybe[#38Q Ak͡K} h ,Zb:~).𖬛dve{z}ţLgJi$dwz9= u ̔KgA!uiR8&3)"F LΐhT GJŜ,58V.0@f,/gUYoe \c1G<^g2;7O4} +UO:nYIK ]{lwZ, W˸YqG8 vS#tJ@ +>'q 7cw2K9c_s==!!es6jWӥٰ Q^|vt}cL!Υz"@5Vlsdqkwc:g'H j~s{nOzQٜk fQ7ncZM["Qsg"JSHI2\<~z)T$󴮈Kk}(wʄPu*EXJStI8ַ@ϓUJ9- St{b_ZMl$л-Y 5j@eeF9_.OeZϛ0 nwyu#r{{'L\"hzb}fr)8+H+_+t>x\}AĿ.g\iYsX ݶx&!ܢIwxt i9EUj)_OZNJ25'vϩ4(Xp7d +5[ge}kDN8)p3k+=AqFD񐙄{^Jw|]hNU8:a/GKys͏K+*9Z|ncR~l;V1x{ƀ^ `&O)byʫ>5.ЫJ?vӿoIzMFx ]-:Q~]0t)G9i&+%ZLj|_C鐤sb wrH + A MCȒ4OJ8-i',/ =IydSR^v<@%-RNgBMtJUR@X<ҘTz3QarUOn%] fH7`}?jtueYWs,ΣUrSTex#~#QK̄8 ؈3);)&`Hz~ 6o 4$wjΠ!Jϔ@vR(ٹRx=5u\| B+w$Î?%}$`\:?H;X/kNC"镢[Rf[NG0{֠ j(¯|[Sb`2@ nګ +\xo#Ԭ4֗qcJ/ +oID>7Tb<ԐTVT4ހr)Tķ+vup:VFL5&}fa )9WE 1K3\F?мӺ #ZU(oOl\-V}ng趣Y|Gs"Мڕw;;S;: I9cdٷ`0HE{x) W9]"#քDyjO8aU>, -%)e%塏udk5߯:PLiϟ2޼VDzFtuXmX3WYS5jh*}jXF{4"]C7*O)^Z*zu!Ʒ^ӷ\ +FCsD)MƱbXlHa?5e`{t[?fХp:e?&{I!,@JoNObK:r)TdzډVWsRI֫F~y&E/hkt5̏oz32OI <Bc6|\X+mmEzgcI_uN77s0t7.Q}Z2mph jVg0W}~>% +"PFrAbi.@?T\Kxľ. 8\Fv[oKG7g`\J[Q^^TBM6cRQJB셄Ѣ1?L;]%DXͲv0ǣ߸k5S5KZ a~RY.^c;׻Oiu1P;lz^7PAWR)k%M2BJOt19c֕-vP *T +b fkFp6x[ʡP;+)u`lh[+j7a5ir5-gS @6(e23LՓd,)üa5 j&ϰ\mM=+ v۟. ǐ +:9n?E̬cwo]۝@N@Q1k:W>qϭ(jUhPUhcCuB;h>c*oHH Yiow2`)FZ3yoso"( +Lm/Z JI~뀴B6~*%y+UU9Y5NYZ;&W߹4l`39gL9^3{>`R$Nwt(XF*d&6_@)0OxHUWLoaCf}dM5 m"JDX#^>ur(S"7F6 nG[ۺN䫮UtSo1%!qX`_meM̻DVik< Qm*P'2IـgF`QaI~%>G}ypA! nbu$y@owmIBr SSi(&Vnx[n #pzθmz՞z#VELӵ]/t쁝:JdsT{>62oHPQ>KWZTo+,%5t@:5 *u:!WՍQ0lj΂N Olc4 xnr*ؘ51ܤ$5=72XNV`1 VbфCBNCΩ&ѭuŬlwCXco}Y'v?-U_zIÕs޴ۂ^ѫO[C?q 9#5>۴uS,@M5-7 m^jb[sSJ]i][g1xˋ{ʖ<yX̲s0<-O#O҃$b +Y  + wnS57j +s Yi:;{^.f%ء ؛Pg-ejU;il)rs|HjΑ}BwAdi U/恧Z1vy^MJAc(8kX8z'N +GJEslq=#Wf~KZ03\ACAh-oq[:Os\=A2kéuYc^!7U*2W:^fye"8Vۜ(vJ#BXk3y" .f|wTϤ_<8N^"ÎW::{9q}ԯugV ^G]Z>UQߍ[L%ZoJHzp&w-xWm]}}i}SAi(LO:Ko@@I56%R [z~Un7x` +H#}i]W{0!V~@zTTs$ƻ{T + 4KT$3P>Ț𲇪hkud`==q3GCW5$ӫ2enssfhu$;w%f}Pg*iU(p +) l^X{N-*)WbCYi=-"z>jjiמ،]z;CQmftZp=OtndӚ|\ϬWF[=F޲V:#I}`fu&%! ikQvBw#rЎ XWd21Z\B4l2``_!.cC ?-j>Mhw9_+ }4kۮͨ1p go䒮f +tnw Z}4Z 3m) pL6KaIqe)ѧ + xscWB2Q!5;A~Xꞯ6䍶d ԽIm8Y1f !֊[3%xD3R6,tѺ4)Q7= O*taڝ.1Wp]^rB8(6l)/ +?v+7q4bj8fʾsS#s1?opz;h;@%rWܫ[Sgl^@]u #tjtWʂ;0)g"dꫡ/Ta(C卙w0~3Xgh?#ePoԬ`?Zڜ^_*b}X'G8յt<,knQf'oe},.3Թ}* ݠtcC +J?ҦJHv{m+=k'ղwLZX-m)t&‹e+6W֞v<8qq=zk큹޸[`կmȰ +^B| mepP~7WIQ*%Z TYdNk~ldɒ5 RcyEBLt>^\RfJ +췞fX2Nvu.((:5(bvl;`=Ǽ~pm2iAm(yL ʙwsӒ?DәOqar|P1e8O1O˼{/K.DxWhfO?{do<Nνt aن++^kή貟}^BvEU*>ދX$tѓLjqüG/:Su#[rz.!kS[jyx 0* o E8^% EqO9l7+{n"RA'(_ir\yٚ8>㸏0A,OvRL?eq>K2pgrT.Pv/j{YBse1q.XW-Ql|wمՕE,}7? $a.Rz3Lb鴦}yhvBY&1ېQe\ov=?䞹:dfL 4~ƅgzЇe͐J'i!k I)Rͨ`]_40{'C!3:Kn< <ЏoXVt>"i +d Ъ*_7Xano~Bu4>xoYAO? ˾ ctEIr5UR\vT2LZV#ӭ1@>=&E/<^+\R;)Eړ'8Kզ=e{;?-k0zP~3McճW-0;pAw(!j/A7C/gMFaдgǤ .9b^ı~/A#^gf=6Rqf oiIEMkd=P&e-͹ѫ?.}VۓvuZN|V9H TS*wp UBi%ࣚ+ BT#U%=VNۚb*DrOE`{qjCq'tt 2!+؈V_#%_yˊu'HB(}T%FL~naM8]/vv*0*Fަx2IōN`!m |JaFbqngG`å`YTи%-uRE|ͼ%c}7B'j]5ZS%|M@4lvcAūn6T5.ޣ^Cܱ쾰eʋT]'1~!±9Vlx|,w} SZSqXh1 4iPsJ[RM*~T"><<ΕNU:E^㡏J$A4Im7G҆W2w (@_TZ*EFD esY+/L%%luu;19njnC$H.`Ě7eӓp;5F^bU/U?w46O2GmGs +V1*-3Qn>U@T%O1o;WݻWM`ۀn`K) ꎹ(J7Ww;reLSv ȍ}]~#Kv34cm&oSP,,S+oT2y>HqG +#E/WxǽM%[?5, qV-M8a^-Z2G>i*l= I9S]P>eWy*r,\>9ǚ|2p\669hLVAc  RGuEjT)65a~%35XڻfqWtx@}xzVO= E٦fAQ1MyY[p!z:OѰh4+3lG3sWJ\vWx3 1Z[ 9uI^|M?s=JڮB;gVūPµYRRӫL[!fol l57QO7hͯEhOư=hJϺRBQkXk;l|SQ Azk/ (*-8~|L2C'Σ%a#,xOFptL:RTI^z| ȝggB‚Fcz*JW4"MètT?sbvd"od-hlHS7*Qs(BӾ=4?QR}9ӟa>/m5d΀ ! J㫿VwVy;"[J#y}Gm`,VkxՌG5}jBCLrϠ^*W 8ϳ>|mUZ< @r.;a"uj<$P4Uݨe3L\y_̾]O'%فݺ/ #4ZH-lWFmqޭ`A/.:I*I3o`8^#蒯|MuaB=眪?3p1858'LcfO.OR& 9'_?β +*G~߯=_ |dYRM.L5RQl"jCڶ/toS -xB/F! ފNZB4]ICc;4ˢ v>R>J,XwMI1(%PI6:OCo_fƣbgQ@_];NLy+Kx(6".P)+ةvZca; +b 4GP޶!Tcx^kYUF%4l׋7kB|2ulAdM_@ձ)=+8"70=<X(ey[Z<jL֠Aڠ +ZX, +%$֖~ߓc с׭˵Z/G +]jVUU-=_оCpblLaIu>El,t3xI%$:vb&w_j}8ޏ{>7}YUY;عaicإӪAB +8o)ٮ3|<)6ȫXq8yuL4\TIk%Q~, +$!bYhtP#S;X1kC +j +o% 9ۻ*rB_yof),5c(UFzw_d)57hG1*Ń$8d;RͦyTNO-A1ITboNBwr=^8mf>_reu)tx&/?6U=]BH(Xeٕ?s49WmmcP_ $8 + gd5]ʿ@|!m0M_Ϊ@Sau2SHf)~O\NY> erFqF>w"`wsU 2|ziڥ,ȬS-1}CF9Ä>v *U=˨3ko۔˥Cg /7skN9 RXwd72۷m:`@.3E/ܮB:dtXU-?>V_ Xcȷ@ЍW_0?-VJ6?lFV^v-kXz\ȎbuY"LG&q7k| q;w9nc!u^xrf(OmӃ |-+gOZ|DŽ:{Keo3ګ^X҃ rBKrQ zԉITOJxi_zhaL|_sAi"o$tI&,pyMKvY=rnׅXf9&Ϲ?(y&^cSGۗNps(W6Yu/rf]E>~ m/=2V5IGz2NM'$VOOM3OŚ2Ő-⽏8 w2']mHžY(tɑM*H*I/[RD-lG3jZ2,eFItz[W?<8ڦXxGu/e *.N0߅IAϊ7{*a'ؾ8NoU;iGYu>F|d \{ARc|lSJ!i 1faRuC5d&W.'v4jRNhPKl .C9-ua~-b0벝l BhSfawS'OΞkPx|:WsnWt$GvSk*-0>LB-qAx͹nǔ"+dQ{Ub>&kd O7N@Vp%ɶB. >ЃOI˳%c-m_2&.ס(aZn >}|^g~Gm =Bw]D-׶a_N;O"{Gqu( nȗ*>6 5[ہUF5?{a&3o%ϺBB}ֵaC<ðV F.JBnywEx3e;Ro #j#2Nmz? +Ͼ*D"5ĭ#v{oԤOLoAy9M`͐ZA-@hɰ~Xd{5 ~OqFO9Yoڧ},tgnB^ǭ;Y+IO*+j-br!'7|Ⱥ^%κWH[5_8ZV5uf(]S +c6gO.s- -xp$ [ו\j}`-ZLiShy-~4UVEY}K:iRmXӼ6xCK?JmuCR+N&GӐ!QT/H(?*Wѣv3s1哹[|$ | +߫ڪdQoZYV|+5 Pp G'˛ OM<)Qޝd9݊a+ŤBA ?co*`F^_q%+,c.AH;gU&⌬.~C9?Yjjr gW3uW1wKKv-1fJ&.[(&&p}íERjI#֢J8ss|&z T; '_kN :z]h5%VX~7&0XE}N{R +c-e.LH4ZY7yk m]J^`nr)o(+î +ӯAD֢>^.KG;!ά;J Fsx~34nUo=zUqAi@_͔ 4ܐ"!| ?[5_+j]#}家 )H}rW$6dPZ(t bkP#.;}M;mwԶ_{0t +~|.<%VHוhQST?H@R鮸~NRmn~(ց,ݪJ=M v抭-u7aյ"n{FE8˪iGkv2zG5QpYh}\7ְ,nn^Zfj^Ԏe\Aʲ]g4g}D {hlkzﱗS'Je1@g@wm[*ƉB6t +6VeKSX}T܍kӥWlmMo[q|R-RޗXjU"~y7rJc~+|gmyI㏻8Xv/Cޓ}ۏ%)v)U>{P_?lx6Qij[yvQ3[wP.HE7?/}̾[Kru'1>Q tUAWK^m4hi^ڜ3g-,bSEJ<n]y%lr8pMyuMkCJ@ E/P^w^D1zR޶`Du j=o*iTMa@ʢX›Kwj_V½Jv'+V ,ҚEzbX3a>yWc Z Rފu\+ɓ9T3%M{\ ]Z15_(q ג[[m{N0&Qu=ms7$W3l i0:]Hz_'-@P<M_f<%!.'cFvF;v)CU!`A Zqa\;<" -6>3o`#,eL86S +FLilߖ*q_b~:ώNa2 ML )/Z5:RLq'UZ17n2;ҟ8ާ_mcHGZ{p52Yla(%z@ (.m=]rEyHuؽ~.j`[ ڧ:;?v_23ᰓLIpk5WzXuua]9p.D[R3uyk @VwgBOsQK&x|8)LDU>1^̗tP`ujcϩT;ų}ڍpiv K޳XoL홿\cV,mʮUzU XF(PWT ՗ns[\ OzX^38F-havDl6޶'c͹3p؝PdP:)>ʕs !S+v5.0LHu0h 1~QQEqu3f׷7RX +̠Ez8dw=?mI/Bߙ^_ML\>XyմIZMbQx !bZ\PS`m?:G[ͪko|Y g%loSmZh.K;9>+f^~_(зߖF탵٢9K\ E/@7d7Wu构 t!^gQtoߓ! ;5dwpioHwԌj( v$~<\C.\(L$A-zXdUdwڻ5a|"O;T>97>Sh\}L +QHs@2°/l1C%Ըv^!dz$1@$Y?WR8>H*n`֭bR={M\ +?=.gX4RwOvkQQOm U%-\$f aHGsYժvo!5w}~1:SDQ7}|GoQ@4|J"c~ }3ˢݗnಞ zZ*QQ }@V."&vkwh5ڣ_`‚ W`8ހªYʅ9\Ӽ>XOe +TaՄL* Iu[CYSe+g=[ċw'0{~_cľSU'*bFA&pE18ɸ:8Cy} #V&B v-w%kFoQ|0k 7yNx?رm+6"ijܴ|xL'ń1`2YuStorvDM,*iGsL=`Bց!~qȍـlnvS83#(+/}G7KgU3Y,.R4nsQkʴ` +PP$N}T䜍wŸ&P|fx0Ĭ*"89pߔ6Ӑ+G~^WF+o`5*{ _yO_)=߸C%e-;}hǬЗ9 /#SvXgv]bRYlv}?w$Д}2n/s5Z-c|.jh"YTM{&8e*zKBB۷ +_n^q\nHZ)< 9|$kK;Awb}[ѥBu-X 5uPҷ +^D|Z7P[h6fЪdAUkQyq7)PJ3*ȍr4rFǨ,~.@[sܥfdؓV,PsvG}f >* E5ai^ꈇv;bX~IbeB→[sgF<Y(a:Jk =74#6/#|^n3sr눓A77Z~zbN, ih+/MF0!ZR:,_Q^: +.0n+S5( dORar-rvo6i[}8ZGgg_`:(] w-CU^ 2\W}RZ> +tdA +m'xk%JpnN)#C&+%(Qxc7/>m[N/ uP[LiABpWa .*ض9[z>dn)"4ԡYLma{MOCN|DUlHFdkګɘa'5)5S0p4%n&ܔeUjYnf6FULdo=M΋x?'L.GJwyX +BHi?PLrvw C '~4FPW}9)7 1WZS] i_h V}EYb-޼MBa>Zt\R!xBu9}մ.}7.'bs[2&ȡxwǒHhM=z;GGrGpno51qc//l?[@tҁA?wⒶ1}v4fDUhހTj1r ^уTM`q5,w{Ԍ) ^<[r6 s5c`J1Ho~E3(_Һ5Xjr.wYgܐ1R. +VYsm4 b!(,\>HˡUw7VqtA:F!V0}]c!9LvMi96~m 'E"6ZvgM @.^TPbձK]8}3VYt@q_sÙc VbO&9sX%&6V|mc&+<%t)nֽoEgqRNzB]z =;y{S&wjpY$k8OmI]cK㦞-[s՘mY\_m^_6B7v]Pр]-l/U&RXn]u}>V!j}msRQ/uj>%6he"BChИ5dB𿂈'8{hփ(R=놬ţ;5PE€omdņIc["(h@ƒ7RNc +"j/w xl:=g&^--pG7N`[V&^GÓF? Zjg.ݤe}I^]Z|XNQn;G{}Z[!1 :~7؃*Iy䱍urt$JA&c35C#n!v"jnUl"e^bx4/NV&)?PS-y'IM@УHC. +/Ž N>R"GKׄ⭅Y;\llۣ ,th^3`0P}тE冴n/lEKrszu +AWQcsteU1+F +-Oq+PY|vڬPkh-M7j;IS'+HRV +p("pLY?kvUH*Cs) +*mP/A:gJwq\utiV %bT2腾 eعP&|Tvm +Κs}TʦSޟ?2Va񳜸1 V*}Vs[Q.`W~bI[-OJn_(c/ZG#E%c#1[*b}BW3rvb~Nm.@roUf!"\gF%okնՒҰ +M295ˆq;gO +^{6ISkTBՀdd/ፚj_Cpo>LWJ)t#9A*LRBZRF*tV7m 7ka5P\,Uqe`a.6 Iߢ#^O+1ioc+`GM [-bB>F)^F"Sm6ۈVB\m] pO?`qj7BGk'QHR ?zG +Tt +E.feOm_c +;V9ףm +۵\?d **n}j i쵴h|71Itϻz[@k>sfEr5;SKiW9cCd[UF$ )nڕ Dn?P1C?:I/lSq+_\a-tiNiΦC2oDj/Kt9YYSE:@Ôjrt@gTPWz(2'X]hl]qv8n;ZBƙQ,D=~OZS[3}f>c=UTZ]Q+s*{ȋ%3M_Rs*t&0EuE(#p[ϛbXE;PjI+Ko~;Oc2n6!}q(FeNv9 Ec.E*6#-tb_o`c8'Iwa8I0&2 ;Q/,ppZToV M6.9x9zvtI莠ꐷ5@?ZϛV33RtpuĒ(Бbk-@Wy¯KI-ךC|*RYav6ݑmv'YO[wPS=OHhE`5FΠF }W&uglXZhX,fѝ'Q#PIIx^o`Zˍ^/}-L}Ʒ?Ո+^ _J2Ut5?heQk /[aYq:%WK>_$RIa:BrX< jpgdP'3q/F':}=`E"F^6M~G$վ%0ozT)(mKa֥ rTS#nw *LW'ʧ[]@*Mt;vqUrŹq$؁}%Be_)1lw' c4gDŽ6B +%l;. HN,L:0eBw +O4_эw +M=XyC""'>sBj3an/Ey˴u^1ڃqff `δ{*GwĢr#ۼ@<-Ja\G2=q.+{\uP?e)7]5;Bi:DƓk靇NM3nF2ehMo<Fߔ8g9ZV]Yk-&a gv{x Gݜn»(wc5RE&ת7L(ȑ]74X-E;7T(w&"diwFn>?[y%#~}0cBio2H˚icK.KQ)ExԾ6A lb +%% endstream endobj 35 0 obj <>stream +Ԩ5!B2X{a-c{suy6\ 'J 7[ G'tՙ% +kNpѹn@7H-+w6iV6-eJ̯NFuc=>+ i¢I7_9n ];L9P{W[2Irjn/(_Ⱥ,9V׉qFG䎾gJ︓L?u<2o7oR!]Co!uQ/,Yhu=,>}}z| z[3Mrk3+5ls" g=C|4ŬxtVP(&V>fr,w6!o 虢#g :vɩLLPBLG+sիb\U7/kʛPAQu7v'f& cLj-{iډu۝P6%77F[ݚ-n35 )3 ~>ӽʴs8S=7\8p]~Q~CV6kf{P-ؐlstP&DXDe}<4Dy\Bྒྷ й*,5ł6GvR_2E/m-k"k-Q,m UZ'݋w\"x!qAiEБ]aT)ފ3W&T+\!mFrBɾ΄UEHkҦ4e`2 Kzd5^N][ UuɠP0>BߜWKm^D{<`7\rà aЯپDDDW$:H*M  Sbe:SfH_quجÚqJZ,l0׹R:Je/BN먮U^] %eʆ }vUX_oR`3M[pqtm鐇óT IkxAir><,>?O4%g<+Z#,G_'w`nx`UWnq|a!΅k;/oz"="/, JV$AF@d;J(&-V/-7Lm:P#Z1&ES=VAkў"3G\,ml`CgL('xZ8:H$E\z>{^\j%f_ ':Vʞ{5c?UӘ"ڇ^sic62V`fۭnV,P1˖]}y8!v_tXV4R\h]>l3Y2=lx!ty=^>|_yV,0f4ُ`/)T,nSC%\D'p7QWrnVR?ƮuYP'5߈ sVy0;Z]^6pޠuOddxrxꡀ`C@؆tMtˁ^뤖'W 7Eto^'mD#aN%)ky?|d1{X( ָU)uoc[p¹N=`C ټ7gXφ0j@T/ߪ:-7ZѺ|4\j.H;MK>um:y01b!Adc$F-y| ІUE54+($}V#`5L%8FezR|d +63zֺwthP݁]SCЇWƷ8- ǔ؁ӎڰ2*zO1B OUTMƈ] f04M:@6) [m55gV,s"lYnxV/lK_Zdt>њupS{܌!jxEZ'PJ_gߙ=>˸=sy J^[LQPY8=CXfYps]ͬ@hj;ZݺMz`2ѮQOvP)@8ZK37Tlw'r;$I;t{a:)ٚlЧzm޻hMOvps'K.HG|pI<:W7kqQ;+ 2kM_^&Pz˛1W |f\!:J5Kkqc SSXkuj{"|۳-Bw)<ەΎ?"9֭|者͹}_7;E墓װaʸt%vxk JжFܫhQOp"vpV 2!zîgR> O|4{ӆ5wKv*ݛ^ˆ䎯=kk0y<\e.]FӳX`_v]y]MCgv{*OaɽΩWqN{h_{lI?vkSK( [O.gc1FБʦ HCy )ևSK?.>2ez{J%1d,wX&ҧfsQ6/b߶xWscUFcig5biL 6: #Lk#B6Z*Ǫ =2d姕Ognh3a5>Gp-S.|s zxml;`no6]h?Ҟ$V+Zы4S{BO!t{{lgUoH[f3^ ZW'u:2H$ڴ7FV:;9[~~ V"=pG 2E6e:j32}6M!/{$§}8| 5%tm>UXzf7=6цsnŏszp/3W-<ab\ɑ9&{kM"\yRe*y+ʭ0 ~Pڳ?tk3Z(OGPwhѱr"`xVÓA~Jx%c#{)vw5`RŊ綽0:i/AQrlt3i܊s4@u=.&C|7Z +X_jDDk]lZ}Ic3_< sÌYyuJp50nj5NyҺ԰{zϫ<0}k=b\$d?T47vV0@O ZA?m\]Ť/" $o`sw[|1zK% j,O `'6^%2 fBb_z#<)^t"A_ξPfHev{T΋YkZ?]taŢ>5]Iw߾::Ko7}am3ui|6ufPPF;21L;wVW w~*q]7}Kz"B5`vwm[8(K:sIC~1<#DjRheg-w(2x5VϺ(>d;BCm!0εG8Tِq|to39W^Qe;yyU"|7Ut>')7Ö竭9P{fŗy]O7P*J'[0#Ёb[k{q!3(DQ=H=DG0t˗1DYd662Wsd!_?y`8ܸl_99荝-fo+v̂ag68`:G:#?Vg 6UW:x'6gFxԅMY8|L Oo4NyZ8&F&wyjm <{IݽhRB6+q*&(Vƀ.}#:փ*;N3So)`^8>=luJn=iKo\~4ytLr/bꕟ caL:̎Gΰiy Xjo3g%(ҒY=e2מ.ý nkuzqHb77lRv] ,Z%e&ipDz7s =ƽ\/Xr{3atFV{٧YЅ .g@IkZtQ4;׃h2^nӵqYfo UQmnu{h9Jkg 'aZX+nSYUʹ@CnEW({43lFYvp WXt K6_-$nHKCm>%0:[A7rwl]sϗ&h;էBlTEt5h,fSŷx 7~ЗުnBNƠ헯Z>Yߔ3;cN$pm 'vݐ?f:Cy#"+?㍭)s˷.bvZgKӏF*5*^ՕD5o.>OhgөgCcҷMD)[k@aVPvBSD4n{"^-LKUV~kE5e*Tz[NZ}1r[ZSvmʵ.S%hm7~oK7k0ȝ|b좿.փm'"/Vqg]uWeb<Ʃ[5 Okn QÆ q,'U_ 8V-P|=snOpQ GKF ]屳 sVߦLS͋7R]"2R$G݇j=~XDBUU)$j^L s9ƑO?©dW%|梾Gu3uHUTb o{ds@ JKQ\Z~buD{\ +ZqDŽu;Rf5 2s Kjuo :\,{0 +ᙳ; }S_a|B)U}`WW"1vcmKл45Yhw(u7d᳟T5(d|"-Rpr5+91,p Q\FLs#@@耕ƻ{lj % +Ap4FyM!Z_ "|?G0>Vgڳ@EGĽRd؞}d\zẍOntۯ*I\yV<w][&&mU/Szorva}/p&u8FX̰m}wg+6ypv։ $%] >To/ǛpydHww$S;a_ +r3+K@9Z!ˮ,{*#u2R>.棯SVLkmE!b#Hylwxݒ(m50kZuс.QYՁGM& \=5 8It"$kdðubZZOO +fo4w㫽[Lz#! +K usV#ݞ2 M +7bw~3 *0#YW mkΉs8-ds2C*vj n>߳UT~Qh|5|Ҏ&ї^pw\ꏨ].kWh@̐14LëʈwըBvI ϔ.fM+cMrN3PѠaSڦJZ߆yqrfF X]|,'>,NCSbFO +1NenXIղ ޚV&{+I*Zm#~=XjVq,t1K~0]QiFZ[slE s=k/o?hGsL#$o0V3$;dJ[7>l1l~V_c1NQ]]lXbT;\o4>Aw]kR]\PMd#.M+ ͺkXq:K,~RNփ~==6ُI{s, (j5ryxW5w%Nq+@s(5(a#p;ni,<;ӻ$&8طN25H+Upi1GݚPV6ߪsL%u͕y@1"AjevvŜ-K;Fht,y쬝_O9N5Ky\}6-ano6zSyDl@"+-|m+)9^=_YwVv/MC˽Q7)^.Fz[u2R\&Agqw]"EC(nz +Ugj5`7ǃy\)e 1=MlSw0^V[TR <}XJIwdqSx{솓h'D2͜ka5Ys/]=thW<祡W],ƁjzIzwYޛ;rhaݟm۽OZw;^wl಻w_uA3U^Ķ^}=^؁8K.AFUs0Jv}+%Zb "Nf{!9,_vk ~s3,9/,G26UӊR8ԙ0ۍ{Gi혱@'EqeZ-mwݽ4Y(,:b?H[E{ +4|}ooa)uLˑ;M*V`ϳŹM}g:/=?x6߅ω@ۓwێ>m_ 75b5$VKn9.m rZ3S9;?ǃ0rM`1WGvOņc;!;c-GUo}7oZV5\PMEr@!]c-ՌKF[ `)vdw`Yo`9p5{Rj0w,\wxOƓa@P.ƴ1_݆=OZU-l,&v')iMS󂵘qp-7;wB8@b=3y7gYNwɸ$TRTG̴#ڤ,l)57n=~m(E3C(~—O9Q{v,l{kAIdBYCʋމ|dmrX#yF y>L1:u,u)f7fmM5}1lF{~GdgH=w@oQO4ֺ]nݔK%/IS]ĕ +dexJ]/blܞUet[*k$5-A#r]ξKu㒀>+a5r%,.֫Z#9Ȧb.[bԓ 8ƕZ5|\q XVm3*ķ純=u0=EQHM;TvoQqWylGϪ F\U͟dC?H>%N\oڥ]N2rvB>u歱s +ݠ?? ^ȸ tr,--s_X^IMG}<>g"l:,9xd2xiQŠ?hz!mj؂%|#-I} מ?|wѠI.5QoL \?C|BŰ_uH3-reGEM tq` koK> 伴QaX3_k:]Wi1s&Jʳw^BW3`$6*AbS^h\T #lED)'j|\UX8b8@ORṵuNԬi5,P4@X9bfAw[t]כT Gu*M^=|&7xU^ qlJQjeo8}Z!*­% >6dĖ8n YNt(D02'KҐT]rP~M5BhNtwy?ՏO>l*TZk\N{i}ZN3:5d)ĨO5z1KʎpjƁnvo? n?/fzp1t<}#x^>N ji;y{֫\m:s&5$QBr'zsBT܌?Li\7Ksm&+ۆټX`]_$Z;jlًԵV5ܼ﬑a}!fmwU=VBmY3G>Nƺh3G%yAfXVe~/Rc0HKQ,lA}c;O9mءz6<2X̲@ڜSp'iK4~c6`OCv\nH:FݢS|˱0KF4O]yʹX;6I^V5Fe@JPkhp7 ^L/ 3v>w.TپӉP6~nIׯqW2V=chfXѬɶ[hhsD;xʚd?MAizmG7РɱL~"g,9nhchKl@3擲 +2n?j%ݨҒs(alAs|.\O"_\QJM<=42$FnZN]aCZ#@uȎx֙^_*lԫKUD? vO/S +?lvl# g^3G9fvU5a \)/vm'y6`.}ǽq=L5,XY,/o E$cT^Z͆azd-ՙh6isZbbtͱIeSxI@2֌ vAێ }Xoo+;_X>彟>ImhY  +o#<1rq`FoW_3⭣i Fe[=R"aOs\=]aPK>]m0`9vU. N܆9yuنȬ6ꝏ&rT¿Uq?vDEVn2+j'{~IQӥӼV KĿ>6z2W{N;?cO}׏JdO֋Lįա{\?fk&ƀqx\yf{“FCBD6 _=1X8 vO:WrYo8wy])QTr6C}"'T !)pYqCL}\:yל_*bcrk7VDm~PMչA{B*}Sk]k-\Ĺs҃E"Zib*Zպ^4gc:amtUJ F=]@&ۛ +yEru ,'`S@[uSaI1*ȥYkp(>P_)| oܮJBLqHcep4$/.|K[yv8a;KcXf&wQ=IHFcAk׳[rD]揥a_-&(V/Ʈ-vN0wXvLZ(y\lFxt17}[2c$lkue]t|\!vRBZ-@[dY2*M6{/ښcYT_F#*+}IJ)/l_KHvk߫=RWM\3ǚҿ@lӸ.v}ԳlۛcdG{:g|yApc[3 E~Tg`~EV{s5owZ/ġGSyOȞڵ7sO3`& +l^gxCX,s[ >"I9Y&&knvf?o;3_n4cw`ʵXvVͭI=#Mwg,bҫZ"t^1o{!J\[Mfic%%5Cl6:tlּEQAYe&2 ̐% MY Hϙy(Ca 2dsj<=*B~`;f 1oe6~lt1܃]3gM( +l} }U 1ս Z%C)G<4ɹ+ؾi7ٵ?(ts2}TC, +!ѽnYMeijeUƿ854/$ޘM K_t3 +Z)7}a Պk&_̰.*znC<^TAe Yee AMSᴲl;YJ};B 'gi'Dl^k xFE5ﱉ^ {ͬm/RTm%끌L׺]b:^i;M(A8ka}xޤV<@O h6%N^- <."JG3vGIC@zb5.u~&69J ^<|$aů^ٮ;3n~ z +z޹C7m[G?`CvҙSN+\]+N%VO7՘;z£gKS.;[$j7gb5;ɡjl?{#n `)sI2(t6祱lZceRDZ M"=mr|;ArZ8{bs +pT)t.jIVjّ9XsA5OzZiScq*>#}4fS$Uݘh-ݸ`1Hgj4DTxlVL/JL>&{dR4G i5ω۲I; :Wun+9~n#;]hFsL+rћty}b3t#ݬd[Y^g}X8*0il3geJE9]?8HE +H٘HN)( b FL p]ҕaiLL˅]kbpkG Kޝa+n6[M_\]Xc״l69 l\| +_E2璘K2SW ."F+",zO%&_y*jSO&(QVJi )C&;ȕt.ހGYo6b;Rʺ`%]>)y*2"_>sg+ ZFH pj!;'UË{Eb_ڥPem/W!r&$ƿ?@<̧s {zWhYl@RwDla6#ΪYm٫W[oaE#<%uusuw>WGëh#X>s:/{U98|7\wOq%,gķJu=x珤7"n#dc;+tM3.h>Ox DFzc V+'@+wk8M*,d=MW(Eg}߇=Q]}*g7KI[;v߲9nf_pE,H=\U0mOc@B|ʼn@tô\=<}ܰ ۢn<j/(W"`窓&z 9ŪbICzVҾhc$od3Z!NHYOR4%vB:L`]h$n3y{8xjzu4m=YR:NFNJ̟N1P˄;4@rݥ|3-y#7P%MҩSA[A^!(&cyll `*Š;Wܭ Bϩ1EAmzt9 +WanP PNNmhu%[ j@wY*o153jKn_)foY׃?ľsr(O%&~Ykյ \r|34LoW," +<"iP1e5 +x_J!+ȬENx@hN|~_ ˳ڑ.T,\@_Z'xC[&nvĤ\5\ +5LMfʔMnat] +EQ6YoRtHa'assї<1^%dWܔo?%6Ws'}ު;, A_j4zS%%IRR7 5UdEEsݞ7-&4q)b\' +U/ChF+Pu}*xX;Buqw/s#d }CIű^]~-Hތ Iiz\Js>`,qڪ tƞVw̨TMnl'q=n( /-v2+DLo>5,?k1n+0ζjXswp@{v0>_(*=M#NE3r[[q0j`إd\+G? U |g$9( 6JYf̲)lӏ mmrl`:MLf*̅oZ!@wL|{gΕw/-]ȡ868H/ +RT0+4[BE.j.AJ4WA޸Yj "f(Rm{ j|4\eS[Sߜ]oCH_u('G@kz~tI]ljD}+h~9Od^·xٹ]pwgۄ\ׂay]XGR цMԵW}ܶF_K-zXy|Vy%w2lX}7gB5p!auq>;gxR뗫|ÒϊųNߴvZ҆*Z X[bQds{ L7WM=XSgCX:.`׌ a,{w]z`z:PAcd#Պ[ϵ%Mٵ]Urc)1-cF{V4,p ]xLp"g}:.6\/QO5]GEvl]][qMym\>9)S؁f?\ž}+umq;EȤCY73F-{+|{:fsAV39 FyQvUcYUOoZ܂WvߺkIjX;>Mѵ[ߧ0=2p5uy@e06!p[teeu%6sNǐBx=`3EBDQq۳-1Z֙ܳl Ʋ"GjMq!(BBE[D2b]oT^q;l}c:.[۾Z}CBw7NԑRiN!yM\uf"-qV)?oޓn_Nj#yF FWW6xACd"#3UatU(h ګՉ+89{8%Q06[l9?(V`ӻrbf+{( rY1uuY,^Q;F}^kipo[d }"xKOV=AjK=[S1e;;7Ӡe*hNi)hípUeWVG;=dU{>J,ᾮF|4*$v]񡘱ӧ>z"'7PMѼ'Y_LɎ%|4W%O dP# eQ#F1ok37&o^Q7H2;*1'UY0՝pJCtHzb4W;$m.oF-/ J;.-nWCTb +{j_^|߱~\,{"b_=Z͔VE5 @k@65bɯ{]˓sG‡dļ +/zml8ť`ϑKk+~.wswOLbw4rlؿ1|W{cm6ȷ;.ң>au;4DcÏ*'cbbv_r zi]mj)Jm`QȾ]ޛmk|\4/SI- lÎ.O&5͍P#nkpnSm?PW?Q*{f{x Sʧ;삁eg-i¾Xys2a9+Yx/)c3 +@'պxV?S{'wkz^*: T U\ y<%4]* +sx\yfAn$֍@v_s +PSǴJŞ)B4ja*vt >M'4${K!Qt}ԉz7Sc +jt9xVw6cߥW㊜6"Z_a| NUiHmډ.六ka` YF2϶bA_H#x:2{.tfi08ĴY͔^o&ڃ47sUF?Q¬MFjl,bLT57$%Ni,eh?tw^:їumoK[ѫ4CfFAOaPe:maA0nZ"UPrlM Lv۹3+WƌrNw%(:DE `a@EA@%I90+*|_TWr[晹'F(ǴNhO~hr +&m8SMC;Y\M}Ucqwm"v^Bc=J@"?׈WL~ҊnJczpҴR_˜2ǖ|/R%Dkّ4t'T'*oFM_m]>DCRpgq>ߑھBygt!1Atjj zvT">SN~ wHu"G=,A,P^+!7ܞ^Z6oonq0l\JjNV{#ݭgu[YBsW^D{ l:L#tdz*磌|ǝ-vS;m\JXU%ENd1>C4կsz0[2U?3@ς`5Bw03~e` +@0;qUv7\=-@6&@]j M)NYy,4pQΕ\3wP!kljjϦ=j`o=u &oϣ2Q^h%YVeHPyUV,p>: Htkvh JK{3H>,{I̚ YO5륯ܥ?3,ΪGtZ/wJs5GѡzTq C\\tk ]tnsôixLt5QM`*c\ZT _ҧgYq&cW>L~%Ϲkx 殥Vw QJ^%c݈[,zkO$< >!Zʨy1g#E3Ur|zM,sTV k +d*6:/2`zC~ZHZ8|j5[Xs˅,/fqA djD䶡ݝ=?o#e>!grOTs5d1HHDuteS Nz݊?##=JjjeCު{QuhaͯK>}01Q rwxhAAilPQ.dP`<2$ӹ.7u9{|}uSLş<:'Wd`YC+9 kGѐ<Μ6^鵩ZjKYȂ'Nzύs0=CB0C˪Q:rTτ6a+U5 *=1M,F?)-z&W71mu>6+`m_)){]91'N`ȘaW<] ז"cW0N*ȶmm{yPaNp4ny8ٓbYEPbn$'kLKqλ;9jv'/ ܙ#{Y5uk5NkU7ؽȭ[ls}rkH9!j0gd +V9ttglӷ#B8e-ilΜاya9acM*F~xoK!FtY듳JtpFG{bA.i>=]fSe{if]FZ%++-3X[=~> $6??`:soWSk'žkI]ZݜݙUXR`p'XĴ*ũmSt 57mtm[ꩈ~` ˴UƊ J4. (W?QW0?ۑڸTZ[clʗr<>$uz+?FWyuii#dmSͫ#"p Na-G}9f8Xk6=o45e7۴j2xHNi˩+L 9)srh碠mZabWPL fmڥ(%jih}w8tcaJw_^)rRD/neg si kmo+zi҇9Q/;:ܮ6Q_G9)uL;g9Xa#}E3+gq#dVk+,#YKe`Cd;>~@6=ӔÒ`_!N,3$NyC-y]wn ryzhB Ou2jծ-R9Ә|7{k6%^\g3i'.B4K zq8 j65z0gWu '#ʷz#|tiCwuJ7Vȣ|wo1yW7`-'߿oMB "Ɋ: Ԣ@!G{Iث7K9)BIq•=5dh4$ä^۩;M$Zow\9N|M>L@FEx qĕa֟}'"|̼4 ӉJqEêv'<,Un.6,va["7>D55OWO[4_p8(x0(;/$f ߸塔dL9tG]:a!1<+bI1lsE)2G3Iejhk\0=Z&1#s[ bO{N,Aζ;aB,ۦ_YzY8wLfIfo|lh1ߓwSihı9Yn$fJ YwI~Gk5ڡXq*qre _ g^l%MΦR6x8՜W h=h徒WNq58/#N][]NX,0jKH>3RˆCuNj +p@Wzr M'G;QBCK==M+E]:6j3bC%p9N~ YYu^2J!B$t}C_Xֳ Hu:7U"׆ uVDn&9ܨ 6U53)T"Tf?l4qyOSXgoc)EOY^&wQ.`ƫvyx* +"%6ɲR/>Ӵ[9M{ <` 2\5vh~4-UwA<Qt'JȎ+7AU(?( a( #$rpKEY8ۤtn/^;WⰀ  F}㧋dsޏV#SQٯLG+n6{a)W}yYJK7G$Z~W^CRU()Ocƞq}(9BFqtZy m&j:9*Ш0-x25lr\zTfw!tײ +%O֫DNd?S+c\jVf=GFEI!n/Wk8cy4~hEc*h`M4Ƣ)#\T%|cvvۄr#CRZ˪*eVKˑ.\M:|ŏ?@xe-.,a%UkňeVOѾǭُVc>_a4.i:yȿO=㍨S/u1FR} o[$.^VBϞvʝg,p^ o%(G!1.¾W];޷l93u:VOOk=4h'A;\8c"u9\ hI Y)>D,9'|5b+ +|3}s"]C3fvRϒd;N䓹RDM;FaӨ'4L؞BueN>XRT2G^Oש#<{upyMMLIurܱď);f8㭼Vlhm13X3Lk}UA5L7[9 'P- DsJxcV^ ՅTw/<^:-PP:ϜDY̒:F?t]0_Ĥ4ܞs71׈5yZQ\__R@;~@h;tU8HUINMp1-)c6#d9OdmO<GKVN[x^Js~mO5cÖ"j>cD;@V MZj%wjd[3wP܉kӑ0`⬴yf]C8kpTeN`,֩ w JsFs99{|CҬ۸Ù{uݪܱOZWQ `%o-uCZ.vpp Cş\i?5G喊с^aQ$ɟhV؂YŜV^x,͘I +7,YOmS*Bn <[>)ZV3yDã%..xٌn)%VB^V}ͥ'*Rp{xjWuӼU/:y +sGCLpDD}t9T<۟QYT:ũ$߼ J&OHּC/zyDC-G͖uK3O՝['woXp Y7ħEovzBab6mb)Ii~Wؗ%]`KMs4wȀ飡qZٙ#k|n{î(p~p^5KQ /x`1h/B^řvE{ `g6|{Z Rg=^)+P21pH` iv2G%7Z rrrpιi{j^c8Բ Ci!Z5lm0:4W]G^l6=MG>l8ſ(F7Uv7/S*%]p>ߚpk؍D}. s[g}Y閷NkEGlmiΞus,d'yTO NeL)i5]\Օ?-\e݊RiMVM&x(>+{܄A ?@ 7Ht{5ܝ~yާ. ՁX 4e%(gDjZ[dmK{֞rاN0BcnUQ`Wo.)%8\R;oE{g$IG ;eZLdSD.fi`m?1u;;i~Sl-ԩ]Uk hA33]^liqOek~}߹u<[:H]KP#}~6U7jwiNNW;SvJm_ +A {LB>3s'^_3 cAC̈FR; 6{/3z\n m˯a) aq .'|5q.C@Y-sy1ۗ!=$fmhЯE nu}~׏e8L;_$4B6qd̵Chuڟ~O{ul6Oy[Gjd1D{'ZدM΍{:Eo"|+ZWVނ|zYnmec[\\#6![TsbL^Pνa!mʻM 8vjT9jy?]u&ň)X<ٟN H!nU lP5cNWN!q6}{`w =~vgOۿ[$/n8ݤ'mRAn93{˖7k1w7yϰt*v>bcMfAnwbOJp)~"Cw,H>HZ#\-gICНT}ܠhʜ;fM])i2c7h_RptF5e􂰏O{0n}mݴ.e*; q"+ |!Ϯ5vV:@-<}wUw}^ߑZxuO~gVjCȢ vg3͘DN['k#FI׊]㮫5PUd'q #Vߠum] JG3DN^"H%iryۥϳkvmX r_Sw,mG3OҴ&Cg p蒇+?1.[W}Ij|O{NvW"eR?*YqXh>E;狅6ve9Q+U{vԑ5IO-p|jʼVn+jzP=mdy#vuNC,T7Kgrީlm,>o۲DqO1S3`^Va3QO1꽗B4i:UKJlj.ug.]&sݶ`w,e%m \'ϧ^XM{ݡBREw2N6\{v>ʮUAYX6[= +;cK(qKRU񹿷/BAݖQx2뉏3WCF}S #j1K=߻wC*s GfN\s"~<ţM0^`*@uD(WxiCs^Zo韅}0a_{)4q3 +1ZV{`D5CvfŧT.Wzb$Ka{S@{ĜLT>G)6QX-;ǶZMeiD2\{-! 7Z7N4vllsKyq˪#S*)ά\X&# )lmz@$oƠٓ$7ڻÍh3kaH:"2{l:X~ˮVݒ֎soٍV7QW|ݻX/scB9l0 yyT[Jm\9zڼ:sNtްBc-#ooiiܗւ1KzxB*/Y0^*'YPoüwADVA}fyW֖qR=-nIQϾl qΤtaXϦ5էa[7m>VZ$Uߧ}195~o{f؋-Ըq@!gl?&P"b7Nܼ?}BZ4\oӓN?/1[ל7iqeB|n`(y嫼9gno&H0aI S I.xȸ\Ȩ(CU[t/͙ܺL]+Ӄy[oo`+PhN] hٖLM7^#LjTᮄoP_L]e`mhI?Jϋ#Q˷vgg<ٹJYoA-9=QMm^_B䴢7_AJV)}wE0E72y%=@_mhrJHz?RɾT^UBc@E6o7sx,kWSͥoq +J* _KZ5uⓌNJk21ژqx/qT҈+׵-fmrّ[2>+Yu/3dxg`3G;Sm6~t]S; fV ᝏXcRѪ.cwg +?E[IOko?pe.յ}ݖl$.^Sn2XI\k?@`^?o: MPƿhxG"mK̘Q/ʀ'_>ًFlDYTD6SiJO?iQ[Xr7׵1;ҧɜ}Yay`oˠodBzujykan)=mm:@e].!ܒ^]X/;F)m< /ƛ08Gl.(nAўwUvGҙv[ά#n?uZbfRk;^_nT.w-tό.wtA^>u:2uC‚""*J֪oD-M;I *===m}`, cbIv.:38h Yn*=mC_X-C ǧ9` n(TvwVTjpl2U~ȁf{XkEJ2A((Й2cc4jp?,IأtdgiP`2ҳңi#'7}Sϥ?c^+MV݂l\ۍKq2+mQ9+Y!QOM /2oww\9Yo%ޡ^xr|M) +mx]s傝/S˒/ 6nCnN%{1`E#",޽k?}>#qVjʥޑn 5?1ޭTRud`I`=s-F孡ӿ= V+L{a̬vTe@l#/zCMϑ;wguvԛME5]?ЩS6%wry{cy&9s5<_ED+F[kI7N[}$\ y'5hryҜʶy:`\%u㍧mjϕJn~;0;ԃ˄4t6Ԫ ۏMkW=c@wEVnia^,vcԍ{91 iONrY=|<]r7wQަ9m +qAxiBcb@r2NǮ ݭMQqq x=[phc>Ij KPNts5/*ٽ;&۪_X`@g؋85+oR8_ +<|߁56JP  $ZY꠽ڪh="ٷ?o'nӐ8˦.Qިke v&Uzn !WiWJجE5963-SZ$yگPOAPXY5SwƗ~82ӗՏ[J,$77KF~P(~T;>n8B@)7fc.޿ySAwpTVg0ˍ/0<=nJ,ԮXj5 S)z%>}?qN=p&BT zK&̑Įӿw񄗯S_<1[Y_.9v}QJޡbŗ!zƤ<:}zV:|M1q^ڤRAoY(qysm* UOۛn%[O0QĊ +EDuiy$tLJvVNUA{[,]={_a\CD[aqR9VДw}DEBs7ܵXXXlE^ +R:7]tU-8poTH^y+w35S*rfV_%uo QQT*VՎsWNѲRA~P%J6kes?.w*ѨݷO@?/UI#HD41pK8 j^'VދBm +5'wO9iPƠوU)h*9 5IurUw #>Ebˁ]{T[Uěrws@<)fw$HU(}0^4Zл1J3`l)wq'AJ}ˮ)RL\}U_Ds 2yY߅h.rE@[<%F ۓ) p|]D'H䆼 j[MAΪ\8Dtz4UH}f2ј7\*B%oZQ›K(y}r`RrTi6"3;9XFi^Zu46K( j*"/.TniY*`wT^|fъ^p տS*<ب:(RjO{!4jHx_wXo0NgOgT#d"LF.ge@_b/N>B04@5ٝ+>Y$) $]o/ zxtZ r^($;814>1r[MyEit.`.rD=BFY:7lze.ǎ|%K18vbnݍ#`˙i#}ͽJׯ[]d$eBv6;X^?G4-'.\\?}KUihlKzUxBw~NYuȅmYkWgO ʴ eU(X[(u.YBrW %`3V9򢋧$ +VULJok-LR{Z)qC%W{^(AGgcGcB` +YG$ʔM,+%aKTkOV*Fů|ZA^uHmv\T Cs"Oy&NJl,j@}yө܃ QԎac!+j~鹡hRAr6জZs][NqZkČl%%pAO|y=݇gӬVOYj~rm=S=>;:xh6{䪱,^筒Y(8>ooSB,X.WDƺKOZk + + tnKGo'񲞕w&Rz\MKS|løvHΝVr .M.r7 :o+>G2 ?o4@NK;Wi҆t3PEC{ j4BrE}B" 0vى{DT'lz%0qfKaz.7S:LЕbmu@+޵"*Qo /ŜwIkK{C&cϝz_*RjM6v~.,Yނ4MltM*#Q7r^(YW$ ܔωzXK5@h&p;04ULyXg}7²e9M5k}7IQwcejTݹW&`h$LUA=_pF S.6)R Մih(3uZl7M4] j0orhjbS +Pv\(p۳ +1'k[h ^>?fdOT]K=ѻCX-ꈮ%ŨL#wܱ @X}K@> 6wطAnY)PK5H:}=Yu4zk +IkD^fd)ᅅ0xn; S;V kxګ/LSS .z"dSb]iL%mTD +C,Kߚ2沼Mȕ&94'͛ƍG/;n q)w'6 9Dr5%Zy [:&:vf`GBHlO2ԀCOu:5~xl l2i/]xp19Z:P JS-sh.TRmKgG7.u*(ŢVVRIy^lluSf !F>644-RjYA陳7E00׈;}tThs*˽B|ќmY&$[0͝::ЮblYV 'g: #`]szzKCvKrN.veu74sEtc͘1](|\c=ml/ͬm]zQo;gDEE5+=nr~[X"PuDT? rh_0O ˏkU&(Y *uhZ=K?+k.[`Dz% %9ZT +tZys}?KuX=$J@e{!n_b10U|XmҿZNd`*AV'wwzE.7eh<.1`1㠇nC|׍E+Jrd})+z It2i"WzW~ǽ^xK+UkQc)d*A傡NJ.[IU*T**X;ءii+˪hpkEy?w^Vg.?zγ4vq䶯U,^VB{^H^n3Lnvptv72ú) dΰ/SPV9H^Zހfؠu j1x6ܴQ +u R:d<p[Il[l+Rs[@2h{Z^ irt mOmǻgXc H9VDWklq#O(X~@z +ʲ.__: ~g#Rut~(%XbJ K;5T* wM7J`jdΦ`^V8|3x5\Wc&p!;d"">ЃKUU#m_; @.7i%6lRxpܠ՞~~TrgVjv0ƳJ"mFV'wRO*4vS􍫮L2X֋)Rù؄>+U |-.?pY 1X5U~ËqދG#yކMl|7u w"*1 t׽szIVw^ZuңWOV*YMu 1Q$=|dxcV,U&J^v5au!J;uړrwiXw(wQ|=~I/ g,Z pUW`/*DN3;zC GVLQ^{`:=}bcdžvkn'Y@n!WT>VˑȻp^ +a^xƸ~ +|LR]H 5pJ6j2'Ϻnhc.vd[ς퉤NͨrĹ_odNǗ6pky#1]3bx~UN Gcf1ZiԢmEG{9ő-[]y)b8iX U[9EgW0%_l%h5eG:XO1`|T(]3f4|HRPڲO%Q\"pm"@؂,QX7EݒHI9E< ,rK\h P%,S[u'hPe͋-襜oaՍFr3`)w/;u#]NС|>kYfAQ^_Hlцrڞ9!ՙ5yn*5mXjZ+ϦZӎ0׉Rl7$)ps_[ڪ"tD#JLO~'ua,-Og'{2kߐ^aA Q/9~s߅וJC^.[bF5|%^%\_0%w' tdrjb7W(7\Dlұex4.bEaN VrCLs/o3=`#u2 ӁlLt^-ה&ӈ1ͣhS Vbs΢vs;_oofm.J> >oJj{++6k䝠KVCC\):5`ʍ;Lh`OEO!ԔRs7Tehj~RV dM@g>ʪ>3J9T ~?HXpC5g~m^hi)*Spk헼TQRUQًԫacA%҇,&OapTNis *FʈVl]ɚZzxY_*ʹ%T/Vx>KCV +i-kr3tCeYk#ڪJQIA!;-_="Sir#jUΓ<ӞݰB'rOW;1njZY`jRxVqO g) +8]6ʛ=I~KWXg4J?F8,TEpgblox,)SC}XU¿1~J`hZ­u_ ӷR5=:4+[,CeURx%.J .24z)+^"M~8>&N"ypU +-2n fȗG-%B ^/2_ث3 +PXV1h~Ǔ%iYjg7)}| i`qyjf"iKЫ-]/A?Zrr gr/~rp9__+sao?<*1JσQYEVQu*o7q(;;n[E^KsƮ@QQTc+mKʱ߅;gVnyG$^BR)פF˒ecrwwdimfQT%LumXS1[J5q)x2;3Q̼c ֨Bܖugmw'&+^vҽ Հâk8^Las?%sAf$\ebo+<]=" 伧|,o)\Z!{T;H~s|[6[*\88Ze“թ0ɡC{2Sp%~i>bOS6ur׏CO:z.4u0Im<ֺؗ<>4jaiRN c-3w,W]vq EC=e_bŽ_V|a\Ӂq/7^C2@/`ss#]c[ +B~Uv$>:_,џ2џ2џo,DfcW3Q4c%{~YXn/7 w kf~g.E_?U}W8_s}{Y{=,s]7M'.3:k@˕/NpVi<!饃r;JZ 2gV|/;c} z߾#V/ /g#xƎCTun?]yd_`Pꝑ&$u{(r B+j%NUu7d4LgcqQȈqr-y{N ,7?y"؃zq~CD:fshb|:hfJp4bx U'HS8MHe."әlbNwuDh(dj(?/O^x]<{Ő>;?o'Mrtگpy42k| BfϾEU +9urF4%ZQf-EqX]K+-7 9Yqk:XW3=x +_SxgUJ=sMݺx}[m bc|/^MR1 Bj<5 r7>ѯ>Of1 `W,λ?ӤeC [7MƮ\1k4ܘW!R{-2. V<ݣR^+M+k=wW5y%VOr,}3+X^( {-S=ds?1:XgyƳ)2S=\>6CDش٩\k͹$LgCRep{Wxo2׹[͢{ r䭱 |˷h=kPhy}cIlK<ٷ|Fx{yή/r{=\ ˾Q φU(YuZN}WŦ3O*Rq9^Oǒ]CM.Rf":#0ǻ+ӧia߀%r㈁ chr~Uc\UKpxL> ?wNIfkNt\B?ϭSVorZg?ۇxhQ;uzvN|8eU%v?/hؽvnhZjkqOx;Y ?hq|`?Jžzdhwle.g O)Q{M3w2#8ϫaIF`!R:mi5[,׶Bt\ 8{a[Q}a4{wou{E ajQ\7d_$mOǻf!E??5_Dj.YKfj.= +UAgV^7A[*f\ f꿰j/{?ЌfW\n&Da1.j H;y,pv67}gen (U.!d{6w2 +j\_"6U8Jb%0?^(~avmpJ>k7έ?huDX/ ܙ_KI"zV~J8w6?w'8lvb"wMO /2D4h9ch vJg;MJqW;hYmtenx_kӯ&~YL3SZ%2% 4NqdӔd4G+u7:G}|\jֳxP4?c# uzF3\7~HZ<#4en2JIؕ^$eo[Yt鴽U!_*ׁh==|?U7<\K?oOdjry'CI| w%WUYsXͣ"jPP2 qxa= ha.Rڜ*t޲J?kfWv80/sxQ^'rYPLv ׳iʒ'B2AfJ4-O}XwrЛU" +ٚK ְ>6+9ޥMI`ϧb ~CLk)LyFYrIۘf=kdpG7 MO igR1#v[ݽ`"7|WJʰ+KgEJ; 2;߯>mkO5Yˏ`!|SH`ԧ's/!ͽ2]>l2ҁYڻwj+o^aL7>|:N]{-O&Mߎ0+?~P@ib)#{öS!*m"ڲZfNӇݛζDdž(߅Џ^A_[,6$[M2$iM3}$r\p],dG<}s_piRwocyvUuKQ~.̢Prɱ2sj7wԎc>_A`ᦾ#mWOדrfý3۱CdpA`0)== u'1.г*ݸ؏{"/Vtw,ux8gQc($q}̞&mHzXho ۿ_z{j+M"G!h6޾L7Sr*a"Zfx)C'a`4s69vxj+5{ &|vq>`AΓ۹I:sXߖ +*ϸW +|4"ER1奧oU0ffe +?Sxh{Bo CYWwVrssgkku},$y-/༆I5}-5:L,9M[pi {z +gR>o*~:9hո??D\~Ͻ[W=tװ8rҺfqjjZp\3 6-ן0S04 ]u?!ajhvgY @>]C:].}A1mPo-$α&Ǯ{ֶ4q DIXN +~_ł' lr*g1GsF%:0)R)D&nEZzE|yO͹d>/Y+93F^KSppsVve2vY\q6ࣆuSX0d4GU uy{/D=tw-4v:Ņl !_bwsYc8#.k=`^C>BoC%ȲR7D݌(wwiV/iʗ Ik1 L[J]Q ),qU}&ד|*3S% / 4+K} +s--NfL4̺L(I.;~_~4ͱ!#rYpj/D65DEӜ^A$t/bODs+#ϳT/gS!?P{5/?ƿkuJ.Ȃ +[Gw<^okuxݧS.S%&8ǸE}## +vKqLv[6veS_=ɰrQk^u7Kw1}wܿ$_FFzR^ +”O;[Vah 4G3% +Ԙf=-~$:/#4j͂stib'Yan2Z?)/8t +]Y"2SF=2z{1 G[|9z#rrt:}fp]6 -AMv ,9Kʜe!r(AH{v>jCK!ZYU\wM.O8 54ֽ]҈j^lkx(\>uq--zjp)lKA,-K3v +׻SznU""lAcXKc&TW? Ns5Ov:jfبe1ޟ5:ۛ_$t*0Rhێ `^4D9X NL89 +Mi$M_ޟk~6zmg(%+"pFyqdFdF ϛU]Uɪ? 0t;4lNޘQ~?g:4N޺Ѿ(❮=]v9.a +ʸN:m'57ELׯriJaU҅5KWWc "yfy7{[@ivlw)%Oo*ۺ/l*_OW#S#g8Fq +1WdW (U:`Y1돮#ssmgжO6:_mVW2{bnvHwoK ߰]oh&, Oz&_ __|i 'jv?A% Q=+Uwl;pψP?l.Xڎ|'Om]=KlomTJ,YC׼|dSV&~h"?L]|Tl5%2*/džRGO@9[?K&Ѹp7oeͪemԪ4U*.!W(KH]te s. +8r%oZ@斯S?bCl! A޸Us<;yX[׊rʜl/!vCkN6z\+vˡY.S}| ox2M i!V#kM҇]J**$RJ&~)quKv (%JAx}k{d4_hUP-iHcj.Bܑ3{1T ܕ:Pu_5Q :(t~=fnfg+|]#cׅQqxrڙWuoZUǡ42ٶuɹ8X`ݿr e&&~i*ˏ.zϳ;23U-h-Dۢi>0# +Z󤮆E 736^< +B; I O_ %~ 郩=`X8l+2GgnngLyvxIdi +@񫚽-p,bV-+LQi1@cWYt!7aE"XټTGAa@ċS Z5M&EI5}sWߡ Dx0qW.9p+)˄Z VQ?- c:K9!_K_×]I@f2h>e;uA ߴ@s .ر!0K>3K+d_#Kq塞Y6W9̛7tjB/c:Xg,_{uFֹ^Cq[t5 hOvI!G2:E:5"zM&@j-~y;_ :Z^™|rЎ:/3^]UQS.2^'&B޽N _"FWfo!++C3[Jx(tDgh~%[]DXoXty\U"2:q$;|2T6mD݂E6ozȆA*jM'fV|,Kms{'SƩifQ<i2=2.=]_l;/ϿZb`HӬU 0y LIdUz*C֞}21>Kb;1FwNG 54NcqiGoq  MyJkt_k Íp;'m-+|UA\9W͓*L&^lzXYF(^m%tKV%~;fD$+<;w-w{q,I21EyZjlr>h7*|62˛B!UP-U3DBűtuN|Ն*ٴ:%ٻ8Fq"7ޒqeC7g:5#m *:uyy$ 1ܚMb-<؀*c$ёcf>'>xGջLm/Æ."dn&}^y5lܝ@fKH@_" M0'Soh~( =OD *7?>˿//KV#Z)Rt#:)EJ-R]6>QlfZDeơaJK$K"S#bTWhܑFD fquZ?JKO [s zUr~#:`='&nf?[9Rzs?{|t?9t:DJuQЋ{\r\ZȞ (t5ҢG^+҉;MGZC'WU\=^gʃ-gM O_ 1!epq8JW2Q,jBCpoɿ7;{^X?7MQѷ.>yا{ϷK*9aqV u +#؊kn%t4,J'P[ӝ6N"cN^(5\_)X$1h\lvb."C SHA>JDwJöf`1y\N,y ffV2-S:k$0+X!G@YQsZ/5_|f9wDVhaϊ{VѤ$Yv QEfK~̱ܜQ1L:"5Uo׌Z2B1#ܮ:SU!qur,7cJiy^@p}a>^B|u#@:}~ ܱNKz) NybdkAjҶU,- :{g/(])\HYwi3vl#ez:2xY٦m[(v5^«7Nm\1+_$?em44)qX]_ne&KV>WS- :({4n ̍x t}bL +/+0'1_ncዙ/ȚZn7{jsҘ(duZlYڌ>̯5QPHv0whؿ[l¹>'Пlyry k,GE}`s>d᏷٬g[OӳvR43IR-s{Nǹ*,dRۺRs&-&BD@걷 ^:4?ϰ#v I>Hd318%vWl="=2 cW*BQ;5ۜ|5n =69MسQZ?E ?5ПJyI*=? RY +/SmlJ}.[O%tԅױx`R=ZIxjAx +Ht"\2 +縇TtFSGF9!^+-މܮkXX?ze +pꕀrߴe{S[z=~=V`1`1bQsQտ*+MޑnV +F*]$b{ә2]JU%\TDYeE525ү~ĕMno1t"\e~\2.F٤ +{mL~ +exg 8I٤]Y2&L}ubӚ#˵qG]u|2 t;1ԁKO;CqդG$7ZvOÕ̲luoaJNIGr~/}!5,F0gg^{RS,W۝V.KS7]'(",j1BsN8.l8xΌ_:cESuydJ 1Y:uWQ}U_'MlҜEzMn4Jp!A ARr߽qŴ]JsFz(e޿vd\,`Cg^jhp3Y_=2MV91V<^J"c&N7~Dt>TCkH@R*s(Š/ SZ; }%ouQ]{dr7{9\a mB57©閬2m>n7tHnZpu}k /1Bb#<+O};lSzlNGLs;q@*0}ѶP~+֭0qЍi/Y[wðV9uyv/G3%ArN&Ѷgfɣof1a|fmsK ' 5~ ;*:5yмe.I[N2𯑀~*,6+U&[8tZ@6"DH/ a~Wxrmd63.xXN2-6 q>܌Xd~g_G\q<1K\Fp\9,{\Iv3AI҇qĕ4,Fx|DQX*8W5haۯx~ +7xgF=.vyU?bkuIdnt^J e(i4@ybD̀(̳۰t-J  t˝\)kμ?р at;ϓ>0UwqyF"1\b۠u1YZq"+pa bnL;/}Z{קFn-[w{fn9վEth#EaM*r-7$?4 @o=q#N8;'B~mқ̽ j`76's69x<'-!tYbe=7Zpo _2F#?D1GP4쨙y~K{f*]N_֌kc}ǏH5pR#Vp/~ 6SJރ]Cs`Nʸ` &zh1Ni .?\ TdJд1ݥ),K9n&;{Sa +F@Yoˇsh~n!&5S8[uj>ȗ{& siMG+g0*1q*yS.}euԿI'__—g}mT=0ط]0|6Y)oV=#lV ՋgTݓyU576lpzȰm9cx`K.t0?Rhf,2%8 ~gUI>9yjxpc~~ q׿Db jG/M\OvNYL2% S=:\3A0Ns˫|ˑ>JxizdDz3;)_߭?D +W,^{eѩc{i36TeV/{˩\@\Nedz1&Ɉfn}0sY9t?@ X_j|6!KN]Oڰ|;ڞ]R}60)" ◨ܲH@hf#3ݾg[. Ǒ[<4,k$Wb?ӱB<*y9W7,EW _qCr?".E@81~f6뛼unp*=pBWe6qCwLq!4JT6ZAp !=p]KʅLMwg:L@_d!iPʯ;uG_7WP0DW=\ZȣHJoG9,uAI퉓?8wj֣ϖMfMy oN\!K\낗56X]WzKI&PL/]ťrJ*ZzY-zi(\zpx805ްFCqn{ܠWBeNtp˻53iu#(%e.[xbb+Ń tMT +2}˵#=Vb3Ƃ}Ƕ&&s7m~;ksg V75R:qі2P.<1 ]b&xd؉j_ۼ OK]-aK)IfMkk/u+}nWk&M-YKffmr;IF2 '?e;$"派%ơF b2Ut8X%H7P|)e n+f 0іg^2F%5(r [qC_>ȯ᭼Nj~vo!5^7|G`YcA{9t- }\d6nnA Yfqu`V2oXN22-U…,KkenrZG1ˠ¡̏nt]}ydD.طӘ>_T^mrm_s-reZ\kȪ͕`?_p[r-2"v+j*cY)K bY0ìIŋqRչt-XNRrg|jU|Ufg::϶IiV~L)^o#Si'=Ld$^Eqܶr;İ0J!ŕHN)wT$C$H@ KF"UGq kSjء#+;v%!*ws]|iL韊~C3coh~*( E KCR=1a6֍Lqe4Qc9T'".+& !>d)-㌶1ε$cz͟[XοTt&q11ܖR>wtwv&Nic?khksl(׽tO;fKKC|$p:8AGn5dɸ?޲icL4j{0EC[U@NV-ijQ_d 3o~-gMPeJ縲ھcd>C6x^ENql^Ϫ?On#֥u]ZNl%$c K{sMՖsF<.j2.# +%s~\m鰭d?X WY*uw^nٴ/Tr,BAb +C>dnʽj:0a&/?Ww{vW!#"kFMK+`yٙ*5*HK^gwƦ{&/(L=&CzTW1u*q0/?4ߗkTdO=t0A-R^txQz8K=]!-]vԴ~r s}{hQbuO_QykVGb/H5!3 p7{г{-﷣l黈KabkWf^:b.Ic79^q@OzOV¦}7ɾ+$ K"Lt#B3I]9)ur؎timYx/wfr֕8KNѹfb~0X_ZB꾵f!~8$Hl j *z62Fp6+ˏ~q0׈IUzMIU]s w_<q%1oo'LDxrX1ءtٚJGS[ԯm3$$W*t+.k[xY9|*ǹCʰu^o|mg;Nɝ/SMe6 FEz7]4 ϗSơ7ZIoi{E bT3—!\4p[1B)OͪG(]{5}'*´Btr궾u6[g>4u=347UgFv"m(k7Xܮw XxiMi7{ʬTUM13-  kr?%?y ^K^*Z"'kxky`)M1>̗h2oPl5?ۡvxI$>_?s @b-R_\ I4f:~|Fr] tx_xF` K<7؏!L@lVޚmF(Oon# F͕5vn(&OMow!{eCFV֌R>ʮsLČEI8L@kn>Y>X ^{ɏޙedY++?fǢ,f:ɸzq>DRMM wTLQ\."<+؛YKZX&=<\p~]Vjau#8O -) 8"LpV$x/ADZ5ع^*]N:P,>p|޽]2+ {&ɇ +N:H:03e k&J g|CU)!LR4fó?B+5дahgCv,[,U 9a.s[LLn3WSЖVReI-WL\"IJϥߐ&% f<'(]1 \m/; +R,rs,PM=K셯ni +^jsD/}VlAQ& $bUÛ,_pxܫoteY+sE ד1ҟ}nV+f(7b.mXt{q%U)nd"[ro#9tZ' J@sQbQHvj!K\/cX>q0ґ#;Aۛ{9TIvJ vo|e}i)we^J./'ev#z6%V1[&y#))W*ǭfkH 3N\t|\V1@4Q^UL}v4Ͼ)Y~6)7qN a8m]Tm(dfВy5BU+vrxX|~Wm(1^~za,읈b 5hN59r\Gl(LWzܺp^6^ݍ=#;RU쉗 Wݸ q]JU? H5^F:[^d2'-1Du<&Q_H @v~&  SA˔޴8P lFj%Qꭺ+=!;/Ɯf\31BD~ދcgP2#r6i,7bpR!(ez.n/[pZeyQȱ uršQQ#|A X&]ΟZ9rx6۷Ɂ+*at+_)jRxa$p%jD"﷟bfrK'H/;-_IRSo:^9yϋKL)[2W+12:i?R)ה;J"ő]Q1OB~a\%g +S{[(3ۂ3dJ/bo}ye\~hp97 II +s虙i9Wh"}i#n{z~2Ʃ= +N2U#KEd6UmuS$T\n +yD!2Q 0a`Nj18J5)\ZWZJk.uNU?}17v-I1~].OoF:s* +g +,*{q;rj*H.)NPTŮgw'?ޞط|`jz1z!<.(~`Yrkp|W|t%PR+#&4rReP[8;%IB~r~QqU)IAۯG s 6m_=-qp~70qaYq<.LCXd95Aq&V\CY-m誟U!)y]>2Y/ͳyNA=|~_sj$cCwj.U 8Yk^Ccv5D^DݢAfkooI|#=)Mt룺(aZ'o:ʼng|8z_Hv|WAL=vCóz ++0 [ .[=oJli庱*yD!7Bntŏ7k*&f5Pm90.LO(FHnJ +8I\| s/JS Ep{Ggж~][inws>7^69|!v3~D&>*]x\[^ܛ-v?NMnM CCYrfp'+yV'){2gV##/_asNGv9nTqq}1tK+,<[FG*Gڛ.-4Gp<3 ̢3H{x/?UUDo%oۓp_?ꗘڻыd6?Py->PMXquK= g-1 #EпOxy*fZR +.l̊6dġUSȓ9&Z'G=}o9-P^}(/=&7'oZn9y<+3_f#LJ LG߸ L6Gy!Usvl}8f ;/kWn2ZvޛzX*ܧ 땺C@ W2 \ +|9Q>7GKՄ!fWbE q-n# Ϩ?&;ȿ?d~!"iPcPlN +0F%P@C槷z6% У'֧J%7bkO fF. Z7 +KSf :/jl@Ma(pzeP@VeaUUvhC'YDR:_tt_*›AbJH1s] p $}RT snU)/eОf *=Vll + +vɦ~^QQ/" +X~1j +p9MAjK ܅]yrMОضD_@S Pxj0FAJU +޲IkݑC퇯8n I+cT.M. 5Hć˫5uq/ڕhݏu~\V= `s䇄MXvpXAr;%{nkosdJ %aC}O7-oPɶtզ#P@_aaټ(e}]u'νZ;Y`?x:<2&M>W#BrXVXc:!С@1;@(f/aZYyl-۴Snq{7֞:{ktvޛ{tq&ץ{=brot?BG\PeRwR]q¡i|#岕ݢ"\lTp_:M>U eQŚFMSz6ӝvR}}_:~/Y e=o)f4$mH UriotWzS/ۚ5Hi[Yhkr;֘<ײ gZ{@u3F.P"W@AiPL3;zh+>nyEme#O3 ˤwVrf)ꮛˎxzkRm^b$jnp`Ąj z: +<ͤzQ#AN CZmk"oEץ.O12lyܵ!֓;}']\J,V` T12О->iPHA sWppai%xTvj>o.̟+n`k* Bp=ʼnLU_\nDA$UAEoB%I#*c+nvi;Xgw-evd~{?U + +mP"fx{s?[sµ8Nw/0:ӕ}V&ASIhhKKlZ* endstream endobj 36 0 obj <>stream +{b> !n%!7e 3_`| k*&a{niH7kY9ym#J2S?6KM>% E,c>B|B/5n@xa|yn??rm_'83xE}˾--zs j'Gz]_(ICq⾻y +$ ӥb0YG{ ^οq8U}95WJo_\fݴPjmVU4cUC-*.dwK >cg4~'c#[o%_n|;:9'pxr]3r"؇cR;]Bzg_(vC]\2l[<I +AG}θu~(`f.-άcOL]dK^̉ ؍tVH-GđdtsfCc$EqQ _0j ]C/Ɯ3A{:s_8?GLGV] 鮸;-dzܶ˯k4B ҠY[a;4(6>W#M||vtӠ`Ht>H`siI+_'z>$jI~0ܦ'_L2g$ d%g̐ +7:^Hfe 55Vk4'(L (U erTa c]+M;6rR{qgF]^{ {;,m}][ii{#ng*xelճ9 Tqt5Wbz^b܌xlK:wJ+~"E J.*"cUNˬd|;Z4=] Vj|dQ4X.UF|3dwY-~aS(Sp9cnE;}Ńd1yvGL>7{^A*wafdsy˵933ާ*y\2(md;Io3j:)(hZ\F?oЪp}!35|13բ5AC!6D]ڽTV+6ۖ(<'.jzJi\o[+ca[saەI%J:C|bR}PfߒowxC# &Z~@yՆ:QN,$Ts$ǦzD8~mez&L$js( N@ʵ)~? ?`О + ޼ F=U?6WM@dD 6ܺSR\#|G/2JYyTnBeqeoFlZ>} zUN^/ P^ߝM0n=`(ktBU'<Ͼ ^ފD :yI}+ZmFI +Lyv< J{wR7o6ɽP5$h?&zX0kU@, k([^޲0=k5d?:ȇ +E,w74zهY{y9f3sht(mZPL\@xvyf[omhV^(8pp~w XBc3~9W=j?@Յ);]ʸB=!$dT-ؕ|qw7`l=j$^쉽_fj3@9{zlg͹ kNF_OlG<@Ρ$A&3{ߡք1hB7UX,cu4W_}e1~5Y=@|puܥšxm*MrgZw +ʹY:\g|p2wTsY>ӛ=riRVC9.ެ!֔gGfw~̀BI/͊Wsi:ЭKOM|ΞHF[;jI,Y|RpTavL"ӡRGGќ;2fPIFoPN*~/W]>ҕjo&uZe}SWSTߛ\eLxĬk)>ahcr;7Hll&믍u#6&qmL8(GG:6wUK p6Nw5-zC4u[|F.gSd"Jw)=?7i8ۉ*-p>ʩkj rp`3/ pgP(a6f +h sg͑o逃u|\7yͮ٫ϖx{po}lip ,K]5{{JϓvCzeUmu>g٩"1)*)L>WOrgv;YS+wte&'vg\ 98P 6S_1nj|gT߇;M&ȑ} J u(]o.$ΰ>R%!;y¤_cƵWm=a7uJ G!Ռ =1H݃M#zAeuڷW\ -ƩAu>CZeHM-\M 1L)L4ju( )gL<lsK}u53y1ב&ɋ^["KJ픲(yF۴p?8cY#0yEjP QŁ47\3Wq;^q^vʕX}A:T3an3j(ٺv'6sAc%vW=ڎVvGt~rzp%ұv)ԽMfϖ'bnyMnT LfO=1ٺ`- ۨr xϒ@uuJpa1`< 0&cHp tap]0=6@a !f*%KUx &NQC/1Q`p/jʼ H ]·Lv]0(I Fm0b@ɺ %&@\+YU6لpkC޼qEѾQچ|݇A^}[\oP8AG}^{%\X20*rR(+ݱ׆|EͲy|3.qĽe)t B.uWG蛁=4!u`/j`?](9BAC6wM2_efeĻ9ko [lGΛ @n>[>X/`R?ܾjo 䯿&t`rW.$\Di,":q5F +" ;*ةEfgF7}7ŵ* Jp_~ R \{Al~d ߯J qaFa\Wpu4$oAM~g8V;QsC5Nk9#Op {⏀i(t~B +6aNP:l38:؃lxT@AJОͽy%7ױp(r >qs[Ofw>8I޻=Ik} cB{g@>GxTZX7p0ʢEicCˇKKg]2s;wz2{˷TCXc=^gM]cygq s 3_N_ ꜂"yFԨػyW0Z{Y.[ YYpQt兀INrF⹳:o\j;-|?G, tMܼ᠔QYS;s%y=O6Jf׹:Xf<`Lm/2}vof1\%[2ntыS;%{ gO p_u^!IJh}1*3v)v+7WVDv/%Sw#exh0r:4S!O_?^9pNtHߨ)~/^60ZyƱ?LI.eE;8HL#|[Zfo}?nv{ΝHJtPM64oܾSg&.Z /;h; _}q+|??~ +u|ȿZVsKvڭStKF{6BM)uft0\&eh1jjM]5a{_RMc k:b:t +q^ SS8W K6sT7˂S݅0\b2#Tԃɾن[=''KQtCd?R[;$^q7|8vP'1Ă:_;p"QrxNR07{?-V %[lk,K##urlxGr)/=S>xԽrs^!7[`X evg(+u]k2=nzaKBWuN˕LMYzwdB+?05@ e@w7siOp/_Q=, cPiriEjUNL8MDέ"U? vZfw*&j1T#1VoA,/AgPHeu]֘̓7yN [3,ٚXPQR>ol ɯZ|gϦOZc2RɃKPG1ꈎ :/^ôi"WI];)c-S]o{69C֎59i}xxębbbo {N1f]BZ1)m͏bk<1RS/Hq}=h3:15Kz/"wZ%Mw'􋇡U;~^``M )oԹSGc1e}ے/bZ @t~,n"-؇Gvml{H's +7jq{݌Rz3:wdQ3V>\(t} ?XZ0D_z8? QCۮHFv{Jx\t8vvy1:k9 |۽5z`{&FzX eby@]>FuGkq^ms]khF!i;Fq\nU0Lci}^SνW<\y 9{bы/һ63\~MWX92ٴ^nlYƻAXoq5o65O+(neoLRU.A;PTYdqqOΧVf'9'Lʞ #IRYm.Abvin_"-QSKqT+uP%ͩgn?|y^gi sxk;:ja$ա7 :輯.t?c ެTzfujt1+77N*UJ*2 ;qtJ<=vv+nĵF󮩃A`;-3^qOcSM]LBhœՓ+K+HԸTjK]%5ċ7fdfLR +|тaﴸͺdHv*n9b=ˍ}.%-[T2\yWl W./.=LT`s°d!v9F-@ī3̶3ڷrl +o"3 &*R"| hsb#Y6׍WQ +q%Pj;v,vb>/Hv+{Xh8(?@9Aw#n +~} ZΌ!Tb^,1!pݼk!Lq_"FƒwGu \ZdCs#C[u !b@; Pm br-YdyG AV;^c^k#Jۈh۳G^ΆaF]zur\CH& CS e~Rh7FR8.|❕G#?W C> sb^I +DqWV{s幁C$}g*}{Sc%#ȇwv`s:xÿCzø@Žk0[gj}%y. ==Lk^C݅ty9+E9(>O >yeع%[dyBE8]Æ043h|"g#hfhRD;gsUܛՉ{YØKcZWNAm3c=?^?'/uKXAykxm+Ci~`f?a U0vߓ1 |+)"3pՎY.; eskVtܴǒ8374- TؾΎ5_R&M hN3/ 7C +tę橵6~ ` +r[o!LJi}20^mt>@=} +Z&˚Tspڨ4tSK~L&A] lvfu)5q>Oખ(vS>Ən]rYU3uwnކѿOy}wPެ66t-e:DZbP2o-J>6~ VgphbG_-pNm\W2Kfaߒvd0}Toi;d"})blJ,z[qxPB:|t--;*[|zR?)C 2 JGMf-`{S /$vr^EAQ 1JyB9e|.)́=!{ֿ ߢaQE +YZ^ٽvgvZLɗ*?񷮐UBCw[YFT.-G&X&GʃUS܌Omq1g5{`BKJAտQ߯s`ӗ{{Ʃ]n3^#4KfMUsCt7cza㓑gD\&t8L͉c)`έ=;&wenU[epZx//VߜXR^a=W}Uڙ1|}HATWtD7R3]Շ?@v5mаa׾Œ~swOͫ+1v ^7zfRU2p}#1QIlf%UYlutNv&E~}w6|m| q@!*ݓo)jˀ8gӟt]o]ӅJ6yQ~CbN([l@9erȽ5E 31.;>QkI| +Mb3WntUQenqusn|>= +={h\Q܈?U=#eYD o<:҇8ׄ4⾤* 滰l_en4cinﹳF3(ZwSgm/=]nr%v/ hXlwOƍLUա}";AP@FkS$5kV1HBҰb]dOԨ_HiO GݝEK7ڻ[MUc-iug\U~M5H`oõ{u|m]݆,w;~wj?JsܻuoQmyX;KErž8Gv~6vs'Υ_6S:CZ}|h6+@x;ޯ.oqu}{%,H ̝a,!ʼnm]r~^Z.̶2l̚c,iGS|QqޏIX?r:iܽj¢[.- 3y[U WW%.5H:M9kxۢJA&YIBLJvԹЦ` bon@r}U{Tb1D*qF>(aoH>S~ڽ཰P;cc4GXy mj xK7thckgcמ箯~ * 0VRh椦=>WjN4<62Ҿ0īj@6ML_Crח%fG\__ZmjiXzZ?H9fur̒ )=#bRN6>[hn1dʟ!^ +¢ݣnG?H;rL[?ob cѰQ xDFqp[3+JyXqK^'UΈi~9oU oCy;>{鈹^x{L{2n[ r~)Z>VKq'Cm)eƕOx50 \ {'D7P87lkajU||S䛨BR$䋍\b;Zt')U2v;鷨=p\AP (**qNj{vRg;/KvEM䥆9L>K|61yG;quK^xO|5y#H|>?V7d_ = +es9K|:V@|ڀ9ekC9j{)dЌiiu, I>#@~דş4Sp8Ce={yo i ) _;A^X~@*,ד&6K6(]ð<}jH6%y˗;O(ቴWrnm&!k ߛN8N hTy@dA43q5 .ahkr_s0Ḟ͗US,.=){ָ+[Aޥ +/뾫ۍkfkzy_u%㼙QړF>@pJ%g*4BGBιm*.MVY%V~G1w^mXJ[ Pf++<((0! e83z>@wTe B*-9y;0Իa;O6ۈOkj^+mpyj/e2j s5bxX|kɁ4'T|/^z;A湬BG-Tڿ7u @ {haݑjOQ׉q9cjz/Zڤ9N')lx0*\vrw| +mJJ]?ޱӊyna\&g?˅]kw6VV;T Zh`g4TTj33 +ft,ʲ<Ն%/xqsܔp߼msh$7R3Im|L|]?EJ̄\Y3"Xh +ݨWMrTV:#Mng >+;U^.{ `PE}L.] +&j+J[8 Pl(>{6f*;? k_;8NpMdL~ t9,,nn_$܊rff-ϪX|;?m@s1q=SLKy#_b9{J{Aem dV[zu% VP`+gS ?ȟ"cP8hPjǗx +ww.ix-1(C]v8W)%\rxM3CQMU{ͦ>:w!G [1W.=4tVc3{0" ԭI׃L6~&L䶪y+Jy05tJS 4'{&Jk L%K|o' M:[e -]7oyeߕ?Ocx$Tj(QػMZ@bTK89e1w><7@pV<@M6NK|nFov]u}z[6hÓL׍?tE_8$Fy/Eyrdh)g[ɵ}b[ %8tx/u+kMɹC-x%޹뇚LfHT&_̫J_~(?[; T E +`y9CVLF&|&b?>9uFfge;^o(m;H3ioҘDK*7EU&K9] +j^ P8ܦQ;qs̛QB4=fӄ[G̣Y71 u8K\>ʰ]+wvB<EͲ1;ۄCCDqT$*bu +QY'BT2m~C5Y_P޽(|Rii߹;a +m.WQ3F 6޻>8۠i8y1;z,YbvG {q!88Y^,1O{mv'\$!]t^fΕ\Ng݅㵲I/=y^ukxn>&#6BϾb[ :\{/U8?t/0?owQ6~ +mDwD얰ʻ>_̳KŻAҡ?Ssݥ)lB B[aK +6Oeyl#WΛ6i"o011oؔ%əS IZ3|7ҟat.Θ% HtnY=^D.3֙Ő_r:/})H9 XZZOi~|S'~8"D΢}Is g6g=g#AV1W# /Yfբ$q=+rnaڭX Z+d7ʬ~0ځ5<#3}T*tg[vـ#ɷ A``;Mgex˱fx!Nn'U5:c3mcg6FI0r=HjR/RJW8t˻5ˌ͍ hX~OC |YevDD{Gx0Ϙ9ɧz[7SvYLxJqLOxge#cnBٿfV}u8(ǟҿګ}bBO۸_"޲_'KߠSyY(q‹cf36^YiC6i 銝{LOTЧ!O=ky/fNdd0Oun}JK^ zxL`Uu+~plAvB-݄2jjZ:a96XE6ʝןCs>!=f@{t1XE-vW-lSmn=mB\sRv(FhW۹ʽǯJ$""UO~(tp|NNwqAُ]k''ik/4ܰ9(R^=^֪̬S업f9:#Rrgp^O'#g!4 +#3$r [ CUuv106s,@b 5n A6XZL8b 4|N1R0qS6,fRPA|2lܶ/*]|AxslSY dSmc[| i u +ItĢQ/Y $)  +Tlm*bحJ S0@}O247ʩ+u .}=FBTK.`22O+4Ť8h9N\d׭܎^&=r5͑hwh: 3l~w_M]v3Z@,T$RCdSQl Zqs7'E,Xާ[%z1}bS3@GkpLAm[~b7O/=P=zxvzgR]-@v_I (@w5+%ҹIu lwˇjOF>*ɟfJ\³3ai㒑y߇wu5N}ҴcWM͌MtNGȹSkO/R_ kb3Gah]~že){B(:^q٭D~5˽]͙i$:Gu 7x3X} ך; ~ɗGg?\i\k V|:AkX>D^FYՑYYS(*V2 YֵYq3m%4JFC)9g> &Ϟp[ R{wNn<.r9“g} ;gDh'?)UQ9k-|/Z2ܙ; nNZFYzh`Dd}<@Oquoqp?zUe\VZac;HC`]?Mê}}5~ۭYs v%~*AUf oPpހBFqz|K+r-f;gݤ$'2.E>Miuԣ5g郫jǜ'(~֫:k6elO<7/q6a@^?ˡ58AB'=5Kf0:ȷ/ۢ_ɼMc~!Q{lO|Th*Se$;!:v$w"~Y]hj>%gk/>Y6 By?4Zz352xֆu:V4%Z̝-m<2n4­#0:}|YKu&1BIqŠ'X?ȟ7dR|_BB2*EwzAï/ 'aX ȝl}tSCW!HSQFl;U4 +$걜35e:^k(ua谕ٹ[R XNV%,H//m|.,$Ww88INNjQ +i/d̿炟J1z;WR1{=,)IlYM|a/r6G]s ̪˽5z2N5oS +^Jr厚sZKj8гea|pU)#?CM*Sw#C3 'JY[ V.md5˜]\+i>R쭷kîY$?_Br>Dz4UŋpvsrEH=v#v-OZqv;ݍv-l\|/zE(鋁ѽm٪$]//=s*6dR}as 8 B<: |pr>KN#&LtH[ZbSyJK]O^ }@M|-W.hر%PSRZ6[dž1K݊B:2Y{siʲѠVViPx6(C$B==+2+BP12–B1MB i۠DvA|0+WS6:n;nM +NYT-j0Gi_67nFg-W[ l헢)=B7ʨm3 |8cxӤYSHfM_ әю)ȜΘ0f[)o戢85ϲyTyb\1g9uxePzOΈʮf[҂#,Wbo<ϵrLka~ Pu_&1A3<_>/V9{}hQJL] o[!#Ӿ(7'S绺!*[\S紮Ww\]#jˬ +:pi(Jkt6zFNynUmioEхrTzRpJ6yDI|usUgXDʪ.Wݠå`UQ򧈺~!?<՗V-,:,0}HƜ#IG7iSL ndBb>s,IRu +'u{얣fQ{b^d2^`|br_1Ã\٫jZö8vt#䲕@47K^!~iHP0"jw[Fn~6Α]VųI.QE0,tuX UEi~Z+GWK$3GL!SI# ޕعOI?ǣLeUM"ka"+sc1#7b=FS̪-ޜTo,Fq0Aq6+EA}uG| +fּ&aC67/y9GoxMLؤWt  +*h o?d`=P->2#f5)~䅬 t[s9aoG4R>*:wg!GÎ8A564h5!6j|q8ؾC p~ pJ JrPKoд )Ak=Igg(GC#x X8 ֹZ7ٯe\?^^кޣvNN_`mry=zVZou5 Je ҫfK6 cSN +ld5M~*AݧjԺd܎ɬ=&UKCd UB 2HGFQ$~m~jtwZ5 >>Ue\eVo7}tng˪vBtB~Fѻ5+p WXnܭ2m,ZEyX%n/ br sOA|* y[itQTZ;;ld) +X{!L {7~mQߩ>߻k)뗒?g-%)`;,r30̬3hO{#_f>NtSCadO2loY'2d5PTlR4PVVtYډao V$z qtΕ/ώ#zc!3kLd&-F9~,B|j!Ot)T@ z}:1.PHqNJVu)ɋtbU4kQc z҆'>²Oyf~?_H>@&Ra%>!p$xX^B~ +<©x'?Ƨ%sG.[}4c3֣p#j›W.l)<+O6Y?eeyf+[Ӣ 9!>0A + vm@4`8ן8GG3@q€Bym8r ,j-2|VLF;1@^(2:g*v *XôjH*Lt{7Ap_XşCc$D^{I Q zGo( |S)h 7 㣗, \!l'bs&oںcgZoSy$kkXM4vw5Er]ahr,C~Uk~fn=8nl8,h2w5 49!^Й QϮ2/vd+歏ky7pKKzfW1DFwAڷ`Ze^|9M[?LIXnxk>u-eK l@)h%w4q:e^7]R{WŠL⁼~cMGC. RRDVsC18'@T#Ι4 +܂]s ჩ w SA@._@ gg^6 67E8t@Zs#u!'"O+cpS{Dbn="yVubw+15>Ik9[.{l9h`/65~|ӟƙ)dnLN{ZpUg)|<*FEa-$Î?tc9<˻%(b6]fioAl;_CmЃܮKȒ5@NASj<"d ao^͓/,G&b]Ъj0ƫC +'sNV:dv28bL֯ n6ĩ[) +dxmbyMYo{e]}W|9_LUJpL[Ot)xMx V(A{yoR=+՘ٺ^qeJݭPC ]Sof(ei?fQ05Ud0QyxMe:.Cy׫ߊ͵PJF=tx<ЌBCi}kKU`)Uq3_{x]SA.б,Z.Z;X ufz g%geWgeicV8{߈*wzuqYVhj2*r: =ϭ0+lvBbFHfVYN[akL:31#6[?oo>[/\( PO~ +m_S'_K_oK.@Qkclul)WӹrxIfat+hoƯzn21>8K6BuP[4MD Lf̽ +K1_ G>֓CbQ[>yg;#`r5rt`iTAAܿg,(f/Q愈i(§z^>`BLmj㏕jg_L;rFtdo/=:D9> wx'[0D-103hOY.HFYoek.^QɴE{[jizB̴Y4V&|4䆹.^2jO aR_oz˭S!EP rrI$e#~v&lM^^nۛq:aL;%XXd']sU{6R=7 +Rqv%ZUnUwZ244 [sHbݺQiIM-xhx F 飮}.޴!6vouY@-"$Ęx̐F6HU)>z⩇͆9Ɲ<1+Scp !|ح69:qnxz$Fh\~]Z֗N }۞]Yx(/iȟ^~}W>tr|'֥ήI/ݿ2F%p:S=_5 Pu=)ǁ򃄝f^lysf@92Wպ5w}񵣞vח~*؝Ck$*5~^uϻ!MQk^Aj[ɮYUOl54><,&4\E׻rCxp{B3Yy*:'њr4WaoٷaUo 1εnl_/]rԕV}fk1/D0#kO)%){k)2nqOX;m[_4 ˱ydy Ix`lhfA+jb˨WU9멙:!urȤY}Y0qa~SE4t?a>av[,2FO}Efá҉Ѿ˰7{{ M.MU`buRE5@ge۽eSOJ丞:@#rTM:F>sbڼR'IIF?rU`mA@B]`5!BV?WS3SEOXYh]v=p3 "ªeܕoҷA?W59#<5: EVS4U&|KڗO/ wP{㉟`]S-5cX Yi`C!}*0wlǖqǨ~Vu~|DD+ڰ>tLgȊ%G1+ib3M &S'wcŗP^>9RB=X fc]ތIAﭖuV +2,%m8c%enwY ʠ(VZu?' N܂ޒ,i9 >K +[2*1eFy2ڍOy"7O?Vtm+fc8,IN'>$JhQ!o۟]ꥶ?=q՝m{=}UP@{h{~yMX?[*l#|}Uȣfd-rQ^M\H ]4p $> Zk]z*?dPmJl~YpMx_2 lmLzMRR}- :Kb, + +\D 5;TW5pفp@ QhFȟ/$m}&{AJIHQ_hnE7m=;y-4(s tpBBF½,k%tHm׋Dӏ@exJ,>+ 8 ~~7sޓ_ƿ!C3+ePm-k.лV)`WMZ()8TiG8܄H)̟NΛv#ހ7aXV1ĎǔH#89O2(Z/[#/`.϶=ښSͤ[Qi`#{$0]KeDsXZϣՖh̉>4*Ϡɼ)`7Y798/?П$+M^cp>.NU+We6>ņM)/⡸[؀?2+6 N:>yM +y/3/suczh +&qa*a>eyנsCjU5YEIDFhs.6VV_`h n+I/@dEg'b2i1ob9 YBTn@nFݸ8.et~iFCIۮ{MO+'KFœRmʼНύ +ʒP gessRdӫ5|~J׋$DV/ކȦ(DD̲Цq*61eyT+ȉמ& 6ɗ޲[H^vBgxxp `lkl5B<(RhdT}8Dyos?=[Y%%醫iJj7ŋgg{QNƽY Sgz)uJ3Bt_O]{8 71j` W2bS!l<B5gE@ÉϿom;wѹ.N |)Jm M6mgӞ'Go\zJsVhaZe2&;FG#_-gY3@koRV!W鮈 +UեJVU3-Y3O5)thp+-/Q)r QX&|"VAis.x4UC%]9/N&]ps e |as;j +p՗du@*:;-J-Y9K2a ^^ї=m6`fu9S[36LlZɁ A8-sͫA"M"\Õ)/u4Ẅ́:{v괯Z~OŸ8AM}R:ZqȜcbZC fFEQs[4ڽ,ž]aL#^.(_'p8&H7FO}5Xt?#wAw< pAA~F?ص +? BnucmnD@AC,۳bfxLNo .ʭg:u!nyUf5_]to5o3V &,{NۆyA5Vׯ(k/^Xn?2Pȋnn-I9dN&C[4O87;WTL>~MTW]q?MZ{y\Yڡ5tz +( 3{ö0*{Mxxj4xBsohEjԎd 3j̼l=:kq}VomT5xʹ]&&РDS4Fێ;[2`%XIxR3帷_i+_l"O$]۟o^Q}Es &OX<yIxqCԚdڡWӲ*WxW@\\P?<{t «}iN5 Yev3j]{e|bb8uUt.Wc7qk4./H.{v.XTǕ~f~8̀v|΀fO^IӜ7ny|w\Ob4~yx3bn#D0VWή \D_ b+GM~#? +={նd)wi<(6cv +e +"+ƛwO=J^o&>]~-Tw Bi_<ܳv}ScULИ]=܆$GoJ)4ZMI(%a1,+$#&cy󀲀/Ya{}l! qӐ6t-K9RמaGr\4Fp޿2|eNs r +V vCF0JڦE[ Aۋ e'+BF=+2ѻ$ۓĂ%hsFue{̝.Fz^#:5z n7:Wgn+wqТϽKR nrՑ :@.8Fμ!NqdE7iFC2Y\3|3 =/[}u9כK +\7v%] 580(ʑL= *sɌz$nC8A#c:WT8FEnpUc?dnϋJ2Hvz6Z!ϻ=| 5rvqwA,llxs3knM.`:WfSc*AEReK HE^yK`pˠK%ȍv]ݛRh"<<1 Wlv]uZt$`y`:؋Z2٫lm`BUK:&H1TQ]#6`y9SB?@QRX yύH1#dn}3wwia٫`cažjS-FkmXJ:y9dSdOÃ7 Y 9Sq'X,ael!T?dŕ},wn]A!t(cgYC ʉq<FU:ꄿÊq=̕2)2'/*yߑh$E,  ʽ"Xh1c;!+:8q7o8ѳ] ]cRc䙍 |~k󦡘=/;#n"/_RJC*n{Twtc鷿-s-pŒ޷갷}eo3~)C4뗼ɹٹ:{UL{t2Sl }mT=\UWEWBP wH@ +W]Y1r|go٬ʖeGdc^h'䜓|\6z!,u`ѶD^H׸5Ip]](R!#GI*c`Sm>=1@o][(wt)N1{*%+bG +LtΘB[Yq1>9/ '/s`djyacDײ:u6pY +[ܔ&~u:|'=sf^dǥ™j^. +֒ZFr7C&rN2(_mC2R&oQN  +p:/;ym _S +'L}>S-Пރ{Eqˣ7vS'1w(G.32 +s/)ާC֯Pqa\,z>Y)RpM |"$\2A0327?>z孶 boīfGn8'`lD7WZgo(NkGGEPKB'X?J +Æ [;C|i;V%nS޾7Rdu6}&K/@8xFC<ޤ Cⴜ8xhnTd%]LEJId!>s1&Q%ۇHdH "8h Y+&/=TfMfjp@B?@h ը>Q9EQ~o{ bjwd#0lg+t +V'^Sj6D!W5 +kxp.K|=>za+g$zV{릔t<5R/WhYRٹ{mv=3SZA㣨Qejg +!.Sz waZt}qnr(S\6v>~V4 -+7Zwڤ[|)/ n37{,b5䨏F,[զ˄-w%l{3`Ą0R|NQ\M+jcީ1MzonwRk.r*K\ƪ g쭊'!+/^ǝn3X0`W`v* <YՌX gTku Y^fr؊O=q\K!_'(vBaXBOu|q`L(Ol sZ;͹y\`\VA1']OAJ.x9] +aGŨ`[uj1ƽ1 #`8E 9T3g?Aa3'r 93ݸa3)G:%M<ί2MDId皤w 鞫sY߹{1Mez彪w`d/ /?jzս 䆵da3]6'9ǥf~ZҌ[[*r4A)͌ftfW7~~]{By~/`+*T}7v|wwMZͳ۰H.T\e<΋m=鴍y/e< tл ɳVoKxD~q/u] +sv[Дۍ01DA2yS?γ88yY5gkso:.G秳{})Sd q(~%--|EϮgw44 9i#-^v1ǏTgj kŝK*g ,ۧ)ZXbߌܸx9\0{Zwﹾ$B-qPm 0(ػ['hB}^>,c莄赃f[lklfdDdk@ h] vV2笰|>ܳ藘W{-:#'!z[[zuC+xzoItq^'J +C5?IdQ^*-{bVi|~1,1,X: 8>|Ϛn^U՛U*t}l}5f}Gƒ9&^|4r\1:_kt|f5CaV2k258YZh[i|(;>rW+q -H@2)ۖz{VNPyQ*'|!#QzmJ&oNxZl쭟'Pu z[ZU)ʋ[,K4QhZ(M8u"D*&(N'bP6y\zx' +\x3,-mϼQgv+zzܵAE +AmlteJbIn\YKkv$ u|aWN&&x:|jyD9n39y%} 讽a 9[[$?/3]2AʒW]H#bnaa'@ 3m[ z&^X dVz;gvXY!Rt{kF|aW_ȀkgzM*T9eKH}Ֆ)Jڮ +$k `r·[Ҋ;=D`o@b%wck<ۃ:^"LK@?~7ph⓾hc]3C۲2_x\hoR%4/8{on5mWl̞җ 湒XF+ufrnEtsz~;R_)RDsfli{SۃZ0[TƑj䑸&IsT1K^. )}271.= g\$X+a4mU2bMnIpWUoT +_bрHD #Q8su]7έL;P;oDγϮ:QӅ + M=T{l\X=Kw;NtVjkf1 tKj%6TqdPuU=bp0usӆpN,8S#Qb}iLPR 0k +KhƤWrFŲN +?-(y _zKӁwV>c6=P>#Oʼʘ^FAOVRܐp- iL[DKnn{#T6atSPA{u~kk;="kk;:ls?XѲ=m +6q(qlg<[Wh0[rih;JbpFQN^"bbJcYCը /_YeUK%eep 7 m+.lUMWp /}g#Z>7S/=?x$uL:__uqszXq֒ѲfaQ f|9'.^is# s&u4hvNMQYΘ1;VgCޗ +tpŔ!V 0_aA+|a/; ܥyzwb:&_T Gx;}3S=Q RA/G*Hrs6 GtNRB\a-ہ{f.maxMliZ9_s2yN8_d_9/~KeV2}t[=yaoiPSǙLQZQa>!ڝFen^LuD{;KmQ}R28`/q}þh%Rnv={]DKZ:\>y !Z6DZĆPfu*E+68Jֿ>ߍzfS쾇5ޝV5oPmoo.͙U&6d؈qf`{[Ѿ=h>kuz 3+EغAX.+P9PW.U +UJߗc۶x|Kz낥LhnVP +XTh#AMEz+S +< +]WMi@i2& `R\ s4/x5(0rqr])Bc'^{Z{s[j,c+} Y|`. R`C%`o:yiqEcLSSvSfW`jJ9f/jL(S6כwզl6RCy}o?P=ܸ~xw@0]|D ]0Gʢ .IL?YDY[Gfx ́#Gw7OB ƆAWN60t=`0F`zkm`iP\ݚI_X)p3%sn:OQ۴ H\Hb; ijsv}C/_~_%ne=v4x4OӝӛJ Z\Fscur܋f_/{09G5`_A@Φ ypN兾5+_%M?+=z@8>V{?ts =n|~ԭw-bfcyuZJFA5s~G%qܒ+&Tqs)4Ep@1͑c{Ao*҉{s}79n>ϷЁK@X8mJVA r>uީ5<ռQQ~ {:"ldD3y^eZ=:h~q!pB'VHA;;bGQɫ hz6|nUb9QcoU㤴&Ef(f\>p=ݻ#~Br᩾hN@_CyM}fX[ygz<:$!T\GU/;-7;ḽajǒu4p~C}U鴉Bqz\us +ȸlxB}Vro̢/2xnDxelFƨ,^gj98yuѺ&:Px+ʽ-+cei PtCS{W'S"\{7脨4X̀u@Kl6؛4L,dƮ:Wch-iqqVDUѢs#+TIڒEE +j>kß +/jbb? Ǧ +% m|DF^vxZ*5@6[ #Me"-买ťq`HFىL !Y DCj|=7!o=J +x1G =ٶCCrF%umzC:WE{/9U_ywIQhKCi p&c­J /-[7*ܩt96\g#9^ ka2cN향|vd.{iᬰ0 FIVp/u(9M*/ѿQXQͺPÜ#|Sl8Ȉ\+?<̙Ќ"0 +6_;7u74*s"uGN pk}d;zb0IJM!n㗓)qnp݂g?cE11-Y̘N)h<3>ѨOcCVJεh&vOœ}hd;jӬM%:AeycdRmސ^gH "=Ȃܑ%lp fH%a$K8יp,317䳥.Tء/guӻJ buS6 \<\*V푥~m ^TIDۻw>A&u wpJLK* /w^dXy ,VʐgDw&B2Ϛ+i9};Tat8>$d nVTC6A8ż oǜɢJn{uC}e=ֻӒ?XqRheoj 6Ǜ|=F9B"ѢlBoBӃVh%<M1fvLp_Oz6{F[V}Z'BF)p||em6zX}TS(4 sV}}TZr.n}l%({lf13h`}wFfEG'A^F} +kn#N#"F[oXUoG -}Mlx) ‾yT4u8]{]Qe#q} "ǧڠ̦4zA<Di &5yx/nnMe +X{$r_&^Ժl""r2ŒJ%/%T tl~|2=vAA-Bly"/uò+9ٲ 2OK>WN$#>e@_A_" (JΒ((b9wzs3 KJV. s-!uO UwlBnWe/K::} ƇJLwQ 䏄l4$egWz*-ng'k.Xy_7Eh){,#I1³F0*2^V_UA _W3Sk" A~.t*cv;`fM[[yay0ZDwLGLݡnxB u(P*J7 +;y.IĨv _V~ %`vjV Ur;R"μ. Xv5(RtA_4ۥ{qHsB ۜ^+s^6*UO>#.f y!DaJ^y:5\U(yGkM/46 ruI2 xb5B]|F{hxif̱2ᡭ \PBOlj5ӵjNI_q!2MLa)Hp;UB_閘9^JLO%mm$^[ͶgmWDDݥl ^#WGdB'KL,J7_i[Ѭ#42rhg$xT"oH}*\Fz(Eзk}ҫgI^no+s4w>>[A +vkn^I/m軓/PqzdA2QRN:*!_lTSф+͑d%Oƾ7[$z`X<*~]]TR*'0=jztgкep[?-%1yήZ< *N6qޘU:s&?zE[dBfU_{/_knIQۏ湖{|=C|S:V "O~2-F:@5# 5vf`w7MSVcIwT_z +GDrU;2NB'~`c]fgQ@,p} FHat^eO%t?ƥՁp/رrSVN몽@-^% kv>pNjY'l@"U"q%RBTOS x[YIFwqe1rԲru:jD_lj ÷|>~Fu:%@TǰL$ANڃ2+ r͊/1jږv6ȢiZIW?09f,Bq=@)̂J?6Nl"<sxUl˝F=D]FB 7+:ZoEtFfq2=- gP}hbLz K#(&ߏ|iz\[~lnc[>H+N]bח)x0|V8˿_OŦ11.7o=yJff=}m. +IN0;tvQf/zw:p6K^^Y&pt{ڈ<a=睱Es\{A|W)A%jּp7kFrM/-m>:sgRr* +:UƴUtZM8qbM} c^9cȍ(c6kQy?|I}ڳϏ+klR=cӳvۀAB[R_C}:]Mim'˦+J5GY>|#)ι1Gw(Y^Y!,Qhg[TƆnAuPhV/-tKSDX`I3YY3Odtvcb{==~O,nβ`neMi>@}!T:G`oIU`#T# 0m|+hxCHNlEC0|Vj 8˹ĭ6 +U3|rp> Hr .Q\l +> nR8[[6}|lWyN.5 +i%9+bظ1?v?Zc5f[iy%e­c=ᓃD}PY"{$@,O1Co364^LjRmdr?ǽb(cO7S#'`9L>.6!jp̢G.Ŧ 5jfSpW9 aOV9^Zbgl޲ gmȤfaIcՓg h֏eMCVNu$֥ *WtH9y?B?RJޭuimnu +,S2xsdwzjhzi%`f5'Byw,j2j% P5I3633iM1lܟcrrZ#=/k@sv泲&f㔗ݭm$}tuW14EvѦiJSTi +MHs^2RHN81ϑm~2GX=;y8{ZgwX]¼uxlpԓPi9cjQtŧi!=J}˱ޞlr=ϸ9@9QPꊾRԦMjRgả=Tx@W;zs90leUQZ4PQWV$gW2WFj-JrI.(29BTǏ@$ ԟh?D L.~9{ 5.F"[ ՝.v{4yCjU0ѡë͡`*vnɨ;ghǵyhNnK!'$rCZĨ!DEfYdƠnWE!Iѱ2hŀ/Wf ~ m+ +ʖH$x^u=K!s<FS ZJJr UNؼtGDcK|Y6s,RAˣ'?-=|gE|1܂hX/98A܅1T9Oc5\婓cfc(xd'}kv1W*5W@Mv@K##\ S;rc5ی–̍]ȚzSi0N͘&f}2e2x/Kp){- +mOkzO%Lxs1^USxg iǼU 5 ِKq }R'گTtUkFR}J1(Kh cwGl5N?l9 ]`-?);DY)?Swgn9)ͼHC' *ڟ +)kO&0$5#{G|@FO. S|LA{ <çz5vsm9j/3>&f/MM*4h#l q;E82āDf=y6 $: b1%cEHGmGx8[n~3]4۱I֪iLS;D5X 'ӂ^/4I6;ZlZ+ܰ'h1f9+@"@M|8`J>'ɸ.C{dt_s~};>sF)7Ŕ`V%v[`CMs zCxw*s_,mhA`vCR' +Np2p8 2V+ j=X3^/=fccZ9#5YShqǼLŕ {/I`/þ|<@0E) G˕ g4- +]Eħ rIl- @ uס!kUmf69d'r06[ i|掵mvEN';7 U (H=ȉyV(ETFG@]@J$@ k(c8 R#, + {я uǚpxAuu0 i)*S*n|L%8@?*`lb0#0.0Ú0q0<Y~L*Ȩf(.=%v^ i~;W*jj_6/[t/ֳ\h08{@/>tz +xSv#z=]c1z`F]J]aX Uy߻_nt8SYhJ(e'bosĂ_=qn|`\򌎭,?_8aSsɾz K#ԇ1lVy>Λꤚ_:Cz72/Fh~ߚ:Z dyb3:_['(ɅW=DXvkTl:`Mu k;~W˳N֘,|?ܜ٘ + +Ypv=-g[?ڻ a:aX{έVÏM5v\ea[닰GOD̙󛟍:tw*}t.iyF&FYxuE"ËTT=orP.֭ə5JFpuG'Kl՟/š?w^u}|2ocR{) \[U $lHlm{+U*#|Yw_d3ϗA_AL]zc /(k͟%48Nu0,]:d ?}%纅' b׮ס;$zKC)WGv?מoŸK]FxXج[A~y9{23 KhZn6pHg6cy5Y(DUqIJ蓰hl܏a@5'LװN}k}zG)|Y=fFTOdtޘdx|ʟUSc//SE cL",S#@C:k|^հ^Z}grZJ{t^ʦ7}KywY`w_}␋o!c'9,^݁VXkGΩwJ{k z\~gP~ˏ֪'-0XQ|+W>GI^]13ԙaɩP<s6Mb/M +nTfMGA3} p֫+ 2Qu;7-UiYѵ$ðm,d7l-ӟ5 x1zi5(_Xd 98uõPVEg&7EnM򊮳rLrp\1m Ͷ(u\];,6D^Q&!}CnZͯ|r).]*uԡH[M +V^daq58{A)wvnz/Uh&sv1üg `f0IZGyIql_7te~nsPrc(% gꕂߧ]+^ ۭ=dgäy]&Yݾ Oy u{kIK+(jU+0+B}Vԗ{8E=vZI!+to?>!>y<,:;Slit+F@-YA aӷF?>zS/:>.Z&sZaԹtTj++|W gxFJ+{H}9ѫ3z1:ӺjBܣ|k[Q~vŭQ>Pc:siӼIc$[`.zMKϮցP+D +C:ňUq_'q$[&7:ܐ;XX2!^Q^kj况C=~*nFnA(O=E,L +jbQL'!:Q+[SU#UcpR;Ty XMtW:C]^'}FbGkկ@Ȭq5id{acf 'P֦hE%VURxWjZGє!;%+7ΣN;Yߴ]-`fP$8 "M^P[,O8ū֞i{ԈQ>Kk5UĦMyq:]uJڗz8M*a^G6\Iա$p^"$iSȱ͑y.bIpr}\]޸5ص^`o,vLtY Tm{\*V mvAs,p8LRxNE,@% @|a9>pJyseMʙaU`` +Qge8[.IyOTcV8vBYc&SP@ hoS}IJ"bMT#9?& _8$Z_f+!F]މځgc+Mq|KO}L)[v-AiJ%8r mNhًȁ_qI'0MHJtO&.f%nCfWvaVOQ}!)^vŴ&h1YҧOEiN]"& I>eb. wED3T%Llɍ˾iWɣ땖M +A~G?PVigƗ HW|7bs}`L@j9BZ[|nYB`ʨ++lɴKFyȳ[ȳ]~7.70}4$ƮcYMS![3>EzK6:ke=myi&ލލ{{ 5ɽ:p ک''u^Vgg**<]Uإ.UuW٥%wrmom8*~ߋ[#6n;ٓJF=yabĤ6tV9AKŊ^pH9MIɻ3.D]ݴ~!&gx{GxP@+);4+5(1e|濻7d+Jm2ǧs2ߥmq7 VBUBܵگH $[hRqb38axSH!~sKq遲Y1G\Wp+P+rZU3P;BؿWA ܁ق~|d ;Zpm 0ZPu:(ZvtqsIv]GZ+r u.ܝqڏGpN:[>7۬\_#/Ϛ,#-ȃϣ4}zd?;i&j\* +Z_V_ڿH7e'$gpwh6t[z˵T٬yi `贸˂={ϙ'gcS2f:1*-41#7$q|kcx/0oy9BLhTj Ϯ +k$Y:wO{]8(d8J4"Ut3YĀVdl cn= +Po-c̣pv ?˿E+W؄b*N~MksEg`O#̦G&+ASӬ n0`]OGxco1xE[.n}8~0Πsg/Fr!|#m(PNZgmVxg +<2wizRÓ&{K?ꍚʈ5;f7n?5zQIˡ;nlׁh}eC]aͶ#TKt;s)Uѿuƅ/w=R'{rWf8X}.%"'}+OrcTgcFnOFS +R2j ʛf,bVܸYVaz- +.f)UZtc䓽.e^^¥'²إ/&2xgsl5%&qүHh)YXh>ߙCߪpcZ\V~@ԮC&>Tׇw9üws!pgew ubvBeoTсm _-H2ͮ~,mnN(|aXzM{>TnW!Lr'{kݑXR=֏YieSTy)goYrB:Љkc|a 굒l_V>WX>cW]~i&^F,Efz?Ύv,:_gw^\5q.r:WAy}yn}I޻ybp[@iۻA ~ ey!<qW1;='O+%-\AxNU;=H-?NTU]}a67B}q順X~k:مϷS v T.oT[ muY0tNP]=C-&ĀH~,ռ,j[#U̹Q_yil底?ӗOlX֘{Z!l] /nj+$A΍L;Z;^엢膭F-_YHѱRْ;4hgV䎛*NOgtz?Y|~qr4w,Cf=@~T^uwY'^}v4Gr;NbY0BUK/9Ŕe0JfVH:1I:5@ +"7g7{CC g+_v:kӗ[Jz+Л[cA:I6S:ظ\BR4)"+U BeĪIptnfyRкkm~;jthV '%res_6U{Xu'ygiC{ǚtԖgLuQpn2r:1pRI8Mo#=)q^H~"^k#l\ 1 [)rnἹ=)@aY\}}^;l)x 5|?#:z¹L[NKR0s,ׄG !H>p:r>O75{6\i0".x좻߰FX2/J ^R*$_36ޝ@Q[ZiXdqtc%깣xwKw XAz}ϟf''+KCVs Ԯ?􊌹nPL\9L]WL= ӻ#SH +2=w }X3QQ&Um-.Y^{Ɩ.+7Q6^VͅOQ<7}\eXmk1̆v󵷘{ݜnuwwN#NaR~ SFT_KH^V3bz9.$5ГRVC_rG-?HOPXK攢r`]`6m[AiW+4tu +6y+0$}'洈ֈ6%3咱eVyCh,gI7dS6]?V4Xn%a$\s[~FNx =;2RU%NUa>$j:q.8.t~'^`?Tj=L"Vt V[=a֚*jQGw0Dw`תiG7/Y6Klhbk[aSm׷Df\иJL]>_XB\l<— {z Jc]h][3hSqmBҸVC(䁊]W 5 5|s |;k74o ^ RU_.RHL8OYl O9+o]vB{1ɢyJv/u5]@ {vXpQT4 kaU+ 7h^Ĵ:t=DKiʮUf#eO{*q$I7n |{湌b rHtaˠ>Q]禵ԩk&}}QU1eb> nTNp'mJ `.ӏJIn wՍ/M^1y?vc~;$C*z!D]+Z*Z1awf(qv-(wr\Yg*}f]7V +aTżl|ޜ +L#`0@Ũ:]հfۉ.|< #TZ/Z5uv̭0Y7}DPrz,Ux60 p@[ @9aʃjp}g/ Pd6♳bzhPwlC,~c MXy~#rhFy![4%+#I,Gd9and9}OJjȝ[o#5D%JIt8?|j#Y +SiČO.}.ucEAixi}j!|@v1/ȹB#l +pn}J=N +{nZ2DT:CGDu["TqDAAAdEQEQgCu*ȌxW f  ) ` `% XIK?F]pizӪig)V;ޓ"iqdb .7ۤw3[ҭLV3`\WO~q 8_n8ݢ_1}o%A~u}VЯJj9~I)u1K|,[8Onn9q[<}P/>}= $ɶ&isk_ˊUB|MoW]}afN +fV~BQ"YGgvE哈n [C7HO/7YkdW^~EϾZ|2"vk,?^%Яdnor$ݥ8\z`_̛W0;souxv1ڇ}fϻ޶1{oAw`}Vǹ[W7cZY19ؚZ~SZ/>FboP>V~FH mb?wP/b^'.߇z[؟?P|hhMcȯQğxdzeP0y,9ՙU5S2R{zv)yΓې~O<>>-?u,>k[xl!1UeڋtϮK9?*^~-gLz2uNg +Z@plx^B;Ef\a{8i%%7:)zR8/"?tZYq:&1`۱0u4%cersL-9X}Ɵi:>4Vq7/Eʸ1k6 qj +-w;<` {BeP_OyoL7C"2=sri;aKN]\ED1 +O1Mao O]l彡6烝0]}s#9ٿO`C{cLql#zɋ3uMO4o*s܊-h$ߝӊ!9j34ɤ9{4VO~za-zOtB-gmחe7ҩSWn޾xA>.̱u4.jƬ~Z?Ja9R֟KUfݳ.UJLWJ=k0zQd;9y~n$~Ui<2*&c̷iv8i?V!sn]מn(u' ڒlE]UI}68ֽtKĄߤOeM_̸ԜX~pmu+5T6exoKԬ{gXy}I51oRwt||^bZ*Lgqw<]Cz$7sC45#v.-?Z@J)YfSjn܀2oUz] t(gk㵾 rT#bU gK8V@<@pXL +wZZmOLsQzBm:fn˦ ˯awg7)T=Rәd4%7;IOiv^_CVjR$orYY˩\UjG{W2_bvnLosR^Oc%س7 e?~v^.lL]x'aY礛:8lsV?!- Z;9|]S˕P"QQ+-wF_t\E4_陝DTD;Nh>>Q4:rs޲n@@{u+qYLS:! JE6*ߵ52JN7ͦ\!W[i69DžV8zW_lyvYԷ/H/j{E}Mͧإ4iM|wZBuףRW=^iw|]ɪԻ Dlu+vŇKı8Y|zIEVB3;Œu}Ix +_vvD VFZ{}G.ϴnhD: ZMu ei\[]Ukmxdj ۢ_II|8ȓ(|kC*{wiJAшjuṱ|._|YbC7 l +SvGW2WܳT>J6VR[CN >| +W'/ð*d Q,MumDB-LɹQ +|)o3 s1ӢeI:-&iBZ';K/ _6ͫ'擟tyg 53MEyld3bx/[4Vtc(o _#׭7'8zYT2A2M&=c#S1.dS@&!>&M\R dȷCǽZ_[-Fp֝"0:Xz-+,;G\]ޢ'̵^?9B/$w<.KZ޼ XhH5*[)E}ɥ u2pW)<#T- Ln:qB>)5@J䤘E0O@xz +_(o ?9lsiq$8}5[ +]dTtNp5\'J2 GtKj ȼٴoOoi=\hG@>vP(d58 +m?{ddko Ts|%[,>@puD*yр3_|o3Wݒ/W[ ] @YPs Fh +5hpe0r۟0>VLofղs)^32-S8QQ +ghk*W\BV_=fxw;ZKwJ=Y.p=ɻ@\\uRX틿ގnGO|IqK[ܔ]P$P c=?c@Þ8zEF<2kA>iA?`텣9Mgp1>&;};6'u}62)ڕ }|$ɾeitjZ]c\̻t8cS|+óSm9*(UmGv14vnG^aW} {ZW.~aM*:u5!)lpDΫz whYy2??VG`5/7 4R8zY=CvrpV*0WI +ӹAϴd&U7볜tV7-2e@x2;t iMyߙ*aisrYh=pܽT.Fѓϱ}~{%}v!<7B/=V).,BH3X,sxQgVjϐuQ ,)ZӳUMce0\kv1ykaq/RmOT+m06}8ԑO:mc*ɨ,?6\H6,MN9wN&u'\6SZ{[95}h&fq}zyx '[-Vy w4eiIcޱoZ8w*6fhxyuqvxe2_wo?(@ֲC_9wR dzdOrbԍB-YJ .MTZy4AԶL5Uv&i8μDS[PqJ|qxiiswsK>'%Fjֵθ珆o;aɵǖ]a-oO3`3ks7vf=YC,=YF38w&9imu^X*4ʲb>䵳CdpTΖm(E?Pzo[;tD]~0^,}{mZ.sg*oz@{ΩRSRA Q]NdmI swF]rnU"\np:we +a"֧EM7;*k LzS}K-:Oj6N˛ݛIGXNQI)ݴ {.N1F7\:Lɦg%uNrGR'WыwId w(jB´T]9Փ*/Φ4 +JŶ1葜΍Z,`&E*pӴ}Z JSƲ/[vwÅтw[LCnŬ œ91wPģo:u47 9pD3?kI3:~0#Ӫk H6݊3[7ɛk,ߐLOg35C& dŶ92}ב4[VV(߳\ + b'YHa`0 SVsimrahw6ӸZkI,Ϭy]6oUgu_m=ΐ4~eZ%z2c(&3oJ@{QsEʭ/+Z&AYXUh_Q;b|DAS Z4a9~`TK1۶Z˂e*Q*<1ajQQ/{إ&r@`3Z}K,l:u*㕚,`j88\껫w #X]Sc64aJyt -ؒS>ۺ9z<[~!L7? [\H9`r[Y[,*Y UJT'+dmw"rw; m]:/F5奌s0H፺V?WUU +kpa$*+*d$7J+W;FzPOD!.r@:yLN]5 f =erowz:&>WZAF~e6 ?·x~jphT3ĽxR\h\tK:? PfssssU6[_'J` h@tu~3P{*wP;h +&z2N 4H r.M)eZhpNd^ 5+ˣdSIwوy3q6 Qb!9T# *KgW|b P@n& +Ȯ"wF6ځ Ơ]]1oj ̨H0du`O+S\#Oltn:)qЭSJl +EJz@=7Z:TA=s)]P/K_=E[)f9M犕$t*갗ޢZ!b%#^#{S,9O{PӟVBzUۣX?~`P?am0y/:`fI1+bh#5`&@JaZW"ʮ$ZffG60p[tL3ǀ_l,JdpY',:A +u%T,nw3lv}s+-B*kCKB}?} +g,­z"3zoYȃ_I=7!w!IDG6#g?>(8bnY{ ~?r7H!j]stIlΫ9$G& +\A͹B]񽍹{ O3`ce~{{b'ϯXOx\_iLev[, ͱ֢'tP z½]Z%1 ozx*>n&i̇5ϊ9]ta"zvW?}>Y +7WGf<|lf(88i9=%”_+_i$~̳Lw!rlZ"Ŭ\~v~N#, k^,Y"oF3dm7业`7%O#i?.;d6.(~俕Pe{uMnyօOߧr`報N4W6ʇˤWZaACxJr{{~yG=/??:j6-ҷt|V1Ԋ.ci@۬:͙w'蕛zUu(AEpm~W +01HSP/K?83QO9gn3a|ط[eln.ª+[QUOsIrfT Bpd  4q@^auzu)* }Mb{}G<џ +rTLkVpO,q+LqaύD.nT ,mPG)_LDAVơDn L:9Mvs +N#WRlwRwfͨbB䬗pn\.6"^+,&YJ ;Qvb$N"WpނѮG7I [-y.ŋX Ex:0aV}aT fxc H5tF}:oVTV>M͵{Fx#Aj~v=VMEf:[Oajg3djf9uɘWmeϗl/_ 3hX/op/ԽruZ`X6:xywWJ2=K;tT]-iis +YB5E6NW}@n|t2UnNW;\uzNvl@,eC`шC< q4'̝x?qp*]o9V{o /E1m7/5sTk5 +WϗŐH܆33W=l>l zC1Nfz8d/o'KM OQt/vN#y/py59 -mT$Kz,RG0XH (Dȗ7X3jX:D!۵3W Gkm>Y#hIԣS㊷oI١žGqKh\9i~ҍSp7+L-׶ƫu"^p@:bYRWWv7+ya!rIӻǎ;ŗ>stream +,ft[/np2ׂ! 1*-l21`2\W@M*S .SLӳ̥+5>XZ#jjBu 7QeiY 1SP| _ᛔ,f!CkUueޤ?ܜa'?P]+=@0b+h`)@l M{D"ӯV& +鯡CθhuYȒ~۾ +bsaRbSƫC󮶴㭼%cd: <+LSPkB9PR0@`ْvLTӯ@n%Sjd.ŖXn'֪8m41Z- oQޝaZ(xu7>qr/x1ҳ9@g%@/ +Az%{I!P'"7(k b(z`LR/7 WaWM*P_,󠾤/ mp[>/n3?%}2ֲPNEP"] hsGk`ilOo?s)rlLS4+ pSx)Ηg_ 4o1B͵{= TY0i}yJod_m q +?W fXEK@kCdMq\p$ H14FT7̺gAC'\lml iso3~b+wJ|JրAO@]5@gdΊNFʀ6!MЕGGƪ]l[jUDS;57?oM~n Eo焇9D}gKn(`i7N\vc#0>F@?z!D]ڜ>^aw~ MZ㭭̪]<<;:|>@>.?@4.[8 c몽vauYcMh5qx|Z!wbu+u] +߻e1jfm̎voy;e}VۚϕW#AI(|ePR}E~TC5Wbw%"5v_=]cԨg =Z>kD 2+uX޴bh4X~z-BƝ.QËt~XexY P =e:Gϓ d|_=,yi~G,^vҁoXX1VZ +Zev@q Obf?. +9W=2VXk0 +bm/COx+=^Ae8>4e7z2۟Be_:~@ӳf+L<-O_}E}sQ(xQ;H|hZ8tq6I>$ :dbwJ/EofJm^$x3 gv\T;8(e}V s|yOfW]M4ѧWP/׳CxC\y{u;6}$YcFgzjnk $~<@=Xq%N>y}zcd^N#؟9R +Hs[;,Oi+*[gȢHDUwO}_=!:Ά]. +MdKm 3mS7ﮑ<_#Ú?~B٦KŅiA}t,a/7X2 j. +y˰ -u {F&JMF+<*<Рb>]SO?N$ؙݻYeQ'ݜ:"Kz7::,o\FC{tk[Q"(sAń9aY^~N1nʪjU=8,깵g;i{OHiE%wq\q*;bS%l(k\NMmxb&Qs1\6F.+h7һE#7KUrKi6:.ҪJ5lq#[咆t>A5b=Q隔*Ma?^KTƐ +/o8xAY0Fی&%_ROsѓ3yGk݊͢7%`%,/ +?|(R++XΚWb7Ee  K `1cԹYژq%p%=˥LnMru;ϗP9-6דof.E*lLMtg4o(.U"O +Pr#SHmwX\^( k]0(PS q?HG 7B;1Y12$;x2b0YzǶڒU[ "M\"ƫxmfY3H°0UvTnǵԮg_!0(?$_x*WZ \u إTh>썘*1MlN#%h]RUN[V*Pp;ldG۵VxRܧ2GbQI!=óŇiEa$3Yd(ӢNGuΊ ϔPfɛ)'i_VÚLvbLA?B;^Zwx٩/s0]rVj7":`"j:'#lw]3'SV3 OP4;8T {%mj`⹸O 6[qK8bl:p2Aqhp +sɩ֓=lUAfpj7l=TmՋJZYs Q+&W!r&3:O;|\)d<v;Paz[6\,v o(x%r=OD\wW)vo@k .6Ԧy <}^Ϭ,MӪd%V^MLwAM XH|S]M4ֱ٢kh0tQ4_\݁zJA"` T a S!`," ԧa{9J{S2mQ_ 8b襟R[:[ٻSaHUxo7^y!\s:gi2uw Z3k +~Uz\ء[\X.)vZOuc>= YYb(EK/Q'Gj6iNVW ;_[|q:y&4֌x01Lq#4oKOq[Kily[?fyl*\A,18lv{*i4OC**'VQpb +DQJ?X\@@+w1seh{SDžFޤ@  {pzG5Kk[f[R>\y|{նQ (Ru z`@F +[BVgWl.ջ?Ԃ栏_o},*_ݔ=Da]#!TW,|Zܪ w!vDfOKg0KQiYDpfDtL7)Slݛ:Zohu(n&+v>-[d"&&YԻ|O=P<̐(72gfrp}/q1?G)9(럲'=ެjn|git,V$/--g%ڱ4M?4)::6&+z鍷?Dv=< +ǡpR^X(jPUGGȬ7ꗞ==j{x7off >5 0gCYcFF`Ռ`}zng~Vwj]'L͜bte::Vmo#0jצs,WxJ: 1!aH[m?ɹݒ{o$4eScw{&c4t(vK!m@s;6vӸ-PSo^ׯ*< >jó}ʸ=3|L3N|7FҨ꼽 e;$mڕ1-T7ʻ)\иu!9ґYJ}2VԦJl0 zLNSXI-6 +RΡF ځ|HbLYmyՕbg־EX:3ը#8:HR`|])LzjU]ռ\5`Fte<NC "-]Σ9췊$f;_i>ѶgZdH];x[JxkZ|g4Pt]9&z. ͥQ ].6z +5 *`'m.|1]qq$x}=MBq-fRt~)۱^L;+©=?ҾMʷRhAVʶ^Ocv(VO "qb:w} ʷ^Vk;׮w3+tϖC;ɩk9^n7vEectlCK3Z/6YtNkP6ȋbmVQ\(BNpNQ@;κY4m"{ Yp#ݠ 6,]G8y2/ :qTzR~h!]Y׮rŦQGuÉ\3o27~1=ghג:nZ'+hC˃6!#{X4ٛ>Cu9U؄\1ћ{oj佯I3µ *~wO%()Fp_n_ZhZ[όOhn6};`G.X-t'KF q񧚺)/BWYmˀ't(u4vhcS\:?@57U0^D1wQ >{mF6{jAj]=)Ri[ʸj[:t*:xr 0Gzr [ͤTHc{')\K_Xٛɻ3z`EzjƀJƎSC`(WTz(ﱰ#׮۩trޗ +"^gB옮)Zxp##<r;rt;*w޾/^w%La֎GFY똪rMWZi+UAʆ +W  Vf E:EI/x=p-ӏ$GnahD<K +0 +/ 7ŻJK 4hʻl\EciN<5t"DVJݣ n2R3C ^Q}We'[2t'8F,y,qNAs堲Iǭ')Td=&t6L@iK=a\f84LzHRTHϙD:NIM轥-0?:mX*76稍Mx-Q:ǘ51Up= *sq( C꾘{_Y61PGvO%e$kXLW,<9v_|?9A<4 &"N&ܘ!s3>iVd eK?)9/P+/fQqЕ+]Gإͭצ^C5|X=Vk PC>[n9gY yrd Y{SutGk өF(`5S@>+E@R +#}P,6xf}ؤZ09|0T`6 +o>7)q[N?񭞫Yseg}vj FlyT7m0HgE(vJh~=Dp'p(0"A  tVn`(^-%[9gPjtТʣ+nm˓QD?ۯ-7Px3_M/!]MV4\ h6 :ɼ{i&V^X%)k0IQq$R,@"λ=~2-uE!aOle5Pr19FC<"L9|^z)c9d)]@.g3@ (4~}[' b2R!@Y5PE1L%wR(9=z]֘1\(5:|J?S7ŧfP|t CyH7*5:uc@u]@#@ s@=20@Wt x^tX+/h)Vѯ;=2nvmzep A+)K»ϛ>+aM{t7o.uL26i-n3@zۄ2pLsrfl )"`z XQ:8T01/Τ?꜎>s|@n#ۙ &F[ۨ? IŷpY6 +ftA*\~p7}=1 +xdP|.'ZBH*-N#?O}䶥ox*jf!t" l? ;g <|D"m~=҈b7 #a#7|KI@Y?_Ӌ% +e? tg$2})q@@@o@~P3YE#DݧsKHst[|P8o7{?<{ݔD䞛}峦&M6ep"D;g3d> .T|O: 裕p7eJ=%sMh΅沅- y>s27l*V3M={}Q9j }P\u");oW6/$sFR;?}^nM7KN8sYq +NlC]*Tu纬yY,dS⏦PCX 6 (-p,WH5tpp'ߓ4Fe4GYxm>ҽ8!_q0ܱI6*u2.߼5&ަc /Sdtn06` яe5覊v VK!S7ܒ"gҖRe U }ݓTS'#R9Vg K^A8lTczӑo:0jynG\nڔ4Ϫ{EU!U4ydZդh b1b"bB0M}Ah A1㏠3h/;ffL}jhp ZF'w^dhH..FAd +Y1co2?LW왭Z`#馤s-;?+**E -^$E(gFEf].Ȼr}q8[bJ1K,|sg8%Z.6q%2kẫPWDþB,N6Zt%7 +TGwFc1eZgϝ0ˠ 3>l)lSuBd!TZ6mPs)Fcc0"(--1_q HޒxGR 5߲OIMlpC9xC1Ki +Jri08ouqSH՗IN|]?SXݩ2ziT=x^`u-5}CK23H:Gj&sw5jg˺E,ڋ{-DfSNE<$QyGG3mM逸ŷq2杏]rXKF9v;Z+UV;VQt͊sv݉tψU0.[&^@S+AػS ϳ)O\!ѤG(f7G$niKbWڇGꗀt>TԑZzMCE Gom=/xv$O'mpVlb .ƅ|6/-_ _+h\}&1\i#D@#,p1c2^ז~!?nLiJbpwesuu ZCE^stՌ('K*X3_ nnƹzDNy&;^6T%hK94${y{dEgeI9i +)`aes'4٭̜?·&m:/$CKE2^A]]|@qՃcC ύeuA7I2fAH9 +*⭃V A/LRn@$|tXT^2p +'q.v:nዱ1BFG(̒(̡8aYx\A!1f&Kӵ1S03*}k]uAa#R 6ip)X]c~gV%M.\c$ ]UR8i8YI#+9Rf) +\Xm0{kOd)Gy`1ɥz `}*} h̥;7S Z~)̖^qmTxZ5jB!sGi ?ptde=c/oO)7,E7VM?f)WC5L^KL`=j.0CVK۽܌ŕ`KbEW/J$nvf +{'@P w +3S"<^Qm"A_753JE[S"h+ߔs>}#Wn`aN(w Ys:q" &yI&Oĵ2Ϸ)FK@rG@' W 4"6Aƀ'ȹg ~g.1ɐp,3?^Q94s-f|m_Q|ryb-SjJU:NU6 ˯@#U44=i@w@5NqkY yșr,?"x.l6=;εN7]afJeF}?:.}Iv@Wa0 fy*8ue&4n1`fR=j%F͂C[nߩXEa]n)T㴋*6Z)VV^i) .W$-ǵ p\RlU k]{r"~ H2|ioM[ӿ=EZ[P+ ݀U~MސH.L1g@åֻ 4|5x4jT$FkUn߳!+v^>nozGmIJ] %KH4NE@v*6i=|HtsH| +FǛL=ߏb7e.m"ֿJc`0e@^1&: ͻwݘ&6Eg$@@HuPjZ}n ^/[^ldO˜QO7o~-6W,[<ðhG/x>:zsf?4ѹsI㭫eYQDṶ3- bqTȏAYQV;շw\rDN su͐ 9MzPa2աpW@SZz#mAʹ>N\vVEPwOwu)̇ybé5&xaՃzLEL;FXب vHV:֭!w--4so +n/- &.GPCޮ9Ր`& =B4 +_|kF_կ6.2Ԟc!S֡p-( +ۈ~={Z}F}dEmuhԦѸ,dza_]K?>_IoFe9:jUix)6 +]Iӝ9SM/5WYaLi<\jmK5Yug:Z_*%9z) x^sj)DMP4??zg̍Ⴓ˖W9}>lnL8p?tenUS6'#1$pJ_CΏ8㽯E)yţ]XŹ.VW&'JcNj+ +YuY]).[\z̢Y2eS_Ct%?kS%MSL3&tI~&w3YYuT GGjmhdPkGC벼w@k%^Vۏ h⥷nPaAWePT5<]rW}QCkmjO͆ɓdSn$mCDLs +k.66j3 &[.!fz&2G%6u/ݸ54Т'pM-ܲ\;U5Am >SR48R>qF4|?\Ǫ)gk7Wxc#P)`(T.ϸ&szE8_9KM>>ng:qqK(`hA[+=NR%GEgU>3 + #M@ +nO':繃?yf<424W) MzXE?_f sanOrah}#ihsas|c|R9յFfh eA=QnSaISgqB Yfbw _ɶ^W$,Mn'\xe F^Uv0^ʧCV_*erVa( 'h}Ze-#P>gHQ)`XYYr5 sLn# >/mV-a,Lc42|L씣3030k+ :|Y Ig1e[rm|={.4VHl',r5 ++B7-Bn8$w`G>\ʪDXݨA~4[w7!"ESVK1x=UU#QsF(bYb!cgw?V[aJ:/|y_m?݅976 +Fn ӪPK[{[??.dj2VxbPqIDS$dL*ם)T?08a/Ks?weQ6X@Tu@pK Q d@R,~і FĘnzH-UoaQ|.A#:U+$>f,ҹ`d$" WO2Xbgr~,f,! s Hc l, w HLYd+@q!dۏlX9d`Lg>ftד.ͽ$"Wm6ɂ_Sq0 ě>@A{ݎ%(vb'LB{^( +Kɱ4™n, ?~|(& / ݭʙsw|\]k OC#>ҍf"1PАrdj#6u N!SBk#~-4 JzpT|1M3Y>呵pTO*`/fUHah6SMEO]ـ/O䪀G@G1`&08RfcyY! +`Ht0r4MDmzOw o^Ucgtk %n^Mx̺5TqQA, ڀh09LSt[̎.Kug,C YIfAԈV +C5v?$Dd{swCN@m~[5`|Ms`h/O6?FMK 'o`x8|8InW΀"%X]-F踭n~ξӼZhJb 8HK25_LuRWRҷ_2f/mm@Hg ߾!8B>!=ā9V,: +H MR|(Yʊ]1W2zu+I9[o|_ k#6n >N3 +@_f-c=9 JH$!c?M-nCcQF`4~MMme~nInI$'mf5L(n j|WOK(^%}k'O,a\J84_B7nsUx۾kXtX|b@z Az-AzAzb7MɎk[ȌK7k\*VtXo;pj٪E.SYjeJo<*fafO׹4L4GA7h`tnlԠCoD}xsD4+aL](s:rJYߖY=&/ƙKw'Y+NNßj0 jckF-~Pa@/3diUQ$14ivn+%l,Y/b&mVxϩ`x\'#tt.r᭶9u, {YCsuQK,?O+ہmZQk9j<' N/$& #e74j'}tɰ ׮f/k'_E湅ָنޯ[⓯9dW6_nrW慈O=t)]bNz+Pmy<7v%oO9jt5{ОE}`˺r8jRVgګpHmkW@˳erk5y;?si*ge5HhhnUg{'2y1e̫[6*twˠ]ď:*Բ{ImZYoޠx]=Ie7kcC#aՁkd:uxTVYw5kXU2w9/sjeZIyf6*8Ů]{Y NY[qq;,@G17l<8׋/?K~n݇X"HhZM(/ AՏ~tG6 A-v󩨃d?oB10o)Ĉab\=0T%Ϊ[|6qw$ N[UnyR%jjlhv|8|3ϔ ^nUow4K7 [8ܺqu.lSYN=b9VѼzBF' kPF{s!sU&t:}CNoʣ,)@˦GZ83}AF?Ӊ} +/V|t\.֠x'eaB'3f͊f=D[x3^JF,aCcPz4`(Ǣ6䚑[? 5[,wwynx}-[ZL[qlms\ st]|yg+WzuOz@<5߲n+rS,A\(| ߱'q9{ ͱ\ɊS"n}1ӣtFlkPqgvb6\XIR}$ݥztUwʥz~E}M6[Q.̮"qrɥl$(ro'T1/]F!Tr&9z3y쟆5*9E˜gb`5սOVʰоMWrA=(Me7i+q(?\ԽnMq81,aZr}~uy nYknA,~6j,*BaPSv`6<&eֆb.sN,UDO6qJSMV4}(&_ hhKP=\DV1*bSw_dm{˜wdU`y1\Coiu(G>-k2,kܡ1NUnTJFϕ.As+/H_{lM+.(Oll=\ȡ¨\MsE5i1O t &\ur{XuP4gg/OEVϟ|ڽsDqN#E".&녭 agUp9#jRg.&;guvO[moz2'Y*^6lv*>y $F|X:s{8Rfɢh,3^ +I1iN+R53%js)iL[lF}+н]kSvBeJ +a3nxpj|c۴`[M-;fPW&_:V,/n Tҕ-ayFE  '7+̴u }zSun&]r~|2{T"ST?W/Z*v2Z .ctrψċ!EiT+&q$t[f,y8v4͔% |u08>L^y&E2 FNs C"*m|5p lgqS4"k{Tlobp4eX۬Ѐ:-O#* I~Lav#ن:LűOrVESլ$WjӏxO9̞1Ok1Rӭ;Uw )^] -U-Uuއ]Y*zLŋrMfzUc9>Lt6k +OjR?)r@>}L)"Ps=K\,ʧNcJ,${Jw' ׷iVT[B=M(X9ALׄHj>1|4NwqO:' +r>1x9^rMBwG2*?wʀ::u]Wiz(ci\byÀnfX6[~e]^+ɀv +ݕ^m&*QpM+Уk!|uj@YDlCZb +!2pf֚`*`fosgoٿb?-`6 QJeSLY[0^],R-ca"Iz.Ԃ*+ҿ䊖r| Ʉ6&`W`DLer, p*BdǺ-F?[M@صXdüz,6~Zlɉ?t} 79Y)BA! ЕTp#?_y9d6$8›NSr*ls iu>zg@L +O#q9Ӏ8UF汴/Zk$M# _й,>.6ܝ$)gznӄovG1P (8+F+dj=_'HLLaDܴd2lE܉ O_U>eORn3~mҵ+H H&H7T{1Fxxg[捨kH'erGbK91FνZ{gQkqV7?0鏭MVPE?YsWxNpQqZԖ¶Ew5GY3bNi3G^tn#<ڙx-w2~yz3fz +u3:ϵ*z/eܩ}60Չ:otI̫Ifo@r'% 7ϋ#e'CLfVu qg]HO;Z'6_AfTnOC{ Saji_9Ίln w x"~u{oGV C{yA:kwEhs[ks\PuhYj__AqZ<C>n]N^Jۆ?+pZ͚ۢ&'/Iğ]>bTsR{u냪*^#—~هVծf9NY> +HJvϝ7ȗjx-}-@`?Uޕhޕs+._q-`# +ř Tu*-۷2YKB3:6XaYT6?uW E1C㽘vN<)MB%#ZN3UwcXEw; TNۯp𙝬x 98>gw2s6ЯC"geL-&th+3z5t>uoYލt`Gj4{z*zVZ#vBV1Uz3& Q[F6f6IsBʈ1/?k4zجt5_o 3R~4Zj=7T ]e*(*y]u.Ց0,+ǛYt:)H= Npe_RLOʍ'3íߊQ7Q|2ocMfٗL77MC +6>HGNZAajh;>s^dip4s rYǟ o B@=s(űhB^gw烾K_y"xgkhk̹ FWOJQ |_.r,NjZTJ;B)z5s۷Z4AʤUu]d0Rbdk+\*3ݳ˖5N+V.:hu& 魜ZރF})} tȫer:-rC&vM="EûEvi`_PϛV;}:ʔ[|7XnQ1eDpǣcþ~ۆSGH}!4ȵY2@>@s*͗-~Iha j%4g@yZzh=-g/Zž҇im W2{ #_v"a33 UF@(tnRqr5Wk 0( GqcYjp;mQ7fQ;/U]PJ$wPoHG=)܈B9a;hy@av k&8N0by)#L>I ` {7:֤?xl`mJ1~Kf\m<2Hbz^8~C(Hy4pzųI9vq`!G8bX ܤpnO70M"~8.eGK,{Kzjߚ=J45Ta} <'hٻatA|u?xG&8<^N@<\&wc9K, +=~Dc}?\g)4hC2{w!/cGdkWn, C{oKygދFr0qcw0>g ozze]PˈĪYͱ4P"(R: 2+BRKH ( < `.ܻ9@[pag˖+xo4r& ۫:fծ&;տ[$=4m5 !%[׿1s +P+֗'oE@fhh9x4u@ R,{pԃ>}#PLG==.jJ;q\_#{Pij:-OeE%P9Dt`c*J&^q, ` Zpc5)^)U!O\"lVM6[ 4vON% _7!If} 8ѿrDGUiv{XvW~vΊ틀t{bI̷lIEs8_0toe%% 8N }2_n|ݶ;逿K߳ QIwKy:(ic wPlBWCV$Յ| 7#A3wG __]&a^VFS (->z@ă!s w&+Z}d_; mO| NI +[Cb5+i/M2:C훠$$ȒDۚ_5t2t94 T[Z:ލKz ^w|Tg&Jeu孴`P c~{{Zݙtտfyt6H? +î:~m1d?v$rsu^礛 ǝk|gt٥Sy%fPMz+ψAz}#Ŭî0J+s~>4 =7Ń˃+zSf[ɭX՚0T5QC/Av&-ߨ}Ӱ搣~eo$m[\'i4U*` (ȽY3{AzTmC6k[uM`mɕokijok3ikyTgTEx,Tֵ[Gk@*Qݭ$GCO, O u:O׺*E_^M@ڽJ&Mc>QseOԲ}sfr ~ԾFnKz)*vK{LxgQW᳛\D8m0β^JX7+p"UDJK7$o#T)vmZ3.,,E;(@B+I7htX]"]32Iw2suLXmeCKcYsouמgѽi}g$G= 9=󕣒u/F60#An> [K +Lo2r{__OhBkK NPc~V|qhqo'R"V]tr;<:"h SH s[YnH-jHF'5e1Wņ1kmfx}XNk}{'T,0AIq?ȿH(w'9#'[,oкL}6ֱg߲hhl~vtxtiBGng2w4Ы=KiAݷJz7 ׬y _rRU/}sLwBoRcn]ۉq]bdc2_*:C_xirMN恹SYJH +uHǔXiP4Sqsmr +\H]rt<BAd<XeB&:/&\>9Ⱥ^SjqlBsK?pUڬvm#Jzd~*8ܽuf ,%Ƈ=œ`qKiHϳUeԯ⊾h+F?8u/*գN%Je˽D˨?sASLz~j'>^!ۣ&f1;0ϐ B י'# i9CA4F:otrtTo"3eJL-@̧/@ǵ[R5L|}U.RZ*zzkSѣP;1pBo1Ɯ-sO.t4yi唢0NPFUȷQ{'-F3 +'DJ-Gwp\,#g?qֶL6#jGVfM (i߲'dEq9gNqB_{'yAg9 +2ub:<J9|q8X8,cmxtp+ی;v!MShzmP.&^|RSa,DRe -܅aFa}Hw)41C&Dn]!8m?ckMxb>Z1:?Xg|*b2-T]M fC],!5b, @rlR(rkW^V5fx 6 ~3>Cq<4:8iy"o_kNAj=*Wɜgmjkjg/fz^zs`z3L=Wte]ل '"ΕnX pcI8⭼Fk{kfi^'h +зP8VSXɃb!c`CkKt6FuUߩ\`d^OjpIKQMpBo1W;9M'|+bH}ƥp`y%~^)` ltx"4X.ez7p rx~MPn/6[C$QZu9s?QB&O:QeK;Q-#[6y-:+ؗqx5TD&+81Qy@z5- YW#@0`ϕz9uRWF#f5BO:fO,+KTxlT?qI$̙7@x; '#Ӏԏi@V%'|MִY,# =e!2)FTW@2-Y{u bora>}y!q]_,$n`5̊ oC +PKq&VmFCJ7&9F- `]Zokq/oz!|4"&'ϱ6MkN/spoM>xv|!}?j+j3_F'eT>s-llgyg8`LgHLM Eu)5OZ ԵFNυb7Cκ$; mD m*ޢ>pqC[9թ9 K"Fg_j &F u_qK,O흿yݪ [ynr{ ;}fVmw5hͳU*U+^u ÓޕO׃Dgz&-1:{͖[{4GX?Vg8Ug0O?t;U|OwM)s]}oK +e׺ --@5J~^ͷ'װ}9MH tp>۔Zٷ^W%Q/O'*[:מ c?~hM]I+'%U7pJ#+q&m=xм2w~sqv$ ~<9[EY%4dϷ{D)ӑCtt+ߎЯ2n5A9峁ZWY>֜w:u9uS c6햣ul6n^etܞYU.,:1?tń3X;gFtQR^3׶@|I-2+-ͧZϭlUBrEE%EQDzFLV\wLeEXSou:fP~;x:1*;->J_iS,AJ[3/  %8n4Qv=k@e!x΍xnp~Fr 2cfCA9*׳mʯrϥNJ@#ㆺ=Һ N)Z/>pІy< 4g< 4 1 u1ȓjތYk0 5tuu.?2e}@zT4秦,J5܄ +f2#()h-ͻ#~.H^jEvJ9y93T8kլk8{;!L TLib\OGu:l:`;vVzhfTû%$IɠJnd" 6qzWW,g;o8.={;j]uLD F(1ΦH{0fTJIQ|HH`X wk"?%y@ܓ,W*vbp]B\N|q"dawV.!׌YoS?f|]$ӣDs4 gjNjT.fѬ4z(TM܄SH_1楟M1.7#'.0\x%t,+Ow\yP~"t䮙G]mz)RvM +'KMf )F*&3Ǐybz*ǮH\ab~E-]u4ؗsTezG(YeWh~4%Thndz(y85Mc#p }*2ڂkFi p7-~Zx +-j6Ev/ +<F(Ac(vc [ʄwfA35,wf%04^}sVq#OU1\ SgMC3!nXd|%&~ṶzX +]U`'a?@(߀ ;/  \5ZvsU:WwKx56CLp㞜sWM*3h:q\RUpH.Uؘ]ܚdֱ[U]EHT=$)(^KP'P␃XfʓIP +ůi(>UڽAnXkmQ,JBE(yʹv22E1GSBC W&u 4D ߫'(q Cō yx­ *g1UPPkUD)_mVBwwT|y'Mͫk2鰋y:g54@{.n/JIJ +pdg @BIw?dOH +JLf1{@H(yPr+cթ#i2o>bK9;[~3 8ƒǯ}2¡d*F,MpeJRtP6SP:Z.}4iȍR^NR*}%Z8kJ z!;h|_t|p%/?%_rwk_ Ň3 +f$:)'@guc*I(mm(MOwPCS_P %0M-Ep>GJ /d ?#1oo 0%S盯 vϡ𹇲e e[9( l*y2Q2kj0 jkwRH>by 5 ؛*>xׇ9|4\_ɫ\Yr|k& +2qAsfJzW F3 9(fĎjt~ypg$ rEcoR*Vzw__ۯiu^h ft@nPaeh.OTHB(ohe(?.*咏/e|( n~_wXw?/ݶ͞Ds*1T.PP֠\T_|?D$GzjP1r&fS/y7Ooxѯזnߩ;,}}o裻 bPuh6j\K+>u*|U*s2|j **+kygy?ƵǡUöC/]ong}|MmJ[i:G;}Zp5r:|#tջT{C&NPmp|?lhY>xk j{H/H]k`zAYB;΁NVg{uP[}? x3^+4s3k3cZb-::b ?yiNl)i+gr5Cu#D0z:LZ \=r"O5 8v̊zŽ%o%sxqS $":=*) O4$ [Cۉ]R}*#ٮifb,k p:8Xx#gJ [X!k`F~k{n"|EmZ+EmZ~{j>mfӖW(5D'߯W2xjC=ѠX7fi_M16$Lԇ5qwrV륭G VT:n2#qrUéM9,ae`:(w]Ag$Kܘc?7g7!NHXvL )\>ңmc +첾dMo]Rt^:ՇցFs(?yF%g1L\O~T¼Y +'w0x#)׿]nĚ)tGi}mOL}Dt2[gbyS):TܷrI~Gm7V~=VCs5q ܤ223eti8;fcuf)5CZdh3ZlVɽT]ĕa% +ܺc>Ђlԕ:}KsNƵzӎo#gc0cꯀ3\l2QP/S]uCf aV/{3'|S1x\^#SW^z/@:Wzy';zTvu-l"=')p%Z829BpRbp )1?QCQO(zLk܉f5ZVcPƖ&皳?nJ5^ѣkk$m\"&FZJ]|=]w|.F.|51j|_Qkx Oz@NִV,2դ$3w%TSBM=+fet'A? g }soL0^˾3j_~o Y?#./DcF?\_pc_~l6?RAUf0sv đN fۢ cbN$J#L7TA/we4~Y"7+9GFS`e'U|udSȓGʓW!z]Y:IAO]žx/Fo7ack+$"nQì<ۦ+>IL SbaUdۧY+[+AX#Z9'ܾAuK2DE kB̈́Rx/SR/Ι8 s>K_g}{;Z;t?!sP;YYxh% V.a mT;ӏ'یaV=(Gz[yI fOT+?S8KPHeO`1~̑S2'0AlߘS?Al}'RÅcF?\_ F}/ + + (,{HX:fξS3.o +@`+WCnFˢtHvDcIP^g@H汧D?G\AW߬|Yaa&柲nLieI赻CE`cADӒ,U)ّC8,C(6&G|$3n$sL3V*/Qi.(N5' ˀP +Y$F=Fߟ%QYABs *"a /]>0ɦ9WCϰQYiޭZ+gj'< +2S i𔒻c.h}]:b7_恟9%:iT +YH]NEJE< +rĥèʳ qQ-Or1X0]#'DM8@ ԺX={[BRUP6ыI)1) %̡MEfHX05 a" a* W +]5 ;BN<21iS2kWlanu{u7@"]ػ5Mh_>>P^6K4K9 cX/"Fn J#NN}/RlOJX'p426* ΊOW$Gp|猝)Ѹi){qs먐֡ +\gwBZCN.38Bl?fcz84ӈ_/?)Җh\Qk%( KJPyX)]Q08޾1v9Su}'v?fw7P$}/H!4aW/Eݹ*cTkX8s#Mp>Pސ&ʥev1W%w^|5-nT+u|d$Z B墨Xj< =6VԽYX:dn92š#r"j+jlOLowJZ!qN8i;CwQ4[?8Z)&YoTm!F}Dey`N$֑m gD֍n:]eCwJZ{dce/%irOт 3y2@dpK<^Ķ, )yt>+YNZ.آ+ Po˜Kx?gh' c ^nR=`™'rDoV|B$>Ǝ;N8| ~+`]-پx=:^w3Ďn–5mq;] +O!߬ו!`&t咨9}muGw˧]g^ǰjs7-?Oi.,f;; c90`\J %W$,6NX[z`0E2@ĹCThXx~k~3Dvi`3="fW ~nvKU{sL%{2Rl'y)qveզEB&IS}ڰH^k4a0ti{}Ҵ&B8,~֮ y2УN:&kN!bIihuO {v1x/Ѥ9Q 3tm:DW85-r!> V˟(e{Mo3qԈK3U;}F8~E$ݡl0W8/$N%$NQ<9p5^">+_@ +nOS9ys]v^)c2QKVI1Nr2j5g `~V{wF`2wx:pnZ~Rf|v9.>Qbȫio^іآlfSItr7Qvg]&$X~jwc}|{0fT[_{=y(˦!eCO8ǔ<[aQmTÅRJ. +cI%b= yoi5rD>S&v +厫KAjg7ϛ"9tjƖ)㻃9cXQY(t'3ﲣFPr+z8Po钱y{d.oa<O:ZX?zN^F1J̯Jo]BdGCNW|@c'v;`u/'sbݎN9q@6}>d};j#tU"o, JhnRs +u +u+bss|^!8wT=ƣy!e;+۷mʴmeL7YƸH!,E`%'^LjlV+BƷ$沅k霻;Aލ^n6ͪ*%t60Xosx]TJRաDmVC8Fॵn?)mIXLb:[:@%uKcpOO``ld;d߈ 2(Pu0sՌ/붓Z'5¢yEsDC%1,zt~X0+An +63[VO %F˛[zoksF}ZBbnr"&률b8KQQHbȿ ˠ L/ + LP?[7o; ?[W?,$TX/h,,=:xNR.n5bR']cPݭar{xt=1IPr+&bj%˒, 'Nk䟘I(?%=*M=^ovPNQ<~"z fv\U;bEWt= 跀˟Ph?Ee>U\%Pd~kI66;i@I߇^鹘fSA*JZm?ϻhc(ź Fɑu9,tXl/ x|FÒZDk4ߵ?33JJ9K\Q+^=r4wh$ +LsflL7spHB3~m#~s>[Bp\BtonfL#_r½"pIm>E[~8/V<_^()UPbD-aa~+='Vsq s0Ⱥ(|hv}WVߚ񺂞@iQAʲΤ3~/1(_?+ttr"k3n/<9``izc򝃫mX,;a诞:.'F C0a=U_|7\$@,f.7cgT+2"CqFw1(AAkWyc$,bϵq K'&+z;EiC{ݾ2f 5OzjBjg1P \ͬʃQ4R89!f[~ƆXNz{sxq~w:ga Ϟw+Gp7tlt*1c䌬^t>~x$,l/̮1q4 0N}5[fav(kzJfz.ῲZ%^B&{u[ 3_@ˬ;p3`{Y5ղ0XbK JkmEp0ßU%(55N!DXK8 &ÏKX=O:ba<'n<ݝ,1[730jIhuyDGXH,%ӞTD_=3I"cFһa{q l,gy94+[gv^Fe2x ur:ߵ֌md؞NQqĶf1 }jX}XQPhN*_7rV.8Z>;˕= S^O֟j_$1Z- ]W{ū#{e, :gOyxW 5|cFJfjy<8|wjQ+ `Yo+C H 1#67liڍ̵ۃ(⷗\H‚NwXwV>g 250s + 0;Ԗ9}؇٘3#P YmkٔD6|4+;Xq L;ܢ}ڰρ;{4Nt_aVf֑z9=Nwx7'{0=pצk࣍=scvŕ=Ak\/m\F&8uM`!C90Rx˯i'ؗgK$Ehףbf2hԨܥV%ksX E [R .k*+.lo.t>KFΛ>80jzCT(Y-_cϲ=o_/ZmDф3 78ߘHfsО7F[:W,qk=/9AQ$EoSa;Z~x Oۇ+OPkNq-Jl`[O {s)\2.[CwW2C8c\!uy.JYQ]tRB՚{=+-u I܎kcsQA<rCtOdJOCn-{tZۤ6?!v}pMYE2f!YTWj{5cp@ٖofۙ\orF>lrVb6^Mqm擒Voiq05{Q5d-Q~+n$<~6tzE_X~yU1ulZ;½W@vD-=Yǹ6V(y縫a] o|+i+st7^V}oĉ]+ ̒E"V".E\흲p/;c<@뮟F${e6J~pW뀐r5ݾ~vX ra=ܰ[aU|+=]߁S1+6ݸv-.LKs1Ǐ8"h3VFӎ& 1/}KE=e +aqr" ?,zX7,şHXo-g0j6.VhMͅC +Gc="F!f\_sQBºCyiy_ qT [JJ?,*xXj|.lXw;7Lng(TLW׮gK͋IiDE`!|]H|s( K+ 5&ɍ*\ ?mY344شc{W]M2ⴺc ynpqdV/zJXd[X^rq+~uT=۶yzlNݕv^O[ Em6DSd1D$.J%rR6l}Y kR^x/oPBҙlM mzVqi6Q߃чf]N;iKPg Y/k C:!X9E%}yPigLƼQvh-2ٹ<vzeĵ:3HZ`ӥWn:iY^DZSw&sӑ+V{N/HY}%$.-&^{\Bn:xY1 ^?.?m繵-+x/€J61! +HD {޻Z}=<Ь5^&T՜EG itM^L# +,x4x +$ۅ=:<|c5v.F(9\VO6.}N:˝Zغ%>vwE{{eˮ˸s|_ȯE) + [:g4}m$>ӡɆKGbޞ^\*zJ71k<2H&.+ͤTW4MzOSLU}U=[ g;ZzksM> ,d89Jlt659{wGĊ~h]|!?`~݀m(y({mj%l~<Ϙ4C:Kc^ό* +Mz nYݙAƀZ=@/61# 5'ҟo@2%h-FLq"unpWg+ڰh(tӭjilϚfwn| |glyYdxP*˿t0Eݸ}zΊJKA4ܹƦ;W6IKay#+M3L2I#>SOO fPذմO\?HRBǩ ީzYuͶcQSZJ|2IX#s>p:swc"L_P|6/lCnEwiNI?~]6O[j\x&:/SvDY 8:ÔfӜdJ]wGkh5W?7@HnIϋAGBO_w5 "t#>"3.?fpc̓K + &z&Vvfo븸Rto|r|2Za[Qw3wӓfKH'IhN$dŸe2g.=ܶbK*OE +_f38uW=$Y(^dcƔ\Ba9 .reEqXwbMRsuݡϫ-UzFS5Ze|q-* =/䯶@X.6$zmWTi1"վQ>Ts'`Ů{DuvYl+z~g/dՀHq\*΃102``½#ȗ3hr'$NEY%VzyB\QϤٺ}<^Yels9A݅>q 2Xi;_;Wa^dX%ͶsUYG0fy:}c[)ּ@GBfZ VæJk7Y xb#ȼ+#ԬSOq;餝n {"EE>OAt=ij)v[o494KW2,Eq~ +/dZp!yzn㷂JrƩQ=1ܺ +7qZoT奝.ö/ ncOWVl݊uJ^Bghcgͼ(\+94rh[ǽ]iC=f2͌`Y 4v]B_߆Qkk~CR~I.[ch̼Ցփ BDv~[hO:Om}͙4?Q_Y7}__b)肋e< I@h4dds a +(A0j*_<$@VAETJX:A *l"WbD{xl] 'X|0= D +s +{0 @j( 5T̻s+*QO4ZV!8O3nQ{M֎%)JDosf6Vkξ/sE4 Hswf<8,I(Auv-q"E#6JeaA6A[=48T$x01 uwodj1;4~TI.r{;h;ƫeދd*M"hpry8gpodnh.z\M.tm_ݰ\8/DKկ&JlP`Zڌ0/u0U0 g2 + \^sl bWOfW{&{8p7.Ԟ9;0H(JlaY=@7{@?Cqd= aҁp4ZX teяۣ:?K- 7ld/b7c[XWQp?SArg\F>ظ ,CoFrn]V\̩3_B~Q@,J'g'lx̳Uʔ U +xHU#la2Գ:?5o4\^Ҭ)y톝s%'_"VZGpLp]@͍9*:Ηj0 VWjGeT+hT?^*Tri :1o?|/; MLԳTRi7MRkRQZ'Ļ7V=r];CsثPePHЉ+;Z&xPBS?GTRU1J)ͼ~FNgL~LQU:QX9@-oBj=3Zoo.<9 J\%_a4KσdHqYRֲ|ڽBn3k-*1.TMȷїSs+I ECШ.BtDヘ!x^?;EdZY29͹u =vx¹<$8Kfgׇzd:OQ)?4|@ȿ]5(H?~e>VGs-ηŰj7h^ aIz@I JtG,s::X؏k +)z{-u Ox,%xQ̬27M7? mm s1\^PGcBdas8b숕3+Hڵ7{k׺O$Cs%^>a+J'iJA}ԉQwyF=62 M8DV;fk]^T9PB |B|(y&B#p);oD]n2oW>E=>G@Gt#׀_B؆ZxŝKGa3ѥ궉VB\%vp .wJ9A1GeI*#HYqszo۸’÷d}Yݩ`+[orB @/ }yx:ꖾ*~e\VÞ1 [<%g[*F[3uqSbBvٙ.,0,X@(=?VugqwAÉ{5|ψ<|-oaPwYop63z UحhhqZ e֟Z$f;jƣSTE6.w\\p3E4N+coy#[`}i:׷JRt֍->ZSN%jJwXmw{[s/`J=:ۢJT3ao.weem-;͋KS~6  +Ť0AEZj0b? 'oN@vGd8 +v:J!wúUNpDpˑ&ƕź[*#S0aMl_ ɇTs ̌ J|>3UfajqYB6$˶TWIarP$as%W]wawّ%:߲Ґuub*\m򌽥t|V?PsUaU9u;si8ZJikB%Ҵ~h3>:Lh%ۛtƕUvϧiis'nkoݻ͜ qj-yR[\^U;8f'=2,/rn P㢏:K[s{>gU| 5 4i_ߌSgU.Bzi=FTP\ͧBf~ z33V(/U& ]a+Q+W0i^DZ}])BF\9܊h}@Dړ幞NoXވ;Z#f['/dcuC;㊝`W+ɡIdo%Ʃc 3^yx!j;9u+`.Xb\cv+`4> Nh} ]z[Ovx 6-z{q.W :34y//[smPjܗfAi˪i4P.+* pL3V$4}E@+=\$Y+02 0;RCva`li=j,, =u/Ѥq _74_EnёE?E5?yQKi>nj(aEgS‡T .Ҁ8#0F< D1{mnC; *;63 +ad1!R&znIo[ +-E~|#LB8yBVTVgW o|P<[S +LLUZ5uG6v-3o`2{L;W6'Hf5Q>I9@2 ڵ ] 9o=Ř9a(+ +=Ӌmg~;雭Įn$Z)c.ޏ<1 ~N$O4! }Sӝ: . &0D1k𘉧*&vX<.'fW VIjRgV`7A$]> ++ +бOȣsz¾ʀ B"^ jDLS]DCeQ:xrj)&[ . ̕$Ԭ>#W@aPW$?"a?~ N8V{T͑ȫ`p9uߙ?;[sK^pQ80:ث}!6]$^AAS ·1cZ*~]ɓXnJdWJU@ #.&_&Ϸjk;5C4' {(Ciz^nh-ZUG􅨽%(|?E['U +q OއAec'a@Z4yXʂU],GMN+fo^S1&g%tT4VSð עJ? xqxE/m'g +.{sMcɜA$X(HԠ_;VD(6\d<}k#OM8aOLZ+[o2i5˼|t:Zx Ǚj#_ȯW>=! h;2Ͻ൭L}-;&Tn)ݯ>ISlK|4ʑ~.܃ݐ٬-<ۂ䛯5 \ѷC {/˩w)H{WKVGG(_/UiUݽ YwtwV[3-y[R*ޒò.uKAاR]!uhe@T +6#W/tƩ[uHֆW|[L&ڰ-oFߔ +[H҅-9~-R$[!Kܗ '`456} a-k1r>,x>/}nD?(~!lin-%՚/ա6WzVYb)3Yv-go2 w㩴 +H^wyZp [~-O=mBxt?GT-Y/V %/_/RlanjM*U4IkNᛒ@ 5S~t ^,k ՛(h8m;4yvezE@|6dγ\qT[+|2"gBF*/N}iK wH\!/3Ea+w|`W"Â)ϟz9NYW}?4'?&)v`k7*~o:c!XӧdrL[a + P.늁z+mmԨTM׭wϛ^'<6Հm;1Fg0/&YVO=[өJbs+ranD7:Nӡ4VXxqFv^{5 +ѿr/8^1J]ztE|w[o7i2ޜyҜlehfUpz*3qԙoHk2 cSFڀVF q[/ 즢]&;tuIn3NLӣhCZ|f<-ӭV7Fb_I+Z㔲FUn@32Pthv6RX1itjY@@(0ׇ 2m[k *?ERkZm ~5B/$fh+3xe?QРIhjMܟPztwr`Y&#r\1W prw(<&D=,`-V`XFBnD +S᷏{*uvh+G $>W?@" |Rhvj@e;LF{z5VX}G99U8~dL[&t^F4|dt2qݏ+.u٩_AQvPp{P% j+137#5¨gm\ +n9OJvEX\a{萩8x?վ?٪~,m.2, \3z8jOT`j"v T%Ic o+[XK~Аg{^BozMݳo\ȫ,=lMY\).Ɇ>@r8[&k-%{P {{ m |6(1w:=\'sVlߦ*T)UV'm/ѣV3껙M2_B~P]݆i>PX&yʡ|wg8ԸTٮ_g%[ohg_bTY+1Qrj5)$ ;3^4)xAvt1yg\Fpٶxj)YnV?*ĴW ϶8\F9UNս}nK|Y0)o``ùl*Al'tՙ~!NM@f|='QΎ/KizlwԽ,VI!'OLFfZo),QϾ(`aDLJ* qp E]GRyhowu;V|ޖJ-*Ա/W?3@Ɏ_+yv4@K"'Tap@fL]>ɤ^ <Ӝw wײY@bq_|] +e@OFj2RoEG- C=DeL*wjױlfŐk?ƣXw|u{7nGrE^/d׿"Nπ[ . +h0i`TQg9Xg{[0:*ΧvvYglc4uc{lg7Պa8/:R{m ࢑d}*謱pWtՙTz-d$q8 +qFw/>~^}瓫\1a0yla Y6^W_z{ͦ%,-kFǀ5-4vHR:2fttӛ^ٍ~<7XKbLi7b\[wmk캞KN/.;6^JbךSxS|0b 5X;T^n)x=ژ\+{vT#w*)eĮ(hwx>I:mu|zBe ɊZ&WRxxl'w*괓糞Vskɥ,FZ׻zxo]!0=o-6ez-\aN3xB=̍s--AosEߕ?9g$i[_tElo/jBˤ.;r~QPku"x엣3acrSߓkZ _L`QjZ-M3ӭ.gr7v7Muvͻ`( p./ يIF2[z nqy#2jKhY3 +fͶΚ,G|,GT Z訶ё1cI!?࡯Xe|>D{\O 1jX?z?D&G,O뉵R/SM3Z,~i=fxUJebUPYܸ#D?Z h韄ias"d?+"xـ 6Z٬"mKCg2L[)me5a/}?{:c!wI3 1휫+q6m;pLw=tU]-xhcll{Ҋ4'Gj4L?$̩ /~gW|:L_7}ݷ}Ujs: e]5PoUϫ`^ڻ_B~GuhK黽fg g^h3 |θަx;va}nLoˎV꺎C|B<@ R7Nq-2p=)+7ϐWjd-'ȽRI4F#]/Ix*py}97.>/LmY4:lœrpFJmܔM-d\T peKA3@Ǎ@ m)g䶻@ Жwhh{r: rrbbKQrM+Ǹw)^E6윇`2zVk kVԄZ`:%-`UPܽ9mOʃ:Aѳ?'NdI+||t5.r55 Z.@#[ݵQ`(whJ.(14u,\@k{<$7;/}ОZE7TO h?EHQkgQbzX,;#\[XY„}"TT0+& :?dj̇3u>]Y"l O BLF;=oO(0r\WCu/8@g\a9~P1s:%S^>yz)Ial[ˁm ݡB~WY9c ٘8|&ן #V`Y*dn#WQL6ZS!T+Vqo]fv[fc Pނ,3MSsPi'9%WpWu7'q*cIcF'eP'B'߭v:uYS.Mv/Ѿ=S[J94~f +jX^"b{ˀhᣘNp=!<[ .S_JFt}a?_{3yIQi'Ljz?%ٷӼ_ 5 أSKT} ,F3T9T~X0|*U _ȼ{: 7jLL~ +I}besF ƿf>Obv +X} q<ṘhTn!OE7R Ʊ<sx/_;\g֘,.=ʵjtFgzuFB,z [ۧ+ ?[N2P=50=b)oS;g)i:$V,{X&=(,& yc=K o`Ojgyي=M۪b1K.UN>U].ۅכM^s<[Jq.٩(ھG8GqUnY(!'%+?!( C#紺3V%o.{49 Uõi/׹/4g^|V֊Gj +`I:V=$I:]E/6|jgN#mȣ1hڷ3];D2Џq|շ!'Ք(JU=Wi{?i:ЧCG' y&%𠦒c$.o nUmvc3eLћNf rpdf|񒑤kqV"^aYET;K9΂ם?Z gWvE5c\m׊].iDđ벱-+k\Q^!,K (p̐̇ˇ{ZӄԤ?4?c%q}\ӀxkQIq,ǙChEcT7Rvg?IZpcOqK>XxY + 曉*j|wߎ/d߆o;"w߷9sߖEm^Ҁߕ *}_Ɩ1a׋sIQyeqC,e:0~쥷ԇq'lryFlr'ռҾk҆ ?Lgj* 4ep^bߊu30i:|ksr)Vkx\xQݖ{no5ʚ#e˚㱾[s{dc{C fnPÜX9 +`\%I"3U(ݨp4TL="gX +iWʷ}ʺKܥ3vXzLnSzSa6YKy ek5(]bj:aջl.mQY4-Ѻ+$)iwQK~O?7yk–\MNkNʿV~_5ŪVevm./ wJ“ +Ov^SȩZGmgm/=#1h8Fq}nA֪*p?s[q]i>y#9cr48L26[pkz%ۥRIUyu'n1J;=\jѽpIxMC7.  J!(lzѕeeU;Ww?AaޡlfBȘ2i+2vW~ņ_esyȎǽ3ɹ q!mPgpl]cKe0󭴼n,YtRkXڅMi0&kEĴjU5C d^^"=#|qqYkwe8.!p_g֎ 99Phl҇^1[[+}X'`$89Akаd %lB7?G6)ԥrV!\G<-f)e3b3\J.YgFEz\7 7YN<¬l0wxS}ZꞧexzJ܋)>]=,jd=6_9v{XG׏RZB{."Ami 4:y'?m%5U{: Z̍m@Qϩm c͔FJ-3zKң.bMa9q`n)b_sŢgV~ќeOH'08e].>b՗2G n]bPnJXtcZlK՛aM#Ovqb G*<![̣yE'N}6;tή"Xš/ya5&+Xf#;t#7siޥs'ьڰ%y "D2ej- g}O'h8Ͼo^;^х-F& FSHm^o&~ۣ͜JgR QRÚs S)-~dS! ތx-0zwfG1TV>fnrRTn2e>,?0vP0/ ꧊.=L?8  ݏe,7}eVkHjHfLT?dP\a2\NJK\9/fRMA~!Gj/(8uN ]& ,@ P+k`L8|X|CDPBA|bXd0몲xU~t<A+K)Dߌo!&T@>^|@(PWB{?n/y~'qIG|yo'@@j,p]K*f,5m@HyB`\xTwa{)=aBSn+^x1}` $9cVq̯|;sf +ОtPB +Wx)s$IY N0x2%eK4 ZBĎĴB%Gb>Ir׆a(wgkw[fs4kwaE qw@ Y(Rwaojio@" XȜ2<"*BTrv$wS1/N^BGzGy>TE +3$ ug w8۱0>ޢ͂^m)7C;'y?< _pNUNƛ<+b ځ>ݵ;Lnkث9LV[NW=fjO(3+=C'ƚ{ΟY*3b}yDYfxGR8N0Ϯ9껫jzrx<_؆B +6p&Ccj I7 Vg]A*YxE:N-:^b\tޮ}Dھtlٶh /^Y% zk 狵B We4,_'g\<$^ԭO/rM-RBՍljU~S4%;=*9 8!4iAc#ߧmE0} W ]_-uM/^ˇL.EEG+*F/kYs7M@]]^5je)W|HC ?C'$)h,֩_^Ք \4Ԟf)8=9#gj"}9ޝ`}3JsU̾Wfrv3o \ZJo d%$2U?]3ʊ*48{>޷)vr徔?Ζ+˼}ݧ .|Kk8&5ܜFS+B$VeNprI +AC:]:;#c]\} lִc)Q]xVtuIz2쌚b\1@t $Bau:g9= i~zҗ5@}yP?pl'utr'vJUirv3z^^)™YQF'G]+Bl +.t^\J΁ȯ*M;1HZ-?޶+ڱc/i.57(S endstream endobj 38 0 obj <>stream +++#kl5G9ICw UJYn+KP&]@6?2u:Ū +d~*H܋۾~M \ў{|ɪC*֕Gk*M'a=w֊1M>Gȫx>U@W0/NKZluf +<.Vs^-VOn޶ewG[clE֕{8)c $|ΊoJ> " +^xrȋϊ<2tMk+-{lA|n1'yʃˇ +u pz/Ml֎rp22^֞|GL_橝|A\׾±~vEʹ\y,˿w HY\eD*^ϛ<8.ѾnM>uƞa~Fh=SZw;}( +` |?+wx-՟r=%Ci0b۩ɸ%fK`-%-X6 {}ENqϛD1w lB(f<FI]:ZWcXʼnLfo5;cwrK#f#[\<ꣾe2塦̄+r_e]3_|,Ov%PcM`zZN7*3i)\mEi8 ԛL8:!m[. `ܯ?϶ß7f,Y/N27+'C#*5ͩcR[vHR4'v-66POGIW֎^7ȍ <1*\[>BAr֫r;,M*eF;o_<<̊;>!}[332M8ZRk6Pk~+ +zH]DEB.ӜZK\ݾ'̩dQeļ!jdzʳM?ni?G󽜚r +l +DlW!f R|̰fVCj7jo=e~H>LvJh0{9,jntS`NYJZa'[Z',N&Lh+1->'xCn* W,5)*ii]dsMg +qbL +gh[eLw j1QgiNQ;Ofk|h|tm DBh~pByWFe{5cԪKȥh;$722Ě–Tw]b}Z6өʿ6E{p,bg.=t`QYizK|84"oK6.܏u$w\Z4{firG5il?Lxs&_fuXj澔*ZT\~?hzs[\_zYkL:{ԧVkUjYsiVsh:Z5c6[T%T\:#(#(SSǘ|S;ܸlVB?q7YC+"y#alQ<A^R% Z{7xɓyxGoUp.CP&cfpeyl<\+Z!de[j`= s S{ B@.YGڻ,B`VllA =La)`J'Scc{QE2KBB_c#~ L &}`+ nMh#9c>aXP!iyMxDAJ,>}QRxYj6@X~AWt!k шD3bșr B*xi%$s&XO'݉&~Fǀ|>Y,Ggho$7jBOݷIhe-(2K*T"@`D񹸀xdj0EnnKcleLC)Zս &x脇W +N+sխ}z/S?U(؍V "\>z " \VʼnKw{ A֗;Oj=L~t'M1|}if3gK΍k|t/VkF hW?G<~L^$Uh[o_BQ|r5nӯG>a_ +\zD={'u[cwGzQL:=^vþHwT+uhTi{p{lb>aN-jy}졕g'(!{p?TTp~cnһ"dлg(8bi?{C!ږ6-ē0~QzX% 8l$4 &(Cz5(s\euя$!gns" ZO}VU5>}aYJ[m*:; vvz]jYKcb`Ddu`Q\V[:5􊲫jWMi} hW} 1S|u64Mvayv "n5rٟeigIr^$1YԬ~.dg7-`>K)VOJ}BCUz_0|ۂs܃9[S~3Vӵ_m9fm[٠#.{kҾ&ߛEB{juaWhGk +{*UUQ%IUnwۊ~S^C鎪/Ax- `)L]l}n@Z' +K-!my=" vs 7tc?ek*T)1Q7? 4ĥ.n~_ +8P5C ޅ[c{X>&ᯔB}E4tPl/5N*yjRգU uUj gA฻]/h8_%̼T += +een7pϩi`67i'qf̹g3S2U +09.#zY6{lUīBPu!T\_y6 iAZjs.Y>z2q^*!krOT3; 8kk:xOA8ّE t4]/' ',Xt}?p{窽6XZ OԗqmϮOyZYq+ZЬ".3?(9 +E6*];r?}sMR/XU6_B +}NV[_y`r~SB]Q}gG5Rb9XQ*(<ׄ;;"Vof9nR &,W-/ ]T}Q|Y,GeInſΡf dsV9>Gz(64C*N3;gՒK,X_Kt%#a_%NR2H򸐣ř3mu+QeN'4TkUz!eze< +7YԳ}O泔ۗj1v\Y]2*gt}I:+E?< +ً?E4,5bqpKo{Pv;]ۧfIt3.Tn>+5kΚi2l(ͨf6? +HO1sy.&i)5jފT kgv$4qÏaMpe 0s/iZv2O<̜Y_hDNk{sb{hb[Qn2*XQv_0Χ)J#n6n>i1;]8dZ a~af7탎:9-i`Jduf0Z8qk27/㽪~tPgy6F2Z%|1b?_q &=wgPΒfii=6# 0bbDѤ73 AZ"eh|)x}gdT4~ȋWcXu?+U*/};4n-g*IuD;c/+:`2zq5"r?cԧ2O/DOƕ^wjv<ʪ"3^LW{:Z{:a{?o'lu}Ƙ%O봼8\s*@z@^'NZ8w:Д!1TzB >TʘHH; E`2=MUL+Ά@Ďo-w +AD @>P4(nw@zHj*Կdw rrd*a5j͙$)d%ph{Alڢ<Pam@OɮVV:s:w_/?A Ce +u5QyIM?}e9@Fp֮  (h +xNv9#q*Un֟Q!gRT(@k(S+ЌU,uSTX}6!`W +`Z%MfqNHJ[Uf}Dz/uo 3Bl Q01,|2iL{R+.ޤI6#>brMK>}]y|TTgs@~ S;2I EɿACy7[kx1@߆ 0apX2FS6$9e2EU9VPS6g3qyhIp3`?lh|n'I +Wp+H'̎֡L%e4DNJp9ɼkzk)|ْC~/2z悑^lhw:O-hjscʿʏK}O1A:4޵֯"V^Tr(^| | 8{MPFCzyi_֮w[1ns:&dN{pp^)>Z%2+1'ZmGكkCoOFѹMfAq?z|cw^Sߜ|pȱ-jwmEtuqNw씲GondϜYns(w솉SILZ~I+;uԏ7(̡LIȡf$3]Vas߯ޑ|&] s9Qon~öFJdN|9ʩMm挓.RF/t~p +~j=Vbk98&VO).@\A&·Z1mpN4ZgvL :*u={0ς+hΊf qnZ7ȻOVf| M\J܌ϏXhN'4Ș`FMI)b|)'=/oܻ1K-mmCxsHmռNӍqH~]J /z0|J(y^G6er!< ̷>9m?u-b(\h~e]_bt?NzV]W,;9rjo@fF|f΄HF~'YΫ6R6X +mi^YqZx-(!Bmy}yi{Qw'&^̇'68;$?: Yrp3]h.޳ / l&mܺRr֔'V!B*&ΛD9\"U+x/ηQ& + 6jGܛkbŋH) 8FeY\d1A =ytGzP4jT2-"c9>nMěu#Px|t9=\X%l3#ȝH_4uid=K)=ۚ 6nEiG<0Lّ'e&u^U buG /O]ٰx Vӣ \`q4kx &opY@>!)mF5aKSιbѷZs)ӴV\RU RLzyÕ2v,iZ.r_n 6/gy97 KQ5/]FO-?~pܖД:6];R2y_b7PQx|Zn|?>H=֘J.B/N"ίIo慮W?,t&O +>grx-+`~a~MJڡg@5k} |#^GuΜ `঱E.!k܂u\0>,BueFn~ēOqW|J hobib??˷ۏ֛m.JX߿tl!ԕ dܺ=8n*߸΂F?Q\#@UgmyAF,&gNy31._dccጧSC=dO?ƃC hO<˽=L^{5nSn4d=&t)Ե;Z7GT0ߥg5=JH:LfXwl3 oFloM4[j~#L th ۰i + ׬Yܵy#XFP+ wv\xT8#m4cs_.i Lܱ[JZ1}ЬG~ֵܸDxu֨cmLf2ݚdUK?h$r;OȖҫSI>Xz pGa(=5\1[ejXIliB5sU+ؽ(,UZR~8BpW~fkoK~NZR +!MEfGf#\C{ ?p!Fb pN޿^)r5bn0`V3ͦmɓ6"aoϼ4iE9u^L~dIs8q(7ZnXOGJ2e=գT ⟠NzU f,JP]2% + +w5uƄp)-2ϵI +֚V$yDEdHK+9(Z#zMJjPCtS wU( QI#CR8T̓h3 u#H'iPv4VzҼoAZ_@z oL =6qVϡƵ? KIGGL(o #+ <@zTAٚ ͖Y)i.EA@02V!Af 3 >u ל$;'ijf-C;!B R 2cqfC?~|PW<8ɅioEs~h1MMM#ؼHE he>f&lu  " /+8k%SG99-Σ*&HQ5Q%l|A'u,kXjgfd#@:~KLyt@]G[Z;%-FX>Γƫ t1]|6x}P%4*0;;2"h>!FSm֌+g8.giv3N'u)m pH? YXثR͜0kk Jh/p3lK}}O|7t`S}+NN\\'\+I=?:${e簽'Qc RKcmm[i?!h(rQF̐)CدT^?\ѫ@ųR7Oz}>LjJ{iJ;Z+@}<9XmobjrYvqv.nڱy|az~L Y*#}AQ}ba_3l{cǺY:,{nw5t{ƶL`r, Q/YeX2o}jU0zE@FC:V-~?Mxd׭r÷q;/anx?NDp ?dv!{ԆmC=mPUŲjJbe(opV?tQXio_l84@Sf1˳Q#ӛX?VsYX<{ی]9bI%$زnBHqmNo-oCO?/ڄQ@܎ A/W[CDIpm}?}T +.3G}+tysF1 D9":{^9eu7J<7F^/к{qs{zPT~Zq4~iM1lk\;.J4;AiK5)iM1FK7zda|ZݴdԠLshIs&ܽ|+߾r'dis3?;}i,(ADdIR"b}Xc0A}jv>Qa]&=H+%Orz6&5~>/7R000G#D]Ŝe//. [q]L%Ϊ3EXhG؝;L=Huo]涪L)H-jI` c¸5 b$ ([][\v~=c@^]zRl|ǂ1މV6WGmdV@$ ZP% 9)0H^#v/=*=tf/ִ(uq򋠴֯\ͼ;hs.S=söWmm<^wZ$l +/6S45^vbﻍ# 6FB#c"}QF˕;:屿"5٫%8OARV.+>,sL.CDz6+uO2/H S-02M0MWIR_߻!W }}Pu>f=#-i3#zuͰC,PۗH[uꁖfRF;\(C8l._"\ԝ')bmnnv!>Y?*Ɛ'{zYg J^3_#o }{s3t=nu&h6h٬>][q'!A<̪&.<,T[C0^Խ2254hf~)`̜Gp9~r0#ȼVoОp.Km2[~5lzye& /8:S2!nљuֻ/a@@x438oMcwO oYɣ{J_˧`{Vc[G.9Ę^Z}no<|ɻp&9.Mrv$0Gek0\!oG.~^>(e2ڞhkfϊ3^u)3dQrrf>'±l&Xᵛ>؋`؆V.m (/M/^==x]mzvgĆy7תjl]ze1g?௿0Kk㷤фk7J{mVV-1',엃.>GYQ^ͩhTZ{Oh]~ƭ:^4I<46q/~}`'YjkҤ6%M{Xs[\>P%&ej{r{Bihv.z,\7hī>,T&pk\;UdP:(Y_Xv"vxX5wĆ{y e>}3#ݞi<~MNfPm5$y7 u~U{D5/}O.n*Zr.m40XOQо¡08tPu~oHMrjp睏Ecvk\T0oT:A^ay~@9]p??~J=FYbqE 3Bv6PjπF9C$g2r֒t3{o+t'4}(J3|VE#c{xW_v]~Оj;śzo E~@,%knNG&'ce~Ffz!SpAmR waR _SN3A + a@UH>#2~?,EcbTawO6]el!DIvLM!c5jsGΰTXHLfGsKFB5c~6!nu0}ţHdJQr[G +PWji?k4G\[44.쇐G1QP;YQk6rb׮ v57z[[x]0+P-mۢY0=4!;i~S[ddCN|L35L~9=c34zR, +mdԫUp="ޡͳ|VPP+NQĴV~#L zp%A]bLmhZ*- .-@4ʍO-WĂl`E: kiκ ,>4x6l^8J']Υ)K4]7k)@wʗJ,|L'#qac6Wo+Ocr_qD?"IoyXšbY_ l_who9}=ӻ3yz{R{&wK9ϗNOQ=cOn3ߜY%E$$W+|iNqӦ好 z F 6Pg##朾C =wߊ\ycwr[qڥ Z>؟l߸׎mYSflKdsF "F/uT)p_{+eO %7i`_Dsip8ק?6|TRiw?r+8jpr7|A5ov V5Rp|M ҜdטDzݶ +BC_Z++ەNbiMBaͥJq Uwsmo&Gϛ0˕es uCZNxO-c>FudRkB>RT\i [2#%/_ ;#뻙xJBu*kbOѥc!]^ٜבּPMQ=Ƥ}30ՑgQ][f=S.k2>qJkZ?7w+5/Q48^7T///2.Y7ô +inM|z[;3SbaHGKUNO],Pī#XUt?v$tQZWUA%-I//EK +GZnfa㞔1d̾Ū0MªKתaD=)F~ȷ%]˂JIzL4š(RH;.!|N! ,qթ}|AFZh0Uhܜq?G̙ypߡfÄyYwPzgT%oF&Mn^b +1 : ;眕.q%k{*9klտm3N EZXLIfɳ;0c)N qQ8W@ L{Yl.a\J怜"`n/9_S|NMU?;J&xlZmNgk;oƿ +Cs~?H [k/bS0aӎWPB#Lxƅ N{Sз糐;Y9Ⱥbka#sY"{:_ FzۓM}sHragqpNt3[KmDe!|~"*`#|:$+#rw*}1a-#7:s=LbTdMj rHH|A4}Y/Rڹe J{xht;kn,l6:T94n*T?0}VvF"}Y9ӕ|c{[ %E06i};bn]#bZXP© 2בះn 8vTh ߫ԨbJep{u O B)\&Y@JΊC׃Ck3vVY}\-4$D}&FV@EDqiGG oDO3 &hYך^ S*vV_L!hMQJk%AW6Vi=e܁1Hld6/oQaU}:~L+u)>FckȢF#s!si +m^>->z=y**$Y8;::Ped7ϝwx룐V)ެhxs, )%ZA#Lh/J֢'*6ΘuS><t'T OrΎ +i-N +Hv +|.K'K:Tn+-[^4r3?,fNʼyUp= 8I网Q7/j'ōM%L*1ףbD=l6MO +<ۧsӱ냓/5'p1>Ν f A.@>O\y";x% +ƶtM{梃]f57GDža[?61l4x8:|i(fذqŮ_9&zS. :Y{h>k~\h{b yFnjt%W{# f!꿰ԆCP]X5yPMy&):c\hx:GThutٶk]D.{jM*uVfaiëA]~J\@=(Tֈ +w%-#qQnXu7t"ȸ1N6߇ s<y]zl# ۰ٸD^z ՄR6z ?`E}UE=M +ҽfO< +2F7mDƒ8w?;#\`1wEh/)IBPNw%-{I$1$Ie9Ip$:?<I$$Er)i&e笓Jt7nC3LjxHR,&Xz9#j%)f`h2ƫE[[35cqݞ_dqЌ8ٙ'o&GITOgu')͍KV;re2|7ɼl&9oY< 8<]cwH<s.}`Iʡ3V~#9Gt(d /8x)#&;K>.'=1cA=^aX7W ,hDq'q+G0=oZov&׃Bd֮ծ(VI>>C;wd\Lx>pvɲW^%cհ=2h +}E6{Xzgǰ*OCYozQnUrEv$Q:_O)|+*{78<$bl47?Ҝ/͈fi߾~Y9WQ^ew]0i-(.)ZϋP`Y"5W~UwGm7dr`$8(9o7 +ޫ^ىVsw?x({im +3\)vw\?)>YS5|V %lD*"DIs3IM4sI H uCm>qt'GL]-O>>{|2u.vP~ڻolse Ĕڄ:` |NVZJ]U=\_YqYjZh)i6e~#7kxx]4|DkQܹ5}zF5d1njgل,fdp.dW4eYiΪkqRA$4R!Z.BKi;iiYB7߯(|_U/gus;q;k(9)C"AC_:f +˧Vq9W4)gP+wv[bBb%F! +/.V^4) 5潄^sC*zN;K9ՇFn]I~*rZ۫qޫWW))O#3AYdW&L7) ^v"(w|c^" +#;;Kp=mUhi4Ӱ6ԇ ׷XWDr͒|EI]o<ƛ{jPw#LSUsI_0& )% ĝEK>nI`'#_|>mhYA6ɕ\zį +Nzx5+Ȼ1"3#"3xUAfs^4C<(Wu h֧bhVS?,ǿ!8[krZnaɓ^wQuxR&q(C3C9/cReQdvq 8r@*>3hCTQvKY\unU>ſFhd3&W')u5I8ܢy-՟FKcԁR?hvB#$/^Ӂ7Km^vd-1U*[%#OZ%S(2ӪˑNQ0zÖe|Ã?.n;|k\y?JX.Bi%Wcy:O5 +6l t,o=ϣsLsu^0"0/m]w"u vGwrhˬVn4:l7ĊeI4CUeY[[r.ιp)7m`:‡t,)9 )IB LQ##ULU%[͂)￐םs\A߄1 +D trܬ\[ͥ~}| 3fF0~ mBU'akx"пFrקR?H˂0_{FVmW6-B{ݻPfE"/7OlhBoFeEd>He0TIfptYLya޿9E-2i,Mx*I)Ce\$Yi9QKdk~c$:"ϊ\#ol?`M,(Xs,f,fs!Ȃ Z` [eK[^ B 2>d0~0}}6k~Jʱ7;zk$ھ,qڣe` z+ܿ$C~?(d(6nu,P/+YNI'LtѼ"zqoxD|ɑ3i!HRE|TFQ%?qz`dNJW?+~袒z:s`Cd !``ݐά_KVbϻvpptCߥK}wp_0 ruy۷϶tEðd +KB=47GvtxY^\l6Y/c<3-j'M}GK9S|@05\mjNY֎Bs8FdoH"(>|^2ŸӜ=&]NuҁsPvv||4`4֙F9UϚG#Mg붣?;W͇m YkvzE53Xr4~Ϥq)74 g +<%kXMQލ ŕƔ ܩZQ}u?Պ_t;BdjAR<.R[}w7~aݹfLf7SҼ?wWR6?l{Hcq7Wj"#Һ6 +MqR7D*[ +nҠ'"6;䥞iaM{kl0fw[kYŵ#hW-@/zl: +QM!l6ĤaHf'J⊣q.#L6Fo3r/rs>N>D,JHs'}lvYWp3ҙ a 9j +g4^kВY-jC1:(|ҵ823/L ׻ݍA! Tgkuʮs~Wom͒]-yF_;Os,%w g0Bmr:4Ѐ5TCaLRΠI\Vi.~]d!r9aî2Vۦ7kWKgtK'f\_գy8 o_]m»w7mn>#rҢLAyϮQڐ^kl;^F % +8] zG=<ώ@=Rz[D构1?gn?i^ZaV|N-!lD/5SC^+')]q{_|M6 .[=cPe 7"]rVDyq3ZFFb|!}Ϥ΄N,=` aELccKD6KRSq,qKGK _ȑ{Hݩ38<2}qCpg|cd|Z43qs):nķhآk[:ےVy⟇Ӛd/mp=qHbڡ?[ʻ|$$ ;n.ki+ͮ=Ej4IyMwW_LaK%gn{Ey,a +jPj{9Exs:%ї/^@(l0#4tGb>wiv_1(>=!z^%f|3tHl.sYoˮbݜs wkfTPL x~rg;M²ʱ<gtpB? 5 M- $x +7OSRJgmԆiBLzMVIZ=+r_E]!<܁, 8S]SIcBzxo_"C5n +mr~@~l6,oZUsp_f]}nr1VPK ɭG2@:!pwZ~_a;̞d/W8NW£vjsSD/YGԸQTnhlC1ޙUg[(ctsWVYj#r8B4_ bKHcz\8#X%֢E +y( /Dz+dܠ8)lLR#¨y~|A;w\A^ɓ&. sX/h5^InlurlyҋٞflL&QD?_JDN<:%=:E^ # +T 2+'OJ\—=#K&zxtA9}?$+Sm@Bbf,d8>lɶ}~O?oP 62J,e4e^hEj|~'~u~R#K#hAGQ Q'${ݓ*ZZt}( }GJӯ(sGQQ~O.ǽb¹[ ˵[W][>y}xlu(Pf2bmY8dX2 ;R3 +LcT pQQ+˾VWUyo9FKw)f1BwmMEN!@/eϰGZbrɷ` 38?_f ɩOfv5體ˑ, -Mld}Y~iYSa>ywPttS81Nk\5^)ƽA_E&}̸SM_YHN_k'8}u^g<V*_߼&Rl~z4 9^EoOHy[I +#8p'NH7IBK`Ymy}')VKistcŰ}cU_7N_2ohIcয়|M~i-GP'LÄz1%v^Joe/CZLtNE>\&>n &TX%jR~v9]&OsAEO3N?ϫeGZ|ps1DMTl5U>/ +Ȣovd |sfK@NP?ӕsvʋji/hFL9lh.YqΗ] -?|n8-֝ et} &Sk = s&z?F~EQNdwzѯZ4{xMQ/pJPT.a#̓ +ž7y͕8=^;N|1ޙmʷj]T_A?+_n&LآQٷ^]ںZլ ԆYW`Wdkd{=5pʭZy,zQ;>=}֣uZ/(jmajcF!zȾytkϿ@:]WSYZaOrJ:_ ~~3^cɒar,wn >;<9 (RicXX{AbU/ZY?] XYQZU+Q:!E!%VsTBQ ? +mAT v$đnIm-٪~wMYHCGxiRXv!PUM+ߔvF#?N,Qkz UyTSz\{9 R㻕ѫjLފȠŃ|~1ި%@ b8{V__T Šg,lψ=?kr|`#@y8C;aMmsFۄށXIT<8'y"{%]>w| +Յ^;3_"?&*ƒJHuT6~JEvpޙv| czSPmUY3Ħt ]bDk{!|ۏh\t">`ݷ؇/0ni$Y){%mj&ұczSYk׷rt7yf" U@ky![]*pksb~3[`49\Ԥ<:>ڌn=h`Me yuyli~p~6(iլ +܂cOž1je] SBᙣd qLpbxݲMq(s18>(l.<!df%d'=2ߦMӃ%':͍K9 {8Hh۲^ z6d5}|w չw#[^G(Wn?tS˷ BTVGKMU/7Ek]%:ytZsSFH:j'OE{r}$K56KkdߠA6.) + yўM۝0`pTt>b)kSb{ ”ܯ{R.L!qV-g ivpeE?1&Y3E5בWOפ.L*ה̈́a aQ|x!f惆`;ĉ(2xǯ"HӰ DCݦ CM)ɐlby:}gMP.JTz>#G04օ%BN=pk>gM0g0k9n + ڽ{WwG] |EMӔ2r32?7Yd;|h$}뼖}?t\^ٮ&ؓ@h.}gb Wp(p }_pi6pfbFXWj{ZB[U\ +v i'*48~sxbѪ!% @&@<cS;@&m}_hG +|k`rMD#hB-ηڭ+rZ +IЙbyyYoOm۟3_iH՘?%Gl1JhRt'v )]SBm/N?%bWIm3/G 'UbdgxlZOVB]K Bb'[ }* U4zƃ9ى(*#u^1CnrrWݫ] ]=q6։ wT? =F?7uy~!R{u(M W)ˑK˺b<`9'iZ-_mKG# u̳'򎲫ݏ/eIj ϴ%vZwUJz~*eXc<><19{BH6&y}b'iZ]qwlwgFY"&re1gVgy1C2}sprċ3OhtktC<'l}trށhNޘ-K/cz畏> +acݰT X$TnM9]H㴞F'K6}wZi#qYOxسTO>>Kܓύnr)mR^vih0\= +h/ʼ x|/7o.CF{wz~FR%T;c՟ WF'lkчu'_%E>Db,F+Wb"i_&l=BpҢ=|ӪnhQdLBb8>p4?H_LTl|&ϤL==rImS|ZV 9 mTE|KJ>/{W}7 2G:KXw\|V0Wש]Hf* a* y; aV ,V{#W_r~$mJIB8 i=c4<_;RtϢљMN*:w#nSөܶ"wѫTydH߆ +Y~ 8Zp7qXIr]ԷOc<._;,Iɬ7Go SzNve{ e]5i`eݘ㖑=$-t6&L̹=7n.fA, via +lMNt4DY:-ObMxcΔx*w󡏑o<5qzǘ6+c5n?6 eO4OIY>cJ֊t!Y2>$N U)2]|C^vՅ2owLq9<8svhg,|*oȫ,$z1/Z$^Z\ȫWUFtȈ"{F>>̀t' Hq; ExƲ:}&\Af*Unڳn&1/!yx]Igq8'xUɡ//g!˭h&ݰޛk<֖OaCǍ1_`®z99`!o8לplL?5}4>Ql|tԧ'q3_M" ",>Ä(Pæ%cgn;֎[BR6n@?ɾ'@OZ~Okr]Aх@#/pYү=*OW>\X7dXH;c*U`44\t !h딻و2wٙ~NѪ]G-Zzr X_M0 =?Ez/;}"CiK UV`ӭGꑻ&%7dr V6.eb/G bc{eN'K5s9vǓ^찤vsS8dJ2Uãfʊz4:rId j u^.fK)>+ C?' }^1}n{,\4&c^xj*bkMsN$;lƻVå$.@7侹}-V&4iݭfsN*`wcptˆ+%dH}8XYRIy.zQ٥r<NXe_Ǥ\р6}%F-J; +P v);Y,& quou;Cqz +:WfIޮFw\jD/8DGg;c#o(j=^,/..iNY7v~ޅ+%nҷSN|G$ۓGz4,'Ї8 +Ȯhx2V$szbTsRq +@NZ٤@7 @Щ(Bx@nz7cn"ϢUG=Irr}:huRy@\۹r7U~6.<@p0cl] _Gu[/h~ϟ3 G_{zfayAOxeQ{E[nuտM]発{ o,}Nh G <}YOed'V.b||U\ݞ_?z+5F6{0[ɿWTisr +d4-ici#Ӻߒ$ 聹 +~wuy[V&ͅ ?蹗\Hn>=^Slh F/iBO^:y0Q= <_=tglp(P4.~0GFHl̪_E6g(U݈lЕ0 rbXq'`ogGk_Bn ֡_2Y2rO>}/G%>ameo5]0;Mtج|vN=|0pmuS ԋ-N&hwsv?&z: ZDz}L)OW^Tě 4 ('s[&z{߈ 伜tS9¶!u9)9. 7} +E}ч7jⶩVoXڑ4KVV( ,JJu auB+ræ5o?#ݟ +k[ޡS 4ʖUA&v} :͊S-*6)NVדZcKOEJj +.  xkF a6`~[cqvro{;ԇds/*݌tھہWP|[WD+o˲7!"M@Nbq#(w]2G&h㞈y}x`s|eZ!9f!8FxL^r)cr=P[ndnh|N[͈nbUvVZ%/D>HfKĻ2ދ[ +,C; +0e+`{b Q6?nfo cٍ[Oϡ9X˷Z7>5 (ɤxV YN7vnrH;~S1 Fw]dLX}b/zA0ZB@"p ܧ8*J~ )krP+Nw\xa@ Q8^}o ۆ]dk{8*Úґ>ڣ{J, ]|@Mƥ<ӆ`J/sEى7_`X^4}aP ue Hzm^As>XYIԚ̑$= ؒ JD9h$>+/T +![/nV)UK 1`p HS5iÏ| >^ 7uS=AƦ'&^!uu";+q^}yJz%>e#\&Dg#Wh`Ը0 8"#Es'IJj72YPmP|sQ=!Kچ67%S=x)U99*m*@O|LD޴වӥ^x6J^4(56zz>f,<[&ی&"Zmr\(!z7 3usIozSLi4vtZ?Ųf>ausOa9*mPͰ-PUkd?a~ʘe;?%s8Jpe5GǭQP!G:\o}ʞ|gg\F //Llmm%=x!IP"wG@ ]+LzƋ uif]<{kL,Nl1hqroqD01a6ӂ#@ 2sֹq ]5lXd|?M>u*lɿ2ye$Ϯyf9C`%*Q1;yv.k+L_lC^> XTO su\uA7 |*MJ]|`6heZh-tX&?zoa! ,umq&_|%t$?l3Oj.,8Wt}GmK{ta_t;Sx.^POsL%xt$G?%Sw Aƿ:6S-=~FZGQ? +UJOq9h,@ + 4-a&GI uRf, ܶJ%֪:<rv1~^PY?)t_еe X#(Ɓ9mk,jnx^O=,.=ua8w5-rwOǻ軲g%__Wҝ=6f]lm֍]yvo ~JȤm#4,2岾\V<:ZCxx{y[O}./f?ΆY:ˇ Ɠ/ks~J>\7ElRkE]nŰ$%e=!ߪu)!#~wL4'yp'ęOuw_9eWNiYցsR5z~kL[OfQw9ڰfjEۊޟ(Gp)_R,,x~~8 ZPsV$a{`/imZ^߷+WZܛVs}73o@P5/P wG-> +ǶHSm^^qX,e|yhƬg#˻9٥0m\ B>/мUi]f4cVHlGVz촥 40 ~ʘPxȻD-Yt*/~n#d;C:.{Wئ[P#թNe4Lq͘yiqrZ3lY+yU+.Ps*s"T˚)eLuIZKFO?/ì O*m^ϟ{Wk8L+}y/LP9ęxúVFشFU lʼnҐ[|hܞ3t 6U:DU~ 9_ZsAp W?l(ok0.9kv8̆sCk󏯯f=BYƻ'TC.ky6s>ے\R]ٶu"a{!Nx?*ƜgξQd*j٦},?Hg ͭwƷF@ؑ̅GGGڜNË\͕ؖakn-2U})mLx c5 6Q߷[`TըLV_=oB<#yhɻIHCG涚xVf{T2&zpz9>S~Ve-\ #V5s#l5[]o-v\ANKjUUASJDlj%ETk"2_BGe 7Bu#l5QPFG~sFž:fBN8쩓#u&X} 5B_՚lERW2+Xe+.4P| [^zjj~ +N߿@JFg7WuݬGoײ}oηCJT6Ce.*2EEl%вV:`d ++?P\#54|盝2͝ǵ-ײ GeWźBzVuij|OR_U{.0i]@/Cj#( +֏dwoĖeR Yξa-t@gveo< "tK-g,?g?2?KX{6M% ^*qb6zmq4d22#M›͑ⳏQj%B~y +WU!&kp-l0U-i|L"VMGi.s:WxWVw"_kf_ٍU2X$BHN< d-Z@"Yd͊;Jeݔ`]"&BxT^l.62Z-1 ~n7h@!KPJoGyg@=J x+>H2_{C?|6@ ~?qm]?r!߰\۰<(|m#`?OHh6*ߝO@@G,DYxHH 2 D4N1:`UHb_%{_;ӯjTm@e1#.in}X0x{+~s/ˎ 2Pu#/ܷ;Bl;,{ȫL>mf[)̰5YpR.;f^'&Gt[Dz6NO7s;>(*CC;|hlPBn_td}ȋmy8p{u 'iйç@>{JmLb$ %wcl8 +6,qjWXזlRa@Ŀ/;a]ӏI7sW4#GvƊOB>ut}6݂k|>j_4ޫv,ou/d^y1[ӞͦB9](F<-ڴx"kW yHzn u5Oymd-o-\<]g!vyLGlȘq9vvZ=^&.ywV5?F6"6?J7wD]0b!` v/|)#O}{>gymh1@ x><35?0Y+:g93FVRc|gD6j Gԩ1Fdg=xvǧy;8eoy<ơttG:06ww('ug;a#GIiЗ?כ*Tsf?ZiTZMv?Pׅή!;>Ƕj˼P8g-ޚSm_&Ѱwé_*/a&Ǎz؛~1ox?VP)2<<5"X/ڶiũW5'"S2F97 Ea3AG~=poYd p.&{%tO^N8E SBx4Zs]gEc1rIHkwiU(\Z uo%Xa^D߫wӎ4~^Gz}[|rY8u9B3ѓYq|`N\i~r‡jÌ$Y]L1özy' +~L3cߋw%yzb zV|nMg{ fv<\X!VLj > ׬?jI9ܙϱ1fKƸozlfن>z0 >r^={+\:/ a9@[v.LF#0'ҟ6 UoX;Y iľv=5 Iav7= 9vVݾ' -BcdoCpNO;uwQs"j͖Q@ un!,[ +h_ Ou̧9&cNV4-$M*JV"\L I`߃[ +c Yj-̉D?d:%0~`߻{/&Q#އzY(S^"gBw…nkaOkS$o`""~qtZ7}=ߡd{5%51Ȕ#[wZ2Tzi\U2F~"( ({9uoTZJf̤֓+حl.af_ST +L"}ZiD JsMsҐ`Zy~=N2Z̀0\a5d Ҵ XR`9)N&~~rAP0,xV4qM]Qd[J:Cyw[\Ff+x>BG& kSN7^ +,b_L1 pq$b)~}S +-i_mUl4kUcKS Vu:zB ds}0^|<=~;fQ*ݶS  Ӵ! }āq|?;@G[@$W +ԌA@]g`'YG-u&YX TR%" ydQ䀡ST@.^u@Pd(޼StE*`<@ s7V1uKm1VlAֳdB^2- kզ㖏;V͏K(wͿz2?OWs +z\@7 +`l[LL<K +ZU8?Eedr|foO6ELGy&ep?q': +~y)Q[r Դ|LO`=_S)68ᏨSqD4FmR9np[nퟳ5B1svt +D65m8oWVJӲvߗ!fc,Ŋ'u} +_=(k]H\\%Y?'#흩UsW(ᷝDߗ?>u }Zn [~\epQI3Wω27ܡb7viZo`]}S<S+&k]&r}ٌXk?'o)uD掃"[k[u|ltzEDqQugo^7w&~5#WVA_G #j\<{9a FË_o%9S>X}d7mݛ`Z <*c`qPQ ̨h0"c^Z`1jca>gì޻^~.=#4*wu‡quvAyA.}T77?*M֦[8Kv9lKhMKRCIՈߎW/^!JF `Ғk[ѻn%wvޠAa; Vn-($+!^@̿G_|#w?Йy͎SKĩmQ,a.Ӂκ}{:X.uuK[gȴ+L'5զ1EJ +F +e3¨CSirY(5 +9m;ˇV7⯨b5/d3a4K'{S>-7.?ovѣ>ѵYV-{|.*T F4'!4wGo̝(s=1wwkm7[m%KGF7_egjY{e + hY<Lu0[ֹ>(k@n4k\ڮJ76¡kw)v׏Ы&rۻҥDj~GcݥomrwOWe@)hbthvZw +i +4coίCN~u2?p;LMsxO~ {x vF,,uB_-ȻƊ}F%VIБ[ogr?|ygWsZ'dlGkX(@NfaXR 3|ol<̚L-pݺW/sv#tw +8BYYeٙDŽ4ZyV7͎5xnVÊx5I_Ca}/jRW_Ej?A`vnZ"7ɢڨVկu~"t'ŎlgtfMW~sDʦUP4å7kj'j/.+8)l(7ϥeq' Ms"F(YajN<ݣczMࡥ+*T{/j1γiIMG{ESR/NR;Ե)6Qwx +TRƦIj鶑O"٭/ڝ|>GB8zzK0Vd=[57AUV>Z4Mv43g|[IDzit Fq^HOBydA}͔n gHdգhXTЎM>Mg_㟳. c EƃX7ҍ +@Św\:flOIn+y!Yj+U")&K'MlXM;VCh/8R?o${ܫ֒_Q6 7F+tĄ@,UJW3j:tlHUZj cϕ,&W4YE)O8ČXbv,J+kG wDf8d[<f[bС8}L6ؖGSʚM_5"u*z3ϗ5 +8yY#(4AdyO)$08 ޖc{p#eۂdn>pz@? s+i?-Ou}gGDaޗ:34%{RjS}&kT(43QlP,}йŭPZg~o?C"9O%PƶgUcD]~Ȝ{XR:ER ?`f>`$.nh5G!aa޲lOkIVUqbW-5F<m\/8bڱ~1=΀իCiC`W(U,iP•XFac:a1a} Jst.^\hX!ɲl VoZ?GY[A"ߨt)=(iO XܯL5uL%=\t(sP[)BʼnU'oeׂ k\2au5zk4j/~) @ߞ-wA$ղVI֏+Q:Sժ,TOT , +Ï)Jg:~&!9WiVx?;),v` NjlJ&J.V{l[caiȟ0B-Ώ|p E:ئ~ZVe~ 97Tp3|d(dr™,+ o6vJMyc#4ѵm+fOHZU=jׄUci.WZ+SY1m1x$Ss]s%zvOG TPjV;] Y74_quendp$GMxx٢|*=_۵CK_%kg*Z8` [?2 {P)_yA=G}dͧs[Tug%p<ݴx{|ZS|po{-;󎾶;-. ]ЧF-9uƌp|~5wg^yE%{Fz?ac9gL{޶Ӆx;NKm`ԯ:Z$6YIFO6x&}>`dڅdݔa4];-Ř8qs5e3jYU`^ۨ#ɏ쵱 <2}% 6S{ҏnYO_^+sO_ oObP} gSj2tOcdѽQ?_<><mD?l?;7jOh|kv|tl[o[Ԩgۨ1Qt3}YN'q+3?FވC\[y֗_uo*Uz|][w7!]x;~tܠx.6=[⇑PJs@شRi]n7QBw;dW~%yj>DȃprlNN|pt!9G:k&u}[]C=iz]h~&Z]^~~#tn<,j֑W=Qף{^*Q7Q2* M;b-X6~u0b~[XJ͸}ZJkk}(us7[{=19fݻ fF?JP}IEtuP][I3sC6Ǧ7s0뎿?#2ϼmxpޭWo|_]HέT{̌,9i~ sKf~׌t ]O=ys('>Z]Vhw>+ -WSݑFT}X ϻw~:~^ݶ\8Q>:!JxbiGʜajZgHQEA4 }T;_A=qTs>8ZJ iv*AAsE;x6J=Z=~YUup=ԃ-ө5W*%{s\L/=NsK^ HMdnTr>(WTTʏ֥-WO×苃wsщWGl}2P5ܨ{AON^z=n МKb,nED[}%:R>>õp-N˧.?Y/ ^3YiJײαۑzvMʡ4sƊ^3wϻWY8GwΚhnhe8WS!#tEn}&uRmp{Q y(-@6zk',0$۴L[zAm#s޾vJ5GZSO-gPK<COMeKmǯrDJ~^| -)aK KŲl%|uk".*v=L* @^eݮ UiZ&qά`g"0c.^hukv)]Q7y"Y*nbiJ8΢a2rBan.&[-6,i4?Lg-?ӉMz^`vۍ!t1!.i;bF?rrLiRsUDjԤvm8o+kZZ0k*bR(V(svAx‚Q=-kmu8YjMTVA>Dɟ+BNUL NX_׃ ".8+quqdܞNNoTun5QI%ioY<|к NGH皟ʶ7D&kϵuϽ;|F- Yz-_,^U0g>z*g4P4,,4?1%ñ^}Lj?#׌^v`%y>9=иr0Et[NO)ӵF!sI,RB.AIq44 'dkZO_U׸nԸdתJV-`vCBEկJ5|GR }r^/Ԙ@Yb9WJx+px51Pl &n%19|ֵ'5~f#hQfV2G*ˊq|+Ezf0TTܴ5|7+HM*1%Uܻ, w5_L])ODE8ftA[x1\X*6^{lߘ)?P}XU:̽Ѕ5T+w[)Q{ +;D9q\kR]l$_<,^0x6Y>n|'u4-RAsءOeJΠa?熻ZW\W6t,4P)-ZښerJn(!O+)7)Cv$t 8y"Z촛7)JJ=MP:< +XM'Z + \\: +vn9zC^dݰf:՞MEWҾDsez^=Аpn,M\nrF>M6̔T_'_-6ςDNKc6ZZ'(UIX75%L*j VHa8?Ao`*7RgGL kd4k<lcK [!< &" ցMKsm*~+ůA-eSdǟhUTahC;QC,ۈX[ؐ 2 E.ޤ0WL134 9Rx9!@.d~4X> i42f6YG~b*GɼRz)ITG"@~إp BV}"o,#$9 0V0^z faF>`0LO K;f4BJ~y)|jBwRbӬ};LfAupzwRq$xr8F + + c Y1.U@2"bqŊC37ܲVbO~b|FuI0J}E5E$ sG)sHrOP`<-QDFV$9!!X_iv{oWea.r?? x/D&4&Sl^' ャ{::8? 9Xʿ̵_Wwzy3Dž3{p)Ą\U*CRтgװaϠT>KGͼGDԔDb!_jlkbM_Q e!~iǘLӜ:Лh4:;h3Ocm04~X^tŒEGuAKf+ps ۝yӺ!{N5/R/Ƿ,N_{9A9Z6S7* zt?G[((6Ipi؋un|"m:x07}.Vn|xam~rSɢvZ6^})[rvv6l +vHIvmL'?OK ~ы3Zr>ٻ9mlҧdrfL9b&4G6`L G ճn搸&c:~w<>gwhsPnHrBb;;BRټ2ChL熘쯋?p3Z +M)P;Mب#{uȻbG0m[F^syw#w!˿Hɲ|uS.3H7R> M `86GW-V[yҫn*᫡e智YnI.r~5(Wmo5KۉW2K%,QNpmTuCeSs[OI{ +^VI 6H'gTFFkNN@msʁCC/F^Aѵptz\5n~7!i=62$YɪXdv= s׼/Ă7v1D]V>B:℔KIfk0CVguJ BjO6bU(0umFy(g_Vj +OI΢$gEg]h0GΌǷ8{+~ڜ8K}aGY_*).fI!PB. +9+f*׷q1b.P QxlS!gx!;8,i9F)Ǔ'Ŋ,:>NaCvs6_p)z[Y^#T!B;]tm/h&]Ƹkz?݂-c˕c000o^湑J\/He)Q9n (e=TT:6^Q~yZ0؆Ym; M Iln0d*V:yH 7~ Zg2ft}>ŮJT?܍"a Ky:oF~PaT̔^傱Lk}:T`qOs!!V6Q,-:4K PTSr,:06ߖՋuc]}We%[ZuɓTmW,4B>7XDyƛ=S['o +p_% X֗72l/y4"5Uu7GCHC\ ]-vʣo] ٜA 3){\յ̎UBi%ÉSMKO'$a +R;^sgۈ璓7<`RDzYtAtң̖JRE7RNN [ +*kJeeH=4Va[6'+z'fdw:)ە @Hj<c*5wՄLT0xp5 Ӑ:9{E{}Kmst0v,ӟmMXQ<'4VXߢsqw Vd/J^WE?,hC^,eڳ 3t1K8/g>e +px jYV;{Qb'lˎx0(W4?=XT7Xi,88"cOXJ, +#@ ^bY?7VʀHyzI̷^*>ӷhYªWRl+*=YtUZv&6"<@jXV>- Ec1XU@f, +IJ ɻ7@ +.Ha.5kw33SM+ґ Y!yM] }F Vn y<ddr7瀂P<r<=MKz P@5dƀj/@uraP50oM"+TZԉfIh'Qf|<^ _j} 6ob hs%= +eﻲE@6?+ ^IX` 0<=ً弰=ЛK|ȒN.yٝ(.k)=%O5KtN|IvܮPH3YPEPPLpFlA6H"~QXΩ6ᠬm^8k|*(fD1x{WD iī}WLgfRlyqpkqe?fR߮!-ǃ8h:Pĵ l$#<>#St3>O>:u si +trOVqr香M$^r#//:X)mx{XſYXI߅y@P;\bm\>U;֑3ԓl"!7\iy|:o,~\/ڍ-i,@V#OM=|jh6W_8OnFͦbǠ6?pWqˉeY;$!1)X7E%Tm|ZI@r3NM% -{MQŅ.vTVzȫs<~;enӱe Yrh>m”d|W9KO7ȤFL=f]ش;do'fZYe Dԑ:ămFs +U=זb˪|r3&nUP 8 T\K4;64t3.WcYeP{.\+[rymѻȕQPe˕_Z C{3`$ô{eTnSsvG+-w(SK5;) 3Ϝ^JN1e0* #]e"\CPY-Xhk1s襔bDcYGeLYPw9;,̱MP]FƬǫWݸ֚Cfj]>EU UP.ĘUXܥRq<ğ D9sIqE3ݞn4^~J^+\Odl h3gR)ˑe$0w~ZAF+sm sJyݭAیw U0ՖBM+w(j Y^yiR2hW#N1HPͶn $ia+SIՠ;,YV!8z5"T!fz +{ˌ/Lw҄!7J!swOxHׇr5Qev{lڦ6rҮBov3`{=2x/+W|^isyR2\ +aYA VANexYJ<{ Ze뇾]jF*NaT˾)bD"̞z= )uY +Ld_RaYP{lfp}Qc&ƛO_zcYo4UR"àII"R+is<~`mNYW3+Ck(n_ĞbO/T'Rgrw a_"]HRQ4ʅ ݲXGxHiS =(OE<Јn R~]sOr#.- wJƃoX#C}uT|ܔΜIfy=O{W)ءcYp>Uq*¹R,oc ujl[m$dqmJPvc& xv;F=_'OjH֣z~O\Y] g#(].[b_M +fVlؐ7ZPV}d[uq 6A< +:4Iyr:y󆂠y.Z7ihA镐ᰔThkqg|` 9xS-IR|&I{N5<m^čx >`.UA͑G/o¹ #Xsu\fo9r8֓ι ӣh&0hV /sxP~=M6~.N>AL9g!',kP~r^0Ht;.BZjvP |gsKhRiyi ~_EO]v[ᓴ]. &_s( ۼOoR;j[:CzlKųe}A2%2M_˥MN35G@SJ .ʮ$B-YҤzbpeT|?܅? zݼ1}:0]o:!ܰ2=vNO j!B:5T.'D,tW|(C ХJt4m V3 ty$=g)OUٸTcق1UUkzk"D0ezF!u>0<t!<X{H^J X#M#$˻ +0=X?zYf8zL[̓5RflE SX_+p)?T Սqj,uNO@^Mo kk./hlLQ6..M%V[Y=H{V]oc+pۄ&4q֧/̰`x:B*(0: +:`otnP:#:ID74if:@tIkdﺾ%Vn%iꖄ& "PnX}t5X`W(~Zܨk>'Ii`zw':uzm߮innOu2(^^Je2ʝAO,^hÓW7GôcM)}h 6# Hu2r+ +Hr߀1閕&$o2ex5}`{%o ]!ރ:!/W- 98ULb~n:czުMT>V4#f}2V;R}om֫b=Ԩ=7c?X_;Dx:WA#<zWN=R[nlTE-V^\"cW57 %w}_4~FE:~Z*XE%pknEpmWh&GX֟8;]1+1Š +F3՝.g/-7 ?NΏ,)̔mUa~ѳvYX<1iUuwjcU+'J)[+W3ggLqeVQ7e:Ejz"G'˻mtFˉX钇RC#gsGЎ\Ɔz:R- [E[%neP^Jz9TO㲎ЦAK`sEӼBu>_y['Ba:_:+M%R㱫_&}[Rrh~e[hExeQFC] sZ7x1"iK\H4D^X!Ut]Ԓ&5{\CVcq8^CB8 +p~`4<éQ>w*Q=嚡j^*ҭs6y\N宕pʠ!ח|i&.FDjߝ1gK']%ZJS|7b4gz0h4W4߱Dw\'Moi˓E *p)DCVc\KP]^ʭx:6'^kzz3>VmY|\_Pa-#7 )Lq9('ٜyeNھ\^l*JOP)ExP.]k_\1=Gpq⛢T/总ަ36kvpYi>c(Cu6S)/dZn1SԕFcTK֊8ƅl۸A[B_ +!6Dm哎@Τ6 ta׻l6^3T'E%e/ {,_Ez2IBhDHxg(d6 ^onUU|䊎A";)Rk7qU +ot2$\ q]L%vw,~Qa-=@Bz'#ڹGjWR5wgy]V1,mtXhL:7!AUR2mꪗwJ7c}VVeSLZX@tt xŗ;, 6ڍ.E!j3HDw<s;.i9&NGas֧X7uC,OIl~,Te,J+jKZX>+))Cw90 endstream endobj 39 0 obj <>stream +oYL,u&Fke_.>]Cl$K- 7#okYZ6>tMZI@Vyœ{~M,0J'|bXr,}M gP`xE⫩2ED, n\ww])',yr__:㿙 SwdLXoPX5P-(]Qx&P9[raS3-1B{pss[Bm#no͂Jm6@x7@D2ʝ8) .6)/tɶM8j'_:Z!oO)<~ +*{ϸ8LkTQW;*^Ǡtn~4+ǛnkG~sz x>zF#/e}I,l&O +MR~f1δG׈aË{,i= ^#ڏ 5z[^>"/z:y7 7=ssiJZPl}^~j 1N߷^E.IQ. +9㼙,tBiT ÅӨCޑCnA 1g1Ŵ5a? {ـ#Ylpu5Nm<V\ٲu ށpGxOh繙T3sX sm|xl|Iݻ4\m!Q?j3mkOlicYzpڎ +cb9]w|Q4ZQ4ˢ_ ͔P06jyX\6ծszcM"=mmlBVZ>gp3[r= eجB*^諧q)ͣ"?FXX6XNdim7 R,yנٜAZ>esݫ1Vee:33PPml22fe· !O69H//uBeA; ]m&iVLU}eXYK#7^BӖ,]}]~Bĩ{$YS%jmkLPyݥoZHìgu7.#ӆ[Bt̝rscޖ#={MNZBfQU)/JTdqsX`ٛ_˦3{g:2Dz04re[xv˳lo2y{煲cy[ی:5X jP۲gnMvDTj+{+]|_[v@ƥ--$fm\x=ƳRjUZ'[]yVEV9[ow^$9/2mŹ=R5mx^͛ki;6cY5+XU#;5ۊɃ3S-(!i!f^JX5R}/ݷ&W%<{ +&+sF;ntOm0Ի'h)=t{7N>=};T4o=?qH ʃ823D2 +kND|7Z +v/PoF7_v?on1N m k:Ӟ|=kU&P$2dyux пDA(ms]wR7/s9]Xl3j,<uم1g+^r +.᷸1⹢1JN6уo7x9GA~_#*>6<<Z#|M%aPQX1ڄ:'NaIYfzt41̪B|_wǟu\gϓKUˈAD?ؖ3҅E:N@DW=Z[ +k1Q{hmء\j/G~GX0vYk,{ii\,aUj|S +W]eAl˖˴v4(zhMș5Xj}G\@8qXLK:ZϮ}kFРzA`Ifj CIP]$#7i7'7uqV,ϖ(ψeaG8V6hp.]2[iY&(e70,5*Ka i'KP*RMso$Μ\HKC0m;XgX<cٙe`R[Vl}i0USDN$ +eBO{iw +݆c}AHf0__`8 ز)2n Xg>aiMbI/$s0z{&FER6V({Q^? /@֠\%s%᜼91ðBgcv]ROf EŲ7q͖yud@ @Dʹt(":[5^W7pyAgJ^1B90"[GyFnu&Pi@m nK   ȫG:fyZsuAeB1ndi><4'/wœ(rR_- ,M,P۰4~Vjy 4{N)y@SO5x+ N>tN_9?''`;ͶwfCnɒl`́V?d`|V!0A{ L- o338oJHW{>dӳw}3jqeFԊxQCgKY)_~ԗstvՉ6$:W+q}{wP$59ӟ̤WZ_LPЅY{fmuŸ َOg?;vP~r?bn*6w[['[2OPæQ_DSJ̛c{9x[<&ք&8<턞=sxfZ.Zwusio'<ܽ3cOsznUq] a}4ջˁ~=^֋Bz#gfi\p9VZM/MW+e\3~,QHDsghL6Y4ylDާs=KIEcl{UmymqGz٬O荦t~WjHNLGl'\gt_ȰIA}>V>Nګ\aѦWu+dһ֚4΍ҟz!LTO2{ +-Rŧ_Hڏ0vFHnDj PEIy?8iͼֶ3-n4an(~?j/ja3 +;XYةLa]6'anF!nۻ"[t +Sܴe]=v }8M'rN?Ϡ{z2,w0W!~ZP|{*ϽT|R_H6b;1}SnTr%$?jsYs6 \6jQm_5J*:[Uh\N2)>oxEBb$ L}PȍM%FvZYt?Q\ 8Џ6Zwx|fI$$$4bi:)f%pBǚѥ07ʁY#uv0Ϩ߄"`3JKo;ū9c=OYW@չ-cPI`J>eK{kܽIğ|Y&<9 +_Zid$H赛޶q:;W{GnmN.U6SnYpM&408ħ뫱AIJ<ʼnH2U +sKh'7)SKՑ[{AeWsm/#ۦŹZk㭽9[gӗ/#j7axNi=ϊiGU=D5 NQ]QQ@:$N9qS'; ^r™8x{Go$Abf0mr _ ]a=ڗZvȰo[͉AW;m'ޮ?&ZGTBTkWMsU5oL$o4d4W(Ӽ=A#yCR}"UsR̰vH4ێm07]&;*ꥴv{UzT=ͥ*7G¿"܎_ˬT'=yW.P/GRQt_YaDEO]e!*YecYvt5ka1kGd-OSUrm`I^̈3,[z*,nK '_BSnW;8: b.QK]MmP8 +]l8U.Yaeh!*1}Ai%INox]mI/ؙ,fDׄv,GJ_qt0bl%bxOw?;. +,>L"|1v.q_aCݹw*Pvb᫠xzV-:&댡tG]YN8'6CJXw 5j(-.-'BxT.Q]YV gًBp"XEF8tV*Dχ1.BXOP/3?ܱ 'jE)/R2*>ׯq-vFucOS-I?BYZ=R7%asRRyƯB?#t8ŋvҗ6AO +P3W¶EuQK +g} l:G`iv 0~xN0ׂۯ3tE\e]x=`82t1d~nxo5P_s Uc:wkW kT!ovRtfj +w'.;OJdJomI#m`3_ k~OSz4qRn؜'zEت+ٮ.oT9 %FNM_)8%d'uu!Y%rX=|=Wx5;xmPI[4he$؊R^ʳ<Иq<5mUf,!ƈgP)AX4!:  bPVc@. Z mbBOak@65Ļ;?_8紵fʟ('iVy+ +lzc hC|7 ) 3~cWhg<:гX@ҀLZU%~ʣlnRMrlV;\{/&x~[nRbF_%^Ax +~d8yi. w  b?""U}/t8\Kt7,}nnn,OK i+7S,gXnZT捊-^sC{u0 ˓(eʃE@5=Pէhi\NMQX*@8hcpzB PKO_7kii H{}wUD}!53_4W8@Э9i:s~6jAO/50,cVX 8 T}>8MmnVa&꣔JDPGE`&TSxN~W&f`F/;/VA-Vƀ7=X(`/Ʒ[vc;Ӄ-ـ}7Vs2anv~yk9c*-!QߴZF\Ԅ5%CRSH"+Zm _  fs{)JyxD{߀r,%']w,DXC؎Ss0=:Aj'1m%h+Fc 3gȣJ3@`WdcX,UgG^Y`j*KKNWڿ\)hhQj~|WjYs/! '%L}e_'*?Ö7$;d`r'SORe'ᛠd#|;83$jY`10Sׇ ,.թ_ +mh}J޷sYnqASq|mF6jz +JMad}}6 +Ձp9gP*3ky-stODyx1n*ܭA/d0Mӑȑ)r//.:Q@>C{1}4{37:Ṽ/ õ2/V#_Hod 4]zv;y +4ʶ:&6\w3I#S ʥ9԰3Wq6Y9a|?[ߺmע̌ьmBjmKGlS0FS [s`I9(B*RSAJ'mki V~M'rX=t3Syj{=5m~`N'p]`SO,i8$!b8hj.M/lA}>nOAfUw,ҡn39vU$-cK'5f5gC>.%#F4$}f=ÿHhi}~H(}Pjޡ{~s q]wUXl9>Eef۴F{2D͔קA6seH|2y|֫6&31E<&npQd,# FM_kFbF^S{h7׶F Y=̢*w\i$v'w*Ϻ;!/i#j#RRY-t͢zZ<(tf5oR6QK)@\T)}IVM7`x"7|5/bb&BF]d1փP:cZT)֕ 7w}-&U:Ֆ@_|i)J=o`W*uv_YAL:32W b)$=5#v;NcMBw(jYJgOWTՔz|!TI)H%ȦӤlKNkd^T]x$sSg͕s ?!H# ToL'R Hw9h'sʮ7E s |AU Sghxgh8}4=qhB۷DyQX- M +toJT(%D[ ܃gL΅CGFR3W_a-Cڟ"`txLܔ ^wT`h&J2f-P!SGxj_}*xX^Ϟb.5-tp:a=>_o+.nL+|#Ѧ,`bTd8$brDv~6s-z +n3zT:L*sw׉2R{.,޳(fBsQsIVg_b&/9%bO K* B\!Aw+5+9ao_w /jmzq]@EƮדڂ?,I&[(j2@kPb(eP{O׆7qZU[գ*;Y!Z{@"Uf+77mw*]\=˳t;DSCiV;XA Yn>>.@'<S% +S@kkuDdmfz.85G^⼰ώ =eG/nrՃD=3ioP=4奇Ľ0MR)r񗱜s$0uRVbC|az` J*WIޑ@sԛ7"U\ۜ|Cf_3lX/#߾܂biO"K!~ú#k؛{1d<]RXGmf,"fDeU7% v'+|KYWsRfOPSW=w15 ki-+Xywz,it|b~>'x= Ҟp(HVNk-FxE[䪗"NyF!%a*x< c<.  7Qꀨ2(A,V,) # 3RQD"?a6/]@H/cN_}c+^kjG~76eVe3,5EQHI&v.._)\Q<_q?2 M`~ۂ sE,+ .ODȢ1G@jH_J鹰qׂ}]O I||#' "Z_xIܶH@,LWKy5[ǀvjٶTJT*-룀>Pqmmf04;+ Jgyֲ72WH44ZQJo74_ (eMv0-cH;&s +`0s W S +$z?=vw$;T/;KC@ǣoU*BgG=됹f[V`aM]׷[70y]lQRv#[2t`)(WUY}*iT?q Z{Rls7M"3Xt}q>)jyAi>E't$V4_HMhmC }I2bۻ0|\')>xx͍0bq+exi.IP/dJ ℞qQ*&Z.2(=Smw}7D9^ M[V(ٛ$an@$!:X=49,ΡԛZaB6ꎹa<<@?Qߺ0v=h~Jt j$jmK,Tfcp׬ٰz2-&2}T0/\Z'84霔xCݽOsA3EGM<~*+wsփfƹNʕh;:ک=Pf}>̪쳬W S*T+qwx/L/rEd ϕ%$׀П_U5Zj\ L} "&mBqJJ ^Tx|fɃs6:FyLVN+B֗CBuTSJo8k'8 L=fn%5ZM<VT^qT91|?OdӹQsڗ>Yt؏WU3dU̟J= *ya_c?,iqxPg ~n&Q'zJ( N bIYew;|wbj[@OuZ2׉|]|<>񄣖r8& f6͢IU-]=˦EJHtG*zKQwHw_$Ó ^Cke+f9F +#KeF39Xw 2~D\϶N]Jlqdxg8dL#!'3b:`PTOT%훔:gٞœol^ 8B1Jcwð†~!LC yiZjkpY\q +8:hy+w9gA!^Г[/ +>vHi!#o>7W'V҃=̆zeWZ [bFn^xu!1f]]CV9EYp+en-B;9?@@HJ2||ok#ZPGlݛXih@_ؒO"-2轹DX=:w[Q/&`Idt|s]7TT>t!X+eprVR`nHmҝDHmM)- ޕH:k[~/vW}CuA!CY֩ەQo]6DXY3 s[7ܧ޳%'4@ng wQ(ܶbyd[\"@1%.wr$gEM;[VB{42NV-Ggp\6ݭ'j$"7xa]:pYA1SZ9{ry}h,Ųw:6K̾x_=:9v!?qhrSηthg.'!2/\Y F΅L=,ζ9+W)f9@`0Y' `X \{S3i ` #⛓^5T٤`("K\\待GRB &zsWo,Y;ll#GQٌ^8oq8N +>Aw kضYH<˙ +vP_ϙ$|%dO-PjX>\ϸ`!t<2Y^vo/lR'TV|yUk~ϱ0?lG8;K|odeMnT(muGz  +w}qeXk|Yž}QG0@,J ^GoƏ%rcb< 24 .q˓ ^ scGPy͌Vo`Mvbw K +TKN[tKW@ + 'xՊzG;wˡKnyOr h~&IO);}nZB&+ x'tcnpp pK. rnsRk] +äs?V_I6bD2&#TauͶ~X*@̾@!|K k LTgfaN +"9S@Zw gpCNz H@eam i\ RyJ'jIXWOFiTbOW'6_ӚVNwQ3q׵xI}.9u#LkYmuuM/ú>Mĺ+"t `-"(((sgN/7UIꆃE:k_wtz+K +xU%v4ǻs\XsvT4IX8d^J)mՔhYMZ@گt^Z:#]RB l6i9!W `;i;j':vmԻ/G۲sB7\5rُiL]](}(i;+nYFnskDiy֥'0bAD[j |# +}_(CI#kj5@79Uj K-8%˯[(ռ6[I]g]rjM2eMS"dJKJP ? +N^xЂs+(!,^,4~SmmtH ]G.Qr+#;G%(VwRQCQ8a]\Ae3ǕC ׮݅;΂`;ToKEf@(gF=U|T]}yuFjqv7p5r-*2֧Oe7H-nMvy9ៅ|[*gKlz|FB] 5sVo[Z7/jk@W9;P}_4(4CjPT\{mWvۖ"&荒[ڬңugz+>|[WdcUSZ$ ^I"Q$ú*uH)LE;=;!.L!cwЊ&A@~'[lb :iOZ5CXjIi:<Ԋ-ߴʻoQX J6씤fŠ_d#uXc ekg;߫>OV0-Ax: +IGާH\ 58et?L#rVۉb1* +j_*$5ATSUϵ25de i2aJ:-4_M9t/E^*s@@PoS?+yr$q9]V:c*e|-c.3̠\m:J ޔwvCۛm=Zf*ǡwnbj'^nu +M 9jAL^B fIU̠8d8N UH 5/{`3)-&STFVtp64jqwVeam$vP2Q#2a= +~K Z-bMy{8WJ'їK鈾OI9= Wg뒌$*|N nsT[yUV8*vnLqQ`cIy[ a8.tzt7PU˸rIZ3/pH#b['^}$ϫroڌ2,w^ a\8 +NHn ֦bv8rf$cM#nuͥu+(G^JTWYEwd@Xd>wۮ#K7Wƪ]~)ہFXz|Skn/֩pT#uiv}ܴAch3~xe£EFFq& /jird' bǵ5>Gݭ]LvWP?T/.9ԣJ7v*)xԹhZ06h/bnl9Y3`k&OXѻDw}@-[ +/L1Ո,Ǐ\Mw^d9BXxnqs"I2FDZXG K-Ncu\ኾm z:3ϣi:$08‹C n#4(bzc#XE'ƣNYKTႸ#N$($͞!he6(_6|LaoI K4UfPS{fb6̪[P~N\ k-1BgszYfWIuFdqE2;b\6;:9e(le ddڨj|)'sjNK[/N^v@E%f* +FLM=q9Dt3%>hvvX Tm}nHS-<@| Zh7hź:fMo -?1nV-\(o4uD@ يI*FJjѕќ05)* Ƕ>Uֱ\raUHg u:@(5!o*f"tʅT3#֓[#мQItb P(Y_DHƭ /S^<'pK]CP˾% X~ɣ eU؍%VrXXhKlU(ǧ(DYẛ#& }OrfTlo ]GZ~pWޙ"sMR ~ #rpm(U^Edžs8w91`[Vf=,uJ[Κ?a:Ak_sܕ`S褺5 k4^&w%G%K֓'3K7 p5Aܘ <+?4Ǯ p ֌<id:rc3JBǩ ֓Yjl 2t<4VsI3 s "R6__1->S^Jgpq!|:O?p]6yDϏm;Z!nb jg;~vC,|L' "$vAzA߁ȰÀHaRdu "*Dl^mVnv8K%:LYfG}sp;Ghdab|驪_bm-MGtiHxߦ@yk*&< ! @Yr軔0y=q.NP$YWSQA@"@H@/X:9UCaj/*x Yq~(O\]aʽ9H) (`GPPt1X(|xʞ6|/r,\__l"CtA#$ylGNx/{r@>??#Xkro%hҬ];:5MGh@#2+S'=DQP842}>L.(Dqx7 4qnY`g`\lbbb(3?#ڀ%``R`8( Kjz=jX5yIEQcfwuWrJ .@, ,Zπcr~UU> b^Nu Ĕ/!aw$7,ttvXgx'1Z>3ߺ?kFWg͆t۠@VH,H E( A ~H_Awe@ NU/OѩփVI>!jI;&h4bgjY(S, %c`Vkg?>6!K=H-HUjEB&HKN笘PԎ7g.Bɏ/zre&0ooPRC,kQF6@0A N?} 4鹛vHU;EJ_}m?+ھ"J_o S <'zTDRvQڀ<2@ޢlP^QPȈ9!ȷ/1E/* Gukmxi?rda-hUQ/w-:ߌӻ3z*k]9TQlwRbKEV¶U.zg픽oC-4@} zsuɺM^ aΥ޹8CV| 9t܎ӽ4ܾ]aSM/uKjY4ՄU/ɂR|.!Г]wQ?P}Dzqk=F'U.m5Z7{e칦5*yho҃|*DDX^l| rL2 :E:#t"cNs{=7AT"w[(eȒ/?Z#+D +%BWjhu-8 0[ϘN㶑&N HI3g3d%G;7O}:͒kg$h.PʴE+Y)l}yyx^%c-d 4n&bQ| +&38xTq|ƿT; P;G'ߴ =E, vH˒LDm꺎 +rc_O$3fn%A@x<N + EcqtAVT +XR쑚o l?oI>Gz"n##&*}#d7GaC17Cy] ٬pˊt/A +\m $>ho *tGd# gM//DK^/cl4:o<J95)\': >v2:Q2;lzHvaGej*(_aZ@V5E@1: V# yYҞ)_N_lnm{&jš1P^<ʳw-5HS} Dg s0rNkpvA%1:H@(AD'@Su +h܄pKMfRoeA#0r$T6).eH$%vW[5NEHAN}Tm$Vp13}khhs5#*E x`;z^~؉+,ab谌U-u"-wu 8 +szᆪDKREeQE& QNtI(N-zmnW}> `YEXwۥ Bب?".4JJ`l 0fkW+7{<͛HXm^>\|{CHX%qO#䕟bn1>0n Ӹ\b `7E8Nx8<#xrSVD)aRtGnFK>) `F_ʲ.o $*do")׎ /xQrQ'ȹ<]lǃB'AnKNtѣ@ 6$o3PKU&AȳF/U$"Lc: $=A4*^8/)p1* 3qHDl|ikw<6u +XSTNMoo=C)#reBP WRǡp*١y;+4@ + [}:ĶޱE3 BmᴃH5\oE@Lʟe&>}N~$Ө`+, 'U$ D +QЯ&3T/'. >|1xj~ x.6w376=f?E$ޅw1`GyF7rd]$sdif!_r(A;ہ">ootN\~KF6`%`6 bsإĚ$bT%A>X7SPcXya0 j~ISm?K|Iemb- sW}x>ֆSNǀko/MRj[2F׉;Arg Gm@n"1f' egK]^WUS$5ou+|S?6wݾ !Gvq"S +aӕHyn[ew/Ce^æЭM; fv2kT=ۅcEqޕY㷝Qn߫x]Y +洞wG-(n;j!Gqը~%j!g_ZٻG +.sɑA/ɕ\":r= 'F|BH)=o{ ןJq#Íhrm#)͍EތGOEy"! JWyQyѬѪF}L ;JZۭ^lR;~^)Y ,X.&SLJb)YJ|”z|.%Me_f&7&>kg gp|-;^U57xxGluZBE|U񼶝Y p.k*xtiݟ跴&mQ_*bk%ZCC'GVnQwI_&O1{~mfU[Q["Hq -w%}^dyI kU;W֊DU܉Iuʷjf뫶h.-^$FOZAS?Q؆ɇu:\N>y>[>zVݚ_T|v:MZ]&K`eA!6:LnІ_<)[d1'UHshɐcfu߶*]8da+-)Ⱥpe2B,/ik|yY->೫­-{a.1ҵ:RtzlZ}T˾S2>ťګE!JMҎa?:곇$t{ক48#4eu78YܪM~"}j<~:3|M8a"|4K&+/Vz1Bm~oR${EVsl7=lĚ^{[er^IE !'>\<򹅾wga 08vZFk}RݦR8 {wy>gmuʶk1kPލoaq/Cϵ#?Nxv[y&VњiLQl-y׈CԬj@>p=g/LP"\Y ao;9tE^sp'y53ۙQߦvBzj, MIwHQ٧k#d~B~2Ǎ:ŕ%kV+/)DFjzʔ:>V +7ΔFeetAnxWĠw嘯Mw-W"5|teOV993 +TL쳔 7*?ܰRZ*FFC݈{Um15?&'~fDP3{pdQ˧n-WuiM.G8ѵȺYF/J߱}d|(%4a?DZ~OJ DMyWz#UOm]Ń+qws}ӱayhTsV<şZ˞#{ tx9z 9םzr^F$)6.Z8I]dnב"et츯GxObd9țZ mKl特ovΒQ./=6ڷ4'*^Kzu+Û~M[,̡w<\OU;NɌPҤ3Pj:歾n.'glwwt7؟#Va2l/loɶ)+Aeao^l.[M %K| XUׂ}9A쪳 $lkE7ٖ*O 6ϏoGFQsJ:ҡWVT麊Ib:J>xјj C +9:CfPD!(ٻBN6uΜr1&Cg65=/Xܮ%/Y2ά]8ަ=O֟G5>N7<Cj;_T }RX5U9?k%DYMVRsf4߉'h01${OME{gׄ0px4h|ފ G,5Ug:SC1a<:c)Ur+h?zqku.¾;gj_x/";Yk)≹R-4~!#7 5 7)W[h^R?*a,jP+u +U +^5 VzE]w8Y[#'/*J7`yt8{\mԬ!J RF|L~W y^*٫]gn/EϑVG!{Y"#1>=q: ^afP+WŠqv}݈{׵gvq7`(ѿ 0ʽ窟ʋkka:@㧱ۗ .Aq7cb[ծ݄}t܇ 1!GG}f>s9}ՆCR< t0OT.=sVp|.\f4ebvj';O]v7 _ +|GFE?A32џ~O_w'>pO&d?lr-$|42O?o%>~W|g#p +q{ͦo ydgw1OR.X2[E}G_K-Gw٧Ȱ|cjOO QO&xa?.Uk{epsSv#8V2m<(VvbQO5f-;`"KUEs)d Kۏ% EHl~y<)okw=w:Uzm>OIeWVa1ՈM Xȿf6"XYV4;vʏymrهcD¶6 { y5>VTfj|oJI =?\tZB? Ѭv ;ӎȏٵ仯mxi)5CkvX"׃ ; ƺnh 媲XW )D s._i`7-=]u/݈Jmt\G'Wikk7G;zR 6Hby._g›uEm[/YP_hM:Mc)尿 +vm3Mb=}U)89,TvŸFf`ZhASs4beO@S!:sWb3!PF7(5.񧂟Ts%}Δ7qVXijQ[誻jnR^ @YpU~s+zm 峏l8svZ2w4EUƆC4Lgvov-i5ueoD nrRI.y!xk60vTV]4g'>مWO#fOY$wrNu({[.1/}%5)]m',B5?ک><;y5'v^FrvUoܚ<ËzìvKP/È9q&՛?\z+ٷҔ{D7~.rNy/|>>n[;W$NNƊEYܫ"jKy6rf'x&ߞWp0> +.?8_GȬ +b +[꒎5R!-G(3+盕Ƽ*kwFh@~JͿ݁/DG>5Q v]p/_)9#ݓVEQS^!7MKCclkǗ)U$g/LbNykUY)dV~,߇44?QvJzܾ̖Yg݁Y}nKJd]jg5N)r/2#T'ۺJFSˍyl܀}!GGXcn'UǍh I+rt'YEL][dN\/\=3J|UM)^̌xZ=oO !엉+b'!'I|@alT{= a8p-1M?@ϔ/w\ؙ9m#k.<{zz*CʞOnNbite7o\OgV{f0g+l1 HK{ +R9gR(8{%[w kZ;GJp"r H\,{X9[ÓcfgI1ȱ #yX (D1잟%4[$^ qZHYۣ6..U{¬j\ b]<&/4[ [J'l;ژ,:/cD)l [eTfn,~un4fMh&n%\≎=9; fTtM[@׵@"E4bHgRʘHk{<0T?{l۱o#47^;}'lzQvpm| Veّ(z4!gR|>o|=H?όݯ|&>O>{a][ uIκHG`SSWo_4[IdhqfƩ\q2sHakΪxaB_gU8jD]6p-_تj ځvB2ǽ[eGYy;23OO9%ÙI`>xq/2$lO}.l_7w_]8nz}҅sIud[}z{@9B&'>{ZggHDT f3$trɽœ&R>E*NJ5]Ũs4 hq$!Y%Z_"xnsPΠ6~uAcDv:&Xyjt7ډB^^fؼ2[pyP'85Ap6Oniz:g|AROB'PrR׋\~ڵ%~rۅ_\3`cT, :t'MtlB~5'zDrCI/x.ѬVw[[uo2%Ubwrڃ}lcjerg@8T^6wCRd_H#&WTS*Y~I(uPYKP+w`F?T~Tߊ 'Qƪp FcwrLdN(6ȁ]u DzdYCp9m{VPbVͮYJr~ED)s[Vz{e/~"Y4gǶqzALHr~}!ϓP&>>klȅ%@^5ANa\/{Q[I`!RlU%=* +Џ D 8,E[-島֪/TVNWB 9pSso<34۴OX5_={I1'|jX<j4JDV-b=1; џvbG4i}s[\nWp/ūQd:,9OLߋ.{mauDekvW}/0LF ATK/XW)!{Ƀy7=H)WsJF}*FV!ed4퍻4n.dӋfz_53͞ ΂rl^x/rXPdZ& Ygρݣ9C]ڶ>)7=W$2_(\{˜R4 + yӆr[<p/ixQPq,D<TTETlLlD'Q/{$z adOŇ*eeGYkOŤц5?U4+/~;[3v{g_oE+DtDsr4Gt9K|vT/ YBJ~Y4C|/#1DދMc9@ *MRgc[83Em3 v {\(Xx[)s{zSp֛lYևWhwHz.z-2Ӡ$S5}GI'^.IK4b/ȃp.׽_/NਦeǛUylXa-/Q2 TuyˉpʝQ!LۮıϨy(.сʴC~ASf=}|'Kk241T脌Sp@f}hw):sՌtufc=VYnLhԎIwRυx? ǗN۳xjKTs6n!C"3Ӳ:˲|m!̸=n#-s[Pۑn&ž.n#9}b8}չmGpBqe4[mmڝ3|R +K- [-#9 +Yͪ}q ,t7g$kiM9hNDV_mÃqox+羹\׹x,kCE=ڕ-5 +2'c,@r$h 7NvmӔIH4xZ O4>"@kn_^ۣ'ke+hlI, ķLtlJ/*_vmVfFTL2$]0Dp7^}aL_ +Yn, 3_a48?$/Q!$w\v9mS2ǧc(L:K:i*)171K:f b7=XTdӺЌM5DSnWtz`0$yMVe}%빾JIKҹǽcxZq:+ rt]ktM.ԙ]:(~w26b7P ax6}q޹Q|Ga̵i'ןQq,r̗]^cq'o0c]|:9T:)?ϓ`DWqy}Ts(O5hA9O0y(B*LzκpPOˌOOX<.d#\$3ꠠC}DS\Z1Œ{ )VU=77 ¦r5(0N3=CdvKSI()IX]FDJ4gHs5l!r;"@QP;ykVl|~g#Z9=rn%Ki--J#_r[̓s&e%ۛ}RIS +^ރZSƎQ sF9<+^_o>r=0@szT~σlDТk`^>Ң +6>c<5j3N (cGIb@x5pR^So٣uyY#kB移J$6o_]_%冟=OdԶ@t;1țZTެV#t-ݽ,w\vT"K#ĸ+Fps87e]by~Ob1'^jҸp$Ďo$5.Ŕsy%Ĥ9aDLbVL={X>>* PI[q}7A_c}%+_O>Nݭ~:\jҝg"r?.u^k[[[=6g= ZT/}j'^AٮnlTxTF T#TZ̧R3qɷcG㚂b?A\JQBWYCjL`Q~^!L}=G|A[?kh&oE-h~Awk~AGO +4AtvxTaTk2tq,ۍl6FVz;dvFF;2a~L(zG#ҧYw$b߇ ,%JbI5ܒPNHTsM5"goc$$Ѱ[1؀^fot e-4̼v ituVs鋥 -TL6eDB$T$[X-?ЏB1=a|ySu[m.delp7S<摩~ &S.i=@4A,IHd=Gmi.>'‚tu,GҰXOߚdBqiT4X3@YDKO!J.ȵs؟V0jE?s ܴ@$++îU?fsu;ˁ*n}hn!WO}~ZHFMjĖ(6'ZT8|c0q49f5M7{XN#"`mA=Y =$-H"-G4-?? +mQ&wy +Rf{.^vKZwfct.SGuOU(U֪ʘKdrC21ڴ+|O|Ai;h,&ShlE^ugqU;Nen5=bi(-+z8v>S;^.rz񵑐]xm~gQj +kdpvc_XRު)ێ~a;^zv?ڮ({*JSL3ĨWlSf}rʆms^-r$K&|~5ȇJclQl*n`g3OJ(%wsŅv%R߃J/ n9 |d [ k¹~xܕ*Иp\U>C솶,W8|,_ӕZIrx>&Wedѷj 'xp9u/\>0fgBꤠo>MݿQP#nb}(X[YK j ;˄SD4W ppo{rf3nĕ +odhNa_2CP)蔯e )&wK٧녇L-Ōt.+es{5f\8_.KWz^D!0OPҷeX8WҿDGIAP \'=]&zgb[ +~I7k.)tb8~O[=5ys Vq)׾܎ѹ+j' Ѓ S ASznCs? KC{vIDhiI{^VDJ>܎sQs ҙЭKGU@7kw_x&؜-g@xwQ=R\zy +wIM c0x,X^ lw)2ޡ~A߇~3UottϪjziI6J rõRIC2!l$?\7EnYmCRg^E1nENpoP]EsG?@^< 4 *|ә#%X4aVo|Z1y`E |A'=ɟN;jg%o ,Ogm[Q +g"_zSMTC]=;'ާ`Ed.+MV5kZ>4jJM-~bv3NG^Rzr*\ ׉u/R:U5B<)(MV76]dCц- 3e6o.cӋ:f53NU~g[nxoܕVd;.N׽H C.qxn.o.$ nV՛zx5̽.\NoAp +ɶu"$SW1Gs1j|vu|Yq;99nȭ y=WJ_[k9l 4h$ k? NZkw^K.b?&&>ڣP16bvׅ?j.Мם=1{/p6bJʹᇻ{ j22B2WHJS>(+@HȌ'gH$G%e&?t5WYlf%vЍyH1l$bjJA(llM݅;Vu{ Iy@u + Mi%J*cV]Yi3xEί37;_Ϊw\GuD?NOK%;k?@xP?}O!FUl0(P7ݨ+9Ý[БS vZޮܱK"umuP%^;|A +Jj%m[ {lpYK1qB?F.A w ?^$sଙ`k\6LWi p^Li劅~CkZqN)]ND@GT9 {r߳T)򓎳eV,aMr YKӎBjhs++Ⳳּ~w{U%$.R5 +9rbda7x"n_Μ*v,sH.V`|:Y}d뎬3kj\AFKݽ[!"__@5`h,/y 6U +ԟ>gW) +?"efT. a*,j%{MX0MOx'35HU4y]h+k2뱶*,yOzK(L}X7KSZB+E6׿'%󗨼\lU.P ]ʞ,;< |4]RDjd&׿)l*^6ٽ0;NIĖ1)FJʫ"zyuH I_+V'_[+}~1sT [ie24Q'H%}nv&كc5(Y*'ECD%y˨F E v?T \Oc_[c0\TDu8 +hr& +*EgqYmQG{cˑbtӺSJi# מs+v|\rșgMdİiҜ_-n-]T iҙ!Vo%qc a]͢ՎSR. |>G3|4O$//ѕ+JD爘 ͚e71ί+!7~Gaָzڥj|m3s6ܝML*稑A* :CM8`^wQaX)@YZ;ޏɄN /WwBziibnSav/P:mfPhϕCSp q0|.Tck{/89ɒj6 +P61~{ݤ++G#cj4IdYy丆}I AD+{ld͛~՝;zvQޕD~W>EfV/Z&KP`~W#T vSF7o\G5FQ\]zB.EHzC+cLXCTg_3tH;d޺F~[ϠVCp/( !M|U!_>}[i|}Lԧ ZiURf{[l%Y웷0C{ 2vv0G͖xPK>~3uQ)|PY}l_ƹzKPAGLUfXzt +{KGn==#]kOdOMV\^ݳ-]ȡh nO \w dAMtOeIeI8k]ZCWc}fTRgs]U;u!a=大 O&TW-/C6y1bRks!ӂ4fTu6X }35)@X?@Pqjʛ _'Ro|V<`?߱*{5x2>6(ߐYxgN,V 0l&lD 껼YsfrOoE)/Q  +P9(Kb5yCi".udŧ B +b"5~ 1q(N%߫xmo' n90},JQaDe6A +x .Gǥ_a!^ =X],e)nj4q~>g'3bPz MPVk DI2*aF>O+zYZ ubgMrɗj)3↥Wi3X]J"z9xϿ@9i88s*)=OSЅxoi:uE(XK80N[f6JG :.UiM)҉fX{hedC>K}#?<X ApѱNmfÃ֣0jE8?)t=qcX+YEi'#hw"dI^qTK+*.([V,yFj̻g~)>3*eZqM +îpd0 >#N[EM**WGϯ'*+5}}n1*~oJ<)):vK^I`hhWw+e^Ngl.t%Y``V*ҪRe}Wj@h,֯:H=nSX7ih F٫=k/ڻ/u\C !:9p(mןPH\I{b>4}Z> PfJϧ 0{$ىtvvڽ߶Rs)J>˷E7~ɻBnQeY"[_vЧ}8Ber/B\2Q!@iKTFJaἼ?lbxU+L~~beőWUQ%(7W)k;]\ezJ<֫;ZqOӋe`4D4lr]Q˸X:?:iT6fd~L)Ck[ |.[7ϙb|wI֕Z5'%)7%Z!%mZJۓ˟IwSaˢٯ.퇬9:z9wb:_Һs<3 +];T\Ϝ=Z:-RQkG·gVT{:u2-7)h)SmW=YN͏ΉS굻,ͤOyJR|)-If=PI%bҺ9(D= +|PsT#^FMGI퉊c>j71Hx#؆H2׳ JmqwBYlv8-S3orHϋL4F_BJ9baqw }yx-ʎsLtL$CZG"ha/5?&@ǸJ.0Pr %>q9&.k/x)=SAU7L7_NOe)5Pv4IJ ?QV¦u_;}.ɝLݺp0ϭ/J/[p7xTٻ1v+Ź0lɽ<{(Iw)p삺vN {S^TkWn3Vhnx͢5rh֋/P%]WZ w6<`"e0򦳝|.Uk +G"XK[T+~,;[̣o-7ڒS6qWm0얰,}erhGF;|zo٭†;:% +N8tD|j[nv a-w@mkYl}ڜnxӉYW{ǯZ3]"WjWCGiw.-)b̙39 WTj$x˝خڳO(xMm۞32lmRէ%XDTN@i`%uM߭_ezC~~)qq XQ\;Pkͣ kPRqg6ͥYmz _3Cmqϲj +M_tORu|t_5J-e[n1ڈY24<ڀx c~v%&ʷuLOn\+\M3*#NKq)h!o9ZS (Xf0M\L{0is@dg{f9=# s{4K<.VJr̀e~̵,7{bҬjS5nN3z8`e4ba1:A;bȯ6x3p,EM=?o-~HU26>]UcVp].?8q)S_>b&Gsp'& A]Ek>ԃ$ºrMrدwEذMM +[6;Iɝ(Y.(U%(PN;&(>1{V6d|V$Ev(]?OT2~4)2Fva&/vOFrDy# tj4?+R_ߋ6G(AV4c֠hPn,P $kr$f)|XI+>.]ƈ0=DX`BJa +Iӿ8IJRg> -P 6( &q$XP*IqEP(3,?BیwDĕb(* +-8!%%lr{l^\wPMI_6z⠜nʩ8 ہ@5\p`P~>9Έr"~2z2jB'ozaxj}){#v\z +, h8yol4z6o(Ϥ9P:uz<.@6'k&ҕXwgiɨQckXhQ>}SȮ0/Ys+]%KQڳvxd@n$R̤70 w!D4 "݂^/{[8(hy;vGfκ0fͽb+ܲ˞o!661 '61Dtu% +v6(Mү0ݒxqDQ%I/$b,z|jo7r! ):˰t +Kdlo]t(kGf=F3^{z衾>1Ⴣ(<fDt +g]PePf1W ٍGv[r+͑akX:;[[7`h zG ` Ԡ6R'g(P$ /]h'@PV =ƕ=AߎܼWG璹d FfWzpb~moUi<9(j(*Uȕ+~7L}|ARW|guND' SZf +kN +6iXt rc`YkoՂ~2izd)rg*Gw;Myj%n$bc)>_4D߽zY[9jKTNV%`Ū3L U>خl}wCϨiKBSOߡ;96v$Yag8n> x+ +|}}gB|M4=M7_?%3rͮog,2V[=:*(H${< @޷X~ɩ +3 ukυӕ!Cs=-1vJ*޿" Ds QP$IR@swvz~POV0ؔRS{C6N9I$|V޷ًhѮknϷ+}*_Rx2q603C%6e*uk]D6iR&ߕSw +4x"|3Xċ\8]cȮԇk?ި@{S|{USw bWLeR+X@޽|<>9=:ԶE$ژgI1.y+EXĬgts 9nJ;9Jy)~eF$ڮ:(ų+^P멂y{fO' &6Yej bzeaO2:GQ }%9ۘL04L4"Ma8ֽʿ~I\jکy]bu~ncȾj2Ӛ=gSzE +$p" ,e˥)9l!,n!=l 0uSsjψw@=2[=4uaսEru>᥊qP8xNqZ=_u lLeO_ȤBzI>'OZAP qM.":U{O+Y3˱^K\К#}Wg}XXڱ(\8{7J}pg~znwbgœ+,ҾoM˻A~>}V+jn$⟛y0ģJdTʯ.5w'4\;pGlMY8^Zp^ +9B*-^gHLFr׵UÕ]B+ S?Myuʩinj aǨV^qVEia|5X_Ua,6笲7:e7wFNy_^ Oݗ)D4Uf;gDo]qn8h+{HԴM['.egZRMw&|->>o~qN.֚1f\ؚV}MEҧꌖ_ +^R_alM +e:/~/ +ǻe6 H/T_KK/jrQܓٍ^F9YoG~[,ѫyj{Za%)=xۄe e;.GX2ˢwEov b'-Cfn N7PZ!+X ~Yp\0L4 p߫#DiOݣl)efr:bPZANqLoMf [-~n٘)a49 (?R㹴7u/[۵0vzTvl5';ץބD?[B,Mp<>$¦ Xyqpϱ:TkBa:„z1ZZHNs%Gxٍf$irmskU<3;m]6?kSN+Aw0qrutԓby3<0>5q.}G˅UYAH$C <'N,{C}zcN)Sߝ ׌Mq+QW[P-^}qBgG v, |rumU~r ksklvڜ6ozHa3a9 rַ(8o7Ci0v&6ѹI3SI(j3ߓ)%ZfC[AAj1g1O\H" % &0'Dkߥ sKǁN%Om9]|i)n#/~C[xqsI/F4YxcE;7y`dbyOFخ[Ml }9rL<&V_)60hUuP6'WE$|WM$xu>E?BI*tt޵6hS]`.k;}!&\KaRݱ0Rh +Jh@'_|[9"F݅ |3h+`zg/Mq5=*0f'p̥H'5}&>fbSӿObWȗ=6jy̓vCe$҆ ي~]~TpK>*;~jrj^V_tKMavN ?PRj[dD/f_ZN;(Db +V+Cxw$:ɰ)jiLep1]Jlcu_5cWŜ׏ƒEAA-FymG^ {&ذnB;?A'A'Rzl=;@VZqܲ&@] ödހx _ɵ(^D^^͊M5v +ÞШ"- rJdTع%L?Jk?IKXt"NR]N/;Y;WfE l;cA)Fn]zEIt-557u?ֽav? -YΥ@L ,ո:2EQCw,|/ +>4^9C\nzuWp薴M;kV+CTg:D*޹? +[_xPX5/Vݷڼx1lA9j͔}絃pV\VqMuBۆ*83f'uCMSt3t$ʺ?@O*A%0`l[OrG+G 9h^?A]Km 3my?pi]ؓcww!jw q m 'Y,?#5%tI %B2s/T|R5sͼ~%![d +n:6LL0SfG9#?@~3;wvE>[*JnZLNV1ӒgGWƨeP4v͸pW<,>]^!I{ea4wv34:׉/X^'*ץ3YE,gJ+47Hd%@¿! PK?mH*4Lڑ2{>zGudgt!l K0fOӏ]ɪZuS~ |xr%%RrǒƵVr)ȲdY)*L ~|PK(>tXv^]aO\:s`%t5o +|Y'w{_]ykzW'|)1;pl{:3Ad2Z'FҦ_otL8l2V):L]Y|r)jjw_ 䔄ug:b׀(a;em:j+V^o-y=-SigXPy6(Ym75^^h_[EY?fl _ݴ%.8!flWhO̘0tV~)EHSWZTu,g ]gυ/}7܆k Yֻ1e<"i<߭%lL;freMkbW&NӴ{/Tu*K%ٓ%K$ITŸ6,yi5Z3tO+ j@2dsv;X i\:M +7NQ[PDVf'nZlv=g_.ПnTqpLɜ`rGwPvo3g5sn&!VC*>Yڋ +Tc61Ӌ޲ZC!3;;ϙgHZK@))w.Z+<Wbf)=f.fc=G\Vʕ!`O=/gVeJA7Q{d< Kv9tCr!G U91q>]+R}('o#`# *nZJ؏'.].|%X&&k-Xanv +EFgά^0;޸f49Vk7~tZMzJ]5X2Xh*ٓN`d [T3f~ݴ6L+Oz;ZwnŞ}'E!2zdHn֟͟q+-}IUv:-*ZXQJj%i1xe5Yz +v^4=>~6cҷpчM4y=Vv.μŸALR}s|²?0Ft6YkD<3yTa{xwm[lii^G^>6ef71>)óC]H? +|JR;s_Lԧu"ObH7wv2YIE>2uE/x@&"^χ7 "0Q`i>>S||kK]΄Ҝ 6:FPo- ]cP/`_mP&ɺvxl|y6*W~lS&}pcK[?EJk|d G6"gv!͎wU7$[xNez*sq~n6PPh,:J@`ݣxFnᙬ]ޜ۹dā u4w:턿9^4xgϱV[g6?:D4:'oG}cj&s|@'wcA][<[wo!r_q I=J4cmm>V8Vf,q۶33cc$McteFV{_tOߝ)O@czHS2z%;f} 8P:GAX7@ke#+{ t3fwXN?Ğ3=9,ajOh:~Oc/2lk?qII{_mםmyE:=>bɩjs߅CYJȰ=z_vTҮĸcW[GSz7Rħv;1OIN|=ŭ;(XUݻi^9Tt C~_*@F0nPLs;J5ZՇ:㊰?ua:U@7Gڇh7FƢ 0"EE=t=rՋI3UMipLpaԥNZ~L*;kgM!Sj]EuCimsnJ$%U0{*GVsH.R{r3.WpH!oz@7?rTODZS-ͷ.[Sh~_+V]0MEd{m6풓嫔短^ZԶ>LҴPN/@@LjQ{F]*;rA>uQݚ:O.iԴb&9B080c}s/r+n9QU*ޕ{b&Uq3[Q-KODWDb0NoL$͵.(uwlA}lh#Nc60g}yݕO@}Vطf?A[ȇv( 4~ SG +Ok&"GvD&V1?P(v]|t[_G,mO~ 3Swn}z+FY-㐴{{ŭMp 9'<a]ܦ+p^×Г_!gnDSw +a|SafF5;@5̉S&%OzTޕ`G$7*;?6AeۄMZ,YcCv(o /9CTV~|}붗Y)Xnz?QnM2kWyVm/_iM{cT(3l\XoXMe^Ψ1k2+7H?܄h'^P@Lʪxɹ8G4;mf+iOl5NK l x_BXqQnm{hhMe=uWV\]bHΐ?FIq{=tu=/s/& +E ]KST348 ++=kU=>>l:ټ-WĽ)ѧ֡~?O]>ъ`pq6͑c}~ +=> ( m;z(j=Ey|z%x E"BeҹgoLf\Y? sڟTy:Se/%PJQ׌3[Zf)"g=A]@Wz;iwd1{t$Pl.U0N{%wl]kK㕽]i8mSX֟;~BlG˺|7@.u: +A+k/vGICtwB |9PnvؾXo}gg:%3} tJțwhf{W_loBƟ*G*}&Eه[}R[B]մA֋mPu; x_ߧVmu#ͧxcf[mmc+F䶔K]'\t}%R|id tvdƏNitavrlmf՜fbKiėRc a9M6ܨ +Ż]tuY](˰p/R{oý<^3W0df-MO͘VƎi>Nv~׬XqK?㖱FQJ-$֭{dx⛌r2̤D#?>ڙ@.SOR<(ݑ<쏇1n߬gr޷VOb>wgR Cٵj4ATn on.k w 2rVe%e,Yc$p8 w}0{zMN嵨]Ug'ɺՆJ3:fdo;_*RIm薡[4X±I٨i<}[.ntVhVnjgf m)7o?Pb~;*r+QfLB ꖴP [|?& :|] +7#=5íYN}R5 pXQӒNj8Ly,b x4c %zo.9%~sp}֮M3@g]`H@Rwg=# b0K!R$v, y9++/dΥO@ VH5S/[ǝY𨚜FsCZ/&c"ouŢƑÀ$r*-Az"Ss*zB;ab } RйN .N1$']&O + Y_Pe%<`$(t"KX~f{$+@֣7@g hN ^)48Yv9) keyUVq)L-_w0,2>@w[wqG +^?  ovR@O +UH(d*WJ(VWu=9~׺>HڝnE"j9> ŖJ)xmΑnY?+Gv#g إ*[՝+S ٲPsUΰIqb*)7͔8 +\ud4b#<瞥*ڍҨ{}#7p7@>vp +-ΰ1N#ZNkqi-RSz$V9;0(67~wwǻCcݻ[3nxdS71 oŔ?\ ?o .6PNfm'_ao+]p8 +UduYTc=#[,vt:|\`o|W f +2o~]t:#)PgV˔kE`މ7ȣ+Aluv`yƸ&t:d:=Ͽ;7n]a)ZyՍM(a* *ۂPH]8GeaՀLk~Aƒ ,d@?ɮTǐc;KR(Q6/R$n'x~-RKpl#×- /R'n7:B7s:T:)G[5L1XݭP-Z HXK5d.8NkFs/1kUّrwe*UO m:[cR*֥+q}Hy9J2sӱ򽻵ؒz@)I9mQ. +KL-]jM8litJƙUdm8u7Du>K>W̜ztn..pVU_c-~Je8Ŵwoa{v|v1yHE D-bp1A ܓhLX7*r 3ҕz\6MGTo/7bYnw-7]*[l|rj_aQO+/[%,gma,pOrlT+1zv߬IT[nuYiĵ/B0 2蕔<^IID 2t"siq +EEK7J?ѳ 7\8+YrH-,o xMsҫiAn m;{>c򧤰:EŪCo|(l٢;R'?0c +歗poWߥь<'%oFNYd{+w؛{kRR9/RܦNµޟNॿӋ(JGp|@y7j، d]NOkҠqvb2Sxy)Z4ƒ9J6q>6f-/|.|Xϥ + UO,32DyVteɣy.:hz=WVJcN#MZq|!c:^|(i\ٳQb÷]:70±S۰Sy𶻿ڮNpa!KyU2B1R}by k,aN[Ҧgpy[[m?~4)BO*R▝Ѹ̿=Go/IxW,LFԊNyFkk8w5g{M-tnPks]2.ZpoiKUM8˗o-W3]6^ *ɣ5.ú7@.ʫbo8 +r#L׉ޟ Xhsӌ{cXa=wҟ*Ls3Eʕ-ijPfYZ`:GPk;k!:>(67*'2u!y]ϠrG<]!J]@m' :1fּ"8HnSpQ{@j!ŢzH^upu{~]"8v~ f%u:ȱa4hDS!A5tyMt1 +/"/9K$.KD)L\P Js6_É+Ѝ  + +zuߐ>%W'S6B=ǜ s|nn   N`ɤf*"ouM^YRX9fJq5 Ŗz#ݠn~EZ2ȑf+ +}am][- ԍ-YP܂̋ Uۛ^ձOE^~:;mpA|C~?='=|ٽF6z|4Aޜt2HeH\ȯ˸aJ7oL:&G/~"yBF@;aTGA[P2U7KRna=䑄u}5qc^ i*_~Y2R75<o_0'3P\} _GMvs^OQM}~ na@< ?sIU`mA("&"$DEi=wj\PmnZԦ)דdu v&e%@r=-  xe@V6+^ !I>(S#&,^߁&rBDzXp"`%X6P%- Q6S&e蹫_$8@ZүR mxUzX%n1Q^ Yl U T&,X +~]t|yCKtu^˭}s{qV-G0w텞} +^ ۩mM*Q6~<f?~L.#7V7{^N:e(ܬ?[ygWͳ#iЎVH 2oUmL\,!h0BrTB}':zzqT 6l8:G7Oa/7I%SWպ6k3N;G2vUuK}C]MͱH_?(^@͐9euz;fQ~+y >d*?{njӹ/wNt<I[zT62[R\fzF`xgdx}8^o,2O`Ѭ]; ]w.φ\;क؟WlzXq㛣k嬭g' k s2g鯍eY0;{~U &*국tu]r^WY/_Nä f8@ū + + Λ@þٔ{۵s\5.*&m'FqƱzhUP%SzQ:pd#k*4YV,R^s҇MJGUO/xf5}+5-כDO3 e6i!X)V[tYϒΗ>).:AZu"Nf,]fݹ'N1*EG'@ `\2 ,H7^WOA ?,(oʹlR7y=_tC@'( Y|F^tE(3#L1lY*ϟS) \6>5X٥ +d~^#?"]f\Iheaf6q +~v]z,*-&݇#2sC}vͣC{0wr -z?L,=iz;LHYX 7M))*wô4t.õkKۛR5/u%W^5c~n˺y2Ȯ ymmoYTD1:͇Q፲vsCUgRVJa5$Rt> m[WWr/UA95Ӫs=0cX@'N/ޟdZ^ xy,g&.UYjon9)%Ǧ*fios"䦬i&uUg!Q#ػv$S I5urpĦKti1r >˿wqz*G0?Q'˜iʨV5%J2mٚ$'>}s|[M6d^sFPCޱi8qe&yM̧DgxyXT:}z]iwJ]hwE{7ޮ9.x }v0m1h-Ub, )8P?5U˭o/GKx2"P'e0Zp馲`aqnˢ0|9,Jf<{nÜ~ { \Fzt#Or)ս{*^=//QTRgɒÈ8Ņ\{>|.7o>!ٽ{?8 wgO@ER+YV@aEmjt~L%%wve-Xpb!d*lڽ%&y$rw3/kO5џCUo早 +ӗ_<-oc\-֦^ii>n']yvp@3tEп-OB~Ѩo3$خD=`)eԘ +;݉c؟&'NS9EX:yoQGe9,\o;lrjj0eqa(Kx5{}Dq>!Qk֠HM&&dL,h0>ͥǧc0f`w8œk8F5c ‰JzTݐ^07nL'9JH݅,n,~z?qm<5fx(acD"*. /ܴmeeUuZŽ^w17EZKl s{=VvBiJnA'>9Xte \e=a݅݅(nO'&YiY̼N]Ov._=#/=6d6螻;nv:g4u*֭KZ-g7WmvkRF +Q6q~wvRfN,,ܷNMBByg|1#$GE{ƪ1S7p4^ݯznwr]J $},%Ka YYero<ƪ xt0HJlh518,A3m>stream +n@_! +1J}=K~͇m]ssDI :(H,]߫ Shfh5Z!~JtXx"so<ɮ$_dQb.ǕkM>V N00F'cb,]1:d=PXlP ׋I%ق}ykP'cPxec#ϛCpSn= 2*}SqNM$Y  0%n^5_˼$gOۀcg9Yd@7*zzm,2oDEYhPF[J]NZG/@|u lI>{7!Jl}fsAS7u"U'{,q=n!vG˿{d{/M2Bvwj8/pgEo]PTNM/@ZHphl +ŝx͑wT2o,kٌC=|ܞ{]9<(Jn~WuJ p!6;ILQD[7PA u{;84m *Ȕd&={ǝƫw_݆$"{-35;q<~Y;7sI)&>?,;Ѽ鲥zbQ/&!/_R99fHxdȖ2np/ytFJ64խ@eVn,:qԊcSz.y<MjឺuSݭ:/ڑ P&w?`^ Y/ug0 k;"{>]xV8XL0_Ftz$Ǿȩ? w_̭.K[AteN&crFUɨ5X>Sma2l&fi2f}lH +h>k*Fm'O)k3B fy^y֛h xnAi%S*[^_= +C3Droik.ħܚ/ʩzoT9e@\r%Z01SZtУV?+u?JN<.)\>N6QSwPX̽'3{{n&P뺝;BWoLQ'$sՖ,Ƹ&,9MI(\DŽGzʎՏ;K0I11czeMU:KpO W_`eg2w!*"tTn:"/ C%^VQXQ;o~%3I{C1C:c=8z^n ]WxLu>q1mB=Tz7\9P&AYHNXd6|u;V9FVVsݖ}5=݅+^=Sy0X,LV*DR ےd>jd?]Xů9Zw6$<fݰ>5q}"k/kGwn?}XZds >0t-˥j;6~U3Zy05Ϩ<BC? o=?$XeYi0gvˇ@ty᪕7(UUoQ_qiqgǺ2=KA@/`Ro,8U!a/V(DM㷐LsԞ*⛞*TGIJXJ'|& {_?]ɇ< Zݕn9{ti?oT1W,Fʜ$ysʕgAN_IMTSơPCm 0T-KT _6qm눏8؉.0,1QeI{f[p(eQ+ +|6=o8i#{k&_4nS-~OkF_8LC-9;?ndnubNGvW~}ea&0Emჾ$Y;{-f̺~JtiX&c&I<}2w 6]͍K'5ZU|jTxѩ4ܔpzWJe='QBYG J^;_Ò541*put"N dR6hD<’ vIlD -?7G6}GGZꢏ=g?lXI|s8nOsfz9['f" 7A飩OQl'qwkn~:BVr߾tK{_v̵imF ,V}K7M4<47pίRƹGԞqHvǹB)uP$X~uc9a\|fP5N6;ʢ\}IQmjVjx1UV&UQY[hJQA]j$WiD ɺ$!W?d^ ?!)g4o}"IT7Fŗ@R|~J̊b?#K}"3}EHkeyM6/swoD301^+5ˋOkèy}jUϩ_y/#Vlg +B~r>Yyagg^D@D{ ~J71]jS{'YߜtkPQJA˫?ė6fӹERfT%&2PI+"Z]m #J2-@.봧e IH>y| RzmR0i,ˁk1u=TظCTY)kA$;dltq\3Hcvw?= 2.3r5 "He Nq~{i:i/*7 OiW]Vb9V!H7'Ncas6{;=G wj0<<ҟC  'dXɇ MgvwNԕ.&,51+'w1\evyS +ŭ{Qfyo^laF!@qhA  Eǯ; ғƳ4 +v1aN-!m`Gf٭vpXX+r| Aa"Z+;6y|1E^;tqf33,I'ސmxΩxoT)J[I_ 0W+]^~~ΉMŽ%-J˥ag>yO&{A[0S؁IJEo#ͽGEu'[MJ#6([-v$  IlөNPMrQ7Zv6bH#^vZb=![rڂUIs& +lMY]&Lfxv*nI,i5LYl@FV\䅻fk5\ӓa~0/{2&.讎 0ÚwmU O\t $I.).ٗLM `9Bhsb7U1$QvIMXo9?ۻ=<:"m~d|Xݳlnmg c̶ ȍ8W]/n͎V5,)uպP?k ~78ŪQm/gS#nc"4sYc^WutYfqFO;T3c#L~}@Z 䍀L^݋ѸFXs۳ת[GN3~ʦ,b,z2bf'iԫUwCWOm8ʜh#)IDL#`p/`=|b.Fx)9?P}X)[F37ѳfa^/%Fj^969H9"?iX#ۇ,OΆ5a;]7/>t`-]mX3~r|M2b2k U pfF)6] uTb^2V݆:t"Y̅'´r7޲y,&٦ԗ?@)Z ԍ@mR}I3Ӧ:?F:m%U׾A~ +W=zP>⬈5oa=![QŦ}=D {{g Sd*FJ?άˮE,-u :vX\Q.j+mYm$bfi&fiH||YJ*im5WBz_5//^},uv]6>v?y(݂V :J[RrR8*HqhGʑ+_%#iJ^lפ2\E^5?uξrV;wS_lkcrЯ^<ԷM1U[n𦬷[dGlYQ,Xz:= swZVV55d[Iiug>=l6ibv ɿ8}'_ N, +lWbZօsMbB0*ƿvMJv,Wm,YLc>g9AɁvhCSX93(a y9_Vqxp'䌳=`NumU\)͇J"Լ(xM9vԦz}}~V_65l;?31;n4v `k_^E}ȉ sz2q;:NݑAos +TfS-֠f_$po2]6= 9:)喞՗pͯh} ,6<~rU8N85mZ=h̽TSvKS+6}e^E摞h1EdUhgmLJ>}pqe`9ݒ02xCF&cl\ Lyk)(0IZj3{cbp3tn[,i-[p2|ـlm0#J|v WG n}+mvzꟗ~b]Q2;qaIvdZ੄"q!4]]o&i;gI|¾rv5Qdm6Mr;aYʻ9d—.:::SHuT*aYN|s @ nNa 0@ 0dl7AoAf"$|Ĩ_y ۂ=ոyH-_ƞX 6ѐcE:<8`8gBlSn'?Xw:gl_wD,|)0Gԑ;^Cmbf"f|8dI?8|afϗvJ/yq)jCu%ѪpXjׁ;Շu#YYJtuƲST +~\R7*R.6έp\AQ8n_a%r_~job_ږpatt(IgNfwƕq:[nוؖ٬W +GͰxbL +G%d͙uQ3&7M  myCNiE))s{?}5K] K* *;i*_2oV8/pig\JD=W[6,4tr^ZYN)^o tQݵCcSD7!B"z2^|SC&vD\*`W[lxjALN rU`\njpdF嬟U+Dd yj@7vc|G9| <$y}J>)꒼T|!H>y14j͒OQ$M>hQɧN%r'YU!^ɧҀQ r 6d$[^f>,ŇSYK|%E\&kLX^]' |7?%S? d?rߛQ@( +3|⊑M+~s63a}Sku00$@Wy@jA +krHi05R|> ߵqAz݄LkǚkZ9 Lџf} .Q;uNnߊ7Ҍmq}|SWܮ !H#J +n%H[ l_ З8_YZ}&γiަb6?^>뎩`+"w=,. rTzhJ'vƭ W,50o$!*]O*@6c/Y*|Yt\`/F{bto8uY.=Q\/#>^Jj^Q,~ 3W~ jItc<h@+Cx@Pj}ْsh9'H]0V+[JN3~H2XQVK)jNJ;_)=.Y,{X\.XeS1~&lG!@VLPvy_u11t(8Trj s@'!W)b~<_Άa=;l'fx⼟-wowEy՜V5+zeYpT`eXrXT]vaKԚVl\$NT.m!7Vets\fZf[^nA-A؀2GDn'K43\#KpbyXւEJz[k)z"֠ese]4wgMԯ/%q[(*YÕżSSgb9~ٹ'urZ2o Ys#cߑ?x4C]K Z?E,bDY$Y{%8 y`YK(*PZݪ0z2^v:o[/6:s +X\kUK]VW#QEnv,ORr\%}NCq)Hx +T82魶;h6g;?)}{+|9|4㱟7[ BMŜo;E ;*ʽ,Y=W$#ҕ@D'XjJTa3|-/O5wΝ5k_몂*vWΧ|&ǩ/nhݗvryu[JNWduIdJ/dׅs wo-3L/zuՏl)OU#j$ڗ2guvoyu˩{o_;uG►;DfYŕN>&zҐ)乥;r-sfMx=`}uvWY]5۽'ʚ *{y;}WjY//_~vsz-b~AvڛfZuS0oP*Ha$ڕL[XJfq ,5~uMΫ͛GkpŀqK{ts--V/@//CO܀L;烓W]JGv:-@J"֕h3ǂ0 '7E\Y鼰%m%L84N\ؾ0ooY{Li3Ff =#yȤuFl&Y'oa\Ν,XQXQ =^z'8aO!7Xd6 ::[X; }T}l|z7m?Y2˙A )Ɛ}vj_\R?\!}Mqlڪ[u-8YCZϕwJ+q4lq 8flrV,$dw S v!?RR,|~Xt~ڜ!9W3OKK~PX$_[ÇlKc`j)3zWduYW p#Ȳ3VO2wOgnRT׊5<aMM*{\R(:Nϓ[zMGn.i+r23O I^/|CX+祮GG. ['Emr!3t=-*׷n:R]c>Թ@a_\*^Q)JEXzy`$daUps`eSaY +˧; uױH'{.z/>B>44 D^Cg:;.? <}谦sh;,PݹGHo4̮6T*>J_/p^Hr]Yc >ɫ3~(NocO/q*9D +?&£ 2<*K+@6s!iu^OF#4u8j>0Kbs{f:^9^ӋkT +Cu=vyS.ƛ`8#GBΫBVBE8xgㅳڽ8Z ,.](3yJkH V~48.0 U:#Oʠ N1`Vj2jvLObC=r:ܺ]]][|])t_p/gF^gvSv{|lEd +@zw̲KkANj66/], EXWVenҤ5Y%q$ B6-m6ZV5%٘ R[,%z,պz+*z0j6Nh+P +vۚI՗UW'SY\/u 64;ARY<͊A?r<68O[O܌j*,"Ptl48qwւhVkVWvVy_ L%/b*}` Sn}^~ݣ^)$9iSpNV1&/`^ui3ݒc\#bjP~Œ}^ Ln<>xtF +JMb + "*q3vR}X +։aKnt4^d㧈 b< pZ{n6ZNP`1W*XRH:W}97nͳ+|8+8f/"߀j[;|O2g!rYMCF($0$$$؍$$ m\>Iy#v:d{[&G(!T3NrPlGh&$5ck^X:<+=+] ! oJލ,m7c7V&|$ytR2>I%I78Ceٱw+I0Yi2+؞IOL~NB|.Yp[;yS7y7m;;:ɇe{3|O8GvT~6y ?I6#q9v,cNvF"JY%-`~Apj>$Qb |[Oi{|m7(pj)0 S@Sp瓵K]$qoZR|7'mF;=8~=k{o_ܔ5!܆}1!-6X.9wd +p"/w? oK>;F<99VLs4Mso2wM)&YN}K5\3VuD]e{dS&ȟwl} nL|P^{"!pMh# +\$i UɰO !|{D5/d|:Gϻ+=w#bA6p{4yp=~>uj~{ҹKxC[rw:;XT|ӝvy[p:vT7o~S{ԩlR474O0riA=Up7!Xq.Z>p\WUظWyB6j2{+'w]_ctʕ.Q@KM5еz'հ:VS%}?sO;zJU6R`Qw +=Ie^Ƽ&(|`Oe +S_5I1εdtأ_[E_{q6Ӌ*or*ASr$6;_drJePҮo_h 27?4My/pp3;kGya宅l.fi_]MɼDU5|W(* 5"5.c<.KFa-V;p +%' +w?ݬÝSr6G$ܰ~!-A04Ne ^CX{9_\ec~3 m,iSVNS'l,-)GiNN +m>z\W-eѬ>7O ;OwMyY~ʇDϮCƎ bMtqFaicz{RN+$KUcn$T.M;4/9ũŵ {vFgǬ[邙^\%ygR6?doI c.-6ˆnoB{n'orb(UZB;9 |x)SܨXٗ7Y*cp}afӇQF>N9nCmϙl!$_M0$ pټš+sޤBWoUnDrOR! /yB{Twֺ q~̻^֙ܨvLi^)ŠTedP$O$I_i|Mu_vS61Q7Da*:EbM:Tަ _1=naZ ,O!h>p4ֈ5j'UudYס/Ì瓐wZx~yo YY`r[Vꈒ9 s`^e[w /Le6?q^;me|?xJE#>fma)KcwMUgQ@}yj˶4/..39fc ^,n?4[C36M/gg4:Uw?y!"+nљmDz"b*{&˽ +LoltɳU \H&r$FOR{-l}_mP((s.H-L;K&FY~MݎkggloT{Vҥfw划Ka{l'ڞ5 Pʹč>B Cf@8@nx +^cFzxkZ\3v]|&aYOQ|-O^F8^oyOm߇ؗL 5c&14`+br_hNlŽ]wο|?wtO{Wpj=MGOךq`*rr%wJ2%@ZgY`vKC>; ፳ +B`5`ۉ?kgIm +X$h W[QFC?y)sZuV#|d|/qpir˸@\i="6i1w:MvD°l00T|[YӻJ4>ҍ| Q=@30ьq^lL&0HYEt 2*IN9u&߻ak? y~( xL~?)? 靌俼Nl3G-Z/a}Sd8?$L^,cTf$AF^ҼYP)$0Ō*I3V+I"D5)9l/, +O*ϊ6TBu9ݏS?pKW\\6Ypk;L$8\1?V>fA졛8̓%.z6/ҺEGv!yfjI%+7JvOuz&0-$ogJެ>NP'ޑQ#]bV?7ow$ZOUN>##Zz)0S=OײI^;_kqg]VToJY1z/fNxxKh˺yrg\VPוּbe~>Ut &D쯯L .cm r˶{,5|G?$]Mp˦6cY݊+\WufLApw=QJ +xЉb+@]%^[*qzn~k4uV`[OwE`1ӊͻKs룕')$.yi +@/0H|ӟ>NJR,bBnhw]Wvt8=kBp2peZh3U½I ,3Y \NDM1qOۣ72y csّMm|iC?ᦶM2G ܼGeNs*۵SkPgn|,]ain4e&;Qv/Yu4WY]m8.uxxe8:p=oCW͂kK5FswQBÅU,Y֪|}/FK>zZ_!DSjE}r +@1i?2~gn/㮝Ia}nVv' qh!4Y4)Y7+[M2-bp#B"S^gh +w!WcfZQqLELΖ; C g Os择7àQ'ν|;uIdmhz +r8~֚zO0k \} Tǁ(*zYamh'-Ʌ=QQe$bB {ubˬ[m+ 7)aX-]a}qWU'@!9Xkl#Rۄ m/!i'E$]>/  +Md,C ?dOI_Ч{q%>wX<7abJ#+ee'Ge:ҮIeqTdĚWEB3O|Q~npWwJq2~[sw7`VjH _g~UJ7 B p]S9z}X+YN[KRi)'( <!yAxH|wڷCn l-8Yn: >}Ƣ/iz7ccmO%iLjf= jaACA'{2=Ejuϭ(*/l-+Tܭ M9.\Yp^й0fy2]?}uI\Fc@lƹ)ފ_z9WǤ$t]薞}5iHq Uz#Gp\ټh6'1) V}.c&VP1vV:>=B!PQR%fXw:n2ñMR'ձ"Cڳo'֍*کMF֫d~qE2J`6g0!1g4('(h~fL|saC6UfuB+b6咠CUwGliԠHؘ čW:r:~80($ش3*+YC-i2wbsq^*{RFuܑ1f{5kFf/ ٭@gMvUwЖC0.Iࢇ*-LtQ٠iͷ}4'r1ŸTZcJpxY?=iwvS%J41X>4wnpoWv UV7T_p!cO +Ņ_ |{v[2Cj D͘3%Ϛ7ع|E;X! ړc$0 pm3hdhv / pů,rgR[7^Z^zZ#u +jwlm^>>S>'ړܥưIЇ*fyvP^#wGXGŠR.PwX9A;ՎZG&G Gh`٤eӃȺ1p-pJ+Aew*<ק]S0p1??!o{jOqAn/]17)u&vF{Ƣvog6۞u{?orRT$i5䕾oݟ>oչsQl9&iո c`\BFn>0R&:[WPz.s1j]d|5A/rE}r2Mm.k +EIU{:Bwzˣrqo#ԠIPU+SR^W/@yտ2уlnKKy X|+> :*4k1zqʈjDo.;B=vKI~(QfBQE/U||~'s9qa.0 /LJFLU aCo X GO9ILKF3En=7}ğQ9ntOv}'Z\&ȭCYPdLi pثS8`E`ߔ΀⿬#' t3K˹ q' :$2;;?P` 챗Ԟ1jigm>k/,^Eq1{.hOre7[_ᵰ:1Ȣ,F0{y?e,Fev|+"bdm_n# +^E>:>Nr[%~ Aq"v;AoѼMPO^vۋ^]$!xFIieJI~#'\p@y4`|rM'ށH{UM޷Vo~$劣 NIG  ^]b ^a|O> w/듟F;b~O@/0 U|jvzHiZFp(ܟ^D\<@E?Q?adzToc?v)KM /jbݩ)}f +8 '3O!;9]r^`0>jnw@ϦCpjOvd8t%)uç"ͱrΥs:TbKO1g=bbz:˪鐔h߫cHCIgm|6uz6[nmysQgKI=j9?ԎӅԁ}+=X쥤iIn`qZ#bj0 +<kaXӏfWCet~dcOvnhE?W֒d*ir=lƀ5 ftk+:57A/1600OjcsI(zGqu iZ=m?TVX~&#gտs.9!NsG.E8㇞.ttOGpn֯j n8ȹ|S]*L"MXTM*Τe ?}Ғ'GKmE_\!gsBW`^/wMأmr'%!e-=6*|u_P"J[2!c=3Kr/S"iac%saq"Viq]vСv1X9M-*׳OyPϾYɎ-Q9ܡU/(R^D > p<<8Ws{j$è3%7SŽ{|ULǺW͖ ͗kr|rJ"0* 42uz`N? AgՍ㰣yčC1v J+'^[/OA]>^c6c3뾺j'JڗmE%b""pʞ?]̈;wyx '9i} +^HZBL0Ž dfVLvzCzrs: &ʹ#UClBOAd֖FR(˜eF۴#˓56]Ųl᪉um0rJR=!AJ^^2'OSЗ0p8ȨySΨRpQ^&=ȅao}K:a:-Xw+9 R0ݨm 7G"$;T +!3ỐسL44;:? C +/Tq=vд$ wWyr닳ɭ&z]E}JVѲn`2RZ'ppXcre\X2"y9_NzMbF#;=GynaQB7yOm2~ݱhXI2|hBTMd3s_宬~bHK#4&Y,xEP&tWyri>m",A~wr7ɨV%fƓ\H4YOwpJld;,CBOc H= jc?:khPA3[e0guy9UrUE-4yARyɮK8듙iO:+o +Ýad>[R'jN|z ^'+[^ٰvWH_`"sf)[i[I /24;Uz|Q{;lH>x:S +oӲ"Pmʼnҡ&2 +'?rӧzX}XNqƖ\!/ڨ`g +WGK\C.־> p4f}\{[oʭG;Bz ;U9G++Z+XetjpLJUB:VK}KG(mjaMݻq_]m]uI*c?;^ k{;Zn۵sZaG-A*4Mֵq#n\9QrOJn!|ClCH_ 0$JF:He%N[LCK(PI*H&7hKZWw<]xSڔCB/E^7u1 0Z_HѺFTΈvԕd+ܜZ$c +}e +sQ}͑_'c^ƻfŪ쫊sJʟLVF hPH 4zl_8_`t/"c(pƏr Lr0gE=i-`Ҭ2{EEŶ@^8Vcp=2'X.fNYP(?I,?LSc`q| +p}gG(E"R"% z.tLƍ"Jl׬f/OG`-ɫ}4knk;yAjQ8j?ydBvtN9 0IA3eB31E'ClUK3vk/iΎޅųJ8Sb!~/%3*2I2.qᤜ䓁ibElUtmv^E^G񞋫uxA=ףnwݫ"d9uhyUE럼lp+2[m˿M#(c?/ڟ7F7pkxm4Pj!뭋%Mke7_/_S`wa'_~B^rSPVjTx%y 3.Ճ>V\Ƭ;FNi|/k;? +AUR_j=o)CnKS "eu+*Sk ZFDlKuN?$o'K>4$*_k(w˪M gP,~]u 뼷e~}AbΒsA byks 5MWotp^FcY? \Wuݜ o|f5nZٸ-\ IXIi+EVU8B6J]9fQoncVvw%D[yaf2$[@Xq:^3]gZAp O(^>&yA36:.ed y-e}Y8<Of~XU`Jgee0K)` _|^$~&/N*]30SZY菺~ +5}ƨÀPV?L?EGNs"ۨ[N" b~z6/TdӚ]2VW%Gvޟb9U6F~|5ln3 C$.~T5{ k,Z(d-`nc\ ((}@<ԣ\=naQeR(_,qRƸqF^?jpm%> U{:67pkY?x暘,L| vڥ3h!8JbTΦ)}oAY:ϔ"/p6,)u:~Y]Uuxg{ vٕ=؝͗%A\Ϸ,SWìd2;hѧqc5vWq:$t@jtrpH*ב>]^]k]t&Y/׸7G"jͷ$977ڬ`j\-I% Tk]JpiG}OZk?FG9'Z4w`R 9_TM:֥c9"k}ժlˬp`v`U~ݬnGޤqi^QÒ穳b}OEYLvkEsY)#/52R&Yz.tΔ%NɆ^+>}/:صd$MH6Wֲi.:ޓXтQS$3YSQ ]QJF^|PVx#I[KoMJ3GYO0:"6íy/ؘE?Sh%oY;frgq'P d$Tgj9V98rSDaQ{E^]:=Y9,iWR(祦UD@XEq0#͹t()+/ё5d)&dŀLOG )jkleu*L2u񽕕U,l%IQs܋x4$>@l tlIq󠶻ENS?':b@j +Ҧ:-VmXV;ikqZ,$^K(D4y-PL:7hOxL4+#@) )|)]m⸮&U4"nLymg9VJe ȋc!\, Lς'9\glقȟEy2%-L%!Z~ND=j+=OT)ޣƈ7͉W}C%A;Ď-wBEUMoZjk&sp/r ggjc:0 O0"+ +_ҵz@mjyx=:`ʰd{)V <+e#F@Ul'A5!d\m)N@9G YuR\RKW \Ky`b \ /՘WTWO0_މv.?w z01ԙ8v.?1PSHrz, DyXރ\5 jw^;Bsݳt{йנ3lޝw;%~/_D,L |0( 'v.i SAWn"7k<Ԡ:^c r3w[#I◻iu'/ab IűMpݢʣ9'/&(dcɗ܆Ʋi6A& bF6앜!٦'*lc#Q@jmnӚ٭<7]o,ڣ!AQ.tyuCY=jAҮr_>?`)1c]ʞVڗ\2Rg,S1˯&F?=rKk6)AϤ́7@Fw*aKYרr^GjyLOe-mV^Sѻ +(Na*EQ~^[ avܠ/f{ 9΢iG#*+-`,@nnD8mԪ +wż. + a[o G4 w}࠰U7x+x3 ?c}h/::HI:B|hQ JJ8E~h@vu+"8@V  7 PК(\p2+e{.:5A@ +"a%vT=`ӲjF>ʈбcSw/aV6u W~2x.r c=9}|+p#p~/~@W &aۢjލto!uҢ2Dž:#WӯA:aZ,@` @@vD ɶHJReZ_Xs% ۔LZ :&w8vzE d,WO&ڳ @ݟT[B 1P5P0}~J+"0,ΰg_wf>-KW2,4 +ُ9J20G<ȕ,v %/I&ϨE3,?n}T-A{}r +Q^joq+8^gXe#iXol¬R +'(֯6ӛ< >*Շ=7}(\и%~_61셓#ss_ F3W].O#,+>ݽ[ޣb簻z[IU֮1/W^eg|*PxJ5">v>s$NwL*eu^_jϠY ;G(qs? +B8cRe?:歘;ېݳ9p]5?V^ũ㵖/Ɨ×-񯅥wvgQRr81/crZtbc{!ii,WMWJZ}H;YN@~v+,N2&/f2aB((w$C8̣ƼQ<3îs鮚aϨR +vIѽ44= |aLQŵʭCdZ2鋅rD5>]|ϛA83k-p:c:Ūa;Hy2 yRtea,=o!O{^}{E6^%}3Z&¤Q$57ny[*zlJFi|n;4/vװNH_U}.d~m˹wrS j`Ƭ7< Y4l՟M +VDxDq@Vޮ,]GGOGo^D>.=2bL~_Ͱfۚ/&ŋŲE^Oc6ًEW6W~ųcX z)"+}U>xRlYn^ _9#3؎9yy^غXP8:<rG}hXtձm e(~ 6nsШKߓa]/odiwjYԵuvD;\|~.7DpJ7Dj1pvJpv,(o8-1raEƠcY4~1~3*k +CZ@|~&wE'P xn}O^[_(sqmG}YsD7{~bX,kg_ %Mv؍d9r%(LZ|x\7Tf 95̛щ&CiPλyD},_>iɞK5+ӵ«*W ^R%CeI/Z"ObO_fFو`Im=xdw#g`:Ga"GAh&;rlܫy%PW-H\Zro[e0q/_D+Z6b/2yK/̜#\_t2ѵV6bߓ3e=v,}l*̼:h0v0lE C^\O"&C;>6ѽy.X<Ղrdu%}oXWb4<;4Qq IsĹ>);57fny7]|c'Įt9j6/[.ϙ6&4> +qs,%[VI'CJX^óPjz HA\#:PvD6,4f}9 7|1-މ(Դ>ǝ,!N\7LN/v~/7ځ% wkHy"ϐ'5r>p<cN09eVrfRZ5e_l&`AtP&zD=T7 hn![pT5, +A::LkP ₒJ!ZM$ B'C1 qbO y`ƬxvRi'üRIU vYbL)~A)ڭAuƎ)Uڪx$Wz.P㷽=mbd1;3 B'h=|E@ E+,V nX Zm_,UD1o2ݐ 5yځ1|ujY܅{ y4g%z= RDj\DsGiOuz]ā=zkʭn[I˚w.ݹeoEt4* prh()8y8vk?µfwwEirOTkTz\)eYˇ ++cZJswD +Mib:` R*'*(,tR\a9T/Zmai Y`ee5lsTEBBRouqEwlڅƯ sea` b*ZT 匕J +<$7z.am=B=g~zZbbNDAWDCR&WTf3@ +rCܺ{u7Ylb];t.zr+\Za+g/m`t~de +\p^ _\2_xϺg+*{*S +},rȸ60;2B&S~ {|E!>9O@lYÊ2چv/%i( U>gԬLE3[D5?*^׮>W<N\ͩz*-g_2_pLڏ ,o{D:^7yy,={%$wJxʑ9U뇨Wjcmmog ~k=8gd]%ED7ڋ G2Aey~񯍿J;] ɇS`fR/eR99Pt˝W?5m68_y޷vHIˏS^DD(N[!yk%]djρޕ.4/M_=k_jߴ.Ǐȭy+i߶Ҝe~y >Ղ(YT  +y}i~QIҰ{Xnn>YM>tI:F8;G +_<5i~y:D-;bW0&ODmB(&k|5~\f2a)^l;^l wQ}OؗX/:.:s\كC +_e9H}lRUqlǾϷ(?zy 2E/ Lf=LLNxZdK?W0;l}rHl pJ" )WT)-Pӊ_d~O0]WLG>Tk7]&S X΁t5m ̺ߍzb:R]d,}֭7Cf_3{ۯYjMfjR2P|භ?♸-m>owN~(9M\=谭!eX%%:M_&s֡/.GS`=uyCHn?k];R,_JC (WSj,Tt]#úXٚќ'C .։k6cQk~>Q58W̊Q1vrbi=2ZV?闛}j#=3OECQ[UCEsg 4}=1:-ta?}Ψ+c+`W3%?v~/8ff@MpchMyn}ړ²}L÷Y}wJ[ 2ShW)xn׶ v36[nE-i5:j"?cϬ/I^v,١VPR뛍T{.Z%UɣpX/)WSo-Hd{ +_Qy[û[89uzvߥAKS}/[%Į\~*3^V{-/JV6y~+<|uakΫlU`S6)q6BtKDkџK09oDWw].- ׆ }5F2ۭϛϗ,yiSU6E}vŖD3Ei%*^cK1zeXRYy,PS&_^p+qE[SW-6Q)Ou9!^tX9'JrLO҆>Ľ5ۼ(XhK_']H$ 8o6 YX̬Fu~E{ы2B i/GV`֞;>S/oOxQ 5y*r5ΤFqzq +_⼫`_ȨRe20g ^t)- B3/?ȭȭ!cqyμ*i\ohkewg'*k5*O H~{C v⇍ȍ;ɝ}ppl:#^L4-2.7::dF6x͢N6K 18n`": _OǼPtEeդL@)ߺ8Hm7s]\}za8oxSjR:S5O%>H Zv(pEaM4FtF5SF ĀPΤō+eDc6QSMN&3g*FpLZ惪&r7Vq_#|B 1w"{.6:7h +)"JFcL?2ftX6wGo8X]Iڮ1mFI-u&/˻ SbU{9*/U&[esc1l`'JU[2 aeG>xAC~0 YvLtߐܡ_r̈Q-w?=7yK2oE*ig^:H6}!S*R^4\(""Nz_xa:۾ڝV;g8u ":m2) 1ѯ&6,(cA-a矫^ ~ ,J9)XTVh;GUwP ǯ~2,tn>wS~1GH595 U8-d6?'.7RBnY [b]S ?`rM-{{ژlZie|nM6ۤi\riҬ5$kns.zjS}\ۤ?Cp~<9rkwqmJa9t+umhQ (+Z]vJϟ;Z_bX?NV**+=i,Ţ2شH] +PPS_ +)J-9BNlIw%[;YdLiU_^6KGZ׸ r{|\XP]~ Ws5}] D;I, 1б8c䰛iHivCK+v>t_hw~9vi'fQq}{}nt'j_zb8+G4+G[ gTWgPGܶG( 4=n{,t_Ov~n˕o\ s*0Ǐ_+~cw_5 +(?G%t5aVYԡ̋=*7|ܾvnZ!WSvǰ ǼM:wЍ7VeSX|g0Iz]ap_C -UBOX,cY6}ٻ='<]]iƲlDn9Z!{I;̼ȃIU6Uvkk8j3(K&QEDnQ̵QeiWiՙ7vkהo3uWLEWhPs/&YhT'kxLW^t@ԯ~(4̩\+ՙQ5Ytqt=ۛ,0Fo|* -M!>~|qĶkx]T~kz{f+!@*aПkű3k΂fS=|}L>Z1>1JaJzUv8mzg/Q3Zl[v i g{'kgo§.x`7}UGɄ&! &qkb.50RmhH >W EdY[kX۞|)ߘ OZehe;u5˧\ + +rmRp1 oŸ%hkD¨_a;q/yuB\yϞjMǷPs~&7dkFQ+g<4^xld]UwYSOi᳝pm\~?W/#ݡS^ Y#\8XyQ['ֺK+O>xH١~.q>Yf{I^)wPoWaYb:;]AޔIQjcu+e_у7<6Y%ac>0wkzR TDdJ:; P7—fw<}[f:Vͭ]d0ak1I n9Zqٱɫ~fI~g>5&Q`| +QfhljTŷY< Tex1$W4'CA}>b{EƲzga +~J?%[<3:v)T596bM}ĒtZ>'&%twߴP +Tli5J] +1,ht)ٌ/YM-P͓ &>ў"nWa m'36j^0$g@i;:_o_6׈:Ya +9`N|59m~} abظ¶Pr-ٛ%q"HĭCFjǡI3@ wwwD! "{ +tGQ*FQ "fE1xg/RY̋SQCmMN/, Xluyvi(qUgNKVMh-5vc`8aM\"94X6Lz@';[ВSPjd#OֻJ,'EoJ6n] l y<9_L >#B%l^Oh;zd[ݾNy\ORl<ރF-B2;F6!YX"4W|oM81WF YCOA8F9!EB&4{$Z='x㝾VEPjyT K}+ X\:{0)i('o4ļUF;yU:zނ^-+#WμIz +RaD{XZxMiHH> :1G@@jA>`2S͋kKw]5L +.q,ajdJ e9,<.-u|Q2}q>}+x,8J\eXαpNu}2w1Chu翭K~:9GZB+6=:ac 'W!Vʓ"[dঙJx\ޯ { -SCtUt1Rq)9%)Sf?P*}#NjȞ1H-Dfv~,h>}5hnREF)^2Y!O agx}RM>/@KRK628j 0~%X!d~urG~RwGs.H OޡOj]Jl{Yw+$Xr zNDU3DwDDj3sNͺHIM n |LMW=rr.! +V83D#sUgͷ`'-o_LUxv6l]}EpVwwY2:\9 AR\. +fOl@tAstLM3  v{pMq5q"@,w-|]{^|QuSf8’>t (޹^(ʙ r-@y`AЧV$3=\@+|||U,JHwre㍔nz'AR[3zSpM5@K h:׀)OG0 * ]q̹-6) t!o ] WTs']Mٚps*UO[ϸ>C'eRZn8|uo4As9캱 gp;|)u<.jAM?lPOT:nWг=w\_?+%\aۛ'[n|D`@u Mp흁Xn"@dh-Drhk,CЯ>;ݲǾaZ)-=9Ѹud.z6mZƷl+_#,Z_ܫg5~zjԧaJUB? +n5/Rÿ)AQTMae:E?cwpV6>\FxNiױ4s##b:-:U!L]۴xOdw<^~n{7r .i?#|/n]W깟0BhWf{.Oh˂=ŨOm;7|:BKX毅VM;oƙݣ lvfyb&5<+;:nV,OzVk B:z8B.Njy.Tb64&iaM|o7/GЀ#p)|)ׂ]|+kv:fY5o}ENԯ@t gN#ќLuFXqWxv?5·s Emf{4#ycb9/ ϫ-XVw8n +i6*S;Xe jK/vO_νѾqF9wgw¾Hp$azz5 +UȪ݅Ĭc򦃞S^_z`lnA*2FzM>Z¸w*wjO|>UW_v0Y@>vJ>>6kj3O5Vl)]l=[z.L +I6%lSwܲk)? r4L&mݦƍ3 XImѕ|lg|?-h9m񈁗X͞>]T]#z}chrm;k766JG.]~&2fGW &S )gbc0)t uHXՖR6 J'Ԉk뚴'BIN8V*[/d[?X~϶5d=rTwT|.S]<[gp:ڍL>eQ`mJt/ Vc?Ŭmm@&ۆ aOiZu'lpח]airΧ)uߦ$եS9M=o)+R)9uhSjNɂ~ύԟ?]~O-uFXB}Yv3S`:z~[{f7q0stL^Z:J]:lSrjjFtf}䔺fDf@=H%\9A2E^5O/e.8lu<~ԫ* qEm<ļ2}Ƭ6I0Z ^:۩ya~|-ٹ4CD Ӳ1jaUVYm̘,|)C,睈@TA?VDZ*6#!NżW;YK+kʯ-# ^۵z ywb}VƝ爯nY>vWmΥcT˞Enj*0(ȓ;>0kIN~2cqN=vmīluRB8.d[fMvۑ9!:̶W*R`)u;`ZeR\qRza8wisNVoYo>rkM.]s|Q"$)oF UUmdmZC ]V}M#žRK\b؛aH{>'n?(Ezf&$d~lv8jNk10ރ^>R"Vu:8qJ)?PCRLsSڔםҁ;s2 WeؠAQeaKe(GeI ?@Q*dOQCr( cX#C m +WTj-mk}4qDqnci0,6f8gQK^}Zx(5KTUhrF,427tBL+bő6'~ulեr#A ~1WLwώSYQd,g q>̭) @plZ|'[< )|Q]~Ejy$shҪi=J竓{:/ 줘YcnTV+"FG[Gc4%hё {RTo"쌬 L@rGϦٰ=CiL6q> +WUO\5Xj`'@MIpV@U PN5L Ѹu{]3vŌ)u~̱iRQ*cT t++wT8m@ENf |*vVH,=Pia8X*R1v #* g'wPij>L_H@.ŭ@!] E')xS* +N(0G}` )*A[Jp46b: 3"alN6k<2ɱ~I=wCp7OCA`ѺVARŊmbq'rx5Eݽ|:O~. J ;$P=@ǟL'~B.A n1rcTwߑne½l4Qmjs!|;QI,}6NΖw{VIق'_3p CI ŞHWrq]h=y"|Od@Tn:Qv('| s' ҁ_ 4$~Y}#> dX'㠶>&o'3P +~-ЫK4=L9} }Ba`j#`[VsBqœ_QuLn m_ +mZfT_[pr^jrݽ̥kMQzia'fP%|o'^`pZ/V0or9oH$s[u-6Y݌,(F^>Ow1ˆ޶&&lom8Fȅ>C< ?%\p$0=z|v.KcLfv;TMdG1>9 v5'w{Z9 fsnv]VVV}bs>4I5.Y>o0O#!F/|29͊"zS7iQ_1ۛbwSR)GxKn '(%:&:K;4֡Ђzh:zite{A2dtOijC9M}]nSճEtÑvIv? }܂]Y>U d^!L:Ѫdþ[v4]1Oh`hbfapӺ< Q*цzY%; WaJZN %wdCaDž~e=LwCV ڳw ƴd\MwzfMQ\6πS >u63ڴ{OZ?j-SCTQ[{U1w_W)#_y,w9+#3/*ݗ2uu|&x; gA/~*69%8zX-fSOFWu׵Җ̹sP[`XCV r B:9Y%UʔlUW5J]M[/AV<'/U(Yo]I͏smmƔr[Zil"!6mS%/ٛ3/C/qA_]͒X=&Y xf9{{-Ҟy|zC{\̥]I 8V߭ʳ:⛹?,Jbdc_ z}s/`M"dӬtmG\6 +yr=~6]gaWýl}~;*> +(rcfDKHސ\aFN̕VW!2>mUUe+KVٸqDdbɋ%R 0l[Ԛ24`\L{f*2YONѺV3k-e[X_}OZ]ͬw2yV)sXóu6XLg=3&F ?`Hq~zyk.q9w'wPmu5n'ﱛMI + +W$N3\JBq&7py˵ݘÈԒ؁%sAvE#4}:ͯs=!7PC';!v&0FYĹ~͟p`Գq4jA7[D9ߔTeOq6n +%D|*bɑ30v8Nb+G IL_8o52 ;d6=~1/}k>uOzf`iLY vZP + .a[c|e.Qa}ߜL:}kq3g^SJͥ2,lwy=SKiö+^izdlJ7\Q]"eG^b<#nVO)l3E̶w*pL3S!]Jp~ +ׂw5و|ٛߺzoճ$r`n:(i\!s CVN$(Qp/xY=l[LjJs?X|_!')FE^c'=yy w9&עs 8 n7w?}جz37UlcK71Y;9sW +;&Ucɳj̜h(7/Vq-p:Y+iJ7J|>,ֽjtI(:ْnd+LTrvYhCU*CnZ?}TVg^e ro`?*1e)a57pJso+թ⼸E87Ƞyr#wE䜜W\vgR-F/kv[Ҵy7CR᛬yEvu OqgU^L1&B8F2?PR~_?yXrs2%D $"\fxL&}YigHO(ᮐzfD]adž^z^e@nZY\*FqE:4ls|h[,Eg9z=X}COBK1'2nħkS-NQ`e-W|(%{f򱩁I +'>  +a#mSx7WO_+3Ŷh^eu:o7};,9/2U'oO@2 2Fa+ĹQmsl))@ifs.wdPLW NLdӪXڝ5\X)}!O_ٌNՓ*?'2U@c rx4Ce9@[3. y69:y @// &vi-ioNpz[b/<m?zK@ѓC~ g/BxY0wFC{+هc&'NpR\9~Wi?>*4تwm)"a!L]Zh^^\b^zl :9:s<*fRpՌځ]Zƾ;ݨ|vm +,2Wo |M5̟Fn¾qo8^óu}LO`F +Pr/ske^ʅ0zH8'r>i$^}{щhߔԡO \Yߣ?Q&x|'e}eU} ; +5ˆ<7-0qbf0em.KO⁁Y#|8CRj^)lO6)Q+PGo܋lARң]x:~1[Gфt{=֟LMc4FH - SPi{.J/CҾD 蚥Xqu{滤plSM= ǥWk*^ (ׯNbQ\l r3`A_g~_z޴Z9\{v:6t]>xn|MJ>UӞj^V_:N0 4b3~8ra:\*A$OX:Iva8nd8p鱏}AxrFJxs՚cO߄;xՂOٴ]܇qWu Q줕[\uuKh?ht(݆ \^55cdr3m.ŞЊmnbڢMGUO? ZĒj<Ԫ zz,t`P},غRj̖}uS`͕;l\ږ !}R( w~y$0RjrUuCinٖެD<3$*0:ʢo0m3*Vz#?TP2]Wk\1`u˪TL5 ٽhqo,qx!mS +} BLMlLpFWg AiXptut`ճ`ECZ}x58nT^~^V׉:̮eZ[iJ2,:inバ$5LTKg8B6<ftNݲ>T}ZhQ 5bjf䩛WK~`eV>VW>B*^Ts+{*h+K+GE<u|؇&3*G9cџzP@&l0ֶDlWmyZrǚ{Ik\}:4NՔ:rHEkB=u&xӼ!<_z?8hbahbð`.[m,H,eP&oV'[[>%(쾤+U*{VxZ= 8Q$H8|.Eh/W7:YΊzMvOSֹ<^,>I$؋S`zZ \OvzVȼ:R#MZiݨD8ɗ W_;\'(̷*/iM46t}ٙ;;R9C#~ӜhjR4'uQIfٟΪ~~Ot\86'N z w2[no&}Ŵ5?YB=y1O#9nRB1 +iL_,G|o+bD_|-g wh7kKt߁SjCͤɽ,49 ~gNV7QsB=(+:9WlRk]*NyZ(*%Oe1d:cȬbHX:)ć!p;]y7:z]' H=FˏCtN7s{}^ j+Jļ -'ߚxi㥹<,P}TV[DۮlP"*6/&Des(:}zh~ ('a"7fgN ގ+ o})\q8ðR$ < ɫ_eG[nRstrA[Y/̗᷻E/s[>1_&c#MҜzYJ-Qu;35fDE$oʞAOR \pU~,zAY$sx\dJe,娑YnTvTʡyVRMmVa [oqU;8x:hb(+.e荜 R +cFiq}Dn%!UH{8_ +AqX-͐^!Qm M4*JUsf;wrs||fJ֏go3%k<:vn׼Qc;cRa'- +Ͳ&$XQ&"Pl$Kl܋m.6ZQ:n".C)yeY?P;NEYf]!~9geRȤZv&lNj3\ɼ8dr*1_ +գwSSRFa^WeتFrk¤n+"g\\a~7'yǹ::(tsv +Ƿ\ IJYot?U0U p 1>Y0JX `86 +; _^Wݣ^v":+Ѣ +aJh=weާV;]~*ZzI͟ҍě+(cd6A)2 N=P&5(W(s)e2RP\uK`v-:-Kj;XfrH<4򸎼((T0qKxp*lCkZ?hU Ag`]$@E LJ`+ d>J:f3@p%oR&S“c6敠:JrseuTʰ:Sm +2dٲ[tou,F1AHp*&*lj2ԼT,e䯰R򻜀}R(Vjv7ݞ=6k)6op5.b#!6 {)\dfT.S PQ*Pi]Zo ϳԜGů  ׀!?a =N88@܎. ɲDcq^oQ|T13c4rHyV;TҰ# +ȿߏD*+!6Ꮜ[=ڀe#@͆7@2y5"t.83ˀN>EoXf+Jt~2z*׷K-AnWO?7*+~KJxg,Lu=9c.[jժm +P-óC$V>8Gbj-!OR9oQ7Mv1g~ ʫ~@pMmH"?a߶^dߌX>kL>+=m(m +7Aڵ5KLzoPӾ +Ǔ;HK?,?ͼZ?9WetnpKtSW/8oG>'= ~`'z/:I;ioZ¦3Q/(TR|bB'wkc +E>{^c-']c8[W;\p_,v#$تmQG4+;8{U;xܖ~\\Dԑt3ٔ(HNyLTŶmSFQE +EQ/+wɳV|Gl7\8🬁C+ ~ lݗû#W61fmt=zksϗ x, }'>2S),aEr:b;q8~ Mc,SACݾ<>a_Q%%e x8}d:oQ33k^h=f\ԮN\Y?x\uFUWK7._%7?y? 8v):zY uCǣ0f0j1=F!͟15<ɾ? wZ cV+u9/;d=sjDK?Pպd,j +T;5Zm.yr=mrEFNpN|g/Y6gr< =w{lM+mѮ֖yu6rSy2lLƶT!'c9CMW(Z5"s'wL2B>~{>1dEl==VPA+YQ . d֛µǁ,?#i}qO6¶83Ut+^`x'Fr܃˜۸t z@,|1(F/=j +p5|ibP'˷T3¯/n*NK׍\QwCwX+$M89cT +JgFbX䇥JONZ 2JNLp08f"7',N +!M{IӮ@s]Mbk9$׹s'yMC=G+I()~>p:2܃UUYZ@L ژBqEՎKwۊ:N1UH/We%vFŬE"]kw~jk=0'k쓮^5kt~rwrJz1qRW3#NKжOKskv6zӃ6B}]ʵ,[SVA,*5l.v3Q,߲>ڞWsTU:d{Ľŋxo4 +yؼ9`B"A;djG.c:~Yq {eoinں1_ 5Z %)@A8{.N!(.sA=^s@ꊿn .C5s 8 V`|+?G#Nrb22fQ4d6vv 꾫.aO-E؆t'7lh*űwdrg\{:Ӯcj٠I:H8P$t 1k| >?=ЖО_~8i2Inǎ{ˌ7y*9^յ7c|͓({R}ǷU`_;].[i´29DnoY//m9zgz&̲4$CC~gn3|zٗә3jE}V44#0]6{oQbfqXT:\ t9xkA +Kً H+Z4yL+j\5M-ih?=XkcHpW]'һ~`Q[تlM6\ـ,*ntnGx~xu I#@ü4bH=g{U'h>&V/v LP=$>MA t}^ |E#ُVҀֹG{e≲E?]# [(U+AEZ<A4-dF4oѓ7* $Oߞրܵܓ@.E9)@N#xZeXɷ:hgm(oi1cddx"]2~vB|_<ހN> Jp:ɥzLPzہjo;bxJ+:<\ +#XʴGw{1/Y;@?-__v /f:A|/$ȷ,ȷM ymN{YZ]9Su2-Z8[F/8<wߠ0BGnp?`<۲XmYr,.G7 +&X6V\mMVٟsJY!7ð#>}:]PXnkXn?8*ڥ((;mch/+nD`6i d}VŒNɫ,+KÊH𘍟:ĖOGNǍ@xG_yW.gү[mgeQd)΁e5fPL/V`)oLʐRB_ڈnʭd7KP̬n_m3AXSrV +rU J(\%d P(To͝RI' +ě(Yyj7JѰs(RJP%X9{R{݆KfWJ$ORn{mm{M|VGGzp rZ5&뵵pg#KQ,.䓧_l`2[vu޴.RtA[5EpKc) ܖCBzӛZ5>[VI($YmL=E㕠fN%LYeyM%bĶ"`X 7Z;#d;'W'wVMnXL +3-Ȋ. a`!vf!Jp"X}L^voVw"ʥl1õ-ר{sW :˛SKRvGD;+Ӡt4\).L/WJ!Uɵ>djh3nn|4~ti'0-;;ٔHizTVEb +ޯkrF\r]֢09qs>׌ӕ |Hiu= +dq܎7 Qv9&@+jU +^9]m nP.obڨJť2$x42欴tm{) @p*]-WrO~m!@qCMjqUr(1l/`NN\1luKpLEDJ-NGax6y%xl|{y]tm&`ynP6ZGyhiqF +Ulu\+ j :7K  /@( r [O;@ؼt'u ;vyWp7V']R1wJu_*47)NQ@v?|hqm'_]M@V0'A}`$qN# Ȓ & @8)yK~(&0 .yrJ1'Y<ɯ-aPNNzq,CJUΟz!ICƫuPP&,"L({p5 [$\h7T{ph TΌpsLm@l "6ֿgT.x|3uoD'@oWM(:j n!ݑ4îgeW3)&Xs_ +dә\ Sp. +g'P+Wʓ;}߉BSgtF<`F`pIf5.f]3׀0[o`]}}GU"^ϡb;]xO2$?RRߪ ?D܏^((L)(L(#Σ +(\PGPp5-BoDlyzH༆FѓCLwp̷+6]%¢t4ߠ4J,Pn%p*+bRK??;dW?R~~ܶwwD7/r+@X7@x 2Hz BgLk^_ Z_Vѽj![^ J5Ҿ7s$izYi'nѮGsjG49TN;ן;NHn~aofRzѴkwR_^<9G?9qptag=tItH嶇s jC;~#\*'/`Yy 쑿`W@o~x]I'~3Cewk1a\l\|g3L@:k-|>X,qua5 fԆQEUt&ǥ{Yr1P+"90(u4kc_C*步o@`s#fT~ ٤wSn>M6ssv/` OhrC?2Ia]d) ,[ڄڧb˱Yc;e{׮[)1ﵾs>4<}={/Tú1]RDrNm趌W_nP.>/}񣎞Kܫ{\,PIۏI-gSMjH ZfЮNL$Faoܢ2C[Zv-?syk K&7_j8KNԎjݸ]XX{c+_xL mZf%0yŦ#Fs^bGާf_{_=ykAX۝{jU2.i_d:jV.:VaPivvIoQaD}c0wt$mrpN?ý^mng -u5Q'^b*r::!H@ڝ_ ĥ*a<sgnue))1A[gkgSF{ͳ!Mx`B*q@z~5OT6ҮSuV~3ji%5Y^9QU,+>=_Qyz^w3c~-}ځLmuEu-{}\Ng]%g'H- -՝s#)|U>hUY:Ye68ǦKcK +I[.as_ ;[FaLI仙xz!htJՇFvɉ66Uoh\?),Ǖ{Dv5+[~dl9;*RD5U)$Ru]G~G?POjen.b{C,_ _]axHOcNNZ8n֫fb(٭8aӕoŞ' ѲHI!tZX)Th,MeBoeWF$Kq@ r/SGSwT;TgI:n6_i^%Y͝Z!͎UҮV@-X'aJ # +sq"qnjm&ݰ|ݰfIpx}1Onc2C,U+/y^b-+ Fw}9l jlZn%8k"}tKoSp\bx>VE.J8Oga2BR6J>S˼MF?[t t+Ӽ'jiWڝ]rAp*ow"ЖR*@8d0TUXƴZe綛:#ǻs0{4:ܩr.BCQٺ!5Unr: +[CkQV(\bX]XAlҭhdӧbJ%y3{ouڅGV%1.=t;B/Eʩּ9wr CIxZ❪J uTܹ/'P0Nr|hrRõNX%yH@K匽YUf U = $(.l]Π|(?-9\hQ5T7 {qvSҍQ"K׎y3o;8K+ ʔCN.QLkđ5yeĥš-hUι +Unѥ\.L7j=xf[E9ѡd  :ԥͱ#cέخ uÐd#I^뱩lEnoRcԚ<.TMH`TkQVGf A 4TrfvseνONOZ/?=iףQN]{%{T'i gma5=yiF1[*iv>.iΦˈ"F6흐k jv?l>҇67' #FpxSe +IUg"XWԣc r rFe)²^Zgvsj7 fv+JMK̽Ae5$fgķS ׽~ +:EЊ 5AlB &j$Ra@ ^=tuU^1<ԉacRӿ rt'߿ ݦ{u#!-)|iynn*Me*(7"krXN|vsI܋4@EaFH T.@G8As`1J0: V@J(:oھEnږSɉܠ:VXvwA2yyR jlXY``/$ 'yklfORh[ԸqkP7aڒEOG0;6Sk#h̔ Ve'|@$c `!c vSxs3x}F`NVMaK}]m~f]eܒ.qP2cho0^ @\{: SWuJp".L/L W@쫙'/Y9AD b@$ N9~9y^+d{ +;SFZ}tTMSĻ%xN_WOzZ-TNo4 &51Hp +U +a PTTF.m'x|%W6Zljlo=JXϟ0CL+1o _7s[@%@FНZ!h;#&ݘFnZ8Y k :}zL_"wNi}Q/+9K_0\O k}tKdN)d*`0V RdCX;fO)E endstream endobj 41 0 obj <>stream +*]4ș."_徯' )nJo[[q$`/bЛ +&x3P\P&߫ROB=mG{eVgU& 9٥&ᴂ2r?ߎ߿bU +.U-QpmwAA_AqNAqbAxhA1F2OQD瓏5[}n}~%݊'-Wz@o&6i@Li % LЇdiu}[CXy>?B zӹ"''Ï*dG#v$F܆{1!n9>78G o lYBUQ=}njm\ftg$؆B}Ʌ緡 Y'?КېȽ1K/ia_m3@{dȁ_#+uo94ֶ/'γ(j!m2mQkn]W阩./gcQZ~ ]:~rm<3{ =fOnSO\nFCZߴ | |t/SEmU Xڋ|̍44i䣎n!4iى˼q)-ޣtmITs8k/xsM6ྙ?05k~aз+~{ٮ(!+x̕#O`􄍆jANN>-A]뛤T}NdU97S*x02H:namy#aP騤o| +ZWN]#no4J!M.z`86m {59ߍֲ?jogYj) Af"m UiӢd1l +7:X]O?>[Λ!4^ Tlڲxln(_5~m3h:^;l?m.(-k+-m/6Y>ߠdԆDý-%xwTxʵ GvV/ߢ=ΰ<R{J#ZjCHuz9;@טv;k|"W+[3{t;U=I]]GH< vv G_K3K˩h'`QOg4 uo)ꭏktF{DVSP$WT)~S)% +Nxfe<2KUB |!|n}"|\w ܳvܣ83Ӽӯ˪.[9n{Iz~=ު&*x |+-:T,?6\.rC#G_TKEFTfU@Sܘ-ooNJr4 +3h"75sa{CMZ[jJ.ˬ֮˾#yGrd!7ؔ\3w)}L6E~ !iyRoȚV͗NbAn ys0krź9ኝ8Q.M8g<J[:ĥeYנ1JَbZtȮjlJږJEV"|QB0gJo]iDN]p`9T +*et҃NT۳6Su˳n8GjyehJNѩ%k[͚D]Ey3S9(9"/y.-~IX!:3oeVҾ@%uIe%MǕlP用 ;;n96`YwaLԓھjNlj o8;k; :,ޔy "Z=jj,jfҫ@-*'`R-]}~Y=ԭ?)8)϶)Shў\t*z¨D"RJzd. YwIs)$a.B9XVcdqOH1$Wh7z0EI8n p׽f-R1MK/ фo.oʃ}i9w3qCoX`p SD%.j|B_.6q3LC[tvcz SZlsn %,%#mSQv[.7VEOVj"u`%!g\ +/ϯF_^6'0&[OR|XC@.ErsunݾsؓOgۍi>Ĺ*tzֻ3Ie^K:ch6YI5Ӏ'K0T/C.ϘVܤ6Ӓӱ:Px'MƦ2 +AP2VxF#皭ɰm.3gEhѯ im<қ=>Uv6 qvz Vl kZB|7SsrƊշ\WNbI/KkP~6|W$7byޠ]qCfMppeD95[Wə˕(Ly ?Y@ + h b^ dk{d  9엠ۭLn]`k߶β#e^E5ͱ3RTٷCH؀b}74v֪s{FՇ_j'2/0CȐ\a`'RFЋQ!= X" +S֩ +;̚b;`8"q@<'e@bs 5@l$})=w:]*͜[}7d𵂨rTPdU+44(~A7, "N&p  n!HA̦  +HonZ|4XiR_uWVŇ kVY澳e +2/wpt`%t2GD4]`@$!*& (+CMPqq(cTVB^كTJ, N4(7ȬlS&'9ڙ!ѵJHn6)5UwpGFh,b6< X'-:xs ]@SDhI FD4np+oBvmX`7^̚Gw˟>]-Pm\+ ڗ=H.J`8` ^?1X-C]:,C`hb>N^J/2` ̼4s{D܊:5z/C׃b.dqHT+w4'RCL+91 J2q%mBTJMLa85XJc p8|szVZNf1p8tq`떐ǜX>jºb aⶥaʳeZ;M0 ;=ʽkq=lb<Ql_g 6Įmr9|V7@`@ asCit2OӎN΁2׍2˰ Wi 'o qL^ !WRk -!~}6~bۢ8)%iz}A_ #m rBEP@h8:Bb9°BKR5 w[ +Y?L.xR,#d9sd%y;?F_nۯ̆GJn\|`%c$/:-&kC@x 8V,|5vD<=d\ ŏwo۷6J@ #G%/l$n[\p ev6>uQEVDx_zW_6kʷUva*@]!P[AjGVGJW}t.x43{Xrf'jQ'EhY劻&)=W(x?DaAԓ.`u7p'kL]2J{hE/{; Vcn:- ++/F%O^c]0(cl7՜̧IHVD Eʲ!E)r5.k奥i4I9mj4BT'A +GTEA|toٸ{WhJ͏ %:+YQyשvnQg{fj;Vl8Ɯ2yn i5I#]LV퉕\΍,!Ib\Y㻅Qzwk*xM{W59c1VlAS洒r6/΄5ku7m~*p&L6䢻'gxMT(M$6ϥӼn٦ݥt;Qo1IKvy;êQ}`~m3B ++UfY:(PÒirjhQ HM*lǙ~dU+)$f)>DU]Po]sl6Vf)+׉"yV4rä9[ 206Cr$~)t2Mҥ'ws,m+VݧTm<ʻUo_u*Ž,=_z3oв$6#L>XJ\"q7WRI)qee ݣ}[ҺsWR玈6N$2bJr&lg,*hڀÐ=% L5)urBO͹jEݬ&ʙtN@MlPN]&eETv aN<ԕS>ŶXv?!]D6̬ڽXGb#fP88}֌ӡ_\(Y.xB~ڹX +Cu1k+jUngTԩs1rF³ czMZsggN42=R#I=i +"96:e`LR+uLPH3C&z=3t^W؄sGc3s'=5N{SFDm&wg4zZ[FqqBneO!"n7 Gxltcuᱱ /xY壎Uh'S88ɷ!=@90{dE>uSgNGZZ8ݭg6\!s,J?M6@Sc\" +iIv#e7<=Ц˳"abr NrEnٽPE8-tD \yi޳^ib W7 StNh[Hz (b "HP>z_|`\h3tNhu +mbi8eSmxkxZ:b^n&A[pPh8:VD;W+mgNTmVd1'zX !6 SV 2 tk 5 ry +j, [j5zE˙QlVKֲ, M|ggEj?bc}'~`m(!A1Ib F%yĖBD +ES< n'wI&;T%H:$8|D.BfүǮv޽Ґ 2@6ŧ&Ԙ`GøTbpj$ :sr@ =;0J!:d6! 7"@U$]șO 5@z.AZu/x|1gp86B9@67T/ vP3(k*4st5W$5dIQPLP-knN4esxљUjR&cDPi\2]ʋHLzv|KsK^ IA@Q 10M@w<n3.\ns@pVAgaw}%M޾jݔk~Y6I~4^c (>c_%Ú`PS~Q!l7^z",} ]0#VL;6`0΂&әFurT;; +;Ait|7c>wpCyӣb 9#YFk{fGo(U7!3+( 1LEׇ`Gzm>W_0f#i);|[2kҲ`4S¸e~i~Ё!['& +A[pg|! qr}gӨQ5ˏ+͜!7(+*_[aI̹:HԿ7x7f=;>B9& +_ZϕNIb5Y.W]]alBv Li ؀?Z o^xyq܎@`\,@t- p r=c|5A5O46>9Py 3 ̀ (2DFD'?,,]luO2ۗK}K|'m<18s (~ {r-zqsS7:n4|i] {ީ^ݿK[~Yn߲wӂi_r +JHF=5 Y-JF$%Hx⡃}*@͏ uS nߜB8Np;:`t"ɾW绛e>]zimyb=Zr?5Wq14}ʖ'4YPO(FNk9EaLn%}Tv3#YXK]1ϙ|}_-TBl f0p]Bt_ٕKfUl?}mA|'ؾmQ ('3h?W?WH޼|kr:kSznRP;\ 8/wO;PsUղG"iijtkMo; ]rguYqS$tP֧uxmvrA^=X/{$YU=5vk8-v>o6x#8JO& <|]O&;&'Wۛ3ȆK-T _RbՊUft\oݖۿz;կorhަmV\^W'Ŕz;AFjK5Di U%ET=/We4(g_ѫ;dQ.[]xq!Kݷ>a_7-_*cT?XW~έfK;xŎ)E n<'⥔J^.)Ql%Ӻ园Z9zd&zdoRg=+)l noPȽua w[}WaW¨%!Ka9YjiX(<)ʛQS&/ڹGadG a W\x}=NVv)n뵤i¨~<_ 3L8NT_RpyzPM>OVR7Hr'ٵM)+.ѕk%Ώ]/vnɪboO[o-B*lx˟l7n^X<2y|sq*㈩eTB5KS>0u\F +tEw"]>H$7KR_{jb'ʌd"w1[?2uDkw!'0M l"HSt)P#SbC̏W*D\*=uMvn[Z]*rr#:=KHko~&RtRH +cN{̵_ӏ fBG]@z[p&$esDi?k17^1K[=)eDv+er.G6Dķ \#&~2نZ#fPR%w>ztLՄÊ,#H +7 (v[,^ +&EL&6HC m; t\m%u yVpVUJێĽ>fVHnwux`&ed,VwLX #|=ib?f@.t(ܽɠ<:r!ƨiHb +.$ Wdg5WcU]խ+ZI1/{ڎR--gM}ʔ"^ݗ;AYr:]+:-"58Hfk xX'8zlC8)"b T Op6|2|:L+~FudMyGr2 )rTGJ:. shϤ]tɱ d9zGPuUVY"" ƞiC/ !fu@cC@ԧ{@44B#kQLc LBڮyڱNL˫Ptn+wEG-N~]YMK&7ˋ%G@ BD 1"Jm +{(@)@jJ :mjZ~]wozpE,-31U:91=YMbWB$D?Q@+WJ?!̰b`f +w 8jѺ*G!r PDl  +?ir48 .gpvzuLm3!f\TNhv@7TS Qc`ڀl@ӗۖ4M!} LTuۦ:N8EmV.`&znfea-g ٷx<\x̀}{TDc"`X a 0Q-&gB.  1+?qOHGУ-h?=I3Y)A\r'6,vq#SY TN{ii%!J j`3b ƍ:`X^sto{6|`)82EVNs&!n8 ܳ>bn Q!=_XU +?bTύFUX"?\{Jj: b)Ɯ[pn z\)3pYwbk6 ^-;%Y +rj՗~7?q`!N'fY|?< +1c+'[;46|K1E\:K;%Lڂ_*MW3oΰ)xyz߂OhV 6B ֪ SuSrOKlz-5jhT8:٦ç XrLP\ +n)%Warn +ļ}閵Dm# w:s˺dI}u+'}?leqmIvUW+oᅭd?RpҭO +Ly OQ + Ò٣/h=ɣƘt>`nq}T۟vz[? NY"]tľA*]@RI Y+"ؾasu~5 N|[ݷKsSv{@hS8ZB\gb}䱚{c7ksDc_^y>3m{Gշ;ux>VdFKvpZ;#{$!,H|`S?&iM箽ڣѫq>`W rև}Krձ}^@sw{oz% ^jDbVxdjYpOK@XѾm07o ~w]?tLdoO]Mvfiޮ xI52mCyt&o;sj!k|gjN4^^ȿ[Փv?uL0maMkQ[6<57㻷mO<Ȇ\)C] /u$kFRSqcUvKi $|PRSjz_#yOJ O+=lnx6[q<3" IfUj#>*`}#,yFX 'RlJeo[%|W?^Nѩz;^]?SkLW-FNUUU]PJR?e_n_ +V08f:՛g2oo[m n]Ư<N ]VrU]]cpZD*ș,>\ .*^QhJ*Jfy/"C0mbZvQExERdl<o1t` JV]d@0VB^&'=x ޟ0;Y= =SC{Rd.ĈbuVjޣַ1rN/OHb"Fl>8L'!gaZ>H;=675{l$eSC'rE5˟8[T]2ޤOOȈ Q[|y)X`X± $J"y86vH X^ `LqDۜ,$Hq&:.TK)Zlv\8'ضս҉gbQ~= XNiZPj{=6^`Vc䦲HzfR` @Fuȅ g\,@nD> PMnti/ +VtxωFO{0MᘛZ,bҠM`/QZD݉*D;IC00$oPQI zuw-ZzS6Qzsh,Zͷ-!O@j5o)K@91:{dOx$ۉc^/O) :#~`l)"LLPF4}jԒFu%О:Yx$@"@L`x 0Z{R -^r y\rSFš'QȞ-3Ԑuj)_m(Z% ScBa .DgHŸd\f83ZI8-`! +X̛+^(_,785p͖d=JiX6"pL"qh\cqje) h8LhGְ!8;C ē8W[eH9KON;pO|A?x +P7 _mopקe೘52 gAY,z/nȪZ-1 +fKW팅qt$QC; +y Hte237@Lү>FLZp/ӄ+bFw!wbĢ0D \Q$Q#erآ֬xΩa|frR^;iT(T~{ap7GEr PR (:{ȇ~܆5`goG0 1IUtUuT*4%F.mhi a8BJ&'!o}s+i&wOFYvрE&J'b +>yGKe!-@7mtTttʋ+3aG󘮼~))Ά)Ԏ]5I@q-{p soP Qg %$*#Yz97^00>+N)NJ|y/V"pF|}m)6Fl4-,jC2R+~%dC {̘bqXvvT(1 * .vX.g -iCnhp}5کhuB|`UOiCbi4Fð1frڂg`\gW'yN]h2n'γ^d{>#ZGuOWwߩO{j?mpuL ڶGCDkFiFi%Wlsg{]UH #l'U:d/F,F҇Axάs 01Dbס^[˝JרjK]~, 2^&v|-}qz>_5{U>VIp|=Ҟ`ە&R,qj[Ql=bylv.:{O|%+fYV^Q:/߼sѕUo?K^*%_jV\,[9+dN_]6i'כ1 w߸@++,ڕSj7L+fΠq_'?[S/5>|+Dz4CH۲[|z-~a^,盍1T&_4)gxhwLM&Q2(\/v\zMd?fsƦׯV~򿹕{/W_ Bξ +8l$s%vӦaaurMxI,f_,ݼۿϷ?AQ7@GrI; /.ׂA}A\id~NWGZbfs"eq1ȶϏI/7{5 <=W+ZJwfv6+[R_uv-vt'TUd2j7Elwq@ 4wS;Pk<]C6J%wDՅS@ݬEHIWƬ h,<ᒻ@m&0RB:GFDnq" jf(ʁAQ x(+T21 + Jβ pSԍw|8[J~`²S&UBa11EEgŲ|`e1:ZPΓCRzI1v 7O+E"svRII {Ca"h]氽S2:r,¡A!4E;dF39RANn!XAaXntBǭALj!F|w2LPx_:W֧]{V)AUkL懚B- n4oݷ.ixN歴U0;#k??:3{MƼP];`U-Ōy8-8V&vG8.V5\]W`ߚNŢqzJ +D5 k'B믆r+x +b5xl R]^>zU)d]:v}#7sZQ(sC(_LraPIP0z^/~+%4- ٤mK6c$ X(sW{U Io-|=/Dr#i-RY$Jq2AHiuj.M50etC߹ٷ:?I-laEyXf:GSfNw팅u |Z2#$NI5 _XQ< $K~64-e&lmٗlS3ٕLz.±}HmM&ADlu/8;CO0Cì/KCrUrb=p6h,L24JF +lo5C$ݢI$Х-y<$CIvP67PZ+Hl1 +BDGgɹ 5ǝNr(^iܖ-?m,Wk# -[CޠT!kS~.Í ʸZh ٞf_1Ki:RzrOy=iW(̙9 dH$p6}B)&ë%|'j6ж.Ô Fxa9VH|?h̶|^7{$Yj:p`! tip|+vJipMk\6RJaˆ3GazXKzy49S,NpUZf;t-~{eI0:5lwzh +pЉ}[ I7q!*(I0rܮdXoU6N)IqggJQn-HlM2ݘ +9ۀJ FuJfhm|wXgf'V;] ֦)JX5q0ϧeͥI3Dt!\"Mk 2ՠnwUsB6SBH񽨠ۇb:(OjB 0j?%}ȣ VV)3mF`씥s)\,Cr {|P㬗C=B%l0i؁j[VG uG0P"UYmVJ?J}֢8@`OW(H]g8OW D2r>C&+!j.$1w3;wRFYb0_dm^p=i0M(LBh2Vm֔$=BB,y©̷0ۺnF<'d=tg5cc.[Ô Aq$ldK&΢Pŵ֞HR>uus4Βʓ¡"jEK+NdnV^P t56DbʺaY$+)I]X62lHuG3obtIhn[jNۦ7gK: es~ƱB93Ag6KΌwc̍5nJj gd8$"( 3LpͰ(% 6Y`F-#DȅQ[P@w4,j67|ofc&ίJ"ې0H}iԑb4MJ0?A'etݦmSmyub\Q8_ӗ-Cʯ't +yysP&ISIld>#>罙dpT*=w@G2&(}.M2r8I{Qԕr911Ӕ*z/ādx]ᄧdՌ,S +AmuCNu;CiMwV'.<<feN\b +ÌA5{B ټSuҀE͔rDlP/K3fa\5 tOPI3Yw(( +s3CD=Y ɥB\9lh$P #u;jvb5*q ޤl1QgSϝ :Nc=S4CJiU +~(]6liH8Gcsb=#s_e;'[=~:?E,5${N-zNfN䙆Z:yv;k5&F3UiSy31O:3U1U͜ΐrX5UR 9W%T ɶEtJҮi#Ϥ4 Ie&I}3 .ւR렂R,?~/|3 \ƴQUla&]'[?SƲXm垚Saevj=b|a&:Y[y6μZyURʂr2Ƒk,ٖV⨋-a9ӕ#ڒFJj t +u?e,ՏPvBWS1."KAfְ~v#Ŕ91tl$dD#xu2p}Ug@i,P^)nd?^'>{]{rrRe76[6uQn?:!iMd ^4LJt^MT ΋ET~a>ʯv2z̓9D(&}^0^DR~cn!]xSsj*˹ͩtz%,Vq:-ǔj?pI%Yasxm% C'YT7'OOO`r.9_orQi|LÒUl+[ހ`0Ɍ pi'U'm.^btT(O"*y::صx̻z^E{:ͭ v)[~ ,ItU:LeRq\;$D< IOS;{n@5vTZs匧ί:|YEQ|Sc==xS&xXŢ\mUŶrUkΩv8EI~a +H8=dq̓:$}K+S>FtUMtÉE]^NBUsYI\ď*פu97*]*cS/eќ3X0oK;n[-y#Uqfi#Luw?z7o,{_M"ms6V_xnv/)k%wV'p,RUJ~/OqE2G ''MH %`S>bfXg(Kk2 jⲘǦolZ^=Ȝs[VŗzQXFA'ۆx[@JQ’R}l'?#u(7.۶V)LXΞ`YJd" Xr;yZi0[^e{Yz2E|-皣a1,4Uw5A 5X& 5,omصGx(cJh.RsV^ɺhZcKo!*q|=[,?\>t},m +3՜p)*9+C|?,_u1'fgHnanf'6v33YF= ԁ&6P[ڷU76 5L:G`8ԧR74:`ːBD'̪,?5,z>--dy6t|zMXW tDz/c5mU{Ҵ@O)K`sFG m4Hn>ss>ZZlW.?>9#DX3hf?^}7u8m|mGdsV:f-Eț./eNY lJtbjA>g,8Sz;ӕӜ8dD&oD>VƋ"65eX`r{꬗:ojPGJ ns4NqN)0z!966UhL#StlxWF> $0c7m7>%Cer pQ|d/s[ +J"{'BQU-zF6F% ܜl6BY2JT9(p73(z7ތTkŸ74a6K +qlNٰƞE8kz$r Z9E(q~8dgt#|qcʺl` +82$GQ@:"KCOf}3|8)}XAAA(LCiچkijQ]fiYҀu&",Oi}ku^!b"2|ibCK">"ulr m(\}1*y1EE.D#ش؇]E}mǸz F?pӵbNf@Yoo\:.DL3Β}D=95y'i9ScQFB4 +BNJo!+b߻/ܡ^+&e {YsQ ̅NL.g̈c|POWm[4s=I;Mt(/Dΐq*jJ| 94jSiyj尳miנZɸ^7tĽd/p5?& c\k-z^[L-S1)m!w6/04ٝPG +`NuiF>NudoyS@d+]\}$zg'\Ug]eA椶Ħv;'`L]T8@V߄ +=6btd c!vm,ջE—4i]4{ MYMs\7n; )d9ÛT8]7}=#gSw=KE :9e>Õsv{^z a:b+;ux%:ec rFC AcXi"pPH +af:d6Ҝ ~9|Z|ttR"^BE_FmJ/|,\(>ebHbRw:1Aw//s fx"fI oS![ua hKrYXK,b+ą9×DEA3E+س]A0GCRr)G"mV$ Du@ P Τ./Mrj7.7΢bC$tŐ$CG MNiizv&U`O- ހrK[Dki2.^,2ԐfE{% 14L _1yq o$ Ujwt~.c>BP? 7B;  +L,g^ Y9fd(n?wB,TLx|01tN:t 6TI+~=ۛG`IܔZN;NEopYLd?)NmW@ǠYZۃ+<)/fBq&z],ߜTZ,q9GLT -蹥^+uk_4>JsyMa<&fJ*Yʒ0S_wu4sf񡱽`0HcwN׹ܻ7>k|}`x™j#%.[e!_׋N^o%mq.WE,T!71(WvUpwUJ/SbS@%2j7WGL勭rL-bO&M+B*ƶ›@z!. +Ν' y#;M̧3ɡ;~Mx{rR8`yA (dv<T0M㒀a1{WNwشt;2*Ѧdh;KDCrJ7n2%իA;F4L4&kw裸$KܢѢ\T)i18)u@QƏ0L_ է3 v9 >yXҌsQuH(ur}ؒq>_H=ɑ*c qF^l#B嗒l_6f5s60ժشzΞiI`}O*l baX75gxLDAM|(N%M0Yy,PLv1Q}&q:oPG4c_ +g^NyZe?x*J~ ex1FEQvjxخϖ0s@{ K'IM|dzC,UY'mxcϺ@dyUD2؞\>ƿM' Md &JMQ`Ret +Rϴ34]Y9CrJPxԞ({S:Sz"Ҙ"xmCOE)Ax5S <1)+Kiy ,HOFrH]ا9({k-@c hۈ 1EqV g}5~G +87%_]Ҵj\<>ʠ诌#9] }ꌾ5Gģ\95t́YP'G<]݌% . RۚV[mH`1͋±7+ޮ$`ʹ:Ͽ":@? اa/B`]f  b,]z{h3ؖ㱣^]`4wRaICf}a1gK [fD,h]7Jx 5Mi,Ev8z9/yoF!o G`/HO#骚.:cGʩ(* )0nYo}{[p3@[jnn"hkoxNwsmB~Bt՘ 5ux3gY=c[k RV:ߞ<Wkve#fn0B ELǰD"*_5]G>]+ tiOsAz5(Y‹c*2 +)x\:kA:XC*I0x^E".yQ+t>( KNhNm|#yfܤK `gy;of[êֱ~s33]h@^?]d4FAn=s| <4ҫh,7YBۺ-m8LJոpHQ!iQLVӘs&()&] XF25݆wyӅTiֆ`.e"VE4< @F;<"OH2T=GUSZ#wG#KG'8FFF=햌gflopMF&bܯ̷Z/qJ{ݞʓ0/PPqp:ir4.6dmϛ [z[O P0?ϫbvp?= +ml&oimZ (_ofm0XȂe. <'SofLlϧb3v<Qwq$<:lSnsRlv)淛ml{` H^fpKbe'Z(?͟//wH0ڮVI[MF1m,q:C(ӷΧO6'*=.nWS6xqvb_V'*|496eW#ysnWCtXw3>،BA3X V_S  HmvG]Zm³@Qc./fsJ5DјFٞNGܢB[S$ ~}Y{90'" +l@LOz゗g8M3,vu+T KJj %?BE y9n9!%D 4fXDžU;NLEs3/o,4jnpl. +d:_誇1 Uw;@+O!)Of9WI s?bK}@/A4Z-vF[8 +?;қYCF}nMYM&0Ŭ )(UHǃw!HQ,n"*f L#ElB5 +ea}1=W~7?ÿ˿__[x/3iQ//_o7G9?՟__ Q7w8~?}I\6~W_/____- W?o/"'}utXZ3v7|F3C TxWnG\mX࿂?>_]ћ9za< އ&е429wvnB}8  D2Xһ(mPЏVwŋQt; kfvݝwLZ^e8!Pߌ/a(lG<{A@ >ߝD>4D^O'+fJA =ZS$6u')Y?ǔS /;by;ZB$*Q}6gOBl&ɬMce+`D5l]&$BZKⶏ˸r`9$D[} `T]^fmxY=6$J.ѻ&nQ[j U)Q:`Nya4XPvԒ[6rP6<-0K'Af\g"(mG1[( ƣ+i9އEXyjbyH'5-XxRIh̼ۤ !1-Bj/Y>TA:PBGWÌ+D%O<nۋ7Wgx:dzՎGjkMB +J.-␚QbkM` ,Զ嬪jU*֚Zu! XUz"pWֵ 5=.Ex5yeH^=Gs(E, JA#iZwU{!hC Ԕp!W/VהؙJGbiWnH_4Ij2 H}#0rR[Jh_AgH ܱsFZҖ^Buim`̴2:q )r5?:4=,xhiG8sfoDw"^^oLj~:ڨjHsse6H9鋢4&W|s͚;kiPS5XP{C +օ1Kd(u]$M> SBC'|"/ +\zRWJx5H) ۃ`!Xd^M黶,.ZR\iS;EPH ,#HuP9MH#[߶niPױ+ ' +*i01FxS DkšY)?ęj(9T-P5Xsua4&w+S) Z8njc=v!Rj gJ'yp,pO +$5.QD1ˍL)mαQ9<4-~Ιؤ8I \b*q]-y )v8%kVL-tΥ(b_mpHR(k@+|>2k^D+7$z*p5g(j<*gvh'_㩄fc޵QWy] wE*3.C ѤhdjԢ< Ȭ*mz:˛¥#iJ==R-tx9}鋂BqsL[\ZH])h]&WLvҫ=f.4cRvC9w,pse"&f&^AAxǘsH&/RdS"yBG`LJ֚uZ@ѷ^c7 +ao/) KpoujZX|B .M0^%>Y(*uGД9%Ѧ+ӥ +NՉRc#`m`WoE5DIL?dWZ:,tbfm#_2bO@q>ЌѫX%E +5ېMB> 30\FdUcD`hM8O֊J~g!qCLy֡d1IFt>2#_Мݛ4cz5ouycH;?w^bWZde>2X}{f7MU&?Y[@J!?)9 "ibTOKAߢ;:\^W[ɺɬ20NA]?:Tb"qgiAWXN Hܛ&Ʉ H$o(ɠ_;+ +쐓[s܄b_aLAɴHs ,T$ԣnPm&i<:7Pƒe{ΗHr7H\_YI]\G~p*]PrJ047^9ͼ-g ˲D k"j&$K׎$෵NӒ6 V$$Lg[[$Sr 6Vw7&P»-\Xן7[;hpCb.Ae| +dV] ^2,gX{X<\7O0sGƪAK8pN.z`9^<ń)ԵOM>E1*E1%l陎k +J}:>b Z^=6K[yR\OR9KYHVx4:y"NVѼ(xĻ}Iwt2ͦ[= 4rL=W \}114aږ j]{g8jfٷXa}CXdt}:.^'nڶ%WZ7pH@GZX0xLX)?9?3f)/,-,b~/V0=`7#m!Kbe`)G͙'e"p,_X*0 =hvH]4? 72mc!3qx|B NÊ={,].aG"m ÌCSHӔ4,c.Sw=yOƏ (n,s#m*/e4KIV$LGY49)O.+x%̽p8[)ٲ5jVnR;{}ąJX{`]/"v5Űx+ez^'G=CbWƫhPn$-sp8 '7(<.xʆmo¿۩ȸvW +`xIYE'&1Fb4ZKFcFH5qyG C`>jBE׾D } +FyI|+T3R\ meCB15pZVR+크\C.֍~QpA:Ug۸pH53UqF̎uϚcß!8,1;4Y~xEgTޓn.0(Q|sI@bBd:H/.ڶ1I $"s>;/^$DKaH|-8v&n' 0RT-#O@-#b S7sL "r'0kr: +:jG~Nݩ5t8 + ŞNZ-d pO5t`ݵZ: +:Nj0*w^h &(_ A}6: !3f4mjwZpۀ@aŰ>sw S4ӂEp>os@].{+fV nM\*Xo/H"m%fi^H8+I*R't 4"{!-$ Lu*zt-#[EgKW 1SR8^}i)xGq*2" ӥpϕ n-As2x]2 ,9| VlqÂgI"4q`qs=`&#vsx4`]9P3釚\:3N+!)z[cM2XTw%=U4|K-4FnF0ˌ'SnAz?3V*W'i#b&o1"5%L<)sβW&Xa#M5FYJ 3b7@[n!]9Vb7rSw&de}E{+v V%\)dmfHZJr7٫5jyCR̺ٞⷭec\p0iTծa7ńӛ^E\nb.=(@DՃp/$.]:$'NK\ Ecn}N g`g3;Č\A=L)0$z)ykq+p#Ĝ&#kYwͻо|F:' in}1 + ZZ_#? Fg|ppBJ('*@C̘ +y3#q.ɍ muz ¹왲(`Dc&{D[A#mXyJ$S?QiHJ|2*"AKh\TnIbw G(vɏI()ß_cIU1લXUbk"_(ˆW(^qUbɳW[(G[xM +:άӋcO_ޔO ' iG#6hܦ|~I(Mx}/mXM0w#4cbHM,%tl皸'ro\_?ew{n(z_?U O+UwOP v{Rp +&lW?NHtd%pIw,~O-EK wz/jɏ` c $Uc}S`W?qq~ς'Ǟǀ)d?<~|@*Jt_?1~){} +X>CJZW;"MuzUi POc'l{~|ߏ_WՏnzUi뇕kW`_qQ}D{^ =܏S7{`=/~V|l`L >PnOء&oox߽Gvct.~ʭ\_?>`v{hyY~\}o*ww:[ 'bBپ~8R޲j' O T_?m_W6E8/kD=\_?aH[婾~vc6D_?(bO>aPȜ~|յ>\{;Ⱦ~ ݆kwl搳a|%w{r + +a.y\ a";Ōa)b^^@1w1LR墇I"LhQ7XsnpP2 q{R[RIesx䕆,Xn*9d;*}߹f<;ݯo}beyi%sԨ(yQ|uNwD;t:(*j`K֨0^M;7n"i(vy%RXޑ-dJSH)YxEq8J:SP}^;pI$;,#3 :UX;%aKH*/㉵2*|mk *͢]E^b%QW~[wPLoq3lA$DZ/9,FXY[K(]|+F|4'xk6[`Nv[iqR\emdC MDL8(T:m0~3|Q0J5ָd,5VU=}-` x=0ӆi.MK*7?"1 9&!//[cOE$mf/p,_ |ewWB zDܒnĂezDJK5IG|_`fj7߬o6DܻMA ߶Mof:Y-eä6iRp3r-ڞߠ~z(/]dKo6 +8!`ww\luM >QD4٫5dC*U-I1nvQ 02Z:ցDm{ Wm'W"Xf+t".9H0H,좕G~RZ t:H`~S5 VF!>@yϒ $cGy%A~B7>V‚hxy.7{'1< +y4]Tй՟b)/|NC+F[ ); +At^ZRֈ^m{XD7L*1K MNH XIikmE`pMBӘh<(.b.8 Z}J5i>.&v )y̅}Ò8W2@lR\DF[&7@+y8TBcNcboZI7(b̫;e能V]W?k™CA[:L$,w\ynHGfMcL^U( +eYIFC ;vZ""젷yxS MIHؕ nʏHo ei݅Nbt2`WVsC +70BE67DPVCgyoʈHۚBF\Vgna>R_"gU*@DK!MvVϝ2#U\t#=ro<(ij^Gv- &1H[Xֈ[q7Tm fKy1EJ*E5^%%LYdUj})bjRQ\_{WGB)H(6MAY%FIY"8Ӹ9߷1γϱҭ&y 0C$esK~CJQ/$ivuR^oz?]lYX?4'@}Էv6kG9'Fn9k`10MAւCO;mf]>o1e,y ֣4emFдao UMz4uBK4wqJ^"g_mx4ySzԹ/skѨj_:V>و +F,F5xu^Z!O-tF<&+HҸ$a)f+^")ycv >G% bd uaL2˯87qɶ5s &A^.#]naAD4c.w+dDZ `sa6=Ch'n/N-.fzn:}7fjNMY&͸˪v*UmhWyhePz)X0˯sf氫Y4^=Ip}خ(_̄o/zA%*r -|wuw}c`vM_p#2;a㒆Q=i_h*2JBƓPdhu'GƉ$f*!;ZH_porTT|y:@oen.ˉʍd/Ix R]irFKc뙊:"K,Nn3| Ýg!;to`p[ZMY{PL|=_һHvT> +(躣~EÉ.N'h +_ +^Z/WA/tQ!<ءK?/|/M5jg(Ak }?s#C3GEqmO;. 0X\_o1H4~]qת~/^~UMYW0 ^0m-RIO,u9rRZ>%s|9*Bf4 1ɴ)<u!AkR,E"sny|}̽T}jzԘSc%HG9kfLaJ7$zJF +VĆueg&ۣ$]V=q_=P&V2Cf:ycSoZ^3էo_Jz̽Sk*g'{eN/nY<ʰwA??,[Oǔf.iRdkRΥ9+հo8}_XƖ"*`麡V<nj;_V .q8=qS5\΀%nRAPJG (]*J3. .\ 0 lݕ?X?^;g7zmB!kV7Jwy}sOwl'v?;^2AWU #~VlwY5؊3ٽ2ȳz&]P/rׇ k|]82"q V3Q>S)o-8QqjaȸO`||:Y37ʿX^|-[NfVKDi,oRӎyHQUjXL4y;y81~V~&Y~tmS%W6Ũ ˛VV/Tm,w?H1)ըSRUI(wg{[+hWzaٌcJU:x:ll#V9 aj'c4J\rZmr͸ .kj49Yggrwj?Cs~#=Ko qDi<|i̸Q?qB;o}ߑX6;݌' r=;ӺՇIOf y_XjyYIT_rUO;NeQVNW_ۣr '~e2{RdAx/W=9aEx̛>@> :")3L} erSNy``"xO(P䩄R$\7A+}trngD }.OƟ񿬾"dim,H{_v/-3uM6-ܜlÍګ~r 7jgPL0 o-ayf!/UCbeDX_ q޲|3W\14,:ߌ#2΢b茳|wgf/ybE1)c9Qgf,?,: 3?cob,:c|3|3q,",: Ȍ|3uqo5ocEE_cX(Yt2{2΢3΢\\Ќ 2΢A3΢͖A-ھ87[(z9,:cl||qoy7& +7c"k:81zX|'cr:iuI`i;zP0miCѽAiL>\gDJ9}(3K3oCմc7(o;70׉;9ח0*`hux[郎NwB7C!B25raD9cD9cqeqeD82QNPqe4 Ts(9r2B8g('63{;K$ʩ1{r׸oc9BdS5x}K?oNHsBe: ogHJx{>oոJ>}x1ڇ~ế2Ϻ@fw>å_C; l')KxЭïKyd ~i ~ΖzG>JsDO/w]h5g.70EXJ"c)yKyK]0JvbUY i2_ˊ@T((p%'b^dI+Q( +!"ԩx(yQ"C"**)Cz3v&x(S9 +2VncgR\ԹK%Da@7w¿D伨B4"!ZTEFaJoQEI%)($X& EXouaiDAKEB(/bRLJ +^x]ҶȋjѸT~ _X0;)D߳@ 'G!}Te4V&i,ZP`!qephʩ1;x/Ez*a endstream endobj 42 0 obj <>stream +%AI12_CompressedDataxٮ%ɕ%GFŹn;plq3Ybl$.#n&7+[P~CA$ߨ ̏m;w*«qak~gynַ_\}ݝ|ŋwo޾Gwv&GtyOn^y~;3G׷};o_ÿ';][|kI81f_k%>[µ9`Y?_/SׯD?8g7/z( l?ʽ7y<ݔ)?,ͯ滻ܾkpy{q's"?*| 2y ]-ŻBS}{_pi&}~vWxϫϿי?wMڙ<~u?< =}LlΎqBnA>ƈwqVL24a2nXsW؁:/_=g_W/a#_?a@L& 1fl~2˟M$/-&ݼ8n_qO1L%) iG ~gOۛ7x֋/__fgtrk|5>9z'O߽9[ۆ,η\N>ŋ??>ܓ-Z~#NnOoV?Y~yYyzw/^ܼ=5W_/_= klqW_&o=4{vCvǰn/NN/O~_G_A +|߯o^|~<o_?zyGٻ7߼:/__A]ϟ^_}Ɨ/n^B~\Ͽ|u4oi!˓[ȫgK2XZ):|?|I ˢ-k߾x}w[e/ypsL|s"{}8g~s P[pr"O3e\.KI5~IW|O?Y?'g)կF~)]?t>/߽/߽9kknRxs c]j.}wyF8I{IqD5>w~w_ ?3є NF7Og׫u8`&gϿ|)׷={x'%/o f'sz6?_ϧﯟ?q-!?c: ë^MoG 5KSӧBG!ZL7Ry#f6?N4g_xrTIx!^>:eg _NY} ̂vۑwyϧ~{7{es>O\ɯn05x~=/kKmö<`J"yz!8### +/n>3>=g؇$? 놦lʙ9Dz<=|۟503O={8<67taL˺~"BOI;{ /O^~ ehg3>gPuprry\V.#ט+.庐\g55+ +rr9\F1_WWR ˕ߑrMJrE\^.eyW应Bs$bJq?r|(Ϻߟ/of;j.s,)5MiS&;iLW2]`_LeNSJ)|r $x/ S1D]1\p<0b7L3'|{ݕt3&pDs7+{i/@grH> d͕4 32fl`4ƌx9^#Ofy,oYta?W./,>rܷ?0AqK1 [q\1Ei}J>nyd8Nb8@;:XDEcW +1#^R犝 f.߹l*sțKiuֹm 鱁gQϬz|M-T1(08TRLMY8E)\`*,(B*Z[\Yt"B,1;5QQeyv.ȁ2R-5J6mlo"(Ⲑ;q:@QeiGy%eH={Y]90 H $ d;8Bҙ EJ,)|4IYNBREXNlգ㖣0yYHPZL*v(W?<~;+/ݥňƋˋs S@ /…Dz"D&T92 GADg'~A!(BPZ<?}6? > Z> !,!~37| |o1z\]u\g%q1G{g;3*,E˕rUm+++kyp8"p |]zqL3̩(c*H+k]y3`sURTx9]A,f1?c/+A؄HؔsMf]Al;v2bG')v#D}d>_{- ߡ[9*4 &=A)?aht aZ@ ģX΀&+@dV`Kb">Hx|5e8UrCkl i򻐋'1s(V7 +rSR#A1e[1bQr] hu$k?kN=k; 5,ae<* ǩ*28,z5g?p5ByYq_qop2n{ͽkXzzߥMxAy>}Mp I +\`%g!yI|~z~v~~~q~ 8^^8H. + \Kw!"dt9_^]_^h>XB9dE0.x^e`/7eVl;t=z)ԉ"4A$+|܎|rf,,ccGy߃1 xF~k}//p=_Ape r.S$\4;O+5U`͚bFgmvfm&DcDk7υ\u%h.krv/{aIARZT i!luW_#\:BU*z<#:ŠdEUPڣʹIoa=o}4w7Nѹ=+עT MMye+O#sx'/7Z;o`o+uI] qn~z#~~xE-2OA @ S%͂ҢP(JPR) +Dj +AE +"zq\R29K,4. 5,z(jaQpTQX,L"h.$檀%#7L` a:MSAOg❿ u)6jY54a͙P yES\5V[.җ++.W5Lk^uvu>!ٕf+H1seW6Oj`6U!%3%%³” $\XK{NSqGaQ7@Qe9wqUUvPZa6 ޠ8T۪wN ,+lb`5:ܪX/mJ]sgם ɧj(Lo,ϔC[}3/T59ȡ +.?kbW]e wYS?r| /+>Jk\n0s WW9謇09$@3ӌE73n޵Otqэээ%zW\ndqt:[R/6>%r<W>mʫulUK ~IW 3yIXֹn1 +w*:?+Er2M2rǢM >Ve#ǢM-5+QU,/wPA$HjJdJVR7/JyUV::*\DNU.Ɋ=8V_!>x-B<, |wHޡ'zWѲw ߎ5ѿCw 9<Y~PgD(gdMy8=$镓w:#(]Lls?Zc_WǮcoй5*A+u.aSGjѻζWO1,ew2sVv?rkؠV(6dK԰1G)Ԓ(ҷI&9RQkҖ(!ֶ¶B.~ U$)/X(]3JJ<, aӳdҚJzՄwHړA֙{ܽu޶oae[B& M&7m}V*i1ubCni,xR{ߋnk6q%b$I%JԨ\2Zlϒ:Ink{I-h\ \JE!IdET4TTQ9;RAE*P@EuM aUF-RKE **i8WWMw)paS{e]^[`SP͞E`2} x3<lE=*݄H)0SƷIM!u&kyM!WU q4o[ t^`+t&g.(BjpQW$պz~A`0*DQ*" Ɇ妖٦iE9Ke'@b8tu:dl4CU)) )qkтwP1">Ohoy2\k| +qC@aλSM=s;wcVzw:7^,<ij6&|Lm1p 0q}L1c 0>&}&>ʯQ1&^`VAt0G]TڰK3Xk=CoUK9dj(&pX1(T[b>kƸfa=v~+_~9x`bZtU.#ZN <۰j7(NViHmMI]i)J<4 [JxgK;v_'gPErtvxn(r-NYZ%NZ m)+ָod8 :^(q +8A5hr̚-%(Bdp'gCE w5q[6|}sof#=j }XK",omiMxY8\4τ8a^`5M!c%]S~$1-*99W='a~N|(wKKO /ukNDKaA_䃺>>?>1VeӺkL4C:j6f Y,Ap;@Z3wXrx_l#8M$ceь5F4NGk+CiҴX) ****ʰꤲR^ڮ)F)(׹e{qA}U?r 8Dc${:݃Fɾt'wW+GfQ%CiRYzĀ/Sm91m'<hgǽswer]mE"N&i3wVoVFoz~fJ1Rn8^[7hby6*] M`? ^B?$z8B^_cױ&[k%mMh|=qZYAZC7d})`cC՞nYK9z|pzxLߏ&ךZ_X'>@I]`[#O%vT7n#a͡{On-ηJJXɋ}~]+A*V'0M&tO?8]*Um /Mrj2}}_vіu1(Pd|zQ + +̥@X + y) 0 K̀Z/`0R4BJJ^ӋuO UjAWZ.PjT95NŶKAK .嵇MqƷ}W 5PLӿۿ~*춟^ +z `Q3<nݷ XnK~ /}>W~w:y3~(/S6fgC'a{ki :ʷ3eۥz9>J9wt&pGz՗Ofۛׯ/;hgH&3fڃf ; #s*?䯦~-ćw_g?-CKuvUދWo/?}wsݷO?)oo_~vWؐ?{<}1]溓Նaw,Y3l7#t$'Os ; (>$;80g$t3Gvܔ_Vxv<{$F~sovfpGG<׺S c-$q2] Ȩ~1[OMn1|'h5'8GWO/c ݴD<LLX[7ٽſf +]0i 6Z(;kӈTb;( (֘T.'12Sq +IiRbi7R$Aqngg] .nSyQT1 +*~N||tC +ЩXp2L)SLXIuYL A;06p hL 1۲:>'`DZ D<C!X8%NlȄ3'$A|S4 +pr0Y#^lq-s/'CtƜ8Zf<qGa9q:eʣy`p^A:̕OJwTd..5Z.Tvc‹F-š'?a]ŝH48PPpgU+21a!x)V ۆ-f2TaXe!QbbKhi5$8G&`bEߎL ATq|8[!`σ@m 8?L3nd)jCL(z=n&{N3:ġ!wi4*>żT¥d6e& 848񎉩$ +" J΍A 4Hb@{KdB(۠H@„X +9Ӟ _}w<G:+UKa  Oi>gAz'A + +XZ#81*UzS'8a[!]茣.'#E&#Jqktj`<'EMdy4c=snۉFmwaKHܘvoñ sB Y;[@@IVɏo_|w>?=}qeg`gs ӷ_X^kJw_q~?y'sOx#`3NLmx +[)`/+R[9R!Ai@̧ثbDЄJ*(ت18{FXZ1U<2rztAc]V:#Wz mJfyZ< x;Γ81i'/'vL9~VIJCpʫ;l/"58>> "] ڥ79&+ *0^]Ac31q,zn9Eix݁\@ Al=X{J$3\ُy x_~KL,/\Ns}^xT>(Dگ!a+7Gz XP<OVe>Hb|:L XMbk>K" +a +ÇaU [M4j+Π %֜%䫠T/F['Dq; eŗ2}<8YxBRZcicҖ(l(-m2 GQ,=T&0,@Si8-!fvkTROE@ Z!(`CqL ʣNy8bD: _ƇpD_8 +:Lh$(0]4_m:ӂ5 +,pif5T_VJK=DNU-^Xh~9tGX3 +*n)A9(u# D8j `y27,J 4`  eL8ҚEC(dҪR p{1GZ 0/jYĽӱUωHz ׆4E*pq $Ȃ1n +#E寰tJ2B I` ?MfB&Pj:9N<{i»nTF@bBV ./Bs/4Ե$Awb0gA3hL?`¬j6!{c"XPdi@ZJ)l3tYJ6܁(;+ʵ~Dlp.2:>͖d=jdO(ȞvW™ [-{e2h ϤO8vG"%ȲH NɁc8PB-9zj9qaKaCҞfJgƹ6p#O^ 9b?̈! +;uZمwSc3Cch΀i9N ]e$Kah&@d5'a& ǀAX8R_2AzTPР@ݮtCу&sٰy%ɢ ) A UW:d + veQ6R QzH"C#1։4N|&D*mk/)`01bh1L`,ٴ.# ~R0g*n4R9SRdLrQw n)jqcYT`Gߙ9eF3}w&R :ⶾ#$Ǻ1:34U =hKʣ; TUR9vVlEyI9Sr*gc^kW_t%(hE(KG0$h%h +Xnъ~ +gLLHt +Q8LnO(Y|*T!A +*UaH +6ZD1)]rWR=@Z)=BEi)]Dyuu#t#^+}GOOw5^SbB7wJiU1{7wjnl4&R0G6BR teϖ3Cq*Ӊ`T2SeZWWF$lP*HVTm +Bt5M*YTqkTՠB{H]*ZYAku׈Fk+[l Vf\޳jJnCЕZL*]U2x1^+u&蟷N * Qyʿs+Z׌x".mbj\E2kcɡkMDUur:^H2Pz|z1If@KiHI"NCYcbr<C2YqgAdtɴżI@X`p塌S̒S͜c~4a)#-oW+DhNDWW eh]z9[׷DےnvM="(^QjO⸙V Мr窱 FCVh+6aڗ3\Vufa`U/BXӬL*)D4 Jh[ Fj !Ht {m 1:8L`Wp3TI)F#'= :$,5|Xy稵#(duD(29 VC ?1ps1^@\xJ-yʠ, X0i_ LU:J8";?]t谎(id d K c1*X=<]K=GbVK0#R8.va(Pr9 ` (M=.,իDt'mDuñZfgzEsԪlrFo_mcae"e 宇_5= +&ojGC7k[.#k-6F˱e'H@N$ }5baMAAYNj0:jVNsQP*&kqBG1eH51# f)&Kz)ޡՁxIYL&RI1,qt!70Z_R1'V5YPʣ'Xɭdi&hSMTa!ƲBU_`rDϧ%V:4jX~$`bQ]y؋|.)Lg$3f{0Lv$ lYw]?z<:=WR9P-&Yv]Sn`@,t ͵*0vo}++H("lMi6c+ 5!$ ;pt`ށ/H-} xlIR*;)ۉS@+t ZSLw%N1j=,f ?N<= CHM;+B@SIoxY. cY5]qI?74܂1"YC[VuQidwpٶsS .Ke5¡-`mԂGZԤfKZvJCI%I?hᠧ]+ͦ}!lvH?n?|D-܎6歌td?( ?8F"~G +#]UtKNb%#dAY:N-b,YGF2ЃJ8Jc4$5'T66&6} 6Njg i c$vJkdgBEҤ,=+%&-k&ݏȖ}oF' ʤ!s)%\]Q9(I|0xF,+ N􁲞D/"L`.$^xi=#z p eHk*u{! {*͖6 Kdq`Ƣd_ĺ,/i\Q3{ 1:{s`]}-;H&$D-.'[^IєZHRC* a,oP.(IkFLo違:$`K}8Pعd6r3+AVB %κgn ~@|hvO@6ƉO,9CZV!ï,("cbz{ͬjK9KU=#S$Fbߑ#gpkEO$fx.| B, ,ɺ-̐/q' S3rG%u9{0W_ " AlMc p'o)D<341ch71Y~bU1s(:2!$j n*Tj O٪+)u.k51@=x <x?fgU@ؘ|UA3YS27 =|1PO)6)ˮ5cE C[aJc J8 +t# %`c*Z>L(ɣ$0&yt=FSoX2E, ~Hޞ%#`[$SET0 Z*!RHu& MH$~1T yQJٌNpz5)[-RS?21gl֒jk2f#03 U{2-ά914u6 %iѶdɰ{Qe +&.fNa޾gd:'Íj紇T=9BMm򶼪KV|Sы=|ߎh@+7iɛ|ۛg18Y7J50^B  +)yMUؠ(z=xoydt"Uxl{&wMIjwz&3κgeTRMu +k8I1 xj6Mt\m`|#~N3e3 v`Cymk˘_9o`yME0 +n`9[Fb-lbPHtF4̌ڶ3 :V7>(Ą~k5\<-n70Šdq-{-l( 1v5 ҨD4ٙe:?zR%@,J`YZ%XOi'H&Mťe X6`C2AHl9px8QM 4`H h^"+.*r$Je+\-}oɍ#=JpVRԲ47d6I&z%?xJjж:cnbNDqΘV${Yt3M4Vgp3' "NP |8o> +r(O,#$eK4?ا0C<>z915t81Iȇ*;3Qoc(}N`~Re<~jPWFqum6}sGFrp=+bk$rta+1y#H.{,Nu_p{2ܔ9&' XPR.+@,VH@"sP42v]rۯ:q2cR +[ MK4bNL^ti ^& YtӠcC7Er+dr)Xm`^*eZqh`ۖb;0R-6RK $`CK16 +6ݒ{d0x[ʩmƭMӡӯrX;,aLFwR❢Oߕ Kԯdj-?Oy7,:jz#EJ٦=@_%dZ0"ߩ"zica8Y͓~%<CthX5 .!Q&{h(@!kKY&P_/nɲ,FQ nPA60"t6 3U5;j'іG..ŠjfE\2m]XhgP %t-7N=@eU7 )CO_QzhP '_A*땮LDVt7 .%w<щ.R[YyztM0^he31u͹9೨:\~ȯi]#F  qG3Ê a E3#JgQ 9|aC鳨,M]O,0Q_TFP;2;I9cQ|3{F[TG{3XށUM+ZT=\g;-V~n@--= #֤i ;ҵݥrh=92odgb$d;A|QܥAfH?+Iݡ{(yMFsUŀN*'0G*\1:3-%vb∸lzͧ"'9EYGZMMzO7gmT}UGkL,n Uײ-@T6)oTh[.Ѝ*Z[OiB\`9`9GJҪ[ӾK!)E/:\U]d_$&Վ6M Au>ib\XLSCki[<(JS2˨ՐUTOE1Րk(SA2^FU[(XH$׶6i x(vYfæ;KkqvvQJF:kOe 6QUFmۦ8^s}W;aePֱh!.Hٱcq\DѝKLk׫ۚR5X6q3hh]}2SX[׈"ֱ=4[F֡oƛpyM곬Xr)|n=ϻMzJ4gtޫ4wy|;&s}{MICbs!Y GL - AᲡÈ1Gzu:4w:dS"nmSA{_.oJR9rUemBmX7W _ªYa:ώ澫<Wծ# C5Ekۦ<4gS*c~0R92iAgd t4ih <4Udt"g#?S"JJ_R**Y`m)F;XHKml -MG W&)mvNi/ǽf4) ,a߬e,,Ht]b]Mvx+&v%Y}HyjЪsPK +gTni{K` '30\; [9DM2;0i(JCA7"5E- +kPUWڶjԽzmm#={ж1J%JKF,jmEmu(fԙhilCkCJҪmbjˏZ~F([V-}YVlU'JmU"KJ4*SUQg]V]ASXXԹ?u` ~]z SoK.=HJaM'`(8Xk)R7*@5e F2 }DT|#f K18x᧣rR9^ ⤏v`\0!3NkF) +JFaV !:$P?8ܕew)IP5 FT4\Nր.c +;w;EgfioE>1 G8Ett6۳W}"N;p16Vs3c ھr̸ TKer!X hW.D@J8W.7Vr] l `-^ޤQ{ +8siUc9M srlOjgZl8sTh^GQK葼IusX3i6'S4+:s>_9VOL* 8qR +G1WJ +-}.6F{y1 +Ibl}x3~㭥gU^'_hmvOX{&Mۜ8GI,~ddm.wZotcQ~[S\͍o12ӷ,I|_rӧO߽k.6=4]M`,hxl640hAhlex?IDgL,T=4OlB##3Yy(D58X̄ƛɂq!!1) %/Yv3%0kb#g 6MEa˂0hJ2psNqbD:FS1IL8d2|ԑ!8d:41YϱRNu\17yȽbx'cXس1#n$hG53}P+Gs;IzL aYw<ďElˆOVꞴuSF3Ia/ޞ&lO(PߤS,%' *m9IUE[CZlz0c5HgJȏLo/-Y6m qD6f-8!,=3$P q`܅u#*$J̷b<;⑩9'eg7[ڛJ.6 Q&Y^31{g5e#A +^wpT#qAR| d@%Yx /}\ ZTYkd9skXE|iYD'v!*u})/.#`Ü-$s=1,:- eTN:%E&e@ LDpjX<.+AaI\DK#NbmG Kɐ2w㇓uxs':pqy !8wE[~E?D#T|b3 8(?~<#WB[ "Ҙ blɲdcS*"&B[r`!HLy˼ɍMQXh|aEfػh,MA#g)CLUb.Pp#wmʊaz[RRgn˃>/tRtd'NrO߹CbH)ATwḊdRI\Tc1bbbzx0&5HVʦڒḏN>f'XN2 (yz)%:eE`LyYG$Cg'ĂځG:\-5@f{Q11P8}xm,̊:˛ff3&еx}xJgՔYz[R1ڜL8 +ks&QْO +\2qDD%d%;(4$%)s4 C ]b*ݱmuS@^Is+c&& 1! b≘,sy#ʓ&`ig\Xq'2eƇlh^d$m,J@ +[12&00Tk`^$8Q)Hs6EaG)M01Cra*[)cʆ&K2",i5l!-NP'%$9bd:XW3.4 [JLRo¦0Lʛz(#HFX/($S"a9p5_aG:3A.(z@'=28ꐣ2Y@ +:tsb:&0}3[6g[--Si'&&fAwjG$}2$`rK[H,sERg@J$@ +TJ/IM$≞_&+i1!Gv"Ɛ{kB5 Ls`K(ƤqѓX 7aGIAna+EXp jH|GA>4}qcO"j#eϱҘwd*Q\(‰-UF:IAN`8s4uhi:VY5P: +idvZp).e"_!D[뿖{E9<4L,A+y)ϚUU:ZĢ[T.taJV}CF2wը@2 iE쨚%,@iӶFojIZiBg8]椦F:DJEĎxs/ߵz<& b 3}̉tص)phsX!t +I`uIB0DfN$:b|Lõ#{Hy[˓XKD_7`q }_t0dqc9 Oǁ,pH9cJH">,S7*WS$k ε1ɑhxP%,~]s'ãATp*Ubg$*8ӱzfVCN; cp|dOsKcǞLA\M$}R&l +EcsW26mϐ( 7'euӌI(<}]Kop՜O.%72r]Wȅœ𡎅8#]2Sr!vpٻ3ވQv:ڎL&֦Ku$ax萎d'T"?G9:l o ++#&pKU2/՞| +X[3``)0n<̜ ~?*nE +t?LlϊJxyXi\fe,Tk4L QReu:G^G,U siCZhVD +MxGV X71º1{b-<Z(\%kb`&Rq H,`Z83pg"0m>b;"Vb@|#0I0.òQ +Z rbq6 y  ћ" PLa +h^PUa Ȭv@A طܰ O*`p Op!- ~CR79 2k= n +$ j 1Knc]C QX,U*'-,1rך_CYBBmmu,6GͱD( ^x-34&^aVZ,z ͯEQ2{sℴW`scP DjnS5+pVu*r|d-hZCZ_ZdmE  a޿?eB'[w"Sp2Ю6W׶8θ@LMt+@l(AM4|w +##"rT'\p(}ꤒBT$/fWJy 1GqY撴k4"łr6g T/98CԦ<,NK>u j6&l3Q.NӂT6l]⣑Ϗta}uA, +d JIU!fj}@v@%3&b(Y|j^HdJEq!cU˸O0}|7D|Wq]i;wbE"S|ښ30P/gwƥnyz`>v`{'RHQZFJj]AmUT@kt>p09 2MIKc?FXg3E RB gg\WfOZ'i؏TlguTLh5T.MZ (5ZfIYgPTzB&.D P.Ue*eR):CZ#IZD]\L +XITwUá&Ҕ) +Ji:;v9㑲c(1NdDsgٖ(lZ$a |jvD\N L.잎$祢D39XG9\/\͓ ]꤃]T\AXoROi<=䦔:4riФ`nͨ) +$wY](BI3wwBHƝi t~3}tQb,df/['2!N.r5' zc1Ả̒2|n u(VIh Ns7êxc>NxR'kgܯrvw:@c|].(|FhAm%oݟd|%ӞLVZ=Za&lK|[ $l MN%9# LA.G=˟855D_K0R-j`9)zC0HW{u5:!k<_?u}`;qY_܉)7 :LEoӣa *R[SY-x;zM?Ќ@m Т/0xl^h{/ :HgEU&$%DJ^! 5=e8B$qU0pb/p8S +8,v̴XTM.NڑJP˨ўHY +[>#KdJt}IЭX2$vu]WT*{s?͂G!(g!Uc#}lk,p~(Qs L*9Ow(/ -".یWv*| 'uAjҤF5/ +ˮޙ$mvД XyLA# +2eHDh TpGsMh!{!t%d#* "j%Ȯ/x?|ˑ|Uj}p + 1'XtEڥݑH* KyuU߿ez$WE:!e{zGkmFe +#ͼJ +K I s+y > +J"c+ĶZ +NC/Q$r:Nٹ+HnT d{|c( ci5@u@B#S܆d2T5 ҚBkm> {LYbxyG]_>u4sH[BZlȈ7*X\s-"$u +|? -mV11^O.B55tع=&Sz?%銙/ ''XeC{`*yg=W ]G Wrp'e9K-3;~V=X8T"@Es~=Ѣc6"CJR>BD>v h}d2$`7RRiFvtl^.6ZșqG(UAG{6avSH~:r9`5DH^f&30nuşfs9s ]9gså 볠 +yZNϠjRlU&HeA57AL hX8Js/Le<-{yEiv붙lx d{:j;o?_eoZ8ZW /|`47Nhٺ6r?u\"8%]Nd6W>TmȻӮI f% $Ķ4?|[ԱO;|UPnN"}\$54!sp79kY,C86-/ IY 39qjA]>zɧ[s WOԞ)-,OY o t1bJSɁb +4y'SI0 v tv:eH-LhVbf7SŬnTi9mC+zA] 4)VWHL+Owv +v^dWmk\DH(!-e@Mg5?6۸5n\ 0.[hľQmt|ZvqI-O窰LaP_`KUpr{Jŀ~%ϊ?s\?IM|)RSu,U6QVBPcN^ Ycf]DܦHm1x"j":T(UԘj{jM3ƣ943,|ԾRG-FroVF7^Is]- +숊ɽpkt}EJI_ fwՎ[]IUՓ!""#H~j>Hnu^ x-b2nFO)Ji/ +V;Vp* '.t0vx&1=TFQFSp< CA ?td"ׇYO7(M:nAѶ3xQ`/G|XX P΀r^T-*.lk =8 #A?фU,-ԉl>M?u~)7<8+ɌGXN@E:"˒6O0wQ8 $ +ÙFxo Ya\V +۸+ +SzPXnLqt466dg-`c!\חph͛O}wi|OOixwg M?Tq Ǖͨds/yR3DBs ? & .&1 O)Q/_' =  +wnX$/,/ pRhujT, Nd@vf&C-쩼 nĈiKwy$)wjriiQDVZdCG + ů!UeQ`,|NP$6yYT(!ZGՇ7 ,{wov|,]H<ߴ;3]M#Zu=֋^o4}2Fߜ07CP}Zu_#*͈}|qP + +9BvFJVk8%3p {לwSgE#nQGܬHq>pe{MN>"# }t>n㤎^F8A6u RiD;i2۝@</8lXJʼ)iYsܞ y/z728μaO@FV޻}' UB:k#9%y먛^./VG)Sgt!+ YG;LvOozYnC*~{K1M'438.RV|/tP-!crt?Mo2DMf[q9HuF/]Dd';Gs貛1ҿ=uxz5 "w:ѣ{.3t5 0@VGBQڙ3 隨nMTRxI(kj0ք4c3SE$6'_o iaO okx/fuq(5mS4R_m(ΉSY3N'{)$z +s:ZzVlR=`%3Mۭ`Ar,6sh ٖYd`SBX$&bZVc}ݵ>X"j,oס¯[nvy޶)Ի_GZwh I~Ȝ=~EAZiaHe/vbC`QY+jNlT~yI{1R/*,9YuE;4$J`VB$oT~uaJ /M xk%<07^F'\kx0 +LfcI&րB 6ov;E,G;%`c8z㤰*k +ܣyl0]lݏ zd3l,}"<[ɛghDx8OGpHO 5"!\p7ڜ)3 ׳wd2Y3C Լσg X#n6QLwwI[ |Զ?DƬädZ+zRgOfBG&Yo=$ɟ_uM2a+$>+x3nO )5-pPf:Tߛ/,ADPdH"[[Q3Vlgt8)=-\EW<8d!HpBqJ8+fXev<46\'.PԚDCTyĆ"ɓRcXz.o6^ d_9G͑`e/a.wXpՉSx̽ߖDB"LPd7@ЈG<`$ZMvVoB*FL'm0J P'dumvg-lFrTM%A`E@-l!"ʸ|Rt.bs$ V'#{S50B;/?s[fsB&T;HLxiO7#f Y3H Jm|xF#)7Jr7$NJyMГhQZċ ޡ$[B_.."qI&Y[Ѳg,9)miˁ-5X'I?g4dwс+3|KG]R] +ɦ)cm3UӼ2[i( S O$ kr /id5|rwL%GB1aq{k r(*w2 bٝr3JsS]gЇxmvW$J0yM帑=5Xv(TRb7_u!.tO],@#CIT)1$o PZP'2xJ!aRۙ82rM4lV?ۈ\Pl)K53hdfafJ +vL2*leIGDݴfU9q* f1KDkhf4a`k7>4~.2?|RoGRd8Xμә(tw )K탇{M #ҾsnVEd;:|F +أl=$ m2%pO3yzm E輙m73D 6A>vA"5,,Jl(>&6ۅ=&`(ꓬF1LAޮj^ D ngFY$؛iZSܟ] 5eFDcO J:7 &3/.n>6'uيe: r& PoC @P8' OrtUn"Sx6qkD rEߢI`gPEqGmUCG-S-@yiv^׹(`tQޛh<;M۔E{kqoJFΠa ~+|0듭ͤaont9DX +/zNq%tݫ4 ! ظ|:!k-15Of6K"J6T߆V)/bm'lhϸ+؅m;AB0PMj kw6:XmL_ބت!M( +!9LJe]E0"@pIVzf !mvsue][rG@4rl&*ΠFl#]Zg=o’X[>=R +$fv'PxdKoKy`$gߤxlK N8; z/ ęulx-d6;]9:qJ\" VJ Kߦ31zp=fC룁Fsͧt)B"Q}^/)2zLA]bM  MeBAabfNEV 0<,;E;E6}r(0߫8CniekʳX%k+\_._f*e֚#x +P6^3e[}Q+E-zSEV*M%H+J>v@tY%boT/{Kcq +5`L1A=t,{ ,a-JzyWP6Mhd8\6XJ~V]]2azx3aTP'l^W.lӗ +Lǀ{}zM%Q櫶UMARy8iU.כ&[єK_yAt5㉵[ !rH_ DcgCS1v`?eQ'ZHsb ҝ(\0 +jMiGG}7$ > `{EOu3Kɀ+×3z1PaWzxxAKR0#numI=ϝbYIӉFzўfKnQӻBAH0T;PƼtmCuֶT#jyw +83t4mcz>P-HdFk4Dzvwl/DIi u㈷&`Z(`3צ%i`H5F83kDHI-m>;V'w + ہءm5aJ?ઢZi +.1, Nh8_:S.'f9d剭ޜݤuln[^|Jzᰰg>D`w *3۽]É|\]suefSƍY=|ᄩ?08R +tTnRS摚Cp-Xf}$I@hE]b,{ 8<ði pGKܸ"?L ׬=p˹ag7Qx:34 Ti;e{m2PT !exz->V3S)nnS4`=t gfИa=4l8Sމ=Biviƨ,8)Tpe2bi]zhx9$+y9N*!;7\irj5>9K`rt${,o:cZ>OZ`o?oGf-x?u-/&|?Ž]?N@'As|ekeD*>nh*WNo+퍠 XkgsVtό1:;uE*oz}e %ap$x:n풘~*_ ++OGV~U.Wj9lK-UbZؼz9q /pv9?(!tDm"OWٿ#+=s5I5|=@jJ?E% q"uQr-'U@!2wSd dR>vHFK(W7z v& mdCKoLXFDx;8%ǁ`lK@yZ;ub5P\FES;]bレA|ݶKJe_|ozY4:)> U:<5T phWDOЃD+bƵ>ͯ3zw W]<5z*g|22!g1 h}qG|oW, \ {_ d-Aa8DHRNs`)زI"<;!Ϙ٠rVJ$*\˪mSNrLBL`T:JTtK֝D(kmØBzu@4Ι.aP`aEV]h~L30XwYfrʬIj}}zhU8ACMo4]`*'x +7rO?nfPO }r&P0I +уdvXAN"I,>#ˠXewl)LʳolA|4/Mh==w_%<²K~!*kL 2wx: BN05tRWpLm/qTiIҝۮlR@m/D8!bs?JS(<,*nad +ln-@K +*'mll$yHPIRCM'6BᙥN52XyU(<DmZJ`S~U:d&pH=HtC!^5,mނ8A} 㵨CAQk"הFJ fy&n<ӱ`,}ʉc@}C,Nf=qM"m ֗UN24"{MFOVN=ʂ o@Y,mACL\+g{ ԠRoR3PM:QtEuߋw^ -)nE&P]7Uq7qAqwݰEݹ^%w׽.J.{]TuQqwE^5x׽.j{]uQw,}vwۃ7و-3<%^dkEy-܂u;tBw%.F^Ĝ +i+O)\x}AM \-6 c-RSVy'R}S"rb=:Dº+m)ăMlx5u/S]ݻJ" +2t|E]Lv|γS~FoDD[PQ+w}G!;|_Mohjnыg۸-G>;_"P[$#*|Fw?=>u$*Au$7p,jIsjؘc>%h cK<aXu* waA:E xnS4\#"7.q]ڏr؇`倐? Q't\:~n}2oF“ 堮>Sk6omugߥ|Y-yX7d쇟HI[o># u*#,v;3\5`=`&IODaa6\JKE{zQdaU|^፜J/qLjjQߦI؞@>-p紽]:;lUĹe vs_gYr$/~Lno.QrSVLིu~fX" C2a(~`§ 3Lḫn)M:?9J(#MQ$E!VsË +y/hH!2! jw9EfX\SLiNOBRg 8̓ ["ZC(Z0 QgG}'Iʆ@%XFlT#A )ֺxJw*Me3e<_.dẙQt @G}(`W,">XqQ':D yʮ槵mi>"Cܜ3"\UCvL`#$B6V+ +eT0N:{VYM/VNB~ap#MI)6CGm=괨|;L"D]G|SZY$]΅Db-:Fz;T5Ͼ [ AI` +TwӔ[|A~}a?zxPtyGOoz;ƹ|cxeKp$dLeGS\üHcs0۾F_?.o~?>/~NG@e@~Þhg?tT!l_5aAY:'aw5/jTdi-*'̚mYW_QH\A~Ņ.N ,ߒLbZtB-"8JXAYL=e!{' jΙzKBP:X\[}eĮ91T}Adl\*&Ayȼ+ H%ùtV@kjb^sGm,(YjTն$~A URNvQ47Jb=9B} :J廳bzb7Ya1Ad',!kSun8κQpNaY‡ԟsWj"/e`^H+ET/oܴKuT\p( ǟRM"Mly5ӒZ^LI< TQb}TӐ\7Y6XՌbG4MP'뀪ON8*ۜ[,Z׻3W Q}Y9_omR6iDr{x Tb3:S!8t,;~0 :- ca4ՠUWDW32&+ +yZnjl>il+ v:r˛9o[\y*Av"L k%ؚHk^d{q1 p暕mlP:D,6삀 `j QW*L`Oqbтgo2Gkp?59&g;{¤]H;,E[7dPnW;%RT[XX>jߕ8ğ}N2&7B +rd3{?hpFa輢fvs]SjducmHEgm&NtDIĚiXIjo3%9I 7WfpK dMj?9b]fI1C;%Ma]!xd51_~v\'g)jŲ53iVDsV14'Ww˖6w:10s5L$VC] ]6mCa,I}ۮ%6Kl/H[IY68X2 A&|hWokWx!x; P.BD ح^aޓuds3<$.MI&lB.9G )f_G'Jώ%t@<}ЍtcS5s=blE c.͉(mobUg|}G2K^9 dm='%(y{"  T .Ov$O+#ۄoHe*"PI[$'TkB6oHuBi+YrsAȐJ XX+nK%S\L){|۸S[1!{7@l;GCe8rϚO$|lbhqIAxbŽE}Z0m;Mn5V`Ϲ4v< +sGI(Bjz 7B^_m:4@ ~# 2u4 Vn$Q:Ȓ. 鹋oHIOz&n~X tWvdVTр郝%8^kPXpY>9ۛ,;>S2Vtv3ȋKYb9h7E.$?bS~@)ßr$iY> B*QS@X[BG4DXZNiMw9DCj'!h3lKqGtԵdi᳘glvq6< x0X] ,DO>8HvA[LH+QjM=@ +B^R7-̤پO1Nt CUz{Z;;AmWv;"ʸvgabEl sC^Vq if~[ s,u9V%:,3_QuXGC[pbC0fZ2TknO;Akէ͐rzVⳄ3 +cVչ§]9Sj-Mƙ7^ˤU[b[ qz_l%hqJnz4v@fV BP&!lE"lЀ7($Ou%8"yN'8S+fhQp|)\owp蒮zh]A^!bKe[G<Ԃn1X!sbE܅dbb(sFBH}Pcpb$m|ZC<Wj +d+6 Vlnc}>d +ǼJ̊Zfx&ytL1M61!½CB,w:y\_3z}H'\0}`͋mV50Ҹ{b]!7shئCHt$},{L}3!nнh=r$8+Hs3"]R\cI'yտǵ^+\cZpex7oV+oE+@=qov`ZU|{wo8;3E4C!1 ogA4(\Nv.-UE`q|L|]^ i0OJDBFqۖ&v##K/<'̵o{=:q䮹Ť/*,*5_*=4By5/LO o›/%~AÁy?dY4ٓb֪3l,$މs lA푔 )̱Έ~f(Hڷ6\ PkcQogz2F9Ff ޼@?5<.Ż{-Ɓ?6s/EucwƥnMn^o8}>7vRL ѵ!Q~wb9Vot1ٞQ W #㝱ަ-s(Hg3; [Hƪ) +<4H;?"%CLS9uw0IBIT |R di.G4"9q)Vn4\duRє/Lhq!=>Д4m.|$x/IPEp=\b ~Ztd=ڪRDJ<#* #0x%ܲ5a6")uNYUSOUtpyL[_XP7ky. PoZ3,>1>(w `sbtmQY>69ܢWp8>Ǭu Gﮞ)P=ofd36yY]HSxf[}gqTׄr:\{EU $G-kq}F$ afnZqQn NT7묏kRI$W|!g=ԶZE4vF"(c=vi]E֝ryəu-4iZ\HWM 9^aJ,h7AVYG=.0Qk/aeBXNMXףObӾ@CڝrW%cCt̊[c.xdG wFnr[0FaY?hس”[vf-4ϰ ÷" +>CX ;0>|F}zn zFI]%iV0 ~%qJ pϩ +Ғ43v<,1HSw1`B:3OJ]H: w#%ĹMޕw RUA^JvK(˒1l ~FuΣ*Z3,g.?]ٙ =*G{@ȧO'Θb$O#E iZzJJO\Bj~2Slo cva*,FRM6/{)9HnON{* jJ0D;N]xL.* һ9vT?1MB>Nuy r^>6i^UR_IRSrڮx/Q +«t,0X,3KmweWW3 (L]&!,w/cȜ]'A JN2З!1[T8'5)a8Q"8ɣm!?F:F/ 0hPٙsqpl_؜Yׯ O,z,aK=FE {\.f*LÒ[\ۑ]R.m)xAP-o,H%oTXKew-3.Bg/IJ:j؆DhHOa8P͹%51dh&Y- T@g7dd*v*[FQN :1 ODml c̽r~ua&@~r@6gs2l܈`Lüi:jvFj[UN#qI}$0<@jb5uk!Eܘuqa`^"W9{eͫgt&vJ5^osnCoJ՞P-mvxٚ1b6 UB i DoO/`Vex1΁Ƴq yۍ pAfQӑI2T W-⸆sBn-E+`눬_)/9L Y芹YS]4ޚOuTmfӡyd`+8rHfyuY{8v%aOoq)A !ͤ +9h^qGG=W?軿'*Ww^~l|s%SN_|WwO?mܠ߽:'m/?lp??o߻_?r]s }??տ.OW=6Oo.T E}_O>?o>zmN~{P?+~?;?o//B[齗LmtHEP҇JN wY=Hr4u'y#Oǿ~|g?noeTy^~;+׏67ۜu3@b_-~1|ՊI/ֶwյOKg,CCB?m[`"+8<88@t4u듨op:gX2`aibqNH?o͟ۀTGlfhVsu:=fJd?gKg H^#k 5O'!TŻyݭ!V ];?PF4π3hZHБ(Y 3D?EAK+ĔnzVmdaDǤhq뎆̰& Pn7_ +x@ p{;gB )δ/0P>UNgR\B 5C ?@OIgW`"A*sRhHt<ȉL59).9Zkg8z<_"I-zJьͫ@z/| A}'H}\,a?Yv|!$:v޳Z0?Yg nfIΩ!i4;I?%֨ :h~FG 󗺻Һ}IH8]r?W`>`ґ%8pz/M_auABiؗ; t. bKh7RxO)okW~s yBJ%> npݱ>5m `؜EŭW ړsl^[BcrF^e@ryl_kЎNy ^lɷ,g~ġYܲ 8QK\s36"2ge,Lg4k\B7.al +HTְM}s&"~mi,f(Yӈ[iA'$3u{Jrv+Li5e!]rw;}ĕ}1B>){Bvaٝ󤋛absdr g,۠ouPAu3iO Zb;'hpQ"O3SA\r Af6:@2  C' xh6N-/0L܀_`D +tKw!(|Ӿ@ܣRBA }1*Hv9ηJ +F9(R3 ItNL:<w]DD[ioNUVP.3@Pa弜dQ<T]%S凎9^%`6F +`vB +r'L6Kha,F Oz7HkN-f3ntv4?YeTsQ? xJ h~U \20al+3֢ǩAdqC!}p8\?< trQFTtD[iA[''b~&\7X.est63IW U 8h;A"$a.j1[OJ6.D`ۅ' +\5!>&j.TZث <M͢"p +If K~VCf 1-N":Q4^ *=$'q0.udkA) +WSN)&ӸhPm`lK{p6d"4`>~U1i*Q3^[LCA wz=yRp`QSOV%+6- +͇Cp#qO8 +NM<~\tWlQhKvxIxIs' ,0H lvs- fmQϺ6 +8"ˮpæ)s>B!-c.F3Vna~$փv_PV6Á4u&4N $˒˳XBPk 9C" lK9TBhA2`< :D+XWBY}My 2vX=WP+>>5GyOL쐂&.ΩhQ0а{h$B0pNbq~q×8m5Ë^T덯0u@Wez/;,6&*0T*K,ԅu@Eqe:Z Jr۷wA37U쳁D"Ā͟Nۦ_Ɗ +9 ru4mzH*jq8~} {"![ 6NpAcjdam 3:aE N%I,1aA +=Lq18[}*~{#Ҡ~:Y]ǿO)ob5ܝ칶fك5^ir𠡶D mʞvɗXw3FHRYL$8?~*T= +f н/4#% \h $ͣpv .EGg.h`pU :=]O4"T“̛9l+3A)%L<$7]Q^ <<\wbׯ:uʁl+ވ 4TjhGׁ %qocqjHި=E1`ӹ`Z\9 tW$ dz@> 65Al;~6- h ߫Dy]yV6|zN0$0$1˃ N ALN|PKqgk>HF o' +A̵xkaAgOhX= 4r1,$U,? +{q69\v?/'i6 Ҏ $@ FɿCZl~]HZK ,#.޿ rZtDE H9@$j۞ r1tlri@YX32,TIepf8lp@_ /E[<t2"T @ +xopE5S2ȚV?y ҠWvT5ɭ.wYf2d n5M0YH8vf iHI*F))))~}8hBA_@K\@ t9KΤC;auB +OmX!؎zIEUL,%Jph3҅!Z (=3w}VeLHazJt;x1իxBt:˅fCsw]gQ ,2꭯~'D̹\ɍ(!Gd/@)KsYs=jݧ+ j,9#gnco(zX&}SAHHPvDͿ?>๰$v78 R1tt$crZF* +!BV4\-ܛz9ÿ$3fa\G|IZXx{='@ԭ(A:gϚD69 ܟ8ʳSu .*_4Q, +^"wI^=i;6?\WkFq'*lݟGjh44ӹMmIog<wr{%KbCP- x2n%-@>:ѠAd&޴꺼@/8RM6qSaOx*GYYX1V,􁜆FUt|O7(Upzl$0..L&>#tGùMd-eqy1otʓIK]_cǕ<ي:( B!:)D!M +7y69f97qLbSU=oU8 70%=m^|j2Ens5rd]_&0a)ًp8᫓xX_%"!x3 +e11A:>l2-gJwzXhfTginhL!Ģ ܂ g_bq|@k4JZ31㌃ e{Ӹ}0 - w 3$+Uѽe6HzKsΖW_tXI:l= VxH,̘~NA1纜Q,þ?H{{!f12.dՀ CD; |,!kV,dbCTfqi$dkXD4K+=j{<Pa79gM۔Ռs=c{W]QvؔnEP,&e!_!?R A)~ɲ2I%?B)tLgĨą S>'# +dt'Ѷ.MV$vyZnNwcntee;xx:m.:z)m*zVjPsl 톾$׈-[* QT B&DYAΟ5 -O^ +7xmZ+(;&m8zi7Bz'3yT`MNu{gTj=PgUJ -0[଑5SivIp&%2ͷ z4%Dj/?Ԁ {mAh$s,( +J®?dڻʖdyOwؗo9b^e@Æ|!Ai&V~{\VbVsfC8?沓ӕQ̾Mض x,Q/fπCy윘'9.Vg28D Γn̋ +D-=pnDESWO.J / +lXum:v:]ݜ]SVzwm=ج|\meeS#VN =G=)^ZnW )h@F80o`PXEk=-3Z$:}Ãd:etE7PB +3xUL!ZT:8Aib稛84QY`Π6,wPѥ(5 cP3vP5S{3gQ)ߴCTź}}p@˼ +*3]6T7]+vfhơKٱ*?==dqbJ*RwB4!!X +n& +Zk[1(YZ4W^MJzN ԤtQĽV핱蹋T%jCPsY6GJO.EK5,[PA3JAA }dFcb&]d]8NDaz#3/hpM Jq+-c 3An#wFăbOw+Xg $u>3sQ@ZpPl"jdqgo9 rAp M쳣 +k vrwG#.XvG+!Z +?A;iwoo??s!%CL)gΎaI~V4O`.Ha m2'E'+Zv?@"?R51==_J C /hstN s`C =iaw߼ʸU@' +FcΜ_@C,,UӪ(wЬ}ƛZ.M} 9`4 +!y8?RK kvbh@ Da @;>RP5vp?~rBYY;(Y3f5aH9nQ0a;Ew'#,āR$ɴh+cm2Lo㫌8ɜQ2_q@3uElH^ʾw=sb|v{0xNa qjs-pA6j +o邰X$Ԏ߭`<-y^B(nXKZ91ɡ-*G "Pt ؑiRmgUb5Ex<B] y;IbGfKД}i/*Ofu +ɇ0M }CI砮 .be_=쥭l#;(ZVN hUky4{M17^"ǃ8n^֏mp:*ȥ[nI>{u呜^=-ݩVMTZbh &>.VOgre)};""#k?oәr ɷT;3w˙T;G`<=رK|dԯn{FXxMA%)B/W!Od#O%)P_9?sK_%p*]KB7"vJf}[i";{~Бl/Gv}U_bj I8%`B!^hKʏ C +DJX0I,BpPpPԳ (\tʬE)ymxRl~4 XY;NPӨ( u $<60 E'@PY̝x4@hVw…uͧPxk@g zq EcxXFٻ~~XePEr)H^{D:dz_FG$ٛ,4Up>H| J\VSԏKݢFzS<4{.-5g¯ pG*0B%Y/̵ZhwD@vč[6 q+K`O1jU, {eّYJ#$Bڕ+@!乧KI"sH 20(B@p@`D y P|;9Ƈ|~[EUKzt(ڞna|I S#ZD  T@KETE8 + +F1/:~$u"?~L!)F2yO`ϊ/uz8r@-hՂ3iTEX .Ї1X/ RľotAvȰ2uXӳx>8" |A`W^Vd Wwr 0w- +uë"wAXOv/(v[=k^"gL `/U3!A/U\|jl}Y#BvBQFVC  e~Gv!ASwڍ C7|67^Ѐ|Agh.EJ=JI. 8ž? %JamN{lOT9g)Q(&yU=]֬&*Jxd AwRHLNEPe(ٸ♴|9^̛w& D`o#\,%64Ȼ̧u(YvC|(!b#8S*)a""A!Wm9dЕo5IwWsGWk*HT z/G1R!Dgp:R}LIlEl 9QfX '?/7E'Ϳa v#~§U͟$5|kˣ^hr,+-jmq"SfvZ]bY,ԜfI9IAn€$v2XA,!bJ>qx9$2:=k/V0KP 9x&@}hFa[Q`D 3OP["j?KRzy5瑦C9E`ݏ2^>(coS9 1&y CئjPr)l*H8tk$Ȓ#Ü|<#^&D2o-z % ӟhx +$,,nȓvU 25#"hzLXؑWO~8V +UF x"GVfsC2(Y; IH|#KviNi\sFi$b‰B +-38YY!_M~cF|YD3 ӱfM&ߞuuo(Sc)ߗl=G + ̲jN2"3*-&PG#s#8Un?%pJ'J(f80$Tn~@Kǎа!H"0'HB[vp;w +ۿcZGSA"Is;RXrB']"]AѸlTt%zǯ zPD0kJ3nܢt= +*U}.*OHyV0~hlLsZX&gh +\wSI7\0 88M)"lFy|ה9+]hu$`:S^(Y+R^R\{5yJ5wg\f`]0?%口_?\O")}F/MNU߭u^g3[>c( MR@j *ļj *t@8?g)wp34c5C f`ZgXR8`M U/R58@ήjU5p^f{G2Qw^2ZXFAv3^MG ۼnlJ^x:ϋEoM0%d*0T>O_H+f~?_yLJ))b endstream endobj 43 0 obj <>stream +s xqz\YrF\XIBY{3a('Hk ox@sOe$m_Fq)h +>1l^( נu,T|`@STC5{Boq8r:{bSq X@^QJþ10%^_ Uui9?KۋSqƦ3MkHWmۚ vzn Ud@ ƣ<|UZEPO+/>DjN,ҵ9zh$< +{dOY"t@ۀ 炽 o`!޲--H} =퀥Й 8tԁ''F3/oC' 8|@M?6Nh_pƠ0mAHnD͋9`Tdl\§S)1ԬA>escwZ|Y Y3OۇS IlIw0n\b{|`L-^ 0ADzmS$A`΃HC ON\\&˔YCu]!Ĉo`;G)ZN~YݺOc?i-G1Qv3﵄.WycĀ!yvAwJe)IQDMBSfJgjfNa%k-^;dS]Qe\c7@1J4pofQZO$o؋PU }{C 3B*IQ#3#1"!\GO:4gm"Ѝ~W:*BPu +M +;}'8ȭFzW}7ņ_be42 BltMJ+D?XOAxW +*'ʼJ>%?4~/Մ XY0:pARodMRW11m-I@CԾ5G|`,Y뗪nd;>tUZhȩd;<x:Ou>dFs +M). +@|45)`0갽/figԩ=>3FQhc`̙ƫ]rGԪcp&ZR)1S]R !=~KیLy@,B&v컶:!1dy ZNq,N +] XeEY"{q\=B@E0$0Hɬ!qHoa<`եꑄH̼Wʖ6m*MSbn#qq +TOJPKUIR>:~+kw[:B.tYNݕpiZuty ,x Nlv܄cO5 da @ôpk.?Tq^qɕ|d)C۟@WDO5k넴KQdeܼ Wo^ğ4kgȿ[<~x"ME儠įccOoa6m8P^Aʞ;;[VX]ޖA:DD%fއ8|u P*э^@W,x71Vz; 2oZH8l#)f>=(SIϘ7x⻄/zUMj(raf:hČK*#p@~;F= +4IQCo$װ~]Nʅw78ǖ@vQ4َ3*uG1_=ͤ|2+q"^OmN5=]1?Sq[ߋR,{|йK3;nQ?K͏Jb$T:ovF7 Տ}RP%W hj6s mXdBIO"| BbEX(+̓U< %Jvf3;>`61}.鲢vvs .x? g2sF?eQ?;#:mXtE70QR[] 1@<0fƜ4QNZJEFV=0 8Do:J"~T= +nWa@ڐ/G~^opfT%Q@9 'mJcjgל;*2elxS Rg& qfrC4d,.xeaKai| -@3#WB=XLe4y=eW[G8g(}1)}]O'a!ZX[WAټ?팷1 /p?'IgPF|>U9"嗡L]eg\C@ *1XX "Ho*|J=N秺EKuf34`!m6\׼*("b\<3C$3Q>$ iնqq/Zql0sI#nx|pP._Lߏt4'Ѽ~%١(BĶd3ŋ3₡W_'C|T:i[!{ٹKw ~2/Taݯ~c,M}} +-J,e?%|1rS@~$bcہ9EBh2$@lhD!>1gĕʁHN#v^#,ʸ(+c~:d@P +؝'y( +0Kq%kMW4\j4NG!>NQV&Sq$oCƪ^TךP*N oay :"]p@F +^A1Ȟny)g5Ck jG Jm ޛ~yDd/CLufMMuAcMkE>ԢE{xSI3 OȨ o}lE˪HtҚ%DS30k X#Nd}ϷTo{j_~_68GuyA/f|g(E!腯jL9aDH+M:ˡRAӻ8Ǘ2WOmFр6@dsgIYZRXZ?pE]1GA)pamϧvծFςWQ2$788e!| V3e@tRIӠS*IcX0#=gƌƌ3bQ3=qOQLrYwgBUȹ#bR'4?etq:bѐrM-:gDf}J?ZMCW 4%2>qXQ7uk8C~:*S|/փ$аM ^G + 6_4ᢱg ˒ ؃:l5: txBaF*\O x*>C +e͹ۃT#L&WNRQ_21]"}*$S*ʅTu$>:n/MqKku,f#rGH' 8+f Ljri`O5٤@SoW4 +43_*X|.%M#dK[q:"3ݛ?>6W'V׆uO!C dd55AM"2EqZ]_ڊ[.J?Ů 4 -0[n&p贛q~ U뛸@RS]n´Ԅ蠮)Fs>/fS|"pjl"<7B;muO'kR @({g땓?_@hUGc͛@dl`= )D[VU~rDLy/Jg<:eb?@"4!At <-B {쯃MD3-vdȻ)_pXd^FBHV2lG[3VS pR[P=2rU%@s׮bm=)oJhC7TĢYFZA-N +}61xj ?K +|W*p^YsLJ­TDBT/@jNQ%LAc8BjD:{ +εfb< EfSAIg7tk)9ĨAwn?!UN;HI[ Ε@hGN+ԋi-}Fb_)[13C 221xby2 7q{O9wP* Qa?y +bhH5x,h&=ĎG:G*8SĪ<=|XC4@;82͂MW_۩0ر@& +`ZA9PKf V`л3NS}…H/X|BIԜ 7æҲ>.o5]WF{/Sh*vr@SH@NyNLNjC vi`euAz4I[Tr(`[#yM2Y3F .բ^a#otb;@cY2/1*C"mpuE hǵ@W*P؏"4]Y;vWB 70~0)̚'6BW%h"X]ؒHcVDf,% OzEw"_ߐKépۀz=tU`~@*`h4ev,KzuO@5l" +vRVhZ5~Ʌ C&Դ,a"ە-H ` #o_Oow????_?_?%N'??ݟO/ows?vnj+~Ų GӤ@1(g>蛼P$ e3<{.n!hR:rOco+PLQ$аc?G*C%q%``)pE~(vId@R5H)fJ=c 4{ i6NP +ݷ"R4pS< L@j9t2FJ}2n%<>A@ltuVԱFZw/8Y@ 1'/62o0%9[eU!ŌܷIXwh@ mߑO?ńO5P{K0ns\yOqQ^d؃g1+OLR8 +!񾀊^_N)k*d5Z&>3@%A cQX<{|hp8сw;)$!aѯgS8o㫏: 7 NZ(uqLpB0Z9t1N] b2+r]LGFEQ5nH+hdpD&xoxih%+ +Ǣd-GX-Gk9 +CSZ(`'/m!s)/6guAyPX"R\j$330`4g3'z<`Bj&.ZҠ2RH=LE$c5!#K14"]5P)X(kLt:oŽT)7 muﵕ^323K (/` )`$} p;hNdxQV MMo 3Xce߅:3X)ۅus8rAAhDσ̈́@Aҫ}kLez~:NEʇ= L(P>eMA &?c?$W&E5F;41بAHc~*!]Դ_̒ͣJ/ &[dC XAcRu1#i{dLm#!V;͘C<`?Tߋh9ȫlh  ~=?t KWI !`GU(R3Ȉ2oJ(3 )͗ +gA"jGrj"<p#t(Apyw||n`..ah&uk*Z"@)"6c/|kI j p1^s^· A~r8PNemP(a()9g=h,8HDc+a/=]d deQOEW|JS!`BӜp,ޭl]JuYsw=Da`;"Ul@0+:i;3">p +:%씭Ԛ>a 2Froa`l}O4'vRTJ8Ey:fAj{l3~%Jyfe _AM(*u6@4*$m%/ۀ-XJ>3xq\>jYk ؂;hx> a4L#\*ʰD4u6sxdf(5ѪNcqy{5k~g1jRѓly; +cK*H݁H,AUl| +#9>  +4KMC?vYV_<7+QD[+v]D[1^S|ɨ9,|~ G!lО /6-|r2W$&h1yzr/"c˻1^X=0kqM^>qOFBjʯ>JoT!0ƻ۪jOCguO{ o (7O=Qğm``dQAR,#?(tCFg7&cCm8[JUJ4YaAy{we,] C+@MmקZx, )F캄ʅmO+]Dm䦩~.}!kWJwLHV#@<ɰʺwOc}]rKu0?cF1E4Z&bcP>[  ?AGE:p@SrJ|_ 6@"tĄO4ܿc3[sף|~(H3 0'G5 qJ~C u:v{,T;BU׿W$x,BQ=0YmZT8GVPɤxQkT%}n:hMdގ^> յ;3u8S/"M3= D+1;4͆ +b,X2[{Nb]2+ʳcH l&T a~+OQdv__=A1KM +J!f9y뵕 `/@&#a8_e5:!()~/d]Q TՁ~HL%xlyKP +_G.& g1C 9Y QĐ;*:h6)XXSsv-Zo3oЯ\Y8Pҵq޻H +G =2灟OT`%&~l/(+ b!ǔ)G"j8]O=PW;:P+YOO3^ ]Sٿ8L#oxϊa ^-|-&cVYaҽW;A8 'de q7D!9fyӶP"'c $\yW*+gº;W)8(KS@@C(<$ucBi.8={:>wcոiKԦێD@O"8zA<,WG I8z*73#0@n>Ltyr2"sPNxf47+pp,l#׼Eq6k5m!8e{ϺJGMq'fU0aσ?0nE>*b홛h}sr;.k6?"`.湞Ixc %, 5 pa7^X:ϑ3je$jRu"IUW3 +gpBe--჎{ r|X* -M!;^ڗ3X`i\˅tɯݾ{lFlW=_})7ݜ%u9,3,7qeAb.^g,+$Q,f !xX)+F~5 +k)ݔBr>ς^F"Bޥ؈Cо"QG4 '>EpYVD}Zs +CroY'G$sDxb5y@F +f!\]TUlDunh+S B>~Dv4)I]RC{:! `WpkDm"Ć+?{)MtqDwIӂYBb.fF;a.ÏW!}G0ۀ`IK¦J2)XP%DU p5L( E|&Z1r츻 +<U`+m&0Jv8q;;?u$OZ޶EL!Дƣoi(O3vJS)e*j*ì5P=p<|4ZBH$4) mH'ȂоW6Ͼ Q=ZT;!ZbuASAʌYY+Os$§UܟV ~5`aZ]F[P&3K] 8|FJ㌩$+mK;NO\w AkNΖBQg +6t +&hI'o+ʇ%(4\T+(wVh=zd{v$ĒY!ǠDAܻM;, OT<axཆ4c?PI%iO/ lo x dSm +KNo' HBil:@8Dk0St+`9X^m i;,ŜSSIh@B7lQ!?ef~'̒0d0(rf/fQ%֬XuWq!\H0c);/Lt}&zu'X3b{0a{<5Nj 7>CP萭x(8<N7Spm0 +qְc ;Xңew5V}a!7 QY9QOC={dVv#Ѕ=8$kOgr:{ZÇ Qw x5.xW^7`KBS΁*jݢFXzܫ8 γ=SI\$ӠGi6X tSlj8e4>'R~;>ɔP&pFLxWzCJa9{KY+[K.-a! +@;j51Ayz+d|د:s`<_&5BՖ&nG̏R ׺|@P O2Y!AKL RkϺ~UtmY.8pj{Ȥc3 1@΄ &׸PH^GF=:̠Wnj}3M)᜽H^4^x#zBZo34i'BdI~^Cvc.yKٚL L5Sd)z5ҧvC5 n&]+ KqA - v Bfiۃ %Qmc-O wGqc3-XZqTvtJЕt>G5y[`*<2.]r;x20hƆE7X835_!@ ezM00:/z=}%Hlzrsh20HEϻ f'יXa'Sd#S.E_U]C{-$(Ol_f +D$ + +~ ?1؂Iٮ^N-[Xx9H)}ңDX`Ib=Qc mjrfZPς!IAf-WM+%TV]-6 ,UGH-O>`Ħ`0U@Ve-lq OPIgq/L0w$U$/ +b5,8Oub.cT=JT*H#dd˨Nyz=pR~{)|6!s dijc::NR"fpdJ}II8u_~&ܸaSXQ࢜ߊbW2| bsp16FOЅ;U4$%0 +tnR4cˀ  ]nM1m"9| +kF. 4.s[n-ɪn4 Hhۯ" G`{"^.Xa Na>xr$boL0=< F@|RR:T"‹Tv*ZDGl8,N>) "ziP^hBBD~a4!N,f? PN/@%\7eCdR,LutrTW82FYzx#.N#aaH |D48Oj/nGY:2ÌK{|uf6lY y{h-PpF/f H 13<δrc7DEҐ]C878!VT!RQEa!Ew='kvtF[ H:1B1ρr6ffA4a%E)Zmv7%mTIt`CǩA~kL\°V`:{0i@6?JIχnp,FPeNRz.;B_@,퇎 vInLz+Bv+pUqXk;;Kf0qvO—f/KTGvdZIXp70+ juAv  Z$ieGe*)yTlJ|炋d`=Dn$<3P陨q&U:)/~ &Ydi3R(WIz{3t'*_=IYjMTaM޲|yʂй5wꡫ{[f"?W0N':V/IjB:Gx6UNa R84fWy ,:Ӎ:& ¡9~z¿QK/@bjF_c(mN\E@(y N^n{o3T5!!RA u;|ȁZUxzQGĪu^p-83 +ƌdV!MBE4;?+*+[lZO1Q[XՒ@kHH")lH]-B}/$x%'a+ H>JTSPL!bRGZ\R uLHCÊt$XYBγ4(4jVHynE #J8)Ǐ#c;}ux{>^G /,{4 }991! N fP` +r*JA eLiz:\ :)SĐ"`UOa@= ꈷlp15  +XA@Ǚ&TJ@ܴ0M@T١ӕZ WM^=W!ze4ziauB,Lj]uV1no*H s(n,* +u'Iz⾼v.u*\JIp@j/S:TJkxmR~+K8vaZa$NMV"S@ړ9IJ9?q'*.ӣ:s zr <79Y2mM!3v!!s75XIB[T+l6К#-͓@2h ,;a."["x5bD5ɶS,+E=4nxVRMQ aG.%Fמ~GXخq{U|Y}?`HxxNTE> +tG#x(/ +Ba_]@sf3F +ETЛei' { +A`"J>)6! C#sԖi~`qǀ (xTi)K%=D)SMJ$Z=GT"MWg_8ofFP }U^*<j YAۛ5$-mPpoU34v}UܣΝ +G'؆ *BƺȢ2c7+k78+0R`1}1RTC Q3 n"^g^.w8+< A/y\;Ȫ.\咧'8Q m-1a 4S/ d6:{\FU2! Km#dT,$c̐ڳJ0]Y&XT9dOZp:<`N\GBJu';@!pGA)1ۅz%a +&6*eHᒌO S xFEC]UdGs5kiV؟gAGewj(mXOGqڅ%`<#cچq 9)ۋV6>C/K`m6vwtwYg~`*^ +#YeV1ĀUUjC-6: zH+e6d/?? A?d5합=p3}ĈT` By\qQP'TzR`{BKE_D5^yfZq.&-!2{Iyzݒ"vܭCKwB}Y'vi0 =֐Lx6S95vϓ*T_ ؔ=EΜoA L/X+`=i)Z>1oN0ymO7AksiaD" zJ`a;F= +*4Ț,nfȁEKd@u{ᐷҤ4ObR;`G}S5h)`F j(j3u:kI[Q3M +_I8OP-GΝEU1FhK _@Q g~ku;C^k?Gǿ ))E e%D`@ GhBUVPثC |Ryw ++sCQ"E䍍!3NάvR&_))]NLPM!xfʉR2A8-F/U7/x_̂3Ĭ",=Fp5p?Ec_6i0齷DPsV F!87@Lzմd^(7ACr5,%2@ziI!{T)sOaut.@f舼#:y]%5?"zJNa>9G Ug 2!wC_SeFi{ 0ء?z0CǙq C lU |EN CS[gF:3.Xj}R ]mfA +wkn`N<=6Q7CI(G"y X'rԄA8Dm}H=V/8(q( a Bϡ4~1 E'>RsU4>A[g j\Rwr)J" !a@RJrፂ35u +)A C6 +䏱D?ݪab?b339jsYsY#n0cqHwPѿ#,fA妖*A +n[ #wGuD@^R\8ǟoSP./SugpԀIzovJXX#pz~H&Ք`L[xT8N ,tnULW{؇c3'#nc{!0`X;獬MR̪̈iNVR9/%dXة͈uD=N唶Dr%IϴG5,= ;NuNLC{0M a;r.|CHm {bD?G x%i<{9cF:5մFYdfXh-Oi%V@V[ ~㗏*KZwRNR$4@ 枢o7Z,3EwAf3_9ڋ1;wKSbm9 y͉$sp +:޵Vn?א,-BzqGP0GwY>ȢksJ|Q,Ea + L~0D&\VCEIJC7وej]GW[R#_! rpc@ Nz;KY zk z&vb|BJn *Ca^yC({;lZcäzR)1N"n<խN|;ڠ Z2+q YyǨ`\A /ǻmA1}ht}QS0?Ct0(q>1zRf@ჼLtR*/3o/"uo犋&d(xJv< 'LtBF<Aa{wVq}#e@HԦ?H. y=_gtǛ&ph& k= 0oHGQU<P Ţ'Ck'Z4sG獈7DNwK$Df=I+pqXm +#L Fބ[B{Z?vt>gcWuvPЮQB + O &i N`2z+[w8YW^Ɍo"y(2AELrE'vk/R9?MhKDLɣPiV5Uiu?-  OB>}3pN)[j I o|.r`6#kR3r֚VN[ "@S YAÎ-:fpdcJ}vYQ׌+3MpUkGwЊڃrgo=4"`)@ [! ||z^ͷ\bա˗^+Rw{D1H]g$O͔@)f(x?3kdnz,Dֿ)+ c|7xGb?1uR2izxKͭ~p!tji0*0W?Pw, %"!*"':"^:wf`X0떔A91;?jDt¶(_y. Yid =F)ySgeף&.ݷb!/.$+;+tNY륑rD&]w~)Eb+ E6뱳7buOZB@osoց)k]kc<.ch3([!ؔϸۑv@%հUTb^Agl,DsǑ@%*zr^/Ebڂ6ghXݦ~I` ~$>H lCpISkm.6bՎ\) i~2 74B?ґ_]pFu?jegZ aqd/?r +CN X1fŘ.I" NYpjL$*RH +g\.(;:tp$~FD #R̺Za3~[}Zn "2ꅿ>qmxx%+Q<*Nӑesʾ6&yotr +ymw-^EpFۗ>#ջqx~k;Ώwͨu.D:+ oLxvGXglD zL3A4Y=BH0 c>""6p/3, &kqR8,}ܤL<垁h1|MF) |F;:V T:C VVx.UGw#ק5H^!X}@͈{( \GJy~> "{9w&A<?wh н|Lhd+ALo~RO*Jw po3|/࿛^-읐yt2Tmdzlq[6Ptw w~Mbr;DZuK&hpѣ +0Z]51> u-(潘 Y-wpaylG| ubFg;Ap1PwHG[9]n\fzݟFsHb!վmPgi6ql'8!cmhQ !^#^$^: O\^-a=MH|_cs8LoIxXw ֺkylWkO񯨎vb^.w5^WTܪj ~fmWJ$ܸB-RHɇX\ +)u3i"KY) VRz8)40w@ZA2+*'W EV5w^im-AcШAE" gv&_L3wJ@Pp->i{2U}k+a;֟(;hU0{W-z~#™m^2ߢnV  [i-ޕt,a 5ׅ$6>Ōzk-#mWDٞEi~`+U[A*H kbEv d-,Wz5&9Tz~Z~wrq`g!G-~Emy?YA'{[cì@~1kva(;˚єâJ:go3k10ew>tb\c_WT#MWGpk8=#˵ədk;w) +V>Zt&8o}5E g.)q M\yT@MBDJx"AHxP- hZ"CsCElwz⎿L&max D fWwaQU ^^wΙEvE58AhǕ`T{q2 ޕPoH! J 0тK&Jd wW#4Y STHMAwhvmM`+-hyjb=%*԰ NṪ՝=&T] ]Bdf߮jY#R+@*ݓM\@cuyG(OyWAPI*ZG|WPFGA%µ+<@.I){Y V-$]b<qD7jlu,Gֺŷ.>)37[:5]Y8+<$EPPeE7GR7]6ܥ rlEMCcOO^qޠ|ϫѯn+`(@'[SWUCTqǨvq"D6@oז"Z迢 B`k+J9Q5XqP/wH&ZN~y̠Ny=01K3]Z7F\|-x ᣸fz:g:_-Tș8 6@́ dvRkE3( r <@Q3ݹ {mjEO*ۢ LK){=ƳoAϡ=AEU@[k"=1nL 3*o/lZv+H}*TARq&EYu@{/z. @Ũ=4DYRk悕6QW^ +e;VoэXw9 hOK] VH'\Q+O袁w\)TMJJS;bocY2;)3l_d"Ct4~ k潗ae)YCU3GQȵ{{E#3H'V?Z=% PGcwv_I_豸p@./Xڝ=S^*A<"utt +7#0$n]zf} 3uq {JVPcSC=(mdE jDx&Ռb7p+'g-A,i*Ȩ3y\Eh)%fb9dʝ׃YfȎ_7[àbv(fex &\QR.-Pk0WI&Gͧ%slz l'Kwݼ6Je`%3hcou?aQoq9~G{aaуA򧢹\ 솠 Gv "/bnXQ֞[@ٗIq=y\QCkFrշҗwϿ t2%n!cjg c N(҂lȵ,FVXWF]P/#Hq܌\ l;pv@}5S)_ȐwAq܁=ݯ~g=˻uc* Y,J 10T(#R25 xd8,E:ndb?}j-3]a +hO ̇!E߫2 蕕 QY+[XS/dmi/?sS<$Q*Oاj{q9zNA70p J@ijxQg!7E%p}C3PR[ֲ~Po6Q"jZgm4KBQPU.m Xf`P1ATZ+5Tq)uw]yZ˼h,;'vm/-A 9vWҰvGlkh qIzp(=SLݑ åȠ?tT<=R5tn^Fum9b: JD +ovXG.k!lG +GmPo;ՊbWCF|n#m9VΓu. "SVy `_qozvFF0wG +u{[⾍M M[# +N@z?6YA%_M)눭6IpDi*%_bΛ7ą _9? 8G;yL;}³:x)CSW^_MBNozI?0"cB zo.`^[C fj˅2 |=DtHCTDe`1_=rdl(M^r~P̪0:TƞN'\B7u69+zCDH!yp};bɪc2Ԓ:˃"'yfD0kٮ]XН꼵֒igOBZ:$]k+'=KlkKjŦaK)_:RN e\bkLuk?[o:ǩ=kƊmtn2@ߩS(IVlMI¬|ns ,Ύ+4 '۶k#} E}P娎]^H"iIvw13umK @w.FEC'E`vzQ["b]1i9(zXYӱ0r1]!..逮z~&a0MWpT$#fg}to?|[Si"Zz& zϑc 8:*f: w 8ȅWcA0NL~*g0膇``ѳ,@Ϧ{(DS|$ _tr<7c ڎITҥ'td>\c81ѤZyqx؇yBkͭ^Uj݉HP_}>ؐߓr]#p}Y[%,N1Ifծ⊺ґ7N +k5 ET$.p,-( KeJndqP,F^gfAbMs6 6FP LH$5nC2>/ts=*aK]Ƅ(A@VĮ-4X3VxR$Rt) \6,Wo;,ǦHHB.̭^@`W>@iƸQ0Pr`5|\+H@!MܲR;~*CA$%2wld3l(H!?aznN4w#97GN*n<9ѾnKLɛY0uXjmtPQoBTf =cc|NYq֬:F8Mޚ +Ȅ 6@{:jP|Th۸yM*mg"̌HBNOE-`l9tƉ7EGD#: afgsp)C؝52qU&dql+ <0 嵋%z$mt=j>DW3 +GQQ(&s溸PAxƃ0 l: z}*UtAgDSg] A=Ӑ#,@TpX@b<$u >i7"~ +g|掬W:X+3A>s_0 M _jq!b'r˅ A\ "&hyPEa%[*'CkCo֠zDWGE{ s:&*3鷣ȣйEuSt`"-:_31‹$:xZL#n5nPl[Cpdk'KE8Kb,<$xH d5%[uR8Cop<èGÌ{Zamu6V1~3B2y&WuJv +؏Ȱ|9Szʷwnh!@ii >Q3Ʀ2{"e|Rn-nl_(PfQIPG?>S>9ߝɖY[@, h'~N60JSGwZSpU)aO^vtځ_ JyELH*(Y$X9j~ +"D 4 nzz ifM/p5B9"\^wF-}9SbErI7|DYbXqaRKU<[m"cme`˙"Hs;|V roXA6](Za:cpE-jXݶ*M,o-xDK/_ )' "`&7,"l0JlWߨjYй"3;h(}%tڐ=Hkrb(R SC(&W:g&Q?~b )hL4ouw7;]&G+XDl"2;ݟ(!mѠc5sGA;P:& [[f{E IDhsAz eq7l3egmeX` c#f\[ώE)<+(+b;B!@Jm UVLV4aU|ߵ<f\ɠ^Z7"ULm9ܕ粃Sǜ[5{B8D@mZ6瞩YP&nYcK2d!ɾdsdlWqeڋN xcTwmQ +h2&r<Ї +_ ly:R'o5t6A x6>U)^N<-Esu5PQFiװx= Z`puK@} x8q'ӺE~p^ }=M@)ur[aZ(<?{{p:q@] F$qi;?A[c9fF!O6> 5fIdM{ Yևd@9#BTt䃻n,$c6ZLJ[`E,2~cZ+;g#O|ӊ}X1)Eu*?p?|@X?x!H~hb\q ǹOXw]wS:K!$]]#M#j2Rr({~^o@{ak.u":Q{LpEvr[J i38ĕة8\u3sWh#C9Ek\JY2fVe˛"WȄ!cMRiO/nbtg۴?ʗNɅYIU~QMQ ߯;Y\y5rY۝8wd/"ݙHS,ʰ)uZu+(l9Z֐D3Pe긃?Z +&_}8aɨA: +d+G -klA8GjA D^:0ֹDōXbpkӠ麁RTP`noR&;ZgIZ7$4[x^%w;8Ҿ^6rh{=hgg*l:~#jg}73jǯ6̂:Sk{t!h +6aB.2}l Q0~@uSl:>I69z/ e@Mml +׉&I@Hz~(&˴bV+I!)q|fx +=~Ѣ< X|L)'P}3-.?Ptyz8:-aխ^pESk~Z +)dP $H!nB 3D*;B*1CРe vMUIyiCuf4 m̅J\vQ48fx^7¥@M(lz֍iHωaIg3TM;O=]be[q37E͍y\a5w̢qlNU}Ff\D[[ '8^~mߦ랚3xv\(!M\zu֥oՌB"ר2ymz}{S\Ng4 ++ +k}Ȃ,a>^uq_ mtn=d猓ḺmU~,i + +;RZX~ +–{QRbu +-J+nnX˓>ĊzP"Ƭ-QgM$4)9 +z̸Kt&SGzEmn-cCTH9[iLOtAYP2=z 4ƞWw_A’TcuwӲo= /d:@姻sBy)r|T-Bd3O @z`-cAU-[v>آu]D`K@OoNh?_V5%`p]}|o{_sɎQ;ע5SCc}]^fKl`[}*ܪӈxnÌRw/&[pW~T.sU̯'OK zdqy͢yxŽ|E{dӊ$Ms;PĸN)ʠ;emS UBհR؜表=m9a)4OKB/smf[4qCsr5=պy@i^??aVbĻrIq>8H牌(w׀26"5hqO/襳( $ +CKby๋#Qo('_G@Sq/&ڂ&?xύrR|cv#+asǔ +;*#oi{G!O/\^R1/鱀,A\Du7A@m`|1я +DE{ۖ^hl +^[5.'dhwe쌺oߔ^0*z+c烥cX4MoCa]|;nQ4Ĕ8oC5't3-$V?]9ܹ))Au;h5K~\:5 H& +촒B?F!Q*tx6m)A#AۦB,7c f9v#?q!TA5[4s6k\.PXl6+A8}O}ی} f/ԗWo"ʈș7UssV;S$^ h4E8t(F臵낹Sni.>1rL?<Ôfnor \!(ap$!3l$gi5MȝTI~;>&L8R!bgh 9QxȺ@5ᣟR' +#~;뙡5d[Qρ=( 0Mß!+MU]+z&Gj]zS3E ?4b?d35é_xQAoP PYZkVx}o8$ȟnjX02t&jKIwݲ xZ۷l#֜`@K6d{DQddBQJ9]:뾿qZn!eT7b[DH}EEK VwoYxm[j \v@gXS&tON[*v%T+jN&Jx%Hz5WHNϼǵ1eATIYzEڂX@@sIaoIrv8pHj0O[_PK]񤨒4xkvU_6A#_cxUi4?Y)= ضzU@݄/w&QH~؞F!]W]6Pzu3|[:ndM-oEbe>&@t=zT\YyOFr9-pZB#.D!׷5G+|7y[ 톰U5N-oaE|p>\Z9 +Y4tDk{8^Yy9Mp&C>9aW䉋?E(a.M/\2H7.~yD :,Ggk`NpeDIv;8 5. GKnAԷCq +{ iAx θB#͸ޏwR&t75E"SG-b2"*"~o̷`vr^Haſ}Y'Xƈ30N܍yn~f7ةY^oৱ*-u`X`UECqjEBieQzd>#9a=D@ Fʷh.zG QT 7ti[OfRP76`l~+Q^o"d>_fwޣI-H8=l¡>^ic\i+1}.vO;m$ZpBxK`5A[y~R.j-Fd2tfg`fD{σ멌'6n[0Z^RVWd6l<`ɞGk +qW ֑Bw? NWhgr_eHghD8P=_u/sg$&op?wCP)<7tfgQYŌNժE <02CS}3öò#(ooE2`N1X7rx❰ymaye|iV``hzcЭ r-)k}@JߜmruIߙqмtFA!ƚo ki2y&^sZ 52<a@DQaz GO7? *B` +@磔azҥ vr#ыaok]sWPp  o5~+kUK}`%G7$!o*ylصҬe uۅCUz-=llua."D,x,bI:ѱV5F4.{ kG}Y!qey_'ZK!'Bn:L`_ |^L00Zᓘ <Ok~#F==zTx6(w6[uڛz]CN%S+*-@}11d <_jZ?bfSAR;\a埣YP}X3wf\UC@sHk^A+kۧof 69-2hs͘E`$UsP3+:ӏ*?4bzϟ{7VY0l>!>"jQp+ܝHv[R*ILGʊ>xZQN踠Vh>z̄ \ R_'Fd Y?'3F;[I=\x5}?SyOAz|HC |՝6pQj{~sxeX{*uRBd`UvC qn}<~<:iT fg(-Up6pp&aR^"zC(:2C.VHZN"Mu:Dwk6鲽n nڨ];Q,ߐ|ε@fa' S#v?vs13&-=0UI_jbg_\3&Lp*og3 5v: %PW;/ˣ3Ø3֧XW]+uLbdsK`]TCmۜ_}~ޏMCHaBc6{ր@{V<I}qblkV_.E -/]~O7E]5<14RR Q0n7$֋;yYM(&㧳GNƃue kP=9Ch=:kW_GB $Xlas ћ9ݸ9BN≅޷ +h]ty;WPBE^7Z1b!cwD]8+-S;@fztDNCYMQらR &VDum$/TJlYyZ'v +^Ҽ)ٳR#5+wf꫇otLU>;l}B6# yp q8v-8QS iܬf(=.0K٤rn21f_3rug殳ſR 8nFYꏮg%XS0Ϝ`a=ʾ~`c2׉JxHIeK8X'-Չ yKl"W׎d +" O"=dݴ&E +~O 3ͺCPjڣ%1S;CR;:qmF)!Fv PV} +r3>A, +h@60A/TP>!<EC.#Vl0`Yuq]n[ .R'z>_'63qZN4Gꈞg{mHҟ&լ!cҳ  i*GZPWљI)x;-P[Y4֬fq +%{x5jsDK2A>ۚXj,8ܸHG[ܲkǸWwe gAvVy +:nj\iY_2Ë yI̬I=`)t1=ʱǙl@rrm˒m`K} E3EN鷘߻sHxӓ hnٶ yy7:n_FP)]aO57Jrm ]Hafd=LP0VM/Hc W5Cy u|hbL/t9Ik!HQ6LJmׇ=EwL>SImGA]ŵ ku4MdaM7W4\楰| :8yL7l-pטtl-9Rs  xLj~Q{D{gыqta3VaT=7{c-Kg[̶0G|W~{H +w_=91nL%bC8طJ7ցJ))o3]ѫqIAr Oc^eqЊAzrϺAD2 ۧ7zØt5jHN" \VS1#1Y\|Iq3*Sb0Hy;欩j/}F+9~ÏavT- +=H:f7~4WJWG(?tE~`Œ3mCH<B,XU ++>D]]C^m2Qȡ[vQŌvc@ꠠPv=p.f!F?y"կ's(q6g-2vuC Zl=uxd^޺DjE{ +N5F]r&U`NwҎ`]XRpCZKNfrgzg8h罼4" 3z|qb641d y׼vBڳ| F̷a›Ȥ#;ϗ\v6z}E'&uov`XJQ-#F<ͪ#Qh+ap#Z=iԐe8ɴ'<(eķA +8CQȅ'_L[}g(#0~_. rhOJ"q>u䜍`Eoqw:(q;MLʙS^CMǮwCRTt9`3AMkwA8ڈgͧQnm#MG(ؽr5Ү +֯Ht~tUc'8H+ڳPywQ&@ixWo#nB=1<{IQ'}Ef s$vBJ"W0oĊ:?˳ '>#oEf!qѴЏ:(2Ƽ"B)u,E%yH)ƕAX`Gf:G*Dh(\Is Է(`ã*ғ;YXӡQZ|Du +$o+9Y%ohFD'v}Cb3ף LqpE5(S7Nm10 EW̿MZiNbeȂٹV'PV$׾~w+Wu|ۧSwYy&,iW3'@JÀd?-K,يc&!wi}ʼ Lh/4 -IˁQ7)svi(=oX+Ҡitol$% V`fǸbph E#K8| %uXf@x0 ;(NE_%#ٹTPAg2B3?gIߘheqի(u"0 LҌߑ0I[yf:ɒ(5RCT<Aܝ8󹖂q{EL4f7$Đ?R ;1M<$헪z{#;vvfHeɌ?ښY e psq^'@L֝NgZ 0Yg,~P@O󛾿Lqja`ylOH[vvM%`}D=uBǦM,߯R>sFsah\ yT*g(-.xFﳤ4aWEAk5cnTUIj}@J{emp }ϮK1.lM[/!дv5N3~Ղ⹢O>3*0>>.R^{c(f HϙmZmӷK'4ډÒ .p JK*Q,DHëapW-+[w'%kogܺf+K,=2gMt _$Z?Y3XPY|T'b`&ى\ANsz"OTGڝ9>`ʯ B1+iTdÙ;A"sf`}E@~YcT0^1@i*DwP㠇Ҏ +|z,)7c|u)^l `:Pȩڇ?~uT.0W2ju@Hmjl_q4`HClH6xe4obruD@U|w=4 .j3C +=sL +'JCWH ϙ,1rbl +ۡ|o!>jJ{z.[>Y7}?0սΆ}u :gvl ꓴߎ`4o+.3=+ʀ|glX.M ΘQ>7u̓TQ"N.;V; i1(.WzLmˏ<C<1w+ +!Q5y;Cdޓ{N"C>c,,/.?BfQ7g`i, e tގ-ml,3SHww5ܧ)mt']1Ǟ׌]#յ6i•3 iϙ^T'_J*  Wd C%3(֟afQ5&C-(g%d J,W9Y' pkYnjXAлw" ~Do3rF:Q?T{i}&Rc{>y bNke;6a8˵Awj- +?25+DY n.#[၀)v;E{}?v(lg|I Q"w,6*?00j 9%Imh"PBHx/MGˌ̳jFCeތx62#w-;C1"kdB* +cH)጑׉wd{xXSjѭN̰: }ܫ ̠f̧ +RNO<:B5|\XKk]q(`XA9L|uEeImBCٲoӆ +;K!H2!$`Dddf9!apRid!Nƻ a\IaHqʋ},MIKri8QR>re[IҕB׌sܶ*bRtfF!R\˕8T5[eVxγnjmAI%B/BYg["ȟv?`3͡c Uif)MK8pGb]6~1/́ز  +dmi ;U V*Td$Z:srTyx]xpeT"Pk q%Ǥt}jƠVP/DքYT7}; $G/~ > p|4%Þ,Y4@N1M|.@Y>[tCqh5c =LO8%H ڶ NiCP$[ +f +q*0ֳ5:Dي5qAv` qCwIQvO+jJa Ѓ"a k N!f\k GbZ$Pmr˨4\iw|le"9WZ9VCha$ wICm}J@V |֮_O+ {k*ewEfQlfG^MOEh"O_:g»y 5b:{:>NN%0wnu\.W,wӎc}\E~uKU!N_Pڛ8Gؐk\x:fqܴ, ҊE56xfU tx&z#=xp1<(24n'8(DۃR:6N` ~]2U!G`.HXgnJ _⥺OxQ֦TZI[P^Ozkc$J"tNY1Mm)&͑H ޮHsҍ9{B_=l@Q#T+u15v\ͦ.=W~3d_<h7l|@TDw0XFTP{Q;ҁ:Dwu?!N 0kcJhN ykb>_PzhdN(ĚQ#wDv3hR 򀫴( SslRJ`~o\IJ#~hUyU7,ڀ)I !>q@ 3~̘qH/lo!753qrh{ o~A)Tac;3,ݭ@L:lu1k`@,vv3][vC_+x@}HUSK/ $R0D+3*H@˙Pz̍Ur"[1rn~d@PUwd'GO4X,cDjJk9 KxmH]S#TM]wH +T-)%:ݦiId? ڛ|K*>'VƖը + Z|4ٯ@%$`6NMYP1 ґY&Nӳ2YKs 7,Rd&Շ4 6x#}v^<^rCu{%~ڜ#sv{ h X+4:YH]6^pUW.,ps\ hT՜P];HĞa5'㰧 +n%s=~ e@4!d + r!mn +ӏ];&WjVn\$*:#xB)SuHVl&T֭y<]PRlۦwT!Y!-q{s^_(J%B@ 2Q^:p ǢoUCevhtf'H!-أq`h)$,ޞ2&]Ctew=d$Ɉ{H(ǀ9%ϔ uЬG#32kc+˖"^= @'M?8Ф4i1-#οSy :_6MXb.zG X}L_w~̇o.H]+1ž՝KK]X߯S-浵<.Vk!43,_QPg/'5(>SHaV xYkn,~RqcR<!fU(/0܄[H/#>ۚ3؈1U噰`h-LN`>UCt~)cqP]j7kzEona.%]=~R[:pe Ӧ`=qZy^pc$#*JRܷpNm\cWF^H$ҷ:Y<ڇQ?|qΌh(]QVS&/@g YڻК&ҶVC{@șC H/nPeCԶؿRaU?9M"eMXDMVܟq]m˗_.iQs +k͵ +ٽ7Ӵ Z<'vhK5qCn&ڠglMBFuMtBeke?<Ϳ؜'D/^HZz:Zpg'ԥS6WYi\wsVh+q{PTW@>~뱙~ +Lcsnr".d۟BcR!SLJmv E,uzLrbw +VQB hïb?ǂކe˃ZBm!u\犩[BA/HZ.RZˀj)m۴V0ym6Y:A3lgykּ+Y9UM2!ܻ٫2Hd[M:_fh FFy*%=Ōr֭mG&Oѧ#BK(mJRKj=CS/?"TQ@݅uB.Ho. +՚"NמGN 5|$-wȝΎ 90'\HZ{ +_iuXo(WR R~D ȕnƵ^r#(T"fwԣIG(vo(u˅JU٨GVT2X5X VMCW>ga`m'T&P*'% 4Aـp˻q"[Wyah|7=f/8àlV [ϛ(3?Z3beH8G 72*XD52xW +c5Xk^v{q@] +<}ԍ;4З .3g~f7٘EoYU€v#7 4TB{XmR;CZ.YXPpZi@մ|`^ieRD1VYLܳWѵc4uFvVT.#gntg=4Z_{\6C!)U(PrRrX?q}d+0~ۮJzwu@aXd! (HG!+2E@>A(Zhlǒ#b)־54w({5ž&>а"EUE::1c&AjpA[ +%y_N߯4r 3D$Wj4I"L4*9 vxǩds?ndg앨J +@ UQDXMXs +޼Qfc HʠlwDn(0h1$M/烗 +r en/3}>FH{$1Mˉp|> +fLǙPVZӇW;cp  ͑p +eɮFK^%D, %*|7Z|_ +Wx=V!& `Β2rH@5ȁBm\%>3]o-<疽'G('(4SmꌢdŴmd*z5x7 GşUS@g!n"υqN$A1'{c)A@(DXϲ `"gE-`|D\WHWd0U瑣2t/)0b*3oIR6Rh4Afih<]51"h2wa%e\Jfs#^a|h㘹#.F>E<!1A-= Ih+tt<}ꔀ;kȖBlSٍ| sZhi[4 +S: Ԯ|1"`Yơv};ϙBmG >݀bBAv5QWz>,p~dNi;T)=)$q:.hpr]Eiy:1ba=|X(*VqlT~3^% gqU E",͖'B_І]cA=71Ft? _V៎]I" )Pɗ&+N5b@{N)̬s&TL/%"R^ ES9_Pэ0I$07/eQuNoD7EԹggF + d^As!(O +` U>ʞԪ}^27  齨NЋpziY&@hǨT=nX B,KQ{,,xYO~ׯL +5;H##A;hxvDzt [ +Ơu)kK_)M1 [W ہ"}^]5[/))Ƌw7˶Ā9! #*9Em؀/\o†SZ=+|F^3aSf\ðJd13د7i0@tBGA[ukΦ:ynQ.;|[,s5tJ2,S'.獹f謲"HL-.gʋߌPYZ_+wt\T)wM|G;J.S*%#II +~5Hxp&U^lgNF'@PlodL/Ѷcr "_3%B{d%p_9;?C?*GT<1 ^@BR=Y%0*e2[b1FW\j$$.Ru!Zc_nȉ,:wI"R& 5nbא3|DJCguDPIV-%1pӼ\( N\De  {L@hc#Lh{%pa sgdO=6Z9 XDܩq`HGĴ|H"0z-ܓo|ib{Gxxe9)!z~35.}OD#YıxBtɨYL=™($9Fv( /M"=[taML9/R&Պyh%ZQ}Hbs/W,TX+curƄ%0IU`ǧOHh8ޥ/aqxt>ϕ'#80Uo^K#D(yX'v:DQQVHe8. WYP`eGD¼Iִ0s|O(s"tC8[Qx)W/D?DȽS4K[&ِV[!2ow=g'X.^DZCJR-Y;L3}gީHVgC#3R0P[}>EN)7x +vV@n _ß +Wq71 +TC~1X5  Zy@ l"'3e v};џ8(ňszh W&#T_cBPП\ OX(QH/_}r,Tˉh'e v37|k_ NອCOOgT#N3`{Y(Ӊ дϚ Ep#Y2KͅAau Ԯ54]GjFn.9D*C>`wjW2^BlfHEU=.aNb@2Џ3\2ar8S_-~K|\#fO:9SDZ-R}mSax1"`M;^jR7e=4ؚ%5`چ_Z51q/z=f]t<+ N/d|nNRU ՝nҔ#хGڊ~yÍ%gIJÜ||3 C..kA{տrt`CSG6=i:   zGiKK iQdm7B#%ƯRrLf Q|>H_E+j`AuϡC9yu3u'Q!^YWg$Qo=D.m19(Hs|hؿQmqASvmGrNdz9a̾T/׉ӭ@| -'ޥ@y[nIIyizeM״/+'d3%{44-7(g.s$^qỲ ;ۼ:H2aYE.#n9~Hix l@T^>,O(׮4j8Q*T1>`XBI!Oe(Yڠ%GYSp+k ?їl޿, ?*vEsiw{h W4րkgM.'ɥHHG,@(: 5B-'eՂ f粣daKvTԅT+M-Nܶ>v_*Җ\p$SuL%JW; hQny95u:ʦONwn#8u_!WDiD˭y$:ˆW_jwR~\| ևc?0'>Od;}Qp5ig0gUM1-rSe`GpH_oc8F#bF @oBvNphZ9iޅu}flwO:׬0ȾxmJVh" d<]y] +dȈޟC@nIu8XKE$|ѤX0)Xt{w X'Hf%9K|m*lDC+nBt70JP܃!ؠ%sݡlIV)H٠F;<_# *^DCOUqYN, L3{c= + (̀'\qwȿ_694)aP)([ w?@;2(<JQGg,22fyWO + +#\42uT4+ >"g)-q)twqǎ"CO@^?{Rxmʁc!FbxwJBDWTŸ\`GJrDCQߛF_"4yf^?9FҧïR'=X@rt = iuugDnDA~3 "ARm%ȽNy!+BX@H̐ Np5L&=ߋdM8W '`D\?BGCd*!e鳾NE,gha3}*;q"U%>j?X@ҬZ~CP2h {z+DGj)C',Z]~ nwJ B`G|> +}b(!}m/XJ`W(h5?HV1a~Omm:=i¾eWy,"fW +f"™Y^ƒYɳj,&眪vB d^L_S}dQFW!;첏(L+uQ[}0j"(¿~QYuk<&"f{H =8].7E\_Ͷ&}LjtS8B8v.iJ# A=Mי" 9[~7JXC$$ػ>Ρ}|OÔE -+ѩBge`*֭6;RʆJ[dz?i&"Jy h480'b2OLډ 9rУn/DBM6z&N)I.,ͪ<:^@Fo,?~0UZưRRʫ'~5GINu=|{̘N{{?15f g pbHͯ~H 0&jTQl=\]gߤqX6\({nI%} 9R*Qa/>ٔ͟:Х +g?4cpKUU8n׮v)ۆBlvD":/.9Q66ҀL5d5a1D}3hX ݫvE 9)Gy?1fQQ~ ("E +ou`#XHUH&@FNa,*+W$\gf]zJQvP⾃4 {?Jr. 7J`@SVP kr Q=";ٻgD+>T򡿈OBZ%y~5_5oQ /vj-h)n{J$SжT(Htф + xm1v3k1?&!u)Q&$G>P}(S 1HZJ0 NJ|Vvγ!l.Gvcs ܈llNJ|Ց8y.Qݙh=2_A ψGab>*V]@G7t:u\9W`@n"5Ys@;RDۃ89j.;7xh  #pI3?zz E@ƅ|v$ּzaK#wtS%zsvd><*8d}ɞnW5È@[ hu%EV UZQ4ң +9!.׉HהЙ?$u{e+6j|(nC*6b(xBvKX)\:#2P{>$C2ХQQUpGe*nĖq4#E>G\QL=h9H91 "~zn?WEmaW}(ɾ%LnKP6qi׉!{񧋻(L+^^gMdB=eUwgv5>6T`̂҇4?W xaD9,`:h ug,Nod_ &HSYq7 b8z;̎]뜉 Ie*!][ +D?,go U.%mεkl /P/K$n\&=3Nr1Hwq%j+&%*<'> {29څ^6U넬# +"ֱM>ܷ̒}z`i\kjfq!]:kM~@o3w5tQER  WP9+;FLLڮgŰDzSVA)5WJbs7פ \a )WR0RO],ѠjlK-ȧ#YkxCǝo.Lcݞ.W| \IF\:a4n]vw>Jd9;)O0S٢v@zʉ6qέO}_Cؖwv6Ei(QٜV7C p%* B$_!0(ŝjOt21#5]C) Տ''hp+*^zVW`kJMz'`B+/=RKpuc@ͼ'^S{@f#MLQ-2#...xmTYK#-aD9ƸpA+^tw.Y6 ->O)Y0ByM=TGd>m +\8bLH +sa߁'Aj%iފAWcK'e9H?ne/| H 2ފQwCaqQMɩj5KtL,'I:c3̳ia8]ψYm؝i?gE]#ًQ'qx/A@8Wihۙl+ fcM +dG Hr7(  ]qPdPX>@N%jdJ+%jgʉtT]\N +C. i ](wsUW>stream +ήXp?E}~{=8CQr髊N]qDkXaL[g'L#bwgX&.ٌaӈ M@1r_XNmW,NE2]_g_>s ̲Ȭ3S4M`YH^aޔj!?臷-D:F C퍎FU50Vhv\u#g5QЁҳ%Q~lRأߗĬoc`G 1yW|`iЌҝ1%D6PF[k&&7 n1_?ELк%wR|Or}%7"!CLlT*#Ұ\#V(va'O;53>l /3 +0!"(E[pn*-"Vk!U& X8S'TcAӲzZ=AF%Qrح!ڈ @b| ow##-Hfq%x zDfz;GphV!:HS-v"j<6b]M( +hs, >9P?yu] 6AD|>3 p@o# A}#G(_+(*g+ =q+;>ۃsN!=8@D@ؖ_PzIo.½po@\)yvA-S({v5mQ؀/K{GG86J}(+ND%B*&dDxPuG :;/ӂZV @Q7,<NrUw 샊:pPӵ22 )1E1 +˝zgn + #-^2TK{:Yyi'S\QP~pZȢw6|ˋe9a*'e.߼_+ 9>̑.T63vob4IèQMnsÖD%BE(ŀSΉLje(Gw5Yu\dAH%{Dx0NJ E/Li96^Q@w!6 hjظ-fHr;ss;B@gckmY.'JzwC /ۋi_>B\͸ `aBIsTAT~N^ 8=\sK2\ }{_H-zߔF/ k1k0qŮ#:zxte Vuz5w(f‘k?|!?9ly0gm9rA=5Q&dx"fQ~y(kM!"DgOwl"eoYWw١;c&3wX0,hp臅N>-죫6 {FTiN~.L*U;*Dh@aPxX띊u&XD7(YgcD (#2~`q&:6GyZTo/a?<:S93F +esHX \`1*SNUǎIbQA Dl8F.vr_r{ ls;WST-ʰׂZ=lՙ<]*>R#=#_eȠ\kH\%RE1JBi:8Lo%2\ h˸O|6ɼMr6 Ӑ(¿}\y% 8?e釬!~o U{p8k2G +"-{,Lő`=W\Yig?ȁz2̰Q"3{xswytoxvQKԲlahPa{=x;U j)AGB +P57 +_ѹ\J W˙v6dDwiSOBh :^pf*An"1M7*f==v^/%,:h7a[2{HXGygɸbXX=S2##b%eա`W BAca@=N4m!$^p0S R|vUI^SVVa%jÚgR@Y +rֈ-0 b?ݱ n{-'BhK;,(KcQ#_-8⛆cW{4 +k7zAhVs(p:>b0K-w~cKGKhYsFGbÿUS|$Vg4TJa&fRnH*j@L=n&F#0J/&`oF{#Y + á,cox}4wgD4P̀o~RbZc :i)Q|פ~OjEVmqO=h rMY]iitu&(vt9A;D:h]h+~b_~z"@';r&3p4If:Bt=7DNKF#jB ͦ[UK}7rb,{20 `hyGx +z3]v!G"@;+\žJ,Le%@" + M2EbCvGF:OS9+" ʧ`w$#Hw\ +4MR\`F}S8Ii"+~?p!)0$; 2yGyEtuy {~ᶀ UIW ݀=* w@Fó?ji&CAIݓzP +eȘF"{K:Z'j(Rs4;>EpEcy%v{Rl +`_Nk4꧆[DIs S˻FsR# +;@W'!D`2pk P}m^s~?"w!t$~U@ꁎ^Ꙧ5?ά +gP_}^Z2b\_"S"3_]ד+<-]jPΑ]sl&Yg|r_Jﴮy WLyHQz@yvCe?֪f|R3]O޾Km$63+ H;4lG`ٵ٩Cȴ(_溸ܨ辎 ڤǒLhS"y&Ȏ S:Ucn/G`\Uu?|Ā`.ׂ:~X!bߣ$ ?p48S mDM(1F͌=&n1u|{<>tavcvLjN^}of[KΖ&SF~,ZG* +_>V{ُ?^2;^G_U7gA@a']Ȃ3lW_e [!l.<ʂY/ +h:Af^a2aFc]r+ջs&jW9i2tzd pGD͍;vA1zl6ѐBZcw$ c-bφ-eD܈4f3i+)OMhXL=VWpYC)/I×Ԯݫ|b09W3 ;9%OIJ2=G=Q8ly:A@2IiH# +Xw-fp R;gJ3:'Bz2hG~Y^gNu2 L*IL^zWj$7mpڞO2QS#hЙ?)8ʽk'qKC +Wqs`2S׉ޫ"b+5>RZ%q}e*pcЋR} "PW/%kg'~]boNΎ dSihf~E:rft/ BR.>q>GU +iyXEKUءX DȿZ`> &:/&%Lp5:kfV)}0M@043=^CAa ~u &;!U84Q-6b"tMә$ߢqQȻG# ̊֨n53KA P-|Y{ ;/dKkQBͪxTE=l]T7}e)Y~ nfu2sz+,5;k*ҐzT{2͇ F={Wqpؿ*P!wVtDb$+#i Óc󞈶ةŭ 1 *st@G?*"Y`ro*0%\p* P5pp +V?e:J"tDoRqy/_'QjХ%kz8R~c$zM;@ v}({~4xMNLlG\(ݪ(K6,ɴGh7( M2S]p#qvUi g&Ũv;͇>`NYzsuA. +_z9ё@I(٢NEhĮoTvzU;#xQ5^쇾ݏ cܹ *dϾb:1KM7c[w+I!l7*qj XkDafa{CUo'9<O^V2a *"lyأFhVPdZX. "0=:z6˿慂vX){ OI",-3!JI9StO1uJx%xs8+C*_n,g+4;z7TXR91Yay }T,gɍS~Tz`pP +18 +ptIj:NvBðrsf'=\ `aJGII@Y%i"Rѓiՠ"2e#Cv{ Qh.5mK}"p-E}z[HE/=m}XK, + r>);L8 ĄZ]-'jLWZ*' +/_0=o``%W*`ps+mVkZB|޻sx+xaΪ ^3a-;Er-'4h!u+ P#`4 xb>(PA NѳO;V'ooUhɟ\¯ /QkU@sVlMuRT2M7U˜Ǝ 4nhc?[z-u3䱻-J?zD:: + +=ij +GJuMK^yD/h@Ǖ"m2ՙ6Gj,}er2fA[K#Y-m*as;IYF0lf 0G3Fp(Uz#ea`vۀW#۬AIA,: j%A^5eXS>}<$T?ۜ_+IfDcI#ҏy3*yةHSujL768+Р=m<\x80p ?ū#_4 .OP3"#̫SK/itM +,7S?ZĀDRo6טʈsbK -%?Q;Ip4eؒ RkgC"ڈW3jL )oQ XЀ&=jxO~—V:>T}sdT5x`XZ򞒛I)ϳ4Ĭh04aUpK3x/fXu~EZx|Ӎ7©Nٴ)tbYYF$R0VXsV0/t'KcȒڠ, 1vU( T~K{(U'yo +Mc0U Rs'd/^י 2jУ a!o nW*JBMLydcld1^Q!'KR@@rᓱiwf'3&9ӄ= tܹ;-Z:hm?2׌GwRճS0- ?W⾓vV^_ڡ@J:4m,r?3(CUXǸ^\^ + "=$~f)YakO^ŐQHzVzS*#u )?V{E>S+Ai5 +~J?%2؇ ]e_Q|oy4`jl7[l?DIt)}^O[3 HݷpK0Х@;+t=aڙWA,^Z}½..hqԦG·Fb6,Hr+ jԌ/_a"Q:Q6_;ta] hr |4Mu^J/O#t苢t#ǫpq2E72qp)U~RS +(#4~ 83)ti + Q-2s|Ѳ2"nnBU3=(NS|2~ YwO2~ ײOJn,3GX+TefE b Iy[XATqMn +HV8Bfl1na_{uq?ٕ.K:?%JRwA/!w;s4! Ɓ#~ [|FعCG)`BR썷+C Ʒî'j}h6Bu/ʐ-t6`kI5>?X +ܾiui:Qu&W?W(<؉QODa%Țl^o 8__1@AkUqF8Gug]$z|$RQUФԫ]OGjŋU9!a(RǢJ,vqi?9; 4a+VuE!n="Ot9%ٽFR=r!*@{; mk/ձcjqa^idt.G=gDŚnu:jD_xϜ)?# +FjQz4 ʹN1SLͲ +@J[6dP􈫅҅5 +1+SU2zQ `м]-y$T p"w ŕa⧪7 \ޜO!~Z{)܏H,Dw~x~tЦZO@MbSTe~ ~F홃ns$)?D)"w[vRտ$Ⱦ>$e;?">j4#i.5!7f*b=ᎪA߬F&':#RmPzEor@؎Ċ*|گN顯fk/V9=:`wJPF\6r?Bs๻ +"U-ڿt3?]??׋ ?Ejjegy>Љu~a? |%)O,!i5Uf~(]aA] K3zFz LJ5fĔ|w('H)J&ŜT8Z p|]Z z.i~:{z z#)󎻀`1SΒ,U}UA":}AZ龃)YiqfeY GnU;LXۃ ƙMW(!-;lzLeQ1aZҮ @6)_F@U85Fͺ'vw*T,tP ࡰz*}"X x|&z$3Kq̞7_̎e +B}^5bnWP{;pq=R0ǼL_\)RN#HL A-X&wK iuN< +Mף|0񎸝rn+M8BD@o/:h "Mߡ)v~u๢mfsc,LX0bF%h&M$/.Iji0#[NpES|IXahlR `o-+jϐR#[pʾ:=/J=ZFwZ0٣FK˕f?"iNßFi2c"~| \̴PtilBsL>'ij׾;aAbFz< Z]HICڿ6&!%EPl|p&%5֋2e@QdН6t.&ODMĴJ^2qZ@MDyJD+q&}AjF5&-ȅ$ +u8FZpoXþ㼬|F3^e3K21O?-}X{OX?EvɼJaQp(ZEJ: ~K(Cqgi(I?['Mޙꀏޏq,i`vC {4+WXjEKĈSTIͪWk4JQr[֚@ݺ/b(|-dNE,1 3"U~H28ΫQJv +q$i%& {Uy^^,v``{ DCM5q?vݮ_'бӝ9.C.7*t2 l'_%sr.kFnʍ=C~% }oOtNZ_u!w}Kl)Ų2UAWF =Jd{G}UrEO 7w@X1ߙ쨡(UvzCs,6y:]/$g4[fSg;j6@C]I]cyL*%k]IE%#<.YLΊ'"&nIVӈ oY/a1|ks7(f4b-#&J1(5x6<[xMġ8宭D۷cCT,R(t&aKg@i%Ԃ&\B[sa@ce` =^@Vvs+'{Qb]1y^i֨+):b:1B0mޯjU}\=pVBN7MhkGC&Ɓ/Z֎RoQu܎WV`a#PZg׹# LSݥ>ֺ9Aߨ?W +q.֎yU|2U%gUcm۳%uk߰uzH +V(>o ";8,X; ,m+9-˱Y=3uKoP)L.vK2&Ui̸r֍pdh5h!\ysyH34UMCl+TM5(aGxoG((1#s\hυXw}NS}/ V`@ivUbE=0F]BA~Ԫ d]#lzkWDYA/86?"Ɗ]6u\AwQ+%(s`wɀgSb3uG}1T5 ΋DohN1ehz/5Zwo 3"kAG0%J%y R|E}s$N!=Q;s +uc=ke ӗnep*_&[׽}a^3'uŃ(:rx 0$SbLDŽ]Lш{:rfdDiKx8Q@!0B -_2_OIG2]ƆU v؈r|uP#i3fQQf|^bwb߷~ U'(=~(1(v20(G$W5!x@0Buc OZ8rA,ȆN& Z2B~p1*QgmvG+8dqh C gb{Z2 V r[2wwA@CFJ FXծ8,Xa@:Mmqe3TךXW9زI)iuQ"+DouDWB4cRV /$FNhFzPq>!HJ3k="R>?bNG~>Dz(AamJAB![w8d>뀲܈q '֒ؠi:٩J R7tU>B}d0j+(*DQ}?wi{g0(m_B +G{RCfMu"N-t띻g3cOɹICzD 玸v2=jJ7TT'T,;5'a <&oo*%l__n%-g uf.х{SnD 6j(ٯ\däLE@] ͕ vї[vlTGZZɤE}SѮk]HqF<T?NZ [ -Plp5GyV-('anuUfWcsڸˣn#|;eC .[us'4dJ$PU htEU_Kb$IRd.tyֿ7_鋕W߿OOwO:op3ֺɾo8X -xjC7&3tAθG FfcVmgsl+UwW=Jl EwQ}?:W]gŌ'k6ApN`N֒m2:XSeCQQ,u*+}=#P|Ũb'~E2H6JATnT~v|F+JC.bLE F̆;m}/XOٓA _;'A3>(HP$ewQn[z`-y=A#=/DCD间U $QyDq8l5)fcow3=:RyI#a^Txe#=Ljs2sȬPq@i-"/x"8G~*Ojl~vwڌj5wZUz:P m ?lyxsΔh}1fsSZ)" ӭkNչЈ6$=Xey0ib6}C"pGRweœNbTuhqW6X^4핾Uo=K^JfŹɴsõ +r[?{|ap!wFCbcp]Ai[0?,D]lzĦd:jDtN6طBWuƖ!&"vEBkhhާvw畺~F+&4:Z-z\o)k4z<%a#Dl Z1L톽,sPmAf.3n#j7"z!lx~"Oy z_H_Fx61_6,*ˑ/L#Z vui"K +=dzIʶrg4l>"OwS;50 Zݻ >Ӳ!΄~ ']쨫Y= Ao` Cq y i$ mMzs#SZ:J<%!Z,k{w݌tn]j۾WslQAZ|sശL{ז;V{5.DCj婝jqZs : &qДvJ<{u@+W;q`iMO]hAGřvy!Sd ˮ_G=ru}EC5F\,ujcf׊9b 8 Re/Oߌ͊-2KiIyϯ22zQhcQv0*HwdRJ@gieX͕Ls˓ugC +@Y v״nvI~W,Wʀ #t8E(o{H1RVZZ*Z6NrVp鼿~{i~b `!0Th=I(uhexbYLpdmjdյߗGG=;KvhjcH:]Dh9 gp$ 4cXת B\cω]QdO#LXRpO+žz.Xgma%AP߃>_E.3&ߩ]Y6r5+E"5O5=ͻ+(*iuW| _ +a \~T՞G^J$ΪOW)Kk Nt;'R$U7>R *1^c**yR= zn\`,2u]n`~aYm"TqeDc:0[`D5!3‰ay_w5PTNuEtMܓMp gEcWxn[qWް/DŲ{i{ &"; foQE1pz5q^;s||ׯFѻw;$"ҩGSߥ +o9[{F$îlp֞<NoF dx {:(wtgok;I#Nlzݔx5/~sPڭFR70$ĝƅ>66y4QL5M1S=o(u6x3?m⻢cжBڛE[o!` y2#9{GaK ̕-rﯔJTcݮZy|'a=i-+CYRV)ydH 0|W)~]CsAoXã՗8p4h@ʲ2~P&QX +_ZysV%ڸ8)E̓7o[įo?NM)ߤul;dGyz6ǁ<7"PitZwfvfzo +Dxm=\QUDT)jD~ȧ&E]ܴsP *SꋙɧMx;XsbF3o|:=TɿQ%1e Mhu 3刞͇ġx JbAvo"ޓ'}QokiT{!-ӃxtN3 痀˱5ez5( zj5&UId{5kD 5iۼ*  9ep +5%&V? ;޵V_~4 +GPR#Ub ׺f S7Ƚ!: BUq|}L]:z 0ΩiB˛ +>Z ,Ut`kvhĴ*Y5]8g̩`p9aJ~9rzPTtG~:!8Rܪǣdb8:r=A?G{liۍ]=c#)'w" +?U|ZBl*aBr1i9U[';fܠYL~مо7?upP0{ f%p +!hkcu#v'4JM=zҷVˣDzP#9a0QR\Wɜ 81Ӛv˶`ĶoL>ZxC d!XxsЯ2~ܔKT>d\?<^%8bXF:';bfViBxEi0΄p }:ʡOQ?Slx}zwC6)kDTΣyX_7RGreZ'}*}A0 ʝ q +.tmE4VJR(v c NIvo=x"H. J~7e)֎@0@ݪuͧ>O"@߈k +u0ho UP5C&VrDy!;=֘u۶?@nbjJ?hQ=dQ-M8D%`?6uGQIco  ~ldI+{;v}e *P>rLN +-5h;o#\5b} \FL[2Bx>o sX:/5tZ)qM±z){J 8udROxoшֳs ن?;P.$hKRUlցT:͐4ke;=Gl|j}< 6>cY.b }ߩĦ(j)ш`敻ϣ]" U xZӇ+__)kZz88peSPi`;"IU!35>VG-;>v $nq{n5~4cYc'&$4F01ͽuS3E4ځE"e bNUh r腢%S5F$gT0cej0JG:*Ы tJ+*fWR_ raؔb({A@$ϓ$_ʀHp΁X'g\zpȄU.TͮxD6fMH2~3."xY_KQS\aWJ^Ur%ɶ?-;J 4T$?11_W3T.dF!0PKv9WQ9@9ta3?=)޷Z嚎Ud"sZH Yc;R3t?*z܌b{VՁm6:14V3lF#;lKNԻD"~t\)`l1Ӈ<9wnUZY);h?[4ȩo$TۜNH\hf5l [4|~%lٳvlZTҙ`5on,maiJ"Oî3KۏI\IyKsgP|r~⮴p@<v +ˏo !^b +&L%Gn;n_ފh4s!8qۭ@+h .Ї.fβ&{*P_/goULwWas*;,$+J2lTO@DZ;{j~pFDHOU72H:ЉqL'M&%n7ʝdVWymi<#x[弴b cYU+][A-(b5´&JPj_+OfO' PtFG[見Isg=ץ+d aR.~yZ #(n|AP1u*%I21?S5b v+5N*~Jw)k^{W ?H^Y +4m<-?asO4HF Q< hso,NC8 D@H ̱vrw&XSbfSN$~Ku?ZX2kݤVu+97yA-sº]×\ e|*6q޹g2?<PXbdwgo~?V v'_;Z?l1Tʒ{өl${E.L>>v0Q՛b>Տi' cW8P +F3qCScCȅ$">gUdz <];kH 2K BZ[k6P3,M5KTr'ߞ6c`O_" a{`Q}NԳ[ +j}uV2#U^~ֵ:[94VÜŅe j3Zֿ{r_f0(s*;44k*Uضͥv j`a`-(}ݝT64kBRߐ^? -&~ +!?݌QX7~ԨLtV$rzu>F.:[t wdKƚ"JEޯyb)IǰvN-?]1U{CT2oҼEg53&rk:GzơOwHi*o,->s˖ 󲱄],ZRAh˹Թfy",_N G'a,9$ky؈|ܹ/F3qJğ>U? TQzJ񩁀%_73ZV1T";F֒1nm`uۜОj@BiŇ-WakD967f +y<`rC KN#R7LI(;>-KY_-),O J ^X_;۶>sS92 Qi? P} <{D|Kë a1+րfVQo}b>[5AdThBg7 pN3p$\S>0PWD.޴Rt

V Pl _e%PT[fY8v0MhAMBN7aټڬͰ-ќS`989[.{U?i[%u|&.XIfX&Y"4_kײp׾R|u (\O+m4\@ZA0:W{ZeBSWx-]Kʵ%Tl5FZ &3lT篡},{mNE"Lx^o(@E +5k=y+ c J W>odFTcȄb +35; aG# Sp [MqCGqY1[6<@;p*xmYdѬ_z=;~a#-OTTG}s_Wb3k$ mT^٠=Oc 3QyҗAk 魙Z,*PtGSzPʫ3|/ z}>hJθC޼̎{2X%uEV%+Wz 8e٪dzEigAkwdKxuq!-eMA ,VXFŝ+T2G Z)a5< 6"ā<S_=ԆROBi'MtAb(ykGa5%k.߰g5ZTrJdҍ{u4٢} k;>*4:V+E΁A|ԅ 4i * F5oRG XE@mVɡ5s6V@}"[d(,ߏWy~lO~G{Ԥ/ؐGBϽAm|1xZm'SPLǀYu9` ":hWEױdxY91vh"86?4^ Zz ҁ,dO }'MUD/3\YWCR.܋ +!6As7Zbj/]bHuo*uK&*q~w;w:&&wS('^f)/hk) |i9hԷzd#tv׋v<"0@SxMEJ*3"]m&"dP=n]V1.j=|Uz]׃ʜ'o#l +fs3wTpcH]/rlԹXHZ[ ȓگyҶ;{+d7C{- W1姽Ea;}m7%bp\6LT뛑+VK*ojB+y?#ki}z@.9|໷.ɚlθrFLZxET`ZYʆ̻߯̓QN ĞRZrWꒁJ1*Sš}MfaiozUm"PsZ 6c=s鯤.tSJNոpK>$!BtюE5a^G>ީ8gӔ"*$kI?oAZaQhLk" !0/dXglwo;*[@(vDea3G~\9?/Juڠ3?rGiUb@RzzbywzVLlA*`S1})y׀J&++o5VDJድUj۶7KDH=Pb aٍB*Q|\ +lMgE5S&Z;k#I]ɼQ@,B\LliZOIEUCXV[][F~ힲO +Q؀ EPkZM@1@6~C2&qn.`{EHŊ$YD*;"zUs'%Z,ڰ\OTi5 ڝ< ;'5H3' cG[dk m뼦}K]6U*[T 5&]Ѥ7וH!?[v'&_L5^O^O5ۙcN=̂iƸ!j"tj< 5%3&~ᦦ}+/Ad?ZۼjA `|n-5pv }@tZUɫ0]?(R S\QXn ?KzXz"AD*]怺zފ=]˕El]1(UZ{Z&Nϝśl\je,؁v;6׿8$zDĩ6)O]I +nJ#ڊ|A,>}]9^l/8pԗٌl5e݆sd7G VΚ+ B$vYZ3Ϟ ҔlmPmՀ]Ǖ%I1]/П)F + v5y^!e\GέݟРƬ?NNZ?Rel.JBT  yVn_وZkoS?]|?fVz'mGtMt9ː&H^(AlQ0`_{'_$}x^tXBёb!|W8K͙cM>4#(O]&n# R +Yk:r + 5M8+ɇXHH$/ 2{Da.R=88+֐~^-H ߣ|J½*Cգ_ܳ+#qs`ٞ +XJN5j`UNG}W}8Ou" +xFG2jxGS2Xav== R_Wr)nduӯLhIY(01BDr 7O:ύX zD;YAy'q3Wuw+l|gv~o~tJJ+ӸOk +ڷE0ouZxbo{N:H\XN(۹i;Ktp\*>"zf1c uٝG$gdfiLMT Og9\/H?v @J&BտM#nj :3V3ҏ(8jX'~Խs\Jko^:Nkr0 +eLTkқ}w$kG?k&Gӯy6P;Ca7,N@Cy/emGj8f$V/8-g!9cggcE/ oWSmn\[>.]zYߑv7$~_gH'Ϗ$ $߽)9mId"7 ++۞L.Sf]6W:o(\+wfu5dz`%h~S|ʕAG#+#x4ngMB߾ؒGqeW:~cS[9mw/#Y?T3Ut&=p2~(^kptvREǧoaX3E7sc (E+$PNQT&%:A),cQd)ƶ-Qt=gEtk]LRg] +)* lnU'D[[^ɳ<{%i1U9rXǛ# +ʑ ;<&.by# Wq́Lf.ޠ +~8 TMs7{|CeA>jVDma@/uP ++BzHn i7?[Gg(kt΍HXѐ@}W&i Ir͍mp>?Ds 5f[me5FNvlv +:\j))QK>وSH1Lּ =2PmTx,r3rmz{F3L Ǐ8i\ ]k_MڙJ=e^$UT0`~ҵWdd-bAd;57w*}cE]wkb|.є,Ⱦ&P{;(OФpᕮgs2JTeD1<yn{lD$ ~4gr0^2EE`?"u mmvi:'?R;.&}%mk<ιU]V⯐BB3$׹eO_NW5TQ_CVKdt=/K>?w8STʁNM) f} d1<*}Z?8y<7T+b8#PARRg[y-P[6 G@]9*Ni6;S>#j b ~G<Ԗ ckEWnJ׀]sq ^)=Xט[Q7Wh_{_PCCk? 9W +.4u@BRʎǁg+5PEKҚr4%JU_oLKTY~2*ws> +?B@Y(#@ >vXKk=}seWĻYFtz34`Ȁ5gk\*q(Jz{*;I{{Oҷ9΋Ft -HmdNx8m޶gA&p6&d@ @[:XQA?ATw+Rª: OB7da#RrڲC/px`̶ $oz`(H@% %!W&9L3jh&kmacl +cg1Z(av}Wohg{L`w`2iٺ4 hqh$3C/R("W@˿qqLB7/$ H6Q~b69'P1LUjV]{j,>H\gN΂eòi .Ku}?HDv$˚<q} 0|D;}||M*qSc;G*+.]Z3!O\+u>Q@KP[Wڄ_Ly@8h"<.i]F dy&oML`GƔ鹀xu}"GМ@(S'*:LRklCh$lUIU4spPN6)Ukk.la޻#h{;#{o2QH"Ԋ-b>xرFCiatjɖUwt B "@)m>rζ^YTw񇡘Dɓe@v #nжF(jH@` ^@(ughi1R u\[&lYh3"_cF[C#2(Lď +F5%f'ܮ@"F7z|6sx_KBq_dd7HXYݤZxMZJG4k>M$a +?|u]c.Oj3$5ذ3`~٬[9QdCggZ"늒C053 uOm`1sZnVTmxnFDT c7Węj=+VV1Jev9dmu3k׶l7uc/gDLݎyfݿb[;bUEys*߱iYy-=8\&35Ϩ󅱰6l-4b-:zпyj̏H9AsCmپ +2[sn2BR2ϨUdX +w9O٪+U4F Nk>?ÎOw>#XM?V{Xpm?AWdȶT Bq2[ P)6QlPF6&@\I>/~N<2[JTh<(!-W#(Q҂quY8S Z?ǁ܃w{j[ùk])í Z"pi3"To}r[BDk 87:?4ESD=wDNzPxWJ_Rx.fRxքj (k;MJRV>k\@J@<@m2[ OrE-+v01K+. MJp?}cC#[nD"ןD%@p$_yKwk|/V_/????~?????%~us~25b֗럙/ *at}QٺX:/1,B(6|DFuF\ňF94[CrϖoDJ)3ů50*\,WViQv(9z}PSeCP7a"N9 [ƻXFʅ'/bvk0ó^y(#V@\ 2ڠ5zR?!g +L@ e#^+P'_g(䬗,\_GRHvvDd2RqZ!%8^g@؎@i9@2BKJɢ(а[?|okdjb +NĊ4jc5>86ڞE6.kP3hh߿݄?U3 QUsixk= fr' by}cblȢ3H +Dψ|+VEP쾑kn2C4Fty~]+MYs!T `ɲYA]d&#mÈ5CY1kACgc495ȤA_LD2397g@5F̕nsz?$4l*q=q@H +mu*<z?kE>Ah F揨+Z'_ʀMȘ"9qj +u}6tXVKA&4 IPU6E5l`Ql=,<8v@AdπMF1@*wwLL˚tM֕K<@4@CEx +s'd qs!b*m؎n@\퍠B#~?nG @>E[xCH 7( ! :o&J./m,C 9:׾^krAXL4D䵌GT=X,!] +#I ܶ|eBS>uR=ߑ*I~@$P>{R0Y6N^_bJ +r]m nkp 0Z"Ph. ]U:?^cBgTbr>pVt3v+DP&=%:`QS+C#Q6h1834yޙ;dp\c7PVnH_^ R7)9!p(}rI]ڵ*Uwnc¶~*/ΕKdFQCfRU[cwI5o| +ˠOg^cLvٌ_pq2Q$H5-0Us'j&)U )zק;S1RM-__iX\ T0CþI!(]uiyH*Y׷$MQD@%R f=C|Z?KG|z* 񪲹>!a2hW{lU4Ƞȁ˪Ěܫp `VM XqJPROdDk1%i&BOuw9uYn)ȵoA'MTc_EBhyY9aU [{I^(N<(hwڣ╴s5za4w8SQ \I٣6Z;rnҿ'k7%?+el1/؎ulu%.8-+Ǫ83g\w5%^C?݃iv_LfW\(l@7A̰&忔H6UakPC +JHP<*p[긶*d4MJ jRW AVUgHՉPh +4a# +]X#ʫ'Tcx蠌mBug?2=;LfoP;Zeֹɭ'IP=Ss ,I'كAzΆyrM6G%m;k%ى)(6y,v.zs%iO+>& QsUVlҳF{\Š8ޘγ/D-pon8vwX[dJ/ #췞D`u_.; aTB]fj~2?7X2G.NkbpYU'¯e.G+xl]; vq6̟,+vS]RcR'RhVt#cX?#.;BAI,|`XРTb>%4;RbeV|{*ZLQ(q7Gޒ[HFFU"jF[G({2 +iCD3 \lRRrI΅*-$H" *x6 +|C'2 +f%0U/7 ȅXvNCkK#hM6><{ϨHIJgMzr! +֭3G?gA+{}s jƛ6Q`p׮EU7I7c"T(.| I<瘟w0"*;hvÅ3!C⩄GA] ]3U1#z":TRW\3 +HӁGD4Da$P89(0]f2e#zTU]U{;>2QJ8UmS*}'W0Q&~]$FtϫZN^Sו4hMIIh'5Xq$⮉N$?a+#.;g6W8uSĨ8Ҧ" +rmj6%K*zF黋*> 90OGqlKwx*E[ +Գ6Q潂니DGp y{06Hnjڋ3Xc'}t; pד^ >0)Qt=ǽj$#UX#&'R{B+ L=ܯ+5[2뾟ټة~gM0M =}:D?WJܛOلˠ\PБM[) +$u5ΖSW}*W1pQ=eP-(hA D#[a:[RQ*J=d.sr%׹(*HB7ٯ\ jkwL3zDmUp :s|L(`ֶ8my47k&S}C7Ok"7+!q} Y|J3΃: o9ϨyYi=0[Ih$WߝG]9ױ`kک#M[WQWb{zvxůY}- +xT._!7A~EF-F-'Z3QGeCA\IO?)9BmɆpjn8݀\YН;/fu YUu ƿv5r3w(q+@uQP\aۊ$}g0VZup=GM8 ǾcJ&UhʹcY>#~qu6m>9+rW}˳Bd'EHSBmx޸ b n Ϗ3OYՔG SWK-]k@s3{" +]Xܺz"\w=DM߆:6갗M8CĚ(2 ڞ(Ր 3;RF#Q7褎Vӂ#2t78'"x%ʹ!+DLƵ|WGˇ=Idx0`]4ߛvfi|C]ū5(Z(5ؘC*n‘TUb2q;ôT;'&UZ`eܰ c`k*L]. rA6ߖg{xM-0`}q<3*;jjW?u\I_az7z'!D4kh&HDBG\ 'g"ؗI{7Sx$jxӧDv 3%SCӗܭ ՞a_|hQϊi=&au;G0X <{(hkńW#}>"3IӄXNۉs,E=I@ 4?F_' 4{sT<{R7ү!,nz3{"EN5.zFfX$3kWs\J& 1nkYrrWcԕtcez)%4%3gD Uޣ.xNsL7u,لuJQ4z76As:§l:0M~k{'Pz3@AQrx{0cxl(`Y bRc\וCq%$ե]p/$ ++B9",tX=s/ h7\RX a-$uOWv M{o}$u`[q1~zG9K` M~A+B{KMRT>?D#s -x^ؗAPra76 tP]Y8˃2rtE"s=ҍ`EN=ۤ(|HC$D5 }/W0QI$AfgC o37tЁA*rw(2}F|,^ߣؑӕZ +ZLp +o&i= /+5v!qۋTje~e^c -'gJXv#nqFe*D<oHL-5B> +S:UU鵿HѭϳlJv?X&F}0KQeO#lQ7@i-&OdtGh4r7  U^mH(^Nx0 {Ueӟ6nhC=(\M y8csպb%)Mf{^dv7Sӎ59ҬNŒaE uE ieћ|R/HX?3%$֣f494Rі3]p6~Er]Itq!D5PFIEDy~@B]SYFP`uui;Srlo{+9/MrVٜ Y]PfzB["P +k}os. _jl冚 aAͰkT]s@f-dI +rȪ瓣L}wE큞똲^Y`@}0ϱ#N Rvɸ pgcr^~.aYcWm^sw:)i&ٙU,KXeot +QՅ#_62-k@J6&hӘ[% OCQ!ug[bUkN; \jg_vAB@)7嶖 @҇lq9dDF :,0Aa1C +:uqjޥGYr%:c[|$5964,D08ǚ@BV?i xGY +@H< pv!q~cCr wr-PRVa;Z_+ EʌRzetdjwܞOǑaP@zЀʟ ͹OFM=~@ѭvFï# >d]&sujievǮU@m:YT!Q`g-@u_6+.IIb26Ɋ峻~uqŤ~paz\/N1χNrvnA ɲ\j<=CX +u +%_;_,c'y(H?@?h]>נ7")n$7'QB&l 2'WNw$ EU㡓4MXP=o*>#W?R#J6Lk9O7iU0ʝ'=I;W t/i] "r+v^} VZ#"6 S857!M8S76D֜0Kϵ쁗2T^q<fgVHV!{5vdxLUflg\dBj)r˼w Hbs_iL"J00>ͳ@e %kGj'?!E)$Lل"S}5ΩNպ ]#bYU@S֘kqZɂ4|lp*..Dlrv2r(6Vk B5?ADb;*upQn{pՔd++Wo&P["픞N ) T2JcΦ3|v{6Agf3TaՠLB+hqP\GUf;z;*G +q# W`Ǘ%Eyٌ&P;w7:H#V` +dW(gA6Ojkd(`/Ve}ex V*EB t>hK~{*BTGk #`N'>zf!>Ch7k 4A*tjS2 DQ#w4㏥MO\ J¼V`6S kaD}tF->6(xE+W"&KU}4< +bSav:qҫ.Tv rC+lox3R]Pxn*(mš_ JKRU}aku H=L~2-.1d@X +0Q#z_b2Wz5oQ`4Pq^n'Y{Vk<ﯬCJWWa Gl+`,(u]{)-c;ݣGwq,FDM 3\Vx_ +< +-j達rc'˶~` qe}[t4X{ ;ܶPKЧBq1 +<> =+Ip5c5`4QI,b82 rLqc-('TsSǺj% .:|2<Y; bTBBwTt$鯘1!o?Os3ٻKl?k? +Hwڤ`k!9MF)sOձpڵ7O=%g{/+pAb|AL;<(W"tGt|pN54a2ʮ(1` 칒^bg#)GĪQ,OMO>6/b¶`h6ڣA`JCj +a{`TjCX S}c.z^ZB@h59PgwdY8JgVag(c{Z[0QlWD5I/t"#¢͚0%B>Y1k*\j0RE;r[$%,)Xчq_N>\"e -;"6n'+޼U;B;5KCo# ez? +aS%ݻ)z/d[y244v]"[~C4|Tj}yGDւl5hBwCeG$.\oBo s}Ϯ'P0|ݯ4\Y,}i8%.`\@_δwqT@cU U|Ļx,i@~İnwA! ͣRtcrT9x*Za6Wx}#~0t8gzdW*@h1QI90xR!H31LOOD.D31"VҊ@|;#/Iӎ@,V +J¦oOb-pЙy`M("l ߃&bIc(7[m[gH@lWQ5(ujPh0v҂%ktܻn3U%y ^W<Ĩ8wnf|eu;$Q<(F@THnVEh{J0E3NK3r.#CִO!S]9A4u 'IrX^6#5k$M׾0Wh nQPk-[]6MU(Ě~UdM,s!pګ?lm"έ(pk n7WV nޛ9ipE2g_^; vi)DaX[6<:S̒q; Qzé.光iSFEОhhP_04OnFߠ/{+s"2}ѨJ؅45"$^-zɫK +^yv _gI!UV54C4?f$ంft 9bdj)g%]uOm_v.LU؏QlzD\0N>:n'lˬCDFdq?,`H08N:H#ʌzr"WZ H/CΚ*9 q΄3~\#Hp[#4|`2ӭL< 4D, 3"(Us1o W? f$h { as>x+45> 3K:Dh/\$DW0 gڅ+8_d^XA'); ZZGS87 ]0ɻv nh﫾tUfUFe#XC:>XRiH;ȡk2e˃nHwja8 ܧ%5܍ڐDPȭ}- +yC?-$Js&"h(05+b~,)FNpF"Qo7 +u`dLco$ bD|*odxXǁ>T<5TTpQdv\Dh#zfc |fH*Mds{cHST`߿'^sTi#pfQc AE {V%ׯ2x#"p, +_mJ!݋8C>@^?Wl9@E@_ +_[YiDnW2S_/}dWl`mJ'2>@nM9qN ''8'ĈMD.lQtɆ:-$\`8i)೦iZ/_*̄V3}Si6V,>Hks`)m"2{.,<)VVa˝$ꪷ16]OlCDMCGXy@p<"wl`&t-j~z)Z I#^V(|އxmQ%Y:*(<+v1aKu`@@xH|7-nbΙ|ɒ,0$%AT RnGEv&zx\+;Ԍ̢ۼasW"O?(E8wUh& Bu +Wu,) Tܣ }X*/+@6d?.2r螯Z٤W_(36X]r/-Cӫ9{4fD9z_-!@*H? +X.c0Ԩ`e2Q KN[HA c x킴{`@l1?]K@ؐ*d"*U O[G>jS*WDo*MV >-ˈ~[ %L@:#R?Sr]I^ 0^>#g#nN u8N9N1Pזe)0 x*w2B3> F`w:@͉5⹔l:+gn?p|l{[+pvv$wkjj>}nݟ "%Cc +璩E.)@x|FL`L:LQ9:PIBqu'Ǵ~UZ PU}q(L\޿җ~Ǡ,{7k$p'`@Ƈv0[U5FPq@rAwd[a!GƥlSdwqV湌]T}Α@6E>"w;<}h_jB?|eD86CR6Ȯ}ۥ1gH/'pՖϐfg ^Gm đg9X]_+5Ĉ󰨻?O/=2.J60AB/9(fM ̎>4 ioVK7[^I_T vOEJ"=;Mu}kR`c9?I\/u0 +_(,A~"\6y ԭD@R +zFj +4jT0Ga|TȪ}iְZ_VQY+-̮)bWc " ei<yeiJ;V@EϰOLR.cG {&SxA=` +.f8Rw/7Sf Fɯ1-p~}K{%|rP _]-9čdh-YYwj D4#jP8uJX4 [ApPM?"^I09F&]1D}0Xd#RGCRJ.$rm6)t@@т):/,WPj] ]4ណnDg"kV┈{W5ol\9$H@1'4;Zj[Wfb4VyvJպ +瀁ɻ(2~[#4k3J<~[FzxT̡{WBi%g8~]^2AQf}wnfG FʅFWq3}Cg/dr׫_X#l^N"'@lCTi~iܝ}_Ř.xA@GxuAsbDf:[všdP>i7n8_L@?ɵ+Lz$Qhk?JZ1zٟK#';P@Sfu: F% @M$/qe}{rBt7+L +Ig7LmU:ව1fe_ڑv>US//4bXLFk%ץ6F<+{q;AiNve#eC6a]CS&q+Lm +~B!?]oN}^9ov{9JrYMD#Z^E[wxE*~)CD^׳oܴx(/&!⩙4g{mɶqЅ!'y%͈ ]bvFsuFh-f럧jE@'ׅ1/s:#B> #TYI+HzU/QJvJ]pGm%vuJ3e~^DBHjB|(|~9>I 0 h+vQЂcM.2:0)_otk `a #y[c׸sunl Nԕ7w"J@(UUNc(Qywp]A}y͌1˹a::u)xHi3D-ޖ4=~8Jt]A\UҾ.a,(b/aiVf?봍jN~`;x9kڡӥQ3$' *D_㐺k:l~DBNHK#&;]5hs.M3)iq'wO G0W+Zs^j#=(T=C_zx*҂ner dL~fT74z紅s̴jm5=a?@U- POxp@W8-ysF0OȨ?`#̷Fl~1Yf8 ; iQ@""o>1i lu"d>"aG~$%[3:ǡQ&/` ]n0gTTZ"˱N9 f o Q?bZ%0 #S+r5̜폈:b[Pxgj!M_[$G(?kT^ 7!1=j{n}E&zG]蚨#P"bSQ ũDG-HfŔiT w!ڡ"ˁ~]ľ]ϯYU}O`ߪDOL;p_̢+prm(ݲl;ci2cOOCiR J ߣ𡧈O9nЧ4yj<MviN-Gru\R>\,cT(isurO, ۨfch[03-Ed:hFAdGX_B\1wgFm3 pg׺jۅ^s:eNjϗm@v(Ī +U|`/H4.3 Mof"vҪ+t=x,TY@>#R1Ǐ?/O4;?5(~*O*6i&}dOoNg1DkYsx>G܏-փ +cIVP8!mCޣ֩Luw6ԥ2rU~8`ʼnؿ/58H"8o[@x3xɩ c|DUm8VUBl) uV +r~\P(|w|Є5F( q +zfdz_A[JQHbhPw eAɛ=ǡA- M- -t=u~!:tAPGFuJǀLߣxj1KH/놆w"n9SM ~yǹ:ł]x.Z=:4ز%P6VH{=Q;Sz~#$³gKeeH-hfZA];Z,pF)6P݃E=xI[R[|Bki:N- %[#։.*ŕaK%'|֣-Lu?Ǯ`U}+}M#LnTlv VdH{SѣjRU:Ôn!yWĉ.6дL5\N<6Dhj=:n3Vr]+/)MoB01@ʈ@`T<(qy]_ĝegPS>\,2QpcuLk'Vc=ķR^vM&8aHψ::Igʃ]ُiooǿoooOIqq +s̽/˿+ '!:OC;[SҌKG}RJAAOr[Eov-6E:IgxL"w0#sjRcVt[؈7>UaQE(Vh4Pe3#wҲ#툥}"I0[ϑ6Bd` ?h:#HߘV3:4}jI;v!;.葈<X5"CD͞W0[Ugv+gl,coRPhQ]D<2w9^ Jv9"M8Q@SSΝPM2ߝ.TLdoot` t,wZSsFgX[޵s>W9 kd#X? V,0ww_(]B<w(0!3L&`@C8׍(;^ح@(O ql4z%.n"${<"0ehJqbdu{eT8DP9q| 7,nȜ}3"<w8_2>-X)mM 옑߿s o9e;sù<^$KtݜLG!G#h3ޒWzd2畂l+uG Wh̸%UPPRLop]5SAL00:O<` %oN]\&=1}I,a9Z^!2sG t&1/%蓣b.Wha m"|yK}~X^ל'n;bejiO^5f +.r+9KerTXB>o56J7\"*`XgN/ao>N>oCJZ--\jm#iEџ8S70K@h_BƆv@?87D ةH6ZW19ϊ7e #TeFĐNKT=( #'==6>  aҚdKV[.9Dc?R7lWLY *$\'72UT NjsڸS"AL`+ŖX0D7t3~'Ⓘ3:*r-u!<룃GĭߜzE@>D,ϋ.緘!Kz+>@N#hWΈA.\ȷ-t{\ygs(#*GS?Rp8W9TGfݺe؉1 K"W(L6c@dDW'wGfP1bH$f#NBwնWV yڨXмG hv{ }| .aZSHɼ(S]k0A5Li`PkETAtӈ/ϔf"C;IDEI5֤N/ȫǑ1ef)m\A8-"U˽-7UlzkU[@"0 +У@ëMf%47P"lNcXKI'jqe 漶^ItfCO}QkFGUQ ^X޾-n}{/~!pgw<]EƑki5{2e(ݯ.q+8eiʆ;9!]kAmSeӔA4X2tN'M#ǩ4sҒ + Y"~0G̨60.\zE30>ǯ,2Ao%ȑ~%}~%y^_{ϫǟЪD +QRJ*{i)o^z~<.`ED[\) .KGs>DQX"k';빏<| wTu.Ngv\l̴3#vwvXk=ag+8k\ #X&fxQ4{gţfYkdKDdDD9-,㈡Hp0H/z( Dyth\q1,by +`<-./Myn0=Nμ >"#~Z | t|+5OSKTX;myO 0"e1G}$Ckψܳ!SqV&!18?7])3d3u-J[F=/H%t{7Ihvψe-O^U <ܪT#5ȑ>y/KΈAؾj}ȝzTwtJ_㐙(*;'?'?3)bA<}[W9Cw#0ֹVX59U*mj3).œ-&M2Mv@MM7F^Gݛ;+Ne1>BδdeoLiIp$(R>͙X>!-)o_&-IxֿBS + eVDv- +wDQD|A_ 8_5ڍC{@cԸƛj|IOڗ(]=9"'>  Ð6%fUK.=- D~_Rl-LO,2"WIgA֜ 1#]fx?zp<+;?'UYXf@8~Qͻٱ_=֝ml.&˭(m6ȱa`S"qoˡշ?õZ#Wߏr̫qB`Byi|̟?ߣHNZ@fv7K/ҏI[+1ph"y45aC8fbڱtd*Wʱa8ib- &)bLCHUvmLtxoJ Բzׇ'cTB'me'DŽ +Q.]#h[>2"wNAFDܶq_'RФ5xe/DgV~H$vqȜp}?vPd4\_(Qj{_Xȷ|vܙ)_ UX}کl㣈g33!tQ\3B"o+QmK(\gqvD/ݍrzԻCvDfQMӮkD}:W"( P_;Ѐ=[\oAl GJsWc@RotC)}y$U,nBnխ ?ҭÿGul!iQt[Ehs@ +BjA:y~gF:ٵBF$AEa4Q߯:*B8wYx(:s4#fz沲uEF$URcX{q KMLD)eW@&z/VAv*^ Q7 +^hnIڇʃG :_P^ b{|7 +Fnye:3 /ˈG|_L/_|4@WMGf/xd1)guE:?h}a-f@΃wͻ/G3[! ]Xԟ#`z-T}  !$&jE?] H>Rb&7Rg@ Fp;Kqh,ʼ`1="?BqZ췏G 1߅b@sHF1*uYiW뫪/e^^66(0?MgD=!H28"D-mPc眩FG{r0M< ؚК, ԍ]`=_G7N3A `"u5f |3b&߷4r@1PӼPMi"|yh3j+u+(< KGB柛n5>#|@ul6〾JL%/lkX&(%fm?{ + Ͼ+~]Uۛ18=ueA 6un:(Kahx`2*Bo{?##\%Q"3OjD梾FhVM'Ln T@7}F\} 4 "!ڻIe6u"7%[W8} e8)4-tIyx qFJKrPERG,íߕ}-1L&' tJ'%{wQ,Hwf?)٭ݿ]]˯8Q-#~RuL2kC"qG-=Nf;#,}(թ~ʹ`6/6HLs%p'cēGA25p. 1G\كK@ ُl"{*GeټOAvp79U j{*QFom4삧bꈨ< _\۸DM-&5UqJrP;X 2+4v#rz@,UFxLB1qk{t U2YwClFK)TS0)q&4Vm0W{+^/S{s16d'`J%ly %ɮ@Հk +0-A43 JJ}M@UsN[`] #)T]!x0GDV b+#"f"ttk_ Ё@UEqøJ.JygAz4Joe.nf?F(GbaWD +*y % e=L$uln˔bx3tNnG- Q\EZ玅0K~0owb0kugهd6aW/#F;I_7+-x}cy(@G: +c8܁@1"X4jz5Pc%7$,A"1Dp5>)F}l9Jc!}(Ic^h4í]5<E_1.P͖7$|Q g݁!loA1gHG+OA;` gQ+;f2q/WEGVc-_va`{^pxyAƽΠ"@S?>k_c9ЦQ`y4kgrvJDLs~ 4?y{7KpuENcq$J+:#,#k{UVK P! MrruW~d}wݱu1T{@~Zmjg6=" -qK>P~PPc;:BJN +ffKӍ:w3W"&e$ƭoԆ)f碾6b%ؤI(^MD"2>֢u.C޷iٱɬ:нձŷ/WuR1h>Q|"JIu15k!%%w/1_Z'QHLuX~j=% "bVKmoYG%K%GBq$ldd23lBB?7Th4=)uU\.2L(5_q"F* $Ny2&ǎA$O(OtɅehґg#E|ܟdt|}#jM˖?T뉨* 9ד%*ʔ׻Ԩi3Ki3ʨ[+v4,zM[ ͵rP".Dמ"+& .ZgCdPߟ}ck4F`㰇BXݴ 8G*A򫫾(ΏIQTy*1*={K UN4Dqzz)7vz&޺qb:]|]k8֒n:+CRqt?6oU]5h+WFw΄uYLpK_}TF˓Ż`sEnG+% #Qe^SЄ>USΟOhCY_ω̑.~vDkyf׊ l <]-]ЪSOs:OF +\t䴾N=h3)]}AuAw\~.@qA]p7gpjΟ>{(@dK!к~`Q^ˁzՅd(8@Mؙqq;ɔϬ1w{6;c'Se1!aydẆ>d9$w F}+dYhi z, @,:RCSd$A"s@x-h^9#*4:V40gŵGK 89& OΗPcKUBe + +kA),P? vCրjTu81CsJl>Sdqf&3߫N#O%t?0w`OGߢy0MAFJW F˹,v{/s螯H>(!aXt#7üYTFȅ@alireF iD;œh8(Iި, &67M6;F$ 0KtQ"BuMReu:/\ +~RQ((n!sdݜ_gb2JTw@W`A7~W\9e~$$vgeA^ +%֛GWoDL[P vx!30ҬFZ`kPŊ~Fg)^%iJe(pQ6j~>r#Ό-@OSE>Ie +:OH{)GD.}߻Ċ0=QCZ,2])bf""揩/}j,r +9(mɽt[D{@iХش_+$f) +$,`g %"0&@U@)%쁔I{ѯ أe[㳋;}qh; =V}?MAfvz11aSv.D'ddwقGS4dGrmerrjIבhEfrw9Z(ǼRd>stream +]&Ki3w'K{.T@ '\1ls ʍm01N$qJK~M<[L8u|FqK]~X]. 0ZZܦC@ZofӫɠDm-&};W+eQЏg?',2U?i4zקk +oL*}?x"Q)5d\|W4eY7UwZR@\&jQH3]mR4u+e!F ɴ'bx{rv-CVl;wޏhauܙi= +ktg?BY>E_"v(`sG)b5&vBT˦{FI,KU9g51]Ұ^pxßjP,d1 颙+ئ*^+H|'3絩rf P}#7E75Ś hS'C7+,UV}}znH̝LޯY"K(݈/)[8i~;AR ?3|z""j!} Q&(N9q{Df7BiVe|"$yw!†(qi5co'wO[bLPHQ):{[S!AnF^:0w΃%9ԋJUEczI/Ro)JȈ +uv\zdPBv! |hjJL_%Cs0k&I_vZYVU>8X1 d N5XO 6^U&αzneFoXlgw{ZN\|vqwiBdIam3䞅dDCQDAi&dl=Hf{s臱Cw`V6:0DFr":d>& +&>nIÉOTףv;sKpFzΤȨǸ I$*tpd*(FjICF#gg>gηbRE5o@'VNp U\zFA#edZY/C f_'QJХTEn30~91ʱvFD9;z~؀Z +bU)ȓ ȯ3&hSquh; +/[trZ͹VNe4z+8ϘE_yo{РezvL!XIW–ڜy@%M*t/5ibCQ O]&3D 2_:.|H0]62yIgI)i,~0I(=5u.yίC^xA޾*`0gOT2`#?:7H碗eeb47<)?s؈f{l,k r1ūFtF$ B]Lr{^=:>T5%EIn)UM׹ZQ:qϐ5xt4{+bvNnzWRn_0irMh(B ^+aQBR8ܨ)l<i4Leo%,Uە\3h`CfiQI'Vᗠ WJ]iN!m3.tE f + =Aa*ko?]BW}U:^eQ\ИEg=o0P_ qÉ/ ׺F[Hsn" 3BDsi$ D-ؕw'$Q\-)4S)]ip&s5"׊TQ~N*w*P%^$ʒHS"qGU}O̥GzGh?Tj:"m({[TuLBAeK|svʧqd-.:iGz7?<} GFFBlB0r/,[ILPAfJ뼤J8G|vw?F{HsFZ?4 +ኡ11Ujm=>gȂwL!8I?<0;b_cIDkߛAPky_;S4?RaE +U ]`.Xr@Ёy/ p% ~X,5tkX0]hr#1 SZP.䷩>/01fW5]ҡ^!7OWϯ3 ,ŸD2$^bN!r|e+STK}H0( 20!6(?(_h|:oqSC{A:|n#( &wC$NM?Kc?rĝYFy |vp& VJ Ҍ%i2Ǜb1ؿMzDMtx(P0`7[E?#^ѱDR$(H {x/[%z{_z3fm_h;}m[:4 u)q>lj8,uRWdSO&<=6#H&}l/~\h6j<גu?h!>8wϨd9ݬm{o|g|m (ĥ bgDI0z% wv DLo"fa%|ohc}w]mEUEz]T3!C>V3̑2uc7b?0o"±Sg`9:GN# D1ȜDT*Ev9u;ctAQ& 8JڴHǦ,AG9]'t,7]*@-h'z](' K2A~}~'# +?`aTPx+o̓9}L(߁C3H֛֫wДO渨u#͹r9'uji~!Q$ +_݉ +4mWKDS$oæn:ÇxD֡/Ws_-,Isڬ&lDODS'9Ot>O*m-" o7< +P=_dv@'FKo4~8u,W~9[045K6KKE\ՙ'` ?4Hړ?Ѩ,_L92_a{^%ϣksP* RեTR0ddz0NQ`nUή1F6hF9p|P `Ht!s/ha,LTϙrPاBIQi$?*@Mj Wz"W^gi3vJU u{eX IGOPGg jZ g4=lO65f%QiNHeLREuUv1Q !FWKݶޯQYS5+bF;ti0F=}z>l650~m4iiL$lNi=oKWχ3?i&@X7i)gby%L\)gğDv]a?sИB"]b(,_-y$Ngml3]X:#&u*ox@.bW0z$j'"3}2?TO߆ܻ ڊʢ|pNԮy?!3٣JsÉINr!ZnCn:;B_Xme>o]?a~8&kuO{?lh+FmZ)MV[Dc]Qz|?{Ohў4yx^?\Q~W뚙82bTNQ9(±okn:n0|??KPx@ezO}O FՖlgw+%CyLB1]LZ ڂ6m!~6~Fs)|eZ~ cM;mo4SD~f]TBOP^vq#9F(?6[fQIHJLAj7x[YүM;jo@=XDs B"A wb oCBFBמqžӠ9N6<^FRcn@/"17m-r zV$qnڎvgQв%?>ޛF^SmOvA]u|xCR*G ovVDmFĦ-8Vi$u$K7Y6 +.C}w7<;ܥk9291",J^d+'rb=baG2Q9Qrԝ\ +VD q8k$® }t!Q1 +=t;_ztIP +B7,B> 8IT':?X%u62F'qQ: jYF٦ b9JSן(۩i, u ߡ94a5]@}FD:Ga]'T@!m#ʡgU̢s:(R[ ӏI] I8-[[p8[KK,|t|VY`אHmvaM >0R`S}|O۔T$я-ls=DBpFpƠ)*v%2z=0aRt"f"F3OD#Q֗u7PH_#m݊KڻږgzOwX7XR-iou BR0 +4YjVUdIs5gUA-v\9Ȉ ?Fzer ֹ$f⪂ކuԭ$*d9/浍Y)[o-T jMS6jŶ$t\7ޅ&ͨZ_bv0!Bř2 3PF8tkR0ًæAzwk>2r1QC~{Cg[Hj_0%c^?Y" QlG rÖHCxU@@[\ZD߫A=}yɉ2nBrx/"a{R]M2bYl|Ļ顴HH;gL#c)j+Dn\Loӳ(lI0ÕƏ׷h*@l@Y7nA/:i4^"?GԋK4!AipC֌KKvϺzH4rM"',ٟ5Ԣ͏UQ HX-ۆ4$4۔o\% 'I߫Iö$-E6DA +E;V=O GI==^O⯾/o׿}__o{;o~ͯ_S|qq^pnj?~ (dV!}k)>HhCFIO|'"[QicG% Iߤ..6QR [*,+!1^ R;6LY)RXHlKU->k-മMCFF&e:Fe4ZN+LYȵRPp +bjFNmھ `2ra!D<|S9SCV^gjJC&][( NSGs *"+D <):`n'>+8hƋ5t]B0GAal櫚!ƙq OV8IIdJ RkŒf5\P wz,WX:N\86!Kl/Gvz/0I_֩n>~T&ߝhYeV:!w>I6)S/rr=V ,D0~*A:ЫM}l*3Sr)ޘbTzl6qw6$I{t|8(he$T&o|!d2&sHJӋ"0l4CL>~hLގo6;IF@30]05sKg%-K^"4TQ\F<6TB4s,֒TyAew☜G-7!P0>af&bq(ܝ, lxdV|p 359zcJZb<^HjzكU;1Yֻ׫d1FrU(&ۈ\f~Idbs)-3[3H}D ӿRFK?Hbӑq}XɐA0b:PVl$qQX$ L6>'=rvl3.ru8g*M83*+ľ nDt =*1- 'bbi`'&yI_rl9h ~9Q3 id!-p Io)cAd%)#RgTfnͅhu@HB֕+{wzKi$ԭ9&.At +@8F:7 +aH%]B硍wZdp^D>"[-kdհ;[o4nD5-*oF/=۟m2U.V;5@с +fUn*ȃ@u\ũ{Ɗd:=6_{}c4\]6t3ҩR3K eQg斘ZJ@x_,Uэ; &eHŏekl5WUtl SQ;JJd{+np]I"r9p:-*=1@xºBK J.LXoPWy +dR Ez8i'vs2C ьD_ AHri&} PX]]?KxE -48@m+dl/BDE|޵F[h!Α + [*j%'o3M<+i_mJkȤ- +]n\*Gc_~˅/5uR:2K`r/S;a=sɣ}|m1=LtKfs @BE}<@AV3y:/WVnSv|%y8qZ *G76L`W@ۖ*瑑nY|њ? @M +' nua3/: +4C>K38}f 7%+Fg,jJXp@#%p.) - \B/I*:POC*OM\/$(dM*N c= +l>sjR z篩)EE5THHHzW#EP 1$FANz 6jwnʾ@֨v${ϯCBD[b + NTi Z;08l4-%, +NUq>ӘF*ǵ|n-I%n89禂X^Zszi6\ZZSzә\",p U@D5=;.,E-WvD "?}x$L`O@>:-l!Z+])U)g&C˂ӫvH僂ͬz&oi|txuI(/F~NhPC{OlU]e\e(m+=8/=T(BTgKq"Oz5@5ܖ0GCrz'|<+%5syB&\)kxa/% +7D LR->[EU%OȞqutIs =?8htp{/56٥#ҴM; "qʋ,_=gjڨ4B&:'K x|[ +, ^J..F[b yp1WW*Qvp~eNȟ(ܦ2ܩRjg4Ta@"0vi]o(S$&||G9)Z*ݶ'F}c/-T>?#Ysw6Gx 85xF"-ݗ$eb/&k% ӣ97%'{Pwt(8cGLXO.8d6S͏cY!02YߍfGXI*fL?XYܗh~`QchͲƑ!R^϶(@i27 Ii8IJXۊ)74̦+SCNԯc.7~+ tʤcp1"ӓ1o;0<7805S;Ȼ k}v@ +J6JH{Zث<&$RKXѲ?V*ڵ\Srxdi3tR =su@@EC(Rzͣ y8X^w}#)SVG)2I&#H-&j0T[FgRbA&NJ3_ͦ6&mTSS̋]@#tG-l>l[JO~uf8@x!}գ{Ϧ֫~t2~:U9>p95넀‚,&z(0u mEs05-N6I&;eb҄)s"łl9Ǯ^f'ߞ{3< qLe(򢇞~蘧^lݯxR dzPѡ Ii +qNbP%e"^&(L ٨!pJ1$aTI\YY ϽF[hP<8~kMh2:݈}CCx &1 aˢr*BեVL+vࢀ]JRF~qg6)$bd(\YEbήmWJ2Df1\g饌gzA: "/*Xi H2b;5M\+a<$9^$ +:&qӖPaEtצPA SeI\%}Z.SQ~l\v5`Xֿ0H PUv!W1^8,k + O)H,UsdJKq +i&4eOw>ɳj2;R6U$ +jƗ0Ws @[̐s\βch%Wq|/%K-Wc:ܕnIP؉ɲ)oη(5\2v%"6VPGzLy2FP)=dtpNщ ,NyZlyHz:bf|SgqHkI yҕS/k#'hEaQT=W(EHBIr<(ZrRz^xv`5^h9 Bc%#9absIq6f"Ő3u`AX=2:ZF|*@Y6[փr+0zH@FgBFyLi?9J$})47agG.(@6 K*<,+`|IDFkpS 4lQOZ!B@@G;PxPAeR1o TkKQ1ZF?sz`U "W +_W}"ͮJ ,}6+I!pxhwk$J܁ w-Ю-=<.@10=g隼 `!?y茶ƥyMh73./;V;roɁtbI/0 NIJܩcP:Nz@qz)碫askU;zr,IH+^%$+r@$hkU7DZnLi"ܯ_nw PB0|GɃ9WHi7 쒻FZuٮgEQʖ &R½n/{֤Wg"2 +1YDy/-3| 67d?#՟aSAP';"s.#Mϝ: %N֞cr%7T(ljl$IͨE +G6JAccbC1 +\9U;4 } -'M EfM%V(l62;PBc'dnC\tKIEl1;+gɈT%zBœXM=``#AX(FAR@+>9 O< +3))mXkCR$u=W/yKlt%{sȧ?g6%O0R$Sզ%Dp&fe +<$$h'bN28k@D'uAK E=r$"/OʧHopqL%2lwV27ś`ﶤh>! *ޮ"b!Ye@ @)e+LF"2$vb5,K` ;2ru2Ļp2.!-U;lCu` \%׋zQ'+w`%%wni3{T'ׅS$I$tJY'x/œH29>1[C0 ƆgS u+6Mu$r͊@"P;n+/stRs6""99!23?B{`rKI*#NL?YH@`d?QC$N$P)Ѩ5$ +kBn"`7*l3ɛR^{^I4/߭rb;XٴѓP^D'M1 KDgmXKf$!cࢂ@@=SaF&)q-3?5ם]UgyV^USFUi,u4HL +xǸZmWF#$diw317 D[MF~5a$\cd:5CE2dWڈ{T2Mfyé&)<لu*IΌZɆqq_@L3lPiTie9w@:Ͷ܆0u. !H /$!UkekPCX"#RAAXNnio<pUI4Q[79@r}Of7 0-l%:Bܓ@IQ3i<븚Bl\h`pPJԮ@/u—SDŸҊl.f E\NOG ;mE;/<)mWd,=,S믤n& +H#MՑ˴p?j[HL"RrL&J +05B2^/vB:)a0'SP4+EtA@bpX +}R]piiܭ@CT>,du^ 0)xWKDEЬB a+hYA+AܓM )>m^ s^~F , ( +9Nð]5VΦƎٽN2)i!N/91|rF@ E׊I"EÖ4280ID2 `G8PrXK(W{xeJQ򊪯8Yu'{poϣm wB#s[]Ⱦ-١ܺ 4VMM@o4nG+DH僈Qo +9n 30 XwYL&0!b2y^c >1Ď=^Khvp;X $KC=Gc0}9JwQ{ppkN^S +)~>J0J<2[42pf`  17N0Ui.T >P7#_)M^ơ + bR6먘$ÄVIԔ$9Ah7M}4[H2̎$9̇i(i`#r]MW:jdͲK$XJn |.m:HWofM'i|@!/Sv;?-#N4k\v-d7wn7mȔPC]'m LjR&WH f&':/s’/Ζlޢ#Ga'7FKN:.iC[tS7,\/ύW z=mpiqWxT=(f$Dql!(H8o<2!UJ [8=0u`S\(ڥ8L;EJGb]j@^eÐ81R,BZEM? ,&Fk%l[R aWl /ZD.QULa>3ոr7ʶhTy5EbqF-Al8>L°KpQ9leh r*Elے~0Rf%`e~/׺[g3dBz,Q&g,Q)6@-RIH]ƜuEW`a͆y[",DQd2I90ʡO3Ua} +2B1>#810Næ-K? `}zWfP=/35^lԣٟۜ +i2yɮJCx}鉚u$LɈjVm4dMTQ[.JP:V%H%lwm +Qt4;rB=T{aC',C0]5Yo}$i"^jZ3SEQ A"6hoR +iY4K(U\I{ tB=A)Y!i^AA,$ƄEԴd PtbK#7(d;gn!E;/TF3""zv-*zZm3Brs~ݣ.ф7u R + V$Z25Z!:lT+Gw- +B4/. +ܦxNKf*S6`^u5~U8 3+.B{"bVyeLBث7WjDXP`F[/L6gg:Γ*5FE59y"+#A=-OԔ?CBl/RY_(cXBxqؖ+vd䥊]^``mLۯ6 +2"%(N g=L V+8 Bm"xx1ۡʫ wbfHuVηJ +,JIxkc2orE]Q@!eNR :]|P>I3II̾Eln<`gi&q$Wm蚜X6TQ'4rKNB8PXE(#k9ҚELQCXl:IX}U,:()FGm/zU5+|z'͆]IaZfF6k&zdWئ~hʀks@t=~B$ iq ɄV/\O .k'&GRi +ZI~O L!! S!%/ԋz-nKx):Xc!ʪǰj=|ER=B&-&YX N +f\\3N[?_$V\/ ^VjANFٲgLgj'l5سgY'"\Sz(mt>~uGqo .;gi~t<H>OR”,;h|7xu睕J/К6 c ]J+iS{ +IXL@ɀH {nYxе"NjZ/J]sI]< ienϿcUs҃} F}R_N +ʝYz2ԐW+Lj^$il PBH!ӣF4NGCzfD<)H "%cޝM'[spah54y๡E@쉱W_t*IE*aNV!w^>Ԫ83H'#L<2Y) A׊x|X$0|8#.RoBkq9@ zW2p.^Iz`N~JK@*hAc3@d KIz|2}*wӟOP~k1%rVVeAxy F.Wa2]fHlb-d)8I^Y ++`YoPT~5tz -NhY굈1FjP XxNByZ`p6b*^Qf-]( d7# U:s6Clb}vHHgvcf(JJZ w3KM01fV 10 +TiHn {Jr)!!y잓cHjva!R,6(Hjٰ!Y:Τ&liۜoR=a5-A5t2Z`\-,i8=JyCD%24QǓN I~iUlXFٝIQ[.W^&>ʄʿ}ÙrKݰys^ "XcSý20+)~<`3;evBs JQuYG^2ycCEmWfm&хil];'j&Gw2z&+X \iˆ`z3} 1{Qi2g\?xctٰBG*/ؿZl!xaؕr-K)JSj K CFG:t1PA*7#e[ j4MANg=0 a0l,6j0i{!c6iL.)]EvoVT +4AgCg,!kGnżlcgSuy.yO;j!qvAW:$J()[zx-#F MU4U/%1iZ" 9Z :b hvI?*+p06 +k Pp۝[^]cD*֙R☸4%Urӱ͸rxVHA@ԖlL۸!* +.!ADbʀzmUMtr4l;rmd&lq8л/ lS쮫vjC#|Rdc %@|7Q(tE.dT!YU*]B%!qJcY [ `ViBo X +}x]\NZr!I,k)Hݐ /uwQqNvi=ȞŒe.<FYҢÁ BpIe Cj>u4;=VBy]o$b%^נ,Wz$l;%pOxNm^9)T)-(N<fLό*; \B],-e'SBW!5KFo/B"^ؾ`t17 crTt@nh;m<])6<;_evGD;[^؊jh q6_oD'Lnq-*4||aׇ$8%.5v6zO#I^d'UlJM&%hž@plz\6U}n&r!^.h9 !+hAkd+ KRgJoA>: '&v*o:SsnZ㱵Q]"MUM8Wm$WxG̺<}mNEJ3:@7.PMiG"/3A sAesӤ.TP^#)SEtHM[$n^G-EtU+˪>Tjp{a>YQHI%_<ƙ2'|ثl yi\P EsX,U OEsFek Bp]>)/L7\FaۇM](M8fg}D3!E.i_]"'Z, ]&843K&am'0(J)Ti?G7"/U#I_2j'H4cFxVl#N]Xlzj#,`%O.q;KRĊC^&gP݇ن*B +ߜ!yJ`MS +Lhܫe7 4кLw)hQɡD Sm#KE~@ (\s俷긺&υpꯤV+Jԣf +s/}D.Q$n^.td[b.BZFtuU/G՟w}E@W 8qK_LO zb%۸rKx)h& U0(}?XO<.GsCE%OA"df)Xb^zp}OW׫d ̑T;Z+S,bJځnL34eֆ:jʉ<`c#ojٴcHZ!vwhNY7l_P"+`p[URX'>`8N|+i + N &haM.>y|JB>(7]VZ|˫(Ttwi%0Z "%%^f"mRRTsդskxMˡ> +la8 ,<+9=Ʒ;SҦb" u"_ztTBatO Q3Cm?w? TJ5)YסBAЃS;wy4^ ^ipƼ1|2 R?vj8DjX &U 0A3f ]24$ec@elU_zL{)evf5RR~Cbd_Lp/_q@kL:X kq!֩z')5SMU-=g"&yZN xEJC{$4;^xp4>!MaJ +a. pT"?= ,IB!cti9AK;.!p;Pن@%*ܔhZ^nc8&*mrfQFsvpҮ|h=8!E&;dZi?8P:U8d +H,:A d>t֧s9ud#lr+ȼxbK0I Drtkr%]lzED)rග{"sij?D^@™ݮ,轙W{cR|SRnp>BT;3IIz,Aj^(>;-f^6]{&I@=O`PZ' +ٚ\bc%eT}&M݇$8<%y>9>hDž\@WH8>KܨVLԹ%K"K]fR/GfX=AL!ϮBm" +$̞D5!6UǙקiIh iAp,y(b4,n jc>%ҍv~w` U-??Y~~w__ͯן?~׿7~}N퟽ͯS~ooomo~xћ矿 ol7o a??|R1ND[5kcndJNtWɯcۖwmWʋ7#ͤ^t 뷉4^~Q4z37k'ҧ7п[o~+W4GxVZcO]K*ﲣmğ_[Y]ˮ'/x iNomD*ך;jY;!=}!lpҽkyH44Al|"$ϸ/Ї {\6{'RUJ:x>*d@6[ +Ow;q5ȣI_$8ߗ'+n홢9z߽bYY{/x_5GħTo1b֙<7W4bs'ojaQ}V3U7kK"L_דL T_q*gńf.+.mGٗ ~w@̷\[5¢A ĥ7^:g >t#ś2Y#Ms-XM[ԳvQeH@~LM>ӯRf^^^sI@ROVƛ |Q1chlYh4u蚢+NO~ʝW)E_#"6 +3wԸ~W#qz+O\1oT[OlQ;"c}E”{ulǼY1o cŝp7^cka!=::{:`<16fqcXPzocI!bB--;JֶW̗cm=?}~_^ҷo}}"ԏAqJ^Y%"t绛 +L7ß')>9w{q |E4?q 0jocϱ?pQ.XgǛF!o v½3pG3P~X杹ǂo+D؀7C>YCQ}>;X?\{ˑ@WЩVswhmFc}2~elE|y?{in J=>e(Ŋ۔sȦgo3{7-O4|=}@(z?[L^TgoLDϙ}.Bk,g>;th@^bD,ݬw(4{dQ S!{^]L61h@,Y=5cr >G~gBir|s|ͭd4λUχ~Q}<_OrK~ɌwJ]& WqGTV Wk57cX # 2ْ\m{c#vS^quEܬ9z yWԤ-׃{TnhQF06>{;ܶ]nA|tfp蘎/.ջ7m ^ +ۂqfao8h"k 弽Ey\?! +vz$[$Tެ=)6n +OkCiǰ}ܭ ^0WPiB45g+VzȱEA蕱ߣ])k:Ǡ~E135^6rv V8 h!$boɷqq oJbó_ӂ\Gں'=_?p$J 4$5r|ެ;4o, ԅo%(v }d7<>(w%d﮹ytok\>Ÿ7:<|FLQ=ʋrM֛{f5ihBs[l?6~eP"oKMg%4nc~8EFr+$yߜf?!n}hrTeW~1WlTkܞ:xNޠZdò1oW$~Ur^>Tm&1nUcYјm/Bk4üg^Η^ˣq!jN=Eanπ7<ѽK{D~9!Z4_XmQ=~. xn}l;CnرY$O?=?}+ +ݷi{sjwϯdt*ɺ;w}[c3iD&>Ҍ @%;E};L#J>,_ u%9d]^W%ǫL+5>n@kֽGO xcԜRbu:٭~DOVwaZ.~ I ğG +4-MӸ?M'6m +wpjq ( C3e#k{|o쟗 +|?*K 7;F'}qc=[l&,go7#ʙ V򴏹c{'mu=.y+>blHk +O`db.Әq߂R3&.(X{ +͢4tg~,DwDCrGUÚû'HX(Ij@K+,{%Yy.&iK[yZQ`Ǐs@m0挴reuO5ʖMXw^9߷[̏Q/:Z;+jV.1Ge/>~43Ĺe45k ;nV`^,av&>uh\=ǠůH-εN ?aiF`(a[4IK="qzGe1pPg/f0+WfU5YO9"ԋ6/[bU2?lҸi#l|7nm˝Q#kOO&P3f)lDcV||j}fc%is}kD{mK ~gowiXx<%qgFg;?tݡ/b>ڽ3քQ*b*s,+nS̎}42(c Zq/Q5QEԋ%Py1MpzYȨDJf^żPFF-_3T("G4.AD+E5Evባʾ\_#m +Kl<A&.]| Xٵݾ3/-kM|wrİ@)GKM XJ/Kpzc1u A$hgz w$dǃs׌B|&1.y*!4CA-jJNhx7io@ǩXU" +R\tvw FMn7G 챣n].6Ep"R/Ұˊ8j#:{i2~]Bi7WyHr] 2_B%ѷ$S5,#hS7Q2.DZE}vTez12iwK~.D܂7"3沙W61r蹳-Jkb;ڹ3*p&"\瀲7Xx]HXN{Pnf4/]0r]%4}+/beo|PJj5B{.Bd~ZD܂KH @m9>fAƾt +i,FV_>0Z2 B4+5v?`)wIqDt|ڐwď%ErQ%aOlX[ء[.gxc-95=IX6%v{GPg ucmi%S* D~XcSe#DXæ_UFAZ%:˨Pϛj Ly_@L-廕-wIcWp[ILoy|p|X1PGn款L\+AC2,w +F162 lVɲ32F,h nB|h8$F)$U$raߥnn\!VOYo,#2j}հEJ@9=Ъ oo\U}'3zhbwEɾk{cWUG{֨ f7v&)Y8|S2Y awɄ+z s=y{oWx2=T֌)Dr~hq*E1@qZL{DC~Vo="J\lVrTCq=fyHǣjB51>9? +cXnI;n*Xਭg( UT~Lk.nUȋpdqbA ^ +,jƮQw97/mPO ;H1i͏Ny*oy[q.tj8+/Ktwܭe^v>L%/[;O> ĸ& ?l@AKqx8!079o*^ uM\kԣgl_Mj#"\ߙ̚~ouwuU\ePr(Js@9YYm9ȒLLʌ)Ww>t3k>p޾8{xny8eI*0#Vɞ<JwRŸI7||+m*)+P)]]ZhIISP (#]HH[bRG謒=V9TR(0CvLDbd&=:"ɐ)JdFxY~3埤xIF\^ȠsYTgg*ym7J*fNG$.+!*%UN RtAB2U 9ь./ +QJ?Kr7WW5̅.d:9b}SVRa/,% (2d+jRQYT٪RJs"!<(QVnì*RׄdTB ʊTB*դUR4zy#O*%ZCGЉfGi +(pҐ "S_NݛP^E+* '2G~V^R +lFRHȤ | @^@+8WTtQ\ ꤸpE{ +L jHUL:!jb:/b K*W/"V*eEϲ(GVF?Id^\EQ~Phu¨5VYZV%+Y('/<%}I򜦒 +cK6C/d?mx@YtKʧ^L |B{4*>J +xz$],TR<{G +T1375TUDlɟYE +%qR=Yk- +\&᧓tJ8ɂ庤 +vF䮲V'j>O0Yw aT +( +G8M,=FȢI?WP>媬(P~)QYƠj[Ӊ%L27AHJHYealy򍎔tAN] ]Nui#\F_cSn<#?ʭ//T:xQIJ\ͫ @B,J)Qԭ)TJ_>*ePu2:ƽpB@2B5pM6WT`UQTMXُERWBVE**zYN%PFJ {HiFjl^Uֵ9Pʠ#bV,RΨ(JFu#˘ j&YCRMTu +CTd`@&[{C{+-Sj~دC2?lp^tNJ^V΢rRʊv0*QtT;jڰ/U"]>y +sǨC|yo*JY^D)ˆtj8JݤS_=T*tc모Rdy3E̟tlǨ`,V!"ťtn>$$(`nS(t~FEp 3mŁ2BK*ސQѨ(VT!A5BŊ(TX Q${U󚷀ug9eUQsZ?$!YUEURI`QVzyړQ,F|B*&4c*T$QB)E]eŜ V] w/kȂ?bL}Z}&CjT.9B^[KiVY0,.([U(ܓHOR2xqO&fL9b %moT(Rx/x1 uᬕUPtϾ*<^Fv`kDLb4)1}V"sgLrt2 UYY|_WŢ4a+i5瀪,TŪ6ȑ.)K + 7"RL[ +9R!;Wң'Do5@?oQ5G}??>/}o-Ғ];cnBd0RB^iQ\C1- M[%"mt#(t0xU| +(I(mT\[i^tdyj *\ k%WWUW? +.;zO:GE8B~Y>/*/Dȏ( G,3?zMyg4S|6ZFePXQgA`aJH_Tg +բ=+{[lugpz3GgUo*R@{Z{EtBk6G7#$f Oˏ-,a{{1=O#ު6\~P,(`b0qFHʼ +LtK-QQ(ppa@ QlsT^9)+TgM0 :~,NMcJ*_]g9?[q޲':o}&#bQeλb4@~%g K H=/@KNQbv+Q^~ 8,j +#Xx5~siԏ Pow1QCeU +?.4B@?.n1 )q>Jw!!aѽ_3벲1إoڕ +42jeL$"QY\h@{Y&RGhFQ__[vS[8Vx/oImt>XZR? ,fo|O+wwp{۰lʘo5c Va eƜ94~af?aDg.y;I#VY˫ ΁i)IMrA݃;tn5ekԦhW族gVeyu}8:=RN-/fp9c@9ix">otĤ6L;“/IkvEL''*_osl} + +g؇ [ه{ҤMr>>' OfM~9R']'NKٽ6?~G#'qIMʦ4])wl|[껦>/3RsUN;t:+7yqwʟݮx>0:+1MM2m56gLIuƸ w @zNqSƣ7T&؜:6/oؚ/ΧxTv +f]ގ_x J cO'sTS3#Q_%$:u­ᾞԆicv%{*%B:]vr9P q*9i7nOsSf7k%Wًy #bEgq/O{dHT0/WMyɵYфCމ=;͢.72Yٲm7M d܆o6(p *ؘGXHZR^yw&S؜R+)^ E`/Mg0.䜊qMrkf/8b2\#/Dx4冱 X@їVɔMx~'sɷs/?|1;=b=%;Z54ʒNewts$2>9j[~!n]J,_o|*ζ_>׿ аwnեC*~6fx6)ũ_.}G}Zqtړn>H8yՄ;dm``-fs.`u[ܐ} Z+!9RΜ[/ޣ;xcIbRL8繧enؠi$J.5;2*.-74TtqX2!"Wj +G.fo4:p-l ]']|>aɦ|ڱ%7 +s9KAĭsrAGw^lH֛8I井wrc^rtop/:_I1Pvd/D +gĮy0Q6s{WpZnA}!8js哶E}1,}ݖpE)Xâځ(J8D8vAM +t{&kܞU6nLJk7E/'6{>Xجw̭'c/E(jҁ}^9Y#0 QbZp<:^|lt_xxw=n +Nf} %+Q `O֍{UIwzn-/ cdS7'k~ $k퉞w_Y2ZIZmINtЄS\ZYfr_ԩ^L/R>e_k!,,;:]fQLY:4YUt2UvjY.Ub" X`] FEG+]FwH۳`o$ *0e|:iګ$\6/'<ʞU˵̒NG.ʭ,;c6az#ˆemn3᫻΢F 聴[p0^?n-y1S rh:8x)p |4{?u̲Esd0&jt#³`1ucwqk~Ĵm3Vi}bTGŋG6v-$Hf/p)Iظb šS|~}o{J]`ڭVȂǼwUKbؗ5S龏_~zKZٞ&M{ i1I{csG:*3%Ws׏vGv0oM[3}Ffz?AGٓuiII֞tުկr[{pQ߾^|diѥAgd<3 msqmYEԼ=VY-?΄ܴ_џMN;Ny`ظ[ѝ ,kEYo&$jY?DR&0t͊VfY؂9Z+"3a؇IcګtLǼCc)o 48Eg~y7VVp |: 0jw7+%2Ug°g`-ޡQ7z]~ \-u%1W{iFrkx9,]Rz3'.QݶwQs*NLhFvqZn8j,)XXLV' +/yr0*@8xXxJ.l``!8]֔Cgs % ~].1mjǃ,]2XY7L:F/!t}{;GXos}M8|]X*[#fGGiVdj3QpVM:8Qk靳zv9UJ^=|ZAw3Wx}]0ݹ%t]S#FyG#Ӽ#UI+::4nԦ}Ad^pr}ͩNs}ƁO:.囀w'(6"3#i΁g:˽_7h +\lUt629aWoc0o{Xt%/ z 2ŁDR.E7vW `{2`-o{2:=]fO ^YUq򞤋w*f~E٠e4}ҎWpVqN).v@Ogrc&moۅ5۰WlcǾ k4lEL)piIxx+8ΪNsaH;b}, KڈF!a0`ʄC\hA&k:Dg3A Dh,9G`GEX3x`g.G`o~lwܴ>!Tʒ] kVE/1S&4)`;K M̡@]J7_*Q3t!5`>݈ٺ>zv-}u&?ذϨ5jVU[tNt&2#Y{,>>g_wWkO6ic\KxKib!hpVqY%swF?/kܿl|+#:~/ﴗ]C֌A?_?ly]o@߰1)=2/ +O1ޟΥݲքдwIÎS}gN)wzMa&70ʳ.: 4JvLISm|p`urmgSi?V`auq!*a'j5+nh4$-DeIϛg7~1@$;`v٘KҰ8sq"Kra/*Op0jDt>E<AO03gT5LHLz N N Fl33QtdN1I#MD<x@޿7f!Y\.xHяH恴ߤK%BWGI/.H!n(z6lSa#£_eV->$jm N%fK;0ۢ0V`ӡksW\&m9mWcYy[گFݪjߗ1Q7&ǧEgbqm4g^>4u¾KKW5>:wJZsy=ci۠!:JϮ=ޚ~?ωkyy{+'w?Z9q p8my.pp7fܩFflO5ogdGg4ۜ8*`wy\P'ƭ蜼347&D'~d؜J⠓ s';?xzOM#)frRvm?fUd0Y6ֈ7lv|dWN5XjE֦ 2RQmeqGǾs2ς-X4ћosJavW aře,o2!2csNt:5Ά"-kY\W `G᫙DHڙF (w 8Hm+8ptl &mn؜8LDxGWJ^}ZA&hss(>hF"ƴդvҋ `iZI|V~G4iΡpĜ_0yu+f?S[5/tW^vjIyu+7OsajY=658Elwdϴc oamf}Aܧd>i{.m`L\ qUӟ_ٞx]6u0lArZe'Gf:.p/-AO|ڤykN x):^^}Iq]M9ìgȆ5R<(i좴;Q.\}j<k xii Ԉ^$xiwr1\r[Æynw4~! Ṙ Iڤo)M ̭Yԩwa~0YTK>!^1t6{0ɲ!taٔfs*)2 _^% k.ع9ِ^rޘ\})Q̊KC"_ےq^P=9%MYlp\Lh`ဘUݱ1-FjR^?}al2 klڗ#FsxtـtTɪC[欦4נǽz|a\>`|y_^u cv4Y+XsJxg9s0ᒶ>6;&|z5Rv{^^/գWODW-MHb37)y߬|U؂=斷mDga%7}iWG:]K*\ ?Ru>57e765=ۢOpP.dYjr!K 2z-sdHfiFfy4`i( 9hP?=~p o2ҫ0߹ɰʐOE sJ'O'_Z}:=(esb# qVtk%wI3ᑷ=ʮ$:vD)9d.88lɭhpqgkNZiz_/H;Y{ i]=;ؼҭwg/=PKg` VEΨ]Ѳ>-}v6.8x:kw5;AZ2i%{=Z\Y6f4m͉j_f6 + qÛ̕ojVKI~/$CZ]oqGZ%u]UI;:vn6uI;<՛𠜍Gޱ6'}S %]իoilJpbro3CWDmdWb6mjq[s^\}} őp9Yٹ謾/b=XҮ..4 +D +=;" Zj]s.|;"t~:8v ]ExuWqɯl:nr$ -F1qIqVt\\xKq>2\pz`qCfEol:d]FzŦ]ջ Mx|أzOfOwI7/qSǀgZWz'Gƀ|J.e c.lor>9;8~4 An!fEM{mKvok"Kw^Xfěcvqf67FҶ)ޱKLmY` 9v=񶀗M΀ bʬe}O>OǻO9!i-\ڍ¨$;'yu7fr`N</v˪ZWGm,-Es¦YlW=ȇt#$>9zgWME&3n$#s C)E2 +ʦ،zwʶSw;n"7q_ׁ. y5Ģg}`nP1lBwڭo5/}3Йs e|"N;S؜M{Q.ݴwxG`L pi 8Ǟ~0ADQIwh99t, zI֊vCג.eگc($wr;Rs+b G&;g[M.hz0HڳAuGo&g`04^o'47^dk6ia܃6uC' YFI/^n[ѵ5'>|>iE:^Hx/!YFA1AOV>&3ҋKQa)X#mƃ3q\ګ[MA!) +} +67+zw;ᑛ``+cNUw­\ɄUgFt֊,Ñ/qEIb^ 눝U 3qј]ٞԮ ٞNxGgM1q{Fv66lݚQ6L})_Ҫ&@dNٱ9#Im=f )^;Oz?X/~y"5 Xۣʬ{М s;$m]ڵɦLgJ1`H„βJ,B-q!M̦zlv?`K'} k,:];8({7z?8 WMx-16H®磳iٹMgϟ4,~Z]^\bKĽna>KxI蜾?0[G3]'q_eg*>tp%I6-OxM8:\qc7bO'q9p: [7`x(>Y2v +6ԧqK|2W, 1bnnNYUҰt"6x$IGG[މl>89/lL{{]`ob?ӖUv~}wlk]XCA燍M~Ag)f2bN-o}YO/?ee|D'#>w(b:ól>/O2sow?EG7甽 k6#)E׭/Hљd©b<]8eҞ7ݰ֘D&M9ȴ36ΈkSCـF%a*ݚ`nfsw| 4Jr"7ntۘl/'ϸۤM:t<ܫdc.Qs'nMKZq0 +%@W}XO{rA%qʻ4m<8jKJ, AFa_~ p+íyAu-~>9DqKikXP֓ pc>aonytd!`|v|^g*pSK썱N 5M}q aܧuvrKzj:Gc=,Zg; a6'~ o~~5#n@O6&2hSoXO'.(3'sI9k +x{tì߸ k!%f=P\vF9 [<ڣ6&4p(A楍 &`@>ݞ蓿*}:beJϐg1{Na7~|ۉylw0\Pm:%-}l܈֖Uޒv eoCb6n櫇nMwKyiuƔ8xc',O@])+>@̣lYM/fFQt@Ύ{> 4*j_wfc׳=U)P ]'"FASA, kY<;,x~˩aG]6. PxX?0 ¦ݰN x(.8 :>ifi^ 8*;;4\hF64v+F5[҉9Eq+d1`N8/}`Gf֜/p0JqVy(n}FxiibsJnl1k6jmNߛE񅾃ӽ.:˯ژ9ٜ T[7pI |~Fx$37>1Yi}v&l4w>x~aXkpV!$<]ˉ{Ԝ iíZs+^Hjw`lw^;r)ZkS㯦EY3o~ `]ݟ 5<858Fv]\2TU4Ag+Z5::p 8'i366m8S"`ҿ5n$=W&|7{̺$ )x`w6 ]BF)#~Y6qiؓtH̭;ntzR5v!iI,ߤ](s^]>!3KwSޱ/P֖]ڀ8|:߆9sXp* EI}.GT#&v G1N ΪaԦhB>$[ Xsz_?U)2~'PKvV̗3OWrO-2˖FwV>ȬXH+ۘ{<|y38xa,`rkR`xW70\O'ၫIh?'ᅳ0mxEUW*\eٰAWe8FobA>".I۷}#I͒i௫oz 47  z6炲./i?kՙEWYp'Ma,"Df5[c9Y}0/O9Dg3){k\rj!;A +0: {x;n5FX3}K:5\ZnԩCyNbaN-wzs[n*O8sϟHOx(>1;rj#qڔִrwك68UҸ5jwf}ګ]?WEMQQ[0pw_-,k`QS[RYU/焉j`4fSҀم緧s֬` +6ṤCp ^nmҥ}&&mFSy1{ +5`N_HuLР 6X )p{ w"&`mQg76 N#/"Lk@j\xd1X5_ctp+brXswVDo/x 9elw`Ypetzfx eK1j`mڇRA9r.5Cnv&W+b}y<=U<[\$-G:icAOgA~]A?nMq>y~Ճl0a6Q]tg_b pU";XqO7eE!ͅ}i9?5?q'$68SsR^QÖt!oE{' _Ojcvi# 8wʚKQ.*Kg :so0ILJ>%'Rr0o +N.ݜ$dͅ:/~f ̢^!"mۆD10.D!@vYoOx4``ϠƂ#-YTJ+e)k,fUu<H8G.&<3+xWt^P4 +Vu׋',­Lb !8qPifq8x%sKdan-sj9׏*xz5>Η>"4jy((ׅ=&xwք؋`dJQ[]ߴ.2;rַl>ft,4a] PY-md'{g'٩&1$jhAR.]J9<>4Yg gZl,!:E= ŬuB>XĄ_K5"L=;#SkYf.jydBLwps:,%\Ɵ)f >RUT~Ζ!,9LcP u:MX?|H̲ω}.fṭX`JGڮQ3s513 ؈ m9b^ȶhYss g?{ bilr 9DAJ +_䚣 yt{\o C_ +/TK!"7MJ:SR2Nِcy,B,2O$356_ %7?t¡dq6- zBʉd_w7?t٠#B ߅}_k)DYg, +``~g  7c.n!h5_q6?@9B>KL1L𙂒 "%D-.+gyvot:'\UN5B=Dʊd 'XX )'مZT ࿋_n76 ;Ax=aZynD5ޭ7@Gq!bR%^[H,r| h`d E[HUP ~u p9jgaW;>{n{P eiLqMM0ypƪ6F0~cZYV\)R҉9XZ{4 +v5 ~2:p$.jS^`TDwF?w\}b+C}*+1GrFFWՖBbW}z?@pu"%Z~-Pgo ڟ)c1ޖreSaNj<@'IAtYBI?WsXeh$!h0|/v7a|')iomJRU 7Ob/5RK|gk26۠Ql^_>n/honuc|LsVZ1K۽H/{.T @Otu[%e]E^6xa7"TE0ȻԔ~߹ Ŗܗ+͹NSx;_Hx_v1'ϰcⷫ A)FsS@;BvIſ1'gߟ@s^;ߙ ekK=]`mrz c9%;v?FGNsw{9#'O#ϵ_KK7߽^1\M0 (@YŨ8ac𭮺~?-zՉۅ׍ݎ0: 81s/2ZEBM:LJ|{/ߗsSﯵC c[]oLZblӜR3L))蹆\&a2TVG+20J2Om"78&-~}_SJ쵴;Xb[`$2 +&#O~J\]UfKvɑ=Nԗnog}6M<DŽccCװ?j 乄_K T6-s v?u5Fs{};am2A9W2%o#5VLøП:>D.>w|ىa_t+x~fA%(w_LvGzXq=DII6ƁRO}{ (iiQe2=[i1q{Tߟ;ꪳi.7tab<|86|*$%06&3F7.ho\xoEN~)*H}TJƉu89BSRJ^Rc3/힚q_SӇ=bIH ;}O;|6wLJ&n|ALM=VUĴ ]]IRm%!w/A[ܯ*nN3ȨS%=Eާrd-!{,ky-Aӷ#mWv{<Χi/9^~[k mæFsӷ$;v羻PSqAO_4M`Cw>-68͔ߟ~ш[i]^I-%ޔSnۣ[~.\eFptwsC~KLvꂆ1Re帹Rhꅺ8 DZחD1]JlN'5xc?v+y轮*ټ(Qߊ`oWۇ2R{0W}[ Ҝ<7ېnjifEx @?;r$:V>You!pFZ |mK ϩ}cwoa#E/׃fDY2v=(BmզW_ qx, f,皯NwXyIVz=^֗?d$Jj `fg+./f+CO blZ**'% wB{5{C[ +ȖvS0L\i-4W ~ }5@<22>kFtosYi<86l yV[]3J5O{"nb#곒TWjeFXb{GpRV#1)N⽥kmbOyׅ>P$$oP㤤d3`Qh tVw +vM/⎾51qZPUxO'rwmT9}3B+bXd|?Yy1_XTE}Yk-u_i V]`o֛K>aFNϯȘI^'Sܒ.)^S圈?[Fh?>NW{ru%;(y[B [ɇF| ,gA<{!rM1Sï3p >6?PUdyPb']׺J>\Iw䥽=7c @[J<lT c|uܮx1m']P߶:K?P0pv¿:z(p!gj?4ğeI0(;k߇AL ^]$NPKY${ M}n=G2| *#gZl +&1rҭ!|Ӭ{qu-LxKI~scwkFy+)K| Hߵ7k7[%_Va^k% +!,bW[s_shqwɸI[qO[1q,FW[j&$3^}aD+s./"&$LQπr$<D-mĦF=RLb.0 +<.l{ޞQLot7K "cQ](&{v EO9@[2/Tt.aq$$}0W +^m4:59w-b4y4*8VG6ȨjrwowfӤIO[M~nrsћ2^ky9ynsmQcӟ-4&;d9[z35Р?;aaWak +^.VXoZz;&* LkN6|.ӍYHWq`?xeas7@ +Mcq mx8aY-;Zmykv\j{9[4Jx0M~߇ +* ۂtq \wOn=Cgø-U #M@ 8B+!G zTLX~(fw]Aix47a t4NMGh8:.J;O[$%k/Tlo+]H^l8HXD +=&3>2R]olgK$T$ǝ+\رdto: R~N?#v_u MMΗ^DX7=m79a:I)wI&)G)iXYnK=/_?ße!l5A?}ޒ֔j rJ&5V hEO*cWܞ}?'_:.񧎍5Ock2WܤGW;KB,5COg&}$FgnCG8a+ @}>(pfYI@祦tŦWi&ty]mdQRz>b# ۮyD^;^jĸOr20.V<^(g|0هZw^mx<nCa^1rV'5zՋ6F =_l$~1E, fz+EWT|,cf<ҳ^m`VbB BEK02EN˜kEZNUeȽQl;AN;RJØA9a6"Bu{=e݉9!F`MCؐQZѹX%? WZx,?ɬG=RX)bTbS\wpN_']]ڋbf£g}6poqd ń) +\%,w6#<ȵ)9;X@\L#?[i/ru}( e9o+2^@}FJCmSs5v!fHJ᷷JCլbaBx+RԂ2^̫ah6OXks 7CWQ&>A>B~И`g'. ~:K,t19Egr™EqVi!z68ӟ\s9Q1'(q+]dSUX) q6\QLQU^oxbN6wDhx/?s1 |[ +?lCXY] !Jm0)piA B #`?s߭6d: RbjAAYA>t؉/44XVRgEO~ +f8R.rZYoKhk(_}ko)n⃕wxݟgڪBnRPS9grʏ%lC:ih(_-UUd5hVygZYZ7e-"sX)'c%5\ df P{Q\x+@P, ~(]*Ḙw+ )OTVOǓIJ!Z?]WiS3 > "L .f_mKcb: b~ 4pXpi#%aq&k%2~qj)*X']/Dif)10 67,=ŋ|2L ytdzh1qk +⡌99JYK[dV ߌ(v/ֺJP̈́T^[RyHR`.v'0:gm [ޟ(16r%(%$'Q7.^\njdΧ }K-{+ 𥆒oa6z.=imcL,2>6SeV漘Fݖ^N{`K9.XC!Ìh$f&j꒞,zD{fefeY62_nk*}_tM9/5Y/[-Sl@'Ti/x )*p]jpZm3Jcf'+H1Fjk!\tbǰ U'ݦ#lSi7H^#ͰQ_C N !GRSJ>H-𧾧SRldNT^J0HqI|Α\}ϵj81XYW~(Wp>Ut!k$-IBE wA N`aOլHC;ims6jҀq+)ba~M\knu#eU/¼sBV@o)t_jyy!٪WZn z`n1D Zf(6HV dmEΗ!ek;:PpKgI3M-Ƈt|M/>stream +DcB~+ _7;+D?X?x,j|?N +PFNX++Ybo~#j3 _џBh)IN ULߟXߗJ&2Y;2R®+F) B[iZL \zYNȇЯסaW;}qIH~+йj:Ýx33Ĕ=SjzrWJXb5[EO zs}{'=9kϮ{~ ScwdQ ( +殘W9 ^lў+}7YEs*v' {¼ns p8ܾr~x(Dz_u6mMt¢**JKƩĄc z"aD^ (NzYWU^-\L|ۢ Vc9?~KϯCК v:A*0Zat$(>]ЇǏ@ݼ;ŝ@.Π@+{s-2τF ÜvAH%S#.qv r%5PN7WP(KP7 /@o\E|j9[ ͘/w\әJ쇕Nt=׿Fu*+D^ x}9;^ (sЃ~@6#ߤlj'~ƪ;U V^pk.hĄo{$N.Dma-|$<}7ܼqs߿#$67OW߽z@E}FL˾׈A v-jجc尥beO]H] Ę++A @_##ۻ#PW\~$<ĥ>~\%wDwu󛺉]wj"Iy]>|޾~ȑ@<^!o5>Bx'Y/0}zX略9VnWa2Gx:>9usӻ@Nx=wp ̎. +Jx}Y$e%Ѻ2<(1`w7g'P;ӳg@ A9(jǗkǫ)F1jkҁյS8=l +8.8"J7P'(3_׮޼ WgЃ|qJv1nE*9 Nɢ)oOPKH_jŏ a!ҿz>Xv=_'@?>u|(?$wbJL=ocaիFl7%^q-[g WΠ@>{yCo@.Ϝ@Wk In b kظY!}{ݐ!,:!8OWb?{} rsz uwг} +䯾WQAڦԱZx5kVKtiϺ((+ljhd-l +>=~!nIߙh*ۛ$g6UmLWvP ýF"Ϛ|fyq\}HlmRF]br]Vj.,S]_"'lz "J%/5ѴC +tʽ:T q]u|gu]$M{K+u@$ Ϻz>PO@N<1D/m(c'* +\N}X)?IDlǵrf@qyz p{9ݽFx^MO +I*W:!fU%8ɃXy3|6[-2k⡆Tu3pWOP0+W[@_CsRK/mBZc;|0ʑ&Y9bk8@Pu +j LK!?:zjP ?/}'7PZhoTc'<~}̤(C䌜y!lq|tyGEk)Q.M ufef-MMχO(/PĠ@d!hrԁ}0UI1y;e9w]/3~c*(O%q&yȎx'="O7@_ߺ^IsHOx./DcH^F̷)AҠ劆Uyj!٨T԰!#|nQ_Dy¢Aщb{y +JKw3—+Sc g--ٞl`nLԓVXѳ$1!C7ц6d#E5(|yo^hWj3|잜WjPH E9ju<܈vr"k]cU&@|ww6Gh+, L6X=FUC\DUTLU{9Iފbuo3T32 T I ,km)^+JVY9D6+jlVx<'Is"m1N[Fm +w'PCUE4EwstGK5v>z7 :Z3 )}guUltsaRi.lOF}56Of1\Ć 6rG+Fr9 Tn@UglWE ]/֏䤴# Ī255v5,u~2fRRYj*ez~Cm埚ʫ=&[ViOyoUן6OmYjO_iG)a5eqOPm>;Ila>GwQtwb&+5CuY%:'kYT3 3ԏ#vj:0 'GCnIvE&9,sgGZe=miGEhM2L7UaL*|/٪-JFY+oM#ʌ8Yk]T7L1ЀPwe13ie4D]Z"aJ[AڭwO͚J~x JkCqݓ)>6SNߗPRRB>H`ucbNtյ*ªp7.=[h\,4t-W<4=kc-&lJـW~_mFv9Yjxr,+4N +ŠeacIhaj9bc+i)x fw=&m0ĩpʾXe>jJ F)xo_xTTUS[k*+弒M19zKL6$4O[MɎs,PGJ:,]- +\򏕊jlR鶅ڶņMMcL[wKQYGzi-nON52wuG.Wo_b_BK~ױV5jrZ/qH+GuJĪ2ņQ'uGΖ5$RejJmI*<l]p!iwދ:T!ms0]*x~fcB/|2J4~XrğLCQtyscN<咐w<%퍕woH7͏pwUFliCq𾢌|֢6u11b0:cꅖ\hF1?+*s-jJZĴPy4yW0aiZ֑ ?+/qݭ굯q{:Ɯ- ?V #9DÀ/sJ6ؾئV94ރkFzH9.RMǞYh]՟'~{Y^c'8^` 6n:2LWtXx\j +lʪfv>β[c z&pq:_?^//?ߪXj1iЩTQlq2Kf%;JyfwRrIN4*b 1lQr\]Q%dشBE>"bѝ4L~s1ˀfQjyھ tK7H!Atr?!t{+AELSyc-xֶ,>[9ۨh\\3kABUE/?A;qO"ŦɡƱwұvhY_5':$'&$z!+{QQ^Yyt,e#f w110~i#rf %*)+ޗ7𑛃 ~M+9gBלϗYa$HF,t,]O:UHA{bL:`uc-ej߿KNLYc#pS]MuV`Va>#!T?TVE/5jd"ń'ёY;PYpPzgc5o" ""4DIՅF?{գO#X/h]~ <],#֪[wR՝٪ht5^GxSvlj3<쏅ꚳT[UIasO)w&ʳߪSZԴ+|<ߕAsm}2jjgNS%Ӗ_ 2d6TYí~x"oU" ` ZQES8YNT$x Pc-pHG3Lwʐ':z9 +TK+馆>r~7ڹޑB,s$,6r Fe RU[SmN4es-p xg[kȟ3բiN񉔖av:.f-z :y<#pw9T>v]xx?"<(Lp&\YlC%{-rps6hTq.IWb*bUPnGD$|8D>Q3!ȍFIIfQ0wql̢$&`ε8 +5 +ylS߁ -JΩ(IV5%`7Nk;o=0c%(ƅ~9ޙraϲqY$.a} {o˨i6$e$MؓG=_ӭjFѡmU2K2Fum]j&tJt8]1ɩ)F)%y&;ǧ98ɪae@9Q@O3L2Lȥ5sתZstiýdI >2Z)<=ͬĸAd,*)9wV{?$U-6 }$hAkOBNS3%Bv]3& Z9z433LI=11TcT{]l&]ks>ss~R6{zkM’µhS+;3ZOt[Q!p + >Gx ۟zlzȾ +9VZ[Իd4|E=H% Lg~1LA?^Y9 |ty-j.HN.u)9\{u\SZV/uW\O$&o,x=^~k}c![E Ĉm",4f=e 晆9+KftuT/Y v͡SQv߱2oKXR=Sde?wWGOB>+2K19Xy7"-zz@ϳjWG}gK,GsI)d<QHBcQ >ͯ>@oO6ʟ p +Aov +_ldq5 +|o-%[faIlձ>r{ +WY@jYS@_X%"821NOՅoǺ'j`M}O^=7w?46f^LFƫcw'*O\@6ֻeԺHdMPYqK+4tjh:ץ2ַq(Z~)P. :Ӱ0VzԚEL4lc m_mOQ^]KSDowZa_EdžYt,.ˣm["ZR$vNa@o3@]^:Ykp{rde=EI`]/1ݣ{-]$!ϵHX,~O1~k + C,w, U}t.bsCP~_ͻf^#Ϗ4G;C+&8& . ey%į:2DKC.si'Z. +(# XC:T\o͹ۗwHLuXaTecs L0rcAiҡxVpzͧbԝ,.֞zoݩS< k}EnQ]<2Yv!+ޙ{B;j?{4 +)&n ae[).`S}XTCCĈDxC,XN@kAG!9dⳮr Gm[yMM5e2w<)Wy[o.P>W:Ĩ O|1r8 BrkUcP|¦>lW4>O&y)Ͽ?;8SW֒ y75yz˞zc:|X7/F N #n{Ue>!.R!j6 `Ss75t-'{BF١)սb2~g?"Bu(.A҉Km4쥦}$YPH*҃<֐kW:ӯo pˠI3UaV!8pV48`P|57kqx(,:L8S1^ >Mʸ#g~pOPاyrtƟJ 7ﶩ ,}k\w7vGɨ#qk4?@'HИz:]lA>XfY:B.P;9ה_7;E>hraȽaO^zTP+5?bWի_$dW!\R +-"L޷A-MOq!{]'xzeQfs䴪~ %#)ħk}2.[L|b(ϋߍy|*l_إ9|[Ij %l^CNLV m }5uv|IP)d5|ޡsҋmS$s虊_r1E.N"3n2nk!H8 7pk9,Y1AL>T1z2*L!Jv+6us.u(]"L* w)ޙ#dM-r S)\jg;WhrK=9/특LShLooֺ>uX)!9oSCWEzK3wxSLR4M;uR^VP/O ÓƈS %'JJ=V${R!b-z*g-whX3#]E*;s@UtUA籎EWJ''ϣsv a}/%aS ~iAh}Rl=*ug/l"(@K( %/908싁8ְ.9v,dg.P7L#3XR'%]cg$:VE|H sSCgBį#rZm=^lA[v'˂NtLG%(y *k|`M̹9b=No&`r}'㧆|Y*toMҶ|K!5/קCZ5+;ޓ}ԝ:XWnMCB˸fK?nW%jZ,'j޳gP1͑VmcvYl +0 S0"xSL)JU,Kj588&2O4 E~Yı[x g3ЈƜ +Ve5<@3K` 7t=2CMݐrEDML}g*싎;NwHuN v #jblmw,د3=,6R1J.tu:aB\Pue>IΒrnI9d +Ni0&)pmj'?3>FG/|G aj~}5s@\/; zo8\ bb4tCWR ':~W 'W R/t=3bSo@"hNl*lӕoU i[A~d $/8DmkuF/Z}ab|50I^%ȽI;PK +6awPN-A>v<쮊>O~f+ /QQTW?TR*OlWs/eZDsWXAR5\T'hʁqKFN1fw cڶʷي\u4$asBMh(3CG$is/RR:e]>0u/\!Gzegоe^E]hEtbaNWSW K5Owebem:V Xitzwmp#xy=]L.3[{` + +J%:ʧcwZK{5@n_a׉汚;[i< 9X$͠vG}/:d8ٙ!R>0w"> + !"K2꒒JsW,)bt csֆkOy2<^g@A;㠗YXu'ܚ=؞VH61T/!߷UD|OMEv] FA/$ckYWJ90cwV~]}>{WD-WjOW-[${uc'aVz +.XF_I`g˟!-~R1St%* ^f}7~3_tS-,:]Vߕ|Н|j}Ǿs!m"rљT$}10GJrWR31[mT7C}}㱉I>P =6Mֆ"c +r^ģm2Jr[NӉliw*hgNo,"GYx{R'awgIYt "*E9}-~i3jlJ%Έ7ԣ^ OqIKS_[]s]8(d-u` l82+,걁AzپL7?ujRP+VP۞z}04bu߃s2xM-J:Iaz/߃ŷJc U2xcA%l8%_< i Sk9:>I]#$,2c )?}C䑑E;5@;svfn3ȕN)w lA'Xx,1k_g=jgEvŕ{[зf +2nlA#g}oqf"--~n{.x#9xIw8m{ +B ;n̯&M:uL0'b} x}6a/se6Hͦ*Yn&|(>}uK׾}%KF"0yXMXbi.ojך+\e*zf}33 bB~Z81p_Ě#)n9 /3K,Cਫ਼>s2)iU~\4,_%!+XAeLN\bo. D(ؗNe]]IݞB:9듸4e;|Ԥ*"$)E8硩x1'6Pg|WŸ 'z.߷޷01{g=P_(w)1J9L[izULLO-Wy\:2[ +bmH=]equ51.mu%!dԥ>TGmO֤fkbwD)NQJړ :RT`Q*<6.gv穥9 _kkϝRJu +=Tiac.2Ϫ5Ha7ֆ*BAk[ʎϮyx_YTb'}?5ɘc}mSZH{fNj;cM]*X(9K)tUz6QKs L !͹𪉥D}+ ]ef8ĸ,Ev.q ; lu_L4>'DLj,0sg}MK%SWqDie6xw3"JG΀D,6W!oJ(Hl bJdO +OؓDeeCc +ccO7oT]!$_^>2W<7Uk׳H;0ia궢#\yl4J1%1DG,[)vw VcoaWJ+(G3]}[ά[d +YkYBfp}9V-Q}W<.gSC*/I=.wO̰JvTh{wƘ!=zۂOE3ħVXS)( U74z,cؓ@Rk^%lٔ#c˨GlSw6V3N5fA.9kx[ǖ-r &8gh-p/`\TKNNWG^j"_*}=MȔ5^nȿ tyfBͩS3W-G}7sM\<$=]@ +M8Q@̴#t,BJMB-|⮔X`:GF6ދMpLsjև昙7KLyk݇arឲI`KL>Ұig+\;Pc`R`JJjͭ'aɛҚ9*} OlF펳MABTŞ"|-5'e|ECUsgE8P-9^S\zbYJf>hu9p}.ws +mF1c˰IE▪NEcgٿe9 ہ'pkt Pֳfi%3һvT_3_p@bE^~ hM=yt.LEWEGjy\,]Dϼ:I}`6⠒Qg$l0@N%-pZKj +k]:a\M:/ +1G+o>mt]ny:@+8!e +/Ϻ)e_4VlTp*FݾOTjSdn_ѹ%cs;\_]yy6@F)ۛ[FnmKjM9*:N7Chro +kB׆+߬ +#GNEWIKs%>]n.x"$_}6bsGhT91!d, F[3#xb N ĺ#=@rX_ +e`EGpz7#;4O\psXx W,K0yJse|({>Z" He{rέ1N^12GTt葙 <,|#@T !$t}b ~SPt[;:/(!\*"P9АN=^)#G K1{*ߙ $۫3쩉6d=!|c>GM:RaS$ܠ3GO4%w/׏VCv)yd(l@GT$XV.Ъֺm ;)h1d!|&GNוpJq桒瓴 ]!rqXJw Y3i'*8.kU5K'⽩OHX?jEo(xc\i̻ R\X/FYTZa41Yq[zslVwFvK#E׈GܓPK>̝5Ԃ;2\,o>5eCFk^wUl??i`XV'ym꤯}xcڨ,!s7&!oG9O`)/ٴ8pUevgNTj[CZ}>%d]ʾw!ŽtW ^C϶- +.~{VQpp%lk-~jɺ %>VCkQȹ ]a> 5]rZ(@Kp{TX +*|hWX4_)}i9h68xp5g+$ET["as%E$Z7G+^|s[7˫NQKsrzw;8c{BH'coajOScOzW@}Y3:Υe|.EcСFP2G2vݑF"m#"}sl*^#ౌ{]!(ڏKt-qN%zW!18$Q\SHi+M=6N%h{ *tPAkZ_,SE@|\$D/w P@w5M'1zj?!2 n?zk# + C)\Bym+;HE:ԷԢEmӾYgapIYAy}wKJNvIeXFq;#5bVVh 5U8Qn95IsK0n n_FK9ѧj  <թ{TƆvegh}9PCE?wK=ϗqFk"Ə_ܓBh}&r_j{l[ r~g.~_ꘆEo C?j. lF?>dڍqdÐ_T@ B鬎Rycl_?)d $̫!J09`Lm[7. { +EلCd%(9adm{բ > ٞb+,rnѲty(@Y_j&~QJN{☥{+/ ʞZ 7!!{3c5v?3 gk]?51_V*DK=N,U(b3^h9mv&m"`=Ofs؄{M ?&%o%7MȲ= \ә`rAHx}Qzems/a7Im>Ӱ +\MXo{`'q}nQ`yl]7*xח+B\:f^P30:Ȣ%IЇ͉W'o;ɻ3il.cÔM| )Zk^l U+"jC~׋|)Ado KX5)KTEa n ;TH9,s\}ViC=ԵPyS!+#<b}c[1"B\Pxg}[ JzskZ^s.O-ϵ@;E-{$>X]PK?Rcnp~"<['/z{{gD|eh}^'7_J68@B͹f6e=q#R/-7g=tU9F,_ss5T>Yrk CXQR +Pɧ:D2D+ɠi.),gON;QSk|дk9YSS䕔,9o T VҪ4 4u2ԷL(7V7=ٚD眭78$Ă!d :^-}:H2'&=[kms)lܼ[gn~S- pf8ϏT$_ܖ= ;Y|}(SZsfouifce}3ǿwڣ XZʗBb*)ӯ?-w}vtk:SQ\+kYpܞ; U22t!lȠy FeDYȍ8Z׸MtOBPnQ@;ӄP9/롌t5W[l)}(ؼ#VP*bjeZ-$YghE["V^ق). "&ɿW}a +vV@.ۘc>Kx}"4x”#-锳ЖyZ|}hwfW(-~LVN@(V7 ANhuў3njT%'Ux,!dOy<9QrnqL 澊n1:H"'P=)hgR`rXDmE*p#qfd^َ`GF&{WBݑ0!L\p4ҰizEEedR-~tcg(L +}= +,7][ژPMվ^j>Y|_ג{ckODxTihx8zr_C4u6]^`F3o(a3k<2hZ_>e2DG}YastanMS4c +"$D^SlBqqF6ͫ[1r6вI|{Oet]eM;h:Ywj#->|04:|nJg +0klIrlbR82ܧv?[FXg"Lw :4 ;> ak|:eiY-@,<вهz.mE;Y&<2Rmlԋ +V */ ]U+%Ѝ1~=z6 ~u@ }JeQ߷_;R>̰I ھ7: MƬvpMWZ]"!oMҭBlV7<\;$Gz?#{ y ve<~WHHpVݝ q16>LtEVI~|浶 /e}h]UIwinR0Ȍ97PWG^}O\bc >/*#<89&+oӸg? "%PZ(dVG?j6h봿@GS{U)#(:8NŽ> >Z@|@4R2 }<zYl ZYͲ'L] hM45SyvORZmQKW3uTSs⚤yn̝ \Ցԗè˳} 2]3X'4]1R~);=g&17Iq9}nXq^-h0Gaإ4pGJ!KMQ;c9'`s*B3CBC-&B@?!w&`y|W"o{_pcRNLW#Kme?Y0Wu~_Pĭ|$'o TXm}t#A# Zb6ͩܝ ${:o;o "F)K^lSG?, 2.k)׬3VQXm}nBG: [ &"^Nlj+晄Ux]ryIP`Zx"݌#ظYZѮ_R%vX 8_smma<`u_Ӹ:9O.ۘfZ [E +<$bcRS֕K3M߇$SL2>XK콣bG) +* w{M1y#<u +XOV!?6.ōq{1Кw{3-Άl)#&1ViԽici(r>ai!-'w?s8D~kQ1q>:6).ycG@~7Dm8_mx#=1!L_y$VNN{'i]hOTkfLLqgWV4ce»/󓓓pwkq7?=r{CUa~8Mo!Tyz~oF$NvھNw*gu^>. piԄ' h̲x9m\b3% &amڴD}Cd@\rLSH=9Ylf@Տ'k"ԚCTQ0Ƞ1ps&Z 9gƧ.8;Ƶ†%G`#`n[;fL'rˉܱcsw-)@u4\nG'?9Lj9Hn^1&ӯذq't>16F6ȅG)#SpYG,PK{ W-g{:(nQ_5a3 no[JenO!5*񌡇;#.?YTX7"g̊~zc ~ԇɮӲ{.H֖5j'45o mcӼ&zEvt@}ԪE1FtҔ僞+/;h5fxJo`.::Z}0Ƣ|n~ +sSWlĂE1wAK+^zPge>ݘ*}87Pt_l8i:n+lgab7tȾSiX@m꒵>XpH;*SI>) `_LU.7iEˠfB*񎇅:^=\Ynݰhaօ4B9 =d6ڷmFXs-6BAQkVf(S) !mDN8zI-Ӂ~2SC&)krZK3dϡ{RϺeS=S6c{ al•qnR岍P26Hi!a+rlگGپ1M'~T+*0q.ii`VmmJٴ ׬u$ 5LDS+k۽ pܬ򩝕pJM' Fռ3 PX=rNM`2y=hL #kOV͌c6[!;1&̝BRWY߆[T6uiDEJ.sS"-HA5T_U|. ,8k–U lg67dUٱˆ%2ce8!d1WpYQ7M̺-;femhMJüQ=uszan˄/ 9㽨y rȅxE VÀ TG-j)Ia$pE@uHρQz/KECrxBM-2n:ݪd-Oc]'Vo{I2%&( Õl e;+_4OםƝ abBP[QVIΒZ5o5GMYʘ!׽$ȲbImXqNaSnʙAZ^6c͚ذY=NvI4_eK&b麃s[{-­3N3"OAFfɀ,Q6mBc +tފO^7ݰb'-DТuBѼQ=*;FO0 &M\556mv86yR f xAe hBR2pR]23԰`/4~L5BsלH[=c.y5;TZ[O|ȗ +|⒅ոhigڻ[C-!XRq4/)EM_{WZt  :JW)32zіOъt¦Nql"сęA`}/K^rpSI .i 06ުV]l䦛gWHVh +^Fӑ>yA 1ʃIhc n2'ƔZ IںU)[-Zy!@M);?@_ O;8/ݔmCzZŴRdBΞNRџƹE7>W<]y{*;er0ʠ}P>i0oĦi [&z\4F/qQ;λ3it$6^G$w u0M()s:rz/JI?BFRppu\a'E'h5//X_gVׇx9;7nn mTfzD' "2P!=.{ш1P~>g%l g:[S\usm`kVdؙNpx>rF5ܠhSZ2ޮ|;m!+zc]bB.jw)cUuk>@{(U^~omr,v@60420ׇy@Kx[XlDJ8o'.Y}::>N]i *A_ h(=5ԁ⳶,ܪ^wto6luoG[*c@S.UD]9ceQqͧLE +VON܆³m5 _ܽ=Bx3yfR%*Zݔo{͓h!M0W)vXPKYgLRά=c6ː jzݬA0>9ޚoe} ddN ʷcĆ!ךҐs絴M~Gj쌆;kfC"ڄ6*iծؘu/a– ){ [n( oRD{'%l;$5Xb2x_cB:bWu;ylmvoeW fƬ] 猈գ̡Q~cf-u?}Lǯ9ufb%3uYؙҋ:>acB0wݲ;Ӧ^Ё-40*6F +r; s9^Iէ]'Mr+u@ MttVZ{%ihz' ۙw. &~{B<3):=[ ZȌȘ3a +8q GOqRuؘ7 >iaƄҽ)ckݴɗ xD-ۻ6`V͠aLnnI!gDn;6-d4Y7v{F4X3k|T|wLж3Nl +]f@Q>)1uv^h]4 #3jbѦOYrs^V '`^?k( כXH^`,Ȼ!֞\˴X.+AhyBY;hK켏O4삛^g/c"Vs#zCKc}vyomH$)cdc|"^Z{sJV=@| =TL%GWbf\F;:n1c™ +k7!瞧t1ǭ2[)qNK~-4@jlK@oFv;m"ބ@e)Dgk#{?uVп;9ogH06 p%dL8 .\ٔPwlv[GSnz%xM&Q6Uǐ7ODӿce$Øн'w MjuK Y)wg[;D# yc^I-kR*pb+AyH9MhZjmZ#mƲ>=CV#@-eJZ{ʚW5?cѼ +N]r +Ɣe+Ñ&7 +LJ:| \@!ӣ"'47ZuW$e pw]WLL7OP]7ZZٕSH`3ft4(1)DSa$9W%8-WsmҶN4꼶͚7&&\j [' ,"3*^_YG9+'^z:$y05K5=E_2IXJVAHC+xϠP^-Cɍ1z ;%h-x#ISCYp u+=|qrUcIG'om nRմ0 +j7;6P Mçirs ZV!:1;{abO461u3 +IøEJu+FD(}f@3{{UvU\.X;vsf(5aS)Iya^:jYvfH~+z҈Ik- QdwuNп?iٙ7'l+iDwHtJ5^!Qp:b5~-N.DKk>nnlqb& /S>X/Qy U\)=LR/_W3'9]]G{f@+NXJѮWib0!ӄdF +i w-lcjٍ➳yҟ#@an%%R~l%ڀ _H~㘮+x.{͞FRU5Y/gZXs\u:Dkkwr"b=G +멧aj.-ԵKS|ܜ`U%|w.!R\L{>EP ,1 + W\/̣rzoĄG.!P#&̤^p,O2SlfGId@{$Z 1GEHbO9_+2??[N|n<ȸ$(N!SmzKA!y@>\AuMӜxӒy%9e=??zx%% cWԄEblO{I>kn{Ez;K 3q䟞>:KQdzǷ= +{G8>v^u؝|X>kC*YD)V:Di#l%9 CIGќR ؄),]h@,xK1ae=~$ĉ;awש.HetZz5y{_҈cw}K =:CYgi{ADBOfMM8y;]Ƽא-;vIm݈jÞ6<&[v!Q!?ѕ/ح|6<\T ~I{M.*paaǽdz*Kʼ9'>oFL"[0&)'0zm0*p8/aOxK:vǰ=Sl"TYO߃ yOLnPΉRp6k.'J:c?yvY Po[~kuwZ9q&@Ԕ9ӭf 4Bm8FgqMࢆ=.Y&5.[|Y縏?kxcU;=eFOL_!+5a֛=Sn: +q4k$b{#TG&}d,Buҫ6yu:ؒSH ٚxNi:]rNLb ((PUǮ/Lb]yRvgIRO {d3Ǥr; Ő]R0rVTg6vTo8^$­°+K?] x-;,-XZxAd IQ2k`/ةUv+*5C+fL!c5ƞ+GVKj܀L)|?sTx-' zĬ3UjWfl8Kxb{g5XjBoɸ U'6.uPF&ł[oHoK_5o0hX/J`n1/MOTYFZF) +y6 m6u|<{Vֱ^ >Y]-[4&gN(,ҐQ9obz~ŀ_W#q;BIZ,ӂZŊDDFfA;[ju {hց \drc:j]G5t M]t)Sz{e*R׀RCQku=`m夁^4g|좝t_.Z)5kC<ʈ3=zm9[sM*S MŽz%ў%Zm V{VCd0C-ѮaJq݃oUMI܋#].SG)ڻuQ(ݗMzUL,Tq_N J~>-^ehm.ZlЁִVީT *D~Gd(Wu!:oohr@Yֈ`;.1*!3*BּT7L䤑^U%C;)׋^iwCd0F +m%Fy]4!+. Sjb괞S4a6a󸵝ӵ (74b}kkC8gjBvΦKn|mG}̒)vZx?cf,zM^ݸIְ +*FMZ- +YWp +LxH nؖ6UcL=nQm5h$nm; t@3GNI]{7"ʦ(evTP^Oc[0鍪A1V^@lSI]/u^(v*]Da&zꝋ|+NST=8"bsrUyI4xf1 !IC&Am(ȧ_v8d|Ŗ} > M>|9w>l 1hsMP}V %36!r%xZi~6a ͙Ʌ+.JղSmU="ϊM[Gߵ{s6ZeP2-:IvvSeײr]XhNrqvvfP 2+5WLΑO3dœ +NSApp;U'Xkq5HЩHNP6d%fN{/yf]KJϪ_Xmߍ tV_ԱCF!|x^2@mxe?GuC]̂xA ~j'lXq:Lww.o? z89l0aK3kVڽ ~7D(*)u!#>s΄1T O 'o&UpN˙a7Ċ U#Â)aXEi9픚7;Nsb +x՟ UN\!:j_1@J|ƄW<;JGoqW7(SJ֚Gl ]Ć};;DP xkϷH(g X~db[8~6ẃۛKbR&ez4|j],EKS t(p= uj0ґwE_zT S +d62]M R {n h~j +]6?|P1g-4u=QݮUi2vgN7 jj,K|8!p ntW=ٳc > &+6n(5g}%ၮ[3»6C;k%:7|`оu/iM + TD|oh/SVyߧآ KSɼ8zF}+8Ń'6=lY쐙\6Og:ӂ16cIԡu•_Gǜ_p KVF0u2LD|raʾʿ-}ttzB)]me/ `U)E+ -=mcVtiI#&[Ƀ[z_??16S)zПueC_tۆHuU3jI}B}*{U_r^Ytأy Qafo3hdž6\rgf;lHڳ?s^|RåZXN&‚]GmYЖ;&q.[6Y!R,2з-="yGM1A?.(룖4 xI,w"3 6|&V~v g9/ @|$W@S澻~^pl*:sݨ̕,Ɩ5W&(+u!k Ʒ1dXaTA߯/SvEw/]/zCډcv]bA~zvcoJe`Po>6=e&=?03w=+`\_Ճee >}لž }xZ;O>7N<=scc51GֆΣuq3}=:E|j> P_EE C:cߍ+ڵRt wT@Wuo f(1% vA~_Q4^E3. "z%9m]K&_dV 5KAQ/;/츸ֶPn^cE_4\VC4+=Sڴ-6!FXz~ `]4a}wa*?p7hpH4Uw]^뇼ԑWIs=uOw &qM xWCOK٥^؛/NlYٳ2] _ ^=ڵR,D3^?,ã9jxh[֤3zҙv֕9P沐&2Жz}M z5Xd^-v @xyBݙ5OG`kvnLݿOѻA2X35_ˊ%(U &Di/-|/&v>zst ōt=ʜ<;-jNb":Jg) +|ӺԟlMރ醝<2*!=2餤a +(3Бf9P3y7ܨm'&{˂L4DV<&3dw5FV]p.;C*vQOwW?^b6uM1!yɽ1Q?X7qD,oxem6t5G{pܢJN/m#fzk#U/L oWO滫8%!BO㇓&bRԿYб'{L +R,/VrٶM@q~JpRr{H)6¡l[&LƮ +ħڈk7Vü ^Ab71^g+qR.MKrˣfzJLt<;ubzּ=tó>9/kAσ؇RH،GsWQO.^5WEshoɉ2 ^7AbӘ_?~^ɟ'eo@5e Lv$]rߝV #~\\5` +^aCo 틋Pcu\"~RϚյq;ּ_p_F7Xj 5uٛmSQqe1Ҿ]8z7љ}uU]^V5D8Sd<, G3mME3}gzA}2n[孛c= Ҝ7O$~(0Vt;mOZL(i:g_Q\(E횫\u #$2(귱η{6tঋ[GS_1 (拗\K7_x0E9Cؓݐ?7FR6c2O(ewUؤ9%nm *cE۷6y ñւKqkچ I깑Jcskޏ^!󫣀-  UN>1oh71q'=|Ps'(6O>U{b 3 +ޟ VDnmcsi-(JZ.vVrnz?~t|oɣ5UMus=}zAf:n(.k_$_5DÕ|v*fdw8#A$o>l+_D7{5@ +pXVt΢zі/ ,{n ֯ C;3G:7Q [o oYftltWٮIμkʣ—~a3On:-ap/U17KXML.;=UoN}>L,q|7w`WyxIKM>硥;p (e/vl8+V9z|-_u%"-}7jɌɜ4b"7ܻS]ٗCMt4' +E~>3 X6 5G(S^Ytyj{_~~G5\PTpqtg^ >oxy2)ʿ4%,[lS0ҙwSeJ 6|s[:rΟs{%5suq2$*"}{\RG2'=OCKnJ:. -316uP{Կ +nLv\YVݶ@GXcw9^ 蟞C>z}{ZJ֏L]с"CݩAч^NգVrd(75ÍONLUg5nh Ȼ)n̸Bt<}홱35oM7}3/z½kEzh׆3)g='DŽio'q+ +~@CgѶ.v )Szj_ب +zl !~t6pl7>r =ǜb~"ՄTo sjxܙQueꗫh^M#4aKC̫QnvcGIl_Y䦶ߣO Zx0 yi%몟u; Ⱦe(yqNSAȺd8,_Fq{Ƅ5CUĆ>(u\b`J3K#b*77,ye;^\ޱa;8bA}F@|rKm8P9]>'oxF&nz|ꢮ)nɘtgI } U#,nBRrs:[@q<-=-䵑L桛ִhBexgMK,3w#.tv}Jy}E1~B)Ҳ/mZHes}Ș5{|ֲ + +]@|m|pp#'sgk +RP{ݓnȻ/ƚn)AZb$rNVvgDEW&JEV˜dg!>l_c "9 ;d/l5̌$J~?ᖮ&5jp(ysK̿:і{at{Xwsvk[zEZc{sgzN8oN eoowp 5ʞ +yYh/1Ͻ"UMMԼX7³mB@z4%lHo}$_ZV'/(?ߏf)uzHܰ(BHjNF&YYt' +}bFwouսX7#V%ᓢ+?g$Wu/|k{vTף9t*z4)~{=ަ i8Ո3 2hct!2tI E{*L>PI!2&~sOL) d2t$[yyѾ;C<d3[ ^8_xs4Bl| # 0N]ɎHoI jO.ty8.9UygN7$L\!-ꠗ]s.ikvem=-o^jcPRwHI0~h}ez*"VO>kNF[[].!زeMU~߮*>LG@?/˺5y{eZz7Wjlܘt+'ܤگ>tѮ 4"ʺBHN+x$'z3˓]"=Na?{oh{]ՕUYYP  rmْ̌p0̲eYeI,CDfuo1s=Νy5/fKKs{^uOanN[|UϭO_C_Z"|B")?(ԄgHy;}W]c-C*LFHG.3O +0$y@{ZBsحC##F 7c\}ȩqjO89t,s zd5ڿĥ_YuC"K치YD ~#愔"9QrgBZtHE-OYpnCN:[{&[cW?uM2KØ+ؙZj*v r +ߡn>_=T5 2.,d0SIWO BG02 br?40',%z8_=;PP oҢ)6)JUd 9%y~w΁sڃ%} !cXۄI+PsOn< ;%*5N)(Xp^԰A_J+{[~3zRE퉵wK (ZED HU9 k5+KYxtɚ욦H+SޯI+k[ko[;7룲wG}Ș|=M( wu/Zx=*`W5V'~M'/_`~5ʸWTҳ[.f[GtUy%h`bf. ƕ~T=!}CJkY3[/K +E,-tM|ʲN 6+$Wsnmux⒮;ϖ]z1z!1,-1]j/JR *'a8SXA)`TE4"jaKv~Q~R?pQ[񕦫drC,i]Q\bіTCһcoO@=J& h/!'WVz^<(ig}ek1.uK0kܚ vwoU +zRejL9>m;uU7K}_gF9u Ko.M*lmr1uND}򣦻o~) G?9Ƨ ?agv:b^؟4~J0QkB*Br?'5?_fD{ejyШQY{R=>9 Z}zi@I+jPTj3eG]Ӵ|WG$c{9d'_ɅA-nOgQC`g=""'E䴚0 +ԇa %L<c^5*nR"& ]~~\s3R1ZC|CMgrBi$$ǧfhQE"Tst +ِ[k[HP/O΄eA9 ]=ZjGp 7\{[ARޗȅwFSF)ޚXy<夶 MGgb ec^f}?aAzg{|3Ft'i9̮]*G\B\ ^yOΟKPÄfwiE[a548 vSItP$R_㏲]߯#,T&pyKIjKmGΧMFQW_5 +uq PsA1 +IKb/'Ŕ⨶d{MLWT߅R=D]A_Њr!Tw*)tz|J_7k6 DkڶFf 5yE$9X[ȩ E `21uo}V H䊸s)̫8(&8>5_9>\'Et&f/&QmOYx ?RݰW1kSZzڞ6]YG8y͙kN#o%򴹷iV*y) sd}o +عÌAmuw 7-+VXEZ%^fyb4wJx>ϱ?|[/"C>԰)5Le¿/םAuG(wVq#QjqHK=X&.5mW@mr5-44YXKXla}\˚ .sJ9T +L2*c;+Qxs)oQ.\ ]W3q.omLgWNiFuq볆-W:iᐒ ϕ)bO{ͯ"ZruXC\釜P9iL掅N"f@oqMϫT=O]1ͪz.1f_/Sr_\L*F 7d^cVGH!2qQ+C)tֶPԱ YVޢ-1ٳ4ɶ ڰZW}K:s[;z , 9M. O%EX9ļH6!*ƫ4h{"/p9^-£-"VbȜg|@$7M;ɯT {=z65蓒v)si~FKmWƧZO|o@ "OqN j Οè {.֤akWO +js{51A(Yk'M#oM6C䌹gMkl~D]Z'^ߚB*1=Eʙ{z-*NgP'CiV_)B@ϛc*N&MԜKcPǛG}6`٧[|Ӝ6ƹS쪰rKٮ)u/q\ hȥ!N^٥¨FLp!4Ǚw~.1Nǭi #坁[9sLO'™o pJr6Qc&-aRjj"c Cnνe)ˎ*p1 =/ 1e FZ^ŷaxs"f*Q}t+cCG\#-W wTܖKX;Fm]֤ dmRl~;QgHQ3xy?5nHBz54$Դz^Ԇ䂀ұ1=cA_ :'1F-R?ThfKYЃ`E(@kib)fŇB@ ё~=) jYt渔[EX&6廯b2]4Gw +ѵ߯o:7n)|}5AL Z/l4JvjW-“f4!/M^U=aeEhZE_Qv~f}x& +Ȟ=؞ 677 2.ħi#OtM?$eo/fh%Q}j^drgIPKiko!ǝc+! 1vr\2FXyP2 tʻ34Y3=)m)" +j$`qtcqr#u|k^)=Iɦ!5$Ou6(5)>i 7:M/?o3| +VqUOݹMP*vkXl^]7] xĄO%iBQ&~Xm޵0HV68Њ!#eδ}-5kv1asКF6 ^rY +oiAs >gEsO +H]v=r5&|l&˦R~Q#o`68e\\PP:vK|O*ɅRKʮ {6FWZ;K'\:0/h 6h&O-"k{ca?2}ƻm6>F7>FV +K3άnM8nS֞)J>3ak]lYXk8WWN.{zX;^}q}Z{qP1UFK +];AUnfm4^N<(qIQm;٫. /-X"ޞխ Q^:A\XbN@5 89JMj;Cl9W ~g!gS؜ߺ܇:$e@>>]v cL|ޜU3jc6)-'~ Lrc*zM(&AzŞUt07@O!=̯$f1:.̣5]!sWGH#E \cv-C\wN"6.O%"g6@2XŐ؛ ixQ'뛓T-8u)(릓I$f< +j%f{};c\[~.a=MA̱ B^"- \͗CzJB׃lwWƋSh8!Lb@FxT\o*f509Lvn53ļ.u_[G[HC*qCXTH+ -v?/HwkҾ| 3bY+@nk(v=MgE\F=9@x7j?5V5=]n˃SҨEĉZ8!'ggYh^ ڤk7Zbos73Kv@ [z} ^$׍Wvgcv9`{Vuis:e߷_9> F]Zw)MY>.\rȸuNlmtNBtIR0"aqF 7ӉY =jj)qjkBP68&X Jk }9bVi~9S8_5 uKR[Sc&ab6ȕ~9&smqa (p:&msy%! +rIBKK}e]Ð3 ,U <3ܐ Y-x߷q 1 +Vyl^hsY(~-6;>K,,!--E1Qjf&І+!O +kmǹqc'h2VRq+C1ĕAE0Nңrj͎ՉV6fh;sm`(LI2_%@/Ok+aA5.wy'+9Fb[[,"GBƮ{n!>v@m1z9lRv]#f +ARw-bFTn(e%1|}e\<LJ|rGF_g B/I;Vi\k ϶c ;",΍quo {ƞq[WO$Q9֒IK)FL_~=:XjzzmPOiU0Ns'6|GAIXب];c#@oEE,?;rbq>9s*G1 >5skB),SuwgElu?U| x~Ns@ZÀ Iر91ã&Bм5\oNPA~9zX^@/!O)l;m'*>u5j +d} w +qO#j:. +9>'DfEa=Я1SpjNc /6ʾq DSoi:[#@dxո|,Xt~XyaCrϡLj-%S+Su?jI}dYȏUA p9#'̱kOԏ ?u_6D]@o +Ep[[=z "faoU(%[ wlLĶ>hXӄpЯ&Woȕ!# !o|=]{֧%a`3B_0;f.*a`6킱sH{Q)^TuBcu g$Yz[PЫU9bΞ iY j.[E$( JzEn)hG/$:@[J8>٫i4bBߖKeܕEOɨ+[퇝F^+)ŀ䜵A̵A4iXN1Eix00&hEKKCti녩kyPSVKJjWE/__s+q`T|?鞆_]A\ +j%S3wĴݠ ]l>99voem{ƍ<,bP[k#mN8Xd昙^wģ/vttU{hwGZ:~vMBέ5^;ք bnD$"J5&#CZ;?oKo!=RJ_#"ξ'v~^|>dFդ vk2Hr jg 풘Y@ݷQq==Uwfsbǽu5N[àٛ>.j:+ +[@<ȉ܄Ű&!lL< Kqv+10VT|&-"z$+JRsϫ:&!#:RyETgUF߲v}Tb\g4 2kcbCj>ԯWUpC&7>Qs4wOs&VsEM=NY`""kUяn~\u|MU t u-{=*Z_Aߘ 滀q:Ȁ 31|iD]hÖz,O/G;;rN ۵[ˆZ7Pki * z 5xXɨ]΅Iy==M/%܌hqsm >JXyr焤 哷L!3 J<(ⶌP ΤOE-͖ZDZ쇟ݚ ̰ gw4uW7SWA9s;&!q#|X>$Jf%'11H1!-?/v/h=ZFCdAA҈9ؚf6͝m; ]rPM\΂֬v+bfn9 z^u7-}7 +>)xu͹7U)*Q&uoUқ X{˒;Qj^֞{VT${K\oKXvWUZ!Ěo[ M 1o5*#6:ˁFVWr@oXɹBg+]ӤZa0>eyaK"Z֫  ▃: %g+XU /oO`cfZ6q@UBOFvqN{-uWᗎsA=E.-J1byBܲ9o904> .d(l%[rNWuqAWpNnYV띡VLbJ.8Su>S(4OW^{ppƹ bᒲ*C +eVF5P519Kx1#.Zo "Z\nP.pˠYibO۝R\һ2r_ƪL~WfzIhS}}]2bwQ=욖^o+r + *k㌠Qڝm!׷&iρ. p_+rW˨ڹNV"f'%; QtV舎\ҫcfvgx{ ۄ8נ_:|ʯC cZH) 0ΤM{2[x0CrBݠQέ1vpr_UgY~"<'f(eks\*Cr̽Eew ԝ׵ʿk|ry0=*lBȉ؀9Im&gy%"*ji@NyȤID #2bFbg X #٧>FBlh(׼vV,I0!=&+GMlW, kȀhEϪ?$h졑fXF녴f 5?V av/+0Yk5+­S7j +L, 8 J(a}]96ۅ5}k Du1$fиQL9ۓ_?+:vlmA= WqS^fD ZoDa|o~s d ZP1]aFn@.8&+:F]a:x AV贰N19~ &fe!vC $.qDQ vh7I[gw<</ba@lh;rI[{R",>/xKYA3d[z9 =IrcomOSzŶ{&, +4|i2{5r@4ֽ~+~PsOٜ@ ~ R WᷗF5 +6&li1BzR>m6 ?9' :7n`Keh ҂2 +1jvL\dsiٝvE jqyѯ`.WMkI Pj#zVcEXDWeOs/+/Q#x,#yqJiwqܭYvl"Vl1cE-r_Qs9x* +X`!Ihw?-WHawZ zP[#+[RF`j} 9DŽTgᠢ:1*Xm6J:1IUt}1%W578 uk0 .k" MUۯlM3V~myTq`,7wtǭb31ۇkvrtNP!:f.вSsw{-|X\3?' |:',QtТNa޶(nu! ȥ!ÊXFZ=ЪeA5zFXPl֨T1 =f}\iOyk|lߙ}1tA,D͞>}'/,`hQ#&|Zf]Ȇy*3=lS3K(p1PO,||ho-g777<,;_^}aG,|XwT帆k9crakv+'D ̺KBep* FM/y V 7ipv >ۂJ *Q-<Ԭqlcn_#ַLSu1\Ӭ7irQhTJx;>qg'j{oh9~'EJ7=2T&ϣvN|]ksR0m`r<@;Gd0FU.WɨLWb2m/J=/;jz15l3};֧QuEߛ{>ͽk:ve;G**bCB6BHnG;O>û!CQ3ӡ]SjLji5)'mEfIJEHܳ;2'f:<1"SRǬ +:B2 n2sļ ~'T[3%/wLl|\~nkW޿Uuoq7>nL|\j}33 +hE@NHwR n!t;{vr*ӝ3@˳Wz^IΦwjr" {*u=Y-9虆Yn=w?KE[ƟלV4o +^yAVXŬ]+1d +7rf cwS0m+7ȋW AuM:Xdc\BĆݽoǵ*䕐-L`ƘxG :g`:4RF{QwSٞA~T~6I(GC;{枮InKm Č'@@Ћ[M3㨵ODB[.DnO*c:&nO^Ȑj|^RJ|2#֥t7n%oMqmO`?Irs>_}U&i&UYY\gkp>9@iZTNމ:!v}ҏJhWzJU薭Ե c-p61EUĊ +peewcmy53v];n9q}z6tku=Vw橰_7xŽMsh)y8uvqYHFT t^,X_5Z?Rb06so,`G|g^ݘhCF.-aecJM؞PRnQK6FⰙem6.%'TG,0>S3j-}ޓ4 ;FfSDAߟe>.Y{\dAxrF;BJ̷ٵ 瞂{"Ko:1cdsU4g.-ٻ<*e쀉_By'!~1Pkv\>&kt/'0q#bu$s<`(7>5*YT +64(bk$<-7Iri^{9N&ay|{N)!懈XЛl  ³-~X~MY$g":B1h[%>5##ɹݰ2LGQ="ۧhK$Z@K h@{ }:ZWCȋiuqp)$juQo&\J{P}*JɾMgt7^U[~S_)eg!暢~53[F&'G;&]S;~-:ֹ8 +t_CLγ1>IR3!f# YA-zoZQrLCةZ>FSrZ&>eY.@NߖrJZ<0հ?;{:!Ѕ`! #>/fFg9[@}Xu9ԯ\f}AG'MIĵąYQ +JI@͵^Ӗƛ@?|6w1ֱw-ڈRHU&D+!V0&jsr?/rD7@~۳ٻV!=%o _|Wc=4ӎ {!NT2v=݀IU !ǯ5}X`aQ‚&,N_ȰR\'#,n2 {v.qC!5\%o5w (299mc9.ډkSBwR"|qI"AO +zyDIDktug7ܲρ4wlm9!eKFY胦ESImM¯i>@JZ:mU91ׯHi [.9KYz9;bB+bWE) hUY!`J8`ggHy1aag_ _ҡ,}XbSFoN _Pz9hPW_0B&u]eRs +`lUXuM!j%K V>WgW_Hh ~H]WFMhn)zoA%[cn\{윬;!dy}oaMYLv܀7nn9x1*L[ͽ;J3pӢT]aTFd͓v,b {/gT:% }f|Q#%u jCF+Z#2Fok,<Ƨ=9YsiE]WW9FjOU7YY1uoQE;'Nm pYIsR,Ws0!9>k] uO4̇y&j릸k&HNMۓ-ily힙 HecNn䑢n08/f{ilz $$-Brl #-6O؈l 0VԱu?ka5%& w)~V`䂠 + m$x?h;Ӛk>B]Z>`UocYҾ.G[_]ӓiHA~tv. !R9N~u?`Rf٭=ʗޢT}4I'ܯ# cʎ9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G!EÉtl>Rɉk؟T~rq1'a$޺tckq(S1ѱ*6@70(*s`IC{o7Q\}NGpc{ }k?t,< Ox)إADE) Kt1ljOΧKv^\~B|]sσ'܋^|$*sg[:ڦ蓋8> ˫h.;zo'?v׏|) u=,gR?>?o +v#o{}oɉh*nqO %9иIBh%SFsh޵QF\IH/h5Yvk*buSRJ +FeLGDQBh M-%7;Uk8>? GĂTVn'BRqE%)^)3fEJHXCR:jTmcbX/۱R"*|^LOKX؝913f`4D4ZäLG[GGr819Y.-'oӼS 2a"2Bx\-F0]#;+]5w!}oIwI'fY3C13JdYf&$bf1שCU%93Ǽ'jy-YR}쳏1SYm?-__}^ir0|J¦u(G|ƞ 9ӊؤt\WΆpɻ:cf,ļJ&\њ/4|O(> 9gޚ:\ƈydD@Ig>3 +u*٘:Ha՛qQ/:b^NY綧Ye*ܥMY{:v~n%hlwX:4(a^~5be`v^-uD$iVO0Of +䌸AtK֙63ϥgyJmA&S7CqwavYίHXUI s{ytIۯ;f:cRҪ%ͣ3jz;Tt%fʿ%-]PV5ns9I[-*QdPCUڔ`y; +]}Q^Gsw㯴d(Orlhr~{@sfwebol%Nҫħ;~>_>ĬӘyZQ2h_1w#A2<*Ԃ06 >lfEU9Q54aC'l ݂jd^@8bɭYX^NU<«>sn8iUvdl:Vœ_'Ω^Dmo_G7ŞDDGWxȠVEx$mWsO|UvO;NRG^%+uj0*y~{'&C_L9;f~*T2vwwDXΡ>zJN)89i#O` dh}Fӈ +! +rI>$>%p ysygk4 :"s>qt7S_lM7~/_4}1Y'96E?Eat4R>Kp s*tBP-½7S[I=5Ost%Eɔv ^UӔEFCڥdmNr` @S\G؈ٙb3V_~3ùvi듼;H+s+9 bzyfs۵?bؔ v{hLLmCXB 㓾!c@]:A)k\B弬2!\FeʦOQ٤Mg|W>ʼ1ӊQީWWXW_xV-pJߞ]{Cb4b]NWZ. bPٍGPZyހS^h?{U=?f7ȸt_HLLIW)ia]Nt.Ve}> +Fέ" [ʹA:`$^2a!ocu!=2_4\@qfax-'݂08:Oƭֶi̫`nI4>f cF>Vif^T]}Dz.=e7eGܞs;a)rqi>2<1eJ.g~Ne!,&Bbf(ٱKbi,hE+n#GOиc\@?S% I<=ic!n~֤+Sw>^{<ܜy#*]}}0><n^M'j&ҧS#Ks. /k6bVEKI9תޏOJ1'pMښrpS󴳸PRGie 8f 8u#󒚜_9c[څuW;M`Xz={*3+HL0&&9gaU)jh۷ >vu*q%h4wE?tؘg myY}lZTC_.'lj*>:/xݱo%PbBpa5LjTZSS +3bדNԱent? !:.m} +nD!.FbT. ⎔[Se*Iҭ*>֣N+j-$mºGyzyZD /"^Yf~A&˽ qr 1#W/a[nr$bT)tp+QG&I[nᡞ"d>'#}wS0_$f'H'PK!eV\aū/G]<֌>a'Zn*A,oc3CXf =xPn%3mUglvօW;3Ϻ!=DPG;8I+|ƻ5>=9yq@z-'dRGk~Izv|}Ug O>IN AD9x_9ڏɘI~HuC\ҶMikqj8ؑas#yѶ'z}K®d퉶}Ok>\~\l2.as.{9 3LS{:J[!)HYD@JǼ0քskꅱci'ռ]TJQqw>oڞޞ ]Ri.Pg|0q + ^z_ dN.^yů$dD'*1h؋B2S.otR#гEPˤw"HpqQ/(D8)_ ^%(E_pzqA@sVyԧSWJT|N!jI/HhH.)dJ!CC#oWi'"=׋C? +uRV ՁV6cNQ}8e+WД-YN+M}?}\NNI95*1RNi+U-GFui])ҳyA9ǚU֜U4lji +AtQdM d^DikVGڿ͚O%cXi+hdgm_{n:SW}̨;S˩IT,|j}.>S.M[vnWrSnTבL;|L;= 9|vɟCw]s!6:cK2Oi{\y x.CjWg7ǚ\~Z8ګ7هwF @j _٭eY4sJ9 '̎y8WȀF_VHp>+f_N,tg_ZrAa}6b_:%[:d`y!z xR¹tʒ+ ;\#moIkDA_>[ Kf+Dux1/ZGavt{ZesQAoSnQ= +BB6#0(0%Tc^sqcɯ̇TR2Ւ.AG8#|Tg$5zx{􃅨aY6e=M;3GQFu9jVi71}>{Θ|q{Q%kf,r>JW|R w[}u/ J=y$fԯ)7bԜl{\p:n+QݰF9ͻ=-(]k~gl)sӾd>LՐ n71>ūZKNŻpU)Lï*hcuGA:ō(\X/k?Y{Q1)3ĦyP+H3>ц|w/n!^= y O}R󼒝I4D"WfºR%ň]'_ xH|F|gh$_J#A%tZ$%$ *Y'jG6G?Ix%|?1F%e`IK~xWXPkN =D@&g0JDbq{&Q+ٗ![x%#۸_)L;xq3rc0|=}Y%m_'7T̸ $킚w) |b_KZȻ6,RO_0^#6M;gIU eƘzXw9ȥ˯>KNȀ>a?"& }p.Ov=ƹ@M-5/gMLq/''8pH7^\/MsO2[:#t;ӄ[n]*Vj~*9uz0#]m1'9+ɚeM[&A!9 }i=kbSY܎uUX&!6^t׍_v%wjE]*:jf)XF5JA-[y88<29W9C.TxTa Vkը]7~uz%[\n}xP ]}VxEvu1"> +!Z&|X-K~+_z]+Gܒvp.!$ +ڦ ||~MnR&aQ;AѠ{JSS8d"d0' 7fJ)m=UB;7w㓾[] I*n>bY!u|d u$._>*QGzr5F -,,Fy58mF.轹##7aꨢ*6ۣ 'YW+E\GR·Sޅ|<aCbװPQz[=naݚgڞ]*W3ii=jOYu洴bjie뾝q)©{&젵>:onu 2+ Wo1+qZmԺRfvbu;cM_Ps}󇍗sY 늏\cjrt99;xͼL>,;Syvi?j򖂑l:ΣyNUM_V!j(aL8Ԫ9MߴDyeMX`i‚Sۃ Ҕt +l.'w2KI w!$=C9)Y }Ta.s RxPX:QtorjdCJS K{ !"ӏR\Rܣd<7fs^K}V ڌucJp Hvj+|T!D!>ޞOc'Z\+E{ z̯W̲LOK->$}Fc&Kۀ!|rFqtXk,~{zNfEaNȅ޻{ZۂFqJdQcX <,^=dج0J洼5mrN<7Fs*aUlLXtmW ]%h*dބ(+I^IruˑFŐ9*mgal^Yq/+?OwFܢgI )3h1^(W_u|Cisqa̔2:3ǼPkw2ᔴ.|oMԚ dig0EϹnosm_'GYNhXm֯<.Yhߟc^,*ޜ}*[qe!37.j<5#L{Îo7JQ.|G/ϻ]Waƙ)%ġZ}13e&F U#@\};˄wV۟r(#F:2)F,Fi|brqF"$cⷫJϫ +9&QQ /+!J1VQ}h&2N}b[a:.0(]2 d`¦#>f˪d^N"Y>#rꑴ$J""ce\<Ӣq5QAwX=~=w[JH9t 54zzaCJY\Bf,7^L{ w/`%"sHXx`kM7L5w1TbjM}zɘYdӪaf#xO%yT”SY#iG֬W֑K|?7k’b4/.)eXPFn֡JEY{&jw7|=*lًB p,<$9*]]}0]C==Yviάb$iqw_vCz$)UoM5<*Z 3by!ܢd./kWf!ǧ:-=xiwם ya/p0X(dUXtsJs^Q[A'F$ jJڶgh'&h'0wCl>Q1r{rUJy`rFRV73K=ܚ$BWGvi+jmXl[f$̪Mż7F1=kRQ0Sԫ#`73yE?}"‚ܺd϶MZ5#W_Dh)Ay ꎌx)CjV΢Ϋl}w>+p`+zUIAixGs!pnH͒FB>;k)FwȻ6AUpG5OOwr.%M`!<=9LF%d̜OHz65 y- dd*(,M#bDϳԧ?͛3{1ӜZ>w >풘ED- ~\|(Ky[Ts=GW?(D4UԲ=þ ynI#j)Zys|q ۳rKB hhY>K%+|?O ўkivE rEщ:TLKʋ-&Uªؙ:,M+:2V s_>D1!z Ƒ[};٨OJ۵S/:U"u/, \8uV?fUP͜[F' +Je߅BH5VY12*>\r!W F gGҚ*s6eRsRI@93<4 :6:=[؈p]2`#mncW2:+1IeP'DthUGڑ0 Ghv&͔f!U7>stream +މxdҮ|D%[R~ac_ʦej涅_8m $b6B1NYA@ɂG539/ؙ)K +;G֞e >9'Xd͉9YkԴZJAq4 9JLzZ.=)->'NEU X43/[G?Y{XQulX۫P{ePfaML;sscEb{;L?똳Gshx x +gwQ[1w#RVLzu)5zdy;8}iZ5~8n}>+뎙e̳[B,+j؁\}CágUqгijCsIw)J.3b+[WHA![P@h@FG]bX܅z՟cAC>hQcTQw2v1,څڿ'g907ԫ`hC X- y.ҩS9} 0"?,rjExXCFry-fq˘h *ye꽁(y7p'2h7-ŠbVz-n[{PS/۱o5G"2fHjbQ۳d_..=]:M.nA5Ņ<^!b͇zsqQ$""Z˪.a\C_֩n qPojo2P&ey%5$n_1lN (I곔9b*l#f!VeGrBZQwgf[CMYvpf 4q.=qr&u5电gJܭR,伜 + jMFRgN}{ ރL)zt#nڄu81$x=n.h!)qk0/n߲`^FYqhdPZJQB-lAZ'gB^1w\Vگ%Zy|UY? [n}o+@=f˯9e[]G7YѰa[Pq"|} u'$*GDt4$.z`0{K}EboI!,2O@Tb~c_/ +` ~=~52\QbXnhjoq>d- k5yZ">jA/hJ9 O5ExKqu]!:Jf;\.Zwº̜= +sc/W_]8>vIXWqA)+,?#wbXyI)(]r/b-`6VYʪ`OMd-I{g^\3y*թcnb%zߑ+A$qO&TN )YYwoY'Dt$ȫ#b:k$BlX'z+ޮ uDD%C +> G X1< GeGn)tgsԾK<#J)w催]\GF!Uw;!KaxPR1XN 9]oa^ ~%<ưlAD4q!6#05PUY; +KZ7p{7`ܹ`bNPGO|J9!JĭUI[v;S܋m^lT QUvW: l0NgiG ~NTAMC4@Qsy5O<7],j+g2Z"06A}fzuze33/UJz%]i$'X%KZ#,T.aStv$PR~-pzE_Ռ[=yhsla̫n*c3:c/j ?_ҁ7d +n*j,?T$Ŗ-hYC.I8e6 FKjM#GR a, M_L(ۓ0I|d8돳#3}IFjS΋+K(ѐ77 cAU"O0&9RR\{FLSq.VI.®kSBϵjH~Emõ#3Ԛ(0Ӥ_}bHyQ!?*^Tj6owy$m=rAvf<=5Qs~JLī%*f֫`ƜGֱ푶l +valw!>9ŨRD ,2UR &`ƜCA'|LFBnԂIЛ VŐ9{rQqQ&xKݛ^R)h@- 5#VCx$7,kuꝜCנuaLL&?4ns!4Ӣ,-^9ڄ3{ފ+c%ӝrm",]4 H4߱cWǐƦ]͎T "( wCΝ9=D9N-OlMOƦQ{1|+h 3~4 ^PƩ}/QZ%eQV) jX@ŃlF'jc>)~{C'ش]E㵷J 7=t91+aV`A^H3a2|@҉uWe]D̲s"p | Ϋ9[X9$xuO4g\~?佌ߋ$=Z)1`r ҪL,Vϫ!"j.擴S zȺɂtF;KYԽ0N/CtwwU9 9;ˆ/{4b"4\4^'->ne++f +޲ָ81@ku3y)Y <*& xM[\jdHL|ΔCڒ - 黛A +[r^ƕ[znO/cpbcrAߛu۰GRj_bf>{qTX@)^a'O-ŌݐR|4|m>sg7Ru^a~a4f5l*UMSЮ +3̓k#]mZ9q(3',q/Ӭ_i|IǤSNEMSB@~xWsH33'K[4 ĥ` *34,c83Siجj}asq8keO[2P;|gu}㺏&ZPP#pP)meNuܼEPZ[Ȣgy)nLp%'R3KPC>Uʋ~]y4k^!fwR5okF\qp>d^^P{mK,쭒PϢR҅EHs+j>Ծ­~0o#R&Ug'.bD-R:! =PQ +SEԒS{h5`\Cڞ¢FW9x/ +4z>*2KJY%nJKo[mK̉^ +. L宴[LyUܴUѱ>!('cHGK[wJZ=&5ݠ% }O +0GF]*f&8ʫ2JNs(njLb}2xӵ̃)ޅ)Ԍzs}rigZij}zf=ag80. u1Kvƹgҍ#yI8$,6x]A' |I疠NnDסѮ)3$cc_B켲֘\^K]сy93xfdǁ4(n^AIΙ's򖍗-_oe<,ba_ͫsfU;nW2̜e ޏƫǒ3i2֮D 4ï٥j% &i-R%*4ʯ)B,Ҭ>Siu)iM/FFe|^R8]C֖\SkV^(hrI"H +.6i~9c^;c#YP {!IX$<,7+a|7#Ug-iUjWv!VyƑ}!uIO)W)sk w%ee_N9ܢƷa)S0:Խ0#jH%kScax7c^A#;`lV_q-g_~;D| +2YOJ?:8ai+̬!J?4"(mɐϥSZ֤u{Rt2TIs\q;[GV}uC,K;Ĭ#1=:8uhn4䝊8,rjZxTrMo:3F=G:*+#?ޏn<}tqǗˏ>_~KBn)kXy?Y|DسgԴb Lr[eБ"̀@&i)`hBAY}ΪB{<@sS#9tDB:aҢh]9[ZVCcK +ߨ +A+\tVI +}u~}ϑ{ ~OPv8Rv'U 䴨;"Ov~ ]+† d.%%퇕G[8̓U_WB퟈2~yM;濹Drv=΁+XоY;pH5l rEveRR{#{bYGL/vZSמuNBO_Y}q~GOy֞Į5+1+[{9N1֯3IƩY{Ai%Θ$19Uu񎃉C]fW~Ȯ[#jiYg/Zӎ&G׉IdU윖5vuτ +E1 lpQǗQ̌j5P_< CRÂZZݪKF';mi#\vVR|#*zqPn_oj8։SVe63pYjyjcǝ[?\|o\v_CunpO,=llyǷếصk~էá#Ӛ?h~m >]eqO5:i r8df.neߦi4g!ϧ*F̪h[{-8o4j#G?'W0[-9'*e:Ż/VW)a5ӈŠ_,>u;/?mcDvwO,ܥ\{9n(xomO/5/jߟT%&'֞wZGGoͻ ;~?yUGiSp_6mv͇[:zF?Ą5*,wwe,>ldJ[g#ȭO֞}էm_o|d3T!0׌+OhGssZAjR]x>}ίBmU,=X/uk#{-w־&u36)(߂zyC?vpviW͟ys\c~yҫf̊73ߵlxI׷G>3̸$5qiw; nzs5tlNKۚQ5ǝ*-36օIcû?% 7}tuiۯ'=}j}WOYJ^ΑIֿ%)otןTGP_&MFjkQI|\tyI?kcTjsRQcԔa/@~y9|ʌK[6&RѵQqY׷1*t_̿v[xprsn߼ +ݪ|?kIuOOӏc6^PMO4]ב ׼쌰z߃5GA~}bJ?^}w}Ew8G+UCon/=nsʻK/[aڑZ_71N427_KYWq_d CooV]71]vc)L뜮Ϗo;/' r?XƣJkINrNz/&W5{^EE7)."(((b+M]ss?n 3bi͜GQw 0]%*x/gh~lh.|~dZ~ +:RU9?o}#n7C;쫡Mqk)]3W[=-ähfKv>ڳV'T<^m#ؐ.rJ3䝮&ue{80Quӂ;S's5iO^Z%%~^)e_L_=~ +qzԭaJN旿&XTlS_N{^ֺbT3EiId4h ݉Yi,VȖ>hiR1+?[l*zяʨ>kZxwgwxQTB#Ӝ(Fkft|SqrꪋhX}Tq3zas gt"99*XWeĨj𛢸OW|_;RBe#Nt{yCJ Sͅހ|N?4r8: ~k7g$\_z8*%q Ok3^jdIZCXp~yf|Rm˵+.).`]n+9Iz{dOOqEw7;2_9q'͘ݑFV;mtU N+E 1F;*T-u iO\3>tc!oEEW;aWu/櫲6j7DO;>m\%}X[ǰ|*]J}=傘a}OR)&aLĕb7̣%̜2L@rK>e1LUIzivSc/GUI(@I)Z@\6eh/x7#J{܇~MA zޝ5H|;NςfOAH7dCyY,&-:W~?Uޕ^r0"phJvVXm<͎ a&ov!BFQ<uDCZҞD(smk`縌S6=L )WQ3RO)9񦚛qݏۗX}C֛[GCn)||Y:zOYX%m!IQs˨ɷ;G#ԗbzBwyhqA`{~Йj9e1f'1 RtWx@J +U8d'm:!Jcmez6a]r_+؉IQo/v>^hyiH2 +pSIBf2_"Bnt`"lQBolbbځn( /И|1sV8U1Nwz +?YvIWmzJnOQd-4 ?2\zW+^K UPf] js뽘 O4GRJX%C$&$kE6:HQ=pKb\ ]Uӹ|ٰi6qQwh-P7}e;-r<"(]l.2'}W?C:ٖ}Kq࡚Y3 +1|650 e=LYİ%nk ˃Eݟkpeg^@'xPgpۃnf1!6*,:~bbxv ׺sЯ +  Q q_I?P)F]NGj)mU7g[Oz;LNtHXywc ̾Q1Ta2=:PR/'<Fg+sp&l|gK>sJ1jSmv}MerJ JPI#Ġ؇]zA76HihƲSϱ5Gjy TZm;Ziq|c ۛbmPSg*~ސ1_3u"}.KKۉQe~Tܮ5dUvH!>V%ǰ 4-Jy5Qs"CEOPTROYS#8Y6t~(*{:* +<"tOJ

7ڴB #Va~N w]BLw{GT7\jǵ7K)/M)^U|PNj4ʞ['gYF;䨕^Bh+649@4Zp~m@c7!:BJqe6-d^Y11΀9H$=߮ FabQhE>lvIazE)4Pbu"軍.\y\Y$MWRUA>v`bc,oiCEmxeZ,wAuGqO&/<&DX1C=$r.߅E""].lVrƧ/4'[ >FGبcG̴Ͱ]+0~MA蝿g= l:4rܕ q6&90 :E~#M.s1OR\ƑZ\Eoyz.6΢%8ge.^7blPpe;Բ΂ILy؝ܼV/`Z6M&xHInX΢c(cif$.#Va_22p\- ]F#Œ"|]Gdݛ<nV|=9hyIj[~uPo3&8}#N|9ZH=+ckEs +fl *&0g3oQJGrN ;U"CA/6zxO &2gU ^kᥬ51w:9 ZGHŏxaAf_F_s +y8C/ -:\߄^hkԑsĭ0Z͠IZ+a9 Ͳe~SRs>=ͮ{X?YjNpKI46KZ,QNjLz?&t2YMC.߆Ϳ)O GIޠ7@/ݾr zO/]]w01,qW8t3Չ*o"~|_31;0%k(ذ 5irRcPb 5(&Xֳxsd2nB c{M I=$ī6LjHUFr%dDBo1xW~~( (S8BGyõfE"(ߕW3p},其P1 ;)y$&b/2^tmP)ׇi4ni==aVR seʚr񁶊6ȐKY/-Ko1R..x J sa SWؚoPx~l| 9H[CS}Xen=炼Я/vpEjB؃l[ 7'ЋٟÜg#!;ب#h,v{f>3U8ʼ`߯1Q~ ?R"4OWslS<}ZHۖp?;bO]{k|nga=ZMBM7^߿ sWku9\ֶ{,)zaYp()awT #} QԔ _0ӢnV_GG !(v'>Rp3^ѩoW0"e']\*{޹=sCЫGwA׮ zv&(5(7B ,FXKwhP0&hn89\Ih@_a^F\ssP;(-P7 @7\E|j9Q~&M}qvѓJԇvVd;Lz,^'\5՝o^=}u}w/ _usCƀ<_|7 "T;7nՂAZcbV;ʿ,wbƄ50 T|ܥ`PgP O{tuЕK>JA8VHG:erR>B݀҄_xt.>/?%\j֝dI!!h1E5\a *Vu VTόHP}!=8S+!(˧ ?= &@ > u]@U T-T#+ {ވG<{y<@o߃^z|N73?>zrwtϫ w@/߃7r?С>jJitkei*X*dMlfϠ.O@:ry +|+{y>p{Y5O2 *:xuQɢ ^fH-[5RǏnﯟ=?<>*֟>pݿy+(bחKG؈]95wK1 ]jH,n  p} +y2ɵk~<}}wAn/d|-fg{:C=ycq 9F+}F?\J >]pGo9' 7ח?qktY`SՈ{Ԉ.6JxRĥOoA|^{uw@o,޾z8Cgo@nO +\$5¯ޟcwgMy%8U[&Fx7|hD|b>y{ |t򟠧7nSk,]d3/tk\g)稈3Z#!1䧝D{Rz(2:*z ;tP;J|ejm&3Y /ZSyڈrűw!17陷qI ))(T30H_)FՕn!,r!"N-<V! o rB3/{? c{O@ɩmHAEޑr +r!4.D- rBD +H5)!0sX$kjQRpo<@tkOЏ.F| z+~5A|^j'ՕT8 4㹪ڽ4U 7 煙B||wE| tݽ~52"B-źxZEP5ƅ喉Q!l~Nj)4 lQɃ)Qj>[=AS.䊸jE,(:>haxEvTմf#4ğ>fl7OxU)z@ʾȸ]z'[_<@?}|A?.&$-*uj[e5 hVpF3-Zx I^Xb\Ɋ p+-"כo/e>HKp +.Gus>S r^֣Ea2)>ZWVm"!&'_|.Dxzrâ@Q .{yJ +MZRa:`Gܼ zX~5ӂ׫NH:QC)E,B1ʸPkkSՍi.jG)5 +<^]jk;X܄Nᚸ0U4؈Xmb&#_m%sWI[elcڡ56RܒWV{Xar_}TOSf%KG]t{M)N r*m}`W6V)T5i.@ +X{ӕAFB'0`{ +JRIN ldƠn`ݢV^`0£B5*iX{U}{zQrpnhO_U~o6Ây+br?EKOU7l~hQ]=;XS2M d73Ǹ):mqA^D%9+`gQ BEՑEG [bnI.gJ]!r̆Ю[}쬙qDw+D.Hƪ]59VW|;^[{s]5ϮS6&+e哽m؉Dy+;L__hǚpZ)jaԏsh4^F|7?ޮs]vMMIUI`g+Z_r@MWPRbY6a~ڔV'{l^TcUsʜjwmV\}Ѣ4A')kȠM 6z]B\`jj=!RdY7м;^w*DޜMjse-C~B%f[\ ZܦX]%d)!sW~>ClU~kuNPN tw 1˸;bnt%c_Sűy82Y'U +Nɺ!G&(i1be .wUm+Y: +PC`wkI(O%7Wj]4mNl;z6u!J҈lՠwlȡ9p|VAMQZ޹XU py?ty?-L@9E& Aq;+`Y.`^sب9j4!1O)*{]rmw*;^{oYPSMbbsg1U}(&YqijscW4 5V\'|y6* F>:IQ5gOc~-Vw59"殌h%ek9E1h"ggy4u9[l*.z#fsg#d(jq:L֮`ygZs,cEAOSxȑ:-t!vlԾ@SSw8US1JFP;y`1VtWA/+%Z`ulMM .EPkŞja}CijGZeo;>qt/@S5T53,o\EtIY{4Ȗ_h>G;ƩYƾN 6loWBx>e['WsQrvP mW4|d- +,"㱭J>Ʀ&T( +k$@$!}CF\ŇY|Byv'Y$A=yyӄ/˝s?% ٔBoGGؕ) +lg'K W=|'QK =%^ k~6sZ`wJTaQq@}6X:&'8mS#-ldX&ys\Syw:Ɵς9ё g2R|e$/6+1idžF5i*lg ld.e=l/lF +|iyJoml)*Ŧ( +03GO/?[{@OGX$S5}%.gg8"(cG)2jM4~إ%k+YRlWr6N)(%11ndo(h;kc]qbҖi\%:qAkDU5g3Tx. ʃw$Ȑ1":I;p?lJ!`n_˂;vRAu'~4tP_]!@l +DЖ}֤ai}0xa`o~N*2dya95 Hisu]g@;5Pd%AbݍβYwG44eA]EއLފe[*5Ii& 6:lm{g= ?~͉C T'C֥=%F$\z%uL-q9Ζ+8-=,rִ/Z'H:oU@N[YCk˙q7`4S40ɩ0!`#8#s|e*roMa9_K\桖\E +}g^mR^>n:bKw"3yB*;g Nmho?0ˎ]qyЮGi}[iAG &Y2rQfj! Bډ*<9M `n };/o ~s`i?E^[P7)+@fLH8ҏK>.>Wg?J<_|2X&dN#ܯ!?dI.no8AvkҒY١"biP +t0="cC`ハCܛi쵪o%NU" +"Y帘Qll6QV W tq{GS x~z7Xр9AJշ&e +vgxKqNQ w,Vbf٤@c&τV u U),#&m}5;_'95tM,=́9ְPV&$fhS}U]jT>SK49,pg,-H?l2\s;ORsfWMt@y ,~{pYW'U{¦%4 kꏾ#%$mKHF)]`x rZ<$ZԟYzĩ$YfE"5ew]|~BeVEm׫ڎɥ{x@|{YP':2NT?[eq~@.2a+Ϧ R)Cʹ)Z*fc5 PӋRbCJ?u bŤc9pazv=j~lQRl+jkQBdR8~YQQFȖWEzjuWy EE4M%oL]dS/%ftvWN7W hX3$eWyqM{ - "$- ^I{=; 5 MRM|ρh3 1O["|x/-w0 +JQ B_E^N>D w920zc+m3i/dXv0DJ@;&)2pP|J2Pbo:hK8IA]ĄZp1"x$X-Lyz3g9U @l W!oS:̓ȝ"y>g+J]b. _)p_hWϕ9[Z;]dZ'V*.Na쀎ZG`m 8|e{*|;}#7Jaߏ|pP,k'/$&'|^l{P4[Ǩ bLng}SKee# +VʈYSF>Ao@FGzh+D#dJ|ܕBn `Ն?#aa2خ`a}*o= ~:s|EcDG)>"e96\kOӑXIɸ^lqiY!ϭ^lu$ ~.d<԰}ܛf gEUrlW[hdN0rrbz7k#!CQႯZQBK_ tQe9՘D $f;hScFany_q0ϲPO*u* d@Y.dmvQTw\tTٶTĘ1oH*PQV%19+ڀl +j6N3au?&925i%~ra Agg*&d*'X}=m":M>%ou7[_*%.V0%^bϥw[/!lPE3z +.f{s%lMS}}YQS]uZ{aCn$AAEӘ4$:ˡ%V-0+rl >tMkVUnQ':!|ޑbbjM#e aLle򓅚gcإ9רQbϟ>pg4+4߫t"6{iuac$4}s>.`<]Obsssu[JNs3ߪ9bVᮘ\qMx$>\m~6KQ?Z>,wz/y4!(%Gh,t,j0JoWڳ_dm3B@lw#'Y%ԏ^ߤy<@I!H_LNӋM_ƽI1_YsRb/ ٦tSkYXT]C-89 fm @4,?+U'z.eOCݟՔ \vOi9_Љ +3O (=2*@9LjN16a4UXJrt \mǭޒ TKY1'ùrLy(c=Uc^q®W>6)&PJ6̶sftf:RRmqw!]pw'XB( !xۙ9|d͝.^hiR/sRf/2j[5_7)lWɪ)x]pW)mj;C57t|^ +}Jņkz1A)>/8UpM cW'LBH YB# j_qE;bRMR/ƭAj윛Ĭko D,p-W Ooq1V1z8\brwLWA G†m򓽷➒r N?Y6Ol_>MlSRS/t^} +8M-wk=Wj:5)!-$0Ւ.1I *h5ǀ vI!oV:nu|2p! vJ暃F-rX_-M!5 1TЯ&(Z{`9r0OF M +a잩z_LUTp=IZ/b#x9\\;t@m_ߜ< jh+U2~s®KkPZsRIc|]W4uvNZ/,D8.wGK,:rS@A,76wȥNs,thsSؤծ?CEck :5Fo!Ee4`Z5{"Mtdq;F:vF@ek|=6 +R ٝ'oOcwkl\Xx25Mʜg: Ͽi@}pQKȕp5 * WRʽŏVz-4]|cH/8Xg<>4\Elf)|WveNXrss^#:mk FAo’1ʙJb^`mʹ7pYBuW_uQw<&{slkbO/le3W8vwLj_o ^?lp~%1so7Im{v`tY5 L] WP. И"OkQYgMJ)(X== +F5oH%{:!5W޵tVܒ1rϋNb.G!Ip{lʗE@K)` [#O3U |f_5Z] 8hsDm:8zcsRkjFN&`K[[=y73=]G՝ ZѶt`S3d4*%E4kl__j=~D_|Ss`I-Wu"6OY!m5w ¼7. "X"o ʍIRkclceO7dm:vSOcҀx5*Tk*[B{u)n)"Suguc6e\r7-J40OU G}_מ)jbS5(j5kёgoHP&/sf9e<:2PzߧMVS}l辺Grh> jm |u-0!x6jWuwL R ":TB+*%܃#t)c#uy $_뒒ve,ǥ˅GIɗl(6K)̽d}`0m1v" 4p2V從ZS"ֆ;7a/õ\#[cq|4]p5 +O؛CA +FhuJ_K[Oε#t`6F!#Ew6-wuJ@?-٣܂=ĜGWZ&̢Խ^ Rl{n!qYV6xԗk]Z2ލTs3{R[k܏ro _e.Q)κ1 +ٙ'~6-$ȁ[MBpu*/80Q~:3h"x b )WB-v3mϿi)~_3C6;7) 7&q;*~W#&|wΣl,Pl4pG +?^c-KKlbP+@nYGRsl|cSt6R?4dwS&ӴYCvLaR_."l㒜SȠ +#| p:\ih#"d@ RI1\CKwLzP9.' 2c *Z..#m}9O {fȏ'ort*ąʖ@]m>׋4\ڒxl]GI. ;kr< +g)d3{RG# +00kٜlrm p }= GDj鉿!^]~=Ι,Fm#ʸG悛ܢ pEw׺!U:x@vg3աձ`\d__,y}5 ~X|^dXȰEljBQ! gsy: _Lϭe,݈h]o*fqSamXV~e/AH=;Lů ;j!٧ ﳬp雓x:AжA8fi]y@ Cb} +PowdB֞I 8ģ%a_[5DH}*Mód\_Dg,NȣU\GA8^5n{ok2^R$&~sZX]s6"(їUQsXsOSG򕕑w9d_k\}>z3ǖc wTS>0 !isWO.^ !tҟ6|ՎЊRnμp:}lk:Z7{qiֶ[<@: o @Xyҁy[¼ +.ϱR/v_/~#;޷W'4)'g Rs(Z;C ܱ9{Z/է瓶\kF[e87*?` .02Q|؜vY|s} m}9ԳbOJ+ +Y#ܳucڵyqYDu R JdҶPS[^~ d6 +ZiZmWS nD6T7J\e4 +0rpyZ%hjl7;򠶞`o(j u)ZSdgd9fHK=Լ?5]QP &)h^,#KhY%,jVU,%F^%:.4 >lܿW[{:Z {LEزg5E=-R0{ A5g0/u3,%?GNsoX{( m_Gq oM3aR̒Zﻦ i6Sk/65RJfv`i .7մJu1[TUCkj HUzo.}Z#9 sKEL@+0i~65[+x=E\,9EV5P_x tny6xW5[, ?mMcsT]%<15Xl/2F +{#%+露oꛛGIdb:3=$:~9ξOFT2 Zowdž^>I)޷t·==k!w :s NkMDɎL\j^) jg!.{ +9x +gy1T1^?CH>C,!}P@C[;oi1Gfqޢc&G@2 :aJ*;M-}\^/䌷Sԕނk ''/H(WWMS6f_L +ed֎.yKR=S%lOI,g=loP~|[wB}2Lwpu.}kѱ$6cr+%S7>MCSp)^5,ۮU ~lΫq3iz۫Wv'8Kՠ{)>>tcQ2(`9簹 ιdQ骏!o-ÈL)3R/O/Y]6~?IZ jYu J4 Ӓ2x2ҥs/ܻkABV6O7Bb3_3pv0Z'rK~F{#e=qNUJNi[O~)UuÈڴ?jwË~}b:rdZ;gCسY*Z.9BNv_՗{1J ؘ!U?tIЩ#8>D(EewۘcHU4 ylbT,$rOC(Q`sZdkw|6]C*uOelL2Z/KwWqzQ9z,~Wm |m&b}vW*D|\Dl?^8+_􊕡L9& d<4!Ժc+ Ӽڏ_\޾A$:Zj ˞=S*^ŵ!>uOX9<'S򮊃4~}1?0Gf*L;X"VI5~3 _f2rmX0!uG-iasODٷv] J.,ԉ{監k۫#Տ2XGEV31KU>cll5ᜣzU0)i[ݙVʈm_ڰ Q N3ςtܮH>2 yo+8 ;DSK/8.-T3.tGUi/zPPa +-AgLP1 2{›ut, YN8)}0McOmz!sk..(ŧ)A2mke'~!N0{Nu煀~LG<_+dI=6*j;z_ΨwVGCh-¢QqR:t,JP̈́J`٣ F!r)K_C7̫͏wrxfٞ^_j~n:1Zav>X3]5!/0r!/wA ;09Sڍ~`^:P>RҼv3(d}o䅦҇Ɔk)2igT$ew>;JZJe0sE(} whWw(MpPV>l7 W ⬫;*B=8w7X[/:{ }p~35e@d?RhYE6ywMkMԣ& /ywi/LPMs}A8ƫޙsn #҃xۍKOy{}HF,FB)*! <>qsQ{z2+@1RXלsQ5BͿ3Ow-%Q'+cNtVGqt𞂓w5K+XOV` +r PW} a8 |wxVJ}O%z BV<}^`ҺNx==piZzǖ)Q I9>l{FsM2 6qz^)l 53WH˭~xɂ_N*Xzk)]~]pksu`Yx_V쐙IX=3 EKQ,+5;rVՁ[wWa^R4>W V\tG˿GH >-am` ^%$wu%U<;cl-O?Lza- "~WC.=0uآm)~5-<46?ZAw%@|xh }7c^JM@7&p'k_$), XnGíJ+289'cgySWٵWLBc6d g- IK]Ohy~%e:5E/pH*&4$#n e +a2N <2*hw R sPPvTzzccZ#}|aPG~wX}9̀׃iF:jk .䧟8;Mcc h<\S_~\ `JG3eٜCC}H!nM%˽S8(/M[o؋θ4Ny󝚒haĻ:qgkb/  K#㽟d}qs2a.:MƆNn*ѯ`-/¾+T\5 0*3Mt A5q[{K^ja" k2socސS2~@%,A}QӒ(clqD{mI86;S25fJrEeG<ޱ5|ސM:4YmOCQ +|ņ'snhΚOC{ ("K(jɮGՊY)Z:Z΋Z!WQRm}M9',#rWC ӊ=(l;8wT?ҋ+.6C^@ L~̀hMY..}ulf⾬бGfL"Є8\ +ytkHq=UVZKə)']f1.,{.lCWl+hƎsSҳӆw}r3DK__妼*>/KXRs# 7j {$ȏht^#ȾVW0qsp 8?[8̀^~>{[: Y2deQg&NS2/~}e0:\)p6IHܭozPn Is c*7$l;Z㷄Egه+~:46Q + /;‚Y$y K}[KSJt#%WOK^|3N=}bv$>217ڹq/!w~ŋo'Fޯ V=n: +#v瑹f2HgZQ0+bH'oQ\sq+s[ڤ_xQ2j_k{ +zY-?窩1箊{uyf)V@ƪ̲`qD\> 8jcgFS*:Ť4 VHJIyYtPgm6@)6@Yo8&iynS%/jI.o"?6*o}撰K[c?&.椥궽 ~k0ˆHg-]t5A{[M8"%eL=؀51KTa R智6jWA.YD`U#FqRH&&xKF+SO9qW„ko=p iP|x}#?U}5-E4~Yr!:/ .3F@#hSSI|o߶<֖{kOKtαّw~U>:Q񛏖(D٣>eQWLshEyw>yРRҜO%tJ۟ѩX84]FTt '3 p¶ iـj<^iWs6rha1< 9,x0Sh1+ x¤_#3T 2*hk>PN-ȹៅ~㕂3rf)Rzh LRķ3{ڈ?2n~}8žm}*yP#詐'$ȥv<[́inq|n@- X5[ %W܂sBS+sRRyƒ蚽8b&3Jq%7m)ve@B8 Xiμr8 "xOJF:+uG&z;s<3u1>*~[K Yu!=R]SbRrs2Yʧs~@<`@'\nyijx9>ieEAyЍͼ C!Vxw}KIuwJCH,2[؜ s@Y "=Džy"Wޡ["B衹 [r>|}- v|sX?o.`j+M 6}S뜀vC^(~Έhɶ7ˎ=&366uּVbm(g_rJqVaO-AbrLKA)hInY0ow-C ;hK~2\z4~eVӪ؇ZZ͎&7Q6)DjAEg*^V J\qms84b 莥9}fP$̼%o62R] jz] +h)RnP'b{^[j[cq?Jigig#\*9{$O Œ K- WGq'E? >sbUu&UxWtOB}S_3gy7RGK +JY?hz"$E;^Z{Kn(*nNuS~SMȃr/,!gtrᓶ7T288fc5U'9H6}uޏ4c&٠ +Gi63\UаgdקyyfO+3ѱ_%6;(>ob 9M[\G5z(\*Tms6@OYAe]'"c\SҀ 6qqCx[ {DSZ)UAh}"L涆Uj~1\rVY[o y`-X䶒R5Ok^5">^)#䆴\WF* 8X&*떠x6סn)ؐ)jKJ-:h%Ye>\R&!g]Bt0uY:oGx#tS)>P_py6AL\D.4޳Vz۪WzsGKutlTPGZǬ#EfhgA +9\\BF0dj .9F{Q>O\nz]ŗf&؋̹!c^G}77?* >}nهS֎GڏMFK=<\jFٻU-7Ngq5kCg֮;=A^~k*5)oQt!^nHEHo/gQ7BPoo}k+0m6*ZJ1'~7zv;U:05R:;C-ܑsJ1;"3ABKpmmRы +.^k}ahg۠I1x;}j/,%{  jxxS/3vD}^"I/lk 0̵qdڎN$2j^Xc9DHpO5`Fye)A[@TR M\wLB! pqUp߸5S\ȣh`lm̶Vk +VqF&ٛO Iם5Qn).>C$--20_ +V1ixW +0S_7f& RbԊQr>N𓕞ĔS{.J̐D]ms/)aV|APz_b|Ϣ+ҘԴ˜§Lb&0itI#,"zIxtd\Som畬#5줾{@VT'Ⱦ_d'f?.Wm?r"wfqS-~P2E*uyQج˔= 6'?]'_^OI2tv|sx 9@R1m enc':scR_8b[{gNh.2%?G0,Ь~ux2p k֋Κr.hř\ӨQ +8Aaq 8iE.5#UҶR7ܓQrr7*J/]rsd頋҉s.n2=g۲sW+#~;φo\41 8Ǘ; *j lc >[z,ģPj1#e6Ø@GSc^0󌃌Y⾄\{sz#X;+-ط8^\us^|8*Tx,FC&DZBl"Sfya2i }監VMwqս5Т.)7Y,o97wbr(biQX+\$2q1Ix^yqu'<݅NvӋoů8661Rd9炒[cŃot nh,,IfVw_ ܱ?@Ω_~OkKѓIn ?L3߄,D@G!>E~Ir9{ js?mϫc+CCG rI+|!نA)cs@.~P?H~5V}6 +_D&/#L& ֱ%$} xv##[?#lc,B<#<G$o褀Pfi"Ui!^Χ4,K-ECWk^8lb ؑ96;z6amY=/dE&1ɛrb~`p̳j0o Q~g)7cka!~0|wmCش˽u{ ĵi+#u)@mu)|gO;Z5 TKM4CcM3%[rVYCX5Ǭ@ŖY5MϷt})LPy-g[MSS.*yZoVaNOfZGi{:&lJ L4)%cո|K#"[ꆼtA/R֞7C%ԬKèS^u,rzN..~.u[B +(!am]#oХ:jmWE x 5MK쳬ְ^U /9g En)!קV8Ry.Bzq4jµ}WݞʼnvIRb;Al8ė8;WgB|2lg\mk- v'8fDAqOӓ''Iz!cWm̊pkz%9|sS6FM3"ͽSQvB?3>Ɇ|}㖲A@ +jM@HڡqleiN +@&T!7G*9u BEAu.rck?ߘt~[N+PԓM|Aq1Na]j!gVxL44!GZ9JDxo+Dx1:tCyP^XoD 0u<$c!w \B@AnJY}ȍ vƌm5V>Xx +SH+ 2u[è1{`B lWI4/f_:X'mcKϚY?ȭ1WZlf6ͯEf5  6Hh˽'i\ + E~5dgbenҏAKCFv(e+JBVICɨڐ uwEksih +sIR)LOT>é37Y%iz,*o歩V5fzev<3м1LJU☥{LQ_n '{d"j0Gs/q6Msԩ1ϴ&jWZI^V_%&L2 &+1oG%z4@dgVs$o4OX$eZ'@{M"o`yhM -sxn=b&%4#xUR7S7٥!# Ӷ(^?g5o9Ȁ-`9'6J2Xi<_֜bKή<pHaҵv))q N 1D w{y3f<̜:`Iu+>[:!5S>Y)hEx|Vs<мlnt@w,EcO߬YfWGL*!l†^cY]EmCCm# 9ʑi,cJD[PPrƚ[]ՑVN92k\eomYm]eᧆYz26UZHsЄԴa6StT!lr@!\Ʈkߴ5ùnLCDsL|joe`iFG-`5Wi:(qɷoWhjښ#tvmshū#LXs*xRRaZeL5u~]j^ ذ:8@nڛm6ޢ9'f.[zҢr|G59.m}SLKˌ,vE.lHsݮe]<-=&kiׁ{~pGjkյе&┤dRGwrZڲ?7t4 Uv*rV(W4:T +0_5w,I[C=~K];I]N*F5GsMj(o6*tB.]10+2ZUkO %/ut膉ٴ7-af:hsK|#G=cwZ6,MK{kd誏S v9掎5]=C[[ެi;6٥ _8UGVdkXmԈYBWt!d̡V6܃v7J EsEǞҰs^߫kc%ߝR4:$[ujڤ fU­f @~ėYiu?G-h`D݅FdmަG;-sWF-G-~wcpoǺL1. 2iAZ'OjYP gE9%MWGX%sbz 3s0ޙT WFEc-zuwiyJ1:ncp٣V]Gஓݽ`]Z:dB7S3tb]aofʹ +'m{}'f,|9^׫A +$O>b^"dy VgkwšlF*k ZC83w<ѥstlM19#(nHbTAA^ki=KG۶n*u&n?e({o{[fdܯw B>B/VULK7pe& 4<]Q^(dHWdM7=jZ,}`3ꫳLCEVȮ]hu wnq-c^S[0.i5Hm󋶖~ְa1ʅV켶 bbosjL=ZvjT-؏B)u6m흫1!3C`ZЂ|l꜔mcׯI F،įꊖM% +x!fm\#JڅY|RZ-WU7ԫ&Gt ?]}҈]5S dfm1k6/jE.YfnƏ*U|L/Q ~(]n3){JIG[ɬ [16GjLf嶳v@Zv;hU/N-}aa/tJ[;㵛\/yf7cj2x\_3)ۣ}-t[;.:bsNz] ?WقMGs9;S6SFddϚZX{SuK&hFE]5ut6G:fM5&NU FD(N5 Hh s('{+gFEaSGY.U[ߺ`K'-j)Z,BE = X}[HGi[P} ћc!֐PO}g& U13n/iVQ}AȭqUH!όsmP\#YNLV TԻ(rEK^Ob~H+J4L?Z3/tk!Tn2C0Kb5aOQz/ 6TSc| THkhe^!?|w9#j;OqnG.=xS6waofeU[[W<ΞM1< ѳrd8LOAK/İ.=xUw~R?EW{3ul`{k;)lGT5^]~RR{'*w9 v{*/Ɠ<˗/A$#kxjB"ᱰ_2u8ڿ9@T- +UFa}B'#hd4O' W5b?@ HJ"]Jp +V-sjH."wk32#D|52CtGj9nq{IswOuE_f]2U]e7%D@j+$&"kWBNLfۏ~!p )?Eܞzue[_~ +ط,Hr<θiH +x?(6r-p52Tn/G1/}"=~_ |7wA~uW@= Zɔ1!p|̟#S~OB?D1 +Ka_$~n_滽z SF(98ж3RŜYx{ЪeRI9ziqfFvq)]?~\0";.~<*f`}-iYvcx&sXt]0wAKb?x=ҧ@30C0¥"RH3ˣ=A9ɟ%:ײ3peiv&'ʟ]zҫ'3gTρSJᔉ,~f7LȚLMDM gVx2c1\^ 3/]͐KɍDa2mJ.nB %<S@7tOSZ B~TZ˾1n{_g@1~Hm"0˅=RF%d.}p`loB&*~x1-5dԎ˞_Cc7KJE/Hx{Ig#nNIJ\1!.]zJPℼΤn{"GyczO8&Dr;{Bb?GͺU()~XUS-_L|BsǤafE;w+:xstJ$"Ot/q€z(uw(w42GyF"ͿVh_Qijp뢡mb&"}Q$)c[P*mpZbNwg=xGǩnAK -?gtHÛ%XՉӈa~Memܯs.Ե gͅUKzWW` +zt9*v|Q۫*̛ `+ۚh`N.Tfdw 8iu=c KH +S>!"2GrAۋZ>4HB" Q 3JjƼ2f +Y-B-N+~U oK.}] 꿙y +ɪfIGV: HKWM{sSEW(9g/#jKp PwS ,8ǯ 6C=vv"#ҵJ9GH~ck0$18-+}W=ͮ3$w')Yq%C=Shբ%3oh>&+ Ur1Ok_X-ST/iBR$A a-~%ȃ"D+4Ў/Ƥ-* K*B sTJ)sP&A x^<ӭͭoP(oWhl Ȋbɥn[ZGL 1g^Zש2V_CѾ/t9.wȚ؟"3~i% +v-\|hǛTQIS嶕_ƁV D8:i$ 8N|x_qpu$d|\{K^d#/}#z4RA>xJJIW|Yhh) z~|5B{t'vY˚RI/G4c/-ef?kD/ؘ%`~e3]zYQWjAy +mOX|IT/!u5"rKKW`Rds}eaGfD06mwqd'UlX 5kEYKz?wU[5?!XEvjkn<`b9h :r{;; +%H;&hNA^Pm#b"bI^gV +2j죋wh,c;x׈5?aׇK +&xZOrёr ")&DMQcΪQQC1:Z6M%eїEAq48l~u.LYaIK%A{Z̡pհ1\Y4g d"k;~ :覎U%@{f,Z0x3g봒E9:r+iaj+9785jo 3|q8B,^BV5 mF1i.f˾YR{ ҤҲjx|_۩vKKCl։\R! JxZ:/zN<x/$TwQ|u|:ع"/2ݙљ5Pu2I/6 v'*?MfX[EIn(K +|>|$U=ˬc4u3p}pkpߙ`1׌i:jRA^,ٺLzh՜MPpv؄q_WڇvD3:P1ϔ{*r榬&vWK]F/J3 +?-=ၺE)p)!Lډ{lX,7| V1 hCfI`KYqW7u xE sB&ЂN]W,?{(C,)0qc+xH)e>}%/;z|olIVm1 +z 0@"6N@#+@35mk.9*f]kO0p;u8*E/Ύ< = 4d_W؄~VDoɻV*tIJ>!;ɰ=6kK:4Tˉ<XFXK˞7bʰM-"f]SgBlqjXA9ahE:jπI>uvɫWs[ĎaKr4GYꜳ xS_kFg\\6]ѢSN^vؾ Uz`2N*J" X:n"9ouv=sM8|J,ٳPdo}0ܵ $|\ڸc** RV9Pϖ+JΧ# do~#fXY_V]S=U,wK;:hHs-;+DsíAd^߯*Nes)nNe^'Vr鉍R*/u/2^l5If\ξ GZW \rO@/>iM}>ѕtWBBoa;2D)vevu)1Ƃ,āa1w=5!kSV"ylOp6_}&.KSs !%~>P#p%&=tf&BfzގFޟ hP)'fLs1aTסc;UvRfOl59 ⏳RJ\xbD$O]E~>#W 2̉Oocu]M2bO ]m*<֡g$EIPuԁ\o,K +ܧMqA/NQҠ5%{c(Ċ9`×wB8s|M ,iOOR#J}6emUeL(^e TDh lekG^pfEH +fNG#vlGf[ˣ6Ԑk@&ٱ6hIRlSt-}JZ,єuod8|p[mC\oUqӦh]V0E&Ȍ˽vIaO> +r *z[qOIYa6dJJ> '{KZAၧF&|U >УM5[ +Dw3<$R@~83Kq9kO?WN.AfAzԀ=}GgY*"dw[o/ AS|dwٛ-)*|YT"ZUI*}g8]^RBLO__ˢ+&Z#g[U!ݾEDi_ 7/rޥ{,kkBaD #-)03O,=-:}ԵU)2mSQ2?UO7FX8),=1oJ^UDwA:wRʨ5 O'@O6."r70@ۑkb @+s ?I7%12EaG?ͦ4Q,ӢR?95q2J[ dC +3XPՁ/ AEڐ@Y\}ɏ7eGٶ.?ɑSYӔ}mu m7![)^54fwk_!-W`Dx'c>EDkpI?5d\:љ`A|×Ș]#y`ml?g (zS +Yy8[whg@<N lC 2>Ae. +r?Nsة苙'#q0Nxz`F1;ZT̶w}HK- .U&y{H3葥tU De!*ʼ=W~]VTy&n\_k_M̪}5){QPiGKQ#"^ۊwZHq.񆁑hWO|ZBV%PuEb_[*&VE1W&em.Q.&en4-">.~sIK8Q@*h=鎙~MGfk{5D}navԘ!xw'nH*u2e{S`W!֦[`6EGr?I |S>֎MEkoo ) :7#RWONNq2"Jk7x}_s*:'Cc( 㣮b<~x" +uU897PzT$߀v?UILwE32\~i¦-ϭSpOwݓ~㯫i񷖥=}uhk\(KVߐw{iGb5q"`o*neC_ger:gzzUYȮoxƦ:IF|fhhI7ifZHؚ$@ߊ컯):2Ew7v)`?'-vZN %q3#ϮW̍oND8|jYI# &̃aD̢0;䏾?|̈;0WTe 6zAᕾ75<3e313nNXa"-m w51:b]r +j3`SVۉ֬{[*hp)N?,Ԩ'Rn>!,#~k.jC>8;2 #6 +YKI1Ń~Rf>S3.*&:ݍΎ'6vA|̕Ґ# 0o%3 9{KQ#G27m5$tIr!"T-=KP؟nCRq[ZĈ&G\跅~\wS1,wm>4 wb&`_'{vV8ӟ93_- Jǧ~wmjҖ0%&rST'XfOvoHfHG\eLuyY7cmٯw]MM<j7|oqlwuls}c3ej}j%p3>eQE6-\{NUU"qSIo2=^Vc|B,Ⱦ-Qq7[z­#" +WfH(\"YwT8@C5F]5~T{"cͤ⹾s |U~a 3oL<@S}ۚ1kOFᩀXUA~T췙ց#+ +B hӔE19ぁ}QV|02Yʪ9nx% +͍P- hA +2֝u򋙕~{KQ^EMubEįkO|fd}hMlϺ; v_BmoA.A{3-9Jxhw3%.%֔yk#﹂]z]Tz Ci䤴G˃Р9e~?IP(1ῌ29kР;j| 10{9:2Nw=XV@elo.1-]nPsœW{ +/Zuw"Jʠx)E<=6bJ MePyȖ WgUIO%5]h/O9l\_i{uk$ڋ?5=u=鏧s^zh_Lgg?ő^ "s:Zn9[[hIx%"˯Q'6l6mYoMS=OB5%6etCP _{OBM xȣrKR$ޒ"B͵gl+JC Gn[E):ybfe 烼 Ǒ%!䈰a;=[xu.giLki"z0fzoJ?'YQRL7pQWWM+=-5OGOs1h[K-Q|gX7xgU :r :dmD 7{9K>,y9aT_SK#sCܐBvT ̯^g/z^鷥O  𠾿-ܧ_4B1B +qwѦ֨ksΦ۫Է;ҸƫkF\30|q4ug3MY+m)xQ藷`ϭ57C؀k\ȫ?U!AAn}W<ٞCֺ2'3rYn!D*3wȴ="0:{cPO1)4*-YǜP.2Բuee=KR , ^ANFEjR)7z][Ѡ]e75Ȕuat{ɖ̇s'2#$Sj׀N=I3jv4MIλӝIȘm=>k~m{Q{6Bm*ѯmNnчYIY(RVN|3#2#P@&5ܷ3ǣ]VPCSF3HM(t ^4櫳p@p2P*Ao6J&v__0NZul@ƝrqlbVQf{DW#-Q;;bX0r䝫ufQ[rx6֖|ik^XWTGoi+;ナ^vIPyCۗw4gW[G{γ&cWK_YUq Egz>[e%)k&m(gA.QxkC&O;jt̉X&+Yp}v+s]%!uBm0S"ls{GI8t 4<fr6 Uy9o㍬VZ$@^ߙ@> Y.fz˼֕cKsݾ[>iKX=My1і|{+ټK:v[m"PK:!b% \MEa"ȆL[`[NP_RV +gEMx6NG +M5;ʨ##,Qrd؎ qp1Kj }9!n(B7Ş;*dE&ܸ9KZ]?5S=ڝRDaҗk"e!%KL? q'ALE,BJ*gF,uW/KV5)zK)i.r&Z L2cWdeާv*l~U4co0m d}ˉ<3)( 瑁 +6Ƣ}AQxg6rgXEDq$.qYجj\|%}Fehͅw82K$-N%+^(lkhO,*:ѪGhU\➁R(`hkMS]ܟ&f"ʢ37.C-!cSTfXȥjtdD[ 6uA5eݝlgDse^&/K?StUCPuY]@kQ)gf"x_OZA } <h͵ +Zc2v%fbE!DGuE 5-x;ڒF,ZQceȈC+.w]\g_N90P +6tЦ +VMÃ}SK㙽t.|vo5g濗R2iMyMҶ ?]WE**64c{-fFJ, +*pF[+RC9`{wΓ v-ܯScE PW!tf"WBc8~d.JC/4Y%*/' Iǣd8 k&0 .eeL?,E96~SiYFm 1*)ŇFdҪ<`_O'6Bў^i,0֞p4]4_`- ZU^m-ebe?֦><4V~7H;uTd!3'̋ ɎjvK-)큥!w' ̺fȨM @ljj2O548T&_T _L侑s$U!G]C;gH6p*ĀH81dܰ:L ˃f _ț +ê(<76' u2ZK^U,f'Y` W+RCu1*+EM]Mbթ.IziHx{yf㦱9_tjgGHl_W.+=TFkQqz>ㅾ)D)?Qˏx$FN ˽/O&Z.3Ԥj1)<ĕqy~ȇRdk˥;Xy6Y_ ԊSPvGcT؞ .2iZ +rgBuAB9w4^uMyz6[v2Ũs +M`w`]kOIT&ȜyQ{~NR :qǎ+<Wt<Aـ>J9hw^W B(xw?C C|SO3؎ +[Y>Z|:W[jtFYaS75UU/|cmLga%tݙZQ`R&{K5?iS~9 cA]zݘ'샏3vˏ +IwIʍ?8^\YzKC]_[_"sCJ\jXå4\rHEhqS_4vٔ[}&>-c]ycY0p` 1So_fuhdoTԦiZwq@I$ tFښnC:BGjU(`cF??L E.IZ 7ήls@/o!rc*FkLo_ӈ֟NW|?w'ee캖@l C/[_x(uMr:O mIc۶gĻ6mL bՖwaf(hkpC>Po/SwQ 7VG%!|ݪO!Oߨ>a=ju7O5,wgv'h|Rוw'rA~)m9F'We.#ccڻ< !]ǬO{r2VgڍkGe_-?o=Ӵ:.]%85 7ί@s_ T_{rNy&!s뜥Zj;ks؆EHD"Mد "JbCXA[}qvW(ښ]uϢn&, $0nCjS5~/wfhuQ#e?qoe sJf1lYFK:deVmxTdyuqwP׊!]m^k#"zHF^v o**FG, dppyLJ6/(Maֿ)g{/њɎO"Ki7pݝN;QSe/l##f1as;a9S/rR/N̪/ncJl4 + [ߝw0>W?̛+C2̭<:3 MDy"n:+>UaE$=3  }kצ~tlxT?F>{]L?r{Zк=Ʃ_[55s3ՓUnLBV'֧[3Z^@w ._ݞw{eBgPdzs ֧Ôܿl!nwr2Koս2DYdS:FWvD#ߒOADHUk/g_vz(͛:=w;3a 3ȫ{Uzu%YUm?!ˢ +!C]iFڞuS 0aVNrL'$|rPNn^}+ҩ`{L1Uƌ,CZƻgjYHTwktBs]Л|} '"UǼ${R`gHYr'j>k_tO : A{AwwHe 0|ҽ9 +ɰ}rJ_EaBjrkcTL9X0WS4csn};x9Ѻ541#{۳11 pm +=>>ҳa3 P&*ܯ&lޤC$ؤYG^ =:&;/bAr垣U9֟zC7(߶Tfc|k{T;93~cgyxlݷ╷[]gӐ0q˲uߘwSn_+KKe6?m; ?3#moz>1(W U3R*ĪYlayKxYk~)`m$ E{V=sֿ+ iyJ㫯vxA11ߒ3K/q Y$WIÄk# tϐnG\wT時_~qtِ\QJ8~dsʡsä%!f*,ϻߚ-Ƨ,u|'l>jq\ 蝎QU͏xoa0s rwWkw3~P6~'Q+\{sS*B?gvjZɟU'o ]:ynM/W{z& e@?XR rLs r=Rp!uU1-hXrBEBKj [N9ͨڞfT4~ [eMlk=4Yά4Iĥ_ ^0(Wҿ. .3ܳB8hw]vՁ_bnLn|_v&PeqDW16'9g +td_byZv|RtW΅9ix΋)PjIu>c'ǥd%bޙW`Rz.:aBJzy9nQ-կEbU`TU1.D,sK E9fsX=Կ91ujAR[RhЋy`lk>9 i7U_Ri~hB#wG;K|])NjeׁELE\%̃\3 ̈́Uܗ^jQ5zvMb82U&Xu> dzG9Q-*,! < +ݓ}=/  .ZRKHm1;,/ӫRbm㎗jGS.8&8a~\?uu&r c "V.|ZK0fCJJ4bPCm8[/7gi [3ܞ̲߷-4Dp拢h%e_"nԯ-$4BDSl81}eeo/j+>۔<94eȲwSA5TE^gٸ>Jdv5ze/ѫ8Fl?Ao'ؑrߓnٞ.lH#:5N,r]S,:;u/C5y-=3袠۳q5Mxl~EqӚo9[]?V}fq11`䊠Z1 yu,2͐XW&h ]t!}_H!D5lsr3Ꚇm\ppKڈ޶o1Σ]CK鐘R̉j<^^ݚf4ekwC{Ao= x[ƆkCr+&rD4 cr=y1&5&mÆ^̉~5z"t,|@΄kS&fήbP9=*HQQ fXMmJ񷣪{m}␂Pd~ZW!%Yxxw>SBDEf%! +.ң{:{"o]1q) ;v=!7Y!\ߚVEVB%m|Ve GFc܀_}Rx^LL6t+SiZ//vksr5 9g}Za}C+4Ȭ\=8 +L?unSz2|r9ml-sZt,QAS99M> x}wckV>­I%踾0WN\$xMY~sQ"'#:r}@Mmؙo$~uN2+Rq{9ےbus=Kԙq"騜1 /+JR^{g +q5f`])8bvlٹ^\&fo|5MNy$]7?8}R}a!\Ɩ\{Ye#|961[@rE Ջئ Vj@;x/nd{Yi-[])3&g +!umkW +jKPMm +^9>$`jk ]%LD%',lEi1y.s-4:-p @3o'uDn)@JXg~~Rvߏc )uOU?ǯau쎘IČ]9m,:u)%[Sfʻ3 쵼ZD(y0/#*%{5"< 5o:ڿ +?P18Li]f- +Jc4=8HW)؄M$9X|u)\V,T®kk3Kk3ȫ' 5fmz6jxFUr8ib`v|TQ~D\SY|fO p5>`[4YV:lG)ژU0P)' s|ذ3O i));ڟ7fNϛLZH}5(ǖ%tHhi{?%=D7h@C۞$z^S5Yxo2\WF*huiv ӫ!3Oڙ#;py%Zدz(TysX>x}28OJ%}AᚧT:g} +į|~ t z~vve@+9>ųH,+2jgV7 +) xYs,BPAzjMj0q2zHɭg [piЃ^jkS fU<2 ov}7,?swM6;s5{ dEԚFk谄^@n#6^CTU9R_! ?zM8XbD儚؀6SkTemS6?xr  +9KpMɎSiEQ ۯ6g#gxپQ )oe'%e?35=lfb&y2 8[R14rp%mNo,R7&YW4,ɐyQ󌎍NH6V)\Q>]8Qjz6'uJYde×O$ Ժo'v&+Jɹ%j{X]{Y-)>ZW^ :FgFuݥ\@η$'Wsl.h:V&%Զ[5zܻ,h0rQ +oj^9G}mLI=e`ˀs*} ՠ}Kgw:aGO,(h"=P:< +b@g\gwP1=%mC:<ȎD\uO6e:Zqb۵$L a9 ޴::~< F.W^A4|镒kc:!)e\ fO2r7߶:LH endstream endobj 48 0 obj <>stream +1 +cԅ"w^UX&7$tT"{Xsa4R|-,اt9Lu7ybѸ;CvMnK!D4 \,3 #5 #W - a5eA"Jz{Lˀ% lLHhh>9)CCΕIuӶo 광LNhz =hObmD* QӚ *@Ы +< ctDrc>zX%&|JH xջ \вq# gz{ǃ:ܽƿ>xne1?oPO=j.2[߁fYJ}kwt(;)Jk }Kۣ//MQQU6>);RbjV4YgTH:,aՌx:JSpZDMn xa>^88԰%ru`鞦UoҊ BsYKD[ c2588)J2'0AL`!9 \_Am +0ywܸ7 t:5G+ÖMȇegM*}0~ dk@뽗|?/viGߝ2^xx1)1#56U,\L8Ln&y;䶨CyY|7Y%8Io7CϽZ6 pޓq%Ezg_)&k +к1^86 +6n,2Qa/ b=ŗԄ=1*L;w5 24IƇu} +r5my:r)퀂%d6;%3&426"D"aBd_+q&}9qn{r޻/h|!?m᣺K i2Xi3Om`KĊXVP}rF$pm՝ tqDvv^vkޙ=iYFK,<4K(xzVƿƛI#I;1ŵ-ޘj_DWy9y]W^^.dea\}۝=*9&%ֻ1%Z]JτVOϪJףjJc@-y9[rb go}JR~i#T1[Z*`%iλڝ'e̎\ kH2RΕ︜](g‚jj7ЏVsLо!7s!nߊ #ؓ01 El=9ᙃgeYzU +Y7Q'aj/cI9l_.MZ؄3Nѯ4LTBτ{PEgjlOc{wdyڇ7!#Dܮ9fSPPCsݳ=< +]4%^\:"LʐIh *{ _Y3r VgZc +zsp^?96Yܜ j5r#2͉kYi6ID-:d| ?o~zg/kL2ggb:fݤZL+kڎB {{HN[8Lx$|a»סR&J;IQpڝvvH %@\TTǁe?z`>oxV둲">j7ӎoU&_g˒~u^d5BZ G:\S緧ϺΣyOFE|gߙaTd`"Fʧ"}rZ ګ?t{hgP7<Žo:}ow0Zz{d_g` > OY0\S }8POA\_o2.q80["rrMLlO<VR.a'IYЭ Z$4=#1pa5+}?IOpq3br_] WքQsA9v-oP4pInuz5>0|Xh8. yn{L#ƴF-,se2f@ǀ.xyknE$OAnLYl}Ly$!߿@4pPq-i5xYQ]DE)F v=%.4wo|=pbgRS^4/etĔ<\LEB\5/*fexc \sBo"r|EHmXi};-4>f5:7fq63nC"*\Ӽ6#)Sgٺ:J 4.(CFհBb5սOWJRSPC܋SQ +툞1:.AAwf6au-ý>rʣpp1c+/P?>!yR!R%ƴһ} +tK{WR6doyQ7_0Q>IsݗzRkFF,tʣ%АYasWPvrWNژ X]QxY>;buX¥F +FہQsHhcz-lO^ywVSj>0f}ؤ"Srkvekv.@o,`~)2.Z!.xeIu; mi#) +,h!q^5lOuVnUql8B/c~mX̴O*q-.{5$ECFgk<.bieW_iE (bdt' H2N=t<ao@= BrvGt_ +[WԈh\d{l㄂ f_֋_a֮rRR M*{`POԚBJ#\ivǀK^L8k8n⓶qE79u6ƻNmuvMRFɝ,f2rUGRVOhs Nj=ts}t^HHU%>nSW{JǁvycGJ 1m 95rpxIgus5/\~xANYRАA& ǫ rBZ.17F{>_sq-P_70 52V.`ڬ-hn|^@e=zeJHwq3{z&ŵ9 ?vf_RP: 5!cl<ีI}߾Dt~)֞ӱ {FfOZGoOpI0A'LԶI\s؄kL\rS\1wP2ØYAg u"P2B}҈ͣ z]1J|?_>cT>Mo~0:}3[e"|Wsx\Fk׉S3Z!9cz> Ht͑cy̭^VruqfUr/8{G^iЎQcx]<Pio=}&] i^j~$]vK$нyyDWWZ m ZEߚUyg +?62Ҟ\DY Rr0WqLzv IDY$74\X,1(bQ- 50^ᳳCRB Nwq>9IGOFkhUߦX9_kl0!#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfp!VwTt EW`MELv &ШXхs/;߄A-3 8 cN;6>]8s¯:ͽzҹ+9\pc.]>wܫWQ]~Rnūsr\_7"g7grEѕ_ʹsrkr8GQ?j'u01s9Y~2c ۔t%0k;u}ԝ;;ױU^=s⅜c9fGG^c)\/{=lv܏:*{XDRDmEFWAhi+-Td10{f=btqE%RVSsI׊%QҺj=- -K"KҸP#n$|BX{QHǀ_[x稕\VtZa)\圠7D U3:afrkDSSF6:icZkDA )U Tn\N`K8 +)5b$4;- Jiu QE3ɇ3~]+ 5`S:q{ +veUq0sri})"ИR !9!`ǵ砢*ZsXA +m~96f&SΙ)=q#X܌YĎ9qwU17r|JRW/ 1JY9Cy91u>aZ6֯"7UɣKNZ؄KYX_U:Bu\K`dziI37ѡQԐjZJcB ֔KXzE  2s #Uabͮs˫ta<[Jc0EO3:\w5sfwXҋN)b-{t+8mȨ1%"F^U?ll{{3*zgT]Ҡq-%r' +> (i)S_\/tlHBOn޳hăCiuQ-KxA5wBlB\=/Aٟ۾ ψqnhwx?A=gl/\<}KHnR˧F{ﻮⴞP֡ +B}W7K}{_߾M,X-6luƊ*K/Di@@,<HޡOؘÅ51_6+j߽J`.d'\K/rAf>&y>GK-!%dÇ LB8+$V>>hm8'ݓg'aU{#0bbcV+f +m +N=:j[GXؘOڅ,;VEm,bDRΌTqNZ3#&fzj9jmڢ%1+f0)_^FGj %Lu-q t Lr@OVMRGAo+i=;kZKc3JA+oLd5w}\PlmC}-ʀ-uc)VFe,OeQӒRBfJnHNF-2Q'mNmk+[jC8FD}E`wV"O8\`/A -% $,FŽ7a0;"KyIM,jIs"0qT3 (Q>c?{ү熌ȳY--;fr$Jciz >"9O&ކ}ƸSWCUĊ jx135q0^徢#/uF-B AG{瞦g')øk[ -'b`PCԴA h%+忙RY8чx:`vbAa&5*z4jjJG ri"d6]4p1 +u2L.y5vԶ,5ݜ h3&dm%drfƣ̴Y%vRrУ,"#"Ƭ5MQkuGC'ܠ% w9'A# hmR$A6lՍa1sQ+U@Mltbώ  101~|]ĉ+zՔT00[Mܜ ~D"4~sw;bdWQ&bV6ng^,Դ"Q4Ł3"q!>3d$yu"EgvDYXhd#S&Bu4+GCuMQR"n)ei+u +ɕ^=уm5=ok |!=M"cUL?o|(oUl K>2Weh K{x&wȀ&FɿlB=MN]O~QH<r$3{}xk=D~gم+=s')_/wU\( Nc\3r6U?00nD/c:!==V|20:mg{▄|R/a㑣ZTě#\ b0h_EYE_[->V aT !T3Hb`c}SMX_kDԨeoO BPvJP]" +@%Si˯ M_8kŭujx5K8,)7``1YD&]u~h!#p:nӂVGCN@f*Q𕵥7j{sa+3_+r֕3}H2A&W'P!4lS6ȅ;[a폼k7g@f!%y,Du4k똣y_"ϫOJd "jy(Vm|3#b~(#6G]kxURۺl:j,>~x5?b}2 +%I[npiyAdگ:0U2'iIðVH4(oST5ֻqQ('kjiufcw'0=FԠ&yhoe췥s1jjT'Dtu~ȶ):҂rd];ؕF NpZY\\FVMV{̽N_Ջ5x$< +;Yu=Zp77Qr.Bl؏:ҋ3Hq lf&ʯaŭ0&zzr\P)ELuO'HɫJ/'A#?fƁe6F Ƶ".d7ϠS0=jf>ri4(jjjQ{us22Oǯj> +C:1= 3lcVzqĜlSS ԴxSV ^ڝQ4G,A{C:'lapE&}^˅s}Ҙ2BO8;NI}g!&o|6& [D]EX&2[KpDt"N5)nֆ 0ٚ Ǹ],鞏-asuu"{Y0'љZیXqVqQw:FQ0nkhO؛cJ~8##nW(Q7457/F݆}Xf6u]]\6!ԠWnM+خIAi(DM:9r!]1fWS-]eyƠal" c5 #Ϻ>3K'kx{Ỷ䒐Q&ܞW-T]Z^Y=o:a@z'bd펋h٨CsoULr|GSQyB'<{ ,-==ȝ,?G +f1qf!dQ5-f?C 6!2Z~a{1xӫoi8AƋ$Sjz\_ï\_N>u4 Ր[1 +FlLS#Ɔ6ޭoH6!*zWK(a#>. +/yA %z0hPH%dY~UD\WWmX#gRJP##3WU&kT"$)m914˶մ숃Kr)aT47ri8(AUl1sq>=ؒ5!T +q\6U&ڝ9 1+Op. >]}W'cE`%Yg4\)?a^5dkż8w*ڄ,dMKIMeRLGQ&!+`2Z.HFlȭ ץ"ZH_R) [Mά>1'|_+4ʭ)fO-ZS1ږ}]'KM=pv]X'yi3y'ic؇}{"S͏jhhS1Z^Z$:yw3vl2YQS6RzzY?)Z{̥ >-y54בuh7[/+=V]K;`|~%!y+ 1-`d[_IJGIFvVH+ T9!>}94BPrPn53~=ikhq9͜X|Ҳ*KZ1ǥUT,O?ɑ>݁cDS{ Kc{x +z r4@&zqVݰ:)&.G!<:"zDpHĮIbndMr4`{sԽ;~8"FL̲wg3܆]:D5ݥƝԭd"$%%``Ł{Z10_'"mA._ge?ٙ,8WH3Z\2=+>a&Տ=)գ1;i>&`ΒOIzG*RUqj~W)T^zOb&YCDTֆ+ξ): "F@/$ U5AvU *![9谆U杠) + ɶQӳ# 'a;ſmOV\^.%!eD n@#FZ<1. 9c{#0%EA37P~V<|oWT(;PgY9G +nRB"tØD-2Y#[MHADQKh + ZXzvAWq!mVsq DψZܙw<=p4On ruuÎS^|6Qifv@% D䈕(dY:"&-oFy cuM@(x{݋E.C[S +̂ j snurAq6+j|f!kTlM3g6fm5M~c]s.M*ۅl ?oaIe!wE EC##d}cK׆Z=ڃZI|Q`@QQ!R▻&X)fVX;.iČ]׆9_^j/886`GnP#e)Fo +Yw&g7&bho.GOBjI@#uS0?;<,PF= jW6X~v=z+lQy" +Ԙg{;lUSU7|j}5~#īn5b2rm5q- |{b$āKqg3s I.eoG!{rP75AC +ZY*F#dѐ^454-Hv\R5؇}l`nA#8Cjې6mfWA1dqV ۥP"d$Z#!mNHxVgP&dU xVӰ N1ۀAtr4#+m#1x{G 記K+n(7‼>'k9U - Q&Ef%իZRjbRht-l!\Hc_ژ]р7'fEY%OΈyO+R_/Aё=hA 6皢e}Oim^CMXh1!}u(W ܡbXHN d}c̬Hf9ܢ FlZɅl&(iY-ӧW|c9\})?mA9Ua= 5dcu`Pgo7(q G4R.j$SK gn.BB&!NoLTFJ^F+(A.@ Ƈ10٧'y&*'fr>%=;Q ,?[DEaڴmk{FæƗ #5}*FAo-uQzz3#82Rw];,dglk^@K>ڡ3ʂZZ~#@^(ܘHd{m,n%<@)Qp0+轥i! 9A4l䌵뫓A14g=8ت1yw6C"kyTq;jA\kd=cgVߝsnٳ}[P+!ṇ|` f~S,9;>aTbm|XyjwXK1X:dAL/qk OZżY9x1 u8] +:51:V%rJFLwBL܅-xŞCija'TR&^-%+cpkWC͈Z@R@ɛC *kA`NHF_rNۻܝ*k}7GYDîsirA(u|p9{5ŭZf0I! G2SS1k~ح;wʏNFŮKuhԫ%{ԬNHp QQg2aŭT`-{K֕q1.kd9=2V!'nMnE0&.>ҐY;#Hk'}R?r>Mŭceas\*b['D ͯW1LQq˶UIV(muȖ~Ksg. m[+d`"zɩ6ݧc:vj'hcUBZ}QS$ژ(: Y.:ʤH]ʠ7N?f +pa3# Y@A0wO#ح? /ebTs]ㄩَEQ [yak^O| h!e^"6㠶!l(7F()3!Do1s|Ԝj +rg{=n&B-B_)5SMCx ٗވ( qtã}j aF!`b7@׼°*Fllݕ'2f|[2%;'EJZ!;9.9n3Ԗ]ƴ- SmWM\\Z/>521^AcY|E̠fvEĩ#fEȧl)EV(E96RP3;slr|^$AMuO ]9glģl)fJ- 0*)HMWSWQ?y=6*~$++lkRY& kZB֚$= +\ݛCȹ^9ӣ#x$X~Y-18v?d/& +>Mݨ}"`,ʄG}BS(@j֫fMV^ 5U"Zkۢ3uo3 p!7 aL,urq9xNx.xJrXR]U7&]HHȾh|,6S">' 3!z! k~C@Lk퍝qKMSTS1H:()d`:ZOINh~mʹ[S"l@+ a#6ݭga{Q#'?ڜCSML8x1{Z[W +"fTػ3?)16q[hb"jp>-#l/*೟ -c3 |sAӜ{@Rt2lfDyQ >#emN#kvhGBV)`">F}:NKmFY$qdߋS*GsAS~oC!.b`SK/B*ӧc<*-(x'9we Dl0*C5vRUC,BA |Woz)ǭǘYjT57CdW ,yy1LkW|(>1( E&N@:܄ӳ) +sPA 9);VB*fa8:n:v r)jӈi1S]sR+v|GKHFh1"y?aڋ66r:C:d;dj|0 qY@CoȰں]}nL0pl3'&{3/ 5YyYdϨYv@΋e^_qb!f3v-Ă56x +5R3_ݶx˃v!ks ~\ֵzXSq4v>6! &dUQ0Wm~ဵe0~ ceZ&mnzW.;$bUQ6 O Hm%[{3K?Ȫ3hX!k𣶸U@OXa 4cs*Z&6mmޔJ6Dm{5B{by!--3A=N}1[Ìԍ~M ΎMGG }jVO*ΈȺW/$zt">'zT+eDuBrXϫB޷Q2:.fs4LY?#%,4-k[E]%oHsݤ{}A|n%p$∉U~:H+hlJB1)U<8+^a}sC[LdfVHCٵ +qO-0facB:FAPC'>OqK#FjjN@KɎ)9a1+T2I&P +-S|Ք_(pWC=fjՉ12!ۋ(!n,AOytUw7U;13*\ \fDwwd=9ԤA7/o'L̊DgON6qg3 ّY>kw^ؙmz 8hzlr ccabr\ڐܕ^N%#;Ay%^jv)!4,8Gpp) zgV;jZbNoF2 lDž졵BրqTgҰa8C~VI#ǝ\sG*^W~]}X]1*X]X(3<ӔMȤ!Q+8YqpLùZbrXKɌieӘ5yK&f:0 ={mbo [c{eY‰ؤ?CA#=FHm_.lP.w.Gwc>N{kw%H{ַ{%aH(A]{{[CkIv b0>6 73o*Ov.'))A-%ceR_5ū"% Z?= -9Ww}q{w{=܍ao ^JwɕþqY$O&I!:0%DBw_WVzOϹ<R_]1@ϱ-'f(a#׃*6*npB:^e"~x曢9y:֯Z5kxA 3A~BԴ>쥕~u4#=NO8%y]x ׬W]'\O6Yu2d ٥IZF`9@7FON0=cQayP-+y!5aA  B="z1mKETaޑ=!=Kx8r-,h`mH)K#^ eƧ⨝K U^#frc[굅=m]n8}ӯ jic4G/LwYދψ 3|4a#ZJkjB0=)! Qtɛ#U7綧mMTD~1$(kꁧQ_'Z=Qujzwhy}lZ7Z=iHPz_)$A_PuapOG1L}hy:k,ٷ ZѰ>[:l{Չ7G77IqIU%i a=ZZX.i.^z{|g{zwѕǖ0gȏ4_/uIr2_7N{{@}TH#ă&&eqq}oUqZMQG{1"9g1p#UrKo5E XlAܭI^oC^ڣw+,ߔw Q{qW?Ts2Qqr-odm zkx{x;Tn.ta.r~t>+?&]_x[q|m7ݸkg3OP|s7c Y`O_BGYY{ۓ*4j ~a-``+=ثI 0Vkܛbh-G~:"5R4Cik8hy\RiO5ů$g^1SK?8^^v !vE,bG'lA}l23#2Qusd-2p}*RZH/ {nί Jnh{ Շы=򔵵b[Ù^>ǞZ^y=EK 7 2`Rn-Ü)<,uSmShVP'6{g+.l W]<3&K|smG/#4򇲓Ck.ajjO%߄nJ1wg7/meTw{ǘ9kaC0$7,)*s#0=Jm*1(9jk+gZzs/ o6`6Iqv3e%o4[" ,RP \ YC6s +u8A wQn͵,8mj)اd~|i}\~pxrmT\호KMmO(Fẫrb-mIMȒ~5dxs͹1m}E?u`MזV +0ǫ#'?Z +r5 ¯jluAWmm%$)1hqə7{ʯIŧPVʡ +k\(5 {B_K|9sju=V/p$]C㞪M-{V{1 m'G=Zy:6۞)yO7NJڞ K--Q_O[z3ms[윭!~ /ktq}YCSڼ30'n}2):NdwR:_Y[JMٵw '?o Iqz7{ 1Ksmڿ,u{, {fKH2Ͽau^,?bg1sk<+KK^ͅm2WzI7.οŜKBgr1lWK]ΗeLMߙ +~Pe|?,}0=s icwu{avUCKچooDbw_6@] G+bG#;pgW#S66׺;+sn +b|EzI''=̯OQGh\C仞oT\U񭱹p߰\RDZe Ɩ&%OP|_9nGE乷cs/@s?[r.= +wS2_S/2>v!?| s㟔?7EW +Lo֖ow3Q܃: Y=$4JL]r<Ǟt<+ܒ|;ol*:]۪[#_/u^њgu!397^b^xsts-q^}sWӲG_k~h^_W^%:Nd)i޿?&< [): 5myA:oASGY !F_'y??YEEr|bn޻Ĥ(^?>w"֒ͽd=]Y@m+?ӊ:N[F8G4U xi-F7+yxܨ?O~-wmp'V>VzVcN-tw?iH٫yvy=SֲCƢ?k w{M%?NW>U1;˓}}}{YwnMY=@ N:O-{:|YxbǃԵ7؏MR\u Ӫ#gO*~yYu)U}>ȖmGY5Uԗ`zZ~xzwuptu ̭#'Խ['%ufU! +snBRxPP~l3P?rЂ?>!cl*g?=(>J>'c仃ԯw6-JPYّ~5)k9+ҥ?i2{R}Gw E IAmm}",;4.ɤ?mY9Z[K~v:5Y_|jT^v=.UBg}~}jEpy-k;!wX{؊:PEN=qU/Ö'3q)˯n V%e(7ߡO؞04McK3,y>mm`{tǂ0/{D"\ʟGEw6ȼwG ױ収 {gg3?`ӄ mugÜ0o}6}S_ST|6b{aJ]0#y}>wb/Hk2u?7.jBťwֶ֧%f^cN[Zc.zAI;eK[>=쇟aq߶]1B7~%\z/ܔKRz|t/p:lQ#,/ I_jjn|sWS+}wk]Ǘ=ycIvu% 3o+V_\|`xR~X~OQw-9 Zr~XDLk0?- +pxqǙ{97?=tqCq n}LN@ +!)꓿}4-s6[S+%e Cez:0]ðQz{fơdmNגܚv醲} XWm(PIA4_;_]yTs05Rz`s~uűx3 QW_=G8Qv~V~(r*.q^%Qŕ/'1Hə=ki^8oIO꺼}sV}_V`.NjCo:?\l/:{.rVXӸ5^os3X@=.9<Ռ:(>+\}Uqj0ڸ8}Yյ^V Uח?g_nlj%+Y?'X7+)ҕQq!nֿl-iRdK܌?gWW`cRwEQ[[ۃԭC}Kҳʹَ3o0kʆ7[9_3R-L( +~6"H|u73s_?w_7!5M9ǫm{O;1a_{_^|2P7G^t}Β G]oTG#z1-6)IԯXKoOnLLUz9g3CJ)|>r71:QǍO~4<㕛_cm +z oPWޗ+?seOBmV̿['D9{P[aT'5>9zGv\(oiV)z7U7}vGɯttL( +u>VWwx'Ԋ=5xakloOqM1 +̥=n--Uȼ#a]%;[Wǫ0UŇ>ЮU»}#P2,"\fbnX'eԜ%ec}yuC͕O/T0|u`qRoR?۩?k_RӔhVe//wLp3Ac6qweo/5Óc3 N.uU\{[tLߔUť䯴 % )wV![lg늏sOBos6F[9˹#+*ρ>2?Gп@V(=Kd^~qCͭdWҲW0z?yUy7ů Yz=7Uiy֞)yÒ}]w&.)/QSu ;uTHڟ?k)< +οT' ǣϿ9HY2wBBX&\8귶f:3uoЖ:5X F$!  A#ù}^#kYFQ1nj oi7 ib"J!vx<]k; ڝ, +u+ʂ.O VηSK-w\,}f +/u] LcNUn#Hz?eϜu׶pV GZ^I^m/㪽+,u!Mu%o#\#睬7ݐa:C 3ͅז9e\j*ؐys>/CD<%?5bu. è]IEGb1eթw(5g>V彔R-SS!ӀQ, ‡suW5Ry=<L<伲&?rGK)2?$E^6g=oI!%N% >짿̯򟬏}X,{#m#`]}:16NZh-}Y4I %Fݖ=fsWlOwRa,P!']tL,Lrzr[NfT KA}`4t[R9&?k]5Z\|h&Xhq\|3ە!wXrKX`%g_G** ,`2r.l9}AwKO6^&I; +bae7yy&k@w>9s؈>ߏKۖ^s.K1L5 >X+xAwcs#U-AF6% BΎ3c/w>ލ +9ILA}hK+(x:Bk]t+?T8.5)ߙ17)cd+'$K6HP!게wcg's]eVxUM) +5J]BG>:x @1=߬Ø(86FL$^nY%U}>éf@ǐ]VTӐ[(聼67dWeޜm.z钓ՈSorb}[q>|sޓa|59.F(Y͐7[D +lJWIq n d"J +x[ûTlU^,x[rնcl"2ۑ! +xF_{5?9쌌胚;{r ;BKYEb;_K>. bc{Б| ^zbkTaR +tm001wҞiԡZԌDb_GȕgEBHz Tcj(8.ŎkYSnSU?rii&QW' 8;rfmOZ`oLaok/oqy =LLq9= 5 x^@~ܕ3!1zk^հN%-ߩZ1a!B3wZ',~ֆ Chk<\F[DžbdU]rc 2j0h5BGهM5J>A˸oIaU<Go;#/rmZFeÇ4_%^z +}c+dzML((buaع_SB֎`yiURa2uP˙qV?Jv9?F?ڇojb FXFZ)ӫf|ͪp6&Φ&hٚN٪-c>h 2ڭk [Fa7\[|rcqV/6gOǥ5xF~&dE}P-Com+VaUNېS3U2$?(XAtz{ً1|ܾ I9v2ψZ߄ y~=*؟nߛn*N%ԡ$|711eaE +ZՖ^j-O/,"F"Vq_]h +,5Z1ۣ읤zT*Rfعngҟ#AkZ &losmNe?2O͍Ƞ''syo8+_K6k"t['d{fd{ U{tܚ][}ܖs߮@r d맡1i?ߝh[ݛeVa=e}7b+& ?wtce! ߿ s4㠀ӿߺx9+?`sMj9y#D58vgՎ. gIvs:37vjIrc/"<}p· S@.ϴVeOVqFh(~tٕY? +zQt8\qn\9QZJ_C»:x($,c*2^{!!z"=Cʂ\3w&z>⃆ {T82*DGG E|mQSѮI:m'p2dĆ ) yR;&遄hiI6 󖄚)!Ļ,!8َ@ TAXtÛ#%bN "?%AN< xؽG҂^DĆʍ;OLtiy礐!a!Bg; +>s=pcruy9~nIJjQ]wU`t$ųρ7'љ }YAs4,,%&ؕxZPz Ko3ҢNrBE>ꔠ0-񊂓q{kSvU؍G BXIja¨HPg΃n; z|2?Oy4(=PnHБs:a==)e!Af}{W~z$oz  -(mKg@/> :u(1˟GejDAOV"ϵ1vOXrʑPPO; zw*[7@N:~)ۀk `[Q1RƉQ^57Z0k ̔_>69(5}`>.= 1sEZ#"m:D@kS#jjі'qꑥg ǡC@ÂAϠȧA/o\vt䟠sݿx +t8''j#.M5=#WFKZv1!A r2>xYP@.]8zzx2 ۷A~t9+WA t1MxɯĄ Z!xK*vjv%2JOSCJDs`NŽ|%л/wn;s6p;Ne${}}&*5Nނ@,c-0e5 $<*7/<q ?ttX @|'|D&;=Y&DΏ+)Y';qؗ{m3Q~Z]FU (sЇ@w/^])O\\:S@<U|K<K[c&{*.<Qqq"KЃkA8{u2󽇠@^+5;|fŞPaJFUM+X@~k/zaSҗFp ]s(!X˧n_߸3zp)t9Gc^>^Ype aSrGWasmV&ns[X>[~${PBP(+O'0WAo=]8}d}}wvRv[УUcݰ?Q1?Ue7kJ▶zI&@%'by + P۠Nq4p7okrV91,LG}CVa2RM +ߎV »mhҍFL) \r[~Q+n'506x'SbdhIYVs!U]L+1 P EhpRsq1pgI gi))HD<_ꆼZDX5M@wOU8/sq|: +b}yUɅ*ʞVaDsMMWW`j01jf6ׇ 955Q#^S]SW]iQWm9FSԖ+z+w&-)!*Do;T@*);,,ְ+ Ze>StTXl䖒4Y`W,@+**]f@۝"n)qjFd}}V 56CYUL鐒mĘ>|R?9fC]*X vmDPGMpxZ!.=4=n-1 v%lqJ7G8JG'b;4<[W]mS +Zᖂ]"GJpQ r!ѦOrR;Eeτgf9ECPN2zcKJ1';'yxO1U8zVtk-ݫUU-sv57,(XU6U,𮙎7 ؇f>5278:-9ߩgܳ zDzr񡙎tk@Q )}2 -;*jΜ^sa^nuoFh +vg*[[Vh.o]`m'P$1FX솾Н֥>7sKXq#|Ng<#NM|DW^搕]9lЃ>cd6G>l\lOiK{D\[ Nj<4tK{FxG@-@Bsʘv,NJ1S5XbUZᮑGpdU 3优)|r\ 8SwZsYHR3t)qIÐkwM}nUG[5uL2\ +REBm*{gLu74=!qqIJ[Kq*^ܣq7ݢ35I9?g['[ +|– $娛Mc^j555b]FHJ*>kwhZ07:gm;چfoz ޔ3ը8]-:.97V{SXql{u'TC'<خa!=JWW]deM4|oZ D|?l+XVT`+]gv4.{s#4h[Ƅ:vf6Bvwv粸VlDXzbYܰ7-5Wy``jbPAz] :B/v!Bc]#YrV&Z,oLy0tOP )R١_r+ kE_vR^lj̽JؑpGIq*&*vouz>*QaWbdAx]K؛,Snfre]ks( ^ 9ǥoJʹ&ߒG,yٵ +tUg{[ٔ4xg8HQUgMTz޾̥:~䧥@0󒹾ejEn˰6JߺQQ?e& +w LO`^XEtiY;TWkN34:|k[-NrqNiƇRm\Y@m2\SMMe ]KTdX*>ڡ:(kUY$J|9ۃf{3u: +6>NJI!+yOF^:'pdm_zC(rhqTSM]S>r;coߟfBсY.{R0Q?fZ6zs]ԋvkS1@|N~0 5!mG)e}0n-:ymuw`b`½)J>UF؟R94ڪB-7h=%wMR7 +X'ʾ횉dGhQkc䌽F΅b#աS}t@%]r_:6v `y&E?~v RZ7brOm8me)]oE+:AG\GVg}0jsF\m֍◖QLԀ0!?jށSp"S`H4o]zkm +lUKtp):|GFm2rDFnaCRg 6_j3UTPs n)tKJJ[AG#c vَ);05t͞ xERe.DZ'@,Ļ&4N,ڝ>4PJ]MYȶ eS9VvlC%yk=z& ߿KMG"#/ +[wUun)ߩA$P#xuZʄjqQL}h' nG LQa6)5Tyx=Ah9h v+EV 1qx~"j: 8uivgAܼbB,R,]gU?k]#&wu\oE(!vc;F0|vkMզ]%D_ Xf=ЈsK<<ҳbTulm1w=R{]5f_JtuU9#joy4V"-P zåR3l4Ow}@ĎsLRa@f6"sՓ:aW]쟋~eO)xۀL9T4*$1*24Į Q?'Yꕑw$N ]RrJ?k! 7) [pcye@.gZ_  /W@ޖY,~'e~;H߄-`C9Iq eOfjrooDWElRr^Dc^jWr;Zܫȥ'ViYRQv1wtD;h`5ch0݂M:$d.5.í%m˱1Ƃ{ØD}+$aGM+JP߶! $e{__sO+l~x b5)gU1k9dRdWjjsQ!gBwD]smʦ &[e+gc `[G+w(}x5VS-rL]ME5믙 j9e̔4&/9Z*~I6=AʲbJJ87o6yOC-pihRBKJ:@0ߝ#=9h乥$hJ{QÏ>=]j}6Rf%-2d{rKϖW>ZA|bծyFQQ٦wƲ@vλ+Nlcߘ䤝װR/DetJ1-#>n׎rR>^QpV .Z/ i[aCoZqʃ%o@xD8e rGu뙕'^96.>c`tSAAm"?y-}Fk~mC+/%A9/VzS^-iww)1D,tӑƎҠAT¢.öǰѶ/aCsm#4XY;-^>?Wr= Gn<~ҥ×JZ*— u?q}g{TsLӍAM{>B{̵ +;:&`ZT?S۽>[#u"k~M(C>ԗ}tc׺KlKQ.W|cSi4`׈J55 H$6__m[RtB9̊]qXYo`ZT}Rsw<0*ңc`ZKU= ]NڒbkpR0hn#'uɺZ>Ւ2rTrWs0JWٛXVfM,j|ZOMsx wk]>ޡbC?m W!7A0(so1[^6ݝbf-tb4~c,}k]/͍ywTW$̤3/-wl1~X ,ܭ$gȩKeAuTrIAi ;bc,n-%A76o#8$b)nލ`߯k|AMW¼[2r~y}HIȏ)~h첒8r;tKdbgm!sc_c ->`V:ңOW9&vNQqϽzxml2Sgkoqؔ0 +{CqMR= +d/3v`b}S|Ɓ9 ௞3R|cr];{*tCƁ,`·Бq\MM}_A|~Vզdx2p<Ʈ?0Z6bg=VyĢ 9ܮkph+ z[NH^Zj}wȈ7zqԵ;?fh 6C5D|݀ot(`1ÈooZ~?z)&I +h@,CNqsSYq|e\e pbݐ@Fm)xP7+jpkx8>:f{e7S=8:O0W,d?((_$ͺyf`+k~ܐ#19-JoKIy\Q8Wp &lyWAo b7ޞκa˿?%μa Xj+|A\BA"|1: _ȹ?zrG/L 92ZOMfrp{a) 򠅎ڒGaVOS_gq1߁~Yъ&lD#\3o7nt!F9n%%GEUk}Y͢s3U -,wam_MaH,.yP~usJMk1; sLX+|[.~i]h.}z7EEy̍ ô-.YQO49j+u"@O9Rܚ`dtQQ(L>Ɋ;7tb#ږ,?aṿL!ׇ\ށx75'\K]#clJ:x]JLpM2a>#䯹 {4!Uy~ݶ\m@_[嘔.`EtYpы*WY}p1fu^T'̿=B[QSgok!(x7#M<Ζ ]$بi*kF>o:fIH37QNN.ȅĘ?6N5?kYd!-4=Ym/~¦j3hJ0VVY3-y;ڊK7ޒ㷆T;/ne3mڲ,6gS89cso.х(e[G|$O~Χ?{W>1I8?`VjnH h>F i}r~@N5?;0rh?Un u}=g!kOD j7s;:RgՐw&ݐȐFka]QZmTt#.T2YCՂܯ^L +Λj2SÄ +G˥!%h*WQ~@tEEHWɯa#gUQ{_l |pH):d6l iFPQKLu0ZOMP͡f6ӭSRZ[ŁhG*"NJɑgWZ_h,#Q0!,asWKNa3rX!з'*GI1RKg:LUӏiСX/ӶӜ2@*! &YX.%!,QS%3~E+xփ-gtXy\K ηô3^=4W+[+jLzOɫ7l6,0 -ފwoq\SIXcŏXRkL~ԒV*$Ȉg7۲PXkS4M(0!>N 1Tb'\sqFڕSm׬ž`VA-)xWG~k?i˻;ߙu˩(k5 +2zΎ=$(e&9WfoKqv܊|iW5肼=)'cǟsolD/O[qk~M?ؼC"2f9.`%ʯƝmHhQs̈lO3 7I-(*oen}{I0kr.7]Y?Ϧz7ͭCА>tQ?zc]JJvMRK]=u 6{J_othKC蚊Ѳ +rs 5ٔ,2DCI11^c}$MT O.0N,%'6 ةdob\#|C#0Rk0"nl FlWT>7=283.vأ]Z6|kTc_|dgޱþN_/]H{ؖzD'~Hj_õdzMKBY$@|jܝfoۈ,ԍTa;:AlzW3=p暪>X:^g]yutg{7R˶$h0.:FN]Yj XhJ_mH{ s]((Z?,tGM1C,1G3S$MWڟ>x?:i@'M\cC&l + AGr^S#ouSL}frnkkC^1oτV5۔Ղ>J12AyaB&@3NiS"tmgfZ\m$:^Qr\rn5E-ۍ~YT+4|}I!B̕qm9^j\\%:啄A\([b_zPfU0P սnbH*n6ϩgZ:k7帄!QBt*1-wǃMr㒬ХnT\PP|v>^IqՑOrtĩv7e]Sz" "icmJr\: Ӈ^#79T*NE9#\ә馴{msL=LdR&W${ +"HcbGETi*`$3g9? ۾:lc + VڎYԿcw-+c`b +l-H=#,憤9+[/D^s79n&oJi +n1/W@l4G'|hCV:'T:c&C4a~iGrw (͖yr5 4Y=՞Zgw }gm#yC +Iݐ>ֵd?xC` n!'&w9<#hz!JDO7I,Vu5:Tuvo˃n1BܣM2Re\lY` =c;_^@z.X_\`oC<ݞ/L_3'߱+o|M/~3"./BFB?rvw&-hh@?:ĨT1[2.xm`_ֿJ,u{^z*/ZD9']|/]mO3mG&,*D夊m5gEU"(Yי?T`1飓L [XBqBh31JoI ~6pH{NQT:/Ǒ?B2O8WD駜Y|{`u=iEjWQ+cԼlImgӫ- 3PV\Ӂ Ukv̏)2 pG/9{]va&p.I=`-@}8|F,#6O +_-ssN.~tt^^NO*8ewV"# [FR3V f 萈Z5 ?m3uXob5l>,^"DTKswE,9&69Y~ 9Ցm<20P%RCC_m2u +JA>lθ' + /ۇZK#GmoMOJLO΁{f/ <S5MEKgqT(۳خ)zUyed3^VI fjݷQGNxYx}c!5 :FGdֱ&4Ҿ%ü^$ǔBGkӚ*%Ajӈ쓋SΞL|56p! }oI:t&mTbw=ys]Vb]{0a96&j<zɧ*Fibe_9uq(F<(=Rv$HYCz~b>4Mk=0vunC p5;蚣U*)f7{wcK=)>xf}9HrL)Ft$lӸ䚈W~B:jk)Vv^ 4{Fc̡XhȽng|mFFǖ6YP7~5nI7 ++83x ޚَq_BJHɡ8{J9pHFdch.GAߞU0TQ1&:<%vA:ٿ2.i@/)Ja s?Ne Z47x_}AQ)=?l-ƩO~ #6fM/䲫1lw..;of<.ъ˙UיN{j 4yM,GFڰ^VA6dMO} ;w(]/|ᓖ&[RM7hm+K0хm9AZh>w5yTw#?I|,xa-Rq-S j(7@TH%F7WK}sb(:#f9dmV`ʧQk{YЕmd]DG>ynx1^f\!ކ߬\V3Cw<υΊ&ơb?炊7FjǁvN@Sa苨wp;&oyudx][/==ͅ; [4>ptOؼC"tYrj`OKۚEe&Y|-8M(w>tꗂ_xxGklo5++=R=? ~ud-2Jo;IzLYo?/z\Dӳ|{*O R~uuhiVNfӰW% e}:ؘNhXz`=zsvXQ0P0"׹U@lܜcv~p߻ИQ`<ޱs(/f2t߀ σ^9*jYgơ] J5]ghXt{_|OY{Z x2I;cЎmsc*: _R?2)hgrgJ_y}= rOOlPqk.Q/JRuT,r6>:x; k u[: +&nQ#_#ZGTȡ<@Jby_WO2W۟HQY_ny+o{e)N)#f$ 52P{BK4яWjqeΉg+[~E 昝Ƞ4q k37zBkuQn""nZxc=KېR$t<Ȩߜe9{@7? 3jKbӖ\6_$~hOݖsJĽA,gsS`žW\R~]fypܱkac1d!j/@Ei> +}Srr+΄oWgp?mD_ܚd29R/NRJ_^;!#f\{]7 @>0ct]OS:YC8|SW +W+ڮ}ӘŸcQ`_:/}#%NFf:oZe̝%\ÎS|d${k髾nWߌ)aCJTgn.+ȁnBI׎kC}ĶFHRkua93S{Ւys~o4g^la"?Ǘ(-q+e*8wP;6:~F'{7mͶ>\>JԳY1d#=?**<{{l{Wt;9=Z-jqCC-8–l솒RgaM,^&;|}QTt|XzEIWwUϿOr_KKxԌ_J eF-5|KM!8ɘ%`kfuk܂y +B@S&<d1MGVpoξ YwtGElQGЩ&VůȌtԬ"6G!Ξ;˼31=9Ty=X{9XvycI|Z'>]6/`.Ɂϋi;*Zá~bFoV{0) V tgS_qE*=WdŖ ++Ex!)^HYu7/NZks\⹰P:y)ՇflY%UZ{[nN ˏ,a$jc7c7*qoG ̖ꋳ۞|6MzWt +Bt<1%W&] W+=4,XޝZD/w ԵidG^h+Ȫ],>xT;\6 ܑ +U`7'i +FM57 `?Ƞ9ax-2Ȋˉ1%*/FՄ-Ei]潙EDS\ˍF"O`pHXJ뒠َx`!wlN^kع'!O#[ZUǷ~1/- MD yVSv`o)@; 6d쐊Xyh +vu l{62M5ύC~k쪹مȿ\o[?2ё- }>P)Qs%v %~Փͦl,ܵ~55 N,~btGFLMGtD3ܑn"Q~)䕱DGZxo}hNݔԽ W=3X 6[_"W_THFk9켂~I=X=.Wm)'B3Ȝ=/l,݉k*ڐ5+,{W!wda߫9}=iGI(.+l6a% (Dy)5۞Wb*sX7fBSXNY`d2Ps5272QOb:xȣXظ.hz_u<2M`f,BPF3{>-0O*{YxwxzsU1G6/%)7Voy蚁Wy= 0 yMN]H`P-M7|kL$ Pa[7*~*꣛p]zЙ>?f[ ˛<פmIKJ{?;_U:KT(EGYe ?X==4$'Ee̬q@OJa +oƵ$hD. Kao4SxfyLME)59y1Zew~p2q54?7x7q8I`'en +lAh]l) ]g56{Z\%zea xwrp[Չ˟uUwKkвq;x-N܁{LN~{xn&{M11U%Te\m]h +MQ gG设9a9\ǯ2^"Vk䑷ĊAq|iӖ-gV^嚚\r.:o,eN.w]+hE?MMIZKg﬏ߍH^nzJˁKq%ExֱS4В-2Ln@]2s`] +1Nn-H:klҫV ~ϯENQIvgZ3=f9)ۨ Qb6h@X )qgD(e\cNXwu}YaU0Q+6ړ}[2Cˮ>ّu¯Sj:15=) *H5Z^O*Fy 7k١9\K$/S'yX ?:wKrxq=GkķZ/͡g֧\}@Ik)L%1֡[tl'@5V%8XaжPr3ޑcw|R|.7_.׽t}~j쪸:ґ-?eaYK헵<5%wj= N\ᾅ͓KKԶUo%#?zC;YBƌ(E\_,J|7iHҎ|dZ&Ho61!ؓyR)tO^ ,98OMQ&cF&*n|Xm +ȑ㘎аmf v`%4̮C3Q{Zr]qiVz3p=@Zx "6N6e謘]A3m/ +L (p&=n%B |Orǡmjz<0('V_\Y=;%~t +?#m2|RR~ 1PFMI쫼dŐ[z^%5eAihݵYlmv3īp4iStG_Eh}#cHhCf`+̥M DX/^fv-|U4̥"7XiW!FD-zVtQA+IE?Zéf@;F`0k^k~_ Fai+H' vwF+<'}M)|q"j^j2zHύ]5sSlЇ&Ae7),%}{'~۔E޳˟B cOݍVqy)>̷!g9gtOOFDdxsJ_T%T>rM{$M)+↤am`S-oT=MO>Iynt(䤗?\;\ByOֳaLYX|h6}X!Bm+tBp03 Hq)?)|$$$ic"6X5yN[H.į0}St:e3Vǐج6)^ٝ37Qu6i{_lK2\<&aYLX]sgl&) )eW6 +EL[I.@"r]unl:D߷;B,xb;C+g*|垕<\_+ 2i[K(cÀ?IF~Å%l(=^ ::IZlL7<4\sü + )-0GZKm_O= +/ BjXsuÌ>AQmd]#G+g [&\_ޑђgpA):Cũ87K5 -xz +IXn{fLMܭ5{oc\VsqS6f4L=BN"'% %w.ŷ]wO]}4*P3ڮ kN &ɕO1qn tOF+U¤@T3'zyg:믹'=A/JbS7,j*؆5yMRqhʗ_M3[ko&܏_ٚe;.Y9^*V~zzۇ2+_֠ytd͝Qi 3κpΝԂPPM$ȡo{=x›p+19鮉q[/gçyH,]/*ཋJ1B*;s_Y| *u:9̯e:Wc?3u]ͷnB.D|^ٜoc<\ᑇ#8Jӑ*BpCJ-T0A!S89?0/ ߝ7&j9Nu%]fZxψ*:7g/Æ$:&)-n&#d{'o6liQ˄F9dƖoy>+;>3ܡDUYnYM'Tyfm[4,c;Q?6*Jm@- w4☞K4v- !JjCFJRR{nYBX BKJ. db?/g,DLa9=ٳ);]j$%D]+гYvwV[z:TϪ> +""$Guĺ +_ +w>2ێD_=| /wvf{V0J^oBA?&jLξZWgFҰ%}YF+Z3rD539s1< ZWy5rIPɁl3֦9!Tcl}Tf-RKstP|&BrzcZNOXNy/0jЬ=9N#ysU#F@8i://ח:3q] H5j~Sܐ Ա:*j]X,^U𜠉f1:rb]d`[zJe=2~LD4zHpޕ+eE& 9MiSA,!&o*՛ +8jDl=aPƭ~oJFuJ\5v|{&]ɞ'M1 x ns[[zq{X_DMCS#[)x궉 k宁yXUu=<'9D`yL S7Q 1r[f.u۫9-3W;N6/dTj,1oI\P)@<ޖI))[&:hba|Kޱe&^LV L쮓aK6P-k= v, +X#bqvνkx]XU72&m/Cǀn;S׬E;/*Ozy&>3n`bB +ZKf? ͖ +U1t&UtHg?ߖS0+Îmk̯㓝28ʲL 8lJx0n撷Lx}[;ktLCwm Uo.29o\$[PRz٫nWi3xi- \ܸfTuwmLbde{6hDG=fZ +YRẔZ]1=ɯU9 QwVQ㥨^b"66g@zso< +lZJo.*z"udu;sq@g醬kaXTaKCg[39Bp>p9_ܲp(%.ʯb45YP琕߹4K^*z9c4ɛu൝rAW> X(X_s|S<ob5iV[|ڮ qkZ1{E9[ Cő>@d@֤Po2¨mhth-;wg]_>S}򎜵ET&(XQZn1{|8`b6tLKCl]w&^ ZD*Z"`~?6`C\ǔ*߱S1;+♨_-[CлmgQ} +jm#5T:'df>k)ٱ Wm;6h[&Q)V9 GLvJr{Qꔄfd+; tX{zVhj:Ke|S#3mKF"i'8I jah"\R1(Łm+j`yM:x$7NW1>$[nd&ôQhfl:7չ?25\ZDg3i|XɜJk%B+ + +~p3!{}3|o7Gw$:MxRDahUΨqϣ36Ě~m;] +u^*@]TK֞C4-| (;pq{gi2O2*;nɣeE H~B$Zc&f;FiBY(`5%uf݆PӚ+3ݭHRKʡ&DP -0eJF/q䵸{8:б\O^vCGkX%FFdž`5<04ejHH&MoaOcuY?jO]yįc6MLwܰ%z_Q4s֞^NL/ `%}ʱzd٩`(E:T?:O>EG9itVpI@.oGwkY⛥ۆaIX?3_F,͌ r{9/e#c>k33~b\3ł"N$q1;TyLC&7.\^&m:bgk a ealWUss|l!,io:UՊN1>/}ǕO^|EE J; (i-ĥ5n(-FF*{5B&^A(KoWo`oeCUW~0&y]ճ1~vz l +\<^mO<P-CТ웛ƎW{^N6ώI1a_PV1֙ed;qGzoE<z' ^/^G[&!g-C?ecLK0srkOazӧO*E~FqεKNĒL=?5kKpWL8[H}wrѱ=qKڪ9& KǃX\f#IP[[3^)eqF|Y%?;UC?VnՑ y(( Q񺦹fkz`l2fTIZ[cֹnXg6Ԍ' +F*&[YSc~* hO#~O r'wLS!9^bJgv3 Z3V$r$|HϐroJuiÓ~O?h?P)*^38*X5v.W *ҳ.%կC"9?CoFQ MW`8 7y?S''!_XYǯ(*VUmSQ3/6=ƚn#շ"2p :{ vx)vxqxIp]ݧe^m_Ҵ6,הF;jJ=aߒJC01ᔔO_RxRU]x_ɵ7nLrËwy)_n~ߦd^sRYkoV6 puxAmC`J7<Ki~38flr>_(7>q qpr+txױ}'lXig]jG$y*Z\s,x Ə)7"jN;#nj|BϜ.1c7 ـ6%H;IؘhY;LJlwk"T,wpA7?ށnvE8D:sG83~p,! d3V8a+%5+J8-q.~.J6io=]PDōLOHǏPh1o|=rFM.2]s""1V,Y%YlY;8lZȦ'7#s68a/AxLobbW5eȽJB}YK4-I\p=\dsR~Rܲ7x*]&WՉBC݅i$m'j+e3}v`͵9#ubF#^J JlG؍a-w#@=W ćLCq2SqQJAOQ7~ `3;8 4ɧ;0=аzN66*inyas&I(VXx 7:&hČPtk`8A>ΝV {MxAE>fYU|XHmoM1K2p3zU|"7)1G2+m2lQG&LrFɖĂ!=1֪̩8=&'[S=.RecL [ZR7+{Ci!ÍQ)٭c~l7|?³5NA z)V>0!FqhS̈́Lsͬ |[K1 Σ77p"C5\wJ{bNҼ뷩dYd5ueRVS0ۺ4>%{\4Y&A?ڑv/oW;YCC=jyvVB=)c\QxRQ;-奫e9[(CwI AgDvA[ Uu im$[c\Z=Q[m,P]ȮJZ^qAYpB(3QXfW(^WëچB[Acթ=3=PGs£6DǐV*{͝)ly1]g^,29ZϫjwS4cmI=~9Gvc=9ʞM[fI=L'l)ЋPak.pq I(uУCHCOxa %iٴM5ǓM&f+ ą\*g+ofGdF3AR vzձv 5죩ꊳ٪J=ߝ4~5LYdi$cAUǘkU&JD|?W>.LSFeb_?vd.953QA#KRBnQ{s:YR U2/!t(}LvےQRSkOL_w{ʜRذoVl 3Cיؤn'M[;u'G%դV{^3SツSn, +U]}`o?D%g=aAYpeAͥ^mesCScuR^QȂzp&GJ9k-scUE!^h7+T +DlMu/U5[j/WJgST,y6,>4eEcKY]n-M5v ]cyGԜeUqIݽ['˙V:r[㔬ᢠ!!}]iR\nVUY112 а ԍ4Ӌ:C=\2uҪ M3sQ[]QgkW-5dTp:qfWZ48@Ef$cÉ.+B>dP8”Ddc%Yf*-bxd]ɶܰ1Rb.lx+/937e:y$wW~lo:lvȠu!e] q*{c)%YI܉Ls=lqjUc`CWեmCul)-eq81?2 \edU/ԯ3<4?NY$Ds9pYZRW} zu>\ +@R?1pQK_$i(Y'ߡs`f&PC5;6ԺT^h9+Cnt7>pVȈS4'͘ʴE 7ygKTe +:ڔSU\}l)6q}hӔRUǓ;dȚnbϪK*/C쎕( eqMGhcC ^l%<0)b% w9C)<2\|/kt_y4'o)"ŭ(c= Ň(_y-6ܜfz0]CkC-1/roGZ,8~| u=RS3%uz*ei}7y#rwֻf,VPx2A/:V[tsU͛ð/ ͩg3X <˵i>|Kzby㞲8wM<"Cͱ0. *'-<9o:^n?^=.lkذ))rNڞh[7j+e1#_LshfTK]iWkNRZM2f{+!*h)Ԩ5!dqWO{vJIRZR&6BI=QsN!6i]ڒO~v$UX<6T0l sBv-t'Xek50v `O]":Q."ܗ=k; ~.%,I;"G+Yϖ;WKq[Cq>v+%ǭwCNTأ1b>^zہubN MEM9T~t4zJ]B;(Yd- 6HK/ܒ6:N7;F/Zj&*#Q \ĺtbb"/g5sO1`㈄,heqMMI6WYsX^klJ qG2з!&Fؘu!gS SslXCLkU*<'$ڙ~? ug,N:1P&|lq4¤H3W. ޼PiXgA -YDgՇ3eC3lӑΧ( 9x-T-ִlga#>@ܶP9:ؿr8t ^W('dTҎƘ=(^u!eK'L1oJW[5I6hG l >is'!˽]:!= lS5 &>H'Tc-ۮHlc<@1V)>ҏrS}ʛW݅3]ؐB\6Sh!W%Tu3cUad ܋~xl +F92h(z8Qwg)͡!.NY^:6FPE[bt$i7K" ؑq>BGȓc HL>QT2¾$m92;N;3WnK]ЏV1jS 4϶s9藬IvHy߹:Y:#'ٔp($*ݕǯ<E5w{8Ȇu Q~rOBg.۫y6qKܖvF + =5I:?Kƙ^sŦ\TVT=).:ޕ@< Tǀj o)q]Y;C} σa endstream endobj 49 0 obj <>stream +)_s:7 'F.LHP; +eu&$9Fh]|1GAPʷؘ!T|{(<\Rz>WVu@/[U_":ܒB +޹s +6E:oK@. M)lQs{bb&hS\-o>ms-bFb;Ʒ27-/lCۭ~'.nGrEݽy#OG֗Dݞ(O~|>]t.r}Q~U~އ e'ݦ$=T%&1]]k*ܖHOLGƋ.rNЋw;T؇Y9jSg&z4)H?2R&bZ_B[֛33t=3tYhG㤄WG:Rʡr$F>֡7S]QZ/[t}kS 蹾dl#7!K0/-:M7ޛ6~oW"]Z+8=)s_H KXZ?sK>PnB\:-!3a&וN/&XPJr] xv TԇJft(G4&>ԕ6VႮ&*#%>.UQQ/얄w\w-):Ppwou⎸Tx-u[ܕ8ߚ}ihjٕ6O7hvx|: +{̶:wﴣC9S.αC|}z}UR,=_T dWGޢ:LvQ ip +%cy ||'$@SřZkJ zd}dS:ۥe?Kga*~/bSOԧ;7 +'7G +8TM5%<Yb֔V1s[1#n|ܝLt*^y~|8V7.}wHx8מ5< ™7ʔWsl9gv(/RIš-J}mGa'ZdV9tM 8}Z|PEXf֖R7]U‡&Nrm tn/ Q=%q|Z+Sma|~[5Qn)w"jԪ wVf֔? +ݔP"tuiOm!)*9Ä+{q>9"UA깑L%&lKy, qmx9DUpw!-xgZ@O79jJ64fGiza-=^{RL0!p#%+ !-2@ tbN/m+p_#*pC=%ѷ]j}5{{"]f/ +KF> +g^6f8m|(#n~}m8y?i+q+gvGE&rϵΤϷ = ;39&2^(酇.RGJRk z~Zd]ڳNשy?p3["L@Q>-f.m{[|®$%/p pSAea .eԠÞp243 v55k#kTÏh\Nrԇ= :H՗$=XyKyW +$DXlն<ϭނhf-v<&yl$;GQSu5, J&p)ߒ#|W\#DŽ*aGjt +x1]x_'Z@ާ#lcu7RNZhȝ'U !LKy:J}1D556"hCKD QF TxP]s/;Ӕj50ݡ:юT&?xL@te2ߘj.l]}Yuo)GU|poc(Hp>j!<|;S6h[FMb&#=57@W:~B\KS roDj??5*@I/Ƈ^n+y¼R5H&][z5Sj%,f1zB̶8I)9(.bWi)BsӾgnwcc.)0qrtdm!guI ?-Rt;զg"{cqI/&Z"o!tcsDO8_ ,-p/ 9X%"+ F|ocWdƉឧjB칖?ZSO)rRGT\@ 9Z`, -iWqAR|dC!J?*- 'gwaF֡:ےq1~m lH0Am7C}b;Aې2;^J?2H =ӟYz >t Er-uRN2F]57ΙPcT%)g.=5rƊK@/e9ϷF;jB0 +fjש+3#=\௓U_gz +V=T_SfW :zALU[0%.xo7:8- +_R Z蹞֝f"RS;KoZ/6O,9ѱFr]hL~!">‰{|kx ?Gm|&.f_m?J}БPC(IwLqNru_AhPrW醤s-yWQ_ZH^c%3t 'J OE'FBl}9)\;ӢSN_X-))P,S0ۣuYqR_RHz$]Nȗ3!dxKfI_ͪiN5':򿜙 0xԧ_x`VOS$:D]r<QF:PRZ} :q !o? ;Sa#K=KNqfu܅r異51g%+귕vHWs@E./%Fԃ?HIqZΡu=Cg bSD)9y07ƹu-IzfgoKh)>ȷkJtxaB\.M\ԉm4;RLag~ 'lKI]3BwGh֡" \Em{5˩7FIaG]CO7<Za^G뀸;T 'f:Or .dNZqe_MЊcU!sk [s{ QZ')3utHG)Az +uVR7sWByg0YB+l\3'j.d9\) rCEn |Zs0LzWF;Ԕc=hCC:i-tng-ud9b,ܱpzcCo)yvܵAD} W@OfDeU+M}]Á qANu)/匐GsMɎ_'6ygdhcDl/-~T&>j< v$cMʷKm*fjGNz:L yhNL hHL7/c3r4"D. CJrEmCTԙd<))9Y,ݟW켃k?K\,r[#}ZӞI`^ƪ3 7gFnaS6P |{ڲߵ@_!#*;yc*zڱ;P`Sl70ݐbGJMYbՙ +] zlt|m1.lz1aBGV !5t lWqd=󩚞cE^򿹶-1x~ Dx%91agMYϬ" s+D]ӹY)%ŬIZU>JJ>Rswe|Ю .m2\{DH!-V)cQE +\Ĝv;LNܗ#0&|KZ3 ;5RĔfB'&ln)4%L,!ͭ/;grޞ=SsMQx tuRF'>΁VjUw笉#/Oi~RD34đ cU"(뼭uYO )ώ5ŀ 3 }Kd$s%!#iT }d>W6,gUP"ok +jXf3Fы $GJ|'erfbVE_VB׹ָ{[7sZ%}ei+s[H>j[Rdņci=E e.RW΋WKY;Cذ355뫑}dWɵKw KoL>vQ~}HwDo鉺z*])yG +*$uoׁo1"mA>xE0'gC$ehV̷K]1~ڃC>F؅:j:dCT2ےXMMH0)K[0ߍ $i)Vz#Qr5G0AbU0r ]oEᱹm\:pzǟə7l3!A\d{W \r߭t9k6#M +(z(`W3zƭd9mA.Go\c䤿&܀WL'36?i}jWC[)<7 X&NJc|Qt]I+xL#-ubN t4 ,ϦZ?ɇJJ2w3.4?s4䉎; s끸XE +NˎBsY i6bb>ruLr6->nW2eKJƚ3=6-2#<7(*@HX*k8/)5xMXߖ@U +.nf ܵa|)L+X17)Q냤]%y}||=B|Xr9U!c-+'ڴY@J9PaBC)ϷE0ceDS/U (kjʎ-Zz U]wa7D=蹈{h!@J֗R1SX ]0q؄4mˆ5Xl ?`%>Rk҃'aM:u!g$+p:Nuҟ*7^"t䷹R +})PPy{S=}<5/g# ~+):W]UiZ^]h0 RB7.@W~x}= `= -AyռB}+&ˁ?y=D(SV-)/vdP?#@_lʡ^{JD("b]u,6.hӍarʑ*nm`#LjsMq{jeJS;뢢˽r] sKxjncTbUAOuٯnWz~ U2B*x ̡9#`Ё +-/~ 3>ε$9o_JyjҲZec +"cwdlXkxk햲.F/v">Mզ<GF?R*j윐a6שN Q^\sܫw. +IbjG#48dOXs8 ؑԥi&_kj|3 sٗAL)1q[ 3-i/Դ +=O[: ?Rs(uL(=luUJM*_# + djQJK]CO&I"Z=Uq±,(dOk/YZR}aS]g/Q檊=}[l%r + ԍRRyԷe>t:cyٚ&CKO~q#Zr<4`S>jz,r6@r5_3o2]KJRGؖrHbBlV}#B~^Gxڒ}>rO 2HxٗrV!(=ٮlf`-iUܕ+\L\R"雚mR<}B/f6-1iܓ$goXizӞ~jd#؈]>\}qߑX3uy+ݨs}I?B3U3V%jNGPpIM)ov%>vhYg&j|n]~T;e]r"/79b^{bÁɅ#Xmӑs&jU<1r-r'Fzᅉ:{9tgޔRA p!$m"\ +p-IF"Q\D+  {:,>2Og)X}]T\̲F*d}x2I=gbfb* rBw֫~?~Y]NPaRu GڥP= }#ꩩ.i.~:gw:@_*p;|"+LŠmI{;Yy*/K `Fh*͵jG=%BX`!kAFAA9t=t0 =P >z/iG93o'MT0wUm)ݮ);1T$K/k_MK?67]LIl잲(e&r'%KG˃e1@96rP +b=T>L5D:Ot3Cnoj=6GJR73!D|S|g--6Nϙi2㡺줭vRˀi+Csuy[U$w5!q\,\GhkA.6-r hœ fw6VL]v{WXJN?5殼$G׵i|K}NM˹U!6p v4F{s:LU Uv8 a&sMgNu b aqRNc&'(ñ^s|rꩉYrYMB!۲_͵WbzB࿊N,)N}\n~5 UAP7Ջ:mcQ>y=e7p^(WԱ U2#6R[jv}Tw ГGӭ/Ԃ Qwf^poTّ3J -|u< K򋙮ǚJh d{ys6)`7V?ӯ{Ң˩ +TNߔSqaB35.f-Dm#:PǁꠟAz dķ|fc#;϶>>}9WQjӕw{#zlҷ)̀hN'%'!nQqB81(51|䮒}jFn +bT»*k ZDt<]^Q<рǫZEL4#&{jEXx@[򑿬KL'1+pD 螑n(\pK=b#&0|j}7Q+v'4L V&6 DW#:2 >p_XbB40i ϢJ v.mV/a& ˿@^dLf U9~nm%a7Cn@XY]׳HװS=g:JK9x'fL [\R!Ul_^d#~0?*V470A btfB ]6 I5}gm*xs,1D01;͏@ߩ%EŒ@m;j[G9fnߎ *p768=ke czj[UOukty}^8 q +9i'3>%I082Ja *8) !ztMΘmp4hN6- >ɹxw[a'NmˠێI~IT\oae1_dN]3vJ?SVP%Bm;U_bNos۶D};RL O%MÜq8^yG>Xyï ɱ)Я\9A-ݞn;6'Y[3q#)7KV_+%(+;ݜ`@I=1\[y8~9 t:h} y6!b 䚴EX}Q} I3X WeQ-;ibB^p) ks +~p֋m.mPB썔XacwltTCi͢3FJKq6*/\_CS9p:FPqKoIg7yr1dVDs{VJy;SM__?-&?R +vf\?<  Έ +W}po|3]gVF-FM"r$d\.`{N?3)ݸI (x9P{bJJ]xP9saMq{=|(k^nSWV.yԬZWS/E3\T48ʄQ#1t e"$Z9g33 acp8R1{fmlR#e\j1巨i_Mo)5Ad0w'H4mar.>5o"VMLyبEP]ƍ +e+}E(Q?sQ{Շ7?Q`A^H-x UNZ JYw=ƥIjՎ}C}w|s~:IHa\T԰F/mM({uwK/6(<-s*"24#V1  ә0[ ˂ +VkP (-qD7y9\`w#Q3GнFWI9 9~,F][3]]zDZw'Ԗ~5o5ձ+ @?C4q{^[@\/ yu5$"Ӗ)MGT,xHkt8;e 0v$HOjtڈԘQ,9^%ׯޜƕf]@mq 7"EAu y j\?~yw|k +o7~UbZBKdP5zwm"F'Դtz1<8Հ +W4siD2loMrks_2p0P/ 5ӈ ~}n',еM^&\C +^!?%4E@+5~,ce +ThDeTs29I@/ {Hq0;*\4_d#Ӷ3!yg5.66j ? Ԑ،BDL}3r >C.OY$ {ʽP'7&(7BXD9ȊBs8}<ϚPPRBJFc*b칇) հA DTt )B܎\^<ҫ2:!s)ZJ G|g^3O,[z>9I(.2_C -R>ܜ .2O5Oٜ& yg+*Ki[E +vJNo^[VzoQY;yo}3s+c5XǍ(}+oz[}5 +򁓷 Nmt֯UCտY~4Piry.Z [*Hvf_HKoѧvm- + -~Zx[Khۨ[K[@ll$ϳVo}\pqo>?ndnK&wh ; ˶``- kzdWqq7rYbeT5I(~Pa6Oŵ*}9V շ]frSAu{BG9x1Q/Hoڞ%Wy]N;8~_(~'*Ni 44ad-\t8,/>5?4|"8j~מ_X̀Ziݘ絙~ž|7 %!@k_$o~S6[4ω1 vr*g}[_duub|.TpڽsjnQ+j}JOm7lX$9Mkc0f\ǁ.|XJGq='1 *Mdł:Җ +Ii4r,;6%yrw6bW#Nj:!mg]#A%]3ZsD=RވJDr1IY)~*R 6quӥr6-ؚDt\d($l<žG[IvmK)ɏI|q:߯eE5l\.v+bFG>85r)6AX_avLri9tw[j8t W4} +ĕYrw1#]#I'q47.aafߑ0*u\ߥ]?kzS+W=۸=C SLB) .v+zN03K6@؞E_9>$WV;I=dcF<؎RdJߞԮ$+?k?Y TU~wmQN{'=1_s==~|2lS6 %D@}g0j1QŖ͟ Iu[Pb-hE2>t]ˎ@gx= 8Q1gYQe6&Ԍr x bTIj%qrc+zNWQ+|}׷feA ?jmnx7~'WY-I_l<ȋYHߘDrqij}_iXEl<=m g*~ys-QrfzwxVZ/n9-#+1a)B(M@sS+۳򍰂٘ <<ھM[a%6nNxȬQD)i0"cZVG `8 *x'p : +)u9 XihOrC܈aC h@0kcj2du5*#{m_1fL+%tBZTxHu~hٔ)GM,P0ׇ0W޴t?-? `3OGCh-BpՅfH ӍlO*ưw>woޚXB9$ R1-L!.9W15q>Ne҈Rqq^fHk_-A1z N4 Z#z.2JG{`zɎw/%!HE:d9HgCRv i.-iᒒFL3Y') l 3Q?mM[ [3zkUK굨OYmX;_7d}Q3z!BO,_[VؘE:7:Re]sHY9K(]~ˎϦغ]'QF "KQ%jRh礁ӛ:'grv-iduG4a-3`#An`aQ)VZ:VgEd,"JI3o +x,BjN_iO w&(宇c L ̦$ЩY3qXI;3e%,lhDˀ$mį:7;,$k=jM, ho[O _uJhi}S ++0gIjX%ߜ"- UvZXIjY耤ԭ qps!e!*j^7~I9iHqeEhWFJox4l^c' 9kB+î?'Y+rzqgYVKA0;lLY6r\R6173 nV쬇ܚ E.@쒌5lhwe+XsJoz>"Er;aCZ(&M2i-. Xղ{⤝h퀫ET會ss(y?#5?$ڽo:.lf=*,c@,E"~a}W2t{Fo-'S W3FZO$'t|Pl04I㟌o| +iϘ 5)#< i'e]. ]=_VGX='H$⌍_NY)5>;;ةX3=BW,Ą>t`R8tEьO8 ^JZėNv -`낳,uscsӯB:'e r.1 ˾1Lp縎 rܴ=+ ,2&6r ŽKʫ0V};g"Zvm OGjDyJh ,j󔦈UI Qݭ͢K0;%LlDDGz{Ӽ9&Zt18:eF^OuiؓsȀn[̭b-z=o YUʻlBfOۅN)YyJo_`+2v.)twzSh ih-I6ILqSH@9ķ=Õ__~5qPق  pE@NG 7=7,j%ykwEg@ɚ;I-5faw㠅P:Hm+e-HG˖c;s9=k +/]!vmbVF( IY咰x9ky:bqݫ|XO~wo}6 +%p$$d ̮`fBVEf-D@C%xb)+dq_sv] D',Cq%='U쮴R͓V7~祇5GbjRm'_;|^lPQ`yq{ O"`<o,F@AkXi 42;m#*ZKT1bzqs~Sx*isob`Qڞw%r~0$Yf5ɻιyTa# ~, +L TmX8ڒqpP}IRIHA10) i@%TI A[^{dQ`Kv])xQTGd)ryh՝I5 uyWyI7+ؚ%ܰ=h2"Vg-u0Y /ϱ ?Sv&,%T9-/8AZ} 쎥wk@"J썄q#sI{ͩa 5% jiM5fDkKY=@v'LN%崍K]} ,澇Jj߭=i8}ZSaEvU(¶)5`[r&fo *P՛S1%! 9 {|`Bjbc' ͘(I)k3#Rs(Ґ.jw@E]'9 #R =ʺ yRY`:"eӜo`gjVa +{1v ILIPJV_ h^665::kvmM!ίF)mE"4G /`OPK>]{Ͻ{.0n#>& bZVgBkkzNG߇CV)y@07&{~ jY t]`D3@bv&'4wu_W~!:'FU¨^LA5u֘RqJx\`PZuyG~fA-@Z뜎-R]7O+%͌K,x8Uqf5ߤ806#/nMt]Ƕtu}]Rʈ+b`R&V;. {.=$@圬qPxQ%S%dMC&MYyBE_Q:Oz~ؚDg-2isCJj-#Y=?=揞'7?z榤YbjPRjjS,.FvR@;ؼ3G4HI3z !a&xI7Ha!ݹ9>M~xAK8};߽ 2i3OO7 Ƙsɔ"*,tOOs 8@U$|w+)07YD[yum5"n tjE} ~IMoL90|Sp6dLV^)ۚj64G^0#VsFfWLΘ17RztIJ\ oê۲S_f2^bsPcgA]. +1>i/o'5e,<8`4%NhKo?AcLؽoPq1J;9;%n=>U;sE^8C MG6Z]KyA=eSy𽘉ޚ:Hhw=k<;)3˲*WUbS/Aa,_GԦ%u`!N];S''EgWyAgu@|Ye3{Vw.fTŘ^HZ䴉RWz< 69 W}йk17gMMx?m.:$4 +rEDC®S*̂#FίB{.ΧaW^6络_潲CLB߷ya'4 $i[)ͣ/% Rf2r+ +{e@"K 7y{xD5ૠN*?Rm ;2@U]zV@ ^Y%קޙfw*>x\[Ө PZVW{L* +6/^,4\L.=z?' Wȅ·DH;eKpť~0gA%xY۟'9Et[c}?'19hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG!CXIWGVб(.O֜G[Q.I'GJrKbQlёbg9ՊCQ۞GNG$^là^'9n''.^|ً\8STtܥ#/]<}/^<{KGh;=w/^_8Bv˧O_:*:{ҹs/;w O蟴|rȱtw}_=v 26ClFqb:Ea||xș3T +n ￝Gj2xIp#}`[/A#.]8sⅿ]_}c_FQcxT=+`*>U*I͆#ݍ0?d?(:"S7KG΁]8sG7яC»ȥg\8})HHa%3=q#'v _Zfv&bFyBipRZ`4&u|t( z\#@u>tmgk޴_=/.ĊTSMBRp~xǰqP\f5lqxjPRJ!"kRk7Ǚ ?ؙG^؜=S26jG\.6c3zFG\M[㘋){105p -?iѳ1'a| +r(0c4bZ\-@F$D߄R s1̙o6^5}?V:2Z>k7&Wu| ;vvm۷9T1%S*FsLJEOa+} +>,0ǂe,`<褉ޝH`#fM8n-g\AHh L_*A#rۯrEXu'޴ʻD xOLhM}A!fba+J;ؘؔ&B]DM){-Op,mJ*Jʴ̥K&M99ش֖jzj2Z7g%򘕏[_ /Z:#&)+i⢢jBED/jxInC<42 4 䕌WczZsHCo kE.i(d;|dUVQsZ:gM1-"77M@ɚȌILNYԔYBO1eF,"%&:rmʂelx!z%U1Čj9EN{C@%c-G?7h,8.!'TrFp|sg ?l>ė%դ0{_tJ-rQ9T /{Wvmi-,%gdf" $ <9 yݼ[>|3;b*fkRl˙ؽ9 ;octӝ5JXi#5! |\.f"Z&eCI +12˸Dv IqB'N.O=YT@MrN0kaófjs҈+;y-j_ 7t)76jkIi;hq=7pλov!'ո-4i?e<2, ;rz +q31f$Gdj:QsP&flbfuλM@Κ)ȝ[ 5?y巓6jG{r{ET))nќGu`W +5^y{4"V#t':‚SXiJ2!&fhgɕ9k=m_oOt~pai| j` ''TQ9G^~Z:VbcEMrAD'=)qz}3?nmw:MK !wgZ'_*TqӺϗ`x)b f%o{/Pe9>gȘIMP# o 0]T zgM!B $̜صYX};}ω;0U 'g +kudq!{$⿮FN!3if)'B,$&Mj<ҡw`8s.>uKI +rn~yn>*,]@12fz{T,N% FPs6C=` V>!efsNz_Fو6:۝9ݸX1=+ia!F*2.>= pywyusm>8 +(i;1g',L"$ZTpV؇F,uMIZ] #^{l4ψABȹFR1yԘ0zJ{Hi؞G;4~Pw<18MZ}+m&?(3g$T' v~]˟!NU]@h(y2^.XG?8Y= 2-&W7#JREdP>8k{42 \ jdDV_981>tCXV" YִjhYPyPV!)&FU$rC!Wmh4cUqI3.xD׷9Z}%t2Y;k@,Huq#6m=L.ɜu~ +4UŠАjcJHcH)P/Kv=B.^ҋQWZv׆ߥ~gc -UӚ"Jzo^ 5s$s +jOAz>;Wt,v~Qzj(4?YΟ'3^/a.kSg;Ϳ1>髭wg,ldIqL|qtckPJ! +jUCf{RםgqQr@E4ƈZ] (דZkKA -R3 3i`nMOvgY^j҂C[5޸@UŘ_[c2imݜfk4aheM9zARZb#C@]f?Sz>/8dҼsN9 k5ZbfFՙ%r ΔޝqIeq(qY;s>PNR66bw/0n!7ic""NpŘRjH{pވPJzjCdPndlL ebqGP}/+,7ՙ%H# f2QٜG"vQF1+Гꀶ1$"*1c0j[PMKZH\ˆE5`@]Ϻ!]3!<!5& UpTWaY[Zy1(㋴B {M[S2 S@we)Ѐ6F_)o#(f#&80O-f!RjVgFhLJ設; +ĕ1J)6q)=L6>/Kpc=4M59eJ;&>9"f)MJ-]VKD)+997‹BmA(];c9[v]wz2m֐wɇsйI\^-F~մֵR,j Ef m&'ry7q2sn&" =:9iRQ`>4A5 < +txO@oI9e@`Kjuh9z' 5&fBJ3,:Σ9o?{nYN.KdPKZDPsU `% V1jn}aVovÏwrq3O>o ,syV 'az.b|nyMD˸d{`0S&0e2P@k;*BoZ1 +9TnhdQN , +N" 4k0(dBi +Կ .g ж Ii) W)5?{<2O(MiIԅ&!9iBDW+~B]jϙE"\(::+{Ϧt@Oٚ@-Ra.Zs;?a`v`N,D踔VF 1ct2~2ta( KogS`=ʣzf= r'ti`Pvyp:~sMM=ncri6 +@3<f8=/ +8˃w:@bz{^`%gm".KKYh +=WM2N"u?e2I'%y@>XokRz+8eM2 ;!'Io׵mk߳Ɇ$;$Nb;13e13K%ILd13s1P%sx[֚kޟ> <vd=zlbS>Bf :!ǡ@'[犞\+RjS +Xd0 +U?xluާj zIh#4׶8٥">%9ᱱ+l>2@zQC&2:m֡ny3:X0U54wEKM@~0}ʪJ۲f]d4BvHg_<*'Đ\s vFi9P`lj!9F.Yg"<*JF?o z!9e|sxwtwkw͐A!fбGjh*hEKݙ"1ެ{ù\#* s{4fOm9W|>8Rp( a~hkƐs)Q"W""T['U*j|deMEzBޞ6x$#~df|jZ]FL 5>`}^Ps@w~#JHYؒұ0~jgb^PWQЖWMʓu;&V4u.4WV+A9}x ښ6o&`l誚Ɛ<;l?9ThNKՑX͡쎤2_Aە.-UsTY|ū<Z[(B*.ѯ.[dMX^g3֣rvmᖱ{R +Lј{y\q}E5쪀Gt-V2/0s1FR}OS^V3N9)#(u$30&(juQ2f +_֙OBε-SwJ٨CpKin3{S/m1 :en. +hN 9þ\+$z5bCgc 0`yRV?x7xWqV&8Ay> z9Z!*m9-OͻP te:htŐdҗ ~MӘ_WTԺ!PЎEjspH*%<҉\熰 og; {;$.^^\72Տ}!Se% W%J\Ox0[UkxMXlYV>[!mLWHUКKd=?8 Cl@>*%[K +*g­RRGA}zdc]7 o]S@ l%h4.0S,# |5aAY1܈+ŊjW[pdj ~c j)OPuඤPN*+՝f!ac*rsUS +k1_(XOօG,*W}p8/3V1]X!u\^)op\HK ,J}r<)vׁ +M*9X$%:elY]RzG*qIx(ew7{K9tKC):joZwM >H%{UcV +s{> V2:, ߅y7{|Pӊ˔4]SrqAȦ@: k:k,3;)y-{bj EBp{2jξ@HܘDe,8G)bL5z +Cr%%R 4̀ΣI\4 +& 2ܘB@ +V5h##Ԁi<v~]MЪpkߏ- +1)n!=e r=&k,kVׇ x?yub\^R2K=jBzّhC~&Z}&qC:\21ummƳLu䖋g}r.ھHN=&DIϓ;ǑϷ\c_Fe1KMSeaA r5Av)w,1 %6y,Zeնy",9io h`RdBt=ښP=|zҧ +e> 2ֺq/'*(kjz(װ'+ lDC䈘ݱDlH> \V<5 +5:ztu~ CLP\*Hİv tF@{tM35{MhW*F;gUi>mMU).S+-kgԤ#cuU!dHlq4hy}]X{dfӂ6ūcmrTl +lBxdn6V0p!.$ʫǶŕG&q 4^㲃f +­.A,Rh' +6§0<* ڃSSa{w%b$bE6kPoNĜ#-:iw*庥?睁ܫ^;7c +!)^5 o#p$ +'f○r:lg,xCQTSRb[_‚үY' +nA3}rzwܻTNsikl0c=s iVD!#FDz耊3LYIn >ErqyVQ@ n%ȣ!s.i0-AE9JOtL4U0`n )-! /jpo~-!ˣ!iCC]UR׶Tfvpa RSRrvԛGW. +諚C*_xbBZ|_N81 ?H~ *.$|f Z +Aklr@E.)YA- BRK/*[:3|+\[ȅ=4hzj +^ ħ4, 7_bw3j,jL~ghSNlUZh:^? +HG 83^ gqYY P1kP+i~ʦc` 7tB hEPYKٷk=U]OW}\2yGеEUО|wltȥ$͗>*eеH٫## g]d*~]&5,e-!3 KhP1E+7 V5>su v'{4챹3p-Txc3eĻchK=Olϼ_eixe.xp,K !#{$/gRJ&Q2Q958t+?V% &1h$8WrЎy65-s.Rw`61{X%T<P6Buk%Tr{lZ*؀y~\0> +* Ub!JOb14}^a@|`WK+?Іc(ïePN5In%9ӧabW-\ah*ҿJ5X4Hhh1Q3Vd" 9 E B͢lTu܀Rrdƃ,ڜ-=jZ: {XV33R˼LKG*ۃ&8xGWp=X}d*?A(}եF +ud=!@Bt?s9*c:<^>ZMA3PbgʞtBt AeyCX[D/tkm OU/=*>)`?zd *`6; fg >9) P:\GP ᑈxUE}P[QJqfBTg1N܀jkeJN@ ,le@O t$2>ͧ$42y~2q_:_ aUV%9e*ga:P[Z~PQQyh k+\2|Lkg~ ?*8cfc|\h=1ӐAaįx^ܥamJ.7r4>ɡF@͜p. jţzV3^jROGGLⱉF&0%|u*fn +TTUN *Gڔ7c$Jo !o Y6 6֫aUݾ:ee5c&hXLxzxfWt +hrxDHG}¦ea#-YXԯCk( o0>gl VuVk]Fգc&2 x0{?oSQNxd!aa}yuX^6 ?## >V ?>5CC=J6lLTzl;WԅA̓Bj2̧$%\Q6t٥"C!bؖ9c42Ia3Nm iX8<.p`k +s{L_ HOsKyվ_v "ё˴hccuGhbXW +X^pn)r@G@-n82  ;co>g\T5 9Lo&ڣ3lrڭaC5d_M/] =y4e:5¦@?Gݝ'&<в @ +y^9 T@kkcmOh F>vf&~ma(&% MsX:Oϥ _wdGO3##}zbUFt1y{lիijZֈ^p]>_98hUQK^Ce$\COYk +S)9o{Rն c[xv<^?DA}ES*Ս{M2.PA tXpb$,`"?LW԰VQs@wogQoXe 3|ZGw/Csо )T(Ye s !j xjt/RVSJl@{:.<Ҡ'A^N,š +ثfA8Yu<蹬зcCy$ex#υ0sTrMsU@XA؜.Z&gɅ&QPރrW%酼0я+lFxV +7h%vTы|r<4Rr€\OIˇ֙B:p}rH[UWu'+,ShABъp5%r($M@(Q1:䑡aدkl`c0N~og +ê+2'ƣ hMjRqSzL2)qUPiNMEe\;bc>2+@ʫ݆ikh ˛ B\aP֦nvY֩mS8ރ>\vl+>ʡP 5> j,QDC( +"nc*;ؤZГ |P>6sXG+FVY zS hKHG;Iɶbs +il}뗳Jm䄃q+$퉑 + QsrjU. 7r8 s"rHTL8s6N 9ӣd.Sҷ(9l_Ou,SRR16/Įcm=q-wH9'ZD-t%IbS.|̉V,Ϛ567z(6OaF&=x:FLslezǺ0U 2BWO -&իtB~x&s g]?A A9\ͧT"? ^5tdsUVǀ/ +A d{.ޯ DBu' +JxġHGς€הu $ȑlr8;N'q"?kO4tC+4::ӎ40X*W15ky#%jҭJx x^mBF*¯:Bx]1,Umnȅ^ *ɫN]譣Ъҭ lB̥CrvJ, +T6+= +ǯgB5Ґx +а:1ɀjO, 5֓ |ԧ!zԤ̀ӐfJ98h +`8N@9;cȧۣ7}Ǯrk߳b쌔,ٛs)ĥT|^-%$yR1J] l2Q +h~ᑰ`Eju:ALv- h,<=}{;z`l ԵKFL9D:R~6d_ڛ/zC +(ILO̰ʙyRWJ<lTRrl *mȾӟf'#f8˞y2)dw.L`=fgN ilsNqCA69Lzױ~-1&)}Ⓜ +Z;57GMuI-dBG}<94,*4/}煡= o{0&26:Gu y63ah \瑱>z\xxtPfB{h kq !(#d'!gLYzw ›;9Ȁ^DzEB>»ȁy8pͳ] "{;#ŷaKwN96(@A51:sizGJ+:~%XOG@~ yg^"&{Jtk)TH(l"-#gb. +=H[c;VKT<)!(c>qÃqs57lA x2HL.'0/vG͑\ 4>)e `nܵ93eJO΄@=yu\0CFyU5%.o ^Tk#אB&o]Jr!}KF/pc ˸~z$x8GN4ެ`^)٠w! U/S& x"D̶ c|4h2p 4}m +HWQ x* +w`$o,;q!^B -`=@04. +/ ^,#XiMOc =pk>BUӁ8l_fm^K~7䟓2N>55esN=\ZH%A5&}sX՚uܞ{` :} +yT m>쳔sDzwMmW{w,ChO +(ȹ9ԐrОaģn䃽\m#arS@"l24uÊYm=t`ltb"nm^#n b#Ap/NPG7Kos.ϊc%mh-f= zs|sd#N_/eL6O½pk +=zږq~ktk`o"8ݳMڽT9Zk]+ 1\k7y1<CBaOZ<0n9%)Mxϯ_575\6fnэp垧'({C'{CǦ_?3{P  d: ̱~3Sz芺!Iƛ/_w](m>/|'kh(u؁>+q2us0~aǿtstw>Ȓ5o jԫޢku g{+RGz}9cc9.: aIzGZ[{8.\Cm=3_\5EJSUCn\'&ld[cŠ/*+!eUKew򮬵mL8;ZPݒw>CjyT<9ugZW9e]ܩa@^dM>jV?肥u[X|i(-IZm?Lbm ~Wf^(9@|"'coXV:1 wr~|ȼ]ro>۔^M0VaWu*W$Zʿt"-ŗo +k.(e|ouwj»'9QKg,=7 %uA'2Ro'+0;ÂArJ{%m]Oں3䟧1NR@=L m#0w\Q7q#}%VKew Eru5I?7~Wvܚ{aKMqKDG2[7r5ftrϑS$u}߆Aoꁿ|5}yw(뒹93gъ{u3}StMݐtZUݲ0kYeF/rsYpy-IzgKo&ɑ_~6%$[m{3ʔSo _N X1Qaji,z0M KQzJ]ЃS/课g47o[|k^ѭklFUfseR }A&K}{ _ޒVg Ak]Ewk3ڦ˫}IF:Y _o#aWtSb_k_WV_Pr yFG~ίkxuuхNcu#?딦༩u{Ywwu!\$+1RĿ_ij>^9,{aS.u`h(3VI6uQ"~%%8JH<5όjE6s|c=ѝyiE/-?N1wo-Vݚ^s >)ElunYJr6t3EW4E2R`Yg7:~Tg]ֿ/P=r4;wg_9i0~Ȼ:L8JιH^65*W ƦߍDyg_̄{^`3$%3s#űxxo㴴NW䷟,>U'~9M|;Q/D).>Vk݅ia&}ы~ɒ0!n2?AŊsʬ3}?JW~~^j;#A;7nZ-c<؍-N AH z)n`F v}־j>jo9;{ ͩ% `L\B?'_ %u(\Q}JSq~+;=76OMWt+z'aG 1J:xlFNVJIOK-UՄ?/LnAvo飭~#CKECSV=Ck#缘t0/<ѷd7f_|Ysy ?7c~lĜ:FU[t^YyfU}0J#!W\p蘡@nٟ,~hhV^s(kSwFl9Aq1;c~Գ[=9iQjn͸\t=66\ED^X:G=T/NIC۟s\t±ug` ċ'_~;oj>khH9=M11CO|g>&%^긯3~9\&LV&m gRME2L. P/aj1'eci]sYsso]'5V?W;5`f&*}Aܿu?Y:[^~Lom2X%b>X,މ|Vzu򝪦B3wO5~|L5˖=!~(yqϷGKsK~ɾ$>ҏoYՐyZ>眬*+e^O=UolFF&/: +VpIVC/gVro 0"~ĥi*jmmM Ry1_mt_l:/miÉҧ_藟ߞZU|?BIuK߬]׼O=~q:,_:KN1F(IEwV]twO֋-LM#7t&_s rLH)dd1lkD[?7^7(d}?AM{[#$iu/HK]7_IY? 1Ń{y>qz83q43Izf2m 񅲦"T2>YZRRԹAn c_{,EVytY\iEC$m3Z;0Ƚ[Yǧ@zyl/SrvAlӸ>5ON>Q~M)ڼKCg|џv~F2aE䫟mvc2J+;'ꊧg^KdHucE^W3w_t|Ez.鬾9X {(3kOvLRƦN"#6nXrj꒿_m/9MG|7_[:GIC=WϽڙ}skzO5U]MG!fw.쳍]-lal܃Fm[zWᕝ@.Znڊn +ެ2Ƹig6KdGBL۝(}1[ecεCлj%~ӝ2be 9~[\ |0(.:/@nb.SZ$bqJĹX[n_p6pr lmy9{^};=澶>4Շxm.gFO'-;H!Fl }Iߐ~zw=/>~yIvnYqN&Ja17ua(`wF8Iq 'e.+qs>s,`U>}eUVݜ%PP,{fu0+w{6ËFהuiw 8ynҪ N[PSY!ůs] +.ɱUe+@ |'G|R~`R[ýQ8ޟZ`op8͝f_f%ng l`ѵA 83dP8N%JZr~KK57'xy 25ꕥ/xʙt I?, +|9zzo5}2@,?z./¶(R46HaF%CI;e[1Aܙ9r!ׇ}ǟr_~k}}` t) +'7X$>zpsz{y8ŅS%u ?lQю%s8ewVy:ρ[zngdѶ磶QE7-vg1Q6 ok6Mp̢#-}qG课d>K?^aݲ*Bp j?IIXT5^|DP/MRV-yB,>yg/:yfܪ}iM:yf?J@hD1Jڛ+O *MVӼ5+ n;.g^Ե'S5%ԉ~ZfZ)}\S j+f9rwl] <d^3ܲqvXcXy䔭a«!J7.ߒڈOqC~]Z$FmOU 嚢_ +oч\t@ZFZ Z3~1tߴ͊q}gYEMh #]Eғweg ? 07{ͣWa/TYwm7֦ѫBc#lrkkvY,go1JoW"y#(yX}aXa z/1T$"f{ +?K.i"[K[U(;!LWnO u엛*f[y2rO9#촊vy4۬j]j.Tsgk m&2|i7MM7 a*|`thu1nm}K[ch悛; /$Zp] 򆾡.؍~bc7 Wղ؆jV7;QZ}`[#;ub ;2؝ڗ$ϲlYKɱ>jgBP҂HiwMS,ewqum[]RV̱K.XlrI%;U2XЖevk[9 +OY*"^fF*0-](X a["*=H86vݺKJqww $X>G\ =}G23砢]2rZU?nLЃ/u(k*}9|6?V!Kp%詒^T 16[ҟζߝe ~.K-VE茙ފ-5nWuQ1cpe]c[փm vJ/rr~GF~?VHk@w;yw7ۗ`>3r6Q2)?@7NU#Nr whҋ?> ?0cTL:I(ã! 7d<>JuVԅ [0Ӧ;Yb>U7zYq518,4hGQ9!'g{{$#PG7Ay_i_Yl%Xxmqum 1vt@.OVe#ĝnD~dy?|Vx(|Qȧo^ݽpQJRC{:h2e$A-2b_Vt}Ppib: }>)7Mg?#NDx|.ILfj1n9PQN=6]C _n` j).8Y7:6/'RqE?ry~\nG%<ce,@JJSD9FN: WٕĬW +n-]V\p:ӻ#_?EE}> :EL; xGR_Eʎ9ޏw(98O^c.f:>@PB q_vqzynvIRrkvIK@9^6)~8m{s}{ȯ,mKѱn5&g7ƨ˃I58_qF'"۹oN#SJXiwɿX5`vq+"su;1н g|sbDY¬jX@[C#rrᦜQGԸ# h7Pس;7/^^ +pߏ\: OS塗uO͓ђ TB;\8Pv<4<6xiH (kPǠW/_zv :yp0oorx꞉_r^=m1>eGm\G <KAwB=ιgW.'c[߾Q#.kWxB-"G)z~Fz +I@/]} +wt'@wcܽtt痯ၿkjrȈVY<ᒈ04vc#̊ȋ cq^y +6r"$T|tg,lnS_|L*āA-8L9^pGKW@o|q'cڡJR]͂/G%򮚬*?01GqG*#~s~< +nsrWώ_lG$3bojke I+݀^BbsrNy +xcнKA|`ӫA~/v +w<Ǡq6g?"zI1K}c//{,<%(/<-UA} } \v?ЍsA|/@.EJ˷)y —V !ui >T!Q 㢎~x6(kЋ79>:o@Μ=t4yqWPbR:|e<ۆll#uQQwBC$~E~zwt Ѝ?ggxq΍02_9dRCۙ0&ơbe%uUВ+?Oy%A\I o~t3k޸ s]PЉU/}TD&u"M]Pm'.XrzVrM`YVERWWY}$PPnD/_Oyp(<;T2χn0뙼 yS: `V'ȋ㵘J~ y҉+Y` 76n"V]%d rb7$2Y!26dn +53XѴtH`i=;]+_#G{,/-mɺ*kԢ]P2!mM,.t㗺Ysu%"PWCkwR(ϫ +7`w*)C X%ksɫn ga<:ݥd,P;Cqdn@P(oNu6Jۚ'ꚣĵ|֗6MXa +aSe*UƁ[ Jb'u \؎EXA.0!fr=PcepKKXksfƨ'hbp*s%KCܜ- &٦:Ѧo5jёS L⥤2@Bd6Ķp_Z l&FX3Ϫڝ1|(]i 9%g~O7~fƺtե.bv4D tpLlQp*N 4[U`k!l+,5%N3]+ 2nΎ2/z橮Q?Z+,k m:T/ |؝fq.-'5\⊔ U=xq ㇬B>Ԍ7gllC#\,z̈́[&1uՕfy%q Imɻv,K&nN,"T82&rUJtzKp*ӵ=C]wFXcV2Rb,`u <BсVUq]z v^->ǣ#lJ1[rZKUߴ=Yux8X *S%&A|Z6t>^*x zuWG8%-=0=N%TʶU1w*niSpNuuUZۖ7%1l&3H-2FHEx#J_Dk01A z8_== H;"'~fG?AsƩʊ99kVL-X!;z_!ﴰq[LWε꽳V\ ]Z`mUtCŬe4KWۺ؇al{8 "TKP ܴGXI^ɫ˃mk`U`;r&xtOړoRIYח:aUTW)aM]@K#eʨHᲠ6]5՘`Z{&ߣ)^[^v̰Yځ `v cqz|@*r)&ac`۟f)w[{.Vc虦[l䣶>:qHN%>.A﨩n%){]Sl4ˁ.Ӣ"mJq"L얔%o|>ōzs8S14:Bڶo)yo{ݦ`BgF(ECtݭnt*mrb=jwA跍 bEfl+@ծe/ +iTb +-~V0nu%{G]]QV\rj0[":أpMWm(9^gv環?l6*r^"Qذk{> X`=4SO[{֟ģ@WFH ]q|GOCaނɪ!#+35]kS<$@G(=0j6N Yis"Do˘йNzZ5gO]|`dS|}~ŞW +~ec} (%2 #*>l wN""e$=Z:'vq§; \6Z+ 5+ljR.1IxCEP4cC1{ ?<[oE@XEL v i UIToq[2.Ҧ&mU +mI+"rLU̥N oIIBu ܝts4]MӶ뀄xU-> V6Ц 'v:ןvLR ]7[Ys3b}lCBG ؔL$ȮBUDY`}<-CzvՐJ {Z`Jd@Ccs(&fz :R:EO3ɿ|i!Sv;Tw#ho_dj81mwѮa s!rwdeRٯDdgUS$ +5Jwg*($K90&Rؙٞr{椒`OK*w8a|]uCxMR?4p:YnI9^ jq;_]7/3]G;\_Oql;z&k.U\ֹ\;bp6d,Ȗ%KT-oSg 5=аۿꟀ~vOVnf|!+I?5l)<&)jI{CCZdW)P``jLGtYa^F/->JwH)tf:>CG,wfܱrj?.9A+ٜ , Öa|ԥìr/ ճHH5O&Pa)2abW ]UT|A{["p4`KJ)0l2>eGW]!N]tl?"zconZB]?&:nq8(嗅\ Cw]m@;}=UD$4̩>6 Ns j\t1)kN)2_Xb?a@.}i0Y1Юya^f#gQϒ~V-=*{elo(.z}Q1aWţz,SCWrcvam` jP!L|PJgY̿996ӡa"קhEN]uLm>{=b{Cid/-bkTpML[o"#rHy`aSã9|41$crH0;F&p9ˢy4N|Uc$B)5Q“Mآ+:!FSS%t5bWC)l9XmsfDOKvDT&vE1u v[%}my @. +kqWؼ~ng8l o@,}8Hӄg؆ 1s Oko2oLċց b^XZ-e]JtGI.؞-?LxttCFEwI9:aG] +p`ާOn96l)hnA'p9!Ǥ9-1:jck̻2״-tq)1ď[N]-ŧYF'|uO&> cPQS<kwM,SI*ܖ`3rr}a|R|ztXTY%DfĞw"uU;&9ՔrVx ÷$d-'5̝4e3FRK ˿mL "dlcRRpnv9!<,1\7:BތɥBmrtELHoM@Q6& c r1*뉹y9em"d-Bd^"1b jdUVh~9H'n+I9]%}#ӕ5;?X\zM2_Ԗԭci*n,}p܉N4ܛ*V#A pX-*Y<|#Fr[1:"c>԰j.!fm 7߀X^+{+~Osrx!tr{엶b˽% =q 6aўlˡ%KX=YB^@o##@?鑱6SLueG_Y1y@ " +V%]qC,aEM\VU:dTRWwPEbwٮܧvTuyWjvݚo!v1G<5\6 + O׻ZcR6!nx`96ֶL*0-+tȾIP7]۽6Y%;CWS*i˾Mח~`Wm#<^dD'(8 #j_1_c\"ܩB뻾2še Jz,To+PN%"a{ +=FwiV-Ū @&lN"ᡫ7ll#&8NU]SF\QbDRW ?rW黅撷HfFMy%lrlJOM hqL5k1NG`!+eׇ?jm- `P:ŘG grM>53s;)Q +VMK?Q>ծƜԫc􄳭ߖ:1!v Xg1HS/rJ;Kh_O'*^3BcS8$f}%`s o,xFqC^׵n'z]+곩QD}n.1΁H9EİSZnx/[#: + 쌘r`pE ^CQLwSSKBg-.}}_á;-^`; R#T;'ܱOra1a/v4l0>Q/,_s.O 1lc֋"%tc +~ɐ} qi. JٮP'xt/lCRonݕ!l"VR*dsfX'q]U擅V)s3qh5J&̥M*N(Xsʷ5u 6e%}{ +,g?w!.1zt,l=#U6zf+[FK4e- owFTpHIleX0+>o06XВts{uwTq@NC*k$+>\Aö&Q9, btu%O&93l.zqlm֣KAv-J‹c;r}>ל}GWqX@'LJ[lAL| A")|eБ`1[-q-%+hC@5Ri) ɹrÌ|6I/Xl~$IOMDnEJС3-e MՙoCMYIwgZI{z2x)rOK 1V`. iP59OĜG^-kw@j:-GIQjery x^Bɺߚk~KX%g}}FTe<1 +"OW]20;:6m;z|pk*Ri9)ϕ{2B1~Qc@pZ*d赖sVup)Y"L_3./>Y}{zȑJ#*"(h%+%*Fėǧh!7J 3u~7q@^؜}#rVٖg=Ҳ0V)`mP!^=ૹ4.*dSpheKU;sܺ- 1[QI.-&gkPZ*W==U`qze^LϽ5B.0!#t7oj!H87a8M NN|s|yF[X-z9X6Lәi@ӌ "׏i`՞[桼CoLS%[&(N +;xtU [n|$51Ge?gx_Co!цL@JӖKT5TZ'8ƽͺYdyZ[Rk\K}}upEޥ+\ k!jr+fߟqul쾖w1TfOkoVnFnt!9Ʊ . :6aĜIJ$-ץf*\[OpjK}ۮUCL4 Ir~~ОexЦZ AJVe-M/G2tUӡϷI- 5Jp)vS%x },,v'- |?)=zU{ރ2B(\3.7| h_z:bW@O~V75ZV[\(x1T<ɲslڒ\*Jپ_= ]l"G +.4?[FT(췅H 6)2ZN-UW,' )%,C4ԕ[n+뙘9_L[Ev1;*bC&lbJ%}]mO͹ZJBN"'=96#L|2|WE)ײn56ݡ&m5Uod1~t՞vIy侞U|>E]|M}TP{b/zґ1^9Q=ZҙQ :Wd~^ AՑY_ߜzRmȻ%mW +sn.mRhL|wȪ]035QKq{79$<}U 1G\o i,(:r%fOC@ۅ;ad[Dgcs SCI)>Gy,~6Pn\z*ڥ/Ty|N\Qq35]Fț91k{  ܅ 2fB Ԅ#i?6ۣĂ9'fy CǥڥWt'N]#wlb>^{CDr)i~ߋ:Tj|b]i}5ݖ0Kn% u=ySS]eO И[4RA3~L/SM/ӈ90ṚYL1~-1WW6ƷED>虤FfWE.rK*_mki15UV1;J|!p@2 m 6AHt%NK=%o;w#|pMֆ(|c9CE`2;.ik??UWwВ*|UA-+h?6Xb䱖z}5y3Yv/,] +VG\[} NRs%"Bt(Il]'Ydh:kեacJ­[@zuL,lR/"<ŨXdʹRӕ2rε bE0!E.]:JΜk/ygIc(:ߚb>y04<ɣf#Bǰ1{c̰{ V92a+MVlrmxO6 by?끡!z/ŕH:v~힎MhD *%SUfg׃7Knc=TؼwcȕRw`hF|7;^1잠Y1qb~~BM^G'J)7Sx,qߎ>Nq(yIFWC.I=Eފ]>p=åOkX{`1R<:wT۹gV{LSINX&KBl"RC._!'IإQ7:OC/E]fqQ' +QQ{ vq%n}9Z)+ +^n-b{սN¥'仵TcqJ*jfn1qC룸BS"6.MƃNpNb,nDXWJ/;iuNP![. +|u85h'8rov8Dղ)*9*+$We`+åmvi+nkT}h$Ҏ,_-#@@gܛ՚+x%]n~fXFMvaUthd~+vLc +\n3tmׁ>4q{cMK\h%\(5l+,hsNFdep'dr s]V \ +>&g nĭ öԥ!\tG/hI%Nyw86Ƌ׳^=19n- o M\oGhbz.rC\8;A+Wͅ&鱷ԧ#[ĸ{v\ zAkÐ᲏N4":p{wNS}ׄILႠsov3-w$ytsuO){} ه iξ3ݔ~k<2I-dݓUcKOS4}~ؙw.lJbEOޖ,XQ9|s$mI1K8SCൡxEغ0|Gᖂ9Iʴ(N=`U`SԼ-o.k`ۤ*le4fsU.n,v~ݞ*>u7WM՘ka'̮\Gɛ_Ytlm,gؤoio~cm0re7݃Rrh?gVwwJk[Râ;)Y%FMZ&vxȷ{ُ|=L%PBJ ꛆ~ss,7)fړcNn b7616ѭ%9դ}#O7[6u,45KXPβ6 .fIV'7.xBS=}m쥹+NApОm"JS(KQ61,2Q#$\*sJHeRx䶤C3Kgl` %~vxMuMOz Z!Gڂ˳蘵!b|[R's2=}5Et;IdŁRѮU^#&߭GqNkivjڝUzyoaciց.{cj-|al)yЃIo]Gٖd{I 0]t<si x>:ylwu_ 5LpVfr&dKRukD:Z ccdOwumN9tK_,gߛY%䢽YN;ۏqyӡcz*Zٝ9 ՍۚjᆔT zaݡڡ`BG)IìAZ8!Vo\ +¡Dl!}M͙7p+"ra^Kteն -C&듥q }lRT;0t~l5ŧ ï>9-qnZ{ <L,R6rkS]~tS^dUQ8CVU +b:ɡ`wl 7Mh10WlWb* +xovhgFPKCik7j FaT֒EP?-'f_gdfg}8g`y5N KEu鰹{KtD,YIkS^ }W ޚG Av.FHqUޛvT @m\%"ׇ͢n H%.=3ïvnS*̮mYVI !˫to-4=sWi=Ы)Hꮑ5{NR(n52Q|i 0KuL4ȹ2z9&a M"j);,wOi=s-Ն0 k]ZtSgf`*^ 1Q\ybs]'򂌗v]x A^-(*jmh/~Vl`s8.q|^!C+vh[G-Aڥ5#5׏X'NIqdR+䃘x~3/g եQۣ; ݙT1zO n/v}w0@O2bRAև[ ?{T}=o3vU)_&gOK,te<[~2@~\?^‡-VoWFJS3oOYZrδ-XۚYE$6^ HRPPnpCme8DwЍs @0Kр5'l[i6J;Sigөwꮴ-!] q' w3~>[#s:yYc +y 퀦;LXѡSfA%)U WE1əw,=Q?U95Rp8pq$ ~\{ō)5?S32~⪅ v)[fden%F~\-&3#C. !SB&bqԏ-gՎ+3տqJmD;֣F'֝mm@0ڏJrK|֬ZT$pfb.?򓾥$r8ƥ, s++= ~r&l-¸a+L g8#pMo 3M7dN_& fW>aB1 +]no)QGʛb~j?sd?}~ᢢٖy#xдf:sw-o0Ю:0GcH)_c/Lt6;UC䐒:Սxok~j^s2 |-ׂYy7+D:cp;DE&_|8Ўܸa 5|  <}y–PbwV _Z5g-YbD5 \?&n{BI 0s?a7'2\WzjcmGB㧥gk!}-+jZޔmqyeߧHߦ-,c#b_[z`=pcKt%+r)%PAyl)z#ԤgjB]@?;P@*1zTIk 4[f(/|F1Ψ3 +l/w,ĄKܻ 2PľP"Xyw%r7_zޜY ^s6Th$Uoz8Q.c<S! _j4+)t7D\ThU;·<ƐS6Llz}a_M* \AȍCq6sMԆ/5GSɶRLE]Jir]9Yj>5#zYO=rm;G$PFJ+&cۂJ942?.^I(y>D%LI{'(=VBFkI:׉=Rg%.jI^ɕ5"aNd}:N㤁JlaI3CKrBꟓ}OX][𬁔迋m`C|r5Ң*|Ekǟm)O7 hnB[mA'AyWț~`_S=>|0^[]Yaɫjxhk ;ٵcxG%˟lazV-輙{G/L.PG,gDc16lʣ>QA΍]U! AF)"flNz=LmvP 4ct? g߳E5V +@3]^=gc1!eC,/o˻}85 Ag>̿u#@?>mU+h-Y;.6&|3¾,i˻1^I*/$^ᄸ?%*dQ(?1 .ɥ@K4#7l ;i*pݝ`RCn*l)|pZسeGxh)25H q]fNo+e/C]'i7 \V؇KPɧוo!IGeWiEMdoG=Ewni4atAwV١O߲3wVg;7߇Ʈ*e㭠zLn=侃]x%({5ni2"75G9$sJl]T/_{e)vFS@>,k.B\Ua—;^,+P_|mR8_GɠaBْ 'xk2^vQOyk)==^h/Z|@ +=Sygԧh`+\˃$Ii4hK7?so? ؏ Tٞ2]\w|vem⠁e8{b,f&Ս>\DnH[tֶ m#6: vvEYC6uVX S!{C?6 vC}ܴs2| G%Z]τGWO* ~vaį kp -hj%~ LPs0r]g_CixҊ̾u)" "1愓|I0ï] mW9E-5?\=߶<:f:7d8GnB,4RJ DM-̺=䳒?aizE7c-ؒo8n>`Xf/)NtWɤm۠ G'+a][V#̸U&?l +T'f_W;9Ƚ>*z@*h"K6= t{_TZ13vdfm:sG5]g`~t?O8T8riw]5N(S,M}u'_3\3ዝӜ·Ќ~. }C<7hgi3)6OC˪.^=IP^? N]&h\ܞOٰJV5褐? @Gӎ؅޺Y m nN K'hkm3~Hи kC_kjw=ͣFDWڲC 6̨#Mpдl1ZrTO ;;㜁~N6xcfeW-M/W4GjT𓒺wg'jv>.+93JZya:ĭ_Q!6 u hXbT8 +hj(`_U=XB|*m%Uŝ 0;Jvp*)|[VbE 6g[!E+frzʌa%OIs>{ϣ0)dw=q^jRRrIdY2Bgvq(1vn_VZyrPp)\X9O}WԄnPXP4wvW>?3R鋛M%5.` ZRɊ)N& endstream endobj 8 0 obj [7 0 R 6 0 R] endobj 50 0 obj <> endobj xref 0 51 0000000000 65535 f +0000000016 00000 n +0000000161 00000 n +0000047552 00000 n +0000000000 00000 f +0000049891 00000 n +0000049511 00000 n +0000049585 00000 n +0001663591 00000 n +0000047603 00000 n +0000048002 00000 n +0000495496 00000 n +0000113801 00000 n +0000113688 00000 n +0000048569 00000 n +0000048950 00000 n +0000048998 00000 n +0000049775 00000 n +0000049806 00000 n +0000049659 00000 n +0000049690 00000 n +0000050027 00000 n +0000050052 00000 n +0000050436 00000 n +0000050697 00000 n +0000050766 00000 n +0000051037 00000 n +0000051125 00000 n +0000113836 00000 n +0000495570 00000 n +0000496129 00000 n +0000497454 00000 n +0000502818 00000 n +0000568406 00000 n +0000633994 00000 n +0000699582 00000 n +0000765170 00000 n +0000830758 00000 n +0000896346 00000 n +0000961934 00000 n +0001027522 00000 n +0001093110 00000 n +0001138887 00000 n +0001204475 00000 n +0001270063 00000 n +0001335651 00000 n +0001401239 00000 n +0001466827 00000 n +0001532415 00000 n +0001598003 00000 n +0001663620 00000 n +trailer <<3663182427704D6BA6D0BFF289E29A48>]>> startxref 1663820 %%EOF \ No newline at end of file diff --git a/versions/4.0/en/physics-2d/image/raycasting-output.png b/versions/4.0/en/physics-2d/image/raycasting-output.png new file mode 100644 index 0000000000..d39d1a66e5 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/raycasting-output.png differ diff --git a/versions/4.0/en/physics-2d/image/regenerate-points.png b/versions/4.0/en/physics-2d/image/regenerate-points.png new file mode 100644 index 0000000000..857a26544c Binary files /dev/null and b/versions/4.0/en/physics-2d/image/regenerate-points.png differ diff --git a/versions/4.0/en/physics-2d/image/relative-joint.gif b/versions/4.0/en/physics-2d/image/relative-joint.gif new file mode 100644 index 0000000000..5ce163d1ab Binary files /dev/null and b/versions/4.0/en/physics-2d/image/relative-joint.gif differ diff --git a/versions/4.0/en/physics-2d/image/relative-joint.png b/versions/4.0/en/physics-2d/image/relative-joint.png new file mode 100644 index 0000000000..509003eb8d Binary files /dev/null and b/versions/4.0/en/physics-2d/image/relative-joint.png differ diff --git a/versions/4.0/en/physics-2d/image/rigidbody-2d.png b/versions/4.0/en/physics-2d/image/rigidbody-2d.png new file mode 100644 index 0000000000..a383d0970e Binary files /dev/null and b/versions/4.0/en/physics-2d/image/rigidbody-2d.png differ diff --git a/versions/4.0/en/physics-2d/image/rigidbody-type.png b/versions/4.0/en/physics-2d/image/rigidbody-type.png new file mode 100644 index 0000000000..c46c7e06fb Binary files /dev/null and b/versions/4.0/en/physics-2d/image/rigidbody-type.png differ diff --git a/versions/4.0/en/physics-2d/image/slider-joint.gif b/versions/4.0/en/physics-2d/image/slider-joint.gif new file mode 100644 index 0000000000..9ddbd11eb5 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/slider-joint.gif differ diff --git a/versions/4.0/en/physics-2d/image/slider-joint.png b/versions/4.0/en/physics-2d/image/slider-joint.png new file mode 100644 index 0000000000..616ddac01e Binary files /dev/null and b/versions/4.0/en/physics-2d/image/slider-joint.png differ diff --git a/versions/4.0/en/physics-2d/image/spring-joint.gif b/versions/4.0/en/physics-2d/image/spring-joint.gif new file mode 100644 index 0000000000..3965e87877 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/spring-joint.gif differ diff --git a/versions/4.0/en/physics-2d/image/spring-joint.png b/versions/4.0/en/physics-2d/image/spring-joint.png new file mode 100644 index 0000000000..a7505970ff Binary files /dev/null and b/versions/4.0/en/physics-2d/image/spring-joint.png differ diff --git a/versions/4.0/en/physics-2d/image/threshold-1.png b/versions/4.0/en/physics-2d/image/threshold-1.png new file mode 100644 index 0000000000..96e7324e98 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/threshold-1.png differ diff --git a/versions/4.0/en/physics-2d/image/wheel-joint.gif b/versions/4.0/en/physics-2d/image/wheel-joint.gif new file mode 100644 index 0000000000..d0c3d62366 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/wheel-joint.gif differ diff --git a/versions/4.0/en/physics-2d/image/wheel-joint.png b/versions/4.0/en/physics-2d/image/wheel-joint.png new file mode 100644 index 0000000000..03df0c7157 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/wheel-joint.png differ diff --git a/versions/4.0/en/physics-2d/image/world-manifold-normal-2.png b/versions/4.0/en/physics-2d/image/world-manifold-normal-2.png new file mode 100644 index 0000000000..1a28862d0b Binary files /dev/null and b/versions/4.0/en/physics-2d/image/world-manifold-normal-2.png differ diff --git a/versions/4.0/en/physics-2d/image/world-manifold-normal.png b/versions/4.0/en/physics-2d/image/world-manifold-normal.png new file mode 100644 index 0000000000..cd4f019e73 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/world-manifold-normal.png differ diff --git a/versions/4.0/en/physics-2d/image/world-manifold-points.png b/versions/4.0/en/physics-2d/image/world-manifold-points.png new file mode 100644 index 0000000000..57b1c99ab0 Binary files /dev/null and b/versions/4.0/en/physics-2d/image/world-manifold-points.png differ diff --git a/versions/4.0/en/physics-2d/physics-2d-collider.md b/versions/4.0/en/physics-2d/physics-2d-collider.md new file mode 100644 index 0000000000..418fa39082 --- /dev/null +++ b/versions/4.0/en/physics-2d/physics-2d-collider.md @@ -0,0 +1,122 @@ +# 2D Collider + +## Create Collider + +The engine currently supports three different colliders: **BoxCollider2D**, **CircleCollider2D** and **PolygonCollider2D**. Click the **Add Component** button on the **Inspector** panel and enter the name of the collider to add it. + +![colliders](image/collider-types.png) + +## Properties + +The different colliders have the following properties in common: + +![inspector](image/collider-inspector.png) + +| Properties | Description | +|:-- | :-- | +| **Editing** | Whether to edit the collider. Check this option to edit the position, style and size of the collider in the scene. For details, please refer to **Editing Collider** below | +| **Tag** | Tag. When a collision occurs, you can distinguish different colliders according to **Tag**. | +| **Group** | Collider group. By [Collision Matrix](../editor/project/physics-configs.md), you can set the possibility of collision between different groups. | +| **Sensor** | Specify whether the collider is of sensor type, the collider of sensor type will generate a collision callback, but no physical collision effect will occur | +| **Density** | The density of the collider, used to calculate the mass of the rigid body. | +| **Friction** | The friction factor of the collider, the motion of the collider will be affected by the friction when it touches. | +| **Restitution**| The elasticity coefficient of the collider, specifying whether the collider will be affected by the elasticity when it collides. | +| **Offset** | The offset of the collider with respect to the center of the node. | + +## BoxCollider + +The Box Collider component is a common collider used to simulate a rectangle-like collider. It can be added by clicking **Add Component -> BoxCollider2D** on the **Inspector** panel of the 2D node. + +![box-collider-2d](image/box-colllider-2d.png) + +### Properties + +| Properties | Description | +| :-- | :-- | +| **Size** | The size of the box collider component | + +Please refer to [BoxCollider2D API](%__APIDOC__%/zh/class/BoxCollider2D) for details. + +### Editing Collider + +For all colliders, check **Editing** to edit them within the scene. + +![editing](image/editing.png) + +Press the left mouse button and drag within the collider to adjust the offset of the collider, and on ![gizmo](image/gizmo.png) and drag the left mouse button to adjust the shape and size of the collider. + +![edit-box-collider](image/edit-box.gif) + +Holding down the Alt button while dragging will keep the **rectangle center position** unchanged during the dragging process. + +![alt](image/edit-box-alt.gif) + +Entering on the **Inspector** panel also refines the size and offset of the collider. + +![input](image/edit-input.gif) + +## CircleCollider2D + +Click **Add Component** on the **Inspector** panel and enter CircleCollider2D to add a circle collider. + +![circle-collider](image/circle-collider.png) + +### Properties + +| Properties | Description | +| :-- | :-- | +| **Radius** | radius of the circle | + +Please refer to [CircleCollider2D API](%__APIDOC__%/zh/class/CircleCollider2D) for details. + +### Editing Collider + +When holding down the Alt button to drag, the **circle center position** will be maintained during the dragging process. + +![edit-circle-collider](image/edit-circle.gif) + +## PolygonCollider2D + +The Polygon Collider component allows you to edit more detailed physics shapes for more accurate physics simulation of objects. + +Click **Add Component** on the **Inspector** panel and enter PolygonCollider2D to add a polygon collider. + +![polygon](image/polygon-collider.png) + +### Properties + +| Properties | Description | +| :-- | :-- | +| **Threshold** | Specifies the minimum distance between the vertices of the generated map outline, the larger the value, the fewer points will be generated, adjustable according to requirements | +| **Points** | The vertices of the polygon can be edited within the scene by checking **Editing**, and can also be adjusted by entering values on the **Inspector** panel | + +Please refer to [PolygonCollider2D API](%__APIDOC__%/zh/class/PolygonCollider2D) for details. + +### Edit Collider + +In the case of Sprite components, the engine generates outlines based on the Sprite. + +![default](image/polygon-default.png) + +By dragging the mouse ![gizmo](image/gizmo.png) you can adjust the position of the outline points. + +![edit](image/edit-polygon.gif) + +By adjusting the **Threshold** and clicking the ![points](image/btn-regenerate-points.png) button, you can adjust the shape of the outline and the number of points. + +![threshold-1](image/threshold-1.png) + +When you mouse over a line segment of a polygon, the line segment will be highlighted and the mouse will change to add style, then click the left mouse button to insert a new point in the line segment. + +![add-point](image/polygon-add-point.gif) + +## Details + +Box2D physics colliders are internally composed of Box2D b2Fixture. Due to some limitations inside Box2D, a polygon physics collider may be composed of more than one b2Fixture. + +These cases are as follows: + +1. when the shape of the vertices of a polygon physics collider is a concave polygon, the physics system automatically splits these vertices into multiple convex polygon. However, it should be noted that the algorithm cannot handle the problem of self-intersection of concave polygons. +2. When the polygon physics collider has more vertices than `b2.maxPolygonVertices` (typically 8), the physics system automatically splits these vertices into multiple convex polygon. + +Normally these details are of no concern, but when ray detection is used and the detection type is `ERaycast2DType.All`, a collider may detect multiple collision points because multiple b2Fixtures are detected. diff --git a/versions/4.0/en/physics-2d/physics-2d-contact-callback.md b/versions/4.0/en/physics-2d/physics-2d-contact-callback.md new file mode 100644 index 0000000000..6d5146e6df --- /dev/null +++ b/versions/4.0/en/physics-2d/physics-2d-contact-callback.md @@ -0,0 +1,186 @@ +# 2D Contact Callback + +When a physics object moves in the scene and collides with other objects, __Box2D__ will handle most of the necessary collision detection and behavior. But the main point of making a physics game is what would happen when an object collides with something: such as a character encounters a monster should lead to damage taken, or when the ball hits the ground it should produce a sound. + +Besides the engine tells us when a collision happens, we also need a way to get these collision information. The physics engine provides contact callback when the collision happens. In the callback we can get the information from callback argument that we can determine what happened and what action needs to be done next. + +> __Notes:__ +> 1. First, set `Enable contact listener` in the [Rigidbody](physics-2d-rigid-body.md) component properties. The corresponding callback will be generated. +> ![enable contact listenr](image/enable-contact.png) +> 2. The information in the argument of callback function is fetched from the cache of the physics engine, so the information is only accessible in the current callback. Do not store the reference to those parameters in your script, but rather copy the data to local variables for later use. +> 3. If creating a physics object (rigidbody, joint or collider) in the callback function, the corresponding __Box2D__ objects will not be created immediately along these objects. The creation of physics object will be done after a physics time step completes. + +## Define a callback function + +There are two ways to register a collision callback function, either through the specified collider or through a global callback function registered with the 2D physics system. + +> __Note__: the built-in 2D physics module will only send `BEGIN_CONTACT` and `END_CONTACT` callback messages. + +```ts +@ccclass('TestContactCallBack') +export class TestContactCallBack extends Component { + start () { + // Registering callback functions for a single collider + let collider = this.getComponent(Collider2D); + if (collider) { + collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); + collider.on(Contact2DType.END_CONTACT, this.onEndContact, this); + } + + // Registering global contact callback functions + if (PhysicsSystem2D.instance) { + PhysicsSystem2D.instance.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); + PhysicsSystem2D.instance.on(Contact2DType.END_CONTACT, this.onEndContact, this); + } + } + onBeginContact (selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { + // will be called once when two colliders begin to contact + console.log('onBeginContact'); + } + onEndContact (selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { + // will be called once when the contact between two colliders just about to end. + console.log('onEndContact'); + } + onPreSolve (selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { + // will be called every time collider contact should be resolved + console.log('onPreSolve'); + } + onPostSolve (selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { + // will be called every time collider contact should be resolved + console.log('onPostSolve'); + } +} + +``` + +The above code example demonstrates how to add all the collision callback functions to a script. There are four types of callbacks, each callback function has three parameters, see [Callback parameters](#callback-parameters) below for details. The role of each callback function is shown in the comments, and developers can implement their own callback functions according to their needs. + +## Callback Order + +The callback order and timing of a collision callback function can be viewed by splitting a simple example collision. Assume that two rigid bodies are moving towards each other, the triangle to the right and the box to the left, and are about to collide. + +![anatomy-aabbs](./image/anatomy-aabbs.png) + + + + +Collision Procedure + + + + + + +
+ Collision 1
+ +
+ Collision 2
+ +
+ Collision 3
+ +
+ When two colliders intersect with each other, Box2D's default behavior is to give them an impulse so that they can get away with each other. But the behavior may not be complete in a single physics cycle. As shown here, the colliders in the example will cover each other for three physics cycles until the "bounce" is complete and they are separated from each other. In this time we can customize the behavior we want. onPreSolve will call back each time before the physics engine processes a collision. You can modify the crash information in this callback. onPostSolve will call back after the collision is processed. In this callback we can get information about the impulse of the collision as calculated by the physics engine.
+ The below output log shows the exact callback order of the example. +
        ...
+        Step
+        Step
+        BeginContact
+        PreSolve
+        PostSolve
+        Step
+        PreSolve
+        PostSolve
+        Step
+        PreSolve
+        PostSolve
+        Step
+        EndContact
+        Step
+        Step
+        ...
+    
+
+ +## Callback parameters + +The callback parameters contain all the collision contact information, and each callback function provides three parameters: + +- __selfCollider__: refers to the collider on the node which the script attached +- __otherCollider__: refers to the other collider. +- __contact__: it's an interface of the class `IPhysics2DContact`. Contains the most important information about the collision. Some useful information in __contact__ object are location of the collision and the normal vector. __contact__ store location information according to rigidbody's local coordinate system. What we need however is information from world space. We can use `contact.getWorldManifold` to get these information. Note that the builtin physics module parameter is null. + +### worldManifold + +Obtaining the `worldManifold`: + +```ts +const worldManifold = contact.getWorldManifold(); +const points = worldManifold.points; +const normal = worldManifold.normal; +``` + +`worldManifold` has the following: + +- __points__ + + The array of contact points, they are not necessarily at the exact place where the collision happens as shown below (unless you set the rigidbody to use the bullet type, but will be more performance costing). By for general usage these points are accurate enough for us to write game logic upon. + + ![world-manifold-points](./image/world-manifold-points.png) + + > __Note__: not every collision will have two contact points, in more simulation cases only one contact point will be produced. The following lists some other examples of the collision. + + ![collision-points-1](./image/collision-points-1.png) + + ![collision-points-2](./image/collision-points-2.png) + + ![collision-points-3](./image/collision-points-3.png) + +- __normal__ + + The normal vector on the contact point is from `selfCollider` to `otherCollider`, indicating the quickest direction to resolve the collision. + + ![world-manifold-normal](./image/world-manifold-normal.png) + + The lines shown in the figure is the normal vectors on the contact point. In this collision, the quickest way to solve the collision is to add the impulse to push the triangle to the top and push the box down to the right. The normal vector here is only about the direction, not with the location properties, nor with connection to any of these contact points. + + It is also necessary to understand that the __collision normal vector is not the same as the angle in which two object collides__. It will only be able to point the direction which can solve the two colliders intersecting with each other. For example, in the above example, if the triangle moves a little faster, the intersection will be shown below: + + ![world-manifold-normal-2](./image/world-manifold-normal-2.png) + + Then the quickest way would be to push the triangle to the right, so using the normal vector as the direction of collision is not a good idea. To know the true direction of the collision, use the following to get the relative velocities of two colliding bodies at the point of collision when they collide with each other. + + ```ts + const vel1 = triangleBody.getLinearVelocityFromWorldPoint(worldManifold.points[0]); + const vel2 = squareBody.getLinearVelocityFromWorldPoint(worldManifold.points[0]); + const relativeVelocity = vel1.sub(vel2); + ``` + +### Disabling contact + +```ts +contact.disabled = true; +``` + +Disabling the contact will cause the physics engine to ignore the contact when calculating the collision. Disabling will continue until the collision is completely resolved unless the contact is enabled in other callbacks. + +To disable contact in the current time step, use `disabledOnce`. + +```ts +contact.disabledOnce = true; +``` + +### Modify contact information + +To modify the contact information in __onPreSolve__ because __onPreSolve__ is called before the physics engine handles the contact information. Changes to the contact information will affect the subsequent collision calculations. + +```ts +// Modify the friction between the collision bodies +contact.setFriction(friction); + +// Modify the elasticity of the collision body +contact.setRestitution(restitution); +``` + +> __Note__: these changes will only take effect within the current time step. diff --git a/versions/4.0/en/physics-2d/physics-2d-joint.md b/versions/4.0/en/physics-2d/physics-2d-joint.md new file mode 100644 index 0000000000..d2207f2e8b --- /dev/null +++ b/versions/4.0/en/physics-2d/physics-2d-joint.md @@ -0,0 +1,189 @@ +# 2D Joint + +The physics system contains a series of joint components for connecting two rigid bodies. Joint components can be used to simulate the interaction between real-world objects such as hinges, pistons, ropes, wheels, pulleys, motor vehicles, chains, etc. Learning how to use joint components can create a realistic and interesting scenario. + +> **Note**. +> 1. Joint components are not available in the Builtin 2D physics module. +> 2. Joint components are required with [rigid body](./physics-2d-rigid-body.md) and [collider](./physics-2d-collider.md) to work correctly. As shown in the following figure. +> +> ![with-body](image/joint-with-body-collider.png) + +## Adding joints + +Click the **Add Component** button on the **Inspector** panel and enter the component name of the 2D joint. + +![add-joint](image/add-joint.png) + +## Common Properties of Joints + +Although each joint has different representations, they also share some common properties: + +| Properties | Description | +| :-- | :-- | +| **ConnectedBody** | The rigid body at the other end of the joint connected
**Note**: In v3.7, if this property is not set, it will take the origin [0, 0] piont by default to be consistent with the implementation of the constraint in **3D Physics** | +| **Anchor** | The anchor point of the rigid body connected to the local end of the joint | +| **ConnectedAnchor** | Anchor point of the rigid body at the other end of the joint connected | +| **CollideConnected** | Can the rigid bodies at the two ends of the joint collide with each other | + +Each joint needs to connect two rigid bodies to perform its function. We consider the rigid body hanging under the same node of the joint as the joint's home end, and **ConnectedBody** as the rigid body on the other end. Usually, each rigid body picks a location around itself and sets it as an anchor point. Depending on the type of joint component, the anchor point determines the center of rotation of the object, or a coordinate point used to maintain a certain distance, etc. + +The **CollideConnected** property is used to determine whether the rigid bodies at the ends of the joints need to continue to follow the regular collision rules. + +If you are now going to make a ragdoll, you might want the thighs and lower legs to partially overlap and then connect together at the knees, then you would set the **CollideConnected** property to false. If you are going to make a lift and want the lift platform and floor to collide, then you would set the **CollideConnected** property to true. + +## Adjusting Anchor Points + +Anchor points can be adjusted in the scene by clicking and dragging the ![gizmo-a](image/gizmo-a.png) to adjust the **Anchor** property, and by ![gizmo-b](image/gizmo-b.png) to adjust the **ConnectedAnchor**. + +![gizmo](image/distance-joint-gizmo.png) + +## DistanceJoint2D + +Click **Add Component** on the **Inspector** panel and enter **DistanceJoint2D** to add a distance joint. + +![distance-joint](image/distance-joint-dis.png) + +**DistanceJoint** constrains the rigid bodies at both ends of the joint to a maximum range. Beyond this range, the motion of the rigid bodies will interact with each other. + +![distance-joint](image/distance-joint.gif) + +Below this range, they will not interact with each other. + +![dis](image/distance-joint-dis.gif) + +### Properties + +| Properties | Description | +| :-- | :-- | +| **MaxLength** | The maximum distance | +| **AutoCalcDistance** | Whether to automatically calculate the distance between two rigid bodies connected by a joint | + +## FixedJoint2D + +Click **Add Component** on the **Inspector** panel and enter **FixedJoint2D** to add a fixed joint. + +![fixed](image/fixed-joint.png) + +**Fixed Joint** holds two points on two objects together based on their initial angles. + +![fixed](image/fixed-joint.gif) + +### Properties + +| Properties | Description | +| :-- | :-- | +| **Frequency** | coefficient of elasticity | +| **DampingRatio** | Damping, indicating the resistance to return to the initial state after the joint has been deformed | + +## HingeJoint2D + +Click **Add Component** on the **Inspector** panel and enter **HingeJoint2D** to add a hinge joint. + +![hinge](image/hinge-joint.png) + +A **Hinge Joint** can be seen as a hinge or peg, where the rigid body rotates around a shared point. + +![hinge](image/hinge-joint.gif) + +### Properties + +| Properties | Description | +| :-- | :-- | +| **EnableLimit** | Whether to enable the limit of the joint | +| **LowerAngle** | The minimum limit of the angle | +| **UpperAngle** | The maximum limit of the angle | +| **EnableMotor** | Whether to turn on the joint motor | +| **MaxMotorTorque** | Maximum torque that can be applied to a rigid body | +| **MotorSpeed** | Desired motor speed | + +When motor speed is enabled, the jointed rigid body will try to gradually accelerate to the desired speed. + +![speed](image/motor-speed.png) + +![motor](image/hinge-joint-motor.gif) + +## RelativeJoint2D + +Relative joints can be added by clicking **Add Component** on the **Inspector** panel and entering **RelativeJoint2D**. + +![relative](image/relative-joint.png) + +**Relative Joint** controls the relative motion between two rigid bodies. + +![relative](image/relative-joint.gif) + +### Properties + +| Properties | Description | +| :-- | :-- | +| **MaxForce** | The maximum force value that can be applied to a rigid body | +| **MaxTorque** | The maximum torque value that can be applied to the rigid body | +| **CorrectionFactor** | Position correction factor, in the range [0, 1] | +| **LinearOffset** | The position offset of the rigid body at the other end of the joint with respect to the rigid body at the starting end | +| **AngularOffset** | The angle offset of the rigid body at the other end of the joint with respect to the starting rigid body | +| **AutoCalcOffset** | Automatically calculate the angularOffset and linearOffset between two rigid bodies connected by a joint | + +## SliderJoint2D + +Click **Add Component** on the **Inspector** panel and enter **SliderJoint2D** to add a sliding joint. + +![slider](image/slider-joint.png) + +With a **Slider Joint**, the angle between the two rigid bodies is fixed and they can only slide on a specified axis. The blue square in the figure below is subject to gravity but can actually only move in the set direction due to the constraints of the joint. + +![slider](image/slider-joint.gif) + +### Properties + +| Properties | Description | +| :-- | :-- | +| **Angle** | Direction of sliding | +| **AutoCalcAngle** | Automatically calculates the sliding direction based on the two rigid bodies connected | +| **EnableMotor** | Whether to turn on the joint motor +| **MaxMotorForce** | The maximum force that can be applied to the rigid body +| **MotorSpeed** | Desired motor speed +| **EnableLimit** | Enables or disables the distance limit of the joint +| **LowerLimit** | The minimum value that the rigid body can move +| **UpperLimit** | The maximum value that the rigid body can move | + +## SpringJoint2D + +Click **Add Component** on the **Inspector** panel and enter **SpringJoint2D** to add a spring joint. + +![spring](image/spring-joint.png) + +The **Spring Joint** connects objects at the ends of the joint like a spring. + +![spring](image/spring-joint.gif) + +### Properties + +| Properties | Description | +| :-- | :-- | +| **Frequency** | Elasticity factor | +| **DamingRatio** | Damping, the resistance to return to the initial state after the joint has been deformed | +| **Distance** | The distance between the two ends of the joint +| **AutoCalcDistance** | Automatically calculates the distance between two rigid bodies connected by a joint. + +## WheelJoint2D + +Click **Add Component** on the **Inspector** panel and enter **WheelJoint2D** to add a wheel joint. + +![wheel](image/wheel-joint.png) + +The **Wheel Joint** is used to simulate the wheels of a motor vehicle. + +![wheel](image/wheel-joint.gif) + +Consider enabling **EnableMotor** for both front and rear wheels in the case of simulated realistic 4WD, and **EnableMotor** for only the drive wheels in the case of non-4WD. The illustration shows only the front wheels with **EnableMotor** enabled, thus simulating a front-wheel drive situation. + +### Properties + +| Properties | Description | +| :-- | :-- | +| **Angle** | Wheel vibration direction | +| **EnableMotor** | Whether to enable the joint motor | +| **MaxMotorTorque** | The maximum torque that can be applied to the rigid body +| **MotorSpeed** | Desired motor speed +| **Frequency** | Elasticity factor +| **DampingRatio** | Damping, indicating the resistance to return to the initial state after joint deformation | diff --git a/versions/4.0/en/physics-2d/physics-2d-rigid-body.md b/versions/4.0/en/physics-2d/physics-2d-rigid-body.md new file mode 100644 index 0000000000..8683f58d0d --- /dev/null +++ b/versions/4.0/en/physics-2d/physics-2d-rigid-body.md @@ -0,0 +1,255 @@ +# 2D Rigidbody + +A rigid body is the basic object that makes up the physical world and can be imagined as an object that you cannot see (draw) or touch (collide) and that cannot be deformed. + +Since the Builtin 2D physics system only has collision detection, rigid bodies do not work for the Builtin 2D physics system, and this setup only works for the Box2D physics system. + +## Add Rigidbody + +Click the **Add Component** button in the **Inspector** panel and enter Rigidbody2D to add a 2D rigid body component. + +![add-rigid](image/add-rigid.png) + +## Properties + +![rigidbody-2d](image/rigidbody-2d.png) + +| Properties | Description | +| :-- | :-- | +| **Group** | The group of rigid bodies. The [Collision Matrix](../editor/project/physics-configs.md) you can set the possibility of collision between different groups | +| **EnabledContactListener** | Whether to enable listening for [CollisionCallback](./physics-2d-contact-callback.md) | +| **Bullet** | Is this rigid body a fast-moving rigid body and needs to be prohibited from passing through other fast-moving rigid bodies
Please refer to [Rigidbody2D API](%__APIDOC__%/zh/class/RigidBody2D) for more information Information | +| **Type** | Rigid body types, please refer to **Rigidbody Type** below for details | +| **AllowSleep** | Whether to allow rigid body sleep
[physics-configuration](../editor/project/physics-configs.md) can be adjusted in the threshold for sleep | +| **GravityScale** | Gravity Scaling
Only for rigid bodies of type **Dynamic** | +| **LinearDamping** | Linear velocity damping factor | +| **AngularDamping** | Angular velocity damping factor | +| **LinearVelocity** | Linear velocity
Only for rigid bodies of type **Dynamic** and **Kinematic** | +| **AngularVelocity** | Angular velocity
Only for rigid bodies of type **Dynamic** and **Kinematic** | +| **FixedRotation** | Whether fixed rotation | +| **AwakeOnLoad** | Wake up the rigid body as soon as the loading is completed | + +Rigidbody component interface please refer to [Rigidbody2D API](%__APIDOC__%/zhclass/RigidBody2D). + +## RigidBody Properties + +### Mass + +The mass of the rigid body is calculated automatically from the **density** and **size** of the [Collider](physics-2d-collider.md). If you need to calculate how much force the object should be subjected to, you may need to use this property. + +```ts +// fetch the mass of rigidbody +const mass = rigidbody.getMass(); +``` + +### Velocity + +```ts +// Get linear velocity +const velocity = rigidbody.linearVelocity; +// Set linear velocity +rigidbody.linearVelocity = velocity; +``` + +Linear velocity damping factor, the larger the value the slower the object moves, can be used to simulate the effect of air friction and so on. + +```ts +// get linear damping +const damping = rigidbody.linearDamping; +// set linear damping +rigidbody.linearDamping = damping; +``` + +If you want to get the velocity of a point on a rigid body, you can get it with `getLinearVelocityFromWorldPoint`. For example, if a box is spinning and flying forward and hits a wall, you may want to get the velocity of the box at the point where the collision occurred. + +```ts +const velocity = rigidbody.getLinearVelocityFromWorldPoint(worldPoint); +``` + +Or pass a `Vec2` object as the second argument to receive the return value, so you can use your cached object to receive the value and avoid creating too many objects for efficiency. + +**The rigid get methods all provide out arguments to receive the function return value.** + +```ts +const velocity = new Vec2(); +rigidbody.getLinearVelocityFromWorldPoint(worldPoint, velocity); +``` + +### Angular Velocity + +```ts +// get angular velocity +const velocity = rigidbody.angularVelocity; +// set angular velocity +rigidbody.angularVelocity = velocity; +``` + +Angular velocity damping factor, same as the linear velocity damping factor. + +```ts +// get angular damping factor +const damping = rigidbody.angularDamping; +// set angular damping factor +rigidbody.angularDamping = damping; +``` + +### Rotation, Translation and Scaling + +The rotation, translation and scaling are the most commonly used functions in game development, and almost every node sets these properties. The physical system automatically synchronizes these properties of the node with the corresponding properties in Box2D. + +> **Note**: +> 1. There is only rotation and translation in Box2D, not scaling, so if you set the scaling property of a node, it will reconstruct all the collision bodies that this rigid body depends on. An effective way to avoid this is to treat the rendered node as a child of the rigid body node and only scale this rendered node, avoiding direct scaling of the rigid body node as much as possible. +> 2. At the end of each iteration of the physics system (the physics system is iterated in postUpdate), all rigid body information is synchronized to the corresponding node, and for performance reasons, the node information is only synchronized to the rigid body when the developer sets the display properties of the node where the rigid body is located, and the rigid body only monitors the node where it is located, i.e., if the That is, if the rotation of the node's parent node is modified, this information will not be synchronized. + +### Fix Rotation + +![fix ratation](image/fix-rotation.png) + +When doing platform jumping games, you usually don't want the rotation property of the main character to be added to the physics simulation, because it will cause the main character to fall over during the movement, so you can set `fixedRotation` of the rigid body to true to fix the rotation, the code example is as follows: + +```ts +rigidbody.fixedRotation = true; +``` + +### EnableContactListener + +![contact](image/enable-contact.png) + +Only when the collision listener of rigid body is enabled, the rigid body will call back to the corresponding component when a collision occurs. The code example is as follows: + +```ts +rigidbody.enabledContactListener = true; +``` + +## Rigidbody Type + +![type](image/rigidbody-type.png) + +Box2D originally had three rigid body types: **Static**, **Dynamic**, **Kinematic**. In Cocos Creator, one more type has been added: **Animated**. + +Animated is derived from the Kinematic type. Generally, when modifying **Rotation** or **Position** properties of rigid body types, the properties are set directly, while Animated will calculate the required velocity based on the current rotation or translation property and the target rotation or translation property, and assign it to the corresponding movement or rotation velocity. + +The main reason for adding the Animated type is to prevent strange phenomena that may occur when animating rigid bodies, such as penetration. + +| Type | Description | +| :-- | :-- | +| **Static** | Static rigid body, zero mass, zero velocity, that is, will not be affected by gravity or velocity, but can set his position to move. This type is usually used for making scenes | +| **Dynamic** | Dynamic rigid body, with mass, can set velocity, and will be affected by gravity
the only rigid body type that can be modified by 'applyforce' and 'applytorque' methods | +| **Kinematic** | Kinematic rigid body, zero mass, can set the velocity, will not be affected by gravity, but can set the velocity to move | +| **Animated** | Animated rigid, already mentioned above, is a type derived from Kinematic, mainly used for rigid bodies in combination with animation editors | + +The type of rigid body can be obtained or modified by code, the code example is as follows: + +```ts +import { RigidBody2D, ERigidBody2DType } from 'cc'; + +const rigibodyType = this.rigidbody2D.type + +this.rigidbody2D.type = ERigidBody2DType.Animated +``` + +The type of the 2D rigid body is defined in the enumeration `ERigidBody2DType`, please note the distinction with the `ERigidBodyType` of the 3D physics. + +### Collision Response + +Collisions between different types of rigid bodies are not always possible, and the results are organized as follows: + +| -- | Static | Dynamic| Kinematic | Animated | +| :-- | :-- | :-- | :-- | :-- | +| **Static** | | √ | √ | √| +| **Dynamic** | √ | √ | √ | √ | +| **Kinematic** | √| √ | √ | √ | +| **Animated** | √ | √ | √ | √ | + +## Rigidbody Methods + +### Get or Convert Rotation and Position Properties + +Using these APIs to get rotations and translations in the world coordinate system is faster than using the nodes, because the nodes also require a matrix operation to get the result, whereas using the APIs gives the result directly. + +#### Local Coordinates and World Coordinate Transformation + +```ts +// world coordinates to local coordinates +const localPoint = rigidbody.getLocalPoint(worldPoint); +// or +localPoint = v2(); +rigidbody.getLocalPoint(worldPoint, localPoint); +``` + +```ts +// local coordinates to world coordinates +const worldPoint = rigidbody.getWorldPoint(localPoint); +// or +worldPoint = v2(); +rigidbody.getLocalPoint(localPoint, worldPoint); +``` + +```ts +// local vector to world vector +const worldVector = rigidbody.getWorldVector(localVector); +// or +worldVector = v2(); +rigidbody.getWorldVector(localVector, worldVector); +``` + +```ts +const localVector = rigidbody.getLocalVector(worldVector); +// or +localVector = v2(); +rigidbody.getLocalVector(worldVector, localVector); +``` + +### Get the RigidBody Mass Center + +When force is applied to a RigidBody, the mass center of the RigidBody is generally chosen as the point of application of the force, which ensures that the force does not affect the rotation value. + +```ts +// Get the mass center in the local coordinate system +let localCenter = v2(); +rigidbody.getLocalCenter(localCenter); + +// Get the mass center in the world coordinate system +let worldCenter = v2(); +rigidbody.getWorldCenter(worldCenter); +``` + +> For the out variables passed when calling `rigidbody.getXXX` in the above code, it is recommended to cache them to avoid creating new ones every time, which may cause GC (Garbage Collection) issues. + +### Force and Impulse + +There are two ways to move an object: + +1. Apply a force or impulse to the object. The force will slowly change the velocity of the object over time, and the impulse will immediately modify the velocity of the object. +2. It is possible to directly modify the location of the object, but this does not give you the real physics simulation, you should try to use force or impulse to move a RigidBody to make the physics world more consistent. + +```ts +// Apply a force to the point specified on the RigidBody, this point is a point in the world coordinate system +rigidbody.applyForce(force, point); + +// or apply force directly to the mass of the rigid body +rigidbody.applyForceToCenter(force); + +// Apply a punch to the point specified on the rigid body, this point is a point in the world coordinate system +rigidbody.applyLinearImpulse(impulse, point); +``` + +Force and impulse can also affect the rotation only, this kind of force is called torque. + +```ts +// Apply torque to RigidBody. because it only affects the rotation, so no longer need to specify a point +rigidbody.applyTorque(torque); + +// Apply the impulse on the rotating shaft to the RigidBody +rigidbody.applyAngularImpulse(impulse); +``` + +### Other Information + +Sometimes you need to get the velocity of a RigidBody at a certain point, you can get by `getLinearVelocityFromWorldPoint` API, such as when the object collides with a platform, we need to determine whether the object is colliding from top or bottom of the platform according to the velocity of the collision point relative to the platform. + +```ts +rigidbody.getLinearVelocityFromWorldPoint(worldPoint); +``` + +More rigid body methods can be found in [Rigidbody2D API](%__APIDOC__%/zh/class/RigidBody2D) diff --git a/versions/4.0/en/physics-2d/physics-2d-system.md b/versions/4.0/en/physics-2d/physics-2d-system.md new file mode 100644 index 0000000000..b7622a50cc --- /dev/null +++ b/versions/4.0/en/physics-2d/physics-2d-system.md @@ -0,0 +1,162 @@ +# 2D Physics Manager + +The physics system hides most of the implementation details of the physics modules (Box2D and Builtin modules) (e.g.: creating rigid bodies, synchronizing rigid body information to nodes, etc.). + +Use the physics system to access some of the common functions of the physics module, such as click testing, ray testing, and setting up test messages. + +## Physics system related settings + +### Enabling the Physics Manager + +The __Physics Manager__ is enabled by default: + +```ts +PhysicsSystem2D.instance.enable = true; +``` + +### Draw physics debugging information + +To enable draw debugging information, use the __debugDrawFlags__. + +The physics system provides a variety of debugging information, you can combine the information to draw the relevant content. + +```ts +PhysicsSystem2D.instance.debugDrawFlags = EPhysics2DDrawFlags.Aabb | + EPhysics2DDrawFlags.Pair | + EPhysics2DDrawFlags.CenterOfMass | + EPhysics2DDrawFlags.Joint | + EPhysics2DDrawFlags.Shape; +``` + +Set the drawing flag to `EPhysics2DDrawFlags.None` to disable drawing. + +```ts +PhysicsSystem2D.instance.debugDrawFlags = EPhysics2DDrawFlags.None; +``` + +### Converting physics units to the world coordinate system units + +General physics modules (Box2D) uses __Metre-Kilogramme-Second (MKS)__ unit system, it has the best performance operating under such a unit system. But we use the __world coordinate system units__ (short for world units) as the unit of length in 2D games, so we need a ratio to convert the physics units to the world units. + +In general, set this ratio to __32__, which can be obtained by `PhysicsManager.PTM_RATIO`, and this value is read-only. Usually the user does not need to care about this value, the physics system will automatically convert the physics units and world units to each other. User can use the familiar world units for all the calculations. + +### Set physics gravity + +Gravity is a very important thing in physics operations, and most physics games use the gravity as a important feature. + +The default gravity is `(0, -320)` world units per second^2, according to the conversion rules described above, that's `(0, -10)` m/s^2 in physics unit. + +Gravity can be set to `0`. Example: + +```ts +PhysicsSystem2D.instance.gravity = v2(); +``` + +It is possible to change the acceleration of gravity to something else, such as a `20 m/s`. Example: + +```ts +PhysicsSystem2D.instance.gravity = v2(0, -20 * PHYSICS_2D_PTM_RATIO); +``` + +### Set physics timestep + +The Physics System updates the physics world according to a fixed timestep, the default timestep is `1/60`. But some games may not want to follow such a high frequency to update the physics world, after all, this operation is more time consuming, then you can reduce the timestep to achieve this effect. + +```ts +const system = PhysicsSystem2D.instance; + +// Physics timestep, default fixedTimeStep is 1/60 +system.fixedTimeStep = 1/30; + +// The number of iterations per update of the Physics System processing speed is 10 by default +system.velocityIterations = 8; + +// The number of iterations per update of the Physics processing location is 10 by default +system.positionIterations = 8; +``` + +> __Note__: reducing the fixed timestep and the number of iterations for each property will reduce the physics detection frequency. Therefore, it is more likely to occur rigid body penetration, which needs to be taken into account when using. + +## Querying physics object + +Often, knowing which physics objects are in a given scene is beneficial. For example, if a bomb explodes, objects in its range will be damaged; or in a strategy game, you may want to let the user drag to move a unit from a certain range. + +The physics system provides several ways to efficiently and quickly look for objects in a region, each of which uses different ways to query objects that fit the needs of the game. + +### Point test + +The point test will test if there's a collider contains a specific point under the world coordinate system. If the test is successful, it will return the collider. If there're multiple collider that contains the point, a random one will be returned. + +```ts +const collider = PhysicsSystem2D.instance.testPoint(point); +``` + +### Rectangle test + +The rectangle test will test a specified rectangle in the world coordinate system, and if the bounding box of a collision body overlaps with this rectangle, then the collision body will be added to the return list. + +```ts +const colliderList = PhysicsSystem2D.instance.testAABB(rect); +``` + +### Ray test + +The __Box2D__ physics module (not available in the Builtin module) also provides ray detection to detect which collision bodies a given line segment passes through. We can also obtain the normal vector at the point where the given line passes through and other useful information. + +```ts +const results = PhysicsSystem2D.instance.raycast(p1, p2, type, mask); + +for (let i = 0; i < results.length; i++) { + const result = results[i]; + const collider = result.collider; + const point = result.point; + const normal = result.normal; + const fraction = result.fraction; +} +``` + +The third parameter of the `raycast` function specifies the type of detection, and the ray detection supports four types. This is because the ray detection of __Box2D__ is not detected from the nearest object of the ray starting point, so the result of the test can not guarantee that the result is sorted by the distance from the object near the start of the ray. __Cocos Creator__'s physics system will determine whether the __Box2d__ test results are sorted based on the type of detection. This type will affect the result return to user. + +- `ERaycast2DType.Any` + + Detect any collider on the ray path. Once it detects any collider, it will immediately end the detection process and will no longer detect other objects. + +- `ERaycast2DType.Closest` + + Detect the nearest collider on the ray path, which is the default for the `raycast` detection, slightly slower than above method. + +- `ERaycast2DType.All` + + Detect all colliders on the ray path, the order of the detected results is not fixed. In this type of detection, a collider may return multiple results because __Box2D__ run the detection by testing the fixture, and a collider may consist of multiple fixtures. This is a more costly method and will be slower than above methods. + +- `ERaycast2DType.AllClosest` + + All colliders on the ray path are detected, but the return result is filtered and only the relevant information about the nearest point of each collider is returned, the slowest method of all. + +#### The result of ray detection + +The results of ray detection contain a lot of useful information, you can utilize these info according to the actual need. + +- __collider__ + + Specifies which collider the ray passes through. + +- __point__ + + Specifies the point at which the ray intersects the collider. + +- __normal__ + + Specifies the normal vector of the surface of the collider at the intersection. + +- __fraction__ + + Specifies the score of the intersection point at the ray. + +The following figure helps to better understand the result of ray detection. + +![raycasting-output](image/raycasting-output.png) + +#### Group and mask for ray detection + +The groups and masks for 2D physics system are the same as for 3D physics system, and can be modified by finding the collision matrix in the __Project Settings__ -> __Physics__ tab. More information can be found in [Grouping and Masks](../physics/physics-group-mask.md). diff --git a/versions/4.0/en/physics-2d/physics-2d.md b/versions/4.0/en/physics-2d/physics-2d.md new file mode 100644 index 0000000000..f9c7f0ae01 --- /dev/null +++ b/versions/4.0/en/physics-2d/physics-2d.md @@ -0,0 +1,19 @@ +# Introduction to Physics 2D + +__Cocos Creator__ supports the built-in lightweight builtin physics system and the powerful __Box2D__ physics system. For simpler physics calculations, we recommend that users use the built-in physics module, which avoids the runtime overhead of loading large __Box2D__ physics modules and building physics worlds. The __Box2D__ physics module provides a more complete interaction with the interface and pre-defined components such as rigid bodies and joints. + +Choose the physics module that suits development needs, and switch the physics module you use through the __Project -> Project Setting -> Feature Cropping__ settings. + +![Module Setup](./image/module.png) + +## Details + +- [2D Physics System](./physics-2d-system.md) +- [2D Rigid Body](./physics-2d-rigid-body.md) +- [2D Collider](./physics-2d-collider.md) +- [2D Contact Callback](./physics-2d-contact-callback.md) +- [2D Physics Joints](./physics-2d-joint.md) + +## Physics 2D Examples + +Examples are available on [physics-samples](https://github.com/cocos-creator/physics-samples/tree/v3.x/2d). diff --git a/versions/4.0/en/physics/character-controller/index.md b/versions/4.0/en/physics/character-controller/index.md new file mode 100644 index 0000000000..a15176de75 --- /dev/null +++ b/versions/4.0/en/physics/character-controller/index.md @@ -0,0 +1,137 @@ +# Character Controller + +> Cocos Creator supports character controllers since v3.8. + +The character controllers provide a simple way to add a character controller to your game. + +## Add Character Controller + +There are two types of character controllers in Cocos Creator, box character controller, and capsule character controller, both of them are inherited from the base class `CharacterController`. + +It is recommended that character controllers are invalid only in the **Bullet** and **PhysX** backends. By clicking on the **Project** menu, open the **Project Setting** panel, and find **Physics System** in the **Feature Cropping** page, choose **Bullet** or **PhysX** in the drop-down menu. + +![backend.jpg](index/backend.jpg) + +### Common Properties + +The following properties are the common properties of the character controller which can be found in the **Inspector** panel of **Box Character Controller** and **Capsule Character Controller** component. + +| Properties | Description | +| :-- | :-- | +| Group | Physics group,refer to [Collision Matrix](physics-configs.md#collision-matrix) for more details. | +| Min Move Distance | The minimum movement distance of the character controller. If the move distance invoked by the `move` method is smaller than this value, the character controller will not move. | +| Center | The center of the character controller in local space | +| Step Offset | The maximum height the character controller can automatically climb. | +| Slope Limit | The slope limit of the character controller in degrees. | +| Skin Width | The skin width of the character controller, please see below for more details| + +### Capsule Character Controller + +By clicking the **Add Component** button in the **Inspector** panel add a **CapsuleCharacterController**. + +![add-capsule-charactercontroller.jpg](./index/add-capsule-charactercontroller.jpg) + +#### Properties + +![capsule-property.jpg](index/capsule-property.jpg) + +| Properties | Description | +| :--- | :---- | +| Radius | The radius of the sphere of the capsule shape of the CharacterController in local space | +| Height | The distance between the center of two half-sphere | + +### Box Character Controller + +By clicking the **Add Component** button in the **Inspector** panel to add a **BoxCharacterController**. + +![add-box-character-controller.jpg](./index/add-box-charactercontroller.jpg) + +#### Properties + +![box-property.jpg](index/box-property.jpg) + +| Properties | Description | +| :--- | :---- | +| Half Height | The half height of the box shape of the CharacterController in local space | +| Half Side Extent | The half-side extent of the box shape of the CharacterController in local space | +| Half Forward Extent | The half-forward extent of the box on the CharacterController in local space | + +## Manipulating the Character Controllers + +To drive a character controller to move, you can use the `move` method with the following code. + +```ts +const movement = v3(1.0, 0, 0); +let characterController = this.node.getComponent(CharacterController); +characterController.move(movement); +``` + +The `move` method which uses the algorithm of the `sweep` method as its internal takes into all the colliders on its path. On the one hand, it will judge the angle between the character controller and the collider, if it is smaller than the slope limit, the character controller will continue to walk along the surface of the collider, on the other hand, if the height difference between the character controller and the collider is smaller than the step offset, the character controller also automatically climbing the step. But if the two conditions above are not satisfied, the controller will stop. + +To reset the position of a character controller, use the `setPosition` and `setWorldPosition` method of the character controller's node. + +It can be noticed that the Character Controller applies no force like gravity by default, developers need to add custom forces or change the velocity to simulate the movement or rotation. + +## Determine if on the Ground + +Use the `isGrounded` method to determine whether a character controller is standing on some colliders with the following code. + +```ts +let characterController = this.node.getComponent(CharacterController); +const isOnGround = characterController.isGrounded; +``` + +## Collision Callback + +The `onColliderHit` event will be emitted when a collision occurs between the character controller and the collider, the code example is as follows. + +```ts +let characterController = this.node.getComponent(CharacterController)!; +characterController.on('onControllerColliderHit', this.onColliderHit, this); +``` + +The callback of the collision is declared as follows: + +```ts +onColliderHit (contact: CharacterControllerContact){} +``` + +The description of the callback is below. + +- contact: the contact information when the collision occurs, refer to [CharacterControllerContact](%__APIDOC__%/en/classes/physics.CharacterControllerContact) + +A trigger event occurs when the character controller hits or leaves a trigger collider, as described below. + +| Event | Description | +| :--------------- | :------- | +| `onControllerTriggerEnter` | Triggered when the trigger starts | +| `onControllerTriggerStay` | Triggered frequently when the trigger is held | +| `onControllerTriggerExit` | Triggered at the end of a trigger | + +## Details + +Normally a character is not a fully simulated physics object when we try to simulate a character, which means it will not exhibit full physics characteristics. This means that when the collision occurs, the force situation of the character controller is different from a dynamic rigid body. Changing the position, or simulating velocity in the collision callback to implement the fore effect. + +To simulate the full physics effect, please use [Dynamic Rigibody](../physics-rigidbody.md). Note that if you attach a rigid body component to the node containing a character controller, this may cause unexpected errors which normally we do not recommend. + +There is no physics effect between character controllers, and this feature will be added in the future version. + +### Skin Width + +The **Skin Width** property allows slight penetration between a character controller and a collider to avoid shaking or stuck. + +It is usually a small, floating number above zero. + +If stuck frequently, you can adjust the **Skin Width** to a large number to avoid precision problems with floating-point numbers. + +## Example + +The address of the character controller is [GIT](https://github.com/cocos/cocos-example-projects). Import the project, and run the **case-character-controller.scene** scene to see the character controller example. + +## API + +For the API of the character controller, please refer to [Character Controller](%__APIDOC__%/en/class/physics.CharacterController), [Box Character Controller](%__APIDOC__%/en/class/physics.BoxCharacterController) and [Capsule Character Controller](%__APIDOC__%/en/class/physics.CapsuleCharacterController). + +## Note + +The character controller is not supported on Douyin mini-game wasm. Use the builtin wasm in Cocos Creator to enable this feature. diff --git a/versions/4.0/en/physics/character-controller/index/add-box-charactercontroller.jpg b/versions/4.0/en/physics/character-controller/index/add-box-charactercontroller.jpg new file mode 100644 index 0000000000..31744850d0 Binary files /dev/null and b/versions/4.0/en/physics/character-controller/index/add-box-charactercontroller.jpg differ diff --git a/versions/4.0/en/physics/character-controller/index/add-capsule-charactercontroller.jpg b/versions/4.0/en/physics/character-controller/index/add-capsule-charactercontroller.jpg new file mode 100644 index 0000000000..1f3db4ced6 Binary files /dev/null and b/versions/4.0/en/physics/character-controller/index/add-capsule-charactercontroller.jpg differ diff --git a/versions/4.0/en/physics/character-controller/index/backend.jpg b/versions/4.0/en/physics/character-controller/index/backend.jpg new file mode 100644 index 0000000000..eaf6892e9b Binary files /dev/null and b/versions/4.0/en/physics/character-controller/index/backend.jpg differ diff --git a/versions/4.0/en/physics/character-controller/index/box-property.jpg b/versions/4.0/en/physics/character-controller/index/box-property.jpg new file mode 100644 index 0000000000..cfe6b691c8 Binary files /dev/null and b/versions/4.0/en/physics/character-controller/index/box-property.jpg differ diff --git a/versions/4.0/en/physics/character-controller/index/capsule-property.jpg b/versions/4.0/en/physics/character-controller/index/capsule-property.jpg new file mode 100644 index 0000000000..062d9cbc34 Binary files /dev/null and b/versions/4.0/en/physics/character-controller/index/capsule-property.jpg differ diff --git a/versions/4.0/en/physics/example-img/arm-apple.gif b/versions/4.0/en/physics/example-img/arm-apple.gif new file mode 100644 index 0000000000..c6c4cff4a9 Binary files /dev/null and b/versions/4.0/en/physics/example-img/arm-apple.gif differ diff --git a/versions/4.0/en/physics/example-img/arm.jpg b/versions/4.0/en/physics/example-img/arm.jpg new file mode 100644 index 0000000000..e747d6023b Binary files /dev/null and b/versions/4.0/en/physics/example-img/arm.jpg differ diff --git a/versions/4.0/en/physics/example-img/bow-pulling.gif b/versions/4.0/en/physics/example-img/bow-pulling.gif new file mode 100644 index 0000000000..65e252211f Binary files /dev/null and b/versions/4.0/en/physics/example-img/bow-pulling.gif differ diff --git a/versions/4.0/en/physics/example-img/composite-shape.jpg b/versions/4.0/en/physics/example-img/composite-shape.jpg new file mode 100644 index 0000000000..5a3c4809e3 Binary files /dev/null and b/versions/4.0/en/physics/example-img/composite-shape.jpg differ diff --git a/versions/4.0/en/physics/example-img/config.jpg b/versions/4.0/en/physics/example-img/config.jpg new file mode 100644 index 0000000000..052093f23c Binary files /dev/null and b/versions/4.0/en/physics/example-img/config.jpg differ diff --git a/versions/4.0/en/physics/example-img/convex.jpg b/versions/4.0/en/physics/example-img/convex.jpg new file mode 100644 index 0000000000..06cfa4ea9a Binary files /dev/null and b/versions/4.0/en/physics/example-img/convex.jpg differ diff --git a/versions/4.0/en/physics/example-img/recycle-area.jpg b/versions/4.0/en/physics/example-img/recycle-area.jpg new file mode 100644 index 0000000000..896d4d7759 Binary files /dev/null and b/versions/4.0/en/physics/example-img/recycle-area.jpg differ diff --git a/versions/4.0/en/physics/example-img/set-raycast.jpg b/versions/4.0/en/physics/example-img/set-raycast.jpg new file mode 100644 index 0000000000..ed4e5ec524 Binary files /dev/null and b/versions/4.0/en/physics/example-img/set-raycast.jpg differ diff --git a/versions/4.0/en/physics/example-img/set-step.gif b/versions/4.0/en/physics/example-img/set-step.gif new file mode 100644 index 0000000000..c2a2f18fac Binary files /dev/null and b/versions/4.0/en/physics/example-img/set-step.gif differ diff --git a/versions/4.0/en/physics/img/add-rigidbody-in-inspector.jpg b/versions/4.0/en/physics/img/add-rigidbody-in-inspector.jpg new file mode 100644 index 0000000000..0ee4f52347 Binary files /dev/null and b/versions/4.0/en/physics/img/add-rigidbody-in-inspector.jpg differ diff --git a/versions/4.0/en/physics/img/apply-pmtl.jpg b/versions/4.0/en/physics/img/apply-pmtl.jpg new file mode 100644 index 0000000000..f68c3d32c3 Binary files /dev/null and b/versions/4.0/en/physics/img/apply-pmtl.jpg differ diff --git a/versions/4.0/en/physics/img/box-all-in-parent.png b/versions/4.0/en/physics/img/box-all-in-parent.png new file mode 100644 index 0000000000..f251bb091e Binary files /dev/null and b/versions/4.0/en/physics/img/box-all-in-parent.png differ diff --git a/versions/4.0/en/physics/img/bulelt.png b/versions/4.0/en/physics/img/bulelt.png new file mode 100644 index 0000000000..292a4e5908 Binary files /dev/null and b/versions/4.0/en/physics/img/bulelt.png differ diff --git a/versions/4.0/en/physics/img/can-collider.png b/versions/4.0/en/physics/img/can-collider.png new file mode 100644 index 0000000000..01f0ccf4f8 Binary files /dev/null and b/versions/4.0/en/physics/img/can-collider.png differ diff --git a/versions/4.0/en/physics/img/cant-collider.png b/versions/4.0/en/physics/img/cant-collider.png new file mode 100644 index 0000000000..8a8ec6aaef Binary files /dev/null and b/versions/4.0/en/physics/img/cant-collider.png differ diff --git a/versions/4.0/en/physics/img/capsule-explain.png b/versions/4.0/en/physics/img/capsule-explain.png new file mode 100644 index 0000000000..f880e9c47a Binary files /dev/null and b/versions/4.0/en/physics/img/capsule-explain.png differ diff --git a/versions/4.0/en/physics/img/center-add-comp.png b/versions/4.0/en/physics/img/center-add-comp.png new file mode 100644 index 0000000000..8f06448556 Binary files /dev/null and b/versions/4.0/en/physics/img/center-add-comp.png differ diff --git a/versions/4.0/en/physics/img/center-add-cupsule.png b/versions/4.0/en/physics/img/center-add-cupsule.png new file mode 100644 index 0000000000..7f1c5ad688 Binary files /dev/null and b/versions/4.0/en/physics/img/center-add-cupsule.png differ diff --git a/versions/4.0/en/physics/img/center-of-mass.gif b/versions/4.0/en/physics/img/center-of-mass.gif new file mode 100644 index 0000000000..c87feeb091 Binary files /dev/null and b/versions/4.0/en/physics/img/center-of-mass.gif differ diff --git a/versions/4.0/en/physics/img/center-of-mass.jpg b/versions/4.0/en/physics/img/center-of-mass.jpg new file mode 100644 index 0000000000..a24e112f06 Binary files /dev/null and b/versions/4.0/en/physics/img/center-of-mass.jpg differ diff --git a/versions/4.0/en/physics/img/center-result.png b/versions/4.0/en/physics/img/center-result.png new file mode 100644 index 0000000000..6e906cbf40 Binary files /dev/null and b/versions/4.0/en/physics/img/center-result.png differ diff --git a/versions/4.0/en/physics/img/collider-box.jpg b/versions/4.0/en/physics/img/collider-box.jpg new file mode 100644 index 0000000000..dafdeedf83 Binary files /dev/null and b/versions/4.0/en/physics/img/collider-box.jpg differ diff --git a/versions/4.0/en/physics/img/collider-capsule.jpg b/versions/4.0/en/physics/img/collider-capsule.jpg new file mode 100644 index 0000000000..d159e26bdc Binary files /dev/null and b/versions/4.0/en/physics/img/collider-capsule.jpg differ diff --git a/versions/4.0/en/physics/img/collider-cobblestone.jpg b/versions/4.0/en/physics/img/collider-cobblestone.jpg new file mode 100644 index 0000000000..c7fa31761f Binary files /dev/null and b/versions/4.0/en/physics/img/collider-cobblestone.jpg differ diff --git a/versions/4.0/en/physics/img/collider-cone.jpg b/versions/4.0/en/physics/img/collider-cone.jpg new file mode 100644 index 0000000000..2fcd309f9e Binary files /dev/null and b/versions/4.0/en/physics/img/collider-cone.jpg differ diff --git a/versions/4.0/en/physics/img/collider-cylinder.jpg b/versions/4.0/en/physics/img/collider-cylinder.jpg new file mode 100644 index 0000000000..37eb25e97b Binary files /dev/null and b/versions/4.0/en/physics/img/collider-cylinder.jpg differ diff --git a/versions/4.0/en/physics/img/collider-matrix.png b/versions/4.0/en/physics/img/collider-matrix.png new file mode 100644 index 0000000000..7ce07bd6c1 Binary files /dev/null and b/versions/4.0/en/physics/img/collider-matrix.png differ diff --git a/versions/4.0/en/physics/img/collider-mesh.jpg b/versions/4.0/en/physics/img/collider-mesh.jpg new file mode 100644 index 0000000000..2e979869ae Binary files /dev/null and b/versions/4.0/en/physics/img/collider-mesh.jpg differ diff --git a/versions/4.0/en/physics/img/collider-non-uniform-scale.jpg b/versions/4.0/en/physics/img/collider-non-uniform-scale.jpg new file mode 100644 index 0000000000..9a472a4d4a Binary files /dev/null and b/versions/4.0/en/physics/img/collider-non-uniform-scale.jpg differ diff --git a/versions/4.0/en/physics/img/collider-plane.jpg b/versions/4.0/en/physics/img/collider-plane.jpg new file mode 100644 index 0000000000..2e1fbe473a Binary files /dev/null and b/versions/4.0/en/physics/img/collider-plane.jpg differ diff --git a/versions/4.0/en/physics/img/collider-simplex.jpg b/versions/4.0/en/physics/img/collider-simplex.jpg new file mode 100644 index 0000000000..f8f8aacf00 Binary files /dev/null and b/versions/4.0/en/physics/img/collider-simplex.jpg differ diff --git a/versions/4.0/en/physics/img/collider-sphere.jpg b/versions/4.0/en/physics/img/collider-sphere.jpg new file mode 100644 index 0000000000..a57c691b99 Binary files /dev/null and b/versions/4.0/en/physics/img/collider-sphere.jpg differ diff --git a/versions/4.0/en/physics/img/collider-terrain.jpg b/versions/4.0/en/physics/img/collider-terrain.jpg new file mode 100644 index 0000000000..156159392e Binary files /dev/null and b/versions/4.0/en/physics/img/collider-terrain.jpg differ diff --git a/versions/4.0/en/physics/img/compound-colliders.png b/versions/4.0/en/physics/img/compound-colliders.png new file mode 100644 index 0000000000..807af5434a Binary files /dev/null and b/versions/4.0/en/physics/img/compound-colliders.png differ diff --git a/versions/4.0/en/physics/img/configurable-angular-driver.png b/versions/4.0/en/physics/img/configurable-angular-driver.png new file mode 100644 index 0000000000..aee220bd04 Binary files /dev/null and b/versions/4.0/en/physics/img/configurable-angular-driver.png differ diff --git a/versions/4.0/en/physics/img/configurable-angular-limit.png b/versions/4.0/en/physics/img/configurable-angular-limit.png new file mode 100644 index 0000000000..b962dbbcce Binary files /dev/null and b/versions/4.0/en/physics/img/configurable-angular-limit.png differ diff --git a/versions/4.0/en/physics/img/configurable-linear-driver.png b/versions/4.0/en/physics/img/configurable-linear-driver.png new file mode 100644 index 0000000000..946996e30a Binary files /dev/null and b/versions/4.0/en/physics/img/configurable-linear-driver.png differ diff --git a/versions/4.0/en/physics/img/configurable-linear-limit.png b/versions/4.0/en/physics/img/configurable-linear-limit.png new file mode 100644 index 0000000000..a8217fe306 Binary files /dev/null and b/versions/4.0/en/physics/img/configurable-linear-limit.png differ diff --git a/versions/4.0/en/physics/img/configurable-main.png b/versions/4.0/en/physics/img/configurable-main.png new file mode 100644 index 0000000000..fce12ab601 Binary files /dev/null and b/versions/4.0/en/physics/img/configurable-main.png differ diff --git a/versions/4.0/en/physics/img/configurable_constraint_axis_config.png b/versions/4.0/en/physics/img/configurable_constraint_axis_config.png new file mode 100644 index 0000000000..852d37f6be Binary files /dev/null and b/versions/4.0/en/physics/img/configurable_constraint_axis_config.png differ diff --git a/versions/4.0/en/physics/img/constant-force.jpg b/versions/4.0/en/physics/img/constant-force.jpg new file mode 100644 index 0000000000..14a514a306 Binary files /dev/null and b/versions/4.0/en/physics/img/constant-force.jpg differ diff --git a/versions/4.0/en/physics/img/constraint_angular_coordinate.png b/versions/4.0/en/physics/img/constraint_angular_coordinate.png new file mode 100644 index 0000000000..2bd9c0929a Binary files /dev/null and b/versions/4.0/en/physics/img/constraint_angular_coordinate.png differ diff --git a/versions/4.0/en/physics/img/constraint_coordinate.png b/versions/4.0/en/physics/img/constraint_coordinate.png new file mode 100644 index 0000000000..3ab4541780 Binary files /dev/null and b/versions/4.0/en/physics/img/constraint_coordinate.png differ diff --git a/versions/4.0/en/physics/img/create-pmtl.jpg b/versions/4.0/en/physics/img/create-pmtl.jpg new file mode 100644 index 0000000000..4f2231d30b Binary files /dev/null and b/versions/4.0/en/physics/img/create-pmtl.jpg differ diff --git a/versions/4.0/en/physics/img/cube-inspector.png b/versions/4.0/en/physics/img/cube-inspector.png new file mode 100644 index 0000000000..14b7d13146 Binary files /dev/null and b/versions/4.0/en/physics/img/cube-inspector.png differ diff --git a/versions/4.0/en/physics/img/cube.png b/versions/4.0/en/physics/img/cube.png new file mode 100644 index 0000000000..f2e8823a74 Binary files /dev/null and b/versions/4.0/en/physics/img/cube.png differ diff --git a/versions/4.0/en/physics/img/fixed-constraint.gif b/versions/4.0/en/physics/img/fixed-constraint.gif new file mode 100644 index 0000000000..8153438a59 Binary files /dev/null and b/versions/4.0/en/physics/img/fixed-constraint.gif differ diff --git a/versions/4.0/en/physics/img/fixed-constraint.png b/versions/4.0/en/physics/img/fixed-constraint.png new file mode 100644 index 0000000000..235c730c44 Binary files /dev/null and b/versions/4.0/en/physics/img/fixed-constraint.png differ diff --git a/versions/4.0/en/physics/img/game-script.png b/versions/4.0/en/physics/img/game-script.png new file mode 100644 index 0000000000..e283203701 Binary files /dev/null and b/versions/4.0/en/physics/img/game-script.png differ diff --git a/versions/4.0/en/physics/img/goose-soft-rock.jpg b/versions/4.0/en/physics/img/goose-soft-rock.jpg new file mode 100644 index 0000000000..c7fa31761f Binary files /dev/null and b/versions/4.0/en/physics/img/goose-soft-rock.jpg differ diff --git a/versions/4.0/en/physics/img/hinge-connectedbody.png b/versions/4.0/en/physics/img/hinge-connectedbody.png new file mode 100644 index 0000000000..401b09bb68 Binary files /dev/null and b/versions/4.0/en/physics/img/hinge-connectedbody.png differ diff --git a/versions/4.0/en/physics/img/hinge-constraint.jpg b/versions/4.0/en/physics/img/hinge-constraint.jpg new file mode 100644 index 0000000000..eafaf6a6f3 Binary files /dev/null and b/versions/4.0/en/physics/img/hinge-constraint.jpg differ diff --git a/versions/4.0/en/physics/img/hinge-joint.png b/versions/4.0/en/physics/img/hinge-joint.png new file mode 100644 index 0000000000..bb9f39c62f Binary files /dev/null and b/versions/4.0/en/physics/img/hinge-joint.png differ diff --git a/versions/4.0/en/physics/img/import-geometry.jpg b/versions/4.0/en/physics/img/import-geometry.jpg new file mode 100644 index 0000000000..a43acd6621 Binary files /dev/null and b/versions/4.0/en/physics/img/import-geometry.jpg differ diff --git a/versions/4.0/en/physics/img/induction.png b/versions/4.0/en/physics/img/induction.png new file mode 100644 index 0000000000..9f9531e4ac Binary files /dev/null and b/versions/4.0/en/physics/img/induction.png differ diff --git a/versions/4.0/en/physics/img/isTrigger.jpg b/versions/4.0/en/physics/img/isTrigger.jpg new file mode 100644 index 0000000000..8380f05a4f Binary files /dev/null and b/versions/4.0/en/physics/img/isTrigger.jpg differ diff --git a/versions/4.0/en/physics/img/mask-all.jpg b/versions/4.0/en/physics/img/mask-all.jpg new file mode 100644 index 0000000000..9a75730d7f Binary files /dev/null and b/versions/4.0/en/physics/img/mask-all.jpg differ diff --git a/versions/4.0/en/physics/img/material-create-pmtl.jpg b/versions/4.0/en/physics/img/material-create-pmtl.jpg new file mode 100644 index 0000000000..4f2231d30b Binary files /dev/null and b/versions/4.0/en/physics/img/material-create-pmtl.jpg differ diff --git a/versions/4.0/en/physics/img/material-create-pmtl.png b/versions/4.0/en/physics/img/material-create-pmtl.png new file mode 100644 index 0000000000..a4b0779ddd Binary files /dev/null and b/versions/4.0/en/physics/img/material-create-pmtl.png differ diff --git a/versions/4.0/en/physics/img/material-panel.png.png b/versions/4.0/en/physics/img/material-panel.png.png new file mode 100644 index 0000000000..e820aa498f Binary files /dev/null and b/versions/4.0/en/physics/img/material-panel.png.png differ diff --git a/versions/4.0/en/physics/img/p2p-joint.png b/versions/4.0/en/physics/img/p2p-joint.png new file mode 100644 index 0000000000..33648ff11c Binary files /dev/null and b/versions/4.0/en/physics/img/p2p-joint.png differ diff --git a/versions/4.0/en/physics/img/physic-material.jpg b/versions/4.0/en/physics/img/physic-material.jpg new file mode 100644 index 0000000000..26edf9613d Binary files /dev/null and b/versions/4.0/en/physics/img/physic-material.jpg differ diff --git a/versions/4.0/en/physics/img/physics-add-boxcollider.png b/versions/4.0/en/physics/img/physics-add-boxcollider.png new file mode 100644 index 0000000000..0f1bab8f4d Binary files /dev/null and b/versions/4.0/en/physics/img/physics-add-boxcollider.png differ diff --git a/versions/4.0/en/physics/img/physics-add-cube.png b/versions/4.0/en/physics/img/physics-add-cube.png new file mode 100644 index 0000000000..7922cc8fd6 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-add-cube.png differ diff --git a/versions/4.0/en/physics/img/physics-add-plane.png b/versions/4.0/en/physics/img/physics-add-plane.png new file mode 100644 index 0000000000..e5a950f843 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-add-plane.png differ diff --git a/versions/4.0/en/physics/img/physics-add-sphere.png b/versions/4.0/en/physics/img/physics-add-sphere.png new file mode 100644 index 0000000000..9d51142f52 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-add-sphere.png differ diff --git a/versions/4.0/en/physics/img/physics-ccd.jpg b/versions/4.0/en/physics/img/physics-ccd.jpg new file mode 100644 index 0000000000..d37e7aab0c Binary files /dev/null and b/versions/4.0/en/physics/img/physics-ccd.jpg differ diff --git a/versions/4.0/en/physics/img/physics-collision.png b/versions/4.0/en/physics/img/physics-collision.png new file mode 100644 index 0000000000..abc95eb175 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-collision.png differ diff --git a/versions/4.0/en/physics/img/physics-config-index.png b/versions/4.0/en/physics/img/physics-config-index.png new file mode 100644 index 0000000000..a0cffe0ed2 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-config-index.png differ diff --git a/versions/4.0/en/physics/img/physics-configs.png b/versions/4.0/en/physics/img/physics-configs.png new file mode 100644 index 0000000000..121d6ebb87 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-configs.png differ diff --git a/versions/4.0/en/physics/img/physics-debugdraw.png b/versions/4.0/en/physics/img/physics-debugdraw.png new file mode 100644 index 0000000000..7a3406c9cf Binary files /dev/null and b/versions/4.0/en/physics/img/physics-debugdraw.png differ diff --git a/versions/4.0/en/physics/img/physics-element.png b/versions/4.0/en/physics/img/physics-element.png new file mode 100644 index 0000000000..797214e8ac Binary files /dev/null and b/versions/4.0/en/physics/img/physics-element.png differ diff --git a/versions/4.0/en/physics/img/physics-hinge.gif b/versions/4.0/en/physics/img/physics-hinge.gif new file mode 100644 index 0000000000..c755a4a22d Binary files /dev/null and b/versions/4.0/en/physics/img/physics-hinge.gif differ diff --git a/versions/4.0/en/physics/img/physics-mat-panel.png b/versions/4.0/en/physics/img/physics-mat-panel.png new file mode 100644 index 0000000000..80b1f842e0 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-mat-panel.png differ diff --git a/versions/4.0/en/physics/img/physics-module.jpg b/versions/4.0/en/physics/img/physics-module.jpg new file mode 100644 index 0000000000..d37687df56 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-module.jpg differ diff --git a/versions/4.0/en/physics/img/physics-p2p.gif b/versions/4.0/en/physics/img/physics-p2p.gif new file mode 100644 index 0000000000..ee00fb91a9 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-p2p.gif differ diff --git a/versions/4.0/en/physics/img/physics-pipeline.png b/versions/4.0/en/physics/img/physics-pipeline.png new file mode 100644 index 0000000000..4b51975a53 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-pipeline.png differ diff --git a/versions/4.0/en/physics/img/physics-scene-movement.gif b/versions/4.0/en/physics/img/physics-scene-movement.gif new file mode 100644 index 0000000000..d7af1c83cf Binary files /dev/null and b/versions/4.0/en/physics/img/physics-scene-movement.gif differ diff --git a/versions/4.0/en/physics/img/physics-system.jpg b/versions/4.0/en/physics/img/physics-system.jpg new file mode 100644 index 0000000000..4e3c6d7ced Binary files /dev/null and b/versions/4.0/en/physics/img/physics-system.jpg differ diff --git a/versions/4.0/en/physics/img/physics-system.png b/versions/4.0/en/physics/img/physics-system.png new file mode 100644 index 0000000000..215ece7b46 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-system.png differ diff --git a/versions/4.0/en/physics/img/physics-type.gif b/versions/4.0/en/physics/img/physics-type.gif new file mode 100644 index 0000000000..b3567a28f9 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-type.gif differ diff --git a/versions/4.0/en/physics/img/physics-with-rigidbody.gif b/versions/4.0/en/physics/img/physics-with-rigidbody.gif new file mode 100644 index 0000000000..dde116ab7c Binary files /dev/null and b/versions/4.0/en/physics/img/physics-with-rigidbody.gif differ diff --git a/versions/4.0/en/physics/img/physics-world.jpg b/versions/4.0/en/physics/img/physics-world.jpg new file mode 100644 index 0000000000..2ac073fa53 Binary files /dev/null and b/versions/4.0/en/physics/img/physics-world.jpg differ diff --git a/versions/4.0/en/physics/img/physics.gif b/versions/4.0/en/physics/img/physics.gif new file mode 100644 index 0000000000..8d5237800b Binary files /dev/null and b/versions/4.0/en/physics/img/physics.gif differ diff --git a/versions/4.0/en/physics/img/physics.jpg b/versions/4.0/en/physics/img/physics.jpg new file mode 100644 index 0000000000..ebcbddeedf Binary files /dev/null and b/versions/4.0/en/physics/img/physics.jpg differ diff --git a/versions/4.0/en/physics/img/plane.png b/versions/4.0/en/physics/img/plane.png new file mode 100644 index 0000000000..d3d45cb42c Binary files /dev/null and b/versions/4.0/en/physics/img/plane.png differ diff --git a/versions/4.0/en/physics/img/pointtopoint-constraint.jpg b/versions/4.0/en/physics/img/pointtopoint-constraint.jpg new file mode 100644 index 0000000000..f5742f1541 Binary files /dev/null and b/versions/4.0/en/physics/img/pointtopoint-constraint.jpg differ diff --git a/versions/4.0/en/physics/img/raycast-mask-setting.png b/versions/4.0/en/physics/img/raycast-mask-setting.png new file mode 100644 index 0000000000..8bdf9b539d Binary files /dev/null and b/versions/4.0/en/physics/img/raycast-mask-setting.png differ diff --git a/versions/4.0/en/physics/img/raycast-with-collider.png b/versions/4.0/en/physics/img/raycast-with-collider.png new file mode 100644 index 0000000000..f9b719d728 Binary files /dev/null and b/versions/4.0/en/physics/img/raycast-with-collider.png differ diff --git a/versions/4.0/en/physics/img/raycast.jpg b/versions/4.0/en/physics/img/raycast.jpg new file mode 100644 index 0000000000..87c23077e2 Binary files /dev/null and b/versions/4.0/en/physics/img/raycast.jpg differ diff --git a/versions/4.0/en/physics/img/rigid-body-player.png b/versions/4.0/en/physics/img/rigid-body-player.png new file mode 100644 index 0000000000..2f992b286c Binary files /dev/null and b/versions/4.0/en/physics/img/rigid-body-player.png differ diff --git a/versions/4.0/en/physics/img/rigid-body.jpg b/versions/4.0/en/physics/img/rigid-body.jpg new file mode 100644 index 0000000000..e6da70d713 Binary files /dev/null and b/versions/4.0/en/physics/img/rigid-body.jpg differ diff --git a/versions/4.0/en/physics/img/rigidbody-prop.png b/versions/4.0/en/physics/img/rigidbody-prop.png new file mode 100644 index 0000000000..af3e6434aa Binary files /dev/null and b/versions/4.0/en/physics/img/rigidbody-prop.png differ diff --git a/versions/4.0/en/physics/img/rigidybody-type.png b/versions/4.0/en/physics/img/rigidybody-type.png new file mode 100644 index 0000000000..6cfed30e52 Binary files /dev/null and b/versions/4.0/en/physics/img/rigidybody-type.png differ diff --git a/versions/4.0/en/physics/img/sample-phy-setting.png b/versions/4.0/en/physics/img/sample-phy-setting.png new file mode 100644 index 0000000000..b9cfd9fad5 Binary files /dev/null and b/versions/4.0/en/physics/img/sample-phy-setting.png differ diff --git a/versions/4.0/en/physics/img/scene.png b/versions/4.0/en/physics/img/scene.png new file mode 100644 index 0000000000..8bc0c36bb3 Binary files /dev/null and b/versions/4.0/en/physics/img/scene.png differ diff --git a/versions/4.0/en/physics/img/servo.png b/versions/4.0/en/physics/img/servo.png new file mode 100644 index 0000000000..178904c1ab Binary files /dev/null and b/versions/4.0/en/physics/img/servo.png differ diff --git a/versions/4.0/en/physics/img/set-collider-config.png b/versions/4.0/en/physics/img/set-collider-config.png new file mode 100644 index 0000000000..4299472ba8 Binary files /dev/null and b/versions/4.0/en/physics/img/set-collider-config.png differ diff --git a/versions/4.0/en/physics/img/set-group.png b/versions/4.0/en/physics/img/set-group.png new file mode 100644 index 0000000000..131542d3a2 Binary files /dev/null and b/versions/4.0/en/physics/img/set-group.png differ diff --git a/versions/4.0/en/physics/img/sweep.jpg b/versions/4.0/en/physics/img/sweep.jpg new file mode 100644 index 0000000000..84591ea11f Binary files /dev/null and b/versions/4.0/en/physics/img/sweep.jpg differ diff --git a/versions/4.0/en/physics/index.md b/versions/4.0/en/physics/index.md new file mode 100644 index 0000000000..ecfacece77 --- /dev/null +++ b/versions/4.0/en/physics/index.md @@ -0,0 +1,9 @@ +# Physical System + +Cocos Creator's physics system provides an efficient componentized workflow and an easy way to use it. It currently supports rigid bodies, collision components, trigger and collision events, physics materials, ray casting, and more. + +![image](img/physics.gif) + +Cocos Creator has a built-in [2D Physics](../physics-2d/physics-2d.md) and [3D physics](physics.md), developers can configure the 2D or 3D physics system as needed via the Creator main menu bar in **Projects -> Project Settings -> Feature Cropping**. + +![physics system](img/physics-system.png) diff --git a/versions/4.0/en/physics/physics-ccd.md b/versions/4.0/en/physics/physics-ccd.md new file mode 100644 index 0000000000..8c7bd32469 --- /dev/null +++ b/versions/4.0/en/physics/physics-ccd.md @@ -0,0 +1,33 @@ +# Continuous Collision Detection + +**Continuous Collision Detection** (abbreviated as **CCD**) is a technology used to avoid collision data inaccuracy due to penetration of objects moving at high speed in discrete motion, and can be used to achieve collisions with objects similar to a bullet moving at high speed without directly penetrating the object. + +## Enable CCD + +CCD is disabled by default. To enable it, set the `useCCD` property of the RigidBody component of the object to `true`: + +```ts +const rigidBody = this.getComponent(RigidBody); +rigidBody.useCCD = true; +``` + +## Sweep-based CCD + +Penetration is a phenomenon caused by the discrete motion of an object, which can be split into **movement** and **rotation**. Cocos Creator currently only supports Sweep-based CCD, which refer to penetration caused by discrete **movement** of objects. + +Penetration usually occurs in objects that are moving at high speed, such as bullets. This is due to the fact that computer simulations are based on discrete forms, and when an object is moving too fast, the energy of a single integration is too large, which may cause the object to pass through another object that should have collided with it, as shown in the following figure: + +![physics-ccd](img/physics-ccd.jpg) + +The ball moves from time T0 to time T1, missing the black square that should have collided at time T0.5. This is a typical linear penetration phenomenon caused by too much linear velocity. + +## Support + +Due to differences in support among physics engines, and for performance reasons, Creator supports CCD in the following cases: + +- Only dynamic rigidbodies are supported. +- Only convex shapes are supported. +- No support for solving rotational penetration problems. +- For performance reasons, CCD events are only supported for types starting with `onCollision`. +- When the physics engine is set to **Bullet**, only single shape objects (containing only one collision component) are supported, and the `center` property of the collision component needs to be set to 0. +- When the physics engine is set to **Cannon**, only SphereCollider components are supported. diff --git a/versions/4.0/en/physics/physics-collider.md b/versions/4.0/en/physics/physics-collider.md new file mode 100644 index 0000000000..370ae33da4 --- /dev/null +++ b/versions/4.0/en/physics/physics-collider.md @@ -0,0 +1,252 @@ +# Collider + +Colliders can be used to define the shape of the object that needs to be physically collided with, and different geometries have different properties. Colliders are usually classified as follows: +1. [Basic Colliders](#Basic%20Colliders). The common ones include [Box](#BoxCollider), [Sphere](#SphereCollider), [Cylinder](#CylinderCollider), [Cone](#ConeCollider) and [Capsule](#CapsuleCollider). +2. [Composite Colliders](#Composite%20Colliders). You can easily simulate game object shapes by adding one or more base colliders to a node, while keeping the performance overhead low. +3. [MeshCollider](#MeshCollider). Generate colliders based on object mesh information, fully fitting the mesh. +4. [SimplexCollider](#SimplexCollider). Provide point, line, triangular surface, tetrahedron collision. +5. [PlaneCollider](#PlaneCollider). Can represent an infinite plane or half-space. This shape can only be used for static, non-moving objects. +6. [TerrainCollider](#TerrainCollider). A special support for concave terrain. + +> **NOTE**: In some (e.g. Bullet) physics backends, very high scaled sizes should be avoided due to floating point inaccuracy, here it is recommended to be below 1000. e.g. a certain box collider with a Y value of 40 and a Z value of 0.01 for its **Size** property, at this point they have a Y, Z ratio of more than 1000, at which case there may be inaccurate floating point calculations. + +## Add Collider Component + +Here we will take the example of adding **BoxCollider** component. + +### Add via Editor + +1. Create a new 3D object Cube, click the **+** Create button in the top left corner of **Hierachy** panel, then select **Create -> 3D Object -> Cube**. + + ![add-cube](img/physics-add-cube.png) + +2. Select the Cube created in **Hierachy** panel, click the **Add Component** button at the bottom of the **Inspector** panel on the right, and select **Physics -> BoxCollider** to add a collider component. + + ![add-boxcollider](img/physics-add-boxcollider.png) + +### Add by Code + +```ts +import { BoxCollider } from 'cc' + +const boxCollider = this.node.addComponent(BoxCollider); +``` + +## Collider Common Properties + +| Property | Description | +| :---|:--- | +| **Attached** | The [Rigidbody](physics-rigidbody.md) to which the collider is attached | +| **Material** | The [Physics Material](physics-material.md) used by the collider, or the engine's default physics material if not set | +| **IsTrigger** | Whether or not it is a [Trigger](physics-event.md), the trigger will not generate physics feedback | + +Please note the following points for getting rigid body. + +- This attribute returns `null` if the node itself does not have a `RigidBody` component. +- The real attribute corresponding to Attached is called `attachedRigidBody`, which is a read-only attribute and cannot be modified. + +```ts +let collider = this.node.addComponent(BoxCollider)!; +let rigidbody = collider.attachedRigidBody; +``` + +To edit Colliders, please refer to: [Collider Gizmo](../editor/scene/collider-gizmo.md). + +> **Note**: Please check [Different physical back-end collision shape support](physics-engine.md#Different%20physical%20back-end%20collision%20shape%20support) before using colliders to make sure the physics engine you are currently using supports it. + +## Colliders + +### Basic Colliders + +#### BoxCollider + +![BoxCollider](img/collider-box.jpg) + +The BoxCollider is a rectangular shaped collider that can be used to achieve collision of wooden boxes, walls, and other objects. It can be combined into composite shapes. + +| Properties | Description | +| :---|:--- | +| **Center** | The center of the shape in the local coordinate system | +| **Size** | The size of the box in the local coordinate system, i.e. length, width and height | + +Please refer to [BoxCollider API](%__APIDOC__%/en/physics/classes/boxcollider.html) for details. + +#### SphereCollider + +![Sphere Collider Component](img/collider-sphere.jpg) + +SphereCollider is a sphere-based collider. + +| Properties | Description | +| :---|:--- | +| **Center** | The center of the shape in the local coordinate system | +| **Radius** | The radius of the sphere in the local coordinate system | + +Please refer to [SphereCollider API](%__APIDOC__%/en/physics/classes/spherecollider.html) for details. + +#### CapsuleCollider + +![Capsule Collider Component](img/collider-capsule.jpg) + +CapsuleCollider is a collider based on the shape of a capsule. + +> **Note**: `cannon.js` does not support capsule components, it is recommended to use two spheres and a cylinder to collocate. + +| Properties | Description | +| :---|:--- | +| **Center** | The center of the shape in the local coordinate system | +| **Radius** | The radius of the sphere on the capsule in the local coordinate system | +| **CylinderHeight** | The height of the cylinder on the capsule in the local coordinate system | +| **Direction** | The orientation of the capsule in the local coordinate system | + +![capsule-explain](img/capsule-explain.png) + +Please refer to [CapsuleCollider API](%__APIDOC__%/en/physics/classes/capsulecollider.html) for more details. + +#### CylinderCollider + +![Cylinder Collider Component](img/collider-cylinder.jpg) + +CylinderCollider is a cylinder-based collider. + +| Properties | Description | +| :---|:--- | +| **Center** | The center of the shape in the local coordinate system | +| **Radius** | The radius of the circle on the cylinder in the local coordinate system | +| **Height** | The height of the cylinder in the local coordinate system in the corresponding axial direction | +| **Direction** | The orientation of the cylinder in the local coordinate system | + +Please refer to [CylinderCollider API](%__APIDOC__%/en/class/physics.CylinderCollider) for details. + +#### ConeCollider + +![Cone Collider Component](img/collider-cone.jpg) + +ConeCollider is a collider based on a cone. + +| Properties | Description | +| :---|:--- | +| **Center** | The center of the shape in the local coordinate system | +| **Height** | The height of the cone in the local coordinate system in the corresponding axis | +| **Direction** | The orientation of the cone in the local coordinate system | + +Please refer to [ConeCollider API](%__APIDOC__%/en/class/physics.ConeCollider) for details. + +### Composite Colliders + +A composite collider is not a single collider component type, but a combination of multiple base colliders that can easily simulate the shape of complex game objects. + +![compound-colliders](img/compound-colliders.png) + +### PlaneCollider + +![Plane collider component](img/collider-plane.jpg) + +PlaneCollider is a collider that belongs to a planar model. Planes can be created by right-clicking **Create -> 3D Objects -> Plane** in the **Hierarchy** panel. + +| Properties | Description | +| :--- |:--- | +| **Center** | The center of the shape in the local coordinate system. | +| **Normal** | The normal of the plane in the local coordinate system. | +| **Constant** | The distance of the plane from the origin along the normal in the local coordinate system | + +Please refer to [PlaneCollider API](%__APIDOC__%/en/class/physics.PlaneCollider) for details. + +### MeshCollider + +![Mesh Collider Component](img/collider-mesh.jpg) + +MeshCollider is a collider based on the model mesh. + +> **Notes**: +> 1. `cannon.js` has poor support for mesh collider components, allowing detection with only a few colliders (spheres, planes). +> 2. **Convex** functionality is currently only supported in the `ammo.js` backend. + +| Property | Description | +| :--- |:--- | +| **Center** | The center of the shape in the local coordinate system | +| **Mesh** | The mesh resource used by the mesh collider to initialize the mesh collider | +| **Convex** | Whether to use the convex packet approximation of the mesh instead, the number of mesh vertices should be less than **255**, and the kinetics can be supported when turned on | + +MeshCollider interface please refer to [MeshCollider API](%__APIDOC__%/en/class/physics.MeshCollider). + +### SimplexCollider + +![Simplex collider component](img/collider-simplex.jpg) + +SimplexCollider is a point, line, face and tetrahedron based collider. + +| Properties | Description | +| :--- | :--- | +| **Center** | The center of the shape in the local coordinate system | +| **ShapeType** | The pure shape type, including four types: point, line, triangle, and tetrahedron | +| **Vertex0** | The vertex 0 of a pure shape, the point (consisting of 0) | +| **Vertex1** | vertex 1 of a simplex, line (composed of 0, 1) | +| **Vertex2** | vertex 2 of a pure form, triangles (and so on) | +| **Vertex3** | Vertex 3 of a pure form, tetrahedron | + +Please refer to [SimplexCollider API](%__APIDOC__%/en/class/physics.SimplexCollider) for the interface to the simplex collider component. + +> **Note**: `cannon.js` support for lines and triangles is currently imperfect. + +### TerrainCollider + +![Terrain Collider Component](img/collider-terrain.jpg) + +TerrainCollider is a collider generated by a terrain surface, with the same specific shape as the object it is attached to in the Terrain asset. + +| Properties | Description | +| :--- | :--- | +| **Center** | The center of the shape in the local coordinate system | +| **Terrain** | Gets or sets the terrain asset referenced by this collider | + +See [TerrainCollider API](%__APIDOC__%/en/#/docs/class/physics.TerrainCollider) for the Terrain Collider component interface. + +## Auto Scaling + +Each component is bound to a node, and some components will dynamically update data based on the bound node. Among them, the collider component will automatically update the corresponding shape data based on the node information, so that the collider can fit the rendered model more closely. Take the model component as an example. + +The model component will automatically update the model's world matrix based on the bound nodes, thus enabling changes to the node's position, scaling, rotation, and other information, which can make the rendered model have the corresponding affine transformation. + +However, there are some properties of colliders that cause scaling to be differ from the bound node. + +- Colliders are generally described in terms of geometric structures +- Colliders are mostly of convex packet type + +These properties restrict transformations such as tangent, non-uniform scaling, etc. Take the sphere as an example. + +Assuming that the scaling information of the bound node is **(1,2,1)** (non-uniform scaling), since the model and the collider describe different structures, the sphere model uses multiple base elements (such as triangular surfaces) to describe, and after scaling will be shaped into a pebble-like shape; while the sphere collider is described according to the radius size, scaling will take the dimension with the largest value to scale the radius (this is to encircle the model as much as possible with the collider), **but it is still a sphere** after scaling, so it cannot precisely encircle the pebble-like size of the sphere model. + +![non-uniformly scaled sphere](img/collider-non-uniform-scale.jpg) + +### Non-standard Shapes + +For non-standard shapes like pebbles, you can use [MeshCollider](#meshcollider) to replace the base colliders. + +> **Note**: If you need to support kinematic rigid bodies, you must enable the **convex** function. + +![cobblestone](img/collider-cobblestone.jpg) + +## Composite Colliders + +For a single node it is easy to see if there is a physical element, but in the case of a node chain (a physical element consisting of a tree of nodes) it is difficult to see which nodes and which components the physical element is composed of. + +For the case of node chains, there are currently two usage options. + +1. Each node in the node chain is an element as long as it has a physical component, so that there is no dependency between the components of the parent and child nodes. If the node needs more than one collider shape, just add the corresponding **Collider** component to that node. + + ![box-all-in-parent](img/box-all-in-parent.png) + + **Disadvantages**: + + - Hierarchical structure is not intuitive enough, multiple shapes can only be added to one node, while displaying shapes requires adding sub-node models and makes it difficult to support local rotation of collision bodies. + - When adjusting the parameters of the node chain, two places need to be adjusted at the same time: the position information of the child nodes and the data information of the corresponding **Collider** component on the parent node. + +2. If you find a **RigidBody** component, you bind your own **Collider** component to that node, otherwise the **Collider** components in the whole chain will share a single **RigidBody** component, and the node corresponding to the element is the node corresponding to the top-level **Collider** component. + + **Disadvantages**: + + - Increases node coupling, and when nodes are updated, the corresponding dependent nodes need to be updated. + - More content needs to be maintained when the node chain is broken, and the node chain needs to handle complex logic when it is repeatedly broken. + +> **Note**: Currently Cocos Creator is using scheme **1**. This may change in subsequent versions, so please pay attention to version update announcements. diff --git a/versions/4.0/en/physics/physics-component.md b/versions/4.0/en/physics/physics-component.md new file mode 100644 index 0000000000..2056f39058 --- /dev/null +++ b/versions/4.0/en/physics/physics-component.md @@ -0,0 +1,19 @@ +# Physics Components + +Cocos Creator currently provides users with the following types of physics components. + +- [Collider Component](physics-collider.md) + + The collider component is used to define the shape of the collider. The shape can be viewed in the editor environment and is not visible at runtime. The shape can be set as desired, and does not need to be identical to the object mesh. + +- [Rigidbody Component](physics-rigidbody.md) + + Rigid bodies are the basic objects that make up the physics world and allow the game objects to move in a physically controlled way. + +- [Constant Force Component](physics-constantForce.md) + + The constant force component is a tool component that depends on a rigid body and will apply a given force and torque to a rigid body at each frame. + +- [Constraints](physics-constraint.md) + + The constraint component depends on the rigid body component and constrains the position, orientation or scale of an object to other objects. diff --git a/versions/4.0/en/physics/physics-configs.md b/versions/4.0/en/physics/physics-configs.md new file mode 100644 index 0000000000..393b16e200 --- /dev/null +++ b/versions/4.0/en/physics/physics-configs.md @@ -0,0 +1,84 @@ +# Physical System Configuration + +The PhysicsSystem module is used to manage the entire physics system and is responsible for synchronizing physics elements, triggering physics events, and scheduling iterations of the physics world. + +## Physics Configuration + +There exists two ways to configure the physics system, one is to configure it in the editor, and the other is to configure it in the code. + +### Through the Physical Configuration panel + +The **Project Settings -> Physical Configuration** allows you to configure the related physics system. + +![Physics](./img/physics-config-index.png) + +| Properties | Description | +| :--- | :--- | +| **Gravity X** | x-component of gravity | +| **Gravity Y** | y-component of gravity | +| **Gravity Z** | z-component of gravity | +| **AllowSleep** | Allow the system to sleep, default value `true` | +| **SleepThreshold** | The default speed threshold to enter sleep, default value `0.1`, minimum value `0` | | +| **AutoSimulation** | Whether to enable auto simulation, default `true` | +| **FixedTimeStep** | Fixed time per simulation step, default `1/60`, min `0` | +| **MaxSubSteps** | The maximum number of sub-steps per step, default `1`, min `0` | +| **DefaultMaterial** | Default physics material | +| **CollisionMatrix** | Collision matrix, for initialization only | + +> **Note**: Currently **2D** / **3D** physics share a common configuration. + +### Programmatic configuration + +The physics system can be configured via `PhysicsSystem.instance`. Some code examples are as follows: + +```ts +import { _decorator, Component, Node, Vec3, PhysicsSystem } from 'cc'; +const { ccclass, property } = _decorator; +@ccclass('Example') +export class Example extends Component { + start () { + PhysicsSystem.instance.enable = true; + PhysicsSystem.instance.gravity = new Vec3(0, -10, 0); + PhysicsSystem.instance.allowSleep = false; + } +} +``` + +> **Note**: The physics system is a singleton class, get an instance of the physics system via `PhysicsSystem.instance`. + +The physics configuration can also be reset through the `PhysicsSystem.resetConfiguration` interface, with the following code example: + +```ts +let conf = game.config.physics +conf.gravity = new Vec3(10, 10, 0); +PhysicsSystem.instance.resetConfiguration(conf); +``` + +For more API content, please check the PhysicsSystem interface please refer to: [PhysicsSystem API](%__APIDOC__%/en/class/physics.PhysicsSystem). + +## Collision Matrix + +The collision matrix is a further wrapper around the [Group and Mask](physics-group-mask.md) function, which is used to initialize the groups and masks of physics elements. + +![physics-collision](img/physics-collision.png) + +By default the collision matrix has only one **DEFAULT** group, new groups do not collide with other groups by default. + +Click the **+** button to add a new group. The **Index** and **Name** of the new group cannot be left unfilled. + +- **Index** represents the collision group value, which supports up to 32 bits, i.e. the value range is `[0, 31)`. Group values cannot be repeated. +- **Name** is the collision group name. The name set here is only for the convenience of the user to configure the collision group, it cannot be obtained by code, only the group value can be obtained by code. + +![collider-matrix](img/collider-matrix.png) + +The figure shows the configuration of a collider group for a flying shooter game. As you can see from the figure, when a group is added, the panel appears with the names of the groups both horizontally and vertically. Let's call the horizontal part **Group** and the vertical part **Mask**. Suppose we now make the following checkboxes: + +![set-collider-config](img/set-collider-config.png) + +This configuration means that the group **SELF_PLANE** can collide with the groups **ENEMY_BULLET** and **SELF_BULLET**. Here the group **ENEMY_BULLET** and **SELF_BULLET** are the masks of the group **ENEMY_PLANE**. Similarly, for the group **ENEMY_BULLET**, **ENEMY_PLANE** is also its mask. + +Once the collision matrix is configured, you can add the **RigidBody** component to the object to be collided with and set the collision group `Group`. + +![set-group](img/set-group.png) + +> **Note**: groups cannot be deleted before v3.5, but the name of the group can be modified. diff --git a/versions/4.0/en/physics/physics-constantForce.md b/versions/4.0/en/physics/physics-constantForce.md new file mode 100644 index 0000000000..9be6829279 --- /dev/null +++ b/versions/4.0/en/physics/physics-constantForce.md @@ -0,0 +1,14 @@ +# Constant Force Components + +The constant force component is a tool component that relies on the [Rigidbody](physics-rigidbody.md) component to apply a given force and torque to a rigid body every frame. + +![constant-force component](img/constant-force.jpg) + +| Properties | Description | +| :---|:--- | +| **Force** | The force applied to the rigid body in the world coordinate system | +| **LocalForce** | the force applied to the rigid body in the local coordinate system | +| **Torque** | the torque applied to a rigid body in the world coordinate system +| **LocalTorque** | the torque applied to a rigid body in the local coordinate system | + +Please refer to [ConstantForce API](%__APIDOC__%/en/class/physics.ConstantForce) for details. diff --git a/versions/4.0/en/physics/physics-constraint.md b/versions/4.0/en/physics/physics-constraint.md new file mode 100644 index 0000000000..0fa458fb83 --- /dev/null +++ b/versions/4.0/en/physics/physics-constraint.md @@ -0,0 +1,179 @@ +# Constraints + +In the physics engine, **Constraints** are used to simulate connections between objects, such as rods, strings, springs, or ragdolls. + +Constraints depend on [Rigidbody](physics-rigidbody.md). If the node does not have a rigidbody component, the engine will automatically add a rigidbody component when adding constraints. + +> **Note**: The current constraint only works if the physics engine is selected as Bullet, PhysX or Cannon.js. + +## HingeConstraint + +Hinge constraints constrain the motion of connected objects to a certain axis. This constraint is useful in situations such as simulating the hinge of a door or the rotation of a motor. + +![HingeConstraint](img/hinge-constraint.jpg) + +| Properties | Description | +| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------ | +| **AttachedBody** | The rigid body to which the constraint is attached, that is, the rigid body component under the same node where the current constraint is located | +| **ConnectedBody** | Gets or sets the rigid body to which the constraint is connected, null means it is linked to a static body | +| **EnableCollision** | Gets or sets whether collision is enabled between two bodies connected by a constraint | +| **PivotA** | Constrain the relative position of a joint in the local space of its own rigid body | +| **PivotB** | Constrain the relative position of the joint in the local space of the connected rigid body | +| **Axis** | The axis that constrains constraint rotation in local space | + +![physics-hinge](img/physics-hinge.gif) + +Please refer to [HingeConstraint API](%__APIDOC__%/en/class/physics.HingeConstraint) for the hinge constraint interface. + +## PointToPointConstraint + +A point-to-point constraint is a simple composite constraint that connects two objects, or one object, to a point in the coordinate system. The connected objects can be freely rotated with respect to each other while sharing a common connection point. + +![point-to-point constraint](img/pointtopoint-constraint.jpg) + +| Properties | Description | +| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------ | +| **AttachedBody** | The rigid body to which the constraint is attached, that is, the rigid body component under the same node where the current constraint is located | +| **ConnectedBody** | Gets or sets the rigid body to which the constraint is connected, null means it is linked to a static body | +| **EnableCollision** | Gets or sets whether collision is enabled between two bodies connected by a constraint | +| **PivotA** | Constrain the relative position of a joint in the local space of its own rigid body | +| **PivotB** | Constrain the relative position of the joint in the local space of the connected rigid body | + +![physics-p2p](img/physics-p2p.gif) + +For point-to-point constraint interface, please refer to [PointToPointConstraint API](%__APIDOC__%/en/class/physics.PointToPointConstraint). + +## FixedConstraint + +A fixed constraint is the simplest constraint, it locks the relative position and rotation of two rigid bodies. The connected objects are not allowed to move relatively to each other. + +![FixedConstraint](img/fixed-constraint.png) + +| Properties | Description | +| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------ | +| **AttachedBody** | The rigid body to which the constraint is attached, that is, the rigid body component under the same node where the current constraint is located | +| **ConnectedBody** | Gets or sets the rigid body to which the constraint is connected, null means it is linked to a static body | +| **EnableCollision** | Gets or sets whether collision is enabled between two bodies connected by a constraint | +| **BreakForce** | Gets or sets the maximum force that can be applied to the constraint before it breaks | +| **BreakTorque** | Gets or sets the maximum torque that can be applied to the constraint before it breaks | + +![physics-fixed](img/fixed-constraint.gif) + +For fixed constraint interface, please refer to [FixedConstraint API](%__APIDOC__%/en/class/physics.FixedConstraint). + +## Configurable constraint + +Configurable constraints are one of the most comprehensive constraints in physics engines, including various commonly used constraint types in game engines. Through configuration, control can be applied to each of the six degrees of freedom, and nearly all commonly used special constraints in physics engines can be achieved by setting constraint parameters in different directions. Different degrees of freedom can have different constraint modes, such as free, limited, and locked. Free means no constraints are applied, limit means the range and process of the rigid body's motion are limited, and locked means the connected rigid bodies must be relatively stationary. For example, setting the constraint mode of all six degrees of freedom to locked is equivalent to using a fixed constraint to restrict the rigid body. In limit mode, the motion of the rigid body can be restricted to a certain extent, such as limiting the movement of that degree of freedom within a certain range. For the rigid body in limit mode, when it reaches the boundary of the restricted range, it will bounce back. At this time, the rebound strength can be adjusted by setting different rebound coefficients, and the elasticity of the constraint can be achieved by adjusting the parameters of the soft constraint, applying a certain amount of resistance to the motion, etc. + +Rigid bodies connected by configurable constraints can produce the desired relative motion driven by the motor. After the motor is enabled, the rigid body will move to the specified relative position or gradually accelerate to the specified relative velocity and maintain motion. The motor has two different modes, servo mode and normal mode. In servo mode, the driver will make the rigid body move to the specified position or angle and stop after reaching the target position or angle. In normal mode, the driver will gradually accelerate the rigid body to achieve the specified linear velocity or angular velocity, and then maintain that velocity. Another adjustable parameter is the maximum driving force of the driver, which determines how fast the rigid body can accelerate towards the specified target position and velocity. When the maximum driving force is large, it will be difficult for external interference to change the relative motion state between rigid bodies. + +![physics-configurable-main](img/configurable-main.png) +| Properties | Description | +| :-- | :-- | +| **AttachedBody** | The rigid body to which the constraint is attached, that is, the rigid body component under the same node where the current constraint is located | +| **ConnectedBody** | Gets or sets the rigid body to which the constraint is connected, null means it is linked to a static body | +| **EnableCollision** | Gets or sets whether collision is enabled between two bodies connected by a constraint | +| **Axis** | Gets or sets the principle axis of the constraint | +| **SecondaryAxis** | Gets or sets the secondary axis of the constraint (orthogonal to the principle axis) | +| **PivotA** | The relative position of the joint in the local space of its own rigid body | +| **PivotB** | The relative position of the joint in the local space of the connected rigid body | +| **AutoPivotB** | Automatically derivate the pivotB from the pivotA and the relative transform between the two rigid bodies | +| **BreakForce** | Gets or sets the maximum force that can be applied to the constraint before it breaks | +| **BreakTorque** | Gets or sets the maximum torque that can be applied to the constraint before it breaks | +| **LinearLimitSettings** | Gets or sets the linear limit settings | +| **AngularLimitSettings** | Gets or sets the angular limit settings | +| **LinearDriverSettings** | Gets or sets the linear motor settings | +| **AngularDriverSettings** | Gets or sets the angular motor settings | + +Example: When the main axis of the constraint is set to the y-axis of the attached rigid body, and the secondary axis is set to the x-axis of the attached rigid body, the coordinate of the constraint is shown in the figure below. +| local coordinate of the attached body | linear coordinate of the joint | angular coordinate of the joint | +| :-- | :-- | :-- | +| ![configurable-constraint-axis-config](img/configurable_constraint_axis_config.png) | ![constraint-coordinate](img/constraint_coordinate.png) | ![constraint-angular-coordinate](img/constraint_angular_coordinate.png) | + +### Linear limit settings + +![physics-configurable-linear-limit](img/configurable-linear-limit.png) +| Properties | Description | +| :-- | :-- | +| **XMotion** | Gets or sets the constraint mode of the x-axis | +| **YMotion** | Gets or sets the constraint mode of the y-axis | +| **ZMotion** | Gets or sets the constraint mode of the z-axis | +| **Upper** | Gets or sets the upper limit of the linear limit | +| **Lower** | Gets or sets the lower limit of the linear limit | +| **Restitution** | Gets or sets the restitution of the constraint | +| **Enable Soft Constraint** | Gets or sets whether to enable soft constraint | +| **Damping** | Gets or sets the damping of the constraint | +| **Stiffness** | Gets or sets the stiffness of the constraint | + +### Angular limit settings + +![physics-configurable-angular-limit](img/configurable-angular-limit.png) + +| Properties | Description | +| :-- | :-- | +| **Twist Motion** | Gets or sets the constraint mode of the twist axis | +| **Swing Motion1** | Gets or sets the constraint mode of the swing y axis | +| **Swing Motion2** | Gets or sets the constraint mode of the swing z axis | +| **Twist Extent** | Gets or sets the limit angle along the twist axis | +| **Swing Extent1** | Gets or sets the limit angle along the swing y axis | +| **Swing Extent2** | Gets or sets the limit angle along the swing z axis | +| **Twist Restitution** | Gets or sets the restitution of the twist constraint | +| **Swing Restitution** | Gets or sets the restitution of the swing constraint | +| **Enable Soft Constraint Twist** | Gets or sets whether to enable soft constraint for twist | +| **Twist Damping** | Gets or sets the damping of the twist constraint | +| **Twist Stiffness** | Gets or sets the stiffness of the twist constraint | +| **Enable Soft Constraint Swing** | Gets or sets whether to enable soft constraint for swing | +| **Swing Damping** | Gets or sets the damping of the swing constraint | +| **Swing Stiffness** | Gets or sets the stiffness of the swing constraint | + +### Linear driver settings + +![physics-configurable-linear-driver](img/configurable-linear-driver.png) + +| Properties | Description | +| :-- | :-- | +| **XDrive** | Gets or sets the driver mode along the x axis | +| **YDrive** | Gets or sets the driver mode along the y axis | +| **ZDrive** | Gets or sets the driver mode along the z axis | +| **target Position** | Gets or sets the target position | +| **Target Velocity** | Gets or sets the target velocity | +| **Strength** | Gets or sets the maximum driving force | + +### Angular driver settings + +![physics-configurable-angular-driver](img/configurable-angular-driver.png) + +| Properties | Description | +| :-- | :-- | +| **Twist Drive** | Gets or sets the driver mode along the twist axis | +| **Swing Drive1** | Gets or sets the driver mode along the swing y axis | +| **Swing Drive2** | Gets or sets the driver mode along the swing z axis | +| **target Orientation** | Gets or sets the target angle | +| **Target velocity** | Gets or sets the target angular velocity | +| **Strength** | Gets or sets the maximum driving force | + +## Drive Mode + +Both linear and angular motors can have their drive modes adjusted by sub-axis, there are 3 drive modes: + +- **DISABLED**: Disabled mode, in which the corresponding split-axis is not affected by the motor driving force or torque. +- **SERVO**: Attempts to move/rotate to a specified position/angle. + - Linear motors: The Drive attributes of X, Y and Z correspond to the three parameters of the Target Position. + + ![servo.png](./img/servo.png) + + For example, if **XDrive** is **SERVO** and the X component of **Target Position** is 10, then the constraint will try to move to 10. + + - Angle motors: Twist Drive, Swig Drive1, Swig Drive2 correspond to the three parameters of Target Orientation respectively +- **INDUCTION**: drive the constraint to the specified speed/angular velocity. + - Linear motors: The Drive attributes of X, Y, and Z correspond to the three parameters of Target Velocity respectively + + The Drive attribute for X, Y, and Z corresponds to the three parameters of Target Velocity. + + ![induction.png](./img/induction.png) + + For example, if **XDrive** is **INDUCTION** and the X component of **Target Velocity** is 10, then the constraint will try to move its velocity to 10. + + - Angle motors: Twist Drive, Swig Drive1, Swig Drive2 correspond to the three parameters of Target Velocity. + +For configurable constraint interface, please refer to [ConfigurableConstraint API](%__APIDOC__%/en/class/physics.ConfigurableConstraint). diff --git a/versions/4.0/en/physics/physics-engine.md b/versions/4.0/en/physics/physics-engine.md new file mode 100644 index 0000000000..c0380f7a6d --- /dev/null +++ b/versions/4.0/en/physics/physics-engine.md @@ -0,0 +1,116 @@ +# Physics Engines + +Click **Project -> Project Settings -> Feature Cropping** in the menu bar at the top of the editor, and in **3D -> Physics System** you can choose the appropriate physics engine for your project to develop, or switch between them at any time during the development process. The physics engines currently supported by Creator include **Bullet (ammo.js)**, **cannon.js**, **PhysX** and **Builtin**, and the default is **Bullet (amo.js)**. + +![Physics engine options](img/physics-module.jpg) + +## Introduction to the Physics Engines + +### Bullet(ammo.js) + +ammo.js ([GitHub](https://github.com/cocos-creator/ammo.js)) is the [Bullet](https://github.com/bulletphysics/bullet3) version of asm.js/wasm for the physics engine, compiled by the [emscripten](https://github.com/emscripten-core/emscripten) tool. + +The **ammo.js** module is large (~1.5MB), but it has good physics functionality and better performance, and we will be putting more work into it in the future. + +### Builtin + +**Builtin** is a built-in physics engine providing **collision detection only**. Compared to other physics engines, it does not have complex physics simulation calculations, so if your project does not need this part of the physics simulation, it is recommended to use **Builtin** to make the game a smaller package. + +When using **Builtin** for development, please note the following: + +- Only **trigger** trigger type events. +- The **IsTrigger** property in collider components is invalid, and all colliders can only be used as [triggers](physics-event.md). +- The `Material` property of the collider component is invalid. +- `Attached` in collider components is always `null`. +- [Rigidbody](physics-rigidbody.md) is invalid. +- [Constant Force Component](physics-constantForce.md) is invalid. +- [Constraints](physics-constraint.md) are invalid. + +### cannon.js + +**cannon.js** ([GitHub](https://github.com/cocos-creator/cannon.js)) is an open source physics engine, developed in JavaScript and implementing a relatively comprehensive physics simulation.The size of the **cannon.js** module is about **141KB**. + +When the selected physics engine is **cannon.js**, you need to add a **Rigidbody** component to the node to perform the physics simulation. Then add **Colllider** components as required, and the node will add the corresponding collider for detecting collisions with other colliders. + +The current cannon.js support is as follows: + +- [Rigidbody](physics-rigidbody.md) +- [Box/Sphere Collider](physics-collider.md) +- Trigger and collision events +- Physics materials +- Raycast detection + +### PhysX + +**PhysX** ([GitHub](https://github.com/NVIDIAGameWorks/PhysX)) is an open-source real-time commercial physics engine developed by NVIDIA Corporation, which offers a full range of functional features and high stability, as well as excellent performance. + +Cocos Creator currently supports **PhysX** version 4.1, which allows for use in most native and web platforms. When publishing to native platforms, PhysX physics is recommended for better physics performance, especially when publishing to iOS. + +However, **PhysX** is currently too large a package (~5MB) and has some limitations of its own that prevent good support for some platforms, including + +- Various mini-game platforms with package size limitations +- Android x86 devices + +Some newer platforms and devices, such as HarmonyOS devices, will be supported in the future, please pay attention to the update announcement. Apple M1 (Silicon) architecture devices are already supported in v3.4. + +In addition, the Douyin platform provides the underlying native physics feature, so it is also available in Douyin mini-games, please refer to [Publish to Douyin Mini Games - Native Physics](../editor/publish/publish-bytedance-mini-game.md). + +### Different physical back-end collision shape support + +| Features | builtin | cannon.js | Bullet | PhysX +|:--------|:--------|:----------|:--------|:----| +| Center-of-mass | ✔ | ✔ | ✔ | ✔ | +| Box, sphere | ✔ | ✔ | ✔ | ✔ | ✔ +| Capsule | ✔ | can be pieced together with base shapes | ✔ |✔ | +| Convex | |✔ |✔ | ✔ | +| Static terrain, static planes | | ✔ | ✔ |✔ +| Static meshes | | very limited support | ✔ |✔ | +| Cones, Cylinders | ✔ | ✔ | ✔ | ✔ | +| Monomorphic | | Limited support | ✔ |✔ +| Composite shapes | ✔ | ✔ | ✔ |✔ +| Raycast detection, mask filtering | ✔ | ✔ | ✔ | ✔ +| Multi-step simulation, collision matrix | ✔ | ✔ | ✔ |✔ | +| Trigger events | ✔ | ✔ | ✔ | ✔ | ✔ | +| Auto-sleep | | ✔ | ✔ |✔ | +| Collision events, collision data | | ✔ | ✔ | ✔ +| Physics Materials | ✔ | ✔ | ✔ | ✔ | +| Static, kinematics | ✔ | ✔ | ✔ | ✔ | +| Dynamics | ✔ | ✔ | ✔ | ✔ | +| Point-to-point, hinge constraints (experimental) | | ✔ | ✔ | ✔ +| wasm | | | ✔ |✔ | + +## Not using physics + +If you don't need to use any physics-related components and interfaces, you can uncheck the physics system option to make the game a smaller package. + +> **Note**: If the physics system option is unchecked, the project will not be able to use physics-related components and interfaces, otherwise it will report an error at runtime. + +## Performance of physics engine + +The performance of **Bullet** and **PhysX** physics is compared for various trivia platforms and native platforms. + +- On both native and Douyin mini game platforms, the best performance is obtained using **PhysX** physics. +- On all types of other mini game platforms, the best performance is obtained with **Bullet** physics. + +## Differences in the effect of the physics engine + +Different physics engines have different internal designs and algorithms, so there will be some cases where the parameters are the same but the effects are different. + +1. the **Damping** property on rigid body components + + The difference is caused by the fact that PhysX physics uses a different damping algorithm model. However, this difference has been eliminated internally, and if you need to synchronize the damping values previously set in PhysX, you can refer to the following code for conversion. + + ```ts + const dt = PhysicsSystem.instance.fixedTimeStep; + const newDamping = 1 - (1 - oldDamping * dt) ** (1 / dt); + ``` + +2. The **Factor** property on rigid body components + + The **Linear Factor** and **Angular Factor** properties on rigid body components only have a fixed effect on PhysX physics, because PhysX physics only fixes the rigid body degrees of freedom and does not provide a scaling factor for the rigid body velocity. This difference will be removed internally. + +3. Physics Materials + + PhysX physics materials support static friction factor, dynamic friction factor, and elasticity factor. Compared to physics materials in Creator, it lacks dynamic friction coefficient. This coefficient is currently the same as the static friction coefficient, and this part of the difference is not available for conversion at this time. + +In addition to the points mentioned above, there are also other algorithmic differences, such as numerical integration methods, LCP solution algorithms, solution accuracy, etc., so there will always be different effects, but these are less easily perceived in actual project development. diff --git a/versions/4.0/en/physics/physics-event.md b/versions/4.0/en/physics/physics-event.md new file mode 100644 index 0000000000..3e969de8a4 --- /dev/null +++ b/versions/4.0/en/physics/physics-event.md @@ -0,0 +1,123 @@ +# Physical Event + +## Trigger and Collider + +![img](img/isTrigger.jpg) + +Collider property **IsTrigger** determines whether the component is a trigger or a collider. When **IsTrigger** is set to `true`, the component is a trigger. Triggers are only used for collision detection and triggering events, and are ignored by the physics engine. With `false` set by default, the component is a collider and can be combined with [Rigidbody](physics-rigidbody.md) to produce collision effects. + +The difference between the two is as follows. + +- Triggers do not do more detailed detection with other triggers or colliders. +- Colliders will do finer detection with colliders and will generate collision data such as collision points, normals, etc. + +## Trigger events + +Trigger events currently include the following three types: + +| Event | Description | +| :--------------- | :------- | +| `onTriggerEnter` | Triggered when the trigger starts | +| `onTriggerStay` | Triggered frequently when the trigger is held | +| `onTriggerExit` | Triggered at the end of a trigger | + +The collision pairs that can generate trigger events are + +| type | Static | Kinematic | Dynamic | +| :--------- | :------- | :--------- | :--------- | +| Static | | ✔ | ✔ | +| Kinematic | ✔ | ✔ | ✔ | +| Dynamic | ✔ | ✔ | ✔ | ✔ | + +> **Note**: To receive a trigger event, both must have a **Collider** component and at least one of them must be a trigger type. When using a physics engine that is not **builtin** physics engine, you also need to make sure that at least one object with a non-static rigid body (objects with only collider components and no rigid body components are considered to hold a static rigid body), whereas the **builtin** physics engine does not have this restriction. + +### Listening to Trigger Events + +```ts +// Make sure that the BoxCollider component is added to the node here +import { BoxCollider, ITriggerEvent } from 'cc' + +public start () { + let collider = this.node.getComponent(BoxCollider); + collider.on('onTriggerStay', this.onTriggerStay, this); +} + +private onTriggerStay (event: ITriggerEvent) { + console.log(event.type, event); +} +``` + +## Collision Events + +Collision events are generated based on collision data, and no collision data is generated between rigid bodies of static types. + +Currently collision events include the following three types: + +| Event | Description | +| :----------------- | :------- | +| `onCollisionEnter` | Triggered when collision starts | +| `onCollisionStay` | Triggered continuously when the collision is held. | +| `onCollisionExit` | triggered when collision ends | + +Where the collision pairs that can generate collision events are + +| type | Static | Kinematic | Dynamic | +| :--------- | :------- | :--------- | :--------- | +| Static | | ✔ | ✔ | +| Kinematic | ✔ | ✔ | ✔ | +| Dynamic | ✔ | ✔ | ✔ | ✔ | + +> **Note**: The collision event must be received if both have a collider component, at least one is a non-static rigid body, and a non-builtin physics engine is used. + +### Listening to Collision Events + +```ts +import { Collider, ICollisionEvent } from 'cc' + +public start () { + let collider = this.node.getComponent(Collider); + // Listening to 'onCollisionStay' Events + collider.on('onCollisionStay', this.onCollision, this); +} + +private onCollision (event: ICollisionEvent) { + console.log(event.type, event); +} +``` + +> **Note**: Currently collision events are in physical elements, and all collider components on that element receive collision events. + +## Difference between Trigger Events and Collision Events + +- Trigger events are generated by triggers, collision events are generated based on collision data. +- Trigger events can be generated by a trigger and another trigger/collider. +- Collision events need to be generated by two colliders, and at least one of them is a dynamic rigid body. + +## Continuous Collision Detection + +Compared to the continuous real world, the physics engine simulation is actually discrete, which means that the physics engine takes 1/60th of a second or some other defined time to sample once. Therefore, for faster objects, the physics engine may not be able to detect the collision results correctly, to solve this phenomenon, you can turn on continuous physics detection (CCD). + +Turn on continuous collision detection in the engine as follows: + +```ts +const rigidBody = this.getComponent(RigidBody); +rigidBody.useCCD = true; +``` + +Refer to [Continuous Collision Detection](physics-ccd.md) for a more detailed description. + +## Event Trigger Rules + +The generation of collision events will vary depending on the type of rigid body, collider or trigger, the results are organized here as shown below. + +> **Note**: The rigid bodies marked here all carry collider and are configured to allow collisions within the collision matrix. + +| - | Static | Kinematic | Dynamical | Collider | Trigger | +| :--------- | :------- | :--------- | :--------- | :----- | :----- | +| Static | | ✔ | ✔ | | | +| Kinematic | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | +| Dynamic | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | +| Collider | ✔ | ✔ | ✔ | | +| Trigger | ✔ | ✔ | ✔ | | + +> **Note**: The collision event between two triggers is not supported in the PhysX backend. diff --git a/versions/4.0/en/physics/physics-example.md b/versions/4.0/en/physics/physics-example.md new file mode 100644 index 0000000000..1b3a52d4f0 --- /dev/null +++ b/versions/4.0/en/physics/physics-example.md @@ -0,0 +1,73 @@ +# Physics Examples + +## Archery Case + +1. Combining crosses with base shapes - composite shapes + + In the following figure, a cross is combined with two box shapes, and all the colliding bodies on the node are combined into a cross shape, which is the most basic way to achieve a shape with a concave surface. + + ![composite-shape](example-img/composite-shape.jpg) + + The easy misuse is to add collision bodies to multiple nodes to put together the cross, hoping that it can keep the overall structure in motion after the collision, which is not possible in the current structure and can only be achieved by adding collision bodies to individual nodes. + +2. Shooting and recovering arrows - kinematics, dynamics, events + + The first step of archery is to draw the bow, and the arrow needs to follow the elastic rope skeleton completely, so we don't want the arrow to be affected by the physical rules. + + ![bow-pulling](example-img/bow-pulling.gif) + + The general process of recovering an arrow is to restore it to the bow once it has touched the trigger area after it has been shot. This can be achieved by creating a listening area, first by piecing together the area using the collider component and checking `IsTrigger` on the collider (the ground in the image below, the part circled in blue, is the listening area). + + ![recycle-area](example-img/recycle-area.jpg) + + The above error-prone areas are: + + 1. You want to modify the transformation information to manipulate the dynamics (Dynamic) type of rigid body motion, which should be controlled by the values of the physical layer such as velocity, force or impulse. + + 2. Using a static rigid body to listen for events, a static rigid body will only generate events with a rigid body of type Kinematic or Dynamic. If both are static, no events will be generated. + + 3. Listen for events with only OnTriggerEnter, but mistakenly think that OnTriggerStay and OnTriggerExit are included. + +3. Aiming - collision matrix (filter detection), raycast detection, static plane + + Aiming is the step before shooting an arrow. The collimator is on the ray where the arrow is pointing, a static plane collider is added in front of the cross, and then the position of the collimator can be obtained using raycast detection. + + ![arm](example-img/arm.jpg) + + The static plane is only used for raycast detection, creating a grouping specifically for it and not with objects such as arrows, apples, etc. This is the most general method of performance optimization. + + ![config](example-img/config.jpg) + + When calling the raycast detection method, set the incoming mask to only and static plane detection, i.e. `0b10` (binary representation). + + The above-mentioned error-prone areas are: + + 1. Not being able to distinguish between groupings on rigid components and layers on nodes. The two concepts are similar, but the users are different, the user of the grouping is the physical module and the user of the layer is the rendering module. + + 2. Not understanding the mask properly and not knowing what value to pass. Here is a little trick, to a mask that can filter out `Others` for example, first Others index value is 2, then just let the binary mask from right to left order of the `2` bit for `1`, you can let `Others` through the filter, that is `ob100` (here it is strongly recommended not to change the index of the grouping arbitrarily). + + 3. Mistakenly assume that the return value of the raycast detection interface is the hit data. There is a dedicated interface for getting the result, which is designed here to emphasize that this is a reused object. To reduce garbage memory, each call to the interface will only update their data, rather than regenerate a new one (if a persistent record is needed, then a copy can be cloned). + +4. Shooting apples - static grid, convex package, multi-step simulation (step size adjustment) + + The average apple comes with a concave surface, and dealing with a good concave class or a model with continuous smooth irregular surfaces are very tricky, this is because the current mature theory and techniques are built on top of the world of discrete, convex packages (differential approximation in calculus to represent the differential is the most typical example). + + In real-time physics engines, such objects can only be supported up to the rigid body level of static or kinematic types, and there is nothing that can be done about dynamics. Unfortunately, however, real apple motion is strongly dependent on dynamics, and in this case the only way to participate in the simulation is to fill the apple with a mesh collider in the form of a convex package (with convex checked) and a dynamic rigid body, using an approximation. + + ![arm-apple](example-img/arm-apple.gif) + + The motion performance is very much related to the simulation parameters, and penetration is the most representative phenomenon, which can be achieved by reducing the step length and increasing the number of steps, with a little trick to adjust the step length: enter the fractional equation, `1/Frame`, where `Frame` represents the frame rate. + + ![set-step](example-img/set-step.gif) + + The above mistakes can be easily made by: + + 1. Dynamic rigid bodies added to a mesh collider with `convex` unchecked are penetrating with other objects or not reacting at all, this is a typical misuse, only those with `convex` checked will support dynamic rigid bodies. + + 2. Turning on `convex` for a model with a very large number of vertices will increase the number of faces of the convex packet, which has a great impact on the performance, and it is not necessary to have a convex packet with a large number of faces. + + 3. It is normal that the contact at the concave side of the model does not fit well after opening the convex packet, which is now solved by the real-time technique of combining the model with multiple convex packets, as shown in the following figure. + + ![set-step](example-img/convex.jpg) + + 4. Only the step length is adjusted, but not the number of steps, both of which need to work with each other to be effective. The tip is that the number of steps can be set to a larger value at will, and the step length is adjusted according to the maximum speed value; the larger the value, the smaller the step length should be. diff --git a/versions/4.0/en/physics/physics-group-mask.md b/versions/4.0/en/physics/physics-group-mask.md new file mode 100644 index 0000000000..9e65569c20 --- /dev/null +++ b/versions/4.0/en/physics/physics-group-mask.md @@ -0,0 +1,123 @@ +# Group and Mask + +Group and masks are necessary for physical collision detection between objects. A group can be simply treated as the group a collision object is in, and a mask can be simply treated as the grouping objects that the collision object needs to collide with. + +## Collision Detection Principle + +In Cocos Creator, collision detection is done in binary by bit, by "matching" the group value to the mask value to determine whether the condition is satisfied or not. The calculation of whether collision detection is allowed between objects is as follows: + +```ts +(GroupA & MaskB) && (GroupB & MaskA) +``` + +As you can see from this formula, group A needs to be in the mask list of group B and group B needs to be in the mask list of group A in order for collision detection to occur between the two objects. How to combine the binary operation with the formula for allowing collision detection is the following part to understand. But before that, you need to configure [Collision Matrix](physics-configs.md#collision-matrix) in **Project Settings -> Physics -> Collision Matrix**. + +![set-collider-config](img/set-collider-config.png) + +According to the configuration above, Cocos Creator will parse the data into the following values (shown here only for the part of the explanation). + +- **DEFAULT**: **Index** value is `0`, the actual value of grouping is `1<<0=1`, the binary value is `0000 0001`; the actual value of mask value is `1<<0=1`, the binary value is `0000 0001`. +- **SELF_PLANE**: **Index** value is `1`, the actual value of grouping is `1<<1=2`, the binary value is `0000 0010`; the actual value of mask value is `(1<<3)+(1<<4)=24`, the binary value is `0001 1000`. +- **ENEMY_BULLET**: **Index** value is `4`, the actual value of the grouping is `1<<4=16`, the binary value is `0001 0000`; the actual value of the mask value is `1<<1=2`, the binary value is `0000 0010`. + +Based on the data it is possible to do a calculation of whether there is a collision between the groups: + +- Does the group **SELF_PLANE** collide with the group **DEFAULT**? + + ![cant-collider](img/cant-collider.png) + + The final value based on the above calculation is `0`, so there is no collision between the two groups. + +- Whether the group **SELF_PLANE** collides with the group **ENEMY_BULLET**? + + ![can-collider](img/can-collider.png) + + The final value based on the above calculation is greater than `0`, so there will be a collision between the two groups. + +> **Note**: `<<` The left shift operator, which is a type of bitwise operator, shifts to the left by pushing in 0 from the right and shedding the leftmost bit.s + +## Dynamically Set Group and Masks + +### Define Groups + +Usually, in game development, you need to set collision-ready groups before the collision occurs and handle the related logic when the collision occurs. In Cocos Creator, all collision data is obtained as numeric values, which is not conducive to judgment during development. Therefore, it is possible to clearly know the meaning of each string of numbers by defining grouping objects or enumerations. + +In can use the left shift operator (<<) to set the group or mask, and the corresponding value of either grouping object or grouping enumeration should be the same as the value defined in the collision matrix, otherwise there may be data inconsistency, which leads to judgment failure. + +Way 1: Defined in an **object** + +```ts +export const PHY_GROUP = { + DEFAULT: 1 << 0, + SELF_PLANE: 1 << 1, + ENEMY_PLANE = 1 << 2, + SELF_BULLET = 1 << 3, + ENEMY_BULLET = 1 << 4, + BULLET_PROP = 1 << 5, +}; +``` + +Way 2: Defined in a **enum** + +```ts +enum PHY_GROUP { + DEFAULT = 1 << 0, + SELF_PLANE = 1 << 1, + ENEMY_PLANE = 1 << 2, + SELF_BULLET = 1 << 3, + ENEMY_BULLET = 1 << 4, + BULLET_PROP = 1 << 5, +}; + +// If the enum needs to be displayed in the Inspector panel, you need to import the Enum function from the cc module and register the defined enum into the editor +Enum(PHY_GROUP); +``` + +> **Note**: For historical reasons, the **Enum** function has special treatment for **-1**, so do not define properties with values of **-1** if you are not familiar with them. + +### Set/Get Group + +```ts +// This case uses the enumeration defined in the "Define Groups" section above +const rigid = this.getComponent(RigidBody); +// Equivalent to rigid.setGroup(1 << 1) or rigid.setGroup(1) +rigid.setGroup(PHY_GROUP.SELF_PLANE); + +const group = rigid.getGroup(); +``` + +### Add/Remove Groups + +```ts +// If the current group is not defined in the collision matrix, it can also be added dynamically +const group = 1 << 7; +const rigid = this.getComponent(RigidBody); +rigid.addGroup(group); +rigid.removeGroup(group); +``` + +### Set/Get Mask + +```ts +const rigid = this.getComponent(RigidBody); +const mask = (1 << 0) + (1 << 1); // Equivalent to 1 << 0 | 1 << 1 +rigid.setMask(mask); +rigid.getMask(); +``` + +> **Note**: Here you need to pay attention to the priority of the operator. For example, 3 + 1 << 2 and 3 + (1 << 2) do not compute equal values, and the operator + has a higher priority than <<. + +#### Using Masks + +Masks can be defined based on grouping, e.g. + +- Define a mask that detects only **DEFAULT** `const maskForGroup1 = PHY_GROUP.DEFAULT;` +- Define a mask that detects **DEFAULT** and **SELF_PLANE** `const maskForGroup01 = PHY_GROUP.DEFAULT | PHY_GROUP.SELF_PLANE;` +- Define a mask that is not detected by all groups `const maskForNone = 0;` +- Define a mask that is detected by all groups `const maskForAll = 0xffffffff;` + +### View Group or Mask in Binary + +See the binary string representation by `(value >>> 0).toString(2)`. + +![View binary](img/mask-all.jpg) diff --git a/versions/4.0/en/physics/physics-material.md b/versions/4.0/en/physics/physics-material.md new file mode 100644 index 0000000000..3445865896 --- /dev/null +++ b/versions/4.0/en/physics/physics-material.md @@ -0,0 +1,94 @@ +# Physics Material + +A physics material is an asset that records information about the surface of an object that is used to calculate the frictional and force of restitution on colliding objects, etc. + +## Create Physics Material + +### Create in Editor + +Physics materials can be created by right-clicking anywhere in the **Inspector** panel or by clicking between the **+** button: + +![create Physics material](img/material-create-pmtl.png) + +### Creating by Code + +Physics materials can also be instantiated by code: + +```ts +import { PhysicsMaterial } from 'cc'; + +let newPMtl = new PhysicsMaterial(); +newPMtl.friction = 0.1; +newPMtl.rollingFriction = 0.1; +newPMtl.spinningFriction = 0.1; +newPMtl.restitution = 0.5; +``` + +## Properties + +Physics material properties are shown in the following figure: + +![physics-mat-panel](img/physics-mat-panel.png) + +| Properties | Description | +| :-- | :-- | +| **Friction** | The friction factor | +| **RollingFriction** | The rolling friction factor | +| **SpinningFriction** | The spinning friction factor | +| **Restitution** | Resilience factor | + +When in contact with other surfaces, these coefficients are used to calculate the corresponding friction and force of restitution. + +## Use Physics Material + +Currently, physics materials are set on a collider basis, and each **Collider** has a **Material** property (when not set, the **Collider** will refer to the default physics material in the physics system). + +Applying to **Collider** is also divided into two ways: editor operations and code operations. + +For in-editor operations, simply drag and drop the asset into the **cc.PhysicalicMaterial** property box, as shown below. + +![apply-physic-material](img/apply-pmtl.jpg) + +Operate in the code: + +```ts +import { Collider } from 'cc'; + +let collider = this.node.getComponent(Collider); +if (collider) { + collider.material = newPMtl; + collider.material.rollingFriction = 0.1; +} +``` + +## Shared Materials + +In the physics system, physics materials have two states, shared materials and instanced materials. + +- Shared material: Different colliders that share the same material, and modifications to that material affect all colliders holding that material. By default, colliders are initialized with the engine's default physics material. This can be accessed through `sharedMaterial`, with the following code example. + + ```ts + import { Collider } from 'cc'; + let collider = this.node.getComponent(Collider); + if (collider) { + let sharedMaterial = collider.sharedMaterial; + + collider.sharedMaterial.friction = 0.5 + + collider.sharedMaterial = newPMtl; + } + ``` + +- Instanced material: Only used by this collider, modifying this material will not affect other colliders. + + If a collider's physics material is in a shared state, a new physics material will be generated when `getter` of `material` is called. + + The following code demonstrates how calling `getter` on a collider changes the material to an instanced state when the physics material is in a shared state. + + ```ts + import { Collider } from 'cc'; + let collider = this.node.getComponent(Collider); + if (collider) { + const material = collider.material; + } + ``` diff --git a/versions/4.0/en/physics/physics-raycast.md b/versions/4.0/en/physics/physics-raycast.md new file mode 100644 index 0000000000..aeb674dc81 --- /dev/null +++ b/versions/4.0/en/physics/physics-raycast.md @@ -0,0 +1,187 @@ +# Raycast Detection + +In this document, we will discuss how to use raycast and line stripe cast to detect colliders in physics world. + +## Raycast Detection + +Raycast detection is a **intersection determination** of one ray and another shape, as shown in the figure below. + +![illustration](img/raycast.jpg) + +### Construct Rays + +A **ray** consists of a **starting point** and a **direction**. There are several more common ways to construct a ray, as follows: + +1. via the constructor of **start** + **direction**, **ray** or the static interface `create`. + + ```ts + import { geometry } from 'cc'; + + // Construct a ray starting from (0, -1, 0) and pointing to the Y-axis + // the first three arguments are the starting point, the last three arguments are the direction + const outRay = new geometry.Ray(0, -1, 0, 0, 0, 1, 0); + + // or by static method 'create' + const outRay2 = geometry.Ray.create(0, -1, 0, 0, 0, 1, 0); + ``` + +2. via the static interface `fromPoints` of **start point** + **another point on ray**, **ray**: + + ```ts + import { geometry, math } from "cc"; + // Construct a ray that points to the Z-axis from the origin + const outRay = new geometry.Ray(); + geometry.Ray.fromPoints(outRay, math.Vec3.ZERO, math.Vec3.UNIT_Z); + ``` + +3. Use the camera to construct a ray emitted from the camera origin to a point on the screen. + + ```ts + import { geometry, Camera } from "cc"; + // Take the example of a script mounted under Camera + const camera = this.node.getComponent(Camera); + // Get a ray emitted from the screen at (0, 0) + const outRay = new geometry; + camera?.screenPointToRay(0, 0, outRay); + ``` + + > **Note**. + > 1. First you need to get a reference to a camera component or camera instance. + > 2. The order of the interface parameters exposed by the camera component and the camera instance are different. + +### Interface Introduction + +Cocos Creator provides a set of raycast detection functions based on the physics engine. + +The interface is currently provided by [**PhysicsSystem**](%__APIDOC__%/en/class/physics.PhysicsSystem) and has the following two classes. + +- `raycast` : Detects all colliders and records all detected results, obtained via `PhysicsSystem.instance.raycastResults`. The interface returns a boolean value indicating whether if the ray intersects with any collider or not. +- `raycastClosest`: detects all colliders and records the result of the detection with the shortest distance to the ray, obtained via `PhysicsSystem.instance.raycastClosestResult`. Also returns a boolean value same as `raycast` method. + +> **Note**. +> 1. The detected object is a physical collider, and the corresponding collider component in the scene panel is a collider component, e.g. **BoxCollider**. +> 2. The object returned by the detection result is read-only and reused, and the corresponding result is updated after each call to the detection interface. + +Common examples of raycast detection of nearest objects based on camera position and screen coordinates. + +```ts +let ray = new geometry.Ray(); +this.camera.screenPointToRay(eventMouse.getLocationX(), eventMouse.getLocationY(), ray); +// The following parameters are optional +const mask = 0xffffffff; +const maxDistance = 10000000; +const queryTrigger = true; + +if (PhysicsSystem.instance.raycastClosest(ray, mask, maxDistance, queryTrigger)) { + const raycastClosestResult = PhysicsSystem.instance.raycastClosestResult; + const hitPoint = raycastClosestResult.hitPoint + const hitNormal = raycastClosestResult.hitNormal; + const collider = raycastClosestResult.collider; + const distance = raycastClosestResult.distance; +} +``` + +The following code demonstrates how multiple objects can be detected: + +```ts +const worldRay = new geometry.Ray(0, -1, 0, 0, 1, 0); +// The following parameters are optional +const mask = 0xffffffff; +const maxDistance = 10000000; +const queryTrigger = true; + +const bResult = PhysicsSystem.instance.raycast(worldRay, mask, maxDistance, queryTrigger); +if(bResult){ + const results = PhysicsSystem.instance.raycastResults; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + const collider = result.collider; + const distance = result.distance; + const hitPoint = result.hitPoint; + const hitNormal = result.hitNormal; + } +} +``` + +#### Parameter Description + +- `worldRay`: the ray in the world space +- `mask`: [mask](physics-group-mask.md) for filtering, you can pass in the group to be detected, default is 0xffffffff +- `maxDistance`: the maximum detection distance, default is 10000000, do not pass `Infinity` or `Number.MAX_VALUE` at this time +- `queryTrigger`: whether to detect the trigger + +#### Result Description + +The result of the ray detection is stored by [PhysicsRayResult](%__APIDOC__%/en/class/physics.PhysicsRayResult), mainly with the following information. + +- `collider`: the collider that hit +- `distance`: the distance between the hit point and the start of the ray +- `hitPoint`: the hit point (in the world coordinate) +- `hitNormal`: the normal of the face where the hit point is located (in the world coordinate) + +Related test cases can be found in the [GitHub repo](https://github.com/cocos-creator/example-3d/blob/v3.8/physics-3d/assets/cases/scenes/csae-physics-raycast.scene). + +## Line Stripe Cast + +Line stripe cast/sample points cast is supported in Cocos Creator since v3.7. There are two methods `lineStripCast` and `lineStripCastClosest` which can be used to cast a line stripe in the physics world to detect colliders. + +### Interface description + +These two methods which encapsulated the `raycast` method are convenient to use used when checking whether one or more colliders are hit by the line stripe provided by the `samplePointsWorldSpace` parameter. + +- lineStripCast: Collision detect all collider and record all the detected results + + ```ts + lineStripCast (samplePointsWorldSpace: Array, mask = 0xffffffff, maxDistance = 10000000, queryTrigger = true): boolean; + ``` + +- lineStripCastClosest: Collision detect all collider, and record the ray test results with the shortest distance + + ```ts + lineStripCastClosest (samplePointsWorldSpace: Array, mask = 0xffffffff, maxDistance = 10000000, queryTrigger = true): boolean; + ``` + +#### Parameters + +- `samplePointsWorldSpace`: Sample points/line stripe in world space +- `mask`: [mask](physics-group-mask.md) for filtering, you can pass in the group to be detected, the default is 0xffffffff +- `maxDistance`: the maximum detection distance, default is 10000000, do not pass `Infinity` or `Number.MAX_VALUE` at this time +- `queryTrigger`: whether to detect the trigger + +#### Return values + +It will return true when the line stripe intersects with other colliders, but the result of detection is stored in `PhysicsSystem.Instance.lineStripCastClosestResult` and `PhysicsSystem.Instance.lineStripCastResults` which can be referred in the following contents. + +### Example + +Use `lineStripCastClosestResult` 和 `lineStripCastResults` to fetch the invoking result if these methods return true. + +```ts +if (PhysicsSystem.instance.lineStripCastClosest(sampleArray, ...)) { + const result = PhysicsSystem.instance.lineStripCastClosestResult; + ... +} +``` + +The properties of lineStripCastClosestResult which type is PhysicsLineStripCastResult inherited from PhysicsRayResult are as follows: + +- `id`: The index of the line stripe when collided +- `collider`: the collider that hit +- `distance`: the distance between the hit point and the start of the ray +- `hitPoint`: the hit point (in the world coordinate) +- `hitNormal`: the normal of the face where the hit point is located (in the world coordinate) + +The description of `lineStripCast` property is an array of `PhysicsLineStripCastResult` which is detailed from the above. The code example is as follows: + +```ts +if (PhysicsSystem.instance.lineStripCast(sampleArray, ... )) { + const results = PhysicsSystem.instance.lineStripCastResults; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + ... + } +} +``` + +If you want to cast other types of geometry, please refer to [Geometry Cast Detection](./physics-sweep.md). diff --git a/versions/4.0/en/physics/physics-rigidbody.md b/versions/4.0/en/physics/physics-rigidbody.md new file mode 100644 index 0000000000..e07654dd61 --- /dev/null +++ b/versions/4.0/en/physics/physics-rigidbody.md @@ -0,0 +1,214 @@ +# Rigidbody + +Rigid bodies are the basic objects that make up the physics world and allow game objects to move in a physically controlled manner. For example, a rigid body can make a game object do free fall under the influence of gravity, or it can make a game object simulate real-world physics phenomena under the action of forces and torques. + +## Adding Rigid Body + +### Adding via Editor + +Click **Add Component -> Physics -> RigidBody** under **Inspector** panel to add a rigid body component to the node. + +![add-rigidbody-in-inspector](img/add-rigidbody-in-inspector.jpg) + +### Via Code + +```ts +import { RigidBody } from 'cc' + +const rigidbody = this.node.addComponent(RigidBody); + +const rigidBody = this.node.getComponent(RigidBody); +``` + +For more information, please refer to the [RigidBody API](%__APIDOC__%/en/class/physics.RigidBody). + +### When to Add Rigid Bodies + +1. Configure collision grouping and make it effective. +2. The object needs to have kinematic or kinetic behavior. + +> **Note**: The object needs to have full physical properties provided the object has both **rigid body** and **collider**, and its center-of-mass position and collider shape are adjusted. + +## Rigidbody Properties + +![rigidbody](img/rigid-body.jpg) + +| Properties | Description | +| :-------- | :------------------------------------------------------------------------------- | +| **Group** | Group of the Rigidbody +| **Type** | Type of the Rigidbody, could be the followins types:
**DYNAMIC**
**STATIC**
**KINEMATIC**
Please refer to the following section for details | + +The following properties only take effect when **Type** is set to **DYNAMIC**. + +| Properties | Description | +| :----------------- | :--------------------------------------------------- | +| **Mass** | The mass of the rigid body, the value must be greater than **0** | +| **AllowSleep** | Whether to allow auto-sleep | +| **LinearDamping** | Linear damping, used to attenuate linear velocity, the larger the value, the faster the attenuation | +| **AngularDamping** | Angular damping, used to attenuate the angular velocity, the larger the value, the faster the attenuation | +| **UseGravity** | Whether to use gravity | +| **LinerFactor** | Linearity factor to scale the physics values (velocity and force) in each axis direction | +| **AngularFactor** | Angular factor for scaling physical values in each axis direction (velocity and force) | + +Please refer to [RigidBody API](%__APIDOC__%/en/class/physics.RigidBody) for rigid body component interface. + +## RigidBody Types + +The current rigid body types include **DYNAMIC**, **KINEMATIC** and **STATIC**. + +![rigidbody-type](img/rigidybody-type.png) + +- **STATIC**: static rigid body. Can be a game object with a manually set rigid body type, or a game object with a collider and no rigid body. If a node has only colliders and no rigid bodies added by default, then the node can be considered to be using **STATIC** by default. Static rigid bodies are used in most cases for game objects that always stay in one place and do not move easily, e.g. buildings. If the object needs to move continuously, it should be set to **KINEMATIC** type. Static rigid objects do not produce physical behavior when they collide with other objects and, therefore, do not move. +- **DYNAMIC**: kinetic rigid body. Has colliding bodies and non-moving rigid bodies. Rigid body collisions are fully simulated by the physics engine and can be performed by **FORCE** on moving objects (need to guarantee mass greater than 0). For example: a snooker game where the mother ball rolls after hitting the ball and collides with other balls. +- **KINEMATIC**: kinematic rigid body. Has a collider and a kinematic rigid body that can be directly transformed by moving the transformation properties of the rigid object, but does not respond to forces and collisions like a kinematic rigid body, and is usually used to represent objects with platform motion like elevators. It is similar to static rigid bodies, with the difference that moving kinematic rigid bodies exert frictional forces on other objects and wake up other rigid bodies on contact. + +**Example**: A simple physics simulation to illustrate the performance that various types of rigid bodies have. The square below uses static rigid bodies in white, kinematic rigid bodies in blue, and kinetic rigid bodies in yellow. Both white and blue are manipulated transformation information, and it is obvious to see several manifestations. +1. There is a penetration between white and blue. +2. The white static object can also be in motion. +3. Two yellow squares behave differently, the white above the stationary, blue above the will follow the movement. + +![physics-type](img/physics-type.gif) + +The reasons for the above phenomena are. +1. Neither static rigid body nor kinematic rigid body will be subject to forces, so the penetration is produced, which is a normal phenomenon. +2. Static objects can indeed move, static means that in spacetime, every moment is static and will not consider the state of other moments. +3. Unlike static objects, kinematic objects will estimate the state of motion (such as speed) according to the nearby moments, and because of the role of friction, therefore driving the yellow square. + +## Rigid Body Center of Mass + +By default, the center of mass of a rigid body coincides with the origin of the model. + +The following figure demonstrates the change in the position of the center of mass when the model origin does not coincide. + +![Centroid](img/center-of-mass.jpg) + +Here is an example to illustrate how to adjust the center of mass for a collision. + +- Create a new empty node **Node** and add the components shown in the following figure. + + ![add comp](img/center-add-comp.png) + +- Add a capsule under the child node of **Node** as shown below. + + ![add capsule](img/center-add-cupsule.png) + +- Adjust the Center of **cc.CapsuleCollider** to the following, so that the center of mass of the capsule is at the bottom of the capsule. + + ![result](img/center-result.png) + +The following figure shows how the motion behaves when the center of mass is inconsistent, with the center of mass of the capsule on the right at the bottom of the capsule and the center of mass of the capsule on the left at the center of the object. + + ![result](img/center-of-mass.gif) + +## Control Rigidbody + +### Making Rigidbody Move + +For different types, there are different ways to make rigid bodies move. + +- For static rigid bodies(**STATIC**), the object should be kept as stationary as possible, but the position of the object can still be changed by transformations (position, rotation, etc.). +- For kinematic rigid bodies(**KINEMATIC**), they should be made to move by changing transformations (position, rotation, etc.). + +For dynamic rigid bodies(**DYNAMIC**), their velocity needs to be changed in several ways. + +#### By Gravity + +The rigid body component provides the **UseGravity** property, which needs to be set to `true` when using gravity. + +#### By Applying Force + +The rigid body component provides the `applyForce` interface, which allows you to apply a force to a point of the rigid body to change its original state according to Newton's second law. + +```ts +import { math } from 'cc' + +rigidBody.applyForce(new math.Vec3(200, 0, 0)); +``` + +#### By Applying Torque + +The rigid body component provides the ``applyTorque`` interface, through which torques can be applied to the rigid body to change its rotation state. + +```ts +rigidBody.applyTorque(new math.Vec3(200, 0, 0)); +``` + +#### By Impulse + +The rigid body component provides an 'applyImpulse' interface that applies an impulse to a point on the rigid body. According to the momentum theorem, this will immediately change the linear velocity of the rigid body. If the point at which the impulse is applied is not on the line extended through the center of mass in the direction of the force, then a non-zero torque will be produced and will affect the angular velocity of the rigid body. + +```ts +rigidBody.applyImpulse(new math.Vec3(5, 0, 0)); +``` + +#### By Changing Velocity + +The rigid body component provides the `setLinearVelocity` interface that can be used to change the linear velocity. + +```ts +rigidBody.setLinearVelocity(new math.Vec3(5, 0, 0)); +``` + +Similarly, the rigid body component also provides the `setAngularVelocity` interface, which can be used to change the angular velocity. + +```ts +rigidBody.setAngularVelocity(new math.Vec3(5, 0, 0)); +``` + +### Limit the Motion + +#### By Sleeping + +When Sleeping a rigid body, it empties the rigid body of all its forces and velocities, bringing it to a stop. + +```ts +if (rigidBody.isAwake) { + rigidBody.sleep(); +} +``` + +The rigid body's force and speed will be restored when it is awakened. + +```ts +if (rigidBody.isSleeping) { + rigidBody.wakeUp(); +} +``` + +> **Note**: Executing parts of the interface, such as applying force or impulse, changing velocity, grouping and masking will attempt to wake up the rigid body. + +#### Via Damping + +The rigid body component provides **linearDamping** linear damping and **angularDamping** rotational damping properties, which can be obtained or set via the `linearDamping` and `angularDamping` methods. + +The range of damping parameters is recommended to be between **0** and **1**, **0** means no damping and **1** means full damping. + +```ts +if (rigidBody) { + rigidBody.linearDamping = 0.5; + let linearDamping = rigidBody.linearDamping; + + rigidBody.angularDamping = 0.5; + let angularDamping = rigidBody.angularDamping; +} +``` + +#### Factors + +The rigid body component provides the **linearFactor** linear velocity factor and **angularFactor** angular velocity factor properties, which can be obtained or set via the `linearFactor` and `angularFactor` methods. + +The factors are of type ``Vec3`` and the values of the corresponding components are used to scale the velocity change in the corresponding axes. The default values are both **1**, which means the scaling is **1** times, i.e. no scaling. + +```ts +if (rigidBody) { + rigidBody.linearFactor = new math.Vec3(0, 0.5, 0); + let linearFactor = rigidBody.linearFactor; + + rigidBody.angularFactor = new math.Vec3(0, 0.5, 0); + let angularFactor = rigidBody.angularFactor; +} +``` + +> **Note**: +> 1. Setting the value of a component of the factor to **0** fixes the movement or rotation of an axis. +> 2. In the case of [**cannon.js**](physics-engine.md#cannon.js) or [**Bullet (ammo.js)**](physics-engine.md#ammo.js) physics engines, the factor acts on different physical quantities, using **cannon.js** for velocity, and **Bullet (ammo.js)** for force. diff --git a/versions/4.0/en/physics/physics-sweep.md b/versions/4.0/en/physics/physics-sweep.md new file mode 100644 index 0000000000..0c427752ba --- /dev/null +++ b/versions/4.0/en/physics/physics-sweep.md @@ -0,0 +1,98 @@ +# Geometry Cast Detection + +Cocos Creator supports the sweep method since v3.8. + +Geometry cast detection, which will emit different geometry along a specified ray, is like using geometry to sweep an area, hence the name `sweep`. The sweep method will check the colliders in the region of the physics world that the geometry sweeps through and return a specific result. + +Quit different from the `rayCast` method, the `sweep` method allow the physics engine to cast different geometries, and return specific collision information. + +The engine now supports **box**, **sphere**, and **capsule** to sweep through. + +## Methods + +- Box sweep + - `sweepBox`: cast a box along the specified ray, and returns all colliders it hits as a result. + - `sweepBoxClosest`: cast a box along the specified ray, and return the closest collider it hits as a result. + + - parameters: + + - worldRay: geometry.Ray: ray in world space + - halfExtent: IVec3Like, half size of the box. + - orientation: IQuatLike, the orientation of the box + - mask: number:mask,the default is 0xffffffff,for more, refer to [Group and Mask](./physics-group-mask.md) and [Raycast Detection](./physics-raycast.md) + - maxDistance: number Maximum detection distance, the default value is 10000000, please do not use Infinity or Number.MAX_VALUE + - queryTrigger: boolean, check the trigger colliders or not, the default value is true, + +- Sphere sweep + - `sweepSphere`: cast a sphere along the specified ray, and returns all colliders it hits as a result. + - `sweepSphereClosest`: cast a sphere along the specified ray, and return the closest collider it hits as a result. + + - parameters: + - worldRay: geometry.Ray: Ray in world space + - radius: number, the sphere radius + - mask: number:mask,the default is 0xffffffff,for more, refer to [Group and Mask](./physics-group-mask.md) and [Raycast Detection](./physics-raycast.md) + - maxDistance: number Maximum detection distance, the default value is 10000000, please do not use Infinity or Number.MAX_VALUE + - queryTrigger: boolean, check the trigger colliders or not, the default value is true, + +- Capsule sweep + - sweepCapsule: cast a sphere along the specified ray, and returns all colliders it hits as a result. + - sweepCapsuleClosest: cast a sphere along the specified ray, and return the closest collider it hits as a result. + - parameters: + - worldRay:geometry.Ray: Ray in world space + - radius:number: the radius of the capsule + - height:number: distance between the two hal-sphere centers of the capsule + - orientation:IQuatLike: orientation of the capsule + mask,the default is 0xffffffff,for more, refer to [Group and Mask](./physics-group-mask.md) and [Raycast Detection](./physics-raycast.md) + - maxDistance: number Maximum detection distance, the default value is 10000000, please do not use Infinity or Number.MAX_VALUE + - queryTrigger: boolean, check the trigger colliders or not, the default value is true, + +Please refer to [API](%__APIDOC__%/zh/class/PhysicsSystem) for more information. + +## Sweep result + +All the sweep methods return true when they hit one or more colliders. For optimal reasons, the results are stored in the PhysicsSystem class. To fetch the result, please use `sweepCastClosestResult/sweepCastResults` or clone it out in case the former result may be invalid after the next sweep. + +- The result of `sweepBoxClosest`, `sweepSphereClosest`, and `sweepCapsuleClosest` is stored in `PhysicsSystem.instance.sweepCastResults`, which type is `PhysicsRayResult`, the code example is as follows + + ```ts + const result = PhysicsSystem.instance.sweepCastClosestResult; + ``` + +- The result of `sweepBox`、`sweepSphere` and `sweepCapsule` is stored in `PhysicsSystem.instance.sweepCastResults`, which type is an array of `PhysicsRayResult`, the code example is as follows + + ```ts + const results = PhysicsSystem.instance.sweepCastResults; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + ... + } + ``` + +The detail of `PhysicsRayResult` is as follows: +- hitPoint: vec3, the hit point in world space. +- distance: number, the distance between the hit-point and the ray origin +- collider: Collider, the collider hit by the ray. +- hitNormal: vec3, the normal of the hit plane, in world space. + +For more, please refer to [PhysicsRayResult](%__APIDOC__%/en/class/physics.PhysicsRayResult)。 + +## Examples + +Let us take the `sweepSphereCloset` as an example: + +```ts +if (PhysicsSystem.instance.sweepSphereClosest(this._ray, this._sphereRadius * this._scale, this._mask, this._maxDistance, this._queryTrigger)) { + const result = PhysicsSystem.instance.sweepCastClosestResult; +} +``` + +When the result of the method is true, you can access the hit result. + +For more examples, please refer to [GIT](https://github.com/cocos/cocos-example-projects/tree/master/physics-3d)。 + +The sweep result can be seen when running the **case-physics-sweep** scene after downloading the example project. + +![sweep.jpg](./img/sweep.jpg) + +The red point in the previous diaphragm shows the sweep result. diff --git a/versions/4.0/en/physics/physics.dot b/versions/4.0/en/physics/physics.dot new file mode 100644 index 0000000000..27d632c0ac --- /dev/null +++ b/versions/4.0/en/physics/physics.dot @@ -0,0 +1,5 @@ +digraph G { + + lateUpdate -> SyncSceneToPhysics -> PhysicsSimulation -> SyncPhysicsToScene -> emitEvents; + +} \ No newline at end of file diff --git a/versions/4.0/en/physics/physics.md b/versions/4.0/en/physics/physics.md new file mode 100644 index 0000000000..5358b45cf3 --- /dev/null +++ b/versions/4.0/en/physics/physics.md @@ -0,0 +1,80 @@ +# 3D Physics System + +## Introduction + +![physics-system](img/physics-system.jpg) + +The physics system is a crucial component of game engine, as it imbues the game world with the physical properties of the real world such as mass, gravity, and friction. This allows the game world to behave in a more realistic manner. The physics engine is responsible for simulating the motion and collision feedback of objects in the game world based on their physical properties. It is worth noting that the physics system is a complex system that interacts with not only game logic, but also the rendering system, animation system, and audio system. Typically, the game physics system only covers the basics of Newtonian mechanics, such as collision detection and rigid body motion. Other physical phenomena, such as fluid dynamics or soft body physics, require alternative approaches. Users can use the API provided by the physics engine to manipulate the physics system and achieve various physical effects in the game. + +Cocos Creator supports the following physics engines. + +- **Bullet**: default physics engine, asm.js/wasm version of [Bullet](https://pybullet.org/wordpress/). A physics engine with collision detection and physics simulation. +- **builtin**: built-in physics engine, lightweight engine for collision detection only. +- **cannon.js**: physics engine with collision detection and physics simulation. +- **PhysX**: Game physics engine developed by [NVIDIA](https://developer.nvidia.com/physx-sdk). A physics engine with collision detection and physics simulation. + +Developers choose different physics engines according to their development needs for physics features or application scenarios, please refer to: [Physics Engines](physics-engine.md) for details. + +> **Note**: PhysX is not supported in earlyer versions. To use PhysX please make sure the engine is upgraded to the latest version. + +## Physics Worlds + +Each element in the physics world can be understood as a separate **rigid body**, which can be made physical in Cocos Creator 3.x by adding a [Collider](physics-collider.md) or [RigidBody](physics-rigidbody.md) to the game object. Gives physics elements their physical properties. The physics system will perform physics calculations for these elements, such as calculating whether the objects collide and what forces are applied to the objects. When the calculations are complete, the physics system will update the physics world to the scene world, simulating the physical behavior that is restored in the real world. + +Scene world and physics world: + +![Scene World vs. Physics World](img/physics-world.jpg) + +> **Note**: The "rigid body" here refers to an object in the physics world that remains unchanged in shape and size and whose relative positions of internal points remain unchanged while in motion or after being acted upon by a force. + +### Physics Work Flow + +After all components are `lateUpdate`, the engine will synchronize the nodes holding physical properties (rigid body component, collider component) to the physics world and drive the physics engine to simulate them, and then synchronize the results calculated by the physics engine to each node of the scene after the simulation is completed. The overall process is shown in the following figure: + +![phy](img/physics-pipeline.png) + +## Add Physics Elements + +![add-element](img/physics-element.png) + +Adding a physics element to the game world can be done in the following steps: + +1. Create a new node. Here create a new cube model **Cube**. +2. Add a collider component, here add a [BoxCollider](physics-collider.md#BoxCollider). Click the **Add Component** button at the bottom of the **Inspector** panel, select **BoxCollider** in the **Physics** directory and adjust the parameters. +3. To give it a physical behavior, then add a [RigidBody](physics-rigidbody.md) component. + +This gives us a physics element with **both collider and physics behavior**. + +## Raycast and Geometry cast + +When developing a game, we may need some type of physics casting, such as whether a bullet hits its target, or whether a piece of furniture can be placed in a certain location. The usual way to do this is to cast a ray or project some kind of geometry(including boxes, spheres, capsules, etc) and can be found in the following documentations. + +- [Raycast Detection](./physics-raycast.md) +- [Geometry Cast Detection](./physics-sweep.md) + +### Physics Debug Draw + +![debug-draw](img/physics-debugdraw.png) + +By default, the physics system does not draw any debug information. If you need to draw debug information, please set the `PhysicsSystem.instance.debugDrawFlags`. + +The physics system provides a variety of debug information, and relevant content can be drawn by combining these pieces of information. + +- EPhysicsDrawFlags.NONE = 0: Draw nothing. +- EPhysicsDrawFlags.WIRE_FRAME = 1: Draw the wireframe of colliders. +- EPhysicsDrawFlags.CONSTRAINT = 2: Draw constraints. +- EPhysicsDrawFlags.ABB = 4: Draw bounding boxes. + +To draw all the information, you can refer to the following code: + +```ts +PhysicsSystem.instance.debugDrawFlags = EPhysicsDrawFlags.WIRE_FRAME + | EPhysicsDrawFlags.AABB + | EPhysicsDrawFlags.CONSTRAINT; +``` + +Set the drawing flag to `EPhysicsDrawFlags.NONE` to turn off the drawing. + +```ts +PhysicsSystem.instance.debugDrawFlags = EPhysicsDrawFlags.NONE; +``` diff --git a/versions/4.0/en/release-notes/animation.png b/versions/4.0/en/release-notes/animation.png new file mode 100644 index 0000000000..0f20acb89d Binary files /dev/null and b/versions/4.0/en/release-notes/animation.png differ diff --git a/versions/4.0/en/release-notes/assets-preview.png b/versions/4.0/en/release-notes/assets-preview.png new file mode 100644 index 0000000000..eb1a6cbaa1 Binary files /dev/null and b/versions/4.0/en/release-notes/assets-preview.png differ diff --git a/versions/4.0/en/release-notes/build-panel.png b/versions/4.0/en/release-notes/build-panel.png new file mode 100644 index 0000000000..9e9e6732f0 Binary files /dev/null and b/versions/4.0/en/release-notes/build-panel.png differ diff --git a/versions/4.0/en/release-notes/build-template-settings-upgrade-guide-v3.6.md b/versions/4.0/en/release-notes/build-template-settings-upgrade-guide-v3.6.md new file mode 100644 index 0000000000..13ef783080 --- /dev/null +++ b/versions/4.0/en/release-notes/build-template-settings-upgrade-guide-v3.6.md @@ -0,0 +1,437 @@ +# Cocos Creator 3.6.0 Build Template and settings.json Upgrade Guide + +> This article will introduce Cocos Creator 3.6.0 build templates and settings.json upgrade considerations. + +Prior to 3.6, the engine code contained some built-in effect data that could not be removed, which took up a lot of the engine package and slowed down the time to parse the engine code. For most projects, this is a waste of resources. + +In the previous version, `application.ejs` and `game.ejs` (`index.ejs` for some platforms) in the build template contained a lot of logical code for the engine startup process, which was more complex and prone to problems during customization, and needed a more easy-to-use and stable customization solution for the engine startup process. + +In addition, in the previous version, the main configuration data of the game was stored in `settings.json`, which was not accessible during the game runtime, which was extremely inconvenient for some plugins. + +To address these issues, in Cocos Creator 3.6 we refactored the engine startup process and provided a new `settings` configuration module in order to optimize the engine package, better maintain the engine's business code, and provide more room for customization. During the refactoring process, although we wanted to ensure as much compatibility as possible between the previous version of the project build template and settings.json, some aspects of the refactoring introduced some incompatibilities. In these cases, you will need to manually upgrade the project build template with the custom content of the configuration file. + +- For **Artists** and **Designers**, all resources in the project, such as scenes, animations, and Prefab, do not need to be modified and do not need to be upgraded. +- For **Programmers** and **Plugin Developers**, the impact is mainly on the custom build templates and `settings.json` in the original project that need to be adapted to the new way. + +This section is described in more detail below. + +## Cases where manual upgrade is required + +- The project has a custom build template with `application.ejs` in it. +- The project has a custom build template with `game.ejs` or `index.ejs`. +- The project has a custom generated `settings.json`. + +## Upgrade steps + +### Migrate the customized application.ejs to the new template + +The `application.ejs` has been changed significantly in v3.6, with two main changes. + +#### Provides a simpler definition of the Application type + +```js +class Application { + init (cc: any): Promise | void; + start(): Promise; +} +``` + +We provide the definition of an Application like the one above, where we provide two lifecycle callbacks. +- The `init` lifecycle function will be called after the engine code is loaded, the engine module will be passed as a parameter and you can listen for events in the `init` function for the engine start process and execute some custom logic when the corresponding event is triggered. +- The `start` lifecycle function will be called after the `init` function and you need to start and run the engine in the way you want in this function. + +Here is the simplest template we have implemented for `application.ejs` with the corresponding explanation. + +```js +let cc; +export class Application { + constructor () { + this.settingsPath = '<%= settingsJsonPath %>'; // settings.json file path, usually passed in by the editor when building, you can also specify your own path + this.showFPS = <%= showFPS %>; // Whether or not to open the profiler, usually passed in when the editor is built, but you can also specify the value you want + } + + init (engine) { + cc = engine; + cc.game.onPostBaseInitDelegate.add(this.onPostInitBase.bind(this)); // Listening for engine start process events onPostBaseInitDelegate + cc.game.onPostSubsystemInitDelegate.add(this.onPostSystemInit.bind(this)); // Listening for engine start process events onPostSubsystemInitDelegate + } + + onPostInitBase () { + // cc.settings.overrideSettings('assets', 'server', ''); + // Implement some custom logic + } + + onPostSystemInit () { + // Implement some custom logic + } + + start () { + return cc.game.init({ // Run the engine with the required parameters + debugMode: <%= debugMode %> ? cc.DebugMode.INFO : cc.DebugMode.ERROR, + settingsPath: this.settingsPath, // Pass in the settings.json path + overrideSettings: { // Override part of the data in the configuration file, this field will be described in detail below + // assets: { + // preloadBundles: [{ bundle: 'main', version: 'xxx' }], + // } + profiling: { + showFPS: this.showFPS, + } + } + }).then(() => cc.game.run()); + } +} +``` + +- This template listens to events in the `init` function during the engine startup process, and can execute some custom logic when the corresponding event is triggered. + `game.init` triggers events at various stages during execution, see the API description of `game.init`, where the events at different stages can use different engine capabilities, and the sequence of triggered events is as follows. + + ``` + -PreBaseInitEvent, no engine capability can be used + -Base module initialization (logging, sys, settings) + -PostBaseInitEvent, can use the capabilities in the base module + -PreInfrastructureInitEvent, can use the capabilities in the base module + -Infrastructure module initialization (assetManager, builtinResMgr, gfxDevice, screen, Layer, macro) + -PostInfrastructureInitEvent, can use the capabilities in the infrastructure module, infrastructure module + -PreSubsystemInitEvent, can use the capabilities of the infrastructure module, infrastructure module + -Subsystem module initialization (animation, physics, tween, ui, middleware, etc.) + -PostSubsystemInitEvent, can use capabilities from base module, infrastructure module, subsystem module + -EngineInitedEvent, can use the capabilities of the base module, infrastructure module, subsystem module + -PreProjectDataInitEvent, which can use the capabilities of the base module, infrastructure module, and subsystem module + -ProjectDataInit(GamePlayScripts, resources, etc) + -PostProjectDataInitEvent, which can use capabilities from the base module, infrastructure module, subsystem module, and project data + -GameInitedEvent, can use all engine capabilities + ``` + +- The `start` function is called to initialize and run the engine. You can control the start of the engine by passing in custom parameters when calling `game.init`. Note the `overrideSettings` field, which can be used to override some configuration data in the configuration file to affect the start of the engine. This field will be discussed in detail in the second part. + +The new `application.ejs` is much cleaner and more customizable than the previous version, so you can refer to this template to implement your own `application.ejs` and execute your custom logic in it. + +#### Engine startup process related logic code migrated to the engine + +We have migrated the logic related to the engine startup process from application.ejs to the engine, including the loading of settings.json (`loadSettingsJson`), the loading of js plugins (`loadJsList`), the loading of project bundles (`loadAssetBundle`), etc. ), and so on. So now you **can't directly modify the engine startup process in application.ejs**, but you can override the configuration file with the `overrideSettings` field passed in `game.init` to influence the startup process, and you can also listen to events during the engine startup process and execute some custom logic. The following section describes in detail how the engine startup process customization can be migrated to the new mechanism. If your project has customizations to the engine startup process, please refer to the following upgrade method for migration. + +- If you inserted custom code logic at a specific stage of engine startup in an earlier template, for example by making a customization such as: + + ```js + function start ({ + findCanvas, + }) { + let settings; + let cc; + return Promise.resolve() + .then(() => topLevelImport('cc')) + .then(() => customLogic1()) // Customized Logic 1 + .then((engine) => { + cc = engine; + return loadSettingsJson(cc); + }) + .then(() => customLogic2()) // Customized Logic 2 + .then(() => { + settings = window._CCSettings; + return initializeGame(cc, settings, findCanvas) + .then(() => { + if (settings.scriptPackages) { + return loadModulePacks(settings.scriptPackages); + } + }) + .then(() => loadJsList(settings.jsList)) + .then(() => loadAssetBundle(settings.hasResourcesBundle, settings.hasStartSceneBundle)) + .then(() => { + return cc.game.run(() => onGameStarted(cc, settings)); + }); + }).then(() => customLogic3()); // Customized Logic 3 + } + ``` + + You can implement custom logic in the new `application.ejs` template by listening for events in the engine startup process as described above, e.g. + + ```js + init (engine) { + cc = engine; + cc.game.onPreBaseInitDelegate.add(this.onPreBaseInit.bind(this)); // Listening for engine start process events onPreBaseInitDelegate + cc.game.onPostBaseInitDelegate.add(this.onPostBaseInit.bind(this)); // Listening for engine start process events onPostBaseInitDelegate + cc.game.onPostProjectInitDelegate.add(this.onPostProjectInit.bind(this)); // Listening for engine start process events onPostProjectInitDelegate + } + + onPreBaseInit () { + customLogic1(); // Customized Logic 1 + } + + onPostBaseInit () { + customLogic2() // Customized Logic 2 + } + + onPostProjectInit () { + customLogic3() // Customized Logic 3 + } + ``` + +- If you customized the loading of the physical module wasm in the previous template, i.e. the following method in the old template. + + ```js + <% if (hasPhysicsAmmo) { %> + promise = promise + .then(() => topLevelImport('wait-for-ammo-instantiation')) + .then(({default: waitForAmmoInstantiation}) => { + return waitForAmmoInstantiation(fetchWasm('')); + }); + <% } %> + ``` + + This method has been changed for internal use in the engine. If you need to customize it, please customize the engine and customize the `cocos/physics/bullet/instantiated.ts` content in the engine directory. + +- If you customized the loading of settings.json in the previous template, i.e. the `loadSettingsJson` function in the previous template. + This method has been changed to be used internally by the engine. If you have customizations for this part, please consider it in the following three cases. + - If you need to customize the path to `settings.json`, you can pass in a custom `settingsPath` path when calling the `game.init` method, e.g: + + ```js + game.init({ settingsPath: this.mySettingsPath }); + ``` + + - If you need to read the contents of settings.json, you can listen to the events after `game.onPostBaseInitDelegate`, where you can safely access the `settings` module in the engine and get the corresponding configuration data through the relevant API of the `settings` module, e.g: + + ```js + init (engine) { + cc = engine; + cc.game.onPostBaseInitDelegate.add(this.onPostBaseInit.bind(this)); + } + + onPostBaseInit () { + const property = cc.settings.querySettings('MyCustomData', 'MyCustomProperty'); + } + ``` + + - If you need to set the contents of a configuration file, you can override some of the data in the configuration file by passing in the `overrideSettings` field when calling the `game.init` method, or you can call `settings.overrideSettings` in the game's event callback to override the data in the configuration file. thus affecting the engine startup process, e.g: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'profiling': { 'showFPS': true } + } + }); + } + ``` + + Or + + ```js + onPostBaseInit () { + cc.settings.overrideSettings('profiling', 'showFPS', true); + } + ``` + +- If you customized the previous template to load js plugins, i.e. the `loadJsList` function in the previous template. + This method has been changed for internal use in the engine, so if you need to modify the list of js files to be loaded, you can do so by passing in the `plugin` field of `overrideSettings` when calling `game.init`, e.g: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'plugins': { 'jsList': ['MyCustom.js'] } + } + }); + } + ``` + +- If you customized the previous template to load item bundles, i.e. the `loadAssetBundle` function in the previous template. + This method has been changed for internal use in the engine, so if you need to modify the list of bundles to be loaded, you can do so by passing in the `assets` field of `overrideSettings` when calling `game.init`, e.g: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'assets': { 'preloadBundles': [ 'main', 'resources', 'myBundle' ]} + } + }); + } + ``` + +- If you customized the initialization of macros and layers in the previous template, i.e. the `initializeGame` function in the previous template. + This method has been changed for internal use in the engine, so if you need to modify the list of macros and layers, you can do so by passing in the `engine` field of `overrideSettings` when calling `game.init`, e.g: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'engine': { + 'macros': {}, + 'customLayers': [], + } + } + }); + } + ``` + +- If you customized the loading of the initial scene in the old template, the `onGameStarted` function in the previous template. + This method has been changed for internal use in the engine, so if you need to modify the initial scene, you can do so by passing in the `launch` field of `overrideSettings` when calling `game.init`, e.g: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'launch': { 'launchScene': 'MyFirstScene' } + } + }); + } + ``` + +- If you customized the previous template by passing in the `game.init` parameter, i.e. the `getGameOptions` function in the previous template. + Please refer to the latest `IGameConfig` interface definition and pass that parameter in `game.init`. + +### Migration of customized game.ejs or index.ejs to the new template + +game.ejs and index.ejs have not changed much compared to previous versions, the main changes are in the following points. +- We migrated the interfaces used by the engine such as `loadJsListFile`, `fetchWasm`, `findCanvas`, etc. to the internal part of the engine. If you have customizations for this part, please customize the engine and modify the contents of pal/env in the engine directory. +- As mentioned in the first part, we have organized the interface definitions for the Application type, and game.ejs and index.ejs as callers of the Application type, so changes have been made in the Application interface calls, for example in some platforms we have changed to the following form: + + ```js + System.import('<%= applicationJs %>') + .then(({ Application }) => { + return new Application(); + }).then((application) => { + return System.import('cc').then((cc) => { + return application.init(cc); + }).then(() => { + return application.start(); + }); + }).catch((err) => { + console.error(err.toString() + ', stack: ' + err.stack); + }); + ``` + +- We packaged the code used for adaptation on different platforms and merged it into a single js file to reduce the package size, so where the code is run it is instead run as a single js file, e.g: + + ```js + require('./libs/common/engine/index.js'); + require('./libs/wrapper/engine/index'); + require('./libs/common/cache-manager.js'); + ``` + + Changed to: + + ```js + require('./engine-adapter'); + ``` + +If you have customizations to game.ejs and index.ejs, simply migrate the customizations to the latest template, with the **note** that if your custom logic relies on **string matches** for insertion, e.g: + +```js +const gameTemplateString = readFileSync('game.ejs', 'utf-8'); +gameTemplateString = gameTemplateString.replace('require('./libs/common/cache-manager.js');', 'require('./libs/common/cache-manager.js');\nCustomLogic();\n') +``` + +Then there may be places where the match string fails and you need to insert it under a new template, or you can refer to the first part to customize it in application.ejs, the new application.ejs template is more conducive to customization. + +### Migrating customizations to settings.json + +In 3.6 we added the `settings` module for in-game access to configuration data, you can add custom data to settings.json and then use the `settings` module to access it at runtime, but to avoid the configuration data between modules affecting each other, we changed the previous one-layer configuration data structure to a ` Category` and `Property`, the following are the specific changes. + +Previous versions: + +```js +interface Settings { + CocosEngine: string; + debug: boolean; // whether debug mode is enabled + designResolution: ISettingsDesignResolution; // design resolution + jsList: string[]; // list of js plugins + launchScene: string; // The first scene + preloadAssets: string[], // preload resources + platform: string; // platform name + renderPipeline: string; // render pipeline + physics?: IPhysicsConfig; // physics-related configuration + exactFitScreen: boolean; // whether or not to align the game frame to the screen under web + + bundleVers: Record; // bundle version information + subpackages: string[]; // bundle's subpackage configuration on the mini-game + remoteBundles: string[]; // list of remote bundles + server: string; // Address of the remote server + hasResourcesBundle: boolean; // whether the resources bundle exists + hasStartSceneBundle: boolean; // if the first scene bundle exists + + scriptPackages?: string[]; // internal fields used by the engine + splashScreen?: ISplashSetting; // SplashScreen related configuration + + customJointTextureLayouts?: ICustomJointTextureLayout[]; // JointTextureLayout related configuration + + macros?: Record; // cc.macros related configuration + engineModules: string[]; // engine modules, used in the preview + customLayers: {name: string, bit: number}[]; // custom layer related configuration + orientation?: IOrientation; // screen rotation direction +} +``` + +New version of Settings.json file format is as follows: + +```ts +interface Settings { + CocosEngine: string; + engine: { + debug: boolean; + macros: Record; + customLayers: {name: string, bit: number}[]; + platform: string; + engineModules?: string[]; + builtinAssets: string[]; + }; + physics?: IPhysicsConfig; + rendering: { + renderPipeline: string; + renderMode?: number; + }; + assets: { + server: string; + remoteBundles: string[]; + bundleVers: Record; + preloadBundles: { bundle: string, version?: string }[]; + importBase?: string; + nativeBase?: string; + subpackages: string[]; + preloadAssets: string[]; + jsbDownloaderMaxTasks?: number; + jsbDownloaderTimeout?: number; + }; + plugins: { + jsList: string[]; + }; + scripting: { + scriptPackages?: string[]; + }; + launch: { + launchScene: string; + }; + screen: { + frameRate?: number; + exactFitScreen: boolean; + orientation?: IOrientation; + designResolution: ISettingsDesignResolution; + }; + splashScreen?: ISplashSetting; + animation: { + customJointTextureLayouts?: ICustomJointTextureLayout[]; + }; + profiling?: { + showFPS: boolean; + }; +} +``` + +The fields have remained almost one-to-one with the previous version, except that they have been migrated to a specific `Category`. A few fields have changed slightly. + +- A new frameRate field under screen has been added to set the target update frequency +- Added a new showFPS field under profiling to control whether to turn on the statistics display in the lower left corner +- The original hasResourcesBundle and hasStartSceneBundle fields have been removed and are now controlled by the preloadBundles field under assets. +- Added jsbDownloaderMaxTasks, jsbDownloaderTimeout fields under assets to control the number of concurrent downloads and timeout of resources in the native environment. + +If you have customized the data fields in settings.json, you need to refer to the new format and migrate the customizations to the new format. If you have custom configuration fields, we recommend that you place the custom configuration data under a custom `Category` to avoid conflicts with other modules' data, e.g: + +```ts +interface MyCustomSettings extends Settings { + 'customSettings': { + 'someProperty': string; + }; +} +``` + +------------------------- +If you encounter any problems during the upgrade process, please feel free to visit [Cocos Creator Forum](https://forum.cocos.org/) or [GitHub](https://github.com/cocos/cocos-) engine/discussions), thank you! diff --git a/versions/4.0/en/release-notes/build.png b/versions/4.0/en/release-notes/build.png new file mode 100644 index 0000000000..b6c85acd86 Binary files /dev/null and b/versions/4.0/en/release-notes/build.png differ diff --git a/versions/4.0/en/release-notes/editor-cmake-path-mac.png b/versions/4.0/en/release-notes/editor-cmake-path-mac.png new file mode 100644 index 0000000000..7702e6d3e1 Binary files /dev/null and b/versions/4.0/en/release-notes/editor-cmake-path-mac.png differ diff --git a/versions/4.0/en/release-notes/editor-cmake-path.png b/versions/4.0/en/release-notes/editor-cmake-path.png new file mode 100644 index 0000000000..ffcdd3798c Binary files /dev/null and b/versions/4.0/en/release-notes/editor-cmake-path.png differ diff --git a/versions/4.0/en/release-notes/engine-common.png b/versions/4.0/en/release-notes/engine-common.png new file mode 100644 index 0000000000..9b884eb61c Binary files /dev/null and b/versions/4.0/en/release-notes/engine-common.png differ diff --git a/versions/4.0/en/release-notes/extension-plugin.png b/versions/4.0/en/release-notes/extension-plugin.png new file mode 100644 index 0000000000..9e71f7e082 Binary files /dev/null and b/versions/4.0/en/release-notes/extension-plugin.png differ diff --git a/versions/4.0/en/release-notes/import-menu.png b/versions/4.0/en/release-notes/import-menu.png new file mode 100644 index 0000000000..80ac57629c Binary files /dev/null and b/versions/4.0/en/release-notes/import-menu.png differ diff --git a/versions/4.0/en/release-notes/import-panel.png b/versions/4.0/en/release-notes/import-panel.png new file mode 100644 index 0000000000..b6d699ffbe Binary files /dev/null and b/versions/4.0/en/release-notes/import-panel.png differ diff --git a/versions/4.0/en/release-notes/import-select-project.png b/versions/4.0/en/release-notes/import-select-project.png new file mode 100644 index 0000000000..9932a5fee6 Binary files /dev/null and b/versions/4.0/en/release-notes/import-select-project.png differ diff --git a/versions/4.0/en/release-notes/index.md b/versions/4.0/en/release-notes/index.md new file mode 100644 index 0000000000..6ffd0133b7 --- /dev/null +++ b/versions/4.0/en/release-notes/index.md @@ -0,0 +1,14 @@ +# Upgrade Guide + +- [Cocos Creator 3.0 Upgrade Guide](upgrade-guide-v3.0.md) +- [Cocos Creator 3.0 Material Upgrade Guide](../material-system/effect-2.x-to-3.0.md) +- [Cocos Creator 3.1 Material Upgrade Guide](../material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md) +- [Cocos Creator 3.5 Material Upgrade Guide](../material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md) +- [Subpackage upgrade guide](../asset/subpackage-upgrade-guide.md) +- [Asset Manager Upgrade Guide](../asset/asset-manager-upgrade-guide.md) +- [v3.5 Built project upgrade Guide](../engine/template/native-upgrade-to-v3.5.md) +- [v3.5 Built Project Upgrade Guide to v3.6](../engine/template/native-upgrade-to-v3.6.md) +- [Cocos Creator 3.6.0 Build Template and settings.json Upgrade Guide](build-template-settings-upgrade-guide-v3.6.md) +- [Upgrade Guide: Effect from v3.5.x to v3.6.0](../material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md) +- [Upgrade Guide: Particle from v3.5.x to v3.6.0](../particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md) +- [Upgrade CMake version before v3.6](./upgrade-cmake.md) diff --git a/versions/4.0/en/release-notes/native-engine.png b/versions/4.0/en/release-notes/native-engine.png new file mode 100644 index 0000000000..8493e50117 Binary files /dev/null and b/versions/4.0/en/release-notes/native-engine.png differ diff --git a/versions/4.0/en/release-notes/project-setting.png b/versions/4.0/en/release-notes/project-setting.png new file mode 100644 index 0000000000..0b9527c46a Binary files /dev/null and b/versions/4.0/en/release-notes/project-setting.png differ diff --git a/versions/4.0/en/release-notes/texture-compress-setting.png b/versions/4.0/en/release-notes/texture-compress-setting.png new file mode 100644 index 0000000000..83f41d08b5 Binary files /dev/null and b/versions/4.0/en/release-notes/texture-compress-setting.png differ diff --git a/versions/4.0/en/release-notes/unzip-cmake-mac.png b/versions/4.0/en/release-notes/unzip-cmake-mac.png new file mode 100644 index 0000000000..98b32722e8 Binary files /dev/null and b/versions/4.0/en/release-notes/unzip-cmake-mac.png differ diff --git a/versions/4.0/en/release-notes/unzip-cmake-win.png b/versions/4.0/en/release-notes/unzip-cmake-win.png new file mode 100644 index 0000000000..3e5e94a8bb Binary files /dev/null and b/versions/4.0/en/release-notes/unzip-cmake-win.png differ diff --git a/versions/4.0/en/release-notes/update-setting.png b/versions/4.0/en/release-notes/update-setting.png new file mode 100644 index 0000000000..a98f12ea56 Binary files /dev/null and b/versions/4.0/en/release-notes/update-setting.png differ diff --git a/versions/4.0/en/release-notes/upgrade-3.8-android.md b/versions/4.0/en/release-notes/upgrade-3.8-android.md new file mode 100644 index 0000000000..6b6547c855 --- /dev/null +++ b/versions/4.0/en/release-notes/upgrade-3.8-android.md @@ -0,0 +1,104 @@ +# Upgrading Android Project to v3.8 + +Starting from version 3.8, the Android project generated by the build supports the new version of Android Studio (Flamingo | 2022.2.1) by default. Due to the [requirement of the Android Gradle plugin](https://developer.android.com/studio/releases#jdk-17), developers need to upgrade their JDK to version 17 and also upgrade Android Studio to the Flamingo version. + +> If the Android project generated by the build is not configured, you can directly delete the `native/engine/android` directory and `build/android`directory, and then rebuild it. This avoids the need for step-by-step modifications and upgrades to the project. However, please note that this step is risky, and developers need to understand the consequences. + +For existing native Android projects, developers can follow the steps below to upgrade their projects: + +## Step 1: Backup the Current Project + +Before proceeding with the upgrade, it is important to backup the current native directory in case of any issues. For example, you can use Git to save the current modifications. + +## Step 2: Upgrade Gradle Plugin Version + +The Gradle plugin version acts as the interface between Gradle and the Android build system. Therefore, before upgrading Gradle, we need to upgrade the Gradle plugin version. In the project's `native/engine/android/build.gradle` file, change the Gradle plugin version in the classpath to 8.0.2. + +```diff + // jcenter() // keeped as anchor, will be removed soon + } + dependencies { +- classpath 'com.android.tools.build:gradle:4.1.0' ++ classpath 'com.android.tools.build:gradle:8.0.2' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files +``` + +## Step 3: Update Package Field + +Remove the package attribute from the `native/engine/android/app/AndroidManifest.xml` file. + +```diff + + + + +``` + +Based on the diff file below, edit `native/engine/android/app/build.gradle` and change `applicationId` to `namespace`. + +```diff + compileSdkVersion PROP_COMPILE_SDK_VERSION.toInteger() + buildToolsVersion PROP_BUILD_TOOLS_VERSION + ndkPath PROP_NDK_PATH ++ namespace APPLICATION_ID + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 +@@ -17,7 +18,6 @@ android { + } + + defaultConfig { +- applicationId APPLICATION_ID + minSdkVersion PROP_MIN_SDK_VERSION + targetSdkVersion PROP_TARGET_SDK_VERSION + versionCode 1 +``` + +### Step 4: Upgrade Gradle Version + +Next, we need to upgrade the Gradle wrapper version. In the project's `build/android/proj/gradle/wrapper/gradle-wrapper.properties` file, change the distributionUrl to 8.0.2 as shown below: + +```properties +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +``` + +## Step 5: Update Proguard Rules + +Add the following code to file `native/engine/android/app/proguard-rules.pro` + +``` +# This is generated automatically by the Android Gradle plugin. +-dontwarn android.hardware.BatteryState +-dontwarn android.hardware.lights.Light +-dontwarn android.hardware.lights.LightState$Builder +-dontwarn android.hardware.lights.LightState +-dontwarn android.hardware.lights.LightsManager$LightsSession +-dontwarn android.hardware.lights.LightsManager +-dontwarn android.hardware.lights.LightsRequest$Builder +-dontwarn android.hardware.lights.LightsRequest +-dontwarn android.net.ssl.SSLSockets +-dontwarn android.os.VibratorManager +``` + +## Step 6: Install JDK 17 + +Download and install JDK 17 from the [official website](https://www.oracle.com/java/technologies/downloads/). During the installation process, make sure to configure the environment variables. + +Once the installation is complete, you can verify if it was successful by running `java -version` in the command line. + +## Step 7: Upgrade Android Studio + +After upgrading Gradle, we also need to upgrade Android Studio. If you are currently using an older version of Android Studio, please download the latest version of Android Studio (Flamingo | 2022.2.1) from [https://developer.android.com/studio](https://developer.android.com/studio). + +Once the download is complete, open Android Studio and import your project. Android Studio will automatically detect the required Gradle version for your project and prompt you to upgrade. Follow the prompts to complete the upgrade of Android Studio. + +If you encounter the error report below when compiling in Android Studio, +![error](./upgrade-3.8-jdk-bad-version.png) +you can refer to the following screenshot to modify the JDK 17 used by Gradle in the settings. +![settings](./upgrade-3.8-jdk-version-as.png) + +After completing the above steps, you can also perform a build by using the build panel. \ No newline at end of file diff --git a/versions/4.0/en/release-notes/upgrade-3.8-jdk-bad-version.png b/versions/4.0/en/release-notes/upgrade-3.8-jdk-bad-version.png new file mode 100644 index 0000000000..535bd5d6bd Binary files /dev/null and b/versions/4.0/en/release-notes/upgrade-3.8-jdk-bad-version.png differ diff --git a/versions/4.0/en/release-notes/upgrade-3.8-jdk-version-as.png b/versions/4.0/en/release-notes/upgrade-3.8-jdk-version-as.png new file mode 100644 index 0000000000..20ef2df6f3 Binary files /dev/null and b/versions/4.0/en/release-notes/upgrade-3.8-jdk-version-as.png differ diff --git a/versions/4.0/en/release-notes/upgrade-cmake.md b/versions/4.0/en/release-notes/upgrade-cmake.md new file mode 100644 index 0000000000..20bd68e341 --- /dev/null +++ b/versions/4.0/en/release-notes/upgrade-cmake.md @@ -0,0 +1,31 @@ +# Upgrade CMake + +Versions prior to v3.6 do not support specifying a cmake path, and can be upgraded by overwriting the editor's built-in cmake directory. + +### Windows platform + +1. Go to https://cmake.org/download/ to download the required cmake version, and extract it + +![unzip cmake for windows](./unzip-cmake-win.png) + +2. Open the editor internal directory `resources/tools/cmake/` + +![editor cmake path](./editor-cmake-path.png) + +3. Overwrite the editor's built-in cmake with the downloaded cmake content, leaving the directory structure unchanged. + +4. Restart the editor and it will take effect. + +### Mac platform + +1. Go to https://cmake.org/download/ to download the required cmake version, and unzip it + +![unzip cmake for mac](./unzip-cmake-mac.png) + +2. Open the editor internal directory `Contents/Resources/tools/cmake/arm64` (or `x86_64`) + +![editor cmake path](./editor-cmake-path-mac.png) + +3. Copy the contents of the downloaded `CMake/Contents` to the editor's corresponding directory. + +4. Restart the editor and it will take effect. diff --git a/versions/4.0/en/release-notes/upgrade-guide-v3.0.md b/versions/4.0/en/release-notes/upgrade-guide-v3.0.md new file mode 100644 index 0000000000..1defda2aa9 --- /dev/null +++ b/versions/4.0/en/release-notes/upgrade-guide-v3.0.md @@ -0,0 +1,558 @@ +# Cocos Creator 3.0 Upgrade Guide + +## Version Introduction + +__Cocos Creator 3.0__ integrates all the functions of the original __2D__ and __3D__ products, brings many major updates, and will become the main version of __Cocos Creator__. At the same time, 3.0 also continues __Cocos's__ advantages of light weight and high efficiency in 2D categories, and provides an efficient development experience for 3D heavy games. + +In order to ensure the smooth transition of an existing __Cocos Creator 2.4__ project, Cocos Creator 2.4 will be used as the LTS (long-term support) version and provide continuous updates for the next __two years__! In __2021__, v2.4 will continue to be updated to provide bug fixes and new mini-game platform support to ensure the successful launch of your project; in __2022__, we will continue to provide developers with the key to v2.4 bug fixes to ensure the smooth operation of online games! Therefore: + +- __Existing 2.x projects can continue to develop without compulsory upgrade to 3.0__. + +- __For new projects, it is recommended to use version 3.0 for development__. We will continue to optimize the development experience and operating efficiency of 3.0 to support the smooth launch of heavy games of different categories such as 2D and 3D. + +__Cocos Creator 3.0__ uses a new future-oriented engine architecture, which will bring high-performance, data-oriented and load-balanced renderers to the engine, and seamlessly support Vulkan & Metal multi-backend rendering. In the future, it will also support mobile VR/AR and some Host platform. + +For a detailed introduction to the __Cocos Creator 3.0__, please visit the [Official Website Update Instructions](https://cocos.com/creator). + +## How to migrate Cocos Creator 2.x projects + +Although __it is not recommended to force projects in development, especially those about to go live, to upgrade to v3.0__, a v2.x resource import tool has been introduced in Cocos Creator 3.0. This tool supports the perfect import of resources from old projects, as well as assistance in migrating code. + +### Resource Import + +Developers only need to click __File -> Import Cocos Creator 2.x project__ in the main menu. + +![import-menu](import-menu.png) + +Next, select the root directory of the v2.x project in the file browse dialog that pops up. + +![import-select-project](import-select-project.png) + +> __Note__: it is recommended to upgrade to v2.4.3 or above before importing to v3.0 for older projects, otherwise the correctness of the import result cannot be ensured. + +All the resources in the v2.x project will be automatically presented in the popup __Import Cocos Creator 2.x Project__ panel. Developers can reconfirm the resources to be imported and then click the __Import__ button in the bottom right corner of the panel to complete the import. If the developer wants to switch the imported 2.x project, click the search icon button in the image below to reselect the project. + +![import-project](import-panel.png) + +The __Manual__ button in the bottom left corner of the panel will take you to the GitHub repository for the Import Plugin, which can be used to [update the Import Plugin](https://github.com/cocos-creator/plugin-import-2.x/blob/main/README.md) or submit feedback. + +### Code Migration + +When importing a v2.x project developed in JavaScript, the code assisted migration feature of the import plugin will first convert the JavaScript to TypeScript and then perform the code migration. + +For example, the imported v2.x project JavaScript code would look like this: + +```typescript +// AudioController.js +cc.Class({ + extends: cc, + + properties: { + audioSource: { + type: cc.AudioSource, + default: null + }, + }, + + play: function () { + this.audioSource.play(); + }, + + pause: function () { + this.audioSource.pause(); + }, + +}); +``` + +Due to the differences in how the code is written in each project and the different levels of complexity, the current import plugin only adds __component type declarations__, __property declarations__ and __function declarations__ to the code migration, the references to the components in the scene are __preserved__ and the code inside the function is migrated as __comments__.
+In addition, the original code of v2.x is kept intact as a comment at the end of the migrated code, so that developers can refer to it when converting manually. + +The example code above, after the code assisted migration by the import plugin, looks like this: + +```typescript +// AudioController.ts + +import { _decorator, Component, AudioSource } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('AudioController') +export class AudioController extends Component { + @property + public audioSource:AudioSource = 'null'; + + play () { + //this.audioSource.play(); + } + + pause () { + //this.audioSource.pause(); + } + +} + + +/** + * Note: The original script has been commented out, due to the large changes in the script, there may be missing in the conversion, you need to manually convert it yourself + */ +// cc.Class({ +// extends: cc.Component, +// +// properties: { +// audioSource: { +// type: cc.AudioSource, +// default: null +// }, +// }, +// +// play: function () { +// this.audioSource.play(); +// }, +// +// pause: function () { +// this.audioSource.pause(); +// }, +// +// }); +``` + +> __Notes__ +> +> 1. When converting from JavaScript to TypeScript, it is necessary to declare __all properties__ in TypeScript and set the default values. +> 2. If the __Inspector__ panel data is missing, check if the property type is the same as v2.x. +> 3. If the JavaScript code uses external types, TypeScript prompts: Fix by importing external source files or declarations. + +## Quick start for developers of older versions + +### Material Upgrade + +In v3.0, there are continued improvements to the design of the material system and the built-in Shader API. When upgrading from v2.x to v3.x, some of the content cannot be upgraded automatically and needs to be adjusted manually by the developer. For details, please refer to the [Material Upgrade Guide](../material-system/effect-2.x-to-3.0.md) documentation. + +### Engine API Upgrade + +- The UI-related interface changes on the node are as follows: + + - The interfaces related to coordinate transformation calculation (e.g.: `size` or `anchor`) are as follows: + + Please get the `UITransform` component on the node first, and then use the corresponding interface, for example: + + ```typescript + const uiTrans = node.getComponent(UITransform)!; + uiTrans.anchorX = 0.5; + uiTrans.setContentSize(size); + ``` + + - The remaining interfaces are as follows: + + - `color`: needs to get the renderable component on the node first (e.g.: `Sprite` component), and then use the corresponding interface. + + - `opacity`: If there is a renderable component on the node, set the `color` of the renderable component directly. If there is no renderable component, you can set the renderable component's `color` by adding the `UIOpacity` component and setting the related property. + + - `skew`: Starting from v3.8.6, you need to add the UISkew component to the node and then call the setSkew method to configure the skew property. + + - `group`: change to `layer`. + + - `zIndex`: change to [priority](%__APIDOC__%/en/class/UITransform) of `UITransform`. + + > **Note**: the `priority` property is deprecated as of v3.1, please use the `setSiblingIndex` function to adjust the order of the node tree. + +- `CCSpriteFrame`: + + - Remove the interfaces: `copyWithZone`, `copy`, `clone` and `ensureLoadTexture`. + + - Change the interface: + + `setFlipX` and `isFlipX` -> `flipUVX` + + `setFlipY` and `isFlipY` -> `flipUVY` + + `getTexture` and `setTexture` -> `texture` (where the type is Texture2D/RenderTexture). + + - The remaining methods corresponding to `get` and `set` (e.g.: `getOffset`) all correspond directly to properties of the same name (e.g.: `offset`) in 3.0. + +- `CCTexture2D`: + + - Change the interface: `genMipmaps` -> `mipmaps`, `initWithElement` -> `image`. + + - `initWithData`, the whole method is removed, similarly the use is to pass the original `ArrayBufferView` data to the new `ImageAsset`, and then `ImageAsset` to the new `Texture2D` to get a copy of the image resource. + +- `Action`: Remove all related. + +- __Physics__: + + - 2D changed components: `cc.Collider` -> `Collider2D`, `cc.BoxCollider` -> `BoxCollider2D`, `cc.RigidBody` -> `RigidBody2D`, etc. + + - 3D changed components: `cc.Collider3D` -> `Collider`, `cc.BoxCollider3D` -> `BoxCollider`, `cc.RigidBody3D` -> `RigidBody`, etc. + +- __tween__: + + - Change the interface: `cc.repeatForever` -> `Tween.repeatForever`, `cc.reverseTime` -> `Tween.reverseTime`, `cc.show` -> `Tween.show`, etc. + +- __Animation__: + + - Change the interface: `addClip`-> `createState`, `getClips`-> `clips`, `playAdditive`-> `crossFade`, `getAnimationState`-> `getState`, etc. + +- __Camera__: + + - Remove the interfaces: `findCamera`, `alignWithScreen`, `main`, `cameras`, `zoomRatio` and `containsNode`. + + - Change the interface: `backgroundColor` -> `clearColor`, `cullingMask` -> `visibility`, `depth`->`clearDepth`, `getScreenToWorldPoint`->`screenToWorld`, `getWorldToScreenPoint`->`worldToScreen`, `getRay`->`screenPointToRay`, etc. + +- __Audio__: + + - Change the interface: `getLoop` and `setLoop` -> `loop`, `getVolume` and `setVolume` -> `volume`, `getCurrentTime` and `setCurrentTime` -> `currentTime`, `src` -> `clip`. + +- __Materials__: + + - All relevant changes need to be done by getting a __Material instance__ on __MeshRenderer__ or its subclasses. + + - Remove the interfaces: `setBlend`, `setDepth`, `setStencilEnabled`, `setStencil` and `setCullMode` and call `overridePipelineStates` to complete the update. `define` calls `recompileShaders` to complete the update. + +- The platform variable changes under __sys__ are as follows: + +| Cocos Creator 2.x | Cocos Creator 3.0 | +|:----------------- |:--------------------- | +| `BAIDU_GAME` | `BAIDU_MINI_GAME` | +| `VIVO_GAME` | `VIVO_MINI_GAME` | +| `OPPO_GAME` | `OPPO_MINI_GAME` | +| `HUAWEI_GAME` | `HUAWEI_QUICK_GAME` | +| `JKW_GAME` | `COCOSPLAY` | +| `ALIPAY_GAME` | `ALIPAY_MINI_GAME` | +| `BYTEDANCE_GAME` | `BYTEDANCE_MINI_GAME` | + +- The __global variables__ are changed as follows: + +| Cocos Creator 2.x | Cocos Creator 3.0 | +|:----------------- |:----------------- | +| `CC_BUILD` | `BUILD` | +| `CC_TEST` | `TEST` | +| `CC_EDITOR` | `EDITOR` | +| `CC_PREVIEW` | `PREVIEW` | +| `CC_DEV` | `DEV` | +| `CC_DEBUG` | `DEBUG` | +| `CC_JSB` | `JSB` | +| `CC_WECHATGAME` | `WECHATGAME` | +| `CC_RUNTIME` | `RUNTIME_BASED` | +| `CC_SUPPORT_JIT` | `SUPPORT_JIT` | + +- __Dynamic Loading__: + + When using `bundle.load` or `resources.load` to dynamically load a `sprite-frame` or `texture` in v3.0, the path needs to be specified to a specific sub-asset: + + ```ts + // Load texture + + // v2.x + resources.load('background', cc.Texture2D, () => {}); + // v3.0 + resources.load('background/texture', Texture2D, () => {}); + ``` + + ```ts + // Load sprite frame + + // v2.x + resources.load('background', cc.SpriteFrame, () => {}); + // v3.0 + resources.load('background/spriteFrame', SpriteFrame, () => {}); + ``` +##### `JSB` API relevant + +- `jsb.FileUtils` + - `getDataFromFile`: return type changed from `Uint8Array` to `ArrayBuffer` + +### Editor upgrade + +#### Build Panel + +The builds of all platforms in __v3.0__ are built-in plug-ins, so the __Build__ panel is also different from __v2.4__. The unique build options of each platform will be placed in a foldable section control separately. + +![image](build-panel.png) + +After clicking the build button, it will jump to the __Build Tasks__ panel, where all built platforms will be displayed. You can modify the build options of the built project in this panel and then rebuild, view the build log, open the project directory, etc. If you need to compile for other platforms, click the __New Build Task__ button at the top left of the panel. + +![image](build.png) + +In addition, it supports the construction of multi-module results with file separation during construction, which facilitates concurrent loading of multiple modules and dynamic loading of modules, and the WeChat engine plug-in supports the selection of different physical engine backends. The `settings.js` generated after the build is also changed to `settings.json` and placed under `src` directory, allowing it to be used as a resource upload server. + +#### Asset Preview Panel + +Select the resource in the __Assets__ panel to display resource thumbnails in the __Asset Preview__ panel. If the folder where the resource is located is selected, the thumbnails of all resources under the folder can be displayed for easy viewing. + +![image](assets-preview.png) + +#### Animation Panel Upgrade + +- Support the search and display filtering of nodes in the node tree panel. +- Support using the system clipboard to copy and paste all animation data (nodes, tracks, and key frames) on nodes. +- Support multi-select nodes to add property tracks in batches. +- Optimize the experience of selecting and deselecting key frames (__Ctrl + mouse click__ to select key frames to deselect them). +- Support continuing to edit node properties in the animation editing state, including particle and model material properties, etc. + +![image](animation.png) + +#### Project Setting Panel Update + +It is divided into three parts: __Engine Manager__, __Project Setting__, and __Build__. + +The Physics Collision Group uses the `PhysicsSystem.PhysicsGroup` type independently, and no longer shares the group configuration with `Node.Layers`: + +![image](project-setting.png) + +__Texture Compression__ is modified to configure the preset in the __Project Setting__ panel, then select the image resource in the __Assets__ panel, and then select the preset method.
+After the old project is upgraded, the editor will automatically scan all the compressed texture configurations in the project and sort out several presets. Since it is automatically scanned, the generated name may not be what you want, you can modify it here. + +![image](texture-compress-setting.png) + +#### Editor Extension System Upgrade + +Cocos Creator 3.x has a more powerful extension system. Almost all internal modules of the editor are built with extension system. You can quickly create your own extensions in the extended menu to achieve the customizations you want. In addition, v3.0 also provides an __Extension Manager__, which can easily manage the operation and uninstallation of all extensions. + +![image](extension-plugin.png) + +### Build Directory Differences + +__Cocos Creator 2.x__ and __Cocos Creator 3.0__ differ to a certain extent in the directories generated after building on different platforms. Let's take __v2.4.3__ as an example and compare it with __v3.0__ on Web, Native and WeChat Mini Game platforms respectively. + +#### Web + +The directory generated by v2.4.3 after building the __Web Desktop__ is as follows: + +![image](web-v243.png) + +The directory generated by v3.0 after building the __Web Desktop__ is as follows: + +![image](web-v3.png) + +From the above two figures, notice the directory generated after building the Web Desktop, v2.4.3 and v3.0 are mostly the same, except with the following differences: + +1. V3.0 puts all engine related code, such as core modules, physics modules, plugin scripts, etc., into the `web-desktop/cocos-js` directory, which looks clearer than v2.4.3, which was decentralized in the `web-desktop` directory. + + ![image](web-cocosjs.png) + +2. V2.4.3 has only one startup script `main.js`, while v3.0 has the following two startup scripts: + + - `index.js` -- Used to do some pre-processing work. + + - `application.js` -- Used to start the game. + +3. The `src/settings.js` used to manage configuration in v2.4.3 is changed to `src/settings.json` in v3.0. + +4. The splash screen `splash.png` in v2.4.3 is stored in `settings.json` by default in v3.0. + +5. The `style-desktop.css` and `style-mobile.css` in v2.4.3 are combined into a single `style.css` in v3.0. + +#### WeChat Mini Game + +The directory generated by v2.4.3 after building the __WeChat Mini Game__ is as follows: + +![image](wechat-v243.png) + +The directory generated by v3.0 after building the __WeChat Mini Game__ is as follows: + +![image](wechat-v3.png) + +From the above two figures, notice the directory generated after building the __WeChat Mini Game__, v2.4.3 and v3.0 are mostly the same, except with the following differences: + +1. V3.0 puts all engine related code, such as core modules, physics modules, plugin scripts, etc., into the `wechatgame/cocos-js` directory. While v2.4.3 scattered part of it in the `wechatgame` directory and part of it in the `wechatgame/cocos` directory. + + ![image](wechat-cocosjs.png) + +2. V2.4.3 compiles all the adaptation layer code of mini games into `adapter-min.js`, while v3.0 stores all the adaptation layer code as loose files in the `libs` directory, without compilation. + +3. The startup script for v2.4.3 is `main.js`, and for v3.0 it is `application.js`. + +4. V2.4.3 records all references of dynamic code in `ccRequire.js`. While v3.0 currently does not have this feature. + +5. The `src/settings.js` used to manage configuration in v2.4.3 is changed to `src/settings.json` in v3.0. + +#### Native + +The release package generated by v2.4.3 after building the __Windows__ platform is as follows: + +![image](v243-windows.png) + +The release package generated by v3.0 after building the __Windows__ platform is as follows: + +![image](v3-windows.png) + +Notice from the above two figures, there is a big difference between v2.4.3 and v3.0 in the release package generated after building the __Windows__ platform. + +1. The release package name for v2.4.3 is based on the __Template__ in the __Build__ panel (e.g.: `jsb-link`), while v3.0 is based on the __current build of the native platform__ (e.g.: `windows`, `Android`). + +2. Since the underlying C++ code generated after building on each native platform (e.g.: Android, Windows) is completely consistent. V3.0 extracts the underlying C++ code that was stored in the release package `jsb-link/frameworks/runtime-src/Classes` directory in v2.4.3 and placed it in a shared `native/engine/common` folder under the project directory. This way, when building the native platform, if the folder is detected to already exist, this part will no longer be processed, to speed up the build. + + ![image](engine-common.png) + +3. The files related to the application layer in the v2.4.3 release package directory have been merged into the `assets` directory in v3.0. + + The application layer files include the following: + + - __assets__ -- Resource directory. + - __jsb-adapter__ -- Directory, store the adaptation layer code. + - __src__ -- Directory, store engine related code, plugin scripts, `settings.js` etc. + - Related configuration files (`.cocos-project.json`, `cocos-project-template.json`, `project.json`). + - The startup script (`main.js`). + + The `assets` directory structure of v3.0 is as follows: + + ![image](v3-assets.png) + + V3.0 has also made adjustments and changes accordingly during the merging process. + + - All the engine related code (such as core modules, physics modules, plugin scripts, etc.) that was originally placed in the `src` file in v2.4.3 is moved to the `assets/src/cocos-js` directory. + + - The `src/settings.js` used to manage configuration in v2.4.3 is changed to `src/settings.json` in v3.0. + +4. V2.4.3 generates all the native build projects in the `frameworks/runtime-src` directory of the release package: + + ![image](v243-build-template.png) + + While v3.0 generates the native build project in the `proj` directory and only generates the native build project for the current built. As shown below: + + ![image](v3-build-template.png) + + Also, v3.0 separates the code and configuration, putting part of the code and configuration into the source code management, located in the `native/engine/name of the currently built platform` folder (e.g.: `native/engine/win32`, `native/engine/android`) under the project directory. Developers can integrate SDKs or do secondary development here. Deleting the release package directory generated after the build (e.g.: `build/windows`) will not affect the already integrated SDKs. + + ![image](v3-build-native.png) + +5. Some assets needed for compilation, such as application icons, application startup scripts, etc., v2.4.3 are stored in the build project, while v3.0 are stored in the `native/engine/name of the currently built platform` directory (e.g.: `native/engine/win32`, `native/engine/android`). + +## Upgrade FAQ + +### After the upgrade, the project script shows errors when opening in VS Code with operations such as binding component definitions + +Cocos Creator 3.x enables Strict Mode for TypeScript, which will review the code more strictly and eliminate any problems that may occur due to negligence during the development process. + +To avoid using strict mode, check __Enable loose mode__ in __Project -> Project Settings -> Scripting__ in the top menu bar of Creator. As a reminder, turning off strict mode is not encouraged as strict null checking can reduce some low-level errors in the code runtime. + +For writing specifications in strict mode, please refer to the [Taxi Game 3D](https://github.com/cocos/cocos-tutorial-taxi-game) as a test case. + +### `Action` actions are all disabled + +Cocos Creator 3.x removes the `Action` system and uses the `Tween` system instead. + +### Modifying `size` and `anchor` of 2D nodes does not work + +Obtain the UITransform component on the node first, and then use the corresponding interface, e.g.: + +```typescript +const uiTrans = node.getComponent(UITransform)! ; +uiTrans.anchorX = 0.5; +uiTrans.setContentSize(size); +``` + +### Modifying the `color` of a 2D node does not work + +Obtain the renderable component on the node (e.g. Sprite component) first, and then use the corresponding interface, e.g.: + +```typescript +const uiColor = node.getComponent(Sprite)! ; +uiColor.color = color(255,255,255); +``` + +### Modifying `skew` for 2D nodes does not work + +Starting from v3.8.6, you need to add the UISkew component to the node and then call the setSkew method to configure the skew property. + +### Grouping is not available, but there are still grouping settings (Layers) in the project settings panel of Creator + +The `group` group management in v2.x has been changed to `Layer` since v3.0, as shown below. In v2.x the group name is obtained from `node.group`, while in v3.x the group value is obtained from `node.layer` as __group value__, and the group value is set in exponential power of 2. + +![update-setting](update-setting.png) + +The layer value of User Layer 0 is: 20 = 1.
+The layer value of User Layer 1 is: 21 = 2.
+The layer value of User Layer 6 is: 26 = 64. + +### The sibling node set by `zIndex` is invalid + +As of v3.0, the `zIndex` interface has been removed. To reorder the node tree please use the `setSiblingIndex` method instead. + +### The script mounted on the node is not available via `getComponent()` + +Please look up the class name of the corresponding script, not the script name. v3.x script components are based on the class name defined in the script, not the script name. There are often problems with scripts not being found due to letter cases. Please refer to the [Create Script](../scripting/setup.md) documentation for more details. + +### Dynamic loading of images in the `resources` folder is not found + +When an image is set to `sprite-frame`, `texture` or other image type, an asset of the corresponding type will be generated in the __Assets__ panel. However, if loading an `testAssets/image` directly, the type obtained will be `ImageAsset` and it is necessary to specify the path to the specific child asset. + +For example, if an image of type `sprite-frame` is set to `testAssets/image` in the `resources` folder, then to load `SpriteFrame` consider the following code: + +```typescript +resources.load("testAssets/image/spriteFrame", SpriteFrame, (err, spriteFrame) => { + this.node.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +When loading an image of type `texture`, change `spriteFrame` to `texture`. + +### The original physics collision callback is gone after the object generates a physics collision + +Starting from v3.0, collision callbacks need to be registered at the beginning, unlike the original v2.x which generated callbacks directly. Therefore, developers need to add registration of the callback function to the physics callback script. For example: + +```typescript +let collider = this.getComponent(Collider2D); +if (collider) { + // Called only once when two colliding bodies start to make contact + collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); + + // Called only once when two colliders end contact + collider.on(Contact2DType.END_CONTACT, this.onEndContact, this); + + // Called each time the collider contact logic is about to be handled + collider.on(Contact2DType.PRE_SOLVE, this.onPreSolve, this); + + // Called each time the collider contact logic is finished + collider.on(Contact2DType.POST_SOLVE, this.onPostSolve, this); + } +}); +``` + +### After the upgrade, the physics collision grouping is gone + +Currently the import plugin does not support the physics collision matrix, so for now the developer needs to set the collision matrix manually, which can be reset in the main menu __Project -> Project Settings -> Physics__ of Creator. + +### The `audioEngine` interface for the audio system is not working and audio cannot be played + +Starting from v3.0, the `audioEngine` interface has been removed and the __AudioSource__ component is used to control the playback of audio. Please refer to the [AudioSource Component](../audio-system/audiosource.md) documentation for more details. + +### `Button` button is not clickable + +To rule out code and rendering level problems, please check if the value of `Z` axis in the `Scale` property of the __Button__ node is 0, if so, change it to 1. + +### The editor is stuck when making changes to the script after upgrade + +Check if the property decorator `property` of the component type defined in the script after upgrade is undefined, if not, it is caused by the imported plugin being too old, please refer to [Plugin Upgrade](https://github.com/cocos-creator/plugin-import-2.x) to update the imported plugin Upgrade. After updating the imported plugins, you need to __re-do the project upgrade__. + +### When modifying the `Position` of a node in a script after an upgrade, direct changes through the node (e.g. `node.x`) do not take effect + +As of v3.0, direct access to coordinate positions is not allowed on `node` nodes, it is necessary to access `position` before accessing coordinate values. And `position` is a __read-only property__ in v3.x. To change it, please use the `setPosition` method. For example: + +```typescript +// v2.x + +// Access the coordinate axes +let xAxis = this.node.x; +// Modify the x-axis coordinates +this.node.x = 200; + +// v3.x + +// Access the pos +let pos = this.node.position; +// Modify the x-axis coordinates +this.node.setPosition(200,pos.y,pos.z); +``` + +## TypeScript Reference Tutorials + +- [Tutorial: v3.0 TypeScript question answering and experience sharing](https://discuss.cocos2d-x.org/t/tutorial-3-0-typescript-question-answering-and-experience-sharing/52932) +- [TypeScript Official Website](https://www.typescriptlang.org/) +- [TypeScript - Classes](https://www.typescriptlang.org/docs/handbook/classes.html) +- [TypeScript - Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) +- [TypeScript - DefinitelyTyped](http://definitelytyped.org/) +- [Learn TypeScript in X minutes [cn]](https://learnxinyminutes.com/docs/zh-cn/typescript-cn/) +- [TypeScript GitHub](https://github.com/Microsoft/TypeScript) +- [The Best Resources For Learning TypeScript for Game Development](https://www.cocos.com/en/the-best-resources-for-learning-typescript-for-game-development) +- [3 Excuses Developers Give To Avoid TypeScript — and the Better Reasons They Should Use It](https://betterprogramming.pub/the-bad-reasons-people-avoid-typescript-and-the-better-reasons-why-they-shouldnt-86f8d98534de) diff --git a/versions/4.0/en/release-notes/v243-build-template.png b/versions/4.0/en/release-notes/v243-build-template.png new file mode 100644 index 0000000000..609649601b Binary files /dev/null and b/versions/4.0/en/release-notes/v243-build-template.png differ diff --git a/versions/4.0/en/release-notes/v243-windows.png b/versions/4.0/en/release-notes/v243-windows.png new file mode 100644 index 0000000000..1bd8d4b7e1 Binary files /dev/null and b/versions/4.0/en/release-notes/v243-windows.png differ diff --git a/versions/4.0/en/release-notes/v3-assets.png b/versions/4.0/en/release-notes/v3-assets.png new file mode 100644 index 0000000000..9c4fc24a48 Binary files /dev/null and b/versions/4.0/en/release-notes/v3-assets.png differ diff --git a/versions/4.0/en/release-notes/v3-build-native.png b/versions/4.0/en/release-notes/v3-build-native.png new file mode 100644 index 0000000000..b3b3182e51 Binary files /dev/null and b/versions/4.0/en/release-notes/v3-build-native.png differ diff --git a/versions/4.0/en/release-notes/v3-build-template.png b/versions/4.0/en/release-notes/v3-build-template.png new file mode 100644 index 0000000000..99b2b22c62 Binary files /dev/null and b/versions/4.0/en/release-notes/v3-build-template.png differ diff --git a/versions/4.0/en/release-notes/v3-windows.png b/versions/4.0/en/release-notes/v3-windows.png new file mode 100644 index 0000000000..8c454efcbb Binary files /dev/null and b/versions/4.0/en/release-notes/v3-windows.png differ diff --git a/versions/4.0/en/release-notes/web-cocosjs.png b/versions/4.0/en/release-notes/web-cocosjs.png new file mode 100644 index 0000000000..a08cb3d527 Binary files /dev/null and b/versions/4.0/en/release-notes/web-cocosjs.png differ diff --git a/versions/4.0/en/release-notes/web-v243.png b/versions/4.0/en/release-notes/web-v243.png new file mode 100644 index 0000000000..2464ee06f6 Binary files /dev/null and b/versions/4.0/en/release-notes/web-v243.png differ diff --git a/versions/4.0/en/release-notes/web-v3.png b/versions/4.0/en/release-notes/web-v3.png new file mode 100644 index 0000000000..f66a653005 Binary files /dev/null and b/versions/4.0/en/release-notes/web-v3.png differ diff --git a/versions/4.0/en/release-notes/wechat-cocosjs.png b/versions/4.0/en/release-notes/wechat-cocosjs.png new file mode 100644 index 0000000000..8675726308 Binary files /dev/null and b/versions/4.0/en/release-notes/wechat-cocosjs.png differ diff --git a/versions/4.0/en/release-notes/wechat-v243.png b/versions/4.0/en/release-notes/wechat-v243.png new file mode 100644 index 0000000000..df618a8b6e Binary files /dev/null and b/versions/4.0/en/release-notes/wechat-v243.png differ diff --git a/versions/4.0/en/release-notes/wechat-v3.png b/versions/4.0/en/release-notes/wechat-v3.png new file mode 100644 index 0000000000..6e09558a29 Binary files /dev/null and b/versions/4.0/en/release-notes/wechat-v3.png differ diff --git a/versions/4.0/en/render-pipeline/builtin-pipeline.md b/versions/4.0/en/render-pipeline/builtin-pipeline.md new file mode 100644 index 0000000000..706c287035 --- /dev/null +++ b/versions/4.0/en/render-pipeline/builtin-pipeline.md @@ -0,0 +1,37 @@ +# Built-in Render Pipeline + +In Cocos Creator 3.1, the built-in render pipeline includes **builtin-forward** and **builtin-deferred**. The render pipeline can be set via **Project -> Project Settings -> Project Data -> RenderPipeline** in the editor's main menu, and **restart** the editor to take effect after the settings are done. + +![setting](./image/setting.png) + +## Forward Render Pipeline + +The engine uses the **Forward Render Pipeline** by default, and the forward render pipeline is executed as shown below: + + + +Forward rendering consists of two main phases, **ShadowFlow** and **ForwardFlow**: + +- **ShadowFlow** includes a **ShadowStage** that prepares shadow map for objects in the scene that need to cast shadows. +- **ForwardFlow** contains a **ForwardStage** that draws all objects in the scene in **Non-Transparent -> Lighting -> Transparent -> UI** order. When calculating light, each object will be calculated with all light to determine if the object is illuminated, and the light illuminated by the object will be drawn and calculated. Currently there will only be one Directional Light in effect in the scene, and the maximum number of acceptable lights is 16. + +## Deferred Render Pipeline + +The engine currently provides a trial version of the builtin **deferred render pipeline**, which can be used to ease the pressure of lighting calculations for projects with a high number of lights. + + + +The builtin deferred pipeline mainly consists of **ShadowFlow**, **GBufferFlow** and **LightingFlow** stages: +1. **ShadowFlow** is consistent with forward rendering and is used to preform shadow mapping. +2. **GBufferFlow** contains a **GBufferStage** that draws non-transparent objects in the scene. +3. **LightingFlow** contains a **LightingStage** and a **PostProcessStage**, where the **LightingStage** performs screen-space based lighting calculations on the non-transparent objects output to the **GBuffer** before painting the translucent objects, and then **PostProcessStage** will draw the full-screen image obtained by **LightingStage** to the main screen, and then finally draw the UI. + +Since the delay pipeline needs to use the GPU's Multiple Render Targets feature to draw the **GBuffer**, most mobile platforms should support it, but for WebGL1.0 environment, if the platform does not support **WEBGL_draw_buffers** extension, it will not render properly. + +For custom Standard materials, you need to refer to the built-in [builtin-standard.effect](https://github.com/cocos/cocos-engine/blob/v3.1-release/editor/assets/effects/builtin-standard.effect) to add the corresponding deferred pass declaration for the deferred pipeline. As follows as shown in the figure. + + + +If you need to dynamically set the material Uniform to achieve material effect changes, you need to specify the corresponding pass index to update in the deferred pipeline to take effect, instead of updating the pass with index 0 by default, for example, the builtin-standard material has a corresponding passIndex of 1. + +The engine's built-in render pipeline will continue to be optimized and feature added, such as post-processing stage, HDR, reflection, etc., to provide developers with more diverse and rich rendering features. diff --git a/versions/4.0/en/render-pipeline/code/PassUtils.ts b/versions/4.0/en/render-pipeline/code/PassUtils.ts new file mode 100644 index 0000000000..a2131fe9f6 --- /dev/null +++ b/versions/4.0/en/render-pipeline/code/PassUtils.ts @@ -0,0 +1,1148 @@ +import { _decorator, gfx, renderer, pipeline, rendering, + Vec4, macro, geometry, toRadian, cclegacy, Material } from 'cc'; +const { ClearFlagBit, Color, LoadOp, + Format, Rect, StoreOp, Viewport } = gfx; +const {Camera, CSMLevel, LightType, ShadowType, SKYBOX_FLAG} = renderer.scene; +const { supportsR32FloatTexture } = pipeline; +const { AccessType, AttachmentType, ComputeView, LightInfo, QueueHint, + RasterView, ResourceResidency, SceneFlags } = rendering; + +function SRGBToLinear (out, gamma: Vec4) { + out.x = gamma.x * gamma.x; + out.y = gamma.y * gamma.y; + out.z = gamma.z * gamma.z; +} + +export function isUICamera (camera): boolean { + const scene = camera.scene!; + const batches = scene.batches; + for (let i = 0; i < batches.length; i++) { + const batch = batches[i]; + if (camera.visibility & batch.visFlags) { + return true; + } + } + return false; +} + + +let profilerCamera = null; + +export function decideProfilerCamera (cameras: any[]) { + for (let i = cameras.length - 1; i >= 0; --i) { + const camera = cameras[i]; + if (camera.window.swapchain) { + profilerCamera = camera; + return; + } + } + profilerCamera = null; +} + +// Anti-aliasing type, other types will be gradually added in the future +export enum AntiAliasing { + NONE, + FXAA, + FXAAHQ, +} + +export function validPunctualLightsCulling (pipeline, camera) { + const sceneData = pipeline.pipelineSceneData; + const validPunctualLights = sceneData.validPunctualLights; + validPunctualLights.length = 0; + const _sphere = geometry.Sphere.create(0, 0, 0, 1); + const { spotLights } = camera.scene!; + for (let i = 0; i < spotLights.length; i++) { + const light = spotLights[i]; + if (light.baked) { + continue; + } + + geometry.Sphere.set(_sphere, light.position.x, light.position.y, light.position.z, light.range); + if (geometry.intersect.sphereFrustum(_sphere, camera.frustum)) { + validPunctualLights.push(light); + } + } + + const { sphereLights } = camera.scene!; + for (let i = 0; i < sphereLights.length; i++) { + const light = sphereLights[i]; + if (light.baked) { + continue; + } + geometry.Sphere.set(_sphere, light.position.x, light.position.y, light.position.z, light.range); + if (geometry.intersect.sphereFrustum(_sphere, camera.frustum)) { + validPunctualLights.push(light); + } + } +} + +const _cameras = []; + +export function getCameraUniqueID (camera) { + if (_cameras.indexOf(camera) === -1) { + _cameras.push(camera); + } + return _cameras.indexOf(camera); +} + +export function getLoadOpOfClearFlag (clearFlag, attachment) { + let loadOp = LoadOp.CLEAR; + if (!(clearFlag & ClearFlagBit.COLOR) + && attachment === AttachmentType.RENDER_TARGET) { + if (clearFlag & SKYBOX_FLAG) { + loadOp = LoadOp.CLEAR; + } else { + loadOp = LoadOp.LOAD; + } + } + if ((clearFlag & ClearFlagBit.DEPTH_STENCIL) !== ClearFlagBit.DEPTH_STENCIL + && attachment === AttachmentType.DEPTH_STENCIL) { + if (!(clearFlag & ClearFlagBit.DEPTH)) loadOp = LoadOp.LOAD; + if (!(clearFlag & ClearFlagBit.STENCIL)) loadOp = LoadOp.LOAD; + } + return loadOp; +} + +export function getRenderArea (camera, width: number, height: number, light = null, level = 0) { + const out = new Rect(); + const vp = camera ? camera.viewport : new Rect(0, 0, 1, 1); + const w = width; + const h = height; + out.x = vp.x * w; + out.y = vp.y * h; + out.width = vp.width * w; + out.height = vp.height * h; + if (light) { + switch (light.type) { + case LightType.DIRECTIONAL: { + const mainLight = light; + if (mainLight.shadowFixedArea || mainLight.csmLevel === CSMLevel.LEVEL_1) { + out.x = 0; + out.y = 0; + out.width = w; + out.height = h; + } else { + const screenSpaceSignY = cclegacy.director.root.device.capabilities.screenSpaceSignY; + out.x = level % 2 * 0.5 * w; + if (screenSpaceSignY) { + out.y = (1 - Math.floor(level / 2)) * 0.5 * h; + } else { + out.y = Math.floor(level / 2) * 0.5 * h; + } + out.width = 0.5 * w; + out.height = 0.5 * h; + } + break; + } + case LightType.SPOT: { + out.x = 0; + out.y = 0; + out.width = w; + out.height = h; + break; + } + default: + } + } + return out; +} + +class FxaaData { + fxaaMaterial: Material; + private _updateFxaaPass () { + if (!this.fxaaMaterial) return; + + const combinePass = this.fxaaMaterial.passes[0]; + combinePass.beginChangeStatesSilently(); + combinePass.tryCompile(); + combinePass.endChangeStatesSilently(); + } + private _init () { + if (this.fxaaMaterial) return; + this.fxaaMaterial = new Material(); + this.fxaaMaterial._uuid = 'builtin-fxaa-material'; + this.fxaaMaterial.initialize({ effectName: 'pipeline/fxaa-hq' }); + for (let i = 0; i < this.fxaaMaterial.passes.length; ++i) { + this.fxaaMaterial.passes[i].tryCompile(); + } + this._updateFxaaPass(); + } + constructor () { + this._init(); + } +} + +let fxaaData: FxaaData | null = null; +export function buildFxaaPass (camera, + ppl, + inputRT: string) { + if (!fxaaData) { + fxaaData = new FxaaData(); + } + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + let width = camera.window.width; + let height = camera.window.height; + const area = getRenderArea(camera, width, height); + width = area.width; + height = area.height; + // Start + const clearColor = new Color(0, 0, 0, 1); + if (camera.clearFlag & ClearFlagBit.COLOR) { + clearColor.x = camera.clearColor.x; + clearColor.y = camera.clearColor.y; + clearColor.z = camera.clearColor.z; + } + clearColor.w = camera.clearColor.w; + + const fxaaPassRTName = `dsFxaaPassColor${cameraName}`; + const fxaaPassDSName = `dsFxaaPassDS${cameraName}`; + + // ppl.updateRenderWindow(inputRT, camera.window); + if (!ppl.containsResource(fxaaPassRTName)) { + ppl.addRenderTarget(fxaaPassRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(fxaaPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(fxaaPassRTName, width, height); + ppl.updateDepthStencil(fxaaPassDSName, width, height); + const fxaaPassIdx = 0; + const fxaaPass = ppl.addRasterPass(width, height, 'fxaa'); + fxaaPass.name = `CameraFxaaPass${cameraID}`; + fxaaPass.setViewport(new Viewport(area.x, area.y, width, height)); + if (ppl.containsResource(inputRT)) { + const computeView = new ComputeView(); + computeView.name = 'sceneColorMap'; + fxaaPass.addComputeView(inputRT, computeView); + } + const fxaaPassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + clearColor); + fxaaPass.addRasterView(fxaaPassRTName, fxaaPassView); + fxaaData.fxaaMaterial.setProperty('texSize', new Vec4(width, height, 1.0 / width, 1.0 / height), fxaaPassIdx); + fxaaPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, fxaaData.fxaaMaterial, fxaaPassIdx, + SceneFlags.NONE, + ); + return { rtName: fxaaPassRTName, dsName: fxaaPassDSName }; +} + +export const MAX_BLOOM_FILTER_PASS_NUM = 6; +export const BLOOM_PREFILTERPASS_INDEX = 0; +export const BLOOM_DOWNSAMPLEPASS_INDEX = 1; +export const BLOOM_UPSAMPLEPASS_INDEX = BLOOM_DOWNSAMPLEPASS_INDEX + MAX_BLOOM_FILTER_PASS_NUM; +export const BLOOM_COMBINEPASS_INDEX = BLOOM_UPSAMPLEPASS_INDEX + MAX_BLOOM_FILTER_PASS_NUM; +class BloomData { + bloomMaterial: Material; + threshold = 0.1; + iterations = 2; + intensity = 0.8; + private _updateBloomPass () { + if (!this.bloomMaterial) return; + + const prefilterPass = this.bloomMaterial.passes[BLOOM_PREFILTERPASS_INDEX]; + prefilterPass.beginChangeStatesSilently(); + prefilterPass.tryCompile(); + prefilterPass.endChangeStatesSilently(); + + for (let i = 0; i < MAX_BLOOM_FILTER_PASS_NUM; ++i) { + const downsamplePass = this.bloomMaterial.passes[BLOOM_DOWNSAMPLEPASS_INDEX + i]; + downsamplePass.beginChangeStatesSilently(); + downsamplePass.tryCompile(); + downsamplePass.endChangeStatesSilently(); + + const upsamplePass = this.bloomMaterial.passes[BLOOM_UPSAMPLEPASS_INDEX + i]; + upsamplePass.beginChangeStatesSilently(); + upsamplePass.tryCompile(); + upsamplePass.endChangeStatesSilently(); + } + + const combinePass = this.bloomMaterial.passes[BLOOM_COMBINEPASS_INDEX]; + combinePass.beginChangeStatesSilently(); + combinePass.tryCompile(); + combinePass.endChangeStatesSilently(); + } + private _init () { + if (this.bloomMaterial) return; + this.bloomMaterial = new Material(); + this.bloomMaterial._uuid = 'builtin-bloom-material'; + this.bloomMaterial.initialize({ effectName: 'pipeline/bloom' }); + for (let i = 0; i < this.bloomMaterial.passes.length; ++i) { + this.bloomMaterial.passes[i].tryCompile(); + } + this._updateBloomPass(); + } + constructor () { + this._init(); + } +} +let bloomData: BloomData | null = null; +export function buildBloomPasses (camera, + ppl, + inputRT: string, + threshold = 0.1, + iterations = 2, + intensity = 0.8) { + if (!bloomData) { + bloomData = new BloomData(); + } + bloomData.threshold = threshold; + bloomData.iterations = iterations; + bloomData.intensity = intensity; + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + let width = camera.window.width; + let height = camera.window.height; + const area = getRenderArea(camera, width, height); + width = area.width; + height = area.height; + // Start bloom + const bloomClearColor = new Color(0, 0, 0, 1); + if (camera.clearFlag & ClearFlagBit.COLOR) { + bloomClearColor.x = camera.clearColor.x; + bloomClearColor.y = camera.clearColor.y; + bloomClearColor.z = camera.clearColor.z; + } + bloomClearColor.w = camera.clearColor.w; + // ==== Bloom prefilter === + const bloomPassPrefilterRTName = `dsBloomPassPrefilterColor${cameraName}`; + const bloomPassPrefilterDSName = `dsBloomPassPrefilterDS${cameraName}`; + + width >>= 1; + height >>= 1; + if (!ppl.containsResource(bloomPassPrefilterRTName)) { + ppl.addRenderTarget(bloomPassPrefilterRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(bloomPassPrefilterDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(bloomPassPrefilterRTName, width, height); + ppl.updateDepthStencil(bloomPassPrefilterDSName, width, height); + const bloomPrefilterPass = ppl.addRasterPass(width, height, 'bloom-prefilter'); + bloomPrefilterPass.name = `CameraBloomPrefilterPass${cameraID}`; + bloomPrefilterPass.setViewport(new Viewport(area.x, area.y, width, height)); + if (ppl.containsResource(inputRT)) { + const computeView = new ComputeView(); + computeView.name = 'outputResultMap'; + bloomPrefilterPass.addComputeView(inputRT, computeView); + } + const prefilterPassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + bloomClearColor); + bloomPrefilterPass.addRasterView(bloomPassPrefilterRTName, prefilterPassView); + bloomData.bloomMaterial.setProperty('texSize', new Vec4(0, 0, bloomData.threshold, 0), 0); + bloomPrefilterPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, bloomData.bloomMaterial, 0, + SceneFlags.NONE, + ); + // === Bloom downSampler === + for (let i = 0; i < bloomData.iterations; ++i) { + const texSize = new Vec4(width, height, 0, 0); + const bloomPassDownSampleRTName = `dsBloomPassDownSampleColor${cameraName}${i}`; + const bloomPassDownSampleDSName = `dsBloomPassDownSampleDS${cameraName}${i}`; + width >>= 1; + height >>= 1; + if (!ppl.containsResource(bloomPassDownSampleRTName)) { + ppl.addRenderTarget(bloomPassDownSampleRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(bloomPassDownSampleDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(bloomPassDownSampleRTName, width, height); + ppl.updateDepthStencil(bloomPassDownSampleDSName, width, height); + const bloomDownSamplePass = ppl.addRasterPass(width, height, `bloom-downsample${i}`); + bloomDownSamplePass.name = `CameraBloomDownSamplePass${cameraID}${i}`; + bloomDownSamplePass.setViewport(new Viewport(area.x, area.y, width, height)); + const computeView = new ComputeView(); + computeView.name = 'bloomTexture'; + if (i === 0) { + bloomDownSamplePass.addComputeView(bloomPassPrefilterRTName, computeView); + } else { + bloomDownSamplePass.addComputeView(`dsBloomPassDownSampleColor${cameraName}${i - 1}`, computeView); + } + const downSamplePassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + bloomClearColor); + bloomDownSamplePass.addRasterView(bloomPassDownSampleRTName, downSamplePassView); + bloomData.bloomMaterial.setProperty('texSize', texSize, BLOOM_DOWNSAMPLEPASS_INDEX + i); + bloomDownSamplePass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, bloomData.bloomMaterial, BLOOM_DOWNSAMPLEPASS_INDEX + i, + SceneFlags.NONE, + ); + } + // === Bloom upSampler === + for (let i = 0; i < bloomData.iterations; ++i) { + const texSize = new Vec4(width, height, 0, 0); + const bloomPassUpSampleRTName = `dsBloomPassUpSampleColor${cameraName}${bloomData.iterations - 1 - i}`; + const bloomPassUpSampleDSName = `dsBloomPassUpSampleDS${cameraName}${bloomData.iterations - 1 - i}`; + width <<= 1; + height <<= 1; + if (!ppl.containsResource(bloomPassUpSampleRTName)) { + ppl.addRenderTarget(bloomPassUpSampleRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(bloomPassUpSampleDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(bloomPassUpSampleRTName, width, height); + ppl.updateDepthStencil(bloomPassUpSampleDSName, width, height); + const bloomUpSamplePass = ppl.addRasterPass(width, height, `bloom-upsample${i}`); + bloomUpSamplePass.name = `CameraBloomUpSamplePass${cameraID}${bloomData.iterations - 1 - i}`; + bloomUpSamplePass.setViewport(new Viewport(area.x, area.y, width, height)); + const computeView = new ComputeView(); + computeView.name = 'bloomTexture'; + if (i === 0) { + bloomUpSamplePass.addComputeView(`dsBloomPassDownSampleColor${cameraName}${bloomData.iterations - 1}`, computeView); + } else { + bloomUpSamplePass.addComputeView(`dsBloomPassUpSampleColor${cameraName}${bloomData.iterations - i}`, computeView); + } + const upSamplePassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + bloomClearColor); + bloomUpSamplePass.addRasterView(bloomPassUpSampleRTName, upSamplePassView); + bloomData.bloomMaterial.setProperty('texSize', texSize, BLOOM_UPSAMPLEPASS_INDEX + i); + bloomUpSamplePass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, bloomData.bloomMaterial, BLOOM_UPSAMPLEPASS_INDEX + i, + SceneFlags.NONE, + ); + } + // === Bloom Combine Pass === + const bloomPassCombineRTName = `dsBloomPassCombineColor${cameraName}`; + const bloomPassCombineDSName = `dsBloomPassCombineDS${cameraName}`; + + width = area.width; + height = area.height; + if (!ppl.containsResource(bloomPassCombineRTName)) { + ppl.addRenderTarget(bloomPassCombineRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(bloomPassCombineDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(bloomPassCombineRTName, width, height); + ppl.updateDepthStencil(bloomPassCombineDSName, width, height); + const bloomCombinePass = ppl.addRasterPass(width, height, 'bloom-combine'); + bloomCombinePass.name = `CameraBloomCombinePass${cameraID}`; + bloomCombinePass.setViewport(new Viewport(area.x, area.y, width, height)); + const computeViewOut = new ComputeView(); + computeViewOut.name = 'outputResultMap'; + bloomCombinePass.addComputeView(inputRT, computeViewOut); + const computeViewBt = new ComputeView(); + computeViewBt.name = 'bloomTexture'; + bloomCombinePass.addComputeView(`dsBloomPassUpSampleColor${cameraName}${0}`, computeViewBt); + const combinePassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + bloomClearColor); + bloomCombinePass.addRasterView(bloomPassCombineRTName, combinePassView); + bloomData.bloomMaterial.setProperty('texSize', new Vec4(0, 0, 0, bloomData.intensity), BLOOM_COMBINEPASS_INDEX); + bloomCombinePass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, bloomData.bloomMaterial, BLOOM_COMBINEPASS_INDEX, + SceneFlags.NONE, + ); + return { rtName: bloomPassCombineRTName, dsName: bloomPassCombineDSName }; +} + +class PostInfo { + postMaterial: Material; + antiAliasing: AntiAliasing = AntiAliasing.NONE; + private _init () { + this.postMaterial = new Material(); + this.postMaterial.name = 'builtin-post-process-material'; + if (macro.ENABLE_ANTIALIAS_FXAA) { + this.antiAliasing = AntiAliasing.FXAA; + } + this.postMaterial.initialize({ + effectName: 'pipeline/post-process', + defines: { + // Anti-aliasing type, currently only fxaa, so 1 means fxaa + ANTIALIAS_TYPE: this.antiAliasing, + }, + }); + for (let i = 0; i < this.postMaterial.passes.length; ++i) { + this.postMaterial.passes[i].tryCompile(); + } + } + constructor (antiAliasing: AntiAliasing = AntiAliasing.NONE) { + this.antiAliasing = antiAliasing; + this._init(); + } +} + +let postInfo: PostInfo | null = null; + +export function buildPostprocessPass (camera, + ppl, + inputTex: string, + antiAliasing: AntiAliasing = AntiAliasing.NONE) { + if (!postInfo || (postInfo && postInfo.antiAliasing !== antiAliasing)) { + postInfo = new PostInfo(antiAliasing); + } + const cameraID = getCameraUniqueID(camera); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + const postprocessPassRTName = `postprocessPassRTName${cameraID}`; + const postprocessPassDS = `postprocessPassDS${cameraID}`; + if (!ppl.containsResource(postprocessPassRTName)) { + ppl.addRenderTexture(postprocessPassRTName, Format.BGRA8, width, height, camera.window); + ppl.addDepthStencil(postprocessPassDS, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderWindow(postprocessPassRTName, camera.window); + ppl.updateDepthStencil(postprocessPassDS, width, height); + const postprocessPass = ppl.addRasterPass(width, height, 'post-process'); + postprocessPass.name = `CameraPostprocessPass${cameraID}`; + postprocessPass.setViewport(new Viewport(area.x, area.y, area.width, area.height)); + if (ppl.containsResource(inputTex)) { + const computeView = new ComputeView(); + computeView.name = 'outputResultMap'; + postprocessPass.addComputeView(inputTex, computeView); + } + const postClearColor = new Color(0, 0, 0, camera.clearColor.w); + if (camera.clearFlag & ClearFlagBit.COLOR) { + postClearColor.x = camera.clearColor.x; + postClearColor.y = camera.clearColor.y; + postClearColor.z = camera.clearColor.z; + } + const postprocessPassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET), + StoreOp.STORE, + camera.clearFlag, + postClearColor); + const postprocessPassDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearDepth, camera.clearStencil, 0, 0)); + postprocessPass.addRasterView(postprocessPassRTName, postprocessPassView); + postprocessPass.addRasterView(postprocessPassDS, postprocessPassDSView); + postprocessPass.addQueue(QueueHint.NONE).addFullscreenQuad( + postInfo.postMaterial, 0, SceneFlags.NONE, + ); + postprocessPass.addQueue(QueueHint.RENDER_TRANSPARENT).addSceneOfCamera(camera, new LightInfo(), + SceneFlags.UI); + if (profilerCamera === camera) { + postprocessPass.showStatistics = true; + } + return { rtName: postprocessPassRTName, dsName: postprocessPassDS }; +} + +export function buildForwardPass (camera, + ppl, + isOffScreen: boolean) { + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + const cameraInfo = buildShadowPasses(cameraName, camera, ppl); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + + const forwardPassRTName = `dsForwardPassColor${cameraName}`; + const forwardPassDSName = `dsForwardPassDS${cameraName}`; + if (!ppl.containsResource(forwardPassRTName)) { + if (!isOffScreen) { + ppl.addRenderTexture(forwardPassRTName, Format.BGRA8, width, height, camera.window); + } else { + ppl.addRenderTarget(forwardPassRTName, Format.RGBA16F, width, height, ResourceResidency.MANAGED); + } + ppl.addDepthStencil(forwardPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + if (!isOffScreen) { + ppl.updateRenderWindow(forwardPassRTName, camera.window); + ppl.updateDepthStencil(forwardPassDSName, width, height); + } else { + ppl.updateRenderTarget(forwardPassRTName, width, height); + ppl.updateDepthStencil(forwardPassDSName, width, height); + } + const forwardPass = ppl.addRasterPass(width, height, 'default'); + forwardPass.name = `CameraForwardPass${cameraID}`; + forwardPass.setViewport(new Viewport(area.x, area.y, width, height)); + for (const dirShadowName of cameraInfo.mainLightShadowNames) { + if (ppl.containsResource(dirShadowName)) { + const computeView = new ComputeView('cc_shadowMap'); + forwardPass.addComputeView(dirShadowName, computeView); + } + } + for (const spotShadowName of cameraInfo.spotLightShadowNames) { + if (ppl.containsResource(spotShadowName)) { + const computeView = new ComputeView('cc_spotShadowMap'); + forwardPass.addComputeView(spotShadowName, computeView); + } + } + const passView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + isOffScreen ? LoadOp.CLEAR : getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearColor.x, camera.clearColor.y, camera.clearColor.z, camera.clearColor.w)); + const passDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + isOffScreen ? LoadOp.CLEAR : getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearDepth, camera.clearStencil, 0, 0)); + forwardPass.addRasterView(forwardPassRTName, passView); + forwardPass.addRasterView(forwardPassDSName, passDSView); + forwardPass + .addQueue(QueueHint.RENDER_OPAQUE) + .addSceneOfCamera(camera, new LightInfo(), + SceneFlags.OPAQUE_OBJECT | SceneFlags.PLANAR_SHADOW | SceneFlags.CUTOUT_OBJECT + | SceneFlags.DEFAULT_LIGHTING | SceneFlags.DRAW_INSTANCING); + let sceneFlags = SceneFlags.TRANSPARENT_OBJECT | SceneFlags.GEOMETRY; + if (!isOffScreen) { + sceneFlags |= SceneFlags.UI; + forwardPass.showStatistics = true; + } + forwardPass + .addQueue(QueueHint.RENDER_TRANSPARENT) + .addSceneOfCamera(camera, new LightInfo(), sceneFlags); + return { rtName: forwardPassRTName, dsName: forwardPassDSName }; +} + +export function buildShadowPass (passName: Readonly, + ppl, + camera, light, level: number, + width: Readonly, height: Readonly) { + const fboW = width; + const fboH = height; + const area = getRenderArea(camera, width, height, light, level); + width = area.width; + height = area.height; + const device = ppl.device; + const shadowMapName = passName; + if (!ppl.containsResource(shadowMapName)) { + const format = supportsR32FloatTexture(device) ? Format.R32F : Format.RGBA8; + ppl.addRenderTarget(shadowMapName, format, fboW, fboH, ResourceResidency.MANAGED); + ppl.addDepthStencil(`${shadowMapName}Depth`, Format.DEPTH_STENCIL, fboW, fboH, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(shadowMapName, fboW, fboH); + ppl.updateDepthStencil(`${shadowMapName}Depth`, fboW, fboH); + const pass = ppl.addRasterPass(width, height, 'default'); + pass.name = passName; + pass.setViewport(new Viewport(area.x, area.y, area.width, area.height)); + pass.addRasterView(shadowMapName, new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + ClearFlagBit.COLOR, + new Color(1, 1, 1, camera.clearColor.w))); + pass.addRasterView(`${shadowMapName}Depth`, new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + LoadOp.CLEAR, StoreOp.DISCARD, + ClearFlagBit.DEPTH_STENCIL, + new Color(camera.clearDepth, camera.clearStencil, 0, 0))); + const queue = pass.addQueue(QueueHint.RENDER_OPAQUE); + queue.addSceneOfCamera(camera, new LightInfo(light, level), + SceneFlags.SHADOW_CASTER); +} + +export function buildReflectionProbePasss (camera, + ppl, + isOffScreen: boolean) { + const probes = cclegacy.internal.reflectionProbeManager.getProbes(); + if (probes.length === 0) { + return; + } + for (let i = 0; i < probes.length; i++) { + const probe = probes[i]; + if (probe.needRender) { + for (let faceIdx = 0; faceIdx < probe.bakedCubeTextures.length; faceIdx++) { + buildReflectionProbePass(camera, ppl, probe, probe.bakedCubeTextures[faceIdx].window!, faceIdx); + } + probe.needRender = false; + } + } +} + +export function buildReflectionProbePass (camera, + ppl, probe, renderWindow, faceIdx: number) { + const cameraName = `Camera${faceIdx}`; + const area = probe.renderArea(); + const width = area.x; + const height = area.y; + const probeCamera = probe.camera; + + const probePassRTName = `reflectionProbePassColor${cameraName}`; + const probePassDSName = `reflectionProbePassDS${cameraName}`; + + probe.updateCameraDir(faceIdx); + + if (!ppl.containsResource(probePassRTName)) { + ppl.addRenderTexture(probePassRTName, Format.RGBA8, width, height, renderWindow); + ppl.addDepthStencil(probePassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderWindow(probePassRTName, renderWindow); + ppl.updateDepthStencil(probePassDSName, width, height); + + const probePass = ppl.addRasterPass(width, height, 'default'); + probePass.name = `ReflectionProbePass${faceIdx}`; + probePass.setViewport(new Viewport(0, 0, width, height)); + + const passView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + getLoadOpOfClearFlag(probeCamera.clearFlag, AttachmentType.RENDER_TARGET), + StoreOp.STORE, + probeCamera.clearFlag, + new Color(probeCamera.clearColor.x, probeCamera.clearColor.y, probeCamera.clearColor.z, probeCamera.clearColor.w)); + const passDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + getLoadOpOfClearFlag(probeCamera.clearFlag, AttachmentType.DEPTH_STENCIL), + StoreOp.STORE, + probeCamera.clearFlag, + new Color(probeCamera.clearDepth, probeCamera.clearStencil, 0, 0)); + probePass.addRasterView(probePassRTName, passView); + probePass.addRasterView(probePassDSName, passDSView); + const passBuilder = probePass.addQueue(QueueHint.RENDER_OPAQUE); + passBuilder.addSceneOfCamera(camera, new LightInfo(), SceneFlags.REFLECTION_PROBE); + updateCameraUBO(passBuilder as unknown as any, probeCamera, ppl); +} + +class CameraInfo { + shadowEnabled = false; + mainLightShadowNames = new Array(); + spotLightShadowNames = new Array(); +} + +export function buildShadowPasses (cameraName: string, camera, ppl): CameraInfo { + validPunctualLightsCulling(ppl, camera); + const pipeline = ppl; + const shadowInfo = pipeline.pipelineSceneData.shadows; + const validPunctualLights = ppl.pipelineSceneData.validPunctualLights; + const cameraInfo = new CameraInfo(); + const shadows = ppl.pipelineSceneData.shadows; + if (!shadowInfo.enabled || shadowInfo.type !== ShadowType.ShadowMap) { return cameraInfo; } + cameraInfo.shadowEnabled = true; + const _validLights: any[] = []; + let n = 0; + let m = 0; + for (;n < shadowInfo.maxReceived && m < validPunctualLights.length;) { + const light = validPunctualLights[m]; + if (light.type === LightType.SPOT) { + const spotLight = light; + if (spotLight.shadowEnabled) { + _validLights.push(light); + n++; + } + } + m++; + } + + const { mainLight } = camera.scene!; + // build shadow map + const mapWidth = shadows.size.x; + const mapHeight = shadows.size.y; + if (mainLight && mainLight.shadowEnabled) { + cameraInfo.mainLightShadowNames[0] = `MainLightShadow${cameraName}`; + if (mainLight.shadowFixedArea) { + buildShadowPass(cameraInfo.mainLightShadowNames[0], ppl, + camera, mainLight, 0, mapWidth, mapHeight); + } else { + const csmLevel = pipeline.pipelineSceneData.csmSupported ? mainLight.csmLevel : 1; + for (let i = 0; i < csmLevel; i++) { + cameraInfo.mainLightShadowNames[i] = `MainLightShadow${cameraName}`; + buildShadowPass(cameraInfo.mainLightShadowNames[i], ppl, + camera, mainLight, i, mapWidth, mapHeight); + } + } + } + + for (let l = 0; l < _validLights.length; l++) { + const light = _validLights[l]; + const passName = `SpotLightShadow${l.toString()}${cameraName}`; + cameraInfo.spotLightShadowNames[l] = passName; + buildShadowPass(passName, ppl, + camera, light, 0, mapWidth, mapHeight); + } + return cameraInfo; +} + +export class GBufferInfo { + color!: string; + normal!: string; + emissive!: string; + ds!: string; +} +// deferred passes +export function buildGBufferPass (camera, + ppl) { + const cameraID = getCameraUniqueID(camera); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + const gBufferPassRTName = `gBufferPassColorCamera`; + const gBufferPassNormal = `gBufferPassNormal`; + const gBufferPassEmissive = `gBufferPassEmissive`; + const gBufferPassDSName = `gBufferPassDSCamera`; + if (!ppl.containsResource(gBufferPassRTName)) { + const colFormat = Format.RGBA16F; + ppl.addRenderTarget(gBufferPassRTName, colFormat, width, height, ResourceResidency.MANAGED); + ppl.addRenderTarget(gBufferPassNormal, colFormat, width, height, ResourceResidency.MANAGED); + ppl.addRenderTarget(gBufferPassEmissive, colFormat, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(gBufferPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(gBufferPassRTName, width, height); + ppl.updateRenderTarget(gBufferPassNormal, width, height); + ppl.updateRenderTarget(gBufferPassEmissive, width, height); + ppl.updateDepthStencil(gBufferPassDSName, width, height); + // gbuffer pass + const gBufferPass = ppl.addRasterPass(width, height, 'default'); + gBufferPass.name = `CameraGBufferPass${cameraID}`; + gBufferPass.setViewport(new Viewport(area.x, area.y, area.width, area.height)); + const rtColor = new Color(0, 0, 0, 0); + if (camera.clearFlag & ClearFlagBit.COLOR) { + if (ppl.pipelineSceneData.isHDR) { + SRGBToLinear(rtColor, camera.clearColor); + } else { + rtColor.x = camera.clearColor.x; + rtColor.y = camera.clearColor.y; + rtColor.z = camera.clearColor.z; + } + } + const passColorView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + rtColor); + const passNormalView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + new Color(0, 0, 0, 0)); + const passEmissiveView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + new Color(0, 0, 0, 0)); + const passDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearDepth, camera.clearStencil, 0, 0)); + gBufferPass.addRasterView(gBufferPassRTName, passColorView); + gBufferPass.addRasterView(gBufferPassNormal, passNormalView); + gBufferPass.addRasterView(gBufferPassEmissive, passEmissiveView); + gBufferPass.addRasterView(gBufferPassDSName, passDSView); + gBufferPass + .addQueue(QueueHint.RENDER_OPAQUE) + .addSceneOfCamera(camera, new LightInfo(), SceneFlags.OPAQUE_OBJECT | SceneFlags.CUTOUT_OBJECT); + const gBufferInfo = new GBufferInfo(); + gBufferInfo.color = gBufferPassRTName; + gBufferInfo.normal = gBufferPassNormal; + gBufferInfo.emissive = gBufferPassEmissive; + gBufferInfo.ds = gBufferPassDSName; + return gBufferInfo; +} + +class LightingInfo { + deferredLightingMaterial: Material; + private _init () { + this.deferredLightingMaterial = new Material(); + this.deferredLightingMaterial.name = 'builtin-deferred-material'; + this.deferredLightingMaterial.initialize({ + effectName: 'pipeline/deferred-lighting', + defines: { CC_RECEIVE_SHADOW: 1 }, + }); + for (let i = 0; i < this.deferredLightingMaterial.passes.length; ++i) { + this.deferredLightingMaterial.passes[i].tryCompile(); + } + } + constructor () { + this._init(); + } +} + +let lightingInfo: LightingInfo | null = null; + +// deferred lighting pass +export function buildLightingPass (camera, ppl, gBuffer: GBufferInfo) { + if (!lightingInfo) { + lightingInfo = new LightingInfo(); + } + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + const cameraInfo = buildShadowPasses(cameraName, camera, ppl); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + + const deferredLightingPassRTName = `deferredLightingPassRTName`; + const deferredLightingPassDS = `deferredLightingPassDS`; + if (!ppl.containsResource(deferredLightingPassRTName)) { + ppl.addRenderTarget(deferredLightingPassRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(deferredLightingPassDS, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(deferredLightingPassRTName, width, height); + ppl.updateDepthStencil(deferredLightingPassDS, width, height); + // lighting pass + const lightingPass = ppl.addRasterPass(width, height, 'deferred-lighting'); + lightingPass.name = `CameraLightingPass${cameraID}`; + lightingPass.setViewport(new Viewport(area.x, area.y, width, height)); + for (const dirShadowName of cameraInfo.mainLightShadowNames) { + if (ppl.containsResource(dirShadowName)) { + const computeView = new ComputeView('cc_shadowMap'); + lightingPass.addComputeView(dirShadowName, computeView); + } + } + for (const spotShadowName of cameraInfo.spotLightShadowNames) { + if (ppl.containsResource(spotShadowName)) { + const computeView = new ComputeView('cc_spotShadowMap'); + lightingPass.addComputeView(spotShadowName, computeView); + } + } + if (ppl.containsResource(gBuffer.color)) { + const computeView = new ComputeView(); + computeView.name = 'gbuffer_albedoMap'; + lightingPass.addComputeView(gBuffer.color, computeView); + + const computeNormalView = new ComputeView(); + computeNormalView.name = 'gbuffer_normalMap'; + lightingPass.addComputeView(gBuffer.normal, computeNormalView); + + const computeEmissiveView = new ComputeView(); + computeEmissiveView.name = 'gbuffer_emissiveMap'; + lightingPass.addComputeView(gBuffer.emissive, computeEmissiveView); + + const computeDepthView = new ComputeView(); + computeDepthView.name = 'depth_stencil'; + lightingPass.addComputeView(gBuffer.ds, computeDepthView); + } + const lightingClearColor = new Color(0, 0, 0, 0); + if (camera.clearFlag & ClearFlagBit.COLOR) { + lightingClearColor.x = camera.clearColor.x; + lightingClearColor.y = camera.clearColor.y; + lightingClearColor.z = camera.clearColor.z; + } + lightingClearColor.w = 0; + const lightingPassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + lightingClearColor); + lightingPass.addRasterView(deferredLightingPassRTName, lightingPassView); + lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, lightingInfo.deferredLightingMaterial, 0, + SceneFlags.VOLUMETRIC_LIGHTING, + ); + lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addSceneOfCamera(camera, new LightInfo(), + SceneFlags.TRANSPARENT_OBJECT | SceneFlags.PLANAR_SHADOW | SceneFlags.GEOMETRY); + return { rtName: deferredLightingPassRTName, dsName: deferredLightingPassDS }; +} + +function getClearFlags (attachment: rendering.AttachmentType, clearFlag, loadOp) { + switch (attachment) { + case AttachmentType.DEPTH_STENCIL: + if (loadOp === LoadOp.CLEAR) { + if (clearFlag & ClearFlagBit.DEPTH_STENCIL) { + return clearFlag; + } else { + return ClearFlagBit.DEPTH_STENCIL; + } + } else { + return ClearFlagBit.NONE; + } + case AttachmentType.RENDER_TARGET: + default: + if (loadOp === LoadOp.CLEAR) { + return ClearFlagBit.COLOR; + } else { + return ClearFlagBit.NONE; + } + } +} + +export function buildUIPass (camera, + ppl) { + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + + const dsUIAndProfilerPassRTName = `dsUIAndProfilerPassColor${cameraName}`; + const dsUIAndProfilerPassDSName = `dsUIAndProfilerPassDS${cameraName}`; + if (!ppl.containsResource(dsUIAndProfilerPassRTName)) { + ppl.addRenderTexture(dsUIAndProfilerPassRTName, Format.BGRA8, width, height, camera.window); + ppl.addDepthStencil(dsUIAndProfilerPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderWindow(dsUIAndProfilerPassRTName, camera.window); + ppl.updateDepthStencil(dsUIAndProfilerPassDSName, width, height); + const uIAndProfilerPass = ppl.addRasterPass(width, height, 'default'); + uIAndProfilerPass.name = `CameraUIAndProfilerPass${cameraID}`; + uIAndProfilerPass.setViewport(new Viewport(area.x, area.y, width, height)); + const passView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearColor.x, camera.clearColor.y, camera.clearColor.z, camera.clearColor.w)); + const passDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearDepth, camera.clearStencil, 0, 0)); + uIAndProfilerPass.addRasterView(dsUIAndProfilerPassRTName, passView); + uIAndProfilerPass.addRasterView(dsUIAndProfilerPassDSName, passDSView); + const sceneFlags = SceneFlags.UI; + uIAndProfilerPass + .addQueue(QueueHint.RENDER_TRANSPARENT) + .addSceneOfCamera(camera, new LightInfo(), sceneFlags); + if (profilerCamera === camera) { + uIAndProfilerPass.showStatistics = true; + } +} + +export function buildNativeForwardPass (camera, ppl) { + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + + // Resources + const forwardPassRTName = `dsForwardPassColor${cameraName}`; + const forwardPassDSName = `dsForwardPassDS${cameraName}`; + if (!ppl.containsResource(forwardPassRTName)) { + ppl.addRenderTexture(forwardPassRTName, Format.BGRA8, width, height, camera.window); + ppl.addDepthStencil(forwardPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + + ppl.updateRenderWindow(forwardPassRTName, camera.window); + ppl.updateDepthStencil(forwardPassDSName, width, height); + // Passes + const forwardPass = ppl.addRasterPass(width, height, 'default'); + forwardPass.name = `CameraForwardPass${cameraID}`; + forwardPass.setViewport(new Viewport(area.x, area.y, width, height)); + + const cameraRenderTargetLoadOp = getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET); + const cameraDepthStencilLoadOp = getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL); + + forwardPass.addRasterView(forwardPassRTName, + new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + cameraRenderTargetLoadOp, + StoreOp.STORE, + getClearFlags(AttachmentType.RENDER_TARGET, camera.clearFlag, cameraRenderTargetLoadOp), + new Color(camera.clearColor.x, camera.clearColor.y, camera.clearColor.z, camera.clearColor.w))); + forwardPass.addRasterView(forwardPassDSName, + new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + cameraDepthStencilLoadOp, + StoreOp.STORE, + getClearFlags(AttachmentType.DEPTH_STENCIL, camera.clearFlag, cameraDepthStencilLoadOp), + new Color(camera.clearDepth, camera.clearStencil, 0, 0))); + + forwardPass + .addQueue(QueueHint.RENDER_OPAQUE) + .addSceneOfCamera(camera, new LightInfo(), + SceneFlags.OPAQUE_OBJECT + | SceneFlags.PLANAR_SHADOW + | SceneFlags.CUTOUT_OBJECT + | SceneFlags.DEFAULT_LIGHTING + | SceneFlags.DRAW_INSTANCING); + forwardPass + .addQueue(QueueHint.RENDER_TRANSPARENT) + .addSceneOfCamera(camera, new LightInfo(), + SceneFlags.TRANSPARENT_OBJECT + | SceneFlags.GEOMETRY); + forwardPass + .addQueue(QueueHint.RENDER_TRANSPARENT) + .addSceneOfCamera(camera, new LightInfo(), + SceneFlags.UI); + forwardPass.showStatistics = true; +} + +export function buildNativeDeferredPipeline (camera, ppl) { + const cameraID = getCameraUniqueID(camera); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + if (!ppl.containsResource('Albedo')) { + // GBuffers + ppl.addRenderTarget('Albedo', Format.RGBA16F, width, height, ResourceResidency.MANAGED); + ppl.addRenderTarget('Normal', Format.RGBA16F, width, height, ResourceResidency.MANAGED); + ppl.addRenderTarget('Emissive', Format.RGBA16F, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil('DepthStencil', Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + // Lighting + ppl.addRenderTexture('Color', Format.BGRA8, width, height, camera.window); + } + if (!lightingInfo) { + lightingInfo = new LightingInfo(); + } + // GeometryPass + { + const gBufferPass = ppl.addRasterPass(width, height, 'default'); + gBufferPass.name = 'GeometryPass'; + gBufferPass.setViewport(new Viewport(area.x, area.y, area.width, area.height)); + + gBufferPass.addRasterView('Albedo', new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, ClearFlagBit.COLOR)); + gBufferPass.addRasterView('Normal', new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, ClearFlagBit.COLOR)); + gBufferPass.addRasterView('Emissive', new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, ClearFlagBit.COLOR)); + gBufferPass.addRasterView('DepthStencil', new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + LoadOp.CLEAR, StoreOp.STORE, + ClearFlagBit.DEPTH_STENCIL, + new Color(1, 0, 0, 0))); + gBufferPass + .addQueue(QueueHint.RENDER_OPAQUE) + .addSceneOfCamera(camera, new LightInfo(), SceneFlags.OPAQUE_OBJECT | SceneFlags.CUTOUT_OBJECT); + } + // LightingPass + { + const lightingPass = ppl.addRasterPass(width, height, 'default'); + lightingPass.name = 'LightingPass'; + lightingPass.setViewport(new Viewport(area.x, area.y, width, height)); + + const lightingClearColor = new Color(0, 0, 0, 0); + if (camera.clearFlag & ClearFlagBit.COLOR) { + lightingClearColor.x = camera.clearColor.x; + lightingClearColor.y = camera.clearColor.y; + lightingClearColor.z = camera.clearColor.z; + } + lightingClearColor.w = 1; + lightingPass.addRasterView('Color', new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + lightingClearColor)); + + lightingPass.addComputeView('Albedo', new ComputeView('gbuffer_albedoMap')); + lightingPass.addComputeView('Normal', new ComputeView('gbuffer_normalMap')); + lightingPass.addComputeView('Emissive', new ComputeView('gbuffer_emissiveMap')); + lightingPass.addComputeView('DepthStencil', new ComputeView('depth_stencil')); + + lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, lightingInfo.deferredLightingMaterial, 0, + SceneFlags.VOLUMETRIC_LIGHTING, + ); + lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addSceneOfCamera(camera, + new LightInfo(), + SceneFlags.TRANSPARENT_OBJECT | SceneFlags.PLANAR_SHADOW | SceneFlags.GEOMETRY); + } +} + +export function updateCameraUBO (setter: any, camera, ppl) { + const pipeline = cclegacy.director.root.pipeline; + const sceneData = ppl.pipelineSceneData; + const skybox = sceneData.skybox; + setter.addConstant('CCCamera'); + setter.setMat4('cc_matView', camera.matView); + setter.setMat4('cc_matViewInv', camera.node.worldMatrix); + setter.setMat4('cc_matProj', camera.matProj); + setter.setMat4('cc_matProjInv', camera.matProjInv); + setter.setMat4('cc_matViewProj', camera.matViewProj); + setter.setMat4('cc_matViewProjInv', camera.matViewProjInv); + setter.setVec4('cc_cameraPos', new Vec4(camera.position.x, camera.position.y, camera.position.z, pipeline.getCombineSignY())); + // eslint-disable-next-line max-len + setter.setVec4('cc_surfaceTransform', new Vec4(camera.surfaceTransform, 0.0, Math.cos(toRadian(skybox.getRotationAngle())), Math.sin(toRadian(skybox.getRotationAngle())))); + // eslint-disable-next-line max-len + setter.setVec4('cc_screenScale', new Vec4(sceneData.shadingScale, sceneData.shadingScale, 1.0 / sceneData.shadingScale, 1.0 / sceneData.shadingScale)); + setter.setVec4('cc_exposure', new Vec4(camera.exposure, 1.0 / camera.exposure, sceneData.isHDR ? 1.0 : 0.0, 1.0 / Camera.standardExposureValue)); +} + diff --git a/versions/4.0/en/render-pipeline/custom-pipeline.md b/versions/4.0/en/render-pipeline/custom-pipeline.md new file mode 100644 index 0000000000..386cb7588b --- /dev/null +++ b/versions/4.0/en/render-pipeline/custom-pipeline.md @@ -0,0 +1,380 @@ +# Custom Render Pipeline(experimental) + +A new **Custom Render Pipeline** has been added in Cocos Creator 3.6. + +The interface and naming are not yet stable and are not recommended for use in formal projects. Currently, only the web platform is supported. + +The interface of the **custom rendering pipeline** is located in `cocos/core/pipeline/custom/pipeline.ts` + +## Overview + +The **CustomPipeline** allows users to customize the **RenderPass**, set the input/output **RenderView**, and the **RenderContent** to be drawn for each **RenderPass**. + +**Render content** can be a **Scene**, a screen **Quad**, or a **Dispatch** of a computational task, depending on the type of **RenderPass**. + +The order in which **rendered content** is drawn can be adjusted by a **RenderQueue** (RenderQueue). + +The [**RenderPass**, **RenderQueue**, **RenderContent**] of **CustomRenderPipeline** constitute a forest of. + + + +**Custom Rendering Pipeline** of [**Render Pass**, **Render View**] constitutes a directed acyclic graph (DAG). + + + +We can stack the above two graphs to get the **RenderGraph**. The **RenderGraph** describes the entire flow of the **Custom Render Pipeline**, and the engine will allocate resources, optimize the flow, and execute the rendering according to the user's customized flow graph. + +## RenderPass + +**RenderPass** There are three types: Raster, Compute, and Resource. + +Each type will have its own different **RenderPass**. + +### Raster + +The raster type uses the rasterization capabilities of the GPU (executed in GraphicsEngine). + +#### 1. RasterPass + + + +- width, height is the resolution of the output rendering target. + +- layoutName is the name of the Effect's Stage. + +- name is the name displayed during debugging. If it is empty, the system will give the default name. + +#### 2. RasterSubpass + +Feature not yet open. Requires GPU Tile-based rendering capability. + +#### 3. PresentPass + +Present the content to the screen. + +### Compute + +The compute type uses the general-purpose computing power of the GPU, as well as ray-tracing capabilities (executable in GraphicsEngine, ComputeEngine). + +#### 1. ComputePass + + + +- layoutName is the name of the Effect's Stage. + +- name is the name displayed during debugging. If it is empty, the system will give the default name. + +#### 2. RaytracePass + +Feature not yet open. Requires GPU ray tracing capability. + +### Resource + +The resource type uses the resource processing power of the GPU (executable in GraphicsEngine, ComputeEngine, CopyEngine). + +#### 1. CopyPass + +Responsible for copying the resource source to the target, which requires the resource format to be compatible. + + + +- name is the name displayed during debugging. If it is empty, the system will give the default name. + +#### 2. MovePass + +Responsible for moving the resource source to the target, requiring the resource to be in the same format. + +Move here is a semantic concept (move semantics): move the source variable to the target variable, expiring the source variable. If the resource cannot be moved for some reason (e.g. the resource source is being read), this is done as a copy. + +Move semantics are used for pipeline optimization for the purpose of bandwidth reduction. If you are not sure how to use **Move Pass** properly, you can use **Copy Pass** instead, which will not affect the rendering result and is easier to debug. + +## RenderView + +There are two types of RenderView: **RasterView**, **ComputeView**. + +### RasterView + +**Rasterized view** will be rasterized. There are two subtypes: RenderTarget, DepthStencil. + + + +- slotName is the name of the shader pixel component. (e.g. color, normal, etc.) + +- accessType is the binding type, which can be Read, ReadWrite, Write, Read as input, Write as output, ReadWrite as both input and output, [Note] DepthStencil When doing DepthTest, although the result is not written to the view, but as output, the binding type is still Write. some platforms open ARM_shader_framebuffer_fetch_depth_stencil extension, DepthStencil binding type is ReadWrite.-> When ARM_shader_framebuffer_fetch_depth_stencil extension is enabled in a shader, the corresponding DepthStencil binding type should be set to ReadWrite + +- attachmentType is the type, can be RenderTarget or DepthStencil. + +- loadOp is the rasterized read option, which can be Read, Clear, or Discard. + +- storeOp is the rasterized store option, can be Write (Store), Discard (Discard). + +- clearFlags is the clear flag bit, if the type is RenderTarget, the flag bit must be Color. if the type is DepthStencil, it is one of Depth, Stencil, Depth | Stencil. + +- clearColor is the clear color, if the type is RenderTarget, it is RGBA (Float4). If the type is DepthStencil, it is RG, where R stores Depth (Float) and G stores Stencil (Uint8). + +### ComputeView + +**Computed view** will not be rasterized. Commonly used for Samples, Unordered Access. + + + +- name is the name of the Shader descriptor (Descriptor). + +- accessType is the read/write type. It can be Read, ReadWrite, or Write. + +- clearFlags is the clear type of the resource, usually None or Color. + +- clearColor is the clear color of the resource, Float4 or Int4. depends on clearValueType. + +- clearValueType is the type of the resource's clear color, Float or Int. + +If the resource is marked with a clear color, then the resource content is cleared with clearColor before executing **ComputePass**. Raster type passes(Raster) do not clear **ComputeView** content. + +## Rendering view settings + +**Raster**: + + + +**Compute**: + + + +## RenderQueue + +The **Render Queue** is a child node of the **Render Pass** (Render Pass) and has a strict (rendering) order of precedence. Only after the contents of one **Render Queue** are fully drawn, the contents of the next **Render Queue** will be drawn. + +There are two types of **render queues**: **rasterization queue**, **computation queue**. are added in **rasterization pass** and **computation pass**, respectively. + +### RasterQueue + +**Rasterization queue** performs rasterization tasks, typically drawing **scenes**, drawing full-screen quads, etc. **Rasterization queue** is internally drawn in unspecified order. + + + + + +- hint is a queue hint with four options None, Opaque, Cutout, Transparent. hint does not affect execution and is only used for performance testing. For example, on mobile platforms, we often want to draw the Opaque queue first (with AlphaTest off) and then the Cutout queue (with AlphaTest on). If an object with AlphaTest on is accidentally mixed in with the draw content of the Opaque queue, it will degrade the graphics performance. Therefore, we will check if the user's commit meets the expectation through the queue hint. + +- name is the name displayed during debugging. If it is empty, the system will give the default name. + +### ComputeQueue + +**Compute Queue** contains only **Dispatch** , executed sequentially. + + + + + +**Compute Pass** No queue hint. + +## RenderContent + +**Rendering content** is sorted by **rendering queue** and consists of multiple elements. + +### Scene + +2D and 3D **scenes** that need to be drawn. Suitable for **RasterQueue**. + + + +Can be added via camera or directly. Certain lighting information can be attached. + +- sceneFlags controls to some extent the rendering of **scene**. For example, which objects are rendered (Opaque, Cutout, Transparent), whether to render only shadow cast objects (ShadowCaster), whether to render only the UI, the lighting method (None, Default, Volumetirc, Clustered, PlanarShadow), whether to render GeometryRenderer, whether to render Profiler, etc. + +### Quad + +Full screen / partial **Quad**. Commonly used for post-effects rendering. Suitable for **RasterQueue**. + + + +### Dispatch + +Used for **ComputeQueue**. + + + +### Dynamic Settings + +We can dynamically set some properties of Queue, Pass. + +For example, viewport, clearRenderTarget, etc. + +## Rendering Data Settings(under development) + +When writing rendering algorithms, we often need to set up some data for the Shader to use. + +The **RenderGraph** provides interfaces for setting data in **RenderPass**, **RenderQueue**. + + + +The user can set Constant, Buffer, Texture, and other data. + +This data can be read-only or always in read/write state. + +For resources with read/write state changes, we recommend tracking with **RenderView**. + +Each **RenderPass**, **RenderQueue** has its own separate storage. + +Each node has a different data update/upload frequency. The update frequency of user filled constants, Shader Descriptor (Descriptor) needs to match the update frequency of the node. + +- **Rendering Pass**: one upload per **rendering pass** (PerPass). + +- **Rendering Queue**: one upload per **rendering phase** (PerPhase). + + + +## Feature Enable + +Check the **Custom Rendering Pipeline** in **Project Settings** -> **Feature Cropping**: + + + +Select a registered **custom rendering pipeline** by filling in the name of the **custom pipeline**. + +**Forward Render Pipeline** (named Custom or Forward) and **Backward Render Pipeline** (name Deferred) are currently supported. + + + +## Write a Custom Render Pipeline + +Create a new TypeScript file, define a class named TestCustomPipeline, have it implement the `rendering.PipelineBuilder` interface, and register the pipeline to the system via the `rendering.setCustomPipeline` method, as shown in the following code. + +```typescript +import { _decorator, rendering, renderer, game, Game } from 'cc'; +import { AntiAliasing, buildForwardPass, buildBloomPasses, + buildFxaaPass, buildPostprocessPass, buildUIPass, isUICamera, decideProfilerCamera } from './PassUtils'; + +export class TestCustomPipeline implements rendering.PipelineBuilder { + setup(cameras: renderer.scene.Camera[], pipeline: rendering.Pipeline): void { + decideProfilerCamera(cameras); + for (let i = 0; i < cameras.length; i++) { + const camera = cameras[i]; + if (camera.scene === null) { + continue; + } + const isGameView = camera.cameraUsage === renderer.scene.CameraUsage.GAME + || camera.cameraUsage === renderer.scene.CameraUsage.GAME_VIEW; + if (!isGameView) { + // forward pass + buildForwardPass(camera, pipeline, isGameView); + continue; + } + // TODO: The actual project is not so simple to determine whether the ui camera, here is just as a demo demonstration. + if (!isUICamera(camera)) { + // forward pass + const forwardInfo = buildForwardPass(camera, pipeline, isGameView); + // fxaa pass + const fxaaInfo = buildFxaaPass(camera, pipeline, forwardInfo.rtName); + // bloom passes + const bloomInfo = buildBloomPasses(camera, pipeline, fxaaInfo.rtName); + // Present Pass + buildPostprocessPass(camera, pipeline, bloomInfo.rtName, AntiAliasing.NONE); + continue; + } + // render ui + buildUIPass(camera, pipeline); + } +} +} + +game.on(Game.EVENT_RENDERER_INITED, () => { + rendering.setCustomPipeline('Test', new TestCustomPipeline); +}); + +``` + +You can see that the above code references the PassUtils script file, which facilitates direct use by users by simply encapsulating the logic associated with the commonly used `RenderPass` (PassUtils can be found here [download](./code/PassUtils.ts)). + +PassUtils has quite a few functions, and we take some of the logic of `buildPostprocessPass` to introduce. + +```typescript +function buildPostprocessPass (camera, + ppl, + inputTex: string, + antiAliasing: AntiAliasing = AntiAliasing.NONE) { + // ... + const postprocessPassRTName = `postprocessPassRTName${cameraID}`; + const postprocessPassDS = `postprocessPassDS${cameraID}`; + if (!ppl.containsResource(postprocessPassRTName)) { + // Register a color texture resource, because current pass is to be on-screen, so pass camera.windows as an on-screen information. If is off-screen, you can use 'ppl.addRenderTarget' instead. + ppl.addRenderTexture(postprocessPassRTName, Format.BGRA8, width, height, camera.window); + // Register a depthStencil texture resource + ppl.addDepthStencil(postprocessPassDS, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + // The follows codes will update the registered information of the color texture and depth stencil texture (mainly for the size), also if is off-screen, use 'ppl.updateRenderTarget' instead + ppl.updateRenderWindow(postprocessPassRTName, camera.window); + ppl.updateDepthStencil(postprocessPassDS, width, height); + // Register a RasterPass with a layoutName of post-process + const postprocessPass = ppl.addRasterPass(width, height, 'post-process'); + postprocessPass.name = `CameraPostprocessPass${cameraID}`; + // Set the viewport information of current rasterPass + postprocessPass.setViewport(new Viewport(area.x, area.y, area.width, area.height)); + // Determine if there is information of the same name of the input texture in the system, and inject the input texture into the sampler of outputResultMap + if (ppl.containsResource(inputTex)) { + const computeView = new ComputeView(); + computeView.name = 'outputResultMap'; + postprocessPass.addComputeView(inputTex, computeView); + } + // Config the clear color of the postprocessPass的clear + const postClearColor = new Color(0, 0, 0, camera.clearColor.w); + if (camera.clearFlag & ClearFlagBit.COLOR) { + postClearColor.x = camera.clearColor.x; + postClearColor.y = camera.clearColor.y; + postClearColor.z = camera.clearColor.z; + } + // Register for color texture related passes to view + const postprocessPassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET), + StoreOp.STORE, + camera.clearFlag, + postClearColor); + // Register a PassView for depth stencil texture + const postprocessPassDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearDepth, camera.clearStencil, 0, 0)); + // Associate the color texture resource with the associated pass view (i.e. the color texture output of the renderpass) + postprocessPass.addRasterView(postprocessPassRTName, postprocessPassView); + // Associate the depth stencil texture resource with the associated pass view + postprocessPass.addRasterView(postprocessPassDS, postprocessPassDSView); + // Add a specific render queue and get the postprocess material to draw a quadrilateral of equal size to the screen + postprocessPass.addQueue(QueueHint.NONE).addFullscreenQuad( + postInfo.postMaterial, 0, SceneFlags.NONE, + ); + // ... + if (profilerCamera === camera) { + // Enable profiler + postprocessPass.showStatistics = true; + } + // Return the resources of color texture and depth stencil texture, which can be used as data source for other render passes + return { rtName: postprocessPassRTName, dsName: postprocessPassDS }; +} +``` + +First we need to know how `RasterPass` configures `layoutName` (i.e. the post-process string in the above code). After opening the `post-process.effect` file, we can see that the `pass` name defined internally is `post-process`, so the pass name in the effect file is used as the `layoutName` of RasterPass. If effect does not define pass name, then `layoutName` of `RasterPass` has to be assigned to `default` (forward/gbuffer related RasterPass are configured by default). So to configure your own post-processing scheme, you need to configure the pass name correctly for the effect file you write. + + + +We also need to use the output texture of the previous pass as the input information of the current pass, as mentioned above, we need to achieve this through `ComputeView`, and here the name of `ComputeView` is set to `outputResultMap`, so how to configure this name correctly? Continuing the analysis of the `post-process.effect` file, we can see the following code, the name of `ComputeView` is the same as the texture input name of the slice shader of `post-process-fs`. + + + +We also need to declare the outputResultMap name with the following line of code, indicating that the input texture is used at Pass level. + +```glsl +#pragma rate outputResultMap pass +``` + +After defining `TestCustomPipeline`, you need to introduce this file through other logical code (e.g. components, etc.) in order to activate the `Game.EVENT_RENDERER_INITED` event listener, and then change **Project Settings** -> **Macro Configurations** -> **CUSTOM_PIPELINE_NAME** to `Test` : + + + +The result after running is shown below, which contains the after-effects of fxaa and bloom. + + + +This is the general process of defining a `RenderPass`. PassUtils also defines other Passes that users can refer to, including `BloomPasses`, `FxaaPass` and so on. These `RenderPasses` provide parameters to adjust the output (e.g. Bloom exposure intensity, number of iterations, etc.), so users can try it out for themselves. diff --git a/versions/4.0/en/render-pipeline/image/BloomEnable.png b/versions/4.0/en/render-pipeline/image/BloomEnable.png new file mode 100644 index 0000000000..65eef4b3d7 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/BloomEnable.png differ diff --git a/versions/4.0/en/render-pipeline/image/DeferredPipeline.png b/versions/4.0/en/render-pipeline/image/DeferredPipeline.png new file mode 100644 index 0000000000..6cf897e2a2 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/DeferredPipeline.png differ diff --git a/versions/4.0/en/render-pipeline/image/builtin-editor-preview.jpg b/versions/4.0/en/render-pipeline/image/builtin-editor-preview.jpg new file mode 100644 index 0000000000..e6b294f569 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/builtin-editor-preview.jpg differ diff --git a/versions/4.0/en/render-pipeline/image/builtin-enable.jpg b/versions/4.0/en/render-pipeline/image/builtin-enable.jpg new file mode 100644 index 0000000000..24cb365ff7 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/builtin-enable.jpg differ diff --git a/versions/4.0/en/render-pipeline/image/builtin-pipeline-settings.png b/versions/4.0/en/render-pipeline/image/builtin-pipeline-settings.png new file mode 100644 index 0000000000..ae8b221fe6 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/builtin-pipeline-settings.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-add-compute-queue.png b/versions/4.0/en/render-pipeline/image/cp-add-compute-queue.png new file mode 100644 index 0000000000..fd2b0cad68 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-add-compute-queue.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-add-compute-view.png b/versions/4.0/en/render-pipeline/image/cp-add-compute-view.png new file mode 100644 index 0000000000..bfaacb51ac Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-add-compute-view.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-add-raster-queue.png b/versions/4.0/en/render-pipeline/image/cp-add-raster-queue.png new file mode 100644 index 0000000000..9b5cff64db Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-add-raster-queue.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-add-raster-view.png b/versions/4.0/en/render-pipeline/image/cp-add-raster-view.png new file mode 100644 index 0000000000..94f59aef02 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-add-raster-view.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-compute-pass.png b/versions/4.0/en/render-pipeline/image/cp-compute-pass.png new file mode 100644 index 0000000000..80be0d197d Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-compute-pass.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-compute-queue.png b/versions/4.0/en/render-pipeline/image/cp-compute-queue.png new file mode 100644 index 0000000000..867419a1f4 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-compute-queue.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-compute-view.png b/versions/4.0/en/render-pipeline/image/cp-compute-view.png new file mode 100644 index 0000000000..a4813e3991 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-compute-view.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-copy-pass.png b/versions/4.0/en/render-pipeline/image/cp-copy-pass.png new file mode 100644 index 0000000000..8f36166070 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-copy-pass.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-data-structure.png b/versions/4.0/en/render-pipeline/image/cp-data-structure.png new file mode 100644 index 0000000000..761e6f41fd Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-data-structure.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-dispatch.png b/versions/4.0/en/render-pipeline/image/cp-dispatch.png new file mode 100644 index 0000000000..867419a1f4 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-dispatch.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-feature-enable.png b/versions/4.0/en/render-pipeline/image/cp-feature-enable.png new file mode 100644 index 0000000000..309e343598 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-feature-enable.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-move-pass.png b/versions/4.0/en/render-pipeline/image/cp-move-pass.png new file mode 100644 index 0000000000..26af0d7d78 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-move-pass.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-pipeline-edit.png b/versions/4.0/en/render-pipeline/image/cp-pipeline-edit.png new file mode 100644 index 0000000000..976dad4ed7 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-pipeline-edit.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-pipeline-selection.png b/versions/4.0/en/render-pipeline/image/cp-pipeline-selection.png new file mode 100644 index 0000000000..a667841d73 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-pipeline-selection.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-quad.png b/versions/4.0/en/render-pipeline/image/cp-quad.png new file mode 100644 index 0000000000..03b0b17edf Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-quad.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-raster-pass.png b/versions/4.0/en/render-pipeline/image/cp-raster-pass.png new file mode 100644 index 0000000000..32a445bb4a Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-raster-pass.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-raster-queue.png b/versions/4.0/en/render-pipeline/image/cp-raster-queue.png new file mode 100644 index 0000000000..8edb470e74 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-raster-queue.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-raster-view.png b/versions/4.0/en/render-pipeline/image/cp-raster-view.png new file mode 100644 index 0000000000..472c2766a0 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-raster-view.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-render-graph-1.png b/versions/4.0/en/render-pipeline/image/cp-render-graph-1.png new file mode 100644 index 0000000000..68a41eeac2 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-render-graph-1.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-render-graph-2.png b/versions/4.0/en/render-pipeline/image/cp-render-graph-2.png new file mode 100644 index 0000000000..f55bbeea85 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-render-graph-2.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-render-queue.png b/versions/4.0/en/render-pipeline/image/cp-render-queue.png new file mode 100644 index 0000000000..9b5cff64db Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-render-queue.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-scene.png b/versions/4.0/en/render-pipeline/image/cp-scene.png new file mode 100644 index 0000000000..ab6e84b6f9 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-scene.png differ diff --git a/versions/4.0/en/render-pipeline/image/cp-setter.png b/versions/4.0/en/render-pipeline/image/cp-setter.png new file mode 100644 index 0000000000..31f33f1641 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/cp-setter.png differ diff --git a/versions/4.0/en/render-pipeline/image/customPipelineBloom.png b/versions/4.0/en/render-pipeline/image/customPipelineBloom.png new file mode 100644 index 0000000000..8e2c325ebb Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/customPipelineBloom.png differ diff --git a/versions/4.0/en/render-pipeline/image/deferred-pipeline.png b/versions/4.0/en/render-pipeline/image/deferred-pipeline.png new file mode 100644 index 0000000000..b1ecc434a5 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/deferred-pipeline.png differ diff --git a/versions/4.0/en/render-pipeline/image/effect.png b/versions/4.0/en/render-pipeline/image/effect.png new file mode 100644 index 0000000000..f43a02d8b5 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/effect.png differ diff --git a/versions/4.0/en/render-pipeline/image/forward-pipeline.png b/versions/4.0/en/render-pipeline/image/forward-pipeline.png new file mode 100644 index 0000000000..9576379c51 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/forward-pipeline.png differ diff --git a/versions/4.0/en/render-pipeline/image/lut_cocos.jpg b/versions/4.0/en/render-pipeline/image/lut_cocos.jpg new file mode 100644 index 0000000000..acef07938f Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/lut_cocos.jpg differ diff --git a/versions/4.0/en/render-pipeline/image/postprocessOutput.png b/versions/4.0/en/render-pipeline/image/postprocessOutput.png new file mode 100644 index 0000000000..2d689ecf3b Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/postprocessOutput.png differ diff --git a/versions/4.0/en/render-pipeline/image/postprocessPass.png b/versions/4.0/en/render-pipeline/image/postprocessPass.png new file mode 100644 index 0000000000..f3506bf816 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/postprocessPass.png differ diff --git a/versions/4.0/en/render-pipeline/image/setting.png b/versions/4.0/en/render-pipeline/image/setting.png new file mode 100644 index 0000000000..d911881719 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/setting.png differ diff --git a/versions/4.0/en/render-pipeline/image/testCustomPipeline.png b/versions/4.0/en/render-pipeline/image/testCustomPipeline.png new file mode 100644 index 0000000000..7f34fb9935 Binary files /dev/null and b/versions/4.0/en/render-pipeline/image/testCustomPipeline.png differ diff --git a/versions/4.0/en/render-pipeline/overview.md b/versions/4.0/en/render-pipeline/overview.md new file mode 100644 index 0000000000..9a0ab0ea64 --- /dev/null +++ b/versions/4.0/en/render-pipeline/overview.md @@ -0,0 +1,44 @@ +# Render Pipeline Overview + +![lut_cocos](./image/lut_cocos.jpg) + +RenderPipeline is used to control the rendering process of a scene, including light management, object culling, rendering object sorting, and rendering target switching. Since each stage can have different optimization methods for different projects, it is difficult to achieve the most optimized results with a unified method for handling the rendering processes of different types of projects. A customizable render pipeline is used for more flexible control over each stage of the rendering scene, allowing for deeper optimization solutions tailored to different projects. + +Since Cocos Creator 3.8.4, there are two sets of render pipelines: the new render pipeline and the legacy render pipeline. + +## New Render Pipeline + +Cocos Creator 3.8.4 introduces a brand-new customizable render pipeline, CRP - Customizable Render Pipeline. + +The new customizable render pipeline mainly includes the following benefits: + +1. In Cocos Creator 3.8.4 and above, the engine's built-in render pipeline is based on the CRP pipeline, making the engine's built-in pipeline and the user's custom pipeline have the same rendering mechanism and process, with stronger compatibility and stability. +2. Based on the CRP pipeline, developers can write rendering processes compatible with all platforms without modifying the engine's source code. +3. Based on the CRP pipeline, developers can customize rendering processes according to the project's needs, remove unnecessary rendering processes, save overhead, and improve performance. +4. Based on the RenderGraph rendering architecture in the CRP pipeline, developers can easily reuse and add rendering processes to achieve the advanced rendering effects and post-effects required by the project. + +Related documentation is as follows: + +- [Using Built-in Render Pipeline](./use-builtin-pipeline.md) +- [Using Post-Processing Effects](./use-post-process.md) +- [Writing a Render Pipeline (Advanced)](./write-render-pipeline.md) + +## Legacy Render Pipeline + +The legacy render pipeline, which started from Cocos Creator 3.0, is a set of render pipeline built based on the traditional rendering architecture. + +It includes forward rendering and deferred rendering. It does not support custom rendering processes and post-effects. + +The legacy render pipeline will be removed in the next major version. It is recommended that everyone use the new render pipeline in new projects. + +Related documentation is below: + +- [Built-in Render Pipeline - Legacy](./builtin-pipeline.md) +- [Custom Render Pipeline - Legacy](./custom-pipeline.md) +- [Post-Effects - Legacy](./post-process.md) + +## Compatibility Mechanism Between New and Legacy Render Pipelines + +1. New projects created in Cocos Creator 3.8.4+ default to use the new pipeline. +2. If an old project did not use a custom pipeline, it will use the lagecy render pipeline after upgrading. +3. If an old project used a custom pipeline, it will use the new render pipeline after upgrading. diff --git a/versions/4.0/en/render-pipeline/post-process.md b/versions/4.0/en/render-pipeline/post-process.md new file mode 100644 index 0000000000..2e46d29c28 --- /dev/null +++ b/versions/4.0/en/render-pipeline/post-process.md @@ -0,0 +1,5 @@ +# Post-Processing Effects + +Users of versions v3.8.3 and below, please refer to the documentation: [Fullscreen Effect Post-Processing Workflow](./post-process/index.md). + +Users of versions v3.8.4 and below, please refer to the documentation: [Using Post-Processing Effects](./use-post-process.md). diff --git a/versions/4.0/en/render-pipeline/post-process/blit-screen.md b/versions/4.0/en/render-pipeline/post-process/blit-screen.md new file mode 100644 index 0000000000..bcb66225df --- /dev/null +++ b/versions/4.0/en/render-pipeline/post-process/blit-screen.md @@ -0,0 +1,22 @@ +# BlitScreen Component + +The BlitScreen component allows users to define various materials to manage [Custom Post Process](./custom.md). + +## Properties + +![blit-screen.png](./img/blit-screen.png) + +| Properties | Description | +| :-- | :-- | +| **Materials** | An array contains post-process materials | + +Assign the **Materials** length property to add different post-process materials, which can be seen as follows. + +![blit-screen-material.png](./img/blit-screen-material.png) + +| Properties | Description | +| :-- | :-- | +| **Material** | The material of post effect which is dragged from the **Assets** panel. | +| **Enable** | Enable or disable the material above | + +The assigned materials will be executed in the order they are added. diff --git a/versions/4.0/en/render-pipeline/post-process/custom.md b/versions/4.0/en/render-pipeline/post-process/custom.md new file mode 100644 index 0000000000..1784774fed --- /dev/null +++ b/versions/4.0/en/render-pipeline/post-process/custom.md @@ -0,0 +1,134 @@ +# Customize a Post Process(No longer maintained) + +There are two methods to customize a post-process, add a simple post-process material to the [BlitScreen Component](./blit-screen.md) or define a post pass for a complex post-process. + +## Blit-Screen Component + +Refer to the [Config Post Process](index.md), add a [BlitScreen Component](./blit-screen.md), and drag the custom post-process material to the **Material** property. The Blit-Screen component will render all the materials in the **Material** property according to the order they are added. + +Every material in the **Materials** property contains a single switch to help developers manage them. + +![BlitScreen](img/custom-1.png) + +![Dot Effect](img/custom-2.png) + +For more, please refer to [cocos-example-render-pipeline](https://github.com/cocos/cocos-example-render-pipeline/blob/main/assets/cases/post-process/post-process.scene). + +## Customize a Post Process Pass + +You can create a custom post-process Pass to accomplish complex post-process effect. + +![custom-pass-1](img/custom-pass-1.png) +![custom-pass-2](img/custom-pass-2.png) + +1. Define a PostProcessSetting component that will be delivered to the CustomPass + + ```js + import { _decorator, Material, postProcess } from 'cc'; + const { ccclass, property, menu } = _decorator; + + @ccclass('CustomPostProcess') + @menu('PostProcess/CustomPostProcess') + export class CustomPostProcess extends postProcess.PostProcessSetting { + @property + blueIntensity = 1 + + @property + showDepth = false + + @property + depthRange = 30 + + @property(Material) + _material: Material | undefined + + @property(Material) + get material () { + return this._material; + } + set material (v) { + this._material = v; + } + } + ``` + +2. Create a CustomPass + + ```js + import { Vec4, gfx, postProcess, renderer, rendering } from "cc"; + import { CustomPostProcess } from "./CustomPostProcess"; + + export class CustomPass extends postProcess.SettingPass { + // custom pass name + name = 'CustomPass' + + // out out slot name + outputNames: string[] = ['CustomPassColor'] + + // reference to post process setting + get setting () { return this.getSetting(CustomPostProcess); } + + // Whether the pass should rendered + checkEnable(camera: renderer.scene.Camera): boolean { + let setting = this.setting; + return setting.material && super.checkEnable(camera); + } + + params = new Vec4 + + render (camera: renderer.scene.Camera, ppl: rendering.Pipeline) { + const cameraID = this.getCameraUniqueID(camera); + + // clear background to black color + let context = this.context; + context.clearBlack() + + // input name from last pass's output slot 0 + let input0 = this.lastPass.slotName(camera, 0); + // output slot 0 name + let output = this.slotName(camera, 0); + + // get depth slot name + let depth = context.depthSlotName; + + // also can get depth slot name from forward pass. + // let forwardPass = builder.getPass(ForwardPass); + // depth = forwardPass.slotName(camera, 1); + + // set setting value to material + let setting = this.setting; + this.params.x = setting.blueIntensity + this.params.y = setting.showDepth ? 1 : 0; + this.params.z = setting.depthRange; + setting.material.setProperty('params', this.params); + + context.material = setting.material; + context + // update view port + .updatePassViewPort() + // add a render pass + .addRenderPass('post-process', `${this.name}${cameraID}`) + // set inputs + .setPassInput(input0, 'inputTexture') + .setPassInput(depth, 'depthTexture') + // set outputs + .addRasterView(output, gfx.Format.RGBA8) + // final render + .blitScreen(0) + // calculate a version + .version(); + } + } + ``` + +3. Register the custom pass + + ```js + let builder = rendering.getCustomPipeline('Custom') as postProcess.PostProcessBuilder; + if (builder) { + // insert CustomPass after a BlitScreenPass + builder.insertPass(new CustomPass, BlitScreenPass); + } + ``` + +For more, please refer to [custom-pass](https://github.com/cocos/cocos-example-render-pipeline/blob/main/assets/cases/post-process/custom-pass.ts). diff --git a/versions/4.0/en/render-pipeline/post-process/img/1.png b/versions/4.0/en/render-pipeline/post-process/img/1.png new file mode 100644 index 0000000000..637056fa58 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/1.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/2.png b/versions/4.0/en/render-pipeline/post-process/img/2.png new file mode 100644 index 0000000000..d348cb3407 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/2.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/3.png b/versions/4.0/en/render-pipeline/post-process/img/3.png new file mode 100644 index 0000000000..84b7dcd231 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/3.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/blit-screen-material.png b/versions/4.0/en/render-pipeline/post-process/img/blit-screen-material.png new file mode 100644 index 0000000000..cbebdbf0fc Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/blit-screen-material.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/blit-screen.png b/versions/4.0/en/render-pipeline/post-process/img/blit-screen.png new file mode 100644 index 0000000000..0590828f3f Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/blit-screen.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/bloom-disabled.png b/versions/4.0/en/render-pipeline/post-process/img/bloom-disabled.png new file mode 100644 index 0000000000..f2dfa611e2 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/bloom-disabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/bloom-enabled.png b/versions/4.0/en/render-pipeline/post-process/img/bloom-enabled.png new file mode 100644 index 0000000000..f46b3a78bf Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/bloom-enabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/bloom.png b/versions/4.0/en/render-pipeline/post-process/img/bloom.png new file mode 100644 index 0000000000..1d48e7e450 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/bloom.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/camera-post-process.png b/versions/4.0/en/render-pipeline/post-process/img/camera-post-process.png new file mode 100644 index 0000000000..98024dc00c Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/camera-post-process.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/color-grading.png b/versions/4.0/en/render-pipeline/post-process/img/color-grading.png new file mode 100644 index 0000000000..f812646959 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/color-grading.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/custom-1.png b/versions/4.0/en/render-pipeline/post-process/img/custom-1.png new file mode 100644 index 0000000000..f1d5d87e9b Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/custom-1.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/custom-2.png b/versions/4.0/en/render-pipeline/post-process/img/custom-2.png new file mode 100644 index 0000000000..2290b8b378 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/custom-2.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/custom-pass-1.png b/versions/4.0/en/render-pipeline/post-process/img/custom-pass-1.png new file mode 100644 index 0000000000..638bdaac60 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/custom-pass-1.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/custom-pass-2.png b/versions/4.0/en/render-pipeline/post-process/img/custom-pass-2.png new file mode 100644 index 0000000000..013bd119ac Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/custom-pass-2.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/enable-post-process.png b/versions/4.0/en/render-pipeline/post-process/img/enable-post-process.png new file mode 100644 index 0000000000..c6b4141e81 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/enable-post-process.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/fsr-disabled.png b/versions/4.0/en/render-pipeline/post-process/img/fsr-disabled.png new file mode 100644 index 0000000000..55813932fa Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/fsr-disabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/fsr-enabled.png b/versions/4.0/en/render-pipeline/post-process/img/fsr-enabled.png new file mode 100644 index 0000000000..0ef53486ec Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/fsr-enabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/fsr.png b/versions/4.0/en/render-pipeline/post-process/img/fsr.png new file mode 100644 index 0000000000..81f646bd33 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/fsr.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/fxaa-disabled.png b/versions/4.0/en/render-pipeline/post-process/img/fxaa-disabled.png new file mode 100644 index 0000000000..3c17273f69 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/fxaa-disabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/fxaa-enabled.png b/versions/4.0/en/render-pipeline/post-process/img/fxaa-enabled.png new file mode 100644 index 0000000000..009d15aae5 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/fxaa-enabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/fxaa.png b/versions/4.0/en/render-pipeline/post-process/img/fxaa.png new file mode 100644 index 0000000000..96ba4d2633 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/fxaa.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/hbao-debugview.png b/versions/4.0/en/render-pipeline/post-process/img/hbao-debugview.png new file mode 100644 index 0000000000..4420bab474 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/hbao-debugview.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/hbao-disabled.png b/versions/4.0/en/render-pipeline/post-process/img/hbao-disabled.png new file mode 100644 index 0000000000..8c2734904f Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/hbao-disabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/hbao-enabled.png b/versions/4.0/en/render-pipeline/post-process/img/hbao-enabled.png new file mode 100644 index 0000000000..e6d7f3f4d4 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/hbao-enabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/hbao.png b/versions/4.0/en/render-pipeline/post-process/img/hbao.png new file mode 100644 index 0000000000..5243e7df91 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/hbao.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/lut-disabled.png b/versions/4.0/en/render-pipeline/post-process/img/lut-disabled.png new file mode 100644 index 0000000000..c95d451805 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/lut-disabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/lut-enabled.png b/versions/4.0/en/render-pipeline/post-process/img/lut-enabled.png new file mode 100644 index 0000000000..c990448df3 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/lut-enabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/order.png b/versions/4.0/en/render-pipeline/post-process/img/order.png new file mode 100644 index 0000000000..01a0df28fc Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/order.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/post-process-component.png b/versions/4.0/en/render-pipeline/post-process/img/post-process-component.png new file mode 100644 index 0000000000..d3bbe8eae1 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/post-process-component.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/post-process.png b/versions/4.0/en/render-pipeline/post-process/img/post-process.png new file mode 100644 index 0000000000..50f42bf0fc Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/post-process.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/taa-disabled.png b/versions/4.0/en/render-pipeline/post-process/img/taa-disabled.png new file mode 100644 index 0000000000..16ef6f94a3 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/taa-disabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/taa-enabled.png b/versions/4.0/en/render-pipeline/post-process/img/taa-enabled.png new file mode 100644 index 0000000000..7ae1594579 Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/taa-enabled.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/img/taa.png b/versions/4.0/en/render-pipeline/post-process/img/taa.png new file mode 100644 index 0000000000..26e5a6a53d Binary files /dev/null and b/versions/4.0/en/render-pipeline/post-process/img/taa.png differ diff --git a/versions/4.0/en/render-pipeline/post-process/index.md b/versions/4.0/en/render-pipeline/post-process/index.md new file mode 100644 index 0000000000..a4a5999d92 --- /dev/null +++ b/versions/4.0/en/render-pipeline/post-process/index.md @@ -0,0 +1,163 @@ +# Full-Screen Post Process + +Post process, short for full-screen post process, means the rendered result image of the camera will be handled once or more by a specific image algorithm to obtain a better effect or special screen effect. + +Post process and some common post effects are added to Cocos Creator in v3.8. + +## Enable Post Process + +1. [Turn on the Custom Pipeline and switch to **Forward Render Pipeline**](../custom-pipeline.md#Feature Enable), as shown in the figure: + + ![1](img/1.png) + + ![2](img/2.png) + +## Config Post Process + +1. Add a new empty node to the **Hierarchy** panel, and rename it to `PostProcess` or any name your like. + +2. Clicking the **Add Component** button on the **Inspector** panel, and input `PostProcess` in the pop-up label. To add a post-process component, please select one component listed in the drop-down box referred to below. + + ![3](img/3.png) + +After adding any post-process component, a **PostProcess** component will be automatically added to the node, for more refer to [Post Process](./post-process.md). + +## Enable Post Process + +The post process can be used for one single camera, or all cameras as a global post process. + +Please note that to enable the post process, please check the **Use Post Process** property to make sure the post process is enabled on this camera. + +![enable-post-process.png](./img/enable-post-process.png) + +## Global Post Process + +To apply all post processes for all cameras with **Use Post Process** on, please check the **Global** property in the **PostProcess** component which is true by default. + +![post-process.png](./img/post-process.png) + +### Post Process for a Single Camera + +To enable the post process for a single camera, drag and drop the post process node created above to the **PostProcess** property in the camera component on the **Inspector** panel. + +![camera-post-process.png](./img/camera-post-process.png) + +## Builtin Post Effects + +### TAA(Temporal Anti-aliasing) + +TAA is an advanced anti-aliasing technic that uses historic frames and blends them. Due to the consumption of TAA, it is recommended that the TAA should be enabled only on mid-to-high-end models. + +![taa.png](./img/taa.png) + +Properties: + +1. Sample Scale: For the sample range scale factor, a small floating point value is recommended. A large number may cause the screen to shake. +2. Feedback: value to blend the historic frames. Better anti-aliasing effect with a large value, but the screen may be blurred. + +- TAA disabled + + ![taa-disabled](img/taa-disabled.png) + +- TAA enabled + + ![taa-enabled](img/taa-enabled.png) + +### FSR (AMD FidelityFX Super Resolution) + +FSR, a whole new, open-source, and high-quality render solution by AMD, is used to generate a high-resolution render result from a low-resolution input. It uses a series of high-technical algorithms, especially emphasizing creating high-quality edges, improving the performances compared with rendering with the original resolution. + +![fsr.png](./img/fsr.png) + +Properties: + +1. Sharpness: screen sharpness + +- FSR disabled + + ![Alt text](img/fsr-disabled.png) + +- FSR enabled + + ![Alt text](img/fsr-enabled.png) + +### FXAA(Fast Approximate Anti-Aliasing) + +FXAA algorithm uses a low cost to reduce the appearance of jagged or "jaggies** on the rendered image. + +![fxaa.png](./img/fxaa.png) + +- FXAA disabled + + ![fxaa-disabled](img/fxaa-disabled.png) + +- FXAA enabled + + ![fxaa-enabled](img/fxaa-enabled.png) + +### Bloom + +The bloom component is used to control the blooming and halo in the highlighted area of the scene, which can make the lighting or strongly reflective materials look more realistic. + +![bloom.png](./img/bloom.png) + +Properties: + +1. Threshold: The luminance threshold of the halo, above which the area will be blooming. Note that this value is the LDR brightness unit as seen by the human eye and is not related to exposure +2. Iterations: iteration counts, with a large value, the halo will be larger in range and softer, which will reduce the performance. +3. Intensity: brightness of the blooming, more bright with a larger value. Please modify moderately + +- Bloom disabled + ![bloom-disabled](img/bloom-disabled.png) +- Bloom enabled + ![bloom-enabled](img/bloom-enabled.png) + +### Color Grading + +Color Grading uses a looking-up table(LUT) to correct the scene color from the artist's perspective. LUT mapping can be downloaded from the internet or created by software such as Photoshop. + +![color-grading.png](./img/color-grading.png) + +Properties: + +1. Contribute the contribution factor which can be adjusted in the range 0-1 for the level of influence on the final color. +2. Color Grading Map: a looking up table map for colors, Nx1 or 8x8 rectangle images are supported automatically by the engine. The location of the built-in LUT map is internal/dependencies/textures/lut/. + +- Color Grading disabled + ![lut-disabled](img/lut-disabled.png) +- Color Grading enabled + ![lut-enabled](img/lut-enabled.png) + +### HBAO(Horizon-Based Ambient Occlusion) + +The HBAO algorithm supports a simple, easy way to calculate the ambient occlusion when calculating the global illumination, which will improve the stereo space sensory of the entire scene. + +![hbao.png](./img/hbao.png) + +Properties: + +1. Radius Scale: the range of the ambient occlusion, the dark area will be better matched to the size of the scene by adjusting this property. +2. AO Saturation: saturation of the ambient occlusion, the larger the value, the darker it is. +3. Need Blur: turn on for softer results and less noise, but it will cost some performance. + +- HBAO disabled + + ![hbao-disabled](img/hbao-disabled.png) + +- HBAO enabled + + ![hbao-enabled](img/hbao-enabled.png) + +- Clicking on the debug menu in the scene, and choose the **Ambient Occlusions** menu to debug the HBAO + + ![hbao-debugview](img/hbao-debugview.png) + +## Custom Post Process + +Post process can be customized by referring to [Custom Post Process](./custom.md). + +## The Execute Order of Post Process + +The execution order of the post-process components attaching to the same node will not be following the order they are added. The post-process components have a strict execute order pre-defined by the engine which can be found in the [post-process-builder.ts](https://github.com/cocos/cocos4/blob/v4.0.0/cocos/rendering/post-process/post-process-builder.ts) file. + +![order.png](./img/order.png) diff --git a/versions/4.0/en/render-pipeline/post-process/post-process.md b/versions/4.0/en/render-pipeline/post-process/post-process.md new file mode 100644 index 0000000000..c003ce9dae --- /dev/null +++ b/versions/4.0/en/render-pipeline/post-process/post-process.md @@ -0,0 +1,19 @@ +# PostProcess Component + +The PostProcess component is an assisted component for [Full Screen Post Process](./index.md), and it will be automatically added to the post-process node after any post-process component is added to config the post-process. + +## Properties + +![post-process-component.png](./img/post-process-component.png) + +| Properties | Description | +| :-- | :-- | +| **Global** | Enable the global post process or not. All cameras with **Use Post Process** on will be affected. | +| **Shading Scale** | The final output image scale factor from 0.01 to 1 based on the screen resolution | +| **Enable Shading Scale In Editor** | Enable or disable the **Shading Scale** in the editor | + +### Description + +- The **Global** property will be ignored when turning on the **Use Post Process** and assigning a post process to the **Post Process** property of the camera. +- When the **Post Process** property of the camera with **Use Post Process** on is leaving empty, the **Global** property ought to be enabled to make the post processes take effect. +- If post processes with the same type are added to the scene multiple times, only the first post process mathed will take effect. diff --git a/versions/4.0/en/render-pipeline/use-builtin-pipeline.md b/versions/4.0/en/render-pipeline/use-builtin-pipeline.md new file mode 100644 index 0000000000..06e3642171 --- /dev/null +++ b/versions/4.0/en/render-pipeline/use-builtin-pipeline.md @@ -0,0 +1,48 @@ +# Using the Built-in Render Pipeline + +Select the **New Render Pipeline** in the Project menu -> Project Settings -> Graphics Settings. + +![builtin-enable](./image/builtin-enable.jpg) + +The default render pipeline is the built-in render pipeline named **Builtin**. + +## Built-in Render Pipeline + +The built-in render pipeline (Builtin) is an implementation based on the CRP - Customizable Render Pipeline, providing basic cross-platform rendering capabilities suitable for all platforms. + +The current built-in render pipeline supports the following features: + +- Forward rendering +- Multisample Anti-aliasing (MSAA) +- Real-time shadows +- Post-processing, including (Bloom, Color Grading, FXAA, FSR, etc.) + +It can meet almost all the needs of conventional projects. + +## BuiltinPipelineSettings Component + +Each camera in the scene is rendered in order and output to the screen or render texture. + +We can add a `BuiltinPipelineSettings` component to each camera node to set up the rendering for that camera. + +Through this component, we can control the camera's output behavior and post-effects. + +> For user's custom render pipelines, it is recommended to copy the source code of this component into the project and modify it as needed. + +## Editor Preview (Experimental) + +When the `Editor Preview (Experimental)` is checked, you can directly preview the rendering effects in the scene editor. + +![builtin-editor-preview](./image/builtin-editor-preview.jpg) + +Since scene rendering may include post-effects, previewing in the editor may have performance impacts, and some editor effects may display abnormally. + +## Using Post-Processing Effects + +For the configuration and use of post-processing effects, please refer to the document: [Using Post-Processing Effects](./use-post-process.md) + +## Code Location + +The implementation of the built-in render pipeline is located at `editor/assets/default_renderpipeline/builtin-pipeline.ts`. + +The `BuiltinPipelineBuilder` class implements the main functionalities of the built-in pipeline. diff --git a/versions/4.0/en/render-pipeline/use-post-process.md b/versions/4.0/en/render-pipeline/use-post-process.md new file mode 100644 index 0000000000..34492a423a --- /dev/null +++ b/versions/4.0/en/render-pipeline/use-post-process.md @@ -0,0 +1,59 @@ +# Using Post-Processing Effects + +> **Note**: This document is applicable to users of Cocos Creator v3.8.4 and above. For users of v3.8.3 and below, please refer to the document: [Fullscreen Effect Post-Processing Workflow](./post-process/index.md). + +## Adding Components + +Every camera in the scene can have a `BuiltinPipelineSettings` component added. `BuiltinPipelineSettings` is used for rendering settings of the current camera, including post-processing effects. + +When enabling the corresponding features, you need to attach the required materials and textures, which are generally prefixed with "builtin", as shown in the figure below. + +![builtin-pipeline-settings](./image/builtin-pipeline-settings.png) + +## Multisample Anti-aliasing (MSAA) + +Multisample Anti-aliasing (MSAA) is an anti-aliasing technique that eliminates the jagged edges of images by blending samples of different depths during the lighting phase. + +Currently supports 2X and 4X multisampling, which is only enabled on native platforms. + +On Web platforms, the `antialias` feature of `WebGL` is used, enabled by the `ENABLE_WEBGL_ANTIALIAS` macro. + +## Shading Scale + +`Shading Scale` is a rendering optimization technique that improves rendering performance by reducing the rendering resolution and thus decreasing the rendering burden. + +For example, if the window size is 1920x1080 and the `Shading Scale` is 0.5, the rendering resolution will be 960x540. + +It is suitable for scenes with high rendering pressure and can be used in conjunction with super-resolution technology to enhance image quality. + +For the UI rendering, the original resolution will be used. + +The `Shading Scale` property set on the `BuiltinPipelineSettings` component only affects the current camera. + +## Bloom + +Bloom is a post-processing effect that enhances the brightness of the image by extracting the bright parts of the image, blurring them, and then superimposing them on the original image. + +`Bloom Iterations` is the number of bloom iterations, and `Bloom Threshold` is the bloom threshold value. + +## Color Grading + +Color Grading is a post-processing effect that adjusts the color effects of the image through a grading map LUT. + +## Fast Approximate Anti-aliasing (FXAA) + +Fast Approximate Anti-aliasing (FXAA) is an anti-aliasing technique that eliminates the jagged edges of images by smoothing the image. + +## FidelityFX Super Resolution (FSR) + +FidelityFX Super Resolution is a rendering technique that enhances image quality by reducing the rendering resolution and then improving the image quality through algorithms. + +The currently used technology is `AMD FidelityFX Super Resolution`. + +It is currently only effective when `Shading Scale < 1`. + +## Tone Mapping + +Tone Mapping is a post-processing effect that improves the visual effect of the image by adjusting its tonal range. + +Currently, Color Grading integrates the Tone Mapping, and if you want to customize it, you need to modify it in conjunction. diff --git a/versions/4.0/en/render-pipeline/user-pipeline.md b/versions/4.0/en/render-pipeline/user-pipeline.md new file mode 100644 index 0000000000..dcb36d4574 --- /dev/null +++ b/versions/4.0/en/render-pipeline/user-pipeline.md @@ -0,0 +1,61 @@ +# Custom Render Pipelines(Deprecated) + +To create a custom render pipeline, first create a new RenderPipeline asset in the **Assets** panel, then create a RenderPipeline script, and then select the corresponding RenderPipeline script in the Pipeline asset to edit the corresponding properties. + +RenderFlow and RenderStage are created and edited in the same way. In the created Pipeline script, add properties and make them editable in the **Inspector** panel just like any other user script, but note that it is not an option to drag and drop entities in the scene, as the RenderPipeline is not bound to a specific scene. + +## Properties and methods in RenderPipeline + +- `flows`: The RenderFlow contained in the RenderPipeline. + +- `renderTextures`: the RenderTextures that can be created when the RenderPipeline is started. + + - `name`: the name of the RenderTexture, which can be obtained by the `getRenderTexture` function of the RenderPipeline after creation. + - `type`: the type of the RenderTexture. + - `viewType`: the corresponding TextureView type of the RenderTexture. + - `usage`: the binding type of the RenderTexture, used to determine whether it is `color` or `depth_stencil`. + - `formate`: the channel format of the RenderTexture. + - `width`: width of the RenderTexture, -1 means the width of the window. + - `height`: height of the RenderTexture, -1 means the height of the window. + +- `framebuffers`: the FrameBuffer that can be created when RenderPipeline starts. + + - `name`: the name of the FrameBuffer, it can be retrieved by the `getFrameBuffer` function of RenderPipeline after creation. + - `renderPass`: the ID of the RenderPass configured in the RenderPipeline. + - `colorViews`: TextureView bound to the ColorAttachment, specifying the RenderTexture configured in the RenderPipeline. + - `depthStencilView`: TextureView bound to DepthStencilAttachment. specifies the RenderTexture configured in the RenderPipeline. +- `renderPasses`: the RenderPasses that can be created when the RenderPipeline is started. + - `index`: ID of the RenderPass, which can be obtained by the `getRenderPass` function of the RenderPipeline. + - `colorAttachments`: the description of the ColorAttachment, the operation of the ColorAttachment when drawing the FrameBuffer. + - `depthStencilAttachment`: the description of the DepthStencilAttachment, the operation of the DepthStencilAttachment when drawing the FrameBuffer. + +- `getTextureView` (name: string), `getRenderTexture` (name: string): get the RenderTexture configured in renderTextures. +- `getFrameBuffer` (name: string): get the FrameBuffer configured in `framebuffers`. +- `getRenderPass` (stage: number): get the RenderPass configured in renderPasses. +- `initialize` (info: IRenderPipelineInfo): initialize function for creating a RenderPipeline by script, the RenderPipeline must be initialized before it can be used. +- `activate` (root: Root): initialization function used when loading a RenderPipeline through an asset. +- `render` (view: RenderView): logic for rendering the scene. +- `updateUBOs` (view: RenderView): update the global UniformBuffer. +- `sceneCulling` (view: RenderView): scene culling, renderable objects are saved in `_renderObjects` after culling. + +## Properties and methods in RenderFlow + +- `name`: the name of the RenderFlow. +- `priority`: the order of execution of the RenderFlow in the RenderPipeline. +- `type`: the type of the RenderFlow. + - `SCENE`: used to draw the scene, this type will be executed for each camera. + - `POSTPROCESS`: post-processing, this type is specified separately for each camera. + - `UI`: used to draw the UI. +- `stages`: RenderStage included in RenderFlow. + +## Properties and methods in RenderStage + +- `name`: the name of the RenderStage. +- `priority`: the order of execution of the RenderStage in the RenderFlow. +- `frameBuffer`: the FrameBuffer that the RenderStage will draw to, should be set to the FrameBuffer configured in the RenderPipeline, or set to `window` to indicate that the default FrameBuffer is used. +- `renderQueues`: render queue, used to control the rendering order of objects. + - `isTransparent`: marks whether the render queue is semi-transparent. + - `sortMode`:
`FRONT_TO_BACK`: sort from front to back;
`BACK_TO_FRONT`: sort from back to front. + - `stages`: specifies which passes in the render queue render material, should be specified as the `phase` in the `pass`. + - `sortRenderQueue()`: sort the render queue. + - `executeCommandBuffer` (view: RenderView): execute the render command. diff --git a/versions/4.0/en/render-pipeline/write-render-pipeline.md b/versions/4.0/en/render-pipeline/write-render-pipeline.md new file mode 100644 index 0000000000..9d3cffc6db --- /dev/null +++ b/versions/4.0/en/render-pipeline/write-render-pipeline.md @@ -0,0 +1,775 @@ +# Writing a Customizable Render Pipeline + +In projects where special effects need to be achieved or precise control over the rendering process is required to maximize performance, it is often necessary to customize the render pipeline. + +Cocos' Customizable Render Pipeline (CRP) allows developers to write cross-platform custom render pipelines without modifying the engine's source code. + +This tutorial uses the built-in render pipeline (Builtin) as an example to show how to write a custom render pipeline based on CRP. + +## Source Code Location + +It is recommended to create your own pipeline based on the built-in one for a quick start and iteration. You can obtain the source code of the built-in render pipeline in the following two ways: + +1. Refer to the [Cocos CRP Example Project on Github](https://github.com/cocos/cocos-example-custom-pipeline), and find the branch corresponding to the engine version you are using. +2. Copy the three pipeline-related scripts under the internal/default_renderpipeline/ directory in the editor to the assets directory, and then make modifications. + + - builtin-pipeline.ts: Pipeline implementation + - builtin-pipeline-types.ts: Related data types required by the pipeline + - builtin-pipeline-settings.ts: Pipeline settings component + +## Overriding the Built-in Pipeline + +Due to the complexity of writing a render pipeline from scratch, it is advisable to modify the existing built-in pipeline. + +Users can copy the code of the built-in pipeline into their project and then make modifications according to their specific needs. + +The files that need to be copied are: +`builtin-pipeline.ts`, `builtin-pipeline-types.ts`, and `builtin-pipeline-settings.ts`. + +After copying, it is necessary to rename the class names and the pipeline name. + +`builtin-pipeline.ts`: + +```typescript +class MyPipelineBuilder implements rendering.PipelineBuilder { + // ... +} +if (rendering) { + rendering.setCustomPipeline('MyPipeline', new MyPipelineBuilder()); +} +``` + +`builtin-pipeline-settings.ts`: + +```typescript +@ccclass('MyPipelineSettings') +@menu('Rendering/MyPipelineSettings') +@requireComponent(Camera) +@disallowMultiple +@executeInEditMode +export class MyPipelineSettings extends Component { + // ... + +} +``` + +## Creating a Render Pipeline + +First, we need to create a class for the render pipeline that inherits from `rendering.Pipeline`. + +```typescript +import { renderer, rendering } from 'cc'; + +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + windowResize( + ppl: rendering.BasicPipeline, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, + nativeWidth: number, + nativeHeight: number, + ): void { + // setup render resources. + } + setup( + cameras: renderer.scene.Camera[], + ppl: rendering.BasicPipeline, + ): void { + // setup camera rendering. + } +} +``` + +Custom pipelines need to implement two core methods: `windowResize` and `setup`. + +### `windowResize` Method + +The `windowResize` method is called when the pipeline is initialized and when the window size changes. In `windowResize`, we need to register the resources will be used when rendering. Only resources registered within the `windowResize` method can be utilized within the pipeline rendering. + +> Note: Registering resources merely adds resource descriptions to the system; if they are not used during rendering, system resources will not be allocated, and memory usage will not increase. + +### `setup` Method + +The `setup` method is called every frame when the camera is rendering, used to set up the rendering process for this camera. This method is for constructing the RenderGraph node graph; once constructed, it is handed over to the render pipeline for rendering. + +> Note: Only resources registered in `windowResize` can be used. + +The specific implementation of the built-in pipeline is in `builtin-pipeline.ts`. + +## Pipeline Registration and Usage + +The render pipeline implemented by the user can be registered with the rendering system during the script import phase. + +```typescript +import { rendering } from 'cc'; + +if (rendering) { + rendering.setCustomPipeline('Builtin', new BuiltinPipelineBuilder()); +} +``` + +The `Builtin` is the name of the built-in pipeline, which you can change to any string you desire. After changing it, switch the pipeline name to your own pipeline name in **Project Settings -> Graphics Settings -> Render Pipeline(New)** to use it. + +## Render Resource Management + +Cocos' Customizable Render Pipeline (CRP) is based on the `Render Graph` concept, but it differs from traditional `Render Graph` in terms of resource management. + +For resources that need to be written to, they must be registered within the pipeline. This is typically done in the `windowResize` method. + +When registering, you need to provide a unique identifier for the resource's name, so do not use the same name for different resources to avoid conflicts. + +If there are multiple cameras, you can append the camera's `Id` to the resource name to distinguish between resources of different cameras. + +```typescript +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + windowResize( + ppl: rendering.BasicPipeline, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, + nativeWidth: number, + nativeHeight: number, + ): void { + const id = window.renderWindowId; + const width = nativeWidth; + const height = nativeHeight; + + ppl.addRenderWindow( + window.colorName, + Format.RGBA8, nativeWidth, nativeHeight, window, + window.depthStencilName); + + ppl.addDepthStencil( + `SceneDepth${id}`, + Format.DEPTH_STENCIL, width, height, + ResourceResidency.MANAGED); + } +} +``` + +Within a single frame, please do not change the size or specifications of the resources, as this may lead to rendering errors. + +If settings are repeated, the parameters from the last setting will be taken. + +## Resource Residency and Lifecycle + +Resources registered within the pipeline have several types of residency (`ResourceResidency`): + +- `MANAGED` - Resources managed by the pipeline, which will be automatically released at the appropriate time. +- `MEMORYLESS` - On-chip resources that do not occupy video memory. They can only be used in one RenderPass and are automatically released after the RenderPass ends. +- `PERSISTENT` - Persistent resources managed by the pipeline, which will not be released until the pipeline is destroyed. Generally used for resources needed across frames. +- `EXTERNAL` - External resources that are not managed by the pipeline and require manual release by the user. +- `BACKBUFFER` - The backbuffer, which does not require user management. + +For resources of type `MANAGED`, the pipeline will release them after the GPU has finished using them, so they may be delayed by a few frames. + +Currently, the engine is optimized for mobile devices, requiring a reduction in the creation and destruction of `Framebuffers`, so the memory positions of resources will not be moved. In the future, on the desktop, `MANAGED` will serve as a transitional (`Transient`) resource, allowing for more memory optimization. + +## Rendering Process + +In the `setup` method, you can set the rendering process for the camera, which is called once per frame. + +The `cameras` parameter passed in is a list of cameras currently being rendered, which has already been sorted. + +The `ppl` parameter passed in is the current render pipeline. + +```typescript +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + setup( + cameras: renderer.scene.Camera[], + ppl: rendering.BasicPipeline, + ): void { + for (const camera of cameras) { + if (!camera.scene || !camera.window) { + continue; + } + + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_BEGIN, camera); + + this._buildSimplePipeline(camera, ppl); + + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_END, camera); + } + } +} +``` + +Above is the basic process for writing a render pipeline. + +## Building a Render Pipeline for Cameras + +Different cameras can have different render pipelines constructed for them to achieve various rendering effects. + +The following demonstrates a simple render pipeline from the built-in pipeline, mainly used for rendering editor `Gizmos`. + +```typescript +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + private _buildSimplePipeline( + ppl: rendering.BasicPipeline, + camera: renderer.scene.Camera, + ): void { + const width = Math.max(Math.floor(camera.window.width), 1); + const height = Math.max(Math.floor(camera.window.height), 1); + const colorName = this._cameraConfigs.colorName; + const depthStencilName = this._cameraConfigs.depthStencilName; + + const viewport = camera.viewport; // Reduce C++/TS interop + this._viewport.left = Math.round(viewport.x * width); + this._viewport.top = Math.round(viewport.y * height); + // Here we must use camera.viewport.width instead of camera.viewport.z, which + // is undefined on native platform. The same as camera.viewport.height. + this._viewport.width = Math.max(Math.round(viewport.width * width), 1); + this._viewport.height = Math.max(Math.round(viewport.height * height), 1); + + const clearColor = camera.clearColor; // Reduce C++/TS interop + this._clearColor.x = clearColor.x; + this._clearColor.y = clearColor.y; + this._clearColor.z = clearColor.z; + this._clearColor.w = clearColor.w; + + const pass = ppl.addRenderPass(width, height, 'default'); + + // bind output render target + if (forwardNeedClearColor(camera)) { + pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, this._clearColor); + } else { + pass.addRenderTarget(colorName, LoadOp.LOAD, StoreOp.STORE); + } + + // bind depth stencil buffer + if (camera.clearFlag & ClearFlagBit.DEPTH_STENCIL) { + pass.addDepthStencil( + depthStencilName, + LoadOp.CLEAR, + StoreOp.DISCARD, + camera.clearDepth, + camera.clearStencil, + camera.clearFlag & ClearFlagBit.DEPTH_STENCIL, + ); + } else { + pass.addDepthStencil(depthStencilName, LoadOp.LOAD, StoreOp.DISCARD); + } + + pass.setViewport(this._viewport); + + // The opaque queue is used for Reflection probe preview + pass.addQueue(QueueHint.OPAQUE) + .addScene(camera, SceneFlags.OPAQUE); + + // The blend queue is used for UI and Gizmos + let flags = SceneFlags.BLEND | SceneFlags.UI; + if (this._cameraConfigs.enableProfiler) { + flags |= SceneFlags.PROFILER; + pass.showStatistics = true; + } + pass.addQueue(QueueHint.BLEND) + .addScene(camera, flags); + } +} +``` + +## Dynamically Construct Render Pipeline + +In the `setup` method, you can dynamically construct the render pipeline based on the camera, platform, hardware parameters, and more. + +Firstly, you can collect the current environmental information at the beginning of the `windowResize` and `setup` methods. + +The `setupPipelineConfigs` in the built-in pipeline is responsible for collecting platform-related information. The `setupCameraConfigs` function is used to collect camera-related information. + +```typescript +function setupPipelineConfigs( + ppl: rendering.BasicPipeline, + configs: PipelineConfigs, +): void { + const device = ppl.device; + // Platform + configs.isWeb = !sys.isNative; + configs.isWebGL1 = device.gfxAPI === gfx.API.WEBGL; + configs.isWebGPU = device.gfxAPI === gfx.API.WEBGPU; + configs.isMobile = sys.isMobile; + + // Rendering + configs.isHDR = ppl.pipelineSceneData.isHDR; + configs.useFloatOutput = ppl.getMacroBool('CC_USE_FLOAT_OUTPUT'); + configs.toneMappingType = ppl.pipelineSceneData.postSettings.toneMappingType; + + // ... +} + +function setupCameraConfigs( + camera: renderer.scene.Camera, + pipelineConfigs: PipelineConfigs, + cameraConfigs: CameraConfigs, +): void { + cameraConfigs.colorName = camera.window.colorName; + cameraConfigs.depthStencilName = camera.window.depthStencilName; + + cameraConfigs.useFullPipeline = (camera.visibility & (Layers.Enum.DEFAULT)) !== 0; + + setupPostProcessConfigs(pipelineConfigs, cameraConfigs.settings, cameraConfigs); + // ... +} + +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + windowResize( + ppl: rendering.BasicPipeline, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, + nativeWidth: number, + nativeHeight: number, + ): void { + setupPipelineConfigs(ppl, this._configs); + setupCameraConfigs(camera, this._configs, this._cameraConfigs); + + // register render resources + // ... + } + setup(cameras: renderer.scene.Camera[], ppl: rendering.BasicPipeline): void { + for (const camera of cameras) { + if (!camera.scene || !camera.window) { + continue; + } + // get camera information + setupCameraConfigs(camera, this._configs, this._cameraConfigs); + + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_BEGIN, camera); + + // build render pipeline according to the camera information. + if (this._cameraConfigs.useFullPipeline) { + this._buildForwardPipeline(ppl, camera, camera.scene); + } else { + this._buildSimplePipeline(ppl, camera); + } + + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_END, camera); + } + } +} +``` + +Then, based on this information, you can dynamically construct the render pipeline. + +Given the diversity of platforms and performance requirements, the selection and determination of algorithms can be quite complex. This is the main scenario where the new pipeline shines, aiming to simplify the construction of the render pipeline and improve development efficiency. + +```typescript +private _buildForwardPipeline( + ppl: rendering.BasicPipeline, + camera: renderer.scene.Camera, + scene: renderer.RenderScene, +): void { + // Init + const settings = this._cameraConfigs.settings; + const nativeWidth = Math.max(Math.floor(camera.window.width), 1); + const nativeHeight = Math.max(Math.floor(camera.window.height), 1); + const width = this._cameraConfigs.enableShadingScale + ? Math.max(Math.floor(nativeWidth * this._cameraConfigs.shadingScale), 1) + : nativeWidth; + const height = this._cameraConfigs.enableShadingScale + ? Math.max(Math.floor(nativeHeight * this._cameraConfigs.shadingScale), 1) + : nativeHeight; + const id = camera.window.renderWindowId; + const colorName = this._cameraConfigs.colorName; + const sceneDepth = this._cameraConfigs.enableShadingScale + ? `ScaledSceneDepth${id}` + : `SceneDepth${id}`; + const radianceName = this._cameraConfigs.enableShadingScale + ? `ScaledRadiance${id}` + : `Radiance${id}`; + const ldrColorName = this._cameraConfigs.enableShadingScale + ? `ScaledLdrColor${id}` + : `LdrColor${id}`; + const mainLight = scene.mainLight; + + // Forward Lighting (Light Culling) + this.forwardLighting.cullLights(scene, camera.frustum); + + // Main Directional light CSM Shadow Map + if (this._cameraConfigs.enableMainLightShadowMap) { + assert(!!mainLight); + this._addCascadedShadowMapPass(ppl, id, mainLight, camera); + } + + // Spot light shadow maps (Mobile or MSAA) + if (this._cameraConfigs.singleForwardRadiancePass) { + // Currently, only support 1 spot light with shadow map on mobile platform. + // TODO(zhouzhenglong): Relex this limitation. + this.forwardLighting.addSpotlightShadowPasses(ppl, camera, this._configs.mobileMaxSpotLightShadowMaps); + } + + this._tryAddReflectionProbePasses(ppl, id, mainLight, camera.scene); + + // Forward Lighting + let lastPass: rendering.BasicRenderPassBuilder; + if (this._cameraConfigs.enablePostProcess) { // Post Process + // Radiance and DoF + if (this._cameraConfigs.enableDOF) { + assert(!!settings.depthOfField.material); + const dofRadianceName = `DofRadiance${id}`; + // Disable MSAA, depth stencil cannot be resolved cross-platformly + this._addForwardRadiancePasses(ppl, id, camera, width, height, mainLight, + dofRadianceName, sceneDepth, true, StoreOp.STORE); + this._addDepthOfFieldPasses(ppl, settings, settings.depthOfField.material, + id, camera, width, height, + dofRadianceName, sceneDepth, radianceName, ldrColorName); + } else { + this._addForwardRadiancePasses( + ppl, id, camera, width, height, mainLight, + radianceName, sceneDepth); + } + // Bloom + if (this._cameraConfigs.enableBloom) { + assert(!!settings.bloom.material); + this._addKawaseDualFilterBloomPasses( + ppl, settings, settings.bloom.material, + id, width, height, radianceName); + } + // Tone Mapping and FXAA + if (this._cameraConfigs.enableFXAA) { + assert(!!settings.fxaa.material); + const copyAndTonemapPassNeeded = this._cameraConfigs.enableHDR + || this._cameraConfigs.enableColorGrading; + const ldrColorBufferName = copyAndTonemapPassNeeded ? ldrColorName : radianceName; + // FXAA is applied after tone mapping + if (copyAndTonemapPassNeeded) { + this._addCopyAndTonemapPass(ppl, settings, width, height, radianceName, ldrColorBufferName); + } + // Apply FXAA + if (this._cameraConfigs.enableShadingScale) { + const aaColorName = `AaColor${id}`; + // Apply FXAA on scaled image + this._addFxaaPass(ppl, settings.fxaa.material, + width, height, ldrColorBufferName, aaColorName); + // Copy FXAA result to screen + if (this._cameraConfigs.enableFSR && settings.fsr.material) { + // Apply FSR + lastPass = this._addFsrPass(ppl, settings, settings.fsr.material, + id, width, height, aaColorName, + nativeWidth, nativeHeight, colorName); + } else { + // Scale FXAA result to screen + lastPass = this._addCopyPass(ppl, + nativeWidth, nativeHeight, aaColorName, colorName); + } + } else { + // Image not scaled, output FXAA result to screen directly + lastPass = this._addFxaaPass(ppl, settings.fxaa.material, + nativeWidth, nativeHeight, ldrColorBufferName, colorName); + } + } else { + // No FXAA (Size might be scaled) + lastPass = this._addTonemapResizeOrSuperResolutionPasses(ppl, settings, id, + width, height, radianceName, ldrColorName, + nativeWidth, nativeHeight, colorName); + } + } else if (this._cameraConfigs.enableHDR || this._cameraConfigs.enableShadingScale) { // HDR or Scaled LDR + this._addForwardRadiancePasses(ppl, id, camera, + width, height, mainLight, radianceName, sceneDepth); + lastPass = this._addTonemapResizeOrSuperResolutionPasses(ppl, settings, id, + width, height, radianceName, ldrColorName, + nativeWidth, nativeHeight, colorName); + } else { // LDR (Size is not scaled) + lastPass = this._addForwardRadiancePasses(ppl, id, camera, + nativeWidth, nativeHeight, mainLight, + colorName, this._cameraConfigs.depthStencilName); + } + + // UI size is not scaled, does not have AA + this._addUIQueue(camera, lastPass); +} +``` + +## BuiltinPipelineSettings + +The configuration of the built-in pipeline, provided by the `BuiltinPipelineSettings` component, will save a JSON object to `camera.pipelineSettings`. + +```typescript +export class BuiltinPipelineSettings extends Component { + @property + private readonly _settings: PipelineSettings = makePipelineSettings(); + + onEnable(): void { + fillRequiredPipelineSettings(this._settings); + const cameraComponent = this.getComponent(Camera)!; + const camera = cameraComponent.camera; + camera.pipelineSettings = this._settings; + + if (EDITOR) { + this._tryEnableEditorPreview(); + } + } + onDisable(): void { + const cameraComponent = this.getComponent(Camera)!; + const camera = cameraComponent.camera; + camera.pipelineSettings = null; + + if (EDITOR) { + this._disableEditorPreview(); + } + } +} +``` + +Based on this JSON object, you can set the rendering effects for the camera. + +```typescript +function setupPostProcessConfigs( + pipelineConfigs: PipelineConfigs, + settings: PipelineSettings, + cameraConfigs: CameraConfigs, +) { + cameraConfigs.enableDOF = pipelineConfigs.supportDepthSample + && settings.depthOfField.enabled + && !!settings.depthOfField.material; + + cameraConfigs.enableBloom = settings.bloom.enabled + && !!settings.bloom.material; + + cameraConfigs.enableColorGrading = settings.colorGrading.enabled + && !!settings.colorGrading.material + && !!settings.colorGrading.colorGradingMap; + + cameraConfigs.enableFXAA = settings.fxaa.enabled + && !!settings.fxaa.material; + + cameraConfigs.enablePostProcess = (cameraConfigs.enableDOF + || cameraConfigs.enableBloom + || cameraConfigs.enableColorGrading + || cameraConfigs.enableFXAA); +} +``` + +## Adding Post-Processing Effects + +Here, we will use the bloom effect as an example to demonstrate how to add post-processing effects. + +```typescript +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + windowResize( + ppl: rendering.BasicPipeline, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, + nativeWidth: number, + nativeHeight: number, + ): void { + //... + + // Bloom (Kawase Dual Filter) + if (this._cameraConfigs.enableBloom) { + let bloomWidth = width; + let bloomHeight = height; + for (let i = 0; i !== settings.bloom.iterations + 1; ++i) { + bloomWidth = Math.max(Math.floor(bloomWidth / 2), 1); + bloomHeight = Math.max(Math.floor(bloomHeight / 2), 1); + ppl.addRenderTarget(`BloomTex${id}_${i}`, this._cameraConfigs.radianceFormat, bloomWidth, bloomHeight); + } + } + + //... + } + + private _addKawaseDualFilterBloomPasses( + ppl: rendering.BasicPipeline, + settings: PipelineSettings, + bloomMaterial: Material, + id: number, + width: number, + height: number, + radianceName: string, + ): void { + // Based on Kawase Dual Filter Blur. Saves bandwidth on mobile devices. + // eslint-disable-next-line max-len + // https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_slides.pdf + + // Size: [prefilter(1/2), downsample(1/4), downsample(1/8), downsample(1/16), ...] + const iterations = settings.bloom.iterations; + const sizeCount = iterations + 1; + this._bloomWidths.length = sizeCount; + this._bloomHeights.length = sizeCount; + this._bloomWidths[0] = Math.max(Math.floor(width / 2), 1); + this._bloomHeights[0] = Math.max(Math.floor(height / 2), 1); + for (let i = 1; i !== sizeCount; ++i) { + this._bloomWidths[i] = Math.max(Math.floor(this._bloomWidths[i - 1] / 2), 1); + this._bloomHeights[i] = Math.max(Math.floor(this._bloomHeights[i - 1] / 2), 1); + } + + // Bloom texture names + this._bloomTexNames.length = sizeCount; + for (let i = 0; i !== sizeCount; ++i) { + this._bloomTexNames[i] = `BloomTex${id}_${i}`; + } + + // Setup bloom parameters + this._bloomParams.x = this._configs.useFloatOutput ? 1 : 0; + this._bloomParams.x = 0; // unused + this._bloomParams.z = settings.bloom.threshold; + this._bloomParams.w = settings.bloom.enableAlphaMask ? 1 : 0; + + // Prefilter pass + const prefilterPass = ppl.addRenderPass(this._bloomWidths[0], this._bloomHeights[0], 'cc-bloom-prefilter'); + prefilterPass.addRenderTarget( + this._bloomTexNames[0], + LoadOp.CLEAR, + StoreOp.STORE, + this._clearColorTransparentBlack, + ); + prefilterPass.addTexture(radianceName, 'inputTexture'); + prefilterPass.setVec4('g_platform', this._configs.platform); + prefilterPass.setVec4('bloomParams', this._bloomParams); + prefilterPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 0); + + // Downsample passes + for (let i = 1; i !== sizeCount; ++i) { + const downPass = ppl.addRenderPass(this._bloomWidths[i], this._bloomHeights[i], 'cc-bloom-downsample'); + downPass.addRenderTarget(this._bloomTexNames[i], LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); + downPass.addTexture(this._bloomTexNames[i - 1], 'bloomTexture'); + this._bloomTexSize.x = this._bloomWidths[i - 1]; + this._bloomTexSize.y = this._bloomHeights[i - 1]; + downPass.setVec4('g_platform', this._configs.platform); + downPass.setVec4('bloomTexSize', this._bloomTexSize); + downPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 1); + } + + // Upsample passes + for (let i = iterations; i-- > 0;) { + const upPass = ppl.addRenderPass(this._bloomWidths[i], this._bloomHeights[i], 'cc-bloom-upsample'); + upPass.addRenderTarget(this._bloomTexNames[i], LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); + upPass.addTexture(this._bloomTexNames[i + 1], 'bloomTexture'); + this._bloomTexSize.x = this._bloomWidths[i + 1]; + this._bloomTexSize.y = this._bloomHeights[i + 1]; + upPass.setVec4('g_platform', this._configs.platform); + upPass.setVec4('bloomTexSize', this._bloomTexSize); + upPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 2); + } + + // Combine pass + const combinePass = ppl.addRenderPass(width, height, 'cc-bloom-combine'); + combinePass.addRenderTarget(radianceName, LoadOp.LOAD, StoreOp.STORE); + combinePass.addTexture(this._bloomTexNames[0], 'bloomTexture'); + combinePass.setVec4('g_platform', this._configs.platform); + combinePass.setVec4('bloomParams', this._bloomParams); + combinePass + .addQueue(QueueHint.BLEND) + .addFullscreenQuad(bloomMaterial, 3); + } +} +``` + +Taking the `Prefilter Pass` as an example: + +First, we add a render pass, specifying its size and the name of the effect pass used. + +```typescript +const prefilterPass = ppl.addRenderPass(this._bloomWidths[0], this._bloomHeights[0], 'cc-bloom-prefilter'); +``` + +The effect pass name is defined within the effect file. Only effects that match the pass name can be used in this render pass. + +``` +// bloom1.effect +CCEffect %{ + techniques: + - passes: + - vert: bloom-vs + frag: prefilter-fs + pass: cc-bloom-prefilter // pass name + rasterizerState: + cullMode: none + depthStencilState: + depthTest: false + depthWrite: false +}% + +CCProgram bloom-vs %{ + ... +}% + +CCProgram prefilter-fs %{ + ... +}% + +``` + +Then, we added a render target to store the results of this render pass. + +We also added the scene color as an input, binding it to the `inputTexture` in the effect. + +```typescript +prefilterPass.addRenderTarget( + this._bloomTexNames[0], + LoadOp.CLEAR, + StoreOp.STORE, + this._clearColorTransparentBlack, +); +prefilterPass.addTexture(radianceName, 'inputTexture'); +``` + +In the effect, we define `inputTexture` to receive the input scene color and mark its frequency as `pass`. + +For UBOs, Textures, and Buffers, you can use the `#pragma rate` directive to mark the frequency and control the `DescriptorSet` where the resources reside. + +If the frequency is marked as `pass`, Textures and Buffers can be used as outputs, and their states will be tracked by the RenderGraph. If not marked, it is the default frequency, commonly used for material parameters. + +```txt +CCProgram prefilter-fs %{ + precision highp float; + #include + + in vec2 v_uv; + + #pragma rate BloomUBO pass + uniform BloomUBO { + mediump vec4 bloomParams;// x: useHDRIlluminance z: threshold, w: enableAlphaMask + }; + + #pragma rate inputTexture pass + uniform sampler2D inputTexture; // hdr input + + layout(location = 0) out vec4 fragColor; + + float luminance(vec3 color) { + return dot(color, vec3(0.3, 0.5, 0.2)); + } + + void main() { + vec4 color = texture(inputTexture, v_uv); + + float contribute = step(bloomParams.z, luminance(color.rgb)); + contribute *= mix(1.0, step(253.0 / 255.0, color.a), bloomParams.w); + + fragColor = vec4(color.xyz * contribute, 1.0); + } +}% +``` + +If the frequency set for UBO is `pass`, we can pass parameters by directly setting the value of Uniform in the render pipeline. + +```typescript +prefilterPass.setVec4('g_platform', this._configs.platform); +prefilterPass.setVec4('bloomParams', this._bloomParams); +``` + +Finally, we add this rendering pass to the queue through `addQueue`. Since the effect does not have Blend enabled, we set `QueueHint` to `OPAQUE`. The `QueueHint` is currently only used for debugging and does not affect the actual rendering. + +Lastly, we draw a fullscreen quad through `addFullscreenQuad`, using the first pass of the `bloomMaterial`, which is the prefilter. + +```typescript +prefilterPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 0); +``` + +For passes such as `Downsample`, `Upsample`, and `Combine`, the process is similar. + +## Resource State Tracking + +From the example above, we can see that when setting up rendering passes, we only need to focus on the names of the resources and not their states. + +The `RenderGraph` will automatically track the state of resources based on their names, ensuring their correct usage. + +Between rendering passes, we will automatically insert `Barriers` based on the resource's bound ShaderStage, read/write status, and resource type, to synchronize, refresh caches, and convert resource layouts. + +In this way, we can focus on implementing rendering effects without worrying about resource state management. diff --git a/versions/4.0/en/scripting/access-node-component.md b/versions/4.0/en/scripting/access-node-component.md new file mode 100644 index 0000000000..a9f636392e --- /dev/null +++ b/versions/4.0/en/scripting/access-node-component.md @@ -0,0 +1,248 @@ +# Access Nodes and Components + +You can modify __Nodes__ and __Components__ in the **Inspector** panel, and also dynamically using scripts. The advantage of dynamic modification is that it can continuously modify properties and transition properties within a period of time to achieve gradual effects. Scripts can also respond to player input, modify, create and destroy __Nodes__ or __Components__, and implement various game logic. To achieve these effects, developers need to obtain the __Node__ or __Component__ that needs to be modified in the script. + +## Document topics + +- Obtain the node where the component is located. +- Obtain other components. +- Use **Inspector** panel to set up nodes and components. +- Find child nodes. +- Global node search. +- Access values in existing variables. + +## Obtain the node where the component is located + +It's easy to get the node where the component is. Just access the `this.node` variable in the component method: +```ts + start(){ + let node = this.node; + node.setPosition(0.0,0.0,0.0); + } +``` + +## Obtaining other components + +It will often be need to get other components on the same node. Use the `getComponent` API, which will help to find the component that is needed. + +```ts +import { _decorator, Component, LabelComponent } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + private label: any = null + + start(){ + this.label = this.getComponent(LabelComponent); + let text = this.name + 'started'; + // Change the text in Label Component + this.label.string = text; + } +} +``` + +It is also possible to pass in a class name for `getComponent`. For user-defined components, the class name is the file name of the script and is **case sensitive**. For example, the component declared in `SinRotate.ts`, the class name is `SinRotate`. + +```ts + let rotate = this.getComponent("SinRotate"); +``` + +There is also ther `getComponent` method on the node, and their functions are the same: + +```ts + start() { + console.log( this.node.getComponent(LabelComponent) === this.getComponent(LabelComponent) ); // true + } +``` + +If the component that is needed is not found on the node, `getComponent` will return `null`. If you try to access the value of `null`, a __TypeError__ error will be thrown at runtime. If you are not sure whether the component exists, please remember to check: + +```ts +import { _decorator, Component, LabelComponent } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + private label: any =null; + + start() { + this.label = this.getComponent(LabelComponent); + if (this.label) { + this.label.string = "Hello"; + } else { + console.error("Something wrong?"); + } + } +} +``` + +## Get other nodes and their components + +It is usually not enough to only have access to the node's own components, and scripts usually require interaction between multiple nodes. For example, a cannon that automatically aims at the player needs to constantly obtain the latest position of the player. __Cocos Creator__ provides some different methods to obtain other nodes or components. + +### Use the Inspector panel to set the node + +The most straightforward way is to set the objects you need in the **Inspector** panel. Take node as an example, this only needs to declare an property with type `Node` in the script: + +```ts +// Cannon.ts + +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("Cannon") +export class Cannon extends Component { + // Declare Player properties + @property({type:Node}) + private player = null; +} +``` + +This code declares a `player` property in `properties`, the default value is `null`, and its object type is specified as `Node`. This is equivalent to declaring `public Node player = null;` in other languages. After the script is compiled, this component looks like this in the **Inspector** panel: + +![player-in-inspector-null](access-node-component/player-in-inspector-null.png) + +Then you can drag any node on the __Hierarchy__ panel to the `Player` control: + +![player-in-inspector](access-node-component/player-in-inspector.png) + +The `Player` property will be set successfully, and can be accessed directly in a script. Example: + +```ts +// Cannon.ts + +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("Cannon") +export class Cannon extends Component { + + @property({type:Node}) + private player = null; + + start() { + console.log('The player is ' + this.player.name); + } +} +``` + +### Use the Inspector panel to set up components + +In the above example, if the type of the property is declared as a `Player` component, when the `Player` node is dragged to the **Inspector** panel, the `Player` property will be set to the `Player` component in this node. This way developers don't need to call `getComponent()` themselves. + +```ts +// Cannon.ts + +import { _decorator, Component } from 'cc'; +const { ccclass, property } = _decorator; +import { Player } from "Player"; + +@ccclass("Cannon") +export class Cannon extends Component { + @property({type:Player}) + private player = null; + + start(){ + let PlayerComp = this.player; + } +} +``` + +The default value of the property can be changed from `null` to array `[]`, so that multiple objects in the **Inspector** panel can be set at the same time. However, if dynamically obtaining other objects at runtime is needed, it is also necessary to use the search method described below. + +### Find child nodes + +Sometimes, there are many objects of the same type in the game scene, such as turrets, enemies, and special effects, and they usually have a global script to manage them uniformly. Using the **Inspector** panel to associate them with this script one by one, the work will be very tedious. In-order to better manage these objects uniformly, they can be put under a unified parent object, and then obtain all the child objects through the parent object: + +```ts +// CannonManager.ts + +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("CannonManager") +export class CannonManager extends Component { + + start() { + let cannons = this.node.children; + //... + } + +} +``` + +Using `getChildByName`: + +```ts +this.node.getChildByName("Cannon 01"); +``` + +If the child node has a deeper level, `find` can be used, and will search step by step according to the path passed in: + +```ts +find("Cannon 01/Barrel/SFX", this.node); +``` + +### Global name lookup + +When `find` only passes in the first parameter, it will be searched step by step from the scene root node: + +```ts +this.backNode = find("Canvas/Menu/Back"); +``` + +## Accessing values in existing variables + +If you have saved references to nodes or components in one place, you can also access them directly. + +### Accessing via module + +Use `import` to implement script cross-file operations. Example: + +```ts +// Global.ts, now the filename matters +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("Global") +export class Global extends Component { + + public static backNode:any=null; + public static backLabel:any=null; +} +``` + +Each script can use `import{} from` + file name (without path) to get the object of the other party's exports. Example: + +```ts +// Back.ts +import { _decorator, Component, Node, LabelComponent } from 'cc'; +const { ccclass, property } = _decorator; +// this feels more safe since you know where the object comes from +import{Global}from "./Global"; + +@ccclass("Back") +export class Back extends Component { + onLoad(){ + Global.backNode=this.node; + Global.backLabel=this.getComponent(LabelComponent); + } +} +``` + +```ts +// AnyScript.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; +// this feels more safe since you know where the object comes from +import{Global}from "./Global"; + +@ccclass("AnyScript") +export class AnyScript extends Component { + start () { + const text = "Back"; + Global.backLabel.string=text; + } +} +``` diff --git a/versions/4.0/en/scripting/access-node-component/player-in-inspector-null.png b/versions/4.0/en/scripting/access-node-component/player-in-inspector-null.png new file mode 100644 index 0000000000..74441fb7ef Binary files /dev/null and b/versions/4.0/en/scripting/access-node-component/player-in-inspector-null.png differ diff --git a/versions/4.0/en/scripting/access-node-component/player-in-inspector.png b/versions/4.0/en/scripting/access-node-component/player-in-inspector.png new file mode 100644 index 0000000000..244e220a0c Binary files /dev/null and b/versions/4.0/en/scripting/access-node-component/player-in-inspector.png differ diff --git a/versions/4.0/en/scripting/basic-node-api.md b/versions/4.0/en/scripting/basic-node-api.md new file mode 100644 index 0000000000..282f61d7ca --- /dev/null +++ b/versions/4.0/en/scripting/basic-node-api.md @@ -0,0 +1,128 @@ +# Common Node and Component Interfaces + +After obtaining a __Node__ or __Component__ instance through the method introduced in the [Access Node and Component](access-node-component.md) documentation, there are common interfaces that can be used to achieve the various effects needed through the node and component instance and it's operation. + +Please review the [Node](%__APIDOC__%/en/class/Node) and the [Component](__APIDOC__/en/class/Component) API documentation. + +## Node Status and Level Operations + +Suppose you are inside a running component script, to access a node inside the current script use `this.node`. + +### Activating and Deactivating a Node + +A node is activated by default. It's activation state can be changed in code by setting the node's `active` property. Example: + +```ts +this.node.active = false; +``` + +Setting the `active` property and switching the active and closed states of the node in the editor have the same effect. When a node is down, all its components will be disabled. At the same time, all its child nodes and components on the child nodes will also be disabled. It should be noted that when child nodes are disabled, their `active` properties are not changed, so they will return to their original state when the parent node is reactivated. + +In other words, `active` actually represents the active state of the node **itself**, and whether this node **current** can be activated depends on its parent node. And if it is not in the current scene, it cannot be activated. We can use the read-only property `activeInHierarchy` on the node to determine whether it is currently activated. Example: + +```ts +this.node.active = true; +``` + +If the node is in the **can be activated** state, modifying `active` to `true` will immediately trigger the following activation operations: + +- Reactivate the node and all child nodes under the node whose active is true in the scene +- All components on this node and all child nodes will be enabled, and the update method in them will be executed every frame afterwards +- If there are `onEnable` methods on these components, these methods will be executed + + ```ts + this.node.active = false; + ``` + +If the node is already activated, changing `active` to `false` will immediately trigger the following shutdown operations: + +- Hide the node and all child nodes under the node in the scene +- All components on this node and all child nodes will be disabled, that is, the code in `update` in these components will no longer be executed +- If there are `onDisable` methods on these components, these methods will be executed + +### Change the Parent Node of a Node + +Suppose the parent node is `parentNode` and the child node is `this.node`. + +This is valid: + +```ts +// method 1 +this.node.parent = parentNode; +``` + +This is also valid: + +```ts +// method 2 +this.node.removeFromParent(); +parentNode.addChild(this.node); +``` + +These two methods are equivalent. + +> **Notes**: +> 1. The `removeFromParent` usually needs to pass a `false`, otherwise it will empty the bound events and actions, etc. on the node by default in versions prior to v3.0. +> 2. After creating a new node through the method introduced in the [Create and Destroy Node](create-destroy.md) documentation, it is a **must** to set a parent node for the node to correctly initialize the node. + +### Child Nodes of the Parent Node + +`this.node.children` will return an array of all child nodes of the node. + +`this.node.children.length` will return the number of children of the node. + +> **Note**: the above two APIs will only return the direct child nodes of the node, not the child nodes of the child node. + +## Changing Node Transformations (position, rotation, scaling) + +### Changing Node Location + +There are two ways: + +1. Use the `setPosition` method: + + ```ts + this.node.setPosition(100, 50, 100); + // Or + this.node.setPosition(new Vec3(100,50,100)); + ``` + +2. Setting the `position` variable: + + ```ts + this.node.position = new Vec3(100,50,100); + ``` + +The above two usages are equivalent. + +### Changing Node Rotation + +Example: + +```ts +this.node.setRotation(quaternion); +``` + +Or set local rotation by __Euler angle__: + +```ts +this.node.setRotationFromEuler(90,90,90); +``` + +### Changing Node Scale + +Example: + +```ts +this.node.setScale(2,2,2); +``` + +## Common Component Interface + +`Component` is the base class of all components, and any component includes the following common interfaces (assuming that we use this to refer to this component in the script of the component): + +- `this.node`: The node instance to which this component belongs. +- `this.enabled`: Whether to execute the `update` method of the component every frame, and also to control whether the renderable component is displayed. +- `update(deltaTime: number)`: As a member method of the component, when the component's `enabled` property is `true`, the code in it will be executed every frame. +- `onLoad()`: Executed when the node where the component is located is initialized (when the node is added to the node tree). +- `start()`: It will be executed before the first `update` of the component, usually used for logic that needs to be executed after the `onLoad` of all components is initialized. diff --git a/versions/4.0/en/scripting/basic.md b/versions/4.0/en/scripting/basic.md new file mode 100644 index 0000000000..ca80114338 --- /dev/null +++ b/versions/4.0/en/scripting/basic.md @@ -0,0 +1,40 @@ +# Operating Environment + +The APIs for the __Cocos Creator__ engine all exist in the module `cc`. They can be imported it using standard ES6 module import syntax. Example: + +```ts +import { + Component, // Import class Component + _decorator, // Import namespace _decorator + Vec3 // Import class Vec3 +} from 'cc'; +import * as modules from 'cc'; // Import the entire Cocos Creator module as a namespace Cocos Creator + +@_decorator.ccclass("MyComponent") +export class MyComponent extends Component { + public v = new Vec3(); +} +``` + +## Reserved identifier `cc` + +Due to historical reasons, `cc` is an identifier reserved for __Cocos Creator__. Its behavior is *equivalent to* having defined an object named `cc` at the top of any module. Therefore, developers should not use `cc` as the name of any **global** object. Example: + +```ts +/* const cc = {}; // Every Cocos Creator script is equivalent to an implicit definition here */ + +import * as modules from 'cc'; // Error: Namespace import name cc is reserved by Cocos Creator + +const cc = { x: 0 }; +console.log(cc.x); // Error: The global object name cc is reserved by Cocos Creator + +function f () { + const cc = { x: 0 }; + console.log(cc.x); // Correct: cc can be used as the name of a local object + + const o = { cc: 0 }; + console.log(o.cc); // Correct: cc can be used as a property name +} + +console.log(cc, typeof cc); // error: behavior is undefined +``` diff --git a/versions/4.0/en/scripting/coding-setup.md b/versions/4.0/en/scripting/coding-setup.md new file mode 100644 index 0000000000..aa8ef73295 --- /dev/null +++ b/versions/4.0/en/scripting/coding-setup.md @@ -0,0 +1,144 @@ +# Configuring The Code Editing Environment + +The [Default Script Editor](../editor/preferences/index.md#external-program) can be specified in the **Preferences** panel. Double-click the script file in **Assets** panel to open the code editor to quickly edit the code. Visual Studio Code is used as an example to introduce how to configure the development environment. [Visual Studio Code](https://code.visualstudio.com/) (hereinafter referred to as VS Code) is a lightweight cross-platform IDE created by Microsoft. It supports Windows, Mac, and Linux platforms. Installation and configuration are very simple. Using VS Code to manage and edit project script codes can easily realize functions such as syntax highlighting, smart code prompts, and web page debugging. + +## Installing VS Code + +Go to the [VS Code Official Website](https://code.visualstudio.com/) and click the download link on the homepage to download it. + +MacOS users should decompress the downloaded package and double-click **Visual Studio Code** to run it. + +Windows users should run **VSCodeUserSetup.exe** after downloading it and follow the prompts to complete the installation. + +## IntelliSence Data + +When creating a project in Cocos Creator 3.x, a [tsconfig.json](tsconfig.md) file will be automatically generated in the project directory, which is configured with a directory file path for code hinting. When opening the project with VS Code and writing code, it will automatically pop up the Cocos Creator engine API suggestions. If the project is upgraded, the engine API will also be updated automatically. + +After starting VS Code, select **File -> Open Folder...** in the main menu, and select the project root directory in the pop-up dialog box, which is the path where `assets` and `project.json` are located. Then when creating a new script, or opening the original script for editing, there will be a syntax prompt. + +![vs code](coding-setup/vscode.png) + +> **Note**: when using a custom engine, or switching to use the built-in engine/custom engine, if the API code hinting are not updated, please follow the steps below to get the latest code hinting: +> +> 1. Delete the `cache` folder under the `bin` directory in the custom engine (the built-in engine is the `resources\resources\3d\engine\bin` directory) +> 2. Execute `npm run build-declaration` in the engine directory + +## Setting file display and search filter + +In the main menu of VS Code, select **File (Windows)/Code (Mac) -> Preferences -> Settings**, or select the **Setting** option in ![gear.png](coding-setup/gear.png) in the lower left corner, this operation will open the user configuration file **USER SETTINGS**: + +![vs code](coding-setup/vscode-setting.png) + +Now, enter **exclude** search in the search box above and find **Files: Exclude** and **Search: Exclude** modules: + +![vs code](coding-setup/vscode-exclude.png) + +Next, click **Add Pattern** to add the missing parts in the following content: + +```json +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.meta": true, + "library/": true, + "local/": true, + "temp/": true + }, + + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "build/": true, + "temp/": true, + "library/": true, + "**/*.anim": true + } +} +``` + +The fields above will set the directories to exclude from the search for VS Code, as well as the types of files to hide in the file list. Since `build`, `temp`, and `library` are all paths automatically generated when the editor is running, and will contain the content of the script we wrote, they should be excluded from the search. And each file in the `assets` directory will generate a `.meta` file. Generally speaking, there is no need to care about its content, just let the editor help manage these files. + +## VS Code Advanced Use + +Cocos Creator integrates the **Add Compilation Task** and **Add Chrome Debug Configuration** functions in **Developer -> VS Code Workflow** in the top menu bar to better assist development: + +![vscode workflow](coding-setup/vscode-workflow.png) + +- **Add compilation task**: used to trigger the script compilation of Creator in VS Code. For details, please refer to the content in the **Use VS Code to activate script compilation** section. + +- **Add Chrome Debug configuration**: used to debug the web version of the game. For details, please refer to the following **Use VS Code to debug the web version of the game** section. + +### Using VS Code to activate script compilation + +After using the external script editor to modify the project script, return to Cocos Creator to trigger script compilation.
The **add compilation task** function is provided in Creator, which activates the compilation of Creator by sending a request to a specific address through the API of a preview server. After modifying the project script the external script editor, executing the **compilation task** can trigger script compilation without returning to Cocos Creator. + +#### Installing cURL + +First, make sure that the [cURL command](https://curl.haxx.se/) can be run in the operating system. If when running the `curl` command on the command line of the Windows operating system and the command cannot be found, install cURL to operating system: + +- Visit this webpage: + +- Click the control shown by the arrow in the figure below to complete the human-machine identity verification. + + ![curl download](coding-setup/curl-download.png) + +- Click `curl-7.46.0-win64.exe` to start downloading and installing + +#### Adding VS Code compilation task + +To activate script compilation in VS Code, perform the following steps: + +1. Click **Developer -> VS Code Workflow -> Add Compilation Task** on the top menu bar of Creator. This operation will add the task configuration file ,`tasks.json`, under the `.vscode` folder of the project directory. + + ![task.json](coding-setup/tasks-json.png) + +2. Press the shortcut key Cmd/Ctrl + P in VS Code to activate the **Quick Open** input box, then enter `task CocosCreator compile`, select `CocosCreator compile`. + + ![task compile](coding-setup/task-compile.png) + + Next, select the output type: + + ![task compile](coding-setup/run-task.png) + +3. After the task is completed, the results will be displayed in the output panel at the bottom of the VS Code window (the output results will vary depending on the VS Code version and configuration). + +In this way, after using VS Code to edit the script, execute the second step to trigger the script compilation of Creator without returning to Creator. + +VS Code can also configure shortcut keys for compilation tasks, select **File (Windows)/Code (Mac) -> Preferences -> Keyboard Shortcuts** in the main menu, or select the **Keyboard shortcut** option in ![gear.png](coding-setup/gear.png) in the lower left corner, this operation will open the shortcut configuration file. Then modify the shortcut keys of the compilation task as needed. For example, set it to Cmd/Ctrl + Shift + B in the following figure: + +![set compile](coding-setup/set-compile.png) + +Next, press the shortcut key Cmd/Ctrl + Shift + B in VS Code and it will automatically display `CocosCreator compile`, no need to search manually. + +For more information about configuring and executing tasks in VS Code, please refer to the [Integrate with External Tools via Tasks](https://code.visualstudio.com/docs/editor/tasks) documentation. + +### Using VS Code to debug web games + +VS Code has excellent debugging capabilities. It is possible to directly debug the web version of the game program in the source code project. + +First, install: + +- [Google Chrome](https://www.google.com/chrome/) + +- VS Code plug-in: Debugger for Chrome + + Click the **Extensions** button in the left navigation bar of VS Code to open the extension panel, enter **Debugger for Chrome** in the search box and click Install. After installation, you may need to restart VS Code to take effect. + + ![debugger for chrome](coding-setup/debugger-for-chrome.png) + +Next, click **Developer -> VS Code Workflow -> Add Chrome Debug Configuration** in the top menu bar of Cocos Creator, this menu command will add a `.vscode/launch.json` file under the project folder as the configuration of the debugger: + + ![launch.json](coding-setup/launch-json.png) + +Third, click the **Debug** button in the left column in VS Code to open the debugging panel, and select `Cocos Creator Launch Chrome against localhost` in the debugging configuration at the top, and then click the green start button on the left to debug. + + ![debug chrome](coding-setup/debug-chrome.png) + +Debugging relies on the built-in Web server of the Cocos Creator editor, therefore debugging can only be carried out when the editor is started. If the port used by the editor to preview the game is not the default port, manually modify the `url` field in `launch.json` to add the correct port. + +During the debugging process, it is possible to directly set breakpoints on the source file for monitoring, which is a more convenient and friendly workflow than using Chrome's built-in DevTools to debug. + +## Learning how to use VS Code + +Visit the [VS Code official website documentation](https://code.visualstudio.com/Docs) to learn how to use it from editing function operations, personalization, syntax highlighting settings to plug-in extensions. diff --git a/versions/4.0/en/scripting/coding-setup/curl-download.png b/versions/4.0/en/scripting/coding-setup/curl-download.png new file mode 100644 index 0000000000..e56a6085c7 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/curl-download.png differ diff --git a/versions/4.0/en/scripting/coding-setup/debug-chrome.png b/versions/4.0/en/scripting/coding-setup/debug-chrome.png new file mode 100644 index 0000000000..550097ab9a Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/debug-chrome.png differ diff --git a/versions/4.0/en/scripting/coding-setup/debugger-for-chrome.png b/versions/4.0/en/scripting/coding-setup/debugger-for-chrome.png new file mode 100644 index 0000000000..814ff4d533 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/debugger-for-chrome.png differ diff --git a/versions/4.0/en/scripting/coding-setup/gear.png b/versions/4.0/en/scripting/coding-setup/gear.png new file mode 100644 index 0000000000..69a8056f44 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/gear.png differ diff --git a/versions/4.0/en/scripting/coding-setup/launch-json.png b/versions/4.0/en/scripting/coding-setup/launch-json.png new file mode 100644 index 0000000000..f3ea1b9ad0 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/launch-json.png differ diff --git a/versions/4.0/en/scripting/coding-setup/run-task.png b/versions/4.0/en/scripting/coding-setup/run-task.png new file mode 100644 index 0000000000..7fd0ed5d22 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/run-task.png differ diff --git a/versions/4.0/en/scripting/coding-setup/set-compile.png b/versions/4.0/en/scripting/coding-setup/set-compile.png new file mode 100644 index 0000000000..3098c72484 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/set-compile.png differ diff --git a/versions/4.0/en/scripting/coding-setup/task-compile.png b/versions/4.0/en/scripting/coding-setup/task-compile.png new file mode 100644 index 0000000000..66da8ea244 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/task-compile.png differ diff --git a/versions/4.0/en/scripting/coding-setup/tasks-json.png b/versions/4.0/en/scripting/coding-setup/tasks-json.png new file mode 100644 index 0000000000..52bc59aff6 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/tasks-json.png differ diff --git a/versions/4.0/en/scripting/coding-setup/vscode-edit.png b/versions/4.0/en/scripting/coding-setup/vscode-edit.png new file mode 100644 index 0000000000..04efa7f5db Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/vscode-edit.png differ diff --git a/versions/4.0/en/scripting/coding-setup/vscode-exclude.png b/versions/4.0/en/scripting/coding-setup/vscode-exclude.png new file mode 100644 index 0000000000..ffdca14360 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/vscode-exclude.png differ diff --git a/versions/4.0/en/scripting/coding-setup/vscode-setting.png b/versions/4.0/en/scripting/coding-setup/vscode-setting.png new file mode 100644 index 0000000000..0b061340c1 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/vscode-setting.png differ diff --git a/versions/4.0/en/scripting/coding-setup/vscode-workflow.png b/versions/4.0/en/scripting/coding-setup/vscode-workflow.png new file mode 100644 index 0000000000..de7c6bd4d0 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/vscode-workflow.png differ diff --git a/versions/4.0/en/scripting/coding-setup/vscode.png b/versions/4.0/en/scripting/coding-setup/vscode.png new file mode 100644 index 0000000000..562464f358 Binary files /dev/null and b/versions/4.0/en/scripting/coding-setup/vscode.png differ diff --git a/versions/4.0/en/scripting/component.md b/versions/4.0/en/scripting/component.md new file mode 100644 index 0000000000..4f4447afde --- /dev/null +++ b/versions/4.0/en/scripting/component.md @@ -0,0 +1,214 @@ +# Components and Component Execution Order + +All classes inherited from [Component](%__APIDOC__%/en/class/Component) are called __Component Classes__. The objects in a __Component Class__ are called __Components__. __Components__ are implement according to the __Cocos Creator__ __Entity Component (EC)__ system. + +The component class must inherit from a `cc` class. Example: + +```ts +import { Component } from 'cc'; + +@ccclass("MyComponent") +class MyComponent extends Component { + +} +``` + +## Component creation and destruction + +The life cycle of a component is completely controlled by the node. Unlike ordinary class objects, components cannot be created by constructors: + +```ts +const component = new MyComponent(); // Error: The component cannot be created by the constructor +``` + +In contrast, __components__ must be created by nodes and added to nodes as follows + +```ts +const myComponent = node.addComponent(MyComponent); +``` + +When the component is no longer needed, call the `node.removeComponent(myComponent)` method to remove the specified component and destroy it. Example: + +```ts +import { Component } from 'cc'; + +@ccclass("MyComponent") +class MyComponent extends Component { + constructor () { + console.log(this.node.name); // Error: The component is not attached to the node + } + + public printNodeName () { + console.log(this.node.name); + } +} +``` + +```ts +const myComponent = node.addComponent(MyComponent); +myComponent.printNodeName(); // Correct +node.removeComponent(myComponent); +myComponent.printNodeName(); // Error: The component is not attached to the node +``` + +## Component execution order + +### Use a unified control script to initialize other scripts + +Generally, developers will have a `Game.ts` script as the overall control script. If there are three components `Configuration.ts`, `GameData.ts`, and `Menu.ts`, then their initialization process would look like the following example: + +```ts +// Game.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +import { Configuration } from './Configuration'; +import { GameData } from './GameData'; +import { Menu }from './Menu'; + +@ccclass("Game") +export class Game extends Component { + private configuration = Configuration; + private gameData = GameData; + private menu = Menu; + + onLoad () { + this.configuration.init(); + this.gameData.init(); + this.menu.init(); + } +} +``` + +Among them, the `init` method needs to be implemented in `Configuration.ts`, `GameData.ts` and `Menu.ts`, and the initialization logic is put in it. In this way, it is guaranteed the initialization sequence of `Configuration`, `GameData` and `Menu`. + +### Use custom methods to control the update sequence in Update + +Similarly, it is necessary to ensure the update order of each frame of the above three scripts, we can also replace the `update` scattered in each script with our own defined method: + +```ts +// Configuration.ts + static updateConfig (deltaTime: number) { + + } +``` + +Then call these methods in the update of the `Game.ts` script: + +```ts +// Game.ts +update (deltaTime: number) { + this.configuration.updateConfig(deltaTime); + this.gameData.updateData(deltaTime); + this.menu.updateMenu(deltaTime); +} +``` + +### Control the execution order of components on the same node + +The execution order of component scripts on the same node can be controlled by the order of the components in the **Inspector** panel. The components arranged above will be executed before the components arranged below. We can adjust the arrangement order and execution order of the components through the `Move Up` and `Move Down` menus in the gear button at the upper right corner of the component. + +If there are two components: `CompA` and `CompB`, their contents may be similar to this example: + +```ts +// CompA.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("CompA") +export class CompA extends Component { + + onLoad () { + console.log('CompA onLoad!'); + } + + start () { + console.log('CompA start!'); + } + + update (deltaTime: number) { + console.log('CompA update!'); + } +} + + +// CompB.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("CompB") +export class CompB extends Component { + + onLoad () { + console.log('CompB onLoad!'); + } + + start () { + console.log('CompB start!'); + } + + update (deltaTime: number) { + console.log('CompB update!'); + } +} +``` + +When `CompA` is above `CompB` on the **Inspector** panel, the output may be this way: + +``` +CompA onLoad! +CompB onLoad! +CompA start! +CompB start! +CompA update! +CompB update! +``` + +After moving CompA under CompB in **Inspector** by `Move Down` in the upper right corner of the CompA component settings menu, the output may be this way: + +``` +CompB onLoad! +CompA onLoad! +CompB start! +CompA start! +CompB update! +CompA update! +``` + +### Set component execution priority + +If the above method still cannot provide the required control granularity, developers can also directly set the `executionOrder` of the component. `executionOrder` affects the execution priority of the component's life cycle callback. The smaller the `executionOrder`, the earlier the component will be executed relative to other components. The `executionOrder` defaults to `0`, so if it is set to a negative number, it will execute before other default components. Example: + +```ts +// Configuration.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, executionOrder } = _decorator; + +@ccclass("Configuration") +@executionOrder(-1) +export class Configuration extends Component { + + onLoad () { + console.log('Configuration onLoad!'); + } +} +``` + +```ts +// Menu.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, executionOrder } = _decorator; + +@ccclass("Menu") +@executionOrder(1) +export class Menu extends Component { + + onLoad () { + console.log('Menu onLoad!'); + } +} +``` + +By setting it as above, `Configuration.ts`'s `onLoad` will be executed before `Menu.ts`'s onLoad method. + +> **Note**: `executionOrder` is only valid for `onLoad`, `onEnable`, `start`, `update` and `lateUpdate`, but not valid for `onDisable` and `onDestroy`. diff --git a/versions/4.0/en/scripting/create-destroy.md b/versions/4.0/en/scripting/create-destroy.md new file mode 100644 index 0000000000..3870a369d2 --- /dev/null +++ b/versions/4.0/en/scripting/create-destroy.md @@ -0,0 +1,105 @@ +# Creating and Destroying Nodes + +## Creating a New Node + +In addition to creating nodes through the scene editor, it can also dynamically create nodes in scripts using `new Node()` and adding it to the scene. Example: + +```typescript +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + start(){ + let node =new Node('box'); + node.setPosition(0,0,-10); + } +} +``` + +## Cloning an Existing Node + +Sometimes it is needed to dynamically clone the existing nodes in the scene, it can be done through the `instantiate` method. Example: + +```typescript +import { _decorator, Component, Node,instantiate, director } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type:Node}) + private target: Node = null; + + start(){ + let scene = director.getScene(); + let node = instantiate(this.target); + + node.parent = scene; + node.setPosition(0, 0,-10); + } +} +``` + +## Creating a Prefab Node + +Similar to cloning an existing node, you can set a prefab ([Prefab](..\asset\prefab.md)) and generate a node through `instantiate`. Example: + +```typescript +import { _decorator, Component, Prefab, instantiate, director } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type:Prefab}) + private target: Prefab = null; + + start(){ + let scene = director.getScene(); + let node = instantiate(this.target); + + node.parent = scene; + node.setPosition(0,0,0); + } +} +``` + +## Destroy the node + +Through the `node.destroy()` function, nodes can be destroyed. It is worth mentioning that the destroyed node will not be removed immediately, but will be executed uniformly after the logic update of the current frame is completed. When a node is destroyed, the node is in an invalid state. Use `isValid` to determine whether the current node has been destroyed. Example: + +```typescript +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type:Node}) + private target: Node = null; + + private positionz: number = -20; + + start(){ + // Destroy the node after 5 seconds + setTimeout(function () { + this.target.destroy(); + }.bind(this), 5000); + } + update(deltaTime: number){ + console.info(this.target.isValid); + this.positionz += 1*deltaTime; + if (this.target.isValid) { + this.target.setPosition(0.0,0.0,this.positionz); + } + } +} +``` + +### The Difference Between `destroy` and `removeFromParent` + +After calling `removeFromParent` on a node, the node is not released from memory because the engine still holds its data internally. **If a node is no longer used, please call its `destroy` directly instead of `removeFromParent`, otherwise, a memory leak will result.** + +In short, if a node is no longer used, `destroy` is right, there is no need to `removeFromParent` nor to set `parent` to `null`. diff --git a/versions/4.0/en/scripting/decorator-group.png b/versions/4.0/en/scripting/decorator-group.png new file mode 100644 index 0000000000..90d2be2e2f Binary files /dev/null and b/versions/4.0/en/scripting/decorator-group.png differ diff --git a/versions/4.0/en/scripting/decorator-group2.png b/versions/4.0/en/scripting/decorator-group2.png new file mode 100644 index 0000000000..03af05d6a8 Binary files /dev/null and b/versions/4.0/en/scripting/decorator-group2.png differ diff --git a/versions/4.0/en/scripting/decorator.md b/versions/4.0/en/scripting/decorator.md new file mode 100644 index 0000000000..36390fed59 --- /dev/null +++ b/versions/4.0/en/scripting/decorator.md @@ -0,0 +1,447 @@ +# Decorator + +## `cc class` + +When applying a decorator `ccclass` to a class, such class is called a `cc class`. A `cc class` injects additional information to control Cocos Creator's serialization of the class object, the editor's presentation of the class object, etc. Therefore, component classes that do not have `ccclass` declared are incomplete, they cannot be added to nodes as components either. + +The parameter `name` of the decorator `ccclass` specifies the name of the `cc class`, which is **unique**, even same `ccclass` name in scripts in different sub folder is forbidden and considered as conflicts. When the corresponding `cc class` needs to be retrieved, it can be found by its `cc class` name, e.g.: + +- Serialization. If the object is a `cc class` object, the `cc class` name of the object will be recorded during serialization, and the corresponding `cc class` will be found for serialization based on this name during deserialization. + +- When the `cc class` is a component class, `Node` can look up the component by the `cc class` name of the component class. + +```ts +@ccclass('Example') +export class Example extends Component { +} +``` + +## Component class decorators + +These decorators are used to decorate sub classes of `Component`. + +### `executeInEditMode` + +By default, all components are executed only at runtime, meaning that their lifecycle callbacks are never triggered in editor mode. `executeInEditMode` allows the current component to be executed in editor mode, the default value of this decorator is `false`. + +```ts +const { ccclass, executeInEditMode } = _decorator; + +@ccclass('Example') +@executeInEditMode(true) +export class Example extends Component { + update (dt: number) { + // Will be executed in editor environment + } +} +``` + +### `requireComponent` + +The `requireComponent` decorator is used to specify a dependent component for the current component, the default value is `null`. When a component is added to a node, the engine will automatically add the dependent component to the same node if the dependent component does not exist yet, preventing script errors. This behavior is also valid at runtime. + +```ts +const { ccclass, requireComponent } = _decorator; + +@ccclass('Example') +@requireComponent(Sprite) +export class Example extends Component { +} +``` + +### `executionOrder` + +`executionOrder` is used to specify the execution priority of component lifecycle callbacks. The execution ordering is described as follows. + +- For different components on the same node, those with smaller `executionOrder` are executed first, and those with the same `executionOrder` are executed in the order in which they are added. +- For the same component on different nodes, the order of execution is determined by the node tree. + +This ordering setting is only valid for `onLoad`, `onEnable`, `start`, `update` and `lateUpdate`, but not for `onDisable` and `onDestroy`. + +```ts +const { ccclass, executionOrder } = _decorator; + +@ccclass('Example') +@executionOrder(3) +export class Example extends Component { +} +``` + +### `disallowMultiple` + +Only allow one component of the same type (with subclasses) to be added to the same node to prevent logic conflicts, the default value is false. + +```ts +const { ccclass, disallowMultiple } = _decorator; + +@ccclass('Example') +@disallowMultiple(true) +export class Example extends Component { +} +``` + +### `menu` + +`@menu(path)` is used to add the current component to the component menu to make it easier for the user to find it. + +Please note that this decorator generates a new menu in the drop-down box when the users click the **Add Component** button on the **Inspector** panel. + +```ts +const { ccclass, menu } = _decorator; + +@ccclass('Example') +@menu('foo/bar') +export class Example extends Component { +} +``` + +![menu](./menu.png) + +### `help` + +Specify the URL of the current component's help page. Once set, a help icon will appear in the **Inspector** panel and can be clicked to open the specified page. + +```ts +const { ccclass, help } = _decorator; + +@ccclass('Example') +@help('https://docs.cocos.com/creator/3.5/manual/en/scripting/decorator.html') +export class Example extends Component { +} +``` + +## Property decorator + +The decorator `property` is applied to a property or accessor of a `cc class`. Similar to the `ccclass` decorator, the `property` decorator injects additional information to control Cocos Creator's serialization of the property, the presentation of the property in the **Inspector** panel, and so on. + +The various features of the `property` decorator are specified via its attributes in `@property({})`. Usage of all attributes can be found in: [Property Attributes](./reference/attributes.md). + +The following code is an example of the usage of the `property` decorator: + +```ts +@property({ + type: Node, + visible: true, +}) +targetNode: Node | null = null; +``` + +Next, some of the important `property` attributes are listed below. + +### `type` attribute + +The `type` attribute specifies a type recognizable by the editor. The type can be specified with several forms of arguments. + +- Built-in primitive type: + + `CCInteger`, `CCFloat`, `CCBoolean`, and `CCString` are built-in property type identifiers that generally work on array properties. Non-array types usually do not need to explicitly declare a type. + + - `CCInteger` declares the type as **integer**. + - `CCFloat` declares the type as **floating point**. + - `CCString` declares the type as **String**. + - `CCBoolean` declares the type as **Boolean**. + +- Other `cc class` type + + All properties with `cc class` type **need to explicitly specify its type**, otherwise the editor will not be able to recognize the type and how to serialization the property. + +- Array type + + When use the built-in primitive type or the `cc class` type as array element type for an array property, the property can be declared using the array type. For example, `[CCInteger]`, `[Node]` will let the **Inspector** panel present the property as an array of integers and an array of nodes, respectively. + +If the property does not specify a type, Cocos Creator will derive its `cc` type from the property's default value or the result of an initialization formula: + +- If the type of the property is the JavaScript primitive types `number`, `string`, and `boolean`, then the property `type` is `CCFloat`, `CCString`, and `CCBoolean`, respectively. +- Under other circumstances, the property's type will be **undefined**, and the editor will prompt a `Type(Unknown)` notice in the **Inspector** panel. + +> **Note**: when using the JavaScript built-in constructors `Number`, `String`, `Boolean` as `type`, there will be a warning and they will be treated as `CCFloat`, `CCString`, `CCBoolean` respectively. Modifying the type of an initialized array property, a manual clear of the original array data is necessary, otherwise the data type will be inconsistent and lead to data mismatch. +> +> ![property-changed](property-changed.png) + + + +> **Note**: editable properties that need to be presented in the **Inspector** panel should not have `_` at the beginning of the property name, otherwise they will be recognized as private properties, and private properties will not be displayed in the component **Inspector** panel. + +The following code demonstrates the declaration of properties for different `cc` types. + +```ts +import { _decorator, CCInteger, Node, Enum } from 'cc'; +const { ccclass, property, integer, float, type } = _decorator; + +enum A { + c, + d +} +Enum(A); + +@ccclass +class MyClass { + @property // JavaScript primitive type, automatically recognized as Creator's floating-point type by default. + index = 0; + + @property(Node) // Declare property cc to be of type Node, which is equivalent to @property({type: Node}) when the property parameter is only type. + targetNode: Node | null = null; // Equivalent to targetNode: Node = null! + + // Declare the cc type of the property children to be a Node array + @property({ + type: [Node] + }) + children: Node[] = []; + + @property({ + type: String, + }) // Warning: the constructor String should not be used. equivalent to CCString. or you can choose not to declare the type + text = ''; + + @property + children2 = []; // Undeclared cc type, inferring from initialization result that the elements are undefined arrays + + @property + _valueB = 'abc'; // Properties starting with '_' here are only serialized and will not be displayed in the editor properties panel + + @property({ type: A }) + accx : A = A.c; +} +``` + +For convenience, several additional decorators are provided to quickly declare `cc` types. If you only need to declare `type` attribute of the property, you can use the following decorators instead of `@property`: + +| Decorators | Corresponding property writes | +| :-------- | :---------------- | +| @type(t) | @property(t) | +| @integer | @property(CCInteger) | +| @float | @property(CCFloat) | + +```ts +import { _decorator, CCInteger, Node } from 'cc'; +const { ccclass, property, integer, float, type } = _decorator; +@ccclass +class MyClass { + @integer // declare the cc type of the property as an integer + index = 0; + + @type([Node]) // Declare the cc type of the property children to be a Node number + children: Node[] = []; + + @type(String) // Warning: should not use constructor String. equivalent to CCString. can also choose not to declare type + text = ''; + // JavaScript primitive types `number`, `string`, `boolean` can usually be undeclared + // You can just write + @property + text = ''; +} +``` + + + + + + + +### `visible` attribute + +In general, whether an property is displayed in the **Inspector** panel depends on whether the property name starts with `_`. **If it starts with `_`, it is not displayed**. + +To force display in the **Inspector** panel, set the `visible` attribute to `true`: + +```typescript +@property({ visible: true }) +private _num = 0; +``` + +To force hiding, set the `visible` attribute to `false`: + +```typescript +@property({ visible: false }) +num = 0; +``` + +#### dynamic visibility +To set visibility dynamically, you must pass a function to it with the `this` argument and return a boolean: + +```typescript +@ccclass('DynamicVisibilityExample') +export class DynamicVisibilityExample extends Component +{ + @property(CCBoolean) + private isNumberAttributeVisible: boolean = true; + + @property({visible(this: DynamicVisibilityExample){return this.isNumberAttributeVisible;}, type: CCInteger}) + private myNumber: number = 500; +} +``` + +- When false ![Image showing before ticking the visibility](dynamic-visibility-0.png) +- When true ![Image showing after ticking the visibility](dynamic-visibility-1.png) + +### `serializable` attribute + +Properties are serialized by default. Once serialized, the property values set in the editor will be saved to the scene and other resource files, and will be automatically restored to the set property values when the scene is loaded. To not serialize, set `serializable` as false. + +```typescript +@property({ serializable: false }) +num = 0; +``` + +### `override` attribute + +All properties are inherited by subclasses. If a subclass wants to override a property of the same name of the parent class, the override attribute needs to be set explicitly, otherwise, there will be a renaming warning: + +```typescript +@property({ tooltip: "my id", override: true }) +id = ""; +``` + +### `group` attribute + +If there are many properties or mixed properties defined in the script, the properties can be grouped and sorted by `group` for easy management. It also supports sorting properties within a group. + +- `@property({ group: { name } })` + +- `@property({ group: { id, name, displayOrder, style } })` + +| Property | Description | +| :--- | :--- | +| `id` | Group ID, `string` type, is a unique identifier for the property group, and defaults to `default`. | +| `name` | The name to classify the properties in the group, `string` type. | +| `displayOrder` | Sort the groups, `number` type. The smaller the number, the higher the sorting. The default is `Infinity`, which means the group is sorted last.
If there are multiple groups without `displayOrder` set, they will be sorted in the order declared in the script. | +| `style` | Grouping styles, currently **tab** and **section** styles are supported. The default value is **tab**. | + +Example script is as follows: + +```ts +import { _decorator, Component, Label, Sprite } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('SayHello') +export class SayHello extends Component { + + // Group 1 + // The property category named "bar" within the group, which contains a Label property named "label". + @property({ group: { name: 'bar' }, type: Label }) + label: Label = null!; + // The property category named "foo" within the group, which contains a Sprite property named "sprite". + @property({ group: { name: 'foo' }, type: Sprite }) + sprite: Sprite = null!; + + // Group 2 + // The property category named "bar2" within the group, which contains a Label property named "label2" and a Sprite property named "sprite2". + @property({ group: { name: 'bar2', id: '2', displayOrder: 1 }, type: Label }) + label2: Label = null!; + @property({ group: { name: 'bar2', id: '2' }, type: Sprite }) + sprite2: Sprite = null!; + +} +``` + +Mounting the script to the node displays the following image in the **Inspector** panel: + +![decorator-group](decorator-group.png) + +Because group 1 does not specify `displayOrder` and group 2 specifies `displayOrder` as `1`, group 2 will be ranked ahead of group 1. + +Sorting the properties within a group can also be done via `displayOrder`. Taking group 2 as an example, it is currently sorted in the order defined in the script, with label2 in front of sprite2. Let's adjust it to: + +```ts +// Group 2 +@property({ group: { name: 'bar2', id: '2', displayOrder: 1 }, displayOrder: 2, type: Label }) +label2: Label = null!; +@property({ group: { name: 'bar2', id: '2' }, displayOrder: 1, type: Sprite }) +sprite2: Sprite = null!; +``` + +Back to the editor, the sprite2 is now in front of label2 in the **Inspector** panel: + +![decorator-group](decorator-group2.png) + +For additional information about the properties, please refer to the [Properties](./reference/attributes.md) documentation. + +## Built-in Type User Interface + +For some common types, the engine supports a default user interface that can be use via the developers' needs. + +- Color + + ![color](./decorator/color.png) + + The code example is as follows: + + ```ts + @property(Color) + color:Color + ``` + +- Curve: Used to save the type of curve, sample, and sample data, and the result can be evaluated. + + ![color](./decorator/curve.png) + + The code example is as follows: + + ```ts + @property(RealCurve) + realCurve:RealCurve = new RealCurve(); + ``` + +- CurveRange: Use constants, curves, two-curves, or two-constants to control the curve-range + + ![color](./decorator/curve_range.png) + + The code example is as follows: + + ```ts + @property(CurveRange) + curveRang : CurveRange = new CurveRange(); + ``` + +- Gradient: You can record the key values of the gradient and evaluate the gradient result + + ![graduebt](./decorator/gradient.png) + + The code example is as follows: + + ```ts + @property(Gradient) + gradient = new Gradient(); + ``` + +- GradientRange: use color, gradient, two gradients, two colors or random color to evaluate the final color + + ![graduebt](./decorator/gradient_range.png) + + The code example is as follows: + + ```ts + @property(GradientRange) + gradientRange:GradientRange = new GradientRange(); + ``` + + +## Reference Link + +- [Property Attributes](./reference/attributes.md) +- [Advanced Scripting](./reference-class.md) diff --git a/versions/4.0/en/scripting/decorator/color.png b/versions/4.0/en/scripting/decorator/color.png new file mode 100644 index 0000000000..7687ae15b1 Binary files /dev/null and b/versions/4.0/en/scripting/decorator/color.png differ diff --git a/versions/4.0/en/scripting/decorator/curve.png b/versions/4.0/en/scripting/decorator/curve.png new file mode 100644 index 0000000000..c1eeecff50 Binary files /dev/null and b/versions/4.0/en/scripting/decorator/curve.png differ diff --git a/versions/4.0/en/scripting/decorator/curve_range.png b/versions/4.0/en/scripting/decorator/curve_range.png new file mode 100644 index 0000000000..caf7505f02 Binary files /dev/null and b/versions/4.0/en/scripting/decorator/curve_range.png differ diff --git a/versions/4.0/en/scripting/decorator/gradient.png b/versions/4.0/en/scripting/decorator/gradient.png new file mode 100644 index 0000000000..46081d87a0 Binary files /dev/null and b/versions/4.0/en/scripting/decorator/gradient.png differ diff --git a/versions/4.0/en/scripting/decorator/gradient_range.png b/versions/4.0/en/scripting/decorator/gradient_range.png new file mode 100644 index 0000000000..07b3ee2320 Binary files /dev/null and b/versions/4.0/en/scripting/decorator/gradient_range.png differ diff --git a/versions/4.0/en/scripting/dynamic-visibility-0.png b/versions/4.0/en/scripting/dynamic-visibility-0.png new file mode 100644 index 0000000000..2afc46dd33 Binary files /dev/null and b/versions/4.0/en/scripting/dynamic-visibility-0.png differ diff --git a/versions/4.0/en/scripting/dynamic-visibility-1.png b/versions/4.0/en/scripting/dynamic-visibility-1.png new file mode 100644 index 0000000000..6a956ded60 Binary files /dev/null and b/versions/4.0/en/scripting/dynamic-visibility-1.png differ diff --git a/versions/4.0/en/scripting/external-scripts.md b/versions/4.0/en/scripting/external-scripts.md new file mode 100644 index 0000000000..0933bcb9ff --- /dev/null +++ b/versions/4.0/en/scripting/external-scripts.md @@ -0,0 +1,79 @@ +# External code support + +> **Note**: as of v3.0, it is recommended to use the [module](modules/index.md) instead of the plugin script! + +## Plugin Scripts + +When a script resource is imported into the **Assets** panel and **Import as Plugin** is set in the **Inspector** panel, the script resource is called a **Plugin Script**. Plugin scripts are usually used to introduce third-party libraries. Currently, only JavaScript plugin scripts are supported. + +![import as plugin](plugin-scripts/import-as-plugin.png) + +Unlike other scripts in the project, Cocos Creator will not modify the content of the plug-in script, but some code may be inserted to adapt to Creator itself. In particular, Cocos Creator will shield the global variables `module`, `exports`, `define`. + +### Import Options + +Many third-party JavaScript libraries provide library functions in the form of global variables. These libraries often write global variables `window`, `global`, `self` and `this`. + +However, these global variables are not necessarily cross-platform. For convenience, when importing plug-in scripts, the option **GlobalThis Alias** is provided. After opening, **Cocos Creator** will insert the necessary code to simulate these global variables. Example: + +```js +(function() { + const window = globalThis; + const global = globalThis; + const self = globalThis; + + (function() { + /* Original code */ + }).call(this); + +}).call(this); +``` + +Common global variables will be simulated by default. For special global variable aliases, you can add the corresponding variable name in the input box. + +### Execution Timing + +#### Execution environment + +Developers can control whether plug-in scripts are executed in certain environments. + +| Options | Affected Platform | Remarks | +| :--------- | :---------- | :---------- | +| **Load In Web** | Browser, Web page preview, Editor | Enabled by default, when disabled, it will be disabled with **Allow editor to load** | +| **Load In Editor** | Editor | Disabled by default. If other common scripts in the editor depend on the current script during the loading process, you need to manually enable this option.
After opening, local variables that are not declared in any function in the script will not be exposed as global variables, so global variables need to be defined with `window.abc = 0` to take effect. | +| **Load In Native** | Native platform, emulator preview | Enabled by default | +| **Load In MiniGame** | Minigame platform | Enabled by default. | + +#### Order of execution + +Plugin scripts will by default be executed after the engine starts and before the project scripts are loaded. The order between plugin scripts is sorted by default according to the naming of the plugin scripts themselves. Since 3.8.3, you can specify the priority order of the plugin scripts in the project settings. The scripts with the specified priority order will be reordered to meet the specified order after the default ordering. + +> The modification interactions for specific project settings can be found in the [Project Settings Documentation](../editor/project/index.md) + +![sort plugin script](plugin-scripts/sort-plugin.png) + +For example, suppose there are plugin scripts with names: `1,2,3,4,5,6,7,8`, the default sort order will be from 1 - 8, if you specify the priority order of some of the scripts in the project settings: `8,3,1,7,5`, then the final sort result will be: `8,3,1,2,4,7,5,6`. + +### Usability and Cross-Platform + +The plug-in script is copied to the build directory almost intact, so the usability and cross-platform of the plug-in script are not guaranteed by **Cocos Creator**. For example, when plug-in scripts use language features that are not supported by certain platforms, errors will result, especially: + +- **The target platform does not provide native node.js support** + + For example, many [npm](https://www.npmjs.com/) modules directly or indirectly depend on node.js, so they cannot be used after being published to native or web platforms. + +- **Plugins that rely on the DOM API will not be able to publish to the native platform** + + A large number of front-end plug-ins can be used in web pages, such as jQuery, but they may depend on the browser's DOM API. Plugins that rely on these APIs cannot be used on the native platform. + +### Interaction + +Plug-in scripts and non-plug-in scripts cannot interact in the form of import. For example, even if the developer knows that their target platform actually supports CommonJS, nor can it be used in a non-plug-in script forcibly through the relative path of `require`. + +Therefore, plug-in scripts generally communicate in the form of global variables (also known as IIFE module format), but the following points need to be noted: + +- Developers should use global variables very carefully. When you want to use global variables, you should be very clear about what you are doing. We do not recommend abusing global variables. Even if you want to use them, it is best to ensure that global variables are read only. + +- When adding global variables, please be careful not to have the same name with the existing global variables in the system. + +- Developers can freely encapsulate or extend the **Cocos Creator** engine in the plug-in script, but this will increase the cost of team communication and make the script difficult to reuse. diff --git a/versions/4.0/en/scripting/index.md b/versions/4.0/en/scripting/index.md new file mode 100644 index 0000000000..4d2e4c2a37 --- /dev/null +++ b/versions/4.0/en/scripting/index.md @@ -0,0 +1,21 @@ +# Scripting + +Scripting in Cocos Creator is used to implement user-defined (game) behavior and supports both **JavaScript** and **TypeScript** programming languages. Objects in the scene are driven by writing a scripting component and mounting it to a scene node. + +During the component scripting process, developers can declare properties to map the variables that need to be adjusted in the script to the **Inspector** panel for game designers and artists to make adjustments. At the same time, it is also possible to register specific callback functions to help initialize, update or even destroy nodes. + +## Content + +- [Programming Language Support](./language-support.md) +- [Scripting Basics](./script-basics.md) +- [Using Scripts](./usage.md) +- [Advanced Scripting](./reference-class.md) +- [Event System](../engine/event/index.md) +- [Modules](./modules/index.md) +- [Plugin Scripts](./external-scripts.md) +- [ReadOnly](./readonly.md) + +## More Reference + +- [Adding Logging within the Engine](./log.md) +- [Recommended Coding Standards](./reference/coding-standards.md) diff --git a/versions/4.0/en/scripting/language-support.md b/versions/4.0/en/scripting/language-support.md new file mode 100644 index 0000000000..9410321ff4 --- /dev/null +++ b/versions/4.0/en/scripting/language-support.md @@ -0,0 +1,121 @@ +# Programming Language Support + +Cocos Creator supports both **TypeScript** and **JavaScript** programming languages. Note that JavaScript can only be imported as a [plugin script](external-scripts.md) to use. + +## TypeScript + +Cocos Creator supports TypeScript 4.1.0. The following restrictions are based on TypeScript 4.1.0: + +- `tsconfig.json` will not be read. The following options are implied for each project: + + ```json5 + { + "compilerOptions": { + "target": "ES2015", + "module": "ES2015", + "isolatedModules": true, + "experimentalDecorators": true, + "moduleResolution": /* Cocos Creator's specific module resolution algorithm */, + "forceConsistentCasingInFileNames": true, + }, + } + ``` + + The implicit `isolatedModules` option means that: + - [const enums](https://www.typescriptlang.org/docs/handbook/enums.html#const-enums) is not supported. + + - Use `export type` when re-exporting TypeScript types and interfaces. For example, use `export type { Foo } from '. /foo';` instead of `export { Foo } from '. /foo';`. + +- `export =` and `import =` are not supported. + +- Variables derived from namespace must be declared as `const`, not `var` or `let`. + +- Different declarations in the same namespace do not share scope and require explicit use of qualifiers. + +- Type errors during compilation will be ignored. + +`tsconfig.json` is not read at compile time, meaning that the compile option for `tsconfig.json` does not affect compilation. + +Developers can still use `tsconfig.json` in their projects to work with the IDE to implement features such as type checking. In order to make the IDE's TypeScript checking compatible with the behavior of Creator, developers need to pay some extra attention to [tsconfig](./tsconfig.md). + +### TypeScript Reference Tutorial + +- [Tutorial: v3.0 TypeScript question answering and experience sharing](https://discuss.cocos2d-x.org/t/tutorial-3-0-typescript-question-answering-and-experience-sharing/52932) +- [TypeScript Official Website](https://www.typescriptlang.org/) +- [TypeScript - Classes](https://www.typescriptlang.org/docs/handbook/classes.html) +- [TypeScript - Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) +- [TypeScript - DefinitelyTyped](http://definitelytyped.org/) +- [Learn TypeScript in Y minutes [cn]](https://learnxinyminutes.com/docs/zh-cn/typescript-cn/) +- [TypeScript GitHub](https://github.com/Microsoft/TypeScript) +- [The Best Resources For Learning TypeScript for Game Development](https://www.cocos.com/en/the-best-resources-for-learning-typescript-for-game-development) +- [3 Excuses Developers Give To Avoid TypeScript — and the Better Reasons They Should Use It](https://betterprogramming.pub/the-bad-reasons-people-avoid-typescript-and-the-better-reasons-why-they-shouldnt-86f8d98534de) + +## JavaScript + +### Language Features + +The JavaScript language specification supported by Creator is ES6. + +In addition, the following language features or proposals, updated to the ES6 specification, are still supported. + +- [Public class fields](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields) +- [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +- [Optional chaining operator `?.`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) +- [Nullish coalescing operator `??`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) +- Logical assignment operators + - [Logical nullish assignment operator `??=`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_nullish_assignment) + - [Logical AND assignment operator `&&=`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND_assignment) + - [Logical OR assignment operator `||=`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR_assignment) +- [Global object `globalThis`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis) + +The following language features are also supported, but require the relevant compilation options to be turned on: + +- [async functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) + +In particular, Creator currently supports **Legacy** decorator proposals, see [babel-plugin-proposal-decorators](https://babeljs.io/docs/en/babel-plugin-proposal-decorators) for their usage and meaning. Since this [proposal](https://github.com/tc39/proposal-decorators) is still in phase 2, all decorator-related functional interfaces exposed by the engine are under the `_decorator` namespace starting with an underscore. + +### Compilation Options + +Creator opens some compilation options that will be applied to the entire project. + +| Option | Name | Meaning | +| :-- | :--- | :-- | +| **useDefineForClassFields** | Conforming class fields | When enabled, class fields will be implemented using the `Define` semantics, otherwise they will be implemented using the `Set` semantics. Only works if the target does not support ES6 class fields. | +| **allowDeclareFields** |Allows declaring class fields| When enabled, the `declare` keyword will be allowed in TypeScript scripts to declare class fields and, when the field is not declared with `declare` and no explicit initialization is specified, it will be initialized according to the specification to `undefined`. |The + +### Runtime Environment + +From the user's perspective, Creator does not bind any JavaScript implementation, so it is recommended that developers write scripts strictly according to the JavaScript specification for better cross-platform support. + +For example, when wishing to use **global objects**, the standard feature `globalThis` should be used: + +```js +globalThis.blahBlah // 'globalThis' must exist in any environment +``` + +instead of `window`, `global`, `self` or `this`: + +```js +typeof window // May be 'undefined' +typeof global // May be 'undefined' in the browser environment +``` + +Again, Creator does not provide a module system for **CommonJS**, so the following code snippet would pose a problem: + +```js +const blah = require('./blah-blah'); // Error, require is undefined +module.exports = blah; // Error, module is undefined +``` + +Instead, the standard module syntax should be used: + +```js +import blah from './blah-blah'; +export default blah; +``` + +### JavaScript Related Tutorials + +- [JavaScript Standard Reference Tutorial [cn]](https://wangdoc.com/javascript/) +- [JavaScript Garden](https://bonsaiden.github.io/JavaScript-Garden/) +- [JavaScript Memory Detailing & Analysis Guide [cn]](https://mp.weixin.qq.com/s/EuJzQajlU8rpZprWkXbJVg) diff --git a/versions/4.0/en/scripting/life-cycle-callbacks.md b/versions/4.0/en/scripting/life-cycle-callbacks.md new file mode 100644 index 0000000000..03009bc4f6 --- /dev/null +++ b/versions/4.0/en/scripting/life-cycle-callbacks.md @@ -0,0 +1,111 @@ + +# Life Cycle Callbacks + +__Cocos Creator__ provides life cycle callback functions for component scripts. As long as the user defines a specific callback function, __Cocos Creator__ will automatically execute related scripts in a specific period, and the user does not need to call them manually. + +The life cycle callback functions currently provided to users mainly include (order by life cycle trigger): + +- `onLoad()` +- `onEnable()` +- `start()` +- `update()` +- `lateUpdate()` +- `onDisable()` +- `onDestroy()` + +## `onLoad()` + +In the initialization phase of the component script, the `onLoad()` callback function is available. The `onLoad()` callback will be triggered when the node is activated for the first time, such as when the scene is loaded or the node is activated. In the `onLoad()` stage, it is guaranteed that you can get other nodes in the scene and the resource data associated with the nodes. `onLoad()` will always be executed before any start method is called, which can be used to arrange the initialization sequence of the script. Usually, some initialization related operations are performed in the `onLoad()` stage. Example: + +```ts +import { _decorator, Component, Node, SpriteFrame, find } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + @property({type:SpriteFrame}) + bulletSprite=null; + @property({type:Node}) + gun=null; + + _bulletRect=null; + + onLoad(){ + this._bulletRect=this.bulletSprite.getRect(); + this.gun = find('hand/weapon', this.node); + } +} +``` + +## `onEnable()` + +When the `enabled` property of the component changes from `false` to `true`, or the node's `active` property changes from `false` to `true`, the `onEnable()` callback will be activated. If the node is created for the first time and `enabled` is `true`, it will be called after `onLoad()` but before `start()`. + +## `start()` + +The `start()` callback function will be triggered before the first activation of the component, that is, before the first execution of `update()`. `start()` is usually used to initialize some intermediate state data. These data may change during update and are frequently enabled and disabled. Example: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("starttest") +export class starttest extends Component { + + private _timer: number = 0.0; + + start () { + this._timer = 1.0; + } + + update (deltaTime: number) { + this._timer += deltaTime; + if(this._timer >= 10.0){ + console.log('I am done!'); + this.enabled = false; + } + } +} +``` + +## `update()` + +A key point of game development is to update the behavior, state and orientation of objects before each frame of rendering. These update operations are usually placed in the `update()` callback. Example: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("updatetest") +export class updatetest extends Component { + + update (deltaTime: number) { + this.node.setPosition(0.0,40.0*deltaTime,0.0); + } +} +``` + +## `lateUpdate()` + +`update()` will be executed before all animations are updated, but if developer's need to perform some additional operations after the animations (such as animation, particles, physics, etc.) are updated, or it is needed to execute the `update()` of all components after doing other operations, use the `lateUpdate()` callback. Example: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("lateupdatetest") +export class lateupdatetest extends Component { + + lateUpdate (deltaTime: number) { + this.node.setPosition(0.0,50,0.0); + } +} +``` + +## `onDisable()` + +When the `enabled` property of the component changes from `true` to `false`, or the node's `active` property changes from `true` to `false`, the `onDisable()` callback will be activated. + +## `onDestroy()` + +When the component or the node where it calls `destroy()`, the `onDestroy()` callback will be called, and the component will be recycled when the frame ends. diff --git a/versions/4.0/en/scripting/load-assets.md b/versions/4.0/en/scripting/load-assets.md new file mode 100644 index 0000000000..b0fa78fda9 --- /dev/null +++ b/versions/4.0/en/scripting/load-assets.md @@ -0,0 +1,78 @@ +# Obtaining and Loading Assets + +__Cocos Creator__ uses the same asset management mechanism as __Cocos Creator__. In this section, we will introduce: + + - Declaration of asset attributes + - How to set assets in the **Inspector** panel + - Loading assets dynamically + - Loading remote assets and device assets + - Dependence and releasing assets + +## Declaration of asset attributes + +In __Cocos Creator__, all types that inherit from `Asset` are collectively called __assets__, such as` Texture2D`, `SpriteFrame`,` AnimationClip`, `Prefab`, etc. Loading assets is unified and interdependent assets can be automatically preloaded. + +> For example: when the engine is loading the scene, it will automatically load the assets associated with the scene first. If these assets are associated with other assets, the other will be loaded first, and the scene loading will end after all loading is completed. + +You can define an __Asset__ property in the script like this: + +```typescript +//test.ts + +import { _decorator, Component, Node, SpriteFrame } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type: SpriteFrame}) + private spriteFrame: SpriteFrame = null; +} +``` + +## How to set Assets in the Inspector panel + +As long as the type is defined in the script, you can easily set the __asset__ directly in the __Inspector__ panel. Suppose, we create a script like this: + +```typescript +//test.ts +import { _decorator, Component, Node, SpriteFrame, Texture2D } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type: Texture2D}) + private texture: Texture2D = null; + + @property({type: SpriteFrame}) + private spriteFrame: SpriteFrame = null; +} +``` + +After adding it to a node, it looks like this in the **Inspector** panel: + +![asset-in-properties-null](load-assets/asset-in-inspector-null.png) + +__Next__, we drag a __Texture__ and a __SpriteFrame__ from the **Assets** panel into the corresponding properties of the **Inspector** panel: + +![asset-in-properties-dnd](load-assets/asset-in-inspector-dnd.png) + +Resulting in: + +![asset-in-properties-dnd](load-assets/asset-in-inspector.png) + +This allows you to get and set assets directly within a script: + +```typescript + start () { + let spriteFrame = this.spriteFrame; + let texture = this.texture; + } +``` + +Although it is intuitive to set assets in the **Attributes Inspector**, the assets can only be set in the __Scene__, in advance. This means there is no way to switch assets dynamically. If you need to switch assets dynamically, take a look at this next section on __Dynamic loading__. + +## Dynamic loading + +For dynamic loading, please refer to [Dynamic Load Resources](../asset/dynamic-load-resources.md) diff --git a/versions/4.0/en/scripting/load-assets/asset-dep.png b/versions/4.0/en/scripting/load-assets/asset-dep.png new file mode 100644 index 0000000000..94dd9241b9 Binary files /dev/null and b/versions/4.0/en/scripting/load-assets/asset-dep.png differ diff --git a/versions/4.0/en/scripting/load-assets/asset-in-inspector 3.png b/versions/4.0/en/scripting/load-assets/asset-in-inspector 3.png new file mode 100644 index 0000000000..e646cfd0cc Binary files /dev/null and b/versions/4.0/en/scripting/load-assets/asset-in-inspector 3.png differ diff --git a/versions/4.0/en/scripting/load-assets/asset-in-inspector-dnd 3.png b/versions/4.0/en/scripting/load-assets/asset-in-inspector-dnd 3.png new file mode 100644 index 0000000000..fd5a3de39c Binary files /dev/null and b/versions/4.0/en/scripting/load-assets/asset-in-inspector-dnd 3.png differ diff --git a/versions/4.0/en/scripting/load-assets/asset-in-inspector-dnd.png b/versions/4.0/en/scripting/load-assets/asset-in-inspector-dnd.png new file mode 100644 index 0000000000..fd5a3de39c Binary files /dev/null and b/versions/4.0/en/scripting/load-assets/asset-in-inspector-dnd.png differ diff --git a/versions/4.0/en/scripting/load-assets/asset-in-inspector-null 3.png b/versions/4.0/en/scripting/load-assets/asset-in-inspector-null 3.png new file mode 100644 index 0000000000..33e18309b2 Binary files /dev/null and b/versions/4.0/en/scripting/load-assets/asset-in-inspector-null 3.png differ diff --git a/versions/4.0/en/scripting/load-assets/asset-in-inspector-null.png b/versions/4.0/en/scripting/load-assets/asset-in-inspector-null.png new file mode 100644 index 0000000000..33e18309b2 Binary files /dev/null and b/versions/4.0/en/scripting/load-assets/asset-in-inspector-null.png differ diff --git a/versions/4.0/en/scripting/load-assets/asset-in-inspector.png b/versions/4.0/en/scripting/load-assets/asset-in-inspector.png new file mode 100644 index 0000000000..e646cfd0cc Binary files /dev/null and b/versions/4.0/en/scripting/load-assets/asset-in-inspector.png differ diff --git a/versions/4.0/en/scripting/load-assets/resources-file-tree 3.png b/versions/4.0/en/scripting/load-assets/resources-file-tree 3.png new file mode 100644 index 0000000000..d44fcfdfe0 Binary files /dev/null and b/versions/4.0/en/scripting/load-assets/resources-file-tree 3.png differ diff --git a/versions/4.0/en/scripting/log.md b/versions/4.0/en/scripting/log.md new file mode 100644 index 0000000000..b29af53229 --- /dev/null +++ b/versions/4.0/en/scripting/log.md @@ -0,0 +1,51 @@ +# Adding Logging within the Engine + +This document mainly explains how to add new Log messages (including **log**, **warning**, and **error**) to the internal code of the engine according to the correct specifications. + +## Log Information Mechanism and Background + +Currently, the **Log** information in Cocos Creator is stored in the form of an error message table independent of the engine, which is stored in the `EngineErrorMap.md` under the engine directory. In the engine code, it is not allowed to write logs, warnings, errors and other information directly in the form of strings. It must be written in the following three APIs: + +``` +import {logID, warnID, errorID} from'core/platform/debug'; + +logID(id, ...params); +warnID(id, ...params); +errorID(id, ...params); +``` + +The main purpose of this is to reduce the package body occupied by the string in the engine source code. + +## EngineErrorMap writing specifications + +`EngineErrorMap` is divided into large modules according to one hundred bits, a total of four bits, from `0000` to `9900`, which means that it supports up to `100` large modules. The tens digit is used to divide the sub-modules, or it can be arranged in a continuous form, which is determined by the person in charge of the module. + +Due to historical reasons, there is currently no strict priority order for sorting, and newly-built modules can simply be postponed. In the future, we will do better sorting management and sorting. + +The specifications for writing specific error messages are as follows: + +``` +### 4 number ID + +Message in english. +``` + +Example: + +``` +### 8300 + +Should only one camera exists, please check your project. +``` + +The information supports the use of parameter receivers such as `%s`, `%d`, and `%f`. When running the output LOG, it will be spliced ​​into the information in the order of the parameters. + +## Maintenance of EngineErrorMap + +After `EngineErrorMap` is modified, if you want the call in the code to take effect, you need to execute it in the engine directory + +```sh +> gulp build-debug-infos +``` + +Changes to `EngineErrorMap.md` should also be submitted to git following other changes to the engine. diff --git a/versions/4.0/en/scripting/menu.png b/versions/4.0/en/scripting/menu.png new file mode 100644 index 0000000000..636b0a6329 Binary files /dev/null and b/versions/4.0/en/scripting/menu.png differ diff --git a/versions/4.0/en/scripting/modules/config.md b/versions/4.0/en/scripting/modules/config.md new file mode 100644 index 0000000000..4402fb0279 --- /dev/null +++ b/versions/4.0/en/scripting/modules/config.md @@ -0,0 +1,75 @@ +# How to Get npm Packages + +## Installing Node.js + +The npm package management tool `npm` is included in the Node.js distribution. It is ready to use after installing Node.js. + +To confirm if the installation of `npm` was successful issue the following command: + +```bash +> npm -v +# Possible output. +# 6.14.9 +``` + +## Installing npm packages + +To install an `npm` package, execute the following command in the project root directory: + +```bash +> npm install --save protobufjs +``` + +The above command will install the `npm` package `protobufjs` into the `/node_modules` directory and write the dependencies for this package to the file `package.json`. + +The `package.json` file is an `npm` manifest file and needs to be included in version control. + +> **Note**: Cocos Creator recommends that the automatically generated `package-lock.json` be included in version control to ensure that the same version of the package is installed between multiple developers. + +The `/node_modules` directory is generally not included in version control. + +Once the dependencies have been written into the `package.json`, the following commands can be executed directly to reinstall or install in other environments: + +```bash +> npm install +``` + +## Expansion: Using npm mirrors + +`npm` reads and downloads packages from the [official npmjs source](https://www.npmjs.com/) by default. Some countries or regions may have network issues that cause the installation to fail or install too slowly, it is recommended to fix this by switching mirrors. + +First, install the `npm` package [nrm](https://www.npmjs.com/package/nrm) globally: + +```bash +> npm install -g nrm +``` + +> **Note**: `-g` means global, the npm package will be installed directly on the current computer. Once it is done, there is no need to do it again. + +To view valid npm images: + +```bash +> npx nrm ls +# Possible output. +# * npm -------- https://registry.npmjs.org/ +# yarn ------- https://registry.yarnpkg.com/ +# cnpm ------- http://r.cnpmjs.org/ +# taobao ----- https://registry.npm.taobao.org/ +# nj --------- https://registry.nodejitsu.com/ +# npmMirror -- https://skimdb.npmjs.com/registry/ +# edunpm ----- http://registry.enpmjs.org/ +``` + +Choose the appropriate mirror according to the current region. The `taobao` mirror is a good choice for users in mainland China. Execute the following command to switch mirrors. + +```bash +> npx nrm use taobao # or any suitable mirror +``` + +> **Note**: this command is also global. Optionally, to switch only the mirror of the current project, see the [nrm options](https://www.npmjs.com/package/nrm#usage) documentation. + +The mirror name `npm` is the name of the official source, and can be switched back to the official source with the following command: + +```bash +> npx nrm use npm +``` diff --git a/versions/4.0/en/scripting/modules/engine.md b/versions/4.0/en/scripting/modules/engine.md new file mode 100644 index 0000000000..87dd6ded7d --- /dev/null +++ b/versions/4.0/en/scripting/modules/engine.md @@ -0,0 +1,82 @@ +# Engine Modules + +The engine expose functional interfaces to developers via modules, which exist as **ECMAScript** modules. + +> **Note**: from v3.0, it is strongly recommended not to use the global variable `cc` to access engine modules or classes. + +## Function + +The module `'cc'` provides access to all engine functions. The contents of module `'cc'` are dynamic and are related to the **Feature Cropping** setting in **Project Setting**. + +### Engine Log Output + +Example: + +```ts +import { log } from 'cc'; +log('Hello world!'); +``` + +## Build-Time Constants + +The engine module `'cc/env'` exposes a number of build-time **constants** that represent the execution environment, debug level, or platform identifier, etc. + +As these constants are declared with `const`, they provide good opportunities for code optimization. + +### Execution Environment + +| Name (all of type `boolean`) | Description | +| :-------- | :------------------- | +| `BUILD` | Whether it is running in the post-build environment | +| `PREVIEW` | Whether it is running in the preview environment | +| `EDITOR` | Whether it running in the editor environment | + +### Debug Level + +| Name (all of type `boolean`) | Description | +| :------ | :------ | +| `DEBUG` | Whether it is in debug mode. Only `false` if the debug option is unchecked at build time, but `true` in all other cases. +| `DEV` | Equivalent to `DEBUG`/`EDITOR`/`PREVIEW` | + +### Platform Identifier + +The constants in the following table indicate whether the application is running on **a particular** or **a class of** platforms, and are all of type `boolean`. + + +| Name | Platform | `MINIGAME` | `RUNTIME_BASED` | `SUPPORT_JIT` | +| :---------- | :---------- | :----------------- | :----------------- | :----------------- | +| `HTML5` | Web | ❌ | ❌ | ❌ | +| `NATIVE` | Native Platforms | ❌ | ❌ | ❌ | +| `ALIPAY` | Alipay Mini Game | ✔️ | ❌ | ✔️ | +| `BAIDU` | Baidu Mini Game | ✔️ | ❌ | ✔️ | +| `BYTEDANCE` | Douyin Mini Game | ✔️ | ❌ | ✔️ | +| `WECHAT` | WeChat Mini Gamee | ✔️ | ❌ | ✔️ | +| `COCOSPLAY` | Cocos Play | ❌ | ✔️ | ✔️ | +| `HUAWEI` | Huawei Quick Game | ❌ | ✔️ | ✔️ | +| `OPPO` | OPPO Mini Game | ❌ | ✔️ | ✔️ | +| `VIVO` | vivo Mini Game | ❌ | ✔️ | ✔️ | + +### Output in Debug Mode + +Examples are as follows: + +```ts +import { log } from 'cc'; +import { DEV } from 'cc/env'; + +if (DEV) { + log('I am in development mode!'); +} +``` + + diff --git a/versions/4.0/en/scripting/modules/example.md b/versions/4.0/en/scripting/modules/example.md new file mode 100644 index 0000000000..9e2ee799dc --- /dev/null +++ b/versions/4.0/en/scripting/modules/example.md @@ -0,0 +1,252 @@ +# External Module Usage Case + +This section provides examples of how to use npm modules in Cocos Creator projects. To obtain `npm`, please refer to the [How to Get npm Packages](./config.md) documentation. + +## ESM and CJS Interaction Rules + +The biggest problem with how to use npm in Cocos Creator 3.x is the interaction between ESM and CJS modules. To understand how these two modules are defined in Cocos Creator, check out the [modules](./spec.md) section. In fact, the way ESM and CJS modules interact is described in the [official Node.js](https://nodejs.org/api/esm.html#esm_interoperability_with_commonjs) documentation. Briefly summarized as follows: + +- CommonJS modules are exported by `module.exports`, and when importing CommonJS modules, use the default import method for ES modules or their sugar syntax counterparts. + + ```ts + import { default as cjs } from 'cjs'; + // syntactic sugar form + import cjsSugar from 'cjs'; + console.log(cjs); // + console.log(cjs === cjsSugar); // true + ``` + +- The `default` export of the **ESM** module points to the `module.exports` of the **CJS** module. +- The non-`default` part of the export is provided by Node.js as a separate ES module through static analysis. + +Next, consider the following code snippet: + +```js +// foo.js +module.exports = { + a: 1, + b: 2, +} + +module.exports.c = 3; + +// test.mjs + +// default points to module.exports +import foo from './foo.js'; // equivalent to import { default as foo } from './foo.js' +console.log(JSON.stringify(foo)); // {"a":1, "b":2, "c":3} + +// Import all exports of the foo module +import * as module_foo from './foo.js' +console.log(JSON.stringify(module_foo)); // {"c":3, "default":{"a":1, "b":2, "c":3}} + +import { a } from './foo.js' +console.log(a); // Error: a is not defined + +// According to the third point above, c has a separate export +import { c } from './foo.js' +console.log(c); // 3 +``` + +## npm Module Usage Cases + +### Case 1: using protobufjs + +First, get the [protobufjs](https://www.npmjs.com/package/protobufjs) package. Open a terminal in the project directory and execute `npm i protobufjs`. If the project is a multi-person collaboration, the `protobufjs` package can be written to the `package.json` as a dependency by adding `npm install --save protobufjs` to the above line to automatically write it to `package.json` from the command line. Once executed, find the `protobufjs` folder in the `node_module` folder under the project directory. + +![module](./example/module.png) + +Once the `protobufjs` module package is installed, determine the module format. + +- Look at the `main` field in the `package.json` file and determine the entry file `index.js`. +- Look at the `type` field in the `package.json` file and notice that there is no `type` field. + +Identify the module format, it can be inferred that this is a **CJS** module. By the way, you can see that each js file in the package corresponds to a `.d.ts` file, which means that the `protobufjs` package comes with a `TypeScript` declaration file, which makes it easy to import the `protobufjs` module and get the internal interface through code hints. + +Next, notice it exported in `index.js`: + +```js +"use strict"; +module.exports = require("./src/index"); +``` + +Determine the module format and export method. The next step is how to use the `protobufjs` module in the script assets. + +First, create a `test.ts` script under `assets`. Then, write the following code in the header of the script. + +```ts +// Most npm modules can be used by directly importing the module name. +import protobufjs from 'protobufjs'; +console.log(protobufjs); +``` + +When run in Chrome, the console output is as follows: + +![protobufjs-print-default](./example/protobufjs-print-default.png) + +Some developers may have received an error when writing `import protobufjs from 'protobufjs'`, indicating that the module `has no default export`. This is because **CJS** does not have `default` export, and **ESM** and **CJS** interact with each other when is treated as `module.exports` **as** `export default` and does not mean that **CJS** modules have true default exports. Therefore, to keep the original way of writing, it is possible to configure TypeScript, see [TypeScript configuration](#TypeScript-configuration). + +Now, all the modules provided by `protobufjs` are directly accessible. If only specific submodule functionality is needed, such as `light` and `minimal`, import the subpaths directly from the package: + +```ts +// Using the light version +import protobuf from 'protobufjs/light.js'; +// Using the minimal version +import protobuf from 'protobufjs/minimal.js'; +``` + +> **Note**: in the case of `protobufjs` and many classic npm packages, the suffix is required when importing subpaths in the package. Please refer to the [Cocos Creator module specification](./spec.md) documentation. + +#### TypeScript configuration + +When importing modules without default export in TypeScript, it usually appears that `Module '"/${project_path}/protobufjs"' has no default export.`. This is because currently `protobufjs` only provides CommonJS modules, and Cocos Creator accesses CommonJS modules via "default import", but CommonJS modules really don't have a "default export". In this case, set the `"allowSyntheticDefaultImports"` option in the `"compilerOptions"` field to `true` by editing the `tsconfig.json` file in the project directory. If the field is not present, manually fill it in. + +```json5 +{ + /* Base configuration. do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + "compilerOptions": { + "allowSyntheticDefaultImports": true, // Needs to be turned on + } +} +``` + +### Case 2: Compiling a proto file from protobuf into a JavaScript file + +This section is about how to compile a proto file into a JavaScript file. In fact, when looking through the `protobufjs` documentation, notice that it provides [command line tools](https://www.npmjs.com/package/protobufjs#command-line) to convert static modules and ts declaration files. This time, let's take an empty 3.0 project, example, as an example to demonstrate the whole process. + +First, install `protobufjs` via npm and write it to the `package.json` dependency in the project directory. + +Next, create a new Proto directory in the project directory and define a few proto files: + +```proto +// pkg1.proto + +package pkg1; +syntax = "proto2"; +message Bar { + required int32 bar = 1; +} + +// pkg2.proto + +package pkg2; +syntax = "proto2"; +message Baz { + required int32 baz = 1; +} + +// unpkg.proto is not part of any package + +syntax = "proto2"; +message Foo { + required int32 foo = 1; +} +``` + +Next, define it in `package.json`: + +```json +"scripts": { + "build-proto:pbjs": "pbjs --dependency protobufjs/minimal.js --target static-module --wrap commonjs --out ./Proto.js/proto.js ./Proto/*.proto", + "build-proto:pbts": "pbts --main --out ./Proto.js/proto.d.ts ./Proto.js/*.js" +}, +``` + +The first directive `build-proto:pbjs` roughly means to compile the proto file into js. The parameter `--dependency protobufjs/minimal.js` is actually because the execution `require`s to `protobufjs`, but all we need to use is its submodule `minimal.js`. Then, generate the js into the Proto.js folder. + +> **Note**: if a Proto.js folder is not present, manually create it. + +The second command `build-proto:pbts` generates the type declaration file based on the output of the first paragraph. + +Following the above steps successfully completes the process of converting the proto file into a JavaScript file. Next, the JavaScript file can be introduced into the project script under `assets`. + +```ts +import proto from './Proto.js/proto.js'; +console.log(proto.pkg1.Bar); +``` + +Here it is still important to state that if anyone gets an error when importing, indicating that proto is not exported by default, there are two solutions: + +- It can be solved by allowing default import fields for modules containing default exports in `tsconfig.json` + + ```json + "compilerOptions": { + "allowSyntheticDefaultImports": true, + } + ``` + +- Add the default export + + Create a `Tools/wrap-pbts-result.js` file in the project directory with the following script code: + + ```js + const fs = require('fs'); + const ps = require('path'); + const file = ps.join(__dirname, '...' , 'proto.js', 'proto.d.ts'); + const original = fs.readFileSync(file, { encoding: 'utf8' }); + fs.writeFileSync(file, ` + namespace proto { + ${original} + } + export default proto; + `); + ``` + + Replace the original `build-proto:pbts` command with the following: + + ```json + "build-proto:pbts": "pbts --main --out ./Proto.js/proto.d.ts ./Proto.js/*.js && node ./Tools/wrap-pbts-result.js" + ``` + +Eventually, it will be ready to run directly. For the full project content, please refer to: [npm-case](https://github.com/cocos/cocos-example-projects/tree/master/npm-case). + +> **Note**: the packaged JavaScript file can be placed in the `assets` directory of the project or elsewhere in the project. The js file in the `assets` directory no longer needs to be checked for export as a plugin, please be aware of that. + +### Case 3: use of lodash-es + +Similar to [Case 1: protobufjs use](#case-1-using-protobufjs), install the `lodash-es` package. The entry file is `lodash.js`, and the entry file also automatically helps to export all submodules under it in **ESM** module format, and according to `type` it also confirms that it is currently an **ESM** module. Therefore, it is possible to import any module directly. Taking the `test.ts` script asset under `assets` as an example, and introduce the submodules within `lodash`: + +```ts +import { array, add } from 'lodash-es'; +``` + +At this point, notice that an error is reported at the code level, but it actually works. This is because there is a clear distinction between JavaScript, which is dynamically typed, and TypeScript, which is statically typed, this means there is no way to know the exact type of the exported module when using JavaScript, and the best thing to do is to declare a type definition file `.d.ts`. Fortunately, when hovering over the error, it will prompt that the type declaration file for the `lodash` module can be installed by running `npm i --save-dev @types/lodash-es`. After installation, restart VS Code and notice the error message disappears, along with the code hint. + +### Case 4: Using web3 + +Install the `web3` package in the same way as the `protobufjs` case above. Using the same detection method, determine that `web3` is a **CJS** module. Import in the same way: + +```ts +import web3 from 'web3'; +``` + +When going back to the editor, a bunch of errors are still present: + +![node-module](./example/node-module.png) + +This is because the package is customized for Node and contains built-in modules from Node.js, which are not available in Creator, resulting in errors. Usually, these packages also take care of web users. When importing the package directly, the package entry points directly to the Node version, while the web version is placed in the `dist/**.js` or `dist/**.min.js` file under the package. In this case, it is necessary to import the custom version of the Web provided in the package as follows: + +```ts +import web3 from 'web3/dist/web3.min.js'; +``` + +At this point, the editor does not report errors anymore, but the code prompts: `Could not find a declaration file for module 'web3/dist/web3.min.js'. '/${project_path}/node_modules/web3/dist/web3.min.js' implicitly has an 'any' type.`. + +This is because even if these packages have a type specification, it is a type specification for the Node version of that entry. In general, the interface definitions are the same for both versions, just "borrow" the type description from the Node version. Just create an arbitrary new `.d.ts` to the project and write: + +```ts +// Add a type specification for the module "/dist/**.js" +declare module "/dist/**.js" { + export * from ""; // "Steal" the type description of the main entry +} +``` + +> **Note**: currently it is necessary to confirm that if a package is customized for Node. But in the new version of Node, package.json has a nice mechanism added to tell the user which version to use for what case. + +### Special case: firebase usage + +This is a special case, take a look at the special features of this case. After installing the `firebase` package in the way described above, and analyzing the package according to the `package.json` file, notice the package is in **CJS** format. Based on the entry file, infer that this package is customized for Node (this can be tested according to [Case 4: Using web3](#case-4-using-web3)), so we have to use the Web custom version `index.esm.js`. The problem is, `index.esm.js` is an **ESM** module, and Creator recognizes this package as a **CJS** module, but it is also an **ESM** module, which naturally causes errors. + +The proposed solution is for the user to package it as a separate js file as a **non-npm module** via a packaging tool like `rollup`. diff --git a/versions/4.0/en/scripting/modules/example/MGOBE-no-module.png b/versions/4.0/en/scripting/modules/example/MGOBE-no-module.png new file mode 100644 index 0000000000..9959604feb Binary files /dev/null and b/versions/4.0/en/scripting/modules/example/MGOBE-no-module.png differ diff --git a/versions/4.0/en/scripting/modules/example/module.png b/versions/4.0/en/scripting/modules/example/module.png new file mode 100644 index 0000000000..a707792598 Binary files /dev/null and b/versions/4.0/en/scripting/modules/example/module.png differ diff --git a/versions/4.0/en/scripting/modules/example/node-module.png b/versions/4.0/en/scripting/modules/example/node-module.png new file mode 100644 index 0000000000..18ab0c6735 Binary files /dev/null and b/versions/4.0/en/scripting/modules/example/node-module.png differ diff --git a/versions/4.0/en/scripting/modules/example/protobufjs-print-default.png b/versions/4.0/en/scripting/modules/example/protobufjs-print-default.png new file mode 100644 index 0000000000..c264d61acd Binary files /dev/null and b/versions/4.0/en/scripting/modules/example/protobufjs-print-default.png differ diff --git a/versions/4.0/en/scripting/modules/import-map.md b/versions/4.0/en/scripting/modules/import-map.md new file mode 100644 index 0000000000..cab75528dc --- /dev/null +++ b/versions/4.0/en/scripting/modules/import-map.md @@ -0,0 +1,94 @@ +# Import Maps (experimental) + +Cocos Creator experimentally supports [Import Maps](https://github.com/WICG/import-maps) starting from v3.3. + +Import maps control the import behavior of TypeScript/JavaScript, in particular, it can specify the import behavior for [bare specifiers](./spec.md#bare-specifiers). + +## Use + +The import map's file path can be specified via the **Import Maps** option in **Project -> Project Settings -> Scripting** in the top menu bar of the editor. After setting, the import maps function will be enabled and the import maps used will be read from the specified file. + +> **Note**: the import map's file path is crucial, because all relative paths in the import maps are relative to the import map's file path itself. + +### Alias Mapping + +If there is a module that is used by all modules in the project, and the developer does not want other modules to refer to it as a relative path, but rather to give it an alias, then using import maps is a good choice. + +For example, if the real absolute path of a module is `/assets/lib/foo.ts` then all modules to refer to it as `import {} from 'foo';`: + +First, create an import-map file `import-map.json` in the project directory: + +```json +// import-map.json + +{ + "imports": { + "foo": "./assets/lib/foo.ts" + } +} +``` + +- `"imports"`: specifies the **Top Level Imports** to be applied to all modules. +- `"foo"`: specifies the name of the module to map. +- `"./assets/lib/foo.ts"`: specifies how to map `"foo"`. `"./assets/lib/foo.ts"` is a relative path, **all relative paths in import maps are relative to the location of the import map's file itself**, so `./assets/lib/foo.ts` will be resolved to the absolute path `/assets/lib/foo.ts`. + +Then `'foo'` will be resolved to the module `/assets/lib/foo.ts` when referencing the module in any module using: + +```ts +import * as foo from 'foo'; +``` + +### Directory Mapping + +Import maps also allows mapping all modules in a given directory. + +For example, to map all modules in the project `assets/lib/bar-1.2.3` directory, the json file for the import maps would look like this: + +```json +// import-map.json + +{ + "imports": { + "bar/": "./assets/lib/bar-1.2.3/" + } +} +``` + +This is consistent with **alias mapping** except that `"bar/"` specifies the directory to map to. + +This way the modules in the project can all refer to the directory as `import {} from 'bar/...' ` to refer to modules in the directory `bar-1.2.3`. + +For example: + +```ts +import * as baz from 'bar/baz'; +import * as quux from 'bar/qux/quux'; +``` + +`'bar/baz'` will be resolved to the module `/assets/lib/bar-1.2.3/baz.ts`.
`'bar/qux/quux'` will be resolved to the module `/assets/lib/bar-1.2.3/qux/quux.ts`. + +### TypeScript configuration + +TypeScript does not support import maps, which can lead to errors when using it. Additional configuration to tell the TypeScript type checker additional module resolution information. + +In the two examples above, the [paths](https://www.typescriptlang.org/tsconfig#paths) field can be configured in the `tsconfig.json` file in the project directory. If this field is not present already, add it. Example: + +```json5 +// tsconfig.json +{ + "compilerOptions": { + "paths": { + // Note: the relative path here is relative to the path where tsconfig.json is located + // Since tsconfig.json and import-map.json are in the same directory in this example, the relative paths here are similar. + "foo": ["./assets/lib/foo"], + "bar/*": ["./assets/lib/bar-1.2.3/*"] + } + } +} +``` + +For more information about import maps features, please refer to the [Import Maps](https://github.com/WICG/import-maps) documentation. + +## Support + +Cocos Creator supports all features in [Import Maps Draft Community Group Report, 12 January 2021](https://wicg.github.io/import-maps/) standard. diff --git a/versions/4.0/en/scripting/modules/index.md b/versions/4.0/en/scripting/modules/index.md new file mode 100644 index 0000000000..ec47c22114 --- /dev/null +++ b/versions/4.0/en/scripting/modules/index.md @@ -0,0 +1,25 @@ +# Module Specifications and Examples + +All code files can be roughly divided into [Plugin Script](../external-scripts.md) and **Module** two kinds, this section mainly introduces the module related. + +Modules are a way of organizing TypeScript/JavaScript code, and code organized in modules is also informally known as **scripts** or **project scripts**. In Cocos Creator, all code except plugin scripts is organized in modules, which are roughly divided into the following kinds, depending on their source: + +- Code created in the project, including component scripts and project (non-component) scripts. + +- Functionality provided by the engine, please refer to the [Engine Modules](./engine.md) for details. + +- Third-party modules, such as the `npm` module. Please review the [External Module Usage Case](./example.md) documentation for details. + +Cocos Creator natively supports and recommends the use of the ECMAScript (ESM for short) module format. To support the use of external modules, Cocos Creator also supports the CommonJS module format to some extent. For additional information about the module format and usage in Cocos Creator, please refer to the [Module Specification](./spec.md) documentation. + +Cocos Creator 3.3 experimentally supports import maps. Please refer to the [Import Maps](./import-map.md) documentation for details. + +## Module Loading Order + +The modules are loaded in the following order: + +1. First import of the [engine module](./engine.md) `"cc"` of Cocos Creator 3.x. + +2. Plugin scripts: all plugin scripts will be executed in the order of the specified plugin script dependencies, there is disorder between plugin scripts that do not have dependencies. Please refer to the [Plugin Scripts](../external-scripts.md) documentation for details. + +3. Common scripts: all common scripts will be imported concurrently, and the import will strictly follow the reference relationships and execution order determined by `import`. diff --git a/versions/4.0/en/scripting/modules/spec.md b/versions/4.0/en/scripting/modules/spec.md new file mode 100644 index 0000000000..58d6827a2e --- /dev/null +++ b/versions/4.0/en/scripting/modules/spec.md @@ -0,0 +1,201 @@ +# Module Specification + +## Module Format + +This section describes how Cocos Creator determines the format of a module. + +All the functionalities provided by the Cocos Creator engine are in the form of ESM modules, see [engine modules](./index.md). + +Files in the project resources directory ending in `.ts`. For example `assets/scripts/foo.ts`. + +For any other module formats, Cocos Creator chooses rules similar to Node.js to [identify](https://nodejs.org/api/packages.html#packages_determining_module_system). Specifically, the following files will be considered in ESM format: + +- Files ending in `.mjs`. + +- Files ending in `.js` and whose nearest parent `package.json` file contains a top-level `"type"` field with a value of `"module"`. + +The rest of the files will be treated as CommonJS module format, which includes + +- Files ending in `.cjs`. + +- Files ending in `.js` and whose nearest `package.json` file contains a top-level `"type"` field with a value of `"commonjs"`. + +- Files ending in `.js` that do not fall under the above conditions. + +## Module Descriptors and Module Parsing + +In an ESM module, interaction with the target module is done through standard import and export statements, e.g. + +```ts +import { Foo } from '. /foo'; +export { Bar } from '. /bar'; +``` + +The string after the keyword `from` in the import/export statement is called a **module specifier**. The module specifier can also appear as a parameter in the dynamic import expression `import()`. + +The module specifier is used to specify the target module, and the process of resolving the target module URL from the module specifier is called **module resolution**. + +Cocos Creator supports three types of module specifiers: + +- **relative specifier**: a specifier like `'./foo'`, `'../bar'` starting with `'./'` and `'../'`. + +- **absolute specifier**: a specifier that specifies a URL. For example: `foo:/bar`. + +- **bare specifier**: a specifier like `foo` or `foo/bar` that is neither a URL nor a relative specifier. + +### Relative Specifiers + +Relative specifiers take the URL of the current module as the base URL and use the relative specifier as input to resolve the URL of the target module. + +For example, for the module `project path/assets/scripts/utils/foo`, `'./bar'` will be parsed as `project path/assets/scripts/utils/bar` in the same directory; `'../baz'` will be parsed as `project path/assets/scripts/baz` in the upper directory. + +### Absolute Specifiers + +The absolute specifier directly specifies the URL of the target module. + +Cocos Creator currently only supports file protocol URLs, but since the file path specified in the file URL is an absolute path, it is rarely used. + +> **Note**: in Node.js, one way to access Node.js built-in modules is through `node:` protocol URLs, e.g.: `node:fs`. Cocos Creator parses all requests for access to Node.js built-in modules as `node:` URL requests. For example, `'fs'` in `import fs from 'fs'` will resolve to `node:fs`. However, Cocos Creator does not support Node.js built-in modules, which means that it does not support the `node:` protocol. Therefore, a loading error will occur. This error may be encountered when using modules in npm. + +### Bare Specifiers + +Currently, Cocos Creator will apply [Import Maps (experimental)](./import-map) and [Node.js module parsing algorithm](https://nodejs.org/api/esm.html#esm_resolver_algorithm_specification) for bare specifiers. + +> This includes parsing of npm modules. + +#### Conditional exports + +In the **Node.js** module parsing algorithm, the [conditional export](https://nodejs.org/api/packages.html#packages_conditional_exports) feature of packages is used to map the subpaths in a package based on some conditions. Similar to **Node.js**, Cocos Creator implements built-in conditions `import` and `default`, but not conditions `require` and `node`. + +Developers can specify **additional** conditions via the **Export conditions** option in the editor's main menu **Project -> Project Settings -> Scripting**, which defaults to `browser`. Multiple additional conditions can be specified using **commas** as separators, e.g. `browser, bar`. + +If the **Export conditions** option uses the default value `browser`, when the `package.json` of an npm package `foo` contains the following configuration: + +```json +{ + "exports": { + ".": { + "browser": "./dist/browser-main.mjs", + "import": "./dist/main.mjs" + } + } +} +``` + +`"foo"` will resolve to the module with path `dist/browser-main.mjs` in the package. + +> The mapping configuration is done in [Multiplayer Framework Colyseus for Node.js](https://www.npmjs.com/package/colyseus) for the `browser` condition. + +If the **Export conditions** option is empty, it means that no additional conditions are specified, and `"foo"` in the above example will resolve to a module with path `dist/main.mjs` in the package. + +### Suffixes and Directory Import + +Cocos Creator's requirements for module suffixes in module specifiers are more web-oriented -- suffixes must be specified and Node.js-style directory import is not supported. However, for historical reasons and some existing restrictions, TypeScript modules do not allow suffixes and support Node.js-style directory import. Specifically: + +When the target module file has the suffix `.js`, `.mjs`, the suffix **must be specified** in the module specifier: `.js`, `.mjs`. + +```ts +import '. /foo.mjs'; // correct +import '. /foo'; // error: the specified module cannot be found +``` + +Node.js-style directory import is not supported: + +```ts +import '. /foo/index.mjs'; // correct +import '. /foo'; // error: module cannot be found. +``` + +> This suffix requirement applies to both relative and absolute specifiers along with the restriction on directory import. For requirements in bare specifiers please refer to the Node.js module parsing algorithm. + +However, when the target module file has a suffix of `.ts`, the suffix is **not allowed to be specified** in the module specifier: + +```ts +import '. /foo'; // correct: parsed as the `foo.ts` module in the same directory +import '. /foo.ts'; // error: the specified module cannot be found +``` + +On the other hand, Node.js-style directory import is supported: + +```ts +import '. /foo'; // correct: parsed as the `foo/index.ts` module +``` + +> **Notes**: +> 1. Cocos Creator supports the Web platform. Implementing complex module parsing algorithms like Node.js on the Web platform is expensive, and the client and server cannot try different suffixes and file paths with frequent communication between them. +> 2. Even if such complex parsing could be done at the build stage with some post-processing tools, it would result in inconsistent algorithms for static import parsing (via `import` statements) and dynamic import parsing (via `import()` expressions). Therefore, specify the full file path in the code for the choice of module parsing algorithm. +> 3. However, this cannot be restricted completely, since TypeScript currently doesn't allow the suffix `.ts` to be specified in the specifier. And TypeScript does not yet support auto-completion of specific target suffixes. With these limitations, it's hard to have it both ways, but we're still watching to see if these conditions improve in the future. + +### The `browser` Field is not Supported + +Some npm packages have `browser` fields documented in the manifest file `package.json`, e.g.: [JSZip](https://github.com/Stuk/jszip). The `browser` field is used to specify a module parsing method specific to the package when it is in a non-Node.js environment, which allows some Node.js-specific modules in the package to be replaced with modules that can be used in the Web. Although Cocos Creator **does not support this field**, if you have the ability to edit npm packages, Cocos Creator recommends using [conditionalized export](https://nodejs.org/api/packages.html#packages_conditional_exports) and [subpath import](https://nodejs.org/api/packages.html#packages_subpath_imports) instead of the `browser` field. + +Otherwise, the target library can be used in a non-npm way. For example, copying modules from the target library that are specifically made for non-Node.js environments into the project and importing them via relative paths. + +## CommonJS Module Parsing + +In CommonJS modules, Cocos Creator applies the [Node.js CommonJS module parsing algorithm](https://nodejs.org/api/modules.html#modules_all_together). + +## Module Format Interaction + +Cocos Creator allows importing CommonJS modules in ESM modules. + +When importing a CommonJS module from an ESM module, the `module.exports` object of the CommonJS module will be used as the default export for the ESM module: + +```ts +import { log } from 'cc'; + +import { default as cjs } from 'cjs'; + +// Another way to write the above import statement: +import cjsSugar from 'cjs'; + +log(cjs); +log(cjs === cjsSugar); +// Print. +// +// true + +``` + +The CommonJS module's [ECMAScript module namespace](https://tc39.es/ecma262/#sec-module-namespace-objects) indicates that it is a namespace containing a `default` export, where the `default` export points to the value of `module.exports` of the CommonJS module. + +This [module namespace foreign object](https://tc39.es/ecma262/#module-namespace-exotic-object) can be observed by `import * as m from 'cjs'`. + +```ts +import * as m from 'cjs'; +console.log(m); +// Print: +// [Module] { default: } +``` + + + +## Cocos Creator ESM Parsing Algorithm Public Notice + +The algorithm used by Cocos Creator to parse ESM module specifiers is given by the following `CREATOR_ESM_RESOLVE` method. It returns the result of parsing the module specifier from the current URL. + +The [external algorithm](https://nodejs.org/api/esm.html#esm_resolution_algorithm) is referenced in the parsing algorithm specification. + +### Parsing Algorithm Specification + +`CREATOR_ESM_RESOLVE(specifier, parentURL)` + 1. Let `resolved` be the result of `ESM_RESOLVE(specifier, parentURL)`. + 2. If both `parentURL` and `resolved` are under project assets directory, then + 1. Let `extensionLessResolved` be the result of `TRY_EXTENSION_LESS_RESOLVE(resolved)`. + 1. If `extensionLessResolved` is not `undefined`, return `extensionLessResolved`. + 3. Return `resolved`. + +`TRY_EXTENSION_LESS_RESOLVE(url)` + 1. If the file at `url` exists, then + 1. Return `url`. + 2. Let `baseName` be the portion after the last "/" in pathname of `url`, or whole pathname if it does not contain a "/". + 3. If `baseName` is empty, then + 1. Return `undefined`. + 4. Let `resolved` be the result URL resolution of "./" concatenated with `baseName` and `.ts`, relative to parentURL. + 1. If the file at `resolved` exists, then + 2. Return `resolved`. + 5. Let `resolved` be the result URL resolution of "./" concatenated with `baseName` and `/index.ts`, relative to parentURL. + 1. If the file at `resolved` exists, then + 2. Return `resolved`. + 6. Return `undefined`. diff --git a/versions/4.0/en/scripting/plugin-scripts/import-as-plugin.png b/versions/4.0/en/scripting/plugin-scripts/import-as-plugin.png new file mode 100644 index 0000000000..767f44ae2f Binary files /dev/null and b/versions/4.0/en/scripting/plugin-scripts/import-as-plugin.png differ diff --git a/versions/4.0/en/scripting/plugin-scripts/sort-plugin.png b/versions/4.0/en/scripting/plugin-scripts/sort-plugin.png new file mode 100644 index 0000000000..d6e399587f Binary files /dev/null and b/versions/4.0/en/scripting/plugin-scripts/sort-plugin.png differ diff --git a/versions/4.0/en/scripting/property-changed.png b/versions/4.0/en/scripting/property-changed.png new file mode 100644 index 0000000000..1051d0299e Binary files /dev/null and b/versions/4.0/en/scripting/property-changed.png differ diff --git a/versions/4.0/en/scripting/readonly.md b/versions/4.0/en/scripting/readonly.md new file mode 100644 index 0000000000..f0424ff49c --- /dev/null +++ b/versions/4.0/en/scripting/readonly.md @@ -0,0 +1,31 @@ +# Development Notes + +## ReadOnly + +Since `Readonly` is a read-only property, it is not recommended to write to it. Therefore, calling the interface directly through the `Readonly` property to modify the value is not guaranteed to work across platforms. + +For example, the position of a node in the world coordinate system is `Readonly` for. + +```typescript +/** +* @en Position in world coordinate system +* @zh 世界坐标系下的坐标 +*/ +get worldPosition(): Readonly; +set worldPosition(val: Readonly); +``` + +In the native platform, the result of `add` here is not saved to `worldPosition`: + +```typescript +this.node.worldPosition.add(xxx); // The results will not be saved! +``` + +To avoid this, it can be passed through an intermediate variable with the following code example: + +```typescript +let ret = this.node.worldPosition.add(diff.multiplyScalar(this.speedFactor)); +this.node.setWorldPosition(ret); +``` + +Common properties include, but are not limited to, those listed below: `position`, `rotation`, `scale`, `worldPosition`, `worldRotation`, `worldScale`, `eulerAngles`, and `worldMatrix`, all of which are recommended to be used in the above way. diff --git a/versions/4.0/en/scripting/reference-class.md b/versions/4.0/en/scripting/reference-class.md new file mode 100644 index 0000000000..2c740b8bbc --- /dev/null +++ b/versions/4.0/en/scripting/reference-class.md @@ -0,0 +1,275 @@ +# Advanced Scripting + +By starting this **Advanced Scripting** section it is assumed one is already familiar with the scripting system, including decorators and so on. Otherwise, please refer to the following documents to get up to speed first: + +- [Basic Scripting](./modules/index.md) +- [Decorator](./decorator.md) + +## Instantiation + +Define a `Foo` class and a `Bar` class through a script. The `Foo` class needs to use the properties defined by the `Bar` class, to allow directly issuing `new` the `Bar` class to an object in the `Foo` class: + +```ts +class Foo { + public bar: Bar = null;; + constructor() { + this.bar = new Bar(); + } +} +let bar = new Foo(); +``` + +### Instance Methods + +Instance methods should be declared in the prototype object: + +```typescript +class Foo { + public text!: string; + constructor() { + this.text = "this is sprite" + } + // Declare an instance method named "print" + print() { + console.log(this.text); + } +} + +let obj = new Foo(); +// Call the instance method +obj.print(); +``` + +## Type Determination + +When making a type determination, use TypeScript's native `instanceof`. + +```typescript +class Sub extends Base { + +} + +let sub = new Sub(); +console.log(sub instanceof Sub); //true +console.log(sub instanceof Base); //true + +let base = new Base(); +console.log(base instanceof Sub); // false +``` + +### Static Variables and Static Methods + +Static variables or static methods can be declared with `static`: + +```typescript +class Foo { + static count = 0; + static getBounds() { + + } +} +``` + +Static members are inherited by subclasses, and Creator gives a **shallow copy** of the parent's static variables to the subclass when inherited, so that: + +```typescript +class Object { + static count = 11; + static range: { w: 100, h: 100 } +} + +class Foo extends Object { + +} + +console.log(Foo.count); // The result is 11, because `count` inherits from the `Object` class + +Foo.range.w = 200; +console.log(Object.range.w); // The result is 200 because 'Foo.range' and 'Object.range' refer to the same object +``` + +Private static members can also be defined directly outside of the class if inheritance is not a concern: + +```typescript +// Local methods +doLoad(sprite) { + // ... +}; +// Local variables +let url = "foo.png"; + +class Sprite { + public url = ''; + load() { + this.url = url; + doLoad(this); + }; +}; +``` + +## Inheritance + +### Parent constructor + +> **Note**: the constructor of the parent class is automatically called before the subclass is instantiated, regardless of whether the subclass has a defined constructor or not. + +```typescript +class Node { + name: string; + constructor() { + this.name = "node"; + } +} + +class Sprite extends Node { + constructor() { + super(); + // The parent constructor has already been called before the child constructor is called, so `this.name` has already been initialized + console.log(this.name); // "node" + // Reset `this.name` + this.name = "sprite"; + } +} + +let obj = new Sprite(); +console.log(obj.name); // "sprite" +``` + +### Rewrite + +All member methods are dummy methods, and subclass methods can override parent methods directly: + +```typescript +class Shape { + getName() { + return "shape"; + } +}; + +class Rect extends Shape { + getName () { + return "rect"; + } +}; + +let obj = new Rect(); +console.log(obj.getName()); // "rect" +``` + +## `get/set` Methods + +If `get`/`set` is defined in a property, then the predefined `get`/`set` methods can be triggered when the property is accessed. + +### `get` + +Define the `get` method in the property: + +```typescript +@property +get num() { + return this._num; +} + +@property +private _num = 0; +``` + +The `get` method can return a value of any type.
+Properties that have a `get` method defined can be displayed in the **Inspector** panel and can be accessed directly in the code. + +```typescript +class Sprite { + + @property + get width() { + return this._width; + } + + @property + private _width = 0; + + print(){ + console.log(this.width); + } +}; +``` + +**Notes**: + +- Properties cannot be serialized after the `get` method is defined, i.e. the `serializable` parameter is not available. + + For example, the `width` property will neither be displayed in the editor nor serialized as shown below: + + ```typescript + get width() { + return this._width; + } + + @property({ type: CCInteger, tooltip: "The width of sprite" }) + private _width = 0; + ``` + +- Properties that have a `get` method defined need to have `property` defined if they need to be displayed by the editor, otherwise they won't be rendered. + + For example, the `width` property will not be rendered on the editor if the `@property` is removed, and the `_width` property will be serialized: + + ```typescript + @property + get width() { + return this._width; + } + + @property({ type: CCInteger, tooltip: "The width of sprite" }) + private _width = 0; + ``` + +- The property itself that defines the `get` method is read-only, but the returned object is not read-only. The developer can still modify the properties inside the object through code, for example: + + ```typescript + get num() { + return this._num; + } + + @property + private _num = 0; + + start() { + console.log(this.num); + } + ``` + +### `set` + +Define the `set` method in the property: + +```typescript + +set width(value) { + this._width = value +} + +private _width = 0; + +start() { + this.width = 20; + console.log(this.width); +} +``` + +The `set` method takes an incoming parameter, which can be of any type. The `set` method is generally used in conjunction with the `get` method: + +```typescript +@property +get width() { + return this._width; +} + +set width(value) { + this._width = value; +} + +@property +private _width = 0; +``` + +> **Note**: the `set` method does not define properties. diff --git a/versions/4.0/en/scripting/reference/attributes.md b/versions/4.0/en/scripting/reference/attributes.md new file mode 100644 index 0000000000..22c6a570d3 --- /dev/null +++ b/versions/4.0/en/scripting/reference/attributes.md @@ -0,0 +1,38 @@ +# Property Attributes + +Attributes are used to attach metadata to defined properties, similar to `Decorator` in scripting languages or `Attribute` in `C#`. + +## Related Attributes on Inspector Panel + +| Attribute Name | Description | Type | Default | Remarks | +| :--- | :--- | :--- | :--- | :--- | +| type | Restrict the data type of the property | (Any) | undefined | See [type attribute](../decorator.md#type-attribute) | +| visible | Show or hide in the **Inspector** panel | boolean | [1] | See [visible attribute](../decorator.md#visible-attribute) | +| displayName | Show as another name in the **Inspector** panel | string | undefined | - | +| tooltip | Add a Tooltip for a property in the **Inspector** panel | string | undefined | - | +| multiline | Use multi-line text boxes in the **Inspector** panel | boolean | false | - | +| readonly | Read-only in the **Inspector** panel | boolean | false | - | +| min | Restrict the minimum value in the **Inspector** panel | number | undefined | - | +| max | Restrict the maximum value in the **Inspector** panel | number | undefined | - | +| step | Restrict the step value in the **Inspector** panel | number | undefined | - | +| range | Set min, max, step | [min, max, step] | undefined | step value optional | +| slide | Show as a slider in the **Inspector** panel | boolean | false | - | +| group | Show as a tab group in the **Inspector** panel | `{ name } or { id, name, displayOrder, style }` | undefined | See [group attribute](../decorator.md#group) | + +## Serialization-Related Attributes + +The following attributes cannot be used with the `get` method. + +| Attribute Name | Description | Type | Default | Remarks | +| :--- | :--- | :--- | :--- | :--- | +| serializable | Serialize this property | boolean | true | See [serializable attribute](../decorator.md#serializable-attribute) | +| formerlySerializedAs | Specify the name of the field used in the previous serialization | string | undefined | Declare this attribute when renaming the property to be compatible with the previously serialized data | +| editorOnly | Exclude this property before exporting the project | boolean | false | - | + +## Other Attributes + +| Attribute Name | Description | Type | Default | Remarks | +| :--- | :--- | :--- | :--- | :--- | +| override | Define this parameter as true when overriding parent properties | boolean | false | See [override attribute](../decorator.md#override-attribute) | + +> [1]: The default value of visible depends on the property name. When the property name starts with an underscore `_`, it is hidden by default, otherwise it is shown by default. diff --git a/versions/4.0/en/scripting/reference/coding-standards.md b/versions/4.0/en/scripting/reference/coding-standards.md new file mode 100644 index 0000000000..b12557ea40 --- /dev/null +++ b/versions/4.0/en/scripting/reference/coding-standards.md @@ -0,0 +1,543 @@ +# Recommended Coding Standards + +The following are the coding standards that the Cocos Creator development team use. They are included in the manual for game developers and tool developers reference. + +## Naming standards + +- When naming the variables, functions and instances, we use camelCase nomenclature: + + ```ts + // Bad + const FOOBar = {}; + const foo_bar = {}; + function FOOBar () {} + + // Good + const fooBar = {}; + function fooBar () {} + ``` + +- When variable, function, and instance naming involves abbreviations, the abbreviations are all lowercase at the beginning, and all uppercase in subsequent words: + + ```ts + // Bad + const Id = 0; + const iD = 0; + function requireId () {} + + // Good + const id = 0; + const uuid = ''; + function requireID () {} + class AssetUUID {} + ``` + +- When naming types or modules, use PascalCase nomenclature: + + ```ts + // Bad + const foobar = cc.Class({ + foo: 'foo', + bar: 'bar', + }); + const foobar = require('foo-bar'); + + // Good + const FooBar = cc.Class({ + foo: 'foo', + bar: 'bar', + }); + const FooBar = require('foo-bar'); + ``` + +- It is recommended to use full uppercase underline to name “constants”: + + ```ts + // Bad + const PRIVATE_VARIABLE = 'should not be unnecessarily uppercased within a file'; + + // Bad + var THING_TO_BE_CHANGED = 'should obviously not be uppercased'; + + // Bad + let REASSIGNABLE_VARIABLE = 'do not use let with uppercase variables'; + + // --- + + // Allowed but does not supply semantic value + export const apiKey = 'SOMEKEY'; + + // Better in most cases + export const API_KEY = 'SOMEKEY'; + + // --- + + // Bad - unnecessarily uppercases key while adding no semantic value + export const MAPPING = { + KEY: 'value' + }; + + // Good + export const Type = { + SIMPLE: 'value' + }; + ``` + +- Use underscores `_` when naming private attributes: + + ```ts + // Bad + this.__firstName__ = 'foobar'; + this.firstName_ = 'foobar'; + + // Good + this._firstName = 'foobar'; + ``` + +- Use dash nomenclature for files: + + ```bash + // bad + fooBar.js + FooBar.js + + // good + foo-bar.js + ``` + +## Grammar standards + +- When a class has a property declaration without initialization style, `declare` should be declared, otherwise may face performance problems. Please refer to [this issue](https://github.com/cocos-creator/3d-tasks/issues/2848) for details. + + ```typescript + // Bad + class A { + public a: number; + constructor (a : number) { + // This is equivalent to another sentence ‘this.a = void 0;’ here. + // Be aware that may face performance problems! + this.a = a; + } + } + + // Good + class A { + public a: number = 0; // Ok. + constructor (a : number) { + // This is equivalent to another sentence ‘this.a = 0;’ here. + // Does not cause major performance problems. + this.a = a; + } + } + + // Best + class A { + public declare a: number; + public b: undefined | object; // OK: b No secondary assignment in the constructor. + public declare c: object | null; + + constructor (a: number, c: object) { + this.a = a; + this.c = c; + } + } + ``` + +- Use `Object.create(null)` to create an object: + + ```ts + // Bad + const obj = new Object(); + + // Bad + const obj = {}; + + // Good + const obj = Object.create(null); + ``` + +- Use `[]` to create an array: + + ```ts + // Bad + const array = new Array(); + + // Good + const array = []; + ``` + +- Try your best to use single quotation marks `''` to define a string in TypeScript code: + + ```ts + // Bad + const str = "Hello World"; + + // Good + const str = 'Hello World'; + ``` + +- When defining multi-lines string, try your best to use `+`: + + ```ts + // Bad + const errorMessage = 'This is a super long error that was thrown out because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.'; + + // Bad + const errorMessage = 'This is a super long error that was thrown out because \ + of Batman. When you stop to think about how Batman had anything to do \ + with this, you would get nowhere \ + fast.'; + + // Good + const errorMessage = 'This is a super long error that was thrown out because ' + + 'of Batman. When you stop to think about how Batman had anything to do ' + + 'with this, you would get nowhere fast.'; + ``` + +- Use `===` and `!==` rather than `==` and `!=`. + +## Grammar standards + +- Choose quadruple spacing or double spacing for indentation according to your own habits and the primary code writer's format: + + ```js + // Bad + function() { + ∙const name; + } + + // Very bad + function() { + ∙∙∙∙const name; + } + + // Good + function() { + ∙∙const name; + } + + // Good + function() { + ∙∙∙∙const name; + } + ``` + +- Do not leave spaces at the end of the line. Leave an empty line at the bottom of the file: + + ```js + // Bad + function () {∙ + ∙∙∙∙const name;∙ + } + /* EOF */ + + // Good + function () { + ∙∙∙∙const name; + } + + /* EOF */ + ``` + +- Please add `;` at the end of the statement: + + ```js + // Bad + proto.foo = function () { + } + + // Good + proto.foo = function () { + }; + + // Bad + function foo () { + return 'test' + } + + // Very bad + // Returns `undefined` instead of the value on the next line, + // Always happens when `return` is on a line by itself because of Automatic Semicolon Insertion! + function foo () { + return + 'test' + } + + // Good + function foo () { + return 'test'; + } + + // Bad + function foo () { + }; + + // Good, this is not the end of the statement + function foo () { + } + ``` + +- Try to put `{` and the expression in the same line: + + ```js + // Bad + if ( isFoobar ) + { + } + + // Good + if ( isFoobar ) { + } + + // Bad + function foobar() + { + } + + // Good + function foobar() { + } + + // Bad + const obj = + { + foo: 'foo', + bar: 'bar', + } + + // Good + const obj = { + foo: 'foo', + bar: 'bar', + } + ``` + +- Put a space before `{`: + + ```js + // Bad + if (isJedi){ + fight(); + } + else{ + escape(); + } + + // Good + if (isJedi) { + fight(); + } else { + escape(); + } + + // Bad + dog.set('attr',{ + age: '1 year', + breed: 'Bernese Mountain Dog', + }); + + // Good + dog.set('attr', { + age: '1 year', + breed: 'Bernese Mountain Dog', + }); + ``` + +- Please put a space before `(` of the logic state expressions ( `if`, `else`, `while`, `switch`): + + ```js + // Bad + if(isJedi) { + fight (); + } + else{ + escape(); + } + + // Good + if (isJedi) { + fight(); + } else { + escape(); + } + ``` + +- Leave one space between the binary ternary operators: + + ```js + // Bad + const x=y+5; + const left = rotated? y: x; + + // Good + const x = y + 5; + const left = rotated ? y : x; + + // Bad + for (let i=0; i< 10; i++) { + } + + // Good + for (let i = 0; i < 10; i++) { + } + ``` + +- The way some functions are declared: + + ```js + // Bad + const test = function () { + console.log('test'); + }; + + // Good + function test () { + console.log('test'); + } + + // Bad + function divisibleFunction () { + return DEBUG ? 'foo' : 'bar'; + } + + // Good + const divisibleFunction = DEBUG ? + function () { + return 'foo'; + } : + function () { + return 'bar'; + }; + + // Bad + function test(){ + } + + // Good + function test () { + } + + // Bad + const obj = { + foo: function () { + } + }; + + // Good + const obj = { + foo () { + } + }; + + // Bad + array.map(x=>x + 1); + array.map(x => { + return x + 1; + }); + + // Good + array.map(x => x + 1); + ``` + +- Put a space between Block definitions: + + ```js + // Bad + if (foo) { + return bar; + } + return baz; + + // Good + if (foo) { + return bar; + } + + return baz; + + // Bad + const obj = { + x: 0, + y: 0, + foo() { + }, + bar() { + }, + }; + return obj; + + // Good + const obj = { + x: 0, + y: 0, + + foo() { + }, + + bar() { + }, + }; + + return obj; + ``` + +- Do not use a comma to define: + + ```js + // Bad + const story = [ + once + , upon + , aTime + ]; + + // Good + const story = [ + once, + upon, + aTime, + ]; + + // Bad + const hero = { + firstName: 'Ada' + , lastName: 'Lovelace' + , birthYear: 1815 + , superPower: 'computers' + }; + + // Good + const hero = { + firstName: 'Ada', + lastName: 'Lovelace', + birthYear: 1815, + superPower: 'computers', + }; + ``` + +- Single line comments, please add a space after the slash: + + ```js + // Bad + // Good + ``` + +- Multiline comments: + + ```js + /* + * Good + */ + ``` + +- A multiline comments that needs to be exported to the API document: + + ```js + /** + * Good + */ + ``` + +## Recommended Reading + +[Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript#table-of-contents) diff --git a/versions/4.0/en/scripting/scene-managing.md b/versions/4.0/en/scripting/scene-managing.md new file mode 100644 index 0000000000..beed3c0994 --- /dev/null +++ b/versions/4.0/en/scripting/scene-managing.md @@ -0,0 +1,70 @@ +# Loading and Switching Scenes + +__Cocos Creator__ uses the scene's file name (without extension) to index the scene. Loading and switching scenes is performed using the `loadScene()` API. Example: + +```ts +director.loadScene("MyScene"); +``` + +In addition, as of v2.4, the Asset Bundle has added a new loading method: + +```js +bundle.loadScene('MyScene', function (err, scene) { + director.runScene(scene); +}); +``` + +The `loadScene` provided by the Asset Bundle will only load scenes from the specified bundle and will not automatically run the scenes, you also need to use `director.runScene` to run the scenes.
+`loadScene` also provides more parameters to control the loading process, so developers can control the loading parameters themselves or do some processing after the loading scene. + +For more information about loading scenes in the Asset Bundle, you can refer to the [Asset Bundle](../asset/bundle.md) documentation. + +## Scene resource management and Persistent Nodes + +The engine will only run one scene at the same time. When switching scenes, all nodes and other instances in the scene will be destroyed by default. Developer's may need to use a component to control the loading of all scenes, or to transfer parameter data between scenes, mark the node where the component is located as a __Persistent Node__ so that it will not be automatically destroyed when the scene is switched, and will remain in memory. Example: + +```ts +director.addPersistRootNode(myNode); +``` + +The above interface will turn `myNode` into a persistent node, so that the components attached to it can continue to function between scenes. This method can be used to store player information, or various things needed for the initialization of the next scene data. + +> **Note**: the target node must be the root node in the hierarchy, otherwise the setting is invalid. + +Cancelling the persistece of a node is easy. Example: + +```ts +director.removePersistRootNode(myNode); +``` + +> **Note**: the above API does not immediately destroy the specified node, but restores the node to a node that can be destroyed when the scene is switched. + +## Scene loading callback + +When loading a scene, you can attach a parameter to specify the callback function after the scene is loaded. Example: + +```ts +director.loadScene("MyScene", onSceneLaunched); +``` + +`onSceneLaunched` is a callback function declared in this script, and can be used for further initialization or data transfer operations after the scene is loaded. + +Since the callback function can only be written in this script, the scene loading callback is usually used in conjunction with the persistent node and used in the script mounted on the persistent node. + +## Preloading a scene + +`director.loadScene` will automatically switch to run the new scene after loading the scene. It is also possible to silently load the new scene in the background and switch manually after the loading is complete. Use the `preloadScene` interface to preload the scene in advance. Example: + +```ts +director.preloadScene("table", function () { + console.log('Next scene preloaded'); +}); +``` + +Then call `loadScene` at an appropriate time to switch scenes. Example: + +```ts +director.loadScene("table"); +``` + +> **Note**: if the preloading is not complete, you can also call `director.loadScene()` directly, and the scene will start after the preloading is complete. diff --git a/versions/4.0/en/scripting/scheduler.md b/versions/4.0/en/scripting/scheduler.md new file mode 100644 index 0000000000..e31436f686 --- /dev/null +++ b/versions/4.0/en/scripting/scheduler.md @@ -0,0 +1,77 @@ +# Scheduler + +__Cocos Creator__ provides a powerful, flexible and convenient timer component. + +## Using the Scheduler + +1. Start a timer + + ```ts + this.schedule(function() { + // Here this refers to component + this.doSomething(); + }, 5); + ``` + + The above timer will be executed every 5s. + +2. A more flexible timer + + ```ts + // time interval in seconds + let interval = 5; + // number of times to repeat + let repeat = 3; + // delay starts + let delay = 10; + this.schedule(function() { + // Here this refers to component + this.doSomething(); + }, interval, repeat, delay); + ``` + + The above timer will start timing after 10 seconds, and execute a callback every 5 seconds, repeating **3 + 1** times. + +3. Timer that executes only once (shortcut) + + ```ts + this.scheduleOnce(function() { + // Here this refers to component + this.doSomething(); + }, 2); + ``` + + The timer above will execute the callback function once after two seconds, and then stop timing. + +4. Cancel a timer + + Developers can use the callback function itself to cancel the timer: + + ```ts + this.count = 0; + this.callback = function () { + if (this.count == 5) { + // Cancel this timer when the callback is executed + // for the sixth time + this.unschedule(this.callback); + } + this.doSomething(); + this.count++; + } + this.schedule(this.callback, 1); + ``` + +> **Note**: when the component's timer calls the callback, the `this` of the callback is specified as the component itself, so `this` can be used directly in the callback. + +Here is a list of all of the timer functions in **Component**: + + - **schedule**: start a timer + - **scheduleOnce**: start a timer that is executed only once + - **unschedule**: cancel a timer + - **unscheduleAllCallbacks**: cancel all timers of this component + +The detailed description of these APIs can be found in the API documentation. + +In addition, if developers need to execute a function every frame, please add the `update` function directly to the Component. This function will be called every frame by default. This is described in [Lifecycle Document](life-cycle-callbacks.md#update). + +> **Note**: `Node` does not include timer related APIs diff --git a/versions/4.0/en/scripting/script-basics.md b/versions/4.0/en/scripting/script-basics.md new file mode 100644 index 0000000000..abe521c33f --- /dev/null +++ b/versions/4.0/en/scripting/script-basics.md @@ -0,0 +1,10 @@ +# Scripting Basics + +This section introduces some basic concepts, creation methods, running environments, etc. of scripts: + +- [Creating scripts](./setup.md) +- [Coding Environment Setup](./coding-setup.md) +- [Operating Environment](./basic.md) +- [Decorator](./decorator.md) +- [Property Attributes](./reference/attributes.md) +- [Life Cycle Callbacks](./life-cycle-callbacks.md) diff --git a/versions/4.0/en/scripting/setup.md b/versions/4.0/en/scripting/setup.md new file mode 100644 index 0000000000..cc475ce7e6 --- /dev/null +++ b/versions/4.0/en/scripting/setup.md @@ -0,0 +1,230 @@ +# Creating Scripts + +## Creating Component Scripts + +In Cocos Creator, scripts are part of assets. A script created in the **Assets** panel is a NewComponent by default, which is called a component script. It can be created in two ways: + +- Select the folder in the **Assets** panel where the component script is to be placed, then right-click and select **TypeScript** > **NewComponent**. +- Click the **+** button in the upper left corner of the **Assets** panel directly, and then select **TypeScript** > **NewComponent**. + +![create script](setup/create-script.png) + +When creating a script, the script name cannot be empty and defaults to `NewComponent`. If the created component script is named as `say-hello`, and then a script file named `say-hello` is generated in the **Assets** panel: + +![say-hello-1](setup/say-hello-1.png) + +![say-hello-2](setup/say-hello-2.png) + +A simple component script may look like this example: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('say_hello') +export class say_hello extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} +``` + +> **Notes**: +> +> 1. The class name `ClassName` (such as 'say_hello' in the above example) of all scripts in the project is not allowed to be repeated. Even if the script files are in different directories, the same class name is not allowed in their respective codes. +> +> 2. The name of the script file is different from the class name of the script. After the initial file name is entered, the file name will be processed as the class name. For details of the processing logic, please refer to **Class Name Generation** below. After the script file is generated, the subsequent operation of the file **renames the script**, the new file name will not be generated and replaced by the class name in the code, and will no longer be affected. +> +> 3. We recommend that users use TypeScript to write scripts. Currently, only TypeScript files are supported in **Assets**. But if users want to use JavaScript to write scripts, they can create JavaScript files directly in the operating system's folder, or create JavaScript files in other code editing software. + +## Class Name Generation + +After obtaining the initial file name data, the class name `ClassName` of the two rules will be generated and provided to the **Custom Script Template** in the form of variables. + +- Underscore format, variable name is `<%UnderscoreCaseClassName%>`. This format is to keep the class name as consistent as possible with the file name. The advantage of being consistent is that it facilitates global search and replacement of code. +- CamelCase format, the variable name is `<%CamelCaseClassName%>`. This format is to maintain consistency with mainstream scripting standards, with capitalized camel case. + +## Add a Script to a Scene Node + +Adding a script to a scene node is actually adding a script component to that node. Select the scene node to which you wish to add a script in the **Hierarchy** panel, at which point the properties of that node will be displayed in the **Inspector** panel. Adding a script component includes the following two ways: + +- Drag and drop the script from **Assets** panel directly into the **Inspector** panel. + +- Click the **Add Component** button at the bottom of the **Inspector** panel and select **Custom script -> say_hello** to add the script component just created. It is also possible to add it by searching for **say_hello** in the **Add Component** search box. + + ![add component](setup/add-component.png) + ![add component done](setup/add-component-done.png) + +## Editing Scripts + +Choose a favorite text-editing tool (such as: **Vim**, **Sublime Text**, **Web Storm**, **VSCode**...) for script editing, please setup in the **Preferences** > **External Program -> Default Script Editor** option of the editor menu bar. + +![preference script editor](setup/preference-script-editor.png) + +By double-clicking the script asset, the script editor directly opens to allow for editing. When the script is edited and saved, then returned to the editor, Cocos Creator will automatically detect the changes to the script and compile it quickly. + +Before writing code, please read the following documentations to learn more about scripts: + +- [Coding Environment Setup](coding-setup.md) +- [Operating Environment](basic.md) + +After the script file is successfully created, rename the file or modify the class name in the code, the file name and class name will no longer affect each other. + +- Take `say-hello` as an example, we renamed it to `hello` in **Asset**. +Re-select it and check the **Inspector**, the code still displays `class say_hello`, which will not change. +Re-select the node **Node** where the component was just added on the **Hierarchy**, check the **Inspector**, the component name still displays `say_hello`, and it will not change. + +We continue to double-click the current `hello` asset, change the class name to **say**, save it and return to the editor: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('say') +export class say extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} +``` + +The script file name `hello` will not change. The component name in the node **Node** becomes **say**. + +> **Note**: don't forget to change the decorator `@ccclass('say_hello')` to @ccclass('say') . + +![modify script](setup/modify-script.png) + +##
Custom Script Template + +Starting from Cocos Creator v3.3, there is support for managing different script templates in a project. + +- Create a new project, the new project will not automatically create the directory `.creator/asset-template/typescript` where the custom script templates are located. +- The above directory can be created manually. You can also use the menu in the right-click menu of **Asset** to generate a directory after clicking. + + ![custom script](setup/custom-script.png) + +The default `NewComponent` script template is still in the engine built-in resources directory `resources\3d\engine\editor\assets\default_file_content\ts`. +The code of the file is below: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('<%UnderscoreCaseClassName%>') +export class <%UnderscoreCaseClassName%> extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} + +/** + * COMMENTS_GENERATE_IGNORE + * Use "COMMENTS_GENERATE_IGNORE" tag if you do not want later created scripts to contain these comments. + * + * Predefined Variables + * You can use predefined variables below to setup your scripting preference. For example, whether to use camel case style. + * + * <%UnderscoreCaseClassName%>, class name in underscore format, like 'new_component' + * <%CamelCaseClassName%>, class name in camel format, like 'NewComponent' + * <%Author%>, Who create this file + * <%DateTime%>, when create this file + * <%FileBasename%>, creating file name with extension + * <%FileBasenameNoExtension%>, creating file name without extension + * <%URL%>, url of this file in COCOS ASSET URL format + * <%ManualUrl%>, url of office help document, like 'https://docs.cocos.com/creator/manual/en/' + * + * + * Example: + * + @ccclass('<%UnderscoreCaseClassName%>') + export class <%UnderscoreCaseClassName%> extends Component { + + // class member could be defined like this. + dummy = ''; + + // Use 'property' decorator if your want the member to be serializable. + @property + serializableDummy = 0; + + start () { + // Your initialization goes here. + } + + update (deltaTime: number) { + // Your update function goes here. + } + + } + * + * Learn more about scripting: <%ManualUrl%>scripting/ + * Learn more about CCClass: <%ManualUrl%>scripting/decorator.html + * Learn more about life-cycle callbacks: <%ManualUrl%>scripting/life-cycle-callbacks.html + */ + +``` + +> **Nodes**: +> +> 1. A large number of comments in the script template will not be generated into the script file, because we use the keyword annotation `COMMENTS_GENERATE_IGNORE` in the comment. As long as this keyword is in a comment, the generated script file will ignore the comment Lose. +> +> 2. `Predefined Variables` We have prepared some pre-made variables, which can be used as auxiliary information when generating script files, such as author `<%Author%>`. +> +> 3. By right-clicking the menu, a shortcut link to the document URL will be automatically generated in the project custom script template directory. Double-click to call up the browser to open the specified webpage. `Custom Script Template Help Documentation` +> +> ![custom script help](setup/custom-script-help.png) + +### Make One Script Template + +We modify the code from copying the above built-in `NewComponent` template, the class name is in camel case format, prefixed with `Robot`, the file is saved as a file `CustomComponent` without extension name, and saved in the path of the project custom script template, that is `.creator/asset-template/typescript/CustomComponent`. + +![custom script file](setup/custom-script-file.png) + +The code of `CustomComponent` template is below: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +/** + * + * <%UnderscoreCaseClassName%> + * <%CamelCaseClassName%> + * <%Author%> + * <%DateTime%> + * <%FileBasename%> + * <%FileBasenameNoExtension%> + * <%URL%> + * <%ManualUrl%> + * + */ + +@ccclass('Robot<%CamelCaseClassName%>') +export class Robot<%CamelCaseClassName%> extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} + +``` + +Then finally, let's create a new `wake up` script asset to see the effect as shown below: + +![custom script menu](setup/custom-script-menu.png) + +![custom script result](setup/custom-script-result.png) diff --git a/versions/4.0/en/scripting/setup/add-component-done.png b/versions/4.0/en/scripting/setup/add-component-done.png new file mode 100644 index 0000000000..821ff5fc93 Binary files /dev/null and b/versions/4.0/en/scripting/setup/add-component-done.png differ diff --git a/versions/4.0/en/scripting/setup/add-component.png b/versions/4.0/en/scripting/setup/add-component.png new file mode 100644 index 0000000000..df2164a988 Binary files /dev/null and b/versions/4.0/en/scripting/setup/add-component.png differ diff --git a/versions/4.0/en/scripting/setup/create-script.png b/versions/4.0/en/scripting/setup/create-script.png new file mode 100644 index 0000000000..f7a5c1eb42 Binary files /dev/null and b/versions/4.0/en/scripting/setup/create-script.png differ diff --git a/versions/4.0/en/scripting/setup/custom-script-file.png b/versions/4.0/en/scripting/setup/custom-script-file.png new file mode 100644 index 0000000000..ddcb7d97d0 Binary files /dev/null and b/versions/4.0/en/scripting/setup/custom-script-file.png differ diff --git a/versions/4.0/en/scripting/setup/custom-script-help.png b/versions/4.0/en/scripting/setup/custom-script-help.png new file mode 100644 index 0000000000..a7a3d772c4 Binary files /dev/null and b/versions/4.0/en/scripting/setup/custom-script-help.png differ diff --git a/versions/4.0/en/scripting/setup/custom-script-menu.png b/versions/4.0/en/scripting/setup/custom-script-menu.png new file mode 100644 index 0000000000..ce620765a1 Binary files /dev/null and b/versions/4.0/en/scripting/setup/custom-script-menu.png differ diff --git a/versions/4.0/en/scripting/setup/custom-script-result.png b/versions/4.0/en/scripting/setup/custom-script-result.png new file mode 100644 index 0000000000..428fd11345 Binary files /dev/null and b/versions/4.0/en/scripting/setup/custom-script-result.png differ diff --git a/versions/4.0/en/scripting/setup/custom-script.png b/versions/4.0/en/scripting/setup/custom-script.png new file mode 100644 index 0000000000..3cd733d165 Binary files /dev/null and b/versions/4.0/en/scripting/setup/custom-script.png differ diff --git a/versions/4.0/en/scripting/setup/modify-script.png b/versions/4.0/en/scripting/setup/modify-script.png new file mode 100644 index 0000000000..d756d9fb62 Binary files /dev/null and b/versions/4.0/en/scripting/setup/modify-script.png differ diff --git a/versions/4.0/en/scripting/setup/preference-script-editor.png b/versions/4.0/en/scripting/setup/preference-script-editor.png new file mode 100644 index 0000000000..9ea71e9704 Binary files /dev/null and b/versions/4.0/en/scripting/setup/preference-script-editor.png differ diff --git a/versions/4.0/en/scripting/setup/say-hello-1.png b/versions/4.0/en/scripting/setup/say-hello-1.png new file mode 100644 index 0000000000..ad4fb7725e Binary files /dev/null and b/versions/4.0/en/scripting/setup/say-hello-1.png differ diff --git a/versions/4.0/en/scripting/setup/say-hello-2.png b/versions/4.0/en/scripting/setup/say-hello-2.png new file mode 100644 index 0000000000..2c7bc10bde Binary files /dev/null and b/versions/4.0/en/scripting/setup/say-hello-2.png differ diff --git a/versions/4.0/en/scripting/tsconfig.md b/versions/4.0/en/scripting/tsconfig.md new file mode 100644 index 0000000000..f44bf95403 --- /dev/null +++ b/versions/4.0/en/scripting/tsconfig.md @@ -0,0 +1,62 @@ +# `tsconfig.json` + +The **majority** of compilation options in `tsconfig.json` in the project do not affect the compilation of **TypeScript**. + +Therefore, some options need to configured carefully to make the IDE's inspection function consistent with the compilation behavior in __Cocos Creator__. + +The following options should **not** be modified explicitly: + +- `compilerOptions.target` +- `compilerOptions.module` + +For example, if `tsconfig.json` is set to: + +```json +{ + "compilerOptions": { + "target": "es5", + "module": "cjs" + } +} +``` + +Then the following script code will not cause an error in the IDE (which uses `tsc` as a checker), because `compilerOptions.module` is set to `cjs`. + +```ts +const myModule = require("path-to-module"); +``` + +However, the implied `compilerOptions.module` in Creator is `es2015`, therefore it may prompt errors such as "require undefined" at runtime. + +The following script code is legal for Creator, but the IDE may report an error. This is because `compilerOptions.target` is set to `es5`, while `Set` was introduced in ES6. + +```ts +const mySet = new Set(); +``` + +---- + +It is also possible to freely modify options. + +For example, when it is needed to prohibit the use of implicit `any` in all TypeScript scripts in your project. + +Set `compilerOptions.noImplicitAny` to `true` in `tsconfig.json`, as using an IDE (such as Visual Studio Code), the corresponding error prompt will be received. + +---- + +For most projects, some options in `tsconfig` are fixed. For example, `compilerOptions.target`, `compilerOptions.module` and __Cocos Creator__ type declarations, file location, etc. + +Due to the good design of `tsc`, the `extends` option allows `tsconfig.json` to be cascadable. __Cocos Creator__ supports this, therefore, the fixed `tsconfig` option is placed under `{project path}/tmp/tsconfig.cocos.json` and managed by __Cocos Creator__. + +Therefore, `tsconfig.json` under the project root path can be configured as follows to share these fixed options: + +```json5 +{ + extends: './tmp/tsconfig.cocos.json', + compilerOptions: { + /* Custom tsconfig.json options*/ + } +} +``` + +Fortunately, Creator will automatically generate such a `tsconfig.json` file when creating a new project. diff --git a/versions/4.0/en/scripting/usage.md b/versions/4.0/en/scripting/usage.md new file mode 100644 index 0000000000..fe964e173c --- /dev/null +++ b/versions/4.0/en/scripting/usage.md @@ -0,0 +1,12 @@ +# Using Scripts + +This section focuses on how to use scripts in the project: + +- [Access Nodes and Components](./access-node-component.md) +- [Common Node and Component Interfaces](./basic-node-api.md) +- [Create and Destroy Nodes](./create-destroy.md) +- [Scheduler](./scheduler.md) +- [Components and Component Execution Order](./component.md) +- [Loading and Switching Scenes](./scene-managing.md) +- [Obtaining and Loading Assets](./load-assets.md) +- [`tsconfig` Configuration](./tsconfig.md) diff --git a/versions/4.0/en/shader/advanced-shader/skin.md b/versions/4.0/en/shader/advanced-shader/skin.md new file mode 100644 index 0000000000..77cf8a91e2 --- /dev/null +++ b/versions/4.0/en/shader/advanced-shader/skin.md @@ -0,0 +1,41 @@ +# Skin material + +In the game skin, if the use of ordinary BRDF material is often not enough red and transparent performance because the real skin structure is more complex, we need to refer to subsurface scattering, restore the skin SSS effect, and make the performance level more rich. + +## Use of skin material + +1. To use skin material, you need to turn on the custom pipeline. For detailed steps, please refer to [Custom rendering of pipelines](../../render-pipeline/custom-pipeline.md#feature-enable). + +2. Using skin materials requires some additional macro definition configuration. Open the **Project Settings** panel by clicking on the **Project** menu, and navigate to the **Macro Configurations** page. + + - Check the **ENABLE_FLOAT_OUTPUT** option. If unchecked, the engine will also automatically detect if a skin material is available each time it starts and change this option to guarantee the correct effect, but a warning message will be generated. + + ![image](skin/define.png) + +3. To use the skin material you need to add **skin** material to the model: + + - Create a new material. To create a new material, please refer to [Material Assets](../../asset/material.md#material-creation) + - Select **advanced/skin** for **effect** in the material. + + ![image](skin/effect.png) + +4. Specify a model as a global **skin** model: **Optional, usually specifying the model corresponding to the head or body**. + The engine needs to know the scale of the model corresponding to the skin material in order to calculate the skin scattered light correctly. If no model is checked, the model with the skin material will be automatically selected for calculation. + + - Select the **Node** of the skin model, find the **cc.MeshRenderer** component in the **Inspector** panel, and check the **Is Global Standard Skin Object** option in the component configuration menu. + + > **Note**: **Is Global Standard Skin Object** can only be checked in one render component in the scene. Repeatedly checking in other rendering components will invalidate the previous check. + + ![image](skin/MeshRenderPanel.png) + +## Adjustment of **skin** parameters + +1. **skin** configuration in the global panel. + + - Adjust the skin effect in the **skin** panel. Skin configuration panel please refer to [skin configuration panel](../../concepts/scene/skin.md). + +2. **skin** configuration in materials. + + - Subcutaneous Scattering Intensity: This value controls the skin's moistness and subcutaneous scattering intensity, which produces a reddish scattering effect in the transition areas between light and dark. When adjusting the skin material intensity for a single model, you can find **Sss Intensity** in the **Inspector** panel of the skin material to set it, or you can add a **USE SSS MAP** map to the skin material to precisely control the intensity of the skin effect for each organ of the face. + + ![image](skin/material.png) diff --git a/versions/4.0/en/shader/advanced-shader/skin/CustomRenderPipeline.png b/versions/4.0/en/shader/advanced-shader/skin/CustomRenderPipeline.png new file mode 100644 index 0000000000..fc6fbe5402 Binary files /dev/null and b/versions/4.0/en/shader/advanced-shader/skin/CustomRenderPipeline.png differ diff --git a/versions/4.0/en/shader/advanced-shader/skin/MeshRenderPanel.png b/versions/4.0/en/shader/advanced-shader/skin/MeshRenderPanel.png new file mode 100644 index 0000000000..5d29ca2545 Binary files /dev/null and b/versions/4.0/en/shader/advanced-shader/skin/MeshRenderPanel.png differ diff --git a/versions/4.0/en/shader/advanced-shader/skin/define.png b/versions/4.0/en/shader/advanced-shader/skin/define.png new file mode 100644 index 0000000000..9aaf06ad11 Binary files /dev/null and b/versions/4.0/en/shader/advanced-shader/skin/define.png differ diff --git a/versions/4.0/en/shader/advanced-shader/skin/effect.png b/versions/4.0/en/shader/advanced-shader/skin/effect.png new file mode 100644 index 0000000000..b77610243f Binary files /dev/null and b/versions/4.0/en/shader/advanced-shader/skin/effect.png differ diff --git a/versions/4.0/en/shader/advanced-shader/skin/material.png b/versions/4.0/en/shader/advanced-shader/skin/material.png new file mode 100644 index 0000000000..3f6837e4f7 Binary files /dev/null and b/versions/4.0/en/shader/advanced-shader/skin/material.png differ diff --git a/versions/4.0/en/shader/common-functions.md b/versions/4.0/en/shader/common-functions.md new file mode 100644 index 0000000000..a9dc6dfa24 --- /dev/null +++ b/versions/4.0/en/shader/common-functions.md @@ -0,0 +1,28 @@ +# Common Functions + +You can find these functions in the **Assets/internal/chunks/common/** directory. + +These functions do not depend on any internal data and can be used directly as utility functions. + +## How to use + +```ts +#include +#include +``` + +## Directory Usage Description + +| Directory | Usage | Main Files +| -------- | ---------------------------------------- | ---- +| color | Color-related functions (color space,tone-mapping, etc) | aces, gamma +| data | Data-related functions(compression, decompression, etc) | packing, unpack +| debug | Functions related to Debug View | +| effect | Scene effect related functions(water,fog,etc) | fog +| lighting | Lighting-related functions(bxdf,reflection,attenuation,baking,etc) | brdf, bxdf, light-map +| math | Math lib(coordinates transformations,numerical determination, calculations, etc) | coordinates, transform +| mesh | Mesh-related functions(mesh,material conversion,animations,etc) | material, vat-animation +| shadow | Shadow-related functions(PCF,PCSS,etc) | native-pcf +| texture | Texture-related functions(sampling,mipmap calculations,etc) | cubemap, texture-lod + +> The Surface Shader already automatically includes the commonly used public function header files internally, so there is no need to include them again. \ No newline at end of file diff --git a/versions/4.0/en/shader/compute-shader.md b/versions/4.0/en/shader/compute-shader.md new file mode 100644 index 0000000000..cd4424fa4a --- /dev/null +++ b/versions/4.0/en/shader/compute-shader.md @@ -0,0 +1,261 @@ +# Compute Shader + +Compute Shader (CS) is a programming model for executing general-purpose computing tasks on a GPU. Cocos Compute Shader inherits the syntax and built-in variables of glsl, and is added in the same way as rasterization Shaders, presented in effect form, and can only be used in custom render pipelines. Compute shader uses multiple threads to achieve parallel processing, making it highly efficient when dealing with large amounts of data. + +## Syntax + +The definition method is the same as that of rasterization Shader shown below. Configuring PipelineStates under Computer Shader is meaningless. + +``` +CCEffect %{ +  techniques: +  - name: opaque +    passes: +    - compute: compute-main // shader entry +      pass: user-compute // pass layout name +      properties: &props +        mainTexture: { value: grey } // material properties +}% +CCProgram compute-main %{ +  precision highp float; + precision mediump image2D; + layout(local_size_x = 8, local_size_y = 4, local_size_z = 1) in; + + #pragma rate mainTexture batch + uniform sampler2D mainTexture; + + #pragma rate outputImage pass + layout (rgba8) writeonly uniform image2D outputImage; + +  void main () { +    imageStore(outputImage, ivec2(gl_GlobalInvocationID.xy), vec4(1, 0, 0, 1)); +  } +}% +``` + +For more, please refer to [Shader Syntax](./effect-syntax.md). + +## Input / Output + +Compute Shader input and output consist of built-in input variables and Shader Resource variables. + +The built-in input includes: + +```c +in uvec3 gl_NumWorkGroups; +in uvec3 gl_WorkGroupID; +in uvec3 gl_LocalInvocationID; +in uvec3 gl_GlobalInvocationID; +in uint gl_LocalInvocationIndex; +layout(local_size_x = X, local_size_y = Y, local_size_z = Z) in; +``` + +Shader Resource includes: +- UniformBuffer +- StorageBuffer +- ImageSampler +- StorageImage +- SubpassInput + +CS has no built-in output, and output can be achieved through StorageBuffer/Image. + +## Shader resource declaration + +Compute shader currently supports resource binding at two frequencies: **PerPass** and **PerBatch**, as shown below: + +```glsl +#pragma rate mainTexture batch +uniform sampler2D mainTexture; + +#pragma rate outputImage pass +layout (rgba8) writeonly uniform image2D outputImage; +``` + +PerPass resources can be defined as resources that require pipeline tracking to handle synchronization, while PerBatch resources are typically constant data or static textures that can be bound through Material. + +The PerBatch `mainTexture` can be configured in the Material panel. + +The PerPass `outputImage` needs to be declared in the pipeline and referenced by ComputePass, and the data read/write synchronization and ImageLayout management need to be managed by RenderGraph. Please see below for details. + +## Pipeline integration + +Adding a Compute Shader in the Custom Render Pipeline involves three steps: + +1. 1.Add a Compute Pass, where `passName` is the Layout Name of the current Pass and must correspond to the pass field in the Effect. + + ```ts + const csBuilder = pipeline.addComputePass('passName'); + ``` + +2. Declare and reference resources, set access types and associate shader resources. + + ```ts + const csOutput = 'cs_output'; + if (!pipeline.containsResource(csOutput)) { + pipeline.addStorageTexture(csOutput, + gfx.Format.RGBA8, + width, height, + rendering.ResourceResidency.MANAGED); + } else { + pipeline.updateStorageTexture(csOutput, + width, height, + gfx.Format.RGBA8); + } + + csBuilder.addStorageImage(csOutput, // resource name + rendering.AccessType.WRITE, // access type + 'outputImage'); // shader resource name + ``` + +3. Add a dispatch call and set Compute material. + + ```ts + csBuild.addQueue().addDispatch(x, y, z, rtMat); + ``` + +## Cross-platform support + +### Feature + +| | WebGL | WebGL2 | Vulkan | Metal | GLES3 | GLES2 | +| :------ | :---- | :----- | :----- | :---- | :----- | :---- | +| support | N | N | Y | Y | Y(3.1) | N | + +It can be queried through `device.hasFeature(gfx.Feature.COMPUTE_SHADER)`. + +### Limitation + +- `maxComputeSharedMemorySize`: maximum total shared storage size, in bytes. +- `maxComputeWorkGroupInvocations`: maximum total number of compute shader invocations in a single local workgroup. +- `maxComputeWorkGroupSize`: maximum size of a local compute workgroup. +- `maxComputeWorkGroupCount`: maximum number of local workgroups that can be dispatched by a single dispatching command. + +It can be queried through `device.capabilities`. + +### Platform-specific differences + +Cocos Creator will convert the Cocos Compute Shader into platform-specific versions of GLSL shaders. Therefore, to ensure compatibility across different platforms, it is necessary to meet the limitation requirements of all platforms as much as possible, including: +1. In Vulkan and GLES, it is required to explicitly specify the format identifier for Storage Image, according to the GLSL specification. +2. GLES requires explicit specification of the Memory identifier for Storage resources, and currently only supports "readonly" and "writeonly". In addition, default precision must be explicitly specified. + +### Best Practices + +1. When performing screen-space image post-processing, it is recommended to prioritize the use of Fragment Shader. +2. It is recommended to avoid using large work groups, especially when using shared memory. The size of each work group should not exceed 64. + +## Sample Code + +The following code demonstrates a simple ray tracing shader using a single sphere with 1 ray per pixel, implemented through ComputePass. It uses UniformBuffer, ImageSampler, and StorageImage. + +Shader Pass Declaration: + +```yaml +techniques: +- name: opaque + passes: + - compute: compute-main + pass: user-ray-tracing + properties: &props + mainTexture: { value: grey } +``` + +`compute-main` implement: + +```c +precision highp float; +precision mediump image2D; + +layout(local_size_x = 8, local_size_y = 4, local_size_z = 1) in; + +#pragma rate tex batch +uniform sampler2D tex; + +#pragma rate constants pass +uniform constants { + mat4 projectInverse; +}; + +#pragma rate outputImage pass +layout (rgba8) writeonly uniform image2D outputImage; + +void main () { + vec3 spherePos = vec3(0, 0, -5); + vec3 lightPos = vec3(1, 1, -3); + vec3 camPos = vec3(0, 0, 0); + float sphereRadius = 1.0; + + vec4 color = vec4(0, 0, 0, 0); + + ivec2 screen = imageSize(outputImage); + ivec2 coords = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); + vec2 uv = vec2(float(coords.x) / float(screen.x), float(coords.y) / float(screen.y)); + + vec4 ndc = vec4(uv * 2.0 - vec2(1.0), 1.0, 1.0); + vec4 pos = projectInverse * ndc; + vec3 camD = vec3(pos.xyz / pos.w); + vec3 rayL = normalize(camD - camPos); + + vec3 dirS = spherePos - camPos; + vec3 rayS = normalize(dirS); + float lenS = length(dirS); + + float dotLS = dot(rayL, rayS); + float angle = acos(dotLS); + float projDist = lenS * sin(angle); + + if (projDist < sphereRadius) { + // intersection + vec3 rayI = rayL * (lenS * dotLS - sqrt(sphereRadius * sphereRadius - projDist * projDist)); + + vec3 N = normalize(rayI - dirS); + vec3 L = normalize(lightPos - rayI); + color = vec4(vec3(max(dot(N, L), 0.05)), 1.0); + } + + imageStore(outputImage, coords, color); +} +``` + +On the API side, it is as follows: + +```ts +export function buildRayTracingComputePass( + camera: renderer.scene.Camera, + pipeline: rendering.Pipeline) { + // Get the screen width and height. + const area = getRenderArea(camera, + camera.window.width, + camera.window.height); + const width = area.width; + const height = area.height; + + // Declare the Storage Image resource. + const csOutput = 'rt_output'; + if (!pipeline.containsResource(csOutput)) { + pipeline.addStorageTexture(csOutput, + gfx.Format.RGBA8, + width, height, + rendering.ResourceResidency.MANAGED); + } else { + pipeline.updateStorageTexture(csOutput, + width, height, + gfx.Format.RGBA8); + } + + // Declare Compute Pass, the layout needs to be consistent + const cs = pipeline.addComputePass('user-ray-tracing'); + // Update the camera projection parameters. + cs.setMat4('projectInverse', camera.matProjInv); + // Declare the reference of the Storage Image in the current Compute Pass. + cs.addStorageImage(csOutput, rendering.AccessType.WRITE, 'outputImage'); + // Add Dispatch parameters and bind Material + cs.addQueue() + .addDispatch(width / 8, height / 4, 1, rtMat); + // Return the name of the current Image resource, which will be used for subsequent Post Processing. + return csOutput; +} +``` + +Users need to update and bind PerPass resources in Compute Pass, while PerBatch resources will be bound by the material system. The final effect after presenting is as follows: + +![Cocos Effect](img/compute-shader-rt.png) diff --git a/versions/4.0/en/shader/effect-builtin-pbr.md b/versions/4.0/en/shader/effect-builtin-pbr.md new file mode 100644 index 0000000000..30dac1a171 --- /dev/null +++ b/versions/4.0/en/shader/effect-builtin-pbr.md @@ -0,0 +1,198 @@ +# Physically Based Rendering + +Cocos Creator provides a physically based rendering (PBR) shader since v3.0: `builtin-standard.effect`. PBR simulates the effect of realistic physical lighting based on the principle of realistic light propagation and the law of energy conservation. + +The advantages of PBR are: + +- Realism: physics-based rendering makes the final effect more realistic +- Consistency: standardized art production process and unified production standards +- Reusability: model materials are separated from the lighting environment and can be reused in all PBR projects + +## Creating materials and textures with PBR + +Materials created manually in the **Assets** panel use the `builtin-standard.effect` shader by default, which we call the PBR material, which uses the Metal/Roughness workflow of the PBR process. + +When rendering with PBR materials, you need to set at least **Albedo**, **Roughness** and **Metallic** of the material in order to get the correct rendering. These can be set in the Material Resource Inspector panel. + +![image](./img/pbr-maps.png) + +In addition to assigning values directly in the material properties panel, you can also assign **textures** to the material's Albedo, Roughness, and Metallic for more accurate material representation. In addition, you can assign Normal maps for more surface structure details, Ambient Occlusion maps for detail relationships, and Emissive maps for emissive effects. + +Let's take the following picture as an example to see the effect obtained by setting the above material parameters. + +![flakes.jpg](./img/final_alarmclock.jpg) + +### Albedo + +![flakes.jpg](./img/albedo.jpg) + +Albedo is used to express the color information expressed by a material in the absence of light. Aesthetically, intrinsic color can be interpreted as the color information expressed by a material when viewed with the naked eye. + +In the PBR process, the Albedo represents the set of **Diffuse** colors of the **Non-Metallic** part of the material and **Specular** colors of the **Metallic** part of the material. + +> **Note**: In the Metal / Roughness workflow, the Diffuse color of all metals is black, and the color representation seen by the naked eye is produced by the reflected light from the metal. In the non-Metal / Roughness workflow, the color of the metal is determined by the Specular color. Color maps made with non-Metal / Roughness workflows will not render the metal's color information correctly in the Creator's default PBR material. + +Users can set the Albedo directly in the **Inspector** property of the material properties panel, or you can check `USE ALBEDO MAP` to assign an RGBA map of the sRGB color space to the material. + +![albedo](img/albedo-map.png) + +According to the production guidelines of the standard PBR process, in order to obtain a physically realistic rendering, it is important to note during the creation of the intrinsic color mapping that +- The intrinsic color sRGB values should not exceed a maximum of **240** and a minimum of **30~50** depending on the material. +- When expressing the intrinsic color of metals, the physical law of **70%~100%** specular reflectance of metals should be followed, and the sRGB values should be in the range of **180~255**. + +### Roughness + +![flakes.jpg](./img/roughness.jpg) + +Roughness is used to express the degree of reflectivity of a material due to the fine structural details of its surface and takes values in the range [0, 1]. + +- When Roughness is set to 0, the material surface is absolutely smooth with 100% specular reflectivity. +- When the roughness is set to 1, the material surface is absolutely rough and the specular reflectivity is 0%. + +You can set the roughness directly in the **Roughness** property of the Material Properties panel. Or assign an RGBA map in the sRGB color space to the PBR material and express the roughness through the **Green Channel** of the map, as follows. + +1. select the PBR material resource in **Assets** panel, then check the **USE PBR MAP** property in the **Inspector** panel and drag the RGBA map to the **PbrMap** property box that appears. +2. then check the **USE METALLIC ROUGHNESS MAP** and drag the RGBA map to the **MetallicRoughnessMap** property box that appears again. + +![metallic](img/use-pbr-map.png) + +### Metallic + +![flakes.jpg](./img/metallic.jpg) + +Metallic is used to express the metallic properties of a material and has a value range of [0, 1]. In use, it is usually set to 0 or 1. + +- When the metallic is **0**, the material is non-metallic. +- When the metallic is **1**, the material is metal. +- When the metallic is a floating point number between **0 and 1**, it is usually used to indicate metals with non-metallic dirt on the surface. + +> **Note**: When the metallic is set to 1, the material is recognized as metal. This is because as the metallic increases, the material undergoes the following changes, which in turn causes it to exhibit metallic characteristics. +> +> - the inherent color is less bright and less saturated than when the metallic is 0. +> - the color of the highlights of the material mixes with the material's intrinsic color. +> - the reflected light more closely resembles the intrinsic color. + +You can set the metallic directly in the **Metallic** property of the material properties panel. Or assign an RGBA map in the sRGB color space to the material and express the metallic through the **Blue Channel** of the map as follows. + +1. select the PBR material resource in the **Assets** panel, then check the **USE PBR MAP** property in the **Inspector** panel and drag the RGBA map into the **PbrMap** property box that appears. +2. then check the **USE METALLIC ROUGHNESS MAP** and drag the RGBA map to the **MetallicRoughnessMap** property box that appears again. + +![roughness](img/metallicRoughnessMap.png) + +### Ambient Occlusion + +![flakes.jpg](./img/ao.jpg) + +Ambient Occlusion is used to express the relationship between light and dark in a material due to the structural details of the surface. Aesthetically, ambient occlusion can be understood as the shadows created by the model's own structure. + +You can assign an RGBA map in the sRGB color space to the PBR material to express the ambient light masking relationship through the **Red Channel** of the map, as follows. + +1. select the PBR material resource in **Assets** panel, then check the **USE PBR MAP** property in **Inspector** panel and drag and drop the RGBA map into the **PbrMap** property box that appears. +2. then check the **USE METALLIC ROUGHNESS MAP** and drag and drop the RGBA map into the **MetallicRoughnessMap** property box that appears. +3. check the **USE OCCLUSION MAP** and drag the RGBA map to the **OcclusionMap** property box that appears. + +![occlusion](img/occluation.png) + +### Normal + +![flakes.jpg](./img/normal.jpg) + +A normal map is a map that uses RGB values in the sRGB color space to represent the vertex coordinate positions in the model's tangent space. Its function is to superimpose the vertex coordinate data in the map onto the model's own vertex coordinate data to participate in the calculation of PBR light and shadow, so that the low model with low vertex count can also express the light and shadow change effect of the high model with high vertex count. Aesthetically, a normal map can be interpreted as a map that expresses the details of the surface structure of an object. + +Normal maps are usually created in two ways. +1. create a high-mode with a high vertex count and a low-mode with a low vertex count, and bake the vertex coordinate data of the high-mode onto a map that uses the UV of the low-mode +2. Convert the type of an image resource to a normal map + + ![Normal map](img/normal-map.png) + +> **NOTE**: When baking normals from a high mode, make sure the baker uses a right-handed coordinate system (Y-axis up) and the MIKK tangent space algorithm. + +### Emissive + +Emissive color expresses the color information of the material itself as a light source emitting light to the outside. + +You can set the emissive directly in the **Emissive** property of the Material Properties panel. Or assign an RGBA map in the sRGB color space to the PBR material, as follows. + +1. check the PBR material resource in **Assets** panel, then check the **USE EMISSIVE MAP** property in **Inspector** panel and drag the RGBA map into the **EMISSIVEMAP** property box that appears. + +2. then adjust the luminous intensity of the red, green and blue channels of the self-luminous color by using the **EmissiveScale** property. + + > **Note**: The **EmissiveScale** property is usually set to **1** or higher for use with emissive. When **EmissiveScale** is set to 1, the emissive effect of the material is equivalent to that of the `builtin-unlit.effect` material. + +![Emissive](img/emission-map.png) + +### Stencil + +![flakes.jpg](./img/leaves.jpg) + +When rendering a material that uses a stencil mask (Stencil), you can turn on the Alpha Test function of the PBR material to remove the pieces outside the mask. The steps are as follows: + +- Store the stencil mask (Stencil) as an **Alpha channel** or **Red channel** in the Intrinsic color map. +- Create a new material in the **Assets** panel and mount the intrinsic color map to the new material, referring to the **INTRINSIC COLOR** section above. +- Check **USE ALPHA TEST** and select the Alpha channel (corresponding to the **a** item in the properties) or the Red channel (corresponding to the **r** item in the properties) where the stencil mask (Stencil) is located in the **ALPHA TEST CHANNEL** property displayed below. +- Select the channel (Alpha channel or Red channel) where the template mask (Stencil) is located in the `ALPHA TEST CHANNEL` parameter. +- adjusting the threshold for discarding the meta-brightness of the slice via the **AlphaThreshold** property. +- If needed, refer to the above to implement effects such as normal and ambient light masking. + +![Stencil buffer](img/alpha-cutoff.png) + +### Transparency materials + +The **Technique** property in the Material Properties panel is used for rendering transparent or translucent materials. When set to **1-transparent**, the Alpha Blending feature is turned on, switching to rendering transparent material mode. + +![transparency](img/transparency.png) + +When switching to transparent material mode, all the functions of the material are no different from opaque mode. Users can follow the above workflow to create materials. + +Since the engine's rendering pipeline's control of depth changes when Alpha Blending is on, **you need to check the `DepthWrite` parameter under PipelineStates -> DepthStencilState in the Material Properties panel when switching to transparent material mode.** + +![depth-write](./img/depth-write.png) + +### PBR Main parameters assembly process + +![pbr process](../material-system/standard-material-graph.png) + +## PBR Parameters + +Open `builtin-standard.effect` in the `internal -> effects` directory in the **Inspector** panel in the external code editor and you can see the following parameters: + +![PBR params](img/pbr-param.png) + +| Parameter | Description | +| :------- | :--- | +| tilingOffset | Scaling and offset of the model UV, xy corresponds to scaling, zw corresponds to offset | +| mainColor | Used to specify the model's albedo or albedo map, the name of which appears as **albedo** in the **Inspector**. | +| albedoMap/mainTexture | Albedo map/texture, if specified, this will be multiplied by the albedo color | +| albedoScale | Albedo intensity of the model, used to control the weight of the albedo color influence on the final color | +| alphaThreshold | Alpha test threshold, pixels with alpha values below this value will be discarded | +| normalMap | Normal mapping for adding surface detail | +| normalStrength | Normal mapping intensity to control the strength of bumpy texture | +| pbrMap
**R**(AO)
**G**(Roughness)
**B**(Metallic) | PBR material parameter mapping, the sampling result will be multiplied with the constant term
R channel: Ambient light masking
G channel: Roughness
B channel: metallic | +| metallicRoughnessMap
**G**(Roughness)
**B**(Metallic) | Independent roughness and metallic mapping, the sampling result will be multiplied with the constant term
G channel: roughness
B channel: metallic | +| occlusionMap | Independent ambient light masking mapping
Sampling results are multiplied by a constant term | +| occlusion | Ambient light occlusion factor | +| roughness | Roughness coefficient | +| metallic | metallic coefficient | +| emissive | Emissive colors, independent of light calculation, directly emitted by the model itself | +| emissiveMap | Emissive mapping
This will multiply with the emissive color if specified, so you need to turn up the emissive color (default is black) to get the effect | +| emissiveScale | Emissive intensity
Used to control the weighting of the emissive color on the final color. | + +## PBR Pre-compiled Macro Definition + +| Macro | Description | +| :---- | :--- | +| USE_INSTANCING | Whether to enable GPU Geomerty Instancing | +| HAS_SECOND_UV | Whether there is a secondary UV | +| ALBEDO_UV | Specify the UVs used for the albedo color map, the first set is used by default | +| EMISSIVE_UV | Specify the UVs used for the sampled emissive mapping, the first set is used by default| +| ALPHA_TEST_CHANNEL | Specify the test channel for alpha test, default is A channel | +| USE_VERTEX_COLOR | If or not the vertex color is enabled. If enabled, the vertex color will be multiplied by the albedo color | +| USE_ALPHA_TEST | Whether to enable alpha text | +| USE_ALBEDO_MAP | Whether to use albedo color reflection mapping | +| USE_NORMAL_MAP | Whether to use normal map | +| USE_PBR_MAP | Whether or not to use PBR parameter triple mapping (**by glTF standard, RGB channels must correspond to ambient light masking, roughness and metallic respectively**) | +| USE_METALLIC_ROUGHNESS_MAP | Whether to use metallic roughness mapping (**by glTF standard, GB channel must correspond to roughness and metallic respectively**)| +| USE_OCCLUSION_MAP | Whether to use ambient light masking (**by glTF standard, only R channel will be used**)| +| USE_EMISSIVE_MAP | Whether to use self-luminous mapping | + +For more information about the PBR principle, please refer to [PBR Theory](https://learnopengl.com/PBR/Theory)。 diff --git a/versions/4.0/en/shader/effect-builtin-toon.md b/versions/4.0/en/shader/effect-builtin-toon.md new file mode 100644 index 0000000000..96b385de3d --- /dev/null +++ b/versions/4.0/en/shader/effect-builtin-toon.md @@ -0,0 +1,118 @@ +# Toon Shading + +Non-Photorealistic Rendering (NPR), as opposed to Physical Based Rendering ([PBR](effect-builtin-pbr.md)), achieves a completely different art representation from the real world through feature-based rendering. + +Toon Shading is one of the common effects of non-realistic rendering. + +Usually cartoon rendering contains the following basic components: + +- Stroke objects +- Reducing the number of color gradations and simulating color gradation discontinuities +- Separation of light and dark tones +- Shade shape interference, etc. + +![toon](img/toon.png) + +## Passes + +Creator provides a built-in toon rendering shader `builtin-toon.effect`, as an example, we switch the shader in the **Effect** property to `builtin-toon.effect` in the material resource, we can see that the toon rendering consists of two rendering passes. + +![toon pass](img/toon-pass.png) + +- Pass 0: used for stroking, not enabled by default, can be enabled by checking **USE_OUTLINE_PASS** on the right side. +- Pass 1: normal rendering of the model + +### Pass 0 + +Pass 0 selects the culling mode in the rasterization state to frontal culling (`CullMode = FRONT`) and expands the model's vertices along the normals, resulting in a larger monochrome model than the original model, after which **Pass 1** renders the model once normally and masks the rendering of **Pass 0**, leaving a solid color edge to form a stroke since the model size of **Pass 1** is smaller than that of **Pass 0**. + +![cull-front](img/cull-front.png) + +**Pass 0** can be turned on or off by checking **USE_OUTLINE_PASS**. + +When **USE_OUTLINE_PASS** is checked to turn on the stroke function for **Pass 0**, the effect is as follows. + +![USE_OUTLINE_PASS on](img/outline-on.png) + +If you need to adjust the depth effect of the stroke, you can do so with the **DepthBias** property. + +![DepthBias](img/toon-depth-bias.png) + +When **USE_OUTLINE_PASS** is unchecked to turn off the stroke function for **Pass 0**, the effect is as follows. + +![USE_OUTLINE_PASS off](img/outline-off.png) + +### Pass 1 + +The core idea of cartoon rendering is to simulate the Celluloid painting style in cartoons by reducing the number of color gradations. + +Reduces the color scale to three shades in the shader and is composed by three colors: + +- **baseColor**:Basic colors +- **shadeColor1**:First-order coloring of colors +- **shadeColor2**:Second-order coloring of colors + +The color corresponds to the editor material properties as follows: + +![toon-shade-color](img/shade-color.png) + +If **USE_1ST_SHADE_MAP** and **USE_2ND_SHADE_MAP** are checked, the texture is used externally to simulate the color scale discontinuity. + +![shade map](img/shade-map.png) + +The parameters of the surface shader (`ToonSurface`) are calculated by the shader's `surf` method, and the final shading is calculated by the `CCToonShading` method. + +![surf code](img/toon-surf.png) + +## Parameters and pre-compiled macro definitions + +### Pass 0 + +| Properties | Description | +| :------------- | :---------------------------------------------------------------- | +| lineWidth | The width of the stroke +| depthBias | Depth shift adjustment factor for stroke +| baseColor | Stroke base color + +### Pass 1 + +| Properties | Description | +| :------------- | :---------------------------------------------------------------- | +| tilingOffset | Scaling and offset of the model UV, xy corresponds to scaling, zw corresponds to offset | +| mainColor | The primary color, which will be used as the initial color gradient | +| colorScale | Color scaling, multiplying RGB channels for primary, first-order and second-order colors | +| alphaThreshold | Set the Alpha test threshold. Pixels with Alpha values below the set value will be discarded. This item is only displayed when **USE_ALPHA_TEST** is checked | +| shadeColor1 | First order color. This color will be used as an intermediate color for cartoon coloring | +| shadeColor2 | Secondary order color. This color will be used as the last color scale of the cartoon coloring | +| specular | specular color | +| baseStep | Step size for first-order coloring | +| baseFeather | First-order shading and primary color mixing factor, and **BaseStep** property to adjust the proportion of first-order shading and the form of mixing | +| shadeStep | Step size for second-order color | +| shadeFeather | Blending factor for second-order shading and first-order shading, and **ShadeStep** property to adjust the proportion of second-order shades and the form of blending | +| shadowCover | Shadow covering factor | +| emissive | Self-emitting colors, independent of lighting calculation, directly emitted by the model itself | +| emissiveScale | Self-emitting intensity, used to control the weight of the impact of self-emitting color on the final color | +| normalStrength | Normal scaling
The xy axis of the normal can only be adjusted and scaled if **USE_NORMAL_MAP** is enabled | +| normalMap | Normal mapping
can only be adjusted if **USE_NORMAL_MAP** is enabled | +| mainTexture | The main texture, which defines the base texture of the object.
Is displayed in the editor as **baseColorMap** and can only be adjusted if the **USE_BASE_COLOR_MAP** macro is enabled | +| shadeMap1 | The first-order color texture, if specified, will be multiplied by the set ShadeColor1. This item is only shown when **USE 1ST SHADER MAP** is checked. +| shadeMap2 | Second-order color texture, if specified, it will be multiplied by the set ShadeColor2. This item is only shown when **USE 2ND SHADER MAP** is checked. +| specularMap | The highlight map, if specified, will be multiplied with the highlight color. This item is only shown when **USE SPECULAR MAP** is checked. +| emissiveMap | Self-Emitting map, if specified, it will be multiplied by the self-emitting color, so you need to turn up the RGBA in the self-emitting color (default is black) to get the effect. This item is only shown when **USE EMISSIVE MAP** is checked. + +## Pre-compiled Macro Definition + +| macro | description | +| :---------------------------- | :------------------------ | +| USE_INSTANCING | Whether to enable dynamic gpu instancing | +| USE_OUTLINE_PASS | Whether to enable strokes Pass | +| USE_NORMAL_MAP | Whether to use normal mapping | +| USE_BASE_COLOR_MAP | Whether to use base color maps| +| USE_1ST_SHADE_MAP | Whether to use the map as a first-order color scale| +| USE_2ND_SHADE_MAP | Whether to use a texture as a second-order color map| +| USE_EMISSIVE_MAP | Whether to use self-emitting maps| +| USE_ALPHA_TEST | Whether to perform alpha tests| +| USE_SPECULAR_MAP | Whether to use the highlight map| +| BASE_COLOR_MAP_AS_SHADE_MAP_1 | Use baseColorMap as first-order shading | +| BASE_COLOR_MAP_AS_SHADE_MAP_2 | Use baseColorMap as second-order shading | +| SHADE_MAP_1_AS_SHADE_MAP_2 | Whether second-order coloring is overlaid with first-order coloring | diff --git a/versions/4.0/en/shader/effect-builtin-unlit.md b/versions/4.0/en/shader/effect-builtin-unlit.md new file mode 100644 index 0000000000..3a070fb218 --- /dev/null +++ b/versions/4.0/en/shader/effect-builtin-unlit.md @@ -0,0 +1,36 @@ +# Unlit + +Unlit is the most basic shading model, under which no light source from the engine can affect the final effect, and is applicable to: + +- Objects that are not affected by light sources +- Scenes with low screen requirements or high performance requirements + +When switching the shader to the Creator's built-in unlit shader (builtin-unlit.effect) in the material's **Effect** property, the following image is shown: + +![unlit](img/unlit-shademode.png) + +## Art standards + +For technical selection, to work with lighting under a model using the **unlit** material, draw the lighting information on the texture map and then drag the texture map into the **MainTexture** property box of the material. + +To use PBR lighting, see: [Physically Based Rendering](effect-builtin-pbr.md) + +## Parameters + +| parameter | description | +| :--- | :--- | +| mainTexture | The main texture | +| tilingOffset | Scaling and offset of the model UV, xy corresponds to scaling, zw corresponds to offset | +| mainColor | The main color, which will be processed within the slice shader | +| colorScale | Multiply with main color to scale the main color | +| alphaThreshold | Used for semi-transparent tests, with USE_ALPHA_TEST enabled, pixels smaller than this value will be discarded | + +## Pre-compiled macro definitions + +| macro | description | +| :--- | :---- | +| USE_INSTANCING | Whether to enable GPU geometry instancing | +| USE_VERTEX_COLOR | Whether to overlay vertex colors and alpha values | +| USE_TEXTURE | Whether to use the main texture (`mainTexture`) | +| USE_ALPHA_TEST | Whether to perform translucency testing (AlphaTest) | +| SAMPLE_FROM_RT | Whether to sample from the RenderTexture or not, when checked the Y value of the UV will be flipped | diff --git a/versions/4.0/en/shader/effect-builtin.md b/versions/4.0/en/shader/effect-builtin.md new file mode 100644 index 0000000000..0af47bb71e --- /dev/null +++ b/versions/4.0/en/shader/effect-builtin.md @@ -0,0 +1,56 @@ +# Built-in Effect + +The engine provides a set of common built-in shaders, located in the `internal -> effects` directory of the **Assets** panel of the editor. Double-click a shader file to open it in an external IDE for viewing and editing (provided that **Default Script Editor** needs to be configured in **Preferences -> External Programs**). + +Creator roughly categorizes built-in shaders into the following categories: + +- `internal`:Internal functionality shaders, such as In-editor gizmo, geometry renderer. Users generally do not need to pay attention to these. +- `pipeline`:Pipeline effects shaders, including deferred lighting, post-effects, anti-aliasing, and more. +- `util`:Holds some loose built-in shaders, such as dcc material importing and sequence frame animations. Users generally do not need to pay attention to these. +- `for2d`:2D rendering shaders, such as spine and sprite. +- `particles`:Particle effect shaders. +- `advanced`:Advanced material with [surface shader](surface-shader.md), such as water, skin, hair, jade. +- Others are built-in shaders, please refer to the description below for details. + +## Built-in pipeline effect + +![pipeline effect](img/pipeline-effect.png) + +| Effect name | description | +| :----------------------- | :--------------------------- | +| bloom.effect | Full-screen bloom effect | +| deferred-lighting.effect | Lighting process in the deferred-rendering pipeline | +| planar-shadow.effect | Planar shadow | +| post-process.effect | Post-processing | +| skybox.effect | Skybox shader | +| smaa.effect | SMAA Antialiasing | +| tonemap.effect | Engine reserved | + +## Built-in effect + +![built-in effect](img/builtin-effect.png) + +| Material Effect name | description | +| :--- | :--- | +| builtin-standard.effect | [PBR](effect-builtin-pbr.md) | +| builtin-terrain.effect | Default terrain effect | +| builtin-toon.effect | [Toon shading](effect-builtin-toon.md) | +| builtin-unlit.effect | [Unlit](effect-builtin-unlit.md) | + + + +| Other Effect name | description | +| :--- | :--- | +| builtin-billboard.effect | Billboard
Billboard is a rendering technique that makes objects always face the camera, suitable for rendering trees, healthing point bars, etc.| +| builtin-camera-texture.effect | camera texture | +| builtin-clear-stencil.effect | clear stencil cache | +| builtin-graphics.effect | [Graphics](../ui-system/components/editor/graphics.md) effect | +| builtin-occlusion-query.effect | Occlusion query | +| builtin-particle-trail.effect | Trail for particle system | +| builtin-particle.effect | CPU-based rendergin for particle system | +| builtin-particle-gpu.effect | GPU-based rendering for particle system
Please refer to [Particle Renderer](../particle-system/renderer.md) to understand the useage of these two effects | +| builtin-reflection-deferred.effect | For reflection handling in deferred-rendering | +| builtin-spine.effect | Effect for spine skeleton animation| +| builtin-sprite.effect | Built-in standard sprite effect | +| builtin-sprite-gpu.effect | Engine reserved | +| builtin-wireframe.effect | Wireframe effect | diff --git a/versions/4.0/en/shader/effect-chunk-index.md b/versions/4.0/en/shader/effect-chunk-index.md new file mode 100644 index 0000000000..d57246c93e --- /dev/null +++ b/versions/4.0/en/shader/effect-chunk-index.md @@ -0,0 +1,47 @@ +# Effect Chunk + +Chunk is a cross-file code reference mechanism that enables shader code fragments to be reused between different files. + +The syntax of the chunk is based on **GLSL 300 ES** and will be precompiled when the asset is loaded to generate the target machine shader code. + +## Create chunk + +Right-click in the **Assets** panel and select **Create -> Chunk**: + +![create chunk](img/create-chunk.png) + +A chunk asset named **chunk** by default can be created, as shown below: + +![chunk template](img/new-chunk.png) + +## Include mechanism + +Based on the standard GLSL syntax, Cocos Effect introduces a C-style syntax extension - the Include mechanism. + +Through the include mechanism, other chunks can be included in any Shader code (CCProgram block or separate header file), as follows: + +```c +// Introduced in-engine chunk +#include + +// Introduce custom chunk +#include "../headers/my-shading-algorithm.chunk" +``` + +Relevant rules and precautions: + +- Chunk extension defaults to `.chunk`, which can be ignored when include. There is no difference between using double brackets or angle brackets when introducing other code fragments, for example: + + ```c + #include "filename.chunk" + #include "filename" // ignorable extension + #include + #include // ignorable extension + ``` + +- Chunks are expanded when Cocos Effect is compiled, and only once. So don't worry when writing, each module can contain its own dependent shader fragments, even if there are repetitions; +- All function declarations that do not participate in the actual calculation process at runtime will be eliminated at compile time, so you can safely include various tool functions without worrying that the generated object code will be redundant; +- Chunk references can specify a relative path based on the current file directory (hereinafter collectively referred to as "relative path"), or a relative path based on the **internal -> chunks** directory in the **Explorer** panel of the editor (Hereinafter collectively referred to as the "Project Absolute Path"). If there are files with the same name in the two directories, the latter (project absolute path) takes precedence; +- Creator provides some built-in shader fragment resources, mainly including some commonly used utility functions and standard lighting models, etc. The built-in asset database (DB for short) located in the [Assets](../editor/assets/index.md) panel in the `chunks` directory of `internal`, so it can be directly referenced without a directory. +- Chunk that reference other DBs (Databases) in the editor can only specify the absolute path of the project. When multiple DBs have the same file in this path, the DB priority is: User Project DB > Plugin DB > Internal DB; +- All CCProgram code blocks declared in the same Cocos Effect file can reference each other. diff --git a/versions/4.0/en/shader/effect-inspector.md b/versions/4.0/en/shader/effect-inspector.md new file mode 100644 index 0000000000..62776c2509 --- /dev/null +++ b/versions/4.0/en/shader/effect-inspector.md @@ -0,0 +1,78 @@ +# Cocos Shader Creation and Usage + +## Creating a Shader + +To create a shader, click on the **+** button in the top-left corner of the **Asset** window (or right-click in the Assets folder), and select **Effect** or **Surface Shader** from the pop-up menu. This will create a new shader file. + +![1](img/create-effect.png) + +There are two types of shaders available. +- **Effect**: A simple shader without lighting. You can refer to internal/effects/builtin-unlit.effect for an example. +- **Surface Shader**: A PBR-based shader. You can refer to internal/effects/builtin-standard.effect for an example. + +Let's take the Surface Shader as an example. The engine will create a shader file named **surface-effect** in the **Assets** Window. + +![image](img/new-effect.png) + +In the **Inspector** panel, you can see that the shader consists of the following main parts. + +| Property |Description| +| :-- | :-- | +|Shaders | The names of the current shader and its rendering processes. +| Precompile Combinations | Whether to enable precompiled macro definition combinations. See the explanation below for more details. +| GLSL 300 ES/100 Output | Shader output. See the explanation below for more details. + +## Shaders + +If the current shader has multiple render passes, you can select different render pass through the dropdown menu on the right side of **Shaders**. After selecting a render pass, you can view the compiled shader code in the **GLSL Output** window. + +![render-pass](img/effect-pass.png) + +## Precompile Combinations + +Normally, shaders are compiled when the corresponding macro definitions are used. However, if there are many macro definitions involved, it may cause stuttering. In such cases, you can use this option to precompile combinations of macro definitions. For example, in the following configuration. + +![image](./img/precompile.png) + +## GLSL Output + +The engine currently provides GLSL 300 ES and GLSL 100 output options. + +By selecting different tabs, you can switch between displaying the compiled vertex shader and fragment shader. + +![vs-fs-switch](img/change-vs-fs.png) + +## Access Built-in Shaders in Code + +The internal/effects/ folder contains built-in shaders provided by the engine, which are automatically loaded after game starts. + +Taking the `builtin-standard` as an example, you can access and use it in code as follows. + +```ts +// Get the built-in Standard shader ‘builtin-standard.effect’ +const effect = EffectAsset.get('builtin-standard'); + +const mat = new Material(); + +// Initialize the material using the built-in PBR sahder ‘builtin-standard.effect’ +mat.initialize({ effectName: "builtin-standard" }); +``` + +## Dynamic Loading Shaders + +Shader files located in the **resources** folder can be loaded and used using the "resources.load" method. + +Here's an example of how to do it in code. + +```ts +resources.load("custom-effect", EffectAsset, (err:Error, data:EffectAsset)=>{ + //get effect + const effectAsset = EffectAsset.get("../resources/custom-effect"); + + //use the loaded effect + const material = new Material(); + material.initialize({ effectName: "../resources/custom-effect" }); +}) +``` + +> **Note:** After successfully loading a custom shader, the effectName should be "../" + the file path. diff --git a/versions/4.0/en/shader/effect-syntax.md b/versions/4.0/en/shader/effect-syntax.md new file mode 100644 index 0000000000..94dad520fb --- /dev/null +++ b/versions/4.0/en/shader/effect-syntax.md @@ -0,0 +1,183 @@ +# Shader Syntax + +Shaders in Cocos Creator are called Cocos Shaders, and have the file extension `*.effect`. It consists of [YAML](yaml-101.md) and [GLSL](glsl.md). The YAML is used for the flow control instructions, while the GLSL is for the shader code. These two parts complement each other to form a complete Cocos Shader. + +> **Note**: It is recommended to use Visual Studio Code to write Cocos Shader and install the Cocos Effect extension from the marketplace in Visual Studio Code to provide syntax highlighting and autocompletion. +> ![Cocos Effect](img/vs-ext.png) + +## Syntax Overview + +A **Cocos Shader** typically consists of two parts. + +- `CCEffect`: Used to declare techniques, passes, render states, properties, etc. +- `CCProgram`: Used for shader code, such as the vs, fs, uniforms, etc. + +Let's explore the syntax of Cocos Shader by taking the built-in shader `builtin-unlit.effect` as an example. + +In VS Code, Open the `builtin-unlit.effect` file located in the `internal/effects/` folder. + +```glsl +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: unlit-vs:vert + frag: unlit-fs:frag + properties: &props + mainTexture: { value: grey } + tilingOffset: { value: [1, 1, 0, 0] } + mainColor: { value: [1, 1, 1, 1], linear: true, editor: { type: color } } + colorScale: { value: [1, 1, 1], target: colorScaleAndCutoff.xyz } + alphaThreshold: { value: 0.5, target: colorScaleAndCutoff.w, editor: { parent: USE_ALPHA_TEST } } + ... + - name: transparent + passes: + - vert: unlit-vs:vert + frag: unlit-fs:frag + depthStencilState: &d1 + ... +}% + +CCProgram unlit-vs %{ + precision highp float; + #include <...> + #cinlude <....> + + vec4 vert(){ + vec4 position; + CCVertInput(position); + .... + return cc_matProj * (cc_matView * matWorld) * position; + } +}% + +CCProgram unlit-fs %{ + precision highp float; + #include <...> + #cinlude <....> + + vec4 frag(){ + vec4 o = mainColor; + .... + return CCFragOutput(o); + } +}% + +``` + +## CCEffect + +The section wrapped by `CCEffect` in Cocos Shader contains the description of the rendering process declared using **YAML**. Developers who are unfamiliar with YAML can refer to [YAML 101](yaml-101.md) for more details. + +The overall structure of `CCEffect` is as follows. + +```glsl +CCEffect %{ + techniques: + - name: tag + passes: + - vert: vs:entry + frag: fs:entry + + + + ... + ... +}% +``` + +A `CCEffect` can declare multiple techniques, but only one can be applied to a material instance during rendering. + +Taking `builtin-unlit.effect` as an example, it contains the following 4 techniques. +- opaque +- transparent +- add +- alpha-blend + +The `opaque` technique is specifically used for rendering opaque objects, while the `transparent`, `transparent`, `add` and `alpha-blend` are used for rendering transparent objects. + +Each technique includes a name and one or more render passes. + +The name is used to describe the purpose of the render technique, and the passes defines all the necessary information for a complete render call. + +## Pass + +The characteristics of a render pass are as follows. + +1. A render technique can contain multiple render passes, and the passes are executed one by one in the defined order. +2. A render pass must include a vertex shader and a fragment shader, while the rest are optional. For details, please refer to [Pass Optional Parameters](pass-parameter-list.md)。 +3. The VS/FS shaders need to specify the **CCProgram** to use and specify the entry function of the shader. If the entry function is not specified, the default entry 'main' will be used. + +**The format is as follows**: + +```glsl +CCEffect %{ + techniques: + - name: opaque # Define an opaque render technique + passes: + - vert: vs: entry # Select a vertex shader defined in CCProgram, with the entry function. + frag: fs: entry # Select a fragment shader defined in CCProgram, with the entry function. + ... + ... +}% +``` + +Each render pass has only one `vert` and one `frag`, which are used to declare the vertex shader and fragment shader used in the current pass. The format is `ShaderName: EntryFunctionName`. + +The `ShaderName` can be a fragment name declared in the current shader file or a standard header file (*.chunk) provided by the engine. + +> **Note**: The customized shader should not use the `main` function. Cocos Shader automatically adds a `main` function during compilation and calls the entry function (e.g. `vert` of `frag` ) of the render pass. The `main` function will use the return value of the entry function as the output of the current shader. + +## Render Pass Properties + +Properties in a render pass are used to configure related uniforms for the shader code. Through properties, you can define how a uniform is displayed in the panel. Here's an example. + +```glsl +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: vs: entry + frag: fs: entry + properties: + mainTexture: { value: grey } + colorScale: { value: [1, 1, 1], target: colorScaleAndCutoff.xyz } + depthStencilState: + depthTest: true + depthWrite: true + ... + ... +}% +``` + +For more details, please refer to [Pass Optional Parameters](pass-parameter-list.md)。 + +## CCProgram + +The section wrapped by `CCProgram` in Cocos Shader is declared using **GLSL** for the shader code. It is recommended to have a basic understanding of [GLSL Basic](./glsl.md). + +```glsl +CCProgram shader-name %{ + + + + + + vec4 entry(){ + // Should return a vec4 type data. + } +}% +``` + +## Macros + +By using macros, you can control code branches and combinations during the compilation of Cocos Shader, enabling efficient and convenient management of shader code. + +For more detailed information, please refer to. + +- [Macros](macros.md) +- [GLSL Basic](glsl.md) + +## More + +If you want to see more complex shader examples, you can refer to the [Surface Shader Overview](./surface-shader/surface-shader-structure.md) and [Built-in Surface Shader](./surface-shader/builtin-surface-shader.md). diff --git a/versions/4.0/en/shader/forward-and-deferred.md b/versions/4.0/en/shader/forward-and-deferred.md new file mode 100644 index 0000000000..231f23f237 --- /dev/null +++ b/versions/4.0/en/shader/forward-and-deferred.md @@ -0,0 +1,26 @@ +# Shader Process in Forward Rendering and Deferred Rendering + +In Cocos Creator, both Forward Rendering and Deferred Rendering are supported. Therefore, the shader framework needs to be compatible with these two rendering pipelines without users having to be aware of it. + +The built-in PBR shaders follow the following process during rendering. + +## Forward Rendering + +1. Invoke vs +2. Invoke the fs -> surf -> lighting calculations + +## Deferred Rendering + +### GBuffer Stage + +1. Invoke vs +2. Invoke fs -> surf -> GBuffer + +### Lighting Stage + +1. Restore StandardSurface information from the GBuffer. +2. Perform lighting calculations. + +As we can see, for PBR materials, whether it is Forward or Deferred, users can only control the vs and surf functions. + +This unifies the process, allowing users to write shaders that can run in both the Forward and Deferred rendering pipelines without modification. diff --git a/versions/4.0/en/shader/glsl.md b/versions/4.0/en/shader/glsl.md new file mode 100644 index 0000000000..f56f66b458 --- /dev/null +++ b/versions/4.0/en/shader/glsl.md @@ -0,0 +1,293 @@ +# Introduction to GLSL Basic + +GLSL is a language for writing shaders tailored for graphics computing, and it includes features for vector and matrix operations that make rendering pipelines programmable. This chapter mainly introduces some syntax commonly used in Shader writing, including the following aspects: + +- Variable +- Statement +- Qualifier +- Preprocessor macro definition + +## Variable + +### Variables and Variable Types + +| Variable type | Description | Default values in Cocos Effect | Options in Cocos Effect | +| :-- | :-- | :-- | :-- | +| bool | boolean | false | | +| int/ivec2/ivec3/ivec4 | contains 1/2/3/4 integer vectors | 0/[0, 0]/[0, 0, 0]/[0, 0, 0, 0] | | +| float/vec2/vec3/vec4 | Contains 1, 2, 3, 4 float vectors | 0/[0, 0]/[0, 0, 0]/[0, 0, 0, 0] | | +| sampler2D | a 2D texture | **default** | black、grey、white、normal、default | +| samplerCube | a Cube texture | **default-cube** | black-cube、white-cube、default-cube | +| mat[2..3] | Representing 2x2 and 3x3 matrices | unavailable | +| mat4 | Represents a 4x4 matrix | [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] | + +### Scalar + +The way to construct a scalar is the same as the C language: + +```glsl +float floatValue = 1.0; +bool booleanValue = false; +``` + +### Vector + +The rules for constructing a vector are as follows: + +- If a scalar is provided to the vector constructor, all values of the vector are set to the scalar value. +- If multiple scalar values or vectors are provided, the provided values are assigned from left to right, provided that the sum of the number of scalars or vectors is equal to the number of vector constructors. + +```glsl +vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0} +vec2 myVec2 = vec2(0.5, 0.5); // myVec2 = {0.5, 0.5} +vec4 newVec4 = vec4(1.0, 1.0, myVec2);// newVec4 = {1.0, 1.0, 0.5, 0.5} +``` + +Vectors can be accessed via `r, g, b, a` or `x, y, z, w`, and multiple indices can be accessed simultaneously: + +```glsl +vec4 myVec4_0 = vec4(1.0); // myVec4_0 = { 1.0, 1.0, 1.0, 1.0 } +vec4 myVec4 = vec4(1.0, 2.0, 3.0, 4.0); // myVec4 = { 1.0, 2.0, 3.0, 4.0 } +float x = myVec4.x; // x = 1.0; +vec3 myVec3_0 = myVec4.xyz; // myVec3_0 = { 1.0, 2.0, 3.0 } +vec3 myVec3_1 = myVec4.rgb; // myVec3_1 = { 1.0, 2.0, 3.0 } +vec3 myVec3_2 = myVec4.zyx; // myVec3_2 = { 3.0, 2.0, 1.0 } +vec3 myVec3_3 = myVec4.xxx; // myVec3_3 = { 1.0, 1.0, 1.0 } +``` + +### matrix + +In GLSL, mat[2..4] can be constructed to represent matrices of order 2 to 4. + +The matrix construction has the following rules: + +- If only a scalar is provided to the matrix constructor, this value constructs the values on the diagonal of the matrix +- Matrices can be constructed from multiple vectors +- Matrices can be constructed from a single scalar from left to right + +```glsl + +mat4 matrix4x4 = mat4(1.0); // matrix4x4 = { 1.0, 0.0, 0.0, 0.0, + // 0.0, 1.0, 0.0, 0.0 + // 0.0, 0.0, 1.0, 0.0 + // 0.0, 0.0, 0.0, 1.0 } + +vec2 col1 = vec2(1.0, 0.0); +vec2 col2 = vec2(1.0, 0.0); + +mat2 matrix2x2 = mat2(coll1, col2); + +// GLSL is a column-matrix store, so when constructed, the constructor fills in column order +mat3 matrix3x3 = mat3(0.0, 0.0, 0.0, // Column 0 + 0.0, 0.0, 0.0, // Column 1 + 0.0, 0.0, 0.0); // Column 2 +``` + +> **Note**: In order to avoid **implicit padding**, the engine stipulates that if you want to use the matrix with the Uniform qualifier, it must be a matrix4x4, and the matrix of order 2 and 3 cannot be used as a Uniform variable. + +#### Access to the matrix + +Matrix can access different columns by index: + +```glsl +mat2 matrix2x2 = mat2(0.0, 0.0, 0.0, 0.0); +vec4 myVec4 = vec4(matrix2x2[0], matrix2x2[1]); +vec2 myVec2 = matrix2x2[0]; + +// Access the first element of the first column +float value = matrix2x2[0][0]; +matrix2x2[1][1] = 2.0; +``` + +### Structure + +The formation of structure is similar to that of C language, and it can be aggregated from different data types: + +```c +struct myStruct +{ + vec4 position; + vec4 color; + vec2 uv; +}; +``` + +An example of code to construct a structure is as follows: + +```glsl +myStruct structVar = myStruct(vec4(0.0, 0.0,0.0,0.0), vec4(1.0, 1.0, 1.0, 1.0), vec2(0.5, 0.5)); +``` + +Structs support assignment (=) and comparison (==, !=) operators, but require that both structs have the same type and must be the same component-wise. + +### Array + +Array usage is similar to C language, the rules are as follows: + +- Arrays must have a declared length +- Array cannot be initialized at the same time as declaration +- Arrays must be initialized by constant expressions +- Arrays cannot be decorated with `const` +- Multidimensional arrays are not supported + +An example of code for array declaration and initialization is as follows: + +```glsl +float array[4]; +for(int i =0; i < 4; i ++) +{ + array[i] = 0.0; +} +``` + +## Statement + +### Control flow + +GLSL supports standard C/C++ control flow, including: +- `if-else`/`switch-case` +- `for`/`while`/`do-while` +- `break`/`continue`/`return` +- There is no `goto`, use `discard` to jump out. This statement is only valid under the fragment shader. It should be noted that using this statement will cause the pipeline to discard the current fragment and will not write to the frame buffer. + +The usage of `if-else` is consistent with the C language, the code example is as follows: + +```glsl +if(v_uvMode >= 3.0) { + i.uv = v_uv0 * v_uvSizeOffset.xy + v_uvSizeOffset.zw; +} else if (v_uvMode >= 2.0) { + i.uv = fract(v_uv0) * v_uvSizeOffset.xy + v_uvSizeOffset.zw; +} else if (v_uvMode >= 1.0) { + i.uv = evalSlicedUV(v_uv0) * v_uvSizeOffset.xy + v_uvSizeOffset.zw; +} else { + i.uv = v_uv0; +} +``` + +In GLSL, loop must be constant or known at compile time. The code example is as follows: + + ```glsl +const float value = 10.; +for(float i = 0.0; i < value; i ++){ + ... +} + ``` + +Error example: + +```glsl +float value = 10.; +// Error, value is not a constant +for(float i =0.0; i < value; i ++){ + ... +} +``` + +### Function + +A GLSL function consists of **return value**, **function name** and **parameter**, where the return value and function name are required. If there is no return value, you need to use `void` instead. + +> **Note**: GLSL functions cannot be recursive. + +The code example is as follows: + +```glsl +void scaleMatrix (inout mat4 m, float s){ + m[0].xyz *= s; + m[1].xyz *= s; + m[2].xyz *= s; +} +``` + +## Qualifier + +### Storage qualifier + +Storage qualifiers are used to describe the role of variables in the pipeline. + +| Qualifier | Description | +|:--|:--| +|< none:default > | Unqualified or using `default`, common local variables, function parameters | +| const | Compile-time constant or read-only as parameter | +| attribute | Communication between application and vertex shader to determine vertex format | +| uniform | Data is exchanged between applications and shaders. Consistent in vertex shaders and fragment shaders | +| varying | Interpolation passed from the vertex shader to the fragment shader | + +#### Uniform + +`Uniform` declared within a render pass cannot be repeated. For example, if the variable `variableA` is defined in the vertex shader, `variableA` will also exist in the fragment shader with the same value, then `variableA` cannot be defined again in the fragment shader. + +Engine does not support discretely declared `uniform` variables. Uniforms must be declared in UBOs, and UBOs must be memory aligned to avoid implicit padding. + +#### Varying + +`varying` is the variable output by the vertex shader and passed to the fragment shader. Under the action of the pipeline, the variable value will not be consistent with the output of the vertex shader, but will be interpolated by the pipeline, which may cause the normal of the vertex output to not be normalized. At this point, manual normalization is required. The code example is as follows: + +```glsl +// normalized the normal from vertex shader +vec3 normal = normalize(v_normal); +``` + +### Parameter qualifier + +Parameter qualifiers for functions in GLSL include the following: + +| qualifier | description | +|:---|:---| +| < none:in > | The default qualifier, similar to the value passing in the C language, indicates that the passed parameter is a value, and the passed value will not be modified in the function | +| inout | Similar to the reference in the C language, the value of the parameter is passed into the function and the value modified in the function is returned | +| out | The value of the parameter will not be passed into the function, it will be modified inside the function and the modified value will be returned | + +### Precision qualifier + +GLSL introduces precision qualifiers for specifying the precision of integer or floating point variables. Precision qualifiers allow shader writers to explicitly define the precision with which shader variables are computed. + +The precision declared in the Shader header applies to the entire Shader, and is the default precision for all floating-point-based variables, and it is also possible to define the precision of a single variable. If no default precision is specified in Shader, all integer and floating point variables are calculated with high precision. + +The precision qualifiers supported by GLSL include the following: + +| qualifier | description | +|:--|:--| +|highp | High precision.
The precision range of floating point type is [-262, 262]
The precision range of integer type is [-216 , 216].| +|mediump | Medium precision.
The precision range of floating point type is [-214, 214]
The precision range of integer type is [-210 , 210]. | +|lowp | Low accuracy.
The precision range of floating point type is [-28, 28]
The precision range of integer type is [-28 , 28].| + +The code example is as follows: + +```glsl +highp mat4 cc_matWorld; +mediump vec2 dir; +lowp vec4 cc_shadowColor; +``` + +## Preprocessor macro definition + +GLSL allows the definition of macros similar to the C language. + +Preprocessing macro definitions allow shaders to define a variety of dynamic branches that determine the final rendering effect. + +An example of code defined using preprocessor macros in GLSL is as follows: + +```glsl +#define +#undef +#if +#ifdef + +#ifndef +#else +#elif +#endif +``` + +The following code example declares a four-dimensional vector named `v_color` only if the `USE_VERTEX_COLOR` condition is true: + +```glsl +#if USE_VERTEX_COLOR + in vec4 v_color; +#endif +``` + +For the interactive part of the preprocessing macro definition and the engine, please refer to: [Preprocessing Macro Definition](macros.md) + +> **Note**: In the engine, the preprocessing macro definition of the material cannot be modified after the material is initialized. If modifications are required, use the `Material.initialize` or `Material.reset` methods. For code examples, please refer to: [Programmatic use of materials](../material-system/material-script.md) diff --git a/versions/4.0/en/shader/img/1-dot.jpg b/versions/4.0/en/shader/img/1-dot.jpg new file mode 100644 index 0000000000..24b3e80eae Binary files /dev/null and b/versions/4.0/en/shader/img/1-dot.jpg differ diff --git a/versions/4.0/en/shader/img/2d-create-guild.png b/versions/4.0/en/shader/img/2d-create-guild.png new file mode 100644 index 0000000000..4be4ff5213 Binary files /dev/null and b/versions/4.0/en/shader/img/2d-create-guild.png differ diff --git a/versions/4.0/en/shader/img/2d-gradient-vs.png b/versions/4.0/en/shader/img/2d-gradient-vs.png new file mode 100644 index 0000000000..1a4e07caa6 Binary files /dev/null and b/versions/4.0/en/shader/img/2d-gradient-vs.png differ diff --git a/versions/4.0/en/shader/img/add-intensity.png b/versions/4.0/en/shader/img/add-intensity.png new file mode 100644 index 0000000000..2d7daed5b6 Binary files /dev/null and b/versions/4.0/en/shader/img/add-intensity.png differ diff --git a/versions/4.0/en/shader/img/adjust-gradient-color.png b/versions/4.0/en/shader/img/adjust-gradient-color.png new file mode 100644 index 0000000000..a04c22e87d Binary files /dev/null and b/versions/4.0/en/shader/img/adjust-gradient-color.png differ diff --git a/versions/4.0/en/shader/img/adjust-option.png b/versions/4.0/en/shader/img/adjust-option.png new file mode 100644 index 0000000000..b75c8bc8db Binary files /dev/null and b/versions/4.0/en/shader/img/adjust-option.png differ diff --git a/versions/4.0/en/shader/img/albedo-map.png b/versions/4.0/en/shader/img/albedo-map.png new file mode 100644 index 0000000000..620869ac7d Binary files /dev/null and b/versions/4.0/en/shader/img/albedo-map.png differ diff --git a/versions/4.0/en/shader/img/albedo.jpg b/versions/4.0/en/shader/img/albedo.jpg new file mode 100644 index 0000000000..baf5db25fd Binary files /dev/null and b/versions/4.0/en/shader/img/albedo.jpg differ diff --git a/versions/4.0/en/shader/img/alpha-cutoff.png b/versions/4.0/en/shader/img/alpha-cutoff.png new file mode 100644 index 0000000000..ab6c4de996 Binary files /dev/null and b/versions/4.0/en/shader/img/alpha-cutoff.png differ diff --git a/versions/4.0/en/shader/img/ao.jpg b/versions/4.0/en/shader/img/ao.jpg new file mode 100644 index 0000000000..eed152f554 Binary files /dev/null and b/versions/4.0/en/shader/img/ao.jpg differ diff --git a/versions/4.0/en/shader/img/builtin-effect.png b/versions/4.0/en/shader/img/builtin-effect.png new file mode 100644 index 0000000000..a8fc558e7b Binary files /dev/null and b/versions/4.0/en/shader/img/builtin-effect.png differ diff --git a/versions/4.0/en/shader/img/change-vs-fs.png b/versions/4.0/en/shader/img/change-vs-fs.png new file mode 100644 index 0000000000..3d3d45188f Binary files /dev/null and b/versions/4.0/en/shader/img/change-vs-fs.png differ diff --git a/versions/4.0/en/shader/img/check-use-texture.png b/versions/4.0/en/shader/img/check-use-texture.png new file mode 100644 index 0000000000..923b0360ef Binary files /dev/null and b/versions/4.0/en/shader/img/check-use-texture.png differ diff --git a/versions/4.0/en/shader/img/compile-binding.gif b/versions/4.0/en/shader/img/compile-binding.gif new file mode 100644 index 0000000000..43a85238fe Binary files /dev/null and b/versions/4.0/en/shader/img/compile-binding.gif differ diff --git a/versions/4.0/en/shader/img/compute-shader-rt.png b/versions/4.0/en/shader/img/compute-shader-rt.png new file mode 100644 index 0000000000..7b28251338 Binary files /dev/null and b/versions/4.0/en/shader/img/compute-shader-rt.png differ diff --git a/versions/4.0/en/shader/img/create-asset.png b/versions/4.0/en/shader/img/create-asset.png new file mode 100644 index 0000000000..007c8a8649 Binary files /dev/null and b/versions/4.0/en/shader/img/create-asset.png differ diff --git a/versions/4.0/en/shader/img/create-chunk.png b/versions/4.0/en/shader/img/create-chunk.png new file mode 100644 index 0000000000..12969f4fd1 Binary files /dev/null and b/versions/4.0/en/shader/img/create-chunk.png differ diff --git a/versions/4.0/en/shader/img/create-effect.png b/versions/4.0/en/shader/img/create-effect.png new file mode 100644 index 0000000000..767e4ae0e7 Binary files /dev/null and b/versions/4.0/en/shader/img/create-effect.png differ diff --git a/versions/4.0/en/shader/img/create-sprite.png b/versions/4.0/en/shader/img/create-sprite.png new file mode 100644 index 0000000000..b49f7a8131 Binary files /dev/null and b/versions/4.0/en/shader/img/create-sprite.png differ diff --git a/versions/4.0/en/shader/img/cull-front.png b/versions/4.0/en/shader/img/cull-front.png new file mode 100644 index 0000000000..956303fe1e Binary files /dev/null and b/versions/4.0/en/shader/img/cull-front.png differ diff --git a/versions/4.0/en/shader/img/custom-material.png b/versions/4.0/en/shader/img/custom-material.png new file mode 100644 index 0000000000..123b78b082 Binary files /dev/null and b/versions/4.0/en/shader/img/custom-material.png differ diff --git a/versions/4.0/en/shader/img/debug-view.jpg b/versions/4.0/en/shader/img/debug-view.jpg new file mode 100644 index 0000000000..a410136d47 Binary files /dev/null and b/versions/4.0/en/shader/img/debug-view.jpg differ diff --git a/versions/4.0/en/shader/img/def-chunk.png b/versions/4.0/en/shader/img/def-chunk.png new file mode 100644 index 0000000000..78d7a8bf3d Binary files /dev/null and b/versions/4.0/en/shader/img/def-chunk.png differ diff --git a/versions/4.0/en/shader/img/default-effect.png b/versions/4.0/en/shader/img/default-effect.png new file mode 100644 index 0000000000..ff289bdd1f Binary files /dev/null and b/versions/4.0/en/shader/img/default-effect.png differ diff --git a/versions/4.0/en/shader/img/depth-write.png b/versions/4.0/en/shader/img/depth-write.png new file mode 100644 index 0000000000..7f38f8363a Binary files /dev/null and b/versions/4.0/en/shader/img/depth-write.png differ diff --git a/versions/4.0/en/shader/img/dot.jpg b/versions/4.0/en/shader/img/dot.jpg new file mode 100644 index 0000000000..2307d500ac Binary files /dev/null and b/versions/4.0/en/shader/img/dot.jpg differ diff --git a/versions/4.0/en/shader/img/effect-by-color.jpg b/versions/4.0/en/shader/img/effect-by-color.jpg new file mode 100644 index 0000000000..2c4a60fceb Binary files /dev/null and b/versions/4.0/en/shader/img/effect-by-color.jpg differ diff --git a/versions/4.0/en/shader/img/effect-inspector.png b/versions/4.0/en/shader/img/effect-inspector.png new file mode 100644 index 0000000000..328801a3bd Binary files /dev/null and b/versions/4.0/en/shader/img/effect-inspector.png differ diff --git a/versions/4.0/en/shader/img/effect-pass.png b/versions/4.0/en/shader/img/effect-pass.png new file mode 100644 index 0000000000..b26034c32a Binary files /dev/null and b/versions/4.0/en/shader/img/effect-pass.png differ diff --git a/versions/4.0/en/shader/img/effect-show.jpg b/versions/4.0/en/shader/img/effect-show.jpg new file mode 100644 index 0000000000..e42bfbdd86 Binary files /dev/null and b/versions/4.0/en/shader/img/effect-show.jpg differ diff --git a/versions/4.0/en/shader/img/effect.png b/versions/4.0/en/shader/img/effect.png new file mode 100644 index 0000000000..9a9e72f402 Binary files /dev/null and b/versions/4.0/en/shader/img/effect.png differ diff --git a/versions/4.0/en/shader/img/emission-map.png b/versions/4.0/en/shader/img/emission-map.png new file mode 100644 index 0000000000..04e1ae60c6 Binary files /dev/null and b/versions/4.0/en/shader/img/emission-map.png differ diff --git a/versions/4.0/en/shader/img/final_alarmclock.jpg b/versions/4.0/en/shader/img/final_alarmclock.jpg new file mode 100644 index 0000000000..c717e4cac6 Binary files /dev/null and b/versions/4.0/en/shader/img/final_alarmclock.jpg differ diff --git a/versions/4.0/en/shader/img/fresnel.jpg b/versions/4.0/en/shader/img/fresnel.jpg new file mode 100644 index 0000000000..58d814dc05 Binary files /dev/null and b/versions/4.0/en/shader/img/fresnel.jpg differ diff --git a/versions/4.0/en/shader/img/intensity.png b/versions/4.0/en/shader/img/intensity.png new file mode 100644 index 0000000000..ee7fc366d9 Binary files /dev/null and b/versions/4.0/en/shader/img/intensity.png differ diff --git a/versions/4.0/en/shader/img/leaves.jpg b/versions/4.0/en/shader/img/leaves.jpg new file mode 100644 index 0000000000..108e71b3ca Binary files /dev/null and b/versions/4.0/en/shader/img/leaves.jpg differ diff --git a/versions/4.0/en/shader/img/load-custom-effect.png b/versions/4.0/en/shader/img/load-custom-effect.png new file mode 100644 index 0000000000..23e89898fc Binary files /dev/null and b/versions/4.0/en/shader/img/load-custom-effect.png differ diff --git a/versions/4.0/en/shader/img/macro-defined.png b/versions/4.0/en/shader/img/macro-defined.png new file mode 100644 index 0000000000..efa5e1f23c Binary files /dev/null and b/versions/4.0/en/shader/img/macro-defined.png differ diff --git a/versions/4.0/en/shader/img/macro-options-example.png b/versions/4.0/en/shader/img/macro-options-example.png new file mode 100644 index 0000000000..7d01f4c52e Binary files /dev/null and b/versions/4.0/en/shader/img/macro-options-example.png differ diff --git a/versions/4.0/en/shader/img/macro-pre-define.png b/versions/4.0/en/shader/img/macro-pre-define.png new file mode 100644 index 0000000000..18291cef0a Binary files /dev/null and b/versions/4.0/en/shader/img/macro-pre-define.png differ diff --git a/versions/4.0/en/shader/img/macro-preview.png b/versions/4.0/en/shader/img/macro-preview.png new file mode 100644 index 0000000000..d46e1d468e Binary files /dev/null and b/versions/4.0/en/shader/img/macro-preview.png differ diff --git a/versions/4.0/en/shader/img/macro-property.png b/versions/4.0/en/shader/img/macro-property.png new file mode 100644 index 0000000000..620869ac7d Binary files /dev/null and b/versions/4.0/en/shader/img/macro-property.png differ diff --git a/versions/4.0/en/shader/img/macro-range-example.png b/versions/4.0/en/shader/img/macro-range-example.png new file mode 100644 index 0000000000..ef0a5672bc Binary files /dev/null and b/versions/4.0/en/shader/img/macro-range-example.png differ diff --git a/versions/4.0/en/shader/img/macro-remapping.png b/versions/4.0/en/shader/img/macro-remapping.png new file mode 100644 index 0000000000..6bac16ae5e Binary files /dev/null and b/versions/4.0/en/shader/img/macro-remapping.png differ diff --git a/versions/4.0/en/shader/img/macro-simple.png b/versions/4.0/en/shader/img/macro-simple.png new file mode 100644 index 0000000000..65c8f24591 Binary files /dev/null and b/versions/4.0/en/shader/img/macro-simple.png differ diff --git a/versions/4.0/en/shader/img/metallic.jpg b/versions/4.0/en/shader/img/metallic.jpg new file mode 100644 index 0000000000..b62c8e1c2a Binary files /dev/null and b/versions/4.0/en/shader/img/metallic.jpg differ diff --git a/versions/4.0/en/shader/img/metallicRoughnessMap.png b/versions/4.0/en/shader/img/metallicRoughnessMap.png new file mode 100644 index 0000000000..2b3c60b222 Binary files /dev/null and b/versions/4.0/en/shader/img/metallicRoughnessMap.png differ diff --git a/versions/4.0/en/shader/img/new-chunk.png b/versions/4.0/en/shader/img/new-chunk.png new file mode 100644 index 0000000000..836b124fc4 Binary files /dev/null and b/versions/4.0/en/shader/img/new-chunk.png differ diff --git a/versions/4.0/en/shader/img/new-effect.png b/versions/4.0/en/shader/img/new-effect.png new file mode 100644 index 0000000000..e1cd07306c Binary files /dev/null and b/versions/4.0/en/shader/img/new-effect.png differ diff --git a/versions/4.0/en/shader/img/no-outline.png b/versions/4.0/en/shader/img/no-outline.png new file mode 100644 index 0000000000..fdad671a9b Binary files /dev/null and b/versions/4.0/en/shader/img/no-outline.png differ diff --git a/versions/4.0/en/shader/img/normal-map.png b/versions/4.0/en/shader/img/normal-map.png new file mode 100644 index 0000000000..f07a6d5a43 Binary files /dev/null and b/versions/4.0/en/shader/img/normal-map.png differ diff --git a/versions/4.0/en/shader/img/normal.jpg b/versions/4.0/en/shader/img/normal.jpg new file mode 100644 index 0000000000..c0685f802b Binary files /dev/null and b/versions/4.0/en/shader/img/normal.jpg differ diff --git a/versions/4.0/en/shader/img/occluation.png b/versions/4.0/en/shader/img/occluation.png new file mode 100644 index 0000000000..8fe893fc42 Binary files /dev/null and b/versions/4.0/en/shader/img/occluation.png differ diff --git a/versions/4.0/en/shader/img/occlusion-map.png b/versions/4.0/en/shader/img/occlusion-map.png new file mode 100644 index 0000000000..d589c09058 Binary files /dev/null and b/versions/4.0/en/shader/img/occlusion-map.png differ diff --git a/versions/4.0/en/shader/img/opt-overview.jpg b/versions/4.0/en/shader/img/opt-overview.jpg new file mode 100644 index 0000000000..47fb62fa9a Binary files /dev/null and b/versions/4.0/en/shader/img/opt-overview.jpg differ diff --git a/versions/4.0/en/shader/img/outline-off.png b/versions/4.0/en/shader/img/outline-off.png new file mode 100644 index 0000000000..468a60727a Binary files /dev/null and b/versions/4.0/en/shader/img/outline-off.png differ diff --git a/versions/4.0/en/shader/img/outline-on.png b/versions/4.0/en/shader/img/outline-on.png new file mode 100644 index 0000000000..56d187f365 Binary files /dev/null and b/versions/4.0/en/shader/img/outline-on.png differ diff --git a/versions/4.0/en/shader/img/outline.png b/versions/4.0/en/shader/img/outline.png new file mode 100644 index 0000000000..52d11dbee9 Binary files /dev/null and b/versions/4.0/en/shader/img/outline.png differ diff --git a/versions/4.0/en/shader/img/pbr-maps.png b/versions/4.0/en/shader/img/pbr-maps.png new file mode 100644 index 0000000000..4c56a2a198 Binary files /dev/null and b/versions/4.0/en/shader/img/pbr-maps.png differ diff --git a/versions/4.0/en/shader/img/pbr-param.png b/versions/4.0/en/shader/img/pbr-param.png new file mode 100644 index 0000000000..fe15dca42c Binary files /dev/null and b/versions/4.0/en/shader/img/pbr-param.png differ diff --git a/versions/4.0/en/shader/img/pipeline-effect.png b/versions/4.0/en/shader/img/pipeline-effect.png new file mode 100644 index 0000000000..a68b5d1959 Binary files /dev/null and b/versions/4.0/en/shader/img/pipeline-effect.png differ diff --git a/versions/4.0/en/shader/img/precompile.png b/versions/4.0/en/shader/img/precompile.png new file mode 100644 index 0000000000..06a0787996 Binary files /dev/null and b/versions/4.0/en/shader/img/precompile.png differ diff --git a/versions/4.0/en/shader/img/preview-instensity.jpg b/versions/4.0/en/shader/img/preview-instensity.jpg new file mode 100644 index 0000000000..50b0ff6ade Binary files /dev/null and b/versions/4.0/en/shader/img/preview-instensity.jpg differ diff --git a/versions/4.0/en/shader/img/ramp-texture.png b/versions/4.0/en/shader/img/ramp-texture.png new file mode 100644 index 0000000000..b3b75d8bf9 Binary files /dev/null and b/versions/4.0/en/shader/img/ramp-texture.png differ diff --git a/versions/4.0/en/shader/img/render-to-xxx.png b/versions/4.0/en/shader/img/render-to-xxx.png new file mode 100644 index 0000000000..0350fc8bb8 Binary files /dev/null and b/versions/4.0/en/shader/img/render-to-xxx.png differ diff --git a/versions/4.0/en/shader/img/rim-light-effect.png b/versions/4.0/en/shader/img/rim-light-effect.png new file mode 100644 index 0000000000..6e0bc2d430 Binary files /dev/null and b/versions/4.0/en/shader/img/rim-light-effect.png differ diff --git a/versions/4.0/en/shader/img/rim-preview.jpg b/versions/4.0/en/shader/img/rim-preview.jpg new file mode 100644 index 0000000000..28ea3bc33d Binary files /dev/null and b/versions/4.0/en/shader/img/rim-preview.jpg differ diff --git a/versions/4.0/en/shader/img/rim-slider.png b/versions/4.0/en/shader/img/rim-slider.png new file mode 100644 index 0000000000..c38ee04a77 Binary files /dev/null and b/versions/4.0/en/shader/img/rim-slider.png differ diff --git a/versions/4.0/en/shader/img/roughness.jpg b/versions/4.0/en/shader/img/roughness.jpg new file mode 100644 index 0000000000..c9e95ce275 Binary files /dev/null and b/versions/4.0/en/shader/img/roughness.jpg differ diff --git a/versions/4.0/en/shader/img/select-effect.png b/versions/4.0/en/shader/img/select-effect.png new file mode 100644 index 0000000000..1893b2de28 Binary files /dev/null and b/versions/4.0/en/shader/img/select-effect.png differ diff --git a/versions/4.0/en/shader/img/shade-color.png b/versions/4.0/en/shader/img/shade-color.png new file mode 100644 index 0000000000..cc3917df01 Binary files /dev/null and b/versions/4.0/en/shader/img/shade-color.png differ diff --git a/versions/4.0/en/shader/img/shade-map.png b/versions/4.0/en/shader/img/shade-map.png new file mode 100644 index 0000000000..6907a46b62 Binary files /dev/null and b/versions/4.0/en/shader/img/shade-map.png differ diff --git a/versions/4.0/en/shader/img/surface-effects.png b/versions/4.0/en/shader/img/surface-effects.png new file mode 100644 index 0000000000..35b70687cd Binary files /dev/null and b/versions/4.0/en/shader/img/surface-effects.png differ diff --git a/versions/4.0/en/shader/img/surface-node.png b/versions/4.0/en/shader/img/surface-node.png new file mode 100644 index 0000000000..9253cf54f5 Binary files /dev/null and b/versions/4.0/en/shader/img/surface-node.png differ diff --git a/versions/4.0/en/shader/img/toon-depth-bias.png b/versions/4.0/en/shader/img/toon-depth-bias.png new file mode 100644 index 0000000000..628388fdcd Binary files /dev/null and b/versions/4.0/en/shader/img/toon-depth-bias.png differ diff --git a/versions/4.0/en/shader/img/toon-pass.png b/versions/4.0/en/shader/img/toon-pass.png new file mode 100644 index 0000000000..50f7743481 Binary files /dev/null and b/versions/4.0/en/shader/img/toon-pass.png differ diff --git a/versions/4.0/en/shader/img/toon-surf.png b/versions/4.0/en/shader/img/toon-surf.png new file mode 100644 index 0000000000..e25a087509 Binary files /dev/null and b/versions/4.0/en/shader/img/toon-surf.png differ diff --git a/versions/4.0/en/shader/img/toon.png b/versions/4.0/en/shader/img/toon.png new file mode 100644 index 0000000000..dfddf117f5 Binary files /dev/null and b/versions/4.0/en/shader/img/toon.png differ diff --git a/versions/4.0/en/shader/img/transparency.png b/versions/4.0/en/shader/img/transparency.png new file mode 100644 index 0000000000..acc486f8cf Binary files /dev/null and b/versions/4.0/en/shader/img/transparency.png differ diff --git a/versions/4.0/en/shader/img/unlit-shademode.png b/versions/4.0/en/shader/img/unlit-shademode.png new file mode 100644 index 0000000000..e06d447c07 Binary files /dev/null and b/versions/4.0/en/shader/img/unlit-shademode.png differ diff --git a/versions/4.0/en/shader/img/use-pbr-map.png b/versions/4.0/en/shader/img/use-pbr-map.png new file mode 100644 index 0000000000..aaacd77c40 Binary files /dev/null and b/versions/4.0/en/shader/img/use-pbr-map.png differ diff --git a/versions/4.0/en/shader/img/use-shade-map1.png b/versions/4.0/en/shader/img/use-shade-map1.png new file mode 100644 index 0000000000..ad68696c1f Binary files /dev/null and b/versions/4.0/en/shader/img/use-shade-map1.png differ diff --git a/versions/4.0/en/shader/img/view-direction.png b/versions/4.0/en/shader/img/view-direction.png new file mode 100644 index 0000000000..ba1dc6676e Binary files /dev/null and b/versions/4.0/en/shader/img/view-direction.png differ diff --git a/versions/4.0/en/shader/img/view-x-gradient.png b/versions/4.0/en/shader/img/view-x-gradient.png new file mode 100644 index 0000000000..92d07effca Binary files /dev/null and b/versions/4.0/en/shader/img/view-x-gradient.png differ diff --git a/versions/4.0/en/shader/img/vs-ext.png b/versions/4.0/en/shader/img/vs-ext.png new file mode 100644 index 0000000000..26432d119a Binary files /dev/null and b/versions/4.0/en/shader/img/vs-ext.png differ diff --git a/versions/4.0/en/shader/index.md b/versions/4.0/en/shader/index.md new file mode 100644 index 0000000000..745597b372 --- /dev/null +++ b/versions/4.0/en/shader/index.md @@ -0,0 +1,55 @@ +# Cocos Shader + +![effect-show](img/effect-show.jpg) + +In modern graphics engines, to correctly render objects, it's necessary to write Vertex and Fragment code snippets that run on the GPU. Those code snippets are known as shaders. + +On the devices driven by OpenGL, a shader programming language called GLSL (OpenGL Shading Language) is widely used. + +To adapt the industrial workflows and enhance the usability of shader fragments, Cocos Creator has encapsulated a shader framework based on GLSL — [Cocos Shader](./effect-syntax.md). + +This chapter mainly introduces the working and usage of Cocos Shader. + +## Table of Contents + +- [Create and Use Shader](effect-inspector.md) +- [Built-in Shaders](effect-builtin.md) + - [Physically Based Rendering - PBR](effect-builtin-pbr.md) + - [Toon Shading](effect-builtin-toon.md) + - [Unlit Shading](effect-builtin-unlit.md) +- [Syntax](effect-syntax.md) + - [Optional Pass Parameters](pass-parameter-list.md) + - [YAML 101](yaml-101.md) + - [GLSL](glsl.md) + - [Preprocessor Macro Definition](macros.md) + - [Chunk](effect-chunk-index.md) +- [Built-in Uniforms](uniform.md) +- [Common Functions](./common-functions.md) +- [Render Flow of Forward Rendering and Deferred Shading](./forward-and-deferred.md) +- [Surface Shader](surface-shader.md) + - [Guide to Built-in Surface Shader](./surface-shader/builtin-surface-shader.md) + - [Surface Shader Overview](./surface-shader/surface-shader-structure.md) + - [Surface Shader Execution Flow](./surface-shader/shader-code-flow.md) + - [Include](./surface-shader/includes.md) + - [Macro Remapping](./surface-shader/macro-remapping.md) + - [Function Replacement Using Macros](./surface-shader/function-replace.md) + - [Surface Shader Built-in Replaceable Functions](./surface-shader/surface-function.md) + - [Render Usages](./surface-shader/render-usage.md) + - [Lighting Models](./surface-shader/lighting-mode.md) + - [Surface Material Data Structure](./surface-shader/surface-data-struct.md) + - [Shader Stages](./surface-shader/shader-stage.md) + - [Shader Assembly](./surface-shader/shader-assembly.md) + - [VS Inputs](./surface-shader/vs-input.md) + - [FS Inputs](./surface-shader/fs-input.md) + - [Customize Surface Shader](./surface-shader/customize-surface-shader.md) + - [Rendering Debug View](./surface-shader/rendering-debug-view.md) +- [Legacy Shader](./legacy-shader/legacy-shader.md) + - [Guide to Built-in Legacy Shaders](./legacy-shader/legacy-shader-builtins.md) + - [Legacy Shader Key Functions and Structures](./legacy-shader/legacy-shader-func-struct.md) +- [Write Shaders](./write-effect-overview.md) + - [2D Sprite Shader: Gradient](./write-effect-2d-sprite-gradient.md) + - [3D Shader: RimLight](./write-effect-3d-rim-light.md) +- [Instanced Attributes](./instanced-attributes.md) +- [UBO Layout](./ubo-layout.md) +- [Fallback to WebGL 1.0](./webgl-100-fallback.md) +- [VSCode Extension - Cocos Effect](./vscode-plugin.md) diff --git a/versions/4.0/en/shader/instanced-attributes.md b/versions/4.0/en/shader/instanced-attributes.md new file mode 100644 index 0000000000..c0ecfe05f8 --- /dev/null +++ b/versions/4.0/en/shader/instanced-attributes.md @@ -0,0 +1,81 @@ +# Instanced Attributes + +The GPU Instancing feature allows for GPU instancing of render objects with the same mesh and material. If we want to modify the visual effect for one of the instanced objects without breaking this feature, we need to add instanced attributes. + +Let's take adding a color property as an example. + +## Add Attribute + +GPU Instancing need to add new attributes and they should be placed within the `USE_INSTANCING` macro to avoid compilation errors. + +```glsl +#if USE_INSTANCING // when instancing is enabled + #pragma format(RGBA8) // normalized unsigned byte + in vec4 a_instanced_color; +#endif +``` + +## Passing to FS + +The instanced attributes are vertex attributes and can only be accessed in the vertex shader, and the `a_instanced_color` is only used to modify the object's color in the fragment shader. Therefore, we need to declare an `out` variable in the vertex shader that will be used to pass attributes to the fragment shader. Here's an example of the code. + +```glsl +CCProgram vs %{ + #if USE_INSTANCING + out vec4 instancedColor; + #endif + + vec4 vert(){ + ... + #if USE_INSTANCING + instancedColor = a_instanced_color; + #endif + ... + } +}% +``` + +## Access in FS + +To achieve the desired functionality, declare the corresponding `in` variable in the fragment shader to retrieve the value passed from the vertex shader. + +```glsl +CCProgram fs %{ + #if USE_INSTANCING + in vec4 instancedColor; + #endif + + vec4 frag(){ + ... + vec4 o = mainColor; + #if USE_INSTANCING + o *= instancedColor; + #endif + ... + } +}% +``` + +## Set Instanced Attribute in Script + +Instanced Attributes belong to specific instances of render objects and cannot be set through the material panel. Instead, they can be set using the `setInstancedAttribute` method on the `MeshRenderer` component. Here's an example of the code. + +```ts +const comp = node.getComponent(MeshRenderer); +comp.setInstancedAttribute('a_instanced_color', [100, 150, 200, 255]); +``` + +## Notes + +Here are a few points to keep in mind. +1. `#pragma format(RGBA8)` is use to specify the specific data format of the property. The parameter can be any enumeration name from the engine's `GFXFormat`[^1]. If not specified, it defaults to the RGBA32F type. + +2. All instanced attributes are input through the vertex attributes. If you want to use them in the fragment shader, you need to pass them to the fragment shader from vertex shader using the varying variables(`in`,`out`). + +3. Ensure that your code can execute correctly in all branches, regardless of whether the `USE_INSTANCING` is enabled. + +4. The values of instanced attributes are initialized to 0 when engine starts. + +5. If the material has been changed on **MeshRenderer**, all values of instanced attributes will be reset and need to be set again. + +[^1]: Integer attributes are not supported on WebGL-1.0-only platforms. If your project needs to be published on these platforms, always use the floating-point type. diff --git a/versions/4.0/en/shader/legacy-shader/legacy-shader-builtins.md b/versions/4.0/en/shader/legacy-shader/legacy-shader-builtins.md new file mode 100644 index 0000000000..f9e1c07dd2 --- /dev/null +++ b/versions/4.0/en/shader/legacy-shader/legacy-shader-builtins.md @@ -0,0 +1,122 @@ +# Guide to Built-in Legacy Shaders + +The shader code related to Legacy Shader is located in two folders. +- internal/chunks/legacy/ +- internal/effects/legacy/ + +In the `chunks/legacy/` folder, there are some common functions, such as decode, fog, input, output, shadows, skeletal skinning, and so on. + +Both Legacy Shader and Surface Shader will call functions provided in `internal/chunks/builtin/` and `internal/chunks/common/`. + +In the `effects/legacy/` folder, there are three built-in Legacy Shaders: + +- standard: for standard PBR +- terrain: for terrain +- toon: for cartoon rendering + +## Basic Structure + +The code of Legacy Shader is usually composed of several parts: +- `CCEffect`: Describes the techniques, passes, shader entries, rendering states, properties, etc. +- `Shared UBOs`: Used to define uniforms that are needed by both vs and fs together for easy access. +- `Shader Body`: Used to implement the shader code + +The CCEffect and Shared UBOs in Legacy Shaders are consistent with that in Surface Shaders, For more details, please go to [Guide to Built-in Surface Shader](../surface-shader/builtin-surface-shader.md). + +## Shader Functions + +For a better understanding of the rendering process, please first check to [Understand the Render Flow of Forward Rendering and Deferred Rendering](./../forward-and-deferred.md)。 + +### standard(PBR) + +In the legacy/standard.effect file, we can see the shader code. + +```ts +CCProgram standard-vs %{ + //... + void main(){ + StandardVertInput In; + CCVertInput(In); + //... + gl_Position = cc_matProj * (cc_matView * matWorld) * In.position; + } +}% + +CCProgram standard-fs %{ + //... + void surf(out StandardSurface s){ + //s.albedo = ... + //s.occlusion = ... + //s.roughness = ... + //s.metallic = ... + //s.specularIntensity = ... + //s.normal = ... + } + CC_STANDARD_SURFACE_ENTRY() +}% +``` + +You can see that in the vertex shader, the main function is directly used as the entry point, while in the fragment shader, there is only a surf function. + +This is because after expanding the `CC_STANDARD_SURFACE_ENTRY`, it becomes the main function, and this main function will call the surf function. + +### terrain + +The terrain uses StandardSurface as the material surface data structure and `CC_STANDARD_SURFACE_ENTRY` as the entry point. This indicates that the rendering process and lighting calculations of the terrain are completely the same as standard. + +However, because the terrain uses a multi-textures blend, the textures used by the terrain and the code of the surf function are significantly different from the standard. + +### toon + +In legacy/toon.effect, we can see. + +```ts +CCProgram toon-vs %{ + //... + void main(){ + StandardVertInput In; + CCVertInput(In); + //... + gl_Position = cc_matProj * (cc_matView * matWorld) * In.position; + } +}% + +CCProgram toon-fs %{ + //... + void surf(out ToonSurface s){ + //s.baseStep = ... + //s.baseFeather = ... + //s.shadeStep = ... + //s.shadeFeather = ... + //s.shadowCover = ... + } + + void frag(){ + ToonSurface s; surf(s); + vec4 color = CCToonShading(s); + return CCFragOutput(color); + } +}% +``` + +The biggest feature of toon is that it defines an additional outline pass in the CCEffect section, and the code of the outline pass is located in chunks/legacy/main-functions/outline-vs(fs). + +The structure of surface material data is **ToonSurface**, which is different from the one used by the standard. In the frag function, you can see that the lighting calculation of the toon uses a special **CCToonShading**. + +Moreover, toon defines its own frag entry function and does not use the `CC_STANDARD_SURFACE_ENTRY` macro. This means that toon does not support the deferred rendering pipeline. + +### shadow-caster + +You can see that the standard, terrain, and toon all have code fragments related to shadows. + +```ts +CCProgram shadow-caster-vs %{ + //... +}% + +CCProgram shadow-caster-fs %{ + //... +}% +``` + +This set of vs/fs is used for shadow map generation. During the shadow stage, the engine rendering pipeline will search for objects which have the pass with the phase of 'shadow-add' to draw. diff --git a/versions/4.0/en/shader/legacy-shader/legacy-shader-func-struct.md b/versions/4.0/en/shader/legacy-shader/legacy-shader-func-struct.md new file mode 100644 index 0000000000..29498a1e96 --- /dev/null +++ b/versions/4.0/en/shader/legacy-shader/legacy-shader-func-struct.md @@ -0,0 +1,219 @@ +# Legacy Shader Key Functions and Structures + +## CCVertInput[^1] + +- To connect with skeletal animation and data decompression processes, we provide the `CCVertInput` function, which has `general` and `standard` versions, as shown below. + + ```glsl + // Located in ‘input.chunk’, used to handle vertex input + #define CCVertInput(position) \ + CCDecode(position); \ + #if CC_USE_MORPH \ + applyMorph(position); \ + #endif \ + #if CC_USE_SKINNING \ + CCSkin(position); \ + #endif \ + #pragma // + + // Located in ‘input-standard.chunk’, used to handle standard vertex input + #define CCVertInput(In) \ + CCDecode(In); \ + #if CC_USE_MORPH \ + applyMorph(In); \ + #endif \ + #if CC_USE_SKINNING \ + CCSkin(In); \ + #endif \ + #pragma // + + ``` + +- If you only need to get the positions, you can use the `general` version, and the code for this situation is as follows. + + ```glsl + #include + vec4 vert () { + vec3 position; + CCVertInput(position); + // ... TO DO + } + ``` + +- If you also need normals, you can use the `standard` version, as follows. + + ```glsl + #include + vec4 vert () { + StandardVertInput In; + CCVertInput(In); + // ... Now the ‘In.position’ is completely initialized, and can be used in the vertext shader + } + ``` + +In the example code above, the `StandardVertInput` object `In` returns the positions, normals, and tangents in model space, and completes the skinning calculation for the skeletal animation model. + +The definition of the `StandardVertInput` structure is as follows. + +```glsl +struct StandardVertInput { + highp vec4 position; + vec3 normal; + vec4 tangent; +}; +``` + +> **Note**: After including the header files, there is no need to declare these attributes anymore, such as `a_position`, `a_normal`, `a_tangent`, etc. For other vertex attributes, such as uv, etc., still need to be declared before used. + +If you want to connect with the dynamic mesh batching and GPU Instancing features, you need to include the `cc-local-batch` and use the `CCGetWorldMatrix` function to get the world matrix. Here's an example. + +```glsl +mat4 matWorld; +CCGetWorldMatrix(matWorld); + +mat4 matWorld, matWorldIT; +CCGetWorldMatrixFull(matWorld, matWorldIT); +``` + +For more details, please refer to [Cocos Shader Built-in Uniforms](uniform.md)。 + +## CCFragOutput + +Cocos Shader provides a `CCFragOutput` function to simplify the output of the fragment shader, which can be used to directly return the values needed by the fragment shader. Here is an example code. + +```glsl +#include +vec4 frag () { + vec4 o = vec4(0.0); + // ... + return CCFragOutput(o); +} +``` + +The `CCFragOutput` will decide whether to perform `ToneMap` based on the pipeline type. This way, the intermediate color does not need to distinguish whether the current rendering pipeline is an HDR process. + +Here is an example code. + +```glsl +vec4 CCFragOutput (vec4 color) { + #if CC_USE_HDR + color.rgb = ACESToneMap(color.rgb); + #endif + color.rgb = LinearToSRGB(color.rgb); + return color; +} +``` + +> **Note**:If you use the `CCFragOutput` function to output the final color of fragment shaders, the color must be converted to the `Linear` space before performing calculations. This is because the `CCFragOutput` assumes that the passed parameters are in the `Linear` space and will always call the `LinearToSRGB` function to transcode. + +If you need to include standard PBR, you can use the `StandardSurface` structure together with the `CCStandardShadingBase` function to form a PBR shading process. + +The content of the `StandardSurface` structure is as follows. + +```glsl + +struct StandardSurface { + // albedo + vec4 albedo; + // these two need to be in the same coordinate system + vec3 position; + vec3 normal; + // emissive + vec3 emissive; + // light map + vec3 lightmap; + float lightmap_test; + // PBR params + float roughness; + float metallic; + float occlusion; +}; +``` + +The example code is as below. + +```glsl +#include +#include +void surf (out StandardSurface s) { + // fill in your data here +} +vec4 frag () { + StandardSurface s; surf(s); + vec4 color = CCStandardShadingBase(s); + return CCFragOutput(color); +} +``` + +You can refer to the `builtin-standard.effect` file, and use the combination of the `surf` function and the `CC_STANDARD_SURFACE_ENTRY()` macro. + +The `CC_STANDARD_SURFACE_ENTRY()` is a wrapper that based on the render pipeline type, uses the `surf` function to construct a usable `main` function for the fragments. Here is an example of the code. + +```glsl +CCProgram shader-fs %{ + #include + + void surf (out StandardSurface s) { + // fill in your data here + } + + CC_STANDARD_SURFACE_ENTRY() +}% +``` + +## StandardSurface + +The `StandardSurface` is the structure of PBR material data that records the surface information required for lighting calculations. The `surf` function in Legacy Shaders is used to fill data and it will be used during the lighting phase. + +```ts +struct StandardSurface { + // albedo + vec4 albedo; + // these two need to be in the same coordinate system + HIGHP_VALUE_STRUCT_DEFINE(vec3, position); + vec3 normal; + // emissive + vec3 emissive; + // light map + vec3 lightmap; + float lightmap_test; + // PBR params + float roughness; + float metallic; + float occlusion; + float specularIntensity; + + #if CC_RECEIVE_SHADOW + vec2 shadowBias; + #endif +}; +``` + +## ToonSurface + +```ts +struct ToonSurface { + vec4 baseColor; + vec4 specular; + // these two need to be in the same coordinate system + HIGHP_VALUE_STRUCT_DEFINE(vec3, position); + vec3 normal; + // shading params + vec3 shade1; + vec3 shade2; + vec3 emissive; + float baseStep; + float baseFeather; + float shadeStep; + float shadeFeather; + float shadowCover; + + #if CC_RECEIVE_SHADOW + vec2 shadowBias; + #endif +}; +``` + +Similar to the `StandardSurface`, the `ToonSurface` is also used for surface information, but it is a structure specifically for cartoon rendering. + +[^1]: Nothing to do with shaders that are not based on Mesh rendering, such as particles, Sprite, post-process, etc. diff --git a/versions/4.0/en/shader/legacy-shader/legacy-shader.md b/versions/4.0/en/shader/legacy-shader/legacy-shader.md new file mode 100644 index 0000000000..36b04f64f2 --- /dev/null +++ b/versions/4.0/en/shader/legacy-shader/legacy-shader.md @@ -0,0 +1,17 @@ +# Legacy Shader + +Compare to Legacy Shader, Surface Shader unifies the shader process and exposes fewer details to the shader writers. Therefore, starting from version 3.72, Surface Shader appears as the default 'builtin-standard'. + +However, both Legacy Shader and Surface Shader have their pros and cons. + +| Type | Pros | Cons | +| :------- | :--- | :--- | +| Legacy Shader | More flexible when facing special requirements | Exposes too many details to the users, makes it difficult to maintain when the engine is upgraded | +| Surface Shader | Unified shading process, no need to worry about details; User-level code is easier to maintain when the engine is upgraded | Need to well-understand the whole implementation mechanism to master;Limited customizable features | + +In addition, the `builtin-unit.effect` offered by the engine still uses part of the legacy shader library. + +Mastering Legacy Shader can also help you to understand more implementation details of Cocos Shaders. + +- [Guide to Built-in Legacy Shaders](./legacy-shader-builtins.md) +- [Legacy Shader Key Functions and Structures](./legacy-shader-func-struct.md) diff --git a/versions/4.0/en/shader/macros.md b/versions/4.0/en/shader/macros.md new file mode 100644 index 0000000000..01c1bff4db --- /dev/null +++ b/versions/4.0/en/shader/macros.md @@ -0,0 +1,116 @@ +# Preprocessor Macro Definition + +Preprocessing macros only take effect when Cocos Effect is compiled. Different combinations of preprocessing macros will generate different codes to better manage the content of Cocos Effect code, and the generated shader code is redundant and efficient. + +Preprocessing macros only take effect when Cocos Effect is compiled, and different combinations of preprocessing macros generate different code to better manage Cocos Effect code content while generating non-redundant and efficient shader code. + +Cocos Creator will collect all macro definitions that appear in Cocos Effect when loading resources, and then display them in the **Inspector** panel, which is convenient for users to make visual adjustments, as shown in the following figure: + +![macro-simple](img/macro-simple.png) + +Taking the use of the preprocessing macro `USE_TEXTURE` as an example, the code example is as follows: + +```glsl +CCProgram unlit-vs %{ + #if USE_TEXTURE + // ... + #endif +}% +``` + +The declaration rules for macro definitions are as follows: + +- All macro definitions default to `false`, so when defining a simple macro definition (such as a macro for a boolean switch), its default value cannot be specified, but can be modified through the **Inspector** panel or code. If there is a mutually exclusive relationship between certain macros in the design (cannot be true at the same time), it can be handled by using the macro declared by tag. For details, please refer to the **Macro Tags** section below. +- All custom macros that appear in Shader will be explicitly defined at runtime (the default value is 0), so **except for GLSL language built-in macros (extensions at the beginning of `GL_`, etc.)**, please do not use `#ifdef` or `#if defined` is used for judgment, otherwise the execution result will always be true; +- Macro definitions can be used not only in `CCProgram` to control the code logic within the macro definition, but also in `CCEffect` to associate the display state of editable properties with the macro definition. + +As shown below, `mainTexture` will only be displayed on the **Inspector** panel when the `USE_TEXTURE` preprocessor macro is turned on: + +```glsl +CCEffect %{ + # ... + properties: + mainTexture: { value: white, editor: { parent: USE_TEXTURE } } + # ... +}% + +CCProgram unlit-fs %{ + // ... + vec4 frag () { + #if USE_TEXTURE + // Handle mainTexture logic + #endif + } +}% +``` + +![macro-property](img/macro-property.png) + +## Macro Tags + +Although the effect compiler will try to be smart and collect all pre-processing branches, sometimes there are more complicated cases: + +```glsl +// macro defined within certain numerical 'range' +#if LAYERS == 4 + // ... +#elif LAYERS == 5 + // ... +#endif +// multiple discrete 'options' +float metallic = texture(pbrMap, uv).METALLIC_SOURCE; +``` + +For these special usages, you'll have to explicitly declare the macro, using macro tags: + +| Tag | Description | Default Value | Usage | +| :------ | :---------- | :------------ | :---- | +| **range** | A two-element array, specifying minimum and maximum value, both inclusive | [0, 3] | For macros with bounded range. The bound should be as tight as possible | +| **options** | An arbitrary-length array, specifying every possible options | nothing | For macros with discrete, explicit choices | + +Declarations for the above case are: + +```glsl +#pragma define-meta LAYERS range([4, 5]) +#pragma define-meta METALLIC_SOURCE options([r, g, b, a]) +``` + +The first line declares a macro named `LAYERS`, with possible range of [4, 5]. + +The second line declares a macro named `METALLIC_SOURCE`, with four possible options: 'r', 'g', 'b', 'a'. + +> **Note**: +> 1. every tag accepts a single parameter, in the syntax of YAML. +> 2. Before v3.5, the syntax for Macro Tags feature is `#pragma define`, but from v3.5, the syntax will be automatically upgraded to `#pragma define-meta` during effect migration process, please pay attention to use the right syntax if you are writing new effect or using external effects without meta file. + +### Function-like Macros + +Due to lack of native support in WebGL platform, functional macros are provided as an effect compile-time feature, all references will be expanded in the output shader. + +This is an good match for inlining some simple utility functions, or similar code repeating several times. + +In fact, many built-in utility functions are functional macros: + +```glsl +#pragma define CCDecode(position) \ + position = vec4(a_position, 1.0) +#pragma define CCVertInput(position) \ + CCDecode(position); \ + #if CC_USE_SKINNING \ + CCSkin(position); \ + #endif \ + #pragma // empty pragma trick to get rid of trailing semicolons at effect compile time +``` + +Meanwhile, same as the macro system in C/C++, the mechanism does nothing on checking the [Hygienic Macro WikiPedia Entry](https://en.wikipedia.org/wiki/Hygienic_macro). Any issues will have to be dealt with by developers manually: + +```glsl +// please do be careful with unhygienic macros like this +#pragma define INCI(i) do { int a=0; ++i; } while(0) +// when invoking +int a = 4, b = 8; +INCI(b); // correct, b would be 9 after this +INCI(a); // wrong! a would still be 4 +``` + +> **Note**: Before v3.5, the standard define in glsl is occupied by Functional Macros, so developers aren't able to use standard define like `#ifdef` or `#ifndef`. But from v3.5, the syntax of Functional Macros is upgraded to `#pragma define`. All Functional Macros will be automatically upgraded during effect migration process, and developers can directly use standard defines inn the shader. Just need some extra attention to use the right syntax if you are writing new effect or using external effects without meta file. diff --git a/versions/4.0/en/shader/pass-parameter-list.md b/versions/4.0/en/shader/pass-parameter-list.md new file mode 100644 index 0000000000..6dd278849c --- /dev/null +++ b/versions/4.0/en/shader/pass-parameter-list.md @@ -0,0 +1,273 @@ +# Optional Pass Parameters + +The parameters in Pass are mainly divided into two parts: +- Developer-customizable **Inspector** panel parameter `properties`. +- `PipelineStates` provided by the engine to control the rendering pipeline state. + +## Properties + +`properties` is used to alias the `uniform` defined in the shader. This mapping can be a complete mapping of a `uniform`, or a mapping of a specific component (using the `target` parameter), the code example is as follows: + +```yaml +properties: + albedo: { value: [1, 1, 1, 1] } # uniform vec4 albedo + roughness: { value: 0.8, target: pbrParams.g } # uniform vec4 pbrParams + offset: { value: [0, 0], target: tilingOffset.zw } # uniform vec4 tilingOffset +# say there is another uniform, vec4 emissive, that doesn't appear here +# so it will be assigned a default value of [0, 0, 0, 0] and will not appear in the inspector +``` + +By default, property parameters defined in `properties` are exposed and displayed in the **Inspector** panel of the editor for easy visual control. + +If you don't want to show the properties on the **Inspector** panel, you can add `editor: { visible: false }` when defining the property, the code example is as follows: + +```yaml +properties: + factor: { value: 1.0, editor: { visible: false } } +``` + +In TypeScript, you can use the `setProperty` method of the `Material` class and the `setUniform` method of `Pass` to set pass properties, the code example is as follows: + +```js +mat.setProperty('emissive', Color.GREY); // Directly set the corresponding ‘Uniform’ variable +mat.setProperty('albedo', Color.RED); +mat.setProperty('roughness', 0.2); // Set only the corresponding component +const h = mat.passes[0].getHandle('offset'); // Get the handle of the corresponding Uniform +mat.passes[0].setUniform(h, new Vec2(0.5, 0.5)); // Use 'Pass.setUniform' to set the Uniform property +``` + +> **Note**: `uniform` defined in Effect can also be set using the above code, even if not defined in `properties`. + +If `uniform` is not specified, Engine will give a default value at runtime based on the automatically parsed data type. For more information on default values, please refer to the description below. + +In order to conveniently declare each `property` sub-property, you can directly declare the `__metadata__` item in `properties`, and all `property` will inherit its declared content, such as: + +```yaml +properties: + __metadata__: { editor: { visible: false } } + a: { value: [1, 1, 0, 0] } + b: { editor: { type: color } } + c: { editor: { visible: true } } +``` + +In this way, the declared parameters of uniform `a` and `b` will not be affected, but will not be displayed in the **Inspector** (visible is false), and uniform `c` will still be displayed normally . + +### Property parameter list + +The configurable parameters in Property are shown in the table below. Any configurable fields can be omitted if they are the same as the default values. + +| parameter | default value | option | remark | +| :--- | :---- | :---- | :-- | +| target | **undefined** | undefined | Any valid uniform channel, which can specify a single or multiple consecutive channels, but cannot be randomly rearranged | +| value | | | See the introduction in the **Default Values** section below | +| sampler.
minFilter | **linear** | none, point, linear, anisotropic | | +| sampler.
magFilter | **linear** | none, point, linear, anisotropic | | +| sampler.
mipFilter | **none** | none, point, linear, anisotropic | | +| sampler.
addressU | **wrap** | wrap, mirror, clamp, border | | +| sampler.
addressV | **wrap** | wrap, mirror, clamp, border | | +| sampler.
addressW | **wrap** | wrap, mirror, clamp, border | | +| sampler.
maxAnisotropy | **16** | 16 | | +| sampler.
cmpFunc | **never** | never, less, equal, less_equal, greater, not_equal, greater_equal, always | | +| sampler.
borderColor | **[0, 0, 0, 0]** | [0, 0, 0, 0] | | +| sampler.
minLOD | **0** | 0 | | +| sampler.
maxLOD | **0** | 0 | If mipmap is allowed, the maximum mip value should be modified according to the map | +| sampler.
mipLODBias | **0** | 0 | | +| editor.
displayName | **\*property name** | \*property name | any string | +| editor.
type | **vector** | vector, color | | +| editor.
visible | **true** | true, false | | +| editor.
tooltip | **\*property name** | \*property name | any string | +| editor.
range | **undefined** | undefined, [ min, max, [step] ] | | +| editor.
deprecated | **false** | true, false | Data marked deprecated means that it was updated when imported or deprecated in the current version, and its contents are automatically deleted when saved | + +### Property default value + +For the default value of Property, Cocos Effect makes the following provisions: + +| types | default values | optional items | +| :---------- | :----- | :------ | +| int | 0 | +| ivec2 | [0, 0] | +| ivec3 | [0, 0, 0] | +| ivec4 | [0, 0, 0, 0] | +| float | 0 | +| vec2 | [0, 0] | +| vec3 | [0, 0, 0] | +| vec4 | [0, 0, 0, 0] | +| sampler2D | **default** | black, grey, white, normal, default | +| samplerCube | **default-cube** | black-cube, white-cube, default-cube | + +For `defines`, there are the following provisions: +- boolean: The default value of boolean type is false. +- number: The default value of the number type is 0, and the default value range is [0, 3]. +- string: The default value of type string is the first element of the options array. + +## PipelineStates + +The following are `PipelineStates` related parameters, all parameters are case insensitive. + +| parameter | description | default value | remark | +| :---- | :-- | :----- | :--- | +| switch | Specifies which define the execution of this pass depends on. Can be any valid macro name, but should not have the same name as any define defined in the shader used | undefined | This field does not exist by default, meaning this pass is executed unconditionally | +| priority | Specify the rendering priority of this pass. The smaller the value, the higher the rendering priority. The value range is **0 ~ 255** | 128 | The relative value can be specified in combination with four operators | +| stage | Specifies which stage of the pipeline this pass belongs to. Can be any registered stage name in the runtime pipeline | **default** | For the default forward pipeline, there is only one stage `default` | +| phase | Specifies which phase of the pipeline this pass belongs to. Can be any registered Phase name in the runtime pipeline | **default** | For the default forward pipeline, can be `default`, `forward-add` or `shadow-caster` | +| propertyIndex | Specify which pass the uniform attribute data of this pass runtime should be consistent with. For example, the pass such as forward add needs to be consistent with the base pass to ensure the correct rendering effect. Can be any valid pass index | undefined | Once this parameter is specified, no further properties for this pass will be displayed on the material panel | +| embeddedMacros | Specifies a constant macro that is additionally defined on the basis of the shader of this pass, which can be an object containing any macro key-value pair | undefined | This parameter can be used to reuse shader resources in multiple passes only when the macro definitions are different | +| properties | Properties stores the customizable parameters of this pass that need to be displayed on the **Inspector** | | See the **Properties** section above for details | +| migrations | Migrate old material data | | See the introduction in the **Migrations** section below for detail | +| primitive | Create material vertex data | **triangle_list** | Options include: point_list, line_list, line_strip, line_loop
**triangle_list**, triangle_strip, triangle_fan
line_list_adjacency, line_strip_adjacency
triangle_list_adjacency, triangle_strip_adjacency
triangle_patch_adjacency, quad_patch_list, iso_line_list | +| RasterizerState | Optional render state when rasterizing | | See the introduction in the **RasterizerState** section below | +| DepthStencilState | Testing and Status of Depth and Stencil Caches | | See the introduction of the **DepthStencilState** section below | +| BlendState | Material blend state | **false** | See the introduction in the **BlendState** section below| + +## Migrations + +In general, when using material system, it is hoped that the underlying effect interface will always be forward compatible, but sometimes the best solution for new requirements still contains certain breaking changes. The material resource data is not affected, or at least can be updated more smoothly, using the effect's migration system. + +After the effect is imported successfully, it will **immediately update all the material resources in the project** that depend on this effect. For each material asset, it will try to find all the specified old parameter data (including **property** and **macro definitions**) and then copy or migrate it into the new property. + +> **Note**: Please back up the project before using the migration function to avoid data loss! + +For an existing effect, the migration field is declared as follows: + +```yaml +migrations: + # macros: # macros follows the same rule as properties, without the component-wise features + # USE_MAIN_TEXTURE: { formerlySerializedAs: USE_MAIN_TEXTURE } + properties: + newFloat: { formerlySerializedAs: oldVec4.w } +``` + +For a material that depends on this effect and holds properties in the corresponding pass: + +```json +{ + "oldVec4": { + "__type__": "cc.Vec4", + "x": 1, + "y": 1, + "z": 1, + "w": 0.5 + } +} +``` + +Immediately after the effect is imported, the data is converted into: + +```json +{ + "oldVec4": { + "__type__": "cc.Vec4", + "x": 1, + "y": 1, + "z": 1, + "w": 0.5 + }, + "newFloat": 0.5 +} +``` + +After re-editing and saving this material asset in the **editor** it will become (assuming the effect and property data themselves have not changed): + +```json +{ + "newFloat": 0.5 +} +``` + +Of course, if you want to delete the old data directly when importing, you can add a migration message to specify this: + +```yaml +oldVec4: { removeImmediately: true } +``` + +This can be useful when the project has a lot of old materials and you can be sure that the data for this property is completely redundant. + +Further, note that the channel instruction here simply takes the `w` component, and in fact can do arbitrary shuffles: + +```yaml +newColor: { formerlySerializedAs: someOldColor.yxx } +``` + +Even based on a certain macro definition: + +```yaml +occlusion: { formerlySerializedAs: pbrParams. } +``` + +It is declared here that the new occlusion property is taken from the old `pbrParams`, and the exact component depends on the `OCCLUSION_CHANNEL` macro definition. And if this macro is not defined in the material resource, the `z` channel will be taken by default.
+However, if a material already has data in the `newFloat` field before the migration upgrade, no changes will be made to it, unless forced update mode is specified. + +```yaml +newFloat: { formerlySerializedAs: oldVec4.w! } +``` + +The forced update mode forces the properties of all materials to be updated, regardless of whether this operation overwrites the data. + +> **Note**: The force update operation is performed on every resource event of the editor (corresponding to almost every mouse click, relatively high frequency), so it is just a means for quick testing and debugging, be sure not to submit the effect in force update mode to version control. + +To again summarize the relevant rules set to prevent data loss. +- To avoid loss of valid old data, old data will not be automatically deleted on import, as long as the `removeImmediately` rule is not explicitly specified. +- To avoid valid new data being overwritten, no migration operation will be done for materials that have both old data and corresponding new data if they are not specified as forced update mode. + +## RasterizerState + +| Parameter Name | Description | Default | Optional | +| :--------- | :-- | :----- | :--- | +| isDiscard | Engine Reserved | **false** | true, false | +| polygonMode | Polygon drawing mode | **fill** | point,line,fill| +| shadeModel | Shading model | **flat** | flat, gourand| +| cullMode | Culling mode on rasterization | **back** | front, back, none | +| isFrontFaceCCW| Counterclockwise (CCW) forward or not | **true** | true,false| +| depthBias| Depth bias | **0** | +| depthBiasSlop | Slope of depth deviation | **0** | +| depthBiasClamp | Depth bias clamp | **0** | | +| isDepthClip | Allows depth clipping operations.
Work only on [Vulkan](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VK_EXT_depth_clip_enable.html) | **true** | true, false +| isMultisample| Whether to enable multisampling | **false** | true, false +| lineWidth | line | 1 | + +## DepthStencilState + +| Parameter Name | Description | Default | Optional | +| :--------- | :-- | :----- | :--- | +| depthTest | Whether to open the depth test | **true** | true,false | +| depthWrite | Whether to enable deep buffer writing | **true** |true, false | +| depthFunc | Depth buffer comparison function | **less** | never, less, equal, less_equal, greater, not_equal, greater_equal, always | +| stencilTestFront | Whether to enable the front stencil buffer test | **false** | true, false | +| stencilFuncFront | Front stencil comparison function | **always** | never, less, equal, less_equal, greater, not_equal, greater_equal, always | +| stencilReadMaskFront | Front stencil read mask | **0xffffffff** | 0xffffffff, `[1, 1, 1, 1]` | +| stencilWriteMaskFront | Front stencil write mask | **0xffffffff** | 0xffffffff, `[1, 1, 1, 1]`| +| stencilFailOpFront | How to handle buffer values when front stencil buffer test fails | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilZFailOpFront | How to handle buffer values when front stencil buffer depth test fails | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilPassOpFront | How to handle the buffer values when the stencil buffer test passes | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilRefFront | The comparison function in the front stencil buffer is used to compare the values | **1** | 1, `[0, 0, 0, 1]` | +| stencilTestBack | Whether to open the back stencil buffer test | **false** | true, false | +| stencilFuncBack | Back stencil buffer comparison function | **always** | never, less, equal, less_equal, greater, not_equal, greater_equal, always | +| stencilReadMaskBack | Back stencil buffer read mask | **0xffffffff** | 0xffffffff, `[1, 1, 1, 1]` +| stencilWriteMaskBack | Back stencil buffer write mask | **0xffffffff** | 0xffffffff, `[1, 1, 1, 1]` | +| stencilFailOpBack | How to handle the buffer value when the back stencil buffer test fails | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilZFailOpBack | How to handle the buffer value when the back stencil buffer depth test fails | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilRefBack | The values used for comparison by the compare function in the back stencil buffer | **1** | 1, `[0, 0, 0, 1]` | + +## BlendState + +| Parameter Name | Description | Default | Optional | +| :--- | :--- |:--- | :--- | +| isA2C | Whether to enable translucent anti-aliasing (Alpha To Coverage) | **false** | true, false +| isIndepend | whether RGB and Alpha are blended separately | **false** | true, false +| blendColor | Specifies the blend color | **0** | 0, `[0, 0, 0, 0]` | +| targets | Blending configuration, please refer to **Targets** below | [] | | + +### Targets + +| Parameter Name | Description | Default | Optional | +| :--------- | :--- | :----- | :--- | +| Targets[i].
blend | Whether to enable **Blend** | **false** | true, false | +| Targets[i].
blendEq | Specify the blend function for RGB for **blend source** and **blend destination** | **add** | add, sub, rev_sub | +| Targets[i].
blendSrc | Specifies the RGB blend factor of the **blend source**. | **one** | one, zero, src_alpha_saturate,
src_alpha, one_minus_src_alpha,
dst_alpha, one_minus_dst_alpha,
src_color, one_minus_src_color,
dst_color, one_minus_dst_color,
constant_color, one_minus_constant_color,
constant_alpha, one_minus_constant_alpha | +| Targets[i].
blendDst | Specifies the RGB blend factor for the **blend destination**. | **zero** | one, zero, src_alpha_saturate,
src_alpha, one_minus_src_alpha,
dst_alpha, one_minus_dst_alpha,
src_color, one_minus_src_color,
dst_color, one_minus_dst_color,
constant_color, one_minus_constant_color,
constant_alpha, one_minus_constant_alpha | +| Targets[i].
blendSrcAlpha | Specifies the alpha blend factor of the **blend source**. | **one** | one, zero, src_alpha_saturate,
src_alpha, one_minus_src_alpha,
dst_alpha, one_minus_dst_alpha,
src_color, one_minus_src_color,
dst_color, one_minus_dst_color,
constant_color, one_minus_constant_color,
constant_alpha, one_minus_constant_alpha | +| Targets[i].
blendDstAlpha | Specify the alpha blend factor for the **blend destination** | **zero** | one, zero, src_alpha_saturate,
src_alpha, one_minus_src_alpha,
dst_alpha, one_minus_dst_alpha,
src_color, one_minus_src_color,
dst_color, one_minus_dst_color,
constant_color, one_minus_constant_color,
constant_alpha, one_minus_constant_alpha | +| Targets[i].
blendAlphaEq | Specifies the alpha blending function for **blend source** and **blend destination** | **add** | add, sub, rev_sub | +| Targets[i].
blendColorMask | Specifies whether the RGB, Alpha component can be written to the frame buffer | **all** | all, none, r, g, b, a, rg, rb, ra, gb, ga, ba, rgb, rga, rba, gba | +| dynamics | Dynamically updatable pipeline status | [] | LINE_WIDTH, DEPTH_BIAS, BLEND_CONSTANTS, DEPTH_BOUNDS, STENCIL_WRITE_MASK, STENCIL_COMPARE_MASK | diff --git a/versions/4.0/en/shader/surface-shader.md b/versions/4.0/en/shader/surface-shader.md new file mode 100644 index 0000000000..2f64df6d2b --- /dev/null +++ b/versions/4.0/en/shader/surface-shader.md @@ -0,0 +1,25 @@ +# Surface Shader + +Surface Shader provides an efficient and unified rendering workflow. It allows users to create shaders easily by creating surface material information with concise code, and just specifying the needed lighting and shading models. + +Compared to [Legacy Shader](./legacy-shader/legacy-shader.md), Surface Shader has the advantages of being easier to write and maintain, better version compatibility, and reduced likelihood of rendering errors. It also provides many hight levels features through a unified process, such as unified environmental lighting and rendering debug view. + +Staring from Cocos Creator v3.7.2, the default `builtin-standard` is written in Surface Shader, which can be viewed in the 'Create' menu. The previous shader have been consolidated under the `internal/effects/legacy/` folder. + +List of content about Surface Shader: +- [Built-in Surface Shader Guide](./surface-shader/builtin-surface-shader.md) +- [Surface Shader Overview](./surface-shader/surface-shader-structure.md) +- [Surface Shader Execution Flow](./surface-shader/shader-code-flow.md) +- [include](./surface-shader/includes.md) +- [Marco Remapping](./surface-shader/macro-remapping.md) +- [Function Replacement Using Macros](./surface-shader/function-replace.md) +- [Built-in Replaceable Functions](./surface-shader/surface-function.md) +- [Render Usages](./surface-shader/render-usage.md) +- [Lighting Models](./surface-shader/lighting-mode.md) +- [Surface Material Data Structure](./surface-shader/surface-data-struct.md) +- [Shader Stages](./surface-shader/shader-stage.md) +- [Shader Assembly](./surface-shader/shader-assembly.md) +- [VS Inputs](./surface-shader/vs-input.md) +- [FS Inputs](./surface-shader/fs-input.md) +- [Customize Surface Shader](./surface-shader/customize-surface-shader.md) +- [Rendering Debug View](./surface-shader/rendering-debug-view.md) diff --git a/versions/4.0/en/shader/surface-shader/builtin-surface-shader.md b/versions/4.0/en/shader/surface-shader/builtin-surface-shader.md new file mode 100644 index 0000000000..5787c7726d --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/builtin-surface-shader.md @@ -0,0 +1,347 @@ +# Built-in Surface Shader Guide + +Starting from Cocos Creator 3.7.2, the `builtin-standard.effect` uses the Surface Shader architecture for implementation. + +This article uses the `builtin-standard.effect` as a typical case to explain the details of Surface Shader. + +You can learn about the structure, syntax and rendering process of Surface Shaders. + +The following content is recommended to be read in combination with internal/effects/builtin-standard.effect. + +## Basic Structure + +The Surface Shader code usually consists of several parts. +- `CCEffect`: Describes the techniques, passes, render states, and vertex attributes used in the shader. +- `Shared UBOs`: Defines uniforms that are needed by both vs and fs together for easy access. +- `Macro Remapping`: Maps some internal macros so that they can be displayed on the material panel. +- `Surface Functions`: Used to declare Surface functions related to Surface Shading. +- `Shader Assembly`: Used to assemble the code modules for each vs and fs. + +For more details, please visit [Surface Shader Structure](./surface-shader-structure.md). + +## CCEffect + +To render an object onto the screen, the following information is needed. +- Model data(vertices, UV, normals, etc.) +- Lighting data +- Rotation, translation, scale in world space. +- Render passes +- Render states +- Textures +- Uniforms +- Shader code + +Among them, model data, lighting information, and world space information are independent of the material, while texture, uniform, rendering state, shader code, and render process are part of the material information. + +`CCEffect` describes the above material-related information, and together with the engine rendering pipeline, completes the rendering process of a model. + +### technique + +The built-in Surface Shader implements two rendering techniques, `opaque` and `transparent`. The former is used for rendering non-transparent objects, and the latter is used for rendering semi-transparent objects. + +### pass + +Each technique of the built-in Surface Shader has only one pass, all of which are in PBR. + +Ignoring other details, we can see the outline of a Surface Shader as follows. + +``` +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: standard-vs + frag: standard-fs + properties: &props + ... + + - name: transparent + passes: + - vert: standard-vs + frag: standard-fs + ... + properties: *props +}% +``` + +### Shader Entry(vert and frag) + +The `opaque` and `transparent` techniques are completely identical in terms of shader code, the only difference is in the render states. + +You can see that they use the same vert and frag entries. + +```glsl +- vert: standard-vs + frag: standard-fs +``` + +### properties + +Since `opaque` and `transparent` are completely identical in terms of shader code, the properties involved are the same. + +All properties used in the render process are placed in the properties section. For syntax about properties, you can check [Optional Pass Parameters](./../pass-parameter-list.md) + +### Section Reuse + +In the properties section, you can see that the `properties` of `opaque` is defined as `properties: &props`, while the `properties` of `transparent` is defined as `properties: *props`。 + +This is a reuse mechanism of sections in `CCEffect`. + +`properties: &props` means to name the current `properties` as `props`. + +`properties: *props` means to use the `properties` named `props` as default value. + +The result of the above configuration is: the `transparent` directly uses the `properties` of `opaque`. + +### phase + +By default, Surface Shader participates in the scene rendering stage. But there are also some special stages, such as shadows, reflection probe baking, etc. + +For such requirements, we can add specific passes and mark the phase to achieve the purpose. + +When the Cocos Creator executes rendering, it will get the pass of the corresponding phase in the material for rendering. If there is none, it means that this object does not participate in this phase. + +In Surface Shader is shown as follows. +- forward-add: Used for the additional lighting phase, when the object is affected by lights other than the main light, this will be called. +- shadow-caster: Used for the shadow map rendering phase. +- reflect-map: Used for reflection probe baking + +```ts +- &forward-add + vert: standard-vs + frag: standard-fs + phase: forward-add + ... +- &shadow-caster + vert: shadow-caster-vs + frag: shadow-caster-fs + phase: shadow-caster + ... +- &reflect-map + vert: standard-vs + frag: reflect-map-fs + phase: reflect-map + ... +``` + +As shown in the code above, the `phase` property is used to mark the participating phase of this pass. And `&forward-add`, `&shadow-caster`, `&reflect-map` are names given to this pass, making it easy for subsequent techniques to reuse. + +For example, the `transparent` directly reuses the `forward-add` and the `shadow-caster` passes from `opaque`. + +```ts +- name: transparent + passes: + - vert: standard-vs + frag: standard-fs + ... + properties: *props + - *forward-add + - *shadow-caster +``` + +### Render State + +As mentioned at the beginning. To complete the rendering of a model, not only define the rendering process and the required properties but also need to be combined the render state. + +Render state mainly involves stencil test, depth test, rasterizer state, transparent blending, etc. + +The same rendering process, properties, and shader code combined with different render states, can achieve different effects. + +```ts +depthStencilState: + depthFunc: equal + depthTest: true + depthWrite: false +blendState: + targets: + - blend: true + blendSrc: one + blendDst: one + blendSrcAlpha: zero + blendDstAlpha: one +rasterizerState: + cullMode: front +``` + +Render states have a set of default values, and modifications can be made when necessary. + +For example, `opaque` and `transparent` only differ in render states. + +### Embedded Macros + +```ts + embeddedMacros: { CC_FORCE_FORWARD_SHADING: true } +``` + +Sometimes, we want to enable or disable some macros for a specific pass. You can use the `embeddedMacros` section to do this. + +## includes + +Surface Shader provides two mechanisms for code block references: header files and CCProgram. For details, please see [include](./includes.md). + +## Shared UBO + +Many constants are used by both vs and fs or are needed by multiple techniques and passes. Defining them together for easy access. + +Shared UBOs are essentially part of the Shader code, written in GLSL. + +```ts +CCProgram shared-ubos %{ + uniform Constants { + vec4 tilingOffset; + vec4 albedo; + vec4 albedoScaleAndCutoff; + vec4 pbrParams; + vec4 emissive; + vec4 emissiveScaleParam; + vec4 anisotropyParam; + }; +}% +``` + +In the subsequent assembly process, you only need to add a single line `#include ` to use. + +## Macro Remapping + +For more details about macro remapping, please refer to [Macro Definition and Remapping](./macro-remapping.md)。 + +In the built-in Surface Shader, the `CCProgram macro-remapping` segment is used to organize all the macro-remapping stuff, which makes it easier to manage. + +As can be seen, in the built-in Surface Shader, `#pragma define-meta` is used to redirect many built-in macros to the panel. + +```ts +CCProgram macro-remapping %{ + // ui displayed macros + #pragma define-meta HAS_SECOND_UV + #pragma define-meta USE_TWOSIDE + #pragma define-meta IS_ANISOTROPY + #pragma define-meta USE_VERTEX_COLOR + + #define CC_SURFACES_USE_SECOND_UV HAS_SECOND_UV + #define CC_SURFACES_USE_TWO_SIDED USE_TWOSIDE + #define CC_SURFACES_LIGHTING_ANISOTROPIC IS_ANISOTROPY + #define CC_SURFACES_USE_VERTEX_COLOR USE_VERTEX_COLOR + + // depend on UI macros +#if IS_ANISOTROPY || USE_NORMAL_MAP + #define CC_SURFACES_USE_TANGENT_SPACE 1 +#endif + + // functionality for each effect + #define CC_SURFACES_LIGHTING_ANISOTROPIC_ENVCONVOLUTION_COUNT 31 +}% +``` + +## Surface Functions + +In Surface Shader, two CCProgram sections are defined to handle the specific shader code. +- CCProgram surface-vertex: Used for handling vs-related calculations. +- CCProgram surface-fragment: Used for handling fs-related calculations. + +### CCProgram surface-vertex + +The built-in vs process can basically meet the requirements of Surface Shader, which makes surface-vertex very simple and clean. + +We take the processing of the second UV as an example. + +It first defines the `CC_SURFACES_VERTEX_MODIFY_UV` macro and then implements the `SurfacesVertexModifyUV` method. + +```ts +#define CC_SURFACES_VERTEX_MODIFY_UV +void SurfacesVertexModifyUV(inout SurfacesStandardVertexIntermediate In) +{ + In.texCoord = In.texCoord * tilingOffset.xy + tilingOffset.zw; + #if CC_SURFACES_USE_SECOND_UV + In.texCoord1 = In.texCoord1 * tilingOffset.xy + tilingOffset.zw; + #endif +} +``` + +This is the core mechanism of Surface Shader, which can rewrite internal functions through macro definitions, and meet specific rendering requirements without modifying the internal source code of the shader framework. + +For more details, please refer to [Function Replacement Using Macros](function-replace.md) and [Surface Shader Built-in Replaceable Functions](surface-function.md)。 + +### CCProgram surface-fragment + +`surface-fragment` mainly implements the filling of surface information needed for PBR calculation. + +#### Macro Switch + +```ts +#if USE_ALBEDO_MAP + uniform sampler2D albedoMap; + #pragma define-meta ALBEDO_UV options([v_uv, v_uv1]) +#endif +``` + +We can see, in the built-in Surface Shader, all textures are wrapped by macro definitions. The advantage of this is that you can turn off the corresponding macros as needed to improve performance. + +#### Macros selectable on the Material Panel + +`#pragma define-meta` + name + `options([item0,item1,....])` can define a macro for users to choose. + +Take the following code as an example. + +```ts +#pragma define-meta ALBEDO_UV options([v_uv, v_uv1]) +``` + +On the material panel, ALBEDO_UV will appear as a drop-down selection box. When the Shader compiles, it will be based on the user's selected value. + +For example, if the user selects `v_uv1`, the generated final code is as below. + +```ts +#define ALBEDO_UV v_uv1 +``` + +```ts +#if USE_ALPHA_TEST + #pragma define-meta ALPHA_TEST_CHANNEL options([a, r]) +#endif + +``` + +The same applies to ALPHA_TEST_CHANNEL. By default, the 'a' channel is used, but the 'r' channel can also be an option. + +#### PBR Channels + +```ts +#pragma define OCCLUSION_CHANNEL r +#pragma define ROUGHNESS_CHANNEL g +#pragma define METALLIC_CHANNEL b +#pragma define SPECULAR_INTENSITY_CHANNEL a +``` + +Surface Shader uses a texture as a PBR map, and according to the definition, we can know the meaning of each channel. +- r: Ambient Occlusion +- g: Roughness +- b: Metallic +- a: Specular Intensity + +#### Implementation + +Like the surface-vertex, the surface-fragment also uses the function replacement mechanism to implement PBR parameter filling. + +For more details, please refer to the following pages: +- [Function Replacement Using Macros](function-replace.md) +- [Surface Shader Built-in Replaceable Functions](surface-function.md) +- [Surface Shader Execution Flow](./shader-code-flow.md) + +## Shader Assembly + +The several CCPrograms mentioned above are listed as follows: +- shared-ubos +- macro-remapping +- surface-vertex +- surface-fragment + +These are just some necessary components to implement the Surface Shader. To implement a complete Surface Shader, these parts need to be assembled in combination with other modules of the Surface Shader. + +For the specific assembly mechanism, please refer to [Surface Shader Assembly](./shader-assembly.md)。 + +The assembled CCProgram is the content referenced by the CCEffect +- CCProgram standard-vs +- CCProgram shadow-caster-vs +- CCProgram standard-fs +- CCProgram shadow-caster-fs +- CCProgram reflect-map-fs diff --git a/versions/4.0/en/shader/surface-shader/customize-surface-shader.md b/versions/4.0/en/shader/surface-shader/customize-surface-shader.md new file mode 100644 index 0000000000..ede0282165 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/customize-surface-shader.md @@ -0,0 +1,82 @@ +# Customize Surface Shader + +Indeed, while the Surface Shader provides a lighting model that can adapt to most scene materials, its functionality is relatively fixed. + +Sometimes, users need to use completely customized lighting calculations and color calculations. For example, some special, stylized materials require outline lights, additional fill lights, unrealistic environmental lighting, etc. + +For such extremely special situations, the Surface Shader also provides customization capabilities. + +However, it should be noted that because the surface material data and lighting calculation process are intervened, the rendering effect may produce unexpected. + +## 1. Customize VS Output and FS Input + +We can define a new varying variable in the VS stage and then calculate and output the value of this variable in a certain Surface function. + +After defining a variable with the same name in the FS stage, we can get and use the value of this variable in a certain Surface function. + +For details, please refer to [Fragment Shader Input](./fs-input.md). + +## 2. Custom Material Data + +Add the following code in the VS function. + +```glsl +//PBR lighting mode +#include +// toon lighting mode +//#include +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + // set user-defined data to surfaceData +} +``` + +The `#include` at the beginning of the function is used to determine the data structure of the surface material. Different `SurfacesMaterialData` structures will be used according to different included files. + +For specific content, you can check the **standard.chunk** and **toon.chunk** under the **internal/chunks/surfaces/data-structures/** directory. + +After defining the `CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA` macro, the Shader compiler will choose your own `SurfacesFragmentModifySharedData` to replace the default function. + +This function will be called during the vs stage, specifically, you can check the **render-to-scene/vs.chunk** and **render-to-shadowmap/vs.chunk** files under the **internal/chunks/shading-entries/main-functions/** directory. + +In this function, we can directly modify the properties in surfaceData to prepare for the lighting stage. + +## Customize Lighting Results + +With the custom SurfacesMaterialData from above, we also need to work with the lighting stage to achieve the calculation results we want. + +In the FS, add the following code. + +```glsl +#include +#define CC_SURFACES_LIGHTING_MODIFY_FINAL_RESULT +void SurfacesLightingModifyFinalResult(inout LightingResult result, in LightingIntermediateData lightingData, in SurfacesMaterialData surfaceData, in LightingMiscData miscData) +{ + // use surfaceData and lightingData for customizing lighting result +} +``` + +This function will be called in fs.chunk. + +As we can see, the function has 4 parameters. +- LightingIntermediateData: Data needed for lighting calculation, such as normal, view direction, view distance, etc. +- SurfacesMaterialData: PBR-related parameters, such as color, normal, etc. +- LightingMiscData: Light source data, such as position, direction, color, intensity, etc. +- LightingResult: Used to return lighting results, such as diffuse, specular, shadow, ao, etc. + +In this function, you can use lighting and material data to perform the lighting calculations and store them in the result. + +For local lights(point light or spotlight), this function will be executed per light. That means if an object is affected by 6 lights, this function will be called 6 times. + +If you want to directly call the built-in lighting module functions within the overloaded function., you can change lighting-models/includes/common to the header file used by the corresponding lighting model. + +For example, if you want to use the built-in lighting functions of the PBR lighting model in the function, you can include the lighting-models/includes/standard header file. + +This header file will include lighting-models/model-functions/standard. + +All built-in functions related to PBR lighting are here, just call them directly. + +## More Customizations + +If the above customization mechanism still cannot meet your needs, it is recommended to refer to chunks/shading-entries to build your own main function and control the entire shading process. diff --git a/versions/4.0/en/shader/surface-shader/fs-input.md b/versions/4.0/en/shader/surface-shader/fs-input.md new file mode 100644 index 0000000000..5e156fccb4 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/fs-input.md @@ -0,0 +1,65 @@ +# Fragment Shader Input + +## Built-in Input Variables + +Many parameters have been passed from Vertex Shader to Fragment Shader, listed below. + +| Fragment Shader Input | Type | Needed Macros | Meaning | +| --------------------- | ----- | ------------------------------ | ------------------ | +| FSInput_worldPos | vec3 | N/A | World Position | +| FSInput_worldNormal | vec3 | N/A | World Normal | +| FSInput_faceSideSign | float | N/A | Two Side Sign be used for double-sided materials | +| FSInput_texcoord | vec2 | N/A | UV0 | +| FSInput_texcoord1 | vec2 | N/A | UV1 | +| FSInput_vertexColor | vec4 | N/A | Vertex Color | +| FSInput_worldTangent | vec3 | N/A | World Tangent | +| FSInput_mirrorNormal | float | N/A | Mirror Normal Sign| +| FSInput_localPos | vec4 | CC_SURFACES_TRANSFER_LOCAL_POS | Local Position | +| FSInput_clipPos | vec4 | CC_SURFACES_TRANSFER_CLIP_POS | Clip Position | + +## Macro Switch + +When you need to use input parameters with macro switches, you need to enable the corresponding macros in the `macro-remapping` code section. Here's an example. + +```glsl +CCProgram macro-remapping %{ + ... + //Enable FSInput_localPos + #define CC_SURFACES_TRANSFER_LOCAL_POS 1 + //Enable FSInput_clipPos + #define CC_SURFACES_TRANSFER_CLIP_POS 1 + ... +} +``` + +## How to use + +Directly call them in your shader code. + +## Customize Varying Variables + +When creating some special effects, the Vertex Shader must pass more information to the Fragment Shader. At this time, we need to add new varying variables. + +Adding a new custom varying variable is quite simple. We will use the example of adding a new variable called `testVec3`. + +First, declare a variable with an `out` tag in the Vertex Shader, as shown in the following example. + +```glsl +CCProgram surface-vertex %{ + ... + out vec3 testVec3; + ... +} +``` + +Then declare a corresponding variable with an `in` tag in the Fragment Shader, as shown below. + +```glsl +CCProgram surface-fragment %{ + ... + in vec3 testVec3; + ... +} +``` + +After that, you can use `testVec3` in the code of the Fragment Shader. diff --git a/versions/4.0/en/shader/surface-shader/function-replace.md b/versions/4.0/en/shader/surface-shader/function-replace.md new file mode 100644 index 0000000000..7fbe8dd701 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/function-replace.md @@ -0,0 +1,67 @@ +# Function Replacement Using Macros + +Sometimes we need to create some common functions for different requirements to reduce code volume and maintenance costs. + +However, for some functions in the process, we hope to replace them with specific versions on certain occasions to meet specific requirements. + +This can be done using the macro definition mechanism. + +```glsl +//example.chunk +#ifndef CC_USER_MODIFY_SOMETHING +void ModifySomething(){ + //do something here +} +#endif + +void Before(){ + //do something here +} + +void After(){ + //do something here +} + +void myFunc(){ + Before(); + ModifySomething(); + After(); +} +``` + +In the code above, `myFunc` has a complete calling process. If you want to modify the implementation of `ModifySomething`, you only need to define the `CC_USER_MODIFY_SOMETHING` macro before `#include `, and implement your `ModifySomething` function. + +```glsl +#define CC_USER_MODIFY_SOMETHING +void ModifySomething(){ + //do what you want +} + +#include +``` + +> **Note**: the definition of the replaced function should be placed before the `include`. + +This mechanism is widely used in the Surface Shader system, such as the `SurfacesFragmentModifySharedData` function in `lighting-models/includes/common` mentioned earlier. + +```glsl +// user-defined-common-surface.chunk: +// base surface function +#ifndef CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + ................. +} +#endif + +// effect +// this function needs overriding +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + ............. +} +// base functions should place after override functions +#include +``` diff --git a/versions/4.0/en/shader/surface-shader/includes.md b/versions/4.0/en/shader/surface-shader/includes.md new file mode 100644 index 0000000000..9f195bac2b --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/includes.md @@ -0,0 +1,68 @@ +# include + +## Header Files + +In the Surface Shader, there are several [Lighting Models](./lighting-mode.md), such as standard, toon, etc. + +At the same time, we have different [Render Usages](./render-usage.md), such as render-to-scene, render-to-shadowmap, etc. + +Different lighting models have different surface material data structures and lighting calculation functions, and there are subtle differences in the process between different render usages. + +If we write code for different lighting models and different render usages independently, it will make the entire framework difficult to maintain. + +Therefore, in the Surface Shader, we define the methods and structures for different lighting models with the same name in different header files, and then combine them using include. + +In this way, it is easy to implement different lighting models under the same process by switching header files. + +For example, in the following code, we import the definition of `SurfacesMaterialData` through **#include **. The structure used in the function is the PBR surface material data structure. + +```glsl +//PBR surface material +#include +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + // set user-defined data to surfaceData +} +``` + +If we change its header file. + +```glsl +//toon surface material +#include +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + // set user-defined data to surfaceData +} +``` + +At this time, the **SurfacesMaterialData** uses the structure defined in `surfaces/data-structures/toon`. + +## CCProgram + +In the Surface Shader, the content of the `CCProgram` is purely GLSL code content. We can split the Shader into different CCProgram sections to form code snippets, and then reference them using `include`. + +Take the following code as an example. + +```ts +CCProgram shared-ubos %{ + ... +}% +CCProgram macro-remampping %{ + ... +}% + +CCProgram vs %{ + #include + #include +}% + +CCProgram fs %{ + #include + #include +}% +``` + +> **Note**: When using `include` to reference code defined in `CCProgram`, it is limited to the same file only. diff --git a/versions/4.0/en/shader/surface-shader/lighting-mode.md b/versions/4.0/en/shader/surface-shader/lighting-mode.md new file mode 100644 index 0000000000..c44b40ce50 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/lighting-mode.md @@ -0,0 +1,12 @@ +# Lighting Models + +The lighting models are used to describe how the surface of an object affects and interacts with light. + +The currently supported lighting models are as follows. + +| Light Model Name | Description | +| ------------ | ------------------------------------------------------------ | +| standard | PBR lighting, supporting GGX BRDF distribution of isotropic and anisotropic lighting, supporting convoluted ambient lighting. | +| toon | Simple cartoon lighting, step-like lighting effect. | + +The built-in shader functions related to the lighting models are placed in the internal/chunks/lighting-models/includes/ folder, During the [Surface Shader Assembly](./shader-assembly.md) phase, using `include` to import the corresponding code, the lighting calculation can be completed. diff --git a/versions/4.0/en/shader/surface-shader/macro-remapping.md b/versions/4.0/en/shader/surface-shader/macro-remapping.md new file mode 100644 index 0000000000..0c06a9eb85 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/macro-remapping.md @@ -0,0 +1,87 @@ +# Macro Remapping + +During internal calculations, Surface Shader will use some macro switches, which start with `CC_SURFACES_`. + +> **Note**: Macros start with the `CC_SURFACES_` won't appear on the material inspector panel. + +The complete macro list is as follows. + +| Macro Nam | Type | Meaning | +| :---------------------------------------------------- | ---- | ------------------------------------------------------------ | +| CC_SURFACES_USE_VERTEX_COLOR | BOOL | Use vertex color | +| CC_SURFACES_USE_SECOND_UV | BOOL | Use second uv | +| CC_SURFACES_USE_TWO_SIDED | BOOL | Use two-side normal for two-side lighting | +| CC_SURFACES_USE_TANGENT_SPACE | BOOL | Use tangent space - must be enabled when using normal map or anisotropy ) | +| CC_SURFACES_TRANSFER_LOCAL_POS | BOOL | Access model space position in FS | +| CC_SURFACES_LIGHTING_ANISOTROPIC | BOOL | Enable anisotropic material | +| CC_SURFACES_LIGHTING_ANISOTROPIC_ENVCONVOLUTION_COUNT | UINT | Sample count of anisotropic convolution, 0 means convolution is disabled, only valid when anisotropy is enabled | +| CC_SURFACES_LIGHTING_USE_FRESNEL | BOOL | Calculate Fresnel coefficient through relative refractive index ior | +| CC_SURFACES_LIGHTING_TRANSMIT_DIFFUSE | BOOL | Enable backside penetration diffuse (used for hair, leaves, ears, etc) | +| CC_SURFACES_LIGHTING_TRANSMIT_SPECULAR | BOOL | Enable light specular (used for water surface, glass, etc.) | +| CC_SURFACES_LIGHTING_TRT | BOOL | Enable light reflected from the interior after transmission(used for hair) | +| CC_SURFACES_LIGHTING_TT | BOOL | Enable light diffused from the interior after transmission(used for hair) | +| CC_SURFACES_USE_REFLECTION_DENOISE | BOOL | Enable environmental reflection denoising, only valid under legacy compatibility mode | +| CC_SURFACES_USE_LEGACY_COMPATIBLE_LIGHTING | BOOL | Enable legacy compatible lighting mode, the rendering effect can be completely consistent with legacy/standard.effect, convenient for upgrade | + +> **Note**: If these macros are not defined, the system will automatically define them as the default value 0. + +Search for the `CCProgram macro-remapping` section, and you can see that the content consists of three parts. + +![macro-remapping](../img/macro-remapping.png) + +## Display Macros on Panel + +```glsl +// ui displayed macros not used in this effect file +#pragma define-meta HAS_SECOND_UV +#pragma define-meta USE_TWOSIDE +#pragma define-meta USE_REFLECTION_DENOISE +#pragma define-meta USE_COMPATIBLE_LIGHTING +``` + +By default, macros starting with `CC_` will not be displayed on the material panel. When we want a macro to be displayed on the material panel, we can do the following. + +1. Use `#pragma define-meta` to define a panel macro, we take `HAS_SECOND_UV` as an example. + +```glsl +#pragma define-meta HAS_SECOND_UV +``` + +2. Remap the macro starting with `CC_SURFACES_` to this macro, for example. + +```glsl +#define CC_SURFACES_USE_SECOND_UV HAS_SECOND_UV +``` + +In this way, on the material panel corresponding to this shader, you can control the value of the `CC_SURFACES_USE_SECOND_UV` macro by controlling `HAS_SECOND_UV`. + +## Macros Used in Surface Functions + +If a macro is used in the shader code and does not start with `CC_`, it will automatically be displayed on the material panel. For example. + +```glsl +// ui displayed macros used in this effect file +#define CC_SURFACES_USE_VERTEX_COLOR USE_VERTEX_COLOR +#if IS_ANISOTROPY || USE_NORMAL_MAP + #define CC_SURFACES_USE_TANGENT_SPACE 1 +#endif +``` + +In this case, `IS_ANISOTROPY` and `USE_NORMAL_MAP` will be displayed on the material panel, and can be switched on and off through the material panel. + +## Internal Functionality Macros + +For some macros, we don't want to control it on the panel, just define its value directly, for example. + +```glsl +// functionality for each effect +#define CC_SURFACES_LIGHTING_ANISOTROPIC_ENVCONVOLUTION_COUNT 31 +``` + +## Hidden Macros + +If you have written some macros in your shader but don't want them to appear on the material panel, you can start them with `CC_`. + +To differentiate from the internal system macros, user-defined internal macros are recommended to start with `CC_USER_`. + +For more details about macros, please see [Macros](../macros.md)。 diff --git a/versions/4.0/en/shader/surface-shader/render-usage.md b/versions/4.0/en/shader/surface-shader/render-usage.md new file mode 100644 index 0000000000..8b1d5a4b92 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/render-usage.md @@ -0,0 +1,53 @@ +# Render Usage + +By default, Surface Shaders mainly output to the screen and display scenes. + +However, sometimes we have some special needs, such as: +- Render to shadow maps +- Render to reflect maps + +Different render usages have different processes and details, so they need special handling. + +The Surface Shader framework has predefined the commonly used flows for different usages. They can be found in the **Assets/internal/chunks/shading-entries/main-functions/** folder. + +Here are built-in render usages. + +| Common Render Usages | Location | Description | +| -------------------- | -------------------- | --- | +| Render to Scene(default) | render-to-scene || +| Render to ShadowMap | render-to-shadowmap || +| Render to ReflectionMap | render-to-reflectmap | Optional | +| Render Cartoon Outlines | misc/silhouette-edge || +| Render Skybox | misc/sky || +| Post-processing or General Compute Pass | misc/quad | Reserved | + +You only need to include the corresponding header files during the [Surface Shader Assembly](./shader-assembly.md) phase to complete the rendering process. + +For example, in `internal/effects/builtin-standard.effect`, we can see the application cases as follows. + +```glsl +CCProgram standard-vs %{ + ... + #include +}% + +CCProgram shadow-caster-vs %{ + ... + #include +}% + +CCProgram standard-fs %{ + ... + #include +}% + +CCProgram shadow-caster-fs %{ + ... + #include +}% + +CCProgram reflect-map-fs %{ + ... + #include +}% +``` diff --git a/versions/4.0/en/shader/surface-shader/rendering-debug-view.md b/versions/4.0/en/shader/surface-shader/rendering-debug-view.md new file mode 100644 index 0000000000..de6452e83c --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/rendering-debug-view.md @@ -0,0 +1,127 @@ +# Rendering Debug View + +> **Note**: The built-in rendering debugging feature is only available for materials using the Surface Shader framework**. + +By selecting the corresponding debug mode in the upper-right corner of the scene preview window in the editor, you can view the model, materials, lighting and other computational data on the same screen, and quickly locate the problem when the rendering effect is abnormal. + +![debug-view](../img/debug-view.jpg) + +To facilitate pixel-by-pixel comparison, we use full-screen debugging instead of picture-in-picture display, which allows us to quickly switch between different data in the same screen to locate rendering errors, and to use the color picker to explore the pixel-specific values. + +In addition, Surface Shader has a built-in **Irrational Number Visualization** feature, so once some pixels appear to have abnormal Red(255, 0, 51) and Green(0, 255, 51) alternately blink, then the rendering calculation of these pixels appears irrational, please use the single debug mode to check the model tangent or other related data. + +The rendering debugging functions are subdivided into the following three categories.
+ +## 1. Common Options + +Debugging options that are in effect in either single or combined mode, including: + +| Name | Function | Debugging Tips | +| :--- | :--- | :--- | +| Lighting with Base Color | Check to show normal material illumination, check off to show the effect of white mold pure illumination. | You can see the effect of AO, GI and other indirect light related effects more obviously. | +| CSM Layer Coloration | Cascading shadows are stained layer by layer from near to far, distributed as reddish, greenish, bluish, and yellowish, with no staining in areas beyond the shadows. | Allows you to view and confirm the fineness of the scene shadows.
If three or four layers accounted for too little shading is too fine, should increase the shading visible distance.
If the ratio of one or two layers is too small, it means that the shadows are too coarse and the visible distance of shadows should be reduced. | + +## 2. Single mode + +The debugging focus is on a particular piece of data that needs to be tested, and the entire scenario visualizes this data output. + +The debuggable data includes four major categories. + +### I. Primitive Model Info + +| Name | Function | Description and debugging tips | Dependencies | +| :--- | :--- | :--- | :--- | +| Vertex Colors | Vertex Colors| | Must check **USE VERTEX COLOR** in material | +| World Normal | World Normal| | | +| World Tangent| World Tangent | If there are irrational numbers or unusual lighting effects in the calculation you can focus on checking here, if it is black then the model tangent is not turned on but normal mapping or anisotropy is turned on. | Exclusion cannot be selected in the model's tangent settings
Must have **USE NORMAL MAP** or **IS ANISOTROPY** checked in the material | +| World Position | Visualize vertex coordinates (no scaling) | RGB channels display the XYZ axis coordinate data.
You can judge the difference of world space axis by color, and the model size by light and dark.| | +| Mirrored Normal | Show normal map flip reuse markers | To improve normal map utilization, some symmetric models (e.g., faces) will only bake half of the normals and reuse the data in the normal map using mirror markers for the other half.
If the value of this marker does not change at all on the model, it may cause the normals of the other half of the model to show unusual effects such as bump flip. | Must check **USE NORMAL MAP** in the material | +| Front Face Coloration | Display front or back faces | With the default single-sided model, the normals are not automatically distinguished and the light and dark seen on the front and back sides of the model are the same.
When a single-sided model uses a double-sided material, it automatically distinguishes between the front and back normals, and the light and dark seen on the front and back sides of the model are different.
The mark is white for the front side of the model and black for the reverse side. It can be used to check the light and darkness of the single-sided model. | | +| UV0 | Display the First UV | | | +| UV1 | Display the second UV | | Must be checked in the material **HAS SECOND UV** | +| Lighting MAP UV | Display the lighting mAP UV | If the light mapping effect is not correct, you can turn on this option and compare the general area of the light mapping to check the error. | Must bake scenes | +| Projection Depth Z | Display (0-1 non-linear variation) depth | Too far away of the far clip plane will result in a large depth value in the near field. | | +| Linear Depth W | Display (0-1 linear variation) depth | Too far away of the far clip plane will result in a large depth value in the near field | | + +### II. Primary Material Info + +| Name | Function | Description and debugging tips | Dependencies | +| :--- | :--- | :--- | :--- | +| World Space Pixel Normals | | You can compare with the corresponding vertex data to see if the effect of normal mapping is correct | | +| World Space Pixel Tangent/Binormals | | Can compare with corresponding vertex data to see if normal mapping affects it correctly | Cannot be selected for exclusion in the model's tangent settings
Must have **USE NORMAL MAP** or **IS ANISOTROPY** checked in the material | +| Base Color | | | | +| Diffuse Color | Base colors affecting diffuse illumination calculated from natural colors and other material data | | | +| Specular Color | The base color affecting specular reflective lighting calculated from intrinsic color and other material data | | | +| Opacity | Smaller means more transparent | | Must have **Alpha Blend** enabled in the material | +| Metallic | | | | +| Roughness | | | | +| Specular Intensity | Display the multiplication of non-metallic reference mirror reflectance F0 | If the specular is black, please check if this field is set to 0. | | + +### III. Lighting Info + +| Name | Function | Description and debugging tips | Dependencies | +| :--- | :--- | :--- | :--- | +| Direct Diffuse | | | | +| Direct Specular | | | | +| Direct Lighting | Direct Diffuse + Direct Specular | | | +| Ambient Diffuse | | | | +| Ambient Specular | | | | +| Ambient Lighting | Ambient Diffuse + Ambient Specular | | | +| Emissive | Display the emissive in the material | | | +| Light Map | Display baked lightmap RGB colors | | Must bake scenes | +| Shadows | Show shadows of directional light, spot light, point light | | Shadows must be enabled in the scene's **Inspector** panel and light sources, and objects enabled to receive shadows | +| Ambient Occlusion | Show the color of AO map in the material and real-time AO | | | + +### IV. Misc Info + +| Name | Function | Description and debugging tips | Dependencies | +| :--- | :--- | :--- | :--- | +| Fog | Display the fog effect factor, the larger it is, the denser the fog is | | Fog effect must be enabled in the scene's **Inspector** panel | + +## 3. Render Composite Options + +The debugging focus is on the overall rendering performance, and each module can be blocked or turned on, with modules unrelated to each other, to see how different modules affect each other's rendering effects. + +Single mode has higher priority than the composite mode, If you want to use the composite mode, you must check No single mode. + +Including: + +| Name | Function | Description and debugging tips | Category | +| -------------- | ----------------------- | ------------------------------------------------------------ | -------- | +| Direct Diffuse | Enable/Disable Direct Light Diffuse | Affect Directional, Spot, and Point Light | Lighting | +| Direct Specular | Enable/Disable Direct Light Specular Reflection | Affect Directional, Spot, and Point Light | Lighting | +| Ambient Diffuse | Enable/Disable Ambient Light Diffuse | Influence Skylight | Lighting | +| Ambient Specular | Enable/Disable Ambient Light Specular Reflection | Influence Skylight | Lighting | +| Emissive | Enable/Disable Emissive | If some objects are too bright or exposed, try to turn off this option is not set in the material unnecessary self-illumination | Lighting | +| Light Map | Enable/Disable Baking Light | Affect Baking | Light | +| Shadows | Enable/Disable Direct Light Shadows | Affect Live Directional Light / Spot Light / Spot Light Shadows and Baked Directional Light Shadows | Lighting | +| Ambient Occlusion | Enable/Disable Ambient Occlusion | Influence Skylight | Light | +| Normal Map | Enable/Disable Normal Map | If the lighting is unusually scattered, try turning this option off to see if the normal map is scrambling at the wrong intensity.
If the lighting effect is wrong or appears irrational, try turning this option off to see if the model is not tangential | Materials | +| Fog | Enable/Disable fog effect | If the scene color is abnormally gray, try turning off this option to see if the fog parameter is not set properly | Environment | +| Tone Mapping | Enable/Disable Tone Mapping | If the scene color is too different from the original material, try to turn off this option to see if it is normal, indicating that UseHDR should not be checked in the scene panel | Color Space | +| Gamma Correction | Enable/Disable Gamma Correction | If the scene is unusually colorful and dark, try turning off this option to see if it is normal, indicating that the mapping resources may have been gamma corrected multiple times | Color Space | + +## 4. Advanced Usage + +1. Manual inserting vertex shader outputs and fragment shader inputs: with the new varying property introduced in the Surface Shader framework, vertex shader outputs and fragment shader inputs can be accessed in particular Surface Functions. +2. Surface Shader and legacy shader codes can be used in the same render loop as long as the varying vertex data are kept consistent between the two. + +## Public function libraries + +The library headers can be found under **assets -> internal -> chunks -> common** folder in different categories. + +The functions in the library do not depend on any internal data (engine related uniform, mapping, etc.) and can be used directly as tool functions. + +Surface already automatically contains common public function headers internally, which can be classified according to type as: + +| Folder Name | Function Usage | +| :-------- | :---------------------------------------- | +| color | color-related functions (color space, tonemapping, etc.) | +| data | Data-related functions (compression and decompression, etc.) | +| debug | Debug View-related functions | +| effect | Scene effect-related functions (water, fog, etc.) | +| lighting | lighting-related features (brdf, bsdf, attenuation, baking, etc.) | +| math | math library (coordinate transformation, numerical determination and operation, etc.) +| mesh | model-related functions (material conversion, model animation, etc.) +| shadow | shadow-related functions (pcf, hcs, etc.) +| texture | mapping-related functions (sampling, mip calculation, etc.) | diff --git a/versions/4.0/en/shader/surface-shader/shader-assembly.md b/versions/4.0/en/shader/surface-shader/shader-assembly.md new file mode 100644 index 0000000000..4e41054ba9 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/shader-assembly.md @@ -0,0 +1,165 @@ +# Surface Shader Assembly + +## Shader Fragment Assembler + +In the built-in Surface Shader files, you will see the following code snippets. + +```glsl +CCProgram standard-vs %{ + //includes +}% + +CCProgram shadow-caster-vs %{ + //includes +}% + +CCProgram standard-fs %{ + //includes +}% + +CCProgram shadow-caster-fs %{ + //includes +}% + +CCProgram reflect-map-fs %{ + //includes +}% +``` + +These CCProgram code snippets named xxx-vs and xxx-fs are the assemblers. + +In these code snippets, we use the `#include` keyword to import different header files as needed and assemble each shader in order. + +## Two Ways to include + +In these includes you can see the following two situations. + +```glsl +//include from CCProgram +#include + +//include from file +#include +``` + +We can import an external chunk file or import a predefined CCProgram by name in the file, such as `macro-remapping`. + +## Main Parts + +Using `standard-fs` as an example, we can see that the entire fragment shader assembly is divided into the following 6 parts. + +## 1. Macros + +First, you need to include the necessary internal macro definitions and mappings. + +Macro mapping uses a custom CCProgram section or chunk file described in the Macro-Remapping section. + +Next, you need to include the common macro definition file `common-macros`, as shown below. + +```glsl +#include +#include +``` + +For some special render usages, it is recommended to directly include the macro definition file corresponding to the render usage. Take render-to-shadowmap as an example. + +```glsl +CCProgram shadow-caster-fs %{ + ... + #include + ... +}% +``` + +## 2. Common Header Files + +Select the corresponding common header file based on the current Shader Stage, as shown below. + +```glsl +//Vertex Shader +CCProgram standard-vs %{ + ... + #include + ... +}% + +//Fragment Shader +CCProgram standard-fs %{ + ... + #include + ... +}% +``` + +## 3. Surface Shader Body + +This part is the main part of the Surface Shader. + +For example, external constant uniforms, shared-ubos code blocks. + +And the main functions in the Surface Shader that users can control, such as the surface-vertex and surface-fragment code segments in the built-in shader. + +As shown below. + +```glsl +CCProgram standard-fs %{ + ... + #include + #include + ... +}% +``` + +## 4. Lighting Model + +This part is optional, and it is not necessary for Vertex Shader when rendering to ShadowMap. + +The function of this part is to use the light model name to select the corresponding light model header file, as shown below. + +```glsl +//Standard PBR Lighting +#include + +//Toon Lighting +#include +``` + +## 5. Surface Material Data Structure + +This part is optional and is not necessary when rendering to ShadowMap. + +Select the surface material data structure corresponding to the lighting model, as shown below. + +```glsl +//Vertex Shader +//Standard +#include +//Toon +#include + +//Fragment Shader +//Standard +#include +//Toon +#include +``` + +## 6. Main Function + +Use the **Render Usage + Shader Stage** to select the corresponding main function header file, as shown below. + +```glsl +//standard-vs: +#include + +//shadow-caster-vs: +#include + +//standard-fs: +#include + +//shadow-caster-fs: +#include +``` + +For more details, please refer to [Guide to Built-in Surface Shader](./builtin-surface-shader.md)。 diff --git a/versions/4.0/en/shader/surface-shader/shader-code-flow.md b/versions/4.0/en/shader/surface-shader/shader-code-flow.md new file mode 100644 index 0000000000..966507fd1c --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/shader-code-flow.md @@ -0,0 +1,200 @@ +# Surface Shader Execution Flow + +Surface Shader unifies the shading process and provides a wide range of custom functions for both vertex and fragment shader. Shader writers can override these functions according to their needs. + +Please refer to [Surface Shader Built-in Replaceable Functions](./surface-function.md) and [Function Replacement Using Macros](./function-replace.md)。 + +This main purpose of this article is to help developers familiarize themselves with the execution flow of Surface Shader and understand the timing of function calls. + +## Entry Function + +Let's first take a look at the CCEffect section in the built-in Surface Shader file. + +```glsl +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: standard-vs + frag: standard-fs + ... +}% +``` + +As we can see, each pass doesn't specify specific entry functions for the vertex shader and fragment shader, which means that the default entry function `main` will be used. + +From the [Surface Shader Structure](./surface-shader-structure.md), we can learn that during the [Surface Shader Assembly](./shader-assembly.md) phase, each Surface Shader includes different header files based on different [Render Usages](./render-usage.md). These header files serve as our entry functions. + +## Main Function for VS + +Take the `standard-vs` of the built-in Surface Shader as an example. + +```glsl +CCProgram standard-vs %{ + #include +}% +``` + +As we can see, it includes the vs.chunk under `render-to-scene` folder. + +By opening render-to-scene/vs.chunk, we can see that it only contains one main function, here is the code and comments. + +```glsl +void main() +{ + SurfacesStandardVertexIntermediate In; + CCSurfacesVertexInput(In); + CCSurfacesVertexAnimation(In); + In.position.xyz = SurfacesVertexModifyLocalPos(In); + In.normal.xyz = SurfacesVertexModifyLocalNormal(In); + + #if CC_SURFACES_USE_TANGENT_SPACE + In.tangent = SurfacesVertexModifyLocalTangent(In); + #endif + + SurfacesVertexModifyLocalSharedData(In); + CCSurfacesVertexWorldTransform(In); + In.worldPos = SurfacesVertexModifyWorldPos(In); + In.clipPos = cc_matProj * cc_matView * vec4(In.worldPos, 1.0); + + In.clipPos = SurfacesVertexModifyClipPos(In); + vec3 viewDirect = normalize(cc_cameraPos.xyz - In.worldPos); + In.worldNormal.w = dot(In.worldNormal.xyz, viewDirect) < 0.0 ? -1.0 : 1.0; + + In.worldNormal.xyz = SurfacesVertexModifyWorldNormal(In); + SurfacesVertexModifyUV(In); + SurfacesVertexModifySharedData(In); + CCSurfacesVertexTransformUV(In); + CCSurfacesVertexTransferFog(In); + CCSurfacesVertexTransferShadow(In); + CCSurfacesVertexTransferLightMapUV(In); + + CCSurfacesVertexOutput(In); +} +``` + +## Main Function for FS + +Similarly, let's take a look at the main function of the fragment shader. In the case of the `standard-fs` of built-in Surface Shader. + +```glsl +CCProgram standard-fs %{ + #include +}% +``` + +the `render-to-scene/fs.chunk` contains the following content: + +```glsl +#if (CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_FORWARD || CC_FORCE_FORWARD_SHADING) + #include +#elif CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_DEFERRED + #include +#endif +``` + +As we can see, it distinguishes between the forward and deferred pipelines. + +### forward-fs + +```glsl +//Define the color output target +layout(location = 0) out vec4 fragColorX; + +void main() { + #if CC_DISABLE_STRUCTURE_IN_FRAGMENT_SHADER + //Get the base color and transparency, can be replaced by macros + vec4 color = SurfacesFragmentModifyBaseColorAndTransparency(); + #else + //Get surface material data + SurfacesMaterialData surfaceData; + CCSurfacesFragmentGetMaterialData(surfaceData); + + //Compute shadow parameters + vec2 shadowBias = vec2(0.0); + ... + + + //Compute fog parameters + #if !CC_FORWARD_ADD + float fogFactor = 1.0; + #endif + + //Compute lighting + LightingResult lightingResult; + CCSurfacesLighting(lightingResult, surfaceData, shadowBias); + + + //Rendering debugging related code + ... + + //Pixel shading calculation + vec4 color = CCSurfacesShading(surfaceData, lightingResult); + + ... + + //Color output + #if CC_USE_RGBE_OUTPUT + fragColorX = packRGBE(color.rgb); // for reflection-map + return; + #endif + + //HDR,LinearToSRGB, and other final computations + #if CC_USE_HDR + #if CC_USE_DEBUG_VIEW == CC_SURFACES_DEBUG_VIEW_COMPOSITE_AND_MISC && CC_SURFACES_ENABLE_DEBUG_VIEW + if (IS_DEBUG_VIEW_COMPOSITE_ENABLE_TONE_MAPPING) + #endif + color.rgb = ACESToneMap(color.rgb); + #endif + #if CC_USE_DEBUG_VIEW == CC_SURFACES_DEBUG_VIEW_COMPOSITE_AND_MISC + if (IS_DEBUG_VIEW_COMPOSITE_ENABLE_GAMMA_CORRECTION) + #endif + color.rgb = LinearToSRGB(color.rgb); + + #if !CC_FORWARD_ADD && CC_USE_FOG != CC_FOG_NONE + CC_APPLY_FOG_BASE(color, fogFactor); + #endif + + fragColorX = CCSurfacesDebugDisplayInvalidNumber(color); +} +``` + +### deferred-fs + +The deferred rendering is divided into two stages: GBuffer and Lighting. + +In the GBuffer stage, the main task is to fill the various render targets by collecting the corresponding surface material data. + +```glsl +//GBuffer 0,1,2 +layout(location = 0) out vec4 fragColor0; +layout(location = 1) out vec4 fragColor1; +layout(location = 2) out vec4 fragColor2; + +void main () { + //Collect surface material data + SurfacesMaterialData surfaceData; + CCSurfacesFragmentGetMaterialData(surfaceData); + + //Fill the GBuffer + fragColor0 = CCSurfacesDeferredOutput0(surfaceData); + fragColor1 = CCSurfacesDeferredOutput1(surfaceData); + fragColor2 = CCSurfacesDeferredOutput2(surfaceData); + + //Debug rendering related code + #if CC_USE_DEBUG_VIEW == CC_SURFACES_DEBUG_VIEW_SINGLE && CC_SURFACES_ENABLE_DEBUG_VIEW + vec4 debugColor = vec4(0.0, 0.0, 0.0, 1.0); + CCSurfacesDebugViewMeshData(debugColor); + CCSurfacesDebugViewSurfaceData(debugColor, surfaceData); + if (IS_DEBUG_VIEW_ENABLE_WITH_CAMERA) { + fragColor0 = debugColor; + } + #endif +} +``` + +In the deferred rendering Lighting stage, it is controlled by the engine's rendering pipeline and performs lighting calculations using the GBuffer. You can refer to `internal/effects/deferred-lighting.effect`. + +Similarly, the main functions for other render stage can be found in the `internal/chunks/shading-entries/` folder. + +> **Note**: The code that can be replaced is named using the `Surface###Modify###` format. diff --git a/versions/4.0/en/shader/surface-shader/shader-stage.md b/versions/4.0/en/shader/surface-shader/shader-stage.md new file mode 100644 index 0000000000..6d79925138 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/shader-stage.md @@ -0,0 +1,29 @@ +# Shader Stage + +Rendering is accomplished by different shaders. Some for processing vertices, some for processing pixels, and some for general computing. + +In the Surface Shader architecture, for better readability and maintainability, different shader stages have a conventional code identifier, as shown in the table below. + +| Shader Stage | Corresponding Code Identifier | +| --------------- | ---------------------------- | +| Vertex Shader | vs | +| Fragment Shader | fs | +| Computer Shader | cs | + +You can find many files named `xxxx-vs` or `xxxx-fs` in the built-in shader and chunk files. + +In the effect and chunk files, there are also many code snippets defined like `CCProgram xxx-vs %{}%` and `CCProgram xxx-fs %{}%`. + +For example. + +```ts +CCProgram standard-vs %{ + //... +}% + +CCProgram standard-fs %{ + //... +}% +``` + +When writing your shaders, it is best to follow this rule to maintain the readability and maintainability of the shader code. diff --git a/versions/4.0/en/shader/surface-shader/surface-data-struct.md b/versions/4.0/en/shader/surface-shader/surface-data-struct.md new file mode 100644 index 0000000000..b2b5a8edf7 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/surface-data-struct.md @@ -0,0 +1,34 @@ +# Surface Material Data Structure + +## What's SurfaceMaterialData + +In Surface Shader, according to the needs of different [Lighting Models](./lighting-mode.md), the corresponding surface material data structure `SurfaceMaterialData` is provided. + +The surface material data structure defines a series of physical parameters used to calculate the final color of the object's surface, such as albedo, roughness, etc. + +> **Note**: The material data model must be used in conjunction with the lighting model. + +## Type of Surface Material Data + +| Material Data Type | Description | +| ------------ | ------------------------------------------------------------ | +| standard | Standard PBR material described by roughness and metallic, similar to material nodes in software like SP, Blender, Maya. | +| toon | Simple cartoon material with various shading methods. | + +## How to use + +The corresponding surface material data structures are stored in the **internal/chunks/data-structures/** folder. + +You only need to include the corresponding surface material data structure header file to use different categories of data structures. + +```glsl +//PBR Surface Material +#include +// toon Surface Material +//#include +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + // set user-defined data to surfaceData +} +``` diff --git a/versions/4.0/en/shader/surface-shader/surface-function.md b/versions/4.0/en/shader/surface-shader/surface-function.md new file mode 100644 index 0000000000..805e0b1d92 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/surface-function.md @@ -0,0 +1,75 @@ +# Replaceable Built-in Functions in Surface Shader + +Surface Shader has unified the shading process and provides a loft custom functions to users. You can use the macro mechanism to rewrite related functions according to your needs. + +## 1. Principle + +The custom functions provided by Surface Shader have a default version internally and are called at the right time. You can refer to [Surface Shader Execution Flow](./shader-code-flow.md). + +These functions are usually named in the way of `Surfaces+Shader+ShaderTypeName+Modify+Attribute`, such as. +- SurfacesVertexModifyLocalPos +- SurfacesVertexModifyLocalNormal +- SurfacesVertexModifyLocalTangent + +All functions can be viewed at [`internal/chunks/surfaces/default-functions/`](https://github.com/cocos/cocos4/tree/v4.0.0/editor/assets/chunks/surfaces/default-functions). + +**If you want to replace the implementation of a function, you can do so by pre-defining the macro corresponding to that function.**. + +For example, you can pre-define the `CC_SURFACES_VERTEX_MODIFY_WORLD_POS` macro, so that Surface Shader can use your defined function to calculate the world position. The sample code is as follows. + +```glsl +#define CC_SURFACES_VERTEX_MODIFY_WORLD_POS +vec3 SurfacesVertexModifyWorldPos(in SurfacesStandardVertexIntermediate In) +{ + vec3 worldPos = In.worldPos; + worldPos.x += sin(cc_time.x * worldPos.z); + worldPos.y += cos(cc_time.x * worldPos.z); + return worldPos; +} +``` + +If you are not familiar with the function replacement mechanism in Surface Shader, you can first refer to [Function Replacement Using Macros](./function-replace.md). + +> The advantage of this method is that it can easily extend a variety of different material data structures, lighting models, and render usages, and does not need to modify the built-in Surface Shader code. + +## 2. Common Replaceable Functions for VS + +The replaceable functions for vs are defined in the internal/chunks/surfaces/default-functions/common-vs.chunk file. + +The built-in functions for vs all have the `SurfacesStandardVertexIntermediate` structure as parameter, which stores the data of vs inputs and outputs. Shader writers don't need to worry about the specific vertex input and output processing, they just need to focus on the modification of certain data. + +| Macro | Function | Material Type | Description | +| ------------------------------------------- | ---------------------------------------- | -------------- | ------------------------------------------------------------ | +| CC_SURFACES_VERTEX_MODIFY_LOCAL_POS | vec3 SurfacesVertexModifyLocalPos | Common | Used to modify local position | +| CC_SURFACES_VERTEX_MODIFY_LOCAL_NORMAL | vec3 SurfacesVertexModifyLocalNormal | Common | Used to modify local normal | +| CC_SURFACES_VERTEX_MODIFY_LOCAL_TANGENT | vec4 SurfacesVertexModifyLocalTangent | Common | Used to modify local tangent and mirror normal marker | +| CC_SURFACES_VERTEX_MODIFY_LOCAL_SHARED_DATA | void SurfacesVertexModifyLocalSharedData | Common | If some textures and calculations need to be used in multiple material nodes, they can be performed in this function, called before world transformation, directly modifying the three local parameters inside the `SurfaceStandardVertexIntermediate` structure. | +| CC_SURFACES_VERTEX_MODIFY_WORLD_POS | vec3 SurfacesVertexModifyWorldPos | Common | Used to modify world position. +| CC_SURFACES_VERTEX_MODIFY_CLIP_POS | vec4 SurfacesVertexModifyClipPos | Common | Used to modify position in clip space (projected position) | +| CC_SURFACES_VERTEX_MODIFY_UV | void SurfacesVertexModifyUV | Common | Used to modify uv coordinates | +| CC_SURFACES_VERTEX_MODIFY_WORLD_NORMAL | vec3 SurfacesVertexModifyWorldNormal | Common | Used to modify world normal | +| CC_SURFACES_VERTEX_MODIFY_ SHARED_DATA | void SurfacesVertexModify SharedData | Common | If some textures and calculations need to be used in multiple material nodes, they can be performed in this function, directly modifying the parameters inside the `SurfaceStandardVertexIntermediate` structure, reducing performance consumption | + +## 3. Common Replaceable Functions for FS + +The replaceable functions for FS are composed of PBR and Toon, which are located in the following two files. +- internal/chunks/surfaces/default-functions/standard-fs.chunk +- internal/chunks/surfaces/default-functions/toon-vs.chunk + +Most of the replaceable functions for FS don't have parameter. Shader writers need to process them in combination with [FS Inputs](./fs-input.md). For some special-purpose functions, corresponding parameters are also provided. For which situation they belong to, please refer to the function definition. + +| Macro | Function | Material Type | Description | +| ------------------------------------------------------- | ---------------------------------------------------- | -------------- | ------------------------------------------------------------ | +| CC_SURFACES_FRAGMENT_MODIFY_ BASECOLOR_AND_TRANSPARENCY | vec4 SurfacesFragmentModify BaseColorAndTransparency | Common | Used to modify the base color, including alpha channel | +| CC_SURFACES_FRAGMENT_ALPHA_CLIP_ONLY | vec4 SurfacesFragmentModify AlphaClipOnly | Common | Used to process alpha test | +| CC_SURFACES_FRAGMENT_MODIFY_ WORLD_NORMAL | vec3 SurfacesFragmentModify WorldNormal | Common | Used to modify world normal | +| CC_SURFACES_FRAGMENT_MODIFY_ SHARED_DATA | void SurfacesFragmentModify SharedData | Common | If some textures and calculations need to be used in multiple material nodes, they can be performed in this function, directly modifying the parameters inside the Surface structure, reducing performance consumption, similar to the surf() function in legacy shader. Necessary header files need to be included before defining the function | +| CC_SURFACES_FRAGMENT_MODIFY_ WORLD_TANGENT_AND_BINORMAL | void SurfacesFragmentModify WorldTangentAndBinormal | Standard PBR | Used modify world tangent | +| CC_SURFACES_FRAGMENT_MODIFY_ EMISSIVE | vec3 SurfacesFragmentModify Emissive | Standard PBR | Used to modify emissive | +| CC_SURFACES_FRAGMENT_MODIFY_ PBRPARAMS | vec4 SurfacesFragmentModify PBRParams | Standard PBR | Used to modify PBR parameters vec4(ao, roughness, metallic, specularIntensity) | +| CC_SURFACES_FRAGMENT_MODIFY_ ANISOTROPY_PARAMS | vec4 SurfacesFragmentModify AnisotropyParams | Standard PBR | Used to modify anisotropy-related parameters vec4(rotation, shape, unused, unused) | +| CC_SURFACES_FRAGMENT_MODIFY_ BASECOLOR_AND_TOONSHADE | void SurfacesFragmentModify BaseColorAndToonShade | Toon | Used to modify the base color and toon shade | +| CC_SURFACES_FRAGMENT_MODIFY_ TOON_STEP_AND_FEATHER | vec4 SurfacesFragmentModify ToonStepAndFeather | Toon | Used to modify step and feather | +| CC_SURFACES_FRAGMENT_MODIFY_ TOON_SHADOW_COVER | vec4 SurfacesFragmentModify ToonShadowCover | Toon | Used to modify toon shadow cover | +| CC_SURFACES_FRAGMENT_MODIFY_ TOON_SPECULAR | vec4 SurfacesFragmentModify ToonSpecular | Toon | Used to modify toon specular | +| CC_SURFACES_LIGHTING_MODIFY_FINAL_RESULT | void SurfacesLightingModifyFinalResult | Common | Custom lighting model, can modify the previously calculated lighting result, such as adding outline light, necessary header files need to be included before defining the function. | diff --git a/versions/4.0/en/shader/surface-shader/surface-shader-structure.md b/versions/4.0/en/shader/surface-shader/surface-shader-structure.md new file mode 100644 index 0000000000..6f018c27a5 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/surface-shader-structure.md @@ -0,0 +1,105 @@ +# Surface Shader Overview + +A typical Surface Shader usually consists of four main parts. + +1. CCEffect +2. Shared UBO +3. Macro Remapping +4. Surface Functions +5. Shader Assembly: Assemble the above four parts with built-in Surface Shader functions. + +## 1. CCEffect + +CCEffect is used to describe the techniques, passes, properties, and render states of the Surface Shader. The material will generate default values and display them on the material panel according to the content in CCEffect. + +For more details, please refer to [Cocos Shader Syntax](../effect-syntax.md)。 + +## 2. Shared UBO + +The shared UBO gathers all the uniforms needed for all passes, simplifying shader writing. + +It is optional, but it is recommended for complex shaders. Here is an example from the built-in Surface Shader. + +```glsl +CCProgram shared-ubos %{ + uniform Constants { + vec4 titlingOffset; + ... + } +}% +``` + +> **Note**: It doesn't necessarily have to be called `shared-ubos` and `Constants`, as long as it's easy to be remembered. + +## 3. Macro Remapping + +Surface Shader provides a large number of built-in macros. These macros are not displayed on the panel by default. If you want these macros to be displayed on the panel, you need to remap them. + +In addition, you can put some macros in this section for easy management. Here is an example of a built-in Surface Shader. + +```glsl +CCProgram macro-remapping %{ + // ui displayed macros + #pragma define-meta HAS_SECOND_UV + #pragma define-meta USE_TWOSIDE + ... + #define CC_SURFACES_USE_SECOND_UV HAS_SECOND_UV + #define CC_SURFACES_USE_TWO_SIDED USE_TWOSIDE +}% +``` + +For more details, please refer to [Macro Definition and Remapping](./macro-remapping.md). + +> **Note**: It doesn't necessarily have to be called `macro-remapping`, as long as it's easy to be remembered. + +## 4. Surface Functions + +Surface Shader unifies the render process while also exposing many functions for users to customize render effects. + +For ease to management, we usually need to declare at least two CCProgram code segments for vs and fs. + +Take the built-in Surface Shader as an example. + +```glsl +CCProgram surface-vertex %{ + ... +}% + +CCProgram surface-fragment %{ + ... +}% +``` + +`surface-vertex` is used to organize vs related processing functions, while `surface-fragment` is used for fs. + +In these two segments, you can use macros to replace the internal functions, you can also add your own vs and fs inputs. + +For more details, please refer to [Replacement Function Using Marcos](./function-replace.md), [Vertex Shader Inputs](./vs-input.md) and [Fragment Shader Inputs](./fs-input.md)。 + +## 5. Shader Assembly + +In Surface Shader, the last part is the assembly of the shader. + +Let's take the built-in Surface Shader as an example. + +```glsl +CCProgram standard-vs %{ + ... +}% + +CCProgram shadow-caster-vs %{ + ... +}% + +CCProgram standard-fs %{ + ... +}% + +CCProgram shadow-caster-fs %{ + ... +}% +``` + +It's worth noting that the code segments of the above three parts(Shared UBO, Marco Remapping and Surface Functions) can have zero or multiple. Finally, the final shader is assembled as needed and referenced in the CCEffect. + +For more details, please refer to [Surface Shader Assembly](./shader-assembly.md)。 diff --git a/versions/4.0/en/shader/surface-shader/vs-input.md b/versions/4.0/en/shader/surface-shader/vs-input.md new file mode 100644 index 0000000000..dae4f62075 --- /dev/null +++ b/versions/4.0/en/shader/surface-shader/vs-input.md @@ -0,0 +1,49 @@ +# Vertex Shader Input + +## Parameters + +The input values for the Vertex Shader are all in the `SurfacesStandardVertexIntermediate` structure and are passed in as function parameters. + +| Vertex Shader Input Values | Type | Needed Macros | Meaning | +| ------------------- | ---- | ----------------------------- | ------------------------------------ | +| position | vec4 | N/A | Local position | +| normal | vec3 | N/A | Local normal | +| tangent | vec4 | CC_SURFACES_USE_TANGENT_SPACE | Local tangent and mirror normal flag | +| color | vec4 | CC_SURFACES_USE_VERTEX_COLOR | Vertex color| +| texCoord | vec2 | N/A | UV0 | +| texCoord1 | vec2 | CC_SURFACES_USE_SECOND_UV | UV1 | +| clipPos | vec4 | N/A | Projected coordinates | +| worldPos | vec3 | N/A | World position | +| worldNormal | vec4 | N/A | World normal and double-sided material flag | +| worldTangent | vec3 | CC_SURFACES_USE_TANGENT_SPACE | World tangent| +| worldBinormal | vec3 | CC_SURFACES_USE_TANGENT_SPACE | World binormal| + +## Macro Switch + +When you need to use input parameters with a macro switch, you need to enable the corresponding macro in the macro-remapping code segment. The example code is as follows. + +```glsl +CCProgram macro-remapping %{ + ... + //Use the second set of UVs + #define CC_SURFACES_USE_SECOND_UV 1 + //Use the world's binormal + #define CC_SURFACES_USE_TANGENT_SPACE 1 + ... +} +``` + +## Usage Example + +In any function with `SurfacesStandardVertexIntermediate` parameters, you can directly access the relevant parameters, taking the `SurfacesVertexModifyWorldPos` function as an example. + +```glsl +#define CC_SURFACES_VERTEX_MODIFY_WORLD_POS +vec3 SurfacesVertexModifyWorldPos(in SurfacesStandardVertexIntermediate In) +{ + vec3 worldPos = In.worldPos; + worldPos.x += sin(cc_time.x * worldPos.z); + worldPos.y += cos(cc_time.x * worldPos.z); + return worldPos; +} +``` diff --git a/versions/4.0/en/shader/ubo-layout.md b/versions/4.0/en/shader/ubo-layout.md new file mode 100644 index 0000000000..1deebf61ba --- /dev/null +++ b/versions/4.0/en/shader/ubo-layout.md @@ -0,0 +1,72 @@ +# UBO Layout + +Cocos Shader specifies that all non-sampler type uniforms should be declared in the form of UBO (Uniform Buffer Object/Uniform Block). + +Taking the `builtin-standard.effect` as an example, its uniform block declaration is as follows. + +```glsl +uniform Constants { + vec4 tilingOffset; + vec4 albedo; + vec4 albedoScaleAndCutoff; + vec4 pbrParams; + vec4 miscParams; + vec4 emissive; + vec4 emissiveScaleParam; + }; +``` + +And all UBOs should follow to the following rules. +1. Should no `vec3` members +2. For array type members, the size of each element should not be smaller than `vec4` +3. The declaration oder of members should not produce any padding. + +During compilation, the Cocos Shader compiler checks these rules to provide timely reminders in case of implicit padding errors. + +This may sound overly strict, but there are practical considerations behind it. + +First, UBOs are the only fundamental unit for efficient data reuse within the rendering pipeline, and separate declarations are no longer an good option. + +Second, UBOs in WebGL 2.0 only support the std140 layout, which follows a set of strict padding rules.[^1] + +- All vec3 members ar padded to `vec4` + + ```glsl + uniform ControversialType { + vec3 v3_1; // offset 0, length 16 [IMPLICIT PADDING!] + }; // total of 16 bytes + ``` + +- Arrays and structures with lengths smaller than `vec4` are padded to `vec4`. + + ```glsl + uniform ProblematicArrays { + float f4_1[4]; // offset 0, stride 16, length 64 [IMPLICIT PADDING!] + }; // total of 64 bytes + ``` + +- The actual offsets of all members within the UBO are aligned to the number of bytes they occupy.[^2] + + ```glsl + uniform IncorrectUBOOrder { + float f1_1; // offset 0, length 4 (aligned to 4 bytes) + vec2 v2; // offset 8, length 8 (aligned to 8 bytes) [IMPLICIT PADDING!] + float f1_2; // offset 16, length 4 (aligned to 4 bytes) + }; // total of 32 bytes + + uniform CorrectUBOOrder { + float f1_1; // offset 0, length 4 (aligned to 4 bytes) + float f1_2; // offset 4, length 4 (aligned to 4 bytes) + vec2 v2; // offset 8, length 8 (aligned to 8 bytes) + }; // total of 16 bytes + ``` + +This results in a significant waste of space, and the graphics driver on certain devices may not fully comply with this standard[^3]. Therefore, Cocos Shader currently chooses a set of strict rules to help eliminate some very subtle runtime issues. + +> **Note**: Through the [Property Target](pass-parameter-list.md#Properties) mechanism, you can independently edit the specific components off any uniform. The types of uniforms do not need to directly correspond to the display in the Inspector panel or the code for assigning runtime parameters. + +[^1]: [OpenGL 4.5, Section 7.6.2.2, page 137](http://www.opengl.org/registry/doc/glspec45.core.pdf#page=159) + +[^2]: Please note that in the example code, the total length of UBO IncorrectUBOOrder is 32 bytes. However, in reality, the actual data is still platform-dependent even today. This discrepancy appears to be due to an oversight in the GLSL standard. For more detailed discussions on this topic, you can refer to [this link](https://bugs.chromium.org/p/chromium/issues/detail?id=988988)。 + +[^3]: **Interface Block - OpenGL Wiki**: diff --git a/versions/4.0/en/shader/uniform.md b/versions/4.0/en/shader/uniform.md new file mode 100644 index 0000000000..c76f33d4e5 --- /dev/null +++ b/versions/4.0/en/shader/uniform.md @@ -0,0 +1,55 @@ +# Built-in Shader Uniforms + +To use built-in shader variables, you need to include the corresponding chunks first. +All currently available built-in uniforms, grouped by the chunks they are located: + +## `cc-local.chunk` + +| Name | Type | Info | +| :-- | :-- | :-- | +| `cc_matWorld` | mat4 | model to world transform matrix | +| `cc_matWorldIT` | mat4 | inverse-transpose of model to world transform matrix | + +## `cc-global.chunk` + +| Name | Type | Info | +| :-- | :-- | :-- | +| `cc_time` | vec4 | x: seconds since engine started. y: The elapsed time since the last frame, in seconds (delta time). z: The current frame count. w: Reserved for future use (usually set to 0). | +| `cc_screenSize` | vec4 | xy: shading screen size
zw: reciprocal of shading screen size | +| `cc_screenScale` | vec4 | xy: screen scale
zw: reciprocal of screen scale | +| `cc_nativeSize` | vec4 | xy: canvas size
zw: reciprocal of canvas size | +| `cc_matView` | mat4 | view transform matrix | +| `cc_matViewInv` | mat4 | inverse view matrix | +| `cc_matProj` | mat4 | projection transform matrix | +| `cc_matProjInv` | mat4 | inverse projection transform matrix | +| `cc_matViewProj` | mat4 | view-projection transform matrix | +| `cc_matViewProjInv` | mat4 | inverse view-projection transform matrix | +| `cc_cameraPos` | vec4 | xyz: camera position | +| `cc_exposure` | vec4 | x: camera exposure
y: reciprocal of camera exposure
z: is HDR enabled w: HDR-LDR scaling factor | +| `cc_mainLitDir` | vec4 | xyz: direction of the main directional light | +| `cc_mainLitColor` | vec4 | xyz: color of the main directional light
w: intensity | +| `cc_ambientSky` | vec4 | xyz: ambient sky color
w: intensity | +| `cc_ambientGround` | vec4 | xyz: ambient ground color | + +## `cc-environment.chunk` + +| Name | Type | Info | +| :-- | :-- | :-- | +| `cc_environment` | samplerCube | IBL environment map | + +## `cc-forward-light.chunk` + +| Name | Type | Info | +| :-- | :-- | :-- | +| `cc_lightPos[LIGHTS_PER_PASS]` | vec4 | xyz: light source position
 w: valid light source type 0: directional light, 1: spherical light, 2: spotlight, 3: point light, 4: range directional light, 5: unknown | +| `cc_lightColor[LIGHTS_PER_PASS]` | vec4 | xyz: spherical light color
w: spherical light intensity | +| `cc_lightSizeRangeAngle[LIGHTS_PER_PASS]` | vec4 | x: light source size, y: light source range, z: cos(outer half-angle), w: enable shadow | +| `cc_lightDir[LIGHTS_PER_PASS]` | vec4 | xyz: direction, w: unused | +| `cc_lightBoundingSizeVS[LIGHTS_PER_PASS]` | vec4 | xyz: range directional light node half-scale + +## `cc-shadow.chunk` + +| Name | Type | Info | +| :-- | :-- | :-- | +| `cc_matLightPlaneProj` | mat4 | planar shadow transform matrix | +| `cc_shadowColor` | vec4 | shadow color | diff --git a/versions/4.0/en/shader/vscode-plugin.md b/versions/4.0/en/shader/vscode-plugin.md new file mode 100644 index 0000000000..278f6eaa36 --- /dev/null +++ b/versions/4.0/en/shader/vscode-plugin.md @@ -0,0 +1,37 @@ +# VSCode Shader Extension(Experimental) + +To enable syntax highlighting for Cocos Shader files (`*.effect`), search for the "Cocos Effect" extension in the Visual Studio Code extensions store and install it. + +After installing completed, If you open a `.effect` from a project which is using Cocos Creator 3.7.3 or later in VS Code, the following features will be available to be used. + +## 1. Autocompletion and Syntax Highlighting + +Autocompletion and syntax highlighting for built-in shader functions, macros, and global uniforms. + +As you type, a list of suggestions will appear for keywords. Use the up and down arrow keys to select from the list. Additionally, information about the selected suggestion, including comments and the location within the file, will be displayed. + +Hovering the mouse over a completed keyword will show a tooltip with relevant information. + +> **Note**: As it is an experimental feature, it currently only supports the autocompletion of commonly used built-in shader code and does not support autocompletion for user-defined code. + +## 2. Syntax Checking and Error Jumping + +When you have a `*.effect` file open in the VS Code, you can use the "View" menu and select "Command Palette" ( or press Ctrl + Shift + P ) to access the command panel. Choose **Cocos Effect:compile effect** to invoke the tool to automatically compile the current file and provide corresponding error messages. You can Ctrl + Click on the file path in the error message window to jump to the corresponding line of code. + +To the right of this option, there is a small gear icon that allows you to add a keyboard shortcut binding by hitting it. After adding the binding, you can use the shortcut key to invoke the syntax checker. See the GIF below for the complete workflow. + +![](img/compile-binding.gif) + +> **Note**: As it is an experimental feature, it currently only supports on Windows. + +## 3. Other Settings + +The extension will automatically search for the corresponding built-in shader files and provide completion suggestions based on the location of the opened `*.effect` or `*.chunk` file. However, this functionality is limited to shader files using Cocos Creator 3.7.3 or later. + +If you open a file from an external folder(a folder not belongs to a Cocos Creator project) or and older version of Cocos Creator, the aforementioned features will not work. In such cases, you can download a newer version of Cocos Creator and specify the folder path for the new. This will enable support for all `*.effect` files. + +Here are the steps to follow. + +1. Go to the "File" -> "Preferences" -> "Settings" +2. In the search box, type "cocos". +3. In the "Cocos-effect: Engine Path" field, enter the folder path of thew newer version of Cocos Creator ( it will be automatically recognized ) diff --git a/versions/4.0/en/shader/webgl-100-fallback.md b/versions/4.0/en/shader/webgl-100-fallback.md new file mode 100644 index 0000000000..63769f6468 --- /dev/null +++ b/versions/4.0/en/shader/webgl-100-fallback.md @@ -0,0 +1,21 @@ +# WebGL 1.0 Fallback Compatibility + +Due to only GLSL 100 is supported in WebGL 1.0, shaders written in higher GLSL versions won't work well with it. Cocos Shader provides fallback code that converts GLSL 300 ES to GLSL 100 during compilation. Therefore, developers don't need to worry about this layer of transformation. + +It's important to note that the current automatic fallback compatibility strategy only supports basic format conversions. If you use features only supported by GLSL 300 ES-specific functions (such as `texelFetch`, `textureGrad`) or some specific extensions, it is recommended to use the `__VERSION__` macro to check the GLSL version and write different code snippets for different GLSL versions. + +Here's an example of the code. + +```glsl +#if __VERSION__ < 300 +#ifdef GL_EXT_shader_texture_lod + vec4 color = textureCubeLodEXT(envmap, R, roughness); +#else + vec4 color = textureCube(envmap, R); +#endif +#else + vec4 color = textureLod(envmap, R, roughness); +#endif +``` + +> **Note**: During compilation, the Cocos Shader compiler will analyze all constant macros and generate multiple versions of shaders. diff --git a/versions/4.0/en/shader/write-effect-2d-sprite-gradient.md b/versions/4.0/en/shader/write-effect-2d-sprite-gradient.md new file mode 100644 index 0000000000..19499a9386 --- /dev/null +++ b/versions/4.0/en/shader/write-effect-2d-sprite-gradient.md @@ -0,0 +1,265 @@ +# 2D Sprite Effect:Gradient + +By default, UI and 2D components use the engine's built-in shaders, which are placed in the **internal -> effects** directory of the **Assets** panel. + +For any UI and 2D component that holds a CustomMaterial property, custom materials can be implemented within the **Inspector** panel by selecting from the drop-down box for that property or by dragging and dropping from within the **Assets** panel. + +Engine specifies that there can be only one custom material for UI/2D components. + +![built-in ui effect](../material-system/img/ui-select.png) + +> **Note**. +> 1. The batching may be interrupted after using custom materials. +> 2. For UI and 2D component batching, please refer to: [2D Renderable Components Batching Guidelines](../ui-system/components/engine/ui-batch.md) + +This article will demonstrate how to use custom shaders for UI and 2D components by implementing a gradient shader for sprites. + +## Project creation + +Create a new 2D project via CocosDashBoard. + +And Create a new scene and add a Sprite to the scene. + +![create sprite](img/create-sprite.png) + +Perform the following actions within **Assets** panel. + +- Create a effect file named gradient.effect; +- Copy the contents of the `builtin-sprite` shader in **Assets -> intenal -> effects** to gradient.effect; +- Create a material named gradient.mtl and select gradient.effect in the Effect column of the **Inspector**; +- Import any valid texture. + +Materials and shaders can be created by right-clicking anywhere in **Assets** panel, or by clicking the `+` button on **Assets** panel. + +![create-mtl-effect](img/2d-create-guild.png) + +The created project is shown in the figure below: + +![create asset](img/create-asset.png) + +Select the created sprite and assign the gradient.mtl material and the imported texture to the corresponding properties of the sprite. + +![custom-material](img/custom-material.png) + +## CCEffect + +By observation, a gradient can be understood as a change in color on an axis as the coordinates change. Therefore, we add two color properties to the properties of the shader in the CCEffect paragraph to represent the start color and end color of the gradient. + +```yaml +startColor: { value: [1.0, 1.0, 1.0, 1.0], # The default value for the RGBA channel of gradientA + editor: {type: color} } # The style displayed in the Inspector, shown here as a color +endColor: { value: [1.0, 1.0, 1.0, 1.0], + editor: {type: color} }} +``` + +The CCEffect code is as follows: + +```yaml +CCEffect %{ + techniques: + - passes: + - vert: sprite-vs:vert + frag: sprite-fs:frag + depthStencilState: + depthTest: false + depthWrite: false + blendState: + targets: + - blend: true + blendSrc: src_alpha + blendDst: one_minus_src_alpha + blendDstAlpha: one_minus_src_alpha + rasterizerState: + cullMode: none + properties: + alphaThreshold: { value: 0.5 } + startColor: { value: [1.0, 1.0, 1.0, 1.0], editor: {type: color} } + endColor: { value: [1.0, 1.0, 1.0, 1.0], editor: {type: color} } +}% +``` + +Note that two color values `startColor` and `endColor` are defined here, and the corresponding Uniform needs to be added if these two colors are to be properly passed to the shader fragment. + +Engine states that discrete use of Uniform is not allowed, so add the following code to the `CCProgram sprite-fs` segment. + +```glsl +uniform Constant{ + vec4 startColor; + vec4 endColor; +}; +``` + +In this case, the engine will automatically associate the properties defined in `properties` with the Uniform in `Constant`. + +## Vertex shader + +Additional processing of vertex shaders is usually not needed, so the system's built-in `sprite-vs` is retained. + +![vs](img/2d-gradient-vs.png) + +## Fragment shader + +Within the default sprite shader, the XY axis of the sprite vertices corresponds to the UV of the texture coordinates, so consider using a change in texture coordinates to reach a gradient by adding the following code to the scope defined by the USE_TEXTURE macro: + +```glsl +#if USE_TEXTURE + o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0); + #if IS_GRAY + float gray = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b; + o.r = o.g = o.b = gray; + #endif + + // Adjust the gradient color according to the UV change + o.rgb *= mix(startColor, endColor, vec4(uv0.x)).rgb; +#endif +``` + +Check the `USE_TEXTURE` option on the original material. + +![use-texture](img/check-use-texture.png) + +By adjusting the **startColor** and **endColor** on the material, different gradients can be observed: **startColor** and **endColor**. + +![material-color](img/adjust-gradient-color.png) + +The color the sprite is as follows: + +![preview](img/view-x-gradient.png) + +## Use preprocessing macros to define + +Within the above fragment shader, only the gradient of the texture coordinate axis U is considered. For flexibility and to support more functionality, the gradient of the different axes is implemented through preprocessing macro definitions, so the following code is deleted. + +```glsl +o.rgb *= mix(startColor, endColor, vec4(uv0.x)).rgb; +``` + +And add the following code: + +```glsl +#if USE_HORIZONTAL + o.rgb *= mix(startColor, endColor, vec4(uv0.x)).rgb; +#endif + +#if USE_VERTICAL + o.rgb *= mix(startColor, endColor, vec4(uv0.y)).rgb; +#endif +``` + +The preprocessing macros `USE_HORIZONTAL` and `USE_VERTICAL` are declared here, representing horizontal and vertical gradients respectively, and can be conveniently used on demand. + +![macro-preview](img/macro-preview.png) + +The complete effect code is as follows: + +```glsl +// Copyright (c) 2017-2020 Xiamen Yaji Software Co., Ltd. +CCEffect %{ + techniques: + - passes: + - vert: sprite-vs:vert + frag: sprite-fs:frag + depthStencilState: + depthTest: false + depthWrite: false + blendState: + targets: + - blend: true + blendSrc: src_alpha + blendDst: one_minus_src_alpha + blendDstAlpha: one_minus_src_alpha + rasterizerState: + cullMode: none + properties: + alphaThreshold: { value: 0.5 } + startColor: { value: [1.0, 1.0, 1.0, 1.0], editor: {type: color} } + endColor: { value: [1.0, 1.0, 1.0, 1.0], editor: {type: color} } +}% + +CCProgram sprite-vs %{ + precision highp float; + #include + #if USE_LOCAL + #include + #endif + #if SAMPLE_FROM_RT + #include + #endif + in vec3 a_position; + in vec2 a_texCoord; + in vec4 a_color; + + out vec4 color; + out vec2 uv0; + + vec4 vert () { + vec4 pos = vec4(a_position, 1); + + #if USE_LOCAL + pos = cc_matWorld * pos; + #endif + + #if USE_PIXEL_ALIGNMENT + pos = cc_matView * pos; + pos.xyz = floor(pos.xyz); + pos = cc_matProj * pos; + #else + pos = cc_matViewProj * pos; + #endif + + uv0 = a_texCoord; + #if SAMPLE_FROM_RT + CC_HANDLE_RT_SAMPLE_FLIP(uv0); + #endif + color = a_color; + + return pos; + } +}% + +CCProgram sprite-fs %{ + precision highp float; + #include + #include + + in vec4 color; + + uniform Constant{ + vec4 startColor; + vec4 endColor; + }; + + #if USE_TEXTURE + in vec2 uv0; + #pragma builtin(local) + layout(set = 2, binding = 11) uniform sampler2D cc_spriteTexture; + #endif + + vec4 frag () { + vec4 o = vec4(1, 1, 1, 1); + + #if USE_TEXTURE + o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0); + #if IS_GRAY + float gray = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b; + o.r = o.g = o.b = gray; + #endif + + #if USE_HORIZONTAL + o.rgb *= mix(startColor, endColor, vec4(uv0.x)).rgb; + #endif + + #if USE_VERTICAL + o.rgb *= mix(startColor, endColor, vec4(uv0.y)).rgb; + #endif + + #endif + + o *= color; + + ALPHA_TEST(o); + return o; + } +}% + +``` diff --git a/versions/4.0/en/shader/write-effect-3d-rim-light.md b/versions/4.0/en/shader/write-effect-3d-rim-light.md new file mode 100644 index 0000000000..64deac62fb --- /dev/null +++ b/versions/4.0/en/shader/write-effect-3d-rim-light.md @@ -0,0 +1,447 @@ +# 3D Effect:RimLight + +This article will demonstrate how to write a shader (Cocos Effect) that can be used in Cocos for 3D model rendering by implementing a RimLight effect. + +**RimLight** is a technique that makes objects more vivid by making their edges emitting. + +**RimLight** is an application of the Fresnel phenomenon [^1], a simple and efficient shader for enhancing rendering by calculating the magnitude of the angle between the object normals and the view direction, and adjusting the position and color of the light emitted. + +In the calculation of rim-light, the greater the angle between the line of sight and the normal, the more pronounced the fringe light is. + +![rimlight preview](img/rim-preview.jpg) + +## Material and effect creation + +First refer to the [Effect asset](./effect-inspector.md) to create a new shader named **rimlight.effect** and create a material **rimlight.mtl** that uses that shader. + +![create rimlight](img/rim-light-effect.png) + +## CCEffect + +Cocos Effect uses YAML as a parser, so CCEffect needs to be written in a way that adheres to the YAML syntax standard, for which you can refer to [YAML 101](./yaml-101.md). + +In this example, the semi-transparent effect will not be considered for the time being, and the `transparent` part can be removed at this point. + +```yaml +# Delete the following sections +- name: transparent + passes: + - vert: general-vs:vert # builtin header + frag: rimlight-fs:frag + blendState: + targets: + - blend: true + blendSrc: src_alpha + blendDst: one_minus_src_alpha + blendSrcAlpha: src_alpha + blendDstAlpha: one_minus_src_alpha + properties: *props +``` + +Modify the `frag` function in the `opaque` section to: `rimlight-fs:frag`, which is the slice shader part of the edge light to be implemented next. + +```yaml +- name: opaque + passes: + - vert: general-vs:vert # builtin header + frag: rimlight-fs:frag +``` + +To make it easier to adjust the color of the rim light, add an attribute `rimLightColor` for adjusting the color of the rim light, and since translucency is not considered, only the RGB channel of this color is used. + +```yaml +rimLightColor: { value: [1.0, 1.0, 1.0], # default values of RGB + target: rimColor.rgb, # Bind to the RGB channel of Uniform rimColor + editor: { # Style definition in material's property inspector + displayName: Rim Color, # Show Rim Color as display name + type: color } } # The type of this field is a color value +``` + +The CCEffect code is as follows: + +```yaml +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: general-vs:vert # builtin header + frag: rimlight-fs:frag + properties: &props + mainTexture: { value: white } + mainColor: { value: [1, 1, 1, 1], editor: { type: color } } + # Rim Light's colors, which depend only on the components of the three RGB channels + rimLightColor: { value: [1.0, 1.0, 1.0], target: rimColor.rgb, editor: { displayName: Rim Color, type: color } } +}% +``` + +> **Note**: The corresponding `rimColor` field needs to be added to the `uniform Constant` of the fragment shader. +> +> ```glsl +> uniform Constant { +> vec4 mainColor; +> vec4 rimColor; +> }; +> ``` + +This binding means that the values of the RGB component of the shader's `rimLightColor` are transferred through the engine to the `rgb` three components of the Uniform `rimColor`. + +> **Note**: The engine specifies that vectors of type vec3 cannot be used to avoid [implicit padding](./effect-syntax.md), so when using a 3-dimensional vector (vec3), you can choose to use a 4-dimensional vector (vec4) instead. Don't worry, the alpha channel will be utilized without being wasted. + +## Vertex shader + +Usually the engine's built-in vertex shader can meet most of the development needs, so the engine's built-in vertex shader can be used directly. + +```yaml + - vert: general-vs:vert # builtin header +``` + +## Fragment shader + +Modify the fragment shader code in the shader created via **Assets** panel by changing `CCProgram unlit-fs` to `CCProgram rimlight-fs`. + +Before modification: + +```glsl +CCProgram unlit-fs %{ + precision highp float; + ... +}% +``` + +After modification: + +```glsl +CCProgram rimlight-fs %{ + precision highp float; + ... +}% +``` + +In lighting calculations, it is usually necessary to calculate the angle between the normal and the line of sight, which is closely related to the **camera position**. + +![view-direction](img/view-direction.png) + +As shown above, to calculate the line of sight, you need to subtract the **position of the object** from the **camera position**. Within the shader, to get the **camera position**, you need to use `cc_cameraPos` in [Built-in Shader Uniforms](uniform.md), which is stored inside the `cc-global` shader fragment. This uniform is stored in the `cc-global` shader fragment via [`include`](./effect-chunk-index.md) keyword, the entire shader fragment can be easily introduced. + +```glsl +#include // Include Cocos Creator built-in global uniforms +``` + +Now the fragment shader is as follows: + +```glsl +CCProgram rimlight-fs %{ + precision highp float; + #include // Include Cocos Creator built-in global uniforms + #include + #include + + ... +} +``` + +The view direction is calculated by subtracting the current camera position (`cc_cameraPos`) from the position information passed in by the vertex shader within the slice shader `in vec3 v_position`. + +```glsl +vec3 viewDirection = cc_cameraPos.xyz - v_position; // Calculate the view direction +``` + +We don't care about the length of the view vector, so we get `viewDirection` and normalize it with the `normalize` method. + +```glsl +vec3 normalizedViewDirection = normalize(viewDirection); // Normalize the view direction +``` + +The xyz component of `cc_cameraPos` represents the position of the camera. + +The fragment shader code is as follows: + +```glsl + vec4 frag(){ + vec3 viewDirection = cc_cameraPos.xyz - v_position; // Calculate the view direction + vec3 normalizedViewDirection = normalize(viewDirection); // Normalize view direction + vec4 col = mainColor * texture(mainTexture, v_uv); // Calculate the final color + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); + } +``` + +Next, you need to calculate the angle between the normal and the view direction. Since the built-in standard vertex shader `general-vs:vert` is used, the normal has been passed from the vertex shader to the fragment shader, but is not declared. + +The following code demonstrates how to declare normals: + +```glsl +in vec3 v_normal; +``` + +The fragment shader code at this point is as follows: + +```glsl +CCProgram rimlight-fs %{ + precision highp float; + #include + #include + #include + + in vec2 v_uv; + in vec3 v_normal; + in vec3 v_position; + + .... +} +``` + +The normal is no longer in a normalized state due to the interpolation of the pipeline, so it needs to be normalized, using the `normalize` function to do so. + +```glsl +vec3 normal = normalize(v_normal); // Renormalize the normals. +``` + +The code of the fragment shader at this point is as follows: + +```glsl +vec4 frag(){ + vec3 normal = normalize(v_normal); // Renormalize the normals. + vec3 viewDirection = cc_cameraPos.xyz - v_position; // Calculate the view direction + vec3 normalizedViewDirection = normalize(viewDirection); // Normalization of the view direction + vec4 col = mainColor * texture(mainTexture, v_uv); // Calculate the final color + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); +} +``` + +At this point, the angle between the normal and the view direction can be calculated. Inside linear algebra, the dot product is expressed as the cosine of the modulus of two vectors multiplied by the angle. + +```txt +a·b = |a|*|b|*cos(θ) +``` + +A simple exchange law leads to. + +```txt +cos(θ) = a·b /(|a|*|b|) +``` + +Since the normal and view directions are normalized, they are modulo 1 and the result of the dot product is expressed as the cos values of the normal and view directions. + +```txt +cos(θ) = a·b +``` + +Translate this into code as follows: + +```glsl +dot(normal, normalizedViewDirection) +``` + +Note that the dot product may be calculated to be less than 0, while the color is positive, and is bounded in the range [0, 1] by the `max` function. + +```glsl +max(dot(normal, normalizedViewDirection), 0.0) +``` + +In this case, the RimLight color can be adjusted according to the result of the dot product: + +```glsl +float rimPower = max(dot(normal, normalizedViewDirection), 0.0);// Calculating the intensity in RimLight +vec4 col = mainColor * texture(mainTexture, v_uv); // Calculate the final color +col.rgb += rimPower * rimColor.rgb; // Increase rim-light +``` + +The fragment shader code is as follows: + +```glsl +vec4 frag(){ + vec3 normal = normalize(v_normal);// 重新归一化法线。 + vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向 + vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化 + float rimPower = max(dot(normal, normalizedViewDirection), 0.0); // 计算 RimLight 的亮度 + vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色 + col.rgb += rimPower * rimColor.rgb; // 增加边缘光 + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); +} +``` + +t can be observed that the center of the object is brighter than the edges because the angle between the normal of the edge vertex and the angle of view is larger and the cosine value obtained is smaller. + +> **Note**: If you cannot observe the effect in this step, you can adjust `MainColor` so that it is not white. This is because the default `MainColor` color is white, covering up the color of the edge light. + +![dot result](img/dot.jpg) + +To adjust this result by simply subtracting the result of the dot product by 1, delete the following code. + +~~``` float rimPower = max(dot(normal, normalizedViewDirection), 0.0); ```~~ + +And add the following code: + +```glsl +float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0); +``` + +The fragment shader code is as follows: + +```glsl +vec4 frag(){ + vec3 normal = normalize(v_normal); // Renormalize the normals. + vec3 viewDirection = cc_cameraPos.xyz - v_position; // Calculate the view direction + vec3 normalizedViewDirection = normalize(viewDirection); // Normalize the view direction + float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0); + vec4 col = mainColor * texture(mainTexture, v_uv); // Calculate the final color + col.rgb += rimPower * rimColor.rgb; // Add rim-light + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); +} +``` + +![one minus dot result](img/1-dot.jpg) + +Although the edge light effect can be observed, the light is too strong and not easily adjustable. An adjustable parameter `rimIntensity` can be added to the CCEffect. Since the alpha component of `rimColor` was not used before, borrowing this component for binding saves additional Uniform. + +> **Note**: When writing shaders, you need to avoid implicit padding, for this see: [UBO memory layout](./effect-syntax.md), where using the unused alpha channel to store the intensity of the edge light maximizes the use of the `rimColor` field. + +Add the following code to CCEffect. + +```yaml +rimIntensity: { value: 1.0, # Default value is 1 + target: rimColor.a, # Bind to the alpha channel of ‘rimColor’ + editor: { # The style of the property inspector + slide: true, # Use the slider as the display style + range: [0, 10], # The range of values for the slider + step: 0.1 } # Value change each time the adjustment button is clicked +``` + +The CCEffect code at this point is as follows: + +```yaml +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: general-vs:vert # builtin header + frag: rimlight-fs:frag + properties: &props + mainTexture: { value: white } + mainColor: { value: [1, 1, 1, 1], editor: { type: color } } + # Rim Light's colors, which depend only on the components of the three rgb channels + rimLightColor: { value: [1.0, 1.0, 1.0], target: rimColor.rgb, editor: { displayName: Rim Color, type: color } } + # The alpha channel of rimLightColor is not used and is reused to describe the intensity of rimLightColor. + rimIntensity: { value: 1.0, target: rimColor.a, editor: {slide: true, range: [0, 10], step: 0.1}} +}% + +``` + +Adding this property will add an adjustable RimIntensity to the material **Inspector** panel. + +![intensity](./img/add-intensity.png) + +The `pow` function adjusts the edge light so that its range does not vary linearly, which can reflect a better effect, by removing the following code. + +~~``` col.rgb += rimPower * rimColor.rgb; ```~~ + +Add the following codes: + +```glsl +float rimIntensity = rimColor.a; // alpha channel is the index of brightness +col.rgb += pow(rimPower, rimIntensity) * rimColor.rgb; // Exponential modification of dot product using 'pow' function +``` + +`pow` is a built-in GLSL function of the form `pow(x, p)`, which represents an exponential function with `x` as the base and `p` as the exponent. + +The final fragment shader code is as follows: + +```glsl + vec4 frag(){ + vec3 normal = normalize(v_normal); // Renormalize the normals + vec3 viewDirection = cc_cameraPos.xyz - v_position; // Calculate the view direction + vec3 normalizedViewDirection = normalize(viewDirection); // Normalize the view direction + float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0);// Calculate the intensity of rim-light + vec4 col = mainColor * texture(mainTexture, v_uv); // Calculate the final color + float rimIntensity = rimColor.a; // alpha channel is the index of brightness + col.rgb += pow(rimPower, rimIntensity) * rimColor.rgb; // Add rim-light + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); + } +``` + +Afterwards, change the value of **rimIntensity** on the material **Inspector** panel to 3. + +![intensity](img/intensity.png) + +More natural edge lighting can be observed at this point. + +![changed preview](img/preview-instensity.jpg) + +The color and intensity of the rim light can be easily adjusted with **Rim Color** and **rimIntensity**. + +![color](img/adjust-option.png) + +![result](img/opt-overview.jpg) + +The complete shader code is as follows: + +```glsl +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: general-vs:vert # builtin header + frag: rimlight-fs:frag + properties: &props + mainTexture: { value: white } + mainColor: { value: [1, 1, 1, 1], editor: { type: color } } + # Rim Light's colors, which depend only on the components of the three rgb channels + rimLightColor: { value: [1.0, 1.0, 1.0], target: rimColor.rgb, editor: { displayName: Rim Color, type: color } } + # The alpha channel of rimLightColor is not used and is reused to describe the intensity of rimLightColor. + rimIntensity: { value: 1.0, target: rimColor.a, editor: {slide: true, range: [0, 10], step: 0.1}} +}% + +CCProgram rimlight-fs %{ + precision highp float; + #include + #include + #include + + in vec2 v_uv; + in vec3 v_normal; + in vec3 v_position; + + uniform sampler2D mainTexture; + + uniform Constant { + vec4 mainColor; + vec4 rimColor; + }; + vec4 frag(){ + vec3 normal = normalize(v_normal); // Renormalize the normals. + vec3 viewDirection = cc_cameraPos.xyz - v_position; // Calculate view direction + vec3 normalizedViewDirection = normalize(viewDirection); // Normalize view direction + float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0);// Calculate the intensity of rim light + vec4 col = mainColor * texture(mainTexture, v_uv); // Calculate the final color + float rimIntensity = rimColor.a; // alpha channel is the index of brightness + col.rgb += pow(rimPower, rimIntensity) * rimColor.rgb; // Add rim light + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); + } +}% +``` + +To make the color of the edge light affected by the texture color, the following code can be used. + +Change the following code: + +```glsl +col.rgb += pow(rimPower, rimIntensity) * rimColor.rgb; // Add rim light +``` + +To: + +```glsl +col.rgb *= 1.0 + pow(rimPower, rimIntensity) * rimColor.rgb; // The rim light is influenced by the object coloring +``` + +The rim light is then influenced by the final texture and vertex color. + +![color](img/effect-by-color.jpg) + +[^1]: Fresnel phenomenon: Augustin-Jean Fresnel was a famous French physicist in the 18th century, who proposed the Fresnel equation to explain the relationship between reflection and refraction of light very well. If you go to observe the calm water surface under the sunlight you can find that the further away from the observation point the stronger the reflection of the water surface, this phenomenon that the intensity of light changes with the angle of observation is known as Fresnel's phenomenon.
![fresnel](img/fresnel.jpg) diff --git a/versions/4.0/en/shader/write-effect-overview.md b/versions/4.0/en/shader/write-effect-overview.md new file mode 100644 index 0000000000..7b512a9551 --- /dev/null +++ b/versions/4.0/en/shader/write-effect-overview.md @@ -0,0 +1,23 @@ +# Custom Effect + +If the built-in effects cannot meet the requirements, users can custom your own effects. + +There are two ways to customize effects: + +1. Refer to [Effect asset](./effect-inspector.md) to create a new effect. + +2. Based on built-in shaders. Copy the corresponding built-in shaders from the **internal -> effects** directory in the **Assets** panel to the **Assets** directory, and then customize them. + +## Preparation + +Since the shader uses YAML as the flow control and GLSL as the shader language, it is necessary to have a certain degree of familiarity and understanding of these knowledge before customizing the shader. + +For unfamiliar developers, we have also prepared some brief introductions: + +- [GLSL Basic](./glsl.md) +- [YAML 101](./yaml-101.md) + +This section will take custom a 2D effect and a 3D effect as examples to introduce the custom process in detail. For details, please refer to: + +- [3D effect: rimlight](write-effect-3d-rim-light.md) +- [2D effect:Gradient](write-effect-2d-sprite-gradient.md) diff --git a/versions/4.0/en/shader/yaml-101.md b/versions/4.0/en/shader/yaml-101.md new file mode 100644 index 0000000000..31538e3d3d --- /dev/null +++ b/versions/4.0/en/shader/yaml-101.md @@ -0,0 +1,151 @@ +# YAML 101 + +Cocos Creator 3.0 uses a parser that conforms to the YAML 1.2 standard, which means that Creator is fully compatible with JSON, and use JSON directly without any problems. + +```yaml +"techniques": + [{ + "passes": + [{ + "vert": "skybox-vs", + "frag": "skybox-fs", + "rasterizerState": + { + "cullMode": "none" + } + # ... hash sign for comments + }] + }] +``` + +But of course it would be cumbersome and error-prone, so what YAML provides is a much simpler representation of the same data: + +- All quotation marks and commas can be omitted + + ```yaml + key1: 1 + key2: unquoted string + ``` + + > **Note**: never omit the space after colon + +- Like Python, indentation is part of the syntax, representing hierarchy of the data[^1] + + ```yaml + object1: + key1: false + object2: + key2: 3.14 + key3: 0xdeadbeef + nestedObject: + key4: 'quoted string' + ``` + +- Array elements are represented by dash+space prefix + + ```yaml + - 42 + - "double-quoted string" + - arrayElement3: + key1: punctuations? sure. + key2: you can even have {}s as long as they are not the first character + key3: { nested1: 'but no unquoted string allowed inside brackets', nested2: 'also notice the comma is back too' } + ``` + +With these in mind, the effect manifest at the beginning of this document can be re-write as follows: + +```yaml +techniques: +- passes: + - vert: skybox-vs + frag: skybox-fs + rasterizerState: + cullMode: none + # ... +``` + +Another YAML feature that comes in handy is referencing and inheriting between data. + +- **Reference** + + ```yaml + object1: &o1 + key1: value1 + object2: + key2: value2 + key3: *o1 + ``` + + This is its corresponding JSON: + + ```json + { + "object1": { + "key1": "value1" + }, + "object2": { + "key2": "value2", + "key3": { + "key1": "value1" + } + } + } + ``` + +- **Inheritance** + + ```yaml + object1: &o1 + key1: value1 + key2: value2 + object2: + <<: *o1 + key3: value3 + ``` + + The corresponding JSON: + + ```json + { + "object1": { + "key1": "value1", + "key2": "value2" + }, + "object2": { + "key1": "value1", + "key2": "value2", + "key3": "value3" + } + } + ``` + +For our purposes, like when multiple pass has the same properties, etc. it could be really helpful: + +```yaml +techniques: +- passes: + - # pass 1 specifications... + properties: &props # declare once... + p1: { value: [ 1, 1, 1, 1 ] } + p2: { sampler: { mipFilter: linear } } + p3: { inspector: { type: color } } + - # pass 2 specifications... + properties: *props # reference anywhere +``` + +Finally, before writing any YAML, wrap it in a **CCEffect** block first: + +```yaml +CCEffect %{ + # YAML starts here +}% +``` + +You can always refer to any [online YAML JSON converter](https://codebeautify.org/yaml-to-json-xml-csv) to play around ideas. + +## Reference Link + +- [YAML on Wikipedia](https://en.wikipedia.org/wiki/YAML) +- [YAML.org Spec](https://yaml.org/spec/1.2/spec.html) + +[^1]: The YAML standard doesn't support tabs, so the effect compiler will try to replace all the tabs in file with 2 spaces first, to avoid the trivial yet annoying trouble of accidentally inserting tabs somewhere. But overall, please try to avoid doing that completely to make sure the compilation goes smoothly. diff --git a/versions/4.0/en/styles/website.css b/versions/4.0/en/styles/website.css new file mode 100644 index 0000000000..e81bf0d72f --- /dev/null +++ b/versions/4.0/en/styles/website.css @@ -0,0 +1,185 @@ +.navigation { + top: 90px; +} +.book-summary nav.autoshow a, +.book-summary nav.autoshow a:hover { + color: inherit; + text-decoration: none; +} +@media (min-width: 800px) { + .book-summary nav.autoshow { + display: none; + } +} +.cocos-navbar { + overflow: visible; + width: 100%; + min-width: 800px; + z-index: 2; + font-size: 0.85em; + color: #7e888b; + background: #f5f5f5; + display: flex; + position: absolute; + top: 0; +} +.cocos-navbar::after { + clear: both; + content: '.'; + display: block; + visibility: hidden; + height: 0; +} +.cocos-navbar a, +.cocos-navbar a:hover { + color: inherit; + text-decoration: none; +} +.cocos-navbar ul { + list-style-type: none; + margin: 0; + padding: 0; + width: 100%; +} +.cocos-navbar ul li { + margin-right: 1px; + position: relative; + float: left; +} +.cocos-navbar ul li a:hover + .hovershow, +.cocos-navbar ul li .hovershow:hover, +.cocos-navbar ul li .hovershow li:hover { + display: block; +} +.cocos-navbar ul li img { + margin-top: 3px; +} +.cocos-navbar ul li ul { + margin: -3px 0 0 0; + background: #f5f5f5; + display: none; + width: 100%; + z-index: 999; + position: absolute; +} +.cocos-navbar ul li ul li { + display: block; + float: none; +} +.cocos-navbar ul li ul li a { + width: auto; + min-width: 100px; + padding: 0 20px; +} +@media screen and (max-width: 800px) { + .cocos-navbar ul li { + /*Make dropdown links appear inline*/ + /*Create vertical spacing*/ + /*Make all menu links full width*/ + } + .cocos-navbar ul li ul { + position: static; + display: none; + } + .cocos-navbar ul li li { + margin-bottom: 1px; + } + .cocos-navbar ul li ul li, + .cocos-navbar ul li li a { + width: 100%; + } +} +.cocos-navbar .btn { + display: inline-block; + height: 40px; + padding: 0px 15px; + border-bottom: none; + color: #aaa; + text-transform: none; + line-height: 40px; + position: relative; + font-size: 14px; +} +.cocos-navbar .btn:hover { + position: relative; + text-decoration: none; + color: #444; + background: none; +} +.cocos-navbar .btn:focus { + outline: none; +} +.cocos-navbar:hover h1 { + opacity: 1; +} +@media (max-width: 800px) { + .cocos-navbar { + display: none; + } +} +@media (min-width: 800px) { + .page-wrapper { + min-width: 800px; + padding-top: 40px; + } +} +@media (max-width: 800px) { + .page-wrapper { + min-width: 800px; + padding-top: 0; + } +} +@media (min-width: 800px) and (max-width: 1000px) { + .cocos-navbar ul .version-link ul { + margin-left: 20px; + } + .cocos-navbar .nav { + list-style-type: none; + margin: 0; + padding: 0; + width: 100%; + } +} +@media (max-width: 1240px) { + .book-body .body-inner { + overflow: visible; + } +} +.cocos-navbar ul .version-link { + float: right; + z-index: 10; + padding-right: 15px; + text-align: right; + min-width: 180px; + display: flex; + align-items: center; +} +.version-link > ul { + top: 100%; + right: 0; + width: auto !important; + min-width: 180px; +} +.version-link ul li .btn { + padding-right: 35px; + padding-left: 0; +} +.version-link a span { + padding: 0; +} + +.version-badge { + display: inline-flex; + align-items: center; + + color: #0C090A; + background-color: #cccccc; + + height: 20px; + padding: 0 8px; + border-radius: 20px; +} + +.book-header { + min-width: 800px; +} diff --git a/versions/4.0/en/styles/website.less b/versions/4.0/en/styles/website.less new file mode 100644 index 0000000000..4f039a2ead --- /dev/null +++ b/versions/4.0/en/styles/website.less @@ -0,0 +1,131 @@ +// Header +@header-height: 40px; +@header-color: hsl(194, 5%, 52%); +@header-background: #f5f5f5; +@header-border: rgba(0, 0, 0, 0.07); +@header-button-color: #aaa; +@header-button-hover-color: #444; +@header-button-hover-background: none; +// Font sizes +@font-size-base: 14px; +@font-size-large: ceil(@font-size-base * 1.25); // ~18px +@font-size-small: ceil(@font-size-base * 0.85); // ~12px +@line-height-base: 1.428571429; // 20/14 +@line-height-computed: floor(@font-size-base * @line-height-base); + +.navigation { + top: 90px; +} + +.book-summary nav.autoshow { + a, a:hover { + color: inherit; + text-decoration: none; + } + @media (min-width: 800px) { + display: none; + } +} + +.cocos-navbar { + overflow: visible; + height: @header-height; + padding: 0px 8px; + z-index: 2; + + font-size: 0.85em; + color: @header-color; + background: @header-background; + + display: flex; + + a, a:hover { + color: inherit; + text-decoration: none; + } + + ul { + list-style-type: none; + margin: 0; + padding: 0; + position: absolute; + li { + margin-right: 1px; + position: relative; + float: left; + a:hover + .hovershow, .hovershow:hover, .hovershow li:hover { + display:block; + } + img { + margin-top: 3px; + } + ul { + margin: -3px 0 0 0; + background: #f5f5f5; + display:none; + width: 100%; + z-index:999; + li { + display: block; + float: none; + a { + width: auto; + min-width: 100px; + padding: 0 20px; + + } + } + } + @media screen and (max-width : 800px){ + /*Make dropdown links appear inline*/ + ul { + position: static; + display: none; + } + /*Create vertical spacing*/ + li { + margin-bottom: 1px; + } + /*Make all menu links full width*/ + ul li, li a { + width: 100%; + } + } + } + } + + .btn { + display: inline-block; + height: @header-height; + padding: 0px 15px; + border-bottom: none; + color: @header-button-color; + text-transform: none; + line-height: @header-height; + position:relative; + font-size: @font-size-base; + + &:hover { + position: relative; + text-decoration: none; + color: @header-button-hover-color; + background: @header-button-hover-background; + } + + &:focus { + outline: none; + } + } + + .autohide{ + @media (max-width: 800px) { + display: none; + } + } + + &:hover { + h1 { + opacity: 1; + } + } +} \ No newline at end of file diff --git a/versions/4.0/en/submit-pr/submit-pr.md b/versions/4.0/en/submit-pr/submit-pr.md new file mode 100644 index 0000000000..f17472d9de --- /dev/null +++ b/versions/4.0/en/submit-pr/submit-pr.md @@ -0,0 +1,140 @@ +# How to Submit Code to Cocos Engine Repository + +Cocos Creator is an open-source engine, and its examples and documentation are also open-source. When you find areas where the engine, documentation, or examples can be improved during your game development process, simply suggesting it to the official team may not guarantee immediate follow-up due to limited resources. Therefore, we welcome all users to actively submit pull requests (PRs) to help make Cocos better. Found a bug in the engine? Submit a PR! Examples don't look appealing? Submit a PR! API comments unclear? Submit a PR! Typos in the documentation? Submit a PR! Want to contribute your valuable modifications to the game community? Submit a PR! The following repositories are currently the commonly used open-source repositories by the official team, and you can submit PRs to these repositories. + +- **cocos/cocos-engine**:[GitHub](https://github.com/cocos/cocos4/) + +Now let's take a look at how to submit code to Cocos on GitHub, starting from scratch. + +## Register a GitHub Account + +Open the [GitHub website](https://github.com/) and register an account. If you already have an account, simply log in. + +## Environment Setup + +### Install Git + +First, make sure that Git is installed on your computer. Enter `git` in the command line, and if Git is installed, it will display the following content: + +![git](submit-pr/git.png) + +If it's not installed, you can [**Download Git**](https://git-scm.com/download/) and install it. Keep all the installation options as default and click "Next" until the installation is complete. + +### Install Git Client —— GitKraken + +Generally, you can use git command line or any Git client tools to operate git repositories. Such as GitKraken Source Tree, GitHub Desktop and so on. + +Because GitKraken is a commonly used Git client tool. Let's take it as an example. + +The following example demonstrates the installation process using the Windows version. + +First, download [GitKraken](https://www.gitkraken.com/) and extract the files, then proceed with the installation. + +![GitKraken install](submit-pr/sourcetree_install.png) + +After the installation is complete, the interface will look like the following: + +![GitKraken install complete](submit-pr/sourcetree_install_complete.png) + +## Fork the Project + +Let's take the documentation repository **creator-docs** ([GitHub](https://github.com/cocos/cocos-docs)) as an example. Go to the documentation repository page and click the "Fork" button in the upper right corner, as shown below: + +![fork](submit-pr/fork.png) + +After forking, you will be automatically redirected to your GitHub repository page, where you can see that a copy of the docs-3d project has been created, as shown below: + +![repository](submit-pr/repository.png) + +## Clone the Remote Repository to Local + +1. First, go to your remote repository and copy the remote repository URL, as shown in the image below: + +![copy](submit-pr/copy.png) + +2、Switch to GitKraken and click the **Clone a Repo** button at the top to go to the Clone page. Paste the **remote repository URL** you copied earlier and fill in the relevant configurations. If you want the local folder name to be the same as the project name, add `/docs-3d` after the local storage path. Once the configuration is complete, click **Clone**. + +![clone repository](submit-pr/clone_repository.png) + +After cloning, the "docs-3d" folder will be automatically created locally, and the project will open in GitKraken. + +![clone finish](submit-pr/clone_finish.png) + +## Upload Local Modifications to Remote Repository + +1: Check out the desired branch. In the **REMOTE** directory on the left side, you will find an `origin` repository, which is your own remote repository. For example, if you want to modify the next branch, double-click the next branch after clicking `origin`, as shown below: + +![checkout](submit-pr/checkout.png) + +the project name, add "/docs-3d" after the local storage path. Once the configuration is complete, click Clone. + +clone repository + +After cloning, the "docs-3d" folder will be automatically created locally, and the project will open in GitKraken. + +clone finish + +Upload Local Modifications to Remote Repository +Check out the desired branch. In the REMOTE directory on the left side, you will find an origin repository, which is your own remote repository. For example, if you want to modify the next branch, double-click the next branch after clicking origin, as shown below: +checkout + +**Note:** Depending on the version, you may need to switch to different branches, such as: + +- **vX.Y** branch: Corresponds to the branch used for version X.Y. +- **develop** branch: Development branch. + +2: Make the necessary modifications in the local "docs-3d" project. After making the changes, view the repository details, as shown below: + +![modification](submit-pr/modification.png) + +3: Commit the staged files to the local repository. Submit the modified files you want to upload to the local staging area and provide a commit message in the lower section. Once done, click **Commit** to submit the staged files to the local repository. Then, click **Push** to push the changes to your remote repository. The steps are shown in the following image: +![commit changes](submit-pr/commit_changes.png) + +4: Push the changes from your local repository to your remote repository (origin). + +![push](submit-pr/push.png) + +5: After completion, go to your GitHub repository for "docs-3d" (you can access it from **Profile -> Repositories -> docs-3d** under the avatar in the upper right corner). You will see the commit message for this submission. Click **New pull request**. + +![push finish](submit-pr/push_finish.png) + +6: Clicking **New pull request** will redirect you to the **Open a pull request** page in the official repository. The title will be automatically filled with the submitted information, but you can modify it if needed. In the area below, provide additional information about the submission. Specifically, for modifications related to the engine itself, please provide a complete description of the issue, changes made, affected versions, relevant platforms, etc. If there are related issues or forum addresses, you can include them as well. At the bottom, you can see the detailed changes for this PR. Once everything is filled out, click **Create pull request**. + +![pull request](submit-pr/pull_request.png) + +7: After creation, you can see the newly created PR in the pull requests page of the official repository "docs-3d". The official team will receive a notification and assign the PR to the relevant individuals for review and merging. This completes the creation of the PR. If the official team approves the review, your changes will be merged into the official repository. If the Open icon above changes to the Merged icon, it indicates that the changes have been merged into the official repository. If necessary, the official team may also initiate further discussions on the PR. Please pay attention to GitHub notifications or follow the PR page to avoid missing any discussions. If you need to modify the content of the PR submission, repeat steps 2, 3, and 4 in the **Upload Local Modifications to Remote Repository** section. + +## Sync Remote Repository + +If it has been a long time since you last cloned the repository, it is recommended to fetch the latest changes from the official repository before submitting a pull request to avoid conflicts with other people's modifications. + +1. Click on the REMOTE + in the left sidebar of GitKraken, as shown below: +![add upstream](submit-pr/add-upstream.png) + +2. In the left sidebar of GitKraken, under the local repository "docs-3d," you will see two remote repositories: "origin" and "cocos-creator3D." "origin" represents your own remote repository, while "cocos-creator3D" represents the official repository. Under REMOTES -> cocos-creator3D, you can see various branches of the official repository, as shown in the image below: +![upstream](submit-pr/upstream.png) + +3. Fetch the latest updates from the official repository. Switch to the branch you want to fetch and click **Pull** in the upper left corner. See the image below for reference: +![pull](submit-pr/pull.png) + +**Note:** Before updating to the latest version, if you are not familiar with Git operations, it is recommended to ensure that no changes have been made to the local Git repository. If there are any changes, it is advisable to revert them manually and then reapply them after the update. + +## How to Provide Feedback on Documentation Issues + +For issues related to the documentation itself, it is recommended to provide feedback through GitHub issues. Let's briefly demonstrate the process. Before submitting an issue, please make sure of the following: + +- Ensure that the documentation version matches the Cocos Creator version. +- Verify that the steps are correct. +- Confirm that it is indeed a documentation issue, such as a code error or an exception encountered while following the documentation steps. + +Once you have confirmed the above points, there are two ways to submit feedback: + +1: Click the **Submit Feedback** button located at the bottom right of the official Creator documentation. This will take you to the issue submission page. + +![issue](submit-pr/issue.png) + +Fill in the required information and click **Submit new issue** to submit the issue. + +2: You can visit the official repository for **creator-docs** ([GitHub](https://github.com/cocos/cocos-docs)). Select **Issue -> New issue** to access the issue submission page. Fill in the details and submit the issue. + +That concludes the tutorial on submitting pull requests and issues. If there is anything you do not understand or if you find any mistakes, please provide feedback following the steps mentioned above. diff --git a/versions/4.0/en/submit-pr/submit-pr/PR.png b/versions/4.0/en/submit-pr/submit-pr/PR.png new file mode 100644 index 0000000000..789073bb52 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/PR.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/add-upstream.png b/versions/4.0/en/submit-pr/submit-pr/add-upstream.png new file mode 100644 index 0000000000..34c1f7a178 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/add-upstream.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/add_ssh_key.png b/versions/4.0/en/submit-pr/submit-pr/add_ssh_key.png new file mode 100644 index 0000000000..25bf9d733a Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/add_ssh_key.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/add_upstream.png b/versions/4.0/en/submit-pr/submit-pr/add_upstream.png new file mode 100644 index 0000000000..8fb03959d0 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/add_upstream.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/check_out_next.png b/versions/4.0/en/submit-pr/submit-pr/check_out_next.png new file mode 100644 index 0000000000..9491209123 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/check_out_next.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/checkout.png b/versions/4.0/en/submit-pr/submit-pr/checkout.png new file mode 100644 index 0000000000..17d6d65368 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/checkout.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/clone.png b/versions/4.0/en/submit-pr/submit-pr/clone.png new file mode 100644 index 0000000000..fbc65da927 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/clone.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/clone_copy.png b/versions/4.0/en/submit-pr/submit-pr/clone_copy.png new file mode 100644 index 0000000000..26b19bf5ca Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/clone_copy.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/clone_finish.png b/versions/4.0/en/submit-pr/submit-pr/clone_finish.png new file mode 100644 index 0000000000..98a7788858 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/clone_finish.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/clone_repository.png b/versions/4.0/en/submit-pr/submit-pr/clone_repository.png new file mode 100644 index 0000000000..e3a0ad6f55 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/clone_repository.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/commit_changes.png b/versions/4.0/en/submit-pr/submit-pr/commit_changes.png new file mode 100644 index 0000000000..93aa2d3ac0 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/commit_changes.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/copy.png b/versions/4.0/en/submit-pr/submit-pr/copy.png new file mode 100644 index 0000000000..4a913d1630 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/copy.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/fork.png b/versions/4.0/en/submit-pr/submit-pr/fork.png new file mode 100644 index 0000000000..5909a7b855 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/fork.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/git.png b/versions/4.0/en/submit-pr/submit-pr/git.png new file mode 100644 index 0000000000..1d8e330d8a Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/git.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/issue.png b/versions/4.0/en/submit-pr/submit-pr/issue.png new file mode 100644 index 0000000000..84e1f5bb0c Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/issue.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/mercurial.png b/versions/4.0/en/submit-pr/submit-pr/mercurial.png new file mode 100644 index 0000000000..a26fb7656f Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/mercurial.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/merge.png b/versions/4.0/en/submit-pr/submit-pr/merge.png new file mode 100644 index 0000000000..460e7fa410 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/merge.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/merge2.png b/versions/4.0/en/submit-pr/submit-pr/merge2.png new file mode 100644 index 0000000000..770429c4b0 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/merge2.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/modification.png b/versions/4.0/en/submit-pr/submit-pr/modification.png new file mode 100644 index 0000000000..142e972aba Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/modification.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/next.png b/versions/4.0/en/submit-pr/submit-pr/next.png new file mode 100644 index 0000000000..029a8f2c7e Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/next.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/output.png b/versions/4.0/en/submit-pr/submit-pr/output.png new file mode 100644 index 0000000000..0689432a06 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/output.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/paste_ssh_key.png b/versions/4.0/en/submit-pr/submit-pr/paste_ssh_key.png new file mode 100644 index 0000000000..770653adab Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/paste_ssh_key.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/pull.png b/versions/4.0/en/submit-pr/submit-pr/pull.png new file mode 100644 index 0000000000..f2379f2a66 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/pull.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/pull_request.png b/versions/4.0/en/submit-pr/submit-pr/pull_request.png new file mode 100644 index 0000000000..65f4f5ada3 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/pull_request.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/pull_upstream.png b/versions/4.0/en/submit-pr/submit-pr/pull_upstream.png new file mode 100644 index 0000000000..37780e9890 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/pull_upstream.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/push.png b/versions/4.0/en/submit-pr/submit-pr/push.png new file mode 100644 index 0000000000..021d8b3e64 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/push.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/push_finish.png b/versions/4.0/en/submit-pr/submit-pr/push_finish.png new file mode 100644 index 0000000000..6ecac6670b Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/push_finish.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/repository.png b/versions/4.0/en/submit-pr/submit-pr/repository.png new file mode 100644 index 0000000000..c6471994ca Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/repository.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/setting_add.png b/versions/4.0/en/submit-pr/submit-pr/setting_add.png new file mode 100644 index 0000000000..01df90c8ac Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/setting_add.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/setting_repository.png b/versions/4.0/en/submit-pr/submit-pr/setting_repository.png new file mode 100644 index 0000000000..809a476364 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/setting_repository.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/sign_up_step.png b/versions/4.0/en/submit-pr/submit-pr/sign_up_step.png new file mode 100644 index 0000000000..022492690c Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/sign_up_step.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/sourcetree.png b/versions/4.0/en/submit-pr/submit-pr/sourcetree.png new file mode 100644 index 0000000000..ff84e7c0d7 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/sourcetree.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/sourcetree1.png b/versions/4.0/en/submit-pr/submit-pr/sourcetree1.png new file mode 100644 index 0000000000..345db11d2c Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/sourcetree1.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/sourcetree2.png b/versions/4.0/en/submit-pr/submit-pr/sourcetree2.png new file mode 100644 index 0000000000..4b0646cdb3 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/sourcetree2.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/sourcetree_configuration.png b/versions/4.0/en/submit-pr/submit-pr/sourcetree_configuration.png new file mode 100644 index 0000000000..74e450f676 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/sourcetree_configuration.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/sourcetree_install.png b/versions/4.0/en/submit-pr/submit-pr/sourcetree_install.png new file mode 100644 index 0000000000..388b93631b Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/sourcetree_install.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/sourcetree_install_complete.png b/versions/4.0/en/submit-pr/submit-pr/sourcetree_install_complete.png new file mode 100644 index 0000000000..ebfdd364be Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/sourcetree_install_complete.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/sourcetree_verify.png b/versions/4.0/en/submit-pr/submit-pr/sourcetree_verify.png new file mode 100644 index 0000000000..6c98b9c67d Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/sourcetree_verify.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/ssh_key.png b/versions/4.0/en/submit-pr/submit-pr/ssh_key.png new file mode 100644 index 0000000000..f4d567a25d Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/ssh_key.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/upstream.png b/versions/4.0/en/submit-pr/submit-pr/upstream.png new file mode 100644 index 0000000000..c7525d74bd Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/upstream.png differ diff --git a/versions/4.0/en/submit-pr/submit-pr/verification.png b/versions/4.0/en/submit-pr/submit-pr/verification.png new file mode 100644 index 0000000000..abec866260 Binary files /dev/null and b/versions/4.0/en/submit-pr/submit-pr/verification.png differ diff --git a/versions/4.0/en/summary.json b/versions/4.0/en/summary.json new file mode 100644 index 0000000000..8339aee8c8 --- /dev/null +++ b/versions/4.0/en/summary.json @@ -0,0 +1,2205 @@ +[ + { + "text": "User Manual 4.0 (LTS)", + "collapsed": false, + "items": [ + { + "text": "Cocos Creator 4.0 LTS", + "link": "index.md" + }, + { + "text": "About Cocos Creator", + "link": "getting-started/introduction/index.md" + }, + { + "text": "Support", + "link": "getting-started/support.md" + } + ] + }, + { + "text": "Understanding the Basics", + "collapsed": false, + "items": [ + { + "text": "Getting Started", + "link": "getting-started/index.md", + "items": [ + { + "text": "Install and Launch", + "link": "getting-started/install/index.md" + }, + { + "text": "Dashboard", + "link": "getting-started/dashboard/index.md" + }, + { + "text": "Hello World!", + "link": "getting-started/helloworld/index.md" + }, + { + "text": "Project Structure", + "link": "getting-started/project-structure/index.md" + } + ], + "collapsed": true + }, + { + "text": "Editor Interfaces", + "link": "editor/index.md", + "items": [ + { + "text": "Scene", + "link": "editor/scene/index.md" + }, + { + "text": "Hierarchy", + "link": "editor/hierarchy/index.md" + }, + { + "text": "Assets", + "link": "editor/assets/index.md" + }, + { + "text": "Inspector", + "link": "editor/inspector/index.md" + }, + { + "text": "Console", + "link": "editor/console/index.md" + }, + { + "text": "Preferences", + "link": "editor/preferences/index.md" + }, + { + "text": "Project Settings", + "link": "editor/project/index.md" + }, + { + "text": "Main Menu", + "link": "editor/mainMenu/index.md" + }, + { + "text": "Tool Bar", + "link": "editor/toolbar/index.md" + }, + { + "text": "Editor Layout", + "link": "editor/editor-layout/index.md" + }, + { + "text": "Preview & Debugging", + "link": "editor/preview/index.md" + } + ], + "collapsed": true + }, + { + "text": "Glossary", + "link": "glossary/index.md" + }, + { + "text": "Cocos Creator Quick Guide for Unity Developers", + "link": "guide/unity/index.md" + } + ] + }, + { + "text": "Tutorials", + "collapsed": false, + "items": [ + { + "text": "Quick Start: Making Your First 2D Game", + "link": "getting-started/first-game-2d/index.md", + "items": [ + { + "text": "Handle Touch Events", + "link": "getting-started/first-game-2d/touch.md" + } + ], + "collapsed": true + }, + { + "text": "Quick Start: Make Your First 3D Game", + "link": "getting-started/first-game/index.md", + "items": [ + { + "text": "Advanced: Add Light, Shadow and Skeleton Animation", + "link": "getting-started/first-game/advance.md" + } + ], + "collapsed": true + }, + { + "text": "Examples & Tutorials", + "link": "cases-and-tutorials/index.md" + } + ] + }, + { + "text": "Basic Workflow", + "collapsed": false, + "items": [ + { + "text": "Upgrade Guide", + "link": "release-notes/index.md", + "items": [ + { + "text": "Cocos Creator 3.0 Upgrade Guide", + "link": "release-notes/upgrade-guide-v3.0.md" + }, + { + "text": "Cocos Creator 3.0 Material Upgrade Guide", + "link": "material-system/effect-2.x-to-3.0.md" + }, + { + "text": "Cocos Creator 3.1 Material Upgrade Guide", + "link": "material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md" + }, + { + "text": "Cocos Creator 3.5 Material Upgrade Guide", + "link": "material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md" + }, + { + "text": "Cocos Creator 3.5 Native Built Project Upgrade Guide", + "link": "engine/template/native-upgrade-to-v3.5.md" + }, + { + "text": "Cocos Creator 3.6 Native Built Project Upgrade Guide", + "link": "engine/template/native-upgrade-to-v3.6.md" + }, + { + "text": "Cocos Creator 3.6.0 Build Template and settings.json Upgrade Guide", + "link": "release-notes/build-template-settings-upgrade-guide-v3.6.md" + }, + { + "text": "Upgrade Guide: Effect from v3.5.x to v3.6.0", + "link": "material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md" + }, + { + "text": "Upgrade Guide: Particle from v3.5.x to v3.6.0", + "link": "particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md" + } + ], + "collapsed": true + }, + { + "text": "Scene Creation", + "link": "concepts/scene/index.md", + "items": [ + { + "text": "Scene Assets", + "link": "asset/scene.md" + }, + { + "text": "Nodes and Components", + "link": "concepts/scene/node-component.md" + }, + { + "text": "Coordinate Systems and Transformations", + "link": "concepts/scene/coord.md" + }, + { + "text": "Node Hierarchy and Rendering Order", + "link": "concepts/scene/node-tree.md" + }, + { + "text": "Build a Scene Image Using the Scene Panel", + "link": "concepts/scene/scene-editing.md" + }, + { + "text": "Level of Details", + "link": "editor/rendering/lod.md" + } + ], + "collapsed": true + }, + { + "text": "Assets", + "link": "asset/index.md", + "items": [ + { + "text": "Asset Workflow", + "link": "asset/asset-workflow.md" + }, + { + "text": "Images", + "link": "asset/image.md", + "items": [ + { + "text": "Textures", + "link": "asset/texture.md" + }, + { + "text": "Sprite Frames", + "link": "asset/sprite-frame.md", + "items": [ + { + "text": "Auto Trim for SpriteFrame", + "link": "ui-system/components/engine/trim.md" + } + ], + "collapsed": true + }, + { + "text": "Texture Compression", + "link": "asset/compress-texture.md" + }, + { + "text": "Texture Cube", + "link": "asset/texture-cube.md" + }, + { + "text": "Atlas", + "link": "asset/atlas.md" + }, + { + "text": "Auto Atlas", + "link": "asset/auto-atlas.md" + }, + { + "text": "Label Atlas", + "link": "asset/label-atlas.md" + } + ], + "collapsed": true + }, + { + "text": "Prefab", + "link": "asset/prefab.md" + }, + { + "text": "Fonts", + "link": "asset/font.md" + }, + { + "text": "Audio", + "link": "asset/audio.md" + }, + { + "text": "Material", + "link": "asset/material.md", + "items": [ + { + "text": "FBX Smart Material Conversion", + "link": "importer/materials/fbx-materials.md" + } + ], + "collapsed": true + }, + { + "text": "Model", + "link": "asset/model/mesh.md", + "items": [ + { + "text": "Importing Models Exported from DCC Tools", + "link": "asset/model/dcc-export-mesh.md" + }, + { + "text": "Export FBX file from 3ds Max", + "link": "asset/model/max-export-fbx.md" + }, + { + "text": "Export FBX file from Maya", + "link": "asset/model/maya-export-fbx.md" + }, + { + "text": "glTF", + "link": "asset/model/glTF.md" + }, + { + "text": "Programmatically Create Meshes", + "link": "asset/model/scripting-mesh.md" + } + ], + "collapsed": true + }, + { + "text": "Spine Skeletal Animation", + "link": "asset/spine.md" + }, + { + "text": "DragonBones Skeletal Animation", + "link": "asset/dragonbones.md" + }, + { + "text": "TiledMap", + "link": "asset/tiledmap.md" + } + ], + "collapsed": true + }, + { + "text": "Scripting Guide and Event System", + "link": "scripting/index.md", + "items": [ + { + "text": "Programming Language Support", + "link": "scripting/language-support.md" + }, + { + "text": "Scripting Basics", + "link": "scripting/script-basics.md", + "items": [ + { + "text": "Script Creation", + "link": "scripting/setup.md" + }, + { + "text": "Coding Environment Setup", + "link": "scripting/coding-setup.md" + }, + { + "text": "Operating Environment", + "link": "scripting/basic.md" + }, + { + "text": "Decorator", + "link": "scripting/decorator.md" + }, + { + "text": "Property Attributes", + "link": "scripting/reference/attributes.md" + }, + { + "text": "Life Cycle Callbacks", + "link": "scripting/life-cycle-callbacks.md" + }, + { + "text": "Development Notes", + "link": "scripting/readonly.md" + } + ], + "collapsed": true + }, + { + "text": "Using Scripts", + "link": "scripting/usage.md", + "items": [ + { + "text": "Access Nodes and Components", + "link": "scripting/access-node-component.md" + }, + { + "text": "Common Node and Component Interfaces", + "link": "scripting/basic-node-api.md" + }, + { + "text": "Create and Destroy Nodes", + "link": "scripting/create-destroy.md" + }, + { + "text": "Scheduler", + "link": "scripting/scheduler.md" + }, + { + "text": "Components and Component Execution Order", + "link": "scripting/component.md" + }, + { + "text": "Loading and Switching Scenes", + "link": "scripting/scene-managing.md" + }, + { + "text": "Obtaining and Loading Assets", + "link": "scripting/load-assets.md" + }, + { + "text": "tsconfig Configuration", + "link": "scripting/tsconfig.md" + } + ], + "collapsed": true + }, + { + "text": "Advanced Scripting", + "link": "scripting/reference-class.md" + }, + { + "text": "Events", + "link": "engine/event/index.md", + "items": [ + { + "text": "Listening to and Launching Events", + "link": "engine/event/event-emit.md" + }, + { + "text": "Input Event System", + "link": "engine/event/event-input.md" + }, + { + "text": "Node Event System", + "link": "engine/event/event-node.md" + }, + { + "text": "Screen Event System", + "link": "engine/event/event-screen.md" + }, + { + "text": "Event API", + "link": "engine/event/event-api.md" + } + ], + "collapsed": true + }, + { + "text": "Modules", + "link": "scripting/modules/index.md", + "items": [ + { + "text": "Engine Modules", + "link": "scripting/modules/engine.md" + }, + { + "text": "External Module Usage Case", + "link": "scripting/modules/example.md" + }, + { + "text": "Module Specification", + "link": "scripting/modules/spec.md" + }, + { + "text": "Import Maps", + "link": "scripting/modules/import-map.md" + } + ], + "collapsed": true + }, + { + "text": "Plugin Scripts", + "link": "scripting/external-scripts.md" + } + ], + "collapsed": true + }, + { + "text": "Cross-platform Publishing", + "link": "editor/publish/index.md", + "items": [ + { + "text": "General Build Options", + "link": "editor/publish/build-options.md" + }, + { + "text": "Fundamentals for Publishing to Native Platforms", + "link": "editor/publish/publish-native-index.md", + "items": [ + { + "text": "General Native Build Options", + "link": "editor/publish/native-options.md" + }, + { + "text": "Setting up Native Development Environment", + "link": "editor/publish/setup-native-development.md" + }, + { + "text": "Debugging JavaScript on Native Platforms", + "link": "editor/publish/debug-jsb.md" + }, + { + "text": "Integrating Input SDK", + "link": "editor/publish/gpg-input-sdk.md" + } + ], + "collapsed": true + }, + { + "text": "Publishing Android Apps", + "link": "editor/publish/android/index.md", + "items": [ + { + "text": "Android Publishing Example", + "link": "editor/publish/android/build-example-android.md" + }, + { + "text": "Build Options - Android", + "link": "editor/publish/android/build-options-android.md" + }, + { + "text": "Upgrading Android Project to v3.8", + "link": "release-notes/upgrade-3.8-android.md" + } + ], + "collapsed": true + }, + { + "text": "Publish to Google Play on PC", + "link": "editor/publish/google-play-games/index.md", + "items": [ + { + "text": "Build and Run", + "link": "editor/publish/google-play-games/build-and-run.md" + }, + { + "text": "Integrating Input SDK", + "link": "editor/publish/gpg-input-sdk.md" + } + ], + "collapsed": true + }, + { + "text": "Publishing iOS Apps", + "link": "editor/publish/ios/index.md", + "items": [ + { + "text": "iOS Publishing Example", + "link": "editor/publish/ios/build-example-ios.md" + }, + { + "text": "Build Options - iOS", + "link": "editor/publish/ios/build-options-ios.md" + } + ], + "collapsed": true + }, + { + "text": "Publish to HuaWei AppGallery Connect", + "link": "editor/publish/publish-huawei-agc.md" + }, + { + "text": "Publishing Huawei HarmonyOS Apps", + "link": "editor/publish/publish-huawei-ohos.md" + }, + { + "text": "Publishing macOS Desktop Application", + "link": "editor/publish/mac/index.md", + "items": [ + { + "text": "macOS Publishing Example", + "link": "editor/publish/mac/build-example-mac.md" + }, + { + "text": "Build Options - macOS", + "link": "editor/publish/mac/build-options-mac.md" + } + ], + "collapsed": true + }, + { + "text": "Publishing Windows Desktop Application", + "link": "editor/publish/windows/index.md", + "items": [ + { + "text": "Windows Publishing Example", + "link": "editor/publish/windows/build-example-windows.md" + }, + { + "text": "Build Options - Windows", + "link": "editor/publish/windows/build-options-windows.md" + } + ], + "collapsed": true + }, + { + "text": "Publish to Mini Game Platforms", + "link": "editor/publish/publish-mini-game.md", + "items": [ + { + "text": "Publish to HUAWEI AppGallery Connect", + "link": "editor/publish/publish-huawei-agc.md" + }, + { + "text": "Publish to Alipay Mini Game", + "link": "editor/publish/publish-alipay-mini-game.md" + }, + { + "text": "Publish to Taobao Mini Game", + "link": "editor/publish/publish-taobao-mini-game.md" + }, + { + "text": "Publish to WeChat Mini Games", + "link": "editor/publish/publish-wechatgame.md", + "items": [ + { + "text": "WeChat Mini Games Engine Plugin Instructions", + "link": "editor/publish/wechatgame-plugin.md" + }, + { + "text": "Access to WeChat PC Mini Games", + "link": "editor/publish/publish-pc-wechatgame.md" + } + ], + "collapsed": true + }, + { + "text": "Publish to Douyin Mini Games", + "link": "editor/publish/publish-bytedance-mini-game.md", + "items": [ + { + "text": "Access to Douyin PC Mini Games", + "link": "editor/publish/publish-pc-bytedance.md" + } + ], + "collapsed": true + }, + { + "text": "Publish to Huawei Quick Games", + "link": "editor/publish/publish-huawei-quick-game.md" + }, + { + "text": "Publish to OPPO Mini Games", + "link": "editor/publish/publish-oppo-mini-game.md" + }, + { + "text": "Publish to vivo Mini Games", + "link": "editor/publish/publish-vivo-mini-game.md" + }, + { + "text": "Publish to Honor Mini Games", + "link": "editor/publish/publish-honor-mini-game.md" + }, + { + "text": "Publish to Baidu Mini Games", + "link": "editor/publish/publish-baidu-mini-game.md" + }, + { + "text": "Access to Open Data Context", + "link": "editor/publish/build-open-data-context.md" + }, + { + "text": "Mini Game Subpackage", + "link": "editor/publish/subpackage.md" + }, + { + "text": "Introduction to the Build Process and FAQ", + "link": "editor/publish/build-guide.md" + } + ], + "collapsed": true + }, + { + "text": "Publish to Facebook Instant Games", + "link": "editor/publish/publish-fb-instant-games.md" + }, + { + "text": "Publish to Web Platforms", + "link": "editor/publish/publish-web.md" + }, + { + "text": "Publish from the Command Line", + "link": "editor/publish/publish-in-command-line.md" + }, + { + "text": "Custom Project Build Template", + "link": "editor/publish/custom-project-build-template.md" + }, + { + "text": "Build Process and FAQ", + "link": "editor/publish/build-guide.md" + } + ], + "collapsed": true + } + ] + }, + { + "text": "Function Modules", + "collapsed": false, + "items": [ + { + "text": "Graphics", + "link": "module-map/graphics.md", + "items": [ + { + "text": "Render Pipeline", + "link": "render-pipeline/overview.md", + "items": [ + { + "text": "Built-in Render Pipeline", + "link": "render-pipeline/builtin-pipeline.md" + }, + { + "text": "Custom rendering of pipelines", + "link": "render-pipeline/custom-pipeline.md" + }, + { + "text": "Use Post Process", + "link": "render-pipeline/use-post-process.md" + }, + { + "text": "Custom Post Process", + "link": "render-pipeline/post-process/custom.md" + } + ], + "collapsed": true + }, + { + "text": "Camera", + "link": "editor/components/camera-component.md" + }, + { + "text": "Lighting", + "link": "concepts/scene/light.md", + "items": [ + { + "text": "Physically Based Lighting", + "link": "concepts/scene/light/pbr-lighting.md" + }, + { + "text": "Lights", + "link": "concepts/scene/light/lightType/index.md", + "items": [ + { + "text": "Directional Lights", + "link": "concepts/scene/light/lightType/dir-light.md" + }, + { + "text": "Spherical Lights", + "link": "concepts/scene/light/lightType/sphere-light.md" + }, + { + "text": "Spotlights", + "link": "concepts/scene/light/lightType/spot-light.md" + }, + { + "text": "Ambient Light", + "link": "concepts/scene/light/lightType/ambient.md" + } + ], + "collapsed": true + }, + { + "text": "Additive Per-Pixel Lights", + "link": "concepts/scene/light/additive-per-pixel-lights.md" + }, + { + "text": "Shadows", + "link": "concepts/scene/light/shadow.md" + }, + { + "text": "Imaged Based Lighting", + "link": "concepts/scene/light/probe/index.md", + "items": [ + { + "text": "Lightmapping", + "link": "concepts/scene/light/lightmap.md" + }, + { + "text": "Light Probes", + "link": "concepts/scene/light/probe/light-probe.md", + "items": [ + { + "text": "Light Probe Panel", + "link": "concepts/scene/light/probe/light-probe-panel.md" + } + ], + "collapsed": true + }, + { + "text": "Reflection Probe", + "link": "concepts/scene/light/probe/reflection-probe.md", + "items": [ + { + "text": "Reflection Probe Panel", + "link": "concepts/scene/light/probe/reflection-probe-panel.md" + }, + { + "text": "Reflection Probe Art Workflow", + "link": "concepts/scene/light/probe/reflection-art-workflow.md" + } + ], + "collapsed": true + }, + { + "text": "IBL Example", + "link": "concepts/scene/light/probe/example.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "Meshes", + "link": "module-map/mesh/index.md", + "items": [ + { + "text": "MeshRenderer", + "link": "engine/renderable/model-component.md" + }, + { + "text": "SkinnedMeshRenderer", + "link": "module-map/mesh/skinnedMeshRenderer.md" + }, + { + "text": "SkinnedMeshBatchRenderer", + "link": "module-map/mesh/skinnedMeshBatchRenderer.md" + }, + { + "text": "Importing Models Exported from DCC Tools", + "link": "asset/model/dcc-export-mesh.md" + }, + { + "text": "Export FBX file from 3ds Max", + "link": "asset/model/max-export-fbx.md" + }, + { + "text": "Export FBX file from Maya", + "link": "asset/model/maya-export-fbx.md" + }, + { + "text": "glTF", + "link": "asset/model/glTF.md" + }, + { + "text": "Programmatically Create Meshes", + "link": "asset/model/scripting-mesh.md" + }, + { + "text": "Vertex Animation Texture (VAT)", + "link": "VAT" + } + ], + "collapsed": true + }, + { + "text": "Textures", + "link": "module-map/texture/index.md", + "items": [ + { + "text": "Textures", + "link": "asset/texture.md" + }, + { + "text": "Texture Cube", + "link": "asset/texture-cube.md" + }, + { + "text": "Texture Compression", + "link": "asset/compress-texture.md" + }, + { + "text": "RenderTexture", + "link": "asset/render-texture.md" + } + ], + "collapsed": true + }, + { + "text": "Material System Overview", + "link": "material-system/overview.md", + "items": [ + { + "text": "Programmatic Use of Materials", + "link": "material-system/material-script.md" + }, + { + "text": "Builtin Materials", + "link": "material-system/builtin-material.md" + }, + { + "text": "Material System Classes Diagram", + "link": "material-system/material-structure.md" + } + ], + "collapsed": true + }, + { + "text": "Shader Overview", + "link": "shader/index.md", + "items": [ + { + "text": "Create and Use Shader", + "link": "shader/effect-inspector.md" + }, + { + "text": "Built-in Shaders", + "link": "shader/effect-builtin.md", + "items": [ + { + "text": "Physically Based Rendering - PBR", + "link": "shader/effect-builtin-pbr.md" + }, + { + "text": "Toon Shading", + "link": "shader/effect-builtin-toon.md" + }, + { + "text": "Unlit Shading", + "link": "shader/effect-builtin-unlit.md" + } + ], + "collapsed": true + }, + { + "text": "Syntax", + "link": "shader/effect-syntax.md", + "items": [ + { + "text": "Optional Pass Parameters", + "link": "shader/pass-parameter-list.md" + }, + { + "text": "YAML 101", + "link": "shader/yaml-101.md" + }, + { + "text": "GLSL", + "link": "shader/glsl.md" + }, + { + "text": "Preprocessor Macro Definition", + "link": "shader/macros.md" + }, + { + "text": "Chunk", + "link": "shader/effect-chunk-index.md" + } + ], + "collapsed": true + }, + { + "text": "Built-in Uniforms", + "link": "shader/uniform.md" + }, + { + "text": "Common Functions", + "link": "shader/common-functions.md" + }, + { + "text": "Render Flow of Forward Rendering and Deferred Shading", + "link": "shader/forward-and-deferred.md" + }, + { + "text": "Surface Shader", + "link": "shader/surface-shader.md", + "items": [ + { + "text": "Guide to Built-in Surface Shader", + "link": "shader/surface-shader/builtin-surface-shader.md" + }, + { + "text": "Surface Shader Overview", + "link": "shader/surface-shader/surface-shader-structure.md" + }, + { + "text": "Surface Shader Execution Flow", + "link": "shader/surface-shader/shader-code-flow.md" + }, + { + "text": "Include", + "link": "shader/surface-shader/includes.md" + }, + { + "text": "Macro Remapping", + "link": "shader/surface-shader/macro-remapping.md" + }, + { + "text": "Function Replacement Using Macros", + "link": "shader/surface-shader/function-replace.md" + }, + { + "text": "Surface Shader Built-in Replaceable Functions", + "link": "shader/surface-shader/surface-function.md" + }, + { + "text": "Render Usages", + "link": "shader/surface-shader/render-usage.md" + }, + { + "text": "Lighting Models", + "link": "shader/surface-shader/lighting-mode.md" + }, + { + "text": "Surface Material Data Structure", + "link": "shader/surface-shader/surface-data-struct.md" + }, + { + "text": "Shader Stages", + "link": "shader/surface-shader/shader-stage.md" + }, + { + "text": "Shader Assembly", + "link": "shader/surface-shader/shader-assembly.md" + }, + { + "text": "VS Inputs", + "link": "shader/surface-shader/vs-input.md" + }, + { + "text": "FS Inputs", + "link": "shader/surface-shader/fs-input.md" + }, + { + "text": "Customize Surface Shader", + "link": "shader/surface-shader/customize-surface-shader.md" + }, + { + "text": "Rendering Debug View", + "link": "shader/surface-shader/rendering-debug-view.md" + } + ], + "collapsed": true + }, + { + "text": "Legacy Shader", + "link": "shader/legacy-shader/legacy-shader.md", + "items": [ + { + "text": "Guide to Built-in Legacy Shaders", + "link": "shader/legacy-shader/legacy-shader-builtins.md" + }, + { + "text": "Legacy Shader Key Functions and Structures", + "link": "shader/legacy-shader/legacy-shader-func-struct.md" + } + ], + "collapsed": true + }, + { + "text": "Write Shaders", + "link": "shader/write-effect-overview.md", + "items": [ + { + "text": "2D Sprite Shader: Gradient", + "link": "shader/write-effect-2d-sprite-gradient.md" + }, + { + "text": "3D Shader: RimLight", + "link": "write-effect-3d-rim-light.md" + } + ], + "collapsed": true + }, + { + "text": "Skin material", + "link": "shader/advanced-shader/skin.md" + }, + { + "text": "Instanced Attributes", + "link": "shader/instanced-attributes.md" + }, + { + "text": "UBO Layout", + "link": "shader/ubo-layout.md" + }, + { + "text": "Fallback to WebGL 1.0", + "link": "shader/webgl-100-fallback.md" + }, + { + "text": "VSCode Extension - Cocos Effect", + "link": "shader/vscode-plugin.md" + }, + { + "text": "Compute Shader", + "link": "shader/compute-shader.md" + } + ], + "collapsed": true + }, + { + "text": "2D Rendering Sorting", + "link": "engine/rendering/sorting-2d.md" + }, + { + "text": "3D Rendering Sorting", + "link": "engine/rendering/sorting.md" + }, + { + "text": "Effects", + "link": "module-map/effects/index.md", + "items": [ + { + "text": "Billboard", + "link": "particle-system/billboard-component.md" + }, + { + "text": "Line", + "link": "particle-system/line-component.md" + } + ], + "collapsed": true + }, + { + "text": "Skybox", + "link": "concepts/scene/skybox.md" + }, + { + "text": "Global Fog", + "link": "concepts/scene/fog.md" + }, + { + "text": "Geometry Renderer", + "link": "geometry-renderer/index.md" + } + ], + "collapsed": true + }, + { + "text": "2D Objects", + "link": "2d-object/index.md", + "items": [ + { + "text": "2D Render", + "link": "2d-object/2d-render/index.md", + "items": [ + { + "text": "Rendering Order", + "link": "ui-system/components/engine/priority.md" + }, + { + "text": "2D Renderable Component Batching Rules", + "link": "ui-system/components/engine/ui-batch.md" + }, + { + "text": "Custom Materials for 2D Rendering Objects", + "link": "ui-system/components/engine/ui-material.md" + }, + { + "text": "2D Renderable Components", + "link": "ui-system/components/editor/render-component.md", + "items": [ + { + "text": "Sprite Component Reference", + "link": "ui-system/components/editor/sprite.md" + }, + { + "text": "Label Component Reference", + "link": "ui-system/components/editor/label.md" + }, + { + "text": "Mask Component Reference", + "link": "ui-system/components/editor/mask.md" + }, + { + "text": "Graphics Component Reference", + "link": "ui-system/components/editor/graphics.md" + }, + { + "text": "RichText Component Reference", + "link": "ui-system/components/editor/richtext.md" + }, + { + "text": "UIStaticBatch Component Reference", + "link": "ui-system/components/editor/ui-static.md" + }, + { + "text": "Spine Skeleton Component Reference", + "link": "editor/components/spine.md" + }, + { + "text": "DragonBones ArmatureDisplay Component Reference", + "link": "editor/components/dragonbones.md" + }, + { + "text": "TiledMap Component Reference", + "link": "editor/components/tiledmap.md" + }, + { + "text": "TiledTile Component Reference", + "link": "editor/components/tiledtile.md" + }, + { + "text": "MotionStreak Component Reference", + "link": "editor/components/motion-streak.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "UI System", + "link": "2d-object/ui-system/index.md", + "items": [ + { + "text": "UI Components", + "link": "ui-system/components/editor/base-component.md", + "items": [ + { + "text": "Canvas Component Reference", + "link": "ui-system/components/editor/canvas.md" + }, + { + "text": "UITransform Component Reference", + "link": "ui-system/components/editor/ui-transform.md" + }, + { + "text": "Widget Component Reference", + "link": "ui-system/components/editor/widget.md" + }, + { + "text": "Button Component Reference", + "link": "ui-system/components/editor/button.md" + }, + { + "text": "Layout Component Reference", + "link": "ui-system/components/editor/layout.md" + }, + { + "text": "EditBox Component Reference", + "link": "ui-system/components/editor/editbox.md" + }, + { + "text": "ScrollView Component Reference", + "link": "ui-system/components/editor/scrollview.md" + }, + { + "text": "ScrollBar Component Reference", + "link": "ui-system/components/editor/scrollbar.md" + }, + { + "text": "ProgressBar Component Reference", + "link": "ui-system/components/editor/progress.md" + }, + { + "text": "LabelOutline Component Reference", + "link": "ui-system/components/editor/label-outline.md" + }, + { + "text": "LabelShadow Component Reference", + "link": "ui-system/components/editor/label-shadow.md" + }, + { + "text": "Toggle Component Reference", + "link": "ui-system/components/editor/toggle.md" + }, + { + "text": "ToggleContainer Component Reference", + "link": "ui-system/components/editor/toggleContainer.md" + }, + { + "text": "Slider Component Reference", + "link": "ui-system/components/editor/slider.md" + }, + { + "text": "PageView Component Reference", + "link": "ui-system/components/editor/pageview.md" + }, + { + "text": "PageViewIndicator Component Reference", + "link": "ui-system/components/editor/pageviewindicator.md" + }, + { + "text": "UIMeshRenderer Component Reference", + "link": "ui-system/components/editor/ui-model.md" + }, + { + "text": "UICoordinateTracker Component Reference", + "link": "ui-system/components/editor/ui-coordinate-tracker.md" + }, + { + "text": "UIOpacity Component Reference", + "link": "ui-system/components/editor/ui-opacity.md" + }, + { + "text": "UISkew Component Reference", + "link": "ui-system/components/editor/ui-skew.md" + }, + { + "text": "BlockInputEvents Component Reference", + "link": "ui-system/components/editor/block-input-events.md" + }, + { + "text": "WebView Component Reference", + "link": "ui-system/components/editor/webview.md" + }, + { + "text": "VideoPlayer Component Reference", + "link": "ui-system/components/editor/videoplayer.md" + }, + { + "text": "SafeArea Component Reference", + "link": "ui-system/components/editor/safearea.md" + } + ], + "collapsed": true + }, + { + "text": "UI Practice Guide", + "link": "ui-system/components/engine/usage-ui.md", + "items": [ + { + "text": "Multi-Resolution Adaption", + "link": "ui-system/components/engine/multi-resolution.md" + }, + { + "text": "Widget Alignment", + "link": "ui-system/components/engine/widget-align.md" + }, + { + "text": "Label Layout", + "link": "ui-system/components/engine/label-layout.md" + }, + { + "text": "Auto Layout Container", + "link": "ui-system/components/engine/auto-layout.md" + }, + { + "text": "Create a List of Dynamically Generated Content", + "link": "ui-system/components/engine/list-with-data.md" + }, + { + "text": "Stretchable UI Sprite", + "link": "ui-system/components/engine/sliced-sprite.md" + }, + { + "text": "Android Large Screen Adaptation", + "link": "ui-system/components/engine/large-screen.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "Animation", + "link": "animation/index.md", + "items": [ + { + "text": "Animation Clip", + "link": "animation/animation-clip.md" + }, + { + "text": "Animation Component Reference", + "link": "animation/animation-comp.md" + }, + { + "text": "Animation Panel", + "link": "animation/animation.md", + "items": [ + { + "text": "Creating Animation Components and Animation Clips", + "link": "animation/animation-create.md" + }, + { + "text": "Get Familiar with the Animation Panel", + "link": "animation/animation-editor.md" + }, + { + "text": "Editing Animation Clips", + "link": "animation/edit-animation-clip.md" + }, + { + "text": "Editing Animation Easing Curve", + "link": "animation/animation-curve.md" + }, + { + "text": "Adding Animation Events", + "link": "animation/animation-event.md" + }, + { + "text": "Using Animation Curves", + "link": "animation/use-animation-curve.md" + }, + { + "text": "Curve Editor", + "link": "animation/curve-editor.md" + } + ], + "collapsed": true + }, + { + "text": "Skeletal Animation", + "link": "animation/skeletal-animation.md", + "items": [ + { + "text": "Joint Texture Layout Settings", + "link": "animation/joint-texture-layout.md" + } + ], + "collapsed": true + }, + { + "text": "Controlling Animation with Scripts", + "link": "animation/animation-component.md", + "items": [ + { + "text": "Animation State", + "link": "animation/animation-state.md" + } + ], + "collapsed": true + }, + { + "text": "Embedded Player", + "link": "animation/embedded-player.md" + }, + { + "text": "Marionette Animation System", + "link": "animation/marionette/index.md", + "items": [ + { + "text": "Animation Graph Assets", + "link": "animation/marionette/animation-graph.md" + }, + { + "text": "Animation Controller Reference", + "link": "animation/marionette/animation-controller.md" + }, + { + "text": "Animation Graph Panel", + "link": "animation/marionette/animation-graph-panel.md" + }, + { + "text": "Animation Graph Layer", + "link": "animation/marionette/animation-graph-layer.md" + }, + { + "text": "Animation State Machine", + "link": "animation/marionette/animation-graph-basics.md" + }, + { + "text": "State Transition", + "link": "animation/marionette/state-transition.md" + }, + { + "text": "Animation Mask", + "link": "animation/marionette/animation-mask.md" + }, + { + "text": "Animation Graph Variants", + "link": "animation/marionette/animation-variant.md" + }, + { + "text": "Procedural Animation", + "link": "animation/marionette/procedural-animation/index.md", + "items": [ + { + "text": "Introduction to Procedural Animation", + "link": "animation/marionette/procedural-animation/introduce.md", + "items": [ + { + "text": "Enable Procedural Animation", + "link": "animation/marionette/procedural-animation/enabling.md" + } + ], + "collapsed": true + }, + { + "text": "Pose Graph", + "link": "animation/marionette/procedural-animation/pose-graph/index.md", + "items": [ + { + "text": "Pose Graph View", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md" + }, + { + "text": "Pose Nodes", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md" + }, + { + "text": "Blend Pose", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md" + }, + { + "text": "Modify Poses", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md" + }, + { + "text": "Play or Sample Animation", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "Audio System", + "link": "audio-system/overview.md", + "items": [ + { + "text": "AudioSource Component Reference", + "link": "audio-system/audiosource.md" + }, + { + "text": "AudioMgr Example", + "link": "audio-system/audioExample.md" + }, + { + "text": "Compatibility Notes", + "link": "audio-system/audioLimit.md" + } + ], + "collapsed": true + }, + { + "text": "Physics System", + "link": "physics/index.md", + "items": [ + { + "text": "Physics 2D", + "link": "physics-2d/physics-2d.md", + "items": [ + { + "text": "2D Physics Manager", + "link": "physics-2d/physics-2d-system.md" + }, + { + "text": "2D RigidBody", + "link": "physics-2d/physics-2d-rigid-body.md" + }, + { + "text": "2D Physics Collider", + "link": "physics-2d/physics-2d-collider.md" + }, + { + "text": "2D Contact Callback", + "link": "physics-2d/physics-2d-contact-callback.md" + }, + { + "text": "2D Physics Joint", + "link": "physics-2d/physics-2d-joint.md" + } + ], + "collapsed": true + }, + { + "text": "Physics 3D", + "link": "physics/physics.md", + "items": [ + { + "text": "Physics Engines", + "link": "physics/physics-engine.md" + }, + { + "text": "Physics System Configuration", + "link": "physics/physics-configs.md" + }, + { + "text": "Group and Mask", + "link": "physics/physics-group-mask.md" + }, + { + "text": "Physics Components", + "link": "physics/physics-component.md", + "items": [ + { + "text": "Collider", + "link": "physics/physics-collider.md" + }, + { + "text": "Rigidbody", + "link": "physics/physics-rigidbody.md" + }, + { + "text": "Constant Force", + "link": "physics/physics-constantForce.md" + }, + { + "text": "Constraint", + "link": "physics/physics-constraint.md" + } + ], + "collapsed": true + }, + { + "text": "Physics Material", + "link": "physics/physics-material.md" + }, + { + "text": "Physics Event", + "link": "physics/physics-event.md" + }, + { + "text": "Raycast Detection", + "link": "physics/physics-raycast.md" + }, + { + "text": "Geometry Cast Detection", + "link": "physics/physics-sweep.md" + }, + { + "text": "Continuous Collision Detection", + "link": "physics/physics-ccd.md" + }, + { + "text": "Character Controller", + "link": "physics/character-controller/index.md" + }, + { + "text": "Physics Application Cases", + "link": "physics/physics-example.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "Particle System", + "link": "particle-system/index.md", + "items": [ + { + "text": "2D Particle System", + "link": "particle-system/2d-particle/2d-particle.md" + }, + { + "text": "3D Particle System", + "link": "particle-system/overview.md", + "items": [ + { + "text": "Particle System Module", + "link": "particle-system/module.md", + "items": [ + { + "text": "Main Module", + "link": "particle-system/main-module.md" + }, + { + "text": "Shape Module", + "link": "particle-system/emitter.md" + }, + { + "text": "Velocity Overtime Module", + "link": "particle-system/velocity-module.md" + }, + { + "text": "Force Overtime Module", + "link": "particle-system/force-module.md" + }, + { + "text": "Size Overtime Module", + "link": "particle-system/size-module.md" + }, + { + "text": "Rotation Overtime Module", + "link": "particle-system/rotation-module.md" + }, + { + "text": "Color Over Life Time Module", + "link": "particle-system/color-module.md" + }, + { + "text": "Texture Animation Module", + "link": "particle-system/texture-animation-module.md" + }, + { + "text": "Limit Velocity Overtime Module", + "link": "particle-system/limit-velocity-module.md" + }, + { + "text": "Trail Module", + "link": "particle-system/trail-module.md" + }, + { + "text": "Renderer Module", + "link": "particle-system/renderer.md" + } + ], + "collapsed": true + }, + { + "text": "Particle Properties Editor", + "link": "particle-system/editor/index.md", + "items": [ + { + "text": "Curve Editor", + "link": "particle-system/editor/curve-editor.md" + }, + { + "text": "Gradient Editor", + "link": "particle-system/editor/gradient-editor.md" + }, + { + "text": "Particle Editor", + "link": "particle-system/editor/particle-effect-panel.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "Tween System", + "link": "tween/index.md", + "items": [ + { + "text": "Tween Interface", + "link": "tween/tween-interface.md" + }, + { + "text": "Tween Function", + "link": "tween/tween-function.md" + }, + { + "text": "Tween Examples", + "link": "tween/tween-example.md" + } + ], + "collapsed": true + }, + { + "text": "Terrain System", + "link": "editor/terrain/index.md" + }, + { + "text": "Asset Manager", + "link": "asset/asset-manager.md", + "items": [ + { + "text": "AssetManager Upgrade Guide", + "link": "asset/asset-manager-upgrade-guide.md" + }, + { + "text": "Asset Bundle Upgrade Guide", + "link": "asset/subpackage-upgrade-guide.md" + }, + { + "text": "Asset Loading", + "link": "asset/dynamic-load-resources.md" + }, + { + "text": "Asset Bundle", + "link": "asset/bundle.md" + }, + { + "text": "Release Of Assets", + "link": "asset/release-manager.md" + }, + { + "text": "Download and Parse", + "link": "asset/downloader-parser.md" + }, + { + "text": "Loading and Preloading", + "link": "asset/preload-load.md" + }, + { + "text": "Cache Manager", + "link": "asset/cache-manager.md" + }, + { + "text": "Optional Parameters", + "link": "asset/options.md" + }, + { + "text": "Pipeline and Task", + "link": "asset/pipeline-task.md" + }, + { + "text": "Resource Management Considerations --- meta files", + "link": "asset/meta.md" + } + ], + "collapsed": true + }, + { + "text": "Localization", + "link": "editor/l10n/overview.md", + "items": [ + { + "text": "Translation Service Provider", + "link": "editor/l10n/translation-service.md" + }, + { + "text": "Collect and Count", + "link": "editor/l10n/collect-and-count.md" + }, + { + "text": "Compile Language", + "link": "editor/l10n/compile-language.md" + }, + { + "text": "L10nLabel", + "link": "editor/l10n/l10n-label.md" + }, + { + "text": "Sample", + "link": "editor/l10n/script-using.md" + } + ], + "collapsed": true + }, + { + "text": "XR", + "link": "xr/index.md", + "items": [ + { + "text": "Version History", + "link": "xr/version-history.md" + }, + { + "text": "Architecture", + "link": "xr/architecture/index.md", + "items": [ + { + "text": "Built-in Resources and Prefabs", + "link": "xr/architecture/assets.md" + }, + { + "text": "XR Components", + "link": "xr/architecture/component.md" + }, + { + "text": "XR Preview", + "link": "xr/architecture/preview.md" + }, + { + "text": "XR Video Player", + "link": "xr/architecture/xr-video-player.md" + }, + { + "text": "XR Preview in Browser", + "link": "xr/architecture/xr-webview.md" + }, + { + "text": "XR Spatial Audio", + "link": "xr/architecture/xr-spatial-audio.md" + }, + { + "text": "XR Composition Layer", + "link": "xr/architecture/xr-composition-layer.md" + }, + { + "text": "Passthrough", + "link": "xr/architecture/xr-pass-through.md" + }, + { + "text": "AR", + "link": "xr/architecture/ar-introduce.md", + "items": [ + { + "text": "AR Camera", + "link": "xr/architecture/ar-camera.md" + }, + { + "text": "AR Manager", + "link": "xr/architecture/ar-manager.md" + }, + { + "text": "AR Automated Behavior Editing", + "link": "xr/architecture/ar-tracking-component.md" + }, + { + "text": "AR Interaction", + "link": "xr/architecture/ar-interaction.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "Quick Start", + "link": "xr/project-deploy/index.md", + "items": [ + { + "text": "VR Project Creation", + "link": "xr/project-deploy/vr-proj-deploy.md" + }, + { + "text": "VR Building and Publishing", + "link": "xr/project-deploy/vr-proj-pub.md" + }, + { + "text": "AR Project Creation", + "link": "xr/project-deploy/ar-proj-deploy.md" + }, + { + "text": "AR Building and Publishing", + "link": "xr/project-deploy/ar-proj-pub.md" + }, + { + "text": "WebXR Project Setup", + "link": "xr/project-deploy/webxr-proj-deploy.md" + }, + { + "text": "WebXR Building and Publishing", + "link": "xr/project-deploy/webxr-proj-pub.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "Native Development", + "link": "native/overview.md", + "items": [ + { + "text": "Native Platform Secondary Development Guide", + "link": "advanced-topics/native-secondary-development.md" + }, + { + "text": "JavaScript and Android Communication with Reflection", + "link": "advanced-topics/java-reflection.md" + }, + { + "text": "JavaScript and iOS/macOS Communication with Reflection", + "link": "advanced-topics/oc-reflection.md" + }, + { + "text": "JavaScript and Java Communication using JsbBridge", + "link": "advanced-topics/js-java-bridge.md" + }, + { + "text": "JavaScript and Objective-C Communication using JsbBridge", + "link": "advanced-topics/js-oc-bridge.md" + }, + { + "text": "JsbBridgeWrapper - An Event Mechanism based on JsbBridge", + "link": "advanced-topics/jsb-bridge-wrapper.md" + }, + { + "text": "Tutorial: JSB 2.0", + "link": "advanced-topics/JSB2.0-learning.md", + "items": [ + { + "text": "JSB Manual Binding", + "link": "advanced-topics/jsb-manual-binding.md" + }, + { + "text": "JSB Auto Binding", + "link": "advanced-topics/jsb-auto-binding.md" + }, + { + "text": "Swig", + "link": "advanced-topics/jsb-swig.md" + }, + { + "text": "Swig Tutorial", + "link": "advanced-topics/jsb/swig/tutorial/index.md" + } + ], + "collapsed": true + }, + { + "text": "CMake Usage Introduction", + "link": "advanced-topics/cmake-learning.md" + }, + { + "text": "Native Engine Memory Leak Detection System", + "link": "advanced-topics/memory-leak-detector.md" + }, + { + "text": "Native Scene Culling", + "link": "advanced-topics/native-scene-culling.md" + }, + { + "text": "Native Profiler", + "link": "advanced-topics/profiler.md" + }, + { + "text": "Native Plugins", + "link": "advanced-topics/native-plugins/brief.md", + "items": [ + { + "text": "Cocos Native Plugin Quick Tutorial", + "link": "advanced-topics/native-plugins/tutorial.md" + } + ], + "collapsed": true + }, + { + "text": "Optimization of Cross-Language Invocation", + "link": "advanced-topics/jsb-optimizations.md" + } + ], + "collapsed": true + } + ] + }, + { + "text": "Advanced Tutorials", + "collapsed": false, + "items": [ + { + "text": "Editor Extension", + "link": "editor/extension/readme.md", + "items": [ + { + "text": "Extension Manager", + "link": "editor/extension/extension-manager.md" + }, + { + "text": "Extension Templates and Compile Builds", + "link": "editor/extension/create-extension.md" + }, + { + "text": "Getting Started Example - Menu", + "link": "editor/extension/first.md" + }, + { + "text": "Getting Started Example - Panel", + "link": "editor/extension/first-panel.md" + }, + { + "text": "Getting Started Example - First Data Interaction", + "link": "editor/extension/first-communication.md" + }, + { + "text": "Change the Name of a Extension", + "link": "editor/extension/extension-change-name.md" + }, + { + "text": "Install and Share", + "link": "editor/extension/install.md" + }, + { + "text": "Submitting Resources to Cocos Store", + "link": "editor/extension/store/upload-store.md" + }, + { + "text": "Extend Existing Functionality", + "link": "editor/extension/contributions.md", + "items": [ + { + "text": "Customize the Main Menu", + "link": "editor/extension/contributions-menu.md" + }, + { + "text": "Customized Messages", + "link": "editor/extension/contributions-messages.md" + }, + { + "text": "Calling the Engine API and Project Script", + "link": "editor/extension/scene-script.md" + }, + { + "text": "Extending the Assets Panel", + "link": "editor/assets/extension.md" + }, + { + "text": "Custom Asset Database", + "link": "editor/extension/contributions-database.md" + }, + { + "text": "Custom Inspector Panel", + "link": "editor/extension/inspector.md" + }, + { + "text": "Extending Build Process", + "link": "editor/publish/custom-build-plugin.md" + }, + { + "text": "Extending Project Settings Panel", + "link": "editor/extension/contributions-project.md" + }, + { + "text": "Extending the Preferences Panels", + "link": "editor/extension/contributions-preferences.md" + }, + { + "text": "Extending Shortcut", + "link": "editor/extension/contributions-shortcuts.md" + } + ], + "collapsed": true + }, + { + "text": "Extension Details", + "link": "editor/extension/basic.md", + "items": [ + { + "text": "Extension Infrastructure", + "link": "editor/extension/package.md" + }, + { + "text": "Definition of Extension", + "link": "editor/extension/define.md" + }, + { + "text": "Message System", + "link": "editor/extension/messages.md" + }, + { + "text": "Configuration System", + "link": "editor/extension/profile.md" + }, + { + "text": "Extension Panel", + "link": "editor/extension/panel.md" + }, + { + "text": "UI Components", + "link": "editor/extension/ui.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "Advanced Topics", + "link": "advanced-topics/index.md", + "items": [ + { + "text": "Submit Code to Cocos Engine Repository", + "link": "submit-pr/submit-pr.md" + }, + { + "text": "User Data Storage", + "link": "advanced-topics/data-storage.md" + }, + { + "text": "Custom loading Wasm/Asm files and modules", + "link": "advanced-topics/wasm-asm-load.md" + }, + { + "text": "Converting Native Code to Wasm/Asm Files Using Emscripten", + "link": "advanced-topics/wasm-asm-create.md" + }, + { + "text": "Engine Customization Workflow", + "link": "advanced-topics/engine-customization.md" + }, + { + "text": "Web Preview Customization Workflow", + "link": "editor/preview/browser.md" + }, + { + "text": "Dynamic Atlas", + "link": "advanced-topics/dynamic-atlas.md" + }, + { + "text": "Mangle Engine Internal Properties", + "link": "advanced-topics/mangle-properties.md" + }, + { + "text": "Hot Update Tutorial", + "link": "advanced-topics/hot-update.md" + }, + { + "text": "AssetManager for Hot Update", + "link": "advanced-topics/hot-update-manager.md" + }, + { + "text": "HTTP Request", + "link": "advanced-topics/http.md" + }, + { + "text": "WebSocket Introduction", + "link": "advanced-topics/websocket-introduction.md", + "items": [ + { + "text": "WebSocket Client", + "link": "advanced-topics/websocket.md" + }, + { + "text": "WebSocket Server", + "link": "advanced-topics/websocket-server.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ] + } +] \ No newline at end of file diff --git a/versions/4.0/en/tween/img/homeImgGame.png b/versions/4.0/en/tween/img/homeImgGame.png new file mode 100644 index 0000000000..1e177e1eae Binary files /dev/null and b/versions/4.0/en/tween/img/homeImgGame.png differ diff --git a/versions/4.0/en/tween/img/tweener.png b/versions/4.0/en/tween/img/tweener.png new file mode 100644 index 0000000000..fd693a69f7 Binary files /dev/null and b/versions/4.0/en/tween/img/tweener.png differ diff --git a/versions/4.0/en/tween/img/updateUntil.gif b/versions/4.0/en/tween/img/updateUntil.gif new file mode 100644 index 0000000000..8c28707250 Binary files /dev/null and b/versions/4.0/en/tween/img/updateUntil.gif differ diff --git a/versions/4.0/en/tween/index.md b/versions/4.0/en/tween/index.md new file mode 100644 index 0000000000..886bb5505a --- /dev/null +++ b/versions/4.0/en/tween/index.md @@ -0,0 +1,19 @@ +# Tween System + +![tween-index.png](img/homeImgGame.png) + +Tweens are widely used in game development as an alternative when premade animations are unavailable or unfitting for project needs. + +In Cocos Creator, tweens can be applied to properties such as transform, rotation, scaling and color, as well as adding additional mechanisms such as delaying, queuing and parallelism. + +## Content + +This chapter contains the following sections: + +- [Tween Interface](./tween-interface.md) +- [Tween Function](./tween-function.md) +- [Tween Examples](./tween-example.md) + +## Example + +Please see [GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8.4/assets/cases/tween) for sample code for using tweens. diff --git a/versions/4.0/en/tween/tween-example.md b/versions/4.0/en/tween/tween-example.md new file mode 100644 index 0000000000..3d0b917570 --- /dev/null +++ b/versions/4.0/en/tween/tween-example.md @@ -0,0 +1,1079 @@ +# Tween Example + +This article will focus on some common uses and interfaces in Cosos Creator Tween System. + +## Construct Tween + +Tween can be constructed either by the `tween` method or by using `new Tween(target: T)`. + +> **Note**: 'tween' is a tool method provided by the engine and is not a member of 'Tween' class, please note the distinction. For this, please refer to the interface description: [Tween Interface](tween-interface.md). + +## Chain API + +Most action-related interfaces return either `this` or a new `Tween` object, so it's easy to use chain calls to combine: + +```ts +tween() + .target(this.node) + .to(1.0, { position: new Vec3(0, 10, 0) }) + .by(1.0, { position: new Vec3(0, -10, 0) }) + .delay(1.0) + .by(1.0, { position: new Vec3(0, -10, 0) }) + .start() +``` + +## to, by + +Here is a demonstration of how to use a `to` type of tween to bind the position information of a node and offset its position by 10 units along the Y-axis. + +```ts +let tweenDuration : number = 1.0; // Duration of tween +tween(this.node.position) + .to( tweenDuration, new Vec3(0, 10, 0), { // The interface 'to' represents the absolute value of the node + onUpdate : (target:Vec3, ratio:number)=>{ // Implement ITweenOption's onUpdate callback to accept the current tweening progress + this.node.position = target; // Assign the position of the node to the result calculated by the tween system + } +}).start(); // Call the start method to enable tween +``` + +### Returning Property Target Values through Functions + +> [!TIP] +> +> Supported since v3.8.4 + +```ts +tween(this.node).to(1, { angle: ()=>90 ).start(); +``` + +The above code is equivalent to: + +```ts +tween(this.node).to(1, { angle: 90 ).start(); +``` + +### Custom Interpolation Functions + +> [!TIP] +> +> Supported since v3.8.4 + +- For a specific property of the current action + +```ts +// Simultaneously animate the 'angle' and 'position' properties on node, but only define a custom interpolation function for the 'angle' property +tween(this.node).to(1, { + angle: { + value: 90, + progress(start: number, end: number, current: number, ratio: number): number { + return lerp(start, end, ratio); + }, + }, + position: v3(90, 90, 90), + } +).start(); +``` + +- For all properties of the current action + +To handle custom interpolation functions for both the 'angle' and 'position' properties associated with the 'to' action's node, consider setting the 'opt.progress' parameter. + +```ts +tween(this.node).to(1, { angle: 90, position: v3(90, 90, 90) }, { + progress(start: number, end: number, current: number, ratio: number): number { + return lerp(start, end, ratio); + }, +}).start(); +``` + +### Custom Easing Functions + +> [!TIP] +> +> Supported since v3.8.4 + +- For a specific property of the current action + +```ts +// 1. Use a built-in easing function +tween(this.node).to(1, { + angle: { + value: 90, + easing: 'backIn', + }, + position: v3(90, 90, 90), +}).start(); + +// Or +// 2. Use a custom easing function +tween(this.node).to(1, { + angle: { + value: 90, + easing: (k: number): number => { + if (k === 1) { + return 1; + } + const s = 1.70158; + return k * k * ((s + 1) * k - s); + }, + }, + position: v3(90, 90, 90), +}).start(); +``` + +- For all properties of the current action + +To handle custom easing functions for both the 'angle' and 'position' properties associated with the 'to' action's node, consider setting the 'opt.easing' parameter. + +```ts +// 1. Use a built-in easing function +tween(this.node).to(1, { angle: 90, position: v3(90, 90, 90) }, { + easing: 'backIn', +}).start(); + +// Or +// 2. Use a custom easing function +tween(this.node).to(1, { angle: 90, position: v3(90, 90, 90) }, { + easing: (k: number): number => { + if (k === 1) { + return 1; + } + const s = 1.70158; + return k * k * ((s + 1) * k - s); + }, +}).start(); +``` + +### Easing Strings + +> [!TIP] +> +> Supported since v3.8.4 + +Consider defining a class: + +```ts +class StringTarget { + string = ''; +} +``` + +#### Example 1 (Integer Strings) + +Using the `to` interface, animate the `string` property from '0' to '100' over one second: + +```ts +const t = new StringTarget(); +t.string = '0'; + +// Here, the type of the `string` value can be either a number or a string that can be converted to a number +tween(t).to(1, { string: 100 }).start(); +tween(t).to(1, { string: '100' }).start(); +``` + +#### Example 2 (Floating-Point Strings) + +```ts +const t = new StringTarget(); +t.string = '10'; + +// Animate from 10 to 110, keeping two decimal places throughout the animation +// For example: '10.00' -> '43.33' -> '76.67' -> '110.00' +tween(t).to(1, { string: { value: 110, toFixed: 2 } }).start(); // Note: `value` represents the target value, `toFixed` specifies the number of decimal places +``` + +#### Example 3 (Custom Handling of Multiple String Properties) + +```ts +const o = { + gold: "¥0.00", + exp: '1000/1000', + lv: 'Lv.100', + attack: '100 points', + health: '10.00', +}; + +const tweenFormat = { + currency(value: number): TTweenCustomProperty { + return { + value: `¥${value}`, + progress(start: number, end: number, current: string, ratio: number): string { // Custom progress function + return `¥${lerp(start, end, ratio).toFixed(2)}`; // Keep two decimal places + }, + convert(v: string): number { // Custom conversion callback to convert string to number + return Number(v.slice(1)); // The string starts with '¥' which isn't part of the animation, so slice it off and convert the remaining part to a number + }, + }; + }, + + health(value: number): TTweenCustomProperty { + // `health` is a floating-point number with two decimal places, so no need for a custom progress function; use the built-in progress function + return { + value: `${value}`, + toFixed: 2, // Specify two decimal places; without this, the result would be an integer string + }; + }, + + exp(value: number): TTweenCustomProperty { + return { + value: () => `${value}/1000`, + progress(start: number, end: number, current: string, ratio: number): string { + return `${lerp(start, end, ratio).toFixed(0)}/1000`; + }, + convert(v: string): number { + return Number(v.slice(0, v.indexOf('/'))); + }, + // `exp` is an integer string, so no need to specify the `toFixed` parameter + }; + }, + + lv(value: number): TTweenCustomProperty { + return { + value: `Lv.${value}`, + progress(start: number, end: number, current: string, ratio: number): string { + return `Lv.${lerp(start, end, ratio).toFixed(0)}`; + }, + convert(v: string): number { + return Number(v.slice(v.indexOf('.') + 1)); + }, + }; + }, +}; + +tween(o).to(1, { + gold: tweenFormat.currency(100), + health: tweenFormat.health(1), + exp: tweenFormat.exp(0), + lv: tweenFormat.lv(0), +}).start(); +``` + +### Custom Easing for Arbitrary Object Types + +```ts +class MyProp { + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + } + + public static lerp (a: MyProp, b: MyProp, out: MyProp, t: number): MyProp { + const x = a.x; + const y = a.y; + out.x = x + t * (b.x - x); + out.y = y + t * (b.y - y); + return out; + } + + public static add (a: MyProp, b: MyProp): MyProp { + const out = new MyProp(); + out.x = a.x + b.x; + out.y = a.y + b.y; + return out; + } + + public static sub (a: MyProp, b: MyProp): MyProp { + const out = new MyProp(); + out.x = a.x - b.x; + out.y = a.y - b.y; + return out; + } + + clone(): MyProp { + return new MyProp(this.x, this.y); + } + + equals (other: MyProp, epsilon = EPSILON): boolean { + return ( + Math.abs(this.x - other.x) <= epsilon * Math.max(1.0, Math.abs(this.x), Math.abs(other.x)) + && Math.abs(this.y - other.y) <= epsilon * Math.max(1.0, Math.abs(this.y), Math.abs(other.y)) + ); + } + + x = 0; + y = 0; +} + +class MyObject { + angle = 0; + str = ''; + private _myProp = new MyProp(); + + set myProp(v) { + this._myProp.x = v.x; + this._myProp.y = v.y; + } + + get myProp() { + return this._myProp; + } +} + +const o = new MyObject(); +o.myProp.x = 1; +o.myProp.y = 1; + +tween(o) + .by(1, { myProp: { + value: new MyProp(100, 100), // Target value + progress: MyProp.lerp, // Provide custom easing function for the object + clone: v => v.clone(), // Provide clone function + add: MyProp.add, // If using the `by` action, provide the `add` method + sub: MyProp.sub, // If using the `by` action with reverse, provide the `sub` method along with `add` + legacyProgress: false, // Set to `false` to use the new progress callback with object parameters, e.g., `MyProp.lerp`, instead of the default number-based progress callback + } }).id(123) + .reverse(123) + .start(); +``` + +## Binding Different Objects + +There are more scenarios where `Node` is used as a binding target in development, and the code example is as follows. + +```ts +let quat : Quat = new Quat(); +Quat.fromEuler(quat, 0, 90, 0); +tween(this.node) + .to(tweenDuration, { + position: new Vec3(0, 10, 0), // Bind position + scale: new Vec3(1.2, 3, 1), // Bind scale + rotation:quat } // Bind rotation + ) + .start(); // Call the start method to enable tween +``` + +In fact the tween can be bound to any object, the code example is as follows: + +```ts +class BindTarget{ + color : Color +} + +let sprite : Sprite = this.node.getComponent(Sprite) ; +let bindTarget : BindTarget = new BindTarget(); +bindTarget.color = Color.BLACK; +tween(bindTarget) + .by( 1.0, { color: Color.RED }, { + onUpdate(tar:BindTarget){ + sprite.color = tar.color; // Set the sprite to the color inside the 'BindTarget' + } +}) +.start() +``` + +## Multiple Actions + +In general, a tween can consist of one or more **actions**, and `Tween` maintains a data structure consisting of multiple **actions** to manage all actions within the current tween. + +The following code demonstrates moving the object's position 10 units along the Y-axis and then 10 units along the -Y-axis. + +```ts +let tweenDuration : number = 1.0; +tween(this.node.position) +.to( tweenDuration, new Vec3(0, 10, 0), { + onUpdate : (target:Vec3, ratio:number)=>{ + this.node.position = target; + } +}) +.to( tweenDuration, new Vec3(0, -10, 0), { + onUpdate : (target:Vec3, ratio:number)=>{ + this.node.position = target; + } +}) // At this point the number of actions in the tween is 2 +``` + +Multiple tweens can also be organized using the `union`, `sequence`, and `parallel` interfaces. By creating some fixed tweens in advance and using `union`, `sequence`, `parallel` to combine them, you can reduce the amount of code written. + +## Union + + The `union` method combines all current actions into one, with the following code example: + +```ts +let tweenDuration : number = 1.0; +tween(this.node) + .to(tweenDuration, { position:new Vec3(0, 10, 0) }) // Here the node is the target of the tween + .to(tweenDuration, { position:new Vec3(0, -10, 0) }) // At this point the number of actions in the Tween is 2 + .union() // The above two tweens will be combined into one, and the number of actions in the Tween will be 1 + .start(); // Call the start method to enable tween +``` + +> [!TIP] +> +> Supported from v3.8.4: `union(fromId)` + +The `union(fromId)` method merges actions from a specified identifier to the current action into a single sequence action. It is often used in conjunction with `id`, `repeat`, and `repeatForever`. The sample code is as follows: + +```ts +const node = new Node(); + +tween(node) + .to(1, { scale: new Vec3(10, 10, 10) }) + .by(1, { position: new Vec3(200, 0, 0) }).id(123) // Mark the by action as 123 + .delay(1) // Delay for one second + .reverse(123) // Reverse the action marked as 123, i.e., the previous 'by' action + .union(123) // Merge actions starting from the action marked as 123 + .repeat(3) // Repeat the merged actions 3 times + .start(); +``` + + +## Sequence + +The `sequence` function will transform the incoming tween into a queue form and add it to the current tween, with the following code example: + +```ts +let tweenDuration: number = 1.0; +let t1 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + +let t2 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }) + +tween(this.node).sequence(t1, t2).start(); // Add t1 and t2 tweens to the new tween queue +``` + +You can achieve the same effect using the [then](#then) interface as well: + +```ts +let tweenDuration: number = 1.0; +let t1 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }); + +let t2 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }); + +tween(this.node).then(t1).then(t2).start(); // First add tween t1 to the queue, then add tween t2 to the queue +``` + +## Parallel + +The `parallel` function will convert the incoming tween into parallel form and add it to the current tween, with the following code example: + +```ts +let tweenDuration: number = 1.0; +let t1 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + +let t2 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }) + +tween(this.node).parallel(t1, t2).start(); // Convert t1 and t2 to parallel tweens and add the current tween +``` + +## Then + +The `then` interface allows a new tween to be passed in and that tween integrated and added to the current tween's action, with the following code example. + +```ts +let tweenAfter = tween(this.node) + .to(1.0, { position: new Vec3(0, -10, 0) }) + +tween(this.node) + .by(1.0, { position: new Vec3(0, 10, 0) }) + .then(tweenAfter) + .start(); +``` + +## Delay + +The `delay` interface adds a delay to the **current** action **after** it. + +Note that in the following code example, different `delay` positions can cause completely different results. + +- After a delay of 1 second, start the movement and perform it twice in a row: + + ```ts + let tweenDuration: number = 1.0; + tween(this.node) + .delay(1.0) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }) + .start() + ``` + +- After the first movement, there is a 1 second delay before the second movement is performed. + + ```ts + let tweenDuration: number = 1.0; + tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + .delay(1.0) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }) + .start() + ``` + +## Repeat + +The `repeat` interface can add a repeat count to the tween, if the `embedTween` parameter is empty, the last action of the current tween will be used as parameter. + +This means that if the current tween consists of more than one tween, only the **last** one will be repeated, note the following example: + +```ts +let tweenDuration: number = 1.0; +tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + .by(tweenDuration, { position: new Vec3(0, -10, 0) }) + .repeat(3) // Note that the 'by' tween is repeated 3 times here + .start() +``` + +If the second parameter `embedTween` is not empty, the embedded tween will be repeated, with the following code example: + +```ts +let tweenDuration: number = 1.0; +let embedTween = tween(this.node) + .by(tweenDuration, { position: new Vec3(0, -10, 0) }) + +tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + .repeat(3, embedTween) // Repeat the 'embedTween' + .start() +``` + +The usage of `repeatForever` interface is similar to `repeat`, but it becomes permanently repeated. + + +## Action Identifier + +> [!TIP] +> +> Supported since v3.8.4 + +The `id` interface is used to assign a numerical identifier to the preceding action. Ensure that **identifiers are unique**, as the tween system will directly use the first action found during internal searches. + +It is frequently used in conjunction with `union(fromId)`, `reverse(id)`, and `reverse(tween, id)`. Here's an example code snippet: + +```ts +tween(this.node) + .by(1, { position: new Vec3(100, 0, 0) }).id(123) // Assign identifier 123 to the by action + .delay(1) // Delay for one second + .reverse(123) // Reverse the action identified by 123, and add the reversed action to the current action queue + .start(); +``` + +## Reversing Actions + +> [!TIP] +> +> Supported since v3.8.4 + +The `reverse` interface has three overloaded implementations: + +```ts +// Returns a "newly created" tween instance that reverses all actions in the current tween. +reverse (): Tween; + +/** + * Reverses a specific action identified within the current tween. + * @param id Identifier of the action to reverse within the current tween. + * @return Returns the instance itself for chaining. + */ +reverse (id: number): Tween; + +/** + * Reverses a specific action identified within another tween. + * @param otherTween Another tween instance to search for the action by identifier. + * @param id Identifier of the action to reverse. + * @return Returns the instance itself for chaining. + */ +reverse (otherTween: Tween, id?: number): Tween; +``` + +### `reverse()` Example + +Reverse all actions in the current tween and return a **newly created** tween instance: + +```ts +const t1 = tween(this.node) + .by(1, { position: new Vec3(100, 0, 0) }) // Node's x coordinate +100 relative to current position + .by(1, { scale: new Vec3(2, 2, 2) }); // Node's overall scale +2 + +const t2 = t1.reverse(); // Reverse entire tween t1 and store it as a new tween instance t2 +// Equivalent to: +// const t2 = tween(this.node) +// .by(1, { scale: new Vec3(-2, -2, -2) }) +// .by(1, { position: new Vec3(-100, 0, 0) }); + +tween(this.node) + .to(1, { position: new Vec3(200, 0, 0) }) + .then(t2) + .start(); +``` + +### `reverse(id)` Example + +Reverse a specific action identified within the current tween: + +```ts +tween(this.node) + .to(1, { scale: new Vec3(10, 10, 10) }) + .by(1, { position: new Vec3(200, 0, 0) }).id(123) // Assign identifier 123 to the by action + .delay(1) + .reverse(123) // Reverse the action identified by 123 within the current tween + .start(); +``` + +### `reverse(otherTween, id?)` Example + +- Without passing an `id`, reverse the entire tween `otherTween` and integrate all actions into a single Sequence action, then add this action to the current tween's action queue. Achieves the same effect as the example in [reverse() Example](#reverse-example): + +```ts +const t = tween(this.node) + .by(1, { position: new Vec3(100, 0, 0) }) // Node's x coordinate +100 relative to current position + .by(1, { scale: new Vec3(2, 2, 2) }); // Node's overall scale +2 + +tween(this.node) + .to(1, { position: new Vec3(200, 0, 0) }) + .reverse(t) // Reverse the entire tween t and add it as an action + .start(); +``` + +- When passing an `id`, reverse a specific action identified within `otherTween` and add this action to the current tween's action queue: + +```ts +const t = tween(node) + .parallel( + tween(node).sequence( + tween(node).by(1, { position: new Vec3(100, 0, 0) }).id(123), // Assign identifier 123 to the by action + tween(node).to(1, { position: new Vec3(1000, 0, 0) }), + ), + tween(node).delay(1), + ); + +tween(node) + .to(1, { position: new Vec3(200, 0, 0) }) + .reverse(t, 123) // Reverse the action identified by 123 within tween t + .start(); +``` + +## Node-related Tween + +The node-related methods only work if `target` is `Node`. + +### Visibility + +The `show` and `hide` interfaces control the display and hiding of the bound nodes, in the following example the nodes are hidden and displayed after a 1 second delay. + +```ts +tween(this.node) + .hide() + .delay(1.0) + .show() + .start(); +``` + +### RemoveSelf + +This method generates a **delete node action** that removes the incoming node from within the scene tree. + +In the following example, the node will be removed from the scene after a delay of 1 second. + +```ts +tween(this.node) + .delay(1.0) + .removeSelf() + .start() +``` + +## Call + +The `call` interface allows a callback action to be added to the tween, which is useful when dealing with certain asynchronous logic, as in the following example: + +```ts +tween(this.node) + .to(1.0, { position: new Vec3(0, 10, 0)}) + // This method will be called when the 'to' action is completed + .call( ()=>{ + console.log("call"); + }) + .start() +``` + +## Set + +The properties of the target can be set via `set`. The following example will set the node at [0, 100, 0] after a delay of 1 second. + +```ts +tween(this.node) + .delay(1.0) + .set({ position: new Vec3(0, 100, 0) }) + .start(); +``` + +It is also possible to set several different properties at the same time, with the following code example: + +```ts +tween(this.node) + // Set the position, scale and rotation of the node at the same time + .set({ position: new Vec3(0, 100, 0), scale: new Vec3(2, 2, 2), rotation: Quat.IDENTITY } ) + .start(); +``` + +## Clone + +The `clone` method copies the current tween to the target parameter. Note that the source tween and the current tween should be of the same type when copying, i.e. the `T` in `new Tween(target: T)` needs to be of the same type. The code example is as follows. + +```ts +const srcTween = tween(this.node).delay(1.0).by(1.0, { position: new Vec3(0, 10, 0) }) +// Copy 'srcTween' to a node named Cone +srcTween.clone(find("Cone")).start(); +``` + +> [!TIP] +> +> Starting from v3.8.4, it is supported to clone tweens without passing the `target` parameter, meaning the cloned tween directly uses the original target. + +Example: + +```ts +const srcTween = tween(this.node).delay(1.0).by(1.0, { position: new Vec3(0, 10, 0) }); +const clonedTween = srcTween.clone(); // clonedTween's target is also the this.node object +``` + +## Specifying Different Target Objects for Sub-tweens in `sequence`, `parallel`, `then` Interfaces + +> [!TIP] +> +> Supported from v3.8.4 + +If you want to handle actions like `position`, `contentSize`, and `color` simultaneously in a tween chain, with their targets being different—node, UITransform component on the node, and Sprite component on the node respectively, you can achieve this using the following code: + +```ts +tween(node) + .parallel( + tween(node).to(1, { position: new Vec3(100, 100, 0) }), + tween(node.getComponent(UITransform) as UITransform).to(1, { contentSize: size(100, 100) }), + tween(node.getComponent(Sprite) as Sprite).to(1, { color: color(100, 100, 100, 100) }), + ) + .start(); +``` + +The `sequence` and `then` interfaces work in a similar manner. + +## Stopping Tweens + +### stop + +Instance method used to stop a specified tween. + +```ts +const t = tween(node) + .by(1, { position: v3(90, 90, 90) }) + .start(); // Start the tween + +// ...... + +t.stop(); // Stop the tween +``` + +### stopAll + +**Static method** to stop all tweens. + +```ts +Tween.stopAll(); +``` + +### stopAllByTag + +**Static method** to stop all tweens with a specified tag. + +- Without target parameter + +```ts +const MY_TWEEN_TAG = 123; +const node1 = new Node(); +const node2 = new Node(); + +const t1 = tween(node1) + .tag(MY_TWEEN_TAG) + .by(1, { position: new Vec3(100, 0, 0) }) + .start(); + +const t2 = tween(node2) + .tag(MY_TWEEN_TAG) + .by(1, { scale: new Vec3(2, 2, 2) }) + .start(); + +Tween.stopAllByTag(MY_TWEEN_TAG); // Both t1 and t2 have the tag MY_TWEEN_TAG, so both will be stopped +``` + +- With target parameter + +```ts +const MY_TWEEN_TAG = 123; +const node1 = new Node(); +const node2 = new Node(); + +const t1 = tween(node1) + .tag(MY_TWEEN_TAG) + .by(1, { position: new Vec3(100, 0, 0) }) + .start(); + +const t2 = tween(node2) + .tag(MY_TWEEN_TAG) + .by(1, { scale: new Vec3(2, 2, 2) }) + .start(); + +Tween.stopAllByTag(MY_TWEEN_TAG, node1); // Both t1 and t2 have the tag MY_TWEEN_TAG, but only t1 will be stopped since its target is node1, t2 will continue running as its target is node2 +``` + +### stopAllByTarget + +**Static method** to stop all tween instances associated with a specified target object. + +```ts +const node1 = new Node(); +const node2 = new Node(); + +const t1 = tween(node1) + .by(1, { position: new Vec3(100, 0, 0) }) + .start(); + +const t2 = tween(node2) + .by(1, { scale: new Vec3(2, 2, 2) }) + .start(); + +const t3 = tween(node1) + .by(1, { angle: 90 }) + .start(); + +Tween.stopAllByTarget(node1); // t1 and t3 are associated with node1, so they will be stopped, t2 will continue running +``` + +## Pausing/Resuming Tweens + +> [!TIP] +> +> Supported from v3.8.4 + +Manual pause and resume: + +```ts +const t = tween(this.node) + .by(1, { position: new Vec3(90, 0, 0) }).id(123) + .reverse(123) + .union() + .repeatForever() + .start(); // Start the tween + +// ...... +t.pause(); // Pause the tween t + +// ...... +t.resume(); // Resume the tween t +``` + +> [!IMPORTANT] +> +> From v3.8.4, if the tween target is of type Node, the tween will automatically pause and resume based on the Node's active state. + +## Scaling Tween Time + +> [!TIP] +> +> Supported from v3.8.4 + +```ts +tween(this.node) + .to(1, { position: new Vec3(100, 100, 100) }) + .timeScale(0.5) + .start(); +``` + +In the above example, `timeScale` is set to 0.5. The `to` action's duration is 1, so the actual total duration of the tween execution will be `duration / timeScale = 1 / 0.5 = 2 seconds`. + +## Getting the Total Duration of a Tween + +> [!TIP] +> +> Supported from v3.8.4 + +```ts +const t = tween(this.node) + .to(1, { position: new Vec3(100, 100, 100) }) + .to(1, { scale: new Vec3(2, 2, 2) }) + .start(); + +console.log(t.duration); // Outputs 2 +``` + +## Custom Actions (constant duration) + +> [!TIP] +> +> Supported from v3.8.4 + +The `update` interface is used to add a custom action with constant duration. + +Its interface declaration is as follows: + +```ts +export type TweenUpdateCallback = (target: T, ratio: number, ...args: Args) => void; +/** + * Add a custom action with constant duration. + * @param duration The tween time in seconds. + * @param cb The callback of the current action. + * @param args The arguments passed to the callback function. + * @return The instance itself for easier chaining. + */ +update (duration: number, cb: TTweenUpdateCallback, ...args: Args): Tween { ... } +``` + +Example code: + +```ts +let done = false; + +tween(this.node) + .delay(1) + .by(1, { position: v3(90, 90, 90) }) + .update(1, (target: Node, ratio: number, a: number, b: boolean, c: string, d: { 'world': () => number }): void =>{ + // ...... + // target, a, b, c, d are all parameters passed to the update callback, n parameters can be specified + // target is this.node + // a is 123 + // b is true + // c is 'hello' + // d is { 'world': (): number => 456 } + }, 123, true, 'hello', { 'world': (): number => 456 }) + .repeat(2) + .call(() => { done = true; }) + .start(); +``` + +## Custom Action (Indeterminate Duration) + +> [!TIP] +> +> Supported from v3.8.4 + +The `updateUntil` interface is used to add a custom action with an indeterminate duration. + +Its interface declaration is as follows: + +```ts +export type TweenUpdateUntilCallback = (target: T, dt: number, ...args: Args) => boolean; + +/** + * Adds a custom action with an indeterminate duration. If the callback function returns `true`, it indicates that the current action has ended. + * @param cb The action callback function. If the callback function returns `true`, it indicates that the current action has ended. + * @param args The arguments to pass to the action callback function. + * @return The instance itself to allow for chained calls. + */ +updateUntil (cb: TweenUpdateUntilCallback, ...args: Args): Tween { ... } +``` + +The following example code is used to track a dynamic object. When the object is approached, the node scales up, resets its position, and repeats the process. + +```ts +import { _decorator, Component, Node, tween, v3, Vec3 } from 'cc'; +const { ccclass, property } = _decorator; + +const positionTmp = new Vec3(); +const positionTmp2 = new Vec3(); + +@ccclass('TweenTest') +export class TweenTest extends Component { + + @property(Node) + targetNode: Node | null = null; + + start() { + tween(this.node) + .updateUntil((curNode: Node, dt: number)=>{ + const d = Vec3.copy(positionTmp2, this.targetNode!.position).subtract(curNode.position); + const length = d.length(); + if (length < 10) { + return true; // The `updateUntil` process ends when the distance between the current node and the target node is less than 10 + } + + const newPos = Vec3.copy(positionTmp, curNode.position).add(d.normalize().multiplyScalar(length / 10 * dt * 10)); + curNode.setPosition(newPos); + return false; // Returning `false` indicates that the `updateUntil` process should continue + }) + .by(0.25, { scale: v3(1, 1, 0) }, { easing: 'cubicInOut' }).id(1) + .reverse(1) + .call((curNode?: Node)=>{ + const newPos = v3((Math.random() - 0.5) * 400, (Math.random() - 0.5) * 400, 0); + if (newPos.y < 100 && newPos.y > 0) newPos.y = 100; + if (newPos.y > -100 && newPos.y < 0) newPos.y = -100; + + if (newPos.x < 100 && newPos.x > 0) newPos.x = 100; + if (newPos.x > -100 && newPos.x < 0) newPos.x = -100; + curNode?.setPosition(newPos); + }) + .union() + .repeatForever() + .start(); + + tween(this.node) + .by(1, { angle: 360 }) + .repeatForever() + .start(); + + } +} +``` + +Result: + +![](img/updateUntil.gif) + +## Starting Tween from a Specific Time + +> [!TIP] +> +> Supported from v3.8.4 + +The `start` interface can receive a `startTime` parameter, in seconds, to start the tween from a specific time. All tweens before this time will be executed immediately. + +```ts +const t = tween(this.node) + .to(1, { position: new Vec3(100, 100, 100) }) + .call(() => {}) + .to(1, { scale: new Vec3(2, 2, 2) }) + .start(1); // Start from the 1st second +``` + +In the above example, two `to` actions are created with a total duration of 2 seconds. Since the tween starts from the 1st second, the position of `this.node` will be immediately set to (100, 100, 100), and the callback of `call` will be invoked immediately. Then, over the next 1 second, the scale animation will enlarge to 2. + +## Destruction + +### Automatic Destruction + +> [!TIP] +> +> Supported from v3.8.4 + +When the target of tween is `Node`, it will listen to its destruction event for automatic destruction of tween, and the call to `target` method will also update the listener automatically. + +### Manual Destruction + +Most tweens destroy themselves after the last action, but tweens that are not destroyed properly, such as `repeatForever`, will remain in memory after switching scenes. You need to call the destroy interface manually to destroy it. + +To stop and destroy the tween, the following methods are available. + +- member `stop` function to destroy the tween, with the following code example. + + ```ts + let t = tween(this.node.position) + .to( 1.0, new Vec3(0, 10, 0), { + onUpdate : (target:Vec3, ratio:number)=>{ + this.node.position = target; + } + }) + t.stop(); + ``` + +- Use the static functions `stopAll`, `stopAllByTag` and `stopAllByTarget` to destroy all or specific tweens, with the following code example. + + ```ts + Tween.stopAll() // Destroy all tweens + + Tween.stopAllByTag(0); // Destroy all tweens with 0 as the tag + + Tween.stopAllByTarget(this.node); // Destroy all tweens on this node + ``` + +> **Note**: In versions prior to v3.8.4, remember to stop the corresponding tweens when switching scenes. From v3.8.4, the engine will handle this automatically. diff --git a/versions/4.0/en/tween/tween-function.md b/versions/4.0/en/tween/tween-function.md new file mode 100644 index 0000000000..1c5aa24346 --- /dev/null +++ b/versions/4.0/en/tween/tween-function.md @@ -0,0 +1,81 @@ +# Tween Functions + +Engine implements a series of different types of tween functions, through which different real-time animation effects can be achieved. These tween functions are mainly used in the `Tween.to` and `Tween.by` interfaces. + +## Built-in Tween Functions + +The current tween functions provided by the engine are shown below: + +```ts +export type TweenEasing = +'linear' | 'smooth' | 'fade' | 'constant' | +'quadIn' | 'quadOut' | 'quadInOut' | 'quadOutIn' | +'cubicIn' | 'cubicOut' | 'cubicInOut' | 'cubicOutIn' | +'quartIn' | 'quartOut' | 'quartInOut' | 'quartOutIn' | +'quintIn' | 'quintOut' | 'quintInOut' | 'quintOutIn' | +'sineIn' | 'sineOut' | 'sineInOut' | 'sineOutIn' | +'expoIn' | 'expoOut' | 'expoInOut' | 'expoOutIn' | +'circIn' | 'circOut' | 'circInOut' | 'circOutIn' | +'elasticIn' | 'elasticOut' | 'elasticInOut' | 'elasticOutIn' | +'backIn' | 'backOut' | 'backInOut' | 'backOutIn' | +'bounceIn' | 'bounceOut' | 'bounceInOut' | 'bounceOutIn'; +``` + +The effect can be seen in the following figure: + +![tweener](img/tweener.png) + +Figure from [http://hosted.zeh.com.br/tweener/docs/en-us/](http://hosted.zeh.com.br/tweener/docs/en-us/) + +The `ITweenOption` interface allows you to modify the tween function. The code example is as follows. + +```ts +let tweenDuration: number = 1.0; // Duration of the tween +tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }, { // + easing: "backIn", // Tween function + }) + .start(); // Start the tween +``` + +## ITweenOption + +`ITweenOption` is an optional property interface definition for tween. The interfaces are all **optional** and can be used on demand. The full example is as follows: + +```ts +let tweenDuration: number = 1.0; // Duration of the tween +tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }, { // + easing: "backIn", // Tween function + onStart: (target?: object) => { // Tween function + + }, + onUpdate: (target: Vec3, ratio: number) => { // Tween process + this.node.position = target; // Assign the position of the node to the result calculated by the tween system + }, + onComplete: (target?: object) => { // Start the tween + + }, + progress: (start: number, end: number, current: number, ratio: number): number => { + // Return custom interpolation progress + return 0.0; + } + }) + .start(); // Start the tween +``` + +The tween progress can also be customized through the `progress` interface, with the following code example: + +```ts +tween(this.node) + .to(1.0, { + position: new Vec3(0, 100, 0), + }, { + progress: (start: number, end: number, current: number, ratio: number): number => { + return math.lerp(start, end, ratio); + } + }) + .start() +``` + +For full API description, please refer to: [ITweenOption](%__APIDOC__%/en/interface/ITweenOption) diff --git a/versions/4.0/en/tween/tween-interface.md b/versions/4.0/en/tween/tween-interface.md new file mode 100644 index 0000000000..58f3ee5356 --- /dev/null +++ b/versions/4.0/en/tween/tween-interface.md @@ -0,0 +1,105 @@ +# Tween Interface + +## Properties and Interface + +### Interface + +The interfaces supported from `v3.8.4` onwards include: `reverse`, `id`, `union(fromId?: number)`, `timeScale`, `pause`, `resume`, `pauseAllByTarget`, `resumeAllByTarget`, `update`, `start(time)`, `duration`. + +| Interface | Description | +| :---------------- | :----------------------------------------------------------- | +| **tag** | Add a numeric tag (`number`) to the current tween | +| **to** | Add an interval action that computes the **absolute value** of properties | +| **by** | Add an interval action that computes the **relative value** of properties | +| **set** | Add an instantaneous action that **directly sets target properties** | +| **delay** | Add an instantaneous action that **delays time** | +| **call** | Add an instantaneous action that **calls a callback** | +| **target** | Add an instantaneous action that **directly sets the tween target** | +| **union** | Package all previous actions into one, or package actions from a specific id | +| **then** | **Insert a new tween into the tween queue** | +| **repeat** | **Execute several times** (previously repeated, please adapt accordingly) | +| **repeatForever** | **Repeat indefinitely** | +| **update** | Add a custom action | +| **id** | Set an id for the previous action, commonly used with reverse and union | +| **reverse** | Reverse **all** or **specific** actions in **another** tween and add them to the **current** tween; or reverse **specific** actions in the **current** tween | +| **timeScale** | Set the time scaling factor for the current tween: 1 is normal speed (default), 0.5 is half speed, 2 is double speed, etc. | +| **sequence** | **Add a sequential tween** | +| **parallel** | **Add a simultaneous tween** | +| **start** | **Start the tween** or start the tween from **a specific time point (in seconds)** | +| **stop** | **Stop the tween** | +| **pause** | **Pause the tween** | +| **resume** | **Resume the tween** | +| **clone** | **Clone the tween**, optionally resetting the target object | +| **show** | **Enable rendering on the node, the tween target must be Node** | +| **hide** | **Disable rendering on the node, the tween target must be Node** | +| **removeSelf** | **Remove the node from the scene tree, the tween target must be Node** | +| **destroySelf** | **Remove the node from the scene tree and call the node destruction function, the tween target must be Node** | +| **duration** | Get the total duration of the current tween. This is a getter, called as `const d = tweenInstance.duration;` | + +### Static Interface + +Static methods in the `Tween` class are as follows: + +```ts +Tween.stopAll() +Tween.stopAllByTag(0); +Tween.stopAllByTarget(this.node); +``` + +| Interface | Description | +| :-------------------- | :----------------------------------------------------------- | +| **stopAll** | Stop all tween instances
This interface will remove all registered tweens at the underlying level
**Note**: This method will affect all objects | +| **stopAllByTag** | Stop all tween instances with a specific tag
This interface will remove all tweens specified by the **tag** method
You can specify a second parameter `target?: object` to only remove tweens with a specific tag on that object | +| **stopAllByTarget** | Stop all tween instances associated with a target object | +| **pauseAllByTarget** | Pause all tween instances associated with a target object | +| **resumeAllByTarget** | Resume all tween instances associated with a target object | + +## Utility Function + +|Interface| Description | +|:-- |:--| +| **tween\** | Utility function to help instantiate the `Tween` class.
**Note**: This function is not a member of the `Tween` class. Users may call `new Tween(target:T)` to instantiate a new tween instance. | + +### Example + +The following is an example of using `to` method to create a tween instance: + +```ts +let tweenDuration : number = 1.0; // Duration of the tween +tween(this.node.position).to( tweenDuration, new Vec3(0, 10, 0), // Here takes the target of the node's position + { // Interface implementation of 'ITweenOption'. + onUpdate : (target:Vec3, ratio:number)=>{ // onUpdate accepts the current tween progress + this.node.position = target; // Assign the position of the node to the result calculated by the tween system + } +}).start(); // Start the tween by calling 'start' function +``` + +Alternatively, + +```ts +let tweenDuration : number = 1.0; // Duration of the tween +tween(this.node).to( // Directly use node as the tween target + tweenDuration, + { position: new Vec3(0, 10, 0) } // Create an object with the position property +).start(); // Call the start method to begin the tween +``` + +Note: If the node is used as the tween target, there is no need to use `setPosition` or the `position` setter within `onUpdate` to update the node's position. The tween system will automatically call the `position` setter internally. + +For more examples, please see [Tween Example](tween-example.md). + +## Caveats + +To avoid frequent updates to the transform data of nodes, `Node` class is constructed with an internal `dirty` state which only permits updating when modifications to the node’s transform data is called. + +Due to pre-existing limitations, such as the position data returned by `this.node.position` being a public vector, certain coding conventions may not behave as expected. + +For instance, when attempting to execute `this.node.position.x = 1`, the code only calls the `getter` for the position data and not the `setter` for the `dirty` state data to be updated, thus no transform data of the node will remain unchanged. + +We advise against coding in such a manner and encourage users to call the `setter` for the position data via method `setPosition` instead, such as: + +```typescript +let _pos = new Vec3(0, 1, 0); +this.node.position = _pos; // Use the setter of 'Transform.position' +this.node.setPosition(_pos); // Or use the 'setPosition' function +``` diff --git a/versions/4.0/en/ui-system/components/editor/add-component.png b/versions/4.0/en/ui-system/components/editor/add-component.png new file mode 100644 index 0000000000..7e986e4986 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/add-component.png differ diff --git a/versions/4.0/en/ui-system/components/editor/base-component.md b/versions/4.0/en/ui-system/components/editor/base-component.md new file mode 100644 index 0000000000..45937998a8 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/base-component.md @@ -0,0 +1,11 @@ +# UI Components + +Some common UI controls can be created by adding nodes. Click the **+** Create Node button in the top left corner of the **Hierarchy** panel, then select **UI** to create the desired UI node, and the corresponding UI component will be automatically mounted to the node. + +![create-ui](create-ui.png) + +Other UI components can be added by manually selecting the node in the **Hierarchy** panel and clicking the **Add Component -> UI** in the **Inspector** panel: + +![add-component](add-component.png) + +For the specific description of each component, please refer to the corresponding component description documentation. diff --git a/versions/4.0/en/ui-system/components/editor/block-input-events.md b/versions/4.0/en/ui-system/components/editor/block-input-events.md new file mode 100644 index 0000000000..d633d92eba --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/block-input-events.md @@ -0,0 +1,7 @@ +# BlockInputEvents Component Reference + +The __BlockInputEvents__ component will intercept all input events (mouse and touch events) in the bounding box of the node to which it belongs, preventing input from penetrating to the lower-level nodes, generally used in the background of the upper-level UI. + +When we make a pop-up UI dialog, the background of the dialog does not intercept events by default. That is, although the background is blocking the game scene, when clicked or touched on the background, the blocked game element underneath will still respond to the click event. At this point we can avoid this by simply adding the __BlockInputEvents__ component to the node where the background is located. + +This component does not have any API interface and is effective when added directly to a scene. diff --git a/versions/4.0/en/ui-system/components/editor/button.md b/versions/4.0/en/ui-system/components/editor/button.md new file mode 100644 index 0000000000..b9d1272958 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/button.md @@ -0,0 +1,128 @@ +# Button Component Reference + +The __Button__ component responds to a click from the user. When the user clicks a __Button__, its status will change. In addition, users can assign a custom behavior to buttons' __click event__. + +![button.png](./button/button.png) + +![button-color](./button/button-color.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/Button__ to add the __Button__ component to the node. + +To use `Button`, please refer to the [Button API](%__APIDOC__%/en/class/Button) documentation and the [Button](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/03.button) scene of the test-cases-3d project. + +## Button Properties + +| Property | Function Explanation | +| ------------ | -------------------- | +| __Target__ | Specify the __Button__ background node. When the __Button__ status changes, the `Color` or `Sprite` property of the node will be modified. | +| __Interactable__ | Boolean type, if set to `false` then the __Button__ component enters the forbidden state. | +| __Transition__ | Enumeration type, including __NONE__, __COLOR__, __SPRITE__ and __SCALE__. Each type corresponds to a different Transition setting. Please see the __Button Transition__ section below for details. | +| __ClickEvents__ | List type, default is null. Each event added by the user is composed of the node reference, component name and a response function. Please see the __Button Event__ section below for details. | + +## Button Transition + +__Button Transition__ is used to choose the action of the button when clicked by the user. Currently the types available are __NONE__, __COLOR__, __SPRITE__ and __SCALE__. + +![transition](button/transition.png) + +### Color Transition + +![color-transition](button/color-transition.png) + +| Property | Function Explanation | +| ---------- | -------------------- | +| __Normal__ | Color of Button under Normal status. | +| __Pressed__ | Color of Button under Pressed status. | +| __Hover__ | Color of Button under Hover status. | +| __Disabled__ | Color of Button under Disabled status. | +| __Duration__ | Time interval needed for Button status switching. | + +### Sprite Transition + +![sprite-transition](button/sprite-transition.png) + +| Property | Function Explanation | +| -------------- | -------------------- | +| __Normal__ | SpriteFrame of Button under Normal status. | +| __Pressed__ | SpriteFrame of Button under Pressed status. | +| __Hover__ | SpriteFrame of Button under Hover status. | +| __Disabled__ | SpriteFrame of Button under Disabled status. | + +### Scale Transition + +![scaleTransition](button/scaleTransition.png) + +| Property | Function Explanation | +| -------------- | ----------- | +| __Duration__ | Time interval needed for Button status switching. | +| __ZoomScale__ | When the user clicks the button, the button will zoom to a scale. The final scale of the button equals to the button's original `scale * zoomScale`, and the zoomScale can be a negative value.| + +## Button Click Events + +The __Button__ can additionally add a __click event__ to respond to the player's __click action__. There are two ways to achieve this. + +### Add a callback using the Properties + +![button-event](button/button-event.png) + +| Property | Function Explanation | +| -------------- | ----------- | +| __Target__ | Node with the script component. | +| __Component__ | Script component name. | +| __Handler__ | Assign a callback function from the given component which will be triggered when the user clicks the Button. | +| __CustomEventData__ | A user-defined string value passed as the last event argument of the event callback. | + +### Add a callback using the script + +There are two ways to add a callback through the script. + +1. The event callback added by this method is the same as the event callback added by the editor, all added by the script. First you need to construct a `EventHandler` object, and then set the corresponding `target`, `component`, `handler` and `customEventData` parameters. + + ```ts + import { _decorator, Component, Event, Node, Button, EventHandler } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass("example") + export class example extends Component { + onLoad () { + const clickEventHandler = new EventHandler(); + // This node is the node to which your event handler code component belongs + clickEventHandler.target = this.node; + // This is the script class name + clickEventHandler.component = 'example'; + clickEventHandler.handler = 'callback'; + clickEventHandler.customEventData = 'foobar'; + + const button = this.node.getComponent(Button); + button.clickEvents.push(clickEventHandler); + } + + callback (event: Event, customEventData: string) { + // The event here is a Touch object, and you can get the send node of the event by event.target + const node = event.target as Node; + const button = node.getComponent(Button); + console.log(customEventData); // foobar + } + } + ``` + +2. By `button.node.on ('click', ...)` to add event callback. This is a very simple way, but the way has some limitations, in the event callback the screen coordinate point of the current click button cannot be obtained. + + ```ts + // Suppose we add an event handler callback to the onLoad method of a component and handle the event in the callback function: + import { _decorator, Component, Button } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass("example") + export class example extends Component { + @property(Button) + button: Button | null = null; + onLoad () { + this.button.node.on(Button.EventType.CLICK, this.callback, this); + } + + callback (button: Button) { + // Note that events registered this way cannot pass customEventData + } + } + ``` diff --git a/versions/4.0/en/ui-system/components/editor/button/button-color.png b/versions/4.0/en/ui-system/components/editor/button/button-color.png new file mode 100644 index 0000000000..75776eb16e Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/button/button-color.png differ diff --git a/versions/4.0/en/ui-system/components/editor/button/button-event.png b/versions/4.0/en/ui-system/components/editor/button/button-event.png new file mode 100644 index 0000000000..ac2a24bd96 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/button/button-event.png differ diff --git a/versions/4.0/en/ui-system/components/editor/button/button.png b/versions/4.0/en/ui-system/components/editor/button/button.png new file mode 100644 index 0000000000..c67d09e7cc Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/button/button.png differ diff --git a/versions/4.0/en/ui-system/components/editor/button/color-transition.png b/versions/4.0/en/ui-system/components/editor/button/color-transition.png new file mode 100644 index 0000000000..0433aa1aac Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/button/color-transition.png differ diff --git a/versions/4.0/en/ui-system/components/editor/button/scaleTransition.png b/versions/4.0/en/ui-system/components/editor/button/scaleTransition.png new file mode 100644 index 0000000000..bb18f051bd Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/button/scaleTransition.png differ diff --git a/versions/4.0/en/ui-system/components/editor/button/sprite-transition.png b/versions/4.0/en/ui-system/components/editor/button/sprite-transition.png new file mode 100644 index 0000000000..0ce4e9bc67 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/button/sprite-transition.png differ diff --git a/versions/4.0/en/ui-system/components/editor/button/transition.png b/versions/4.0/en/ui-system/components/editor/button/transition.png new file mode 100644 index 0000000000..551c0ac71c Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/button/transition.png differ diff --git a/versions/4.0/en/ui-system/components/editor/canvas.md b/versions/4.0/en/ui-system/components/editor/canvas.md new file mode 100644 index 0000000000..3ff51806dc --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/canvas.md @@ -0,0 +1,22 @@ +# Canvas Component Reference + +![canvas component](canvas/canvas.png) + +The node where the **RenderRoot2D** component is located is the data collection entry point for the 2D renderable component, and the **Canvas** component inherits from the **RenderRoot2D** component, so the **Canvas** component is also the data collection entry point. There can be multiple Canvas nodes in the scene, **all 2D rendering elements must be rendered as children of RenderRoot2D**. + +In addition to the data entry capability inherited from RenderRoot2D, the Canvas node itself is also an important component for screen adaptation, and plays a key role in multi-resolution adaptation in game production. Please refer to the [Multi-Resolution Adaptation](../engine/multi-resolution.md) documentation. The design resolution and adaptation scheme of Canvas are configured through **Project Settings**. + +![design-resolution](canvas/design-resolution.png) + +The Canvas itself has nothing to do with the camera, its main role is to adapt the screen as described above, so the rendering of the Canvas only depends on the camera corresponding to its node `layer`, and it can control the properties of the camera to determine the rendering effect of the nodes under the Canvas. + +## Canvas Properties + +| Property | Function Description | +| :------------- | :---------- | +| CameraComponent | The camera associated with the Canvas. This camera does not necessarily render the content under the Canvas, but can be used in conjunction with the `AlignCanvasWithScreen` property to automatically change some of the Camera's parameters to align with the Canvas. +| AlignCanvasWithScreen | Whether the camera associated with the Canvas should be aligned with the Canvas, do not check this option if it is necessary to control the camera position manually (like side-scrolling games, etc.) + +## Caution + +If UI rendering errors, blurred screen, splash screen, etc are encountered, please refer to this [detailed explanation](../engine/priority.md#detailed-explanation). diff --git a/versions/4.0/en/ui-system/components/editor/canvas/canvas.png b/versions/4.0/en/ui-system/components/editor/canvas/canvas.png new file mode 100644 index 0000000000..18378b3de6 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/canvas/canvas.png differ diff --git a/versions/4.0/en/ui-system/components/editor/canvas/design-resolution.png b/versions/4.0/en/ui-system/components/editor/canvas/design-resolution.png new file mode 100644 index 0000000000..c0faf90b08 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/canvas/design-resolution.png differ diff --git a/versions/4.0/en/ui-system/components/editor/create-ui.png b/versions/4.0/en/ui-system/components/editor/create-ui.png new file mode 100644 index 0000000000..526666a018 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/create-ui.png differ diff --git a/versions/4.0/en/ui-system/components/editor/editbox.md b/versions/4.0/en/ui-system/components/editor/editbox.md new file mode 100644 index 0000000000..a2429196a4 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/editbox.md @@ -0,0 +1,125 @@ +# EditBox Component Reference + +__EditBox__ is a text input component, use this component to get user input easily. + +![editbox](editbox/editbox.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/EditBox__ to add the __EditBox__ component to the node. + +To use `EditBox`, please refer to the [EditBox API](%__APIDOC__%/en/class/EditBox) documentation and the [EditBox](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/12.editbox) scene of the test-cases-3d project. + +## EditBox Properties + +| Property | Function Explanation | +| -------------- | ----------- | +| __BackgroundImage__ | The Sprite component attached to the node for EditBox's background | +| __FontColor__ | The input text color of EditBox | +| __FontSize__ | The input text size of EditBox | +| __InputFlag__ | Specify the input flag: password or capitalized word. (Only supports Android platform) | +| __InputMode__ | Specify the input mode: multiline or single line | +| __LineHeight__ | The input text line height of EditBox | +| __MaxLength__ | The maximize input characters of EditBox | +| __Placeholder__ | The text content of EditBox placeholder | +| __PlaceholderFontColor__ | The text font color of EditBox placeholder | +| __PlaceholderFontSize__ | The text font size of EditBox placeholder | +| __PlaceholderLabel__ | The Label component attached to the node for EditBox's placeholder text label | +| __ReturnType__ | The keyboard return type of EditBox. This is useful for keyboard of mobile device | +| __String__ | The initial input text of EditBox, which displays the text of the placeholder if not set | +| __TabIndex__ | Set the `tabIndex` of the DOM input element, only useful on the Web | +| __TextLabel__ | The Label component attached to the node for EditBox's input text label | + +## EditBox Events + +![editbox-event](./editbox/editbox-event.png) + +For event structure you can refer to the [Button](./button.md) documentation. + +- __Editing Did Began__: This event will be triggered when the user __clicks__ on the __EditBox__. +- __Editing Did Ended__: This event will be triggered when the __EditBox loses focus__. + - When in __single line input mode__, it's triggered after user presses __Enter__ key or __clicks__ the area __outside__ of __EditBox__. + - When in __multiline input mode__, it's triggered only after user __clicks__ the area __outside__ of __EditBox__. +- __Text Changed__: This event will be triggered when the __content__ in __EditBox__ is __changed each time__. However, it is not dispatched if it is set by `setter` of `EditBox.string`. + +## Detailed Explanation + +- If you want to input password, you need set __Input Flag__ to `PASSWORD` and the __Input Mode__ mustn't be `ANY`, usually we use __Single Line__. +- If you want to enable multiline input support, you can set the __Input Mode__ to `Any`. +- The background image of EditBox support 9-slicing sprite frame, you could customize the border as you did in Sprite component. + +## Add a callback through the script code + +### Method one + +The event callback added by this method is the same as the event callback added by the editor, all added by code. First you need to construct a `EventHandler` object, and then set the corresponding `target`, `component`, `handler` and `customEventData` parameters. + +```ts +import { _decorator, Component, EditBoxComponent, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad() { + const editboxEventHandler = new EventHandler(); + // This node is the node to which your event handler code component belongs. + editboxEventHandler.target = this.node; + editboxEventHandler.component = 'example'; + editboxEventHandler.handler = 'onEditDidBegan'; + editboxEventHandler.customEventData = 'foobar'; + + const editbox = this.node.getComponent(EditBoxComponent); + editbox.editingDidBegan.push(editboxEventHandler); + // You can also register other callback functions in a similar way. + // editbox.editingDidEnded.push(editboxEventHandler); + // editbox.textChanged.push(editboxEventHandler); + // editbox.editingReturn.push(editboxEventHandler); + } + + onEditDidBegan(editbox, customEventData) { + // The editbox here is a EditBox object. + // The customEventData parameter here is equal to the "foobar" you set before. + } + + // Suppose this callback is for the editingDidEnded event. + onEditDidEnded(editbox, customEventData) { + // The editbox here is a EditBox object. + // The customEventData parameter here is equal to the "foobar" you set before. + } + + // Suppose this callback is for the textChanged event. + onTextChanged(text, editbox, customEventData) { + // The text here indicates the text content of the modified EditBox. + // The editbox here is a EditBox object. + // The customEventData parameter here is equal to the "foobar" you set before. + } + // Suppose this callback is for the editingReturn event. + onEditingReturn(editbox, customEventData) { + // The editbox here is a EditBox object. + // The customEventData parameter here is equal to the "foobar" you set before. + } +} +``` + +### Method two + +Added with **Node**'s event API `editbox.node.on('editing-did-began', ...)`. + +```ts +// Suppose we add an event handler callback inside a component's onLoad method and event handlers in the callback function. +import { _decorator, Component, EditBoxComponent } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + @property(EditBoxComponent) + editbox: EditBoxComponent | null = null; + onLoad(){ + this.editbox.node.on('editing-did-began', this.callback, this); + } + + callback(editbox: EditBoxComponent){ + // The callback parameter is the EditBox component, note that events registered this way cannot pass customEventData. + } +} +``` + +Similarly, you can register events such as `editing-did-ended`, `text-changed`, `editing-return`, etc. The parameters of the callback function for these events are consistent with the parameters of `editing-did-began`. diff --git a/versions/4.0/en/ui-system/components/editor/editbox/editbox-event.png b/versions/4.0/en/ui-system/components/editor/editbox/editbox-event.png new file mode 100644 index 0000000000..63ac407fa3 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/editbox/editbox-event.png differ diff --git a/versions/4.0/en/ui-system/components/editor/editbox/editbox.png b/versions/4.0/en/ui-system/components/editor/editbox/editbox.png new file mode 100644 index 0000000000..f0eeee963e Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/editbox/editbox.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics.md b/versions/4.0/en/ui-system/components/editor/graphics.md new file mode 100644 index 0000000000..c67ea1499f --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics.md @@ -0,0 +1,63 @@ +# Graphics Component Reference + +The __Graphics__ component provides a series of drawing functions that reference the Web canvas's drawing APIs. + +![graphics](graphics/graphics.png) + +Select a node in the __Hierarchy__ panel, then click the __Add Component__ button at the bottom of the __Inspector__ panel and select __Graphics__ from __UI -> Render__. Then you can add the __Graphics__ component to the node. + +To use graphics, please refer to the [graphics API](%__APIDOC__%/en/class/GraphicsComponent) documentation and the [graphics](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/14.graphics) scene of the test-cases-3d project. + +## Graphic Properties + +| Property | Function Explanation | +| :-------------- | :----------- | +| [**CustomMaterial**](../engine/ui-material.md)| Custom materials that can be used to achieve rendering effects such as dissolve, external glow, etc. | +| [**LineWidth**](graphics/lineWidth.md) | Current line width. | +| [**LineJoin**](graphics/lineJoin.md) | LineJoin determines how two connecting segments (of lines, arcs or curves) with non-zero lengths in a shape are joined together. | +| [**LineCap**](graphics/lineCap.md) | LineCap determines how the end points of every line are drawn. | +| [**StrokeColor**](graphics/strokeColor.md) | Stroke color. Sets or returns the color used for the stroke. | +| [**FillColor**](graphics/fillColor.md) | Sets or returns the color used for the `fill` function. | +| [**MiterLimit**](graphics/miterLimit.md) | Sets or returns the maximum miter length. | + +## Graphics API + +### Path + +| Function | Function Explanation | +| :-------------- | :----------- | +| [**moveTo**](graphics/moveTo.md) (x, y) | Move the render cursor to a specified point in the canvas without creating lines. | +| [**lineTo**](graphics/lineTo.md) (x, y) | Adds a straight line to the path. | +| [**bezierCurveTo**](graphics/bezierCurveTo.md) (c1x, c1y, c2x, c2y, x, y) | Adds a cubic Bézier curve to the path. | +| [**quadraticCurveTo**](graphics/quadraticCurveTo.md) (cx, cy, x, y) | Adds a quadratic Bézier curve to the path. | +| [**arc**](graphics/arc.md) (cx, cy, r, startAngle, endAngle, counterclockwise) | Adds an arc to the path which is centered at (cx, cy) position with radius r starting at startAngle and ending at endAngle going in the given direction by counterclockwise (defaulting to false). | +| [**ellipse**](graphics/ellipse.md) (cx, cy, rx, ry) | Adds an ellipse to the path. | +| [**circle**](graphics/circle.md) (cx, cy, r) | Adds a circle to the path. | +| [**rect**](graphics/rect.md) (x, y, w, h) | Adds a rectangle to the path. | +| [**close**](graphics/close.md) () | Close the previous path by connecting the last point and the beginning point. | +| [**stroke**](graphics/stroke.md) () | Stroke the path as lines | +| [**fill**](graphics/fill.md) () | Close and fill a path's inner surface | +| [**clear**](graphics/clear.md) () | Erase any previously drawn content. | + +### Modify the drawing pattern through script code + +```ts +import { _decorator, Component, Graphics } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Example') +export class Example extends Component { + start () { + const g = this.getComponent(Graphics); + g.lineWidth = 10; + g.fillColor.fromHEX('#ff0000'); + g.moveTo(-40, 0); + g.lineTo(0, -80); + g.lineTo(40, 0); + g.lineTo(0, 80); + g.close(); + g.stroke(); + g.fill(); + } +} +``` diff --git a/versions/4.0/en/ui-system/components/editor/graphics/arc.md b/versions/4.0/en/ui-system/components/editor/graphics/arc.md new file mode 100644 index 0000000000..5343cf4c69 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/arc.md @@ -0,0 +1,28 @@ +# Arc + +The `arc()` method creates an arc/curve (used to create circles or partial circles). + +> __Note__: to create a circle with `arc()`, set the start angle to `0` and the end angle to `2 * Math.PI`. + +| Parameter | Description | +| :-------------- | :----------- | +| **x** | The x coordinate of the center control point. | +| **y** | The y coordinate of the center control point. | +| **r** |Arc radian. | +| **startAngle** | Start radian, measured clockwise from positive x axis. | +| **endAngle** | End radian, measured clockwise from positive x axis. | +| **counterclockwise** | Determines the drawing direction.
**Clockwise** when set to `false` (default).
**Counterclockwise** when set to `true`. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.arc(100,75,50,0,1.5 * Math.PI); +ctx.stroke(); +``` + + + +


+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/arc.png b/versions/4.0/en/ui-system/components/editor/graphics/arc.png new file mode 100644 index 0000000000..991ae59d29 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/arc.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/bezierCurveTo.md b/versions/4.0/en/ui-system/components/editor/graphics/bezierCurveTo.md new file mode 100644 index 0000000000..beb93f9fbb --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/bezierCurveTo.md @@ -0,0 +1,29 @@ +# Bezier Curves + +The `bezierCurveTo()` method adds a bezier curve to the current path by using the specified control point that can generate the cubic bezier curve. + +> __Note__: Cubic Bezier curves require three control points. The first two points are for the control points in the cubic Bezier calculation, and the third point is the end point of the curve. The starting point of the curve is the last point in the current path. + +| Parameter | Description | +| :-------------- | :----------- | +| **cp1x** | The x coordinate of the first bezier control point. | +| **cp1y** | The y coordinate of the first bezier control point. | +| **cp2x** | The x coordinate of the second Bezier control point. | +| **cp2y** | The x coordinate of the second Bezier control point. | +| **x** | The x coordinate of the end point. | +| **y** | The y coordinate of the end point. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20,20); +ctx.bezierCurveTo(20,100,200,100,200,20); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/bezierCurveTo.png b/versions/4.0/en/ui-system/components/editor/graphics/bezierCurveTo.png new file mode 100644 index 0000000000..886fbc3693 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/bezierCurveTo.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/circle.md b/versions/4.0/en/ui-system/components/editor/graphics/circle.md new file mode 100644 index 0000000000..448854e028 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/circle.md @@ -0,0 +1,23 @@ +# Circles + +The `circle()` method is used to create a circle. + +| Parameter | Description | +| :-------------- | :----------- | +| **cx** | The x coordinate of the center of the circle. | +| **cy** | The y coordinate of the center of the circle. | +| **r** | Radius of the circle. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.circle(200,200, 200); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/circle.png b/versions/4.0/en/ui-system/components/editor/graphics/circle.png new file mode 100644 index 0000000000..ca3afb34d8 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/circle.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/clear.md b/versions/4.0/en/ui-system/components/editor/graphics/clear.md new file mode 100644 index 0000000000..b7becb197c --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/clear.md @@ -0,0 +1,19 @@ +# Clears + +The `clear()` function is used to clear all paths. + +## Example + +```ts +update: function (dt) { + const ctx = node.getComponent(Graphics); + ctx.clear(); + ctx.circle(200,200, 200); + ctx.stroke(); +} + +``` + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/close.md b/versions/4.0/en/ui-system/components/editor/graphics/close.md new file mode 100644 index 0000000000..300c33c885 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/close.md @@ -0,0 +1,20 @@ +# Close + +The `close()` method is used to create a path from a current point to the beginning point. + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20,20); +ctx.lineTo(20,100); +ctx.lineTo(70,100); +ctx.close(); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/close.png b/versions/4.0/en/ui-system/components/editor/graphics/close.png new file mode 100644 index 0000000000..629fc0ddc1 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/close.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/ellipse.md b/versions/4.0/en/ui-system/components/editor/graphics/ellipse.md new file mode 100644 index 0000000000..bb9f6e93e3 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/ellipse.md @@ -0,0 +1,24 @@ +# Ellipse + +The `ellipse()` method is used to create an ellipse. + +| Parameter | Description | +| --------- | ----------- | +| *cx* | The x coordinate of the center of the ellipse. | +| *cy* | The y coordinate of the center of the ellipse. | +| *rx* | The x radius of the ellipse. | +| *ry* | The y radius of the ellipse. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.ellipse(200,100, 200,100); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/ellipse.png b/versions/4.0/en/ui-system/components/editor/graphics/ellipse.png new file mode 100644 index 0000000000..b376bd1d62 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/ellipse.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/fill.md b/versions/4.0/en/ui-system/components/editor/graphics/fill.md new file mode 100644 index 0000000000..6fff0c3c8c --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/fill.md @@ -0,0 +1,20 @@ +# Fill + +The `fill()` method is used to fill the current image (path). The color used is [`fillColor`](./fillColor.md). + +> __Note__: if the path is not closed, the `fill()` method adds a line from the end of the path to the start point to close the path and then fills the path. + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.rect(20,20,150,100); +ctx.fillColor = Color.GREEN; +ctx.fill(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/fill.png b/versions/4.0/en/ui-system/components/editor/graphics/fill.png new file mode 100644 index 0000000000..e0f8558284 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/fill.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/fillColor.md b/versions/4.0/en/ui-system/components/editor/graphics/fillColor.md new file mode 100644 index 0000000000..2e0ec47247 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/fillColor.md @@ -0,0 +1,18 @@ +# Fill Color + +The `fillColor` property represents the color used for the [`fill`](./fill.md) function. + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.fillColor = new Color().fromHEX('#0000ff'); +ctx.rect(20,20,250,200); +ctx.stroke(); +``` + +![fillColor](fillColor.png) + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/fillColor.png b/versions/4.0/en/ui-system/components/editor/graphics/fillColor.png new file mode 100644 index 0000000000..b2909f54bf Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/fillColor.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/graphics.png b/versions/4.0/en/ui-system/components/editor/graphics/graphics.png new file mode 100644 index 0000000000..6d86a1521d Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/graphics.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/lineCap.md b/versions/4.0/en/ui-system/components/editor/graphics/lineCap.md new file mode 100644 index 0000000000..8deb638b06 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/lineCap.md @@ -0,0 +1,26 @@ +# Line Cap + +The `lineCap` property represents the style of the line end cap. + +| Possible line cap options | Description | +| :-------------- | :----------- | +| **Graphics.LineCap.BUTT** | Default. Add a straight edge to each end of the line. | +| **Graphics.LineCap.ROUND** | Add a circular cap to each end of the line. | +| **Graphics.LineCap.SQUARE** | Add a square line cap to each end of the line. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.lineCap = Graphics.LineCap.ROUND; +ctx.lineWidth = 10; +ctx.moveTo(100, 100); +ctx.lineTo(300, 100); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/lineCap.png b/versions/4.0/en/ui-system/components/editor/graphics/lineCap.png new file mode 100644 index 0000000000..1791251ea7 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/lineCap.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/lineJoin.md b/versions/4.0/en/ui-system/components/editor/graphics/lineJoin.md new file mode 100644 index 0000000000..cfe68c41cb --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/lineJoin.md @@ -0,0 +1,26 @@ +# Line Join + +The `lineJoin` property represents the style of the joint between two line segments. + +| Possible line join options | Description | +| :-------------- | :----------- | +| **Graphics.LineJoin.BEVEL** | Creates a bevel | +| **Graphics.LineJoin.ROUND** | Create a fillet | +| **Graphics.LineJoin.MITER** | Default. Create sharp corners | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.lineJoin = Graphics.LineJoin.ROUND; +ctx.moveTo(20,20); +ctx.lineTo(100,50); +ctx.lineTo(20,100); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/lineJoin.png b/versions/4.0/en/ui-system/components/editor/graphics/lineJoin.png new file mode 100644 index 0000000000..1e4a39e6b2 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/lineJoin.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/lineTo.md b/versions/4.0/en/ui-system/components/editor/graphics/lineTo.md new file mode 100644 index 0000000000..b74aa33b71 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/lineTo.md @@ -0,0 +1,24 @@ +# Line To + +The `lineTo()` method is used to add a new point, and then create a line from the current graphic cursor to that point. + +| Parameter | Description | +| --------- | ----------- | +| *x* | The x coordinate of the target location of the path. | +| *y* | The y coordinate of the target position of the path. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20,100); +ctx.lineTo(20,20); +ctx.lineTo(70,20); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/lineTo.png b/versions/4.0/en/ui-system/components/editor/graphics/lineTo.png new file mode 100644 index 0000000000..6fb8b8e6db Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/lineTo.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/lineWidth.md b/versions/4.0/en/ui-system/components/editor/graphics/lineWidth.md new file mode 100644 index 0000000000..c503f1e234 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/lineWidth.md @@ -0,0 +1,22 @@ +# Line Width + +The `lineWidth` defines the width of drawing line for [`stroke`](./stroke.md) function. + +| Parameter | Description +| :-------------- | :----------- | +| **number** | The width of the current line, in pixels. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.lineWidth = 20; +ctx.rect(20,20,80,100); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/lineWidth.png b/versions/4.0/en/ui-system/components/editor/graphics/lineWidth.png new file mode 100644 index 0000000000..b7c36f5d0d Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/lineWidth.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/miterLimit.md b/versions/4.0/en/ui-system/components/editor/graphics/miterLimit.md new file mode 100644 index 0000000000..1de5ab4331 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/miterLimit.md @@ -0,0 +1,30 @@ +# Miter Limit + +The `miterLimit` represents the maximum miter length. The miter length refers to the distance between the inner and outer corners of the intersection of the two lines. + +> __Note__: `miterLimit` is valid only if the `lineJoin` property is `miter`. + +When the angle of the corners is smaller, the length of the miter is greater. To avoid miter length getting too long, we can use the `miterLimit` property. + +If the miter length exceeds the value of `miterLimit`, the corners are displayed with the `bevel` type of `lineJoin`. + +| Parameter | Description | +| :-------------- | :----------- | +| **number** | Positive number. Specifies the maximum miter length. If the miter length exceeds the value of `miterLimit`, the corners are displayed with the `bevel` type of `lineJoin`. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.miterLimit = 10; +ctx.moveTo(20,20); +ctx.lineTo(100,50); +ctx.lineTo(20,100); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/miterLimit.png b/versions/4.0/en/ui-system/components/editor/graphics/miterLimit.png new file mode 100644 index 0000000000..b67cd1d12d Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/miterLimit.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/moveTo.md b/versions/4.0/en/ui-system/components/editor/graphics/moveTo.md new file mode 100644 index 0000000000..6d2685cbc7 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/moveTo.md @@ -0,0 +1,23 @@ +# Move To + +The `moveTo()` method sets the starting point of a path. + +Parameter | Description | +| :-------------- | :----------- | +| **x** | The x coordinate of the target location of the path. | +| **y** | The y coordinate of the target position of the path. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(0,0); +ctx.lineTo(300,150); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/moveTo.png b/versions/4.0/en/ui-system/components/editor/graphics/moveTo.png new file mode 100644 index 0000000000..7cb6a62156 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/moveTo.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/quadraticCurveTo.md b/versions/4.0/en/ui-system/components/editor/graphics/quadraticCurveTo.md new file mode 100644 index 0000000000..f3dcfa19f2 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/quadraticCurveTo.md @@ -0,0 +1,27 @@ +# Quadratic Curve To + +The `quadraticCurveTo()` method adds a point to the current path by using the specified control point that represents the __Quadratic Bezier curve__. + +> __Note__: the **Quadratic Bezier curve** requires two points. The first point is for the control point in the second bessel calculation, and the second point is the end point of the curve. The starting point of the curve is the last point in the current path. + +| Parameter | Description | +| :-------------- | :----------- | +| **cpx** | The x coordinate of the Bezier control point. | +| **cpy** | The y coordinate of the Bezier control point. | +| **x** | The x coordinate of the end point. | +| **y** | The y coordinate of the end point. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20,20); +ctx.quadraticCurveTo(20,100,200,20); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/quadraticCurveTo.png b/versions/4.0/en/ui-system/components/editor/graphics/quadraticCurveTo.png new file mode 100644 index 0000000000..c848e44ecd Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/quadraticCurveTo.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/rect.md b/versions/4.0/en/ui-system/components/editor/graphics/rect.md new file mode 100644 index 0000000000..3ef436f9c8 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/rect.md @@ -0,0 +1,48 @@ +# Rect + +The `rect()` method is used to create a rectangle. + +| Parameter | Description | +| :-------------- | :----------- | +| **x** | The x coordinate of the lower left point of the rectangle. | +| **y** | The y coordinate of the lower left point of the rectangle. | +| **width** | The width of rectangle. | +| **height** | The height of rectangle. | + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.rect(20,20,150,100); +ctx.stroke(); +``` + + + +```ts +const ctx = node.getComponent(Graphics); + +// Red rectangle +ctx.lineWidth = 6; +ctx.strokeColor = Color.RED; +ctx.rect(5,5,290,140); +ctx.stroke(); + +// Green rectangle +ctx.lineWidth=4; +ctx.strokeColor = Color.GREEN; +ctx.rect(30,30,50,50); +ctx.stroke(); + +// Blue rectangle +ctx.lineWidth = 10; +ctx.strokeColor = Color.BLUE; +ctx.rect(50,50,150,80); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/rect.png b/versions/4.0/en/ui-system/components/editor/graphics/rect.png new file mode 100644 index 0000000000..1d3f0513dd Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/rect.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/rect2.png b/versions/4.0/en/ui-system/components/editor/graphics/rect2.png new file mode 100644 index 0000000000..0a7e468377 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/rect2.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/stroke.md b/versions/4.0/en/ui-system/components/editor/graphics/stroke.md new file mode 100644 index 0000000000..b94cf37f14 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/stroke.md @@ -0,0 +1,19 @@ +# Stroke + +The `stroke()` method draws the previous path generated by path related functions like `lineTo`, `rect` etc. The color used is [`strokeColor`](./strokeColor.md) and the line width is defined by [`lineWidth`](./lineWidth.md). + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20, 100); +ctx.lineTo(20, 20); +ctx.lineTo(70, 20); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/stroke.png b/versions/4.0/en/ui-system/components/editor/graphics/stroke.png new file mode 100644 index 0000000000..72c89a8428 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/stroke.png differ diff --git a/versions/4.0/en/ui-system/components/editor/graphics/strokeColor.md b/versions/4.0/en/ui-system/components/editor/graphics/strokeColor.md new file mode 100644 index 0000000000..95c31cdf8c --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/graphics/strokeColor.md @@ -0,0 +1,19 @@ +# Stroke Color + +The `strokeColor` defines the color used for the [`stroke`](./stroke.md) function. + +## Example + +```ts +const ctx = node.getComponent(Graphics); +ctx.lineWidth = 2; +ctx.strokeColor = hexToColor('#0000ff'); +ctx.rect(20,20,250,200); +ctx.stroke(); +``` + + + +
+ +Return to the [Graphics Component Reference](../graphics.md) documentation. diff --git a/versions/4.0/en/ui-system/components/editor/graphics/strokeColor.png b/versions/4.0/en/ui-system/components/editor/graphics/strokeColor.png new file mode 100644 index 0000000000..d68cbd6700 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/graphics/strokeColor.png differ diff --git a/versions/4.0/en/ui-system/components/editor/label-outline.md b/versions/4.0/en/ui-system/components/editor/label-outline.md new file mode 100644 index 0000000000..c7cef8a763 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/label-outline.md @@ -0,0 +1,16 @@ +# LabelOutline Component Reference + +The __LabelOutline__ component will add an outline effect to the __Label__ component on the node. __LabelOutline__ won't have any effect on __Label__ using __BMFont__. + +![labeloutline](label/labeloutline.png) + +Select a node in the __Hierarchy__ panel, then click the __Add Component__ button at the bottom of the __Inspector__ panel and select __LabelOutline__ from __UI__. Then you can add the LabelOutline component to the node. + +**注意**: Starting from version 3.8.2, the Label component has built-in outline functionality, so there's no need to add the LabelOutline component. + +## LabelOutline Properties + +| Property | Function Explanation | +| :-------------- | :----------- | +| **Color** | The outline color | +| **Width** | The outline width | diff --git a/versions/4.0/en/ui-system/components/editor/label-shadow.md b/versions/4.0/en/ui-system/components/editor/label-shadow.md new file mode 100644 index 0000000000..b8ce8d75d5 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/label-shadow.md @@ -0,0 +1,23 @@ +# LabelShadow Component Reference + +The LabelShadow component adds a shadow effect to the Label component. + +![label-shadow](label/label-shadow.png) + +Click the **Add Component** button at the bottom of the **Inspector** panel and select **LabelShadow** from the **UI** to add the LabelShadow component to the node. + +> **Notes**: +> 1. Starting from version 3.8.2, the Label component has built-in shadow functionality, so there's no need to add the LabelOutline component. +> 2. LabelShadow can only be used for **system fonts** or **TTF fonts**. +> 3. LabelShadow does not take effect when the **CacheMode** property of the Label component is set to **CHAR**. +> 4 . LabelShadow does not support Windows and Mac native platforms. + +LabelShadow script interface, please refer to the [LabelShadow API](%__APIDOC__%/api/en/classes/ui.labelshadow.html) for details. + +## LabelShadow Properties + +| Property | Description | +| :--------- | :---------- | +| Color | The color of the shadow. | +| Offset | Offset between label and shadow. | +| Blur | A non-negative float specifying the level of shadow blur. | diff --git a/versions/4.0/en/ui-system/components/editor/label.md b/versions/4.0/en/ui-system/components/editor/label.md new file mode 100644 index 0000000000..a32c3b59f9 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/label.md @@ -0,0 +1,52 @@ +# Label Component Reference + +The __Label__ component is used to display texts, the text can use __System Font__, __TrueType Font__, or __Bitmap Font__. In addition, __Label__ also has some basic layout functionalities like alignment, line wrap, auto fit etc. + +![label-property](./label/label-property.png) + +Select a node in the __Hierarchy__ panel, then click the __Add Component__ button at the bottom of the __Inspector__ panel and select __Label__ from __2D__ to add the __Label__ component to the node. + +To use `Label`, please refer to the [Label API](%__APIDOC__%/en/class/Label) documentation and the [label](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/02.label) scene of the test-cases-3d project. + +## Label Properties + +| Property | Function Explanation | +| :-------------- | :----------- | +| **CustomMaterial** | Custom Material, please refer to [UI Custom Material](../engine/ui-material.md). | +| **Color** | Label color. | +| **String** | Text content string. | +| **HorizontalAlign** | Horizontal alignment of the text, including `LEFT`, `CENTER` and `RIGHT`. | +| **VerticalAlign** | Vertical alignment of the text, including `TOP`, `CENTER` and `BOTTOM`. | +| **FontSize** | Font size of the text. | +| **FontFamily** | Font family name (Takes effect when using __System Font__). | +| **LineHeight** | Line height of the text. | +| **Overflow** | Layout of the text. Currently supports `CLAMP`, `SHRINK` and `RESIZE_HEIGHT`. For details, see [Label Layout](#label-layout) below or [Label Layout](../engine/label-layout.md) document. | +| **EnableWrapText** | Enable or disable the text line wrap (which takes effect when the `Overflow` is set to `CLAMP` or `SHRINK`). | +| **Font** | Specify the [font assets](../../../asset/font.md) required for text rendering. To use LabelAtlas, please refer to the [LabelAtlas Asset](../../../asset/label-atlas.md) documentation for configuration.
This property can be empty if the System Font is used. | +| **UseSystemFont** | If enabled, the given __FontFamily__ will be used for rendering. | +| **CacheMode** | Text cache mode, including `NONE`, `BITMAP` and `CHAR`. Takes effect only for __System Font__ or __TTF Font__, BMFont does not require this optimization. See [Cache Mode](#cache-mode) below for details. | +| **IsBold** | If enabled, bold effect will be added to the text. (Takes effect when using __System Font__ or some __TTF Font__). Not effective when the **CacheMode** is in CHAR mode currently.| +| **IsItalic** | If enabled, italic effect will be added to the text. (Takes effect when using __System Font__ or some __TTF Font__). Not effective when the **CacheMode** is in CHAR mode currently.| +| **IsUnderline** | If enabled, underline effect will be added to the text. (Takes effect when using __System Font__ or __TTF Font__). Not effective when the **CacheMode** is in CHAR mode currently.| + +## Label Layout + +| Options | Function Explanation | +| :-------------- | :----------- | +| **CLAMP** | The text size won't zoom in/out as the __UITransform__ `contentSize` changes.
When Wrap Text is disabled, texts exceeding the width will be clipped according to the normal character layout.
When Wrap Text is enabled, it will try to wrap the text exceeding the boundaries to the next line. If the vertical space is not enough, any text out of bounds will also be hidden. | +| **SHRINK** | The text size could be scaled down as the __UITransform__ `contentSize` changes (it won't be scale up automatically, the maximum size that will show is specified by `FontSize`).
When __EnableWrapText__ is enabled, if the width is not enough, it will try to wrap the text to the next line first.
Then no matter whether the text is wrapped, if the text still exceed the `contentSize` of __UITransform__, it will be scaled down automatically to fit the boundaries.
__Note__: this mode may consumes more CPU resources when the label is refreshed. | +| **RESIZE_HEIGHT** | The __UITransform__ `contentSize` will be adapted to make sure the text is completely shown in its boundary. The developer cannot manually change the height of text in __UITransform__, it is automatically calculated by the internal algorithm. | + +## Cache Mode + +| Options | Function Explanation | +| :-------------- | :----------- | +| **NONE** | Defaults, the entire text in label will generate a bitmap. | +| **BITMAP** | Currently it behaves in the same way as __NONE__. The entire text in the Label will still generate a bitmap. As long as the requirements of Dynamic Atlas are met, the Draw Call will be merged with the other Sprite or Label in the Dynamic Atlas. Because Dynamic Atlas consume more memory, __this mode can only be used for Label with infrequently updated text__. __Note__: Similar to NONE, BITMAP will force a bitmap to be generated for each Label component, regardless of whether the text content is equivalent. If there are a lot of Labels with the same text in the scene, it is recommended to use CHAR to reuse the memory space. | +| **CHAR** | The principle of this mode is similar to BMFont, Label will cache text to a global shared bitmap in characters, each character of the same font style and font size will share the same cache globally. This mode is the most friendly to performance and memory if the characters is limited in a small set. However, there are currently restrictions on this mode, which we will optimize in subsequent releases:
1. __This mode can only be used for font style and fixed font size (by recording the fontSize, fontFamily, color, and outline of the font as key information for repetitive use of characters, other users who use special custom text formats need to be aware). And will not frequently appear with a huge amount of unused characters of Label.__ This is to save the cache, because the global shared bitmap size is __1024 * 1024__, it will only be cleared when the scene is switched. Once the bitmap is full, the newly appearing characters will not be rendered.
2. Overflow does not support SHRINK.
3. Cannot participate in dynamic atlas (multiple labels with CHAR mode enabled can still merge draw call in the case of without interrupting the rendering sequence).
4. The **IsBold**, **IsItalic** and **IsUnderline** properties are not supported currently. | + +> __Note__: __Cache Mode__ has an optimized effect for all platforms. + +## Use font assets + +By dragging the __TTF__ font asset and __BMFont__ font asset into the `Font` property in the __Inspector__ panel, the __Label__ component can alter the rendering font type. If you want to stop using a font file, use the system font again by checking __UseSystemFont__. diff --git a/versions/4.0/en/ui-system/components/editor/label/label-property.png b/versions/4.0/en/ui-system/components/editor/label/label-property.png new file mode 100644 index 0000000000..d5829c2a4b Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/label/label-property.png differ diff --git a/versions/4.0/en/ui-system/components/editor/label/label-shadow.png b/versions/4.0/en/ui-system/components/editor/label/label-shadow.png new file mode 100644 index 0000000000..de99c0f9c4 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/label/label-shadow.png differ diff --git a/versions/4.0/en/ui-system/components/editor/label/labeloutline.png b/versions/4.0/en/ui-system/components/editor/label/labeloutline.png new file mode 100644 index 0000000000..70c95a42eb Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/label/labeloutline.png differ diff --git a/versions/4.0/en/ui-system/components/editor/layout.md b/versions/4.0/en/ui-system/components/editor/layout.md new file mode 100644 index 0000000000..971929c375 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/layout.md @@ -0,0 +1,56 @@ +# Layout Component Reference + +__Layout__ is a component for UI container nodes. This component provide to the container various layout functionalities to automatically arrange all the children, so that the developer can make list, page, grid container, etc conveniently. + + ![layout](layout/layout.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI -> Layout__ to add the __Layout__ component to the node. + +To use `Layout`, please refer to the [Layout API](%__APIDOC__%/en/class/Layout) documentation and the [05.layout example](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/05.layout) of the **test-cases-3d** project. + +## Layout Properties + +| Property | Function Explanation | +| :-------------- | :----------- | +| __Type__ | Layout type, including __NONE__, __HORIZONTAL__, __VERTICAL__ and __GRID__. | +| __ResizeMode__ | Resize mode, including __NONE__, __CHILDREN__ and __CONTAINER__. | +| __PaddingLeft__ | The left padding between the children and the container frame. | +| __PaddingRight__ | The right padding between the children and the container frame. | +| __PaddingTop__ | The top padding between the children and the container frame. | +| __PaddingBottom__ | The bottom padding between the children and the container frame. | +| __SpacingX__ | The spacing between children in the horizontal layout. __NONE__ mode doesn't have this property. | +| __SpacingY__ | The spacing between children in the vertical layout. __NONE__ mode doesn't have this property. | +| __HorizontalDirection__ | When it is designated as horizontal layout, this property determines which side(left or right) does the first child node start in the layout. When the __Layout__ type is set to __GRID__, this property along with `Start Axis` property determine the starting horizontal alignment of __GRID__ layout elements. | +| __VerticalDirection__ | When it is designated as vertical layout, this property determines which side(top or bottom) does the first child node start in the layout. When the __Layout__ type is set to __GRID__, this property with `Start Axis` property determines the starting vertical alignment of __GRID__ layout elements. | +| __CellSize__ | This property is only available in __GRID__ layout, __CHILDREN__ resize mode, determines the size of each child node. | +| __StartAxis__ | This property is only available in __GRID__ layout, determines the arrangement direction of children. | +| __AffectedByScale__ | Whether the scale of the child node affects the layout. | +| __AutoAlignment__ | Auto-alignment, in `Type` type __HORIZONTAL__ or __VERTICAL__ mode, ensures that the other axial value is zero. | +| __Constraint__ | Layout constraints that constrain the number of alignments in a given direction, supporting __NONE__, __FIXED_ROW__ and __FIXED_COL__. | +| __ConstraintNum__ | The layout constraint value, valid in `Constraint` of type __FIXED_ROW__ or __FIXED_COL__ mode. | + +## Detailed Explanation + +After adding the `Layout` component, the default layout type is __NONE__, you can toggle the container alignment type by modifying `Type` in __Inspector__. The types are __HORIZONTAL__, __VERTICAL__ and __GRID__ layouts. Also, `ResizeMode` is supported for all layout types except __NONE__. + +- The modes of `Resize Mode`: + + - When __Resize Mode__ is __NONE__, the size of the container and children is independent from each other. + + - When __Resize Mode__ is __CHILDREN__, the size of the children will change with the size of the container to make sure all children fit inside the container's bounding box. + + - When __Resize Mode__ is __CONTAINER__, the size of the container will change with the size of the children to make sure the container is large enough to contain all children inside its bounding box. + + All alignments are calculated based on the container size. If the sorting needs to be fixed, set the `Type` to `Grid` and then set the `Constraint` and `ConstraintNum` to fix the sorting. + +- The modes of `Constraint`: + + - When __Constraint__ is __NONE__, the layout is free of __Constraint__. + + - When __Constraint__ is __FIXED_ROW__, a fixed number of rows is used with `ConstraintNum`. + + - When __Constraint__ is __FIXED_COL__, a fixed number of columns is used with `ConstraintNum`. + +> __Note__: if the __Layout__'s configuration is set in runtime, the results need to be updated until the next frame, unless you manually call `updateLayout` API. + +For more layout examples, please refer to the [Auto Layout](../engine/auto-layout.md) documentation for details. diff --git a/versions/4.0/en/ui-system/components/editor/layout/layout.png b/versions/4.0/en/ui-system/components/editor/layout/layout.png new file mode 100644 index 0000000000..b84347c343 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/layout/layout.png differ diff --git a/versions/4.0/en/ui-system/components/editor/mask.md b/versions/4.0/en/ui-system/components/editor/mask.md new file mode 100644 index 0000000000..6ab976a426 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/mask.md @@ -0,0 +1,78 @@ +# Mask Component Reference + +__Mask__ is used to specify the range which clip the render results of the children. A node with a __Mask__ component will use a bounding box (which is specified by the `contentSize` of the __UITransform__ component in the __Inspector__) to create a rectangular rendered mask. All child nodes of this node will only appear inside the mask's boundary. + +![mask](mask/mask.png) + +Select a node in the __Hierarchy__ panel, then click the __Add Component__ button at the bottom of the __Inspector__ panel and select __Mask__ from __UI -> Render__. Then you can add the Mask component to the node. + +After adding a Mask component, a [Graphics](./graphics.md) component will be automatically added on the same node. Please do not delete the graphics component. When the __Type__ property is __SPRITE_STENCIL__,the graphics component will be removed, and a new sprite component will be added. Please also do not delete the sprite component because it provides the shape information for the stencil. + +> __Note__: the Mask component cannot be added to a node with other renderer components such as __Sprite__, __Label__, etc. + +To use `Mask`, please refer to the [Mask API](%__APIDOC__%/en/class/Mask) documentation and the [Mask](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/08.mask) scene of the test-cases-3d project. + +## Mask Properties + +| Property | Function Explanation | +| :-------------- | :----------- | +| **Type** | Mask type, including `RECT`, `ELLIPSE`, `GRAPHICS_STENCIL`, `SPRITE_STENCIL`. | +| **Segments** | The segments for ellipse mask, which takes effect only when the Mask type is set to `ELLIPSE`. | +| **Inverted** | The Reverse mask. | + +### Type + +#### RECT + +![mask](mask/mask-rect.png) + +#### ELLIPSE + +![mask](mask/mask-ellipse.png) + +It can also be set by code at runtime. Example: + +```ts +const mask = this.getComponent(Mask); +mask.type = Mask.Type.ELLIPSE; +mask.segments = 32; +``` + +#### GRAPHICS_STENCIL + +![mask](mask/mask-graphics.png) + +It can also be set by code at runtime. Example: + +```ts +const g = this.mask.node.getComponent(Graphics); +//const g = this.mask.graphics; +g.lineWidth = 10; +g.fillColor.fromHEX('#ff0000'); +g.moveTo(-40, 0); +g.lineTo(0, -80); +g.lineTo(40, 0); +g.lineTo(0, 80); +g.close(); +g.stroke(); +g.fill(); +``` + +#### SPRITE_STENCIL + +![mask](mask/mask-image.png) + +It can also be set by code at runtime. Example: + +```ts +const mask = this.getComponent(Mask); +mask.type = Mask.Type.SPRITE_STENCIL; +const sprite = this.getComponent(Sprite); +sprite.spriteFrame = this.stencilSprite; +mask.alphaThreshold = 0.1; +``` + +> __Notes__: +> 1. After adding the __Mask__ component to a node, all nodes in the sub tree of this node will be affected by __Mask__ during rendering. +> 2. The `GRAPHICS_STENCIL` simply provides the __graphics__ component, which developers can use graphics property in the __mask__ component to draw custom graphics. But the node click events are still calculated based on the size of the node. +> 3. The `SPRITE_STENCIL` type requires a picture resource by default. If it is not set, it is equivalent to no mask. diff --git a/versions/4.0/en/ui-system/components/editor/mask/mask-ellipse.png b/versions/4.0/en/ui-system/components/editor/mask/mask-ellipse.png new file mode 100644 index 0000000000..fe95ed3311 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/mask/mask-ellipse.png differ diff --git a/versions/4.0/en/ui-system/components/editor/mask/mask-graphics.png b/versions/4.0/en/ui-system/components/editor/mask/mask-graphics.png new file mode 100644 index 0000000000..ffa6be0cae Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/mask/mask-graphics.png differ diff --git a/versions/4.0/en/ui-system/components/editor/mask/mask-image.png b/versions/4.0/en/ui-system/components/editor/mask/mask-image.png new file mode 100644 index 0000000000..e6d6174e26 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/mask/mask-image.png differ diff --git a/versions/4.0/en/ui-system/components/editor/mask/mask-rect.png b/versions/4.0/en/ui-system/components/editor/mask/mask-rect.png new file mode 100644 index 0000000000..4a754cc1f9 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/mask/mask-rect.png differ diff --git a/versions/4.0/en/ui-system/components/editor/mask/mask.png b/versions/4.0/en/ui-system/components/editor/mask/mask.png new file mode 100644 index 0000000000..2640bf6d59 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/mask/mask.png differ diff --git a/versions/4.0/en/ui-system/components/editor/pageview.md b/versions/4.0/en/ui-system/components/editor/pageview.md new file mode 100644 index 0000000000..29836fa82f --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/pageview.md @@ -0,0 +1,109 @@ +# PageView Component Reference + +The __PageView__ component is derived from __ScrollView__, the difference is that when scrolls it automatically snaps to next page of content. + +![pageview-inspector](./pageview/pageview-inspector.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/PageView__ to add the __PageView__ component to the node. + +To use `PageView`, please refer to the [PageView API](%__APIDOC__%/en/class/PageView) documentation and the [PageView](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/15.pageview) scene of the test-cases-3d project. + +## PageView Properties + +| Property | Function Description | +| -------------- | ----------- | +| *AutoPageTurningThreshold* | Auto page turning velocity threshold. When users swipe the __PageView__ quickly, it will calculate a velocity based on the scroll distance and time, if the calculated velocity is larger than the threshold, then it will trigger page turning | +| *Bounce Duration* | The elapse time of bouncing back. The value range is 0 ~ 10, and when the value is 0, it will bounce back immediately | +| *Brake* | It determines how quickly the content stop moving. A value of 1 will stop the movement immediately. A value of 0 will never stop the movement until it reaches to the boundary of page view | +| *Content* | It is a node reference that is used to contain the contents of the page view | +| *Direction* | The page view direction | +| *Elastic* | When elastic is set, the content will be bounce back when move out of boundary | +| *Indicator* | The Page View Indicator, please refer to __PageViewIndicator Setting__ below for details | +| *Inertia* | When inertia is set, the content will continue to move for a little while when touch ended | +| *PageEvents* | A list of the page view's events callback | +| *PageTurningEventTiming* | Change the timing for sending the __PAGE_TURNING__ event | +| *CancelInnerEvents* | If it's set to true, the scroll behavior will cancel touch events on inner content nodes. It's set to true by default | +| *ScrollThreshold* | This value will be multiplied by the distance between two pages, to get the threshold distance. If user scroll distance is larger than this threshold distance, the page will turn immediately | +| *SizeMode* | Specify the size type of each page in PageView, including __Unified__ and __Free__ | + +### PageViewIndicator Setting + +__PageViewIndicator__ is optional, the component is used to display the number of pages and mark the current page. + +The association can be done by dragging a node with a __PageViewIndicator__ component into the __Indicator__ property of the __PageView__ component in the __Hierarchy__ panel. + +### PageView Event + +![pageview-event](./pageview/pageview-event.png) + +| Property | Function Description | +| -------------- | ----------- | +| *Target* | Node with script component | +| *Component* | Script component name | +| *Handler* | Specifies a callback function that will be called when the __PageView__ event occurs | +| *CustomEventData* | The user specifies an arbitrary string as the last parameter of the event callback | + +The __PageView__ event callback has two parameters, the first parameter is the __PageView__ itself, the second parameter is the event type of the __PageView__. + +## Detailed Explanation + +The __PageView__ component must have the specified content node to work. Each child node in content is a separate page, and the size of each page is the size of the __PageView__ node. If the node size is larger than the content size, it may result in an incomplete scroll. Under the __PageView__ component there is a node object, which combines with `ScrollThreshold` to determine whether the current sliding distance is such that the page can be turned. The operation effect is divided into two kinds: + +- __Slow sliding__ - by dragging the page in the view to reach the specified `ScrollThreshold` value (the value is the percentage of page size), after the release will automatically slide to the next page. +- __Fast sliding__ - quickly drag in one direction, automatically slide to the next page. Only slide up to one page at a time. + +## Add callback via script code + +### Method one + +The event callback added by this method is the same as the event callback added by the editor, all added by code. First you need to construct a `EventHandler` object, and then set the corresponding `target`, `component`, `handler` and `customEventData` parameters. + +```ts +import { _decorator, Component, Event, Node, PageView, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad(){ + const pageChangedEventHandler = new EventHandler(); + // This Node is the node to which your event processing code component belongs + pageChangedEventHandler.target = this.node; + // This is the script class name + pageChangedEventHandler.component = 'example'; + pageChangedEventHandler.handler = 'callback'; + pageChangedEventHandler.customEventData = 'foobar'; + + const page = this.node.getComponent(PageView); + page.clickEvents.push(pageChangedEventHandler); + } + + callback(event: Event, customEventData: string){ + // The event here is a Touch Event object, and you can get the send node of the event by event.target + const node = event.target as Node; + const pageview = node.getComponent(PageView); + console.log(customEventData); // foobar + } +} +``` + +### Method two + +By `pageView.node.on('page-turning', ...)` way to add. + +```ts +// Suppose we add event handling callbacks to the onLoad method of a component and perform event handling in the callback function: + +import { _decorator, Component, Event, Node, PageView } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad(){ + this.pageView.node.on('page-turning', this.callback, this); + } + + callback(pageView: PageView){ + // The parameter of the callback is the PageView component. Note that events registered this way cannot pass customEventData + } +} +``` diff --git a/versions/4.0/en/ui-system/components/editor/pageview/pageview-event.png b/versions/4.0/en/ui-system/components/editor/pageview/pageview-event.png new file mode 100644 index 0000000000..4c4cad833b Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/pageview/pageview-event.png differ diff --git a/versions/4.0/en/ui-system/components/editor/pageview/pageview-hierarchy.png b/versions/4.0/en/ui-system/components/editor/pageview/pageview-hierarchy.png new file mode 100644 index 0000000000..311908849a Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/pageview/pageview-hierarchy.png differ diff --git a/versions/4.0/en/ui-system/components/editor/pageview/pageview-inspector.png b/versions/4.0/en/ui-system/components/editor/pageview/pageview-inspector.png new file mode 100644 index 0000000000..5ead0e326b Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/pageview/pageview-inspector.png differ diff --git a/versions/4.0/en/ui-system/components/editor/pageviewindicator.md b/versions/4.0/en/ui-system/components/editor/pageviewindicator.md new file mode 100644 index 0000000000..e7121a9d05 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/pageviewindicator.md @@ -0,0 +1,22 @@ +# PageViewIndicator Component Reference + +The __PageViewIndicator__ component is used to display the current page number of the __PageView__ and the page where the tag is currently located. + +![pageviewindicator.png](./pageviewindicator/pageviewindicator.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/PageViewIndicator__ to add the __PageViewIndicator__ component to the node. + +To use `PageView`, please refer to the [PageView API](%__APIDOC__%/en/class/PageViewIndicator) documentation and the [PageView](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/15.pageview) scene of the test-cases-3d project. + +## PageviewIndicator Properties + +| Property | Function Description | +| ----------- | ----------- | +| **CellSize** | The cellsize for each element | +| **Direction** | The location direction of __PageViewIndicator__, including __HORIZONTAL__ and __VERTICAL__ | +| **Spacing** | The distance between each element | +| **SpriteFrame** | The spriteFrame for each element | + +## Detailed Explanation + +__PageViewIndicator__ is not used alone, it needs to be used with `PageView` to create a tag of the number of pages corresponding to the page. When you slide to a page, __PageViewIndicator__ will highlight its corresponding mark. diff --git a/versions/4.0/en/ui-system/components/editor/pageviewindicator/pageviewindicator.png b/versions/4.0/en/ui-system/components/editor/pageviewindicator/pageviewindicator.png new file mode 100644 index 0000000000..c1a901fbe8 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/pageviewindicator/pageviewindicator.png differ diff --git a/versions/4.0/en/ui-system/components/editor/progress.md b/versions/4.0/en/ui-system/components/editor/progress.md new file mode 100644 index 0000000000..5226345433 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/progress.md @@ -0,0 +1,27 @@ +# ProgressBar Component Reference + +__ProgressBar__ is usually used to show the progress of a certain operation in the game. Add the __ProgressBar__ component to a node and associate a __Bar Sprite__ to this component. Then the __Bar Sprite__ can be controlled to show progression in the scene. + +![add-progressbar](progress/add-progressbar.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/ProgressBar__ to add the __ProgressBar__ component to the node. + +To use `Progress`, please refer to the [Mask API](%__APIDOC__%/en/class/ProgressBar) documentation and the [Progress](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/11.progress) scene of the test-cases-3d project. + +## ProgressBar Properties + +| Property | Function Explanation | +| :-------------- | :----------- | +| **BarSprite** | The __Sprite__ component needed for rendering __ProgressBar__. It can be linked by dragging a node with the __Sprite__ component to this property | +| **Mode** | Currently supports the __HORIZONTAL__, __VERTICAL__ and __FILLED__ modes. The initial direction can be changed by cooperating with the __reverse__ property | +| **Progress** | Floating point. The value range is __0~1__, and values outside the range are not allowed. | +| **Reverse** | Boolean value. The default fill direction is from left to right / bottom to top, when enable, it becomes right to left / top to bottom | +| **Total Length** | The total length / total width of the __BarSprite__ when the __ProgressBar__ is at __100%__. In __FILLED__ mode, __Total Length__ represents the percentage of the total display range for __Bar Sprite__, with values ranging from __0 to 1__ | + +## Detailed Explanation + +After adding the ProgressBar component, drag a node with the __Sprite__ component from the __Hierarchy__ to the BarSprite property. Then you can control the display of the ProgressBar by dragging the progress sliding block. + +Bar Sprite could be its own node, child node or any node that comes with the __Sprite__ component. Also, Bar Sprite can freely choose the `SIMPLE`, `SLICED` or `FILLED` render types. + +If the mode of the progress bar is __FILLED__, the __Type__ of __BarSprite__ should to be set to __FILLED__, otherwise a warning will appear. diff --git a/versions/4.0/en/ui-system/components/editor/progress/add-progressbar.png b/versions/4.0/en/ui-system/components/editor/progress/add-progressbar.png new file mode 100644 index 0000000000..15c6e2ae23 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/progress/add-progressbar.png differ diff --git a/versions/4.0/en/ui-system/components/editor/render-component.md b/versions/4.0/en/ui-system/components/editor/render-component.md new file mode 100644 index 0000000000..7daa3ae1e7 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/render-component.md @@ -0,0 +1,13 @@ +# 2D Renderable Components + +- [Sprite Component Reference](sprite.md) +- [Label Component Reference](label.md) +- [Mask Component Reference](mask.md) +- [Graphics Component Reference](graphics.md) +- [RichText Component Reference](richtext.md) +- [UIStaticBatch Component Reference](ui-static.md) +- [TiledMap Component Reference](../../../editor/components/tiledmap.md) +- [TiledTile Component Reference](../../../editor/components/tiledtile.md) +- [Spine Skeleton Component Reference](../../../editor/components/spine.md) +- [DragonBones ArmatureDisplay Component Reference](../../../editor/components/dragonbones.md) +- [MotionStreak Component Reference](../../../editor/components/motion-streak.md) diff --git a/versions/4.0/en/ui-system/components/editor/renderroot2d.md b/versions/4.0/en/ui-system/components/editor/renderroot2d.md new file mode 100644 index 0000000000..3928b59947 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/renderroot2d.md @@ -0,0 +1,28 @@ +# RenderRoot2D + +![renedertoot2d.png](./renderroot2d/renedertoot2d.png) + +The RenderRoot2D component is the parent class of [Canvas](./canvas.md), any 2D/UI elements that need to renderer should be attached under a node with the RenderRoot2D component. + +The RenderRoot2D collects information from 2D/UI components in its children and submits it to the render pipeline. 3D Objects, like mesh, even if it is under the RenderRoot2D node, will not be collected by the RenderRoot2D component. + +The UITransform component is required and automatically added when add a RenderRoot2D component to the node. + +There are two typical usages of the RenderRoot2D component. + +- Inherited by other class like [Canvas](./canvas.md) to handle the render of 2D/UI elements, and screen adaptation. +- Draw 2D/UI elements in the 3D Space. + +To make A 3D Label like a player's name, you can use a RenderRoot2D component as its parent. + +It can be noticed that, when creating a 2D element, Cocos Creator will automatically create a Canvas node. + +![canvas-label.png](./renderroot2d/canvas-label.png) + +So, to create a Label in the 3D space, after the creation, delete the Canvas and Camera node, and drag the Label to the RenderRoot2D as a child node to make it visible. + +![3dui.png](./renderroot2d/3dui.png) + +Normally, the scene camera(Main Camera) will not render nodes with layer UI_2D. For 2D/UI nodes to be displayed in the 3D space, please make sure they have the right visibility layer such as UI_3D. + +![preview.png](./renderroot2d/preview.png) diff --git a/versions/4.0/en/ui-system/components/editor/renderroot2d/3dui.png b/versions/4.0/en/ui-system/components/editor/renderroot2d/3dui.png new file mode 100644 index 0000000000..58a3c86e8b Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/renderroot2d/3dui.png differ diff --git a/versions/4.0/en/ui-system/components/editor/renderroot2d/canvas-label.png b/versions/4.0/en/ui-system/components/editor/renderroot2d/canvas-label.png new file mode 100644 index 0000000000..01acb65c79 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/renderroot2d/canvas-label.png differ diff --git a/versions/4.0/en/ui-system/components/editor/renderroot2d/create-label.png b/versions/4.0/en/ui-system/components/editor/renderroot2d/create-label.png new file mode 100644 index 0000000000..280d4c5857 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/renderroot2d/create-label.png differ diff --git a/versions/4.0/en/ui-system/components/editor/renderroot2d/preview.png b/versions/4.0/en/ui-system/components/editor/renderroot2d/preview.png new file mode 100644 index 0000000000..119ad685fa Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/renderroot2d/preview.png differ diff --git a/versions/4.0/en/ui-system/components/editor/renderroot2d/renedertoot2d.png b/versions/4.0/en/ui-system/components/editor/renderroot2d/renedertoot2d.png new file mode 100644 index 0000000000..df28e8e531 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/renderroot2d/renedertoot2d.png differ diff --git a/versions/4.0/en/ui-system/components/editor/richText/example-components.png b/versions/4.0/en/ui-system/components/editor/richText/example-components.png new file mode 100644 index 0000000000..f1dbc6d801 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/richText/example-components.png differ diff --git a/versions/4.0/en/ui-system/components/editor/richText/example-console.png b/versions/4.0/en/ui-system/components/editor/richText/example-console.png new file mode 100644 index 0000000000..0522245ccd Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/richText/example-console.png differ diff --git a/versions/4.0/en/ui-system/components/editor/richText/example-running.png b/versions/4.0/en/ui-system/components/editor/richText/example-running.png new file mode 100644 index 0000000000..c195175b0e Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/richText/example-running.png differ diff --git a/versions/4.0/en/ui-system/components/editor/richText/example-string.png b/versions/4.0/en/ui-system/components/editor/richText/example-string.png new file mode 100644 index 0000000000..76f2f6d6f4 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/richText/example-string.png differ diff --git a/versions/4.0/en/ui-system/components/editor/richText/richtext.png b/versions/4.0/en/ui-system/components/editor/richText/richtext.png new file mode 100644 index 0000000000..6b59474f1c Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/richText/richtext.png differ diff --git a/versions/4.0/en/ui-system/components/editor/richtext.md b/versions/4.0/en/ui-system/components/editor/richtext.md new file mode 100644 index 0000000000..8262778bec --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/richtext.md @@ -0,0 +1,146 @@ +# RichText Component Reference + +RichText component could be used for displaying a string with multiple styles. You could customize the text style of each text segment with a few simple BBCode. + +The currently supported tags are: `color`, `size`, `outline`, `b`, `i`, `u`, `br`, `img` and `on`, these tags could also be nested within each other. + +For more information about BBCode, please refer to the __BBCode format__ section below. + +![richtext](richText/richtext.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __RichText__ from __UI -> Render__ to add the RichText component to the node. + +To use `RichText`, please refer to the [RichText API](%__APIDOC__%/en/class/RichText) documentation and the [RichText](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/07.richtext) scene of the test-cases-3d project. + +## RichText Properties + +| Property | Function Explanation | +| -------------- | ----------- | +| Font | Custom TTF font of __RichText__, all the label segments will use the same custom TTF font. | +| FontSize | Font size, in points (__Note__: this field does not affect the font size set in BBCode.) | +| HandleTouchEvent | Once checked, the __RichText__ will block all input events (mouse and touch) within the bounding box of the node, preventing the input from penetrating into the underlying node. | +| Horizontal Align | Horizontal alignment | +| Vertical Align | Vertical alignment | +| ImageAtlas | The image atlas for the `img` tag. For each `src` value in the `img` tag, there should be a valid `spriteFrame` in the imageAtlas. | +| Font Family | Custom system font of RichText. | +| Use System Font | Whether to use the system default font. | +| MaxWidth | The maximize width of __RichText__, set to 0 means the maximize width is not limited. | +| LineHeight | Line height, in points. | +| String | Text of the __RichText__, you could use BBcode in the string. | + +## BBCode format + +### Basics + +Currently the supported tag list is: `size`, `color`, `b`, `i`, `u`, `img` and `on`. There are used for customizing the font size, font color, bold, italic, underline, image and click event. +Every tag should has a start tag and an end tag. The name and attribute format of the start tag must meet the requirements and be all lowercase. + +It will check the start tag name, but the end tag name restrict is loose, it only requires a `` tag, the end tags name doesn't matter. + +Here is an example of the `size` and `color` tag: + +`Hello, Creator` + +### Supported tags + +> __Note__: all tag names should be lowercase and the attribute assignment should use `=` sign. + +| Name | Description | Example | Note | +| :------ |:------ | :----| :----- | +| color | Specify the font rendering color, the color value could be a built-in value or a hex value. eg, use `#ff0000` for red. | `Red Text` | | +| size | Specify the font rendering size, the size should be a integer. | `enlarge me` | The size assignment should use `=` sign. | +| outline | Specify the font outline, you could customize the outline color and width by using the `color` and `width` attribute. | `A label with outline` | If you don't specify the color or width of outline, the default color value is `#ffffff` and the default width is `1`. | +| b | Render text as bold font | `This text will be rendered as bold`| The tag name must be lowercase and cannot be written in `bold`. | +| i | Render text as italic font | `This text will be rendered as italic`| The tag name must be lowercase and cannot be written in `italic`. | +| u | Add a underline to the text |`This text will have a underline`| The tag name must be lowercase and cannot be written in `underline`. | +| on | Specify a event callback to a text node, when you click the node, the callback will be triggered. | ` click me! ` | Except for `on` tag, `color` and `size` tags can also add `click` event parameter. eg. `click me` | +| param | When the click event is triggered, the value can be obtained in the second attribute of the callback function. | ` click me! ` | Depends on the click event. | +| br | Insert a empty line | `
` | `

` and `
` are both invalid tags. | +| img | Add image and emoji support to your __RichText__. The emoji name should be a valid sprite frame name in the ImageAtlas. | `` | Only `` is a valid img tag. If you specify a large emoji image, it will scale the sprite height to the line height of the __RichText__ together with the sprite width. | + +#### Optional attributes of img tag + +For better typography, additional optional attributes to the img tag have been provided. Developers can use `width`, `height` to specify the size of the SpriteFrame. This will allow the image to be larger or smaller than the line height (but it will not change the line height). + +When the height or width of the SpriteFrame changes, The `align` attribute may need to be used to adjust the alignment of the image in the line. + +| Attribute | Description | Example | Note | +| :-------- | :---------- | :------ | :----- | +| height | Specify the SpriteFrame height size, the size should be a integer.| `` | If you only assign height the SpriteFrame will auto keep aspect-ratio +| width | Specify the SpriteFrame width size, the size should be a integer.| `` | Use both Height and Width `` +| align | Specify the SpriteFrame alignment in line, the value should be `bottom`, `top` or `center`.| `` | Default SpriteFrame alignment will be bottom + +To support custom image layout, the `offset` attribute can be used to fine-tune the position of the specified SpriteFrame in the RichText component. When setting the `offset` attribute, keep in mind that the value must be an integer, and it will cause the image to overlap the text if it is not set properly. + +| `offset` attribute | Example | Description | Note | +| :-------- | :------ | :---------- | :----- | +| Y | `` | Specify the SpriteFrame to offset y + 5 | If offset only set one Integer value it's will be offset Y +| Y | `` | Specify the SpriteFrame to offset y - 5 | Use minus to decrease Y position +| X, Y | `` | Specify the SpriteFrame to offset x + 6 and y - 5 | The offset values should only contains `0-9`, `-` and `,` characters + +### Nested Tags + +Tags could be nested, the rules is the same as normal HTML tags. For example, the following settings will render a label with font size 30 and color value green. + +`I'm green` + +is equal to: + +`I'm green` + +## Cache Mode + +Since the RichText component is assembled from multiple Label nodes, the number of drawcalls for complex rich text will also be high. Therefore, the engine provides the CacheMode setting of the Label component for the RichText component to avoid the increase of drawcall. For a detailed description of each cache type, please refer to the [Cache Mode of the Label component](./label.md) documentation. + +| Mode | Description | +| :-------------- | :----------- | +| **NONE** | By default, for each Label node created by RichText, set its CacheMode to NONE, that is, generate a bitmap of the entire text of each Label and render it separately. | +|**BITMAP**| After selection, for each Label node created by RichText, set its CacheMode to BITMAP type, that is, generate a bitmap of the entire text of each Label, and add the bitmap to the dynamic atlas, and then according to the dynamic atlas to assemble and render. | +| **CHAR** | After selection, each Label node created by RichText has its CacheMode set to CHAR type, that is, the text of each Label is cached in a globally shared bitmap in "words". Each of the same font style and size is Characters will share a cache globally. | + +> **Note**: the **RenderTexture** module in the **Project -> Project Settings -> Module Config** panel cannot be removed when using the cache mode. + +## Detailed Explanation + +The __RichText__ component is implemented in the JavaScript layer and uses the Label node as the rendering part. All the layout logic goes also in JavaScript layer. This means if you create a very complex __RichText__, it will end up with many label node created under the hook. And all these label node are using system font for rendering. + +Avoid modifying the __RichText__ content frequently in the game's main loop, which can lead to lower performance. Also, try to use the normal Label component instead of the __RichText__ component if possible, and BMFont is the most efficient. + +## Example + +### on + +Let's take the `on` tag as an example. Please create a new RichText node, and a custom script component, and attach it to the rich text node. + +![richText](./richText/example-components.png) + +Copy the following text to the **String** property of the RichText Component. + +```html +Click Me! +``` + +![example-string.png](./richText/example-string.png) + +Copy the following code to your custom script. + +```ts +import { _decorator, Component, EventTouch } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('ClickEventHandler') +export class ClickEventHandler extends Component { + + onClicked(eventTouch:EventTouch, param:string){ + console.log("onClicked", param); + } +} +``` + +Then run the game. + +![example-running.png](./richText/example-running.png) + +Click the RichText on the screen, and you can see the following output in the console. + +![example-console.png](./richText/example-console.png) \ No newline at end of file diff --git a/versions/4.0/en/ui-system/components/editor/safearea.md b/versions/4.0/en/ui-system/components/editor/safearea.md new file mode 100644 index 0000000000..b5a3fa7dfb --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/safearea.md @@ -0,0 +1,15 @@ +# SafeArea component reference + +This component is used to adjust the layout of current node to respect the safe area of a notched mobile device such as the **iPhone X**. + +It is typically used for the top node of the UI interaction area. For specific usage, refer to the [SafeArea](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/23.safe-area) example in the example-cases. + +The concept of safe area is to give you a fixed inner rectangle in which you can safely display content that will be drawn on screen. + +It is strongly discouraged from providing controls outside of this area. But the screen background could embellish edges. + +![Renderings](./safearea/renderings.png) + +The developer only needs to add the component on the node, without any settings. This component internally uses the API `sys.getSafeAreaRect` to obtain the safe area of the current iOS or Android device and implements the adaptation by using the Widget component and set anchor. (If there is no Widget component, it will be added automatically) + +Please refer to the [SafeArea API](%__APIDOC__%/en/class/SafeArea). diff --git a/versions/4.0/en/ui-system/components/editor/safearea/renderings.png b/versions/4.0/en/ui-system/components/editor/safearea/renderings.png new file mode 100644 index 0000000000..d17a5cba75 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/safearea/renderings.png differ diff --git a/versions/4.0/en/ui-system/components/editor/scroll/scrollbar.png b/versions/4.0/en/ui-system/components/editor/scroll/scrollbar.png new file mode 100644 index 0000000000..88b1b0af6c Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/scroll/scrollbar.png differ diff --git a/versions/4.0/en/ui-system/components/editor/scroll/scrollview-content.png b/versions/4.0/en/ui-system/components/editor/scroll/scrollview-content.png new file mode 100644 index 0000000000..83bc2c36fb Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/scroll/scrollview-content.png differ diff --git a/versions/4.0/en/ui-system/components/editor/scroll/scrollview-event.png b/versions/4.0/en/ui-system/components/editor/scroll/scrollview-event.png new file mode 100644 index 0000000000..f564ed3e69 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/scroll/scrollview-event.png differ diff --git a/versions/4.0/en/ui-system/components/editor/scroll/scrollview-hierarchy.png b/versions/4.0/en/ui-system/components/editor/scroll/scrollview-hierarchy.png new file mode 100644 index 0000000000..757b0c79a0 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/scroll/scrollview-hierarchy.png differ diff --git a/versions/4.0/en/ui-system/components/editor/scroll/scrollview-inspector.png b/versions/4.0/en/ui-system/components/editor/scroll/scrollview-inspector.png new file mode 100644 index 0000000000..29d452dddb Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/scroll/scrollview-inspector.png differ diff --git a/versions/4.0/en/ui-system/components/editor/scrollbar.md b/versions/4.0/en/ui-system/components/editor/scrollbar.md new file mode 100644 index 0000000000..db44f6393f --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/scrollbar.md @@ -0,0 +1,24 @@ +# ScrollBar Component Reference + +The ScrollBar allows the user to scroll by dragging a sliding block. It's a bit similar to the __Slider__ component, but it is mostly used as a part of the __ScrollView__ while __Slider__ is used independently to set values. + +![scrollbar.png](scroll/scrollbar.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/ScrollBar__ to add the ScrollBar component to the node. + +Please refer to the [ScrollBar API](%__APIDOC__%/en/class/ScrollBar). + +## ScrollBar Properties + +| Property | Function Explanation | +| :-------------- | :----------- | +| **AutoHideTime** | Time to hide the __ScrollBar__ automatically, need to set `Enable Auto Hide` to true for it to take effect | +| **Direction** | Scroll direction, including __HORIZONTAL__ and __VERTICAL__ +| **EnableAutoHide** | Enable or disable auto hide. If this property is enabled, the __ScrollBar__ will automatically disappear after actions stopped for __AutoHideTime__. | +| **Handle** | ScrollBar foreground picture. The length/width will be calculated according to the content size of ScrollView and the dimensions of the actual display area | + +## Detailed Explanation + +The ScrollBar normally is used together with `ScrollView` instead of being used alone. Also, ScrollBar needs to assign a `Sprite` component, i.e. `Handle` in the Inspector panel. + +Normally we will also designate a background image to ScrollBar. This can be used to indicate the length or width of the whole ScrollBar. diff --git a/versions/4.0/en/ui-system/components/editor/scrollview.md b/versions/4.0/en/ui-system/components/editor/scrollview.md new file mode 100644 index 0000000000..0c999cd1a1 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/scrollview.md @@ -0,0 +1,111 @@ +# ScrollView Component Reference + +__ScrollView__ is a container with a scroll function. It provides a way to browse more contents within a limited display area. Generally, ScrollView will be used along with the [__Mask__ component](./mask.md) and the [__ScrollBar__ component](./scrollbar.md) can also be added to show the current offset location within the browsing content. + +![scrollview-content](scroll/scrollview-content.png) + +![scrollview-inspector](scroll/scrollview-inspector.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/ScrollView__ to add the ScrollView component to the node. + +To use `ScrollView`, please refer to the [ScrollView API](%__APIDOC__%/en/class/ScrollView) documentation and the [ScrollView](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/06.scrollview) scene of the test-cases-3d project. + +## ScrollView Properties + +| Property | Function Explanation | +| :------------- | :---------- | +| BounceDuration | Floating point number, the time duration for bounce back. The value range is __0-10__ | +| Brake | Floating point number, the deceleration coefficient after scrolling. The value range is __0-1__. If set to 1, then the scrolling will stop immediately, if set to 0, then the scrolling will continue until the content border | +| CancelInnerEvents | If it is set to true, then the scroll behavior will cancel the touch events registered on the child node. The default setting is true | +| Content | A reference node for the inner content of the __ScrollView__. It could be a node containing a very large image or long texts. | +| Elastic | Boolean value, whether to bounce back or not while scroll to the border. | +| Horizontal | Boolean value, whether horizontal scroll is allowed or not | +| HorizontalScrollBar | A reference node for creating a scroll bar showing the horizontal position of the content | +| Inertia | Is there an accelerating velocity when scrolling | +| ScrollEvents | Default list type is null. Each event added by the user is composed of the node reference, component name and a response function. Please see the __ScrollView Event__ section below for details | +| Vertical | Boolean value, whether vertical scroll is allowed or not | +| VerticalScrollBar | A reference node for creating a scroll bar showing vertical position of the contents | + +### ScrollView Event + +![scrollview-event](scroll/scrollview-event.png) + +For event structure you can refer to the [Button](./button.md) documentation. + +The ScrollView event callback will have two parameters, the first one is the ScrollView itself and the second one is the event type of ScrollView. + +### ScrollBar Setting + +ScrollBar is optional. You can choose to set either a Horizontal ScrollBar or a Vertical ScrollBar or of course set them both. + +To build a connection, you can drag a node with the ScrollBar component in the __Hierarchy__ over to the corresponding field in ScrollView. + +## Detailed Explanation + +The ScrollView component can only work with the specified content node. It calculates location information during scrolling using both the designated scroll direction and the length of the content node in this direction. + +The Content node can also set up the auto resize by adding a `Widget`, or it can arrange the layout of child nodes by adding a `Layout`, but these two components should not be added to a node at the same time to avoid unintentional consequences. + +Normally a ScrollView node tree resembles the following: + +![scrollview-hierarchy](scroll/scrollview-hierarchy.png) + +The `view` here is used to define a scroll area that can be displayed. As a result, the [Mask Component](./mask.md) will normally be added to the `view`. Contents that can scroll can be put in the content node or added to its child node. + +## Add a callback through the script code + +### Method one + +The event callback added by this method is the same as the event callback added by the editor, all added by code. First you need to construct a `EventHandler` object, and then set the corresponding `target`, `component`, `handler` and `customEventData` parameters. + +```ts +import { _decorator, Component, ScrollView, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad() { + const scrollViewEventHandler = new EventHandler(); + // This node is the node to which your event handler code component belongs + scrollViewEventHandler.target = this.node; + // This is the script class name + scrollViewEventHandler.component = 'example'; + scrollViewEventHandler.handler = 'callback'; + scrollViewEventHandler.customEventData = 'foobar'; + + const scrollview = this.node.getComponent(ScrollView); + scrollview.scrollEvents.push(scrollViewEventHandler); + } + + callback(scrollview, eventType, customEventData){ + // here scrollview is a ScrollView component object instance + // here the eventType === ScrollView.EventType enum + // here the customEventData parameter is equal to the "foobar" you set before + } +} +``` + +### Method two + +By `scrollview.node.on('scroll-to-top', ...)` way to add. + +```js +// Suppose we add an event handler callback to the onLoad method of a component and handle the event in the callback function: +import { _decorator, Component, ScrollView } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + @property(ScrollView) + scrollview: ScrollView | null = null; + onLoad(){ + this.scrollview.node.on('scroll-to-top', this.callback, this); + } + + callback(scrollView: ScrollView) { + // The callback parameter is the ScrollView component, note that events registered this way cannot pass customEventData. + } +} +``` + +Similarly, you can register events such as `scrolling`, `touch-up`, `scroll-began`, etc. The parameters of the callback function for these events are consistent with the parameters of `scroll-to-top`. diff --git a/versions/4.0/en/ui-system/components/editor/slider.md b/versions/4.0/en/ui-system/components/editor/slider.md new file mode 100644 index 0000000000..9cd19d6586 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/slider.md @@ -0,0 +1,93 @@ +# Slider Component Reference + +Slider is a component for the production of UI components such as volume adjustment. + +![slider-inspector](slider/slider-inspector.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/Slider__ to add the Slider component to the node. + +To use `Slider`, please refer to the [Slider API](%__APIDOC__%/en/class/Slider) documentation and the [Slider](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/10.slider) scene of the test-cases-3d project. + +## Slider Properties + +| Property | Description | +| :------------- | :---------- | +| Handle | The button part of the __Slider__ that allows to adjust value by sliding the button | +| Direction | The direction of the slider, including __Horizontal__ and __Vertical__ | +| Progress | Current progress value, the value range is 0 ~ 1 | +| SlideEvents | __Slider__ component event callback function | + +## Slider Event + +![slider-event](slider/slider-event.png) + +For event structure you can refer to the [Button](./button.md) documentation. + +The __Slider__ event callback has two parameters, the first one is the Slider itself and the second is the `customEventData`. + +## Detailed Explanation + +The __Slider__ is usually used to adjust the value of the UI (for example, volume adjustment), and its main component is a slider button, which is used for user interaction. You can adjust the value of the __Slider__ through this part. + +Usually a __Slider__ node tree as shown below: + +![slider-hierarchy](slider/slider-hierarchy.png) + +## Add a callback by script code + +### Method one + +The event callback added by this method is the same as the event callback added by the editor, all added by code. First you need to construct a `EventHandler` object, and then set the corresponding `target`, `component`, `handler` and `customEventData` parameters. + +```ts +import { _decorator, Component, Event, Node, Slider, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + + onLoad () { + const sliderEventHandler = new EventHandler(); + // This Node is the node to which your event processing script component belongs + sliderEventHandler.target = this.node; + // This is the script class name + sliderEventHandler.component = 'example'; + sliderEventHandler.handler = 'callback'; + sliderEventHandler.customEventData = 'foobar'; + + const slider = this.node.getComponent(Slider); + slider!.slideEvents.push(sliderEventHandler); + } + + callback(slider: Slider, customEventData: string) { + // The event here is a Touch Event object, and you can get the send node of the event by event.target + // The customEventData parameter here is equal to the "foobar" you set before + } +} +``` + +### Method two + +By `slider.node.on('slide', ...)` way to add. + +```ts +// Suppose we add event handling callbacks to the onLoad method of a component and perform event handling in the callback function + +import { _decorator, Component, Slider } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + + @property(Slider) + slider: Slider | null = null; + + onLoad () { + this.slider!.node.on('slide', this.callback, this); + } + + callback(slider: Slider) { + // The parameter of the callback is the Slider component. Note that events registered this way cannot pass "customEventData" + } +} +``` diff --git a/versions/4.0/en/ui-system/components/editor/slider/slider-content.png b/versions/4.0/en/ui-system/components/editor/slider/slider-content.png new file mode 100644 index 0000000000..31e14d5087 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/slider/slider-content.png differ diff --git a/versions/4.0/en/ui-system/components/editor/slider/slider-event.png b/versions/4.0/en/ui-system/components/editor/slider/slider-event.png new file mode 100644 index 0000000000..b85551981e Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/slider/slider-event.png differ diff --git a/versions/4.0/en/ui-system/components/editor/slider/slider-hierarchy.png b/versions/4.0/en/ui-system/components/editor/slider/slider-hierarchy.png new file mode 100644 index 0000000000..ef11b6224e Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/slider/slider-hierarchy.png differ diff --git a/versions/4.0/en/ui-system/components/editor/slider/slider-inspector.png b/versions/4.0/en/ui-system/components/editor/slider/slider-inspector.png new file mode 100644 index 0000000000..88d4c04771 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/slider/slider-inspector.png differ diff --git a/versions/4.0/en/ui-system/components/editor/sprite.md b/versions/4.0/en/ui-system/components/editor/sprite.md new file mode 100644 index 0000000000..c43fc9a7dc --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/sprite.md @@ -0,0 +1,84 @@ +# Sprite Component Usage Example + +![sprite](sprite/sprite-component.png) + +A Sprite is typically a 2D image rendering component that reads texture colors from image resources to render game backgrounds or UI elements. Follow these steps to quickly create a game background displaying image colors in a scene: + +1. **Add a Sprite**: Add a child node to a node that already has a Canvas component in the scene. Set this child node's Layer property to `Layers.Enum.UI_2D`, then add a Sprite component to this child node. + +2. **Add spriteFrame**: Set a SpriteFrame type resource from the Asset Manager as the Sprite component's spriteFrame property to render the game background color. + +3. **Add Solid Color spriteFrame**: To create a solid color background (e.g., cyan background), use the default_sprite_splash.png SpriteFrame resource from the default_ui folder in the internal built-in resource library as the Sprite component's spriteFrame. + +4. **Modify Sprite Node Size**: After adding a SpriteFrame type resource to the Sprite component's spriteFrame property, the Sprite component will read the width and height values of the spriteFrame resource and set them to the node's UITransform component's contentSize property. When the Sprite component's Size Mode is Trimmed or Raw, it will use the spriteFrame resource's dimensions as the node's size, overriding the node's original size. If the Size Mode property is Custom, it will not change the node's dimensions. + +5. **Modify Sprite Color**: If the Sprite's spriteFrame property is not empty, modifying the Sprite component's color property will change the Sprite component's final display color. + +## Manually Adding Sprite Components in the Editor + +Click the **Add Component** button on the node's **Property Inspector**, then select **2D/Sprite** to add a Sprite component to the node. At this point, the Sprite component's spriteFrame property is empty and cannot display any image. Developers need to manually add any SpriteFrame type resource to the spriteFrame property for rendering. + +![editor add sprite](sprite/sprite_editor_add.png) + +## Sprite Properties + +| Property | Description | +| :-------------- | :----------- | +| CustomMaterial | Custom material. For usage, refer to [Custom Materials](../engine/ui-material.md) | +| Color | Image color | +| Sprite Atlas | The atlas to which the Sprite's displayed image resource belongs (refer to [Atlas](../../../asset/atlas.md)). | +| Sprite Frame | The [SpriteFrame image resource](../../../asset/sprite-frame.md) used to render the Sprite. | +| Grayscale | Grayscale mode. When enabled, the Sprite will be rendered in grayscale. | +| Size Mode | Specifies the Sprite's size:
**Trimmed**: Uses the original image resource's dimensions after trimming transparent pixels
**Raw**: Uses the original image's untrimmed dimensions
**Custom**: Uses custom dimensions. When the user manually modifies the **Size** property, **Size Mode** will automatically be set to **Custom** unless specified as one of the previous two modes. | +| Type | Rendering mode, including Simple, Sliced, Tiled, and Filled. For details, refer to the **Rendering Modes** section below. | +| Trim | Whether to render transparent pixel areas around the original image. For details, refer to [Automatic Cropping of Image Resources](../engine/trim.md) | + +After adding the Sprite component, drag a SpriteFrame type resource from the **Asset Manager** to the **SpriteFrame** property reference to display the resource image through the Sprite component. + +If the dragged SpriteFrame resource is part of an Atlas resource, the Sprite's **Atlas** property will also be set automatically. + +**To dynamically replace SpriteFrame, first dynamically load the image resource and then perform the replacement. For details, refer to [Acquiring and Loading Resources: Dynamic Loading](../../../asset/dynamic-load-resources.md#loading-spriteframe-or-texture2d).** + +## Rendering Modes + +The Sprite component supports the following rendering modes: + +- `Simple Mode`: Renders the Sprite based on the original image resource. In this mode, we generally do not manually modify the node's dimensions to ensure the image displayed in the scene matches the proportions of the image produced by the artist. + +- `Sliced Mode`: The image is divided into a 9-slice grid and scaled according to specific rules to adapt to freely set dimensions (`size`). Typically used for UI elements or to save game resource space by creating 9-slice images that can be infinitely enlarged without affecting image quality. For details, read the [Creating 9-Slice Images with the Sprite Editor](../engine/sliced-sprite.md#-) section. + +- `Tiled Mode`: When the Sprite's size increases, the image is not stretched. Instead, it repeats according to the original image's size, tiling the entire Sprite like tiles. + + ![tiled](sprite/tiled.png) + +- `Filled Mode`: Draws part of the original image based on the origin and fill mode settings, following a certain direction and proportion. Often used for dynamic displays like progress bars. + + + +### Filled Mode + +After selecting Filled mode for the **Type** property, a new set of properties becomes available for configuration: + +| Property | Description | +| :-------------- | :----------- | +| Fill Type | Fill type selection: **HORIZONTAL** (horizontal fill), **VERTICAL** (vertical fill), and **RADIAL** (radial fill). | +| Fill Start | Normalized value for the fill start position (0 ~ 1, representing the percentage of total fill). When selecting horizontal fill, setting **Fill Start** to 0 starts filling from the leftmost part of the image. | +| **Fill Range** | Normalized value for the fill range (also 0 ~ 1). Setting it to 1 fills up to the entire original image range. | +| **Fill Center** | Fill center point. This property can only be modified when `RADIAL` fill type is selected. Determines which point on the Sprite the radial fill will revolve around. | + + +![radial](sprite/radial.png) + +#### Additional Notes on Fill Range + +For **HORIZONTAL** and **VERTICAL** fill types, the value set by **Fill Start** affects the total fill amount. If **Fill Start** is set to 0.5, even with **Fill Range** at 1.0, the actual filled range will only be half the total size of the Sprite. + +In **RADIAL** type, **Fill Start** only determines the starting direction of the fill. When **Fill Start** is 0, filling starts from the positive x-axis direction. **Fill Range** determines the total fill amount, with a value of 1 filling the entire circle. Positive **Fill Range** values fill counterclockwise, while negative values fill clockwise. + +#### API Documentation + +For the component interface of images, refer to [Sprite API](%__APIDOC__%/en/class/Sprite). + +### Example Demo + +For usage examples, see the **Sprite** sample ([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/01.sprite) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/01.sprite)). diff --git a/versions/4.0/en/ui-system/components/editor/sprite/radial.png b/versions/4.0/en/ui-system/components/editor/sprite/radial.png new file mode 100644 index 0000000000..3261ca3a4f Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/sprite/radial.png differ diff --git a/versions/4.0/en/ui-system/components/editor/sprite/sprite-component.png b/versions/4.0/en/ui-system/components/editor/sprite/sprite-component.png new file mode 100644 index 0000000000..f97e64a65f Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/sprite/sprite-component.png differ diff --git a/versions/4.0/en/ui-system/components/editor/sprite/sprite_editor_add.png b/versions/4.0/en/ui-system/components/editor/sprite/sprite_editor_add.png new file mode 100644 index 0000000000..8703173f32 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/sprite/sprite_editor_add.png differ diff --git a/versions/4.0/en/ui-system/components/editor/sprite/tiled.png b/versions/4.0/en/ui-system/components/editor/sprite/tiled.png new file mode 100644 index 0000000000..602dce5ae9 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/sprite/tiled.png differ diff --git a/versions/4.0/en/ui-system/components/editor/toggle.md b/versions/4.0/en/ui-system/components/editor/toggle.md new file mode 100644 index 0000000000..10d84b7c6b --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/toggle.md @@ -0,0 +1,89 @@ +# Toggle Component Reference + +The Toggle component is a CheckBox, when it's used together with a ToggleContainer, it could be treated as a RadioButton. + +![toggle1](toggle/toggle.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/Toggle__ to add the Toggle component to the node. + +To use `Toggle`, please refer to the [Toggle API](%__APIDOC__%/en/class/Toggle) documentation and the [Toggle](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/09.toggle) scene of the test-cases-3d project. + +## Toggle Properties + +| Property | Function Explanation | +| -------------- | ----------- | +| IsChecked | Boolean type. When set to true, enable the check mark component | +| CheckMark | Sprite type. The image displayed when Toggle is selected | +| CheckEvents | List type, default is null. Each event added by the user is composed of the node reference, component name and a response function. Please see the __Toggle Event__ section below for details | + +> __Note__: because Toggle is inherited from Button, so the properties exists in Button also apply to Toggle, please refer to the [Button Component](button.md) for details. + +## Toggle Event + +For event structure you can refer to the [Button](./button.md) documentation. + +The Toggle event callback has two parameters, the first one is the Toggle itself and the second is the `customEventData`. + +## Detailed Explanation + +The generic node hierarchy of Toggle is as below: + +![toggle-node-tree](toggle/toggle-node-tree.png) + +> __Note__: the checkMark node needs to be placed on the upper level of the background node in the __Scene__. + +## Add a callback through the script code + +### Method one + +The event callback added by this method is the same as the event callback added by the editor, all added by code. First you need to construct a `EventHandler` object, and then set the corresponding `target`, `component`, `handler` and `customEventData` parameters. + +```ts +import { _decorator, Component, Event, Node, ToggleComponent, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad(){ + const checkEventHandler = new EventHandler(); + // This Node is the node to which your event processing code component belongs + checkEventHandler.target = this.node; + // This is the script class name + checkEventHandler.component = 'example'; + checkEventHandler.handler = 'callback'; + checkEventHandler.customEventData = 'foobar'; + + const toggle = this.node.getComponent(ToggleComponent); + toggle.checkEvents.push(checkEventHandler); + } + + callback(event: Event, customEventData: string){ + // The event here is a Touch Event object, and you can get the send node of the event by event.target + // The customEventData parameter here is equal to the "foobar" you set before + } +} +``` + +### Method two + +Added by the way of `toggle.node.on('toggle', ...)`. + +```js +// // Suppose we add an event handler callback inside a component's onLoad method and event handlers in the callback function: + +import { _decorator, Component, ToggleComponent } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + @property(ToggleComponent) + toggle: ToggleComponent | null = null; + onLoad(){ + this.toggle.node.on('toggle', this.callback, this); + } + + callback(toggle: ToggleComponent){ + // The callback parameter is the Toggle component, note that events registered this way cannot pass customEventData. + } +} +``` diff --git a/versions/4.0/en/ui-system/components/editor/toggle/toggle-container.png b/versions/4.0/en/ui-system/components/editor/toggle/toggle-container.png new file mode 100644 index 0000000000..0a0ea38d39 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/toggle/toggle-container.png differ diff --git a/versions/4.0/en/ui-system/components/editor/toggle/toggle-group.png b/versions/4.0/en/ui-system/components/editor/toggle/toggle-group.png new file mode 100644 index 0000000000..551a8434fc Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/toggle/toggle-group.png differ diff --git a/versions/4.0/en/ui-system/components/editor/toggle/toggle-node-tree.png b/versions/4.0/en/ui-system/components/editor/toggle/toggle-node-tree.png new file mode 100644 index 0000000000..5f7d957ffc Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/toggle/toggle-node-tree.png differ diff --git a/versions/4.0/en/ui-system/components/editor/toggle/toggle.png b/versions/4.0/en/ui-system/components/editor/toggle/toggle.png new file mode 100644 index 0000000000..bff7a6ec3f Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/toggle/toggle.png differ diff --git a/versions/4.0/en/ui-system/components/editor/toggleContainer.md b/versions/4.0/en/ui-system/components/editor/toggleContainer.md new file mode 100644 index 0000000000..d6c151a031 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/toggleContainer.md @@ -0,0 +1,58 @@ +# ToggleContainer Component Reference + +ToggleContainer is not a visible UI component but it can be used to modify the behavior of a set of Toggle components. Toggles that belong to the same ToggleContainer could only have one of them to be switched on at a time. + +![toggle-container](toggle/toggle-container.png) + +> __Note__: all the first layer child node containing the Toggle component will auto be added to the container. + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/ToggleContainer__ to add the ToggleContainer component to the node. + +To use `ToggleContainer`, please refer to the [ToggleContainer API](%__APIDOC__%/en/class/ToggleContainer) documentation and the [Toggle](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/09.toggle) scene of the test-cases-3d project. + +## ToggleContainer Properties + +| Property | Functions Explanation | +| :-------------- | :----------- | +| **AllowSwitchOff** | If it is enabled, then the toggle button can be checked and unchecked repeatedly when it is clicked. If it is disabled, it will make sure there is always only one toggle could be checked and the already checked toggle can't be unchecked | +| **CheckEvents** | List type, default is null. Each event added by the user is composed of the node reference, component name and a response function. Please see the __ToggleContainer Event__ section below for details | + +## ToggleContainer Event + +For event structure you can refer to the [Button](./button.md) documentation. + +The ToggleContainer event callback has two parameters, the first one is the ToggleContainer itself and the second is the `customEventData`. + +## Detailed Explanation + +The ToggleContainer won't be used alone and it usually be used with `Toggle` to implement the RadioButton. + +## Add a callback through the script code + +The event callback added by this method is the same as the event callback added by the editor, all added by code. First you need to construct a `EventHandler` object, and then set the corresponding `target`, `component`, `handler` and `customEventData` parameters. + +```ts +import { _decorator, Component, Event, Node, ToggleContainerComponent, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad(){ + const containerEventHandler = new EventHandler(); + // This Node is the node to which your event processing code component belongs + containerEventHandler.target = this.node; + // This is the script class name + containerEventHandler.component = 'example'; + containerEventHandler.handler = 'callback'; + containerEventHandler.customEventData = 'foobar'; + + const container = this.node.getComponent(ToggleContainerComponent); + container.checkEvents.push(containerEventHandler); + } + + callback(event: Event, customEventData: string){ + // The event here is a Touch Event object, and you can get the send node of the event by event.target + // The customEventData parameter here is equal to the "foobar" you set before + } +} +``` diff --git a/versions/4.0/en/ui-system/components/editor/ui-coordinate-tracker.md b/versions/4.0/en/ui-system/components/editor/ui-coordinate-tracker.md new file mode 100644 index 0000000000..1268ee7179 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/ui-coordinate-tracker.md @@ -0,0 +1,27 @@ +# UICoordinateTracker component reference + +The UICoordinateTracker component performs coordinate conversion on the UI and simulates the foreshortening effects of 3D objects in the perspective camera. Return the converted coordinates and the proportion of the object in the viewport through events. Suitable for functions such as 3D character blood bars and name bars. + +Click the **Add Component** button at the bottom of the **Inspector** panel and select **UICoordinateTracker** from **UI** to add the **UICoordinateTracker** component to the node. + +## UICoordinateTracker properties + +| **Property** | **Function Description** | +| :-------------- | :---------- | +| **Target** | Target object. Which UI node needs to be converted to. | +| **Camera** | The 3D camera representing the original coordinate system. | +| **UseScale** | Whether to enable scaling, needs to be used in combination with the `Distance` property. If enabled, the scaling of the mapped object in the perspective camera will be adjusted according to the distance between the 3D node coordinates and the camera, resulting in a foreshortening effects. | +| **Distance** | The distance from the camera is calculated for normal display. Adjust the best position according to the illumination effect of the model under the camera, and use this position as the dividing line to calculate the proportion in the viewport. | +| **SyncEvents** | Map data events. The first parameter of the callback is the local coordinates after mapping, and the second is the distance ratio from the camera. | + +## SyncEvents + +You can use method likes `onSyncEvents(localUIPos:Vec3,distanceScale: number,customEventData: string)` to handle the `SyncEvents` + +Parematers as following: + +- `localUIPos:Vec3` ui coordinates after mapping +- `distanceScale: number` distance ratio from camera +- `customEventData: string` customize data, can be configured on the Inspector panel. + +For specific usage, please refer to the **rocker** scene in the [UI display example](https://github.com/cocos/cocos-example-ui/tree/v3.0/assets/scene). diff --git a/versions/4.0/en/ui-system/components/editor/ui-model.md b/versions/4.0/en/ui-system/components/editor/ui-model.md new file mode 100644 index 0000000000..83f7efe7be --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/ui-model.md @@ -0,0 +1,9 @@ +# UIMeshRenderer Reference + +UIMeshRenderer is a renderer component with a conversion feature that converts 3D models from 3D render pipeline to 2D. This component supports rendering the 3D model and particle system in the UI, without the component, the 3D model and particle system nodes will not be rendered. + +> **Note**: if the 3D model cannot be displayed in a UI scene, please try to enlarge the model. + +The UIMeshRenderer component is added by selecting the node in the __Hierarchy__ panel with the __MeshRenderer__ or classes inherited from it, then clicking the __Add Component__ button below the __Inspector__ panel and selecting __UI -> UIMeshRenderer__. The particles are added to the particle nodes. The usual structure is as below: + +![ui-model-hierachy](uimodel/ui-model-hierarchy.png) diff --git a/versions/4.0/en/ui-system/components/editor/ui-opacity.md b/versions/4.0/en/ui-system/components/editor/ui-opacity.md new file mode 100644 index 0000000000..7ea2bc3601 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/ui-opacity.md @@ -0,0 +1,24 @@ +# UIOpacity Component References + +The UIOpacity Component records a transparency modification flag for the node, which is used to influence all the render nodes inside its sub tree. Normally it's used on a non render nodes, otherwise its opacity will be multiplied with the render component's opacity. The render nodes can set transparency individually by setting the alpha channel of `color`. + +> **Note**: this component is only functional when its root node have Canvas component or RenderRoot2D component. + +The method of use is as follows: + +![ui-opacity](uiopacity/ui-opacity.png) + +You can also set transparency by code. Example: + +```ts +const opacityComp = this.getComponent(UIOpacity); +opacityComp.opacity = 157; +``` + +To use `UIOpacity`, please refer to the [UIOpacity API](%__APIDOC__%/en/class/UIOpacity) documentation and the [Opacity](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/other/opacity) scene of the test-cases-3d project. + +## UIOpacity Properties + +| Property | Function Description | +| -------- | ----------- | +| Opacity | transparency | diff --git a/versions/4.0/en/ui-system/components/editor/ui-skew.md b/versions/4.0/en/ui-system/components/editor/ui-skew.md new file mode 100644 index 0000000000..ee7df6cc09 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/ui-skew.md @@ -0,0 +1,53 @@ +# UISkew Component Reference + +The UI Skew component is used to control the skew transformation of UI elements. You can achieve different visual effects by setting the skew angles in both horizontal and vertical directions. + +Click the **Add Component** button below the **Inspector** panel and select **UI/UISkew** to add the UISkew component to a node. + +For UISkew scripting interface reference, please see [UISkew API](%__APIDOC__%/en/class/UISkew). + +## UISkew Properties + +| Property | Description | +| :-------------- | :----------- | +| rotational | Whether to use rotation-based skew algorithm +| skew | Skew angles in horizontal and vertical directions (Type: Vec2, x and y values set the skew angles for X and Y axes respectively) + +--- + +## UISkew Component Examples + +![ui-skew](../../../../zh/ui-system/components/editor/skew/skew-component.jpg) + +![skew-rotational-true](../../../../zh/ui-system/components/editor/skew/skew-rotational-true.jpg) + +![skew-rotational-false](../../../../zh/ui-system/components/editor/skew/skew-rotational-false.jpg) + +### Modifying Skew Angles through Script + +```ts +import { _decorator, Component, Node, UISkew } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Example') +export class Example extends Component { + + start () { + const uiSkew = this.getComponent(UISkew); + if (uiSkew) { + // Method 1 + uiSkew.setSkew(new Vec2(30, 15)); + + // Method 2 + uiSkew.x = 30; + uiSkew.y = 15; + } + } +} +``` + +### Notes + +1. The node's skew transformation affects the rendering of child nodes but does not change the node's size properties +2. When a node undergoes skew transformation, it emits a `'transform-changed'` event, which can be used to monitor node transformations +3. The skew transformation process involves multiple trigonometric calculations; frequent skew transformations will increase game performance overhead \ No newline at end of file diff --git a/versions/4.0/en/ui-system/components/editor/ui-static.md b/versions/4.0/en/ui-system/components/editor/ui-static.md new file mode 100644 index 0000000000..087181abd3 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/ui-static.md @@ -0,0 +1,31 @@ +# UIStaticBatch Component References + +The UIStaticBatch component is used to improve UI rendering performance. The script will collect all the rendering data under the UI node tree (except Model, Mask, and Graphic) during the initialization of the current frame rendering and store it as a static input assembler(IA) rendering data. And rendering with fixed data in the subsequent rendering process, no longer traversing its node tree, after which the coordinate transformation will no longer take effect. + +To use `UIStaticBatch`, please refer to the [UIStaticBatch API](%__APIDOC__%/en/class/UIStaticBatch) documentation and the [UIStaticBatch](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/19.static-ui) scene of the test-cases-3d project. + +## Enable static batching through script code + +Example: + +```ts +import { _decorator, Component } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + start(){ + const uiStatic = this.node.getComponent(UIStaticBatch); + // Choose the time you want to start static batching, call this interface to start static batching + uiStatic.markAsDirty(); + } +} +``` + +## Detailed Explanation + +The following points need to be noted when using this component: + +- Do not trigger static batches frequently, as the original stored IA data will be emptied and re-collected, resulting in some performance and memory loss. +- Not applicable for child node tree which contains Mask, Graphic and Model. +- For a node that will not have any changes in the node tree (e.g.: 2D Map), all child nodes can be deleted after data collection to get the best performance and memory performance. diff --git a/versions/4.0/en/ui-system/components/editor/ui-transform.md b/versions/4.0/en/ui-system/components/editor/ui-transform.md new file mode 100644 index 0000000000..11fc4c773d --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/ui-transform.md @@ -0,0 +1,52 @@ +# UITransform Component Reference + +The UITransform component defines the rectangle information on the UI, including the content size and anchor position of the rectangle. This component allows developers to modify the size and position of the rectangle freely, generally for rendering, calculation of click events, UI layout, screen adaptation, etc. + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UITransform__ from __UI__ to add the UITransform component to the node. + +Please refer to the [UITransform API](%__APIDOC__%/en/class/UITransform). + +## UITransform Properties + +| Property | Function Explanation +| :-------------- | :----------- | +| **ContentSize** | The content size of UI rectangle. +| **AnchorPoint** | The anchor position of UI rectangle. + +### change the size and anchor point in script. Example: + +```ts +import { _decorator, Component, Node, UITransform } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Example') +export class Example extends Component { + + start () { + const uiTransform = this.getComponent(UITransform); + // method one + uiTransform.setContentSize(200, 120); + uiTransform.setAnchorPoint(0, 0.5); + + // method two + uiTransform.width = 200; + uiTransform.height = 120; + uiTransform.anchorX = 0; + uiTransform.anchorY = 0.5; + } +} +``` + +### Deprecation of the priority property + +The `priority` property of the **UITransform** component was deprecated in v3.1, and users can adjust the rendering order by setting the order of the node tree using `setSiblingIndex()`. + +**Description of the deprecated `priority` property and the recommended `setSiblingIndex()` method**: + +The `priority` property on the **UITransform** component was deprecated in v3.1 due to a lack of clarity and naming conflicts with other properties in the engine. The `priority` property was originally designed to provide a shortcut for the user to sort the node tree, but has no other use in itself and is not related to the meaning of "priority", and actually still adjusts the rendering order by changing the order of the node tree. + +After deprecating the `priority` property, users can replace it with the `setSiblingIndex()` method, which adjusts the order of the node tree by affecting the `siblingIndex` property of the node. The difference is that the `priority` property has a default value and the `siblingIndex` property of a node is actually the position of the node in its parent node, so the value of the node's `siblingIndex` property will change when the node tree changes. This requires that when using the `setSiblingIndex()` method, the relative position of the node in the parent node is known and controlled in order to obtain the desired result. + +> **Note**: the `siblingIndex` property should not be used in the same way as the `priority` (deprecated) property, as they have different meanings. To change the `siblingIndex` property, need to understand and know that it represents the position under the parent node and will change when the node tree changes, and can only be modified by the `setSiblingIndex()` method. + +Considering the need for quick sorting of nodes, a more convenient and quick interface for users to sort nodes will be provided in future versions. diff --git a/versions/4.0/en/ui-system/components/editor/uimodel/ui-model-hierarchy.png b/versions/4.0/en/ui-system/components/editor/uimodel/ui-model-hierarchy.png new file mode 100644 index 0000000000..928fd53f24 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/uimodel/ui-model-hierarchy.png differ diff --git a/versions/4.0/en/ui-system/components/editor/uiopacity/ui-opacity.png b/versions/4.0/en/ui-system/components/editor/uiopacity/ui-opacity.png new file mode 100644 index 0000000000..2c1fbedd2e Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/uiopacity/ui-opacity.png differ diff --git a/versions/4.0/en/ui-system/components/editor/videoplayer.md b/versions/4.0/en/ui-system/components/editor/videoplayer.md new file mode 100644 index 0000000000..9364e655f0 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/videoplayer.md @@ -0,0 +1,180 @@ +# VideoPlayer Component Reference + +**VideoPlayer** is a component for playing videos, you could use this component for playing local video and remote videos. + +**Playing a local video**: + +![videoplayer](./videoplayer/videoplayer.png) + +**Playing a remote video**: + +![videoplayer-remote](./videoplayer/videoplayer-remote.png) + +Click **Add Component** at the bottom of **Properties** panel and select **VideoPlayer** from **UI Component** to add the **VideoPlayer** component to the node. + +For more information about **VideoPlayer**'s scripting interface, please refer to the [VideoPlayer API](%__APIDOC__%/en/class/VideoPlayer) documentation. + +## VideoPlayer Properties + +| Property | Function Explanation | +|:-------- | :----------- | +| **Resource Type** | The resource type of videoplayer, REMOTE for remote url and LOCAL for local file path. | +| **Remote URL** | Displayed when Resource Type is REMOTE, feed it with a remote video URL. | +| **Clip** | Displayed when Resource Type is LOCAL, feed it with a local video path. | +| **Play On Awake** | Whether the video start playing automatically after loaded? | +| **Current Time** | The current playback time of the now playing item in seconds, you could also change the start playback time. | +| **Volume** | The volume of the video. (0.0 ~ 1.0) | +| **Mute** | Mutes the VideoPlayer. Mute sets the volume=0, Un-Mute restore the original volume. | +| **Keep Aspect Ratio** | Whether keep the aspect ratio of the original video. | +| **Full Screen On Awake** | Whether play video in fullscreen mode. | +| **Stay On Bottom** | Display video below the game view (Only available on web). | +| **Video Player Event** | The video player's callback, it will be triggered when certain event occurs. Please refer to the `VideoPlayer Event` section below or [VideoPlayerEvent API](%__APIDOC__%/en/class/VideoPlayer?id=videoPlayerEvent) for more details. | + +> **Note**: in the **Node** of the **Video Player Event** property, you should fill in a Node that hangs the user script component, and in the user script you can use the relevant **VideoPlayer** event according to the user's needs. + +## VideoPlayer Event + +### VideoPlayerEvent Event + +| Property | Function Explanation | +| :-------------- | :----------- | +| **target** | Node with the script component.| +| **component** | Script component name. | +| **handler** | Specify a callback, when the video player is about to playing or paused, it will be called. There is a parameter in the callback which indicate the state of played videos.| +| **customEventData** | The user specifies that any string is passed in as the last parameter of the event callback | + +For more information, please refer to the [Component.EventHandler Class](%__APIDOC__%/en/class/EventHandler) documentation. + +### Parameter of VideoPlayerEvent + +| Name | Function Explanation | +| :-------------- | :----------- | +| **NONE** | None | +| **PLAYING** | Video is playing. | +| **PAUSED** | Video is paused. | +| **STOPPED** | Video is stopped. | +| **COMPLETED** | Video is completed. | +| **META_LOADED** | Video's meta data is loaded. | +| **READY_TO_PLAY** | Video is ready to play. | +| **ERROR** | Video Trigger Error | +| **CLICKED** | Video is clicked by the user. (Only supports Web platform.) | + +> **Note**: on iOS, due to the platform limitations, the **CLICKED** event can't be fired when **VideoPlayer** is in fullscreen mode. If you want to let the Video played in fullscreen and also fire the **CLICKED** event properly, you should use a **Widget** component to hack the **VideoPlayer's** size. + +For more information, please refer to the [VideoPlayer Events](%__APIDOC__%/en/class/VideoPlayer?id=videoPlayerEvent) documentation or the [21.video-player example](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/21.video-player) in the `test-cases-3d` samples bundled with __Cocos Creator__. + +## Detailed Explanation + +The supported video types is **mp4** format. + +### Add a callback via script + +#### Method one + +This method uses the same API that editor uses to add an event callback on Button component. You need to construct a `Component.EventHandler` object first, and then set the corresponding `target`, `component`, `handler` and `customEventData` parameters. + +```ts +import { _decorator, Component, VideoPlayer } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + @type(VideoPlayer) + videoPlayer = null; + + start () { + const eventHandler = new Component.EventHandler(); + eventHandler.target = newTarget; + eventHandler.component = "MyComponent"; + eventHandler.handler = "callback"; + eventHandler.customEventData = "foobar"; + this.videoplayer.videoPlayerEvent.push(eventHandler); + } + + // the order of parameters should not change + callback: function(videoplayer, eventType, customEventData) { + // videoplayer is a VideoPlayer component instance + // eventType is typed as VideoPlayer.EventType + // customEventData is "foobar" + } +} +``` + +#### Method two + +Add event callback with `videoplayer.node.on(VideoPlayer.EventType.READY_TO_PLAY, ...)` + +```ts +// Suppose we add event handling callbacks in the onLoad method of a component and perform event handling in the callback function: +import { _decorator, Component, find, VideoPlayer } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('VideoPlayerCtrl') +export class VideoPlayerCtrl extends Component { + @type(VideoPlayer) + videoPlayer = null; + + start () { + this.videoplayer.node.on(VideoPlayer.EventType.READY_TO_PLAY, this.callback, this); + } + + callback (videoplayer) { + // The "videoplayer" here represents the VideoPlayer component. + // do whatever you want with videoplayer + // you can't pass customEventData in this way + } +} +``` + +Likewise, it is also posible to register the `meta-loaded`, `clicked`, `playing` events, and the parameters of the callback function for these events are consistent with the `ready-to-play` parameters. + +Please refer to the [VideoPlayer API](%__APIDOC__%/en/class/VideoPlayer) documentation for details on **VideoPlayer** events. + +> **Note**: as **VideoPlayer** is a special component, it cannot register `touch` or `mouse` events on the node with **VideoPlayer** component. + +## How to display a UI upon a video + +You can display a UI upon a video in two steps: + +1. Make sure the **ENABLE_TRANSPARENT_CANVAS** checkbox is checked. It can be found in the **Macro Config** page in **Project Settings** + + ![ENABLE_TRANSPARENT_CANVAS](videoplayer/ENABLE_TRANSPARENT_CANVAS.png) + +2. Check the **stayOnBottom** property on the **VideoPlayer** in the **Properties** panel. + +> **Notes**: +> 1. This feature is only supported on Web. +> 2. The specific effects are not guaranteed to be consistent, depending on whether each browser supports or restricts. +> 3. After the **stayOnBottom** is enabled, the `clicked` event in `VideoPlayerEvent` cannot be listened normally. + +For more information, please refer to the [21.video-player example](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/21.video-player) in the `test-cases-3d` samples bundled with __Cocos Creator__. Results as shown below: + +![videoplayer-stayOnButtom](videoplayer/videoplayer-stayonbuttom.png) + +## Support platform + +Because different platforms have different authorization, API and control methods for **VideoPlayer** component. And have not yet formed a unified standard, only **Web**, **iOS**, **Android**, **WeChat Mini Games**, **Facebook Instant Games** and **Google Play Instant** platforms are currently supported. + +### Questions about autoplay + +Some mobile browsers or **WebView** do not allow auto-playing of videos and users need to play the video manually in a touch event. + +```ts +import { _decorator, Node, Component, find, VideoPlayer } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('VideoPlayerCtrl') +export class VideoPlayerCtrl extends Component { + @type(VideoPlayer) + videoPlayer = null; + + start () { + let canvas = find('Canvas'); + canvas.on(Node.EventType.TOUCH_START, this.playVideo, this); + } + + playVideo () { + this.videoplayer.play(); + } +} +``` diff --git a/versions/4.0/en/ui-system/components/editor/videoplayer/ENABLE_TRANSPARENT_CANVAS.png b/versions/4.0/en/ui-system/components/editor/videoplayer/ENABLE_TRANSPARENT_CANVAS.png new file mode 100644 index 0000000000..4dad25ba3a Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/videoplayer/ENABLE_TRANSPARENT_CANVAS.png differ diff --git a/versions/4.0/en/ui-system/components/editor/videoplayer/stayonbuttom.png b/versions/4.0/en/ui-system/components/editor/videoplayer/stayonbuttom.png new file mode 100644 index 0000000000..5153c0b79c Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/videoplayer/stayonbuttom.png differ diff --git a/versions/4.0/en/ui-system/components/editor/videoplayer/videoplayer-remote.png b/versions/4.0/en/ui-system/components/editor/videoplayer/videoplayer-remote.png new file mode 100644 index 0000000000..6c68b0a29e Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/videoplayer/videoplayer-remote.png differ diff --git a/versions/4.0/en/ui-system/components/editor/videoplayer/videoplayer-stayonbuttom.png b/versions/4.0/en/ui-system/components/editor/videoplayer/videoplayer-stayonbuttom.png new file mode 100644 index 0000000000..d501f7ac46 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/videoplayer/videoplayer-stayonbuttom.png differ diff --git a/versions/4.0/en/ui-system/components/editor/videoplayer/videoplayer.png b/versions/4.0/en/ui-system/components/editor/videoplayer/videoplayer.png new file mode 100644 index 0000000000..c0a55c0557 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/videoplayer/videoplayer.png differ diff --git a/versions/4.0/en/ui-system/components/editor/webview.md b/versions/4.0/en/ui-system/components/editor/webview.md new file mode 100644 index 0000000000..1e8365cf75 --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/webview.md @@ -0,0 +1,206 @@ +# WebView Component Reference + +**WebView** is a component for displaying web pages, you could use this component to embed a mini web browser in your games. Because different platforms have different authorization, API and control methods for WebView component. And have not yet formed a unified standard, only **Web**, **iOS**, and **Android** platforms are currently supported. + +![webview](./webview/webview.png) + +Click **Add Component** at the bottom of **Properties** panel and select **WebView** from **UI Component** to add the WebView component to the node. + +For more information, please refer to the [WebView API](%__APIDOC__%/en/class/WebView) documentation. + +## WebView Properties + +| Property | Function Explanation +|:-------- | :----------- | +| **URL** | A given URL to be loaded by the WebView, it should have a http or https prefix. +| **WebView Events** | The webview's event callback, it will be triggered when certain webview event occurs. + +> **Note**: in the **Node** of the **WebView Events** property, you should fill in a Node that hangs the user script component, and in the user script you can use the relevant **WebView** event according to the user's needs. + +## WebView Event + +### WebViewEvents Event + +| Property | Function Explanation +| :-------------- | :----------- | +|**Target**| Node with the script component. +|**Component**| Script component name. +|**Handler**| Specify a callback, when the WebView is loading the web pages, or the loading is finished or there are errors occurred. The callback will be called. For more information, please refer to `Parameter of WebViewEvents`. +| **CustomEventData** | The user specifies that any string is passed in as the last parameter of the event callback. | + +For more information, please refer to the [Component.EventHandler Class](%__APIDOC__%/en/class/EventHandler) documentation. + +### Parameter of WebViewEvents + +| Name | Function Explanation +| :-------------- | :----------- | +| **LOADING** | WebView is loading. +| **LOADED**| WebView is finished loading. +| **ERROR**| Errors occurred when loading web pages. + +For more information, please refer to the [WebView Events](%__APIDOC__%/en/class/WebView?id=webviewEvents) documentation or [22.webview example](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/22.webview) of the `test-cases-3d` samples bundled with Creator. + +## Details Explanation + +Currently, this component is only available on Web (Both PC and Mobile, iOS and Android (Not supported in the v2.0.0~2.0.6). It cannot be use on Mac or Windows which means if you preview **WebView** on these platforms, there is nothing to show. + +> **Notes**: +> 1. This component doesn't support load HTML file or execute JavaScript. +> 2. If you don't use **WebView** related features in your project, please ensure that the **WebView** module is removed from the **Project -> Project Settings -> Module Config** to help your game approval go as smoothly as possible on iOS App Store. If you really needs to use WebView (or the added third-party SDK comes with **WebView**), and therefore if the game is rejected by App Store, you can still try to appeal through email. + +### Add a callback via script + +#### Method one + +This method uses the same API that editor uses to add an event callback on Button component. You need to construct a `Component.EventHandler` object first, and then set the corresponding `target`, `component`, `handler` and `customEventData` parameters. + +```ts +import { _decorator, Component, WebView } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + @type(WebView) + webview = null; + + start () { + const eventHandler = new Component.EventHandler(); + eventHandler.target = newTarget; // This node is the one that the component that contains your event handler code belongs to + eventHandler.component = "MyComponent"; + eventHandler.handler = "callback"; + eventHandler.customEventData = "foobar"; + this.webview.webviewEvents.push(eventHandler); + } + + // Note that the order and type of parameters are fixed + callback: function(webview, eventType, customEventData) { +        // here webview is a WebView component instance +        // here the value of eventType === WebView.EventType enum +        // The customEventData parameter here is equal to the "foobar" + } +} +``` + +#### Method two + +Add event callback with `webview.node.on (WebView.EventType.LOADED, ...)` + +```ts +// Suppose we add event handling callbacks in the onLoad method of a component and perform event handling in the callback function: +import { _decorator, Component, WebView } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('WebViewCtrl') +export class WebViewCtrl extends Component { + @type(WebView) + webview = null; + + start () { + this.webview.node.on(WebView.EventType.LOADED, this.callback, this); + } + + callback (webview) { + // The 'webview' here is a WebView component object + // Do whatever you want with webview + // Also, note that this way the registered event can not pass 'customEventData' + } + +} +``` + +Likewise, you can also register `WebView.EventType.LOADING`, `WebView.EventType.ERROR` events, and the parameters of the callback function for these events are consistent with the `WebView.EventType.LOADED` parameters. + +## How to interact with WebView internal pages + +### Calling the WebView internal page + +```ts +import { _decorator, Component, WebView } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('WebViewCtrl') +export class WebViewCtrl extends Component { + @type(WebView) + webview = null; + + start () { + // The Test here is a global function defined in your webView's internal page code + this.webview.evaluateJS('Test()'); + } +} +``` + +> **Note**: cross-domain issues on Web platform need to be resolved by yourself as __Cocos Creator__ does not assist with this. + +### WebView internal pages call external code + +The current mechanism used by Android and iOS is to determine if the keywords in the URL prefix are the same, and if they are, then a callback is made. + +1. Setting the URL prefix keyword through `setJavascriptInterfaceScheme` +2. The callback function is set by `setOnJSCallback`, and the function parameter is URL + +```ts +import { _decorator, Component, WebView } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('WebViewCtrl') +export class WebViewCtrl extends Component { + @type(WebView) + webview = null; + + // Setting in onLoad will make the callback useless, so we must set the WebView callback in the start cycle. + start () { + // Here are the keywords that are agreed with the internal page + // Please set the scheme with lower case, the native won't identify the uppercase char scheme. + let scheme = "testkey"; + + function jsCallback (target, url) { + // The return value here is the URL value of the internal page, and it needs to parse the data it needs. + let str = url.replace(scheme + '://', ''); // str === 'a=1&b=2' + // webview target + console.log(target); + } + + this.webview.setJavascriptInterfaceScheme(scheme); + this.webview.setOnJSCallback(jsCallback); + } +} +``` + +When you need to interact with **WebView** through an internal page, you should set the internal page URL: `testkey://(the data you want to callback to WebView later)`. **WebView** internal page code: + +```html + + + + + + + + +``` + +Due to limitations of the Web platform, it can not be implemented by this mechanism, but internal pages can interact in the following ways: + +```html + + + + + + + + +``` + +> **Note**: cross-domain issues on Web platform need to be resolved by yourself as __Cocos Creator__ does not assist with this. diff --git a/versions/4.0/en/ui-system/components/editor/webview/webview.png b/versions/4.0/en/ui-system/components/editor/webview/webview.png new file mode 100644 index 0000000000..ceacb92aef Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/webview/webview.png differ diff --git a/versions/4.0/en/ui-system/components/editor/widget.md b/versions/4.0/en/ui-system/components/editor/widget.md new file mode 100644 index 0000000000..cbe940477d --- /dev/null +++ b/versions/4.0/en/ui-system/components/editor/widget.md @@ -0,0 +1,102 @@ +# Widget Component Reference + +Widget is a frequently used UI layout component. It can automatically align the current node to any position in the parent node's bounding box, or constrain the size, to make your game easily adaptable to different resolutions. About the alignment scheme, please see [Widget Alignment](../engine/widget-align.md) for details. + +![default](widget/widget-default.png) + +Click the __Add Component__ button at the bottom of the __Inspector__ panel and select __UI/Widget__ to add the Widget component to the node. + +To use `Widget`, please refer to the [Widget API](%__APIDOC__%/en/class/Widget) documentation and the [Widget](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/04.widget) scene of the test-cases-3d project. + +## Widget Properties + +Properties | Function Explanation | Note | +-- | -- | -- +**Top** | Upper border alignment | Once selected, an input field will appear to set the distance between the upper border of the current node and the upper border of the parent object. | +**Bottom** | Lower border alignment | Once selected, an input field will appear to set the distance between the lower border of the current node and the lower border of the parent object. | +**Left** | Left border alignment | Once selected, an input field will appear to set the distance between the left border of the current node and the left border of the parent object. | +**Right** | Right border alignment | Once selected, an input field will appear to set the distance between the right border of the current node and the right border of the parent object. | +**HorizontalCenter** | Horizontal center alignment | +**VerticalCenter** | Vertical center alignment | +**Target** | Alignment target | Specifies an alignment target that can only be one of the parent nodes of the current node. The default value is null, and when null, indicates the current parent.
If the parent node is the entire scene, it will be aligned to the visible area of the screen (`visibleRect`), and can be used to dock UI elements to the edge of the screen. | +**Align Mode** | Specifies the alignment mode of the Widget, which determines when the widget should refresh at runtime | Normally set to __ALWAYS__, only to be initialized and realigned whenever the window size changes.
Set to __ONCE__, will only make alignment when the component is enabled.
Set to __ON_WINDOW_RESIZE__, will update Widget's alignment every time when the window changes. | + +## Border alignment + +We can create a new Sprite node under the Canvas node, add a Widget component to the Sprite, and do the following test: + +### Left alignment, left border distance 100 px: + +![left-100px](widget/widget-left-100px.png) + +### Bottom alignment, bottom border distance 50%: + +The percentage will take the width or height of the parent node as a benchmark. + +![bottom-0.5](widget/widget-bottom-0.5.png) + +### Bottom right alignment, border distance 0 px: + +![bottom-right-0px](widget/widget-bottom-right-0px.png) + +## Center Alignment + +### Horizontal center alignment: + +![bottom-right-0px](widget/widget-h-center.png) + +### Vertical center alignment and right border distance 50%: + +![v-center-right-0.5](widget/widget-v-center-right-0.5.png) + +## Limit size + +If you align the left and right side at the same time, or align the top and bottom at the same time, then the size will be stretched in the corresponding direction. + +Let us look at a demonstration. Place two rectangular Sprites in the Scene and take the bigger one as the dialog box background and the smaller one as the button on the dialog box. Take the button node as the child node of the dialog box and set the button into __SLICED__ mode so that you can observe the stretch effect. + +### Horizontal stretch, left and right margin 10%: + +![h-stretch](widget/widget-h-stretch.png) + +### Vertical stretch, no margins on each end and horizontal center alignment: + +![v-stretch](widget/widget-v-stretch.png) + +### Stretch in the horizontal and vertical directions, margin 50 px: + +![margin-50px](widget/widget-margin-50px.png) + +## Limitation on node position control + +If __Align Mode__ property is set to `ALWAYS`, the Widget will set the alignment for the current node every frame at runtime according to the alignment policy you set. The `position` and `size` (`width`, `height`) properties of the node where the component is located may be restricted and cannot be freely modified via the API or animation system. + +This is because the alignment set by the widget is processed at the end of each frame, so if you set the previously set alignment related properties in the Widget component, those settings will eventually be reset by the widget component's own settings. + +To make sure you can update node's position or size during runtime: + +1. Set __Align Mode__ property of Widget to `ONCE`, so it will only align during onEnable process. +2. Use Widget's API to update node's position and size, for example updating Widget's `top`, `bottom`, `left`, `right` instead of node's `x`, `y`, `width`, `height`. + +### Modify the alignment distance in script code. Example: + +```ts +import { _decorator, Component, Widget } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Example') +export class Example extends Component { + start () { + const widget = this.getComponent(Widget); + // Set the default alignment unit to px + widget!.bottom = 50; + widget!.top = 50; + + // The alignment unit is% + widget!.isAbsoluteTop = false; + widget!.isAbsoluteBottom = false; + widget!.bottom = 0.1; // 10% + widget!.top = 0.1; // 10% + } +} +``` diff --git a/versions/4.0/en/ui-system/components/editor/widget/widget-bottom-0.5.png b/versions/4.0/en/ui-system/components/editor/widget/widget-bottom-0.5.png new file mode 100644 index 0000000000..e9f5e686e6 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/widget/widget-bottom-0.5.png differ diff --git a/versions/4.0/en/ui-system/components/editor/widget/widget-bottom-right-0px.png b/versions/4.0/en/ui-system/components/editor/widget/widget-bottom-right-0px.png new file mode 100644 index 0000000000..c6ab1cae17 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/widget/widget-bottom-right-0px.png differ diff --git a/versions/4.0/en/ui-system/components/editor/widget/widget-default.png b/versions/4.0/en/ui-system/components/editor/widget/widget-default.png new file mode 100644 index 0000000000..a8831bcb91 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/widget/widget-default.png differ diff --git a/versions/4.0/en/ui-system/components/editor/widget/widget-h-center.png b/versions/4.0/en/ui-system/components/editor/widget/widget-h-center.png new file mode 100644 index 0000000000..ad4cf214d9 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/widget/widget-h-center.png differ diff --git a/versions/4.0/en/ui-system/components/editor/widget/widget-h-stretch.png b/versions/4.0/en/ui-system/components/editor/widget/widget-h-stretch.png new file mode 100644 index 0000000000..57bfcc4d85 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/widget/widget-h-stretch.png differ diff --git a/versions/4.0/en/ui-system/components/editor/widget/widget-left-100px.png b/versions/4.0/en/ui-system/components/editor/widget/widget-left-100px.png new file mode 100644 index 0000000000..2fd59d3b41 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/widget/widget-left-100px.png differ diff --git a/versions/4.0/en/ui-system/components/editor/widget/widget-margin-50px.png b/versions/4.0/en/ui-system/components/editor/widget/widget-margin-50px.png new file mode 100644 index 0000000000..5f4079f5b4 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/widget/widget-margin-50px.png differ diff --git a/versions/4.0/en/ui-system/components/editor/widget/widget-v-center-right-0.5.png b/versions/4.0/en/ui-system/components/editor/widget/widget-v-center-right-0.5.png new file mode 100644 index 0000000000..b3d857843a Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/widget/widget-v-center-right-0.5.png differ diff --git a/versions/4.0/en/ui-system/components/editor/widget/widget-v-stretch.png b/versions/4.0/en/ui-system/components/editor/widget/widget-v-stretch.png new file mode 100644 index 0000000000..6716da0f19 Binary files /dev/null and b/versions/4.0/en/ui-system/components/editor/widget/widget-v-stretch.png differ diff --git a/versions/4.0/en/ui-system/components/engine/auto-layout.md b/versions/4.0/en/ui-system/components/engine/auto-layout.md new file mode 100644 index 0000000000..1ea19d2de7 --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/auto-layout.md @@ -0,0 +1,62 @@ +# Auto Layout Container + +The __Layout__ component can be mounted to any __Node__, making the node into a container with the auto layout function. The so-called auto layout container can automatically array the child nodes according to certain rules and adjust the container type nodes of its own size according to the sum total of bounding boxes of the node content. + +For the next layout types, the node structure is as follows: + +![layout-node](auto-layout/layout-node.png) + +## Layout Type + +Auto layout components have several basic layout types. These can be set up by the `Layout Type` property, which includes the following types. + +### Horizontal Layout + +![horizontal-no-align](auto-layout/horizontal-no-align.png) + +When Layout `Type` is set to `Horizontal`, all child nodes will be automatically aligned horizontally and the component will modify the position or height of the node on the y-axis by default. If the child node needs to be placed outside the height range of the Layout node's bounding box, you can uncheck `AutoAlignment` (as shown above). + +The situation that the content exceeds the container easily under horizontal sorting, the following measures can be taken as needed. + + - If the container is to adapt to the size of the content, you can set the `ResizeMode` to `Container`, which will set the width of the Layout node based on the sum of the widths of the child nodes (`Width`) (left in the figure below). + + - If the content object is always to remain inside the container, you can set `ResizeMode` to `Children`, which will limit the size of the content object to the container (right in the figure below). + + - If you want the child nodes to be aligned upwards on the y-axis, you can add a widget component to the child node and turn on the `Top` or `Bottom` alignment mode. + +![horizontal-resizemode](auto-layout/horizontal-resizemode.png) + +#### Horizontal Direction + +In the horizontal layout, you can set the horizontal orientation with `HorizontalDirection`. There are two types of orientation: `LEFT_TO_RIGHT` and `RIGHT_TO_LEFT`. The former will arrange the nodes from left to right according to their display order in __Hierarchy__. The latter will arrange the nodes from right to left according to their display order. + +### Vertical Layout + +The layout and orientation of vertical layout is almost the same as __horizontal layout__, only the orientation is different. + +### Grid Layout + +Layout `Type` set to `Grid` will start the grid layout. The grid layout will determine the starting point of the layout based on the combination of `HorizontalDirection` and `VerticalDirection` within a fixed container size, and the layout direction based on the `StartAxis` property. + +#### Grid Direction + +Layout arranges the child nodes in the order in which they are displayed in the __Hierarchy__, plus the start point and the alignment direction set by the `StartAxis` property. + +- Start axis + - Set to either `HORIZONTAL` or `VERTICAL` orientation. The former will be aligned horizontally, the latter vertically. +- Start point + - The start point is created by combining `HorizontalDirection` and `VerticalDirection`. + - Suppose `HorizontalDirection` is `LEFT_TO_RIGHT` and VerticalDirection is `TOP_TO_BOTTOM`, then the start point is __top left__. + - Suppose `HorizontalDirection` is `RIGHT_TO_LEFT` and VerticalDirection is `BOTTOM_TO_TOP`, then the start point is __bottom right__. + +Two examples are given in conjunction with alignment directions: + +- If the `HorizontalDirection` is set to `LEFT_TO_RIGHT`, `VerticalDirection` is `TOP_TO_BOTTOM` and `StartAxis` is `HORIZONTAL`. This tells the component to be sorted horizontally starting from the __top left__ of the container (below left). + +- If the currently set `HorizontalDirection` is `RIGHT_TO_LEFT`, VerticalDirection is `BOTTOM_TO_TOP` and `StartAxis` is `VERTICAL`, it is telling the component to be sorted vertically starting from the __BOTTOM RIGHT__ of the container (right in the figure below). + + ![grid-layout](auto-layout/grid-layout.png) + +Grid sorting may also cause the content to exceed the container, which can also be solved by using the `Children` and `Container` modes of `ResizeMode` mentioned in `Horizontal Layout`. + +For more information, please refer to the [Layout component](../editor/layout.md) documentation. diff --git a/versions/4.0/en/ui-system/components/engine/auto-layout/grid-layout.png b/versions/4.0/en/ui-system/components/engine/auto-layout/grid-layout.png new file mode 100644 index 0000000000..4bf27d53d6 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/auto-layout/grid-layout.png differ diff --git a/versions/4.0/en/ui-system/components/engine/auto-layout/horizontal-no-align.png b/versions/4.0/en/ui-system/components/engine/auto-layout/horizontal-no-align.png new file mode 100644 index 0000000000..3fa0613ec5 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/auto-layout/horizontal-no-align.png differ diff --git a/versions/4.0/en/ui-system/components/engine/auto-layout/horizontal-resizemode.png b/versions/4.0/en/ui-system/components/engine/auto-layout/horizontal-resizemode.png new file mode 100644 index 0000000000..ac983ffd0e Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/auto-layout/horizontal-resizemode.png differ diff --git a/versions/4.0/en/ui-system/components/engine/auto-layout/layout-node.png b/versions/4.0/en/ui-system/components/engine/auto-layout/layout-node.png new file mode 100644 index 0000000000..df3aee3bee Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/auto-layout/layout-node.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout.md b/versions/4.0/en/ui-system/components/engine/label-layout.md new file mode 100644 index 0000000000..aa0f373855 --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/label-layout.md @@ -0,0 +1,122 @@ +# Label Layout + +The __Label__ component is one of the key renderable components. You need to learn about how to set up label layout in order to have a perfect display when the UI system adapts to various resolutions and sets up alignment. + +## Alignment of labels in the bounding box + +Like other renderer components, the layout of the Label component is also based on the size information (`contentSize`) possessed by the [UITransform](../editor/ui-transform.md) component, that is, the range specified by the Bounding Box. + +![label in bounding box](label-layout/label_in_boundingbox.png) + +What is shown in the picture above is the display effect of labels rendered by Label in the blue bounding box. The following properties in Label determines the position of labels in the bounding box: + +- `Horizontal Align`: the horizontal alignment of labels in the bounding box, which can be chosen from 3 positions: Left, Right, Center. + +- `Vertical Align`: the vertical alignment of labels in the bounding box, which can be chosen from 3 positions: Top, Bottom, Center. + + ![horizontal-vertical-align](label-layout/horizontal-vertical-align.png) + +In the figure above, the horizontal alignment position is set to `Right`, and the vertical alignment position is set to `Bottom`. It can be seen that the label appears at the bottom of the Bounding Box and is aligned to the right. The developer can modify the two properties above to make other combinations, labels will appear on the corresponding position of the blue bounding box according to the settings. + +## Label Size and Line Height + +`Font Size` determines the display size of labels. Its unit is Point (it can also be called 'pound'), which is the size unit for fonts commonly used in most image editing softwares and font editing softwares. For dynamic fonts, `Font Size` can be zoomed in losslessly. But the display of bitmap fonts will be more and more vague when the set value of `Font Size` exceeds the font size stipulated by the font. + +`Line Height` determines the height occupied by each line when multiple lines of labels display, the unit of which is also Point. The displaying of multiple lines of labels can be carried out by using two methods: + +- When inputting labels in the `String` property, manually input `\r` or `\n` +- Open the `Enable Wrap Text` property, which will be introduced in detail later + +__Relation of label size and line height__: + +- If the values of `Font Size` and `Line Height` are the same, labels will occupy the height of the most part of one line. + + ![font_equal_line_height](label-layout/font_equal_line_height.png) + +- If the value of `Font Size` is less than that of `Line Height`, space between multiple lines of labels will be enlarged + + ![font_smaller](label-layout/font_smaller.png) + +- If the value of `Font Size` is larger than that of `Line Height`, space between multiple lines of labels will be narrowed between multiple lines of labels. Overlapping of labels may appear. + + ![font_bigger](label-layout/font_bigger.png) + +## Overflow + +The __Overflow__ property determines the array of labels in the bounding box when the content of labels is increased. There are four modes: __NONE__, __CLAMP__, __SHRINK__, and __RESIZE_HEIGHT__. Only in the latter three modes can the size of the bounding box be adjusted through the __Rectangle Transform Tool__ (or click the keyboard button **T**) in the upper left corner of the editor or modifying the __Size__ in the __Inspector__ panel or add the __Widget__ component. + +__NONE__ mode will automatically fix the size of the bounding box according to the text size, line height, etc. + +### Clamp + +![clamp](label-layout/clamp.png) + +When in Clamp mode, labels will firstly be rendered according to the requirements of alignment type and size, but the parts exceeding the bounding box will be concealed (clamped). + +### Auto Shrink + +![shrink](label-layout/shrink.png) + +When in auto shrink mode, if labels exceed the bounding box when being rendered according to the original size, the size of the labels will be automatically shrink to display all the labels. + +**Attention!** Auto shrink mode will not zoom in on labels to adapt to bounding box. + +### Resize Height + +![resize-height](label-layout/resize-height.png) + +Resize height mode will make sure the bounding box of the labels fits the height of the labels, no matter the quantity of the labels' lines. This mode is suitable for displaying paragraphs of labels that don't have the same amount of content. An infinite amount of label content can be displayed in the arbitrary UI field when using this mode together with the [ScrollView](../components/scrollview.md) component. + +## Enable Wrap Text + +The `Enable Wrap Text` property of the Label component can switch the auto wrap switch of labels. When `Enable Wrap Text` is opened, labels will automatically wrap acccording to the width of the bounding box without manual input of `\r` or `\n` when inputting labels. + +> __Note__: the `Enable Wrap Text` property is only available in the __CLAMP__ and __SHRINK__ modes of the label layout mode. In __RESIZE_HEIGHT__ mode, the `Enable Wrap Text` property is compulsorily opened. + +### Auto Wrap in Clamp Mode + +When clamp mode opens auto wrap, labels will be wrapped in the allowable scale of the bounding box in priority. Only when not all the words can be displayed after being wrapped will clamp mode function. + +![clamp_wrap](label-layout/clamp_wrap.png) + +The following two pictures are taken when `Clamp` + `Enable Wrap Text` are opened, the difference of which is the different width of the bounding boxes of the labels; + +![clamp_wrap 1](label-layout/clamp_wrap1.png) + +![clamp_wrap 2](label-layout/clamp_wrap2.png) + +When the width of the bounding box is changing from the left picture to the right picture, labels will be continuously adjusted and wrapped. In the end, the clamped display will appear due to the insufficient height of the bounding box. + +### Auto Wrap in Auto shrink mode + +Similar to clamp mode, labels will be wrapped in priority when labels exceed the width of the bounding box in auto shrink mode. Only when the width and length of the bounding box are exceeded will the labels be automatically shrink to adapt to the bounding box. + +![shrink_wrap](label-layout/shrink_wrap.png) + +### Enable Wrap Text of Chinese + +The auto wrap behavior of Chinese is different from that of English. English is wrapped by the unit of words. Blank space is the smallest unit of wrap adjustment. Chinese is wrapped by the unit of characters. Every character can be wrapped alone. + +## Anchor point of label node + +The anchor point of a label node and the alignment mode of labels in the bounding box are two concepts that need to be differentiated. In a layout type that needs labels to enlarge the bounding box (e.g., `Resize Height`), only correct setting up of anchor points can make the bounding box be extended to the expected direction. + +For example, if you want the bounding box to extend downward, you need to set the `y` property of `Anchor` as `1`. As shown below: + +![anchor1](label-layout/anchor1.png) + +![anchor2](label-layout/anchor2.png) + +## Widget + +Add a __Widget__ component to the node with the Label component, then you can make various layouts for label nodes relative to their parent nodes. + +![widget](label-layout/widget.png) + +In the above picture, two Label child nodes are added to the background node. After respectively adding the Widget component to them, set the `Right` property of the Widget for the labels on the left side as `50%`, and set the `Left` property of the Widget for the labels on the right side as `60%`, then the multi-column layout of labels in the picture above can be realized. + +And by setting margins on the Widget, plus the layout type of labels, a flexible and beautiful layout for labels can be easily realized without concrete and minor adjustments to the bounding box. + +## Reference for checking components + +For properties of the Label component, you can also check [Label Component](../editor/label.md) document. diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/anchor1.png b/versions/4.0/en/ui-system/components/engine/label-layout/anchor1.png new file mode 100644 index 0000000000..156b1df484 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/anchor1.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/anchor2.png b/versions/4.0/en/ui-system/components/engine/label-layout/anchor2.png new file mode 100644 index 0000000000..5a198a26e8 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/anchor2.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/clamp.png b/versions/4.0/en/ui-system/components/engine/label-layout/clamp.png new file mode 100644 index 0000000000..ca127041bf Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/clamp.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/clamp_wrap.png b/versions/4.0/en/ui-system/components/engine/label-layout/clamp_wrap.png new file mode 100644 index 0000000000..9a9321ff4b Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/clamp_wrap.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/clamp_wrap1.png b/versions/4.0/en/ui-system/components/engine/label-layout/clamp_wrap1.png new file mode 100644 index 0000000000..013d5c39f9 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/clamp_wrap1.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/clamp_wrap2.png b/versions/4.0/en/ui-system/components/engine/label-layout/clamp_wrap2.png new file mode 100644 index 0000000000..63c79058cd Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/clamp_wrap2.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/font_bigger.png b/versions/4.0/en/ui-system/components/engine/label-layout/font_bigger.png new file mode 100644 index 0000000000..b2852acf63 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/font_bigger.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/font_equal_line_height.png b/versions/4.0/en/ui-system/components/engine/label-layout/font_equal_line_height.png new file mode 100644 index 0000000000..03225321f1 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/font_equal_line_height.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/font_smaller.png b/versions/4.0/en/ui-system/components/engine/label-layout/font_smaller.png new file mode 100644 index 0000000000..bf06bc4d1a Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/font_smaller.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/horizontal-vertical-align.png b/versions/4.0/en/ui-system/components/engine/label-layout/horizontal-vertical-align.png new file mode 100644 index 0000000000..fcc2377979 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/horizontal-vertical-align.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/label_in_boundingbox.png b/versions/4.0/en/ui-system/components/engine/label-layout/label_in_boundingbox.png new file mode 100644 index 0000000000..7672acf262 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/label_in_boundingbox.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/resize-height.png b/versions/4.0/en/ui-system/components/engine/label-layout/resize-height.png new file mode 100644 index 0000000000..3907ed7116 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/resize-height.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/shrink.png b/versions/4.0/en/ui-system/components/engine/label-layout/shrink.png new file mode 100644 index 0000000000..699f866d56 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/shrink.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/shrink_wrap.png b/versions/4.0/en/ui-system/components/engine/label-layout/shrink_wrap.png new file mode 100644 index 0000000000..59f755d6b3 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/shrink_wrap.png differ diff --git a/versions/4.0/en/ui-system/components/engine/label-layout/widget.png b/versions/4.0/en/ui-system/components/engine/label-layout/widget.png new file mode 100644 index 0000000000..06fbe2f0cd Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/label-layout/widget.png differ diff --git a/versions/4.0/en/ui-system/components/engine/large-screen.md b/versions/4.0/en/ui-system/components/engine/large-screen.md new file mode 100644 index 0000000000..a3796ae603 --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/large-screen.md @@ -0,0 +1,13 @@ +# Android Large Screen Adaptation + +![hyper-resolution.png](./large-screen/hyper-resolution.png) + +Cocos Creator is currently able to adapt to large screen devices as well as folding screens. + +For large screen Android devices, our recommended adaptation scheme is: + +- Set Wie to On_WINDOW_RESIZE or ALWAYS for the Align Mode on the 2D/UI node used to adapt the widget component. + + ![scale-ui.png](./large-screen/scale-ui.png) + +- Refer to [Responsive layouts for large screen development](https://developer.android.com/large-screens). diff --git a/versions/4.0/en/ui-system/components/engine/large-screen/hyper-resolution.png b/versions/4.0/en/ui-system/components/engine/large-screen/hyper-resolution.png new file mode 100644 index 0000000000..5d733ecc7a Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/large-screen/hyper-resolution.png differ diff --git a/versions/4.0/en/ui-system/components/engine/large-screen/scale-ui.png b/versions/4.0/en/ui-system/components/engine/large-screen/scale-ui.png new file mode 100644 index 0000000000..2d22bb6147 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/large-screen/scale-ui.png differ diff --git a/versions/4.0/en/ui-system/components/engine/list-with-data.md b/versions/4.0/en/ui-system/components/engine/list-with-data.md new file mode 100644 index 0000000000..67b7b9948e --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/list-with-data.md @@ -0,0 +1,149 @@ +# Create a list of dynamically generated content + +We can build and edit static UI interfaces with __Scene__ panel easily, but in real world game project it's not enough. We'll need dynamically generated UI elements with data, such as character selection, inventory and level selection. + +## Prepare data + +Let's take an inventory interface as example, we need following data structure to generate an item dynamically: + +- Item id +- Icon id, we can put up a icon id to spriteFrame reference dictionary or array +- Item name +- Item price + +We will introduce how to define a data class and generate those data in __Inspector__ panel. If you're not familiar with component system of __Cocos Creator__, please start with [Workflow of script development](../../../scripting/index.md) documentation. + +### Custom data class + +For most game project, you can get inventory data from game server. For the simplicity let's define a data class for data structure and input. Let's add a new script `ItemList.js` and add the following properties: + +```ts +@ccclass('Item') +export class Item { + @property + id = 0; + @property + itemName = ''; + @property + itemPrice = 0; + @property(SpriteFrame) + iconSF: SpriteFrame | null = null; +} + +@ccclass +export class ItemList extends Component { + @property([Item]) + items: Item[] = []; + @property(Prefab) + itemPrefab: Prefab | null = null; + + onLoad () { + for (let i = 0; i < this.items.length; ++i) { + const item = instantiate(this.itemPrefab); + const data = this.items[i]; + this.node.addChild(item); + item.getComponent('ItemTemplate').init(data); + } + } +} +``` + +We defined an `Item` class at the top of the script for storing and easily updating data needed by item. Please notice this class does not extends `Component`, so it can be defined as a property type for any component. Please refer to the [Declare class](../../../scripting/decorator.md) documentation for additional details. + +After the `Item` class definition, we defined a component class. Each script file can only contains one component definition and the component name will be the same as the file name. So the component we define is `ItemList`. In this component we have a list property which type is `Item`. This way we can populate the list with data input in __Inspector__ panel. + +Now let's create an empty node and add `ItemList` component. We can find `Items` property in __Inspector__ panel. To populate data, let's give the list a length. Type `3` in the field and you can input data like these: + +![item list](list-with-data/itemlist.png) + +We have our data ready for now, you can also type in more data entries as you wish. If you're making a game with lots of data, please consider using more specialized data source like Excel and database. It's easy to convert such data sources to JSON for Cocos Creator. + +## Make the view for data: Prefab as template + +Now move on to the **view** to visualize data, we also need a template resource that can be used to instantiate each item at runtime. We can use [Prefab](../../../asset/prefab.md) to do this job. Let's create a prefab that looks like this: + +![item template](list-with-data/item-template.png) + +The child nodes `icon`, `name`, `price` will be used to display icon, item name and price from data. + +### Data binding + +You can freely customize the prefab as you need, the picture above only shows an example. Now we need a component script to bind the data to the components that show them. Create a new script `ItemTemplate.js` and add it to the prefab root node. The script contains: + +```ts +@ccclass +export class ItemTemplate extends Component { + @property + public id = 0; + @property(Sprite) + public icon: Sprite | null = null; + @property(Label) + public itemName: Label | null = null; + @property(Label) + public itemPrice: Label | null = null; +} +``` + +Drag all those nodes onto the property fields of `ItemTemplate` component. + +![item binding](list-with-data/item-binding.png) + +> __Note__: we will assign value for `id` property through script, no data binding needed. + +### Update template display with script + +Modify `ItemTemplate.js` script to add function to update the renderer components with input data. Let's add the following to the end of script: + +```ts +// data: { id, iconSF, itemName, itemPrice } +init(data: Item) { + this.id = data.id; + this.icon.spriteFrame = data.iconSF; + this.itemName.string = data.itemName; + this.itemPrice.string = data.itemPrice; +} +``` + +`init` method takes a data object and use the data to update each renderer component on bound nodes. Now we can save `Item` node as a Prefab asset and use it as the template of our item entries. + +## Instantiate template with data + +Go back to `ItemList.js` script, and add reference to our Prefab and then instantiate it with data. + +```ts +//... +@property(Prefab) +itemPrefab: Prefab | null = null; + +onLoad () { + for (let i = 0; i < this.items.length; ++i) { + const item = instantiate(this.itemPrefab); + const data = this.items[i]; + this.node.addChild(item); + item.getComponent('ItemTemplate').init(data); + } +} +``` + +In the `onLoad` callback method, we traverse each data stored in `items` in turn, instantiate `itemPrefab` to generate a new node and add it to the node where `ItemList.js` is. Then call the `init` method in `ItemTemplate.js` to update its display. + +Now we can add a __Layout__ component to the node that holds `ItemList.js` through __Add Component -> Add UI Component -> Layout__ under the __Inspector__ panel, and set the following properties: + +- `Type`: `HORIZONTAL` +- `Resize Mode`: `CONTAINER` + +Don't forget to drag and drop `item` Prefab to `itemPrefab` property field of `ItemList` component. You can also add a __Sprite__ component to the node as the background. + +All steps have been completed. Now `itemList` node should look like this: + +![itemlist complete](list-with-data/itemlist-complete.png) + +## Preview + +Running preview of the scene will get the result like this (the actual look depends on how your template was setup and your data): + +![result](list-with-data/result.png) + +The __Layout__ component added in previous step is not necessary. We can use it to help putting multiple items in a container in order but you can also use the script program to do that. You can also add a __ScrollView__ component together to display a large amount of content in a limited space. + +For details of layout methods please read the [Auto Layout Container](auto-layout.md) and the [ScrollView Component](../editor/scrollview.md) documentation. diff --git a/versions/4.0/en/ui-system/components/engine/list-with-data/item-binding.png b/versions/4.0/en/ui-system/components/engine/list-with-data/item-binding.png new file mode 100644 index 0000000000..ecc0a57c1f Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/list-with-data/item-binding.png differ diff --git a/versions/4.0/en/ui-system/components/engine/list-with-data/item-template.png b/versions/4.0/en/ui-system/components/engine/list-with-data/item-template.png new file mode 100644 index 0000000000..f79470545c Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/list-with-data/item-template.png differ diff --git a/versions/4.0/en/ui-system/components/engine/list-with-data/itemlist-complete.png b/versions/4.0/en/ui-system/components/engine/list-with-data/itemlist-complete.png new file mode 100644 index 0000000000..44719c4f29 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/list-with-data/itemlist-complete.png differ diff --git a/versions/4.0/en/ui-system/components/engine/list-with-data/itemlist.png b/versions/4.0/en/ui-system/components/engine/list-with-data/itemlist.png new file mode 100644 index 0000000000..16f1a8bb06 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/list-with-data/itemlist.png differ diff --git a/versions/4.0/en/ui-system/components/engine/list-with-data/result.png b/versions/4.0/en/ui-system/components/engine/list-with-data/result.png new file mode 100644 index 0000000000..a9d37263d1 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/list-with-data/result.png differ diff --git a/versions/4.0/en/ui-system/components/engine/multi-resolution.md b/versions/4.0/en/ui-system/components/engine/multi-resolution.md new file mode 100644 index 0000000000..a1e5caad65 --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/multi-resolution.md @@ -0,0 +1,60 @@ +# Auto Fit for Multi-Resolution + +Cocos Creator still provides the solution to adapting to screens with various resolutions with one set of assets. Generally speaking, we realize the adaptation scheme for multi-resolution through the following technology: + +- __Canvas__ component immediately obtains the actual resolution of the device screen and appropriately adjusts the size of all the render elements in the scene. +- __Widget__ component add to UI nodes. It can align the element with different referential positions of the target node(e.g. the default is the parent node) according to different needs. +- __Label__ component has a built-in function that provides various dynamic layout modes. When the bounding box of labels change because of the alignment requirement of the Widget, labels will present the perfect layout effect according to your needs. +- __Sliced Sprite__ provides images whose size can be arbitrarily designated. Simultaneously, it can meet various alignment requirements and display images with high definition on screens of any resolution. + +Next, start off by getting to know the concepts of design resolution and screen resolution, then we will go on to learn the zooming in/out function of the [Canvas](../editor/canvas.md) component. + +## Design resolution and screen resolution + +__Design resolution__ is the resolution sample used by content creators when creating the scene. But __screen resolution__ is the actual resolution of the device that the game is running on. + +Normally, design resolution will use the screen resolution of the device that is being used the most by the targeted group on the market, such as: screen resolutions of `800x480` and `1280x720` that are being used by Android devices currently, or screen resolutions of `1136x640` and `960x640` that are being used by iOS devices. Therefore, after designers or directors set up the scene by using design resolution, the game will automatically adapt to the device used by the major targeted group. + +Then when the design resolution is different from the screen resolution, how could **Cocos Creator** adapt to the device? + +Supposing the design resolution is `800x480`, designers will create a background image of the same resolution. + +![design resolution](multi-resolution/design_resolution.png) + +### When design resolution and screen resolution have the same aspect ratio + +When design resolution and screen resolution have the same aspect ratio, supposing the screen resolution is `1600x960`, enlarging the background image to 1600/800 = __2 times__ will perfectly fit the screen. This is the simplest situation, which will not be discussed in detail here. + +### When the aspect ratio of design resolution is more than that of screen resolution, the height should be adjusted to avoid black borders + +Supposing the screen resolution is `1024x768`, a red frame is used in the following picture to indicate the visible area of the device screen. We use `Fit Height` mode provided by the Canvas component to make the height of the design resolution automatically cover the height of the screen, i.e., enlarging the scene image to 768/480 = __1.6 times__. + +![fit height](multi-resolution/fit_height.png) + +This is a fairly good adaptation mode when the aspect ratio of the design resolution is more than that of the screen resolution. As illustrated above, although some parts of the background image will be cut down on the two sides of the screen, it can be ensured that no goof or black borders will appear in the visible area of the screen. Then the position of UI elements can be adjusted by the Widget, which makes sure that the UI elements will appear in the visible area of the screen. We will introduce this in detail in the next section, the [Widget Align](widget-align.md) documentation. + +### When the aspect ratio of the design resolution is less than that of screen resolution, the width should be adjusted to avoid black borders + +Supposing the screen resolution is `1920x960`, a red frame is also used in the following picture to indicate the visible area of the device screen. We use `Fit Width` mode provided by the Canvas component to make the width of the design resolution automatically cover the width of the screen, i.e., enlarging the scene to 1920/800 = __2.4 times__ + +![fit width](multi-resolution/fit_width.png) + +When the aspect ratio of the design resolution is relatively small, the use of this mode will cut down some parts of the background image on the upper/lower sides of the screen. + +### No matter how much the aspect ratio of the screen is, all the contents of design resolution will be completely displayed, and black borders are permitted + +In the last example, supposing the screen has a resolution of `640 x 960`. If you want to make sure the background image is completely displayed on the screen, you need to simultaneously open __Fit Height__ and __Fit Width__ in the Canvas component. The zooming in/out proportion of the scene image is calculated according to the smaller dimension in the screen resolution. In the example in the following picture, because the aspect ratio of the screen is less than 1, the zoom ratio will be calculated based on the width, that is, 640/800 = __0.8 times__. + +![show all](multi-resolution/show_all.png) + +Under such a displaying mode, there might be black borders on the screen or scene image that exceed the design resolution (goof). Although developers try their best to avoid black borders in general, if you want to make sure all the contents within the scale of design resolution are displayed on the screen, use this mode too. + +### According to the screen aspect ratio, `Fit Width` or `Fit Height` will be automatically selected + +If there is no strict requirement for the content that may be cropped down on the four sides of the screen, you can not enabled any adaptation mode in the Canvas component. At this time, __Fit Width__ or __Fix Height__ will be automatically selected according to the screen aspect ratio to avoid black border. In other words, when the aspect ratio of the design resolution is more than the screen resolution, __Fit Height__ will be automatically opened (as in the first picture above); when the aspect ratio of the design resolution is less than the screen resolution, __Fit Width__ will be automatically opened (as in the second picture above). + +### Design resolution can only be configured in the project settings + +The current design mode does not allow multi-resolution coexistence, so the design resolution of multiple Canvas in the project still uses the same set of design resolution and adaptation scheme, and you can configure it in __Project -> Project Setting -> General -> Default canvas setting__. + +![resolution-config](multi-resolution/resolution_config.png) diff --git a/versions/4.0/en/ui-system/components/engine/multi-resolution/canvas_property.png b/versions/4.0/en/ui-system/components/engine/multi-resolution/canvas_property.png new file mode 100644 index 0000000000..d9ad901f04 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/multi-resolution/canvas_property.png differ diff --git a/versions/4.0/en/ui-system/components/engine/multi-resolution/design_resolution.png b/versions/4.0/en/ui-system/components/engine/multi-resolution/design_resolution.png new file mode 100644 index 0000000000..6e9b88bcdb Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/multi-resolution/design_resolution.png differ diff --git a/versions/4.0/en/ui-system/components/engine/multi-resolution/fit_height.png b/versions/4.0/en/ui-system/components/engine/multi-resolution/fit_height.png new file mode 100644 index 0000000000..2533f7dbf7 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/multi-resolution/fit_height.png differ diff --git a/versions/4.0/en/ui-system/components/engine/multi-resolution/fit_width.png b/versions/4.0/en/ui-system/components/engine/multi-resolution/fit_width.png new file mode 100644 index 0000000000..c66f155eb7 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/multi-resolution/fit_width.png differ diff --git a/versions/4.0/en/ui-system/components/engine/multi-resolution/resolution_config.png b/versions/4.0/en/ui-system/components/engine/multi-resolution/resolution_config.png new file mode 100644 index 0000000000..16dc56d1ae Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/multi-resolution/resolution_config.png differ diff --git a/versions/4.0/en/ui-system/components/engine/multi-resolution/show_all.png b/versions/4.0/en/ui-system/components/engine/multi-resolution/show_all.png new file mode 100644 index 0000000000..f96c830ae0 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/multi-resolution/show_all.png differ diff --git a/versions/4.0/en/ui-system/components/engine/priority.md b/versions/4.0/en/ui-system/components/engine/priority.md new file mode 100644 index 0000000000..c6576a9de7 --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/priority.md @@ -0,0 +1,35 @@ +# Rendering Order + +## 2D rendering node ordering + +2D rendering nodes can be divided into nodes under Canvas and nodes that are not under Canvas: + +- - Nodes under Canvas use **[UI node ordering](#ui-node-ordering)** by default. Use the [Sorting2D component](./sorting-2d.md) for custom rendering order. + +- The nodes that are not under Canvas, the user can choose to enable depth detection and occlusion display of 3D objects through [custom materials](ui-material.md), which will render the occlusion according to the Z-axis coordinates of the object when enabled (see example [2d-rendering-in-3d](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/2D)). + +## UI node ordering + +UI nodes refer to the UI nodes under the Canvas node, the mixing of nodes is strictly sorted according to the node tree. The rendering order of the UI uses a **Breadth-First Sorting** scheme, and the node tree order is the final rendering data submission order. So the user can change the order of the nodes under their parents by setting the `siblingIndex` of the nodes and thus change the rendering order. + +For example: + +![priority.png](priority/priority.png) + +Therefore, the overall rendering order in the figure above is **A -> a1 -> a2 -> B -> b1 -> C**, and the rendering state on the screen is **C -> b1 -> B -> a2 -> a1 -> A**. i.e. **from top to bottom**. + +`setSiblingIndex` is used to change the position of the current node in the children array of the parent node. If set at runtime by script, the changed node tree data will not be serialized. If the parameter passed in is larger than the length of the children array, it will be set to the end of the array, and if it is within the range, it will be inserted into the corresponding position. This operation is related to the state of the node tree in real time, and the user needs to know the current state of the node tree and perform the operation to get the expected result. + +## Detailed Explanation + +**Sorting** is a very simple function, but the final rendering is based on the rendering capabilities provided by different platforms.
+Therefore, explain here. If you encounter an error in **UI rendering**, such as flickering or unwanted artifacts or other please consider the following. The first thing to check is the **ClearFlag** of all cameras (**Camera** and **Canvas**) in the scene, and make sure that the lowest **Canvas** or **Camera**'s **ClearFlag** property is set to **SOLID_COLOR** in each scene. + +To set the **ClearFlag** property, please refer to the following situations: + +- If there is only one **UI Canvas** or **3D Camera** in the scene, then the **ClearFlag** property is set to `Solid_Color`. +- If the scene contains 2D background layer, 3D scene layer, 2D UI layer, then: + - For cameras used for 3D scene rendering, make sure the first rendered camera is **SOLID_COLOR** (or set to **SKYBOX** if there is a skybox configured), and the rest of the cameras are determined by the project requirements. + - Cameras used for UI rendering (under Canvas) should be **DEPTH ONLY**. + - If a camera has a targetTexture set, set it to **SOLID_COLOR**. + ![sort](./priority/sort.png) diff --git a/versions/4.0/en/ui-system/components/engine/priority/intersperse.png b/versions/4.0/en/ui-system/components/engine/priority/intersperse.png new file mode 100644 index 0000000000..47817c23aa Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/priority/intersperse.png differ diff --git a/versions/4.0/en/ui-system/components/engine/priority/overlay.png b/versions/4.0/en/ui-system/components/engine/priority/overlay.png new file mode 100644 index 0000000000..95bd5f33f9 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/priority/overlay.png differ diff --git a/versions/4.0/en/ui-system/components/engine/priority/priority.png b/versions/4.0/en/ui-system/components/engine/priority/priority.png new file mode 100644 index 0000000000..7405de82d2 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/priority/priority.png differ diff --git a/versions/4.0/en/ui-system/components/engine/priority/sort.png b/versions/4.0/en/ui-system/components/engine/priority/sort.png new file mode 100644 index 0000000000..e9d544e6ee Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/priority/sort.png differ diff --git a/versions/4.0/en/ui-system/components/engine/render.png b/versions/4.0/en/ui-system/components/engine/render.png new file mode 100644 index 0000000000..54e39a5436 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/render.png differ diff --git a/versions/4.0/en/ui-system/components/engine/sliced-sprite.md b/versions/4.0/en/ui-system/components/engine/sliced-sprite.md new file mode 100644 index 0000000000..214a330a4e --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/sliced-sprite.md @@ -0,0 +1,34 @@ +# Use a Sliced Sprite to make a UI image + +The core design principle of the UI system is to automatically adapt to different device screen sizes. When developing the UI, we need to correctly set the each node's `size`, and which can be automatically stretched and adapted according to the screen size of the device. To achieve this, we usually use 9-sliced format images to render these nodes. In this way, even if small original images can be used to generate background images that can cover the entire screen. On the one hand, the game package is reduced, and on the other hand, it can be flexibly adapted to different layout requirements. + +![compare](sliced-sprite/compare.png) + +The right side of the picture above displays the texture of original size. The left side displays the effect of choosing Sliced mode and enlarging the `size` property. + +## Setting up your SpriteFrame for 9-slicing + +To use a 9-sliced image effect that can be infinitely enlarged, we need to cut the image asset into a 9-slicing at first. First open the __Sprite Editor__, select the image asset in __Assets__, then click the __Edit__ button on the bottom of __Inspector__. If the height of your window is not large enough, you might need to scroll __Inspector__ downward to see the button at the bottom. + +After opening __Sprite Editor__, you will see there is a green line around the image, which indicates the position of the current 9-sliced split line. Drag the mouse to the split line, you will see the shape of the cursor change, then you can press down and drag the mouse to modify the position of the split line. + +We click and drag the four split lines at the top, bottom, and sides respectively and cut the image into a 9-slicing. The nine areas will apply different zooming in/out strategies when the Sprite size changes, which is as illustrated below: + +![sliced](sliced-sprite/editing.png) + +And the following picture illustrates the state of zooming in/out in different areas (the picture comes from [Yannick Loriot's Blog](http://yannickloriot.com/2011/12/create-buttons-in-cocos2d-by-using-cccontrolbutton/)): + +![scaling](sliced-sprite/scaling.png) + +After cutting, don't forget to click the green check mark on the upper right corner of __Sprite Editor__ to save modifications to the asset. + +## Set the Sprite component to use Sliced mode + +After you have prepared the 9-sliced asset, you can modify the draw mode of the Sprite and modify the `size` to make a UI element that can specify any size. + +1. First, select the Sprite node in the __Scene/Hierarchy__, set the `Type` property of the Sprite as `Sliced` in the __Inspector__. +2. Then drag the control point with the [Rect Transform Tool](../../../editor/scene/index.md) to enlarge the `size` property of the node. You can also modify the `size` property value directly in the __Inspector__. Because the image asset has been set to 9-slicing, no matter how much the Sprite zooms in, there will be no vagueness or distortion. + +## Precautions + +When using tools or directly modifying the size property of Sliced ​​Sprite, note that the `size` property value cannot be negative, otherwise it cannot be displayed normally in Sliced ​​mode. diff --git a/versions/4.0/en/ui-system/components/engine/sliced-sprite/compare.png b/versions/4.0/en/ui-system/components/engine/sliced-sprite/compare.png new file mode 100644 index 0000000000..7fb2c339ca Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/sliced-sprite/compare.png differ diff --git a/versions/4.0/en/ui-system/components/engine/sliced-sprite/edit_from_asest.png b/versions/4.0/en/ui-system/components/engine/sliced-sprite/edit_from_asest.png new file mode 100644 index 0000000000..79516ede3b Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/sliced-sprite/edit_from_asest.png differ diff --git a/versions/4.0/en/ui-system/components/engine/sliced-sprite/editing.png b/versions/4.0/en/ui-system/components/engine/sliced-sprite/editing.png new file mode 100644 index 0000000000..5b84822683 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/sliced-sprite/editing.png differ diff --git a/versions/4.0/en/ui-system/components/engine/sliced-sprite/scaling.png b/versions/4.0/en/ui-system/components/engine/sliced-sprite/scaling.png new file mode 100644 index 0000000000..12e4832345 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/sliced-sprite/scaling.png differ diff --git a/versions/4.0/en/ui-system/components/engine/trim.md b/versions/4.0/en/ui-system/components/engine/trim.md new file mode 100644 index 0000000000..c8e42da1f1 --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/trim.md @@ -0,0 +1,37 @@ +# Auto Trim for SpriteFrame + +Once a texture is imported, the SpriteFrame asset generated with the texture will be trimmed automatically. Any fully transparent pixels around the image will be cropped. This will help us get the exact node size we need for Sprites. + +When the SpriteFrame is auto-trimmed, the auto-trimmed information in the image below is grayed out and cannot be modified: + +![trim inspector](trim/trim_inspector.png) + +## Trim Related Properties in Sprite Component + +There are two properties related to trim setting in __Sprite__ component: + +- `Trim` If checked, the node's bounding box will not include transparent pixels around the image. Instead the bounding box will be an exact fit to trimmed image. If unchecked the bounding box will be showing original texture including transparent pixels. + +- `Size Mode` Use the options in this property to set node's size to the original texture size or trimmed image size. Options are: + + - `TRIMMED` Select this option will set the `size` of the node to use trimmed image size of the current SpriteFrame used by Sprite component. + + - `RAW` Select this option will set the size of the node to use the original texture size, including transparent pixels. + + - `CUSTOM` This option make sure the size of the node will not be changed along with SpriteFrame, and should be managed by yourself. If you use the __Rect Transform Tool__ to drag and change the `size` of the node, or modify the `size` property in __Inspector__ panel, or modify the `width` or `height` in the script, the `Size Mode` property will be automatically set to `CUSTOM`. + +The following picture shows the comparison of two size modes: + +![trim compare](trim/trim-compare.png) + +## Sprite Animation with offset + +There are a lot of animator prefer to draw the moving motion in texture, commonly seen in attack animations. Usually animator will use a large texture and put character on different positions on the texture for different animation frames. In this case, we need to set the __Sprite__ component's `Trim` property to `false`, and set the `Size Mode` to `RAW`. In this way, the animation will use the original texture size when playing each sequence frame, and retain the information of transparent pixels around the image. So that the character's position drawn in the texture can be displayed correctly. + +When `Trim` property is set to `true`, it is more suitable for animation where the displacement is completely controlled by the character's `position` property. + +## TexturePacker Setting + +We recommend users to use sprite sheet tools such as [TexturePacker](https://www.codeandweb.com/texturepacker) for generating sprite animation texture assets. In TexturePacker before you publish your sprite sheet, please make sure you choose `Trim` in __Trim Mode__ setting of Sprites section. Please do not use `Crop, flush position`, or the trim information will be lost and you can't get back originial texture offset anymore. It is currently recommended to use version __4.x__ or higher for packaging to prevent the import failure caused by inconsistent export data in the lower version. + +![trim texturepacker](trim/trim-texturepacker.png) diff --git a/versions/4.0/en/ui-system/components/engine/trim/trim-compare.png b/versions/4.0/en/ui-system/components/engine/trim/trim-compare.png new file mode 100644 index 0000000000..89d1254809 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/trim/trim-compare.png differ diff --git a/versions/4.0/en/ui-system/components/engine/trim/trim-texturepacker.png b/versions/4.0/en/ui-system/components/engine/trim/trim-texturepacker.png new file mode 100644 index 0000000000..0ec775a8b8 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/trim/trim-texturepacker.png differ diff --git a/versions/4.0/en/ui-system/components/engine/trim/trim_inspector.png b/versions/4.0/en/ui-system/components/engine/trim/trim_inspector.png new file mode 100644 index 0000000000..45cf0e0af9 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/trim/trim_inspector.png differ diff --git a/versions/4.0/en/ui-system/components/engine/ui-batch.md b/versions/4.0/en/ui-system/components/engine/ui-batch.md new file mode 100644 index 0000000000..8ef38ef9d4 --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/ui-batch.md @@ -0,0 +1,90 @@ +# 2D Renderable Components Batching Guidelines + +## Prerequisites + +To batch render 2D renderable components, the following criteria are required: + +| Prerequisite | Description | +| :-- | :--- | +| Nodes with same visibility | All nodes with renderable components are rendered in camera order, if two sibling nodes doesn't have the same visibility in one camera, they can not be batched. | +| Renderable components are using the same material | With either different effect, defines or uniform values, gpu draws cannot be batched together, so this is the first thing to guarantee for batching elements in one draw call. | +| Material instances share the same settings for `BlendState` and `DepthStencilState` | `DepthStencilState` is automatically set by the engine to perform depth & stencil writing and testing. It is primarily used to implement mask component and depth test. In short, user need to remember elements inside masks and elements outside can not be batched together. | +| Vertex data are transferred in the same **buffer** (new in v3.4.1.) | Vertex data are automatically set by the engine under most scenarios and require no manual management. | +| Textures share the same source and sampler type | Sprites and Labels are unable to be batched due to the fact that labels are using independent textures. | + +## Batching Workflow Guidelines + +> **Note**: Cocos Creator renders components based on the hierarchy of the **Node Tree** structure. Batching is prone to be interrupted when encountering a node that doesn't follow the same prerequisite settings with previous one, thus it starts to create a new batch using its own prerequisite settings. Users are encouraged to manage their Node Tree structure to achieve an optimized outcome. + +Components that are prohibited from batching include: + +- Mask +- Graphics +- UIMeshRenderer + +These components are prohibited from batching due to utilization of a different material type and different data structures. + +Components below are also prohibited from render batching but follows their separate internal batching mechanism: + +- TiledMap +- Spine +- DragonBones + +Spites and Labels are in general unable to be batched together due to different texture types, but this can be circumvented by combining textures. The following is a workaround solution to achieve the same goal: +- For sprites, combine textures with [Auto Atlas](../../../asset/auto-atlas.md) and [Dynamic Atlas](../../../advanced-topics/dynamic-atlas.md). Texture Atlases can be batched with other components as long as the prerequisites are met. +- For labels, create a bitmap cache to combine textures allowing them to be batched with sprites. However, it is ill-advised to frequently alter the content of label texts once bitmap caches are created. + +It is recommended to optimize the Node Tree structure in conjunction with combing textures with Auto Atlas, Dynamic Atlas, and bitmap caches to achieve ideal batching results. + +## MeshBuffer Guidelines + +MeshBuffer is an internal data type used to store vertex and index data in 2D renderable components. Please be reminded that it is required for vertex data to be transferred in the same MeshBuffer for being batched successfully. The following scenarios will result in switching between MeshBuffers: + +### Before v3.4.1 + +The total vertex number in the scene exceeds the maximum capacity of a MeshBuffer (65535). + +### After v3.4.1 + +Render data structures are redesigned in v3.4.1. Please take note: + +1. Property **BATCHER2D_MEM_INCREMENT** under **Project -> Project Settings -> Macro Configurations** indicates the maximum memory size for a MeshBuffer. Increasing the value will allow a MeshBuffer to host more data to be rendered but will also increase memory consumption. + +2. **BATCHER2D_MEM_INCREMENT** is measured in **kilobytes**. Users can follow the instructions below to calculate the corresponding capacity for vertex numbers: + + Vertices in Cocos Creator are formatted as [vfmtPosUvColor](https://github.com/cocos/cocos4/blob/v4.0.0/cocos/2d/renderer/vertex-format.ts#L43), its definitions as follows: + + ```ts + export const vfmtPosUvColor = [ + // RGB32F represents three 32-bit float numbers. + new Attribute(AttributeName.ATTR_POSITION, Format.RGB32F), + // RG32F represents two 32-bit float numbers. + new Attribute(AttributeName.ATTR_TEX_COORD, Format.RG32F), + // RGBA32F represents four 32-bit float numbers. + new Attribute(AttributeName.ATTR_COLOR, Format.RGBA32F), + ]; + ``` + + It can be inferred that a vertex takes 9 `float` numbers, which in memory space equates to: `1 * 9 * 4 / 1024 (KB)`, specifically: + + - **1** is the number of vertices + + - **9** is the number of `floats` defined in each `vfmtPosUvColor` + + - **4** is the bytes occupied by each `float` + + - **1024** is the transference rate from bytes to kilobytes + + The default value for **BATCHER2D_MEM_INCREMENT** is 144KB with a capacity of `144 * 1024 / (9 * 4)= 4096` vertices. + + Please note that the maximum capacity for a MeshBuffer is **65535**. In other words, the maximum value for **BATCHER2D_MEM_INCREMENT** is `65535 * 9 * 4 / 1024 ≈ 2303.96` kilobytes. + +3. The core mechanism for 2D rendering is **static vertex buffer**, specifically: + + - **Static vertex buffer** presides over the whole lifespan of components while index buffer is filled on a per frame basis and does not implement memory caches in any form. Due to the volume of data in vertex buffer, it is advisable to store it separately to give access to specific memory segments while avoiding unnecessary updates. + + - Index buffer has a simpler data structure and lighter data volume compared to vertex buffer. Updating at each frame, index buffer defines the order for vertices to be rendered and takes an insignificant amount of processing power, requiring no sophisticated memory management. + + - Due to vertex buffer being static, it is advisable to preload vertex buffer at the very beginning of the component's lifespan. At loading, component will request relevant vertex buffers from MeshBuffer and returns them at destruction. + + - When MeshBuffer is unable to provide the vertex buffer requested by the component, the engine will create a new MeshBuffer allocated with the amount of memory space as indicated in **BATCHER2D_MEM_INCREMENT** so that vertex buffer can be successfully distributed. diff --git a/versions/4.0/en/ui-system/components/engine/ui-material.md b/versions/4.0/en/ui-system/components/engine/ui-material.md new file mode 100644 index 0000000000..30c7203acd --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/ui-material.md @@ -0,0 +1,28 @@ +# Custom Materials for 2D Rendering Objects + +Custom materials for 2D rendering objects are a best practice to extend the performance of 2D rendering objects and enhance the capabilities of 2D rendering objects themselves, allowing for cool rendering effects such as dissolve and glow. + +Most of the 2D renderable components in v3.0 support the use of custom materials, with the following interface (using the Sprite component as an example). + +![UIMaterial](ui-material/UIMaterial.png) + +The usage is no different from other built-in materials, just drag and drop the material to be used into the **CustomMaterial** property box, but there are some points to note as follows: + +1. When no custom material is specified, the built-in material will be used for rendering, please refer to the [Sprite Component Reference](../editor/sprite.md) documentation. +2. 2D rendering objects do not support multiple materials, the maximum number of custom materials is one. +3. Please use a 2D-specific shader such as **builtin-spine** or **builtin-sprite** to customize materials, do not choose a shader used by other 3D components. +4. The **Grayscale** property on the panel is disabled when a custom material for 2D rendering objects is used, and the user can choose to implement this feature in the material itself. +5. If the BlendFactor is set in the code, when a custom material is used, the BlendFactor setting in the custom material will prevail. +6. When a custom material is used, the depth detection information of the component will be based on the material. To achieve occlusion with 3D objects, please use custom materials and turn on depth detection. See the example [2d-rendering-in-3d](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/2D). + +7. If you want to change the properties of the custom material, it can be done by getting the **customMaterial** on the 2D renderer component as following code( take Sprite for example): + + ```ts + let spriteComp = this.node.getComponent(Sprite); + let material = spriteComp.customMaterial; + //material.setProperty(proName,val) + ``` + +## Write your own 2D Shader + +If the built-in shaders does not meet the demand, please refer to the [2D Sprite Effect:Gradient](../../../shader/write-effect-2d-sprite-gradient.md) to customize the shader. diff --git a/versions/4.0/en/ui-system/components/engine/ui-material/UIMaterial.png b/versions/4.0/en/ui-system/components/engine/ui-material/UIMaterial.png new file mode 100644 index 0000000000..94ee731b7e Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/ui-material/UIMaterial.png differ diff --git a/versions/4.0/en/ui-system/components/engine/ui-material/dissolve.png b/versions/4.0/en/ui-system/components/engine/ui-material/dissolve.png new file mode 100644 index 0000000000..fb87d4121c Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/ui-material/dissolve.png differ diff --git a/versions/4.0/en/ui-system/components/engine/usage-ui.md b/versions/4.0/en/ui-system/components/engine/usage-ui.md new file mode 100644 index 0000000000..9bca95a414 --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/usage-ui.md @@ -0,0 +1,8 @@ +# UI Practice Guide + +- [Multi-Resolution Adaptation Scheme](multi-resolution.md) +- [Alignment Strategy](widget-align.md) +- [Label Layout](label-layout.md) +- [Auto Layout Container](auto-layout.md) +- [Create a List of Dynamically Generated Content](list-with-data.md) +- [Use a Sliced Sprite to make a UI image](sliced-sprite.md) diff --git a/versions/4.0/en/ui-system/components/engine/widget-align.md b/versions/4.0/en/ui-system/components/engine/widget-align.md new file mode 100644 index 0000000000..6d10e9678e --- /dev/null +++ b/versions/4.0/en/ui-system/components/engine/widget-align.md @@ -0,0 +1,91 @@ +# Alignment Strategy + +To achieve a perfect multi-resolution fit effect, presenting UI elements according to the positions stipulated in the design resolution is not enough. When the width and height of the screen change, UI elements must be able to intelligently sense the positions of the borders of the screen to make sure that they are presenting themselves in the visible area of the screen and being distributed in suitable positions. We can do this with the **Widget** component. + +Next, we categorize different alignment workflows according to the categories of elements that need to be aligned: + +## Aligning Buttons and Small Elements with the Border + +For elements with relatively small areas like a pause menu, gold coins in the game, etc., normally, aligning them by the borders of the screen would be enough. Only a few simple steps are needed: + +1. Creating a 2D object in the **Hierarchy** panel will automatically create a Canvas node as its parent by default, and these element nodes need to be placed under the Canvas node. +2. Add the Widget component to element nodes. +3. To align something with the bottom left corner of the screen for example, check the `Left` and `Bottom` tick boxes in the Widget component. +4. Then set up the distance between the node and the borders of the screen. In the picture below, the left margin is set as 40px, the bottom margin is set as 30px. + + ![align left bottom](widget-align/align-basic.png) + +After setting up the Widget component like this, no matter what the actual screen resolution is, this node element will remain at the bottom left corner of the screen. The distance between the left side of the node's bounding box and left border of the screen remains at 40px. The distance between the bottom of the node's bounding box and the bottom of the screen remains at 30px. + +> **Note**: the alignment distance provided by the Widget component refers to the border of the bounding box in the same direction as the child node and the parent node.
+> For example, `Left` is ticked on in the above example to align the element with the left border, then the distance between the left border of the child node's bounding box and the left border of the parent node's bounding box is the set value 40px, where the parent node is the Canvas node and the bounding box is constantly the same size as the screen, provided that only the **Fit Width** or **Fit Height** option is checked in the **Project -> Project Settings -> Project Data** in the top menu bar of the editor. + +## Nest Alignment Elements + +We just showed how to align something with the border of the screen in the example above. + +Because the default alignment reference of Widget is the parent node, we can add different node hierarchies and make the nodes on every hierarchy use the auto alignment function. + +Here is a simple example to explain it. Suppose we have a node hierarchy as follows: + +![nested nodes](widget-align/hierarchy.png) + +In the example above, `parent` is a panel, and `button` is a button. We can add Widget component to both of these nodes, and respectively set their alignment distance. + +For the `parent` node, the distance of aligning the top left corner of the `Canvas` node remains at 80px: + +![nested outer element](widget-align/nested-outer.png) + +For the `button` node, the distance of aligning the top left corner of the `parent` node remains at 50px: + +![nested inner element](widget-align/nested-inner.png) + +With a workflow like this, we can group UI elements according to their display areas or functions, and elements of a different hierarchy can be aligned according to design. + +## Automatically zooming in/out the size of a node according to alignment requirements + +In the above example, two borders that simultaneously align on one axis in opposite directions don't exist. If we want to make a panel that fully covers the width of the whole screen, we can simultaneously tick off the alignment switches `Left` and `Right`: + +![stretch](widget-align/stretch.png) + +When simultaneously ticking off the alignment switches in opposite directions, Widget obtains the ability of modifying the `Size` of the node according to alignment requirements. In the picture above, we ticked off the left and right directions and set up margins, then Widget can dynamically set up the `Width` property of the node according to the width of the parent node. As a result, no matter how wide the screen is, the distance between the panel and the left & right borders of the screen remains at 100px permanently. + +### Create a node whose size is in accordance with the size of screen + +Making use of the features of a node that can automatically zoom in/out, we can make the size of the node the same with that of the screen by setting up the Widget component of the node. + +To make such a node, we should first make sure that the size of the parent node of this node remains the same with that of the screen. The Canvas node is the best choice. Next, set up the Widget component of this node according to the following method: + +![full screen node](widget-align/full-screen.png) + +Therefore the size of the node will remain constantly the same with that of the Canvas node when running, i.e., the same as the size of the screen. After being set up like this, the child node of this node can transmit the same screen size by the same settings. + +> **Note**: for this to work, only the **Fit Width** or **Fit Height** option can be checked in the **Project -> Project Settings -> Project Data** in the top menu bar of the editor. + +## Set up percentage alignment distance + +After the alignment in a certain direction is enabled on the Widget component, in addition to specifying the margin in pixels, we can also input a percentage value (For example: by clicking the symbol circled in the box), therefore, Widget will multiply the width or height of the parent node on the corresponding axis by the input percentage to get the value of the actual margin. + +Let's take a look at a real example. Take a child node that has been directly put under Canvas as an example. We hope this node panel remains on the right side of the screen and constantly covers 60% of the total height of screen. Therefore, setting up the Widget component according to the following picture will realize this effect: + +![percentage](widget-align/percentage.png) + +When inputting the margin value when opening alignment direction, Widget can use the pixel unit together with percentage unit according to various needs. For example, input `50%` on the `Left` direction that needs to align with the center of screen and input `20px` on the `Right` direction that needs to align with the right of screen; when calculating the position and size of the child node at last, all the margins will be positioned after being converted to pixel distance according to the size of the parent node. + +Making use of the percentage alignment distance, we can create UI elements that can zoom in/out infinitely according to the size of the screen. Exerting your imagination, fitting a thousand types of phones with one set of resources will be a piece of cake! + +## Update alignment and optimization strategies for every frame at runtime + +Widget component is generally used to locate the position of each element when the scene is initialized on the target device, but once the scene is initialized, we often do not need to use the Widget component for alignment. The `alignOnce` property is used to ensure that the Widget component only performs alignment and positioning at initialization, and no longer consumes time for alignment at runtime. + +![alignOnce](widget-align/align-once.png) + +If **AlignMode** is set to **ONCE** or **ON_WINDOW_RESIZE**, and the alignment is performed once when the component is initialized, the engine will automatically set the `enabled` property of the Widget component to `false`, disabling the Widget component to disable the automatically update for subsequent every frame. + +If the alignment needs to be changed at runtime, manually set the **AlignMode** mode to **ALWAYS**, or manually traverse the Widgets that need to be aligned and set their `enabled` property to `true` when the alignment needs to be updated each frame at runtime. + +For a scene with many UI elements, ensuring that the **AlignMode** property of the Widget component is set to **ON_WINDOW_RESIZE**, which can significantly improve the running performance of the scene. + +## Limitation on the position and size of node + +When the Widget component enables one or more alignment settings, the `position`, `width` and `height` properties of the node may be restricted and cannot be freely modified through the API or __Animation__ panel. If you need to modify the position or size of the alignment node at runtime, please refer to the [Widget Component: Limitation on node position control](../editor/widget.md#limitation-on-node-position-control) for details. diff --git a/versions/4.0/en/ui-system/components/engine/widget-align/align-basic.png b/versions/4.0/en/ui-system/components/engine/widget-align/align-basic.png new file mode 100644 index 0000000000..2377651743 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/widget-align/align-basic.png differ diff --git a/versions/4.0/en/ui-system/components/engine/widget-align/align-once.png b/versions/4.0/en/ui-system/components/engine/widget-align/align-once.png new file mode 100644 index 0000000000..068c2d2e6d Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/widget-align/align-once.png differ diff --git a/versions/4.0/en/ui-system/components/engine/widget-align/full-screen.png b/versions/4.0/en/ui-system/components/engine/widget-align/full-screen.png new file mode 100644 index 0000000000..d5d2636de4 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/widget-align/full-screen.png differ diff --git a/versions/4.0/en/ui-system/components/engine/widget-align/hierarchy.png b/versions/4.0/en/ui-system/components/engine/widget-align/hierarchy.png new file mode 100644 index 0000000000..0f36761f41 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/widget-align/hierarchy.png differ diff --git a/versions/4.0/en/ui-system/components/engine/widget-align/nested-inner.png b/versions/4.0/en/ui-system/components/engine/widget-align/nested-inner.png new file mode 100644 index 0000000000..b6dd21e08c Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/widget-align/nested-inner.png differ diff --git a/versions/4.0/en/ui-system/components/engine/widget-align/nested-outer.png b/versions/4.0/en/ui-system/components/engine/widget-align/nested-outer.png new file mode 100644 index 0000000000..1093817200 Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/widget-align/nested-outer.png differ diff --git a/versions/4.0/en/ui-system/components/engine/widget-align/percentage.png b/versions/4.0/en/ui-system/components/engine/widget-align/percentage.png new file mode 100644 index 0000000000..f71dd5af3e Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/widget-align/percentage.png differ diff --git a/versions/4.0/en/ui-system/components/engine/widget-align/stretch.png b/versions/4.0/en/ui-system/components/engine/widget-align/stretch.png new file mode 100644 index 0000000000..83b13e101b Binary files /dev/null and b/versions/4.0/en/ui-system/components/engine/widget-align/stretch.png differ diff --git a/versions/4.0/en/xr/architecture/ar-camera.md b/versions/4.0/en/xr/architecture/ar-camera.md new file mode 100644 index 0000000000..1f3c90f666 --- /dev/null +++ b/versions/4.0/en/xr/architecture/ar-camera.md @@ -0,0 +1,21 @@ +# AR Camera + +Similar to head-mounted displays, to abstract the camera with AR capabilities on mobile devices in the scene, the XR plugin uses the AR Camera component to encapsulate a series of properties that map to the camera AR functionality of physical devices. + +![ar-camera-node](ar-camera/ar-camera-node.png) + +The AR Camera object consists of three essential components: `cc.Camera`, `cc.PoseTracker`, and `cc.ARCameraMgr`. + +ar-camera-comp + +## cc.Camera + +`cc.Camera` is the camera component provided by Cocos Creator. To ensure a good experience, it is recommended to set the `Clear Flags` to `SOLID_COLOR` and the `Near Plane` to `0.01`. For more information about camera parameters, please refer to the [Camera Component](../../editor/components/camera-component.md) documentation. + +## `cc.PoseTracker` + +`cc.PoseTracker` is used to synchronize the pose information of the physical device to the AR Camera, ensuring that the camera can render virtual content correctly and overlay it with the video stream. Unlike XR HMD, when adapting to handheld devices on mobile, the `Tracking Type` should be set to `VIEW_POSE_ACTIVE_HANDHELD`. + +## cc.ARCameraMgr + +`cc.ARCameraMgr` is a component used to manage the AR camera functionality. For detailed property descriptions, please refer to the **Device Mapping Component -> ARCameraMgr**. \ No newline at end of file diff --git a/versions/4.0/en/xr/architecture/ar-camera/ar-camera-comp.png b/versions/4.0/en/xr/architecture/ar-camera/ar-camera-comp.png new file mode 100644 index 0000000000..f6c87447e3 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-camera/ar-camera-comp.png differ diff --git a/versions/4.0/en/xr/architecture/ar-camera/ar-camera-node.png b/versions/4.0/en/xr/architecture/ar-camera/ar-camera-node.png new file mode 100644 index 0000000000..886ccc6a43 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-camera/ar-camera-node.png differ diff --git a/versions/4.0/en/xr/architecture/ar-feature-introduce.md b/versions/4.0/en/xr/architecture/ar-feature-introduce.md new file mode 100644 index 0000000000..a2904c6501 --- /dev/null +++ b/versions/4.0/en/xr/architecture/ar-feature-introduce.md @@ -0,0 +1,3 @@ +# AR Features + +Developers often need to publish AR applications for different platforms. To mitigate the challenges posed by platform differences, Cocos CreatorXR provides a unified set of high-level AR components to support the design and development of augmented reality applications. When using AR components, developers do not need to worry about the functional differences of various AR SDKs and can focus on business-related development. diff --git a/versions/4.0/en/xr/architecture/ar-feature-support.md b/versions/4.0/en/xr/architecture/ar-feature-support.md new file mode 100644 index 0000000000..7234d88fb9 --- /dev/null +++ b/versions/4.0/en/xr/architecture/ar-feature-support.md @@ -0,0 +1,29 @@ +# AR Features on Platforms + +The current version of the AR extension supports the following AR features: + +| AR Feature | Description | +| :-------------- | :----------------------------------------------------------- | +| AR Session | Enables, disables, and configures the AR session on the target platform to control the lifecycle of the AR application. | +| Device Tracking | Tracks the device's position and rotation in physical space. | +| AR Camera | Renders the live video feed from the device's camera and provides environment-based lighting estimation. | +| Plane Tracking | Detects and tracks flat surfaces in the physical world. | +| Image Tracking | Detects and tracks specific images in the physical world. | +| Hit Detection | Supports ray casting for hit detection against tracked entities. | +| Anchors | Tracks fixed points in the scene space. | +| Meshing | Converts the physical world into a mesh representation. | +| Light Estimate | Provides lighting estimation in the AR environment. | + +Cocos CreatorXR v1.1.0 provides the following AR feature support for each platform: + +| AR Feature/Platform | ARKit | ARCore | AREngine | Spaces | +| :-------------- | :---- | :----- | :------- | :----- | +| AR Session | ✓ | ✓ | ✓ | ✓ | +| Device Tracking | ✓ | ✓ | ✓ | ✓ | +| AR Camera | ✓ | ✓ | ✓ | ✓ | +| Plane Tracking | ✓ | ✓ | ✓ | ✓ | +| Image Tracking | ✓ | ✓ | ✓ | ✓ | +| Hit Detection | ✓ | ✓ | ✓ | | +| Anchors | ✓ | ✓ | ✓ | | +| Meshing | ✓ | | | | +| Light Estimate | | ✓ | ✓ | | diff --git a/versions/4.0/en/xr/architecture/ar-interaction.md b/versions/4.0/en/xr/architecture/ar-interaction.md new file mode 100644 index 0000000000..68e9bf23dd --- /dev/null +++ b/versions/4.0/en/xr/architecture/ar-interaction.md @@ -0,0 +1,77 @@ +# AR Interaction + +AR interaction is mainly driven by the `cc.ScreenTouchInteractor` component, which converts touch events into gestures such as tap, drag, and pinch. The interactor passes these gestures to interactive virtual objects, triggering corresponding behaviors. + +## Gesture Interaction + +The AR gesture interactor component converts screen touches into gestures. Cocos Creator's input system passes gesture signals to interactive objects, which respond to gesture events by transforming their behaviors. To enable interactive behaviors, the interactive objects must be bound with the `cc.Selectable` component. For a detailed description of the properties of this component, see the [XR Component - Selectable](component.md#Selectable). + +To use the **Screen Touch Interactor**, right-click in the Hierarchy and create **XR -> Screen Touch Interactor**. + +screen-touch-interactor-node + +Create a 3D object of your choice (using a Cube as an example). + +Modify the `scale` property of the Cube to (0.1, 0.1, 0.1), making it an actual size of 1000cm³. Modify the `position` property to (0, -0.1, -0.5), placing it 50cm away from the origin in space and 10cm below, and add the **XR > Interaction -> Selectable** component. + +![create-3d-obj](ar-interaction/create-3d-obj.png) + +add-selectable + +Next, let's create a selection effect. + +In the `Assets` folder, create a prefab named "Selected Visualizer". + +![create-selected-visualizer](ar-interaction/create-selected-visualizer.png) + +Under the root node of the prefab, create another Cube object with the same properties, but set the `scale` to 1.2 times based on the parent node. + +![set-selected-prefab](ar-interaction/set-selected-prefab.png) + +Create a new material to highlight the selected state. + +![set-selected-material](ar-interaction/set-selected-material.png) + +Adjust the material settings. It is recommended to select "builtin-unlit" for the `effect` and "1-transparent" for the `technique`. + +adjust-material + +After creating the material, apply it to the `cc.MeshRenderer` of the Cube in the prefab to complete the selection effect. + +![refer-to-material](ar-interaction/refer-to-material.png) + +Finally, apply the prefab to the `Selected Visualization` property of the `cc.Selectable` component. + +![refer-to-selected-visualizer](ar-interaction/refer-to-selected-visualizer.png) + +During runtime, you can move, rotate, and scale the virtual object by combining gestures. + +![select-effect](ar-interaction/select-effect.png) + +## Placement + +When using the **Screen Touch Interactor**, the AR hit test capability is enabled. It calculates the collision between the touch position on the screen, converted to the camera's coordinates, and the AR plane using Ray Cast. This way, it obtains the position of the collision point and places virtual objects in that position. The objects that can be placed must be equipped with the [`cc.Placeable` component](component.md#Placeable). + +Using the previously created selectable object as an example, let's give it the ability to be placed. + +Select the Cube object in the scene and add the **XR -> Interaction -> Placeable** component to it. + +![add-placeable](ar-interaction/add-placeable.png) + +Drag this scene node into the Assets panel to generate a prefab, then delete the Cube object in the scene. + +![create-placement-prefab](ar-interaction/create-placement-prefab.png) + +Drag this scene node into the Assets panel to generate a prefab, then delete the Cube object in the scene. + +Refer to the newly generated Cube prefab in the **Screen Touch Interactor -> Place Action > Placement Prefab** property. Set the `Calculation Mode` to `AR_HIT_DETECTION`. + +![refer-to-placement-prefab](ar-interaction/refer-to-placement-prefab.png) + +The placement calculation relies on `AR planes`, so we need to create a `Plane Tracking` node to request the device to activate the plane detection capability of the AR SDK. Right-click in the Hierarchy panel and create **XR -> Plane Tracking** to create the plane agent node. + +![create-plane-tracking](ar-interaction/create-plane-tracking.png) + +Once all the work is done, you can build and publish the project to see the placement effect during runtime. + +![interacion-effect](ar-interaction/interacion-effect.png) diff --git a/versions/4.0/en/xr/architecture/ar-interaction/add-placeable.png b/versions/4.0/en/xr/architecture/ar-interaction/add-placeable.png new file mode 100644 index 0000000000..7674573609 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/add-placeable.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/add-selectable.png b/versions/4.0/en/xr/architecture/ar-interaction/add-selectable.png new file mode 100644 index 0000000000..d265d65408 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/add-selectable.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/adjust-material.png b/versions/4.0/en/xr/architecture/ar-interaction/adjust-material.png new file mode 100644 index 0000000000..a72989375c Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/adjust-material.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/create-3d-obj.png b/versions/4.0/en/xr/architecture/ar-interaction/create-3d-obj.png new file mode 100644 index 0000000000..0e240f9e41 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/create-3d-obj.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/create-placement-prefab.png b/versions/4.0/en/xr/architecture/ar-interaction/create-placement-prefab.png new file mode 100644 index 0000000000..1ed0837be8 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/create-placement-prefab.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/create-plane-tracking.png b/versions/4.0/en/xr/architecture/ar-interaction/create-plane-tracking.png new file mode 100644 index 0000000000..9ff0af8f05 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/create-plane-tracking.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/create-selected-visualizer.png b/versions/4.0/en/xr/architecture/ar-interaction/create-selected-visualizer.png new file mode 100644 index 0000000000..579055da93 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/create-selected-visualizer.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/interacion-effect.png b/versions/4.0/en/xr/architecture/ar-interaction/interacion-effect.png new file mode 100644 index 0000000000..487a8e2b66 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/interacion-effect.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/refer-to-material.png b/versions/4.0/en/xr/architecture/ar-interaction/refer-to-material.png new file mode 100644 index 0000000000..f242c5610c Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/refer-to-material.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/refer-to-placement-prefab.png b/versions/4.0/en/xr/architecture/ar-interaction/refer-to-placement-prefab.png new file mode 100644 index 0000000000..bf195174f2 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/refer-to-placement-prefab.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/refer-to-selected-visualizer.png b/versions/4.0/en/xr/architecture/ar-interaction/refer-to-selected-visualizer.png new file mode 100644 index 0000000000..e7ce669eed Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/refer-to-selected-visualizer.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/screen-touch-interactor-node.png b/versions/4.0/en/xr/architecture/ar-interaction/screen-touch-interactor-node.png new file mode 100644 index 0000000000..b9cc8ed92b Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/screen-touch-interactor-node.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/select-effect.png b/versions/4.0/en/xr/architecture/ar-interaction/select-effect.png new file mode 100644 index 0000000000..8f2da713f5 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/select-effect.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/selected-effect.png b/versions/4.0/en/xr/architecture/ar-interaction/selected-effect.png new file mode 100644 index 0000000000..487a8e2b66 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/selected-effect.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/set-selected-material.png b/versions/4.0/en/xr/architecture/ar-interaction/set-selected-material.png new file mode 100644 index 0000000000..a572e38c5a Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/set-selected-material.png differ diff --git a/versions/4.0/en/xr/architecture/ar-interaction/set-selected-prefab.png b/versions/4.0/en/xr/architecture/ar-interaction/set-selected-prefab.png new file mode 100644 index 0000000000..4d5bcef1de Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-interaction/set-selected-prefab.png differ diff --git a/versions/4.0/en/xr/architecture/ar-introduce.md b/versions/4.0/en/xr/architecture/ar-introduce.md new file mode 100644 index 0000000000..15a5815e97 --- /dev/null +++ b/versions/4.0/en/xr/architecture/ar-introduce.md @@ -0,0 +1,8 @@ +# AR + +Cocos CreatorXR extension allows you to create cross-platform Augmented Reality (AR) applications. You can add corresponding AR components to nodes in your scene to choose which AR features to enable, without worrying about the differences between AR SDKs of different platforms. When you build and run the application on an AR device, the underlying AR module of the Cocos Creator engine will invoke the native AR SDK of the device to enable AR functionality. This allows you to develop once and publish to multiple AR platforms. + +**Version Requirements**: + +- Use Cocos Creator v3.7.1 or higher. +- Use xr-plugin v1.1.0 or higher. diff --git a/versions/4.0/en/xr/architecture/ar-manager.md b/versions/4.0/en/xr/architecture/ar-manager.md new file mode 100644 index 0000000000..1b9a37281d --- /dev/null +++ b/versions/4.0/en/xr/architecture/ar-manager.md @@ -0,0 +1,59 @@ +# AR Manager + +The AR module of Cocos CreatorXR provides a global manager for collecting and managing the AR features used in the current project. Each feature in the feature manager has global properties, and adjusting these parameters will modify the device-specific or project-wide functionality. The `cc.ARManager` is automatically attached to the `XR Agent` node by default. When you create AR automation behavior nodes, the `ARManager` will collect these nodes into their corresponding feature lists for easy management and maintenance of all feature nodes. + +The corresponding global feature properties are provided for the supported AR features in the current version. + +## Plane Tracking + +When you create one or more Plane Tracking nodes in the scene, the `Configuration` in the `ARManager` will have an additional `Plane Feature` property. You can adjust the parameters under the feature or locate the corresponding feature node. + +![plane-tracking-node](ar-manager/plane-tracking-node.png) + +plane-feature-manager + +- `Direction Type`: Collects the orientations of all plane proxies that need to be recognized in the current scene. + +- `Tracking Visualizer`: Creates default visualization models for all plane proxies. + +- `Tracking Quality Condition`: Represents the minimum quality of the tracked plane proxy. When the stability quality is below this value, the plane will not be visualized. + +- `Use Plane Shape`: When enabled, the visualization effect will use polygon rendering based on the real physical shape of the plane in the real environment. When disabled, quad rendering will be used instead. + +- `Unsupported Event`: Triggered when plane tracking is not supported by the device. Users can add events according to their needs. + +## Image Tracking + +When you create one or more `Image Tracking` nodes in the scene, the `Configuration` in the `ARManager` will have an additional `Image Feature` property. You can adjust the parameters under the feature or locate the corresponding feature node. + +![image-feature-node](ar-manager/image-feature-node.png) + +image-feature-manager + +The `Max Tracking Number` on each platform represents the maximum number of images that can be simultaneously tracked within the camera's field of view. This value can be dynamically modified as needed. + +Note: The upper limit of `Max Tracking Number` varies depending on the device platform. Currently known limits are: + +- ARCore: The platform can track up to 20 images simultaneously, with a maximum inventory of 1000 individual images. +- ARKit: The platform can track up to 4 images simultaneously, with a maximum inventory of 100 images. +- AREngine: The platform can track up to 1 image simultaneously. + +The `Unsupported Event` will be triggered when image tracking is not supported by the device. Users can add events according to their needs. + +## Meshing(Experimental) + +When you create one or more `Meshing` nodes in the scene, the `Meshing Feature` property will be added to the `ARManager`'s configuration. Since the Meshing feature is experimental and requires high hardware requirements for environmental reconstruction, it does not currently support parameter control for this feature. + +meshing-manager + +`Normals` are enabled by default, allowing you to obtain normal vectors based on the mesh information. + +## APIs + +`ARManager` also provides interfaces to control feature switches and feature visualization switches: + +```typescript +public enableFeatureTracking (type: ARTrackingType, enable: boolean); +public showAllVisualizer (type: ARTrackingType); +public hideAllVisualizer (type: ARTrackingType); +``` diff --git a/versions/4.0/en/xr/architecture/ar-manager/image-feature-manager.png b/versions/4.0/en/xr/architecture/ar-manager/image-feature-manager.png new file mode 100644 index 0000000000..03eea3557f Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-manager/image-feature-manager.png differ diff --git a/versions/4.0/en/xr/architecture/ar-manager/image-feature-node.png b/versions/4.0/en/xr/architecture/ar-manager/image-feature-node.png new file mode 100644 index 0000000000..991787e800 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-manager/image-feature-node.png differ diff --git a/versions/4.0/en/xr/architecture/ar-manager/meshing-manager.png b/versions/4.0/en/xr/architecture/ar-manager/meshing-manager.png new file mode 100644 index 0000000000..fe579d0336 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-manager/meshing-manager.png differ diff --git a/versions/4.0/en/xr/architecture/ar-manager/plane-feature-manager.png b/versions/4.0/en/xr/architecture/ar-manager/plane-feature-manager.png new file mode 100644 index 0000000000..77c7bc88b1 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-manager/plane-feature-manager.png differ diff --git a/versions/4.0/en/xr/architecture/ar-manager/plane-tracking-node.png b/versions/4.0/en/xr/architecture/ar-manager/plane-tracking-node.png new file mode 100644 index 0000000000..442f2edae3 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-manager/plane-tracking-node.png differ diff --git a/versions/4.0/en/xr/architecture/ar-sdk-summary.md b/versions/4.0/en/xr/architecture/ar-sdk-summary.md new file mode 100644 index 0000000000..ae4a6cc040 --- /dev/null +++ b/versions/4.0/en/xr/architecture/ar-sdk-summary.md @@ -0,0 +1,13 @@ +# Supported Devices for AR SDKs + +## ARKit + +Compatible with devices running iOS 12.0 or later. The `Meshing` feature requires devices to have **LiDAR**. The development environment requires Xcode 12.4 or later. + +## ARCore + +Refer to Google's official documentation: [Supported Devices for ARCore | Google Developers](https://developers.google.com/ar/devices?hl=zh-cn). Alternatively, check if your Android phone comes pre-installed with the "Google Play Store" and "Google Play Services for AR" applications or not. + +AREngine: + +Please refer to Huawei's official documentation: [Introduction to AR Engine | huawei.com](https://developer.huawei.com/consumer/cn/doc/development/graphics-Guides/introduction-0000001050130900). diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component.md b/versions/4.0/en/xr/architecture/ar-tracking-component.md new file mode 100644 index 0000000000..de7a687619 --- /dev/null +++ b/versions/4.0/en/xr/architecture/ar-tracking-component.md @@ -0,0 +1,99 @@ +# AR Automated Behavior Editing + +In AR scenes, there is always an unknown dependency between virtual objects and real entities. If we can clearly and conveniently describe the conditional characteristics of real entities and execute matching behaviors based on these conditions, it can greatly simplify the handling of complex AR features for developers and allow them to focus on writing business-related code. Cocos CreatorXR provides AR automated behavior editing components that abstract common physical features and logical behaviors into elements that developers can freely combine. The graphical interface significantly reduces the cost and entry barrier for AR application development. + +Each automated behavior editing component for a specific feature has its own unique feature library and action library. The following describes all the AR features supported by the automated behavior editing in the current version: + +## Plane Tracking + +Right-click in the **Hierarchy** panel and choose **Create -> XR -> Plane Tracking** to create a plane agent node that can describe a plane entity in the physical world. + +create-plane-tracking-node + +Select the created **Plane Tracking** node, and in the **Inspector** panel, you can see the default `cc.ARPlaneTracking` component that has been added. Select the **Factor** or **Action** tab to view the existing features or actions. Click "Add Factor" or "Add Action" to add new items from the feature library or action library. + +plane-tracking-factors + +plane-tracking-actions + +Drag the virtual object that needs to be displayed under the created **Plane Tracking** node and adjust its `size` and `scale` accordingly. Add the **Display** action item in the **Actions** section (already added by default), and the virtual object will be displayed when a plane that meets the conditions is recognized during runtime. + +![plane-tracking-display](ar-tracking-component/plane-tracking-display.png) + +![plane-tracking-effect](ar-tracking-component/plane-tracking-effect.png) + +## Image Tracking + +**Image Tracking** allows you to use the device's AR capabilities to recognize 2D image resources at runtime. + +Right-click in the Hierarchy and select **Create -> XR -> Image Tracking** to create an image agent node that can describe an image entity in the physical world. + +image-tracking-node + +Select the created **Image Tracking** node, and in the Inspector panel, you can see the default `cc.ARImageTracking` component that has been added. In the **Factor** tab, under the `Image Source` property, add a new image resource. + +image-tacking-comp + +Drag and drop or directly select an image resource from the `Assets` window in the `Image` property. In the scene editor, you can see the currently referenced image and set its default physical size. + +![set-image-source](ar-tracking-component/set-image-source.png) + +Drag the virtual object that needs to be displayed under the created **Image Tracking** node and adjust its `size` and `scale` accordingly. Add the **Display** action item in the **Actions** section (already added by default), and the virtual object will be displayed when the image is recognized during runtime. + +![image-tracking-display](ar-tracking-component/image-tracking-display.png) + +![image-tracking-effect](ar-tracking-component/image-tracking-effect.png) + +## **Meshing(Experimental)** + +Currently, the **Meshing** feature only works on iOS devices with depth scene reconstruction capabilities (such as iPhone/iPad Pro series with LiDAR scanner). **Meshing** allows you to create 3D meshes based on the real environment. + +Right-click in the Hierarchy and select **Create -> XR -> Meshing** to create a Meshing agent node. + +meshing-node + +In the `cc.ARMeshing` component's `Mesh Visualizer` property, select the desired mesh visualization effect. + +meshing-comp + +This will enable real-time surface meshing based on the physical environment during runtime. + +![meshing-effect](ar-tracking-component/meshing-effect.jpeg) + +## Feature Library + +Currently, you can add the following features: + +| Feature | Attribute | Description | +| --------------- | -------------------- | ------------------------------------------------------------ | +| Plane Direction | | Specifies the required orientation for the plane. The plane can be set to a specific direction (horizontal, vertical, or other). | +| Plane Size | | Sets the required size for the plane. When setting the size, you can choose to restrict the minimum/maximum size range of the plane. | +| Image Source | Image | Sets the image resource for Image Tracking. | +| | Enable Physical Size | When enabled, the recognized image will default to the specified physical size as the size of the real-world image, without calculating the image size based on depth features. This can speed up the recognition process. | +| | ImagePhysicalSize | Sets the physical size constraint for the calibrated image (in meters). When you change the width or height value, the other value will be automatically calculated based on the aspect ratio. | + +### Action Library + +Currently, you can add the following actions: + +| Action | Attribute | Description | +| --------------- | --------------------- | ------------------------------------------------------------ | +| Display | | Activates the child objects if all the features of the automation behavior editor component match the real-world entities; otherwise, disables the child objects. | +| | Display Children Node | When enabled, displays the child nodes under the Tracking node by default. | +| | Stop Tracking | Disables AR tracking for this node when the specified conditions are met. | +| | Reset When Loss | Determines whether the behavior of the child nodes should be reset when tracking is lost. | +| Align | | Specifies the alignment relationship between the position of the proxy node and the position of the real-world entity. | +| | Towards | Sets the orientation of the child object. If set to Local_Up, it uses the pose of the child object directly. If set to World_Up, the child object's Y-axis will always align with the world coordinate's up direction. | +| | Face to Camera | When enabled, the child object faces the direction of the AR Camera along the Z-axis. | +| | MatchTrackingUpdate | When the real-world data matched by this node is updated, the layout and alignment effects are also refreshed. | +| Surface Overlay | | Replaces the original visualization effect with the specified prefab when the conditions are met. | +| | Surface Offset | Replaces the original visualization effect with the specified prefab when the conditions are met. | +| | Replace Visualizer | Disables and replaces the tracking visualization effect after creation. | +| Adaptive Scale | | Scales the child object based on the boundaries of the matched AR object. | +| | Max Scale | Sets the maximum limit for scaling adjustment. | +| | Match Tracking Update | Determines whether the behavior of scaling should be continuously refreshed along with tracking. | +| Track Event | | Collection of events called during the matching of tracking features. | +| | on Track Success | Event called when tracking is successful. | +| | on Track Refresh | Event called when tracking information is refreshed. | +| | on Track Loss | Event called when tracking is lost. | +| | on Track Timeout | Event called when tracking times out. You can set the time for tracking check, and if no successful tracking data is matched within this time range, the tracking failure event will be triggered. | diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/create-plane-tracking-node.png b/versions/4.0/en/xr/architecture/ar-tracking-component/create-plane-tracking-node.png new file mode 100644 index 0000000000..2154341ddd Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/create-plane-tracking-node.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/image-tacking-comp.png b/versions/4.0/en/xr/architecture/ar-tracking-component/image-tacking-comp.png new file mode 100644 index 0000000000..e6bf216f3b Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/image-tacking-comp.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/image-tracking-display.png b/versions/4.0/en/xr/architecture/ar-tracking-component/image-tracking-display.png new file mode 100644 index 0000000000..1b5133cc58 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/image-tracking-display.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/image-tracking-effect.png b/versions/4.0/en/xr/architecture/ar-tracking-component/image-tracking-effect.png new file mode 100644 index 0000000000..9b2510de96 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/image-tracking-effect.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/image-tracking-node.png b/versions/4.0/en/xr/architecture/ar-tracking-component/image-tracking-node.png new file mode 100644 index 0000000000..f33477cbfc Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/image-tracking-node.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/meshing-comp.png b/versions/4.0/en/xr/architecture/ar-tracking-component/meshing-comp.png new file mode 100644 index 0000000000..d086875bcd Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/meshing-comp.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/meshing-effect.jpeg b/versions/4.0/en/xr/architecture/ar-tracking-component/meshing-effect.jpeg new file mode 100644 index 0000000000..75f830d5a7 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/meshing-effect.jpeg differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/meshing-node.png b/versions/4.0/en/xr/architecture/ar-tracking-component/meshing-node.png new file mode 100644 index 0000000000..cf2026b97b Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/meshing-node.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-actions.png b/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-actions.png new file mode 100644 index 0000000000..c9789676c9 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-actions.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-display.png b/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-display.png new file mode 100644 index 0000000000..232d4c0450 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-display.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-effect.png b/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-effect.png new file mode 100644 index 0000000000..486e9f378f Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-effect.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-factors.png b/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-factors.png new file mode 100644 index 0000000000..8e66685945 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/plane-tracking-factors.png differ diff --git a/versions/4.0/en/xr/architecture/ar-tracking-component/set-image-source.png b/versions/4.0/en/xr/architecture/ar-tracking-component/set-image-source.png new file mode 100644 index 0000000000..9fdb5dbc11 Binary files /dev/null and b/versions/4.0/en/xr/architecture/ar-tracking-component/set-image-source.png differ diff --git a/versions/4.0/en/xr/architecture/assets.md b/versions/4.0/en/xr/architecture/assets.md new file mode 100644 index 0000000000..25d0478f31 --- /dev/null +++ b/versions/4.0/en/xr/architecture/assets.md @@ -0,0 +1,43 @@ +# Built-in Resources and Prefabs + +After enabling the XR extension in the Cocos Creator Extension Manager, you can create XR-related objects just as the way you create a general node. + +Right-click in the Hierarchy and select **Create -> XR**. On the right menu, you will see all the available XR prefabs that you can create instances of in the scene. + +create_xr_node + +|Name|Description|Components| +| ---------------------------------------------- | --------------------------------------------------- | ----------------------------------- | +| XR Agent | A proxy node in the virtual scene that represents information related to the real-world main character and controls the lifecycle of the XR main character in the virtual world. | TrackingOrigin | +| XR HMD | An abstract node representing a head-mounted display device in the virtual world, based on the Camera object, used to synchronize input signals from the real-world head-mounted display and output the engine rendering result to the device. | Camera
AudioSource
HMDCtrl
PoseTracker
TargetEye | +| AR Camera | An abstract representation of the camera on mobile devices with AR capabilities, used to map the camera's AR functionality to the physical device. | Camera
PoseTracker
ARCameraMgr | +| Ray Interactor | A ray interactor used for long-distance interaction, including mapping of XR device controller I/O and ray interaction functionality. | PoseTracker
XRController
RayInteractor
Line | +| Direct Interactor | An interactor used for close-range direct interaction, which also includes mapping of XR device controller I/O and interaction functionality. | PoseTracker
XRController
DirectInteractor | +| Gaze Pointer Interactor | An interactor used for gaze-based interaction, follows head movement, and triggers interaction based on gaze duration. | UITransform
RenderRoot2D
XRGazeInteractor | +| ScreenTouchInteractor | An interactor for screen gesture interaction on handheld mobile devices, converts screen gestures into interaction behaviors with objects in the scene. | ScreenTouchInteractor | +| Locomotion Checker | A motion checker that acts as an arbiter for all virtual motion drivers to access the XR Agent, ensuring the maintenance of a unique motion state within a fixed time. | LocomotionChecker | +| Teleportable | An interaction object that can trigger teleportation interaction with the interactor, allowing the XR Agent to be teleported to a location related to this object. | Teleportable
InteractableEvents | +| Simple Interactable | A simple interactive object where users can customize and extend arbitrary interaction behaviors. | InteractableEvents | +| Grab Interactable | An interactable object that supports grabbing behavior with the interactor. | RigidBody
GrabInteractable
InteractableEvents | +| XR Simulator | Used for previewing XR content, provides web-based and wireless streaming options. | XRInteractiveSimulator | +| XR Video Player | An XR video player that supports playing videos in windowed, 180-degree, and 360-degree modes in space. | XRVideoPlayer
XRVideoController
XRVideoCaption | +| XRUI | 3D UI that can be rendered and interacted with in space. | RaycastChecker
RenderRoot2D
BoxCollider | +| Plane Tracking | Empowers the application with plane recognition capabilities, using the device's AR capabilities to identify plane features in the physical world during runtime, and visualizes and displays these plane data in the application. | ARPlaneTracking | +| Image Tracking | Empowers the application with image recognition capabilities, using the device's AR capabilities to recognize 2D image resources during runtime. | ARImageTracking | +| Meshing | Empowers the application with environment reconstruction capabilities, creating 3D meshes based on the real-world environment. | ARMeshing | + +## Built-in Resources + +After enabling the XR extension, the XR prefabs, materials, models, and other resources will be added to the built-in resource database (xr-plugin) and can be directly used by users. The specific location is shown in the following image. + +### Prefabs + +![prefabs](assets/prefabs.png) + +### Materials + +![material](assets/material.png) + +### Models + +![model](assets/model.png) diff --git a/versions/4.0/en/xr/architecture/assets/create_xr_node.png b/versions/4.0/en/xr/architecture/assets/create_xr_node.png new file mode 100644 index 0000000000..a68d841ba5 Binary files /dev/null and b/versions/4.0/en/xr/architecture/assets/create_xr_node.png differ diff --git a/versions/4.0/en/xr/architecture/assets/material.png b/versions/4.0/en/xr/architecture/assets/material.png new file mode 100644 index 0000000000..6fd008309b Binary files /dev/null and b/versions/4.0/en/xr/architecture/assets/material.png differ diff --git a/versions/4.0/en/xr/architecture/assets/model.png b/versions/4.0/en/xr/architecture/assets/model.png new file mode 100644 index 0000000000..4afe8a3cb7 Binary files /dev/null and b/versions/4.0/en/xr/architecture/assets/model.png differ diff --git a/versions/4.0/en/xr/architecture/assets/prefabs.png b/versions/4.0/en/xr/architecture/assets/prefabs.png new file mode 100644 index 0000000000..638925080f Binary files /dev/null and b/versions/4.0/en/xr/architecture/assets/prefabs.png differ diff --git a/versions/4.0/en/xr/architecture/component.md b/versions/4.0/en/xr/architecture/component.md new file mode 100644 index 0000000000..7172b66d4e --- /dev/null +++ b/versions/4.0/en/xr/architecture/component.md @@ -0,0 +1,510 @@ +# XR Components + +Cocos CreatorXR empowers entities through the combination of components, and different functional systems manage entities based on their different characteristics. Therefore, all XR-related features in Cocos Creator are driven by encapsulated XR components. + +The functional components in Cocos CreatorXR consist of five main parts: + +- [Device Mapping](#device-mapping-components) +- [Interaction Component](#interaction-components) +- [Interaction Constraint](#interaction-constraint-components) +- [Virtual Movement](#virtual-movement-components) +- [XR UI](#xr-ui) + +After enabling the xr-plugin extension, you can add XR-related functional components to objects in the scene by clicking the **Add Component** button in the Inspector. In the component list that appears, navigate to the XR category and select the desired XR component, and then select the corresponding component under that category. + +## Device Mapping Components + +This component is mainly used to synchronize input/output information between physical devices in the real world and agent nodes in the virtual world. It ensures consistency between user interactions on XR devices and feedback in the virtual world. + +The main components are as following: + +### TrackingOrigin + +![tracking_origin](component/tracking_origin.png) + +| Property | Description | +| -------------------- | ------------------------------------------------------------ | +| Offset Object | Specifies the object to be vertically offset. If the selected object has child objects, the offset will be applied to the selected object and all its child objects vertically. | +| Tracking Origin Mode | The mode of tracking offset. When "Unbond" or "Device" is selected, the YOffsetValue will appear below for manually inputting the value. When "Floor" is selected, the YOffsetValue is hidden. If "Floor" is selected and the device has boundary settings enabled, the current viewpoint height will be determined by the distance of the device from the floor (currently only supported by Quest 2). If "Device" is selected, the offset height will be the input value. | +| YOffset Value | The value of the device offset. Manually input the offset value in meters. The default value is 1.36144m. If it is a fixed value, the Y value of the Transform property of the selected OffsetObject will be set to the entered value. | + +### HMDCtrl + +HMD (Head Mounted Display) controller is responsible for handling the head-mounted display devices. It can be considered as the general category for XR glasses devices that have the capability of rendering stereoscopic images. This component defines the parameters related to the rendering output for XR glasses. + +hmd + +| Property | Description | +| -------------- | ------------------------------------------------------------ | +| Per Eye Camera | Enable per-eye rendering. When checked, the "Sync With Main Camera" option appears below. The two sub-nodes, LeftEye and RightEye, under XR HMD, transition from hidden to visible. | +| IPDOffset | Adjust the inter-pupillary distance (IPD). When selecting "Manual" from the drop-down list, the OffsetValue input field appears below. When PerEyeCamera is enabled, adjusting the Manual parameter will change the Transform property's X value for LeftEye and RightEye (with a change of ±IPDOffset/2). | + +### PoseTracker + +PoseTracker is a component for tracking the position and orientation of an object. + +![pose_tracker](component/pose_tracker.png) + +| Property | Description | +| --------------- | ------------------------------------------------------------ | +| Tracking Source | Select the tracking source device. | +| Tracking Type | Tracking mode. Select "POSITION_AND_ROTATION" to track both position and rotation of the device, "POSITION" to track only the position, or "ROTATION" to track only the rotation. | + +### TargetEye + +Specifies the target eye. + +![target_eye](component/target_eye.png) + +| Property | Description | +| ---------- | ------------------------------------------------------------ | +| Target Eye | Specifies the target eye for rendering. "Both" displays for both left and right eyes, "Left" displays for the left eye, and "Right" displays for the right eye. | + +### XRController + +Abstract component for controllers. + +![xr_controller](component/xr_controller.png) + +| Property | Description | +| -------------------- | ---------------------------------------- | +| InputDevice | Binds the input device to the controller. | +| SelectAction | Maps the "Select" action to a button on the controller. | +| ActivateAction | Maps the "Activate" action to a button on the controller. | +| UIPressAction | Maps the UI press action to a button on the controller. | +| AxisToPressThreshold | The threshold for triggering the action. | +| Model | Specifies the model object for the controller. | + +### ARCameraMgr + +Properties for the mobile handheld device camera in AR. + +| Property | Description | +| ------------------------ | ------------------------------------------------------------ | +| Auto Focus | Enable or disable the camera's auto-focus function. When disabled, fixed-focus mode is used. The availability of auto-focus depends on the device's camera. | +| Light Estimate(Experimental) | When enabled, it estimates the ambient light properties at runtime and adjusts the scene lighting in real-time to provide consistent lighting between virtual and real objects. | + +> **Note**: Starting from version 1.2.0, light estimation supports two modes: Basic and HDR + +To enable the Basic mode, set the `Envmap` property of the Skybox in the scene to LDR. + +To enable the HDR mode, set the `Envmap` property of the Skybox in the scene to HDR. + +> **Note**: Currently, the Basic mode is only supported on iOS platforms. + +## Interaction Components + +An interaction operation requires coordination between two objects: the interaction subject and the interactable object. Correspondingly, the interaction components can be divided into two categories: **Interactor** and **Interactable**. + +### RayInteractor + +RayInteractor is a component for ray-based interaction. + +ray_interactor + +| Property | Description | +| ------------------------ | ------------------------------------------------------------ | +| AttachTransform | The position used as the final position where the grabbed object will be placed. If empty, the position of the current Interactor is used. | +| ForceGrab | Enables long-range grabbing. When enabled, the grabbed object adheres to the Attach Transform, and when disabled, it grabs the position attached to the interaction point. | +| RayOriginTransform | Allows changing the starting position from which the ray is cast. If empty, the current Interactor's position is used by default. | +| LineType | Changes the ray detection and ray style. "StraightLine" is a straight line, "Projectile Line" is a projectile curve, and "BezierLine" is a Bezier curve. | +| MaxRayDistance | The maximum distance at which the ray interaction can be triggered. | +| ReferenceNode | Appears when LineType is set to "ProjectileLine" or "BezierLine". Defines the reference frame for the curve, including the ground plane and the up vector. If not set at startup, it will attempt to find the XR Agent, and if no reference is provided, it will default to the global up vector and origin. | +| Velocity | Appears when LineType is set to "ProjectileLine". Initial velocity. Increasing this value extends the curve further. | +| Acceleration | Appears when LineType is set to "ProjectileLine". Gravity acceleration. | +| Additional Ground Height | Appears when LineType is set to "ProjectileLine". Additional height below the ground plane. If the ray goes below the ground, it continues to be projected downward. Increasing this value lowers the height of the endpoint. | +| Additional Flight Time | Appears when LineType is set to "ProjectileLine". Additional flight time after landing. If the ray goes below the ground, it continues to be projected downward. Increasing this value extends the flight time of the projection. | +| End Point Distance | Appears when LineType is set to "BezierLine". Increasing this value moves the endpoint of the curve further from the starting point. | +| End Point Height | Appears when LineType is set to "BezierLine". Decreasing this value lowers the endpoint of the curve relative to the starting point. | +| Control Point Distance | Appears when LineType is set to "BezierLine". Increasing this value moves the peak of the curve further from the starting point. | +| Control Point Height | Appears when LineType is set to "BezierLine". Increasing this value makes the peak of the curve higher relative to the starting point. | +| Sample Frequency | Appears when LineType is set to "ProjectileLine" or "BezierLine". The number of sample points used to approximate the curve path. A higher number of samples provides better approximation but lower performance. | +| RaycastMask | Only interacts with objects of this layer type. | +| SelectActionTrigger | Selects the action trigger mechanism for the Select behavior. See the interaction functionality section for details. | + +> **Note**: The functionality of projectile curves and Bezier curves requires the extended version >= v1.1.0 and the Cocos Creator version >= 3.7.1. + +### DirectInteractor + +DirectInteractor is a component for direct interaction. + +![direct_interactor](component/direct_interactor.png) + +| Property | Description | +| ------------------- | ------------------------------------------------------------ | +| AttachTransform | The position of the AttachTransform is used as the final position where the grabbed object will end up. If it is empty, the current Interactor's position is used instead. | +| SelectActionTrigger | Select action trigger mechanism. See the interaction functionality for details. | + +### XRGazeInteractor + +GazeInteractor allows interaction with objects (UI) by gazing at them with the center point of the HMD when there are no controllers or other input devices available. After gazing for a certain period, the interaction behavior is triggered. + +xr_gaze_interactor + +| Property | Description | +| ----------------------- | ---------------------------- | +| Gaze Pointer Offset | Vertical offset of the gaze point (in meters). | +| Max Ray Distance | Maximum distance for ray projection (in meters). | +| Gaze Default Distance | Default distance of the gaze point from the UI (in meters). | +| Gaze Timer Duration | Duration (in seconds) for the gaze interaction to be triggered. | +| Gaze Reticle Outer Ring | Outer ring UI of the gaze point. | + +> **Note**: GazeInteractor requires an extension version of >= v1.1.0 and Cocos Creator version of >=3.7.1. + +### **ScreenTouchInteractor** + +ScreenTouchInteractor is a component for screen gesture interaction. + +screen_touch_interactor + +| Action | Property | Description | +| ------------- | ----------------- | ------------------------------------------------------------ | +| Select Action | | Configuration for the select action, which controls whether it is enabled. | +| | Gesture | Optional gesture type for user interaction with virtual objects. | +| | Double Tap Gap | Appears when Gesture is set to DoubleTap. Determines the maximum time gap between two taps for them to be considered a double tap. | +| | HoldTouchDuration | Appears when Gesture is set to HoldTouch. Determines the minimum duration of touch for it to be considered a hold. | +| Move Action | | Configuration for the move action, which requires Select Action to be enabled and controls whether it is enabled. | +| | Gesture | Gesture associated with the move action. | +| RotateAction | | Configuration for the rotate action, which requires Select Action to be enabled and controls whether it is enabled. | +| | Gesture | Gesture associated with the rotate action. | +| | Drag Degree | Appears when Gesture is set to 2FingersDrag. Determines the rate at which the object is dragged using two fingers. | +| | Twist Degree | Appears when Gesture is set to 2FingersRotate. Determines the rate at which the object is rotated using two fingers. | +| Scale Action | | Configuration for the scale action, which requires Select Action to be enabled and controls whether it is enabled. | +| | Gesture | Gesture associated with the scale action. | +| | Sensitivity | Sensitivity of the scaling action. | +| Place Action | | Configuration for the place action, which controls whether it is enabled. | +| | Gesture | Gesture associated with the place action. | +| | Calculation Mode | Method used to calculate the placement point when placing virtual objects. There are three modes:
- AR_HIT_DETECTION: Uses AR Hit Test to detect the placement point and places the object at that location.
- SPATIAL_DISTANCE: Places the object at a fixed distance in front of the screen interactor.
- COLLISION_DETECTION: Uses raycast collision detection to determine the placement point and places the object at that location. | +| | Distance | Appears when Calculation Mode is set to SPATIAL_DISTANCE. Sets the distance between the interaction point and the object placement. | +| | Avoid Occlusion | Appears when Calculation Mode is set to SPATIAL_DISTANCE. Controls whether the object placement is affected by occlusion within the specified distance. When enabled, the object is placed at a fixed position regardless of occlusion. When disabled, if there is occlusion within the specified distance, the object is placed at the occluded position. | +| | Located Prefab | Appears when Calculation Mode is set to SPATIAL_DISTANCE. Specifies the prefab used for locating the placement position of the virtual object. | +| | Placement Prefab | Reference to the prefab with the Placeable component attached. | + +**Note**: + +1. ScreenTouchInteractor requires an extension version of >=v1.1.0 and an editor version of >=3.7.1. The toggle functionality for each gesture action requires an extension version of >=v1.2.0. +2. Double-finger gesture input is not supported on the WebXR platform. + +### GrabInteractable + +The GrabInteractable component allows an object to be grabbed and interacted with. + +![grab_interactable](component/grab_interactable.png) + +| Property | Description | +| ------------------------- | ------------------------------------------------------------ | +| AttachTransform | The position of the AttachTransform is used as the final position where the grabbed object will end up. If it is empty, the current position of the node is used instead. | +| AttachEaseInTime | The duration in seconds for the object to be eased into the AttachTransform position. During this time, the grabbed object will have a trailing effect that becomes less prominent as the duration increases. | +| GrabTrigger | Specifies when the grab action should be triggered: On Select action trigger or On Activate action trigger. | +| RayReticle | The object referenced by this property will be displayed at the collision point when the interactor interacts with this interactable object. | +| HideController | When enabled, the XR Controller's model will be hidden when the object is grabbed. | +| ThrowOnDetach | When enabled, allows simulating a throwing action when the object is detached. | +| ThrowSimulationMode | Determines how the throwing velocity is calculated when ThrowOnDetach is enabled:
- InheritRigidbody: The velocity of the Rigidbody component is inherited when throwing the object.
- CurveComputation: Allows customizing the calculation of the throw velocity using the ThrowSmoothingDuration and ThrowSmoothingCurve properties. | +| ThrowSmoothingDuration | The duration in seconds used as the sampling interval for calculating the weighted average velocity of the object before it is thrown. This value is used as the initial velocity of the object when thrown. | +| ThrowSmoothingCurve | The curve used for velocity sampling. The weighted average velocity is calculated based on the drawn curve. | +| ThrowVelocityScale | The coefficient for the initial velocity. A higher value increases the instantaneous velocity of the throw, multiplying the inherited or calculated initial velocity by a factor. | +| ThrowAngularVelocityScale | The coefficient for the initial angular velocity. A higher value increases the instantaneous angular velocity of the throw, multiplying the inherited or calculated initial angular velocity by a factor. | + +### Teleportable + +The Teleportable component allows an object to be teleported. + +![teleportable](component/teleportable.png) + +| Property | Description | +| -------------------- | ------------------------------------------------------------ | +| TeleportableType | The type of teleportation point: Area or Anchor. When set to Area, the object is teleported to the interaction point where the ray intersects with the teleportation area. When set to Anchor, the object is teleported to a fixed position within the teleportation area, independent of the interaction point. | +| Teleport Anchor Node | This property appears when TeleportableType is set to Anchor. It is used to specify the teleportation destination. If it is empty, the object is teleported to the default central area of the teleportation area. If it references another object, the object is teleported to the position of the referenced object. | +| RayReticle | The object referenced by this property will be displayed at the collision point when the interactor interacts with this interactable object. | +| TeleportTrigger | Specifies the event that triggers the teleportation action: OnSelectExited for when the Select action ends (button release), or OnSelectEntered for when the Select action is triggered (button press). | +| Teleporter | Specifies the subject (usually an XR Agent) to be teleported. The subject must have the Teleporter component attached. | + +### Selectable + +The Selectable component allows an object to be selected and enables displacement, rotation, and scaling actions while selected. + +selectable + +| Property | Description | +| ---------------------- | -------------------------------------------- | +| Allowed Actions | The actions that are allowed when the object is selected. | +| Selection Events | Callbacks for selection actions triggered by specific events. | +| Selected Visualization | The visual effects that are activated when the object is selected. | + +> **Note**: The selection interaction requires an extension version of >= v1.1.0 and Cocos Creator version of >= 3.7.1. + +### Placeable + +The Placeable component allows an object to be placed in space, on an AR Plane, or an AR Mesh using a specified method. + +placeable + +| 属性 | 说明 | +| ------------------------ | -------------------------------------------- | +| Preview Placement Prefab | The placeholder object to be placed before the placement of the actual object. | +| Placement Offset | The offset from the placement position specified by the interactor. | +| Placement Events | Callbacks for placement actions triggered by specific events. | + +> **Note**: The placement interaction requires an extension version of >= v1.1.0 and Cocos Creator version of >= 3.7.1. + +### Events + +During the development process, users often encounter logic that relies on signals from XR components to trigger certain behaviors. To allow users to focus on logic development without worrying about the notification relationship between the components, Cocos CreatorXR encapsulates a set of event signals in some interactive components that are commonly used for event dependency. Users only need to bind the desired objects and methods to specific signals to trigger the desired behavior. + +#### Event Signals + +Event signals are divided into the following three types: + +- Hover:Triggered when the ray from the XR input device hovers over an object. +- Select:Triggered when the Select button mapped to the input device is pressed. +- Active:Triggered when the Active button mapped to the input device is pressed. + +The interaction behaviors are described as follows: + +| Interaction Behavior | Event Signal | Description | +| -------- | ---------------- | --------------------- | +| Hover | OnHoverEntered | Moment when the Hover behavior is executed | +| | OnHoverExited | Moment when the Hover behavior exits | +| | OnHoverCanceled | Moment when the Hover behavior is canceled | +| Select | OnSelectEntered | Moment when the Select behavior is executed | +| | OnSelectExited | Moment when the Select behavior exits | +| | OnSelectCanceled | Moment when the Select behavior is canceled | +| Active | OnActivated | Moment of activation | +| | OnDeactivated | Moment when activation is canceled | + +These event signals can be used to handle input states in specific scenarios and can be selected in the following components. + +#### Interactive Event Components + +Interactive Components + +The interactive components are as follows: + +- InteractorEvents: Interactor event component. +- InteractableEvents:Interactable object event component. + +#### InteractorEvents + +| Property | Description | +| ---------------- | ---------------------------------------- | +| AudioEvents | Interactable object event component. | +| HapticEvents | Enables controller vibration feedback for triggered events | +| InteractorEvents | Enables binding arbitrary callback functions | + +##### Audio Events + +Audio Events can trigger different types of events based on the event signals and play specific audio clips when triggered. + +![audio_events](component/audio_events.png) + +By enabling or disabling the event signals on the right side, you can select which event signals to use. Once enabled, you can choose different audio resources from the dropdown menu on the right side of the AudioClip property. + +![audio_clip](component/audio_clip.png) + +##### Haptic Events + +> **Note**: This event requires an extension version of >= v1.1.0 and the Cocos Creator version of >=3.7.1. + +haptic_event + +By selecting the radio button on the right side of the event signals, you can adjust the controller's vibration feedback. Vibration feedback can provide users with a more realistic tactile experience. + +![haptic_setting](component/haptic_setting.png) + +Haptic Intensity:The intensity of the vibration [0,1] + +Duration:The duration of the vibration. + +##### Interactor Events + +![interactor_events](component/interactor_events.png) + +Enter any integer value in the input box on the right side to add elements to the Interactor Events array. + +![interactor_event_setting](component/interactor_event_setting.png) + +After adding an event, you can configure the event callback: + +- Node:The node that receives the callback. +- Component:The component of the callback. +Method: The callback method. +- CustomEventData:Custom data for the event, which will be passed as an argument to the callback method. + +#### InteractableEvents + +![interactable_events](component/interactable_events.png) + +## Interaction Constraint Components + +For interactive objects that have the `Selectable` component attached, you can use interaction restriction components to limit their interaction effects. + +### MinMaxScaleConstrain + +min_max_scale_constrain + +| Property | Description | +| --------- | ------------------------ | +| Min Scale | The minimum scale ratio that can be scaled down. | +| Max Scale | The maximum scale ratio that can be scaled up. | + +### RotationAxisConstrain + +rotate_axis_constrain + +| Property | Description | +| --------------- | ------------------------------------------------------------ | +| Axis Flag | Restricts the interactive object to rotate only around the selected rotation axis. The screen interactor can only control dual-axis (x, y) rotation at most. | +| Use Local Space | Whether to rotate based on the local coordinate system. By default, rotation is based on the world coordinate system. | + +## Virtual Movement Components + +In most VR projects, users navigate through virtual scenes from a first-person perspective. This type of movement behavior generally does not rely on real-world movement feedback, as tracking of positional data is limited by the physical space. Therefore, we need a component that can drive movement behaviors by accepting input signals from controllers, similar to how it's done in traditional 3D games. This component is called the Locomotion Component. + +### LocomotionChecker + +Movement checker. + +![locomotion_checker](component/locomotion_checker.png) + +| Property | Description | +| -------- | ------------------------------------------------------------ | +| XR Agent | Specifies the XR Agent (or other objects) that needs to perform movement. When adding this component, it is automatically bound to the first node in the scene hierarchy that has a TrackingOrigin component attached, usually the XR Agent. Users can also manually specify the object that needs to perform locomotion operations. | + +### Teleporter + +Teleportation driver component. + +![teleporter](component/teleporter.png) + +| Property | Description | +| ------- | ------------------------------------------------------------ | +| Checker | When adding this component, it is automatically bound to the first node in the scene hierarchy that has a Locomotion Checker component attached. Users can also manually assign the Locomotion Checker component they want to use. | + +### SharpTurner + +Instant turning driver. + +![sharp_turn](component/sharp_turn.png) + +| Property | Description | +| ----------------- | ------------------------------------------------------------ | +| Checker | When adding this component, it is automatically bound to the first node in the scene hierarchy that has a Locomotion Checker component attached. Users can also manually assign the Locomotion Checker component they want to use. | +| InputDevice | Binds to the controller object that has the XRController component attached. | +| TurnAngle | The angle of rotation. | +| EnableTurnAround | When enabled, pressing the joystick button allows a 180° rotation (clicking the joystick button). | +| ActivationTimeout | The time to wait for continuous turns to be executed. | + +### ContinuousTurner + +Continuous turning driver. + +![ContinuousTurner](component/ContinuousTurner.png) + +| Property | Description | +| ------------ | ------------------------------------------------------------ | +| Checker | When adding this component, it is automatically bound to the first node in the scene hierarchy that has a Locomotion Checker component attached. Users can also manually assign the Locomotion Checker component they want to use. | +| InputDevice | Binds to the controller object that has the XRController component attached. | +| InputControl | Binds to the joystick that accepts input. | +| TurnSpeed | The angular speed | + +#### ContinuousMover + +Translation movement driver. + +![ContinuousMover](component/ContinuousMover.png) + +| Property | Description | +| ------------- | ------------------------------------------------------------ | +| Checker | When adding this component, it is automatically bound to the first node in the scene hierarchy that has a Locomotion Checker component attached. Users can also manually assign the Locomotion Checker component they want to use. | +| InputDevice | Binds to the controller object that has the XRController component attached. | +| InputControl | Binds to the joystick that accepts input. | +| MoveSpeed | The movement speed. | +| ForwardSource | Select an object to use its orientation as the forward direction for movement. | + +## XR UI + +The traditional way of generating UI controls is to draw them on a canvas, which doesn't have depth information (immutable position attributes). This results in the canvas being projected onto the screen, and interaction with the UI controls requires feedback from the screen. + +However, XR devices have dual lenses and lack interactive capabilities, which doesn't meet the requirements of XR projects. Therefore, we need to change the interaction method of UI controls to interact with them in 3D space using interactors. This involves detaching UI controls from the canvas, allowing them to exist independently in space with complete positional attributes and collision detection capabilities. + +XR UI is an extension of 2D UI and can be created by adding the XRUI component. For information on how to use 2D/UI components, refer to the 2D Objects Overview. + +### Creating XR UI + +To create XR UI, go to **Hierarchy -> Create -> XR -> XRUI**. + +create_xrui + +Compared to traditional UI controls, XR UI introduces additional components for collision detection and interaction. As shown in the image, the RaycastChecker and BoxCollider components enable XR input interaction with the UI. + +![xrui_comp](component/xrui_comp.png) + +> **Note**: For UI in 3D space, the root node requires the RenderRoot2D component for proper rendering. + +#### Converting Existing UI + +If you have already created UI and want to convert it from a 2D canvas to XR UI, you can follow these steps. + +##### Method 1 + +Right-click in the Hierarchy and create an empty node (name it UIRoot). Add the RenderRoot2D component to the node, which will automatically add the UITransform component. + +![add_renderroot](component/add_renderroot.png) + +Detach the existing 2D UI nodes from the Canvas and move them under the UIRoot node. Modify the position and layer properties of the buttons. Set the Layer property of the button and its child nodes to match the layer of the Camera (e.g., DEFAULT). + +![change_layer](component/change_layer.gif) + +Add a material to the button and its child nodes. In the Assets window, navigate to **xr-plugin -> xr -> res -> default_materials** and select `xr-3dui-sprite-material`. Drag it onto the `CustomMaterial` property of the `Sprite` component. + +![add_3d_sprite](component/add_3d_sprite.gif) + +Add the `RaycastChecker` component for ray interaction to the button. Click on the button node and, in the Inspector panel, click the "Add Component" button. Select **XR -> UI -> RaycastChecker**. + +![add_raycastchecker](component/add_raycastchecker.png) + +The `RaycastChecker` and `BoxCollider` components will appear in the Inspector panel. Ensure that the xy values of the BoxCollider's `size` property match the `Content Size` value of the node's `UITransform`. You can replace the `BoxCollider` with other 3D colliders as needed, as long as they fit the UI components. + +![change_collider_size](component/change_collider_size.png) + +Move the button node to the desired position in the scene and adjust the Rotation and Scale values to meet the design requirements. + +Once all the UI components have been added, delete the old Canvas node. + +With these steps, you have successfully converted your existing UI to XR UI. + +##### Method 2 + +Starting from version v1.1.0, there is a new feature to convert 2D UI to XR UI with a single click. + +Right-click on the traditional 2D UI in the scene. In the context menu, you will see the option "2DUI to XRUI". + +![convert_2d_to_3d](component/convert_2d_to_3d.png) + +After successful conversion, the UI node will automatically add the following three components, and you can adjust their spatial properties in the `cc.Node`. + +![auto_add_comp](component/auto_add_comp.png) + +> **Note**: Once UI is converted to XRUI, it cannot be converted back. + +### XR Keyboard + +To add an EditBox as XR UI and attach a keyboard to it, create a child node named "KeyboardRoot" (you can choose any name) under the EditBox node and adjust the position information of KeyboardRoot as needed (you can temporarily place the XR Keyboard under the node for adjustment). + +![xr_keyboard_root](component/xr_keyboard_root.png) + +Create an XR Keyboard object: In the Asset Browser, navigate to **xr-plugin -> xr -> res -> default_prefab** and select XR Keyboard. Drag it into the scene. + +![xr_keyboard_prefab](component/xr_keyboard_prefab.png) + +Add the `XRKeyboardInputField` component to the EditBox node and bind it to `SuspendTransform` and `XRKeyboard` by dragging the nodes into the corresponding fields. + +![add_xr_keyboard_input_field](component/add_xr_keyboard_input_field.png) + +![bind_suspend](component/bind_suspend.png) + +### Raycast Material + +When interacting with XR UI using raycasting, you need to assign the xr-default-line-material to the ray. You can find this material in **Assets -> xr-plugin -> xr -> res -> default_materials**. + +![xr_line_material](component/xr_line_material.png) diff --git a/versions/4.0/en/xr/architecture/component/ContinuousMover.png b/versions/4.0/en/xr/architecture/component/ContinuousMover.png new file mode 100644 index 0000000000..6964ee5b44 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/ContinuousMover.png differ diff --git a/versions/4.0/en/xr/architecture/component/ContinuousTurner.png b/versions/4.0/en/xr/architecture/component/ContinuousTurner.png new file mode 100644 index 0000000000..a983a2883a Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/ContinuousTurner.png differ diff --git a/versions/4.0/en/xr/architecture/component/add_3d_sprite.gif b/versions/4.0/en/xr/architecture/component/add_3d_sprite.gif new file mode 100644 index 0000000000..46e86f9bf1 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/add_3d_sprite.gif differ diff --git a/versions/4.0/en/xr/architecture/component/add_raycastchecker.png b/versions/4.0/en/xr/architecture/component/add_raycastchecker.png new file mode 100644 index 0000000000..0ff2a0d2bb Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/add_raycastchecker.png differ diff --git a/versions/4.0/en/xr/architecture/component/add_renderroot.png b/versions/4.0/en/xr/architecture/component/add_renderroot.png new file mode 100644 index 0000000000..82f73d3212 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/add_renderroot.png differ diff --git a/versions/4.0/en/xr/architecture/component/add_xr_keyboard_input_field.png b/versions/4.0/en/xr/architecture/component/add_xr_keyboard_input_field.png new file mode 100644 index 0000000000..4e841bf57f Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/add_xr_keyboard_input_field.png differ diff --git a/versions/4.0/en/xr/architecture/component/audio_clip.png b/versions/4.0/en/xr/architecture/component/audio_clip.png new file mode 100644 index 0000000000..68947fe852 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/audio_clip.png differ diff --git a/versions/4.0/en/xr/architecture/component/audio_events.png b/versions/4.0/en/xr/architecture/component/audio_events.png new file mode 100644 index 0000000000..55546db704 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/audio_events.png differ diff --git a/versions/4.0/en/xr/architecture/component/auto_add_comp.png b/versions/4.0/en/xr/architecture/component/auto_add_comp.png new file mode 100644 index 0000000000..0a4e309f59 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/auto_add_comp.png differ diff --git a/versions/4.0/en/xr/architecture/component/bind_suspend.png b/versions/4.0/en/xr/architecture/component/bind_suspend.png new file mode 100644 index 0000000000..786bdf490d Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/bind_suspend.png differ diff --git a/versions/4.0/en/xr/architecture/component/change_collider_size.png b/versions/4.0/en/xr/architecture/component/change_collider_size.png new file mode 100644 index 0000000000..59b5e1bd2f Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/change_collider_size.png differ diff --git a/versions/4.0/en/xr/architecture/component/change_layer.gif b/versions/4.0/en/xr/architecture/component/change_layer.gif new file mode 100644 index 0000000000..bd4ac2a4b0 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/change_layer.gif differ diff --git a/versions/4.0/en/xr/architecture/component/convert_2d_to_3d.png b/versions/4.0/en/xr/architecture/component/convert_2d_to_3d.png new file mode 100644 index 0000000000..60bbbdb5a7 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/convert_2d_to_3d.png differ diff --git a/versions/4.0/en/xr/architecture/component/create_xrui.png b/versions/4.0/en/xr/architecture/component/create_xrui.png new file mode 100644 index 0000000000..bf1747a4bc Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/create_xrui.png differ diff --git a/versions/4.0/en/xr/architecture/component/direct_interactor.png b/versions/4.0/en/xr/architecture/component/direct_interactor.png new file mode 100644 index 0000000000..d74974e79a Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/direct_interactor.png differ diff --git a/versions/4.0/en/xr/architecture/component/grab_interactable.png b/versions/4.0/en/xr/architecture/component/grab_interactable.png new file mode 100644 index 0000000000..573bc4c7a3 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/grab_interactable.png differ diff --git a/versions/4.0/en/xr/architecture/component/haptic_event.png b/versions/4.0/en/xr/architecture/component/haptic_event.png new file mode 100644 index 0000000000..f27c2cd1a2 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/haptic_event.png differ diff --git a/versions/4.0/en/xr/architecture/component/haptic_setting.png b/versions/4.0/en/xr/architecture/component/haptic_setting.png new file mode 100644 index 0000000000..536c4006ab Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/haptic_setting.png differ diff --git a/versions/4.0/en/xr/architecture/component/hmd.png b/versions/4.0/en/xr/architecture/component/hmd.png new file mode 100644 index 0000000000..0996aec66a Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/hmd.png differ diff --git a/versions/4.0/en/xr/architecture/component/interactable_events.png b/versions/4.0/en/xr/architecture/component/interactable_events.png new file mode 100644 index 0000000000..3c81ea449a Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/interactable_events.png differ diff --git a/versions/4.0/en/xr/architecture/component/interactor_event_setting.png b/versions/4.0/en/xr/architecture/component/interactor_event_setting.png new file mode 100644 index 0000000000..20cb255be4 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/interactor_event_setting.png differ diff --git a/versions/4.0/en/xr/architecture/component/interactor_events.png b/versions/4.0/en/xr/architecture/component/interactor_events.png new file mode 100644 index 0000000000..24cdd0edb0 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/interactor_events.png differ diff --git a/versions/4.0/en/xr/architecture/component/locomotion_checker.png b/versions/4.0/en/xr/architecture/component/locomotion_checker.png new file mode 100644 index 0000000000..faaba24c6c Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/locomotion_checker.png differ diff --git a/versions/4.0/en/xr/architecture/component/min_max_scale_constrain.png b/versions/4.0/en/xr/architecture/component/min_max_scale_constrain.png new file mode 100644 index 0000000000..a106093bdf Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/min_max_scale_constrain.png differ diff --git a/versions/4.0/en/xr/architecture/component/placeable.png b/versions/4.0/en/xr/architecture/component/placeable.png new file mode 100644 index 0000000000..5498a7ddfd Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/placeable.png differ diff --git a/versions/4.0/en/xr/architecture/component/pose_tracker.png b/versions/4.0/en/xr/architecture/component/pose_tracker.png new file mode 100644 index 0000000000..9232ac6134 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/pose_tracker.png differ diff --git a/versions/4.0/en/xr/architecture/component/ray_interactor.png b/versions/4.0/en/xr/architecture/component/ray_interactor.png new file mode 100644 index 0000000000..a665a52c96 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/ray_interactor.png differ diff --git a/versions/4.0/en/xr/architecture/component/rotate_axis_constrain.png b/versions/4.0/en/xr/architecture/component/rotate_axis_constrain.png new file mode 100644 index 0000000000..116b4c949e Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/rotate_axis_constrain.png differ diff --git a/versions/4.0/en/xr/architecture/component/screen_touch_interactor.png b/versions/4.0/en/xr/architecture/component/screen_touch_interactor.png new file mode 100644 index 0000000000..d9e6187347 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/screen_touch_interactor.png differ diff --git a/versions/4.0/en/xr/architecture/component/selectable.png b/versions/4.0/en/xr/architecture/component/selectable.png new file mode 100644 index 0000000000..e5ea50993a Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/selectable.png differ diff --git a/versions/4.0/en/xr/architecture/component/sharp_turn.png b/versions/4.0/en/xr/architecture/component/sharp_turn.png new file mode 100644 index 0000000000..6f2bfe367f Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/sharp_turn.png differ diff --git a/versions/4.0/en/xr/architecture/component/target_eye.png b/versions/4.0/en/xr/architecture/component/target_eye.png new file mode 100644 index 0000000000..11ffaa9f14 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/target_eye.png differ diff --git a/versions/4.0/en/xr/architecture/component/teleportable.png b/versions/4.0/en/xr/architecture/component/teleportable.png new file mode 100644 index 0000000000..9e78c20abb Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/teleportable.png differ diff --git a/versions/4.0/en/xr/architecture/component/teleporter.png b/versions/4.0/en/xr/architecture/component/teleporter.png new file mode 100644 index 0000000000..7adb384bbe Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/teleporter.png differ diff --git a/versions/4.0/en/xr/architecture/component/tracking_origin.png b/versions/4.0/en/xr/architecture/component/tracking_origin.png new file mode 100644 index 0000000000..8301705d09 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/tracking_origin.png differ diff --git a/versions/4.0/en/xr/architecture/component/xr_controller.png b/versions/4.0/en/xr/architecture/component/xr_controller.png new file mode 100644 index 0000000000..63d67b6406 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/xr_controller.png differ diff --git a/versions/4.0/en/xr/architecture/component/xr_gaze_interactor.png b/versions/4.0/en/xr/architecture/component/xr_gaze_interactor.png new file mode 100644 index 0000000000..a1442d3854 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/xr_gaze_interactor.png differ diff --git a/versions/4.0/en/xr/architecture/component/xr_keyboard_prefab.png b/versions/4.0/en/xr/architecture/component/xr_keyboard_prefab.png new file mode 100644 index 0000000000..6c9009cb39 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/xr_keyboard_prefab.png differ diff --git a/versions/4.0/en/xr/architecture/component/xr_keyboard_root.png b/versions/4.0/en/xr/architecture/component/xr_keyboard_root.png new file mode 100644 index 0000000000..77320867de Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/xr_keyboard_root.png differ diff --git a/versions/4.0/en/xr/architecture/component/xr_line_material.png b/versions/4.0/en/xr/architecture/component/xr_line_material.png new file mode 100644 index 0000000000..81747ec57e Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/xr_line_material.png differ diff --git a/versions/4.0/en/xr/architecture/component/xrui_comp.png b/versions/4.0/en/xr/architecture/component/xrui_comp.png new file mode 100644 index 0000000000..36a5e1bdc2 Binary files /dev/null and b/versions/4.0/en/xr/architecture/component/xrui_comp.png differ diff --git a/versions/4.0/en/xr/architecture/index.md b/versions/4.0/en/xr/architecture/index.md new file mode 100644 index 0000000000..1f9610e17b --- /dev/null +++ b/versions/4.0/en/xr/architecture/index.md @@ -0,0 +1,5 @@ +# Architecture + +The extension architecture refers to the system structure of the extension. As a user of Cocos CreatorXR, you can quickly and comprehensively build XR applications by understanding the components of the extension architecture, their responsibilities, and how they are combined and interconnected. + +![index/xr-framework.png](index/xr-framework.png) diff --git a/versions/4.0/en/xr/architecture/index/xr-framework.png b/versions/4.0/en/xr/architecture/index/xr-framework.png new file mode 100644 index 0000000000..32a771e0b8 Binary files /dev/null and b/versions/4.0/en/xr/architecture/index/xr-framework.png differ diff --git a/versions/4.0/en/xr/architecture/preview.md b/versions/4.0/en/xr/architecture/preview.md new file mode 100644 index 0000000000..130bf57bf3 --- /dev/null +++ b/versions/4.0/en/xr/architecture/preview.md @@ -0,0 +1,50 @@ +# Preview + +To facilitate developers in real-time debugging and quickly verifying traditional functional logic during project development, Cocos CreatorXR offers a preview feature for XR projects based on Cocos Creator's **Preview in Browser** functionality. + +## How to Use + +In the xr-plugin repository, find XR Simulator and drag it into the scene. + +xr_web_simulator_prefab + +Select the **Preview in Browser** option in the editor's preview settings and click to play. + +![preview_in_browser](preview/preview_in_browser.png) + +Once running, you can simulate the preview in the browser. + +![effect_of_preview_in_browser](preview/effect_of_preview_in_browser.png) + +Use the WASD keys to control the movement of the character as a whole (HMD + controllers) in the forward, left, backward, and right directions. Use QE to control the overall up and down. + +The numeric keys 1,2,3 on the Latin part of the keyboard have the following functions: + +- **Key 1**: Switch the control target of the mouse and keyboard to the XR Agent (the character itself). In this mode, the movements of forward, backward, left, right, up, and down will affect the character as a whole. Mouse movement controls the rotation of the HMD (Camera), and the raycasting originates from the center of the HMD. Press the spacebar to trigger a click, and hold the spacebar while dragging the mouse to trigger a drag. +- **Key 2 and 3**: Switch the control target of the mouse and keyboard to the left/right controller, respectively. In this mode, the movements of forward, backward, left, right, up, and down will affect the left/right controller individually. The raycasting originates from the position of the controller. Press the spacebar to trigger a click, and hold the spacebar while dragging the mouse to trigger a drag. + +Hold down the B key to reset the controller position. + +When controlling the movement of the controllers, the forward vector always aligns with the forward direction of the XR Agent. + +## XR Device Wireless Streaming Debugging + +The preview component in version 1.1.0 introduces wireless streaming mode. Content validation is a time-consuming process during project development. Due to the independence of XR devices and the closed nature of streaming tools, debugging and validating XR projects in the editor is more challenging compared to traditional mobile/PC projects. To address this, Cocos CreatorXR v1.1.0 introduces wireless streaming debugging. Developers can directly preview XR projects in a web browser and synchronize all signals from the XR devices, rendering real-time visuals and providing feedback on various controller signal-triggering logic. It allows for a complete and quick experience of all XR project content without the need to package the application onto the device. + +xr_preview_by_wireless + +Select REMOTE mode for XR Preview Type, and Wifi mode for XR Connect Type, and ensure that the computer and the device are on the same WIFI network. + +Enter the XR device's IP address in the XR Device `IP` property. + +Check Remote Preview in the Build panel and package the project for the XR device. + +remote_preview_type + +Choose **Preview in Browser** as the project preview method. + +![preview_in_browser](preview/preview_in_browser.png) + +Click Run in the editor for browser preview and run the packaged APK on the device simultaneously. + +> **Note**: The wireless streaming debugging feature requires extension version >= v1.1.0 and the Cocos Creator version >= 3.7.1. diff --git a/versions/4.0/en/xr/architecture/preview/effect_of_preview_in_browser.png b/versions/4.0/en/xr/architecture/preview/effect_of_preview_in_browser.png new file mode 100644 index 0000000000..ae2cb75a10 Binary files /dev/null and b/versions/4.0/en/xr/architecture/preview/effect_of_preview_in_browser.png differ diff --git a/versions/4.0/en/xr/architecture/preview/preview_in_browser.png b/versions/4.0/en/xr/architecture/preview/preview_in_browser.png new file mode 100644 index 0000000000..f50e8d151c Binary files /dev/null and b/versions/4.0/en/xr/architecture/preview/preview_in_browser.png differ diff --git a/versions/4.0/en/xr/architecture/preview/remote_preview_type.png b/versions/4.0/en/xr/architecture/preview/remote_preview_type.png new file mode 100644 index 0000000000..e02f813ae6 Binary files /dev/null and b/versions/4.0/en/xr/architecture/preview/remote_preview_type.png differ diff --git a/versions/4.0/en/xr/architecture/preview/xr_preview_by_wireless.png b/versions/4.0/en/xr/architecture/preview/xr_preview_by_wireless.png new file mode 100644 index 0000000000..59bb85f4cd Binary files /dev/null and b/versions/4.0/en/xr/architecture/preview/xr_preview_by_wireless.png differ diff --git a/versions/4.0/en/xr/architecture/preview/xr_web_simulator_prefab.png b/versions/4.0/en/xr/architecture/preview/xr_web_simulator_prefab.png new file mode 100644 index 0000000000..c0908e254f Binary files /dev/null and b/versions/4.0/en/xr/architecture/preview/xr_web_simulator_prefab.png differ diff --git a/versions/4.0/en/xr/architecture/xr-composition-layer.md b/versions/4.0/en/xr/architecture/xr-composition-layer.md new file mode 100644 index 0000000000..0f56f0a022 --- /dev/null +++ b/versions/4.0/en/xr/architecture/xr-composition-layer.md @@ -0,0 +1,60 @@ +# XR Composition Layer + +In XR application development, the Composition Layer is a commonly used technique, often applied in mixed-reality scenarios to blend virtual reality scenes with real-world scenes. The Composition Layer renders different layers into separate textures based on the layer depth set by the user. These layers are then composited together to form a complete XR scene. By adjusting the transparency and depth of the layers, seamless integration between virtual and real-world objects can be achieved. The Composition Layer technology enables high-quality XR rendering effects, providing significant help and support for XR application development and user experience. + +## Composition Layer Features + +| Property | Description | +| -------------- | ------------------------------------------------------------ | +| Layer Setting | Composition layer effect settings. | +| --Type | The type of the composition layer: Overlay: Renders the texture in front of the Eye Buffer. Underlay: Renders the texture behind the Eye Buffer. | +| --Shape | Provides two types of composition layer shapes:
Quad: A flat texture with four vertices, usually used to display text or information in the scene.
Cylinder: A cylindrical texture with curved radius, typically used for displaying curved UI interfaces. | +| --Redius | Appears when selecting Cylinder, sets the curvature radius. | +| --CentralAngle | Appears when selecting Cylinder, sets the central angle size. | +| --Depth | Defines the order of the composition layer in the scene. The smaller the value, the closer it is to the Eye Buffer. | +| TextureSetting | Material effect settings. | +| --Camera | Binds the camera that captures dynamic textures for the composition layer. | +| --Width | Sets the width of the camera's Render Texture. | +| --Height | Sets the height of the camera's Render Texture. | + +> **Note**: +> 1. The Composition Layer feature is integrated with the core API extensions of OpenXR and is applicable to all devices that comply with the OpenXR standard. +> 2. The camera must be placed inside the inscribed sphere of the cylinder. If the camera is too close to the surface of the inscribed sphere, the composition layer will display abnormally. + +## Using the Composition Layer + +Currently, the Composition Layer feature can render dynamic textures and is mainly used to render the visuals captured by a camera. + +The following example demonstrates the creation of a mirror object using the **Overlay** type, which reflects the actions of an XR character. + +### Setup Steps + +First, create a complete XR proxy node in the scene for device tracking. Bind a simple HMD/controller model to it. + +![xr-composition-layer/create-xr-actor.png](xr-composition-layer/create-xr-actor.png) + +Add any node to the scene, using an empty node as an example. Add the Composition Layer component to it by clicking Add Component in the Inspector panel and finding **XR > Extra > XRCompositionLayer**. + + + +add-composition-comp + +Create a Camera node in the scene and position it as desired. + + + +![xr-composition-layer/change-camera-pos.png](xr-composition-layer/change-camera-pos.png) + +Set the Camera's Clear Flags to SOLID_COLOR and set the Clear Color's alpha value to 0. + + + +Attach this Camera node to the `cc.XRCompositionLayer` component's Camera property of the node with the added `cc.XRCompositionLayer` component. Adjust the rendering resolution, scale size, and position of the camera. Try to ensure that the aspect ratio of the scale is the same as the aspect ratio of the rendering resolution; otherwise, the image may appear stretched. Set the Type in Layer Setting to Overlay and the Shape to Quad. + + + +The result after packaging: + +![overlay-effect](xr-composition-layer/overlay-effect.gif) + +> **Note**: Using the Composition Layer feature requires extension version >=1.2.0 and the Cocos Creator version >= 3.7.3. diff --git a/versions/4.0/en/xr/architecture/xr-composition-layer/add-camera.png b/versions/4.0/en/xr/architecture/xr-composition-layer/add-camera.png new file mode 100644 index 0000000000..eae63c2a43 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-composition-layer/add-camera.png differ diff --git a/versions/4.0/en/xr/architecture/xr-composition-layer/add-composition-comp.png b/versions/4.0/en/xr/architecture/xr-composition-layer/add-composition-comp.png new file mode 100644 index 0000000000..3716369ba1 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-composition-layer/add-composition-comp.png differ diff --git a/versions/4.0/en/xr/architecture/xr-composition-layer/add-empty-node.png b/versions/4.0/en/xr/architecture/xr-composition-layer/add-empty-node.png new file mode 100644 index 0000000000..00196200de Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-composition-layer/add-empty-node.png differ diff --git a/versions/4.0/en/xr/architecture/xr-composition-layer/change-camera-pos.png b/versions/4.0/en/xr/architecture/xr-composition-layer/change-camera-pos.png new file mode 100644 index 0000000000..4ccc47d0a5 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-composition-layer/change-camera-pos.png differ diff --git a/versions/4.0/en/xr/architecture/xr-composition-layer/config-compositionlayer.png b/versions/4.0/en/xr/architecture/xr-composition-layer/config-compositionlayer.png new file mode 100644 index 0000000000..742c19d158 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-composition-layer/config-compositionlayer.png differ diff --git a/versions/4.0/en/xr/architecture/xr-composition-layer/create-xr-actor.png b/versions/4.0/en/xr/architecture/xr-composition-layer/create-xr-actor.png new file mode 100644 index 0000000000..1afb3f9cde Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-composition-layer/create-xr-actor.png differ diff --git a/versions/4.0/en/xr/architecture/xr-composition-layer/overlay-effect.gif b/versions/4.0/en/xr/architecture/xr-composition-layer/overlay-effect.gif new file mode 100644 index 0000000000..f9a9269206 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-composition-layer/overlay-effect.gif differ diff --git a/versions/4.0/en/xr/architecture/xr-composition-layer/set-clear-flags.png b/versions/4.0/en/xr/architecture/xr-composition-layer/set-clear-flags.png new file mode 100644 index 0000000000..ee46b380bd Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-composition-layer/set-clear-flags.png differ diff --git a/versions/4.0/en/xr/architecture/xr-pass-through.md b/versions/4.0/en/xr/architecture/xr-pass-through.md new file mode 100644 index 0000000000..ced0bdab26 --- /dev/null +++ b/versions/4.0/en/xr/architecture/xr-pass-through.md @@ -0,0 +1,33 @@ +# Passthrough + +The implementation of **Passthrough** involves capturing real-world scenes through the cameras of XR devices and transmitting them to the display, allowing users to see the real-world environment. In a virtual reality setting, users cannot perceive the real-world environment, which can lead to collision or safety issues. By using Passthrough technology, users can perceive the real-world environment, enabling safer interactions in the virtual reality environment and enhancing the sense of immersion. + +## Passthrough Feature + +XR extensions provide dedicated layers for rendering passthrough images while using composition layers to control the blending and display relationship between passthrough images and virtual scenes. + +| Property | Description | +| --------- | ----------------------------------------------------- | +| Placement | Specifies the composition mode for the Passthrough Layer. | +| Depth | Sets the depth to specify the ordering of the Passthrough within the Composition Layer. | +| Opacity | Sets the opacity of the Passthrough image. | + +> **Note**: The Passthrough functionality is based on non-core extensions of OpenXR. The current version only supports Meta Quest series devices. + +## Enabling Passthrough + +Adjust the Clear Flags of the Camera component of the **XR HMD** node to SOLID_COLOR, and set the opacity of Clear Color to 0. + + + +Add the passthrough component to the XR HMD node. Find **XR > Extra > XRPassThroughLayer** and click Add. + + + +To display the passthrough video image below all 3D content, set the `Placement` property to `Underlay`. + +After packaging, the passthrough effect will be visible. + +![xr-pass-through/pass-through-effect.png](xr-pass-through/pass-through-effect.png) + +> **Note**: To use the passthrough functionality, you need extension version >=1.2.0 and the Cocos Creator version >= 3.7.3. diff --git a/versions/4.0/en/xr/architecture/xr-pass-through/add-pass-throught-layer.png b/versions/4.0/en/xr/architecture/xr-pass-through/add-pass-throught-layer.png new file mode 100644 index 0000000000..4f8228143b Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-pass-through/add-pass-throught-layer.png differ diff --git a/versions/4.0/en/xr/architecture/xr-pass-through/pass-through-effect.png b/versions/4.0/en/xr/architecture/xr-pass-through/pass-through-effect.png new file mode 100644 index 0000000000..4640eedd17 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-pass-through/pass-through-effect.png differ diff --git a/versions/4.0/en/xr/architecture/xr-pass-through/set-hmd-camera.png b/versions/4.0/en/xr/architecture/xr-pass-through/set-hmd-camera.png new file mode 100644 index 0000000000..0711878763 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-pass-through/set-hmd-camera.png differ diff --git a/versions/4.0/en/xr/architecture/xr-spatial-audio.md b/versions/4.0/en/xr/architecture/xr-spatial-audio.md new file mode 100644 index 0000000000..8c08a2278f --- /dev/null +++ b/versions/4.0/en/xr/architecture/xr-spatial-audio.md @@ -0,0 +1,28 @@ +# XR Spatial Audio + +XR Spatial Audio is an important technology in the virtual reality field that can simulate audio environments from the real world, providing users with a more immersive auditory experience in virtual reality. **Sound-Tracking** technology based on head-mounted displays can simulate audio environments from the real world by tracking the user's head movements. When users move their heads in a virtual reality environment, the system can adjust the position and direction of the audio based on the user's head movements and position, creating a simulation of real-world audio environments. + +## XR Spatial Audio Features + +| Property | Description | +| ---------------------- | -------------------------------- | +| Clip | References the audio file to be played. | +| Loop | Determines if the audio should loop. | +| Play On Wake | Determines if the audio should play automatically when the project starts. | +| Volume | Sets the volume level of the audio. | +| Distance Rolloff Model | Determines the distance-dependent volume attenuation model. | + +## Using XR Spatial Audio + +Select the node object to which you want to add audio, and add the component in the property manager **XR -> Extra -> XRSpatialAudioSource**. + + + +## Acknowledgments + +The Spatial Audio feature references the GoogleVR library. Please refer to the `LICENSE_googlevr.txt` file in the licenses folder for the licensing details. + +For Google API service terms that cover this SDK, please refer to + [https://developers.google.com/terms/](https://developers.google.com/terms/)。 + +> **Note**: The Spatial Audio feature requires an extension version >= 1.2.0 and the Cocos Creator version >= 3.7.3. diff --git a/versions/4.0/en/xr/architecture/xr-spatial-audio/add-spatial-audio.png b/versions/4.0/en/xr/architecture/xr-spatial-audio/add-spatial-audio.png new file mode 100644 index 0000000000..7eb0377314 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-spatial-audio/add-spatial-audio.png differ diff --git a/versions/4.0/en/xr/architecture/xr-video-player.md b/versions/4.0/en/xr/architecture/xr-video-player.md new file mode 100644 index 0000000000..63fc0c0653 --- /dev/null +++ b/versions/4.0/en/xr/architecture/xr-video-player.md @@ -0,0 +1,65 @@ +# XR Video Player + +XR headsets offer a variety of ways to display videos compared to traditional monitors. With their multi-axis positional tracking and dual-screen rendering capabilities, XR devices can fulfill the need for browsing panoramic videos or dynamic materials in a 3D environment. Cocos CreatorXR v1.1.0 provides a versatile XR Video Player that optimizes video rendering pipelines for XR devices and supports switching display windows, as well as various styles of videos such as 180-degree and 360-degree. Additionally, the player offers interactive features to assist with playback control. By simply adding or replacing video resources, you can easily develop video playback functionality, simplifying the creative process and lowering the barrier to entry. + +To create a video player, right-click in the Hierarchy and select **Create -> XR -> XR Video Player**. + +![xr_video_player_node](xr-video-player/xr_video_player_node.png) + +The core components contained within the node are as follows: + +## XR Video Player + +### cc.XRVideoPlayer + +Used to adjust various properties of the video. + +| Property | Description | +| ------------------ | ------------------------------------------------------------ | +| Source Type | The source of the video: REMOTE for remote video URL, LOCAL for local video file path. | +| Remote URL | Appears when Source Type is REMOTE, for the URL of the remote video. | +| Clip | Appears when Source Type is LOCAL, for the local video clip. | +| Play On Awake | Determines whether the video automatically starts playing after loading. | +| Playback Rate | The playback speed of the video, ranging from [0.0, 2.5]. | +| Volume | The volume of the video, ranging from [0.0, 1.0]. | +| Mute | Whether the video is muted. When muted, the volume is set to 0; when unmuted, it restores the original volume. | +| Loop | Whether the video should replay when it reaches the end. | +| Keep Aspect Ratio | Whether to maintain the original aspect ratio of the video (useful for viewing portrait videos). | +| Shape | The style of the video. | +| Content | Associates a VideoContent with a MeshRenderer component to render the video texture. | +| Video Player Event | A callback function triggered under specific conditions during video playback, such as during play, pause, stop, or completion. | + +## XR Video Controller + +### cc.XRVideoController + +Used to associate UI elements with video functionality. + +| Property | Description | +| --------------------- | ------------------------------------------- | +| Player | The associated VideoPlayer used to control its playback functionality. | +| HMD Control | The controller object node bound to the HMD (head-mounted display). | +| Left Hand Controller | The controller object node bound to the left hand controller. | +| Right Hand Controller | The controller object node bound to the right hand controller. | +| Play Pause | UI element for play/pause control. | +| Progress Bar | UI element for the progress bar. | +| Fast Forward | UI element for the fast forward button. | +| Rewind | UI element for the rewind button. | +| Video Shape UI | UI element for video style selection. | +| Player Back Rate Bar | UI element for the playback speed. | +| Volume UI | UI element for volume adjustment. | + +## Video Caption + +### cc.XRVideoCaption + +Used to parse subtitle files. Currently, only .srt subtitle files are supported. + +| Property | Description | +| ------------------- | ------------------------------------------------------------ | +| Caption Source Type | The source of the captions: REMOTE for a file specified by a URL to parse the captions, LOCAL for a local subtitle file. | +| Remote URL | Appears when Caption Source Type is REMOTE, for the URL of the remote subtitle file. | +| Caption File | Appears when Caption Source Type is LOCAL, for the local subtitle file. | +| Video Player | Associates a specified VideoPlayer to synchronize the subtitles with the video based on time. | + +> **Note**: Vulkan does not currently support the video player functionality. diff --git a/versions/4.0/en/xr/architecture/xr-video-player/xr_video_player_node.png b/versions/4.0/en/xr/architecture/xr-video-player/xr_video_player_node.png new file mode 100644 index 0000000000..764814dbf0 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-video-player/xr_video_player_node.png differ diff --git a/versions/4.0/en/xr/architecture/xr-webview.md b/versions/4.0/en/xr/architecture/xr-webview.md new file mode 100644 index 0000000000..39dea27625 --- /dev/null +++ b/versions/4.0/en/xr/architecture/xr-webview.md @@ -0,0 +1,28 @@ +# XR Web Browser + +In XR, a web browser allows users to access and browse the web within a virtual reality environment. Users can interact with web interfaces using device controllers, such as selecting links, scrolling through pages, and more, enhancing the immersion and experience for users. + +## XR Web Browser Features + +| Property | Description | +| ------- | ---------------------------------------- | +| Content | Specifies the `MeshRenderer` object for rendering web content. | +| Url | The URL of the web page. | + +## Using the XR Web Browser + +Right-click in the Hierarchy panel and select **Create -> XR -> XR Webview**. + +By default, it creates a node with the `cc.MeshRenderer` component as a child node of the Webview Content. + +![xr-webview/create-webview.png](xr-webview/create-webview.png) + +Build and publish the application to see the web page content. + +![xr-webview/web-effect.png](xr-webview/web-effect.png) + +Note: + +- The web browser functionality requires the **XR extension version >= 1.2.0** and the **Cocos Creator version >= 3.7.3**. + +- Vulkan does not currently support the video player feature. diff --git a/versions/4.0/en/xr/architecture/xr-webview/create-webview.png b/versions/4.0/en/xr/architecture/xr-webview/create-webview.png new file mode 100644 index 0000000000..437e9deecb Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-webview/create-webview.png differ diff --git a/versions/4.0/en/xr/architecture/xr-webview/web-effect.png b/versions/4.0/en/xr/architecture/xr-webview/web-effect.png new file mode 100644 index 0000000000..61eca84fa4 Binary files /dev/null and b/versions/4.0/en/xr/architecture/xr-webview/web-effect.png differ diff --git a/versions/4.0/en/xr/index.md b/versions/4.0/en/xr/index.md new file mode 100644 index 0000000000..7bda3279ea --- /dev/null +++ b/versions/4.0/en/xr/index.md @@ -0,0 +1,5 @@ +# Cocos CreatorXR + +Cocos CreatorXR is an XR content creation tool built on top of Cocos Creator and Cocos Engine. It leverages the OpenXR standard protocol to bridge the differences between various XR devices, allowing developers to create and publish content for different XR devices without the need to adapt to specific device features. + +The middleware layer provides XR content creation support through a set of XR middleware with different functionalities, allowing users to customize and extend component content. The upper layer extends the Cocos Creator panel to provide various forms of XR function menus and component styles, offering users a more convenient content creation interface. \ No newline at end of file diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy.md b/versions/4.0/en/xr/project-deploy/ar-proj-deploy.md new file mode 100644 index 0000000000..eb6f11a362 --- /dev/null +++ b/versions/4.0/en/xr/project-deploy/ar-proj-deploy.md @@ -0,0 +1,79 @@ +# AR Project Creation + +Follow the steps below to set up AR-related features for your project. + +Three methods are provided, and you can choose any one of them to set up the extension or open the built-in AR project directly. + +## 1. Apply xr-plugin to the Project + +Search for xr-plugin in the Cocos Store, obtain the extension, and install it. For detailed installation instructions, please refer to the [Extension Install Documentation](../../../editor/extension/install)。 + +After installation, add the extension to the corresponding project. + +This method is suitable for migrating XR mode to existing 3D projects. + +![search-in-store](ar-proj-deploy/search-in-store.png) + +![install-for-proj](ar-proj-deploy/install-for-proj.png) + +## 2. Two Ways to Create from AR Project + +### Create from AR Template + +Create a new project in the Cocos Dashboard, select editor version v3.7.1 or higher, and choose **Templates/Empty (AR Mobile)** as the template for creation. + +![create-by-template](ar-proj-deploy/create-by-template.png) + +Open the project and go to the scene. The scene already includes the initial AR Camera configuration, allowing you to directly develop functionalities. + +![open-ar-template](ar-proj-deploy/open-ar-template.png) + +### Create from AR Example + +Create a new project in the Cocos Dashboard, select editor version v3.7.1 or higher, and choose **Examples/AR (Mobile) Example** as the example for creation. + +![create-by-ar-example](ar-proj-deploy/create-by-ar-example.png) + +The example project contains complete XR-related features for the current version of the extension, allowing you to directly build and experience. For AR application building and publishing instructions, please refer to [Build and Publish](ar-proj-pub.md). + +## 3. Scene Configuration + +If you choose the first method of applying **xr-plugin** to the project to get AR ability, you also need to follow the steps below to complete the basic AR feature configuration for a regular 3D scene. + +Each AR scene in the application must include two key objects: XR Agent and AR Camera. + +We recommend two methods to configure the scene, and you can choose either one: + +1. Right-click in the Hierarchy and select **Create XR -> XR Agent**. Select the XR Agent node, right-click to create an Empty Node, and rename it to **TrackingSpace**. Select the TrackingSpace node, and right-click to create **XR -> AR Camera**. + + ![create-ar-camera](ar-proj-deploy/create-ar-camera.png) + + Select the XR Agent node and click "Add Component" in the Inspector to add **XR -> AR Tracking -> ARSession** and **XR -> AR Tracking -> ARManager**. + + set-ar-comp + +2. For an empty scene or an existing project, you can directly select the main camera in the scene, right-click, and choose Convert to AR Camera to obtain the default structure mentioned above. + + ![convert-to-ar-camera](ar-proj-deploy/convert-to-ar-camera.png) + +XR Agent and AR Camera, along with their components, play important roles in AR projects. For more detailed information about them, please refer to [Device Mapping](../architecture/component.md) and [AR Camera](../architecture/ar-camera.m) respectively. + +## Setup Spaces Platform Project + +Create a new empty scene and right-click the Main Camera in the scene to convert it to `XR HMD`. + +Select the XR Agent and click Add Component to add `ARSession` and `ARManager` components. + +spaces-add-ar-comp + +You can refer to [Plane Tracking](../architecture/ar-tracking-component.md#plane-tracking) and [Image Tracking](../architecture/ar-tracking-component.md#image-tracking) to empower your application with AR capabilities. + +After completing the development of functionalities, you can directly build and publish the application. + +In the Dashboard's VR cases, a simple **Spaces** dedicated scene is provided. You can package the application directly and set this scene as the launch scene. + +![open-spaces-example](ar-proj-deploy/open-spaces-example.png) + +![build-spaces](ar-proj-deploy/build-spaces.png) + +For specific information about the AR SDK for the **Qualcomm Spaces** platform, please refer to [[here](https://docs.spaces.qualcomm.com)](https://docs.spaces.qualcomm.com/). diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/build-spaces.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/build-spaces.png new file mode 100644 index 0000000000..5177084627 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/build-spaces.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/convert-to-ar-camera.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/convert-to-ar-camera.png new file mode 100644 index 0000000000..397459e564 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/convert-to-ar-camera.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/create-ar-camera.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/create-ar-camera.png new file mode 100644 index 0000000000..886ccc6a43 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/create-ar-camera.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/create-by-ar-example.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/create-by-ar-example.png new file mode 100644 index 0000000000..425b8d7f40 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/create-by-ar-example.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/create-by-template.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/create-by-template.png new file mode 100644 index 0000000000..b5bfb71669 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/create-by-template.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/install-for-proj.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/install-for-proj.png new file mode 100644 index 0000000000..1c3e3f7911 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/install-for-proj.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/open-ar-template.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/open-ar-template.png new file mode 100644 index 0000000000..e1027485de Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/open-ar-template.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/open-spaces-example.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/open-spaces-example.png new file mode 100644 index 0000000000..e1beaaa147 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/open-spaces-example.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/search-in-store.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/search-in-store.png new file mode 100644 index 0000000000..3510de4951 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/search-in-store.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/set-ar-comp.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/set-ar-comp.png new file mode 100644 index 0000000000..dfb6db5963 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/set-ar-comp.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-deploy/spaces-add-ar-comp.png b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/spaces-add-ar-comp.png new file mode 100644 index 0000000000..54c66f9dd4 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-deploy/spaces-add-ar-comp.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-pub.md b/versions/4.0/en/xr/project-deploy/ar-proj-pub.md new file mode 100644 index 0000000000..9de03f58d2 --- /dev/null +++ b/versions/4.0/en/xr/project-deploy/ar-proj-pub.md @@ -0,0 +1,47 @@ +# Building And Publishing AR Project + +After completing the project development, you can build and publish the AR application by clicking on **Menu -> Project -> Build**. + +## ARCore、AREngine + +To publish an AR application for Android and Huawei platforms, create a new build task and select the `Android` platform. + +select-android-platform + +Enter the application ID and check **Enable AR**. Connect your mobile device and click **Build -> Make -> Run** to publish the AR application. + +build-android-platform + +> **Note**:The rendering backend for AR applications on the Android platform does not support VULKAN. + +## ARKit + +For iOS publishing, please refer to the [Build Options - iOS](../../editor/publish/ios/build-options-ios.md). Make sure to configure your developer account in Xcode. + +To publish an AR application for the iOS platform, create a new build task and select the `iOS` platform. + +select-ios-platform + +For the Application Bundle name, it is recommended to use the same name as the developer account configured in Xcode. Select iPhone OS Application as the target platform and check **Enable AR**. + +Click **Build** to generate the Xcode project. + +build-ios-platform + +> **Note**: Currently, Cocos Creator only supports building the iOS project. Compilation and running need to be done in Xcode. + +After the build is complete, locate the generated `xcodeproj` file and open it with Xcode. Configure the signing and developer team, connect the device, and click Run to run the application. + +open-ios-build-folder + +select-xcodeproj + +![compile-with-xcode](ar-proj-pub/compile-with-xcode.png) + +## Spaces + +To publish an AR application for Qualcomm Spaces devices, create a new build task and select the `XR Spaces` platform. + +![select-spaces-platform](ar-proj-pub/select-spaces-platform.png) + +Enter the application ID, connect the Spaces device (or mobile device for standalone devices), and click **Build -> Make -> Run** to publish the AR application. diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-pub/build-android-platform.png b/versions/4.0/en/xr/project-deploy/ar-proj-pub/build-android-platform.png new file mode 100644 index 0000000000..49cc6a4a98 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-pub/build-android-platform.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-pub/build-ios-platform.png b/versions/4.0/en/xr/project-deploy/ar-proj-pub/build-ios-platform.png new file mode 100644 index 0000000000..cd772ded7d Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-pub/build-ios-platform.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-pub/compile-with-xcode.png b/versions/4.0/en/xr/project-deploy/ar-proj-pub/compile-with-xcode.png new file mode 100644 index 0000000000..57e9aed143 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-pub/compile-with-xcode.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-pub/open-ios-build-folder.png b/versions/4.0/en/xr/project-deploy/ar-proj-pub/open-ios-build-folder.png new file mode 100644 index 0000000000..dae479bfb0 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-pub/open-ios-build-folder.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-android-platform.png b/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-android-platform.png new file mode 100644 index 0000000000..55f3f61eb6 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-android-platform.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-ios-platform.png b/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-ios-platform.png new file mode 100644 index 0000000000..dd5f9bdb95 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-ios-platform.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-spaces-platform.png b/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-spaces-platform.png new file mode 100644 index 0000000000..83bd668b5f Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-spaces-platform.png differ diff --git a/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-xcodeproj.png b/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-xcodeproj.png new file mode 100644 index 0000000000..aa54416421 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/ar-proj-pub/select-xcodeproj.png differ diff --git a/versions/4.0/en/xr/project-deploy/index.md b/versions/4.0/en/xr/project-deploy/index.md new file mode 100644 index 0000000000..26d37c2f74 --- /dev/null +++ b/versions/4.0/en/xr/project-deploy/index.md @@ -0,0 +1,3 @@ +# Quick Start Guide + +Here are the recommended steps to create and publish XR projects. Developers can follow the following workflow to quickly get started with Cocos CreatorXR. diff --git a/versions/4.0/en/xr/project-deploy/vr-proj-deploy.md b/versions/4.0/en/xr/project-deploy/vr-proj-deploy.md new file mode 100644 index 0000000000..dad47efbdc --- /dev/null +++ b/versions/4.0/en/xr/project-deploy/vr-proj-deploy.md @@ -0,0 +1,21 @@ +# VR Project Creation + +Cocos CreatorXR supports users to quickly create VR projects using the following methods. + +> **Note**: When creating XR projects, make sure that the Cocos Creator version >= 3.6.1. + +## Create VR Project From Template + +In the Cocos Dashboard, create a new project and choose an editor version of v3.6.1 or above (for full functionality, choose an engine version of v3.7.1 or above). Select the **Templates/Empty(VR)** template to create the project. + +![deploy-by-template](vr-proj-deploy/deploy-by-template.png) + +## Create VR Project From Example + +In the Cocos Dashboard, create a new project and choose an editor version of v3.6.1 or above (for full functionality, choose an engine version of v3.7.1 or above). Select the **Examples/VR Example** to create the project. + +![deploy-by-example](vr-proj-deploy/deploy-by-example.png) + +## Apply XR Extension To An Existing Project + +Add XR extension to an existing or empty project: Search for xr-plugin on the Store page in the Dashboard and download and install it for your project. Alternatively, in Cocos Creator, go to **Extensions -> Store** and download and install the extension for your project (installing it globally is not recommended). For instructions on downloading and installing extensions, please refer to the Cocos Creator Extension Installation Tutorial. [Cocos Creator Extension - Install and Share](../../editor/extension/install.md). diff --git a/versions/4.0/en/xr/project-deploy/vr-proj-deploy/deploy-by-example.png b/versions/4.0/en/xr/project-deploy/vr-proj-deploy/deploy-by-example.png new file mode 100644 index 0000000000..693c961138 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/vr-proj-deploy/deploy-by-example.png differ diff --git a/versions/4.0/en/xr/project-deploy/vr-proj-deploy/deploy-by-template.png b/versions/4.0/en/xr/project-deploy/vr-proj-deploy/deploy-by-template.png new file mode 100644 index 0000000000..fc5ccee9e4 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/vr-proj-deploy/deploy-by-template.png differ diff --git a/versions/4.0/en/xr/project-deploy/vr-proj-pub.md b/versions/4.0/en/xr/project-deploy/vr-proj-pub.md new file mode 100644 index 0000000000..1de46d3327 --- /dev/null +++ b/versions/4.0/en/xr/project-deploy/vr-proj-pub.md @@ -0,0 +1,31 @@ +# Building and Publishing VR Project + +Once the project development is completed, it needs to be published to the target platform. In the menu bar, select **Project -> Build** to open the **Build** panel. Choose the target platform from the dropdown menu. + +![select-platform](vr-proj-pub/select-platform.png) + +## Options + +For general build options, you can refer to the: [General Build Options](../../editor/publish/build-panel.md)。 + +The supported VR devices currently use the Android system, so developers need to set up the corresponding development environment. For details, please refer to the [Setting up the Native Development Environment](../../editor/publish/setup-native-development.md) documentation. + +The XR-specific build options are described as follows: + +- **Rendering Scale**: Adjusts the rendering resolution. + +- **MSAA**: Adjusts the level of multisample anti-aliasing. + +- **Remote Preview**: Enables wireless projection preview. + +- **Foveation Level**: Adjusts the foveation rendering level. A higher level reduces the GPU load but also decreases the resolution of the per-eye rendering textures at the edges. (Note: 1. Foveation rendering uses the OpenXR universal interface. If the device's FFR feature is not integrated with the OpenXR standard, this feature will not work. 2. This feature requires **extension version >= 1.2.0** and the **Cocos Creator version >= 3.7.3**). + +## Build + +Afterward, in the build task, choose Build, Make, and Run according to your needs. + +![build](vr-proj-pub/build.png) + +## Publish + +Once the application is generated, you can transfer the application to the target device using `adb commands` or the device's file transfer function. After that, you can run the application. diff --git a/versions/4.0/en/xr/project-deploy/vr-proj-pub/build.png b/versions/4.0/en/xr/project-deploy/vr-proj-pub/build.png new file mode 100644 index 0000000000..076fcb68f8 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/vr-proj-pub/build.png differ diff --git a/versions/4.0/en/xr/project-deploy/vr-proj-pub/select-platform.png b/versions/4.0/en/xr/project-deploy/vr-proj-pub/select-platform.png new file mode 100644 index 0000000000..c2a5502699 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/vr-proj-pub/select-platform.png differ diff --git a/versions/4.0/en/xr/project-deploy/webxr-proj-deploy.md b/versions/4.0/en/xr/project-deploy/webxr-proj-deploy.md new file mode 100644 index 0000000000..9de3dd332d --- /dev/null +++ b/versions/4.0/en/xr/project-deploy/webxr-proj-deploy.md @@ -0,0 +1,27 @@ +# WebXR Project Setup + +The process of creating a WebXR project is the same as creating a regular XR project. + +If you want to create a project for an immersive virtual reality (VR) experience, you can refer to [VR Project Creation](vr-proj-deploy.md). + +If you want to create a project for an immersive augmented reality (AR) experience, you can refer to [AR Project Creation](ar-proj-deploy.md). + +After completing, you need to add the `cc.WebXRSessionController` component to the **XR Agent** node. The component can be found at **XR > Device > WebXRSessionController**. + +Choose the default `Session Mode` based on your needs: + +- IMMERSIVE_AR: The session will have exclusive access to the immersive XR device, and the rendered content will be blended with the real-world environment. +- IMMERSIVE_VR: The session's rendering of the scene will not be overlaid or blended with the real-world environment. +- INLINE: The 3D content will be displayed inline within the element context of a standard HTML document, without occupying the entire visual space. Inline sessions can be rendered on both monoscopic and stereoscopic devices, and they do not require positional tracking. Inline sessions can be used on any [User Agent](https://developer.mozilla.org/en-US/docs/Glossary/User_agent) that supports the WebXR API. + + + +## WebXR Examples + +There are two examples in the Cocos Dashboard that are specifically designed for WebXR. + +The main WebXR scene for the VR example is "webxr-main". Make sure to set this scene as the startup scene. + +The main WebXR scene for the AR (mobile) example is also "webxr-main". Make sure to set this scene as the startup scene. Other scenes are shown below. + + diff --git a/versions/4.0/en/xr/project-deploy/webxr-proj-deploy/add-wenxr-session-ctrl.png b/versions/4.0/en/xr/project-deploy/webxr-proj-deploy/add-wenxr-session-ctrl.png new file mode 100644 index 0000000000..548e523afc Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/webxr-proj-deploy/add-wenxr-session-ctrl.png differ diff --git a/versions/4.0/en/xr/project-deploy/webxr-proj-deploy/ar-scenes.png b/versions/4.0/en/xr/project-deploy/webxr-proj-deploy/ar-scenes.png new file mode 100644 index 0000000000..06eb976be9 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/webxr-proj-deploy/ar-scenes.png differ diff --git a/versions/4.0/en/xr/project-deploy/webxr-proj-pub.md b/versions/4.0/en/xr/project-deploy/webxr-proj-pub.md new file mode 100644 index 0000000000..42254c63f1 --- /dev/null +++ b/versions/4.0/en/xr/project-deploy/webxr-proj-pub.md @@ -0,0 +1,45 @@ +# Building and Publishing WebXR Project + +After completing the [WebXR Project Setup](webxr-proj-deploy.md) and finishing the development, you can build and publish the WebXR application by clicking on **Menu Bar -> Project -> Build**. + +## Setting up WebXR Build Options + +Choose **Web-Mobile** as the target build platform. + + + +Enable the `WebXR` option in the **Build** panel. + + + +Click on **Build** to proceed. + +## Configure HTTPS Environment + +The server used to provide web resources for WebXR must be served in a [Secure Context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). + +To start an HTTPS server, a .pem (certificate file) needs to be configured for the domain hosting service. + +Since the current version does not support starting a built-in HTTPS server, users need to start it manually. + +Place the .pem file in the root directory of the build folder. + +![webxr-proj-pub/https-license.png](webxr-proj-pub/https-license.png) + +In the command terminal, navigate to this directory and enter the command: `https-server -S`. This will start the HTTPS server. + + + +## Select Compatible Devices and Browsers + +Refer to the [ARCore official documentation](https://developers.google.com/ar/devices) for a list of devices that support ARCore. + +Refer to [WEBXR browser_compatibility - mozilla.org]((https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API#browser_compatibility) +) for a list of browsers that support WebXR. + +## Enable the WebXR Feature of the Browser + +Before accessing a WebXR application using the Chrome browser, it's necessary to ensure that the WebXR feature is enabled. + +Go to **[chrome://flags](chrome://flags)** and set the `webxr incubations` to `Enable`. + diff --git a/versions/4.0/en/xr/project-deploy/webxr-proj-pub/enable-webxr.png b/versions/4.0/en/xr/project-deploy/webxr-proj-pub/enable-webxr.png new file mode 100644 index 0000000000..136bf706a4 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/webxr-proj-pub/enable-webxr.png differ diff --git a/versions/4.0/en/xr/project-deploy/webxr-proj-pub/https-license.png b/versions/4.0/en/xr/project-deploy/webxr-proj-pub/https-license.png new file mode 100644 index 0000000000..8884d5879c Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/webxr-proj-pub/https-license.png differ diff --git a/versions/4.0/en/xr/project-deploy/webxr-proj-pub/select-web-mobile.png b/versions/4.0/en/xr/project-deploy/webxr-proj-pub/select-web-mobile.png new file mode 100644 index 0000000000..3b31b607b1 Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/webxr-proj-pub/select-web-mobile.png differ diff --git a/versions/4.0/en/xr/project-deploy/webxr-proj-pub/start-https-server.png b/versions/4.0/en/xr/project-deploy/webxr-proj-pub/start-https-server.png new file mode 100644 index 0000000000..9848b3f98f Binary files /dev/null and b/versions/4.0/en/xr/project-deploy/webxr-proj-pub/start-https-server.png differ diff --git a/versions/4.0/en/xr/version-history.md b/versions/4.0/en/xr/version-history.md new file mode 100644 index 0000000000..1fc84debf1 --- /dev/null +++ b/versions/4.0/en/xr/version-history.md @@ -0,0 +1,53 @@ +# Version History + +## V1.2.0 + +Added: +- Added support for basic mode light estimation on iOS and basic/HDR mode light estimation on Huawei Android platforms. +- Enabled building and publishing of XR applications to the WebXR platform, with support for `inline`, `immersive-vr`, and `immersive-ar` session modes. +- Added behavior control functionality to the screen gesture interactor, allowing users to choose gesture behaviors for interactive objects. +- Enhanced the placement feature of the screen gesture interactor component, providing options to place content based on a fixed distance from the interactor. +- Added the `Composition Layer` feature, supporting `Overlay` and `Underlay` rendering modes. +- Added `Pass-Through` functionality, enabling video passthrough on standalone devices using OpenXR standard interfaces. Combined with the Composition Layer, it allows adjusting the rendering mode (Overlay/Underlay) of the passthrough video stream. +- Supported XR Web Preview feature, enabling browsing of web content on standalone devices. +- XR video player now supports playback of 3D video resources. +- Added support for spatial audio, which allows rendering audio from different directions to simulate sound behavior in the physical world, enhancing immersion. +- Added adjustments for `Fixed Foveated Rendering (FFR)`, supporting the use of OpenXR standard interfaces to enable FFR and adjust rendering levels. +- Updated `Snapdragon Spaces SDK` to version 0.11.1 and added support for RGB Camera and Meshing features. + +Fixed: + +- Improved the effectiveness of screen gesture interaction. +- Fixed the issue where the vibration duration of `Huawei VR Glass` controller was not working when set to 0. +- Fixed the error when converting AR Camera. +- Resolved compatibility issues with higher versions of Android Target API. + +## v1.1.1 + +Added: + +- Adapted to Cocos Creator 3.7.2。 + +## v1.1.0 + +Added: + +- Added AR application development module, enabling publishing of AR applications to AREngine, ARCore, ARKit, and Qualcomm Spaces platforms. Provides automated behavior editing components for quick creation and experience of AR content, supporting features such as device tracking (AR Camera), plane tracking, image tracking, anchors, and Mesh. +- Added non-buffered haptic feedback for controller vibration. Select the desired event types for vibration in `cc.InteractorEvent`'s `Haptic Event` and adjust the vibration parameters. +- Added a **one-click conversion** feature for XRUI, enabling the transformation of traditional 2D UI into XR UI with spatial properties. +- Added gaze interactor for interaction based on the gaze center position of the head-mounted device. +- Added XR video player, optimizing the video rendering pipeline for XR devices and supporting display window switching and multiple styles (180-degree, 360-degree) of videos. Meets the need for browsing panoramic videos or dynamic materials in 3D scenes. +- Added wireless streaming option for XR content preview, allowing previewing of XR projects in a web browser and synchronizing all signals from XR devices. Provides a complete and fast experience of all XR project content without the need to package the application to the device. +- Added screen gesture interactor for manipulating AR virtual objects using screen gestures. +- Added support for using the phone as a 3DOF air mouse for Rokid Air devices. + +Fixed: + +- Full support for 6DOF input on Huawei VR Glass (6DOF Kit). +- Resolved the issue of the random signal generation when using the 6DOF controller of `Huawei VR Glass` while touching the joystick. + +## v1.0.1 + +- Supported publishing XR applications to Rokid Air, HuaweiVR, Meta Quest/Quest2, Pico Neo3/Pico4, and Monado devices. +- Provided device mapping, interaction, virtual movement, and modularized XR UI components for XR content creation. +- Supported previewing XR content using a web browser, allowing simulation of headsets and controller devices using keyboard and mouse controls. diff --git a/versions/4.0/index.md b/versions/4.0/index.md new file mode 100644 index 0000000000..8834b91550 --- /dev/null +++ b/versions/4.0/index.md @@ -0,0 +1,6 @@ +# 欢迎查看 4.0 + +[english doc](./en/index.md) + + +[中文文档](./zh/index.md) diff --git a/versions/4.0/public/favicon.ico b/versions/4.0/public/favicon.ico new file mode 100644 index 0000000000..fb67f27ff9 Binary files /dev/null and b/versions/4.0/public/favicon.ico differ diff --git a/versions/4.0/public/logo.png b/versions/4.0/public/logo.png new file mode 100644 index 0000000000..dc4af327ee Binary files /dev/null and b/versions/4.0/public/logo.png differ diff --git a/versions/4.0/zh/2d-object/2d-render/add-render-component.png b/versions/4.0/zh/2d-object/2d-render/add-render-component.png new file mode 100644 index 0000000000..00384113be Binary files /dev/null and b/versions/4.0/zh/2d-object/2d-render/add-render-component.png differ diff --git a/versions/4.0/zh/2d-object/2d-render/create-2d.png b/versions/4.0/zh/2d-object/2d-render/create-2d.png new file mode 100644 index 0000000000..c0400a8c67 Binary files /dev/null and b/versions/4.0/zh/2d-object/2d-render/create-2d.png differ diff --git a/versions/4.0/zh/2d-object/2d-render/index.md b/versions/4.0/zh/2d-object/2d-render/index.md new file mode 100644 index 0000000000..5b9f23d4a0 --- /dev/null +++ b/versions/4.0/zh/2d-object/2d-render/index.md @@ -0,0 +1,47 @@ +# 2D 渲染 + +引擎中所有不拥有的 model 的渲染对象都为 2D 渲染对象。与 3D 对象不同,2D 对象本身不拥有 model 信息,其顶点信息是由 UITransform 组件的 Rect 信息持有并由引擎创建的,且本身没有厚度。由于引擎的设计要求,2D 渲染对象需要为 RenderRoot 节点(带有 RenderRoot2D 组件的节点)的子节点才能完成数据的收集操作。 + +所以 2D 渲染对象的渲染要求有两点: +1. 自身带有 UITransform 组件 +2. 需要为带有 RenderRoot2D/Canvas 组件节点的子节点 + +## 2D 渲染对象可见性说明 + +由于 2D 渲染对象在 Camera 的可见性判断上和 3D 渲染节点并无区别,所以用户需要自己控制节点的 layer 属性并设置 Camera 的 Visibility 来配合进行分组渲染,如果场景中出现多个相机的情况,错误的 layer 设置导致节点重复渲染或不渲染。 + +**这里请 3D 1.2 版本升级的用户注意,我们纠正了之前的 Canvas 只会渲染其子节点的行为,目前需要用户自己管理节点的 layer 和相机的 Visibility,之前使用了多 Canvas 渲染的用户可能会需要对项目做出调整以达到更合理的场景结构。** + +## 2D 渲染组件 + +本身拥有渲染能力的组件我们称为 2D 渲染组件,包括: + +- [Sprite 组件参考](../../ui-system/components/editor/sprite.md) +- [Label 组件参考](../../ui-system/components/editor/label.md) +- [Mask 组件参考](../../ui-system/components/editor/mask.md) +- [Graphics 组件参考](../../ui-system/components/editor/graphics.md) +- [RichText 组件参考](../../ui-system/components/editor/richtext.md) +- [UIStaticBatch 组件参考](../../ui-system/components/editor/ui-static.md) +- [TiledMap 组件参考](../../editor/components/tiledmap.md) +- [TiledTile 组件参考](../../editor/components/tiledtile.md) +- [Spine(骨骼动画)Skeleton 组件参考](../../editor/components/spine.md) +- [DragonBones(龙骨)ArmatureDisplay 组件参考](../../editor/components/dragonbones.md) +- [MotionStreak 组件参考](../../editor/components/motion-streak.md) + +### 组件添加方式 + +我们在编辑器内置了一些 2D 渲染组件,在创建了 RenderRoot 节点之后,即可在此节点下创建带有 2D 渲染组件的节点: + +![create-2d](./create-2d.png) + +也可以通过在节点上添加组件的方式来添加 2D 渲染组件,组件菜单中的 2D 菜单下的节点均为 2D 渲染组件: + +![add-render-component](./add-render-component.png) + +> **注意**:每个节点上只能添加一个渲染组件,重复添加会导致报错。 + +### 组件规则介绍 + +- [渲染排序规则](../../ui-system/components/engine/priority.md) +- [2D 渲染组件合批规则说明](../../ui-system/components/engine/ui-batch.md) +- [2D 渲染对象自定义材质](../../ui-system/components/engine/ui-material.md) diff --git a/versions/4.0/zh/2d-object/index.md b/versions/4.0/zh/2d-object/index.md new file mode 100644 index 0000000000..cca7702026 --- /dev/null +++ b/versions/4.0/zh/2d-object/index.md @@ -0,0 +1,20 @@ +# 2D 对象概述 + +区别于 3D 模型对象,我们将不涉及模型的图片渲染体统称为 2D 渲染对象。2D 渲染对象的处理在底层的数据提交上与 3D 模型存在差异,其遵循自己的规则做出了一些针对性的调整以实现更好的效率表现和使用体验。 + +## 2D 对象渲染结构说明 + +2D 渲染对象的收集采用树状结构,RenderRoot 节点(带有 RenderRoot2D 组件的节点)为 2D 对象数据收集的入口节点,所有的 2D渲染对象需在 RenderRoot 节点下才可以被渲染。由于 Canvas 组件本身继承 RenderRoot2D 组件,所以 Canvas 组件也可以作为数据收集的入口。2D 渲染节点必须带有 UITransform 组件作为渲染顶点数据、点击或者对齐策略等功能生效的必要条件。 + +2D 渲染也可以支持对模型进行渲染,唯一的条件是带有网格渲染器组件(例如 `MeshRenderer`/`SkinnedMeshRenderer`)的节点必须添加 **UI/UIMeshRenderer** 组件才可以和 UI 在相同的管线上进行渲染。 + +2D 渲染流程如下: + +![render](render.png) + +## 2D 对象分类 + +2D 对象大致上可以分为两类,**2D 渲染对象** 和 **用户界面 User-interface(UI)**,其区别主要在于 2D 渲染对象一般只负责将 2D 对象渲染出来,而 UI 则更多的承担着用户交互的能力。具体的差别用户可参考具体的详细说明: + +- [2D 渲染](2d-render/index.md) +- [UI 系统](ui-system/index.md) diff --git a/versions/4.0/zh/2d-object/render.png b/versions/4.0/zh/2d-object/render.png new file mode 100644 index 0000000000..87660bc658 Binary files /dev/null and b/versions/4.0/zh/2d-object/render.png differ diff --git a/versions/4.0/zh/2d-object/ui-system/add-ui-component.png b/versions/4.0/zh/2d-object/ui-system/add-ui-component.png new file mode 100644 index 0000000000..c329cb01c9 Binary files /dev/null and b/versions/4.0/zh/2d-object/ui-system/add-ui-component.png differ diff --git a/versions/4.0/zh/2d-object/ui-system/create-ui.png b/versions/4.0/zh/2d-object/ui-system/create-ui.png new file mode 100644 index 0000000000..c7042f971d Binary files /dev/null and b/versions/4.0/zh/2d-object/ui-system/create-ui.png differ diff --git a/versions/4.0/zh/2d-object/ui-system/index.md b/versions/4.0/zh/2d-object/ui-system/index.md new file mode 100644 index 0000000000..f3741a7cdb --- /dev/null +++ b/versions/4.0/zh/2d-object/ui-system/index.md @@ -0,0 +1,47 @@ +# UI 系统 + +本章将介绍 Cocos Creator 中强大而灵活的 UI(用户界面)系统,通过组合不同 UI 组件来生产能够适配多种分辨率屏幕的、通过数据动态生成和更新显示内容,以及支持多种排版布局方式的 UI 界面。 + +## UI 入门 + +在引擎中界定 UI 和 2D 渲染对象的区别主要在于适配和交互,所有的 UI 需要在 Canvas 节点下,以做出适配行为,而 Canvas 组件本身继承自 `RenderRoot2D` 组件,所以也可以作为数据收集的入口。 + +UI 是游戏开发的必要交互部分,一般游戏上的按钮、文字、背景等都是通过 UI 来制作的。在开始制作一款 UI 时,首先需要确定当前设计的内容显示区域大小(设计分辨率),可以在菜单栏的 **项目 -> 项目设置 -> 项目数据** 面板中设置: + +![resolution-config](resolution_config.png) + +设计分辨率设置完成后,开始创建 UI 元素,所有的 UI 元素都包含在 Canvas 节点下。可以在 **层级管理器** 面板中点击左上方的 **+** 按钮,然后选择 **UI Component -> Canvas** 来创建 Canvas 节点。Canvas 节点上有一个 [Canvas](../../ui-system/components/editor/canvas.md) 组件,该组件可以关联一个 camera。 + +> **注意**: +> +> 1. 在一个场景中可以存在多个 Canvas 节点,但是 Canvas 节点不应该嵌套在另一个 Canvas 节点或其子节点下。 +> 2. Canvas 组件并非和 camera 是一一对应关系,它们之前的渲染与否完全取决于 node 的 layer 和 camera 的 Visibility,在多 Canvas 的时候要格外注意 layer 管理以得到想要的渲染效果。 + +接下来就可以在 Canvas 节点下创建 UI 节点了。编辑器自带的 UI 节点有以下几种: + +![create-ui](./create-ui.png) + +可以通过选中节点,在 **属性检查器** 面板中点击 **添加组件** 来查看 UI 组件。 + +![add-ui-component](./add-ui-component.png) + +UI 渲染组件的先后顺序采用的是深度排序方案,也就是 Canvas 节点下的子节点的排序就已经决定了之后的整个 [渲染排序](../../ui-system/components/engine/priority.md)。 + +在一般的游戏开发中,必要的 UI 元素除了 Sprite(精灵图)、Label(文字)、Mask(遮罩)等基础 2D 渲染组件外,还有用于快速搭建界面的 Layout(布局)、Widget(对齐)等。其中 Sprite 和 Label 用于渲染图片和文字。Mask 主要用于限制显示内容,比较常用的地方是一些聊天框和背包等。Layout 主要用于排版,一般用于按钮单一排列,背包内道具整齐排列等。
+最后一个比较重要的功能其实是 Widget,主要用于显示对齐。这里可能涉及到另外一个功能,那就是多分辨率适配,在我们设计完 UI 需要发布到不同平台时,势必会出现平台的实际设备分辨率和我们的设计分辨率不符的情况,这个时候为了适配不得不做一些取舍,比如头像框,是不能做缩放的,但是我们又希望它没有很大程度受设备影响,那么我们则需要为它添加上 Widget 组件,并且始终保证它对齐在我们的设计分辨率的左上方,具体参考:[对齐策略](../../ui-system/components/engine/widget-align.md) 和 [对齐](../../ui-system/components/editor/widget.md)。 + +当我们的界面制作完成之后,可能有人会发现,怎么发布 iPhone 7 和 iPhone X 的显示效果不一样?这个其实也是我们上面提到的设备分辨率的问题。在你以设计分辨率设计,最终以设备分辨率发布的时候,因为不同型号的手机设备分辨率可能不一致,这中间存在像素偏差的问题,因此,还需要做的一道转换工序那就是屏幕适配。
+在菜单栏的 **项目 -> 项目设置 -> 项目数据** 页面中可以看到,还有两个选项是 **适配屏幕宽度 / 适配屏幕高度**,按照屏幕适配规则再结合 Widget 组件,就可以实现不同设备的轻松适配。具体适配规则可参考 [多分辨率适配方案](../../ui-system/components/engine/multi-resolution.md)。 + +## UI 组件 + +UI 组件大部分自身不具有渲染能力,但持有了 2D 渲染组件用于渲染,其本身更多拥有着快速构成用户交互界面的能力,承担着事件响应,排版适配等功能。各 UI 组件具体说明请参考 [UI 组件](../../ui-system/components/editor/base-component.md)。 + +## UI 实践指南 + +- [多分辨率适配方案](../../ui-system/components/engine/multi-resolution.md) +- [对齐策略](../../ui-system/components/engine/widget-align.md) +- [文字排版](../../ui-system/components/engine/label-layout.md) +- [自动布局容器](../../ui-system/components/engine/auto-layout.md) +- [制作动态生成内容的列表](../../ui-system/components/engine/list-with-data.md) +- [制作可任意拉伸的 UI 图像](../../ui-system/components/engine/sliced-sprite.md) diff --git a/versions/4.0/zh/2d-object/ui-system/resolution_config.png b/versions/4.0/zh/2d-object/ui-system/resolution_config.png new file mode 100644 index 0000000000..6ab88f3dfe Binary files /dev/null and b/versions/4.0/zh/2d-object/ui-system/resolution_config.png differ diff --git a/versions/4.0/zh/CONTRIBUTING.md b/versions/4.0/zh/CONTRIBUTING.md new file mode 100644 index 0000000000..d9e5d83b35 --- /dev/null +++ b/versions/4.0/zh/CONTRIBUTING.md @@ -0,0 +1,361 @@ +# 中文文档编写规范 + +格式规范的目的是为了提供统一的书写准则,并使成品文档有更好的阅读体验。 + +## 标题使用 #,与上下正文需要使用空行隔开 + +一级标题使用一个 #,二级标题使用两个 ##,以此类推。 + +一般情况下,不要跳级使用标题,例如一级标题下,不能直接出现三级标题。 + +## 关于产品名的写法 + +1. Cocos Creator +2. Cocos Creator 2.4.3 +3. v2.4.3 +4. v3.0 +5. The engine (where refer to the runtime) +6. The editor (where refer to the IDE) + +**版本**: + +- 2.4.3 (3.0.0) +- 2.4.3 Preview (GA/RC/Alpha/Beta ...) +- 2.4.x (3.0.x) +- 2.4 (3.0) +- 2.x (3.x) + +**不要使用以下写法**: + +1. CCC or ccc +2. Cocos (where refer to Cocos Creator) +3. IDE (where refer to Cocos Creator) + +## 专有英文名词、组件名使用正确的首字母大写 + +正确: + +> 使用 GitHub 登录 +> +> Sprite 组件 + +错误: + +> 使用 github 登录 +> +> sprite 组件 + +## 使用空格 + +### 中英文之间、中文与数字之间需要增加空格 + +正确: + +> 本站点使用 Jekyll 搭建,应用 HPSTR 主题。文章保存在 `_posts` 目录下。 +> +> 今天出去买菜花了 5000 元 + +错误: + +> 本站点使用Jekyll搭建,应用HPSTR主题。文章保存在`_posts`目录下。 +> +> 今天出去买菜花了5000元 + +完整的正确用法: + +> 请尽量避免直接使用系统自带的 Ruby,推荐使用 rbenv 来管理本地 Ruby 运行环境,同时使用 ruby-build 来安装 Ruby,现在使用的 Ruby 版本为 2.1.1。 + +### 数字与单位之间需要增加空格 + +正确: + +> 我家的带宽有 1 Gbps,硬盘一共有 10 TB。 + +错误: + +> 我家的带宽有 1Gbps,硬盘一共有 10TB。 + +**例外:度/百分比与数字之間不需要增加空格** + +正确: + +> 今天是 233° 的高溫。 +> +> 新 MacBook Pro 有 15% 的 CPU 性能提升。 + +错误: + +> 今天是 233 ° 的高溫。 +> +> 新 MacBook Pro 有 15 % 的 CPU 性能提升。 + +### 跳转链接和相邻的正文之间需要增加空格 + +跳转链接格式:**[跳转的文档名称]\(跳转的文档路径)**。使用半角(halfwidth)英文标点,且 [] 与 () 之间不要有空格 + +> e.g:[监听和发射事件]\(../scripting/events.md)。 + +正确用法: + +> 详情请参考 [监听和发射事件](../scripting/events.md) 文档。 + +错误用法: + +> 详情请参考[监听和发射事件](../scripting/events.md)文档。 + +### 添加跳转到 API 文档的跳转链接,两边需要加空格 + +跳转链接格式:**[跳转的文档名称]\(跳转的文档目录)**。使用半角(halfwidth)英文标点,且 [] 与 () 之间不要有空格 + +> e.g:[Mask API]\(\_\_APIDOC\_\_/zh/#/docs/3.5/zh/ui/Class/Mask)。跨文档的文件名后缀要使用 **.html** + +## 使用加粗且和相邻的正文之间加空格 + +**编辑器中的面板名称、组件或其他重要界面元素**,使用加粗表示,并且和相邻的正文之间需要加空格。 + +> 格式:打开 \*\*属性检查器\*\* 查看属性 + +正确: + +> 打开 **属性检查器** 查看属性 +> +> 点击 **创建** 按钮创建新节点 +> +> 拖拽任意一张图片资源到 **Sprite Frame** 属性中 + +需要注意的是编辑器中的属性名,要按照属性检查器中显示的格式书写。 + +## 使用 backtick,且和相邻的正文之间加空格 + +### 脚本属性和方法名按照 API 中显示的格式书写,使用 backtick 表示,并且和相邻的正文之间加空格 + +> 格式:通过 \`this.node.scale\` 来设置节点的缩放 + +效果如下: + +> 通过 `this.node.scale` 来设置节点的缩放 + +### 文件名和文件路径,使用 backtick 表示,并且和相邻的正文之间加空格 + +格式:\`/mypath/myfile.ts\` + +如果是完整路径,前面需要加 /,如果不是完整路径则不需要 + +正确: + +> 分包的目录在 `build/quickgame/dist` 目录下 + +## 使用空行 + +### 代码段落与上下文之间需要使用空行隔开 + +例如: + +> 保存 Prefab,代码范例如下:
+> +> \```js
+> Editor.Ipc.sendToPanel('scene', 'scene:apply-prefab', node.uuid);
+> \``` +> +> 继续正文部分。 + +效果如下: + +> 保存 Prefab,代码范例如下: +> +> ```js +> Editor.Ipc.sendToPanel('scene', 'scene:apply-prefab', node.uuid); +> ``` +> +> 继续正文部分。 + +### 图片与上下正文之间需要使用空行隔开 + +图片格式:**![图片说明]\(图片的相对路径)**。使用半角(halfwidth)英文标点,且 !、[]、() 三者之间不要有空格 + +> e.g:!\[background]\(quick-start/background.png) + +如果图片是添加在正文中,那么和相邻的正文之间需要加空格 + +## 换行使用空行或者 \ + +如果使用回车键换行,在 GitBook 上没有换行效果并且会增加一个空格 + +**使用空行换行效果**: + +> 第一行 +> +> 第二行 + +**使用 \ 换行效果**: + +> 第一行
+> 第二行 + +**使用回车键换行效果(不推荐)**: + +> 第一行 +> 第二行 + +## 使用中英文标点符号 + +### 中文排版时,一律使用全角(fullwidth)中文标点 + +正确: + +> 嗨!你知道吗?今天前台的小妹跟我说“喵”了哎! + +错误: + +> 嗨!你知道吗?今天前台的小妹跟我说"喵"了哎! +> +> 嗨!你知道吗?今天前台的小妹跟我说"喵"了哎! + +### 中英排版时,一律使用全角(fullwidth)中文标点 + +正确: + +> 核磁共振成像(NMRI)是什么原理都不知道?JFGI! + +错误: + +> 核磁共振成像(NMRI)是什么原理都不知道?JFGI! +> +> 核磁共振成像(NMRI)是什么原理都不知道?JFGI! + +### 中英排版时,遇到完整的英文整句,整句内容使用半角(halfwidth)英文标点 + +正确: + +> 乔帮主那句话怎么说的?“Stay hungry, stay foolish.” + +错误: + +> 乔帮主那句话怎么说的?“Stay hungry,stay foolish。” + +## 使用顿号 + +**中文文档句子内部的并列词应该用全角顿号(、)分隔,而不用逗号,即使并列词是英语也是如此。** + +正确: + +> 科技公司有 Google、Facebook、腾讯、阿里和百度等。 + +错误: + +> 科技公司有 Google, Facebook, 腾讯, 阿里和百度等。 + +**英文句子中,并列词语之间使用半角逗号(,)分隔** + +> e.g.: Microsoft Office includes Word, Excel, PowerPoint, Outlook and other components. + +**中文句子内部的并列词,最后一个尽量使用(和)来连接,使句子读起来更加连贯** + +例如下面两个句子都可以,但第二个更优。 + +> 科技公司有 Google、Facebook、腾讯、阿里,以及百度等。 +> +> 科技公司有 Google、Facebook、腾讯、阿里和百度等。 + +更多关于标点符号的使用请参考:。 + +## 注意事项的书写格式 + +> 格式:> \*\*注意\*\*:中英文文档的中英文符号不要混用 + +当注意事项在两点以上时书写格式如下: + +> \> \*\*注意\*\*:
+> \> 1. 第一点。
+> \> 2. 第二点。
+> \> 3. 最后一点。
+ +效果如下: + +> **注意**: +> 1. 第一点。 +> 2. 第二点。 +> 3. 最后一点。 + +## 分点介绍 + +当正文使用 **-** 或者 **数字** 分点介绍时,分点中的正文包括图片都需要保留 4 个空格的缩进。 + +例如使用 **-** 分点介绍时: + +> \- 分点介绍 1 +> +> (4 个空格)开始第一个分点的正文部分。 +> +> \- 分点介绍 2 +> +> (4 个空格)开始第二个分点的正文部分。 +> +> (4 个空格)!\[image]\(图片链接) + +效果如下: + +> - 分点介绍 1 +> +> 开始第一个分点的正文部分。 +> +> - 分点介绍 2 +> +> 开始第二个分点的正文部分。 +> +> !\[image]\(图片链接) + +例如使用 **数字** 分点介绍时: + +> 1\. 分点介绍 1 +> +> (4 个空格)开始第一个分点的正文部分。 +> +> 2\. 分点介绍 2 +> +> (4 个空格)开始第二个分点的正文部分。 +> +> (4 个空格)!\[image]\(图片链接) + +效果如下: + +> 1. 分点介绍 1 +> +> 开始第一个分点的正文部分。 +> +> 2. 分点介绍 2 +> +> 开始第二个分点的正文部分。 +> +> !\[image]\(图片链接) + +## 表格统一使用左对齐 + +例如: + +> | 属性 | 功能说明 |
+> | \:---- | :------ |
+> | 属性 1 | 说明 1 |
+> | 属性 2 | 说明 2 | + +前后可保留适当的空格,方便编辑。 + +## 脚注写法 + +例如: + +这是有脚注的内容。\[^1] + +\[^1]: 这是脚注的注释内容。 + +效果如下: + +这是有脚注的内容。[^1] + +[^1]: 这是脚注的注释内容。 + +## 参考链接 + +- [中文文档格式规范](https://github.com/anjuke/coding-style/blob/master/text/chinese.md) +- [文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines) +- [中文技术文档的写作规范](https://github.com/ruanyf/document-style-guide) diff --git a/versions/4.0/zh/SUMMARY.md b/versions/4.0/zh/SUMMARY.md new file mode 100644 index 0000000000..4203f3b786 --- /dev/null +++ b/versions/4.0/zh/SUMMARY.md @@ -0,0 +1,550 @@ +# Summary + +## 用户手册 4.0 (LTS) + +- [Cocos Creator 4.0 LTS](index.md) +- [关于 Cocos Creator](getting-started/introduction/index.md) +- [获取帮助和支持](getting-started/support.md) + +## 基础知识 + +- [新手入门](getting-started/index.md) + - [安装和启动](getting-started/install/index.md) + - [使用 Dashboard](getting-started/dashboard/index.md) + - [Hello World!](getting-started/helloworld/index.md) + - [项目结构](getting-started/project-structure/index.md) +- [编辑器界面](editor/index.md) + - [场景编辑器](editor/scene/index.md) + - [层级管理器](editor/hierarchy/index.md) + - [资源管理器](editor/assets/index.md) + - [属性检查器](editor/inspector/index.md) + - [控制台](editor/console/index.md) + - [偏好设置](editor/preferences/index.md) + - [项目设置](editor/project/index.md) + - [主菜单](editor/mainMenu/index.md) + - [工具栏](editor/toolbar/index.md) + - [编辑器布局](editor/editor-layout/index.md) + - [预览调试](editor/preview/index.md) +- [术语](glossary/index.md) +- [Unity 开发者快速入门指南](guide/unity/index.md) + +## 示例和教程 + +- [快速上手:制作第一个 2D 游戏](getting-started/first-game-2d/index.md) + - [监听触摸事件](getting-started/first-game-2d/touch.md) +- [快速上手:制作第一个 3D 游戏](getting-started/first-game/index.md) + - [进阶篇 - 添加阴影、光照和骨骼动画](getting-started/first-game/advance.md) +- [示例与教程](cases-and-tutorials/index.md) + +## 工作流 + +- [升级指南](release-notes/index.md) + - [v3.0 升级指南](release-notes/upgrade-guide-v3.0.md) + - [v3.0 材质升级指南](material-system/effect-2.x-to-3.0.md) + - [v3.1 材质升级指南](material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md) + - [v3.3 动画剪辑数据升级指南](animation/animation-clip-migration-3.3.x.md) + - [v3.5 材质升级指南](material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md) + - [资源分包升级指南](asset/subpackage-upgrade-guide.md) + - [资源管理模块升级指南](asset/asset-manager-upgrade-guide.md) + - [v3.5 已构建工程升级指南](engine/template/native-upgrade-to-v3.5.md) + - [v3.6 已构建工程升级指南](engine/template/native-upgrade-to-v3.6.md) + - [v3.6 构建模板与 settings.json 升级指南](release-notes/build-template-settings-upgrade-guide-v3.6.md) + - [Cocos Creator 3.6 材质升级指南](material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md) + - [升级指南:粒子从 v3.5.x 升级到 v3.6.0](particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md) + - [v3.8 Android 工程升级](release-notes/upgrade-3.8-android.md) + +- [场景制作](concepts/scene/index.md) + - [场景资源](asset/scene.md) + - [节点和组件](concepts/scene/node-component.md) + - [坐标系和节点变换](concepts/scene/coord.md) + - [节点层级和渲染顺序](concepts/scene/node-tree.md) + - [使用场景编辑器搭建场景](concepts/scene/scene-editing.md) + - [多层次细节](editor/rendering/lod.md) + +- [资源系统](asset/index.md) + - [资源工作流](asset/asset-workflow.md) + - [图像资源](asset/image.md) + - [纹理贴图资源](asset/texture.md) + - [精灵帧资源](asset/sprite-frame.md) + - [图像资源的自动剪裁](ui-system/components/engine/trim.md) + - [压缩纹理资源](asset/compress-texture.md) + - [立方体贴图资源](asset/texture-cube.md) + - [图集资源](asset/atlas.md) + - [自动图集资源](asset/auto-atlas.md) + - [艺术数字资源](asset/label-atlas.md) + - [预制资源](asset/prefab.md) + - [字体资源](asset/font.md) + - [音频资源](asset/audio.md) + - [材质资源](asset/material.md) + - [FBX 智能材质导入](importer/materials/fbx-materials.md) + - [模型资源](asset/model/mesh.md) + - [从第三方工具导出模型资源](asset/model/dcc-export-mesh.md) + - [从 3ds Max 中导出 FBX 模型资源](asset/model/max-export-fbx.md) + - [从 Maya 中导出 FBX 模型资源](asset/model/maya-export-fbx.md) + - [glTF 模型](asset/model/glTF.md) + - [程序化创建网格](asset/model/scripting-mesh.md) + - [Spine 骨骼动画资源](asset/spine.md) + - [DragonBones 骨骼动画资源](asset/dragonbones.md) + - [TiledMap 瓦片图资源](asset/tiledmap.md) + - [JSON 资源](asset/json.md) + - [文本资源](asset/text.md) + +- [脚本指南及事件机制](scripting/index.md) + - [编程语言支持](scripting/language-support.md) + - [脚本基础](scripting/script-basics.md) + - [创建脚本](scripting/setup.md) + - [配置代码编辑环境](scripting/coding-setup.md) + - [脚本运行环境](scripting/basic.md) + - [装饰器使用](scripting/decorator.md) + - [属性参数参考](scripting/reference/attributes.md) + - [生命周期回调](scripting/life-cycle-callbacks.md) + - [开发注意事项](scripting/readonly.md) + - [脚本使用](scripting/usage.md) + - [访问节点和其他组件](scripting/access-node-component.md) + - [常用节点和组件接口](scripting/basic-node-api.md) + - [创建和销毁节点](scripting/create-destroy.md) + - [使用计时器](scripting/scheduler.md) + - [组件和组件执行顺序](scripting/component.md) + - [加载和切换场景](scripting/scene-managing.md) + - [获取和加载资源](scripting/load-assets.md) + - [tsconfig 配置](scripting/tsconfig.md) + - [脚本进阶](scripting/reference-class.md) + - [事件系统](engine/event/index.md) + - [发射和监听事件](engine/event/event-emit.md) + - [输入事件系统](engine/event/event-input.md) + - [节点事件系统](engine/event/event-node.md) + - [屏幕事件系统](engine/event/event-screen.md) + - [事件 API](engine/event/event-api.md) + - [模块规范与示例](scripting/modules/index.md) + - [引擎模块](scripting/modules/engine.md) + - [外部模块使用案例](scripting/modules/example.md) + - [模块规范](scripting/modules/spec.md) + - [导入映射](scripting/modules/import-map.md) + - [外部代码支持](scripting/external-scripts.md) + +- [跨平台发布](editor/publish/index.md) + - [通用构建选项介绍](editor/publish/build-options.md) + - [发布到小游戏平台](editor/publish/publish-mini-game.md) + - [发布到微信小游戏](editor/publish/publish-wechatgame.md) + - [启用微信小游戏引擎插件](editor/publish/wechatgame-plugin.md) + - [接入微信 PC 小游戏](editor/publish/publish-pc-wechatgame.md) + - [iOS 微信小游戏内存与性能优化指南](editor/publish/wechat-ios-optimize.md) + - [发布到淘宝小游戏](editor/publish/publish-taobao-mini-game.md) + - [启用淘宝小游戏引擎插件](editor/publish/taobaominigame-plugin.md) + - [发布到支付宝小游戏](editor/publish/publish-alipay-mini-game.md) + - [发布到抖音小游戏](editor/publish/publish-bytedance-mini-game.md) + - [接入抖音 PC 小游戏](editor/publish/publish-pc-bytedance.md) + - [iOS 抖音小游戏内存与性能优化指南](editor/publish/bytedance-ios-optimize.md) + - [发布到华为快游戏](editor/publish/publish-huawei-quick-game.md) + - [发布到荣耀小游戏](editor/publish/publish-honor-mini-game.md) + - [发布到 OPPO 小游戏](editor/publish/publish-oppo-mini-game.md) + - [发布到 vivo 小游戏](editor/publish/publish-vivo-mini-game.md) + - [开放数据域](editor/publish/build-open-data-context.md) + - [小游戏分包](editor/publish/subpackage.md) + - [发布到 Web 平台](editor/publish/publish-web.md) + - [发布到 Facebook Instant Games 平台](editor/publish/publish-fb-instant-games.md) + - [原生平台发布通用基础](editor/publish/publish-native-index.md) + - [安装配置原生环境](editor/publish/setup-native-development.md) + - [原生平台通用构建选项](editor/publish/native-options.md) + - [原生平台 JavaScript 调试](editor/publish/debug-jsb.md) + - [集成 Input SDK](editor/publish/gpg-input-sdk.md) + - [发布到 HUAWEI AppGallery Connect](editor/publish/publish-huawei-agc.md) + - [发布 HUAWEI HarmonyOS 应用](editor/publish/publish-huawei-ohos.md) + - [发布 HarmonyOS Next 应用](editor/publish/publish-openharmony.md) + - [发布 Android 应用](editor/publish/android/index.md) + - [Android 构建示例](editor/publish/android/build-example-android.md) + - [Android 构建选项](editor/publish/android/build-options-android.md) + - [v3.8 Android 工程升级](release-notes/upgrade-3.8-android.md) + - [发布 Google Play 示例](editor/publish/google-play/build-example-google-play.md) + - [发布到谷歌 GPG 平台](editor/publish/google-play-games/index.md) + - [发布和运行](editor/publish/google-play-games/build-and-run.md) + - [集成 Input SDK](editor/publish/gpg-input-sdk.md) + - [发布 iOS 应用](editor/publish/ios/index.md) + - [iOS 发布示例](editor/publish/ios/build-example-ios.md) + - [iOS 构建选项](editor/publish/ios/build-options-ios.md) + - [发布 macOS 应用](editor/publish/mac/index.md) + - [macOS 发布示例](editor/publish/mac/build-example-mac.md) + - [macOS 构建选项](editor/publish/mac/build-options-mac.md) + - [发布 Windows 应用](editor/publish/windows/index.md) + - [Windows 构建示例](editor/publish/windows/build-example-windows.md) + - [Windows 构建选项](editor/publish/windows/build-options-windows.md) + - [命令行发布项目](editor/publish/publish-in-command-line.md) + - [定制项目的构建模版](editor/publish/custom-project-build-template.md) + - [构建流程简介与常见错误处理](editor/publish/build-guide.md) + +## 功能模块 + +- [图形渲染](module-map/graphics.md) + - [渲染管线](render-pipeline/overview.md) + - [使用内置管线](render-pipeline/use-builtin-pipeline.md) + - [自定义渲染管线](render-pipeline/custom-pipeline.md) + - [使用后期效果](render-pipeline/use-post-process.md) + - [自定义后效](render-pipeline/post-process/custom.md) + - [相机](editor/components/camera-component.md) + - [光照](concepts/scene/light.md) + - [基于物理的光照](concepts/scene/light/pbr-lighting.md) + - [光源](concepts/scene/light/lightType/index.md) + - [平行光](concepts/scene/light/lightType/dir-light.md) + - [球面光](concepts/scene/light/lightType/sphere-light.md) + - [聚光灯](concepts/scene/light/lightType/spot-light.md) + - [环境光](concepts/scene/light/lightType/ambient.md) + - [基于多 Pass 的多光源支持](concepts/scene/light/additive-per-pixel-lights.md) + - [阴影](concepts/scene/light/shadow.md) + - [基于图像的光照](concepts/scene/light/probe/index.md) + - [光照探针](concepts/scene/light/probe/light-probe.md) + - [光照探针面板](concepts/scene/light/probe/light-probe-panel.md) + - [反射探针](concepts/scene/light/probe/reflection-probe.md) + - [反射探针面板](concepts/scene/light/probe/reflection-probe-panel.md) + - [反射探针美术工作流](concepts/scene/light/probe/reflection-art-workflow.md) + - [基于图像的光照示例](concepts/scene/light/probe/example.md) + - [光照贴图](concepts/scene/light/lightmap.md) + - [网格](module-map/mesh/index.md) + - [MeshRenderer 组件](engine/renderable/model-component.md) + - [SkinnedMeshRenderer 组件](module-map/mesh/skinnedMeshRenderer.md) + - [SkinnedMeshBatchRenderer 组件](module-map/mesh/skinnedMeshBatchRenderer.md) + - [从第三方工具导出模型资源](asset/model/dcc-export-mesh.md) + - [从 3ds Max 中导出 FBX 模型资源](asset/model/max-export-fbx.md) + - [从 Maya 中导出 FBX 模型资源](asset/model/maya-export-fbx.md) + - [glTF 模型](asset/model/glTF.md) + - [程序化创建网格](asset/model/scripting-mesh.md) + - [顶点动画贴图 (VAT)](asset/model/vat.md) + - [纹理](module-map/texture/index.md) + - [纹理贴图](asset/texture.md) + - [压缩纹理](asset/compress-texture.md) + - [立方体贴图](asset/texture-cube.md) + - [渲染纹理](asset/render-texture.md) + - [材质系统](material-system/overview.md) + - [在脚本中使用材质](material-system/material-script.md) + - [内置材质](material-system/builtin-material.md) + - [材质系统类图](material-system/material-structure.md) + - [着色器](shader/index.md) + - [创建与使用](shader/effect-inspector.md) + - [内置着色器](shader/effect-builtin.md) + - [基于物理的光照模型 PBR](shader/effect-builtin-pbr.md) + - [卡通渲染](shader/effect-builtin-toon.md) + - [无光照](shader/effect-builtin-unlit.md) + - [着色器语法](shader/effect-syntax.md) + - [Pass 可选配置参数](shader/pass-parameter-list.md) + - [YAML 101 语法简介](shader/yaml-101.md) + - [GLSL 语法简介](shader/glsl.md) + - [预处理宏定义](shader/macros.md) + - [着色器片段(Chunk)](shader/effect-chunk-index.md) + - [内置全局 Uniform](shader/uniform.md) + - [公共函数库](shader/common-functions.md) + - [前向渲染与延迟渲染 Shader 执行流程](shader/forward-and-deferred.md) + - [表面着色器 - Surface Shader](shader/surface-shader.md) + - [内置 Surface Shader 导读](shader/surface-shader/builtin-surface-shader.md) + - [Surface Shader 基本结构](shader/surface-shader/surface-shader-structure.md) + - [Surface Shader 执行流程](shader/surface-shader/shader-code-flow.md) + - [include 机制](shader/surface-shader/includes.md) + - [宏定义与重映射](shader/surface-shader/macro-remapping.md) + - [使用宏定义实现函数替换](shader/surface-shader/function-replace.md) + - [可替换的内置函数](shader/surface-shader/surface-function.md) + - [渲染用途](shader/surface-shader/render-usage.md) + - [光照模型](shader/surface-shader/lighting-mode.md) + - [表面材质数据结构](shader/surface-shader/surface-data-struct.md) + - [着色器类别](shader/surface-shader/shader-stage.md) + - [组装器](shader/surface-shader/shader-assembly.md) + - [VS 输入](shader/surface-shader/vs-input.md) + - [FS 输入](shader/surface-shader/fs-input.md) + - [自定义 Surface Shader](shader/surface-shader/customize-surface-shader.md) + - [渲染调式功能](shader/surface-shader/rendering-debug-view.md) + - [传统着色器 - Legacy Shader](shader/legacy-shader/legacy-shader.md) + - [内置 Legacy Shader 导读](shader/legacy-shader/legacy-shader-builtins.md) + - [Legacy Shader 主要函数与结构体](shader/legacy-shader/legacy-shader-func-struct.md) + - [自定义着色器](shader/write-effect-overview.md) + - [2D 精灵着色器:Gradient](shader/write-effect-2d-sprite-gradient.md) + - [3D 着色器:RimLight](shader/write-effect-3d-rim-light.md) + - [皮肤材质](shader/advanced-shader/skin.md) + - [自定义几何体实例化属性](shader/instanced-attributes.md) + - [UBO 内存布局策略](shader/ubo-layout.md) + - [WebGL 1.0 向下兼容支持](shader/webgl-100-fallback.md) + - [VSCode 着色器插件](shader/vscode-plugin.md) + - [计算着色器](shader/compute-shader.md) + + - [2D 渲染排序](engine/rendering/sorting-2d.md) + - [3D 渲染排序](engine/rendering/sorting.md) + - [特效组件](module-map/effects/index.md) + - [广告牌](particle-system/billboard-component.md) + - [线段组件](particle-system/line-component.md) + - [天空盒](concepts/scene/skybox.md) + - [全局雾](concepts/scene/fog.md) + - [几何渲染器](geometry-renderer/index.md) + - [调试渲染器(Debug-Renderer)](debug-renderer/index.md) + +- [2D 对象](2d-object/index.md) + - [2D 渲染](2d-object/2d-render/index.md) + - [渲染排序规则](ui-system/components/engine/priority.md) + - [2D 渲染组件合批说明](ui-system/components/engine/ui-batch.md) + - [2D 渲染对象自定义材质](ui-system/components/engine/ui-material.md) + - [2D 渲染组件](ui-system/components/editor/render-component.md) + - [Sprite 组件参考](ui-system/components/editor/sprite.md) + - [Label 组件参考](ui-system/components/editor/label.md) + - [Mask 组件参考](ui-system/components/editor/mask.md) + - [Graphics 组件参考](ui-system/components/editor/graphics.md) + - [RichText 组件参考](ui-system/components/editor/richtext.md) + - [UIStaticBatch 组件参考](ui-system/components/editor/ui-static.md) + - [Spine Skeleton 组件参考](editor/components/spine.md) + - [DragonBones ArmatureDisplay 组件参考](editor/components/dragonbones.md) + - [TiledMap 组件参考](editor/components/tiledmap.md) + - [TiledTile 组件参考](editor/components/tiledtile.md) + - [MotionStreak 组件参考](editor/components/motion-streak.md) + - [UI 系统](2d-object/ui-system/index.md) + - [UI 组件](ui-system/components/editor/base-component.md) + - [Canvas 组件参考](ui-system/components/editor/canvas.md) + - [UITransform 组件参考](ui-system/components/editor/ui-transform.md) + - [Widget 组件参考](ui-system/components/editor/widget.md) + - [Button 组件参考](ui-system/components/editor/button.md) + - [Layout 组件参考](ui-system/components/editor/layout.md) + - [EditBox 组件参考](ui-system/components/editor/editbox.md) + - [ScrollView 组件参考](ui-system/components/editor/scrollview.md) + - [ScrollBar 组件参考](ui-system/components/editor/scrollbar.md) + - [ProgressBar 组件参考](ui-system/components/editor/progress.md) + - [LabelOutline 组件参考](ui-system/components/editor/label-outline.md) + - [LabelShadow 组件参考](ui-system/components/editor/label-shadow.md) + - [Toggle 组件参考](ui-system/components/editor/toggle.md) + - [ToggleContainer 组件参考](ui-system/components/editor/toggleContainer.md) + - [Slider 组件参考](ui-system/components/editor/slider.md) + - [PageView 组件参考](ui-system/components/editor/pageview.md) + - [PageViewIndicator 组件参考](ui-system/components/editor/pageviewindicator.md) + - [UIMeshRenderer 组件参考](ui-system/components/editor/ui-model.md) + - [UICoordinateTracker 组件参考](ui-system/components/editor/ui-coordinate-tracker.md) + - [UIOpacity 组件参考](ui-system/components/editor/ui-opacity.md) + - [UISkew 组件参考](ui-system/components/editor/ui-skew.md) + - [BlockInputEvents 组件参考](ui-system/components/editor/block-input-events.md) + - [WebView 组件参考](ui-system/components/editor/webview.md) + - [VideoPlayer 组件参考](ui-system/components/editor/videoplayer.md) + - [SafeArea 组件参考](ui-system/components/editor/safearea.md) + - [UI 实践指南](ui-system/components/engine/usage-ui.md) + - [多分辨率适配方案](ui-system/components/engine/multi-resolution.md) + - [对齐策略](ui-system/components/engine/widget-align.md) + - [文字排版](ui-system/components/engine/label-layout.md) + - [自动布局容器](ui-system/components/engine/auto-layout.md) + - [制作动态生成内容的列表](ui-system/components/engine/list-with-data.md) + - [制作可任意拉伸的 UI 图像](ui-system/components/engine/sliced-sprite.md) + - [安卓大屏幕适配](ui-system/components/engine/large-screen.md) + +- [动画系统](animation/index.md) + - [动画剪辑](animation/animation-clip.md) + - [动画组件参考](animation/animation-comp.md) + - [使用动画编辑器](animation/animation.md) + - [创建 Animation 组件和动画剪辑](animation/animation-create.md) + - [动画编辑器面板介绍](animation/animation-editor.md) + - [编辑动画剪辑](animation/edit-animation-clip.md) + - [关键帧编辑视图](animation/animation-keyFrames.md) + - [曲线编辑视图](animation/animation-curve.md) + - [曲线编辑器](animation/curve-editor.md) + - [辅助曲线编辑视图](animation/animation-auxiliary-curve.md) + - [添加动画事件](animation/animation-event.md) + - [程序化编辑动画剪辑](animation/use-animation-curve.md) + - [骨骼动画](animation/skeletal-animation.md) + - [骨骼贴图布局设置](animation/joint-texture-layout.md) + - [程序化控制动画](animation/animation-component.md) + - [动画状态](animation/animation-state.md) + - [变形动画](animation/morph.md) + - [嵌入播放器](animation/embedded-player.md) + - [Marionette 动画系统](animation/marionette/index.md) + - [动画图资源](animation/marionette/animation-graph.md) + - [动画控制器组件参考](animation/marionette/animation-controller.md) + - [动画图面板](animation/marionette/animation-graph-panel.md) + - [动画图层级](animation/marionette/animation-graph-layer.md) + - [动画状态机](animation/marionette/animation-graph-basics.md) + - [状态过渡](animation/marionette/state-transition.md) + - [动画遮罩资源](animation/marionette/animation-mask.md) + - [动画图变体](animation/marionette/animation-variant.md) + - [程序式动画](animation/marionette/procedural-animation/index.md) + - [程序式动画](animation/marionette/procedural-animation/introduce.md) + - [启用程序式动画功能](animation/marionette/procedural-animation/enabling.md) + - [姿态图](animation/marionette/procedural-animation/pose-graph/index.md) + - [姿态图节点视图](animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md) + - [姿态结点](animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md) + - [混合姿态](animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md) + - [修改姿态](animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md) + - [播放或采样动画](animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md) + +- [音频系统](audio-system/overview.md) + - [AudioSource 组件参考](audio-system/audiosource.md) + - [全局音频管理器示例](audio-system/audioExample.md) + - [兼容性说明](audio-system/audioLimit.md) + +- [物理系统](physics/index.md) + - [2D 物理](physics-2d/physics-2d.md) + - [2D 物理系统](physics-2d/physics-2d-system.md) + - [2D 刚体组件](physics-2d/physics-2d-rigid-body.md) + - [2D 碰撞体](physics-2d/physics-2d-collider.md) + - [2D 碰撞回调](physics-2d/physics-2d-contact-callback.md) + - [2D 物理关节](physics-2d/physics-2d-joint.md) + - [3D 物理系统](physics/physics.md) + - [设置物理引擎](physics/physics-engine.md) + - [物理系统配置](physics/physics-configs.md) + - [分组和掩码](physics/physics-group-mask.md) + - [物理组件](physics/physics-component.md) + - [碰撞体](physics/physics-collider.md) + - [刚体](physics/physics-rigidbody.md) + - [恒力组件](physics/physics-constantForce.md) + - [约束](physics/physics-constraint.md) + - [物理材质](physics/physics-material.md) + - [物理事件](physics/physics-event.md) + - [射线检测](physics/physics-raycast.md) + - [几何投射检测](physics/physics-sweep.md) + - [连续碰撞检测](physics/physics-ccd.md) + - [角色控制器](physics/character-controller/index.md) + - [物理应用案例](physics/physics-example.md) + +- [粒子系统](particle-system/index.md) + - [2D 粒子](particle-system/2d-particle/2d-particle.md) + - [3D 粒子](particle-system/overview.md) + - [粒子系统模块](particle-system/module.md) + - [主模块](particle-system/main-module.md) + - [发射器模块](particle-system/emitter.md) + - [速度模块](particle-system/velocity-module.md) + - [加速度模块](particle-system/force-module.md) + - [大小模块](particle-system/size-module.md) + - [旋转模块](particle-system/rotation-module.md) + - [颜色模块](particle-system/color-module.md) + - [贴图动画模块](particle-system/texture-animation-module.md) + - [限速模块](particle-system/limit-velocity-module.md) + - [拖尾模块](particle-system/trail-module.md) + - [渲染模块](particle-system/renderer.md) + - [粒子属性编辑](particle-system/editor/index.md) + - [控制面板](particle-system/editor/particle-effect-panel.md) + - [粒子曲线编辑器](particle-system/editor/curve-editor.md) + - [渐变色编辑器](particle-system/editor/gradient-editor.md) + +- [缓动系统](tween/index.md) + - [缓动接口](tween/tween-interface.md) + - [缓动函数](tween/tween-function.md) + - [缓动示例](tween/tween-example.md) + +- [地形系统](editor/terrain/index.md) + +- [资源管理](asset/asset-manager.md) + - [loader 升级 assetManager 指南](asset/asset-manager-upgrade-guide.md) + - [子包升级 Asset Bundle 指南](asset/subpackage-upgrade-guide.md) + - [资源加载](asset/dynamic-load-resources.md) + - [Asset Bundle](asset/bundle.md) + - [资源释放](asset/release-manager.md) + - [下载与解析](asset/downloader-parser.md) + - [加载与预加载](asset/preload-load.md) + - [缓存管理器](asset/cache-manager.md) + - [可选参数](asset/options.md) + - [管线与任务](asset/pipeline-task.md) + - [资源管理注意事项 - meta 文件](asset/meta.md) + +- [多语言(L10N)](editor/l10n/overview.md) + - [译文服务商](editor/l10n/translation-service.md) + - [收集并统计](editor/l10n/collect-and-count.md) + - [语言编译](editor/l10n/compile-language.md) + - [L10nLabel 组件](editor/l10n/l10n-label.md) + - [示例](editor/l10n/script-using.md) + - [API](editor/l10n/localization-editor-api.md) + - [EXCEL 导入示例](editor/l10n/example-excel.md) + +- [XR](xr/index.md) + - [版本历史](xr/version-history.md) + - [架构](xr/architecture/index.md) + - [内置资源与预制体](xr/architecture/assets.md) + - [XR 组件](xr/architecture/component.md) + - [预览](xr/architecture/preview.md) + - [XR 视频播放器](xr/architecture/xr-video-player.md) + - [XR 网页浏览器](xr/architecture/xr-webview.md) + - [XR 空间音频](xr/architecture/xr-spatial-audio.md) + - [XR 合成层](xr/architecture/xr-composition-layer.md) + - [透视](xr/architecture/xr-pass-through.md) + - [AR](xr/architecture/ar-introduce.md) + - [AR 相机](xr/architecture/ar-camera.md) + - [AR Manager](xr/architecture/ar-manager.md) + - [AR 自动化行为编辑](xr/architecture/ar-tracking-component.md) + - [AR 交互](xr/architecture/ar-interaction.md) + - [AR 降级](xr/architecture/xr-fallback.md) + + - [快速部署指南](xr/project-deploy/index.md) + - [VR 项目创建](xr/project-deploy/vr-proj-deploy.md) + - [VR 项目构建与发布](xr/project-deploy/vr-proj-pub.md) + - [AR 项目创建](xr/project-deploy/ar-proj-deploy.md) + - [AR 项目构建与发布](xr/project-deploy/ar-proj-pub.md) + - [WebXR 项目配置](xr/project-deploy/webxr-proj-deploy.md) + - [WebXR 项目构建与发布](xr/project-deploy/webxr-proj-pub.md) + +- [原生开发](native/overview.md) + - [原生平台二次开发指南](advanced-topics/native-secondary-development.md) + - [Java 原生反射机制](advanced-topics/java-reflection.md) + - [Objective-C 原生反射机制](advanced-topics/oc-reflection.md) + - [ArkTS 原生反射机制](advanced-topics/arkts-reflection.md) + - [JsbBridge JS 与 JAVA 通信](advanced-topics/js-java-bridge.md) + - [JsbBridge JS 与 Objective-C 通信](advanced-topics/js-oc-bridge.md) + - [JsbBridgeWrapper 基于原生反射机制的事件处理](advanced-topics/jsb-bridge-wrapper.md) + - [JSB 2.0 使用指南](advanced-topics/JSB2.0-learning.md) + - [JSB 手动绑定](advanced-topics/jsb-manual-binding.md) + - [JSB 自动绑定](advanced-topics/jsb-auto-binding.md) + - [Swig](advanced-topics/jsb-swig.md) + - [Swig 示例](advanced-topics/jsb/swig/tutorial/index.md) + - [CMake 使用简介](advanced-topics/cmake-learning.md) + - [原生引擎内存泄漏检测系统](advanced-topics/memory-leak-detector.md) + - [原生场景剔除](advanced-topics/native-scene-culling.md) + - [原生性能剖析器](advanced-topics/profiler.md) + - [原生插件](advanced-topics/native-plugins/brief.md) + - [原生插件创建范例](advanced-topics/native-plugins/tutorial.md) + - [原生引擎跨语言调用优化](advanced-topics/jsb-optimizations.md) + +## 进阶教程 + +- [扩展编辑器](editor/extension/readme.md) + - [扩展管理器面板](editor/extension/extension-manager.md) + - [扩展模板与编译构建](editor/extension/create-extension.md) + - [入门示例-菜单](editor/extension/first.md) + - [入门示例-面板](editor/extension/first-panel.md) + - [入门示例-扩展间通信](editor/extension/first-communication.md) + - [扩展改名](editor/extension/extension-change-name.md) + - [安装与分享](editor/extension/install.md) + - [上架扩展到资源商店](editor/extension/store/upload-store.md) + - [增强已有功能](editor/extension/contributions.md) + - [自定义主菜单](editor/extension/contributions-menu.md) + - [自定义消息](editor/extension/contributions-messages.md) + - [操作当前场景](editor/extension/scene-script.md) + - [增强资源管理器面板](editor/assets/extension.md) + - [自定义资源数据库](editor/extension/contributions-database.md) + - [自定义属性检查器面板](editor/extension/inspector.md) + - [自定义构建流程](editor/publish/custom-build-plugin.md) + - [自定义项目设置面板](editor/extension/contributions-project.md) + - [自定义偏好设置面板](editor/extension/contributions-preferences.md) + - [快捷键](editor/extension/contributions-shortcuts.md) + - [扩展系统详解](editor/extension/basic.md) + - [基础结构](editor/extension/package.md) + - [扩展包定义](editor/extension/define.md) + - [消息系统](editor/extension/messages.md) + - [多语言系统(i18n)](editor/extension/i18n.md) + - [配置系统](editor/extension/profile.md) + - [面板系统](editor/extension/panel.md) + - [UI 组件](editor/extension/ui.md) + - [使用第三方工具](editor/npm.md) + - [Editor 接口说明](editor/extension/editor-api.md) + - [App](editor/extension/api/app.md) + - [Clipboard](editor/extension/api/clipboard.md) + - [Dialog](editor/extension/api/dialog.md) + - [I18n](editor/extension/api/i18n.md) + - [Logger](editor/extension/api/logger.md) + - [Message](editor/extension/api/message.md) + - [Network](editor/extension/api/network.md) + - [Package](editor/extension/api/package.md) + - [Panel](editor/extension/api/panel.md) + - [Profile](editor/extension/api/profile.md) + - [Project](editor/extension/api/project.md) + - [Selection](editor/extension/api/selection.md) + - [Utils](editor/extension/api/utils.md) +- [进阶主题](advanced-topics/index.md) + - [如何向 Cocos 提交代码](submit-pr/submit-pr.md) + - [存储和读取用户数据](advanced-topics/data-storage.md) + - [自定义加载 Wasm/Asm 文件与模块](advanced-topics/wasm-asm-load.md) + - [使用 Emscripten 将原生代码转化为 Wasm/Asm 文件](advanced-topics/wasm-asm-create.md) + - [引擎定制工作流程](advanced-topics/engine-customization.md) + - [网页预览定制工作流程](editor/preview/browser.md) + - [i18n 游戏多语言支持](advanced-topics/i18n.md) + - [动态合图](advanced-topics/dynamic-atlas.md) + - [压缩引擎内部属性](advanced-topics/mangle-properties.md) + - [热更新范例教程](advanced-topics/hot-update.md) + - [热更新管理器](advanced-topics/hot-update-manager.md) + - [HTTP 请求](advanced-topics/http.md) + - [WebSocket](advanced-topics/websocket-introduction.md) + - [WebSocket 客户端](advanced-topics/websocket.md) + - [WebSocket 服务器](advanced-topics/websocket-server.md) diff --git a/versions/4.0/zh/_layouts/website/header.html b/versions/4.0/zh/_layouts/website/header.html new file mode 100644 index 0000000000..f926c03b42 --- /dev/null +++ b/versions/4.0/zh/_layouts/website/header.html @@ -0,0 +1,48 @@ +{% extends template.self %} +{% block book_header %} + + +{% endblock %} \ No newline at end of file diff --git a/versions/4.0/zh/_layouts/website/page.html b/versions/4.0/zh/_layouts/website/page.html new file mode 100644 index 0000000000..8107f43d7b --- /dev/null +++ b/versions/4.0/zh/_layouts/website/page.html @@ -0,0 +1,56 @@ +{% extends template.self %} +{% block book_sidebar %} +
+ {{ super() }} +{% endblock %} +{% block javascript %} + {{ super() }} + +{% endblock %} diff --git a/versions/4.0/zh/advanced-topics/JSB2.0-learning.md b/versions/4.0/zh/advanced-topics/JSB2.0-learning.md new file mode 100644 index 0000000000..49850c9d47 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/JSB2.0-learning.md @@ -0,0 +1,1198 @@ +# JSB 2.0 绑定教程 + +## 抽象层 + +### 架构 + +![Architecture](jsb/JSB2.0-Architecture.png) + +### 宏(Macro) + +抽象层必然会比直接使用 JS 引擎 API 的方式多占用一些 CPU 执行时间,如何把抽象层本身的开销降到最低成为设计的第一目标。 + +JS 绑定的大部分工作其实就是设定 JS 相关操作的 CPP 回调,在回调函数中关联 CPP 对象。其实主要包含如下两种类型: + +- 注册 JS 函数(包含全局函数,类构造函数、类析构函数、类成员函数,类静态成员函数),绑定一个 CPP 回调 +- 注册 JS 对象的属性读写访问器,分别绑定读与写的 CPP 回调 + +如何做到抽象层开销最小而且暴露统一的 API 供上层使用? + +以注册 JS 函数的回调定义为例,JavaScriptCore、SpiderMonkey、V8、ChakraCore 的定义各不相同,具体如下: + +- JavaScriptCore + + ```c++ + JSValueRef JSB_foo_func( + JSContextRef _cx, + JSObjectRef _function, + JSObjectRef _thisObject, + size_t argc, + const JSValueRef _argv[], + JSValueRef* _exception + ); + ``` + +- SpiderMonkey + + ```c++ + bool JSB_foo_func( + JSContext* _cx, + unsigned argc, + JS::Value* _vp + ); + ``` + +- V8 + + ```c++ + void JSB_foo_func( + const v8::FunctionCallbackInfo& v8args + ); + ``` + +- ChakraCore + + ```c++ + JsValueRef JSB_foo_func( + JsValueRef _callee, + bool _isConstructCall, + JsValueRef* _argv, + unsigned short argc, + void* _callbackState + ); + ``` + +我们评估了几种方案,最终确定使用 `宏` 来抹平不同 JS 引擎回调函数定义与参数类型的不同,不管底层是使用什么引擎,开发者统一使用一种回调函数的定义。我们借鉴了 lua 的回调函数定义方式,抽象层所有的 JS 到 CPP 的回调函数的定义为: + +```c++ +bool foo(se::State& s) +{ + ... + ... +} +SE_BIND_FUNC(foo) // 此处以回调函数的定义为例 +``` + +开发者编写完回调函数后,记住使用 `SE_BIND_XXX` 系列的宏对回调函数进行包装。目前提供了如下几个宏: + +- **SE_BIND_PROP_GET**:包装一个 JS 对象属性读取的回调函数 +- **SE_BIND_PROP_SET**:包装一个 JS 对象属性写入的回调函数 +- **SE_BIND_FUNC_AS_PROP_GET**: 普通函数转化为读取属性的回调 +- **SE_BIND_FUNC_AS_PROP_SET**: 普通函数转化为写入属性的回调 +- **SE_BIND_FUNC**:包装一个 JS 函数,可用于全局函数、类成员函数、类静态函数 +- **SE_BIND_FUNC_FAST**: 包装无参的 JS 函数, 相比 `SE_BIND_FUNC` 更高效 +- **SE_DECLARE_FUNC**:声明一个 JS 函数,一般在 `.h` 头文件中使用 +- **SE_BIND_CTOR**:包装一个 JS 构造函数 +- **SE_BIND_SUB_CLS_CTOR**:包装一个 JS 子类的构造函数,此子类可以继承 +- **SE_BIND_FINALIZE_FUNC**:包装一个 JS 对象被 GC 回收后的回调函数 +- **SE_DECLARE_FINALIZE_FUNC**:声明一个 JS 对象被 GC 回收后的回调函数 +- **_SE**:包装回调函数的名称,转义为每个 JS 引擎能够识别的回调函数的定义 + > **注意**:第一个字符为下划线,类似 Windows 下用的 `_T("xxx")` 来包装 Unicode 或者 MultiBytes 字符串。 + +## API + +### CPP 命名空间(namespace) + +CPP 抽象层所有的类型都在 `se` 命名空间下,其为 ScriptEngine 的缩写。 + +### 类型 + +#### se::ScriptEngine + +`se::ScriptEngine` 为 JS 引擎的管理员,掌管 JS 引擎初始化、销毁、重启、Native 模块注册、加载脚本、强制垃圾回收、JS 异常清理、是否启用调试器。它是一个单例,可通过 `se::ScriptEngine::getInstance()` 得到对应的实例。 + +#### se::Value + +`se::Value` 可以被理解为 JS 变量在 CPP 层的引用。JS 变量有 `object`、`number`、 `bigint`, `string`、`boolean`、`null` 和 `undefined` 六种类型。因此 `se::Value` 使用 `union` 包含 `object`、`number`、`string`、`boolean` 5 种 **有值类型**。**无值类型** 包含 `null` 和 `undefined`,可由 `_type` 直接表示。 + +```c++ +namespace se { + class Value { + enum class Type : char + { + Undefined = 0, + Null, + Number, + Boolean, + String, + Object, + BigInt, // 多用于存储指针类型 + }; + ... + ... + private: + union { + bool _boolean; + double _number; + std::string* _string; + Object* _object; + int64_t _bigint; + } _u; + + Type _type; + ... + ... + }; +} +``` + +如果 `se::Value` 中保存基础数据类型,比如 `number`,`string`,`boolean`,其内部是直接存储一份值副本。
+`object` 的存储比较特殊,是通过 `se::Object*` 对 JS 对象的弱引用 (weak reference)。 + +#### se::Object + +`se::Object` 继承于 `se::RefCounter` 引用计数管理类。目前抽象层中只有 `se::Object` 继承于 `se::RefCounter`。 + +上一小节我们说到,`se::Object` 是保存了对 JS 对象的弱引用,这里有必要解释一下为什么是弱引用。 + +**原因一:JS 对象控制 CPP 对象的生命周期的需要** + +当在脚本层中通过 `var xhr = new XMLHttpRequest();` 创建了一个 XMLHttpRequest 后,在构造回调函数绑定中我们会创建一个 `se::Object` 并保留在一个全局的 `map (NativePtrToObjectMap)` 中,此 map 用于查询 `XMLHttpRequest*` 指针获取对应的 JS 对象 `se::Object*`。 + +```c++ +/// native/cocos/bindings/manual/jsb_xmlhttprequest.cpp +static bool XMLHttpRequest_constructor(se::State& s) +{ + XMLHttpRequest* cobj = JSB_ALLOC(XMLHttpRequest); + s.thisObject()->setPrivateData(cobj); + // ... + return true; +} +SE_BIND_CTOR(XMLHttpRequest_constructor, __jsb_XMLHttpRequest_class, XMLHttpRequest_finalize) + +/// native/cocos/bindings/jswrapper/v8/Object.cpp +void Object::setPrivateObject(PrivateObjectBase *data) { + // ... + if (data != nullptr) { + _privateData = data->getRaw(); + NativePtrToObjectMap::emplace(_privateData, this); + } else { + _privateData = nullptr; + } +} +``` + +设想如果强制要求 `se::Object` 为 JS 对象的强引用(strong reference),即让 JS 对象不受 GC 控制,由于 `se::Object` 一直存在于 map 中,finalizer 回调将永远无法被触发,从而导致内存泄露。 + + +**原因二:更加灵活,手动调用 root 方法以支持强引用** + +`se::Object` 中提供了 `root`/`unroot` 方法供开发者调用,`root` 会把 JS 对象放入到不受 GC 扫描到的区域,调用 `root` 后,`se::Object` 就强引用了 JS 对象,只有当 `unroot` 被调用,或者 `se::Object` 被释放后,JS 对象才会放回到受 GC 扫描到的区域。 + +一般情况下,如果对象是非 `cc::Ref` 的子类,会采用 CPP 对象控制 JS 对象的生命周期的方式去绑定。引擎内 Spine, DragonBones, Box2d 等第三方库的绑定就是采用此方式。当 CPP 对象被释放的时候,需要在 `NativePtrToObjectMap` 中查找对应的 `se::Object`,然后手动 `unroot` 和 `decRef`。以 Spine 中 `spTrackEntry` 的绑定为例: + +```c++ +spTrackEntry_setDisposeCallback([](spTrackEntry* entry) { + // spTrackEntry 的销毁回调 + se::Object* seObj = nullptr; + + auto iter = se::NativePtrToObjectMap::find(entry); + if (iter != se::NativePtrToObjectMap::end()) { + // 保存 se::Object 指针,用于在下面的 cleanup 函数中释放其内存 + seObj = iter->second; + // Native 对象 entry 的内存已经被释放,因此需要立马解除 Native 对象与 JS 对象的关联。 + // 如果解除引用关系放在下面的 cleanup 函数中处理,有可能触发 se::Object::setPrivateData 中 + // 的断言,因为新生成的 Native 对象的地址可能与当前对象相同,而 cleanup 可能被延迟到帧结束前执行。 + se::NativePtrToObjectMap::erase(iter); + } else { + return; + } + + auto cleanup = [seObj]() { + + auto se = se::ScriptEngine::getInstance(); + if (!se->isValid() || se->isInCleanup()) + return; + + se::AutoHandleScope hs; + se->clearException(); + + // 由于上面逻辑已经把映射关系解除了,这里传入 false 表示不用再次解除映射关系, + // 因为当前 seObj 的 private data 可能已经是另外一个不同的对象 + seObj->clearPrivateData(false); + seObj->unroot(); // unroot,使 JS 对象受 GC 管理 + seObj->decRef(); // 释放 se::Object + }; + + // 确保不再垃圾回收中去操作 JS 引擎的 API + if (!se::ScriptEngine::getInstance()->isGarbageCollecting()) { + cleanup(); + } else { + // 如果在垃圾回收,把清理任务放在帧结束中进行 + CleanupTask::pushTaskToAutoReleasePool(cleanup); + } +}); +``` + +**C++ 对象的生命周期管理** + +在 3.6 之前, 析构回调 `_finalize` 会根据对象类型和是否存在于`se::NonRefNativePtrCreatedByCtorMap` 来决定调用 `delete` 或者 `release` 以释放对应的 C++ 对象. 3.6 开始 `_finalize` 回调被弃用, 暂时为方便调试保留为空函数. `se::Object` 通过 `se::PrivateObjectBase` 对象和 C++ 对象建立生命周期的关联. `se::PrivateObjectBase` 的三个子类对应了不同的释放策略. + +- `se::CCSharedPtrPrivateObject` + +使用 `cc::IntrusivePtr` 存储 C++ 对象的指针, 要求 C++ 类继承 `cc::RefCounted`. 其中 `cc::IntrusivePtr` 为智能指针类型, 会自动增减 `cc::RefCounted` 的引用计数. 当引用计数为 0 时, 触发析构. + +- `se::SharedPrivateObject` + +使用 `std::shared_ptr` 存储 C++ 对象指针, 要求 C++ 类**不继承** `cc::RefCounted`. 由于 `shared_ptr` 本身的特性, 要求所有强引用都 `shared_ptr`. 所有 `shared_ptr` 都销毁时触发 C++ 对象的析构. + +- `se::RawRefPrivateObject` + +使用裸指针, 默认为对 C++ 对象的弱引用. 可通过调用 `tryAllowDestroyInGC` 转为强引用. 作为弱引用时, GC 不触发对象的析构. + +**关联原生对象** + +3.6 之后 `se::Object::setPrivateData(void *)` 扩展成了: +```c++ +template +inline void setPrivateData(T *data); +``` +能自动根据类型信息创建 `SharedPrivateObject` 或者 `CCSharedPtrPrivateObject`, 但是不支持 `RawRefPrivateObject`. + +我们可以使用 `setPrivateObject` 显示指定 `PrivateObject` 的类型: +```c++ +// se::SharedPrivateObject +obj->setPrivateObject(se::shared_private_object(v)); + +// se::CCSharedPtrPrivateObject +obj->setPrivateObject(se::ccshared_private_object(v)); + +// se::RawRefPrivateObject +obj->setPrivateObject(se::rawref_private_object(v)); +``` + + + +**对象类型** + +绑定对象的创建已经被隐藏在对应的 `SE_BIND_CTOR` 和 `SE_BIND_SUB_CLS_CTOR` 函数中,开发者在绑定回调中如果需要用到当前对象对应的 `se::Object`,只需要通过 `s.thisObject()` 即可获取。其中 s 为 `se::State` 类型,具体会在后续章节中说明。 + +此外,`se::Object` 目前支持以下几种对象的手动创建: + +- Plain Object:通过 `se::Object::createPlainObject` 创建,类似 JS 中的 `var a = {};` +- Array Object:通过 `se::Object::createArrayObject` 创建,类似 JS 中的 `var a = [];` +- Uint8 Typed Array Object:通过 `se::Object::createTypedArray` 创建,类似 JS 中的 `var a = new Uint8Array(buffer);` +- Array Buffer Object:通过 `se::Object::createArrayBufferObject`,类似 JS 中的 `var a = new ArrayBuffer(len);` + +**手动创建对象的释放** + +`se::Object::createXXX` 方法与 Cocos Creator 中的 `create` 方法不同,抽象层是完全独立的一个模块,并不依赖与 Cocos Creator 的 autorelease 机制。虽然 `se::Object` 也是继承引用计数类,但开发者需要处理 **手动创建出来的对象** 的释放。 + +```c++ +se::Object* obj = se::Object::createPlainObject(); +... +... +obj->decRef(); // 释放引用,避免内存泄露 +``` + +#### se::HandleObject(推荐的管理手动创建对象的辅助类) + +- 在比较复杂的逻辑中使用手动创建对象,开发者往往会忘记在不同的逻辑中处理 `decRef` + + ```c++ + bool foo() + { + se::Object* obj = se::Object::createPlainObject(); + if (var1) + return false; // 这里直接返回了,忘记做 decRef 释放操作 + + if (var2) + return false; // 这里直接返回了,忘记做 decRef 释放操作 + ... + ... + obj->decRef(); + return true; + } + ``` + + 就算在不同的返回条件分支中加上了 decRef 也会导致逻辑复杂,难以维护,如果后期加入另外一个返回分支,很容易忘记 decRef。 + +- JS 引擎在 `se::Object::createXXX` 后,如果由于某种原因 JS 引擎做了 GC 操作,导致后续使用的 `se::Object` 内部引用了一个非法指针,引发程序崩溃 + +为了解决上述两个问题,抽象层定义了一个辅助管理 **手动创建对象** 的类型,即 `se::HandleObject`。 + +`se::HandleObject` 是一个辅助类,用于更加简单地管理手动创建的 `se::Object` 对象的释放、root 和 unroot 操作。 + +以下两种代码写法是等价的,使用 `se::HandleObject` 的代码量明显少很多,而且更加安全。 + +```c++ +{ + se::HandleObject obj(se::Object::createPlainObject()); + obj->setProperty(...); + otherObject->setProperty("foo", se::Value(obj)); +} +``` + +等价于: + +```c++ +{ + se::Object* obj = se::Object::createPlainObject(); + obj->root(); // 在手动创建完对象后立马 root,防止对象被 GC + + obj->setProperty(...); + otherObject->setProperty("foo", se::Value(obj)); + + obj->unroot(); // 当对象被使用完后,调用 unroot + obj->decRef(); // 引用计数减一,避免内存泄露 +} +``` + +> **注意**: +> +> 1. 不要尝试使用 `se::HandleObject` 创建一个 native 与 JS 的绑定对象,在 JS 控制 CPP 的模式中,绑定对象的释放会被抽象层自动处理,在 CPP 控制 JS 的模式中,前一章节中已经有描述了。 +> 2. `se::HandleObject` 对象只能够在栈上被分配,而且栈上构造的时候必须传入一个 `se::Object` 指针。 + +#### se::Class + +`se::Class` 用于暴露 CPP 类到 JS 中,它会在 JS 中创建一个对应名称的 `constructor function`。 + +它有如下方法: + +- `static se::Class* create(className, obj, parentProto, ctor)`:创建一个 Class,注册成功后,在 JS 层中可以通过`var xxx = new SomeClass();`的方式创建一个对象 +- `bool defineFunction(name, func)`:定义 Class 中的成员函数 +- `bool defineProperty(name, getter, setter)`:定义 Class 属性读写器 +- `bool defineStaticFunction(name, func)`:定义 Class 的静态成员函数,可通过 `SomeClass.foo()` 这种非 new 的方式访问,与类实例对象无关 +- `bool defineStaticProperty(name, getter, setter)`:定义 Class 的静态属性读写器,可通过 `SomeClass.propertyA` 直接读写,与类实例对象无关 +- `bool defineFinalizeFunction(func)`:定义 JS 对象被 GC 后的 CPP 回调 +- `bool install()`:注册此类到 JS 虚拟机中 +- `Object* getProto()`:获取注册到 JS 中的类(其实是 JS 的 constructor)的 prototype 对象,类似 `function Foo(){}` 的 `Foo.prototype` +- `const char* getName() const`:获取当前 Class 的名称 + +> **注意**:Class 类型创建后,不需要手动释放内存,它会被封装层自动处理。 + +更具体 API 说明可以翻看 API 文档或者代码注释。 + +#### se::AutoHandleScope + +`se::AutoHandleScope` 对象类型完全是为了解决 V8 的兼容问题而引入的概念。V8 中,当有 CPP 函数中需要触发 JS 相关操作,比如调用 JS 函数,访问 JS 属性等任何调用 `v8::Local<>` 的操作,V8 强制要求在调用这些操作前必须存在一个 `v8::HandleScope` 作用域,否则会引发程序崩溃。 + +因此抽象层中引入了 `se::AutoHandleScope` 的概念,其只在 V8 上有实现,其他 JS 引擎目前都只是空实现。 + +开发者需要记住,在任何代码执行中,需要调用 JS 的逻辑前,声明一个 `se::AutoHandleScope` 即可,比如: + +```c++ +class SomeClass { + void update(float dt) { + se::ScriptEngine::getInstance()->clearException(); + se::AutoHandleScope hs; + + se::Object* obj = ...; + obj->setProperty(...); + ... + ... + obj->call(...); + } +}; +``` + +#### se::State + +之前章节我们有提及 State 类型,它是绑定回调中的一个环境,我们通过 `se::State` 可以取得当前的 CPP 指针、`se::Object` 对象指针、参数列表、返回值引用。 + +```c++ +bool foo(se::State& s) +{ + // 获取 native 对象指针 + SomeClass* cobj = (SomeClass*)s.nativeThisObject(); + // 获取 se::Object 对象指针 + se::Object* thisObject = s.thisObject(); + // 获取参数列表 + const se::ValueArray& args = s.args(); + // 设置返回值 + s.rval().setInt32(100); + return true; +} +SE_BIND_FUNC(foo) +``` + +## 抽象层依赖 Cocos Creator 引擎么? + +不依赖。 + +ScriptEngine 这层设计之初就将其定义为一个独立模块,完全不依赖 Cocos Creator 引擎。开发者可以通过 copy、paste 把 `cocos/bindings/jswrapper` 下的所有抽象层源码拷贝到其他项目中直接使用。 + +## 手动绑定 + +### 回调函数声明 + +```c++ +static bool Foo_balabala(se::State& s) +{ + const auto& args = s.args(); + int argc = (int)args.size(); + + if (argc >= 2) // 这里约定参数个数必须大于等于 2,否则抛出错误到 JS 层且返回 false + { + ... + ... + return true; + } + + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 2); + return false; +} + +// 如果是绑定函数,则用 SE_BIND_FUNC,构造函数、析构函数、子类构造函数等类似 +SE_BIND_FUNC(Foo_balabala) +``` + +### 为 JS 对象设置一个属性值 + +```c++ +se::Object* globalObj = se::ScriptEngine::getInstance()->getGlobalObject(); // 这里为了演示方便,获取全局对象 +globalObj->setProperty("foo", se::Value(100)); // 给全局对象设置一个 foo 属性,值为 100 +``` + +在 JS 中就可以直接使用 foo 这个全局变量了 + +```js +log("foo value: " + foo); // 打印出 foo value: 100 +``` + +### 为 JS 对象定义一个属性读写回调 + +```c++ +// 全局对象的 foo 属性的读回调 +static bool Global_get_foo(se::State& s) +{ + NativeObj* cobj = (NativeObj*)s.nativeThisObject(); + int32_t ret = cobj->getValue(); + s.rval().setInt32(ret); + return true; +} +SE_BIND_PROP_GET(Global_get_foo) + +// 全局对象的 foo 属性的写回调 +static bool Global_set_foo(se::State& s) +{ + const auto& args = s.args(); + int argc = (int)args.size(); + if (argc >= 1) + { + NativeObj* cobj = (NativeObj*)s.nativeThisObject(); + int32_t arg1 = args[0].toInt32(); + cobj->setValue(arg1); + // void 类型的函数,无需设置 s.rval,未设置默认返回 undefined 给 JS 层 + return true; + } + + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 1); + return false; +} +SE_BIND_PROP_SET(Global_set_foo) + +void some_func() +{ + se::Object* globalObj = se::ScriptEngine::getInstance()->getGlobalObject(); // 这里为了演示方便,获取全局对象 + globalObj->defineProperty("foo", _SE(Global_get_foo), _SE(Global_set_foo)); // 使用_SE 宏包装一下具体的函数名称 +} +``` + +### 为 JS 对象设置一个函数 + +```c++ +static bool Foo_function(se::State& s) +{ + ... + ... +} +SE_BIND_FUNC(Foo_function) + +void some_func() +{ + se::Object* globalObj = se::ScriptEngine::getInstance()->getGlobalObject(); // 这里为了演示方便,获取全局对象 + globalObj->defineFunction("foo", _SE(Foo_function)); // 使用_SE 宏包装一下具体的函数名称 +} +``` + +### 注册一个 CPP 类到 JS 虚拟机中 + +```c++ +static se::Object* __jsb_ns_SomeClass_proto = nullptr; +static se::Class* __jsb_ns_SomeClass_class = nullptr; + +namespace ns { + class SomeClass + { + public: + SomeClass() + : xxx(0) + {} + + void foo() { + printf("SomeClass::foo\n"); + + Director::getInstance()->getScheduler()->schedule([this](float dt){ + static int counter = 0; + ++counter; + if (_cb != nullptr) + _cb(counter); + }, this, 1.0f, CC_REPEAT_FOREVER, 0.0f, false, "iamkey"); + } + + static void static_func() { + printf("SomeClass::static_func\n"); + } + + void setCallback(const std::function& cb) { + _cb = cb; + if (_cb != nullptr) + { + printf("setCallback(cb)\n"); + } + else + { + printf("setCallback(nullptr)\n"); + } + } + + int xxx; + private: + std::function _cb; + }; +} // namespace ns { + +static bool js_SomeClass_finalize(se::State& s) +{ + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + delete cobj; + return true; +} +SE_BIND_FINALIZE_FUNC(js_SomeClass_finalize) + +static bool js_SomeClass_constructor(se::State& s) +{ + ns::SomeClass* cobj = new ns::SomeClass(); + s.thisObject()->setPrivateData(cobj); + return true; +} +SE_BIND_CTOR(js_SomeClass_constructor, __jsb_ns_SomeClass_class, js_SomeClass_finalize) + +static bool js_SomeClass_foo(se::State& s) +{ + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + cobj->foo(); + return true; +} +SE_BIND_FUNC(js_SomeClass_foo) + +static bool js_SomeClass_get_xxx(se::State& s) +{ + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + s.rval().setInt32(cobj->xxx); + return true; +} +SE_BIND_PROP_GET(js_SomeClass_get_xxx) + +static bool js_SomeClass_set_xxx(se::State& s) +{ + const auto& args = s.args(); + int argc = (int)args.size(); + if (argc > 0) + { + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + cobj->xxx = args[0].toInt32(); + return true; + } + + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 1); + return false; +} +SE_BIND_PROP_SET(js_SomeClass_set_xxx) + +static bool js_SomeClass_static_func(se::State& s) +{ + ns::SomeClass::static_func(); + return true; +} +SE_BIND_FUNC(js_SomeClass_static_func) + +bool js_register_ns_SomeClass(se::Object* global) +{ + // 保证 namespace 对象存在 + se::Value nsVal; + if (!global->getProperty("ns", &nsVal)) + { + // 不存在则创建一个 JS 对象,相当于 var ns = {}; + se::HandleObject jsobj(se::Object::createPlainObject()); + nsVal.setObject(jsobj); + + // 将 ns 对象挂载到 global 对象中,名称为 ns + global->setProperty("ns", nsVal); + } + se::Object* ns = nsVal.toObject(); + + // 创建一个 Class 对象,开发者无需考虑 Class 对象的释放,其交由 ScriptEngine 内部自动处理 + auto cls = se::Class::create("SomeClass", ns, nullptr, _SE(js_SomeClass_constructor)); // 如果无构造函数,最后一个参数可传入 nullptr,则这个类在 JS 中无法被 new SomeClass() 出来 + + // 为这个 Class 对象定义成员函数、属性、静态函数、析构函数 + cls->defineFunction("foo", _SE(js_SomeClass_foo)); + cls->defineProperty("xxx", _SE(js_SomeClass_get_xxx), _SE(js_SomeClass_set_xxx)); + + cls->defineFinalizeFunction(_SE(js_SomeClass_finalize)); + + // 注册类型到 JS VirtualMachine 的操作 + cls->install(); + + // JSBClassType 为 Cocos Creator 引擎绑定层封装的类型注册的辅助函数,此函数不属于 ScriptEngine 这层 + JSBClassType::registerClass(cls); + + // 保存注册的结果,便于其他地方使用,比如类继承 + __jsb_ns_SomeClass_proto = cls->getProto(); + __jsb_ns_SomeClass_class = cls; + + // 为每个此 Class 实例化出来的对象附加一个属性 + __jsb_ns_SomeClass_proto->setProperty("yyy", se::Value("helloyyy")); + + // 注册静态成员变量和静态成员函数 + se::Value ctorVal; + if (ns->getProperty("SomeClass", &ctorVal) && ctorVal.isObject()) + { + ctorVal.toObject()->setProperty("static_val", se::Value(200)); + ctorVal.toObject()->defineFunction("static_func", _SE(js_SomeClass_static_func)); + } + + // 清空异常 + se::ScriptEngine::getInstance()->clearException(); + return true; +} +``` + +### 如何绑定 CPP 接口中的回调函数? + +```c++ +static bool js_SomeClass_setCallback(se::State& s) +{ + const auto& args = s.args(); + int argc = (int)args.size(); + if (argc >= 1) + { + ns::SomeClass* cobj = (ns::SomeClass*)s.nativeThisObject(); + + se::Value jsFunc = args[0]; + se::Value jsTarget = argc > 1 ? args[1] : se::Value::Undefined; + + if (jsFunc.isNullOrUndefined()) + { + cobj->setCallback(nullptr); + } + else + { + assert(jsFunc.isObject() && jsFunc.toObject()->isFunction()); + + se::Object *jsTargetObj = jsTarget.isObject() ? jsTarget.toObject() : nullptr; + + // 如果当前 SomeClass 是可以被 new 出来的类,我们 使用 se::Object::attachObject 把 jsFunc 和 jsTarget 关联到当前对象中 + s.thisObject()->attachObject(jsFunc.toObject()); + if(jsTargetObj) .thisObject()->attachObject(jsTargetObj); + + // 如果当前 SomeClass 类是一个单例类,或者永远只有一个实例的类,我们不能用 se::Object::attachObject 去关联 + // 必须使用 se::Object::root,开发者无需关系 unroot,unroot 的操作会随着 lambda 的销毁触发 jsFunc 的析构,在 se::Object 的析构函数中进行 unroot 操作。 + // js_audio_AudioEngine_setFinishCallback 的绑定代码就是使用此方式,因为 AudioEngine 始终只有一个实例, + // 如果使用 s.thisObject->attachObject(jsFunc.toObject);会导致对应的 func 和 target 永远无法被释放,引发内存泄露。 + + // jsFunc.toObject()->root(); + // jsTarget.toObject()->root(); + + cobj->setCallback([jsFunc, jsTargetObj](int counter){ + + // CPP 回调函数中要传递数据给 JS 或者调用 JS 函数,在回调函数开始需要添加如下两行代码。 + se::ScriptEngine::getInstance()->clearException(); + se::AutoHandleScope hs; + + se::ValueArray args; + args.push_back(se::Value(counter)); + + jsFunc.toObject()->call(args, jsTargetObj); + }); + } + + return true; + } + + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", argc, 1); + return false; +} +SE_BIND_FUNC(js_SomeClass_setCallback) +``` + +SomeClass 类注册后,就可以在 JS 中这样使用了: + +```js + var myObj = new ns.SomeClass(); + myObj.foo(); + ns.SomeClass.static_func(); + log("ns.SomeClass.static_val: " + ns.SomeClass.static_val); + log("Old myObj.xxx:" + myObj.xxx); + myObj.xxx = 1234; + log("New myObj.xxx:" + myObj.xxx); + log("myObj.yyy: " + myObj.yyy); + + var delegateObj = { + onCallback: function(counter) { + log("Delegate obj, onCallback: " + counter + ", this.myVar: " + this.myVar); + this.setVar(); + }, + + setVar: function() { + this.myVar++; + }, + + myVar: 100 + }; + + myObj.setCallback(delegateObj.onCallback, delegateObj); + + setTimeout(function(){ + myObj.setCallback(null); + }, 6000); // 6 秒后清空 callback +``` + +Console 中会输出: + +``` +SomeClass::foo +SomeClass::static_func +ns.SomeClass.static_val: 200 +Old myObj.xxx:0 +New myObj.xxx:1234 +myObj.yyy: helloyyy +setCallback(cb) +Delegate obj, onCallback: 1, this.myVar: 100 +Delegate obj, onCallback: 2, this.myVar: 101 +Delegate obj, onCallback: 3, this.myVar: 102 +Delegate obj, onCallback: 4, this.myVar: 103 +Delegate obj, onCallback: 5, this.myVar: 104 +Delegate obj, onCallback: 6, this.myVar: 105 +setCallback(nullptr) +``` + +### 如何使用 Cocos Creator bindings 这层的类型转换辅助函数? + +类型转换辅助函数位于 `cocos/bindings/manual/jsb_conversions.h` 中,其包含以下内容。 + +#### se::Value 转换为 C++ 类型 + +支持基础类型 `int*t`/`uint*_t`/`float`/`double`/`const char*`/`bool`, `std::string`,绑定类型, 其容器类型 `std::vector`, `std::array`, `std::map`, `std::unordered_map` 等. + + +```c++ +template +bool sevalue_to_native(const se::Value &from, T *to, se::Object *ctx); + +template +bool sevalue_to_native(const se::Value &from, T *to); +``` +#### C++ 类型转换为 se::Value + +```c++ +template +bool nativevalue_to_se(const T &from, se::Value &to, se::Object *ctx); + +template +bool nativevalue_to_se(const T &from, se::Value &to); +``` + +**3.6 之前的 以下这些转换函数已被弃用, 需要改为上面的两组函数** + +```c++ +bool seval_to_int32(const se::Value &v, int32_t *ret); +bool seval_to_uint32(const se::Value &v, uint32_t *ret); +bool seval_to_int8(const se::Value &v, int8_t *ret); +bool seval_to_uint8(const se::Value &v, uint8_t *ret); +bool seval_to_int16(const se::Value &v, int16_t *ret); +bool seval_to_uint16(const se::Value &v, uint16_t *ret); +bool seval_to_boolean(const se::Value &v, bool *ret); +bool seval_to_float(const se::Value &v, float *ret); +bool seval_to_double(const se::Value &v, double *ret); +bool seval_to_size(const se::Value &v, size_t *ret); +bool seval_to_std_string(const se::Value &v, std::string *ret); +bool seval_to_Vec2(const se::Value &v, cc::Vec2 *pt); +bool seval_to_Vec3(const se::Value &v, cc::Vec3 *pt); +bool seval_to_Vec4(const se::Value &v, cc::Vec4 *pt); +bool seval_to_Mat4(const se::Value &v, cc::Mat4 *mat); +bool seval_to_Size(const se::Value &v, cc::Size *size); +bool seval_to_ccvalue(const se::Value &v, cc::Value *ret); +bool seval_to_ccvaluemap(const se::Value &v, cc::ValueMap *ret); +bool seval_to_ccvaluemapintkey(const se::Value &v, cc::ValueMapIntKey *ret); +bool seval_to_ccvaluevector(const se::Value &v, cc::ValueVector *ret); +bool sevals_variadic_to_ccvaluevector(const se::ValueArray &args, cc::ValueVector *ret); +bool seval_to_std_vector_string(const se::Value &v, std::vector *ret); +bool seval_to_std_vector_int(const se::Value &v, std::vector *ret); +bool seval_to_std_vector_uint16(const se::Value &v, std::vector *ret); +bool seval_to_std_vector_float(const se::Value &v, std::vector *ret); +bool seval_to_std_vector_Vec2(const se::Value &v, std::vector *ret); +bool seval_to_Uint8Array(const se::Value &v, uint8_t *ret); +bool seval_to_uintptr_t(const se::Value &v, uintptr_t *ret); +bool seval_to_std_map_string_string(const se::Value &v, std::map *ret); +bool seval_to_Data(const se::Value &v, cc::Data *ret); +bool seval_to_DownloaderHints(const se::Value &v, cc::network::DownloaderHints *ret); +template +bool seval_to_native_ptr(const se::Value& v, T* ret); +template +typename std::enable_if::value && !std::is_same::value, T>::type +seval_to_type(const se::Value &v, bool &ok); +template +typename std::enable_if::value, T>::type +seval_to_type(const se::Value &v, bool &ok); +template +typename std::enable_if::value, T>::type +seval_to_type(const se::Value &v, bool &ok); +template +typename std::enable_if::value, T>::type +seval_to_type(const se::Value &v, bool &ok); + +template +typename std::enable_if::value, T>::type +seval_to_type(const se::Value &v, bool &ok); +template +typename std::enable_if::value && std::is_class::type>::value, bool>::type +seval_to_std_vector(const se::Value &v, std::vector *ret); + +template +typename std::enable_if::value, bool>::type +seval_to_std_vector(const se::Value &v, std::vector *ret); +template +bool seval_to_Map_string_key(const se::Value& v, cc::Map* ret) +``` + +改用 `sevalue_to_native` + +```c++ +bool int8_to_seval(int8_t v, se::Value *ret); +bool uint8_to_seval(uint8_t v, se::Value *ret); +bool int32_to_seval(int32_t v, se::Value *ret); +bool uint32_to_seval(uint32_t v, se::Value *ret); +bool int16_to_seval(uint16_t v, se::Value *ret); +bool uint16_to_seval(uint16_t v, se::Value *ret); +bool boolean_to_seval(bool v, se::Value *ret); +bool float_to_seval(float v, se::Value *ret); +bool double_to_seval(double v, se::Value *ret); +bool long_to_seval(long v, se::Value *ret); +bool ulong_to_seval(unsigned long v, se::Value *ret); +bool longlong_to_seval(long long v, se::Value *ret); +bool uintptr_t_to_seval(uintptr_t v, se::Value *ret); +bool size_to_seval(size_t v, se::Value *ret); +bool std_string_to_seval(const std::string &v, se::Value *ret); +bool Vec2_to_seval(const cc::Vec2 &v, se::Value *ret); +bool Vec3_to_seval(const cc::Vec3 &v, se::Value *ret); +bool Vec4_to_seval(const cc::Vec4 &v, se::Value *ret); +bool Mat4_to_seval(const cc::Mat4 &v, se::Value *ret); +bool Size_to_seval(const cc::Size &v, se::Value *ret); +bool Rect_to_seval(const cc::Rect &v, se::Value *ret); +bool ccvalue_to_seval(const cc::Value &v, se::Value *ret); +bool ccvaluemap_to_seval(const cc::ValueMap &v, se::Value *ret); +bool ccvaluemapintkey_to_seval(const cc::ValueMapIntKey &v, se::Value *ret); +bool ccvaluevector_to_seval(const cc::ValueVector &v, se::Value *ret); +bool std_vector_string_to_seval(const std::vector &v, se::Value *ret); +bool std_vector_int_to_seval(const std::vector &v, se::Value *ret); +bool std_vector_uint16_to_seval(const std::vector &v, se::Value *ret); +bool std_vector_float_to_seval(const std::vector &v, se::Value *ret); +bool std_map_string_string_to_seval(const std::map &v, se::Value *ret); +bool ManifestAsset_to_seval(const cc::extension::ManifestAsset &v, se::Value *ret); +bool Data_to_seval(const cc::Data &v, se::Value *ret); +bool DownloadTask_to_seval(const cc::network::DownloadTask &v, se::Value *ret); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T *v_c, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value && !std::is_pointer::value, bool>::type +native_ptr_to_seval(T &v_ref, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +bool native_ptr_to_rooted_seval( + typename std::enable_if::value, T>::type *v, + se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T *vp, se::Class *cls, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T &v_ref, se::Class *cls, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +bool native_ptr_to_rooted_seval( + typename std::enable_if::value, T>::type *v, + se::Class *cls, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T *vp, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +typename std::enable_if::value, bool>::type +native_ptr_to_seval(T *vp, se::Class *cls, se::Value *ret, bool *isReturnCachedValue = nullptr); +template +bool std_vector_to_seval(const std::vector &v, se::Value *ret); +template +bool seval_to_reference(const se::Value &v, T **ret); +``` + +改用 `nativelue_to_se` + +辅助转换函数不属于 `Script Engine Wrapper` 抽象层,属于 Cocos Creator 绑定层,封装这些函数是为了在绑定代码中更加方便的转换。每个转换函数都返回 `bool` 类型,表示转换是否成功,开发者如果调用这些接口,需要去判断这个返回值。 + +以上接口,直接根据接口名称即可知道具体的用法,接口中第一个参数为输入,第二个参数为输出参数。用法如下: + +```c++ +se::Value v; +bool ok = nativevalue_to_se(100, v); // 第二个参数为输出参数,传入输出参数的地址 +``` + +```c++ +int32_t v; +bool ok = sevalue_to_native(args[0], &v); // 第二个参数为输出参数,传入输出参数的地址 +``` + +更多关于手动绑定的内容可参考 [使用 JSB 手动绑定](jsb-manual-binding.md)。 + +## 自动绑定 + +### 配置模块 ini 文件 + +具体可以参考引擎目录下的 `tools/tojs/cocos.ini` 等 `ini` 配置。 + +### 理解 ini 文件中每个字段的意义 + +```ini +# 模块名称 +[cocos] + +# 绑定回调函数的前缀,也是生成的自动绑定文件的前缀 +prefix = engine + +# 绑定的类挂载在 JS 中的哪个对象中,类似命名空间 +target_namespace = jsb + +# 自动绑定工具基于 Android 编译环境,此处配置 Android 头文件搜索路径 +android_headers = + +# 配置 Android 编译参数 +android_flags = -target armv7-none-linux-androideabi -D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS -DANDROID -D__ANDROID_API__=14 -gcc-toolchain %(gcc_toolchain_dir)s --sysroot=%(androidndkdir)s/platforms/android-14/arch-arm -idirafter %(androidndkdir)s/sources/android/support/include -idirafter %(androidndkdir)s/sysroot/usr/include -idirafter %(androidndkdir)s/sysroot/usr/include/arm-linux-androideabi -idirafter %(clangllvmdir)s/lib64/clang/5.0/include -I%(androidndkdir)s/sources/cxx-stl/llvm-libc++/include + +# 配置 clang 头文件搜索路径 +clang_headers = + +# 配置 clang 编译参数 +clang_flags = -nostdinc -x c++ -std=c++17 -fsigned-char -mfloat-abi=soft -U__SSE__ + +# 配置引擎的头文件搜索路径 +cocos_headers = -I%(cocosdir)s/cocos -I%(cocosdir)s/cocos/platform/android -I%(cocosdir)s/external/sources + +# 配置引擎编译参数 +cocos_flags = -DANDROID -DCC_PLATFORM=3 -DCC_PLATFORM_MAC_IOS=1 -DCC_PLATFORM_MAC_OSX=4 -DCC_PLATFORM_WINDOWS=2 -DCC_PLATFORM_ANDROID=3 + +# 配置额外的编译参数 +extra_arguments = %(android_headers)s %(clang_headers)s %(cxxgenerator_headers)s %(cocos_headers)s %(android_flags)s %(clang_flags)s %(cocos_flags)s %(extra_flags)s + +# 需要自动绑定工具解析哪些头文件 +headers = %(cocosdir)s/cocos/platform/FileUtils.h %(cocosdir)s/cocos/platform/CanvasRenderingContext2D.h %(cocosdir)s/cocos/platform/Device.h %(cocosdir)s/cocos/platform/SAXParser.h + +# 在生成的绑定代码中,重命名头文件 +replace_headers= + +# 需要绑定哪些类,可以使用正则表达式,以空格为间隔 +classes = FileUtils$ SAXParser CanvasRenderingContext2D CanvasGradient Device DownloaderHints + +# 哪些类需要在 JS 层扩展,以空格为间隔 +classes_need_extend = + +# 需要为哪些类绑定属性,以逗号为间隔 +field = + +# 需要忽略绑定哪些类,以逗号为间隔 +skip = FileUtils::[getFileData setFilenameLookupDictionary destroyInstance getFullPathCache getContents listFilesRecursively], + SAXParser::[(?!(init))], + Device::[getDeviceMotionValue], + CanvasRenderingContext2D::[setCanvasBufferUpdatedCallback set_.+ fillText strokeText fillRect measureText], + Data::[takeBuffer getBytes fastSet copy], + Value::[asValueVector asValueMap asIntKeyMap] + +# 需要为哪些类绑定访问属性,以逗号为间隔 +getter_setter = CanvasRenderingContext2D::[width//setWidth height//setHeight fillStyle//setFillStyle font//setFont globalCompositeOperation//setGlobalCompositeOperation lineCap//setLineCap lineJoin//setLineJoin lineWidth//setLineWidth strokeStyle//setStrokeStyle textAlign//setTextAlign textBaseline//setTextBaseline] + +# 重命名函数,以逗号为间隔 +rename_functions = FileUtils::[loadFilenameLookupDictionaryFromFile=loadFilenameLookup] + +# 重命名类,以逗号为间隔 +rename_classes = SAXParser::PlistParser + +# 配置哪些类不需要搜索其父类 +classes_have_no_parents = SAXParser + +# 配置哪些父类需要被忽略 +base_classes_to_skip = Ref Clonable + +# 配置哪些类是抽象类,抽象类没有构造函数,即在 js 层无法通过 var a = new SomeClass();的方式构造 JS 对象 +abstract_classes = SAXParser Device + +``` + +更多关于自动绑定的内容可参考 [使用 JSB 自动绑定](jsb-auto-binding.md)。 + +## 远程调试与 Profile + +默认远程调试和 Profile 是在 debug 模式中生效的,如果需要在 release 模式下也启用,需要手动修改构建原生平台后生成的文件 `native/engine/common/CMakeLists.txt` 中的宏开关。 + +```cmake +# ... +if(NOT RES_DIR) + message(FATAL_ERROR "RES_DIR is not set!") +endif() + +include(${RES_DIR}/proj/cfg.cmake) + +if(NOT COCOS_X_PATH) + message(FATAL_ERROR "COCOS_X_PATH is not set!") +endif() +# ... + +``` + +改为: + +```cmake +if(NOT RES_DIR) + message(FATAL_ERROR "RES_DIR is not set!") +endif() + +include(${RES_DIR}/proj/cfg.cmake) +set(USE_V8_DEBUGGER_FORCE ON) ## 覆盖 USE_V8_DEBUGGER_FORCE 的值 + +if(NOT COCOS_X_PATH) + message(FATAL_ERROR "COCOS_X_PATH is not set!") +endif() +# ... +``` + +再修改 `native/engine/common/Classes/Game.cpp` +```c++ +#if CC_DEBUG + _debuggerInfo.enabled = true; +#else + _debuggerInfo.enabled = false; +#endif + // 覆盖配置 + _debuggerInfo.enabled = true; +``` + +### Chrome 远程调试 V8 + +#### Windows/Mac + +- 编译、运行游戏(或在 Creator 中直接使用模拟器运行) + +- 用 Chrome 浏览器打开 。(若使用的是旧版 Chrome,则需要将地址开头的 `devtools` 改成 `chrome-devtools`) + +- 断点调试: + + ![](jsb/v8-win32-debug.jpg) + +- 抓取 JS Heap: + + ![](jsb/v8-win32-memory.jpg) + +- Profile: + + ![](jsb/v8-win32-profile.jpg) + +#### Android/iOS + +- 保证 Android/iOS 设备与 PC 或者 Mac 在同一个局域网中 + +- 编译,运行游戏 + +- 用 Chrome 浏览器打开 ,其中 `xxx.xxx.xxx.xxx` 为局域网中 Android/iOS 设备的 IP 地址。(若使用的是旧版 Chrome,则需要将地址开头的 `devtools` 改成 `chrome-devtools`) + +>如果端口被占用,端口会做+1自增处理,若连接不上,请查看 App 启动时控制台打印的端口号。 + +- 调试界面与 Windows 相同 + +## Q & A + +### se::ScriptEngine 与 ScriptingCore 的区别,为什么还要保留 ScriptingCore? + +在 1.7 中,抽象层被设计为一个与引擎没有关系的独立模块,对 JS 引擎的管理从 ScriptingCore 被移动到了 `se::ScriptEngine` 类中,ScriptingCore 被保留下来是希望通过它把引擎的一些事件传递给封装层,充当适配器的角色。 + +ScriptingCore 只需要在 AppDelegate 中被使用一次即可,之后的所有操作都只需要用到 `se::ScriptEngine`。 + +```c++ +bool AppDelegate::applicationDidFinishLaunching() +{ + ... + ... + director->setAnimationInterval(1.0 / 60); + + // 这两行把 ScriptingCore 这个适配器设置给引擎,用于传递引擎的一些事件, + // 比如 Node 的 onEnter, onExit, Action 的 update,JS 对象的持有与解除持有 + ScriptingCore* sc = ScriptingCore::getInstance(); + ScriptEngineManager::getInstance()->setScriptEngine(sc); + + se::ScriptEngine* se = se::ScriptEngine::getInstance(); + ... + ... +} +``` + +### `se::Object::root/unroot` 与 `se::Object::incRef/decRef` 的区别? + +`root`/`unroot` 用于控制 JS 对象是否受 GC 控制,root 表示不受 GC 控制,unroot 则相反,表示交由 GC 控制,对一个 `se::Object` 来说,`root` 和 `unroot` 可以被调用多次,`se::Object` 内部有 `_rootCount` 变量用于表示 `root` 的次数。当 `unroot` 被调用,且 `_rootCount` 为 0 时,`se::Object` 关联的 JS 对象将交由 GC 管理。还有一种情况,即如果 `se::Object` 的析构被触发了,如果 `_rootCount > 0`,则强制把 JS 对象交由 GC 控制。 + +`incRef`/`decRef` 用于控制 `se::Object` 这个 `cpp` 对象的生命周期,前面章节已经提及,建议用户使用 `se::HandleObject` 来控制 **手动创建非绑定对象** 的方式控制 `se::Object` 的生命周期。因此,一般情况下,开发者不需要接触到 `incRef`/`decRef`。 + +### 对象生命周期的关联与解除关联 + +使用 `se::Object::attachObject` 关联对象的生命周期
+使用 `se::Object::dettachObject` 解除对象的生命周期。 + +`objA->attachObject(objB);` 类似于 JS 中执行 `objA.__nativeRefs[index] = objB`,只有当 objA 被 GC 后,objB 才有可能被 GC。
+`objA->dettachObject(objB);` 类似于 JS 中执行 `delete objA.__nativeRefs[index];`,这样 objB 的生命周期就不受 objA 控制了。 + +### 请不要在栈(Stack)上分配 cc::RefCounted 的子类对象 + +Ref 的子类必须在堆(Heap)上分配,即通过 `new`,然后通过 `release` 来释放。当 JS 对象的 finalize 回调函数中统一使用 `release` 来释放。如果是在栈上的对象,reference count 很有可能为 0,而这时调用 `release`,其内部会调用 `delete`,从而导致程序崩溃。所以为了防止这个行为的出现,开发者可以在继承于 `cc::RefCounted` 的绑定类中,标识析构函数为 `protected` 或者 `private`,保证在编译阶段就能发现这个问题。 + +例如: + +```c++ +class CC_EX_DLL EventAssetsManagerEx : public EventCustom +{ +public: + ... + ... +private: + virtual ~EventAssetsManagerEx() {} + ... + ... +}; + +EventAssetsManagerEx event(...); // 编译阶段报错 +dispatcher->dispatchEvent(&event); + +// 必须改为 + +EventAssetsManagerEx* event = new EventAssetsManagerEx(...); +dispatcher->dispatchEvent(event); +event->release(); +``` + +### 如何监听脚本错误 + +在 AppDelegate.cpp 中通过 `se::ScriptEngine::getInstance()->setExceptionCallback(...)` 设置 JS 层异常回调。 + +```c++ +bool AppDelegate::applicationDidFinishLaunching() +{ + ... + ... + se::ScriptEngine* se = se::ScriptEngine::getInstance(); + + se->setExceptionCallback([](const char* location, const char* message, const char* stack){ + // Send exception information to server like Tencent Bugly. + // ... + // ... + }); + + jsb_register_all_modules(); + ... + ... + return true; +} +``` diff --git a/versions/4.0/zh/advanced-topics/arkts-reflection.md b/versions/4.0/zh/advanced-topics/arkts-reflection.md new file mode 100644 index 0000000000..ac572d2257 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/arkts-reflection.md @@ -0,0 +1,354 @@ +# 基于反射机制实现 JavaScript 与 HarmonyOS Next 系统原生通信 + +> 本文主要用于 Cocos 与 ArkTS 相关的通信,以实现引擎和系统交互的能力。 +> 如果要对 Cocos 引擎的 C++ 层做扩展,或者集成 C++ 库,请参考 JSB 相关文档。 + +## native.reflection.callStaticMethod + +使用 Cocos Creator 3.8.5及以上 打包的鸿蒙原生应用中,我们可以通过反射机制直接在 JavaScript/TypeScript 中调用 ArkTS 的静态方法。 + +它的函数原型声明如下: + +```js +// clsPath (ets 脚本路径)。 例如:entry/src/main/ets/Test, +// methodName (模块名称/静态方法名称)。 例如:entry/test(模块名称可省略,省略为clsPath第一个字符串) +// paramStr (方法入参)。如果是空的,需要传入'', 如果有多个参数,可以转换成JSON字符串 +// isSync (调用的 ArkTs 方法是同步方法或是异步方法)。如果调用异步的ArkTs方法可能会阻塞当前线程并等待异步回调的结果。 +// result (返回值类型)。只支持基础的三种类型:string,number,boolean,如果需要返回复杂的类型,可以转换为json之后返回 +var result = native.reflection.callStaticMethod(clsPath, methodName, paramStr, isSync); +``` + +> **注意**:创建的 ArkTS 文件需要放在 `entry/src/main/ets` 文件夹下才能被 JS/TS 调用。 + +在 callStaticMethod 方法中,我们通过传入 ArkTS 是否同步,模块路径或者依赖包名或者远程库名,方法名,参数就可以直接调用 ArkTS 的静态方法,并且可以获得 ArkTS 方法的返回值。 + +> callStaticMethod 这个函数本身是同步的,但是可以调用异步的 ArkTs 的异步方法,但是会阻塞当前的线程,等待 ArkTs 调用完成,然后获取结果返回。 + +## 使用场景 + +| 场景 | 详细分类 | 说明 | +| --- | --- | --- | +| 本地工程模块 | HAP 加载模块内文件路径 | 要求路径以 moduleName 开头 | +| 本地工程模块 | HAP 加载 HAR 模块名 | - | +| 远程包 | HAP 加载远程 HAR 模块名 | - | +| 远程包 | HAP 加载 ohpm 包名 | - | +| API | HAP 加载 @ohos.或 @system. | - | +| 模块 Native 库 | HAP 加载 libNativeLibrary.so | - | + +>本文示范了本地工程模块的两种场景,其他场景可参阅详细说明使用 + +## 使用示例 + +### HAP 加载模块内文件路径 + +通常情况下,我们会把新增的原生代码添加到自己的项目中,则会采用这个通信方式。 + +1、新建一个 ArkTS 文件(假设命名为 Test.ets),并添加如下代码: + +``` ts + +//同步调用 +function test(param: string): string { + console.log("param::", param); + return param; +} + +//异步调用 +function syncTest(param: string, cb: Function): void { + console.log("param::", param); + setTimeout(() => { + cb(param); + }, 1000); +} + +export { test, syncTest }; +``` + +> 虽然创建 Test.ts 也是可以的,但推荐在 HarmonyOS NEXT 原生项目中使用 ArkTS(*.ets) 文件。 + +2、需要在 entry 文件夹下的 build-profile.json5 文件中进行以下配置 + +> 千万不能忘,如果不将文件添加到这里,`callStaticMethod` 函数是调用不到的。 + +```json +"buildOption": { + "arkOptions": { + "runtimeOnly": { + "sources": [ + "./src/main/ets/Test.ets" + ] + } + } +} +``` + +3、在游戏脚本中调用 + +``` js +//定义参数对象 +let param = { + a:1, + b:2 +} + +//同步调用 +let o1 = native.reflection.callStaticMethod("entry/src/main/ets/Test","entry/test",JSON.stringify(param), true); +console.log("result::", o1, typeof o1, JSON.parse(o1).a); + +//异步调用 +let o2 = native.reflection.callStaticMethod("entry/src/main/ets/Test","entry/syncTest",JSON.stringify(param), false); +console.log("result::", o2, typeof o2, JSON.parse(o2).a); +``` + +> 注意:异步调用会产生等待,暂停游戏线程。 如果调用了需要较长时间才返回的异步调用,或者是永远不会有返回的异步调用,将导致游戏卡死。 如果需要异步调用,请配合 cocos.evalString 实现 callback 机制。 + +### HAP 加载 HAR 模块名 + +当公司有多个项目时,我们通常会把一些通用能力抽象为 HAR 静态库,`callStaticMethod` 可以直接调用 HAR 包中的静态函数。 + +1、新建一个 HAR 包,名字叫 'harlibtest' + +2、在 HAR 包的 Index.ets 文件中加入如下代码 + +``` ts +function test(param: string): string { + console.log("param::", param); + return param; +} + +function syncTest(param: string, cb: Function): void { + console.log("param::", param); + setTimeout(() => { + cb(param); + }, 1000) +} + +export { test, syncTest }; +``` + +3、在 oh-package.json5 文件中配置 dependencies 项 + +```json +{ + "dependencies": { + "library": "file:../harlibtest" + } +} +``` + +4、其次,还需要在 build-profile.json5 中进行配置 + +```json +"buildOption": { + "arkOptions": { + "runtimeOnly": { + "packages": [ + "harlibtest" + ] + } + } +} +``` + +5、游戏中调用 + +``` ts +let param = { + a:1, + b:2 +} +let o1 = native.reflection.callStaticMethod("harlibtest","entry/test",JSON.stringify(param), true); +console.log("result::", o1, typeof o1, JSON.parse(o1).a); + +let o2 = native.reflection.callStaticMethod("harlibtest","entry/syncTest",JSON.stringify(param), false); +console.log("result::", o2, typeof o2, JSON.parse(o2).a); +``` + +## ArkTS 调用 Cocos 引擎脚本 + +使用 Cocos Creator 3.8.6 及以上 打包的鸿蒙原生应用中,C++ 封装了 evalString 方法提供给开发者直接从 AtkTS 直接执行游戏脚本代码。 + +### 使用方法 + +1、在游戏脚本中增加一个全局函数 + +```ts +//方法1:使用 [] 添加 window 对象的全局属性 +window['foo'] = (str:string,n:number)=>{ + console.log(str,n); +} + +//方法2: 使用 . 添加 window 对象的全局属性,并用 @ts-ingnore 忽略编译报错。 +//@ts-ingnore +window.foo = (str:string,n:number)=>{ + console.log(str,n); +} + +//方法3:添加全局对象,方便统一管理 +class NativeMessageHandler{ + foo(str:string,n:number){ + console.log(str,n); + } +} + +//@ts-ingnore +window.nmh = new NativeMessageHandler(); +``` + +2、在 ArkTS 的 Cocos Worker 线程中调用: + +```ts +import cocos from 'libcocos.so'; +//调用全局方法 +cocos.evalString(`foo('abc',123)`); + +//调用全局对象的方法 +cocos.evalString(`nmh.foo('abc',123)`); +``` + +--- + +### ⚠️注意事项 + +1. 此方法只能在 cocos worker 线程执行,因此如果有业务需求在主线程执行后调用的话,需要在主线程将结果发送给 cocos worker 线程后再调用 evalString。 +2. 此方法需要在 worker 线程的 js 引擎初始化完成之后才可以调用,也就是 renderContext.nativeEngineInit() 执行之后 +3. 此方法只能在 v8 和 jsvm 两种 js 引擎中有效,方舟引擎不支持。tips:方舟引擎的 js 交互可直接在 globalThis 上绑定对象后访问 +4. 此方法只支持返回 number,string,boolean,纯对象相较于老办法 nativeSdkUtil.gameMsgHandle,性能提升约 30%+,推荐使用。 +5. 请阅读 [线程安全](#线程安全) 部分,深入理解执行线程相关内容。 + +--- + +很多时候,我们的调用其实是从主线程发起的,这就需要我们做一个跨线程通信。 + +下面是一个完整的主线程中调用 `cocos.evalString` 的示例。 + +#### 游戏脚本代码 + +```ts +window.test1 = (a,b) => { + console.log(a + b); +} +``` + +#### ArkTS 代码 + +``` ts +//cocos_worker.ts 中添加 evalString 消息处理 +uiPort._messageHandle = function (e) { + //.... + case "backPress": + appLifecycle.onBackPress(); + break; + //添加 evalString 用于处理消息 + case "evalString": + cocos.evalString(msg.param); + break; + default: + console.error("cocos worker: message type unknown"); + break; + //...... +} + +// 在其他地方调用时 +import { WorkerManager } from '../cocos/WorkerManager'; +let callStr = "test1(1,2)"; +const cocosWorker = WorkerManager.getInstance().getWorker(); +cocosWorker.postMessage({ type: "async", data: { name:"evalString", param: callStr } }); +``` + +> 注意1:跨线程通信走的是 worker 间的消息机制,其性能远低于直接的函数调用,因此需要避免高频调用。 + +## 线程安全 + +> 注意:在做 HarmonyOS NEXT 原生交互时,一定要明白当前代码是在哪个线程里执行的,错误的跨线程访问,必然引发崩溃。 + +### 主线程与游戏线程 + +HarmonyOS NEXT 运行时,会有两个主要的线程。 + +- **主线程**:这是 HarmonyOS NEXT 的原生 App 启动时的默认线程,用于执行原生 UI 的渲染、 +ArkTS 的装载和执行、启动其他线程。 +- **游戏线程**:主线程启动成功后,会开启游戏线程,引擎启动、渲染更新、JSVM/V8 脚本的执行都在这个游戏线程。 + +由于线程资源的竞争性,为了安全起见,主线程和游戏线程是不能直接访问彼此负责的资源的,否则会导致 App 崩溃闪退。 + +可以通过以下方式确认线程 ID: + +```ts +import process from '@ohos.process'; +console.log('current thread id:',process.tid); +``` + +> **小技巧**:如果将上述代码写在函数体外,则打印的是主线程 id,因为 TS/ArkTS 代码是由主线程负责加载的。 + +### 主线程调用游戏线程函数 + +在主线程启动后,会立即启动游戏线程,主要源码为 `WorkerManager.ts` 和 `cocos_worker.ts` 这两个。 + +如果要新增处理任务,需要在 `cocos_worker.ts` 中新增一个消息处理函数。 + +其他线程(比如主线程),可以通过下面的方式向游戏线程发送消息: + +```ts +const cocosWorker = WorkerManager.getInstance().getWorker(); +cocosWorker.postMessage({ type: "async", data: { name:msgName, param: paramStr } }); +``` + +`name:string`:msgName 对应的是 `cocos_worker.ts` 中的消息类型,用于分发任务。 +`param:string`:paramStr 对应的是消息参数。 + +### 游戏线程调用主线程函数 + +游戏线程访问主线程,有两种情况。 + +#### 情况1:游戏脚本层访问主线程 + +我们可以通过 `native.reflection.callStaticMethod` 方式来实现游戏脚本调用原生层 TS/ArkTS 函数。 + +需要注意的是,虽然引擎脚本是在游戏线程(`Cocos Worker`)中执行的,但被调用的函数是在主线程环境中执行,以方便我们访问原生系统的功能、资源以及原生 UI 控件。 + +线程关系如下图所示: + +![](./arkts-reflection/threads-image.png) + +#### 情况2:游戏线程里访问主线程 + +有时候,我们也需要在引擎的 `Cocos Worker` 中调用主线程的函数。需要我们做两件事情。 + +1. 在 pages/index.ts 中添加需要执行的消息 +2. 在**游戏线程**代码中使用下面的方式向主线程发送消息 + + ```ts + let mainThreadPort = worker.workerPort; + mainThreadPort.postMessage({ type: "async", data: { name:msgName, param: paramStr } }); + ``` + +## 自定义项目模板代码 + +在目前的版本中,cocos_worker.ts 等 native 中的代码,会在 Cocos Creator 构建时被还原,此问题会在后面的版本中修复。 + +当前版本中,开发者可以用下面的方式解决。 + +1. 将需要自定义的文件复制到根目录下(与 `assets` 平级),如果有多个文件,则建议放入一个文件夹,后期对这些文件的修改,以这个目录下的文件为主 +2. 新建一个构建扩展 +3. 在构建扩展的 `hook.ts` 中的 `onAfterBuild` 里,添加如下代码: + +```ts +//这是单个文件的方式,多个文件请遍历目录再进行复制 +if(options.platform === 'harmonyos-next' ){ + let src = `${Editor.Project.path}/cocos_worker.ts`; + let dest = `${Editor.Project.path}/native/engine/harmonyos-next/entry/src/main/ets/workers/cocos_worker.ts`; + console.log(`copy ${src} to ${dest}`); + const fs = require('fs'); + fs.writeFileSync(dest, fs.readFileSync(src)); +} +``` + +编译扩展: + +```sh +npm install +npm run build +``` + +刷新扩展后,再次打包,就可以实现 cocos_worker.ts 等源码的修改生效了。 diff --git a/versions/4.0/zh/advanced-topics/arkts-reflection/threads-image.png b/versions/4.0/zh/advanced-topics/arkts-reflection/threads-image.png new file mode 100644 index 0000000000..18227da12a Binary files /dev/null and b/versions/4.0/zh/advanced-topics/arkts-reflection/threads-image.png differ diff --git a/versions/4.0/zh/advanced-topics/cmak-learning/code1.png b/versions/4.0/zh/advanced-topics/cmak-learning/code1.png new file mode 100644 index 0000000000..45ce7a8e21 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/cmak-learning/code1.png differ diff --git a/versions/4.0/zh/advanced-topics/cmak-learning/code2.png b/versions/4.0/zh/advanced-topics/cmak-learning/code2.png new file mode 100644 index 0000000000..f003e6f321 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/cmak-learning/code2.png differ diff --git a/versions/4.0/zh/advanced-topics/cmak-learning/folder3.png b/versions/4.0/zh/advanced-topics/cmak-learning/folder3.png new file mode 100644 index 0000000000..f6e3a1e0d1 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/cmak-learning/folder3.png differ diff --git a/versions/4.0/zh/advanced-topics/cmak-learning/folder4.png b/versions/4.0/zh/advanced-topics/cmak-learning/folder4.png new file mode 100644 index 0000000000..45d5877074 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/cmak-learning/folder4.png differ diff --git a/versions/4.0/zh/advanced-topics/cmak-learning/project.png b/versions/4.0/zh/advanced-topics/cmak-learning/project.png new file mode 100644 index 0000000000..c0e5a23585 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/cmak-learning/project.png differ diff --git a/versions/4.0/zh/advanced-topics/cmake-learning.md b/versions/4.0/zh/advanced-topics/cmake-learning.md new file mode 100644 index 0000000000..d15cf94059 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/cmake-learning.md @@ -0,0 +1,392 @@ +# CMake 使用简介 + + +## CMake 介绍 + +CMake 是一个非常强大的构建工具,可以大大简化软件的编译过程,提高开发效率。Cocos Creator 在各个原生平台都使用了 CMake。以下是一些快速了解 CMake 的优点: + +- 使用 CMakeLists.txt 文件描述整个项目的构建过程,而不是像其他构建工具一样使用脚本文件。 +- 是跨平台的,可以在 Windows、Linux、macOS 等操作系统上运行。 +- 可以自动生成 Makefile、Visual Studio 等 IDE 的工程文件,从而简化了软件的编译过程。 +- 可以轻松的管理依赖库,将代码组织成模块等。 +- 支持多种编程语言,包括 C、C++、Fortran、Java、Python 等。 + +虽然 CMake 是一个非常强大的构建工具,但是它也有一些缺点,比如语法比较复杂,需要一定的学习成本。 + +开发者可以学习 CMake 的语法并添加自己的模块,以便在构建过程中执行特定的任务。例如,他们可以定义自己的预处理器宏或编译器选项,以便在构建期间执行自定义操作。另外,他们还可以编写脚本来修改工程文件,以便在不同的平台上进行不同的配置。详情可参考 [二次开发](../editor/publish/native-options.md#%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91)。 + +如果您想深入学习 CMake,可以阅读本教程的其他章节,了解如何安装 CMake、创建 CMakeLists.txt 文件、指定编译器、添加源文件、添加依赖库和构建项目等。由于篇幅限制,本文无法对其进行详细介绍,例如如何使用 FindPackage、如何设置构建类型、如何安装和测试等内容。为了更好地掌握它,开发者需要查阅其他文档并进行更多的实践。 + +## 安装 + +为方便开发者,Cocos Creator 内部集成了 cmake 程序,构建流程会使用它来完成。因此,一般情况下开发者不需要手动安装 cmake。 + +如果开发者希望编辑器使用设备上的 cmake,则可以通过编辑相关的配置完成。 + +如果开发者想要在命令行中使用 cmake,可以前往[官网下载](https://cmake.org/download/)。在 Mac 平台上,也可以使用 Homebrew 进行安装,执行以下命令进行安装: + +```bash +brew install cmake + +``` + +## 快速开始 + +`CMakeLists.txt` 文件是 CMake 的核心文件,用于描述整个项目的构建过程。使用该文件可以方便地管理项目的构建和编译过程。其中包含了一系列命令和变量,用于指定项目名称、版本号、源文件、依赖库等信息,以及指定编译器、编译选项等参数。 + +下面是一个简单的 CMake helloworld工程的例子。 + +首先,创建一个名为 `CMakeLists.txt` 的文件。在此文件中,添加以下内容: + +``` +# CMake 版本 +cmake_minimum_required(VERSION 3.10) + +# 项目名称, 指定语言为 C++ +project(helloworld CXX) + +# 可执行文件 +add_executable(helloworld main.cpp) + +``` + +然后,在项目的根目录下创建一个名为 `main.cpp` 的文件,并添加以下内容: + +``` +#include + +int main() { + std::cout << "Hello, world!" << std::endl; + return 0; +} + +``` + +最后,在项目的根目录下创建一个名为 `build` 的目录,并在其中执行以下命令: + +```bash +# 在 build 目录下生成默认的工程文件。如果已经安装了 Visual Studio,则默认为 Visual Studio 工程;在 Mac 下默认为 Makefile 工程。通过指定 -G 可以设置工程文件的类型, 比如 -GXcode。 +cmake -B build -S . +# 生成可执行文件 +cmake --build build +``` + +执行完这些命令后,将在 `build` 目录中生成可执行文件 `helloworld`。运行该文件,将输出 "Hello, world!"。 + +这里用到的两个命令 project 和 add_executable + +`project` 是 CMake 中的一个命令,用于指定项目名称、版本号、语言等信息,其语法如下: + +``` +project(project_name [version] [LANGUAGES languages...]) + +``` + +其中,`project_name` 用于指定项目的名称,`version` 用于指定项目的版本号,`languages` 用于指定项目所使用的编程语言。如果不指定 `version` 或 `languages` 参数,则可以省略它们。例如: + +``` +project(MyProject) + +``` + +这个命令将设置项目名称为 `MyProject`,不指定版本号和编程语言。 + +`add_executable` 用于添加可执行文件的构建规则,其语法如下: + +``` +add_executable(executable_name [source1] [source2] ...) + +``` + +其中,`executable_name` 用于指定可执行文件的名称,`source1`、`source2` 等参数用于指定源文件的名称。例如: + +``` +add_executable(MyProject main.cpp) + +``` + +这个命令将设置可执行文件名称为 `MyProject`,并将 `main.cpp` 文件作为源文件添加到项目中。 + +### 其他常用 CMake 命令 + +### `message` + +`message()` 命令用于在 CMake 运行时向用户显示消息。它接受一个或多个参数,作为要显示的消息。例如: + +```cmake +message("Hello, world!") + +``` + +这个命令将在 CMake 运行时向用户显示 "Hello, world!" 消息。 + +你也可以在 `message()` 命令中使用变量和表达式。例如: + +```cmake +set(SRC_FILES main.cpp) +message("Source files: ${SRC_FILES}") + +``` + +这个命令将在 CMake 运行时向用户显示 "Source files: main.cpp" 消息。 + +`message()` 命令还可以用于输出调试信息。例如: + +```cmake +if (DEBUG) + message("Debug mode enabled") +endif () + +``` + +这个命令将在 CMake 运行时检查变量 `DEBUG` 是否为真,如果为真,则向用户输出 "Debug mode enabled" 消息。 + +#### `message` 命令还有其他用途,例如: + +- 输出警告信息:`message(WARNING "This is a warning message")` +- 输出错误信息:`message(FATAL_ERROR "This is an error message")` +- 输出调试信息:`message(STATUS "This is a status message")` + +### `set` + + `set()` 命令主要用于创建或修改变量。该命令至少接受两个参数:变量名和值。例如,你可以使用 `set(SRC_FILES main.cpp)` 来设置变量 `SRC_FILES` 的值为 `main.cpp`。如果你想要为变量设置多个值(比如列表),你可以在命令中添加更多参数,如 `set(SRC_FILES main.cpp util.cpp)`。如果你想要读取变量的值,可以使用 `${}` 语法,如 `message(${SRC_FILES})`。 + +可以使用 `set` 命令向列表变量中添加元素。具体来说,可以使用 `set(SRC_FILES ${SRC_FILES} util.cpp)` 命令将 `util.cpp` 添加到 `SRC_FILES` 列表的末尾。其中,`${SRC_FILES}` 表示取出 `SRC_FILES` 变量的当前值。这个命令还可以使用其他的 `set` 命令选项,如 `CACHE` 和 `APPEND` 等。 + +### `list` + +`list()` 命令用于处理列表类型的变量。它可以接受多种子命令,如 `APPEND`(在列表尾部添加元素)、`INSERT`(在指定位置插入元素)、`REMOVE_ITEM`(删除指定的元素)等。例如,`list(APPEND SRC_FILES util.cpp)` 命令会将 `util.cpp` 添加到 `SRC_FILES` 列表的末尾。 + +### `add_library` + +`add_library` 命令用于定义一个库目标。它至少需要两个参数:库的名称和源文件。如果你只提供了一个源文件,那么 CMake 将创建一个由这个源文件构建的库。例如,`add_library(MyLib main.cpp)`。如果你有多个源文件,你可以将它们全部放到 `add_library()` 命令中,例如 `add_library(MyLib main.cpp util.cpp)`。 + +CMake支持创建静态库和动态库。默认情况下,`add_library()` 命令会创建一个静态库。如果你想要创建一个动态库,你需要在命令中添加 `SHARED` 参数,例如:`add_library(MyLib SHARED main.cpp)`。 + +如果你想要同时创建静态库和动态库,你可以将它们都列出来,例如:`add_library(MyLibStatic STATIC main.cpp)` 和 `add_library(MyLibShared SHARED main.cpp)`。 + +静态库是指在编译时链接到可执行文件中的库,而动态库是指在运行时加载的库。静态库通常只包含可执行文件所需的代码,因此它们比较小。动态库通常包含更多的代码和数据,因为它们需要在运行时执行。动态库的优点是可以在不重新编译可执行文件的情况下更新库。它们也可以在多个可执行文件之间共享,从而节省磁盘空间。 + +使用 `add_library()` 命令时,你可以指定库的名称和类型(静态库或动态库),以及要包含的源文件和头文件。例如,`add_library(MyLib STATIC main.cpp)` 命令将 `main.cpp` 源文件添加到名为 `MyLib` 的静态库中。 + +### `find_library` + +命令用于查找并定位系统上的库文件。你需要提供一个变量名(用于存储找到的库的路径)和库的名称。例如,`find_library(MY_LIB NAMES MyLib)`。在这个例子中,CMake 会在系统的库路径中搜索名为 `MyLib` 的库。如果找到了,`MY_LIB` 变量的值将会被设置为该库的全路径。 + +可以使用 `find_library` 命令来查找系统上的库文件。你需要提供一个变量名(用于存储找到的库的路径)和库的名称。 + +例如,假设你想要查找名为 `libexample` 的库,它在 `/usr/local/lib` 目录中。 这里的绝对路径可以使用`${CMAKE_CURRENT_LIST_DIR}`等变量转化为相对路径。你可以在 `CMakeLists.txt` 文件中添加以下命令: + +``` +find_library(EXAMPLE_LIB libexample /usr/local/lib) + +``` + +这个命令会将 `libexample` 库的路径保存到变量 `EXAMPLE_LIB` 中。如果找不到该库,`EXAMPLE_LIB` 变量的值将会是空的。 + +在使用 `find_library` 命令时,你可以指定库的名称、路径、版本和语言。例如,`find_library(EXAMPLE_LIB NAMES example PATHS /usr/local/lib VERSION 1.0 LANGUAGES CXX)` 命令将查找名为 `example`、版本为 `1.0`、语言为 `C++` 的库,并将其路径保存到 `EXAMPLE_LIB` 变量中。 + +如果你想要查找多个库,可以在命令中添加多个库名称。例如,`find_library(LIB1 NAMES lib1 lib1.a PATHS /usr/local/lib)` 命令将查找名为 `lib1` 或 `lib1.a` 的库,并将其路径保存到 `LIB1` 变量中。 + +注意,在使用 `find_library` 命令时,你需要确保库文件的名称、路径、版本和语言与你的项目相匹配。否则,你的项目可能无法正确地链接到库文件。 + +### `target_link_libraries` + + `target_link_libraries()` 命令用于将指定的库链接到目标。这个命令至少需要两个参数:目标名称和库名称。例如,`target_link_libraries(MyApp MyLib)`。这个命令将 `MyLib` 库链接到 `MyApp` 目标。这意味着 `MyApp` 在构建时会使用 `MyLib`。 + +### `target_include_directories` + +`target_include_directories()` 命令用于为指定的目标添加包含目录。这个命令需要至少两个参数:目标名称和要添加的目录。例如,`target_include_directories(MyApp PRIVATE include/)`。这个命令将 `include/` 目录添加到 `MyApp` 目标的包含目录中。这意味着编译 `MyApp` 时,编译器会在 `include/` 目录中查找头文件。 + +### `target_compile_options` + +```CMake +message(STATUS "my custom debug info") +``` + +`target_compile_options()` 命令用于为指定的目标设置编译选项。这个命令至少需要两个参数:目标名称和编译选项。例如,`target_compile_options(MyApp PRIVATE -Wall)`。这个命令将 `-Wall` 选项添加到 `MyApp` 的编译选项中。这意味着 `MyApp` 在编译时会启用所有的警告(这是 `-Wall` 选项的作用)。 + +## 常见的任务 + +### 添加源文件 + +可以使用以下命令添加源文件: + +``` +add_executable(MyProject main.cpp math/vec3.cpp math/vec4.cpp) +``` + +在这个示例中,`MyProject` 的源文件包括 **`main.cpp`**、 `math/vec3.cpp` 和 `math/vec4.cpp` . 如果有更多的源文件,只需要往这个列表添加它们. + +### 添加依赖库 + +可以使用以下命令添加依赖库: + +``` +target_link_libraries(MyProject MyLibrary) +``` + +下面的例子中, `find_library()` 命令将在 `libs` 目录下查找名为 `libexample` 的静态库, 并将其路径保存到变量 `LIBS` 中. `target_link_libraries()` 命令将这个库链接到 `MyProject` 目标. + +``` +find_library(LIBS libexample libs PATHS ${CMAKE_CURRENT_LIST_DIR}/libs/android/${ANDROID_ABI}) + +add_executable(MyProject main.cpp) +target_link_libraries(MyProject ${LIBS}) + +# 添加头文件的搜索路径 +target_include_directories(MyProject PUBLIC ${CMAKE_CURRENT_LIST_DIR}/libs/include) +``` + +### CMake变量 + +CMake内置了一些以CMAKE_开头的变量,方便与环境交互。这些变量的使用可以使你的CMakeLists.txt文件更加简洁、易于维护。比如,`CMAKE_CURRENT_LIST_DIR` 变量用于存储当前处理的CMakeLists.txt文件所在的目录的路径。在CMakeLists.txt文件中使用此变量的示例如下: + +``` +add_library(MyLibrary STATIC ${CMAKE_CURRENT_LIST_DIR}/src/my_library.cpp) + +``` + +上述示例中,我们使用CMAKE_CURRENT_LIST_DIR变量指定源文件的路径。同样,CMAKE_BINARY_DIR变量用于存储二进制文件的根目录的路径。在CMakeLists.txt文件中使用此变量的示例如下: + +``` +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) + +``` + +这里,CMAKE_BINARY_DIR变量用于指定可执行文件输出的根目录。在编译项目时,可执行文件将被输出到${CMAKE_BINARY_DIR}/bin目录中。 + +请注意,${CMAKE_BINARY_DIR}和${CMAKE_CURRENT_BINARY_DIR}变量之间的区别。${CMAKE_BINARY_DIR}是指二进制文件的根目录,而${CMAKE_CURRENT_BINARY_DIR}是指当前处理的CMakeLists.txt文件的二进制目录。 + +此外,其他常用的变量包括但不限于: + +- CMAKE_SOURCE_DIR: CMakeLists.txt文件所在的目录 +- CMAKE_CURRENT_SOURCE_DIR: 当前处理的CMakeLists.txt所在的目录 +- CMAKE_BINARY_DIR: 二进制文件的根目录 +- CMAKE_CURRENT_BINARY_DIR: 当前处理的CMakeLists.txt的二进制目录 +- CMAKE_INSTALL_PREFIX: 安装目录的根目录 +- CMAKE_MODULE_PATH: CMake模块的根目录 +- CMAKE_BUILD_TYPE: 编译类型 +- CMAKE_CXX_FLAGS: C++编译器选项 + +## 在 Cocos 中使用 CMake + +Android 在编译 C++ 代码使用了 cmake,这是原生支持的. 我们会通过 gradle 去配置参数和调用 cmake 命名生成/编译/打包 C++ 代码。对于其他的原生平台,我们会通过构建插件调用对于的 cmake 命令去生成工程文件。在Windows 上的 Visual Studio 工程,Mac 上的 Xcode 工程. 后续的开发就只需通过 IDE 去完成. + +由于 CMake 的特性,可能会因为不同的开发环境和配置而产生差异,因此不建议共享生成的工程文件。另外,对生成工程的修改很容易被后续生成的工程覆盖。相反,应该将 CMakeLists.txt 文件包含在项目中,并在每个开发环境中使用 CMake 来生成相应的工程文件。所有对工程的修改都应该以 CMake 指令的方式写入到 CMakeLists.txt 中。 + +在使用 CMake 和 Xcode cocoapods 时,可能会出现一些问题。主要的问题是,CMake 生成的 Xcode 工程文件与 cocoapods 集成不兼容。这是因为 cocoapods 使用了自己的方式来管理 Xcode 项目文件,而 CMake 生成的工程文件没有考虑这一点。这可能会导致一些问题,如编译错误、链接错误, 修改被覆盖等等。 + +为了解决这个问题,我们可以在构建 Mac/iOS 平台时勾选 “跳过 Xcode 工程更新” 选项。这样做的意思是,后续引擎或工程的 CMake 配置更新不会同步到 Xcode 工程。勾选该选项后,就可以与 CocoaPods 协作,以普通的 Xcode 工程形式进行构建。 + +### 共享生成的 Xcode 工程文件 + +CMake 生成的 Xcode 工程记录了依赖路径,这些路径来自于以下几个方面: + +- Xcode 的安装路径 +- Cocos Creator 版本和安装路径 +- 工程的版本和安装路径 +- 工程文件所在的路径 + +通过在不同设备上使用相同的目录结构,Xcode 可以实现不同设备间的共享。 + +另一个做法是修改 Xcode 工程文件内部引用的路径,这个方法比较 hack,这里不做详细介绍。 + +和 Xcode 不同,Android Studio 使用 CMake 是直接将 CMakeLists.txt 作为配置文件进行编译,而不是生成工程文件。因此,CMake 生成的 Android 平台的 native 库在不同设备上的表现应该是一致的。此外,Android Studio 的 Gradle 插件会自动处理依赖关系,因此不需要像 Xcode 那样手动管理依赖目录。 + + +### 目录结构 + +当选择某个原生平台进行构建时,项目目录 `native\engine` 目录下会生成 `当前构建的平台名称` 文件夹(例如 `android`),以及 `common` 文件夹。CMake 在第一次运行时将会在这两个目录下分别生成 `CMakeLists.txt` 文件,作用各不相同: + +- `当前构建的平台名称` 文件夹:`CMakeLists.txt` 主要用于配置对应的构建平台。以 Android 平台为例: + + ![folder2](./cmak-learning/folder3.png) + +- `common` 文件夹:`CMakeLists.txt` 主要用于配置整个项目。 + + ![folder2](./cmak-learning/folder4.png) + +`CMakeLists.txt` 的语法比较简单,由 **命令**、**注释** 和 **空格** 组成。其中命令是不区分大小写的,但命令中的参数和变量则是大小写敏感的。 + +### CMakeLists.txt 文件介绍 + +那如何利用 CMake 将项目编译成动态库提供给其他项目使用呢?简单来说就是先录入编译信息,然后 CMake 命令再根据 `CMakeLists.txt` 中的配置生成编译所需的 Makefile 文件。 + +下面我们以 Android 平台为例,具体看一下如何配置项目目录 `native/engine/android` 目录下的 `CMakeLists.txt`。 + +```cmake +cmake +# 要求 CMake 版本在 3.8 或更高 +cmake_minimum_required(VERSION 3.8) + +# 设置项目名称选项 +option(APP_NAME "项目名称" "NewProject") + +# 设置项目名并启用 C++ +project(${APP_NAME} CXX) + +# 设置库名称 +set(CC_LIB_NAME cocos) + +# 设置项目目录 +set(CC_PROJECT_DIR ${CMAKE_CURRENT_LIST_DIR}) + +# 设置项目源文件 +set(CC_PROJ_SOURCES) + +# 设置常用源文件 +set(CC_COMMON_SOURCES) + +# 设置所有的源文件 +set(CC_ALL_SOURCES) + +# 包含常用 CMake 函数 +include(${CC_PROJECT_DIR}/../common/CMakeLists.txt) + +# 如果需要添加源码可以在这里修改 CC_PROJ_SOURCES + +# 调用 Android 预构建步骤 +cc_android_before_target(${CC_LIB_NAME}) + +# 添加库 +add_library(${CC_LIB_NAME} SHARED ${CC_ALL_SOURCES}) + +# 调用 Android 后构建步骤 +cc_android_after_target(${CC_LIB_NAME}) +``` + + +项目目录 `native/engine/common` 目录下的 `CMakeLists.txt` 文件的配置方法也是一致的,但是会多一些基础的配置。例如: + +```CMake +option(USE_SPINE "Enable Spine" ON) +``` + +构建后生成的发布包目录(例如 `build/android`)下有一个 `proj/cfg.cmake` 文件,用于存放当前项目的一些配置。因为 `CMakeLists.txt` 中有对 `cfg.cmake` 文件进行引入,所以当 `cfg.cmake` 文件中的配置做了修改,便会同步到 `CMakeLists.txt` 中;若是相同的配置,则直接覆盖,以 `cfg.cmake` 文件中的为准。 + +从 3.6.2 开始,开发者可以在 `native/engine/common/localCfg.cmake` 中覆盖 `cfg.cmake` 中设置的选项,而且 `localCfg.cmake` 会从 GIT 中忽略。 + +```CMake +CMakeLists.txt + +# 引入 cfg.cmake +include(${RES_DIR}/proj/cfg.CMake) +``` + +例如将编辑器主菜单 **项目 -> 项目设置 -> 功能裁剪** 中的 **Spine 动画** 去掉勾选: + +![project](./cmak-learning/project.png) + +则在再次构建时重新生成的 `cfg.make` 中就会将 `USE_SPINE` 设置为 `OFF`: + +![code1](./cmak-learning/code1.png) + +然后在编译时,CMake 便会根据配置(例如 `CMakeLists.txt` 以及 `CMakeLists.txt` 中引入的 `cfg.make` 等配置文件)生成 **CMakeCache.txt** 文件,该文件中包含了项目构建时 **需要依赖的各种输入参数**。 + +![code2](./cmak-learning/code2.png) + + +## **进一步学习** + +你可以查阅以下资料来进一步学习 [CMake 官方文档](https://cmake.org/documentation/) diff --git a/versions/4.0/zh/advanced-topics/data-storage.md b/versions/4.0/zh/advanced-topics/data-storage.md new file mode 100644 index 0000000000..e30080ecea --- /dev/null +++ b/versions/4.0/zh/advanced-topics/data-storage.md @@ -0,0 +1,127 @@ +# 存储和读取用户数据 + +我们在游戏中通常需要存储用户数据,如音乐开关、显示语言等,如果是单机游戏还需要存储玩家存档。Cocos Creator 中我们使用 `localStorage` 接口来进行用户数据存储和读取的操作。 + +`localStorage` 接口是按照 [Web Storage API](http://devdocs.io/dom/storage) 来实现的,在 Web 平台运行时会直接调用 Web Storage API,在原生平台上会调用 sqlite 的方法来存储数据。一般用户不需要关心内部的实现。 + +配合本篇文档可以参考 **数据存储范例**([GitHub](https://github.com/cocos-creator/tutorial-storage) | [Gitee](https://gitee.com/mirrors_cocos-creator/tutorial-storage))。 + +## 存储数据 + +`sys.localStorage.setItem(key, value)` + +上面的方法需要两个参数,用来索引的字符串键值 `key`,和要保存的字符串数据 `value`。 + +假如我们要保存玩家持有的金钱数,假设键值为 `gold`: + +`sys.localStorage.setItem('gold', 100);` + +对于复杂的对象数据,我们可以通过将对象序列化为 JSON 后保存: + +```js +userData = { + name: 'Tracer', + level: 1, + gold: 100 +}; + +sys.localStorage.setItem('userData', JSON.stringify(userData)); +``` + +## 读取数据 + +`sys.localStorage.getItem(key)` + +和 `setItem` 相对应,`getItem` 方法只要一个键值参数就可以取出我们之前保存的值了。对于上文中储存的用户数据: + +```js +var userData = JSON.parse(sys.localStorage.getItem('userData')); +``` + +**存储数据** 和 **读取数据** 的完整示例代码参考如下: + +```ts +import { _decorator, Component, sys } from 'cc'; +const { ccclass, property } = _decorator; + +const coinKey: string = 'gold' +const userDataKey = 'userData'; + +interface UserData{ + name:string + level:number + gold:number +} + +@ccclass('LocalStorageExample') +export class LocalStorageExample extends Component { + start() { + this.saveCoin(100); + this.loadCoin(); + this.saveUserData(); + this.loadUserData(); + } + + saveCoin(value: number) { + sys.localStorage.setItem(coinKey, value.toString()); + } + loadCoin() { + const value = sys.localStorage.getItem(coinKey); + if (value) { + console.log(`${coinKey} = ${value}`) + } else { + console.log(`${coinKey} is not exist`); + } + } + + saveUserData(){ + const userData = { name:"Tracer", level:1, gold:100} + const jsonStr = JSON.stringify(userData); + sys.localStorage.setItem(userDataKey,jsonStr); + } + + loadUserData(){ + const jsonStr = sys.localStorage.getItem(userDataKey); + const userData = JSON.parse(jsonStr) as UserData;; + console.log(userData); + } +} +``` + +## 移除键值对 + +当我们不再需要一个存储条目时,可以通过下面的接口将其移除: + +`sys.localStorage.removeItem(key)` + +## 清空数据 + +当我们不再需要已存储的用户数据时,可以通过下面的接口将其清空: + +`sys.localStorage.clear()` + +## 数据加密 + +对于单机游戏来说,对玩家存档进行加密可以延缓游戏被破解的时间。要加密存储数据,只要在将数据通过 `JSON.stringify` 转化为字符串后调用你选中的加密算法进行处理,再将加密结果传入 `setItem` 接口即可。 + +您可以搜索并选择一个适用的加密算法和第三方库,比如 [encryptjs](https://www.npmjs.com/package/encryptjs),将下载好的库文件放入你的项目。 + +存储时: + +```typescript +import encrypt from 'encryptjs'; +var secretkey = 'open_sesame'; // 加密密钥 +var dataString = JSON.stringify(userData); +var encrypted = encrypt.encrypt(dataString,secretkey,256); + +sys.localStorage.setItem('userData', encrypted); +``` + +读取时: + +```typescript +var cipherText = sys.localStorage.getItem('userData'); +var userData=JSON.parse(encrypt.decrypt(cipherText,secretkey,256)); +``` + +> **注意**:数据加密不能保证对用户档案的完全掌控,如果您需要确保游戏存档不被破解,请使用服务器进行数据存取。 diff --git a/versions/4.0/zh/advanced-topics/dynamic-atlas.md b/versions/4.0/zh/advanced-topics/dynamic-atlas.md new file mode 100644 index 0000000000..d3899dbfc4 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/dynamic-atlas.md @@ -0,0 +1,37 @@ +# 动态合图 + +降低 DrawCall 是提升游戏渲染效率一个非常直接有效的办法,而两个 DrawCall 是否可以合并为一个 DrawCall 的其中一个重要因素就是这两个 DrawCall 是否使用了同一张贴图。 + +Cocos Creator 提供了 **动态合图**(Dynamic Atlas)的功能,它能在项目运行时动态地将贴图合并到一张大贴图中。当渲染一张贴图的时候,动态合图系统会自动检测这张贴图是否已经被合并到了图集(图片集合)中,如果没有,并且此贴图又符合动态合图的条件,就会将此贴图合并到图集中。 + +动态合图是按照 **渲染顺序** 来选取要将哪些贴图合并到一张大图中的,这样就能确保相邻的 DrawCall 能合并为一个 DrawCall(又称“合批”)。 + +## 启用、禁用动态合图 + +Cocos Creator 在初始化过程中,会根据不同的平台设置不同的 [CLEANUP_IMAGE_CACHE](%__APIDOC__%/zh/interface/Macro?id=CLEANUP_IMAGE_CACHE) 参数,当禁用 `CLEANUP_IMAGE_CACHE` 时,动态合图就会默认开启。
+启用动态合图会占用额外的内存,不同平台占用的内存大小不一样。目前在小游戏和原生平台上默认会禁用动态合图,但如果你的项目内存空间仍有富余的话建议开启。 + +**若希望强制开启动态合图**,请在代码中加入: + +```ts +macro.CLEANUP_IMAGE_CACHE = false; +DynamicAtlasManager.instance.enabled = true; +``` + +> **注意**:这些代码请写在项目脚本中的最外层,不要写在 `onLoad`/`start` 等类函数中,才能确保在项目加载过程中即时生效。否则如果在部分贴图缓存已经释放的情况下才启用动态图集,可能会导致报错。 + +**若希望强制禁用动态合图**,可以直接通过代码控制: + +```ts +dynamicAtlasManager.enabled = false; +``` + +## 贴图限制 + +动态合图系统限制了能够进行合图的贴图大小,默认只有贴图宽高都小于 **512** 的贴图才可以进入到动态合图系统。开发者可以根据需求修改这个限制: + +```ts +dynamicAtlasManager.maxFrameSize = 512; +``` + +详情可在 API 文档中查找 `DynamicAtlasManager` 进行参考。 diff --git a/versions/4.0/zh/advanced-topics/engine-customization.md b/versions/4.0/zh/advanced-topics/engine-customization.md new file mode 100644 index 0000000000..f084dd2d07 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/engine-customization.md @@ -0,0 +1,212 @@ +# 引擎定制工作流程 + +Cocos Creator 拥有两套引擎内核,C++ 内核 和 TypeScript 内核。C++ 内核用于原生平台,TypeScript 内核用于 Web 和小游戏平台。 在引擎内核之上,是用 TypeScript 编写的引擎框架层,用以统一两套内核的差异,让开发更便捷。 + +![engine-core](./engine-customization/engine-core-architecture.png) + +## 获取引擎源码 + +![download repo js](engine-customization/select-repo.png) + +### 1. 进入 Cocos 引擎仓库 + +Cocos 是一个开源引擎,不同版本的引擎源码可以从 Cocos 引擎官方开源仓库中获取,地址如下: + +- Cocos 引擎:[GitHub](https://github.com/cocos/cocos4/) | [GitEE](https://gitee.com/mirrors_cocos-creator/engine/) + +打开仓库主页,可以看到上图中左侧子图所示的内容,根据需求自行决定使用哪一个版本即可。 + +### 2. 选择分支(Branch) + +如果是要采用最新的分支,选择上图中间子图所示的 `develop` 分支即可。 + +> **注意**:此分支由于是开发中的最新分支,并不确保稳定性。 + +### 选择 Tag + +如果要采用对应 Cocos Creator 版本的稳定分支,则按上图右侧子图所示,切换到 Tags 标签,并选择对应的版本号。 比如,你所用的 Cocos Creator 为 3.7.3 版本,那么你就选择 3.7.3 即可。 + +> **注意**:不同的 Cocos Creator 版本,需要对应不同的引擎分支,请确保二者版本匹配。 + +### 3. Clone 或者 下载 + +![download-repo](engine-customization/download-repo.png) + +如上图所示,选择了对应的分支或者 Tag 后,就会切换到相应仓库(1)。 + +点击右边的 **代码**(2)按钮,会弹出代码下载面板。 + +你可以 **复制仓库链接**(3),然后使用 git 工具 clone 到本地,也可以选择 `Download ZIP`(4) 下载源码的 zip 包到本地并解压即可。 + +> 记得在页面右上角给 Cocos 引擎的开源仓库加个星(5),谢谢! + +## 配置环境 + +### 1. 引擎源码放到指定目录 + +根据自身项目需求,从 Cocos 引擎的源码仓库获得引擎源码,放到适合的目录下。 + +- 如果自定义引擎仅为某一个项目使用,则建议放入对应的项目目录。 +- 如果多个项目会共享一套自定义引擎,则根据项目管理方案,放到适合的公共目录。 + +### 2. 获取 external + +Cocos 原生引擎的编译,需要依赖 `cocos-engine/native/external` 中的库。这些库有几百 MB 大小,所以源码包内默认是没有的,可以通过以下三种方式获取。 + +**第一种方式**:通过 git 命令行。 + +```ts + cd cocos-engine/native + git clone https://github.com/cocos/cocos-engine-external external + cd external + git checkout -b branch_name tag +``` + +- branch_name:是新的分支名 +- tag:是第三方库对应的 tag 名字,可以从 cocos-engine/native/external-config.json 里的 checkout 字段获取该值。 + +> 示例: 假如我们要使用 3.7.3-1 版本,则可以写成 `git checkout -b 3.7.3-1 3.7.3-1` + +**第二种方式**:使用引擎自带的脚本下载。 + +该脚本只会下载某个 tag 对应的第三方库。 + +```ts + cd cocos-engine/native + npm install + gulp init +``` + +> 只要使用的 tag 有变化,就需要重新下载。每次需要下载几百兆的内容。 + +**第三种方式**:下载 ZIP 包 + +如果某些原因不能通过 NodeJS 和 git 命令下载 `external` 源码包,则可以直接在 [cocos-engine-external](https://github.com/cocos/cocos-engine-external) 仓库页面,按照以下步骤下载。 + +![download-external-zip](engine-customization/download-external-zip.png) + +1. 选择对应的分支,需要使用哪一个分支可以从 cocos-engine/native/external-config.json 中的 `checkout` 字段中查看。 +2. 点击右边的 **源代码(Code)** 按钮,弹出源码下载页面。 +3. 点击 **下载ZIP(Download ZIP)** 按钮,下载源码压缩包。 +4. 解压,重命名为 `external`,并放到 `cocos-engine/native` 目录下。 + +> **注意**:external 与 cocos-engine 的版本号要一致,否则可能导致编译失败。 + +### 3. 安装编译依赖 + +编译依赖项需要使用 `NodeJS`,请确保电脑安装了 `NodeJS v12.0` 以上版本,如未安装,请前往 [https://nodejs.org/](https://nodejs.org/) 下载安装。 + +安装完成后,在自定义引擎根目录,执行以下命令。 + +```bash +# 安装 gulp 构建工具 +npm install -g gulp +# 安装依赖的模块 +npm install +``` + +### 4. 修改引擎路径 + +通过 **Cocos Creator/File -> 偏好设置** 的 **引擎管理器** 选项卡来设置需要使用的引擎源码路径。需要注意的是 **修改引擎路径后需要重启编辑器**。 + +![custom ts engine](engine-customization/custom-engine.png) + +#### 指定 TypeScript 引擎 + +默认情况下,`Use Built-in` 会处于选中状态,表示使用的是 Cocos Creator 内置的引擎。 为了使用自定义引擎,我们只需要将选项卡切换到 `Use Custom`, 并将路径指定为引擎源码根目录即可。 + +#### 指定原生(C++)引擎 + +如果需要定制和原生平台相关的引擎功能, 则还需要勾选 `Native Module` 下的 `Use Custom`。 会自动识别自定义引擎根目录下的 `native` 作为路径,不可更改。 + +#### 配置存储路径 + +![custom-engine-store-path](engine-customization/custom-engine-store-path.png) + +默认情况下,自定义引擎的修改会保存为全局配置,将影响所有使用当前 Cocos Creator 版本的项目。 + +将鼠标移动到 `Cocos Engine` 文字上,文字前方会出现一个设置按钮,点击按钮可以选择存储位置。 + +#### 重启 Cocos Creator + +自定义引擎路径修改完成后,需要重启 Cocos Creator 编辑器才能生效。 直接关闭 Cocos Creator 再次打开,或者点击界面上的 **重启(Relaunch)** 按钮。 + +> **注意**:自定义引擎生效后,Cocos Creator 中的场景编辑器也会采用自定义引擎进行渲染。默认情况下采用的是 TypeScript 内核运行,如果要让编辑器也使用原生引擎进行渲染,需要在 **偏号设置** -> **实验室** 面板下,开启 **启用原生引擎加载场景编辑器**。 + +![use-native-engine-for-editor](engine-customization/use-native-engine-for-editor.png) + +## 修改 TypeScript 引擎 + +如果您只需要为 Web 和 小游戏平台的游戏定制引擎功能,或只需要修改 TypeScript 层的引擎相关代码(如 UI 系统,动画系统等),那么只需要在修改完成之后在 Cocos Creator 编辑器的菜单栏中点击 **开发者 -> 编译引擎** 进行编译即可。 + +![build](engine-customization/build-ts-engine.png) + +该命令会在引擎目录下生成一个 `bin` 文件夹,并将引擎源码编译到这里。 + +## 修改原生(C++)引擎 + +如果需要定制和原生平台相关的引擎功能,除了修改 TypeScript 代码,可能还需要修改 C++ 代码。 + +为了使原生部分的代码能够正常被编译,需要确保 `Native Module` 中的 `Use Custom` 被选中。 + +### 在 Cocos Creator 中编译 + +引擎修改完成后,打开 **构建发布** 面板,点击**构建**,构建完成后,点击 **编译**。 + +### 在 IDE 中编译 + +第一次配置完自定义引擎路径后,需要 **在构建发布** 面板点击 **构建** 按钮,才会同步相关配置到 IDE(如 Xcode, Android Studio, Visual Studio 等)中。 + +此时的 IDE 中引用的 `Cocos Engine` 路径将会是自定义引擎的路径。修改完原生相关代码后,直接使用 IDE 的编译即可。 + +## 重新编译原生引擎模拟器 + +```bash +cd cocos-engine/native +git clone https://github.com/cocos/cocos-engine-external external --branch tag +``` + +- `branch_name` 是新的分支名; + +- `tag` 是第三方库对应的 `tag` 名字,可以从 `cocos-engine/native/external-config.json` 里的 `checkout` 字段获取该值。 + +>使用引擎自带的脚本下载。该脚本只会下载某个 `tag` 对应的第三方库。后续只要使用的 `tag` 有变化,就需要重新下载。每次需要下载几百兆的内容。 + +Cocos Creator 提供了基于原生引擎的模拟器预览功能: + +![custom-native-simulator](engine-customization/custom-native-simulator.png) + +### 自定义 TypeScript 引擎 + +若使用了自定义 TypeScript 引擎,则这个原生模拟器在启动时,会去加载自定义引擎路径下的`自定义引擎路径/native/simulator` 的 TS 引擎模块。 + +首次启动时,需要点击编辑器顶部菜单栏中的 **开发者 -> 编译原生模拟器引擎**,否则会无法成功加载 TS 引擎模块。 + +### 自定义原生引擎 + +若勾选了自定义原生引擎,在运行模拟器时,Cocos Creator 会启动 `自定义引擎路径/native/simulator/` 路径下的模拟器应用程序。 + +为了避免安装包过大,Cocos Creator 在发布时剔除了原生引擎模拟器相关工程,需要按以下步骤配置环境,重新编译生成,否则无法启动: + +### CMake 安装与配置 + +> Cocos Creator 在构建、编译、发布应用时,使用的是自带的 CMake,并配置好了路径,不需要再做相关配置。 + +原生模拟器的编译不属于构建流程,因此需要自己配置 CMake 编译环境。 + +由于不同的系统平台和系统版本有差异,请参考 [CMake 官方文档](https://cmake.org/install/) 安装 CMake 并配置系统环境变量。 + +### 编译 + +`CMake` 配置完成后在 `自定义引擎路径/native` 目录下依次执行以下命令: + +```bash +# 安装依赖的模块 +npm install +# 生成原生模拟器相关文件 +gulp gen-simulator +``` + +编译完成后,会在 `自定义引擎路径/native/simulator` 路径下生成一个模拟器工程和模拟器可执行文件,便可运行原生模拟器了。 + +模拟器编译成功后,若后续只改动了 **TypeScript** 部分,则只需要点击编辑器顶部菜单栏中的 **开发者 -> 编译原生模拟器引擎** 即可。 diff --git a/versions/4.0/zh/advanced-topics/engine-customization/bin.png b/versions/4.0/zh/advanced-topics/engine-customization/bin.png new file mode 100644 index 0000000000..d459f2b6bb Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/bin.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/build-ts-engine.png b/versions/4.0/zh/advanced-topics/engine-customization/build-ts-engine.png new file mode 100644 index 0000000000..3877879b96 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/build-ts-engine.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/build.png b/versions/4.0/zh/advanced-topics/engine-customization/build.png new file mode 100644 index 0000000000..0a4673d45a Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/build.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/custom-engine-store-path.png b/versions/4.0/zh/advanced-topics/engine-customization/custom-engine-store-path.png new file mode 100644 index 0000000000..010a27e807 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/custom-engine-store-path.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/custom-engine.png b/versions/4.0/zh/advanced-topics/engine-customization/custom-engine.png new file mode 100644 index 0000000000..c34355fbf0 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/custom-engine.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/custom-native-engine.png b/versions/4.0/zh/advanced-topics/engine-customization/custom-native-engine.png new file mode 100644 index 0000000000..7ed3f1de28 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/custom-native-engine.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/custom-native-simulator.png b/versions/4.0/zh/advanced-topics/engine-customization/custom-native-simulator.png new file mode 100644 index 0000000000..b9e6adf35a Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/custom-native-simulator.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/custom-ts-engine.png b/versions/4.0/zh/advanced-topics/engine-customization/custom-ts-engine.png new file mode 100644 index 0000000000..7ed3f1de28 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/custom-ts-engine.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/download-external-zip.png b/versions/4.0/zh/advanced-topics/engine-customization/download-external-zip.png new file mode 100644 index 0000000000..2f9b226387 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/download-external-zip.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/download-repo-js.png b/versions/4.0/zh/advanced-topics/engine-customization/download-repo-js.png new file mode 100644 index 0000000000..e5338fbfad Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/download-repo-js.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/download-repo.png b/versions/4.0/zh/advanced-topics/engine-customization/download-repo.png new file mode 100644 index 0000000000..e7758ca928 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/download-repo.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/engine-core-architecture.png b/versions/4.0/zh/advanced-topics/engine-customization/engine-core-architecture.png new file mode 100644 index 0000000000..74fe96434f Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/engine-core-architecture.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/open-engine.png b/versions/4.0/zh/advanced-topics/engine-customization/open-engine.png new file mode 100644 index 0000000000..8f9e2c038b Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/open-engine.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/select-repo.png b/versions/4.0/zh/advanced-topics/engine-customization/select-repo.png new file mode 100644 index 0000000000..38559e4130 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/select-repo.png differ diff --git a/versions/4.0/zh/advanced-topics/engine-customization/use-native-engine-for-editor.png b/versions/4.0/zh/advanced-topics/engine-customization/use-native-engine-for-editor.png new file mode 100644 index 0000000000..124cc9aa80 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/engine-customization/use-native-engine-for-editor.png differ diff --git a/versions/4.0/zh/advanced-topics/hot-update-manager.md b/versions/4.0/zh/advanced-topics/hot-update-manager.md new file mode 100644 index 0000000000..c9ab998001 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/hot-update-manager.md @@ -0,0 +1,243 @@ +# 热更新管理器 AssetsManager + +这篇文档将全面覆盖热更新管理器 AssetsManager 的设计思路,技术细节以及使用方式。由于热更新机制的需求对于开发者来说可能各不相同,在维护过程中开发者也提出了各个层面的各种问题,说明开发者需要充分了解热更新机制的细节才能够定制出符合自己需要的工作流。所以这篇文档比较长,也尽力循序渐进得介绍热更新机制,但是并不会介绍过多使用层面的代码,对于想要先了解具体如何使用热更新机制来更新自己游戏的开发者,可以先尝试我们提供的一个 [简易教程](hot-update.md)。 + +## 资源热更新简介 + +资源热更新是为游戏运行时动态更新资源而设计的,这里的资源可以是图片,音频甚至游戏逻辑。在游戏漫长的运营维护过程中,你将可以上传新的资源到你的服务器,让你的游戏跟踪远程服务器上的修改,自动下载新的资源到用户的设备上。就这样,全新的设计,新的游玩体验甚至全新的游戏内容都将立刻被推送到你的用户手上。重要的是,你不需要针对各个渠道去重新打包你的应用并经历痛苦的应用更新审核! + +## 设计目标和基本原理 + +热更新机制本质上是从服务器下载需要的资源到本地,并且可以执行新的游戏逻辑,让新资源可以被游戏所使用。这意味着两个最为核心的目标:下载新资源,覆盖使用新逻辑和资源。同时,由于热更新机制最初在 Cocos2d-JS 中设计,我们考虑了什么样的热更新机制才更适合 Cocos 的 JavaScript 用户群。最终我们决定使用类似 Web 网页的更新模式来更新游戏内容,我们先看一下 Web 的更新模式: + +1. Web 页面在服务端保存完整的页面内容 +2. 浏览器请求到一个网页之后会在本地缓存它的资源 +3. 当浏览器重新请求这个网页的时候会查询服务器版本的最后修改时间(Last-Modified)或者是唯一标识(Etag),如果不同则下载新的文件来更新缓存,否则继续使用缓存 + +浏览器的缓存机制远比上面描述的要复杂,不过基本思路我们已经有了,那么对于游戏资源来说,也可以在资源服务器上保存一份完整的资源,客户端更新时与服务端进行比对,下载有差异的文件并替换缓存。无差异的部分继续使用包内版本或是缓存文件。这样我们更新游戏需要的就是: + +1. 服务端保存最新版本的完整资源(开发者可以随时更新服务器) +2. 客户端发送请求和服务端版本进行比对获得差异列表 +3. 从服务端下载所有新版本中有改动的资源文件 +4. 用新资源覆盖旧缓存以及应用包内的文件 + +这就是整个热更新流程的设计思路,当然里面还有非常多具体的细节,后面会结合实际流程进行梳理。这里需要特别指出的是: + +**Cocos 默认的热更新机制并不是基于补丁包更新的机制,传统的热更新经常对多个版本之间分别生成补丁包,按顺序下载补丁包并更新到最新版本。Cocos 的热更新机制通过直接比较最新版本和本地版本的差异来生成差异列表并更新**。这样即可天然支持跨版本更新,比如本地版本为 A,远程版本是 C,则直接更新 A 和 C 之间的差异,并不需要生成 A 到 B 和 B 到 C 的更新包,依次更新。所以,在这种设计思路下,新版本的文件以离散的方式保存在服务端,更新时以文件为单位下载。 + +## 热更新基本流程 + +在理解了上面基本的设计思路之后,我们来看一次典型的热更新流程。我们使用 manifest 资源描述文件来描述本地或远程包含的资源列表及资源版本,manifest 文件的定义会在后面详述。运行环境假定为用户安装好 app 后,第一次检查到服务端的版本更新: + +![](hot-update/assets-manager.png) + +上图分为三个部分,中间是热更新的流程,左边是更新过程中 AssetsManager 向用户发送的消息,右边则是各个步骤产出的中间结果,其中粗体字表示中间结果所在的位置,比如内存中、临时文件夹中或者是缓存文件夹。 + +相信看完这张图还是有很多疑问,下面会从细节上来解析各个步骤中需要注意或者不容易理解的地方。 + +## 技术细节解析 + +### Manifest 格式 + +Manifest 格式是我们用来比较本地和远程资源差异的一种 json 格式,其中保存了主版本信息、引擎版本信息、资源列表及资源信息等: + +``` +{ + "packageUrl" : 远程资源的本地缓存根路径 + "remoteVersionUrl" : [可选项] 远程版本文件的路径,用来判断服务器端是否有新版本的资源 + "remoteManifestUrl" : 远程资源 Manifest 文件的路径,包含版本信息以及所有资源信息 + "version" : 资源的版本 + "engineVersion" : 引擎版本 + "assets" : 所有资源列表 + "key" : 资源的相对路径(相对于资源根目录) + "md5" : md5 值代表资源文件的版本信息 + "compressed" : [可选项] 如果值为 true,文件被下载后会自动被解压,目前仅支持 zip 压缩格式 + "size" : [可选项] 文件的字节尺寸,用于快速获取进度信息 + "searchPaths" : 需要添加到 FileUtils 中的搜索路径列表 +} +``` + +Manifest 文件可以通过 Cocos Creator 热更新范例中的 `version_generator.js`([GitHub](https://github.com/cocos-creator/tutorial-hot-update/blob/master/version_generator.js) | [Gitee](https://gitee.com/mirrors_cocos-creator/tutorial-hot-update/blob/master/version_generator.js))来自动生成。 + +这里需要注意的是,remote 信息(包括 `packageUrl`、`remoteVersionUrl`、`remoteManifestUrl`)是该 manifest 所指向远程包信息,也就是说,当这个 manifest 成为本地包或者缓存 manifest 之后,它们才有意义(偷偷透露个小秘密,更新版本时更改远程包地址也是一种玩法呢)。另外,md5 信息可以不是文件的 md5 码,也可以是某个版本号,这完全是由用户决定的,本地和远程 manifest 对比时,只要 md5 信息不同,我们就认为这个文件有改动。 + +### 工程资源和游戏包内资源的区别 + +大家在创建一个 Cocos Creator 工程的时候,可以看到它的目录下有 assets 目录,里面保存了你的场景、脚本、prefab 等,对应编辑器中的**资源管理器**面板。但是这些工程资源并不等同于打包后的资源,在使用**构建发布**面板构建原生版本时,我们会在构建目录下找到 assets 和 src 文件夹,这两个文件夹内保存的才是真正让游戏运行起来的游戏包内资源。其中 src 包含引擎脚本,assets 包含其他资源。 + +所以我们的资源热更新自然应该更新构建出来的资源,而不是工程的 assets 目录。 + +### 包内资源、本地缓存资源和临时资源 + +在开发者的游戏安装到用户的手机上时,它的游戏是以 .ipa(iOS)或者 .apk(Android)形式存在的,这种应用包在安装后,它的内容是无法被修改或者添加的,应用包内的任何资源都会一直存在。所以热更新机制中,我们只能更新本地缓存到手机的可写目录下(应用存储空间或者 SD 卡指定目录),并通过 FileUtils 的搜索路径机制完成本地缓存对包内资源的覆盖。同时为了保障更新的可靠性,我们在更新过程中会首先将新版本资源放到一个临时文件夹中,只有当本次更新正常完成,才会替换到本地缓存文件夹内。如果中途中断更新或者更新失败,此时的失败版本都不会污染现有的本地缓存。这一步骤在上一章节的流程图中有详细介绍: + +![](hot-update/am-part2.png) + +在长期多次更新的情况下,本地缓存会一直被替换为最新的版本,而应用包只有等到用户在应用商店中更新到新版本才会被修改。 + +### 进度信息 + +在前面章节的流程图中,可以看到热更新管理器有发送 `UPDATE_PROGRESSION` 消息给用户。目前版本中,用户可以接收到下面进度信息: + +1. 字节级进度(百分比) +2. 文件级进度(百分比) +3. 已接收到的字节数 +4. 总字节数 +5. 已接收到的文件数 +6. 总文件数 + +```js +function updateCb (event) { + switch (event.getEventCode()) + { + case native.EventAssetsManager.UPDATE_PROGRESSION: + log("Byte progression : " + event.getPercent() / 100); + log("File progression : " + event.getPercentByFile() / 100); + log("Total files : " + event.getTotalFiles()); + log("Downloaded files : " + event.getDownloadedFiles()); + log("Total bytes : " + event.getTotalBytes()); + log("Downloaded bytes : " + event.getDownloadedBytes()); + break; + } +} +``` + +### 断点续传 + +肯定有开发者会问,如果在更新过程中网络中断会怎么样?答案是热更新管理器支持断点续传,并且同时支持文件级别和字节级别的断点续传。 + +那么具体是怎么做的呢?首先我们使用 Manifest 文件来标识每个资源的状态,比如未开始、下载中、下载成功,在热更新过程中,文件下载完成会被标识到内存的 Manifest 中,当下载完成的文件数量每到一个进度节点(默认以 10% 为一个节点)都会将内存中的 Manifest 序列化并保存到临时文件夹中。具体的步骤展示在流程图多线程并发下载资源部分: + +![am-part1.png](hot-update/am-part1.png) + +在中断之后,再次启动热更新流程时,会去检查临时文件夹中是否有未完成的更新,校验版本是否和远程匹配后,则直接使用临时文件夹中的 Manifest 作为 Remote Manifest 继续更新。此时,对于下载状态为已完成的,不会重新下载,对于下载中的文件,会尝试发送续传请求给服务器(服务器需要支持 `Accept-Ranges`,否则从头开始下载)。 + +> **注意**:自 v3.6 版本起,iOS 和 Android 平台也支持断点续传。 + +### 控制并发 + +热更新管理器提供了控制下载并发数量的 API,使用方式如下: + +```js +assetsManager.setMaxConcurrentTask(10); +``` + +### 版本对比函数 + +热更新流程中很重要的步骤是比较客户端和服务端的版本,默认情况下只有当服务端主版本比客户端主版本更新时才会去更新。引擎中实现了一个支持 x.x.x.x 四个序列版本的对比函数(x 为纯数字),不符合这种版本号模式的情况下会继续使用字符串比较函数。 + +除此之外,我们还允许用户使用自己的版本对比函数,使用方法如下: + +```js +// versionA 和 versionB 都是字符串类型 +assetsManager.setVersionCompareHandle(function (versionA, versionB) { + var sub = parseFloat(versionA) - parseFloat(versionB); + // 当返回值大于 0 时,versionA > versionB + // 当返回值等于 0 时,versionA = versionB + // 当返回值小于 0 时,versionA < versionB + return sub; +}); +``` + +### 下载后文件校验 + +由于下载过程中仍然有小概率可能由于网络原因或其他网络库的问题导致下载的文件内容有问题,所以我们提供了用户文件校验接口,在文件下载完成后热更新管理器会调用这个接口(用户实现的情况下),如果返回 true 表示文件正常,返回 false 表示文件有问题。 + +```js +assetsManager.setVerifyCallback(function (filePath, asset) { + var md5 = calculateMD5(filePath); + if (md5 === asset.md5) + return true; + else + return false; +}); +``` + +由于 Manifest 中的资源版本建议使用 md5 码,那么在校验函数中计算下载文件的 md5 码去和 asset 的 md5 码对比即可判断文件是否正常。除了 md5 信息之外,asset 对象还包含下面的属性: + +| 属性 | 说明 | +| :---- | :---- | +| path | 服务器端相对路径 | +| compressed | 是否被压缩 | +| size | 文件尺寸 | +| downloadState | 下载状态,包含 `UNSTARTED`、`DOWNLOADING`、`SUCCESSED`、`UNMARKED` | + +### 错误处理和失败重试 + +在流程图的左侧,大家应该注意到了不少的用户消息,这些用户消息都是可以通过热更新的事件监听器来获得通知的,具体可以参考 **范例**([GitHub](https://github.com/cocos-creator/tutorial-hot-update/blob/master/assets/hotupdate/HotUpdate.ts) | [Gitee](https://gitee.com/mirrors_cocos-creator/tutorial-hot-update/blob/master/assets/hotupdate/HotUpdate.ts))。流程图标识了所有错误信息的触发时机和原因,开发者可以根据自己的系统设计来做出相应的处理。 + +最重要的就是当下载过程中出现异常,比如下载失败、解压失败、校验失败,最后都会触发 `UPDATE_FAILED` 事件。而所有下载失败的资源列表会被记录在热更新管理器中,可以通过以下方式下载重试: + +```js +assetsManager.downloadFailedAssets(); +``` + +这个接口调用之后,会重新进入热更新流程,仅下载之前失败的资源。该流程和正常的热更新流程是一致的。 + +### 重启的必要性 + +如果要使用热更新之后的资源,需要重启游戏。有两个原因,第一是更新之后的脚本需要干净的 JS 环境才能正常运行。第二是场景配置,AssetManager 中的配置都需要更新到最新才能够正常加载场景和资源。 + +1. JS 脚本的刷新 + + 在热更新之前,游戏中的所有脚本已经执行过了,所有的类、组件、对象已经存在 JS context 中。所以热更新之后如果不重启游戏就直接加载脚本,同名的类和对象虽然会被覆盖,但是之前旧的类创建的对象是一直存在的。而被直接覆盖的全局对象,原先的状态也被重置了,就会导致新版本和旧版本的对象混杂在一起。并且对内存也会造成额外开销。 + +2. 资源配置的刷新 + + 因为 Creator 的场景和资源都依赖于 [Asset Bundle](../asset/bundle.md)。如果 Asset Bundle 没有被重新加载,并被 [Asset Manager](../asset/asset-manager.md) 重新读取,那么游戏中是加载不到热更新后的场景和资源的。 + +而如何启用新的资源,就需要依赖 Cocos 引擎的搜索路径机制了。Cocos 中所有文件的读取都是通过 FileUtils,而 FileUtils 会按照搜索路径的优先级顺序查找文件。所以我们只要将热更新的缓存目录添加到搜索路径中并且前置,就会优先搜索到缓存目录中的资源。以下是示例代码: + +```js +import {NATIVE} from 'cc/env'; +// ... +if (NATIVE) { + // 创建 AssetsManager + var assetsManager = new native.AssetsManager(manifestUrl, storagePath); + // 初始化后的 AssetsManager 的 local manifest 就是缓存目录中的 manifest + var hotUpdateSearchPaths = assetsManager.getLocalManifest().getSearchPaths(); + // 默认的搜索路径 + var searchPaths = native.fileUtils.getSearchPaths(); + + // hotUpdateSearchPaths 会前置在 searchPaths 数组的开头 + Array.prototype.unshift.apply(searchPaths, hotUpdateSearchPaths); + + native.fileUtils.setSearchPaths(searchPaths); +} +``` + +需要注意的是,这段代码必须放在 `main.js` 中 require 其他脚本之前执行,否则还是会加载到应用包内的脚本。 + +## 进阶主题 + +上面的章节介绍了热更新管理器的大部分实现和使用细节,应该可以解答开发者的大多数疑问。不过在一些特殊的应用场景下,可能需要一些特殊的技巧来避免热更新引发的问题。 + +### 迭代升级 + +对于游戏开发者来说,热更新是比较频繁的需求,从一个大版本升级到另一个大版本的过程中,中间可能会发布多个热更新版本。那么下面两个问题是开发者比较关心的: + +1. 在本地缓存覆盖过程中会发生什么问题 + + 当用户环境中已经包含一个本地缓存版本时,热更新管理器会比较缓存版本和应用包内版本,使用较新的版本作为本地版本。如果此时远程版本有更新,热更新管理器在更新过程中,按照正常流程会使用临时文件夹来下载远程版本。当远程版本更新成功后,临时文件夹中的内容会被复制到本地缓存文件夹中。如果本地缓存文件夹中有同名文件则直接覆盖,而其他文件则保留,因为这些文件仍然可能是有效的,只是它们没有在这次版本中被修改。最后删除临时文件夹。 + + 所以理论上小版本的持续热更新不会遇到什么问题。 + +2. 游戏大版本更新过程中,如何清理本地缓存 + + 在游戏包更新过程中,若要彻底清理本地的热更新缓存有很多种做法,比如可以记录当前的游戏版本号,检查与 `sys.localStorage` 中保存的版本是否匹配,如果两者不匹配则执行以下清理操作: + + ```js + // 之前保存在 local Storage 中的版本号,如果没有,则认为是新版本 + var previousVersion = parseFloat( sys.localStorage.getItem('currentVersion') ); + // game.currentVersion 为当前版本的版本号 + if (previousVersion < game.currentVersion) { + // 热更新的储存路径,如果旧版本中有多个,可能需要记录在列表中,全部清理 + native.fileUtils.removeDirectory(storagePath); + } + ``` + +### 更新引擎 + +升级游戏使用的引擎版本可能会对热更新产生巨大影响,开发者们可能有注意到在原生项目中存在 `src/cocos-js/cc.js` 文件,这个文件是 JS 引擎编译出来的。在不同版本的引擎中,它的代码会产生比较大的差异,而 C++ 底层也会随之发生一些改变。这种情况下,如果游戏包内的 C++ 引擎版本和 `src/cocos-js/cc.js` 的引擎版本不一致,可能会导致严重的问题,甚至游戏完全无法运行。 + +建议更新引擎之后,尽量推送大版本到应用商店。如果仍采用热更新方案,请一定要仔细完成各个旧版本更新到新版本的测试。 diff --git a/versions/4.0/zh/advanced-topics/hot-update.md b/versions/4.0/zh/advanced-topics/hot-update.md new file mode 100644 index 0000000000..22f1d22081 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/hot-update.md @@ -0,0 +1,148 @@ +# 资源热更新教程 + +## 前言 + +之所以这篇文档的标题为教程,是因为目前 Cocos Creator 资源热更新的工作流还没有彻底集成到编辑器中,不过引擎本身对于热更新的支持是完备的,所以借助一些外围脚本和一些额外的工作就可以达成。 + +本篇文档的范例工程可以从 [GitHub](https://github.com/cocos-creator/tutorial-hot-update/tree/master) 获取(master 分支)。 + +![hot update](./hot-update/title.jpg) + +## 使用场景和设计思路 + +资源热更新的使用场景相信游戏开发者都非常熟悉,对于已发布的游戏,在游戏内通过从服务器动态下载新的游戏内容,来时刻保持玩家对游戏的新鲜感,是保持一款游戏长盛不衰非常重要的手段。当然热更新还有一些其他的用途,不过在此不再深入讨论,我们下面将主要讨论 Cocos Creator 对热更新支持的原理和手段。 + +Cocos Creator 中的热更新主要源于 Cocos 引擎中的 AssetsManager 模块对热更新的支持。它有个非常重要的特点: + +**服务端和本地均保存完整版本的游戏资源**,热更新过程中通过比较服务端和本地版本的差异来决定更新哪些内容。这样即可天然支持跨版本更新,比如本地版本为 A,远程版本是 C,则直接更新 A 和 C 之间的差异,并不需要生成 A 到 B 和 B 到 C 的更新包,依次更新。所以,在这种设计思路下,新版本的文件以离散的方式保存在服务端,更新时以文件为单位下载。 + +除此之外,由于 WEB 版本可以通过服务器直接进行版本更新,**所以资源热更新只适用于原生发布版本**。AssetsManager 类也只在 native 命名空间下,在使用的时候需要注意判断运行环境。 + +## Manifest 文件 + +对于不同版本的文件级差异,AssetsManager 中使用 Manifest 文件来进行版本比对。本地和远端的 Manifest 文件分别标示了本地和远端的当前版本包含的文件列表和文件版本,这样就可以通过比对每个文件的版本来确定需要更新的文件列表。 + +Manifest 文件中包含以下几个重要信息: + +1. 远程资源包的根路径 +2. 远程 Manifest 文件地址 +3. 远程 Version 文件地址(非必需) +4. 主版本号 +5. 文件列表:以文件路径来索引,包含文件版本信息,一般推荐用文件的 md5 校验码来作为版本号 +6. 搜索路径列表 + +其中 Version 文件内容是 Manifest 文件内容的一部分,不包含文件列表。由于 Manifest 文件可能比较大,每次检查更新的时候都完整下载的话可能影响体验,所以开发者可以额外提供一个非常小的 Version 文件。AssetsManager 会首先检查 Version 文件提供的主版本号来判断是否需要继续下载 Manifest 文件并更新。 + +## 在 Cocos Creator 项目中支持热更新 + +在这篇教程中,将提出一种针对 Cocos Creator 项目可行的热更新方案,我们也开放了 Downloader 的 JavaScript 接口,用户可以自由开发自己的热更新方案。 +对于 Cocos Creator 来说,引擎脚本将会打包到 src 目录中,其他 Assets 资源将会被导出到 assets 目录。 + +基于这样的项目结构,本篇教程中的热更新思路很简单: + +1. 基于原生打包目录中的 assets 和 src 目录生成本地 Manifest 文件。 +2. 创建一个热更新组件来负责热更新逻辑。 +3. 游戏发布后,若需要更新版本,则生成一套远程版本资源,包含 assets 目录、src 目录和 Manifest 文件,将远程版本部署到服务端。 +4. 当热更新组件检测到服务端 Manifest 版本不一致时,就会开始热更新 + +![](./hot-update/table.png) + +### 使用 Version Generator 来生成 Manifest 文件 + +在范例工程中,我们提供了一个 **version_generator.js**([GitHub](https://github.com/cocos-creator/tutorial-hot-update/blob/master/version_generator.js) | [Gitee](https://gitee.com/mirrors_cocos-creator/tutorial-hot-update/blob/master/version_generator.js)),这是一个用于生成 Manifest 文件的 NodeJS 脚本。使用方式如下: + +``` +> node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/ +``` + +下面是参数说明: + +- `-v` 指定 Manifest 文件的主版本号。 +- `-u` 指定服务器远程包的地址,这个地址需要和最初发布版本中 Manifest 文件的远程包地址一致,否则无法检测到更新,。 +- `-s` 本地原生打包版本的目录相对路径, 比如 ./build/android/assets。 +- `-d` 保存 Manifest 文件的相对路径。 + +### 热更新组件 + +在范例工程中,热更新组件的实现位于 `assets/hotupdate/HotUpdate.ts`([GitHub](https://github.com/cocos-creator/tutorial-hot-update/blob/master/assets/hotupdate/HotUpdate.ts) | [Gitee](https://gitee.com/mirrors_cocos-creator/tutorial-hot-update/blob/master/assets/hotupdate/HotUpdate.ts))中,开发者可以参考这种实现,也可以自由的按自己的需求修改。 + +除此之外,范例工程中还搭配了一个 `Scene/Canvas/update` 节点用于提示更新和显示更新进度供参考。 + +![](./hot-update/editor.png) + +### 部署远程服务器 + +为了让游戏可以检测到远程版本,可以在本机上模拟一个远程服务器,搭建服务器的方案多种多样(比如 Python [SimpleHTTPServer](https://docs.python.org/2/library/simplehttpserver.html)),这里不做讨论,开发者可以使用自己习惯的方式。搭建成功后,访问远程包和 Manifest 文件的地址与范例工程中不同,所以需要修改以下几个地方来让游戏可以成功找到远程包: + +1. `assets/project.manifest`:游戏的本地 Manifest 文件中的 `packageUrl`、`remoteManifestUrl` 和 `remoteVersionUrl` +2. `remote-assets/project.manifest`:远程包的 Manifest 文件中的 `packageUrl`、`remoteManifestUrl` 和 `remoteVersionUrl` +3. `remote-assets/version.manifest`:远程包的 Version 文件中的 `packageUrl`、`remoteManifestUrl` 和 `remoteVersionUrl` + +> `remote-assets` 代表远程服务端资源目录。 + +### 打包原生版本 + +下载完成范例工程后,可以用 Cocos Creator 直接打开这个工程。打开 **构建发布** 面板,构建原生版本,建议使用 Windows / Mac 来测试。 + +> **注意**: +> 1. 构建时请不要勾选 MD5 Cache,否则会导致热更新无效。 +> 2. 并且应该确保在工程目录的 extensions 文件夹里导入 hot-update 编辑器插件(范例工程里已经导入了该插件) + +该编辑器插件会在每次构建结束后,自动给 `main.js` 附加上搜索路径设置的逻辑和更新中断修复代码: + +```js +// 在 main.js 的开头添加如下代码 +(function () { + if (typeof window.jsb === 'object') { + var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths'); + if (hotUpdateSearchPaths) { + var paths = JSON.parse(hotUpdateSearchPaths); + jsb.fileUtils.setSearchPaths(paths); + + var fileList = []; + var storagePath = paths[0] || ''; + var tempPath = storagePath + '_temp/'; + var baseOffset = tempPath.length; + + if (jsb.fileUtils.isDirectoryExist(tempPath) && !jsb.fileUtils.isFileExist(tempPath + 'project.manifest.temp')) { + jsb.fileUtils.listFilesRecursively(tempPath, fileList); + fileList.forEach(srcPath => { + var relativePath = srcPath.substr(baseOffset); + var dstPath = storagePath + relativePath; + + if (srcPath[srcPath.length] == '/') { + jsb.fileUtils.createDirectory(dstPath) + } + else { + if (jsb.fileUtils.isFileExist(dstPath)) { + jsb.fileUtils.removeFile(dstPath) + } + jsb.fileUtils.renameFile(srcPath, dstPath); + } + }) + jsb.fileUtils.removeDirectory(tempPath); + } + } + } +})(); +``` + +> **注意**: 这里的 `fileUtils` 位于传统的 `window.jsb` 中, 而非 `native`. 目前的脚本编译系统只支持在 TypeScript 中通过 `import` 导入 `native` 对象. + +这一步是必须要做的原因是,热更新的本质是用远程下载的文件取代原始游戏包中的文件。Cocos 引擎的搜索路径恰好满足这个需求,它可以用来指定远程包的下载地址作为默认的搜索路径,这样游戏运行过程中就会使用下载好的远程版本。另外,这里搜索路径是在上一次更新的过程中使用 `localStorage`(它符合 WEB 标准的 [Local Storage API](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/localStorage))固化保存在用户机器上,`HotUpdateSearchPaths` 这个键值是在 `HotUpdate.js` 中指定的,保存和读取过程使用的名字必须匹配。 + +此外,打开工程过程中如果遇到这个警告可以忽略:`loader for [.manifest] not exists!`。 + +### 运行范例工程 + +如果一切正常,此时运行原生版本的范例工程,就会发现检测到新版本,提示更新,更新之后会自动重启游戏,此时会进入新版本场景。 + +![](./hot-update/update.png) + +## 结语 + +以上介绍的是目前一种可能的热更新方案,Cocos Creator 在未来版本中提供更成熟的热更新方案,直接集成到编辑器中。当然,也会提供底层 Downloader API 来允许用户自由实现自己的热更新方案,并通过插件机制在编辑器中搭建完整可视化的工作流。这篇教程和范例工程提供给大家参考,也鼓励开发者针对自己的工作流进行定制。如果有问题和交流也欢迎反馈到 [论坛](https://forum.cocos.org/c/Creator/58)。 + +## Next Step + +[热更新管理器文档](hot-update-manager.md) diff --git a/versions/4.0/zh/advanced-topics/hot-update/am-part1.png b/versions/4.0/zh/advanced-topics/hot-update/am-part1.png new file mode 100644 index 0000000000..e6c585daa7 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/hot-update/am-part1.png differ diff --git a/versions/4.0/zh/advanced-topics/hot-update/am-part2.png b/versions/4.0/zh/advanced-topics/hot-update/am-part2.png new file mode 100644 index 0000000000..03c5cbcc9f Binary files /dev/null and b/versions/4.0/zh/advanced-topics/hot-update/am-part2.png differ diff --git a/versions/4.0/zh/advanced-topics/hot-update/assets-manager.png b/versions/4.0/zh/advanced-topics/hot-update/assets-manager.png new file mode 100644 index 0000000000..79229290d4 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/hot-update/assets-manager.png differ diff --git a/versions/4.0/zh/advanced-topics/hot-update/editor.png b/versions/4.0/zh/advanced-topics/hot-update/editor.png new file mode 100644 index 0000000000..89a4aa7d8b Binary files /dev/null and b/versions/4.0/zh/advanced-topics/hot-update/editor.png differ diff --git a/versions/4.0/zh/advanced-topics/hot-update/table.png b/versions/4.0/zh/advanced-topics/hot-update/table.png new file mode 100644 index 0000000000..bb0527bc63 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/hot-update/table.png differ diff --git a/versions/4.0/zh/advanced-topics/hot-update/title.jpg b/versions/4.0/zh/advanced-topics/hot-update/title.jpg new file mode 100644 index 0000000000..a86dcc07d4 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/hot-update/title.jpg differ diff --git a/versions/4.0/zh/advanced-topics/hot-update/update.png b/versions/4.0/zh/advanced-topics/hot-update/update.png new file mode 100644 index 0000000000..8b886d5622 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/hot-update/update.png differ diff --git a/versions/4.0/zh/advanced-topics/http.md b/versions/4.0/zh/advanced-topics/http.md new file mode 100644 index 0000000000..4bf57b3caf --- /dev/null +++ b/versions/4.0/zh/advanced-topics/http.md @@ -0,0 +1,63 @@ +# HTTP 请求 + +在某些情况下,可能需要向网络后端请求一些数据,在 Cocos Creator 里面可以通过 `fetch` 方法,`fetch` 方法是 JavaScript 的一部分: + +```ts +declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise; +``` + +## 搭建服务 + +首先通过自身熟悉的后端语言搭建一个简易的 http 服务,非后端开发人员可忽略本段。本示例中采用 golang 搭建,代码示例如下: + +```go +package main + +import ( +"fmt" +"log" +"net/http" +) + +func handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "http request received") +} + +func main() { + http.HandleFunc("/", handler) + log.Fatal(http.ListenAndServe(":8080", nil)) +} +``` + +上述代码会搭建一个监听在 8080 端口的网络服务器。 + +## 创建 Http 请求 + +在 Cocos Creator 里面通过 `fetch` 方法向服务器请求数据,此处以 `GET` 方法为例,并以文本格式返回服务器的数据,代码示例如下: + +```ts +import { _decorator, Component } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('HttpTest') +export class HttpTest extends Component { + start() { + + fetch("http://127.0.0.1:8080").then((response: Response) => { + return response.text() + }).then((value) => { + console.log(value); + }) + } +} +``` + +运行场景后会打印如下日志: + +```bash +> http request received. +``` + +也可通过 `response.json()` 来获取 JSON 格式的返回。 + +您可以通过 [MDN Web Doc 社区](https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch) 来查看更多详细的信息。 diff --git a/versions/4.0/zh/advanced-topics/i18n.md b/versions/4.0/zh/advanced-topics/i18n.md new file mode 100644 index 0000000000..c2d41f5c44 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/i18n.md @@ -0,0 +1,153 @@ +# i18n 游戏多语言支持 + +Cocos Creator 从 3.1 版本起,提供了 i18n 游戏多语言扩展插件,目前支持 Label 和 Sprite 组件,可在 Cocos Store 中下载使用。 + +> **注意**:从 v3.6 起,引擎已内置 [本地化(L10N)](../editor/l10n/overview.md) 提供功能更丰富、强大的多语言功能。如果您使用的是 v3.6 之后的版本我们建议您使用该功能。 + +## 什么是 i18n + +**i18n** 是 **国际化** 的简称,来源是英文单词 **internationalization** 的首末字符 **i** 和 **n**,18 为单词中间的字符数。在资讯领域,国际化(i18n)可以让产品无需做大的改变,就能够满足不同语言和地区的需要。对程序来说,在不修改内部代码的情况下,就能根据不同语言及地区切换相应的语言界面。如今全球化的时代,国际化尤为重要,因为产品的潜在用户可能来自世界的各个角落。 + +通常与 i18n 相关的还有 L10n(“本地化”的简称),多语言国际化和本地化的区别在于: + +- **国际化** 需要软件里包括多种语言的文本和图片数据,并根据用户所用设备的默认语言或菜单选择进行实时切换。 +- **本地化** 是在发布软件时针对某一特定语言的版本定制文本和图片内容。 + +> **注意**:i18n 游戏多语言扩展插件不提供构建项目时的多语言资源管理,若面向不同地区发布项目,需要移除一部分多语言资源时,请手动处理。 + +## 安装 + +1. 点击 Creator 顶部菜单栏中的 **扩展 -> 商城** 进入 **Cocos Store**,查找 [i18n 多语言化扩展插件](http://store.cocos.com/app/detail/1865) 或者直接在上方搜索框中搜索: + + ![i18n-search](i18n/i18n-search.png) + +2. 单击进入详情页,可以看到 i18n 的基本信息、资源介绍和历史版本记录。然后点击 **下载** 按钮: + + ![i18n-store](i18n/i18n-store.png) + +3. 下载完成后会显示在右侧自动打开的下载历史栏中,继续点击下图中的安装按钮,然后选择扩展插件安装位置(全局/项目)即可。 + + > **注意**:若是首次安装 i18n 多语言化扩展插件,安装完成后需要重启 Creator。 + + ![i18n-install](i18n/i18n-install.png) + +4. 安装完成后刷新 **资源管理器** 可以看到新增了 `i18n` 文件夹及几个脚本,用于后续使用扩展插件。 + + ![i18n-script](i18n/i18n-script.png) + +## 使用 + +### 多语言配置 + +i18n 扩展插件安装完成后,会在 Creator 顶部菜单栏的 **扩展** 中生成 **I18n Settings** 菜单,点击即可打开 **本地化控制面板**,进行多语言配置。 + +![i18n-menu](i18n/i18n-menu.png) + +点击面板左上角的 `+` 按钮,在下方新增的输入框中输入需要新增的语言 ID。语言 ID 可自定义,但不支持使用符号,例如 `zh-CN` 请写为 `zhCN`。 + +![i18n-langu](i18n/i18n-langu.png) + +首个新增语言的左侧默认会有一个 ![i18n-display](i18n/i18n-display.png) 按钮,表示作为预览项目时显示的语言;之后新增的语言则默认为 ![i18n-undisplay](i18n/i18n-undisplay.png),表示不作为在预览项目时显示的语言,点击按钮即可切换语言显示状态。 +当鼠标悬浮在任意语言上时,对应语言右侧会出现一个删除按钮,用于删除当前选中的语言。 + +语言新增完成后,在项目的 `assets\resources\i18n` 目录下会生成相应的语言配置模板,用于配置需要多语言化的 Label,详情请参考下文介绍。 + +![i18n-langu-file](i18n/i18n-langu-file.png) + +### 本地化 Label + +打开语言配置模板,例如项目目录下的 `assets\resources\i18n\zh.ts`,添加 "test" 字段,并在其中定义 "main" 和 "hello",用于后续在编辑器中调用当前语言下要显示的文本内容的 key: + +```typescript +const win = window as any; + +export const languages = { + "test": { + "main": "测试", + "hello": "你好", + } +}; + +if (!win.languages) { + win.languages = {}; +} + +win.languages.zh = languages; +``` + +然后回到 Creator,在 **层级管理器** 选中 Label 节点,然后点击 **属性检查器** 下方的 **添加组件** 按钮,选择 **自定义脚本 -> LocalizedLabel**,将脚本挂载到节点上。 + +或者也可以直接将 `LocalizedLabel.ts` 从 **资源管理器** 拖拽到 **属性检查器**。 + +![i18n-localizedlable](i18n/i18n-localizedlable.png) + +然后在 **LocalizedLabel** 脚本组件的 Key 属性框中输入刚刚在 `zh.ts` 中配置的 `test.main` 或者 `test.hello`: + +![i18n-label](i18n/i18n-label.gif) + +之后可参照 `zh.ts` 的配置对 `assets\resources\i18n\en.ts` 进行配置,配置完成后即可在 **本地化控制** 面板切换语言显示,如下所示: + +![i18n-label-change](i18n/i18n-label-change.gif) + +> 注意:若需要修改 Label 渲染的文字,请直接修改刚才配置的语言脚本的 key 值,不要直接在 Label 组件中修改 string 属性。 + +### 本地化 Sprite 图片 + +在 Creator 的 **层级管理器** 选中 Sprite 节点,然后点击 **属性检查器** 下方的 **添加组件** 按钮,选择 **自定义脚本 -> LocalizedSprite**,即可在 Sprite 节点上挂载 `LocalizedSprite` 脚本组件: + +![i18n-sprite1](i18n/i18n-sprite1.png) + +然后在 **SpriteList** 属性框中设置语言数量,填写对应的 **语言 ID** 并拖入对应的 **图片资源**。例如下图设置为中/英文两种语言: + +![i18n-sprite2](i18n/i18n-sprite2.gif) + +配置完成后即可在 **本地化控制** 面板中切换语言显示。 + +![i18n-sprite3](i18n/i18n-sprite3.gif) + +### 动态切换语言 + +在游戏运行时,若需要动态切换语言,可以调用 `i18n.init(language)` 方法。若语言切换后,需要马上更新当前场景,则需要再调用 `i18n.updateSceneRenderer()` 方法。 + +```typescript +changeLang() { + if (i18n._language === 'en') { + i18n.init('zh'); + } else { + i18n.init('en'); + } + i18n.updateSceneRenderers(); +} +``` + +> **注意**:请确保运行时 `i18n.init(language)` 的执行是在包含有 `LocalizedLabel` 组件的场景加载完成之后。 + +我们以在场景中添加一个按钮来控制中/英文的切换为例,需新建一个脚本挂载在 Button 节点上。然后在脚本中将 `changeLang` 方法绑定在按钮点击事件中,例如: + +```typescript +import { _decorator, Component, Node, Button } from 'cc'; +const { ccclass, property } = _decorator; +// import * as i18n from '../extensions/i18n/assets/LanguageData' +import * as i18n from 'db://i18n/LanguageData'; + +@ccclass('Button') +export class Button extends Component { + + start() { + this.node.on(Button.EventType.CLICK, this.changeLang, this); + } + + changeLang() { + if (i18n._language === 'en') { + i18n.init('zh'); + } else { + i18n.init('en'); + } + i18n.updateSceneRenderers(); + } +} +``` + +代码编写完成后回到 Creator,点击预览即可看到效果如下图: + +![i18n-changelang](i18n/i18n-changelang.gif) diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-changelang.gif b/versions/4.0/zh/advanced-topics/i18n/i18n-changelang.gif new file mode 100644 index 0000000000..dc47f5247a Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-changelang.gif differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-display.png b/versions/4.0/zh/advanced-topics/i18n/i18n-display.png new file mode 100644 index 0000000000..7bc4fa26d4 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-display.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-install.png b/versions/4.0/zh/advanced-topics/i18n/i18n-install.png new file mode 100644 index 0000000000..b75d9f696d Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-install.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-label-change.gif b/versions/4.0/zh/advanced-topics/i18n/i18n-label-change.gif new file mode 100644 index 0000000000..803439f710 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-label-change.gif differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-label.gif b/versions/4.0/zh/advanced-topics/i18n/i18n-label.gif new file mode 100644 index 0000000000..b906a0aa91 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-label.gif differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-langu-file.png b/versions/4.0/zh/advanced-topics/i18n/i18n-langu-file.png new file mode 100644 index 0000000000..8c65598d84 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-langu-file.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-langu.png b/versions/4.0/zh/advanced-topics/i18n/i18n-langu.png new file mode 100644 index 0000000000..e2c8c33309 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-langu.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-localizedlable.png b/versions/4.0/zh/advanced-topics/i18n/i18n-localizedlable.png new file mode 100644 index 0000000000..dce7afb585 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-localizedlable.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-menu.png b/versions/4.0/zh/advanced-topics/i18n/i18n-menu.png new file mode 100644 index 0000000000..c860f6164f Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-menu.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-script.png b/versions/4.0/zh/advanced-topics/i18n/i18n-script.png new file mode 100644 index 0000000000..c2d9169e88 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-script.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-search.png b/versions/4.0/zh/advanced-topics/i18n/i18n-search.png new file mode 100644 index 0000000000..2f81edecc0 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-search.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-sprite1.png b/versions/4.0/zh/advanced-topics/i18n/i18n-sprite1.png new file mode 100644 index 0000000000..649a3acc6c Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-sprite1.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-sprite2.gif b/versions/4.0/zh/advanced-topics/i18n/i18n-sprite2.gif new file mode 100644 index 0000000000..f11adb18c5 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-sprite2.gif differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-sprite3.gif b/versions/4.0/zh/advanced-topics/i18n/i18n-sprite3.gif new file mode 100644 index 0000000000..7d5c08b8f6 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-sprite3.gif differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-store.png b/versions/4.0/zh/advanced-topics/i18n/i18n-store.png new file mode 100644 index 0000000000..24dc3c4cec Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-store.png differ diff --git a/versions/4.0/zh/advanced-topics/i18n/i18n-undisplay.png b/versions/4.0/zh/advanced-topics/i18n/i18n-undisplay.png new file mode 100644 index 0000000000..160efd4bcc Binary files /dev/null and b/versions/4.0/zh/advanced-topics/i18n/i18n-undisplay.png differ diff --git a/versions/4.0/zh/advanced-topics/index.md b/versions/4.0/zh/advanced-topics/index.md new file mode 100644 index 0000000000..8498fb6e0c --- /dev/null +++ b/versions/4.0/zh/advanced-topics/index.md @@ -0,0 +1,17 @@ +# 进阶主题 + +- [如何向 Cocos 提交代码](../submit-pr/submit-pr.md) +- [存储和读取用户数据](data-storage.md) +- [自定义加载 Wasm/Asm 文件与模块](wasm-asm-load.md) +- [使用 Emscripten 将原生代码转化为 Wasm/Asm 文件](wasm-asm-create.md) +- [引擎定制工作流程](engine-customization.md) +- [网页预览定制工作流程](../editor/preview/browser.md) +- [i18n 游戏多语言支持](i18n.md) +- [动态合图](dynamic-atlas.md) +- [压缩引擎内部属性](mangle-properties.md) +- [热更新范例教程](hot-update.md) +- [热更新管理器](hot-update-manager.md) +- [HTTP 请求](http.md) +- [WebSocket](websocket-introduction.md) + - [WebSocket 客户端](websocket.md) + - [WebSocket 服务器](websocket-server.md) diff --git a/versions/4.0/zh/advanced-topics/java-reflection.md b/versions/4.0/zh/advanced-topics/java-reflection.md new file mode 100644 index 0000000000..73d29514d8 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/java-reflection.md @@ -0,0 +1,251 @@ +# 基于反射机制实现 JavaScript 与 Android 系统原生通信 + +## JavaScript 调用 Java 静态方法 + +使用 Cocos Creator 打包的安卓原生应用中,我们可以通过反射机制直接在 JavaScript 中调用 Java 的静态方法。它的定义如下: + +```js +import { native } from 'cc'; +var o = native.reflection.callStaticMethod(className, methodName, methodSignature, parameters...) +``` + +- className:类名 +- methodName:方法名 +- methodSignature:方法签名 +- parameters:参数列表 + +接下来,我们以 `com.cocos.game` 包下面的 `Test` 类为例,来具体说明。 + +```java +// package "com.cocos.game"; + +public class Test { + + public static void hello (String msg) { + System.out.println (msg); + } + + public static int sum (int a, int b) { + return a + b; + } + + public static int sum (int a) { + return a + 2; + } +} +``` + +### className + +`className` 需要包含包名信息,如果要调用上面的 Test 类中的静态方法,`className` 应该为 "com/cocos/game/Test"。 + + > **注意**:这里必须是斜线 `/`,而不是在 Java 代码中的 `.`。 + +### methodName + +`methodName` 就是方法本来的名字,例如要调用 `sum` 方法的话,methodName 传入的就是 "sum"。 + +### methodSignature + +由于 Java 支持函数重载功能,方法签名用于告诉反射系统对应的参数类型和返回值类型,以确定唯一的方法。 + +它的格式为:**(参数类型)返回值类型**。 + +目前 Cocos Creator 中支持的 Java 类型签名有以下 4 种: + +| Java 类型 | 签名 | +| :------ | :----- | +| int | I | +| float | F | +| boolean | Z | +| String | Ljava/lang/String; | + +> **注意**:String 类型的签名为 `Ljava/lang/String;`,不要漏掉了最后的 `;`。 + +下面是一些案例 + +- `()V` 表示没有参数,没有返回值 +- `(I)V` 表示参数为一个 int,没有返回值的方法 +- `(I)I` 表示参数为一个 int,返回值为 int 的方法 +- `(IF)Z` 表示参数为一个 int 和一个 float,返回值为 boolean 的方法 +- `(ILjava/lang/String;F)Ljava/lang/String;` 表示参数类型为一个 int,一个 String 和一个 float,返回值类型为 String 的方法 + +### parameters + +传递的参数与签名匹配即可,支持 number、bool 和 string。 + +### 使用示例 + +接下来我们看几个 Test 类中的静态方法的调用示例: + +```js +if(sys.os == sys.OS.ANDROID && sys.isNative){ + // 调用 hello 方法 + native.reflection.callStaticMethod("com/cocos/game/Test", "hello", "(Ljava/lang/String;)V", "this is a message from JavaScript"); + + // 调用第一个 sum 方法 + var result = native.reflection.callStaticMethod("com/cocos/game/Test", "sum", "(II)I", 3, 7); + log(result); // 10 + + // 调用第二个 sum 方法 + var result = native.reflection.callStaticMethod("com/cocos/game/Test", "sum", "(I)I", 3); + log(result); // 5 +} +``` + +`sys.isNative` 用于判断是否为原生平台,`sys.os` 用于判断当前运行系统。由于各平台通信机制不同,建议先判断再处理。 + +运行后,可以在 **控制台** 中看到相应的输出结果。 + +## Java 调用 JavaScript + +除了 JavaScript 调用 Java,引擎也提供了 Java 调用 JavaScript 的机制。 + +通过引擎提供的 `CocosJavascriptJavaBridge.evalString` 方法可以执行 JavaScript 代码。需要注意的是,由于 JavaScript 相关代码会在 GL 线程中执行,我们需要利用 `CocosHelper.runOnGameThread` 来确保线程是正确的。 + +接下来,我们给刚才的 Alert 对话框增加一个按钮,并在它的响应函数中执行一段 JavaScript 代码。 + +```java +alertDialog.setButton("OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 一定要在 GL 线程中执行 + CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("cc.log(\"Javascript Java bridge!\")"); + } + }); + } +}); +``` + +### 调用全局函数 + +我们可以在脚本中通过如下代码新增一个全局函数: + +```js +window.callByNative = function(){ + //to do +} +``` + +> `window` 是 Cocos 引擎脚本环境中的全局对象,如果要让一个变量、函数、对象或者类全局可见,需要将它作为 `window` 的属性。可以使用 `window.变量名` 或者 `变量名` 进行访问。 + +然后像下面这样调用: + +```java +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("window.callByNative()"); + } +}); +``` + +或者: + +```js +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("callByNative()"); + } +}); +``` + +### 调用类的静态函数 + +假如在 TypeScript 脚本中有一个类具有如下静态函数: + +```ts +export class NativeAPI{ + public static callByNative(){ + //to do + } +} +//将 NativeAPI 注册为全局类,否则无法在 Java 中被调用 +window.NativeAPI = NativeAPI; +``` + +我们可以像这样调用: + +```java +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("NativeAPI.callByNative()"); + } +}); +``` + +### 调用单例函数 + +如果脚本代码中,有实现可以全局访问的单例对象 + +```ts +export class NativeAPIMgr{ + private static _inst:NativeAPIMgr; + + public static get inst():NativeAPIMgr{ + if(!this._inst){ + this._inst = new NativeAPIMgr(); + } + return this._inst; + } + + public static callByNative(){ + //to do + } +} + +//将 NativeAPIMgr 注册为全局类,否则无法在 Java 中被调用 +window.NativeAPIMgr = NativeAPIMgr; +``` + +我们可以像下面这样调用: + +```java +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("NativeAPIMgr.inst.callByNative()"); + } +}); +``` + +### 参数传递 + +以上几种 Java 调用 JS 的方式,均支持参数传递,但参数只支持 string, number 和 bool 三种基础类型。 + +我们以全局函数为例: + +```js +window.callByNative = function(a:string, b:number, c:bool){ + //to do +} +``` + +可像这样调用: + +```java +CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("window.callByNative('test',1,true)"); + } +}); +``` + +## 在 C++ 代码中调用 JavaScript + +如果要在 C++ 中调用 `evalString`,我们可以参考下面的方式,确保 `evalString` 在 JavaScript 引擎所在的线程被执行: + +```c++ +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=]() { + se::ScriptEngine::getInstance()->evalString(script.c_str()); +}); +``` + +## 线程安全 + +可以看到,上面的代码中,使用了 `CocosHelper.runOnGameThread` 和 `CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread`。这是为了代码在执行时处于正确的线程,详情请参考:[线程安全](./thread-safety.md)。 diff --git a/versions/4.0/zh/advanced-topics/js-java-bridge.md b/versions/4.0/zh/advanced-topics/js-java-bridge.md new file mode 100644 index 0000000000..4b0c005d84 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/js-java-bridge.md @@ -0,0 +1,158 @@ +# 使用 JsbBridge 实现 JavaScript 与 Java 通信 + +## 背景 + +[基于反射机制实现 JavaScript 与 Android 系统原生通信](./java-reflection.md) 的方法中,我们不仅需要严格声明包名和函数签名,还需要严格校对参数数量以确保正常运行,步骤较为复杂。 + +因此我们额外提供了另外一种方法,用于简化脚本层到原生层的调用。这是一种通道,或者说是一个桥梁,我们将其命名为 `JsbBridge`,意为通过 `JSB` 绑定作为沟通脚本和原生 APP 的桥梁。 + +> **注意**:两种方式都是可以正常使用的,开发者可以根据实际需要选择使用。 + +## 调用机制 + +### JavaScript 接口介绍 + +在脚本层只有 `sendToNative` 和 `onNative` 两个接口,定义如下: + +```js +// JavaScript +export namespace bridge{ + /** + * Send to native with at least one argument. + */ + export function sendToNative(arg0: string, arg1?: string): void; + /** + * Save your own callback controller with a JavaScript function, + * Use 'jsb.bridge.onNative = (arg0: String, arg1: String | null)=>{...}' + * @param args : received from native + */ + export function onNative(arg0: string, arg1?: string | null): void; +} +``` + +见名知义,`sendToNative` 用于调用原生层的代码,而 `onNative` 用于响应原生层的调用。 + +使用时需要注意以下几点: + +- 由于现在这个功能还在实验阶段,所以只支持 `string` 的传输,如果需要传输包含多种参数的对象,请考虑将其转化为 `Json` 形式进行传输,并在不同层级解析。 +- `onNative` 同一时间只会记录一个函数,当再次 `set` 该属性时会覆盖原先的 `onNative` 方法。 +- `sendToNative` 方法是单向通信,不会关心下层的返回情况,也不会告知 `JavaScript` 操作成功或者失败。开发者需要自行处理操作情况。 + +### Java 接口介绍 + +在 `JAVA` 中,也有两对应的接口, `sendToScript` 和 `onScript`,定义如下: + +```JAVA +// JAVA +public class JsbBridge { + public interface ICallback{ + /** + * Applies this callback to the given argument. + * + * @param arg0 as input + * @param arg1 as input + */ + void onScript(String arg0, String arg1); + } + /** Add a callback which you would like to apply + * @param f ICallback, the method which will be actually applied. multiple calls will override + * */ + public static void setCallback(ICallback f); + /** + * Java dispatch Js event, use native c++ code + * @param arg0 input values + */ + public static void sendToScript(String arg0, String arg1); + public static void sendToScript(String arg0); +} +``` + +其中 `sendToScript` 用于调用脚本层代码,而 `onScript` 用于响应脚本层的调用。 + +我们需要实现 `ICallback` 接口,并且使用 `setCallback` 注册,来响应 `onScript` 的具体行为。 + +## 基本使用 + +### JavaScript 触发 Java 的方法 + +假设我们用 Java 写了一个打开广告的接口,当玩家点击打开广告的按钮时,应该由 JavaScript 调用对应的接口,触发打开广告的操作。 + +我们需要先实现一个 ICallback 接口,用于响应操作,并利用 `JsbBridge.setCallback` 注册,代码如下: + +```JAVA +JsbBridge.setCallback(new JsbBridge.ICallback() { + @Override + public void onScript(String arg0, String arg1) { + //TO DO + if(arg0.equals("open_ad")){ + //call openAd method. + } + } +}); +``` + +> 实际项目中,上面的代码一般会在 `AppActivity.java` 的 `onCreated` 中直接或者间接被调用,以确保能够响应所有来自脚本层的调用。 + +在 JavaScript 脚本中,我们就可以像下面一样调用: + +```ts +import { native } from 'cc' +public static onclick(){ + native.bridge.sendToNative('open_ad', defaultAdUrl); +} +``` + +### JAVA 触发 JavaScript 的方法 + +假设我们的广告播放完成后,需要通知 JavaScript 层,可以像下面这样操作。 + +首先,需要在 JavaScript 使用 `onNative` 响应事件: + +```ts +native.bridge.onNative = (arg0:string, arg1: string):void=>{ + if(arg0 == 'ad_close'){ + if(arg1 == "finished") { + //ad playback completed. + } + else{ + //ad cancel. + } + } + return; +} +``` + +> 实际项目中,可以将以面代码写在一个程序启动时就要加载的脚本脚本的 onload 函数中,以确保尽早监听来自原生层的事件。 + +然后,在 `Java` 中,用如下代码调用: + +```JAVA +JsbBridge.sendToScript("ad_close", "finished"); +``` + +通过上述操作,便可以通知 JavaScript 广告的播放结果了。 + +## 最佳实践 + +JsbBridge 提供了 arg0 和 arg1 两个 string 类型的参数用于传递信息,可以根据不同的需求进行分配。 + +### 1. arg0 和 arg1 均用于参数 + +如果通信需求比较简单,不需要进行分类处理,则可以将 arg0 和 arg1 用作参数传递。 + +### 2. arg0 用于分类标记, arg1 用于参数 + +如果通信需求相对复杂,可以使用 arg0 作为分类标记,根据 arg0 来分类处理, arg1 用于参数传递 + +### 3. arg0 用于分类标记,arg1 作为 JSON 字符串 + +对于特别复杂的需求, 单纯的 string 类型参数无法满足,此时可以将需要传递的对象通过 `JSON.stringfy` 转化为字符串,再通过 arg1 进行传递。 使用时,再利用 `JSON.parse` 还原为对象,做后续的处理。 +> 由于涉及到 JSON 的序列化和反序列化操作,这种使用方式不建议高频调用。 + +## 线程安全 + +注意,如果相关代码涉及到原生 UI 的部分,就需要考虑线程安全问题,详情请参考:[线程安全](./thread-safety.md)。 + +## 示例工程:简单的多事件调用 + +Cocos Creator 提供了 **native-script-bridge**([GitHub](https://github.com/cocos-creator/example-3d/tree/v3.8/native-script-bridge) | [Gitee](https://gitee.com/mirrors_cocos-creator/example-3d/tree/v3.8/native-script-bridge))范例,开发者可根据需要自行下载以参考使用。 diff --git a/versions/4.0/zh/advanced-topics/js-oc-bridge.md b/versions/4.0/zh/advanced-topics/js-oc-bridge.md new file mode 100644 index 0000000000..5de3d78858 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/js-oc-bridge.md @@ -0,0 +1,151 @@ +# 使用 JsbBridge 实现 JavaScript 与 Objective-C 通信 + +## 背景 + +[基于反射机制实现 JavaScript 与 iOS/macOS 系统原生通信](./oc-reflection.md) 的方法中,我们不仅需要严格声明包名和函数签名,还需要严格校对参数数量以确保正常运行,步骤较为复杂。 + +因此我们额外提供了另外一种方法,用于简化脚本层到原生层的调用。这是一种通道,或者说是一个桥梁,我们将其命名为 `JsbBridge`,意为通过 `JSB` 绑定作为沟通脚本和原生 APP 的桥梁。 + +> **注意**:两种方式都是可以正常使用的,开发者可以根据实际需要选择使用。 + +## 调用机制 + +### JavaScript 接口介绍 + +在脚本层只有 `sendToNative` 和 `onNative` 两个接口,定义如下: + +```js +// JavaScript +export namespace bridge{ + /** + * Send to native with at least one argument. + */ + export function sendToNative(arg0: string, arg1?: string): void; + /** + * Save your own callback controller with a JavaScript function, + * Use 'jsb.bridge.onNative = (arg0: String, arg1: String | null)=>{...}' + * @param args : received from native + */ + export function onNative(arg0: string, arg1?: string | null): void; +} +``` + +见名知义,`sendToNative` 用于调用原生层的代码,而 `onNative` 用于响应原生层的调用。 + +使用时需要注意以下几点: + +- 由于现在这个功能还在实验阶段,所以只支持 `string` 的传输,如果需要传输包含多种参数的对象,请考虑将其转化为 `Json` 形式进行传输,并在不同层级解析。 +- `onNative` 同一时间只会记录一个函数,当再次 `set` 该属性时会覆盖原先的 `onNative` 方法。 +- `sendToScript` 方法是单向通信,不会关心下层的返回情况,也不会告知 `JavaScript` 操作成功或者失败。开发者需要自行处理操作情况。 + +### Objective-C 接口介绍 + +在 `Objective-C` 中,也有两对应的接口, `sendToScript` 和 `callByScript`,定义如下: + +```objc +//Objective-c +typedef void (^ICallback)(NSString*, NSString*); + +@interface JsbBridge : NSObject + ++(instancetype)sharedInstance; +-(bool)setCallback:(ICallback)cb; +-(bool)callByScript:(NSString*)arg0 arg1:(NSString*)arg1; +-(void)sendToScript:(NSString*)arg0 arg1:(NSString*)arg1; +-(void)sendToScript:(NSString*)arg0; + +@end +``` + +其中 `sendToScript` 用于调用脚本层代码,而 `callByScript` 用于响应脚本层的调用。 + +我们需要实现 `ICallback` 接口,并且使用 `setCallback` 注册,来响应 `callByScript` 的具体行为。 + +## 基本使用 + +### JavaScript 触发 Objective-C 的回调 + +假设我们用 Objective-C 写了一个打开广告的接口,当玩家点击打开广告的按钮时,应该由 JavaScript 调用对应的 Objective-C 接口,触发打开广告的操作。 + +我们需要先实现一个 ICallback 接口,用于响应操作,然后通过 `setCallback` 方法,注册到 `JsbBridge`。 + +Objective-C 代码如下: + +```ObjC +#include "platform/apple/JsbBridge.h" + +static ICallback cb = ^void (NSString* _arg0, NSString* _arg1){ + if([_arg0 isEqual:@"open_ad"]){ + //open Ad + } +}; + +JsbBridge* m = [JsbBridge sharedInstance]; +[m setCallback:cb]; +``` + +在 JavaScript 脚本中,我们就可以像下面一样调用: + +```ts +import { native } from 'cc' +public static onclick(){ + native.bridge.sendToNative('open_ad', defaultAdUrl); +} +``` + +### Objective-C 触发 JavaScript 的回调 + +假设我们的广告播放完成后,需要通知 JavaScript 层,可以像下面这样操作。 + +首先,需要在 JavaScript 使用 `onNative` 响应事件: + +```ts +native.bridge.onNative = (arg0:string, arg1: string):void=>{ + if(arg0 == 'ad_close'){ + if(arg1 == "finished") { + //ad playback completed. + } + else{ + //ad cancel. + } + } + return; +} +``` + +然后,在 `Objective-C` 中,用如下代码调用: + +```ObjC +#include "platform/apple/JsbBridge.h" + +JsbBridge* m = [JsbBridge sharedInstance]; +[m sendToScript:@"ad_close" arg1:@"finished"]; +``` + +通过上述操作,便可以通知 JavaScript 广告的播放结果了。 + +## 最佳实践 + +JsbBridge 提供了 arg0 和 arg1 两个 string 类型的参数用于传递信息,可以根据不同的需求进行分配。 + +### 1. arg0 和 arg1 均用于参数 + +如果通信需求比较简单,不需要进行分类处理,则可以将 arg0 和 arg1 用作参数传递。 + +### 2. arg0 用于分类标记, arg1 用于参数 + +如果通信需求相对复杂,可以使用 arg0 作为分类标记,根据 arg0 来分类处理, arg1 用于参数传递 + +### 3. arg0 用于分类标记,arg1 作为 JSON 字符串 + +对于特别复杂的需求, 单纯的 string 类型参数无法满足,此时可以将需要传递的对象通过 `JSON.stringfy` 转化为字符串,再通过 arg1 进行传递。 使用时,再利用 `JSON.parse` 还原为对象,做后续的处理。 + +> 由于涉及到 JSON 的序列化和反序列化操作,这种使用方式不建议高频调用。 + +## 线程安全 + +注意,如果相关代码涉及到原生 UI 的部分,就需要考虑线程安全问题,详情请参考:[线程安全](./thread-safety.md)。 + +## 示例工程:简单的多事件调用 + +Cocos Creator 提供了 **native-script-bridge**([GitHub](https://github.com/cocos-creator/example-3d/tree/v3.8/native-script-bridge) | [Gitee](https://gitee.com/mirrors_cocos-creator/example-3d/tree/v3.8/native-script-bridge))范例,开发者可根据需要自行下载以参考使用。 diff --git a/versions/4.0/zh/advanced-topics/jsb-auto-binding.md b/versions/4.0/zh/advanced-topics/jsb-auto-binding.md new file mode 100644 index 0000000000..e6a783abe3 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/jsb-auto-binding.md @@ -0,0 +1,372 @@ +# 使用 JSB 自动绑定 + +> 本文转载自 [腾讯在线教育部技术博客](https://oedx.github.io/2019/07/03/cocos-creator-js-binding-auto/)
+> 作者:张鑫(kevinxzhang) + +尽管 Creator 提供了 `native.reflection.callStaticMethod` 方式支持从 ts 端直接调用 Native 端(Android/iOS/Mac)的接口,但是经过大量实践发现此接口在大量频繁调用情况下性能很低下,尤其是在 Android 端,比如调用 Native 端实现的打印 log 的接口,而且会容易引起一些 native crash,例如 `local reference table overflow` 等问题。纵观 Cocos 原生代码的实现,基本所有的接口方法的实现都是基于 JSB 的方式来实现,所以此文主要讲解下 JSB 的自动绑定逻辑,帮助大家能快速实现 `callStaticMethod` 到 JSB 的改造过程。 + +## 背景 + +对于用过 Cocos Creator(为了方便后文直接简称 CC)的人来说,`jsb.reflection.callStaticMethod` 这个方法肯定不陌生,其提供了我们从 ts 端调用 Native 端的能力,例如我们要调用 Native 实现的 log 打印和持久化的接口,就可以很方便的在 JavaScript 中按照如下的操作调用即可: + +```typescript + +import {NATIVE} from 'cc/env'; + +if (NATIVE && sys.os == sys.OS.IOS) { + msg = this.buffer_string + '\n[cclog][' + clock + '][' + tag + ']' + msg; + native.reflection.callStaticMethod("ABCLogService", "log:module:level:", msg, 'cclog', level); + return; +} else if (NATIVE && sys.os == sys.OS.ANDROID) { + msg = this.buffer_string + '\n[cclog][' + clock + '][' + tag + ']' + msg; + native.reflection.callStaticMethod("com/example/test/CommonUtils", "log", "(ILjava/lang/String;Ljava/lang/String;)V", level, 'cclog', msg); + return; +} +``` + +尽管使用很简单,一行代码就能实现跨平台调用,稍微看下其实现就可以知道,在 C++ 层 Android 端是通过 jni 的方式实现的,IOS 端是通过运行时的方式动态调用,但是为了兼顾通用性和支持所有的方法,Android 端没有对 jni 相关对象做缓存机制,就会导致短时间大量调用时出现很严重的性能问题,之前我们遇到的比较多的情况就是在下载器中打印 log,某些应用场景短时间内触发大量的下载操作,就会出现 `local reference table overflow` 的 crash,甚至在低端机上导致界面卡顿无法加载出来的问题。 + +修复此问题就需要针对 log 调用进行 JSB 的改造,同时还要加上 jni 的相关缓存机制,优化性能。jSB 绑定说白了就是 C++ 和脚本层之间进行对象的转换,并转发脚本层函数调用到 C++ 层的过程。 + +JSB 绑定通常有 **手动绑定** 和 **自动绑定** 两种方式。手动绑定方式可以参考 [使用 JSB 手动绑定](jsb-manual-binding.md)。 +- 手动绑定方式优点是灵活,可定制型强;缺点就是全部代码要自己书写,尤其是在 ts 类型跟 c++ 类型转换上,稍有不慎容易导致内存泄漏,某些指针或者对象没有释放。 +- 自动绑定方式则会帮你省了很多麻烦,直接通过一个脚本一键生成相关的代码,后续如果有新增或者改动,也只需要重新执行一次脚本即可。所以自动绑定对于不需要进行强定制,需要快速完成JSB的情况来说就再适合不过了。下面就一步步说明下如何实现自动绑定 JSB。 + +## 环境配置和自动绑定展示 + +### 环境配置 + +自动绑定,说简单点,其实就只要执行一个 python 脚本即可自动生成对应的 `.cpp`、`.h` 文件。所以首先要保证电脑有 python 运行环境,这里以 Mac 上安装为例来讲解。 + +1. 安装 python 3.0 版本,从 python 官网下载安装包: + + https://www.python.org/downloads/release/python-398/ + +2. 通过 pip3 安装 python 的一些依赖库: + + ```shell + sudo pip3 install pyyaml==5.4.1 + sudo pip3 install Cheetah3 + ``` + +3. 安装 NDK,涉及到 c++ 肯定这个是必不可少的,建议安装 [Android NDK r21e](https://developer.android.com/ndk/downloads/older_releases?hl=zh-cn) 版本,然后在 `~/.bash_profile` 中设置 `PYTHON_ROOT` 和 `NDK_ROOT` 这两个环境变量,因为在后面执行的 python 文件里面就会直接用到这两个环境变量: + + ```shell + export NDK_ROOT=/Users/kevin/android-ndk-r21e + export PYTHON_BIN=python3 + ``` + +Windows 下直接参考上面需要安装的模块直接安装就好了,最后也要记得配置环境变量。 + +### 自动绑定展示 + +这里演示的是 cocos 引擎下面也即 **generated/cocos/bindings/auto** 目录下的文件(如下图所示)是怎么自动生成的: + +![](jsb/auto-file.png) + +> **注意**:该目录会在构建时自动成成,未构建时并不会创建。 + +其实从这些文件名的开头也能看出,这些文件命名都是有某些特定规律的,那么这些文件是怎么生成的呢?首先打开终端,先 cd 到 **tools/tojs** 目录下,然后直接运行 `./genbindings.py`: + +![](jsb/generate-file.png) + +大概运行一分钟左右后,会出现如下的提示,说明已经顺利生成完了: + +![](jsb/generate-file-complete.png) + +经过上面的步骤后,**cocos/bindings/auto** 下的文件就全部自动生成出来了,是不是非常方便。 + +下面再以 ts 层通过 jsb 调用 Native 层的 log 方法打印日志为例,详细的告知下如何实现通过自动绑定工具,依据自己写的 c++ 代码,生成对应的自动绑定文件。 + +## 编写 c++ 层的实现 + +C++ 作为连接 ts 层和 Native 层的桥梁,既然要实现 jsb 调用,那第一步肯定是要先把 C++ 层的头文件和实现准备好,这里我们在 cocos/⁩ 创建一个 test 文件夹用于存放相关文件: + +![](jsb/store-file.png) + +这里先准备 `ABCJSBBridge.h`,里面主要是申明了一个 `abcLog` 的函数,此函数就是供 ts 层调用打 log 的,另外由于打 log 方法肯定在 ts 层很多地方都会使用,所以这里采用了一个单例模式,提供了 `getInstance()` 来获取当前类的实例。 + +```cpp +#pragma once + +#include + +namespace abc +{ + class JSBBridge + { + public: + void abcLog(const std::string& msg); + /** + * Returns a shared instance of the director. + * @js _getInstance + */ + static JSBBridge* getInstance(); + + /** @private */ + JSBBridge(); + /** @private */ + ~JSBBridge(); + bool init(); + }; +} +``` + +下面是对应的实现 `ABCJSBBridge.cpp`: + +```cpp +#include +#include "ABCJSBBridge.h" + +namespace abc +{ + // singleton stuff + static JSBBridge *s_SharedJSBBridge = nullptr; + + JSBBridge::JSBBridge() + { + CC_LOG_ERROR("Construct JSBBridge %p", this); + init(); + } + + JSBBridge::~JSBBridge() + { + CC_LOG_ERROR("Destruct JSBBridge %p", this); + s_SharedJSBBridge = nullptr; + } + + JSBBridge* JSBBridge::getInstance() + { + if (!s_SharedJSBBridge) + { + CC_LOG_ERROR("getInstance JSBBridge "); + s_SharedJSBBridge = new (std::nothrow) JSBBridge(); + CCASSERT(s_SharedJSBBridge, "FATAL: Not enough memory for create JSBBridge"); + } + + return s_SharedJSBBridge; + } + + bool JSBBridge::init(void) + { + CC_LOG_ERROR("init JSBBridge "); + return true; + } + + void JSBBridge::abcLog(const std::string& msg) + { + CC_LOG_ERROR("%s", msg.c_str()); + } +} +``` + +## JSB 配置脚本编写 + +我们在 **tools/tojs** 目录下找到 `genbindings.py` 脚本,复制并重命名为 `genbindings_test.py`,然后修改`genbindings_test.py` 模块配置,只保留 cocos2dx_test 模块。 + +![](jsb/cancel-output_dir.png) + +接下来是在 **tools/tojs** 目录下添加自定义的配置文件 `cocos2dx_test.ini`,其实就跟 **tools/tojs** 下的其他 `.ini` 文件类似,主要让自动绑定工具知道哪些 API 要被绑定和以什么样的方式绑定,写法上直接参考 Cocos 已有的 ini 文件,这里展示下 `cocos2dx_test.ini` 的内容: + +``` ini +[cocos2dx_test] +# the prefix to be added to the generated functions. You might or might not use this in your own +# templates +prefix = cocos2dx_test + +# create a target namespace (in javascript, this would create some code like the equiv. to `ns = ns || {}`) +# all classes will be embedded in that namespace +target_namespace = abc + +macro_judgement = + +android_headers = + +android_flags = -target armv7-none-linux-androideabi -D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS -DANDROID -D__ANDROID_API__=14 -gcc-toolchain %(gcc_toolchain_dir)s --sysroot=%(androidndkdir)s/platforms/android-14/arch-arm -idirafter %(androidndkdir)s/sources/android/support/include -idirafter %(androidndkdir)s/sysroot/usr/include -idirafter %(androidndkdir)s/sysroot/usr/include/arm-linux-androideabi -idirafter %(clangllvmdir)s/lib64/clang/5.0/include -I%(androidndkdir)s/sources/cxx-stl/llvm-libc++/include + +clang_headers = +clang_flags = -nostdinc -x c++ -std=c++17 -fsigned-char -mfloat-abi=soft -U__SSE__ + +cocos_headers = -I%(cocosdir)s/cocos -I%(cocosdir)s/cocos/platform/android -I%(cocosdir)s/external/sources + +cocos_flags = -DANDROID -DCC_PLATFORM=3 -DCC_PLATFORM_MAC_IOS=1 -DCC_PLATFORM_MAC_OSX=4 -DCC_PLATFORM_WINDOWS=2 -DCC_PLATFORM_ANDROID=3 + + +cxxgenerator_headers = + +# extra arguments for clang +extra_arguments = %(android_headers)s %(clang_headers)s %(cxxgenerator_headers)s %(cocos_headers)s %(android_flags)s %(clang_flags)s %(cocos_flags)s %(extra_flags)s + +# what headers to parse +headers = %(cocosdir)s/cocos/test/ABCJSBBridge.h + +# cpp_headers = network/js_network_manual.h + +# what classes to produce code for. You can use regular expressions here. When testing the regular +# expression, it will be enclosed in "^$", like this: "^Menu*$". +classes = JSBBridge + +# what should we skip? in the format ClassName::[function function] +# ClassName is a regular expression, but will be used like this: "^ClassName$" functions are also +# regular expressions, they will not be surrounded by "^$". If you want to skip a whole class, just +# add a single "*" as functions. See bellow for several examples. A special class name is "*", which +# will apply to all class names. This is a convenience wildcard to be able to skip similar named +# functions from all classes. +skip = JSBBridge::[init] + +rename_functions = + +rename_classes = + +# for all class names, should we remove something when registering in the target VM? +remove_prefix = + +# classes for which there will be no "parent" lookup +classes_have_no_parents = JSBBridge + +# base classes which will be skipped when their sub-classes found them. +base_classes_to_skip = Clonable + +# classes that create no constructor +# Set is special and we will use a hand-written constructor +abstract_classes = JSBBridge +``` + +其实从里面的注释也讲的非常详细,这里说几个主要的属性及含义: + +![](jsb/ini-file-properties.png) + +以上的配置完成后,就可以 cd 到 **tools/tojs** 目录下,运行 `./genbindings_test.py` 自动生成绑定文件。然后就会看到在 **cocos/bindings/auto** 下面会多出了两个个绑定文件: + +![](jsb/binding-file.png) + +打开生成的 `jsb_cocos2dx_test_auto.cpp`: + +```cpp +#include "cocos/bindings/auto/jsb_cocos2dx_test_auto.h" +#include "cocos/bindings/manual/jsb_conversions.h" +#include "cocos/bindings/manual/jsb_global.h" +#include "test/ABCJSBBridge.h" + +#ifndef JSB_ALLOC +#define JSB_ALLOC(kls, ...) new (std::nothrow) kls(__VA_ARGS__) +#endif + +#ifndef JSB_FREE +#define JSB_FREE(ptr) delete ptr +#endif +se::Object* __jsb_abc_JSBBridge_proto = nullptr; +se::Class* __jsb_abc_JSBBridge_class = nullptr; + +static bool js_cocos2dx_test_JSBBridge_abcLog(se::State& s) // NOLINT(readability-identifier-naming) +{ + auto* cobj = SE_THIS_OBJECT(s); + SE_PRECONDITION2(cobj, false, "js_cocos2dx_test_JSBBridge_abcLog : Invalid Native Object"); + const auto& args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 1) { + HolderType arg0 = {}; + ok &= sevalue_to_native(args[0], &arg0, s.thisObject()); + SE_PRECONDITION2(ok, false, "js_cocos2dx_test_JSBBridge_abcLog : Error processing arguments"); + cobj->abcLog(arg0.value()); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 1); + return false; +} +SE_BIND_FUNC(js_cocos2dx_test_JSBBridge_abcLog) + +static bool js_cocos2dx_test_JSBBridge_getInstance(se::State& s) // NOLINT(readability-identifier-naming) +{ + const auto& args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 0) { + abc::JSBBridge* result = abc::JSBBridge::getInstance(); + ok &= nativevalue_to_se(result, s.rval(), nullptr /*ctx*/); + SE_PRECONDITION2(ok, false, "js_cocos2dx_test_JSBBridge_getInstance : Error processing arguments"); + SE_HOLD_RETURN_VALUE(result, s.thisObject(), s.rval()); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 0); + return false; +} +SE_BIND_FUNC(js_cocos2dx_test_JSBBridge_getInstance) + +bool js_register_cocos2dx_test_JSBBridge(se::Object* obj) // NOLINT(readability-identifier-naming) +{ + auto* cls = se::Class::create("JSBBridge", obj, nullptr, nullptr); + + cls->defineFunction("abcLog", _SE(js_cocos2dx_test_JSBBridge_abcLog)); + cls->defineStaticFunction("getInstance", _SE(js_cocos2dx_test_JSBBridge_getInstance)); + cls->install(); + JSBClassType::registerClass(cls); + + __jsb_abc_JSBBridge_proto = cls->getProto(); + __jsb_abc_JSBBridge_class = cls; + + se::ScriptEngine::getInstance()->clearException(); + return true; +} +bool register_all_cocos2dx_test(se::Object* obj) +{ + // Get the ns + se::Value nsVal; + if (!obj->getProperty("abc", &nsVal)) + { + se::HandleObject jsobj(se::Object::createPlainObject()); + nsVal.setObject(jsobj); + obj->setProperty("abc", nsVal); + } + se::Object* ns = nsVal.toObject(); + + js_register_cocos2dx_test_JSBBridge(ns); + return true; +} +``` + +看到这里是不是感觉很熟悉,跟 Cocos 已有的那些 cpp 完全一样,甚至包括里面的注册函数和类的定义都给全部自动生成了。 + +## Cocos 编译配置 + +尽管经过上面一步后我们已经生成出来了绑定文件,但是 ts 层还是没法直接使用,因为还需要把生成的绑定文件,配置到 CMakeLists.txt 文件中,从而跟其他 c++ 文件一起编译才行,这部分主要就是最后的 CMakeLists.txt 编译配置。 + +1. 打开 `CMakeLists.txt` 文件,在其中加上最开始实现的 ABCJSBBridge.h 和 ABCJSBBridge.cpp,还有自动绑定生成的 jsb_cocos2dx_test_auto.h 和 jsb_cocos2dx_test_auto.cpp 文件: + + ![](jsb/111.png) + +2. 打开 `cocos/bindings/manual/jsb_module_register.cpp`,添加 cocos2dx_test 模块的注册代码: + + ![](jsb/112.png) + + +经过上面这些配置后,最终就可以在 ts 层直接像下面这样来进行调用: + +``` typescript +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Test') +export class Test extends Component { + start () { + // @ts-ignore + abc.JSBBridge.getInstance().abcLog("JSB 绑定测试成功") + } +} +``` + +## 自动绑定的限制条件 + +自动绑定依赖于 Bindings Generator 工具,Cocos 官方还在 GitHub 上单独把这部分拎出来了:。Bindings Generator 工具它可以将 C++ 类的公共方法和公共属性绑定到脚本层。自动绑定工具尽管非常强大,但是还是会有一些限制: +1. 只能够针对类生成绑定,不可以绑定结构体,独立函数等。 +2. 不能够生成 `Delegate` 类型的 API,因为脚本中的对象是无法继承 C++ 中的 `Delegate` 类并重写其中的 `Delegate` 函数的。 +3. 子类中重写了父类的 API 的同时,又重载了这个 API。 +4. 部分 API 实现内容并没有完全体现在其 API 定义中。 +5. 在运行时由 C++ 主动调用的 API。 + +> **注意**:从 3.6 开始自动绑定所涉及的参数及返回值的类型也需要绑定,或者提供转换方法 `sevalue_to_native`/`nativevalue_to_se`,否则会在编译期报错。在 3.5 之前为运行时报错。 + +## 总结 + +总的来说,自动绑定 JSB 只需要要求开发者编写相关的实现 C++ 实现类,一个配置文件,然后执行一条命令就能完整整个的绑定过程。如果没有什么特殊定制,相对于手动绑定来说,效率上还是提高了不少。实际工作做可以依据具体情况先用自动绑定功能,然后再去手动修改生成的绑定文件,达到事倍功半的效果。 diff --git a/versions/4.0/zh/advanced-topics/jsb-bridge-wrapper.md b/versions/4.0/zh/advanced-topics/jsb-bridge-wrapper.md new file mode 100644 index 0000000000..c3ec0ab1a9 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/jsb-bridge-wrapper.md @@ -0,0 +1,233 @@ +# JsbBridgeWrapper 基于原生反射机制的事件处理 + +> **注意**:在 v3.6 之后,jsb 模块将会逐步废弃,接口将会迁移到 cc 命名空间下的 native 模块。 + +JsbBridgeWrapper 是基于事件机制,用于 JS 层与原生层进行通信的接口。 + +## 建立于 JsbBridge 上的事件分发机制 + +`JsbBridgeWrapper` 是封装在 `JsbBridge` 之上的事件派发机制,相对于 `JsbBridge` 而言它更方便易用。开发者不需要手动去实现一套消息收发机制就可以进行多事件的触发。但它不具有多线程稳定性或者是 100% 安全。如果遇到复杂需求场景,仍然建议自己实现对应的事件派发。 + +## JsbBridgeWrapper 接口介绍 + +```js +/** + * Listener for jsbBridgeWrapper's event. + * It takes one argument as string which is transferred by jsbBridge. + */ +export type OnNativeEventListener = (arg: string) => void; +export namespace jsbBridgeWrapper { + /** If there's no event registered, the wrapper will create one */ + export function addNativeEventListener(eventName: string, listener: OnNativeEventListener); + /** + * Dispatch the event registered on Objective-C, Java etc. + * No return value in JS to tell you if it works. + */ + export function dispatchEventToNative(eventName: string, arg?: string); + /** + * Remove all listeners relative. + */ + export function removeAllListenersForEvent(eventName: string); + /** + * Remove the listener specified + */ + export function removeNativeEventListener(eventName: string, listener: OnNativeEventListener); + /** + * Remove all events, use it carefully! + */ + export function removeAllListeners(); +} +``` + +`OnNativeEventListener` 是实际注册的 **回调(callback)** 类型,为了防止因为类型不匹配导致的低级错误,因此使用显示声明该类型。`addNativeEventListener` 中的第二个参数即为传入的 callback。当然也可以使用匿名函数代替。代码示例如下: + +```js +import { native } from 'cc' +// 当事件 “A” 触发时, ‘this.A’ 方法会被调用 +native.jsbBridgeWrapper.addNativeEventListener("A", (usr: string) => { + this.A(usr); +}); +``` + +> **注意**:这里是为了防止 this 指向不明确,所以使用匿名函数封装一层作用域。 + +### JsbBridgeWrapper 接口说明 + +#### addNativeEventListener + +增加一个事件监听。 + +参数: + +- eventName: string 事件名称 +- listener: OnNativeEventListener 回调函数 + +#### dispatchEventToNative + +派发一个事件到原生层。 + +参数: + +- eventName: string 事件名称 +- arg?: string 参数 + +#### removeNativeEventListener + +删除事件监听。 + +参数: + +- eventName: string 事件名称 +- listener: OnNativeEventListener 要删除的回调函数 + +#### removeAllListeners + +删除所有的事件监听。 + +## 原生平台 JsbBridgeWrapper 的实现 + +`JsbBridgeWrapper` 在不同平台有不同的实现,开发者可以通过下列方式进行查看: + +- 在 Objective-C 端,可查看 [JsbBridgeWrapper.h](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/platform/apple/JsbBridgeWrapper.h) : + + ```objc + //In Objective-C + typedef void (^OnScriptEventListener)(NSString*); + + @interface JsbBridgeWrapper : NSObject + /** + * Get the instance of JsbBridgetWrapper + */ + + (instancetype)sharedInstance; + /** + * Add a listener to specified event, if the event does not exist, the wrapper will create one. Concurrent listener will be ignored + */ + - (void)addScriptEventListener:(NSString*)eventName listener:(OnScriptEventListener)listener; + /** + * Remove listener for specified event, concurrent event will be deleted. Return false only if the event does not exist + */ + - (bool)removeScriptEventListener:(NSString*)eventName listener:(OnScriptEventListener)listener; + /** + * Remove all listener for event specified. + */ + - (void)removeAllListenersForEvent:(NSString*)eventName; + /** + * Remove all event registered. Use it carefully! + */ + - (void)removeAllListeners; + /** + * Dispatch the event with argument, the event should be registered in javascript, or other script language in future. + */ + - (void)dispatchEventToScript:(NSString*)eventName arg:(NSString*)arg; + /** + * Dispatch the event which is registered in javascript, or other script language in future. + */ + - (void)dispatchEventToScript:(NSString*)eventName; + @end + + ``` + +- 安卓可查看 [JsbBridgeWrapper.java](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/platform/android/java/src/com/cocos/lib/JsbBridgeWrapper.java): + + ```JAVA + // In JAVA + public class JsbBridgeWrapper { + public interface OnScriptEventListener { + void onScriptEvent(String arg); + } + /** + * Add a listener to specified event, if the event does not exist, the wrapper will create one. Concurrent listener will be ignored + */ + public void addScriptEventListener(String eventName, OnScriptEventListener listener); + /** + * Remove listener for specified event, concurrent event will be deleted. Return false only if the event does not exist + */ + public boolean removeScriptEventListener(String eventName, OnScriptEventListener listener); + /** + * Remove all listener for event specified. + */ + public void removeAllListenersForEvent(String eventName); + /** + * Remove all event registered. Use it carefully! + */ + public void removeAllListeners() { + this.eventMap.clear(); + } + /** + * Dispatch the event with argument, the event should be registered in javascript, or other script language in future. + */ + public void dispatchEventToScript(String eventName, String arg); + /** + * Dispatch the event which is registered in javascript, or other script language in future. + */ + public void dispatchEventToScript(String eventName); + } + ``` + +- Huawei HarmonyOS 也可以通过 [JsbBridgeWrapper.java](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/platform/ohos/libcocos/src/main/java/com/cocos/lib/JsbBridgeWrapper.java) 查看其实现方式。 + +## 使用 JsbBridgeWrapper + +常见的需求如数据存放在原生层,当需要将数据取至 JS 层时,可以通过 JsbBridgeWrapper 实现。 + +下文通过一个示例说明,如何通过原生的回调结果改变 label 内容,当原生层的事件被触发时,将目标文本字符回传给 JS 层。 + +### 注册 JS 事件 + +JS 层需要首先注册一个 `changeLabelContent` 事件监听。 + +```js +public changeLabelContent(user: string): void { + console.log("Hello " + user + " I'm K"); + this.labelForContent!.string = "Hello " + user + " ! I'm K"; +} +native.jsbBridgeWrapper.addNativeEventListener("changeLabelContent", (usr: string) => { + this.changeLabelContent(usr); +}); +``` + +当 JS 层的 `changeLabelContent` 事件被触发时,标签的内容会变成对应的字符串组合。接下来需要处理原生的事件注册。 + +### 原生事件注册与派发 + +- 在 Objective-C 端使用下列代码: + + ```Objc + // Objective-C + JsbBridgeWrapper* m = [JsbBridgeWrapper sharedInstance]; + OnScriptEventListener requestLabelContent = ^void(NSString* arg){ + JsbBridgeWrapper* m = [JsbBridgeWrapper sharedInstance]; + [m dispatchEventToScript:@"changeLabelContent" arg:@"Charlotte"]; + }; + [m addScriptEventListener:@"requestLabelContent" listener:requestLabelContent]; + ``` + +- 在 JAVA 端使用如下代码: + + ```JAVA + // JAVA + JsbBridgeWrapper jbw = JsbBridgeWrapper.getInstance(); + jbw.addScriptEventListener("requestLabelContent", arg ->{ + System.out.print("@JAVA: here is the argument transport in" + arg); + jbw.dispatchEventToScript("changeLabelContent","Charlotte"); + }); + ``` + + > **注意**:JAVA 可以通过匿名函数的方法来实现 interface 的需求,此处写法简化。 + +这里原生的返回值被设置成固定字符,但开发者可以根据需求实现异步亦或是延后的字符赋值,时机并非固定。简而言之,当原生收到 `requestLabelContent` 的事件时,原生将会反过来触发 JS 层的 `changeLabelContent` 的事件,并将字符作为事件触发的传参。 + +### 在场景中派发事件 + +最后一步,我们在场景中添加一个按钮和对应的事件。 + +```js +// 按钮点击事件 SAY HELLO +public sayHelloBtn() { + native.jsbBridgeWrapper.dispatchEventToNative("requestLabelContent"); +} +``` + +最终的效果和 JsbBridge 的测试例效果相同。点击 `SAY HELLO` 按钮,第一行的内容会改变为打过招呼的信息,否则即为失败。 + +使用 `JsbBridgeWrapper` 模块时,开发者不需要自己去维护多余的机制,只需要关心是否正确注册和取消注册即可。 diff --git a/versions/4.0/zh/advanced-topics/jsb-manual-binding.md b/versions/4.0/zh/advanced-topics/jsb-manual-binding.md new file mode 100644 index 0000000000..e7f807b310 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/jsb-manual-binding.md @@ -0,0 +1,398 @@ +# 使用 JSB 手动绑定 + +> 本文转载自 [腾讯在线教育部技术博客](https://oedx.github.io/2019/05/29/cocos-creator-js-binding-manual/)
+> 作者:晋中望(xepherjin) + +## 背景 + +一直以来,ABCmouse 项目中的整体 JS/Native 通信调用结构都是基于 `callStaticMethod <-> evalString` 的方式。通过 `callStaticMethod` 方法我们可以通过反射机制直接在 JavaScript 中调用 `Java/Objective-C` 的静态方法。而通过 `evalString` 方式,则可以执行 JS 代码,这样便可以进行双端通信。 + + +

新版 ABCmouse 的应用架构:基于 callStaticMethod 与 evalString 进行通信

+ +虽然基于这个方式上层封装接口后,新增业务逻辑会比较方便。但是过度依赖 evalString,往往也会带来一些隐患。举个 Android 侧的例子: + +```js +CocosJavascriptJavaBridge.evalString("window.sample.testEval('" + param + "',JSON.stringify(" + jsonObj + "))"); +``` + +对于常见的参数结构,这样运行是没有问题的,然而基于实际场景的种种情况,我们会发现针对 **引号** 的控制格外重要。如代码所示,为了保证 JS 代码能够被正确执行,我们在拼接字符串时必须明确 `'` 与 `"` 的使用,稍有不慎就会出现 `evalString` 失败的情况。在 Cocos 的官方论坛上,从大量的反馈中我们也能了解这里的确是一个十分容易踩坑的地方。而另一方面,对于我们项目本身而言,过度依赖 `evalString` 所产生的种种不确定因素也往往很难掌控,我们又不能一味地通过 `try/catch` 去解决。所幸的是,经过全局业务排查,目前项目中在绝大多数因此,在查阅官方文档后,我们决定绕过 `evalString`,直接基于 JSB 绑定的方式进行通信。 + +这里以下载器的接入为例。在我们的项目中,下载器是在 Android 与 iOS 侧分别各自实现。在改造之前的版本中,下载器的调用与回调基于 `callStaticMethod <-> evalString` 的方式。 + +每次调用下载都需要这样执行: + +```ts +import {NATIVE} from 'cc/env'; + +if(NATVE && sys.os == sys.OS.IOS) { + jsb.reflection.callStaticMethod('ABCFileDownloader', 'downloadFileWithUrl:cookie:savePath:', url, cookies, savePath); +} else if(NATVE && sys.os == sys.OS.ANDROID) { + jsb.reflection.callStaticMethod("com/tencent/abcmouse/downloader/ABCFileDownloader", "downloadFileWithUrl", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", url, cookies, savePath); +} +``` + +下载成功抑或是失败都需要通过拼接出类似如下的语句执行 JS: + +```java +StringBuilder sb = new StringBuilder(JS_STRING_ON_DOWNLOAD_FINISH + "("); +sb.append("'" + success + "',"); +sb.append("'" + url + "',"); +sb.append("'" + savePath + "',"); +sb.append("'" + msg + "',"); +sb.append("'" +code + "')"); +CocosJavascriptJavaBridge.evalString(sb.toString()); +``` + +无论是调用抑或是回调都拼接繁琐又容易出错,全部数据不得不转化为字符串 ~~(emmmm也不美观)~~,而且还要考虑到 `evalString` 的执行效率问题。如果只是仅有的少数业务场景在使用尚勉强接受,但是当业务日趋复杂庞大,如果都要这样写,同时又没有详细的文档去规范约束,其后期维护成本可想而知。 + +而当使用 JSB 改造后,我们调用只需如下寥寥几行代码且无需区分平台,更不必担心上述拼接隐患,相比之下逻辑要清晰许多: + +```js +jsb.fileDownloader.requestDownload(url, savePath, cookies, options, (success, url, savePath, msg, code) => { + // do whatever you want +}); +``` + +那么接下来就以一个最简单的下载器的绑定流程为例,我来带大家学习下 JSB 手动绑定的大致流程。
+**(虽然 Cocos Creator 很人性化提供了自动绑定的配置文件,可以通过一些配置直接生成目标文件,减少了很多工作量。但是亲手来完成一次手动绑定的流程会帮助更为全面地了解整个绑定的实现流程,有助于加深理解。另一方面,当存在特殊需要自动绑定无法满足时,手动绑定也往往会更为灵活)** + +## 前置 + +在开始之前,我们需要知道有关 ScriptEngine 抽象层、相关 API 等相关知识,这部分内容如果已从 Cocos Creator 文档了解可跳过,直接进行 **实践** 部分。 + +### 抽象层 + +![JSB2.0-Architecture](jsb/JSB2.0-Architecture.png) + +首先先来看一下上图 Cocos 官方提供的一张抽象层架构,在 1.7 版本中,抽象层被设计为一个与引擎没有关系的独立模块,对 JS 引擎的管理从 `ScriptingCore` 被移动到了 `se::ScriptEngine` 类中,`ScriptingCore` 被保留下来是希望通过它把引擎的一些事件传递给封装层,充当适配器的角色。在这个抽象层提供了对 JavaScriptCore、SpiderMonkey、V8、ChakraCore 等多种可选的 JS 执行引擎的封装。JSB 的大部分工作其实就是设定 JS 相关操作的 C++ 回调,在回调函数中关联 C++ 对象。它其实主要包含如下两种类型: + +- 注册 JS 函数(包含全局函数,类构造函数、类析构函数、类成员函数,类静态成员函数),绑定一个 C++ 回调 +- 注册 JS 对象的属性读写访问器,分别绑定读与写的 C++ 回调 + +考虑到不同多种 JS 引擎的关键方法的定义各不相同,Cocos 团队使用 **宏** 来抹平这种回调函数定义与参数类型的差异,这里就不展开,详细可阅读文末 Cocos Creator 的相关文档。
+**值得一提的是,ScriptEngine 这层设计之初 Cocos 团队就将其定义为一个独立模块,完全不依赖 Cocos 引擎。** 我们开发者完全可以把 **cocos/bindings/jswrapper** 下的所有抽象层源码移植到其他项目中直接使用。 + +### SE 类型 + +C++ 抽象层所有的类型都在 `se` 命名空间下,其为 ScriptEngine 的缩写。 + +- **se::ScriptEngine** + + 它是 JS 引擎的管理员,掌管 JS 引擎初始化、销毁、重启、Native 模块注册、加载脚本、强制垃圾回收、JS 异常清理、是否启用调试器。它是一个单例,可通过 `se::ScriptEngine::getInstance()` 得到对应的实例。 + +- **se::Value** + + 可以被理解为 JS 变量在 C++ 层的引用。JS 变量有 `object`、`number`、`string`、`boolean`、`null`、`undefined` 六种类型,因此 `se::Value` 使用 union 包含 `object`、`number`、`string`、`boolean` 4 种有值类型,无值类型: `null`、`undefined` 可由私有变量 `_type` 直接表示。 + + 如果 `se::Value` 中保存基础数据类型,比如 `number`、`string`、`boolean`,其内部是直接存储一份值副本。`object` 的存储比较特殊,是通过 `se::Object*` 对 JS 对象的弱引用。 + +- **se::Object** + + 继承于 `se::RefCounter` 引用计数管理类,它保存了对 JS 对象的弱引用。我们在绑定回调中如果需要用到当前对象对应的 `se::Object`,只需要通过 `s.thisObject()` 即可获取。其中 s 为 `se::State` 类型。 + +- **se::Class** + + 用于暴露 C++ 类到 JS 中,它会在 JS 中创建一个对应名称的构造函数。Class 类型创建后,不需要手动释放内存,它会被封装层自动处理。`se::Class` 提供了一些 API 用于定义 Class 的创建、静态/动态成员函数、属性读写等等,后面在实践时用到会做介绍。完整内容可查阅 Cocos Creator 文档。 + +- **se::State** + + 它是绑定回调中的一个环境,我们通过 `se::State` 可以取得当前的 C++ 指针、`se::Object` 对象指针、参数列表、返回值引用。 + +### 宏 + +前面有提到,抽象层使用宏来抹平不同 JS 引擎关键函数定义与参数类型的不同,不管底层是使用什么引擎,开发者统一使用一种函数的定义。 + +例如,抽象层所有的 JS 到 C++ 的回调函数的定义为: + +```cpp +bool foo(se::State& s) +{ + ... + ... +} +SE_BIND_FUNC(foo) // 此处以回调函数的定义为例 +``` + +我们在编写完回调函数后,需要记住使用 `SE_BIND_XXX` 系列的宏对回调函数进行包装。目前全部的 `SE_BIND_XXX` 宏如下所示: + +- `SE_BIND_PROP_GET`:包装一个 JS 对象属性读取的回调函数 +- `SE_BIND_PROP_SET`:包装一个 JS 对象属性写入的回调函数 +- `SE_BIND_FUNC`:包装一个 JS 函数,可用于全局函数、类成员函数、类静态函数 +- `SE_DECLARE_FUNC`:声明一个 JS 函数,一般在 `.h` 头文件中使用 +- `SE_BIND_CTOR`:包装一个 JS 构造函数 +- `SE_BIND_SUB_CLS_CTOR`:包装一个 JS 子类的构造函数,此子类可以继承 +- `SE_BIND_FINALIZE_FUNC`:包装一个 JS 对象被 GC 回收后的回调函数 +- `SE_DECLARE_FINALIZE_FUNC`:声明一个 JS 对象被 GC 回收后的回调函数 +- `_SE`:包装回调函数的名称,转义为每个 JS 引擎能够识别的回调函数的定义,注意,第一个字符为下划线,类似 Windows 下用的 _T("xxx") 来包装 Unicode 或者 MultiBytes 字符串 + +在我们的简化版例子中,只需要用到 `SE_DECLARE_FUNC`、`SE_BIND_FUNC` 即可。 + +### 类型转换辅助函数 + +类型转换辅助函数位于 **cocos/bindings/manual/jsb_conversions.h** 中,包含了 `se::Value` 与 C++ 类型相互转化的方法。主要是以下两个: + +- `bool sevalue_to_native(const se::Value &from, T *to, se::Object * /*ctx*/)`, 从 `se::Value` 到 C++ 类型 +- `bool nativevalue_to_se(const T &from, se::Value& out, se::Object * /*ctx*/)`, 从 C++ 类型到 `se::Value` + +> 第三个参数多数情况可以直接传 `nullptr`, 目前只有 `function` 类型对 `ctx` 又依赖. + +## 实践 + +在开始之前,我们需要明确一下流程。JSB 绑定简单来讲就是在 C++ 层实现一些类库,然后经过一些特定处理可以在 JS 端进行对应方法调用的过程。因为采用 JS 为主要业务编写语言,使得我们在做一些 Native 的功能时会比较受限,例如文件、网络等等相关操作。 + +以 Cocos Creator 文档中 `spine.SkeletonRenderer` 为例,在 JSB 中 如果使用 `new` 操作符来调用 `spine.SkeletonRenderer` 的构造函数,实际上在 C++ 层会调用 `js_spine_SkeletonRenderer_constructor` 函数。在这个 C++ 函数中,会为这个精灵对象分配内存,并把它添加到自动回收池,然后调用 JS 层的 `_ctor` 函数来完成初始化。在 `_ctor` 函数中会根据参数类型和数量调用不同的 init 函数,这些 init 函数也是 C++ 函数的绑定: + +```cpp +#define SE_BIND_CTOR(funcName, cls, finalizeCb) \ + void funcName##Registry(const v8::FunctionCallbackInfo& _v8args) \ + { \ + v8::Isolate* _isolate = _v8args.GetIsolate(); \ + v8::HandleScope _hs(_isolate); \ + bool ret = true; \ + se::ValueArray args; \ + se::internal::jsToSeArgs(_v8args, &args); \ + se::Object* thisObject = se::Object::_createJSObject(cls, _v8args.This()); \ + thisObject->_setFinalizeCallback(_SE(finalizeCb)); \ + se::State state(thisObject, args); \ + ret = funcName(state); \ + if (!ret) { \ + SE_LOGE("[ERROR] Failed to invoke %s, location: %s:%d\n", #funcName, __FILE__, __LINE__); \ + } \ + se::Value _property; \ + bool _found = false; \ + _found = thisObject->getProperty("_ctor", &_property); \ + if (_found) _property.toObject()->call(args, thisObject); \ + } +``` + +三层的方法对应关系如下: + +| Javascript | JSB | Cocos Creator | +| :--------- | :-----| :----------- | +| jsb.SkeletonRenderer.initWithSkeleton | js_spine_SkeletonRenderer_initWithSkeleton | spine::SkeletonRenderer::initWithSkeleton | +| jsb.SkeletonRenderer.initWithUUID | js_spine_SkeletonRenderer_initWithUUID | spine::SkeletonRenderer::initWithUUID | + +这个调用过程的时序如下: + + +

调用时序图(引自 Cocos Creator 文档)

+ +和上面的过程类似。首先,我们需要确定接口和字段,我们随便拟定一个最简单的下载器 `FileDownloader`,它所具备的是 `download(url, path, callback)` 接口,而在 `callback` 中我们需要拿到的则是 `code`,`msg`。并且为了方便使用,我们将它挂载在 `jsb` 对象下,这样我们便可以使用如下代码进行简单地调用: + +```js +jsb.fileDownloader.download(url, path, (msg, code) => { + // do whatever you want +}); +``` + +确定接口后,我们可以开始着手码 C++ 部分了。首先来一发 `FileDownloader.h`,作为公共头文件供 Android/iOS 使用。接着 Android/iOS 分别实现各自的具体下载实现即可(此处略过),`reqCtx` 则用于存储回调对应关系: + +```cpp +class FileDownloader { + public: + typedef std::function ResultCallback; + static FileDownloader* getInstance(); + static void destroyInstance(); + void download(const std::string& url, + const std::string& savePath, + const ResultCallback& callback); + void onDownloadResult(const std::string msg, const int code); + ... ... + protected: + static FileDownloader* s_sharedFileDownloader; + std::unordered_map reqCtx; +}; +``` + +接下来我们进行最关键的绑定部分。
+因为下载器就功能上分类属于 network 模块,我们可以选择将我们的 `FileDownloader` 的绑定实现在 Cocos 源码中现有的 `jsb_cocos_network_auto` 中。在 `jsb_cocos_network_auto.h` 中声明 JS 函数: + +```cpp +SE_DECLARE_FUNC(js_network_FileDownloader_download); // 声明成员函数,下载调用 +SE_DECLARE_FUNC(js_network_FileDownloader_getInstance); // 声明静态函数,获取单例 +``` + +随后在 `jsb_cocos_network_auto.cpp` 中来注册 `FileDownloader` 和新声明的这两个函数到 JS 虚拟机中。首先先写好对应的两个方法实现留空,等注册逻辑完成后再来补全: + +```cpp +static bool js_network_FileDownloader_download(se::State &s) { // 方法名与声明时一致 + // TODO +} + +SE_BIND_FUNC(js_network_FileDownloader_download); // 包装该方法 + +static bool js_network_FileDownloader_getInstance(se::State& s) { // 方法名与声明时一致 + // TODO +} + +SE_BIND_FUNC(js_network_FileDownloader_getInstance); // 包装该方法 +``` + +现在我们开始编写注册逻辑,新增一个注册方法用于收归 `FileDownloader` 的全部注册逻辑: + +```cpp +bool js_register_network_FileDownloader(se::Object* obj) { + auto cls = se::Class::create("FileDownloader", obj, nullptr, nullptr); + cls->defineFunction("download", _SE(js_network_FileDownloader_download)); + cls->defineStaticFunction("getInstance", _SE(js_network_FileDownloader_getInstance)); + cls->install(); + JSBClassType::registerClass(cls); + se::ScriptEngine::getInstance()->clearException(); + return true; +} +``` + +我们来看看这个方法里做了些什么重要的事情: + +1. 调用 `se::Class::create(className, obj, parentProto, ctor)` 方法,创建了一个名为 `FileDownloader` 的 Class,注册成功后,在 JS 层中可以通过 `let xxx = new FileDownloader();`的方式创建实例。 +2. 调用 `defineFunction(name, func)` 方法,定义了一个成员函数 `download`,并将其实现绑定到包装后的 `js_network_FileDownloader_download` 上。 +3. 调用 `defineStaticFunction(name, func)` 方法,定义了一个静态成员函数 `getInstance`,并将其实现绑定到包装后的 `js_network_FileDownloader_getInstance` 上。 +4. 调用 `install()` 方法,将自己注册到 JS 虚拟机中。 +5. 调用 `JSBClassType::registerClass` 方法,将生成的 Class 与 C++ 层的类对应起来(内部通过 `std::unordered_map` 实现)。 + +通过以上这几步,我们完成了关键的注册部分,当然不要忘记在 `network` 模块的注册入口添加 `js_register_network_FileDownloader` 的调用: + +```cpp +bool register_all_cocos_network(se::Object* obj) +{ + // Get the ns + se::Value nsVal; + if (!obj->getProperty("jsb", &nsVal)) + { + se::HandleObject jsobj(se::Object::createPlainObject()); + nsVal.setObject(jsobj); + obj->setProperty("jsb", nsVal); + } + se::Object* ns = nsVal.toObject(); + + ... ... + // 将前面生成的 Class 注册 设置为 jsb 的一个属性,这样我们便能通过 + // let downloader = new jsb.FileDownloader(); + // 获取实例 + js_register_network_FileDownloader(ns); + return true; +} +``` + +完成这一步,我们的 Class 已经成功绑定,现在回来继续完善刚才留空的方法。 + +首先是 `getInstance()`: + +```cpp +static bool js_network_FileDownloader_getInstance(se::State& s) +{ + const auto& args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 0) { + FileDownloader* result = FileDownloader::getInstance(); // C++ 单例 + ok &= nativevalue_to_se(result, s.rval(), nullptr); + SE_PRECONDITION2(ok, false, "js_network_FileDownloader_getInstance : Error processing arguments"); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 0); + return false; +} +``` + +前面提到,我们可以通过 `se::State` 获取到 C++ 指针、`se::Object` 对象指针、参数列表、返回值引用。梳理逻辑如下: + +1. `args()` 获取 JS 带过来的全部参数(`se::Value` 的 vector); +2. 参数个数判断,因为这里的 `getInstance()` 并不需要额外参数,因此参数为 0; +3. `native_ptr_to_seval()` 用于在绑定层根据一个 C++ 对象指针获取一个 `se::Value`,并赋返回值给 `rval()` 至 JS 层; + +到这里,`getInstance()` 的绑定层逻辑已全部完成,我们已经可以通过 `let downloader = jsb.FileDownloader.getInstance()` 获取实例了。 + +接着是 `download()`: + +```cpp +static bool js_network_FileDownloader_download(se::State &s) { + FileDownloader *cobj = (FileDownloader *) s.nativeThisObject(); + SE_PRECONDITION2(cobj, false, + "js_network_FileDownloader_download : Invalid Native Object"); + const auto &args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 3) { + std::string url; + std::string path; + ok &= sevalue_to_native(args[0], &url, nullptr); // 转化为std::string url + ok &= sevalue_to_native(args[1], &path, nullptr); // 转化为std::string path + std::function callback; + do { + if (args[2].isObject() && args[2].toObject()->isFunction()) + { + se::Value jsThis(s.thisObject()); + // 获取 JS 回调 + se::Value jsFunc(args[2]); + // 如果目标类是一个单例则不能用 se::Object::attachObject 去关联 + // 必须使用 se::Object::root,无需关心 unroot,unroot 的操作会随着 lambda 的销毁触发 jsFunc 的析构,在 se::Object 的析构函数中进行 unroot 操作。 + // 如果使用 s.thisObject->attachObject(jsFunc.toObject);会导致对应的 func 和 target 永远无法被释放,引发内存泄露。 + jsFunc.toObject()->root(); + auto lambda = [=](const std::string& msg, + const int code) -> void { + se::ScriptEngine::getInstance()->clearException(); + se::AutoHandleScope hs; + CC_UNUSED bool ok = true; + se::ValueArray args; + args.resize(2); + ok &= nativevalue_to_se(msg, args[0], nullptr); + ok &= nativevalue_to_se(code, args[1], nullptr); + se::Value rval; + se::Object* thisObj = jsThis.isObject() ? jsThis.toObject() : nullptr; + se::Object* funcObj = jsFunc.toObject(); + // 执行 JS 方法回调 + bool succeed = funcObj->call(args, thisObj, &rval); + if (!succeed) { + se::ScriptEngine::getInstance()->clearException(); + } + }; + callback = lambda; + } + else + { + callback = nullptr; + } + } while(false); + SE_PRECONDITION2(ok, false, "js_network_FileDownloader_download : Error processing arguments"); + cobj->download(url, path, callback); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int) argc, 3); + return false; +} +``` + +1. 通过 `seval_to_std_string` 方法获取转化 C++ 后的 url、path 参数和原始 jsFunc。 +2. 手动构造回调 function,将 msg 和 code 转化为 `se::Value`。 +3. 通过 `funcObj->call` 执行 JS 方法进行回调。 + +最后,考虑到内存释放的风险,我们还需要在 `Application.cpp` 中的 `close()` 方法中进行相关回收: + +```cpp +network::FileDownloader::destroyInstance(); +``` + +================================================ + +以上就是全部的绑定流程,在分别编译到 Android/iOS 环境后,我们就能够通过 `jsb.fileDownloader.download()` 进行下载调用了。
+(PS:一定切记在使用前进行 `NATIVE` 的宏判断,因为非 JSB 环境下是无法使用的) + +```typescript +import {NATIVE} from 'cc/env'; +... +if(NATIVE) { + // JSB 相关逻辑 +} + +``` + +## 总结 + +我们现在来总结一下手动绑定改造的详细流程。一般而言,常用的 JSB 的改造流程大致如下: +- 确定方法接口与 JS/Native 公共字段 +- 声明头文件,并分别实现 Android JNI 与 OC 具体业务代码 +- 编写抽象层代码,将必要的类与对应方法注册到 JS 虚拟机中 +- 将绑定的类挂载在 JS 中的指定对象(类似命名空间)中 diff --git a/versions/4.0/zh/advanced-topics/jsb-optimizations.md b/versions/4.0/zh/advanced-topics/jsb-optimizations.md new file mode 100644 index 0000000000..d26ae8969c --- /dev/null +++ b/versions/4.0/zh/advanced-topics/jsb-optimizations.md @@ -0,0 +1,875 @@ +# 原生引擎跨语言调用优化 + +## 前言 + +在 Cocos Creator 3.6.0 版本的原生实现上,我们提高了原生(CPP)层级,主要体现在节点树(Scene、Node)、资产(Asset 及其子类)、材质系统(Material、Pass、ProgramLib)、3D 渲染器 (包含 Model、SubModel)、2D 渲染器(Batch2D、RenderEntity)都在 CPP 中实现,并通过绑定技术暴露给 JS 层使用。 + +众所周知,在 Cocos Creator 3.x 中,开发者只能使用 TS 脚本开发游戏业务逻辑。尽管我们将更多的引擎代码在 CPP 中实现,但是开发者并无法直接使用 CPP 语言进行游戏业务逻辑的开发,因此我们通过`脚本引擎封装层(Script Engine Wrapper)的 API,简称 SE API`,将这些 CPP 层实现的类型绑定并暴露给 JS 中,暴露到 JS 中的接口保持跟 Web 环境中一致。 + +原生层级的上移最直接的好处就是:在原生平台上引擎代码的执行性能得到提升,特别是不支持 JIT 的平台(比如:iOS)尤为明显。但在 3.6.0 正式版发布前,我们也面临了一系列由于`原生层级提升`带来的副作用,最主要的方面就是 JSB 调用(JS <-> CPP 语言之间的交互)次数比之前版本多了不少,而这直接导致`原生层级提升`带来的收益被抵消甚至性能相比之前版本( 3.5 )变得更差。本文将介绍一些降低 `JSB 调用 `的优化方式,若开发者自己的 CPP 代码中也存在类似的 JSB 调用过多的问题,希望本文也能提供一些优化思路。 + +## 共享内存 + +对 Node 中需要频繁同步的属性,我们使用 CPP 与 JS 共享内存的方式,避免 JSB 调用。为了更方便地共享 CPP 的内存到 JS 中,我们封装了 bindings::NativeMemorySharedToScriptActor 辅助类。 + +bindings/utils/BindingUtils.h + +```c++ +namespace cc::bindings { + +class NativeMemorySharedToScriptActor final { +public: + NativeMemorySharedToScriptActor() = default; + ~NativeMemorySharedToScriptActor(); + + void initialize(void *ptr, uint32_t byteLength); + void destroy(); + + inline se::Object *getSharedArrayBufferObject() const { return _sharedArrayBufferObject; } + +private: + se::Object *_sharedArrayBufferObject{nullptr}; + + CC_DISALLOW_COPY_MOVE_ASSIGN(NativeMemorySharedToScriptActor) +}; + +} // namespace cc::bindings +``` + +bindings/utils/BindingUtils.h + +```c++ +#include "bindings/utils/BindingUtils.h" +#include "bindings/jswrapper/SeApi.h" + +namespace cc::bindings { + +NativeMemorySharedToScriptActor::~NativeMemorySharedToScriptActor() { + destroy(); +} + +void NativeMemorySharedToScriptActor::initialize(void* ptr, uint32_t byteLength) { + CC_ASSERT_NULL(_sharedArrayBufferObject); + // The callback of freeing buffer is empty since the memory is managed in native, + // the external array buffer just holds a reference to the memory. + _sharedArrayBufferObject = se::Object::createExternalArrayBufferObject(ptr, byteLength, [](void* /*contents*/, size_t /*byteLength*/, void* /*userData*/) {}); + // Root 此对象,防止对象被 GC,当 Actor 对象销毁的时候会调用 destroy 函数,其内部会 unroot ArrayBuffer 对象 + _sharedArrayBufferObject->root(); +} + +void NativeMemorySharedToScriptActor::destroy() { + if (_sharedArrayBufferObject != nullptr) { + _sharedArrayBufferObject->unroot(); + _sharedArrayBufferObject->decRef(); + _sharedArrayBufferObject = nullptr; + } +} + +} // namespace cc::bindings +``` + +bindings/jswrapper/v8/Object.h + +```c++ +using BufferContentsFreeFunc = void (*)(void *contents, size_t byteLength, void *userData); +static Object *createExternalArrayBufferObject(void *contents, size_t byteLength, BufferContentsFreeFunc freeFunc, void *freeUserData = nullptr); +``` + +bindings/jswrapper/v8/Object.cpp + +```c++ +/* static */ +Object *Object::createExternalArrayBufferObject(void *contents, size_t byteLength, BufferContentsFreeFunc freeFunc, void *freeUserData /* = nullptr*/) { + Object *obj = nullptr; + std::shared_ptr backingStore = v8::ArrayBuffer::NewBackingStore(contents, byteLength, freeFunc, freeUserData); + v8::Local jsobj = v8::ArrayBuffer::New(__isolate, backingStore); + + if (!jsobj.IsEmpty()) { + obj = Object::_createJSObject(nullptr, jsobj); + } + return obj; +} +``` + +分析以上代码可知,其实 NativeMemorySharedToScriptActor 最终是调用 v8::ArrayBuffer::NewBackingStore 和 v8::ArrayBuffer::New 函数创建了一个 External 类型的 ArrayBuffer,取名为 External 的原因是其内存不在 V8 内部分配和管理,即其内存完全交由 V8 的上层管理,当 ArrayBuffer 对象被 GC 的后,freeFunc 回调函数会被触发。由于在 Node 中需要共享的内存是 Node 中的若干连续属性,内存的创建和释放完全交由 Node 自己维护,而 CPP Node 实例的销毁也是由 GC 控制的,因此 NativeMemorySharedToScriptActor::initialize 内部调用 se::Object::createExternalArrayBufferObject 的时候,传递了一个**空实现**的回调函数。 + +Node.h + +```c++ +class Node : public CCObject { + ...... + inline se::Object *_getSharedArrayBufferObject() const { return _sharedMemoryActor.getSharedArrayBufferObject(); } // NOLINT + ...... + bindings::NativeMemorySharedToScriptActor _sharedMemoryActor; + ...... + // Shared memory with JS + // NOTE: TypeArray created in node.jsb.ts _ctor should have the same memory layout + uint32_t _eventMask{0}; // Uint32: 0 + uint32_t _layer{static_cast(Layers::LayerList::DEFAULT)}; // Uint32: 1 + uint32_t _transformFlags{0}; // Uint32: 2 + index_t _siblingIndex{0}; // Int32: 0 + uint8_t _activeInHierarchy{0}; // Uint8: 0 + uint8_t _active{1}; // Uint8: 1 + uint8_t _isStatic{0}; // Uint8: 2 + uint8_t _padding{0}; // Uint8: 3 + ...... +}; +``` + +Node.cpp + +```c++ +Node::Node(const ccstd::string &name) { +#define NODE_SHARED_MEMORY_BYTE_LENGTH (20) + static_assert(offsetof(Node, _padding) + sizeof(_padding) - offsetof(Node, _eventMask) == NODE_SHARED_MEMORY_BYTE_LENGTH, "Wrong shared memory size"); + _sharedMemoryActor.initialize(&_eventMask, NODE_SHARED_MEMORY_BYTE_LENGTH); +#undef NODE_SHARED_MEMORY_BYTE_LENGTH + + _id = idGenerator.getNewId(); + if (name.empty()) { + _name.append("New Node"); + } else { + _name = name; + } +} +``` + +Node 构造函数中调用 `_sharedMemoryActor.initialize(&_eventMask, NODE_SHARED_MEMORY_BYTE_LENGTH);` 将 _eventMask 属性开始的 20 字节设置为共享内存。 + +node.jsb.ts + +> **注意**:所有的 .jsb.ts 结尾的文件最终在打包的时候会替换对应不带 .jsb 的文件,比如这里 node.jsb.ts 会替换掉 node.ts,具体可以查看引擎根目录下的 cc.config.json 文件,有对应的 overrides 字段:*"cocos/scene-graph/node.ts"*: "cocos/scene-graph/node.jsb.ts", + +```ts +// JS 的 _ctor 回调函数会在 JS 的 var node = new Node(); 流程的最后阶段被触发,即 CPP Node 对象创建之后,因此 _getSharedArrayBufferObject 绑定函数返回的 ArrayBuffer 一定存在。 +nodeProto._ctor = function (name?: string) { + ...... + // 通过 _getSharedArrayBufferObject 绑定方法,取得 CPP 共享给 JS 的 ArrayBuffer 对象 + const sharedArrayBuffer = this._getSharedArrayBufferObject(); + // Uint32Array with 3 elements, offset from the start: eventMask, layer, dirtyFlags + this._sharedUint32Arr = new Uint32Array(sharedArrayBuffer, 0, 3); + // Int32Array with 1 element, offset from 12th bytes: siblingIndex + this._sharedInt32Arr = new Int32Array(sharedArrayBuffer, 12, 1); + // Uint8Array with 3 elements, offset from 16th bytes: activeInHierarchy, active, static + this._sharedUint8Arr = new Uint8Array(sharedArrayBuffer, 16, 3); + // + + this._sharedUint32Arr[1] = Layers.Enum.DEFAULT; // this._sharedUint32Arr[1] is layer + ...... +}; +``` + +采用共享内存的方式,也意味着我们无法通过 JSB 绑定函数的方式把要设置的值从 JS 传递到 CPP 中。因此我们需要在 .jsb.ts 中定义对应的 getter / setter 函数,其内部通过直接操作 TypedArray 的方式修改共享内存。 + +```ts +Object.defineProperty(nodeProto, 'activeInHierarchy', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[0] != 0; // Uint8, 0: activeInHierarchy + }, + set (v) { + this._sharedUint8Arr[0] = (v ? 1 : 0); // Uint8, 0: activeInHierarchy + }, +}); + +Object.defineProperty(nodeProto, '_activeInHierarchy', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[0] != 0; // Uint8, 0: activeInHierarchy + }, + set (v) { + this._sharedUint8Arr[0] = (v ? 1 : 0); // Uint8, 0: activeInHierarchy + }, +}); + +Object.defineProperty(nodeProto, 'layer', { + configurable: true, + enumerable: true, + get () { + return this._sharedUint32Arr[1]; // Uint32, 1: layer + }, + set (v) { + this._sharedUint32Arr[1] = v; // Uint32, 1: layer + if (this._uiProps && this._uiProps.uiComp) { + this._uiProps.uiComp.setNodeDirty(); + this._uiProps.uiComp.markForUpdateRenderData(); + } + this.emit(NodeEventType.LAYER_CHANGED, v); + }, +}); + +Object.defineProperty(nodeProto, '_layer', { + configurable: true, + enumerable: true, + get () { + return this._sharedUint32Arr[1]; // Uint32, 1: layer + }, + set (v) { + this._sharedUint32Arr[1] = v; // Uint32, 1: layer + }, +}); + +Object.defineProperty(nodeProto, '_eventMask', { + configurable: true, + enumerable: true, + get () { + return this._sharedUint32Arr[0]; // Uint32, 0: eventMask + }, + set (v) { + this._sharedUint32Arr[0] = v; // Uint32, 0: eventMask + }, +}); + +Object.defineProperty(nodeProto, '_siblingIndex', { + configurable: true, + enumerable: true, + get () { + return this._sharedInt32Arr[0]; // Int32, 0: siblingIndex + }, + set (v) { + this._sharedInt32Arr[0] = v; // Int32, 0: siblingIndex + }, +}); + +nodeProto.getSiblingIndex = function getSiblingIndex() { + return this._sharedInt32Arr[0]; // Int32, 0: siblingIndex +}; + +Object.defineProperty(nodeProto, '_transformFlags', { + configurable: true, + enumerable: true, + get () { + return this._sharedUint32Arr[2]; // Uint32, 2: _transformFlags + }, + set (v) { + this._sharedUint32Arr[2] = v; // Uint32, 2: _transformFlags + }, +}); + +Object.defineProperty(nodeProto, '_active', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[1] != 0; // Uint8, 1: active + }, + set (v) { + this._sharedUint8Arr[1] = (v ? 1 : 0); // Uint8, 1: active + }, +}); + +Object.defineProperty(nodeProto, 'active', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[1] != 0; // Uint8, 1: active + }, + set (v) { + this.setActive(!!v); + }, +}); + +Object.defineProperty(nodeProto, '_static', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._sharedUint8Arr[2] != 0; + }, + set (v) { + this._sharedUint8Arr[2] = (v ? 1 : 0); + }, +}); +``` + +### 性能对比 + +![](jsb/opt-1.jpg) + +## 避免接口传参 + +如果 JSB 函数调用中包含参数,V8 内部需要对参数的合理性做校验,这些校验工作也会影响调用性能。针对 Node 中可能会高频调用的 JSB 函数,我们通过复用一个全局的 Float32Array 来避免浮点类型的参数传递。 + +scene-graph/utils.jsb.ts + +```ts +import { IMat4Like, Mat4 } from '../core/math'; + +declare const jsb: any; + +// For optimize getPosition, getRotation, getScale +export const _tempFloatArray = new Float32Array(jsb.createExternalArrayBuffer(20 * 4)); + +export const fillMat4WithTempFloatArray = function fillMat4WithTempFloatArray (out: IMat4Like) { + Mat4.set(out, + _tempFloatArray[0], _tempFloatArray[1], _tempFloatArray[2], _tempFloatArray[3], + _tempFloatArray[4], _tempFloatArray[5], _tempFloatArray[6], _tempFloatArray[7], + _tempFloatArray[8], _tempFloatArray[9], _tempFloatArray[10], _tempFloatArray[11], + _tempFloatArray[12], _tempFloatArray[13], _tempFloatArray[14], _tempFloatArray[15] + ); +}; +// +``` + +以上定义了一个全局的 `_tempFloatArray` ,用于存储 number 或者 number 的复合类型(Vec3/Vec4/Mat4等)参数。 + +node.jsb.ts + +```ts +// ...... +// 将 FloatArray 设置给 CPP 层 +Node._setTempFloatArray(_tempFloatArray.buffer); +// ...... +// 在 JS 中重新实现带有参数 setPosition 函数 +nodeProto.setPosition = function setPosition (val: Readonly | number, y?: number, z?: number) { + if (y === undefined && z === undefined) { + // 当 y 和 z 都是 undefined 的时候,表示第一个参数为 Vec3 类型的对象,_tempFloatArray 的第一个元素表示参数个数 + _tempFloatArray[0] = 3; + const pos = val as Vec3; + // 将新的 pos 赋值到 FloatArray 数组和 this._lpos 缓存中 + this._lpos.x = _tempFloatArray[1] = pos.x; + this._lpos.y = _tempFloatArray[2] = pos.y; + this._lpos.z = _tempFloatArray[3] = pos.z; + } else if (z === undefined) { + // 如果 z 为 undefined,那么只有 x 与 y 2 个参数 + _tempFloatArray[0] = 2; + this._lpos.x = _tempFloatArray[1] = val as number; + this._lpos.y = _tempFloatArray[2] = y as number; + } else { + _tempFloatArray[0] = 3; + this._lpos.x = _tempFloatArray[1] = val as number; + this._lpos.y = _tempFloatArray[2] = y as number; + this._lpos.z = _tempFloatArray[3] = z as number; + } + this._setPosition(); // 此为原生绑定出来的无参的函数 +}; +``` + +jsb_scene_manual.cpp + +```c++ +namespace { + +/** + * 对共享的全局 FloatArray 操作的辅助类 + */ +class TempFloatArray final { +public: + TempFloatArray() = default; + ~TempFloatArray() = default; + + inline void setData(float *data) { _data = data; } + + ...... + + inline const float &operator[](size_t index) const { return _data[index]; } + inline float &operator[](size_t index) { return _data[index]; } + +private: + float *_data{nullptr}; + + CC_DISALLOW_ASSIGN(TempFloatArray) +}; + +TempFloatArray tempFloatArray; + +} // namespace +``` + +```c++ +static bool js_scene_Node_setTempFloatArray(se::State &s) // NOLINT(readability-identifier-naming) +{ + const auto &args = s.args(); + size_t argc = args.size(); + CC_UNUSED bool ok = true; + if (argc == 1) { + uint8_t *buffer = nullptr; + args[0].toObject()->getArrayBufferData(&buffer, nullptr); + // 初始化 TempFloatArray 关联的数据 + tempFloatArray.setData(reinterpret_cast(buffer)); + return true; + } + SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d", (int)argc, 1); + return false; +} +SE_BIND_FUNC(js_scene_Node_setTempFloatArray) +``` + + + +```c++ +bool register_all_scene_manual(se::Object *obj) // NOLINT(readability-identifier-naming) +{ + ...... + __jsb_cc_Node_proto->defineFunction("_setPosition", _SE(js_scene_Node_setPosition)); + __jsb_cc_Node_proto->defineFunction("_setScale", _SE(js_scene_Node_setScale)); + __jsb_cc_Node_proto->defineFunction("_setRotation", _SE(js_scene_Node_setRotation)); + __jsb_cc_Node_proto->defineFunction("_setRotationFromEuler", _SE(js_scene_Node_setRotationFromEuler)); + __jsb_cc_Node_proto->defineFunction("_rotateForJS", _SE(js_scene_Node_rotateForJS)); + ...... +} +``` + +```c++ +// 无参的 node._setPosition() 绑定 +static bool js_scene_Node_setPosition(void *s) // NOLINT(readability-identifier-naming) +{ + auto *cobj = reinterpret_cast(s); + auto argc = static_cast(tempFloatArray[0]); + if (argc == 2) { + // 从 tempFloatArray 中获取参数 + cobj->setPositionInternal(tempFloatArray[1], tempFloatArray[2], true); + } else { + cobj->setPositionInternal(tempFloatArray[1], tempFloatArray[2], tempFloatArray[3], true); + } + return true; +} +SE_BIND_FUNC_FAST(js_scene_Node_setPosition) // 注意,这里使用了新的 SE_BIND_FUNC_FAST 宏 +``` + +Node.h + +```c++ + inline void setPositionInternal(float x, float y, bool calledFromJS) { setPositionInternal(x, y, _localPosition.z, calledFromJS); } + void setPositionInternal(float x, float y, float z, bool calledFromJS); +``` + +bindings/jswrapper/v8/HelperMacros.h + +```c++ +#define SE_BIND_FUNC(funcName) \ + void funcName##Registry(const v8::FunctionCallbackInfo &_v8args) { \ + JsbInvokeScope(#funcName); \ + jsbFunctionWrapper(_v8args, funcName, #funcName); \ + } + +#define SE_BIND_FUNC_FAST(funcName) \ + void funcName##Registry(const v8::FunctionCallbackInfo &_v8args) { \ + auto *thisObject = static_cast(_v8args.This()->GetAlignedPointerFromInternalField(0)); \ + auto *nativeObject = thisObject != nullptr ? thisObject->getPrivateData() : nullptr; \ + funcName(nativeObject); \ + } +``` + +bindings/jswrapper/v8/HelperMacros.cpp + + + +```c++ +// SE_BIND_FUNC 宏会调用 jsbFunctionWrapper 函数,其内部做了更多的事情 +SE_HOT void jsbFunctionWrapper(const v8::FunctionCallbackInfo &v8args, se_function_ptr func, const char *funcName) { + bool ret = false; + v8::Isolate *isolate = v8args.GetIsolate(); + v8::HandleScope scope(isolate); + bool needDeleteValueArray{false}; + se::ValueArray &args = se::gValueArrayPool.get(v8args.Length(), needDeleteValueArray); + se::CallbackDepthGuard depthGuard{args, se::gValueArrayPool._depth, needDeleteValueArray}; + se::internal::jsToSeArgs(v8args, args); + se::Object *thisObject = se::internal::getPrivate(isolate, v8args.This()); + se::State state(thisObject, args); + ret = func(state); + if (!ret) { + SE_LOGE("[ERROR] Failed to invoke %s\n", funcName); + } + se::internal::setReturnValue(state.rval(), v8args); +} +``` + +SE_BIND_FUNC_FAST 宏的内部实现非常快,因为其内部非常简单,拿到 private data 后立马出发回调函数,没有任何 se 相关的 API 调用。这避免了 se::Value 与 jsvalue 之间的转换,也避免了 V8 内部对参数进行校验。大家可以对比一下标准的 SE_BIND_FUNC 宏的内部实现。 + +另外,你可能会好奇,为什么直接不通过「共享内存」章节的方式来共享 position、rotation、scale 信息,而要通过 `_setPosition()`,`_setRotation()`,`_setScale()` 这种**无参**的 JSB 方法调用。原因是,这里的这个 JSB 调用无法被优化掉,我们来看 Node::setPosition 的实现: + +```c++ +class Node : public CCObject { + ...... + inline void setPosition(const Vec3 &pos) { setPosition(pos.x, pos.y, pos.z); } + inline void setPosition(float x, float y) { setPosition(x, y, _localPosition.z); } + inline void setPosition(float x, float y, float z) { setPositionInternal(x, y, z, false); } + inline void setPositionInternal(float x, float y, bool calledFromJS) { setPositionInternal(x, y, _localPosition.z, calledFromJS); } + void setPositionInternal(float x, float y, float z, bool calledFromJS); + + ...... + inline uint32_t getChangedFlags() const { + return _hasChangedFlagsVersion == globalFlagChangeVersion ? _hasChangedFlags : 0; + } + inline void setChangedFlags(uint32_t value) { + _hasChangedFlagsVersion = globalFlagChangeVersion; + _hasChangedFlags = value; + } + ...... +}; +``` + +```c++ +void Node::setPositionInternal(float x, float y, float z, bool calledFromJS) { + _localPosition.set(x, y, z); + invalidateChildren(TransformBit::POSITION); + + if (_eventMask & TRANSFORM_ON) { + emit(TransformBit::POSITION); + } + + if (!calledFromJS) { + notifyLocalPositionUpdated(); + } +} +``` + +setPosition 不止对 _localPostion 赋值,其还需要触发 invalidateChildren 函数的调用,invalidateChildren 是个递归函数,其内部还会遍历所有子节点,进而修改一些其它属性,比如 _transformFlags, _hasChangedFlagsVersion, _hasChangedFlags。因此,我们无法通过第一章节中「共享内存」的方式来优化 setPosition。 + +### 性能对比 + +![](jsb/opt-2.jpg) + +## 缓存属性 + +在 JS 层中缓存属性,避免 getter 访问 c++ 接口,也能减少 JSB 调用。 + +node.jsb.ts + +```ts +Object.defineProperty(nodeProto, 'position', { + configurable: true, + enumerable: true, + get (): Readonly { + return this._lpos; + }, + set (v: Readonly) { + this.setPosition(v as Vec3); + }, +}); + +nodeProto.getPosition = function getPosition (out?: Vec3): Vec3 { + if (out) { + return Vec3.set(out, this._lpos.x, this._lpos.y, this._lpos.z); + } + return Vec3.copy(new Vec3(), this._lpos); +}; + +nodeProto._ctor = function (name?: string) { + ...... + this._lpos = new Vec3(); + this._lrot = new Quat(); + this._lscale = new Vec3(1, 1, 1); + this._euler = new Vec3(); + ....... +}; +``` + +### 性能对比 + +![](jsb/opt-3.jpg) + +## 节点同步 + +用户的逻辑代码中,也经常会这样使用: + +```ts +const children = node.children; +for (let i = 0; i < children.length; ++i) { + const child = children[i]; + // do something with child +} +``` + +`.children` getter 可能会被频繁调用,这也导致 JSB 调用过多。children 的 getter 绑定比较特殊,因为它是一个 JS array,在 CPP 中,它对应的是 `ccstd::vector _children;`类型,因此 JS Array 与 CPP 的 std::vector 并没有很好的方式进行数据同步。如果每次调用 `.children` getter 都是 JSB 的方式返回的话,那么每次调用都会通过 `se::Object::createArrayObject` 生成一个临时的 JS Array ,并通过 `nativevalue_to_se` 依次转换 CPP 的 child 为 JS 的 child 并赋值到 JS Array 中,这算是比较重度的转换开销,而且会生成需要被 GC 的临时数组,给 GC 造成一定的额外压力。 + +为了解决此问题,我们在 JS 层中缓存了 _children 属性,当 CPP 层中有更改 _children 时候,通过监听 ChildAdded, ChildRemoved 事件的方式通知 JS 层中的 _children 更新内容。 + +以下以 `node.addChild(child);` 为例阐述: + +```c++ +class Node : public CCObject { + ...... + inline void addChild(Node *node) { node->setParent(this); } + + inline void removeChild(Node *node) const { + auto idx = getIdxOfChild(_children, node); + if (idx != -1) { + node->setParent(nullptr); + } + } + inline void removeFromParent() { + if (_parent) { + _parent->removeChild(this); + } + } + void removeAllChildren(); + ...... +}; +``` + +```c++ +void Node::setParent(Node *parent, bool isKeepWorld /* = false */) { + ...... + onSetParent(oldParent, isKeepWorld); + emit(oldParent); + if (oldParent) { + if (!(oldParent->_objFlags & Flags::DESTROYING)) { + index_t removeAt = getIdxOfChild(oldParent->_children, this); + if (removeAt < 0) { + return; + } + // 将 child 从旧的父节点中移除 + oldParent->_children.erase(oldParent->_children.begin() + removeAt); + oldParent->updateSiblingIndex(); + oldParent->emit(this); + } + } + if (newParent) { + ...... + // 将 child 添加到新的父节点中 + newParent->_children.emplace_back(this); + _siblingIndex = static_cast(newParent->_children.size() - 1); + newParent->emit(this); + } + onHierarchyChanged(oldParent); +} +``` + +调用完 addChild 后,CPP 层中的 Node::_children 被修改后,会触发 `emit` 事件,Node 初始化的时候会设置监听此事件,见 jsb_scene_manual.cpp 中的代码: + +```c++ +// jsb_scene_manual.cpp +static void registerOnChildAdded(cc::Node *node, se::Object *jsObject) { + node->on( + [jsObject](cc::Node * /*emitter*/, cc::Node *child) { + se::AutoHandleScope hs; + se::Value arg0; + nativevalue_to_se(child, arg0); + // 调用 JS 的私有函数 _onChildAdded + se::ScriptEngine::getInstance()->callFunction(jsObject, "_onChildAdded", 1, &arg0); + }); +} + +static bool js_scene_Node_registerOnChildAdded(se::State &s) // NOLINT(readability-identifier-naming) +{ + auto *cobj = SE_THIS_OBJECT(s); + SE_PRECONDITION2(cobj, false, "Invalid Native Object"); + + auto *jsObject = s.thisObject(); + + registerOnChildAdded(cobj, jsObject); + return true; +} +SE_BIND_FUNC(js_scene_Node_registerOnChildAdded) // NOLINT(readability-identifier-naming) + +bool register_all_scene_manual(se::Object *obj) // NOLINT(readability-identifier-naming) +{ + ...... + __jsb_cc_Node_proto->defineFunction("_registerOnChildAdded", _SE(js_scene_Node_registerOnChildAdded)); + ...... +} +``` + +node.jsb.ts + +```ts +Object.defineProperty(nodeProto, 'children', { + configurable: true, + enumerable: true, + get () { + return this._children; + }, + set (v) { + this._children = v; + }, +}); + +nodeProto._onChildRemoved = function (child) { + this.emit(NodeEventType.CHILD_REMOVED, child); +}; + +// 此函数会被 CPP 层 registerOnChildAdded 函数中调用 +nodeProto._onChildAdded = function (child) { + this.emit(NodeEventType.CHILD_ADDED, child); +}; + +nodeProto.on = function (type, callback, target, useCapture: any = false) { + switch (type) { + ...... + case NodeEventType.CHILD_ADDED: + if (!(this._registeredNodeEventTypeMask & REGISTERED_EVENT_MASK_CHILD_ADDED_CHANGED)) { + this._registerOnChildAdded(); // 调用 JSB 方法注册监听 + this._registeredNodeEventTypeMask |= REGISTERED_EVENT_MASK_CHILD_ADDED_CHANGED; + } + break; + ...... + default: + break; + } + this._eventProcessor.on(type, callback, target, useCapture); +}; + +nodeProto._ctor = function (name?: string) { + ...... + this._children = []; + + // 使用 on 接口监听 CHILD_ADDED 和 CHILD_REMOVED 事件 + this.on(NodeEventType.CHILD_ADDED, (child) => { + // 同步 JS 层中的 this._children 数组 + this._children.push(child); + }); + + this.on(NodeEventType.CHILD_REMOVED, (child) => { + const removeAt = this._children.indexOf(child); + if (removeAt < 0) { + errorID(1633); + return; + } + this._children.splice(removeAt, 1); + }); + + ...... +}; +``` + +### 性能对比 + +![](jsb/opt-4.jpg) + +## 参数数组对象池 + +截止 3.6.0,Cocos Creator 引擎还未实现高效的内存池,而引擎内部使用 se ( Script Engine Wrapper )在做 JS -> CPP 交互的时候,都需要生成临时的 `se::ValueArray args(argCount);`,se::ValueArray 类型是 `ccstd::vector` 的 `typedef`, 因此会有大量临时内存的申请、释放,极大影响性能。在 3.6.0 之前的版本并没有暴露出此问题是因为原来的原生层级比较低,JSB 调用次数比较低,而 3.6.0 版本中,随着原生层级提升,JSB 调用变多,此问题变得愈发严重。 + +我们想到的解决方案是:使用对象池的方式复用 se::ValueArray。对象池(se::ValueArrayPool)的实现也比较简单,具体如下: + +bindings/jswrapper/ValueArrayPool.h + +```c++ +// 调用深度警卫类,用于使用完 se::ValueArray 后对其中的 se::Value 进行复位的操作 +// 如果 se::ValueArray 是 new 出来的,将会处理 delete 流程 +class CallbackDepthGuard final { +public: + CallbackDepthGuard(ValueArray &arr, uint32_t &depth, bool needDelete) + : _arr(arr), _depth(depth), _needDelete(needDelete) { + ++_depth; + } + + ~CallbackDepthGuard() { + --_depth; + for (auto &e : _arr) { + e.setUndefined(); + } + if (_needDelete) { + delete &_arr; + } + } + +private: + ValueArray &_arr; + uint32_t &_depth; + const bool _needDelete{false}; +}; + +class ValueArrayPool final { +public: + // 绑定函数的参数个数的上限为 20,如果有参数大于 20 个的情况,需要考虑重构函数参数 + static const uint32_t MAX_ARGS = 20; + + ValueArrayPool(); + + ValueArray &get(uint32_t argc, bool &outNeedDelete); + + uint32_t _depth{0}; + +private: + void initPool(uint32_t index); + ccstd::vector> _pools; +}; + +extern ValueArrayPool gValueArrayPool; +``` + +bindings/jswrapper/ValueArrayPool.cpp + +```c++ +ValueArrayPool gValueArrayPool; + +// 定义最大深度为 5,如果超过此深度,则不使用对象池 +#define SE_DEFAULT_MAX_DEPTH (5) + +ValueArrayPool::ValueArrayPool() { + _pools.resize(SE_DEFAULT_MAX_DEPTH); + for (uint32_t i = 0; i < SE_DEFAULT_MAX_DEPTH; ++i) { + initPool(i); + } +} + +ValueArray &ValueArrayPool::get(uint32_t argc, bool &outNeedDelete) { + // 如果深度大于对象池的大小,直接 new ValueArray + if (SE_UNLIKELY(_depth >= _pools.size())) { + outNeedDelete = true; + auto *ret = ccnew ValueArray(); + ret->resize(argc); + return *ret; + } + + outNeedDelete = false; + CC_ASSERT_LE(argc, MAX_ARGS); + // 从对象池中取出 ValueArray 对象 + auto &ret = _pools[_depth][argc]; + CC_ASSERT(ret.size() == argc); + return ret; +} + +// 初始化对象池 +void ValueArrayPool::initPool(uint32_t index) { + auto &pool = _pools[index]; + uint32_t i = 0; + for (auto &arr : pool) { + arr.resize(i); + ++i; + } +} +``` + +我们回头再来看下 jsbFunctionWrapper 的实现: + +```c++ +SE_HOT void jsbFunctionWrapper(const v8::FunctionCallbackInfo &v8args, se_function_ptr func, const char *funcName) { + bool ret = false; + v8::Isolate *isolate = v8args.GetIsolate(); + v8::HandleScope scope(isolate); + + /* 原来的代码 + se::ValueArray args; + args.reserve(10); + */ + + // 新实现的优化代码------ 开始 ------- + bool needDeleteValueArray{false}; + // 从全局的 se::ValueArray 对象池中取出 se::ValueArray,注意:needDeleteValueArray 是输出参数 + se::ValueArray &args = se::gValueArrayPool.get(v8args.Length(), needDeleteValueArray); + // 定义「调用深度警卫」变量 depthGuard,当此变量销毁的时候,会自动清理对象池 + se::CallbackDepthGuard depthGuard{args, se::gValueArrayPool._depth, needDeleteValueArray}; + // 新实现的优化代码------ 结束 ------- + + se::internal::jsToSeArgs(v8args, args); + se::Object *thisObject = se::internal::getPrivate(isolate, v8args.This()); + se::State state(thisObject, args); + ret = func(state); + if (!ret) { + SE_LOGE("[ERROR] Failed to invoke %s\n", funcName); + } + se::internal::setReturnValue(state.rval(), v8args); +} +``` + +### 性能对比 + +![opt-5.jpg](jsb/opt-5.jpg) + +## 总结 + +在 Cocos Creator 3.6.0 版本中,我们通过以下主要优化手段改进了原生引擎的性能: + +1. 在原生层(C++层)中实现引擎的核心模块,充分利用 C++ 代码的执行性能。 +2. 尽可能减少跨语言的交互(JS <-> CPP)频率,我们采用了以下五种方法进行优化: + - 共享内存:通过减少内存拷贝的方式提高性能。 + - 避免接口传参:使用成员变量替代频繁的参数传递。 + - 缓存属性:将频繁访问的属性缓存起来,避免重复获取。 + - 节点同步:在节点操作时,通过事件监听机制同步节点的变化。 + - 参数数组对象池:使用对象池复用参数数组对象,减少内存分配和释放的开销。 + +通过这些优化措施,我们改进了 Cocos Creator 引擎的性能表现。这些优化技术可以显著降低跨语言交互的开销,提高了引擎的整体性能和响应速度。 diff --git a/versions/4.0/zh/advanced-topics/jsb-sebind.md b/versions/4.0/zh/advanced-topics/jsb-sebind.md new file mode 100644 index 0000000000..b6319e2e47 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/jsb-sebind.md @@ -0,0 +1,571 @@ +# `sebind` 使用教程 + +在 `sebind` 之前,不论是手动绑定还是自动绑定,开发者需要比较多的步骤才能完成绑定,而且需要对 JSB 有比较多了解。`sebind` 利用 C++ 的模板,最大化地减少了中间代码。 + +## 简单的示例 + +我们将在全局空间中定义一个对象 `simpleMath`,并且添加一个 `lerp` 方法: + +```js +let v = simpleMath.lerp(a, b, t); +``` + +这个方法在 C++ 中实现。 + +### 准备工作 + +我们需要创建一个空的工程,保存场景,并且新建任一原生平台的构建任务。本示例使用的是 Windows 平台。 + +### 第一步:添加绑定代码 + +在 `native/engine/common/Classes` 目录,新建文件 `HelloSEBind.cpp`,写入以下内容: + +```c++ +// HelloSEBind.cpp +#include "bindings/sebind/sebind.h" + +namespace { +struct Empty {}; // act as a namespace +float lerp(float a, float b, float t) { return (1 - t) * a + t * b; } +} // namespace + +bool jsb_register_simple_math(se::Object *globalThis) { + + sebind::class_ demoMathClass("simpleMath"); + { + // invoke through simpleMath.lerp(a, b, t); + demoMathClass.staticFunction("lerp", &lerp).install(globalThis); + } + return true; +} +``` + +在 `native/engine/common/CMakeLists.txt` 中加入源文件 `HelloSEBind.cpp`: + +```cmake + +# ... + +list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp + ${CMAKE_CURRENT_LIST_DIR}/Classes/HelloSEBind.cpp # new file +) +``` + +### 第二步:注册到 `ScriptEngine` + +编辑 `Game.cpp`,位于 `native/engine/common/Classes` 目录: + +```c++ +// ... + +// declare registry function +extern bool jsb_register_simple_math(se::Object *); // #1 + +Game::Game() = default; + + +int Game::init() { + +// ... + _xxteaKey = SCRIPT_XXTEAKEY; + + // add callback to script engine + auto *seengine = se::ScriptEngine::getInstance(); // #2 + seengine->addRegisterCallback(jsb_register_simple_math); // #3 + + BaseGame::init(); + //.... +``` + +按照上述标记(#1、2、3)依次添加编辑对应的代码。 + +### 第三步:验证 + +启动程序,在 Debug 模式下通过 Chrome devtools 验证: + +![devtool](./sebind/devtool-test.png) + +此时可观察到导出成功。 + +相比之前的做法,对 `se` 接口的使用大大减少,代码量也随之减少。 + +## 更复杂的绑定示例 + +`sebind` 以类为单位进行接口绑定,每一个 JS 类都需要构造对应的 `sebind::class_` 实例。所有的类的绑定都是通过 `sebind::class_` 所提供的方法来完成的。 + +下面,我们通过导出示例代码的 `User` 类来熟悉 `sebind` 的绑定流程。 + +> **注意**:示例代码只用作说明 `sebind` 的用法, 内部接口设计和实现在实际开发中无参考价值。 + +```c++ +class User { +private: + static int userCount; + +public: + // static methods + static int doubleUserCount() { return 2 * userCount; } + // static attributes + static int getUserCount() { return userCount; } + static void setUserCount(int v) { userCount = v; } + + // constructors with different parameters + User() { userCount++; } + User(const std::string &name_) : User() { name = name_; } + User(const std::string &name_, const std::string &token) + : User() { + name = name_; + _token = token; + } + User(const std::string &name_, const std::string &token, int credit) + : User(name, token) { + _credit = credit; + name = name_; + _token = token; + } + + + ~User() = default; + + // attributes + std::string getToken() const { return _token; } + void setToken(const std::string &t) { _token = t; } + + // override function + std::string toString() const { return name + ":" + _token; } + std::string toString(const std::string &tag) const { + return "[" + tag + "]:" + name + ":" + _token; + } + + // function args with bound type + std::string mergeName1(User &other) { return name + "|" + other.name; } + std::string mergeName2(User *other) { return name + "|" + other->name; } + std::string mergeName3(const std::shared_ptr &other) { + return name + "|" + other->name; + } + // public fields + std::string name{"unset"}; + +private: + std::string _token{"unset"}; + int _credit{-1}; +}; + +int User::userCount = 0; + +} // namespace + +``` +#### 实例化 `sebind::class_` + +关联 C++ 类和指定 JS 类名 + +```c++ + // 定义绑定类和 JS 类名 + sebind::class_ userClass("User"); +``` + +#### 绑定构造函数 + +```c++ +userClass.constructor<>() // JS: new User + .constructor() // JS: new User("Jone") + .constructor() // JS: new User("Jone", "343453") + .constructor() //JS: new User("Jone", "343453", 5678) +``` + +这里声明了 4 个构造模式:分别为 0、1、2、3 个参数. 每一个模板参数都对应于构造函数的参数类型。 + +在 JS 中调用 `new User(...)` 时, 会根据参数的数目触发对应的 C++ 构造函数。 + +> **注意**:如果不声明任何的 `constructor`, `sebind:class_` 会使用默认的无参构造函数。 + +我们也可以将普通函数定义为构造函数, 比如: + +```c++ +User *createUser(int credit) { + return = new User("Lambda", "ctor", credit); +} +// ... + .constructor(&createUser) // JS: new User(234) +``` + +其返回值需要是一个 `User*` 类型. 这里相当于在 JS 中声明了 构造函数 `constructor(credit:number)`。 + +#### 导出成员属性 + +把 C++ 公开的字段定义为 JS 中的属性,代码示例如下: + +```c++ +.property("name", &User::name) // JS: user.name +``` + +也可以将 `getter`/`setter` 函数定义为属性。这里的 `getter` 函数需要有返回值, 且无参。`setter` 函数接受一个参数。 + +```c++ +.property("token", &User::getToken, &User::setToken) // JS: user.token +``` + +> **注意**:这里的 `getter`/`setter` 可以只提供一个,另一个为 `nullptr`,但不能同时为 `nullptr`。 + +普通函数,第一个参数是 `User*`,可以作为成员函数使用。如: + +```c++ +std::string tokenLong_get(User *u) { + return "token[" + u->getToken() + "]"; +} +void tokenLong_set(User *u, const std::string &s) { + u->setToken("token[" + u->getToken() + "]"); +} +//... +.property("tokenPrefix", &tokenLong_get, &tokenLong_set) // JS: user.tokenPrefix +``` + +#### 导出成员函数 + +绑定成员函数,代码示例如下: + +```c++ +.function("mergeName1", &User::mergeName1) // JS: user1.mergeName1(user2) +.function("mergeName2", &User::mergeName2) // JS: user2.mergeName1(user2) +.function("mergeName3", &User::mergeName3) // JS: user3.mergeName1(user2) +``` + +JS 中绑定类型的实例可以作为参数传递给 C++ 绑定函数。C++ 函数可以使用 **引用**、**指针** 或者 **智能指针** 的方式接收绑定对象实例。这里如果 `User` 继承了 `cc::RefCounted`, 我们可以使用 `cc::IntrusivePtr` 持有。如果没有继承 `cc::RefCounted`,正如现在的情况,我们也可以通过 `std::shared_ptr` 持有。持有后,就是关联的 JS 对象被 GC 了,C++ 层持有的对象不会被析构。 + +> **注意**:绑定类型需要在调用 sebind 接口之前,通过宏 `JSB_REGISTER_OBJECT_TYPE(User);` 进行注册。后续的 `jsb_conversions` 方法才能正确处理类型转换。 + +如果对函数进行了重载,我们需要通过 `static_cast` 指定函数指针对应的具体类型。 + +```c++ +.function("toString", static_cast(&User::toString)) ///JS: (new User).toString() +.function("toString", static_cast(&User::toString)) //JS: (new User).toString("1111") +``` + +和构造函数类似,重载函数是根据参数的数目进行匹配的,应该避免相同参数的情形。如果需要运行时判断参数类型可以参考绑定 [ SE 函数](#手动类型转换)。 + +#### 导出类静态函数 + +通过下方的代码示例,可以导出类的静态函数: + +```c++ +.staticFunction("doubleUserCount", &User::doubleUserCount) //JS: User.doubleUserCount() +``` + +同样可以把普通函数导出为类静态函数: + +```c++ +int static_add(int a, int b) { return a + b; } +///... + .staticFunction("add", &static_add) //JS: User.add(1,2) +``` + +#### 类静态属性 + +将类静态函数导出为类的静态属性,代码示例如下: + +```c++ +.staticProperty("userCount", &User::getUserCount, &User::setUserCount) //JS: User.userCount +``` + +或普通函数: + +```c++ +int gettime() { return time(nullptr); } +/// ... +.staticProperty("time", &gettime, nullptr) //JS: User.time +``` + +#### 注册析构回调 + +注册绑定对象被 GC 时的回调示例如下: + +```c++ +... +.finalizer([](User *usr) { + std::cout << "release " << usr->name << std::endl; +}) +``` + +#### 导出到 JS 全局对象 + +将 `User` 类挂载到 `globalThis`对象, 完成导出. JS 脚本中可在全局访问. + +```c++ +.install(globalThis); +``` + +#### 继承 + +`sebind::class_` 的构造函数,第二个参数为父类的 `prototype` 对象。这里的 `SuperUser` 类继承了 `User` 类。 + +```c++ +sebind::class_ superUser("SuperUser", userClass.prototype()); +{ + superUser.constructor() + .function( + "superName", +[](User *user) { return user->name + ".super"; + }) //JS: (new SuperUser("Mok")).superName() + .install(globalThis); +} +``` + +> **注意**:需要注意的是,父类的静态方法不会被子类继承。 + +## 其他用法 + +### C++ 调用 JS 函数 + +从 3.6.1 起,通过 `sebind::bindFunction` 可以将 `se::Value` 对象,绑定为 C++ 中的 `std::function`,不需要处理参数的转换。 类似地,可以使用 `sebind::callFunction` 直接调用 JS 函数。 + +示例如下: +```c++ +demo.staticFunction( + "add", + +[](const se::Value &func, int a, int b) { + // bind js function as a std::function + auto addFunc = sebind::bindFunction(func); + // .. + // invoke std::function + auto result = addFunc(a, b); + + // call JS function with automatic arguments assembling + auto result2 = sebind::callFunction(func, a, b); + auto result3 = sebind::callFunction(func, 6, 8); + + // argument type computing + auto result4 = sebind::callFunction(func, a, b); + auto result5 = sebind::callFunction(func, 6, 8); + + std::cout << "result 1 " << result << std::endl; + std::cout << "result 2 " << result2 << std::endl; + }); +``` + +### 绑定抽象类 + +`sebind::class_` 要求提供构造函数,但抽象类的构造函数不可用。通过提供空的构造函数解决此冲突,实现抽象类型的注册。 + +示例如下: + +```c++ + +class AbstractClass { +public: + virtual bool tick() = 0; +}; + +class SubClass : public AbstractClass { +public: + bool tick() override { return true; } +}; + +AbstractClass *fakeConstructor() { + assert(false); // Abstract class cannot be instantiated + return nullptr; +} + +//.. +sebind::class_ base("AbstractBase"); + +base.constructor<>(&fakeConstructor) // add constructor + .function("tick", &AbstractClass::tick) + .install(globalThis); + +sebind::class_ sub("SubClass", base.prototype()); +sub.install(globalThis); +``` + +### 手动类型转换 + +`sebind` 支持绑定传统 SE 函数,实现手动执行转换,代码示例如下: + +```c++ +bool jsb_sum(se::State &state) { + double result = 0; + auto &args = state.args(); + for (int i = 0; i < args.size(); i++) { + result += (args[i].isNumber() ? args[i].toDouble() : 0); + } + state.rval().setDouble(result); + return true; +} +/// +.staticFunction("sum", &jsb_sum) // JS: User.sum(1,2,3,4,5) +``` + +这样就可以支持变长参数 和 灵活的参数转换。相比自动转换,这个方式更灵活同时也要求开发者对 `SE API` 更熟悉。 + +### 获取 JS this 对象 + +在 C++ 构造函数中获取对应的 JS `this` 对象是一个常见的需求,也能简化了从 C++ 到 JS 的访问流程。 + +我们只需要在 `constructor` 的参数类型中指定占位符 `sebind::ThisObject` 同时将对应构造函数的参数类型声明为 `se::Object *`。 + +```c++ +// constructor +User(se::Object *self, const std::string &name_) { + self->setProperty("fromNative", se::Value(true)); + name = name_; +} +/// ... + superUser.constructor() // JS: new SuperUser("Jone") +``` +JS 中调用对应构造函数的时候,需要忽略 `sebind::ThisObject` 参数。 + +![sebind::ThisObject](./sebind/thisobject_placeholder.png) + +HelloSEBind.cpp 完整代码如下: + +```c++ + +#include "bindings/sebind/sebind.h" +#include + +namespace { + + +struct Empty {}; // act as a namespace +float lerp(float a, float b, float t) { return (1 - t) * a + t * b; } + +class User { + static int userCount; + +public: + static int doubleUserCount() { return 2 * userCount; } + static int getUserCount() { return userCount; } + static void setUserCount(int v) { userCount = v; } + + User() { userCount++; } + // User(const std::string &name_) : User() { name = name_; } + User(const std::string &name_, const std::string &token) : User() { + name = name_; + _token = token; + } + User(const std::string &name_, const std::string &token, int credit) + : User(name, token) { + _credit = credit; + name = name_; + _token = token; + } + + User(se::Object *self, const std::string &name_) { + self->setProperty("fromNative", se::Value(true)); + name = name_; + } + + ~User() { userCount--; } + + std::string getToken() const { return _token; } + void setToken(const std::string &t) { _token = t; } + std::string toString() const { return name + ":" + _token; } + std::string toString(const std::string &tag) const { + return "[" + tag + "]:" + name + ":" + _token; + } + + std::string mergeName1(User &other) { return name + "|" + other.name; } + std::string mergeName2(const std::shared_ptr &other) { + return name + "|" + other->name; + } + std::string mergeName3(User *other) { return name + "|" + other->name; } + + std::string name{"unset"}; + +private: + std::string _token{"unset"}; + int _credit{-1}; +}; + +int User::userCount = 0; + + +class UserExt : public User { +public: + using User::User; +}; + +///////////////////////////////////////////////////// + + + +User *createUser(int credit) { return new User("Lambda", "ctor", credit); } + +std::string tokenLong_get(User *u) { return "token[" + u->getToken() + "]"; } +void tokenLong_set(User *u, const std::string &s) { + u->setToken("token[" + u->getToken() + "]"); +} + +int static_add(int a, int b) { return a + b; } + +bool jsb_sum(se::State &state) { + double result = 0; + auto &args = state.args(); + for (int i = 0; i < args.size(); i++) { + result += (args[i].isNumber() ? args[i].toDouble() : 0); + } + state.rval().setDouble(result); + return true; +} + +int gettime() { return time(nullptr); } + + +} // namespace + +JSB_REGISTER_OBJECT_TYPE(User); + +bool jsb_register_simple_math(se::Object *globalThis) { + + sebind::class_ demoMathClass("simpleMath"); + { + // invoke through simpleMath.lerp(a, b, t); + demoMathClass.staticFunction("lerp", &lerp).install(globalThis); + } + + sebind::class_ userClass("User"); + { + + userClass + .constructor<>() + .constructor() + .constructor() + .constructor() + // .constructor(&createUser) + .property("name", &User::name) + .property("token", &User::getToken, &User::setToken) + .property("tokenPrefix", &tokenLong_get, nullptr) + .function("mergeName1", &User::mergeName1) + .function("mergeName2", &User::mergeName2) + // .function("mergeName3", &User::mergeName3) + .function("toString", + static_cast(&User::toString)) + .function("toString", + static_cast( + &User::toString)) + .staticFunction("doubleUserCount", &User::doubleUserCount) + .staticProperty("userCount", &User::getUserCount, &User::setUserCount) + .staticProperty("time", &gettime, nullptr) + .staticFunction("sum", &jsb_sum) + .finalizer([](User *usr) { + std::cout << "release " << usr->name << std::endl; + }) + .install(globalThis); + } + + sebind::class_ superUser("SuperUser", userClass.prototype()); + { + superUser.constructor() + .function( + "superName", +[](UserExt *user) { return user->name + ".super"; }) + .install(globalThis); + } + + return true; +} +``` + + + + diff --git a/versions/4.0/zh/advanced-topics/jsb-swig.md b/versions/4.0/zh/advanced-topics/jsb-swig.md new file mode 100644 index 0000000000..870c1c1f4a --- /dev/null +++ b/versions/4.0/zh/advanced-topics/jsb-swig.md @@ -0,0 +1,69 @@ +## 简介 + +从 Cocos Creator 3.7.0 开始,我们将生成 JS 绑定代码的方式从 [bindings-generator](https://github.com/cocos/cocos4/tree/d08a11244d2a31da1aac7af7d2aa8f1b6152e30c/native/tools/bindings-generator) 改为 [Swig](https://www.swig.org)。 Swig 通过解析与 C++ 兼容的接口定义语言 (IDL) 的方式来生成胶水代码,此方式有较多好处 。 关于我们为什么改用 Swig,可以参考[此 issue](https://github.com/cocos/cocos-engine/issues/10792)。 + +## 为引擎模块生成绑定代码 + +在 3.8 及以上的版本中,开发者不再需要手动触发绑定代码的生成,因为 CMake 会自动在编译过程中调用 SWIG 命令来生成绑定代码。如果生成失败, 请注意终端的输出日志。 + +需要注意: 如果为引擎添加或删除了 `.i` 文件,你需要重新生成工程来确保更新对 `.i` 文件的引用,并生成更新的绑定代码。 + +## 为开发者的项目生成绑定代码 + +- 确保你已经安装了 NodeJS,版本号大于或等于 v8.9.4 + +- 打开终端 ( macOS / Linux) 或者命令提示符 ( Windows ), + +- 创建一个用于存放自动绑定胶水代码的目录,例如: `/Users/abc/my-project/native/engine/common/Classes/bindings/auto` + +- 写一个 JS 配置文件 + + - 创建一个 JS 配置文件, 路径例如: `/Users/abc/my-project/tools/swig-config/swig-config.js` ,内容为: + + ```js + 'use strict'; + const path = require('path'); + + // 开发者自己的模块定义配置 + // configList 是必须的 + const configList = [ + [ 'your_module_interface_0.i', 'jsb_your_module_interface_0_auto.cpp' ], + [ 'your_module_interface_1.i', 'jsb_your_module_interface_1_auto.cpp' ], + // ...... + ]; + + const projectRoot = path.resolve(path.join(__dirname, '..', '..')); + // interfaceDir 是可选的 + const interfacesDir = path.join(projectRoot, 'tools', 'swig-config'); + // bindingsOutDir 是可选的 + const bindingsOutDir = path.join(projectRoot, 'native', 'engine', 'common', 'Classes', 'bindings', 'auto'); + + module.exports = { + interfacesDir, // 可选参数, 如果没有指定,configList 中的路径必须为绝对路径或者相对于当前 swig-config.js 的相对路径 + bindingsOutDir, // 可选参数,如果没有指定, configList 中的路径必须为绝对路径或者相对于当前 swig-config.js 的相对路径 + configList // 必填参数 + }; + ``` + + - 执行如下命令 + + ```bash + # 如果当前终端或者命令提示符所在的目录不是在 '/Users/abc/my-project/tools/swig-config' + $ node < 引擎的根目录 >/native/tools/swig-config/genbindings.js -c /Users/abc/my-project/tools/swig-config/swig-config.js + ``` + + ```bash + # 如果你已经在 '/Users/abc/my-project/tools/swig-config' 目录, 你执行命令的时候可以不需要带上 -c 参数,例如: + $ cd /Users/abc/my-project/tools/swig-config + $ node < 引擎的根目录 >/native/tools/swig-config/genbindings.js + ``` + +## Swig 接口定义文件 + +- 在引擎的 `engine/native/tools/swig-config` 目录下有一个 [swig-interface-template.i](https://github.com/cocos/cocos4/blob/1f928364f4cad22681e7830c53dc7da71a87d11f/native/tools/swig-config/swig-interface-template.i) 模版文件。你可以拷贝其到自己的工程目录下并重命名。此模版文件中包含一些注释用于展示如何在 .i 文件中配置你的模块。你也可以参考在 `engine/native/tools/swig-config` 目录下引擎内部的 .i 文件,例如:参考 `scene.i` 或者 `assets.i` 来快速上手。 +- 如果你使用 `Visual Studio Code`, 你可以安装 `Hong-She Liang` 开发的 `SWIG Language` 扩展,其可用于 .i 文件的语法高亮。 +- 关于编写 .i 文件的更多详细信息,建议参考下面 [教程](#Tutorial) 章节。 + +## 教程 + +请访问 [在 Cocos Creator 中的 Swig 工作流教程](jsb/swig/tutorial/index.md), 其包含如何一步一步地为引擎内的新模块或用户工程模块配置绑定。 diff --git a/versions/4.0/zh/advanced-topics/jsb-web-difference.md b/versions/4.0/zh/advanced-topics/jsb-web-difference.md new file mode 100644 index 0000000000..b3a7de7b12 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/jsb-web-difference.md @@ -0,0 +1,88 @@ +# 纯 JS 对象与绑定对象之间的差异 + +Cocos Creator 引擎使用 `V8` 将 CPP 类导出到 Javascript 环境中,我们称之为 JS-Binding。 而纯 JS 和 JS-Binding 对象之间存在一些差异。 如果你的应用程序需要在所有平台上运行,尤其是在原生平台上,最好不要使用一些在 `Web` 和 `Native` 平台上表现不同的 API。 如果你必须使用它们,你需要编写一些平台指定的代码,如 `if (NATIVE) {...} else {...}` 。 + +## 属性 + +纯 JS 对象的属性是被赋值到对象实例上,但是绑定对象的属性是被赋值到对象的原型上的。 + +### 纯 JS 对象 + +```typescript +class MyClass { + constructor() { + this.a = 'a'; + this.b = false; + this.c = 100; + } +}; + +const myobj = new MyClass(); +const ownA = myobj.hasOwnProperty('a'); +console.log(`ownA: ${ownA}`); // ownA: true +``` + +### JS 绑定对象 + +```c++ +// C++ 类 +class MyClass { +public: + std::string a; + bool b; + int32_t c; +}; + +// MyClass 绑定代码片段 +bool js_register_cc_MyClass(se::Object* obj) { + auto* cls = se::Class::create("MyClass", obj, nullptr, _SE(js_new_cc_MyClass)); + cls->defineStaticProperty("__isJSB", se::Value(true), se::PropertyAttribute::READ_ONLY | se::PropertyAttribute::DONT_ENUM | se::PropertyAttribute::DONT_DELETE); + + cls->defineProperty("a", _SE(js_cc_MyClass_a_get), _SE(js_cc_MyClass_a_set)); + cls->defineProperty("b", _SE(js_cc_MyClass_b_get), _SE(js_cc_MyClass_b_set)); + cls->defineProperty("c", _SE(js_cc_MyClass_c_get), _SE(js_cc_MyClass_c_set)); + + cls->defineFinalizeFunction(_SE(js_delete_cc_MyClass)); + cls->install(); + JSBClassType::registerClass(cls); + + __jsb_cc_MyClass_proto = cls->getProto(); + __jsb_cc_MyClass_class = cls; + se::ScriptEngine::getInstance()->clearException(); + return true; +} +// +``` + +```typescript +const myobj = new jsb.MyClass(); +const ownA = myobj.hasOwnProperty('a'); +console.log(`ownA: ${ownA}`); // ownA: false +const protoOwnA = myobj.__proto__.hasOwnProperty('a'); +console.log(`protoOwnA: ${protoOwnA}`); // protoOwnA: true +``` + +因此,如果你的代码逻辑中依赖了 `hasOwnProperty` ,你需要谨记上述差异,因为其可能会导致你的应用在 Web 环境下运行正常,但是在 Native 环境下出现异常的情况。 + +### 可能的解决方案 + +如果你的应用需要运行在 Native 环境下,当判断一个绑定对象上的属性是否存在的时候,需要额外再判断一下原型上是否存在此属性,例如: + +```typescript +function doSomething(v) { + // ...... +} + +function foo() { + for (const key in myObj) { + if (myObj.hasOwnProperty('a')) { + doSomething(myObj['a']); + } else if (NATIVE) { + if (myObj.__proto__.hasOwnProperty('a')) { + doSomething(myObj['a']); + } + } + } +} +``` + diff --git a/versions/4.0/zh/advanced-topics/jsb/100.png b/versions/4.0/zh/advanced-topics/jsb/100.png new file mode 100644 index 0000000000..b1a59d754e Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/100.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/110.png b/versions/4.0/zh/advanced-topics/jsb/110.png new file mode 100644 index 0000000000..b83f27e9a2 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/110.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/111.png b/versions/4.0/zh/advanced-topics/jsb/111.png new file mode 100644 index 0000000000..0e44591dbc Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/111.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/112.png b/versions/4.0/zh/advanced-topics/jsb/112.png new file mode 100644 index 0000000000..eaa2ba0e32 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/112.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/JSB2.0-Architecture.png b/versions/4.0/zh/advanced-topics/jsb/JSB2.0-Architecture.png new file mode 100644 index 0000000000..c7b115ed21 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/JSB2.0-Architecture.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/auto-file.png b/versions/4.0/zh/advanced-topics/jsb/auto-file.png new file mode 100644 index 0000000000..c9dbacb6fc Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/auto-file.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/binding-file.png b/versions/4.0/zh/advanced-topics/jsb/binding-file.png new file mode 100644 index 0000000000..c3736eb6fd Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/binding-file.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/called-injs.png b/versions/4.0/zh/advanced-topics/jsb/called-injs.png new file mode 100644 index 0000000000..ee07044673 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/called-injs.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/cancel-output_dir.png b/versions/4.0/zh/advanced-topics/jsb/cancel-output_dir.png new file mode 100644 index 0000000000..1d06a17983 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/cancel-output_dir.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/error-1.png b/versions/4.0/zh/advanced-topics/jsb/error-1.png new file mode 100644 index 0000000000..d25b0fd6f5 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/error-1.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/error-2.png b/versions/4.0/zh/advanced-topics/jsb/error-2.png new file mode 100644 index 0000000000..6778af5cb1 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/error-2.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/generate-binding-file.png b/versions/4.0/zh/advanced-topics/jsb/generate-binding-file.png new file mode 100644 index 0000000000..1a624881ea Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/generate-binding-file.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/generate-file-complete.png b/versions/4.0/zh/advanced-topics/jsb/generate-file-complete.png new file mode 100644 index 0000000000..b98713583a Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/generate-file-complete.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/generate-file.png b/versions/4.0/zh/advanced-topics/jsb/generate-file.png new file mode 100644 index 0000000000..0cf0c68877 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/generate-file.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/infrastructure.png b/versions/4.0/zh/advanced-topics/jsb/infrastructure.png new file mode 100644 index 0000000000..7b7f6a2c65 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/infrastructure.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/ini-file-properties.png b/versions/4.0/zh/advanced-topics/jsb/ini-file-properties.png new file mode 100644 index 0000000000..ec60c4bde8 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/ini-file-properties.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/jsb_process.jpg b/versions/4.0/zh/advanced-topics/jsb/jsb_process.jpg new file mode 100644 index 0000000000..b4e9c0f273 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/jsb_process.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/opt-1.jpg b/versions/4.0/zh/advanced-topics/jsb/opt-1.jpg new file mode 100644 index 0000000000..3529892643 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/opt-1.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/opt-2.jpg b/versions/4.0/zh/advanced-topics/jsb/opt-2.jpg new file mode 100644 index 0000000000..1e3599ac7c Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/opt-2.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/opt-3.jpg b/versions/4.0/zh/advanced-topics/jsb/opt-3.jpg new file mode 100644 index 0000000000..4dbaf1927f Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/opt-3.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/opt-4.jpg b/versions/4.0/zh/advanced-topics/jsb/opt-4.jpg new file mode 100644 index 0000000000..355f57f538 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/opt-4.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/opt-5.jpg b/versions/4.0/zh/advanced-topics/jsb/opt-5.jpg new file mode 100644 index 0000000000..b5a9c45165 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/opt-5.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/store-file.png b/versions/4.0/zh/advanced-topics/jsb/store-file.png new file mode 100644 index 0000000000..b32fecfed0 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/store-file.png differ diff --git a/versions/4.0/zh/advanced-topics/jsb/swig/tutorial/MyRefCompileError.jpg b/versions/4.0/zh/advanced-topics/jsb/swig/tutorial/MyRefCompileError.jpg new file mode 100644 index 0000000000..dee74edb03 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/swig/tutorial/MyRefCompileError.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/swig/tutorial/another-module-compile-error.jpg b/versions/4.0/zh/advanced-topics/jsb/swig/tutorial/another-module-compile-error.jpg new file mode 100644 index 0000000000..bc3fb929a8 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/swig/tutorial/another-module-compile-error.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/swig/tutorial/index.md b/versions/4.0/zh/advanced-topics/jsb/swig/tutorial/index.md new file mode 100644 index 0000000000..6f65f71a5e --- /dev/null +++ b/versions/4.0/zh/advanced-topics/jsb/swig/tutorial/index.md @@ -0,0 +1,1144 @@ +# 在 Cocos Creator 中的 Swig 工作流教程 + +## 如何为引擎内的新模块添加绑定 + +### 添加一个新模块的接口文件 + +- 添加一个新模块的接口文件到 `native/tools/swig-config` 目录, 例如: `new-engine-module.i` + +- 拷贝 [swig-interface-template.i](https://github.com/cocos/cocos4/blob/1f928364f4cad22681e7830c53dc7da71a87d11f/native/tools/swig-config/swig-interface-template.i) 文件中的内容到 new-engine-module.i + +- 添加必要的配置,可以参考 `native/tools/swig-config` 目录下现有的 .i 文件配置,或者参考[下面的章节内容](#如何为开发者的项目绑定一个新模块)。 + +### 修改 `engine/native/cocos/CMakeLists.txt` + +```cmake +######## auto +cocos_source_files( + NO_WERROR NO_UBUILD ${SWIG_OUTPUT}/jsb_cocos_auto.cpp # 添加此行 + ${SWIG_OUTPUT}/jsb_cocos-auto.h # 添加此行 + NO_WERROR NO_UBUILD ${SWIG_OUTPUT}/jsb_cocos_auto.cpp + ${SWIG_OUTPUT}/jsb_cocos_auto.h + ...... +``` + +### 为脚本引擎注册新的模块 + +打开 `jsb_module_register.cpp` ,做如下修改 + +```c++ +...... +#if CC_USE_PHYSICS_PHYSX + #include "cocos/bindings/auto/jsb_physics_auto.h" +#endif +#include "cocos/bindings/auto/jsb_new_engine_module_auto.h" // 添加此行 + +bool jsb_register_all_modules() { + se::ScriptEngine *se = se::ScriptEngine::getInstance(); + ...... + se->addRegisterCallback(register_all_my_new_engine_module); // 添加此行 + + se->addAfterCleanupHook([]() { + cc::DeferredReleasePool::clear(); + JSBClassType::cleanup(); + }); + return true; +} +``` + +## 如何为开发者的项目绑定一个新模块 + +假定我们已经有一个 Cocos Creator 的工程,其位于 `/Users/james/NewProject` 目录下。 + +打开 Cocos Creator 的构建面板,构建出一个原生平台的工程,会生成 `/Users/james/NewProject/native` 目录。 + +### 绑定一个简单的类 + +#### 创建一个简单类 + +创建一个头文件,其位于 `/Users/james/NewProject/native/engine/common/Classes/MyObject.h` , 其内容为: + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +namespace my_ns { +class MyObject { +public: + MyObject() = default; + MyObject(int a, bool b) {} + virtual ~MyObject() = default; + void print() { + CC_LOG_DEBUG("==> a: %d, b: %d\n", _a, (int)_b); + } + + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; +}; +} // namespace my_ns { +``` + +#### 编写一个 Swig 接口文件 + +创建一个名称为 `my-module.i` 的接口文件,其位于 `/Users/james/NewProject/tools/swig-config`目录下。 + +```c++ +// my-module.i +%module(target_namespace="my_ns") my_module + +// %insert(header_file) %{ ... }%} 代码块中的内容最终会被原封不动地插入到生成的头文件(.h)开头的地方 +%insert(header_file) %{ +#pragma once +#include "bindings/jswrapper/SeApi.h" +#include "bindings/manual/jsb_conversions.h" + +#include "MyObject.h" // 添加这行,%include 指令表示让 swig 解析此文件,并且为此文件中的类生成绑定代码。 +%} + +// %{ ... %} 代码块中的内容最终会被原封不动地插入到生成的源文件(.cpp)开头的地方 +%{ +#include "bindings/auto/jsb_my_module_auto.h" +%} + +%include "MyObject.h" +``` + +#### 编写一个 Swig 配置文件(swig-config.js) + +创建一个名为 swig-config.js 的文件,例如: `/Users/james/NewProject/tools/swig-config`目录下。 + +```js +// swig-config.js +'use strict'; +const path = require('path'); +const configList = [ + [ 'my-module.i', 'jsb_my_module_auto.cpp' ], +]; + +const projectRoot = path.resolve(path.join(__dirname, '..', '..')); +const interfacesDir = path.join(projectRoot, 'tools', 'swig-config'); +const bindingsOutDir = path.join(projectRoot, 'native', 'engine', 'common', 'bindings', 'auto'); +// includeDirs 意思是 swig 执行时候使用的头文件搜索路径 +const includeDirs = [ + path.join(projectRoot, 'native', 'engine', 'common', 'Classes'), +]; + +module.exports = { + interfacesDir, + bindingsOutDir, + includeDirs, + configList +}; +``` + +#### 为项目生成自动绑定文件 + +```bash +cd /Users/james/NewProject/tools/swig-config +node < 引擎根目录 >/native/tools/swig-config/genbindings.js -c swig-config.js +``` + +如果成功,包含自动绑定代码的 `jsb_my_module_auto.cpp/.h` 两个文件将被创建到 `/Users/james/NewProject/native/engine/bindings/auto` 目录下。 + +#### 修改项目的 CMakeLists.txt 文件 + +- 打开 `/Users/james/NewProject/native/engine/common/CMakeLists.txt`, 添加 `MyObject.h` 和自动绑定代码文件 + + ```cmake + include(${COCOS_X_PATH}/CMakeLists.txt) + + list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp + ############### 添加下面几行 ############## + ${CMAKE_CURRENT_LIST_DIR}/Classes/MyObject.h + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.h + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.cpp + ######################################################## + ) + ``` + +- 修改 `/Users/james/NewProject/native/engine/mac/CMakeLists.txt` + + ```cmake + cmake_minimum_required(VERSION 3.8) + # ...... + cc_mac_before_target(${EXECUTABLE_NAME}) + add_executable(${EXECUTABLE_NAME} ${CC_ALL_SOURCES}) + ############### 添加下面几行 ############## + target_include_directories(${EXECUTABLE_NAME} PRIVATE + ${CC_PROJECT_DIR}/../common + ) + ######################################################## + cc_mac_after_target(${EXECUTABLE_NAME}) + ``` + +#### 打开项目工程 + +macOS: `/Users/james/NewProject/build/mac/proj/NewProject.xcodeproj` + +Windows: `< 一个存放项目的目录 >/NewProject/build/win64/proj/NewProject.sln` + +#### 为脚本引擎注册新的模块 + +修改工程目录下的 `Game.cpp` + +```c++ +#include "Game.h" +#include "../bindings/auto/jsb_my_module_auto.h" // 添加此行 +//...... +int Game::init() { + // ...... + se::ScriptEngine::getInstance()->addRegisterCallback(register_all_my_module); // 添加此行 + BaseGame::init(); + return 0; +} +// ...... +``` + +#### 测试绑定 + +- 在项目的根目录下添加一个 `my-module.d.ts` 文件,使 TS 编译器识别我们的绑定类型 + + ```ts + // my-module.d.ts + declare namespace my_ns { + class MyObject { + constructor(); + constructor(a: number, b: number); + + publicFloatProperty : number; + print() : void; + } + } + ``` + +- 修改 `/Users/james/NewProject/temp/tsconfig.cocos.json` 文件 + + ```js + { + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2015", + "module": "ES2015", + "strict": true, + "types": [ + "./temp/declarations/cc.custom-macro", + "./temp/declarations/jsb", + "./temp/declarations/cc", + "./temp/declarations/cc.env", + "./my-module" // 添加这行 + ], + // ...... + "forceConsistentCasingInFileNames": true + } + } + ``` + +- 在 Cocos Creator 中打开 NewProject 项目, 在场景中添加一个立方体,并且添加一个脚本组件到这个立方体上,脚本的内容是: + + ```ts + import { _decorator, Component } from 'cc'; + const { ccclass } = _decorator; + + @ccclass('MyComponent') + export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyObject(); + myObj.print(); // 调用原生的 print 方法 + console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); // 获取原生中定义的属性值 + } + } + ``` + +- 在 Xcode 或者 Visual Studio 中运行项目, 如果成功,可以看到如下日志输出 + + ``` + 17:31:44 [DEBUG]: ==> a: 100, b: 1 + 17:31:44 [DEBUG]: D/ JS: ==> myObj.publicFloatProperty: 1.2300000190734863 + ``` + +#### 本节总结 + +在此节中,我们学会了如何使用 Swig 工具绑定一个简单的 C++ 类,并把它的公有方法与属性导出到 JS 中。从下一节开始,我们将更多地关注如何使用 Swig 的一些特性来满足各式各样的 JS 绑定需求,例如: + +- 如何使用 %import 指令导入头文件依赖? +- 如何忽略绑定某些特殊的类、方法或属性? +- 如何重命名类、方法或属性? +- 如何将 C++ 的 getter 和 setter 函数绑定为 JS 属性? +- 如何配置 C++ 模块宏 + +### 导入头文件依赖 + +假定我们让 MyObject 类继承于 MyRef 类。但是我们并不想绑定 MyRef 类型。 + +```c++ +// MyRef.h +#pragma once +namespace my_ns { +class MyRef { +public: + MyRef() = default; + virtual ~MyRef() = default; + void addRef() { _ref++; } + void release() { --_ref; } +private: + unsigned int _ref{0}; +}; +} // namespace my_ns { +``` + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" +namespace my_ns { +// MyObject 继承于 MyRef +class MyObject : public MyRef { +public: + MyObject() = default; + MyObject(int a, bool b) {} + virtual ~MyObject() = default; + void print() { + CC_LOG_DEBUG("==> a: %d, b: %d\n", _a, (int)_b); + } + + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; +}; +} // namespace my_ns { +``` + +当 Swig 解析 MyObject.h 的时候, 它并不知道 `MyRef` 是什么, 因此它会在终端输出一个警告信息。 + +```bash +.../Classes/MyObject.h:7: Warning 401: Nothing known about base class 'MyRef'. Ignored. +``` + +要解决此警告也容易,我们只需要使用 `%import` 指令让 Swig 知道 MyRef 类的存在即可。 + +```c++ +// ...... +// Insert code at the beginning of generated source file (.cpp) +%{ +#include "bindings/auto/jsb_my_module_auto.h" +%} + +%import "MyRef.h" // 添加此行 +%include "MyObject.h" +``` + +尽管 Swig 不再报错了,但是生成的代码却无法编译通过,会出现如下报错: + +![MyRefCompileError](MyRefCompileError.jpg) + +我们将在下一节中使用 `%ignore` 指令来修复此问题。 + +### 忽略某些类、方法或属性 + +#### 忽略某些类 + +在上一节中,我们在 `js_register_my_ns_MyObject` 函数中碰到了一个编译错误。这是因为 `MyRef` 类型并不应该被绑定,我们可以用 `%ignore` 指令来忽略它。 + +```c++ +// my-module.i +// ...... +%ignore my_ns::MyRef; // 添加此行 +%import "MyRef.h" +%include "MyObject.h" +``` + +重新生成绑定,现在应该可以编译通过了。 + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject)); // parentProto will be set to nullptr + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set)); + cls->defineFunction("print", _SE(js_my_ns_MyObject_print)); + // ...... +} +``` + +#### 忽略某些方法和属性 + +我们为 `MyObject` 类添加一个名为 `methodToBeIgnored` 的方法,再添加一个名为 `propertyToBeIgnored` 的属性。 + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" +namespace my_ns { +// MyObject 继承于 MyRef +class MyObject : public MyRef { +public: +// ..... + void methodToBeIgnored() {} // 添加此行 + float propertyToBeIgnored{345.123F}; // 添加此行 +// ...... + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; +}; +} // namespace my_ns { + +``` + +重新生成绑定, 我们可以发现 `methodToBeIgnored` 和 `propertyToBeIgnored` 的绑定代码已经被自动生成。 + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject)); + cls->defineProperty("propertyToBeIgnored", _SE(js_my_ns_MyObject_propertyToBeIgnored_get), _SE(js_my_ns_MyObject_propertyToBeIgnored_set)); // this property should not be bound + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set)); + cls->defineFunction("print", _SE(js_my_ns_MyObject_print)); + cls->defineFunction("methodToBeIgnored", _SE(js_my_ns_MyObject_methodToBeIgnored)); // this method should not be bound + // ...... +} +``` + +修改 `my-module.i`以忽略绑定这个方法与属性。 + +```c++ +// my-module.i +// ...... + +%ignore my_ns::MyRef; +%ignore my_ns::MyObject::methodToBeIgnored; // 添加此行 +%ignore my_ns::MyObject::propertyToBeIgnored; // 添加此行 + +%import "MyRef.h" +%include "MyObject.h" +``` + +重新生成绑定,它们将被忽略。 + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject)); + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set)); + cls->defineFunction("print", _SE(js_my_ns_MyObject_print)); +// ...... +} +``` + +### 重命名类、方法或属性 + +Swig 定义了一个名为 `%rename` 指令用于重命名类、方法或者属性。我们继续使用 `MyObject` 类来展示。 + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" +namespace my_ns { +// MyObject 继承于 MyRef +class MyObject : public MyRef { +public: +// ...... + void methodToBeRenamed() { // 添加此方法 + CC_LOG_DEBUG("==> hello MyObject::methodToBeRenamed"); + } + int propertyToBeRenamed{1234}; // 添加此属性 + + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; +}; +} // namespace my_ns { +``` + +重新生成绑定,我们发现 `methodToBeRenamed` 与 `propertyToBeRenamed` 的绑定代码已经被生成: + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyObject", obj, nullptr, _SE(js_new_MyObject)); + cls->defineProperty("propertyToBeRenamed", _SE(js_my_ns_MyObject_propertyToBeRenamed_get), _SE(js_my_ns_MyObject_propertyToBeRenamed_set)); + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyObject_publicFloatProperty_get), _SE(js_my_ns_MyObject_publicFloatProperty_set)); + + cls->defineFunction("print", _SE(js_my_ns_MyObject_print)); + cls->defineFunction("methodToBeRenamed", _SE(js_my_ns_MyObject_methodToBeRenamed)); +``` + +如果我们要重命名 `propertyToBeRenamed` 为 `coolProperty` ,重命名 `methodToBeRenamed` 为 `coolMethod`,那么按照下面的方式修改 `my-module.i` : + +```c++ +// my-module.i +// ...... +%ignore my_ns::MyRef; +%ignore my_ns::MyObject::methodToBeIgnored; +%ignore my_ns::MyObject::propertyToBeIgnored; +%rename(coolProperty) my_ns::MyObject::propertyToBeRenamed; // 添加此行 +%rename(coolMethod) my_ns::MyObject::methodToBeRenamed; // 添加此行 + +%import "MyRef.h" +%include "MyObject.h" +``` + +如果我们还想把 `MyObject` 类重命名为 `MyCoolObject`,我猜想你已经知道如何做了吧。没错,只要添加这行: + +```c++ +%rename(MyCoolObject) my_ns::MyObject; +``` + +重新生成绑定代码,所有需要被重命名的类、方法与属性都按照我们的意愿被重命名了。 + +```c++ +// jsb_my_module_auto.cpp +// MyCoolObject, coolProperty, coolMethod are all what we want now. +bool js_register_my_ns_MyObject(se::Object* obj) { + auto* cls = se::Class::create("MyCoolObject", obj, nullptr, _SE(js_new_MyCoolObject)); + cls->defineProperty("coolProperty", _SE(js_my_ns_MyCoolObject_coolProperty_get), _SE(js_my_ns_MyCoolObject_coolProperty_set)); + cls->defineProperty("publicFloatProperty", _SE(js_my_ns_MyCoolObject_publicFloatProperty_get), _SE(js_my_ns_MyCoolObject_publicFloatProperty_set)); + cls->defineFunction("print", _SE(js_my_ns_MyCoolObject_print)); + cls->defineFunction("coolMethod", _SE(js_my_ns_MyCoolObject_coolMethod)); + // ...... +} +``` + +现在测试一下吧,更新如下文件 `my-module.d.ts` and `MyComponent.ts` + +```c++ +// my-module.d.ts +declare namespace my_ns { +class MyCoolObject { + constructor(); + constructor(a: number, b: number); + + publicFloatProperty : number; + print() : void; + coolProperty: number; + coolMethod() : void; +} +} +``` + +```ts +// MyComponent.ts +import { _decorator, Component } from 'cc'; +const { ccclass } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyCoolObject(); // 这里改为 MyCoolObject,因为我们在前面重命名了 + myObj.print(); + console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); + // Add the follow lines + console.log(`==> old: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolProperty = 666; + console.log(`==> new: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolMethod(); + } +} +``` + +在 Xcode 或者 Visual Studio 中运行项目,将得到如下日志: + +``` +17:53:28 [DEBUG]: ==> a: 100, b: 1 +17:53:28 [DEBUG]: D/ JS: ==> myObj.publicFloatProperty: 1.2300000190734863 +17:53:28 [DEBUG]: D/ JS: ==> old: myObj.coolProperty: 1234 +17:53:28 [DEBUG]: D/ JS: ==> new: myObj.coolProperty: 666 +17:53:28 [DEBUG]: ==> hello MyObject::methodToBeRenamed +``` + +### 定义一个 attribute + +`%attribute` 指令用于把 C++ 的 `getter` 和 `setter` 函数绑定为一个 JS 属性。 + +> **注意**:如果 C++ 属性是公有的,那么理论上无需再配置 attribute 了,Swig 会自动绑定类的公有属性。 + +#### 用法 + +1. 定义一个没有 `setter` 函数的 JS 属性,即只读的 JS 属性。 + + ```c++ + %attribute(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_getter_function_name) + ``` + +2. 定义一个有 `getter` 和 `setter` 函数的 JS 属性,即可读可写的 JS 属性。 + + ```c++ + %attribute(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_getter_function_name, cpp_setter_function_name) + ``` + +3. 定义一个没有 `getter` 的 JS 属性,即可写不可读的 JS 属性。 + + ```c++ + %attribute_writeonly(your_namespace::your_class_name, cpp_member_variable_type, js_property_name, cpp_setter_function_name) + ``` + +#### 示例 + +为了方便演示,我们为 `MyObject` 添加两个新方法:`setType` 和 `getType`。 + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" +namespace my_ns { +// MyObject 继承于 MyRef +class MyObject : public MyRef { +public: +// ...... + void setType(int v) { _type = v; CC_LOG_DEBUG("==> setType: v: %d", v); } // 添加此行 + int getType() const { return _type; } // 添加此行 + + float publicFloatProperty{1.23F}; +private: + int _a{100}; + bool _b{true}; + int _type{333}; +}; +} // namespace my_ns { +``` + +```c++ +// my-module.i +// ...... +%attribute(my_ns::MyObject, int, type, getType, setType); // 添加此行 + +%import "MyRef.h" +%include "MyObject.h" +``` + +```c++ +// jsb_my_module_auto.cpp +bool js_register_my_ns_MyObject(se::Object* obj) { +// ...... + cls->defineProperty("type", _SE(js_my_ns_MyCoolObject_type_get), _SE(js_my_ns_MyCoolObject_type_set)); +// ...... +} +``` + +```ts +// MyComponent.ts +import { _decorator, Component } from 'cc'; +const { ccclass } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyCoolObject(); + myObj.print(); + console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); + console.log(`==> old: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolProperty = 666; + console.log(`==> new: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolMethod(); + console.log(`==> old: myObj.type: ${myObj.type}`); + myObj.type = 888; + console.log(`==> new: myObj.type: ${myObj.type}`); + } +} +``` + +在 Xcode 或者 Visual Studio 中运行项目: + +``` +18:09:53 [DEBUG]: ==> a: 100, b: 1 +18:09:53 [DEBUG]: D/ JS: ==> myObj.publicFloatProperty: 1.2300000190734863 +18:09:53 [DEBUG]: D/ JS: ==> old: myObj.coolProperty: 1234 +18:09:53 [DEBUG]: D/ JS: ==> new: myObj.coolProperty: 666 +18:09:53 [DEBUG]: ==> hello MyObject::methodToBeRenamed +18:09:53 [DEBUG]: D/ JS: ==> old: myObj.type: 333 +18:09:53 [DEBUG]: ==> setType: v: 888 // Cool, C++ setType is invoked +18:09:53 [DEBUG]: D/ JS: ==> new: myObj.type: 888 // Cool, C++ getType is invoked, 888 is return from C++ +``` + +#### %attribute_writeonly 指令 + +`%attribute_writeonly` 指令是我们为 swig `Cocos` 后端添加的一个扩展指令,它用于 C++ 只有 `setter` 函数没有 `getter` 函数的情况。 + +例如在 `native/tools/swig-config/cocos.i` 中有如下定义: + +```c++ +%attribute_writeonly(cc::ICanvasRenderingContext2D, float, width, setWidth); +%attribute_writeonly(cc::ICanvasRenderingContext2D, float, height, setHeight); +%attribute_writeonly(cc::ICanvasRenderingContext2D, float, lineWidth, setLineWidth); +%attribute_writeonly(cc::ICanvasRenderingContext2D, ccstd::string&, fillStyle, setFillStyle); +%attribute_writeonly(cc::ICanvasRenderingContext2D, ccstd::string&, font, setFont); +``` + +它的作用类似于 JS 中如下代码: + +```javascript +Object.defineProperty(MyNewClass.prototype, 'width', { + configurable: true, + enumerable: true, + set(v) { + this._width = v; + }, + // No get() for property +}); +``` + +#### 关于引用类型 + +如果 C++ 的 `get` 函数返回的是一个引用数据类型或者 `set` 函数接受一个引用数据类型,别忘记在 %attribute 或 %attribute_writeonly 指令的编写中添加 `&` 后缀。以下 `ccstd::string&` 是一个例子: + +```c++ +%attribute_writeonly(cc::ICanvasRenderingContext2D, ccstd::string&, fillStyle, setFillStyle); +``` + +如果 `&` 没有被添加,当绑定函数被调用的时候,一个临时的 `ccstd::string` 实例将被创建。这种临时对象的创建与销毁是不划算的且是可以完全避免的。 + +#### %arg() 指令 + +有时候 C++ 变量的类型是用模版的方式来修饰的,例如: + +```c++ +class MyNewClass { + public: + const std::map& getConfig() const { return _config; } + void setConfig(const std::map &config) { _config = config; } + private: + std::map _config; +}; +``` + +我们可能会在 `.i` 中写一个这样的 `%attribute` 指令: + +```c++ +%attribute(MyNewClass, std::map&, config, getConfig, setConfig); +``` + +但当你执行 `node genbindings.js`的时候,你将得到如下错误: + +``` +Error: Macro '%attribute_custom' expects 7 arguments +``` + +这是因为 `swig` 看到 `std::map&` 的时候并不知道如何处理逗号 (`,`) ,它将其分割为两部分,即: + +1. std::map& + +因此, %attribute 指令这行将被解析为 6 个参数,而不是正确的 5 个参数。 + +为了避免这种情况出现,我们需要使用 `%arg` 指令来告诉 `swig` `std::map&` 是一个整体。 + +```c++ +%attribute(MyNewClass, %arg(std::map&), config, getConfig, setConfig); +``` + +重新执行 `node genbindings.js`,之前的错误即消失了。 + +#### 不要添加 `const` + +在上一示例中,我们在 %attribute 指令中使用 `%arg(std::map&)`。你可能会考虑在 `std::map` 前面添加一个 `const` 前缀,比如:`%arg(const std::map&)`。如果你这样做了,你将添加一个 **只读的**、只绑定 `MyNewClass::getConfig` 的 `config` 属性。这明显不是我们所期望的。如果我们需要属性是只读的,只需要不配置 `setter` 函数即可。 + +```c++ +// 不配置 setConfig 意味着属性是只读的 +%attribute(MyNewClass, %arg(std::map&), config, getConfig); +``` + +因此,为了让事情简单化,我们只要记得,**永远不要在定义 %attribute 的时候为 C++ 变量类型使用 const 前缀**。 + +### 配置 C++ 模块宏(用于 C++ 模块裁剪) + +有时候是否需要让一个类参与编译依赖于某个宏是否启用。比如,我们在 `MyObject.h` 文件中添加一个 `MyFeatureObject` 类: + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" + +#ifndef USE_MY_FEATURE +#define USE_MY_FEATURE 1 // Enable USE_MY_FEATURE +#endif + +namespace my_ns { + +#if USE_MY_FEATURE +class MyFeatureObject { +public: + void foo() { + CC_LOG_DEBUG("==> MyFeatureObject::foo"); + } +}; +#else +class MyFeatureObject; +#endif + +// MyObject 继承于 MyRef +class MyObject : public MyRef { +public: +//...... + MyFeatureObject* getFeatureObject() { +#if USE_MY_FEATURE // getFeatureObject 只在宏 USE_MY_FEATURE 启用的情况下返回有效值 + if (_featureObject == nullptr) { + _featureObject = new MyFeatureObject(); + } +#endif + return _featureObject; + } +private: + int _a{100}; + bool _b{true}; + int _type{333}; + MyFeatureObject* _featureObject{nullptr}; // 添加此行 +}; +} // namespace my_ns { +``` + +```c++ +// my-module.i +// ...... +%rename(MyCoolObject) my_ns::MyObject; + +%attribute(my_ns::MyObject, int, type, getType, setType); + +%module_macro(USE_MY_FEATURE) my_ns::MyFeatureObject; // 添加此行,用于让 Swig 知道生成出来的 MyFeatureObject 类的绑定代码需要被包在 USE_MY_FEATURE 下 +%module_macro(USE_MY_FEATURE) my_ns::MyObject::getFeatureObject; // 添加此行,用于让 Swig 知道生成出来的 MyObject::getFeatureObject 方法的绑定代码需要被包在 USE_MY_FEATURE 下 + +#define USE_MY_FEATURE 1 // 这里定义为 1 是骗过 Swig,让它帮我们生成绑定代码。注意,这行必须在 %module_macro 之后 + +%import "MyRef.h" +%include "MyObject.h" +``` + +```c++ +// my-module.d.ts +declare namespace my_ns { +class MyFeatureObject { + foo() : void; +} + +class MyCoolObject { + constructor(); + constructor(a: number, b: number); + + publicFloatProperty : number; + print() : void; + coolProperty: number; + coolMethod() : void; + type: number; + getFeatureObject() : MyFeatureObject; +} +} +``` + +```ts +// MyComponent.ts +import { _decorator, Component } from 'cc'; +const { ccclass } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyCoolObject(); + myObj.print(); + console.log(`==> myObj.publicFloatProperty: ${myObj.publicFloatProperty}`); + console.log(`==> old: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolProperty = 666; + console.log(`==> new: myObj.coolProperty: ${myObj.coolProperty}`); + myObj.coolMethod(); + console.log(`==> old: myObj.type: ${myObj.type}`); + myObj.type = 888; + console.log(`==> new: myObj.type: ${myObj.type}`); + const featureObj = myObj.getFeatureObject(); + console.log(`==> featureObj: ${featureObj}`); + if (featureObj) { + featureObj.foo(); + } + } +} +``` + +重新生成绑定代码,自动绑定代码如下: + +```c++ +#if USE_MY_FEATURE // 注意,现在所有 MyFeatureObject 相关的绑定代码都被包在 USE_MY_FEATURE 宏下面了。 + +se::Class* __jsb_my_ns_MyFeatureObject_class = nullptr; +se::Object* __jsb_my_ns_MyFeatureObject_proto = nullptr; +SE_DECLARE_FINALIZE_FUNC(js_delete_my_ns_MyFeatureObject) + +static bool js_my_ns_MyFeatureObject_foo(se::State& s) +{ +// ...... +} +// ...... +bool js_register_my_ns_MyFeatureObject(se::Object* obj) { + auto* cls = se::Class::create("MyFeatureObject", obj, nullptr, _SE(js_new_my_ns_MyFeatureObject)); +// ...... +} + +#endif // USE_MY_FEATURE + +// ...... +static bool js_my_ns_MyCoolObject_getFeatureObject(se::State& s) +{ +#if USE_MY_FEATURE // getFeatureObject 函数的绑定代码也被包在 USE_MY_FEATURE 宏下面了。 +// ...... + ok &= nativevalue_to_se(result, s.rval(), s.thisObject() /*ctx*/); + SE_PRECONDITION2(ok, false, "MyCoolObject_getFeatureObject, Error processing arguments"); + SE_HOLD_RETURN_VALUE(result, s.thisObject(), s.rval()); +#endif // USE_MY_FEATURE + return true; +} +SE_BIND_FUNC(js_my_ns_MyCoolObject_getFeatureObject) + +// ...... +bool register_all_my_module(se::Object* obj) { + // Get the ns + se::Value nsVal; + if (!obj->getProperty("my_ns", &nsVal, true)) + { + se::HandleObject jsobj(se::Object::createPlainObject()); + nsVal.setObject(jsobj); + obj->setProperty("my_ns", nsVal); + } + se::Object* ns = nsVal.toObject(); + /* Register classes */ +#if USE_MY_FEATURE + js_register_my_ns_MyFeatureObject(ns); // js_register_my_ns_MyFeatureObject 也被包在 USE_MY_FEATURE 宏下面了。 +#endif // USE_MY_FEATURE + js_register_my_ns_MyObject(ns); + return true; +} +``` + +在 Xcode 或者 Visual Studio 中运行项目,会得到如下输出: + +``` +18:32:20 [DEBUG]: D/ JS: ==> featureObj: [object Object] // 在 USE_MY_FEATURE 启用的情况下,featureObj 是个有效值 +18:32:20 [DEBUG]: ==> MyFeatureObject::foo // 调用 C++ foo 方法 +``` + +当我们不需要 `MyFeatureObject` 类的时候,把宏设置为 0 即可,代码示例如下: + +```c++ +// MyObject.h +#pragma once +#include "cocos/cocos.h" +#include "MyRef.h" + +#ifndef USE_MY_FEATURE +#define USE_MY_FEATURE 0 // Disable USE_MY_FEATURE +#endif +``` + +在 Xcode 或者 Visual Studio 中运行项目: + +``` +18:54:00 [DEBUG]: D/ JS: ==> featureObj: undefined // getFeatureObject returns undefined if USE_MY_FEATURE is disabled. +``` + +### 多个 Swig 模块的配置 + +我们创建另外一个头文件,名为 `MyAnotherObject.h`。 + +```c++ +// MyAnotherObject.h +#pragma once +namespace my_another_ns { +struct MyAnotherObject { + float a{135.246}; + int b{999}; +}; +} // namespace my_another_ns { +``` + +更新 MyObject.h + +```c++ +// MyObject.h +//...... +class MyObject : public MyRef { +public: +// ...... + void helloWithAnotherObject(const my_another_ns::MyAnotherObject &obj) { + CC_LOG_DEBUG("==> helloWithAnotherObject, a: %f, b: %d", obj.a, obj.b); + } +// ...... +}; +} // namespace my_ns { +``` + +创建 `/Users/james/NewProject/tools/swig-config/another-module.i` + +```c++ +// another-module.i +%module(target_namespace="another_ns") another_module + +// %insert(header_file) %{ ... }%} 代码块中的内容最终会被原封不动地插入到生成的头文件(.h)开头的地方 +%insert(header_file) %{ +#pragma once +#include "bindings/jswrapper/SeApi.h" +#include "bindings/manual/jsb_conversions.h" + +#include "MyAnotherObject.h" // 添加此行 +%} + +// %{ ... %} 代码块中的内容最终会被原封不动地插入到生成的源文件(.cpp)开头的地方 +%{ +#include "bindings/auto/jsb_another_module_auto.h" +%} + +%include "MyAnotherObject.h" +``` + +修改 `/Users/james/NewProject/tools/swig-config/swig-config.js` + +```c++ +'use strict'; + +const path = require('path'); + +const configList = [ + [ 'my-module.i', 'jsb_my_module_auto.cpp' ], + [ 'another-module.i', 'jsb_another_module_auto.cpp' ], // 添加此行 +]; + +const projectRoot = path.resolve(path.join(__dirname, '..', '..')); +const interfacesDir = path.join(projectRoot, 'tools', 'swig-config'); +const bindingsOutDir = path.join(projectRoot, 'native', 'engine', 'common', 'bindings', 'auto'); +const includeDirs = [ + path.join(projectRoot, 'native', 'engine', 'common', 'Classes'), +]; + +module.exports = { + interfacesDir, + bindingsOutDir, + includeDirs, + configList +}; +``` + +修改 `/Users/james/NewProject/native/engine/common/CMakeLists.txt` + +```cmake +# /Users/james/NewProject/native/engine/common/CMakeLists.txt +list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp + ${CMAKE_CURRENT_LIST_DIR}/Classes/MyObject.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/MyAnotherObject.h # Add this line + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.h + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_my_module_auto.cpp + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_another_module_auto.h # Add this line + ${CMAKE_CURRENT_LIST_DIR}/bindings/auto/jsb_another_module_auto.cpp # Add this line +) +``` + +重新生成绑定。 + +更新 Game.cpp: + +```c++ +#include "Game.h" +#include "../bindings/auto/jsb_my_module_auto.h" +#include "bindings/auto/jsb_another_module_auto.h" // Add this line +//...... + +int Game::init() { +//...... + se::ScriptEngine::getInstance()->addRegisterCallback(register_all_my_module); + se::ScriptEngine::getInstance()->addRegisterCallback(register_all_another_module); // Add this line +// + BaseGame::init(); + return 0; +} +``` + +在 Xcode 或者 Visual Studio 中编译,但是得到如下错误: + +![](another-module-compile-error.jpg) + +因为 MyObject 类依赖了 MyAnotherObject 类,而 MyAnotherObject 类是被定义在另外一个模块中的。我们需要修改 `my-module.i` 并添加 `#include "bindings/auto/jsb_another_module_auto.h"`。 + +```c++ +// my-module.i +%module(target_namespace="my_ns") my_module + +// %insert(header_file) %{ ... }%} 代码块中的内容最终会被原封不动地插入到生成的头文件(.h)开头的地方 +%insert(header_file) %{ +#pragma once +#include "bindings/jswrapper/SeApi.h" +#include "bindings/manual/jsb_conversions.h" + +#include "MyObject.h" +%} + +// %{ ... %} 代码块中的内容最终会被原封不动地插入到生成的源文件(.cpp)开头的地方 +%{ +#include "../bindings/auto/jsb_my_module_auto.h" +#include "bindings/auto/jsb_another_module_auto.h" // Add this line +%} + +// ...... +``` + +在 Xcode 或者 Visual Studio 中编译项目,现在应该可以正常编译了。 + +下一步,我们需要更新 .d.ts 文件: + +```ts +// my-module.d.ts +declare namespace my_ns { +class MyFeatureObject { + foo() : void; +} + +class MyCoolObject { + constructor(); + constructor(a: number, b: number); + + publicFloatProperty : number; + print() : void; + coolProperty: number; + coolMethod() : void; + type: number; + getFeatureObject() : MyFeatureObject; + helloWithAnotherObject(obj: another_ns.MyAnotherObject) : void; // 添加这行 +} +} + +// 添加以下行 +declare namespace another_ns { +class MyAnotherObject { + a: number; + b: number; +} +} +``` + +添加更多的用于读取 `MyAnotherObject` 类属性的测试代码: + +```ts +// MyComponent.ts +import { _decorator, Component } from 'cc'; +const { ccclass } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + start() { + const myObj = new my_ns.MyCoolObject(); + // ...... + const anotherObj = new another_ns.MyAnotherObject(); // 添加此行 + myObj.helloWithAnotherObject(anotherObj); // 添加此行 + } +} +``` + +在 Xcode 或者 Visual Studio 中编译项目,得到如下输出: + +``` +15:05:36 [DEBUG]: ==> helloWithAnotherObject, a: 135.246002, b: 999 +``` diff --git a/versions/4.0/zh/advanced-topics/jsb/v8-win32-debug.jpg b/versions/4.0/zh/advanced-topics/jsb/v8-win32-debug.jpg new file mode 100644 index 0000000000..40b6aac3cd Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/v8-win32-debug.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/v8-win32-memory.jpg b/versions/4.0/zh/advanced-topics/jsb/v8-win32-memory.jpg new file mode 100644 index 0000000000..54f8ba2a7c Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/v8-win32-memory.jpg differ diff --git a/versions/4.0/zh/advanced-topics/jsb/v8-win32-profile.jpg b/versions/4.0/zh/advanced-topics/jsb/v8-win32-profile.jpg new file mode 100644 index 0000000000..3297816571 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/jsb/v8-win32-profile.jpg differ diff --git a/versions/4.0/zh/advanced-topics/mangle-properties.md b/versions/4.0/zh/advanced-topics/mangle-properties.md new file mode 100644 index 0000000000..273063ea97 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/mangle-properties.md @@ -0,0 +1,72 @@ +# 压缩引擎内部属性 + +为了让用户能够进一步减少游戏包体,Cocos 引擎团队在 3.8.6 版本开始提供引擎内部属性压缩功能,该功能可以对引擎 TS 代码中的属性进行压缩,在不损失游戏性能的前提下有效减少游戏包体。 + +> 注意:目前此功能暂不支持原生平台。 + +## 构建时开启引擎内部属性压缩 + +![open-mangle-properties](mangle-properties/open-mangle-properties.png) + +## 生成压缩配置 + +在构建时会在项目根目录生成 `engine-mangle-config.json` 文件。用户如果想要快速生成此配置文件,可以先进行一次构建,等项目根目录下生成配置文件后再中断构建任务即可。 + +## 压缩配置字段 + +此功能默认对引擎 TS 代码中的 private 类型属性进行压缩,如果需要压缩 public 和 protected 类型的属性,需要在配置文件中进行相应设置。 + +| 字段 | 功能说明 +| :-------------- | :----------- | +| mangleProtected | 是否压缩 protected 类型的属性(默认为 false) +| mangleList | 添加需要压缩的引擎属性 +| dontMangleList | 添加不需要压缩的引擎属性 + +### 配置示例 + +#### "Profiler._meshRenderer" 为自定义添加的属性,其余均为默认压缩配置。 + +```json +{ + "COMMON": { + "mangleProtected": false, + "mangleList": [ + "UITransform._sortSiblings", + "UITransform._cleanChangeMap", + "Node._findComponents", + "Node._findChildComponent", + "Node._findChildComponents", + "Node.idGenerator", + "Node._stacks", + "Node._stackId", + "Node._setScene", + "EffectAsset._layoutValid", + "EffectAsset._effects", + "ReflectionProbe.DEFAULT_CUBE_SIZE", + "ReflectionProbe.DEFAULT_PLANER_SIZE", + "WebGLDeviceManager.setInstance", + "WebGL2DeviceManager.setInstance", + "CanvasPool", + + ], + "dontMangleList": [ + "Component", + "Profiler._meshRenderer" + ] + }, + "MINIGAME": { + "extends": "COMMON", + "mangleList": [], + "dontMangleList": [] + }, + "WECHAT": { + "extends": "MINIGAME", + "mangleList": [], + "dontMangleList": [] + } +} +``` + +## 注意事项 + +开启此功能并启用调试模式打包时,会少量增加 debug 包的体积。 \ No newline at end of file diff --git a/versions/4.0/zh/advanced-topics/mangle-properties/open-mangle-properties.png b/versions/4.0/zh/advanced-topics/mangle-properties/open-mangle-properties.png new file mode 100644 index 0000000000..34f7416896 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/mangle-properties/open-mangle-properties.png differ diff --git a/versions/4.0/zh/advanced-topics/memory-leak-detector.md b/versions/4.0/zh/advanced-topics/memory-leak-detector.md new file mode 100644 index 0000000000..b5a05618d7 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/memory-leak-detector.md @@ -0,0 +1,57 @@ +# 原生引擎内存泄漏检测系统 + +原生引擎使用 C++ 语言开发,为了方便游戏和引擎开发者快速查找内存泄漏,Creator 从 v3.4 开始提供了 **内存泄漏检测系统**。 + +相对其他内存泄漏查找工具,Cocos Creator 内置的内存泄漏检测工具有以下优点: + +- **跨平台**:支持 Windows/Android/Mac/iOS 平台。 +- **易用性**:无需下载额外的工具以及进行复杂的配置。支持输出内存泄漏处的堆栈信息,方便快速定位泄漏。 +- **一致性**:各平台的使用流程几乎一致,都是从原生平台对应的 IDE 中启动游戏 -> 运行一段时间 -> 关闭游戏 -> 查看 IDE 输出日志。 +- **实时性**:查找泄露过程中的游戏帧率虽有下降,但仍然保持实时运行帧率。 +- **精确性**:理论上零漏报,零误报。 + +## 使用步骤 + +1. 内存泄漏检测系统默认是关闭的。若要开启,需要将引擎目录下 `engine/native/cocos/base/Config.h` 文件中宏 `USE_MEMORY_LEAK_DETECTOR` 的值修改为 **1**。 + + ```c++ + #ifndef USE_MEMORY_LEAK_DETECTOR + #define USE_MEMORY_LEAK_DETECTOR 1 + #endif + ``` + +2. 由于实现机制的不同,Android 平台上需要额外执行以下步骤: + + 在项目目录的 `native/engine/android/CMakeLists.txt` 文件中添加一行代码 `set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -finstrument-functions")`,如下所示: + + ``` + set(CC_PROJ_SOURCES) + set(CC_COMMON_SOURCES) + set(CC_ALL_SOURCES) + + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -finstrument-functions") + ``` + +3. 从原生平台对应的 IDE(如 Visual Studio、Android Studio、Xcode)启动游戏,运行一段时间后关闭游戏,若存在内存泄漏,则此时会在 IDE 的输出窗口中输出内存泄漏的详细信息。 + + - **Windows 平台** + + ![visual studio](./memory-leak-detector/visualstudio.png) + + 若项目构建编译的是 Release 版本,如果需要更友好的堆栈信息,可在 Visual Studio 中右键点击左侧的可执行项目,选择 **属性**,打开项目属性页,进行如下设置: + + - 链接器 -> 调试 -> 生成调试信息:生成调试信息(/DEBUG) + + - C/C++ -> 优化 -> 优化:已禁用(/Od) + + - C/C++ -> 优化 -> 内联函数扩展:已禁用(/Ob0) + + - **Android 平台** + + ![android studio](./memory-leak-detector/androidstudio.png) + + - **Mac/iOS 平台** + + ![xcode](./memory-leak-detector/xcode.png) + +4. 根据原生平台 IDE 输出的信息修复泄漏,如此反复,直到没有泄漏为止。 diff --git a/versions/4.0/zh/advanced-topics/memory-leak-detector/androidstudio.png b/versions/4.0/zh/advanced-topics/memory-leak-detector/androidstudio.png new file mode 100644 index 0000000000..ba37bb8981 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/memory-leak-detector/androidstudio.png differ diff --git a/versions/4.0/zh/advanced-topics/memory-leak-detector/visualstudio.png b/versions/4.0/zh/advanced-topics/memory-leak-detector/visualstudio.png new file mode 100644 index 0000000000..87c3998f88 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/memory-leak-detector/visualstudio.png differ diff --git a/versions/4.0/zh/advanced-topics/memory-leak-detector/xcode.png b/versions/4.0/zh/advanced-topics/memory-leak-detector/xcode.png new file mode 100644 index 0000000000..cd9b357773 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/memory-leak-detector/xcode.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/brief.md b/versions/4.0/zh/advanced-topics/native-plugins/brief.md new file mode 100644 index 0000000000..3c696f1023 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/native-plugins/brief.md @@ -0,0 +1,111 @@ +# 原生插件 + +> **注意**: +> +> * 请在 CocosCreator 3.6.3 及以上版本使用原生插件功能。 +> * 原生插件功能目前还未适配鸿蒙相关平台 + +原生插件是编辑器插件的一部分。开发者通过原生插件调用脚本绑定接口(如 sebind)可以扩展 JS 脚本调用 C++ 的接口的能力,对解决脚本的性能瓶颈和复用现有代码库都非常有利。 + +## 和现有插件系统的关系 + +原生插件能独立于编辑器插件存在,用户通过拷贝到指定目录就可以使用原生插件。 + +同时,原生插件也作为现有编辑器插件系统的补充,扩展游戏运行时的能力。利用编辑插件的能力实现对原生插件管理, 如:下载/开关/版本升级等功能。 + +### 插件的结构 + +每个插件的根目录下都有一个插件的描述文件 `cc_plugin.json`,这是一个标准的 JSON 文件。 + +在构建原生工程的时候,构建系统会从工程的 `extensions` 和 `native` 目录中去递归查找这所有的 `cc_plugin.json` 文件,以定位原生插件。一旦在目录中找到 `cc_plugins.json`,就不会再查找子目录中的内容。 + +## 安装依赖 + +在少数未安装编辑器的环境下,需要安装 [NodeJS](https://nodejs.org/en/download/) 8.0 以上的版本,以支持插件配置解析。开发者可以将 NodeJS 并设置环境变量 `PATH`,也可以在 `CMakeLists.txt` 中通过设置 `NODE_EXECUTABLE` 指定。 + +也可以设置环境变量 `NODE_EXECUTABLE` 为 node 的完整路径。3.6.2 开始,如果 CMake 仍然定位不到 nodejs,可以在 `native/engine/common/localCfg.cmake` 中直接设置 `NODE_EXECUTABLE`。 + +## 基础目录结构示例 + +``` +├── cc_plugin.json +├── android +│ ├── arm64-v8a +│ ├── armeabi-v7a +│ ├── x86 +│ └── x86_64 +│__ google-play +│ ├── arm64-v8a +│ ├── armeabi-v7a +│ ├── x86 +│ └── x86_64 +├── ios +│ ├── include +│ └── lib +├── mac +│ ├── include +│ └── lib +│── windows +│ ├── include +│ └── lib +``` + +文件 `cc_plugin.json` 是提供了加载插件所必须的信息,是原生插件的标识。每一个支持的原生平台对应一个目录,目录中至少包含一个 `-Config.cmake` 文件。构建系统会子使用 CMake 的 [`find_package`](https://cmake.org/cmake/help/latest/command/find_package.html#id7) 机制定位或链接到所需的库文件。 + +如果插件中存在可跨平台的源文件或 CMake 配置, 可以将这些文件合并到顶层目录。详情请参考 [示例工程](https://github.com/PatriceJiang/ccplugin_tutorial/tree/main/NewProject/native/plugins/hello_cocos)。 + +## 描述文件 `cc_plugin.json` 格式 + +```ts +{ + "name": string; // 必填:插件名称 + "version": string; // 必填:插件版本 + "engine-version":string; // 必填:对应引擎版本的区间 + "author": string; // 必填:插件作者 + "description": string; // 必填:插件描述 + "platforms":string[]; // 可选:支持的平台列表,不填默认支持所有原生平台。包括 windows, android, gppgle-play, mac, ios + "disabled":true; // 可选:禁用插件 + "disable-by-platforms":string[]; //可选:指定平台禁用插件 + "modules": [{ // 必填:插件包含的库, + "target":string; // 必填:对应 `find_package` 名称,需和 `CC_PLUGIN_ENTRY` 的首参数保持一致 + "depends": string|string[]; // 可选:依赖其他 module 名称 + "platforms":string[]; // 可选:重新限定支持的原生平台 + }] + +} +``` + +`engine-version` 可以指定版区间和排除指定版本,代码示例如下: + +```ts +"engine-version": ">=3.3 <= 3.6.0 !3.5.2|| 4.x" +``` + +### 文件示例 + +```json +{ + "name":"hello-cocos-demo", + "version":"1.0.0", + "author":"cocos", + "engine-version":">=3.6.3", + "disabled":false, + "modules":[ + { + "target":"hello_cocos_glue" + } + ], + "platforms":["windows", "android", "mac", "ios", "google-play"] +} +``` +## 创建原生插件 + +Cocos 原生工程使用 CMake 管理,原生插件会通过 find_package 的搜索路径/目录来进行管理,因此只要目录符合 CMake find_package 的搜索规则,插件就能正确加载。所以,原生插件的开发过程,就是提供 CMake 配置和相关的资源,以及编写 cc_plugin.json 的过程。相关示例请参考 [原生插件创建与使用示例](./tutorial.md)。 + +## 安装与关闭原生插件 + +从 [原生插件创建与使用示例](https://github.com/zhefengzhang/cocos-native-plugins) 中下载创建好的插件包,根据项目需要选择一个文件夹复制到 `extensions` 或 `native` 目录下。 + +如果想关闭插件,或者仅在特地平台关闭,可以修改 cc_plugin.json 中的 `disabled` 和 `disable-by-platforms` 字段。 + +> **注意**:原生插件要求 CMake 为 3.12+,Android 需要 [指定 CMake 版本](https://developer.android.com/studio/projects/install-ndk#vanilla_cmake) 为 3.18.1. 其他平台使用编辑器内建的 CMake,可不必指定版本号。 \ No newline at end of file diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/1_2_save_emtpy_scene.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/1_2_save_emtpy_scene.png new file mode 100644 index 0000000000..1b0cdd7053 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/1_2_save_emtpy_scene.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/1_3_create_windows_build.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/1_3_create_windows_build.png new file mode 100644 index 0000000000..b0ad424636 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/1_3_create_windows_build.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/1_create_empty_project.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/1_create_empty_project.png new file mode 100644 index 0000000000..b9fb948ee2 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/1_create_empty_project.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_1_link_error.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_1_link_error.png new file mode 100644 index 0000000000..14d679daf9 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_1_link_error.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_1_vs_project.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_1_vs_project.png new file mode 100644 index 0000000000..566a607565 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_1_vs_project.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_3_debug_url.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_3_debug_url.png new file mode 100644 index 0000000000..d44f661a3e Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_3_debug_url.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_3_empty_window.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_3_empty_window.png new file mode 100644 index 0000000000..1f4ca1f600 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_3_empty_window.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_4_devtool.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_4_devtool.png new file mode 100644 index 0000000000..6756351ddc Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_4_devtool.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_5_devtool.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_5_devtool.png new file mode 100644 index 0000000000..6d9dd69bd6 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/2_5_devtool.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/doc/images/3_1_android_build.png b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/3_1_android_build.png new file mode 100644 index 0000000000..0aa1a08654 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-plugins/doc/images/3_1_android_build.png differ diff --git a/versions/4.0/zh/advanced-topics/native-plugins/tutorial.md b/versions/4.0/zh/advanced-topics/native-plugins/tutorial.md new file mode 100644 index 0000000000..cee19842b9 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/native-plugins/tutorial.md @@ -0,0 +1,387 @@ +# 原生插件创建范例 + +如果想在原生项目中使用第三方原生库,则可以按照本文的步骤进行。 + +本文需要对原生工程的编译生成有一定了解,开发者可以通过 [CMake 官网](https://cmake.org/) 了解。 我们也准备了 [原生插件创建与使用示例](https://github.com/zhefengzhang/cocos-native-plugins) 以供参考。 + +## 创建原生插件 + +### 编译依赖库或静态库 + +通过应用平台提供的编译工具将 c 或 cpp 文件编译为 .lib 或 .a 文件。在 [原生插件创建与使用示例](https://github.com/zhefengzhang/cocos-native-plugins) 仓库中已经将 src 文件夹中的 hello_cocos.cpp 编译为 .lib 与 .a 文件并添加到各个平台的插件目录下。仓库中 jni 目录提供了 android 平台使用 `ndk-build` 命令编译 .a 文件时所使用的配置与代码,供开发者参考。其余平台请自行编译。 + +### 插件开发工程 Windows 配置 + +示例中,我们将引入 hello_cocos.lib 作为 windows 平台上的插件,引入引擎并使其支持在 TS/JS 中使用。其他平台将使用 hello_cocos.a 为例,如果要使用其他库,请提前编译到对应平台。 + +- 使用 Cocos Creator 3.6.3 及以上版本创建工程 + + 启动 CocosCreator,在指定目录执行 `创建空工程`。 + + ![create](doc/images/1_create_empty_project.png) + +- 创建并保存一个空的场景 + + ![save scene](doc/images/1_2_save_emtpy_scene.png) + +- 通过 **构建发布** 面板导出原生工程并构建,生成 `native/` 目录 + + 这里在 Windows 上新建构建任务。 + + ![build windows](doc/images/1_3_create_windows_build.png) + + 执行 **构建**,同时生成 `native/` 目录。 + + 通过控制台(Windows CMD 或 PowerShell 等类似软件)查看目录的内容: + + ```console + $ tree native/ -L 2 + native/ + └── engine + ├── common + └── win64 + + ``` + +- 在 `native/` 中创建插件存放的目录 + + ```console + mkdir -p native/native-plugin/ + ``` + +### 添加原生插件对 Windows 的支持 + +- 添加 Windows 平台相关的子目录: + + ```console + mkdir -p native/native-plugin/windows/ + ``` + +- 把预先编译好的依赖库 `hello_cocos.lib` 和头文件拷贝到对应的目录: + + ```console + $ tree native/native-plugin/ + + native/native-plugin/ + ├── include + │   └── hello_cocos.h + └── windows + └── lib + ├── hello_cocos.lib + └── hello_cocosd.lib + ``` + +- 添加文件 `hello_cocos_glue.cpp`,`CMakeLists.txt` 和 `hello_cocos_glue-config.cmake`: + + ```console + mkdir native/native-plugin/src + touch native/native-plugin/src/hello_cocos_glue.cpp + touch native/native-plugin/src/CMakeLists.txt + touch native/native-plugin/hello_cocos_glue-config.cmake + ``` + + 当前插件目录的内容: + + ```console + $ tree native/native-plugin/ + native/native-plugin/ + ├── include + │   └── hello_cocos.h + ├── src + │   ├── CMakeLists.txt + │   └── hello_cocos_glue.cpp + └── windows + ├── hello_cocos_glue-config.cmake + └── lib + ├── hello_cocos.lib + └── hello_cocosd.lib + ``` + +- 编辑 `hello_cocos_glue-config.cmake` 中添加声明 `hello_cocos.lib` 和导入的内容: + + ```cmake + set(_hello_cocos_GLUE_DIR ${CMAKE_CURRENT_LIST_DIR}) + + add_library(hello_cocos STATIC IMPORTED GLOBAL) + set_target_properties(hello_cocos PROPERTIES + IMPORTED_LOCATION ${_hello_cocos_GLUE_DIR}/lib/hello_cocos.lib + IMPORTED_LOCATION_DEBUG ${_hello_cocos_GLUE_DIR}/lib/hello_cocosd.lib + ) + + include(${_hello_cocos_GLUE_DIR}/../src/CMakeLists.txt) + ``` + +- 编辑 `native/native-plugin/src/CMakeLists.txt`,并添加如下内容: + + ```cmake + set(_hello_cocos_GLUE_SRC_DIR ${CMAKE_CURRENT_LIST_DIR}) + + add_library(hello_cocos_glue ${_hello_cocos_GLUE_SRC_DIR}/hello_cocos_glue.cpp) + + target_link_libraries(hello_cocos_glue + hello_cocos + ${ENGINE_NAME} # cocos_engine + ) + + target_include_directories(hello_cocos_glue PRIVATE + ${_hello_cocos_GLUE_SRC_DIR}/../include + ) + ``` + +- 在目录 `native/native-plugin/` 中创建配置文件 `cc_plugin.json` + + ```json + { + "name":"hello-cocos-demo", + "version":"1.0.0", + "author":"cocos", + "engine-version":">=3.6.3", + "disabled":false, + "modules":[ + { + "target":"hello_cocos_glue" + } + ], + "platforms":["windows"] + } + ``` + + 现在原生插件所需的文件已经创建,但还不能编译。文件 `hello_cocos_glue.cpp` 需要注册插件的初始化函数。 + + 再次执行 **构建** 触发 Visual Studio 工程的更新。 + +- 使用 Visual Studio 打开目录 `build/windows/proj/` 中的 sln 文件 + + - 自动生成一个 `plugin_registry` 目标,用于初始化所有启用的插件: + + ![Solution Explorer](./doc/images/2_1_vs_project.png) + + - 直接运行目标会导致类似的报错: + + ![link error](./doc/images/2_1_link_error.png) + +- 编辑 `hello_cocos_glue.cpp` + + ```c++ + #include "hello_cocos.h" + #include "bindings/sebind/sebind.h" + #include "plugins/bus/EventBus.h" + #include "plugins/Plugins.h" + + // export c++ methods to JS + static bool register_demo(se::Object *ns) { + + sebind::class_ klass("Demo"); + + klass.constructor() + .function("hello", &Demo::hello); + klass.install(ns); + return true; + } + + void add_demo_class() { + using namespace cc::plugin; + static Listener listener(BusType::SCRIPT_ENGINE); + listener.receive([](ScriptEngineEvent event) { + if (event == ScriptEngineEvent::POST_INIT) { + se::ScriptEngine::getInstance()->addRegisterCallback(register_demo); + } + }); + } + + /** + * Regist a new cc plugin entry function + * first param: should match the name in cc_plugin.json + * second param: callback when engine initialized + */ + CC_PLUGIN_ENTRY(hello_cocos_glue, add_demo_class); + ``` + + 再次编译后不在报错,此时可正确的编译和运行工程。 + +- 运行目标工程: + + ![empty window](./doc/images/2_3_empty_window.png) + +- 为了验证我们的原生插件是否已经加载,我们需要连接 devtools: + + 从 `Output` 面板,获取调试连接。 + + ![debug url](./doc/images/2_3_debug_url.png) + + 打开浏览器,输入上图中的调试链接地址,在控制台(Console)中键入下面的代码: + + ```javascript + new Demo("World").hello("Cocos") + ``` + + ![devtools](./doc/images/2_5_devtool.png) + + 根据输出可以确认,我们的接口已经成功通过原生插件导出。 + +### 添加原生插件对 Android 的支持 + +- 添加 Android 的构建任务 + +- 创建 Android 相关的原生插件目录 + + ```console + mkdir native/native-plugin/android + ``` + +- 将预先编译好的依赖库和头文件拷贝到对应的目录,创建 `hello_cocos_glue-config.cmake` + + Android 目录的状态: + + ```console + $ tree native/native-plugin/android/ + native/native-plugin/android/ + ├── hello_cocos_glue-config.cmake + ├── arm64-v8a + │   └── lib + │   └── libhello_cocos.a + └── armeabi-v7a + └── lib + └── libhello_cocos.a + + ``` + +- 编辑 `hello_cocos_glue-config.cmake` + + ```cmake + set(_hello_cocos_GLUE_DIR ${CMAKE_CURRENT_LIST_DIR}) + + add_library(hello_cocos STATIC IMPORTED GLOBAL) + set_target_properties(hello_cocos PROPERTIES + IMPORTED_LOCATION ${_hello_cocos_GLUE_DIR}/${ANDROID_ABI}/lib/libhello_cocos.a + ) + + include(${_hello_cocos_GLUE_DIR}/../src/CMakeLists.txt) + ``` + +- 更新 `cc_plugin.json`,添加 `android` 到 `platforms` 字段 + + ```json + { + "name":"hello-cocos-demo", + "version":"1.0.0", + "author":"cocos", + "engine-version":">=3.6.3", + "disabled":false, + "modules":[ + { + "target":"hello_cocos_glue" + } + ], + "platforms":["windows", "android"] + } + + ``` + +- 新增 Android 的构建任务 + + ![Android build](./doc/images/3_1_android_build.png) + +构建后可使用 Android Studio 打开工程,并使用 devtool 调试验证。 + +### 添加原生插件对 iOS 的支持 + +- 添加 iOS 的构建任务 + +- 创建 iOS 相关的原生插件目录 + + ``` + mkdir -p native/native-plugin/ios/lib + ``` + +- 将预先编译好的依赖库和头文件拷贝到对应的目录,创建 `hello_cocos_glue-config.cmake`,如根据下列示例编辑: + + ```cmake + set(_hello_cocos_GLUE_DIR ${CMAKE_CURRENT_LIST_DIR}) + + + add_library(hello_cocos STATIC IMPORTED GLOBAL) + set_target_properties(hello_cocos PROPERTIES + IMPORTED_LOCATION ${_hello_cocos_GLUE_DIR}/lib/libhello_cocos.a + ) + + include(${_hello_cocos_GLUE_DIR}/../src/CMakeLists.txt) + ``` + +### 添加原生插件对 MacOS 的支持 + +- 添加 MacOS 的构建任务 + +- 创建 MacOS 相关的原生插件目录 + + ```console + mkdir -p native/native-plugin/mac/lib + ``` + +- 将预先编译好的依赖库和头文件拷贝到对应的目录,创建`hello_cocos_glue-config.cmake` + + ```cmake + set(_hello_cocos_GLUE_DIR ${CMAKE_CURRENT_LIST_DIR}) + + add_library(hello_cocos STATIC IMPORTED GLOBAL) + set_target_properties(hello_cocos PROPERTIES + IMPORTED_LOCATION ${_hello_cocos_GLUE_DIR}/lib/libhello_cocos.a + ) + + include(${_hello_cocos_GLUE_DIR}/../src/CMakeLists.txt) + ``` + +- 更新 `cc_plugin.json`,添加 `iOS` 和 `mac` 到 `platforms` 字段 + + ```json + { + "name":"hello-cocos-demo", + "version":"1.0.0", + "author":"cocos", + "engine-version":">=3.6.3", + "disabled":false, + "modules":[ + { + "target":"hello_cocos_glue" + } + ], + "platforms":["windows", "android", "iOS", "mac"] + } + + ``` + +至此,一个支持 Android、Windows、MacOS 以及 iOS 的原生插件就开发完成了。 + +原生插件目录的最终内容如下: + +```console +$ tree native/native-plugin/ +native/native-plugin +├── cc_plugin.json +├── include +│ └── hello_cocos.h +├── src +│ ├── CMakeLists.txt +│ └── hello_cocos_glue.cpp +├── android +│ ├── hello_cocos_glue-config.cmake +│ ├── arm64-v8a +│ │ └── lib +│ │ └── libhello_cocos.a +│ └── armeabi-v7a +│ └── lib +│ └── libhello_cocos.a +├── ios +│ ├── hello_cocos_glue-config.cmake +│ └── lib +│ └── libhello_cocos.a +├── mac +│ ├── hello_cocos_glue-config.cmake +│ └── lib +│ └── libhello_cocos.a +└── windows + ├── hello_cocos_glue-config.cmake + └── lib + ├── hello_cocos.lib + └── hello_cocosd.lib +``` diff --git a/versions/4.0/zh/advanced-topics/native-profiler/add-stats.png b/versions/4.0/zh/advanced-topics/native-profiler/add-stats.png new file mode 100644 index 0000000000..2ff1942433 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-profiler/add-stats.png differ diff --git a/versions/4.0/zh/advanced-topics/native-profiler/enable-profiler.png b/versions/4.0/zh/advanced-topics/native-profiler/enable-profiler.png new file mode 100644 index 0000000000..e218692666 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-profiler/enable-profiler.png differ diff --git a/versions/4.0/zh/advanced-topics/native-profiler/profiler.png b/versions/4.0/zh/advanced-topics/native-profiler/profiler.png new file mode 100644 index 0000000000..67a0ca8621 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-profiler/profiler.png differ diff --git a/versions/4.0/zh/advanced-topics/native-scene-culling.md b/versions/4.0/zh/advanced-topics/native-scene-culling.md new file mode 100644 index 0000000000..90f0c235a0 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/native-scene-culling.md @@ -0,0 +1,39 @@ +# 原生场景剔除 + +Creator 从 v3.4.0 开始支持原生场景剔除,包括 **八叉树场景剔除** 和 **遮挡查询剔除**,仅对原生平台生效。 + +## 八叉树场景剔除 + +一般情况下,引擎剔除不在视锥(摄像机的可见范围)内的模型,是通过逐个检测模型的包围盒是否在视锥内,速度较慢。若开启八叉树场景剔除,则通过八叉树可以快速剔除不在视锥内的模型。 + +该功能默认关闭,若需要开启,在 **层级管理器** 中选中场景根节点 **Scene**,然后在 **属性检查器** 中便可看到 **Octree Scene Culling** 项,勾选 **Enabled** 即可: + +![octree scene culling](./native-scene-culling/octree-scene-culling.png) + +开启八叉树场景剔除后,在 **场景编辑器** 中会显示整个世界的包围盒(例如上图场景中白色边框的立方体)。 + +**Octree Scene Culling** 的属性说明如下: + +| 属性 | 说明 | +| :-- | :-- | +| Enabled | 勾选该项即可开启八叉树场景剔除,仅对原生平台生效 | +| World MinPos | 世界包围盒的最小顶点坐标(超出包围盒的物体不渲染) | +| World MaxPos | 世界包围盒的最大顶点坐标(超出包围盒的物体不渲染) | +| Depth | 八叉树深度,默认值为 8。若场景较小,该值建议不要设置太大,否则可能会耗费内存 | + +## 遮挡查询剔除 + +**遮挡查询剔除** 默认关闭,如果开启,则 GFX 后端会通过图形 API 进行遮挡查询,若物体被遮挡,则只使用简化的包围盒及材质来渲染该物体,以提升性能。 + +该功能可通过代码开启: + +```ts +director.root.pipeline.setOcclusionQueryEnabled(true); +``` + +> **注意**:GLES 2.0 不支持遮挡查询剔除,某些 GLES 3.0 设备如果没有 `GL_EXT_occlusion_query_boolean` 扩展也不支持。 + +## 性能优化建议 + +- 如果场景中大部分物体都可见,建议不要开启 **八叉树场景剔除** 和 **遮挡查询剔除**。 +- 不同设备可能表现略有差异,可通过性能测试对比后,决定是否开启相应的剔除功能。 diff --git a/versions/4.0/zh/advanced-topics/native-scene-culling/octree-scene-culling.png b/versions/4.0/zh/advanced-topics/native-scene-culling/octree-scene-culling.png new file mode 100644 index 0000000000..c537849390 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/native-scene-culling/octree-scene-culling.png differ diff --git a/versions/4.0/zh/advanced-topics/native-secondary-development.md b/versions/4.0/zh/advanced-topics/native-secondary-development.md new file mode 100644 index 0000000000..eccbe6f44c --- /dev/null +++ b/versions/4.0/zh/advanced-topics/native-secondary-development.md @@ -0,0 +1,148 @@ +# 原生平台二次开发指南 + +如果你需要为增加第三方 SDK 库,或者增删 C++,OC,JAVA 代码文件,以下内容可以帮你更好地理解。 + +## 原生项目目录 + +当点击 **构建** 按钮后,会生成三个原生平台相关的文件夹。 + +### 公共目录 + +公共目录位置:`native/engine/common` + +此目录用于存放共公内容,如引擎库配置,以及一些所有平台都会用上的第三方库。 + +> 这个目录下的代码多数是由 C/C++ 编写。 + +### 原生平台目录 + +平台目录名字规则:native/engine/当前构建的平台名称 + +这个目录用于存放对应平台相关的信息,比如: + +- native/engine/android +- native/engine/ios +- native/engine/win64 +- native/engine/mac + +> win64 用于 windows, 目前已不再支持 win32, 仅支持 win64 应用程序发布。 + +### 项目目录 + +项目目录名字规则:`build/当前构建的平台名称` + +这个目录,包含的是最终生成的原生工程,用于编译、调试和发布。如: + +- build/android +- build/ios +- build/windows +- build/mac + +每一次构建时,引擎会将公共目录和原生平台目录,以及 Cocos Creator 项目中的资源、脚本等结合在一起,生成项目目录。 + +项目目录中的代码和相关配置引用原生平台目录下的文件,在 IDE 中改动对应的部分,平台目录下的文件也会做对应修改。 + +**例外**:`native/engine/ios/info.plist` 与 `native/engine/mac/info.plist` 文件由于`CMake`的机制,使用的是复制方式。 如果要对 `info.plist` 进行修改,则需要注意。 + +项目目录包含下内容: +- `assets`:`data` 目录的软链,用于兼容各平台 +- `data`:Cocos Creator 项目中的资源和脚本生成的内容 +- `proj`:存放当前构建的原生平台工程,可用于对应平台的 IDE(如 Xcode,Android Studio 等) 执行编译、调试和发布。 +- `cocos.compile.config.json`:本次构建的采用的构建选项配置 + +## 原生项目定制开发 + +因为项目需要,有时候我们需要修改、增删原生平台相关的源代码,或者引入第三方SDK,修改项目配置等。这些工作,我们称为项目定制开发。 + +下面我们就分类说明,不同情况下的需求,如何操作。 + +### 修改引擎代码 + +请参考 [引擎定制工作流程](./engine-customization.md)。 + +### 修改项目代码 + +如果需要修改项目相关的代码,只需要找到对应文件进行修改即可,修改完即可编译,不需要额外配置。 + +### 增删项目代码文件 + +增删代码文件,将会涉及到编译配置,而不同的语言和平台有所差异,下面将分类说明。 + +#### 增删 C++ 文件 + +如果需要增加和删除项目相关的 `C++` 文件,需要做对应的 `CMakeList.txt` 修改。 + +如果增删的是 `native/engine/common/` 目录下的代码,则需要修改 `native/engine/common/CMakeLists.txt` + +```bat +list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp +) +``` + +如上面代码所示,找到对应位置,添加或者删除自己的源码即可。 + +如果增删的是平台相关的 `C++` 代码文件,则需要修改 `native/engine/平台名称/CMakeLists.txt`。参考下面代码: + +```bat +include(${CC_PROJECT_DIR}/../common/CMakeLists.txt) + +//在这个位置添加自己的 C++ 文件 +list(APPEND CC_PROJ_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/MyTest2.hpp + ${CMAKE_CURRENT_LIST_DIR}/MyTest2.cpp +) +``` + +#### 增删 OC 文件 + +Cocos Creator 生成的 iOS/macOS 原生工程中,Objective-C 文件的管理方式,与 C++ 完全一致,参考上面的内容即可。 + +#### 增删 Java 文件 + +Java 语言本身是基于路径的包管理机制,增删 `JAVA` 文件不需要做特殊处理。 + +### 引入第三方 C++/OC 库 + +如果引入的库是由 C++/OC 编写而成,则根据情况将 SDK 放入 `native/engine/common/` 或者 `native/engine/平台名称/` 目录下,并修改对应目录下的 `CMakeList.txt`。 + +OC 库只能放在平台目录,不能放在 `native/engine/common/`,否则会导致在其他原生平台出现编译错误。 + +大部分 C++ SDK 也提供了自己的 `CMakeList.txt`,直接通过 `include` 的方式集成就行。 + +关于 CMake 的配置,可以参考项目中已有的 `CMakeList.txt` 进行修改。更多关于 CMake 的使用详情,可参考 [CMake 使用简介](../advanced-topics/cmake-learning.md)。 + +### 引入 Jar 库 + +如果引入的库是 Android 平台特有的库,直接放到对应的 `native/engine/android/` 目录,配置 `native/android/build.gradle` 即可。 + +## 脚本与原生通信 + +新写的原生方法,或者新引入的原生 SDK,如果想要导出到脚本层使用,可以采用以下几种方案。 + +### 使用 JsbBridge + +如果需要调用一些简单,非高频的函数,可以使用 `JsbBridge` 机制进行调用。 + +- [使用 JsbBridge 实现 JavaScript 与 Java 通信](js-java-bridge.md) +- [使用 JsbBridge 实现 JavaScript 与 Objective-C 通信](js-oc-bridge.md) + +### JSB 自动绑定 + +对于需要高频调用,或者批量导出 API 到脚本层的接口,建议使用 [JSB 自动绑定](jsb-auto-binding.md) 机制实现脚本与原生交互。 + +### 基于语言反射机制 + +基于 Java 和 OC 语言反射机制的通信,也可以很方便实现脚本与原生的交互,但由于 iOS 的审核规则越来越严格,iOS 上使用反射机制有审核失败的风险。 + +- [基于反射机制实现 JavaScript 与 Android 系统原生通信](java-reflection.md) +- [基于反射机制实现 JavaScript 与 iOS/macOS 系统原生通信](oc-reflection.md) + +## 源码版本管理 + +如果你的团队使用源码版本管理软件进行多人协同工作,`native` 目录需要全部加入到源码版本管理。 + +所有的项目定制化工作都应该尽量放到 `native` 目录,这样 `build` 目录就可以随时被删除,它不需要加入到源代码版本管理。 + +对于一些特殊的项目需求,无法在 `native` 目录下完成,则需要改动 `build` 目录下的内容,此时应该根据需求将对应的文件夹加入管理。 diff --git a/versions/4.0/zh/advanced-topics/oc-reflection.md b/versions/4.0/zh/advanced-topics/oc-reflection.md new file mode 100644 index 0000000000..1209e29d55 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/oc-reflection.md @@ -0,0 +1,247 @@ +# 基于反射机制实现 JavaScript 与 iOS/macOS 系统原生通信 + +## JavaScript 调用 Objective-C 代码 + +在 Cocos Creator 中提供了依靠语言反射机制的跨语言通信方式,使 JavaScript 可以直接调用 Objective-C 函数的方法。 方法原型示例如下: + +```js +var result = native.reflection.callStaticMethod(className, methodName, arg1, arg2, .....); +``` + +在 `native.reflection.callStaticMethod` 方法中,我们通过传入 Objective-C 的类名、方法名、参数就可以直接调用 Objective-C 的静态方法,并且可以获得 Objective-C 方法的返回值。 + +> **注意**:仅支持调用可访问类的静态方法。 + +**警告**:苹果 App Store 在 2017 年 3 月对部分应用发出了警告,原因是使用了一些有风险的方法,其中 `respondsToSelector:` 和 `performSelector:` 是反射机制使用的核心 API,在使用时请谨慎关注苹果官方对此的态度发展,相关讨论:[JSPatch](https://github.com/bang590/JSPatch/issues/746)、[React-Native](https://github.com/facebook/react-native/issues/12778) 以及 [Weex](https://github.com/alibaba/weex/issues/2875)。 + +为了降低提审不通过的风险,建议参考 [使用 JsbBridge 实现 JavaScript 与 Objective-C 通信](js-oc-bridge.md)。 + +### 类名与静态方法 + +参数中的类名不需要路径,只需要传入 Objective-C 中的类名即可。比如你在工程目录下的任意文件中新建一个类 `NativeOcClass`,只要你将它引入工程即可。 + +再次强调一下,仅支持 JavaScript 调用 Objective-C 中类的静态方法。 + +### 带参方法 + +方法名比较需要注意。我们需要传入完整的方法名,特别是当某个方法带有参数的时候,需要将它的 **:** 也带上。 + +有参方法定义示例: + +```objc +import +@interface NativeOcClass : NSObject ++(BOOL)callNativeUIWithTitle:(NSString *) title andContent:(NSString *)content; +@end +``` + +有参方法调用示例: + +```js +if(sys.isNative && (sys.os == sys.OS.IOS || sys.os == sys.OS.OSX)){ + var ret = native.reflection.callStaticMethod("NativeOcClass", + "callNativeUIWithTitle:andContent:", + "cocos2d-js", + "Yes! you call a Native UI from Reflection"); +} +``` + +`sys.isNative` 用于判断是否为原生平台,`sys.os` 用于判断当前运行系统。由于各平台通信机制不同,建议先判断再处理。 + +> **注意**:此时的方法名是 `callNativeUIWithTitle:andContent:`,不要漏掉了冒号 **:** 。 + +### 无参方法 + +如果是没有参数的函数,那么就不需要 **:**。如下面代码中的方法名是 `callNativeWithReturnString`,由于没有参数,就不需要 **:**,跟 Objective-C 的 method 写法一致。 + +无参方法定义示例: + +```objc ++(NSString *)callNativeWithReturnString; +``` + +无参方法调用示例: + +```js +var ret = native.reflection.callStaticMethod("NativeOcClass", + "callNativeWithReturnString"); +``` + +### 返回值 + +这里是这个方法在 Objective-C 的实现,可以看到是弹出了一个原生对话框。并把 `title` 和 `content` 设置成你传入的参数,并返回一个 boolean 类型的返回值。 + +```objc ++(BOOL)callNativeUIWithTitle:(NSString *) title andContent:(NSString *)content{ + UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:title message:content delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil]; + [alertView show]; + return true; +} +``` + +此时,你就可以在 `ret` 中接收到从 Objective-C 传回的返回值(true)了。 + +### 参数类型转换 + +在 Objective-C 的实现中,如果方法的参数需要使用 float、int 和 bool 的,请使用如下类型进行转换: + +- **float、int 请使用 NSNumber 类型** +- **bool 请使用 BOOL 类型** + +例如下面代码,传入两个浮点数,然后计算它们的合并返回,我们使用 NSNumber 作为参数类型,而不是 int 和 float。 + +```objc ++(float) addTwoNumber:(NSNumber *)num1 and:(NSNumber *)num2{ + float result = [num1 floatValue]+[num2 floatValue]; + return result; +} +``` + +目前参数和返回值支持以下类型: + +- `int` +- `float` +- `bool` +- `string` + +其余的类型暂时不支持。 + +如果对如何在项目中新增 `Objective-C` 文件不熟悉,可参考 [原生平台二次开发指南](native-secondary-development.md)。 + +## Objective-C 执行 JavaScript 代码 + +在 Cocos Creator 项目中,我们也可以通过 `evalString` 方法,在 C++ 或者 Objective-C 中执行 JavaScript 代码。 + +调用示例如下: + +```c++ +#include "application/ApplicationManager.h" +#include "cocos/bindings/jswrapper/SeApi.h" + +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString(script.c_str()); +}); +``` + +> **注意**:除非确定当前线程是 **主线程**,否则都需要使用 `performFunctionInCocosThread` 方法将函数分发到主线程中去执行。 + +### 调用全局函数 + +我们可以在脚本中通过如下代码新增一个全局函数: + +```js +window.callByNative = function(){ + //to do +} +``` + +> `window` 是 Cocos 引擎脚本环境中的全局对象,如果要让一个变量、函数、对象或者类全局可见,需要将它作为 `window` 的属性。可以使用 `window.变量名` 或者 `变量名` 进行访问。 + +然后像下面这样调用: + +```c++ +#include "application/ApplicationManager.h" +#include "cocos/bindings/jswrapper/SeApi.h" + +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("window.callByNative()"); +}); +``` + +或者: + +```c++ +#include "application/ApplicationManager.h" +#include "cocos/bindings/jswrapper/SeApi.h" + +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("callByNative()"); +}); +``` + +### 调用类的静态函数 + +假如在 TypeScript 脚本中有一个类具有如下静态函数: + +```ts +export class NativeAPI{ + public static callByNative(){ + //to do + } +} +// 将 NativeAPI 注册为全局类,否则无法在 OC 中被调用 +window.NativeAPI = NativeAPI; +``` + +我们可以像这样调用: + +```c++ +#include "application/ApplicationManager.h" +#include "cocos/bindings/jswrapper/SeApi.h" + +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("NativeAPI.callByNative()"); +}); +``` + +### 调用单例函数 + +如果脚本代码中,有实现可以全局访问的单例对象 + +```ts +export class NativeAPIMgr{ + private static _inst:NativeAPIMgr; + + public static get inst():NativeAPIMgr{ + if(!this._inst){ + this._inst = new NativeAPIMgr(); + } + return this._inst; + } + + public static callByNative(){ + //to do + } +} + +// 将 NativeAPIMgr 注册为全局类,否则无法在 OC 中被调用 +window.NativeAPIMgr = NativeAPIMgr; +``` + +我们可以像下面这样调用: + +```c++ +#include "application/ApplicationManager.h" +#include "cocos/bindings/jswrapper/SeApi.h" + +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("NativeAPIMgr.inst.callByNative()"); +}); +``` + +### 参数传递 + +以上几种 OC 调用JS的方式,均支持参数传递,但参数只支持 string, number 和 bool 三种基础类型。 + +我们以全局函数为例: + +```js +window.callByNative = function(a:string, b:number, c:bool){ + //to do +} +``` + +可像这样调用: + +```c++ +#include "application/ApplicationManager.h" +#include "cocos/bindings/jswrapper/SeApi.h" + +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + se::ScriptEngine::getInstance()->evalString("window.callByNative('test',1,true)"); +}); +``` + +## 线程安全 + +可以看到,上面的代码中,使用了 `CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread`。这是为了代码在执行时处于正确的线程,详情请参考:[线程安全](./thread-safety.md)。 diff --git a/versions/4.0/zh/advanced-topics/profiler.md b/versions/4.0/zh/advanced-topics/profiler.md new file mode 100644 index 0000000000..c60e789c99 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/profiler.md @@ -0,0 +1,101 @@ +# 性能剖析器(Profiler) + +性能剖析器是用于性能分析和统计的工具,目前仅限原生平台。 + +## 默认统计数据 + +- 性能剖析器的效果展示如图: + + ![profiler](native-profiler/profiler.png) + +- 与左下角旧版的 `Profiler` 不同,新版的 `Profiler` 提供更详细的游戏性能与内存统计数据,并支持扩展,开发者可自由添加关心的性能及内存数据,默认的数据如下: + + 1. 核心数据统计 `CoreStats` ,包括: + - 帧数 + - 每帧渲染时间 + - GFX 后端 + - 是否开启多线程 + - 是否开启遮挡查询 + - 是否开启阴影贴图 + - 屏幕分辨率 + + 2. 对象个数统计 `ObjectStats` ,包括: + - 渲染调用次数 + - 实例化个数 + - 三角面个数 + - 2D渲染批次个数 + - 渲染模型个数 + - 摄像机个数 + + 3. 内存使用统计 `MemoryStats` ,包括: + - DebugRenderer 的顶点缓冲区大小 + - 原生字体内存大小 + - 贴图占用的显存 + - Buffer占用的显存 + - GeometryRenderer 顶点缓冲区大小 + + 4. 性能数据统计 `PerformanceStats` ,显示逻辑线程调用堆栈,包含每个剖析代码段的: + - 每帧执行总时间 + - 每帧单次执行最大时间 + - 每帧执行总次数 + - 每帧单次执行平均时间 + - 历史执行总时间 + - 历史单次执行最大时间 + - 历史执行总次数 + - 历史单次执行平均时间 + +## 使用方式 + +- 在编辑器的主菜单:**项目** -> **项目设置** -> **功能裁剪** 里勾选 **调试文字渲染器**,此选项默认关闭,需要打开才能显示调试信息: + + ![enable profiler](native-profiler/enable-profiler.png) + +- 在 `native/cocos/base/Config.h` 中把 `CC_USE_PROFILER` 宏定义改为 `1`,等性能及内存优化完成后,再改回 `0`,此时性能剖析器完全关闭,不会对代码造成任何副作用: + + ```c++ + #ifndef CC_USE_PROFILER + #define CC_USE_PROFILER 0 + #endif + ``` + +- 如果想要添加 `ObjectStats` 的统计信息, 比如统计每帧渲染模型的个数(需在每帧调用的函数如update中): + + 下述宏都定义在 [native/cocos/profiler/Profiler.h](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/profiler/Profiler.h) 内。开发者可根据需求使用。 + + ```c++ + void RenderScene::update(uint32_t stamp) { + ... + CC_PROFILE_OBJECT_UPDATE(Models, _models.size()); + } + ``` + + - `CC_PROFILE_OBJECT_UPDATE` 用于更新统计数量 + - `CC_PROFILE_OBJECT_INC` 用于递增统计数量 + - `CC_PROFILE_OBJECT_DEC` 用于递减统计数量 + +- 如果想要添加 `MemoryStats` 的统计信息, 比如统计 `GeometryRenderer` 顶点缓冲区的内存使用量: + + ```c++ + void GeometryVertexBuffer::init(gfx::Device *device, + uint32_t maxVertices, const gfx::AttributeList &attributes) { + ... + CC_PROFILE_MEMORY_INC(GeometryVertexBuffer, static_cast(_maxVertices * sizeof(T))); + } + ``` + + - `CC_PROFILE_MEMORY_UPDATE` 用于更新内存使用量(字节) + - `CC_PROFILE_MEMORY_INC` 用于递增内存使用量(字节) + - `CC_PROFILE_MEMORY_DEC` 用于递减内存使用量(字节) + +- 如果想要添加 `PerformanceStats` 的统计信息, 比如统计 `ForwardPipeline::render` 函数的执行时间(毫秒): + + ```c++ + void ForwardPipeline::render(const ccstd::vector &cameras) { + CC_PROFILE(ForwardPipelineRender); + ... + } + ``` + +- 通过以上修改后,编译,运行,屏幕上就可以看到新增的统计数据: + + ![add-stats](native-profiler/add-stats.png) diff --git a/versions/4.0/zh/advanced-topics/sebind/devtool-test.png b/versions/4.0/zh/advanced-topics/sebind/devtool-test.png new file mode 100644 index 0000000000..20f59eb095 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/sebind/devtool-test.png differ diff --git a/versions/4.0/zh/advanced-topics/sebind/thisobject_placeholder.png b/versions/4.0/zh/advanced-topics/sebind/thisobject_placeholder.png new file mode 100644 index 0000000000..b1a11679cd Binary files /dev/null and b/versions/4.0/zh/advanced-topics/sebind/thisobject_placeholder.png differ diff --git a/versions/4.0/zh/advanced-topics/supported-versions.md b/versions/4.0/zh/advanced-topics/supported-versions.md new file mode 100644 index 0000000000..340445ab04 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/supported-versions.md @@ -0,0 +1,22 @@ +# 特性与系统版本 + +## 功能模块与最低支持的系统版本 + +| 功能模块 | Android | iOS | +| :-- | :--- | :-- | +| VULKAN 渲染后端 | API Level 24(7.0)| - | +| Google Play Instant | API Level 23(6.0)| - | +| TBB 任务调度系统 | API Level 21(5.0)| 10.0 | +| TaskFlow 任务调度系统 | API Level 18(4.3)| 12.0 | +| 延迟渲染管线 | API Level 21(5.0)| 10.0 | + +## C++ 与最低支持的系统版本 + +| Cocos Creator | C++ | Android | iOS | +| :-- | :--- | :-- | :-- | +| 3.0 | C++14 | API Level 18(4.3) | 10.0 | +| 3.1~3.3.1 | C++17 | API Level 21(5.0) | 11.0 | +| 3.3.2 ~ 3.5.1 | C++14 | API Level 18 (4.3) | 10.0 | +| 3.6.0 以上 | C++17 | API Level 21(5.0) | 11.0 + +> **注意**:启用 TaskFlow 任务调度系统,会自动开启 C++17 以支持编译,并且 TaskFlow 需要的特性需要 iOS 12+ 的支持。 \ No newline at end of file diff --git a/versions/4.0/zh/advanced-topics/thread-safety.md b/versions/4.0/zh/advanced-topics/thread-safety.md new file mode 100644 index 0000000000..c454f91d46 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/thread-safety.md @@ -0,0 +1,78 @@ +# 线程安全 + +## 线程安全 + +在 Cocos Creator 发布的原生应用中,至少有两个线程:**GL 线程** 和 **原生系统的 UI 线程**。 +- **GL 线程**:执行 Cocos 引擎的渲染相关代码和 JavaScript 脚本代码 +- **UI 线程**:平台的原生 UI 创建、响应和更新 + +所以,我们需要注意以下两个问题: +1. 当 UI 线程中的代码要调用 GL 线程中的代码时,需要处理线程安全问题 +2. 当 GL 线程要调用 UI 线程中的代码时,需要处理线程安全问题 + +接下来,我们看看不同情况下处理线程安全的方法。 + +## 在 UI 线程中执行 + +当我们在 Android 平台编写的 Java 方法有 UI 相关的操作,在被 GL 线程中的代码调用时,都需要利用 `app.runOnUiThread` 方法使代码在 UI 线程中执行,从而确保线程安全。 + +我们以弹出一个 Android 的 Alert 对话框为例。 + +```java +public class AppActivity extends CocosActivity { + + private static AppActivity app = null; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + app = this; + } + + public static void showAlertDialog(final String title,final String message) { + app.runOnUiThread(new Runnable() { + @Override + public void run() { + AlertDialog alertDialog = new AlertDialog.Builder(app).create(); + alertDialog.setTitle(title); + alertDialog.setMessage(message); + alertDialog.setIcon(R.drawable.icon); + alertDialog.show(); + } + }); + } +} +``` + +## 在 GL 线程中执行 + +在 UI 线程中的代码要调用 GL 线程中的代码时,需要使用 `CocosHelper.runOnGameThread` 来确保线程安全。 + +我们以按钮事件响应为例: + +```java +alertDialog.setButton("OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // 一定要在 GL 线程中执行 + CocosHelper.runOnGameThread(new Runnable() { + @Override + public void run() { + CocosJavascriptJavaBridge.evalString("cc.log(\"Javascript Java bridge!\")"); + } + }); + } +}); +``` + +## Objective-C / C++ + +如果写的 Objective-C 或者 C++ 代码想要确保线程安全,可以通过 `CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread` 方法让代码在 GL 线程中执行。 + +```c++ +#include "application/ApplicationManager.h" + +CC_CURRENT_ENGINE()->getScheduler()->performFunctionInCocosThread([=](){ + //TO DO +}); +``` + +> **注意:** 这是 C++ 方法,如果想要在 Objective-C 代码中调用,需要 Objective-C 代码文件后缀为 *.mm。 diff --git a/versions/4.0/zh/advanced-topics/wasm-asm-create.md b/versions/4.0/zh/advanced-topics/wasm-asm-create.md new file mode 100644 index 0000000000..fa0de3cbbd --- /dev/null +++ b/versions/4.0/zh/advanced-topics/wasm-asm-create.md @@ -0,0 +1,110 @@ +# 使用 Emscripten 将原生代码转化为 Wasm/Asm 文件 + +## 在 Windows 电脑中安装 Emscripten + +1. **下载 emsdk:** 通过在电脑中执行 git 命令: `git clone https://github.com/emscripten-core/emsdk.git` 下载 emsdk 源码。 + + > **注意:** 执行 git 命令需要电脑中安装 git 软件,未安装的用户请前往 [git 官网](https://git-scm.com/) 下载并安装。 + +2. **选择一个 emsdk 版本:** 目前 emsdk 的最新版本为 4.0.10,经过测试存在意外的兼容性问题。建议用户选择使用 3.1.41 版本,此为引擎团队编译 Spine 模块 Wasm 文件时使用的版本。 + +3. **安装 emsdk 依赖的运行库:** 在 emsdk 源码根目录执行命令行: `./emsdk install 3.1.41`,执行之后将自动安装 emsdk 依赖的运行库。 + + > **注意:** + > * 切换 emsdk 版本时,不仅需要切换源码版本,也需要重新安装 emsdk 依赖的运行库。在 `./emsdk install` 后面加入版本号的字符串即可。 + > + > * 安装最新版本的 emsdk 依赖运行库则在 `./emsdk install` 后面加入 `latest` 即可。 + > + > * 安装依赖库的过程中如果出现下载失败的情况,可以尝试打开 VPN 加速。 + +4. **配置 emsdk 运行库的系统环境变量:** 在 emsdk 源码根目录执行命令行: `./emsdk_env.bat` 将自动配置 emsdk 运行库所需使用的系统环境变量。 + +5. **检查 emsdk 运行库的系统环境变量:** 在 emsdk 源码目录中打开 upstream/emscripten/.emscripten 文件,检查 LLVM_ROOT 和 BINARYEN_ROOT 是否为如下参考地址: + + ```python + LLVM_ROOT = 'D:\\git\\emsdk\\upstream\\bin' + BINARYEN_ROOT = 'D:\\git\\emsdk\\upstream' + ``` + + 如果不是,则手动修改为能够读取到本地 llvm 工具和 binaryen 工具的地址 + +6. **激活 emsdk 运行环境:** 在 emsdk 源码根目录执行命令行: `./emsdk activate --system` 激活 emsdk 运行环境,之后用户可以在命令行窗口使用 `emcc` 命令。 + + > **注意:** + > * 加入 `--system` 可以让 emsdk 的运行环境在所有命令行窗口生效,否则每次打开新的命令行窗口时都需要执行一次 `./emsdk activate`。 + > + > * 在旧版本中通常使用 `./emsdk activate --global` 进行全局激活 + +7. **验证 emsdk 安装成功:** 在命令行窗口中执行命令行: `emcc --version`,如果出现信息为 `emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.41` 则说明安装成功。 + +## 生成 Wasm 文件 + +1. **创建 C 语言代码文件:** 在任意文件夹中创建一个 Cocos.c 的 C 语言代码文件,填入以下代码信息: + + ```C + #include + + int main() { + printf("Hello World\n"); + } + ``` + +2. **将 C 语言代码文件转化为 Wasm/Asm 文件:** 在 Cocos.c 文件所在根目录(假设 Cocos.c 文件放在 Windows 系统的 D 盘根目录)执行命令行 `emcc D:\Cocos.c -s WASM=1`,执行完成后将在命令行所执行的目录中生成 wasm 文件 + + > **注意:** + > + > * 命令行中添加 `-s WASM=1` 为指定生成 wasm 文件,如果添加为 `-s WASM=0` 则生成 Asm 文件,用于在不支持 WebAssembly 平台中加载。 + > + > * 生成 Asm 文件时如果原生语言代码文件中需要初始化的内存数据较大,则还会在目录中生成 .mem 后缀的文件,此文件也需要被加载。 + > + > * 更多的命令行参数请执行 `emcc --help` 查看,或者到外部网站搜索相关知识。 + +## 加载 Wasm/Asm 文件 + +参考 [自定义加载 Wasm/Asm 文件与模块](./wasm-asm-load.md) 中的内容加载 Wasm/Asm 文件。 + +## 编译 Cocos 引擎的 Spine Wasm(适用于 3.8.0 ~ 3.8.4 版本引擎) + +1. **安装 CMake:** 前往 [CMake 官网](https://cmake.org/) 下载并安装 + +2. **配置 CMake 系统环境变量:** 在系统环境变量中添加 `C:\Program Files\CMake\bin`。 + +3. **安装 MinGW:** 前往 [MinGW 发布页面](https://github.com/niXman/mingw-builds-binaries/releases),下载文件名包含 `win32-seh-ucrt` 的包,之后解压到电脑中。 + +4. **配置 MinGW 系统环境变量:** 在系统环境变量中添加 `C:\mingw64\bin`。 + +5. **进入 Spine Wasm 的 C++ 代码文件夹:** 打开命令行工具,使用 cd 命令进入 Cocos 引擎中用于生成 Spine Wasm 的 C++ 代码文件夹 + + ```cmd + cd D:\CocosCreator\Creator\3.8.3\resources\resources\3d\engine-native\cocos\editor-support\spine-wasm + ``` + + > **注意:** 此为参考路径,实际使用时请修改为设备中的真实路径 + +6. **生成 CMakeFiles:** 输入命令行 `emcmake cmake -B ./build` 生成 CMakeFiles 并存放在 build 文件夹中。 + +7. **进入 build 文件夹:** 使用 cd 命令进入 build 文件夹:`cd ./build`。 + +8. **生成 Spine Wasm 文件:** 输入命令行 `emmake make`,之后就可以在 build 文件夹中看到 spine.js 和 spine.wasm 生成。 + + ![spine-wasm-create](./wasm-asm-create/spine-wasm-create.png) + +9. **修改 CMakeLists.txt 中的 emcc 命令**: CMakeLists.txt 文件位于 spine-wasm 文件夹中,打开它并修改 `emcc` 命令参数即可改变最终生成的产物。 + + > **注意:** + > + > * 若需编译 Debug 版,将 CMakeLists.txt 中的 CMAKE_BUILD_TYPE 设置为 Debug,取消 emcc 命令中的 -O3 优化选项,增加 -g 编译选项。 + +10. **更新 Cocos 引擎自带的 Spine Wasm:** 将生成后的 spine Wasm 与 spine.js 拷贝到相对路径 `engine-native/external/emscripten/spine` 所在的文件夹下,将 spine.js 文件改名为 spine.wasm.js 并替换旧文件。 + + > **注意:** 切换 debug 或 release 模式后需要重启编辑器。 + +11. **更新编辑器缓存的 spine.wasm.js:** 删除编辑器位于相对路径 `resources\3d\engine\bin\.cache` 文件夹下的 spine.wasm.js,删除之后重启编辑器。 + +## 编译 Cocos 引擎的 Spine Wasm(适用于 3.8.5 及以上版本引擎) + +参考 [Spine WASM Compilation Guide](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/editor-support/spine-wasm/README.md) 中的内容。 + +## 调试 Debug 版本的 Spine Wasm + +使用 Google 浏览器并安装 [C/C++ DevTools Support 拓展插件](https://chromewebstore.google.com/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb),安装完成后运行后在谷歌浏览器控制台中可以打开 Spine Wasm C++ 相关的代码文件并进行调试。 \ No newline at end of file diff --git a/versions/4.0/zh/advanced-topics/wasm-asm-create/spine-wasm-create.png b/versions/4.0/zh/advanced-topics/wasm-asm-create/spine-wasm-create.png new file mode 100644 index 0000000000..73d01a2814 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/wasm-asm-create/spine-wasm-create.png differ diff --git a/versions/4.0/zh/advanced-topics/wasm-asm-load.md b/versions/4.0/zh/advanced-topics/wasm-asm-load.md new file mode 100644 index 0000000000..30c08739cb --- /dev/null +++ b/versions/4.0/zh/advanced-topics/wasm-asm-load.md @@ -0,0 +1,266 @@ +# 自定义加载 Wasm/Asm 文件与模块 + +CocosCreator 引擎对 Spine、Box2D 等模块提供了手动加载 Wasm/Asm 的接口,但是用户如果想加载自定义的 Wasm/Asm 文件与模块,则需要自己在项目中实现加载逻辑。 + +> #### 注意:本文不涉及 Wasm/Asm 文件如何创建,仅介绍如何在 CocosCreator 中加载。 + +1. 在加载器与解析器中添加 Wasm/Asm 文件的支持,请将以下示例代码添加到脚本的 class 类之外,或者翻译为 js 作为 js 插件放到项目中。 + + ```js + //判断当前平台是否支持加载 wasm 文件,Cocos 引擎目前暂不支持在 iOS 平台加载 wasm 文件。 + if (sys.hasFeature(sys.Feature.WASM) || (sys.isNative && sys.os !== sys.OS.IOS)) { + if (sys.isNative) { + //@ts-ignore + assetManager.downloader.register('.wasm', assetManager.downloader._downloaders[".bin"]); + //@ts-ignore + assetManager.parser.register('.wasm', assetManager.parser._parsers[".bin"]); + } + else if (sys.isBrowser || sys.platform === sys.Platform.WECHAT_GAME) { + //@ts-ignore + assetManager.downloader.register('.wasm', assetManager.downloader._downloadArrayBuffer); + //@ts-ignore + assetManager.downloader.register('.mem', assetManager.downloader._downloadArrayBuffer); + } + } else { + if (sys.isNative) { + //@ts-ignore + assetManager.downloader.register('.mem', assetManager.downloader._downloaders[".bin"]); + //@ts-ignore + assetManager.parser.register('.mem', assetManager.parser._parsers[".bin"]); + } + } + ``` + +* wasm、mem 文件都是二进制文件,所以将引擎内置的二进制文件加载函数作为这两个格式资源的下载与解析函数。 + +2. 修改项目中的 tsconfig.json 文件,添加 `CommonJS` 模块支持,以便动态 import js 脚本。 + + ```json + { + /* Base configuration. Do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + /* Add your custom configuration here. */ + "compilerOptions": { + "strict": false, + "module": "CommonJS" + } + } + ``` + +3. 将 Wasm/Asm 文件导入到 CocosCreator 编辑器的资源管理器中,并将 .wasm 文件与 .mem 文件放到同一个 bundle 文件夹中。加载示例的文件分布结构如下,供开发者参考: + + ![](./wasm-asm-load/wasm-asm-load-01.png) + + ![](./wasm-asm-load/wasm-asm-load-02.png) + +4. 添加 wasmOrAsmLoadTest 函数,导入 Wasm/Asm 模块脚本。添加成功与失败回调,在成功回调里加载 Wasm/Asm 二进制文件。 + + > #### 注意:这里 loadWasmOrAsm 函数接收的第三个参数是 Wasm/Asm 文件在编辑器内的资源 Uuid,请开发者更换为自己项目内的资源 Uuid。 + + ```js + wasmOrAsmLoadTest () { + if (sys.hasFeature(sys.Feature.WASM) || (sys.isNative && sys.os !== sys.OS.IOS)) { + import('./effekseer.js').then(({ default: wasmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer", "44cacb3c-e901-455d-b3e1-1c38a69718e1").then((wasmFile)=>{ + //TODO: 初始化 wasm 文件 + + }, (err)=> { + console.error("wasm load failed", err); + }) + }); + } else { + import('./effekseer.asm.js').then(({ default: asmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer.asm", "3400003e-dc3c-43c1-8757-3e082429125a").then((asmFile)=> { + //TODO: 初始化 asm 文件 + + }, (err)=> { + console.error("asm load failed", err); + }); + }); + } + } + ``` + +5. 添加 loadWasmOrAsm 方法,该方法用于加载 Wasm/Asm 二进制文件。 + + ```js + loadWasmOrAsm (bundleName, fileName, editorWasmOrAsmUuid): Promise { + return new Promise((resolve, reject) => { + if (EDITOR) { + //编辑器内通过 uuid 加载资源比较便捷,无法通过 bundle 加载 + if (editorWasmOrAsmUuid) { + assetManager.loadAny(editorWasmOrAsmUuid, (err, file: Asset)=> { + if (!err) { + //@ts-ignore + resolve(file); + } else { + reject(err); + } + }) + } + } else { + if (bundleName && fileName) { + assetManager.loadBundle(bundleName, (err, bundle)=>{ + if (!err) { + bundle.load(fileName, Asset, (err2: any, file: Asset) => { + if (!err2) { + //@ts-ignore + resolve(file); + } else { + reject(err2); + } + }) + } else { + reject(err); + } + }) + } + } + }) + } + ``` + +6. 编写 Wasm 初始化代码,在 Wasm 二进制文件加载完成后,调用 instantiateWasm 函数完成初始化。 + + ```js + initWasm (wasmFactory, file): Promise { + var self = this; + return new Promise((resolve, reject) => { + wasmFactory({ + instantiateWasm (importObject, receiveInstance) { + self.instantiateWasm(file, importObject).then((result) => { + receiveInstance(result.instance, result.module); + }).catch((err) => reject(err)); + } + }).then((instance: any)=>{ + resolve(instance); + }).catch((err) => reject(err)); + }); + } + + instantiateWasm (wasmFile: Asset, importObject: WebAssembly.Imports): Promise { + if (sys.isBrowser || sys.isNative) { + //@ts-ignore + return WebAssembly.instantiate(wasmFile._file, importObject); + } else if (sys.platform === sys.Platform.WECHAT_GAME){ + //@ts-ignore + return CCWebAssembly.instantiate(wasmFile.nativeUrl, importObject) + } + } + ``` + +7. 在 wasmOrAsmLoadTest 函数中添加 initWasm 函数的调用,完成 Wasm 的加载。 + + ```js + wasmOrAsmLoadTest () { + if (sys.hasFeature(sys.Feature.WASM) || (sys.isNative && sys.os !== sys.OS.IOS)) { + import('./effekseer.js').then(({ default: wasmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer", "44cacb3c-e901-455d-b3e1-1c38a69718e1").then((wasmFile)=>{ + this.initWasm(wasmFactory, wasmFile).then((instance: any)=> { + Effekseer = instance; + Effekseer._myFunction(); + console.log("effekseer wasm module inited", Effekseer); + }, (err) => { + console.error("effekseer wasm module init failed", err); + }); + + }, (err)=> { + console.error("wasm load failed", err); + }) + }); + } else { + import('./effekseer.asm.js').then(({ default: asmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer.asm", "3400003e-dc3c-43c1-8757-3e082429125a").then((asmFile)=> { + //TODO: 初始化 asm 文件 + + }, (err)=> { + console.error("asm load failed", err); + }); + }); + } + } + ``` + +* Wasm 加载完成后,在本示例中应该在浏览器控制台看到如下输出: + + ![](./wasm-asm-load/wasm-asm-load-03.png) + + 8.编写 asm 初始化代码。首先添加 asm 模块的内存大小常量,之后添加 initAsm 函数用于初始化 Asm 模块。 + + ```js + //添加 asm 模块的内存大小常量,在初始化 asm 模块时使用。 + const PAGESIZE = 65536; // 64KiB + + // How many pages of the wasm memory + // TODO: let this can be canfiguable by user. + const PAGECOUNT = 32 * 16; + + // How mush memory size of the wasm memory + const MEMORYSIZE = PAGESIZE * PAGECOUNT; // 32 MiB + ``` + + ```js + initAsm (asmFactory, file): Promise { + const asmMemory: any = {}; + asmMemory.buffer = new ArrayBuffer(MEMORYSIZE); + const module = { + asmMemory, + memoryInitializerRequest: { + //@ts-ignore + response: file._file, + status: 200, + } as Partial, + }; + return asmFactory(module); + } + ``` + +9. 在 wasmOrAsmLoadTest 函数中添加 initAsm 函数的调用,完成 Asm 的加载。 + + ```js + wasmOrAsmLoadTest () { + if (sys.hasFeature(sys.Feature.WASM) || (sys.isNative && sys.os !== sys.OS.IOS)) { + import('./effekseer.js').then(({ default: wasmFactory })=> { + this.loadWasmOrAsm("wasmFiles", "effekseer", "44cacb3c-e901-455d-b3e1-1c38a69718e1").then((wasmFile)=>{ + this.initWasm(wasmFactory, wasmFile).then((instance: any)=> { + Effekseer = instance; + Effekseer._myFunction(); + console.log("effekseer wasm module inited", Effekseer); + }, (err) => { + console.error("effekseer wasm module init failed", err); + }); + + }, (err)=> { + console.error("wasm load failed", err); + }) + }); + } else { + import('./effekseer.asm.js').then(({ default: asmFactory })=> { + + this.loadWasmOrAsm("wasmFiles", "effekseer.asm", "3400003e-dc3c-43c1-8757-3e082429125a").then((asmFile)=> { + this.initAsm(asmFactory, asmFile).then((instance: any)=>{ + Effekseer = instance; + Effekseer._myFunction(); + console.log("effekseer asm module inited", Effekseer); + }, (err) => { + console.error("effekseer asm module init failed", err); + }); + + }, (err)=> { + console.error("asm load failed", err); + }); + }); + } + } + ``` + +* Asm 加载完成后,在本示例中应该在浏览器控制台看到如下输出: + + ![](./wasm-asm-load/wasm-asm-load-04.png) + +### 示例工程仓库地址 + +[GitHub 仓库地址](https://github.com/cocos/cocos-awesome-tech-solutions/tree/3.8.x-release/demo/Creator3.8.6_WasmOrAsmLoad) + +[Gitee 国内仓库地址](https://gitee.com/zzf2019/cocos-awesome-tech-solutions/tree/3.8.x-release/demo/Creator3.8.6_WasmOrAsmLoad) \ No newline at end of file diff --git a/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-01.png b/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-01.png new file mode 100644 index 0000000000..b59ed5f43e Binary files /dev/null and b/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-01.png differ diff --git a/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-02.png b/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-02.png new file mode 100644 index 0000000000..279f1576b4 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-02.png differ diff --git a/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-03.png b/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-03.png new file mode 100644 index 0000000000..2d84029154 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-03.png differ diff --git a/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-04.png b/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-04.png new file mode 100644 index 0000000000..265cf8b87d Binary files /dev/null and b/versions/4.0/zh/advanced-topics/wasm-asm-load/wasm-asm-load-04.png differ diff --git a/versions/4.0/zh/advanced-topics/web-socket/http.png b/versions/4.0/zh/advanced-topics/web-socket/http.png new file mode 100644 index 0000000000..add0572dd3 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/web-socket/http.png differ diff --git a/versions/4.0/zh/advanced-topics/web-socket/web-socket.png b/versions/4.0/zh/advanced-topics/web-socket/web-socket.png new file mode 100644 index 0000000000..5d643cb619 Binary files /dev/null and b/versions/4.0/zh/advanced-topics/web-socket/web-socket.png differ diff --git a/versions/4.0/zh/advanced-topics/websocket-introduction.md b/versions/4.0/zh/advanced-topics/websocket-introduction.md new file mode 100644 index 0000000000..c4dce92d03 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/websocket-introduction.md @@ -0,0 +1,24 @@ +# WebSocket 简介 + +通常在开发网络应用或者游戏时,出于数据安全或其他目标,需要将用户的数据存放在网络后端服务端中,而浏览器一般来说只支持 HTTP 协议。 + +HTTP(HyperText Transfer Protocol)协议[^1]是基于 TCP 协议的应用层传输协议,使用方法是通过客户端(网页浏览器或通过 [API](http.md))向服务端请求数据,服务端将数据存储在 HTTP 协议的响应内,返回给客户端。 + +![http](web-socket/http.png) + +在 HTTP 协议初期(1.1 之前的版本中)一个请求对应一次网络连接,也就是说每次发起 HTTP 请求时,客户端和服务端都需要重新建立 TCP 连接,这无疑是对资源的浪费。而在 HTTP/1.1 之后,可以通过 keep-alive 将 HTTP 协议进行合并,从而将多个请求(Request)和响应(Respnose)进行合并。但此时的 HTTP 协议仍然是一个请求对应一个响应。 + +通常情况下,这样的通信方式是足够的,可以满足服务端被动接受客户端请求返回数据。而在特定的情况下,如服务端需将数据推送给客户端时,就需要客户端进行轮询请求,即客户端通过定时或者其他循环的方式不断的向服务端请求数据。由于 HTTP 协议本身携带有较大的 HTTP 头以及不断的轮询行为会造成客户端、服务端资源以及带宽的浪费。 + +为了解决这个问题,浏览器标准标准委员会于 2008 年制定了 WebSocket 协议,在 2011 年时,WebSocket 成为浏览器标准,这也意味着所有的浏览器都支持 WebSocket。 + +HTTP/1.1 协议可以通过升级将连接升级为 Websocket 协议[^2],完成后客户端和服务端都可向对方主动推送需要的数据。这样就解决了客户端与服务端的实时交互的问题。 + +![web-socket](web-socket/web-socket.png) + +WebSocket 传输时可以选择使用二进制(Binary)或 字符串(String)以此来提高私有协议的特定制性和安全性。 + +在 Cocos Creator 内如果要使用 Websocket 可以参考 [使用 WebSocket 服务端](websocket-server.md)以及 [WebSocket 客户端](websocket.md)。 + +[^1]: [The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455) +[^2]: [HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) diff --git a/versions/4.0/zh/advanced-topics/websocket-server.md b/versions/4.0/zh/advanced-topics/websocket-server.md new file mode 100644 index 0000000000..166171de31 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/websocket-server.md @@ -0,0 +1,102 @@ +# 使用 WebSocket 服务器 + +开发者可以在游戏进程中启动一个 **WebSocket 服务器**,提供 **RPC 接口**。通过完善和调用这些 **RPC 接口**,开发者能够对游戏进程内部状态进行监控,增加对游戏进程状态的管理能力。 + +## 如何启用 + +**WebSocket 服务器** 默认是剔除的。若要启用,需要在编辑器顶部菜单栏中 **项目设置 -> 功能裁剪** 中勾选**WebSocket Server** 配置。 + +## 如何调用 WebSocket 服务器接口 + +可参考下方实例代码: + +```ts +// 在原生平台的 Release 模式下或者在 Web /微信小游戏等平台中,WebSocketServer 可能没有定义 +if (typeof WebSocketServer === "undefined") { + console.error("WebSocketServer is not enabled!"); + return; +} + +let s = new WebSocketServer(); +s.onconnection = function (conn) { + conn.onmessage = function (data) { + conn.send(data, (err) => {}); + } + conn.onclose = function () { + console.log("connection gone!"); + }; +}; + +s.onclose = function () { + console.log("server is closed!"); +} +s.listen(8080, (err) => { + if (!err); + console.log("server booted!"); +}); +``` + +## API + +接口定义如下: + +```typescript +/** + * 服务器对象 + */ +class WebSocketServer { + /** + * 关闭服务 + */ + close(cb?: WsCallback): void; + /** + * 监听并启动服务 + */ + listen(port: number, cb?: WsCallback): void; + /** + * 处理新的请求 + */ + set onconnection(cb: (client: WebSocketServerConnection) => void); + /** + * 设置服务器关闭回调 + */ + set onclose(cb: WsCallback); + /** + * 获取所有的连接对象 + */ + get connections(): WebSocketServerConnection[]; +} + +/** + * 服务器中客户端的连接对象 + */ +class WebSocketServerConnection { + /** + * 关闭连接 + */ + close(code?: number, reason?: string): void; + /** + * 发送数据 + */ + send(data: string|ArrayBuffer, cb?: WsCallback): void; + + set ontext(cb: (data: string) => void); + set onbinary(cb: (data: ArrayBuffer) => void); + set onmessage(cb: (data: string|ArrayBuffer) => void); + set onconnect(cb: () => void;); + set onclose(cb: WsCallback); + set onerror(cb: WsCallback); + + get readyState(): number; +} + +interface WsCallback { + (err?: string): void; +} +``` + +> **注意**: `ondata` 回调已经在 v3.7.0 版本废弃, 请使用 `onmessage` 代替. + +## 参考链接 + +接口设计参考了 [nodejs-websocket](https://www.npmjs.com/package/nodejs-websocket#server)。 diff --git a/versions/4.0/zh/advanced-topics/websocket.md b/versions/4.0/zh/advanced-topics/websocket.md new file mode 100644 index 0000000000..0d2c958308 --- /dev/null +++ b/versions/4.0/zh/advanced-topics/websocket.md @@ -0,0 +1,15 @@ +# WebSocket 客户端 + +原生环境支持 [Web 标准的](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket) WebSocket 接口。 + +## 差异 + +在 v3.5 版本之前,Android 和 Windows 上的 WebSocket API 使用 [libwebsockets](https://github.com/warmcat/libwebsockets) 实现。需要开发者通过第三个参数指定 CA 文件路径: + +```ts +this.wsInstance = new WebSocket('wss://echo.websocket.org', [], caURL); +``` + +详情请参考 [示例代码](https://github.com/cocos/cocos-test-projects/blob/07f5671e18ef3ed4494d8cba6c2f9499766467a6/assets/cases/network/NetworkCtrl.ts#L113-L120)。可访问 [下载 CA 证书](https://curl.se/docs/caextract.html) 下载对应的证书。 + +在 v3.5 版本之后, Android 上不再强制要求提供证书。 diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve.md b/versions/4.0/zh/animation/animation-auxiliary-curve.md new file mode 100644 index 0000000000..355ab7ce29 --- /dev/null +++ b/versions/4.0/zh/animation/animation-auxiliary-curve.md @@ -0,0 +1,49 @@ +# 辅助曲线编辑视图 + +辅助曲线是 Cocos Creator v3.8 新增的动画辅助功能,您可以阅读 [辅助曲线](./marionette/procedural-animation/auxiliary-curve/index.md) 来了解辅助曲线的基础概念。 + +如果要启用辅助曲线,请在 **偏好设置** 中的 **实验室** 分页中启用 **动画辅助曲线**。 + +![enable.png](./animation-auxiliary-curve/enable.png) + +本节将主要介绍如何在动画编辑器中使用辅助曲线。辅助曲线编辑器是曲线编辑器的一种,曲线编辑器的基础操作可参考 [曲线编辑器](./curve-editor.md)。 + +选中带有动画组件(Animation、SkeletalAnimation 或 AnimationController)的节点。点击下图中的按钮进入编辑模式。 + +![start-edit.png](./animation-auxiliary-curve/start-edit.png) + +下图中的 Auxiliary Curves 部分则为辅助曲线 + +![overview.png](./animation-auxiliary-curve/overview.png) + +## 添加和删除曲线 + +在动画编辑器中点击辅助曲线上的 “+” 号按钮,会在当前动画中添加一条辅助曲线。 + +![add-curve.png](./animation-auxiliary-curve/add-curve.png) + +添加后辅助曲线的名称可以修改。也可以在之后通过鼠标右键点击该辅助曲线,通过弹出菜单内的重命名菜单修改。 + +![add-curve.png](./animation-auxiliary-curve/menu.png) + +在弹出菜单中,点击移除菜单可以删除(Remove)当前选中的辅助曲线或者重命名曲线(Rename)。 + +## 新建关键帧 + +在辅助曲线编辑器的右侧点击鼠标右键,可以添加关键帧。 + +![create-key-frame.png](./animation-auxiliary-curve/create-key-frame.png) + +创建好关键帧后,在关键帧上点击右键的弹出菜单可以调整关键帧或插值的信息。 + +![keyframe-menu.png](./animation-auxiliary-curve/keyframe-menu.png) + +| 菜单 | 说明 | +| :--- | :-- | +| 删除 | 删除该关键帧 | +| 复制 | 辅助关键帧,复制后在点击其他关键帧或空白处可以粘贴该关键帧的信息 | +| 粘贴 | 将之前复制的关键帧复制到鼠标点击的位置或者替换选中的关键帧的信息 | +| 编辑 | 点击后弹出框内可以修改当前关键帧所在的时间和值
![keyframe-menu.png](./animation-auxiliary-curve/edit-pop.png) +| 切线设置为水平 | 点击后,曲线在该处的切线将切换为水平模式
![flat.png](./animation-auxiliary-curve/flat.png) +| 插值模式 | 请参考 [线性插值模式](./curve-editor.md#线性插值模式) | +| 切线权重模式 | 请参考 [切线权重模式](./curve-editor.md#切线权重模式) | diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/add-curve.png b/versions/4.0/zh/animation/animation-auxiliary-curve/add-curve.png new file mode 100644 index 0000000000..46af0aad67 Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/add-curve.png differ diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/create-key-frame.png b/versions/4.0/zh/animation/animation-auxiliary-curve/create-key-frame.png new file mode 100644 index 0000000000..c4fb841c31 Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/create-key-frame.png differ diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/edit-pop.png b/versions/4.0/zh/animation/animation-auxiliary-curve/edit-pop.png new file mode 100644 index 0000000000..80d1da9650 Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/edit-pop.png differ diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/enable.png b/versions/4.0/zh/animation/animation-auxiliary-curve/enable.png new file mode 100644 index 0000000000..312fa506ac Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/enable.png differ diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/flat.png b/versions/4.0/zh/animation/animation-auxiliary-curve/flat.png new file mode 100644 index 0000000000..1981efb9ff Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/flat.png differ diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/keyframe-menu.png b/versions/4.0/zh/animation/animation-auxiliary-curve/keyframe-menu.png new file mode 100644 index 0000000000..fd35e3cf65 Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/keyframe-menu.png differ diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/menu.png b/versions/4.0/zh/animation/animation-auxiliary-curve/menu.png new file mode 100644 index 0000000000..0499bf0f25 Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/menu.png differ diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/overview.png b/versions/4.0/zh/animation/animation-auxiliary-curve/overview.png new file mode 100644 index 0000000000..ca0b76b2f6 Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/overview.png differ diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/right-menu.png b/versions/4.0/zh/animation/animation-auxiliary-curve/right-menu.png new file mode 100644 index 0000000000..7fec349e70 Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/right-menu.png differ diff --git a/versions/4.0/zh/animation/animation-auxiliary-curve/start-edit.png b/versions/4.0/zh/animation/animation-auxiliary-curve/start-edit.png new file mode 100644 index 0000000000..3fbc6f8dcc Binary files /dev/null and b/versions/4.0/zh/animation/animation-auxiliary-curve/start-edit.png differ diff --git a/versions/4.0/zh/animation/animation-clip-migration-3.3.x.md b/versions/4.0/zh/animation/animation-clip-migration-3.3.x.md new file mode 100644 index 0000000000..df2c63c0d6 --- /dev/null +++ b/versions/4.0/zh/animation/animation-clip-migration-3.3.x.md @@ -0,0 +1,39 @@ +# v3.3 动画剪辑数据升级指南 + +在 v3.3 中,Creator 大幅度重构了动画剪辑类,摒弃了之前难以操纵、难以理解的接口,引入了轨道、通道等概念,使动画分量编辑变得更容易。更主要的是动画剪辑统一与其他引擎模块使用公共的曲线对象,而非自立门户定义自己的曲线类型。 + +## 资源升级 + +v3.3 之前的动画剪辑资源使用 v3.3 及后续版本打开后会自动升级至新的数据类型,动画效果不变。 + +但有以下几种边缘情况需要注意: + +- 渐变方式 + + 旧版本的动画剪辑数据中若使用了非线性且非常量的渐变方式时,运行时仍会生效,但在 **动画编辑器** 中无法再编辑,需要手动在曲线编辑器中重新编辑。 + +- 动画曲线类型 + + 旧版本的动画曲线都将被转换为相对应的动画属性轨道,包括以下几种曲线类型: + + - `number` + - `Vec2`、`Vec3`、`Vec4` + - `Quat` + - `Color` + - `Size` + + 其余类型的曲线将被转换为对象轨道。详情请参考 [动画属性轨道](use-animation-curve.md)。 + +- 通过模型文件导入的动画数据将不能再通过代码访问 + +## API 更改 + +旧版动画剪辑对象的以下字段被废弃: + +- `times` + +- `curves` + +- `commonTargets` + +考虑到兼容旧版的使用,我们保留了这些 API 的效果。程序化地设置这些字段后,在 **动画运行之前** 都会被正确地转换为新格式。 diff --git a/versions/4.0/zh/animation/animation-clip.md b/versions/4.0/zh/animation/animation-clip.md new file mode 100644 index 0000000000..f9288ca58e --- /dev/null +++ b/versions/4.0/zh/animation/animation-clip.md @@ -0,0 +1,29 @@ +# 动画剪辑 + +动画剪辑(Animation Clip)是一份动画的声明数据,即包含动画数据的资源,是动画系统的核心之一。将动画剪辑挂载到 [动画组件](animation-comp.md) 上,就能够将这份动画数据应用到动画组件所在的节点上。 + +目前 Creator 支持从外部导入动画,或者直接在 Creator 内部创建一个全新的动画剪辑。 + +## Creator 内部创建的动画 + +通过 **动画编辑器** 可以直接创建全新的动画剪辑,并进行编辑和预览,详情请参考 [使用动画编辑器](animation.md)。 + +也可以通过脚本创建,详情请参考 [程序化编辑动画剪辑](use-animation-curve.md)。 + +## 外部导入的骨骼动画 + +外部导入的动画大概包括以下几种: + +1. 第三方美术工具生产的骨骼动画 + +2. 模型导入后附带的骨骼动画 + + 带动画的模型导入后,会同时导入模型中包含的动画。这个动画和内部新建资源的使用方式是一样的,骨骼动画的裁剪可以参考 [模型资源的动画模块介绍](../asset/model/mesh.md)。 + +Creator 从 v3.4 开始引入了全新的 Marionette 动画系统,实现了由状态机控制的骨骼动画流程,具体内容请参考 [Marionette 动画系统](./marionette/index.md)。 + +更多关于骨骼动画的设置等,详情请参考 [骨骼动画](skeletal-animation.md)。 + +> **注意**:外部导入的骨骼动画不支持在 **动画编辑器** 中查看和编辑,各节点也是锁住状态,只能在外部美术工具中进行编辑。 +> +> ![skeletal animation](animation-clip/skeletal-animation.png) diff --git a/versions/4.0/zh/animation/animation-clip/skeletal-animation.png b/versions/4.0/zh/animation/animation-clip/skeletal-animation.png new file mode 100644 index 0000000000..06dcb920f1 Binary files /dev/null and b/versions/4.0/zh/animation/animation-clip/skeletal-animation.png differ diff --git a/versions/4.0/zh/animation/animation-comp.md b/versions/4.0/zh/animation/animation-comp.md new file mode 100644 index 0000000000..9467f842d2 --- /dev/null +++ b/versions/4.0/zh/animation/animation-comp.md @@ -0,0 +1,27 @@ +# 动画组件参考 + +Animation(动画)组件可以以动画方式驱动所在节点和子节点上的节点和组件属性,包括用户自定义脚本中的属性。 + +![animation component](./animation-create/animation-component.png) + +添加动画组件有三种方式: + +1. 在 **动画编辑器** 中添加动画组件,详情请参考 [创建 Animation 组件和动画剪辑](animation-create.md)。 +2. 在 **层级管理器** 中选中需要添加动画的节点,然后在 **属性检查器** 中选择 **添加组件 -> Animation -> Animation** 来添加一个动画组件到节点上。 +3. 通过脚本添加,详情请参考 [使用脚本控制动画](animation-component.md)。 + +> **注意**:动画组件/骨骼动画组件不能和动画控制器组件挂载在同一个节点上,详情请参考 [Marionette 动画系统](marionette/index.md)。 + +## 动画组件属性 + +| 属性 | 功能说明 | +| :-- | :------ | +| Clips | 添加的动画剪辑资源,默认为空,支持添加多个。在这里添加的 AnimationClip 可以直接在 **动画编辑器** 中进行编辑。 | +| DefaultClip | 默认的动画剪辑,若该项在挂载了动画剪辑的同时,还勾选了下方的 **PlayOnLoad** 属性,那么动画便会在加载完成后自动播放 Default Clip 的内容。 | +| PlayOnLoad | 布尔类型。若勾选该项,则动画加载完成后会自动播放 Default Clip 的内容。 | + +## 说明 + +如果一个动画需要包含多个节点,那么一般会新建一个节点作为动画的 **根节点**,再将动画组件挂载到根节点上,则这个根节点下的其他子节点都会自动进入到这个动画剪辑中,并显示在 **动画编辑器** 的 **节点列表** 区域。详情可参考 [熟悉动画编辑器 — 节点列表](animation-editor.md#2-%E8%8A%82%E7%82%B9%E5%88%97%E8%A1%A8)。 + +Animation 组件也提供了一些常用的动画控制函数,若要通过脚本程序化控制动画,请参考 [使用脚本控制动画](animation-component.md)。 diff --git a/versions/4.0/zh/animation/animation-component.md b/versions/4.0/zh/animation/animation-component.md new file mode 100644 index 0000000000..4eec8e85e3 --- /dev/null +++ b/versions/4.0/zh/animation/animation-component.md @@ -0,0 +1,177 @@ +# 程序化控制动画 + +## 动画组件 + +动画组件管理了一组动画状态,用于控制各动画的播放、暂停、继续、停止、切换等。动画组件会为每一个动画剪辑都创建相应的 [动画状态](animation-state.md) 对象,动画状态用于控制需要在对象上使用的动画剪辑。 + +在动画组件中,动画状态是通过名称来标识的,每个动画状态的默认名称就是其动画剪辑的名称。 + +在脚本中为节点添加动画组件的方式如下: + +```ts +import { Animation, Node } from 'cc'; + +function (node: Node) { + const animationComponent = node.addComponent(Animation); +} +``` + +### 动画的播放与切换 + +#### 播放动画 + +动画组件通过 [play()](%__APIDOC__%/zh/class/Animation?id=play) 控制指定动画的播放,例如: + +```ts +// 播放动画状态 'idle' +animationComponent.play('idle'); +``` + +使用 `play` 播放动画时若未指定具体动画,并且设置了 `defaultClip`,则会播放 defaultClip 动画。若动画组件的 `playOnLoad` 也设置为 `true`,则动画组件将在第一次运行时自动播放 `defaultClip` 的内容。 + +```ts +// 未指定播放的动画,并且设置了 defaultClip 的话,则会播放 defaultClip 动画 +animationComponent.play(); +``` + +#### 切换动画 + +使用 `play` 接口播放一个动画时,如果此时还有其他的动画正在播放,则会立即停止其他动画的播放。这种切换是非常突兀的,在某些情况下,我们希望这种切换是“淡入淡出”的效果,那么便可以使用 [crossFade()](%__APIDOC__%/zh/class/Animation?id=crossfade),在指定的周期内平滑地完成切换。例如: + +```ts +// 播放动画状态 ‘walk’ +animationComponent.play('walk'); + +/* ... */ + +// 在 0.3 秒内平滑地从走的动画切换为跑的动画 +animationComponent.crossFade('run', 0.3); +``` + +`crossFade()` 的这种淡入淡出机制使得同一时刻可能有不止一个动画状态在播放。因此,动画组件没有 **当前动画** 的概念。 + +即便如此,动画组件仍提供了 `pause()`、`resume()`、`stop()` 方法,这些方法在暂停、继续以及停止正在播放的所有动画状态的同时,也暂停、继续以及停止动画的切换。 + +**备注:只作用于实时骨骼动画** + +关于动画组件更多相关的控制接口,详情请参考 [类 `Animation`](%__APIDOC__%/zh/class/Animation)。 + +## 动画状态 + +动画组件只提供了一些简单的控制函数,大部分情况下是足够和易于使用的,但若想要得到更多的动画信息以及动画控制接口,需要使用 [动画状态](animation-state.md)。 + +## 帧事件 + +动画编辑器支持可视化编辑 [事件帧](animation-event.md),也可以直接在脚本里添加帧事件。 + +`AnimationClip` 的 `events` 包含了此动画所有的帧事件,每个帧事件都具有以下属性: + +```ts +{ + frame: number; + func: string; + params: any[]; +} +``` + +- `frame`:表示事件触发的时间点,单位为秒。例如 `0.618` 就表示当动画到达第 0.618 秒时将触发事件。时间轴刻度单位之间的转换,详情请参考 [时间轴的刻度单位显示](animation-editor.md#%E6%97%B6%E9%97%B4%E8%BD%B4%E7%9A%84%E5%88%BB%E5%BA%A6%E5%8D%95%E4%BD%8D%E6%98%BE%E7%A4%BA)。 +- `func`:表示事件触发时回调的函数名称。事件触发时,动画系统会搜索 **动画根节点中的所有组件**,若组件中有实现动画事件 `func` 中指定的函数,便会对其进行调用,并传入 `params` 中的参数。 + +例如,在动画时间轴的第 0.5s 添加了一个事件帧: + +![keyframe](./animation/keyframe.png) + +那么在脚本中实现的代码如下: + +```ts +{ + frame: 0.5; + func: 'onTrigger'; + params: [ 0 ]; +} +``` + +### 示例 + +以下代码表示 `MyScript` 脚本组件所在节点的动画组件的默认动画剪辑在进行到第 0.5 秒时,将调用 `MyScript` 组件的 `onTriggered()` 方法并传递参数 `0`。 + +```ts +import { Animation, Component, _decorator } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("MyScript") +class MyScript extends Component { + public start() { + const animation = this.node.getComponent(Animation); + if (animation && animation.defaultClip) { + const { defaultClip } = animation; + defaultClip.events = [ + { + frame: 0.5, // 第 0.5 秒时触发事件 + func: 'onTriggered', // 事件触发时调用的函数名称 + params: [ 0 ], // 向 `func` 传递的参数 + } + ]; + + animation.clips = animation.clips; + } + } + + public onTriggered(arg: number) { + console.log('I am triggered!', arg); + } +} +``` + +## 动画事件 + +除了 **动画编辑器** 中的帧事件提供了回调,动画系统还提供了动画事件回调方式。目前支持的回调事件包括: + +- `PLAY`:开始播放时触发 +- `STOP`:停止播放时触发 +- `PAUSE`:暂停播放时触发 +- `RESUME`:恢复播放时触发 +- `LASTFRAME`:假如动画循环次数大于 1,当动画播放到最后一帧时触发。 +- `FINISHED`:动画播放完成时触发 + +其定义枚举 `Animation.EventType` 内,以骨骼动画组件为例,代码示例如下: + +```ts +import { _decorator, Component, Node, SkeletalAnimation, Animation, SkeletalAnimationState } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('SkeletonAnimationEvent') +export class SkeletonAnimationEvent extends Component { + start() { + + let skeletalAnimation = this.node.getComponent(SkeletalAnimation); + skeletalAnimation.on(Animation.EventType.FINISHED, this.onAnimationFinished, this); + } + + onAnimationFinished(type:Animation.EventType, state:SkeletalAnimationState){ + + } +} +``` + +非骨骼动画,代码示例如下: + +```ts +import { _decorator, Component, Node, Animation, AnimationState } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('AnimationEvent') +export class AnimationEvent extends Component { + + start() { + let animation = this.node.getComponent(Animation); + animation.on(Animation.EventType.FINISHED, this.onAnimationEvent, this) + } + + onAnimationEvent(type: Animation.EventType, state: AnimationState) { + + } +} +``` + +更多内容请参考 [Animation.EventType](%__APIDOC__%/zh/namespace/Animation?id=EventType)。 diff --git a/versions/4.0/zh/animation/animation-create.md b/versions/4.0/zh/animation/animation-create.md new file mode 100644 index 0000000000..855c0cc2d5 --- /dev/null +++ b/versions/4.0/zh/animation/animation-create.md @@ -0,0 +1,47 @@ +# 创建动画组件和动画剪辑 + +在使用 **动画编辑器** 制作动画之前需要先在 **层级管理器** 或者 **场景编辑器** 中选择要添加动画的节点,然后添加 [动画组件](./animation-comp.md),并在组件上挂载 **动画剪辑(Animation Clip)**,便可以编辑动画数据,编辑后的动画数据会保存在当前的动画剪辑中。没有挂载 Clip 的节点是无法编辑动画数据的。 + +如果当前选中节点没有动画组件,则 **动画编辑器** 的界面上会显示 **添加 Animation 组件** 按钮,点击即可在 **属性检查器** 上添加 **动画组件**。 + +![add component](./animation-create/add-component.png) + +继续点击 **动画编辑器** 中的 **新建 AnimationClip 文件** 按钮并命名(例如 `animation`): + +![add clip](./animation-create/add-clip.png) + +便可在 **资源管理器** 中自动创建一个动画剪辑(`animation.anim`)并挂载到 Animation 组件的 `DefaultClip` 属性上: + +![mount clip](./animation-create/mount-clip.png) + +如果已有 **动画剪辑(Animation Clip)**,即使该节点无动画组件或无任何动画剪辑,也可以通过拖拽 Animation Clip 到 Animation 窗口上,引擎会自动创建 **Animation** 组件并 + +![drag and drop](animation-create/drag-clip.gif) + +以上简单介绍了如何在 **动画编辑器** 中创建动画组件和动画剪辑,更多关于动画组件的创建和属性说明请参考 [动画组件参考](./animation-comp.md)。更多创建动画剪辑的方法请参考文末部分的内容。 + +然后继续点击 **进入动画编辑模式** 即可开始 [编辑动画剪辑](edit-animation-clip.md)。新建的空的动画剪辑在动画编辑器中显示如下: + +![empty clip](./animation-create/empty-clip.png) + +## 挂载新动画剪辑 + +一个 Animation 组件可以挂载多份动画剪辑,若需要额外在已有动画剪辑的对象上创建并挂载新的动画剪辑,有以下几种方式: + +1. 在 **资源管理器** 中点击左上方的 **+** 按钮,或者右键点击空白区域,然后选择 **Animation Clip**,这时候会在 **资源管理器** 中生成一个动画剪辑文件(默认名为 `animation`)。 + + 然后在 **层级管理器** 中选中对应节点,在 **属性检查器** 中找到 Animation 组件(`cc.Animation`),修改 `Clips` 属性的数值。例如原本只挂载了一个 clip 文件,现在想要再添加一个,那么就将原本的 **1** 改成 **2**。 + + ![add-clip](./animation-create/add-new-clip.png) + + 最后将刚刚在 **资源管理器** 中创建的动画剪辑,拖拽到上图中的 `cc.AnimationClip` 选择框中即可。 + +2. 在 **属性检查器** 中找到 Animation 组件(`cc.Animation`),修改 `Clips` 属性的数值。 + + 然后点击新出现的空的 `cc.AnimationClip` 选择框后面的查找按钮,在弹出的搜索窗口中点击右上方的 **创建** 按钮,即可自动在 **资源管理器** 中创建动画剪辑并挂载到 `cc.AnimationClip` 选择框中。 + + ![add-clip](./animation-create/add-new-clip2.png) + +3. 通过脚本动态创建动画剪辑,详情请参考 [程序化编辑动画剪辑](use-animation-curve.md)。 + +可以在 **动画编辑器** 左上角的 **Clips** 下拉列表切换需要编辑的动画剪辑。 diff --git a/versions/4.0/zh/animation/animation-create/add-clip.png b/versions/4.0/zh/animation/animation-create/add-clip.png new file mode 100644 index 0000000000..63238b2786 Binary files /dev/null and b/versions/4.0/zh/animation/animation-create/add-clip.png differ diff --git a/versions/4.0/zh/animation/animation-create/add-component.png b/versions/4.0/zh/animation/animation-create/add-component.png new file mode 100644 index 0000000000..b4d837f671 Binary files /dev/null and b/versions/4.0/zh/animation/animation-create/add-component.png differ diff --git a/versions/4.0/zh/animation/animation-create/add-new-clip.png b/versions/4.0/zh/animation/animation-create/add-new-clip.png new file mode 100644 index 0000000000..9f7cbba5f5 Binary files /dev/null and b/versions/4.0/zh/animation/animation-create/add-new-clip.png differ diff --git a/versions/4.0/zh/animation/animation-create/add-new-clip2.png b/versions/4.0/zh/animation/animation-create/add-new-clip2.png new file mode 100644 index 0000000000..93c05e0268 Binary files /dev/null and b/versions/4.0/zh/animation/animation-create/add-new-clip2.png differ diff --git a/versions/4.0/zh/animation/animation-create/animation-component.png b/versions/4.0/zh/animation/animation-create/animation-component.png new file mode 100644 index 0000000000..f778df4aba Binary files /dev/null and b/versions/4.0/zh/animation/animation-create/animation-component.png differ diff --git a/versions/4.0/zh/animation/animation-create/drag-clip.gif b/versions/4.0/zh/animation/animation-create/drag-clip.gif new file mode 100644 index 0000000000..2b04aceb33 Binary files /dev/null and b/versions/4.0/zh/animation/animation-create/drag-clip.gif differ diff --git a/versions/4.0/zh/animation/animation-create/empty-clip.png b/versions/4.0/zh/animation/animation-create/empty-clip.png new file mode 100644 index 0000000000..7e985ddf20 Binary files /dev/null and b/versions/4.0/zh/animation/animation-create/empty-clip.png differ diff --git a/versions/4.0/zh/animation/animation-create/mount-clip.png b/versions/4.0/zh/animation/animation-create/mount-clip.png new file mode 100644 index 0000000000..b605e29a7e Binary files /dev/null and b/versions/4.0/zh/animation/animation-create/mount-clip.png differ diff --git a/versions/4.0/zh/animation/animation-curve.md b/versions/4.0/zh/animation/animation-curve.md new file mode 100644 index 0000000000..e66e1b7961 --- /dev/null +++ b/versions/4.0/zh/animation/animation-curve.md @@ -0,0 +1,31 @@ +# 曲线编辑视图 + +在创建了基本的动画剪辑之后,我们可以**通过编辑动画曲线来控制关键帧之间如何随着时间推移而变化**。动画曲线实际上是由当前属性轨道上各个关键帧连接而成的线性轨迹,每个关键帧是这条曲线路径上的控制点。曲线编辑视图下,可以更直观的看到关键帧值的变化,方便更加细腻的调节关键帧数值变化。 + +![curve btn](animation-curve/curve-btn.png) + +点击上图所示的属性列表右边上的切换视图按钮,可以切换到曲线编辑视图。 +切换到曲线编辑视图后,可以看到属性列表上会显示线条标记,不支持曲线编辑的属性轨道上不会显示线条标记,选中对应的属性轨道也无法正常显示曲线。当对应的曲线在曲线编辑区域显示时,线条标记将会会显示对应曲线颜色,在属性菜单区域也会显示当前选中的曲线名称。 + +![show-line](animation-curve/show-line.png) + +## 认识曲线编辑器 + +动画编辑器里内置了曲线编辑器用于动画曲线编辑,这里的曲线编辑器和粒子的曲线编辑器其实是一样的。只不过在通用曲线编辑的基础上,动画编辑器内做了一些定制处理,更加完善的支持了曲线关键帧的编辑。 + +关于曲线编辑的一些通用功能请参考 [曲线编辑器](./curve-editor.md) 的相关文档。 + +除了一些通用的曲线编辑操作以外,动画编辑器内的曲线编辑器区域还支持以下功能: + +- 显示所有关键帧,点击曲线编辑区域上方的按钮 ![show-all-keys](./animation-curve/show-all-keys.png),或者按下 F 快捷键即可快速的将当前视图区域缩放到可以显示所有关键帧的比例。 +- 支持动画编辑器在关键帧显示视图下所有关键帧相关快捷键与处理,比如双击关键帧时间控制线将会移动到关键帧位置、复制粘贴快捷键等等。 +- 曲线编辑区域的复制粘贴与关键帧视图模式是共享的,可以在不同的显示视图下的情况下互相粘贴复制的关键帧数据。 +- 曲线编辑的间隔排列关键帧功能使用的间隔数将会与动画编辑器菜单栏上的间隔数同步。 + +## 使用贝塞尔曲线预设 + +在进入曲线编辑视图后,曲线编辑区域的右边会有曲线预设面板的 Tab 控件。点击即可开关贝塞尔曲线预设面板。 + +动画编辑器内置了一些常见的贝塞尔缓动曲线预设,双击两帧之间的曲线片段后可以选中曲线,选中曲线后点击指定的曲线预设便会应用在选中的曲线片段上。在应用曲线预设后,曲线两端关键帧的插值模式都会改为曲线模式,并且关键帧靠近曲线的一侧插值权重模式都会开启。 + +![动画曲线预设](./animation-curve/curve-preset.gif) diff --git a/versions/4.0/zh/animation/animation-curve/add-keyframe-to-curve.png b/versions/4.0/zh/animation/animation-curve/add-keyframe-to-curve.png new file mode 100644 index 0000000000..c327b1f885 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/add-keyframe-to-curve.png differ diff --git a/versions/4.0/zh/animation/animation-curve/add-keyframe.png b/versions/4.0/zh/animation/animation-curve/add-keyframe.png new file mode 100644 index 0000000000..816239da4e Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/add-keyframe.png differ diff --git a/versions/4.0/zh/animation/animation-curve/animation-curve-preset.gif b/versions/4.0/zh/animation/animation-curve/animation-curve-preset.gif new file mode 100644 index 0000000000..3f7a16801c Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/animation-curve-preset.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/change-preset.gif b/versions/4.0/zh/animation/animation-curve/change-preset.gif new file mode 100644 index 0000000000..e5ca97aef6 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/change-preset.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/constant.png b/versions/4.0/zh/animation/animation-curve/constant.png new file mode 100644 index 0000000000..e0b397f328 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/constant.png differ diff --git a/versions/4.0/zh/animation/animation-curve/cubic.png b/versions/4.0/zh/animation/animation-curve/cubic.png new file mode 100644 index 0000000000..d0cfaff2f6 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/cubic.png differ diff --git a/versions/4.0/zh/animation/animation-curve/curve-btn.png b/versions/4.0/zh/animation/animation-curve/curve-btn.png new file mode 100644 index 0000000000..36eda835b4 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/curve-btn.png differ diff --git a/versions/4.0/zh/animation/animation-curve/curve-preset.gif b/versions/4.0/zh/animation/animation-curve/curve-preset.gif new file mode 100644 index 0000000000..3f7a16801c Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/curve-preset.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/edit-key.png b/versions/4.0/zh/animation/animation-curve/edit-key.png new file mode 100644 index 0000000000..997827d5aa Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/edit-key.png differ diff --git a/versions/4.0/zh/animation/animation-curve/interopMode-key.gif b/versions/4.0/zh/animation/animation-curve/interopMode-key.gif new file mode 100644 index 0000000000..74dac4f745 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/interopMode-key.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/interopMode.png b/versions/4.0/zh/animation/animation-curve/interopMode.png new file mode 100644 index 0000000000..ae1a9d40ab Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/interopMode.png differ diff --git a/versions/4.0/zh/animation/animation-curve/linear.png b/versions/4.0/zh/animation/animation-curve/linear.png new file mode 100644 index 0000000000..d88fa70b5c Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/linear.png differ diff --git a/versions/4.0/zh/animation/animation-curve/main.png b/versions/4.0/zh/animation/animation-curve/main.png new file mode 100644 index 0000000000..78333b342f Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/main.png differ diff --git a/versions/4.0/zh/animation/animation-curve/move-keys.gif b/versions/4.0/zh/animation/animation-curve/move-keys.gif new file mode 100644 index 0000000000..0eb5726707 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/move-keys.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/preset.gif b/versions/4.0/zh/animation/animation-curve/preset.gif new file mode 100644 index 0000000000..3ed087067a Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/preset.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/save-curve.gif b/versions/4.0/zh/animation/animation-curve/save-curve.gif new file mode 100644 index 0000000000..2550586434 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/save-curve.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/scale-keys.gif b/versions/4.0/zh/animation/animation-curve/scale-keys.gif new file mode 100644 index 0000000000..369a73cce1 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/scale-keys.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/select-key.gif b/versions/4.0/zh/animation/animation-curve/select-key.gif new file mode 100644 index 0000000000..92f57f44cf Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/select-key.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/select-preset.gif b/versions/4.0/zh/animation/animation-curve/select-preset.gif new file mode 100644 index 0000000000..77afdc90b4 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/select-preset.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/show-all-keys.png b/versions/4.0/zh/animation/animation-curve/show-all-keys.png new file mode 100644 index 0000000000..828928e7c9 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/show-all-keys.png differ diff --git a/versions/4.0/zh/animation/animation-curve/show-line.png b/versions/4.0/zh/animation/animation-curve/show-line.png new file mode 100644 index 0000000000..eb269b990d Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/show-line.png differ diff --git a/versions/4.0/zh/animation/animation-curve/tangentWeightMode-key.gif b/versions/4.0/zh/animation/animation-curve/tangentWeightMode-key.gif new file mode 100644 index 0000000000..278837b6a1 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/tangentWeightMode-key.gif differ diff --git a/versions/4.0/zh/animation/animation-curve/tangentWeightMode.png b/versions/4.0/zh/animation/animation-curve/tangentWeightMode.png new file mode 100644 index 0000000000..ee15949957 Binary files /dev/null and b/versions/4.0/zh/animation/animation-curve/tangentWeightMode.png differ diff --git a/versions/4.0/zh/animation/animation-editor.md b/versions/4.0/zh/animation/animation-editor.md new file mode 100644 index 0000000000..cf9f9888de --- /dev/null +++ b/versions/4.0/zh/animation/animation-editor.md @@ -0,0 +1,180 @@ +# 熟悉动画编辑器 + +**动画编辑器** 面板用于编辑和预览当前所选节点的动画剪辑。编辑动画数据或者相关属性时,鼠标焦点离开后会立即生效。 + +Cocos Creator 的默认布局中已经包含了 **动画编辑器**,也可以通过点击编辑器上方主菜单的 **面板 -> 动画 -> 动画编辑器** 打开 **动画编辑器**。 + +## 面板介绍 + +**动画编辑器** 面板可以划分为以下几个主要部分: + +![animation-editor](animation-editor/animation-panel.png) + +1. **菜单工具栏**:放置一些常用的功能按钮,例如播放/暂停/停止动画、添加事件帧、保存动画、退出编辑等。 +2. **节点列表**:用于显示/设置当前选中节点及其子节点。 +3. **动画时间轴与关键帧预览**:用于显示/设置当前节点(及其子节点)添加的事件帧以及所添加的动画属性上所有关键帧的预览。 +4. **动画属性列表**:用于显示/设置当前选中的节点在当前编辑的动画剪辑中包含的所有动画属性。 +5. **动画属性关键帧预览**:用于显示/设置当前选中节点的各个动画属性上的所有关键帧,当切换到曲线视图时,将显示关键帧连接的曲线轨迹。 +6. **设置当前动画剪辑的相关属性**:用于设置当前动画剪辑的播放模式、播放速度等。 + +更多具体内容请参考下文介绍。 + +### 1. 菜单工具栏 + +**菜单工具栏** 中的功能按钮从左到右依次为: + +| 图标 | 功能说明 | 快捷键 | +| :---- | :--- | :--- | +| ![menu clips](animation-editor/menu_clips.png) | 切换动画剪辑。当动画组件上挂载了多个动画剪辑时,可通过该按钮切换进行编辑。 | - | +| ![jump to first frame](animation-editor/menu_first.png) | 将红色的时间控制线移动到当前动画剪辑的开头 | Ctrl/Cmd + | +| ![jump to previous frame](animation-editor/menu_prev.png) | 将时间控制线移动到上一帧 | | +| ![play/pause](animation-editor/menu_play.png) | 播放/暂停当前动画 | Ctrl/Cmd + P | +| ![jump to next frame](animation-editor/menu_next.png) | 将时间控制线移动到下一帧 | | +| ![jump to last frame](animation-editor/menu_last.png) | 将时间控制线移动到当前动画剪辑的结尾 | Ctrl/Cmd + | +| ![stop](animation-editor/menu_stop.png) | 点击停止当前动画,停止后时间控制线将会移动到第一帧 | Ctrl/Cmd + S | +| ![time](animation-editor/menu_time.png) | 时间轴的刻度单位显示方式,包括三种:
**时间:0-00**
**帧:0**
**时间(s):0 s**
详情可参考下文 **时间轴的刻度单位显示** 部分的内容。 | - | +| ![spacing](animation-editor/menu-spacing.png)| 用于设置同时生成多个关键帧时,关键帧之间的间隔大小。| - | +| ![spacing](animation-editor/menu_spacing_btn.png) | 间隔排列同时选中的多个关键帧。选中的关键帧将会以第一个帧为基准,以 **Spacing** 输入框中的数值作为间隔大小依次排列 | - | +| ![add event](animation-editor/menu_event.png) | 添加事件帧,点击该按钮即可在当前时间线上方的位置添加事件帧 | - | +| ![menu save](animation-editor/menu_save.png) | 保存当前编辑的动画数据。 | - | +| ![menu doc](animation-editor/menu_doc.png) | 点击该按钮即可跳转到官方手册文档关于动画编辑器部分的内容 | Ctrl/Cmd + S | +| ![shortcuts](./animation-editor/shortcuts.png) | 打开快捷键面板,支持自定义快捷键。详情可参考下文 **快捷键** 部分的内容 | - | +| ![exit](animation-editor/menu_exit.png)| 退出动画编辑模式 | Ctrl + Q | + +### 2. 节点列表 + +![node list](animation-editor/2-node-list.png) + +该区域会显示当前选中的动画节点及其子节点,并以与 **层级管理器** 中的节点树一一对应的方式排列。可点击上方的 ![hide](animation-editor/hide-node.png) 按钮来隐藏/显示无动画数据的节点,或者直接在输入框中输入节点名称来快速查找节点。 + +目前支持右键点击节点来选择清空节点数据、迁移节点数据、复制粘贴节点数据。具体内容请参考 [节点数据常见操作](edit-animation-clip.md#%E8%8A%82%E7%82%B9%E6%95%B0%E6%8D%AE%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C)。 + +![node operation](animation-editor/node-operation.png) + +### 3. 动画时间轴与关键帧预览 + +![animation timeline](animation-editor/animation-timeline.png) + +动画时间轴主要用于显示/设置当前节点添加的自定义 [事件帧](animation-event.md)、节点(及其子节点)添加的所有动画属性上的 [关键帧](edit-animation-clip.md)(蓝色菱形)并显示预览。 + +时间控制线(红色竖线)表示当前动画所处的时间节点,可通过以下几种方式更改当前选中时间: + +- 直接拖拽时间控制线 +- 双击关键帧 +- 在菜单工具栏中使用相关移动控制按钮 +- 使用快捷键,具体控制键可查询下文 **快捷键** 部分的内容 +- 在动画时间轴上方区域内点击任意位置 + + ![about the timeline](animation-editor/above-timeline.png) + +移动时间控制线则 **场景编辑器** 中的节点也会根据动画轨迹进行相应的移动。 + +![move line](animation-editor/move-line.gif) + +#### 时间轴的刻度单位显示 + +**菜单工具栏** 中的 ![time](animation-editor/menu_time.png) 按钮可用于切换动画时间轴的刻度显示方式。输入框中的数值会随着时间控制线的移动而变化,支持手动输入,输入完成后时间控制线会自动定位到相应的位置。 + +目前支持以下三种显示方式: + +- **时间:0-00**(默认):以秒和帧组合为单位的方式来显示动画时间轴的刻度。输入框前面的数值表示 **秒**,后面的数值表示 **帧**,例如 `01-05` 表示 1 秒又 5 帧。 + + ![time](animation-editor/time.png) + +- **帧:0**:以帧为单位的方式来显示动画时间轴的刻度。 + + ![frame](animation-editor/frame.png) + +- **时间(s):0 s**:以秒为单位的方式来显示动画时间轴的刻度。 + + ![times](animation-editor/times.png) + +我们一般用帧率(Sample)来表示一秒要划分为多少帧,可以在 **动画编辑器** 底部的 **Sample** 选项中进行调整。当时间轴的刻度单位显示方式不同时,受影响程度也不同。 + +当刻度单位设置为 **帧** 时,以帧数为单位,不受帧率影响。
+当设置为 **时间** 或者 **时间(s)** 时,同一个刻度表示的时间点会随着帧率的变化而有所不同,这两者之间的转换计算方式如下: + +| 帧率(Sample)| 时间:00-00 | 时间(s):0 s | +| :-- | :--------- | :------------ | +| 30 | 01-05 | 1 + 5/30 = 1.17 s | +| 10 | 01-05 | 1 + 5/10 = 1.5 s | + +例如将帧率设置为 30,在 `01-05` 刻度上添加了一个关键帧,则该关键帧位于动画开始后的第 35 帧。然后把帧率修改为 10,该关键帧所在的总帧数并没有发生变化,仍然处在动画开始后的第 35 帧,而此时关键帧所在位置的刻度读数变成了 `03-05`,换算成时间(s)以后正好是之前的 3 倍。 + +#### 更改动画时间轴缩放比例 + +在操作中如果觉得 **动画编辑器** 显示的范围太小,需要按比例缩小,让更多的关键帧显示到编辑器内怎么办? + +在 **动画时间轴** 和 **动画属性关键帧预览** 任一区域内滚动鼠标滚轮,即可放大或者缩小时间轴的横向显示比例。 + +![scale](./animation-editor/scale.gif) + +#### 移动动画时间轴显示区域 + +在 **动画时间轴** 或者 **动画属性关键帧预览** 任一区域按下鼠标中键/右键进行拖拽,即可查看动画时间轴左/右侧超出显示区域而被隐藏的关键帧。 + +![scale canvas](./animation-editor/scale-canvas.gif) + +### 4. 动画属性列表 + +![node list](./animation-editor/4-pro-list.png) + +该区域主要用于显示/添加/设置当前选中的节点在当前动画剪辑中的动画属性。点击右上角的 **+** 按钮即可添加动画属性,动画属性包括了节点自有的属性、组件属性(包含用户自定义脚本组件中的属性)。组件包含的属性前会加上组件的名字,比如 `cc.Sprite.spriteFrame`。 + +右键点击动画属性或者单击动画属性右侧的 ![property](./animation-editor/set-pro.png) 按钮即可选择 **移除当前属性轨道**、**清空关键帧**,或者 **复制粘贴当前属性轨道**。具体内容请参考 [动画属性数据常见操作](edit-animation-clip.md#%E5%8A%A8%E7%94%BB%E5%B1%9E%E6%80%A7%E6%95%B0%E6%8D%AE%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C)。 + +![property operation](./animation-editor/pro-operation.png) + +### 5. 动画属性关键帧预览 + +![animation property track](./animation-editor/animation-property-track.png) + +该区域主要用于显示当前选中节点在各动画属性上具体的关键帧设置情况,也是关键帧编辑的主要区域。可以直接在右侧的动画属性上点击右键来添加关键帧。同时在该区域也支持框选、点选关键帧来进行移动、复制、粘贴等操作。详情请参考 [关键帧常见操作](edit-animation-clip.md#%E5%85%B3%E9%94%AE%E5%B8%A7%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C)。 + +单击选中某一关键帧,关键帧会由蓝色变成白色并且该区域上方会显示当前关键帧的相关信息,若双击关键帧还会将时间控制线移动到当前关键帧所在位置。 + +![key info](animation-editor/key-info.png) + +### 6. 设置当前动画剪辑的相关属性 + +当前动画剪辑在动画编辑器上可直接设置的属性包括:**WrapMode**、**Sample**、**Speed** 和 **Duration**。具体内容请参考 [动画剪辑属性设置](edit-animation-clip.md#%E5%8A%A8%E7%94%BB%E5%89%AA%E8%BE%91%E5%B1%9E%E6%80%A7%E8%AE%BE%E7%BD%AE)。 + +## 调整动画编辑器布局 + +节点列表与时间轴之间的分界线,以及属性列表与时间轴之间的分界线都是可以用于拖拽更改布局的,通过拖拽可以自由地将 **动画编辑器** 调整成适合编辑的布局效果。 + +![layout](./animation-editor/layout.gif) + +## 快捷键 + +点击菜单工具栏的 ![shortcuts](./animation-editor/shortcuts.png) 按钮,即可打开快捷键面板: + +![shortcuts manager](./animation-editor/shortcuts-manager.png) + +支持开发者自定义 **动画编辑器** 的快捷键,直接点击要修改的功能的快捷键组合,会出现如下图所示的提示,即可直接修改快捷键。 + +![shortcuts change](./animation-editor/shortcuts-change.png) + +### 默认快捷键汇总 + +功能 | 快捷键 | 说明 +:--- | :----- | :--- +进入/退出动画编辑器 | Ctrl/Cmd + E | - +保存动画数据 | Ctrl/Cmd + S | - +向前移动一帧 | | 如果已经在第 0 帧,则忽略当前操作。未选中关键帧时移动的是时间控制线,选中关键帧时移动的是关键帧 +向后移动一帧 | | 未选中关键帧时移动的是时间控制线,选中关键帧后移动的是关键帧 +移动到第一帧 | Ctrl/Cmd + | 将红色的时间控制线移动到当前动画剪辑的开头 +移动到最后一帧 | Ctrl/Cmd + | 将时间控制线移动到当前动画剪辑的结尾 +删除当前选中关键帧 | Delete / Cmd + Backspace | - +播放/暂停动画 | P | - +停止动画 | Alt + S | 当前时间将变为 0,且时间控制线将会移动到第一帧 +添加关键帧 | K | 选中任一动画属性后,将会在时间控制线所在位置添加关键帧,没有选中动画属性则忽略 +跳到上一个关键帧 | Ctrl/Cmd + Shift + | 将时间控制线移动到左侧最近的一个关键帧(在选中的属性轨道上或选中的节点上) +跳到下一个关键帧 | Ctrl/Cmd + Shift + | 将时间控制线移动到右侧最近的一个关键帧(选中的属性轨道上或选中的节点上) +多选关键帧 | Ctrl | 按住 Ctrl 点击关键帧可多选关键帧。再次按下 Ctrl 后也可以继续选取关键帧进行补充 +全选动画属性关键帧 | Ctrl/Cmd + A | 全选选中的动画属性上的所有关键帧 +复制选中的动画数据 | Ctrl/Cmd + C | 支持复制选中的关键帧、属性轨道、节点数据,同时只能复制一种数据,且按照这个顺序来决定复制优先级 +粘贴上一次复制的动画数据 | Ctrl/Cmd + V | 支持跨编辑器(限 v3.x)粘贴上一次复制的动画数据(例如动画关键帧数、动画属性、动画节点数据,并按照该排序依次检查当前可粘贴的数据)。 +取消选中的关键帧/事件帧/属性轨道 | Esc | - + +> **注意**:目前除了 **进入/退出动画编辑器**、**保存动画** 的快捷键是全局可用以外,其他快捷键都需要鼠标焦点在动画编辑器面板时才生效。 diff --git a/versions/4.0/zh/animation/animation-editor/2-node-list.png b/versions/4.0/zh/animation/animation-editor/2-node-list.png new file mode 100644 index 0000000000..e1f55f79a8 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/2-node-list.png differ diff --git a/versions/4.0/zh/animation/animation-editor/4-pro-list.png b/versions/4.0/zh/animation/animation-editor/4-pro-list.png new file mode 100644 index 0000000000..ed3b197dd7 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/4-pro-list.png differ diff --git a/versions/4.0/zh/animation/animation-editor/above-timeline.png b/versions/4.0/zh/animation/animation-editor/above-timeline.png new file mode 100644 index 0000000000..3debddb753 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/above-timeline.png differ diff --git a/versions/4.0/zh/animation/animation-editor/animation-panel.png b/versions/4.0/zh/animation/animation-editor/animation-panel.png new file mode 100644 index 0000000000..e702b7d5e2 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/animation-panel.png differ diff --git a/versions/4.0/zh/animation/animation-editor/animation-property-track.png b/versions/4.0/zh/animation/animation-editor/animation-property-track.png new file mode 100644 index 0000000000..0c33b10e25 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/animation-property-track.png differ diff --git a/versions/4.0/zh/animation/animation-editor/animation-timeline.png b/versions/4.0/zh/animation/animation-editor/animation-timeline.png new file mode 100644 index 0000000000..e1e325bbed Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/animation-timeline.png differ diff --git a/versions/4.0/zh/animation/animation-editor/frame.png b/versions/4.0/zh/animation/animation-editor/frame.png new file mode 100644 index 0000000000..4093b2aea4 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/frame.png differ diff --git a/versions/4.0/zh/animation/animation-editor/hide-node.png b/versions/4.0/zh/animation/animation-editor/hide-node.png new file mode 100644 index 0000000000..d5258d84f2 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/hide-node.png differ diff --git a/versions/4.0/zh/animation/animation-editor/key-info.png b/versions/4.0/zh/animation/animation-editor/key-info.png new file mode 100644 index 0000000000..75d00cf5ee Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/key-info.png differ diff --git a/versions/4.0/zh/animation/animation-editor/layout.gif b/versions/4.0/zh/animation/animation-editor/layout.gif new file mode 100644 index 0000000000..a882fe8ce8 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/layout.gif differ diff --git a/versions/4.0/zh/animation/animation-editor/menu-spacing.png b/versions/4.0/zh/animation/animation-editor/menu-spacing.png new file mode 100644 index 0000000000..e20bce5e81 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu-spacing.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_clips.png b/versions/4.0/zh/animation/animation-editor/menu_clips.png new file mode 100644 index 0000000000..48699315e8 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_clips.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_doc.png b/versions/4.0/zh/animation/animation-editor/menu_doc.png new file mode 100644 index 0000000000..ca00e9c0ba Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_doc.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_event.png b/versions/4.0/zh/animation/animation-editor/menu_event.png new file mode 100644 index 0000000000..e8eb1457a5 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_event.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_exit.png b/versions/4.0/zh/animation/animation-editor/menu_exit.png new file mode 100644 index 0000000000..2e6ae62e00 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_exit.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_first.png b/versions/4.0/zh/animation/animation-editor/menu_first.png new file mode 100644 index 0000000000..9d6a0f26fe Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_first.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_last.png b/versions/4.0/zh/animation/animation-editor/menu_last.png new file mode 100644 index 0000000000..258dbac7bf Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_last.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_next.png b/versions/4.0/zh/animation/animation-editor/menu_next.png new file mode 100644 index 0000000000..d4f1f50abc Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_next.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_play.png b/versions/4.0/zh/animation/animation-editor/menu_play.png new file mode 100644 index 0000000000..920a812f01 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_play.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_prev.png b/versions/4.0/zh/animation/animation-editor/menu_prev.png new file mode 100644 index 0000000000..3bd9d542f0 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_prev.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_save.png b/versions/4.0/zh/animation/animation-editor/menu_save.png new file mode 100644 index 0000000000..c1eec3cbcd Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_save.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_spacing_btn.png b/versions/4.0/zh/animation/animation-editor/menu_spacing_btn.png new file mode 100644 index 0000000000..9a732235a7 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_spacing_btn.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_stop.png b/versions/4.0/zh/animation/animation-editor/menu_stop.png new file mode 100644 index 0000000000..63b7867cc1 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_stop.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_time.png b/versions/4.0/zh/animation/animation-editor/menu_time.png new file mode 100644 index 0000000000..0193f7b9ff Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_time.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_time_frame.png b/versions/4.0/zh/animation/animation-editor/menu_time_frame.png new file mode 100644 index 0000000000..0154ab7379 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_time_frame.png differ diff --git a/versions/4.0/zh/animation/animation-editor/menu_time_times.png b/versions/4.0/zh/animation/animation-editor/menu_time_times.png new file mode 100644 index 0000000000..b3e16b326f Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/menu_time_times.png differ diff --git a/versions/4.0/zh/animation/animation-editor/move-line.gif b/versions/4.0/zh/animation/animation-editor/move-line.gif new file mode 100644 index 0000000000..d5ee2c1393 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/move-line.gif differ diff --git a/versions/4.0/zh/animation/animation-editor/mult-select.png b/versions/4.0/zh/animation/animation-editor/mult-select.png new file mode 100644 index 0000000000..f40f5ade3a Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/mult-select.png differ diff --git a/versions/4.0/zh/animation/animation-editor/node-operation.png b/versions/4.0/zh/animation/animation-editor/node-operation.png new file mode 100644 index 0000000000..13c7917ebe Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/node-operation.png differ diff --git a/versions/4.0/zh/animation/animation-editor/pro-operation.png b/versions/4.0/zh/animation/animation-editor/pro-operation.png new file mode 100644 index 0000000000..1d0d00172e Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/pro-operation.png differ diff --git a/versions/4.0/zh/animation/animation-editor/scale-canvas.gif b/versions/4.0/zh/animation/animation-editor/scale-canvas.gif new file mode 100644 index 0000000000..5961c9f5d3 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/scale-canvas.gif differ diff --git a/versions/4.0/zh/animation/animation-editor/scale.gif b/versions/4.0/zh/animation/animation-editor/scale.gif new file mode 100644 index 0000000000..cd091b90fb Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/scale.gif differ diff --git a/versions/4.0/zh/animation/animation-editor/set-pro.png b/versions/4.0/zh/animation/animation-editor/set-pro.png new file mode 100644 index 0000000000..8ab665f3be Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/set-pro.png differ diff --git a/versions/4.0/zh/animation/animation-editor/shortcuts-change.png b/versions/4.0/zh/animation/animation-editor/shortcuts-change.png new file mode 100644 index 0000000000..a45048436a Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/shortcuts-change.png differ diff --git a/versions/4.0/zh/animation/animation-editor/shortcuts-manager.png b/versions/4.0/zh/animation/animation-editor/shortcuts-manager.png new file mode 100644 index 0000000000..f18d8fca14 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/shortcuts-manager.png differ diff --git a/versions/4.0/zh/animation/animation-editor/shortcuts.png b/versions/4.0/zh/animation/animation-editor/shortcuts.png new file mode 100644 index 0000000000..74d05bfd13 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/shortcuts.png differ diff --git a/versions/4.0/zh/animation/animation-editor/time.png b/versions/4.0/zh/animation/animation-editor/time.png new file mode 100644 index 0000000000..4e4f4ae13c Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/time.png differ diff --git a/versions/4.0/zh/animation/animation-editor/times.png b/versions/4.0/zh/animation/animation-editor/times.png new file mode 100644 index 0000000000..cd00284823 Binary files /dev/null and b/versions/4.0/zh/animation/animation-editor/times.png differ diff --git a/versions/4.0/zh/animation/animation-event.md b/versions/4.0/zh/animation/animation-event.md new file mode 100644 index 0000000000..025d17911d --- /dev/null +++ b/versions/4.0/zh/animation/animation-event.md @@ -0,0 +1,50 @@ +# 添加动画事件 + +通过在动画时间轴的指定帧调用 **动画事件** 函数可以更好地充实动画剪辑。在动画时间轴某一帧上添加 **事件帧** 后,动画系统将会在动画执行到该帧时,根据事件帧中设置的触发函数名称去匹配动画根节点中对应的函数方法并执行。 + +若要通过脚本添加动画事件,详情请参考 [帧事件](animation-component.md#%E5%B8%A7%E4%BA%8B%E4%BB%B6)。 + +## 添加事件帧 + +在 **动画编辑器** 中添加事件帧包括以下两种方式: + +1. 将时间控制线拖动到需要添加事件帧的位置,然后点击 **菜单工具栏** 中的 ![add event](animation-editor/menu_event.png) 按钮,即可在 **动画时间轴** 上方添加事件帧。 + +2. 在动画时间轴上方区域点击右键,然后选择 **新建事件帧** 即可。 + +事件帧添加完成后,将鼠标移动到事件帧,颜色会从白色变成黄色。右键点击事件帧即可执行 **编辑**、**删除**、**复制粘贴** 等操作。同时也支持批量操作事件帧,在按住 Ctrl 的同时点击多个事件帧即可。 + +![add-event](animation-event/animation-event-menu.png) + +- **编辑**:用于打开事件编辑器,添加事件函数 +- **删除**:用于删除事件帧 +- **复制/粘贴**:事件帧数据的复制和粘贴,支持跨编辑器(v3.x)使用。 + +### 编辑事件帧 + +右键点击添加的事件帧并选择 **编辑** 或者直接双击,即可打开事件编辑器。在事件编辑器中可以手动输入需要触发的事件函数名称,触发时会根据这个函数名,去动画根节点的各个组件内匹配相应的函数方法,并对其进行调用,传入参数。 + +![event editor](animation-event/event-editor.png) + +- 1 — 用于添加新的触发函数 +- 2 — 用于保存事件函数 +- 3 — 填写需要触发的函数名称 +- 4 — 用于删除当前事件函数 +- 5 — 用于添加传入的参数,目前支持 **String**、**Number**、**Boolean** 三种类型 + + ![add animation event](animation-event/add-animation-event.png) + + - 1 — 用于添加传入的参数,可根据需要选择参数类型 + - 2 — 用于删除下方所有已添加的传入参数 + - 3 — 当鼠标移动到某一参数上时,便会出现该按钮,点击即可删除当前选中的参数 + +### 删除事件帧 + +在动画时间轴上方右键点击已经添加的事件帧(可多选),然后选择 **删除** 或者使用快捷键 Delete 即可删除该事件帧及所有的事件函数。 + +### 复制粘贴事件帧 + +事件帧及其事件函数的复制和粘贴,支持跨编辑器(v3.x)使用。复制粘贴包括以下两种使用方式: + +- 选中事件帧后(可多选),使用快捷键 Ctrl + CCtrl + V 即可进行复制粘贴。需要注意的是快捷键粘贴的位置将会以当前时间控制线所在的位置为起点。 +- 选中事件帧后(可多选),右键点击(任一)事件帧,在弹出的菜单中选择 **复制**,然后在动画时间轴上方点击右键,选择 **粘贴事件帧** 即可。 diff --git a/versions/4.0/zh/animation/animation-event/add-animation-event.png b/versions/4.0/zh/animation/animation-event/add-animation-event.png new file mode 100644 index 0000000000..4ae7ede0ff Binary files /dev/null and b/versions/4.0/zh/animation/animation-event/add-animation-event.png differ diff --git a/versions/4.0/zh/animation/animation-event/animation-event-menu.png b/versions/4.0/zh/animation/animation-event/animation-event-menu.png new file mode 100644 index 0000000000..2b8e934ad2 Binary files /dev/null and b/versions/4.0/zh/animation/animation-event/animation-event-menu.png differ diff --git a/versions/4.0/zh/animation/animation-event/event-editor.png b/versions/4.0/zh/animation/animation-event/event-editor.png new file mode 100644 index 0000000000..0b16cfcb69 Binary files /dev/null and b/versions/4.0/zh/animation/animation-event/event-editor.png differ diff --git a/versions/4.0/zh/animation/animation-keyFrames.md b/versions/4.0/zh/animation/animation-keyFrames.md new file mode 100644 index 0000000000..ac3bf0ec5a --- /dev/null +++ b/versions/4.0/zh/animation/animation-keyFrames.md @@ -0,0 +1,89 @@ +# 关键帧编辑视图 + +节点轨道显示区域与属性轨道显示区域,默认均为关键帧编辑视图的显示方式。在这种视图下,可以更方便的编辑整体的关键帧排布、添加以及删除关键帧。下面介绍在关键帧编辑视图下,支持的各种关键帧操作方法,了解这些方法技巧可以更快更方便地编辑动画剪辑。 + +## 选中关键帧 + +选中的关键帧会由蓝色变成白色,包括以下几种: + +- 单击动画属性轨道上的关键帧即可选中 +- 双击关键帧则会在选中关键帧的同时将时间控制线移动到当前关键帧所在位置 +- 单击节点在动画时间轴中的关键帧,即可同时选中节点的各个动画属性在同一位置上的所有关键帧。 + + ![choose keyframe](edit-animation-clip/choose-keyframe.png) + +同时也支持多选关键帧,包括以下几种方式: + +- 按住 Ctrl/Cmd 的同时点击多个关键帧 + +- 通过框选的方式选择多个连续的关键帧,方框左右两侧会分别显示首尾关键帧在动画时间轴中的帧数。 + + ![choose by box](edit-animation-clip/choose-by-box.png) + +- **选中任一属性轨道** 后,按下 Ctrl/Cmd + A 即可全选当前动画属性轨道上的所有关键帧。 + +## 添加关键帧 + +除了 **创建动画曲线** 中介绍的通过修改属性的方式来添加关键帧,还可以通过以下几种方式添加: + +1. 选中动画属性,使用快捷键 K,将会在在时间控制线所在位置添加关键帧,若没有选中动画属性则忽略 + +2. 移动时间控制线到需要添加关键帧的位置,点击动画属性右侧的 ![add keyframe](edit-animation-clip/add-keyframe-button.png) 按钮即可在当前动画属性轨道上添加关键帧。 + +3. 在动画属性的属性轨道中,右键点击需要添加关键帧的位置,然后在弹出的菜单中选择 **添加关键帧** 即可,菜单上同时也会显示当前添加关键帧位置的帧数。 + + ![add keyframe](edit-animation-clip/add-keyframe.png) + +4. 将动画属性对应的资源批量从 **资源管理器** 拖拽到属性轨道上时,将会按照工具栏中 ![spacing](edit-animation-clip/menu-spacing.png) 设置的数值为间隔大小,以资源被选中的顺序依次排列添加关键帧。 + +## 移除关键帧 + +1. 选中需要删除的关键帧(可多选),按下 Delete 键(Windows)或者 Cmd + Backspace(macOS)即可。 + +2. 选中需要删除的关键帧(可多选),然后点击右键,在弹出的菜单中选择 **移除关键帧** 即可。 + +3. 拖动时间控制线到需要移除的关键帧的位置或者直接 **双击** 关键帧,然后在动画属性列表中点击对应的动画属性右侧的 ![remove keyframe](edit-animation-clip/remove-key-btn.png) 按钮即可。 + +![remove keyframe](edit-animation-clip/remove-keyframes.gif) + +## 移动关键帧 + +在选中关键帧的同时拖动,即可将关键帧移动到想要的位置。 + +- 当拖动单个关键帧时,关键帧下方会出现一个白色小方框,显示移动过程中的帧数以及移动的距离。 +- 当拖动多个关键帧时,方框左右两侧会分别显示移动过程中首尾关键帧在动画时间轴中的帧数。 + +![move keyframe](edit-animation-clip/move-keyframes.gif) + +## 缩放关键帧 + +当选中多个关键帧时,首尾关键帧会显示两条控制辅助杆,拖动任意一个控制杆移动即可对选中的关键帧进行整体缩放。 + +![scale keyframe](edit-animation-clip/scale-keyframes.gif) + +## 间隔排列关键帧 + +选中多个关键帧后,点击上方菜单栏中的 ![spacing](edit-animation-clip/menu_spacing_btn.png) 间隔排列关键帧按钮,选中的关键帧便会以第一个关键帧为基准,以 ![spacing](edit-animation-clip/menu-spacing.png) 输入框中设置的数值为间隔帧数依次排列。 + +## 复制/粘贴关键帧 + +关键帧数据的复制和粘贴,支持跨节点、跨 Clip 使用。包括以下两种使用方式: + +- 选中关键帧后(可多选),使用快捷键 Ctrl + CCtrl + V 即可进行复制粘贴。需要注意的是快捷键粘贴的位置将会以当前时间控制线所在的位置为起点。 +- 选中关键帧后(可多选),右键点击(任一)关键帧,在弹出的菜单中选择 **复制关键帧**,然后在目标动画属性轨道上点击右键,选择 **粘贴关键帧** 即可在当前动画属性轨道上粘贴关键帧。 + +以上两种粘贴方式在复制粘贴多个属性轨道数据时有所差异,区别主要在于: + +- 使用快捷键粘贴关键帧数据时,将会在复制关键帧的动画属性轨道上按照复制的关键帧顺序一一粘贴 + + ![copy keyFrames1](edit-animation-clip/copy-keyframes2.gif) + +- 在目标属性轨道上点击右键选择粘贴时,只有目标属性轨道上会被粘贴上复制的数据。 + + ![copy keyFrames1](edit-animation-clip/copy-keyframes1.gif) + +分量属性轨道(例如动画属性 `position` 下的 x、y、z 属性)作为单独的属性轨道也会遵循这个规则。如果在主轨道(例如动画属性 `position`)上复制了关键帧数据后,在目标分量轨道上点击右键进行粘贴,那么只有目标分量轨道才会粘贴上关键帧数据。请务必根据实际需要粘贴的位置来复制对应数据,以免产生预期之外的结果。 + +## 嵌入播放器 + +在 v3.6 中我们新增了嵌入播放器,用于在播放动画的同时播放其他特效或者动画,开发者可移步 [嵌入播放器](./embeded-player.md) 查看详情。 diff --git a/versions/4.0/zh/animation/animation-state.md b/versions/4.0/zh/animation/animation-state.md new file mode 100644 index 0000000000..4c87b08ab8 --- /dev/null +++ b/versions/4.0/zh/animation/animation-state.md @@ -0,0 +1,94 @@ +# 动画状态 + +动画剪辑仅描述某一类对象的动画数据,例如角色的跑、走、跳等,但并未绑定具体要执行动画的对象。动画状态便是用于控制在某个对象上使用的动画剪辑,类似于播放机,除了提供动画组件也有的简单的控制函数外,还提供了更多的动画信息以及动画控制接口,允许对动画播放进行调速、设置循环模式等控制。一个动画剪辑可以同时被多个动画状态使用。 + +动画状态由 [类 `AnimationState`](%__APIDOC__%/zh/class/AnimationState) 管理。 + +## 设置播放速度 + +首先可以通过 [getState()](%__APIDOC__%/zh/class/Animation?id=getState) 获取动画状态: + +```ts +// 获取动画组件 +const animationComponent = node.getComponent(Animation); +// 获取动画组件上的动画剪辑 +const [ idleClip, runClip ] = animationComponent.clips; + +// 获取 idleClip 的动画状态 +const idleState = animationComponent.getState(idleClip.name); +``` + +然后设置动画播放的速度: + +```ts +// 以二倍速播放 idleClip 动画 +animationComponent.getState('idle').speed = 2.0; // speed 值越大速度越快,值越小则速度越慢 +``` + +动画状态也提供了 `play()`、`pause()`、`resume()`、`stop()` 等用于播放控制的方法,详情可参考下文 **播放状态** 部分的内容。 + +## 播放时间 + +动画状态记录了动画的 **累计播放时间**。初始时累计播放时间为 0。当动画自然播放时,时间会不断累计。例如,当动画循环播放时,刚好第二次循环完毕后,累计播放时间将为 **动画周期 * 2**。 + +任意时刻动画所处的播放位置称为 **进度时间**,因此进度时间总是在 `[0, 动画周期]` 范围内。 + +- **累计播放时间** 由 `AnimationState` 的 [time](%__APIDOC__%/zh/class/AnimationState?id=time) 字段获取,并且可以显式设置。 +- **进度时间** 由 `AnimationState` 的 [current](%__APIDOC__%/zh/class/AnimationState?id=wrapMode) 字段获取,是 **只读** 的。 + +动画播放的循环模式与循环次数决定了累计播放至某一时间时动画的进度时间,不管 **累计播放时间** 因为时间的推移而增加还是因为直接设置而更改,**进度时间** 都会相应发生改变。 + +## 循环模式与循环次数 + +动画可以播放到结尾就停止,或者一直循环播放,或者也可以先播放到结尾再从结尾播放到开头如此循环,这些统称为循环模式,由枚举 [`AnimationClip.WrapMode`](%__APIDOC__%/zh/class/AnimationClip?id=wrapMode) 表示,包括以下几种: + +| 循环模式 | 说明 | +| :--- | :--- | +| `AnimationClip.WrapMode.Normal` | 从开头播放至结尾后停止。 | +| `AnimationClip.WrapMode.Loop` | 不断地从开头播放至结尾。 | +| `AnimationClip.WrapMode.PingPong` | 从开头播放至结尾后,再从结尾反向播放至开头,如此循环往复。 | + +除此之外,上表中的每种循环模式还存在对应的 **反向** 循环模式: + +| 循环模式 | 说明 | +| :--- | :--- | +| `AnimationClip.WrapMode.Reverse` | 从结尾播放至开头后停止。 | +| `AnimationClip.WrapMode.LoopReverse` | 不断地从结尾播放至开头。 | +| `AnimationClip.WrapMode.PingPongReverse` | 从结尾播放至开头后,再从开头反向播放至结尾,如此循环往复。 | + +动画状态的初始循环模式将从动画剪辑中读取。需要改变动画状态的循环模式时,简单地设置动画状态的 `wrapMode` 字段即可。 + +> **注意**:设置循环模式时会重置动画状态的 **累计播放时间**。 + +除 `AnimationClip.WrapMode.Normal` 和其对应的 `AnimationClip.WrapMode.Reverse` 外(它们可以理解为单次循环),其余的循环模式执行的都是无限次循环。无限次循环需要与 `AnimationState` 的 `repeatCount` 配合使用才能达到效果,并且可以通过 `repeatCount` 字段来设置和获取循环的次数。 + +当动画循环模式为: +- 单次循环模式:`repeatCount` 将被设置为 **1**。 +- 无限次循环模式:`repeatCount` 将被设置为 `Number.Infinity`,即无限循环。 + +> **注意**:设置循环次数应该在设置循环模式之后进行,因为重新设置循环模式时会重置循环次数。 + +## 播放控制 + +动画状态提供了以下几种方法用于控制动画的播放、暂停、恢复和停止: + +| 方法 | 说明 | +| :--- | :--- | +| `play()` | 重置播放时间为 0 并开始播放动画。 | +| `pause()` | 暂停动画。 | +| `resume()` | 从当前时间开始继续播放动画。 | +| `stop()` | 停止播放动画。 | + +也可以通过以下字段查询动画的播放状态: + +| 字段(只读) | 说明 | +| :--- | :--- | +| `isPlaying` | 动画是否处于播放状态。 | +| `isPaused` | 动画是否处于暂停状态。 | +| `isMotionless` | 动画是否处于暂停状态或者已被停止。 | + +播放控制与播放状态之间的关系如下图所示: + +![Playback control](./animation-state/playback-control.svg) + +通过动画状态可以获取到所有动画的信息,以便利用这些信息来判断需要做哪些事,更多接口请参考 [类 `AnimationState`](%__APIDOC__%/zh/class/AnimationState)。 diff --git a/versions/4.0/zh/animation/animation-state/playback-control.dot b/versions/4.0/zh/animation/animation-state/playback-control.dot new file mode 100644 index 0000000000..267f5b44e3 --- /dev/null +++ b/versions/4.0/zh/animation/animation-state/playback-control.dot @@ -0,0 +1,22 @@ +strict digraph { + graph [fontname = "Courier New"]; + node [fontname = "Courier New"]; + edge [fontname = "Courier New"]; + + entry [labe="",shape="diamond"] + { + rank=same; + playing [label="isPlaying"] + playingPaused [label="isPlaying &&\nisPaused"] + } + stopped [label=""] + + entry -> stopped + + stopped -> playing [label="play()"] + playing -> playingPaused [label="play()/resume()"] + playingPaused -> playing [label="pause()"] + + playing -> stopped [label="stop()"] + playingPaused -> stopped [label="stop()"] +} \ No newline at end of file diff --git a/versions/4.0/zh/animation/animation-state/playback-control.svg b/versions/4.0/zh/animation/animation-state/playback-control.svg new file mode 100644 index 0000000000..5b7cbee67e --- /dev/null +++ b/versions/4.0/zh/animation/animation-state/playback-control.svg @@ -0,0 +1,78 @@ + + + + + + + + + +entry + +entry + + + +stopped + +<stopped> + + + +entry->stopped + + + + + +playing + +isPlaying + + + +playingPaused + +isPlaying && +isPaused + + + +playing->playingPaused + + +play()/resume() + + + +playing->stopped + + +stop() + + + +playingPaused->playing + + +pause() + + + +playingPaused->stopped + + +stop() + + + +stopped->playing + + +play() + + + diff --git a/versions/4.0/zh/animation/animation.md b/versions/4.0/zh/animation/animation.md new file mode 100644 index 0000000000..f98aa6023a --- /dev/null +++ b/versions/4.0/zh/animation/animation.md @@ -0,0 +1,32 @@ +# 动画编辑器 + +Creator 支持在 **动画编辑器** 中直接创建、编辑和预览动画剪辑,除了可以对节点基本属性进行动画化,还支持对材质和部分组件的属性进行动画化,并且可以通过调用 [动画事件](animation-event.md) 的事件函数来充实动画剪辑。 + +在使用 **动画编辑器** 制作动画之前需要先为节点添加 **动画组件**,并为动画组件挂载 **动画剪辑** 后才可以进行编辑。详情请参考 [创建 Animation 组件和动画剪辑](animation-create.md)。 + +![animation play](animation/animation-play.gif) + +## 动画编辑模式 + +只有在 **动画编辑模式** 下才可以编辑动画剪辑中的动画数据。在动画编辑模式下,无法对节点进行添加/删除/重命名等操作,并且没有记录在动画关键帧内的属性修改,在退出动画编辑模式后也会被还原。 + +当处于动画编辑模式时,不参与动画数据编辑的节点在 **层级管理器** 中为置灰状态。在 **动画编辑器** 中选中节点时,**层级管理器** 中也会相应地跳转到对应的节点,反之亦然。 + +![select node](./animation/select_node.gif) + +**打开编辑模式包括以下两种方式**: + +- 在 **层级管理器** 中选中一个带有 Animation 组件,并且组件中包含一个以上 clip 文件的节点,然后在 **动画编辑器** 中点击 **进入动画编辑模式** 按钮。 +- 快捷键 Ctrl/Cmd + E + +**保存编辑后的动画数据包括以下三种方式**: + +- 点击 **动画编辑器** 右上角的 ![save](./animation/save.png) 保存按钮 +- 点击 **场景编辑器** 左上角的 **保存** 按钮 +- 快捷键 Ctrl/Cmd + S + +**退出编辑模式包括以下三种方式**: + +- 点击 **动画编辑器** 右上角的 ![exit](./animation/exit.png) 退出按钮 +- 点击 **场景编辑器** 左上角的 **关闭** 按钮 +- 快捷键 Ctrl/Cmd + E diff --git a/versions/4.0/zh/animation/animation/animation-play.gif b/versions/4.0/zh/animation/animation/animation-play.gif new file mode 100644 index 0000000000..a0ae8b7ae1 Binary files /dev/null and b/versions/4.0/zh/animation/animation/animation-play.gif differ diff --git a/versions/4.0/zh/animation/animation/exit.png b/versions/4.0/zh/animation/animation/exit.png new file mode 100644 index 0000000000..3b0d07833c Binary files /dev/null and b/versions/4.0/zh/animation/animation/exit.png differ diff --git a/versions/4.0/zh/animation/animation/keyframe.png b/versions/4.0/zh/animation/animation/keyframe.png new file mode 100644 index 0000000000..392398ed28 Binary files /dev/null and b/versions/4.0/zh/animation/animation/keyframe.png differ diff --git a/versions/4.0/zh/animation/animation/save.png b/versions/4.0/zh/animation/animation/save.png new file mode 100644 index 0000000000..8324f2a20a Binary files /dev/null and b/versions/4.0/zh/animation/animation/save.png differ diff --git a/versions/4.0/zh/animation/animation/select_node.gif b/versions/4.0/zh/animation/animation/select_node.gif new file mode 100644 index 0000000000..0ea70b2d35 Binary files /dev/null and b/versions/4.0/zh/animation/animation/select_node.gif differ diff --git a/versions/4.0/zh/animation/animation/sockets-attach0.png b/versions/4.0/zh/animation/animation/sockets-attach0.png new file mode 100644 index 0000000000..4850285819 Binary files /dev/null and b/versions/4.0/zh/animation/animation/sockets-attach0.png differ diff --git a/versions/4.0/zh/animation/animation/sockets-attach1.gif b/versions/4.0/zh/animation/animation/sockets-attach1.gif new file mode 100644 index 0000000000..58e6468c53 Binary files /dev/null and b/versions/4.0/zh/animation/animation/sockets-attach1.gif differ diff --git a/versions/4.0/zh/animation/animation/useBakedAnimation.png b/versions/4.0/zh/animation/animation/useBakedAnimation.png new file mode 100644 index 0000000000..7f78149a3d Binary files /dev/null and b/versions/4.0/zh/animation/animation/useBakedAnimation.png differ diff --git a/versions/4.0/zh/animation/curve-editor.md b/versions/4.0/zh/animation/curve-editor.md new file mode 100644 index 0000000000..c8e75c66ab --- /dev/null +++ b/versions/4.0/zh/animation/curve-editor.md @@ -0,0 +1,128 @@ +# 曲线编辑器 + +曲线编辑器主要用于编辑关键帧之间变化的曲线轨迹,在编辑器中的 [动画](./animation-curve.md) 和 [粒子](../particle-system/editor/curve-editor.md) 都使用了曲线编辑器来编辑关键帧曲线。 + +每一条曲线上的关键点,**横坐标** 表示关键帧时间/帧数,**纵坐标** 表示当前曲线属性在对应时间上的值。曲线编辑器支持同时编辑多条曲线,但在粒子的使用场景内目前只支持单条曲线编辑。 + +## 缩放移动曲线显示区域 + +- 直接在曲线显示区域内滚动鼠标滚轮,即可同时放大或者缩小横纵时间轴的显示比例。 +- 按住鼠标右键不放拖动也可以直接平移当前的显示区域范围。 + +除此之外还可以结合快捷键来单独控制横纵时间轴的平移缩放: + +- 按住 Shift 不放后滚动鼠标滚轮,可以向左或向右 **平移** 曲线视图区域; +- 按住 Ctrl/Cmd + Shift 不放后滚动鼠标滚轮,可以向左或向右 **缩放** 曲线视图区域; +- 按住 Alt/Option 不放后滚动鼠标滚轮,可以向上或向下 **平移** 曲线视图区域; +- 按住 Ctrl+Alt / Cmd+Option 不放后滚动鼠标滚轮,可以向上或向下 **缩放** 曲线视图区域; + +## 编辑曲线轨迹 + +### 添加关键帧 + +1. 当只显示一条曲线轨迹时,可以在任意空白处右键点击,然后在弹出的菜单中选择 **新建关键帧** 即可。 + + 以粒子编辑器中的曲线为例: + + ![add-keyframe-to-curve.png](animation-curve/add-keyframe-to-curve.png) + +2. 对于动画组件,可以点击下图中的红框内的按钮添加关键帧: + + ![add-keyframe.png](animation-curve/add-keyframe.png) + +### 选择关键帧 + +直接点击关键帧,或者框选关键帧即可选中关键帧,被选中的关键帧将会显示为黄色。单独点选关键帧时,左右坐标轴上会显示当前关键帧的数值。 + +![select-key](animation-curve/select-key.gif) + +### 移动关键帧 + +单击选中关键帧后直接拖动关键帧即可。 + +![move-keys](animation-curve/move-keys.gif) + +### 缩放关键帧 + +框选多个关键帧后,拖动选中区域的边框可以整体缩放关键帧位置。 + +![scale-keys](animation-curve/scale-keys.gif) + +### 间隔排列关键帧 + +框选多个关键帧后,点击选中区域后,在弹出的菜单里选择 **间隔排列选中关键帧** 即可。 + +### 移除关键帧 + +选中关键帧后,直接按下 Delete 键(Windows)或者 Cmd + Backspace(macOS) 或者右键点击选中区域,在弹出的菜单中选择 **删除** 即可。 + +### 复制粘贴关键帧 + +- 复制关键帧 + + 选中关键帧后,右键点击选中区域并选择 **复制**,或者按下 Ctrl/Cmd + C 即可。 + +- 粘贴关键帧 + + 在任意空白处,点击选中区域右键并选择 **粘贴**,或者直接使用快捷键 Ctrl/Cmd + V 即可。 + +### 编辑关键帧数据 + +在曲线编辑器内,关键帧的数值已经用可视化的方式显示,直接上下左右拖动关键帧本身实际上就是在修改关键帧数据。在一些需要精确控制关键帧数值的情况时,可以直接右键点击关键帧,在弹出的菜单中选择 **编辑关键帧数据** 后,将会显示一块编辑区域,修改后点击空白处即可应用。 + +![edit-key](animation-curve/edit-key.png) + +### 插值模式 + +右键点击关键帧,在弹出的菜单中选择 **插值模式** 的对应值即可。插值模式将会影响当前关键帧到下一帧之间数值的变化方式,不同选项值会有不同的显示效果,具体可以参考下文的选项值介绍。 + +![interopMode](./animation-curve/interopMode.png) + +![interopMode Key](./animation-curve/interopMode-key.gif) + +插值模式包括以下几个选项值: + +- **Linear**:线性模式,线性变化当前帧到下一关键帧之间的值,切线方向指向下一关键帧; + + ![linear](animation-curve/linear.png) + +- **Constant**:常量模式,当前关键帧到下一个关键帧之间的变化保持为当前关键帧值不变; + + ![constant](animation-curve/constant.png) + +- **Cubic**:自由曲线模式,当前帧到下一关键帧之间的值呈曲线变化。只有在当前模式下,点选关键帧才会出现左右切线控制杆; + + ![cubic](animation-curve/cubic.png) + +### 编辑关键帧切线 + +一个关键帧有左右两条切线,切线的长度和方向将会控制关键帧之间曲线的形状。**Cubic** 插值模式下,直接拖动左右两侧的切线控制点即可编辑。在不同的切线权重模式下,切线控制杆的可操作范围不同,具体可以参考切线权重模式的介绍。 + +#### 切线权重模式 + +右键点击关键帧,即可看到插值模式的修改选项菜单,点击修改即可。仅在 **插值模式** 为 **Cubic** 的情况下,切线权重模式的值才会生效。 + +切线选项范围实际上主要是两种: + +- **无权重模式**。关键帧之间的变化采用 Hermite 算法,曲线的走向根据关键帧的左右切线方向而定。因而此时,关键帧的左右切线控制杆只能更改切线方向。此时的切线控制点只是为了方便修改方向而绘制显示的,因而缩放此时的曲线视图区域,切线控制杆长度也不会发生变化。 + +- **权重模式**。关键帧之间的变化采用贝塞尔算法,曲线的走向也受关键帧左右切点控制点影响。左切线控制点可以任意移动调整控制杆长度,此时的左切线控制点是真实的坐标点,缩放曲线视图区域控制杆也会同步缩放。 + + ![interopMode Key](./animation-curve/tangentWeightMode-key.gif) + +关键帧有左右两切线,切线权重模式有以下选项值: + +![tangentWeightMode](./animation-curve/tangentWeightMode.png) + +- `None`:左右切线均为无权重模式。 +- `Left`:左切线为权重模式,右切线为无权重模式。 +- `Right`:右切线为权重模式,左切线为无权重模式。 +- `Both`:左右切线均为权重模式。 + +#### Broken + +默认情况下关键帧的左右切线斜率是保持一致的,调整任何一边的切线控制杆另一边都会跟随调整,这让我们更容易调整出过渡更加自然的曲线。但在一些情况下,我们希望曲线并不总是平和过渡,此时便可以右键点击关键帧,然后选择菜单中的 Broken 选项,断开关键帧左右控制杆的关联,断开后即可分开调整关键帧的左右控制杆。 + +### 上下移动曲线 + +双击选中整条曲线后可以直接上下拖动整条曲线。 diff --git a/versions/4.0/zh/animation/curve-example-test.html b/versions/4.0/zh/animation/curve-example-test.html new file mode 100644 index 0000000000..2105cefba7 --- /dev/null +++ b/versions/4.0/zh/animation/curve-example-test.html @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/versions/4.0/zh/animation/curve-example.js b/versions/4.0/zh/animation/curve-example.js new file mode 100644 index 0000000000..44445316cc --- /dev/null +++ b/versions/4.0/zh/animation/curve-example.js @@ -0,0 +1,95 @@ + +(() => { + /** + * @param {HTMLCanvasElement} canvas + * @param {{xAxisText?: string; yAxisText?: string;}} config + */ + window.drawCurve = (canvas, countGenPoint, config) => { + config = config || {}; + + const xrange = { min: 0.1, max: 0.9 }; + const yrange = { min: 0.1, max: 0.8 }; + const xAxisLine = { start: 0.0 * canvas.width, end: 0.7 * canvas.width }; // In pixel. + const yAxisLine = { start: 0.7 * canvas.height, end: 0.1 * canvas.height }; // In pixel. + const xAxisLength = xAxisLine.end - xAxisLine.start; + const yAxisLength = yAxisLine.start - yAxisLine.end; + + const generateIncresingNumbers = (count, min, max) => { + const intervals = new Array(count); + let sum = 0; + for (let i = 0; i < count; ++i) { + const interval = Math.random(); + intervals[i] = interval; + sum += interval; + } + const extent = max - min; + let sumNi = 0; + return intervals.map((interval) => { + const ni = interval / sum; + const result = min + sumNi * extent; + sumNi += ni; + return result; + }); + }; + + const context = canvas.getContext('2d'); + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + context.fill(); + + context.strokeStyle = 'black'; + + const xs = generateIncresingNumbers(countGenPoint, xrange.min, xrange.max); + const points = xs.map((x) => { + return { + x, + y: yrange.min + (yrange.max - yrange.min) * Math.random(), + }; + }); + const pointPixels = points.map((point) => { + return { + x: xAxisLine.start + point.x * xAxisLength, + y: yAxisLine.start - point.y * yAxisLength, + }; + }); + + // Draw axises. + context.moveTo(xAxisLine.start, yAxisLine.start); + context.lineTo(xAxisLine.end, yAxisLine.start); + context.moveTo(xAxisLine.start, yAxisLine.start); + context.lineTo(xAxisLine.start, yAxisLine.end); + + // Draw curve. + if (pointPixels.length > 1) { + context.moveTo(pointPixels[0].x, pointPixels[0].y); + for (let iPoint = 1; iPoint < points.length; ++iPoint) { + context.lineTo(pointPixels[iPoint].x, pointPixels[iPoint].y); + } + } + + context.stroke(); + + // Draw frames. + context.setLineDash([1, 3]); + pointPixels.forEach((pointPixel) => { + context.moveTo(pointPixel.x, pointPixel.y); + context.lineTo(pointPixel.x, yAxisLine.start); + }); + context.stroke(); + + // Draw points. + context.fillStyle = 'red'; + pointPixels.forEach((pointPixel) => { + const rectSize = 4; + context.fillRect(pointPixel.x - rectSize / 2, pointPixel.y - rectSize / 2, rectSize, rectSize); + }); + + if (config.xAxisText) { + context.fillText(config.xAxisText, xAxisLine.end + 1, yAxisLine.start); + } + + if (config.yAxisText) { + context.fillText(config.yAxisText, xAxisLine.start, yAxisLine.end - 1); + } + }; +})(); \ No newline at end of file diff --git a/versions/4.0/zh/animation/easing-method-example.js b/versions/4.0/zh/animation/easing-method-example.js new file mode 100644 index 0000000000..2fa61f2774 --- /dev/null +++ b/versions/4.0/zh/animation/easing-method-example.js @@ -0,0 +1,81 @@ + +(() => { + const makeOutIn = (infx, outfx) => { + return (k) => { + if (k < 0.5) { + return outfx(k * 2) / 2; + } + return infx(2 * k - 1) / 2 + 0.5; + }; + }; + + const quadInFx = (k) => k * k; + + const quadOutFx = (k) => k * (2 - k); + + const methods = [ + {name: 'linear', fx: ((k) => k),}, + {name: 'constant', fx: ((k) => 0)}, + {name: 'quadIn', fx: quadInFx}, + {name: 'quadOut', fx: quadOutFx}, + {name: 'quadInOut', fx: makeOutIn(quadInFx, quadOutFx)}, + {name: 'quadOutIn', fx: ((k) => k)}, + ]; + + window.initializeEasingMethodsExample = (parent) => { + const table = parent.appendChild(document.createElement('table')); + const showcases = []; + for (const method of methods) { + const { name, fx } = method; + const tr = table.appendChild( + document.createElement('tr')); + + const label = tr.appendChild(document.createElement('td')). + appendChild(document.createElement('label')); + label.textContent = name; + + const image = tr.appendChild(document.createElement('td')). + appendChild(document.createElement('img')); + image.src = "https://forum.cocos.org/images/logo.png"; + + const slider = tr.appendChild(document.createElement('td')). + appendChild(document.createElement('input')); + slider.type = 'range'; + slider.min = 0; + slider.max = 100; + slider.value = 0; + + showcases.push({ + label, + image, + fx, + slider, + }); + } + + const firstTime = Date.now(); + let duration = 3000; + setInterval(() => { + const timePast = Date.now() - firstTime; + const ratio = (timePast % duration) / duration; + for (const showcase of showcases) { + const mappedRatio = showcase.fx(ratio); + showcase.image.style.opacity = (1.0- mappedRatio); + showcase.slider.value = 100 * mappedRatio; + } + }, 16); + }; +})(); + +(() => { + let easingMethodsExampleInitialized = false; + window.onEasingMethodExampleButtonClicked = () => { + const panel = document.getElementById('easing-method-example-panel'); + if (!easingMethodsExampleInitialized) { + easingMethodsExampleInitialized = true; + initializeEasingMethodsExample(panel); + } else { + panel.hidden = !panel.hidden; + } + }; +})(); diff --git a/versions/4.0/zh/animation/edit-animation-clip.md b/versions/4.0/zh/animation/edit-animation-clip.md new file mode 100644 index 0000000000..9e3f7e5e6d --- /dev/null +++ b/versions/4.0/zh/animation/edit-animation-clip.md @@ -0,0 +1,138 @@ +# 编辑动画剪辑 + +在节点的动画组件上挂载了动画剪辑后,点击 **进入动画编辑模式** 或者使用快捷键 Ctrl/Cmd + E 进入动画编辑模式,便可以在动画剪辑中添加关键帧数据,以此实现节点的动画化。在编辑动画剪辑之前请先 [熟悉动画编辑器](animation-editor.md)。 + +一个动画剪辑内可能包含了多个节点(节点及其子节点),每个节点上可挂载多个动画属性。通过对节点进行移动、旋转、缩放等操作,便会在当前选中节点相对应的动画属性上添加关键帧,动画属性上添加的所有关键帧在对应的动画属性中显示为线性轨迹的清单模式,我们可以称之为动画曲线。 + +## 创建动画曲线 + +在添加关键帧之前需要先了解一下动画属性,动画属性包括了节点自有的 `position`、`rotation`、`scale` 等属性,也包含了组件 Component 中自定义的属性。组件包含的属性前会加上组件的名字,比如 `cc.Sprite.spriteFrame`。 + +点击 **属性列表** 区域右上角的 **+** 按钮即可根据需要添加动画属性,根据节点类型的不同,可添加的动画属性也有所不同。已添加的动画属性则为置灰状态,不可重复添加。 + +![add property](edit-animation-clip/add-property.png) + +添加了动画属性后便可以在右侧的属性轨道上添加关键帧了。当节点及其动画属性在列表中显示为蓝色时,表示确定该节点的属性为当前创建关键帧的目标对象。然后在 **动画编辑器** / **属性检查器** / **场景编辑器** 中修改对应属性时,便会在动画属性右侧时间轴的时间控制线所在位置上生成一个蓝色实心的菱形,便是关键帧。(也可以通过该方法修改选中的关键帧数据) + +![add keyframe](edit-animation-clip/add-keyframe.gif) + +更多关于动画剪辑的设计以及如何通过代码程序化控制,请参考 [程序化编辑动画剪辑](use-animation-curve.md)。 + +### 编辑 Sprite 动画 + +接下来我们以创建 Sprite 动画为例,来看一下具体的操作流程。 + +1. 创建 Sprite 节点 + + 在 **层级管理器** 中创建一个 Sprite 节点。或者在节点上添加 Sprite 组件,选中节点后在 **属性检查器** 中点击 **添加组件** 按钮,选择 **2D -> Sprite** 即可。 + +2. 在节点上添加 Animation 组件,并挂载 Clip 文件,然后进入动画编辑模式。详情可参考 [创建 Animation 组件和动画剪辑](animation-create.md)。 + +3. 在属性列表中添加 `cc.Sprite.spriteFrame` 动画属性 + + 点击属性列表右上角的 **+** 按钮,然后选择 **cc.Sprite -> spriteFrame** 即可添加一个 `cc.Sprite.spriteFrame` 动画属性。 + + ![add SpriteFrame](edit-animation-clip/add-spriteframe.gif) + +4. 添加关键帧 + + 从 **资源管理器** 中将 spriteFrame 资源拖拽到 `cc.Sprite.spriteFrame` 动画属性右侧的属性轨道上,再将下一关键帧需要显示的 spriteFrame 拖到指定位置,或者在属性轨道上方的属性框中选择所需的 spriteFrame。然后点击播放就可以预览刚刚创建的动画了。 + + ![add SpriteFrame](edit-animation-clip/animation-sprite.gif) + +## 关键帧数据编辑 + +### 节点数据常见操作 + +动画剪辑通过节点的名字定义动画数据的位置,本身忽略了根节点,其余的子节点通过与根节点的相对路径索引找到对应的数据。 + +目前支持在 **动画编辑器** 的 **节点列表** 区域右键点击节点来选择清空节点数据、迁移节点数据,以及复制粘贴节点数据。 + +![node operation](edit-animation-clip/node-operation.png) + +#### 清空节点数据 + +右键点击需要清空所有动画数据(关键帧)的节点,然后选择 **清空数据**,在弹出的窗口中点击 **清除** 即可。 + +#### 迁移节点数据 + +右键点击需要迁移所有动画数据的节点,然后选择 **迁移数据**,节点便会显示动态的虚线外框,当鼠标移动到其他节点时,便会显示“迁移数据到该节点”的提示,单击后在弹出的窗口选择 **迁移** 即可,若不需要迁移则选择 **取消** 即可。 + +![move data](edit-animation-clip/move-data.png) + +> **注意**:节点数据迁移默认会覆盖原节点上的数据 + +因为动画剪辑会记录所有参与动画数据编辑的节点路径信息,所以当节点信息发生变化(例如节点重命名/删除/移动位置),与原本保存在动画剪辑中的不一致时,**动画编辑器** 便会将其在节点列表中显示为黄色的不可编辑的丢失状态,关键帧也是置灰的不可编辑状态。此时便可以通过节点的 **迁移数据** 功能将丢失节点的动画数据迁移到其他节点上。 + +![change node](edit-animation-clip/change-node.png) + +#### 复制粘贴节点数据 + +节点动画数据的复制和粘贴,支持跨编辑器(v3.x)使用。 + +- **复制**:在节点列表中选中要复制数据的节点,点击右键并选择 **复制数据**,或者直接使用快捷键 Ctrl/Cmd + C 即可。 +- **粘贴**:在节点列表中选中要粘贴动画数据的目标节点,点击右键并选择 **粘贴数据**,或者直接使用快捷键 Ctrl/Cmd + V 即可。 + +> **注意**:粘贴功能不支持自动创建动画属性,因此复制和粘贴的节点至少要有一个相同的动画属性,如果没有,请预先创建。当在同一个节点上粘贴多个节点的动画数据时,重叠部分的动画数据会被后者覆盖,差异部分则会互相融合。 + +#### 编辑同名子节点数据 + +在父节点的动画剪辑中编辑动画数据时也可以对子节点进行动画编辑,动画数据都会保存到父节点的动画剪辑中,用于实现类似人物模型的手以不同的速度跟随身体一起移动等效果。但若父节点下同时包含多个同名子节点,只支持对第一个同名子节点进行编辑,其他的同名字节点会置灰为不可编辑状态。 + +![same name node](edit-animation-clip/same-name-node.png) + +但分别为同名子节点添加动画组件和动画剪辑并单独进行编辑是支持的。 + +### 属性数据常见操作 + +右键点击动画属性或者单击轨道右侧的 ![property](edit-animation-clip/set-pro.png) 按钮即可选择 **移除当前属性轨道**、**清空关键帧**,或者 **复制粘贴当前属性轨道**。 + +![property operation](edit-animation-clip/pro-operation.png) + +- 移除当前动画属性:移除当前选中的动画属性,包括该轨道上的所有关键帧。或者也可以使用快捷键 Delete 键(Windows)或者 Cmd + Backspace(macOS)。 + +- 清空关键帧数据:删除当前选中的动画属性上的所有关键帧 + +- 复制/粘贴当前动画属性:复制当前选中的动画属性上的所有动画数据,并粘贴到目标动画属性上,支持使用快捷键 Ctrl/Cmd + CCtrl/Cmd + V。粘贴时,重叠部分的关键帧会被后者覆盖,差异部分则会互相融合。 + + 目前支持在不同编辑器(v3.x)之间复制/粘贴动画属性数据。动画数据的复制粘贴仅支持 `ccType` 同类型数据。 + +与节点一样,动画属性也存在丢失的可能。例如在 **属性检查器** 中移除了与动画属性对应的属性,则在动画属性列表中便会将其显示为黄色的不可编辑的丢失状态,关键帧也是置灰的不可编辑状态。在这种情况下可以退出编辑模式后为对应节点添加相关属性或是直接移除该丢失的属性轨道。 + + ![change property](edit-animation-clip/change-pro.png) + +### 节点 / 属性轨道关键帧数据编辑 + +动画编辑器的关键帧有两种视图显示方式:**关键帧编辑视图与曲线编辑视图**。进入动画编辑器后,默认是关键帧模式,通过点击属性列表边上的切换视图按钮 ![curve btn](animation-curve/curve-btn.png) 可以将属性轨道区域的关键帧切换到曲线编辑视图,再次点击即可恢复关键帧编辑视图。 + +#### 关键帧编辑模式 + +关键帧编辑模式的一些关键帧常见操作方式,请参见 [关键帧编辑模式](./animation-keyFrames.md) 的文档说明。 + +#### 曲线编辑模式 + +曲线编辑模式的一些关键帧操作细节,请参见 [曲线编辑模式](./animation-curve.md) 的文档说明。 + +## 动画剪辑属性设置 + +当前动画剪辑在 **动画编辑器** 上可直接设置的属性包括:**WrapMode**、**Sample**、**Speed** 和 **Duration**。 + +![set AnimationClip](edit-animation-clip/set-animation-clip.png) + +- **WrapMode**:用于设置当前动画剪辑播放的循环模式,目前包括: + - Default:效果与 Normal 一致 + - Normal:正向单次播放 + - Loop:循环正向播放 + - PingPong:以先正向播放再逆向播放的方式循环播放 + - Reverse:反向单次播放 + - LoopReverse:循环反向播放 + + 更多关于如何通过脚本代码设置循环模式,请参考 [循环模式](use-animation-curve.md#循环模式)。 + +- **Sample**:定义当前动画数据每秒的帧率,也就是一秒要划分为多少帧,默认为 60。详情请参考上文 **时间轴的刻度单位显示** 部分的内容。 + +- **Speed**:用于设置当前动画的播放速度,默认为 1,值越小播放速度越慢。 + +- **Duration**:前面的数字表示当动画播放速度为 1 的时候,动画的持续时间,根据最后一个关键帧所在位置决定。后面括号中的数字表示实际运行的持续时间,当调整 Speed 时,会随之变化。 + +动画剪辑是可以复用的,其状态保存在称为动画状态的对象中,通过动画状态的接口我们可以对动画进行播放、暂停、停止、变速等控制。详情请参考 [动画状态](animation-state.md)。 diff --git a/versions/4.0/zh/animation/edit-animation-clip/add-keyframe-button.png b/versions/4.0/zh/animation/edit-animation-clip/add-keyframe-button.png new file mode 100644 index 0000000000..2d2fba2a79 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/add-keyframe-button.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/add-keyframe.gif b/versions/4.0/zh/animation/edit-animation-clip/add-keyframe.gif new file mode 100644 index 0000000000..ed98e7a201 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/add-keyframe.gif differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/add-keyframe.png b/versions/4.0/zh/animation/edit-animation-clip/add-keyframe.png new file mode 100644 index 0000000000..a385de7a60 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/add-keyframe.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/add-property.png b/versions/4.0/zh/animation/edit-animation-clip/add-property.png new file mode 100644 index 0000000000..2d0dae9cb8 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/add-property.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/add-spriteframe.gif b/versions/4.0/zh/animation/edit-animation-clip/add-spriteframe.gif new file mode 100644 index 0000000000..bb3d5e53ec Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/add-spriteframe.gif differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/animation-sprite.gif b/versions/4.0/zh/animation/edit-animation-clip/animation-sprite.gif new file mode 100644 index 0000000000..efb2e33175 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/animation-sprite.gif differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/change-node.png b/versions/4.0/zh/animation/edit-animation-clip/change-node.png new file mode 100644 index 0000000000..444cf0e582 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/change-node.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/change-pro.png b/versions/4.0/zh/animation/edit-animation-clip/change-pro.png new file mode 100644 index 0000000000..f8a5d1b1eb Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/change-pro.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/choose-by-box.png b/versions/4.0/zh/animation/edit-animation-clip/choose-by-box.png new file mode 100644 index 0000000000..9469d70017 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/choose-by-box.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/choose-keyframe.png b/versions/4.0/zh/animation/edit-animation-clip/choose-keyframe.png new file mode 100644 index 0000000000..158648516a Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/choose-keyframe.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/copy-keyframes1.gif b/versions/4.0/zh/animation/edit-animation-clip/copy-keyframes1.gif new file mode 100644 index 0000000000..4b1462fad9 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/copy-keyframes1.gif differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/copy-keyframes2.gif b/versions/4.0/zh/animation/edit-animation-clip/copy-keyframes2.gif new file mode 100644 index 0000000000..f875d59cdb Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/copy-keyframes2.gif differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/menu-spacing.png b/versions/4.0/zh/animation/edit-animation-clip/menu-spacing.png new file mode 100644 index 0000000000..e20bce5e81 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/menu-spacing.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/menu_spacing_btn.png b/versions/4.0/zh/animation/edit-animation-clip/menu_spacing_btn.png new file mode 100644 index 0000000000..9a732235a7 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/menu_spacing_btn.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/move-data.png b/versions/4.0/zh/animation/edit-animation-clip/move-data.png new file mode 100644 index 0000000000..bb636f90a5 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/move-data.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/move-keyframes.gif b/versions/4.0/zh/animation/edit-animation-clip/move-keyframes.gif new file mode 100644 index 0000000000..866ed1996e Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/move-keyframes.gif differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/node-operation.png b/versions/4.0/zh/animation/edit-animation-clip/node-operation.png new file mode 100644 index 0000000000..13c7917ebe Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/node-operation.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/pro-operation.png b/versions/4.0/zh/animation/edit-animation-clip/pro-operation.png new file mode 100644 index 0000000000..1d0d00172e Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/pro-operation.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/remove-key-btn.png b/versions/4.0/zh/animation/edit-animation-clip/remove-key-btn.png new file mode 100644 index 0000000000..3c5b52cf70 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/remove-key-btn.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/remove-keyframes.gif b/versions/4.0/zh/animation/edit-animation-clip/remove-keyframes.gif new file mode 100644 index 0000000000..8baa243885 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/remove-keyframes.gif differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/same-name-node.png b/versions/4.0/zh/animation/edit-animation-clip/same-name-node.png new file mode 100644 index 0000000000..064d6d8d26 Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/same-name-node.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/scale-keyframes.gif b/versions/4.0/zh/animation/edit-animation-clip/scale-keyframes.gif new file mode 100644 index 0000000000..eff95666ce Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/scale-keyframes.gif differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/set-animation-clip.png b/versions/4.0/zh/animation/edit-animation-clip/set-animation-clip.png new file mode 100644 index 0000000000..56927bef3b Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/set-animation-clip.png differ diff --git a/versions/4.0/zh/animation/edit-animation-clip/set-pro.png b/versions/4.0/zh/animation/edit-animation-clip/set-pro.png new file mode 100644 index 0000000000..8ab665f3be Binary files /dev/null and b/versions/4.0/zh/animation/edit-animation-clip/set-pro.png differ diff --git a/versions/4.0/zh/animation/embedded-player.md b/versions/4.0/zh/animation/embedded-player.md new file mode 100644 index 0000000000..700f2b57b5 --- /dev/null +++ b/versions/4.0/zh/animation/embedded-player.md @@ -0,0 +1,78 @@ +# 嵌入播放器(实验性功能) + +嵌入播放器的作用是允许在动画剪辑的制作时,可以同时绑定粒子特效或其他动画等,用于满足在播放动画同时播放动效的功能。可以用于实现挥动武器会出现刀光,或者脚踩在地面上会出现烟尘等功能。 + +在动画窗口展开 **播放器轨道列表** 即可观察整个嵌套系统: + +![overview](./embedded/embedded-overview.png) + +> **注意**:在 v3.6 中,该功能为实验性功能,开发者需要在 **偏好设置** -> **实验室中** 启用。
+> 有任何的使用体验问题或者建议,欢迎在 [论坛](https://forum.cocos.org/t/topic/137740) 反馈给我们。 +> +> ![enable](embedded/enable.png) + +## 轨道操作 + +嵌入播放器支持 **动画播放器** 以及 **粒子播放器**。 + +在 **动画轨道列表** 的右侧点击 **+** 按钮即可添加不同的轨道: + +![track](embedded/add-track.png) + +在添加后的轨道上点击鼠标右键可以 **移除播放器轨道** 和 **清除数据**。 + +![delete](embedded/clear-data.png) + +> **注意**:清空数据操作会清空整个轨道上的内容。 + +在轨道数据上点击鼠标右键可以下选择 **复制** 和 **删除** 操作。 + +删除操作仅删除当前选中的数据。 + +![menu](embedded/track-data-menu.png) + +未选中任何数据时,可以选择 **创建**、**粘贴** 和 **清除数据**。 + +![unselect](embedded/unselect.png) + +添加轨道完成后,可以调整轨道数据的位置以及时长: + +![duration](embedded/track-adjust.gif) + +### 粒子播放器轨道 + +在 **动画轨道列表** 中选中左侧已添加的粒子播放器轨道,在 **属性检查器** 可以调整轨道的属性。 + +![inspector](embedded/particle-inspector.png) + +- **同步速度**:使嵌入粒子系统与当前动画剪辑保持一致的播放速率 +- **子节点**:通过下拉框可以选择不同的子节点上的粒子系统: + + ![select child](embedded/inspector-select-child.png) + +### 动画播放器轨道 + +在 **动画轨道列表** 中选中左侧已添加的动画播放器轨道,在 **属性检查器** 可以调整轨道的属性。 + +![anim](embedded/inspector-select-anim.png) + +- **同步速度**:使嵌入动画与当前的动画剪辑保持一致的播放速率 +- **子节点**:通过下拉框可以选择不同的子节点上的动画组件: + + ![anim](embedded/inspector-select-child-anim.png) + +- **动画剪辑**:通过下拉菜单可以选择不同的动画剪辑: + + ![clip](embedded/inspector-select-clip.png) + +## 预览 + +通过上述操作即可在动画播放时嵌套不同的粒子播放器和动画播放器,展示效果如下: + +![preview](embedded/preview.gif) + +## 添加帧事件 + +在嵌套栏点击鼠标右键也可以添加帧事件,其使用方式和 [添加动画事件](animation-event.md) 一致。 + +![evt](embedded/add-keyframe-event.png) diff --git a/versions/4.0/zh/animation/embedded/add-keyframe-event.png b/versions/4.0/zh/animation/embedded/add-keyframe-event.png new file mode 100644 index 0000000000..7556452996 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/add-keyframe-event.png differ diff --git a/versions/4.0/zh/animation/embedded/add-patricle.png b/versions/4.0/zh/animation/embedded/add-patricle.png new file mode 100644 index 0000000000..104e7ea268 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/add-patricle.png differ diff --git a/versions/4.0/zh/animation/embedded/add-track.png b/versions/4.0/zh/animation/embedded/add-track.png new file mode 100644 index 0000000000..c8677ffe96 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/add-track.png differ diff --git a/versions/4.0/zh/animation/embedded/clear-data.png b/versions/4.0/zh/animation/embedded/clear-data.png new file mode 100644 index 0000000000..528ac6d871 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/clear-data.png differ diff --git a/versions/4.0/zh/animation/embedded/embedded-overview.png b/versions/4.0/zh/animation/embedded/embedded-overview.png new file mode 100644 index 0000000000..690807d04c Binary files /dev/null and b/versions/4.0/zh/animation/embedded/embedded-overview.png differ diff --git a/versions/4.0/zh/animation/embedded/enable.png b/versions/4.0/zh/animation/embedded/enable.png new file mode 100644 index 0000000000..ffd17af49f Binary files /dev/null and b/versions/4.0/zh/animation/embedded/enable.png differ diff --git a/versions/4.0/zh/animation/embedded/inspector-select-anim.png b/versions/4.0/zh/animation/embedded/inspector-select-anim.png new file mode 100644 index 0000000000..4d63c0ab7f Binary files /dev/null and b/versions/4.0/zh/animation/embedded/inspector-select-anim.png differ diff --git a/versions/4.0/zh/animation/embedded/inspector-select-child-anim.png b/versions/4.0/zh/animation/embedded/inspector-select-child-anim.png new file mode 100644 index 0000000000..3e948ace44 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/inspector-select-child-anim.png differ diff --git a/versions/4.0/zh/animation/embedded/inspector-select-child.png b/versions/4.0/zh/animation/embedded/inspector-select-child.png new file mode 100644 index 0000000000..41eb922cf5 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/inspector-select-child.png differ diff --git a/versions/4.0/zh/animation/embedded/inspector-select-clip.png b/versions/4.0/zh/animation/embedded/inspector-select-clip.png new file mode 100644 index 0000000000..cf8c4fbf49 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/inspector-select-clip.png differ diff --git a/versions/4.0/zh/animation/embedded/particle-inspector.png b/versions/4.0/zh/animation/embedded/particle-inspector.png new file mode 100644 index 0000000000..c500231223 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/particle-inspector.png differ diff --git a/versions/4.0/zh/animation/embedded/preview.gif b/versions/4.0/zh/animation/embedded/preview.gif new file mode 100644 index 0000000000..e4ab7cd308 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/preview.gif differ diff --git a/versions/4.0/zh/animation/embedded/track-adjust.gif b/versions/4.0/zh/animation/embedded/track-adjust.gif new file mode 100644 index 0000000000..c494906b21 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/track-adjust.gif differ diff --git a/versions/4.0/zh/animation/embedded/track-data-menu.png b/versions/4.0/zh/animation/embedded/track-data-menu.png new file mode 100644 index 0000000000..2d1a4ab716 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/track-data-menu.png differ diff --git a/versions/4.0/zh/animation/embedded/unselect.png b/versions/4.0/zh/animation/embedded/unselect.png new file mode 100644 index 0000000000..803c2fd7e2 Binary files /dev/null and b/versions/4.0/zh/animation/embedded/unselect.png differ diff --git a/versions/4.0/zh/animation/index.md b/versions/4.0/zh/animation/index.md new file mode 100644 index 0000000000..e378b3396c --- /dev/null +++ b/versions/4.0/zh/animation/index.md @@ -0,0 +1,29 @@ +# 动画系统 + +Cocos Creator 内置了通用的动画系统用以实现基于关键帧的动画。除了支持标准的位移、旋转、缩放动画和帧动画之外,还支持任意组件属性和用户自定义属性的驱动,再加上可任意编辑的时间曲线和创新的移动轨迹编辑功能,能够让内容生产人员不写一行代码就制作出细腻的各种动态效果。 + +![animation cover](index/main.gif) + +> **注意**:Cocos Creator 自带的动画编辑器适用于制作一些不太复杂的、需要与逻辑进行联动的动画,例如 UI 动画。如果要制作复杂的特效、角色动画、嵌套动画等,可以考虑改用 Spine、DragonBones 或者 3D 模型骨骼动画编辑器进行制作。 + +## 内容 + +- [动画剪辑](animation-clip.md)(Animation Clip):包含了动画数据的资源,可复用。动画剪辑可以通过 **动画编辑器** 产出,或者通过某些已经包含了骨骼动画的外部资源导入。 + +- [动画组件参考](animation-comp.md):动画组件可以以动画方式驱动所在节点和子节点上的节点/组件属性,包括用户自定义脚本中的属性。 + +- [使用动画编辑器](animation.md):了解动画编辑器的使用,通过动画编辑器创建/修改/生成动画剪辑资源。 + +- [骨骼动画](skeletal-animation.md):常见但类型特殊的动画,本篇主要对其进行介绍及说明用法。 + +- [使用脚本控制动画](animation-component.md):动画组件管理了一组动画状态,用于控制各动画的播放、暂停、继续、停止、切换等。 + + - [动画状态](animation-state.md)(Animation State):动画剪辑的状态保存在称为动画状态的对象中,动画状态可以控制需要在对象上使用的动画剪辑。动画状态提供了更多动画控制接口,通过这些接口可以对动画进行播放、停止、变速、设置循环模式等更为细致的控制。 + +- [嵌入播放器](embedded-player.md):嵌入播放器用于在动画剪辑中嵌入其他粒子播放器或动画播放器。 + +- [Marionette 动画系统](./marionette/index.md):v3.4 新增,实现了由状态机控制的自动化且可复用的骨骼动画流程。 +- [程序化动画](./marionette/procedural-animation/index.md):v3.8 新增,可通过不同的动画节点对动画的采样过程进行程序化控制。 +- [辅助曲线](./animation-auxiliary-curve.md): v3.8 新增,用于在动画剪辑中增加可以辅助获取信息的曲线编辑器。 + +具体的动画实现根据不同的动画需求,操作步骤以及代码实现都不同,可参考官方范例 **animation**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/animation) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/animation)),主要介绍一些常见的编辑操作方法以及代码范例,以供参考。 diff --git a/versions/4.0/zh/animation/index/main.gif b/versions/4.0/zh/animation/index/main.gif new file mode 100644 index 0000000000..6b202df4ea Binary files /dev/null and b/versions/4.0/zh/animation/index/main.gif differ diff --git a/versions/4.0/zh/animation/joint-texture-layout.md b/versions/4.0/zh/animation/joint-texture-layout.md new file mode 100644 index 0000000000..bd427f17df --- /dev/null +++ b/versions/4.0/zh/animation/joint-texture-layout.md @@ -0,0 +1,88 @@ +# 骨骼贴图布局设置 + +要确保 [骨骼动画](./skeletal-animation.md) 也能够完全正确地参与 [动态 Instancing](../engine/renderable/model-component.md#instancing-%E5%90%88%E6%89%B9),需要用户手动指定每张骨骼贴图的数据分配方式。 + +比如一个场景中要绘制大量相同的人物角色,每个角色可能在走/跳/攻击。如果希望一个 Drawcall 就能够正确完成所有角色的绘制,一个重要的前提条件是 **这三个动画(走、跳、攻击)的数据都储存在同一张骨骼贴图内**。 + +目前在默认的 [预烘焙骨骼动画模式](./skeletal-animation.md#%E9%A2%84%E7%83%98%E7%84%99%E9%AA%A8%E9%AA%BC%E5%8A%A8%E7%94%BB%E7%B3%BB%E7%BB%9F) 下,骨骼贴图已经做到全局自动复用,但每张贴图的大小和它们各储存哪些动画是不可预知的。如果不做任何处理直接开启蒙皮模型的 instancing 的话,最终的运行时效果可能会出现有的动画效果正确,有的动画效果完全错乱,并且完全无法预测。 + +为此打开 **项目设置** 找到 **骨骼贴图布局设置** 分页,用于手动指定每张骨骼贴图中要存储哪些骨骼的哪些动画信息。 + +![joint texture layout panel](./joint-texture-layout/joint-texture-layout-panel.png) + +> **注意**:骨骼贴图布局设置面板提供的,本质上是运行时的 **内存分配指导规则**。对于指定的骨骼和动画资源,会保证按照指定规则分配。但如果运行时用到了并未指定规则的资源,还是会回到全局复用的自动分配模式上去。 + +## 骨骼贴图布局设置 + +下面我们以范例工程 **show-cases** 中的 **instanced-skinning** 场景([GitHub](https://github.com/cocos-creator/example-3d/tree/v3.8/show-cases/assets/scenes) | [Gitee](https://gitee.com/mirrors_cocos-creator/example-3d/tree/v3.8/show-cases/assets/scenes))为例,来看一下骨骼贴图布局具体的设置流程以及实际效果。 + +下图中展示了一个示例场景,有多个来自同一模型的实例,同时播放完全不同的动画。这些模型使用实时计算动画模式,并没有开启 instancing。可以看到,当前场景加上 UI,总 Drawcall 为 60,instance 计数为 0。这个状态将作为后面改动的基础,以对照使用。 + +![Baseline](./joint-texture-layout/instancing_baseline.gif) + +要创建开启 instancing 版本的模型,需要以下步骤: + +1. 在 **属性检查器** 中,勾选骨骼动画组件 (SkeletalAnimation) 中的 `UseBakedAnimation` 属性(使用预烘焙动画模式) + + ![use baked animation](./joint-texture-layout/use-baked-animation.png) + +2. 对所有蒙皮模型 (SkinningModel) 使用的材质,勾选 `USE INSTANCING` + + ![Enabling](./joint-texture-layout/enabling_instancing.png) + +在我们的示例场景中实际是制作了两套 Prefab,并在开启 instancing 的版本中将材质漫反射颜色设为蓝色,以便同时观察和区分两套系统的表现。可以看到效果已完全正确,并且只用了 5 个 Drawcall(每个模型分为 5 个部分),instance 数量为 45。 + +![Normal](./joint-texture-layout/instancing_normal.gif) + +> **注意**:这里能够正确渲染所有模型的原因是,动画数据量相对还比较小,通用的骨骼贴图全局复用逻辑就已经把所有动画数据写入了同一张贴图,因此效果是正确的。但随时可能加入的新动画如果超出了默认骨骼贴图的大小(360 * 360),动画效果就一定会出问题,这也就是为什么骨骼贴图布局面板必须存在的原因。 + +## 骨骼贴图布局效果 + +出于展示目的,我们可以在 **骨骼贴图布局设置** 面板故意将每个动画都单独放到一张贴图上,看看最后的渲染效果。 + +首先,打开 **骨骼贴图布局设置** 面板: + +![Panel](./joint-texture-layout/joint_texture_layout_new.png) + +面板中三个 **+** 的用法如下: + +- ① — 用于增加 Texture 单元,一个 Texture 单元由多个 Skeleton 单元组成。 +- ② — 用于增加 Skeleton 单元,一个 Skeleton 单元由一个 Skeleton 资源和一至多个 AnimationClip 资源组成。 +- ③ — 用于增加 AnimationClip 资源槽。 + +这里我们将 9 个不同的动画分开放在 9 个 Texture 单元内: + +![WrongLayout](./joint-texture-layout/joint_texture_layout_wrong.png) + +重新运行场景,效果变为: + +![Wrong](./joint-texture-layout/instancing_wrong.gif) + +可以看到此时动画效果出现了问题,所有动画都变成了执行 attack 动作,并且不时地出现模型消失的问题。这背后的原因可以精确地分析清楚: + +- 每个 Drawcall 绘制 9 个实例,分别在播 9 个不同的动画。 +- 但骨骼动画贴图每个 Drawcall 只能用一张,这里明显使用了 Texture 单元 0,只有一个 attack 动画。 +- 而不同动画片段的长度不同,有些长度超过 attack 片段,那么在最后一段时间便会读到 Texture 单元 0 的有效区域之外,这里的数据并未定义(一般是默认全 0),不是有效的骨骼变换数据,自然无法正确渲染了。 + +> **注意**:这里的 9 张贴图上都只有同一个骨骼的动画信息,所以最终效果哪怕贴图已经错了,渲染出来也只是动作错了而已;但如果一张贴图内有多个骨骼的动画信息,同时出现贴图不匹配情况的话,渲染效果就会完全错乱。 + +对于上面的示例场景,因为这个模型的确需要同时同屏播放这 9 个动画片段,所以正确的骨骼贴图布局设置应该为: + +![CorrectLayout](./joint-texture-layout/joint_texture_layout_correct.png) + +这样就能够 **保证** 正确渲染了。观察面板上相关数据的变化: + +- Texture 单元 0 总大小为 276 x 276(由算法自动生成的,足够存放指定的所有动画数据的最小尺寸)。 +- 指定的 9 组动画数据占了这张贴图的 94.41%,有 5.59% 的多余空间(这部分空间在运行时并不会参与全局复用)。 + +另外,贴图尺寸旁的图标颜色表示当前贴图的设备适配情况: + +- 绿色(边长 1024 以下):所有设备都确保有效。 +- 黄色(边长 1024 ~ 2048):某些不支持浮点贴图的移动设备或小游戏平台可能会不支持。 +- 红色(边长 2048 以上):许多移动设备上都不支持。 + +> **注意**:这里只是在一张贴图上放了一套骨骼的 9 个动画,但只要总大小没有超过设备上限,每张贴图上便可以放 **任意多套骨骼** 的任意数量动画。通常一张贴图放多套骨骼是更为常见的情况,比如对于 [蒙皮模型的平面阴影](./skeletal-animation.md#%E5%85%B3%E4%BA%8E%E5%8A%A8%E6%80%81-instancing)。 + +我们继续在场景中增加更多的实例数量,可以看到 Drawcall 数量并不会改变,只有 instance 数量的增加: + +![Bulk](./joint-texture-layout/instancing_bulk.gif) diff --git a/versions/4.0/zh/animation/joint-texture-layout/enabling_instancing.png b/versions/4.0/zh/animation/joint-texture-layout/enabling_instancing.png new file mode 100644 index 0000000000..572a50a021 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/enabling_instancing.png differ diff --git a/versions/4.0/zh/animation/joint-texture-layout/instancing_baseline.gif b/versions/4.0/zh/animation/joint-texture-layout/instancing_baseline.gif new file mode 100644 index 0000000000..8ccfa39a27 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/instancing_baseline.gif differ diff --git a/versions/4.0/zh/animation/joint-texture-layout/instancing_bulk.gif b/versions/4.0/zh/animation/joint-texture-layout/instancing_bulk.gif new file mode 100644 index 0000000000..9913a49ff6 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/instancing_bulk.gif differ diff --git a/versions/4.0/zh/animation/joint-texture-layout/instancing_normal.gif b/versions/4.0/zh/animation/joint-texture-layout/instancing_normal.gif new file mode 100644 index 0000000000..be18cef6f3 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/instancing_normal.gif differ diff --git a/versions/4.0/zh/animation/joint-texture-layout/instancing_wrong.gif b/versions/4.0/zh/animation/joint-texture-layout/instancing_wrong.gif new file mode 100644 index 0000000000..bee0f02ba6 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/instancing_wrong.gif differ diff --git a/versions/4.0/zh/animation/joint-texture-layout/joint-texture-layout-panel.png b/versions/4.0/zh/animation/joint-texture-layout/joint-texture-layout-panel.png new file mode 100644 index 0000000000..a34c6e04f8 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/joint-texture-layout-panel.png differ diff --git a/versions/4.0/zh/animation/joint-texture-layout/joint_texture_layout_correct.png b/versions/4.0/zh/animation/joint-texture-layout/joint_texture_layout_correct.png new file mode 100644 index 0000000000..12abeb7c26 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/joint_texture_layout_correct.png differ diff --git a/versions/4.0/zh/animation/joint-texture-layout/joint_texture_layout_new.png b/versions/4.0/zh/animation/joint-texture-layout/joint_texture_layout_new.png new file mode 100644 index 0000000000..41af682980 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/joint_texture_layout_new.png differ diff --git a/versions/4.0/zh/animation/joint-texture-layout/joint_texture_layout_wrong.png b/versions/4.0/zh/animation/joint-texture-layout/joint_texture_layout_wrong.png new file mode 100644 index 0000000000..50648f4818 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/joint_texture_layout_wrong.png differ diff --git a/versions/4.0/zh/animation/joint-texture-layout/use-baked-animation.png b/versions/4.0/zh/animation/joint-texture-layout/use-baked-animation.png new file mode 100644 index 0000000000..430acb7490 Binary files /dev/null and b/versions/4.0/zh/animation/joint-texture-layout/use-baked-animation.png differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/figures/cover.png b/versions/4.0/zh/animation/marionette/additive-animation/figures/cover.png new file mode 100644 index 0000000000..d7b539b8cd Binary files /dev/null and b/versions/4.0/zh/animation/marionette/additive-animation/figures/cover.png differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/figures/crouch+leaning-pose.png b/versions/4.0/zh/animation/marionette/additive-animation/figures/crouch+leaning-pose.png new file mode 100644 index 0000000000..1dc0cb55f6 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/additive-animation/figures/crouch+leaning-pose.png differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/figures/crouch-pose.png b/versions/4.0/zh/animation/marionette/additive-animation/figures/crouch-pose.png new file mode 100644 index 0000000000..79f6775083 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/additive-animation/figures/crouch-pose.png differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/figures/import-setting-import-as-additive.png b/versions/4.0/zh/animation/marionette/additive-animation/figures/import-setting-import-as-additive.png new file mode 100644 index 0000000000..cf6ee09495 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/additive-animation/figures/import-setting-import-as-additive.png differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/figures/layer-additive.png b/versions/4.0/zh/animation/marionette/additive-animation/figures/layer-additive.png new file mode 100644 index 0000000000..cd141e20b9 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/additive-animation/figures/layer-additive.png differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/figures/leaning-pose.png b/versions/4.0/zh/animation/marionette/additive-animation/figures/leaning-pose.png new file mode 100644 index 0000000000..de5ab96e7e Binary files /dev/null and b/versions/4.0/zh/animation/marionette/additive-animation/figures/leaning-pose.png differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/figures/standing-pose.png b/versions/4.0/zh/animation/marionette/additive-animation/figures/standing-pose.png new file mode 100644 index 0000000000..8f4dafe4b4 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/additive-animation/figures/standing-pose.png differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/index.md b/versions/4.0/zh/animation/marionette/additive-animation/index.md new file mode 100644 index 0000000000..332532d681 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/index.md @@ -0,0 +1,62 @@ +# 叠加动画 + +叠加动画是一种复用动画效果的方式。 + +试想你的角色有两种基本姿态:站立和蹲伏: + +![](./figures/standing-pose.png) + +![](./figures/crouch-pose.png) + +在站立状态下,你为角色制作了一个上半身向右倾斜一定角度的姿态: + +![](./figures/leaning-pose.png) + +如果希望角色在蹲伏状态下也有类似的倾斜姿态,一种方法当然是再为蹲伏状态制作一个单独的姿态,但这样不免属于一种重复性工作。 + +既然从站立到站立倾斜,以及从蹲伏到蹲伏倾斜的“变化”类似,那么可以通过将倾斜姿态“减去”站立姿态,从而得到一个差量姿态。将这样的差量姿态叠加到蹲伏姿态,就可以得到蹲伏倾斜的姿态: + +![](./figures/crouch%2Bleaning-pose.png) + +示意图如下: + +![](./figures/cover.png) + +倾斜姿态“减去”站立姿态得到的姿态称为 **叠加姿态**,将其应用到蹲伏姿态从而产生的动画效果称为 **叠加动画** (Additive Animation)。 + +正式来讲,叠加动画是一个动画相对于一个参考姿态的差量,这个差量可以被叠加到其它的动画上。使用叠加动画的底层逻辑是可以给不同的动画,添加相同类型的变化,例如人物不同动作时的前倾、后倾、左倾、右倾,如果使用叠加动画,就可以给走、跑、跳、站立等添加相同的效果,而不需要为每种状态制作一套类似动画,这样就减少了每种状态所需的动画资产。 + +## 产生叠加动画 + +目前,叠加动画仅能由动画剪辑产生。 + +在动画导入配置面板中,勾选“导入为叠加动画” 后,所选的动画剪辑在动画图中使用时将产生叠加动画: + +![](./figures/import-setting-import-as-additive.png) + +> 注意,一个动画要么被导入为叠加动画,要么导入为非叠加动画。 +> 如果希望一个动画同时作为叠加和非叠加动画,可以在导入面板中复制此动画并分别配置。 + +## 应用叠加动画 + +动画图通过两种方式来应用叠加动画,分别是: + +- 配置层级为叠加层级。 + +- 在姿态图中使用 [叠加混合结点](../procedural-animation/pose-graph/pose-nodes/blend-poses.md#叠加混合)。 + +> 注意,叠加层级中使用的动画必须产生叠加姿态。 +> 例如,若叠加层级最终输出了非叠加姿态,将导致动画效果异常——往往会发现模型“变大”。 +> 反之,若非叠加层级最终输出了叠加动画,也将导致动画效果异常——往往会发现模型“消失”或变小。 + +## 叠加层级 + +在动画图层级属性面板,可以将所选层级设置为 **叠加** : + +![](./figures/layer-additive.png) + +勾选后,当此层级所产生的动画效果(姿态)将以叠加形式混合至前序层级。 + +## 姿态图:叠加混合结点 + +见:[叠加混合结点](../procedural-animation/pose-graph/pose-nodes/blend-poses.md#叠加混合)。 \ No newline at end of file diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration.blend b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration.blend new file mode 100644 index 0000000000..485fd67c49 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration.blend differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/.gitignore b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/.gitignore new file mode 100644 index 0000000000..576a8e3741 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/.gitignore @@ -0,0 +1,26 @@ + +#/////////////////////////// +# Cocos Creator 3D Project +#/////////////////////////// + +/library/ +/temp/ +/local/ +/build/ +/profiles/ +/native/engine/android/**/*/assets + +#////////////////////////// +# NPM +#////////////////////////// +node_modules/ + +#////////////////////////// +# VSCode +#////////////////////////// +.vscode/ + +#////////////////////////// +# WebStorm +#////////////////////////// +.idea/ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration.meta b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration.meta new file mode 100644 index 0000000000..e0e66f3f8f --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.2.0", + "importer": "directory", + "imported": true, + "uuid": "29781f57-d558-4cd4-b7ae-6d6a37d4ae71", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph new file mode 100644 index 0000000000..5897720292 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph @@ -0,0 +1,557 @@ +[ + { + "__type__": "cc.animation.AnimationGraph", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "_layers": [ + { + "__id__": 1 + }, + { + "__id__": 34 + } + ], + "_variables": { + "BaseLayerPose": { + "__id__": 44 + } + } + }, + { + "__type__": "cc.animation.Layer", + "_stateMachine": { + "__id__": 2 + }, + "name": "Base", + "weight": 1, + "mask": null, + "additive": false, + "_stashes": {} + }, + { + "__type__": "cc.animation.StateMachine", + "__editorExtras__": { + "name": "", + "id": "16866441677320.5891144306941865", + "clone": null, + "viewport": { + "scale": 1, + "top": 0, + "left": -1 + } + }, + "_states": [ + { + "__id__": 3 + }, + { + "__id__": 4 + }, + { + "__id__": 5 + }, + { + "__id__": 6 + }, + { + "__id__": 10 + }, + { + "__id__": 14 + } + ], + "_transitions": [ + { + "__id__": 18 + }, + { + "__id__": 19 + }, + { + "__id__": 24 + }, + { + "__id__": 29 + } + ], + "_entryState": { + "__id__": 3 + }, + "_exitState": { + "__id__": 4 + }, + "_anyState": { + "__id__": 5 + } + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441677320.3764511364334071", + "clone": null, + "centerX": -265, + "centerY": 3 + }, + "name": "Entry" + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441677320.3923465698017483", + "clone": null, + "centerX": 125, + "centerY": 0 + }, + "name": "Exit" + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441677320.3550700390662027", + "clone": null, + "centerX": 125, + "centerY": 0 + }, + "name": "Any" + }, + { + "__type__": "cc.animation.Motion", + "__editorExtras__": { + "name": "", + "id": "16866441797990.8001792367096607", + "clone": null, + "centerX": -122, + "centerY": 23 + }, + "name": "Crouch", + "_components": [], + "motion": { + "__id__": 7 + }, + "speed": 1, + "speedMultiplier": "", + "speedMultiplierEnabled": false, + "transitionInEventBinding": { + "__id__": 8 + }, + "transitionOutEventBinding": { + "__id__": 9 + } + }, + { + "__type__": "cc.animation.ClipMotion", + "__editorExtras__": { + "name": "", + "id": "16866441798030.8594645607386218", + "clone": null + }, + "clip": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@2ff01", + "__expectedType__": "cc.AnimationClip" + } + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Motion", + "__editorExtras__": { + "name": "", + "id": "16866458424730.7772643494706266", + "clone": null, + "centerX": -128, + "centerY": -29 + }, + "name": "Standing", + "_components": [], + "motion": { + "__id__": 11 + }, + "speed": 1, + "speedMultiplier": "", + "speedMultiplierEnabled": false, + "transitionInEventBinding": { + "__id__": 12 + }, + "transitionOutEventBinding": { + "__id__": 13 + } + }, + { + "__type__": "cc.animation.ClipMotion", + "__editorExtras__": { + "name": "", + "id": "16866458424760.04644098735663049", + "clone": null + }, + "clip": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e7f91", + "__expectedType__": "cc.AnimationClip" + } + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Motion", + "__editorExtras__": { + "name": "", + "id": "16866487662470.8543040974437142", + "clone": null, + "centerX": -117, + "centerY": 87 + }, + "name": "Leaning_Original", + "_components": [], + "motion": { + "__id__": 15 + }, + "speed": 1, + "speedMultiplier": "", + "speedMultiplierEnabled": false, + "transitionInEventBinding": { + "__id__": 16 + }, + "transitionOutEventBinding": { + "__id__": 17 + } + }, + { + "__type__": "cc.animation.ClipMotion", + "__editorExtras__": { + "name": "", + "id": "16866487662500.9587811069492036", + "clone": null + }, + "clip": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@d03ee", + "__expectedType__": "cc.AnimationClip" + } + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Transition", + "__editorExtras__": null, + "from": { + "__id__": 3 + }, + "to": { + "__id__": 10 + }, + "conditions": [] + }, + { + "__type__": "cc.animation.AnimationTransition", + "__editorExtras__": null, + "from": { + "__id__": 5 + }, + "to": { + "__id__": 10 + }, + "conditions": [ + { + "__id__": 20 + } + ], + "destinationStart": 0, + "relativeDestinationStart": false, + "startEventBinding": { + "__id__": 22 + }, + "endEventBinding": { + "__id__": 23 + }, + "duration": 0.3, + "relativeDuration": false, + "exitConditionEnabled": false, + "_exitCondition": 1 + }, + { + "__type__": "cc.animation.BinaryCondition", + "operator": 0, + "lhs": 0, + "lhsBinding": { + "__id__": 21 + }, + "rhs": 0 + }, + { + "__type__": "cc.animation.TCVariableBinding", + "type": 3, + "variableName": "BaseLayerPose" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationTransition", + "__editorExtras__": null, + "from": { + "__id__": 5 + }, + "to": { + "__id__": 6 + }, + "conditions": [ + { + "__id__": 25 + } + ], + "destinationStart": 0, + "relativeDestinationStart": false, + "startEventBinding": { + "__id__": 27 + }, + "endEventBinding": { + "__id__": 28 + }, + "duration": 0.3, + "relativeDuration": false, + "exitConditionEnabled": false, + "_exitCondition": 1 + }, + { + "__type__": "cc.animation.BinaryCondition", + "operator": 0, + "lhs": 0, + "lhsBinding": { + "__id__": 26 + }, + "rhs": 1 + }, + { + "__type__": "cc.animation.TCVariableBinding", + "type": 3, + "variableName": "BaseLayerPose" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationTransition", + "__editorExtras__": null, + "from": { + "__id__": 5 + }, + "to": { + "__id__": 14 + }, + "conditions": [ + { + "__id__": 30 + } + ], + "destinationStart": 0, + "relativeDestinationStart": false, + "startEventBinding": { + "__id__": 32 + }, + "endEventBinding": { + "__id__": 33 + }, + "duration": 0.3, + "relativeDuration": false, + "exitConditionEnabled": false, + "_exitCondition": 1 + }, + { + "__type__": "cc.animation.BinaryCondition", + "operator": 0, + "lhs": 0, + "lhsBinding": { + "__id__": 31 + }, + "rhs": 2 + }, + { + "__type__": "cc.animation.TCVariableBinding", + "type": 3, + "variableName": "BaseLayerPose" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Layer", + "_stateMachine": { + "__id__": 35 + }, + "name": "Leaning", + "weight": 1, + "mask": null, + "additive": true, + "_stashes": {} + }, + { + "__type__": "cc.animation.StateMachine", + "__editorExtras__": { + "name": "", + "id": "16866441867500.6467469582792678", + "clone": null, + "viewport": { + "scale": 1, + "top": 0, + "left": 0 + } + }, + "_states": [ + { + "__id__": 36 + }, + { + "__id__": 37 + }, + { + "__id__": 38 + }, + { + "__id__": 39 + } + ], + "_transitions": [ + { + "__id__": 43 + } + ], + "_entryState": { + "__id__": 36 + }, + "_exitState": { + "__id__": 37 + }, + "_anyState": { + "__id__": 38 + } + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441867500.2990931519407911", + "clone": null, + "centerX": -125, + "centerY": 0 + }, + "name": "Entry" + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441867500.16966542714941157", + "clone": null, + "centerX": 125, + "centerY": 0 + }, + "name": "Exit" + }, + { + "__type__": "cc.animation.State", + "__editorExtras__": { + "name": "", + "id": "16866441867500.650266379833792", + "clone": null, + "centerX": 125, + "centerY": 0 + }, + "name": "Any" + }, + { + "__type__": "cc.animation.Motion", + "__editorExtras__": { + "name": "", + "id": "16866449866630.2924323274845875", + "clone": null, + "centerX": -1, + "centerY": -4 + }, + "name": "Leaning", + "_components": [], + "motion": { + "__id__": 40 + }, + "speed": 1, + "speedMultiplier": "", + "speedMultiplierEnabled": false, + "transitionInEventBinding": { + "__id__": 41 + }, + "transitionOutEventBinding": { + "__id__": 42 + } + }, + { + "__type__": "cc.animation.ClipMotion", + "__editorExtras__": { + "name": "", + "id": "16866449866670.9509605487815147", + "clone": null + }, + "clip": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@94e83", + "__expectedType__": "cc.AnimationClip" + } + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.AnimationGraphEventBinding", + "methodName": "" + }, + { + "__type__": "cc.animation.Transition", + "__editorExtras__": null, + "from": { + "__id__": 36 + }, + "to": { + "__id__": 39 + }, + "conditions": [] + }, + { + "__type__": "cc.animation.PlainVariable", + "_type": 3, + "_value": 0 + } +] \ No newline at end of file diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph.meta b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph.meta new file mode 100644 index 0000000000..d25c93115e --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.animgraph.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.2.0", + "importer": "animation-graph", + "imported": true, + "uuid": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb new file mode 100644 index 0000000000..39805bcb0d Binary files /dev/null and b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb differ diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb.meta b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb.meta new file mode 100644 index 0000000000..0b68f5da70 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.glb.meta @@ -0,0 +1,400 @@ +{ + "ver": "2.3.8", + "importer": "gltf", + "imported": true, + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe", + "files": [ + "__original-animation-0.cconb", + "__original-animation-1.cconb", + "__original-animation-2.cconb", + "__original-animation-3.cconb" + ], + "subMetas": { + "60865": { + "importer": "gltf-skeleton", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@60865", + "displayName": "", + "id": "60865", + "name": "Rig.skeleton", + "userData": { + "gltfIndex": 0, + "jointsLength": 27 + }, + "ver": "1.0.1", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "e2349": { + "importer": "gltf-mesh", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e2349", + "displayName": "", + "id": "e2349", + "name": "Mesh.mesh", + "userData": { + "gltfIndex": 0, + "triangleCount": 324 + }, + "ver": "1.1.1", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {} + }, + "2ff01": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@2ff01", + "displayName": "", + "id": "2ff01", + "name": "Crouch.animation", + "userData": { + "gltfIndex": 0, + "wrapMode": 2, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [] + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "94e83": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@94e83", + "displayName": "", + "id": "94e83", + "name": "Leaning_Additive.animation", + "userData": { + "gltfIndex": 1, + "wrapMode": 2, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [], + "additive": { + "enabled": true, + "refClip": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e7f91" + } + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "d03ee": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@d03ee", + "displayName": "", + "id": "d03ee", + "name": "Leaning_Original.animation", + "userData": { + "gltfIndex": 1, + "wrapMode": 2, + "speed": 1, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [] + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "aff49": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@aff49", + "displayName": "", + "id": "aff49", + "name": "ReferencePose.animation", + "userData": { + "gltfIndex": 2, + "wrapMode": 2, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [] + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "e7f91": { + "importer": "gltf-animation", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e7f91", + "displayName": "", + "id": "e7f91", + "name": "Standing.animation", + "userData": { + "gltfIndex": 3, + "wrapMode": 2, + "sample": 30, + "span": { + "from": 0, + "to": 0.0416666679084301 + }, + "events": [], + "additive": { + "enabled": false + } + }, + "ver": "1.0.16", + "imported": true, + "files": [ + ".cconb" + ], + "subMetas": {} + }, + "b4e6b": { + "importer": "gltf-material", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@b4e6b", + "displayName": "", + "id": "b4e6b", + "name": "Material.material", + "userData": { + "gltfIndex": 0 + }, + "ver": "1.0.14", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "bbd5d": { + "importer": "gltf-scene", + "uuid": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "displayName": "", + "id": "bbd5d", + "name": "AdditivePoseDemonstration.prefab", + "userData": { + "gltfIndex": 0 + }, + "ver": "1.0.13", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + } + }, + "userData": { + "imageMetas": [], + "animationImportSettings": [ + { + "name": "Crouch", + "duration": 0.0416666679084301, + "fps": 30, + "splits": [ + { + "name": "Crouch", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "previousId": "2ff01" + } + ] + }, + { + "name": "Leaning", + "duration": 0.0416666679084301, + "fps": 30, + "splits": [ + { + "name": "Leaning_Additive", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "previousId": "94e83", + "additive": { + "enabled": true, + "refClip": "3705c9df-8a1c-4d36-92d3-512664e51cfe@e7f91" + } + }, + { + "name": "Leaning_Original", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "speed": 1, + "previousId": "d03ee" + } + ] + }, + { + "name": "ReferencePose", + "duration": 0.0416666679084301, + "fps": 30, + "splits": [ + { + "name": "ReferencePose", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "previousId": "aff49" + } + ] + }, + { + "name": "Standing", + "duration": 0.0416666679084301, + "fps": 30, + "splits": [ + { + "name": "Standing", + "from": 0, + "to": 0.0416666679084301, + "wrapMode": 2, + "previousId": "e7f91", + "additive": { + "enabled": false + } + } + ] + } + ], + "redirect": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "lods": { + "enable": false, + "hasBuiltinLOD": false, + "options": [ + { + "screenRatio": 0.25, + "faceCount": 1 + }, + { + "screenRatio": 0.125, + "faceCount": 0.25 + }, + { + "screenRatio": 0.01, + "faceCount": 0.1 + } + ] + }, + "assetFinder": { + "meshes": [ + "3705c9df-8a1c-4d36-92d3-512664e51cfe@e2349" + ], + "skeletons": [ + "3705c9df-8a1c-4d36-92d3-512664e51cfe@60865" + ], + "textures": [], + "materials": [ + "3705c9df-8a1c-4d36-92d3-512664e51cfe@b4e6b" + ], + "scenes": [ + "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d" + ] + }, + "materials": { + "3705c9df-8a1c-4d36-92d3-512664e51cfe@b4e6b": { + "__type__": "cc.Material", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "_effectAsset": { + "__uuid__": "c8f66d17-351a-48da-a12c-0212d28575c4", + "__expectedType__": "cc.EffectAsset" + }, + "_techIdx": 0, + "_defines": [ + {}, + {}, + {}, + {}, + {} + ], + "_states": [ + { + "rasterizerState": { + "cullMode": 0 + }, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + }, + { + "rasterizerState": {}, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + }, + { + "rasterizerState": {}, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + }, + { + "rasterizerState": {}, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + }, + { + "rasterizerState": {}, + "depthStencilState": {}, + "blendState": { + "targets": [ + {} + ] + } + } + ], + "_props": [ + { + "albedoScale": { + "__type__": "cc.Vec3", + "x": 0.2694019675254822, + "y": 0.2722896635532379, + "z": 0.8000000715255737 + }, + "roughness": 0, + "specularIntensity": 0.1 + }, + {}, + {}, + {}, + {} + ] + } + } + } +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene new file mode 100644 index 0000000000..58c63c17d4 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene @@ -0,0 +1,1990 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "AdditivePoseDemonstration", + "_objFlags": 0, + "__editorExtras__": {}, + "_native": "", + "scene": { + "__id__": 1 + } + }, + { + "__type__": "cc.Scene", + "_name": "AdditivePoseDemonstration", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 5 + }, + { + "__id__": 7 + }, + { + "__id__": 22 + }, + { + "__id__": 39 + }, + { + "__id__": 56 + }, + { + "__id__": 73 + }, + { + "__id__": 76 + }, + { + "__id__": 83 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 90 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "autoReleaseAssets": false, + "_globals": { + "__id__": 91 + }, + "_id": "71e36a35-a552-4298-90b5-3d1064fa30cb" + }, + { + "__type__": "cc.Node", + "_name": "Main Light", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 3 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.06397656665577071, + "y": -0.44608233363525845, + "z": -0.8239028751062036, + "w": -0.3436591377065261 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -117.894, + "y": -194.909, + "z": 38.562 + }, + "_id": "c0y6F5f+pAvI805TdmxIjx" + }, + { + "__type__": "cc.DirectionalLight", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 250, + "b": 240, + "a": 255 + }, + "_useColorTemperature": false, + "_colorTemperature": 6550, + "_staticSettings": { + "__id__": 4 + }, + "_visibility": -325058561, + "_illuminanceHDR": 65000, + "_illuminance": 65000, + "_illuminanceLDR": 1.6927083333333335, + "_shadowEnabled": false, + "_shadowPcf": 0, + "_shadowBias": 0.00001, + "_shadowNormalBias": 0, + "_shadowSaturation": 1, + "_shadowDistance": 50, + "_shadowInvisibleOcclusionRange": 200, + "_csmLevel": 4, + "_csmLayerLambda": 0.75, + "_csmOptimizationMode": 2, + "_csmAdvancedOptions": false, + "_csmLayersTransition": false, + "_csmTransitionRange": 0.05, + "_shadowFixedArea": false, + "_shadowNear": 0.1, + "_shadowFar": 10, + "_shadowOrthoSize": 5, + "_id": "597uMYCbhEtJQc0ffJlcgA" + }, + { + "__type__": "cc.StaticLightSettings", + "_baked": false, + "_editorOnly": false, + "_castShadow": false + }, + { + "__type__": "cc.Node", + "_name": "Main Camera", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 6 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.6228552625332178, + "y": 1.2214186893935963, + "z": 4.468293757497586 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "c9DMICJLFO5IeO07EPon7U" + }, + { + "__type__": "cc.Camera", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 5 + }, + "_enabled": true, + "__prefab": null, + "_projection": 0, + "_priority": 0, + "_fov": 45, + "_fovAxis": 0, + "_orthoHeight": 1.5, + "_near": 0.1, + "_far": 100, + "_color": { + "__type__": "cc.Color", + "r": 51, + "g": 51, + "b": 51, + "a": 255 + }, + "_depth": 1, + "_stencil": 0, + "_clearFlags": 14, + "_rect": { + "__type__": "cc.Rect", + "x": 0, + "y": 0, + "width": 1, + "height": 1 + }, + "_aperture": 19, + "_shutter": 7, + "_iso": 0, + "_screenScale": 1, + "_visibility": 1822425087, + "_targetTexture": null, + "_postProcess": null, + "_usePostProcess": false, + "_cameraType": -1, + "_trackingType": 0, + "_id": "7dWQTpwS5LrIHnc1zAPUtf" + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 8 + }, + "__editorExtras__": {} + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 7 + }, + "asset": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "__expectedType__": "cc.Prefab" + }, + "fileId": "6eteDuTGJdMpDat7uq2H51", + "instance": { + "__id__": 9 + }, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "a74QLAgbVA66mIBu/fvUTR", + "prefabRootNode": null, + "mountedChildren": [], + "mountedComponents": [ + { + "__id__": 10 + }, + { + "__id__": 13 + } + ], + "propertyOverrides": [ + { + "__id__": 17 + }, + { + "__id__": 19 + } + ], + "removedComponents": [ + { + "__id__": 20 + }, + { + "__id__": 21 + } + ] + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 11 + }, + "components": [ + { + "__id__": 12 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "71nT0fGWxfJ7Ln9zk1h3p9" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 7 + } + }, + "node": { + "__id__": 7 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "b6E+SRjatAP6rP7rSbIwm4" + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 14 + }, + "components": [ + { + "__id__": 15 + }, + { + "__id__": 16 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 7 + } + }, + "node": { + "__id__": 7 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "04yDjhtuFECLr+R5dbRpvX" + }, + { + "__type__": "9177brmDQ1BxYgGRYI7jZZd", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 7 + } + }, + "node": { + "__id__": 7 + }, + "_enabled": true, + "__prefab": null, + "baseLayerPose": 2, + "additiveLayerWeight": 0, + "_id": "0dW2UVM+ZHDoaXsOVlxnmL" + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 18 + }, + "propertyPath": [ + "_name" + ], + "value": "Leaning_Original" + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 18 + }, + "propertyPath": [ + "_lpos" + ], + "value": { + "__type__": "cc.Vec3", + "x": -2.863, + "y": 0, + "z": 0 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "46p/wNB7td07WS89tf2KfI" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b1BxQNz2Fd1qVeG4pkCjo+" + ] + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 23 + }, + "__editorExtras__": {} + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 22 + }, + "asset": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "__expectedType__": "cc.Prefab" + }, + "fileId": "6eteDuTGJdMpDat7uq2H51", + "instance": { + "__id__": 24 + }, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "1bq5EPIq5NvrvsCKGygUQs", + "prefabRootNode": null, + "mountedChildren": [], + "mountedComponents": [ + { + "__id__": 25 + }, + { + "__id__": 28 + } + ], + "propertyOverrides": [ + { + "__id__": 32 + }, + { + "__id__": 34 + }, + { + "__id__": 35 + }, + { + "__id__": 36 + } + ], + "removedComponents": [ + { + "__id__": 37 + }, + { + "__id__": 38 + } + ] + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 26 + }, + "components": [ + { + "__id__": 27 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "71nT0fGWxfJ7Ln9zk1h3p9" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 22 + } + }, + "node": { + "__id__": 22 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "2ewNSgCdBMfJdOFfHmji7x" + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 29 + }, + "components": [ + { + "__id__": 30 + }, + { + "__id__": 31 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 22 + } + }, + "node": { + "__id__": 22 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "69m0LjdqRFdLaM/zqAkw5I" + }, + { + "__type__": "9177brmDQ1BxYgGRYI7jZZd", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 22 + } + }, + "node": { + "__id__": 22 + }, + "_enabled": true, + "__prefab": null, + "baseLayerPose": 0, + "additiveLayerWeight": 0, + "_id": "1bPjyjBAhGFI0thugs+fo9" + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 33 + }, + "propertyPath": [ + "_name" + ], + "value": "Standing" + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 33 + }, + "propertyPath": [ + "_lpos" + ], + "value": { + "__type__": "cc.Vec3", + "x": -1.411, + "y": 0, + "z": 0 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 33 + }, + "propertyPath": [ + "_lrot" + ], + "value": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 33 + }, + "propertyPath": [ + "_euler" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "46p/wNB7td07WS89tf2KfI" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b1BxQNz2Fd1qVeG4pkCjo+" + ] + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 40 + }, + "__editorExtras__": {} + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 39 + }, + "asset": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "__expectedType__": "cc.Prefab" + }, + "fileId": "6eteDuTGJdMpDat7uq2H51", + "instance": { + "__id__": 41 + }, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "d54z2Ijy5P9auHqubsgUaK", + "prefabRootNode": null, + "mountedChildren": [], + "mountedComponents": [ + { + "__id__": 42 + }, + { + "__id__": 45 + } + ], + "propertyOverrides": [ + { + "__id__": 49 + }, + { + "__id__": 51 + }, + { + "__id__": 52 + }, + { + "__id__": 53 + } + ], + "removedComponents": [ + { + "__id__": 54 + }, + { + "__id__": 55 + } + ] + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 43 + }, + "components": [ + { + "__id__": 44 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "71nT0fGWxfJ7Ln9zk1h3p9" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 39 + } + }, + "node": { + "__id__": 39 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "d5zFrWqnhAxakZStdvmRZA" + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 46 + }, + "components": [ + { + "__id__": 47 + }, + { + "__id__": 48 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 39 + } + }, + "node": { + "__id__": 39 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "81d14rOElFe7/iPZEHkQT4" + }, + { + "__type__": "9177brmDQ1BxYgGRYI7jZZd", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 39 + } + }, + "node": { + "__id__": 39 + }, + "_enabled": true, + "__prefab": null, + "baseLayerPose": 1, + "additiveLayerWeight": 0, + "_id": "e2ulA7F8xLfZBW2GTmZecQ" + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 50 + }, + "propertyPath": [ + "_name" + ], + "value": "Crouch" + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 50 + }, + "propertyPath": [ + "_lpos" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 50 + }, + "propertyPath": [ + "_lrot" + ], + "value": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 50 + }, + "propertyPath": [ + "_euler" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "46p/wNB7td07WS89tf2KfI" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b1BxQNz2Fd1qVeG4pkCjo+" + ] + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 57 + }, + "__editorExtras__": {} + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 56 + }, + "asset": { + "__uuid__": "3705c9df-8a1c-4d36-92d3-512664e51cfe@bbd5d", + "__expectedType__": "cc.Prefab" + }, + "fileId": "6eteDuTGJdMpDat7uq2H51", + "instance": { + "__id__": 58 + }, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "7daGB70FlJ24/isQrmW3WQ", + "prefabRootNode": null, + "mountedChildren": [], + "mountedComponents": [ + { + "__id__": 59 + }, + { + "__id__": 62 + } + ], + "propertyOverrides": [ + { + "__id__": 66 + }, + { + "__id__": 68 + }, + { + "__id__": 69 + }, + { + "__id__": 70 + } + ], + "removedComponents": [ + { + "__id__": 71 + }, + { + "__id__": 72 + } + ] + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 60 + }, + "components": [ + { + "__id__": 61 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "71nT0fGWxfJ7Ln9zk1h3p9" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 56 + } + }, + "node": { + "__id__": 56 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "d5R8c8TaVHlbC4CkeP6OZ0" + }, + { + "__type__": "cc.MountedComponentsInfo", + "targetInfo": { + "__id__": 63 + }, + "components": [ + { + "__id__": 64 + }, + { + "__id__": 65 + } + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "cc.animation.AnimationController", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 56 + } + }, + "node": { + "__id__": 56 + }, + "_enabled": true, + "__prefab": null, + "_graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "graph": { + "__uuid__": "815e2fd3-f7fc-4013-b18c-b42e9128d911", + "__expectedType__": "cc.animation.AnimationGraph" + }, + "_id": "4apPF/cRRBa6W/mcXucxjA" + }, + { + "__type__": "9177brmDQ1BxYgGRYI7jZZd", + "_name": "", + "_objFlags": 0, + "__editorExtras__": { + "mountedRoot": { + "__id__": 56 + } + }, + "node": { + "__id__": 56 + }, + "_enabled": true, + "__prefab": null, + "baseLayerPose": 1, + "additiveLayerWeight": 1, + "_id": "46f+xniiRKIZ5Rq59QZthK" + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 67 + }, + "propertyPath": [ + "_name" + ], + "value": "Result" + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6eteDuTGJdMpDat7uq2H51" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 67 + }, + "propertyPath": [ + "_lpos" + ], + "value": { + "__type__": "cc.Vec3", + "x": 1.553, + "y": 0, + "z": 0 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 67 + }, + "propertyPath": [ + "_lrot" + ], + "value": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + } + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 67 + }, + "propertyPath": [ + "_euler" + ], + "value": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "46p/wNB7td07WS89tf2KfI" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b1BxQNz2Fd1qVeG4pkCjo+" + ] + }, + { + "__type__": "cc.Node", + "_name": "Minus", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 74 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": -2.11, + "y": 0.883, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "017nKpSLdEP6NxjvHU06X4" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 73 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 75 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "71XyR0nzdCRKtTw8wMmWIP" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.Node", + "_name": "Add", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 77 + }, + { + "__id__": 80 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.791, + "y": 0.906, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "a5V6Eg9ZZA3Zf/TOqw9lmm" + }, + { + "__type__": "cc.Node", + "_name": "Minus-001", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 76 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 78 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "853FDaObxMdYj1iraJPtSL" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 77 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 79 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "efmgEXaWFKi6lYVacN/BQ+" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.Node", + "_name": "Minus-002", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 76 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 81 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0.7071067811865475, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 90 + }, + "_id": "0d4Kvp7KxKIbgRbYuiSVD9" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 80 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 82 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "14JoQDoc5Af5mrbw3mrlaC" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.Node", + "_name": "Equal", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 84 + }, + { + "__id__": 87 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.768, + "y": 1.014, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "55tRriDhFAhobge/nkmlEq" + }, + { + "__type__": "cc.Node", + "_name": "Minus-001", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 83 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 85 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "cevtUbQ6xI8axpqv2Z40Mc" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 84 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 86 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "9a+ZDOoUNCKrpS9Rc4Mkj8" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.Node", + "_name": "Minus-002", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 83 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 88 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -0.207, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.476, + "y": 0.137, + "z": 0.249 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "6dd9ucUANKFIyA8Fk6cljr" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "Cube", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 87 + }, + "_enabled": true, + "__prefab": null, + "_materials": [ + { + "__uuid__": "620b6bf3-0369-4560-837f-2a2c00b73c26", + "__expectedType__": "cc.Material" + } + ], + "_visFlags": 0, + "bakeSettings": { + "__id__": 89 + }, + "_mesh": { + "__uuid__": "1263d74c-8167-4928-91a6-4e2672411f47@a804a", + "__expectedType__": "cc.Mesh" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_shadowBias": 0, + "_shadowNormalBias": 0, + "_reflectionProbeId": -1, + "_reflectionProbeBlendId": -1, + "_reflectionProbeBlendWeight": 0, + "_enabledGlobalStandardSkinObject": false, + "_enableMorph": true, + "_id": "adhfUXA8VFBI93rDl6+Nrs" + }, + { + "__type__": "cc.ModelBakeSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64, + "_useLightProbe": false, + "_bakeToLightProbe": true, + "_reflectionProbeType": 0, + "_bakeToReflectionProbe": true + }, + { + "__type__": "cc.PrefabInfo", + "root": null, + "asset": null, + "fileId": "71e36a35-a552-4298-90b5-3d1064fa30cb", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": [ + { + "__id__": 7 + }, + { + "__id__": 22 + }, + { + "__id__": 39 + }, + { + "__id__": 56 + } + ] + }, + { + "__type__": "cc.SceneGlobals", + "ambient": { + "__id__": 92 + }, + "shadows": { + "__id__": 93 + }, + "_skybox": { + "__id__": 94 + }, + "fog": { + "__id__": 95 + }, + "octree": { + "__id__": 96 + }, + "skin": { + "__id__": 97 + }, + "lightProbeInfo": { + "__id__": 98 + }, + "bakedWithStationaryMainLight": false, + "bakedWithHighpLightmap": false + }, + { + "__type__": "cc.AmbientInfo", + "_skyColorHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyColor": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.5, + "z": 0.8, + "w": 0.520833125 + }, + "_skyIllumHDR": 20000, + "_skyIllum": 20000, + "_groundAlbedoHDR": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_groundAlbedo": { + "__type__": "cc.Vec4", + "x": 0.2, + "y": 0.2, + "z": 0.2, + "w": 1 + }, + "_skyColorLDR": { + "__type__": "cc.Vec4", + "x": 0.452588, + "y": 0.607642, + "z": 0.755699, + "w": 0 + }, + "_skyIllumLDR": 0.8, + "_groundAlbedoLDR": { + "__type__": "cc.Vec4", + "x": 0.618555, + "y": 0.577848, + "z": 0.544564, + "w": 0 + } + }, + { + "__type__": "cc.ShadowsInfo", + "_enabled": false, + "_type": 0, + "_normal": { + "__type__": "cc.Vec3", + "x": 0, + "y": 1, + "z": 0 + }, + "_distance": 0, + "_shadowColor": { + "__type__": "cc.Color", + "r": 76, + "g": 76, + "b": 76, + "a": 255 + }, + "_maxReceived": 4, + "_size": { + "__type__": "cc.Vec2", + "x": 1024, + "y": 1024 + } + }, + { + "__type__": "cc.SkyboxInfo", + "_envLightingType": 0, + "_envmapHDR": { + "__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0", + "__expectedType__": "cc.TextureCube" + }, + "_envmap": { + "__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0", + "__expectedType__": "cc.TextureCube" + }, + "_envmapLDR": { + "__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0", + "__expectedType__": "cc.TextureCube" + }, + "_diffuseMapHDR": null, + "_diffuseMapLDR": null, + "_enabled": true, + "_useHDR": true, + "_editableMaterial": null, + "_reflectionHDR": null, + "_reflectionLDR": null, + "_rotationAngle": 0 + }, + { + "__type__": "cc.FogInfo", + "_type": 0, + "_fogColor": { + "__type__": "cc.Color", + "r": 200, + "g": 200, + "b": 200, + "a": 255 + }, + "_enabled": false, + "_fogDensity": 0.3, + "_fogStart": 0.5, + "_fogEnd": 300, + "_fogAtten": 5, + "_fogTop": 1.5, + "_fogRange": 1.2, + "_accurate": false + }, + { + "__type__": "cc.OctreeInfo", + "_enabled": false, + "_minPos": { + "__type__": "cc.Vec3", + "x": -1024, + "y": -1024, + "z": -1024 + }, + "_maxPos": { + "__type__": "cc.Vec3", + "x": 1024, + "y": 1024, + "z": 1024 + }, + "_depth": 8 + }, + { + "__type__": "cc.SkinInfo", + "_enabled": true, + "_blurRadius": 0.01, + "_sssIntensity": 3 + }, + { + "__type__": "cc.LightProbeInfo", + "_giScale": 1, + "_giSamples": 1024, + "_bounces": 2, + "_reduceRinging": 0, + "_showProbe": true, + "_showWireframe": true, + "_showConvex": false, + "_data": null, + "_lightProbeSphereVolume": 1 + } +] \ No newline at end of file diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene.meta b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene.meta new file mode 100644 index 0000000000..d6504e354d --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.scene.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.1.45", + "importer": "scene", + "imported": true, + "uuid": "71e36a35-a552-4298-90b5-3d1064fa30cb", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts new file mode 100644 index 0000000000..a24a50dcbc --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts @@ -0,0 +1,27 @@ +import { _decorator, animation, ccenum, Component, Node } from 'cc'; +const { ccclass, property, requireComponent } = _decorator; + +enum BaseLayerPose { + Standing, + Crouch, + Leaning_Original, +} +ccenum(BaseLayerPose); + +@ccclass('AdditivePoseDemonstration') +@requireComponent(animation.AnimationController) +export class AdditivePoseDemonstration extends Component { + @property({ type: BaseLayerPose }) + baseLayerPose = BaseLayerPose.Standing; + + @property({ range: [0.0, 1.0, 0.01], slide: true }) + additiveLayerWeight = 0.0; + + update(deltaTime: number) { + const controller = this.node.getComponent(animation.AnimationController); + controller.setValue('BaseLayerPose', this.baseLayerPose); + controller.setLayerWeight(1, this.additiveLayerWeight); + } +} + + diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts.meta b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts.meta new file mode 100644 index 0000000000..6df311a021 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/assets/AdditivePoseDemonstration/AdditivePoseDemonstration.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.23", + "importer": "typescript", + "imported": true, + "uuid": "9177bae6-0d0d-41c5-8806-45823b8d965d", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/package.json b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/package.json new file mode 100644 index 0000000000..c6a30e5e2c --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/package.json @@ -0,0 +1,7 @@ +{ + "name": "additive-pose-demonstration", + "uuid": "578bc671-b84c-4255-b3d5-49e0ac13ad5e", + "creator": { + "version": "3.8.0" + } +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/builder.json b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/builder.json new file mode 100644 index 0000000000..b2b2e5b558 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/builder.json @@ -0,0 +1,3 @@ +{ + "__version__": "1.3.5" +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/cocos-service.json b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/cocos-service.json new file mode 100644 index 0000000000..5ba4c7387f --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/cocos-service.json @@ -0,0 +1,23 @@ +{ + "__version__": "3.0.7", + "game": { + "name": "未知游戏", + "app_id": "UNKNOW", + "c_id": "0" + }, + "appConfigMaps": [ + { + "app_id": "UNKNOW", + "config_id": "b34724" + } + ], + "configs": [ + { + "app_id": "UNKNOW", + "config_id": "b34724", + "config_name": "Default", + "config_remarks": "", + "services": [] + } + ] +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/device.json b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/device.json new file mode 100644 index 0000000000..70e599e6c9 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/device.json @@ -0,0 +1,3 @@ +{ + "__version__": "1.0.1" +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/engine.json b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/engine.json new file mode 100644 index 0000000000..9f2f363f4b --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/engine.json @@ -0,0 +1,3 @@ +{ + "__version__": "1.0.7" +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/information.json b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/information.json new file mode 100644 index 0000000000..9a4b0a4f55 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/information.json @@ -0,0 +1,23 @@ +{ + "__version__": "1.0.0", + "information": { + "customSplash": { + "id": "customSplash", + "label": "customSplash", + "enable": true, + "customSplash": { + "complete": false, + "form": "https://creator-api.cocos.com/api/form/show?sid=e6da5f66f96264c0a291dafa04093ac7" + } + }, + "removeSplash": { + "id": "removeSplash", + "label": "removeSplash", + "enable": true, + "removeSplash": { + "complete": false, + "form": "https://creator-api.cocos.com/api/form/show?sid=e6da5f66f96264c0a291dafa04093ac7" + } + } + } +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/program.json b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/program.json new file mode 100644 index 0000000000..356db6b721 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/settings/v2/packages/program.json @@ -0,0 +1,3 @@ +{ + "__version__": "1.0.3" +} diff --git a/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/tsconfig.json b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/tsconfig.json new file mode 100644 index 0000000000..7dc649a956 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/additive-animation/source/additive-pose-demonstration/tsconfig.json @@ -0,0 +1,9 @@ +{ + /* Base configuration. Do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + /* Add your custom configuration here. */ + "compilerOptions": { + "strict": false + } +} diff --git a/versions/4.0/zh/animation/marionette/animation-controller.md b/versions/4.0/zh/animation/marionette/animation-controller.md new file mode 100644 index 0000000000..68478311ef --- /dev/null +++ b/versions/4.0/zh/animation/marionette/animation-controller.md @@ -0,0 +1,71 @@ +# 动画控制器(Animation Controller)组件参考 + +动画控制器组件用于将 [动画图资源](animation-graph.md) 应用到对象上。 + +在 **层级管理器** 中选中需要应用动画图的节点,然后在 **属性检查器** 中点击下方的 **添加组件 -> Animation -> Animation Controller**,即可添加一个动画控制器组件到节点上。 + +![add-animation-controller](animation-controller/add-animation-controller.png) + +> **注意**:因为 Marionette 动画系统与旧式动画系统不可同时使用,因此 **动画控制器组件也不能和动画组件或者骨骼动画组件挂载在同一个节点上**。 + +## 属性 + +![animation-controller](animation-graph/animation-controller.png) + +| 属性 | 说明 | +| :---- | :----------- | +| Graph | 添加的 [动画图资源](animation-graph.md),默认为空 | + +## 程序化控制动画图 + +动画控制器组件提供了一些方法用于在运行时控制动画图的 [动画状态机](animation-graph-basics.md): + +| 方法 | 说明 | +| :----------------------- | :----------------------- | +| `getVariables` | 获取所有的变量及变量名/类型/值等相关信息。| +| `setValue` | 设置变量的值。 | +| `getValue` | 获取变量的值。 | +| `getCurrentStateStatus` | 获取当前状态的信息。 | +| `getCurrentClipStatuses` | 获取当前的动画剪辑的状态。 | +| `getCurrentTransition` | 获取当前的状态过渡。 | +| `getNextStateStatus` | 获取下一个状态的信息。 | +| `getNextClipStatuses` | 获取下一个动画剪辑的状态。 | +| `setLayerWeight` | 设置某个 [动画图层级](animation-graph-layer.md) 的权重。| + +- **变量** 控制 + + 例如我们在 [动画图面板](./animation-graph-panel.md#%E5%8F%98%E9%87%8F%EF%BC%88variables%EF%BC%89) 添加了多个变量,通过代码可以获取和修改变量的值,代码示例如下: + + ```ts + // 获取动画控制器组件 + let animationController:animation.AnimationController = this.node.getComponent(animation.AnimationController) + + // 获取所有的变量 + let variables= animationController.getVariables(); + + // 获取名为 ‘vertical’ 的变量的值 + let vertical: Number = animationController.getValue("vertical"); + + // 修改名为 ‘vertical’ 的变量的值为 1.0。修改后,将会在下一帧 update 的时候计算条件过渡 + animationController.setValue("vertical", 1.0) + ``` + + > **注意**:动画状态机的变量目前只能在 [动画图面板](animation-graph-panel.md#%E5%8F%98%E9%87%8F%EF%BC%88variables%EF%BC%89) 进行添加和删除。 + +- 获取当前 **状态** + + 获取第 0 层级状态机当前 **状态** 的归一化进度,代码示例如下: + + ```ts + let states: animation.MotionStateStatus = animationController.getCurrentStateStatus(0) + console.log(states.progress); + ``` + +- 获取当前 **过渡** + + 获取第 0 层级状态机即将发生的 **过渡** 的相关信息: + + ```ts + let transition: animation.TransitionStatus = animationController.getCurrentTransition(0) + console.log(transition.duration, transition.time) + ``` diff --git a/versions/4.0/zh/animation/marionette/animation-controller/add-animation-controller.png b/versions/4.0/zh/animation/marionette/animation-controller/add-animation-controller.png new file mode 100644 index 0000000000..289921eb51 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-controller/add-animation-controller.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics.md b/versions/4.0/zh/animation/marionette/animation-graph-basics.md new file mode 100644 index 0000000000..8b3f52900a --- /dev/null +++ b/versions/4.0/zh/animation/marionette/animation-graph-basics.md @@ -0,0 +1,303 @@ +# 动画状态机 + +我们把对象所处的播放某种特定动画的动作,例如待机、移动、奔跑、攻击等,称为 **状态**。
+一般情况下,一个对象拥有多个状态,并按照一定的逻辑顺序在状态之间切换,以执行不同的动作,这种切换在动画图中称为 [过渡](state-transition.md)。发生过渡时需要满足的条件称为 **过渡条件**。 + +而动画状态机则是用于管理和控制对象上各个状态及状态之间的过渡,类似于流程图,我们可以直接在 [动画图面板](animation-graph-panel.md) 中对其进行可视化编辑。目前一个动画图包含一个状态机,当状态机位于动画图中的某个状态时,便会播放该状态对应的动画。那么按照事先搭建好的动画流程图,通过状态机便可自动控制骨骼动画的播放和切换等。 + +## 状态基础 + +状态机中的状态除了用于控制动画播放的状态,还包括了 **入口**、**出口**、**任意**、**子状态机** 这几个特殊的状态,因为其本身并不承载动画,只是为了标志状态机开始、结束等特殊用途,因此为了与状态区分我们暂且称之为“伪状态”。 + +[子状态机](#%E5%AD%90%E7%8A%B6%E6%80%81%E6%9C%BA) 是嵌套在状态机中的,用于降解复杂的动画逻辑,具体可查看下文 **子状态机** 部分的内容。一般我们将动画图最上层的状态机称为 **顶层状态机**,类似于场景中的根节点。 + +**入口**(Entry)、**出口**(Exit)、**任意**(Any)则是默认存在于状态机中的固定单元,不可删除。在打开动画图编辑状态机时便可在网格布局区域中看到,需要注意的是 **出口** 只存在于 **子状态机** 中。 + +![animation-graph-panel](animation-graph-panel/animation-graph-panel.png) + +- **入口**:标志着状态机的进入。**入口** 只能作为状态过渡发生的源头,无法作为过渡的目标。 + +- **出口**:当状态过渡到 **出口** 后,退出状态机。**出口** 只能作为状态过渡的目标,无法作为过渡的源头。 + + **出口仅存在于子状态机中**,因为退出子状态机后,需要返回到父状态机继续执行下一个过渡;若退出顶层状态机,则动画流程终止,角色静止不动,而在一般情况下,角色是在持续做运动的。
若需要让角色完全静止不动,可以通过关闭当前动画图所在的动画控制器组件来实现。 + +- **任意**:可指代状态机中其它任何状态,但不包括“伪状态”。**任意** 只能作为状态过渡的源头,无法作为过渡的目标。 + + 从 **任意** 中引出过渡等价于从所有状态都引出了该过渡,也就是说,状态机中所有状态在满足某个条件时都会切换到另一个状态,就可以使用 **任意**。例如角色在走、跑、跳等状态下触发攻击条件,都需要切换到攻击状态。 + + > **注意**:**任意** 是可以继承的,若在父状态机中设置了从 **任意** 中引出状态过渡,则其子状态机中所有的状态若满足条件都会执行该过渡。 + +## 创建状态 + +状态机由状态和状态之间的过渡组成,但首先我们需要先创建基础单元 — **状态**。 + +右键点击 [动画图面板](animation-graph-panel.md) 的网格布局区域,即可在菜单中选择添加状态: + +- **动画剪辑**:用于播放指定的动画剪辑,默认名为 `Clip Motion`; +- **一维/二维混合**:用于播放指定的一维/二维动画混合,默认名为 `Blend 1D`/`Blend 2D`。动画混合是由多个动画混合而得到的新动画,详情请参考下文的 **动画混合**。 +- **子状态机**:用于播放子状态机中的动画内容,默认名为 `State Machine`。详情请参考下文的 **子状态机**。 + +这些状态都以蓝色图形显示在网格区域中。 + +![Edit](animation-graph-basics/edit.png) + +### 设置状态属性 + +选中创建的动画状态,蓝色图形外框会显示高亮,并且 **属性检查器** 中也会显示可设置的状态属性: + +![state-property](animation-graph-basics/state-property.png) + +> 这里是以状态为例展开介绍,动画混合和子状态机可设置的属性会有所差异,具体请参考下文相应的内容介绍。 + +- 当前选中状态的名称会显示在 **属性检查器** 上,例如上图中的 `Clip Motion`,点击右侧的齿轮图标按钮可 **重命名** 或 **删除此状态**。 + +- **速度(Speed)**:用于设置当前状态上挂载的动画剪辑的播放速度,默认为 1,值越小播放速度越慢。 +- **速度乘数**:启用后,可为速度指定一个乘积因子。速度乘积因子会与速度相乘来控制动画状态的速度。 + + ![speed-multiplier](animation-graph-basics/speed-multiplier.png) + +- **动画剪辑内容**:用于指定当前状态上挂载的动画剪辑。可直接从 **资源管理器** 中将动画剪辑拖拽到 **Clip** 属性框中,也可以直接点击 **Clip** 属性框后面的箭头按钮选择。设置了动画剪辑后,当前状态将会播放指定的动画剪辑内容,并且状态名称也会随之更换为动画剪辑名称。 + + > **注意**:若未指定动画剪辑,将会导致问题。 + + 点击右侧的齿轮图标菜单,可删除当前动画剪辑。点击 **添加动画** 便可根据需要重新选择添加动画剪辑内容/一维动画混合/二维动画混合。 + + ![add-motion](animation-graph-basics/add-motion.png) + +- **状态组件**:用于添加动画状态组件。动画状态组件的基类中提供了一些方法,表示状态机的一些特定事件,用于在状态进入、离开时添加一些业务逻辑。详情请参考下文 **动画状态组件** 部分的内容。同样的,点击右侧的齿轮图标按钮,即可删除当前动画状态组件并根据需要重新选择;点击右上方的齿轮图标按钮可同时添加多个状态组件。 + +- 点击状态名右侧的齿轮图标菜单,支持 **删除此状态**/**重命名** + + ![change-state](animation-graph-basics/change-state.png) + +- 动画剪辑右方的齿轮支持 **替换为动画剪辑**/**替换为一维动画混合**/**替换为二维动画混合**/**删除此动画** + + ![clip-menu](animation-graph-basics/clip-menu.png) + +- 点击下方的 **添加状态组件** 按钮可状态组件脚本,详情请参考下方 **动画状态组件** + +## 动画混合 + +动画混合可以根据输入参数和预设的一些参数将多个动画进行混合,用于实现类似根据速度混合走和跳的效果,建议参加混合的动画尽量相似,使混合后的运动尽量合理。根据输入参数的不同可将动画混合分为以下几种混合方式: + +- **一维混合**:根据 **一个输入参数** 通过 **线性插值算法** 对多个动画进行混合。 + +- **二维混合**:根据 **两个输入参数** 通过 **重心坐标插值混合算法** 对多个动画进行混合。 + +动画混合跟 [状态过渡](state-transition.md) 的区别在于: + +- 过渡是在给定的周期内从一个状态平滑过渡到其他状态。 + +- 混合是通过混合参数在不同的程度下合并多个动画,使其平滑混合。 + +### 添加动画混合 + +右键点击动画图面板的网格布局区域即可选择添加 **一维动画混合** 或者 **二维动画混合**。创建完成后在 **属性检查器** 中即可设置属性,除了动画混合设置项,其他项与上文 **设置状态属性** 中的一致。 + +![blend edit](animation-graph-basics/blend-edit.png) + +进入动画混合的编辑页面可以设置相关参数及参与混合的动画,进入方式包括以下三种: + +- 点击动画混合设置项中的 **编辑** +- 双击网格布局中的混合状态 +- 右键点击网格布局中的混合状态,然后选择 **编辑** + +退出动画混合的编辑页面,包括以下两种方式: + +- 右键点击网格布局区域,然后选择 **返回上层编辑** +- 网格布局区域左上方的 **位置** 表示当前所处位置,点击层级即可返回相应位置 + +![create blend](animation-graph-basics/create-blend.gif) + +### 一维动画混合 + +**一维动画混合** 是根据 **一个输入参数** 通过 **线性插值算法** 混合多个动画,每个动画都可配置一个阈值。 + +- 当输入参数恰好位于某个阈值上,就使用该阈值上的动画 +- 当输入参数位于两个阈值之间,将根据输入参数在阈值区间上的比例 **线性** 混合这两个动画。线性公式为 **A * (1 - t) + B * t**,其中 A、B 表示阈值,t 表示输入参数值在此阈值区间上的比率。 + +进入动画混合编辑页面,在网格布局区域点击右键,即可根据需要添加参与混合的动画,支持添加 **动画剪辑**、**一维动画混合**、**二维动画混合**。 + +![add-motion-of-blend](animation-graph-basics/add-motion-of-blend1D.png) + +- **入参**:该项用于选择动画混合的输入参数。输入参数及参数值可在动画图面板的 **属性检查器** 中创建,目前支持使用 **浮点型** 变量,详情请参考 [动画图面板 — 变量](animation-graph-panel.md)。
输入参数也可以直接在网格布局区域的动画混合中选择。 + +- **阈值**:当动画混合上添加了动画后该项才可见。每添加一个动画,下方便会新增一个相对应的阈值,可根据需要手动设置每个阈值大小;也可以勾选 **自动调整阈值** 自动均分各个阈值,二者选其一。所有的阈值按从小到大的顺序从上到下排列。 + +- 可直接在网格布局区域的动画上挂载动画剪辑,也可以选中动画后在 **属性检查器** 中指定动画剪辑。 + + > **注意**:若未指定动画剪辑,将会导致问题。 + + ![choose clip](animation-graph-basics/choose-clip.png) + + 若需要移除动画,则直接在网格布局区域中右键点击动画以删除,也可以选中动画后在 **属性检查器** 中点击齿轮图标按钮以删除。 + +例如要实现根据角色速率来均匀混合待机、走、跑的动画,可设置如下图: + +![blend-speed](animation-graph-basics/blend1-speed.png) + +### 二维动画混合 + +**二维动画混合** 是根据 **两个输入参数** 通过 **重心坐标插值算法** 混合多个动画,每个动画都可配置一个阈值。 + +二维动画混合的基本操作与一维动画混合基本一致,区别在于: +- 二维动画混合在 **属性检查器** 上的 **入参** 有两个; +- 阈值是以 `[x, y]` 表示,按照动画的添加顺序自上而下排列。并且没有 **自动调整阈值** 选项。 +- 第一个输入参数的值对应的是下方阈值 `X` 的区间,第二个输入参数的值对应的是下方阈值 `Y` 的区间。 + +二维动画混合经常用于插值混合不同方向上的移动动画,例如: + +![动画混合 2D](animation-graph-basics/blend2.png) + +上图中第一个输入参数的范围是 `[-1, 1]`,第二个输入参数的范围也是 `[-1, 1]`。 + +## 子状态机 + +因为对象一般会有多个阶段的复杂动作组合,为了降解复杂的逻辑,还可以在状态机中(递归地)嵌套 **子状态机**。一般我们将动画图最上层的状态机称为 **顶层状态机**,类似于场景中的根节点。 + +右键点击网格布局区域,然后选择 **添加子状态机** 即可创建。创建完成后在网格布局区域会生成一个默认名为 `State Machine` 的子状态机: + +![set sub state](animation-graph-basics/set-sub-state.png) + +- 双击子状态机可以进入编辑页面; +- ① 号按钮用于添加状态组件,详情请参考下文 **动画状态组件** 部分的内容; +- 点击齿轮图标按钮,展开的菜单中包括 **重命名**/**删除此状态**。 + +### 编辑子状态机 + +进入子状态机编辑页面后,界面如下图所示。子状态机的编辑与顶层状态机基本一致,主要的区别在于,子状态机中默认包含 **出口** 伪状态,当状态过渡到 **出口** 后,退出子状态机,返回到父状态机继续执行下一个过渡,直到回到顶层状态机继续下一个过渡。详情可参考上文 **状态机基础** 部分的内容。 + +![edit sub state](animation-graph-basics/edit-sub-state.png) + +点击界面上方的 **位置** 便可根据需要跳转到任一个上层状态机。右键点击网格布局区域,然后选择 **返回上层编辑** 便可返回父状态机。 + + + +## 状态机事件绑定 + +状态机事件绑定用于在状态机发生特定事件时进行相关的处理。 + +每个事件绑定需要指定一个 **方法名称**,当事件发生时,动画控制器所在结点上的所有组件的同名方法将被调用。 + +### 状态相关事件 + +当选中状态对象时,可在属性检查器中绑定该状态的进入、退出事件。 + +![Alt text](animation-graph-basics/state-event-bindings.png) + +> 仅有动作状态和程序化姿态(状态)提供了事件绑定。 + +| 事件类型 | 触发时机 | +|----------|--------------------------| +| transitionInEventBinding | 当开始进入此状态时触发。 | +| transitionOutEventBinding | 当开始离开此状态时触发。 | + +### 过渡相关事件 + +当选中过渡对象时,可在属性检查器中绑定该过渡对象的开始和结束事件。 + +![Alt text](animation-graph-basics/transition-event-bindings.png) + +> 仅当过渡源头是动作状态、程序化姿态或空状态时,才提供事件绑定。 + +| 事件类型 | 触发时机 | +|----------|----------------------| +| startEventBinding | 当此过渡开始时触发。 | +| endEventBinding | 当此过渡结束时触发。 | + + +## 动画状态组件 + +> 相较于动画状态组件, +> [状态机事件绑定](#状态机事件绑定) 提供了更方便的机制处理状态机事件。 + +状态和子状态机都可以挂载动画状态组件。动画状态组件可直接在 **资源管理器** 中创建,右键点击 **资源管理器** 左上方的 **+** 按钮,然后选择 **动画状态组件** 即可: + +![create-graph-script](animation-graph-basics/create-graph-script.png) + +创建完成后会生成一个默认名为 **AnimationGraphComponent** 的组件: + +![animation-graph-component](animation-graph-basics/animation-graph-component.png) + +创建好动画状态组件后,在动画图面板右侧的网格布局区域中选中状态,状态上点击鼠标右键的弹出菜单或在 **属性检查器** 中点击 **添加状态组件** 即可将组件挂载到当前状态。支持添加多个。 + +![add-graph-script](animation-graph-basics/add-graph-script.png) + +点击添加的状态组件右侧的齿轮图标按钮,即可删除当前状态组件: + +![create-graph-script](animation-graph-basics/remove-graph-script.png) + +### 方法 + +动画状态组件类继承自 `animation.StateMachineComponent`,其基类中提供了一些方法,表示状态机的一些特定事件。子类可以覆写这些方法,以便在状态进入、离开时添加一些业务逻辑。 + +| 方法名 | 说明 | +| :-- | :-- | +| `onMotionStateEnter` | 在进入状态时调用 | +| `onMotionStateExit` | 在完全退出状态时调用 | +| `onMotionStateUpdate` | 在状态更新时调用 | +| `onStateMachineEnter` | 在进入子状态机时调用 | +| `onStateMachineExit` | 在退出子状态机时调用 | + +以上表格中的方法可能接受以下参数: + +- `controller: animation.AnimationController`:表示运行此动画图的动画控制器组件。上述所有方法都会接受此参数。 + +- `motionStateStatus: animation.MotionStateStatus`:表示事件主体动作状态的运作状态。仅状态(伪状态除外)相关的方法(`onMotionStateEnter`、`onMotionStateExit`)可接受此参数。 + +### 示例:进入状态时播放特效 + +```ts +import { _decorator, animation, PhysicsSystem, ParticleSystem } from "cc"; +const { ccclass, property } = _decorator; + +@ccclass("AnimationGraphComponent") +export class AnimationGraphComponent extends animation.StateMachineComponent { + /** + * Called when a motion state right after it entered. + * @param controller The animation controller it within. + * @param stateStatus The status of the motion. + */ + public onMotionStateEnter (controller: animation.AnimationController, stateStatus: Readonly): void { + // 播放动画控制器组件所在节点上的所有粒子特效 + for (const particleSystem of controller.node.getComponents(ParticleSystem)) { + particleSystem.play(); + } + } +} +``` + +## 预览 + +### 预览动画剪辑 + +选中任意状态或混合后,在 **属性检查器** 内可以进行预览。 + +![preview](animation-graph-basics/preview-clip.png) + +预览时,用户可以通过下列按钮进行操作: + +![bar](state-transition/preview-bar.png) + +其属性与描述如下: + +- ![start](preview-bar/start.png) 跳转至第一帧 +- ![play](preview-bar/prev.png) 预览前一帧 +- ![play](preview-bar/play.png)/![pause](preview-bar/pause.png) 开始/暂停播放 +- ![stop](preview-bar/stop.png) 停止播放 +- ![end](preview-bar/next.png) 跳转至后一帧 +- ![end](preview-bar/end.png) 跳转至最后一帧 + +### 预览混合 + +混合在预览时,除上述操作外,也可以通过调整混合的变量值来进行预览,此处的修改并不会保存。 + +- 一维混合: + + ![blend1d](animation-graph-basics/preview-blend1.png) + +- 二维混合: + + ![blend2d](animation-graph-basics/preview-blend2.png) diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/add-graph-script.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/add-graph-script.png new file mode 100644 index 0000000000..64e57ef77e Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/add-graph-script.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/add-motion-of-blend1D.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/add-motion-of-blend1D.png new file mode 100644 index 0000000000..d95b22c1c2 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/add-motion-of-blend1D.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/add-motion.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/add-motion.png new file mode 100644 index 0000000000..aea7919dd5 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/add-motion.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/animation-graph-component.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/animation-graph-component.png new file mode 100644 index 0000000000..1b5cd4cfcf Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/animation-graph-component.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/blend-edit-panel.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend-edit-panel.png new file mode 100644 index 0000000000..b3a0cf1c10 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend-edit-panel.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/blend-edit.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend-edit.png new file mode 100644 index 0000000000..98a5cb3dab Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend-edit.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/blend-property.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend-property.png new file mode 100644 index 0000000000..dd43f4bf9b Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend-property.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/blend1-speed.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend1-speed.png new file mode 100644 index 0000000000..ed9f0060ec Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend1-speed.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/blend2.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend2.png new file mode 100644 index 0000000000..103bf1a557 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/blend2.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/change-state.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/change-state.png new file mode 100644 index 0000000000..c63203fc07 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/change-state.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/choose-clip.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/choose-clip.png new file mode 100644 index 0000000000..e51a0f12f6 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/choose-clip.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/clip-menu.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/clip-menu.png new file mode 100644 index 0000000000..037b6d0e46 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/clip-menu.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/create-blend.gif b/versions/4.0/zh/animation/marionette/animation-graph-basics/create-blend.gif new file mode 100644 index 0000000000..596cd7ca0b Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/create-blend.gif differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/create-graph-script.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/create-graph-script.png new file mode 100644 index 0000000000..ee42c28c16 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/create-graph-script.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/edit-sub-state.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/edit-sub-state.png new file mode 100644 index 0000000000..07141a1e47 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/edit-sub-state.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/edit.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/edit.png new file mode 100644 index 0000000000..6caa4d878a Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/edit.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/example.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/example.png new file mode 100644 index 0000000000..584eee47ea Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/example.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/preview-blend1.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/preview-blend1.png new file mode 100644 index 0000000000..a7efb4d208 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/preview-blend1.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/preview-blend2.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/preview-blend2.png new file mode 100644 index 0000000000..66037279fa Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/preview-blend2.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/preview-clip.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/preview-clip.png new file mode 100644 index 0000000000..05fd2e8adc Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/preview-clip.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/remove-graph-script.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/remove-graph-script.png new file mode 100644 index 0000000000..894c222930 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/remove-graph-script.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/set-sub-state.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/set-sub-state.png new file mode 100644 index 0000000000..4f767b4723 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/set-sub-state.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/speed-multiplier.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/speed-multiplier.png new file mode 100644 index 0000000000..af77ddcfee Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/speed-multiplier.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/state-event-bindings.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/state-event-bindings.png new file mode 100644 index 0000000000..b83718a9f5 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/state-event-bindings.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/state-property.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/state-property.png new file mode 100644 index 0000000000..359f57d8bb Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/state-property.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/sub-state.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/sub-state.png new file mode 100644 index 0000000000..51f4862185 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/sub-state.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-basics/transition-event-bindings.png b/versions/4.0/zh/animation/marionette/animation-graph-basics/transition-event-bindings.png new file mode 100644 index 0000000000..17f7539bc8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-basics/transition-event-bindings.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-layer.md b/versions/4.0/zh/animation/marionette/animation-graph-layer.md new file mode 100644 index 0000000000..6a01ed8099 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/animation-graph-layer.md @@ -0,0 +1,93 @@ +# 动画图层级 + +动画图可以有多个层级,层级之间同时运行,其上播放的动画效果可以根据层级的配置进行混合。 + +## 创建层级 + +![add-layer](animation-graph-panel/add-layer.png) + +在动画图中点击 **层级(Layers)** 按钮,点击 ![add layer](animation-graph-panel/btn-add-layer.png) 按钮,在弹出的输入框内输入层级的名字即可创建层级。 + +## 删除层级 + +鼠标悬停在层级上后,可以点击层级右侧的 **×** 进行删除,此操作仅在有多个层级时生效。 + +![remove](animation-graph-panel/remove-layer.png) + +## 重命名层级 + +在层级名字处双击鼠标左键可对层级进行重命名: + +![rename](animation-graph-panel/rename-layer.png) + +## 层级属性 + +在动画图层级中选中任意层级后,在 **属性查看器** 内可以查看层级属性。 + +![inspector](animation-graph-panel/layer-inspector.png) + +| 属性 | 说明 | +| :-- | :-- | +| **Mask** | 动画遮罩,用于指定对应的 [动画遮罩资源](animation-mask.md) | +| **Weight** | 层级权重,用于指定当前层级在和 **先前层级** 混合时的权重,取值范围为 [0, 1] | + +### 动画遮罩 + +从 **资源管理器** 拖拽动画遮罩到动画图窗口的 **遮罩(Mask)** 属性或者在层级中选中 **遮罩(Mask)** 属性都可以将遮罩赋予该层级。 + +操作步骤可参考下图,按照 1,2,3 步骤: + +- 选中任意层级 +- 在 **属性检查器** 内点击下拉菜单 +- 选中提前创建好的 **遮罩** 资源或从 **资源管理器** 拖拽到此处 + +![select](animation-mask/select.png) + +指定后,该层级上的所有动画效果都受到该遮罩的影响。 + +例如,可以向层级指定一个仅保留上半身骨骼、下半身骨骼都被禁用的动画遮罩,则该层级的动画效果将仅作用于上半身。 + +### 层级权重 + +每一层级的动画效果将以一定比例与前面层级的动画效果进行混合,该混合的比例由层级的权重属性指定。 + +0 代表完全使用前面层级的动画效果,1 代表该层级完全覆盖前面层级的动画效果,当处于 [0,1] 时则进行适当的混合。 + +层级的混合仅会混合那些未被动画遮罩禁用的骨骼。例如,若层级 1 启用了某骨骼,但其后的层级 2 利用动画遮罩禁用了该骨骼,则 +无论层级 2 权重指定为多少,该骨骼都仅全量播放层级 1 的动画效果。 + +也可以在代码中通过 `AnimationController.prototype.setLayerWeight` 动态修改层级权重。 + +## 网格布局区域操作 + +在网格内空白处点击鼠标右键可以打开层级菜单。 + +![Edit](animation-graph-panel/edit.png) + +菜单中可以添加 **子状态、子状态机、混合以及空状态**。 + +- **子状态、子状态机** 和 **混合** 的使用方式请参考 [动画状态机](animation-graph-basics.md)。 + +- **添加空状态**:该菜单会在层级上创建一个默认名为 Empty 的空状态,请参考下文 **空状态** 部分获取更多信息。 + + ![create empty](animation-layer/create-empty.png) + +- **返回中心视角**:该菜单会将层级的视角返回到层级的中心。 + +- 按住鼠标右键或中键,可以移动层级网格;使用滚轮滚动可以对层级进行缩放。 + +## 空状态 + +很多情况下,一种需求是:仅在特定情况下,启用某一层级;其它情况下都使此层级不生效。 + +对于这种需求,可以使用 **空状态**。 **空状态** 可以看作是一种动画,它的动画效果就是前面层级的动画效果。 + +从 **空状态** 状态引出的过渡,就相当于前面层级的动画效果逐渐过渡到目标动画。此过渡上可以指定过渡条件、过渡周期。但过渡周期仅可指定为绝对的时长(以秒为单位)。 + +![layer](animation-layer/empty-state.png) + +引出到 **空状态** 状态的过渡,就相当于源头动画的动画效果在不断衰减,直到过渡完成,相当于当前层级被禁用。 + +如在下图的示例中,当 **Empty** 仅在触发器 **Trigger** 被触发后才会过渡到 **RunShoot** 状态,而在 **Empty** 状态下,该层级不会影响到前面层级的动画效果。 + +![duration](animation-layer/transit-empty-state.png) diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel.md b/versions/4.0/zh/animation/marionette/animation-graph-panel.md new file mode 100644 index 0000000000..b73ac9c35e --- /dev/null +++ b/versions/4.0/zh/animation/marionette/animation-graph-panel.md @@ -0,0 +1,88 @@ +# 动画图面板 + +动画图面板用于查看和编辑动画图资源。当准备好对象所需的骨骼动画后,便可在 **动画图面板** 中将其组装结合成完整的动画流程。 + +## 打开面板 + +在 **资源管理器** 中选中动画图资源,然后在 **属性检查器** 中点击 **编辑**: + +![open-animation-graph-panel](animation-graph-panel/open-animation-graph-panel.png) + +即可打开动画图面板,可以看到动画图面板主要由网格布局区域,以及右侧的 **层级**、**变量** 分页构成: + +![animation-graph-panel](animation-graph-panel/animation-graph-panel.png) + + + +## 层级(Layers) + +该分页主要用于创建、查看和编辑动画图的层级,每个层级分别由一个状态机控制,点击 **Layers** 按钮可以进入层级编辑状态。 + +![Layers](animation-graph-panel/layers.png) + +更多关于层级的信息请参考 [动画图层级](animation-graph-layer.md)。 + +## 网格布局区域 + +该区域主要展示了 **层级** 分页中所选的层级对应的状态机,用户可以可视化地创建、编辑、排列状态机中各个状态,以及设置状态之间的过渡。在区域内点击右键即可根据需要创建各类状态: + +![Edit](animation-graph-panel/edit.png) + +当编辑完成后可根据需要在右上角点击 **保存** 或者 **重置**(重置到上一次保存时)。更多操作请参考 [动画状态机](animation-graph-basics.md)。 + +## 变量(Variables) + +该分页主要用于创建、查看和编辑当前动画图层中状态机的变量,用户可在动画图中通过自定义的 **变量** 来操控动画图表达的流程,作用如下: + +- 作为状态之间过渡的 **过渡条件** + +- 作为混合动画状态的 **输入参数** 来混合不同的动画剪辑 + +![variables](animation-graph-panel/variables.png) + +### 创建变量 + +以下图为例,操作步骤为: +- 在下拉框中选择变量的 **类型**。支持的变量类型包括 **浮点型**、**布尔类型**、**触发器类型** 和 **整型**,具体说明请查看下文 **变量类型** 部分的内容。 +- 在 **名称** 输入框中填写变量名 + +![add variables](animation-graph-panel/add-variables.png) + +鼠标滑过变量时,每个创建好的变量右侧会有一个 **×** 图标按钮,点击即可删除对应变量。设置完成后请不要忘记点击动画图面板右上角的 **保存** 按钮以保存设置。 + +![delete variables](animation-graph-panel/delete-variable.png) + +### 变量类型 + +目前支持创建的变量类型包括: + +- **浮点型**:变量的值可以是任意实数 + +- **布尔类型**:变量的值可以为真/假 + +- **整型**:变量的值可以是任意整数 + +- **触发器类型**:变量为触发器,若勾选则表示值为触发。触发器用于一次性的条件判断,当触发条件满足时,发生状态过渡,过渡完成后,所引用到的所有触发器都会被重置为 **未触发状态**。 + + > **注意**:仅当过渡到状态时,触发器才会被重置,若是过渡到“伪状态”,触发器不会被重置。 + + 例如下图,只要在 ① 中设置触发器 T1 或 T2 一次,状态 A 就能达到子状态机中的状态 B 或者 C,不需要再次设置 ② 和 ③ 中的触发器: + + ![variable](animation-graph-panel/variable-eg.png) + + + 在 v3.5.0 中,我们增加了新的机制 **下一帧重置(Reset on next frame)**,在勾选后,触发器会在动画更新后立即重置。 + + ![reset on next frame](animation-graph-panel/reset-on-nextframe.png) + +### 通过脚本控制变量 + +开发者也可以在脚本中通过动画控制器组件获取和修改变量的值,详情请参考 [动画控制器组件参考](animation-controller.md)。 + + diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/add-layer.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/add-layer.png new file mode 100644 index 0000000000..23d9cc20b8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/add-layer.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/add-variables.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/add-variables.png new file mode 100644 index 0000000000..0c6a966031 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/add-variables.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/animation-graph-panel.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/animation-graph-panel.png new file mode 100644 index 0000000000..961b5e4542 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/animation-graph-panel.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/btn-add-layer.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/btn-add-layer.png new file mode 100644 index 0000000000..01e6b73f53 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/btn-add-layer.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/delete-variable.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/delete-variable.png new file mode 100644 index 0000000000..90d2f03485 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/delete-variable.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/edit.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/edit.png new file mode 100644 index 0000000000..99fa0d4387 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/edit.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/inspector.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/inspector.png new file mode 100644 index 0000000000..56f49212b1 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/layer-inspector.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/layer-inspector.png new file mode 100644 index 0000000000..bb2a152b09 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/layer-inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/layers.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/layers.png new file mode 100644 index 0000000000..1f8c16d5c8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/layers.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/open-animation-graph-panel.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/open-animation-graph-panel.png new file mode 100644 index 0000000000..3abcc30d4a Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/open-animation-graph-panel.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/remove-layer.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/remove-layer.png new file mode 100644 index 0000000000..7eabda741a Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/remove-layer.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/rename-layer.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/rename-layer.png new file mode 100644 index 0000000000..8204adc8f8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/rename-layer.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/reset-on-nextframe.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/reset-on-nextframe.png new file mode 100644 index 0000000000..76925ac4c8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/reset-on-nextframe.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/variable-eg.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/variable-eg.png new file mode 100644 index 0000000000..25f39f35f3 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/variable-eg.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph-panel/variables.png b/versions/4.0/zh/animation/marionette/animation-graph-panel/variables.png new file mode 100644 index 0000000000..0e3c14b715 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph-panel/variables.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph.md b/versions/4.0/zh/animation/marionette/animation-graph.md new file mode 100644 index 0000000000..e193ee14ec --- /dev/null +++ b/versions/4.0/zh/animation/marionette/animation-graph.md @@ -0,0 +1,29 @@ +# 动画图资源 + +动画图资源用于存储对象的整个动画流程数据,通过状态机描述动画的流程,目前一个动画图只支持一个状态机。 + +## 创建 + +在 **资源管理器** 点击左上方的 **+** 按钮,然后选择 **动画图(Animation Graph)**: + +![create-animation-graph](animation-graph/create-animation-graph.png) + +即可创建一个默认名为 `Animation Graph` 的动画图资源: + +![animation-graph-asset](animation-graph/animation-graph-asset.png) + +## 编辑 + +动画图创建完成后即可在动画图面板中进行编辑,详情请参考 [动画图面板](animation-graph-panel.md)。 + +## 应用 + +动画图资源需要依赖于 [动画控制器(Animation Controller)组件](animation-controller.md) 才能应用到对象上。 + +参考 [动画控制器组件](animation-controller.md) 文档,在节点上添加动画控制器组件,然后点击动画控制器组件的 **Graph** 属性框后面的箭头图标按钮选择动画图资源: + +![animation-graph-select](animation-graph/animation-graph-select.png) + +或者直接将 **资源管理器** 中的动画图资源拖拽到动画控制器组件的 **Graph** 属性框中即可: + +![animation-controller](animation-graph/animation-controller.png) diff --git a/versions/4.0/zh/animation/marionette/animation-graph/animation-controller.png b/versions/4.0/zh/animation/marionette/animation-graph/animation-controller.png new file mode 100644 index 0000000000..50a05c5abb Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph/animation-controller.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph/animation-graph-asset.png b/versions/4.0/zh/animation/marionette/animation-graph/animation-graph-asset.png new file mode 100644 index 0000000000..9c902f373f Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph/animation-graph-asset.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph/animation-graph-select.png b/versions/4.0/zh/animation/marionette/animation-graph/animation-graph-select.png new file mode 100644 index 0000000000..2a15927c1c Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph/animation-graph-select.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph/create-animation-graph.png b/versions/4.0/zh/animation/marionette/animation-graph/create-animation-graph.png new file mode 100644 index 0000000000..e20b078933 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph/create-animation-graph.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-graph/create-empty-state.png b/versions/4.0/zh/animation/marionette/animation-graph/create-empty-state.png new file mode 100644 index 0000000000..a9b29aaaa2 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-graph/create-empty-state.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-layer/create-empty.png b/versions/4.0/zh/animation/marionette/animation-layer/create-empty.png new file mode 100644 index 0000000000..48d6c42ede Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-layer/create-empty.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-layer/empty-state.png b/versions/4.0/zh/animation/marionette/animation-layer/empty-state.png new file mode 100644 index 0000000000..42a8561bc3 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-layer/empty-state.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-layer/transit-empty-state.png b/versions/4.0/zh/animation/marionette/animation-layer/transit-empty-state.png new file mode 100644 index 0000000000..ba999f76fc Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-layer/transit-empty-state.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask.md b/versions/4.0/zh/animation/marionette/animation-mask.md new file mode 100644 index 0000000000..60035f7f5f --- /dev/null +++ b/versions/4.0/zh/animation/marionette/animation-mask.md @@ -0,0 +1,57 @@ +# 动画遮罩资源 + +动画遮罩是一种资源(Asset),用在动画图层级中,用于屏蔽指定骨骼上的动画效果。 + +常见的应用场景如上下半身分开播放动画,假如要制作一个在射击时行走的动画,可以通过动画遮罩对射击动画的上半身和行走动画的下半身进行混合来实现。 + +## 创建 + +在 **资源管理器** 点击左上方的 **+** 按钮,然后选择 **动画遮罩(Animation Mask)**: + +![create-animation-mask](animation-mask/create.png) + +即可创建一个默认名为 **Animation Mask** 的动画遮罩资源: + +![animation-mask-asset](animation-mask/default-mask.png) + +## 编辑遮罩 + +在 **资源管理器** 中选中动画遮罩资源后,可在 **属性检查器** 中对动画遮罩资源进行编辑。 + +![inspector](animation-mask/inspector.png) + +### 骨骼导入 + +在一切开始前,需要确定选择骨骼遮罩影响的所有骨骼。 + +在 **属性检查器** 上,点击 ![import](animation-mask/import.png) 按钮,在弹出的预制体选择对话框中,我们选择骨骼层次所在的预制体。一般情况下,我们选择目标模型所在模型文件(FBX、glTF)中导入的预制体,且通常只有一个。 + +![import mask](animation-mask/import-skeleton.png) + +选定后,预制体中所包含的所有骨骼将列入到骨骼遮罩资源中: + +![bones-imported](animation-mask/import-finish.png)。 + +也可以通过点击 ![clear](animation-mask/clear.png) 来清除已导入的所有骨骼。 + +### 骨骼启用与禁用 + +当希望某骨骼上的动画效果被禁用时,确保其前方的勾选框保持未勾选状态即可。 + +操作方式为在导入的骨骼中点击骨骼名字旁边的单选框 ![checkbox](animation-mask/checkbox.png)。 + +如果只想操作单一骨骼,则可以通过按下 Alt 的同时使用鼠标左键进行选择。 + +不按下 Alt 时点击 ![checkbox](animation-mask/checkbox.png) 单选框会同时选中/取消当前骨骼以及其子骨骼。 + +通过点击 ![expand](animation-mask/expand.png) 可以展开/隐藏子骨骼。 + +> **注意**:子骨骼和父级骨骼直接的启用/禁用关系是互不影响的。也就是说,即使父级骨骼被禁用,子骨骼依然可以生效。 + +遮罩所在层级的动画,如其骨骼未被选中,则所有跟该骨骼有关的动画不会播放。在下图中,**Bip001 Pelvis** 未被选中,则该层级的所有动画都无法影响到该骨骼。 + +![check](animation-mask/check-skeleton.png) + +## 示例 + +完整示例请参考([GitHub](https://github.com/cocos-creator/example-marionette)|[Gitee](https://gitee.com/mirrors_cocos-creator/MarionetteDemo))。 diff --git a/versions/4.0/zh/animation/marionette/animation-mask/check-skeleton.png b/versions/4.0/zh/animation/marionette/animation-mask/check-skeleton.png new file mode 100644 index 0000000000..b5f33fc50d Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/check-skeleton.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/checkbox.png b/versions/4.0/zh/animation/marionette/animation-mask/checkbox.png new file mode 100644 index 0000000000..b6dc2e1f2d Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/checkbox.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/clear.png b/versions/4.0/zh/animation/marionette/animation-mask/clear.png new file mode 100644 index 0000000000..a4d18352bb Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/clear.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/create.png b/versions/4.0/zh/animation/marionette/animation-mask/create.png new file mode 100644 index 0000000000..749ff88b60 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/create.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/default-mask.png b/versions/4.0/zh/animation/marionette/animation-mask/default-mask.png new file mode 100644 index 0000000000..0ee6b8b021 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/default-mask.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/expand.png b/versions/4.0/zh/animation/marionette/animation-mask/expand.png new file mode 100644 index 0000000000..213dfb498e Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/expand.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/import-finish.png b/versions/4.0/zh/animation/marionette/animation-mask/import-finish.png new file mode 100644 index 0000000000..77c2a811c2 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/import-finish.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/import-skeleton.png b/versions/4.0/zh/animation/marionette/animation-mask/import-skeleton.png new file mode 100644 index 0000000000..31eadfcb22 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/import-skeleton.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/import.png b/versions/4.0/zh/animation/marionette/animation-mask/import.png new file mode 100644 index 0000000000..7e18574bf7 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/import.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/inspector.png b/versions/4.0/zh/animation/marionette/animation-mask/inspector.png new file mode 100644 index 0000000000..f16b64e557 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/mask-example-skeleton.png b/versions/4.0/zh/animation/marionette/animation-mask/mask-example-skeleton.png new file mode 100644 index 0000000000..a479a526ee Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/mask-example-skeleton.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/revert.png b/versions/4.0/zh/animation/marionette/animation-mask/revert.png new file mode 100644 index 0000000000..1ade8e1f56 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/revert.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/save.png b/versions/4.0/zh/animation/marionette/animation-mask/save.png new file mode 100644 index 0000000000..4efd79783d Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/save.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/select.png b/versions/4.0/zh/animation/marionette/animation-mask/select.png new file mode 100644 index 0000000000..3c0060fa99 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/select.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-mask/weight.png b/versions/4.0/zh/animation/marionette/animation-mask/weight.png new file mode 100644 index 0000000000..77d8f58cf0 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-mask/weight.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-variant.md b/versions/4.0/zh/animation/marionette/animation-variant.md new file mode 100644 index 0000000000..016f6a7a81 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/animation-variant.md @@ -0,0 +1,26 @@ +# 动画图变体 + +如果两个动画图他们的动画逻辑相同但对应的动画剪辑不同,重新制作一个动画图会带来额外的维护负担。通过使用动画图变体,可以更好的解决此问题。 + +动画图变体是一种资源,在 **资源管理器** 内右键创建一个新的动画图变体资源: + +![create](animation-variant/create-asset.png) + +创建后的变体如下图所示: + +![default](animation-variant/variant-asset.png) + +## 属性 + +![default](animation-variant/empty-varint.png) + +| 属性 | 说明 | +| :-- | :-- | +| **Animation Graph** | 需要变体的动画图,通过下拉或者从 **资源管理器** 内选择
![select](animation-variant/select.png)| +| **Clip Overrides** | 需要覆盖的动画剪辑
![override](animation-variant/overrides.png)
通过右侧 Overrides 列,从 **资源管理器** 内选择需要替换的动画剪辑 | + +## 使用变体 + +在 Animation Controller 组件上下拉其 **Animation Graph** 属性即可选择不同的变体。 + +![controller](animation-variant/controller.png) diff --git a/versions/4.0/zh/animation/marionette/animation-variant/controller.png b/versions/4.0/zh/animation/marionette/animation-variant/controller.png new file mode 100644 index 0000000000..65f186361a Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-variant/controller.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-variant/create-asset.png b/versions/4.0/zh/animation/marionette/animation-variant/create-asset.png new file mode 100644 index 0000000000..f62a61ade1 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-variant/create-asset.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-variant/empty-varint.png b/versions/4.0/zh/animation/marionette/animation-variant/empty-varint.png new file mode 100644 index 0000000000..317cd26341 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-variant/empty-varint.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-variant/overrides.png b/versions/4.0/zh/animation/marionette/animation-variant/overrides.png new file mode 100644 index 0000000000..5bef6ed4ec Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-variant/overrides.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-variant/select.png b/versions/4.0/zh/animation/marionette/animation-variant/select.png new file mode 100644 index 0000000000..08ad16e3b2 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-variant/select.png differ diff --git a/versions/4.0/zh/animation/marionette/animation-variant/variant-asset.png b/versions/4.0/zh/animation/marionette/animation-variant/variant-asset.png new file mode 100644 index 0000000000..d8efd39e94 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/animation-variant/variant-asset.png differ diff --git a/versions/4.0/zh/animation/marionette/flow-chart.pos b/versions/4.0/zh/animation/marionette/flow-chart.pos new file mode 100644 index 0000000000..43198f7b7c --- /dev/null +++ b/versions/4.0/zh/animation/marionette/flow-chart.pos @@ -0,0 +1 @@ +{"diagram":{"image":{"x":-107,"width":1433,"y":-37,"pngdata":"iVBORw0KGgoAAAANSUhEUgAABZkAAARICAYAAABgJt9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAgAElEQVR4nOzdeZyVZd0/8O8ZZgAHBwURkERwyQzEZXAFVDQt91xRnkw0gjR/+jPU1CczNR81XJNSI1MEywVREOuHpkIiqCgICgLKYsouIOsw+/37gzjPTDPIcMQ54bzfr5cv5lz3fd3355wDAh+uuU4EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGRTKtsBACDLcgsLC3+YJMmPI+LAVCrVLNuBGpiiJElmRMQj77777p8ioizbgQAAANg6SmYAGrLcwsLCpyPizGwHISJJklfefffdk0LRDAAAsF1RMgPQYBUWFl4cEY/sueee8Ytf/CL22WefKCgoyHasBmXdunUxf/78GDhwYHzwwQeRJMn177777h3ZzgUAAEDd5WQ7AABky7+2yIhf/OIXcfDBByuYs2DHHXeMLl26xA033BAREalUqneWIwEAALCVlMwANGQHRkTss88+2c7R4O2+++6bvvxmNnMAAACw9ZTMADRYmz7kzwrm7GvWLP15iztkMwcAAABbT8kMAAAAAEDGlMwAAAAAAGRMyQwAAAAAQMaUzAAAAAAAZEzJDAAAAABAxpTMAAAAAABkTMkMAAAAAEDGlMwAAAAAAGRMyQwAAAAAQMaUzAAAAAAAZEzJDAAAAABAxpTMAAAAAABkTMkMAAAAAEDGlMwAkAU//elPY9q0adXGrrzyypg3b16Ul5dv1bUeeuihGDp06LaMBwAAAHWmZAaALJgzZ0506dKl2tj06dOjffv28dprr8Utt9wSy5cvr9O1FixYEC1bttxm2UaOHBmjRo3aZtcDAADg603JDAD1oKysLFauXJl+3KhRo8jJqf7bcOPGjSMvLy+OO+642HHHHeOmm26q07WXLVsWe+65Z43xWbNmxR133LHVWV9++eV4/fXXt3oeAAAADVNutgMAQEOQl5cXv//976NXr17xrW99K1KpVI1zqo5dcsklMXHixGrHX3jhhRg2bFg0b9682rnTpk2LG2+8MXbZZZda7z158uTo2rVrnXIWFRXFAQccECNHjqzT+QAAAKBkBoB6cuaZZ8Yll1wSzz///BZL5qZNm8abb74ZBx10ULRq1SoiIk499dQ49dRTq82ZO3du/OY3v4krrrgiSkpK6lwmb860adPisMMOi1deeSUWL14cu+2225e6HgAAAF9/SmYAqCf7779/tG/fPl0mv/zyy/Hggw9GixYtIicnJ1atWhVnn312tRXJL7zwQlx00UWbveZvf/vb6N+/f+y///4xZMiQSKVSUVhYmHHGGTNmxIUXXhhdunSJ999/X8kMAADAFimZAaAeDRkyJL0X8/HHHx/HH398+tjpp58eF110UZx22ml1utagQYOiuLg49thjj4iI6NOnT9xxxx0xYcKEuPDCC2OnnXba6nwbNmyIxo0bx/777x/vvfdefPe7393qawAAANCw+OA/AKhHOTk5sXLlyigqKqo2XlZWFqlUKtauXRtlZWVfeI3FixfH7bffHnvttVfcf//98Zvf/CYGDRoUqVQqrr/++th9993j/PPPj2uvvTZGjRoVs2bNqlO28vLyyM3d+O/PXbp0iffeey+zJwkAAECDomQGgHo0e/bsGD16dOTn51cbnzt3buy9997Rvn37mDJlSo15SZLE22+/HX/9619j+vTpcdVVV8XJJ58cubm58atf/SrGjh0ba9asiYiNez+PGDEiCgsL46233kqvnN6SWbNmxapVq+KZZ56JKVOmxPz586O0tPTLP2kAAAAAgK+jwsLCpLCwMKkPlZWVyfDhw5Obb745qaysTE4++eRqxx999NHkqaeeStasWZPceOONtV6jtLQ0SZIkWb16dZIkSfKDH/wg+eSTT5Jbbrkl+fOf/1zt3Dlz5iTl5eVblXHYsGHJqlWr0o8vvvjiZOrUqVt1jS9j0/uR7Z8XAAAAbB0rmQGgHowZMyZGjx4d1113XfqD/6p65ZVX4vjjj4+CgoJYuXJlLFy4sMY5eXl5UVJSEk8++WRERCxfvjzat28fAwYMiKFDh8b06dMjImL8+PHRvHnz+OlPfxoffvhhnTOuWLGi2j7OnTt3jvfff39rnyoAAAANjJIZAOrBiSeeGPfee280btw4IjZuf7HJ+PHjo0uXLtGyZcuI2PgBgLfffnu1czZ58MEH01ttNGrUKCIimjVrFrfffnvst99+UV5eHr/73e9i1113jdNPPz0uvfTSqKysrFPGDRs2VHvcuXPnmDZt2tY/WQAAABoUJTMA1INUKpUukSM2lsyVlZWxYcOGeOqpp+Kyyy5LH/vOd74Ty5Yti4EDB1YriCdPnhyvvfZanHDCCelrbHLwwQdHbm5uLFy4MBYsWBBr166NE088MY4//vg67cn8yCOPxMSJE6utfJ48eXJMmDAhxo4d+6WeOwAAAF9vNb9fFwAaiE37/06ePLne733CCSfE6NGj43e/+12ce+650aFDh2rHp0+fHn379o3vfOc78etf/zq9armqW265JT766KPIy8tLjy1fvjwiIq6++uo4+uij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YEGSJEn6Oc+fPz89Z+3atckPf/jD9ON+/frVeL2WLFmSzJw5M3nttdeqjf/4xz9Of33xxRcnSZIkffv2rfX4lowaNSr9PpxyyilJkmx8n55//vlq55188slfeJ0XXnghWbx4cZ2uedlllyUlJSVJkiTJwoUL0+9JkiRJtn/9AAAA/KfywX8A1KtRo0bFueeem3680047xVVXXRWvvvpqjXMLCwvjgQceiGuuuaZO1y4rK4v99tsvbr755igoKIgdd9wxfaxPnz4xePDg9ONly5bFQw89FD/72c+iadOm8d5776VXVk+YMCF+8IMfRETE+PHj4+yzz4733nsvDjjggPT8iRMnRk5OTlx99dWxfv36+MMf/hDHHnts3HnnnXH99denV/7+u8ceeyxmzZoVq1evjuXLl8eKFSvikksuiYiNq6z79+8fFRUVscsuu8T+++8fjRo1imeffTbGjBmTvsaQIUNi/vz58e6778Zf/vKX9HhFRUXMnTs3Ro0aFTvttFOMGTMm3n333bj77rtj+fLl0apVq+jTp0888cQTceGFF8bee++dXtm7YMGCGDFiROywww4xbdq0WLt2bfTv3z8iNq4ovuyyy2p9Pi1atIgjjjgi8vLy0hk2zVu0aFFUVlZWG5s3b15d3sqIiOjRo0e0bNmy2lhBQUEcddRR1cY2rVLfnCVLlkTbtm3rdM3Zs2fHvHnzYr/99ot27drFsmXL6pwXAAAAAPiKJUmSXHjhhUllZWWN1aabVpX+u6qrYGuzfv365NJLL0369u2bHHfcccmaNWuSysrK5Kabbkrmzp2bPu/CCy+sMXfIkCHJE088kYwfPz5ZvXp1ctJJJyX9+vVLevbsmV4BffTRRyf9+vVLbrjhhvSK488//7zaCuuioqLk7rvvTk466aTkvvvuS9auXVtr1k8//TS59NJLkz59+iRJkiQPPvhgtYybVv7+uw0bNiQbNmyoNta7d+9k3bp11cbWrFmT9O/fP0mSJFm3bl3y9ttvp1cODxgwIEmS/11JvGk19aaVzJt+3KTqc73++utrZCouLq4164wZM9JfDx8+PPnlL3+ZfPbZZ8m0adOSe+65J1m/fn3y97//vdafA19kcz8/tnTs7bffTqZNm1bnedOmTUu/f0uWLEm/nkliJTMAAMDmWMkMQL1atWrVFleeVrVo0aIvPJ6fnx+33XZbNG/ePC6++OIoKCiIiIgzzjgjbr755njsscc2O7d3795RWloaf/3rX6NHjx6x++67x+DBg+OnP/1plJeXR0TEDjvsUG0FdETEzjvvHAUFBTF58uSYMWNGFBUVxWmnnRbr16+Ppk2bRr9+/aJRo0axxx57RMuWLePkk0+Ob3/72/GnP/0pbr311rj66qujsrIyunTpkt73+N+tWbMmmjdvHhFR48MRly5dGs2aNYvly5dHs2bN0uMrV66MnXfeOSIimjVrFoccckhcf/310b9//5gzZ070798//dpv6T3YtKr7nXfeienTp6dXIm/yySefxAMPPJDOv3z58rjiiiuiWbNmkUqloqKiImbOnBn7779/XHDBBdGuXbvIzc2NK6+8MiIiysvL48QTT/zCDNvCtGnTom/fvnU+v+pq9eHDh0e/fv2+ilgAAABfK0pmAOpVSUlJFBcX1yhOa7NkyZKorKzc4nmbitWqH5p34IEHxo9+9KMvnNe4ceMoKSmJJEmisrIyNi1WPf/88+Poo4+OiIgxY8ZEaWlp+sPxkiSJKVOmRHFxcXTo0CG6du2avt7kyZPj2WefjZ/85CcxadKkWLp0abRt2zY6deoU8+fPjz59+qS3ali1alUMGzasWgm+qQiOiPjoo4/iqquuilNPPbVG7tGjR8cpp5wSlZWVce2118Z5550XhYWFsXz58mjRokW1czt06BCDBw+Oq666Ku6+++709f99YW5FRUWtr9Frr70Wd911V3zyySdx/PHHR8TGgvnOO++MPffcM31eq1at0lt3VFZWxt133x2XXHJJHHroobF48eIYNGhQnHfeeXHggQd+4XuyLX366aexxx57ZDR3+vTpsfvuu8chhxyyjVMBAAB8/SiZAahX7dq1i/fffz8OPfTQLZ47efLk2G233TK+1zHHHLPFc1544YU4/PDDY8WKFdGyZcv405/+FK+88ko8/vjjERExa9asWLNmTfTq1SsiNq4A7tq1ayxfvjyuu+66SKVS6VXBn332WXp/5Y8//ji6desWN910U0REtUI2IqJly5bxm9/8JgoKCtKrhn/0ox/VWDX979auXRuvvvpqPProo9GkSZP42c9+FhdffHHccssttZbM8+bNq7aSeZNNezFv+rFJkya13mvNmjWx7777xnvvvRcrV66Mli1bxv333x9XXXVVrauhly1bFv/4xz/ihz/8YaxZsyYiNpbON9xwQzz00ENx3333RdeuXaNjx47x3e9+N13efxVeffXVuOCCC7Z63vLly+PDDz+Ms8466ytIBQAA8PWjZAagXh1zzDHx3HPP1SiZN21PUdWIESPisMMO2yb3/feVuytWrIiRI0fGiy++GL17947JkydHhw4dom/fvtGmTZs4+eSTY/bs2fHQQw9V+6DCTVq1ahX33HNPLFy4MPLz86NDhw7Rq1evuO+++yJi4zYeW7LTTjt9Ycba3HXXXdVK4bZt28Ydd9wRubm5sWTJkth1113T55aVlUWnTp3id7/7XQwYMCDuueeedNH86KOPxh577BH9+/eP++67r9aS+S9/+Ut6q4lTTz01brvttjjggAPiwAMPjI4dO1Y7d+nSpTFr1qy4//77Y5dddom///3vMXv27Bg6dGjcfPPNkZu78Y8cc+bMiXXr1sVxxx33lRbM69ati/z8/Gqr2+uioqIiXn755Tj//PMjImL+/PnRoUOH9D8EAAAAUJO/MQFQr84999yYMmVKjB8/PiIiVq9eHc8880wccsgh8fTTT8fq1asjImLkyJExa9asOPvss+t03cWLF0dRUVGUlJSkx4qLi2P06NFxySWX1NieY5dddokTTjgh2rZtGxER77//fnrri8MOOyxuv/32uPfee+Omm27a7P7Fubm5ceutt8a7776bHsvPz4/nn38+/vKXv9RanH+RLW0NMnz48Gjfvn307Nmz2viBBx4YnTt3jk8++STatGmTHv/kk0+iS5cuEVGzxK+6jcSaNWviueeeq3Z8wYIFsWbNmvT8pk2bxj777BN/+MMf4swzz6yRbdddd41jjjkmWrduHYMHD47BgwfH3nvvHR06dIhGjRqlxzp06BDDhw+PTp06bfkFqaPaXrcxY8bE9773va2+1ssvvxzf//73049Hjx6tYAYAANgCK5kBqFf5+flx9913x4ABA+KXv/xl9OjRI84555w455xz0ue89NJLcccdd8TVV18drVu33uI1X3zxxZg0aVI88sgjMXz48CgoKIjCwsJo27ZtnHbaadGxY8do3rx5JEkS69ati3Xr1sVuu+0Wn332WXo17cyZM+OHP/xhRGzc8mHhwoXRuHHj+Oyzz2psQRERsWHDhrj11lvjmmuuiYMOOqjasfPPPz+GDh0aP/jBD+Khhx6qMX9z+x+XlZVt9jm+8MILUVJSEj/+8Y/TYyUlJfHOO+/ErrvuGuXl5fHOO+/EgAED0sffeeeddGFaXFycHv/ggw9i6dKlsWrVqli9enXMmjUr3nzzzdhtt92ie/fuUVpaGk888UT8n//zf9JznnnmmVi0aFHccMMN0a9fvzjvvPPipJNOSq+A3lTEJkmSXi09Z86c9F7Xm8b++c9/RkVFxVatMB4zZkysWrUqxowZU+3DAufOnRuvvvpqLF26NB5++OE4+uijY999943KyspYt25d+oMTt+aav//979Or0cvKytL/CAEAAAAA/AdIqpg3b17Sp0+f5Oqrr07+9re/JW+88Uby7LPPJpdeemnSrVu35Mknn0zqoqKiIhk4cGCyYcOG9NgHH3yQ3HfffckVV1yR9O3bN+nTp09y7rnnJieeeGJy2mmnJTNnzkySJElmzZqV3Hbbbcn48eOTN998M/nss8+SRx55JBk2bFhSWlqaLFmyJLn66quTiy66KBk0aFCyZMmSJEmSZPHixcngwYOTpUuXJmVlZcmoUaOSn//850nv3r2rZfvFL36RjB8/vkbmXr161fpczjrrrBpj5eXlyejRo5N33nmn1jmrVq1K7r///uSII45InnvuufR4UVFR8ve//z39+PXXX08efvjh5Nprr02GDx+ePProo8msWbOSioqK9Gs2ZcqUpKysLHnuueeSdevWJZWVlclbb72VDB48OJk6dWr6WsuXL09uvPHG5Jhjjkmuu+669OuSJEly3XXXpb/+7W9/m5SXlyc/+clP0mO33XZbUlpaWutz2VbGjh2bfPzxx9v8utn+9QMAAPCfqvbv/wWAr0BtRd20adPirbfeiqVLl0ZeXl7stddecdxxx0WrVq3qLdeMGTOiSZMmsXr16jjooINqrLL9+OOPY/HixXHkkUdGxMb9h6tuSxER8frrr0e7du1ir732So+Vl5dHTk5Oje0WapsfsXGVb4cOHaqNFRcXR+PGjbe4ZcPatWujoKAg/bisrCzy8vKqnbNhw4bIycmpdf/lTUpLSyMvLy9KS0tj2rRp0aFDh1qzRkQsWrQoPv/88+jcufMXZtvalctf1ld1v9Tm9k0BAABo4PxlCYB6YzUo2zMlMwAAQO18kg0AAAAAABlTMgMAAAAAAAA0VIWFhUlhYaGtSAAAAMgKK5kBAAAAAMiYkhkAAAAAgIwpmQEAAAAAyJiSGQAAAACAjCmZAQAAAADImJIZAAAAAICMKZkBAAAAAMiYkhkAAAAAgIwpmQEAAAAAyJiSGQAAAACAjCmZAQAAAADImJIZAAAAAICMKZkBAAAAAMiYkhkAAAAAgIwpmQEAAAAAyJiSGQAAAACAjCmZAQAAAADImJIZAAAAAICMKZkBAAAAAHcR8HoAACAASURBVMiYkhkAAAAAgIwpmQEAAAAAyJiSGQAAAACAjCmZAQAAAADIWG62AwD/mbp3757U5bwJEyakzNt+5xUXF+88efLk1VXmfR4RO29pXpIkHSdOnPjPTY+7des2P5VKddzSvFQqddDrr78+rcq8d1Op1EFbmldZWdnzjTfe+EeVnGMjomcdcp4xceLEUVXu91wqlTpjS/Mi4qIJEyY8VmXeo6lU6qI65LzyjTfe+G2VnPdGxJVbO69bt26/SqVSN21pXpIkN02cOPHmTY+PPPLI/5uTk3PfluZFxH0TJkz4WZWcfSJiSB3uN2TixIkXZzBv5MSJE8/c9Lhbt27fT6VSI+uQc9yECROO3fTgyCOPPCYnJ2dcHe43deLEiQdvetyjR48DkySZWod5H0+cOHHPKjk7pFKpj+uQc9WECRNabHrQtWvXnZo2bbqqDvO2m/9XmPfVzgMAgO2dlcxADd27d7+mruc+/9mapOp/5m1f864dMXpV1XlNmjXbYsEcEXHpHx/7uOq8ndq06ViXeX3uGTS16rw2e+29xYI5IuIHt905ruq89vt36VmXeWf9940jq87b94hudSmY4+QrBgypOq/Ld064qC7zju93yX1V5x1y2hlbLJhrm9ej9wU31WVej94X3FT1/T6+3yV1KZjjkNPOuLLq/U6+YsCQuszr8p0TLspk3r5HdDuj6ryz/vvGuhTM0X7/Lj2rzvvBbXeOq8u8NnvtfVDVeX3uGbTFgjkiYqc2bTpWnXfpHx/7uC7zmjRrtnPVedeOGF2ngjli+/l/hXnbft7Ty5btWNfzAQDgP51VFECttuYvy0B23fS9nht/fHFcVnMAdVecFBf0at16XbZzAADAtmAlMwAA1KOykuKYNGJEfrZzAADAtmJPZgAAqEeD+vSOsuLipT179iwYN26c1cwAAGz3rGQGaujevfvAsUMeznYMAAAAALYDSmagNtdMGjki2xkAAAAA2A4omQEAAAAAyJiSGQAAAACAjCmZAQAAAADImJIZAAAAAICMKZkBAAAAAMiYkhkAAAAAgIwpmQEAoB7lNW4SEVG0cuXKJNtZAABgW8jNdgAAAGhILh/6ZBQnxW16tW69PttZAABgW7CSGajNz3v26ZvtDAAAAABsB1LZDgD8Z3r+szW+hRf+Q817d3J89PZb6cdvjHg6IiKOPLtXeqx9p87Rqccx9Z4NqJvipLigV+vW67KdAwAAtgXbZQDAdianUaN0sVxV1bH2nW6pz0jAVigrKY5Jo0fkR4SSGQCArwXbZQDAdqZ9p/1jh4KCzR5vkp8f3zr8yHpMBGyNQX16xxuPP760Z8+eO2Y7CwAAbAtKZqCG7t27Dxw75OFsxwA2o1FubnQ+5rjNHt+r8JBolJdXj4kAAABoyJTMQG2umTRyRLYzAF+gU/ejNnus81H2YgYAAKD+KJkBYDu0R5cDokl+fo3xvCZN4lvdemQhEQAAAA2VkhkAtkO5eY3j2z1qrlje6+Cukde4SRYSAQAA0FApmQFgO9Wplm0xvt396CwkAQAAoCFTMgPAdqrjAQdF46ZN049zGzeptXgGAACAr5KSGQC2U42bNo39quy/3PGAA6PxDjtkMREAAAANkZIZALZjVfdl3s8H/gEAAJAFSmYA2I7tdXDXyM1rHDm5udH56GOzHQeog399OGfRypUrk2xnAQCAbSE32wEAgMw1yc+PfQ8/MjasWxs7FBRkOw5QB5cPfTKKk+I2vVq3Xp/tLAAAsC1YyQzU5uc9+/TNdgagjjof09NWGQAAAGRNKtsBgP9Mz3+2xrfw0mDMnPFpTBg/M5Ys+jwqKiqzHWfrVZREJElEbtNsJ9lqjRrlROs2O8WRPb4dXQ7skO04UG+Kk+KCXq1br8t2DgAA2BZslwFAgzZzxqfx9F9ez3aML6dRk2wnyFhFRWUsXvR5PPv0xCgtLYuuh+6T7UjwlSsrKY5Jo0fkR4SSGQCArwXbZQDQoE0YPzPbEfiXtyZ+mO0IUC8G9ekdbzz++NKePXvumO0sAACwLSiZgRq6d+8+cOyQh7MdA+rFkkWfZzsC/7JixdpsRwAAACADSmagNtdMGjki2xmgXmyXezB/TVV6LwAAALZLSmYAAAAAADKmZAYAAAAAIGNKZgAAAAAAMqZkBgAAAAAgY0pmAAAAAAAypmQGAAAAACBjSmYAAKhHeY2bREQUrVy5Msl2FgAA2BZysx0AAAAaksuHPhnFSXGbXq1br892FgAA2BasZAZq8/OeffpmOwMAAAAA2wElM1DDhAkT7jz8zHOyHQMAAACA7YCSGQAA6lFZSXFMGjEiP9s5AABgW7EnMwAA1KNBfXpHWXHx0p49exaMGzduXbbzAADAl2UlM1BD9+7dB44d8nC2YwBZ0q1z+8jJSWU7BgAAANsJJTNQm2smjRyR7QzwtXbofu3i4G+23ezxB648JQ7cu/rxey87MfbarUXkNtq6375/ctohceF3D6zz+bf86NhotdP/fif/rjtX/67+drsUbNX9AQAA+HqzXQYAbGPX9u4RnTq2ipLSis2ec8DebWLZqvVx/s3PRFFJWY3je3+jRbw/f2m1sf33bB2ffrY6jj6gQ/Toskc8MOrtWL66aIt5dt+1ebw1c0Gd85eVV8ayz9enH//pmu/HkpX/+x393+6wa/x62D/ipbfn1vmaAAAAfH0pmQFgG/vNE69v9lhBfuO4qle3eHv2whj5+qx0wZyXmxMF+U1i5ZoNERFRUZlEZWVSbW5ZeUWUlVfGq+/Oj4P2aRu/6tMzLr//b1vM07pFfsxb/HmN8f32aBXf775fjbxJUv2+ubk50f/u0enHo2/7rxj/3j+3eF8AAAAaBiUzAHzFclKpSKVS8a32u8SRndvHb0e8GZ+vLa52Tll5ZVx2xqHx9NgPYvany2sUvRERlVXGHhr9TnTr3L7a8VOP3DcuOOGAWFNUElFl+gF7t4lbLj42XWD/u8J9d4spHy6udp9GOTlx8hH7xOiJH9aaZUNJeZ2eOwAAAF9/SmYA+Io1zmsUT/zynBj60tT409+mbPa858bPigd/dkp8/4YnopZet9pYcWl5HP7t3WPqnCXpLTNeeOPDeOGND6vN2btdy7i2d/e4/9m3oklebkz+cFGt9+6yV+u4+rxuUVJaEW1a7BgP/uyUSCKJ9+cti8rKrX/OAAAANBxKZgD4ipWWVcQuzXeI0ROrF8C77pwfnTu2jnFTP46IiOnzl8Wnn61Ol8nfKdwrLv3+IfH52uJIkiR23rFpPHNzr2orkk85Yt947MWpm733/z378Bj8wuSYPn9ZXHTiQZEkSUz5aHGN896ftyz63D4y2rduHs/9+vxq22MAAADAF1EyA8A2cvlZh0WXPdvUeqxp49x44MpTqo11aLtTNM9vGpfe+0JMnbMkIiIuvmNUeluMV6bMi1emzEuf//z/9I7HXpxao6z+ojxNG+fGJ0tXR0TEYy9Ojev+q0d079I+hr74XqxeX1xjzhGddo+IiNO7fytem/bPWLWu5jkAAABQlZIZALaRx8ZMi7KKilr3Kx5330V1Wh1cmSTRsvkOkd80r9p4Xm5OVCZJFOzQJPJyc6KsfPN7WOy2y47R53sHxXvzlsYfX5gS//Pj42L+4lXxu+cmxe1/fj3OPGq/eOLGs+O9uUtj4oxP48NPV8SsT5ZHRMSxB+0Z64tLY+qcJfHY9WfEzY/9YytfBWBL8ho3ibLi4qKVK1fWsjEOAABsf5TMALCNrCkq+dLX2Lf9LnFEp92jqLis2vje7VrGvEWfx6efrY7Cb7aLt2YuqHY8lYo45FvtYtedm0VZeUXcM/yNKCuviEY5OXHzY/+IIdeeEUNfmhZr1pfEc+NnxYuT5sap3faNI769e8z658aC+Rutmsdnq9fH2qKd4pOlq+O2x1+P5vlNIpX60k8LqOLyoU9GcVLcplfr1uuznQUAALaFnGwHAP4j/bxnn77ZzgANSioVcfbRneL8Y/ePYS9Nq3H88G/vHm9+sCCmzlkSJx2+T43jSRIxdc6S+NubH8VbHyyM0rKKGHr9WbHbLjvGFWcdHs/844NYs/5/S/C2u+wYI/4xM/774VfiwwUrIiLi3J6d4g/PT06f89bMBTFt7pJYuXZDjfsBAADAJkpmoIYJEybcefiZ52Q7BjQoJx62T5zWbd+44y+vpz/4r6rvdN0zXp4yL9YWlUbL5jtEu1YFNc4pK6+MJnm5cf5x+0dERKud8uPTZWvi3uFvxg+/e2Dsv2friIg46oA9Ym1RSfz+ypNj3913iYiNq5jnL14Vi1asjVQq0quXD9qnbbwza9Fmc/fq2flLPnMAAAC2d0pmAPgPMGbSnBjwwItRWl4RERE5VfaoOOqAPWL6vGWxcs3GFcWjJ86O6//rqFq3sbjk9EOiqGTjVhsVlRv3bV5fXBr//fDLMeuT5ZHbKCcuO+Ow+GxVUTw/cXY88LNTIieVil13zo9RE2ZFREQqlYrUvy5+Qte9Y9SE2bVmzkml4qTDv7ltXgBoQMpKimPSiBH52c4BAADbipIZAL4ijXI2/jbbOLfRFs9NkkiXyBERkdpY4u7QJDd69dw/fj/y7fShV6bMj9Yt8uOa87tXK6O77tsujj5wj/j7O/M2XqJKCf3uR0uivKIy2rUqiN13bR4F+Y3jxUlz45Up86IySWLqnCVVcqciJ5WKvdu1jGWr1sc/l66qnjWS2KlZ09j7Gy1i3/YtY692LbbqdYGGblCf3vHG448v7dmz547ZzgIAANuCD/4DaujevfvAsUMejmMv+nG2o8B27dD92sXZR3eKg/ZpG58uW7NVc3NzcqJxXqP46fcPi7uemhjri0vTxyork/j10Nfi4WtOj52bNY1fPjI2KiorY/KHi+LsG59On/fWBwtj6H+fGWX/Wh0dsXELjRVriuLgfXaL1977Z9z+59dr3DsnlRPNmjaOkw7fJx6oUm5vMur12THoipMiUhGTZi6KdUWlNc4BAACg4fB58UAN3bt3TyIirh35/7IdBb5yN//iia/0+qlURP9TD4nRE2fHohVr6zzv8rMOi4+XrIo3ZiyI5auLaj2nc8fW8cE/l9W6h/OX8eNTCuPVKfPjk2Wro7yicttefAt+9T+96/V+kA33nH9mlBUXR15eXsG4cePWZTsPAAB8WVYyA8BXKEki/jD6na2eN+jZSVs8Z8bHyzKJtEUP/3XKV3JdAAAAvp7syQwAAAAAQMaUzAAAAAAAZEzJDAAAAABAxpTMAAAAAABkTMkMAAAAAEDGlMwAAFCP8ho3iYgoWrlyZZLtLAAAsC3kZjsAAAA0JJcPfTKKk+I2vVq3Xp/tLAAAsC1YyQzU5uc9+/TNdgYAAAAAtgNKZqCGCRMm3Hn4medkOwYAAAAA2wElMwAA1KOykuKYNGJEfrZzAADAtmJPZgAAqEeD+vSOsuLipT179iwYN27cumznAQCAL8tKZqCG7t27Dxw75OFsxwAAAABgO6BkBmpzzaSRI7KdAQAAAIDtgJIZgAatUSO/Ff6nyMlJZTsCAAAAGfA3awAatNZtdsp2BP6lRcsdsx0BAACADCiZAWjQjuzx7WxH4F+6HrZPtiMAAACQgdxsBwCAbOpyYIcoLS2LtyZ+GCtWrI3KispsR2pQcnJS0aLljtH1sH3iyO77ZTsOAAAAGVAyA9DgdT10n+h66Pa7ivam7/WMiIhj+/wojvmvC7MbBgAAgAbHdhkAAAAAAGRMyQwAAPUor3GTiIiilStXJtnOAgAA24LtMgAAoB5dPvTJKE6K2/Rq3Xp9trMAAMC2YCUzUJuf9+zTN9sZAAAAANgOKJmBGiZMmHDn4Week+0YAAAAAGwHlMwAAFCPykqKY9KIEfnZzgEAANuKPZkBAKAeDerTO8qKi5f27NmzYNy4ceuynQcAAL4sK5mBGrp37z5w7JCHsx0DAAAAgO2AkhmozTWTRo7IdgYAAAD+P3t3Hh5lee4P/J6EBAQDiBpAURDcQRYRUwxqVKx7rUup2Cpalx57jv5s3Xq62Frt4tp67KIVK+JaFYviXitpa0BQlqKouIEbCISwx4EkM78/OMyBEpgQAkPg87muXvV5572fuTO8vnh988zzAjQDQmYAAAAAABpNyAwAAAAAQKMJmQEAAAAAaDQhMwAAAAAAjSZkBgAAAACg0YTMAAAAAAA0mpAZAAC2oILClhER1VVVVelc9wIAAE2hRa4bAACA7cmlIx+JZDrZcUhx8fJc9wIAAE3BSmagPleXDbsg1z0AAAAA0AwImYF1VFRU3Fxy2pm5bgMAAACAZkDIDAAAW1DNimRMHDWqda77AACApmJPZgAA2ILuGDY0apLJuWVlZUXl5eXLct0PAABsKiuZgXWUlpbeNHbE8Fy3AQAAAEAzIGQG6nPVxNGjct0DAAAAAM2AkBkAAAAAgEYTMgMAAAAA0GhCZgAAAAAAGk3IDAAAAABAowmZAQAAAABoNCEzAAAAAACNJmQGAIAtqKCwZUREdVVVVTrXvQAAQFNokesGAABge3LpyEcimU52HFJcvDzXvQAAQFOwkhmoz9Vlwy7IdQ8AAAAANANCZmAdFRUVN5ecdmau2wAAAACgGRAyAwDAFlSzIhkTR41qnes+AACgqdiTGQAAtqA7hg2NmmRybllZWVF5efmyXPcDAACbykpmYB2lpaU3jR0xPNdtAAAAANAMCJmB+lw1cfSoXPcAAAAAQDMgZAYAAAAAoNHsyQzQjJyyS1GuW2ATjalcmusWNprrrvlrjtcdAADQfFjJDAAAAABAo1nJDABstHQ6HfPmzYva2tpo1apVdOjQIRKJRK7bAgAAIAesZAZoxt5999146KGH4umnn46nn346Ro0aFSNGjIg333xzo+ZJpVLx2WefbfCcyy67LKqrq9c6tmLFipg7d+46586bNy8qKioa/P7Tpk1r8Lmr/fnPf46VK1eudez++++PL774ot7z/733l156KX75y19GKpXa6Pfe3i1atCiWL18eu+yyS+y+++6x0047xcqVK2PZsmUbNY/rDgAAYNsgZAZoph5++OGYMmVKDB06NE4++eQ4+eST44wzzojzzjsvPv300/jzn//c4Lny8vLiz3/+czzzzDPrPWfhwoXRunXrtY61bNkyhg8fHuXl5RERmaCtuLg4XnzxxRg/fnzm3PWFiVOnTo0bbrhhrYBywYIFMX369PX28tZbb8X9998fy5Yti08//TTq6uoiIuLJJ5+MwsLCemuqq6vj+uuvj0WLFkVExPLly6OgoCDy8vxV2FDpdDrKy8ujdevWseOOO0Z+fn5ErLp+WrZsGW3atIkZM2ZEOp1u0HyuOwAAgG2D7TIAmqEnn3wyamtr45xzzonKysp47LHHYsGCBdGpU6c4++yz4/jjj49x48bFk08+GaeeemqD5vzmN78Z1157bZx00kn1vr56K4QPPvggevTokTl+wQUXxPDhw6OsrCzOPPPM2H333SMiorKyMmbNmhX33ntvpNPpePPNN+P++++PvffeO1ObTqfj9ttvjxYtWsT3vve9zPHZs2dHIpGIxx9/PFq2bLlOL6NGjYrLL788OnToEDfeeGMce+yxMXjw4LWCz3+3yy67RElJSVx33XXx61//OpYuXRqdO3du0Gfz72bMmBG33nprHH300fG73/0uLr300njllVfiwgsvjN69e8fMmTNj8uTJ8eijj25U2L+1e+yxx2Lw4MFRWFgY1dXV8eyzz8b8+fOjZ8+ecfjhh0cikYi99947/vGPf8SRRx7ZoDlddw2X7bqbPXt2PPPMM1FUVBRvv/12nH322bHffvs16r3YvAoKW0ZNMlldVVXVsN/IAADAVk7IDNDMVFZWxrPPPht33nlnpFKpuPvuu+O73/1utGrVKkaPHh2XXnpp3HPPPXHYYYfFzTffHJWVlbHLLrtknbe4uDjuuOOOeOedd+K2225b5/WZM2fGxRdfHNOnT48rr7wyTj755CgoKIhOnTrFj370o1i6dGnsvvvu8cc//jEefPDBOOGEE6JDhw4xYsSI+PrXvx477LDDOnM++OCD0bNnzzjooINi8ODBkZ+fH1VVVXHhhRfG9ddfX2/QV1VVFXPmzInBgwdHxKpg8Oijj46I/wsk58+fH7Nnz44+ffqsVbs6EIyImDNnTgwYMGCd+d95553Yf//9N/hZzZgxI371q19Fhw4d4oEHHoghQ4bECSecEOXl5dG7d+/Ya6+9Yq+99oo//elPG5ynOfnkk09i2bJl0aFDh/jiiy9i+PDhce6550ZRUVHcf//9UV5eHtdee23k5+dHKpWKTz75JPbYY4+s87ruVmmK6+4Xv/hF3HbbbVFYWBizZ8+OH/7wh3HvvfducE5y49KRj0Qynew4pLh4ea57AQCApiBkBupzddmwC27KdRPUb/To0XHqqadGIpGIGTNmxPvvvx+tWrWKiIivfvWrcfvtt0dVVVV06NAhjjvuuBg9enRceOGFDZq7pqYm9t9//7juuuuiqKgoE4xFRAwbNiz++Mc/Zsbz5s2LO++8MxNwT5s2LbMFQEVFRXzjG9+IiIh//vOfccYZZ8S0adOid+/emfpx48ZFXl5eXHnllbF8+fK466674qijjoqbb745/vu//zt69uxZb4/33XdfvPPOO7F48eKorKyMBQsWxH/8x39ExKrVrhdffHHU1dXFzjvvHL169Yr8/Px44okn4vnnn8/MMWLEiJg5c2ZMmTIlHnrooczxurq6+OCDD+LJJ5+Mdu3arfdzGjRoUHTo0GGtY0VFRXH44YevdWxbehDeX/7yl8xq47feeivKy8vjsssui4iI0047LY499ti49tprIyKif//+MWLEiMzr2bjumua6mzFjRnz44Yex//77x2677Rbz5s3b8AcPAADQRITMwDoqKipuvmb0c0LmjfDyiHviozf+tdnf55T77o0JEybEjTfeGBER3bt3X2ubgYULF0ZhYWG0bds2IiIOPPDA+O1vf7vBkLm6ujquvPLKWLlyZcycOTNGjx4dnTp1ip/97GdxzjnnRPfu3eutKy4ujq5du8YzzzwTXbp0id69e8fHH38cF198cbz33ntx8cUXR0TE+++/H1dccUV07NgxevXqFXl5ebFo0aIoLi6Oww47LCJW7c2bTCbjiiuuiOOOOy4OOOCAet/z008/jffeey/23HPPaNeuXTz88MPx29/+NtPjt771rbUCydVOPPHEOPHEEzNhfETE2WefHXfffXe0adMmc2zp0qVx5ZVXbjDoi4h1gr7V2rdvv8G6iIh7r2hY8NoYU55/Jj6c9HqTz3vKfffG1KlT47/+678iYlWI/OCDD2ZenzZtWmaFb0RE27Ztsz580nXX9NfdrbfeGl26dImIiLlz52b+OWLzXHedeuwdJ3xn813PAABA8yFkBmgC/3zkgQY/7GxTzZs3LxM2FRYWxumnn5557de//nVcddVV0aLFqtt7ixYtYsGCBRucr3Xr1vGLX/wi2rZtG+eff34UFRVFxKpV0dddd13cd999660dOnRorFy5Mp555pkYNGhQdOnSJf74xz/Gd77znaitrY2IiB122GGdAK59+/ZRVFQUkyZNiunTp0d1dXWccsopsXz58mjVqlVcdNFFkZ+fH3vuuWd06NAhTjzxxDjggAPinnvuiRtuuCGuvPLKSKVScdBBB603jFyyZEkmbF8z5ItYFcC1adMmKisr1wr7qqqqGhQUb4qP3py22eZeNHduLJo7d7PMvXDhwrUeVrfDDjvEhAkTYsKECbFgwYK45ppr1jp/zpw5G5zPddf0192aK7Yfe+yxuOiiizLjzXHdffTmtDju2/8ZeevZj5r1q1mRjIljRrWOiGVZTwYAgGZAyAzQBFYHzOff+j+b/b1qamrqPX7XXXfFIYccstaK0lQqFUuXLs065+qAa82Hl/Xp0ye+9a1vbbCusLAwVqxYEel0OlKpVOZzOOuss+KII46IiIjnn38+Vq5cGYWFhRGx6rOaPHlyJJPJ6Nq1a/Tv3z8z36RJk+KJJ56Ib3/72zFx4sSYO3dudOrUKQ488MCYOXNmDBs2LBOwL1q0KO6///61wsj3338/s5L1vffeiyuuuCJOPvnkdfoeM2ZMnHTSSZFKpeKaa66Jr3/963HwwQdHZWVl7LTTTlk/r02xOa6RL5YuiWVVC2OXPffcbFt0LF++PPPnWFtbG4lEIkpKSqKkpCQ++OCDOP/88+O+++6LoqKiqKysjBUrVmSd03W3ea67N998M7p06RKHHHJI5lhTX3f3Xvn/IrbQL9a2RXcMGxo1yeTcsrKyovLyckEzAADNnpAZWEdpaelNY0cMj6POa9g+vvyfrr16Zz9pE7Vr1y4WLly4Vig1fPjw6Nu3b5SUlETEqj1ie/ToEbNmzVrvV+wb4sgjj8x6ztNPPx0lJSWxYMGC6NChQ9xzzz3xt7/9LR544IGIWPVAsyVLlsSQIUMiYtU+xf3794/Kysr4/ve/H4lEYq0Hp63e53bWrFlx2GGHxU9/+tOIiNhrr73Wet8OHTrEjTfeGEVFRZkVtuvbtmBNS5cujZdffjnuvffeaNmyZXz3u9+N888/P372s59tkZB5S1wjm0OnTp3i7bffjj59+sRdd90ViUQivvOd70RERI8ePWK33XaLd999N/r37x9Tp06Njh07Nvq9XHeNV1lZGe++++5a33CIaPrrLi8vL1J1dU06JwAA0HwJmYH6XDVx9Cgh81bq0EMPjYkTJ8Zxxx0XqVQq7r777jjmmGNi7733johVqxiXLFkSPXr0iIqKiujTp0+TvO+/bweyYMGCGD16dLzwwgsxdOjQmDRpUnTt2jUuuOCC6NixY5x44okxY8aMuPPOO+NrX/vaOvPtsssucdttt8Vnn30WrVu3jq5du8aQzfPMhAAAIABJREFUIUPiN7/5TUSs2k4hm3/fw7YhW5bccsstcfHFF0fLli0jYlV4+qtf/SpatGgRn3/+eey6665Z59gelZaWxlNPPRV9+vSJQYMGxXvvvZd5bc6cObFixYrMdg1PPPHEWiuFN4XrruHq6uripZdeirPOOisiImbOnBldu3Zda5sTAACAzUHIDNDMnHXWWXHttdfGscceG++8806Ul5fHhAkTMq8nk8m47rrrYuXKlfHII4/Ebbfd1qB558yZE9XV1bFixYpMEJZMJuOvf/1rPPPMM+vsL7vzzjvHscceG//616oHHr7xxhuZYPHQQw+NX/7yl/HRRx/FjTfeuN4tHFq0aBE33HBDfO1rX4uuXbtGxKqQ75FHHolUKhVDhgzJ7C/dEKlUaoOvP/bYY7HHHntEWVnZWsdXB/GPP/74WtuNbKps/TQnZ511Vpx++ulxyimnRN++fSOZTMY999wTiUQi6urq4pZbbomCgoIYO3ZsvP766/GDH/ygQfO67pruunvppZfi1FNPzYzHjBkTl13mwXwAAMDmJ2QGaGY6d+4cX/nKV+Kuu+6KSy65JB5++OF6z/v5z38egwYNiv322y/rnC+88EJMnDgx/vSnP8Vjjz0WRUVFcfDBB0enTp3ilFNOiW7dukXbtm0jnU7HsmXLYtmyZdG5c+eYP39+Zs/bt99+O84555yIWPVwws8++ywKCwtj/vz59W4F8MUXX8QNN9wQV111VfTt23et184666wYOXJkfOMb34g777xznfq69XxNf337VUes2l5hxYoVceGF/7dCf8WKFfH666/HrrvuGrW1tfH666/H9773vayf12rPP/98LFq0KJ5//vk4/vjjM8c/+OCDePnll2Pu3LkxfPjwOOKII2Lfffdt8Lxbo3bt2sUvfvGLuPrqq+PWW2/N7Me8ptdeey2uvfba+Pa3vx1dunTJOqfrrmmvu9/97neZFdk1NTXRqVOnBs8JAACwKYTMAM3QKaecEi+88ELceeedcf7552dWgEasekDbbbfdFtXV1XH99ddnnSuVSsW0adPiqquuilatWsU3v/nNePvtt+OJJ56IDz/8MJYvXx61tbVRXV0dS5cujYKCgrjpppuic+fOseOOO8bOO+8cr7zySpx++umxcOHCGDNmTBQUFMTtt98eVVVVccstt0RlZWX0798/vva1r0XHjh3j888/jzFjxsTll18eHTp0iKeeeioqKirWWj167rnnxrvvvhvTp0+PQYMGrdVzMpms92ep72FzdXV18dxzz0Xnzp3XeRhby5Yto1evXjFy5Mh46KGH4pprromioqKsn9lqxx9//Foh32o9evSIHj16xEUXXdTguZqDkpKSuPnmm+MnP/lJDBgwII455pjYaaedYvbs2fHXv/41XnrppTj33HPjggsuyDqX667pr7unnnqqwXMAAAA0pc3zCHqgWSstLU1HRFwz+rlct9Js/PS4slX//0L5Zn2fU3ZZO4havHhx/P3vf48WLVpETU1NzJ8/P+bNmxdHHHHEOgHZ5jR9+vRo2bJlLF68OPr27Rv5+flrvT5r1qyYM2dODBw4MCIi5s6du86D4V555ZXYbbfdonv37pljtbW1kZeXt86esvXVR0R89NFHme0PVksmk1FYWJh1X9qlS5duVNDXWGMql27292hq/37d1dXVxdixY2PixIkxd+7cKCwsjO7du8dxxx231p/f5ua6a7imvu5+duIxkaqri2uf/Vvk/dvnTna3nXVa1CSTUVBQUFReXr4s1/0AAMCmspIZoBlr165dfOUrX8l1G9GzZ88Nvt6tW7fo1q1bZlxfUFdfKL6+fXHrq4+IdYK+iFhnT9/12RJB37YiPz8/Bg8e3KT7VzeG6w4AAGDr4HHjAAAAAAA0mpAZAAC2oILClhER1VVVVelc9wIAAE3BdhkAzUhz3M+X5s91B03r0pGPRDKd7DikuHh5rnsBAICmYCUzUJ+ry4ZdkOseAAAAAGgGhMzAOioqKm4uOe3MXLcBAAAAQDMgZAYAgC2oZkUyJo4a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GAYffvihrbdxo0aNcHd3T9fm4qmnnkq3HR0dze7du9O11vD392f16tX8+eef7N69m1WrVnH48GEWLVrErFmz2Lt3L0WKFKFq1aq2MXFxcYwYMYIGDRqwY8cOZs+ejb+/PxEREVSoUIFVq1ZhGAaNGzdmzRrbwtH8+OOPjB071tb+wzAMYmJiGDFiRLo6k5KSqFOnju25s3L8+HGaNm1q265duzabNm263beeVatW8eCDD1KzZs1sz122bBn/+9//MJvNrFmzhsuXL2Mymfjyyy9v+75SwNWubf315MmMi9UVLQqjRlk/0vz8853dZ80ayKRdDmANoLt3Bx+f218wr3p160deq1cPNmywBs2enjB5svXDYrHOXjabMwbbxYrlfV0iIiIi2WgZ3I9GbR8d/72fn0JmEZE8pJBZ5DZUr16dDRs2EBMTQ9euXXnttddwdnambdu2tGvXDqebfuTcbDan2+fk5MTKlStts5wDAgLSbcfHx9O8eXNbT+cbFSlShLi4OKZMmULhwoVJSEjgxIkTODg44OPjw7hx42znJiUl8d///pfAwECGDRvGo48+So8ePQgJCeHkyZNcunQJi8VCVFQUxYsXTxceBwUF8f3339u2Dxw4wNixY9m8eXO6ej788EMuXLiQ7XsWHx+fbmHDIkWKEBERke24G1ksFj7//HMmpbUDyEavXr3o1asXgYGBeHp68t133+Hl5UX79u3x9PQkMjISwzAo9k8IlpiYaJvJLfep2w13b1dWATOAmxuULZu3988NmfWAd3CwhvEiIiIiIiJyX1PILHIHZs6cSceOHW2zajMLhYEMC/2lpKRkmLl883baeTde0zAMBg4cyJgxY2jSpAkAERERxMXFAVClShWqVKliO9/V1ZUPP/yQIkWKMGXKFF588UXKlCnDmDFjmDZtGmXLluX333/HbDZTMZtFxjZv3szDDz/M4cOHKV++vK1388WLF9OFx1lxdHRM9z44OzuTlJSU7bgbff/99xQqVCjLhQCzu//mzZtp06YNGzduZP369XTo0IHY2FjbzOb27dvf9nVFRERERERERMRKIbPIbbBYLMyZM4eNGzfSo0cPxo4dS+3atenUqRMPP/wwZcqUsZ1rGAYXLlxgz549ODo6AvDTTz8BsH//fiZPnkx0dDQlS5akQoUKhISEZAib02zZsoXLly/zwAMP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5APx8fFpL6/Zsw4RERERkdyikFn+FcMwwgFOnjxp71JEJIfOnTsHgMlk+tPOpYgUGGn/Hv7xxx/2LkXucampqVy4cAHDMAyz2XzM3vWIiIiIiOQGhczyb30MMGPGDI4ePcq1a5qQI3KvSkhI4MSJE4SEhKTt+sqe9YgUMLZ/D3///Xf9eygZmM1mzp49y4wZM4iKisJkMm379ddfL9q7LhGRgm5P6ErmPPfcaJR/iIjkKZO9C5B8z7levXobTCbTI/YuRERyzjCMvbGxsc2PHz+eZO9aRAoI53r16q0xmUyP27sQyRfOWSyWVgcPHtTU9wImKCjIABgbusHepeQaw2Lhj5/3sCf0K07/Fk7S9XYvUoCYHBzwKlGSOq0fJfCJjniXKm3vknLN2107YElNJSoqyjU8PNxs73pERAoqfSdP/q3kAwcOPG4YxjjgFyDB3gWJSJYSgd8Mw5imgFkk1yUfOHCgo8VieckwjIP806NZ5AbJhmGcAhaYzeb6CpglPzAsFtbMmsFnE17m+L69CpgLMMNiIfrSRb5f/invP9ubs7//Zu+SREQkn9FMZhERERERkVwQFBQ0uO2QYfPqtnvC3qXkil93bmPl9CmUKFGCsWPHUq9ePYoWLWrvsiQPJCcnc/bsWZYvX86KFSso6lOGYYuW4ujkZO/S/jXNZBYRuTs0k1lERERERCQXhIWFzS8oATPAvvXrAJgyZQqtW7dWwFyAOTs74+vry8svv0yjRo2IvHCekwf327ssERHJRxQyi4iIiIiISAZnj1pbJtSsWdPOlcjdVKNGDQDO/XHUzpWIiEh+opBZREREREREMjAnWJdb8fb2tnMlcjelfb7Vg1tERG6HQmYREREREZFcEBQUNG/T3Dn2LkNERETkrlPILCIiIiIikjsGH9y03t41iIiIiNx1CplFRERERERERERE5I4pZBYRERERERGRAqlh566YTKaZ4eHhqfauRUSkIFPILCIiIiIiIiIFUsvgfoyYP388oJBZRCQPKWQWERERERERERERkTumkFlERERERERERERE7phCZhEREREREREpkPaErmTOc8+NRvmHiEie0l+yIiIiIiIiUqAEBATQpUuXdPsCAwN58sknOXDgADt27MhybHJyMj169GD06NF5XKXcDTuXLsGSkjLdz8/Pyd61iIgUZPpLVkRERERERAqE+Ph43N3dszxuMpkIDQ3lhx9+IDAwEA8PD+bMmZPunLi4OI4dO8bly5czHPPx8aFbt25ZXn/u3LksXLgQgJCQEB5++OF/8TQiIiL5h0JmERERERGR3DGk7ZBh8+xdxP1q9erVzJ49m40bN2Z63GKx4OTkxJAhQ9iyZQvHjx/H39+fJUuWZHr+1atXMxyrXbv2LUPmDRs24O7uTnx8PBs3blTILCIi9w2FzCIiIiIiIrkgLCxs/tjQDQqZ7aRy5cpcu3aNnTt3ZjiWkpICgJOTE2XKlGHZsmVUrlwZgH379tnOW7lyJW+88QalS5fGzc2N2NhYPvjgA6pXr57t/Q8dOsTZs2dp06YNYWFh7Nq1i2vXruHh4ZFLTygiInLvUk9mERERERERyffq1KmDt7d3un7LAQEBBAQE0KhRIwBOnDhBQEAA3bp1Y+XKlbbzYmNjmT17Nm+++SaOjo6MGjWKt99+m6SkJPr06cPcuXOJjIy85f3Xr18PQLNmzWjYsCFms5lt27bl/oOKiIjcgxQyi4iIiIiISL7n4OBAUFBQup7MlSpVolKlSpQrV862r0KFClSqVAkvLy9OnTrFxIkTad++PcuWLcPV1ZVq1aoxYcIEtm3bxrx58/D29mbhwoU8/vjjDB06lGvXrmW4d0pKClu2bMFkMtG0aVOaNGkCWNtniIiI3A/ULkNERERERCQXBAUFzds0dw5thw63dyn3ralTpxIVFcXatWsBWLVqFQBhYWGMGDECgLfeeosaNWoA1kX+du3aRXx8PM2bN2fUqFE4OjoyZcoUFi9eTLNmzVixYgUffPABa9aswcnJKdP2F2FhYURHR1OjRg2KFStGUFAQAHv37uXKlSsUL178bjy+iNx7HOrWrdvOwcFhBNAE8LJ3QZInUg3DOA18npycPPfXX389be+C7EEhs4iIiIiISO4YfHDTeoXMdrR161befPPNDPvDwsIAcHNzY8+ePbaQ2dPTk6lTp1KuXDl8fX0xDIOYmBgMw6B///48+OCDAIwaNYrAwEBq1qyZ6X3TWmUAzJkzx3avxMRENm/eTI8ePXL1OUUkX3CoX7/+IqCvvQuRPOdoMpl8gXHOzs4jAgICHtm3b99Pdq7prlPILCIiIiIiIvne22+/zfLly/HySj9RMCYmhm+++YZy5cpRv3591q9fT3BwsO34H3/8wZ49ewgLC+PUqVN8/PHHnD9/3taH+aWXXmLatGls2LCBZcuWZbjvtWvX2LVrFwBHjx7l6NGj6Y5v2LBBIbPIfcjf3/8poG+JEiUYO3Ys9erVo2jRovYuS/JAcnIyZ8+eZfny5axYscLDMIzPgBpAir1ru5vUk1lERERERETyvZIlSxIQEMDy5cvT7X/rrbeIi4uje/futGrVimPHjrF582bb8Q0bNhAREWHb9vf3Z8WKFTz11FM89thjfPLJJwQGBgIwa9asDPf99ttvMZvNlC5dmr1797Jv3z727dvHF198AUB4eDinT9+XPzl9T2jYuSsmk2lmeHh4qr1rkfuLo6PjYIApU6bQunVrBcwFmLOzM76+vrz88stpC81Wrl+//qP2rutuU8gsIiIiIiIi+V7Xrl2ZN28epUuXBsAwDEJCQti4cSOVK1eme/fuNG/enHLlyvH6668THh7Ob7/9xp9//knLli1xcLB+eXz+/Hnc3NwYN24cxYsXZ8GCBSxcuJD27dtz6dIloqOj0903rVXG448/brsGQNWqVW1tOTZu3Hg33gLJRMvgfoyYP388oJBZ7rZAIMs2O1Iwpf29DwTYsw57ULsMERERERERyfcKFy6cbjspKYndu3dTokQJZs2ahYuLCwDjxo1jxIgRnDlzhgYNGtC3b1+aN2/Ojh07OHnyJB06dMhw7V69etG2bVvc3d1xckr/ZfT8+fOzrOmzzz7LhScTkXyqMIC3t7e965C76IbP9323yKNCZhERERERESlQBgwYQNGiRWnYsCEeHh74+PjYjjVp0oQlS5bg5+cHwPDh1oUaJ0yYQL169YiMjMQwDACcnJzw8/MjKCjo7j+EiIhIPqKQWURERERERAqU55577pbH0wLmG3l6evL000/nVUliJ3tCV7Jz2SejgbcAi73rEREpqNSTWUREREREREQKpJ1Ll2BJSZnu5+enSXYiInlIIbOIiIiIiIiIiIiI3DGFzCIiIiIiIrljSNshw+xdg4iIiMhdp5BZREREREQkF4SFhc2v2+4Je5chIiIictepJ5GIiIiIiIiISD5iGIZh7xrygslkMtm7BhG5M5rJLCIiIiIiIiIiIiJ3TCGziIiIiIhILggKCpq3ae4ce5chIpLrUlNTb3ncMAyOHDmS4+tFRET825IkDwUEBNClS5d0+wIDA3nyySc5cOAAO3bsyHJscnIyPXr0YPTo0Xlcpdxr1C5DREREREQkdww+uGk9bYcOt3cducLJ2YWUZDMJCQkUKlTI3uXIXZKYmAhYP/9SMKWmpjJlyhQmTZqEg8P1uYfx8fHMmDGDcePG4erqmm7MgAEDGD9+PNWrV8/0msnJyQQHB7Nv3z4AoqOjmTx5Mq+88golSpRId258fDyDBw9m2bJluLu75/LT5Z369esPS05OXn/48OE/7V1LXomPj7/l58RkMhEaGsoPP/xAYGAgHh4ezJmT/purcXFxHDt2jMuXL2c45uPjQ7du3TJcNyAgIN09vL29ady4MaNGjaJ48eL/8qnkblHILCIiIiIiIhkUK1eOS3+d5OzZs1StWtXe5chdcubMGQCKlS1n50okO6GhoUydOjXH56cFwBcuXOCnn35KFzADLFu2jKSkpAwBM0CzZs2YPn06ixcvJidtk729vfHx8aF3797MmzePkSNHYjabbcevXr1Kp06dbPdq3Lgxr776ao6fxU7mODs7z6lfv/4BwzA+TUlJ+ebw4cO/27uo3LJ69Wpmz57Nxo0bMz1usVhwcnJiyJAhbNmyhePHj+Pv78+SJUsyPf/q1asZjtWuXTvTkDnN008/jcViYefOnWzcuJGIiAgWLFhwp48kd5lCZhEREREREcmgzsOPsnXxR7z77rvMmjULZ2dne5ckeezQoUNs3boVZ1dXajYJsnc5uaJh5678tGrFzPDw8Fv3e8iHOnXqxJNPPglAw4YN2blzZ7qfOshsH8Dp06epXLlyun3nzp1j+fLlLFu2DIvFwrPPPsuMGTNss5CDg4NZtWoV69ev54knnshRfS+99BIxMTF4e3tz6dIlvv/+ewDCwsIICrL+/tq1axctWrS4szfAfuqZTKZ6zs7OIfXq1TtiGMay1NTUdYcPHz5s78L+jcqVK3Pt2jV27tyZ4VhKSgoATk5OlClThmXLltl+D6V98wJg5cqVvPHGG5QuXRo3NzdiY2P54IMPspwBf7OBAwdSpEgR/u///o8ePXrwyy+/5MKTyd2ikFlEREREREQyCHyyMwc3b+CHH36gdevWlCxZUm0zCiiLxUJMTAwXL17EMAweGzgUVw8Pe5eVK1oG96NR20fHf+/nV+BCZpPJhKOjo23bwcEh3XZm+/r168eRI0cwmUw0a9aMhIQE9u7dy5QpU+jVqxdly5YFrAF2SEgIb775JgCurq5MmTKFChUqMG3aNLZt25ZpTa1btwZg06ZNODs78/rrr2c4Z8KECbbxkydPzvJa+YHJZKplMpmmOzg4TK9fv/4JYBnw9f79+/fbu7bbVadOHby9vdP1W76xjQXAiRMnbPvGjRvHU089BUBsbCwLFy7k008/xdHRkVGjRvHAAw/Qr18/+vTpQ+/evXn66acpWrRojmqJj4+31ST5h0JmERERERERycDNw4P+s99ny0fzOPjtJk6dOmXvkiSPeZUoyWMDh1C71SP2LiXX1a9f33LTLiPdhpFuE5PJlH5HRrc8btx8wWzOz+54DurJkcWLFzNp0iQaNWpEmzZteOyxx5gxYwZ79+4lNTWV7du3Exsby7Vr14iJieHQoUP4+/sD0KBBAwBeffVVxo8fn67dhtlspkmTJrbAeMaMGSQlJTF27FhcXFxITU21LSQXGxub4fXDDz/M8OHDqV+/fmRmdf/zdt7qPbiT9+9ffU4yUQWYBEyqX7/+6dsca3cODg4EBQXh5HQ9KqxUqRJgncl89uxZACpUqICDgwNeXl6cOnWKRYsWsX37duLj43Fzc8PX15cJEybQv39/5s2bx6hRo1i4cCGffPIJ9erVIyQkBI8svon10UcfYbFY+PbbbwkMDLytdjBifwqZRUREREREJFMe3kXo/OLLPP7cCKIvXSQlOdneJUkeMJnAzbMwRUr75Kjfbn5z4dQpUyahr43JKsPuLF7nyL38Ph49epT+/ftz8eJFypYty6OPPkq1atUoX748JUqUoFixYnh7e7N27VoWLVpE48aNWbx4MXFxcezevRuLxULv3r0ZMWIEDRs2zPQeI0eO5LXXXuOjjz7iv//9L46OjqxatQqwznbO7PU/imR2vXv5/cyMYRgu+a1mgKlTpxIVFcXatWsBbJ+bsLAwRowYAcBbb71FjRo1AOsif7t27SI+Pp7mzZszatQoHB0dmTJlCosXL6ZZs2asWLGCDz74gDVr1uDk5JRlwAywfPly2+uiRYvy999/U7Jkybx6XMllCplFRERERETkllzd3Snl+4C9yxC5bXtCV7Jz2SejDhw44AzcPJs5N9wqSczq2C3HtGrVKt2O2NjYzM4339zKoHnz5hlOunHfoEGDCA4O5sqVK1SsWJGtW7dSvXp1GjRoYJulfKPHH3+cRx55hMKFC9OzZ0+aNGkCWGe8jhs3jpEjR/Lee+/x4IMPZhjr4uLCtGnTbL18U1OvdyuJjY2le/fumT58TEyM9837LBZLpu9X0aJFSU1NzfK9vPlYVtfJ7NzMxnl6ekZkdQ6AYRhnTCbTBuDrAwcOrMtk9vw9b+vWrbYWKTcKCwsDwM3NjT179thCZk9PT6ZOnUq5cuXw9fXFMAxiYmIwDIP+/fvbfm+MGjWKwMBAatasme393d3d2bx5M5MmTWLMmDFs2bIlwyKVcm9SyCwiIiIiIiIiBdLOpUuwpKZO9/PzmxkeHm7Og1vccQuHrNzYE/dW0hZcS2tVsWfPnnT9lwMCAvjuu+9wd3e37Tt06BCFCxe2zUpOC6oDAgIoVapUuutHRUWxe/fuTO/t5+fH+PHjOXToUKYhM8Cff/5JsWLFcHNzS9fPfe/evVk+0/Hjx2OyeWy7qV+/foZ9hmGcNJlM6y0Wy2cHDx78wQ5l5Zq3336b5cuX4+XllW5/TEwM33zzDeXKlaN+/fqsX7+e4OBg2/E//viDPXv2EBYWxqlTp/j44485f/48c+fOJTIykpdeeolp06axYcMGli1blm0dLi4utm9oREVFkZSUpPUA8gmFzCIiIiIiIiIi+VRERAReXl4ZFv3LjL+/P+3bt6dPnz7ExcUxcuRIwBrsbdiwId25aUFfVh5++GESExOxWCxcvHgxw2zT0NBQXF1defLJJylWrBjdu3fn/PnzODk52VogXLlyhZUrV+Z4Qbh7xFHDML4xmUyfHThwYJ+9i8ktJUuWJCAggKlTp9K+fXvb/rfeeou4uDgGDhxI+fLl+frrr9m8eTOPPfYYABs2bKBKlSq28/39/VmxYgWzZ8/mscce45NPPiEwMJANGzYwa9Ys5s+fn2UNH330EY6Ojnz//fcABAYGKmDORxQyi4iIiIiI5I4hbYcMm2fvIkTk/vLjjz/i5+eX4/OfffZZ9u/fz5EjRzh58iQPPfRQtmNOnz6NxWLBbDbj4uJi2//qq6+yfft2ANq1a5duTHh4OAMGDODo0aNUq1aNN954gy1bthASEsKiRYtITU1l5syZ+SZgNgzjDYvF8umhQ4fC7V1LXujatSu9e/e2fbPAMAxCQkLYuHEjlStXpnv37jg6OlKuXDlef/11ypUrh4ODA3/++Sf9+/fn+PHjAJw/f54yZcowbtw4zpw5w4IFCyhRogTt27fnl19+ITo6Gm/vDF1RAGtPZpPJRJEiRejUqZPtmyCSPyhkFhERERERyQVhYWHzx4ZuUMgsInfNn3/+ydy5c3nttddyPCY0NJSoqCiGDBnC0KFDWbp06S3PP3v2LEOGDKF58+Y8//zzjB49mqpVqwLw5ptvkpycjJOTE87OzrYxSUlJHD16FH9/f6ZNm2br+dymTRt+//13nn/+ecxmMxMnTryDp7aPAwcOjLd3DXmpcOHC6baTkpLYvXs3JUqUYNasWbZvLowbN44RI0Zw5swZGjRoQN++fWnevDk7duzg5MmTdOjQIcO1e/XqRdu2bXF3d8fJKWMUmdb6RfI3hcwiIiIiIiIiIvnMmjVrmDVrFoMGDSIoKChHYz7//HO+/PJL5s+fT6lSpXjooYeoXLkyycnJdOrUKdMx4eHhdOvWjb59+7JmzRomTpzI+fPncXBwwNHREScnJ0wmE2azmcTERJydnZk0aRIVKlTg6tWr/Pjjj4wfP57ExEQOHTpEbGwsZ86cAWDnzp2YzWYeeOABPDw8cu29kX9nwIABFC1alIYNG+Lh4YGPj4/tWJMmTViyZIlt9vzw4cMBmDBhAvXq1SMyMhLDsLYjd3Jyws/PL8e/PyV/U8gsIiIiIiIiIpLPlC5dmnfeeYd69eplenzQoEHpZhcD+Pr6snDhQooXLw5ArVq1AGjevDmzZ89Od+4LL7wAWGcfm0wmADp16pQujE5JSSElJQXAFjo7ODiwbds2WrVqxYULF+jXrx/h4eGMGzeOhx56iNatWzNq1CguX77MihUrmDJlCv3798/QbkPs57nnnrvl8czas3h6evL000/nVUmSD5jsXYCIiIiIiEhBEBQUNK9u2/aD2w4dbu9SROQfb3ftgCU1laioKNfw8HCzvevJLUbaVNF8wjAMDMPIsDjgzUxpaXYBUL9+fQPUCuJ+s2TJEubMmQMwY//+/WPtXc/ddOs/3SIiIiIiIpJTgw9uWm/vGkRE7jkmkynbgFlE8je1yxARERERERERyUcK0oxfESkY9G0kERERERERESmQGnbuislkmhkeHp5q71pERAoyhcwiIiIiIiIiUiC1DO7HiPnzxwMKmUVE8pBCZhERERERERERERG5YwqZRURERERERDJxYNN6Yq9ctncZueL0kV/5c//PpCSb7V2KiIgUQFr4T0RERERERCQTa2bNAKBS7Yeo1aIV1Rs1oahPGTtXdWf+/vUwWxbNx9XdnQebtaRW85b4PlQXFzc3e5eWp/aErmTnsk9GA28BFnvXI/eVRMAtISGBQoUK2bsWuUsSExMBMAwj0c6l3HUKmUVERERERERu4dSvv3Dq11/Y8OF7lK1WnTqt21CtYWNKlK9g79JuW1J8PAc3b+Dg5g24uLlRs2kzHmzWksr1AnB1d7d3eblu59IlWFJTp/v5+c0MDw/XNG65m44Dtc+ePUvVqlXtXYvcJWfOnEl7edyeddiDQmYRERERERGRHDr3xzHO/XGMTfM/oJTvA9Ru1ZoajZtS+oEq9i7ttpkTE/ll27f8su1bnJxdqN6oCbVatKRK/UAKFS5s7/JE8jXDMD41mUxvvPvuu8yaNQtnZ2d7lyR57NChQ2zduhXDMK45ODistXc9d5vJ3gWIiIiIiIgUBEFBQYPbDhk2r267J+xdSq6Y3LaVvUvIV4qWKUvk+XP2LiNXODg5Ua1BQ/xatqZqg0DcvbztXdIde7trByypqURFRblqJrPcTVWrVvUqXLjwXpPJVN3d3Z2SJUuqbUYBZbFYiImJ4eLFixiGgWEYLx44cGCmveu62zSTWUREREREJBeEhYXNHxu6YZ696xD7SDEXoPzSMEiIjSX2ymVSk5PtXY1IvnT8+PGYevXqNQNmXLt2rXd8fLyDvWuSPHf2n4B5ub0LsQfNZBYREREREcklayNiDHvXILknu9ncRcuUpWqDhtR5+BEq1KqNyXTvfokd9uXnbFk0P8vjDk5OVKr9EDWbNqN2q9Z4eBe5i9XlHc1klntBjRo1Cru5uVV0dHQs2Ctt3qdSU1MthmFE/fLLL38B9+3/AzSTWURERERERCSHSlSoSLWGjXmo9aOUqVrd3uX8K04urvjWeYiaQc3xa/Gw+jCL5JGjR4/GAuH2rkMkLylkFhERERERyQVBQUHzNs2dQ9uhw+1diuQyn8pVbcFyyUq+9i7nX3F2daVyvQAeDGpBreYtcVGPWBERyQUKmUVERERERHLH4IOb1itkLkBa9x3AQ63bUKS0j71L+ddKVa7CU+MnUqNxU5xd9RP7IiKSuxQyi4iIiIiIiGSiRY/gXL3e1XNnOfbTbhr/31MAJCXEkxgXx7WoKOIirxJ39QrVAhtRuHiJXL0vQLUGDXP9miIiImkUMouIiIiIiIjkMcMwWPfeLFr07M2CYYMwJyTg7FYIN09PUsxmSj9QGc+ixYi+HMEnY15IN7ZBh078uGoFANEREXiXLGk7NnLpF3f1OfKbhp278tOqFTPDw8NT7V2LiEhBdu8ufSsiIiIiIpKPBAUFGQBjQzfYuxS5B21ZOA/DYvDYoKG8PyCYYYuW2o7dvJ1mRreOjFmx1rYddfECX06dyKD3F9yVmguKxEunXbv7+ZntXYeISEGmmcwiIiIiIiJyz0gxm3FycbFtX4uOolBhLxwcHOxY1b/z1y8HuXjyTzq+MIZThw9hsViYN3SA7XjUxQu2bXdvb3q/OSvT6/y6YxuBHf/vrtQsIiJyOxQyi4iIiIiISJ4I37UDvxatbNupKSkkJyWRnJRIcmIi5oQESlbyxdHJ+qXpX78c5I+9P9FmwGDbmI9HDafr2FcpW73GLe+1dclC9q9fR0JcLD5VqtJhxGjKVqueJ891uyrWfoj/1KjJV29Oo0bjpjg4ODBk7iLb8fcHBGfYBkiMi+P9AcEMnf8xJpMDh7dvJfZKBC5ublRt0BBXd4+7/iwiIiKZUcgsIiIiIiIieWLrxwvwa9GKRSP/S3JSIs6ublz48wQ1GjfFzdOT5KQk2jw7GM+ixQCoVMefkwf328ZHR1zCSE2lTA7C4rLVatBkYXccnRzZtmQRK1+fzIgln+XZs90OBwcHdq9Zzekj4XR/9TXCViwHYNqTj1Gqki8uhQqxYNggLp36i1e/3mxrnTGjW0fb64NbNlKzaRB7vw7lwp8nOPrjD3QZ84rdnim/2BO6kp3LPhkNvAVY7F2PiEhBlX9/3khERERERETuSbs+W8r7A4KJjrjE+wOCebhPf4qWKUefGbPxLlmSTqPHEBd5laoNAvEsWozNC+byXt+eTO/0OIe3fct7fXvyXt+eHN7+LXFRkczp94xt33t9e5IQF5vhng8GNcfdywtXdw8eevQxrkVF2eHJM3f8570c/TEM10KFcHB0xDAM2zEHR0fbR1bMCQn8FPoVTZ/6DwCtevXl8t+nCN+1Pc9rz+92Ll2CJSVlup+fnybZiYjkIf0lKyIiIiIiIrmqRc9gWvQM5r2+PRm2aCkLhg0iOSmJj0cNI+rSRRaPHoE5MZHdX33JnrWhDJj9Pm0GDmFmjy622ccpycm817cH//fSOB4MagHA2//5P176YnWW9zUMg9grV9j91ZcEPPGkbf/P36xl57JPSIiLpVHnrunacdwNJSpW5Knxk1jy4vNphQJQpFRpnn13ru28tDYZ5sRELv55HHNiIstfexXPosUI6tbD1h4dZK8YAAAROElEQVTD0cmJTqNfZtn4l/D1r4eHd5G7+jwiIiI3U8gsIiIiIiKSO4a0HTJsnr2LuNekpqQw6P0F/LHnRw5t3UxSfDw1g5pjSU2lRc/etgX9LKmpODhe/xJ198ovMDk4EnnhPACxV6/gWbRolvc589sRFo58DoCqDRrSefTLtnHfzJnNgHc+pGTFilw5cyavHjVLRUqVtr2Oj47GzcMTgKhLF1n4/FDbsahLF7ly5jTLXh1LjcZNeWbaW5gTEjiyawe1W7VOd83SD1SmcZduRF+8qJBZRETsTu0yREREREREckFYWNj8uu2esHcZ94ykhHjioiL5aMQQ9q3/mtNHwuky9lVMJhMtegRT+oHKfD5pPAc2rbctCHgtKpL3+vZkWoc2FCtXno4vvMSZI+EAnD4STtnqNbO8X/kHazFp4zaGL1pKcmIi37z/DgCOTs6YHByIunAeV3ePbBcQzGunj/xKqQcqA9dnMqd9FClVmmLlyvP8ks9oN2QYD/jXo3K9+nR4fnSm1wrq9rTdn0dERAQ0k1lERERERERyWdiXn3Ppr5O4unvQokcw3368AHcvL/488DOXz5y2zd5NTUmllO8DODo5EXvlMj6VqzJwzjw+HNyPirXr4Fm0GF/PfhtzQgKHt33LQ488esv7mkwOFC9fgdZ9B/DZxPF0GjUGdy8vur48gc0fzSVsxed0GDGKcjUevBtvQ6YObFpPndZtAOvM5QXDBtmORV26iMlkSne+s6vbXa1PRETkTihkFhERERERkVzV5Kn/4ODgwHt9e1KreUt2LF1s6z28YNgg2+sPB/W1Bb5RFy/gWawYYF3ozrWQOyaTieqNm7Jm1lucP36Mp8ZPzFkBJhNOzs62Tb8WrXgwqDk7li1h5RtTef6fvs93W9nqNanVohXVGzUBoG6bdnQYMcp2fN17s7K9xtOTp+dZfSIiIndKIbOIiIiIiEguCAoKmrdp7hzaDh1u71LsLq3P8o0+HjUMgMtnTtteR164YDv+1y8HKf+gHwDmxASc3awzeP1atGLxi8/zaP9BODpl/iVs5PlznDr8C7VatCIxLo4d/1uMX8uHAYi5fJmr585SoVYtipUpR0pSUu496G26OSS/MWDObDszFf1q52pNIiIiuUEhs4iIiIiISO4YfHDTeoXMWeg/633AOpM57fWHg/oCkBgXx/4N6+gb8h6J165hMjlgMpm4cOI4q99+g6ZP/YcfvvqCwsWL4/9o2wzXdnZzY+/XoXz9bgguboWo3ao1bZ4dAoBhSWXdezOJPH+Ooj5l6Pziy3fngUVERO4jCplFREREREQkV8VFRpKakoyDo6NtX1of5sun/7a9jrxwHoCN89+nRpNmnDywjy2L5lO7ZWt2r1rB98s/5YkRo6jVrAW1W7Zm5fTX2L/hG4LfnJmuHYZn0WIMnDMv01q8S5Vm2ML/5dWj5liK2YyTi4tt+1p0FIUKe2U661tERCS/UcgsIiIiIiIiuerXHVv5KfQrAp540rYvrQ/zjdJmMjf/zzMU8fHBwdEJ/zbtcHJ25q9fDvLsux9StExZAMpWr8HQ+Ys5d+z3dAGzPYTv2oFfi1a27dSUFJKTkkhOSiQ5MRFzQgIlK/na2nv89ctB/tj7E20GDLaN+XjUcLqOfZWy1Wvc8l6GYXDmt3B+C/uOxwYOzfK8y2dOs/XjBZw8dIAUsxnfh/zp+MIYvEqU/HcPm8817NyVn1atmBkeHp5q71pERAoyU/aniIiIiIiISHaCgoIMgLGhG+xdiuSx9/r2ZMSSz1g08r8kJyXi7OrGhT9PUKNxU9w8PUlOSqLNs4PxLGpdyNAwDHYsXczDvfsDEB1xiU9eGsnwxZ9iMt36y/JZzzyFYTG4FhXJxA3bsjzv+y8/w8WtEHUefgRLaipfvxOCOTGR3m/OzL0Hz6cSL5127e7nZ7Z3HSIiBZlmMouIiIiIiIjkwK7PlvLL1s1ER1zi/QHBtB82kr1fr6Hry68yb+gAOo0ew1dvTsOvRSs8ixZj84K5/P7Dd8RevUrhYsU4vO1bAOq370BcVCRz+j2T7voD359PIc/C6fY9PXk6Ts7OzBs64Ja1NX3q6XStN4L+05P/jR2dS08uIiJya//f3r1GV1leeQDf5yQEQaWClwIiFVtRiReUkQCBlHpDR+tIsTowxcELDHYhtlqr06mddurSdgAdLgsUFVG0XriloFKKMuESMWoqglFHW8kCFMRIoiQEQk4yH5wctbo8oMF0sn6/T2fnvP/32ScfzlrZedbzGjIDAADAXigYMTIKRoyMKaNGxLh758TMcWNiz+7dMeu6cVG17Z247/rxUbdrV6yZ/1g8t6gwrrxjWpw9emxMGv69GD/7dxERUb9nT0wZNTyG3vCvcUJ+QURETLh0aNzw6MLPXLPrsT1jW/mGjL399dnONVVV0eHwj47KeOGJRbHiwfujtnpH5F007BNHdwDAl2XIDAAAAPsoVV8fY6bNjDeeezZeevqPsXvnzjg+f1A0pFJRMOKy9NC3IZWKZNZHf3qvmfdoJJJZ6Yce7tj+XhzUsWOz9tbQ0BDPzHskTj3nvPQaT0y9I678r+lxePfu8d7mzc263t+y5wrnxYoH778+In4bEQ0t3Q9Aa+UxtgAAALCXdtfujOqqyrh7/NgofXJxbHqlLL53488jkUhEwfCR8fUex8TD//6zeHHpk+kHAtZUVcaUUSPilgvOjk5HdosLf3xDbH6lLCIiNr1SFl17Ht+sPf5hxtRobGiM/sMuiYiIrOw2kUgmo2rrlmjb/sCMDxtsTVbMmR0N9fW35ubm2mQHsB/5kgUAAIC9UPzYw7GtfEO0bX9gFAwfGU/NmhntO3SIN198ISo2b4p7rr06IiJS9ak44ugekZWdHTveq4jOx3wrRk+9M6b/y+XR/cST4qCOnWLxHROirrY21i9/Kk4+86xm6/Hp++6O8pdejFETp0RW9od/8rfv0CGG3XRz/PHuGVE89+G4YPx1ceRxJzTbmgBgJzMAAEDzGDtk7LiW7oH9qP/Fl8bQn/4s2uTkRK9B347sNm3iqskz4qrJM6LrsT3Tr1N76tJD3Kp3tsZBnTpFRERdbW20bdc+EolE9Ow3IH5/+29jy59fj555A5qlv/9+YFa89szq+Of/vCPad+jwifdyCwbHtbN/F8f27Rfzbvt1s6wHAE3sZAYAAGgGxcXFd91YuOTOlu6D/eevH64XETHrug//sVCxeVP6deXWren3y9etjW4n5EZERN2u2mhzwAER8eHQ976fXBtnXTEmveP4yyh6cHaUrSyKURMmx4GHfPKM5w8qKmL722/FUb16RacuR0b97t1fej0A+DhDZgAAAPiCrrh9WkREzBw3Jv16+phRERGxq7o6/rTk8Rg1cUrsqqmJRCIZiUQitv7lz7Fwwm0x4OJL45n5j8bBhx4ap5w15DPv/8shgz/1+pdLi2Ln++/H/Tf+OK6+c1ZERBTNmR0RERP/cWj6+mQyGb9YsjwaG1Lx+JRJUbnl7ejYuUtc9JObmvE3AAARiZZuAAAAoLVY9O4HjS3dA/tPdWVlpOr3xJybro9x986J6WNGRU67dhERsa18QxxxdI+IiHhnw5vxb4uWRuGk30SbtgdE52O+GcvuvStO/PYZcWi3o2L1Iw/F+eOvi14DC+Lt1/8n5t36qzj40MNi5G8mRXabNi35EVudCcMuiIZUKqqqqtqWlZXVtXQ/AK2VncwAAADNID8//86lM6bGkKuvaelW2E9eLno6SgrnR5/zv5v+2VWTZ3zquqadzIMu/ac4pHPnSGZlxylnnxvZbdpE+bq1cdXk6dGxS9eIiOja87i4+q774u3XXzNgBuD/LTuZAQAAmkF+fn5jRMSNhUtauhXg/9jJDPDV+PRTCwAAAAAAYC8ZMgMAAACtUt+LhkUikZhUVlaWauleAFozx2UAAAA0A8dlwN+mXds2tb0kN9dRGQD7kZ3MAAAAAAB8YYbMAAAAAAB8YYbMAAAAQKv0XOG8mPrDH14f5h8A+5UvWQAAAKBVWjFndjTU19+am5ub3dK9ALRmhswAAAAAAHxhhswAAADNY+yQseNaugcAgK+cITMAAEAzKC4uvqv3uee3dBsAAF85ZxIBAAA0o8fvmBDvbHgz43V/f82Posuxx6XrJ6feHlveeCNjbsjV46LbCbnpeun0KbH5tVcz5s4ePTa6n3RKul42c3psfHl9xtwZV4yOHr1PS9dPz5oZ5Wtf3Odc0f2z4i+lz2fMDb7s8vjm3/VN1ysfuj/eKHk2Y27QiJHRs9+AdF386EPxWvHqjLn8S4bH8QML9jmXN3RYnPids9J1yYK58XLR8oy5PhdcGL3POS9dv7C4MF5atjRjrvc550afC/4hXa9d+mSUPrE4Y+7E75wZeUMvTtcvL38qSgrnZ8wdP3BQ5F8yIl2/unpFPPPYIxlzx/bNi4IfjErXb5SsiZUPPbDPuQ0vlsby++7JmOtx6mlxxuWj0/XGl9fFspkz0nVDKpXxHgB8eYbMAAAAzaQhVX/y6yVrHtizq7Z3pmufWzT/yu/+6Kfpqevra4rv2b1zZ9/Py0REPF84f2zXnsc901S/Wrxyxu6amvxMuTUL5o7v1iu3qKl+ZWXRlF3VOwZnyj27YO713zjp5GXpXNHTE3d+8ME5+5pbv3zZbTvfr8q41btkwdybepx62pNN9bqnlv5HTWXlRRnXW/jYL751et/Cpnrt0iU3V29/7/uZc/N+3bP/gLn7miv5/fwJvQoGz2mqS5csvmHHu++OzJR7ftGCqSefefbd6dwTi66p2rpl9OdlIiJeeHzRzFPPO3/aR/dZOHr7W5uvyZT705LFD5x+4UUT030vWjCyYmP5DZlya/+w47H+wy65JZ1bOP/7FRvLb86U2/lB1aKBw3/w86b62cK5F1ZsLL/l8zKfmVsw95yKjeUTPy8TEVFTtX3p4MsuT3+ekoXzCio2lk/7+DWNjY3bysrK9mS6FwBfXKKlGwAAAGhNBg4ceEwqlTow03U1NTVvrlu3rqap7tu3b4+srKyDMuXatm27oaioqLqp7tev39GJROLgTLlkMlleXFy8o6keMGDANxobGztkytXV1W0sLS19v6keOHBg91Qq9bV9zZ1++ulHZWdnH5IpV1tbu2nt2rVVTXVeXl63ZDLZMVMuKytr8+rVqyub6v79+x8ZEZ0y5SLirTVr1mzf11xdXd2W0tLSiqa6T58+XXJycg7LlMvOzt66atWqd5vqvLy8ryeTySMy5RoaGraVlJS801QPGjTo8Pr6+s57sV7FqlWrtnysz8NycnK6ZMolk8n3iouL326q+/fv3ykijtyLPitLSko2N9W9e/c+pF27dkfta65Pnz5fy8nJ6Z4pl5WV9f7q1as3NtX5+fkHNzQ0HP3xa6qrqzevX7++8lNhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDm97+aoECvVjXYygAAAABJRU5ErkJggg==","height":1096},"elements":{"elements":{"QuxAPESuAl939574":{"fontStyle":{"fontFamily":"黑体","size":14},"props":{"zindex":31},"dataAttributes":[],"points":[{"x":385,"y":197.89409255981445},{"x":385,"y":197.89409255981445}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":385,"y":143.7881851196289,"angle":4.71238898038469,"id":"BOZzIFqiwv080095"},"textPos":{"t":16,"pos":"in","x":385,"y":187.91527404785157},"to":{"x":385,"y":252.7881851196289,"angle":1.570796326794897,"id":"LrfDSUkhXm666325"},"id":"QuxAPESuAl939574","text":"① 过渡条件为触发器 T1/T2","locked":false,"group":""},"agukVAlhCz403244":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","x":0,"h":"h","y":0},"text":"最后一帧
停留 1.2s
"}],"anchors":[],"title":"","fontStyle":{"size":12},"dataAttributes":[],"props":{"zindex":53,"w":84,"x":857,"h":39,"y":720,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardText","fillStyle":{},"theme":{},"id":"agukVAlhCz403244","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"category":"standard","locked":false,"group":""},"SypMMbosuj035158":{"fontStyle":{"fontFamily":"黑体","size":14},"props":{"zindex":32},"dataAttributes":[],"points":[{"x":385,"y":351.64409255981445},{"x":225.01388549804688,"y":351.64409255981445}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":385,"y":303.7881851196289,"angle":4.71238898038469,"id":"LrfDSUkhXm666325"},"to":{"x":225.01388549804688,"y":399.5,"angle":1.570796326794897,"id":"DSkMisjWOm425681"},"id":"SypMMbosuj035158","text":"② 过渡条件为 T1","locked":false,"group":""},"LrfDSUkhXm666325":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"子状态机"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":16,"color":"255,255,255"},"dataAttributes":[{"name":"序号","id":"dTtcktIOLC729055","type":"number","category":"default","value":""},{"name":"名称","id":"sGiYRnWpmR375338","type":"string","category":"default","value":""},{"name":"所有者","id":"ekqNGGdDgI080445","type":"string","category":"default","value":""},{"name":"连接","id":"FSvorrBldx827818","type":"link","category":"default","value":""},{"name":"便笺","id":"TrWluBgnfu492975","type":"string","category":"default","value":""}],"props":{"zindex":28,"w":100,"x":335,"h":51,"y":252.7881851196289,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{"lineColor":"125,200,227","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{"color":"51,102,153","type":"solid"},"theme":{},"id":"LrfDSUkhXm666325","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"MATMaFbgjN204862":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 A
动画时长为 3 秒
结束次数为 (3 - 0.3)/3 = 0.9 遍"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":14},"dataAttributes":[{"name":"序号","id":"pRACDmrZfe160856","type":"number","category":"default","value":""},{"name":"名称","id":"vZKjYqFBFO852812","type":"string","category":"default","value":""},{"name":"所有者","id":"diNTnKgWyI303854","type":"string","category":"default","value":""},{"name":"连接","id":"nREDFhSrMO478942","type":"link","category":"default","value":""},{"name":"便笺","id":"tsEPHrBSOp105062","type":"string","category":"default","value":""}],"props":{"zindex":34,"w":237.50868368148804,"x":31.243917226791382,"h":64.7673568725586,"y":636.8489646911621,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"MATMaFbgjN204862","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"GzvfAXRGKz669720":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"fontFamily":"黑体","size":24,"vAlign":"top"},"title":"矩形","dataAttributes":[{"name":"序号","id":"hptjuCleeV248452","category":"default","type":"number","value":""},{"name":"名称","id":"axXhaRVApX210753","category":"default","type":"string","value":""},{"name":"所有者","id":"YasBcpEkcp107705","category":"default","type":"string","value":""},{"name":"连接","id":"SIvgjbwyhD242661","category":"default","type":"link","value":""},{"name":"便笺","id":"oLstSpiqzZ729966","category":"default","type":"string","value":""}],"props":{"zindex":26,"w":473.0451383590698,"h":255.1562614440918,"x":148.4774308204651,"angle":0,"y":223.8437385559082},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineStyle":"dashed","lineColor":"59,59,59","lineWidth":2},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"178,235,242","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"GzvfAXRGKz669720","category":"basic","locked":false,"group":""},"vGOfwwnBKx332586":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 C"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":16,"color":"255,255,255"},"dataAttributes":[{"name":"序号","id":"srOHaCwHbp078562","type":"number","category":"default","value":""},{"name":"名称","id":"NoozbVQUGa425311","type":"string","category":"default","value":""},{"name":"所有者","id":"wSmxXkEpQc737187","type":"string","category":"default","value":""},{"name":"连接","id":"NsYavWwerg813225","type":"link","category":"default","value":""},{"name":"便笺","id":"OlNLtfaCzX103361","type":"string","category":"default","value":""}],"props":{"zindex":30,"w":100,"x":492.0138854980469,"h":51,"y":399.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{"lineColor":"125,200,227","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{"color":"51,102,153","type":"solid"},"theme":{},"id":"vGOfwwnBKx332586","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"eluChmQMLQ844377":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":14,"bold":true},"dataAttributes":[{"name":"序号","id":"wcvfIDBMKB939913","type":"number","category":"default","value":""},{"name":"名称","id":"gRnbANycCM219711","type":"string","category":"default","value":""},{"name":"所有者","id":"dWIDtjxoTO597808","type":"string","category":"default","value":""},{"name":"连接","id":"BRTDyVwwfR565453","type":"link","category":"default","value":""},{"name":"便笺","id":"KIWkNhCXeZ891688","type":"string","category":"default","value":""}],"props":{"zindex":38,"w":139.77777099609375,"x":481.74479818344116,"h":70,"y":817.2326431274414,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"eluChmQMLQ844377","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"wHvLfYVkjp576282":{"fontStyle":{"fontFamily":"黑体","size":14,"color":"255,0,0"},"props":{"zindex":39},"dataAttributes":[],"points":[{"x":375.625,"y":852.2326431274414},{"x":375.625,"y":852.2326431274414}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":269.50520181655884,"y":852.2326431274414,"angle":3.141592653589793,"id":"eylwQzJALs204157"},"to":{"x":481.74479818344116,"y":852.2326431274414,"angle":0,"id":"eluChmQMLQ844377"},"id":"wHvLfYVkjp576282","text":"过渡周期\n设置为 0.3 遍","locked":false,"group":""},"eJraDwpUGc777013":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"fontFamily":"黑体","size":24,"vAlign":"top"},"title":"矩形","dataAttributes":[{"name":"序号","id":"FGkIfyXTbq664794","category":"default","type":"number","value":""},{"name":"名称","id":"VFdjstByFV351886","category":"default","type":"string","value":""},{"name":"所有者","id":"qpqwyqZuMp850782","category":"default","type":"string","value":""},{"name":"连接","id":"YCJufzsDap172106","category":"default","type":"link","value":""},{"name":"便笺","id":"vBlcmZhZzw375489","category":"default","type":"string","value":""}],"props":{"zindex":56,"w":229.5225691795349,"h":126.1562614440918,"x":741.4774308204651,"angle":0,"y":862.8437385559082},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineStyle":"dashed","lineColor":"59,59,59","lineWidth":2},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"178,235,242","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"eJraDwpUGc777013","category":"basic","locked":false,"group":""},"yondKCrIIK620777":{"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":793,"y":721},"to":{"x":868.5,"y":721},"id":"yondKCrIIK620777","text":"","locked":false,"props":{"zindex":52},"dataAttributes":[],"group":"","points":[{"x":830.75,"y":721},{"x":830.75,"y":721}]},"UcVpfHyYlq805818":{"fontStyle":{"fontFamily":"黑体","size":14,"color":"244,67,54"},"props":{"zindex":36},"dataAttributes":[],"points":[{"x":375.625,"y":671.8489646911621},{"x":375.625,"y":671.8489646911621}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":269.50520181655884,"y":671.8489646911621,"angle":3.141592653589793,"id":"MATMaFbgjN204862"},"to":{"x":481.74479818344116,"y":671.8489646911621,"angle":0,"id":"NZAPcPzVpQ732100"},"id":"UcVpfHyYlq805818","text":"过渡周期\n设置为 0.3 秒","locked":false,"group":""},"TjeXVcYEDA847150":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"fontFamily":"黑体","size":24,"vAlign":"top"},"title":"矩形","dataAttributes":[{"name":"序号","id":"ZEhNvlenyx576941","category":"default","type":"number","value":""},{"name":"名称","id":"aeGgSdyumS256807","category":"default","type":"string","value":""},{"name":"所有者","id":"yGsvNFuzdg690269","category":"default","type":"string","value":""},{"name":"连接","id":"nAykxhgNfH631668","category":"default","type":"link","value":""},{"name":"便笺","id":"inzdjBpjRE331276","category":"default","type":"string","value":""}],"props":{"zindex":47,"w":229.5225691795349,"h":126.1562614440918,"x":741.4774308204651,"angle":0,"y":644.8437385559082},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineStyle":"dashed","lineColor":"59,59,59","lineWidth":2},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"178,235,242","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"TjeXVcYEDA847150","category":"basic","locked":false,"group":""},"IUAYCHpwvR585542":{"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":792,"y":945},"to":{"x":867.5,"y":945},"id":"IUAYCHpwvR585542","text":"","locked":false,"props":{"zindex":58},"dataAttributes":[],"group":"","points":[{"x":829.75,"y":945},{"x":829.75,"y":945}]},"eylwQzJALs204157":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 A
动画时长为 3 s
结束次数为 0.7 遍"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":14},"dataAttributes":[{"name":"序号","id":"pYeCfEHYee569827","type":"number","category":"default","value":""},{"name":"名称","id":"kTsRKlpAAQ519464","type":"string","category":"default","value":""},{"name":"所有者","id":"zyFOmOanwF025376","type":"string","category":"default","value":""},{"name":"连接","id":"OJYzTJqrzL111338","type":"link","category":"default","value":""},{"name":"便笺","id":"jEaMAeEiFr520764","type":"string","category":"default","value":""}],"props":{"zindex":37,"w":239.01388549804688,"x":30.491316318511963,"h":70,"y":817.2326431274414,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"eylwQzJALs204157","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"EuWypzMFNb181054":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","x":0,"h":"h","y":0},"text":"循环
播放 1.2s
"}],"anchors":[],"title":"","fontStyle":{"size":12},"dataAttributes":[],"props":{"zindex":61,"w":84,"x":845,"h":39,"y":944,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardText","fillStyle":{},"theme":{},"id":"EuWypzMFNb181054","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"category":"standard","locked":false,"group":""},"BOZzIFqiwv080095":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 A"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":16},"dataAttributes":[{"name":"序号","id":"ORcnvWmmWM025489","type":"number","category":"default","value":""},{"name":"名称","id":"BcZMZeflVZ856847","type":"string","category":"default","value":""},{"name":"所有者","id":"CFLvAuLNss445483","type":"string","category":"default","value":""},{"name":"连接","id":"TWLQRrpCaB490510","type":"link","category":"default","value":""},{"name":"便笺","id":"StTowUqbOp569640","type":"string","category":"default","value":""}],"props":{"zindex":27,"w":100,"x":335,"h":51,"y":92.7881851196289,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"BOZzIFqiwv080095","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"DbetzvYcev176085":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","x":0,"h":"h","y":0},"text":"播放 3s"}],"anchors":[],"title":"","fontStyle":{"size":12},"dataAttributes":[],"props":{"zindex":60,"w":84,"x":785.5,"h":39,"y":940,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardText","fillStyle":{},"theme":{},"id":"DbetzvYcev176085","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"category":"standard","locked":false,"group":""},"XPNpRTzEXZ529813":{"fontStyle":{"fontFamily":"黑体","size":14},"props":{"zindex":33},"dataAttributes":[],"points":[{"x":385,"y":351.64409255981445},{"x":542.0138854980469,"y":351.64409255981445}],"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":385,"y":303.7881851196289,"angle":4.71238898038469,"id":"LrfDSUkhXm666325"},"to":{"x":542.0138854980469,"y":399.5,"angle":1.570796326794897,"id":"vGOfwwnBKx332586"},"id":"XPNpRTzEXZ529813","text":"③ 过渡条件为 T2","locked":false,"group":""},"DSkMisjWOm425681":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":16,"color":"255,255,255"},"dataAttributes":[{"name":"序号","id":"GnMJNreToX513402","type":"number","category":"default","value":""},{"name":"名称","id":"NyedfxulLk937048","type":"string","category":"default","value":""},{"name":"所有者","id":"TCeMmDPXDJ177212","type":"string","category":"default","value":""},{"name":"连接","id":"lAMTzTrEpp916241","type":"link","category":"default","value":""},{"name":"便笺","id":"XYMjxIlXzv579719","type":"string","category":"default","value":""}],"props":{"zindex":29,"w":100,"x":175.01388549804688,"h":51,"y":399.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{"lineColor":"125,200,227","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{"color":"51,102,153","type":"solid"},"theme":{},"id":"DSkMisjWOm425681","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"jpuvdHUhVy414669":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 A"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"bold":true},"dataAttributes":[{"name":"序号","id":"riQgeTtery132433","type":"number","category":"default","value":""},{"name":"名称","id":"bJfLOfXqib044660","type":"string","category":"default","value":""},{"name":"所有者","id":"muQlwSsYUd247625","type":"string","category":"default","value":""},{"name":"连接","id":"loILkILonf339531","type":"link","category":"default","value":""},{"name":"便笺","id":"urwjujTXHo818864","type":"string","category":"default","value":""}],"props":{"zindex":57,"w":126.47743082046509,"x":793,"h":37.767356872558594,"y":887.2326431274414,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"jpuvdHUhVy414669","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"iMNighSMHQ583553":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","x":0,"h":"h","y":0},"text":"播放 3s"}],"anchors":[],"title":"","fontStyle":{"size":12},"dataAttributes":[],"props":{"zindex":50,"w":84,"x":785.5,"h":39,"y":712,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardText","fillStyle":{},"theme":{},"id":"iMNighSMHQ583553","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"category":"standard","locked":false,"group":""},"rNUmsWvSyG945511":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 A"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"bold":true},"dataAttributes":[{"name":"序号","id":"WSIubHDThP178530","type":"number","category":"default","value":""},{"name":"名称","id":"PhXHvppcko311621","type":"string","category":"default","value":""},{"name":"所有者","id":"KHOHNXqmnP773144","type":"string","category":"default","value":""},{"name":"连接","id":"bicQHSmgPf506557","type":"link","category":"default","value":""},{"name":"便笺","id":"rjugFMfNLN374927","type":"string","category":"default","value":""}],"props":{"zindex":48,"w":126.47743082046509,"x":793,"h":37.767356872558594,"y":663.8489646911621,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"rNUmsWvSyG945511","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"wfKxHaQHWV679637":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":""}],"anchors":[{"x":"0","y":"h*0.5"},{"x":"0","y":"h*0.5"},{"x":"0","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"title":"注释","fontStyle":{},"dataAttributes":[{"name":"序号","id":"coAPIVfOPt538260","type":"number","category":"default","value":""},{"name":"名称","id":"IkKOvcwkHP406545","type":"string","category":"default","value":""},{"name":"所有者","id":"UqxLxMoiiK828196","type":"string","category":"default","value":""},{"name":"连接","id":"PTgmSFCyLY968905","type":"link","category":"default","value":""},{"name":"便笺","id":"guBUjfiJTw426706","type":"string","category":"default","value":""},{"name":"成本","id":"JyDKRbpXuh400233","type":"number","category":"default","value":""},{"name":"时间","id":"tTKkVDVKPD165262","type":"number","category":"default","value":""},{"name":"部门","id":"BQqcsMOTTQ865175","type":"string","category":"default","value":""},{"name":"输入","id":"dBoLsFiMjh541980","type":"string","category":"default","value":""},{"name":"输出","id":"qMRbAwdWtn409176","type":"string","category":"default","value":""},{"name":"风险","id":"uMJLGXKpVU618435","type":"string","category":"default","value":""},{"name":"备注","id":"TOMQrNnuyk448030","type":"string","category":"default","value":""}],"props":{"zindex":49,"w":44,"x":874.5,"h":47,"y":677.5,"angle":4.71238898038469},"path":[{"fillStyle":{"type":"none"},"actions":[{"x":"Math.min(w/6, 20)","action":"move","y":"0"},{"x":"0","action":"line","y":"0"},{"x":"0","action":"line","y":"h"},{"x":"Math.min(w/6, 20)","action":"line","y":"h"}]},{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"annotation","fillStyle":{},"theme":{},"id":"wfKxHaQHWV679637","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"flow","locked":false,"group":""},"cOovlqhAvB974537":{"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":971,"y":707.9218692779541,"angle":3.141592653589793,"id":"TjeXVcYEDA847150"},"to":{"x":1125,"y":707.9218692779541},"id":"cOovlqhAvB974537","text":"开始过渡","locked":false,"props":{"zindex":54},"dataAttributes":[],"group":"","points":[{"x":1048,"y":707.9218692779541},{"x":1048,"y":707.9218692779541}]},"vfMAMAEdfU585662":{"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":971,"y":925.9218692779541,"angle":3.141592653589793,"id":"eJraDwpUGc777013"},"to":{"x":1128,"y":925.9218692779541},"id":"vfMAMAEdfU585662","text":"开始过渡","locked":false,"props":{"zindex":62},"dataAttributes":[],"group":"","points":[{"x":1049.5,"y":925.9218692779541},{"x":1049.5,"y":925.9218692779541}]},"grihhKRoFz459452":{"linkerType":"broken","lineStyle":{},"name":"linker","from":{"x":869,"y":945},"to":{"x":919.4774308204651,"y":945},"id":"grihhKRoFz459452","text":"","locked":false,"props":{"zindex":59},"dataAttributes":[],"group":"","points":[{"x":894.2387154102325,"y":945},{"x":894.2387154102325,"y":945}]},"HMrWDfiIXv077034":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"bold":true},"dataAttributes":[{"name":"序号","id":"CmutjSuqBd983227","type":"number","category":"default","value":""},{"name":"名称","id":"AhcGkGJIFf625730","type":"string","category":"default","value":""},{"name":"所有者","id":"ufAieBEiym148768","type":"string","category":"default","value":""},{"name":"连接","id":"dYzQnpDfCR698604","type":"link","category":"default","value":""},{"name":"便笺","id":"naBDauKLHr554632","type":"string","category":"default","value":""}],"props":{"zindex":55,"w":126.47743082046509,"x":1130,"h":37.767356872558594,"y":689.0381908416748,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"HMrWDfiIXv077034","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"NZAPcPzVpQ732100":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"fontFamily":"黑体","size":14,"bold":true},"dataAttributes":[{"name":"序号","id":"UBWmkFvEsl100123","type":"number","category":"default","value":""},{"name":"名称","id":"daeLZQzlZu890923","type":"string","category":"default","value":""},{"name":"所有者","id":"rwaZGiiTiX954509","type":"string","category":"default","value":""},{"name":"连接","id":"FJFnjBxCGz270128","type":"link","category":"default","value":""},{"name":"便笺","id":"YJQsKopFiI143414","type":"string","category":"default","value":""}],"props":{"zindex":35,"w":139.77777099609375,"x":481.74479818344116,"h":70,"y":636.8489646911621,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"NZAPcPzVpQ732100","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""},"pexwZPjfQk789747":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"状态 B"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"圆角矩形","fontStyle":{"bold":true},"dataAttributes":[{"name":"序号","id":"TINjkTVodC371430","type":"number","category":"default","value":""},{"name":"名称","id":"KvaEbizvRU877134","type":"string","category":"default","value":""},{"name":"所有者","id":"OdHfVqpYja209101","type":"string","category":"default","value":""},{"name":"连接","id":"gJJETHlUJD839151","type":"link","category":"default","value":""},{"name":"便笺","id":"bEdRACVLhP446660","type":"string","category":"default","value":""}],"props":{"zindex":63,"w":126.47743082046509,"x":1130,"h":37.767356872558594,"y":907.0381908416748,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"4"},{"y1":"0","x":"4","action":"quadraticCurve","x1":"0","y":"0"},{"x":"w-4","action":"line","y":"0"},{"y1":"0","x":"w","action":"quadraticCurve","x1":"w","y":"4"},{"x":"w","action":"line","y":"h-4"},{"y1":"h","x":"w-4","action":"quadraticCurve","x1":"w","y":"h"},{"x":"4","action":"line","y":"h"},{"y1":"h","x":"0","action":"quadraticCurve","x1":"0","y":"h-4"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"roundRectangle","fillStyle":{},"theme":{},"id":"pexwZPjfQk789747","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"basic","locked":false,"group":""}},"page":{"padding":20,"backgroundColor":"transparent","orientation":"portrait","gridSize":15,"width":1622,"showGrid":true,"lineJumps":false,"height":2346}}},"meta":{"exportTime":"2021-11-29 17:56:46","member":"5a801cd0e4b0874437c6723b","diagramInfo":{"creator":"5a801cd0e4b0874437c6723b","created":"2021-11-23 11:54:56","modified":"2021-11-29 17:17:50","title":"状态变量","category":"flow"},"id":"619c66106376897069ce4e78","type":"ProcessOn Schema File","version":"1.0"}} \ No newline at end of file diff --git a/versions/4.0/zh/animation/marionette/index.md b/versions/4.0/zh/animation/marionette/index.md new file mode 100644 index 0000000000..e5c1950d7a --- /dev/null +++ b/versions/4.0/zh/animation/marionette/index.md @@ -0,0 +1,44 @@ +# Marionette 动画系统 + +Cocos Creator 3.4 引入了一个全新的 Marionette 动画系统,通过状态机控制对象的骨骼动画,实现了自动化、可复用的动画流程。 + +为了跟 v3.4 之前的动画系统区分,我们将新的动画系统称为木偶(Marionette)动画系统,称 v3.4 之前使用的动画系统为旧式动画系统。两种动画系统都可以正常使用,但不支持同时使用。主要的区别在于: + +- 旧式动画系统:以动画组件、动画状态为核心,手动简单控制动画剪辑的播放暂停等。动画剪辑支持使用通过编辑器创建的 Animation Clip 和外部导入的骨骼动画(`.fbx` 、 `.gltf` 和 `.glb`)。 + +- Marionette 动画系统:以动画控制器组件、动画图为核心,按照事先搭建好的动画图,通过状态机自动控制动画剪辑的播放和切换等。动画剪辑只支持外部导入的骨骼动画(`.fbx` 、 `.gltf` 和 `.glb`)。 + +## 内容 + +Marionette 动画系统主要内容包括: + +- [动画图资源](animation-graph.md) +- [动画控制器组件参考](animation-controller.md) +- [动画图面板](animation-graph-panel.md) +- [动画图层级](animation-graph-layer.md) +- [动画状态机](animation-graph-basics.md) +- [状态过渡](state-transition.md) +- [动画遮罩资源](animation-mask.md) +- [动画图变体](animation-variant.md) +- [程序化动画](./procedural-animation/index.md) + +## 名词解释 + +Marionette 动画系统相关功能名词的说明如下: + +| 功能名词 | 说明 | +| :----- | :--- | +| 动画图资源 | 用于存储对象的整个动画流程数据,可直接在 **资源管理器** 面板创建。详情请参考 [动画图资源](animation-graph.md)。 | +| 动画控制器组件 | 引用动画图资源并将其应用于对象。详情请参考 [动画控制器组件参考](animation-controller.md)。 | +| 动画图面板 | 当准备好对象所需的骨骼动画后,便可通过动画图面板将其组合成完整的动画流程。具体的操作请参考 [动画图面板](animation-graph-panel.md)。| +| 状态 | 对象所处的一种播放特定动画剪辑的动作,例如待机、行走、移动、攻击等。
该状态与旧式动画系统中,动画组件为每一个动画剪辑创建的 [动画状态](../animation-state.md) 不同。| +| 状态过渡 | 大多数情况下,一个对象会拥有多个状态,并按照一定的需求逻辑在各个状态之间切换,这种切换便称为 [状态过渡](state-transition.md)。例如角色在行走时触发了死亡条件,行走状态便会切换到死亡状态。 | +| 动画状态机 | 用于可视化地管理控制某个对象上各个状态及状态之间的过渡,可视为一个流程图。详情请参考 [动画状态机](animation-graph-basics.md)。| + +状态及状态过渡在动画图面板中是以图形的方式显示,例如下图,方块表示状态,箭头表示状态之间的过渡: + +![example](animation-graph-basics/example.png) + +## 范例参考 + +Creator 提供了 **Ms.Amoy**([GitHub](https://github.com/cocos-creator/example-marionette)|[Gitee](https://gitee.com/mirrors_cocos-creator/MarionetteDemo))范例,演示了 Marionette 动画系统的使用方式,用户可根据需要下载参考使用。 diff --git a/versions/4.0/zh/animation/marionette/preview-bar/end.png b/versions/4.0/zh/animation/marionette/preview-bar/end.png new file mode 100644 index 0000000000..c5c1cb1ff2 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/preview-bar/end.png differ diff --git a/versions/4.0/zh/animation/marionette/preview-bar/next.png b/versions/4.0/zh/animation/marionette/preview-bar/next.png new file mode 100644 index 0000000000..2a04bda7d8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/preview-bar/next.png differ diff --git a/versions/4.0/zh/animation/marionette/preview-bar/pause.png b/versions/4.0/zh/animation/marionette/preview-bar/pause.png new file mode 100644 index 0000000000..059f6da6b9 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/preview-bar/pause.png differ diff --git a/versions/4.0/zh/animation/marionette/preview-bar/play.png b/versions/4.0/zh/animation/marionette/preview-bar/play.png new file mode 100644 index 0000000000..fdaf9c5103 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/preview-bar/play.png differ diff --git a/versions/4.0/zh/animation/marionette/preview-bar/prev.png b/versions/4.0/zh/animation/marionette/preview-bar/prev.png new file mode 100644 index 0000000000..d8de20e414 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/preview-bar/prev.png differ diff --git a/versions/4.0/zh/animation/marionette/preview-bar/start.png b/versions/4.0/zh/animation/marionette/preview-bar/start.png new file mode 100644 index 0000000000..be3e88ab98 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/preview-bar/start.png differ diff --git a/versions/4.0/zh/animation/marionette/preview-bar/stop.png b/versions/4.0/zh/animation/marionette/preview-bar/stop.png new file mode 100644 index 0000000000..623a23e7a7 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/preview-bar/stop.png differ diff --git a/versions/4.0/zh/animation/marionette/preview-bar/time.png b/versions/4.0/zh/animation/marionette/preview-bar/time.png new file mode 100644 index 0000000000..3ee904d103 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/preview-bar/time.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/auxiliary-curve/img/auxiliary-curve.png b/versions/4.0/zh/animation/marionette/procedural-animation/auxiliary-curve/img/auxiliary-curve.png new file mode 100644 index 0000000000..747d9aa672 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/auxiliary-curve/img/auxiliary-curve.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/auxiliary-curve/img/inspector-property.png b/versions/4.0/zh/animation/marionette/procedural-animation/auxiliary-curve/img/inspector-property.png new file mode 100644 index 0000000000..d52b7978a0 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/auxiliary-curve/img/inspector-property.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/auxiliary-curve/index.md b/versions/4.0/zh/animation/marionette/procedural-animation/auxiliary-curve/index.md new file mode 100644 index 0000000000..098f1efb34 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/auxiliary-curve/index.md @@ -0,0 +1,58 @@ +# 辅助曲线(实验性) + +![auxiliary-curve.png](./img/auxiliary-curve.png) + +> **注意**:辅助曲线是实验性功能,请谨慎使用。Cocos Creator 期待你的反馈。 + +## 动机 + +有时,在角色进行某个动作时,除了骨骼的可视变化,还伴随着其它一些关联属性的变化。例如: + +- 选择性地使用 IK 纠正脚的位置:当角色原地站立时进行纠正;在角色行走时不进行纠正。 + +- 从跑开始起跳动作时,可能需要根据起跳时两脚的方位来决定是使用左脚起跳的动作还是右脚起跳的动作。这实质上是需要跑动作中反应出两脚的方位变化。 + +- 伴随着动作的进行,有材质变化、粒子特效变化、声音变化等。 + +其中的有些变化可以通过 [嵌入播放器](../../../embedded-player.md) 实现。但有时嵌入播放器无法胜任: + +- 嵌入播放器所描述的变化无法像骨骼那样参与混合。 + + 例如,当动作淡出时,嵌入播放器将戛然而止;两个动作的嵌入播放器是独立的——即使它们描述的可能的是同一份材质的颜色变化,但它们的效果无法跟随动作进行混合。 + +- 嵌入播放器描述的值的变化无法直接在动画图中使用。 + 动画图中无法直接地读取嵌入播放器的结果。即使可以,也需要较为繁琐的设置步骤。 + +> 这并不是说我们不再鼓励使用嵌入播放器。当你没有上述的混合需求时,嵌入播放器仍是一种快速附加其它效果在动作上的途径。 + +因此,Cocos Creator 引入了一项新的功能来解决上述问题。 + +## 概念:辅助曲线 + +**辅助曲线** 是一条带名称的 **浮点数** 曲线,它的值作为姿态中除骨骼外的另一项属性: + +- 从动画剪辑中采样姿态时,辅助曲线的也会被采样; + +- 混合姿态时,同名的辅助曲线值也会被混合。 + +如其名,辅助曲线在骨骼动画中扮演了辅助角色。辅助曲线的来源和应用都是由使用者定义的。 + +> 辅助曲线和骨骼动画之间最大的区别就是:骨骼动画的值应用时会被写入场景中,驱动骨骼进而产生可视的模型动画;但辅助曲线的值不会直接产生可视效果,它仅可以被读取,由使用者决定其效用。 + +## 编辑辅助曲线 + +在动画剪辑编辑器中,可编辑 [动画剪辑中的辅助曲线](../../../animation-auxiliary-curve.md)。 + +在姿态图中,可通过 [设置辅助曲线结点](../pose-graph/pose-nodes/modify-pose.md#设置辅助曲线) 设置或修改姿态的辅助曲线当前值。 + +## 使用辅助曲线 + +辅助曲线可在动画图和动画控制器中读取和使用: + +- 在 [过渡条件](../../state-transition.md) 中引用辅助曲线。 + +- 姿态图中,有些姿态结点的属性可以通过辅助曲线的当前值来指定。例如: + + - [应用变换结点](../pose-graph/pose-nodes/modify-pose.md#应用变换) 的强度值可使用辅助曲线来指定。 + +- [动画控制器](../../animation-controller.md) 组件提供了方法以读取辅助曲线的当前值。 \ No newline at end of file diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/enabling.md b/versions/4.0/zh/animation/marionette/procedural-animation/enabling.md new file mode 100644 index 0000000000..d581fce8bd --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/enabling.md @@ -0,0 +1,14 @@ +# 启用程序式动画功能 + +程序式动画目前为实验性功能,如要启用,请在 **偏好设置** 中找到 **实验室** 功能并开启下列选项: + +- 动画辅助曲线(**Animation auxiliary curve**) +- 开启姿态图功能(**Enable Pose-Express Function**) + +![enable.png](./index/enable.png) + +在 **项目** 菜单打开 **项目设置** 面板,导航到 **功能剪裁** 分页并启用 **程式化动画**。 + +![cropping.png](index/cropping.png) + +之后重启编辑器或者使用 Ctrl + R 刷新即可。 diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/index.md b/versions/4.0/zh/animation/marionette/procedural-animation/index.md new file mode 100644 index 0000000000..f55926cdc3 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/index.md @@ -0,0 +1,19 @@ +# 程序式动画(实验性) + +> **注意**:程序式动画是实验性功能,请谨慎使用。Cocos Creator 期待你的反馈。 + +Cocos Creator 3.8 开始实验性地支持程序式动画效果,使得开发者有能力实现一些复杂的骨骼动画。 + +## 内容 + +- 在 [程序式动画简介](./introduce.md) 中,介绍了关于程序式动画的起因和基础。 + +- 程序式动画目前作为 Cocos Creator 实验性功能,默认关闭,[启用程序式动画功能](./enabling.md) 中讲述了如何对其进行开启。 + +- [姿态图](./pose-graph/index.md) 中介绍了程序式动画的核心构件——姿态图。 + +- [加性动画](../additive-animation/index.md) 是一种复用动画资产的手段。 + +- 除姿态图外,[辅助曲线](./auxiliary-curve/index.md) 也是实现程序式动画的有效手段。 + +- [姿态暂存](./pose-stash/index.md) 用于实现姿态图的复用。 diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/index/cropping.png b/versions/4.0/zh/animation/marionette/procedural-animation/index/cropping.png new file mode 100644 index 0000000000..8f86942ac7 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/index/cropping.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/index/enable.png b/versions/4.0/zh/animation/marionette/procedural-animation/index/enable.png new file mode 100644 index 0000000000..55cf98a893 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/index/enable.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/introduce.md b/versions/4.0/zh/animation/marionette/procedural-animation/introduce.md new file mode 100644 index 0000000000..b3532603b1 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/introduce.md @@ -0,0 +1,43 @@ +# 程序式动画简介 + +在了解程序化动画之前,我们需要先了解动画中所指的 “姿势(Pose)”,相信下面的图中的姿势您都有见过: + +![T-Pose](introduce/t-pose.png) + +> 图片来自 [https://sketchfab.com](https://sketchfab.com/3d-models/man-in-coat-character-human-riged-model-758a855697be47a1be0d707623e3907e) + +这就是动画制作中的 *绑定姿势*。因为整个人形伸直双手做出和 T 一样的形状,该姿势又称为 T-Pose。 + +> 除了 T-Pose 之外,广泛应用在人形骨骼上的还有 A-Pose。 + +不知道您有没有考虑过为什么会有 T-Pose 这样的 “怪异” 的姿势。 + +最初的原因可能已无法考证,但就现在来说 T-Pose 是最适合动画师制作动画的起始动画,动画师会从 T-Pose 开始制作角色的动画。因此 T-Pose 就这么作为动画工作流的一部分保留了下来。 + +我们可以从一个数学概念上去理解 T-Pose,如果正向 X 轴的起始位置 0,那么动画的起始姿态就是 T-Pose。 + +理解到这一点,我们就可以理解到角色的 “姿态”。我们可以理解为角色在某个时刻所体现出外观,这些外观由它的动画信息内所提供的变换(位置,旋转等)以及蒙皮等要素决定。也就是说当我们将数轴上的“光标”移动到某个时刻,从而得出数轴上当前的值是多少。同理我们在时间轴上移动动画,那么当时间轴停在某个时刻时,角色所处的样子,就称为他的一个姿态。 + +在引擎中,角色的 **姿态**,是指角色的所有骨骼在某一时刻的所有变换(位置、旋转)信息。求值动画图将产生姿态,而后,姿态被写入到场景中,配合蒙皮渲染实现骨骼动画。 + +程序式动画,是指以类似流程图的形式表达姿态的生成和变化。 + +![flow.png](introduce/flow.png) + +如上图就是一个典型的程序式动画。 + +程序式动画和普通的动画差别在于,普通的动画通常来说开发者不能介入动画的采样行为,而程序式动画提供额外的能力可以供开发者在动画采样时,提取某些动画、混合或者姿态的信息于当前的动画进行加权混合等操作。 + +程序化动画系统提供了对动画剪辑、混合以及姿态的操作功能,这些功能可以使得开发者在运行时动态的根据需求来调整动画的采样过程,并产生输出,而输出既可以作为最终的输出输出到屏幕上,也可以作为其他动画采样的输入,从而形成复杂的动画系统。 + +我们以一个简单例子来说明程序化动画。 + +比如角色行走在不平的地面上,通常在制作动画剪辑时并不会考量这一点,因为美术资产的制作者并不清楚角色的脚可能处于什么高度,因此会制作角色平地行走的动画,而在运行时,由于地面的高低起伏,因此需要动态的计算出脚应该所在的高度,并将角色的脚 “移动” 到对应的高度,这里通常是采用逆向动力学(IK,Inverse Kinematic)的算法。 + +使用程序化动画,我们只需在姿势图内添加和 IK 相关的节点,并在运行时将计算 IK 所需要的信息传递给姿态图系统,通过 IK 解算器的运算即可得到正确的结果。 + +可以看到程序化动画通过提供可视化视图,从而使复杂动画的制作更易操作和理解。 + +## 何时需要程序式动画 + +简单来说,当你需要动态修改骨骼,尤其需要根据环境来调整骨骼时,你应该考虑使用程序式动画能力。例如,当人形角色去触碰开关时,你需要将手部骨骼移动至开关位置,并且同时保证手臂做出相应调整。 diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/introduce/flow.png b/versions/4.0/zh/animation/marionette/procedural-animation/introduce/flow.png new file mode 100644 index 0000000000..40ddcb7af1 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/introduce/flow.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/introduce/t-pose.png b/versions/4.0/zh/animation/marionette/procedural-animation/introduce/t-pose.png new file mode 100644 index 0000000000..66cd7278c8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/introduce/t-pose.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/index.md b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/index.md new file mode 100644 index 0000000000..652c8801a3 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/index.md @@ -0,0 +1,79 @@ +# 姿态图(实验性) + +**姿态图** 是表达了姿态的产生和转变的结点图,是实现程序式动画的核心构件。 + +姿态图依附在动画图的其它构件中: + +- 状态机中,每个 [程序式姿态](../../state-machine/procedural-pose-state.md)(状态)包含一张姿态图。 + +- 每个 [姿态暂存](../pose-stash/index.md) 中包含一张姿态图。 + +本节将讲述姿态图的结构。 + +在开始了解程序式动画之前,我们建议您阅读和 [Marionette 动画系统](../../../index.md) 和动画图相关的内容。 + +## 值及类型 + +广泛来说,姿态图描述了值的流动。每个值都有类型,姿态图支持的值的类型如下: + +| 类型 | 含义 | +|----------|------------| +| 姿态对象 | 角色姿态。 | +| 浮点数 | 浮点数值。 | +| 整数 | 整数值。 | +| 三维向量 | 三维向量。 | +| 四元数 | 四元数。 | + +其中,姿态对象无法凭空产生和指定,仅由姿态结点产生(见下文)。 + +## 结点 + +![nodes-type.png](./pose-nodes/img/nodes-type.png) + +(姿态图)**结点** 描述了一项操作。结点上可能存在零或多项 **输入**,也可能存在零或多项 **输出**。有些结点的输入数量是不固定的且可以删除。目前,所有结点的输出数量都是固定的且不可删除。 + +> 姿态图的节点(Pose Node)是姿态图中用于处理程序式动画的节点,并非引擎的节点(Node)。 + +输入和输出都关联着类型。当结点存在某类型的输入时,表示该结点可从该输入处接受该类型的值;当结点存在某类型的输出时,表示该结点将产出该类型的值到该输出处。在求值时,结点将它输入的值作以运算,并产出结果值到输出上。 + +结点输出可以连接到另一结点的 **同类型** 输入,表示求值时,将其输出的值传递给另一结点的作为输入。有时候也称这个行为为 **绑定**。 + +> 注意,这样的连接是单向的——无法将输入连接到输出,这是没有意义的。 + +一个结点的输出可以连接到多个结点的输入上。但存在例外:姿态输出仅可以连接至一个结点。 + +> 若需要在多处引用一个姿态,请考虑使用 [姿态暂存](../pose-stash/index.md)。 + +姿态图中存在以下几类结点: + +| 结点分类 | 含义 | 输入 | 输出 | +|----------------|----------------------------|--------------|----------------------------| +| 姿态图输出结点 | 作为整张姿态图的输出姿态。 | 一个姿态输入 | 无 | +| 姿态结点 | 输出姿态对象。 | 零或多个 | 一个姿态输出 | +| 变量获取结点 | 获取指定的变量并输出 | 无 | 一个输出,类型为变量的类型 | + +您可以参考 [姿态图节点视图](./pose-nodes/node-operation.md) 来查看如何可视化的操作姿态图节点。 + +### 姿态图输出结点 + +![output.png](./pose-nodes/img/output.png) + +**姿态图输出结点** 是一种特殊的结点,代表整张姿态图的输出姿态。它内置于姿态图中,不可删除和创建。 + +姿态图输出结点有且仅有一项姿态输入。当有结点连接到该姿态输入时,输入的姿态成为姿态图的产出姿态。 + +> 若姿态图输出结点未被连接,姿态图产出的姿态为默认姿态。 + +姿态图输出结点没有输出。 + +### 姿态结点 + +**姿态结点** 是指所有输出姿态对象的结点。目前,所有姿态结点都只产生一项姿态输出。但姿态结点可根据结点本身的情况存在零或多项任意类型的输入。 + +[姿态结点](./pose-nodes/index.md) 中列举了所有类型的姿态结点。 + +### 变量获取结点 + +**变量获取结点** 获取指定的动画图变量并输出其值。该类结点没有输入,并只有一项输出,输出的类型即是变量的类型。 + +变量获取结点不可获取触发器变量。 \ No newline at end of file diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md new file mode 100644 index 0000000000..deb28e0945 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md @@ -0,0 +1,70 @@ +# 混合姿态 + +姿态图提供了几种用于混合姿态的结点。 + +> 这些结点的混合都在本地空间中进行。 + +混合姿态可以在姿态图中点击鼠标右键选择 **姿态节点** -> **混合** 来创建。 + +![blend.png](./img/create-blend.png) + +## 混合双姿态 + +![blend-two-pose.png](./img/blend-two-pose.png) + +**混合双姿态结点** 按指定的比例混合两个姿态。 + +| 输入 | 类型 | 含义 | +| ---------- | ------ | ----------------------------------------------------------------------- | +| **姿态 1** | 姿态 | 参与混合的姿态。 | +| **姿态 2** | 姿态 | 参与混合的姿态。 | +| **比例** | 浮点数 | 混合比例。例如,0.2 表示最终姿态中将包含 80% 的姿态 1 和 20% 的姿态 2。 | + +## 混合多姿态 + +![blend-in-proportion.png](./img/blend-in-proportion.png) + +**按占比混合结点** 按每个姿态的占比混合多个姿态。 + +| 输入 | 类型 | 含义 | +| ---------- | ------ | ------------------------------------------------------- | +| **姿态 N** | 姿态 | 参与混合的姿态。 | +| **占比 N** | 浮点数 | 姿态占比。例如,0.2 表示最终姿态中将包含 20% 的姿态 N。 | + +点击右侧的 “+” 号按钮并选择 **poses** 菜单可以添加更多的姿态。 + +![add-blend-in-proportion.png](./img/add-blend-in-proportion.png) + +添加后在输入框内输入比例可以设置该姿态在最终姿态中的占比. + +![blend-in-proportion-ratio.png](./img/blend-in-proportion-ratio.png) + +## 过滤混合 + +![filtering-blend](./img/filtering-blend.png) + +**过滤混合结点** 按指定的比例,将姿态的一部分混合到另一姿态中。 + +![filtering-blend-inspector.png](./img/filtering-blend-inspector.png) + +| 输入 | 类型 | 含义 | +| ---------- | ------ | ----------------------------------------------------------------------- | +| **姿态 1** | 姿态 | 参与混合的姿态。 | +| **姿态 2** | 姿态 | 参与混合的姿态。 | +| **比例** | 浮点数 | 混合比例。例如,0.2 表示最终姿态中将包含 80% 的姿态 1 和 20% 的姿态 2。 | + +| 对象属性 | 含义 | +| -------- | ------------------------------------------------------------------------ | +| **遮罩** | 在混合时使用的动画遮罩。姿态 2 中只有被遮罩限定的部分会混合到姿态 1 中。 | + +## 叠加混合 + +![additively-blend.png](./img/additively-blend.png) + +**叠加混合结点** 按指定的比例,将一个叠加姿态叠加到基础姿态中。 + +| 输入 | 类型 | 含义 | +| ------------ | ------ | --------------------------------------------------------------- | +| **基础姿态** | 姿态 | 基础姿态。 | +| **叠加姿态** | 姿态 | 叠加姿态。 | +| **比例** | 浮点数 | 混合比例。例如,0.2 表示仅有 20% 的叠加姿态会被混合到基础姿态。 | diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/add-blend-in-proportion.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/add-blend-in-proportion.png new file mode 100644 index 0000000000..0754b0cb76 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/add-blend-in-proportion.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/additively-blend.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/additively-blend.png new file mode 100644 index 0000000000..a638e3616b Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/additively-blend.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform-inspector.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform-inspector.png new file mode 100644 index 0000000000..92b66da80d Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform-inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform.png new file mode 100644 index 0000000000..9ebada5067 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/apply-transform.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion-ratio.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion-ratio.png new file mode 100644 index 0000000000..5a65f42a8f Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion-ratio.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion.png new file mode 100644 index 0000000000..e5022cffc5 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-in-proportion.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-two-pose.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-two-pose.png new file mode 100644 index 0000000000..788e74a153 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend-two-pose.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend.png new file mode 100644 index 0000000000..9457f25506 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/blend.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform-inspector.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform-inspector.png new file mode 100644 index 0000000000..7e41773c1c Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform-inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform.png new file mode 100644 index 0000000000..cfaabcdfb9 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/copy-transform.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-blend.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-blend.png new file mode 100644 index 0000000000..cd5b40fee8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-blend.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-link.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-link.png new file mode 100644 index 0000000000..75d4fcc2a0 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-link.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-pose-node.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-pose-node.png new file mode 100644 index 0000000000..1533975040 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/create-pose-node.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/curve.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/curve.png new file mode 100644 index 0000000000..dd45865934 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/curve.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend-inspector.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend-inspector.png new file mode 100644 index 0000000000..70d38b3399 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend-inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend.png new file mode 100644 index 0000000000..4e7318cfac Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/filtering-blend.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/get-value.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/get-value.png new file mode 100644 index 0000000000..478fea4f7c Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/get-value.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/input.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/input.png new file mode 100644 index 0000000000..e53ae6fe1c Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/input.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link-success.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link-success.png new file mode 100644 index 0000000000..4eb6e79b56 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link-success.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link.gif b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link.gif new file mode 100644 index 0000000000..b756c72d8d Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/link.gif differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/menu.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/menu.png new file mode 100644 index 0000000000..79d45267bd Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/menu.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/name.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/name.png new file mode 100644 index 0000000000..2b433ca041 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/name.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/node-options.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/node-options.png new file mode 100644 index 0000000000..f3ff5fd8cc Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/node-options.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/nodes-type.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/nodes-type.png new file mode 100644 index 0000000000..9a46862283 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/nodes-type.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/output.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/output.png new file mode 100644 index 0000000000..65922277e9 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/output.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/play-animation-clip-inspector.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/play-animation-clip-inspector.png new file mode 100644 index 0000000000..954c4b5083 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/play-animation-clip-inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/play-animation-clip-sync.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/play-animation-clip-sync.png new file mode 100644 index 0000000000..25dc0c7ec8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/play-animation-clip-sync.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/play-animation-clip.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/play-animation-clip.png new file mode 100644 index 0000000000..2ba29a2307 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/play-animation-clip.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/remove-link.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/remove-link.png new file mode 100644 index 0000000000..d1aa889788 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/remove-link.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/sample-animation-inspector.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/sample-animation-inspector.png new file mode 100644 index 0000000000..ce4f329ffc Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/sample-animation-inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/sample-animation.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/sample-animation.png new file mode 100644 index 0000000000..499e8912b8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/sample-animation.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/shrink.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/shrink.png new file mode 100644 index 0000000000..400c2f662c Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/shrink.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-inspector.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-inspector.png new file mode 100644 index 0000000000..121f5a8928 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-solver.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-solver.png new file mode 100644 index 0000000000..17b7518ca1 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/img/two-bone-ik-solver.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md new file mode 100644 index 0000000000..1a84bdeb3a --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md @@ -0,0 +1,26 @@ +# 姿态结点 + +动画图中可以创建姿态图并对姿态图进行编辑,编辑后的结果将作为动画图资产的一部分存储在动画图内部。 +您可以参考动画图的用法来创建、删除和修改姿态图。 + +本章我们将向您介绍姿态图的概念以及用法。 + +姿态结点输出姿态。引擎提供了以下作用的姿态结点: + +- [播放或采样动画](./play-or-sample-motion.md) + +- [混合姿态](./blend-poses.md) + +- [选择姿态](./choose-pose.md) + +- [修改姿态](./modify-pose.md) + +- [状态机](./state-machine-node.md) + +- [使用暂存姿态](./use-stashed-pose.md) + +可视化姿态图节点编辑器器的内容请参考 [姿态图节点视图](./node-operation.md)。 + +## 内容 + +- [姿态图节点操作](./node-operation.md) diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md new file mode 100644 index 0000000000..0f6f8d262d --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md @@ -0,0 +1,116 @@ +# 修改姿态 + +姿态图提供了几种用于修改姿态的结点。这些结点都接受一个输入姿态,并输出修改后的姿态。 + +|输入|类型|含义| +|--|--|--| +|`姿态`|姿态|要修改的姿态。| + +姿态节点的操作请参考 [姿态图节点视图](./node-operation.md)。 + +## 应用变换 + +![apply-transform.png](./img/apply-transform.png) + +**应用变换结点** 应用变换(位移和旋转)到输入姿态的指定结点。 + +|输入|类型|含义| +|--|--|--| +|`位置`|三维向量|对位置的修改量。| +|`旋转`|四元数|对旋转的修改量。| +|`强度值`|浮点数|修改的应用强度,在 [0, 1] 范围内。0 表示完全不应用修改,1 表示完全应用修改。| + +![apply-transform-inspector.png](./img/apply-transform-inspector.png) + +|对象属性|含义| +|--|--| +|`结点`|要修改的结点的名称。| +|`位置操作`| 指定如何修改位置。见 [变换操作选项](#变换操作选项) | +|`旋转操作`|指定如何修改旋转。见 [变换操作选项](#变换操作选项)| +|`空间`|修改发生的空间;也即指定的 `位置` 输入 和 `旋转`输入所处的空间。 | + +### 变换操作选项 + +变换操作选项指定了应用变换结点如何修改位置或旋转。选项如下: + +|选项|含义| +|--|--| +|LEAVE_UNCHANGED|不修改。| +|REPLACE|作为位置的应用选项时,将 `位置` 输入直接用作结点的新位置;作为旋转的应用选项时,将 `旋转` 输入直接用作结点的新旋转。| +|ADD|作为位置的应用选项时,将 `位置` 输入加到结点目前的位置上;作为旋转的应用选项时,将 `旋转` 输入加到结点目前的旋转上。| + +## 拷贝变换 + +![copy-transform.png](./img/copy-transform.png) + +**拷贝变换结点** 拷贝输入姿态中一个结点的变换至另一结点上。 + +![copy-transform-inspector.png](./img/copy-transform-inspector.png) + +|对象属性|含义| +|--|--| +|`源结点`|要拷贝的结点的名称。| +|`目标结点`|拷贝到的结点的名称。| +|`空间`|指定拷贝发生的空间。当为 `COMPONENT` 时表示在组件空间中进行拷贝;当为 `LOCAL` 时表示在本地空间中进行拷贝。| + +## 设置辅助曲线 + +![curve.png](./img/curve.png) + +**设置辅助曲线结点** 修改输入姿势中指定辅助曲线的当前值。 + +|输入|类型|含义| +|--|--|--| +|`曲线值`|浮点值|对辅助曲线的修改量。| + +![inspector-property.png](../../auxiliary-curve/img/inspector-property.png) + +|对象属性|含义| +|--|--| +|`曲线名`|要修改的辅助曲线的名称。| +|`标志`|指定如何修改辅助曲线。当为 `LEAVE_UNCHANGED` 时不修改;当为 `REPLACE` 时表示将 `曲线值` 输入作为辅助曲线的当前值;当为 `ADD` 时表示将 `曲线值` 输入加到辅助曲线的当前值上。| + +## 双骨骼 IK + +![two-bone-ik-solver.png](./img/two-bone-ik-solver.png) + +IK 解算往往用于移动或旋转骨骼到目标位置,并且带动父级骨骼,使之保持与父级骨骼之间的距离不变(或在可接受的范围内)。例如,在移动脚时,膝盖骨骼和大腿骨骼应该相应弯曲。 + +**双骨骼 IK 解算器结点** 为输入姿势解算两段(三根)骨骼构成的 IK 问题。这三根骨骼构成直接父子关系,即 “子级 - 父级 - 父级的父级”。其中,“子级” 称为 **末端执行器** 或 **末端骨骼**,“父级的父级” 称为 (在此 IK 问题中的)**根骨骼**,“父级” 称为 **中间骨骼**。 + +> 因此,“双骨骼 IK” 中的 “双” 应该理解为 “两段” 而非 “两根”。 + +求值时,解算器将变换末端骨骼和中间骨骼,并旋转根骨骼,以使末端骨骼到达指定位置;并且在此过程中,保持父子级之间的距离不变、保持根骨骼的位置不变。 + +![Two Bone IK Demonstration](./two-bone-ik-demonstration.gif) + +对于固定的末端骨骼和根骨骼位置,在解算时,中间骨骼存在无穷多个解。如图: + +![Pole Target Demonstration](./pole-target-demonstration.gif) + +解算器会尝试找到“最佳”的解,但这样的解可能仍然不止一个,且可能并不是期望的。 + +因此,为了明确指定中间骨骼的方位,解算器还允许指定一个 **极向目标** 来确定骨骼链的弯曲方向。 + +![two-bone-ik-inspector.png](./img/two-bone-ik-inspector.png) + +|输入|类型|含义| +|--|--|--| +|`末端执行器目标`|三维向量|末端执行器的目标位置。| +|`极向目标`|三维向量|极向目标的位置。| + +|对象属性|含义| +|--|--| +|`末端执行器结点`|末端执行器结点的名称。| +|`末端执行器目标`|末端执行器的目标设置。| +|`极向目标`|极向目标设置。| + +### 目标设置 + +目标设置用于描述末端执行器目标或极向目标。选项如下: + +|对象属性|含义| +|--|--| +|`类型`|目标类型。当为 `VALUE` 时,表示以指定(三维向量)值的形式表示目标位置;当为 `BONE` 时,表示以另一骨骼的位置作为目标位置;当为 `NONE` 时,表示以当前位置作为目标位置,也即不对该目标进行解算。| +|`目标骨骼`| 当目标类型为 `BONE` 时,目标骨骼的名称。| +|`目标位置空间`| 当目标类型为 `VALUE` 时,目标位置的空间。| diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md new file mode 100644 index 0000000000..21d6004238 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md @@ -0,0 +1,95 @@ +# 姿态图节点视图 + +## 创建节点 + +创建节点前需要先在动画图中创建一个姿势图,之后通过鼠标双击进入到姿势图的可视化编辑界面内。 + +在姿势图内点击鼠标右键,在弹出菜单中的姿势图节点(Post Nodes)单击鼠标既可以创建姿势图节点。 + +![create-pose-node.png](./img/create-pose-node.png) + +| 菜单 | 说明 | +| :-- | :-- | +| **播放动画** | 请参考 [播放或采样动画](./play-or-sample-motion.md) +| **状态机** | 请参考 [状态机](./state-machine.md) +| **采样动画** |请参考 [播放或采样动画](./play-or-sample-motion.md) +| **混合** | 请参考[混合姿态](./blend-poses.md) +| **选择** | 开发者可以通过不同的类型的数据对姿势和动画进行选择,包含布尔值和索引 | +| **应用变换** | 请参考 [应用变换](./modify-pose.md#%E5%BA%94%E7%94%A8%E5%8F%98%E6%8D%A2)| +| **拷贝变换** | 请参考 [拷贝变换](./modify-pose.md#%E6%8B%B7%E8%B4%9D%E5%8F%98%E6%8D%A2)| +| **设置辅助曲线** | 请参考 [辅助曲线](../../auxiliary-curve/index.md)| +| **反向动力学** | 通过反向动力学验算器计算反向计算骨骼,目前支持 [双骨骼 IK](./modify-pose.md#%E5%8F%8C%E9%AA%A8%E9%AA%BC-ik) | + +创建完成后,将在姿态图中以不同颜色的矩形框显示 + +![nodes-type.png](./img/nodes-type.png) + +我们以 **混合** 节点为例说明节点的结构,其他类型的节点可以参考。 + +![blend.png](./img/blend.png) + +可以看到节点有几个部分构成: + +- 类型:在最上方显示的节点的类型 +- 展开/收起:点击部分节点左上方的箭头符号可以展开/收起节点以保持更清爽的视图 + + ![shrink.png](./img/shrink.png) + +- 输入:通常在节点的左侧,表示节点的输入,以不同颜色空心(未连接)/实心(已连接)的菱形表示,可以将其他节点的输出作为输入,部分节点也可以直接在姿态图内输入在右侧 **属性检查器** 面板内输入。不同的节点类型输入不同,详情请参考不同类型的节点文档。 + + ![input.png](./img/input.png) + + 部分节点类型如变量节点,没有输入只有输出。 + +- 输出:输出部分通常在节点的右侧,以空心(未连接)/实心(已连接)的菱形表示,可以通过鼠标左键按下并拖拽形成连接。 + + ![output.png](./img/output.png) + + 通常来说节点的输入只能使用在另一个节点的输入,并且只能使用一次,这意味加入该输出已被使用,那么再次通过该输入连接到其他节点时会断开之前的连接 + +## 节点操作 + +在任意节点上点击鼠标右键会弹出节点操作菜单。 + +![node-options.png](./img/node-options.png) + +- 复制此节点:复制操作会将当前选中的节点复制以供他用 +- 克隆此节点:在当前姿势图内克隆选中的节点 +- 删除此节点:会将选中的节点从姿势图中删除 + +## 菜单 + +在姿势图的空白内点击鼠标右键会弹出姿势图的菜单。 + +![menu.png](./img/menu.png) + +- 删帖姿态图节点:将通过上面复制此节点操作的节点复制道此处 +- 姿态节点:创建新的姿态节点,请参考上文 **创建节点** 部分 +- 获取变量:此菜单可以获取到动画图内创建的变量,在动画图内配置的非触发器变量都可以在此处获取 + + ![get-value.png](img/get-value.png) + +- 暂存此图:会将当前的姿态图转储为新的姿态图 +- 返回中心视角:视图将自动以合适的视角返回到中心视角 + +## 创建连接 + +![create-link.png](./img/create-link.png) + +连接意味着将一个节点的输出,作为输入参数传递给另一个节点。 + +对于不同节点间,可以通过拖拽节点的输出去连接另一个节点的输入,当然前提是另一个节点接受该输出类型。 + +通过拖拽节点输出端,到另一个节点的输入端。 + +![link.gif](./img/link.gif) + +链接成功后,节点上会以实心的菱形表示: + +![link-success.png](./img/link-success.png) + +## 删除连接 + +在已连接的线上点击鼠标右键可以删除连接,选中的连线的周边会显示蓝色的高亮泛光。 + +![remove-link.png](./img/remove-link.png) \ No newline at end of file diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md new file mode 100644 index 0000000000..f6149a0e5e --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md @@ -0,0 +1,57 @@ + +# 播放或采样动画 + +姿态图提供了两种从动画中读取姿态的结点:播放动画结点和采样动画结点。 + +在下文,“动作”是指动画剪辑或动画混合。 + +## 播放动画 + +![play-animation-clip.png](./img/play-animation-clip.png) + +**播放动画结点** 播放指定动作,每一帧对其进行更新,并获取动画当帧的姿态作为输出。 + +| 输入 | 含义 | +|------------|--------------------------------------------------| +| `起始时间` | 每当重入此结点时,从何时开始播放动作。单位为秒。 | +| `速度乘数` | 动作的播放速率。 | + +![play-animation-clip-inspector](./img/play-animation-clip-inspector.png) + +| 对象属性 | 含义 | +|----------|----------------------| +| `动作` | 要采样的动作。 | +| `同步` | 动画同步选项。见下。 | + +### 同步 + +在有些情形下,我们可能希望多个动作的播放是同步的。所谓同步,即是指当多个动画都需要播放时,它们在某些时间点上是对齐的。例如,当同时混合走、跑动作时,角色的步态应该一致——两个动作的四肢应该有相同的动画趋势。 + +动画图目前仅支持时间上的同步。也即,被同步的动作永远会处在相同的百分比进度。 + +如果仅使用了状态机中的动画混合,那么无需考虑同步。因为动画混合中的每一项动作都是同步的——它们会一直处于相同的百分比进度。然而在姿态图中情况变得不大一样。 + +想象两个动作 _A_、_B_,我们希望它们总是同步播放的。但如果 _A_ 和 _B_ 分别处于状态机的两个状态中。当 _A_ 播放了一段时间后,_B_ 所属状态才进入,然后 _B_ 开始从 0 播放。这就导致了它们时间上的不同步。 + +为了解决这个问题,我们可以将需要同步的动作指定为为同一个 **同步组**。而后,同一同步组中的所有动作都将得到同步。具体地,**播放动作结点** 的 `同步` 属性下,有如下选项: + +| 对象属性 | 含义 | +|----------|--------------------| +| `组` | 动作所属的同步组。 | + +## 采样动画 + +![sample-animation.png](./img/sample-animation.png) + +**采样动画结点** 采样指定动作某一时刻的姿态作为输出。 + +|输入|含义| +|--|--| +|`时刻`|要采样的时刻。| + +![sample-animation-inspector.png](./img/sample-animation-inspector.png) + +|对象属性|含义| +|--|--| +|`动作`|要采样的动作。| +|`使用标准化时间`|结点输入`时刻`是否指定的是标准化时间。标准化时间是指在 [0, 1] 范围内的动作进度。例如 1 表示动作的最后一帧, 0.5 表示 50% 的动作进度。| diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/pole-target-demonstration.gif b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/pole-target-demonstration.gif new file mode 100644 index 0000000000..d091cacf95 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/pole-target-demonstration.gif differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/state-machine-node.md b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/state-machine-node.md new file mode 100644 index 0000000000..bb97fed34d --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/state-machine-node.md @@ -0,0 +1,8 @@ +# 状态机结点 + +**状态机结点** 包含一个状态机。状态机每一时刻产生的姿态就作为该结点的姿态。 + + + +双击状态机结点可进入状态机的编辑。 + diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/two-bone-ik-demonstration.gif b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/two-bone-ik-demonstration.gif new file mode 100644 index 0000000000..7ad832ade5 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-graph/pose-nodes/two-bone-ik-demonstration.gif differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/delete.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/delete.png new file mode 100644 index 0000000000..a73a3a6dfd Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/delete.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/edit.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/edit.png new file mode 100644 index 0000000000..a2dbd18aec Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/edit.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/index.md b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/index.md new file mode 100644 index 0000000000..92ff9fbcb2 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/index.md @@ -0,0 +1,63 @@ +# 姿态暂存 + +姿态图中无法将一个姿态结点的输出连接至多个结点,当你试图连接姿态结点至新的结点时,旧的连接将被断开。若需要在多处复用一个姿态,需要用到姿态暂存。 + +## 概念 + +**姿态暂存** 是指将一张姿态图的结果存储起来,并在其它姿态图中使用该图产生的姿态的机制。**姿态暂存对象**(下称姿态暂存或暂存)是实现这样机制的手段,它关联了一张姿态图。 + +> 一个形象但不完全的理解是将姿态暂存视为保存了姿态对象的缓存变量。 + +姿态暂存归属于动画图层级。每个层级都可以创建多项暂存,创建后,可以在该层级中的任何姿态图中通过 **使用暂存姿态** 结点来引用该暂存。 + +> 我们可以理解为将某些已经编辑好的姿态节点,保存为某种蓝图或者资源,以供在其他地方使用。只是这个蓝图有限定范围,只能在此动画图中的特定层级中。 +> 也就意味着我们可以将暂存当做某些特殊的姿态节点,和普通的姿态节点一样使用。 + +姿态暂存允许被引用多次,其所关联的姿态图每帧仅会被更新、求值一次。 + +## 编辑姿态暂存 + +在选定动画图层级的 **属性检查器** 上可以查看、创建、编辑该层级的所有姿态暂存。 + +点击动画图中的层级,在 **属性检查器** 中找到 **姿势暂存(Layer Stashes)** 属性。 + +![Alt text](layer-stashes.png) + +点击 ![edit.png](edit.png) 按钮开始编辑选定暂存所关联的姿态图。点击 ![delete.png](delete.png) 删除暂存。删除后不可恢复,请谨慎操作。 + +下方的本文信息会说明该暂存的引用次数,如果大于 0 则表示该暂存已被引用。 + +点击 **创建暂存** 按钮可以创建新的暂存。 + +## 使用暂存的姿态 + +在任意姿态图中,创建结点的菜单里列举了所有可以使用的暂存。点选后将创建 **使用暂存姿态** 结点。 + +![Alt text](use-stash-node-list.png) + +![Alt text](use-stash-node.png) + +**使用暂存姿态** 结点输出指定暂存所产生的姿态;该结点没有额外输入。 + +双击 **使用暂存姿态** 结点,可进入该结点所关联的暂存的编辑界面。 + +## 暂存此图 + +很多时候并不是设计之初就能想到要暂存哪些姿态;更多的情况是在已经开发了一张图,才发现需要将这张图复用在多处。 +为此,可在任意姿态图中单击右键进入上下文菜单,并单击执行菜单项 **暂存此图(Stash This Graph)**: + +![Alt text](menu-item-stash-this-pose.png) + +执行后,当前图中原本的内容将被移动到一项新建的姿态暂存中,并且会在当前图中生成一个 **使用暂存姿态** 结点来引用新建的姿态暂存。 + +点击 **暂存此姿态**,会创建出新的姿态暂存。 + +这里以播放动画剪辑的姿态为例。 + +![original-graph.png](./original-graph.png) + +点击 **暂存此姿态** 菜单,此时可通过下图观察到变化: + +![stash2.png](./stash2.png) + +可以观察到之前的姿态节点 **播放动画剪辑** 节点已不存在,取而代之的是名为 **Use Stash StashX**(X 为编号,暂存的名称可以在**属性检查器**内修改) 的节点。该节点即为新建的 **姿态暂存**。 diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/layer-stashes.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/layer-stashes.png new file mode 100644 index 0000000000..77d0fa5281 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/layer-stashes.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/menu-item-stash-this-pose.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/menu-item-stash-this-pose.png new file mode 100644 index 0000000000..6e66e347ad Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/menu-item-stash-this-pose.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/original-graph.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/original-graph.png new file mode 100644 index 0000000000..5bddbfe35c Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/original-graph.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/stash2.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/stash2.png new file mode 100644 index 0000000000..951c84e220 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/stash2.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/use-stash-node-list.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/use-stash-node-list.png new file mode 100644 index 0000000000..933fa117e7 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/use-stash-node-list.png differ diff --git a/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/use-stash-node.png b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/use-stash-node.png new file mode 100644 index 0000000000..0c6277f206 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/procedural-animation/pose-stash/use-stash-node.png differ diff --git a/versions/4.0/zh/animation/marionette/state-machine/procedural-pose-state.md b/versions/4.0/zh/animation/marionette/state-machine/procedural-pose-state.md new file mode 100644 index 0000000000..2ce75243a5 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/state-machine/procedural-pose-state.md @@ -0,0 +1,5 @@ +# 程序式姿态(状态) + +![state-machine.png](./state-machine.png) + +**程序式姿态**(状态) 是状态机中的一种状态。该状态持有一张姿态图,当状态机运行到此状态时,其产生的姿态就是姿态图的输出姿态。 diff --git a/versions/4.0/zh/animation/marionette/state-machine/state-machine.png b/versions/4.0/zh/animation/marionette/state-machine/state-machine.png new file mode 100644 index 0000000000..49b159e94e Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-machine/state-machine.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition.md b/versions/4.0/zh/animation/marionette/state-transition.md new file mode 100644 index 0000000000..d0183235e2 --- /dev/null +++ b/versions/4.0/zh/animation/marionette/state-transition.md @@ -0,0 +1,157 @@ +# 状态过渡 + +**过渡** 代表着两个状态之间的转换,根据过渡发生源头的不同,我们可以将其分为: + +- **普通过渡**:过渡发生的源头为伪状态 **入口** 或者 **子状态机**。 + + ![transition](state-transition/transition.png) + +- **动画过渡**:过渡发生的源头为 **状态** 或者伪状态 **任意**。与普通过渡相比,动画过渡还可以控制过渡周期使得动画进行平滑切换。 + + ![animation-transition](state-transition/animation-transition.png) + +## 创建过渡 + +在网格布局区域中右键选中作为过渡源头的状态,然后选择 **添加过渡**,当鼠标离开当前状态,会产生一条箭头,此时选中作为过渡目标的状态,即可在两者之间产生过渡: + +![add-transition](state-transition/add-transition.gif) + +两个状态之间允许存在多个同向/反向过渡,有几个同向/反向过渡便会在箭头上标相应的数字,配置项也都会展示在 **属性查看器** 中。
+当状态上存在多条同时满足的过渡时,将优先使用排在过渡列表上方的过渡。但 Creator 并不建议依赖这种优先级排序,最好尽可能地使用下文中介绍的 **过渡条件** 来过渡。 + +![add-transition](state-transition/add-more-transitions.png) + +状态也允许过渡到自身: + +![add-transition](state-transition/add-transition-for-self.png) + +## 设置过渡 + +在网格布局区域选中过渡后,**属性检查器** 可设置过渡的相关触发条件,包括 **周期**、**使用结束次数**、**结束次数**、**条件**。 + +> **注意**:**普通过渡** 仅能指定 **条件**,普通过渡更像是一个选择器。 + +![condition-properties](state-transition/condition-properties.png) + +- **周期**:设置完成当前过渡的周期,使动画平滑切换。单位为 **秒** 或者 **遍**,默认为 0.3 秒,可点击输入框后面的单位进行切换。 + + ![change duration](state-transition/change-duration.gif) + + - 当设置为 **秒**(默认)时:表示状态过渡在设定的秒数内完成 + + - 当设置为 **遍** 时:表示状态过渡的周期是基于过渡源头状态的播放次数。例如过渡源头状态的动画时长为 3 s,当 **周期** 设置为 0.9 遍时,表示过渡周期为 0.9 ✖️ 3s = 2.7s。 + +- **使用结束次数**:表示是否将过渡源头状态结束播放作为过渡条件之一。若勾选,过渡的发生需要满足下方设置的 **结束次数** 条件。播放次数作为一种特殊的条件,当启用播放次数后,仅当播放次数条件和过渡条件都满足时,过渡才会发生。 + +- **结束次数**:设置过渡源头状态在开始过渡之前的动画播放总时长(总时长 = 结束次数 ✖️ 动画时长),默认为 1 次,仅在勾选 **使用结束次数** 时生效。需要注意的是动画若是不循环动画,会在第一次播放完成后停止,等待剩余次数播放完成后开始过渡。若需要根据次数自动循环播放动画,请事先在骨骼动画资源中设置好循环模式。例如: + + - 若过渡源头的动画是 **不循环动画**,播放时长为 3 s,**结束次数** 设置为 1.4,那么过渡源头的动画会在播放 3s 后,继续在最后一帧停留 1.2s(3s × 0.4),才开始过渡。 + + ![non-loop](state-transition/non-loop.png) + + - 若过渡源头的动画是 **循环动画**,播放时长为 3 s,**结束次数** 设置为 1.4,那么过渡源头的动画会在播放 3s 后,继续循环播放动画到 1.2s(3s × 0.4),才开始过渡。 + + ![loop](state-transition/loop.png) + +- **终点起始时间**:指定当过渡发生时,终点状态的动画会从哪里开始播放。可以以秒为单位指定,也可以以相对单位指定。当使用相对单位时,该时间是相对于终点状态的周期的。例如,在过渡终点周期为 2 秒的情况下,若指定为 0.5,则表示终点状态的动画从 50% (即 1 秒处)开始播放。 + +- **条件**:设置状态之间发生过渡时需要满足的某些条件,具体内容请参考下文说明。 + +在不使用其他过渡条件的情况下,若希望过渡源头状态的动画在快播放完一遍时便逐渐切换到其他状态,并且在切换完成时源头状态的动画刚好完成播放,那么我们可以通过设置 **周期** 和 **结束次数** 来实现,推荐将 **周期** 的单位设置为 **遍**,然后令 **周期** 和 **结束次数** 之和为 **1** 即可。 + +例如,过渡源头状态的动画时长为 3 秒,将 **周期** 设置为 0.3 遍,则 **结束次数** 设置为 0.7 即可;若 **周期** 设置为 0.3 秒相对比较麻烦,**结束次数** 需要将秒换算为遍:(3 - 0.3)/ 3 = 0.9 遍。 + +![set example](state-transition/set-example.png) + +### 过渡条件 + +有些状态之间的过渡需要满足某些条件时才能触发,这样的条件被称为 **过渡条件**,简称 **条件**。若过渡没有设置条件,则会直接触发。条件可在动画图面板的 [变量](animation-graph-panel.md) 分页进行创建。 + +在网格布局区域选中过渡后,即可点击 **属性查看器** 上 **添加条件** 栏后面的 **+** 号键,通过下拉菜单选择不同的过渡条件。 + +![add-condition](state-transition/add-condition.png) + +目前支持的过渡条件包括以下三种: + +- **布尔条件**:判断布尔类型的变量为真/假。点击右侧的齿轮图标按钮可删除当前条件。 + + ![布尔条件](state-transition/boolean-condition.png "布尔条件") + +- **数值条件**:判断数值类型的变量与另一固定数值的逻辑关系,包括 **等于**、**不等于**、**大于**、**小于**、**大于等于**、**小于等于**。当等式(不等式)成立时条件满足。变量的类型与数值的类型是匹配的,若变量选择浮点型,则数值类型也是浮点型。 + + ![数值条件](state-transition/number-condition.png "数值条件") + +- **触发条件**:当触发器变量触发时条件满足。 + + ![触发器条件](state-transition/trigger-condition.png "触发器条件") + +**过渡支持同时指定多个过渡条件,当且仅当所有条件都满足时过渡才会发生。** + +### 无条件过渡 + +对于 **普通过渡** 来说,允许不指定任何过渡条件,例如,很多时候 **入口** 需要无条件地过渡到其它的状态。 + +但对于 **动画过渡** 来说,既未指定 **条件**,也未指定 **结束次数** 条件的过渡是没有意义的,Creator 将直接忽略该过渡。 + +## 一种边缘情况 + +有些情况下会导致状态机停留在 **入口** 或者 **出口** 上,例如: + +- 过渡到了子状态机的 **入口**,但在子状态机中并没有能满足条件的过渡。 + +- 过渡到了子状态机的 **出口**,但在父状态机中,并没有能满足该子状态机的过渡。 + +我们把这种情况称为 **状态机悬停**。 + +当发生状态机悬停时,会直接中断更新,直到后续能过渡至状态,此时的表现为动画被暂停。 + +> **注意**:我们不建议依赖这种行为,并且后续可能会对这种行为做出调整。 + +## 预览 + +选中任意转移后,在 **属性检查器** 内可以对当前转移进行预览。 + +![preview](state-transition/preview-overview.png) + +预览时,用户可以通过下列按钮进行操作: + +![bar](state-transition/preview-bar.png) + +其属性与描述如下: + +- ![start](preview-bar/start.png) 跳转至第一帧 +- ![play](preview-bar/prev.png) 预览前一帧 +- ![play](preview-bar/play.png)/![pause](preview-bar/pause.png) 开始/暂停播放 +- ![stop](preview-bar/stop.png) 停止播放 +- ![end](preview-bar/next.png) 跳转至后一帧 +- ![end](preview-bar/end.png) 跳转至最后一帧 +- ![time](preview-bar/time.png) 显示当前的时间 + +也可以通过进度条进行快速预览和调整转移的时长: + +![progress](state-transition/progress.png) + +- 在 ① 号处点击或拖拽拉杆可快速预览转移 +- 在 ② 号通过鼠标拖拽边缘可以调整 **周期** + + ![duration](state-transition/duration.png) + +- 在 ③ 号处通过鼠标左键拖拽可以调整转移的 **终点起始时间** + + ![dst-start-time](state-transition/dest-start-time.png) + +点击播放后,既可在预览视图进行预览: + +![preview-view](state-transition/preview-view.png) + +## 多过渡 + +自 3.8.0 开始,状态机开始支持多过渡。 + +> 在此前版本中,相当于仅允许一支过渡的存在。 + +> [过渡中断](https://docs.cocos.com/creator/3.7/manual/zh/animation/marionette/state-transition.html#%E8%BF%87%E6%B8%A1%E4%B8%AD%E6%96%AD) 是在 3.6.0 加入的实验性功能。现已移除,其功能由多过渡机制代替。 + +**多过渡** 是指当状态机正在进行状态过渡时,由于条件满足,当前的目标状态又需要过渡至其它状态;此时,新的过渡加入,并与已有的过渡形成过渡队列。 + +在多过渡过程中,所有过渡将 **同时** 进行。当某支过渡完成后,该过渡将被移出队列,其前方的过渡也将因此被中断并移出队列。 diff --git a/versions/4.0/zh/animation/marionette/state-transition/add-condition.png b/versions/4.0/zh/animation/marionette/state-transition/add-condition.png new file mode 100644 index 0000000000..b11da6ccad Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/add-condition.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/add-more-transitions.png b/versions/4.0/zh/animation/marionette/state-transition/add-more-transitions.png new file mode 100644 index 0000000000..2ba7d07021 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/add-more-transitions.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/add-transition-for-self.png b/versions/4.0/zh/animation/marionette/state-transition/add-transition-for-self.png new file mode 100644 index 0000000000..04f6c27b26 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/add-transition-for-self.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/add-transition.gif b/versions/4.0/zh/animation/marionette/state-transition/add-transition.gif new file mode 100644 index 0000000000..fedbf66b82 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/add-transition.gif differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/animation-transition.png b/versions/4.0/zh/animation/marionette/state-transition/animation-transition.png new file mode 100644 index 0000000000..7cd34c1c3f Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/animation-transition.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/boolean-condition.png b/versions/4.0/zh/animation/marionette/state-transition/boolean-condition.png new file mode 100644 index 0000000000..190762062e Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/boolean-condition.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/change-duration.gif b/versions/4.0/zh/animation/marionette/state-transition/change-duration.gif new file mode 100644 index 0000000000..7828cdabc1 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/change-duration.gif differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/condition-properties.png b/versions/4.0/zh/animation/marionette/state-transition/condition-properties.png new file mode 100644 index 0000000000..0476987b32 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/condition-properties.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/dest-start-time.png b/versions/4.0/zh/animation/marionette/state-transition/dest-start-time.png new file mode 100644 index 0000000000..9b4475c87e Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/dest-start-time.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/duration.png b/versions/4.0/zh/animation/marionette/state-transition/duration.png new file mode 100644 index 0000000000..de3e9f31bf Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/duration.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/interruptible-inspector.png b/versions/4.0/zh/animation/marionette/state-transition/interruptible-inspector.png new file mode 100644 index 0000000000..55d1390fc4 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/interruptible-inspector.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/interruptible-laboratory.png b/versions/4.0/zh/animation/marionette/state-transition/interruptible-laboratory.png new file mode 100644 index 0000000000..bf9eb563c8 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/interruptible-laboratory.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/interruption-demo.gif b/versions/4.0/zh/animation/marionette/state-transition/interruption-demo.gif new file mode 100644 index 0000000000..13bcc90e80 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/interruption-demo.gif differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/loop.png b/versions/4.0/zh/animation/marionette/state-transition/loop.png new file mode 100644 index 0000000000..7c921635e5 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/loop.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/non-loop.png b/versions/4.0/zh/animation/marionette/state-transition/non-loop.png new file mode 100644 index 0000000000..a47c85f0f2 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/non-loop.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/number-condition.png b/versions/4.0/zh/animation/marionette/state-transition/number-condition.png new file mode 100644 index 0000000000..9186066424 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/number-condition.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/preview-bar.png b/versions/4.0/zh/animation/marionette/state-transition/preview-bar.png new file mode 100644 index 0000000000..d0bbbd8286 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/preview-bar.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/preview-overview.png b/versions/4.0/zh/animation/marionette/state-transition/preview-overview.png new file mode 100644 index 0000000000..1323f03cbf Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/preview-overview.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/preview-view.png b/versions/4.0/zh/animation/marionette/state-transition/preview-view.png new file mode 100644 index 0000000000..fdecb0b2f3 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/preview-view.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/progress.png b/versions/4.0/zh/animation/marionette/state-transition/progress.png new file mode 100644 index 0000000000..865bd66531 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/progress.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/set-example.png b/versions/4.0/zh/animation/marionette/state-transition/set-example.png new file mode 100644 index 0000000000..2a3f063e60 Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/set-example.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/transition.png b/versions/4.0/zh/animation/marionette/state-transition/transition.png new file mode 100644 index 0000000000..32b89b075c Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/transition.png differ diff --git a/versions/4.0/zh/animation/marionette/state-transition/trigger-condition.png b/versions/4.0/zh/animation/marionette/state-transition/trigger-condition.png new file mode 100644 index 0000000000..1a8ee9073a Binary files /dev/null and b/versions/4.0/zh/animation/marionette/state-transition/trigger-condition.png differ diff --git a/versions/4.0/zh/animation/morph.md b/versions/4.0/zh/animation/morph.md new file mode 100644 index 0000000000..8ecb39bb6d --- /dev/null +++ b/versions/4.0/zh/animation/morph.md @@ -0,0 +1,49 @@ +# 变形动画(Morph) + +![result](./morph/morph.gif)[^1] + +变形动画是一种通过对模型顶点进行变形,从而实现动画效果的技术。通常在处理角色面部表情变化时,会使用变形动画。 + +## 资源导入 + +通常变形动画由外部工具(3DMax、Blender、Maya 等)制作,并导入引擎使用。导入方式请参考 [模型资源 - 模型导入](../asset/model/mesh.md#%E6%A8%A1%E5%9E%8B%E5%AF%BC%E5%85%A5)。 + +![import](./morph/import.png) + +## 接口和说明 + +如要使用变形动画,请确保导入的资源拥有中变形动画数据。这些数据存储在网格数据中,可通过下列代码获取: + +```ts +const meshRenderer = this.node.getComponent(MeshRenderer)!; +const morph = meshRenderer.mesh.struct.morph; +``` + +如果要改变变形动画的状态,可通过 [setWeight](%__APIDOC__%/zh/class/MeshRenderer?id=setWeight) 可以修改动画的权重。代码示例如下: + +```ts +const meshRenderer = this.node.getComponent(MeshRenderer); +meshRenderer.setWeight(weight, subMeshIndex, shapIndex); +``` + +其接口和参数说明如下: + +| 参数 | 类型 | 说明 | +| :-- | :-- | :-- | +| weight | number | 变形的权重 +| subMeshIndex | number | 子网格的索引 | +| shapIndex | number | 子网格的变形索引 | + +通过 [getWeight](%__APIDOC__%/zh/class/MeshRenderer?id=getWeight) 可以获取外形的权重,以及通过 [setWeights](__APIDOC__/zh/class/MeshRenderer?id=setWeights) 可以同时改变多个子网格的权重。 + +更多接口说明请参考 [MeshRenderer](%__APIDOC__%/zh/class/MeshRenderer?id=setWeight)。 + +## 着色器 + +当检测到网格资源内持有变形数据时,引擎会重设着色器 `CC_USE_MORPH` 的值为 `true`。如需自定义着色器,请确保着色持有 `CC_USE_MORPH` 预编译宏定义。 + +## 程序化使用范例 + +使用方式可参考范例 **Morph**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/animation/morph) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/animation/morph))。 + +[^1]: 模型来自 [https://sketchfab.com/](https://sketchfab.com/3d-models/head-expressions-86e58ee1bda5406480e30c796e1a2df4) diff --git a/versions/4.0/zh/animation/morph/import.png b/versions/4.0/zh/animation/morph/import.png new file mode 100644 index 0000000000..6ddf323f77 Binary files /dev/null and b/versions/4.0/zh/animation/morph/import.png differ diff --git a/versions/4.0/zh/animation/morph/morph.gif b/versions/4.0/zh/animation/morph/morph.gif new file mode 100644 index 0000000000..caf217fa7a Binary files /dev/null and b/versions/4.0/zh/animation/morph/morph.gif differ diff --git a/versions/4.0/zh/animation/skeletal-animation.md b/versions/4.0/zh/animation/skeletal-animation.md new file mode 100644 index 0000000000..f331bdc199 --- /dev/null +++ b/versions/4.0/zh/animation/skeletal-animation.md @@ -0,0 +1,180 @@ +# 骨骼动画 + +骨骼动画是一种常见但类型特殊的动画。 + +## 骨骼动画组件 + +导入带有动画文件的 [模型资源](../asset/model/mesh.md) 后,若模型网格中带有蒙皮信息,在使用模型时,SkeletalAnimation 组件便会自动添加到模型节点上。 + +或者在 **层级管理器** 中选中需要添加骨骼动画组件的节点,然后在 **属性检查器** 中选择 **添加组件 -> Animation -> SkeletalAnimation** 即可。 + +![skeletalAnimation component](./skeletal-animation/skeletalAnimation-component.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Clips**、**DefaultClip**、**PlayOnLoad** | 与动画组件的属性功能一致,详情请参考 [动画组件属性](./animation-comp.md#%E5%8A%A8%E7%94%BB%E7%BB%84%E4%BB%B6%E5%B1%9E%E6%80%A7)。 | +| **Sockets** | 用于将某些外部节点挂到指定的骨骼关节上,属性的值表示挂点的数量。详情请参考下文 **挂点系统** 部分的内容。| +| **useBakedAnimation** | 该项用于切换使用 **预烘焙骨骼动画** 或 **实时计算骨骼动画**,详情请参考下文 **骨骼动画系统** 部分的内容。| + +骨骼动画的组件接口请参考 [SkeletalAnimation API](%__APIDOC__%/zh/class/SkeletalAnimation)。 + +## 骨骼动画系统 + +Creator 提供了 **预烘焙骨骼动画** 和 **实时计算骨骼动画** 两套系统,针对不同方向的需求,分别优化。 + +这两套系统的唯一开关是 **SkeletalAnimation** 组件中的 **useBakedAnimation** 属性,运行时也可以无缝切换。 + +- 启用 **useBakedAnimation** 时会使用预烘焙骨骼动画系统 +- 禁用 **useBakedAnimation** 后会使用实时计算骨骼动画系统 + +![animationComponent](./animation/useBakedAnimation.png) + +### 预烘焙骨骼动画系统 + +这个系统的压倒性目的是性能,因此部分表现力的牺牲被认为是可以接受的。我们针对性地做了很多底层优化,目前的运行时流程大致如下: + +- 所有动画数据都会按照指定帧率提前预采样、烘焙到全局复用的骨骼动画贴图合集上; +- 根据运行平台是否支持浮点纹理,对应使用 **RGBA32F** 或 **RGBA8** 格式的备用方案(该步骤流程用户不必关心,不会对最终表现有影响,只是低端平台最后的保底策略); +- 每个 **骨骼动画组件**(SkeletalAnimation)负责维护当前的播放进度,以 UBO(一个 Vec4)的形式存储; +- 各 [蒙皮网格渲染器组件](../module-map/mesh/skinnedMeshRenderer.md)(SkinnedMeshRenderer)持有预烘焙蒙皮模型类(BakedSkinningModel),根据同样提前烘焙好的包围盒信息计算 Culling,更新 UBO,在 GPU 上从贴图合集内取到当前数据完成蒙皮。 + +### 实时计算骨骼动画系统 + +这个系统的压倒性目的是表现力,确保所有细节的正确显示,以及完整的程序控制能力。 + +目前的运行时流程大致如下: + +- 所有动画数据根据当前全局时间动态插值计算; +- 动画数据会输出到场景的骨骼节点树中; +- 用户和其他任何系统都可以通过操纵这个骨骼节点树对蒙皮效果产生影响; +- 各蒙皮网格渲染器组件(SkinnedMeshRenderer)持有普通蒙皮模型类(SkinningModel),每帧提取骨骼节点树信息计算 culling,将当前帧完整骨骼变换信息上传 UBO,在 GPU 内完成蒙皮。更多关于蒙皮的介绍,请参考下文 **蒙皮算法** 部分的内容。 + +这为以下所有功能提供了最基础的支撑: + +- blendshape 支持 +- 任意数量动画片段的混合和 masking +- IK、二级物理影响 +- 纯程序控制关节位置 + +### 两套系统的选择与最佳实践 + +目前所有模型资源在导入后,Prefab 中全部默认使用 **预烘焙系统**,以达到最佳性能。建议只在明显感到预烘焙系统的表现力无法达标的情况下,再使用 **实时计算系统**。 + +> **注意**:虽然两套系统可以在运行时无缝切换,但尽量不要高频执行,因为每次切换都涉及底层渲染数据的重建。 + +## 蒙皮算法 + +Creator 内置了两种常见标准蒙皮算法,它们性能相近,只对最终表现有影响: + +1. **LBS(线性混合蒙皮)**:骨骼信息以 **3 x 4** 矩阵形式存储,直接对矩阵线性插值实现蒙皮。目前有体积损失等典型已知问题。 +2. **DQS(双四元数蒙皮)**:骨骼信息以 **双四元数** 形式插值,对不含有缩放变换的骨骼动画效果更精确自然,但出于性能考虑,对所有缩放动画有近似简化处理。 + +引擎默认使用 LBS,可以通过修改引擎 `skeletal-animation-utils.ts` 的 `updateJointData` 函数引用与 `cc-skinning.chunk` 中的头文件引用来切换蒙皮算法。 + +推荐对蒙皮动画质量有较高追求的项目可以尝试启用 DQS,但因 GLSL 400 之前都没有 `fma` 指令,例如 `cross` 等操作在某些 GPU 上无法绕过浮点抵消问题,误差较大,可能引入部分可见瑕疵。 + +## 挂点系统 + +如果需要将某些外部节点挂到指定的骨骼关节上,使其在动画过程中随骨骼关节一起运动变换,需要使用骨骼动画组件的 **挂点(Socket)系统**。下面通过一个范例来介绍如何使用骨骼挂点。 + +> **注意**:**FBX** 或 **glTF** 资源内的挂点模型会自动对接挂点系统,无需任何手动操作。 + +对于 **预烘焙系统** 系统来说,将物体挂载在骨骼节点上是无效的,必须使用挂点系统,这是因为 **预烘焙系统** 模式下,骨骼变换是在 GPU 端进行计算的,CPU 内无法获取到变换的结果。 + +也就是说,对于 **预烘焙系统** 动画来说,在编辑器内挂在到某个子骨骼下的物体(如武器)在运行时是不会动的,只有使用挂点系统,才会获取到期望的结果。 + +### 通过编辑器实现 + +1. 将骨骼动画资源导入到 **资源管理器** 中,然后将其拖拽到 **层级管理器** 中,生成骨骼动画节点(例如下图中的 `Fox` 节点); + +2. 在 Fox 节点下新建一个空子节点 `Node`; + + ![add-sub-node](./skeletal-animation/add-sub-node.png) + +3. 在 **层级管理器** 中选中 Fox 节点,在 **属性检查器** 中将 SkeletalAnimation 组件的 **Sockets** 属性设置为 1(Sockets 属性的值代表了挂点的数量)。然后设置 **Sockets** 中的 **Path** 和 **Target** 属性: + + - Path 的下拉框中会列出所有的骨骼,选择想要挂载的目标骨骼,这里以 Fox 的尾巴为例 + + - 将刚才创建的子节点 `Node` 拖拽到 Target 属性框中,可以看到 **层级管理器** 中的子节点 `Node` 根据所选的目标骨骼名称被自动重命名了。 + + ![set-sockets](./skeletal-animation/set-sockets.gif) + +4. 这样子节点就成为目标挂点了,接下来我们在子节点下创建一个 Torus(圆环)节点,即可在 **场景编辑器** 中看到圆环套在了狐狸尾巴上。 + + ![torus](./skeletal-animation/torus.png) + + Torus 节点可以根据自己的需要调整其位置大小等属性,也可以在作为目标节点的子节点下添加多个节点,都会跟随指定骨骼的变换而变换。 + +5. 保存场景,点击编辑器上方的预览按钮,可以看到圆环套在狐狸的尾巴上,并随着狐狸的尾巴一起晃动。 + +> **注意**:若要设置多个挂点,则相对应地,也需要在骨骼动画节点下创建多个空的子节点作为目标挂点。如下图: +> +> ![sockets](./skeletal-animation/sockets.png) + +### 通过脚本配置挂点 + +1. 前两个步骤与通过编辑器实现的一致。然后在骨骼动画节点的空子节点下再创建一个 Cube 节点。 + + ![image](./skeletal-animation/create-cube.png) + +2. 在 **资源管理器** 中新建一个 **TypeScript** 脚本并命名为 `SkeletalAttach`,编写组件脚本。脚本代码如下: + + ```ts + import { _decorator, Component, Node, SkeletalAnimation, Socket } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('SkeletalAttach') + export class SkeletalAttach extends Component { + + @property(Node) + cubeNode: Node | undefined; + + private _sockets: Array = []; + + start () { + let target = new Node(); + if(this.cubeNode != undefined) { + // cubeNode 包含一个立方体模型 + this.cubeNode.parent = target; + } + let skeletalAnimation = this.node.getComponent(SkeletalAnimation); + if(skeletalAnimation! = null) { + // target 父节点需设置为 SkeletalAnimation 所在的节点 + target.parent = skeletalAnimation.node; + let path = "root/_rootJoint/b_Root_00/b_Hip_01/b_Tail01_012/b_Tail02_013/b_Tail03_014"; + // 基于路径和目标节点创建一个 ‘Socket’ + let socket = new SkeletalAnimation.Socket(path, target); + this._sockets.push(socket); + skeletalAnimation.sockets = this._sockets; + } + } + + } + ``` + + > **注意**:若不知道目标骨骼的名称,可将 SkeletalAnimation 组件中的 Sockets 属性设置为 1,然后在 Path 的下拉框中查找所需的目标骨骼名称。查找完成后再将 Sockets 属性还原为 0 即可。 + +点击预览按钮,即可看见立方体小块随狐狸模型的尾巴一起摆动。 + +![attach1](./animation/sockets-attach1.gif) + +## 关于动态 Instancing + +基于 **预烘焙系统** 的框架设计,蒙皮模型的 instancing 也成为了触手可及的功能,但要保证正确性还需要收集一些比较底层的信息。 + +这里的根本问题是,同一个 Drawcall 内的各个模型使用的骨骼贴图必须是同一张,如果不是同一张,显示效果会完全错乱。所以如何将动画数据分配到每张骨骼贴图上,就成为了一个需要用户自定义的信息,可以在编辑器菜单栏 **面板 -> 动画** 的 [骨骼贴图布局面板](joint-texture-layout.md) 进行配置。 + +> **注意**: +> +> 1. 只有 **预烘焙系统** 下支持 instancing。我们虽然没有严格禁止在 **实时计算框架** 下启用 instancing(只有编辑器内的警告),但动画效果一定会有问题,取决于模型实际的材质分配情况。最好的情况是不同 instance 间显示完全一致的动画,最坏情况下会导致模型完全错乱。 +> 2. 对于材质中已经开启 instancing 的模型,平面阴影系统也会自动同步使用 instancing 绘制。特别地,蒙皮模型的阴影合批对骨骼贴图布局的要求更高一些,因为阴影的管线状态是统一的,**所有开启阴影的蒙皮模型的动画** 都需要保证在同一张贴图上(相比绘制模型本身,只需要相同 Drawcall 内的 instance 之间保持骨骼贴图一致)。 + +## BatchedSkinnedMeshRenderer 组件 + +目前底层上传 GPU 的骨骼纹理已做到全局自动合批复用,上层数据目前可以通过使用 **批量蒙皮网格渲染器组件(BatchedSkinnedMeshRenderer)** 将同一个骨骼动画组件控制的所有子蒙皮模型合并。 + +**批量蒙皮网格渲染器组件** 相关,详情请参考文档 [BatchedSkinnedMeshRenderer](../module-map/mesh/skinnedMeshBatchRenderer.md)。 + +合批版 effect 书写相对复杂一点,但基本可以基于子材质使用的普通 effect 加入一些相对直接的预处理和接口改动即可。可参考编辑器 **资源管理器** 面板中 `internal -> effects -> util` 目录下的内置 effect 资源 `batched-unlit`,`batched-unlit` 是合批版的 `builtin-unlit`。 + +> **注意**:只有预烘焙系统下使用批量蒙皮网格渲染器组件可以保证正确性,实时计算框架下虽然也能使用,但当合并后的骨骼数量超过 30 个(Uniform 数组最大数量限制)时会有渲染问题。 diff --git a/versions/4.0/zh/animation/skeletal-animation/add-sub-node.png b/versions/4.0/zh/animation/skeletal-animation/add-sub-node.png new file mode 100644 index 0000000000..c49b152614 Binary files /dev/null and b/versions/4.0/zh/animation/skeletal-animation/add-sub-node.png differ diff --git a/versions/4.0/zh/animation/skeletal-animation/create-cube.png b/versions/4.0/zh/animation/skeletal-animation/create-cube.png new file mode 100644 index 0000000000..0732fce0d9 Binary files /dev/null and b/versions/4.0/zh/animation/skeletal-animation/create-cube.png differ diff --git a/versions/4.0/zh/animation/skeletal-animation/set-sockets.gif b/versions/4.0/zh/animation/skeletal-animation/set-sockets.gif new file mode 100644 index 0000000000..61e515f227 Binary files /dev/null and b/versions/4.0/zh/animation/skeletal-animation/set-sockets.gif differ diff --git a/versions/4.0/zh/animation/skeletal-animation/skeletalAnimation-component.png b/versions/4.0/zh/animation/skeletal-animation/skeletalAnimation-component.png new file mode 100644 index 0000000000..473eb553b6 Binary files /dev/null and b/versions/4.0/zh/animation/skeletal-animation/skeletalAnimation-component.png differ diff --git a/versions/4.0/zh/animation/skeletal-animation/sockets.png b/versions/4.0/zh/animation/skeletal-animation/sockets.png new file mode 100644 index 0000000000..1590003527 Binary files /dev/null and b/versions/4.0/zh/animation/skeletal-animation/sockets.png differ diff --git a/versions/4.0/zh/animation/skeletal-animation/torus.png b/versions/4.0/zh/animation/skeletal-animation/torus.png new file mode 100644 index 0000000000..461f2a0d14 Binary files /dev/null and b/versions/4.0/zh/animation/skeletal-animation/torus.png differ diff --git a/versions/4.0/zh/animation/skeletal-animation/useBakedAnimation.png b/versions/4.0/zh/animation/skeletal-animation/useBakedAnimation.png new file mode 100644 index 0000000000..7f78149a3d Binary files /dev/null and b/versions/4.0/zh/animation/skeletal-animation/useBakedAnimation.png differ diff --git a/versions/4.0/zh/animation/use-animation-curve.md b/versions/4.0/zh/animation/use-animation-curve.md new file mode 100644 index 0000000000..a32f8886c1 --- /dev/null +++ b/versions/4.0/zh/animation/use-animation-curve.md @@ -0,0 +1,181 @@ +# 程序化编辑动画剪辑 + +> **注意**:从 v3.3 开始,动画剪辑接口经历了较大的变动,详情可参考 [动画剪辑数据升级指南](./animation-clip-migration-3.3.x.md)。 + +Creator 除了支持在 **动画编辑器** 中 [创建动画剪辑](animation-create.md),还可以通过脚本模块程序化地创建动画剪辑,例如: + +```ts +import { animation, AnimationClip, Vec3 } from "cc"; + +const animationClip = new AnimationClip(); +animationClip.duration = 1.0; // 整个动画剪辑的周期 + +const track = new animation.VectorTrack(); // 创建一个向量轨道 +track.componentsCount = 3; // 使用向量轨道的前三条通道 +track.path = new animation.TrackPath().toHierarchy('Foo').toProperty('position'); // 指定轨道路径,即指定目标对象为 "Foo" 子节点的 "position" 属性 +const [x, y, z] = track.channels(); // x, y, z 是前三条通道 +x.curve.assignSorted([ // 为 x 通道的曲线添加关键帧 + [0.4, ({ value: 0.4 })], + [0.6, ({ value: 0.6 })], + [0.8, ({ value: 0.8 })], +]); + +// 如果关键帧的组织是 [时间, 向量] 数组,可以利用解构语法赋值每一条通道曲线。 +const vec3KeyFrames = [ + [0.4, new Vec3(1.0, 2.0, 3.0)], + [0.6, new Vec3(1.0, 2.0, 3.0)], + [0.8, new Vec3(1.0, 2.0, 3.0)], +] as [number, Vec3][]; +x.curve.assignSorted(vec3KeyFrames.map(([time, vec3]) => [time, { value: vec3.x }])); +y.curve.assignSorted(vec3KeyFrames.map(([time, vec3]) => [time, { value: vec3.y }])); +z.curve.assignSorted(vec3KeyFrames.map(([time, vec3]) => [time, { value: vec3.z }])); + +// 最后将轨道添加到动画剪辑以应用 +animationClip.addTrack(track); +``` + +具体的说明请查看下文介绍。 + +## 动画属性轨道 + +动画剪辑中的任一节点支持添加多条 **动画属性轨道**,动画属性轨道由类 `animation.Track` 表示,描述了某一对象上的某一动画属性随着时间推移而发生的变化,并规定了如何将其应用到目标对象上。 + +动画属性轨道根据下文中介绍的 [轨道类型](#%E8%BD%A8%E9%81%93%E7%B1%BB%E5%9E%8B) 的不同可包含一至多条通道,一般情况下一条动画属性轨道对应一条通道,除了复合轨道,例如 `position`,有 `X`、`Y`、`Z` 三条通道。每条通道都含有一条曲线,曲线是 **可编辑的最小单元**,若动画属性轨道上未添加关键帧,则曲线为空曲线。 + +根据曲线类型的不同,通道包括以下几种: + +- **实数通道**,含有一条实数曲线 `RealCurve` + +- **四元数通道**,含有一条四元数曲线 `QuatCurve` + +- **对象通道**,含有一条动画曲线 `ObjectCurve` + +动画剪辑运行时,每条属性轨道都将绑定到某个对象或某个对象的动画属性上,并通过赋值对象属性或下文介绍的 **值代理** 来产生动画效果。 + +### 轨道类型 + +动画属性轨道的类型决定了轨道包含多少条通道(曲线),以及每条通道(曲线)的类型和含义,Creator 提供了以下类型的轨道: + +| 轨道类型 | 类 | 产生的值 | 说明 | +| :-------------------- | :------------------- | :----------- | :--- | +| 数值轨道 | `animation.RealTrack` | `number` | 数值轨道产生 JavaScript 数值,包含 **一条** 实数通道。| +| 向量轨道(2/3/4 维)| `animation.VectorTrack` | `Vec2`/`Vec3`/`Vec4` | 向量轨道包括 2/3/4 维向量,值分别由向量类 `Vec2`、`Vec3`、`Vec4` 表示。向量轨道的维度是通过 `componentsCount` 字段获取和设置的,需要注意的是向量轨道共包含 **4 条** 实数通道,但运行时仅会使用前 `componentsCount` 条通道。 | +| 四元数轨道 | `animation.QuatTrack` | `Quat` | 四元数轨道(对应节点上的 `rotation` 属性)产生四元数,值由 `Quat` 表示。四元数轨道仅包含 **一条** 四元数通道,这意味着四元数的各个分量属性不可单独编辑,但大多数情况下分量属性的单独编辑也是没有意义的。 | +| 颜色轨道 | `animation.ColorTrack` | `Color` | 颜色轨道产生颜色值,值由 `Color` 表示。颜色轨道包含 **4 条** 实数通道,分别对应于红色、绿色、蓝色、透明度,且范围在 [0-255] 内。各个通道采样后产生的颜色值,将按照类 `Color` 规定的方式将实数转换为整数颜色分量值。 | +| 尺寸轨道 | `animation.SizeTrack` | `Size` | 尺寸轨道产生尺寸值,值由 `Size` 表示。尺寸轨道包含 **两条** 实数通道,分别对应于尺寸的宽度和高度。| +| 对象轨道 | `animation.ObjectTrack` | 任意值 | 对象轨道产生任意类型的值,仅包含 **一条** 对象曲线。对象轨道产生的值即是对象曲线产生的值。| + +### 轨道路径 + +每个动画属性轨道都记录了一个路径,称为 **轨道路径**,由类 `animation.TrackPath` 表示。轨道路径指定了在 **运行时** 如何从当前节点对象寻址到目标对象,因为寻址是在运行时完成的,这种特性使得动画剪辑可以复用到多个对象上。 + +轨道路径由多个子路径组成,每个子路径都指定了如何从上一级路径的寻址结果寻址到另一个对象,最后一个子路径寻址到的结果就是目标对象。类似文件路径用于定位文件夹或文件,而轨道路径用于定位目标对象。 + +通过下表中 `animation.TrackPath` 的方法可根据目标对象类型添加子路径,以及获取、判断子路径的类型: + +| 目标对象类型 | 添加子路径方法 | 获取解析子路径类型 | 判别子路径类型 | +| :--------- | :---------------|----------------------|-------------------| +| 对象属性 | `toProperty()` | `parsePropertyAt()` | `isPropertyAt()` | +| 数组元素 | `toElement()` | `parseElementAt()` | `isElementAt()` | +| 节点的子节点 | `toHierarchy()` | `parseHierarchyAt()` | `isHierarchy()` | +| 节点上的组件 | `toComponent()` | `parseComponentAt()` | `isComponentAt()` | + +以下代码片段演示了如何指定轨道路径: + +```ts +function specifyTrackPath(track: animation.TrackPath) { + const { path } = track; + path // 从当前节点对象寻址到目标对象 + .toHierarchy('path/to/children') // 目标对象为当前节点的 “path/to/child” 子节点 + .toComponent('MyComponent') // 目标对象为 “path/to/child” 子节点的 “MyComponent” 组件 + .toProperty('myProperty') // 目标对象为 “MyComponent” 组件上的 “myProperty” 属性 + .toElement(1) // 目标对象为 “myProperty” 属性中的第二个数组元素 + ; +} +``` + +轨道路径中的子路径可以任意组合,只要它们具有正确的含义,但以下几种情况的轨道路径是无效的: + +1. 空路径 +2. 路径的末尾不是属性或数组元素,且未设置 **值代理**(参考下文介绍) +3. 对象属性、数组元素、节点的子节点、节点上的组件不存在时 + +对于无效的路径,运行时,此条轨道会被忽略并给出警告。 + +### 值代理 + +在轨道路径定位到目标对象后,若最后定位到的是一个 **属性**,默认情况下 Creator 将通过对该属性赋值以完成动画。 + +但在某些情况下,对象可能并没有提供“属性设置”接口,就不能通过赋值来完成设置。例如,材质对象是通过 `Material.prototype.setProperty(name, value)` 来改变其材质属性的值,并没有提供“属性设置”接口,这时候便可以通过在轨道指定 **值代理**,自定义赋值给目标对象。 + +要创建值代理,需要实现 `animation.ValueProxyFactory` 接口,代码示例如下: + +```ts +class SetMaterialPropertyValueProxyFactory { + /* + * 材质属性名称。 + */ + private _propertyName: string; + + constructor (propertyName: string) { + this._propertyName = propertyName; + } + + /** + * 需要实现该接口。‘target’ 是轨道路径的解析结果。 + * 返回的结果应实现值代理接口 ‘animation.ValueProxy’。 + */ + public forTarget (target: unknown): animation.ValueProxy { + // 一个好的实现方法这里应该指定 'target' 一定是材质对象 + // asserts(target instanceof Material); + const material = target as Material; + return { + set: (value) => { + // ‘value’ 是轨道产生的值 + material.setProperty(this._propertyName, value); + }, + }; + } +} +``` + +然后我们便可以设置一个能修改材质属性的动画属性轨道,代码示例如下: + +```ts +import { MeshRenderer, animation } from 'cc'; + +function setupMaterialPropertyTrack(track: animation.TrackPath) { + // 先设置轨道路径,指定目标对象为材质 + track.path + .toHierarchy('path/to/children') + .toComponent(MeshRenderer) + .toProperty('materials') + .toElement(1) + ; + + // 应用值代理 + track.valueProxy = new SetMaterialPropertyValueProxyFactory('mainColor'); +} +``` + +因为动画是可重用的,它可以绑定到多个对象上,Creator 支持 **不同对象应由不同的值代理**,所以 `animation.Track` 的 `valueProxy` 字段是 `animation.ValueProxyFactory` 而不是 `animation.ValueProxy`。另一方面,实现可以在 `forTarget` 这一层面做些优化。 + +> **注意**:此例仅为阐述值代理的创建和使用,Creator 本身提供了用于设置材质属性(`Uniform`)的值代理工厂:`animation.UniformProxyFactory`。 + +## 循环模式 + +动画剪辑通过 `AnimationClip.wrapMode` 可以设置不同的循环模式。以下列出了几种常用的循环模式: + +| `AnimationClip.wrapMode` | 说明 | +| :--- | :--- | +| `WrapMode.Normal` | 播放到结尾后停止 | +| `WrapMode.Loop` | 循环播放 | +| `WrapMode.PingPong` | 从动画开头播放到结尾后,从结尾开始反向播放到开头,如此循环往复 | + +更多循环模式,详情请参考 API [WrapMode](%__APIDOC__%/zh/class/AnimationClip?id=wrapMode) 以及文档 [循环模式与循环次数](./animation-state.md#%E5%BE%AA%E7%8E%AF%E6%A8%A1%E5%BC%8F%E4%B8%8E%E5%BE%AA%E7%8E%AF%E6%AC%A1%E6%95%B0)。 + +## 外来动画 + +有些动画数据并不由轨道表示,但它以另一种形式存在于动画剪辑中,并在运行时产生动画效果。这部分动画数据称为外来动画(Exotic Animation)。外来动画旨在于让 Creator 更高效地存储和计算一些复杂的动画。 + +用户无法访问和编辑外来动画。由编辑器从模型中导入的骨骼动画就存储在外来动画中。 diff --git a/versions/4.0/zh/asset/asset-manager-upgrade-guide.md b/versions/4.0/zh/asset/asset-manager-upgrade-guide.md new file mode 100644 index 0000000000..a2490c441e --- /dev/null +++ b/versions/4.0/zh/asset/asset-manager-upgrade-guide.md @@ -0,0 +1,394 @@ +# 资源管理模块升级指南 + +> 文:Santy-Wang、Xunyi +> +> 本文将详细介绍 Cocos Creator 3D 的 loader 升级到 assetManager 时的注意事项。v2.4 的资源管理与 v3.0 差别不大,无需升级。 + +在 Cocos Creator 2.4 以前,[获取和加载资源](https://github.com/cocos/cocos-docs/blob/e02ac31bab12d3ee767c0549050b0e42bd22bc5b/zh/scripting/load-assets.md) 是通过 `loader` 模块(包括 `loader.load`、`loader.loadRes`、`loader.loadResDir` 等系列 API)来实现的,`loader` 模块主要用于加载资源。但随着 Creator 的不断发展,开发者对于资源管理的需求不断增加,原来的 `loader` 已无法满足大量的资源管理需求,一个新的资源管理模块呼之欲出。 + +因此,Creator 在 v2.4 推出了全新的资源管理模块 —— **Asset Manager**。相较之前的 `loader`,Asset Manager 不但提供了更好的加载性能,而且支持 Asset Bundle、预加载资源以及更加方便的资源释放管理。同时 Asset Manager 还拥有强大的扩展性,大大提升开发者的开发效率和使用体验,我们建议所有开发者都进行升级。 + +为了带来平滑的升级体验,我们仍保留了对 `loader` 相关 API 的兼容。除个别项目使用了无法兼容的特殊用法的 API 必须手动升级外,大部分项目都可以照常运行。之后我们会在时机成熟时才逐渐完全移除对 `loader` 的兼容。如果由于项目周期等原因暂时不方便升级,你可以在确保测试通过的情况下继续保留原来的写法。 + +目前在使用旧的 API 时,引擎会输出警告并提示升级方法。请你根据警告内容和本文的说明对代码进行调整,升级到新的用法。比较抱歉的是,由于底层经过了升级,我们遗留了个别无法兼容的 API,在运行时会输出错误信息。如果你已经决定好要进行升级,那么请仔细阅读以下内容。 + +- 对 **美术策划** 而言,项目中的所有资源,例如场景、动画、Prefab 都不需要修改,也不需要升级。 +- 对 **程序** 而言,影响主要体现在原先代码中使用的 `loader` 的所有 API,都需要改为 `assetManager` 的 API。以下将详细介绍这部分内容。 + +**注意**:因为 v2.4 支持 Asset Bundle,项目中的分包功能也需要进行升级,具体内容请参考 [分包升级指南](./subpackage-upgrade-guide.md)。 + +## 需要手动升级的情况 + +- 你在自己的代码中使用了以 `loader` 开头的 API,比如 `loader.loaderRes`、`loader.loadResDir`、`loader.release` 等。 +- 你在自己的代码中使用了以 `AssetLibrary` 开头的 API,比如 `AssetLibrary.loadAsset`。 +- 你在自己的代码中使用了 `url` 开头的 API,比如 `url.raw`。 +- 你在自己的代码中使用了 `Pipeline`,`LoadingItems` 等类型。 +- 你在自己的代码中使用了 `macro.DOWNLOAD_MAX_CONCURRENT` 属性。 + +## 升级步骤 + +- **备份好旧项目** +- 在 Dashboard 中使用 Cocos Creator v3.0 打开需要升级的旧项目,Creator 将对有影响的资源重新导入,第一次导入时会稍微多花一点时间,导入完毕后就会打开编辑器主窗口。此时可能会出现较多的报错或警告信息,别担心,请打开代码编辑工具根据报错或警告信息对代码进行升级。 + +### 将 `loader` 相关的 API 替换为 `assetManager` 相关的 API + +从 v2.4 开始,不建议使用 `loader`,并且在后续的版本中也会逐渐被彻底移除,请使用新的资源管理模块 `assetManager` 进行替换。 + +#### 加载相关接口的替换 + +如果你在自己的代码中使用了 `loader.loadRes`、`loader.loadResArray`、`loader.loadResDir`,请使用 `assetManager` 中对应的 API 进行替换。可参考下方的替换方式: + +- **loader.loadRes** + + `resources.load` 的参数与 `loader.loadRes` 完全相同。替换方式如下: + + ```typescript + // 修改前 + loader.loadRes(...); + + // 修改后 + resources.load(...); + ``` + +- **loader.loadResArray** + + `assetManager` 为了降低学习成本,将 `loadResArray` 与 `load` 进行了合并。`resources.load` 的第一个参数可支持多个路径,所以可以使用 `resources.load` 进行替换: + + ```typescript + // 修改前 + loader.loadResArray(...); + + // 修改后 + resources.load(...); + ``` + +- **loader.loadResDir** + + `resources.loadDir` 的参数与 `loader.loadResDir` 完全相同: + + ```typescript + // 修改前 + loader.loadResDir(...); + + // 修改后 + resources.loadDir(...); + ``` + + **注意**:为了简化接口,`resources.loadDir` 的加载完成回调将 **不再提供** `paths` 的列表。请避免以下的使用方式: + + ```typescript + loader.loadResDir('images', Texture2D, (err, assets, paths) => console.log(paths)); + ``` + + 如果你想要查询 `paths` 列表,可以使用以下方式: + + ```typescript + const infos = resources.getDirWithPath('images', Texture2D); + let paths = infos.map(function (info) { + return info.path; + }); + ``` + +- **loader.load** + + 如果你在自己的代码中使用了 `loader.load` 来加载远程图片或远程音频,为了方便理解,在 `assetManager` 中将有专门的 API 用于此项工作,如下所示: + + - 加载远程图片 + + ```typescript + // 修改前 + loader.load('http://example.com/remote.jpg', (err, texture) => console.log(texture)); + + // 修改后 + assetManager.loadRemote('http://example.com/remote.jpg', (err, texture) => console.log(texture)); + ``` + + - 加载远程音频 + + ```typescript + // 修改前 + loader.load('http://example.com/remote.mp3', (err, audioClip) => console.log(audioClip)); + + // 修改后 + assetManager.loadRemote('http://example.com/remote.mp3', (err, audioClip) => console.log(audioClip)); + ``` + + - 加载远程文本 + + ```typescript + // 修改前 + loader.load('http://example.com/equipment.txt', (err, text) => console.log(text)); + + // 修改后 + assetManager.loadRemote('http://example.com/equipment.txt', (err, textAsset) => console.log(textAsset.text)); + ``` + +**注意**: + +1. 如果你在自己的代码中使用了 `loader.downloader.loadSubpackage` 来加载分包,请参考 [分包升级指南](./subpackage-upgrade-guide.md) 进行升级。 + +2. 为了避免产生不必要的错误,`loader.onProgress` 在 `assetManager` 中没有对应实现。你可以自己实现全局回调机制,但建议将回调传入到每个加载函数中,避免并发加载时互相干扰。 + +#### 释放相关接口的替换 + +如果你在自己的代码中使用了 `loader.release`、`loader.releaseAsset`、`loader.releaseRes`、`loader.releaseResDir`,请使用 `assetManager` 中对应的 API 进行替换。可参考下方的替换方式: + +- **loader.release** + + `loader.release` 可用 `assetManager.releaseAsset` 替换。 + + **注意**:为了避免开发者关注资源中一些晦涩难懂的属性,`assetManager.releaseAsset` **不再接受** 数组、资源 UUID、资源 URL 进行释放,仅能通过资源本身进行释放。 + + ```typescript + // 修改前 + loader.release(texture); + // 修改后 + assetManager.releaseAsset(texture); + + // 修改前 + loader.release([texture1, texture2, texture3]); + // 修改后 + [texture1, texture2, texture3].forEach(t => assetManager.releaseAsset(t)); + + // 修改前 + const uuid = texture._uuid; + loader.release(uuid); + // 修改后 + assetManager.releaseAsset(texture); + + // 修改前 + const url = texture.url; + loader.release(url); + // 修改后 + assetManager.releaseAsset(texture); + ``` + + **注意**:为了增加易用性,在 `assetManager` 中释放资源的依赖资源将 **不再需要** 手动获取资源的依赖项,在 `assetManager.releaseAsset` 内部将会尝试自动去释放相关依赖资源,例如: + + ```typescript + // 修改前 + const assets = loader.getDependsRecursively(texture); + loader.release(assets); + + // 修改后 + assetManager.releaseAsset(texture); + ``` + +- **loader.releaseAsset** + + `loader.releaseAsset` 可直接使用 `assetManager.releaseAsset` 替换: + + ```typescript + // 修改前 + loader.releaseAsset(texture); + + // 修改后 + assetManager.releaseAsset(texture); + ``` + +- **loader.releaseRes** + + `loader.releaseRes` 可直接使用 `resources.release` 替换: + + ```typescript + // 修改前 + loader.releaseRes('images/a', Texture2D); + + // 修改后 + resources.release('images/a', Texture2D); + ``` + +- **loader.releaseAll** + + `loader.releaseAll` 可直接使用 `assetManager.releaseAll` 替换: + + ```typescript + // 修改前 + loader.releaseAll(); + + // 修改后 + assetManager.releaseAll(); + ``` + +**注意**: + +1. 出于安全考虑,`loader.releaseResDir` 在 `assetManager` 中没有对应实现,请使用 `assetManager.releaseAsset` 或 `resources.release` 进行单个资源释放。 + +2. 因为 `assetManager.releaseAsset` 会自动释放依赖资源,所以你不需要再显式调用 `loader.getDependsRecursively`。如果需要查找资源的相关依赖,请参考 `assetManager.dependUtil` 中相关的 API。 + +3. 出于安全考虑,`assetManager` 仅支持在场景中设置的自动释放,其他的已移除。`assetManager` 中没有实现 `loader.setAutoRelease`、`loader.setAutoReleaseRecursively`、`loader.isAutoRelease` 这几个 API,建议你使用全新的基于引用计数的自动释放机制,详细请参考 [资源释放](release-manager.md)。 + +#### 扩展相关接口的替换 + +- **Pipeline** + + 如果你的代码中有使用 `loader.insertPipe`、`loader.insertPipeAfter`、`loader.appendPipe`、`loader.addDownloadHandlers`、`loader.addLoadHandlers` 系列 API 对 `loader` 的加载流程做过扩展,或者直接使用了 `loader.assetLoader`、`loader.md5Pipe`、`loader.downloader`、`loader.loader`、`loader.subPackPipe` 中的方法,请使用 `assetManager` 中对应的 API 进行替换。 + + 因为 `assetManager` 是更通用的模块,不再继承自 `Pipeline`,所以 `assetManager` 不再实现 `loader.insertPipe`、`loader.insertPipeAfter`、`loader.appendPipe`。具体的替换方式如下: + + ```typescript + // 修改前 + const pipe1 = { + id: 'pipe1', + handle: (item, done) => { + let result = doSomething(item.uuid); + done(null, result); + } + }; + + const pipe2 = { + id: 'pipe2', + handle: (item, done) => { + let result = doSomething(item.content); + done(null, result); + } + }; + + loader.insertPipe(pipe1, 1); + loader.appendPipe(pipe2); + + // 修改后 + function pipe1 (task, done) { + let output = []; + for (let i = 0; i < task.input.length; i++) { + let item = task.input[i]; + item.content = doSomething(item.uuid); + output.push(item); + } + + task.output = output; + done(null); + } + + function pipe2 (task, done) { + let output = []; + for (let i = 0; i < task.input.length; i++) { + let item = task.input[i]; + item.content = doSomething(item.content); + output.push(item); + } + + task.output = output; + done(null); + } + + assetManager.pipeline.insert(pipe1, 1); + assetManager.pipeline.append(pipe2); + ``` + + **注意**: + + 1. `assetManager` **不再继承** 自 `Pipeline`,而是 `assetManager` 下拥有的多个 `Pipeline` 实例。详情请参考 [管线与任务](pipeline-task.md)。 + + 2. 为了易用性,Pipe 的定义不再需要定义一个拥有 `handle` 方法和 `id` 的对象,只需要一个方法即可。详情请参考 [管线与任务](..pipeline-task.md)。 + + 3. 为了简化逻辑、提高性能,Pipe 中处理的内容不再是 `item`,而是 `task` 对象。详情请参考 [管线与任务](pipeline-task.md)。 + + 4. 为了降低学习成本,`Pipeline` 中不再支持 `insertPipeAfter` 形式的 API,请使用 `insert` 插入指定的位置。 + +- **addDownloadHandlers、addLoadHandlers** + + 出于模块化考虑,`assetManager` 中没有实现 `addDownloadHandlers`、`addLoadHandlers`,请参考以下方式替换: + + ```typescript + // 修改前 + const customHandler = (item, cb) => { + let result = doSomething(item.url); + cb(null, result); + }; + + loader.addDownloadHandlers({png: customHandler}); + + // 修改后 + const customHandler = (url, options, cb) => { + let result = doSomething(url); + cb(null, result); + }; + + assetManager.downloader.register('.png', customHandler); + ``` + + 或者: + + ```typescript + // 修改前 + const customHandler = (item, cb) => { + let result = doSomething(item.content); + cb(null, result); + }; + + loader.addLoadHandlers({png: customHandler}); + + // 修改后 + const customHandler = (file, options, cb) => { + let result = doSomething(file); + cb(null, result); + }; + + assetManager.parser.register('.png', customHandler); + ``` + + **注意**: + + 1. 因为 **下载模块** 与 **解析模块** 都是依靠 **扩展名** 来匹配对应的处理方式,所以调用 `register` 时,传入的第一个参数需要以 `.` 开头。 + + 2. 出于模块化的考虑,自定义的处理方法将不再传入一个 `item` 对象,而是直接传入与其相关的信息。`downloader` 的自定义处理方法传入的是 **待下载的 URL**,`parser` 传入的则是 **待解析的文件**。具体的内容请参考 [下载与解析](downloader-parser.md)。 + + 3. 新的拓展机制提供了一个额外的 `options` 参数,可以极大地增加灵活性。但如果你不需要配置引擎内置参数或者自定义参数,可以无视它。具体内容请参考文档 [可选参数](options.md)。 + +- **downloader,loader,md5Pipe,subPackPipe** + + `loader.downloader` 可由 `assetManager.downloader` 代替,`loader.loader` 可由 `assetManager.parser` 代替。但其中的接口没有完全继承,具体内容请参考文档 [下载与解析](downloader-parser.md) 或者 API 文档 [assetManager.downloader](%__APIDOC__%/zh/class/AssetManager?id=downloader) 和 [assetManager.parser](__APIDOC__/zh/class/AssetManager?id=parser)。 + + **注意**:出于对性能、模块化和易读性的考虑,`loader.assetLoader`、`loader.md5Pipe`、`loader.subPackPipe` 已经被合并到 `assetManager.transformPipeline` 中,你应该避免使用这三个模块中的任何方法与属性。关于 `assetManager.transformPipeline` 的具体内容可参考 [管线与任务](pipeline-task.md)。 + +### 其他更新 + +`url` 与 `AssetLibrary` 在 **v2.4** 中已经被移除,请避免使用 `url` 与 `AssetLibrary` 中的任何方法和属性。 + +`Pipeline` 可由 `AssetManager.Pipeline` 进行替换,请参考以下方式进行替换: + +```typescript +// 修改前 +const pipe1 = { + id: 'pipe1', + handle: function (item, cb) { + let result = doSomething(item); + cb(null, result); + } +} + +const pipeline = new Pipeline([pipe1]); + +// 修改后 +function pipe1 (task, cb) { + task.output = doSomething(task.input); + cb(null); +} + +const pipeline = new AssetManager.Pipeline('test', [pipe1]); +``` + +**注意**:`LoadingItem` 在 `assetManager` 中已经不支持,请避免使用这个类型。 + +为了支持更多加载策略,`macro.DOWNLOAD_MAX_CONCURRENT` 已经从 `macro` 中移除,你可以用以下方式替换: + +```typescript +// 修改前 +macro.DOWNLOAD_MAX_CONCURRENT = 10; + +// 修改后 +assetManager.downloader.maxConcurrency = 10; +``` + +或者 + +```typescript +// 修改前 +macro.DOWNLOAD_MAX_CONCURRENT = 10; + +// 修改后(设置预设值) +assetManager.presets['default'].maxConcurrency = 10; +``` + +具体内容可参考 [下载与解析](downloader-parser.md)。 diff --git a/versions/4.0/zh/asset/asset-manager.md b/versions/4.0/zh/asset/asset-manager.md new file mode 100644 index 0000000000..a15c036f94 --- /dev/null +++ b/versions/4.0/zh/asset/asset-manager.md @@ -0,0 +1,152 @@ +# Asset Manager 概述 + +> 文:Santy-Wang、Xunyi + +在游戏的开发过程中,一般需要使用到大量的图片、音频等资源来丰富整个游戏内容,而大量的资源就会带来管理上的困难。所以 Creator 提供了 **Asset Manager** 资源管理模块来帮助开发者管理其资源的使用,大大提升开发效率和使用体验。 + +**Asset Manager** 是 Creator 在 v2.4 新推出的资源管理器,用于替代之前的 `loader`。新的 Asset Manager 资源管理模块具备加载资源、查找资源、销毁资源、缓存资源、Asset Bundle 等功能,相比之前的 `loader` 拥有更好的性能,更易用的 API,以及更强的扩展性。所有函数和方法可通过 `assetManager` 进行访问,所有类型和枚举可通过 `AssetManager` 命名空间进行访问。 + +**注意**:为了带来平滑的升级体验,我们会在一段时间内保留对 `loader` 的兼容,但还是建议新项目统一使用 **Asset Manager**。 + +你可以参考以下文章升级: + +- [loader 升级 assetManager 指南](asset-manager-upgrade-guide.md) +- [子包升级 Asset Bundle 指南](subpackage-upgrade-guide.md) + +## 加载资源 + +### 动态加载资源 + +除了在编辑场景时,可以将资源应用到对应组件上,Creator 还支持在游戏运行过程中动态加载资源并进行设置。而动态加载资源 Asset Manager 提供了以下两种的方式: + +1. 通过将资源放在 resources 目录下,并配合 `resources.load` 等 API 来实现动态加载。 +2. 开发者可以自己规划资源制作为 Asset Bundle,再通过 Asset Bundle 的 `load` 系列 API 进行资源的加载。例如: + + ```typescript + resources.load('images/background/spriteFrame', SpriteFrame, (err, asset) => { + this.getComponent(Sprite).spriteFrame = asset; + }); + ``` + +相关的 API 列表如下: + +| 类型 | 支持 | 加载 | 释放 | 预加载 | 获取 | 查询资源信息 | +| :-- | :-- | :-- | :-- | :-- | :-- | :-- | +| 单个资源 | Asset Bundle | load | release | preload | get | getInfoWithPath | +| 文件夹 | Asset Bundle | loadDir | releaseAsset | preloadDir | N/A | getDirWithPath | +| 场景 | Asset Bundle | loadScene | N/A | preloadScene | N/A | getSceneInfo | +| 单个资源 | `resources` | load | release | preload | get | getInfoWithPath | +| 文件夹 | `resources` | loadDir | releaseAsset | preloadDir | N/A | getDirWithPath | +| 远程 | Asset Manager | loadRemote | releaseAsset | N/A  | N/A | N/A | + +相关文档可参考: + +- [动态加载资源](dynamic-load-resources.md) + +所有加载到的资源都会被缓存在 `assetManager` 中。 + +### 预加载 + +为了减少下载的延迟,`assetManager` 和 Asset Bundle 中不但提供了加载资源的接口,每一个加载接口还提供了对应的预加载版本。开发者可在游戏中进行预加载工作,然后在真正需要时完成加载。预加载只会下载必要的资源,不会进行反序列化和初始化工作,所以性能消耗更小,适合在游戏过程中使用。 + +```typescript +start () { + resources.preload('images/background/spriteFrame', SpriteFrame); + setTimeOut(this.loadAsset.bind(this), 10000); +} + +loadAsset () { + resources.load('images/background/spriteFrame', SpriteFrame, (err, asset) => { + this.getComponent(Sprite).spriteFrame = asset; + }); +} +``` + +关于预加载的更多内容请参考 [预加载与加载](preload-load.md)。 + +## Asset Bundle + +开发者可以将自己的场景、资源、代码划分成多个 Asset Bundle,并在运行时动态加载资源,从而实现资源的模块化,以便在需要时加载对应资源。例如: + +```typescript +assetManager.loadBundle('testBundle', function (err, bundle) { + bundle.load('textures/background', (err, asset) => { + // ... + }); +}); +``` + +更多关于 Asset Bundle 的介绍请参考 [bundle](bundle.md)。 + +## 释放资源 + +Asset Manager 提供了更为方便的资源释放机制,在释放资源时开发者只需要关注该资源本身而不再需要关注其依赖资源。引擎会尝试对其依赖资源根据引用数量进行释放,以减少用户管理资源释放的复杂度。例如: + +```typescript +resources.load('prefabs/enemy', Prefab, function (err, asset) { + assetManager.releaseAsset(asset); +}); +``` + +Creator 还提供了引用计数机制来帮助开发者控制资源的引用和释放。例如: + +- 当需要持有资源时,请调用 `addRef` 来增加引用,确保该资源不会被其他引用到的地方自动释放。 + + ```typescript + resources.load('textures/armor/texture', Texture2D, function (err, texture) { + texture.addRef(); + this.texture = texture; + }); + ``` + +- 当不再需要持有该资源时,请调用 `decRef` 来减少引用,`decRef` 还将根据引用计数尝试自动释放。 + + ```typescript + this.texture.decRef(); + this.texture = null; + ``` + +更多详细内容请参考文档 [资源释放](release-manager.md)。 + +## 缓存管理器 + +在某些平台上,比如微信小游戏,因为存在文件系统,所以可以利用文件系统对一些远程资源进行缓存。此时需要一个缓存管理器来管理所有缓存资源,例如缓存资源、清除缓存资源、修改缓存周期等。从 v2.4 开始,Creator 在所有存在文件系统的平台上都提供了缓存管理器,以便对缓存进行增删改查操作。例如: + +```typescript +// 获取某个资源的缓存 +assetManager.cacheManager.getCache('http://example.com/bundle1/import/9a/9aswe123-dsqw-12xe-123xqawe12.json'); + +// 清除某个资源的缓存 +assetManager.cacheManager.removeCache('http://example.com/bundle1/import/9a/9aswe123-dsqw-12xe-123xqawe12.json'); +``` + +更多缓存管理器的介绍请参考 [缓存管理器](cache-manager.md)。 + +## 可选参数 + +`assetManager` 和 Asset Bundle 的部分接口都额外提供了 `options` 参数,可以极大地增加灵活性以及扩展空间。`options` 中除了可以配置 Creator 内置的参数之外,还可以自定义任意参数,这些参数将提供给下载器、解析器以及加载管线。 + +```typescript +bundle.loadScene('test', { priority: 3 }, callback); +``` + +更多关于 `options` 的内容可参考文档 [可选参数](options.md)。 + +如果不需要配置引擎内置参数或者自定义参数来扩展引擎功能,可以无视它,直接使用更简单的 API 接口,比如 `resources.load`。 + +## 加载管线 + +为了更方便地自定义资源加载流程,Asset Manager 底层使用了名为 **管线与任务**、**下载与解析** 的机制来完成资源的加载工作,极大地增加了灵活性和定制性。如果需要自定义加载管线或自定义管线,可以参考: + +- [管线与任务](pipeline-task.md) +- [下载与解析](downloader-parser.md) + +## 更多参考 + +- [Asset Bundle](bundle.md) +- [资源释放](release-manager.md) +- [下载与解析](downloader-parser.md) +- [加载与预加载](preload-load.md) +- [缓存管理器](cache-manager.md) +- [可选参数](options.md) +- [管线与任务](pipeline-task.md) diff --git a/versions/4.0/zh/asset/asset-workflow.md b/versions/4.0/zh/asset/asset-workflow.md new file mode 100644 index 0000000000..50fa453e87 --- /dev/null +++ b/versions/4.0/zh/asset/asset-workflow.md @@ -0,0 +1,51 @@ +# 资源工作流 + +## 导入资源 + +Creator 提供了三种 **导入资源** 的方式: + +- 以新建文件的方式,通过 Cocos Creator 窗口的 **资源管理器** 面板 **创建按钮** 导入资源 +- 以复制文件的方式,在操作系统的文件管理器中,将资源文件复制到项目资源文件夹下,之后打开编辑器或激活编辑器窗口会自动刷新 **资源管理器** 的资源列表,完成导入资源。 +- 以拖拽文件的方式,从操作系统的文件管理器中拖拽资源文件到 **资源管理器** 面板的某个文件夹位置,完成导入资源。 + +资源相关的名称如下: + +| 名称 | 说明 | +| :--- | :--- | +| dataBase | 数据库 | +| asset-db | 项目资源数据库 | +| internal-db | 内置数据库 | +| uuid | 唯一标识符 | +| meta | 元信息 | + +## 同步资源 + +**资源管理器** 面板中的资源和 **操作系统的文件管理器** 中看到的项目资源文件是同步的,在 **资源管理器** 中对资源的移动、重命名和删除,都会同步到 **操作系统的文件管理器**,反之亦然。 + +## 资源配置信息 `.meta` 文件 + +所有资源文件都会在导入时生成一份同名的 `.meta` 后缀的配置文件 这份配置文件提供了该资源在项目中的唯一标识 **UUID** 以及其他的一些配置信息,如图集中的小图引用,贴图资源的裁剪数据等,是识别一份合法资源的必要因素。 + +在 **资源管理器** 面板中 `.meta` 文件是不可见的,对资源的重命名,移动,删除,都会由编辑器自动同步该资源对应的 `.meta` 文件,以确保配置信息如 **UUID** 等保持不变,即不影响现有的引用。 + +不推荐直接在 **操作系统的文件管理器** 对资源文件进行操作,如有操作,请同步处理相应的 `.meta` 文件,如下建议: + +- 关闭正在使用的编辑器,避免因为文件锁定或资源名称相同导致更新失败。 +- 删除,重命名,移动资源时,请连同 `.meta` 文件一起删除,重命名,移动。 +- 复制资源时如果连同 `.meta` 文件一起复制,将直接使用复制进来的 `.meta` 文件,而不是再生成新的 `.meta` 文件;如果只复制了资源文件,则会生成对应名称的新的 `.meta` 文件。 + +## Library 中的资源 + +资源经过导入后会生成一份新的数据存在项目的 **Library** 文件夹里。在 **Library** 里的文件,其结构和资源是面向引擎的,是最终游戏时需要的格式,即机器友好,但对人的阅读不友好。 + +当 library 丢失或损坏的时候,只要删除整个 library 文件夹再打开项目,就会重新生成资源库。 + +## 如何定位资源 + +一个资源有唯一的 uuid 来定位到该资源,但这种方式不够直观,还有另一种直观的方式:**Database URL** 格式例如 `asset-db` 对应的协议头是 `db://assets`,`internal-db` 对应的协议头是 `db://internal`。 + +有文件夹层级的资源格式,如 `db://assets/prefabs/fire.prefab` + +## SVN 或 GIT 同步资源 + +需要注意 `.meta` 文件的换行符,建议统一下团队成员电脑的换行符风格和规则,避免同步项目资源后打开项目,出现了大量的 `.meta` 文件修改 diff --git a/versions/4.0/zh/asset/atlas.md b/versions/4.0/zh/asset/atlas.md new file mode 100644 index 0000000000..626466bb2f --- /dev/null +++ b/versions/4.0/zh/asset/atlas.md @@ -0,0 +1,49 @@ +# 图集资源(Atlas) + +图集(Atlas)也称作 Sprite Sheet,是游戏开发中常见的一种美术资源。图集是通过专门的工具将多张图片合并成一张大图,并通过 **plist** 等格式的文件索引的资源。可供 Cocos Creator 使用的图集资源由 **plist** 和 **png** 文件组成。下面就是一张图集使用的图片文件: + +![atlas sheep](atlas/sheep_atlas.png) + +## 为什么要使用图集资源 + +在游戏中使用多张图片合成的图集作为美术资源,有以下优势: + +- 合成图集时会去除每张图片周围的空白区域,加上可以在整体上实施各种优化算法,合成图集后可以大大减少游戏包体和内存占用 +- 多个 Sprite 如果渲染的是来自同一张图集的图片时,这些 Sprite 可以使用同一个渲染批次来处理,大大减少 CPU 的运算时间,提高运行效率。 + +更形象生动的解释可以观看来自 CodeAndWeb 的教学视频 [What is a Sprite Sheet(什么是图集)](https://www.codeandweb.com/what-is-a-sprite-sheet),需要使用 VPN 打开视频。 + +## 制作图集资源 + +要生成图集,首先您应该准备好一组原始图片: + +![single sheep](atlas/single_sheep.png) + +接下来可以使用专门的软件生成图集,我们推荐的图集制作软件包括: + +- [TexturePacker 4.x](https://www.codeandweb.com/texturepacker) + + +使用这些软件生成图集时请选择 cocos2d-x 格式的 plist 文件。最终得到的图集文件是同名的 **plist** 和 **png**。 + +![atlas files](atlas/atlas_files.png) + +使用 TexturePacker 打包图集时的设置可参考 [TexturePacker 设置](../ui-system/components/engine/trim.md#texturepacker-%E8%AE%BE%E7%BD%AE)。 + +> **注意**:Cocos Creator 3.x 不支持 TexturePacker 4.x 以下的图集格式,否则导入时会出现报错。 + +## 导入图集资源 + +将上面所示的 **plist** 和 **png** 文件同时拖拽到 **资源管理器** 中,就可以生成可以在编辑器和脚本中使用的图集资源了。 + +### Atlas 和 SpriteFrame + +导入图集资源后,点击类型为 **Atlas** 的图集资源左边的三角图标,展开后可以看到图集资源里包含了很多类型为 `SpriteFrame` 的子资源,每个子资源都可以单独使用/引用。 + +![sprite frame](atlas/spriteframes.png) + +关于 SpriteFrame 的使用方式,详情请参考 [SpriteFrame](sprite-frame.md) 文档。 + + diff --git a/versions/4.0/zh/asset/atlas/atlas_files.png b/versions/4.0/zh/asset/atlas/atlas_files.png new file mode 100644 index 0000000000..89a36353ce Binary files /dev/null and b/versions/4.0/zh/asset/atlas/atlas_files.png differ diff --git a/versions/4.0/zh/asset/atlas/sheep_atlas.png b/versions/4.0/zh/asset/atlas/sheep_atlas.png new file mode 100644 index 0000000000..4be0dbd0ee Binary files /dev/null and b/versions/4.0/zh/asset/atlas/sheep_atlas.png differ diff --git a/versions/4.0/zh/asset/atlas/single_sheep.png b/versions/4.0/zh/asset/atlas/single_sheep.png new file mode 100644 index 0000000000..f14dd4f2f4 Binary files /dev/null and b/versions/4.0/zh/asset/atlas/single_sheep.png differ diff --git a/versions/4.0/zh/asset/atlas/spriteframes.png b/versions/4.0/zh/asset/atlas/spriteframes.png new file mode 100644 index 0000000000..fe647d1ac3 Binary files /dev/null and b/versions/4.0/zh/asset/atlas/spriteframes.png differ diff --git a/versions/4.0/zh/asset/audio-clip.png b/versions/4.0/zh/asset/audio-clip.png new file mode 100644 index 0000000000..b37bd89d6d Binary files /dev/null and b/versions/4.0/zh/asset/audio-clip.png differ diff --git a/versions/4.0/zh/asset/audio.md b/versions/4.0/zh/asset/audio.md new file mode 100644 index 0000000000..d5cc8edad8 --- /dev/null +++ b/versions/4.0/zh/asset/audio.md @@ -0,0 +1,21 @@ +# 音频资源(AudioClip) + +Cocos Creator 支持导入大多数常见的音频文件格式,将其直接拖拽到 **资源管理器** 面板即可,导入后会在 **资源管理器** 中生成相应的音频资源(AudioClip)。 + +![audio-clip](audio-clip.png) + +我们根据音频的长短将其分为长度较长的 **音乐** 和长度短的 **音效**,Creator 通过 AudioSource 组件控制播放不同的音频资源来实现游戏内的背景音乐和音效。详情请参考 [AudioSource 组件参考](../audio-system/audiosource.md)。 + +## 支持的音频资源的格式 + +目前 Cocos Creator 支持导入以下格式的音频文件: + +| 音频格式 | 说明 | +| :------ | :-- | +| `.ogg` | `.ogg` 是一种开源的有损音频压缩格式,与同类型的音频压缩格式相比,优点在于支持多声道编码,采用更加先进的声学模型来减少损失音质,同时文件大小在相同条件下比 `.mp3` 格式小。目前 Android 系统所有的内置铃声也都使用 `.ogg` 文件。[IOS 不支持该格式](https://developer.apple.com/documentation/audiotoolbox/audiofiletypeid) | +| `.mp3` | `.mp3` 是最常见的一种数字音频编码和有损压缩格式。通过舍弃 PCM 音频资料中对人类听觉不重要的部分,达到压缩成较小文件的目的。但对于大多数用户的听觉感受来说,压缩后的音质与压缩前的相比并没有明显的下降。MP3 被大量软硬件支持,应用广泛,是目前的主流。 | +| `.wav` | `.wav` 是微软与 IBM 公司专门为 Windows 开发的一种标准数字音频文件,该文件能记录各种单声道或立体声的声音信息,并能保证声音不失真,因为音频格式未经过压缩。但文件占用相对较大。 | +| `.mp4` | `.mp4` 是一套用于音频、视频信息的压缩编码标准,对于不同的对象可采用不同的编码算法,从而进一步提高压缩效率。 | +| `.m4a` | `.m4a` 是仅有音频的 MP4 文件。音频质量是压缩格式中非常高的,同时在相同的比特率下,文件占用更小。 | + +采用不同的音频编码格式,在相同的条件下,生成的音频文件大小和音质都各有不同。 diff --git a/versions/4.0/zh/asset/auto-atlas.md b/versions/4.0/zh/asset/auto-atlas.md new file mode 100644 index 0000000000..13e34f8e26 --- /dev/null +++ b/versions/4.0/zh/asset/auto-atlas.md @@ -0,0 +1,54 @@ +# 自动图集资源 (Auto Atlas) + +**自动图集** 作为 Cocos Creator 自带的合图功能,可以将指定的一系列碎图打包成一张大图,具体作用和 Texture Packer 的功能很相近。 + +## 创建自动图集资源 + +在 **资源管理器** 中点击左上角的 **+** 创建按钮,然后选择 **自动图集配置**,即可在 **资源管理器** 中新建一个 **auto-atlas.pac** 资源。 + +![create auto atlas](auto-atlas/create-auto-atlas.png) + +**自动图集资源** 目前是以当前文件夹下的所有 **SpriteFrame** 作为碎图资源,然后在构建过程中将其打包成一个大的 **Sprite Atlas**,之后我们会增加其他的选择碎图资源的方式。如果碎图资源 **SpriteFrame** 有配置过,那么打包后重新生成的 **SpriteFrame** 将会保留这些配置。 + +## 配置自动图集资源 + +在 **资源管理器** 中选中一个 **自动图集资源** 后,**属性检查器** 面板将会显示 **自动图集资源** 的所有可配置项。 + +![auto atlas properties](auto-atlas/autoatlas-properties.png) + +| 属性 | 功能说明 +| :-------------- | :----------- | +| 最大宽度 | 单张图集最大宽度 +| 最大高度 | 单张图集最大高度 +| 间距 | 图集中碎图之间的间距 +| 允许旋转 | 是否允许旋转碎图 +| 输出大小为正方形 | 是否强制将图集长宽大小设置成正方形 +| Power of Two | 是否将图集长宽大小设置为二次方倍数 +| 算法 | 算法,图集打包策略,目前暂时只有一个选项 `MaxRects` +| 扩边 | 扩边,在碎图的边框外扩展出一像素外框,并复制相邻碎图像素到外框中。该功能也称作 **Extrude** +| 剔除未使用的图片 | 构建时不包含未被引用的资源。默认勾选,仅在构建后生效,预览时不生效 +| 剔除在 Bundle 内未被使用的 Texture2D 资源 | 默认勾选,仅在构建后生效,预览时不生效 +| 剔除在 Bundle 内未被使用的图片资源 | 默认勾选,仅在构建后生效,预览时不生效 +| 剔除在 Bundle 内未被使用的 Sprite 图集资源 | 默认勾选,仅在构建后生效,预览时不生效 +| UseCompressTexture | 是否使用压缩纹理,详情请参考 [压缩纹理](compress-texture.md)。 + +其余属性与 Texture 是一样的,详情请参考 [纹理贴图](./texture.md#%E5%AD%90%E8%B5%84%E6%BA%90-texture2d-%E7%9A%84%E5%B1%9E%E6%80%A7%E9%9D%A2%E6%9D%BF)。 + +配置完成后可以点击 **预览** 按钮来预览打包的结果,按照当前自动图集配置生成的相关结果将会展示在 **属性检查器** 下面的区域。 + +> **注意**:每次配置完成后,需要重新点击 **预览** 才会重新生成预览图。 + +结果分为: + +- Packed Textures:显示打包后的图集图片以及图片相关的信息,如果会生成的图片有多张,则会往下在 **属性检查器** 中列出来。 +- Unpacked Textures:显示不能打包进图集的碎图资源,造成的原因有可能是这些碎图资源的大小比图集资源的大小还大导致的,这时候可能需要调整下图集的配置或者碎图的大小了。 + +## 生成图集 + +预览项目或者在 Cocos Creator 中使用碎图的时候都是直接使用的碎图资源,在 **构建项目** 这一步才会真正生成图集到项目中。正常情况下,生成图集资源后,会删除包体内原有的小图的 texture 和 image 图片资源,以下两种特殊情况会有特殊处理: + +1. 当图集资源在 **Bundle** 目录下,除了正常生成图集资源以外,也会同时生成原始 spriteFrame 生成的 texture 以及 image 资源,如果对图集的资源有明确的使用范围 **请勾选对应的剔除选项以免造成包体过大**。 + +2. 当图集资源文件夹内任意 spriteFrame 依赖的 texture 被其他资源直接使用(例如被直接作为纹理贴图使用),被依赖的 texture 及其 image 资源将会被一同打包出来。 + +以上两种情况事实上都会增大包体,构建将会警告提示,如非必须,请不要这样使用。 diff --git a/versions/4.0/zh/asset/auto-atlas/autoatlas-properties.png b/versions/4.0/zh/asset/auto-atlas/autoatlas-properties.png new file mode 100644 index 0000000000..6767a7b192 Binary files /dev/null and b/versions/4.0/zh/asset/auto-atlas/autoatlas-properties.png differ diff --git a/versions/4.0/zh/asset/auto-atlas/create-auto-atlas.png b/versions/4.0/zh/asset/auto-atlas/create-auto-atlas.png new file mode 100644 index 0000000000..1b46867f85 Binary files /dev/null and b/versions/4.0/zh/asset/auto-atlas/create-auto-atlas.png differ diff --git a/versions/4.0/zh/asset/bundle.md b/versions/4.0/zh/asset/bundle.md new file mode 100644 index 0000000000..8f8fa5808f --- /dev/null +++ b/versions/4.0/zh/asset/bundle.md @@ -0,0 +1,446 @@ +# Asset Bundle 介绍 + +> 文:Santy-Wang、Xunyi + +从 v2.4 开始,Creator 正式支持 Asset Bundle 功能。Asset Bundle 作为资源模块化工具,允许开发者按照项目需求将贴图、脚本、场景等资源划分在多个 Asset Bundle 中,然后在游戏运行过程中,按照需求去加载不同的 Asset Bundle,以减少启动时需要加载的资源数量,从而减少首次下载和加载游戏时所需的时间。 + +Asset Bundle 可以按需求放置在不同地方,比如可以放在远程服务器、本地、或者小游戏平台的分包中。 + +从 v3.8 开始,Bundle 的配置方案被转移到 **项目设置** -> **Bundle 配置** 分页内。您可以点击 **属性检查器** 上的 **编辑按钮** 或者通过 **项目** 菜单打开 **项目设置** 对 Bundle 进行配置。 + +## 内置 Asset Bundle + +项目中除了自定义的 Asset Bundle 外,Creator 还有内置的 3 个 Asset Bundle。与其他自定义 Asset Bundle 一样,内置 Asset Bundle 也可以根据不同平台进行配置。 + +![builtinBundles](bundle/builtin-bundles.png) + +| 内置 Asset Bundle | 功能说明 | 配置 | +| :--------------- | :-- | :-------- | +| `main` | 存放所有在 **构建发布** 面板的 **参与构建场景** 中勾选的场景以及其依赖资源 | 通过配置 **构建发布** 面板的 **主包压缩类型** 和 **配置主包为远程包** 两项 | +| `resources` | 存放 `resources` 目录下的所有资源以及其依赖资源 | 通过配置 **资源管理器** 中的 `assets -> resources` 文件夹 | +| `start-scene` | 如果在 **构建发布** 面板中勾选了 **初始场景分包**,则首场景将会被构建到 `start-scene` 中。 | 无法进行配置 | +| `internal` | 引擎模块内置的一些默认资源 | 无法进行配置 | + +在构建完成后,内置 Asset Bundle 会根据配置决定它所生成的位置,具体的配置方法以及生成规则请参考 [配置 Asset Bundle](bundle.md#%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95)。 + +内置 Asset Bundle 是在 `application.js` 中进行加载的,你可以通过自定义构建模板功能修改 `application.js` 中的加载代码,如下所示: + +```typescript +// ... + +function loadAssetBundle (hasResourcesBundle, hasStartSceneBundle) { + let promise = Promise.resolve(); + const mainBundleRoot = 'http://myserver.com/assets/main'; + const resourcesBundleRoot = 'http://myserver.com/assets/resources'; + const bundleRoot = hasResourcesBundle ? [resourcesBundleRoot, mainBundleRoot] : [mainBundleRoot]; + return bundleRoot.reduce((pre, name) => pre.then(() => loadBundle(name)), Promise.resolve()); +} + +function loadBundle (name) { + return new Promise((resolve, reject) => { + assetManager.loadBundle(name, (err, bundle) => { + if (err) { + return reject(err); + } + resolve(bundle); + }); + }); +} + +``` + +## 配置方法 + +自定义 Asset Bundle 是以 **文件夹** 为单位进行配置的。当我们在 **资源管理器** 中选中一个文件夹时,**属性检查器** 中就会出现一个 **配置为 Bundle** 的选项,勾选后会出现如下图的配置项: + +![bundle](./subpackage/inspector.png) + +| 配置项 | 功能说明 | +| :--- | :---- | +| Bundle 名称 | Asset Bundle 构建后的名称,默认会使用这个文件夹的名字,可根据需要修改。 | +| Bundle 优先级 | Creator 开放了 20 个可供配置的优先级,构建时将会按照优先级 **从大到小** 的顺序对 Asset Bundle 依次进行构建。具体内容请参考 [Asset Bundle - 优先级](bundle.md#%E4%BC%98%E5%85%88%E7%BA%A7)。 | +| 目标平台 | 不同平台可使用不同的配置,构建时将根据对应平台的设置来构建 Asset Bundle。支持通过下拉框选择不同的平台配置,目前为默认配置。开发者可以通过 **项目设置** -> **Bundle 配置** 自定义自己的配置方案 | +| 压缩类型 | 决定 Asset Bundle 最后的输出形式,包括 **合并依赖**、**无压缩**、**合并所有 JSON**、**小游戏分包**、**Zip** 5 种压缩类型。具体内容请参考 [Asset Bundle - 压缩类型](bundle.md#%E5%8E%8B%E7%BC%A9%E7%B1%BB%E5%9E%8B) | +| 配置为远程包 | 是否将 Asset Bundle 配置为远程包。
若勾选了该项,则 Asset Bundle 在构建后会被放到 **remote** 文件夹,你需要将整个 **remote** 文件夹放到远程服务器上。
构建 OPPO、vivo、华为等小游戏平台时,若勾选了该项,则不会将 Asset Bundle 打包到 rpk 中。 | +| **Bundle 资源过滤** | 资源过滤可以过滤掉 Bundle 内的某些资源,通过下方的 **预览** 按钮,可以查看 Bundle 最终会的资源列表,Bundle 过滤分为包含和排除两部分。详情请查看 [下文](./bundle.md#Bundle%20资源过滤)。| +| **构建 Bundle** | 构建 Bundle 可以针对当前选中的 Bundle 进行构建,详情请查看 [下文](./bundle.md#构建%20Bundle) | + +配置完成后点击面板右上方的 **绿色打钩按钮**,这个文件夹就被配置为 Asset Bundle 的打包预设集合了,在放置需要的资源后,然后在 **构建发布** 面板选择对应的平台进行构建即可得到对应的 Asset Bundle。 + +**注意**: +1. Creator 有 4 个 [内置 Asset Bundle](bundle.md#%E5%86%85%E7%BD%AE-asset-bundle),包括 **internal**、**resources**、**main**、**start-scene**,在设置 **Bundle 名称** 时请不要使用这几个名称。 +2. [小游戏分包](../editor/publish/subpackage.md) 只能放在本地,不能配置为远程包。所以当 **压缩类型** 设置为 **小游戏分包** 时,**配置为远程包** 项不可勾选。 +3. Zip 压缩类型主要是为了降低网络请求数量,如果放在本地,不用网络请求,则没什么必要。所以要求与 **配置为远程包** 搭配使用。 +4. 设置为 Bundle 的文件夹配置是作为 Asset Bundle 的选项配置集合,我们不建议您非常直接地将资源都放置在其中。和之前版本的 resources 类似,Bundle 配置文件夹最好是放置 Scene、Prefab 等入口资源或者需要在脚本内动态加载的资源,最后在构建阶段将会根据依赖关系导出所有引用的资源文件最终填充整个 Asset Bundle。通过这样的方式,可以最大限度的较少不必要的资源导出。 + +## 优先级 + +当文件夹设置为 Asset Bundle 之后,会将文件夹中的资源以及文件夹外的相关依赖资源都合并到同一个 Asset Bundle 中。这样就有可能出现某个资源虽然不在 Asset Bundle 文件夹中,但因为同时被两个 Asset Bundle 所依赖,所以属于两个 Asset Bundle 的情况,如图所示: + +![shared](bundle/shared.png) + +另一种情况是某个资源在一个 Asset Bundle 文件夹中,但同时又被其他 Asset Bundle 所依赖,如图所示: + +![shared2](bundle/shared2.png) + +在这两种情况下,资源 c 既属于 Asset Bundle A,也属于 Asset Bundle B。那资源 c 究竟存在于哪一个 Asset Bundle 中呢?此时就需要通过调整 Asset Bundle 的优先级来指定了。
+Creator 开放了 20 个可供配置的优先级,编辑器在构建时将会按照优先级 **从大到小** 的顺序对 Asset Bundle 依次进行构建。 + +- 当同个资源被 **不同优先级** 的多个 Asset Bundle 引用时,资源会优先放在优先级高的 Asset Bundle 中,低优先级的 Asset Bundle 只会存储一条记录信息。此时低优先级的 Asset Bundle 会依赖高优先级的 Asset Bundle。
+如果你想在低优先级的 Asset Bundle 中加载此共享资源,必须在加载低优先级的 Asset Bundle **之前** 先加载高优先级的 Asset Bundle。 +- 当同个资源被 **相同优先级** 的多个 Asset Bundle 引用时,资源会在每个 Asset Bundle 中都复制一份。此时不同的 Asset Bundle 之间没有依赖关系,可按任意顺序加载。所以请尽量确保共享的资源(例如 `Texture`、`SpriteFrame`、`Audio` 等)所在的 Asset Bundle 优先级更高,以便让更多低优先级的 Asset Bundle 共享资源,从而最小化包体。 + +四个内置 Asset Bundle 文件夹的优先级分别为: + +| Asset Bundle | 优先级 | +| :--- | :--- | +| `main` | 7 | +| `resources` | 8 | +| `start-scene` | 20 | +| `internal` | 21 | + +当四个内置 Asset Bundle 中有相同资源时,资源会优先存储在优先级高的 Asset Bundle 中。建议其他自定义的 Asset Bundle 优先级 **不要高于** 内置的 Asset Bundle,以便尽可能共享内置 Asset Bundle 中的资源。 + +## 压缩类型 + +Creator 目前提供了 **合并依赖**、**无压缩**、**合并所有 JSON**、**小游戏分包**、**Zip** 这几种压缩类型用于优化 Asset Bundle。所有 Asset Bundle 默认使用 **合并依赖** 压缩类型,开发者可重新设置包括内置 Asset Bundle 在内的所有 Asset Bundle 的压缩类型。 + +| 压缩类型 | 功能说明 | +| :------ | :------ | +| **合并依赖** | 构建 Asset Bundle 时会将相互依赖的资源的 JSON 文件合并在一起,从而减少运行时的加载请求次数 | +| **无压缩** | 构建 Asset Bundle 时没有任何压缩操作 | +| **合并所有 JSON** | 构建 Asset Bundle 时会将所有资源的 JSON 文件合并为一个,从而最大化减少请求数量,但可能会增加单个资源的加载时间 | +| **小游戏分包** | 在提供了分包功能的小游戏平台,会将 Asset Bundle 设置为对应平台上的分包。 | +| **Zip** | 在部分小游戏平台,构建 Asset Bundle 时会将资源文件压缩成一个 Zip 文件,从而减少运行时的加载请求数量 | + +如果开发者在不同平台对 Asset Bundle 设置了不同的压缩类型,那么在构建时将根据对应平台的设置来构建 Asset Bundle。 + +## Asset Bundle 的构建 + +在构建时,配置为 Asset Bundle 的文件夹中的资源(包含场景、代码和其他资源)以及文件夹外的相关依赖资源都会被合并到同一个 Asset Bundle 文件夹中。比如场景 A 放在 a 文件夹中,当 a 文件夹配置为 Asset Bundle 后,场景 A 以及它所依赖的资源都会被合并到 Asset Bundle a 文件夹中。 + +配置为 Asset Bundle 的文件夹中的所有 **代码** 和 **资源**,会进行以下处理: + +- **代码**:文件夹中的所有代码会根据发布平台合并成一个 `index.js` 或 `game.js` 的入口脚本文件。 +- **资源**:文件夹中的所有资源以及文件夹外的相关依赖资源都会放到 `import` 或 `native` 目录下。 +- **资源配置**:所有资源的配置信息包括路径、类型、版本信息都会被合并成一个 `config.json` 文件。 + +构建后生成的 Asset Bundle 目录结构如下图所示: + +![export](bundle/exported.png) + +构建完成后,这个 Asset Bundle 文件夹会被打包到对应平台发布包目录下的 **assets** 文件夹中。但有以下两种特殊情况: +- 配置 Asset Bundle 时,若勾选了 **配置为远程包**,则这个 Asset Bundle 文件夹会被打包到对应平台发布包目录下的 **remote** 文件夹中。 +- 配置 Asset Bundle 时,若设置了 **压缩类型** 为 **小游戏分包**,则这个 Asset Bundle 文件夹会被打包到对应平台发布包目录下的 **subpackages** 文件夹中。 + +**assets**、**remote**、**subpackages** 这三个文件夹中包含的每个文件夹都是一个 Asset Bundle。 + +例如:将 example 工程中的 **cases/01_graphics** 文件夹在 Web Mobile 平台配置为 Asset Bundle,那么项目构建后将会在发布包目录下的 **assets** 中生成 **01_graphics** 文件夹,**01_graphics** 文件夹就是一个 Asset Bundle。 + +![asset-bundle](./subpackage/asset-bundle.png) + +### Asset Bundle 中的脚本 + +Asset Bundle 支持脚本分包。如果开发者的 Asset Bundle 中包含脚本文件,则所有脚本会被合并为一个 js 文件。在加载 Asset Bundle 时,就会去加载这个 js 文件。 + +**注意**: +1. 有些平台不允许加载远程的脚本文件,例如微信小游戏,在这些平台上,Creator 会将 Asset Bundle 的代码拷贝到 `src/bundle-scripts` 目录下,从而保证正常加载。 +2. 不同 Asset Bundle 中的脚本建议最好不要互相引用,否则可能会导致在运行时找不到对应脚本。如果需要引用某些类或变量,可以将该类和变量暴露在一个你自己的全局命名空间中,从而实现共享。 + +## 加载 Asset Bundle + +引擎提供了一个统一的 API `assetManager.loadBundle` 来加载 Asset Bundle,加载时需要传入 Asset Bundle 配置面板中的 **Bundle 名称** 或者 Asset Bundle 的 **url**。但当你复用其他项目的 Asset Bundle 时,则只能通过 **url** 进行加载。使用方法如下: + +```typescript +assetManager.loadBundle('01_graphics', (err, bundle) => { + bundle.load('xxx'); +}); + +// 当复用其他项目的 Asset Bundle 时 +assetManager.loadBundle('https://othergame.com/remote/01_graphics', (err, bundle) => { + bundle.load('xxx'); +}); +``` + +`assetManager.loadBundle` 还支持传入用户空间中的路径来加载用户空间中的 Asset Bundle。通过对应平台提供的下载接口将 Asset Bundle 提前下载到用户空间中,然后再使用 `loadBundle` 进行加载,开发者就可以完全自己管理 Asset Bundle 的下载与缓存过程,更加灵活。例如: + +```typescript +// 提前下载某个 Asset Bundle 到用户空间 pathToBundle 目录下。需要保证用户空间下的 Asset Bundle 和对应原始 Asset Bundle 的结构和内容完全一样 +// ... + +// 通过 Asset Bundle 在用户空间中的路径进行加载 +// 原生平台 +assetManager.loadBundle(jsb.fileUtils.getWritablePath() + '/pathToBundle/bundleName', (err, bundle) => { + // ... +}); + +// 微信小游戏平台 +assetManager.loadBundle(wx.env.USER_DATA_PATH + '/pathToBundle/bundleName', (err, bundle) => { + // ... +}); +``` + +**注意**:在配置 Asset Bundle 时,若勾选了 **配置为远程包**,那么构建时请在 **构建发布** 面板中填写 **资源服务器地址**。 + +在通过 API 加载 Asset Bundle 时,引擎并没有加载 Asset Bundle 中的所有资源,而是加载 Asset Bundle 的 **资源清单**,以及包含的 **所有脚本**。
+当 Asset Bundle 加载完成后,会触发回调并返回错误信息和 `AssetManager.Bundle` 类的实例,这个实例就是 Asset Bundle API 的主要入口,开发者可以使用它去加载 Asset Bundle 中的各类资源。 + +### Asset Bundle 的版本 + +Asset Bundle 在更新上延续了 Creator 的 MD5 方案。当你需要更新远程服务器上的 Asset Bundle 时,请在 **构建发布** 面板中勾选 **MD5 Cache** 选项,此时构建出来的 Asset Bundle 中的 `config.json` 文件名会附带 Hash 值。如图所示: + +![md5 cache](subpackage/bundle_md5.png) + +在加载 Asset Bundle 时 **不需要** 额外提供对应的 Hash 值,Creator 会在 `settings.json` 中查询对应的 Hash 值,并自动做出调整。
+但如果你想要将相关版本配置信息存储在服务器上,启动时动态获取版本信息以实现热更新,你也可以手动指定一个版本 Hash 值并传入 `loadBundle` 中,此时将会以传入的 Hash 值为准: + +```typescript +assetManager.loadBundle('01_graphics', {version: 'fbc07'}, function (err, bundle) { + if (err) { + return console.error(err); + } + console.log('load bundle successfully.'); +}); +``` + +这样就能绕过缓存中的老版本文件,重新下载最新版本的 Asset Bundle。 + +## 加载 Asset Bundle 中的资源 + +在 Asset Bundle 加载完成后,返回了一个 `AssetManager.Bundle` 类的实例。我们可以通过实例上的 `load` 方法来加载 Asset Bundle 中的资源,此方法的参数与 `resources.load` 相同,只需要传入资源相对 Asset Bundle 的路径即可。但需要注意的是,路径的结尾处 **不能** 包含文件扩展名。 + +```typescript +// 加载 Prefab +bundle.load(`prefab`, Prefab, function (err, prefab) { + let newNode = instantiate(prefab); + director.getScene().addChild(newNode); +}); + +// 加载 Texture +bundle.load(`image/texture`, Texture2D, function (err, texture) { + console.log(texture) +}); +``` + +与 `resources.load` 相同,`load` 方法也提供了一个类型参数,这在加载同名资源或者加载 SpriteFrame 时十分有效。 + +```typescript +// 加载 SpriteFrame +bundle.load(`image/spriteFrame`, SpriteFrame, function (err, spriteFrame) { + console.log(spriteFrame); +}); +``` + +### 批量加载资源 + +Asset Bundle 提供了 `loadDir` 方法来批量加载相同目录下的多个资源。此方法的参数与 `resources.loadDir` 相似,只需要传入该目录相对 Asset Bundle 的路径即可。 + +```typescript +// 加载 textures 目录下的所有资源 +bundle.loadDir("textures", function (err, assets) { + // ... +}); + +// 加载 textures 目录下的所有 Texture 资源 +bundle.loadDir("textures", Texture2D, function (err, assets) { + // ... +}); +``` + +### 加载场景 + +Asset Bundle 提供了 `loadScene` 方法用于加载指定 bundle 中的场景,你只需要传入 **场景名** 即可。
+`loadScene` 与 `director.loadScene` 不同的地方在于 `loadScene` 只会加载指定 bundle 中的场景,而不会运行场景,你还需要使用 `director.runScene` 来运行场景。 + +```typescript +bundle.loadScene('test', function (err, scene) { + director.runScene(scene); +}); +``` + +## 获取 Asset Bundle + +当 Asset Bundle 被加载过之后,会被缓存下来,此时开发者可以使用 Asset Bundle 名称来获取该 bundle。例如: + +```typescript +let bundle = assetManager.getBundle('01_graphics'); +``` + +## 预加载资源 + +除了场景,其他资源也可以进行预加载。预加载的加载参数和正常加载时一样,不过因为预加载只会去下载必要的资源,并不会进行资源的反序列化和初始化工作,所以性能消耗更小,更适合在游戏过程中使用。 + +Asset Bundle 中提供了 `preload` 和 `preloadDir` 接口用于预加载 Asset Bundle 中的资源。具体的使用方式和 `assetManager` 一致,详情可参考文档 [预加载与加载](preload-load.md)。 + +## 释放 Asset Bundle 中的资源 + +在资源加载完成后,所有的资源都会被临时缓存到 `assetManager` 中,以避免重复加载。当然,缓存中的资源也会占用内存,有些资源如果不再需要用到,可以通过以下三种方式进行释放: + +1. 使用常规的 `assetManager.releaseAsset` 方法进行释放。 + + ```typescript + bundle.load(`image/spriteFrame`, SpriteFrame, function (err, spriteFrame) { + assetManager.releaseAsset(spriteFrame); + }); + ``` + +2. 使用 Asset Bundle 提供的 `release` 方法,通过传入路径和类型进行释放,只能释放在 Asset Bundle 中的单个资源。参数可以与 Asset Bundle 的 `load` 方法中使用的参数一致。 + + ```typescript + bundle.load(`image/spriteFrame`, SpriteFrame, function (err, spriteFrame) { + bundle.release(`image`, SpriteFrame); + }); + ``` + +3. 使用 Asset Bundle 提供的 `releaseAll` 方法,此方法与 `assetManager.releaseAll` 相似,`releaseAll` 方法会释放所有属于该 bundle 的资源(包括在 Asset Bundle 中的资源以及其外部的相关依赖资源),请慎重使用。 + + ```typescript + bundle.load(`image/spriteFrame`, SpriteFrame, function (err, spriteFrame) { + bundle.releaseAll(); + }); + ``` + +**注意**:在释放资源时,Creator 会自动处理该资源的依赖资源,开发者不需要对其依赖资源进行管理。 + +更多资源释放相关的内容,可参考文档 [资源释放](./release-manager.md)。 + +## 移除 Asset Bundle + +在加载了 Asset Bundle 之后,此 bundle 会一直存在整个游戏过程中,除非开发者手动移除。当手动移除了某个不需要的 bundle,那么此 bundle 的缓存也会被移除,如果需要再次使用,则必须再重新加载一次。 + +```typescript +let bundle = assetManager.getBundle('bundle1'); +assetManager.removeBundle(bundle); +``` + +**注意**:在移除 Asset Bundle 时,并不会释放该 bundle 中被加载过的资源。如果需要释放,请先使用 Asset Bundle 的 `release` / `releaseAll` 方法: + +```typescript +let bundle = assetManager.getBundle('bundle1'); +// 释放在 Asset Bundle 中的单个资源 +bundle.release(`image`, SpriteFrame); +assetManager.removeBundle(bundle); + +let bundle = assetManager.getBundle('bundle1'); +// 释放所有属于 Asset Bundle 的资源 +bundle.releaseAll(); +assetManager.removeBundle(bundle); +``` + +## Bundle 资源过滤 + +资源过滤可以将选中的 Bundle 内的某些资源包括或者排除在 Bundle 内。 + +![D:\Develop\creator-docs\zh\asset\bundle](./bundle/filter.png) + +### 过滤类型 + +过滤类型目前有两种,**Asset** 资源以及 **URL**。默认为 **URL**。 + +![filter-type](./bundle/filter-type.png) + +- **Asset**:将资源作为过滤的类型,从 **资源管理器** 拖拽或者点击右侧的 ![lock.png](./bundle/lock.png) 图标从下拉菜单中选择资源,每次仅可以选择一个(或一个目录)。 +- **URL**:根据后面的过滤规则过滤 Bundle 中的资源。过滤规则目前分为 4 种,分别为 **Glob 表达式**、**以 ... 开头**、**以 ... 结尾** 和 **包含 ...**。选择好过滤规则以后,在右侧的输入框内,输入对应的字符串来过滤对应的资源。 + + ![filter-rule.png](./bundle/filter-rule.png) + +| 规则 | 说明 | +| :---| :---| +| **Glob 表达式** | Glob 是一种类似正则的表达式,可以参考 [npm](https://www.npmjs.com/package/glob) 文档 | +| **以 ... 开头** | 过滤出以右侧输入框内内容为开头的资源 | +| **以 ... 结尾** | 过滤出以右侧输入框内内容为结尾的资源 | +| **包含 ...** | 过滤出包含右侧输入框内内容的资源 | + +点击右侧的 “+” 号按钮可以添加新的过滤规则,“-” 将删除选中的规则,无选中时最后一个添加的规则。 + +### 包含 + +按照上述规则添加在 **包含** 规则内的资源将包含在 Bundle 内。 + +### 排除 + +按照上述规则添加在 **排除** 规则内的资源将 **不会** 包含在 Bundle 内。 + +### 预览 + +点击预览可以查看当前选中的 Bundle 内最终有哪些资源会打包进入 Bundle。 + +## 独立构建 Bundle + +如果只是想更新某些 Bundle 而不是对整个游戏进行打包,引擎自 v3.8 开始提供了更方便的构建 Bundle 功能。 + +在 **属性检查器** 的下方找到 **构建 Bundle** 点击该按钮会弹出构建面板。 + +![bundle-build.png](./bundle/bundle-build.png) + +必须拥有至少一个构建任务,才可以构建 Bundle。点击面板上的 **打开构建面板** 来创建新的构建任务。 + +![build-task.png](./bundle/build-task.png) + +创建完成后,该面板才可以进行操作。 + +![build-budle-withtask.png](./bundle/build-budle-withtask.png) + +通过下拉菜单 找到要构建的 bundle,点击 **构建** 按钮, 该 Bundle 会自动构建到 **发布路径** 内。 + +![select-bundle.png](./bundle/select-bundle.png) + +**发布路径** 有两种: + +![build-path.png](./bundle/build-path.png) + +- file:选择要输出的绝对地址 +- project:相对于项目目录下的相对路径 + +![select-open.png](./bundle/select-open.png) + +点击右侧的选择路径 ![select.png](./bundle/select.png) 可以将 Bundle 发布到不同的位置,或者待构建完成后点击 ![open.png](./bundle/open.png) 可以定位到 Bundle 的输出目录。 + +通过 **发布配置** 右侧的列表,勾选该 Bundle 要发布的平台极其配置。 + +![task.png](./bundle/task.png) + +点击 **构建按钮** 后开始构建,期间可以点击 **取消** 按钮取消构建任务。 + +![building.png](./bundle/building.png) + +该面板的设计目的是针对较大的项目,或者某些耗时的 Bundle,开发者可以单独对其进行打包。例如要热更新某个包。可以有效的降低打包耗时。 + +### 命令行执行独立发布 Bundle + +详情请参考 [命令行执行独立发布 Bundle](./../editor/publish/publish-in-command-line.md#命令行执行独立发布-Bundle) + +## FAQ + +- **Q**:Asset Bundle 与 v2.4 之前的资源分包有什么区别? + **A**: + 1. 资源分包实际上是将一些图片和网格拆分出去单独放在一个包内,但这个包是不完整的、无逻辑的,无法复用。
+ Asset Bundle 是通过逻辑划分对资源进行模块化。Asset Bundle 中包含资源、脚本、元数据和资源清单,所以 Asset Bundle 是完整的、有逻辑的、可复用的,我们可以从 Asset Bundle 中加载出整个场景或其他任何资源。Asset Bundle 通过拆分,可以极大减少首包中的 json 数量以及 `settings.json` 的大小。

+ + 2. 资源分包本质上是由小游戏平台控制的一项基础功能。例如微信小游戏支持分包功能,Creator 就在此基础上做了一层封装,帮助开发者设置资源分包,如果微信小游戏不支持分包功能了,则 Creator 也不支持。
+ Asset Bundle 则完全由 Creator 设计实现,是一个帮助开发者对资源进行划分的模块化工具,与游戏平台无关,理论上可支持所有平台。

+ + 3. 资源分包与平台相关,意味着需要按照平台要求的方式设置,比如微信小游戏的分包无法放在远程服务器上,只能放在腾讯的服务器上。
+ 而 Asset Bundle 不受这些限制,Asset Bundle 可以放在本地、远程服务器,甚至就放在微信小游戏的分包中。

+ +- **Q**:Asset Bundle 是否支持大厅加子游戏的模式? + **A**:支持,子游戏的场景可以放在 Asset Bundle 中,在需要时加载。

+ +- **Q**:Asset Bundle 可以减少 `settings.json` 的大小吗? + **A**:当然可以。实际上从 v2.4 开始,打包后的项目完全是基于 Asset Bundle 的,`settings.json` 不再存储跟资源相关的任何配置信息,所有的配置信息都会存储在每个 Asset Bundle 的 `config.json` 中。每一个 `config.json` 只存储各自 Asset Bundle 中的资源信息,也就减小了首包的包体。可以简单地理解为所有的 `config.json` 加起来等于之前的 `settings.json`。

+ +- **Q**:Asset Bundle 支持跨项目复用吗 + **A**:目前版本支持,但我们**不建议跨项目复用**,随着引擎的更新迭代,这可能会产生各类兼容性问题。在目前跨项目复用需要满足以下条件: + 1. 引擎版本相同。 + 2. Asset Bundle 中引用到的所有脚本都要放在 Asset bundle 下。 + 3. Asset Bundle 没有其他外部依赖 bundle,如果有的话,必须加载。 + 4. Asset Bundle 之间尽可能不复用脚本

+ +- **Q**:Asset Bundle 支持分离首场景吗 + **A**:目前仅在部分平台支持。你可以在 **构建发布** 面板中勾选 **初始场景分包**,则首场景会被放到内置 Asset Bundle 的 `start-scene` 中,从而实现分离首场景。

+ +- **Q**:Asset Bundle 支持嵌套设置吗?比如 A 文件夹中有 B 文件夹,A 和 B 都可以设置为 Asset Bundle? + **A**:Asset Bundle 不支持嵌套。

+ +- **Q**: 为什么在 Asset Bundle 内放置图集可能会引起包体变大? + **A**:Asset Bundle 和最早之前的 resources 文件的放置规则是类似的,**当图集放置在其中时,则默认图集本身代表的资源 SpriteAtlas、图集大图 Image、图集文件夹内的小图 Image 资源等等都可能被脚本加载,按照既定规则会将 Bundle 内包含的所有资源都打包出来**。因而不建议直接将图集放在 Bundle 内,而是通过 Bundle 内资源对其的引用来自然的打包到最终的 Asset Bundle 内。目前在图集资源上有开放了一些剔除的配置,实在需要放置在 Bundle 文件夹内的,可以根据需要进行配置。 diff --git a/versions/4.0/zh/asset/bundle/build-budle-withtask.png b/versions/4.0/zh/asset/bundle/build-budle-withtask.png new file mode 100644 index 0000000000..44a1b1cfa3 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/build-budle-withtask.png differ diff --git a/versions/4.0/zh/asset/bundle/build-path.png b/versions/4.0/zh/asset/bundle/build-path.png new file mode 100644 index 0000000000..53f4d12713 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/build-path.png differ diff --git a/versions/4.0/zh/asset/bundle/build-task.png b/versions/4.0/zh/asset/bundle/build-task.png new file mode 100644 index 0000000000..5fdce6b94d Binary files /dev/null and b/versions/4.0/zh/asset/bundle/build-task.png differ diff --git a/versions/4.0/zh/asset/bundle/building.png b/versions/4.0/zh/asset/bundle/building.png new file mode 100644 index 0000000000..841d3c6093 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/building.png differ diff --git a/versions/4.0/zh/asset/bundle/builtin-bundles.png b/versions/4.0/zh/asset/bundle/builtin-bundles.png new file mode 100644 index 0000000000..cc1447e38c Binary files /dev/null and b/versions/4.0/zh/asset/bundle/builtin-bundles.png differ diff --git a/versions/4.0/zh/asset/bundle/bundle-build.png b/versions/4.0/zh/asset/bundle/bundle-build.png new file mode 100644 index 0000000000..5f71ba2e43 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/bundle-build.png differ diff --git a/versions/4.0/zh/asset/bundle/export-config.png b/versions/4.0/zh/asset/bundle/export-config.png new file mode 100644 index 0000000000..ae97c802fa Binary files /dev/null and b/versions/4.0/zh/asset/bundle/export-config.png differ diff --git a/versions/4.0/zh/asset/bundle/exported.png b/versions/4.0/zh/asset/bundle/exported.png new file mode 100644 index 0000000000..a0591ef16e Binary files /dev/null and b/versions/4.0/zh/asset/bundle/exported.png differ diff --git a/versions/4.0/zh/asset/bundle/filter-rule.png b/versions/4.0/zh/asset/bundle/filter-rule.png new file mode 100644 index 0000000000..e70f232a6f Binary files /dev/null and b/versions/4.0/zh/asset/bundle/filter-rule.png differ diff --git a/versions/4.0/zh/asset/bundle/filter-type.png b/versions/4.0/zh/asset/bundle/filter-type.png new file mode 100644 index 0000000000..7b6165f841 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/filter-type.png differ diff --git a/versions/4.0/zh/asset/bundle/filter.png b/versions/4.0/zh/asset/bundle/filter.png new file mode 100644 index 0000000000..345a7b4f44 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/filter.png differ diff --git a/versions/4.0/zh/asset/bundle/lock.png b/versions/4.0/zh/asset/bundle/lock.png new file mode 100644 index 0000000000..53b80d9934 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/lock.png differ diff --git a/versions/4.0/zh/asset/bundle/open.png b/versions/4.0/zh/asset/bundle/open.png new file mode 100644 index 0000000000..5d607f2318 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/open.png differ diff --git a/versions/4.0/zh/asset/bundle/select-bundle.png b/versions/4.0/zh/asset/bundle/select-bundle.png new file mode 100644 index 0000000000..68c16b4385 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/select-bundle.png differ diff --git a/versions/4.0/zh/asset/bundle/select-open.png b/versions/4.0/zh/asset/bundle/select-open.png new file mode 100644 index 0000000000..4f61abe38a Binary files /dev/null and b/versions/4.0/zh/asset/bundle/select-open.png differ diff --git a/versions/4.0/zh/asset/bundle/select.png b/versions/4.0/zh/asset/bundle/select.png new file mode 100644 index 0000000000..f8932354f1 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/select.png differ diff --git a/versions/4.0/zh/asset/bundle/shared.png b/versions/4.0/zh/asset/bundle/shared.png new file mode 100644 index 0000000000..2c000537fd Binary files /dev/null and b/versions/4.0/zh/asset/bundle/shared.png differ diff --git a/versions/4.0/zh/asset/bundle/shared2.png b/versions/4.0/zh/asset/bundle/shared2.png new file mode 100644 index 0000000000..353024dd05 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/shared2.png differ diff --git a/versions/4.0/zh/asset/bundle/task.png b/versions/4.0/zh/asset/bundle/task.png new file mode 100644 index 0000000000..5f9390ab21 Binary files /dev/null and b/versions/4.0/zh/asset/bundle/task.png differ diff --git a/versions/4.0/zh/asset/cache-manager.md b/versions/4.0/zh/asset/cache-manager.md new file mode 100644 index 0000000000..b237df65a7 --- /dev/null +++ b/versions/4.0/zh/asset/cache-manager.md @@ -0,0 +1,105 @@ +# 缓存管理器 + +> 文:Santy-Wang、Xunyi + +在 Web 平台,资源下载完成之后,缓存是由浏览器进行管理,而不是引擎。
+而在某些非 Web 平台,比如微信小游戏,这类平台具备文件系统,可以利用文件系统对一些远程资源进行缓存,但并没有实现资源的缓存机制。此时需要由引擎实现一套缓存机制用于管理从网络上下载下来的资源,包括缓存资源、清除缓存资源、查询缓存资源等功能。 + +从 v2.4 开始,Creator 在所有存在文件系统的平台上都提供了缓存管理器,以便对缓存进行增删改查操作,开发者可以通过 `assetManager.cacheManager` 进行访问。 + +## 资源的下载、缓存及版本管理 + +引擎下载资源的逻辑如下: + +1. 判断资源是否在游戏包内,如果在则直接使用; + +2. 如果不在则查询资源是否在本地缓存中,如果在则直接使用; + +3. 如果不在则查询资源是否在临时目录中,如果在则直接使用(原生平台没有临时目录,跳过该步骤); + +4. 如果不在就从远程服务器下载资源,资源下载到临时目录后直接使用(原生平台是将资源下载到缓存目录); + +5. 后台缓慢地将临时目录中的资源保存到本地缓存目录中,以便再次访问时使用(原生平台跳过该步骤); + +6. 当缓存空间占满后资源会保存失败,此时会使用 LRU 算法删除比较久远的资源(原生平台的缓存空间没有大小限制,跳过该步骤,开发者可以手动调用清理)。 + +对小游戏平台来说,一旦缓存空间占满,所有需要下载的资源都无法保存,只能使用下载保存在临时目录中的资源。而当退出小游戏时,所有的临时目录都会被清理,再次运行游戏时,这些资源又会被再次下载,如此循环往复。 + +> **注意**:缓存空间超出限制导致文件保存失败的问题不会在微信小游戏的 **微信开发者工具** 上出现,因为微信开发者工具没有限制缓存大小,所以测试缓存时需要在真实的微信环境中进行测试。 + +当开启引擎的 **md5Cache** 功能后,文件的 URL 会随着文件内容的改变而改变,这样当游戏发布新版本后,旧版本的资源在缓存中就自然失效了,只能从服务器请求新的资源,也就达到了版本控制的效果。 + +### 上传资源到远程服务器 + +当包体过大时,需要将资源上传到远程服务器,请将资源所在的 Asset Bundle 配置为远程包。接下来我们以微信小游戏为例,来看一下具体的操作步骤: + +1. 合理分配资源,将需要模块化管理的资源文件夹(例如 `resources` 文件夹)配置为 Asset Bundle,并勾选 **配置为远程包**,具体可参考文档 [配置 Asset Bundle](./bundle.md#%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95)。 + + ![bundle_is_remote](./cache-manager/remote-bundle.png) + +2. 如果主包需要配置为远程包,请在 **构建发布** 面板中勾选 **配置主包为远程包**。 + +3. 在 **构建发布** 面板中勾选 **MD5 Cache**,设置 **资源服务器地址**,然后点击 **构建**。 + +4. 构建完成后将发布包目录下的 `remote` 文件夹完整地上传到上一步填写的服务器上。 + +5. 删除本地发布包目录下的 `remote` 文件夹。 + +> **注意**:微信小游戏在测试阶段时,开发者可能无法将项目部署到正式服务器,那就需要在本地服务器测试,请在微信开发者工具的菜单栏中打开 **工具 -> 详情 -> 本地设置** 页面,勾选 **不校验安全域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书** 选项。 +> +> ![details](./cache-manager/details.png) + +### 查询缓存文件 + +缓存管理器提供了 `getCache` 接口以查询所有的缓存资源,开发者可以通过传入资源的原路径来查询缓存路径。 + +```typescript +resources.load('images/background/texture', Texture2D, function (err, texture) { + const cachePath = assetManager.cacheManager.getCache(texture.nativeUrl); + console.log(cachePath); +}); +``` + +### 查询临时文件 + +当资源下载到本地后,可能会以临时文件的形式存储在临时目录中。缓存管理器提供了 `tempFiles` 接口以查询所有下载到临时目录中的资源,开发者可以通过传入资源的原路径进行查询。 + +```typescript +assetManager.loadRemote('http://example.com/background.jpg', function (err, texture) { + const tempPath = assetManager.cacheManager.getTemp(texture.nativeUrl); + console.log(tempPath); +}); +``` + +## 缓存资源 + +缓存管理器中提供了一些参数用于控制资源的缓存: + +- `cacheManager.cacheDir` —— 控制缓存资源的存储目录。 +- `cacheManager.cacheInterval` —— 控制缓存单个资源的周期,默认 500ms 缓存一次。 +- `cacheManager.cacheEnabled` —— 控制是否要缓存资源,默认为缓存。另外,开发者也可以通过指定可选参数 `cacheEnabled` 来覆盖全局设置,例如: + + ```typescript + assetManager.loadRemote('http://example.com/background.jpg', {cacheEnabled: true}, callback); + ``` + +### 清除缓存资源 + +如果缓存资源超出限制,开发者需要手动清除资源,可以使用缓存管理器 `assetManager.cacheManager` 提供的 `removeCache`, `clearCache`, `clearLRU` 来清除缓存资源。 + +- `clearCache` —— 清除缓存目录下的所有缓存资源,请慎重使用。 +- `clearLRU` —— 清除缓存目录下比较久远的资源。小游戏平台会在缓存空间满了后自动调用 `clearLRU`。 +- `removeCache` —— 清除单个缓存资源,使用时需要提供资源的原路径,例如: + + ```typescript + assetManager.loadRemote('http://example.com/background.jpg', function (err, texture) { + assetManager.cacheManager.removeCache(texture.nativeUrl); + }); + ``` + +当开发者升级引擎版本后,留在本地的缓存资源还是之前旧版本引擎对应的资源,并不会自动清空。这可能会导致资源加载出错或渲染错误等问题。解决方案有以下两种: + +1. 构建时在 **构建发布** 面板勾选 **MD5 Cache** 选项,这将确保使用最新版本的资源。 +2. 手动清空之前缓存的资源。 + - 在 **真机** 上通过 `assetManager.cacheManager.clearCache()` 清空缓存。 + - 微信小游戏在 **微信开发者工具** 中点击菜单栏的 **工具 -> 清除缓存 -> 全部清除** 来清空缓存。 diff --git a/versions/4.0/zh/asset/cache-manager/details.png b/versions/4.0/zh/asset/cache-manager/details.png new file mode 100644 index 0000000000..63d84ed764 Binary files /dev/null and b/versions/4.0/zh/asset/cache-manager/details.png differ diff --git a/versions/4.0/zh/asset/cache-manager/remote-bundle.png b/versions/4.0/zh/asset/cache-manager/remote-bundle.png new file mode 100644 index 0000000000..cecc1ef8e0 Binary files /dev/null and b/versions/4.0/zh/asset/cache-manager/remote-bundle.png differ diff --git a/versions/4.0/zh/asset/compress-texture.md b/versions/4.0/zh/asset/compress-texture.md new file mode 100644 index 0000000000..849fae26f8 --- /dev/null +++ b/versions/4.0/zh/asset/compress-texture.md @@ -0,0 +1,105 @@ +# 压缩纹理 + +Cocos Creator 可以直接在编辑器中设置纹理需要的压缩方式,然后在项目发布时自动对纹理进行压缩。支持同一平台同时导出多种图片格式,引擎将根据设备对压缩纹理格式的支持情况加载合适的压缩纹理。 + +## 压缩纹理的优势 + +* 对于 png、jpg、webp 等压缩纹理 + - 通过配置压缩纹理能够在构建项目时压缩纹理像素数据减少资源体积,提高游戏的资源下载速度。 +* 对于 astc、etc1、etc2、pvrtc 等 GPU 压缩纹理 + - 通过配置压缩纹理能够在构建项目时将纹理像素数据转化为GPU专用的压缩格式,这些格式可以直接在GPU内存中使用,无需运行时解压,从而显著减少内存占用、降低带宽需求,提高游戏的渲染性能和加载速度。 + +> **注意**: +> * png、jpg、webp 等非 GPU 压缩纹理格式的图片资源,在压缩图片质量后并不能减少解码图片资源的时间以及减少游戏的内存。 +> * 压缩 png、jpg、webp 等格式图片使用的是 [sharp](https://github.com/lovell/sharp) 开源库,它的压缩率稍低于 [tinypng](https://tinypng.com/) 并且可能出现压缩后图片更大的情况,如果你需要优化此问题,建议使用 [自定义纹理压缩](../editor/publish/custom-build-plugin.md#自定义纹理压缩处理) 自行解决此问题。 + +## 压缩纹理支持 + +Cocos Creator 支持加载多种格式的图片(具体见下表)。 + +| 图片格式 | Android | iOS | 小游戏 | Web | Windows | Mac +| :------ | :------ | :------ | :----- | :------ | :----- | :------ | +| PNG | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | +| JPG | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | +| WEBP | Android 4.0 以上原生支持,其他版本可以使用 [解析库](https://github.com/alexey-pelykh/webp-android-backport) | 可以使用 [解析库](https://github.com/carsonmcdonald/WebP-iOS-example) | 支持 | [部分支持](https://caniuse.com/#feat=webp) | 不支持 | 不支持 | +| PVR | 不支持 | 支持 | 支持 iOS 设备 | 支持 iOS 设备 | 不支持 | 不支持 | +| ETC1 | 支持 | 不支持 | 支持 Android 设备 | 支持 Android 设备 | 不支持 | 不支持 | +| ETC2 | 部分支持,取决于手机硬件 | 不支持 | 不支持 | 支持部分 Android 设备 | 不支持 | 不支持 | +| ASTC | 支持(Android 5.0+) | 支持(iOS 9.0+/iPhone6+) | 微信、抖音、阿里、淘宝等平台支持,详情请看[各平台压缩纹理支持详情](#各移动平台压缩纹理支持详情)。注:开发者工具不支持,需真机调试 | 部分支持 | 不支持 | 不支持 | + +### 各移动平台压缩纹理支持详情 + +除全平台支持的 `JPG` 和 `PNG` 外,其他纹理压缩格式的支持情况如下: + +| 平台名称 | 支持的压缩格式 | +| :---------------- | :------------------- | +| Web Mobile | ASTC / ETC1 / ETC2 / PVR / WEBP | +| WeChat Mini Game | ASTC / ETC1 / ETC2 / PVR | +| ByteDance Mini Game | ASTC / ETC1 / ETC2 / PVR | +| AliPay Mini Game | ASTC / ETC1 / PVR | +| TaoBao Mini Game | ASTC / ETC1 / PVR | +| OPPO Mini Game | ETC1 | +| vivo Mini Game | ETC1 / ASTC | +| Huawei Quick Game | ETC1 | +| iOS | ASTC / ETC1 / ETC2 / PVR / WEBP | +| Android / Huawei AGC | ASTC / ETC1 / ETC2 / WEBP | + +**注意**:目前小游戏平台的通用配置中添加 astc 压缩纹理暂时不支持 vivo 小游戏平台,但是可以使用 [平台覆盖配置](#平台覆盖配置) 让 vivo 小游戏平台也能打包出 astc 压缩纹理。 + +## 关于 ASTC + +对于需要 GPU 压缩纹理格式的项目,推荐使用 ASTC 压缩格式,因为它压缩率高并且质量也更好,同时还支持 HDR 贴图。ASTC 于 2012 年发布,距今已经 12 年,在移动端已接近 100% 的支持率。 + +对于画质要求较高的游戏,可以选择 2D 元素、UI 使用 astc 5x5,模型纹理使用 astc 6x6。 + +对于画质要求较低的游戏,可以选择 2D 元素、UI 使用 6x6,模型纹理使用 8x8。 + +## 压缩纹理配置 + +在未配置压缩纹理的情况下项目构建时输出的是原始图片,如果需要对项目中的图片资源或者自动图集产生的大图进行压缩,可以在 **资源管理器** 中选中这张图片或自动图集,然后在 **属性检查器** 中勾选 `useCompressTexture`,再在 `presetId` 中选择图片的纹理压缩预设,设置完成后点击右上角的绿色打钩按钮,之后项目构建后该图片资源就会变成压缩纹理资源。 + +![compress-texture](compress-texture/compress-texture.png) + +配置压缩纹理预设时默认使用平台通用配置,此配置会根据构建打包的平台是否支持配置中的压缩纹理格式而停止生成此格式的压缩纹理。 + +`presetId` 中提供了编辑器的默认压缩纹理预设,不支持编辑。若需要自定义预设,则点击 `presetId` 旁边的 **编辑预设** 按钮即可前往 [项目设置 -> 压缩纹理](../editor/project/index.md#压缩纹理) 面板进行设置。 + +图片资源上的压缩纹理选项将会存储在资源 meta 文件内,其中 `presetId` 是选择的压缩纹理预设的 ID。图片较多的项目可以通过插件或者脚本的方式批量管理图片压缩所使用的 `presetId`。 + +![meta](compress-texture/meta.png) + +**注意**: +配置了压缩纹理后,只有对应格式的图片会在构建时生成,若有些图片格式在部分设备上不支持就会导致显示异常。为避免该问题,在添加纹理压缩预设时,请额外选择一些通用图片格式(例如 PNG、JPG)作为默认图。 + +## 平台覆盖配置 +在配置小游戏平台的压缩纹理预设时,如果开发者需要忽视内部规则确保构建时生成所配置的压缩纹理资源,则需要添加平台覆盖配置。 + +![平台覆盖配置](compress-texture/compress-3.jpg) + +## 压缩纹理详解 + +Cocos Creator 3.0 在构建图片的时候,会查找当前图片是否进行了压缩纹理的配置,如果没有,则最后按原图输出。 + +如果查找到了压缩纹理的配置,那么会按照找到的配置对图片进行纹理压缩。项目设置里压缩纹理配置是按照平台大类划分的,具体到实际平台的支持程度会有一些差异。构建将会根据 **实际构建平台** 以及当前 **图片纹理的透明通道** 情况来对配置的纹理格式做一定的剔除和优先级选择,关于这块规则可以参考下文的示例来理解。 + +这些生成的图片不会都被加载到引擎中,引擎会根据 [macro.SUPPORT_TEXTURE_FORMATS](%__APIDOC__%/zh/interface/Macro?id=SUPPORT_TEXTURE_FORMATS) 中的配置来选择加载合适格式的图片。`macro.SUPPORT_TEXTURE_FORMATS` 列举了当前平台支持的所有图片格式,引擎加载图片时会从生成的图片中找到在这个列表中 **优先级靠前**(即排列靠前)的格式来加载。 + +开发者可以通过修改 `macro.SUPPORT_TEXTURE_FORMATS` 来自定义平台的图片资源支持情况以及加载顺序的优先级。 + +## 智能剔除 + +### 示例一 + +![1](compress-texture/compress-1.png) + +如上图所示,对于 MiniGame 平台的压缩纹理预设,假如 **构建的是华为快游戏这类仅在安卓设备上运行的,构建时将不会打包出 PVR 的纹理格式**。更多的平台剔除细则可以参考文末的 [各平台压缩纹理支持详情](#各移动平台压缩纹理支持详情)。 + +### 示例二 + +![2](compress-texture/compress-2.png) + +在上面的示例图中,ETC1 和 PVR 类型都 **同时配置了 RGB 和 RGBA 两种类型的纹理格式,这种情况下构建将会根据当前图片的是否带有透明通道来优先选择其中一种格式**。示例图中的图片是带透明通道的,则此时构建将只会打包出带有 REGA 类型的压缩纹理格式。当然这种剔除只有同时存在时才会,假如配置里只有 RGB 的图片格式,即便当前图片是带透明通道的也会正常打包出来。 + +## 自定义构建纹理压缩处理 + +纹理压缩目前是在构建后生效,编辑器自带了一套处理工具。若需要自定义压缩工具,请参考 [自定义纹理压缩](../editor/publish/custom-build-plugin.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BA%B9%E7%90%86%E5%8E%8B%E7%BC%A9%E5%A4%84%E7%90%86)。 diff --git a/versions/4.0/zh/asset/compress-texture/compress-1.png b/versions/4.0/zh/asset/compress-texture/compress-1.png new file mode 100644 index 0000000000..3ab583d162 Binary files /dev/null and b/versions/4.0/zh/asset/compress-texture/compress-1.png differ diff --git a/versions/4.0/zh/asset/compress-texture/compress-2.png b/versions/4.0/zh/asset/compress-texture/compress-2.png new file mode 100644 index 0000000000..637aad67e6 Binary files /dev/null and b/versions/4.0/zh/asset/compress-texture/compress-2.png differ diff --git a/versions/4.0/zh/asset/compress-texture/compress-3.jpg b/versions/4.0/zh/asset/compress-texture/compress-3.jpg new file mode 100644 index 0000000000..fa889ac66e Binary files /dev/null and b/versions/4.0/zh/asset/compress-texture/compress-3.jpg differ diff --git a/versions/4.0/zh/asset/compress-texture/compress-texture.png b/versions/4.0/zh/asset/compress-texture/compress-texture.png new file mode 100644 index 0000000000..5084532e60 Binary files /dev/null and b/versions/4.0/zh/asset/compress-texture/compress-texture.png differ diff --git a/versions/4.0/zh/asset/compress-texture/meta.png b/versions/4.0/zh/asset/compress-texture/meta.png new file mode 100644 index 0000000000..e53caa434e Binary files /dev/null and b/versions/4.0/zh/asset/compress-texture/meta.png differ diff --git a/versions/4.0/zh/asset/downloader-parser.md b/versions/4.0/zh/asset/downloader-parser.md new file mode 100644 index 0000000000..a9386f3ebc --- /dev/null +++ b/versions/4.0/zh/asset/downloader-parser.md @@ -0,0 +1,166 @@ +# 下载与解析 + +> 文:Santy-Wang、Xunyi + +Asset Manager 底层使用了多条加载管线来加载和解析资源,每条管线中都使用了 `downloader` 和 `parser` 模块,也就是下载器和解析器。开发者可以通过 `assetManager.downloader` 和 `assetManager.parser` 来访问。 + +## 下载器 + +下载器是一个全局单例,包括 **下载重试**、**下载优先级排序** 和 **下载并发数限制** 等功能。 + +### 下载重试 + +下载器如果下载资源失败,会自动重试下载,开发者可以通过 `maxRetryCount` 和 `retryInterval` 属性来设置重试下载的相关参数。 + +- `maxRetryCount` 属性用于设置重试下载的最大次数,默认 3 次。若不需要重试下载,可设置为 0,则下载失败时会立即返回错误。 + + ```typescript + assetManager.downloader.maxRetryCount = 0; + ``` + +- `retryInterval` 属性用于设置重试下载的间隔时间,默认 2000 ms。若设置为 4000 ms,则下载失败时会先等待 4000 ms,然后再重新下载。 + + ```typescript + assetManager.downloader.retryInterval = 4000; + ``` + +### 下载优先级 + +Creator 开放了四个下载优先级,下载器将会按照优先级 **从大到小** 的顺序来下载资源。 + +| 资源 | 优先级 | 说明 | +| :-- | :---- | :--- | +| 脚本或 Asset Bundle | 2 | 优先级最高 | +| 场景资源 | 1 | 包括场景中的所有资源,确保场景能够快速加载 | +| 开发者手动加载的资源 | 0 | | +| 预加载资源 | -1 | 优先级最低,因为预加载更多是提前加载资源,时间要求相对较为宽松 | + +开发者也可以通过可选参数 `priority` 传入一个优先级来覆盖默认设置,从而控制加载顺序。详情可参考下方的“通过可选参数设置”部分。 + +### 设置下载并发数 + +开发者可以通过 `maxConcurrency` 和 `maxRequestsPerFrame` 来设置下载器的最大下载并发数等限制。 + +- `maxConcurrency` 用于设置下载的最大并发连接数,若当前连接数超过限制,将会进入等待队列。 + + ```typescript + assetManager.downloader.maxConcurrency = 10; + ``` + +- `maxRequestsPerFrame` 用于设置每帧发起的最大请求数,从而均摊发起请求的 CPU 开销,避免单帧过于卡顿。如果此帧发起的连接数已经达到上限,将延迟到下一帧发起请求。 + + ```typescript + assetManager.downloader.maxRequestsPerFrame = 6; + ``` + +另外,`downloader` 中使用了一个 `jsb.Downloader` 类的实例,用于在 **原生平台** 从服务器上下载资源。`jsb.Downloader` 与 Web 的 [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) 类似。目前 `jsb.Downloader` 类的实例的下载并发数限制默认为 **32**,超时时长默认为 **30s**,如果需要修改默认值,可以在 `main.js` 中修改: + +```typescript +// main.js +assetManager.init({ + bundleVers: settings.bundleVers, + remoteBundles: settings.remoteBundles, + server: settings.server, + jsbDownloaderMaxTasks: 32, // 最大并发数 + jsbDownloaderTimeout: 60 // 超时时长 +}); +``` + +## 解析器 + +解析器用于将文件解析为引擎可识别的资源,开发者可以通过 `assetManager.parser` 来访问。 + +## 通过可选参数设置 + +在下载器和解析器中的设置都是全局设置,若开发者需要单独设置某个资源,可以通过 **可选参数** 传入专有设置来覆盖全局设置,例如: + +```typescript +assetManager.loadAny({'path': 'test'}, {priority: 2, maxRetryCount: 1, maxConcurrency: 10}, callback); +``` + +具体内容可参考文档 [可选参数](options.md)。 + +## 预设 + +Creator 预先对正常加载、预加载、场景加载、Asset Bundle 加载、远程资源加载、脚本加载这 6 种加载情况的下载/解析参数做了预设,其中预加载因为性能考虑,所以限制较大,最大并发数更小。如下所示: + +```typescript +{ + 'default': { + priority: 0, + }, + + 'preload': { + maxConcurrency: 2, + maxRequestsPerFrame: 2, + priority: -1, + }, + + 'scene': { + maxConcurrency: 8, + maxRequestsPerFrame: 8, + priority: 1, + }, + + 'bundle': { + maxConcurrency: 8, + maxRequestsPerFrame: 8, + priority: 2, + }, + + 'remote': { + maxRetryCount: 4 + }, + + 'script': { + priority: 2 + } +} +``` + +开发者可以通过 `assetManager.presets` 对每种预设进行修改,使用时需要传入预设的名称来访问对应的参数项。 + +```typescript +// 修改预加载的预设优先级为 1 +let preset = assetManager.presets.preload; +preset.priority = 1; +``` + +也可以增加自定义预设,并通过可选参数 `preset` 传入。 + +```typescript +// 自定义预设,并通过可选参数 preset 传入 +assetManager.presets.mypreset = {maxConcurrency: 10, maxRequestsPerFrame: 6}; +assetManager.loadAny({'path': 'test'}, {preset: 'mypreset'}, callback); +``` + +**注意**:通过可选参数、预设以及下载器/解析器本身,均能设置下载和解析过程中的相关参数(例如下载并发数、重试次数等)。当使用多种方式设置了同一个参数时,引擎会按照 **可选参数 > 预设 > 下载器/解析器** 的先后顺序进行选取使用。也就是说,如果引擎在可选参数中找不到相关设置时,会去预设中查找,如果预设中也找不到时,才会去下载器/解析器中查找。 + +## 自定义处理方法 + +下载器和解析器都拥有一张注册表,在使用 `downloader` 或 `parser` 时,下载器和解析器会根据传入的后缀名称去注册表中查找对应的下载方式和解析方式,并将参数传入对应的处理方式之中。当开发者需要修改目前格式的处理方式,或者在项目中增加一个自定义格式时,可以通过注册自定义的处理方式来实现扩展引擎。下载器与解析器都提供了 `register` 接口用于注册处理方法,使用方式如下: + +```typescript +assetManager.downloader.register('.myformat', function (url, options, callback) { + // 下载对应资源 + ...... +}); + +assetManager.parser.register('.myformat', function (file, options, callback) { + // 解析下载完成的文件 + ...... +}); +``` + +自定义的处理方法需要接收三个参数: +- 第一个参数为处理对象,在下载器中是 url,在解析器中是文件。 +- 第二个参数是可选参数,可选参数可以在调用加载接口时指定。 +- 第三个参数是完成回调,当注册完成处理方法时,需要调用该函数,并传入错误信息或结果。 + +在注册了处理方法之后,当下载器/解析器遇到带相同扩展名的请求时,会使用对应的处理方式,这些自定义的处理方式将供全局所有加载管线使用。 + +```typescript +assetManager.loadAny({'url': 'http://example.com/myAsset.myformat'}, callback); +``` + +需要注意的是,处理方法可以接收传入的可选参数,开发者可以利用可选参数实现自定义扩展,具体内容可查看文档 [可选参数](options.md#%E6%89%A9%E5%B1%95%E5%BC%95%E6%93%8E)。 diff --git a/versions/4.0/zh/asset/dragonbones.md b/versions/4.0/zh/asset/dragonbones.md new file mode 100644 index 0000000000..59b91f002d --- /dev/null +++ b/versions/4.0/zh/asset/dragonbones.md @@ -0,0 +1,33 @@ +# 骨骼动画资源(DragonBones) + +DragonBones 骨骼动画资源是由 [DragonBones 编辑器](http://dragonbones.com/) 导出的数据格式(支持 DragonBones v5.6.3 及以下)。 + +## 导入 DragonBones 骨骼动画资源 + +DragonBones 骨骼动画资源包括: + +- `.json`/`.dbbin` 骨骼数据 +- `.json` 图集数据 +- `.png` 图集纹理 + + ![DragonBones](dragonbones/import.png) + +## 创建骨骼动画资源 + +在场景中使用 DragonBones 骨骼动画资源需要两个步骤: + +1. 创建节点并添加 DragonBones 组件: + + 从 **资源管理器** 里将骨骼动画资源拖动到已创建 DragonBones 组件的 Dragon Asset 属性中: + + ![DragonBones](dragonbones/set_asset.png) + +2. 为 DragonBones 组件设置图集数据 + + 从 **资源管理器** 里将图集数据拖动到 DragonBones 组件的 Dragon Atlas Asset 属性中: + + ![DragonBones](dragonbones/set_atlas.png) + +## 在项目中的存放 + +为了提高资源管理效率,建议将导入的资源文件存放在单独的目录下,不要和其他资源混在一起。 diff --git a/versions/4.0/zh/asset/dragonbones/import.png b/versions/4.0/zh/asset/dragonbones/import.png new file mode 100644 index 0000000000..2bc6358e3a Binary files /dev/null and b/versions/4.0/zh/asset/dragonbones/import.png differ diff --git a/versions/4.0/zh/asset/dragonbones/set_asset.png b/versions/4.0/zh/asset/dragonbones/set_asset.png new file mode 100644 index 0000000000..5415425b43 Binary files /dev/null and b/versions/4.0/zh/asset/dragonbones/set_asset.png differ diff --git a/versions/4.0/zh/asset/dragonbones/set_atlas.png b/versions/4.0/zh/asset/dragonbones/set_atlas.png new file mode 100644 index 0000000000..404a1197ff Binary files /dev/null and b/versions/4.0/zh/asset/dragonbones/set_atlas.png differ diff --git a/versions/4.0/zh/asset/dynamic-load-resources.md b/versions/4.0/zh/asset/dynamic-load-resources.md new file mode 100644 index 0000000000..7cdba75a18 --- /dev/null +++ b/versions/4.0/zh/asset/dynamic-load-resources.md @@ -0,0 +1,176 @@ +# 加载资源 + +## 动态加载 resources + +通常我们会把项目中需要动态加载的资源放在 `resources` 目录下,配合 `resources.load` 等接口动态加载。你只要传入相对 resources 的路径即可,并且路径的结尾处 **不能** 包含文件扩展名。 + +```typescript +// 加载 Prefab +resources.load("test_assets/prefab", Prefab, (err, prefab) => { + const newNode = instantiate(prefab); + this.node.addChild(newNode); +}); + +// 加载 AnimationClip +resources.load("test_assets/testAnim", AnimationClip, (err, clip) => { + this.node.getComponent(Animation).addClip(clip, "anim"); +}); +``` + +- 所有需要通过脚本动态加载的资源,都必须放置在 `resources` 文件夹或它的子文件夹下。`resources` 文件夹需要在 **assets 根目录** 下手动创建。如下所示: + + ![asset-in-properties-null](load-assets/resources-file-tree.png) + + > **resources** 文件夹中的资源,可以引用文件夹外部的其它资源,同样也可以被外部场景或资源所引用。项目构建时,除了在 **构建发布** 面板中勾选的场景外,**resources** 文件夹中的所有资源,包括它们关联依赖的 **resources** 文件夹外部的资源,都会被导出。 + > + > 如果一份资源仅仅是被 **resources** 中的其它资源所依赖,而不需要直接被 `resources.load` 调用,那么 **请不要** 放在 resources 文件夹中。否则会增大 `config.json` 的大小,并且项目中无用的资源,将无法在构建的过程中自动剔除。同时在构建过程中,JSON 的自动合并策略也将受到影响,无法尽可能合并零碎的 JSON。 + + **注意**:从 v2.4 开始,`loader` 等接口不再建议使用,请使用最新的 `assetManager` 相关接口,升级文档请参考 [资源加载升级指南](asset-manager-upgrade-guide.md)。 + +### 加载 SpriteFrame 或 Texture2D + +图片设置为 sprite-frame 或 texture 或其他图片类型后,将会在 **资源管理器** 中生成一个对应类型的资源。但如果直接加载 `test_assets/image`,得到的类型将会是 `ImageAsset`。你必须指定路径到具体的子资源,才能加载到图片生成的 `SpriteFrame`: + +```typescript +// 加载 SpriteFrame,image 是 ImageAsset,spriteFrame 是 image/spriteFrame,texture 是 image/texture +resources.load("test_assets/image/spriteFrame", SpriteFrame, (err, spriteFrame) => { + this.node.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +```typescript +// 加载 texture +resources.load("test_assets/image/texture", Texture2D, (err: any, texture: Texture2D) => { + const spriteFrame = new SpriteFrame(); + spriteFrame.texture = texture; + this.node.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +> 如果指定了类型参数,就会在路径下查找指定类型的资源。当你在同一个路径下同时包含了多个重名资源(例如同时包含 player.clip 和 player.psd)就需要声明类型。当你需要获取 “子资源”(例如获取 ImageAsset 的子资源 SpriteFrame),就需要指定子资源的路径。 + +### 加载图集中的 SpriteFrame + +对从 TexturePacker 等第三方工具导入的图集而言,如果要加载其中的 SpriteFrame,则只能先加载图集,再获取其中的 SpriteFrame。这是一种特殊情况。 + +```typescript +// 加载 SpriteAtlas(图集),并且获取其中的一个 SpriteFrame +// 注意 atlas 资源文件(plist)通常会和一个同名的图片文件(png)放在一个目录下, 所以需要在第二个参数指定资源类型 +resources.load("test_assets/sheep", SpriteAtlas, (err, atlas) => { + const frame = atlas.getSpriteFrame('sheep_down_0'); + sprite.spriteFrame = frame; +}); +``` + +### 加载 FBX 或 glTF 模型中的资源 + +在将 FBX 模型或 glTF 模型导入编辑器后,会解析出该模型中包含的相关资源如网格,材质,骨骼,动画等,如下图所示: + +![](./load-assets/model.png) + +你可以在运行时动态加载模型中的单一资源,只需指定到某个具体子资源的路径即可,如下所示: + +```typescript +// 加载模型中的网格资源 +resources.load("Monster/monster", Mesh, (err, mesh) => { + this.node.getComponent(MeshRenderer).mesh = mesh; +}); + +// 加载模型中的材质资源 +resources.load("Monster/monster-effect", Material, (err, material) => { + this.node.getComponent(MeshRenderer).material = material; +}); + +// 加载模型中的骨骼 +resources.load("Monster/Armature", Skeleton, (err, skeleton) => { + this.node.getComponent(SkinnedMeshRenderer).skeleton = skeleton; +}); +``` + +### 资源批量加载 + +`resources.loadDir` 可以加载相同路径下的多个资源: + +```typescript +// 加载 test_assets 目录下所有资源 +resources.loadDir("test_assets", function (err, assets) { + // ... +}); + +// 加载 test_assets 目录下所有 SpriteFrame,并且获取它们的路径 +resources.loadDir("test_assets", SpriteFrame, function (err, assets) { + // ... +}); +``` + +## 预加载资源 + +从 v2.4 开始,除了场景能够预加载之外,其他资源也可以预加载。预加载的加载参数与正常加载时一样,不过预加载只会去下载必要的资源,并不会进行资源的反序列化和初始化工作,所以性能消耗更小,适合游戏运行中使用。 + +`resources` 提供了 `preload` 和 `preloadDir` 用于预加载资源。 + +```typescript +resources.preload('test_assets/image/spriteFrame', SpriteFrame); + +// wait for while +resources.load('test_assets/image/spriteFrame', SpriteFrame, (err, spriteFrame) => { + this.node.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +开发者可以使用预加载相关接口提前加载资源,不需要等到预加载结束即可使用正常加载接口进行加载,正常加载接口会直接复用预加载过程中已经下载好的内容,缩短加载时间。 + +关于预加载的说明,请参考 [预加载与加载](preload-load.md)。 + +## 加载远程资源和设备资源 + +在目前的 Cocos Creator 中,我们支持加载远程贴图资源,这对于加载用户头像等需要向服务器请求的贴图很友好,需要注意的是,这需要开发者直接调用 `assetManager.loadRemote` 方法。同时,如果开发者用其他方式下载了资源到本地设备存储中,也需要用同样的 API 来加载,上文中的 `resources.load` 等 API 只适用于应用包内的资源和热更新的本地资源。下面是这个 API 的用法: + +```typescript +// 远程 url 带图片后缀名 +let remoteUrl = "http://unknown.org/someres.png"; +assetManager.loadRemote(remoteUrl, function (err, imageAsset) { + const spriteFrame = new SpriteFrame(); + const texture = new Texture2D(); + texture.image = imageAsset; + spriteFrame.texture = texture; + // ... +}); + +// 远程 url 不带图片后缀名,此时必须指定远程图片文件的类型 +remoteUrl = "http://unknown.org/emoji?id=124982374"; +assetManager.loadRemote(remoteUrl, {ext: '.png'}, function (err, imageAsset) { + const spriteFrame = new SpriteFrame(); + const texture = new Texture2D(); + texture.image = imageAsset; + spriteFrame.texture = texture; + // ... +}); + +// 用绝对路径加载设备存储内的资源,比如相册 +const absolutePath = "/dara/data/some/path/to/image.png"; +assetManager.loadRemote(absolutePath, function (err, imageAsset) { + const spriteFrame = new SpriteFrame(); + const texture = new Texture2D(); + texture.image = imageAsset; + spriteFrame.texture = texture; + // ... +}); + +// 远程音频 +remoteUrl = "http://unknown.org/sound.mp3"; +assetManager.loadRemote(remoteUrl, function (err, audioClip) { + // play audio clip +}); + +// 远程文本 +remoteUrl = "http://unknown.org/skill.txt"; +assetManager.loadRemote(remoteUrl, function (err, textAsset) { + // use string to do something +}); +``` + +目前的此类手动资源加载还有一些限制,对开发者影响比较大的是: + +1. 这种加载方式只支持图片、声音、文本等原生资源类型,不支持 SpriteFrame、SpriteAtlas、TiledMap 等资源的直接加载和解析。(如需远程加载所有资源,可使用 [Asset Bundle](bundle.md)) +2. Web 端的远程加载受到浏览器的 [CORS 跨域策略限制](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS),如果对方服务器禁止跨域访问,那么会加载失败,而且由于 WebGL 安全策略的限制,即便对方服务器允许 http 请求成功之后也无法渲染。 diff --git a/versions/4.0/zh/asset/font.md b/versions/4.0/zh/asset/font.md new file mode 100644 index 0000000000..2b9f7598af --- /dev/null +++ b/versions/4.0/zh/asset/font.md @@ -0,0 +1,64 @@ +# 字体资源 + +使用 Cocos Creator 制作的游戏中可以使用三类字体资源:系统字体,动态字体和位图字体。 + +其中系统字体是通过调用游戏运行平台自带的系统字体来渲染文字,不需要用户在项目中添加任何相关资源。要使用系统字体,请使用 [Label组件](../ui-system/components/editor/label.md) 中的 **Use System Font** 属性。 + +## 导入字体资源 + +### 动态字体 + +目前 Cocos Creator 支持 **TTF** 格式的动态字体。只要将扩展名为 **TTF** 的字体文件拖拽到 **资源管理器** 中,即可完成字体资源的导入。 + +### 位图字体 + +位图字体由 **fnt** 格式的字体文件和一张 **png** 图片组成,fnt 文件提供了对每一个字符小图的索引。这种格式的字体可以由专门的软件生成,请参考: + +- [Glyph Designer](https://71squared.com/glyphdesigner) +- [Hiero](https://github.com/libgdx/libgdx/wiki/Hiero) +- [BMFont (Windows)](http://www.angelcode.com/products/bmfont/) +- [swnob-bmf](https://github.com/SilenceLeo/snowb-bmf) + +在导入位图字体时,请务必将 fnt 文件和 png 文件同时拖拽到 **资源管理器** 中。 + +**请注意,在导入位图字体之后,需要将 png 文件的类型更改为 sprite-frame,否则位图字体将无法正常使用。** + +导入后的字体在 **资源管理器** 中显示如下: + +![imported font asset](font/imported.png) + +**注意** 为了提高资源管理效率,建议将导入的 `fnt` 和 `png` 文件存放在单独的目录下,不要和其他资源混在一起。 + +## 使用字体资源 + +字体资源需要通过 Label 组件来渲染,下面是在场景中创建带有 Label 组件的节点的方法。 + +### 使用菜单创建 Label(字体)节点 + +在 **层级管理器** 中点击左上角的 **创建节点** 按钮,并选择 `创建渲染节点/Label(文字)`,就会在场景中创建出一个带有 **Label** 组件的节点。 + +![from hierarchy](font/create_label.png) + +您也可以通过主菜单的 `节点/创建渲染节点/Label(文字)` 来完成创建,效果和上面的方法一样。 + +![from main menu](font/create_label_main_menu.png) + +### 关联字体资源 + +使用上面方法创建的字体组件默认使用系统字体作为关联的资源,如果想要使用导入到项目中的 TTF 或位图字体,可以将您的字体资源拖拽到创建的 **Label** 组件中的 `Font` 属性栏中。 + +![assign font file](font/assign_font_file.png) + +这时场景中的字体会立刻用刚才指定的字体资源进行渲染。您也可以根据项目需要,自由的切换同一个 **Label** 组件的 `Font` 属性,来使用 TTF 或位图字体。切换字体文件时,Label 组件的其他属性不受影响。 + +如果要恢复使用系统字体,可以点击 `Use System Font` 的属性复选框,来清除 `Font` 属性中指定的字体文件。 + +### 拖拽创建 Label(字体)节点 + +另外一种快捷使用指定资源创建字体节点的方法,是直接从 **资源管理器** 中拖拽字体文件(TTF 或位图字体都可以)到 **层级管理器** 中。和上面用菜单创建的唯一区别,是使用拖拽方式创建的文字节点会自动使用拖拽的字体资源来设置 **Label** 组件的 `Font` 属性。 + + + + diff --git a/versions/4.0/zh/asset/font/assign_font_file.png b/versions/4.0/zh/asset/font/assign_font_file.png new file mode 100644 index 0000000000..afd049c6cd Binary files /dev/null and b/versions/4.0/zh/asset/font/assign_font_file.png differ diff --git a/versions/4.0/zh/asset/font/create_label.png b/versions/4.0/zh/asset/font/create_label.png new file mode 100644 index 0000000000..9b6b4154a4 Binary files /dev/null and b/versions/4.0/zh/asset/font/create_label.png differ diff --git a/versions/4.0/zh/asset/font/create_label_main_menu.png b/versions/4.0/zh/asset/font/create_label_main_menu.png new file mode 100644 index 0000000000..fdfad297c8 Binary files /dev/null and b/versions/4.0/zh/asset/font/create_label_main_menu.png differ diff --git a/versions/4.0/zh/asset/font/imported.png b/versions/4.0/zh/asset/font/imported.png new file mode 100644 index 0000000000..42d072368e Binary files /dev/null and b/versions/4.0/zh/asset/font/imported.png differ diff --git a/versions/4.0/zh/asset/image.md b/versions/4.0/zh/asset/image.md new file mode 100644 index 0000000000..1d6f52b957 --- /dev/null +++ b/versions/4.0/zh/asset/image.md @@ -0,0 +1,49 @@ +# 图像资源 + +图像资源又经常被称作贴图、图片,是游戏中绝大部分图像渲染的数据源。图像资源一般由图像处理软件(比如 Photoshop、Windows 上自带的画图)制作而成并输出成 Cocos Creator 可以使用的文件格式,目前支持 **JPG**、**PNG**、**BMP**、**TGA**、**HDR**、**WEBBP**、**PSD**、**TIFF** 等格式。 + +## 导入图像资源 + +将图像资源直接拖拽到 **资源管理器** 即可将其导入到项目中,之后我们就可以在 **资源管理器** 中看到如下图所示的图像资源: + +![imported](texture/imported.png) + +**属性检查器** 中图像资源的相关属性说明如下: + +| 属性 | 说明 | +| :--- | :--- | +| **useCompressTexture** | 是否使用压缩纹理,详情请参考 [压缩纹理](compress-texture.md)。 | +| **Type** | 用于设置图像资源的类型,包括 **raw**、**texture**(默认)、**normal map**、**sprite-frame**、**texture cube**,具体说明可参考下文 **图像资源类型** 部分的内容。 | +| **Flip Vertical** | 是否沿 X 轴垂直翻转导入的贴图。 | +| **Bake Offline Mimmaps** | 是否烘焙离线 Mipmap | +| **Fix Alpha Transparency Artifacts** | 消除透明伪影
为全透明像素填充相邻像素的颜色,防止纹理过滤引起的黑边问题。当使用 Alpha 透明通道时,请启用此功能。
仅对 **texture**、**raw**、**sprite-frame** 以及 **texture cube** 生效 | +| **Is RGBE** | 是否是 RGBE 压缩格式,该选项仅在 **Type** 为 [立方体贴图](texture-cube.md) 时生效 | +| **Flip Green Channel** | 翻转绿通道
在 Cocos Creator 内使用的法线贴图是 OpenGL 的,如果导入的是基于 DirectX 的法线贴图,如果出现的凹陷的错误,那么勾选此选项可以修复该错误 | + +## 图像资源的类型 + +在 **资源管理器** 面板选中导入的图像资源,在 **属性检查器** 面板中便可根据需要设置图像资源的使用类型,目前支持以下几种: + +![type-change](texture/type-change.png) + +- **raw**:原始图片类型,无作用,用户不需要关心。 + +- **texture**:图像资源类型,也是导入的图像资源的默认类型,详情可参考 [纹理贴图资源](texture.md)。 + +- **normal map**:法线贴图类型,常用于渲染 3D 模型,可在模型材质中勾选 `USE NORMAL MAP` 属性使用。需要注意的是,如果材质中没有定义 `USE NORMAL MAP` 就没有该属性。 + + ![normal-map](texture/normal-map.png) + +- **sprite-frame**:精灵帧资源,用于 2D/UI 制作上,详情可参考 [SpriteFrame](sprite-frame.md)。 + +- **texture cube**:立方贴图类型,使用在全景图上,常用于制作天空盒。详情可参考 [立方体贴图](texture-cube.md)。 + +## 子资源 + +每个图像资源导入后或者设置图像资源类型后,编辑器便会自动在其下方创建相应类型的子资源。在 **资源管理器** 中点击图像资源左侧的三角图标,即可展开查看图像资源的子资源。 + +下图以 texture 类型的图像资源为例: + +![image info](texture/sub-texture.png) + +子资源的详细属性说明请参考对应资源类型的文档说明。**normal map** 的子资源属性与 **texture** 一致,可参考 [纹理贴图资源](texture.md) diff --git a/versions/4.0/zh/asset/index.md b/versions/4.0/zh/asset/index.md new file mode 100644 index 0000000000..84b8080305 --- /dev/null +++ b/versions/4.0/zh/asset/index.md @@ -0,0 +1,42 @@ +# 关于资源 + +本章节将详细介绍 Cocos Creator 中资源的整体工作流程,并对各类资源的使用方法及可能需要注意的地方做出说明。 + +## 资源管理器 + +**资源管理器** 作为访问管理资源的重要工具,开发者在管理资源时推荐先熟悉资源管理器的使用方法,关于资源管理器的详细介绍可见:[资源管理器](../editor/assets/index.md) + +## 资源工作流 + +**资源工作流** 通用的资源工作流程包括导入资源、同步资源、定位资源等,详情请参考 [资源工作流](asset-workflow.md)。 + +## 常见资源类型 + +接下来我们会介绍 Cocos Creator 中的主要资源类型: + +- [场景资源](scene.md) +- [图像资源](image.md) + - [纹理贴图资源](texture.md) + - [精灵帧资源](sprite-frame.md) + - [立方体贴图资源](../concepts/scene/skybox.md#cubemap) + - [图像资源的自动裁剪](../ui-system/components/engine/trim.md) + - [图集资源](atlas.md) + - [渲染纹理](render-texture.md) +- [预制资源](prefab.md) +- [脚本资源](script.md) +- [字体资源](font.md) +- [音频资源](audio.md) +- [材质资源](material.md) +- [模型资源](./model/mesh.md) + - [从第三方工具导出模型资源](./model/dcc-export-mesh.md) + - [glTF 模型](./model/glTF.md) +- [动画资源](../animation/animation-clip.md) +- [Spine 骨骼动画资源](spine.md) +- [DragonBones 骨骼动画资源](dragonbones.md) +- [TiledMap 瓦片图资源](tiledmap.md) +- [JSON 资源](json.md) +- [文本资源](text.md) + +## 资源管理 + +运行时资源管理部分的内容请参考 [Asset Manager](asset-manager.md)。 diff --git a/versions/4.0/zh/asset/json.md b/versions/4.0/zh/asset/json.md new file mode 100644 index 0000000000..57099a6032 --- /dev/null +++ b/versions/4.0/zh/asset/json.md @@ -0,0 +1,64 @@ +# JSON 资源 + +Creator 支持使用 Json 文件,通过 [资源导入](./asset-workflow.md#%E5%AF%BC%E5%85%A5%E8%B5%84%E6%BA%90) 的方式将其导入到编辑器,所有的 JSON 文件都会导入为 `cc.JsonAsset` 格式的资源。 + +## 使用方式 + +开发者可通过 **编辑器挂载** 和 **代码中动态加载** 两种方式获取 Json 数据。 + +### 通过编辑器 + +首先在 **资源管理器** 中新建一个 TypeScript,脚本内容示例如下: + +```ts + +import { _decorator, Component, JsonAsset } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('ItemTemplate') +export class ItemTemplate extends Component { + + // 声明属性 ‘itemGiftJson‘ 的类型为 JsonAsset + @property(JsonAsset) + itemGiftJson: JsonAsset = null!; + + start () { + + // 获取到 Json 数据 + const jsonData: object = this.itemGiftJson.json!; + + } +} + +``` + +保存脚本内容后回到编辑器,将脚本挂载到相应的节点上,然后将 **资源管理器** 中的 Json 资源拖拽到脚本组件相应的属性框中。例如下图: + +![itemGift](json/json.png) + +### 通过代码动态加载 + +开发者也可以直接通过代码 [动态加载](./dynamic-load-resources.md#%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD-resources) 来获取 Json 数据,代码示例如下: + +```ts +import { _decorator, Component, JsonAsset, resources, error } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('ItemTemplate') +export class ItemTemplate extends Component { + + start () { + + resources.load('gameGiftJson', (err: any, res: JsonAsset) => { + if (err) { + error(err.message || err); + return; + } + // 获取到 Json 数据 + const jsonData: object = res.json!; + + }) + + } +} +``` diff --git a/versions/4.0/zh/asset/json/json.png b/versions/4.0/zh/asset/json/json.png new file mode 100644 index 0000000000..37a0a05fdc Binary files /dev/null and b/versions/4.0/zh/asset/json/json.png differ diff --git a/versions/4.0/zh/asset/label-atlas.md b/versions/4.0/zh/asset/label-atlas.md new file mode 100644 index 0000000000..6a604761ca --- /dev/null +++ b/versions/4.0/zh/asset/label-atlas.md @@ -0,0 +1,37 @@ +# 艺术数字资源 (LabelAtlas) + +**艺术数字资源** 是一种用户自定义的资源,它可以用来配置艺术数字字体的属性。 + +## 创建艺术数字资源 + +在 **资源管理器** 中点击右键,然后选择 **创建 -> 艺术数字配置**,或者点击 **资源管理器** 左上角的加号按钮: + +![create label atlas](label-atlas/create-label-atlas.png) + +即可新建一个艺术数字资源: + +![label atlas](label-atlas/label-atlas.png) + +## 属性 + +| 属性 | 功能说明 +| :---------- | :---------- +| SpriteFrame | 设置事先绘制好的包含所需字体样式的图片 +| Item Width | 指定每一个字符的宽度 +| Item Height | 指定每一个字符的高度 +| Start Char | 指定艺术数字字体里面的第一个字符,如果字符是空格,也需要在这个属性里面输入空格字符 +| Font Size | 显示每个字符的大小,由 **Item Width** 和 **Item Height** 自动计算得出,不可编辑 + +**艺术数字资源** 在使用之前需要先配置包含所需字体样式的 SpriteFrame 贴图,如下图: + +![raw_texture_file](label-atlas/raw_texture_file.png) + +然后将其拖拽到 **艺术数字资源** 的 **SpriteFrame** 属性框中: + +![sprite-frame](label-atlas/save-label-atlas.png) + +设置完成后请点击右上角的绿色打钩按钮保存。 + +## 使用艺术数字资源 + +使用艺术数字资源只需要在 **层级管理器** 中新建一个 Label 节点,然后在 **属性检查器** 中将新建好的艺术数字资源拖拽到节点 Label 组件的 Font 属性上即可。详情可参考 [Label 组件](../ui-system/components/editor/label.md)。 diff --git a/versions/4.0/zh/asset/label-atlas/create-label-atlas.png b/versions/4.0/zh/asset/label-atlas/create-label-atlas.png new file mode 100644 index 0000000000..288f02a86b Binary files /dev/null and b/versions/4.0/zh/asset/label-atlas/create-label-atlas.png differ diff --git a/versions/4.0/zh/asset/label-atlas/label-atlas.png b/versions/4.0/zh/asset/label-atlas/label-atlas.png new file mode 100644 index 0000000000..a9082762b4 Binary files /dev/null and b/versions/4.0/zh/asset/label-atlas/label-atlas.png differ diff --git a/versions/4.0/zh/asset/label-atlas/raw_texture_file.png b/versions/4.0/zh/asset/label-atlas/raw_texture_file.png new file mode 100644 index 0000000000..ff58433135 Binary files /dev/null and b/versions/4.0/zh/asset/label-atlas/raw_texture_file.png differ diff --git a/versions/4.0/zh/asset/label-atlas/save-label-atlas.png b/versions/4.0/zh/asset/label-atlas/save-label-atlas.png new file mode 100644 index 0000000000..2c3152a36c Binary files /dev/null and b/versions/4.0/zh/asset/label-atlas/save-label-atlas.png differ diff --git a/versions/4.0/zh/asset/load-assets/asset-dep.png b/versions/4.0/zh/asset/load-assets/asset-dep.png new file mode 100644 index 0000000000..94dd9241b9 Binary files /dev/null and b/versions/4.0/zh/asset/load-assets/asset-dep.png differ diff --git a/versions/4.0/zh/asset/load-assets/model.png b/versions/4.0/zh/asset/load-assets/model.png new file mode 100644 index 0000000000..6f6faf9ccc Binary files /dev/null and b/versions/4.0/zh/asset/load-assets/model.png differ diff --git a/versions/4.0/zh/asset/load-assets/resources-file-tree.png b/versions/4.0/zh/asset/load-assets/resources-file-tree.png new file mode 100644 index 0000000000..d44fcfdfe0 Binary files /dev/null and b/versions/4.0/zh/asset/load-assets/resources-file-tree.png differ diff --git a/versions/4.0/zh/asset/material.md b/versions/4.0/zh/asset/material.md new file mode 100644 index 0000000000..2b288d07ba --- /dev/null +++ b/versions/4.0/zh/asset/material.md @@ -0,0 +1,86 @@ +# 材质资源 + +## 材质创建 + +在 **资源管理器** 面板中点击右键并选择 **创建 -> 材质**: + +![material-create](material/material-create.png) + +或者点击 **资源管理器** 左上角的 **+** 号按钮并选择 **材质**: + +![material-create-menu](material/material-create-menu.png) + +此时便会在 **资源管理器** 中创建一个默认名为 **material** 的材质资源: + +![default-material](material/default-material.png) + +材质控制着每个模型最终的着色,材质由着色器构成,通过材质和着色器控制最终的着色流程。 + +## 材质资源属性 + +| 属性 | 说明 | +| :-- | :-- | +| Effect(着色器) | 当前材质所使用的着色器,默认使用的是内置 PBR 着色器 [builtin-standard.effect](../shader/effect-builtin-pbr.md)。
点击 Effect 的下拉框,可以看到当前项目中所有的着色器,开发者可根据需要选择使用。当切换了着色器后其他属性也会同步更新,详情请参考 [着色器](../shader/effect-inspector.md)。
点击右侧的 ![image](../material-system/img/locate.png) 按钮,会在 **资源管理器** 中定位当前使用的着色器。| +| Technique | Technique 下拉框会列出当前使用的着色器中所有的 Technique。一个着色器中可能会存在多个 Technique,每个 Technique 适用于不同的情况,例如效果差一点但是性能更好的 Technique 更适合用于手机平台。当切换了 Technique 后 Pass 列表也会同步更新。 | +| USE INSTANCING | 是否启用动态 Instancing。需要注意的是,Instancing 只应该在场景中有大量相同模型的实例时启用,适当合理地应用 Instancing 可以有不错的性能提升,但过度使用反而很可能会因为额外的开销维护导致性能下降 | +| USE BATCHING | 是否启用动态 VB 合并式合批。
在 v3.6.2 中编辑器面板上已移除该选项,如果要合批请使用 **USE_INSTANCING** 选项 | +| Pass | Pass 列表会列出当前使用的 Technique 中所有的 Pass(例如 Pass 0、Pass 1、Pass 2......)。
每个 Pass 可能会有不同的属性和定义,开发者可以分别设置这些属性和定义。如果属性是被定义包裹住的,需要先勾上定义才能看到对应的属性。详情请参考 [Pass 可选配置参数](../shader/pass-parameter-list.md) | + +在 **属性检查器** 面板右上方还可以选择其他的模型以预览效果: + +![image](../material-system/img/preview-model-select.png) + +**属性检查器** 会对当前材质的用户数据进行缓存,用户数据是指现在修改的或者之前修改的数据,当切换 `Effect` 或 `Technique` 时,缓存的数据会进行迁移,使用户数据得到维持。 + +若修改了材质属性,面板右上角会出现 ![image](../material-system/img/save-material.png) 保存和 ![image](../material-system/img/revert-material.png) 重置按钮。重置会将材质属性回退到上一次保存时的设置,而一旦点击保存后便无法再重置。 + +材质编辑过程中,支持撤销(undo)和重做(redo)的快捷键操作。 +在 **属性检查器** 面板获得焦点的情况下, +- undo :Ctrl/Cmd + Z +- redo :Ctrl/Cmd + Shift + Z + +## 使用材质 + +一般情况下,3D 模型、2D 对象以及粒子系统都需要指定材质才可以正确渲染出物体表面的外观。 + +### 在 3D 模型中使用 + +通过 [网格/蒙皮网格/批量蒙皮网格渲染器组件](../module-map/mesh/) 上的 `Materials` 属性,我们可以指定当前 3D 模型所使用的材质。点击 `Materials` 属性框右侧的箭头图标按钮,可以看到当前项目中所有的材质资源,根据需要选择即可。或者也可以将所需的材质资源从 **资源管理器** 直接拖拽到 `Materials` 属性框中。 + +#### 导出模型资源中的材质 + +通常 [模型资源](./model/mesh.md) 都是由第三方工具制作并导出,然后导入到 Creator 中使用的(支持 FBX 和 glTF 格式)。这些模型可能带有材质资源,可在 **资源管理器** 内展开模型文件进行查看。 + +![fbx-mat](../material-system/img/readonly-material.png) + +这些材质可以直接在模型文件内进行编辑,也可以提取后进行编辑。 + +若要提取这些材质,在 **资源管理器** 中选中模型资源,然后在 **属性检查器** 的 **材质** 分页中勾选 **提取材质**,并设置 **材质提取目录**,最后点击右上角的绿色打钩按钮,即可将模型资源的材质提取到指定目录。详情请参考 [模型资源 - Material 模块](./model/mesh.md#material-%E6%A8%A1%E5%9D%97)。 + +![导出模型](../material-system/img/dump-material.png) + +材质提取完成后,会自动和模型节点的网格渲染器组件中的 `Materials` 属性绑定。例如: + +材质提取前: + +![材质提取前](../material-system/img/dump-result.png) + +材质提取后: + +![材质提取后](../material-system/img/post-dump.png) + +### 在 2D 以及 UI 渲染组件中使用 + +2D 对象(包括 2D 渲染和 UI 系统)默认情况下只支持一个单独的自定义材质。若留空则会使用引擎内置的标准材质。 + +若要进行自定义,在组件 **属性检查器上**,通过选择 `Custom Material` 的下拉框选择相应的材质。 + +![ui-select-mat](../material-system/img/ui-select.png) + +### 在粒子系统中使用 + +粒子系统渲染模块 Renderer 中的 **ParticleMaterial** 和 **TrailMaterial** 属性分别用于渲染粒子材质和粒子拖尾,将所需的材质资源从 **资源管理器** 直接拖拽到 **渲染器(Renderer)** 相应的属性框中即可。 + +![img-particle](../material-system/img/particle-material.png) + +详情请参考 [粒子渲染模块](../particle-system/renderer.md)。 diff --git a/versions/4.0/zh/asset/material/default-effect.png b/versions/4.0/zh/asset/material/default-effect.png new file mode 100644 index 0000000000..19222d0ff5 Binary files /dev/null and b/versions/4.0/zh/asset/material/default-effect.png differ diff --git a/versions/4.0/zh/asset/material/default-material.png b/versions/4.0/zh/asset/material/default-material.png new file mode 100644 index 0000000000..c7e15c179f Binary files /dev/null and b/versions/4.0/zh/asset/material/default-material.png differ diff --git a/versions/4.0/zh/asset/material/effect-create.png b/versions/4.0/zh/asset/material/effect-create.png new file mode 100644 index 0000000000..fe8631e40f Binary files /dev/null and b/versions/4.0/zh/asset/material/effect-create.png differ diff --git a/versions/4.0/zh/asset/material/effect-show.png b/versions/4.0/zh/asset/material/effect-show.png new file mode 100644 index 0000000000..424b850f31 Binary files /dev/null and b/versions/4.0/zh/asset/material/effect-show.png differ diff --git a/versions/4.0/zh/asset/material/effects.png b/versions/4.0/zh/asset/material/effects.png new file mode 100644 index 0000000000..3e51d2acc5 Binary files /dev/null and b/versions/4.0/zh/asset/material/effects.png differ diff --git a/versions/4.0/zh/asset/material/material-create-menu.png b/versions/4.0/zh/asset/material/material-create-menu.png new file mode 100644 index 0000000000..d0462977ca Binary files /dev/null and b/versions/4.0/zh/asset/material/material-create-menu.png differ diff --git a/versions/4.0/zh/asset/material/material-create.png b/versions/4.0/zh/asset/material/material-create.png new file mode 100644 index 0000000000..b570d74ff2 Binary files /dev/null and b/versions/4.0/zh/asset/material/material-create.png differ diff --git a/versions/4.0/zh/asset/meta.md b/versions/4.0/zh/asset/meta.md new file mode 100644 index 0000000000..8f4b74c99d --- /dev/null +++ b/versions/4.0/zh/asset/meta.md @@ -0,0 +1,141 @@ +# 资源管理注意事项 --- meta 文件 + +> 本文全文转载自 [微信公众号:奎特尔星球](https://mp.weixin.qq.com/s/MykJaytb3t_oacude1cvIg),转载前已获得作者授权 +> 作者:ShawnZhang + +Cocos Creator 会为 assets 目录下的每一个文件和目录生成一个同名的 meta 文件,相信大家一定不会太陌生。理解 Creator 生成 meta 文件的作用和机理,能帮助您和您的团队解决在多人开发时常会遇到的资源冲突、文件丢失、组件属性丢失等问题。那 meta 文件是做什么用的呢?下面我们来了解一下。 + +![missingscript](meta/missingscript.png) + +## meta 文件的作用 + +先来看下场景中的 meta 文件长什么样子: + +```json +{ + "ver": "1.0.0", // 版本 + "uuid": "911560ae-98b2-4f4f-862f-36b7499f7ce3", // 全局唯一 id + "asyncLoadAssets": false, // 异步加载 + "autoReleaseAssets": false, // 自动释放资源 + "subMetas": {} // 子元数据 +} +``` + +预制件的 meta 文件与场景是一样的。我们再来看一下 png 图片的 meta 文件: + +```json +{ + "ver": "1.0.0", + "uuid": "19110ebf-4dda-4c90-99d7-34b2aef4d048", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "subMetas": { + "img_circular": { + "ver": "1.0.3", + "uuid": "a2d1f885-6c18-4f67-9ad6-97b35f1fcfcf", + "rawTextureUuid": "19110ebf-4dda-4c90-99d7-34b2aef4d048", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 100, + "height": 100, + "rawWidth": 100, + "rawHeight": 100, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} +``` + +png 图片的 meta 文件信息比较多,除了基本的 `ver` 和 `uuid` 外,还记录了图片的宽高、偏移、九宫格等数据。上面这么多信息,我们这里只关心一个:**UUID**。 + +> UUID: 通用唯一标识符(Universally Unique Identifier) + +UUID 是 Creator 用来管理游戏资源的。它会为每个文件分配一个唯一的 id,图集会生成多个。由此可以了解在 Creator 引擎中,识别一个文件不是简单地通过 `路径 + 文件名` 定位,而是通过 UUID 来引用文件。因此可以在编辑器资源管理中,随意删除、移动文件。 + +## meta 文件更新时机 + +Creator 生成 meta 文件有以下几种情况: + +**1、打开工程时** + +打开项目工程时,Creator 会先扫描 assets 目录,如果哪个文件还没有 meta 文件,此时就会生成。 + +**2、更新资源时** + +更新资源也会引发 meta 文件的更新: + +- 在 **资源管理器** 中可以对资源进行文件名修改、改变目录、删除文件,添加文件等操作,请参考 [资源管理器](../getting-started/basics/editor-panels/assets.md)。也可以直接从桌面或操作系统的文件管理器中将文件拖入到 **资源管理器** 中。 + + ![add](meta/add.png) + +- 还有一种情况是在操作系统的文件管理器中对 assets 目录中的文件进行增、删、改之后切换到编辑器界面,此时可以看到 **资源管理器** 刷新的过程。 + + ![refresh](meta/refresh.png) + +如果一个文件的 meta 文件不存在,上面两种情况都会触发引擎去生成 meta 文件。 + +## meta 文件出错的几种情况及解决方法 + +下面我们分析下 meta 文件出错的几种可能情况。 + +### UUID 冲突 + +UUID 是全局唯一的,产生冲突肯定是有不同的文件的 UUID 相同了,一旦出现这个问题会导致 Cocos Creator 资源管理器目录结构加载不完整。如下图所示,遇到这种情况估计会让你吓出一身冷汗: + +![conflict](meta/conflict.png) + +从提示中可以看到冲突的 UUID 字符串,然后打开操作系统文件管理或代码编辑器,搜索这个 UUID: + +![search_uuid](meta/search_uuid.png) + +此时先关闭 Creator 编辑器,再任意删除其中一个 meta 文件,然后再打开 Creator 编辑器就可以解决。 + +这种方法虽然可以解决问题,但在编辑器中引用到这个资源的地方将会出现资源丢失,需要重新编辑或者重新配置一次。最好是通过版本管理工具还原此 meta 文件。 + +出现这种问题的原因一般有以下两个: + +- 在操作系统的文件管理器中移动文件时,将剪切、粘贴不小心操作成了复制、粘贴,同时也把 meta 文件复制过去了。导致项目中同时出现两个相同的 meta 文件。 + +- 在多人协作时,从版本管理工具中,更新资源时碰巧遇到别人生成的 UUID 与你的电脑上某个文件生成的 UUID 一样了,但这种情况非常非常罕见。 + +总的来说,要减少 UUID 冲突发生,最好在引擎资源管理工具中进行添加、移动文件。 + +### UUID 变化 + +还有一种情况是 UUID 变了,使得旧的 UUID 对应的资源无法找到,这样的话,你曾经编辑的界面将会出现资源、图片丢失,还可能出现组件属性丢失。 + +![lost](meta/lost.png) + +如果找不到旧的 UUID 对应的资源,通过 **控制台** 可以看到 Creator 给出了所在的场景文件名、节点路径、组件、UUID 等非常详细的警告信息。通过警告信息可以快速定位出错的地方。 + +这种情况又是怎么造成的呢?当有一个人将新资源添加进项目时,忘记切换到编辑器界面使其生成 meta 文件,同时又将这些新增的文件提交到了版本管理中(不包含 meta 文件)。然后,有另一个人去更新了他所提交的资源,同时切换到了编辑器界面进行编辑,这时 Creator 会检查到新资源没有 meta 文件便会立即生成。而当第一个人切换到编辑器的时候也会生成 meta 文件,这样两个人的电脑上为同一个文件,但是生成的 meta 文件中的 UUID 都不相同。 + +这种情况下,后面进行资源提交或更新的人,肯定也会遇到冲突,如果不明就理就强行解决冲突,就会产生上面所说的问题。下面的时序图就描述了这种错误的工作流程: + +![resources](meta/resources.png) + +因为第一个 A 同学忘记生成 meta 文件并提交,之后其他人都编辑过项目,但每个人生成的 UUID 都不同,这样就会陷入无限的资源出错中,编辑好的东西,一提交更新又出现冲突了。 + +要解决这个问题注意下面几点: + +- 提交前检查是否有新增文件,有新增文件时,注意是否有 meta 文件,需要一起提交; + +- 拉取文件时,注意是否有新增文件,并且是有 meta 文件成对,如果没有的话,提醒之前提交文件的同学,把 meta 文件一并提交; + +- 提交时,如果发现只有新增的 meta 文件,那这个 meta 文件肯定是自己生成的,需要注意是否使用过这个 meta 文件对应的资源(同名文件)。如果没用过,那请最早提交者把 meta 文件提交了。千万不能将这个 meta 文件提交上去。 + +注意上面几点基本上就可以杜绝 meta 文件 UUID 变化导致的工程出错了。 + +## 小结 + +meta 文件是 Creator 用于资源管理的重要手段,但在多人协同开发中稍有不慎就容易产生资源错误。要解决这个问题,不仅需要理解 meta 文件的产生机制和导致冲突的原因,同时还应该规范资源提交流程。 diff --git a/versions/4.0/zh/asset/meta/add.png b/versions/4.0/zh/asset/meta/add.png new file mode 100644 index 0000000000..a5d0bc042e Binary files /dev/null and b/versions/4.0/zh/asset/meta/add.png differ diff --git a/versions/4.0/zh/asset/meta/conflict.png b/versions/4.0/zh/asset/meta/conflict.png new file mode 100644 index 0000000000..ab79e55cbc Binary files /dev/null and b/versions/4.0/zh/asset/meta/conflict.png differ diff --git a/versions/4.0/zh/asset/meta/lost.png b/versions/4.0/zh/asset/meta/lost.png new file mode 100644 index 0000000000..f2859768c7 Binary files /dev/null and b/versions/4.0/zh/asset/meta/lost.png differ diff --git a/versions/4.0/zh/asset/meta/missingscript.png b/versions/4.0/zh/asset/meta/missingscript.png new file mode 100644 index 0000000000..e6a8c96a2c Binary files /dev/null and b/versions/4.0/zh/asset/meta/missingscript.png differ diff --git a/versions/4.0/zh/asset/meta/refresh.png b/versions/4.0/zh/asset/meta/refresh.png new file mode 100644 index 0000000000..bfa510ff46 Binary files /dev/null and b/versions/4.0/zh/asset/meta/refresh.png differ diff --git a/versions/4.0/zh/asset/meta/resources.png b/versions/4.0/zh/asset/meta/resources.png new file mode 100644 index 0000000000..ccedd691eb Binary files /dev/null and b/versions/4.0/zh/asset/meta/resources.png differ diff --git a/versions/4.0/zh/asset/meta/search_uuid.png b/versions/4.0/zh/asset/meta/search_uuid.png new file mode 100644 index 0000000000..25ab061545 Binary files /dev/null and b/versions/4.0/zh/asset/meta/search_uuid.png differ diff --git a/versions/4.0/zh/asset/model/dcc-export-mesh.md b/versions/4.0/zh/asset/model/dcc-export-mesh.md new file mode 100644 index 0000000000..7a6595d5a6 --- /dev/null +++ b/versions/4.0/zh/asset/model/dcc-export-mesh.md @@ -0,0 +1,50 @@ +# 导入从 DCC 工具导出的模型 + +目前大多数数字内容制作(Digital Content Creation, DCC)工具([3ds Max](max-export-fbx.md)、[Maya](maya-export-fbx.md)、Blender)都能导出 **FBX** 和 **glTF** 这两种格式的模型文件,所以这些工具导出的内容都能在 Cocos Creator 中得到良好的展示。 + +## 导出 FBX + +因为 DCC 工具的坐标系和游戏引擎的坐标系可能不一致,所以在导出模型时需要进行一些变换才能在引擎中得到想要的结果。例如:Blender 的坐标系为 X 轴朝右,Y 轴朝里,Z 轴朝上,而 Cocos Creator 3.x 的坐标系为 X 轴朝右,Y 轴朝上,Z 轴朝外,所以需要调整旋转才能使得轴向一致。 + +以下以 Blender 2.8 作为例子,介绍模型的导入流程,首先我们在 Blender 中创建一个模型。 + +![blender model](./mesh/blender_model.png) + +在 [Blender 的 FBX 导出选项](https://docs.blender.org/manual/zh-hans/2.80/addons/io_scene_fbx.html) 中,我们选择 Up 为 Y Up,Forward 为 -Z Forward。 + +![blender export](./mesh/blender_export_fbx_1.png) + +导入到 Cocos Creator 中,可以看到节点在 X 轴做了 -90 的旋转,以便将轴和 Creator 的对齐。 + +![blender export c3d](./mesh/blender_model_c3d.png) + +如果不想要这个旋转值,Blender 的 FBX 导出插件提供了一个实验性功能(Apply Transform),可以将旋转数据直接变换到模型的顶点数据中。 + +![blender export bake](./mesh/blender_export_bake.png) + +返回编辑器,在 **属性检查器** 中可以看到旋转数据没有了: + +![blender export bake c3d](./mesh/blender_model_bake_c3d.png) + +## 导出 glTF + +[glTF 使用的也是右手坐标系](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#coordinate-system-and-units),Blender 的 [导出 glTF 的选项](https://docs.blender.org/manual/zh-hans/2.80/addons/io_scene_gltf2.html) 比较简单,只要把 +Y Up 选项勾上就可以了,导出的数据中也没有旋转值。 + +![blender export glTF](./mesh/blender_export_gltf.png) + +## 朝向问题 + +游戏开发过程中可能会需要用到模型的朝向,例如想要一些物体面向玩家(使用了 LookAt 方法),这时就需要考虑模型的初始朝向,这里提供两种方法来调整模型的初始朝向。 + +1. Cocos Creator 3.x 是以 -Z 轴做为正前方的朝向,而在 Blender 中正前方朝向为 +Y 轴,所以在制作模型时需要以 Y 轴正方向做为物体的朝向,经过导出的变换后,在 Creator 中就会是以 -Z 轴做为正前方的朝向。 +2. 如果不想在 DCC 工具中改变朝向,可以在场景中尝试为导入的模型增加一个父节点,然后旋转模型以使得模型的初始朝向为 -Z 轴,之后的各种旋转相关的操作都以父节点为操作对象。 + +## 美术制作规范说明 + +1. 合理制定模型资源下的子资源命名(例如:网格、材质),每修改一次子资源命名就会导致项目内关联该子资源的地方出现丢失。 + +2. 当模型有一部分需要透明,一部分不需要透明时,应该分两个材质导出。如果是一个材质导出容易出现模型穿透现象,需要手动调整材质。 + +3. 外部资源引用,导出的时候使用相对路径。否则,在多人合作下,会识别不到原资源路径,导致模型内置材质无法正确获取到贴图而呈现黄色。3ds Max 导出本地路径修改方式如下: + + ![relative path](./mesh/relative_path.png) diff --git a/versions/4.0/zh/asset/model/glTF.md b/versions/4.0/zh/asset/model/glTF.md new file mode 100644 index 0000000000..9effd67b99 --- /dev/null +++ b/versions/4.0/zh/asset/model/glTF.md @@ -0,0 +1,163 @@ +# glTF 模型 + +Cocos Creator 支持 glTF 2.0 及更早的文件格式。 + +## URI 解析 + +Creator 支持 glTF 中指定以下形式的 URI: + +- Data URI + +- 相对 URI 路径 + +- 文件 URL + +- 文件路径 + +## 转换关系 + +当导入 glTF 模型到 Creator 时,glTF 中的资源将会按照以下关系转换为 Creator 中的资源: + +| glTF 资源 | Cocos Creator 资源 | +| :---------- | :---------------- | +| [glTF 场景](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-scene) | 预制体 | +| [glTF 网格](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-mesh) | 网格 | +| [glTF 蒙皮](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-skin) | 骨骼 | +| [glTF 材质](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-material) | 材质 | +| [glTF 贴图](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-texture) | 贴图 | +| [glTF 图像](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-image) | 图像 | +| [glTF 动画](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-animation) | 动画剪辑 | + +### glTF 场景 + +导入后,glTF 场景将转换为 Creator 中的预制体资源,glTF 场景中递归包含的节点也将按照相同层级关系一一转换为预制体中的节点。 + +#### 场景根节点 + +预制体将使用一个不带任何空间转换信息的节点作为根节点,glTF 场景的所有 [根节点](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenenodes) 将作为该节点的子节点。 + +#### 节点转换 + +glTF 节点中的属性将按照下表中的映射关系转换为预制体节点中的属性: + +| glTF 节点属性 | 预制体节点属性 | +| :----------- | :----------- | +| [层级关系](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodechildren) | 层级关系 | +| [位移](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodetranslation) | 位置 | +| [旋转](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#noderotation) | 旋转 | +| [缩放](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodescale) | 缩放 | +| [矩阵](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodematrix) | 解压,并分别设置位置、旋转、缩放 | +| [网格引用](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodemesh) | 网格渲染器组件 | +| [蒙皮引用](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodeskin) | 蒙皮网格渲染器组件 | +| [初始权重](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodeweights) |(蒙皮)网格渲染器组件权重 | + +#### 网格渲染器 + +若 glTF 节点引用了网格,那么导入后相对应的预制体节点也会添加网格渲染组件(MeshRenderer)。若该 glTF 节点还引用了蒙皮,那么相对应的预制体节点还会添加蒙皮网格渲染组件(SkinnedMeshRenderer)。 + +(蒙皮)网格渲染组件中的网格、骨骼和材质,都会与转换后的 glTF 网格、蒙皮、材质资源一一对应。 + +若 glTF 节点指定了初始权重,则转换后的(蒙皮)网格渲染器也将带有此权重。 + +### glTF 网格 + +导入后,glTF 网格将转换为 Cocos Creator 中的网格资源。 + +glTF 网格中的所有 [基元体](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#meshprimitives-white_check_mark) 将被一一转换为 Creator 中的子网格。 + +若 glTF 网格指定了 [权重](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#meshweights),则相应地,转换后的 Creator 网格中也将存储相应的权重。 + +#### glTF 基元体 + +glTF 基元体的索引数组将一一对应转换为 Cocos Creator 子网格的索引数组。 + +glTF 基元模式将按照下表中的映射关系转换为 Cocos Creator 基元模式: + +| glTF 基元模式 | Cocos Creator 基元模式 | +| :--------------- | :------------------------------- | +| `POINTS` | `gfx.PrimitiveMode.POINT_LIST` | +| `LINES` | `gfx.PrimitiveMode.LINE_LIST` | +| `LINE_LOOP` | `gfx.PrimitiveMode.LINE_LOOP` | +| `LINE_STRIP` | `gfx.PrimitiveMode.LINE_STRIP` | +| `TRIANGLES` | `gfx.PrimitiveMode.TRIANGLE_LIST` | +| `TRIANGLE_STRIP` | `gfx.PrimitiveMode.TRIANGLE_STRIP` | +| `TRIANGLE_FAN` | `gfx.PrimitiveMode.TRIANGLE_FAN` | + +glTF 顶点属性将转换为 Cocos Creator 顶点属性,属性名称的转换如下表所示: + +| glTF 顶点属性名称 | Cocos Creator 顶点属性名称 | +| :------------------------ | :----------------------------------------------------------------------- | +| `POSITION` | `gfx.AttributeName.ATTR_POSITION` | +| `NORMAL` | `gfx.AttributeName.ATTR_NORMAL` | +| `TANGENT` | `gfx.AttributeName.ATTR_TANGENT` | +| `TEXCOORD_0` | `gfx.AttributeName.ATTR_TEX_COORD` | +| `TEXCOORD_1`..`TEXCOORD_8` | `gfx.AttributeName.ATTR_TEX_COORD1`..`gfx.AttributeName.ATTR_TEX_COORD8` | +| `COLOR_0` | `gfx.AttributeName.ATTR_COLOR` | +| `COLOR_1`..`COLOR_2` | `gfx.AttributeName.ATTR_COLOR1`..`gfx.AttributeName.ATTR_COLOR2` | +| `JOINTS_0` | `gfx.AttributeName.ATTR_JOINTS` | +| `WEIGHTS_0` | `gfx.AttributeName.ATTR_WEIGHTS` | + +> **注意**:若 glTF 基元体中存在其他 `JOINTS`、`WEIGHTS` 顶点属性,例如 `JOINTS_1`、`WEIGHTS_1`,则意味着此 glTF 网格的顶点可能受到多于 4 根骨骼的影响。 + +对于每个顶点,所有由 `JOINTS_{}`、`WEIGHTS_{}` 确定的权重信息将按权重值进行排序,取出影响权重最大的四根骨骼作为 `gfx.AttributeName.ATTR_JOINTS` 和 `gfx.AttributeName.ATTR_WEIGHTS`。 + +glTF 形变目标将被转换为 Cocos Creator 子网格形变数据。 + +### glTF 蒙皮 + +导入后,glTF 蒙皮将转换为 Cocos Creator 中的骨骼资源。 + +### glTF 材质 + +导入后,glTF 材质将转换为 Cocos Creator 中的材质资源。 + +### glTF 贴图 + +导入后,glTF 贴图将转换为 Cocos Creator 中的贴图资源。 + +glTF 贴图中引用的 glTF 图像将转换为对相应转换后的 Cocos Creator 图像的引用。 + +glTF 贴图属性将按照下表中的映射关系转换为 Cocos Creator 贴图属性: + +| glTF 贴图属性 | Cocos Creator 贴图属性 | +| :----------------------- | :----------------------- | +| [放大筛选器](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplermagfilter) | 放大筛选器 | +| [缩小筛选器](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplerminfilter) | 缩小筛选器、Mip Map 筛选器 | +| [S 环绕模式](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplerwraps) | S 环绕模式 | +| [T 环绕模式](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplerwrapt) | 环绕模式 | + +glTF 贴图放大筛选器将按照下表中的映射关系转换为 Cocos Creator 贴图放大筛选器: + +| glTF 贴图放大筛选器 | Cocos Creator 贴图放大筛选器 | +| :---------------- | :--------------------------- | +| `NEAREST` | `TextureBase.Filter.NEAREST` | +| `LINEAR` | `TextureBase.Filter.LINEAR` | + +glTF 贴图缩小筛选器将按照下表中的映射关系转换为 Cocos Creator 贴图缩小筛选器和 Cocos Creator 贴图 Mip Map 筛选器: + +| glTF 贴图缩小筛选器 | Cocos Creator 贴图缩小筛选器 | Cocos Creator 贴图 Mip Map 筛选器 | +|:------------------------ | :--------------------------- | :-------------------------------- | +| `NEAREST` | `TextureBase.Filter.NEAREST` | `TextureBase.Filter.NONE` | +| `LINEAR_MIPMAP_LINEAR` | `TextureBase.Filter.LINEAR` | `TextureBase.Filter.LINEAR` | +| `LINEAR_MIPMAP_NEAREST` | `TextureBase.Filter.LINEAR` | `TextureBase.Filter.NEAREST` | +| `LINEAR` | `TextureBase.Filter.LINEAR` | `TextureBase.Filter.NONE` | +| `NEAREST_MIPMAP_LINEAR` | `TextureBase.Filter.NEAREST` | `TextureBase.Filter.LINEAR` | +| `NEAREST_MIPMAP_NEAREST` | `TextureBase.Filter.NEAREST` | `TextureBase.Filter.NEAREST` | + +glTF 贴图环绕模式将按照下表中的映射关系转换为 Cocos Creator 贴图环绕模式: + +| glTF 贴图环绕模式 | Cocos Creator 贴图环绕模式 | +| :---------------- | :------------------------------------- | +| `CLAMP_TO_EDGE` | `TextureBase.WrapMode.CLAMP_TO_EDGE` | +| `REPEAT` | `TextureBase.WrapMode.REPEAT` | +| `MIRRORED_REPEAT` | `TextureBase.WrapMode.MIRRORED_REPEAT` | + +### glTF 图像 + +导入后,glTF 图像将转换为 Cocos Creator 中的图像资源。 + +当 glTF 图像的 [URI](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#imageuri) 是 Data URI 时,图像数据将从 Data URI 中获取。否则,将根据 [Cocos Creator 图像位置解析算法](./image-location-resolution.md) 解析并引用外部图像文件,其中 `url` 就是 glTF 图像的 URI,`startDir` 为 glTF 文件所在目录。 + +### glTF 动画 + +导入后,glTF 动画将转换为 Cocos Creator 动画资源。 diff --git a/versions/4.0/zh/asset/model/image-location-resolution.md b/versions/4.0/zh/asset/model/image-location-resolution.md new file mode 100644 index 0000000000..f637cb9a82 --- /dev/null +++ b/versions/4.0/zh/asset/model/image-location-resolution.md @@ -0,0 +1,35 @@ +# Cocos Creator 图像位置解析算法 + +Cocos Creator 图像位置解析算法中给定的参数及参数说明如下: + +| 参数 | 说明 | +| :---- | :------ | +| `url` | 期望的 URL | +| `startDir` | 起始搜索目录 | +| `DEPTH` | 搜索深度,固定为 **2** | +| `SEARCH_DIR_NAMES` | 贴图文件夹名称数组,默认为:`textures`、`materials` | +| `SEARCH_EXT_NAMES` | 需要搜索的扩展名数组,固定为:`.jpg`、`.jpeg`、`.png`、`.tga`、`.webp` | + +Cocos Creator 图像位置解析算法由以下过程给出: + +- 如果 `url` 对应的文件存在,则返回 `url` + +- 令 `expectedExtName` 为 `url` 的扩展名 + +- 令 `expectedBaseName` 为 `url` 去扩展后的文件名 + +- 令 `searchExtNames` 为 `[expectedExtName, ...SEARCH_EXT_NAMES]` 去重之后的数组 + +- 令 `currentDir` 为 `startDir`,进行 `DEPTH` 次循环: + + - 如果 `currentDir` 处于项目 `assets` 目录外,则退出循环 + + - 如果 `currentDir` 目录中没有任何一个子目录的名称匹配 `SEARCH_DIR_NAMES`,则执行下次循环 + + - 令 `dir` 为 `currentDir` 目录中名称匹配 `SEARCH_DIR_NAMES` 的子目录 + + - 在 `dir` 中搜索是否有文件基础名称匹配 `expectedBaseName` 且扩展名匹配 `searchExtNames`的,如果有,则返回其路径 + + - 将 `currentDir` 置为其上层目录 + +- 返回搜索失败 diff --git a/versions/4.0/zh/asset/model/max-export-fbx.md b/versions/4.0/zh/asset/model/max-export-fbx.md new file mode 100644 index 0000000000..18cda13f60 --- /dev/null +++ b/versions/4.0/zh/asset/model/max-export-fbx.md @@ -0,0 +1,38 @@ +# 从 3ds Max 中导出 FBX 模型资源 + +## 导出步骤 + +1. 在 3ds Max 选中要导出的模型 + + ![选择要导出的模型](max/01-select-mesh.png) + +2. 选中主菜单 **File -> Export -> Export Selected** 展开导出选项: + + ![导出按钮](max/02-export-selected.png) + +3. 将 **Save as type** 设置为 **.fbx**, 点击 **save** 按钮 + + ![导出路径名称设置](max/03-export-file-name.png) + +4. 将 **Current Preset** 设置为 **Autodesk Media and Entertainment**: + + ![选项设置](max/04-export-preset-selection.png) + +5. 开启 **Embed Media**: + + ![开启Embed Media](max/05-embed-media.png) + +6. 点击 **OK** 导出文件: + + ![导出文件](max/06-export-file.png) + +**备注** : 更多信息请参阅 [3DS Max Fbx Files](https://help.autodesk.com/view/3DSMAX/2022/ENU/?guid=GUID-26E80277-1645-4C4E-A6B2-44399376490F) + +## 导入 Cocos Creator + +1. 将导出文件 **simpleBox** 放入 Cocos Creator 工程目录的 **Asset** 文件夹下。关于如何将 FBX 文件导入 Cocos Creator 请参阅 [模型资源](mesh.md)。 +2. 导入结果对比: + +| 3ds Max Viewport | Cocos Creator Scene Viewport | +|-----------------------------------------------------|--------------------------------------------------------| +| | | diff --git a/versions/4.0/zh/asset/model/max/01-select-mesh.png b/versions/4.0/zh/asset/model/max/01-select-mesh.png new file mode 100644 index 0000000000..3ee3253c29 Binary files /dev/null and b/versions/4.0/zh/asset/model/max/01-select-mesh.png differ diff --git a/versions/4.0/zh/asset/model/max/02-export-selected.png b/versions/4.0/zh/asset/model/max/02-export-selected.png new file mode 100644 index 0000000000..cabad99aaf Binary files /dev/null and b/versions/4.0/zh/asset/model/max/02-export-selected.png differ diff --git a/versions/4.0/zh/asset/model/max/03-export-file-name.png b/versions/4.0/zh/asset/model/max/03-export-file-name.png new file mode 100644 index 0000000000..2f03b4cdd2 Binary files /dev/null and b/versions/4.0/zh/asset/model/max/03-export-file-name.png differ diff --git a/versions/4.0/zh/asset/model/max/04-export-preset-selection.png b/versions/4.0/zh/asset/model/max/04-export-preset-selection.png new file mode 100644 index 0000000000..ed5125bf51 Binary files /dev/null and b/versions/4.0/zh/asset/model/max/04-export-preset-selection.png differ diff --git a/versions/4.0/zh/asset/model/max/05-embed-media.png b/versions/4.0/zh/asset/model/max/05-embed-media.png new file mode 100644 index 0000000000..be5e1f3810 Binary files /dev/null and b/versions/4.0/zh/asset/model/max/05-embed-media.png differ diff --git a/versions/4.0/zh/asset/model/max/06-export-file.png b/versions/4.0/zh/asset/model/max/06-export-file.png new file mode 100644 index 0000000000..643b3beaa5 Binary files /dev/null and b/versions/4.0/zh/asset/model/max/06-export-file.png differ diff --git a/versions/4.0/zh/asset/model/max/07-1-max-viewport.png b/versions/4.0/zh/asset/model/max/07-1-max-viewport.png new file mode 100644 index 0000000000..49d78584bb Binary files /dev/null and b/versions/4.0/zh/asset/model/max/07-1-max-viewport.png differ diff --git a/versions/4.0/zh/asset/model/max/07-2-cocos-viewport.png b/versions/4.0/zh/asset/model/max/07-2-cocos-viewport.png new file mode 100644 index 0000000000..3de3e9c8f1 Binary files /dev/null and b/versions/4.0/zh/asset/model/max/07-2-cocos-viewport.png differ diff --git a/versions/4.0/zh/asset/model/maya-export-fbx.md b/versions/4.0/zh/asset/model/maya-export-fbx.md new file mode 100644 index 0000000000..0c94a500d2 --- /dev/null +++ b/versions/4.0/zh/asset/model/maya-export-fbx.md @@ -0,0 +1,37 @@ +# 从 Maya 中导出 FBX 模型资源 + +## 导出步骤 +1. 选中要导出的模型: + + ![选择要导出的模型](maya/01-select-mesh.png) + +2. 选中主菜单 **File -> Export Selection Option** 展开导出选项: + + ![导出按钮](maya/02-export-selection-option.png) + +3. 将 **File type** 设置为 **FBX export**, 点击 **export selection** 按钮: + + ![导出选项](maya/03-export-type-selection.png) + +4. 将 **Current Preset** 设置为 **Autodesk Media and Entertainment**: + + ![选项设置](maya/04-export-preset-selection.png) + +5. 开启 **Embed Media** + + ![开启Embed Media](maya/05-embed-media.png) + +6. 选择导出路径,命名为 **simpleBox**, 点击 **Export Selection** 导出文件 + + ![导出文件](maya/06-export-file.png) + +**备注**:更多信息可参阅 [Maya FBX Plugin-in](https://help.autodesk.com/view/MAYAUL/2022/ENU/index.html?guid=GUID-BD85FA4C-4D40-457C-BE66-47BC08B82FC3) + +## 导入 Cocos Creator + +1. 将导出文件 simpleBox 放入 Cocos Creator 工程目录的`Asset`文件夹下。关于如何将 FBX 文件导入 Cocos Creator 请参阅 [模型资源](mesh.md) +2. 导入结果对比: + +| Maya Viewport | Cocos Creator Scene Viewport | +|-------------------------------------------------------|--------------------------------------------------------| +| | | diff --git a/versions/4.0/zh/asset/model/maya/01-select-mesh.png b/versions/4.0/zh/asset/model/maya/01-select-mesh.png new file mode 100644 index 0000000000..048de976e6 Binary files /dev/null and b/versions/4.0/zh/asset/model/maya/01-select-mesh.png differ diff --git a/versions/4.0/zh/asset/model/maya/02-export-selection-option.png b/versions/4.0/zh/asset/model/maya/02-export-selection-option.png new file mode 100644 index 0000000000..9478922a90 Binary files /dev/null and b/versions/4.0/zh/asset/model/maya/02-export-selection-option.png differ diff --git a/versions/4.0/zh/asset/model/maya/03-export-type-selection.png b/versions/4.0/zh/asset/model/maya/03-export-type-selection.png new file mode 100644 index 0000000000..b2f3f03d07 Binary files /dev/null and b/versions/4.0/zh/asset/model/maya/03-export-type-selection.png differ diff --git a/versions/4.0/zh/asset/model/maya/04-export-preset-selection.png b/versions/4.0/zh/asset/model/maya/04-export-preset-selection.png new file mode 100644 index 0000000000..9d244a97bc Binary files /dev/null and b/versions/4.0/zh/asset/model/maya/04-export-preset-selection.png differ diff --git a/versions/4.0/zh/asset/model/maya/05-embed-media.png b/versions/4.0/zh/asset/model/maya/05-embed-media.png new file mode 100644 index 0000000000..1b5361d970 Binary files /dev/null and b/versions/4.0/zh/asset/model/maya/05-embed-media.png differ diff --git a/versions/4.0/zh/asset/model/maya/06-export-file.png b/versions/4.0/zh/asset/model/maya/06-export-file.png new file mode 100644 index 0000000000..3876add5dc Binary files /dev/null and b/versions/4.0/zh/asset/model/maya/06-export-file.png differ diff --git a/versions/4.0/zh/asset/model/maya/07-1-maya-viewport.png b/versions/4.0/zh/asset/model/maya/07-1-maya-viewport.png new file mode 100644 index 0000000000..d3a7deaaf4 Binary files /dev/null and b/versions/4.0/zh/asset/model/maya/07-1-maya-viewport.png differ diff --git a/versions/4.0/zh/asset/model/maya/07-2-cocos-viewport.png b/versions/4.0/zh/asset/model/maya/07-2-cocos-viewport.png new file mode 100644 index 0000000000..3de3e9c8f1 Binary files /dev/null and b/versions/4.0/zh/asset/model/maya/07-2-cocos-viewport.png differ diff --git a/versions/4.0/zh/asset/model/mesh.md b/versions/4.0/zh/asset/model/mesh.md new file mode 100644 index 0000000000..b776f5581d --- /dev/null +++ b/versions/4.0/zh/asset/model/mesh.md @@ -0,0 +1,159 @@ +# 模型资源 + +目前,Creator 支持 **FBX** 和 **glTF** 两种格式的模型文件。 + +- FBX(.fbx):支持 FBX 2020 及更早的文件格式。 +- glTF(.gltf 、.glb):支持 glTF 2.0 及更早的文件格式,详情可参考 [glTF 模型](./glTF.md)。 + +关于如何从第三方工具导出这两种模型文件,请参考 [导入从 DCC 工具导出的模型](./dcc-export-mesh.md)。 + +## 模型导入 + +从外部导入编辑器中后,在 **资源管理器** 中可得到对应的模型资源文件,其目录结构如下:(以 glTF 文件为例,fbx 文件相同) + +- 无动画的模型文件结构如下: + + ![mesh list](mesh/mesh_list.png) + +- 包含动画的模型文件结构如下: + + ![mesh list1](mesh/mesh_list_1.png) + + - `.material` — 材质文件 + - `.mesh` — 模型文件 + - `.texture` — 模型贴图文件 + - `.animation` — 模型动画文件 + - `.skeleton` — 模型骨骼文件 + - `.prefab` — 导入时自动生成的预制件 + +### Mesh 文件 + +Mesh 文件中存储了模型的顶点、索引、纹理坐标等数据。在 **层级管理器** 中选中 Mesh 文件后,在 **属性检查器** 便可以看到 Mesh 的一些信息以及它的预览效果。 + +![Mesh Asset](mesh/mesh-asset-preview.png) + +| 属性 | 功能说明 | +| :-- | :-- | +| Vertices | 模型的顶点数 | +| Triangles | 模型的三角形数 | +| UV | 模型的纹理坐标索引 | +| MinPos | 模型的最小坐标值 | +| MaxPos | 模型的最大坐标值 | + +## 模型使用 + +将模型文件导入后,直接将模型文件的根节点从 **资源管理器** 拖拽到 **层级管理器** 中想要放置的节点下,即可完成节点创建,此时模型就成功在场景中创建了。
+或者也可以将模型文件的节点展开,选中模型文件节点下的 `.prefab` 文件,从 **资源管理器** 拖拽到 **层级管理器** 中同样能够完成创建。 + +![mesh use](mesh/mesh_use.gif) + +## 模型资源属性 + +当在 **资源管理器** 中选中模型资源文件时( `.fbx` 、 `.gltf` 或 `.glb`),在 **属性检查器** 中就可以直接设置模型资源的相关属性。 + +一共有四个标签可以选择: +| 标签 | 说明 | +| :--- | :--- | +| __模型(Model)__ | 用于设置模型网格导入时的相关选项 | +| __动画(Animation)__ | 用于查看和编辑模型动画片段 | +| __材质(Material)__ | 用于设置模型材质导入的相关选项 | +| __FBX__ | 用于设置 FBX 文件相关选项 | + +## 模型(Model) + +![mesh model](mesh/mesh-model.jpg) + +| 属性 | 说明 | +| :--- | :--- | +| 法线(Normals) | 导入法线信息,包括以下四种选项:
1. 可选(Optional):只导入模型文件中包含的法线,适用于非常清楚自己模型数据的情况。
2. 排除(Exclude):不导入法线。
3. 仅在必要时重新计算(Required):导入模型文件中的法线,若模型文件中不包含法线则重新计算,适用于大部分情况。模型数据本身没问题的话还是推荐使用该项,没有额外的处理。
4. 重新计算(Recalculate):无论模型文件中是否包含法线,都直接重新计算并导入。选择该项会增加计算量,但是可以消除模型原始数据法线没有归一化带来的后续问题。 | +| 切线(Tangents) | 导入切线信息,包括 Optional、Exclude、Require、Recalculate 四种选项,选项功能可参考 **法线** 的说明,二者相差不大。 | +| 形变法线(Morph normals) | 导入形变法线信息,包括:
可选(Optional):只导入模型文件中包含的形变法线,适用于非常清楚自己模型数据的情况。
排除(Exclude):不导入形变法线。 | +| 跳过验证(Skip Validation)| 是否跳过对模型文件的标准检测。 | +| 是否禁用 Mesh 拆分(Disable mesh split) | 为了解决实时骨骼动画系统下 uniform vector 数量限制问题,目前在资源导入期会根据骨骼数量做拆分,这会对其他系统也产生影响。如果确定不会使用实时计算模式(对应 SkeletalAnimation 组件的 useBakedAnimation 选项未勾选时),可以勾选此项以提升性能。但注意改变此选项会影响生成的 prefab 内容,需要对应更新场景中的引用。
详情请参考下文。 | +| 允许数据访问(Allow Data Access)| 标识此模型中的所有网格数据是否可被读写,此接口只对静态网格资源生效。若不勾选,网格数据提交至 CPU 后会自动释放 | +| 提升单一根节点(Promote Single Root Node )| 开启此选项,若模型场景只有一个根节点,那么该节点就作为预制体的根节点,否则会为根节点新建一个父节点 | + +### 是否禁用 Mesh 拆分(Disable mesh split) + +在 v3.6 的更新中我们优化了此选项的策略: + +- 默认情况下不再拆分模型,不对导入的模型数据做修改(也维持以前的模型设置不变) +- 如果骨骼数量未超过实际运行时驱动的限制,直接使用 uniform 传递 +- 如果骨骼数量超过限制,则使用纹理传递 + +![mesh-advopts](mesh/mesh-advopts.jpg) + +### 网格优化 (Mesh Optimize) +| 属性 | 说明 | +| :-- | :-- | +| **顶点缓存(Vetex Cache)** | 优化三角形顺序,提升 GPU 缓存命中率,建议对顶点较多的模型开启。 | +| **顶点提取(Vertex Fetch)** | 优化三角形顺序,提升顶点数据提取效率,建议对顶点较多的模型开启。 | +| **过度绘制(Overdraw)** | 优化三角形顺序,减少过度绘制,建议对顶点较多的模型开启。 | + +> 若有多个选项同时开启,优化算法会自动调节参数,达到最佳效果。 + +### 网格简化(Mesh Simplify) +| 属性 | 说明 | +| :-- | :-- | +| **目标比率(Target Ratio)** | 目标顶点数比率,建议设置为 0.5 | +| **自动误差率(Auto Error Rate)** | 是否自动计算误差率 | +| **误差率(Error Rate)** | 调整此值,获得最佳效果 | +| **锁定边界(Lock Boundary)** | 是否锁定网格数据边界 | + +> 已知的问题为:可能会存在在减面后丢失 UV 排布的情况
开发者需关注 **属性检查器** 内的警告情况以决定是否使用该选项。 + +### 网格切块(Mesh Cluster) +| 属性 | 说明 | +| :-- | :-- | +| **生成包围体(Generate Bounding)** | 是否为切块生成包围球和法线锥 | + +### 网格压缩(Mesh Compress) +| 属性 | 说明 | +| :-- | :-- | +| **编码(Encode)** | 使用二进制编码 | +| **压缩(Compress)** | zlib 算法,使用 LZ77 和哈弗曼进行编码 | +| **量化(Quantize)** | 量化,压缩浮点信息,减小包体 | + +### LODS + +网格导入器会检查网格的子节点是否以 _lodN 结尾,如有,则会自动识别这些子节点为 LOD 节点。如果没有,也可以通过勾选 LODS 选项,提供自动的 LOD 功能。 + +LOD1、LOD2 选项用于设置不同等级的三角面比例。 + +更多请参考 [多层次细节](../../editor/rendering/lod.md)。 + +以上选项算法细节,请参考开源库:https://github.com/zeux/meshoptimizer + +### 动画(Animation) + +![mesh animation](mesh/mesh_animation.png) + +上方的动画文件表格是当前模型下的所有动画资源信息,下方是当前选中动画的具体帧数信息的编辑区域,可以在此处更改动画名称或进行简单的动画裁剪。 + +- 点击图上红框内的 **+** 按钮可以添加动画文件,添加的新文件默认拷贝一份完整的 clip 数据,可以在下方的 `Start`、`End` 输入框中输入帧数来裁剪动画。(目前暂时不支持拖拽裁剪动画) + +- 点击图上红框内的 **-** 按钮可以删除当前选中的动画文件 + +### 材质(Material) + +![mesh material](mesh/mesh_material.png) + +上半部分的属性说明如下,下半部分则展示了当前模型中包含的材质。 + +| 属性 | 说明 | +| :--- | :--- | +| 提取材质 | 若想对模型文件自带的材质进行修改时,可开启该项,将文件结构目录下的材质文件提取出模型资源,便可以进行材质的调整修改了。 | +| 材质提取目录 | 用于指定或者查看提取出来的材质所在的目录。 | +| 使用顶点色 | 是否使用顶点色。 | +| 混合模式下的深度写入 | 当 Alpha 模式为 **Blend** 时开启深度写入。 | + +### FBX + +![mesh material](mesh/mesh_fbx.png) + +| 属性 | 说明 | +| :--- | :--- | +| 与 1.x 版本兼容 | 若勾选该项,则在导入模型时会兼容 Cocos Creator 3D 1.x 版本的导入方式。**注意**:启用该项可能会影响已经导入且被使用/引用的资源。 | +| 动画烘焙速率 | 指定动画烘焙速率,单位为 **帧/秒**,可选项包括 **自动**、**24**、**25**、**30** 和 **60**。 | +| 提升单一根节点 | 当开启该项,若 FBX 资源中包含的场景仅有一个根节点,则在将 FBX 场景转换为 Creator 的预制件时,会以该根节点作为预制件的根节点。否则将以 FBX 场景作为根节点。 | +| 优先使用文件时间范围 | 若勾选该项,则导入的 FBX 动画会优先使用 FBX 文件中记录的动画时间范围。
若不勾选该项,则不使用 FBX 的动画时间范围,将会粗略地计算动画时间范围。
有些 FBX 工具可能没有导出动画时间范围信息,那么也是粗略地计算动画时间范围。 diff --git a/versions/4.0/zh/asset/model/mesh/blender_export_bake.png b/versions/4.0/zh/asset/model/mesh/blender_export_bake.png new file mode 100644 index 0000000000..be4bfc8568 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/blender_export_bake.png differ diff --git a/versions/4.0/zh/asset/model/mesh/blender_export_fbx_1.png b/versions/4.0/zh/asset/model/mesh/blender_export_fbx_1.png new file mode 100644 index 0000000000..c81837e020 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/blender_export_fbx_1.png differ diff --git a/versions/4.0/zh/asset/model/mesh/blender_export_gltf.png b/versions/4.0/zh/asset/model/mesh/blender_export_gltf.png new file mode 100644 index 0000000000..b58b289886 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/blender_export_gltf.png differ diff --git a/versions/4.0/zh/asset/model/mesh/blender_model.png b/versions/4.0/zh/asset/model/mesh/blender_model.png new file mode 100644 index 0000000000..13a4a92716 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/blender_model.png differ diff --git a/versions/4.0/zh/asset/model/mesh/blender_model_bake_c3d.png b/versions/4.0/zh/asset/model/mesh/blender_model_bake_c3d.png new file mode 100644 index 0000000000..9e3af8a0bd Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/blender_model_bake_c3d.png differ diff --git a/versions/4.0/zh/asset/model/mesh/blender_model_c3d.png b/versions/4.0/zh/asset/model/mesh/blender_model_c3d.png new file mode 100644 index 0000000000..39a391a4ca Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/blender_model_c3d.png differ diff --git a/versions/4.0/zh/asset/model/mesh/dynamic-mesh.gif b/versions/4.0/zh/asset/model/mesh/dynamic-mesh.gif new file mode 100644 index 0000000000..c9342983e1 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/dynamic-mesh.gif differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh-advopts.jpg b/versions/4.0/zh/asset/model/mesh/mesh-advopts.jpg new file mode 100644 index 0000000000..b64bf7a7f5 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh-advopts.jpg differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh-asset-preview.png b/versions/4.0/zh/asset/model/mesh/mesh-asset-preview.png new file mode 100644 index 0000000000..50f53cff64 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh-asset-preview.png differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh-model.jpg b/versions/4.0/zh/asset/model/mesh/mesh-model.jpg new file mode 100644 index 0000000000..513b1865d1 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh-model.jpg differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh-optimizer-warn.png b/versions/4.0/zh/asset/model/mesh/mesh-optimizer-warn.png new file mode 100644 index 0000000000..e1d24f1478 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh-optimizer-warn.png differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh-optimizer.png b/versions/4.0/zh/asset/model/mesh/mesh-optimizer.png new file mode 100644 index 0000000000..b3b126092d Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh-optimizer.png differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh_animation.png b/versions/4.0/zh/asset/model/mesh/mesh_animation.png new file mode 100644 index 0000000000..e4c9e9e120 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh_animation.png differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh_fbx.png b/versions/4.0/zh/asset/model/mesh/mesh_fbx.png new file mode 100644 index 0000000000..8f25396e36 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh_fbx.png differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh_list.png b/versions/4.0/zh/asset/model/mesh/mesh_list.png new file mode 100644 index 0000000000..41fe8f050d Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh_list.png differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh_list_1.png b/versions/4.0/zh/asset/model/mesh/mesh_list_1.png new file mode 100644 index 0000000000..52db5e7097 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh_list_1.png differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh_material.png b/versions/4.0/zh/asset/model/mesh/mesh_material.png new file mode 100644 index 0000000000..ea787416d7 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh_material.png differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh_model.png b/versions/4.0/zh/asset/model/mesh/mesh_model.png new file mode 100644 index 0000000000..55bdf47e3d Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh_model.png differ diff --git a/versions/4.0/zh/asset/model/mesh/mesh_use.gif b/versions/4.0/zh/asset/model/mesh/mesh_use.gif new file mode 100644 index 0000000000..64ddde3749 Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/mesh_use.gif differ diff --git a/versions/4.0/zh/asset/model/mesh/relative_path.png b/versions/4.0/zh/asset/model/mesh/relative_path.png new file mode 100644 index 0000000000..a34cff8e8d Binary files /dev/null and b/versions/4.0/zh/asset/model/mesh/relative_path.png differ diff --git a/versions/4.0/zh/asset/model/scripting-mesh.md b/versions/4.0/zh/asset/model/scripting-mesh.md new file mode 100644 index 0000000000..5ab0c13183 --- /dev/null +++ b/versions/4.0/zh/asset/model/scripting-mesh.md @@ -0,0 +1,22 @@ +# 程序化创建网格 + +当由 DCC(Digital Content Creation)软件制作或引擎内的地形编辑器制作的模型无法满足需求时,可以通过 API 来创建网格。如需要在运行时创建某种可以生长的蛇、动态编辑模型或实现某些曲面,都可以通过程序化来创建网格。 + +## 创建网格 + +引擎支持两种网格:**静态网格** 和 **动态网格**,适用于不同的场景,开发者可按需使用。 + +- 静态网格,通过 `utils.MeshUtils.createMesh` 创建,一旦创建成功,网格内的几何体不可编辑的。 +- 动态网格:通过 `utils.MeshUtils.createDynamicMesh` 创建,创建成功后,网格内的几何体仍然可以修改。 + +返回值为 `Mesh` 组件,因此我们方便的将其赋值给 `MeshRenderer` 的 `mesh` 属性,如此即可将其显示在屏幕上。 + +## API + +API 请参考 [MeshUtils](%__APIDOC__%/zh/class/utils.MeshUtils)。 + +## 范例 + +![dynamic mesh](./mesh/dynamic-mesh.gif) + +动态网格的范例请参考 [GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8)。 diff --git a/versions/4.0/zh/asset/model/vat.md b/versions/4.0/zh/asset/model/vat.md new file mode 100644 index 0000000000..191246bf50 --- /dev/null +++ b/versions/4.0/zh/asset/model/vat.md @@ -0,0 +1,68 @@ +# 顶点动画贴图 (VAT) + +顶点动画贴图(Vertex Animation Texture,VAT)通常用于表达刚体破碎、布料、流体等物理模拟动画。在 Houdini 等 PCG 软件中计算好的物理模拟动画可烘焙为 VAT 数据导出,并在实时渲染引擎中回放,以极低的性能消耗来重现复杂的物理效果。 + +目前支持的 VAT 数据格式为 Houdini 导出的刚体、柔体和流体数据,以及泽森科工 Zeno 的流体数据。 + +## 数据导出与使用 + +导出的 VAT 数据包含 fbx 模型、动画贴图、包含 VAT 材质属性的 json 文件(帧数量和包围盒最大最小值等)三部分。 + +### 刚体 + +1、导出:Houdini 设置好刚体碰撞、破碎等动画后,选择 **VAT2.0 版本,UE 模式,无 paddle,LDR方式** 导出。 + +2、材质:在 Cocos Creator 中创建一个材质,Effect 选择 **util/dcc/vat/houdini-rigidbody-v2**。 + +3、参数:查看导出的 json 文件,将 Animation Speed、NumOfFrames、PivotMin/Max、PosMin/Max 等数据填到材质中。 + +4、贴图:展开导出的 Position 形状贴图,将本体拖入PositionMap,下属的 Sign 和 Alpha 分别拖入 PosSignMap 和 PosAlphaMap(若有)。 +Rotation 法线贴图同理。 + +### 柔体 + +1、导出:Houdini 设置好柔体碰撞、破碎等动画后,选择 **VAT3.0 版本,Unity 模式,无 paddle,LDR方式**导出。 + +2、材质:在 Cocos Creator 中创建一个材质,Effect 选择 **util/dcc/vat/houdini-softbody-v3**。 + +3、参数:查看导出的 json 文件,将 Animation Speed、NumOfFrames 等数据填到材质中。 + +4、贴图:展开导出的 Position 形状贴图,将本体拖入 PositionMap,下属的 Sign 和 Alpha 分别拖入 PosSignMap 和 PosAlphaMap(若有)。 +Rotation 法线贴图同理。 + +### 流体(Houdini) + +1、导出:Houdini 设置好流体动画后,选择 **VAT3.0 版本,Unity 模式,无 paddle,LDR方式**导出。 + +2、材质:在 Cocos Creator 中创建一个材质,Effect 选择 **util/dcc/vat/houdini-fluid-v3-liquid**。 + +3、参数:查看导出的 json 文件,将 Animation Speed、NumOfFrames 等数据填到材质中。 + + - 勾选 Use Lookup Texture,将导出的 lookup 图拖入 LookupMap + - 勾选 Use Lookup Auto Frame,将导出的 lookup 图的分辨率填入 LUTTextureWidth/Height + - 选中导出的 fbx 模型,将其顶点数记录下来填入 FBXVertexCount + +4、贴图:展开导出的 Position 形状贴图,将本体拖入PositionMap,下属的 Sign 和 Alpha 分别拖入 PosSignMap 和 PosAlphaMap(若有)。 +Rotation 法线贴图同理。 + +### 流体(Zeno) + +1、导出:Zeno 设置好流体动画后,选择 **VAT模式** 导出。 + +2、材质:在 Cocos Creator 中创建一个材质,Effect 选择 **util/dcc/vat/zeno-fluid-liquid**。 + +3、参数:查看导出的 json 文件,将 Animation Speed、NumOfFrames 等数据填到材质中。 + +4、贴图:展开导出的 Position 形状贴图拖入 PositionMap,Rotation 法线贴图拖入 RotationMap。 + +## 错误效果调试 + +### 刚体 + +若出现碎块绕圈转等动画异常,请确认导出选项是否指定的 VAT2.0 UE 模式,若选错为 Unity 模式可能导致此情况发生。 + +### 流体 + +若出现面破碎甚至有些面错乱,请检查 NumOfFrames 是否与 DCC 软件中的一致 + +若整个流体在抖或有穿插的乱面,请确认两张贴图的过滤模式为 Nearest,且一定不能勾选消除透明伪影(fix alpha transparency artifacts) diff --git a/versions/4.0/zh/asset/options.md b/versions/4.0/zh/asset/options.md new file mode 100644 index 0000000000..2ade59478b --- /dev/null +++ b/versions/4.0/zh/asset/options.md @@ -0,0 +1,62 @@ +# 可选参数 + +> 文:Santy-Wang,Xunyi + +为了增加灵活性和可扩展性,Asset Manager 中大部分的加载接口包括 `assetManager.loadAny` 和 `assetManager.preloadAny` 都提供了 `options` 参数。`options` 除了可以配置 Creator 的内置参数,还可以自定义任意参数用于扩展引擎功能。如果开发者不需要配置引擎内置参数或者扩展引擎功能,可以无视它,直接使用更简单的 API 接口,比如 `resources.load`。 + +目前 `options` 中引擎已使用的参数包括: + +`uuid`, `url`, `path`, `dir`, `scene`, `type`, `priority`, `preset`, `audioLoadMode`, `ext`, +`bundle`, `onFileProgress`, `maxConcurrency`, `maxRequestsPerFrame`, `maxRetryCount`, `version`, `xhrResponseType`, +`xhrWithCredentials`, `xhrMimeType`, `xhrTimeout`, `xhrHeader`, `reloadAsset`, `cacheAsset`, `cacheEnabled` + +**注意**:请 **不要** 使用以上字段作为自定义参数的名称,避免与引擎功能发生冲突。 + +## 扩展引擎 + +开发者可以通过在 [管线](pipeline-task.md) 和 [自定义处理方法](downloader-parser.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%A4%84%E7%90%86%E6%96%B9%E6%B3%95) 中使用可选参数来扩展引擎的加载功能。 + +```typescript +// 扩展管线 +assetManager.pipeline.insert(function (task, done) { + const input = task.input; + for (let i = 0; i < input.length; i++) { + if (input[i].options.myParam === 'important') { + console.log(input[i].url); + } + } + task.output = task.input; + done(); +}, 1); + +assetManager.loadAny({'path': 'images/background'}, {'myParam': 'important'}, callback); + +// 注册处理方法 +assetManager.downloader.register('.myformat', function (url, options, callback) { + // 下载对应资源 + const img = new Image(); + if (options.isCrossOrigin) { + img.crossOrigin = 'anonymous'; + } + + img.onload = function () { + callback(null, img); + }; + + img.onerror = function () { + callback(new Error('download failed'), null); + }; + + img.src = url; + +}); + +assetManager.parser.register('.myformat', function (file, options, callback) { + // 解析下载完成的文件 + callback(null, file); +}); + +assetManager.loadAny({'url': 'http://example.com/myAsset.myformat'}, {isCrossOrigin: true}, callback); +``` + +通过可选参数,再结合管线和自定义处理方法,引擎可以获得极大的扩展度。Asset Bundle 可以看做是使用可选参数扩展的第一个实例。 diff --git a/versions/4.0/zh/asset/pipeline-task.md b/versions/4.0/zh/asset/pipeline-task.md new file mode 100644 index 0000000000..f005885bd0 --- /dev/null +++ b/versions/4.0/zh/asset/pipeline-task.md @@ -0,0 +1,73 @@ +# 管线与任务 + +> 文:Santy-Wang,Xunyi + +> 本文适用于对加载流程有定制需求的进阶开发者 + +为了更方便地修改或者扩展引擎资源加载流程,Asset Manager 底层使用了名为 **管线与任务** 和 **下载与解析** 的机制对资源进行加载,本篇内容主要介绍 **管线与任务**。 + +虽然在 v2.4 之前的 `loader` 已经开始使用管线的概念来进行资源加载,但是在 Asset Manager 中,我们对管线进行了重构,使得逻辑更加清晰,也更容易扩展。开发者可以扩展现有管线,也可以使用引擎提供的类 `AssetManager.Pipeline` 来自定义管线。 + +## 管线 + +**管线** 可以理解为一系列过程的串联组合,当一个请求经过管线时,会被管线的各个阶段依次进行处理,最后输出处理后的结果。如下图所示: + +![pipeline](pipeline-task/pipeline.png) + +管线与一般的固定流程相比,优势在于管线中的所有环节都是可拼接和组合的,这意味着开发者可以在现有管线的任意环节插入新的阶段或者移除旧的阶段,极大地增强了灵活性和可扩展性。 + +### 内置管线 + +Asset Manager 中内置了三条管线: + +![builtin-pipeline](pipeline-task/builtin-pipeline.jpg) + +- 第一条管线用于转换资源路径,找到真实资源路径。 +- 第二条管线用于正常加载。 +- 第三条管线用于预加载。 + +**注意**:第二条管线用到了下载器和解析器,第三条管线则用到了下载器,具体内容可参考 [下载与解析](downloader-parser.md)。 + +### 自定义管线 + +开发者可以对内置管线进行自定义扩展以实现自己的定制需求: + +```typescript +assetManager.pipeline.insert(function (task, done) { + task.output = task.input; + for (let i = 0; i < task.input; i++) { + console.log(task.input[i].content); + } + done(); +}, 1); +``` + +也可以构建一条新的管线: + +```typescript +const pipeline = new AssetManager.Pipeline('test', [(task, done) => { + console.log('first step'); + done(); +}, (task, done) => { + console.log('second step'); + done(); +}]); +``` + +构建管线需要一系列方法,每个方法需要传入一个任务参数和一个完成回调参数。开发者可以在方法中访问任务的所有内容,在完成时调用完成回调即可。 + +## 任务 + +**任务** 就是在管线中流动的请求,一个任务中包括输入、输出、完成回调、[可选参数](options.md) 等内容。当任务在管线中流动时,管线的各个阶段会取出任务的输入,做出一定的处理后存回到输出中。 + +```typescript +assetManager.pipeline.insert(function (task, done) { + for (let i = 0; i < task.input.length; i++) { + task.input[i].content = null; + } + task.output = task.input; + done(); +}, 1); +``` + +具体内容可参考 [AssetManager.Task](%__APIDOC__%/zh/class/AssetManager.Task) 类型。 diff --git a/versions/4.0/zh/asset/pipeline-task/builtin-pipeline.jpg b/versions/4.0/zh/asset/pipeline-task/builtin-pipeline.jpg new file mode 100644 index 0000000000..a0d18a27a6 Binary files /dev/null and b/versions/4.0/zh/asset/pipeline-task/builtin-pipeline.jpg differ diff --git a/versions/4.0/zh/asset/pipeline-task/pipeline.png b/versions/4.0/zh/asset/pipeline-task/pipeline.png new file mode 100644 index 0000000000..d44c781b84 Binary files /dev/null and b/versions/4.0/zh/asset/pipeline-task/pipeline.png differ diff --git a/versions/4.0/zh/asset/prefab.md b/versions/4.0/zh/asset/prefab.md new file mode 100644 index 0000000000..701e5af5ae --- /dev/null +++ b/versions/4.0/zh/asset/prefab.md @@ -0,0 +1,108 @@ +# 预制件(Prefab) + +预制件用于存储一些可以复用的场景对象,它可以包含节点、组件以及组件上的数据。由预制件生成的实例既可以继承模板的数据,又可以有自己定制化的数据修改。 + +## 基础概念 + +| 名称 | 说明 | 示例 | +| :--- | :--- | :--- | +| 预制件资源 | 在 **资源管理器** 中的预制件资源,是预制件的序列化文件。| ![prefab asset](prefab/prefab-asset.png) | +| 预制件实例 | 预制件资源被拖拽到 **层级管理器** 中就会生成一个预制件实例,它的根节点目前标记为暗绿色,它的子节点为亮绿色 | ![prefab instance](prefab/prefab-instance.png) | +| 预制件编辑模式 | 双击预制件资源,会进入预制件的编辑模式,此时所有的非嵌套预制件节点都显示为亮绿色。可点击 **场景编辑器** 左上方的按钮来 **保存** 修改或者 **关闭** 编辑模式 | ![prefab edit mode](prefab/prefab-edit-mode.png) | +| 嵌套的预制件实例 | 一个预制件资源中的某个子节点是另一个预制件资源的实例,则这个子预制件实例就是一个嵌套的预制件实例 | ![nested prefab](prefab/nested-prefab.png) | + +## 创建预制件 + +创建预制件有两种方法: + +1. 在场景中将节点编辑好之后,直接将节点从 **层级管理器** 拖到 **资源管理器** 中即可完成预制件资源的创建。 + +2. 点击 **资源管理器** 左上方的 **+** 按钮,或者点击面板空白处,然后选择 **Node Prefab** 即可。(v3.1.1 新增) + + ![create](prefab/create.png) + +创建完成后,原节点自动变为该预制件的实例,根节点呈现 **亮绿色**,非嵌套预制件子节点呈现 **暗绿色**。 + +![create prefab](prefab/create-prefab.gif) + +## v3.4.1 预制体更新说明 + +在 v3.4.1 中我们对预制体资源增加了 **Persistent** 选项。该选项目前为引擎预留,开发者不必关心。其说明如下: + +| 属性 | 说明 | 默认值 | 可选项 | +| :--- | :--- | :--- | :--- | +| **Persistent** | 标记该预制体是否需要同步 | **false** | true, false | + +![persistent](prefab/persistent.png) + +## 预览 + +在 v3.8.5 中,我们在 Prefab 的属性查看器面板中,添加了预览支持。用户选中某个预制体后,就可以在面板中看到 Prefab 的渲染内容,能够极大地提升资源检查效率。 +![preview](prefab/prefab-preview.png) + +## 使用预制件 + +将预制件资源从 **资源管理器** 拖拽到 **层级管理器** 或 **场景编辑器**,即可在场景中生成一个预制件的实例。
+场景中的预制件实例对象,数据源来自预制件资源的反序列化,所以它的数据默认同步了预制件资源,如果对预制件实例中的各项属性进行修改,修改的数据会被存储在预制件实例中,所以不会影响到预制件资源和它生成的其它预制件实例的数据。 + +![use prefab](prefab/use-prefab.gif) + +## 进入预制件编辑模式 + +在 **资源管理器** 中双击预制件资源可从场景编辑模式切换到预制件编辑模式。 + +此时可以在编辑器中编辑预制件资源,编辑完成之后,点击场景编辑器中的 **保存** 按钮即可保存编辑后的预制件资源,之后点击 **关闭** 按钮即可返回场景编辑模式。 + +> **注意**:请尽量避免多人同时修改同一个 Prefab 资源,否则可能会导致冲突,且无法通过 `git` 合并解决冲突。 + +![prefab edit mode](prefab/prefab-edit-mode.gif) + +## 预制件节点的状态 + +预制件节点在 **属性检查器** 中呈现 **绿色** 时表示与资源关联正常;呈现 **红色** 则表示关联的资源丢失。 + +## 场景中编辑预制件节点 + +### 通用操作 + +在 **层级管理器** 中选中预制件节点,**属性检查器** 的顶部便会出现几个可操作的按钮: + +![edit prefab](prefab/edit-prefab.png) + +| 按钮图标 | 说明 | +| :--- | :--- | +| ![unlink prefab button](prefab/unlink-prefab-button.png) | 还原为普通节点。预制件节点可变为普通节点,即完全脱离和资源的关系。还可以通过点击 Creator 顶部菜单栏中的 **节点 -> 取消关联当前的 Prefab 资源** 来实现。v3.4 支持批量取消关联。 | +| ![locate prefab button](prefab/locate-prefab-button.png) | 定位资源。便于快速在 **资源管理器** 中定位到预制件资源。 | +| ![revert prefab button](prefab/revert-prefab-button.png) | 从资源还原。将当前预制件实例的数据还原为预制件资源中的数据,其中名字、位置和旋转不会被还原为预制件资源中的数据。 | +| ![apply prefab button](prefab/apply-prefab-button.png) | 更新到资源。将当前预制件实例的所有数据更新到所关联的预制件资源中。 | + +### 新增节点 + +在预制件实例下增加的新节点,在节点名字的右下角会有一个 **+** 标志,它的数据存储在预制件的实例下,所以不会影响关联的预制件资源的数据。 + +![prefab mounted children](prefab/prefab-mounted-children.png) + +### 新增组件 + +在预制件实例下增加的新组件,在组件名字的后面会有一个 **+** 标志,它的数据存储在预制件的实例下,所以不会影响关联的预制件资源的数据。 + +![instance add component](prefab/instance-add-component.png) + +### 删除组件 + +在预制件实例下删除 **非预制件实例下新增的组件**,会在 **属性检查器** 上增加一条删除的组件的数据,它的数据存储在预制件的实例下,所以不会影响关联的预制件资源的数据。 + +![instance remove component](prefab/instance-remove-component.png) + +同时会在这条数据后面出现以下两个按钮: + +| 按钮图标 | 功能说明 | +| :--- | :--- | +| ![revert remove component](prefab/revert-remove-component.png) | 还原该删除的组件 | +| ![apply remove component](prefab/apply-remove-component.png) | 将该删除的组件在预制件资源中同步删除 | + +## 目前的一些限制 + +- 不允许在预制件实例中删除从预制件资源中创建的节点 +- 不允许在预制件实例中更改从预制件资源中创建的节点的层级关系 +- 不允许预制件嵌套自己 diff --git a/versions/4.0/zh/asset/prefab/apply-prefab-button.png b/versions/4.0/zh/asset/prefab/apply-prefab-button.png new file mode 100644 index 0000000000..14bbe2c0da Binary files /dev/null and b/versions/4.0/zh/asset/prefab/apply-prefab-button.png differ diff --git a/versions/4.0/zh/asset/prefab/apply-remove-component.png b/versions/4.0/zh/asset/prefab/apply-remove-component.png new file mode 100644 index 0000000000..e42783e37a Binary files /dev/null and b/versions/4.0/zh/asset/prefab/apply-remove-component.png differ diff --git a/versions/4.0/zh/asset/prefab/create-prefab.gif b/versions/4.0/zh/asset/prefab/create-prefab.gif new file mode 100644 index 0000000000..7b42b57359 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/create-prefab.gif differ diff --git a/versions/4.0/zh/asset/prefab/create.png b/versions/4.0/zh/asset/prefab/create.png new file mode 100644 index 0000000000..86ca61d3bd Binary files /dev/null and b/versions/4.0/zh/asset/prefab/create.png differ diff --git a/versions/4.0/zh/asset/prefab/edit-prefab.png b/versions/4.0/zh/asset/prefab/edit-prefab.png new file mode 100644 index 0000000000..83c10dbb9c Binary files /dev/null and b/versions/4.0/zh/asset/prefab/edit-prefab.png differ diff --git a/versions/4.0/zh/asset/prefab/instance-add-component.png b/versions/4.0/zh/asset/prefab/instance-add-component.png new file mode 100644 index 0000000000..b6c48344b0 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/instance-add-component.png differ diff --git a/versions/4.0/zh/asset/prefab/instance-remove-component.png b/versions/4.0/zh/asset/prefab/instance-remove-component.png new file mode 100644 index 0000000000..b192763ca7 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/instance-remove-component.png differ diff --git a/versions/4.0/zh/asset/prefab/locate-prefab-button.png b/versions/4.0/zh/asset/prefab/locate-prefab-button.png new file mode 100644 index 0000000000..bfd99f6179 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/locate-prefab-button.png differ diff --git a/versions/4.0/zh/asset/prefab/nested-prefab.png b/versions/4.0/zh/asset/prefab/nested-prefab.png new file mode 100644 index 0000000000..ed989bf6be Binary files /dev/null and b/versions/4.0/zh/asset/prefab/nested-prefab.png differ diff --git a/versions/4.0/zh/asset/prefab/persistent.png b/versions/4.0/zh/asset/prefab/persistent.png new file mode 100644 index 0000000000..e591262b53 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/persistent.png differ diff --git a/versions/4.0/zh/asset/prefab/prefab-asset.png b/versions/4.0/zh/asset/prefab/prefab-asset.png new file mode 100644 index 0000000000..a8fe20ce25 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/prefab-asset.png differ diff --git a/versions/4.0/zh/asset/prefab/prefab-edit-mode.gif b/versions/4.0/zh/asset/prefab/prefab-edit-mode.gif new file mode 100644 index 0000000000..2b3a543abf Binary files /dev/null and b/versions/4.0/zh/asset/prefab/prefab-edit-mode.gif differ diff --git a/versions/4.0/zh/asset/prefab/prefab-edit-mode.png b/versions/4.0/zh/asset/prefab/prefab-edit-mode.png new file mode 100644 index 0000000000..5204fc1a47 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/prefab-edit-mode.png differ diff --git a/versions/4.0/zh/asset/prefab/prefab-instance.png b/versions/4.0/zh/asset/prefab/prefab-instance.png new file mode 100644 index 0000000000..61a77415b7 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/prefab-instance.png differ diff --git a/versions/4.0/zh/asset/prefab/prefab-mounted-children.png b/versions/4.0/zh/asset/prefab/prefab-mounted-children.png new file mode 100644 index 0000000000..1721c66177 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/prefab-mounted-children.png differ diff --git a/versions/4.0/zh/asset/prefab/prefab-preview.png b/versions/4.0/zh/asset/prefab/prefab-preview.png new file mode 100644 index 0000000000..de89d431c4 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/prefab-preview.png differ diff --git a/versions/4.0/zh/asset/prefab/revert-prefab-button.png b/versions/4.0/zh/asset/prefab/revert-prefab-button.png new file mode 100644 index 0000000000..10107f5855 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/revert-prefab-button.png differ diff --git a/versions/4.0/zh/asset/prefab/revert-remove-component.png b/versions/4.0/zh/asset/prefab/revert-remove-component.png new file mode 100644 index 0000000000..0b10bbaff5 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/revert-remove-component.png differ diff --git a/versions/4.0/zh/asset/prefab/unlink-prefab-button.png b/versions/4.0/zh/asset/prefab/unlink-prefab-button.png new file mode 100644 index 0000000000..254f2ec0a5 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/unlink-prefab-button.png differ diff --git a/versions/4.0/zh/asset/prefab/use-prefab.gif b/versions/4.0/zh/asset/prefab/use-prefab.gif new file mode 100644 index 0000000000..bf9284a350 Binary files /dev/null and b/versions/4.0/zh/asset/prefab/use-prefab.gif differ diff --git a/versions/4.0/zh/asset/preload-load.md b/versions/4.0/zh/asset/preload-load.md new file mode 100644 index 0000000000..2a2eb090f4 --- /dev/null +++ b/versions/4.0/zh/asset/preload-load.md @@ -0,0 +1,26 @@ +# 加载与预加载 + +> 文:Santy-Wang,Xunyi + +为了尽可能缩短下载时间,很多游戏都会使用预加载。Asset Manager 中的大部分加载接口包括 `load`、`loadDir`、`loadScene` 都有其对应的预加载版本。加载接口与预加载接口所用的参数是完全一样的,两者的区别在于: + +1. 预加载只会下载资源,不会对资源进行解析和初始化操作。 +2. 预加载在加载过程中会受到更多限制,例如最大下载并发数会更小。 +3. 预加载的下载优先级更低,当多个资源在等待下载时,预加载的资源会放在最后下载。 +4. 因为预加载没有做任何解析操作,所以当所有的预加载完成时,不会返回任何可用资源。 + +相比 Creator v2.4 以前的版本,以上优化手段充分降低了预加载的性能损耗,确保了游戏体验顺畅。开发者可以充分利用游戏过程中的网络带宽缩短后续资源的加载时间。 + +因为预加载没有去解析资源,所以需要在预加载完成后配合加载接口进行资源的解析和初始化,来完成资源加载。例如: + +```typescript +resources.preload('images/background/spriteFrame', SpriteFrame); + +// wait for while +resources.load('images/background/spriteFrame', SpriteFrame, function (err, spriteFrame) { + spriteFrame.addRef(); + self.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +**注意**:加载不需要等到预加载完成后再调用,开发者可以在任何时候进行加载。正常加载接口会直接复用预加载过程中已经下载好的内容,缩短加载时间。 diff --git a/versions/4.0/zh/asset/release-manager.md b/versions/4.0/zh/asset/release-manager.md new file mode 100644 index 0000000000..ec56d8e970 --- /dev/null +++ b/versions/4.0/zh/asset/release-manager.md @@ -0,0 +1,132 @@ +# 资源释放 + +> 文:Santy-Wang、Xunyi + +Asset Manager 中提供了资源释放模块,用于管理资源的释放。 + +在资源加载完成后,会被临时缓存到 `assetManager` 中,以便下次复用。但是这也会造成内存和显存的持续增长,所以有些资源如果不需要用到,可以通过 **自动释放** 或者 **手动释放** 的方式进行释放。释放资源将会销毁资源的所有内部属性,比如渲染层的相关数据,并移出缓存,从而释放内存和显存(对纹理而言)。 + +**首先最为重要的一点就是:资源之间是互相依赖的。** + +比如下图,Prefab 资源中的 Node 包含 Sprite 组件,Sprite 组件依赖于 SpriteFrame,SpriteFrame 资源依赖于 Texture 资源,而 Prefab,SpriteFrame 和 Texture 资源都被 assetManager 缓存起来了。这样做的好处是,有可能有另一个 SpriteAtlas 资源依赖于同样的一个 SpriteFrame 和 Texture,那么当你手动加载这个 SpriteAtlas 的时候,就不需要再重新请求贴图资源了,assetManager 会自动使用缓存中的资源。 + +![](load-assets/asset-dep.png) + +## 自动释放 + +场景的自动释放可以直接在编辑器中设置。在 **层级管理器** 选中场景后,**属性检查器** 中会出现 **自动释放资源** 选项。 + +![自动释放](release-manager/auto-release.png) + +勾选后,点击右上方的 **应用** 按钮,之后在切换该场景时便会自动释放该场景所有的依赖资源。建议场景尽量都勾选自动释放选项,以确保内存占用较低,除了部分高频使用的场景(例如主场景)。 + +另外,所有 `Asset` 实例都拥有成员函数 `Asset.addRef` 和 `Asset.decRef`,分别用于增加和减少引用计数。一旦引用计数为零,Creator 会对资源进行自动释放(需要先通过释放检查,具体可参考下部分内容的介绍) + +```typescript +start () { + resources.load('images/background', Texture2D, (err, texture) => { + this.texture = texture; + // 当需要使用资源时,增加其引用 + texture.addRef(); + // ... + }); +} + +onDestroy () { + // 当不需要使用资源时,减少引用 + // Creator 会在调用 decRef 后尝试对其进行自动释放 + this.texture.decRef(); +} +``` + +自动释放的优势在于不用显式地调用释放接口,开发者只需要维护好资源的引用计数,Creator 会根据引用计数自动进行释放。这大大降低了错误释放资源的可能性,并且开发者不需要了解资源之间复杂的引用关系。对于没有特殊需求的项目,建议尽量使用自动释放的方式来释放资源。 + +### 释放检查 + +为了避免错误释放正在使用的资源造成渲染或其他问题,Creator 会在自动释放资源之前进行一系列的检查,只有检查通过了,才会进行自动释放。 + +1. 如果资源的引用计数为 0,即没有其他地方引用到该资源,则无需做后续检查,直接摧毁该资源,移除缓存。 + +2. 资源一旦被移除,会同步触发其依赖资源的释放检查,将移除缓存后的资源的 **直接** 依赖资源(不包含后代)的引用都减 1,并同步触发释放检查。 + +3. 如果资源的引用计数不为 0,即存在其他地方引用到该资源,此时需要进行循环引用检查,避免出现自己的后代引用自己的情况。如果循环引用检查完成之后引用计数仍不为 0,则终止释放,否则直接摧毁该资源,移除缓存,并触发其依赖资源的释放检查(同步骤 2)。 + +## 手动释放 + +当项目中使用了更复杂的资源释放机制时,可以调用 Asset Manager 的相关接口来手动释放资源。例如: + +```typescript +assetManager.releaseAsset(texture); +``` + +因为资源管理模块在 v2.4 做了升级,所以释放接口与之前的版本有一点区别: + +1. `assetManager.releaseAsset` 接口仅能释放单个资源,且为了统一,接口只能通过资源本身来释放资源,不能通过资源 UUID、资源 url 等属性进行释放。 + +2. 在释放资源时,开发者只需要关注资源本身,引擎会 **自动释放** 其依赖资源,不再需要通过 `getDependsRecursively` 手动获取依赖。 + +**注意**:`release` 系列接口(例如 `release`、`releaseAsset`、`releaseAll`)会直接释放资源,而不会进行释放检查,只有其依赖资源会进行释放检查。所以当显式调用 `release` 系列接口时,可以确保资源本身一定会被释放。 + +## 引用计数统计 + +在 v2.4 之前,Creator 选择让开发者自行控制所有资源的释放,包括资源本身及其依赖项,开发者必须手动获取资源所有的依赖项并选择需要释放的依赖项。这种方式给予了开发者最大的控制权,对于小型项目来说工作良好。但随着 Creator 的发展,项目的规模不断扩大,场景所引用的资源不断增加,其他场景也可能复用了这些资源,这就会导致释放资源的复杂度越来越高,开发者要掌握所有资源的使用非常困难。 + +为了解决这个痛点,Asset Manager 提供了一套基于引用计数的资源释放机制,让开发者可以简单高效地释放资源,不用担心项目规模的急剧膨胀。需要说明的是 Asset Manager 只会自动统计资源之间的静态引用,并不能真实地反应资源在游戏中被动态引用的情况,动态引用还需要开发者进行控制以保证资源能够被正确释放。原因如下: + +- JavaScript 是拥有垃圾回收机制的语言,会对其内存进行管理,在浏览器环境中引擎无法知道某个资源是否被销毁。 +- JavaScript 无法提供赋值运算符的重载,而引用计数的统计则高度依赖于赋值运算符的重载。 + +### 资源的静态引用 + +当开发者在编辑器中编辑资源时(例如场景、预制件、材质等),需要在这些资源的属性中配置一些其他的资源,例如在材质中设置贴图,在场景的 Sprite 组件上设置 SpriteFrame。那么这些引用关系会被记录在资源的序列化数据中,引擎可以通过这些数据分析出依赖资源列表,像这样的引用关系就是静态引用。 + +引擎对资源的静态引用的统计方式为: + +1. 在使用 `assetManager` 或者 Asset Bundle 加载某个资源时,引擎会在底层加载管线中记录该资源所有 **直接依赖资源** 的信息,并将所有 **直接依赖资源** 的引用计数加 1,然后将该资源的引用计数初始化为 0。 + +2. 在释放资源时,取得该资源之前记录的所有 **直接依赖资源** 信息,并将所有依赖资源的引用计数减 1。 + +因为在释放检查时,如果资源的引用计数为 0,才可以被自动释放。所以上述步骤可以保证资源的依赖资源无法先于资源本身被释放,因为依赖资源的引用计数肯定不为 0。也就是说,只要一个资源本身不被释放,其依赖资源就不会被释放,从而保证在复用资源时不会错误地进行释放。下面我们来看一个例子: + +1. 假设现在有一个 A 预制件,其依赖的资源包括 a 材质和 b 材质。a 材质引用了 α 贴图,b 材质引用了 β 贴图。那么在加载 A 预制件之后,a、b 材质的引用计数都为 1,α、β 贴图的引用计数也都为 1。 + + ![](release-manager/pica.png) + +2. 假设现在又有一个 B 预制件,其依赖的资源包括 b 材质和 c 材质。则在加载 B 预制件之后,b 材质的引用计数为 2,因为它同时被 A 和 B 预制件所引用。而 c 材质的引用计数为 1,α、β 贴图的引用计数也仍为 1。 + + ![](release-manager/picb.png) + +3. 此时释放 A 预制件,则 a,b 材质的引用计数会各减 1 + - a 材质的引用计数变为 0,被释放,所以贴图 α 的引用计数减 1 变为了 0,也被释放。 + - b 材质的引用计数变为 1,被保留,所以贴图 β 的引用计数仍为 1,也被保留。 + - 因为 B 预制件没有被释放,所以 c 材质的引用计数仍为 1,被保留。 + + ![](release-manager/picc.png) + +### 资源的动态引用 + +当开发者在编辑器中没有对资源做任何设置,而是通过代码动态加载资源并设置到场景的组件上,则资源的引用关系不会记录在序列化数据中,引擎无法统计到这部分的引用关系,这些引用关系就是动态引用。 + +如果开发者在项目中使用动态加载资源来进行动态引用,例如: + +```typescript +resources.load('images/background/spriteFrame', SpriteFrame, function (err, spriteFrame) { + self.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +此时会将 SpriteFrame 资源设置到 Sprite 组件上,引擎不会做特殊处理,SpriteFrame 的引用计数仍保持 0。如果动态加载出来的资源需要长期引用、持有,或者复用时,建议使用 `addRef` 接口手动增加引用计数。例如: + +```typescript +resources.load('images/background/spriteFrame', SpriteFrame, function (err, spriteFrame) { + self.getComponent(Sprite).spriteFrame = spriteFrame; + spriteFrame.addRef(); +}); +``` + +增加引用计数后,可以保证该资源不会被提前错误释放。而在不需要引用该资源以及相关组件,或者节点销毁时,请 **务必记住** 使用 `decRef` 移除引用计数,并将资源引用设为 `null`,例如: + +```typescript +this.spriteFrame.decRef(); +this.spriteFrame = null; +``` diff --git a/versions/4.0/zh/asset/release-manager/auto-release.png b/versions/4.0/zh/asset/release-manager/auto-release.png new file mode 100644 index 0000000000..23f6dddb3d Binary files /dev/null and b/versions/4.0/zh/asset/release-manager/auto-release.png differ diff --git a/versions/4.0/zh/asset/release-manager/pica.png b/versions/4.0/zh/asset/release-manager/pica.png new file mode 100644 index 0000000000..b51f82075d Binary files /dev/null and b/versions/4.0/zh/asset/release-manager/pica.png differ diff --git a/versions/4.0/zh/asset/release-manager/picb.png b/versions/4.0/zh/asset/release-manager/picb.png new file mode 100644 index 0000000000..dd0f43d691 Binary files /dev/null and b/versions/4.0/zh/asset/release-manager/picb.png differ diff --git a/versions/4.0/zh/asset/release-manager/picc.png b/versions/4.0/zh/asset/release-manager/picc.png new file mode 100644 index 0000000000..063aacc9d4 Binary files /dev/null and b/versions/4.0/zh/asset/release-manager/picc.png differ diff --git a/versions/4.0/zh/asset/render-texture.md b/versions/4.0/zh/asset/render-texture.md new file mode 100644 index 0000000000..60faf056c3 --- /dev/null +++ b/versions/4.0/zh/asset/render-texture.md @@ -0,0 +1,148 @@ +# 渲染纹理资源(Render Texture) + +渲染纹理是一张在 GPU 上的纹理。通常我们会把它设置到相机的 **目标纹理** 上,使相机照射的内容通过离屏的 `frambuffer` 绘制到该纹理上。一般可用于制作汽车后视镜,动态阴影等功能。 + +## 创建渲染纹理资源 + +在 **资源管理器** 中点击左上方的 **+** 按钮,然后选择 **渲染纹理**,即可创建渲染纹理资源: + +![add-render-texture](render-texture/add-render-texture.png) + +然后在 **属性检查器** 中便可以设置渲染纹理资源的相关属性: + +![render-texture-property](render-texture/render-texture-property.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Width** | 设置渲染纹理的宽 | +| **Height** | 设置渲染纹理的高 | +| **Anisotropy** | 各项异性值 | +| **Min Filter** | 缩小过滤算法 | +| **Mag Filter** | 放大过滤算法 | +| **Mip Filter** | 多级纹理过滤算法 | +| **Wrap Mode S** | S(U)方向纹理寻址模式 | +| **Wrap Mode T** | T(V)方向纹理寻址模式 | + +## 在编辑器使用 + +在相机组件中,给相机的 **TargetTexture** 属性赋予 RenderTexture 可以将相机照射的结果绘制到 RenderTexture 上。 + +![camera](render-texture/camera.png) + +### 在 2D / UI 中使用 + +RenderTexture 可以像普通贴图一样使用。以 Sprite 为例,从 **资源管理器** 拖拽到 **SpriteFrame** 属性即可。 + +![sprite rt](render-texture/sprite-rt.png) + +### 设置 RenderTexture 为材质贴图 + +将 RenderTexture 设置为材质贴图包括以下两个步骤: + +1. 在 effect 中处理 uv。判断 `SAMPLE_FROM_RT`,并调用 `CC_HANDLE_RT_SAMPLE_FLIP` 函数: + + ``` + #if USE_TEXTURE + v_uv = a_texCoord * tilingOffset.xy + tilingOffset.zw; + #if SAMPLE_FROM_RT + CC_HANDLE_RT_SAMPLE_FLIP(v_uv); + #endif + #endif + ``` + +2. 在 **资源管理器** 中选中对应的材质,然后在 **属性检查器** 中勾选 `SAMPLE FROM RT` + + ![SAMPLE_FROM_RT](render-texture/SampleFormRT.png) + +## 程序化使用 + +程序化使用 RenderTexture 有以下两种方法: + +- **方法一**:把 3D 相机映射的内容绘制到 UI 的精灵帧上 + + ```typescript + import { _decorator, Component, RenderTexture, SpriteFrame, Sprite, Camera } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('CaptureToWeb') + export class CaptureToWeb extends Component { + @property(Sprite) + sprite: Sprite = null; + @property(Camera) + camera: Camera = null; + + protected _renderTex: RenderTexture = null; + + start() { + const sp = new SpriteFrame(); + const renderTex = this._renderTex = new RenderTexture(); + renderTex.reset({ + width: 256, + height: 256, + }); + this.camera.targetTexture = renderTex; + sp.texture = renderTex; + this.sprite.spriteFrame = sp; + } + } + ``` + +- **方法二**:把 3D 相机映射的内容绘制到 3D 模型上 + + ```typescript + import { _decorator, Component, MeshRenderer, RenderTexture, Camera, Material } from 'cc'; + const { ccclass, property, requireComponent } = _decorator; + + @ccclass("RenderCameraToModel") + @requireComponent(Camera) + export class RenderCameraToModel extends Component { + @property(MeshRenderer) + model: MeshRenderer = null; + + start() { + const renderTex = new RenderTexture(); + renderTex.reset({ + width: 256, + height: 256, + }); + const cameraComp = this.getComponent(Camera); + cameraComp.targetTexture = renderTex; + const pass = this.model.material.passes[0]; + // 设置 'SAMPLE_FROM_RT' 宏为 'true' 的目的是为了使 RenderTexture 在各个平台能正确显示 + const defines = { SAMPLE_FROM_RT: true, ...pass.defines }; + const renderMat = new Material(); + renderMat.initialize({ + effectAsset: this.model.material.effectAsset, + defines, + }); + this.model.setMaterial(renderMat, 0); + renderMat.setProperty('mainTexture', renderTex, 0); + } + } + ``` +- **方法三**:把 3D 相机映射的内容通过readPixels方法读取到ArrayBuffer中 + + ```typescript + import { _decorator, Component, RenderTexture } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass("RenderReadPixels") + export class RenderReadPixels extends Component { + @property(RenderTexture) + public renderTexture: RenderTexture; + start() { + const width = this.renderTexture.width; + const height = this.renderTexture.height; + const texPixels = new Uint8Array(width * height * 4); + this.renderTexture.readPixels(0, 0, + this.renderTexture.width, this.renderTexture.height, + texPixels); + } + } + ``` + + 如果要正确的显示绘制结果,请确保着色器拥有 `mainTexture` 属性并已在材质中启用。如果使用是 builtin-standard 着色器,请确保 **USE ALBEDO MAP** 选项被勾选: + + ![use albedo](render-texture/use-albedo.png) + +更多使用方法可参考范例 **RenderTexture**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/rendertexture) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/rendertexture))。 diff --git a/versions/4.0/zh/asset/render-texture/SampleFormRT.png b/versions/4.0/zh/asset/render-texture/SampleFormRT.png new file mode 100644 index 0000000000..82da886d34 Binary files /dev/null and b/versions/4.0/zh/asset/render-texture/SampleFormRT.png differ diff --git a/versions/4.0/zh/asset/render-texture/add-render-texture.png b/versions/4.0/zh/asset/render-texture/add-render-texture.png new file mode 100644 index 0000000000..904f5cf6f3 Binary files /dev/null and b/versions/4.0/zh/asset/render-texture/add-render-texture.png differ diff --git a/versions/4.0/zh/asset/render-texture/camera.png b/versions/4.0/zh/asset/render-texture/camera.png new file mode 100644 index 0000000000..71368ff093 Binary files /dev/null and b/versions/4.0/zh/asset/render-texture/camera.png differ diff --git a/versions/4.0/zh/asset/render-texture/render-texture-property.png b/versions/4.0/zh/asset/render-texture/render-texture-property.png new file mode 100644 index 0000000000..6c61f52114 Binary files /dev/null and b/versions/4.0/zh/asset/render-texture/render-texture-property.png differ diff --git a/versions/4.0/zh/asset/render-texture/sprite-rt.png b/versions/4.0/zh/asset/render-texture/sprite-rt.png new file mode 100644 index 0000000000..a4c23ed7b9 Binary files /dev/null and b/versions/4.0/zh/asset/render-texture/sprite-rt.png differ diff --git a/versions/4.0/zh/asset/render-texture/use-albedo.png b/versions/4.0/zh/asset/render-texture/use-albedo.png new file mode 100644 index 0000000000..553c3883d6 Binary files /dev/null and b/versions/4.0/zh/asset/render-texture/use-albedo.png differ diff --git a/versions/4.0/zh/asset/scene.md b/versions/4.0/zh/asset/scene.md new file mode 100644 index 0000000000..a998204f8a --- /dev/null +++ b/versions/4.0/zh/asset/scene.md @@ -0,0 +1,70 @@ +# 场景资源 + +在 Cocos Creator 3.0 中,游戏场景(Scene)是游戏开发时组织游戏内容的中心,也是呈现给玩家所有游戏内容的载体。而场景文件本身也作为游戏资源存在,并保存了游戏的大部分信息,也是创作的基础。 + +> **注意**:请尽量避免多人同时修改同一个场景资源,否则可能会导致冲突,且无法通过 `git` 合并解决冲突。 + +## 创建场景 + +创建场景目前有以下几种方式: + +1. 在 **资源管理器** 中右键点击想要放置场景文件的文件夹,然后选择 **创建 -> Scene** 即可。为了使项目具备良好的文件夹目录结构,强烈建议使用该方法创建场景。 + + ![create scene 1](scene/new_scene_1.png) + +2. 在 **资源管理器** 中点击左上角的 **+** 创建按钮,然后选择 **Scene** 即可。 + + ![create scene 2](scene/new_scene_2.png) + +3. 在顶部菜单栏中选择 **文件 -> 新建场景**,即可在 **场景编辑器** 中直接创建一个新场景。但 **资源管理器** 中不会出现新场景文件,需要在保存场景时弹出的 **保存场景** 窗口中手动保存场景文件,保存完成后才会在 **资源管理器** 的根目录下出现 `scene.scene` 场景文件。 + + ![create scene 3](scene/new_scene_3.png) + +## 保存场景 + +方法一:使用快捷键 Ctrl + S(Windows)或 Command + S(MacOS)来快速保存场景。 + +方法二:在顶部菜单栏中选择 **文件 -> 保存场景**。 + +## 切换场景 + +在 **资源管理器** 中,通过双击场景文件打开场景。
+如果需要在游戏过程中切换场景,可通过 [director.loadScene](%__APIDOC__%/zh/class/Director?id=loadscene) 等 API 来实现游戏中动态场景加载及切换。 + +## 场景属性 + +双击打开场景文件后,可以看到 **层级管理器** 中的 `scene` 是场景节点树的根节点。选中 `scene` 节点,在 **属性检查器** 中可设置场景是否自动释放,以及整个场景相关的属性,包括环境光设置,阴影设置、全局雾和天空盒设置。 + +![scene node set](scene/scene_node.png) + +场景资源的自动释放,详情请参考下文 **场景资源自动释放策略** 部分的内容。 + +各类属性的详细说明请参考: +- [环境光](../concepts/scene/light/lightType/ambient.md) +- [阴影](../concepts/scene/light/shadow.md) +- [全局雾](../concepts/scene/fog.md) +- [天空盒](../concepts/scene/skybox.md) +- [原生场景剔除](../advanced-topics/native-scene-culling.md) + +## 场景资源自动释放策略 + +如果项目中的场景很多,随着新场景的切换,内存占用就会不断上升。除了使用 `assetManager.releaseAsset` 等 API 来精确释放不使用的资源,我们还可以使用场景的自动释放功能。 + +双击打开场景文件后,在 **层级管理器** 选中 `scene` 节点,即可在 **属性检查器** 中设置场景是否自动释放: + +![scene node set](scene/scene_node_set.png) + +从当前场景切换到下一个场景时,如果当前场景不自动释放资源,则该场景中直接或间接引用到的所有资源(脚本动态加载的不算),**默认** 都不主动释放。反之如果启用了自动释放,则这些引用到的资源 **默认** 都会自动释放。 + +### 防止特定资源被自动释放 + +启用了某个场景的资源自动释放后,如果在脚本中保存了对该场景的资源的“特殊引用”,则当场景切换后,由于资源已经被释放,这些引用可能会变成非法的,有可能引起渲染异常等问题。为了让这部分资源在场景切换时不被释放,我们可以使用 [Asset.addRef](%__APIDOC__%/zh/class/Asset?id=addRef) 增加引用计数来锁住这些资源。 + +> “特殊引用”:以全局变量、单例、闭包、“动态资源”等形式进行的引用。
+> “动态资源”:在脚本中动态创建或动态修改的资源。 + +以上关于场景资源自动释放部分的内容可以归纳为下图中的几种情况: + +![release assets](scene/release-assets.png) + +关于资源释放,详细请查看 [资源释放](./release-manager.md#auto-release)。 diff --git a/versions/4.0/zh/asset/scene/new_scene_1.png b/versions/4.0/zh/asset/scene/new_scene_1.png new file mode 100644 index 0000000000..e601708be7 Binary files /dev/null and b/versions/4.0/zh/asset/scene/new_scene_1.png differ diff --git a/versions/4.0/zh/asset/scene/new_scene_2.png b/versions/4.0/zh/asset/scene/new_scene_2.png new file mode 100644 index 0000000000..64be724193 Binary files /dev/null and b/versions/4.0/zh/asset/scene/new_scene_2.png differ diff --git a/versions/4.0/zh/asset/scene/new_scene_3.png b/versions/4.0/zh/asset/scene/new_scene_3.png new file mode 100644 index 0000000000..565c953d4b Binary files /dev/null and b/versions/4.0/zh/asset/scene/new_scene_3.png differ diff --git a/versions/4.0/zh/asset/scene/release-assets.numbers b/versions/4.0/zh/asset/scene/release-assets.numbers new file mode 100644 index 0000000000..68293f5d23 Binary files /dev/null and b/versions/4.0/zh/asset/scene/release-assets.numbers differ diff --git a/versions/4.0/zh/asset/scene/release-assets.png b/versions/4.0/zh/asset/scene/release-assets.png new file mode 100644 index 0000000000..191ea2427b Binary files /dev/null and b/versions/4.0/zh/asset/scene/release-assets.png differ diff --git a/versions/4.0/zh/asset/scene/scene_node.png b/versions/4.0/zh/asset/scene/scene_node.png new file mode 100644 index 0000000000..fd090ce009 Binary files /dev/null and b/versions/4.0/zh/asset/scene/scene_node.png differ diff --git a/versions/4.0/zh/asset/scene/scene_node_set.png b/versions/4.0/zh/asset/scene/scene_node_set.png new file mode 100644 index 0000000000..af7dbe2d37 Binary files /dev/null and b/versions/4.0/zh/asset/scene/scene_node_set.png differ diff --git a/versions/4.0/zh/asset/script.md b/versions/4.0/zh/asset/script.md new file mode 100644 index 0000000000..fedf088905 --- /dev/null +++ b/versions/4.0/zh/asset/script.md @@ -0,0 +1,7 @@ +# 脚本资源 + +在 Cocos Creator 中,脚本也是资源的一部分。 + +关于脚本的详细介绍,可参考:[脚本指南](../scripting/index.md) + +关于脚本资源的创建和使用方法,详情可见:[脚本创建](../scripting/setup.md) diff --git a/versions/4.0/zh/asset/spine.md b/versions/4.0/zh/asset/spine.md new file mode 100644 index 0000000000..d009204fb5 --- /dev/null +++ b/versions/4.0/zh/asset/spine.md @@ -0,0 +1,85 @@ +# Spine 动画资源 + +Creator 中的骨骼动画资源是由 [Spine 编辑器](http://zh.esotericsoftware.com/) 导出的,目前支持 [JSON](http://zh.esotericsoftware.com/spine-export/#JSON) 和 [二进制](http://zh.esotericsoftware.com/spine-export/#%E4%BA%8C%E8%BF%9B%E5%88%B6) 两种数据格式。 + +各 Creator 版本对应支持的 Spine 版本如下所示: + +| Creator 版本 | Spine 版本 | +| :---------- | :-------- | +| v3.0 及以上 | v3.8(原生平台不支持特定版本 v3.8.75)| +| v2.3 及以上 | v3.8 | +| v2.2 | v3.7 | +| v2.0.8~v2.1 | v3.6 | +| v2.0.7 及以下 | v2.5 | + +## 导入 Spine 骨骼动画资源 + +骨骼动画所需资源有: + +- `.json/.skel` 骨骼数据 +- `.png` 图集纹理 +- `.txt/.atlas` 图集数据 + + ![spine](spine/import.png) + +## Spine 预览 + +在 v3.8.5 中,我们在 Spine 的属性查看器面板中,添加了预览支持。用户选中某个 Spine 文件后,就可以在面板中看到对应的渲染内容,能够极大地提升资源检查效率。 + +![preview](spine/spine-preview.png) + +## 创建 Spine 骨骼动画 + +从 **资源管理器** 中将骨骼动画资源拖动到 **属性检查器** Spine 组件的 SkeletonData 属性中: + +![spine](spine/set_skeleton.png) + +## 从服务器远程加载 Spine + +### 加载文本格式的 Spine 资源 + +```ts +let comp = this.getComponent('sp.Skeleton') as sp.Skeleton; + +let image = "http://localhost/download/spineres/1/1.png"; +let ske = "http://localhost/download/spineres/1/1.json"; +let atlas = "http://localhost/download/spineres/1/1.atlas"; +assetManager.loadAny([{ url: atlas, ext: '.txt' }, { url: ske, ext: '.txt' }], (error, assets) => { + assetManager.loadRemote(image, (error, img: ImageAsset) => { + let texture = new Texture2D(); + texture.image = img; + let asset = new sp.SkeletonData(); + asset.skeletonJson = assets[1]; + asset.atlasText = assets[0]; + asset.textures = [texture]; + asset.textureNames = ['1.png']; + asset._uuid = ske; // 可以传入任意字符串,但不能为空 + skeleton.skeletonData = asset; + }); +}); +``` + +### 加载二进制格式的 Spine 资源 + +```ts +let comp = this.getComponent('sp.Skeleton') as sp.Skeleton; + +let image = "http://localhost/download/spineres/1/1.png"; +let ske = "http://localhost/download/spineres/1/1.skel"; +let atlas = "http://localhost/download/spineres/1/1.atlas"; +assetManager.loadAny([{ url: atlas, ext: '.txt' }, { url: ske, ext: '.bin' }], (error, assets) => { + assetManager.loadRemote(image, (error, img: ImageAsset) => { + let texture = new Texture2D(); + texture.image = img; + let asset = new sp.SkeletonData(); + asset._nativeAsset = assets[1]; + asset.atlasText = assets[0]; + asset.textures = [texture]; + asset.textureNames = ['1.png']; + asset._uuid = ske; // 可以传入任意字符串,但不能为空 + asset._nativeURL = ske; // 传入一个二进制路径用作 initSkeleton 时的 filePath 参数使用 + comp.skeletonData = asset; + let ani = comp.setAnimation(0, 'walk', true); + }); +}); +``` diff --git a/versions/4.0/zh/asset/spine/import.png b/versions/4.0/zh/asset/spine/import.png new file mode 100644 index 0000000000..a1f2340347 Binary files /dev/null and b/versions/4.0/zh/asset/spine/import.png differ diff --git a/versions/4.0/zh/asset/spine/set_skeleton.png b/versions/4.0/zh/asset/spine/set_skeleton.png new file mode 100644 index 0000000000..61e9dfbc63 Binary files /dev/null and b/versions/4.0/zh/asset/spine/set_skeleton.png differ diff --git a/versions/4.0/zh/asset/spine/spine-preview.png b/versions/4.0/zh/asset/spine/spine-preview.png new file mode 100644 index 0000000000..fe8a1c69bc Binary files /dev/null and b/versions/4.0/zh/asset/spine/spine-preview.png differ diff --git a/versions/4.0/zh/asset/sprite-frame.md b/versions/4.0/zh/asset/sprite-frame.md new file mode 100644 index 0000000000..85efb24b27 --- /dev/null +++ b/versions/4.0/zh/asset/sprite-frame.md @@ -0,0 +1,110 @@ +# 精灵帧资源(SpriteFrame) + +Cocos Creator 的 SpriteFrame 是 UI 渲染基础图形的容器。其本身管理图像的裁剪和九宫格信息,默认持有一个与其同级的 Texture2D 资源引用。 + +## 导入精灵帧资源 + +使用默认的 [资源导入](asset-workflow.md) 方式将图像资源导入到项目中,然后在 **属性检查器** 中将图像资源的类型设置为 **sprite-frame**,并点击右上角的绿色打钩按钮保存: + +![set sprite-frame](sprite-frame/set-spriteframe.png) + +Creator 便会自动在导入的图像资源下创建一个如下图所示的 **spriteFrame** 资源: + +![spriteframe](sprite-frame/spriteframe.png) + +图像资源在 **资源管理器** 中会以自身图片的缩略图作为图标。在 **资源管理器** 中选中图像子资源后,**属性检查器** 下方会显示该图片的缩略图。 + +## 属性 + +spriteFrame 资源属性如下: + +| 属性 | 功能说明 | +| :--- | :--- | +| Packable | 是否参与动态合图以及自动图集的构建处理。详情请参考下文 **Packable** 部分的内容 | +| Rotated | 只读属性,不可更改。用于查看 Texture Packer 资源中的子资源是否被旋转 | +| Offset X、Y | 只读属性,不可更改。用于查看 Texture Packer 资源中矩形框的偏移量 | +| Trim Type | 裁剪类型,包括:
1. Auto — 自动裁剪(默认),详情请参考 [图像资源的自动剪裁](../ui-system/components/engine/trim.md)
2. Custom — 自定义裁剪
3. None — 无裁剪,使用原图 | +| Trim Threshold | 透明度阈值,默认为 1,取值范围为 0~1,会将透明度在设定值以下的像素裁减掉。当 Trim Type 设置为 **Auto** 时生效 | +| Trim X、Y、Width、Height | 设置裁剪矩形框,当 Trim Type 设置为 **Custom** 时生效 | +| Border Top、Bottom、Left、Right | 设置九宫格图边距,可点击下方的 **编辑** 按钮进行可视化编辑 | + +### Packable + +如果引擎开启了 [动态合图](../advanced-topics/dynamic-atlas.md) 功能,动态合图会自动将合适的贴图在开始场景时动态合并到一张大图上来减少 Drawcall。但是将贴图合并到大图中会修改原始贴图的 UV 坐标,如果在自定义 `effect` 中使用了贴图的 UV 坐标,这时 `effect` 中的 UV 计算将会出错,需要将贴图的 **Packable** 属性设置为 **false** 来避免贴图被打包到动态合图中。 + +## 使用 SpriteFrame + +### 使用 texture 渲染 + +将 SpriteFrame 资源拖拽到 [Sprite 组件](../ui-system/components/editor/sprite.md) 的 **SpriteFrame** 属性框中,即可切换 Sprite 显示的图像。 + +![use spriteframe](sprite-frame/use-spriteframe.png) + +在运行时,以上图中导入的名为 **content** 的图片为例,整个资源分为三部分: + +- **content**:图像源资源 ImageAsset +- **content** 的子资源 **spriteFrame**,即精灵帧资源 SpriteFrame +- **content** 的子资源 **texture**,即贴图资源 Texture2D + +当资源存放在 `resources` 目录下时,我们可直接加载到 spriteFrame 资源,代码示例如下: + +```typescript +const url = 'test_assets/test_atlas/content/spriteFrame'; +resources.load(url, SpriteFrame, (err: any, spriteFrame) => { + const sprite = this.getComponent(Sprite); + sprite.spriteFrame = spriteFrame; +}); +``` + +但在有些情况下只能加载到图像源资源 ImageAsset,因此我们提供了 [createWithImage](%__APIDOC__%/zh/class/SpriteFrame?id=createWithImage) 方法来帮助用户通过加载到的 ImageAsset 创建一个 SpriteFrame 资源。根据 ImageAsset 的来源不同,有以下两种创建方式: + +1. 存放在服务器上的资源只能加载到图像源资源 ImageAsset,加载方法请参考 [动态加载资源](./dynamic-load-resources.md)。创建 SpriteFrame 资源的代码示例如下: + + ```typescript + const self = this; + const url = 'test_assets/test_atlas/content'; + resources.load(url, ImageAsset, (err: any, imageAsset) => { + const sprite = this.getComponent(Sprite); + sprite.spriteFrame = SpriteFrame.createWithImage(imageAsset); + }); + ``` + + 或者用户也可以手动填充信息,代码示例如下: + + ```typescript + const self = this; + const url = 'test_assets/test_atlas/content'; + resources.load(url, ImageAsset, (err: any, imageAsset) => { + const sprite = this.getComponent(Sprite); + const spriteFrame = new SpriteFrame(); + const tex = new Texture2D(); + tex.image = imageAsset; + spriteFrame.texture = tex; + sprite.spriteFrame = spriteFrame; + }); + ``` + +2. 通过 Canvas 绘制的 ImageAsset 创建,代码示例如下: + + ```typescript + const sprite = this.getComponent(Sprite); + sprite.spriteFrame = SpriteFrame.createWithImage(canvas); + ``` + + 或者用户也可以手动填充信息,代码示例如下: + + ```typescript + const sprite = this.getComponent(Sprite); + const img = new ImageAsset(canvas); + const tex = new Texture2D(); + tex.image = img; + const sp = new SpriteFrame(); + sp.texture = tex; + sprite.spriteFrame = sp; + ``` + +### 使用 RenderTexture 渲染 + +RenderTexture 是一个渲染纹理,它可以将摄像机上的内容直接渲染到一张纹理上而不是屏幕上。SpriteFrame 通过管理 RenderTexture 可以轻松地将 3D 相机内容显示在 UI 上。具体的使用方法及代码示例可参考 [渲染纹理资源](render-texture.md)。 + +API 接口文档:[SpriteFrame 资源类型](%__APIDOC__%/zh/class/SpriteFrame)。 diff --git a/versions/4.0/zh/asset/sprite-frame/set-spriteframe.png b/versions/4.0/zh/asset/sprite-frame/set-spriteframe.png new file mode 100644 index 0000000000..7b0e3924e1 Binary files /dev/null and b/versions/4.0/zh/asset/sprite-frame/set-spriteframe.png differ diff --git a/versions/4.0/zh/asset/sprite-frame/spriteframe.png b/versions/4.0/zh/asset/sprite-frame/spriteframe.png new file mode 100644 index 0000000000..f7abdeee29 Binary files /dev/null and b/versions/4.0/zh/asset/sprite-frame/spriteframe.png differ diff --git a/versions/4.0/zh/asset/sprite-frame/use-spriteframe.png b/versions/4.0/zh/asset/sprite-frame/use-spriteframe.png new file mode 100644 index 0000000000..0bb011a738 Binary files /dev/null and b/versions/4.0/zh/asset/sprite-frame/use-spriteframe.png differ diff --git a/versions/4.0/zh/asset/subpackage-upgrade-guide.md b/versions/4.0/zh/asset/subpackage-upgrade-guide.md new file mode 100644 index 0000000000..65946cb76a --- /dev/null +++ b/versions/4.0/zh/asset/subpackage-upgrade-guide.md @@ -0,0 +1,42 @@ +# 资源分包升级指南 + +> 文:Santy-Wang、Xunyi +> +> 本文将详细介绍 Cocos Creator 3D 的小游戏子包升级到 Asset Bundle 的注意事项。v2.4 的资源分包与 v3.0 差别不大,无需升级。 + +在 v2.4 之前,[分包加载](https://github.com/cocos/cocos-docs/blob/e02ac31bab12d3ee767c0549050b0e42bd22bc5b/zh/scripting/subpackage.md) 功能仅支持各类小游戏平台,如微信小游戏、OPPO 小游戏等。但随着 Creator 的发展,开发者对分包的需求不断增加,例如多平台支持,原有的分包加载已经远远不能满足了。所以,Creator 从 v2.4 开始正式支持功能更为完整的 **Asset Bundle**。 + +- 对 **美术策划** 而言,项目中的所有资源,例如场景、动画、Prefab 都不需要修改,也不用升级。 +- 对 **程序** 而言,影响主要体现在原先代码中使用的 `loader.downloader.loadSubpackage` 需要改为 Asset Manager 中的 `assetManager.loadBundle`。以下将详细介绍这部分内容。 + +> **注意**:如果你在旧项目中使用了分包功能,也就是在 **属性检查器** 中勾选了 **配置为子包** 选项,那么当项目升级到 v2.4 之后,将自动转变为一个普通文件夹。你可以参考这里进行 Asset Bundle 的配置: + +[配置 Asset Bundle](bundle.md) + +## 需要手动升级的情况 + +你在自己的代码中使用了 `loader.downloader.loadSubpackage` 来加载分包。 + +## 升级步骤 + +- **备份好旧项目** +- 在 Dashboard 中使用 Cocos Creator v3.0 打开需要升级分包的旧项目,Creator 会对有影响的资源重新导入。第一次导入时会稍微多花一点时间,导入完毕后就会打开编辑器主窗口。然后使用代码编辑器将所有 `loader.downloader.loadSubpackage` 替换为 `assetManager.loadBundle`。 + + ```typescript + // 修改前 + loader.downloader.loadSubpackage('sub1', (err) => { + loader.loadRes('sub1/sprite-frames/background', SpriteFrame); + }); + + // 修改后 + assetManager.loadBundle('sub1', (err, bundle) => { + // 传入该资源相对 Asset Bundle 根目录的相对路径 + bundle.load('sprite-frames/background/spriteFrame', SpriteFrame); + }); + ``` + + > **注意**:加载 Asset Bundle 中的资源需要使用 Asset Bundle 相关的 API,具体请查看 API 文档 [Asset Bundle](%__APIDOC__%/zh/class/AssetManager.Bundle)。 + +## Asset Bundle 的使用方式 + +关于 Asset Bundle 的具体使用方式,请参考文档 [Asset Bundle](bundle.md)。 diff --git a/versions/4.0/zh/asset/subpackage/asset-bundle.png b/versions/4.0/zh/asset/subpackage/asset-bundle.png new file mode 100644 index 0000000000..965b850792 Binary files /dev/null and b/versions/4.0/zh/asset/subpackage/asset-bundle.png differ diff --git a/versions/4.0/zh/asset/subpackage/bundle1.png b/versions/4.0/zh/asset/subpackage/bundle1.png new file mode 100644 index 0000000000..11f78be994 Binary files /dev/null and b/versions/4.0/zh/asset/subpackage/bundle1.png differ diff --git a/versions/4.0/zh/asset/subpackage/bundle_md5.png b/versions/4.0/zh/asset/subpackage/bundle_md5.png new file mode 100644 index 0000000000..0683a785a2 Binary files /dev/null and b/versions/4.0/zh/asset/subpackage/bundle_md5.png differ diff --git a/versions/4.0/zh/asset/subpackage/inspector.png b/versions/4.0/zh/asset/subpackage/inspector.png new file mode 100644 index 0000000000..0cddd41ade Binary files /dev/null and b/versions/4.0/zh/asset/subpackage/inspector.png differ diff --git a/versions/4.0/zh/asset/subpackage/package.jpg b/versions/4.0/zh/asset/subpackage/package.jpg new file mode 100644 index 0000000000..3536c73f68 Binary files /dev/null and b/versions/4.0/zh/asset/subpackage/package.jpg differ diff --git a/versions/4.0/zh/asset/subpackage/profile.png b/versions/4.0/zh/asset/subpackage/profile.png new file mode 100644 index 0000000000..57818c878c Binary files /dev/null and b/versions/4.0/zh/asset/subpackage/profile.png differ diff --git a/versions/4.0/zh/asset/subpackage/subpackage.jpg b/versions/4.0/zh/asset/subpackage/subpackage.jpg new file mode 100644 index 0000000000..2159a988f1 Binary files /dev/null and b/versions/4.0/zh/asset/subpackage/subpackage.jpg differ diff --git a/versions/4.0/zh/asset/subpackage/subpackage2.png b/versions/4.0/zh/asset/subpackage/subpackage2.png new file mode 100644 index 0000000000..aecb36c8e3 Binary files /dev/null and b/versions/4.0/zh/asset/subpackage/subpackage2.png differ diff --git a/versions/4.0/zh/asset/text.md b/versions/4.0/zh/asset/text.md new file mode 100644 index 0000000000..6ea6fb6fe9 --- /dev/null +++ b/versions/4.0/zh/asset/text.md @@ -0,0 +1,61 @@ +# 文本资源 + +Creator 支持使用文本文件,常见的文本格式如:`.txt`、`.plist`、`.xml`、`.json`、`.yaml`、`.ini`、`.csv`、`.md`。通过 [资源导入](./asset-workflow.md#%E5%AF%BC%E5%85%A5%E8%B5%84%E6%BA%90) 的方式将其导入到编辑器,所有的文本文件都会导入为 `cc.TextAsset` 格式的资源。 + +## 使用方式 + +开发者可通过 **编辑器挂载** 和 **代码中动态加载** 两种方式获取文本数据。 + +### 通过编辑器 + +首先在 **资源管理器** 中新建一个 TypeScript,脚本内容示例如下: + +```ts +import { _decorator, Component, TextAsset } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('ItemTemplate') +export class ItemTemplate extends Component { + + // 声明属性 ‘itemGiftText‘ 的类型为 TextAsset + @property(TextAsset) + itemGiftText: TextAsset = null!; + + start () { + + const data: string = this.itemGiftText.text!; + + } +} +``` + +保存脚本内容后回到编辑器,将脚本挂载到相应的节点上,然后将 **资源管理器** 中的文本资源拖拽到脚本组件相应的属性框中。例如下图: + +![text](text/text.png) + +### 通过代码动态加载 + +开发者也可以直接通过代码 [动态加载](./dynamic-load-resources.md#%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD-resources) 来获取文本数据,代码示例如下: + +```ts +import { _decorator, Component, resources, error, TextAsset } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('ItemTemplate') +export class ItemTemplate extends Component { + + start () { + + resources.load('itemGiftText', (err: any, res: TextAsset) => { + if (err) { + error(err.message || err); + return; + } + + // 获取到文本数据 + const textData = res.text; + }) + + } +} +``` diff --git a/versions/4.0/zh/asset/text/text.png b/versions/4.0/zh/asset/text/text.png new file mode 100644 index 0000000000..9cfbcf2160 Binary files /dev/null and b/versions/4.0/zh/asset/text/text.png differ diff --git a/versions/4.0/zh/asset/texture-cube.md b/versions/4.0/zh/asset/texture-cube.md new file mode 100644 index 0000000000..bc046cec5e --- /dev/null +++ b/versions/4.0/zh/asset/texture-cube.md @@ -0,0 +1,37 @@ +# 立方体贴图 + +TextureCube 为立方体纹理,常用于设置场景的 [天空盒](../concepts/scene/skybox.md)。立方体贴图可以通过设置全景图 ImageAsset 为 TextureCube 类型获得,也可以在 Creator 中制作生成。 + +## 设置为立方体贴图 + +在将 ImageAsset [导入](asset-workflow.md) 到 Creator 后,即可在 **属性检查器** 面板将其设置为 **texture cube** 类型,设置完成后请点击右上角的绿色打钩按钮,以保存修改。 + +![set-texture-cube](texture/set-texture-cube.png) + +设置完成后在 **资源管理器** 面板可以看到原先的图像资源下方生成了一个 **textureCube** 子资源,以及组成 TextureCube 的六张 texture: + +![texture-cube](texture/texture-cube.png) + +## 制作立方体贴图 + +在 Creator 中通过制作 CubeMap 获得的 TextureCube 如下图: + +![CubeMap](../concepts/scene/skybox/cubemap-properties.png) + +关于 TextureCube 具体的使用,以及制作 CubeMap 的方式,请参考 [天空盒 — 设置 CubeMap](../concepts/scene/skybox.md)。 + +## 立方贴图的 mipmap 范围 + +TextureCube 可以在运行时动态选择 mipmap 的范围。设置完 mipmap 范围后,只有在范围之内的 mipmap 可以被使用。这允许我们通过跳过低层级来达到节约带宽的目的,同时也可以避免使用过高层级而降低效果。 + +可以通过以下方法设置 TextureCube 的 mipmap 层级范围: + +```Javascript +texture.setMipRange(minLevel, maxLevel); +``` + +其中 `minLevel` 指定了最小限制,`maxLevel` 指定了最大限制。 + +> **注意**: +> 1. 该限制无法超出已有的 mipmap 层级。 +> 2. 该方法对 WebGL 和 GLES2 后端无效。 diff --git a/versions/4.0/zh/asset/texture.md b/versions/4.0/zh/asset/texture.md new file mode 100644 index 0000000000..07bd49ad3b --- /dev/null +++ b/versions/4.0/zh/asset/texture.md @@ -0,0 +1,132 @@ +# 纹理贴图资源(Texture) + +纹理贴图资源是一种用于程序采样的资源,如模型上的贴图、精灵上的 UI。当程序渲染 UI 或者模型时,会使用纹理坐标获取纹理颜色,然后填充在模型网格上,再加上光照等等一系列处理便渲染出了整个场景。 + +纹理贴图资源可由图像资源(ImageAsset)转换而来,图像资源包括一些通用的图像转换格式如 PNG、JPEG 等等。 + +## Texture2D + +Texture2D 是纹理贴图资源的一种,通常用于 3D 模型的渲染,如模型材质中的反射贴图、环境光遮罩贴图等等。 + +在将图像资源 [导入](asset-workflow.md) 到 Creator 后,即可在 **属性检查器** 面板将其设置为 **texture** 类型,texture 类型便是 Texture2D 纹理资源。 + +![Texture2D](texture/set-texture.png) + +## Texture2D 属性 + +当导入图像资源时,编辑器默认将其设置为 **texture** 类型,并且在导入的图像资源下会自动创建一个或多个子资源,点击 **资源管理器** 中图像资源左侧的三角图标即可展开查看所有的子资源,如下图所示: + +![sub-texture](texture/sub-texture.png) + +选中生成的 Texture2D 子资源后可以在 **属性检查器** 设置相关属性: + +![ Texture2D 子资源](texture/sub-texture-pro.png) + +| 属性 | 说明 | +| :----------------- |:--- | +| **Anisotropy** | 各向异性值,应用各向异性过滤算法的最大阈值 | +| **Filter Mode** | 过滤模式,可选项为 **Nearest(None)**、**Bilinear**、**Bilinear with mipmaps**、**Trilinear with mipmaps** 以及 **Advanced**,请参考下方 **过滤方式** 以获取更多信息| +| **Wrap Mode** | 设置寻址模式,可选项为 **Repeat**、**Clamp**、**Mirror** 以及 **Advanced**
在选择 **Advanced** 时,设置 S(U)/ T(V)方向上的纹理寻址模式,也就是像素对纹理在 S(U)或者 T(V)方向上的映射模式,请参考下文 **寻址模式** 以获取更多信息 | + +> **注意**:由于默认的 Wrap Mode 在渲染图像的透明边缘时可能会出现黑边,所以在将图像资源类型设置为 **sprite-frame** 时,Creator 会自动将 texture 资源的 Wrap Mode S 和 Wrap Mode T 属性自动调整为 **clamp-to-edge**。如有特殊需要,开发者可以自行修改。 + +接下来我们对部分属性进行简单的说明。 + +### 过滤方式 + +![filter mode](./texture/filter-mode.png) + +当 Texture2D 的原始大小与屏幕映射的纹理图像尺寸不一致时,通过不同的纹理过滤方式进行纹理单元到像素的映射会产生不同的效果。 + +Texture2D 中的 **Min Filter** 和 **Mag Filter** 属性,分别用于设置在缩小或者放大贴图时采用的纹理过滤方式: + +1. 邻近过滤(`nearest`) + + 邻近过滤是 **默认** 使用的纹理过滤方式。使用中心位置距离采样点最近的纹理单元颜色值作为该采样点的颜色值,不考虑其他相邻像素的影响。
+ 需要注意的是使用临近过滤方式可能会出现边缘不平滑,锯齿较为明显的情况。 + +2. 线性过滤(`linear`) + + 线性过滤使用距离采样点最近的 2 x 2 的纹理单元矩阵进行采样,取四个纹理单元颜色值的平均值作为采样点的颜色,像素之间的颜色值过渡会更加平滑。
+ 需要注意的是使用线性过滤方式可能会出现边缘黑边的情况,如果是像素类游戏,可能会出现模糊情况。 + +### Generate Mipmaps + +为了加快 3D 场景渲染速度和减少图像锯齿,贴图被处理成由一系列被预先计算和优化过的图片组成的序列,这样的贴图被称为 mipmap。mipmap 中每一个层级的小图都是原图的一个特定比例的缩小细节的复制品,当贴图被缩小或者只需要从远距离观看时,mipmap 就会转换到适当的层级。 + +![generate-mipmaps](./texture/generate-mipmaps.png) + +当勾选 **Generate Mipmaps** 属性或者将 texture 的 **Filter Mode** 选择为 **Bilinear with mipmaps** 或 **Trilinear with mipmaps** 时,会在两个相近的层级之间插值,自动生成 mipmap。因为渲染远距离物体时,mipmap 贴图比原图小,提高了显卡采样过程中的缓存命中率,所以渲染的速度得到了提升。同时因为 mipmap 的小图精度较低,从而减少了摩尔纹现象,可以减少画面上的锯齿。另外因为额外生成了一些小图,所以 mipmap 需要额外占用约三分之一的内存空间。 + +Texture2D 可以在运行时动态选择 mipmap 的范围。设置完 mipmap 范围后,只有在范围之内的 mipmap 可以被使用。这允许我们通过跳过低层级来达到节约带宽的目的,同时也可以避免使用过高层级而降低效果。 + +可以通过以下方法设置 Texture2D 的 mipmap 层级范围: + +```Javascript +texture.setMipRange(minLevel, maxLevel); +``` + +其中 `minLevel` 指定了最小限制,`maxLevel` 指定了最大限制。 + +> **注意**: +> 1. 该限制无法超出已有的 mipmap 层级。 +> 2. 该方法对 WebGL 和 GLES2 后端无效。 + +### 寻址模式 + +一般来说,纹理坐标在水平轴(U)和垂直轴(V)的取值范围为 `[0,1]`,当传递的顶点数据中的纹理坐标取值超出 `[0,1]` 范围时,就可以通过不同的寻址模式来控制超出范围的纹理坐标如何进行纹理映射。 + +Texture2D 中的 **Wrap Mode S** 和 **Wrap Mode T** 属性便是分别用于设置纹理在 UV 方向上的寻址模式: + +1. 重复寻址模式(`repeat`) + + 重复寻址模式是默认使用的纹理寻址模式。对于超出 `[0,1]` 范围的纹理坐标,使用 `[0,1]` 内的纹理坐标内容不断重复,也就是在超出纹理坐标范围外的地方重复放置纹理贴图。 + + > **注意**:在 WebGL1.0 平台,当贴图宽高不是二次幂时,`repeat` 寻址模式是无效的。运行时会自动将其切换为 `clamp-to-edge` 寻址模式,这将会使材质的 `tilingOffset` 等属性失效。 + + 当 Texture2D 的 **Wrap Mode S** 和 **Wrap Mode T** 属性都设置为 `repeat` 时,效果图如下: + + ![repeat](texture/repeat.png) + +2. 钳位寻址模式(`clamp-to-edge`) + + 将纹理坐标约束在 0 到 1 之间,只复制一遍 `[0,1]` 的纹理坐标。对于 `[0,1]` 之外的部分,将使用边缘的纹理坐标内容进行延伸,产生类似边缘被拉伸的效果。 + + 当两个属性都设置为 `clamp-to-edge` 时,效果图如下: + + ![clamp-to-edge](texture/clamp-to-edge.png) + +3. 镜像寻址模式(`mirrored-repeat`) + + 类似重复寻址模式,只不过贴图是镜像重复放置的。 + + 当两个属性都设置为 `mirrored-repeat` 时,效果图如下: + + ![mirrored-repeat](texture/mirrored-repeat.png) + +## 使用 Texture2D + +Texture2D 是使用范围非常广泛的资源,在 Creator 中的使用方式主要包括在编辑器中使用和动态获取使用: + +- 在编辑器的 **属性检查器** 面板中,任何标记为 Texture2D 类型的属性,都可以将 Texture2D 资源拖拽到属性框中使用。例如设置材质资源的 Texture2D 类型资源: + + ![mirrored-repeat](texture/use-texture2d.png) + + > **注意**:如果材质中没有定义 `USE TEXTURE` 就没有该属性。 + +- 动态使用时,需要先获取图像资源(ImageAsset),然后根据获取到的 ImageAsset 实例化出 Texture2D 资源。 + + ```ts + resources.load("testAssets/image/texture", Texture2D, (err: any, texture: Texture2D) => { + const spriteFrame = new SpriteFrame(); + spriteFrame.texture = texture; + this.node.getComponent(Sprite).spriteFrame = spriteFrame; + }); + ``` + + 详情请参考 [资源加载](./dynamic-load-resources.md#%E5%8A%A0%E8%BD%BD-spriteframe-%E6%88%96-texture2d)。 + +## 参考链接 + +[LearnOpenGL — 纹理](https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/#_1) + \ No newline at end of file diff --git a/versions/4.0/zh/asset/texture/clamp-to-edge.png b/versions/4.0/zh/asset/texture/clamp-to-edge.png new file mode 100644 index 0000000000..4d27cd9d6b Binary files /dev/null and b/versions/4.0/zh/asset/texture/clamp-to-edge.png differ diff --git a/versions/4.0/zh/asset/texture/filter-mode.png b/versions/4.0/zh/asset/texture/filter-mode.png new file mode 100644 index 0000000000..3486273560 Binary files /dev/null and b/versions/4.0/zh/asset/texture/filter-mode.png differ diff --git a/versions/4.0/zh/asset/texture/generate-mipmaps.png b/versions/4.0/zh/asset/texture/generate-mipmaps.png new file mode 100644 index 0000000000..e75acbcc24 Binary files /dev/null and b/versions/4.0/zh/asset/texture/generate-mipmaps.png differ diff --git a/versions/4.0/zh/asset/texture/imported.png b/versions/4.0/zh/asset/texture/imported.png new file mode 100644 index 0000000000..f94151640a Binary files /dev/null and b/versions/4.0/zh/asset/texture/imported.png differ diff --git a/versions/4.0/zh/asset/texture/mirrored-repeat.png b/versions/4.0/zh/asset/texture/mirrored-repeat.png new file mode 100644 index 0000000000..1e07ceac2a Binary files /dev/null and b/versions/4.0/zh/asset/texture/mirrored-repeat.png differ diff --git a/versions/4.0/zh/asset/texture/normal-map.png b/versions/4.0/zh/asset/texture/normal-map.png new file mode 100644 index 0000000000..2d28fc5c03 Binary files /dev/null and b/versions/4.0/zh/asset/texture/normal-map.png differ diff --git a/versions/4.0/zh/asset/texture/repeat.png b/versions/4.0/zh/asset/texture/repeat.png new file mode 100644 index 0000000000..203af666f4 Binary files /dev/null and b/versions/4.0/zh/asset/texture/repeat.png differ diff --git a/versions/4.0/zh/asset/texture/set-texture-cube.png b/versions/4.0/zh/asset/texture/set-texture-cube.png new file mode 100644 index 0000000000..cbd7e68270 Binary files /dev/null and b/versions/4.0/zh/asset/texture/set-texture-cube.png differ diff --git a/versions/4.0/zh/asset/texture/set-texture.png b/versions/4.0/zh/asset/texture/set-texture.png new file mode 100644 index 0000000000..b26e343336 Binary files /dev/null and b/versions/4.0/zh/asset/texture/set-texture.png differ diff --git a/versions/4.0/zh/asset/texture/sub-texture-pro.png b/versions/4.0/zh/asset/texture/sub-texture-pro.png new file mode 100644 index 0000000000..a78a4b9437 Binary files /dev/null and b/versions/4.0/zh/asset/texture/sub-texture-pro.png differ diff --git a/versions/4.0/zh/asset/texture/sub-texture.png b/versions/4.0/zh/asset/texture/sub-texture.png new file mode 100644 index 0000000000..1dc1b8b828 Binary files /dev/null and b/versions/4.0/zh/asset/texture/sub-texture.png differ diff --git a/versions/4.0/zh/asset/texture/texture-cube.png b/versions/4.0/zh/asset/texture/texture-cube.png new file mode 100644 index 0000000000..f9fb7100c0 Binary files /dev/null and b/versions/4.0/zh/asset/texture/texture-cube.png differ diff --git a/versions/4.0/zh/asset/texture/type-change.png b/versions/4.0/zh/asset/texture/type-change.png new file mode 100644 index 0000000000..3107a3a43d Binary files /dev/null and b/versions/4.0/zh/asset/texture/type-change.png differ diff --git a/versions/4.0/zh/asset/texture/use-texture2d.png b/versions/4.0/zh/asset/texture/use-texture2d.png new file mode 100644 index 0000000000..1a7242ad4f Binary files /dev/null and b/versions/4.0/zh/asset/texture/use-texture2d.png differ diff --git a/versions/4.0/zh/asset/tiledmap.md b/versions/4.0/zh/asset/tiledmap.md new file mode 100644 index 0000000000..93e9c09e0d --- /dev/null +++ b/versions/4.0/zh/asset/tiledmap.md @@ -0,0 +1,103 @@ +# 瓦片图资源(TiledMap) + +## 问题分析 +由于使用图集时,如果没有设置1像素的拉伸,会与相邻的图片发生混合,导致边缘颜色不对,例如出现黑边的问题。 +因此在制作图集时,需要针对每个图片拉伸一像素作为边缘像素。 + +引擎之前的实现是自动处理这种问题,会缩进1像素,代码: +``` +if (spFrame) { + grid._name = spFrame.name; + const lm = spFrame.unbiasUV[0]; + const bm = spFrame.rotated ? spFrame.unbiasUV[1] : spFrame.unbiasUV[5]; + grid.l = lm + (grid.x + 0.5) / texWidth; + grid.t = bm + (grid.y + 0.5) / texHeight; + grid.r = lm + (grid.x + grid.width - 0.5) / texWidth; + grid.b = bm + (grid.y + grid.height - 0.5) / texHeight; + grid._rect = new Rect(grid.x, grid.y, grid.width, grid.height); +} +``` +但是这样会产生额外的问题,会让UI设计的好地图可能看起来有明显的缩进,类似[issue](https://github.com/cocos/cocos-engine/issues/17257) + +为了解决这个问题,在**3.8.5**版本里,我们将移除自动缩进1像素的代码,并且需要用户制作图集时,需要设置延伸1像素。 + +## 制作瓦片贴图的图集 +一般需要制作成图集,可以使用[TexturePacker](https://www.codeandweb.com/texturepacker)工具。 +- 把素材添加到工具中,如下图: +![alt text](tiledmap/image.png) +- 整理导出的图集布局,并设置 +![alt text](tiledmap/image-14.png) +**这里需要拉伸1像素,因为相邻的两个纹理如果不拉伸一个像素,使用的时候会出现边缘异常** +- 发布精灵,然后设置保存路径即可 +![alt text](tiledmap/image-2.png) + +## 使用tiled创建与编辑地图 +瓦片图资源是由 [Tiled 编辑器](https://www.mapeditor.org/) 所导出的数据格式。 + +| Creator 版本 | Tiled 版本 | +| :---------- | :-------- | +| v3.0 及以上 | v1.4 | +| v2.2 及以上 | v1.2.0 | +| v2.1 及以下 | v1.0.0 | + +- 创建地图块 + +![alt text](tiledmap/image-3.png) +- 设置地图块大小 + +![alt text](tiledmap/image-4.png) +![alt text](tiledmap/image-5.png) + +- 新建图集块 + +![alt text](tiledmap/image-6.png) + +- 选择使用上面texturepacker制作的图集 + +![alt text](tiledmap/image-8.png) + + +**这里需要设置为要使用原图片的大小,即32X32,并且设置margin为1(图片偏移上下左右1像素),间距设置为2(左右1像素,就是两个像素,上下也一样)。如果在texturepacker里没设置拉伸,这里就需要设置margin为0,间距设置为0** + +- 完成地图,如下图: +![alt text](tiledmap/image-12.png) + +- 保存tmx文件 + + +## 导入地图资源 + +地图所需资源有: + +- `.tmx` 地图数据 +- `.png` 图集纹理 +- `.tsx` tileset 数据配置文件(部分 tmx 文件需要) + + ![tiledmap](tiledmap/import.png) + +## 创建瓦片图资源 + +从 **资源管理器** 里将地图资源拖动到已创建 TiledMap 组件的 Tmx File 属性中: + +![tiledmap](tiledmap/set_asset.png) + +## 在项目中的存放 + +为了提高资源管理效率,建议将导入的 `tmx`、`tsx` 和 `png` 文件存放在单独的目录下,不要和其他资源混在一起。需要注意的是要把 `tmx` 文件和 `tsx` 文件放在同一目录管理,否则可能会导致资源无法被正确加载。 + +### 注意事项 +1. 如果在texturepacker不设置拉伸则效果会有偏差 + +不使用拉伸,效果如下图: +![alt text](tiledmap/image-15.png) +使用拉伸,效果如下图: +![alt text](tiledmap/image-16.png) + +2. 如果存在老的图集和地图,可以采用更新图集的方式,不需要重新制作 + +(1)新增一个新的图集,采用新的配置,注意不要使用嵌入地图 +![alt text](tiledmap/image-1.png) + +(2)在旧的图集tab里,选择点击替换图集,选择在(1)中新增的图集即可替换成新的图集即可完成更新 + +![alt text](tiledmap/image-7.png) \ No newline at end of file diff --git a/versions/4.0/zh/asset/tiledmap/image-1.png b/versions/4.0/zh/asset/tiledmap/image-1.png new file mode 100644 index 0000000000..2994c53a49 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-1.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-12.png b/versions/4.0/zh/asset/tiledmap/image-12.png new file mode 100644 index 0000000000..a7a73477b8 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-12.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-14.png b/versions/4.0/zh/asset/tiledmap/image-14.png new file mode 100644 index 0000000000..ad1b6a772d Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-14.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-15.png b/versions/4.0/zh/asset/tiledmap/image-15.png new file mode 100644 index 0000000000..9d4c72d889 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-15.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-16.png b/versions/4.0/zh/asset/tiledmap/image-16.png new file mode 100644 index 0000000000..07e5b59f19 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-16.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-2.png b/versions/4.0/zh/asset/tiledmap/image-2.png new file mode 100644 index 0000000000..9bac168b3d Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-2.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-3.png b/versions/4.0/zh/asset/tiledmap/image-3.png new file mode 100644 index 0000000000..4320465be7 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-3.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-4.png b/versions/4.0/zh/asset/tiledmap/image-4.png new file mode 100644 index 0000000000..8a95760a8e Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-4.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-5.png b/versions/4.0/zh/asset/tiledmap/image-5.png new file mode 100644 index 0000000000..5371ed3114 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-5.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-6.png b/versions/4.0/zh/asset/tiledmap/image-6.png new file mode 100644 index 0000000000..70aae9aaf6 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-6.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-7.png b/versions/4.0/zh/asset/tiledmap/image-7.png new file mode 100644 index 0000000000..4e8ff45f21 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-7.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-8.png b/versions/4.0/zh/asset/tiledmap/image-8.png new file mode 100644 index 0000000000..cadba31313 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-8.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image-9.png b/versions/4.0/zh/asset/tiledmap/image-9.png new file mode 100644 index 0000000000..6d4f2a1078 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image-9.png differ diff --git a/versions/4.0/zh/asset/tiledmap/image.png b/versions/4.0/zh/asset/tiledmap/image.png new file mode 100644 index 0000000000..37bc41bb01 Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/image.png differ diff --git a/versions/4.0/zh/asset/tiledmap/import.png b/versions/4.0/zh/asset/tiledmap/import.png new file mode 100644 index 0000000000..ad4698cdab Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/import.png differ diff --git a/versions/4.0/zh/asset/tiledmap/set_asset.png b/versions/4.0/zh/asset/tiledmap/set_asset.png new file mode 100644 index 0000000000..a5844582bf Binary files /dev/null and b/versions/4.0/zh/asset/tiledmap/set_asset.png differ diff --git a/versions/4.0/zh/audio-system/audio/audioEdit.png b/versions/4.0/zh/audio-system/audio/audioEdit.png new file mode 100644 index 0000000000..f9c7f5677b Binary files /dev/null and b/versions/4.0/zh/audio-system/audio/audioEdit.png differ diff --git a/versions/4.0/zh/audio-system/audio/audiocilp.gif b/versions/4.0/zh/audio-system/audio/audiocilp.gif new file mode 100644 index 0000000000..869f06cbcf Binary files /dev/null and b/versions/4.0/zh/audio-system/audio/audiocilp.gif differ diff --git a/versions/4.0/zh/audio-system/audio/audiocontroller.png b/versions/4.0/zh/audio-system/audio/audiocontroller.png new file mode 100644 index 0000000000..02cfaece28 Binary files /dev/null and b/versions/4.0/zh/audio-system/audio/audiocontroller.png differ diff --git a/versions/4.0/zh/audio-system/audio/audiosource.png b/versions/4.0/zh/audio-system/audio/audiosource.png new file mode 100644 index 0000000000..fbbd089f84 Binary files /dev/null and b/versions/4.0/zh/audio-system/audio/audiosource.png differ diff --git a/versions/4.0/zh/audio-system/audioExample.md b/versions/4.0/zh/audio-system/audioExample.md new file mode 100644 index 0000000000..ff2d241b73 --- /dev/null +++ b/versions/4.0/zh/audio-system/audioExample.md @@ -0,0 +1,124 @@ +# 音频播放管理器示例 + +由于 Cocos Creator 3.x 移除了 v2.x `cc.audioEngine` 系列的 API,统一使用 AudioSource 控制音频播放。 + +但在实际项目开发中,我们仍然需要一个方便随时调用的音频播放管理器,可参考或直接使用以下代码: + +```typescript +//AudioMgr.ts +import { Node, AudioSource, AudioClip, resources, director } from 'cc'; +/** + * @en + * this is a sington class for audio play, can be easily called from anywhere in you project. + * @zh + * 这是一个用于播放音频的单件类,可以很方便地在项目的任何地方调用。 + */ +export class AudioMgr { + private static _inst: AudioMgr; + public static get inst(): AudioMgr { + if (this._inst == null) { + this._inst = new AudioMgr(); + } + return this._inst; + } + + private _audioSource: AudioSource; + constructor() { + //@en create a node as audioMgr + //@zh 创建一个节点作为 audioMgr + let audioMgr = new Node(); + audioMgr.name = '__audioMgr__'; + + //@en add to the scene. + //@zh 添加节点到场景 + director.getScene().addChild(audioMgr); + + //@en make it as a persistent node, so it won't be destroied when scene change. + //@zh 标记为常驻节点,这样场景切换的时候就不会被销毁了 + director.addPersistRootNode(audioMgr); + + //@en add AudioSource componrnt to play audios. + //@zh 添加 AudioSource 组件,用于播放音频。 + this._audioSource = audioMgr.addComponent(AudioSource); + } + + public get audioSource() { + return this._audioSource; + } + + /** + * @en + * play short audio, such as strikes,explosions + * @zh + * 播放短音频,比如 打击音效,爆炸音效等 + * @param sound clip or url for the audio + * @param volume + */ + playOneShot(sound: AudioClip | string, volume: number = 1.0) { + if (sound instanceof AudioClip) { + this._audioSource.playOneShot(sound, volume); + } + else { + resources.load(sound, (err, clip: AudioClip) => { + if (err) { + console.log(err); + } + else { + this._audioSource.playOneShot(clip, volume); + } + }); + } + } + + /** + * @en + * play long audio, such as the bg music + * @zh + * 播放长音频,比如 背景音乐 + * @param sound clip or url for the sound + * @param volume + */ + play(sound: AudioClip | string, volume: number = 1.0) { + if (sound instanceof AudioClip) { + this._audioSource.stop(); + this._audioSource.clip = sound; + this._audioSource.play(); + this.audioSource.volume = volume; + } + else { + resources.load(sound, (err, clip: AudioClip) => { + if (err) { + console.log(err); + } + else { + this._audioSource.stop(); + this._audioSource.clip = clip; + this._audioSource.play(); + this.audioSource.volume = volume; + } + }); + } + } + + /** + * stop the audio play + */ + stop() { + this._audioSource.stop(); + } + + /** + * pause the audio play + */ + pause() { + this._audioSource.pause(); + } + + /** + * resume the audio play + */ + resume(){ + this._audioSource.play(); + } +} +``` diff --git a/versions/4.0/zh/audio-system/audioLimit.md b/versions/4.0/zh/audio-system/audioLimit.md new file mode 100644 index 0000000000..239bce3b5e --- /dev/null +++ b/versions/4.0/zh/audio-system/audioLimit.md @@ -0,0 +1,21 @@ +# 兼容性说明 + +## Web 平台音频资源的加载模式 + +Web 平台上的音频资源比较特别,因为 Web 标准支持以两种不同的方式加载音频资源,分别是: +- Web Audio:提供相对更加现代化的声音控制接口,在引擎内是以一个 audio buffer 的形式缓存的。这种方式的优点是兼容性好,问题比较少。 +- DOM Audio:通过生成一个标准的 audio 元素来播放音频资源,在引擎内缓存的就是这个 audio 元素。使用标准的 audio 元素播放音频资源时,在某些浏览器上可能会遇到一些兼容性问题,比如 iOS 上的浏览器不支持调整音量大小,所有 volume 相关属性将不会生效。 + +目前 Cocos Creator 默认以 Web Audio 的方式加载音频资源,但如果检测到当前浏览器不支持加载 Web Audio,则会切换使用 DOM Audio 的方式加载音频。 + +如果项目需要强制通过 DOM Audio 的方式加载音频资源,请使用以下方式动态加载: + +```typescript +assetManager.loadRemote('http://example.com/background.mp3', { + audioLoadMode: AudioClip.AudioType.DOM_AUDIO +}, (err, clip: AudioClip) => { + if(err){ + console.log(err); + } +}); +``` diff --git a/versions/4.0/zh/audio-system/audiosource.md b/versions/4.0/zh/audio-system/audiosource.md new file mode 100644 index 0000000000..af0eef79c7 --- /dev/null +++ b/versions/4.0/zh/audio-system/audiosource.md @@ -0,0 +1,156 @@ +# AudioSource 组件参考 + +AudioSource 组件用于控制音乐和音效的播放。 + +![audioSource](audio/audiosource.png) + +在 **层级管理器** 中选中节点,然后点击 **属性检查器** 下方的 **添加组件** 按钮,选择 **Audio -> AudioSource** 即可添加 AudioSource 组件到节点上。 + +## AudioSource 属性 + +|属性 | 说明 | +|:-- | :-- | +|Clip | 添加的用于播放的 [音频资源](../asset/audio.md),默认为空,点击后面的箭头按钮即可选择 | +|Loop | 是否循环播放 | +|PlayOnAwake | 是否在游戏运行(组件激活)时自动播放音频 | +|Volume | 音量大小,范围在 0~1 之间 | + +## 音频播放 + +Cocos Creator 3.x 使用 AudioSource 控制音频的播放。AudioSource 是组件,可以添加到场景中,由 **编辑器** 设置,也可以在 **脚本** 中进行调用。 + +另外,Creator 根据音频的长短将其分为较长的 **音乐** 和短的 **音效** 两种: + +- 若通过编辑器控制音频播放,则播放音乐和音效没有区别,但推荐使用长音乐。详情可参考下文 **通过编辑器播放** 部分的内容。 +- 若通过脚本控制音频播放,则 AudioSource 组件额外提供了 `playOneShot` 接口用于播放短音效,详情请参考下文 **音效播放** 部分的内容。 + +> **注意**:Cocos Creator 3.x 移除了 v2.x 中的 `audioEngine` API,统一使用 AudioSource 组件播放音频。 + +### 通过编辑器 + +1. 在节点上添加 AudioSource 组件。 + +2. 将所需的音频资源从 **资源管理器** 拖拽到 AudioSource 组件的 Clip 属性框中,如下所示: + + ![audioClip](audio/audiocilp.gif) + +3. 根据需要对 AudioSource 组件的其他属性进行设置即可。 + +### 通过脚本 + +如果要更灵活地控制 AudioSource 播放音频,可以将自定义脚本添加到 **AudioSource 组件** 所在的节点,然后调用相应的 API 来控制音频播放。 + +1. 在节点上添加 AudioSource 组件并指定音频资源。 + +2. 在 **资源管理器** 中 [创建脚本](../scripting/setup.md) 并命名(例如 `AudioController`),然后双击打开脚本进行编写,内容如下: + + ```typescript + import { _decorator, Component, Node, AudioSource, assert } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass("AudioController") + export class AudioController extends Component { + + @property(AudioSource) + public _audioSource: AudioSource = null!; + + onLoad () { + // 获取 AudioSource 组件 + const audioSource = this.node.getComponent(AudioSource)!; + // 检查是否含有 AudioSource,如果没有,则输出错误消息 + assert(audioSource); + // 将组件赋到全局变量 _audioSource 中 + this._audioSource = audioSource; + } + + play () { + // 播放音乐 + this._audioSource.play(); + } + + pause () { + // 暂停音乐 + this._audioSource.pause(); + } + } + ``` + +3. 在 **层级管理器** 选中节点,然后将 **资源管理器** 中的脚本拖拽到 **属性检查器** 即可添加脚本组件到节点。如下所示: + + ![audioSource](audio/audiocontroller.png) + +#### 音效播放 + +相较于长的音乐播放,音效播放具有以下特点: + +- 播放时间短 +- 同时播放的数量多 + +**AudioSource** 组件提供了 `playOneShot` 接口来播放音效。具体代码实现如下: + +```typescript +// AudioController.ts + +import { AudioClip, AudioSource, Component, _decorator } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("AudioController") +export class AudioController extends Component { + + @property(AudioClip) + public clip: AudioClip = null!; + + @property(AudioSource) + public audioSource: AudioSource = null!; + + playOneShot () { + this.audioSource.playOneShot(this.clip, 1); + } +} +``` + +> **注意**:`playOneShot` 是一次性播放操作,播放后的音效无法暂停或停止播放,也无法监听播放结束的事件回调。 + +更多音频相关的脚本接口请参考 [AudioSource API](%__APIDOC__%/zh/class/AudioSource)。 + +更多对音频的播放控制,可以参考文档 [AudioSource 播放示例](./audioExample.md)。 + +## 监听音频播放事件 + +AudioSource 组件从 v3.3.0 开始支持了事件监听接口,具体使用范例如下: + +```typescript +import { _decorator, Component, Node, AudioSource } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('AudioDemo') +export class AudioDemo extends Component { + + @property(AudioSource) + audioSource: AudioSource = null!; + + onEnable () { + // Register the started event callback + this.audioSource.node.on(AudioSource.EventType.STARTED, this.onAudioStarted, this); + // Register the ended event callback + this.audioSource.node.on(AudioSource.EventType.ENDED, this.onAudioEnded, this); + } + + onDisable () { + this.audioSource.node.off(AudioSource.EventType.STARTED, this.onAudioStarted, this); + this.audioSource.node.off(AudioSource.EventType.ENDED, this.onAudioEnded, this); + } + + onAudioStarted () { + // TODO... + } + + onAudioEnded () { + // TODO... + } +} +``` + +## Web 平台的播放限制 + +目前 Web 平台的音频播放需要遵守最新的 [Audio Play Policy](https://www.chromium.org/audio-video/autoplay),即使 **AudioSource** 组件设置了 `playOnAwake`,也需要在第一次用户点击事件发生后,才会播放。 diff --git a/versions/4.0/zh/audio-system/overview.md b/versions/4.0/zh/audio-system/overview.md new file mode 100644 index 0000000000..902778258b --- /dev/null +++ b/versions/4.0/zh/audio-system/overview.md @@ -0,0 +1,8 @@ +# 音频系统 + +音乐是游戏中不可或缺的一部分,好的音乐能让游戏更加真实、富有沉浸感。Cocos Creator 的音频系统支持导入并播放大多数常见的音频文件格式,具体内容请参考: + +- [音频资源](../asset/audio.md) +- [AudioSource 组件参考](./audiosource.md) +- [AudioSource 播放示例](./audioExample.md) +- [兼容性说明](./audioLimit.md) diff --git a/versions/4.0/zh/book.json b/versions/4.0/zh/book.json new file mode 100644 index 0000000000..b139e0aa74 --- /dev/null +++ b/versions/4.0/zh/book.json @@ -0,0 +1,109 @@ +{ + "language" : "zh-hans", + "structure": { + "summary": "SUMMARY.md", + "readme": "index.md" + }, + "variables": { + "docType": "手册", + "products": [ + { + "name": "Cocos Creator", + "links": [ + { + "name": "手册文档", + "link": "https://docs.cocos.com/creator/4.0/manual/zh" + }, + { + "name": "API 参考", + "link": "https://docs.cocos.com/creator/4.0/api/zh/" + } + ] + }, + { + "name": "Cocos2d-x", + "links": [ + { + "name": "手册文档", + "link": "https://docs.cocos.com/cocos2d-x/manual/zh" + }, + { + "name": "API 参考", + "link": "http://docs.cocos2d-x.org/api-ref/index.html" + } + ] + } + ], + "version": [ + { + "name": "3.8", + "links": [ + { + "name": "3.8", + "link": "https://docs.cocos.com/creator/4.0/manual/zh" + }, + { + "name": "3.7", + "link": "https://docs.cocos.com/creator/3.7/manual/zh" + }, + { + "name": "3.6", + "link": "https://docs.cocos.com/creator/3.6/manual/zh" + }, + { + "name": "3.5", + "link": "https://docs.cocos.com/creator/3.5/manual/zh" + }, + { + "name": "3.4", + "link": "https://docs.cocos.com/creator/3.4/manual/zh" + }, + { + "name": "3.3", + "link": "https://docs.cocos.com/creator/3.3/manual/zh" + }, + { + "name": "3.2", + "link": "https://docs.cocos.com/creator/3.2/manual/zh" + }, + { + "name": "3.1", + "link": "https://docs.cocos.com/creator/3.1/manual/zh" + }, + { + "name": "3.0", + "link": "https://docs.cocos.com/creator/3.0/manual/zh" + }, + { + "name": "2.4", + "link": "https://docs.cocos.com/creator/2.4/manual/zh" + }, + { + "name": "2.3", + "link": "https://docs.cocos.com/creator/2.3/manual/zh" + }, + { + "name": "2.2", + "link": "https://docs.cocos.com/creator/2.2/manual/zh" + }, + { + "name": "2.1", + "link": "https://docs.cocos.com/creator/2.1/manual/zh" + }, + { + "name": "2.0", + "link": "https://docs.cocos.com/creator/2.0/manual/zh" + }, + { + "name": "1.10", + "link": "https://docs.cocos.com/creator/1.10/manual/zh" + }, + { + "name": "1.9", + "link": "https://docs.cocos.com/creator/1.9/manual/zh" + } + ] + } + ] + } +} diff --git a/versions/4.0/zh/cases-and-tutorials/index.md b/versions/4.0/zh/cases-and-tutorials/index.md new file mode 100644 index 0000000000..8004a28ac7 --- /dev/null +++ b/versions/4.0/zh/cases-and-tutorials/index.md @@ -0,0 +1,50 @@ +# 示例与教程 + +## 示例项目 + +- **一步两步**([GitHub](https://github.com/cocos/tutorial-mind-your-step-3d)):也就是 [快速上手](../getting-started/first-game/index.md) 文档里分步讲解制作的游戏。 +- **材质展示 Demo**([GitHub](https://github.com/cocos/cocos-example-materials)):展示各种引擎内置和自定义材质效果,包括标准 PBR、卡通渲染,也包含皮肤、头发、眼球、树叶等高级材质。 +- **渲染管线 Demo**([GitHub](https://github.com/cocos/cocos-example-render-pipeline)):展示如何使用内置后处理渲染管线以及各种后处理特性的开启效果。 +- **物理范例集合**([GitHub](https://github.com/cocos/cocos-example-physics)):包含了 2D & 3D 的两个物理测试例工程,也包含一些物理小游戏如吞噬黑洞、简化小车、坠落小球等,介绍了一些基础的功能和使用方法,方便用户结合文档了解物理功能。 +- **Marionette 动画系统范例**([GitHub](https://github.com/cocos/cocos-example-marionette)):全方位展示了 [Marionette 动画系统](../animation/marionette/index.md)的使用,包含动画状态机、事件处理、姿态图、反向动力学 IK 等功能的展示。 +- **UI 展示 Demo**([GitHub](https://github.com/cocos/cocos-example-ui/)| [Cocos Store](https://store.cocos.com/app/detail/2799)):各类 UI 组件组合使用的演示 Demo。 +- **部分功能范例**([GitHub](https://github.com/cocos/cocos-example-projects) | [Gitee](https://gitee.com/cocos/example-projects)):包括含了引擎网络、NPM 模块等功能的独立示例。 +- **模块展示集合**([GitHub](https://github.com/cocos/cocos-test-projects)):引擎各个功能的范例项目,基本涵盖了引擎的大部分功能模块,用户在使用功能时可参考此项目中的用法进行开发。 +- **实用解决方案**([GitHub](https://github.com/cocos/cocos-awesome-tech-solutions)):官方技术支持团队分享的一系列实用的技术解决方案。 +- **弹弹乐 3D**([GitHub](https://github.com/cocos/cocos-example-ball) | [Cocos Store](https://store.cocos.com/app/detail/2802)):用户可通过此项目制作弹弹球游戏。 +- **快上车 3D**([GitHub](https://github.com/cocos/cocos-tutorial-taxi-game) | [Cocos Store](https://store.cocos.com/app/detail/2796)):基于物理的游戏制作 demo,用户可通过此项目制作快上车游戏。 + +> 所有 GitHub 上的演示和范例项目都会跟随版本进行更新,主分支对应最新的 Cocos Creator 版本,下载的时候请注意。 + +## 教程 + +### 游戏开发 + +- [Cocos Creator 零基础教程:3D 跑酷游戏开发](https://space.bilibili.com/491120849/channel/collectiondetail?sid=842152) +- [Cocos Creator 零基础教程:3D 俯视角 RPG 割草游戏](https://space.bilibili.com/5981196/channel/collectiondetail?sid=902461) +- [鹰击长空 3D射击游戏开发](https://www.bilibili.com/video/BV1HY411H7V5) +- [快上车 3D游戏开发](https://www.bilibili.com/video/BV1AE411j7L9/) + +### 图形渲染 + +- [图形学与Shader渲染实战系列](https://space.bilibili.com/634931989/channel/collectiondetail?sid=967750) +- [Cocos Creator Shader入门基础系列](https://www.bilibili.com/video/BV1Cq4y1d726) +- [烘焙|粒子|皮肤|车漆](https://space.bilibili.com/491120849/channel/collectiondetail?sid=741263) + +## 实用方案分享(由官方技术支持团队提供) + +- [【CocosCreator 3.x 技术方案分享】第一期](https://forum.cocos.org/t/topic/124637) +- [【CocosCreator 3.x 技术方案分享】第二期](https://forum.cocos.org/t/topic/128862) +- [【CocosCreator 3.x 技术方案分享】第三期](https://forum.cocos.org/t/topic/134725) +- [【CocosCreator 3.x 技术方案分享】第四期](https://forum.cocos.org/t/topic/139736) + +- [【CocosCreator 3.x 自定义渲染材质方案分享】第一期](https://forum.cocos.org/t/topic/131501) +- [【CocosCreator 3.x 自定义渲染材质方案分享】第二期](https://forum.cocos.org/t/topic/137605) +- [【CocosCreator 3.x 自定义渲染材质方案分享】第三期](https://forum.cocos.org/t/topic/140525) +- [Cocos Creator 3.x 学习资料整理](https://forum.cocos.org/t/topic/122399) + +## 更多 + +- [Cocos 官方论坛](https://forum.cocos.org/):提问、查找答案、与其他开发者交流 +- [Cocos Store](http://store.cocos.com/):各类美术资源、插件、源码、学习DEMO +- [哔哩哔哩 — Cocos 官方](https://space.bilibili.com/491120849/dynamic):不定期直播、以及官方视频教程等持续更新中 diff --git a/versions/4.0/zh/concepts/scene/coord.md b/versions/4.0/zh/concepts/scene/coord.md new file mode 100644 index 0000000000..30c4ef7949 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/coord.md @@ -0,0 +1,84 @@ +# 坐标系和节点变换属性 + +在文档 [场景编辑器](../../editor/scene/index.md) 和 [节点和组件](node-component.md) 中,我们介绍了可以通过 **变换工具 Gizmo** 和编辑 **属性检查器** 中节点的属性来变更节点的显示行为。这篇文档我们将会深入了解节点所在场景空间的坐标系,以及节点的 **位置(Position)**、**旋转(Rotation)**、**缩放(Scale)** 三大变换属性的工作原理。 + +## 坐标系 + +我们已经知道可以为节点设置位置属性,那么一个有着特定位置属性的节点在游戏运行时将会呈现在屏幕上的什么位置呢?就好像日常生活的地图上有了经度和纬度才能进行卫星定位,我们也要先了解 Cocos Creator 3.0 的坐标系,才能理解节点位置的意义。 + +### 世界坐标系(World Coordinate) + +世界坐标系也叫做绝对坐标系,在 Cocos Creator 3.0 游戏开发中表示场景空间内的统一坐标体系,「世界」用来表示我们的游戏场景。 + +Creator 3.0 的世界坐标系采用的是笛卡尔右手坐标系,默认 x 向右,y 向上,z 向外,同时使用 -z 轴为正前方朝向。 + +![right hand](coord/right_hand.png) + +### 本地坐标系(Local Coordinate) + +本地坐标系也叫相对坐标系,是和节点相关联的坐标系。每个节点都有独立的坐标系,当节点移动或改变方向时,和该节点关联的坐标系将随之移动或改变方向。 + +Creator 3.0 的 **节点(Node)** 之间可以有父子关系的层级结构,我们通过修改节点的 `Position` 属性设定的节点位置是该节点相对于父节点的 **本地坐标系**,而非世界坐标系。 + +最后在绘制整个场景时 Creator 会把这些节点的本地坐标映射成世界坐标系坐标。
+假设场景中有三个节点:NodeA、NodeB、NodeC,节点的结构如下图所示: + +![node tree](coord/node-tree.png) + +当场景中包含不同层级的节点时,会按照以下的流程确定每个节点在世界坐标系下的位置: + +1. 从场景根级别开始处理每个节点,上图中 NodeA 就是一个根级别节点。首先根据 NodeA 的 **位置(Position)** 属性,在世界坐标系中确定 NodeA 的本地坐标系原点位置(也就是 `Position`)。 +2. 接下来处理 NodeA 的所有直接子节点,也就是上图中的 NodeB(以及其他和 NodeB 平级的节点)。根据 NodeB 的 `Position` 属性,在 NodeA 的本地坐标系中确定 NodeB 在世界坐标系中的位置。 +3. 之后不管有多少级节点,都继续按照层级高低依次处理,每个节点都使用父节点的坐标系和自身位置属性来确定在世界坐标系中的位置。 + +## 变换属性 + +节点包括了 **位置(Position)**、**旋转(Rotation)** 和 **缩放(Scale)** 三个主要的变换属性,下面我们依次介绍。 + +![transform properties](coord/transform-properties.png) + +### 位置(Position) + +**位置(Position)** 由 `X`、`Y` 和 `Z` 属性组成,分别规定了节点在当前坐标系 x 轴、y 轴和 z 轴上的坐标,默认为 `(0, 0, 0)`。 + +![position](coord/position-nodeA.png) + +![position](coord/position-nodeB.png) + +上图中节点 NodeA 的世界坐标是 `(50, 50, 50)`,子节点 NodeB 的本地坐标是 `(0, 2, 0)`,此时若将 NodeB 移动到场景根目录,可以看到 NodeB 的世界坐标变成了 `(50, 52, 50)`: + +![position](coord/position-nodeB-world.png) + +由此可见,子节点 NodeB 的 `Position` 是以父节点 NodeA 的 `Position` 为坐标系原点的。 + +如果父节点 NodeA 改变 `Position`,子节点 NodeB 也会跟着改变位置(世界坐标系),但是子节点 NodeB 的 `Position` 属性不会发生变化,因为子节点 NodeB 在以父节点 NodeA 的 `Position` 为原点的本地坐标系中没有发生变化。 + +在 **场景编辑器** 中,可以随时使用 [移动变换工具](../../editor/toolbar/index.md#%E7%A7%BB%E5%8A%A8%E5%8F%98%E6%8D%A2%E5%B7%A5%E5%85%B7) 来改变节点位置。 + +### 旋转(Rotation) + +**旋转(Rotation)** 由 `X`、`Y` 和 `Z` 属性组成,默认为 `(0, 0, 0)`,是另外一个会对节点本地坐标系产生影响的重要属性。当改变 `X` 属性时,表示节点会以 x 轴为中心进行逆时针/顺时针旋转,以此类推,改变 `Y` 或者 `Z` 属性时也是一样的。 +- 当属性值为 **正** 时,节点 **逆时针** 旋转。 +- 当属性值为 **负** 时,节点 **顺时针** 旋转。 + +![rotation](coord/rotation.png) + +上图所示的节点层级关系和前一张图相同,只是节点 NodeA 在 z 轴上的 **旋转(Rotation)** 属性设为了 **60** 度,可以看到除了 NodeA 本身在以 z 轴为中心 **逆时针** 旋转了 60 度之外,其子节点 NodeB 也以 NodeA 的 z 轴为中心,一起 **逆时针** 旋转了 60 度。这也意味着旋转属性会影响到子节点。 + +> **注意**:节点上的四元数 [rotation](%__APIDOC__%/zh/class/Node?id=rotation) 属性,表示的是绕任意轴旋转的角度。而 **属性检查器** 中的 `Rotation` 所对应的属性是欧拉角属性 [EulerAngles](__APIDOC__/zh/class/Node?id=eulerAngles),在脚本中可通过 `Node.eulerAngles` 获取 `Rotation` 的值。这两个属性可以根据需求分别使用,在使用 API 时请一定要注意它们和编辑器面板属性名的对应区别。 + +在 **场景编辑器** 中,可以随时使用 [旋转变换工具](../../editor/toolbar/index.md#%E6%97%8B%E8%BD%AC%E5%8F%98%E6%8D%A2%E5%B7%A5%E5%85%B7) 来设置节点的旋转。 + +### 缩放(Scale) + +**缩放(Scale)** 属性也是由 `X`、`Y` 和 `Z` 三个属性组成,分别表示节点在 x 轴、y 轴和 z 轴上的缩放倍率,默认为 `(1, 1, 1)`。 + +![scale](coord/scale.png) + +上图所示的节点层级关系和介绍 `Position` 时的相同。将节点 NodeA 的缩放属性设为 `(2, 1, 1)`,也就是将 NodeA 在 x 轴方向放大到原来的 **2** 倍,y 轴和 z 轴则保持不变。可以看到子节点 NodeB 也在 x 轴方向放大到了原来的两倍,所以缩放属性会影响所有子节点。 + +在子节点上设置的缩放属性会和父节点的缩放叠加作用,子节点的子节点会将每一层级的缩放属性全部 **相乘** 来获得在世界坐标系下显示的缩放倍率。这一点和 **位置**、**旋转** 属性其实是一致的,只不过 **位置** 和 **旋转** 属性是 **相加** 作用,而 **缩放** 属性是 **相乘**,作用表现得更加明显。 + +**缩放** 属性不会影响当前节点的 **位置** 和 **旋转**,但会影响子节点的 **位置**。 + +在 **场景编辑器** 中,可以随时使用 [缩放变换工具](../../editor/toolbar/index.md#%E7%BC%A9%E6%94%BE%E5%8F%98%E6%8D%A2%E5%B7%A5%E5%85%B7) 来修改节点缩放。 diff --git a/versions/4.0/zh/concepts/scene/coord/node-tree.png b/versions/4.0/zh/concepts/scene/coord/node-tree.png new file mode 100644 index 0000000000..8dcab96f23 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/coord/node-tree.png differ diff --git a/versions/4.0/zh/concepts/scene/coord/position-nodeA.png b/versions/4.0/zh/concepts/scene/coord/position-nodeA.png new file mode 100644 index 0000000000..e444abf77f Binary files /dev/null and b/versions/4.0/zh/concepts/scene/coord/position-nodeA.png differ diff --git a/versions/4.0/zh/concepts/scene/coord/position-nodeB-world.png b/versions/4.0/zh/concepts/scene/coord/position-nodeB-world.png new file mode 100644 index 0000000000..5d53d779e4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/coord/position-nodeB-world.png differ diff --git a/versions/4.0/zh/concepts/scene/coord/position-nodeB.png b/versions/4.0/zh/concepts/scene/coord/position-nodeB.png new file mode 100644 index 0000000000..1caae1b87d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/coord/position-nodeB.png differ diff --git a/versions/4.0/zh/concepts/scene/coord/right_hand.png b/versions/4.0/zh/concepts/scene/coord/right_hand.png new file mode 100644 index 0000000000..ff5af90ef8 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/coord/right_hand.png differ diff --git a/versions/4.0/zh/concepts/scene/coord/rotation.png b/versions/4.0/zh/concepts/scene/coord/rotation.png new file mode 100644 index 0000000000..aafc29915e Binary files /dev/null and b/versions/4.0/zh/concepts/scene/coord/rotation.png differ diff --git a/versions/4.0/zh/concepts/scene/coord/scale.png b/versions/4.0/zh/concepts/scene/coord/scale.png new file mode 100644 index 0000000000..ec40dc4e7d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/coord/scale.png differ diff --git a/versions/4.0/zh/concepts/scene/coord/transform-properties.png b/versions/4.0/zh/concepts/scene/coord/transform-properties.png new file mode 100644 index 0000000000..c0c1807d32 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/coord/transform-properties.png differ diff --git a/versions/4.0/zh/concepts/scene/fog.md b/versions/4.0/zh/concepts/scene/fog.md new file mode 100644 index 0000000000..fa9db2c8bc --- /dev/null +++ b/versions/4.0/zh/concepts/scene/fog.md @@ -0,0 +1,121 @@ +# 全局雾 + +全局雾用于在游戏中模拟室外环境中的雾效果。在游戏中除了用于雾效表现外,还可以用于隐藏摄像机远剪切平面外的模型来提高渲染的性能。 + +全局雾的类型目前包括 **线性雾**、**指数雾**、**指数平方雾**、**层雾** 四种,详情请参考下文 **全局雾类型** 部分的内容。 + +## 开启全局雾 + +在 **层级管理器** 中选中场景根节点,然后在 **属性检查器** 的 **Fog** 组件中勾选 **Enabled** 属性即可开启全局雾。 + +![image](./fog/enable-fog.png) + +## 精确雾效计算(Accurate) + +在 v3.4 之前,Creator 默认使用的是基于 **顶点** 的雾效计算,这在 **顶点数较少而体积较大** 的物体上会出现异常的雾效过渡效果。因此为了解决该问题,Creator 从 v3.4 开始增加了 **Accurate** 选项,用于开启基于 **像素** 的精确雾效计算。 + +![accurate](./fog/accurate.png) + +当不勾选 **Accurate** 选项,即未开启精确雾效时的效果如下: + +![accuracy_off](./fog/accuracy_off.png) + +当勾选 **Accurate** 选项,即开启精确雾效时的效果如下: + +![accuracy_on](./fog/accuracy_on.png) + +### 版本升级 — Cocos Shader(Effect)迁移 + +当旧项目升级到 v3.4 时,**所有调用到 `CC_APPLY_FOG` 的 Shader 代码都需要修改,增加第二个参数 `worldPos`**。例如: + +- 原代码如下: + + ```ts + CC_APPLY_FOG(finalColor); + ``` + +- 升级到 v3.4 后需要改成: + + ```ts + CC_APPLY_FOG(finalColor, v_position.xyz); + ``` + +## 全局雾类型 + +全局雾的雾效类型包括 **线性雾**(LINEAR)、**指数雾**(EXP)、**指数平方雾**(EXP_SQUARED)、**层雾**(LAYERED)四种。类型取决于 **相机** 与 **模型顶点** 的计算结果,这个计算结果称为 **雾效混合因子**。雾效混合因子决定了雾效颜色和模型颜色的混合方式,最终展现出来的不同的全局雾效果。 + +### 线性雾(Linear) + +![linear_fog](./fog/linear_fog.png) + +线性雾相关属性说明如下: + +| 属性 | 说明 | +| :--- | :---- | +| **Enabled** | 勾选该项以开启全局雾 | +| **Accurate** | 若勾选该项,使用像素雾计算,像素雾在顶点数少面积大的物体上有更精确的雾化效果;
若不勾选该项,使用顶点雾计算,顶点雾具有更好的性能。| +| **FogColor** | 设置全局雾的颜色 | +| **Type** | 全局雾的雾效类型 | +| **FogStart** | 雾效影响的起始位置 | +| **FogEnd** | 雾效影响的结束位置 | + +线性雾的雾效混合因子计算公式为: + +**f = (FogEnd - Cam_dis) / (FogEnd - FogStart)** + +- 当 `Cam_dis = FogEnd`,也就是相机与模型顶点的距离等于 `FogEnd` 时,混合因子的计算结果为 0,此时物体为全雾效。 + +- 当 `Cam_dis = FogStart`,也就是相机与模型顶点的距离等于 `FogStart` 时,混合因子的计算结果为 1,此时物体不受任何雾效的影响。 + +在相机与模型顶点的距离固定的情况下,若要增加线性雾的浓度,有以下两种方式: + +1. 固定 `FogStart` 数值,减小 `FogEnd` 数值。 +2. 减小 `FogStart` 数值,固定 `FogEnd` 数值。 + +如果要调整合适的雾效浓度,最好同时对 `FogStart` 和 `FogEnd` 属性进行适当的调整。 + +### 指数雾(Exponential)和指数平方雾(Exponential Squared) + +![exp-properties](./fog/exp-properties.png) + +指数雾和指数平方雾的属性一致,这里以指数雾为例: + +| 属性 | 说明 | +| :---| :--- | +| **Enabled** | 是否开启全局雾 | +| **Accurate** | 若勾选该项,使用像素雾计算,像素雾在顶点数少面积大的物体上有更精确的雾化效果;
若不勾选该项,使用顶点雾计算,顶点雾具有更好的性能。 | +| **FogColor** | 设置全局雾的颜色 | +| **Type** | 全局雾的雾效类型 | +| **FogDensity** | 雾效浓度,取值范围为 0 ~ 1,值越大浓度越高 | +| **FogStart** | 雾效影响的起始位置 | +| **FogAtten** | 雾效衰减系数,该值越小则雾越浓 | + +**指数雾** 的雾效混合因子计算公式为: + +**f = e^(-max(0, distance-fogStart) * fogDensity)** + +**指数平方雾** 的雾效混合因子计算公式为: + +**f = e^(-max(0, distance-fogStart) * fogDensity)²** + +开发者可以通过 `FogStart` 来调整全局雾在远近的分布,通过 `FogDensity` 和 `FogAtten` 来调整雾在不同位置的浓度。 + +### 层雾(Layered) + +层雾(Layered)平行于水平面,具有一定的高度。可在场景世界坐标系垂直方向上的任一位置设定层雾的顶部,以此来确定雾的高低。 + +![image](./fog/layerfog.png) + +| 属性 | 说明 | +| :---| :--- | +| **Enabled** | 是否开启全局雾 | +| **Accurate** | 若勾选该项,使用像素雾计算,像素雾在顶点数少面积大的物体上有更精确的雾化效果;
若不勾选该项,使用顶点雾计算,顶点雾具有更好的性能。 | +| **FogColor** | 设置全局雾的颜色 | +| **Type** | 全局雾的雾效类型 | +| **FogAtten** | 雾效衰减系数,该值越小则雾越浓 | +| **FogTop** | 模型顶点在世界坐标系垂直方向上的位置,小于该位置时所有的顶点都会受到雾效效果的影响 | +| **FogRange** | 雾化效果从设置的 **FogTop** 向下所影响的范围 | + +层雾的雾效计算相比其他三种雾效类型稍显复杂,引入了 `FogTop` 的概念,同时还需要在 **X-Z** 平面进行距离计算。 + +层雾在现实中还是比较常见的,高耸入云的山脉和建筑物都有它的身影,如果能合理利用,相信对场景展现效果有不错的提升,但与此同时计算量也会有一定的增大,开发者可根据需要决定是否使用。 diff --git a/versions/4.0/zh/concepts/scene/fog/accuracy_off.png b/versions/4.0/zh/concepts/scene/fog/accuracy_off.png new file mode 100644 index 0000000000..8d6fbfb2cc Binary files /dev/null and b/versions/4.0/zh/concepts/scene/fog/accuracy_off.png differ diff --git a/versions/4.0/zh/concepts/scene/fog/accuracy_on.png b/versions/4.0/zh/concepts/scene/fog/accuracy_on.png new file mode 100644 index 0000000000..7adf8b8eee Binary files /dev/null and b/versions/4.0/zh/concepts/scene/fog/accuracy_on.png differ diff --git a/versions/4.0/zh/concepts/scene/fog/accurate.png b/versions/4.0/zh/concepts/scene/fog/accurate.png new file mode 100644 index 0000000000..227d445a05 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/fog/accurate.png differ diff --git a/versions/4.0/zh/concepts/scene/fog/enable-fog.png b/versions/4.0/zh/concepts/scene/fog/enable-fog.png new file mode 100644 index 0000000000..ef4b189ba2 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/fog/enable-fog.png differ diff --git a/versions/4.0/zh/concepts/scene/fog/exp-properties.png b/versions/4.0/zh/concepts/scene/fog/exp-properties.png new file mode 100644 index 0000000000..7dbf579a74 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/fog/exp-properties.png differ diff --git a/versions/4.0/zh/concepts/scene/fog/layerfog.png b/versions/4.0/zh/concepts/scene/fog/layerfog.png new file mode 100644 index 0000000000..ad1f81f339 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/fog/layerfog.png differ diff --git a/versions/4.0/zh/concepts/scene/fog/linear_fog.png b/versions/4.0/zh/concepts/scene/fog/linear_fog.png new file mode 100644 index 0000000000..a297a51ab0 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/fog/linear_fog.png differ diff --git a/versions/4.0/zh/concepts/scene/index.md b/versions/4.0/zh/concepts/scene/index.md new file mode 100644 index 0000000000..b8106f0d64 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/index.md @@ -0,0 +1,21 @@ +# 场景制作工作流程 + +场景是游戏中的环境因素的抽象集合,是创建游戏环境的局部单位,我们可以理解为游戏开发设计人员通过在编辑器中制作一个场景,来表现游戏中的一部分世界内容。 + +![scene world](./scene/world01.jpg) + +## 场景结构 + +Cocos Creator 通过节点树和节点组件系统实现了自由的场景结构。其中 Node 负责管理节点树的父子关系以及空间矩阵变换 Transform,这样可以轻松地在场景中管理和摆放所有的实体节点。
+组件系统赋予了节点各种各样的高级功能,比如模型渲染(MeshRenderer 组件)、动画(Animation 组件)、光源(Light 组件)、地形(Terrain 组件)等。其中 3D 场景中的必要元素是 Camera 组件,Camera 组件代表的是游戏中的玩家视角,没有 Camera 就什么也看不见。因而在创建场景时,Creator 会默认创建一个挂载了 Camera 组件的节点。 + +## 场景制作相关工作流程 + +- [场景资源](../../asset/scene.md) +- [节点和组件](node-component.md) +- [坐标系和节点属性变换](coord.md) +- [节点层级和显示顺序](node-tree.md) +- [使用场景编辑器搭建场景](scene-editing.md) +- [天空盒](skybox.md) +- [全局雾](fog.md) +- [阴影](./light/shadow.md) diff --git a/versions/4.0/zh/concepts/scene/layer.md b/versions/4.0/zh/concepts/scene/layer.md new file mode 100644 index 0000000000..87e73c5d4a --- /dev/null +++ b/versions/4.0/zh/concepts/scene/layer.md @@ -0,0 +1,26 @@ +# 层级 + +节点的 Layer 属性是一个无符号 32 位的整数,最多支持 32 个不同类型的 Layer,可在编辑器上方菜单栏的 **项目 -> 项目设置 -> [Layers](../../editor/project/index.md#layers)** 中设置。其中开发者可自定义第 0 ~ 19 个 Layer,剩下后面的 12 个 Layers 则是引擎内置的。 + +相机的 Visibility 属性跟节点的 Layer 属性,都是用来控制其可见性的。但只有当节点设置的 Layer 属性包含在相机的 Visibility 中时,节点才可以被相机看见。相机的 Visibility 属性采用 `|` 和 `&` 这种位操作符来判断节点的 Layer 是否应该被观察到,且支持同时选择多个 Layer。具体内容可参考 [Camera — Visibility 属性](../../editor/components/camera-component.md#%E7%9B%B8%E6%9C%BA%E5%88%86%E7%BB%84%E6%B8%B2%E6%9F%93)。 + +## 引擎内置的层级 + +![layer gizmo](scene/layer-gizmo.png) + +| 属性 | 说明 | 值 | +| :--- | :--- | :--- | +| **NONE** | 设置全都不可见 | 0 | +| **IGNORE_RAYCAST** | 设置忽略射线检测 | 1 << 20 | +| **GIZMOS** | 设置配置信息可见 | 1 << 21 | +| **EDITOR** | 设置编辑器可见 | 1 << 22 | +| **UI_3D** | 设置 3D **UI** 节点可见 | 1 << 23 | +| **SCENE_GIZMO** | 设置场景配置节点可见 | 1 << 24 | +| **UI_2D** | 设置 2D **UI** 节点可见 | 1 << 25 | +| **PROFILER** | 设置分析工具节点可见 | 1 << 28 | +| **DEFAULT** | 设置默认节点可见 | 1 << 30 | +| **ALL** | 设置所有节点可见 | 0xffffffff | + +## 用户自定义层级 + +![layer gizmo](scene/layer-edit.png) diff --git a/versions/4.0/zh/concepts/scene/light.md b/versions/4.0/zh/concepts/scene/light.md new file mode 100644 index 0000000000..80784e4dfe --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light.md @@ -0,0 +1,17 @@ +# 光照 + +本章主要介绍 Cocos Creator 中光照的工作方式和使用方式。 + +光照是指光的照射,Creator 中光照的实现模拟了光对真实世界的影响。在场景中添加光源可以使场景产生相应的光照和阴影效果,获得更好的视觉效果。 + +![light scene](light/lighting.png) + +## 更多内容 + +- [基于物理的光照](light/pbr-lighting.md) +- [光源类型](light/lightType/index.md) +- [基于多 Pass 的多光源支持](light/additive-per-pixel-lights.md) +- [阴影](light/shadow.md) +- [光照贴图](light/lightmap.md) + +Creator 提供了 **lighting 范例**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/light) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/light)),主要介绍一些光照常见的编辑操作方法以及代码范例,以供参考。 diff --git a/versions/4.0/zh/concepts/scene/light/additive-per-pixel-lights.md b/versions/4.0/zh/concepts/scene/light/additive-per-pixel-lights.md new file mode 100644 index 0000000000..6d6ffe0d2d --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/additive-per-pixel-lights.md @@ -0,0 +1,39 @@ +# 基于多 Pass 的多光源支持 + +超着色器(Uber Shader)目前在一些性能受限的平台上仍然是主流方案,但随着硬件性能的增强和画质需求的提高,固定数量的光源再也无法满足实际应用的需求,于是就有了支持多光源的方案 — **多遍绘制**。 + +下面以 Creator 中默认的光照材质 `default-material.mtl` 为例,介绍如何实现基于多 Pass 的多光源支持。 + +![default-material](additivelights/default-material.png) + +首先在 **层级管理器** 中新建一个 **Sphere 球体** 节点,然后继续添加一个平行光,两个聚光灯,将它们设置环绕在球体周围,如下图所示: + +![using Light](additivelights/usingLight.png) + +场景搭建完成后,选择编辑器上面的浏览器预览,可以在左下角看到 Draw Call。 + +![Draw Call](additivelights/drawCall.png) + +我们可以通过第三方软件,例如 RenderDoc,打开 Frame Debug 来看看这些到底是如何渲染到屏幕上的: + +![Frame Debug](additivelights/debug.png) + +由上图可以看出来,第一遍渲染的是 `Directional Light` 的光照: + +![main light pass](additivelights/pass1.png) + +第二遍,渲染的是 `Spot Light 1` 的光照: + +![ForwardAdd pass](additivelights/pass2.png) + +第三遍,渲染的是 `Spot Light 2` 的光照: + +![ForwardAdd pass](additivelights/pass3.png) + +这种渲染方式便是支持多种光照模型的 Forward-Pipeline。Forward 一般由两个 Pass 组成: + +- 第一个 Pass 是 BasePass,用于渲染平行光的光照。 + +- 第二个 Pass 是 LightPass,用于渲染剩余光源的光照。 + +因此,当一个物体同时被多个灯光照射时,Draw Call 也会增加。 diff --git a/versions/4.0/zh/concepts/scene/light/additivelights/debug.png b/versions/4.0/zh/concepts/scene/light/additivelights/debug.png new file mode 100644 index 0000000000..4f4c480978 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/additivelights/debug.png differ diff --git a/versions/4.0/zh/concepts/scene/light/additivelights/default-material.png b/versions/4.0/zh/concepts/scene/light/additivelights/default-material.png new file mode 100644 index 0000000000..86d522a9b0 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/additivelights/default-material.png differ diff --git a/versions/4.0/zh/concepts/scene/light/additivelights/drawCall.png b/versions/4.0/zh/concepts/scene/light/additivelights/drawCall.png new file mode 100644 index 0000000000..65cb910191 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/additivelights/drawCall.png differ diff --git a/versions/4.0/zh/concepts/scene/light/additivelights/pass1.png b/versions/4.0/zh/concepts/scene/light/additivelights/pass1.png new file mode 100644 index 0000000000..3951c220c0 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/additivelights/pass1.png differ diff --git a/versions/4.0/zh/concepts/scene/light/additivelights/pass2.png b/versions/4.0/zh/concepts/scene/light/additivelights/pass2.png new file mode 100644 index 0000000000..a13ad81e76 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/additivelights/pass2.png differ diff --git a/versions/4.0/zh/concepts/scene/light/additivelights/pass3.png b/versions/4.0/zh/concepts/scene/light/additivelights/pass3.png new file mode 100644 index 0000000000..98f80309ea Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/additivelights/pass3.png differ diff --git a/versions/4.0/zh/concepts/scene/light/additivelights/usingLight.png b/versions/4.0/zh/concepts/scene/light/additivelights/usingLight.png new file mode 100644 index 0000000000..e339d31af7 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/additivelights/usingLight.png differ diff --git a/versions/4.0/zh/concepts/scene/light/light-bulbs.jpg b/versions/4.0/zh/concepts/scene/light/light-bulbs.jpg new file mode 100644 index 0000000000..6ea594a3f0 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/light-bulbs.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/light-cd.jpeg b/versions/4.0/zh/concepts/scene/light/light-cd.jpeg new file mode 100644 index 0000000000..346d24fbf1 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/light-cd.jpeg differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/ambient.md b/versions/4.0/zh/concepts/scene/light/lightType/ambient.md new file mode 100644 index 0000000000..dd6f09aa65 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/lightType/ambient.md @@ -0,0 +1,23 @@ +# 环境光 + +在生活中,错综复杂的光线与凹凸不平的物体表面相互反射,使得整个环境都被照亮,仿佛被一层光均匀笼罩,这个光一般称为 **环境光**,也称为 **漫射环境光**。 + +因为环境光可以均匀地照亮场景中的所有物体,常用于解决模型背光面全黑的问题,一般需要配合其他类型的光源一起使用。例如场景中只有一个平行光,那么在模型的背光源处会显得非常暗,加入环境光则可以提升模型背部的亮度,显得更加美观。 + +![ambient](ambient/ambient.png) + +在 **层级管理器** 中选中场景根节点,然后在 **属性检查器** 的 **ambient** 组件中即可设置环境光属性。 + +> **注意**:由于环境光是没有方向的,所以不能产生阴影。 + +## 环境光属性 + +![ambient 面板](ambient/ambient-prop.png) + +| 属性 | 说明 | +| :--- | :--- | +| SkyLightingColor | 设置天空颜色 | +| SkyIllum | 调节环境光亮度 | +| GroundLightingColor | 设置地面反射光的颜色 | + +环境光可配合天空盒一起使用,详情可参考 [天空盒](../../skybox.md)。 diff --git a/versions/4.0/zh/concepts/scene/light/lightType/ambient/ambient-prop.png b/versions/4.0/zh/concepts/scene/light/lightType/ambient/ambient-prop.png new file mode 100644 index 0000000000..e6300390ba Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/ambient/ambient-prop.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/ambient/ambient.png b/versions/4.0/zh/concepts/scene/light/lightType/ambient/ambient.png new file mode 100644 index 0000000000..b289dbff97 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/ambient/ambient.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/dir-light.md b/versions/4.0/zh/concepts/scene/light/lightType/dir-light.md new file mode 100644 index 0000000000..845b2973f1 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/lightType/dir-light.md @@ -0,0 +1,97 @@ +# 平行光 + +平行光又称为方向光(Directional Light),是最常用的一种光源,模拟了无限远处的光源发出的光线,常用于实现太阳光。 + +![image](dirlights/dir-light.jpg) + +因为光源与被照射目标的距离是未定义的(无限远),所以光照效果不受 **光源位置** 和 **朝向** 的影响(如下图,平行光在平面上产生的光照亮度都是一样的)。但是 **旋转** 会影响到平行光照射的方向,而光照方向又会影响到模型接受光照的范围以及模型产生阴影的位置。可通过编辑器左上角的 [旋转变换工具](../../../../editor/toolbar/index.md#%E6%97%8B%E8%BD%AC%E5%8F%98%E6%8D%A2%E5%B7%A5%E5%85%B7) 来调整平行光照射的方向。 + +![image](dirlights/dir-light-scene.jpg) + +在场景中添加平行光的方式可参考 [添加光源](index.md)。 + +> **注意**:Cocos Creator 目前只支持一个平行光。若同时添加多个,则以最后一个添加的为准。 + +新建场景时,默认会自动创建一个 `Main Light` 平行光节点。 + +平行光组件相关接口,请参考 [DirectionalLight API](%__APIDOC__%/zh/class/DirectionalLight)。 + +> **注意**:从 v3.5 开始,**平行光阴影** 从场景设置面板中独立出来,不在全局设置阴影属性。 + +## 平行光属性 + +![image](dirlights/dir-light-prop.png) + +| 属性 | 说明 | +| :------ | :-- | +| Color | 设置光源颜色 | +| UseColorTemperature | 是否启用色温 | +| ColorTemperature | 调节色温 | +| StaticSettings | 设置静态灯光,详情请参考 [光照贴图](../lightmap.md) | +| Illumination | 照度,单位 **勒克斯(lx)** | + +### 平行光阴影属性 + +阴影属性需要在场景中开启阴影。开启方法为请参考 [阴影 - 开启阴影](../shadow.md#%E5%BC%80%E5%90%AF%E9%98%B4%E5%BD%B1)。 + +开启阴影后,平行光 **属性检查器** 会展示 **动态阴影设置** 如下图: + +![image](dirlights/dir-light-shadow-prop.png) + +其属性和说明请参考 [阴影 - 平行光阴影属性](../shadow.md#%E5%B9%B3%E8%A1%8C%E5%85%89%E9%98%B4%E5%BD%B1%E5%B1%9E%E6%80%A7) + +### 平行光阴影属性 + +![image](dirlights/dir-light-shadow-prop.png) + +| 属性 | 说明 | +| :------ | :-- | +| ShadowEnabled | 是否开启平行光阴影 | +| ShadowPcf | 设置阴影边缘反走样等级,目前支持 **HARD**、**SOFT**、**SOFT_2X**,详情可参考下文 **PCF 软阴影** 部分的介绍。 | +| ShadowBias | 设置阴影偏移值,防止 z-fighting | +| ShadowNormalBias | 设置法线偏移值,防止曲面出现锯齿状 | +| ShadowSaturation | 调节阴影饱和度,建议设置为 **1.0**。若需要减小方向光阴影的饱和程度,推荐通过增加环境光来实现,而不是调节该值 | +| ShadowInvisibleOcclusionRange | 设置 Camera 可见范围外的物体产生的阴影是否需要投射到可见范围内,若需要则调大该值即可 | +| ShadowDistance | 设置 Camera 可见范围内显示阴影效果的范围,阴影质量与该值的大小成反比 | +| Enable CSM | 设置是否开启 Cascaded Shadow Map 模式 | +| FixedArea | 设置是否手动控制 Camera 可见范围内显示阴影效果 | + +#### Enable CSM 模式 + +基础的阴影贴图方法对于大型场景渲染显得力不从心,很容易出现阴影抖动和锯齿边缘现象。Cascaded Shadow Maps(CSM) 方法根据对象到观察者的距离分块提供深度纹理来解决上述问题。它将相机的视锥体分割成若干部分,然后为分割的每一部分生成独立的深度贴图。 + +对于近处的场景使用较高质量的阴影贴图,对于远处的场景使用较低质量的阴影贴图,在两张阴影贴图过渡的地方选择其中一张使用。因为远处的对象只占画面的很少一部分像素,而近处的对象占据了画面的很大一部分,这样保证了观察者在近处看到的阴影质量较高,远处的阴影质量较低。 + +- 关闭CSM: + + ![image](../shadow/csm-off.png) + +- 开启CSM: + + ![image](../shadow/csm-on.png) + +#### FixedArea 模式 + +FixedArea 模式用于设置是否手动控制 Camera 可见范围内显示阴影效果的范围: + +- 若不勾选该项(默认),则引擎会使用和 CSM(级联阴影算法)模式相同的裁切流程和相机计算,根据 Camera 的方向和位置来计算阴影产生的范围。 +- 若勾选该项,则根据手动设置的 `Near`、`Far`、`OrthoSize` 属性来控制阴影产生的范围。阴影会跟随方向光节点的位置,在方向光包围盒附近分布,而非跟随相机。 + + ![image](dirlights/dir-fixedarea.png) + +| 属性 | 说明 | +| :------ | :-- | +| ShadowFixedArea | 是否开启固定区域的阴影 | +| ShadowNear | 设置主光源相机的近裁剪面 | +| ShadowFar | 设置主光源相机的远裁剪面 | +| ShadowOrthoSize | 设置主光源相机的正交视口大小,阴影质量与该值的大小成反比 | + +### PCF 软阴影 + +百分比渐近过滤(PCF)是一个简单、常见的用于实现阴影边缘反走样的技术,通过对阴影边缘进行平滑处理来消除阴影贴图的锯齿现象。原理是在当前像素(也叫做片段)周围进行采样,然后计算样本跟片段相比更接近光源的比例,使用这个比例对散射光和镜面光成分进行缩放,然后再对片段着色,以达到模糊阴影边缘的效果。 + +目前 Cocos Creator 支持 **硬采样**、**4 倍采样(SOFT 模式)**、**9 倍采样(SOFT_2X 模式)**,倍数越大,采样区域越大,阴影边缘也就越柔和。 + +## 支持动态合批提高性能 + +对于材质中已经开启 instancing 的模型,平面阴影也会自动同步使用 instancing 绘制,详情请参考 [动态合批](../../../engine/renderable/model-component.md#%E5%85%B3%E4%BA%8E%E5%8A%A8%E6%80%81%E5%90%88%E6%89%B9)。 diff --git a/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-fixedarea.png b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-fixedarea.png new file mode 100644 index 0000000000..2bfcb576fd Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-fixedarea.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-fixed-shadow-prop.png b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-fixed-shadow-prop.png new file mode 100644 index 0000000000..faf858b04d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-fixed-shadow-prop.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-panel.png b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-panel.png new file mode 100644 index 0000000000..d38d31542d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-panel.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-prop.png b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-prop.png new file mode 100644 index 0000000000..7d9cc8d8f1 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-prop.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-scene.jpg b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-scene.jpg new file mode 100644 index 0000000000..95d8adcb49 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-scene.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-shadow-prop.png b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-shadow-prop.png new file mode 100644 index 0000000000..02ed89695f Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light-shadow-prop.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light.jpg b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light.jpg new file mode 100644 index 0000000000..c8c096791e Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/dir-light.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/dirlights/spot-light-shadow-prop.png b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/spot-light-shadow-prop.png new file mode 100644 index 0000000000..84d44759f3 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/dirlights/spot-light-shadow-prop.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/index.md b/versions/4.0/zh/concepts/scene/light/lightType/index.md new file mode 100644 index 0000000000..8a688c013e --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/lightType/index.md @@ -0,0 +1,20 @@ +# 光源 + +光源决定了物体所受到的光照的颜色、色温、强度、方向、以及产生的阴影效果等。目前 Creator 支持的光源类型包括: + +- [平行光](dir-light.md) +- [球面光](sphere-light.md) +- [聚光灯](spot-light.md) +- [环境光](ambient.md) + +## 添加光源 + +添加光源有以下两种方式: + +1. 在 **层级管理器** 中点击左上角的 **+** 按钮,选择 **光源**,然后根据需要选择光源类型就可以创建一个带有对应类型 **光源组件** 的节点到场景中。 + + ![add light](index/add-light.png) + +2. 在 **层级管理器** 中选择需要添加光源的节点,然后点击 **属性检查器** 下方的 **添加组件** 按钮,选择 **Light**,即可选择所需的光源组件到节点上。 + + ![add light2](index/add-light2.png) diff --git a/versions/4.0/zh/concepts/scene/light/lightType/index/add-light.png b/versions/4.0/zh/concepts/scene/light/lightType/index/add-light.png new file mode 100644 index 0000000000..135f0d33f6 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/index/add-light.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/index/add-light2.png b/versions/4.0/zh/concepts/scene/light/lightType/index/add-light2.png new file mode 100644 index 0000000000..a2ef6ed771 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/index/add-light2.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/sphere-light.md b/versions/4.0/zh/concepts/scene/light/lightType/sphere-light.md new file mode 100644 index 0000000000..0c42be3b4a --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/lightType/sphere-light.md @@ -0,0 +1,35 @@ +# 球面光 + +Cocos Creator 3.x 的球面光与 v2.x 的点光源(Point Light)类似。 + +球面光会向所有方向均匀地发散光线,接近于蜡烛产生的光线。物体受到的光照强度会随着跟光源距离的增大而减弱,当距离超过设置的光照影响范围,则光照强度为 0。 + +在实际应用中可用于模拟火把、蜡烛、灯泡等光源,照亮四周一定距离内的环境。 + +![sphere-light-edit](spherelight/sphere-light.jpg) + +在编辑器中可以直观地看到光源位置、颜色,以及它的照射范围,如下图所示。通过修改 **属性检查器** 中球面光组件的 `Range` 属性即可调整球面光的光照范围。 + +![sphere-light-edit](spherelight/sphere-light-edit.png) + +在场景中添加球面光的方式可参考 [添加光源](index.md)。 + +球面光组件接口请参考 [SphereLight API](%__APIDOC__%/zh/class/SphereLight)。 + +## 球面光属性 + +![image](spherelight/sphere-light-prop.png) + +| 属性 | 说明 | +| :---- | :---- | +| Color | 设置光源颜色 | +| UseColorTemperature | 是否启用色温 | +| ColorTemperature | 调节色温 | +| Size | 设置光源大小 | +| Range | 设置光照影响范围 | +| Term | 设置光照强度单位的类型,包括 **光通量(LUMINOUS_POWER**)和 **亮度(LUMINANCE)** 两种 | +| LuminousPower | 光通量,单位 **流明(lm)**
当 **Term** 设置为 **LUMINOUS_POWER** 时生效 | +| Luminance | 亮度,单位 **坎德拉每平方米(cd/m2)**
当 **Term** 设置为 **LUMINANCE** 时生效 | +| StaticSettings | 静态灯光设置,详情请参考 [光照贴图](../lightmap.md) | + +> **注意**:目前球面光的 `Size` 属性在实际运行中不生效,以及暂不支持显示阴影,我们会在后续版本进行优化,请关注版本更新公告。 diff --git a/versions/4.0/zh/concepts/scene/light/lightType/spherelight/sphere-light-edit.png b/versions/4.0/zh/concepts/scene/light/lightType/spherelight/sphere-light-edit.png new file mode 100644 index 0000000000..5db157a9a1 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/spherelight/sphere-light-edit.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/spherelight/sphere-light-prop.png b/versions/4.0/zh/concepts/scene/light/lightType/spherelight/sphere-light-prop.png new file mode 100644 index 0000000000..a2932b9234 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/spherelight/sphere-light-prop.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/spherelight/sphere-light.jpg b/versions/4.0/zh/concepts/scene/light/lightType/spherelight/sphere-light.jpg new file mode 100644 index 0000000000..f1dcee109b Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/spherelight/sphere-light.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/spot-light.md b/versions/4.0/zh/concepts/scene/light/lightType/spot-light.md new file mode 100644 index 0000000000..4e4c487bb5 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/lightType/spot-light.md @@ -0,0 +1,73 @@ +# 聚光灯 + +**聚光灯** 是由一个点向一个方向发射一束锥形光线,类似于手电筒或舞台照明灯产生的光线。与其他光源相比,聚光灯多了 `SpotAngle` 属性,用于调整聚光灯的光照范围。 + +![spotlight](spotlight/spot-light.jpg) + +在编辑器中可以直观地看到光源的位置、颜色、光照范围以及它的聚光角度等,如下图所示。配合编辑器左上角的 [变换工具](../../../../editor/toolbar/index.md) 可调整聚光灯的位置及照射方向等。 + +![spotlight](spotlight/spot-light-scene.jpg) + +在场景中添加聚光灯的方式可参考 [添加光源](index.md)。 + +聚光灯组件接口请参考 [SpotLight API](%__APIDOC__%/zh/class/SpotLight)。 + +> **注意**:从 v3.5 开始,**聚光灯光阴影** 从场景设置面板中独立出来,不再受到全局阴影参数的影响。 + +## 聚光灯属性 + +![image](spotlight/spot-light-prop.png) + +| 属性 | 说明 | +| :------ | :--- | +| Color | 设置光源颜色 | +| UseColorTemperature | 是否启用色温 | +| ColorTemperature | 调节色温 | +| Size | 设置光源大小 | +| Range | 设置光照影响范围 | +| SpotAngle | 调整聚光角度,控制光照范围 | +| AngleAttenuationStrength | 聚光灯角度衰减强度。值越大,边缘越柔和,值越小,边缘越硬 | +| Term | 设置光照强度单位类型,包括 **光通量(LUMINOUS_POWER)** 和 **亮度(LUMINANCE)** 两种 | +| LuminousPower | 光通量,单位 **流明(lm)**
当 **Term** 设置为 **LUMINOUS_POWER** 时生效 | +| Luminance | 亮度,单位 **坎德拉每平方米(cd/m2)**
当 **Term** 设置为 **LUMINANCE** 时生效 | +| StaticSettings | 静态灯光设置,详情请参考 [光照贴图](../lightmap.md) | + +### 聚光灯阴影属性 + +阴影属性需要在场景中开启阴影。开启方法请参考 [阴影 - 开启阴影](../shadow.md#%E5%BC%80%E5%90%AF%E9%98%B4%E5%BD%B1)。 + +开启后,聚光灯会展示如下动态阴影界面: + +![image](dirlights/spot-light-shadow-prop.png) + +| 属性 | 说明 | +| :------ | :-- | +| ShadowEnabled | 是否开启平行光阴影 | +| ShadowPcf | 设置阴影边缘反走样等级,目前支持 **HARD**、**SOFT**、**SOFT_2X**,详情可参考下文 **PCF 软阴影** 部分的介绍。 | +| ShadowBias | 设置阴影偏移值,防止 z-fighting | +| ShadowNormalBias | 设置法线偏移值,防止曲面出现锯齿状 | + +### **注意**: +* 由于 3D 场景中实时阴影的性能开销在 openharmony、harmonyNext、android、ios 等移动端原生平台的性能开销较大,引擎默认在这些平台不允许多个 spot light 灯光开启阴影。如果要解除这个限制,需要在引擎完成渲染管线初始化之前,调用相关的修改代码,参考如下代码示例: + ```ts + import { _decorator, Component, macro, rendering } from 'cc'; + const { ccclass, property } = _decorator; + + //解除移动端对多 spot light 开启阴影的限制 + var builder = rendering.getCustomPipeline(macro.CUSTOM_PIPELINE_NAME) as any; + builder._configs.isMobile = false; + + @ccclass('NewComponent') + export class NewComponent extends Component {} + ``` +* 如果游戏开启了 msaa 抗锯齿,也会导致移动端原生平台无法对多个 spot light 灯光开启阴影。 + +### PCF 软阴影 + +百分比渐近过滤(PCF)是一个简单、常见的用于实现阴影边缘反走样的技术,通过对阴影边缘进行平滑处理来消除阴影贴图的锯齿现象。原理是在当前像素(也叫做片段)周围进行采样,然后计算样本跟片段相比更接近光源的比例,使用这个比例对散射光和镜面光成分进行缩放,然后再对片段着色,以达到模糊阴影边缘的效果。 + +目前 Cocos Creator 支持 **硬采样**、**4 倍采样(SOFT 模式)**、**9 倍采样(SOFT_2X 模式)**,倍数越大,采样区域越大,阴影边缘也就越柔和。 + +## 支持动态合批提高性能 + +对于材质中已经开启 instancing 的模型,平面阴影也会自动同步使用 instancing 绘制,详情请参考 [动态合批](../../../engine/renderable/model-component.md#%E5%85%B3%E4%BA%8E%E5%8A%A8%E6%80%81%E5%90%88%E6%89%B9)。 diff --git a/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light-edit.png b/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light-edit.png new file mode 100644 index 0000000000..ab6df2447f Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light-edit.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light-prop.png b/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light-prop.png new file mode 100644 index 0000000000..ee1df2c401 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light-prop.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light-scene.jpg b/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light-scene.jpg new file mode 100644 index 0000000000..2334e5a24d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light-scene.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light.jpg b/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light.jpg new file mode 100644 index 0000000000..f9866e9bcc Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightType/spotlight/spot-light.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/lighting.png b/versions/4.0/zh/concepts/scene/light/lighting.png new file mode 100644 index 0000000000..b759377766 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lighting.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap.md b/versions/4.0/zh/concepts/scene/light/lightmap.md new file mode 100644 index 0000000000..89817eea47 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/lightmap.md @@ -0,0 +1,83 @@ +# 光照贴图 + +烘焙系统会对光源稳定的静态物体所受到的光照和阴影等进行预先计算,计算产生的结果存放在一张纹理贴图中,这张贴图我们称之为 **光照贴图**。 + +生成的光照贴图 Creator 会在运行时自动处理并使用。在光源固定的场景中,使用光照贴图代替实时的光照计算,可以减少资源消耗,从而提高场景运行效率。 + +## 光照贴图面板 + +点击编辑器菜单栏的 **项目 -> 光照贴图**,打开光照贴图面板。面板由 **Scene** 和 **Baked** 两个页面组成。 + +![bake result](./lightmap/lightmap-panel.png) + +- **Scene**:主要用于配置生成光照贴图相关的参数。 +- **Baked**:主要用于展示生成的光照贴图及其相关信息。 + +具体内容请查看下方 **生成光照贴图** 部分的内容。 + +### 属性说明 + +**Scene** 页面各参数的说明如下: + +| 参数 | 说明 | +| :--- | :--- | +| MSAA | 多重采样,可选值包括:1、2、4、8 | +| Resolution | 生成的光照贴图的分辨率,可选值包括:128、256、512、1024、2048 | +| Gamma | Gamma 矫正值 | +| GIScale | 全局光照缩放系数 | +| GISamples | 全局光照采样系数 | +| AOLevel | AO(Ambient Occlusion,环境光遮蔽)级别 | +| AOStrength | AO 强度 | +| AORadius | AO 半径 | +| AOColor | AO 颜色 | + +## 生成光照贴图 + +1. 在 **层级管理器** 中选中光源节点(带有光源组件),然后在 **属性检查器** 中设置光源组件的 **StaticSettings**。 + + ![enable lightbake](./lightmap/light-bakeable.png) + + - **EditorOnly**:是否只在编辑器中生效 + + - **CastShadow**:是否投射静态阴影 + +2. 在 **层级管理器** 中选中要生成光照贴图的模型节点(带有 [MeshRenderer 组件](./../../../engine/renderable/model-component.md)),然后在 **属性检查器** 中设置 **Light Map Settings**,勾选 `Bakeable` 属性。 + + ![model lighting map settings](./lightmap/meshrenderer-bakeable.png) + + - **Bakeable**:是否烘焙静态光照 + + - **CastShadow**:是否投射静态阴影 + + - **ReceiveShadow**:是否接受静态阴影 + + - **LightmapSize**:模型光照贴图尺寸 + + > **注意**:要生成光照贴图的模型有以下要求:美术人员在制作模型资源时,除了模型本身的 UV,还需要另外包含一套 UV,用于光照贴图。 + +3. 光照贴图UV + 不同与纹理的UV, 光照贴图UV不能重叠 + + > **注意**:不正确的UV产生错误: + > + > 1. 不同平面的uv交错在一起 + > + > ![lightmap uv overlap](./lightmap/overlap_back.png) + > ![lightmap uv overlap](./lightmap/overlap_front.png) + > ![lightmap uv overlap](./lightmap/overlap_lightmap.png) + > + > 2. UV块间没有保留间隔 + > + > ![lightmap uv space](./lightmap/uvspace_lightmap.png) + +4. 打开 **光照贴图** 面板,并设置好对应参数。然后点击 **生成光照贴图** 按钮,会弹出一个文件存储对话框,需要指定一个文件夹(必须在 `assets` 目录下)用于存放生成的光照贴图数据信息。即可看到在 **光照贴图** 面板下方输出了烘焙进度的日志信息。 + + ![bake param](./lightmap/lightmap-generate.png) + +5. 烘焙结束后可在 **光照贴图** 面板的 **Baked** 页面查看生成的光照贴图,以及文件名、尺寸等相关信息。生成的光照贴图引擎会自动处理使用,无需开发者手动操作。 + + ![bake result](./lightmap/lightmap-result.png) + + 1. **烘焙结果**:显示烘焙后生成的光照贴图,格式为 **RGBE8**,可根据需要选择 **R**/**G**/**B** 选项查看光照贴图对应的通道。 + 2. **清空光照贴图**:用于删除生成的光照贴图及相关信息。 + 3. **信息输出面板**:显示生成的光照贴图的文件名、大小等相关信息。 diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/light-bakeable.png b/versions/4.0/zh/concepts/scene/light/lightmap/light-bakeable.png new file mode 100644 index 0000000000..011ecd0394 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/light-bakeable.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/lightmap-generate.png b/versions/4.0/zh/concepts/scene/light/lightmap/lightmap-generate.png new file mode 100644 index 0000000000..9bb88d06a1 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/lightmap-generate.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/lightmap-panel.png b/versions/4.0/zh/concepts/scene/light/lightmap/lightmap-panel.png new file mode 100644 index 0000000000..dabf9de4a0 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/lightmap-panel.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/lightmap-result.png b/versions/4.0/zh/concepts/scene/light/lightmap/lightmap-result.png new file mode 100644 index 0000000000..1845cba46c Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/lightmap-result.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/materials.png b/versions/4.0/zh/concepts/scene/light/lightmap/materials.png new file mode 100644 index 0000000000..5a777e91c5 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/materials.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/meshrenderer-bakeable.png b/versions/4.0/zh/concepts/scene/light/lightmap/meshrenderer-bakeable.png new file mode 100644 index 0000000000..22eee66e54 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/meshrenderer-bakeable.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/overlap_back.png b/versions/4.0/zh/concepts/scene/light/lightmap/overlap_back.png new file mode 100644 index 0000000000..149465e671 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/overlap_back.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/overlap_front.png b/versions/4.0/zh/concepts/scene/light/lightmap/overlap_front.png new file mode 100644 index 0000000000..4f89856ac8 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/overlap_front.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/overlap_lightmap.png b/versions/4.0/zh/concepts/scene/light/lightmap/overlap_lightmap.png new file mode 100644 index 0000000000..f9e77b4d15 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/overlap_lightmap.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lightmap/uvspace_lightmap.png b/versions/4.0/zh/concepts/scene/light/lightmap/uvspace_lightmap.png new file mode 100644 index 0000000000..9b4f9e4ad9 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lightmap/uvspace_lightmap.png differ diff --git a/versions/4.0/zh/concepts/scene/light/lux-outdoor.jpg b/versions/4.0/zh/concepts/scene/light/lux-outdoor.jpg new file mode 100644 index 0000000000..971e3ec7b3 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/lux-outdoor.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/pbr-lighting.md b/versions/4.0/zh/concepts/scene/light/pbr-lighting.md new file mode 100644 index 0000000000..a51d031165 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/pbr-lighting.md @@ -0,0 +1,58 @@ +# 基于物理的光照 + +Cocos Creator 中采用光学度量单位来描述光源参数。基于光学度量单位,我们可以将光源的相关参数全部转化为真实世界中的物理值。这样,设计人员可根据相关灯光的工业参数以及真实环境的实际物理参数来调节光照强度、颜色、范围等信息,使整体光照效果更加符合真实的自然环境。 + +![pbr lighting](pbrlighting/pbr-lighting.jpg) + +## 真实世界中的光源 + +基于物理的光照符合真实世界中的光源描述,在真实环境中,我们所见到的光源产品都具有自身的工业参数,我们先来看一个宜家的灯泡💡 + +![light bulb size](pbrlighting/light-bulb.jpg) + +从产品包装上,我们可以了解到这个灯泡的几个重要工业参数: +- **光通量** +- **色温** +- **尺寸** + +这三个重要参数影响真实世界中的光源表现效果,下面我们来重点讲解一下这三个参数的物理意义。 + +## 光学度量单位 + +**光学度量单位(Photometric Unit)** 是用于测量和量化光的强度和其他相关属性的度量标准。在光学和照明工程中,光学度量单位帮助我们定量描述光的特性,如亮度、光通量、照度等。这些单位在设计和分析照明系统时至关重要,确保照明效果符合预期标准。 + +- **光通量(Luminous Flux)** + + 单位 **流明(lm)**,单位时间内光源所发出或者被照物体所接收的总光能。改变光源大小不会影响场景照明效果。 + +- **亮度(Luminance)** + + 单位 **坎德拉每平方米(cd/m2)**,单位面积光源在给定方向上,在每单位面积内所发出的总光通量。改变光源大小会影响场景照明效果。 + +- **照度(Illuminance)** + + 单位 **勒克斯(lux 或 lx)**,每单位面积所接收到的光通量。该值受光的传播距离影响,对于同样光源而言,当光源的距离为原先的两倍时,照度减为原先的四分之一,呈平方反比关系。 + +在真实世界中,由于描述光源的重要物理参数不一样,我们通常用 **光通量(Luminous Flux)** 和 **亮度(Luminance)** 来描述生活中常见的带有照明面积的光源,用 **照度(Illuminance)** 来描述太阳光。 + +![light power](pbrlighting/light-power.jpg) + +## 色温(ColorTemperature) + +**色温** 是指绝对黑体从绝对零度(-273℃)开始加温后所呈现的颜色。 + +色温是影响光源颜色的重要属性,是个可选属性,当启用色温时,色温也参与了光源颜色的组成部分。 + +真实世界环境中,一天不同时段的环境色温也会动态发生变化: + +![color temp of day](pbrlighting/color-temp-of-day.jpg) + +可参考下表: + +![kelvin](pbrlighting/kelvin.jpg) + +## 光源大小 + +真实世界中的光源都具有真实的物理尺寸,在 **相同光通量** 的情况下,光源的尺寸会影响 **亮度** 和 **照度**。 + +![light bulb size](pbrlighting/light-bulb-size.png) diff --git a/versions/4.0/zh/concepts/scene/light/pbrlighting/color-temp-of-day.jpg b/versions/4.0/zh/concepts/scene/light/pbrlighting/color-temp-of-day.jpg new file mode 100644 index 0000000000..31a1d979a2 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/pbrlighting/color-temp-of-day.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/pbrlighting/kelvin.jpg b/versions/4.0/zh/concepts/scene/light/pbrlighting/kelvin.jpg new file mode 100644 index 0000000000..68607c7a6d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/pbrlighting/kelvin.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/pbrlighting/light-bulb-size.png b/versions/4.0/zh/concepts/scene/light/pbrlighting/light-bulb-size.png new file mode 100644 index 0000000000..bd9a644d4b Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/pbrlighting/light-bulb-size.png differ diff --git a/versions/4.0/zh/concepts/scene/light/pbrlighting/light-bulb.jpg b/versions/4.0/zh/concepts/scene/light/pbrlighting/light-bulb.jpg new file mode 100644 index 0000000000..72b4bba4eb Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/pbrlighting/light-bulb.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/pbrlighting/light-power.jpg b/versions/4.0/zh/concepts/scene/light/pbrlighting/light-power.jpg new file mode 100644 index 0000000000..a370edb2b3 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/pbrlighting/light-power.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/pbrlighting/pbr-lighting.jpg b/versions/4.0/zh/concepts/scene/light/pbrlighting/pbr-lighting.jpg new file mode 100644 index 0000000000..5e919970b4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/pbrlighting/pbr-lighting.jpg differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example.md b/versions/4.0/zh/concepts/scene/light/probe/example.md new file mode 100644 index 0000000000..010979a928 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/probe/example.md @@ -0,0 +1,90 @@ +# 基于图像的光照示例 + +在 Cocos Creator 中开发者可通过组合基于图像光照的功能。这些功能包含: + +- 通过 [天空盒](../../skybox.md) 的烘焙反射卷积图功能以提供更好的环境反射效果 +- 通过 [光照贴图](../lightmap.md) 将光照信息烘焙到贴图以提高光照性能 +- 通过 [光照探针](./light-probe.md)/[反射探针](./reflection-probe.md) 检测物体间的反射信息 + +本文将从艺术资产工作者的工作流演示如何在您的场景中烘焙基于图像的照明。 + +## 准备工作 + +由于光照探针和反射探针都是针对基于物理的光照模型,因此在制作美术资源时,请遵循 PBR 工作流程。 + +请提前准备好使用以下着色器的材质文件;或通过 [导入从 DCC 工具导出的模型](../../../../asset/model/dcc-export-mesh.md) 导出,导入到 **资源管理器** 后会自动识别模型内的材质并将其着色器转化为引擎支持的 PBR 着色器。 + +![effects](example/effects.png) ![effects](example/surface-standard-effect.png) + +- builtin-standard:内置标准 PBR 着色器 +- dcc/imported-metallic-roughness:基于 metallic-roughness 工作流导出的模型使用的着色器 +- dcc/imported-specular-glossiness:基于 specular-glossiniess 工作流导出的模型使用的着色器 +- surface/standard:内置标准表面 PBR 着色器 +- dcc/surface-imported-metallic-roughness:内置标准表面基于 metallic-roughness 工作流导出的模型使用的着色器 +- dcc/surface-imported-specular-glossiness:内置标准表面基于 specular-glossiniess 工作流导出的模型使用的着色器 + +也可以待模型导入到 **资源管理器** 后手动调整至使用上述标准着色器。 + +开发者也可以查看下列文档以便了解整个 PBR 的工作流。 + +- [基于物理的光照](../pbr-lighting.md) +- [基于物理的光照模型(Physically Based Rendering - PBR)](../../../../shader/effect-builtin-pbr.md) +- [导入从 DCC 工具导出的模型](../../../../asset/model/dcc-export-mesh.md)。 +- [FBX 智能材质导入](../../../../importer/materials/fbx-materials.md) + +## 烘焙光照 + +通过 [光照探针面板](light-probe-panel.md)、[反射探针面板](reflection-probe-panel.md) 和 [光照贴图](../lightmap.md) 可进行光照的烘焙用于生成基于图像的光照。 + +### 烘焙流程 + +以手动搭建的场景为例: + +![scene](example/scene.png) + +- 增加 [光照探针](light-probe.md) + + ![light-probe](example/light-probe.png) + +- 添加 [反射探针](relfection-probe.md) + + ![reflection-probe](example/reflection-probe.png) + +- 调整节点的属性 + - 对于需要使用烘焙结果的节点,调整其属性如下: + + ![mobility](example/mobility.png) + + ![setting](example/probe-setting.png) + + - 对于要烘焙的节点 + - 确保其 **属性检查器** 内的 **Mobility** 属性为 **Static**: + + ![static](example/static.png) + + - 勾选其 **MeshRenderer** 属性的 **Bake To Light Probe** 以及 **Bake To Reflection Probe** 属性,并合理选取 **Reflection Probe**: + + ![relfection-probe](example/bake-option.png) + +- 打开 **反射探针** 和 **光照探针** 面板。 + + - 点击上述面板上的 **烘焙** 按钮并等待烘焙流程结束。 + + ![baking-panels](example/baking-panels.png) + +- 可以选择通过 **光照烘焙** 面板烘焙 [光照贴图](../lightmap.md): + + ![lightmap manel](example/lightmap-panel.png) + +- 在 **层级管理** 内 **场景节点**,在其 **属性检查器** 上找到 [天空盒](../../skybox.md) 组件并调整相应的属性以便获得更好的效果 + + ![skybox](example/skybox.png) + + - 将 **Env Lighting Type** 调整为 **AUTOGEN_HEMISPHERE_DIFFUSE_WITH_REFLECTION(漫反射卷积图和环境反射)** + - 点击 **天空盒** 组件上的烘焙按钮以烘焙反射卷积图。 + + 以此可以获取更真实的环境反射光照效果 + +- 检查烘焙的结果 + + ![result](example/baking-result.png) diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/bake-option.png b/versions/4.0/zh/concepts/scene/light/probe/example/bake-option.png new file mode 100644 index 0000000000..85113581e5 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/bake-option.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/baking-panels.png b/versions/4.0/zh/concepts/scene/light/probe/example/baking-panels.png new file mode 100644 index 0000000000..9f1f80c000 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/baking-panels.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/baking-result.png b/versions/4.0/zh/concepts/scene/light/probe/example/baking-result.png new file mode 100644 index 0000000000..11f9070bf8 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/baking-result.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/basic.png b/versions/4.0/zh/concepts/scene/light/probe/example/basic.png new file mode 100644 index 0000000000..679b0d823b Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/basic.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/effects.png b/versions/4.0/zh/concepts/scene/light/probe/example/effects.png new file mode 100644 index 0000000000..c259a50011 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/effects.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/light-probe.png b/versions/4.0/zh/concepts/scene/light/probe/example/light-probe.png new file mode 100644 index 0000000000..a540ddb2f0 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/light-probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/lightmap-panel.png b/versions/4.0/zh/concepts/scene/light/probe/example/lightmap-panel.png new file mode 100644 index 0000000000..dabf9de4a0 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/lightmap-panel.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/mobility.png b/versions/4.0/zh/concepts/scene/light/probe/example/mobility.png new file mode 100644 index 0000000000..55af8a6b7b Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/mobility.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/probe-setting.png b/versions/4.0/zh/concepts/scene/light/probe/example/probe-setting.png new file mode 100644 index 0000000000..94767bc455 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/probe-setting.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/reflection-probe.png b/versions/4.0/zh/concepts/scene/light/probe/example/reflection-probe.png new file mode 100644 index 0000000000..7e63463705 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/reflection-probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/scene.png b/versions/4.0/zh/concepts/scene/light/probe/example/scene.png new file mode 100644 index 0000000000..e4a0525763 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/scene.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/skybox.png b/versions/4.0/zh/concepts/scene/light/probe/example/skybox.png new file mode 100644 index 0000000000..a8648e1557 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/skybox.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/static.png b/versions/4.0/zh/concepts/scene/light/probe/example/static.png new file mode 100644 index 0000000000..bee765c372 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/static.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/example/surface-standard-effect.png b/versions/4.0/zh/concepts/scene/light/probe/example/surface-standard-effect.png new file mode 100644 index 0000000000..556a0828b4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/example/surface-standard-effect.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/img/index.png b/versions/4.0/zh/concepts/scene/light/probe/img/index.png new file mode 100644 index 0000000000..0862cae2e6 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/img/index.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/index.md b/versions/4.0/zh/concepts/scene/light/probe/index.md new file mode 100644 index 0000000000..8a6b34df34 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/probe/index.md @@ -0,0 +1,40 @@ +# 基于图像的光照 + +![preview](img/index.png) + +由于实时计算光照对硬件要求过高,因此在游戏或三维仿真中大多会使用基于图像的光照(IBL,Image Based Lighting)以获取更高的性能,也就是将光照信息存储在计算机的存储介质上,在运行时通过采样图形中的信息对光照进行重建。 + +在引擎中,基于图像的光照通常由下列功能组成。 + +## 光照贴图 + +通过 [光照贴图](../lightmap.md) 开发者可以将场景的光照信息记录在贴图上,并在运行时使用。从而可以避免或减少场景内使用实时光,提高渲染效率。 + +## 天空盒 + +在 [天空盒](../../skybox.md) 中通过 **烘焙反射卷积图** 可以给环境贴图生成预卷积的反射效果以提高环境光照的质量。 + +## 光照探针 + +自 v3.7 开始,Cocos Creator 支持 [光照探针](light-probe.md) 和 [反射探针](reflection-probe.md)。 + +光照探针是全局光照的一部分,将探针布置在场景内,通过探针检测光线在场景内的反弹,并对离线结果进行存储,用以提高光照效果,提升渲染品质。 + +## 反射探针 + + [反射探针](reflection-probe.md) 目前支持实时和烘焙两种情况,实时情况下开发者可以通过配置反射探针,实时渲染物体的反射效果。烘焙的情况下,可以 + +通过结合 [天空盒](../../skybox.md)、[光照贴图](../lightmap.md)、[光照探针](./light-probe.md) 以及 [反射探针](reflection-probe.md) 以获得更加真实可信同时兼顾性能的光照效果。 + +## 内容 + +本章将包含以下内容: + +- [光照探针](light-probe.md) + - [光照探针面板](light-probe-panel.md) +- [反射探针](reflection-probe.md) + - [反射探针面板](reflection-probe-panel.md) + - [反射探针美术工作流](reflection-art-workflow.md) +- [天空盒](../../skybox.md) +- [光照贴图](../lightmap.md) +- [基于图像的光照示例](example.md) diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel.md b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel.md new file mode 100644 index 0000000000..465b566b1f --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel.md @@ -0,0 +1,36 @@ +# 光照探针面板 + +通过编辑器顶部菜单,选择 **项目** -> **光照烘焙** -> **光照探针** 可以打开光照探针烘焙面板。 + +**光照烘焙面板** 会将 **场景编辑器** 内已布置好的 [光照探针](light-probe.md) 进行烘焙。烘焙后,静态物体的间接光信息将被记录到存储介质中;而所有被配置为 **Movable** 属性的节点将可以获取到更详细和真实可信的间接光光照效果。 + +## 属性 + +![light-probe-panel](light-probe-panel/bake-panel.png) + +| 属性 | 说明 | +| :-- | :-- | +| **GIScale** | GI([Global Illumination](https://en.wikipedia.org/wiki/Global_illumination))的缩放系数 | +| **GISample** | GI 的采样系数 | +| **Bounces** | 光线反弹的次数,取值范围 [1, 4]
反弹的含义是当前光源照射到某个物体,反弹后抵达探针的过程
如光线的反弹次数超出该属性,将不会被记录到烘焙结果内
| +| **Reduceing Ringing** | 在某些情况下,光照探针的光照可能从亮部穿透到暗部从而形成环状,修改这个属性可以改善这种情况,但是可能会导致光照不真实的情况 | +| **Show Probe** | 是否显示光照探针
需要在 **层级管理器** 内选中带有 **LightProbeGroup** 组件的节点
![probe](light-probe-panel/probe.png) | +| **Show Wireframe** | 是否显示光照探针间的连线
需要在 **层级管理器** 内选中带有 **LightProbeGroup** 组件的节点
![wireframe](light-probe-panel/wireframe.png) | +| **Show Convex** | 是否显示凸包线框及顶点法线
需要在 **层级管理器** 内选中带有 **LightProbeGroup** 组件的节点
![convex](light-probe-panel/convex.png)| +| **Bake Light Probe** | 烘焙光照探针 | +| **Clear Result** | 清除烘焙的结果 | +| **Cancel** | 取消烘焙过程,该按钮仅当点击 **Bake Light Probe** 后会生效 | + +## 操作 + +点击 **Bake Light Probe** 可以烘焙光照探针,点击 **Clear Result** 清除烘焙的结果。 + +烘焙过程中可以点击 **Cancel** 取消烘焙。 + +在下方的信息展示面板可以查看到烘焙的进度。 + +![info](light-probe-panel/info.png) + +开发者也可通过 **Show Probe**、**Show Wireframe** 以及 **Show Convex** 选项在场景内查看探针是否符合预期的结果。 + +可参考 [探针示例](example.md) 以查看美术工作流。 diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/bake-panel.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/bake-panel.png new file mode 100644 index 0000000000..0a1956b249 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/bake-panel.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/convex.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/convex.png new file mode 100644 index 0000000000..3aaf09cb30 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/convex.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/info.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/info.png new file mode 100644 index 0000000000..d2a80c9fb4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/info.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/probe.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/probe.png new file mode 100644 index 0000000000..c1022530a4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/wireframe.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/wireframe.png new file mode 100644 index 0000000000..083dd7c9df Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe-panel/wireframe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe.md b/versions/4.0/zh/concepts/scene/light/probe/light-probe.md new file mode 100644 index 0000000000..5aee5f6de0 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/probe/light-probe.md @@ -0,0 +1,102 @@ +# 光照探针 + +![result](light-probe/probe.png) + +光照探针旨在通过预计算为场景内的物体提供高质量的光照信息;同时为需要 LOD 的物体提供光照信息。 + +在场景中光线在照射到某个物体后,可能反弹到另一个物体,这样的反弹可能会有多次。 + +下图演示了光线在场景反弹一次并被光照探针捕获的情景。 + +![basic](light-probe/../example/basic.png) + +光线照射到 A 物体表面后,反射至 B 物体,通过光照探针,将 B 物体对此光线的反射情况捕获到贴图上,之后在运行时对光线进行重建,以此来获得场景内静态物体的间接反射光信息。 + +Cocos Creator 支持最大 4 次反弹,详情可参考 [光照探针面板](light-probe-panel.md)。 + +## 添加光照探针组 + +在 **主菜单** 选中 **节点** -> **光源** -> **光照探针组** 可以在场景内添加光照探针。 + +![add-component](light-probe/light-probe.png) + +## 属性 + +![property](light-probe/probe-property.png) + +| 属性 | 说明| +| :-- | :-- | +| **Generating Method** | 生成方式,目前仅支持内置 | +| **Generating Min Pos** | 生成时的包围盒的最小值 | +| **Generating Max Pos** | 生成时的包围盒的最大值 | +| **Number Of Probes X** | X 轴上探针的数量 | +| **Number Of Probes Y** | Y 轴上探针的数量 | +| **Number Of Probes Z** | Z 轴上探针的数量 | +| **Edit Area Box** | 编辑生成的包围盒
点击此按钮后在场景内会进入探针包围盒的编辑模式,请参考下方编辑文档获取更详细的信息
编辑的过程会影响到上述的 **Generating Min Pos** 以及 **Generating Max Pos** 的值 | +| **Generate Probes** | 生成光照探针
点击按钮后,会重新生成所有的探针,旧的探针会被删除
此时生成的探针是均匀的,如果需要手动调整,请点击下放 **Enter Probe Edit Mode** 按钮 | +| **Enter Probe Edit Mode** | 进入编辑探针模式
点击此按钮后在场景编辑器内可以通过 **鼠标左键** 选择不同的探针,并通过 **变换工具** 修改探针的位置 | + +## 编辑探针 + +### 编辑探针范围 + +点击 **Edit Area Box** 可以进行光照探针范围编辑。此时场景内会以 **绿色** 包围盒的形式显示,点击包围盒上的绿色 Gizmo 即可调整其大小。 + +![edit area box](light-probe/edit-area-box.gif) + +点击 **Done Edit** 按钮可以退出编辑模式: + +![done edit](light-probe/done-edit.png) + +之后可以点击 **Generate Probes** 重新生成探针。 + +### 生成探针 + +点击 **Generate Probes** 按钮可以生成探针。此时旧的探针会被删除,如果需要重新烘焙探针,请参考下方 **烘焙光照探针** 部分。 + +![generate probes](light-probe/generate-probes.png) + +### 编辑探针位置 + +生成探针后,可以点击 **Enter Probe Edit Mode** 按钮进入探针编辑模式。 + +此时场景的探针才可以被选择。 + +![select](light-probe/select-probe.png) + +通过鼠标拖拽这些 Gizmo 可以调整探针的位置。 + +![edit](light-probe/edit-probe.gif) + +## 启用探针 + +在节点的 **属性检查器** 内勾选 **使用光照探针(Use Light Probe)** 并将节点的 **Mobility** 属性设置为 **Movable** 可启用节点探针。 + +![mobility](light-probe/mobility.png) + +![use light probe](light-probe/use-light-probe.png) + +启用后在 **层级管理器** 内选中节点,可查看节点受那些探针影响,图示中的白色小点为光照探针。 + +![probes](light-probe/node-probes.png) + +## 烘焙光照探针 + +对于需要烘焙的节点,可勾选节点的 **Bake To Light Probe**: + +![bake to light probe](light-probe/bake-to-light-probe.png) + +在编辑好光照探针后,可以选择 **项目** -> **光照烘焙** -> **光照探针** 打开 [光照探针面板](light-probe-panel.md)。 + +![result](light-probe/result.png) + +更多示例可参考 [基于图像的光照示例](example.md)。 + +## 理论参考 + +通常光照探针是通过球谐函数计算,对于技术细节感兴趣的开发者,我们也准备了一些参考文档: + +- [Spherical Harmonic Lighting: The Gritty Details](http://www.cse.chalmers.se/~uffe/xjobb/Readings/GlobalIllumination/Spherical%20Harmonic%20Lighting%20-%20the%20gritty%20details.pdf) +- [Spherical harmonic lighting Wiki](https://en.wikipedia.org/wiki/Spherical_harmonic_lighting) +- [Fourier Transform Wiki](https://en.wikipedia.org/wiki/Fourier_transform) +- [Spherical Harmonic](https://en.wikipedia.org/wiki/Spherical_Harmonic#:~:text=.%20In%20mathematics%20and%20physical%20science%2C%20spherical%20harmonics,solving%20partial%20differential%20equations%20in%20many%20scientific%20fields.) diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/bake-to-light-probe.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/bake-to-light-probe.png new file mode 100644 index 0000000000..70525dedb2 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/bake-to-light-probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/done-edit.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/done-edit.png new file mode 100644 index 0000000000..570fa2b7de Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/done-edit.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/edit-area-box.gif b/versions/4.0/zh/concepts/scene/light/probe/light-probe/edit-area-box.gif new file mode 100644 index 0000000000..5f2aa74e8c Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/edit-area-box.gif differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/edit-probe.gif b/versions/4.0/zh/concepts/scene/light/probe/light-probe/edit-probe.gif new file mode 100644 index 0000000000..af09f34a78 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/edit-probe.gif differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/generate-probes.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/generate-probes.png new file mode 100644 index 0000000000..c1b83c142d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/generate-probes.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/light-probe.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/light-probe.png new file mode 100644 index 0000000000..04bd4381d4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/light-probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/mobility.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/mobility.png new file mode 100644 index 0000000000..ce3d1265e1 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/mobility.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/node-probes.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/node-probes.png new file mode 100644 index 0000000000..3b23c51be4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/node-probes.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/probe-property.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/probe-property.png new file mode 100644 index 0000000000..ec1e973d75 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/probe-property.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/probe.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/probe.png new file mode 100644 index 0000000000..80f68402a8 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/result.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/result.png new file mode 100644 index 0000000000..46cd209a21 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/result.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/select-probe.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/select-probe.png new file mode 100644 index 0000000000..1576153cd3 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/select-probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/light-probe/use-light-probe.png b/versions/4.0/zh/concepts/scene/light/probe/light-probe/use-light-probe.png new file mode 100644 index 0000000000..fd6482a280 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/light-probe/use-light-probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-art-workflow.md b/versions/4.0/zh/concepts/scene/light/probe/reflection-art-workflow.md new file mode 100644 index 0000000000..2f67fb7b39 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/probe/reflection-art-workflow.md @@ -0,0 +1,53 @@ +# 反射探针美术工作流 + +## 烘焙反射探针工作流 + +- 在场景内创建 **反射探针** 节点 + +- 将需要烘焙反射的节点的 **Mobility** 属性修改为 **Static** + + ![static](reflection-probe/static.png) + +- 在需要烘焙反射的节点的 **属性检查器** 上下拉找到 **Reflection Probe Settings**,并调整其对应的属性: + + ![setting](reflection-probe/mesh-renderer-reflect-probe.png) + + - **Reflection Probe**: 选择反射探针的类型 + - **Bake To Reflection Probe**: 勾选是否将该网格渲染器的反射信息烘焙至反射探针相关的贴图 + + 详情请参考 [MeshRenderer 组件参考](../../../../engine/renderable/model-component.md) + +- 烘焙 + + - 点击 **属性检查器** 上的 **Bake** 按钮,烘焙当前已选择的反射探针: + + ![bake](reflection-probe/bake.png) + + - 主菜单上选择 **项目** -> **光照烘焙** -> **反射探针**,打开 [反射探针面板](reflection-probe-panel.md),通过点击面板上的烘焙按钮进行烘焙。 + +- 检查烘焙结果 + + 烘焙完成后,**资源管理器** 内会创建以 **reflectionProbe_** 开头为命名的贴图。开发者可查看这些贴图是否满足预期。 + +更多示例请参考 [基于图像的光照示例](example.md)。 + +## 实时反射探针工作流示例 + +- 搭建如图示的场景: + + ![scene](reflection-probe/plannar-scene.png) + +- 场景中创建 **反射探针** 节点: + + - 修改 **探针类型** 为 **PLANNAR** + - 配置 **Source Camera** 属性为上述步骤中创建的 **Main Camera** 节点 + + ![inspector](reflection-probe/plannar-probe-property.png) + +- 修改场景中 **Plane** 节点的 **MeshRenderer** 属性的 **Reflection Probe** 为 **PLANNAR_REFLECTION**: + + ![inspector](reflection-probe/plane-reflection-probe-property.png) + +- 此时可以观察到场景内,该平面的反射变化: + + ![plannar-reflection-result](reflection-probe/plannar-reflection-result.png) diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel.md b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel.md new file mode 100644 index 0000000000..7857c54056 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel.md @@ -0,0 +1,23 @@ +# 反射探针面板 + +通过主菜单上的 **项目** -> **光照烘焙** -> **反射探针** 可以打开反射探针烘焙面板。 + +![open-panel](reflection-probe-panel/open-panel.png) + +反射探针烘焙面板会对项目中所有含有 [反射探针](reflection-probe.md) 组件的节点进行烘焙。 + +烘焙的结果会放在项目的 **资源管理器** 内以 **reflectionProbe_** 命名。 + +## 属性 + +![panel](reflection-probe-panel/reflection-panel.png) + +| 属性 | 说明 | +| :-- | :-- | +| **Bake All Reflection Probes** | 烘焙按钮,点击后会开始烘焙整个场景内所有的反射探针 | +| **Clear All Bake Results** | 清除烘焙的结果,点击后会将项目内已经存在的反射探针烘焙结果 | +| **Cancel** | 取消当前的烘焙进程,仅在烘焙按钮被按下后起效
![baking](reflection-probe-panel/baking.png) | + +## 美术工作流 + +美术工作流请参考 [反射探针](reflection-probe.md) 或 [基于图像的光照示例](example.md)。 diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel/baking.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel/baking.png new file mode 100644 index 0000000000..9f582f6d04 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel/baking.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel/open-panel.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel/open-panel.png new file mode 100644 index 0000000000..0b88de90e3 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel/open-panel.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel/reflection-panel.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel/reflection-panel.png new file mode 100644 index 0000000000..948bbfe4e4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe-panel/reflection-panel.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe.md b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe.md new file mode 100644 index 0000000000..aa9f39fd27 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe.md @@ -0,0 +1,52 @@ +# 反射探针 + +自 v3.7 开始,Cocos Creator 支持反射探针。 + +反射探针是将选择范围内的反射光,使用烘焙或实时的方式应用到当前场景上,以提高场景光照可信度的组件。 + +在 **层级管理器** 或顶部菜单上选择 **光源** -> **反射探针** 即可在场景内创建反射探针。 + +![add-component](reflection-probe/add-reflect-probe.png) + +## 属性 + +![property](reflection-probe/property.png) + +| 属性 | 说明 | +| :-- | :-- | +| **Size** | 反射探针的范围,在场景内可以通过操作 Gizmo 来调整反射探针的大小 | +| **Probe Type** | 反射探针的类型
可选项:
**CUBE**:支持烘焙的反射探针
**PLANNAR**:支持实时反射的反射探针
![probe-type](reflection-probe/probe-type.png)| +| **Resolution** | 反射探针烘焙后的立方体贴图每个面的分辨率
可选项: **Low_256x256**/**Medium_512x512**/**Hight_768x768**
该选项仅在 **Probe Type** 为 **CUBE** 时生效
![resolution](reflection-probe/resolution.png)| +| **Background Color** | 背景颜色,仅在 **Probe Type** 为 **PLANNAR** 时生效 | +| **Clear Flag** | 相机的缓冲清除标志位,指定帧缓冲的哪部分要每帧清除。包含:
SOLID_COLOR:清空颜色、深度与模板缓冲;
SKYBOX:启用天空盒,只清空深度
![clear-flag](reflection-probe/clear-flag.png)| +| **Visibility** | 可见性掩码,声明在当前反射探针中可见的节点层级集合
通过下拉菜单选择
![visibility](reflection-probe/visibility.png)| +| **Source Camera** | 指定实时反射的相机
该属性仅在 **Probe Type** 为 **PLANNAR** 时生效 | +| **Bake** | 烘焙按钮,点击后即可以对反射探针进行烘焙 | + +### 探针类型 + +![probe-type](reflection-probe/probe-type.png) + +Cocos Creator 的反射探针有两种类型,分别为: + +- **CUBE**:将区域内的反射信息烘焙到一张 CUBE Map 上。 + + ![cube](reflection-probe/cube.png) + + 在反射探针选择为 **CUBE** 时,开发者可以通过下方的 **RESOLUTION** 下拉菜单选择最终烘焙贴图的大小。 + +- **PLANNAR**:实时反射探针。 + + 常用模拟水面、镜子、大理石或者湿润的地面等。 + + ![plannar](reflection-probe/plannar.png) + + 当反射探针的类型修改为 **PLANNAR** 时,开发者需要配置 **Source Camera** 属性以决定使用哪个相机作为反射探针的相机。 + +通过 **场景编辑器** 内的 Gizmo,可以调整 **Size** 属性,以此来修改反射探针的范围。 + +![edit](reflection-probe/edit-area-box.gif) + +## 美术工作流 + +反射探针支持的两种美术工作流可参考 [反射探针美术工作流](./reflection-art-workflow.md) 和 [基于图像的光照示例](example.md)。 diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/add-reflect-probe.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/add-reflect-probe.png new file mode 100644 index 0000000000..523f86bb58 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/add-reflect-probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/bake-result.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/bake-result.png new file mode 100644 index 0000000000..25f40a0610 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/bake-result.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/bake.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/bake.png new file mode 100644 index 0000000000..f45923b3ca Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/bake.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/clear-flag.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/clear-flag.png new file mode 100644 index 0000000000..53ad29b4b5 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/clear-flag.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/cube.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/cube.png new file mode 100644 index 0000000000..4eef5eea91 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/cube.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/edit-area-box.gif b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/edit-area-box.gif new file mode 100644 index 0000000000..f16e1d4503 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/edit-area-box.gif differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/mesh-renderer-reflect-probe.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/mesh-renderer-reflect-probe.png new file mode 100644 index 0000000000..7d1cb11b6c Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/mesh-renderer-reflect-probe.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plane-reflection-probe-property.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plane-reflection-probe-property.png new file mode 100644 index 0000000000..5813c67182 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plane-reflection-probe-property.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-camera-config.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-camera-config.png new file mode 100644 index 0000000000..40863d64e7 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-camera-config.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-probe-property.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-probe-property.png new file mode 100644 index 0000000000..96ba9c3d90 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-probe-property.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-reflection-result.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-reflection-result.png new file mode 100644 index 0000000000..8b26fe39a4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-reflection-result.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-scene.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-scene.png new file mode 100644 index 0000000000..cd769af020 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar-scene.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar.png new file mode 100644 index 0000000000..6001be79f2 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/plannar.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/probe-type.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/probe-type.png new file mode 100644 index 0000000000..69abda969f Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/probe-type.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/property.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/property.png new file mode 100644 index 0000000000..093069f0c7 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/property.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/reflection-camera.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/reflection-camera.png new file mode 100644 index 0000000000..b1b43037d5 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/reflection-camera.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/resolution.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/resolution.png new file mode 100644 index 0000000000..961ffcf403 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/resolution.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/static.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/static.png new file mode 100644 index 0000000000..84ff2ac170 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/static.png differ diff --git a/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/visibility.png b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/visibility.png new file mode 100644 index 0000000000..209f5ea1b2 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/probe/reflection-probe/visibility.png differ diff --git a/versions/4.0/zh/concepts/scene/light/shadow.md b/versions/4.0/zh/concepts/scene/light/shadow.md new file mode 100644 index 0000000000..93ecbe7515 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/light/shadow.md @@ -0,0 +1,136 @@ +# 阴影 + +在 3D 世界中,光与影一直都是极其重要的组成部分,它们能够丰富整个环境,质量好的阴影可以达到以假乱真的效果,并且使得整个世界具有立体感。 + +Creator 3.0 目前支持 **Planar** 和 **ShadowMap** 两种阴影类型。 + +![shadow](shadow/shadowExample.png) + +## 开启阴影 + +物体开启阴影效果的步骤如下: + +1. 在 **层级管理器** 中选中 **Scene**,然后在 **属性检查器** 的 **shadows** 组件中勾选 **Enabled** 属性。 + + ![enable-shadow](shadow/enable-shadow.png) + +2. 在 **层级管理器** 中选中 **Light**,然后在 **属性检查器** 的 **Dynamic Shadow Settings** 组件中勾选 **Shadow Enabled** 属性。 + + ![enable-shadow](shadow/enable-light-shadow.png) + +3. 在 **层级管理器** 中选中需要显示阴影的 3D 节点,然后在 **属性检查器** 的 **MeshRenderer** 组件中将 **Cast Shadows** 属性设置为 **ON**。 + + ![set-meshrenderer](shadow/set-meshrenderer.png) + + 若阴影类型是 **ShadowMap**,还需要将 MeshRenderer 组件上的 **Receive Shadows** 属性设置为 **ON**。 + +> **注意**:如果阴影无法正常显示,需要调整一下方向光的照射方向。 + +## shadows 类型 + +阴影类型可在 shadows 组件的 **Type** 属性中设置。 + +### Planar shadow + +Planar 阴影类型一般用于较为简单的场景。 + +![planar properties](shadow/planar-properties.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Enabled** | 是否开启阴影效果 | +| **Type** | 阴影类型 | +| **Saturation** | 调节阴影饱和度,建议设置为 **1.0**。若需要减小方向光阴影的饱和程度,推荐通过增加环境光来实现,而不是调节该值 | +| **ShadowColor** | 设置阴影颜色 | +| **Normal** | 阴影接收平面的法线,垂直于阴影,用于调整阴影的倾斜度 | +| **Distance** | 阴影在接收平面上与坐标原点的距离 | + +调节方向光照射的方向可以调节阴影的投射位置。 + +> **注意**:Planar 类型的阴影只有投射在平面上才能正常显示,不会投射在物体上,也就是说 MeshRenderer 组件中的 **ReceiveShadow** 属性是无效的。 + +### ShadowMap + +ShadowMap 是以光源为视点来渲染场景的。从光源位置出发,场景中看不到的地方就是阴影产生的地方。 + +![shadow Map 面板细节](shadow/shadowmap-properties.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Enabled** | 勾选该项以开启阴影效果 | +| **Type** | 设置阴影类型 | +| **MaxReceived** | 最多支持产生阴影的光源数量,默认为 4 个,可根据需要自行调整 | +| **ShadowMapSize** | 设置阴影贴图分辨率,目前支持 **Low_256x256**、**Medium_512x512**、**High_1024x1024**、**Ultra_2048x2048** 四种精度的纹理 | + +> **注意**:从 v3.3 开始,**属性检查器** 中阴影的 **Linear**、**Packing** 项被移除,Creator 将自动判断硬件能力,并选用最优方式进行阴影渲染。 + +ShadowMap 在开启了物体 **MeshRenderer** 组件上的 **ReceiveShadow** 后,就会接收并显示其它物体产生的阴影效果。 + +ShadowMap 一般用于要求光影效果比较真实,且较为复杂的场景。但不足之处在于如果不移动光源,那么之前生成的 ShadowMap 就可以重复使用,而一旦移动了光源,那么就需要重新计算新的 ShadowMap。 + +## 阴影属性 + +在启用类型为 **ShadowMap** 的阴影后,光源的 **属性检查器** 内才会出现 **动态阴影设置** 选项。如未能正确开启,请参考上文 **开启阴影** 部分。 + +目前引擎对不同光源的 **ShadowMap** 支持情况如下: + +| 光源类型 | 是否支持| +| :-- | :-- | +| 平行光 | 支持 | +| 聚光灯 | 支持 | +| 球形光 | 不支持 | + +### 平行光阴影属性 + +![image](./lightType/dirlights/dir-light-shadow-prop.png) + +| 属性 | 说明 | +| :------ | :-- | +| ShadowEnabled | 是否开启平行光阴影 | +| ShadowPcf | 设置阴影边缘反走样等级,目前支持 **HARD**、**SOFT**、**SOFT_2X**、**SOFT_4X**,详情可参考下文 **PCF 软阴影** 部分的介绍。 | +| ShadowBias | 设置阴影偏移值,防止 Z-Fighting | +| ShadowNormalBias | 设置法线偏移值,防止曲面出现锯齿状 | +| ShadowSaturation | 调节阴影饱和度,建议设置为 **1.0**。若需要减小方向光阴影的饱和程度,推荐通过增加环境光来实现,而不是调节该值 | +| ShadowInvisibleOcclusionRange | 设置 Camera 可见范围外的物体产生的阴影是否需要投射到可见范围内,若需要则调大该值即可 | +| ShadowDistance | 设置 Camera 可见范围内显示阴影效果的范围,阴影质量与该值的大小成反比 | + +平行光的使用请参考 [平行光](./lightType/dir-light.md)。 + +#### FixedArea 模式 + +FixedArea 模式用于设置是否手动控制 Camera 可见范围内显示阴影效果的范围: + +- 若不勾选该项(默认),则引擎会使用和 CSM(级联阴影算法)模式相同的裁切流程和相机计算,根据 Camera 的方向和位置来计算阴影产生的范围。 +- 若勾选该项,则根据手动设置的 `Near`、`Far`、`OrthoSize` 属性来控制阴影产生的范围。阴影会跟随方向光节点的位置,在方向光包围盒附近分布,而非跟随相机。 + + ![image](./lightType/dirlights/dir-fixedarea.png) + +| 属性 | 说明 | +| :------ | :-- | +| ShadowFixedArea | 是否开启固定区域的阴影 | +| ShadowNear | 设置主光源相机的近裁剪面 | +| ShadowFar | 设置主光源相机的远裁剪面 | +| ShadowOrthoSize | 设置主光源相机的正交视口大小,阴影质量与该值的大小成反比 | + +### 聚光灯阴影属性 + +![image](./lightType/dirlights/spot-light-shadow-prop.png) + +| 属性 | 说明 | +| :------ | :-- | +| ShadowEnabled | 是否开启平行光阴影 | +| ShadowPcf | 设置阴影边缘反走样等级,目前支持 **HARD**、**SOFT**、**SOFT_2X**、**SOFT_4X**,详情可参考下文 **PCF 软阴影** 部分的介绍。 | +| ShadowBias | 设置阴影偏移值,防止 z-fighting | +| ShadowNormalBias | 设置法线偏移值,防止曲面出现锯齿状 | + +聚光灯的使用请参考 [聚光灯](./lightType/spot-light.md)。 + +### PCF 软阴影 + +百分比渐近过滤(PCF)是一个简单、常见的用于实现阴影边缘反走样的技术,通过对阴影边缘进行平滑处理来消除阴影贴图的锯齿现象。原理是在当前像素(也叫做片段)周围进行采样,然后计算样本跟片段相比更接近光源的比例,使用这个比例对散射光和镜面光成分进行缩放,然后再对片段着色,以达到模糊阴影边缘的效果。 + +目前 Cocos Creator 支持 **硬采样**、**4 倍采样(SOFT 模式)**、**9 倍采样(SOFT_2X 模式)**、**16 倍采样(SOFT_4X 模式)**,倍数越大,采样区域越大,阴影边缘也就越柔和。 + +## 支持动态合批提高性能 + +对于材质中已经开启 instancing 的模型,平面阴影也会自动同步使用 instancing 绘制,详情请参考 [动态合批](../../../engine/renderable/model-component.md#%E5%85%B3%E4%BA%8E%E5%8A%A8%E6%80%81%E5%90%88%E6%89%B9)。 diff --git a/versions/4.0/zh/concepts/scene/light/shadow/csm-off.png b/versions/4.0/zh/concepts/scene/light/shadow/csm-off.png new file mode 100644 index 0000000000..f9f18ae337 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/shadow/csm-off.png differ diff --git a/versions/4.0/zh/concepts/scene/light/shadow/csm-on.png b/versions/4.0/zh/concepts/scene/light/shadow/csm-on.png new file mode 100644 index 0000000000..232f280df1 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/shadow/csm-on.png differ diff --git a/versions/4.0/zh/concepts/scene/light/shadow/enable-light-shadow.png b/versions/4.0/zh/concepts/scene/light/shadow/enable-light-shadow.png new file mode 100644 index 0000000000..5e3a98f25d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/shadow/enable-light-shadow.png differ diff --git a/versions/4.0/zh/concepts/scene/light/shadow/enable-shadow.png b/versions/4.0/zh/concepts/scene/light/shadow/enable-shadow.png new file mode 100644 index 0000000000..66c3e9eeb7 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/shadow/enable-shadow.png differ diff --git a/versions/4.0/zh/concepts/scene/light/shadow/planar-properties.png b/versions/4.0/zh/concepts/scene/light/shadow/planar-properties.png new file mode 100644 index 0000000000..9e9286f903 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/shadow/planar-properties.png differ diff --git a/versions/4.0/zh/concepts/scene/light/shadow/set-meshrenderer.png b/versions/4.0/zh/concepts/scene/light/shadow/set-meshrenderer.png new file mode 100644 index 0000000000..4e4d627634 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/shadow/set-meshrenderer.png differ diff --git a/versions/4.0/zh/concepts/scene/light/shadow/shadowExample.png b/versions/4.0/zh/concepts/scene/light/shadow/shadowExample.png new file mode 100644 index 0000000000..ac1f48447f Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/shadow/shadowExample.png differ diff --git a/versions/4.0/zh/concepts/scene/light/shadow/shadowmap-properties.png b/versions/4.0/zh/concepts/scene/light/shadow/shadowmap-properties.png new file mode 100644 index 0000000000..f614e583d0 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/light/shadow/shadowmap-properties.png differ diff --git a/versions/4.0/zh/concepts/scene/node-component.md b/versions/4.0/zh/concepts/scene/node-component.md new file mode 100644 index 0000000000..3bd9745476 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/node-component.md @@ -0,0 +1,114 @@ +# 节点和组件 + +Cocos Creator 3.0 的工作流程是以组件式开发为核心的,组件式架构也称作 **实体 — 组件架构**(Entity-Component System),简单来说,就是以组合而非继承的方式进行游戏中各种元素的构建。 + +在 Cocos Creator 3.0 中,**节点(Node)** 是承载组件的实体,我们通过将具有各种功能的 **组件(Component)** 挂载到节点上,来让节点具有各式各样的表现和功能。接下来我们看看如何在场景中创建节点和添加组件。 + +## 节点 + +节点是场景的基础组成单位。节点之间是树状的组织关系,每个节点可以有多个子节点: + +![nodes](scene/nodes.jpg) + +节点具有以下特性: +- 节点包含一组基础属性(位移、旋转、缩放),节点之间通过一组相对变换关系组织在一起,详情可参考 [坐标系和变换](./coord.md)。 +- 节点间的更新顺序是逐级更新的。子节点的更新依赖于父节点,子节点跟随父节点变换 +- 节点上可以添加组件,将多个组件与节点关联在一起 + +### 创建节点(Node) + +要最快速地获得一个具有特定功能的节点,可以通过 **层级管理器** 左上角的 **创建节点** 按钮。我们以创建一个最简单的 Sphere(球体)节点为例,点击左上角的 **+** 创建节点按钮,然后选择 **创建 3D 对象 -> 创建 Sphere 球体**: + +![create](scene/create.png) + +之后我们就可以在 **场景编辑器** 和 **层级管理器** 中看到新添加的 Sphere 节点了。新节点命名默认为 `Sphere`,表示这是一个主要由 Sphere 组件负责提供功能的节点。您也可以尝试再次点击 **创建节点** 按钮,选择其他的节点类型,可以看到它们的命名和表现会有所不同。需要注意的是:创建 UI 节点时会自动创建一个 Canvas 节点作为 UI 节点的根节点,具体内容可参考文档 [UI 结构说明](../../2d-object/ui-system/index.md)。 + +更多关于节点在层级管理器中的单选、多选、复制、删除等操作,可参考 [层级管理器](../../editor/hierarchy/index.md)。 + +若要在脚本中动态创建节点,可参考文档 [创建和销毁节点](../../scripting/create-destroy.md)。 + +## 属性 + +![property](node-tree/node-property.png) + +节点除了记录其位置、旋转、缩放外还拥有 Mobility 以及 Layer 属性。 + +- **Mobility**:节点的可移动性。不同的可移动性会导致节点在光照上有不同的特性和表现 + + ![mobility](node-tree/mobility.png) + + 对于含有光源组件的节点,Mobility 的表现略有不同,分别说明如下: + - 对于持有光源组件的节点 + - Static 静态光源:会烘焙直接光与间接光,烘焙完运行时不参与计算 + - Stationary 固定光源:只烘焙间接光,只在运行时计算直接光 + - Movable 可移动光源:不参与烘焙,只在运行时计算直接光 + - 对于持有 MeshRenderer 的节点 + - Static & Stationary 静态物体:可使用光照贴图 + - Movable 动态物体:可使用光照探针 + 通常来说在拥有 MeshRenderer 组件的节点上可以添加灯光组件,但并不建议,可以考虑分开多个节点来实现这样的需求。 + +- **Layer**:设定节点的可见性能力。请参考下方 **设置节点的 Layer 属性** 文档。 + +## 组件 + +我们刚刚创建了节点,现在我们来看看什么是组件,以及组件和节点的关系。
+选中我们刚才创建的 `Sphere` 节点,可以看到 **属性检查器** 中的显示: + +![Properties](scene/inspector.png) + +**属性检查器** 中以 `Node` 标题开始的部分就是节点的属性,节点属性包括了节点的位置、旋转、缩放等变换信息。我们将在 [坐标系和节点属性变换](coord.md) 部分进行详细介绍。 + +接下来以 `cc.MeshRenderer` 标题开始的部分就是 Sphere 上挂载的 MeshRenderer 组件的属性。在 Creator 中,MeshRenderer 组件用于渲染静态的 3D 模型,其中的 `Mesh` 属性用于指定渲染所用的网格资源。因为我们刚刚创建的是 Sphere 节点,所以这里默认是 `sphere.mesh`。
+而 `Materials` 属性用于指定渲染所用的 [材质资源](../../asset/material.md),你可以尝试从 **资源管理器** 中拖拽任意一个材质到 **属性检查器** 的 `Materials` 属性中,可以看到刚才默认的材质变成了指定的材质。 + +> 组件上设置好的任何资源,比如这里的 `sphere.mesh`,都会在场景加载时自动同时加载好。你也可以在自定义的组件中声明需要设置和自动加载的资源类型,详见 [获取和加载资源](../../scripting/load-assets.md)。 + +除了在编辑器中手动添加组件,还可以通过脚本来控制组件,详情请参考 [组件的创建和销毁](../../scripting/component.md)。 + +### 节点属性对组件的影响 + +节点和 MeshRenderer 组件进行组合之后,就可以通过修改节点属性来控制对网格资源的渲染,您可以按照下图中红线标记属性的设置对您的节点进行调整,可以看到模型的旋转和缩放都发生了变化。 + +**调整前**: + +![node property](scene/node-before.png) + +**调整后**: + +![node property](scene/node-after.png) + +我们前面提到了组件式的结构是以组合方式来实现功能的扩展,节点和 MeshRenderer 组件的组合如下图所示: + +![node component relationship](scene/node-chart.png) + +## 添加其他组件 + +在一个节点上可以添加多个组件,来为节点添加更多功能。举个例子: + +我们可以在上面的例子中继续选中 `Sphere` 这个节点,然后点击 **属性检查器** 最下方的 **添加组件** 按钮,选择 **Light -> DirectionalLight** 来添加一个 **平行光** 组件。 + +之后对 **平行光** 组件的属性进行设置,例如将平行光的 `Color` 属性调整为红色,可以看到球体模型的颜色发生了变化,也就是我们为节点添加的 DirectionalLight 组件生效了! + +![button property](scene/directional-light.png) + +> 这里只是简单举个效果较为明显的例子,使用中并不建议在 sphere 节点上添加 DirectionalLight 组件。 + +## 设置节点的可见性 + +引擎采用更加通用的节点,和相机相匹配。当节点设置的 Layer 属性包含在相机的 [Visibility 属性](../../editor/components/camera-component.md) 中时,节点便可以被相机看见,同时支持 3D 组件与 2D 组件的混合渲染。以便更灵活地控制节点组件的可见性,使分组显示多样化。 + +### 设置节点的 Layer 属性 + +![node layer gizmo](scene/node-layer-gizmo.png) + +节点的 Layer 属性是全局且唯一的,但是不同的节点可以设置相同的 Layer 属性,使其被同一个相机所观察。开发者可以使用引擎内置的 Layer 属性,也可以使用自定义的 Layer 属性,点击下图中的 **Edit** 按钮即可前往 **项目设置 -> Layers** 页面进行设置。详情请参考 [层级](layer.md) 文档。 + +![layer edit](scene/node-layer-edit.png) + +其中 `User Layer 0` - `User Layer 19` 是提供给用户自定义设置的 layer 属性,用户只需要在 layer 后面填入自定义 layer name 就可以启用这个 layer 属性,并在节点上编辑。 + +## 小结 + +上面的例子中,我们先是将 MeshRenderer 组件和节点组合,有了可以指定渲染材质的网格资源,接下来我们通过修改节点属性,能够对这个模型进行缩放和旋转等不同方式的显示。现在我们又为这个节点添加了 DirectionalLight 组件,让节点可以根据平行光源的不同状态展现不同的效果。这就是 Cocos Creator 3.0 组件式开发的工作流程,我们可以用这样的方式将不同的功能组合在一个节点上,实现更多复杂目标。 + +需要注意的是,一个节点上只能添加一个渲染组件,渲染组件包括 **MeshRenderer**、**Sprite**、**Label**、**Graphics**、**Mask**、**RichText**、**UIStaticBatch** 等。 diff --git a/versions/4.0/zh/concepts/scene/node-tree.md b/versions/4.0/zh/concepts/scene/node-tree.md new file mode 100644 index 0000000000..9468498eeb --- /dev/null +++ b/versions/4.0/zh/concepts/scene/node-tree.md @@ -0,0 +1,41 @@ +# 节点层级和渲染顺序 + +通过前面的内容,我们了解了通过节点和组件的组合,能够在场景中创建多种元素。当场景中的元素越来越多时,我们就需要通过节点层级来将节点按照逻辑功能归类,并按需排列它们的显示顺序。 + +## 了解层级管理器 + +创建和编辑节点时,**场景编辑器** 可以展示直观的可视化场景元素。而节点之间的层级关系则需要使用 **层级管理器** 来检查和操作。请先阅读 [层级管理器](../../editor/hierarchy/index.md) 面板介绍,来掌握 **层级管理器** 的使用方法。 + +## 节点树 + +通过 **层级管理器** 或运行时脚本的操作,建立的节点之间的完整逻辑关系,就叫做节点树。 + +我们用一个简单的游戏场景来看一下什么是节点树。下图中包括背景图像、一个主角(小球)、标题、跳板、钻石和开始游戏的按钮: + +![](node-tree/rolling-ball.png) + +每个视觉元素都是一个节点,通常我们不会把所有节点平铺在场景上,而是会按照一定的分类和次序(比如根据自己的喜好)组织成节点树,例如: + +![](node-tree/node-tree.png) + +我们把显示在上层的叫做父节点,显示在下层的叫做子节点。在 **层级管理器** 中,上图的节点树就会是这个样子: + +![](node-tree/in_hierarchy.png) + +因为 Creator 3.0 的 UI 节点需要其任意上级节点至少得有一个含有 UITransform 组件,在创建时若不符合规则,便会自动添加一个 Canvas 节点作为它的父级,所以上图中的节点树便将 UI 节点都统一放在了 Canvas 节点下。然后根据类别分别创建父节点,并将同类节点放在一个父节点下,从而构建出的节点树。 + +在实际游戏项目中我们还可以根据需要采用其他方式(例如游戏逻辑)来组织节点树。 + +## 节点渲染顺序 + +3D 节点的渲染与距离相机的 Z 坐标值,透明度等有关系。
+UI 节点的渲染和遮挡关系则受节点树的影响,在 **层级管理器** 中会按照节点排列顺序从上到下依次渲染,也就是说显示在列表上面的节点在场景中会被下面的节点遮盖住。因此,子节点永远都会遮盖住父节点。详情可参考 [UI 渲染排序规则](../../ui-system/components/engine/priority.md)。 + +其他渲染相关可参考: +- [图形渲染](../../module-map/graphics.md) +- [粒子渲染](../../particle-system/renderer.md) +- [模型分组渲染](../../engine/renderable/model-component.md#%E6%A8%A1%E5%9E%8B%E5%88%86%E7%BB%84%E6%B8%B2%E6%9F%93) + +## 性能考虑 + +注意,虽然说父节点可以用来组织逻辑关系甚至是当做承载子节点的容器,但节点数量过多时,场景加载速度会受影响,因此在制作场景时应该避免出现大量无意义的节点,尽可能合并相同功能的节点。 diff --git a/versions/4.0/zh/concepts/scene/node-tree/game-scene.jpg b/versions/4.0/zh/concepts/scene/node-tree/game-scene.jpg new file mode 100644 index 0000000000..9a5da28d52 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/node-tree/game-scene.jpg differ diff --git a/versions/4.0/zh/concepts/scene/node-tree/in_hierarchy.png b/versions/4.0/zh/concepts/scene/node-tree/in_hierarchy.png new file mode 100644 index 0000000000..1e0581e48e Binary files /dev/null and b/versions/4.0/zh/concepts/scene/node-tree/in_hierarchy.png differ diff --git a/versions/4.0/zh/concepts/scene/node-tree/mobility.png b/versions/4.0/zh/concepts/scene/node-tree/mobility.png new file mode 100644 index 0000000000..47081928db Binary files /dev/null and b/versions/4.0/zh/concepts/scene/node-tree/mobility.png differ diff --git a/versions/4.0/zh/concepts/scene/node-tree/node-op.jpg b/versions/4.0/zh/concepts/scene/node-tree/node-op.jpg new file mode 100644 index 0000000000..d790243402 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/node-tree/node-op.jpg differ diff --git a/versions/4.0/zh/concepts/scene/node-tree/node-property.png b/versions/4.0/zh/concepts/scene/node-tree/node-property.png new file mode 100644 index 0000000000..97971345c7 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/node-tree/node-property.png differ diff --git a/versions/4.0/zh/concepts/scene/node-tree/node-tree.png b/versions/4.0/zh/concepts/scene/node-tree/node-tree.png new file mode 100644 index 0000000000..12671235c6 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/node-tree/node-tree.png differ diff --git a/versions/4.0/zh/concepts/scene/node-tree/rolling-ball.png b/versions/4.0/zh/concepts/scene/node-tree/rolling-ball.png new file mode 100644 index 0000000000..6915444476 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/node-tree/rolling-ball.png differ diff --git a/versions/4.0/zh/concepts/scene/scene-editing.md b/versions/4.0/zh/concepts/scene/scene-editing.md new file mode 100644 index 0000000000..c73ab91a1b --- /dev/null +++ b/versions/4.0/zh/concepts/scene/scene-editing.md @@ -0,0 +1,95 @@ +# 使用场景编辑器搭建场景图像 + +本文将介绍使用 [场景编辑器](../../editor/scene/index.md) 创建和编辑场景图像时的工作流程和技巧。 + +## 使用节点创建菜单快捷添加基本节点类型 + +当我们开始在场景中添加内容时,一般会先从 **层级管理器** 的 **创建节点菜单** 开始,也就是点击左上角的 **+** 按钮弹出的菜单,从几个简单的节点分类中选择我们需要的基础节点类型并添加到场景中。 + +添加节点时,在 **层级管理器** 中选中的节点将成为新建节点的父节点,如果你选中了一个折叠显示的节点然后通过菜单添加了新节点,需要展开刚才选中的节点才能看到新添加的节点。 + +### 空节点 + +选择 **创建节点菜单** 中的 **创建空节点** 就能够创建一个不包含任何组件的节点。空节点可以作为组织其他节点的容器,也可以用来挂载开发者编写的逻辑和控制组件。另外在下文中我们也会介绍如何通过空节点和组件的组合,创造符合自己特殊要求的控件。 + +### 3D 对象 + +选择 **创建节点菜单** 中的 **创建 3D 对象** 可以创建编辑器自带的一些比较基础的静态模型控件,目前包括立方体、圆柱体、球体、胶囊、圆锥体、圆环体、平面和四方形。若需要创建其他类型的模型,可参考 [MeshRenderer 组件](../../engine/renderable/model-component.md)。 + +### UI 节点 + +选择 **创建节点菜单** 中的 **创建 UI** 可以创建 UI 节点。Creator 3.0 的 UI 节点需要其任意上级节点至少得有一个含有 UITransform 组件,在创建时若不符合规则,便会自动添加一个 Canvas 节点作为它的父级。并且每一个 UI 节点本身也会带有 UITransform 组件。 + +所以 Canvas 节点是 UI 渲染的 **渲染根节点**,所有渲染相关的 UI 节点都要放在 Canvas 下面,这样做有以下好处: + +- Canvas 能提供多分辨率自适应的缩放功能,以 Canvas 作为渲染根节点能够保证我们制作的场景在更大或更小的屏幕上都保持较好的图像效果,详见 [多分辨率适配方案](../../ui-system/components/engine/multi-resolution.md)相关文档。 +- Canvas 节点会根据屏幕大小自动居中显示,所以 Canvas 下的 UI 节点会以屏幕中心作为坐标系的原点。根据我们的经验,这样设置会简化场景和 UI 的设置(比如让按钮元素的文字默认出现在按钮节点的正中),也能让控制 UI 节点位置的脚本更容易编写。 + +#### 2D 渲染节点 + +通过 **创建节点菜单** 可以创建像 ParticleSystem(粒子)、Sprite(精灵)、Label(文字)、Mask(遮罩)等由节点和基础渲染组件组成的节点类型。 + +这里的基础 2D 渲染组件,是无法用其他组件的组合来代替的。需要注意的是每个节点上只能添加一个渲染组件,重复添加会导致报错。但是可以通过将不同渲染节点组合起来的方式实现复杂的界面控件,比如下面的 UI 控件节点。 + +#### UI 控件节点 + +从 **创建节点菜单** 中的 **UI** 类别里可以创建包括 Button(按钮)、Widget(对齐挂件)、Layout(布局)、ScrollView(滚动视图)、EditBox(输入框)等节点在内的常用 UI 控件。 + +UI 节点大部分都是由渲染节点组合而成的,比如我们通过菜单创建的 Button 节点,就包括了一个包含 Button + Sprite 组件的按钮背景节点,加上一个包含 Label 组件的标签节点: + +![](scene-editing/button-breakdown.png) + +使用菜单创建基础类型的节点,是快速向场景中添加内容的推荐方法,之后我们还可以根据需要对使用菜单创建的节点进行编辑,创造我们需要的组合。 + +关于 UI 节点的更多内容,可参考 [UI 结构说明](../../2d-object/ui-system/index.md)。 + +### 逻辑节点的归属 + +除了有具体渲染等任务的节点之外,建议在场景根目录设置一些节点只负责挂载脚本,执行游戏逻辑,不包含任何渲染等相关内容。通常我们将这些节点放置在场景根层级,和 Canvas 节点并列,方便协作的时候其他开发者能够第一时间找到游戏逻辑并进行相关的数据绑定。 + +## 提高场景制作效率的技巧 + +**场景编辑器** 包括 3D 和 2D 两种视图,3D 视图用于 3D 场景编辑,2D 视图则主要用于 UI 节点等 2D 元素的编辑,可通过编辑器左上方工具栏中的 3D/2D 按钮切换场景视图。
+以下几种快捷方式对两种视图都适用: + +- 在 **层级管理器** 里选中一个节点,然后双击或者按 F 就可以在 **场景编辑器** 中聚焦这个节点。 +- 选中一个节点后按 Cmd/Ctrl + D 会在该节点相同位置复制并粘贴一个同样的节点,当我们需要快速制作多个类似节点时可以用这个命令提高效率。 +- 在 **场景编辑器**/**层级管理器** 中,按住 Cmd/Ctrl 键依次点击你想要的节点,就可以同时选中多个节点。Shift 键则可以连选节点,不需要一个一个选中。 + +### 对齐/平均分布节点 + +当 **场景编辑器** 为 **2D** 视图时,左上角有一排按钮可以用来在选中多个节点时将这些节点对齐或者平均分布。具体的规则如下: + +![alignment](scene-editing/alignment.png) + +假设三个 Label 节点都已经选中,从左到右的 6 个对齐按钮会依次将这些节点: + +- 顶部对齐,按照最靠近上方的边界对齐(而不是最上方的节点的上边界) +- 垂直居中对齐,按照整体的水平中线对齐 +- 底部对齐,按照最靠近下方的边界对齐 +- 左对齐,按照最靠近左边的边界对齐 +- 水平居中对齐,按照整体的垂直中线对齐 +- 右对齐,按照最靠近右边的边界对齐 + +后半部分从左到右的 6 个分布按钮会依次将这些节点: + +- 顶部分布,按照节点的上边界平均分布 +- 垂直居中分布,按照节点的水平中线平均分布 +- 底部分布,按照节点的下边界平均分布 +- 左分布,按照节点的左边界平均分布 +- 水平居中分布,按照节点的垂直中线平均分布 +- 右分布,按照节点的右边界平均分布 + +**注意**:不管是一开始测定左右边界和中线还是之后将每个节点对齐/平均分布时的参照,都是节点约束框的中心或某条边界,而不是节点的位置坐标。 + +例如下图中我们将三个宽度不同的 Label 节点向右对齐后,得到的是三个节点约束框的右边界对齐,而不是三个节点位置的 `x` 坐标变成一致。 + +![align to right](scene-editing/align-to-right.png) + +## 场景显示效果 + +目前还支持在场景中设置天空盒、全局雾效果以及阴影等,以便更好地丰富场景,渲染并展示场景环境。具体可参考: + +- [天空盒](./skybox.md) +- [全局雾效](./fog.md) +- [阴影](./light/shadow.md) diff --git a/versions/4.0/zh/concepts/scene/scene-editing/align-to-right.png b/versions/4.0/zh/concepts/scene/scene-editing/align-to-right.png new file mode 100644 index 0000000000..d621bdbf76 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene-editing/align-to-right.png differ diff --git a/versions/4.0/zh/concepts/scene/scene-editing/alignment.png b/versions/4.0/zh/concepts/scene/scene-editing/alignment.png new file mode 100644 index 0000000000..a152de01b1 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene-editing/alignment.png differ diff --git a/versions/4.0/zh/concepts/scene/scene-editing/button-breakdown.png b/versions/4.0/zh/concepts/scene/scene-editing/button-breakdown.png new file mode 100644 index 0000000000..0b0bb80167 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene-editing/button-breakdown.png differ diff --git a/versions/4.0/zh/concepts/scene/scene.md b/versions/4.0/zh/concepts/scene/scene.md new file mode 100644 index 0000000000..cbf93c2ac4 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/scene.md @@ -0,0 +1,11 @@ +# 场景结构 + +Cocos Creator 在 Creator 的 ECS 实体组件系统框架上增加了3D场景结构,3D 场景通过 RenderScene 表示,ECS 结构中相应的 Component 引用了 RenderScene 中维护的模型(Model)、相机(Camera)、灯光(Light)等元素,它们通过 Node 关联在一起,RenderScene 中更新元素的各种 Transform,也是通过 Node 来操作。 + +注意区别 ECS 结构中的 Scene 与 3D 场景结构中的 RenderScene,ECS 中的 Scene 作为 Node 的逻辑组织结构,而 RenderScene 是作为场景渲染元素的组织结构,ECS Scene 中包含了 RenderScene 成员变量,两者一一对应。 + +ECS 与 3D 场景结构的关系,如下图所示: + +![ecs & scene](scene/ecs-scene.jpg) + +整个 3D 场景结构是被封装在 Component 之下的,通过 Node 建立起组织关系,这样对于 ECS 的上层用户来说,是完全透明的,用户层面对于 3D 场景结构对象是无感知的。 diff --git a/versions/4.0/zh/concepts/scene/scene/create.png b/versions/4.0/zh/concepts/scene/scene/create.png new file mode 100644 index 0000000000..f32562b34c Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/create.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/directional-light.png b/versions/4.0/zh/concepts/scene/scene/directional-light.png new file mode 100644 index 0000000000..ed82696731 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/directional-light.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/ecs-scene.jpg b/versions/4.0/zh/concepts/scene/scene/ecs-scene.jpg new file mode 100644 index 0000000000..895a266640 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/ecs-scene.jpg differ diff --git a/versions/4.0/zh/concepts/scene/scene/inspector.png b/versions/4.0/zh/concepts/scene/scene/inspector.png new file mode 100644 index 0000000000..4e70ae296d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/inspector.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/layer-edit.png b/versions/4.0/zh/concepts/scene/scene/layer-edit.png new file mode 100644 index 0000000000..9e5d933ba6 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/layer-edit.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/layer-gizmo.png b/versions/4.0/zh/concepts/scene/scene/layer-gizmo.png new file mode 100644 index 0000000000..68e8fae78c Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/layer-gizmo.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/node-after.png b/versions/4.0/zh/concepts/scene/scene/node-after.png new file mode 100644 index 0000000000..c8da0ad838 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/node-after.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/node-before.png b/versions/4.0/zh/concepts/scene/scene/node-before.png new file mode 100644 index 0000000000..0c59049360 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/node-before.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/node-chart.png b/versions/4.0/zh/concepts/scene/scene/node-chart.png new file mode 100644 index 0000000000..024cea1a68 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/node-chart.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/node-layer-edit.png b/versions/4.0/zh/concepts/scene/scene/node-layer-edit.png new file mode 100644 index 0000000000..910a8dba2d Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/node-layer-edit.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/node-layer-gizmo.png b/versions/4.0/zh/concepts/scene/scene/node-layer-gizmo.png new file mode 100644 index 0000000000..77226e00f1 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/node-layer-gizmo.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/nodes.jpg b/versions/4.0/zh/concepts/scene/scene/nodes.jpg new file mode 100644 index 0000000000..2d998652d5 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/nodes.jpg differ diff --git a/versions/4.0/zh/concepts/scene/scene/sortingLayer-edit.png b/versions/4.0/zh/concepts/scene/scene/sortingLayer-edit.png new file mode 100644 index 0000000000..762f30fc90 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/sortingLayer-edit.png differ diff --git a/versions/4.0/zh/concepts/scene/scene/world01.jpg b/versions/4.0/zh/concepts/scene/scene/world01.jpg new file mode 100644 index 0000000000..3a7f31548e Binary files /dev/null and b/versions/4.0/zh/concepts/scene/scene/world01.jpg differ diff --git a/versions/4.0/zh/concepts/scene/skin.md b/versions/4.0/zh/concepts/scene/skin.md new file mode 100644 index 0000000000..e838d6d776 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/skin.md @@ -0,0 +1,10 @@ +# 全局皮肤材质配置面板 + +![image](skin/ScenePanel.png) + +全局设置皮肤材质的强度,默认是开启皮肤材质的,不可关闭,如果场景中没有使用皮肤材质,将不会带来额外的性能损耗。 + +| 属性 | 说明 | +| :---| :--- | +| **Enabled** | 是否开启皮肤材质 | +| **Sss Intensity** | 全局皮肤皮下散射强度设置 | diff --git a/versions/4.0/zh/concepts/scene/skin/ScenePanel.png b/versions/4.0/zh/concepts/scene/skin/ScenePanel.png new file mode 100644 index 0000000000..1d4d7f5080 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skin/ScenePanel.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox.md b/versions/4.0/zh/concepts/scene/skybox.md new file mode 100644 index 0000000000..e732f64cb0 --- /dev/null +++ b/versions/4.0/zh/concepts/scene/skybox.md @@ -0,0 +1,162 @@ +# 天空盒 + +游戏中的天空盒是一个包裹整个场景的立方体,可以很好地渲染并展示整个场景环境,在基于 PBR 的工作流中天空盒也可以贡献非常重要的 IBL 环境光照。 + +## 开启天空盒 + +在 **层级管理器** 中选中场景根节点,然后在 **属性检查器** 的 **Skybox** 组件中勾选 **Enabled** 属性即可开启天空盒。 + +![Enable SkyBox](skybox/enable-skybox.jpg) + +Skybox 组件属性如下: + +| 属性 | 说明 | +| :---| :--- | +| **Enabled** | 勾选该项即可开启天空盒 | +| **Env Lighting Type** | 环境光类型,请参考下文 **漫反射照明** 获取更多参考 | +| **UseHDR** | 若勾选该项则开启 HDR(高动态范围),若不勾选该项,则使用 LDR(低动态范围)。详情请参考下文 **切换 HDR/LDR 模式** 部分的内容。 | +| **Envmap** | 天空盒的环境贴图,[TextureCube](../../asset/texture-cube.md) 类型,具体设置方法可参考下文介绍。
当该属性为空时,天空盒默认使用和显示的是像素贴图 | +| **Reflection Convolution** | 点击bake按钮后会生成一张低分辨率的环境贴图并且会对此图进行卷积计算,卷积图用于环境反射 | +| **Reflection Map** | 自动生成的用于环境反射的卷积图,不支持手动编辑。渲染效果参考下文 **烘焙反射卷积图** 部分的内容。 | +| **DiffuseMap** | 自动生成的用于高级漫反射的卷积图,不支持手动编辑。
该选项只在 **Env Lighting Type** 为 **DIFFUSEMAP_WITH_REFLECTION** 时生效 | +| **Reflection Convolution** | 反射卷积,详情请参考下方 **反射卷积** 部分 | +| **SkyboxMaterial** | 为天空盒添加自定义的材质。请参考下方 **天空盒材质** 部分 | + +## 设置天空盒的环境贴图 + +开启天空盒之后还需要设置天空盒的环境贴图,用于在场景中产生环境光照。将贴图资源拖拽到 Skybox 组件的 **Envmap** 属性框中,或者点击 **Envmap** 属性框后面的箭头按钮选择所需的贴图资源即可。若不设置,则天空盒默认使用和显示的是像素贴图。 + +![envmap](skybox/envmap.png) + +天空盒的环境贴图资源支持: + +1. TextureCube 类型的单张贴图,可在 Creator 中设置。 + + - Cube Cross 图片 + + - PNG 或 HDR 格式的图片 + +2. 图片文件形式的 CubeMap(立方体贴图) + +3. 在 Creator 中手动创建的由六张 texture 类型的贴图组合而成的 CubeMap + +### 通过设置 TextureCube 类型的贴图资源 + +1. 导入贴图资源,直接将图片资源拖拽到 **资源管理器** 面板即可。 + +2. 选中导入的贴图资源,在右侧的 **属性检查器** 面板中将 **Type** 属性设置为 **texture cube**,然后点击右上角的绿色打钩按钮保存设置。 + + ![设置为 TextureCube](skybox/texturecube.png) + +3. 在 **层级管理器** 中选中 **Scene**,然后将设置好的图片资源拖拽到 **属性检查器** 中 **skybox** 组件的 **Envmap** 属性框中: + + ![设置天空盒的环境贴图](skybox/set-envmap.png) + +这样子就设置完成了,开发者可以直接在 **场景编辑器** 中看到设置后的天空盒的环境贴图。若贴图没有正确显示,需要检查 **SkyIllum 参数** 的值是否太低,或者 **修改 Camera 的 Clear Flag**。 + +### 使用引擎内置的资源 + +在 **资源管理器 -> internal** 目录下,引擎提供了部分内置的 TextureCube 资源,开发者也可以根据上述步骤按需使用。 + +![builtin skybox](skybox/builtin.png) + +#### SkyIllum 参数 + +在 **层级管理器** 中选中场景根节点,然后在 **属性检查器** 的 **ambient** 组件中即可看到 SkyIllum 参数,默认值为 20000。 + +若 SkyIllum 参数值设置 **太低** 可能会导致天空盒的环境贴图无法在 **场景编辑器** 中正确显示。一般情况下: + +- 当 SkyIllum 参数值小于 300 时,天空盒的环境贴图便无法正常显示。 + +- 当 SkyIllum 参数值为 5000 时,效果相当于月夜的光照强度。 + +#### 修改 Camera 的 Clear Flag + +若 **场景编辑器** 中天空盒的环境贴图已经可以正确显示,但是在项目运行之后仍然没有生效,此时就需要修改 Camera 组件的 **ClearFlag** 属性为 **SKYBOX**: + +![skybox-camera](skybox/skybox-camera.png) + +### 制作 CubeMap(立方体贴图) + +可在 Creator 中手动通过六张普通贴图生成一张立方体贴图,操作步骤如下: + +1. 在 **资源管理器** 中将准备好的六张贴图资源全部选中,然后在 **属性检查器** 中将这些贴图资源的 **Type** 属性批量设置为 **texture**,并点击右上角的绿色打钩按钮。 + + ![cubeMap-texture-type](skybox/cubemap-texture-type.png) + +2. 新建 CubeMap 资源。在 **资源管理器** 中选中要存放 CubeMap 的文件夹,点击左上角的 **+** 按钮,然后选择 **CubeMap** 即可。或者也可以右键点击要存放 CubeMap 的文件夹,选择 **新建 -> CubeMap**。 + + ![create CubeMap](skybox/create-cubemap.png) + +3. 将刚才设置为 texture 类型的 6 张贴图拖拽到 CubeMap 对应的属性框中,完成后点击右上方的绿色打钩按钮,这样就完成了一张 CubeMap。 + + ![Set CubeMap](skybox/cubemap-properties.png) + + > **注意**: + > + > 1. CubeMap 中未设置贴图的属性框将使用默认资源进行填充。 + > 2. CubeMap 中的 6 个属性框 **不要使用同一张贴图**,否则会导致某些平台无法正常显示。 + +### 漫反射照明 + +Creator 支持以下三种方式的环境漫反射照明,可以在 **Env Lighting Type** 属性的下拉列表中进行选择。 + +![type](skybox/sky-box-type.png) + +其类型与描述如下所示: + +| 类型 | 描述 | +| :-- | :-- | +| **HEMISPHERE_DIFFUSE** | 半球漫反射 | +| **AUTOGEN_HEMISPHERE_DIFFUSE_WITH_REFLECTION** | 半球漫反射和环境反射 | +| **DIFFUSEMAP_WITH_REFLECTION** | 漫反射卷积图和环境反射 | + +1. **半球漫反射**:当 **Env Lighting Type** 属性为 **HEMISPHERE_DIFFUSE** 时,使用半球光漫反射。该方式由 **Ambient** 组件中的 **SkyLightingColor** 和 **GroundLightingColor** 属性控制,渲染性能更高,但是细节度不够,照明方向性差。**可手动调节,但可能会和环境贴图变得不统一**。在此模式下,IBL 将不可用,环境贴图对反射不起作用。 + + ![ambient-diffuse](skybox/hemisphere.png) + +2. **半球漫反射和环境反射**:当 **Env Lighting Type** 属性为 **AUTOGEN_HEMISPHERE_DIFFUSE_WITH_REFLECTION** 时,该方式可以通过 **Ambient** 组件中的 **SkyLightingColor** 和 **GroundLightingColor** 属性控制漫反射。同时也会表达环境贴图所产生的镜面反射效果。 + + ![autogen-hemisphere](skybox/autogen-hemisphere.png) + +3. **漫反射卷积图和环境反射**:当 **Env Lighting Type** 属性为 **DIFFUSEMAP_WITH_REFLECTION** 时,使用卷积图漫反射。该方式是高级漫反射,可以正确表达环境贴图产生的漫反射照明,有较好的照明方向性和细节。但和 **AUTOGEN_HEMISPHERE_DIFFUSE_WITH_REFLECTION** 不同的是漫反射是由自动生成的卷积图来表达,不允许编辑。 + + ![apply-diffuseMap](skybox/diffuse-map-with-reflection.png) + +通过下面的 GIF 图可以更明显地看到 **AUTOGEN_HEMISPHERE_DIFFUSE_WITH_REFLECTION** 与 **DIFFUSEMAP_WITH_REFLECTION** 的对比,在 **Env Lighting Type** 为 **DIFFUSEMAP_WITH_REFLECTION** 的情况下背光面较暗,突出了整体的层次感,明暗对比细节也有较大的提升。 + +![Compare](skybox/compare.gif) + +> **注意**:当更换 **Envmap** 属性中的环境贴图时,Creator 会自动计算对应的环境光照信息,以及漫反射光照(仅支持图片文件形式的 CubeMap,不包括手动制作的 CubeMap) + +## 切换 HDR/LDR 模式 + +Skybox 组件中的 **UseHDR** 选项用于切换 HDR/LDR 模式,当勾选时使用 HDR 模式。 + +![use-hdr](skybox/use-hdr.png) + +- HDR(High Dynamic Range):高动态范围,配合 **光源的光度学强度** 和 **相机的曝光参数** 可以实现更真实的明暗对比层次。若使用该模式,则所有的光源(包括平行光、点光源、聚光灯等)**强度会变成光度学物理单位**,环境光立方体贴图应使用 **HDR 格式的图片** 以提供高动态范围的数据源。 + +- LDR(Low Dynamic Range):低动态范围。若使用该模式,则 **光源强度会变成无单位**,不再与光度学和相机曝光有任何联系。此模式适用于希望无任何色染的体现原贴图颜色的情景,环境光立方体贴图可使用 **PNG 等格式的图片**。 + +## 天空盒材质 + +![material](skybox/material.png) + +天空盒材质默认使用的着色器是 **资源管理器 -> internal/pipeline/skybox.effect**,如想自定义材质,可参考 [新建材质](../../asset/material.md) 创建新的材质并选择 [自定义着色器](../../shader/write-effect-overview.md): + +![cusom skybox material](skybox/custom-skybox-material.png) + +## 反射卷积 + +使用 GGX BRDF 光照模型对环境贴图生成预卷积的反射结果,在生成时会考虑粗糙度的影响,并将最终的结果按照粗糙度顺序存储在 Mipmap 中(粗糙度从 0 到 1,粗糙度为 0 时, Mipmap 等级为 0)。 + +由于该算法考虑了粗糙度对环境反射的影响(即随着粗糙度增加,环境反射更模糊),因此使 PBR 材质产生更加逼真的反射效果。 + +## 烘焙反射卷积图 + +在 **属性检查器** 内点击 ![bake](skybox/bake.png) 按钮则可以进行反射卷积贴图的烘焙工作。在生成完成之后,也可以选择点击 ![remove](skybox/remove.png) 按钮进行删除。 + +生成的环境反射卷积图会填充到 TextureCube 的 mipmaps,在 Shader 中根据材质粗糙度采样对应层级的 mipmap,从而提供更加真实的 IBL 效果,通过下面的 GIF 图可以明显看到对比效果。 + +![Compare](skybox/convolution-map.gif) diff --git a/versions/4.0/zh/concepts/scene/skybox/autogen-hemisphere.png b/versions/4.0/zh/concepts/scene/skybox/autogen-hemisphere.png new file mode 100644 index 0000000000..490850b5ec Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/autogen-hemisphere.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/bake.png b/versions/4.0/zh/concepts/scene/skybox/bake.png new file mode 100644 index 0000000000..0508854cc4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/bake.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/builtin.png b/versions/4.0/zh/concepts/scene/skybox/builtin.png new file mode 100644 index 0000000000..396f475878 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/builtin.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/compare.gif b/versions/4.0/zh/concepts/scene/skybox/compare.gif new file mode 100644 index 0000000000..ba7d6b376f Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/compare.gif differ diff --git a/versions/4.0/zh/concepts/scene/skybox/convolution-map.gif b/versions/4.0/zh/concepts/scene/skybox/convolution-map.gif new file mode 100644 index 0000000000..e5fbc2eb30 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/convolution-map.gif differ diff --git a/versions/4.0/zh/concepts/scene/skybox/create-cubemap.png b/versions/4.0/zh/concepts/scene/skybox/create-cubemap.png new file mode 100644 index 0000000000..8d00089d3c Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/create-cubemap.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/cubemap-properties.png b/versions/4.0/zh/concepts/scene/skybox/cubemap-properties.png new file mode 100644 index 0000000000..9637da61b4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/cubemap-properties.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/cubemap-texture-type.png b/versions/4.0/zh/concepts/scene/skybox/cubemap-texture-type.png new file mode 100644 index 0000000000..bfd5145c89 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/cubemap-texture-type.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/custom-skybox-material.png b/versions/4.0/zh/concepts/scene/skybox/custom-skybox-material.png new file mode 100644 index 0000000000..9f2a26a012 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/custom-skybox-material.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/diffuse-map-with-reflection.png b/versions/4.0/zh/concepts/scene/skybox/diffuse-map-with-reflection.png new file mode 100644 index 0000000000..e47e4dff14 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/diffuse-map-with-reflection.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/enable-skybox.jpg b/versions/4.0/zh/concepts/scene/skybox/enable-skybox.jpg new file mode 100644 index 0000000000..92c9b77d5b Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/enable-skybox.jpg differ diff --git a/versions/4.0/zh/concepts/scene/skybox/envmap.png b/versions/4.0/zh/concepts/scene/skybox/envmap.png new file mode 100644 index 0000000000..11f17e8441 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/envmap.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/generate-rc.png b/versions/4.0/zh/concepts/scene/skybox/generate-rc.png new file mode 100644 index 0000000000..56ec995c34 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/generate-rc.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/hemisphere.png b/versions/4.0/zh/concepts/scene/skybox/hemisphere.png new file mode 100644 index 0000000000..67025c5d3e Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/hemisphere.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/material.png b/versions/4.0/zh/concepts/scene/skybox/material.png new file mode 100644 index 0000000000..1f3d48646f Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/material.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/remove.png b/versions/4.0/zh/concepts/scene/skybox/remove.png new file mode 100644 index 0000000000..75eca35630 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/remove.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/set-envmap.png b/versions/4.0/zh/concepts/scene/skybox/set-envmap.png new file mode 100644 index 0000000000..08024c5a7a Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/set-envmap.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/sky-box-type.png b/versions/4.0/zh/concepts/scene/skybox/sky-box-type.png new file mode 100644 index 0000000000..0546441ce4 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/sky-box-type.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/skybox-camera.png b/versions/4.0/zh/concepts/scene/skybox/skybox-camera.png new file mode 100644 index 0000000000..880b9b1051 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/skybox-camera.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/texturecube.png b/versions/4.0/zh/concepts/scene/skybox/texturecube.png new file mode 100644 index 0000000000..9912b3b253 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/texturecube.png differ diff --git a/versions/4.0/zh/concepts/scene/skybox/use-hdr.png b/versions/4.0/zh/concepts/scene/skybox/use-hdr.png new file mode 100644 index 0000000000..127fef4c82 Binary files /dev/null and b/versions/4.0/zh/concepts/scene/skybox/use-hdr.png differ diff --git a/versions/4.0/zh/debug-renderer/debug-renderer-demo.png b/versions/4.0/zh/debug-renderer/debug-renderer-demo.png new file mode 100644 index 0000000000..72a8314c0f Binary files /dev/null and b/versions/4.0/zh/debug-renderer/debug-renderer-demo.png differ diff --git a/versions/4.0/zh/debug-renderer/debug-renderer-setting.png b/versions/4.0/zh/debug-renderer/debug-renderer-setting.png new file mode 100644 index 0000000000..d99301feb4 Binary files /dev/null and b/versions/4.0/zh/debug-renderer/debug-renderer-setting.png differ diff --git a/versions/4.0/zh/debug-renderer/index.md b/versions/4.0/zh/debug-renderer/index.md new file mode 100644 index 0000000000..e0933e15fb --- /dev/null +++ b/versions/4.0/zh/debug-renderer/index.md @@ -0,0 +1,47 @@ +# 调试渲染器(Debug-Renderer) + +调试渲染器是引擎提供的一种批量渲染屏幕文字的功能接口,主要用于调试,输出任意的文字调试信息到屏幕上。目前仅支持原生平台。 + +其效果图如下所示: + +![debug-renderer-demo](./debug-renderer-demo.png) + +## 使用方式 + +1. 确保在项目设置面板中,调试渲染器处于勾选状态 +![debug-renderer-setting](./debug-renderer-setting.png) + +2. 由于每帧渲染完这些文字后会清空顶点缓存,所以需要在 `update` 等函数中,每帧添加调试文本,示例 `TS` 代码如下: + +```TS +import { native, Vec2 } from 'cc'; +import { NATIVE } from 'cc/env'; + +if (NATIVE) { + native.DebugRenderer.getInstance().addText("Stanley", new Vec2(100, 100)); +} +``` + +## C++ 接口描述 + +```cpp +void addText(const ccstd::string &text, const Vec2 &screenPos, const DebugTextInfo &info = DebugTextInfo()); +``` + +其参数描述如下: + +- text:要输出的文字 +- screenPos:文字展示的位置 +- info:定义字体的样式 + +DebugTextInfo 可以定制输出文字的外观,其参数描述如下: + +| 属性 | 说明 | +| :-- | :-- | +| color | 文字颜色 | +| bold | 是否粗体 | +| italic | 是否斜体 | +| shadow | 是否开启阴影效果 | +| shadowThickness | 阴影宽度 | +| shadowColor | 阴影颜色 | +| scale | 文字缩放比例 | diff --git a/versions/4.0/zh/editor/assets/extension.md b/versions/4.0/zh/editor/assets/extension.md new file mode 100644 index 0000000000..6bcfc797f3 --- /dev/null +++ b/versions/4.0/zh/editor/assets/extension.md @@ -0,0 +1,184 @@ +# 增强资源管理器面板 + +为了能更好的理解本篇文档内容,在继续阅读本文档之前,推荐大家先阅读 Cocos Creator [扩展编辑器](../extension/readme.md) 文档,了解扩展开发相关知识。 + +## 自定义右击菜单 + +若要自定义右击菜单,请先参考 [创建并安装扩展](../extension/first.md/#%E5%88%9B%E5%BB%BA%E5%B9%B6%E5%AE%89%E8%A3%85%E6%89%A9%E5%B1%95) 新建一个扩展,在扩展的 `package.json`文件中,通过定义 `contributions.assets.menu` 字段,即可对 **资源管理器** 面板的右击菜单显示事件进行监听,可以实现菜单的追加,如下所示: + +```json5 +// package.json +{ + "contributions": { + "assets": { + "menu": { + "methods": "./dist/assets-menu.js", + "createMenu": "onCreateMenu", + "assetMenu": "onAssetMenu", + "dbMenu": "onDBMenu", + "panelMenu": "onPanelMenu" + } + } + } +} +``` + +各字段含义: + +- `methods` - 菜单事件处理函数(示例中的 `on***Menu` 系列)所在的脚本相对路径 +- `createMenu` - **创建资源** 菜单显示时触发的事件,有两个触发时机: + - 点击资源管理器面板左上角的 **+** 按钮 + - 资源菜单中的 **新建** 菜单项被选中时 +- `dbMenu` - 右击资源数据库根节点 `assets` 时触发的事件 +- `assetMenu` - 右击普通资源节点或目录时触发的事件 +- `panelMenu` - 右击资源管理面板空白区域时触发的事件 + +生成 `./dist/assets-menu.js` 文件的 `assets-menu.ts` 内容如下: + +```typescript +export function onCreateMenu(assetInfo: AssetInfo) { + return [ + { + label: 'i18n:extend-assets-demo.menu.createAsset', + click() { + if (!assetInfo) { + console.log('get create command from header menu'); + } else { + console.log('get create command, the detail of diretory asset is:'); + console.log(assetInfo); + } + }, + }, + ]; +}; + +export function onAssetMenu(assetInfo: AssetInfo) { + return [ + { + label: 'i18n:extend-assets-demo.menu.assetCommandParent', + submenu: [ + { + label: 'i18n:extend-assets-demo.menu.assetCommand1', + enabled: assetInfo.isDirectory, + click() { + console.log('get it'); + console.log(assetInfo); + }, + }, + { + label: 'i18n:extend-assets-demo.menu.assetCommand2', + enabled: !assetInfo.isDirectory, + click() { + console.log('yes, you clicked'); + console.log(assetInfo); + }, + }, + ], + }, + ]; +}; +``` + +`assets-menu.ts` 中 `on****Menu(assetInfo:AssetInfo):MenuItem[]` 系列函数参数和返回值说明如下: + +- 参数 `AssetInfo`:{} + + - `displayName`:string - 资源用于显示的名字 + - `extends`:string[] - 可选,继承类 + - `importer`:string - 导入器名字 + - `isDirectory`:boolean - 是否是文件夹 + - `imported`:boolean - 是否导入完成 + - `invalid`:boolean - 是否导入失败 + - `name`:string - 资源名字 + - `file`:string - 资源文件所在的磁盘绝对路径 + - `readonly`:boolean - 是否只读 + - `type`:string - 资源类型 + - `url`:string - db:// 开头的资源地址 + - `uuid`:string - 资源 ID + +- 返回值 [`MenuItem`] + + - `type`:string - 可选,可选项分别为 normal、separator、submenu、checkbox 或 radio + - `label`:string - 可选,显示的文本 + - `sublabel`:string - 可选,显示的二级文本 + - `submenu`:MenuItem[] - 可选,子项菜单 + - `click`:function - 可选,点击事件 + - `enabled`:boolean - 可选,是否可用,不可用会有置灰样式 + - `visible`:boolean - 可选,是否显示 + - `accelerator`:string - 可选,显示快捷键 + - `checked`:boolean - 可选,当 type 为 `checkbox` / `radio` 时是否选中 + + 更多属性可参考 [electron menu-item](https://www.electronjs.org/docs/api/menu-item) 的数据格式。 + +示例中以 `i18n:` 开始的字符串,需要配置多语言相关内容,请参考[多语言系统(i18n)](../extension/i18n.md)。 + +最终实现效果如下图所示: + +![extend-create-menu](img/extend-create-menu.png) + +## 拖入识别 + +假设我们做了一个拥有若干资源的扩展包,且有一个面板用于展示这些资源的图标。 我们希望实现将面板上的图标拖放到资源窗口时,即可将资源包中的资源拷贝到资源窗口。 + +在 Cocos Creator 扩展中实现这个流程并不复杂。只需要定义一个 `` UI 组件,自定义一个拖入类型,并注入到 **资源管理器** 面板的识别范围内。后续在编辑器其他面板将含有该自定义类型的 `` 元素拖入 **资源管理器** 面板时,**资源管理器** 面板便能识别到它,并给自定义类型的注册方(扩展)发送消息,注册方便能执行一个自定义的动作,比如执行新建一组资源。 + +和 **自定义右击菜单** 一样,我们需要在 `package.json` 文件做对应的配置。 + +```json5 +// package.json +{ + "contributions": { + "assets": { + "drop": [ + { + "type": "my-defined-asset-type-for-drop", // 对应 Demo 示例中 panel.html 的用法 + "message": "drop-asset", + }, + ], + }, + }, + "messages": { + "drop-asset": { + "methods": ["default.dropAsset"], // 'default' 是指当前扩展的默认面板 + }, + }, +} +``` + +- 在扩展的 `default` 面板中加入 `dropAsset` 方法,如下所示: + + ```typescript + export const methods = { + dropAsset(assetInfo: any, dragInfo: any) { + console.log(Editor.I18n.t('extend-assets-demo.drop.callback')); + console.log(assetInfo); + console.log(dragInfo); + }, + }; + ``` + + `assetInfo` 参数说明: + - `uuid`:string - 拖拽资源时,鼠标释放位置的资源 uuid + - `type`:string - 该资源的类型 + - `isDirectory`:boolean - 该资源是否是文件夹 + +- 在扩展的 `defualt` 面板中加入 `ui-drag-item` UI 组件,如下所示: + + ```html + + Drag me to assets panel, and look conosole log. + + ``` + + > **注意**:`ui-drag-item` 中的 `type` 需要与 `contributions.assets.drop.type` 一致,才能产生关联。 + +执行效果如下图所示: + +![extend-assets-drop](./img/extend-assets-drop.png) + +## Demo 示例 + +两个扩展功能的可执行代码可下载 范例 查看 diff --git a/versions/4.0/zh/editor/assets/img/create.png b/versions/4.0/zh/editor/assets/img/create.png new file mode 100644 index 0000000000..7569f34f79 Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/create.png differ diff --git a/versions/4.0/zh/editor/assets/img/drag.png b/versions/4.0/zh/editor/assets/img/drag.png new file mode 100644 index 0000000000..1f70ec2042 Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/drag.png differ diff --git a/versions/4.0/zh/editor/assets/img/extend-assets-demo.zip b/versions/4.0/zh/editor/assets/img/extend-assets-demo.zip new file mode 100644 index 0000000000..7dd7e5282b Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/extend-assets-demo.zip differ diff --git a/versions/4.0/zh/editor/assets/img/extend-assets-drop.png b/versions/4.0/zh/editor/assets/img/extend-assets-drop.png new file mode 100644 index 0000000000..e940d4b17c Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/extend-assets-drop.png differ diff --git a/versions/4.0/zh/editor/assets/img/extend-create-menu.png b/versions/4.0/zh/editor/assets/img/extend-create-menu.png new file mode 100644 index 0000000000..510fcea2b9 Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/extend-create-menu.png differ diff --git a/versions/4.0/zh/editor/assets/img/package-asset/exporting.png b/versions/4.0/zh/editor/assets/img/package-asset/exporting.png new file mode 100644 index 0000000000..f67659081d Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/package-asset/exporting.png differ diff --git a/versions/4.0/zh/editor/assets/img/package-asset/importing.png b/versions/4.0/zh/editor/assets/img/package-asset/importing.png new file mode 100644 index 0000000000..0d676eb29f Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/package-asset/importing.png differ diff --git a/versions/4.0/zh/editor/assets/img/preview.png b/versions/4.0/zh/editor/assets/img/preview.png new file mode 100644 index 0000000000..eb1a6cbaa1 Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/preview.png differ diff --git a/versions/4.0/zh/editor/assets/img/right-click.png b/versions/4.0/zh/editor/assets/img/right-click.png new file mode 100644 index 0000000000..3a609fe8e7 Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/right-click.png differ diff --git a/versions/4.0/zh/editor/assets/img/search-name.png b/versions/4.0/zh/editor/assets/img/search-name.png new file mode 100644 index 0000000000..082b13deae Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/search-name.png differ diff --git a/versions/4.0/zh/editor/assets/img/search-types.png b/versions/4.0/zh/editor/assets/img/search-types.png new file mode 100644 index 0000000000..fb32910cd5 Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/search-types.png differ diff --git a/versions/4.0/zh/editor/assets/img/search-uuid.png b/versions/4.0/zh/editor/assets/img/search-uuid.png new file mode 100644 index 0000000000..1ae821a2b7 Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/search-uuid.png differ diff --git a/versions/4.0/zh/editor/assets/img/thumb.gif b/versions/4.0/zh/editor/assets/img/thumb.gif new file mode 100644 index 0000000000..7d4d273a6b Binary files /dev/null and b/versions/4.0/zh/editor/assets/img/thumb.gif differ diff --git a/versions/4.0/zh/editor/assets/index.md b/versions/4.0/zh/editor/assets/index.md new file mode 100644 index 0000000000..d2c5cdcd80 --- /dev/null +++ b/versions/4.0/zh/editor/assets/index.md @@ -0,0 +1,165 @@ +# 资源管理器 + +**资源管理器** 面板是用于访问和管理项目资源的重要工作区域。在开始制作游戏时,**导入资源** 通常是必须的步骤。在新建项目时可以使用 **HelloWorld** 模板项目,就可以看到 **资源管理器** 中已经包含了一些基本资源类型。 + +![面板操作预览](img/thumb.gif) + +## 面板介绍 + +**资源管理器** 面板主要可以分为 **工具栏** 和 **资源列表** 两部分内容: + +- **工具栏** 中的功能包括 **新建资源**、**排序方式**、**搜索过滤**、**搜索框**、**全部折叠/展开** 和 **刷新列表**。详情可参考下文介绍。 + +- **资源列表** 将项目资源文件夹中的内容以树状结构展示出来,默认包括 **assets** 和 **internal** 两个基本资源库(简称 DB): + + - **assets** 类似操作系统中的文件管理器,只有放在项目文件夹的 `assets` 目录下的资源才会显示在这里。关于项目文件夹结构说明请参考 [项目结构](../../getting-started/project-structure/index.md)。 + - **internal** 中的是默认的内置资源(只读),内置资源可以复制,但不能进行增删改操作。可以作为资源模板拖拽到 **assets** 中,即新建了一个项目资源。 + +- 面板目前支持操作资源的快捷方式包括: + - **复制**:Ctrl/Cmd + C + - **粘贴**:Ctrl/Cmd + V + - **克隆**:Ctrl/Cmd + D、Ctrl + 拖动资源 + - **删除**:Delete + - **上下选择**:上箭头,下箭头 + - **上一层级**:左箭头 + - **下一层级**:右箭头 + - **多选**:Ctrl/Cmd + 点击 + - **连续多选**:Shift + 点击 + - **全选**:Ctrl/Cmd + A + - **重命名**:Enter/F2 + - **取消输入**:Esc + +### 创建资源 + +创建资源有两种方式: +1. 点击 **资源管理器** 左上方的 **+** 按钮。 +2. 在 **资源管理器** 面板中点击鼠标右键,然后选择 **创建**。 + +![create](img/create.png) + +> **注意**:创建资源时会先出现一个 **输入框** 要求填入新资源的名称,名称不能为空。 + +创建资源时,可以在树形资源列表中选中要放置资源的文件夹,则新建的资源会创建到当前选中的文件夹目录下。若没有选中任何文件夹,则新建资源会默认创建到当前根目录下(`assets`)。 + +更多资源相关的内容请参考 [资源手册](../../asset/index.md)。 + +### 选择资源 + +在资源列表中可以使用以下的资源选择操作: + +- 单击以选中单个资源。选中的资源会呈现 **蓝底黄字** 的高亮状态 +- 双击以打开某个资源,比如 scene、script、image。双击文件夹则折叠/展开文件夹 +- 按住 Ctrl/Cmd,然后选择资源,可以同时选中多个资源 +- 按住 Shift,然后选择资源,可以连续选中多个资源 + +对于选中的资源,可以执行移动、删除等操作。 + +### 移动资源 + +选中资源后(可多选),按住鼠标拖拽可以将资源移动到其他位置。将要移动的资源拖拽到目标文件夹上时,会看到鼠标悬停的文件夹呈现黄字的高亮状态,并且在其周围有一个蓝色的方框。这时松开鼠标,就会将资源移动到高亮显示的文件夹目录下。 + +![drag](img/drag.png) + +资源的移动还包括以下操作: + +- 将 **资源管理器** 中的资源拖拽到 **场景编辑器** 或 **层级管理器** 面板可生成节点,目前支持拖拽 `cc.Prefab`、`cc.Mesh`、`cc.SpriteFrame` 资源。 +- 从 **系统的文件管理器** 拖拽文件到 **资源管理器** 列表中,即可导入资源。 +- 从 **层级管理器** 面板拖拽节点到 **资源管理器** 面板的某个文件夹中,可将节点保存为一个 `cc.Prefab` 资源,详见 [预制资源(Prefab)](../../asset/prefab.md)。 + +### 排序资源 + +工具栏中的 **排序方式**包括 **按名称排序** 和 **按类型排序**。当前的排序方式会被记录,下次打开编辑器时会保持当前的排序方式。 + +### 折叠资源 + +折叠分为单一折叠或含子集的全部折叠: + +- **工具栏** 中的 **全部折叠/展开** 按钮作用于全局 +- 单击一个父级资源(例如文件夹)的三角图标,可以展开或折叠它的子集。使用快捷键 **Alt** 并点击三角图标,可全部展开或折叠所有的子资源 + +资源当前的折叠状态会被记录,下次打开编辑器时会保持当前的折叠状态。 + +### 搜索资源 + +搜索功能是一种组合功能,可限定搜索类型并指定搜索字段,这两种都可以达到类型过滤的效果,根据选择的类型/字段,面板中会显示所有相应的资源。 + +![search-types](img/search-types.png) + +- **限定搜索类型**:可多选。类型为资源类型 `assetType`,不是后缀名称或 importer 名称。 + +- **指定搜索字段**:搜索名称不区分大小写,包括以下几种搜索方式: + + 1. **搜索名称或 UUID**,支持搜索资源文件扩展名,例如 `.png`: + + ![search-name](img/search-name.png) + + 2. **搜索 UUID** + + 3. **搜索 URL**,以 `db://` 协议开头 + + 4. **查找 UUID 的使用**,用于查找该 uuid 资源被哪些资源所使用,如下图: + + ![search-uuid](img/search-uuid.png) + +若只想在某个父级资源(例如文件夹)中搜索,则右键点击并选择 **在文件夹中查找**,即可缩小搜索范围。 + +在搜索结果列表中选中资源,双击资源等同于在正常模式下的操作。清空搜索内容后,会重新定位到选中的资源。 + +### 大图预览 + +在 **资源管理器** 中选中资源,即可在 **资源预览** 面板中显示资源的缩略图。若选中资源所在的文件夹,即可显示文件夹下所有资源的缩略图,方便查看。 + +![preview](img/preview.png) + +## 资源的右击菜单 + +选中具体的资源/文件夹后,点击鼠标右键,可以在弹出的菜单中对资源执行一系列操作: + +![right-click](img/right-click.png) + +- **创建**:和 **资源管理器** 面板的 **创建** 按钮功能相同,会将资源添加到当前选中的文件夹下,如果当前选中的是资源文件,会将新增资源添加到和当前选中资源所在文件夹中。 +- **复制**/**剪切**/**粘贴**:将选中的资源(可多选)复制/剪切,然后粘贴到该文件夹下或者另外的文件夹下。 +- **生成副本**:生成和选中资源(可多选)完全相同的资源副本,生成的资源和选中的资源在同一层级中。 +- **重命名**:修改资源名称,详情请查看下文介绍。 +- **删除**:删除资源(资源可多选),也可以使用快捷键 **Delete**。资源删除后会保留在 **系统的回收站** 中,必要时可将其还原。 +- **全选**:选中同一层级中的所有资源。 +- **在文件夹中查找**:使用搜索功能时,只搜索该文件夹中的资源。 +- **查找 UUID 的使用**:通过资源的 UUID 来查找该资源被哪些资源所引用。 +- **导入/导出资源包**:详情请参考下文介绍。 +- **在浏览器中预览此场景**:仅对场景资源有效。 +- **在资源管理器(Windows)或 Finder(Mac)中显示**:在操作系统的文件管理器窗口中打开该资源所在的文件夹。 +- **重新导入资源**:更新资源到项目的 `./library` 文件夹,支持多选批量导入。 +- **在 library 中显示**:打开该资源在项目文件夹的 `Library` 中的位置,详情请阅读 [项目结构](../../getting-started/project-structure/index.md)。 +- **UUID/URL/PATH**:复制资源的 UUID/URL/PATH,并且在 **控制台** 面板中输出。 + +另外对于特定资源类型,双击资源可以进入该资源的编辑状态,如场景资源和脚本资源。 + +### 重命名资源 + +选中需要重命名的资源,然后点击右键,选择 **重命名** 即可修改资源名称,或者也可以直接使用快捷键 **Enter** 或者 **F2**。点击面板其他地方或者按快捷键 **Esc** 便可以取消此次重命名。 + +### 导出资源包 + +选中需要导出的资源并点击右键,然后选择 **导出资源包**,在弹出的 **导出资源** 面板中会自动列出当前选中的资源与其依赖的相关资源。如果不需要导出相关依赖资源,可以在 **导出资源** 面板左下角取消勾选 **包含依赖资源**。 + +确定要导出的资源后,点击 **导出** 按钮,会弹出文件存储对话框,用户需要指定一个文件夹位置和文件名,点击 **存储**,就会生成 **文件名.zip** 的压缩包文件,包含导出的全部资源。 + +> **注意**:扩展会自动把资源打成一个 zip 包,该 zip 包只适用于 **导入资源包** 扩展使用。 + +![exporting](img/package-asset/exporting.png) + +### 导入资源包 + +选中并右键点击要导入资源的文件夹,或者右键点击 **资源管理器** 面板的 **空白处**,然后选择 **导入资源包**,在弹出的文件浏览对话框中选择由 **导出资源包** 导出的 zip 包。zip 包中的资源便会自动解析到弹出的 **导入资源包** 面板。 + +导入过程中也会让用户再次确认导入资源,这时候可以通过取消某些资源的勾选来去掉不需要导入的资源。 + +![importing](img/package-asset/importing.png) + +> **注意**: +> 1. 导入的 zip 包,仅支持由 Cocos Creator 3.0.0 及以上版本使用 **导出资源包** 导出的 zip 包。 +> 2. 不支持导入相同类名的脚本。 + +## 增强资源管理器面板 + +目前支持的扩展包括 **右击菜单** 和 **拖入识别**,详情请参考 [增强资源管理器面板](./extension.md)。 diff --git a/versions/4.0/zh/editor/components/camera-component.md b/versions/4.0/zh/editor/components/camera-component.md new file mode 100644 index 0000000000..b0d869c1c3 --- /dev/null +++ b/versions/4.0/zh/editor/components/camera-component.md @@ -0,0 +1,75 @@ +# 相机 + +游戏中的相机是用来捕捉场景画面的主要工具。我们通过调节相机相关参数来控制可视范围的大小,在 Cocos Creator 编辑器中相机呈如下表示: + +![camera](camera/camera.png) + +相机的可视范围是通过 6 个平面组成一个 **视锥体(Frustum)** 构成,**近裁剪面(Near Plane)** 和 **远裁剪面(Far Plane)** 用于控制近处和远处的可视距离与范围,同时它们也构成了视口的大小。 + +![camera view](camera/camera-view.gif) + +相机组件接口请参考 [Camera API](%__APIDOC__%/zh/class/Camera)。 + +## 相机组件 + +相机组件是我们用来呈现场景画面的重要功能组件。 + +![camera component](camera/camera-component.png) + +| 属性名称 | 说明 | +|:-------|:---| +| Priority | 相机的渲染优先级,值越小越优先渲染 | +| Visibility | 可见性掩码,声明在当前相机中可见的节点层级集合 | +| ClearFlags | 相机的缓冲清除标志位,指定帧缓冲的哪部分要每帧清除。包含:
DONT_CLEAR:不清空;
DEPTH_ONLY:只清空深度;
SOLID_COLOR:清空颜色、深度与模板缓冲;
SKYBOX:启用天空盒,只清空深度 | +| ClearColor | 指定清空颜色 | +| ClearDepth | 指定深度缓冲清空值 | +| ClearStencil | 指定模板缓冲清空值 | +| Projection | 相机投影模式。分为 **透视投影(PERSPECTIVE)** 和 **正交投影(ORTHO)** | +| FovAxis | 指定视角的固定轴向,在此轴上不会跟随屏幕长宽比例变化 | +| Fov | 相机的视角大小 | +| OrthoHeight | 正交模式下的视角 | +| Near | 相机的近裁剪距离,应在可接受范围内尽量取最大 | +| Far | 相机的远裁剪距离,应在可接受范围内尽量取最小 | +| Aperture | 相机光圈,影响相机的曝光参数 | +| Shutter | 相机快门,影响相机的曝光参数 | +| Iso | 相机感光度,影响相机的曝光参数
**Aperture**,**Shutter** 和 **Iso** 属性请参考下方 **曝光量** 获取更多信息 | +| Rect | 相机最终渲染到屏幕上的视口位置和大小 | +| TargetTexture | 指定相机的渲染输出目标贴图,默认为空,直接渲染到屏幕 | + +## 相机分组渲染 + +分组渲染功能是通过相机组件的 Visibility 属性配合节点的 [Layer 属性](../../concepts/scene/node-component.md#%E8%AE%BE%E7%BD%AE%E8%8A%82%E7%82%B9%E7%9A%84-layer-%E5%B1%9E%E6%80%A7) 共同决定的。用户可通过代码设置 Visibility 的值来完成分组渲染。所有节点默认都属于 DEFAULT 层,在所有相机都可见。 + +### 设置 Visibility 属性 + +Visibility 属性用于设置哪些层级(Layer)的节点应该被相机观察到,可同时选择多个 Layer。 + +> **注意**:从 Cocos Creator 3.0 开始,2D 元素(例如 Sprite)的渲染也遵从 Layer 与 Visibility 的判断,开发者可以根据需要自行调整 Layer 与 Visibility。 + +当开发者在 Visibility 属性中勾选了多个 Layer 时,Visibility 属性值便是通过将多个 Layer 的属性值执行 `|` 操作计算得出。 + +例如,下图中相机的 Visibility 属性同时勾选了 **UI_3D** 和 **DEFAULT** 这两个 Layer,通过 [查询 Layer 属性值](../../concepts/scene/layer.md) 可以知道 **UI_3D** 的属性值是 **1 << 23**,**DEFAULT** 的属性值是 **1 << 30**,那么 Visibility 属性值便是 **1 << 23 | 1 << 30 = 1082130432**。 + +![camera visibility gizmo](camera/camera-visibility-gizmo.png) + +关于 Layer 的实现详情,请参考 [层级](../../concepts/scene/layer.md) 文档。 + +### 相机的可见性计算 + +Visibility 属性可以同时选择多个 Layer,同时 Node 上的 Layer 也有自身的值,因此相机的 Visibility 属性是一个 232 位的整数,每一种可见的 layer 占一位,采用位操作运算,最高支持 32 个不同的 Layer 标签(每一种 Layer 值占一位,即用 232 表示)。在相机 culling 时,每个节点的 layer 值会跟相机进行 `&` 操作运算,如果相机的 Visibility 属性包含这个 Layer,那么当前节点就会被相机所看见,反之则看不见。 + +### 曝光量 + +**Aperture**, **Shutter**,**Iso** 这三个相机的物理参数会决定进光量,进而影响曝光量(Exposure Value)。仅在场景开启 HDR 的情况下生效。其算法通常为: + +ev = log 2 (ApertureValue2 / ShutterValue*k / IsoValue) + +其中 `ApertureValue`、`ShutterValue` 和 `IsoValue` 通过 **Aperture**, **Shutter**,**Iso** 这三个属性的枚举值查表得出。k 为常量。 + +下图演示了曝光量对场景的影响: + +![hdr1](./camera/hdr1.png) + +![hdr2](./camera/hdr2.png) + +若要启用场景 HDR 可以参考:[切换 HDR/LDR 模式](../../concepts/scene/skybox.md#切换-hdrldr-模式)。 diff --git a/versions/4.0/zh/editor/components/camera/camera-component.png b/versions/4.0/zh/editor/components/camera/camera-component.png new file mode 100644 index 0000000000..f5d33fb4d4 Binary files /dev/null and b/versions/4.0/zh/editor/components/camera/camera-component.png differ diff --git a/versions/4.0/zh/editor/components/camera/camera-frustum.jpg b/versions/4.0/zh/editor/components/camera/camera-frustum.jpg new file mode 100644 index 0000000000..cd53fe1768 Binary files /dev/null and b/versions/4.0/zh/editor/components/camera/camera-frustum.jpg differ diff --git a/versions/4.0/zh/editor/components/camera/camera-view.gif b/versions/4.0/zh/editor/components/camera/camera-view.gif new file mode 100644 index 0000000000..417e3314d9 Binary files /dev/null and b/versions/4.0/zh/editor/components/camera/camera-view.gif differ diff --git a/versions/4.0/zh/editor/components/camera/camera-visibility-gizmo.png b/versions/4.0/zh/editor/components/camera/camera-visibility-gizmo.png new file mode 100644 index 0000000000..e267e40a33 Binary files /dev/null and b/versions/4.0/zh/editor/components/camera/camera-visibility-gizmo.png differ diff --git a/versions/4.0/zh/editor/components/camera/camera.png b/versions/4.0/zh/editor/components/camera/camera.png new file mode 100644 index 0000000000..9c196ee77b Binary files /dev/null and b/versions/4.0/zh/editor/components/camera/camera.png differ diff --git a/versions/4.0/zh/editor/components/camera/hdr1.png b/versions/4.0/zh/editor/components/camera/hdr1.png new file mode 100644 index 0000000000..ab580824fc Binary files /dev/null and b/versions/4.0/zh/editor/components/camera/hdr1.png differ diff --git a/versions/4.0/zh/editor/components/camera/hdr2.png b/versions/4.0/zh/editor/components/camera/hdr2.png new file mode 100644 index 0000000000..0f885981d3 Binary files /dev/null and b/versions/4.0/zh/editor/components/camera/hdr2.png differ diff --git a/versions/4.0/zh/editor/components/dragonbones.md b/versions/4.0/zh/editor/components/dragonbones.md new file mode 100644 index 0000000000..9bb8e27c1e --- /dev/null +++ b/versions/4.0/zh/editor/components/dragonbones.md @@ -0,0 +1,126 @@ +# DragonBones ArmatureDisplay 组件参考 + +ArmatureDisplay 组件可以对 DragonBones(龙骨)资源进行渲染和播放。 + +![dragonbones](./dragonbones/properties.png) + +在 **层级管理器** 中选中需要添加 ArmatureDisplay 组件的节点,然后点击 **属性检查器** 下方的 **添加组件 -> DragonBones -> ArmatureDisplay** 按钮,即可添加 ArmatureDisplay 组件到节点上。 + +- ArmatureDisplay 组件的使用方法可参考 **DragonBones**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/dragonbones) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/dragonbones))范例。 +- DragonBones 相关的脚本接口请参考 [DragonBones API](%__APIDOC__%/zh/class/dragonBones.ArmatureDisplay)。 + +## DragonBones 属性 + +| 属性 | 功能说明 +| :-------- | :---------- | +| CustomMaterial | 自定义材质,可用于实现溶解、外发光等渲染效果。详情请参考 [自定义材质](../../ui-system/components/engine/ui-material.md)。 +| Color | 设置骨骼动画颜色 +| DragonAsset | 骨骼信息数据,包含了骨骼信息(绑定骨骼动作、slots、渲染顺序、attachments、皮肤等)和动画,但不持有任何状态。
多个 ArmatureDisplay 可以共用相同的骨骼数据。
可拖拽 DragonBones 导出的骨骼资源到这里 +| DragonAtlasAsset | 骨骼数据所需的 Atlas Texture 数据。可拖拽 DragonBones 导出的 Atlas 资源到这里 +| Armature | 当前使用的 Armature 名称 +| Animation | 当前播放的骨骼动画名称 +| Animation Cache Mode | 渲染模式,包括 **REALTIME**(默认)、**SHARED_CACHE** 和 **PRIVATE_CACHE** 三种。
1. **REALTIME** 模式,实时运算,支持 DragonBones 所有的功能。
2. **SHARED_CACHE** 模式,将骨骼动画及贴图数据进行缓存并共享,相当于预烘焙骨骼动画。拥有较高性能,但不支持动作融合、动作叠加、骨骼嵌套,只支持动作开始和结束事件。至于内存方面,当创建 N(N>=3)个相同骨骼、相同动作的动画时,会呈现内存优势。N 值越大,优势越明显。综上 **SHARED_CACHE** 模式适用于场景动画、特效、副本怪物、NPC 等,能极大地提高帧率和降低内存。
3. **PRIVATE_CACHE** 模式,与 **SHARED_CACHE** 类似,但不共享动画及贴图数据,且会占用额外的内存,仅存在性能优势,但如果大量使用该模式播放动画可能会造成卡顿。若想利用缓存模式的高性能,但又存在换装的需求(不能共享贴图数据)时,那么 **PRIVATE_CACHE** 就适合你。 +| TimeScale | 当前骨骼中所有动画的时间缩放率,默认为 **1**,表示不缩放。 +| PlayTimes | 播放默认动画的循环次数。
**-1** 表示使用 DragonBones 资源文件中的默认值
**0** 表示无限循环
**>0** 表示循环次数 +| PremultipliedAlpha | 图片是否启用贴图预乘,默认为 True。
当图片的透明区域出现色块时需要关闭该项。
当图片的半透明区域颜色变黑时需要启用该项 +| DebugBones | 是否显示 bone 的 debug 信息 +| Sockets | 用于将某些外部节点挂到指定的骨骼关节上,属性的值表示挂点的数量。详情请参考下文介绍。 +| Enable Batch | 是否开启合批 + +> **注意**: +> 1. 当使用 ArmatureDisplay 组件时,**属性检查器** 中 Node 组件上的 **Anchor** 与 **Size** 属性是无效的。 +> 2. ArmatureDisplay 组件属于 UI 渲染组件,而 Canvas 节点是 UI 渲染的 **渲染根节点**,所以带有该组件的节点必须是 Canvas 节点(或者是带有 RenderRoot2D 组件的节点)的子节点才能在场景中正常显示。 + +## DragonBones 换装 + +下面通过一个范例介绍 DragonBones 如何换装。通过替换插槽的显示对象,将下图机器人的武器替换为红色框中的武器。 + +![dragonbones-cloth](./dragonbones/cloth.png) + +1. 首先在 **层级管理器** 中新建一个 Canvas 节点,然后在 Canvas 节点下新建一个空节点并命名为 `replaceDBNode`。选中 `replaceDBNode` 并在 **属性检查器** 中添加 ArmatureDisplay 组件,将绿色框中的武器资源拖拽至 ArmatureDisplay 组件的属性框中,如下图所示: + + ![dragonbones-cloth](./dragonbones/cloth2.png) + +2. 再次新建一个空节点并命名为 `dbNode`,然后在 **属性检查器** 中添加 ArmatureDisplay 组件,并将机器人的资源拖拽至 ArmatureDisplay 组件的属性框中,如下图所示。可更改 ArmatureDisplay 组件的 `Animation` 属性用于设置想要播放的动画。 + + ![dragonbones-cloth](./dragonbones/cloth3.png) + +3. 在 **资源管理器** 中新建一个 TypeScript 脚本并命名为 `ReplaceSlotDisplay`,编写组件脚本。脚本代码如下: + + ```ts + import { _decorator, Component, dragonBones } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('ReplaceSlotDisplay') + export class ReplaceSlotDisplay extends Component { + + @property({ type: dragonBones.ArmatureDisplay }) + armatureDisplay: dragonBones.ArmatureDisplay | null = null + @property({ type: dragonBones.ArmatureDisplay }) + replaceArmatureDisplay: dragonBones.ArmatureDisplay | null = null; + + _leftWeaponIndex = 0; + _rightDisplayIndex = 0; + _rightDisplayNames:string[] = []; + _rightDisplayOffset:{x: number, y: number}[] = []; + + start () { + this.replaceArmatureDisplay!.node.active = false; + this._leftWeaponIndex = 0; + this._rightDisplayIndex = 0; + this._rightDisplayNames = ["weapon_1004s_r", "weapon_1004e_r"]; + this._rightDisplayOffset = [{ x: 0, y: 0 }, { x: -60, y: 100 }]; + } + + left () { + let armature = this.armatureDisplay!.armature(); + let slot = armature!.getSlot("weapon_hand_l"); + slot!.displayIndex = slot!.displayIndex == 0 ? 4 : 0; + } + + right () { + this._rightDisplayIndex++; + this._rightDisplayIndex %= this._rightDisplayNames.length; + let armature = this.armatureDisplay!.armature(); + let slot = armature!.getSlot("weapon_hand_r"); + let replaceArmatureName = this.replaceArmatureDisplay!.armatureName; + const displayName = this._rightDisplayNames[this._rightDisplayIndex]; + let factory = dragonBones.CCFactory.getInstance() as any; + factory.replaceSlotDisplay(this.replaceArmatureDisplay!.getArmatureKey(), replaceArmatureName , "weapon_r", displayName, slot); + + let offset = this._rightDisplayOffset[this._rightDisplayIndex]; + slot!.parent.offset.x = offset.x; + slot!.parent.offset.y = offset.y; + armature!.invalidUpdate(); + } + } + ``` + +4. 然后将 `ReplaceSlotDisplay` 脚本挂载到 Canvas 节点上,即将脚本拖拽到 Canvas 节点的 **属性检查器** 中。再将 **层级管理器** 中的 `dbNode` 节点和 `replaceDBNode` 节点分别拖拽到脚本组件对应的属性框中,并保存场景。 + + ![dragonbones-cloth](./dragonbones/dragonbone_tscomponent.png) + +5. 接下来我们需要利用 Button 组件的点击事件来触发 `ReplaceSlotDisplay` 脚本中的 `left` 和 `right` 回调,实现通过点击按钮来替换机器人左/右手的武器。 + + 在 **层级管理器** 的 Canvas 节点下新建两个 Button 节点并命名为 `left` 和 `right`,根据需要调整其位置、大小、文字显示等属性。 + + 然后在 **属性检查器** 中设置 `left` 和 `right` 节点的点击事件,将挂载了 `ReplaceSlotDisplay` 脚本组件的 Canvas 节点分别拖拽到两个节点的 `ClickEvents` 属性的 `cc.Node` 属性框中,指定脚本组件为 `ReplaceSlotDisplay`,并设置回调为 `left`/`right`(下图以 `right` 节点为例): + + ![spine-cloth](./dragonbones/click-events.png) + +6. 保存场景后,点击编辑器上方的预览按钮,然后点击 **Left**/**Right** 按钮即可看到机器人左/右手的武器已经被替换。 + + ![dragonbones-cloth](./dragonbones/cloth4.png) + + 详情可参考官方范例 **ReplaceSlotDisplay**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/dragonbones) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/dragonbones))。 + + > **注意**: + > + > 1. 范例运行起来后右手替换的武器样式与场景中预备的武器样式不一致,这是资源问题导致的,开发者请参考具体的代码实现。 + > 2. 若预览时未显示场景,请检查各节点的 Layer 属性是否与 Camera 节点的保持一致。 + > + > ![layer](./dragonbones/layer.png) + +## DragonBones 挂点与碰撞检测 + +DragonBones 挂点和碰撞检测的方法与 Spine 完全相同,详情请参考 [Spine 挂点与碰撞检测](./spine.md)。 diff --git a/versions/4.0/zh/editor/components/dragonbones/click-events.png b/versions/4.0/zh/editor/components/dragonbones/click-events.png new file mode 100644 index 0000000000..fab4088ec8 Binary files /dev/null and b/versions/4.0/zh/editor/components/dragonbones/click-events.png differ diff --git a/versions/4.0/zh/editor/components/dragonbones/cloth.png b/versions/4.0/zh/editor/components/dragonbones/cloth.png new file mode 100644 index 0000000000..39a09e6b5c Binary files /dev/null and b/versions/4.0/zh/editor/components/dragonbones/cloth.png differ diff --git a/versions/4.0/zh/editor/components/dragonbones/cloth2.png b/versions/4.0/zh/editor/components/dragonbones/cloth2.png new file mode 100644 index 0000000000..cec303adc5 Binary files /dev/null and b/versions/4.0/zh/editor/components/dragonbones/cloth2.png differ diff --git a/versions/4.0/zh/editor/components/dragonbones/cloth3.png b/versions/4.0/zh/editor/components/dragonbones/cloth3.png new file mode 100644 index 0000000000..8f84946a37 Binary files /dev/null and b/versions/4.0/zh/editor/components/dragonbones/cloth3.png differ diff --git a/versions/4.0/zh/editor/components/dragonbones/cloth4.png b/versions/4.0/zh/editor/components/dragonbones/cloth4.png new file mode 100644 index 0000000000..93774e2510 Binary files /dev/null and b/versions/4.0/zh/editor/components/dragonbones/cloth4.png differ diff --git a/versions/4.0/zh/editor/components/dragonbones/dragonbone_tscomponent.png b/versions/4.0/zh/editor/components/dragonbones/dragonbone_tscomponent.png new file mode 100644 index 0000000000..15378ac77a Binary files /dev/null and b/versions/4.0/zh/editor/components/dragonbones/dragonbone_tscomponent.png differ diff --git a/versions/4.0/zh/editor/components/dragonbones/layer.png b/versions/4.0/zh/editor/components/dragonbones/layer.png new file mode 100644 index 0000000000..f77f10b251 Binary files /dev/null and b/versions/4.0/zh/editor/components/dragonbones/layer.png differ diff --git a/versions/4.0/zh/editor/components/dragonbones/properties.png b/versions/4.0/zh/editor/components/dragonbones/properties.png new file mode 100644 index 0000000000..a0152ad984 Binary files /dev/null and b/versions/4.0/zh/editor/components/dragonbones/properties.png differ diff --git a/versions/4.0/zh/editor/components/layer-gizmo.png b/versions/4.0/zh/editor/components/layer-gizmo.png new file mode 100644 index 0000000000..68e8fae78c Binary files /dev/null and b/versions/4.0/zh/editor/components/layer-gizmo.png differ diff --git a/versions/4.0/zh/editor/components/motion-streak.md b/versions/4.0/zh/editor/components/motion-streak.md new file mode 100644 index 0000000000..6045028fb5 --- /dev/null +++ b/versions/4.0/zh/editor/components/motion-streak.md @@ -0,0 +1,26 @@ +# MotionStreak(拖尾)组件参考 + +MotionStreak(拖尾)是运动轨迹,用于在游戏对象的运动轨迹上实现拖尾渐隐效果。 + +![motionstreak](motion-streak/motionstreak.png) + +点击 **属性检查器** 下方的 **添加组件** 按钮,然后从 **Effects** 中选择 **MotionStreak**,即可添加 MotionStreak 组件到节点上。 + +具体的使用方式可以参考 **MotionStreak** ([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/24.motion-streak) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/24.motion-streak)) 范例。 + +![add motionStreak](motion-streak/add-motion-streak.png) + +拖尾的脚本接口请参考 [MotionStreak API](%__APIDOC__%/zh/class/MotionStreak)。 + +## MotionStreak 属性 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| CustomMaterial | 自定义材质,使用方法请参考 [自定义材质](./../../ui-system/components/engine/ui-material.md)。 | +| Preview | 是否启用预览。若勾选该项,则可在 **场景编辑器** 中预览拖尾效果 | +| FadeTime | 拖尾的渐隐时间,以秒为单位。| +| MinSeg | 最小的片段长度(渐隐片段的大小)。拖尾条带相连顶点间的最小距离。| +| Stroke | 拖尾的宽度。| +| Texture | 拖尾的贴图。| +| FastMode | 是否启用快速模式。当启用快速模式,新的顶点会被更快地添加,但精度较低。| +| Color | 拖尾的颜色。 | diff --git a/versions/4.0/zh/editor/components/motion-streak/add-motion-streak.png b/versions/4.0/zh/editor/components/motion-streak/add-motion-streak.png new file mode 100644 index 0000000000..1571e733ee Binary files /dev/null and b/versions/4.0/zh/editor/components/motion-streak/add-motion-streak.png differ diff --git a/versions/4.0/zh/editor/components/motion-streak/motionstreak.png b/versions/4.0/zh/editor/components/motion-streak/motionstreak.png new file mode 100644 index 0000000000..eb120c85ed Binary files /dev/null and b/versions/4.0/zh/editor/components/motion-streak/motionstreak.png differ diff --git a/versions/4.0/zh/editor/components/spine.md b/versions/4.0/zh/editor/components/spine.md new file mode 100644 index 0000000000..9e2da2c289 --- /dev/null +++ b/versions/4.0/zh/editor/components/spine.md @@ -0,0 +1,290 @@ +# Spine Skeleton 组件参考 + +Spine 工具版本支持与资源导入请参考文档:[Spine 骨骼动画资源](../../asset/spine.md)。 + +Spine Skeleton 组件支持 Spine 官方工具导出的数据格式,并对 Spine(骨骼动画)资源进行渲染和播放。 + +![spine](./spine/spine-properties.png) + +在 **层级管理器** 中选中需要添加 ArmatureDisplay 组件的节点,然后点击 **属性检查器** 下方的 **添加组件 -> Spine -> Skeleton** 按钮,即可添加 Skeleton 组件到节点上。 + +- Spine Skeleton 组件的使用方法可参考 **Spine**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/spine) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/spine))范例。 + +- Spine Skeleton 相关的脚本接口请参考 [Skeleton API](%__APIDOC__%/zh/class/Skeleton)。 + +## Spine 属性 + +| 属性 | 功能说明 +| :-------------------- | :----------------- | +| CustomMaterial | 自定义材质,可用于实现溶解、外发光等渲染效果。详情请参考 [自定义材质](../../ui-system/components/engine/ui-material.md)。 +| Color | 设置骨骼动画颜色 +| SkeletonData | 骨骼信息数据,拖拽 Spine 导出后的骨骼资源到该属性中 +| Default Skin | 选择默认的皮肤 +| Animation | 当前播放的动画名称 +| Animation Cache Mode | 渲染模式,包括 **REALTIME**(默认)、**SHARED_CACHE** 和 **PRIVATE_CACHE** 三种。
1. **REALTIME** 模式,实时运算,支持 Spine 所有的功能。
2. **SHARED_CACHE** 模式,将骨骼动画及贴图数据进行缓存并共享,相当于预烘焙骨骼动画。拥有较高性能,但不支持动作融合和动作叠加,只支持动作开始和结束事件。至于内存方面,当创建 N(N>=3)个相同骨骼、相同动作的动画时,会呈现内存优势。N 值越大,优势越明显。综上 **SHARED_CACHE** 模式适用于场景动画、特效、副本怪物、NPC 等,能极大提高帧率和降低内存。
3. **PRIVATE_CACHE** 模式,与 **SHARED_CACHE** 类似,但不共享动画及贴图数据,且会占用额外的内存,仅存在性能优势,如果大量使用该模式播放动画可能会造成卡顿。若想利用缓存模式的高性能,但又存在换装需求(不能共享贴图数据)时,那么 **PRIVATE_CACHE** 就适合你。 +| Loop | 是否循环播放当前动画 +| PremultipliedAlpha | 图片是否启用贴图预乘,默认为 True。
当图片的透明区域出现色块时需要关闭该项,当图片的半透明区域颜色变黑时需要启用该项。 +| TimeScale | 当前骨骼中所有动画的时间缩放率 +| DebugSlots | 是否显示 Slot 的 Debug 信息 +| DebugBones | 是否显示骨骼的 Debug 信息 +| DebugMesh | 是否显示 Mesh 的 Debug 信息 +| UseTint | 是否开启染色效果,默认关闭。 +| Sockets | 用于将某些外部节点挂到指定的骨骼关节上,属性的值表示挂点的数量。详情请参考下文介绍。| +| Enable Batch | 是否开启 Spine 合批 | + +> **注意**: +> 1. 当使用 Spine Skeleton 组件时,**属性检查器** 中 Node 组件上的 **Anchor** 与 **Size** 属性是无效的。 +> 2. Spine Skeleton 组件属于 UI 渲染组件,而 Canvas 节点是 UI 渲染的 **渲染根节点**,所以带有该组件的节点必须是 Canvas 节点(或者是带有 RenderRoot2D 组件的节点)的子节点才能在场景中正常显示。 +> 3. 当使用 Spine Skeleton 组件时,由于拥有 UseTint 属性,所以其自定义材质需要有两个颜色信息,可参考引擎内置的 **builtin-spine.effect**([GitHub](https://github.com/cocos/cocos-engine/blob/v3.0.0/editor/assets/effects/builtin-spine.effect) | [Gitee](https://gitee.com/mirrors_cocos-creator/engine/blob/v3.0.0/editor/assets/effects/builtin-spine.effect))实现,否则 Spine 的染色效果可能会出错。 + +## Spine 换装 + +下面通过一个范例介绍 Spine 如何换装。 + +![spine-cloth](./spine/cloth0.png) + +1. 首先在 **层级管理器** 中新建一个 Canvas 节点,然后在 Canvas 节点下新建一个空节点并命名为 girl。选中 girl 节点并在 **属性检查器** 中添加 Skeleton 组件,将资源拖拽至 Skeleton 组件的 SkeletonData 属性框中。可更改 Skeleton 组件的 Animation 属性用于设置想要播放的动画。 + + ![spine-cloth](./spine/cloth1.png) + +2. 在 **资源管理器** 中新建一个 TypeScript 脚本并命名为 SpineSkin,编写组件脚本。脚本代码如下: + + ```ts + import { _decorator, Component, sp } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('SpineSkin') + export class SpineSkin extends Component { + + @property({ type:sp.Skeleton }) + spine: sp.Skeleton | null = null; + + skinId: number = 0; + + start () { + // Your initialization goes here. + } + + change() { + const skins =['girl', 'boy', 'girl-blue-cape', 'girl-spring-dress'].map(x=> `full-skins/${x}`); + this.skinId = (this.skinId + 1) % skins.length; + this.spine!.setSkin(skins[this.skinId]); + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } + } + ``` + +3. 然后将 `SpineSkin` 脚本挂载到 Canvas 节点上,即将脚本拖拽到 Canvas 节点的 **属性检查器** 中。再将 **层级管理器** 中的 `girl` 节点拖拽到 `SpineSkin` 脚本组件对应的属性框中,并保存场景。 + + ![spine-jscom](./spine/spine-jscom.png) + +4. 接下来我们需要利用 Button 组件的点击事件来触发 SpineSkin 脚本中的 `change` 回调,实现通过点击按钮来切换皮肤。 + + 在 **层级管理器** 的 Canvas 节点下新建一个 Button 节点并命名为 `change_skin`,根据需要调整其位置、大小、文字显示等属性。 + + 然后在 **属性检查器** 中设置 `change_skin` 节点的点击事件,将挂载了 `SpineSkin` 脚本组件的 Canvas 节点拖拽到 `ClickEvents` 属性的 `cc.Node` 属性框中,指定脚本组件为 `SpineSkin`,并设置回调为 `change`: + + ![spine-cloth](./spine/click_event.png) + +5. 根据需要调整场景结构,保存场景后点击编辑器上方的预览按钮,点击 change skin 按钮,可以看到人物皮肤已被替换。 + + ![spine-cloth](./spine/cloth2.png) + + > **注意**:若预览时未显示场景,请检查各节点的 Layer 属性是否与 Camera 节点的保持一致。 + > + > ![layer](./spine/layer.png) + +## Spine 顶点效果 + +顶点效果只有当 Spine Skeleton 组件的 **Animation Cache Mode** 属性设置为 **REALTIME** 模式时有效,下面通过一个范例介绍 Spine 如何设置顶点效果。 + +1. 首先在 **层级管理器** 中新建一个 Canvas 节点,然后在 Canvas 节点下新建一个空节点并命名为 `Spine`。选中 `Spine` 节点,并在 **属性检查器** 中添加 Skeleton 组件,将资源拖拽到 Skeleton 组件的 SkeletonData 属性框中,设置好 Skeleton 组件属性。 + +2. 在 **资源管理器** 中新建一个 TypeScript 脚本并命名为 `SpineExample`,编写组件脚本。脚本代码如下: + + ```ts + import { _decorator, Component, sp } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('SpineExample') + export class SpineExample extends Component { + + @property({ type:sp.Skeleton }) + skeleton: sp.Skeleton | null = null; + + private _jitterEffect?:sp.VertexEffectDelegate; + + start () { + this._jitterEffect = new sp.VertexEffectDelegate(); + // 设置好抖动参数。 + this._jitterEffect.initJitter(20, 20); + // 调用 Skeleton 组件的 setVertexEffectDelegate 方法设置效果。 + this.skeleton!.setVertexEffectDelegate(this._jitterEffect!); + } + + }; + ``` + +3. 然后将 `SpineExample` 脚本挂载到 Canvas 节点上,即将脚本拖拽到节点的 **属性检查器** 中。再将 **层级管理器** 中挂载了 Skeleton 组件的 `Spine` 节点拖拽到脚本组件对应的 Skeleton 属性框中,并保存场景。 + +4. 点击编辑器上方的预览按钮,即可看到 Spine 动画顶点抖动的效果。详情可参考 **SpineMesh**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/spine) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/spine))范例。 + +## Spine 挂点 + +在使用骨骼动画时,经常需要在骨骼动画的某个部位上挂载节点,以实现节点与骨骼动画联动的效果。我们可以通过使用编辑器和脚本两种方式来实现 Spine 挂点。下面通过一个范例来介绍 Spine 如何使用挂点将星星挂在龙的尾巴上,并随一起晃动。 + +![attach0](./spine/attach0.png) + +### 通过编辑器实现 Spine 挂点 + +1. 首先在 **层级管理器** 中新建一个 Canvas 节点,然后在 Canvas 节点下新建一个空节点并命名为 `Spine`。选中 `Spine` 节点,并在 **属性检查器** 中添加 Skeleton 组件,将资源拖拽到 Skeleton 组件的 SkeletonData 属性框中,设置好 Skeleton 组件属性。 + +2. 在 **层级管理器** 中右键点击 Spine 节点,选择 **创建 -> 空节点** 在 Spine 节点下添加一个空节点并命名为 `targetNode`。 然后在 `targetNode` 节点下添加 Sprite 子节点并命名为 `star`。然后将星星资源拖拽到 **属性检查器** 中 Sprite 组件的 **SpriteFrame** 属性上。 + + ![attach1](./spine/attach1.png) + +3. 在 **层级管理器** 中选中 Spine 节点,在 **属性检查器** 中将 Skeleton 组件的 Sockets 属性设置为 1(Sockets 属性的值代表了挂点的数量)。 + + ![attach2](./spine/attach2.png) + +4. 然后设置 **Sockets** 中的 **Path** 和 **Target** 属性,**Path** 的下拉框中会列出所有的骨骼,选择想要挂载的目标骨骼,这里以龙的尾巴为例,然后将 `targetNode` 节点拖拽到 **Target** 属性框中。即可在 **场景编辑器** 中看到星星挂在了龙的尾巴上。 + + > **注意**:请不要直接将 `star` 节点设置为 **Target**,这样会让 `star` 节点自身的 UITransform 无效。请 **新建空节点** 作为 **Target**,并将要挂载的组件作为 **Target** 节点的子节点。 + + ![attach3](./spine/attach3.png) + +5. 保存场景,点击编辑器上方的预览按钮,也可以看到星星挂在龙的尾巴上,并随着龙的尾巴一起晃动。具体可参考官方 **SpineAttach**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/spine) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/spine))范例。 + +### 通过脚本实现 Spine 挂点 + +1. 前两个步骤与通过编辑器实现的一致。 + +2. 在 **资源管理器** 中新建一个 TypeScript 脚本并命名为 `SpineAttach`,编写组件脚本。脚本代码如下: + + ```ts + import { _decorator, Component, sp, Label, Node } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('SpineAttach') + export class SpineAttach extends Component { + + @property({ type: sp.Skeleton }) + skeleton: sp.Skeleton = null!; + + @property({ type: Node }) + attachNode: Node = null!; + + start () { + var socket = new sp.SpineSocket("root/hip/tail1/tail2/tail3/tail4/tail5/tail6/tail7/tail8/tail9/tail10", this.attachNode); // 第一个参数传入的是挂点的目标骨骼。第二个参数传入的是挂点的节点 + this.skeleton!.sockets.push(socket); + this.skeleton!.sockets = this.skeleton!.sockets; + } + + } + ``` + + > **注意**:若不知道目标骨骼的名称,可将 Spine 组件中的 Sockets 属性设置为 1,然后在 `Path` 的下拉框中查找所需的目标骨骼名称。查找完成后再将 Sockets 属性还原为 0 即可。 + +3. 然后将 `SpineAttach` 脚本挂载到 Canvas 节点或者其他节点上,即将脚本拖拽到节点的 **属性检查器** 中。再将 **层级管理器** 中挂载了 Skeleton 组件的 `Spine` 节点和 `targetNode` 节点分别拖拽到脚本组件对应的 **Skeleton** 属性框和 **AttachNode** 属性框中,并保存场景。 + +4. 点击编辑器上方的预览按钮,即可看到星星挂在龙的尾巴上,并随着龙的尾巴一起晃动。 + + ![attach-ts](./spine/attach-ts.gif) + +## Spine 碰撞检测 + +通过 Spine 挂点功能可以对骨骼动画的某个部位做碰撞检测。下面通过一个范例来介绍 Spine 如何实现碰撞检测,通过判断人物脚与地面接触与否来实现当人物跑动时,动态地改变地面颜色。 + +![collider](./spine/collider0.png) + +1. 首先需要在编辑器菜单栏的 **项目 -> 项目设置 -> 功能裁剪** 中将 **2D 物理系统** 设置为 **内置 2D 物理系统**。 + + ![collider](./spine/collider1.png) + +2. 与 Spine 挂点的前两个步骤一样,创建好 Spine 节点和其空子节点(命名为 `frontFoot`),以及 Sprite 节点作为地面(命名为 `Ground`),并设置好位置大小等属性。 + + ![collider](./spine/collider2.png) + +3. 在 **层级管理器** 中选中 frontFoot 节点,在 **属性检查器** 中点击 **添加组件 -> Physics2D -> Colliders -> Polygon Collider2D** 添加碰撞组件,然后设置好碰撞组件参数。 + + ![collider](./spine/collider3.png) + + 参考通过编辑器实现 Spine 挂点的第 3、4 个步骤,将 frontFoot 节点挂载到 Sprite 节点的目标骨骼上(例如脚上),frontFoot 节点便会随着骨骼动画一起运动,从而碰撞组件的包围盒也会实时地与骨骼动画保持同步。 + + ![collider](./spine/collider4.png) + +4. 在 **层级管理器** 中选中 Ground 节点,在 **属性检查器** 中点击 **添加组件 -> Physics2D -> Colliders -> BoxCollider2D** 添加碰撞组件,然后设置好碰撞组件参数。 + +5. 在 **资源管理器** 中新建一个 TypeScript 脚本并命名为 `SpineCollider`,然后将脚本挂载到地面节点 `Ground` 上。脚本代码如下: + + ```ts + import { _decorator, Component, Node, PhysicsSystem2D, Contact2DType, Collider2D, Color, Sprite, ParticleSystem2D, EPhysics2DDrawFlags } from 'cc'; + const { ccclass } = _decorator; + + @ccclass('SpineCollider') + export class SpineCollider extends Component { + + touchingCountMap : Map < Node, number > = new Map; + + private debugDrawFlags : number = 0; + + start () { + // Your initialization goes here. + PhysicsSystem2D.instance.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); + PhysicsSystem2D.instance.on(Contact2DType.END_CONTACT, this.onEndContact, this); + this.debugDrawFlags = PhysicsSystem2D.instance.debugDrawFlags; + } + + onEnable () { + PhysicsSystem2D.instance.debugDrawFlags = this.debugDrawFlags | EPhysics2DDrawFlags.Shape; + } + + onDisable () { + PhysicsSystem2D.instance.debugDrawFlags = this.debugDrawFlags; + } + + addContact (c: Collider2D) { + let count = this.touchingCountMap.get(c.node) || 0; + this.touchingCountMap.set(c.node, ++count); + + let sprite = c.getComponent(Sprite); + if (sprite) { + sprite.color = Color.RED; + } + } + + removeContact (c: Collider2D) { + let count = this.touchingCountMap.get(c.node) || 0; + --count; + if (count <= 0) { + this.touchingCountMap.delete(c.node); + + let sprite = c.getComponent(Sprite); + if (sprite) { + sprite.color = Color.WHITE; + } + } else { + this.touchingCountMap.set(c.node, count); + } + } + + onBeginContact (a: Collider2D, b: Collider2D) { + this.addContact(a); + this.addContact(b); + } + + onEndContact (a: Collider2D, b: Collider2D) { + this.removeContact(a); + this.removeContact(b); + } + } + ``` + +6. 点击编辑器上方的预览按钮,即可看到效果。具体可参考 **SpineCollider**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/spine) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/spine))范例。 + + > **注意**:由于挂点的实现机制,会导致基于挂点的碰撞检测存在延迟一帧的问题。 + + ![collider](./spine/collider.gif) diff --git a/versions/4.0/zh/editor/components/spine/attach-ts.gif b/versions/4.0/zh/editor/components/spine/attach-ts.gif new file mode 100644 index 0000000000..db5f5465d5 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/attach-ts.gif differ diff --git a/versions/4.0/zh/editor/components/spine/attach0.png b/versions/4.0/zh/editor/components/spine/attach0.png new file mode 100644 index 0000000000..8ac5c2e1be Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/attach0.png differ diff --git a/versions/4.0/zh/editor/components/spine/attach1.png b/versions/4.0/zh/editor/components/spine/attach1.png new file mode 100644 index 0000000000..677b8749cf Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/attach1.png differ diff --git a/versions/4.0/zh/editor/components/spine/attach2.png b/versions/4.0/zh/editor/components/spine/attach2.png new file mode 100644 index 0000000000..39c82fd585 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/attach2.png differ diff --git a/versions/4.0/zh/editor/components/spine/attach3.png b/versions/4.0/zh/editor/components/spine/attach3.png new file mode 100644 index 0000000000..d5dd5f1ab4 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/attach3.png differ diff --git a/versions/4.0/zh/editor/components/spine/click_event.png b/versions/4.0/zh/editor/components/spine/click_event.png new file mode 100644 index 0000000000..9033d779e0 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/click_event.png differ diff --git a/versions/4.0/zh/editor/components/spine/cloth0.png b/versions/4.0/zh/editor/components/spine/cloth0.png new file mode 100644 index 0000000000..cee994f916 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/cloth0.png differ diff --git a/versions/4.0/zh/editor/components/spine/cloth1.png b/versions/4.0/zh/editor/components/spine/cloth1.png new file mode 100644 index 0000000000..dd6072b16e Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/cloth1.png differ diff --git a/versions/4.0/zh/editor/components/spine/cloth2.png b/versions/4.0/zh/editor/components/spine/cloth2.png new file mode 100644 index 0000000000..6d763ae0ae Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/cloth2.png differ diff --git a/versions/4.0/zh/editor/components/spine/collider.gif b/versions/4.0/zh/editor/components/spine/collider.gif new file mode 100644 index 0000000000..5e5f1c7781 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/collider.gif differ diff --git a/versions/4.0/zh/editor/components/spine/collider0.png b/versions/4.0/zh/editor/components/spine/collider0.png new file mode 100644 index 0000000000..c97db18065 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/collider0.png differ diff --git a/versions/4.0/zh/editor/components/spine/collider1.png b/versions/4.0/zh/editor/components/spine/collider1.png new file mode 100644 index 0000000000..b6f6d58007 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/collider1.png differ diff --git a/versions/4.0/zh/editor/components/spine/collider2.png b/versions/4.0/zh/editor/components/spine/collider2.png new file mode 100644 index 0000000000..a284c0a2e0 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/collider2.png differ diff --git a/versions/4.0/zh/editor/components/spine/collider3.png b/versions/4.0/zh/editor/components/spine/collider3.png new file mode 100644 index 0000000000..3afbd503c3 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/collider3.png differ diff --git a/versions/4.0/zh/editor/components/spine/collider4.png b/versions/4.0/zh/editor/components/spine/collider4.png new file mode 100644 index 0000000000..5dccdfc5c4 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/collider4.png differ diff --git a/versions/4.0/zh/editor/components/spine/layer.png b/versions/4.0/zh/editor/components/spine/layer.png new file mode 100644 index 0000000000..f77f10b251 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/layer.png differ diff --git a/versions/4.0/zh/editor/components/spine/spine-jscom.png b/versions/4.0/zh/editor/components/spine/spine-jscom.png new file mode 100644 index 0000000000..f8b6f69f89 Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/spine-jscom.png differ diff --git a/versions/4.0/zh/editor/components/spine/spine-properties.png b/versions/4.0/zh/editor/components/spine/spine-properties.png new file mode 100644 index 0000000000..b2e914ef6b Binary files /dev/null and b/versions/4.0/zh/editor/components/spine/spine-properties.png differ diff --git a/versions/4.0/zh/editor/components/tiledmap.md b/versions/4.0/zh/editor/components/tiledmap.md new file mode 100644 index 0000000000..93292ba929 --- /dev/null +++ b/versions/4.0/zh/editor/components/tiledmap.md @@ -0,0 +1,85 @@ +# TiledMap 组件参考 + +TiledMap(地图)用于在游戏中显示 TMX 格式的地图。 + +![tiledmap-component](tiledmap/tiledmap-component.png) + +点击 **属性检查器** 下方的 **添加组件 -> Components -> TiledMap** 按钮,即可添加 TiledMap 组件到节点上。 + +![](./tiledmap/add_tiledmap.png) + +TiledMap 的脚本接口请参考 [TiledMap API](%__APIDOC__%/zh/class/TiledMap)。 + +## TiledMap 属性 + +| 属性 | 功能说明 +| :---------------- | :----------------- | +| Tmx Asset | 指定 `.tmx` 格式的地图资源(请将 `.tmx` 和 `.tsx` 放置于同一文件夹) | +| EnableCulling | 启用裁剪,如果需要旋转地图或者把地图置于 3D 相机中,则需要关闭裁剪。如果地图块不是非常多,例如小于 5000 块,那么关闭裁剪能减少 CPU 的运算负担,GPU 直接使用缓存进行渲染 | + +## 详细说明 + +- 添加 TiledMap 组件之后,从 **资源管理器** 中拖拽一个 **.tmx** 格式的地图资源到 Tmx Asset 属性上就可以在场景中看到地图的显示了。 +- 在 TiledMap 组件中添加了 Tmx Asset 属性后,会在节点中自动添加与地图中的 Layer 对应的节点(如下图中的 floor、barrier 和 players 节点)。这些节点都添加了 TiledLayer 组件。**请勿删除这些 Layer 节点中的 TiledLayer 组件**。 + + ![](./tiledmap/tiledlayer.png) + +- TiledMap 组件不支持 `mapLoaded` 回调,在 `start` 函数中可正常使用 TiledMap 组件。 + +## TiledLayer 与节点遮挡 + +TiledLayer 组件会将添加到地图层的节点坐标转化为地图块行列坐标。当按行列顺序渲染地图层中的地图块时,如果该地图块的行列中存在节点,那么将会中断渲染地图块转而渲染节点。当地图块中的节点渲染完毕后,会继续渲染地图块。以此实现节点与地图层相互遮挡关系。 + +> **注意**:该遮挡关系只与节点的坐标有关,与节点的大小无关。 + +下面通过一个范例来介绍 TiledLayer 如何与节点相互遮挡。 + +1. 在场景中新建一个节点并添加 TiledMap 组件,设置好 TiledMap 组件属性后会自动生成带有 TiledLayer 组件的节点(即地图层)。 + +2. 创建 [预制资源](../../asset/prefab.md) 以便在场景中实例化出多个节点。 + +3. 在 **资源管理器** 中新建一个 TypeScript 脚本,编写组件脚本。脚本代码如下: + + ```ts + import { _decorator, Component, Node, TiledLayer, loader, Prefab, v2, instantiate, Vec3, EventTouch } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass('ShieldNode') + export class ShieldNode extends Component { + + @property({ type: TiledLayer }) + public tiledLayer: TiledLayer | null = null; + + @property({ type: Prefab }) + public nodePrefab: Prefab | null = null; + + start () { + this.initScene(this.nodePrefab!); + } + + initScene (prefab: Prefab) { + const posArr = [v2(-249, 96), v2(-150, 76), v2(-60, 54), v2(-248, -144), v2(-89, -34)]; + const tmpP = new Vec3(); + for (let i = 0; i < posArr.length; i++) { + const shieldNode = instantiate(prefab); + shieldNode.setPosition(posArr[i].x, posArr[i].y); + this.tiledLayer!.addUserNode(shieldNode); + shieldNode.on(Node.EventType.TOUCH_MOVE, (event:EventTouch) => { + const deltaMove = event.getDelta(); + shieldNode.getPosition(tmpP); + tmpP.x += deltaMove.x; + tmpP.y += deltaMove.y; + shieldNode.setPosition(tmpP); + }); + } + } + } + ``` + +4. 将脚本组件挂载到 Canvas 节点上,即将脚本拖拽到 Canvas 节点的 **属性检查器** 中。再将 **层级管理器** 中自动生成的带有 TiledLayer 组件的节点以及 **资源管理器** 中的预制资源拖拽至脚本组件对应的属性框中,然后保存场景。 + +5. 点击编辑器上方的预览按钮,即可看到节点与地图层相互遮挡的效果。关于代码可参考 **ShieldNode**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/middleware/tiled-map) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/tiled-map))范例。 + + ![shieldNode](./tiledmap/shieldNode.png) + +若想移除地图层中的节点,调用 TiledLayer 的 `removeUserNode` 方法即可。 diff --git a/versions/4.0/zh/editor/components/tiledmap/add_tiledmap.png b/versions/4.0/zh/editor/components/tiledmap/add_tiledmap.png new file mode 100644 index 0000000000..89b32df536 Binary files /dev/null and b/versions/4.0/zh/editor/components/tiledmap/add_tiledmap.png differ diff --git a/versions/4.0/zh/editor/components/tiledmap/shieldNode.png b/versions/4.0/zh/editor/components/tiledmap/shieldNode.png new file mode 100644 index 0000000000..9ed5de9157 Binary files /dev/null and b/versions/4.0/zh/editor/components/tiledmap/shieldNode.png differ diff --git a/versions/4.0/zh/editor/components/tiledmap/tiledlayer.png b/versions/4.0/zh/editor/components/tiledmap/tiledlayer.png new file mode 100644 index 0000000000..293588271c Binary files /dev/null and b/versions/4.0/zh/editor/components/tiledmap/tiledlayer.png differ diff --git a/versions/4.0/zh/editor/components/tiledmap/tiledmap-component.png b/versions/4.0/zh/editor/components/tiledmap/tiledmap-component.png new file mode 100644 index 0000000000..2539041575 Binary files /dev/null and b/versions/4.0/zh/editor/components/tiledmap/tiledmap-component.png differ diff --git a/versions/4.0/zh/editor/components/tiledtile.md b/versions/4.0/zh/editor/components/tiledtile.md new file mode 100644 index 0000000000..78408bf5cd --- /dev/null +++ b/versions/4.0/zh/editor/components/tiledtile.md @@ -0,0 +1,62 @@ +# TiledTile 组件参考 + +TiledTile 组件可以单独对某一个地图块进行操作。 + +![tiledTile-component](./tiledtile/tiledtile-component.png) + +## 创建方式 + +### 通过编辑器创建 + +在创建 [TiledMap 组件](tiledmap.md) 过程中 **自动生成** 的 Layer 节点下创建一个空节点。然后选中该空节点,点击 **属性检查器** 下方的 **添加组件 -> TiledMap -> TiledTile**,即可添加 TiledTile 组件到节点上。再通过设置 TiledTile 组件上的属性来操作地图块。 + +![add tiledTile](./tiledtile/add_tiledtile.png) + +相关 TiledTile 脚本接口请参考 [TiledTile API](%__APIDOC__%/zh/class/TiledTile) + +### 通过代码创建 + +在代码中设置地图块有两种方式。当你在某个 Layer 节点中设置了 TiledTile 之后,该 Layer 节点原先所在位置的 TiledTile 将会被取代。 + +#### 通过对一个节点添加 TiledTile 组件创建 + +```ts +// 创建一个新节点 +const node = new Node(); +// 然后把该节点的父节点设置为任意的 layer 节点 +node.parent = this.layer.node; +// 最后添加 TiledTile 组件到该节点上,并返回 TiledTile 对象,就可以对 TiledTile 对象进行一系列操作 +const tiledTile = node.addComponent(TiledTile); +``` + +#### 通过 getTiledTileAt 获取 TiledTile + +```ts +// 获取 layer 上横向坐标为 0,纵向坐标为 0 的 TiledTile 对象,就可以对 TiledTile 对象进行一系列操作 +const tiledTile = this.layer.getTiledTileAt(0, 0); +``` + +Layer 脚本接口相关请参考 [TiledLayer API](%__APIDOC__%/zh/class/TiledLayer) + +## TiledTile 属性 + +| 属性 | 功能说明 +| :-----| :---------- | +| X | 指定 TiledTile 的横向坐标,以地图块为单位 +| Y | 指定 TiledTile 的纵向坐标,以地图块为单位 +| Gid | 指定 TiledTile 的 gid 值,来切换 TiledTile 的样式 + +TiledTile 可以控制指定的地图块,以及将节点的位移、旋转和缩放等应用到地图块。用户可以通过更改 TiledTile 的 gid 属性来更换地图块样式。 + +> **注意**: 只能使用地图中现有地图块的 gid 来切换地图块的样式,无法通过自定义 Sprite Frame 来切换地图块的样式。 + +## 可作用到 TiledTile 上的节点属性 + +| 属性 | 功能说明 +| :-----| :---------- | +| Position | 可对指定的 TiledTile 进行 **平移** 操作 +| Rotation | 可对指定的 TiledTile 进行 **旋转** 操作 +| Scale | 可对指定的 TiledTile 进行 **缩放** 操作 +| Color | 可对指定的 TiledTile 进行更改 **颜色** 操作 +| Opacity | 可对指定的 TiledTile 调整 **不透明度** +| Skew | 可对指定的 TiledTile 调整 **倾斜角度** diff --git a/versions/4.0/zh/editor/components/tiledtile/add_tiledtile.png b/versions/4.0/zh/editor/components/tiledtile/add_tiledtile.png new file mode 100644 index 0000000000..c90a45a63e Binary files /dev/null and b/versions/4.0/zh/editor/components/tiledtile/add_tiledtile.png differ diff --git a/versions/4.0/zh/editor/components/tiledtile/tiledtile-component.png b/versions/4.0/zh/editor/components/tiledtile/tiledtile-component.png new file mode 100644 index 0000000000..bab5fc2276 Binary files /dev/null and b/versions/4.0/zh/editor/components/tiledtile/tiledtile-component.png differ diff --git a/versions/4.0/zh/editor/console/index.md b/versions/4.0/zh/editor/console/index.md new file mode 100644 index 0000000000..9fe93f41cd --- /dev/null +++ b/versions/4.0/zh/editor/console/index.md @@ -0,0 +1,84 @@ +# 控制台 + +![index](index/index.png) + +**控制台** 输出编辑器或引擎信息,信息类型包括 **log 日志**、**warn 警告** 和 **error 报错**。不同类型的信息会以不同颜色显示。 + +- `console.log()`:输出日志,灰色文字,通常用来显示正在进行的操作。 + + ![log](index/log.png) + +- `console.warn()`:输出警告,黄色文字,用于提示开发者最好进行处理的异常情况,但不处理也不会影响运行。 + + ![warn](index/warn.png) + +- `console.error()`:输出错误,红色文字,表示出现了严重错误,必须解决才能进行下一步操作或者运行游戏。 + + ![error](index/error.png) + +## 面板操作 + +顶部工具栏功能依次是: + +- 清空当前控制台内的所有日志 +- 输入文本模糊搜索 +- 是否将输入文本转为正则进行搜索 +- 选择要显示的日志类型 +- 打开备份在磁盘里的日志文件,每次编辑器启动时会重置该文件数据。 + + ![open-log-file](index/open-log-file.png) + +## 参数设置 + +控制台的一些参数可在 **偏好设置 -> 控制台** 中配置,包括是否显示日期,调整文字大小。 + +![preferences](index/preferences.png) + +## 自定义输出信息 + +为了方便定位文件、节点或资源,或者提供跳转到帮助文档的链接等,Cocos Creator 支持在编辑器主菜单的 **开发者 -> 开发人员工具** 中自定义输出到 **控制台** 的日志,目前支持输出以下内容: + +- 根据 URL 跳转链接 +- 根据 URL 显示图片 +- 根据 URL 或 UUID 定位到 Asset 资源 +- 根据 UUID 定位到 Node 节点 +- 根据磁盘文件路径 path 定位到脚本文件 +- 输出对应语言的文案 + +### 数据格式 + +根据输出内容的不同,输入的数据格式包括以下两种: + +- `{type[text](url)}` +- `{type(text | url | uuid | path)}` + +数据格式说明如下: + +- 整体匹配 `{}` 中的字符; +- `[text]`:跳转链接的显示文本,选填; +- `type`:输出的信息类型,包括以下几种。填写时不区分大小写;若不填写则直接输出输入的内容,不带有数据格式。 + + - `link`:外部跳转链接 + - `image`:显示图片 + - `asset`:定位到资源 + - `node`:定位到节点 + - `i18n`:多语言翻译 + +### 示例 + +打开编辑器主菜单中的 **开发者 -> 开发人员工具**,然后输入: + +```sh +console.log('Open {link[the help doc url](https://docs.cocos.com/creator/manual/en/editor/console/)}'); +console.log('Locate {link[ the file in library](D:/cocos-creator/a/library/36/36b55a90-1547-4695-8105-abd89f8a0e5f.js)}'); +console.log('Locate Node UUID {node(f6zHdGKiZDhqbDizUsp8mK)}'); +console.warn('Locate Asset UUID {asset(17185449-5194-4d6c-83dc-1e785375acdb)}'); +console.error('Locate Asset URL {asset(db://assets/animation.anim)}'); +console.log('The URL is {asset[{asset(db://assets/animation.anim)}](db://assets/animation.anim)}'); +console.log('Show image {image(https://forum.cocos.org/images/logo.png)}'); +console.log('Translate: {i18n(console.description)}'); +``` + +可以在 **控制台** 中看到输出的日志: + +![content](index/content.png) diff --git a/versions/4.0/zh/editor/console/index/content.png b/versions/4.0/zh/editor/console/index/content.png new file mode 100644 index 0000000000..49e97861a2 Binary files /dev/null and b/versions/4.0/zh/editor/console/index/content.png differ diff --git a/versions/4.0/zh/editor/console/index/error.png b/versions/4.0/zh/editor/console/index/error.png new file mode 100644 index 0000000000..b6db81ad83 Binary files /dev/null and b/versions/4.0/zh/editor/console/index/error.png differ diff --git a/versions/4.0/zh/editor/console/index/index.png b/versions/4.0/zh/editor/console/index/index.png new file mode 100644 index 0000000000..2a81b90704 Binary files /dev/null and b/versions/4.0/zh/editor/console/index/index.png differ diff --git a/versions/4.0/zh/editor/console/index/log.png b/versions/4.0/zh/editor/console/index/log.png new file mode 100644 index 0000000000..20dd9ffa23 Binary files /dev/null and b/versions/4.0/zh/editor/console/index/log.png differ diff --git a/versions/4.0/zh/editor/console/index/open-log-file.png b/versions/4.0/zh/editor/console/index/open-log-file.png new file mode 100644 index 0000000000..60f2516aa4 Binary files /dev/null and b/versions/4.0/zh/editor/console/index/open-log-file.png differ diff --git a/versions/4.0/zh/editor/console/index/preferences.png b/versions/4.0/zh/editor/console/index/preferences.png new file mode 100644 index 0000000000..63b0b12cab Binary files /dev/null and b/versions/4.0/zh/editor/console/index/preferences.png differ diff --git a/versions/4.0/zh/editor/console/index/warn.png b/versions/4.0/zh/editor/console/index/warn.png new file mode 100644 index 0000000000..e2d938337b Binary files /dev/null and b/versions/4.0/zh/editor/console/index/warn.png differ diff --git a/versions/4.0/zh/editor/editor-layout/index.md b/versions/4.0/zh/editor/editor-layout/index.md new file mode 100644 index 0000000000..171b687215 --- /dev/null +++ b/versions/4.0/zh/editor/editor-layout/index.md @@ -0,0 +1,40 @@ +# 编辑器布局 + +编辑器布局是指 Cocos Creator 里各个面板的位置、大小和层叠情况。 + +选择主菜单里的 **Cocos Creator/File -> 布局** 菜单,目前只支持 **默认布局**。在默认布局的基础上,也可以继续对各个面板的位置和大小进行调节。对布局的修改会自动保存在全局目录下: + +- **Windows**:`%USERPROFILE%\.CocosCreator\editor\window.json` +- **macOS**:`$HOME/.CocosCreator/editor/window.json` + +## 调整面板大小 + +将鼠标悬浮到两个面板之间的边界线上,看到鼠标指针变成 ![mouse-cursor](index/mouse-cursor.jpg) 后,按下鼠标,可以看到边界线变成了黄色,就可以拖动鼠标来修改相邻两个面板的大小。 + +![resize](index/resize.png) + +> **注意**:部分面板设置了最小尺寸,当拖拽到最小尺寸限度后就无法再继续缩小面板了。 + +## 移动面板 + +点击面板的标签栏并拖拽,可以将面板整个移动到编辑器窗口中的任意位置。下图中红框表示可拖拽的标签栏区域,箭头表示拖拽方向: + +![drag tab](index/drag_tab.png) + +移动面板的过程中,蓝色半透明的方框表示松开鼠标后,移动的面板将会被放置的位置。 + +## 层叠面板 + +除了移动面板位置,拖拽面板标签栏的时候还可以移动鼠标到另一个面板的标签栏区域: + +![stack before](index/stack_before.png) + +在目标面板的标签栏出现红色竖线时松开鼠标,就能够将两个面板层叠在一起,同时只能显示一个面板: + +![stack after](index/stack_after.png) + +层叠面板在屏幕分辨率不足,或排布使用率较低的面板时非常实用。层叠中的面板可以随时拖拽出来,恢复永远在最上的显示。 + +面板也支持弹出独立面板或者关闭面板操作,点击面板右上方的按钮即可。但需要注意的是 **场景编辑器** 面板不支持弹出/关闭。 + +![popup](index/popup.png) diff --git a/versions/4.0/zh/editor/editor-layout/index/drag_tab.png b/versions/4.0/zh/editor/editor-layout/index/drag_tab.png new file mode 100644 index 0000000000..17d7800e77 Binary files /dev/null and b/versions/4.0/zh/editor/editor-layout/index/drag_tab.png differ diff --git a/versions/4.0/zh/editor/editor-layout/index/mouse-cursor.jpg b/versions/4.0/zh/editor/editor-layout/index/mouse-cursor.jpg new file mode 100644 index 0000000000..df66301fc4 Binary files /dev/null and b/versions/4.0/zh/editor/editor-layout/index/mouse-cursor.jpg differ diff --git a/versions/4.0/zh/editor/editor-layout/index/popup.png b/versions/4.0/zh/editor/editor-layout/index/popup.png new file mode 100644 index 0000000000..de83312559 Binary files /dev/null and b/versions/4.0/zh/editor/editor-layout/index/popup.png differ diff --git a/versions/4.0/zh/editor/editor-layout/index/resize.png b/versions/4.0/zh/editor/editor-layout/index/resize.png new file mode 100644 index 0000000000..c8a7d9f1f4 Binary files /dev/null and b/versions/4.0/zh/editor/editor-layout/index/resize.png differ diff --git a/versions/4.0/zh/editor/editor-layout/index/stack_after.png b/versions/4.0/zh/editor/editor-layout/index/stack_after.png new file mode 100644 index 0000000000..c631652c96 Binary files /dev/null and b/versions/4.0/zh/editor/editor-layout/index/stack_after.png differ diff --git a/versions/4.0/zh/editor/editor-layout/index/stack_before.png b/versions/4.0/zh/editor/editor-layout/index/stack_before.png new file mode 100644 index 0000000000..de3d8e49d8 Binary files /dev/null and b/versions/4.0/zh/editor/editor-layout/index/stack_before.png differ diff --git a/versions/4.0/zh/editor/extension/api/app.md b/versions/4.0/zh/editor/extension/api/app.md new file mode 100644 index 0000000000..574242a4f2 --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/app.md @@ -0,0 +1,77 @@ +# App + +Creator 基本信息 + +## 变量 + +### dev + +• **dev**: `boolean` + +是否是开发模式 + +```typescript +Editor.App.dev; // true +``` + +### home + +• **home**: `string` + +Creator 的主目录,存放一些临时文件和配置信息 + +```typescript +Editor.App.home; // "C:\\Users\\Administrator\\.CocosCreator_Develop" +``` + +### path + +• **path**: `string` + +Creator 程序所在文件夹 + +```typescript +Editor.App.path; // "D:\\Program\\CocosEditor\\Creator\\3.4.0\\resources\\app.asar" +``` + +### temp + +• **temp**: `string` + +获取当前编辑器的临时缓存目录 + +```typescript +Editor.App.temp; // "C:\\Users\\ADMINI~1\\AppData\\Local\\Temp\\CocosCreator\\3.4.0" +``` + +### userAgent + +• **userAgent**: `string` + +Creator 使用的用户代理信息 + +```typescript +const UA = Editor.App.userAgent; // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) CocosCreator/3.4.0 Chrome/91.0.4472.106 Electron/13.1.4 Safari/537.36" +``` + +### version + +• **version**: `string` + +Creator 版本号 + +```typescript +const version = Editor.App.version; // "3.4.0" +``` + +## 函数 + +### quit + +▸ **quit**(): `void` + +正常退出 Creator,会逐个询问所有扩展,当所有扩展都允许关闭后,才会开始关闭流程 + +```typescript +Editor.App.quit(); +``` diff --git a/versions/4.0/zh/editor/extension/api/clipboard.md b/versions/4.0/zh/editor/extension/api/clipboard.md new file mode 100644 index 0000000000..629f4ff2d3 --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/clipboard.md @@ -0,0 +1,79 @@ +# Clipboard + +系统剪切板,当调用剪切板接口的时候,会修改系统剪切板内容。请谨慎使用 + +## 接口说明 + +```typescript +export type ICopyType = 'image' | 'text' | 'files' | string; +``` + +## 函数 + +### clear + +▸ **clear**(): `void` + +清空剪贴板 + +```typescript +Editor.Clipboard.clear(); +``` + +### has + +▸ **has**(type: `ICopyType`): `boolean` + +判断当前剪贴板内容是否含有指定类型的数据 + +**请求参数** + +| Name | Type | Description | +| :----- | :---------- | --------------- | +| `type` | `ICopyType` | 剪贴板内容的类型 | + +**返回结果** + +`boolean` + +```typescript +const res = Editor.Clipboard.has('files'); // false +``` + +### read + +▸ **read**(type: `ICopyType`): `any` + +获取剪贴板内容 + +**请求参数** + +| Name | Type | Description | +| :----- | :---------- | ------------- | +| `type` | `ICopyType` | 剪贴板内容的类型 | + +```typescript +const textRes = Editor.Clipboard.read('text'); // 'your copy text' +const filesRes = Editor.Clipboard.read('files'); // [] +``` + +### write + +▸ **write**(type: `ICopyType`, value: `string | FileList`): `boolean` + +写入剪贴板内容,请勿频繁调用,这个操作会覆盖系统剪切板,频繁调用可能会影响用户正常操作 + +**请求参数** + +| Name | Type | Description | +| :------ | :------------------------- | ----------------------- | +| `type` | `ICopyType` | 剪贴板内容的类型 | +| `value` | `string` \| `FileList` | 复制到剪贴板中的内容 | + +**返回结果** + +`boolean` + +```typescript +Editor.Clipboard.write('text', 'you can test other type'); +``` diff --git a/versions/4.0/zh/editor/extension/api/dialog.md b/versions/4.0/zh/editor/extension/api/dialog.md new file mode 100644 index 0000000000..043f6a1e56 --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/dialog.md @@ -0,0 +1,191 @@ +# Dialog + +对话框弹窗,当在扩展进程使用的时候,弹出普通弹窗,如果在面板进程使用,则会弹出模态弹窗 + +## 通用接口说明 + +### MessageDialogOptions + +| Name | Type | Description | +| :------------------ | :--------------------- | :-------------------------- | +| `title?` | `string` | 标题,在部分可能会被隐藏 | +| `detail?` | `string` | 详细描述 | +| `buttons?` | `string array` | 在弹窗上新增一个或者多个按钮 | +| `default?` | `number` | 打开弹窗时默认选中的按钮 | +| `cancel?` | `number` | 当弹窗关闭时,默认触发的按钮 | +| `checkboxLabel?` | `number` | 在弹窗上附加一个可选项 | +| `checkboxChecked?` | `number` | 弹窗上附加的可选项默认值 | + +### SelectDialogOptions + +| Name | Type | Description | +| :------------ | :--------------------- | :-------------------------------- | +| `title?` | `string` | 标题,在部分可能会被隐藏 | +| `button?` | `string` | 弹窗按钮上的文字 | +| `path?` | `string` | 默认打开位置 | +| `type?` | `directory | file` | 是选择文件夹还是文件 | +| `multi?` | `boolean` | 是否可以多选 | +| `filters?` | `object array` | 过滤弹窗可选择的对象 | +| `extensions?` | `string array` | 设置可选择的文件扩展名,例如 'png' | + +### SaveDialogReturnValue + +| Name | Type | Description | +| :------------ | :--------------------- | :-------------------------------- | +| `canceled` | `boolean` | 用户是否取消 | +| `filePath` | `string` | 选择的文件路径 | + +### OpenDialogReturnValue + +| Name | Type | Description | +| :------------ | :--------------------- | :-------------------------------- | +| `canceled` | `boolean` | 用户是否取消 | +| `filePaths` | `string array` | 选择的文件路径 | + +#### filters 示例 + +```typescript +{ + filters: [ + { name: 'Images', extensions: ['jpg', 'png', 'gif'] }, + { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] }, + { name: 'Custom File Type', extensions: ['as'] }, + { name: 'All Files', extensions: ['*'] } + ] +} +``` + +## 函数 + +### info + +▸ **info**(message: `string`, options?: `MessageDialogOptions`): Promise<`MessageBoxReturnValue`\> + +普通信息弹窗 + +**请求参数** + +| Name | Type | Description | +| :--------- | :--------------------- | :----------------------------------- | +| `message` | `string` | 显示的消息 | +| `options?` | `MessageDialogOptions` | 信息弹窗可选参数 | + +**返回结果** + +Promise<`MessageBoxReturnValue`\> + +```typescript +const result = await Editor.Dialog.info('Dialog Message', { + buttons: ['confirm', 'cancel'], + title: 'Dialog Title', +}); +if (0 == result.response) { + // ... confirm event +} else { + // ... cancel event +} +``` + +### warn + +▸ **warn**(message: `string`, options?: `MessageDialogOptions`): Promise<`MessageBoxReturnValue`\> + +警告弹窗 + +**请求参数** + +| Name | Type | Description | +| :--------- | :--------------------- | :----------------------------------- | +| `message` | `string` | 警告信息 | +| `options?` | `MessageDialogOptions` | 警告弹窗可选参数 | + +**返回结果** + +Promise<`MessageBoxReturnValue`\> + +```typescript +await Editor.Dialog.warn('Warn Message'); +``` + +### error + +▸ **error**(message: `string`, options?: `MessageDialogOptions`): Promise<`MessageBoxReturnValue`\> + +错误弹窗 + +**请求参数** + +| Name | Type | Description | +| :--------- | :--------------------- | :----------------------------------- | +| `message` | `string` | 错误信息 | +| `options?` | `MessageDialogOptions` | 错误弹窗可选参数 | + +**返回结果** + +Promise<`MessageBoxReturnValue`\> + +```typescript +await Editor.Dialog.error('error content', { + title: 'options-title' +}); +``` + +### save + +▸ **save**(options?: `MessageDialogOptions`): Promise<`SaveDialogReturnValue`\> + +保存文件弹窗,保存文件时只能选择文件夹,且无法多选,相关参数不会生效 + +**请求参数** + +| Name | Type | Description | +| :--------- | :-------------------- | :----------------------------------- | +| `options?` | `SelectDialogOptions` | 保存文件窗口参数 | + +**返回结果** + +Promise<`SaveDialogReturnValue`\> + +```typescript +const result = await Editor.Dialog.save({ + path: Editor.Project.path, + title: 'Save Title', + filters: [ + { name: 'Package', extensions: ['zip'] }, + ], +}); +if (!result.filePath) { + return; +} +``` + +### select + +▸ **select**(options?: `SelectDialogOptions`): Promise<`OpenDialogReturnValue`\> + +选择文件弹窗 + +**请求参数** + +| Name | Type | Description | +| :--------- | :-------------------- | :----------------------------------- | +| `options?` | `SelectDialogOptions` | 选择弹窗参数 | + +**返回结果** + +Promise<`OpenDialogReturnValue`\> + +```typescript +const result = await Editor.Dialog.select({ + title: 'Select Title', + path: Editor.Project.path, + filters: [{ name: 'Package', extensions: ['zip'] }], +}); +if (result.filePaths && result.filePaths[0]) { + return result.filePaths[0]; +} else { + return ''; +} +``` + +更多说明请参考 [Electron 官方文档](https://www.electronjs.org/zh/docs/latest/api/dialog) \ No newline at end of file diff --git a/versions/4.0/zh/editor/extension/api/i18n.md b/versions/4.0/zh/editor/extension/api/i18n.md new file mode 100644 index 0000000000..064453fd3b --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/i18n.md @@ -0,0 +1,71 @@ +# I18n + +本地化翻译,通过在扩展中注册对应的数据,可以使用 i18n 进行翻译 + +## 接口说明 + +```typescript +export type I18nMap = { + [key: string]: I18nMap | string; +}; +``` + +## 函数 + +### getLanguage + +▸ **getLanguage**(): `any` + +获取当前的语言 + +**返回结果** + +| value | Type | Description | +| :--------- | :------- | :----------------- | +| `zh` | `string` | 中文 | +| `en` | `string` | English | + +```typescript +const langeage = Editor.I18n.getLanguage(); // "zh" +``` + +### select + +▸ **select**(language: `string`): `any` + +选择一种翻译语言 + +**请求参数** + +| Name | Type | Description | +| :--------- | :------- | :----------------- | +| `language` | `string` | 选择当前使用的语言 | + +```typescript +Editor.I18n.select('zh'); +``` + +### t + +▸ **t**(key: `string`, obj?: `{[key: string]: string}`): `any` + +传入 key,翻译成当前语言 +允许翻译变量 {a},传入的第二个参数 obj 内定义 a + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | :---------------------------------------------- | +| `key` | `string` | 用于翻译的 key 值 | +| `obj?` | `Object` | 翻译字段内如果有 {key} 等可以在这里传入替换字段 | + +```typescript +/* +* zh.js 文件定义的翻译映射数据 +* showUuid: '复制并打印 UUID' +* cancelSearchType: '取消搜索类型 {type},默认 搜索名称或 UUID', +*/ + +Editor.I18n.t('hierarchy.menu.showUuid'); // '复制并打印 UUID' +Editor.I18n.t('hierarchy.menu.cancelSearchType', { type: 'UUID' }); // '取消搜索类型 UUID,默认 搜索名称或 UUID' +``` diff --git a/versions/4.0/zh/editor/extension/api/logger.md b/versions/4.0/zh/editor/extension/api/logger.md new file mode 100644 index 0000000000..10b2fa93de --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/logger.md @@ -0,0 +1,25 @@ +# Logger + +Creator 日志管理 + +## 函数 + +### clear + +▸ **clear**(): `any` + +清空所有的日志 + +```typescript +Editor.Logger.clear(); +``` + +### query + +▸ **query**(): `any` + +查询所有日志 + +```typescript +const list = await Editor.Logger.query(); +``` diff --git a/versions/4.0/zh/editor/extension/api/message.md b/versions/4.0/zh/editor/extension/api/message.md new file mode 100644 index 0000000000..bca723c639 --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/message.md @@ -0,0 +1,102 @@ +# Message + +Creator 消息系统,消息在 Creator 非常重要,几乎所有的操作和数据传递都是通过消息进行的。 + +具体扩展支持调用的消息可以在菜单栏 [`开发者->消息列表`](../messages.md##查看消息列表) 查看。 + +## 函数 + +### send + +▸ **send**(name: `string`, message: `string`, ...args: `any[]`): `void` + +发送一个消息,没有返回 + +**请求参数** + +| Name | Type | Description | +| :-------- | :---------------- | :------------- | +| `name` | `string` | 目标扩展的名字 | +| `message` | `string` | 触发消息的名字 | +| `...args` | `any[]` | 消息需要的参数 | + +```typescript +Editor.Message.send('builder', 'open-devtools'); +``` + +### request + +▸ **request**(name: `string`, message: `string`, ...args: `any[]`): Promise<`any`> + +发送一个消息,并等待返回 + +**请求参数** + +| Name | Type | Description | +| :-------- | :------------------ | :------------- | +| `name` | `string` | 目标扩展的名字 | +| `message` | `string` | 触发消息的名字 | +| `...args` | `any[]` | 消息需要的参数 | + +**返回结果** + +Promise<`any`> + +```typescript +const sceneDirty = await Editor.Message.request('scene', 'query-dirty'); // false +``` + +### broadcast + +▸ **broadcast**(message: `string`, ...args: `any[]`): `void` + +广播一个消息 + +我们约定每个扩展自己发出的广播消息,将以 `[扩展名]:xxx` 的形式命名,希望后续其他各个扩展也可以遵守这样的规范 + +**请求参数** + +| Name | Type | Description | +| :-------- | :------- | :------------- | +| `message` | `string` | 消息的名字 | +| `...args` | `any[]` | 消息附加的参数 | + +```typescript +Editor.Message.broadcast('console:update-log-level', []); +``` + +### addBroadcastListener(废弃) + +▸ **addBroadcastListener**(message: `string`, func: `Function`): `any` + +**废弃警告,请通过扩展监听广播消息** +新增一个广播消息监听器,不监听的时候,需要主动取消监听 + +**请求参数** + +| Name | Type | Description | +| :-------- | :--------- | :---------- | +| `message` | `string` | 消息名 | +| `func` | `Function` | 处理函数 | + +```typescript +Editor.Message.addBroadcastListener('console:logsUpdate', () => {}); +``` + +### removeBroadcastListener(废弃) + +▸ **removeBroadcastListener**(message: `string`, func: `Function`): `any` + +**废弃警告,请通过扩展监听广播消息** +新增一个广播消息监听器 + +**请求参数** + +| Name | Type | Description | +| :-------- | :--------- | :---------- | +| `message` | `string` | 消息名 | +| `func` | `Function` | 处理函数 | + +```typescript +Editor.Message.removeBroadcastListener('console:logsUpdate', () => {}); +``` diff --git a/versions/4.0/zh/editor/extension/api/network.md b/versions/4.0/zh/editor/extension/api/network.md new file mode 100644 index 0000000000..4f727f4fb2 --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/network.md @@ -0,0 +1,122 @@ +# Network + +Creator 网络工具函数 + +## 函数 + +### get + +▸ **get**(url: `string`, data?: `Object`): Promise<`Buffer`\> + +Get 方式请求某个服务器数据 + +**请求参数** + +| Name | Type | Description | +| :------ | :------- | -------------------- | +| `url` | `string` | 请求的 url | +| `data?` | `Object` | 请求时带上的数据 | + +**返回结果** + +Promise<`Buffer`\> + +```typescript +network.get(RUNTIME_REQUEST_URL).then((ret: any) => { + ret = ret.toString(); +}).catch((e: any) => { + console.error('error', e); +}); +``` + +### post + +▸ **post**(url: `string`, data?: `Object`): Promise<`Buffer`\> + +Post 方式请求某个服务器数据 + +**请求参数** + +| Name | Type | Description | +| :------ | :------- | -------------------- | +| `url` | `string` | 请求的 url | +| `data?` | `Object` | 请求时带上的数据 | + +**返回结果** + +Promise<`Buffer`\> + +```typescript +let res: Buffer = await Editor.Network.post('https://creator-api.cocos.com/api/session/token', { + ip: '127.0.0.1', + client_type: 1 +}); +``` + +### portIsOccupied + +▸ **portIsOccupied**(port: `number`): Promise<`boolean`\> + +检查一个端口是否被占用 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `port` | `number` | 端口号 | + +**返回结果** + +Promise<`boolean`\> + +```typescript +const isOccupied = await Editor.Network.portIsOccupied(8000); // false +``` + +### queryIPList + +▸ **queryIPList**(): `string[]` + +查询当前电脑的 ip 列表 + +**返回结果** + +`string[]` + +```typescript +const ipList = Editor.Network.queryIPList(); // ["127.0.0.1", "192.168.52.154"] +``` + +### testConnectServer + +▸ **testConnectServer**(): Promise<`boolean`\> + +测试是否可以联通 passport.cocos.com 服务器 + +**返回结果** + +Promise<`boolean`\> + +```typescript +const res = await Editor.Network.testConnectServer(); // true +``` + +### testHost + +▸ **testHost**(ip: `string`): Promise<`boolean`\> + +测试是否可以联通某一台主机 + +**请求参数** + +| Name | Type | Description | +| :--- | :------- | ----------- | +| `ip` | `string` | ip 地址 | + +**返回结果** + +Promise<`boolean`\> + +```typescript +const res = await Editor.Network.testHost('127.0.0.1'); // true +``` diff --git a/versions/4.0/zh/editor/extension/api/package.md b/versions/4.0/zh/editor/extension/api/package.md new file mode 100644 index 0000000000..c16caaa34b --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/package.md @@ -0,0 +1,99 @@ +# Package + +扩展管理器 + +## 接口说明 + +```typescript +interface GetPackageOptions { + name?: string; + debug?: boolean; + path?: string; + enable?: boolean; + invalid?: boolean; +} + +interface EditorInterfacePackageInfo { + debug: boolean; + invalid: boolean; + enable: boolean; + name: string; + path: string; + version: string; + info: PackageJson; +} + +type PathType = 'home' | 'data' | 'temp'; +``` + +## 函数 + +### disable + +▸ **disable**(path: `string`): `any` + +关闭一个扩展 + +**请求参数** + +| Name | Type | Description | +| :-------- | :------- | ---------------- | +| `path` | `string` | 扩展所在路径 | +| `options` | `any` | 关闭时带上的配置 | + +```typescript +Editor.Package.disable('D:\\Program\\CocosEditor\\Creator\\3.4.0\\resources\\app.asar\\builtin\\assets', {}); +``` + +### enable + +▸ **enable**(path: `string`): `any` + +启动一个扩展 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ---------------- | +| `path` | `string` | 扩展所在路径 | + +```typescript +Editor.Package.enable('D:\\Program\\CocosEditor\\Creator\\3.4.0\\resources\\app.asar\\builtin\\assets', {}); +``` + +### getPackages + +▸ **getPackages**(options?: `GetPackageOptions`): `EditorInterfacePackageInfo[]` + +查询扩展列表 + +**请求参数** + +| Name | Type | Description | +| :--------- | :------------------ | ----------- | +| `options?` | `GetPackageOptions` | 查询条件 | + +**返回结果** + +`EditorInterfacePackageInfo[]` + +```typescript +const pkgs = Editor.Package.getPackages({ enable: true }); +``` + +### getPath + +▸ **getPath**(extensionName: `string`, type?: `PathType`): `any` + +获取一个扩展的几个预制目录地址 + +**请求参数** + +| Name | Type | Description | +| :-------------- | :--------- | :------------ | +| `extensionName` | `string` | 扩展的名字 | +| `type?` | `PathType` | 地址类型(temp 临时目录,data 需要同步的数据目录,不传则返回现在打开的扩展路径) | + +```typescript +const path = Editor.Package.getPath('menu'); // "D:\\Program\\CocosEditor\\Creator\\3.4.0\\resources\\app.asar\\builtin\\menu" +``` diff --git a/versions/4.0/zh/editor/extension/api/panel.md b/versions/4.0/zh/editor/extension/api/panel.md new file mode 100644 index 0000000000..156cac55fa --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/panel.md @@ -0,0 +1,105 @@ +# Panel + +面板管理器 + +## 函数 + +### open + +▸ **open**(name: `string`, ...args: `any[]`): `any` + +传入面板名字,打开一个面板 + +**请求参数** + +| Name | Type | Description | +| :-------- | :------- | ------------------------ | +| `name` | `string` | 面板名称 | +| `...args` | `any`[] | 打开面板时传递的参数 | + +```typescript +Editor.Panel.open('console'); +``` + +### close + +▸ **close**(name: `string`): `any` + +传入面板名字,关闭同名的面板 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `name` | `string` | 面板名称 | + +```typescript +Editor.Panel.close('package-asset.import'); +``` + +### focus + +▸ **focus**(name: `string`): `any` + +将焦点传递给找到的第一个同名面板 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `name` | `string` | 面板名称 | + +```typescript +Editor.Panel.focus('assets'); +``` + +### has + +▸ **has**(name: `string`): Promise<`boolean`\> + +检查面板是否已经打开 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `name` | `string` | 面板名称 | + +**返回结果** + +Promise<`boolean`\> + +```typescript +const res = await Editor.Panel.has('package-asset.import'); +``` + +### define + +▸ **define**(options: `Options`): `PanelObject` + +定义一个面板,如果我们用 typescript 书写面板内容,ready 等生命周期函数内无法解析出正确的 this 对象,所以 Creator 里提供了一个 define 函数。 + +这个函数传入一个 PanelObject,返回一个 PanelObject,并不进行逻辑处理。但在这个函数传入的 PanelObject 上,能够正常识别出 this 对象。 + +**请求参数** + +| Name | Type | Description | +| :-------- | :------------------------------- | ----------- | +| `options` | `Options`<`Selector`, `M`, `U`\> | 面板的配置 | + +```typescript +module.exports = Editor.Panel.define({ + template: '
', + $: { + app: '#app', + }, + methods: { + init() { }, + }, + async ready() { + new App({ + el: this.$.app, + }); + }, +}); +``` diff --git a/versions/4.0/zh/editor/extension/api/profile.md b/versions/4.0/zh/editor/extension/api/profile.md new file mode 100644 index 0000000000..cb3e3d5264 --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/profile.md @@ -0,0 +1,268 @@ +# Profile + +配置 + +## 接口说明 + +```typescript +type preferencesProtocol = 'default' | 'global' | 'local'; + +type projectProtocol = 'default' | 'project'; +``` + +## 函数 + +### getConfig + +▸ **getConfig**(name: `string`, key?: `string`, type?: `preferencesProtocol`): Promise<`any`\> + +读取扩展配置 + +**请求参数** + +| Name | Type | Description | +| :------ | :-------------------- | :--------------- | +| `name` | `string` | 扩展名 | +| `key?` | `string` | 配置路径 | +| `type?` | `preferencesProtocol` | 配置的类型,选填 | + +**返回结果** + +Promise<`any`\> + +```typescript +const value = await Editor.Profile.getConfig('asset-db', 'autoScan'); +``` + +### setConfig + +▸ **setConfig**(name: `string`, key: `string`, value: `any`, type?: `preferencesProtocol`): Promise<`void`\> + +设置扩展配置 + +**请求参数** + +| Name | Type | Description | +| :------ | :-------------------- | :--------------- | +| `name` | `string` | 扩展名 | +| `key` | `string` | 配置路径 | +| `value` | `any` | 配置的值 | +| `type?` | `preferencesProtocol` | 配置的类型,选填 | + +**返回结果** + +Promise<`void`\> + +```typescript +await Editor.Profile.setConfig('package-asset', 'import-path', dirname(result.filePaths[0])); +await Editor.Profile.setConfig('reference-image', 'show', true); +``` + +### removeConfig + +▸ **removeConfig**(name: `string`, key: `string`, type?: `preferencesProtocol`): Promise<`void`\> + +删除某个扩展配置 + +**请求参数** + +| Name | Type | Description | +| :------ | :-------------------- | :--------------- | +| `name` | `string` | 扩展名 | +| `key` | `string` | 配置路径 | +| `type?` | `preferencesProtocol` | 配置的类型,选填 | + +**返回结果** + +Promise<`void`\> + +```typescript +await Editor.Profile.removeConfig('device', 'enable', 'global'); +``` + +### getProject + +▸ **getProject**(`name: `string`, key?: `string`, type?: `projectProtocol`): Promise<`any`\> + +读取扩展内的项目配置 + +**请求参数** + +| Name | Type | Description | +| :------ | :---------------- | :--------------- | +| `name` | `string` | 扩展名 | +| `key?` | `string` | 配置路径 | +| `type?` | `projectProtocol` | 配置的类型,选填 | + +**返回结果** + +Promise<`any`\> + +```typescript +const engineModules = await Editor.Profile.getProject('engine', 'modules.includeModules'); +``` + +### setProject + +▸ **setProject**(name: `string`, key: `string`, value: `any`, type?: `preferencesProtocol`): Promise<`void`\> + +设置扩展内的项目配置 + +**请求参数** + +| Name | Type | Description | +| :------ | :---------------- | :--------------- | +| `name` | `string` | 扩展名 | +| `key` | `string` | 配置路径 | +| `value` | `any` | 配置的值 | +| `type?` | `projectProtocol` | 配置的类型,选填 | + +**返回结果** + +Promise<`void`\> + +```typescript +await Editor.Profile.setProject('node-library', 'custom', {}); +``` + +### removeProject + +▸ **removeProject**(name: `string`, key: `string`, type?: `projectProtocol`): Promise<`void`\> + +删除扩展内的项目配置 + +**请求参数** + +| Name | Type | Description | +| :------ | :---------------- | :--------------- | +| `name` | `string` | 扩展名 | +| `key` | `string` | 配置路径 | +| `type?` | `projectProtocol` | 配置的类型,选填 | + +**返回结果** + +Promise<`void`\> + +```typescript +await Editor.Profile.removeProject('engine', 'modules.includeModules'); +``` + +### getTemp + +▸ **getTemp**(name: `string`, key?: `string`): Promise<`any`\> + +读取扩展配置 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | :-------------- | +| `name` | `string` | 扩展名 | +| `key?` | `string` | 配置路径,选填 | + +**返回结果** + +Promise<`any`\> + +```typescript +const state = await Editor.Profile.getTemp('assets', 'state'); +``` + +### setTemp + +▸ **setTemp**(name: `string`, key: `string`, value: `any`): Promise<`void`\> + +设置扩展配置 + +**请求参数** + +| Name | Type | Description | +| :------ | :------- | :---------- | +| `name` | `string` | 扩展名 | +| `key` | `string` | 配置路径 | +| `value` | `any` | 配置的值 | + +**返回结果** + +Promise<`void`\> + +```typescript +Editor.Profile.setTemp('assets', 'state', {}); +``` + +### removeTemp + +▸ **removeTemp**(name: `string`, key: `string`): Promise<`void`\> + +删除某个扩展配置 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | :---------- | +| `name` | `string` | 扩展名 | +| `key` | `string` | 配置路径 | + +**返回结果** + +Promise<`void`\> + +```typescript +await Editor.Profile.removeTemp('assets', 'state'); +``` + +### migrateProject + +▸ **migrateProject**(pkgName: `string`, profileVersion: `string`, profileData: `any`): `void` + +迁移扩展某个版本的项目配置数据到编辑器最新版本 + +**请求参数** + +| Name | Type | Description | +| :--------------- | :------- | --------------------- | +| `pkgName` | `string` | 扩展名 | +| `profileVersion` | `string` | 要迁移的扩展版本号 | +| `profileData` | `any` | 迁移的数据 | + +```typescript +await Editor.Profile.migrateProject('builder', '1.2.1', buildJson); +``` + + +### migrateGlobal + +▸ **migrateGlobal**(pkgName: `string`, profileVersion: `string`, profileData: `any`): `void` + +迁移扩展某个版本的全局配置数据到编辑器最新版本 + +**请求参数** + +| Name | Type | Description | +| :--------------- | :------- | --------------------- | +| `pkgName` | `string` | 扩展名 | +| `profileVersion` | `string` | 要迁移的扩展版本号 | +| `profileData` | `any` | 迁移的数据 | + +```typescript +// const buildJson = { xxx }; +await Editor.Profile.migrateGlobal('builder', '1.2.1', buildJson); +``` + +### migrateLocal + +▸ **migrateLocal**(pkgName: `string`, profileVersion: `string`, profileData: `any`): `void` + +迁移扩展某个版本的本地配置数据到编辑器最新版本 + +**请求参数** + +| Name | Type | Description | +| :--------------- | :------- | --------------------- | +| `pkgName` | `string` | 扩展名 | +| `profileVersion` | `string` | 要迁移的扩展版本号 | +| `profileData` | `any` | 迁移的数据 | + +```typescript +await Editor.Profile.migrateLocal('builder', '1.2.1', buildJson); +``` \ No newline at end of file diff --git a/versions/4.0/zh/editor/extension/api/project.md b/versions/4.0/zh/editor/extension/api/project.md new file mode 100644 index 0000000000..aaf8413cfe --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/project.md @@ -0,0 +1,45 @@ +# Project + +当前打开的项目基本信息 + +## 变量 + +### name + +• **name**: `string` + +当前项目名称(取自 package.json) + +```typescript +const projectName = Editor.Project.name; // "Hello World 3.4.0" +``` + +### path + +• **path**: `string` + +当前项目路径 + +```typescript +const projectPath = Editor.Project.path; // "E:\\workSpace\\Hello World 3.4.0" +``` + +### tmpDir + +• **tmpDir**: `string` + +当前项目临时文件夹 + +```typescript +const projectTmpDir = Editor.Project.tmpDir; // "E:\\workSpace\\Hello World 3.4.0\\temp" +``` + +### uuid + +• **uuid**: `string` + +当前项目 uuid + +```typescript +const projectUUID = Editor.Project.uuid; // "7aa7c089-8e53-4611-8689-98b69ab28e22" +``` diff --git a/versions/4.0/zh/editor/extension/api/selection.md b/versions/4.0/zh/editor/extension/api/selection.md new file mode 100644 index 0000000000..810bef841a --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/selection.md @@ -0,0 +1,136 @@ +# Selection + +Creator 选择管理器,编辑器内所有的选中物体,都通过这个管理器进行管理 + +选中物体需要记录 "类型" 和 "ID" 两个属性 + +## 函数 + +### clear + +▸ **clear**(type: `string`): `any` + +清空一个类型的所有选中元素 + +如果有元素被取消选中,会发送 selection:unselect 广播消息 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `type` | `string` | 选中的类型 | + +```typescript +Editor.Selection.clear('asset'); +``` + +### hover + +▸ **hover**(type: `string`, uuid?: `string`): `any` + +悬停触碰了某个元素 + +会发出 selection:hover 的广播消息 + +**请求参数** + +| Name | Type | Description | +| :------ | :------- | ----------- | +| `type` | `string` | 选中的类型 | +| `uuid?` | `string` | 元素的 uuid | + +```typescript +Editor.Selection.hover('asset', '7bf9df40-4bc9-4e25-8cb0-9a500f949102'); +``` + +### select + +▸ **select**(type: `string`, uuid:`string` | `string[]`): `any` + +选中一个或者一组元素 + +当没有选中的元素变成选中状态时,会发出 selection:select 的广播消息 + +**请求参数** + +| Name | Type | Description | +| :----- | :--------------------- | ----------- | +| `type` | `string` | 选中的类型 | +| `uuid` | `string | string[]` | 元素的 uuid | + +```typescript +Editor.Selection.select('asset', '7bf9df40-4bc9-4e25-8cb0-9a500f949102'); +``` + +### unselect + +▸ **unselect**(type: `string`, uuid:`string` | `string[]`): `any` + +取消一个或者一组元素的选中状态 + +当元素被取消选中的时候,会发送 selection:unslect 广播消息 + +**请求参数** + +| Name | Type | Description | +| :----- | :--------------------- | ----------- | +| `type` | `string` | 选中的类型 | +| `uuid` | `string | string[]` | 元素的 uuid | + +```typescript +Editor.Selection.unselect('asset', '7bf9df40-4bc9-4e25-8cb0-9a500f949102'); +``` + +### getSelected + +▸ **getSelected**(type: `string`): `string[]` + +获取一个类型选中的所有元素数组 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `type` | `string` | 选中的类型 | + +**返回结果** + +`string[]` + +```typescript +const uuids = Editor.Selection.getSelected('asset'); // ["b0a4abb1-db32-49c3-9e09-a45b922a2024"] +``` + +### getLastSelected + +▸ **getLastSelected**(type: `string`): `string` + +获取某个类型内,最后选中的元素 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `type` | `string` | 选中的类型 | + +**返回结果** + +`string` + +```typescript +const elem = Editor.Selection.getLastSelected('asset'); // "b0a4abb1-db32-49c3-9e09-a45b922a2024" +``` + +### getLastSelectedType + +▸ **getLastSelectedType**(): `string` + +获取最后选中的元素的类型 + +**返回结果** + +`string` + +```typescript +const type = Editor.Selection.getLastSelectedType(); // "asset" +``` diff --git a/versions/4.0/zh/editor/extension/api/utils.md b/versions/4.0/zh/editor/extension/api/utils.md new file mode 100644 index 0000000000..070f5f4b24 --- /dev/null +++ b/versions/4.0/zh/editor/extension/api/utils.md @@ -0,0 +1,615 @@ +# Utils + +Creator 内置的一些工具函数 + +## File + +### 接口说明 + +```typescript +interface UnzipOptions { + peel?: boolean; +} +``` + +### 函数 + +#### copy + +▸ **copy**(source: `string`, target: `string`): `void` + +复制一个文件到另一个位置 + +**请求参数** + +| Name | Type | Description | +| :------- | :------- | ----------- | +| `source` | `string` | | +| `target` | `string` | | + +```typescript +Editor.Utils.File.copy('C:\\CocosCreatorWorkSpace\\HelloWorld', 'E:\\CocosCreatorWorkSpace\\HelloWorld'); +``` + +#### getName + +▸ **getName**(file: `string`): `string` + +初始化一个可用的文件名,返回可用名称的文件路径 +遇到重名时会自动递增直到获取到一个不会冲突的可用文件地址 + +Parameters + +| Name | Type | Description | +| :----- | :------- | :----------- | +| `file` | `string` | 初始文件路径 | + +**返回结果** + +`string` + +```typescript +const newFileName = Editor.Utils.File.getName('E:\\CocosCreatorWorkSpace\\HelloWorld'); +``` + +#### unzip + +▸ **unzip**(zip: `string`, target: `string`, options?: `UnzipOptions`): Promise<`void`\> + +解压文件夹 + +**请求参数** + +| Name | Type | Description | +| :--------- | :------------- | ----------- | +| `zip` | `string` | | +| `target` | `string` | | +| `options?` | `UnzipOptions` | | + +**返回结果** + +Promise<`void`\> + +```typescript +await Editor.Utils.File.unzip('E:\\repositories\\utils.zip', 'E:\\CocosCreatorWorkSpace\\HelloWorld'); +``` + +## Math + +### 函数 + +#### add + +▸ **add**(arg1: `string | number`, arg2: `string | number`): `number` + +加法函数 +入参:函数内部转化时会先转字符串再转数值,因而传入字符串或 number 均可 +返回值:arg1 加上 arg2 的精确结果 + +**请求参数** + +| Name | Type | Description | +| :----- | :------------------- | ----------- | +| `arg1` | `string` \| `number` | | +| `arg2` | `string` \| `number` | | + +**返回结果** + +`number` + +```typescript +const res = Editor.Utils.Math.add('123', 12.12); // 135.12 +``` + +#### clamp + +▸ **clamp**(val: `number`, min: `number`, max: `number`): `any` + +取给定边界范围的值 + +**请求参数** + +| Name | Type | Description | +| :---- | :------- | ----------- | +| `val` | `number` | | +| `min` | `number` | | +| `max` | `number` | | + +```typescript +const res = Editor.Utils.Math.clamp(100, 1, 99); // 99 +``` + +#### clamp01 + +▸ **clamp01**(val: `number`): `number` + +取给 0 - 1 范围内的值 + +**请求参数** + +| Name | Type | Description | +| :---- | :------- | ----------- | +| `val` | `number` | | + +**返回结果** + +`number` + +```typescript +const res = Editor.Utils.Math.clamp01(0.5); // 0.5 +``` + +#### sub + +▸ **sub**(arg1: `string | number`, arg2: `string | number`): `number` + +减法函数 +入参:函数内部转化时会先转字符串再转数值,因而传入字符串或number均可 +返回值:arg1 减 arg2的精确结果 + +**请求参数** + +| Name | Type | Description | +| :----- | :------------------- | ----------- | +| `arg1` | `string` \| `number` | | +| `arg2` | `string` \| `number` | | + +**返回结果** + +`number` + +```typescript +const res = Editor.Utils.Math.sub('123', 12.12); // 110.88 +``` + +#### toFixed + +▸ **toFixed**(val: `number`, num: `number`): `number` + +保留小数点 + +**请求参数** + +| Name | Type | Description | +| :---- | :------- | ----------- | +| `val` | `number` | | +| `num` | `number` | | + +**返回结果** + +`number` + +```typescript +const res = Editor.Utils.Math.toFixed(12.1294, 2); // 12.13 +``` + +## Path + +### 变量 + +#### delimiter + +• **delimiter**: `";" | ":"` + +#### sep + +• **sep**: `"\\" | "/"` + +### 函数 + +#### basename + +▸ **basename**(p: `string`, ext?: `string`): `string` + +返回路径的最后一部分 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ---------------- | +| `p` | `string` | 路径 | +| `ext?` | `string` | 文件扩展名,可选 | + +**返回结果** + +`string` + +```typescript +const fileName = Editor.Utils.Path.basename('E:\\CocosCreatorWorkSpace\\HelloWorld\\package.json', '.json'); // package +``` + +#### basenameNoExt + +▸ **basenameNoExt**(path: `string`): `string` + +返回一个不含扩展名的文件名 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `path` | `string` | | + +**返回结果** + +`string` + +```typescript +const fileName = Editor.Utils.Path.basenameNoExt('E:\\CocosCreatorWorkSpace\\HelloWorld\\package.json'); // package +``` + +#### contains + +▸ **contains**(pathA: `string`, pathB: `string`): `boolean` + +判断路径 pathA 是否包含 pathB + +**请求参数** + +| Name | Type | Description | +| :------ | :------- | ----------- | +| `pathA` | `string` | | +| `pathB` | `string` | | + +**返回结果** + +`boolean` + +```typescript +const res = Editor.Utils.Path.contains('foo/bar', 'foo/bar/foobar'); // true +const res = Editor.Utils.Path.contains('foo/bar', 'foo/bar'); // true +const res = Editor.Utils.Path.contains('foo/bar/foobar', 'foo/bar'); // false +const res = Editor.Utils.Path.contains('foo/bar/foobar', 'foobar/bar/foo'); // false +``` + +#### dirname + +▸ **dirname**(p: `string`): `string` + +返回路径的目录名 + +**请求参数** + +| Name | Type | Description | +| :--- | :------- | ----------- | +| `p` | `string` | | + +**返回结果** + +`string` + +```typescript +const dirname = Editor.Utils.Path.dirname('E:\\CocosCreatorWorkSpace\\HelloWorld\\package.json'); +// "E:\\CocosCreatorWorkSpace\\HelloWorld" +``` + +#### extname + +▸ **extname**(p: `string`): `string` + +返回路径的扩展名 + +**请求参数** + +| Name | Type | Description | +| :--- | :------- | ----------- | +| `p` | `string` | | + +**返回结果** + +`string` + +```typescript +const extname = Editor.Utils.Path.extname('E:\\CocosCreatorWorkSpace\\HelloWorld\\package.json'); // .json +``` + +#### format + +▸ **format**(p: `FormatInputPathObject`): `string` + +根据对象数据返回路径字符串 + +**请求参数** + +| Name | Type | Description | +| :--- | :---------------------- | ----------- | +| `p` | `FormatInputPathObject` | | + +**返回结果** + +`string` + +```typescript +const path = Editor.Utils.Path.format({ + root: '/ignored', + dir: '/home/user/dir', + base: 'file.txt' +}); // "/home/user/dir\\file.txt" +``` + +#### isAbsolute + +▸ **isAbsolute**(p: `string`): `boolean` + +判断路径是否为绝对路径 + +**请求参数** + +| Name | Type | Description | +| :--- | :------- | ----------- | +| `p` | `string` | | + +**返回结果** + +`boolean` + +```typescript +const res = Editor.Utils.Path.isAbsolute('E:\\CocosCreatorWorkSpace\\HelloWorld\\package.json'); // true +``` + +#### join + +▸ **join**(...paths: `string[]`): `string` + +使用特定于平台的分隔符作为定界符将所有给定的 `path` 片段连接在一起,然后规范化生成的路径 + +**请求参数** + +| Name | Type | Description | +| :--------- | :--------- | ----------- | +| `...paths` | `string[]` | | + +**返回结果** + +`string` + +```typescript +const path = Editor.Utils.Path.join('/foo', 'bar', 'abc/def', 'g'); // "\\foo\\bar\\abc\\def\\g" +``` + +#### normalize + +▸ **normalize**(path: `string`): `string` + +格式化路径 +如果是 Windows 平台,需要将盘符转成小写进行判断 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `path` | `string` | | + +**返回结果** + +`string` + +```typescript +const path = Editor.Utils.Path.normalize('/foo/bar//abc/def/g'); // "\\foo\\bar\\abc\\def\\g" +``` + +#### parse + +▸ **parse**(p: `string`): `ParsedPath` + +返回一个对象,表示路径的重要信息 + +**请求参数** + +| Name | Type | Description | +| :--- | :------- | ----------- | +| `p` | `string` | | + +**返回结果** + +`ParsedPath` + +```typescript +const res = Editor.Utils.Path.parse('/foo/bar//abc/def/g.txt'); +``` + +#### relative + +▸ **relative**(from: `string`, to: `string`): `string` + +根据当前工作目录返回从 `from` 到 `to` 的相对路径 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `from` | `string` | | +| `to` | `string` | | + +**返回结果** + +`string` + +```typescript +const relativePath = Editor.Utils.Path.relative('C:\\program\\test\\aaa', 'C:\\program\\impl\\bbb'); // "..\\..\\impl\\bbb" +``` + +#### resolve + +▸ **resolve**(...pathSegments: `string[]`): `string` + +将路径或路径片段的序列解析为绝对路径 + +**请求参数** + +| Name | Type | Description | +| :---------------- | :--------- | ----------- | +| `...pathSegments` | `string[]` | | + +**返回结果** + +`string` + +```typescript +const path = Editor.Utils.Path.resolve('/foo/bar', './abc'); // "D:\\foo\\bar\\abc" +``` + +#### slash + +▸ **slash**(path: `string`): `string` + +将 \ 统一换成 / + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `path` | `string` | | + +**返回结果** + +`string` + +```typescript +const path = Editor.Utils.Path.slash('\\foo\\bar'); // "/foo/bar" +``` + +#### stripExt + +▸ **stripExt**(path: `string`): `string` + +删除一个路径的扩展名 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `path` | `string` | | + +**返回结果** + +`string` + +```typescript +const path = Editor.Utils.Path.stripExt('E:\\HelloWorld\\package.json'); // "E:\\HelloWorld\\package" +``` + +#### stripSep + +▸ **stripSep**(path: `string`): `string` + +去除路径最后的斜杆,返回一个不带斜杆的路径 + +**请求参数** + +| Name | Type | Description | +| :----- | :------- | ----------- | +| `path` | `string` | | + +**返回结果** + +`string` + +```typescript +const path = Editor.Utils.Path.stripSep('E:\\HelloWorld\\package.json\\'); // "E:\\HelloWorld\\package.json" +``` + +## Url + +### 函数 + +#### getDocUrl + +▸ **getDocUrl**(relativeUrl: `string`, type?: `"manual" | "api"`): `string` + +快捷获取文档路径 + +**请求参数** + +| Name | Type | Description | +| :------------ | :------------------------ | ----------- | +| `relativeUrl` | `string` | | +| `type?` | `"manual" | "api"` | | + +**返回结果** + +`string` + +```typescript +const url = Editor.Utils.Url.getDocUrl('publish/publish-bytedance.html'); +// "https://docs.cocos.com/creator/3.4/manual/zh/publish/publish-bytedance.html" +``` + +## UUID + +### 函数 + +#### compressUUID + +▸ **compressUUID**(uuid: `string`, min: `boolean`): `string` + +压缩 UUID + +**请求参数** + +| Name | Type | Description | +| :----- | :-------- | ----------- | +| `uuid` | `string` | | +| `min` | `boolean` | | + +**返回结果** + +`string` + +```typescript +const uuid = Editor.Utils.UUID.compressUUID('7bf9df40-4bc9-4e25-8cb0-9a500f949102'); // "7bf9d9AS8lOJYywmlAPlJEC" +``` + +#### decompressUUID + +▸ **decompressUUID**(str: `string`): `string` + +解压 UUID + +**请求参数** + +| Name | Type | Description | +| :---- | :------- | ----------- | +| `str` | `string` | | + +**返回结果** + +`string` + +```typescript +const uuid = Editor.Utils.UUID.decompressUUID('7bf9d9AS8lOJYywmlAPlJEC'); // "7bf9df40-4bc9-4e25-8cb0-9a500f949102" +``` + +#### generate + +▸ **generate**(): `string` + +生成一个新的 uuid + +**返回结果** + +`string` + +```typescript +const uuid = Editor.Utils.UUID.generate(); +``` + +#### isUUID + +▸ **isUUID**(str: `string`): `string` + +检查输入字符串是否是 UUID + +**请求参数** + +| Name | Type | Description | +| :---- | :------- | ----------- | +| `str` | `string` | | + +**返回结果** + +`string` + +```typescript +const isUUID = Editor.Utils.UUID.isUUID('7bf9d9AS8lOJYywmlAPlJEC'); // true +``` diff --git a/versions/4.0/zh/editor/extension/basic.md b/versions/4.0/zh/editor/extension/basic.md new file mode 100644 index 0000000000..4f460bf812 --- /dev/null +++ b/versions/4.0/zh/editor/extension/basic.md @@ -0,0 +1,11 @@ +# 扩展系统详解 + +编辑器内提供了许多功能,这里主要介绍一些书写扩展时常用的功能。 + +- [基础结构](./package.md) +- [扩展包定义](./define.md) +- [消息系统](./messages.md) +- [多语言系统(i18n)](./i18n.md) +- [配置系统](./profile.md) +- [面板系统](./panel.md) +- [UI 组件](./ui.md) diff --git a/versions/4.0/zh/editor/extension/contributions-database.md b/versions/4.0/zh/editor/extension/contributions-database.md new file mode 100644 index 0000000000..4dc7d300a3 --- /dev/null +++ b/versions/4.0/zh/editor/extension/contributions-database.md @@ -0,0 +1,86 @@ +# 自定义资源数据库 + +所有项目内的资源文件都是通过资源数据库进行管理,其中项目内的 `assets` 目录存放的是当前项目的资源,引擎仓库里 `editor/assets` 里存放的是引擎内置的资源(如:常见的图片、脚本等)。 + +当我们在扩展内使用了资源时,需要将扩展内的资源文件夹注册到资源数据库里,并在扩展发布时将资源随着扩展一起发布。 + +通过本文我们将学会如何注册一个资源文件夹,并在脚本里使用资源。 + +## 注册配置 + +资源注册需要在 `contributions` 中使用 `asset-db` 字段进行配置,如下所示: + +```json5 +{ + "name": "test-package", + "contributions": { + "asset-db": { + "mount": { + "path": "./assets", + "readonly": true + } + } + } +} +``` + +上面的示例中,我们把扩展 `test-package` 根目录下的 `assets` 文件夹注册到了资源数据库中。 + +## 脚本资源 + +在 `test-package/assets/` 目录下创建一个脚本 `foo.ts`,内容如下: + +```typescript +/// foo.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Foo') +export class Foo extends Component { + start () { + console.log('foo'); + } +} +``` + +为了使用 cc 的定义,我们需要拷贝 `{项目目录}\temp\declarations` 的定义文件到扩展根目录下。 + +由于 `foo.ts` 只是作资源使用,不属于扩展源码,所以我们需要在 `tsconfig.json` 中加入 `exclude` 配置进行排除,否则会出现编译错误。 + +```json5 +{ + "compilerOptions": { + ... + }, + "exclude": ["./assets"] +} +``` + +> **注意**:扩展中的脚本资源可以在 Cocos Creator 工程中编写并测试完成后,再复制到扩展的 `assets` 目录。 + +## 其他资源 + +图片、文本、字体等资源直接放入 `assets` 目录下即可。 + +## 使用扩展中的资源 + +刷新扩展,可以在 Cocos Creator 编辑器的 **资源管理器** 窗口中看到新增了一个 `test-package` 资源包,如下图所示: + +![extension-database](./image/extension-database.png) + +## 拖拽引用 + +若要以拖拽到组件方式引用包内的资源,使用方式与 `assets` 和 `internal` 中的资源一致。 + +## import 脚本 + +若要在项目中引用脚本,只需要从资源目录库引用即可,如下所示: + +```typescript +/// bar.ts +import { Foo } from 'db://test-package/foo'; +``` + +可以不用刻意关注某个类是否来自某个扩展包,TypeScript 开发环境的自动补齐功能会提示 **import** 目录的,无需担心。 + +> **注意**:扩展资源包中的类名应当保持全局唯一,否则会造成冲突,实际开发中尽量添加一个适合的前缀(如 `test-package` 可简称为 `TP`,资源包中所有类统一加上 `TP` 前缀,变成 `TPFoo` )。 diff --git a/versions/4.0/zh/editor/extension/contributions-menu.md b/versions/4.0/zh/editor/extension/contributions-menu.md new file mode 100644 index 0000000000..751bb4723e --- /dev/null +++ b/versions/4.0/zh/editor/extension/contributions-menu.md @@ -0,0 +1,70 @@ +# 自定义主菜单 + +编辑器顶部有一栏主菜单,在扩展内可以方便的在这个菜单栏添加自己的菜单。 + +## 注册菜单 + +当扩展需要添加菜单的时候,只需要填写 `contributions.menu` 对象。例如我们在 "扩展" 菜单里增加一个菜单项,可以修改 `package.json`,代码示例如下: + +```json5 +{ + // package.json + "name": "hello-world", + "contributions": { + "messages": { + "open-panel": { + "methods": ["openPanel"] + } + }, + "menu": [ + { + "path": "i18n:menu.extension", + "label": "Open Hello World", + "icon": "./static/icon.png", + "message": "open-panel" + } + ] + } +} +``` + +上面的配置信息将会在编辑器的 "扩展" 菜单里新增一个 "Open Hello World" 菜单,点击这个菜单后将会按照 message 配置发送一条 `open-panel` 消息给当前扩展,若当前扩展配置了这个消息的监听以及对应的 `openPanel` 处理函数,将会被触发。 + +关于消息的定义请参考文档 [自定义消息](./contributions-messages.md)。 + +下面我们来看看 `menu` 对象中各字段的意义: + +### path + +类型 {string} 必填 + +填写格式为:[顶部已有菜单路径][/路径1][/路径2],以下写法都是合理的: +- `i18n:menu.extension` - 以扩展菜单作为父菜单 +- `i18n:menu.extension/Hello World` - 在扩展菜单中添加一个 `Hello World` 菜单项作为父菜单 +- `MyMenu` - 在顶部菜单栏添加一个 `MyMenu` 菜单作为父菜单 +- `MyMenu/Hello World` - 在顶部菜单栏添加一个 `MyMenu`,并再添加一个 `Hello World` 菜单项作为父菜单 + +顶部菜单栏中,预设的菜单有: +- i18n:menu.project - “项目” 菜单 +- i18n:menu.node - “节点” 菜单 +- i18n:menu.panel - “面板” 菜单 +- i18n:menu.extension - “扩展” 菜单 +- i18n:menu.develop - “开发者” 菜单 + +### label + +类型 {string} 必填 + +菜单项目的名称,支持 i18n:key 语法。 + +### icon + +类型 {string} 可选 + +菜单的图标相对路径,扩展使用的素材一般放在名为 `static` 的文件夹下面,若不存在则新建一个。 + +### message + +类型 {string} 可选 + +菜单点击后触发的消息,此消息需要在 `contributions.messsages` 中先定义。 diff --git a/versions/4.0/zh/editor/extension/contributions-messages.md b/versions/4.0/zh/editor/extension/contributions-messages.md new file mode 100644 index 0000000000..a74011ba98 --- /dev/null +++ b/versions/4.0/zh/editor/extension/contributions-messages.md @@ -0,0 +1,122 @@ +# 自定义消息 + +在 Cocos Creator 编辑器架构中,所有的交互都是通过消息通信实现的,本文将讲解如何自定义一条消息,并调用这条消息。 + +## 定义一条消息(监听消息) + +只有在 `package.json` 文件的 `contributions.messages` 字段里定义过的消息才能被使用。消息的定义如下所示: + +```json +{ + "name": "hello-world", + "contributions": { + "messages": { + "test-messasge": { + "public": false, + "description": "", + "doc": "", + "methods": [] + } + } + } +} +``` + +`test-messasge` 为消息名称,下面我们逐一讲解每个属性的含义。 + +### public + +类型 {string} 可选 + +是否对外显示这条消息,如果为 true,则会在 **开发者->消息列表** 面板显示这条消息的基本信息。 + +### description + +类型 {string} 可选 + +消息摘要信息,如果 public 为 true,则会在消息管理面板显示,支持 i18n:key 语法。 + +### doc + +类型 {string} 可选 + +消息文档说明,如果 public 为 true,则会在消息管理面板显示,支持 i18n:key 语法。 + +这个文档使用 markdown 格式撰写并渲染。 + +### methods + +类型 {string[]} 可选 + +消息触发的方法队列。 + +这是一个字符串数组,字符串为扩展或者面板上的方法(methods)。 +如果是触发扩展主程序的方法,则直接定义 `methodName`,如果要触发扩展里定义的面板上的方法,则要填写 `panelName.methodName`。 + +下面的示例中,`package-message` 将触发扩展主程序中的 `receiveMessage` 方法,`panel-message` 将触发 `test-panel` 面板中的 `receiveMessage` 方法。 + +```json5 +{ + "name": "hello-world", + "panels": { + "test-panel": { + "title": "HelloWorld", + "main": "./dist/panel/index.js" + } + }, + "contributions": { + "messages": { + "package-message": { + "public": true, + "description": "Test Message: send to extension main.js", + "doc": "Unable to find inheritance data. Please check the specified source for any missing or incorrect information.\nLine breaks are also supported.\n- options {any}", + "methods": [ + "receiveMessage" + ] + }, + "panel-message": { + "methods": [ + "test-panel.receiveMessage" + ] + }, + "hello-world:ready": { + "public": true, + "description": "Test Broadcast Message" + } + } + } +} +``` + +![Alt text](./message-manager.png) + +![Alt text](./message-manager-2.png) + +定义完消息后,我们需要在扩展主入口和面板入口里,增加一个 `sendMessage` 方法: + +```typescript +export const methods: { [key: string]: (...any: any) => any } = { + receiveMessage() { + console.log('Received a message'); + }, +}; + +export function load() { } + +export function unload() { } +``` + +## 通过消息触发函数(执行消息) + +刚刚我们定义了两个消息,`package-message` 和 `panel-message`。 + +我们可以通过消息系统的 API 触发这个消息监听器: + +```typescript +// 不需要返回值 +Editor.Message.send('hello-world', 'panel-message'); +// Or 需要等待数据返回 +const result = await Editor.Message.request('hello-world', 'panel-message'); +``` + +关于更多消息机制,请参考文档 [消息系统](./messages.md)。 diff --git a/versions/4.0/zh/editor/extension/contributions-preferences.md b/versions/4.0/zh/editor/extension/contributions-preferences.md new file mode 100644 index 0000000000..2662f46925 --- /dev/null +++ b/versions/4.0/zh/editor/extension/contributions-preferences.md @@ -0,0 +1,148 @@ +# 自定义偏好设置面板 + +偏好设置,通常来说存放的是开发者个性化定制的设置,或一些绝对路径的配置,这些信息可能和当前的硬件绑定,如环境配置、一些开发工具的绝对路径。因此通常来说不需要通过 git 同步给该项目其他开发者。 + +在自定义偏好设置面板之前,开发者应该提前设计好是将配置存放在项目设置内,还是偏好设置内。 + +并且在偏好设置面板里,还能选择存放在项目还是存放在全局。存放在项目的配置只对当前项目生效,如果存放在全局,则会影响所有编辑器。 + +## 偏好设置面板简介 + +在顶部菜单栏可找到 **Cocos Creator/File -> 偏好设置** 菜单,如下图所示: + +![preferences](./image/preferences-menu.png) + +点击可打开偏好设置面板,如下图示: + +![preferences](./image/preferences-tool.png) + +偏好设置面板分成左右两侧: + +- 左侧显示的是提供配置项目的功能扩展的名字。 +- 右侧是根据配置渲染出来的操作面板。 + +面板上的修改,会立即修改到对应的配置项目上,更多关于 **偏好设置** 面板的介绍,请参考文档 [偏好设置](../../editor/preferences/index.md)。 + +## 自定义面板 + +Cocos Creator 允许每个扩展注册自己的编辑器配置,然后在偏好设置面板内显示。 + +偏好设置控制的是编辑器相关的配置,会作用到所有项目上,如果只想增加特定项目的配置,请参考文档 [自定义项目设置面板](./contributions-project.md)。 + +### 偏好设置的两种方式 + +偏好设置允许以两种方式显示配置: + +1. 通用配置 +2. 实验室配置 + +通用设置直接以选项卡的形式展示,而实验室开关则单独一个选项卡集中展示。 + +- 当扩展提供的功能比较稳定时建议将配置数据放在通用功能内。 +- 当扩展提供的功能处于开发阶段时建议将功能的开关配置数据放在实验室配置中。 + +### 偏好设置定义 + +我们在偏好设置里新增扩展的配置,也需要通过在扩展定义文件 `package.json` 里添加 `contributions` 配置来实现。 + +注册自定义偏好设置需要在 `contributions.profile.editor` 里定义好相关数据字段。更多详细信息可以参看 [配置系统](./profile.md) + +这里我们举例新建一个叫做 `first-panel` 的扩展: + +```JSON5 +{ + //`package.json` + "name": "first-panel", + "contributions": { + "profile": { + "editor": { + "foo": { + "default": 1, + "label":"foo" + }, + "foo1": { + "default": 1, + "label":"foo1" + }, + "foo2": { + "default": false, + "label":"foo2" + }, + "foo3": { + "default": 0, + "label":"foo3" + } + } + }, + } +} +``` + +> **注意**:偏好设置里的配置数据,都应该存放在 `profile.editor` 字段中。 + +当定义好数据字段后,还需要在 `contributions.preferences` 字段里定义需要显示的数据以及用什么 UI 组件来显示。如下所示: + +```JSON5 +{ + //`package.json` + "name": "first-panel", + "contributions": { + "profile": { + // ... + }, + "preferences": { + "properties": { + "foo1": { + "ui": "ui-slider", + "attributes": { + "min": 0, + "max": 1, + "step": 0.1 + } + }, + "foo2": { + "ui": "ui-checkbox" + }, + "foo3": { + "ui": "ui-select", + "items": [ + { + "value": 0, + "label": "ITEM 0" + }, + { + "value": 1, + "label": "ITEM 1" + }, + { + "value": 2, + "label": "ITEM 2" + } + ] + } + }, + "laboratory": ["foo"] + } + } +} +``` + +上面的示例中,在 `contributions.profile.editor` 字段定义了 4 个数据项: `foo`、`foo1`、`foo2`、`foo3`。详看 [配置系统](./profile.md)。 + +在 `contributions.preferences` 字段中,我们定义了 `properties` 和 `laboratory`。 + +### 通用配置(properties) + +`properties` 中定义的字段,将在偏好设置面板中新建一个与扩展同名的标签页独立显示,如下图所示: + +![preferences-tool-custom](./image/preferences-tool-custom.png) + +### 实验室配置(laboratory) + +`laboratory` 中定义的字段,将在偏好设置面板中的 **实验室(Laboratory)** 标签页中显示,如下图所示: + +![preferences-tool-custom-laboratory](./image/preferences-tool-custom-laboratory.png) + +## UI 组件配置 + +本示例展示了 4 种常见 UI 组件在自定义偏好设置面板时的用法,理论上所有带 `value` 属性的 UI 组件都可以用于偏好设置面板,具体用法请参考文档 [UI 组件](./ui.md)。 diff --git a/versions/4.0/zh/editor/extension/contributions-project.md b/versions/4.0/zh/editor/extension/contributions-project.md new file mode 100644 index 0000000000..2bfabb4f7a --- /dev/null +++ b/versions/4.0/zh/editor/extension/contributions-project.md @@ -0,0 +1,139 @@ +# 自定义项目设置面板 + +项目设置是存放和项目开发相关配置的地方,例如引擎模块、文件后处理配置等,对应配置的修改变动会记录在项目目录的 settings 目录下。 + +> **注意**:settings 目录默认是进入项目版本管理的,在扩展自定义项目设置时,请不要记录一些临时数据或者是存在系统差异的数据配置,以免不同设备的用户在同步数据时遇到问题。 + +## 面板介绍 + +**项目设置** 里存放的是和项目运行相关的配置,这部分配置存放在项目目录下的 `settings` 文件夹中,需要纳入版本管理,多人共享配置,否则可能导致不同机器上运行不一致的问题。 + +可在顶部菜单栏中找到 **项目** -> **项目设置** 菜单,如下图所示: + +![project-settings-menu](./image/project-settings-menu.png) + +点击后可打开设置设置面板,如下图所示: + +![project-settings-panel](./image/project-settings-panel.png) + +项目设置面板左侧是功能模块选项卡,右侧则对应功能的配置修改界面。 + +我们可以通过自定义此面板的显示数据,为项目新增自定义配置,借助项目设置面板实现项目配置的可视化管理。 + +如果不需要纳入版本管理,同时对本机的所有项目生效,则需要自定义编辑器相关的配置,请参考文档 [自定义偏好设置](./contributions-preferences.md)。 + +**自定义项目设置** 功能允许一个我们注册多个选项卡,所以左侧选项卡上会有一行小字,标示选项卡属于哪一个扩展。 + +## 数据配置与显示 + +我们在项目配置里新增扩展的配置,也需要通过在扩展定义文件 `package.json` 里添加 `contributions` 配置来实现。 + +自定义项目设置需要依赖数据配置,需要先在 `contributions.profile.project` 里定义好相关数据字段。更多详细信息可以参看 [配置系统](./profile.md) + +这里我们举例新建一个叫做 `first-panel` 的扩展: + +```json5 +// package.json +{ + "name": "first-panel", + "contributions": { + "profile": { + "project": { + "foo": { + "default": 1 + }, + "foo1": { + "default": 0.4 + }, + "foo2": { + "default": false + }, + "foo3": { + "default": 0 + }, + "foo4": { + "default": "label" + } + } + } + } +} +``` + +> **注意**:项目设置里的配置数据,都应该存放在 `profile.project` 字段中,标记这份配置需要跟随项目,并且需要同步给该项目的其他开发者。 + +当定义好数据字段后,还需要在 `contributions.project` 字段里定义需要显示的数据以及用什么 UI 组件来显示。如下所示: + +```json5 +// package.json +{ + "name": "first-panel", + "contributions": { + "profile": { + // ... + }, + "project": { + "tab1": { + "label": "test", + "content": { + "foo": { + "ui": "ui-num-input" + }, + "foo1": { + "ui": "ui-slider", + "attributes": { + "min": 0, + "max": 1, + "step": 0.1 + } + }, + "foo2": { + "ui": "ui-checkbox" + }, + "foo3": { + "ui": "ui-select", + "items": [ + { + "value": 0, + "label": "ITEM 0" + }, + { + "value": 1, + "label": "ITEM 1" + }, + { + "value": 2, + "label": "ITEM 2" + } + ] + } + } + }, + "tab2": { + "label": "test2", + "content": { + "foo4": { + "ui": "ui-input" + } + } + } + } + } +} +``` + +上面的示例中,在 `contributions.profile.project` 字段定义了 4 个数据项:`foo`、`foo1`、`foo2`、`foo3`。 + +关于如何定义 `profile` 相关配置,请参看 [配置系统](./profile.md)。 + +在 `contributions.project` 字段中定义了 2 个标签页:`test`、`tes2`。 + +在 `test` 标签页中,对 4 个数据项分别做了配置,具体的配置属性请看后面的 `UI 组件配置`。 + +在扩展管理器列表中刷新扩展后,再次打开 **项目设置面板**, 可看到如下界面: + +![project-settings-panel-custom](./image/project-settings-panel-custom.png) + +## UI 组件配置 + +本示例展示了 4 种常见 UI 组件在自定义项目设置面板时的用法,理论上所有带 `value` 属性的 UI 组件都可以用于自定义项目设置面板,具体用法请参考文档 [UI 组件](./ui.md)。 diff --git a/versions/4.0/zh/editor/extension/contributions-shortcuts.md b/versions/4.0/zh/editor/extension/contributions-shortcuts.md new file mode 100644 index 0000000000..026c960e22 --- /dev/null +++ b/versions/4.0/zh/editor/extension/contributions-shortcuts.md @@ -0,0 +1,71 @@ +# 自定义快捷键 + +编辑器内的快捷键由 "快捷键管理器" 统一管理。每一个快捷键可以绑定一个消息,当快捷键按下的时候,会触发绑定的消息。 + +## 定义快捷键 + +定义快捷键需要在 `package.json` 的 `contributions.shortcuts` 字段中进行,如下所示: + +```json5 +// package.json +{ + "name": "hello-world", + "panels": { + "default": { + "main": "./panel.js" + } + }, + "contributions": { + "messages": { + "undo": { + "title": "i18n:hello.messages.undo.title", + "methods": ["say-undo"] + } + }, + "shortcuts": [ + { + "message": "undo", + "when": "panelName === 'hello-world'", + "win": "ctrl+z", + "mac": "cmd+z", + } + ] + } +} +``` + +本示例中,我们定义了一个 **撤销** 操作的快捷键,在 Windows 系统下是 `CTRL + Z`,在 macOS 系统下是 `CMD + Z`。 + +当对应快捷键被按下时,会发送 `undo` 消息。 + +> **注意**:此消息需要在 `contributions.messages` 里面先定义好,详请请参考文档 [自定义消息](./contributions-messages.md)。 + +## 参数说明 + +下面我们来看看 `contributions.shortcuts` 各参数的具体说明。 + +### message + +类型 {string} 必填 + +快捷键绑定的消息,当这个快捷键被触发时,会发送此消息。快捷键按下的消息只能发送给当前扩展。 + +### when + +类型 {string} 可选 + +在某些条件下才会触发这个快捷键。 + +`"when": "panelName === 'hello-world'"` 表示当获得焦点的面板名称为 `hello-world` 时,按下快捷键才会发送 `message` 消息。 + +### win + +类型 {string} 必填 + +在 Windows 平台上,监听的按键。 + +### mac + +类型 {string} 必填 + +在 macOS 上,监听的按键。 diff --git a/versions/4.0/zh/editor/extension/contributions.md b/versions/4.0/zh/editor/extension/contributions.md new file mode 100644 index 0000000000..f8a55744d9 --- /dev/null +++ b/versions/4.0/zh/editor/extension/contributions.md @@ -0,0 +1,51 @@ +# 增强已有的功能 + +Cocos Creator 支持各个扩展间互相提供数据(`contributions`)。 + +我们在编写一个扩展的时候,可以查询编辑器内已有功能是否提供了对外接收 `contributions` 的功能。如果对应功能提供该功能,则能够在编写扩展的时候使用这些功能。 + +## contributions 数据定义 + +`contributions` 功能,统一在 `package.json` 里的 `contributions` 字段中定义,如下所示: + +```JSON5 +{ + "name": "hello-world", + "contributions": { + "builder":{ ... }, + "assets":{ ... }, + "profile": { ... }, + "scene": { ... }, + "menu": [ ... ], + "inspector":{ ... }, + "messages": { ... }, + "shortcuts": { ... }, + "preferences": { ... }, + "project": { ... } + }, +} +``` + +## 字段说明 + +`contributions` 提供了与编辑器各功能系统交互的能力,主要涉及到的功能如下: + +- `builder` - 自定义构建流程,详细信息请参考文档 [自定义构建流程](../publish/custom-build-plugin.md)。 + +- `assets` - 增强资源管理器面板,详细信息请参考文档 [增强资源管理器面板](../assets/extension.md)。 + +- `profile` - 定义扩展需要用到的配置,详细信息请参看文档 [配置系统](./profile.md)。 + +- `scene` - 在扩展中编写需要和引擎、项目脚本交互的脚本,详细信息请参看文档 [调用引擎 API 和项目脚本](./scene-script.md)。 + +- `inspector` - 自定义 **属性检查器** 面板,详细信息请参看文档 [自定义属性检查器面板](./inspector.md)。 + +- `menu` - 定义扩展需要新增的菜单信息,详细信息请参看文档 [自定义主菜单](./contributions-menu.md)。 + +- `messages` - 定义扩展需要用到的[消息列表](./messages.md#查看消息列表),详细信息请参看文档 [自定义消息](./contributions-messages.md)。 + +- `shortcuts` - 定义扩展需要用到的快捷键,详细信息请参看文档 [自定义快捷键](./contributions-shortcuts.md)。 + +- `preferences` - 自定义偏好设置,详细信息请参看文档 [自定义偏好设置面板](./contributions-preferences.md)。 + +- `project` - 自定义项目设置,详细信息请参看文档 [自定义项目设置面板](./contributions-project.md)。 diff --git a/versions/4.0/zh/editor/extension/create-extension.md b/versions/4.0/zh/editor/extension/create-extension.md new file mode 100644 index 0000000000..dbb4ff43bc --- /dev/null +++ b/versions/4.0/zh/editor/extension/create-extension.md @@ -0,0 +1,59 @@ +# 扩展模板与编译构建 + +本文将详细介绍如何通过 Creator 提供的扩展模板创建一个带有面板的扩展并使用。 + +在编辑器主菜单上选择 **扩展 -> 创建扩展** 菜单,即可打开新建面板。 + +![create-extension-menu](./image/create-extension-menu.png) + +![create-extension-panel](./image/create-extension-panel.png) + +## 模板类型 + +Cocos Creator 提供了 4 种扩展模板,用于快速创建一个新的扩展项目。 +- **Blank**:空扩展。 +- **HTML Panel**:基于 HTML 的扩展模板,并带有一个弹出面板。 +- **Vue2.x Panel**:基于 Vue2.x 的扩展模板,并带有一个弹出面板。 +- **Vue3.x Panel**:基于 Vue3.x 的扩展模板,并带有一个弹出面板。 + +以上 4 种方式创建出来的扩展仅在工作量和技术选型上有差别,扩展可实现的能力无差别,开发者可根据自己的需求和技术背景自行选择。 + +> **注意**:每个模板创建出来的扩展都不一样,请在创建后参考对应扩展包目录下的 `README.md` 文件。 + +## 选项说明 + +| 扩展选项 | 功能说明 | +| :--- | :----- | +| **扩展名** | 创建的扩展名称,要求不能以 `_` 或 `.` 开头、不能含有大写字母。因为扩展名会成为 URL 的一部分,因此也不能含有 URL 的非法字符、不能含有 `.`、`'` 和 `,`。 | +| **作者** | 扩展的作者 | +| **依赖的编辑器版本** | 创建的扩展运行时要求的 Cocos Creator 版本。 | +| **扩展管理器中显示** | 若勾选该项,则扩展创建完成后,会自动打开 **扩展管理器**,并显示创建的扩展。
若不勾选该项,则扩展创建完成后,可点击编辑器顶部菜单栏中的 **扩展 -> 扩展管理器** 查看。 | +| **在文件夹中展示** | 若勾选该项,则扩展创建完成后会自动在系统文件管理器中打开扩展包。 | +| **扩展创建的位置** | 创建的扩展包所在目录,自 v3.7 后,仅能创建位置为 **项目(Project)** 的扩展。| + +## 扩展位置 + +### 项目(Project) + +将扩展包应用到指定的 Cocos Creator 项目,**项目** 路径为: + +- `${你的项目地址}/extensions` + +## 依赖安装与编译构建 + +创建完成后打开扩展包所在目录,执行以下命令: + +```bash +# 安装依赖模块 +npm install +# 构建 +npm run build +``` + +扩展会依赖 **Node.js** 的第三方库,所以在启用扩展之前需要先在扩展目录下执行 `npm install` 安装依赖模块才能正常编译。 + +TypeScript 在编写时能够带上完整的代码提示,具备更强的工程性,因此 Cocos Creator 推荐使用基于 TypeScript 的工作流。 + +默认提供的扩展模版也都是基于 TypeScript,需需要执行 `npm run build` 编译后才能运行。 + +> **注意**:扩展的 `tsconfig.json` 配置文件中开启了 `resolveJsonModule`,以便通过导入的扩展的 `package.json` 来获取扩展名称,因此 TypeScript 需要 **v4.3** 及以上版本,否则在导入根目录以外的 `json` 时会出现编译结果路径错误的问题。 diff --git a/versions/4.0/zh/editor/extension/define.md b/versions/4.0/zh/editor/extension/define.md new file mode 100644 index 0000000000..e761eefb50 --- /dev/null +++ b/versions/4.0/zh/editor/extension/define.md @@ -0,0 +1,91 @@ +# 扩展包的定义 + +扩展包需要在 `package.json` 文件里预先定义好所有功能以及一些基础信息,如下所示: + +```JSON5 +{ + "package_version": 2, + "version": "1.0.0", + "name": "first-panel", + "tilte": "i18n:first-panel.title", + "description": "i18n:first-panel.description", + "author": "Cocos Creator", + "editor": ">=3.4.2", + "main": "./dist/main.js", + "dependencies": { ... }, + "devDependencies": { ... }, + "panels": { ... }, + "contributions": { + }, + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + } +} +``` + +## package_version + +类型 {number} 必填 + +扩展的版本号,用于提交扩展的版本校验,以及扩展自身的一些升级,数据迁移作为对比的依据。 + +## version + +类型 {string} 必填 + +扩展的版本号,主要用于显示,如需进行逻辑判断,推荐使用 `package_version`。 + +## name + +类型 {string} 必填 + +扩展的名称,这个名字需要和扩展文件夹一一对应。 + +## title + +类型 {string} 可选 + +扩展的显示标题,当配置了 `title` 时,在需要展示扩展名的地方会优先采用 `title`,支持 [多语言(i18n)](./i18n.md) 配置。 + +## description + +类型 {string} 可选 + +扩展的描述,简单概括一下扩展的功能。支持 [多语言(i18n)](./i18n.md) 的多语言语法。 + +## author + +类型 {string} 可选 + +扩展作者的名字,将会显示在 "扩展管理器" 内。 + +## editor + +类型 {string} 可选 + +描述扩展支持的编辑器版本,符合 [`semver` 语义化版本控制规范](https://semver.org/)。 + +## main + +类型 {string} 可选 + +一个 js 文件的相对路径,定义功能入口文件,当扩展启动的时候,就会执行 `main` 字段指向的 js 文件,并根据流程触发或执行对应的方法。 + +## panels + +类型 {[name: string]: PanelInfo} 可选 + +扩展内定义的面板信息。可以使用 `Editor.Panel.open('hello-world.list');` 打开定义好的面板。详细信息请参看 [面板系统](./panel.md)。 + +## contributions + +类型 {[name: string]: any} 可选 + +`contributions` 提供了与编辑器各功能系统交互的能力,更多信息请参看文档 [增强已有功能](./contributions.md)。 + +## scripts + +类型 {[name: string]: any} 必填 + +扩展可执行的命令行。 diff --git a/versions/4.0/zh/editor/extension/editor-api.md b/versions/4.0/zh/editor/extension/editor-api.md new file mode 100644 index 0000000000..7f90bb3d68 --- /dev/null +++ b/versions/4.0/zh/editor/extension/editor-api.md @@ -0,0 +1,17 @@ +# Editor API 说明 + +本文介绍编辑器提供的接口的说明 + +- [App](./api/app.md) +- [Clipboard](./api/clipboard.md) +- [Dialog](./api/dialog.md) +- [I18n](./api/i18n.md) +- [Logger](./api/logger.md) +- [Message](./api/message.md) +- [Network](./api/network.md) +- [Package](./api/package.md) +- [Panel](./api/panel.md) +- [Profile](./api/profile.md) +- [Project](./api/project.md) +- [Selection](./api/selection.md) +- [Utils](./api/utils.md) diff --git a/versions/4.0/zh/editor/extension/extension-change-name.md b/versions/4.0/zh/editor/extension/extension-change-name.md new file mode 100644 index 0000000000..49bb91a1c1 --- /dev/null +++ b/versions/4.0/zh/editor/extension/extension-change-name.md @@ -0,0 +1,25 @@ +# 扩展改名 + +## 修改显示名称 + +如果想对扩展改名,只需修改 `package.json` 文件中的 `name` 字段即可。代码示例如下: + +```JSON5 +// "name": "simple-1649426645745" +"name": "hello-world" +``` + +像上面一样将 `name` 字段改为 "hello-world" 并在扩展管理器面板刷新扩展,就可以看到扩展的名称改成了 **hello-world**。 + +![extension](first/extension-hello-world.png) + +## 修改扩展文件夹名称 + +如果想连同文件夹名称一并修改,需要在文件夹修改后重启 Cocos Creator,才能让修改过文件夹名称的扩展重新生效。 + +如果修改了文件夹名称,**i18n** 多语言相关的字符串路径也需要一并修改,如下所示: + +```json5 +// "description": "i18n:simple-1649426645745.description", +"description": "i18n:hello-world.description", +``` diff --git a/versions/4.0/zh/editor/extension/extension-manager.md b/versions/4.0/zh/editor/extension/extension-manager.md new file mode 100644 index 0000000000..857a6e790a --- /dev/null +++ b/versions/4.0/zh/editor/extension/extension-manager.md @@ -0,0 +1,35 @@ +# 扩展管理器面板 + +**扩展管理器** 用于管理编辑器内的扩展。点击 Cocos Creator 顶部菜单栏中的 **扩展 -> 扩展管理器** 即可打开: + +![extension-manager-menu](./image/extension-manager-menu.png) + +扩展管理器面板如下: + +![extension-manager](./image/extension-manager.png) + +## 相关功能说明 + +1. 扩展类型,分为 **Cocos 官方** 以及 **内置**,通过下拉菜单选择。 +2. 已安装,点击后展示目前已安装的扩展。 +3. 从左到右依次为 **搜索**、**导入** 和 **刷新** + - **搜索**:点击后可在下图所示的输入框内通过关键字查找当前项目内的扩展: + ![search](./image/search.png) + - **导入**:点击后通过 zip 压缩文件导入新的扩展 + - **刷新**: 刷新当前所有扩展的状态 +4. 扩展列表: + + ![detail](./image/ext-detail.png) + + 对于每一个扩展,左侧显示的是扩展名称、图标、版本号以及描述。 + 右侧的按钮分别为: + - **打开扩展所在目录** + - **删除** 该扩展 + - **启用/禁用** 该扩展 + 内置和官方扩展无法删除和禁用,部分按钮需要将鼠标移动到该条目才可见。 +5. 扩展的详细信息 + +## 导入方式 + +- **导入扩展文件夹**:选择这种导入方式,需要将扩展拷贝到项目的 extensions 目录。 +- **开发者导入**:选择这种导入方式后,扩展会以软链方式被引用,不需要拷贝到 extensions 目录,方便开发者进行项目管理。 diff --git a/versions/4.0/zh/editor/extension/first-communication.md b/versions/4.0/zh/editor/extension/first-communication.md new file mode 100644 index 0000000000..93b3cf9b8a --- /dev/null +++ b/versions/4.0/zh/editor/extension/first-communication.md @@ -0,0 +1,157 @@ +# 入门示例-扩展间通信 + +在前面两篇文档 [入门示例-菜单](./first.md) 和 [入门示例-面板](./first-panel.md) 中,我们介绍了: +- 怎么创建扩展 +- 怎么在扩展中定义菜单 +- 怎么在扩展中定义消息 +- 怎么在扩展中定义面板 + +本文主要演示两个扩展之间如何通信,将涉及到三个话题: +- 如何打开另一个扩展的面板 +- 如何向另一个扩展发送消息 +- 如何发送和监听广播消息 + +## 打开另一个扩展的面板 + +有时候我们需要在自己写的扩展中打开另一个扩展,接下来我们就试着对 **入门示例-菜单** 中的扩展示例进行改造,使它可以打开 **入门示例-面板**。 + +修改后的 `package.json` 如下: + +```JSON5 +{ + "package_version": 2, + "version": "1.0.0", + "name": "hello-world", + ... + "contributions": { + "menu": [ + { + "path": "Develop/HelloWorld", + "label": "test", + "message": "log" + }, + { + "path": "Develop/HelloWorld", + "label": "open other", + "message": "open-other" + } + ], + "messages": { + "log": { + "methods": [ + "log" + ] + }, + "open-other": { + "methods": [ + "openOther" + ] + } + } + } +} +``` + +我们修改了 `contributions.menu`,新增了 `open other` 菜单项,并且把此扩展的菜单都放到了 Develop/HelloWorld 下。 刷新扩展后,可以在顶部菜单栏找到如下图所示的菜单内容: + +![extension-menu-hw.png](./first/extension-menu-hw.png) + +在 `contributions.messages` 中,我们新增了一个 `open-other` 消息,并让 `main.ts` 里的 `openOther` 函数处理此消息。 + +**入门示例-面板** 中的扩展名为 `first-panel`,因此我们使用 `Editor.Panel.open('扩展名')` 来打开它的默认面板,如下所示: + +```typescript +openOther(){ + Editor.Panel.open('first-panel'); +} +``` + +在 `hello-world` 根目录执行 `npm run build` 命令后,前往 **扩展管理列表** 刷新 `hello-world` 扩展。 + +点击 **Develop** -> **HelloWorld** -> **open other** 菜单项,即可以看到示例面板被打开。 + +## 与其他扩展通信 + +### 定向通信 + +在上面的示例中, 我们在 `hello-world` 中通过 `Editor.Panel.open('扩展名')` 来打开 `first-panel` 的面板。 但如果我们是想做其他操作,这种方案就不行了。 + +当一个扩展想要调用另一个扩展的功能时,可以通过以下函数向某个扩展发送消息来实现: + +```typescript +Editor.Message.send(extensionName:string,messasge:string,...args:any[]) +``` + +每一个扩展的 `contributions.messages` 中定义的消息,默认都是可以对外的。在 `first-panel` 中我们找到了 `open-panel` 消息,它用于打开自己的默认面板。为了简单起见,我们将 `hello-world` 中 `main.ts` 的 `openOther` 函数改为如下内容: + +```typescript +openOther(){ + Editor.Message.send('first-panel','open-panel'); +} +``` + +重新编译 `hello-world` 扩展并刷新后,再次点击 **Develop** -> **HelloWorld** -> **open other** 菜单项,可以看到 `first-panel` 的默认面板被打开了。 + +### 广播通信 + +当一个扩展想要向整个系统所有扩展通知某个事件完成的时候,可以通过以下函数广播一条消息来实现: + +```typescript +Editor.Message.broadcast(message:string, ...args:any[])` +``` + +接下来我们定义一个叫 `first-panel:open` 的广播消息,由 `first-panel` 扩展来广播,由 `hello-world` 扩展来监听。 + +在 `hello-world` 中,我们新增一个消息监听,并指定处理函数,修改后的 `contributions.messages` 如下: + +```json5 +{ + "messages": { + "log": { + "methods": [ + "log" + ] + }, + "open-other": { + "methods": [ + "openOther" + ] + }, + "first-panel:open":{ + "methods": [ + "onFirstPanelOpen" + ] + } + } +} +``` + +然后在 `hello-world` 的 `main.ts` 增加如下处理函数: + +```typescript +onFirstPanelOpen(){ + console.log("hello-world knows first-panel is open"); +} +``` + +作为监听方的改造就完成了,接下来我们修改一下广播方 `first-panel`。 + +在 `first-panel` 项目的 `src/panels/default/index.ts :ready` 函数中加入如下广播消息代码: + +```typescript +Editor.Message.broadcast("first-panel:open"); +``` + +`ready` 函数会在 `first-panel` 的默认面板打开时调用,此时会对 `first-panel:open` 消息进行广播。 + +> **注意**:广播方也可以在 messages 中监听自己的广播消息,但通常没必要。 + +分别编译并刷新两个扩展,再次点击 **Develop** -> **HelloWorld** -> **open other** 菜单项,除了可以看到示例面板被打开,还能在 Cocos Creator 的控制台窗口中看到如下打印: + +``` +hello-world knows first-panel is open +``` + +这就表示 `hello-world` 扩展收到了 `first-panel` 扩展的广播消息。 + +更多消息相关的详细内容请参考文档 [消息系统](./messages.md)。 diff --git a/versions/4.0/zh/editor/extension/first-panel.md b/versions/4.0/zh/editor/extension/first-panel.md new file mode 100644 index 0000000000..87481df803 --- /dev/null +++ b/versions/4.0/zh/editor/extension/first-panel.md @@ -0,0 +1,148 @@ +# 入门示例-面板 + +在文档 [入门示例-菜单](./first.md) 中讲解了怎么创建一个最简单的扩展,接下来我们看看如何创建一个面板并与之通信。 + +## 通过模板创建 + +在 Cocos Creator 中创建面板最快捷的方式是通过 **包含面板的扩展模板** 创建,如下图所示: + +![extension-first-panel-create](./first/extension-first-panel-create.png) + +点击 **创建扩展** 按钮后,可以在项目根目录下找到 extensions/first-panel 扩展。 + +### 编译、安装 + +在命令行工具中,定位到 extensions/first-panel 目录,并执行以下语句: + +```bash +npm install +npm run build +``` + +命令执行完成后,回到 **扩展管理器** 中,找到 first-panel 扩展,启用并刷新,如下图所示: + +![extension-first-panel-enable](./first/extension-first-panel-enable.png) + +### 查看面板 + +启用并刷新扩展后,可以在 **面板(Panel)** 菜单中找到如下图所示的菜单项: + +![extension-first-panel-menu](./first/extension-first-panel-menu.png) + +点击 **默认面板(Default panel)** 菜单项,即可弹出如下所示面板: + +![extension-first-panel](./first/extension-first-panel.png) + +本示例还在 **开发者(Developer)** 菜单中定义了另一个用于通信的菜单项,如下图所示: + +![extension-first-panel-sendmsg](./first/extension-first-panel-sendmsg.png) + +点击上图中红色方框所示的 **发送消息给面板(Send message to Default Panel)** 按钮后,可以看到面板上显示的内容会发生改变。 + +> **注意**:如果没有显示该菜单,请打开 **扩展管理器** 面板,并确认该扩展已启用。 +> +> ![enable-first-panel](./image/enable-first-panel.png) + +## 面板讲解 + +接下来,我们逐一讲解面板目录结构、定义与通信机制。 + +### 面板目录结构 + +在项目所在目录,找到 `./extensions/first-panel` 文件夹,即可以总览整个扩展的目录: + +![extension-first-panel-folder](./first/extension-first-panel-folder.png) + +如上图所示,比 hello-world 多出了 `static` 和 `panels`目录。 + +`static` - 用于存放面板布局文件,如 css\html 等。 + +`panels` - 用于存放面板相关的源代码,每一个面板有一个 `index.ts` 入口源文件。 + +`index.ts`、`style`、`template` 请参考文档 [编写面板](./panel-boot.md) + +### 描述文件 package.json + +在理解面板之前,我们先看看 `package.json` 中,面板相关的定义,如下所示: + +```json +{ + "package_version": 2, + "version": "1.0.0", + "name": "first-panel", + "description": "i18n:first-panel.description", + "main": "./dist/main.js", + "dependencies": { + "fs-extra": "^10.0.0" + }, + "devDependencies": { + "@types/node": "^16.0.1", + "@types/fs-extra": "^9.0.5", + "typescript": "^4.3.4" + }, + "panels": { + "default": { + "title": "first-panel Default Panel", + "type": "dockable", + "main": "dist/panels/default", + "size": { + "min-width": 400, + "min-height": 300, + "width": 1024, + "height": 600 + } + } + }, + "contributions": { + "menu": [ + { + "path": "i18n:menu.panel/first-panel", + "label": "i18n:first-panel.open_panel", + "message": "open-panel" + }, + { + "path": "i18n:menu.develop/first-panel", + "label": "i18n:first-panel.send_to_panel", + "message": "send-to-panel" + } + ], + "messages": { + "open-panel": { + "methods": [ + "openPanel" + ] + }, + "send-to-panel": { + "methods": [ + "default.hello" + ] + } + } + }, + "author": "Cocos Creator", + "editor": ">=3.4.2", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + } +} +``` + +`panels`:{} - 本扩展中定义的面板 +- default:String - 定义了一个名为 default 的面板 + - title:String - 面板标题 + - type:String - 面板类型 + - main:String - 面板源码目录 + - size:{} - 大小信息 + - min-width:Number - 最小宽度 + - min-height:Number - 最小高度 + - width:Number - 面板默认宽度 + - height:Number - 面板默认高度 + +## 更多阅读 + +`panel` 详细的面板讲解,请参考文档 [面板系统](./panel.md)。 + +`i18n` 为多语言配置,请参考文档 [多语言系统(i18n)](./i18n.md)。 + +`messages` 完整的消息定义机制,请参考文档 [自定义消息](./contributions-messages.md)。 diff --git a/versions/4.0/zh/editor/extension/first.md b/versions/4.0/zh/editor/extension/first.md new file mode 100644 index 0000000000..cb14784990 --- /dev/null +++ b/versions/4.0/zh/editor/extension/first.md @@ -0,0 +1,200 @@ +# 入门示例-菜单 + +本文将演示如何创建一个 Cocos Creator 扩展,本文将包含以下知识点: +- 创建扩展 +- 新增菜单 +- 菜单消息 + +## 创建并安装扩展 + +在编辑器的菜单栏中找到 **扩展 -> 创建扩展** 菜单,如下图所示: + +![create-extension-menu](image/create-extension-menu.png) + +点击 **创建扩展** 后,会弹出如下图所示的创建面板: + +![create-extension-panel](image/create-extension-panel.png) + +Cocos Creator 提供了如上图所示 4 种扩展模板,用于快速创建一个新的扩展项目。 + +为了更简单的演示模板创建流程,我们选择 **Blank** 模板,点击面板右下方的 **创建扩展** 按钮建一个扩展包。 + +更多模板创建相关内容,请参考文档 [扩展模板与编译构建-模板类型](./create-extension.md)。 + +## 扩展管理 + +扩展创建成功后,点击顶部菜单栏中的 **扩展 -> 扩展管理器 -> 已安装扩展**,即可看到刚才创建的扩展。新创建的扩展默认是未启用状态,点击启用按钮即可启用此扩展: + +![extension](first/extension.png) + +更多扩展管理内容请参考文档 [扩展管理器-扩展列表](./extension-manager.md)。 + +## 扩展目录 + +点击 ![folder](first/folder.png) 按钮可打开扩展包所在目录。以 **Blank** 模板为例,目录结构如下: + +![extension-folder](image/extension-folder-blank.png) + +各子文件(夹)功能如下: +- `@types` - TypeScript 定义文件。包含编辑器全局对象 Editor 的定义,以及一些消息,和扩展对外的类型定义。 +- `dist` - TypeScript 生成的 javascript 代码。 +- `i18n` - 多语言配置。 +- `src` - TypeScript 源代码。 +- `package.json` - 扩展描述文件。 +- `README-CN/EN.md` - 中文/英文说明文件。 +- `tsconfig.json` - TypeScript 配置文件。 + +## 扩展定义文件 `package.json` + +每个扩展都需要有一份 `package.json` 文件,用于描述扩展的用途。 + +只有完整定义了描述文件 `package.json` 后,编辑器才能知道这个扩展里定义的具体功能、加载入口等信息。 + +> **注意**:虽然 `package.json` 很多字段的定义和 `node.js` npm 模块的 `package.json` 相似,但从 npm 社区中下载的 npm 模块并不能直接作为 Cocos Creator 扩展使用。可以在 Cocos Creator 扩展中调用 npm 模块,使扩展具备相应的能力。 + +打开 `package.json` 文件,可以看到以下内容: + +```json +{ + "package_version": 2, + "version": "1.0.0", + "name": "simple-1649426645745", + "description": "i18n:simple-1649426645745.description", + "main": "./dist/main.js", + "devDependencies": { + "@types/node": "^16.0.1", + "typescript": "^4.3.4" + }, + "author": "Cocos Creator", + "editor": ">=3.4.2", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + } +} + +``` + +各字段含义如下: +- `package_version`:Number - 版本号数值。 +- `version`:String - 版本号字符串,推荐使用 [semver](http://semver.org/) 格式管理你的包版本。 +- `name`:String - 定义了包的名字,包的名字是全局唯一的。命名规则请参考 [选项说明](./create-extension.md)。 +- `description`:Stirng - 扩展描述,用于简要介绍扩展关键特性、用途等信息,支持 **i18n** 多语言设置。 +- `main`:String - 入口程序文件。 +- `devDependencies`:{} - 扩展依赖。如本示例中,扩展依赖的 NodeJS 版本为 16.0.1,依赖的 TypeScript 版本为 4.3.4。 +- `author`:String - 作者信息。 +- `editor`:String - 支持的 Cocos Creator 编辑器版本。 +- `scripts`:{} - 脚本编译相关命令。 + +## 定义菜单和消息 + +将 `package.json` 改为如下内容: + +```json +{ + "package_version": 2, + "version": "1.0.0", + "name": "simple-1649426645745", + "description": "i18n:simple-1649426645745.description", + "main": "./dist/main.js", + "devDependencies": { + "@types/node": "^16.0.1", + "typescript": "^4.3.4" + }, + "author": "Cocos Creator", + "editor": ">=3.4.2", + "scripts": { + "build": "tsc -b", + "watch": "tsc -w" + }, + //------------------------------ + "contributions": { + "menu": [{ + "path": "Develop", + "label": "test", + "message": "log" + }], + "messages": { + "log": { + "methods": ["log"] + } + } + } +} +``` + +新增字段含义如下: +- `contributions`:Object(可选)- 对编辑器已有功能进行扩展的相关配置 + - `menu`:[],注册菜单,并绑定消息。具体内容请参考 [自定义主菜单](./contributions-menu.md)。 + - `messages`:[] - 注册编辑器消息,可以绑定一个或多个的扩展内定义的方法。更多定义数据请参考 [自定义消息](./contributions-messages.md)。 + +更多关于 `package.json` 格式的定义,请参考 [扩展包的定义](./define.md)。 + +## 安装依赖和编译构建 + +扩展创建完成后打开扩展包所在目录,执行以下命令: + +```bash +# install dependencies +npm install +# build +npm run build +``` + +更多扩展编译构建相关信息参考文档 [扩展模板与编译构建](./create-extension.md)。 + +## 运行扩展 + +返回编辑器,点击顶部菜单栏中的 **扩展 -> 扩展管理器 -> 项目**,找到之前创建的扩展。点击扩展右侧的 ![refresh](first/refresh.png) 按钮,使上面的修改内容生效。 + +若扩展已生效,在 Cocos Creator 顶部菜单栏区域会出现一个 **Develop** 菜单,并带有一个 **test** 菜单项,如下图所示: + +![menu-test](first/extension-menu-test.png) + +此时点击 **test** 菜单项会发现没有任何反应,这是因为我们还没有为菜单信息编写对应的代码。 + +接下来我们便看看如何让菜单与扩展通信。 + +## 入口程序 `main.ts` + +每一个扩展都有一个唯一的入口程序 `main.ts`,默认生成的内容如下: + +```typescript +/** + * @en Registration method for the main process of Extension + * @zh 为扩展的主进程的注册方法 + */ +export const methods: { [key: string]: (...any: any) => any } = { }; + +/** + * @en Hooks triggered after extension loading is complete + * @zh 扩展加载完成后触发的钩子 + */ +export const load = function() { }; + +/** + * @en Hooks triggered after extension uninstallation is complete + * @zh 扩展卸载完成后触发的钩子 + */ +export const unload = function() { }; +``` + +`export const methods` 中定义的方法,将会作为操作的接口,通过 [消息系统](./messages.md) 跨扩展调用,或者是和面板通信。 + +入口程序是扩展的主进程,会在 Cocos Creator 的启动过程中被加载。 + +## 菜单消息处理 + +我们对入口程序稍作修改,添加一个接收 `log` 消息的处理函数,如下所示: + +```typescript +export const methods: { [key: string]: (...any: any) => any } = { + log(){console.log('Hello World')}, +}; +``` + +执行 `npm run build` 命令,编译扩展。 + +点击扩展右侧的 ![refresh](first/refresh.png) 按钮,并禁用再启用扩展,使上面的修改内容生效。 + +再次点击 `Develop/test`菜单项,会发现在 Cocos Creator **控制台** 打印出了 “Hello World”。 diff --git a/versions/4.0/zh/editor/extension/first/delete.png b/versions/4.0/zh/editor/extension/first/delete.png new file mode 100644 index 0000000000..a0bd4163e6 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/delete.png differ diff --git a/versions/4.0/zh/editor/extension/first/enable-or-not.png b/versions/4.0/zh/editor/extension/first/enable-or-not.png new file mode 100644 index 0000000000..d0c512bbd0 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/enable-or-not.png differ diff --git a/versions/4.0/zh/editor/extension/first/enable.png b/versions/4.0/zh/editor/extension/first/enable.png new file mode 100644 index 0000000000..c49a0d4eb5 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/enable.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-first-panel-create.png b/versions/4.0/zh/editor/extension/first/extension-first-panel-create.png new file mode 100644 index 0000000000..a69964799b Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-first-panel-create.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-first-panel-enable.png b/versions/4.0/zh/editor/extension/first/extension-first-panel-enable.png new file mode 100644 index 0000000000..dd600f3274 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-first-panel-enable.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-first-panel-folder-origin.png b/versions/4.0/zh/editor/extension/first/extension-first-panel-folder-origin.png new file mode 100644 index 0000000000..6ee820b818 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-first-panel-folder-origin.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-first-panel-folder.png b/versions/4.0/zh/editor/extension/first/extension-first-panel-folder.png new file mode 100644 index 0000000000..fa9c130698 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-first-panel-folder.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-first-panel-install.png b/versions/4.0/zh/editor/extension/first/extension-first-panel-install.png new file mode 100644 index 0000000000..fc95c5c9ae Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-first-panel-install.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-first-panel-menu.png b/versions/4.0/zh/editor/extension/first/extension-first-panel-menu.png new file mode 100644 index 0000000000..81ab2017a6 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-first-panel-menu.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-first-panel-sendmsg.png b/versions/4.0/zh/editor/extension/first/extension-first-panel-sendmsg.png new file mode 100644 index 0000000000..39f279bee5 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-first-panel-sendmsg.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-first-panel.png b/versions/4.0/zh/editor/extension/first/extension-first-panel.png new file mode 100644 index 0000000000..de9ffd9ada Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-first-panel.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-hello-world.png b/versions/4.0/zh/editor/extension/first/extension-hello-world.png new file mode 100644 index 0000000000..efb558bf9a Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-hello-world.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-menu-hw.png b/versions/4.0/zh/editor/extension/first/extension-menu-hw.png new file mode 100644 index 0000000000..847403389c Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-menu-hw.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-menu-test.png b/versions/4.0/zh/editor/extension/first/extension-menu-test.png new file mode 100644 index 0000000000..9205042cb1 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-menu-test.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension-package.png b/versions/4.0/zh/editor/extension/first/extension-package.png new file mode 100644 index 0000000000..8977bf3054 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension-package.png differ diff --git a/versions/4.0/zh/editor/extension/first/extension.png b/versions/4.0/zh/editor/extension/first/extension.png new file mode 100644 index 0000000000..aea3df2aa9 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/extension.png differ diff --git a/versions/4.0/zh/editor/extension/first/folder.png b/versions/4.0/zh/editor/extension/first/folder.png new file mode 100644 index 0000000000..ae8f1ec9f6 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/folder.png differ diff --git a/versions/4.0/zh/editor/extension/first/refresh.png b/versions/4.0/zh/editor/extension/first/refresh.png new file mode 100644 index 0000000000..b719f388b2 Binary files /dev/null and b/versions/4.0/zh/editor/extension/first/refresh.png differ diff --git a/versions/4.0/zh/editor/extension/i18n.md b/versions/4.0/zh/editor/extension/i18n.md new file mode 100644 index 0000000000..a9b714f84c --- /dev/null +++ b/versions/4.0/zh/editor/extension/i18n.md @@ -0,0 +1,110 @@ +# 多语言系统(i18n) + +## 什么是 i18n + +**i18n**(其来源是英文单词 internationalization 的首末字符 i 和 n ,18 为中间的字符数)是“国际化”的简称。 + +在资讯领域,i18n 指让产品(出版物,软件,硬件等)无需做太多改变就能够适应不同的语言和地区的需要。 + +在程序开发领域,i18n 则是指在不修改内部代码的情况下,能根据不同语言及地区显示相应的本地化内容。 + +Cocos Creator 扩展系统中内置的多语言方案(i18n)允许扩展配置多份语言的 **键值映射**,并根据编辑器当前的语言设置在扩展里使用不同语言的字符串。 + +> **注意**:本文将介绍如何赋予 **扩展** 多语言的能力,如需在工程内添加运行时的多语言能力,请参考: +> 1. [多语言本地化(L10N)](../l10n/overview.md) +> 2. [i18n 游戏多语言支持](../../advanced-topics/i18n.md) + +## i18n 文件夹 + +要启用多语言功能(以下简称 i18n),需要在扩展的目录下新建一个名叫 i18n 的文件夹,并为每种语言添加一个相应的 `JavaScript` 文件,作为键值映射数据。 + +数据文件名应该和语言的代号一致,如 `en.js` 对应英语映射数据,`zh.js`对应中文映射数据。如下图所示: + +![i18n-folder](./image/i18n-folder.png) + +映射以 `JavaScript` 对象的 key 作为键值,并以模块信息导出,如下所示: + +- `en.js` + + ```javascript + "use strict"; + module.exports = { + open_panel:"Default Panel", + send_to_panel:"Send message to Default Panel", + description:"Extension with a panel" + }; + ``` + +- `zh.js` + + ```javascript + module.exports = { + open_panel:"默认面板", + send_to_panel:"发送消息给面板", + description:"含有一个面板的扩展" + }; + ``` + +以 `open_panel` 为例,假设注册的扩展名字叫做 `first-panel`,则对应的文本翻译 key 为 `first-panel.open_panel`。 + +## 在脚本中使用 + +在 TypeScript 或者 JavaScript 脚本中,可通过 `Editor.I18n.t` 接口获取当前语言对应的翻译后的文本: + +```typescript +let str = Editor.I18n.t('first-panel.open_panel'); +``` + +## 在 HTML 模板中使用 + +在 HTML 模版里需要翻译的话可以使用 ui-label 元素进行翻译: + +```html + +``` + +> **注意**:`ui-label` 是一个普通的行内元素,类似 span。 + +### 在 json 文件中使用 + +例如在扩展包的 `package.json` 中注册菜单路径时,只要这个字段支持 i18n 格式的路径,该路径就可以用 `i18n:${key}` 的形式实现多语言翻译功能。 + +示例1:扩展描述 + +```json5 +// "package_version": 2, +// "version": "1.0.0", +// "name": "first-panel", +"description": "i18n:first-panel.description", +// "main": "./dist/main.js", +``` + +示例2:面板标题 + +```json5 +"panels": { + "default": { + "title": "first-panel Default Panel", + // "type": "dockable", + // "main": "dist/panels/default", + // "size": {...} + } +}, +``` + +示例3:菜单路径与显示内容 + +```json +"menu":[ + { + "path": "i18n:menu.panel/first-panel", + "label": "i18n:first-panel.open_panel", + "message": "open-panel" + }, + { + "path": "i18n:menu.develop/first-panel", + "label": "i18n:first-panel.send_to_panel", + "message": "send-to-panel" + } +] +``` diff --git a/versions/4.0/zh/editor/extension/image/btn-import.png b/versions/4.0/zh/editor/extension/image/btn-import.png new file mode 100644 index 0000000000..6349734047 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/btn-import.png differ diff --git a/versions/4.0/zh/editor/extension/image/category.png b/versions/4.0/zh/editor/extension/image/category.png new file mode 100644 index 0000000000..f23a305b36 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/category.png differ diff --git a/versions/4.0/zh/editor/extension/image/create-extension-menu.png b/versions/4.0/zh/editor/extension/image/create-extension-menu.png new file mode 100644 index 0000000000..3c05598adb Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/create-extension-menu.png differ diff --git a/versions/4.0/zh/editor/extension/image/create-extension-panel.png b/versions/4.0/zh/editor/extension/image/create-extension-panel.png new file mode 100644 index 0000000000..6dbc663601 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/create-extension-panel.png differ diff --git a/versions/4.0/zh/editor/extension/image/create.png b/versions/4.0/zh/editor/extension/image/create.png new file mode 100644 index 0000000000..2c2970a8d3 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/create.png differ diff --git a/versions/4.0/zh/editor/extension/image/dashboard-store.png b/versions/4.0/zh/editor/extension/image/dashboard-store.png new file mode 100644 index 0000000000..aa2e597c21 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/dashboard-store.png differ diff --git a/versions/4.0/zh/editor/extension/image/default-extension-panel.png b/versions/4.0/zh/editor/extension/image/default-extension-panel.png new file mode 100644 index 0000000000..1d45b29cf4 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/default-extension-panel.png differ diff --git a/versions/4.0/zh/editor/extension/image/electron-process.png b/versions/4.0/zh/editor/extension/image/electron-process.png new file mode 100644 index 0000000000..642b4117a3 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/electron-process.png differ diff --git a/versions/4.0/zh/editor/extension/image/enable-extension.png b/versions/4.0/zh/editor/extension/image/enable-extension.png new file mode 100644 index 0000000000..277baf243a Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/enable-extension.png differ diff --git a/versions/4.0/zh/editor/extension/image/enable-first-panel.png b/versions/4.0/zh/editor/extension/image/enable-first-panel.png new file mode 100644 index 0000000000..5113c17750 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/enable-first-panel.png differ diff --git a/versions/4.0/zh/editor/extension/image/ext-detail.png b/versions/4.0/zh/editor/extension/image/ext-detail.png new file mode 100644 index 0000000000..95ae974526 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/ext-detail.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-database.png b/versions/4.0/zh/editor/extension/image/extension-database.png new file mode 100644 index 0000000000..492c2585bf Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-database.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-folder-blank.png b/versions/4.0/zh/editor/extension/image/extension-folder-blank.png new file mode 100644 index 0000000000..2701aad028 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-folder-blank.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-global.png b/versions/4.0/zh/editor/extension/image/extension-global.png new file mode 100644 index 0000000000..384280592b Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-global.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-internal.png b/versions/4.0/zh/editor/extension/image/extension-internal.png new file mode 100644 index 0000000000..9e1d1f90a7 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-internal.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-list.png b/versions/4.0/zh/editor/extension/image/extension-list.png new file mode 100644 index 0000000000..52d06c7ab5 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-list.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-manager-menu.png b/versions/4.0/zh/editor/extension/image/extension-manager-menu.png new file mode 100644 index 0000000000..863e70870e Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-manager-menu.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-manager.png b/versions/4.0/zh/editor/extension/image/extension-manager.png new file mode 100644 index 0000000000..98c9ea92c0 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-manager.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-message-mgr-menu.png b/versions/4.0/zh/editor/extension/image/extension-message-mgr-menu.png new file mode 100644 index 0000000000..0b13c7222c Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-message-mgr-menu.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-message-mgr-panel.png b/versions/4.0/zh/editor/extension/image/extension-message-mgr-panel.png new file mode 100644 index 0000000000..2357044169 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-message-mgr-panel.png differ diff --git a/versions/4.0/zh/editor/extension/image/extension-project.png b/versions/4.0/zh/editor/extension/image/extension-project.png new file mode 100644 index 0000000000..2f66e88892 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/extension-project.png differ diff --git a/versions/4.0/zh/editor/extension/image/i18n-folder.png b/versions/4.0/zh/editor/extension/image/i18n-folder.png new file mode 100644 index 0000000000..b82d28db85 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/i18n-folder.png differ diff --git a/versions/4.0/zh/editor/extension/image/import-drop-menu.png b/versions/4.0/zh/editor/extension/image/import-drop-menu.png new file mode 100644 index 0000000000..48ce19b686 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/import-drop-menu.png differ diff --git a/versions/4.0/zh/editor/extension/image/introduction.png b/versions/4.0/zh/editor/extension/image/introduction.png new file mode 100644 index 0000000000..3c6c37ec04 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/introduction.png differ diff --git a/versions/4.0/zh/editor/extension/image/output.png b/versions/4.0/zh/editor/extension/image/output.png new file mode 100644 index 0000000000..0dafdb8e8a Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/output.png differ diff --git a/versions/4.0/zh/editor/extension/image/preferences-menu.png b/versions/4.0/zh/editor/extension/image/preferences-menu.png new file mode 100644 index 0000000000..d2ebf81094 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/preferences-menu.png differ diff --git a/versions/4.0/zh/editor/extension/image/preferences-tool-custom-laboratory.png b/versions/4.0/zh/editor/extension/image/preferences-tool-custom-laboratory.png new file mode 100644 index 0000000000..76d3405fc2 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/preferences-tool-custom-laboratory.png differ diff --git a/versions/4.0/zh/editor/extension/image/preferences-tool-custom.png b/versions/4.0/zh/editor/extension/image/preferences-tool-custom.png new file mode 100644 index 0000000000..0daceecf34 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/preferences-tool-custom.png differ diff --git a/versions/4.0/zh/editor/extension/image/preferences-tool.png b/versions/4.0/zh/editor/extension/image/preferences-tool.png new file mode 100644 index 0000000000..55b0c1eee3 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/preferences-tool.png differ diff --git a/versions/4.0/zh/editor/extension/image/pricing.png b/versions/4.0/zh/editor/extension/image/pricing.png new file mode 100644 index 0000000000..fb469f478d Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/pricing.png differ diff --git a/versions/4.0/zh/editor/extension/image/project-settings-menu.png b/versions/4.0/zh/editor/extension/image/project-settings-menu.png new file mode 100644 index 0000000000..1826d6e213 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/project-settings-menu.png differ diff --git a/versions/4.0/zh/editor/extension/image/project-settings-panel-custom.png b/versions/4.0/zh/editor/extension/image/project-settings-panel-custom.png new file mode 100644 index 0000000000..71e531437b Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/project-settings-panel-custom.png differ diff --git a/versions/4.0/zh/editor/extension/image/project-settings-panel.png b/versions/4.0/zh/editor/extension/image/project-settings-panel.png new file mode 100644 index 0000000000..1a08df4740 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/project-settings-panel.png differ diff --git a/versions/4.0/zh/editor/extension/image/search.png b/versions/4.0/zh/editor/extension/image/search.png new file mode 100644 index 0000000000..e24a3964db Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/search.png differ diff --git a/versions/4.0/zh/editor/extension/image/store.png b/versions/4.0/zh/editor/extension/image/store.png new file mode 100644 index 0000000000..ec7b3db78b Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/store.png differ diff --git a/versions/4.0/zh/editor/extension/image/submit-for-review.png b/versions/4.0/zh/editor/extension/image/submit-for-review.png new file mode 100644 index 0000000000..139a8992f6 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/submit-for-review.png differ diff --git a/versions/4.0/zh/editor/extension/image/ui-component-menu.png b/versions/4.0/zh/editor/extension/image/ui-component-menu.png new file mode 100644 index 0000000000..d301482e8b Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/ui-component-menu.png differ diff --git a/versions/4.0/zh/editor/extension/image/ui-component.png b/versions/4.0/zh/editor/extension/image/ui-component.png new file mode 100644 index 0000000000..88a46280ab Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/ui-component.png differ diff --git a/versions/4.0/zh/editor/extension/image/upload-store.png b/versions/4.0/zh/editor/extension/image/upload-store.png new file mode 100644 index 0000000000..9f8bcfd8b6 Binary files /dev/null and b/versions/4.0/zh/editor/extension/image/upload-store.png differ diff --git a/versions/4.0/zh/editor/extension/inspector.md b/versions/4.0/zh/editor/extension/inspector.md new file mode 100644 index 0000000000..d895ad28d7 --- /dev/null +++ b/versions/4.0/zh/editor/extension/inspector.md @@ -0,0 +1,250 @@ +# 自定义属性检查器面板 + +有自定义属性检查器需求的开发者建议先参考文档 [通过修饰器定义属性](../../scripting/decorator.md),若该文档满足需求,建议优先使用该文档中的方法。 + +**属性检查器** 作为 Cocos Creator 里显示当前选中状态的模块,为大家提供了一些基础的扩展能力。 + +在 **属性检查器** 里,定义了两个层级的数据: + +1. 选中的物体主类型 +2. 渲染主类型内容时,内容里包含的子数据类型 + +当在 **层级管理器**/**资源管理器** 中选中一个 **节点**/**资源** 时,Cocos Creator 会将物体被选中的消息进行广播。当 **属性检查器** 接收到消息后,会检查被选中的物体的类型,例如选中的是节点,那么类型便是 `node`。 + +针对两种类型,允许注册两种渲染器: +1. 主类型渲染器 +2. 主类型渲染器接收数据开始渲染的时候,允许附带子类型渲染器 + +示例里选中的是 `node`,`node` 上携带了多个 `component`,所以主类型就是 `node`,而子类型则是 `component`。 + +在 **属性检查器** 收到选中物体的广播消息后,首先确定类型,而后 **属性检查器** 会将收到的数据(物体的 uuid),传递给 `node` 渲染器,将渲染权限完全移交。 + +而在 `node` 渲染器里,会根据自身的实现,渲染到每个 `组件` 的时候,将该区域的渲染权限交给 `子类型渲染器`。大部分情况下,我们不需要关注这些。我们先来看看几种常用的的自定义方式吧。 + +## 自定义 Component 渲染 + +默认提供的组件渲染器有时候并不能满足我们的需求,这时候我们就需要自定义一个组件的渲染方式。 + +首先在项目内新建一个脚本组件 `CustomLabelComponent.ts`,并添加一个字符串属性,内容如下: + +```typescript + +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('CustomLabelComponent') +export class CustomLabelComponent extends Component { + @property + label = ''; + + start () { + + } +} + +``` + +将组件拖到节点上,这时候能看到组件上有一个输入框。 + +新建一个扩展,在扩展的 `package.json` 里注册如下的 `contributions.inspector` 信息: + +```json +{ + "contributions": { + "inspector": { + "section": { + "node": { + "CustomLabelComponent": "./dist/contributions/inspector/comp-label.js" + } + } + } + } +} +``` + +### 自动渲染 + +编写一个 `src/contributions/inspector/comp-label.ts` 文件,内容如下: + +```typescript +'use strict'; + +import { methods } from "../../main"; + +type Selector<$> = { $: Record } + +export const template = ` + +`; + +export const $ = { + test: '.test', +}; + +export function update(this: Selector & typeof methods, dump: any) { + // 使用 ui-porp 自动渲染,设置 prop 的 type 为 dump + // render 传入一个 dump 数据,能够自动渲染出对应的界面 + // 自动渲染的界面修改后,能够自动提交数据 + this.$.test.render(dump.value.label); +} +export function ready(this: Selector & typeof methods) {} +``` + +编译并刷新扩展后,我们可以发现 `CustomLabelComponent` 组件的渲染被接管了。 + +> **注意**:每一个 `ui-prop` 对应一条属性,若要显示多条属性需要定义多个 `ui-prop`。 + +### 手动渲染 + +上面的自动渲染示例中,我们使用了类型为 `dump` 的特殊的 `ui-prop` 进行渲染数据提交,它使我们可以快捷的接管组件的渲染,但若面临一些极端情况却很难处理一些细节问题,此时可以切换到手动渲染模式,代码如下所示: + +```typescript +'use strict'; + +type Selector<$> = { $: Record } + +export const template = ` + + + + + +`; + +export const $ = { + label:'.label', + test: '.test', + testInput: '.test-input', +}; + +type PanelThis = Selector & { dump: any }; + +export function update(this: PanelThis, dump: any) { + // 缓存 dump 数据,请挂在 this 上,否则多开的时候可能出现问题 + this.dump = dump; + // 将 dump 数据传递给帮忙提交数据的 prop 元素 + this.$.test.dump = dump.value.label; + // 更新负责输入和显示的 input 元素上的数据 + this.$.testInput.value = dump.value.label.value; + this.$.label.value = dump.value.label.name; +} +export function ready(this: PanelThis) { + // 监听 input 上的提交事件,当 input 提交数据的时候,更新 dump 数据,并使用 prop 发送 change-dump 事件 + this.$.testInput.addEventListener('confirm', () => { + this.dump.value.label.value = this.$.testInput.value; + this.$.test.dispatch('change-dump'); + }); +} +``` + +手动渲染模式下依然要通过一个类型为 `dump` 的 `ui-prop` 元素进行数据提交。但 `template` 中的 `html` 布局就是一个完全可以自由掌控的内容,可根据需求制定出十分复杂的显示方式。 + +## 自定义 Asset 渲染 + +当在资源管理窗口(Assets)中选中文件时,属性检查器窗口会显示当前选中文件的重要属性,如果默认显示的信息不满足要求,我们也可以自定义渲染内容。 + +在 `package.json` 中添加 `contributions.section.asset` 字段,并定义对应资源类型的自定义渲染脚本,如下所示: + +```json +{ + "contributions": { + "inspector": { + "section": { + "asset": { + "effect": "./dist/contributions/inspector/asset-effect.js" + } + } + } + } +} +``` + +`effect` 表示我们要对 Cocos Shader(*.effect) 文件类型的资源属性面板自定义渲染。常见的资源文件类型如下: +- `scene` - 场景文件 +- `typescript` - TypeScript 脚本文件 +- `prefab` - 预制体文件 +- `fbx` - FBX 文件 +- `material` - 材质文件 +- `directory` - 文件夹 +- `image` - 图片文件 + +可通过查看文件对应的 `*.meta` 中的 `importer` 字段获取该文件的类型定义。 + +接下来,在扩展目录下新建一个 `src/contributions/inspector/asset-effect.ts` 脚本文件,并编写如下代码: + +```typescript +'use strict'; + +interface Asset { + displayName: string; + file: string; + imported: boolean; + importer: string; + invalid: boolean; + isDirectory: boolean; + library: { + [extname: string]: string; + }; + name: string; + url: string; + uuid: string; + visible: boolean; + subAssets: { + [id: string]: Asset; + }; +} + +interface Meta { + files: string[]; + imported: boolean; + importer: string; + subMetas: { + [id: string]: Meta; + }; + userData: { + [key: string]: any; + }; + uuid: string; + ver: string; +} + +type Selector<$> = { $: Record } & { dispatch(str: string): void, assetList: Asset[], metaList: Meta[] }; + +export const $ = { + 'test': '.test', +}; + +export const template = ` + + Test + + +`; + +type PanelThis = Selector; + +export function update(this: PanelThis, assetList: Asset[], metaList: Meta[]) { + this.assetList = assetList; + this.metaList = metaList; + this.$.test.value = metaList[0].userData.test || false; +}; + +export function ready(this: PanelThis) { + this.$.test.addEventListener('confirm', () => { + this.metaList.forEach((meta: any) => { + // 修改对应的 meta 里的数据 + meta.userData.test = !!this.$.test.value; + }); + // 修改后手动发送事件通知,资源面板是修改资源的 meta 文件,不是修改 dump 数据,所以发送的事件和组件属性修改不一样 + this.dispatch('change'); + }); +}; + +export function close(his: PanelThis, ) { + // TODO something +}; +``` + +编译、刷新扩展后,回到 Cocos Creator 编辑器界面,选中某个 `Cocos Shader` 文件,可以在属性检查器底部发现,多了一个 **Test 复选框**。 + +> **注意**:多个扩展注册的数据是并存的。如果一个 **组件**/**资源** 已经有了自定义渲染器,那么再次注册的自定义渲染器都会附加在后面。如果一个 **组件**/**资源** 没有内置自定义渲染器,使用的是默认的渲染器,那么当扩展注册自定义渲染器的时候,会完全接管渲染内容。 diff --git a/versions/4.0/zh/editor/extension/install.md b/versions/4.0/zh/editor/extension/install.md new file mode 100644 index 0000000000..4ab2377f8d --- /dev/null +++ b/versions/4.0/zh/editor/extension/install.md @@ -0,0 +1,79 @@ +# 安装与分享 + +## 安装位置 + +Cocos Creator 在启动项目的过程中会搜索并加载 **项目** 路径下的扩展。 + +### 项目 + +扩展存放的地址为: + +- `${你的项目地址}/extensions` + +## 安装扩展 + +可以通过三种方式获得扩展: +- 其他开发者打包分享,请参考下文 [打包扩展](#%E6%89%93%E5%8C%85%E6%89%A9%E5%B1%95)。 + +- 从 **Dashboard->商城** 下载。 + + ![dashboard-store](./image/dashboard-store.png) + +- 从 [Cocos Store·资源商城](http://store.cocos.com) 页面下载。 + +获得扩展压缩包(zip 文件)后,在编辑器顶部菜单栏中点击 **扩展 -> 扩展管理器**: + +![extension-manager-menu](image/extension-manager-menu.png) + +点击后可打开如下图所示的 **扩展管理器** 面板。 + +![extension-manager](image/extension-manager.png) + +在 **扩展管理器** 中选择 **项目**(上图所示 1) 选项卡,点击 ![import](image/btn-import.png) (上图所示 3) 按钮。 + +在弹出的文件选择框中选择要导入的扩展压缩包,点击 **打开** 按钮即可导入。 + +导入的扩展压缩包会被解压并放到指定的 [安装位置](#%E5%AE%89%E8%A3%85%E4%BD%8D%E7%BD%AE)。 + +最后在 **扩展管理器** 对应的 **项目** 选项卡中找到扩展,点击右边的 **启用** 按钮,刚刚导入的扩展就可以正常运行了。如下图所示: + +![enable-extension](image/enable-extension.png) + +对于解压后的扩展,也可以通过点击 ![import](image/btn-import.png) 右侧的下拉按钮导入。 + +![import](image/import-drop-menu.png) + +## 卸载已安装的扩展 + +在 **扩展管理器** 中找到需要删除的扩展,点击 **删除按钮** ![delete](first/delete.png) 即可,同时扩展所在的文件夹也会删除。如果只需要禁用,可以只选择 "关闭"。 + +## 重载扩展 + +若扩展的内容有修改,是不会自动更新的,这时候就需要在编辑器内手动重新载入一次扩展。 + +在 **扩展管理器** 中找到对应的扩展,然后点击 **重新载入按钮** ![refresh](first/refresh.png),这时候编辑器中的扩展将使用最新的代码和文件重新运行。 + +## 打包扩展 + +当编写好一个扩展后,如果要将扩展分享给其使用者,则需要将该扩展打包为 zip 格式的压缩包。 + +我们以 `first-panel` 扩展为例,其目录结构如下: + +![extension-first-panel-folder-origin](./first/extension-first-panel-folder-origin.png) + +进入扩展根目录,选择相应文件并将所有文件压缩成 zip 包,如下图所示(截图为 macOS 系统,其余平台同理): + +![extension-first-panel-install](./first/extension-first-panel-install.png) + +上图中所选文件(夹)为必选,缺一不可,它们作用如下: +- `dist` - 生成的 javascript 代码。 +- `i18n` - 多语言配置。 +- `node_modules` - 依赖的 Node.js 模块。 +- `package.json` - 扩展描述文件。 +- `static` - 静态资源文件。 + +将压缩包命名为 `first-panel.zip`(建议与扩展文件夹同名),分享给其他人或者上传 Cocos Store 即可完成分享。 + +> **注意**:要在扩展的目录下进行文件选择操作,否则可能导致目录结构不对。 + +若想上架扩展到 [资源商店(Cocos Store)](https://store.cocos.com),请参考文档 [上架扩展到资源商店](./store/upload-store.md)。 diff --git a/versions/4.0/zh/editor/extension/message-manager-2.png b/versions/4.0/zh/editor/extension/message-manager-2.png new file mode 100644 index 0000000000..d2fa10a76b Binary files /dev/null and b/versions/4.0/zh/editor/extension/message-manager-2.png differ diff --git a/versions/4.0/zh/editor/extension/message-manager.png b/versions/4.0/zh/editor/extension/message-manager.png new file mode 100644 index 0000000000..8259a524ef Binary files /dev/null and b/versions/4.0/zh/editor/extension/message-manager.png differ diff --git a/versions/4.0/zh/editor/extension/messages.md b/versions/4.0/zh/editor/extension/messages.md new file mode 100644 index 0000000000..e5c26640ff --- /dev/null +++ b/versions/4.0/zh/editor/extension/messages.md @@ -0,0 +1,176 @@ +# 消息系统 + +Cocos Creator 中有许多独立运行进程,且这些进程间是相互隔离的。在编辑器内需要与其他功能进行交互的时候,需要通过 "消息机制" 进行交互。 + +编辑器里的"消息系统"是 IPC(进程间通信)的功能扩展封装。这个系统承担起了整个编辑器内通讯交互的重担。 + +更多关于多进程构架和跨进程通信的介绍请参考文档 [基础结构](./package.md). + +## 消息类型 + +Cocos Creator 系统内的消息有两种类型: + +1. 普通消息:主动发送某条消息到某个功能(扩展) +2. 广播消息:某个功能(扩展)完成了一个操作后向所有人发送通知,告知操作已经完成 + +### 普通消息 + +可以理解成一种对外的接口,例如引擎的 **场景编辑器** 模块已经定义好了一个用于查询节点的 `query-node` 消息,如下所示: + +```json +{ + "name": "scene", + "contributions": { + "messages": { + "query-node": { + "methods": ["queryNode"] + } + } + } +} +``` + +关于如何自定义消息以及消息各字段的含义,请参考文档 [自定义消息](./contributions-messages.md)。 + +当我们在自己编写的扩展中想要查询场景节点时,就可以使用这个消息来完成,如下所示: + +```typescript +const info = await Editor.Message.request('scene', 'query-node', uuid); +``` + +这种消息类似一种远程调用 (RPC), 拿到的 `info` 对象就是实际查询的节点上的部分数据。 + +> **注意**:由于是远程调用,`request` 是不会立即返回的,因此需要使用 `await` 将异步转为同步。 + +#### 普通消息的命名规范 + +请使用 **小写** 单词,并且不能包含特殊字符,单词间以 **-** 连接。如 `open-panel`、`text-changed`。 + +### 广播消息 + +广播消息是某一个功能内的操作完成后,对外进行的一种通知。 + +比如,**场景编辑器** 在启动一个场景后,需要通知所有人 "场景" 已经启动完毕,**场景编辑器** 发送广播消息使用的是如下代码: + +```typescript +Editor.Message.broadcast('scene:ready', sceneUUID); +``` + +#### 接收广播消息 + +若一个扩展想要接收 `scene:ready` 消息,则需要在 `package.json` 里先定义,如下所示: + +```json +{ + "name": "hello-world", + "contributions": { + "messages": { + "scene:ready": { + "methods": ["initData"] + } + } + } +} +``` + +每当场景准备就绪后,广播的 `scene:ready` 消息就会触发 "hello-world" 扩展里的 `initData` 方法。 + +#### 发送广播消息 + +若一个扩展想要发送广播消息,也需要在 `package.json` 里先定义。 + +比如,"hello-world" 在准备好数据后,同样可以向外广播一条消息,以方便其他扩展与之配合。如下所示: + +```json +{ + "name": "hello-world", + "contributions": { + "messages": { + "scene:ready": { + "methods": ["initData"] + }, + "hello-world:ready": { + "public": true, + "description": "hello-world ready notification." + } + } + } +} +``` + +在适当的时机,"hello-world" 扩展内调用如下代码即可广播给所有人: + +```typescript +Editor.Message.broadcast('hello-world:ready'); +``` + +> **注意**:广播消息可以没有 `methods`,当一条消息没有 methods 的时候,不会触发处理函数,我们可以给他增加 public 字段,让它出现在 [**消息列表面板**](./messages.md#查看消息列表)。如上面的定义所示,表示 `hello-world:ready` 没有触发函数,但是会在 [**消息列表面板**](./messages.md#查看消息列表) 上显示。 + +#### 广播消息的命名规范 + +格式为 `packageName:actionName`,以下命名都是合法的: +- scene:ready +- scene:query-node +- hello-world:ready +- hello-world:data-loaded + +加上 `packageName` 可以防止命名出现冲突,在 `package.json` 中定义消息的时候也能够更加直观的看到监听的是哪一个扩展的哪个广播消息(动作)。 + +## 查看消息列表 + +编辑器内的功能以及扩展对外开放的消息列表,可以通过 **开发者 -> 消息管理** 面板查看。详细定义规则请参考文档 [自定义消息](./contributions-messages.md)。 + +## 在代码中发送消息 + +`send` 方法只发送消息,并不会等待返回。如果不需要返回数据,且不关心是否执行完成,请使用这个方法。 + +```typescript +Editor.Message.send(pkgName, message, ...args); +``` + +`request` 方法返回一个 promise 对象,这个 promise 会接收消息处理后返回的数据。 + +```typescript +await Editor.Message.request(pkgName, message, ...args); +``` + +`broadcast` 方法只发送,并且发送给所有监听对应消息的功能扩展。 + +```typescript +Editor.Message.broadcast(`${pkgName}:${actionName}`, ...args); +``` + +## 查看公开消息列表 + +在编辑器的顶部菜单栏中找到 **开发者** -> **消息列表**,可以打开消息管理面板。 + +![extension-message-mgr-menu](./image/extension-message-mgr-menu.png) + +面板里显示了编辑器各系统公开的消息以及其说明。 + +![extension-message-mgr-panel](./image/extension-message-mgr-panel.png) + +这个面板里包含了几个部分。 + +最左侧是扩展列表,也就是提供消息接口的扩展的名字。 + +右侧分成了两部分, 通过 tab 页面区分出了 Mesasage 和 Broadcast。 + +Message 是普通消息,我们可以通过 `Editor.Message.send` 和 `Editor.Message.request` 发送到这个扩展上执行操作。 + +Broadcast 则是这个扩展可能对外发出的广播消息,我们需要在 `contributions.messages` 里监听这个广播消息: + +```json +{ + "name": "hello-world", + "contributions": { + "messages": { + "scene:ready": { + "methods": ["initData"] + } + } + } +} +``` + +我们可以在自己的扩展里注册消息并展示到面板,只需要在消息定义里标记 `public` 字段为 `true`,详看[自定义消息](./contributions-messages.md) diff --git a/versions/4.0/zh/editor/extension/package.md b/versions/4.0/zh/editor/extension/package.md new file mode 100644 index 0000000000..744bc09a97 --- /dev/null +++ b/versions/4.0/zh/editor/extension/package.md @@ -0,0 +1,41 @@ +# 基础结构 + +在编写扩展之前,我们首先需要了解一下 Cocos Creator 内,扩展的基础结构。 + +## Electron + +Cocos Creator 编辑器是基于 GitHub 的 [Electron](https://www.electronjs.org/) 内核开发。 + +Electron 是一个集成了 [Node.js](https://nodejs.org/) 和 [Google Chromium](https://github.com/chromium/chromium) 的跨平台开发框架。 + +## 多进程机制 + +在 Electron 的架构中,一份应用程序由主进程和渲染进程组成,其主进程负责管理平台相关的调度,如窗口的开启关闭、菜单选项、基础对话框等等。而每一个新开启的窗口就是一个独立的渲染进程。每个进程独立享有自己的 JavaScript 内容,且彼此之间无法直接访问。当需要在进程之间传递数据时,需要使用进程间通信(IPC)机制。 + +相关功能可以通过阅读 [Electron's introduction document](https://github.com/atom/electron/blob/master/docs/tutorial/quick-start.md) 更深入的理解 Electron 中的主进程和渲染进程的关系。 + +简单点说,Electron 的主进程相当于一个 Node.js 服务端程序,而每一个窗口(渲染进程)则相当于一份客户端网页程序。 + +Cocos Creator 编辑器沿用了 Electron 的主进程和渲染进程的结构设计。所以扩展在编辑器内启动并运行的时候,扩展定义的 main 其实是在主进程启动,而 panels 定义的面板,则在渲染进程启动。进程结构简要概括如下: + +![electron-process](./image/electron-process.png) + +## 进程间通信 + +进程间通信实际上就是在一个进程中发消息,然后在另外一个进程中监听消息的过程。 + +Electron 提供了进程间通信对应的模块 `ipcMain` 和 `ipcRenderer` 来帮助我们完成这个任务。 + +由于这两个模块仅完成了非常基本的通信功能,并不能满足编辑器,扩展面板与主进程之间的通信需求,所以 Cocos Creator 在这之上又进行了封装,扩展了进程间消息收发的方法,方便扩展开发者和编辑器开发者制作更多复杂情景。更多说明请查看文档 [消息系统](./messages.md)。 + +## 扩展的能力 + +扩展内拥有完整的 Node.js 环境,可以很方便的使用 [npm](https://www.npmjs.com/) 市场上大量的工具,用于完成自己想要的功能。[详细参考](../npm.md) + +如果需要和其他功能交互,则需要对应功能开放对应的操作消息,我们在自己的扩展内,通过 [消息系统](./messages.md) 触发、查询和处理编辑器内的功能或者数据。 + +已经开放的消息列表可以在顶部菜单 [**开发者 -> 消息列表**](./messages.md#查看消息列表) 面板里查看,如下图所示: + +![extension-message-mgr-menu](./image/extension-message-mgr-menu.png) + +![extension-message-mgr-panel](./image/extension-message-mgr-panel.png) diff --git a/versions/4.0/zh/editor/extension/panel-boot.md b/versions/4.0/zh/editor/extension/panel-boot.md new file mode 100644 index 0000000000..ac69f0f76b --- /dev/null +++ b/versions/4.0/zh/editor/extension/panel-boot.md @@ -0,0 +1,326 @@ +# 编写面板 + +我们已经在 [package.json](./panel.md) 里写好了面板的定义,这时候就需要实现面板的逻辑功能了。 + +这时候就需要在 panel 定义中标识 main 入口文件,并填充其内容: + +Javascript + +```javascript +'use strict'; + +// HTML 文本 +exports.template = ''; +// 样式文本 +exports.style = ''; +// 渲染后 HTML 选择器 +exports.$ = {}; +// 面板上的方法 +exports.methods = {}; +// 面板上触发的事件 +exports.listeners = {}; + +// 当面板渲染成功后触发 +exports.ready = async function() {}; +// 尝试关闭面板的时候触发 +exports.beforeClose = async function() {}; +// 当面板实际关闭后触发 +exports.close = async function() {}; +``` + +Typescript + +```typescript +'use strict'; + +// HTML 文本 +export const template = ''; +// 样式文本 +export const style = ''; +// 渲染后 HTML 选择器 +export const $ = {}; +// 面板上的方法 +export const methods = {}; +// 面板上触发的事件 +export const listeners = {}; + +// 当面板渲染成功后触发 +export async function ready() {}; +// 尝试关闭面板的时候触发 +export async function beforeClose() {}; +// 当面板实际关闭后触发 +export async function close() {}; +``` + +如果使用的是 Typescript,这时候 ready 等函数内识别的 this 不正确,我们可以加上 this 定义: + +```typescript +'use strict'; + +type Selector<$> = { $: Record } + +export const $ = { + test: '.test', +}; + +export const methods = { + update() {}, +}; + +export async function ready(this: Selector & typeof methods) { + this.update(); +}; +``` + +也可以使用 Editor.Pabel.define 常见 panel 对象: + +```typescript +module.exprots = Editor.Panel.define({ + methods: { + update() {}, + }, + ready() { + this.update(); + }, +}); +``` + +`Editor.Panel.define` 是 v3.3 新增的接口。 + +## template + +html 字符串,例如: + +Javascript + +```javascript +exports.template = ` +
+ Header +
+
+ Section +
+`; +``` + +Typescript + +```typescript +export const template = ` +
+ Header +
+
+ Section +
+`; +``` + +也可以直接读取一个 HTML 文件: + +Javascript + +```javascript +const { readFileSync } = require('fs'); +const { join } = require('path'); +exports.template = readFileSync(join(__dirname, '../static/default.html'), 'utf8'); +``` + +Typescript + +```typescript +import { readFileSync } from 'fs'; +import { join } from 'path'; +export const template = readFileSync(join(__dirname, '../static/default.html'), 'utf8'); +``` + +当定义好 template 后,面板被打开的时候,将自动把 template 的内容渲染到界面上。 + +此外编辑器也提供了一些 custom element,可以参考 [UI 组件](./ui.md) 使用。 + +## style + +有了 HTML,还需要自定义一些样式就需要使用 style 了,style 和 template 一样是一个字符串。 + +Javascript + +```javascript +exports.style = ` +header { padding: 10px; } +`; +``` + +Typescript + +```typescript +export const style = ` +header { padding: 10px; } +`; +``` + +当然,也可以读取一个 css 文件: + +Javascript + +```javascript +const { readFileSync } = require('fs'); +const { join } = require('path'); +exports.style = readFileSync(join(__dirname, '../static/default.css'), 'utf8'); +``` + +Typescript + +```typescript +import { readFileSync } from 'fs'; +import { join } from 'path'; +export const style = readFileSync(join(__dirname, '../static/default.css'), 'utf8'); +``` + +## $ + +这是一个 HTML 元素选择器,直接调用 querySelector 查找到指定元素后,作为一个快捷方式使用。 + +Javascript + +```javascript +exports.$ = { + header: 'header', + test: '.test', +}; +``` + +Typescript + +```typescript +export const $ = { + header: 'header', + test: '.test', +}; +``` + +首先定义好选择器,编辑器会在 template 渲染完成后,自动调用 document.querySelector 找到对应的元素,并挂在 this.$ 上: + +Javascript + +```javascript +exports.ready = function() { + console.log(this.$.header); //
+ console.log(this.$.test); //
+} +``` + +Typescript + +```typescript +export function ready() { + console.log(this.$.header); //
+ console.log(this.$.test); //
+} +``` + +## methods + +面板上定义的方法。面板对外的功能都需要封装成方法,以函数为单位对外提供。消息也可以直接触发面板上的方法,详细请参考 [自定义消息](./contributions-messages.md)。 + +这个对象里都是函数,请不要挂载其他类型的对象到这里。 + +Javascript + +```javascript +const packageJSON = require('./package.json'); +exports.methods = { + open() { + Editor.Panel.open(packageJSON.name); + }, +}; +``` + +Typescript + +```typescript +import { name } from './package.json'; +export const methods = { + open() { + Editor.Panel.open(packageJSON.name); + }, +}; +``` + +## listeners + +基础的布局完成后,我们有时候需要根据一些情况,去更新一些面板上的状态,这时候就需要使用 listeners 功能了。 + +Javascript + +```javascript +exports.listeners = { + /** + * 面板隐藏的时候触发 + */ + hide() { + console.log(`hide: ${this.hidden}`); + }, + /** + * 面板显示的时候触发 + */ + show() { + console.log(`hide: ${this.hidden}`); + }, + /** + * 面板大小更改的时候触发 + */ + resize() { + console.log(`height: ${this.clientHeight}`); + console.log(`width: ${this.clientWidth}`); + }, +}; +``` + +Typescript + +```typescript +interface PanelInfo { + hidden: boolean; + clientHeight: number; + clientWidth: number; +} + +export const listeners = { + /** + * 面板隐藏的时候触发 + */ + hide(this: PanelInfo) { + console.log(`hide: ${this.hidden}`); + }, + /** + * 面板显示的时候触发 + */ + show(this: PanelInfo) { + console.log(`hide: ${this.hidden}`); + }, + /** + * 面板大小更改的时候触发 + */ + resize(this: PanelInfo) { + console.log(`height: ${this.clientHeight}`); + console.log(`width: ${this.clientWidth}`); + }, +}; +``` + +## ready + +当面板启动完成的时候,将会触发这个生命周期函数。 + +## beforeClose + +当面板尝试被关闭的时候,将会触发这个函数,beforeClose 可以是一个 async 函数,可以进行异步判断,如果 return false,则会终止当前的关闭操作。 + +请不要在这个函数里执行实际的销毁和关闭相关的逻辑代码,这一步骤只是进行询问,实际的销毁请放到 close 函数里。 + +**请谨慎使用** 如果判断错误,可能导致编辑器或者面板窗口无法正常关闭。 + +## close + +当窗口内的所有面板允许关闭后,会触发面板的 close,一旦触发 close,结束后将强制关闭窗口,所以请在 close 进行数据的保存,如果出现异常关闭,请做好数据的备份,以便在重新启动的时候尽可能恢复数据。 diff --git a/versions/4.0/zh/editor/extension/panel-messages.md b/versions/4.0/zh/editor/extension/panel-messages.md new file mode 100644 index 0000000000..54f818a785 --- /dev/null +++ b/versions/4.0/zh/editor/extension/panel-messages.md @@ -0,0 +1,160 @@ +# 面板与扩展的通信 + +> **注意**:在 v3.5 版本后,我们对扩展文档进行了更新,因此该文档已废弃,请移步 [消息系统](./messages.md) 或 [自定义消息](./contributions-messages.md) 获取更多信息。如果您在线上看到此文档,请在 [github](https://github.com/cocos/cocos-docs/issues/new) 发起 issue,告知官方人员处理。 + +一些实用工具或者是简单的功能可以直接写在面板上,但是面板不是可靠的数据存储位置,窗口随时可能被关闭,面板也会被关闭。 + +最常见的例子就是某个面板被拖拽停靠到主窗口里。这时候面板会先关闭,然后在主窗口内重新打开,而面板上使用的内存里的数据如果不进行存储和备份,则会随着重启而丢失。 + +这时候就需要与扩展主体进行一定程度的数据交互。 + +在看这章节前,需要对 [消息系统](./messages.md) 有一定程度的了解。 + +## 定义扩展上和面板的方法 + +首先我们定义一份 package.json: + +```json +{ + "name": "hello-world", + "main": "./browser.js", + "panels": { + "default": { + "title": "hw", + "main": "./panel.js" + } + }, + "contributions": { + "messages": { + "upload": { + "methods": ["saveData"] + }, + "query": { + "methods": ["queryData"] + } + } + } +} +``` + +然后定义扩展的 main 文件 `browser.js`: + +Javascript + +```javascript +exports.methods = { + saveData(path, data) { + // 收到数据后缓存起来 + this.cache[path] = data; + }, + queryData(path) { + const result = this.cache[path]; + delete this.cache[path]; + return result; + }, +}; + +exports.load = function() {}; +exports.unload = function() {}; +``` + +Typescript + +```typescript +interface PackagelItem { + cache: { + [path: string]: any; + } +} +export const methods = { + saveData(this: PackagelItem, path: string, data: any) { + // 收到数据后缓存起来 + this.cache[path] = data; + }, + queryData(this: PackagelItem, path: string) { + const result = this.cache[path]; + delete this.cache[path]; + return result; + }, +}; + +export function load() {}; +export function unloal() {}; +``` + +然后定义面板的 main 文件: + +Javascript + +```javascript +const packageJSON = require('./package.json'); +exports.ready = async () => { + const tab = await Editor.Message.request(packageJSON.name, 'query', 'tab'); + const subTab = await Editor.Message.request(packageJSON.name, 'query', 'subTab'); + // 打印查询到的数据 + console.log(tab, subTab): + // TODO 使用这两个数据初始化 +}; +exports.close() { + // 收到数据后上传到扩展进程 + Editor.Message.send(packageJSON.name, 'upload', 'tab', 1); + Editor.Message.send(packageJSON.name, 'upload', 'subTab', 0); +}; +``` + +Typescript + +```typescript +import { name } from './package.json'; +exports.ready = async () => { + const tab = await Editor.Message.request(name, 'query', 'tab'); + const subTab = await Editor.Message.request(name, 'query', 'subTab'); + // 打印查询到的数据 + console.log(tab, subTab): + // TODO 使用这两个数据初始化 +}; +exports.close() { + // 收到数据后上传到扩展进程 + Editor.Message.send(name, 'upload', 'tab', 1); + Editor.Message.send(name, 'upload', 'subTab', 0); +}; +``` + +## 发送消息 + +当定义好扩展和扩展里的面板后,就可以尝试触发这些消息。 + +按下 **ctrl(cmd) + shift + i** 打开控制台。在控制台打开面板: + +```javascript +// default 可以省略,如果面板名字是非 default,则需要填写 'hello-world.xxx' +Editor.Panel.open('hello-world'); +``` + +打开面板后,控制台会打印出一句: + +```sh +undefined, undefined +``` + +这是因为数据还没有提交。关闭这个面板,然后再次打开,这时候控制台打印出了数据: + +```sh +1, 0 +``` + +这是因为面板在关闭的时候,发送了两条消息: + +```javascript +Editor.Message.send(packageJSON.name, 'upload', 'tab', 1); +Editor.Message.send(packageJSON.name, 'upload', 'subTab', 0); +``` + +通过这两条消息,Message 系统首先根据 messages 里的 upload 定义 `"methods": ["saveData"]`,将数据保存到扩展进程里。当再次打开面板时,通过以下代码查询到刚刚保存的数据,并初始化界面、打印到控制台: + +```javascript +const tab = await Editor.Message.send(packageJSON.name, 'query', 'tab'); +const subTab = await Editor.Message.send(packageJSON.name, 'query', 'subTab'); +``` + +至此,我们完成了一次面板与扩展进程的交互。 diff --git a/versions/4.0/zh/editor/extension/panel.md b/versions/4.0/zh/editor/extension/panel.md new file mode 100644 index 0000000000..d61b94c118 --- /dev/null +++ b/versions/4.0/zh/editor/extension/panel.md @@ -0,0 +1,194 @@ +# 面板系统 + +扩展默认情况下是没有界面显示的,如果一个扩展需要实现界面交互,就需要使用到面板系统相关功能。 + +## 面板的定义 + +在 `package.json` 里可以在 `panels` 字段定义一个或者多个面板,如下所示: + +```json +{ + "name": "hello-world", + "panels": { + "default": { + "title": "world panel", + "type": "dockable", + "main": "./dist/panels/default", + "icon": "./static/default.png" + }, + "list": { + "title": "world list", + "type": "simple", + "main": "./dist/panels/list", + "icon": "./static/list.png", + + "flags": {}, + "size": {} + } + } +} +``` + +我们定义了两个面板:`defualt` 和 `list`。`default` 为默认面板,当不指名特定面板的时候,它就做为默认操作对象。 + +面板各字段含义如下: +- `title`:string - 面板标题,支持 i18n:key,必填 +- `main`:string - 面板源码相对目录,必填 +- `icon`:string - 面板图标相对目录,必填 +- `type`:string - 面板类型(dockable | simple),可选 +- `flags`:{} - 标记,可选 + - resizable - 是否可以改变大小,默认 true,可选 + - save - 是否需要保存,默认 false,可选 + - alwaysOnTop - 是否保持顶层显示,默认 flase,可选 +- `size`:{} - 大小信息,可选 + - min-width:Number - 最小宽度,可选 + - min-height:Number - 最小高度,可选 + - width:Number - 面板默认宽度,可选 + - height:Number - 面板默认高度,可选 + +## 编写面板 + +在扩展根目录下分别建立两个文件 `src/panels/default/index.ts` 和 `src/panels/list/index.ts`,并将下面的最简面板模板代码分别贴到两个 `index.ts` 文件中: + +```typescript + +module.exports = Editor.Panel.define({ + listeners: { + show() { console.log('show'); }, + hide() { console.log('hide'); }, + }, + template: '
Hello
', + style: 'div { color: yellow; }', + $: { + elem: 'div', + }, + methods: { + + }, + ready() { + + }, + beforeClose() { }, + close() { }, +}); +``` + +`listeners` - 面板的一些事件监听 +`template` - 面板的 HTML 布局文件 +`style` - 面板的 css 文件 +`$` - 全局选择器,用于快速访问一些元素 +`methods` - 此面板对外的方法接口 +`ready` - 当面板被打开时会调用 +`beforeClose` - 面板在关闭前会调用 +`close` - 面板在关闭后会调用 + +## 显示面板 + +可以使用 `Editor.Panel.open` 方法打开任何面板(本扩展自己的面板以及其他扩展的面板)。 + +假设扩展为 `hello-world`,那以下两种方式都可以打开默认面板: + +```typescript +// Editor.Panel.open('hello-world.defualt'); +Editor.Panel.open('hello-world'); +``` + +打开其它面板的方式为: + +```typescript +// Editor.Panel.open('{extension-name}.panelName'); +Editor.Panel.open('hello-world.list'); +``` + +## 通信交互 + +Cocos Creator 扩展系统基于 Electron 的多进程方式构建,每一个扩展是一个独立的进程,扩展中的每一个面板,也是一个独立的进程。 因此,扩展与面板之间、面板与面板的交互只能通过进程间通信(IPC)实现。 详细信息请参考文档 [消息系统](./messages.md)。 + +### 面板向外发送消息 + +由于面板关闭后,进程也会退出,因此我们通常将扩展作为内存数据的载体。面板中需要用到的数据和逻辑接口一般会从扩展主进程里获取。 + +如果想 **查询** 和 **设置** 位于扩展主进程中的数据,假设扩展中定义了 `queryData` 和 `saveData` 两个消息,我们可以这样使用: + +```typescript +const data = await Editor.Message.request(packageName, 'queryData', dataName); +await Editor.Message.request(packageName, 'saveData', dataName,dataValue); +``` + +如果想广播通知整个扩展系统,则可以利用 **广播消息** 机制实现,详细信息请参考文档 [消息系统](./messages.md)。 + +### 面板接收消息 + +```json5 +// package.json +{ +"contributions": { + "messages": { + "log": { + "methods": ["log"] + } + } + } +} +``` + +上面的消息定义了一个 log 消息,并由此扩展的主进程中的 log 方法处理。接下来我们稍作修改,使消息的接收方变成面板: + +```json5 +// package.json +{ +"contributions": { + "messages": { + "log": { + "methods": ["default.log"] + } + } + } +} +``` + +`default.log` 使得消息接收方变成了 `default` 面板,只需要在面板中实现一个 `log` 方法,就能够顺利处理此消息了。 + +## 更好的面板资源组织方式 + +在上面的最简面板模板中,我们有两行面板显示相关的代码: + +```typescript +module.exports = Editor.Panel.define({ + ... + template: '
Hello
', + style: 'div { color: yellow; }', + ... +}); +``` + +大部分情况下,面板布局不可能这么简单。如果继续将复杂的 HTML 布局和 css 样式写在这里,代码将变得不可维护。可以参考文档 [入门示例-面板](./first-panel.md) 中创建的项目,我们可以将 `html` 和 `css` 代码分离为独立的文件,放入 `static` 文件夹中。 + +最终形成的面板模板代码如下所示: + +```typescript +import { readFileSync } from 'fs-extra'; +import { join } from 'path'; + +module.exports = Editor.Panel.define({ + listeners: { + show() { console.log('show'); }, + hide() { console.log('hide'); }, + }, + template: readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8'), + style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'), + $: { + app: '#app', + }, + methods: { + + }, + ready() { + + }, + beforeClose() { }, + close() { }, +}); +``` + +更多面板实用详情请参考 [入门示例-面板](./first-panel.md)。 diff --git a/versions/4.0/zh/editor/extension/profile.md b/versions/4.0/zh/editor/extension/profile.md new file mode 100644 index 0000000000..7cb8500f05 --- /dev/null +++ b/versions/4.0/zh/editor/extension/profile.md @@ -0,0 +1,149 @@ +# 配置系统 + +Cocos Creator 扩展提供了一套配置管理机制,用于在扩展保存用户设置和数据。 + +## 配置类型 + +配置类型有两种: +1. 编辑器配置(editor) +2. 项目配置(project) + +### 编辑器配置 + +编辑器配置用于存放一些编辑器相关的用户设置和数据,分成三个优先级,从高到低今次为: + +``` +local -> global -> default +``` + +在进行配置数据获取时,会优先采用 `local` 中的配置项,若 `local` 中无对应配置项,则会采用 `global` 中的配置项,若 `global` 中也找不到对应配置项,则会采用默认的 `default` 配置。 + +### 项目配置 + +项目配置用于存放一些和项目相关的用户设置和数据,分成两优先级,从高到低为: + +``` +local -> default +``` + +和 **编辑器配置** 的规则类似,在进行配置数据获取时,会优先采用 `local` 中的配置项,若 `local` 中无对应配置项,则会采用默认的 `default` 配置。 + +## 注册配置 + +若要使用配置系统,需要在扩展定义文件 `package.json` 的 `contributions` 字段中定义 `profile` 相关信息,如下所示: + +```json +{ + "name": "hello-world", + "contributions": { + "profile": { + "editor": { + "test.a": { + "default": 0, + "message": "editorTestAChanged", + "label": "测试编辑器配置" + } + }, + "project": { + "test.a": { + "default": 1, + "message": "projectTestAChanged", + "label": "测试项目配置" + } + } + } + }, +} +``` + +**contributions.profile** 相关的字段释义如下: +- `editor`:{} - 编辑器配置 +- `project`:{} - 项目配置 +- `test.a`:{} - key 为 test.a 的配置项 +- `default`:any - 此配置项的默认值,可选参数 +- `message`:string - 此配置项被修改时会触发此消息,可选参数 +- `label`:string - 在可以显示配置的地方,可能会显示这个描述。支持 i18n:key 格式,可选参数 + +`profile` 相关的 TypeScript 接口定义如下: + +```typescript +interface ProfileInfo { + editor: { [ key: string ]: ProfileItem }; + project: { [ key: string ]: ProfileItem }; +} + +interface ProfileItem { + // 配置的默认数据 + default: any; + // 配置更改后,会自动发送这个消息进行通知 + message: string; + // 简单的描述配置信息的作用,支持 i18n:key 语法 + label: string; +} +``` + +## 读取与修改配置 + +### 导入扩展定义 + +```typescript +import packageJSON from '../package.json'; +``` + +`Editor.Profile.getConfig` 最后一个参数为空的情况,会进行 [优先级](#%E7%BC%96%E8%BE%91%E5%99%A8%E9%85%8D%E7%BD%AE) 匹配。 + +若指定了获取位置(`local` 、 `global` 、`default` 三者之一),则会返回对应的值。如下所示,获取到的 `local` 和 `global` 为 `undefined` 是因为未对其进行设置。 + +```typescript +await Editor.Profile.getConfig(packageJSON.name, 'test.a'); // 0 +await Editor.Profile.getConfig(packageJSON.name, 'test.a', 'local'); // undefined +await Editor.Profile.getConfig(packageJSON.name, 'test.a', 'global'); // undefined +``` + +### 修改编辑器配置 + +用以下代码修改配置后再调用 `getConfig` 可以看到对应变化。 + +```typescript +await Editor.Profile.setConfig(packageJSON.name, 'test.a', 1); +await Editor.Profile.setConfig(packageJSON.name, 'test.a', 'local', 2); +await Editor.Profile.setConfig(packageJSON.name, 'test.a', 'global', 3); +``` + +### 读取项目配置 + +`Editor.Profile.getProject` 最后一个参数为空的情况,会进行 [优先级](#%E9%A1%B9%E7%9B%AE%E9%85%8D%E7%BD%AE) 匹配。 + +若指定了获取位置(`local` 、`default` 二者之一),则会返回对应的值。如下所示,获取到的 `local` 为 `undefined` 是因为未对其进行设置。 + +```typescript +await Editor.Profile.getProject(packageJSON.name, 'test.a'); // 1 +await Editor.Profile.getProject(packageJSON.name, 'test.a', 'local'); // undefined +``` + +### 修改项目配置 + +用以下代码修改配置后再调用 `getProject` 可以看到对应变化。 + +```typescript +await Editor.Profile.setProject(packageJSON.name, 'test.a', 1); +await Editor.Profile.setProject(packageJSON.name, 'test.a', 'local', 2); +``` + +## 存储路径 + +### 编辑器配置存储路径 + +| 层级 | 路径 | +| ------- | ------------------------------------------------------------ | +| local | `{projectPath}/profiles/v2/extensions/{extensionName}.json` | +| global(mac) | `Users/{name}/.CocosCreator/profiles/v2/extensions/{extensionName}.json` | +| global(window) | `c/Users/{name}/.CocosCreator/profiles/v2/extensions/{extensionName}.json` | +| default | `{extensionPath}/package.json` | + +### 项目配置存储路径 + +| 层级 | 路径 | +| ------- | ----------------------------------------------------------- | +| local | `{projectPath}/settings/v2/extensions/{extensionName}.json` | +| default | `{extensionPath}/package.json` | diff --git a/versions/4.0/zh/editor/extension/readme.md b/versions/4.0/zh/editor/extension/readme.md new file mode 100644 index 0000000000..f863ca36e5 --- /dev/null +++ b/versions/4.0/zh/editor/extension/readme.md @@ -0,0 +1,13 @@ +# 扩展编辑器 + +本章主要介绍一些编辑器的扩展功能,包括以下内容: +- [扩展管理器面板](./extension-manager.md) +- [入门示例-菜单](./first.md) +- [入门示例-面板](./first-panel.md) +- [入门示例-扩展间通信](./first-communication.md) +- [扩展改名](./extension-change-name.md) +- [安装与分享](./install.md) +- [上架扩展到商店](./store/upload-store.md) +- [增强已有功能](./contributions.md) +- [扩展系统详解](./basic.md) +- [Editor 接口说明](./editor-api.md) diff --git a/versions/4.0/zh/editor/extension/scene-script.md b/versions/4.0/zh/editor/extension/scene-script.md new file mode 100644 index 0000000000..95570519a8 --- /dev/null +++ b/versions/4.0/zh/editor/extension/scene-script.md @@ -0,0 +1,124 @@ +# 操作当前场景 + +在一些时候我们需要去操作当前场景里的一些数据,比如修改节点位置、增删节点、增删组件等。 + +这时候我们可以通过消息去调用场景已经开放的一些接口,详情参考 [消息系统](./messages.md)。 + +但如果有一些定制操作的时候,全部使用开放的接口拼接会比较困难,或者步骤非常多。这时候我们可以在扩展中可以定义一个特殊的 **场景脚本** 文件,该脚本会和项目中 `assets\` 目录下的脚本处于同一运行进程,具有相同的运行环境,需要注意的是场景脚本只运行在场景进程,属于编辑器能力。 + +在 **场景脚本** 里可以调用引擎 `API` 和其他项目脚本,通过这个特性我们可以实现: + +- 查询、遍历场景中的节点,获取或修改节点数据 + +- 调用节点上的引擎组件相关函数,完成工作 + +## 注册场景脚本 + +在 `package.json` 中定义 `contributions.scene.script` 字段,该字段的值是一个相对于扩展包根目录的脚本文件路径,如下所示: + +```json +{ + "contributions": { + "scene": { + "script": "./dist/scene.js" + } + } +} +``` + +## 场景脚本模板 + +在 `src` 目录下新建一个 `scene.ts`,编写如下代码: + +```typescript +export function load() { }; +export function unload() { }; +export const methods = { }; +``` + +`load` - 模块加载的时候触发的函数 + +`unload` - 模块卸载的时候触发的函数 + +`methods` - 模块内定义的方法,可用于响应外部消息 + +## 通过场景调用引擎 API + +接下来,我们通过对主摄像机进行旋转,来演示场景脚本如何调用引擎 API。 + +为了调用引擎 API,我们需要在 `scene.ts` 开头加入引擎脚本的搜索路径,并编写相应的代码,最终代码如下所示: + +```typescript +import { join } from 'path'; + +// 临时在当前模块增加编辑器内的模块为搜索路径,为了能够正常 require 到 cc 模块,后续版本将优化调用方式 +module.paths.push(join(Editor.App.path, 'node_modules')); + +// 当前版本需要在 module.paths 修改后才能正常使用 cc 模块 +// 并且如果希望正常显示 cc 的定义,需要手动将 engine 文件夹里的 cc.d.ts 添加到插件的 tsconfig 里 +// 当前版本的 cc 定义文件可以在当前项目的 temp/declarations/cc.d.ts 找到 +import { director } from 'cc'; + +export function load() { }; + +export function unload() { }; + +export const methods = { + /** + * 旋转 Main Camera 的角度 + * 这个函数可以是一个异步(async)函数,函数的返回值,将会返回给触发执行函数的消息,详看下面的示例 + * @param num + * @returns {boolean} + */ + rotateCamera(num: number) { + // TS 定义是确定的,但是触发这个函数的消息所发送的参数可能是非法的 + // 所以这里严谨的话,还是需要一些基础的容错处理 + if (typeof num !== 'number') { + num = parseFloat(num + ''); + } + if (isNaN(num)) { + num = 10; + } + + // 通过引擎的方法,进行一些基础处理 + const mainCamera = director.getScene().getChildByName('Main Camera'); + if (mainCamera) { + let euler = new Vec3(); + euler.set(mainCamera.eulerAngles.x, mainCamera.eulerAngles.y + num, mainCamera.eulerAngles.z); + mainCamera.setRotationFromEuler(euler); + return true; + } + return false; + }, +}; +``` + +上面的代码中,我们定义了一个 `rotateCamera` 方法,此方法每执行一次,就会让主摄像机绕 `Y` 轴旋转传入的 num 或者 `10` 度。 + +在其他扩展脚本中,我们可以通过消息使用刚刚定义的 `rotateCamera` 函数,代码如下: + +```typescript +// 这个定义在生成的扩展的根目录的 @types 文件夹里 +// 所有的扩展公开定义都按照规范 @types/packages/{ExtensionName}/@types/public 存放 +// 这里引入 Scene 扩展内针对场景脚本的定义 +import type { ExecuteSceneScriptMethodOptions } from '../@types/packages/scene/@types/public'; + +// 这里直接找到插件的 package.json 定义,拿到里面的扩展名字 +import packageJSON from '../package.json'; + +const options: ExecuteSceneScriptMethodOptions = { + name: packageJSON.name, + method: 'rotateCamera', + args: [10], +}; + +// result: true/false +const result = await Editor.Message.request('scene', 'execute-scene-script', options); +``` + +`ExecuteSceneScriptMethodOptions` 的属性定义如下: +- name - `scene.ts` 所在的扩展包名,如果是本扩展中可以使用 `packageJSON.name` +- method:`scene.ts` 中定义的方法 +- args: 参数,可选,任意可序列化数据,将会原封不动的传递给执行函数 + +由于扩展间通信实现是基于 Electron 的底层跨进程 IPC 机制,传输的数据均会被序列化为JSON。所以传输的数据不可以包含原生对象,否则可能导致进程崩溃或者内存暴涨。如上面代码中的 `options.args` 参数和场景脚本方法的返回值,建议只传输纯 `JSON` 对象。 diff --git a/versions/4.0/zh/editor/extension/store/upload-store.md b/versions/4.0/zh/editor/extension/store/upload-store.md new file mode 100644 index 0000000000..b8cd2ef617 --- /dev/null +++ b/versions/4.0/zh/editor/extension/store/upload-store.md @@ -0,0 +1,61 @@ +# 上架扩展到资源商店 + +Cocos Creator 内置了 **扩展商店**,可供用户浏览、下载和自动安装官方或者第三方扩展、资源。同时用户也可以将自己开发的扩展、美术素材、音乐音效等资源提交到扩展商店以便分享或者售卖。下面我们以提交扩展为例来讲解具体的提交流程。 + +![img](../image/store.png) + +## 打包扩展 + +扩展在提交之前需要根据打包规则进行打包,关于扩展打包的详细讲解,请参考文档 [安装与分享](../install.md)。 + +## 提交扩展 + +访问 [Cocos 开发者中心](https://auth.cocos.com/#/) 并登录,然后进入 [商店](https://store-my.cocos.com/#/seller/resources/) 栏目,点击右上方的 **发布新资源**。 + +![img](../image/create.png) + +- 然后进入 **资源类别** 页面,填写 **名称** 和 **类别**,勾选已阅读协议。 + + ![img](../image/category.png) + + - **名称**:扩展显示在扩展商店中的名称。需要注意的是,名称一经确认后不可更改,请谨慎填写。 + - **类别**:要提交的资源类别,我们这里选择 **Creator 扩展 -> 扩展**。 + + 设置完成后点击 **下一步**,进入 **资源介绍** 页面。 + +- 在 **资源介绍** 页面填写相关信息。 + + ![img](../image/introduction.png) + + - **关键字**:方便用户更快搜索到您的扩展,支持多个关键字 + - **支持平台**:包括 Android、iOS、HTML5 + - **图标**:图标尺寸为 **256 \* 256**,大小不超过 **500KB**,**png** 格式。 + - **截图**:最多上传 **5** 张截图,**jpg** / **png** 格式。每张截图的尺寸限制范围为最小 **640px**,最大 **2048px**,且大小不超过 **1000KB**。 + - **资源介绍**:填写扩展的基本功能和使用方法。包括 **中文** 和 **英文** 两种语言,填写后才会显示在对应语言版本的扩展商店中。
扩展商店对扩展的资源介绍有一定的格式规范要求,详情请参考 [资源介绍模板](https://store.cocos.com/document/zh/cocos-store-template-extension.html)。 + + 填写完成后点击 **下一步** 进入 **定价** 页面。 + +- 在 **定价** 页面设置扩展的售价,包括 **CNY** 和 **USD** 两种,如果免费请填写 **0**。 + + ![img](../image/pricing.png) + + 填写完成后点击 **下一步** 进入 **上传资源** 页面 + +- 在 **上传资源** 页面上传扩展扩资源并填写相关信息。 + + ![img](../image/upload-store.png) + + - **资源包**:zip 格式,最大 100MB。 + - **扩展包名**:扩展包的名称,定义在扩展包的 `package.json` 文件中。 + - **版本号**:扩展版本号,定义在扩展包的 `package.json` 文件中。书写规范请遵守 [semver 规范](http://semver.org/lang/zh-CN/)。 + - **Creator 版本要求**:扩展对 Creator 版本的要求。 + + > **注意**:由于 Creator 2.x 和 3.x 扩展互不兼容,如果没有对应的版本支持扩展包,作品将不会显示到对应版本的 Creator 扩展商店中。 + + 填写完成后点击 **下一步** 进入 **提交审核** 页面。 + +- 在 **提交审核** 页面点击 **提交审核** 按钮即可,也可以点击 **查看** 按钮重新编辑该扩展资源。 + + ![img](../image/submit-for-review.png) + +- 提交审核后,扩展商店的管理人员会在 **3** 个工作日内审核扩展内容和信息: diff --git a/versions/4.0/zh/editor/extension/to-panel-messages.md b/versions/4.0/zh/editor/extension/to-panel-messages.md new file mode 100644 index 0000000000..86b7677c85 --- /dev/null +++ b/versions/4.0/zh/editor/extension/to-panel-messages.md @@ -0,0 +1,92 @@ +# 与面板通信 + +> **注意**:在 v3.5 版本后,我们对扩展文档进行了更新,因此该文档已废弃,请移步 [消息系统](./messages.md) 或 [自定义消息](./contributions-messages.md) 获取更多信息。如果您在线上看到此文档,请在 [github](https://github.com/cocos/cocos-docs/issues/new) 发起 issue,告知官方人员处理。 + +一般情况下,我们的交互模型是 "扩展进程" 为主,"面板" 为数据展示。对位传统 web 的话,就是 "扩展" 功能是服务端,"面板" 功能是客户电脑上的浏览器。 + +这种情况下,一般不会有直接发送数据给面板的情况,有的大部分是一些状态的同步,使用 broadcast 广播即可。 + +但一些简单的扩展,或者需要浏览器环境的扩展,实际功能可能放在面板上,这时候就需要向面板上发送请求。 + +在看这章节前,需要对 [消息系统](./messages.md) 有一定程度的了解。 + +## 定义扩展上和面板的方法 + +首先我们定义一份 package.json: + +```json +{ + "name": "hello-world", + "panels": { + "default": { + "title": "hw", + "main": "./panel.js" + } + }, + "contributions": { + "messages": { + "console": { + "methods": ["default.console"] + } + } + } +} +``` + +messages.console 里的 methods 定义的方法名称是 default.console。表示发给 default 面板里的 console 方法。 +(发送到扩展进程的话,是直接填写 methodName) + +然后定义面板的 panel.js 文件: + +Javascript + +```javascript +exports.template = ''; +exports.style = ''; + +exports.methods = { + console(str) { + console.log(str); + }, +}; + +exports.ready = async function() {}; + +exports.close = function() {}; +``` + +Typescript + +```typescript +export const template = ''; +export const style = ''; + +export const methods = { + console(str: string) { + console.log(`console: ${str}`); + }, +}; + +export async function ready() {}; + +export function close() {}; +``` + +## 发送消息 + +当我们定义好扩展和扩展里的面板后,就可以尝试触发这些消息。 + +按下 ctrl(cmd) + shift + i 打开控制台。在控制台打开面板: + + ```javascript + // default 可以省略,如果面板名字是非 default,则需要填写 'hello-world.xxx' + Editor.Panel.open('hello-world'); + // 向 hello-world 扩展发送一个 console 消息 + Editor.Message.send('hello-world', 'console', 'log'); + ``` + +hello world 扩展收到消息后,会转给 panel.js 里的 methods.console 进行处理。 + +所以会在控制台输出一个字符串 "log" + +至此,我们完成了一次与面板的交互。 diff --git a/versions/4.0/zh/editor/extension/ui.md b/versions/4.0/zh/editor/extension/ui.md new file mode 100644 index 0000000000..662ec689b3 --- /dev/null +++ b/versions/4.0/zh/editor/extension/ui.md @@ -0,0 +1,50 @@ +# UI 组件 + +## UI 组件面板 + +为了方便布局,编辑器内提供了许多预设的 UI 组件。 + +1、找到编辑器顶部主菜单中的 **开发者 -> UI 组件** 查看。 + +![ui-component-menu](image/ui-component-menu.png) + +2、点击后可打开如下的面板: + +![ui component](image/ui-component.png) + +该面板由两部分组成,左边栏列出了目前引擎支持的 UI 种类,右侧栏提供了部分示例,开发者可按需使用。 + +## 在 HTML 中使用 + +在 HTML 使用 UI 组件非常简单,只需要将对应代码复制到你的 HTML 文件中,即可使用。 + +## 在扩展面板时使用 + +在扩展编辑器面板时,可以使用 json 方式进行配置。理论上所有带 `value` 属性的 UI 组件都可以用于扩展编辑器面板,下面列出常见的几种: + +### 输入框 + +- 组件:`ui-num-input` +- 无额外属性 + +### 滑动条 + +- 组件:`ui-slider` +- `attributes` 组件属性 + - `min` 最小值 + - `max` 最大值 + - `step` 步长 + +### 复选框 + +- 组件:`ui-checkbox` +- 无额外属性 + +### 选择列表 + +- 组件 `ui-select` +- `items` 列表元素 + - `value` 值 + - `label` 显示标签 + +具体使用示例请参考文档 [自定义偏好设置面板](./contributions-preferences.md) 和 [自定义项目设置面板](./contributions-project.md)。 diff --git a/versions/4.0/zh/editor/hierarchy/img/after.png b/versions/4.0/zh/editor/hierarchy/img/after.png new file mode 100644 index 0000000000..66204497b1 Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/after.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/context-menu.png b/versions/4.0/zh/editor/hierarchy/img/context-menu.png new file mode 100644 index 0000000000..eda312e7cb Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/context-menu.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/create.png b/versions/4.0/zh/editor/hierarchy/img/create.png new file mode 100644 index 0000000000..92ad456f25 Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/create.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/drop.png b/versions/4.0/zh/editor/hierarchy/img/drop.png new file mode 100644 index 0000000000..969cfaeaea Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/drop.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/drop1.png b/versions/4.0/zh/editor/hierarchy/img/drop1.png new file mode 100644 index 0000000000..dea4ec5af9 Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/drop1.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/drop2.png b/versions/4.0/zh/editor/hierarchy/img/drop2.png new file mode 100644 index 0000000000..642a3372b2 Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/drop2.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/hierarchy.png b/versions/4.0/zh/editor/hierarchy/img/hierarchy.png new file mode 100644 index 0000000000..e88acee20c Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/hierarchy.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/node-name.png b/versions/4.0/zh/editor/hierarchy/img/node-name.png new file mode 100644 index 0000000000..6590dca28f Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/node-name.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/rename.png b/versions/4.0/zh/editor/hierarchy/img/rename.png new file mode 100644 index 0000000000..9ba5d01e2a Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/rename.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/search-type.png b/versions/4.0/zh/editor/hierarchy/img/search-type.png new file mode 100644 index 0000000000..d67d8e9764 Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/search-type.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/search.png b/versions/4.0/zh/editor/hierarchy/img/search.png new file mode 100644 index 0000000000..bcf4f9573d Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/search.png differ diff --git a/versions/4.0/zh/editor/hierarchy/img/thumb.gif b/versions/4.0/zh/editor/hierarchy/img/thumb.gif new file mode 100644 index 0000000000..cb2dce658c Binary files /dev/null and b/versions/4.0/zh/editor/hierarchy/img/thumb.gif differ diff --git a/versions/4.0/zh/editor/hierarchy/index.md b/versions/4.0/zh/editor/hierarchy/index.md new file mode 100644 index 0000000000..8bde1cba2e --- /dev/null +++ b/versions/4.0/zh/editor/hierarchy/index.md @@ -0,0 +1,99 @@ +# 层级管理器 + +**层级管理器** 面板上主要包括 **工具栏** 和 **节点列表** 两部分内容,用于展现当前场景中可编辑的节点之间的关系。场景中仍有一些不可见的私有节点,不会在此显示。 + +你可以单选、多选、创建、复制、移动、删除和重命名节点,任意节点都可创建出子节点,子节点的坐标相对于父级节点,跟随父级节点移动。 + +![面板全览](img/thumb.gif) + +- 选中节点,节点呈现高亮状态,节点的详细属性会在 **属性检查器** 中显示。点击面板空白区域,可以 **取消选中**。 +- **工具栏** 中的功能包括:**新建节点按钮(+)**、**搜索类型按钮**、**搜索框**、**全部折叠/展开按钮** 和 **刷新列表按钮**。 +- **节点列表** 主要体现节点的层级关系,根节点是 **场景节点(Scene)**,编辑 Prefab 文件时,自身的节点作为根节点。可以在这里用右键菜单或者拖拽操作对节点进行增删修改。 +- 面板支持键盘快捷方式: + - 复制:Ctrl/Cmd + C + - 粘贴:Ctrl/Cmd + V + - 克隆:Ctrl/Cmd + D,Ctrl + 拖动节点 + - 删除:Delete + - 上下选择:上下箭头 + - 节点的折叠:左箭头 + - 节点的展开:右箭头 + - 多选:Ctrl or Cmd + 点击 + - 连续多选:Shift + 点击 + - 重命名:Enter/F2 + - 取消输入:Esc + +## 新建节点 + +点击面板左上角的 **新建节点(+)** 按钮或者直接在面板中点击右键,即可创建节点。 + +![create-node](img/create.png) + +创建节点的时候,会先出现一个 **输入框** 要求填入节点的名称,节点的名称允许为空,若为空则会以默认节点名称命名。 + +![node-name](img/node-name.png) + +- 如果没有在树形列表中选中节点,则新建的节点会默认创建到当前根节点下(`Scene`) +- 如果有多个选中节点,则新建的节点会创建到第一个选中节点下。 + +### UI 节点 + +对于 UI 节点,为了让其正常显示,它的任意上级节点至少得有一个含有 **UITransform** 组件。所以创建 UI 节点时,如果不符合规则,则会自动创建一个 **Canvas** 节点作为 UI 节点的根节点,具体内容可参考 [UI 结构说明](../../2d-object/ui-system/index.md)。 + +### Prefab 节点 + +对于 Prefab 节点,可以直接将 **资源管理器** 中的一个 **Prefab** 资源拖动到 **层级管理器** 中生成一个 Prefab 节点。当然也可以将 **层级管理器** 中的一个 **Prefab** 节点拖动到 **资源管理器** 中生成一个 Prefab 资源。 + +## 搜索节点 + +搜索类型包括:**搜索名称或 UUID**、**搜索 UUID**、**搜索 Path**、**搜索组件名称** 和 **搜索资源 UUID**。 + +![search-type](img/search-type.png) + +其中 **搜索组件名称** 会搜索出节点列表中包含指定组件的所有节点,可在 **属性检查器** 查看到组件名称,如 **MeshRenderer**。 + +**搜索框** 会根据输入内容即时更新搜索结果。在搜索结果中选中节点,清空搜索内容后,仍会在资源列表中定位到该选中节点。 + +![search](img/search.png) + +## 更改节点的显示顺序 + +通过拖拽节点上下移动,可以更改节点在列表中的排序。移动节点分为 **被移动的节点** 和 **目标放置节点**: + +- 将 **被移动的节点** 拖动到 **目标放置节点** 的上方,两者是 **平级** 的。 + + 如下图,选中 Cube 节点,并将其拖动到 Sphere 节点的上方,Sphere 节点会呈现黄字的高亮状态,并且在其上方有一条蓝色的线,表示 Cube 节点将会被插入的位置。 + + ![drop](img/drop.png) + +- 将 **被移动的节点** 拖动到 **目标放置节点** 上,**被移动的节点** 会作为 **目标放置节点** 的子节点排在最末尾。 + + 如下图,选中 Cube 节点,并将其拖动到 Sphere 节点上,Sphere 节点会呈现浅蓝底黄字的高亮状态,表示 Cube 节点将会成为 Sphere 的子节点。 + + ![drop](img/drop1.png) + +- 将 **被移动的节点** 拖动到 **目标放置节点** 的下方,两者是 **平级** 的。 + + 如下图,选中 Sphere 节点,并将其拖动到 Cube 节点的下方,Cube 节点会呈现黄字的高亮状态,并且在其下方有一条蓝色的线,表示 Sphere 节点将会被插入的位置。 + + ![drop](img/drop2.png) + +## 重命名节点 + +选中某个节点,然后点击右键,选择 **重命名** 即可修改节点名称,或者也可以直接使用快捷键 **Enter** 或者 **F2**。点击面板其他地方或者按快捷键 **Esc** 可以取消此次重命名。 + +不同节点可以有相同的名称。 + +![rename](img/rename.png) + +## 其他操作 + +右键点击节点弹出的菜单里还包括下列操作: + +- **复制/粘贴**:将节点复制到剪贴板上,然后可以粘贴到另外的位置,或打开另一个场景来粘贴刚才拷贝的节点。 +- **生成副本**:生成和选中节点完全相同的节点副本,生成节点和选中节点在同一层级中。 +- **全选**:选中同一层级中的所有节点。 +- **复制并打印 UUID/PATH**:在复杂场景中,我们有时候需要获取节点的 UUID 或者完整的层级路径,以便在脚本运行时访问该节点。点击该选项,就可以在 **控制台** 中看到当前选中节点的 UUID 以及节点的层级路径。 +- **锁定节点**:鼠标移到节点上,左侧会有一个锁定按钮,节点锁定后便无法在 **场景编辑器** 中选中该节点。 + + 从 v3.1.1 开始,支持批量锁定节点及其子节点。选中任意节点,按住 Alt 并点击锁定按钮,该节点及其子节点便会全部被锁定。 + 从 v3.5.0 开始,节点默认为级联操作,如点击父节点锁定按钮,其子节点便会全部被锁定。按住 Alt 则只针对当前节点。 diff --git a/versions/4.0/zh/editor/index.md b/versions/4.0/zh/editor/index.md new file mode 100644 index 0000000000..7330bdf497 --- /dev/null +++ b/versions/4.0/zh/editor/index.md @@ -0,0 +1,40 @@ +# 编辑器界面介绍 + +这一章将会介绍编辑器界面,熟悉组成编辑器的各个面板、菜单和功能按钮。Cocos Creator 编辑器由多个面板组成,面板可以自由移动、组合,以适应不同项目和开发者的需要。我们在这里将会以默认编辑器布局为例,快速浏览各个面板的名称和作用: + +![main](index/editor.png) + +- (**A**)[层级管理器](./hierarchy/index.md):以树状列表的形式展示场景中的所有节点和它们的层级关系,所有在 **场景编辑器** 中看到的内容都可以在 **层级管理器** 中找到对应的节点条目,在编辑场景时这两个面板的内容会同步显示,一般我们也会同时使用这两个面板来搭建场景。 +- (**B**)[资源管理器](./assets/index.md):显示了项目资源文件夹(`assets`)中的所有资源。这里会以树状结构显示文件夹并自动同步在操作系统中对项目资源文件夹内容的修改。您可以将文件从项目外面直接拖拽进来,或使用菜单导入资源。 +- (**C**)[场景编辑器](./scene/index.md):用于展示和编辑场景中可视内容的工作区域。通过在场景编辑器中搭建场景,即可获得所见即所得的场景预览。 +- (**D**)[动画编辑器](../animation/index.md):用于编辑并存储动画数据。 +- (**E**)[属性检查器](./inspector/index.md):用于查看并编辑当前选中节点和组件属性的工作区域,这个面板会以最适合的形式展示和编辑来自脚本定义的属性数据。 +- (**F**)[项目预览](./preview/index.md):在场景搭建完成之后,在 Web 或原生平台预览游戏的运行效果。 + +其他重要的编辑器基础界面包括: + +- **控制台** + + ![console](index/console.png) + + **控制台** 会显示报错、警告或其他编辑器和引擎生成的日志信息。详情请阅读 [控制台](console/index.md) 一节。 + +- **偏好设置** + + ![preferences](index/preferences.png) + + **偏好设置** 里提供各种编辑器个性化的全局设置,包括原生开发环境、游戏预览、其他扩展的全局设置等。详情请阅读 [偏好设置](preferences/index.md) 一节。 + +- **项目设置** + + ![settings](index/settings.png) + + **项目设置** 里提供各种项目特定的个性化设置,包括分组管理、功能裁剪、项目预览、自定义引擎等。详情请阅读 [项目设置](project/index.md) 一节。 + +- **服务** + + ![service](index/service.png) + + Cocos Service 是集成在 Cocos Creator 内的 **服务** 面板。我们甄选优质技术方案商,提供高性价比服务接入,致力于给用户一键式的接入体验,以及提供相应的技术支撑。同时依托广大 Cocos 开发者群体,我们也将为开发者争取到更加优惠的价格。 + + 具体内容请参考 [Cocos Service 简介](https://service.cocos.com/document/zh/)。 diff --git a/versions/4.0/zh/editor/index/animation.png b/versions/4.0/zh/editor/index/animation.png new file mode 100644 index 0000000000..9d4391514f Binary files /dev/null and b/versions/4.0/zh/editor/index/animation.png differ diff --git a/versions/4.0/zh/editor/index/assets.png b/versions/4.0/zh/editor/index/assets.png new file mode 100644 index 0000000000..f29251e5aa Binary files /dev/null and b/versions/4.0/zh/editor/index/assets.png differ diff --git a/versions/4.0/zh/editor/index/console.png b/versions/4.0/zh/editor/index/console.png new file mode 100644 index 0000000000..2d17e7da73 Binary files /dev/null and b/versions/4.0/zh/editor/index/console.png differ diff --git a/versions/4.0/zh/editor/index/editor.png b/versions/4.0/zh/editor/index/editor.png new file mode 100644 index 0000000000..9bd13f27dc Binary files /dev/null and b/versions/4.0/zh/editor/index/editor.png differ diff --git a/versions/4.0/zh/editor/index/hierarchy.png b/versions/4.0/zh/editor/index/hierarchy.png new file mode 100644 index 0000000000..e88acee20c Binary files /dev/null and b/versions/4.0/zh/editor/index/hierarchy.png differ diff --git a/versions/4.0/zh/editor/index/inspector.png b/versions/4.0/zh/editor/index/inspector.png new file mode 100644 index 0000000000..afd9af3969 Binary files /dev/null and b/versions/4.0/zh/editor/index/inspector.png differ diff --git a/versions/4.0/zh/editor/index/main.jpg b/versions/4.0/zh/editor/index/main.jpg new file mode 100644 index 0000000000..f31dd09e05 Binary files /dev/null and b/versions/4.0/zh/editor/index/main.jpg differ diff --git a/versions/4.0/zh/editor/index/preferences.png b/versions/4.0/zh/editor/index/preferences.png new file mode 100644 index 0000000000..cc67b46004 Binary files /dev/null and b/versions/4.0/zh/editor/index/preferences.png differ diff --git a/versions/4.0/zh/editor/index/scene.png b/versions/4.0/zh/editor/index/scene.png new file mode 100644 index 0000000000..fbb94090a3 Binary files /dev/null and b/versions/4.0/zh/editor/index/scene.png differ diff --git a/versions/4.0/zh/editor/index/service.png b/versions/4.0/zh/editor/index/service.png new file mode 100644 index 0000000000..15561df35e Binary files /dev/null and b/versions/4.0/zh/editor/index/service.png differ diff --git a/versions/4.0/zh/editor/index/settings.png b/versions/4.0/zh/editor/index/settings.png new file mode 100644 index 0000000000..65b71c64a0 Binary files /dev/null and b/versions/4.0/zh/editor/index/settings.png differ diff --git a/versions/4.0/zh/editor/inspector/index.md b/versions/4.0/zh/editor/inspector/index.md new file mode 100644 index 0000000000..9736cd1815 --- /dev/null +++ b/versions/4.0/zh/editor/inspector/index.md @@ -0,0 +1,118 @@ +# 属性检查器 + +**属性检查器** 是我们查看并编辑当前选中节点、节点组件和资源的工作区域。在 **场景编辑器** 或者 **层级管理器** 选中节点,或者在 **资源管理器** 选中资源,就可以在 **属性检查器** 中显示并编辑属性。 + +![introduce](index/introduce.gif) + +## 面板总览 + +![inspector](index/inspector-panel.png) + +**属性检查器** 面板大致可以分为 **工具栏** 和 **属性设置** 两个部分。 + +## 工具栏 + +左上角的 **两个箭头** 是编辑历史记录,点击可前进/后退选中过的节点/资源。
+右上角的 **锁图标** 按钮可锁定面板,固定当前编辑的对象,不让面板随新的选中项而变动。 + +![header](index/header.png) + +## 属性设置 + +在 **属性设置** 区域可以设置节点属性、组件属性、资源属性等。 + +### 节点名称和激活开关 + +节点名称,和 **层级管理器** 中的节点显示名称一致。 + +节点激活复选框表示节点的激活状态。不勾选时,节点处于非激活状态,节点会被暂停渲染,包括节点的子节点都会被隐藏(置灰)。 + +### 节点属性 + +点击节点名称下方的 `Node` 可以将节点的属性折叠或展开。`Node` 右侧有帮助文档和节点设置按钮: + +- 点击帮助文档按钮可以跳转到节点相关的官方文档 + +- 点击节点设置按钮可对节点进行以下操作: + + 1. 重置节点属性 + 2. 复制/粘贴节点的值, + 3. 复制/粘贴节点的世界坐标,包括 `worldPosition`/`worldRotation`/`worldScale` 属性。 + 4. 分别重置节点的 `Position`/`Rotation`/`Scale` 属性。 + + ![node-menu](index/node-menu.png) + +节点的变换属性包括 **位置(Position)**、**旋转(Rotation)** 和 **缩放(Scale)**,修改节点的属性通常可以立刻在 **场景编辑器** 中看到节点的外观或位置变化。详情可参考 [坐标系和变换](../../concepts/scene/coord.md#%E5%8F%98%E6%8D%A2%E5%B1%9E%E6%80%A7) + +如果需要批量修改节点属性,可在 **层级管理器** 中按 Shift 键选中多个节点,然后在 **属性检查器** 中批量设置。节点属性的批量设置与资源类似,具体可参考文末的资源多选批量设置部分。 + +### 组件属性设置 + +组件开启复选框表示组件的开启状态。不勾选时,组件处于关闭状态,不会参与渲染。 + +在节点属性下方,会列出节点上挂载的所有组件和组件的属性。和节点属性一样,点击组件的名称就会切换该组件属性的折叠/展开状态。在节点上挂载了很多组件的情况下,可以通过折叠不常修改的组件属性来获得更大的工作区域。 + +组件名称的右侧有 **帮助文档** 和 **组件设置** 的按钮: +- 帮助文档按钮可以跳转到该组件相关的官方文档介绍页面 +- 组件设置按钮可以对组件执行重置、删除、向上移动、向下移动、复制组件、粘贴组件的值、粘贴成为新组件等功能。 + +各个组件的属性及设置都是不同的,详情请参考相应的组件说明文档。 + +## 添加组件 + +点击 **添加组件** 按钮会出现组件列表,包含系统提供的组件和自定义脚本组件。添加组件的列表有一个搜索框,支持使用键盘的上下箭头切换,以及 **Enter** 确定选中。 + +![add-component](index/add-component.png) + +开发者在 **资源管理器** 中的脚本,可直接拖拽到 **属性检查器** 面板生成一个脚本组件,或者通过 **添加组件 -> 自定义脚本** 添加。脚本组件的属性是由脚本声明的。不同类型的属性在 **属性检查器** 中有不同的控件外观和编辑方式。我们将在 [声明属性](../../scripting/decorator.md) 一节中详细介绍属性的定义方法。 + +## 属性类型 + +**属性** 是组件脚本中声明的公开的并可被序列化存储在场景和动画数据中的变量。通过 **属性检查器** 可以快捷地修改属性设置,达到不需要修改脚本就可以调整游戏数据和玩法的目的。 + +通常可以根据变量使用内存位置不同将属性分为 **值类型** 和 **引用类型** 两大类。 + +### 值类型属性 + +**值类型** 包括数字、字符串、布尔、枚举等简单的占用很少内存的变量类型: + +- `数值(Number)`:可以直接使用键盘输入,也可以按输入框旁边的上下箭头逐步增减属性值。 +- `向量(Vec2)`:向量的控件是两个数值输入组合在一起,并且输入框上会以 x、y 标识每个数值对应的子属性名。 +- `字符串(String)`:直接在文本框里用键盘输入字符串,字符串输入控件分为单行和多行两种,多行文本框可以按回车换行。 +- `布尔(Boolean)`:以复选框的形式来编辑,选中状态表示属性值为 true,非选中状态表示 false。 +- `枚举(Enum)`:以下拉菜单的形式编辑,点击枚举菜单,然后从弹出的菜单列表里选择一项,即可完成枚举值的修改。 +- `颜色(Color)`:点击颜色属性预览框,会弹 **颜色取色器** 窗口,在这个窗口里可以用鼠标直接点选需要的颜色,或在下面的 RGBA 颜色输入框中直接输入指定的颜色。点击 **颜色取色器** 窗口以外的任何位置会关闭窗口并以最后选定的颜色作为属性值。例如颜色取色器组件: + + ![ui-color](index/ui-color.png) + +### 引用类型属性 + +**引用类型** 包括 object 对象,比如节点、组件或资源。可通过 **拖拽节点或资源到属性栏中** 或 **弹出资源面板** 的方式选择并赋值。 + +![assets-panel](index/assets-panel.png) + +## 批量操作 + +当需要批量设置 **同类型** 资源属性时,可在 **资源管理器** 中按 Shift 键并选中多个资源,**属性检查器** 中便会显示选中的资源数量以及可编辑的资源属性。设置完成后点击右上方的 **应用** 按钮即可。 + +![multiple-edit](index/multiple-edit1.png) + +批量修改节点属性也是一样的。但如果 **属性检查器** 中的属性显示以下几种状态,表示该属性在选中的多个资源中的属性值不一致,可根据需要选择是否继续批量修改该属性: + +- 勾选框显示 **灰色** +- 输入框显示 **-** +- 选择框显示 **空白** + +> **注意**: +> 1. 目前 Material 资源不支持批量设置操作。 +> 2. **不同类型** 的资源可同时选中,但是不支持批量设置属性,**属性检查器** 中只会显示当前同时选中了几个资源。 + +## 编辑 Prefab 节点属性 + +Prefab 节点在 **属性检查器** 顶部工具栏中的功能包括:取消关联、定位资源、从资源还原、更新到资源。具体内容请参考 [预制资源(Prefab)](../../asset/prefab.md)。 + +![prefab-menu](index/prefab-menu.png) + +> **注意**:编辑资源时请注意一定要记得点击右上角的 **绿色打勾** 按钮保存。 +> +> ![edit-assets](index/edit-assets.png) diff --git a/versions/4.0/zh/editor/inspector/index/add-component.png b/versions/4.0/zh/editor/inspector/index/add-component.png new file mode 100644 index 0000000000..20cb0408e4 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/add-component.png differ diff --git a/versions/4.0/zh/editor/inspector/index/assets-panel.png b/versions/4.0/zh/editor/inspector/index/assets-panel.png new file mode 100644 index 0000000000..26d8208e44 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/assets-panel.png differ diff --git a/versions/4.0/zh/editor/inspector/index/component-menu.png b/versions/4.0/zh/editor/inspector/index/component-menu.png new file mode 100644 index 0000000000..8d9ca875e2 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/component-menu.png differ diff --git a/versions/4.0/zh/editor/inspector/index/drag-assets.png b/versions/4.0/zh/editor/inspector/index/drag-assets.png new file mode 100644 index 0000000000..c055e45481 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/drag-assets.png differ diff --git a/versions/4.0/zh/editor/inspector/index/edit-assets.png b/versions/4.0/zh/editor/inspector/index/edit-assets.png new file mode 100644 index 0000000000..c10f9afb87 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/edit-assets.png differ diff --git a/versions/4.0/zh/editor/inspector/index/header.png b/versions/4.0/zh/editor/inspector/index/header.png new file mode 100644 index 0000000000..5a0b1d9a20 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/header.png differ diff --git a/versions/4.0/zh/editor/inspector/index/inspector-panel.png b/versions/4.0/zh/editor/inspector/index/inspector-panel.png new file mode 100644 index 0000000000..7bd27ae8bd Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/inspector-panel.png differ diff --git a/versions/4.0/zh/editor/inspector/index/introduce.gif b/versions/4.0/zh/editor/inspector/index/introduce.gif new file mode 100644 index 0000000000..76aa5e7cd5 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/introduce.gif differ diff --git a/versions/4.0/zh/editor/inspector/index/material-in-node.png b/versions/4.0/zh/editor/inspector/index/material-in-node.png new file mode 100644 index 0000000000..23d4f19d05 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/material-in-node.png differ diff --git a/versions/4.0/zh/editor/inspector/index/multiple-edit.png b/versions/4.0/zh/editor/inspector/index/multiple-edit.png new file mode 100644 index 0000000000..74feed07d1 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/multiple-edit.png differ diff --git a/versions/4.0/zh/editor/inspector/index/multiple-edit1.png b/versions/4.0/zh/editor/inspector/index/multiple-edit1.png new file mode 100644 index 0000000000..ff0c25147e Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/multiple-edit1.png differ diff --git a/versions/4.0/zh/editor/inspector/index/node-attrs.png b/versions/4.0/zh/editor/inspector/index/node-attrs.png new file mode 100644 index 0000000000..542cf06f8d Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/node-attrs.png differ diff --git a/versions/4.0/zh/editor/inspector/index/node-menu.png b/versions/4.0/zh/editor/inspector/index/node-menu.png new file mode 100644 index 0000000000..cb7ccd21c0 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/node-menu.png differ diff --git a/versions/4.0/zh/editor/inspector/index/prefab-edit-menu.png b/versions/4.0/zh/editor/inspector/index/prefab-edit-menu.png new file mode 100644 index 0000000000..c436178d80 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/prefab-edit-menu.png differ diff --git a/versions/4.0/zh/editor/inspector/index/prefab-menu.png b/versions/4.0/zh/editor/inspector/index/prefab-menu.png new file mode 100644 index 0000000000..28681e0fd9 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/prefab-menu.png differ diff --git a/versions/4.0/zh/editor/inspector/index/ui-color.png b/versions/4.0/zh/editor/inspector/index/ui-color.png new file mode 100644 index 0000000000..95b0ee9294 Binary files /dev/null and b/versions/4.0/zh/editor/inspector/index/ui-color.png differ diff --git a/versions/4.0/zh/editor/l10n/collect-and-count.md b/versions/4.0/zh/editor/l10n/collect-and-count.md new file mode 100644 index 0000000000..80e22240a4 --- /dev/null +++ b/versions/4.0/zh/editor/l10n/collect-and-count.md @@ -0,0 +1,27 @@ +# 收集并统计 + +收集功能会将项目内文本、Typescript 脚本、场景资源、预制体、视频、引擎和图片等文件搜集起来,并允许开发者进行本地化配置。 + +![overview](collect/overview.png) + +## 属性和说明 + +- 本地开发语言: 本地开发语言,指的是开发者在开发时使用的语言。此处选择的语言会作为源语言,提供给译文服务商进行翻译。开发者可以通过下拉菜单,根据当前自己的喜好进行选择: + + ![lang](collect/diff-language.png) + + 该选项为必选。 + +- 从资源文件中收集:本地化编辑功能可以从不同的资源目录收集所需翻译的文本信息,同时也可以选择过滤或排除某些文件/文件夹。 + + ![group](collect/group.png) + + - 收集组:可添加多个,通过 **+** 按钮添加不同的收集组,对于已添加的项,鼠标滑过时会显示 **删除** 按钮。 + + ![add-remove](collect/add-remove.png) + + - 搜索目录:可以指定当前资源数据库内特定的目录,用于收集需要翻译的资源,最少为 1 个即 `db://assets`。为减少收集时间,我们建议开发者将需要翻译的资源存放在单独的目录。再通过此选项进行分类整理。 + - 扩展名:指定通过特定的文件后缀名进行收集,若无,则收集所有文件。 + - 排除路径:排除的路径将不会被收集。 + +当配置好要收集的目录以后,点击收集和统计按钮后,数据会被记录下来,并可通过 [语言编译](compile-language.md) 进行编译和整理。 diff --git a/versions/4.0/zh/editor/l10n/collect/add-remove.png b/versions/4.0/zh/editor/l10n/collect/add-remove.png new file mode 100644 index 0000000000..5132d27015 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/collect/add-remove.png differ diff --git a/versions/4.0/zh/editor/l10n/collect/diff-language.png b/versions/4.0/zh/editor/l10n/collect/diff-language.png new file mode 100644 index 0000000000..bc9156621d Binary files /dev/null and b/versions/4.0/zh/editor/l10n/collect/diff-language.png differ diff --git a/versions/4.0/zh/editor/l10n/collect/group.png b/versions/4.0/zh/editor/l10n/collect/group.png new file mode 100644 index 0000000000..808c8f8348 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/collect/group.png differ diff --git a/versions/4.0/zh/editor/l10n/collect/overview.png b/versions/4.0/zh/editor/l10n/collect/overview.png new file mode 100644 index 0000000000..f655b7cb85 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/collect/overview.png differ diff --git a/versions/4.0/zh/editor/l10n/compile-language.md b/versions/4.0/zh/editor/l10n/compile-language.md new file mode 100644 index 0000000000..84d343325d --- /dev/null +++ b/versions/4.0/zh/editor/l10n/compile-language.md @@ -0,0 +1,189 @@ +# 语言编译 + +![overview](compile/overview.png) + +当在 [收集并统计](collect-and-count.md) 视图中选定本地开发语言后,即可对编译过程进行配置。这里将包含 **自动翻译**、**手动翻译** 以及 **变体** 等功能。 + +## 语言 + +开发者可以在此处选择目标版本的语言。 + +![add](compile/add-lang.png) + +第一行的语言为 **本地开发语言**,记录当前开发过程中所使用的本地语言,通常可以选择为开发者熟悉的语言。在该语言下,**操作** 栏中会出现 **补全** 功能,用于记录 Label 组件的原文,需搭配 [L10nLabel](l10n-label.md) 使用,在有键无值的情况下使用;或匹配非文本资源的多语言。详情请参考下文 **补全** 部分。 + +从第二行开始,则是目标国家/地区所使用的语言,开发者需至少选择一个目标国家/地区的语言,才会出现翻译选项。 + +## 译文服务商识别的语言 + +![provider](compile/lang-provider.png) + +上图用于在自动翻译时选择译文服务商的输入/输出的语言类型: + +- 对于本地开发语言,选择的是输入语言类型。 + +- 对于译文语言来说,选择的是输出语言类型。 + +例如上图中,本地开发语言为英语(en),那么下拉菜单需要选择 **English**。 + +而译文是 **简体中文(zh-Hans-CN)**,那么在下拉菜单中请选择 **zh-CHS**。 + +> **注意**:如果下拉菜单不可用,请检查 [AppKey/AppSecret](translation-service.md) 是否配置正确。 + +## 翻译进度 + +![progress](compile/progress.png) + +展示当前的翻译进度。 + +## 操作 + +操作栏内对本地开发语言提供了 **补齐** 功能,对于目标译文语言提供 **翻译** 功能。两者的通用功能包含 **预览**、**导出** 和 **删除**。 + +### 补全 + +**补全** 供仅在当前语言设置为 **本地开发语言** 时生效。补齐功能的作用是记录当前开发语言中所要翻译的内容的原文。所有通过 **搜集并统计** 功能得到的原文数据将会在这里展示。点击后,可以查看所有的结果。 + +![complemenet](compile/complement.png) + +当在预制体或场景内通过 [L10nLabel](l10n-label.md) 组件添加键后: + +![new key](compile/new-key.png) + +可在 **补全** 界面中找到对应的键值,在 原文(Original) 一栏中输入该键对应的值,然后点击 **保存** 按钮即可将原文保存,如果 Label 的 String 属性有值,这里将被自动填充。 + +![unfilled](compile/unfilled.png) + +非本文内容如图片、音频等资源也进行补全,具体请参考下文 **非文本资源** 部分。 + +### 变体 + +变体功能专门用于解决翻译时遇到的复数问题,系统会根据变体的规则自动调整翻译的内容,用户也可以自由设定变体的规则。 + +在 **补全** 界面中,点击 **变体** 按钮可以输入新的变体: + +![otehr](compile/other.png) + +在 **补全** 完成后,记得需要点击上方的 **保存** 按钮进行保存。 + +变体的数量和语种有关,属于国际规则的一种,感兴趣的用户可移步 [https://cldr.unicode.org/](https://cldr.unicode.org/) 进行查看。 + +详情可以查看 [L10nLabel 变体数量](l10n-label.md)。 + +### 翻译 + +当在 **补全** 功能中,添加了键值和原文后,即可以使用译文服务商的翻译服务进行翻译,点击 **翻译按钮** 可弹出翻译界面: + +![translate](compile/translate.png) + +点击右上方的 **翻译** 按钮,进行翻译: + +![translate-overview](compile/translate-overview.png) + +翻译完成后可在 **翻译** 分页内查看翻译结果: + +![complete](compile/translate-complete.png) + +对于已翻译的文本,也可以点击 **变体**,添加不同的翻译: + +![variant](compile/translated-variant.png) + +> **注意**:该功能仅在译文服务商正确配置情况下生效。 + +### 预览 + +点击预览可快速预览当前场景持有 [L10nLabel](l10n-label.md) 组件的节点/图片资源等在该语言下的展示情况: + +![preview](compile/preview-overview.png) + +预览翻译前: + +![pev](compile/original-preview.png) + +翻译后: + +![prev](compile/translate-preview.png) + +### 导出 + +点击 **导出** 按钮可以将原文/译文导出为 PO 文件。 + +> PO 文件是软件开发中一种常见的基于文本对象的文件,通常用于记录界面翻译的结果。 + +![export](compile/export.png) + +以下是一些辅助翻译工具,开发者可根据需求自行选择: + +- Manipulating PO Files +- PO 格式编辑器 [https://poedit.net/](https://poedit.net/) +- PO 格式工具箱 [https://github.com/translate/translate](https://github.com/translate/translate) +- 离线 translation memory 工具 [OmegaT - The Free Translation Memory Tool - OmegaT 3](https://omegat.org/) + +### 删除 + +![del](compile/delete.png) + +删除当前语言已翻译的进度,确认后结果不会被保存,请谨慎操作。本地开发语言不可删除。 + +## 非文本资源 + +当项目中存在画在图片上的文字、不同语种配音的视频和音频时,也可能有本地化的需求。此时翻译界面会提供 **导入** 按钮,此处以图片为示例,展示如何配置本地化: + +![image](compile/import-image.png) + +当 **收集和统计** 完成后,被检测出的资源将会以路径作为键值的形式存在。 + +![assets](compile/asset.png) + +通过点击 **导入** 按钮,导入目标语言所需的图片即可: + +![dest image](compile/import-other-lang-jpg.png) + +> **注意**: +> 1. 导入时,只可导入项目内的文件。 +> 2. 由于自动合图会修改资源的 uuid 会导致重定向失败,致使需要翻译的图片不能勾选自动合图功能 + +### 智能匹配 + +智能匹配按钮会弹出二级确认菜单,点击确认后将会开启智能匹配。智能匹配会通过文件路径或文件名来进行修改并匹配对应的语言。例如: + +源文文件为: assets/aassb/cn-abc001.jpg,需将其翻译为英语,其译文文件为:assets/aassb/en-abc001.jpg,我们将自动把 assets/aassb/cn-abc001.jpg 中的 cn 替换为 en。 + +操作步骤: + +- 例如当我们本地开发语言为中文(zh)而需要翻译的语言为英语(en),此时可能会准备如下图中的资源: + + ![source](compile/source-matching.png) + +- 点击 **智能匹配** 后,在弹出的二级菜单内选择确认: + + ![before](compile/before-matching.png) + +- 开发者无需手动添加,不同语言的 role.jpg 将会被自动匹配: + + ![result](compile/matching-result.png) + +## 导入文件 + +通过 **导入文件** 按钮可以将外部数据文件进行导入,支持 PO、CSV 以及 XLSX 格式。 + +对于 CSV 和 XLSX 的格式要求如下图: + +![xlsx](compile/xlsx.png) + +文件需采用 UTF-8 编码格式。 + +![csv](compile/import-csv.png) + +导入后,如需手动翻译,其流程和上述文档一致。 + +详细的导入示例请参考 [示例](example-excel.md)。 + +### 冲突解决 + +如重复导入同一个外部文件,会出现冲突的情况,此时需要手动解决冲突: + +![conflict](compile/conflict.png) + +- **跳过**:跳过冲突的键值,不做额外的处理。 +- **覆盖**:覆盖已有的键值,旧的数据会被丢弃。 diff --git a/versions/4.0/zh/editor/l10n/compile/add-lang.png b/versions/4.0/zh/editor/l10n/compile/add-lang.png new file mode 100644 index 0000000000..c9c5097972 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/add-lang.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/asset.png b/versions/4.0/zh/editor/l10n/compile/asset.png new file mode 100644 index 0000000000..a56e032116 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/asset.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/before-matching.png b/versions/4.0/zh/editor/l10n/compile/before-matching.png new file mode 100644 index 0000000000..fd7c54868a Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/before-matching.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/complement.png b/versions/4.0/zh/editor/l10n/compile/complement.png new file mode 100644 index 0000000000..bc2a350f5e Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/complement.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/conflict.png b/versions/4.0/zh/editor/l10n/compile/conflict.png new file mode 100644 index 0000000000..8c15ee19f1 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/conflict.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/delete.png b/versions/4.0/zh/editor/l10n/compile/delete.png new file mode 100644 index 0000000000..e70db7a858 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/delete.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/export.png b/versions/4.0/zh/editor/l10n/compile/export.png new file mode 100644 index 0000000000..4e9ae19bb8 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/export.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/import-csv.png b/versions/4.0/zh/editor/l10n/compile/import-csv.png new file mode 100644 index 0000000000..f5c8caa606 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/import-csv.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/import-image.png b/versions/4.0/zh/editor/l10n/compile/import-image.png new file mode 100644 index 0000000000..2ef3887ec1 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/import-image.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/import-other-lang-jpg.png b/versions/4.0/zh/editor/l10n/compile/import-other-lang-jpg.png new file mode 100644 index 0000000000..4b1cce3969 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/import-other-lang-jpg.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/lang-provider.png b/versions/4.0/zh/editor/l10n/compile/lang-provider.png new file mode 100644 index 0000000000..df244c0ced Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/lang-provider.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/matching-result.png b/versions/4.0/zh/editor/l10n/compile/matching-result.png new file mode 100644 index 0000000000..4a552a8c9e Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/matching-result.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/new-key.png b/versions/4.0/zh/editor/l10n/compile/new-key.png new file mode 100644 index 0000000000..270eedd9b0 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/new-key.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/original-preview.png b/versions/4.0/zh/editor/l10n/compile/original-preview.png new file mode 100644 index 0000000000..06549fee47 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/original-preview.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/other.png b/versions/4.0/zh/editor/l10n/compile/other.png new file mode 100644 index 0000000000..c26eb121d5 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/other.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/overview.png b/versions/4.0/zh/editor/l10n/compile/overview.png new file mode 100644 index 0000000000..8236703abc Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/overview.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/preview-overview.png b/versions/4.0/zh/editor/l10n/compile/preview-overview.png new file mode 100644 index 0000000000..6df220616c Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/preview-overview.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/progress.png b/versions/4.0/zh/editor/l10n/compile/progress.png new file mode 100644 index 0000000000..292484eba4 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/progress.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/source-matching.png b/versions/4.0/zh/editor/l10n/compile/source-matching.png new file mode 100644 index 0000000000..d8f1ceb130 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/source-matching.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/translate-complete.png b/versions/4.0/zh/editor/l10n/compile/translate-complete.png new file mode 100644 index 0000000000..1cb5d1e0a4 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/translate-complete.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/translate-overview.png b/versions/4.0/zh/editor/l10n/compile/translate-overview.png new file mode 100644 index 0000000000..fa6ef06255 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/translate-overview.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/translate-preview.png b/versions/4.0/zh/editor/l10n/compile/translate-preview.png new file mode 100644 index 0000000000..33c899b304 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/translate-preview.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/translate.png b/versions/4.0/zh/editor/l10n/compile/translate.png new file mode 100644 index 0000000000..ebcd99dc18 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/translate.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/translated-variant.png b/versions/4.0/zh/editor/l10n/compile/translated-variant.png new file mode 100644 index 0000000000..2ca7671654 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/translated-variant.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/unfilled.png b/versions/4.0/zh/editor/l10n/compile/unfilled.png new file mode 100644 index 0000000000..d50cb44eb8 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/unfilled.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/variant.png b/versions/4.0/zh/editor/l10n/compile/variant.png new file mode 100644 index 0000000000..fccfe7f465 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/variant.png differ diff --git a/versions/4.0/zh/editor/l10n/compile/xlsx.png b/versions/4.0/zh/editor/l10n/compile/xlsx.png new file mode 100644 index 0000000000..ff0e68e1c4 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/compile/xlsx.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel.md b/versions/4.0/zh/editor/l10n/example-excel.md new file mode 100644 index 0000000000..d4c394ebf2 --- /dev/null +++ b/versions/4.0/zh/editor/l10n/example-excel.md @@ -0,0 +1,91 @@ +# EXCEL 导入示例 + +在开始本篇之前,请开发者准备好支持 L10N 的引擎(v3.6以及以上),并创建一个空的项目,我们将通过该示例演示如何在项目中使用 EXCEL 文件作为多语言的数据文件。 + +> 其他文件类型如 PO、CSV,其使用流程与 EXCEL 类似。 + +## 准备工作 + +- 首先打开 Dashboard 创建任意空项目 + + ![create-project](example-excel/create-project.png) + +- 通过菜单打开 L10N 的面板 + + ![open-editor](example-excel/open-editor.png) + +- 启用 L10N 功能 + + ![enable](example-excel/enable-l10n.png) + +- 配置好对应的语言,这里 **本地开发语言** 以简体中文为例,目标语言为 **英语(美国)**: + + ![setup-lang](example-excel/setup-lang.png) + +## 导出文件 + +> **注意**:为确保流程正确,请首先使用 **导出** 功能将对应语言的文件导出为 EXCEL。 + +- 添加 **键**,此处我们将以 L10NLabel 为例: + + 在场景中添加任意的 Label ,并在 Label 上添加 L10NLabel 组件。点击刷新按钮生成新的 **键**。 + + ![chinese](example-excel/s-chinese.png) + +- 在 L10N 面板内点击 **补全** 功能填充该 **键** 的原文并点击 **保存** 按钮: + + ![filled](example-excel/filled-text.png) + +- 可选:在 L10N 面板内,点击 **英语(美国)** 的翻译按钮,翻译完成并点击保存: + + ![text](example-excel/translated-text.png) + +- 点击 **导出** 按钮导出 EXCEL 文件: + + ![export excel](example-excel/export-execel.png) + +- 打开 EXCEL 文件可以观察到里面的内容和面板一致: + + ![preview](example-excel/preview-execel.png) + +- 此时可将该文件给与项目中的母语者负责润色或翻译该 EXCEL,翻译后结果如下: + + ![polished](example-excel/polished.png) + +## 导入文件 + +- 在 **编译语言** 中选择下图标识的按钮: + + ![how to import](example-excel/import-data-file.png) + +- 点击 **导入按钮**: + + ![btn-import](example-excel/btn-import.png) + +- 从文件管理器中选择对应的 EXCEL 文件: + + ![select](example-excel/select-execel.png) + +- 此时可以观察到 **导入的文件** 分页内已可以查看刚刚导入的数据。 + + ![imported](example-excel/imported-files.png) + + > **注意**:由于此时导入的键值出现冲突,您必须选择一种解决冲突的方式。 + + 这里我们选择 **覆盖**,这将会使用 EXCEL 内的内容,覆盖项目内原有的内容。 + +- 点击 **保存** 按钮,将已翻译的字段进行保存。 + + ![translated](example-excel/translated.png) + +## 使用 L10N + +- 此时回到场景中,查看原有的 L10NLabel 节点: + + ![create-node](example-excel/create-node.png) + +- 通过 **预览** 按钮可以切换目标语言,并得到正确的结果: + + ![preview](example-excel/preview.png) + +开发者可以下载我们的 [示例](example-excel/en-US.xlsx) 以了解文件格式。但是请注意,如您的项目需要使用数据文件,请从项目中导出。 diff --git a/versions/4.0/zh/editor/l10n/example-excel/btn-import.png b/versions/4.0/zh/editor/l10n/example-excel/btn-import.png new file mode 100644 index 0000000000..4ca9374472 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/btn-import.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/create-node.png b/versions/4.0/zh/editor/l10n/example-excel/create-node.png new file mode 100644 index 0000000000..ef105b724b Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/create-node.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/create-project.png b/versions/4.0/zh/editor/l10n/example-excel/create-project.png new file mode 100644 index 0000000000..a527efc4b2 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/create-project.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/en-US.xlsx b/versions/4.0/zh/editor/l10n/example-excel/en-US.xlsx new file mode 100644 index 0000000000..fd22e6e217 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/en-US.xlsx differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/enable-l10n.png b/versions/4.0/zh/editor/l10n/example-excel/enable-l10n.png new file mode 100644 index 0000000000..50c8e6ffc6 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/enable-l10n.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/export-execel.png b/versions/4.0/zh/editor/l10n/example-excel/export-execel.png new file mode 100644 index 0000000000..76b1ba65f7 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/export-execel.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/filled-text.png b/versions/4.0/zh/editor/l10n/example-excel/filled-text.png new file mode 100644 index 0000000000..275c03df1e Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/filled-text.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/import-data-file.png b/versions/4.0/zh/editor/l10n/example-excel/import-data-file.png new file mode 100644 index 0000000000..3489628da6 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/import-data-file.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/import-execel.png b/versions/4.0/zh/editor/l10n/example-excel/import-execel.png new file mode 100644 index 0000000000..632f760d6a Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/import-execel.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/imported-files.png b/versions/4.0/zh/editor/l10n/example-excel/imported-files.png new file mode 100644 index 0000000000..a2de3cd494 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/imported-files.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/open-editor.png b/versions/4.0/zh/editor/l10n/example-excel/open-editor.png new file mode 100644 index 0000000000..d67405568c Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/open-editor.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/polished.png b/versions/4.0/zh/editor/l10n/example-excel/polished.png new file mode 100644 index 0000000000..d1aa92c1fc Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/polished.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/preview-execel.png b/versions/4.0/zh/editor/l10n/example-excel/preview-execel.png new file mode 100644 index 0000000000..557815e60c Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/preview-execel.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/preview.png b/versions/4.0/zh/editor/l10n/example-excel/preview.png new file mode 100644 index 0000000000..6cae100163 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/preview.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/s-chinese.png b/versions/4.0/zh/editor/l10n/example-excel/s-chinese.png new file mode 100644 index 0000000000..e057a56f9b Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/s-chinese.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/select-execel.png b/versions/4.0/zh/editor/l10n/example-excel/select-execel.png new file mode 100644 index 0000000000..15a1040ab0 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/select-execel.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/select-key.png b/versions/4.0/zh/editor/l10n/example-excel/select-key.png new file mode 100644 index 0000000000..ab2d6346f2 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/select-key.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/setup-lang.png b/versions/4.0/zh/editor/l10n/example-excel/setup-lang.png new file mode 100644 index 0000000000..15d040b2ff Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/setup-lang.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/translated-text.png b/versions/4.0/zh/editor/l10n/example-excel/translated-text.png new file mode 100644 index 0000000000..7adcc5ef8a Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/translated-text.png differ diff --git a/versions/4.0/zh/editor/l10n/example-excel/translated.png b/versions/4.0/zh/editor/l10n/example-excel/translated.png new file mode 100644 index 0000000000..6ae297b98a Binary files /dev/null and b/versions/4.0/zh/editor/l10n/example-excel/translated.png differ diff --git a/versions/4.0/zh/editor/l10n/l10n-label.md b/versions/4.0/zh/editor/l10n/l10n-label.md new file mode 100644 index 0000000000..839289f0de --- /dev/null +++ b/versions/4.0/zh/editor/l10n/l10n-label.md @@ -0,0 +1,82 @@ +# L10nLabel 组件 + +L10nLabel 是可以根据内容进行定制化翻译的组件。配合文本组件使用,可对文本组件的内容进行翻译。 + +![overview](l10nlabel/overview.png) + +## 添加组件 + +用户可以在 **属性检查器** 上点击 **添加组件** 并选择 L10nLabel 来添加: + +![add](l10nlabel/add.png) + +> **注意**:L10nLabel 必须搭配 Label 组件才可以使用,如果节点上没有 Label 组件,则会自动创建一个。 + +## 属性和说明 + +| 属性 | 说明 | +| :-- | :-- | +| **String** | Label 组件内文本
不可编辑| +| **Count** | 变体数量
详情请参考下文 | +| **Key** | 本地化的键值 | + +### 变体数量 + +通过输入不同的数量,本地化会切换不同的变体。 + +在某些语言中,表示单数和复数会采用不同的格式,比如在英语中,一个苹果表述为:one apple,而两个苹果则会使用苹果的复数格式:two apples。为了可以在翻译后使用正确的格式,我们可以在 **变体数量** 内填入当前这句话内指定的物体的数量究竟是多少。 + +通常来说,不同语言的变体数量和当地语言有关,开发者可以向母语者进行咨询。 + +例如:英语有单数/复数的区别,因此在配置时变体有两种 `one` 和 `other`。某些语言如阿拉伯语有 5 种变体而俄语有 3 种变体。引擎通过适配国际规则来生成变体的数量,如果希望了解这些规则,可以参考下列地址: + +- ECMAScript Internationalization API:[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) +- Unicode CLDR Project:[https://cldr.unicode.org/](https://cldr.unicode.org/) + +下面我们将通过一个详细的例子来进行说明,对于不熟悉 **本地化编辑** 面板的用户,可以先参考 [本地化](./overview.md): + +- 首先启用 **本地化编辑**,并选择中文作为开发语言: + + ![dev-lang](l10nlabel/dev-lang.png) + +- 在场景内添加一个带有 Label 组件的节点: + + ![apple](l10nlabel/pingguo.png) + +- 给这个节点添加一个 L10nLabel 的组件,并将 Key 修改为:apple-key: + + ![apple-key](l10nlabel/apple-key.png) + +- 添加需要翻译的语言为英语 + + ![english](l10nlabel/english.png) + +- 在 **翻译** 界面内添加变体: + + ![l10nlabel/apple-variant.png](l10nlabel/apple-variant.png) + +- 点击 **本地化编辑** 面板的 **预览** 操作,可以预览翻译后的结果,在 Count 属性中切换单数/复数时,将会显示不同的变体: + + ![result](l10nlabel/variant-result.gif) + +### 键 + +全局唯一的键,本地化会根据键不同,获取到该键在本地化中的结果。开发者可以自行输入自定义的键。也可以通过 **重置** 增加新的键。 + +![key](l10nlabel/key-prop.png) + +#### 重置键 + +![reset](l10nlabel/reset-key.png) + +如果需要新的键可以点击图示的重置按钮,此时 L10N 将会生成新的键,这些键随后也可以在 **本地化编辑** 中,在 **编译** 操作打开的面板内进行编辑: + +![compile](l10nlabel/compile.png) + +详情请参考 [语言编译](compile-language.md) + +#### 选择键 + +通过图示的下拉菜单可以选择已有键: + +![select](l10nlabel/select-key.png) diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/add.png b/versions/4.0/zh/editor/l10n/l10nlabel/add.png new file mode 100644 index 0000000000..034490af48 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/add.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/apple-key.png b/versions/4.0/zh/editor/l10n/l10nlabel/apple-key.png new file mode 100644 index 0000000000..c20d5600bb Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/apple-key.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/apple-variant.png b/versions/4.0/zh/editor/l10n/l10nlabel/apple-variant.png new file mode 100644 index 0000000000..51637e7a23 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/apple-variant.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/compile.png b/versions/4.0/zh/editor/l10n/l10nlabel/compile.png new file mode 100644 index 0000000000..cb30c40949 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/compile.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/dev-lang.png b/versions/4.0/zh/editor/l10n/l10nlabel/dev-lang.png new file mode 100644 index 0000000000..51d364a6fd Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/dev-lang.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/english.png b/versions/4.0/zh/editor/l10n/l10nlabel/english.png new file mode 100644 index 0000000000..bff0b02d2d Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/english.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/key-prop.png b/versions/4.0/zh/editor/l10n/l10nlabel/key-prop.png new file mode 100644 index 0000000000..94960dd963 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/key-prop.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/overview.png b/versions/4.0/zh/editor/l10n/l10nlabel/overview.png new file mode 100644 index 0000000000..d62fcb2cff Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/overview.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/pingguo.png b/versions/4.0/zh/editor/l10n/l10nlabel/pingguo.png new file mode 100644 index 0000000000..2b0c0bf78a Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/pingguo.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/reset-key.png b/versions/4.0/zh/editor/l10n/l10nlabel/reset-key.png new file mode 100644 index 0000000000..8a509c3f43 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/reset-key.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/select-key.png b/versions/4.0/zh/editor/l10n/l10nlabel/select-key.png new file mode 100644 index 0000000000..ebeda568be Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/select-key.png differ diff --git a/versions/4.0/zh/editor/l10n/l10nlabel/variant-result.gif b/versions/4.0/zh/editor/l10n/l10nlabel/variant-result.gif new file mode 100644 index 0000000000..b320f60b32 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/l10nlabel/variant-result.gif differ diff --git a/versions/4.0/zh/editor/l10n/localization-editor-api.md b/versions/4.0/zh/editor/l10n/localization-editor-api.md new file mode 100644 index 0000000000..5841b8668a --- /dev/null +++ b/versions/4.0/zh/editor/l10n/localization-editor-api.md @@ -0,0 +1,346 @@ +# Localization Editor Api + +## 快速开始 + +### 核心功能 `l10n` + +l10n 提供了核心翻译功能以及 ICU 功能,同时也提供的切换语言的功能。 + +我们会将切换后的目标语言存储于 `localStorage` 中,同时也会自动重启项目运行时,并在下次启动时读取 `localStorage` 配置以完成整个语言切换流程。 + +> **注意**:因此我们希望用户在切换语言之前务必处理好数据持久化工作。 + +### 导入 `l10n` + +localization-editor 所提供的所有 API 都将从 db://localization-editor/l10n 进行具名导入,导入示例如下: + +```typescript +import { l10n } from 'db://localization-editor/l10n' +``` + +### 使用翻译api + +```typescript +// 任意 component 组件代码中 + +// l10n 是 localization 的核心功能 +import { l10n } from 'db://localization-editor/l10n' +import { _decorator, Label, Component } from 'cc'; + +@ccclass('SomeComponent') +class SomeComponent extends Component { + // ...... + someMethod() { + // 将返回 this_is_an_apple 所对应文案 + const text = l10n.t("this_is_an_apple") + } + // ...... +} +``` + +## API详细说明 + +- 类:[`L10nManager`](#l10nmanager) + +--- + +- 接口:[`ResourceList`](#resourcelist) + +- 接口:[`ResourceBundle`](#resourcebundle) + +- 接口:[`ResourceData`](#resourcedata) + +- 接口:[`ResourceItem`](#resourceitem) + +- 接口:[`FallbackLanguageObjectList`](#fallbacklanguageobjectlist) + +- 接口:[`L10nOptions`](#l10noptions) + +- 接口:[`StandardOption`](#standardoption) + +--- + +- 枚举:[`L10nListenEvent`](#l10nlistenevent) + +--- + +- 别名:[`L10nKey`](#别名) + +- 别名:[`L10nValue`](#别名) + +- 别名:[`TextInfoDirection`](#别名) + +- 别名:[`FallbackLanguage`](#别名) + +--- + +## `L10nManager` + +导入示例: + +```ts +import { L10nManager } from 'db://localization-editor/l10n' +``` + +描述: + +通常我们不建议您自行使用或构造该类型。 + +而我们提供了[`l10n`](#l10n)作为全局单例以使用翻译功能。 + +--- + +### 索引 + +#### 构造函数 + +- `L10nManager` **private** + +--- + +#### 全局变量 + +#### `l10n` + +定义: `const l10n: L10nManager` + +--- + +### 静态属性 + +#### `LOCAL_STORAGE_LANGUAGE_KEY` + +定义: `static LOCAL_STORAGE_LANGUAGE_KEY: string` + +描述: 当调用[`changeLanguage`](#changelanguage)切换游戏语言时,将使用`localStorage` +存储所切换的目标语言标记,并且使用[`LOCAL_STORAGE_LANGUAGE_KEY`](#localstoragelanguagekey)作为`localStorage`的key + +备注: + +| 默认值 | localization-editor/language | +|-----|------------------------------| +--- + +### 实例方法 + +#### `config` + +定义: `config(options: L10nOptions): void` + +描述: 用于配置l10n的某些设置,探索更多选项可以查看[`L10nOptions`](#l10noptions) + +用例: + +```ts +l10n.config({ + // 用于在默认语言没有找到相应翻译时,以该值进行补充显示 + fallbackLanguage: 'zh-Hans-CN', + // 如果不喜欢LOCAL_STORAGE_LANGUAGE_KEY的默认值,可以在此修改,但是需要确保在changeLanguage之前 + localStorageLanguageKey: 'localization-editor/langauge' +}) + +``` + +--- + +##### `changeLanguage` + +定义: `changeLanguage(language: Intl.BCP47LanguageTag): void` + +描述: 用于动态切换语言,请查看[`BCP47 Language Tag`](https://www.techonthenet.com/js/language_tags.php)以获得更多信息 + +用例: + +```ts +l10n.changeLanguage('zh-Hans-CN') +``` + +> **注意**: 在调用此方法后,会自动重启游戏,请务必做好数据持久化工作。 + +--- + +##### `t` + +定义: `t(key: L10nKey, options?: StandardOption): L10nValue` + +描述: 根据传入的L10nKey,返回当前语言数据中所对应的L10nValue,探索更多选项可以查看[`StandardOption`](#standardoption) + +用例: + +```ts +console.log(l10n.t('this_is_apple')) +// 这是一个苹果 +``` + +> **注意**: 语言数据需要配合 Localization Editor 扩展在编译后生成。 +--- + +##### `exists` + +定义: `exists(key: L10nKey): boolean` + +描述: 返回是否存在key + +用例: + +```ts +console.log(l10n.exists('test_key')) +``` + +--- + +##### `currentLanguage` + +定义: `get currentLanguage(): Intl.BCP47LanguageTag` + +描述: 返回当前语言的[`BCP47 Language Tag`](https://www.techonthenet.com/js/language_tags.php) + +用例: + +``` +console.log(l10n.currentLanguage) +// 'zh-Hans-CN' +``` + +--- + +##### `languages` + +定义: `get languages(): readonly Intl.BCP47LanguageTag[]` + +描述: 返回当前可用语言的[`BCP47 Language Tag`](https://www.techonthenet.com/js/language_tags.php)数组,可利用该方法作为切换语言下拉框的数据源 + +用例: + +```ts +console.log(l10n.languages) +// ['zh-Hans-CN', 'en-US'] +``` + +--- + +##### `direction` + +定义: `direction(language?: Intl.BCP47LanguageTag): TextInfoDirection` + +描述: 绝大多数语言都尊崇从左到右的阅读习惯,但某些语言却例外比如阿拉伯语,此方法可以得知所传入语言的[TextInfoDirection](#textinfodirection) + +用例: + +```ts +console.log(l10n.direction('ar')) +// 'rtl' +``` + +--- + +##### `on` + +定义: `on(event: L10nListenEvent, callback: (...args: any[]) => void)` + +描述: 用于注册[l10n](#l10n)的[L10nListenEvent](#l10nlistenevent)事件回调,比如`languageChanged` + +用例: + +```ts +l10n.on(L10nListenEvent.languageChanged, (...args: any[]) => { + //在切换语言后的一些操作,某些数据可以放在这里持久化,之后便会重启整个游戏场景 +}) +``` + +--- + +##### `off` + +定义: `off(event: L10nListenEvent, callback: (...args: any[]) => void)` + +描述: 用于反注册[l10n](#l10n)的[L10nListenEvent](#l10nlistenevent)事件回调 + +> ***请务必使on与off成对出现,确保正确的销毁无用数据*** + +--- + +## 别名 + +| 别名 | 原类型 | +|---------------------|-------------------------------------| +| `L10nKey` | `string` | +| `L10nValue` | `string` | +| `TextInfoDirection` | `'ltr' / 'rtl'` | +| `FallbackLanguage` | `string / readonly string[] / FallbackLanguageObjectList / ((language: Intl.BCP47LanguageTag) => string / readonly string[] / FallbackLanguageObjectList` | +--- + +## 接口 + +### `L10nOptions` + +| 函数/变量名 | 类型 | 可选 | +|---------------------------|-------------------------------------|-----| +| `fallbackLanguage` | `false` / [`FallbackLanguage`](#别名) | 是 | +| `localStorageLanguageKey` | `string` | 是 | +| `beforeTranslate` | `(key: L10nKey) => L10nValue` | 是 | +| `afterTranslate` | `(key: L10nKey) => L10nValue` | 是 | +| `returnNull` | `boolean` | 是 | +| `returnEmptyString` | `boolean` | 是 | + +--- + +### `ResourceList` + +| 函数/变量名 | 类型 | 可选 | +|--------------------|---------------------------|-----| +| `defaultLanguage` | `Intl.BCP47LanguageTag` | 是 | +| `fallbackLanguage` | `Intl.BCP47LanguageTag` | 是 | +| `languages` | `Intl.BCP47LanguageTag[]` | 否 | + +### `ResourceBundle` + +| 函数/变量名 | 类型 | 可选 | +|-------------------------------------|---------------------------------|-----| +| `[language: Intl.BCP47LanguageTag]` | [`ResourceData`](#resourcedata) | 否 | + +--- + +### `ResourceData` + +| 函数/变量名 | 类型 | 可选 | +|-----------------------|---------------------------------|-----| +| `[namespace: string]` | [`ResourceItem`](#resourceitem) | 否 | + +--- + +### `ResourceItem` + +| 函数/变量名 | 类型 | 可选 | +|-----------------|-------|-----| +| `[key: string]` | `any` | 否 | + +--- + +### `FallbackLanguageObjectList` + +| 函数/变量名 | 类型 | 可选 | +|----------------------|---------------------|-----| +| `[language: string]` | `readonly string[]` | 否 | + +--- + +### `StandardOption` + +| 函数/变量名 | 类型 | 可选 | +|--------------------|---------------------------|-----| +| `count` | `number` | 是 | +| `defaultValue` | `L10nValue` | 是 | +| `language` | `Intl.BCP47LanguageTag` | 是 | +| `fallbackLanguage` | [`FallbackLanguage`](#别名) | 是 | + +--- + +## 枚举 + +### `L10nListenEvent` + +| 函数/变量名 | 类型 | +|-------------------|-------------------| +| `languageChanged` | `languageChanged` | +| `onMissingKey` | `missingKey` | diff --git a/versions/4.0/zh/editor/l10n/overview.md b/versions/4.0/zh/editor/l10n/overview.md new file mode 100644 index 0000000000..2f1872a25e --- /dev/null +++ b/versions/4.0/zh/editor/l10n/overview.md @@ -0,0 +1,85 @@ +# 多语言本地化(L10N) + +多语言本地化(以下简称 L10N 或本地化)是 Cocos Creator 3.6 推出的功能,该功能整合了第三方译文服务商的翻译服务,同时运行将文本、音频和图片等资源的本地化功能整合到引擎内,并支持一键发布到不同语言。 + +> L10N 是单词 Localization 的首字母以及尾字母的缩写,10 代表 Localization 中间有 10 个字母。 + +## L10N 总览 + +在引擎顶部菜单中选择 **面板** -> **本地化编辑器** 中即可打开本地化编辑器面板。
+ +首次启动时,用户需手动启用 L10N 功能: + +![enable](overview/enable.png) + +启动后即可进行编辑。 + +![overview](overview/overview.png) + +在面板的右上角,L10N 提供了 **关闭 L10N** 以及 **卸载数据** 两项功能: + +![menu](overview/menu.png) + +- **关闭 L10N**:关闭 L10N 功能。在弹出的二级菜单内选择确认后,数据依旧保留,开发者可再次启用该功能: + + ![alert](overview/close-alert.png) + +- **卸载数据**:在弹出的二级菜单内选择确认后,L10N 的数据将会被清除,场景内相关的组件也会被删除: + + ![alert](overview/uninstal-alert.png) + + > **注意**:卸载后的数据无法恢复,请谨慎操作。 + +## 译文服务商 + +![overview](translation-service/overview.png) + +引擎整合了多家译文服务商的翻译服务,开发者可以选择不同的译文服务商以提供更完善和多样化的服务。详情请参考 [译文服务商](translation-service.md)。 + +## 收集并统计 + +![ovefview](collect/overview.png) + +收集并统计将项目中可能需要翻译的文本内容进行收集,方便进行翻译,开发者也可以通过指定特定的文件、目录或通过配置进行过滤。详情请参考 [搜集和统计](collect-and-count.md) + +## 语言编译 + +![overview](compile/overview.png) + +语言编译可以查看当前已配置和翻译的语言,并查看其进度或删除操作。同时也提供对某些非文本资源的手动处理。详情请参考 [语言编译](compile-language.md) + +## 程序化使用 + +请参考 [示例](script-using.md) 以获取程序化使用 L10N 的示例。 + +API 参考请移步 [Localization Editor Api](localization-editor-api.md)。 + +## 发布 + +根据上述的步骤完成翻译后,可以在发布界面进行一键发布: + +![publish](overview/publish.png) + +- 使用的语言:这里将包含所有在 **本地化编辑** 中设置的语言,开发者可根据版本不同按需选择 +- 默认语言:发布后,项目启动时所使用的语言 +- 备用语言:默认语言出现问题后使用的备用语言 + +## Q & A + +- Q:若我不慎修改了数据文件导致数据文件损坏而无法打开 L10N 时,是否有解决办法? + A:可以通过在 "{项目地址}\localization-editor\translate-data" 搜索对应文件进行修改。 + +## 示例 + +大型项目的多语言解决方案通常会采用数据文件(PO、CSV、EXCEL 等)来解决,为此我们也准备一篇 [示例](example-excel.md) 供开发者参考。 + +## 内容 + +本章将包含以下内容: + +- [L10nLabel 组件](./l10n-label.md) +- [收集并统计](./collect-and-count.md) +- [语言编译](./compile-language.md) +- [EXCEL 导入示例](./example-excel.md) +- [脚本使用示例](./script-using.md) + - [L10N API 参考](./localization-editor-api.md) diff --git a/versions/4.0/zh/editor/l10n/overview/close-alert.png b/versions/4.0/zh/editor/l10n/overview/close-alert.png new file mode 100644 index 0000000000..870e0b2724 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/overview/close-alert.png differ diff --git a/versions/4.0/zh/editor/l10n/overview/enable.png b/versions/4.0/zh/editor/l10n/overview/enable.png new file mode 100644 index 0000000000..bb5db137f0 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/overview/enable.png differ diff --git a/versions/4.0/zh/editor/l10n/overview/menu.png b/versions/4.0/zh/editor/l10n/overview/menu.png new file mode 100644 index 0000000000..0febbf8ec2 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/overview/menu.png differ diff --git a/versions/4.0/zh/editor/l10n/overview/overview.png b/versions/4.0/zh/editor/l10n/overview/overview.png new file mode 100644 index 0000000000..8517baad20 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/overview/overview.png differ diff --git a/versions/4.0/zh/editor/l10n/overview/publish.png b/versions/4.0/zh/editor/l10n/overview/publish.png new file mode 100644 index 0000000000..22fc414a15 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/overview/publish.png differ diff --git a/versions/4.0/zh/editor/l10n/overview/uninstal-alert.png b/versions/4.0/zh/editor/l10n/overview/uninstal-alert.png new file mode 100644 index 0000000000..58d8dcd293 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/overview/uninstal-alert.png differ diff --git a/versions/4.0/zh/editor/l10n/script-using.md b/versions/4.0/zh/editor/l10n/script-using.md new file mode 100644 index 0000000000..0e2f72f2c9 --- /dev/null +++ b/versions/4.0/zh/editor/l10n/script-using.md @@ -0,0 +1,75 @@ +# 脚本使用示例 + +## 导入 + +导入示例: + +```ts +import l10n from 'db://localization-editor/core/L10nManager' +``` + +- 描述:l10n以api的方式提供了在代码中翻译文本的能力 + +## 动态切换语言 + +代码示例如下: + +```ts +l10n.changeLanguage('zh-Hans-CN') +``` + +参数类型请参考 查看 [BCP47 Language Tag](https://www.techonthenet.com/js/language_tags.php) 以获得更多信息。 + +> **注意**: 在调用此方法后,会自动重启游戏,请务必做好数据持久化工作。 + +- 接口定义: `t(key: L10nKey, options?: StandardOption): L10nValue` + +## 根据键获取 L10N 的值 + +```ts +console.log(l10n.t('this_is_apple')) +// 这是一个苹果 +``` + +此处可以获取到以 `this_is_apple` 为键的当前语言的值。 + +## 查询某个键是否存在 + +代码示例如下: + +```ts +console.log(l10n.exists('test_key')) +``` + +### 获取当前的语言 + +代码示例如下: + +```ts +console.log(l10n.currentLanguage) +// 'zh-Hans-CN' +``` + +返回当前语言的 [BCP47 Language Tag](https://www.techonthenet.com/js/language_tags.php)。 + +### 获取所有可用语言 + +代码示例如下: + +```ts +console.log(l10n.languages) +// ['zh-Hans-CN', 'en-US'] +``` + +返回当前可用的语言的 [BCP47 Language Tag](https://www.techonthenet.com/js/language_tags.php) 数组。 + +## 获取语言的方向 + +绝大多数语言都遵循从左到右的阅读习惯,但某些语言却例外比如阿拉伯语,此方法可以得知所传入语言的 `TextInfoDirection` + +```ts +console.log(l10n.direction('ar')) +// 'rtl' +``` + +更多详细的 API 描述请参考 [Localization Editor Api](localization-editor-api.md) diff --git a/versions/4.0/zh/editor/l10n/translation-service.md b/versions/4.0/zh/editor/l10n/translation-service.md new file mode 100644 index 0000000000..29c502bf20 --- /dev/null +++ b/versions/4.0/zh/editor/l10n/translation-service.md @@ -0,0 +1,22 @@ +# 译文服务商 + +译文服务商是第三方软件商,引擎通过抹平他们之间 API 的差异将其整合到一起。通常开发者需要注册服务商的账号并开启相应的 API 才可以启动自动翻译的功能。 + +若您没有对应的开发者账号也无需担心,L10N 支持手动翻译。 + +![service](translation-service/overview.png) + +- **译文服务商**:该下拉菜单允许开发者选择不同的译文服务商,如选择 **None** 则无法使用自动翻译功能,手动翻译功能不受影响。 + + ![select](translation-service/select.png) + + 目前支持的服务商的地址如下: + + - [有道智云平台](https://ai.youdao.com/gw.s#/) + - [Google Cloud](https://cloud.google.com) + +- **AppKey**/**AppSecret**:选择不同的服务商后,开发者需要输入 AppKey 和 AppSecret 才可以继续后续的操作。通常这些信息需要在译文服务商的网站上获取。 + + ![key](translation-service/youdao.png) + + 输入完成后,点击保存即可。 diff --git a/versions/4.0/zh/editor/l10n/translation-service/overview.png b/versions/4.0/zh/editor/l10n/translation-service/overview.png new file mode 100644 index 0000000000..8cedb5221d Binary files /dev/null and b/versions/4.0/zh/editor/l10n/translation-service/overview.png differ diff --git a/versions/4.0/zh/editor/l10n/translation-service/select.png b/versions/4.0/zh/editor/l10n/translation-service/select.png new file mode 100644 index 0000000000..7c323a7e46 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/translation-service/select.png differ diff --git a/versions/4.0/zh/editor/l10n/translation-service/youdao.png b/versions/4.0/zh/editor/l10n/translation-service/youdao.png new file mode 100644 index 0000000000..2ff5c9c549 Binary files /dev/null and b/versions/4.0/zh/editor/l10n/translation-service/youdao.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/cocos.png b/versions/4.0/zh/editor/mainMenu/img/cocos.png new file mode 100644 index 0000000000..9b38b3a004 Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/cocos.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/developer.png b/versions/4.0/zh/editor/mainMenu/img/developer.png new file mode 100644 index 0000000000..75b9a2fe31 Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/developer.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/edit.png b/versions/4.0/zh/editor/mainMenu/img/edit.png new file mode 100644 index 0000000000..e40f0b2007 Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/edit.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/extension.png b/versions/4.0/zh/editor/mainMenu/img/extension.png new file mode 100644 index 0000000000..7bf30d8d8e Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/extension.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/file.png b/versions/4.0/zh/editor/mainMenu/img/file.png new file mode 100644 index 0000000000..a027dd0b00 Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/file.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/help.png b/versions/4.0/zh/editor/mainMenu/img/help.png new file mode 100644 index 0000000000..6eba952977 Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/help.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/menu.png b/versions/4.0/zh/editor/mainMenu/img/menu.png new file mode 100644 index 0000000000..8c3c60f9dd Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/menu.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/node.png b/versions/4.0/zh/editor/mainMenu/img/node.png new file mode 100644 index 0000000000..e643f686f9 Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/node.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/panel.png b/versions/4.0/zh/editor/mainMenu/img/panel.png new file mode 100644 index 0000000000..2b526f8265 Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/panel.png differ diff --git a/versions/4.0/zh/editor/mainMenu/img/project.png b/versions/4.0/zh/editor/mainMenu/img/project.png new file mode 100644 index 0000000000..6b5ad1d5c8 Binary files /dev/null and b/versions/4.0/zh/editor/mainMenu/img/project.png differ diff --git a/versions/4.0/zh/editor/mainMenu/index.md b/versions/4.0/zh/editor/mainMenu/index.md new file mode 100644 index 0000000000..9e2ff3bebb --- /dev/null +++ b/versions/4.0/zh/editor/mainMenu/index.md @@ -0,0 +1,150 @@ +# 主菜单 + +Cocos Creator 顶部的主菜单栏中包括 **文件**、**编辑**、**节点**、**项目**、**面板**、**扩展**、**开发者** 和 **帮助** 8 个菜单项,集成了 Cocos Creator 大部分的功能点。 + +![menu](./img/menu.png) + +## 文件 + +该项主要用于项目或场景的新建、打开、保存等操作,以及导入 Cocos Creator 2.x 项目。 + +![file](./img/file.png) + +| 选项 | 说明 | +| :--- | :-- | +| 新建项目 | 打开 Dashboard 的 [项目](../../getting-started/dashboard/index.md#%E9%A1%B9%E7%9B%AE) 分页,以新建一个项目。若使用的 Dashboard 版本是 v1.0.19,则会打开 Dashboard 的 [新建项目](../../getting-started/dashboard/index.md#%E6%96%B0%E5%BB%BA%E9%A1%B9%E7%9B%AE) 分页 | +| 打开项目 | 打开 Dashboard 的 [项目](../../getting-started/dashboard/index.md#%E9%A1%B9%E7%9B%AE) 分页 | +| 新建场景(Ctrl/Cmd + N) | 关闭当前场景并创建一个新场景,新创建的场景需要手动保存才会添加到项目目录下 | +| 保存场景(Ctrl/Cmd + S) | 保存当前正在编辑的场景,如果是使用 **文件 -> 新建场景** 创建的场景,在第一次保存时会弹出对话框,需要选择场景文件保存的位置并填写文件名,然后点击 **保存** 即可。场景文件以 `.scene` 作为扩展名 | +| 另存为(Ctrl/Cmd +Shift+ S) | 生成当前场景文件的副本,并保存在项目中| +| 偏好设置 | 打开 [偏好设置](../preferences/index.md) 面板,对编辑器进行个性化设置 | +| 布局 | 设置编辑器界面布局,通过二级菜单可以保存当前局部至 **自定义布局** | +| 快捷键 | 打开快捷键设置面板,查看编辑器各面板或功能模块默认使用的快捷键,支持自定义 | +| 导入 Cocos Creator 2.x 项目 | v2.x 资源导入工具,支持旧项目资源完美导入,以及代码的辅助迁移。详情请参考 [v3.0 升级指南](../../release-notes/upgrade-guide-v3.0.md) | +| 关闭窗口(Ctrl/Cmd + W)| 关闭当前窗口| +| 退出 | 关闭当前编辑器,并自动打开 Dashboard | + +## 编辑 + +该项主要包括撤销、重做、复制和粘贴等常用编辑功能。 + +![edit](./img/edit.png) + +| 选项 | 说明 | 默认快捷键 | +| :--- | :-- | :--| +| 撤销 | 撤销上一次对场景的修改 | Ctrl/Cmd + Z +| 重做 | 恢复上一步骤的撤销动作 | Ctrl/Cmd + Shift + Z +| 剪切 | 剪切当前选中的节点或字符到剪贴板| Ctrl/Cmd + X +| 复制 | 复制当前选中的节点或字符到剪贴板 | Ctrl/Cmd + C +| 粘贴 | 将剪贴板中的内容粘贴到相应的位置| Ctrl/Cmd + V +| 全选 | 焦点在层级管理器内为选中同一层级中的所有节点,焦点在资源管理器则选中同一层级中的所有资源 | Ctrl/Cmd + A + +> **注意**:节点的 **复制**、**剪切** 和 **粘贴** 请使用快捷键进行操作。 + +## 节点 + +该项包含调整视角、取消关联预制节点和创建节点等功能。 + +![node](./img/node.png) + +| 选项 | 说明 | +| :--- | :-- | +| 视角对齐(Ctrl/Cmd + Shift + F) | 将当前选中节点移动到 **场景编辑器** 的视角中心 | +| 将视角和节点对齐 | 将 **场景编辑器** 中的视角设置为以当前选中节点为中心 | +| 取消关联当前的 Prefab 资源 | 用于将选中的预制节点转化成普通节点,详情请参考 [Prefab](../../asset/prefab.md) | +| 递归地取消关联当前的 Prefab 资源 | 用于将场景中包含嵌套的预制节点递归地转化为普通节点,详情请参考 [Prefab](../../asset/prefab.md) | +| 空节点 | 在场景中创建一个空节点,如果执行命令前场景中已经选中了节点,新建的节点会成为选中节点的子节点| +| 3D 对象 | Creator 提供了一些比较基础的静态模型控件以便使用,目前包括 **立方体**、**圆柱体**、**球体**、**胶囊**、**圆锥体**、**圆环体**、**平面** 和 **四方形**。若需要创建其他类型的模型,可参考 [MeshRenderer 组件](../../engine/renderable/model-component.md)| +| 2D 对象 | 在场景中创建 Creator 预设好的包含基础渲染组件的 2D 节点,目前包括 **Graphics**(绘图)、**Label**(文本)、**Mask**(遮罩)、**ParticleSystem2D**(粒子)、**Sprite**(精灵)、**SpriteSplash**(单色)和 **TiledMap**(地图),详细情况可参考 [2D 渲染组件介绍](../../ui-system/components/editor/render-component.md)| +| UI 组件 | 在场景中创建 Creator 预设好的包含基础 UI 组件的节点,目前包括 **Button**(按钮)、**Widget**(对齐挂件)、**Layout**(布局)、**ScrollView**(滚动视图)、**EditBox**(输入框)等节点在内的常用 UI 控件。更多 UI 组件介绍,可以查看 [UI 基础组件](../../ui-system/components/editor/base-component.md)| +| 光线 | 在场景中创建 Creator 预设好的包含基础光源组件的节点,目前包括 **平行光**、**球面光**、**聚光** 以及 **光照探针** 和 **反射探针**,详情请参考 [光照](../../concepts/scene/light.md)| +| 特效 | 在场景中创建 Creator 预设好的包含 [粒子系统](../../particle-system/overview.md) 组件的节点。更多特效组件可在 **属性检查器** 面板点击 **添加组件 -> 特效** 进行添加。| +| 摄像机 | 在场景中创建 Creator 预设好的包含 Camera 组件的节点。具体的使用方式,详情请参考 [Camera 组件](../components/camera-component.md)| +| 地形 | 在场景中创建 Creator 预设好的包含地形组件的节点。具体的使用方式,详情请参考 [地形系统](../terrain/index.md)| + +## 项目 + +该项主要用于执行预览运行构建项目、项目配置和自定义构建等。 + +![project](./img/project.png) + +| 选项 | 说明 | +| :--- | :-- | +| 项目设置 | 打开 [项目设置](../project/index.md) 面板,设置特定项目的相关配置项 | +| 光照贴图 | 打开 [光照贴图](../../concepts/scene/light/lightmap.md) 面板,配置生成光照贴图的烘焙参数 | +| 运行预览(Ctrl/Cmd + P) | 点击该项即可在选择的浏览器/模拟器中预览项目效果,详情请参考 [预览调试](../preview/index.md)| +| 刷新预览(Ctrl/Cmd + Shift + P) | 刷新已经打开的浏览器预览窗口 | +| 生成预览模板 | 该项用于自定义想要的预览效果,详情请参考 [自定义预览模板](../preview/browser.md)| +| 构建发布(Ctrl/Cmd + Shift + B) | 打开 [构建发布](../publish/index.md) 面板,将游戏打包发布到目标平台上 | +| 构建项目构建模板 | 该项用于定制项目的构建模板,详情请参考 [自定义构建模板](../publish/custom-project-build-template.md)| +| 新建构建扩展包 | 该项用于自定义构建流程,详情请参考文档 [自定义构建流程](../publish/custom-build-plugin.md)| + +## 面板 + +该项主要用于打开编辑器中的各式面板。 + +![panel](./img/panel.png) + +| 选项 | 说明 |默认快捷键 | +| :--- | :-- |:-- | +| 控制台 | 打开 [控制台](../console/index.md),用于查看输出的日志信息 |Ctrl/Cmd + 0 +| 场景编辑器 | 打开 [场景编辑器](../scene/index.md) 面板。用于选择和摆放场景图像、角色、特效、UI 等各类游戏元素 |Ctrl/Cmd + 1 +| 资源管理器 | 打开 **资源管理器**/**资源预览** 面板,用于访问/管理/查看项目资源,详情请参考 [资源管理器](../assets/index.md) |资源管理器为 Ctrl/Cmd + 2 +| 属性检查器 | 打开 [属性检查器](../inspector/index.md) 面板,用于查看并编辑当前选中节点、节点组件和资源的工作区域 |Ctrl/Cmd + 3 +| 层级管理器 | 打开 [层级管理器](../hierarchy/index.md) 面板,该面板以树状列表的形式展示了场景中所有节点之间的层级关系 |Ctrl/Cmd + 4 +| 预览 | 打开 **摄像机预览** 面板,该面板展示的画面与在场景中选中对应 Camera 节点时,**场景编辑器** 右下角展示的画面一致。当调整场景时,摄像机预览面板中的画面也会实时同步 |Ctrl/Cmd + 5 +| 动画 | 可以打开 [动画编辑器](../../animation/index.md) 面板或者 [Joint Texture Layout(骨骼贴图布局)](../../animation/joint-texture-layout.md) 面板,用于对帧动画或者骨骼动画进行编辑、查看等功能 |动画编辑器为 Ctrl/Cmd + 6 +| 节点预制库 | 打开节点预制库面板。该面板是一个非常简单直接的可视化控件仓库,开发者可以将这里列出的控件拖拽到 **场景编辑器** 或 **层级管理器** 中,快速完成预设控件的创建|Ctrl/Cmd + 7 +| 服务 | 打开 [服务](https://service.cocos.com/document/zh) 面板,该面板提供了一些优质的官方以及第三方服务,方便开发者获得更多引擎之外的扩展能力,让游戏开发工作变得更加简单 |Ctrl/Cmd + 8 +| 扩展示例 | 可以打开一个默认的扩展示例 | +| Cocos 小秘书 | 打开 [Cocos 小秘书](https://www.cocos.com/assistant) 面板,该面板与 Cocos Creator 编辑器紧密结合,⽆缝嵌⼊开发环境,⽆论开发者遇到任何问题,随时可以向官方技术人员发起交流。目前仅支持 Creator 中文版 | + +## 扩展 + +该项主要包括扩展管理器、商城和创建扩展等,详情请参考 [编辑器扩展](../extension/readme.md) 一章。 + +![extension](./img/extension.png) + +| 选项 | 说明 | +| :--- | :-- | +| 扩展管理器 | 打开 **扩展管理器** 面板,其中包括了编辑器内置扩展、项目目录及全局目录下安装的扩展,详情请参考 [编辑器扩展](../extension/readme.md) | +| 商城 | 点击该项即可进入 Cocos Creator 内置的 [扩展商城](https://store.cocos.com/app/),可供用户浏览、下载和自动安装官方或者第三方扩展和资源。同时用户也可以将自己开发的扩展、美术素材、音乐音效等资源提交到扩展商店以便分享或者售卖。详情请参考 [提交资源到商店](../extension/store/upload-store.md) | +| 创建扩展 | 该项用于在项目/全局目录下生成一个 [扩展包](../extension/first.md),以便对编辑器进行功能扩展 | + +## 开发者 + +该项主要包含脚本、引擎和调试工具等开发相关的菜单功能。 + +![developer](./img/developer.png) + +| 选项 | 说明 | +| :--- | :-- | +| 编译引擎(Ctrl/Cmd + F7) | 编译自定义引擎,详情请参考 [引擎定制工作流程](../../advanced-topics/engine-customization.md) | +| 编译原生模拟器引擎 | 编译自定义原生引擎模拟器,详情请参考 [定制原生模拟器引擎](../../advanced-topics/engine-customization.md#25-%E5%AE%9A%E5%88%B6%E5%8E%9F%E7%94%9F%E5%BC%95%E6%93%8E%E6%A8%A1%E6%8B%9F%E5%99%A8) | +| 消息调试工具 | 打开消息调试工具,用于调试编辑器内部运行时的 IPC 交互| +| Tester | 编辑器内置扩展的测试工具,目前暂未开放全部功能 | +| 开关图形工具 | 开/关图形工具面板,用于调试场景渲染| +| 消息列表 | 打开 [消息列表](../messages.md#查看消息列表) 面板,用于显示编辑器每个功能定义的公开消息及其说明| +| Export.d.ts |导出编辑器相关的 API | +| 重新加载(Ctrl/Cmd + R) | 重新加载编辑器界面 | +| UI 组件 | 打开 UI 组件面板,该面板列举了编辑器内提供的预设 UI 组件的使用方式,详情请参考 [UI 组件](../extension/ui.md) | +| 缓存 | 该项中的 **清除代码缓存** 用于清除脚本编译时产生的缓存 | +| VS Code Workflow | VS Code 代码编辑器的工作环境相关功能,目前支持 **添加 Chrome debug 配置** 和 **添加编译任务**,详情请参考 [配置代码编辑环境](../../scripting/coding-setup.md)。 | +| 开关开发人员工具(Ctrl/Cmd + Shift + I) | 开/关开发者工具窗口,用于编辑器界面扩展的开发。同时也可以自定义输出到 **控制台** 的日志,详情请参考 [自定义输出消息](../console/index.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E8%BE%93%E5%87%BA%E4%BF%A1%E6%81%AF)| +| 打开资源调试工具 | 打开资源调试面板,用于查看修改 asset-db 进程期间的日志消息| +| 打开场景调试工具 | 打开场景调试面板,用于查看修改场景时的日志消息| +| 打开构建调试工具 | 打开构建调试工具,用于查看在构建过程中产生的全部日志信息包括调用栈| + +## 帮助 + +![help](./img/help.png) + +| 选项 | 说明 | +| :--- | :-- | +| 使用手册 | 在默认浏览器打开 [用户手册文档](../../index.md) | +| API 文档 | 在默认浏览器打开 [API 参考文档](%__APIDOC__%/zh) | +| 论坛 | 在默认浏览器打开 [Cocos Creator 论坛](https://forum.cocos.org/c/58)| +| 软件许可| 可以查看 Cocos Creator 的软件许可信息 | +| 更新日志 | 在默认浏览器打开 Cocos Creator 各版本的 [更新日志](https://www.cocos.com/creator) | +| 引擎仓库 | 在默认浏览器打开 [TypeScript 引擎仓库](https://github.com/cocos/cocos4/) | +| 关于 Cocos Creator | 显示 Cocos Creator 相关版本号以及版权信息 | \ No newline at end of file diff --git a/versions/4.0/zh/editor/npm.md b/versions/4.0/zh/editor/npm.md new file mode 100644 index 0000000000..38978d8fd0 --- /dev/null +++ b/versions/4.0/zh/editor/npm.md @@ -0,0 +1,85 @@ +# 使用第三方工具 + +在开发一个扩展的过程中,不可避免的会需要使用到许多第三方的工具或者库,这里介绍几种实用方式。 + +## 使用 NPM 上的库 + +[获取 NPM 包](../scripting/modules/config.md) + +### 安装运行时依赖 + +一个完整的 Creator 扩展其实就是一个 `NPM` 模块,我们可以在命令行里进入到扩展的根目录,执行: + +```bash +npm install fs-extra +``` + +这时候,`NPM` 会自动在 `package.json` 文件里增加上 `dependencies` 字段: + +```json5 +{ + "name": "test-extension", + "dependencies": { + "fs-extra": "^10.0.0" + } +} +``` + +我们也可以手动的增加这个字段,然后执行: + +```bash +npm install +``` + +`NPM` 则会将标记的依赖安装到当前扩展里的 `node_modules` 目录。 + +当我们将需要使用的库文件安装好后,在代码里就可以使用了: + +```ts +import { outputFile } from 'fs-extra'; +``` + +如果出现 TS 定义问题,可以尝试取安装库文件的定义: + +```bash +npm install @types/fs-extra +``` + +### 安装开发依赖 + +如果有一些依赖库是开发时候才需要使用的,比如编译工具等。我们也可以将依赖的库标记为开发依赖: + +```bash +npm install fs-extra --save-dev +``` + +这样 `package.json` 里标记的依赖会是这样的: + +```json5 +{ + "name": "test-extension", + "devDependencies": { + "fs-extra": "^10.0.0" + } +} +``` + +**我们在准备发布这个扩展的时候,可以先清空 node_modules,然后执行命令:** + +```bash +npm install --production +``` + +**这样就只会安装 `dependencies` 里的依赖库,从而减小发布扩展的包体。** + +## 使用可执行程序 + +在 Creator 提供的 Node.js 环境里可以使用子进程的方式调用第三方的可执行程序: + +```typescript +import { join } from 'path'; +import { spawn } from 'child_process'; + +const child = spawn(join(__dirname, '../ps.exe'), {}); +child.on('error', function () {}); +``` diff --git a/versions/4.0/zh/editor/preferences/index.md b/versions/4.0/zh/editor/preferences/index.md new file mode 100644 index 0000000000..3060716b48 --- /dev/null +++ b/versions/4.0/zh/editor/preferences/index.md @@ -0,0 +1,232 @@ +# 偏好设置 + +**偏好设置** 面板中提供了编辑器的个性化设置,点击编辑器主菜单栏中的 **Cocos Creator/File -> 偏好设置** 即可打开。 + +**偏好设置** 由几个不同的分页组成,包括 **通用设置**、**外部程序**、**设备管理器**、**引擎管理器**、**资源数据库**、**控制台**、**属性检查器**、**预览**、**构建发布** 和 **实验室**。修改设置之后 **偏好设置** 面板会自动保存修改。 + +## 通用设置 + +![general](index/general.png) + +**通用设置** 分页主要是针对编辑器相关的一些基础信息进行配置,包括: + +- **编辑器语言**:可以选择中文或英文,修改语言设置后编辑器会自动切换语言,若有点地方没有切换,可刷新一下编辑器。 + +- **选择本机预览 IP 地址**:用户可以在本机有多个 IP 地址的情况下,手动选择其中之一作为预览时的默认地址和二维码地址。这里会列出所有本机的 IP,编辑器默认自动挑选一个 IP。 + +- **预览服务器端口号**:修改编辑器预览游戏时使用的端口号,修改完成后需要重启编辑器才能生效。 + +- **数值默认步长**:用于设置 **属性检查器** 中通过步进按钮调整数值属性时的步长幅度。默认步长为 0.001。**属性检查器** 中的步进按钮包括以下两种: + + - 当鼠标移动到数值属性输入框的右边时,会出现一组上下箭头,可以按照一定的步进幅度连续增加或减小数值。 + + ![step button](index/step.png) + + - 当鼠标悬浮在数值属性的名称附近时,光标会变成 ![mouse cursor](index/mouse-cursor.jpg) 这样的形状,然后左右拖动鼠标,也可以按照一定的步进幅度连续增加或减小数值。 + +## 外部程序 + +**外部程序** 分页用于设置构建发布到原生平台时所需的开发环境,以及配置一些第三方程序。当鼠标移动到具体的配置项时,左侧会显示一个灰色的圆形问号图标,点击该图标可设置将配置项应用于当前项目中或者应用于全局所有项目中。当设置为应用于当前项目时,灰色的图标会变成黄色。 + +![external-program](./index/external-program.png) + +- **WeChat 开发者工具**:配置微信小游戏开发者工具,详情请参考 [发布到微信小游戏](../publish/publish-wechatgame.md)。 + +- **Android NDK**:设置 Android NDK 路径,详情请参考 [安装配置原生开发环境](../publish/setup-native-development.md)。 + +- **Android SDK**:设置 Android SDK 路径,详情请参考 [安装配置原生开发环境](../publish/setup-native-development.md)。 + +- **HarmonyOS NDK**:设置 HarmonyOS NDK 路径,详情请参考 [发布 Huawei HarmonyOS 应用](../publish/publish-huawei-ohos.md)。 + +- **HarmonyOS SDK**:设置 HarmonyOS SDK 路径,详情请参考 [发布 Huawei HarmonyOS 应用](../publish/publish-huawei-ohos.md)。 + +- **默认脚本编辑器**:可以选用任意外部文本编辑工具(例如 [VS Code](../../scripting/coding-setup.md))的可执行文件,作为在 **资源管理器** 中双击脚本文件时的打开方式。可以点击输入框后面的 **搜索图标** 按钮选择偏好的文本编辑器的可执行文件。文件夹图标则用于打开已设置完成的文本编辑器的所在路径。详情可参考 [配置代码编辑环境](../../scripting/coding-setup.md)。 + +- **默认浏览器**:用于选择编辑器预览时使用的浏览器,可点击输入框后面的 **搜索图标** 按钮指定一个浏览器的路径。 + +## 设备管理器 + +**设备管理器** 分页用于管理使用模拟器或者浏览器预览时的设备分辨率,支持在面板右侧手动添加/修改/删除自定义的设备分辨率。编辑器默认的设备分辨率不支持修改/删除。 + +![device-manager](./index/device-manager.png) + +## 引擎管理器 + +**引擎管理器** 分页用于自定义引擎时配置引擎路径。 + +![engine-manager](./index/engine-manager.png) + +- **使用内置 TypeScript 引擎**:是否使用 Cocos Creator 安装路径下自带的 engine 路径作为 TypeScript 引擎路径。这个引擎用于编辑器里场景的渲染、内置组件的声明(也就是使用代码编辑器,如 VSCode 时的智能提示),以及场景在 Web 环境下的预览。 + +- **自定义 TypeScript 引擎路径**:除了使用自带的 engine,也可以前往 **engine 仓库**([GitHub](https://github.com/cocos/cocos4/) | [Gitee](https://gitee.com/mirrors_cocos-creator/engine/))克隆或 fork 一份引擎到本地的任意位置进行定制,然后取消勾选 **使用内置 TypeScript 引擎**,并将 **自定义 TypeScript 引擎路径** 指定为定制好的引擎路径,就可以在编辑器中使用这份定制后的引擎了。 + +- **使用内置原生引擎**:是否使用 Cocos Creator 安装路径下自带的 `engine/naive` 路径作为原生引擎路径。这个引擎用于构建发布时所有原生平台(iOS、 Android、Mac、Windows)的工程构建和编译。 + +- **自定义原生引擎路径**:取消上一项 **使用内置原生引擎** 的选择后,就可以手动指定原生引擎路径了。注意这里使用的原生引擎必须从 **engine-native**([GitHub](https://github.com/cocos/cocos4/) | [Gitee](https://gitee.com/mirrors_cocos-creator/engine))或该仓库的 fork 下载。 + +关于自定义引擎,具体内容可参考 [引擎定制工作流程](../../advanced-topics/engine-customization.md) + +## 资源数据库 + +**资源数据库** 分页用于设置 [资源管理器](../assets/index.md) 面板中的资源数据库的相关信息。 + +![asset-db](./index/asset-db.png) + +- **日志等级**:用于设置 **资源管理器** 中的资源数据库输出到 **控制台** 的信息类型。目前包括 **仅输出错误**、**仅输出错误和警告**、**输出错误、警告以及日志** 和 **输出所有信息** 四种。 +- **忽略文件(Glob)**:使用 Glob 表达式匹配需要被忽略的文件。此处 Glob 表达式中使用 ! 前缀的作用是保留匹配的资源搜索结果,之后编辑器将不再导入这些资源。修改后需要重启编辑器生效。 + - 例如 `!**/*.txt`,表示忽略所有的 `.txt` 文件。 + - 例如 `!test`,表示忽略相对路径 assets/test 的文件夹中的所有文件。 + - 例如 `!**/Node.*`,表示忽略所有文件名为 Node 的文件。 +- **自动刷新资源**:从外部返回编辑器界面时自动刷新资源。详情参考下文介绍。 +- **导入资源时自动覆盖 Meta**:在导入资源替换原有资源时,若导入资源自带 Meta,可通过该项设置是否覆盖掉原有资源的 Meta。 +- **默认资源导入类型配置**:用于设置项目内资源导入时的默认配置。详情参考下文介绍。 + +### 自动刷新资源 + +若开启该项,无论开发者在 Creator 外部是否有对资源进行操作,再回到 Creator 时都会自动对所有资源进行检查。那么当项目中的文件数量过多,或者硬盘随机读写速度偏低时,就容易导致资源系统响应延迟。例如在 **资源管理器** 中选中资源,**属性检查器** 会延迟一会儿才显示资源相关属性。 + +这时候便可以关闭 **自动刷新资源** 功能,关闭该功能后,如果有对资源进行操作,那么手动点击 **资源管理器** 面板右上方的 **刷新** 按钮即可刷新资源。 + +> **注意**:如果没有遇到资源系统响应问题,不建议关闭 **自动刷新资源** 选项。 + +### 默认资源导入类型配置 + +该项用于设置项目内资源导入时的默认配置。例如希望导入的图片默认为 sprite-frame 类型,那么便可以点击该项右侧的 **编辑** 按钮,然后在打开的 json 文件中填入以下内容: + +```json5 +{ + // image 表示资源的类型为图片 + "image": { + "type": "sprite-frame" + } +} +``` + +编辑完成并保存后,返回编辑器,点击 **应用** 按钮即可生效。 + +资源类型为 key,value 需要是一个 object,这个 object 就是最终导入资源时使用的默认配置。
+例如上方示例代码中的 key 为 `image`,value 是 `image` 中配置的内容,点击 **应用** 后便会刷新资源数据库的配置信息,将 `image` 中的内容一一对应配置到资源 meta 文件的 `userData` 字段中。例如 `image.type` 设置为 `sprite-frame`,导入图片资源时,默认的 `userData.type` 就会被设置为 `sprite-frame`。便可以根据项目情况,动态地设置各种资源的默认导入配置。 + +如果想要知道资源类型,可以在 **资源管理器** 中右键点击资源,选择 **在资源管理器中显示**,然后在打开的文件夹中找到资源对应的 meta 文件并打开,`importer` 字段标记的便是资源的类型。 + +例如材质资源的 meta 文件如下,`importer` 字段中的 `material` 便是资源类型。 + +```json +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "482a5162-dad9-446c-b548-8486c7598ee1", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} +``` + +## 控制台 + +**控制台** 分页用于设置 [控制台](../console/index.md) 面板输出的日志,包括 **显示日期** 和 **文字大小**。 + +![console](./index/console.png) + +- **显示日期**:是否在 **控制台** 面板输出的日志前面显示日期。 +- **文字大小**:用于设置 **控制台** 面板输出的日志文字大小。 + +## 属性检查器 + +**属性检查器** 分页中的 **离开编辑自动保存** 功能用于设置 [属性检查器](../inspector/index.md) 面板是否在属性编辑完成后自动保存修改。 + +![inspector](./index/inspector.png) + +## 预览 + +**预览** 分页主要用于在使用编辑器正上方的 [预览](../preview/index.md) 按钮时,可以设置的各种选项,但只对当前项目有效。 + +![preview](./index/preview.png) + +- **保存场景自动刷新预览**:若勾选该项,则在编辑器中保存场景时,会自动刷新已经打开的预览页面,目前暂时不支持使用模拟器预览。 +- **模拟器自动清除缓存**:若勾选该项,则使用模拟器预览时会自动清除缓存。 +- **模拟器是否开启调试面板**:若勾选该项,则使用模拟器预览项目时将自动打开调试窗口。 +- **模拟器是否等待调试面板开启**:该项会在勾选了 **模拟器是否开启调试面板** 后生效,作用是暂停模拟器启动过程直至调试器连接完成,用于调试加载过程。 +- **预览服务器端口号**:该配置存储于项目内用于区分不同项目间的服务器端口号 +- **开启 HTTPS**:是否开启 HTTPS +- **HTTPS 端口**: HTTPS 服务的端口号 +- **私钥 key**:HTTPS 证书的私钥路径 +- **公钥 cert**:HTTPS 证书的公钥路径 +- **签发文件 csr**: CSR 证书文件的路径 + +## 动画编辑器 + +**动画编辑器** 分页用于设置 [动画系统](../../animation/index.md) 在编辑时的一些参数和配置。 + +![animation](./index/animation.png) + +- **开启动画即时缓存功能**:开启后,编辑中的动画会根据 **缓存间隔时间** 来进行自动缓存 +- **缓存间隔时间**:缓存间隔时间,单位:毫秒 +- **缓存文件数量最大值**:最大可以缓存的动画数量 + +> **注意**:目前缓存的动画文件并不会自动恢复,如果要恢复缓存的文件,请参考 [动画系统](../../animation/index.md)。 + +## 构建发布 + +**构建发布** 分页用于设置执行 [构建发布](../publish/build-panel.md) 时相关的配置和信息。 + +![build](./index/build.png) + +- **日志文件打开方式**:该项用于设置点击 [平台构建任务](../publish/build-panel.md#%E5%B9%B3%E5%8F%B0%E6%9E%84%E5%BB%BA%E4%BB%BB%E5%8A%A1) 左下方的打开日志按钮时,会直接打开构建日志文件还是打开日志文件所在的目录。默认为直接打开日志文件。 + +- **缓存资源的序列化 JSON**:为了加快构建速度,减少重复反序列化未修改资源,在资源构建过程中将会缓存资源的序列化 JSON,这部分 JSON 会放置在项目的 `temp/asset-db/assets/uuid/build` 目录下,根据 **debug** 和 **release** 模式分为 `debug.json` 和 `release.json` 存放。 + + ![build](./index/json.png) + + 当存在缓存资源时构建将会直接取用,这部分缓存资源会在每次资源导入后重新更新,基本不需要关心。该选项默认勾选,但如果遇到了某些特殊需求,希望构建时不存储这部分序列化构建缓存资源,取消勾选该项即可。 + +- **缓存编译后的引擎**:缓存编译后的引擎,该缓存将在全局目录内生效。开发者可以通过 **开发者 -> 开关开发人员工具** 中使用 `Editor.App.temp` 命令打印该缓存地址: + + ![build-temp](./index/build-temp.png) + + 通过该功能,可以加快构建速度。该功能也可以在日志中进行查看: + + ![cached-log](./index/build-log-cached-engine.png) + +- **缓存纹理压缩资源**:缓存压缩后的纹理资源,如对纹理参数进行修改,则缓存会失效,缓存目录为:`temp/builder/CompressTexture` +- **缓存自动图集资源**:自动化的图集的缓存可以在构建时清理,详情请参考 [构建任务页面](../publish/build-panel.md#%E6%9E%84%E5%BB%BA%E4%BB%BB%E5%8A%A1%E9%A1%B5%E9%9D%A2) +- **保存组件节点序列化的 UUID 数据**:默认的情况下构建,组件的 UUID 都不会序列化进预制体/资源内,而是由引擎在运行时自动生成的,在某些情况下如果需要通过编辑时的组件 UUID 去查找组件,则可以开启此选项。开启后,组件的 UUID 在构建时会序列化到对应预制体或资源内,并在运行时和编辑器时保持一致。 + +## 实验室 + +**实验室** 分页中会不定期提供一些新的技术方案或实验性质的功能,可以通过开关选项来选择是否使用,大部分情况下默认开启。 + +![laboratory](./index/laboratory.png) + +- **启用延迟渲染管线**:启用或者禁用延迟渲染管线。在默认情况下,延迟渲染管线处于关闭状态。详情可参考 [延迟渲染管线](../../render-pipeline/builtin-pipeline.md#%E5%BB%B6%E8%BF%9F%E6%B8%B2%E6%9F%93%E7%AE%A1%E7%BA%BF)。 + +- **移动脚本自动修改导入路径**:启用后,在编辑器中移动脚本时,脚本的 import 部分会自动修改。 + +- **优化调度策略**:该策略会尝试在多次重复导入资源时进行合并,以减少调度次数。 + +- **场景即时缓存**:该项默认开启,主要用于在场景编辑过程中每隔一段时间(目前时间间隔为 5s)便将场景文件缓存到项目目录下的 `temp/scene/[SCENE_UUID]/[TIME].json` 文件中。如遇突发情况比如场景崩溃、进程卡死等,再次打开编辑器时将会弹窗提示是否应用缓存内最近一次的场景文件。 + + > **注意**:在日常使用中,只要场景正常打开了,那么当前场景在打开之前缓存的所有场景文件都会被清空。如有特殊需求需要查看指定场景的缓存文件,请先在编辑器中关闭对应场景。 + +- **保持场景主循环运行**:是否允许场景的渲染方式和预览时一样不断的进行渲染循环 +- **启用原生引擎加载场景编辑器**:启用后编辑器内的渲染模块将使用原生引擎。 +- **调试原生引擎场景**:是否可以开启原生引擎场景,这个选项开启后,会阻止原生引擎启动,弹出一个对话框,点击确认后才会继续启动。主要是用于调试原生化后的场景内的逻辑的。 + +- **动画嵌入播放器**:此功能支持用户在编辑动画时,同步配合播放其他粒子和动画 +- **动画辅助曲线**:是否启用动画辅助曲线功能,详情请查看 [辅助曲线编辑视图](../../animation/animation-auxiliary-curve.md) + +- **开启姿态图功能**:是否启用动画姿态图功能,详情请查看 [程序式动画](../../animation/marionette/procedural-animation/index.md) +- **开启烘焙功能**:用于开启烘焙功能,详情可参考 [光照贴图](./../../concepts/scene/light/lightmap.md) + +### 注意事项 + +在未来的版本中,**实验室** 中的这些功能可能被合并,但也有可能出现破坏兼容性的修改,甚至可能会被取消。如果想在开发环境中使用这些功能,请务必严格测试,并保持关注新版本的更新公告。 + +我们欢迎用户开启试用这些功能,并且在 [论坛](https://forum.cocos.org/c/58) 提供宝贵的反馈意见,让这些功能更适合自己的使用场景,为项目提供更强大的助力。 + +## 自定义偏好设置面板 + +Creator 支持在 **偏好设置** 右侧添加自定义功能页,详情请参考 [自定义偏好设置面板](../../editor/extension/contributions-preferences.md)。 diff --git a/versions/4.0/zh/editor/preferences/index/add-component.png b/versions/4.0/zh/editor/preferences/index/add-component.png new file mode 100644 index 0000000000..e4873386ac Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/add-component.png differ diff --git a/versions/4.0/zh/editor/preferences/index/animation.png b/versions/4.0/zh/editor/preferences/index/animation.png new file mode 100644 index 0000000000..06d918598d Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/animation.png differ diff --git a/versions/4.0/zh/editor/preferences/index/asset-db.png b/versions/4.0/zh/editor/preferences/index/asset-db.png new file mode 100644 index 0000000000..05c82ae66f Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/asset-db.png differ diff --git a/versions/4.0/zh/editor/preferences/index/build-log-cached-engine.png b/versions/4.0/zh/editor/preferences/index/build-log-cached-engine.png new file mode 100644 index 0000000000..79664a0d29 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/build-log-cached-engine.png differ diff --git a/versions/4.0/zh/editor/preferences/index/build-temp.png b/versions/4.0/zh/editor/preferences/index/build-temp.png new file mode 100644 index 0000000000..7de1fe5306 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/build-temp.png differ diff --git a/versions/4.0/zh/editor/preferences/index/build.png b/versions/4.0/zh/editor/preferences/index/build.png new file mode 100644 index 0000000000..89fbe3d5e9 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/build.png differ diff --git a/versions/4.0/zh/editor/preferences/index/console.png b/versions/4.0/zh/editor/preferences/index/console.png new file mode 100644 index 0000000000..a9c921f224 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/console.png differ diff --git a/versions/4.0/zh/editor/preferences/index/device-manager.png b/versions/4.0/zh/editor/preferences/index/device-manager.png new file mode 100644 index 0000000000..66a0e9ebba Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/device-manager.png differ diff --git a/versions/4.0/zh/editor/preferences/index/engine-manager.png b/versions/4.0/zh/editor/preferences/index/engine-manager.png new file mode 100644 index 0000000000..92bf8c1376 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/engine-manager.png differ diff --git a/versions/4.0/zh/editor/preferences/index/external-program.png b/versions/4.0/zh/editor/preferences/index/external-program.png new file mode 100644 index 0000000000..4d19ca130d Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/external-program.png differ diff --git a/versions/4.0/zh/editor/preferences/index/general.png b/versions/4.0/zh/editor/preferences/index/general.png new file mode 100644 index 0000000000..a7d901c7b4 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/general.png differ diff --git a/versions/4.0/zh/editor/preferences/index/inspector.png b/versions/4.0/zh/editor/preferences/index/inspector.png new file mode 100644 index 0000000000..eb093a66e5 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/inspector.png differ diff --git a/versions/4.0/zh/editor/preferences/index/json.png b/versions/4.0/zh/editor/preferences/index/json.png new file mode 100644 index 0000000000..cbd3490d40 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/json.png differ diff --git a/versions/4.0/zh/editor/preferences/index/laboratory.png b/versions/4.0/zh/editor/preferences/index/laboratory.png new file mode 100644 index 0000000000..e4fd48bc9b Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/laboratory.png differ diff --git a/versions/4.0/zh/editor/preferences/index/mouse-cursor.jpg b/versions/4.0/zh/editor/preferences/index/mouse-cursor.jpg new file mode 100644 index 0000000000..df66301fc4 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/mouse-cursor.jpg differ diff --git a/versions/4.0/zh/editor/preferences/index/preview.png b/versions/4.0/zh/editor/preferences/index/preview.png new file mode 100644 index 0000000000..1ffbc09f63 Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/preview.png differ diff --git a/versions/4.0/zh/editor/preferences/index/step.png b/versions/4.0/zh/editor/preferences/index/step.png new file mode 100644 index 0000000000..71626d43fe Binary files /dev/null and b/versions/4.0/zh/editor/preferences/index/step.png differ diff --git a/versions/4.0/zh/editor/preview/browser.md b/versions/4.0/zh/editor/preview/browser.md new file mode 100644 index 0000000000..e62ec6e945 --- /dev/null +++ b/versions/4.0/zh/editor/preview/browser.md @@ -0,0 +1,48 @@ +# 网页预览定制工作流程 + +## 自定义预览模板 + +预览支持自定义模板方便用户自定义需要的预览效果,自定义的预览模板可以放置在项目目录的 `preview-template` 文件夹中。或者点击编辑器主菜单中的 **项目 -> 生成预览模板** 就可以在项目目录下创建一个最新的预览模板。编辑器中的预览也是使用模板来注入最新的项目数据,预览时将会查找该目录下的 index 文件,如果存在就是要该文件作为预览的模板。 + +`preview-template` 文件夹的结构类似: + +```js +project-folder + |--assets + |--build + |--preview-template + // 必须的入口文件 + |--index.ejs + // 其他文件可根据想要实现的预览效果进行添加 +``` + +开始自定义网页预览,需要注意的是,预览模板中存在一些预览菜单项以及预览调试工具等内容,所以在增删一些模板语法的内容时要稍加注意,如果随意修改可能会导致预览模板不可用。建议使用 ejs 注入的内容都保留,然后在此基础上添加需要的内容即可。另外,假如 `index.html` 与 `index.ejs` 共存时,**`index.html` 将会替代 `index.ejs`** 成为预览的页面内容。 + +## 使用示例 + +以下示例可以在 [GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/preview-template) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/preview-template) 查找到。 + +1. 点击编辑器主菜单中的 **项目 -> 生成预览模板**,**控制台** 便会输出“预览模板生成成功”的提示,并显示预览模板的生成路径。 + +2. 添加需要使用的脚本如 `test.js`,其中 `<%- include(cocosTemplate, {}) %>` 中包含的是默认的启动游戏逻辑,添加的脚本可以根据需要在游戏逻辑启动前/后来决定存放的位置。下面的 `test.js` 是在游戏启动后加载。 + + - 打开 `index.ejs` 修改如下: + + ```html + + ... + + ... + <%- include(cocosTemplate, {}) %> // 游戏启动处理逻辑 + // 新增脚本 + + + ``` + + - `test.js` 放置在页面内标识的相对路径(只能在 `preview-template` 文件夹中) + + ``` + |--preview-template + |--index.ejs + |--test.js + ``` diff --git a/versions/4.0/zh/editor/preview/browser/custom-devices.jpg b/versions/4.0/zh/editor/preview/browser/custom-devices.jpg new file mode 100644 index 0000000000..6755ec843c Binary files /dev/null and b/versions/4.0/zh/editor/preview/browser/custom-devices.jpg differ diff --git a/versions/4.0/zh/editor/preview/browser/user_device.jpg b/versions/4.0/zh/editor/preview/browser/user_device.jpg new file mode 100644 index 0000000000..87c979ef5a Binary files /dev/null and b/versions/4.0/zh/editor/preview/browser/user_device.jpg differ diff --git a/versions/4.0/zh/editor/preview/index.md b/versions/4.0/zh/editor/preview/index.md new file mode 100644 index 0000000000..8e653e986e --- /dev/null +++ b/versions/4.0/zh/editor/preview/index.md @@ -0,0 +1,142 @@ +# 项目预览调试 + +在使用主要编辑器面板进行资源导入、场景搭建、组件配置、属性调整之后,我们可以通过预览和构建来看到游戏在 Web 或原生平台运行的效果了。 + +## 在编辑器中选择预览平台 + +在游戏开发过程中我们可以随时点击编辑器窗口正上方的 **预览** 按钮,来看到游戏运行的实际情况。 + +![select-platform](index/select-platform.png) + +- 最左边的下拉菜单中可以选择预览的平台,目前支持使用 **编辑器内**、**浏览器** 和 **模拟器** 预览。 +- ![play](index/play.png):预览按钮,点击可运行预览 +- 在 **预览** 按钮右侧可以选择预览的场景,下拉框中会列出项目中所有的场景,默认使用 **当前场景**。 + + ![select-scene](index/select-scene.png) + +- ![play](index/refresh.png):刷新按钮,点击该按钮可刷新所有已打开的预览页面。如果想要在执行保存场景操作后自动刷新预览页面,在编辑器主菜单中选择 **Cocos Creator/File -> 偏好设置 -> 预览**,然后勾选 **保存场景自动刷新预览** 即可。 + +> **注意**:必须双击打开场景才能预览游戏内容,在没有打开任何场景,或者新建了一个空场景的情况下预览是看不到任何内容的。 + +### 浏览器 + +选择使用 **浏览器** 预览后,点击旁边的 **预览** 按钮,会在默认桌面浏览器中直接运行游戏的网页版本。推荐使用谷歌浏览器(Chrome)作为开发过程中预览调试用的浏览器,因为谷歌浏览器(Chrome)的开发者工具是最为全面强大的。 + +浏览器预览界面的最上边有一系列控制按钮可以对预览效果进行控制: + +- 最左边选择预览窗口的比例大小,来模拟在不同移动设备上的显示效果,可以在 **Cocos Creator/File -> 偏好设置 -> 设备管理器** 中手动添加设备分辨率。 +- **Rotate**:决定显示横屏还是竖屏 +- **Debug Mode**:可以选择脚本中哪些级别的日志会输出到浏览器控制台中 +- **Show FPS**:用于选择是否在左下角显示每秒帧数和 Drawcall 数量等调试信息 +- **FPS**:限制最高每秒帧数 +- **Pause**:暂停游戏 + +![browser](index/browser.png) + +Cocos Creator 3.0 支持自定义网页预览功能,详情请参考 [网页预览定制工作流程](browser.md)。 + +#### 浏览器兼容性 + +Cocos Creator 开发过程中测试的桌面浏览器包括:**Chrome**、**Firefox(火狐)** 和 **QQ 浏览器**,其他浏览器只要内核版本够高也可以正常使用,对部分浏览器来说请勿开启 IE 兼容模式。 + +移动设备上测试的浏览器包括:**Safari(iOS)**、**Chrome(Android)**、**QQ 浏览器(Android)** 和 **UC 浏览器(Android)**。 + +#### 使用浏览器开发者工具进行调试 + +以谷歌浏览器为例,点击编辑器主菜单中的 **开发者 -> 开发人员工具** 便可打开开发者工具界面。在开发者工具中,我们可以查看运行日志、打断点进行调试、在调用堆栈中查看每一步各个变量的值、甚至进行资源加载和性能分析。 + +要学习使用开发者工具进行调试,请阅读 [Chrome Dev Tools 使用指南](https://developers.google.com/web/tools/chrome-devtools?hl=zh-cn),或其他浏览器的开发者工具帮助。 + + + +### 编辑器预览 + +![editor preview](index/editor-preview-props.png) + +如要在编辑内预览,需在下拉菜单中选择 **编辑器预览**(下图标识 1),再点击 **运行预览**(下图标识 2)按钮进行预览: + +![bar](index/edit-bar.png) + +点击 ![pause](index/pause.png) 可以暂停当前预览,暂停后也可通过点击 ![step](index/step.png) 进行单步运行。 + +当不在需要继续预览时,可点击 ![stop](index/stop.png) 按钮停止。 + +选择编辑器预览时,也可以通过预览窗口上方的菜单配置预览的输出: + +- ![state](index/state.png) 点击选择是否在视图内显示下列的统计信息: + + ![info](index/state-info.png) + +- ![FPS](index/FPS.png) 预览时的最大帧率,也可以输入自定义帧率: + + ![max](index/max-fps.png) + +- ![resolution](index/resolution.png) 预览时的分辨率,通过下拉菜单可以选择不同的分辨率,如当前引擎内置的分辨率无法满足,也可以将下拉框拉到最下方选择 **管理**: + + ![edit](index/edit.png) + + 也可在顶部菜单栏的 **偏好设置** 的 [设备管理器](../preferences/index.md#%E8%AE%BE%E5%A4%87%E7%AE%A1%E7%90%86%E5%99%A8) 中添加/修改/删除分辨率: + + ![device manager](index/device-manager.png) + +- ![rotate](index/rotate-view.png) 切换横屏/竖屏预览 +- ![scale](index/scale-game-view.png) 缩放视图 +- ![fit](index/fit.png) 使游戏视图适配当前的视图 + +预览时也可以在 **属性检查器** 内修改组件的属性进行调试,但是需要注意的是,运行时输入的数据并不会被保存。如果希望保存调整后的组件,可以点击右侧的 **组件菜单** 并在下拉列表内选择 **复制组件**,然后在退出预览的情况下选择 **粘贴组件的值**。 + +![copy](index/copy-component.png) + +### 模拟器 + +选择使用 **模拟器** 预览后,会使用 Cocos Simulator(桌面模拟器)运行当前的游戏场景。使用模拟器运行游戏时,脚本中的日志信息(使用 `cc.log` 打印的内容)和报错信息会显示在 **控制台** 面板中。 + +![simulator](index/simulator.png) + +使用 **模拟器** 预览时,支持自动打开调试窗口进行调试,可在 **Cocos Creator/File -> 偏好设置 -> 预览** 中设置,详情请参考 [偏好设置 — 预览](../preferences/index.md)。 + +若需要在 Apple M1(Silicon)架构的设备中添加对 iOS 模拟器的支持,需要在 Creator 右上角点击 **编辑器** 按钮,打开编辑器目录 `resources\3d\engine\native\external` 目录下的 `CMakeLists.txt` 文件,取消 iOS 模拟器相关的代码注释,同时注释 iOS 真机相关代码,如下图所示: + +![simulator](index/ios-simulator-m1.png) + +## 手机端预览 + +调试手机端的预览效果有以下方式: + +- 使用 **浏览器开发者工具的手机端预览模式** + +- **扫描预览二维码** + + 将鼠标移动到编辑器工具栏左边的 IP 预览地址上,会显示一个预览的二维码,用手机扫描即可。 + + ![preview-url](index/preview-url.png) + +- 直接 **在手机浏览器里输入预览地址** + +> **注意**:手机需要和电脑在 **同一网段**。由于电脑的网络有可能有多个,如果编辑器预览 URL 的 IP 地址选择不正确,可以在主菜单栏的 **Cocos Creator/File -> 偏好设置 -> 通用设置 -> 选择本机预览 IP 地址** 中修改。 + +## 构建发布 + +预览和调试之后,如果您对您的游戏比较满意了,就可以通过主菜单的 **项目/构建发布** 打开 **构建发布** 面板,将游戏打包发布到目标平台上,包括 Web、iOS、Android、各类"小游戏"、PC 客户端等平台。详细的构建发布流程,请参考 [跨平台发布游戏](../publish/index.md) 一章的内容。 + +> **注意**:使用 **模拟器** 运行游戏的效果,和最终发布到原生平台可能会有一定差别,对于任何重要的游戏功能,都请以构建发布后的版本来做最终的测试。 + +## 其他参考 + +- [预览流程简介与常见错误处理](preview-guid.md) diff --git a/versions/4.0/zh/editor/preview/index/FPS.png b/versions/4.0/zh/editor/preview/index/FPS.png new file mode 100644 index 0000000000..e3d9429e13 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/FPS.png differ diff --git a/versions/4.0/zh/editor/preview/index/browser.png b/versions/4.0/zh/editor/preview/index/browser.png new file mode 100644 index 0000000000..5aa0d58d78 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/browser.png differ diff --git a/versions/4.0/zh/editor/preview/index/copy-component.png b/versions/4.0/zh/editor/preview/index/copy-component.png new file mode 100644 index 0000000000..745b270be6 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/copy-component.png differ diff --git a/versions/4.0/zh/editor/preview/index/custom.png b/versions/4.0/zh/editor/preview/index/custom.png new file mode 100644 index 0000000000..b761bbea4a Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/custom.png differ diff --git a/versions/4.0/zh/editor/preview/index/device-manager.png b/versions/4.0/zh/editor/preview/index/device-manager.png new file mode 100644 index 0000000000..50c86d166e Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/device-manager.png differ diff --git a/versions/4.0/zh/editor/preview/index/edit-bar.png b/versions/4.0/zh/editor/preview/index/edit-bar.png new file mode 100644 index 0000000000..7a9cc87757 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/edit-bar.png differ diff --git a/versions/4.0/zh/editor/preview/index/edit.png b/versions/4.0/zh/editor/preview/index/edit.png new file mode 100644 index 0000000000..1887a24d10 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/edit.png differ diff --git a/versions/4.0/zh/editor/preview/index/editor-preview-props.png b/versions/4.0/zh/editor/preview/index/editor-preview-props.png new file mode 100644 index 0000000000..97cc7fc077 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/editor-preview-props.png differ diff --git a/versions/4.0/zh/editor/preview/index/fit.png b/versions/4.0/zh/editor/preview/index/fit.png new file mode 100644 index 0000000000..d374e4d18d Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/fit.png differ diff --git a/versions/4.0/zh/editor/preview/index/gameview.png b/versions/4.0/zh/editor/preview/index/gameview.png new file mode 100644 index 0000000000..33c1603959 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/gameview.png differ diff --git a/versions/4.0/zh/editor/preview/index/ios-simulator-m1.png b/versions/4.0/zh/editor/preview/index/ios-simulator-m1.png new file mode 100644 index 0000000000..a249cccc66 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/ios-simulator-m1.png differ diff --git a/versions/4.0/zh/editor/preview/index/max-fps.png b/versions/4.0/zh/editor/preview/index/max-fps.png new file mode 100644 index 0000000000..828a6be9b0 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/max-fps.png differ diff --git a/versions/4.0/zh/editor/preview/index/pause.png b/versions/4.0/zh/editor/preview/index/pause.png new file mode 100644 index 0000000000..3da2bf5106 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/pause.png differ diff --git a/versions/4.0/zh/editor/preview/index/play.png b/versions/4.0/zh/editor/preview/index/play.png new file mode 100644 index 0000000000..884367029b Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/play.png differ diff --git a/versions/4.0/zh/editor/preview/index/preferences.png b/versions/4.0/zh/editor/preview/index/preferences.png new file mode 100644 index 0000000000..ce93c7606b Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/preferences.png differ diff --git a/versions/4.0/zh/editor/preview/index/preview-type-drop-down.png b/versions/4.0/zh/editor/preview/index/preview-type-drop-down.png new file mode 100644 index 0000000000..2974d3682b Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/preview-type-drop-down.png differ diff --git a/versions/4.0/zh/editor/preview/index/preview-url.png b/versions/4.0/zh/editor/preview/index/preview-url.png new file mode 100644 index 0000000000..8157c9de21 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/preview-url.png differ diff --git a/versions/4.0/zh/editor/preview/index/refresh.png b/versions/4.0/zh/editor/preview/index/refresh.png new file mode 100644 index 0000000000..159322e6c6 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/refresh.png differ diff --git a/versions/4.0/zh/editor/preview/index/resolution.png b/versions/4.0/zh/editor/preview/index/resolution.png new file mode 100644 index 0000000000..d4b6c5cc49 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/resolution.png differ diff --git a/versions/4.0/zh/editor/preview/index/rotate-view.png b/versions/4.0/zh/editor/preview/index/rotate-view.png new file mode 100644 index 0000000000..ae3e19045f Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/rotate-view.png differ diff --git a/versions/4.0/zh/editor/preview/index/scale-game-view.png b/versions/4.0/zh/editor/preview/index/scale-game-view.png new file mode 100644 index 0000000000..e98e8562c4 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/scale-game-view.png differ diff --git a/versions/4.0/zh/editor/preview/index/select-platform.png b/versions/4.0/zh/editor/preview/index/select-platform.png new file mode 100644 index 0000000000..e3beced184 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/select-platform.png differ diff --git a/versions/4.0/zh/editor/preview/index/select-preview-type.png b/versions/4.0/zh/editor/preview/index/select-preview-type.png new file mode 100644 index 0000000000..c1568919a6 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/select-preview-type.png differ diff --git a/versions/4.0/zh/editor/preview/index/select-scene.png b/versions/4.0/zh/editor/preview/index/select-scene.png new file mode 100644 index 0000000000..06255ae8e2 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/select-scene.png differ diff --git a/versions/4.0/zh/editor/preview/index/simulator.png b/versions/4.0/zh/editor/preview/index/simulator.png new file mode 100644 index 0000000000..0a8ff618a9 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/simulator.png differ diff --git a/versions/4.0/zh/editor/preview/index/state-info.png b/versions/4.0/zh/editor/preview/index/state-info.png new file mode 100644 index 0000000000..539b3b85dd Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/state-info.png differ diff --git a/versions/4.0/zh/editor/preview/index/state.png b/versions/4.0/zh/editor/preview/index/state.png new file mode 100644 index 0000000000..affad83f9b Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/state.png differ diff --git a/versions/4.0/zh/editor/preview/index/step.png b/versions/4.0/zh/editor/preview/index/step.png new file mode 100644 index 0000000000..be0ac0f620 Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/step.png differ diff --git a/versions/4.0/zh/editor/preview/index/stop.png b/versions/4.0/zh/editor/preview/index/stop.png new file mode 100644 index 0000000000..bf3441191e Binary files /dev/null and b/versions/4.0/zh/editor/preview/index/stop.png differ diff --git a/versions/4.0/zh/editor/preview/preview-guid.md b/versions/4.0/zh/editor/preview/preview-guid.md new file mode 100644 index 0000000000..7398d9db34 --- /dev/null +++ b/versions/4.0/zh/editor/preview/preview-guid.md @@ -0,0 +1,23 @@ +# 预览流程简介与常见错误处理 + +本章节主要介绍浏览器预览的流程。 + +## 流程简介 + +浏览器预览本质上是编辑器开启了一个 express 服务器,在点击预览时打开用户的默认浏览器去访问预览 URL。预览模板里写了一些加载引擎和初始化场景的简单逻辑,而关于用户资源加载主要还是依赖于 `settings.json` 的生成,因为 `settings.json` 里面记载了当前项目的资源、脚本以及项目设置信息。预览的 `settings.json` 是调用构建扩展的接口生成的,因此如果 `settings.json` 没有正常生成,便可以打开 **开发者 -> 构建调试工具** 查看。 + +## 常见错误处理 + +在遇到预览无法正常显示时,请先打开开发者工具的 NetWork 查看此时是否有资源或者脚本加载失败。 + +### `settings.json` 加载失败 + +在寻找这个问题的原因之前,请先确保在预览前编辑器内没有报错信息。 + +`settings.json` 加载失败说明 `settings.json` 没能正常生成,这时候可以打开 **开发者 —> 构建调试工具** 查看是否有报错信息。正常情况下,如果生成失败基本会有对应的报错提示。比较常见的是脚本错误,因为在生成 `settings` 时会先加载一遍项目脚本,如果项目脚本内存在不合法的书写,那么在加载过程中便会抛出异常导致 `settings.json` 生成失败。具体报错的脚本信息可以参考报错信息内的提示,通常在此处的报错内容,有效信息都是资源的 UUID,可以复制对应的 UUID 到 **资源管理器** 中搜索定位该脚本。 + +关于 `settings.json` 具体的生成流程,可以参考 [构建流程简介](../publish/build-guide.md)。预览的 `settings.json` 生成规则基本和只勾选了调试模式的生成规则一样,区别在于预览只梳理资源信息,并不会进行资源的打包生成。 + +### 资源加载 404 + +请将报 404 错误的资源的 UUID 到编辑器的 **资源管理器** 中搜索对应资源。 diff --git a/versions/4.0/zh/editor/project/index.md b/versions/4.0/zh/editor/project/index.md new file mode 100644 index 0000000000..7f5a57e995 --- /dev/null +++ b/versions/4.0/zh/editor/project/index.md @@ -0,0 +1,405 @@ +# 项目设置 + +**项目设置** 面板通过点击编辑器主菜单栏中的 **项目 -> 项目设置** 即可打开,主要用于设置特定项目的相关配置项。这些设置会保存在项目的 `settings/packages` 文件夹中。如果需要在不同开发者之间同步项目设置,请将 `settings` 目录加入到版本控制。 + +**项目设置** 由几个不同的分页组成,包括 **项目数据**、**Layers**、**物理**、**脚本**、**Macro Config**、**功能裁剪** 和 **纹理压缩**。修改设置之后 **项目设置** 面板会自动保存修改。 + +## 项目数据 + +**项目数据** 分页主要用于设置默认 Canvas、渲染管线等,只对当前项目生效。 + +![project-data](./index/project-data.png) + +### 默认 Canvas 设置 + +默认 Canvas 设置包括 **设计分辨率** 和 **适配屏幕宽度/高度**,用于规定在新建场景或 Canvas 组件时,Canvas 中默认的设计分辨率数值,以及 `Fit Height` 和 `Fit Width`。详情请参考 [多分辨率适配方案](../../ui-system/components/engine/multi-resolution.md)。 + +### 高画质模式 + +在使用 Cocos Dashboard 通过默认的模板 3D HQ 创建工程时,默认开启阴影与光照等相关设置以达到更高质量的渲染效果,省去了手动开启场景、物体、灯光等多重阴影和光照的设置过程。 +这个高画质模式就是模板功能的切换开关。 + +### 渲染管线 + +渲染管线用于控制场景的渲染流程,目前内置的渲染管线包括 **builtin-forward** (前向渲染管线) 和 **builtin-deferred** (延迟渲染管线),详情请参考 [内置渲染管线](../../render-pipeline/builtin-pipeline.md)。 + +## Layers + +![Layers](./index/layers.png) + +- Layers 能让相机渲染部分场景,让灯光照亮部分场景。 +- 可自定义 0 到 19 个 Layers,清空输入框则删除原先的设置。 +- 后 12 个 Layers 是引擎内置的,不可修改。 +- 目前使用到 Layer 的位置包括: + + 1. Node 节点在 **属性检查器** 中的 [Layer 属性](../../concepts/scene/node-component.md#%E8%AE%BE%E7%BD%AE%E8%8A%82%E7%82%B9%E7%9A%84%E5%8F%AF%E8%A7%81%E6%80%A7)。 + + ![Layers-node](./index/layers-node.png) + + 2. Camera 节点在 **属性检查器** 中的 Visibility 属性,节点的 Layer 属性匹配相机的 Visibility 属性。只有当节点设置的 Layer 属性包含在相机的 Visibility 中时,节点才可以被相机看见。更多说明可以参考 [Camera 组件](./../components/camera-component.md)。 + + ![Layers-camera](./index/layers-camera.png) + + + +## 排序图层 + +![sorting-layer](index/sorting-layer.png) + +自定义 2D UI 以及 3D 物体的渲染排序。 + +* 操作说明 + - 点击右侧的删除按钮可以删掉对应的图层。 + - 除 `default` 图层不可编辑外,其他图层都可以手动编辑名称。 + - 点击加号可以添加不同的图层。 + - 使用鼠标拖拽排序图层左侧的灰色条纹状按钮可以手动对图层进行排序。 + + ![drag](index/drag-to-sort.gif) + +* **(2D UI 渲染排序)** 添加渲染层级后,在任意 2D 渲染组件的节点上添加 [Sorting2D](../../engine/rendering/sorting-2d.md) 组件并设置排序图层与图层内排序,即可自定义 2D UI 的渲染顺序。 +![sorting-2d](../../engine/rendering/sorting-2d/sorting2D-component.png) + +* **(3D 物体渲染排序)** 添加渲染层级后,在任意拥有 MeshRenderer 或者 SpriteRenderer 组件的节点上添加 [Sorting](../../engine/rendering/sorting.md) 组件并设置排序图层与图层内排序,即可自定义 3D 物体的渲染顺序。 +![sorting](index/sorting.png) + +## 物理 + +![physics](./index/physics.png) + +用于配置物理的各项参数,详情请参考 [物理配置](physics-configs.md)。 + +## 脚本 + +![scripting](./index/scripting.png) + +- **符合规范的类字段**:当开启时,将使用 Define 语义实现类字段,否则,将使用 Set 语义实现类字段。 + +- **允许声明类字段**:当开启时,在 TypeScript 脚本中将允许使用 declare 关键字来声明类字段。当字段以 declare 声明且未指定显式的初始化式时,将依照规范初始化为 undefined。 + +- **启用宽松模式**:启用宽松模式进行脚本编译。 + +- **导入映射**:该项用于控制 TypeScript/JavaScript 的导入行为,详情请参考 [Import Map](../../scripting/modules/import-map.md)。 + +- **用于预览的浏览器列表**:设置在预览时 TypeScript/JavaScript 代码编译的浏览器列表文件。 + > **示例**: + > 比如我们在预览的时候,async/await是使用的polyfill。但我们希望不使用polyfill, + > 那么我们可以在项目根目录下创建一个 previewbrowserlist.txt 文件,内容为 chrome 80 + > 然后在项目设置 -> 脚本 -> 用于预览的浏览器列表 -> 选择 previewbrowserlist.txt 这个文件。 + > 重启编辑器 或者 重启 脚本编译进程。 对于async/await的profill就没有了。 + +- **导出条件**:为条件化导出模块指定解析条件,详情可参考 [条件性导出](../../scripting/modules/spec.md#%E6%9D%A1%E4%BB%B6%E6%80%A7%E5%AF%BC%E5%87%BA)。 + +### 插件脚本排序 + +关于插件脚本的部分介绍,可以参见文档 [插件脚本](../../scripting/external-scripts.md) + +![Sorting Plugin Scripts](index/sorting-plugin-script.png) + +当部分插件脚本之间有一定的优先加载顺序时,可以在此处添加排序。(仅需添加有优先顺序的插件脚本即可) + +**操作方式**: + +- 点击 `+` 按钮,可以添加项目内已被识别但未被添加的插件脚本 +- 单击选中插件脚本后点击 `-` 按钮可以删除选中的插件脚本 +- 点击 `┇` 按钮,可以展开菜单,选择`添加所有插件脚本`或者`以 JSON 形式编辑`。 + + ![右键菜单](index/plugin-script-menu.png) + + 其中以 JSON 形式编辑主要是为了方便希望直接编辑数据的用户,此处仅提供了简单的转换,请自行做好数据的检查、格式的验证,不合法的 JSON 格式将无法被提交保存。 + + ![以 JSON 形式编辑](index/json-editor.png) + +## 模型 + +![model](./index/model.png) + +- [智能材质转换](../../importer/materials/fbx-materials.md):启用后,引擎会根据导出模型在原有 DCC 软件内的材质,将其导出为引擎支持的材质。 + +## 骨骼贴图布局设置 + +![joint texture layout](./index/joint-texture-layout.png) + +配置项目内的骨骼贴图布局。请参考 [骨骼贴图布局设置](../../animation/joint-texture-layout.md)。 + +## Macro Config(引擎宏配置) + +**引擎宏设置** 提供了修改宏配置的快捷方式,配置的宏将会在预览、构建时生效,同时也会跟随自定义引擎的配置更新当前宏配置的默认值。 + +![macro](./index/macro.png) + +- **ENABLE_TILEDMAP_CULLING**:是否开启 TiledMap 的自动裁减功能,默认开启。需要注意的是 TiledMap 如果设置了 `skew` 和 `rotation` 的话,建议手动关闭该项,否则会导致渲染出错。 + +- **TOUCH_TIMEOUT**:用于甄别一个触点对象是否已经失效并且可以被移除的延时时长。开发者可通过修改这个值来获得想要的效果,默认值是 5000 毫秒。详情请参考 API 文档 [TOUCH_TIMEOUT](%__APIDOC__%/zh/interface/Macro?id=TOUCH_TIMEOUT)。 + +- **ENABLE_TRANSPARENT_CANVAS**:用于设置 Canvas 背景是否支持 Alpha 通道,默认不开启支持。 + + - 若希望 Canvas 背景是透明的,并显示背后的其他 DOM 元素,便可开启该项。 + + - 若关闭该项,则会有更高的性能表现。 + +- **ENABLE_WEBGL_ANTIALIAS**:是否开启 WebGL 的抗锯齿配置,默认开启。这个配置只影响 WebGL 后端,对应在创建 WebGL Context 时是否传入抗锯齿选项(仅适用于前向渲染管线)。 + +- **ENABLE_ANTIALIAS_FXAA**:用于开启 FXAA 抗锯齿(仅适用于经典延迟渲染管线,要自定义FXAA功能请参考[可定制渲染管线](../../render-pipeline/custom-pipeline.md)。 + +- **ENABLE_BLOOM**:用于开启 BLOOM 后处理特效(仅适用于经典延迟管线,要自定义Bloom功能请参考[可定制渲染管线](../../render-pipeline/custom-pipeline.md)。 + +- **CLEANUP_IMAGE_CACHE**:是否在将贴图上传至 GPU 之后删除原始图片缓存,删除之后图片将无法进行 [动态合图](../../advanced-topics/dynamic-atlas.md)。该项默认不开启。 + +- **ENABLE_MULTI_TOUCH**:是否开启多点触摸,默认开启。 + +- **MAX_LABEL_CANVAS_POOL_SIZE**:设置 Label 使用的 Canvas 对象池的最大数量,请根据项目同场景的 Label 数量进行调整。 + +- **ENABLE_WEBGL_HIGHP_STRUCT_VALUES**(v3.4.1 新增):在带有 WebGL 后端的 Android 平台上,片元着色器中定义的结构体内部变量使用的是 mediump 精度,可能会导致一些需要高精度的数据(如位置信息)计算出现错误结果。可以通过勾选该项开启 WebGL 使用 highp 精度计算变量来避免该问题。 + + - 若开启该项,在片元着色器代码中需要使用 `HIGHP_VALUE_STRUCT_DEFINE` 宏来定义结构体变量,使用 `HIGHP_VALUE_TO_STRUCT_DEFINED` 和 `HIGHP_VALUE_FROM_STRUCT_DEFINED` 在结构体变量和非结构体变量之间赋值。 + + - 关于上述宏调用的具体信息与代码,详情请参考 **packing.chunk**([GitHub](https://github.com/cocos/cocos4/blob/v4.0.0/editor/assets/chunks/common/data/packing.chunk#L40) | [Gitee](https://gitee.com/mirrors_cocos-creator/engine/blob/v3.8.0/editor/assets/chunks/common/data/packing.chunk#L40))。 + +- **BATCHER2D_MEM_INCREMENT**(v3.4.1 新增):该项会影响每个 MeshBuffer 可容纳的最大顶点数量,默认值为 144KB,数量与值之间的转换关系请参考 [MeshBuffer 合批说明](../../ui-system/components/engine/ui-batch.md#meshbuffer-%E5%90%88%E6%89%B9%E8%AF%B4%E6%98%8E)。 + +- **Custom Macro**:用于自定义宏配置,为当前项目脚本提供一个宏标记的功能,便于可视化配置。点击下方的 **+** 按钮即可添加新的宏配置,将鼠标悬浮在已添加的宏配置上,左侧会显示 **删除** 和 **修改** 按钮,分别用于删除/重命名当前宏配置。 + + ![macro](./index/custom-macro.png) + +更多关于引擎宏模块的具体信息与代码可以参考 **Engine Macro**([GitHub](https://github.com/cocos/cocos4/blob/3d/cocos/core/platform/macro.ts#L824) | [Gitee](https://gitee.com/mirrors_cocos-creator/engine/blob/3d/cocos/core/platform/macro.ts#L824))。 + +## 功能裁剪 + +**功能裁剪** 分页主要是针对发布游戏时引擎中使用的模块进行裁剪,达到减小发布版引擎包体的效果。列表中未选中的模块在打包、预览时将会被裁剪掉。建议打包后进行完整的测试,避免场景和脚本中使用到了被裁剪掉的模块。 + +![feature-core](./index/feature-crop.png) + +## 压缩纹理 + +与 Cocos Creator 2.x 不同,Cocos Creator 3.0 的压缩纹理是在 **项目设置** 中配置预设,然后在 **属性检查器** 中选择图片资源的预设方式。旧版本的项目在升级到 v3.0 后,编辑器会自动扫描项目中所有的压缩纹理配置情况,整理出几个预设,由于是自动扫描的,所以预设名称可能不匹配项目,可以自行在此处修改。 + +### 纹理压缩预设 + +![compress-texture](./texture-compress/compress-texture.png) + +该分页主要用于添加压缩纹理预设配置,可添加多个,每个压缩纹理配置允许针对不同的平台制定配置细则。添加完成后,在 **层级管理器** 中选中图片资源,就可以在 **属性检查器** 中快速添加压缩纹理预设。同时也可以在该分页中直接修改预设来达到批量更新压缩纹理配置的使用需求。 + +目前配置压缩纹理支持以下平台: + +1. Web:包括 Web-Mobile 和 Web-Desktop 两个平台 +2. iOS +3. Mini Game: 包括目前 Creator 支持的所有小游戏平台,比如微信小游戏、华为快游戏等 +4. Android + +各平台对纹理压缩的支持情况,详情请参考 [压缩纹理](../../asset/compress-texture.md)。 + +### 属性 + +- **Mipmaps for compressed textures**: 对压缩的贴图实现预生成 Mipmap,可选项: **Allow Pre-generation(default)** 预生成;**Disabled** 禁用 + - 如项目是从 v3.7 之前的版本升级而来,则此选项默认关闭 + - 开启后生成多张图片,会影响到包大小,请根据需求选择 + - 如果用户使用的 texture 纹理勾选了 generate mip maps,需要生成贴图的 mipmaps 又勾选了纹理压缩配置了一些需要第三方工具压缩处理的格式比如 pvr etc 等,则需要在项目设置开启此选项。开启后,构建将会生成对应格式压缩过的 mipmaps 供运行时使用 + +#### 添加/删除纹理压缩预设 + +在上方的输入框中输入压缩纹理预设名称,点击 Enter 键或者右侧的加号按钮即可添加一个预设。另外两个按钮是用于导入/导出压缩纹理预设,详情请参考下文介绍。 + +![add](./texture-compress/add.png) + +添加完压缩纹理后,如需删除可以直接将鼠标移到预设名称上,点击右侧的删除按钮即可。 + +![delete](./texture-compress/delete.png) + +> **注意**:面板中内置的 **default** 和 **transparent** 这两个预设不可修改/删除。 + +#### 添加/删除纹理压缩格式 + +选择平台,然后点击 **Add Format** 按钮,选择需要的纹理格式,再配置好对应的质量等级即可,目前同类型的图片格式只能添加一次。 + +![add-format](./texture-compress/add-format.png) + +如需删除,将鼠标移至纹理格式上方,点击右侧的红色删除按钮即可。 + +![delete-format](./texture-compress/delete-format.png) + +#### 修改压缩纹理预设名称 + +压缩纹理预设的名称仅仅是作为 **显示** 使用,在添加压缩纹理预设时,就会随机生成 uuid 作为该预设的 ID,因而直接修改预设名称并不会影响图片资源处对预设的引用。 + +![edit](./texture-compress/edit.png) + +#### 导出/导入压缩纹理预设 + +压缩纹理配置页面允许导入/导出压缩纹理预设,以便更好地跨项目复用配置,也可以自行在外部编辑好压缩纹理预设再导入到编辑器。 + +大部分情况下直接导入导出即可,如果需要自行编写压缩纹理配置,请参考下方接口定义与范例: + +**接口定义**: + +```ts +type IConfigGroups = Record; +type ITextureCompressPlatform = 'miniGame' | 'web' | 'ios' | 'android' | 'pc'; +type ITextureCompressType = + | 'jpg' + | 'png' + | 'webp' + | 'pvrtc_4bits_rgb' + | 'pvrtc_4bits_rgba' + | 'pvrtc_4bits_rgb_a' + | 'pvrtc_2bits_rgb' + | 'pvrtc_2bits_rgba' + | 'pvrtc_2bits_rgb_a' + | 'etc1_rgb' + | 'etc1_rgb_a' + | 'etc2_rgb' + | 'etc2_rgba' + | 'astc_4x4' + | 'astc_5x5' + | 'astc_6x6' + | 'astc_8x8' + | 'astc_10x5' + | 'astc_10x10' + | 'astc_12x12'; +type IConfigGroupsInfo = Record +interface ICompressPresetItem { + name: string; + options: IConfigGroups; +} +``` + +**示例参考**: + +```json +{ + "default": { + "name": "default", + "options": { + "miniGame": { + "etc1_rgb": "fast", + "pvrtc_4bits_rgb": "fast" + }, + "android": { + "astc_8x8": "-medium", + "etc1_rgb": "fast" + }, + "ios": { + "astc_8x8": "-medium", + "pvrtc_4bits_rgb": "fast" + }, + "web": { + "astc_8x8": "-medium", + "etc1_rgb": "fast", + "pvrtc_4bits_rgb": "fast" + }, + } + }, + "transparent": { + "name": "transparent", + "options": { + "miniGame": { + "etc1_rgb_a": "fast", + "pvrtc_4bits_rgb_a": "fast" + }, + "android": { + "astc_8x8": "-medium", + "etc1_rgb_a": "fast" + }, + "ios": { + "astc_8x8": "-medium", + "pvrtc_4bits_rgb_a": "fast" + }, + "web": { + "astc_8x8": "-medium", + "etc1_rgb_a": "fast", + "pvrtc_4bits_rgb_a": "fast" + }, + } + } +} +``` + +### 自定义纹理压缩行为 + +自 v3.5.0 起,为了方便用户定制纹理压缩工具以及一些自定义参数、加密等等处理,支持了自定义纹理压缩方式的页面。界面交互与纹理压缩预设类似。 + +![custom-compress](./texture-compress/custom-compress.png) + +#### 配置方式 + +- **压缩格式**:指定当前压缩方式的处理格式类型,选择不同的格式质量选项会与之保持一致,同时如果自定义压缩方式处理失败会自动回退到编辑器原有格式的压缩方案; +- **覆盖原格式**:勾选后,将会自动覆盖已有纹理压缩预设内的原始压缩格式,原始配置名旁会出现自定义压缩方式名称的标识,同一个格式只能被一个自定义压缩方式覆盖; + + ![overwrite-format](./texture-compress/overwrite-format.png) + +- **压缩工具**: + - **程序**: 指定实际需要调用的压缩库地址; + - **命令行参数**:配置调用程序需要传递的参数,其中构建纹理压缩默认会传递的参数字段名可以通过参数输入框右侧的控件来快捷加入,目前默认会传递 `src, dest, quality` 字段。 + + ![custom-compress-options](./texture-compress/custom-compress-options.png) + +自定义压缩方式添加完成后,纹理压缩预设的 **添加压缩方式** 菜单中将会新增此配置,可以直接在预设里添加。 + +![overwrite-format](./texture-compress/custom-format.png) + +#### 构建阶段效果 + +配置为自定义纹理格式后,将会打印 `custom-compress` 字样,并附上对应的命令行参数信息等。 + +![custom-compress-log](./texture-compress/custom-compress-log.png) + +## 插屏设置 + +插屏设置是在游戏开始时,显示引擎的 LOGO 或开发者自定义的 LOGO 的功能。 + +![splash](./index/splash.png) + +- **最小显示时间**:显示插屏的最小时间(毫秒),最低为 500 毫秒 +- **自定义图片显示比例**:图片的放大比例,最低为 100% +- **插屏预览**:如果你想改变闪屏图像,将鼠标悬停在预览窗口,然后点击'+'按钮,然后选择一个新的图像路径。 + +如果你想禁用闪屏,请参考 [构建选项介绍](../publish/build-options.md) 详情。 + +> **注意**: +> 1. 在构建选项中选择不同的构建平台后,再次调整插屏规则可以实现不同平台插屏的多样性 +> 2. 插屏只会在打包后生效,预览时不会生效 +> 3. 部分国家和地区未开放完整的插屏功能,若对您造成不便,我们深表歉意。 + +## Bundle 配置 + +![bundle-config.png](index/bundle-config.png) + +Bundle 配置,可以让用户针对性的对不同的平台配置自己的 Bundle 方案,举一些例子,如:区分普通素材的和高清素材,针对不同的商店,等等。请参考 [Asset Bundle](../../asset/bundle.md)。 + +目前 Cocos Creator 支持发布的平台主要为三类:原生平台、Web 以及小游戏。 + +点击不同平台的按钮可以修改不同平台的配置。 + +![bundle-platforms.png](./index/bundle-platforms.png) + +通过 Bundle 配置面板右上角的菜单,可以选择 **新建配置**、**导入配置** 或 **导出配置**。 + +![bundle-menus.png](./index/bundle-menus.png) + +- **新建配置**:新建一个 Bundle 配置,新建后开发者可以在 Bundle 中修改配置。 + + ![bundle-change-type.png](./index/bundle-change-type.png) + + 选中任何配置的 Bundle,在 **平台设置** 选项中的下拉菜单中选择需要的配置。 + + > 不同的 Bundle 可以采用不同的配置。 + +- **导入配置**:从外界导入已导出的 JSON 文件格式的 Bundle 配置 +- **导出配置**:将配置导出为 JSON 可供其他项目使用 + +对于新增的配置,可以通过点击右侧的按钮 **复制配置**、**复制 ID**、**重命名** 和 **删除**。 + +![bundle-copy.png](./index/bundle-copy.png) + +### 平台覆盖 + +目前 Cocos Creator 支持多种平台发布,在各平台发布时,可以选择新增配置覆盖当前已经配置的平台选项。 + +点击 **平台覆盖** 右侧的 “+” 号,选择目标平台。 + +![bundle-override.png](./index/bundle-override.png) + +根据需要选择新的平台之后,这些新平台的配置将覆盖之前的通用配置。 + +## 自定义项目设置面板 + +Creator 支持在 **项目设置** 右侧添加自定义功能页,详情请参考 [自定义项目设置面板](../../editor/extension/contributions-project.md)。 diff --git a/versions/4.0/zh/editor/project/index/bundle-change-type.png b/versions/4.0/zh/editor/project/index/bundle-change-type.png new file mode 100644 index 0000000000..af0a09072e Binary files /dev/null and b/versions/4.0/zh/editor/project/index/bundle-change-type.png differ diff --git a/versions/4.0/zh/editor/project/index/bundle-config.png b/versions/4.0/zh/editor/project/index/bundle-config.png new file mode 100644 index 0000000000..7e2d9e0377 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/bundle-config.png differ diff --git a/versions/4.0/zh/editor/project/index/bundle-copy.png b/versions/4.0/zh/editor/project/index/bundle-copy.png new file mode 100644 index 0000000000..5ae6af4067 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/bundle-copy.png differ diff --git a/versions/4.0/zh/editor/project/index/bundle-menus.png b/versions/4.0/zh/editor/project/index/bundle-menus.png new file mode 100644 index 0000000000..c357c129b3 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/bundle-menus.png differ diff --git a/versions/4.0/zh/editor/project/index/bundle-override.png b/versions/4.0/zh/editor/project/index/bundle-override.png new file mode 100644 index 0000000000..b43a417de7 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/bundle-override.png differ diff --git a/versions/4.0/zh/editor/project/index/bundle-platforms.png b/versions/4.0/zh/editor/project/index/bundle-platforms.png new file mode 100644 index 0000000000..e1b9baad94 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/bundle-platforms.png differ diff --git a/versions/4.0/zh/editor/project/index/custom-macro.png b/versions/4.0/zh/editor/project/index/custom-macro.png new file mode 100644 index 0000000000..fc70d145a4 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/custom-macro.png differ diff --git a/versions/4.0/zh/editor/project/index/drag-to-sort.gif b/versions/4.0/zh/editor/project/index/drag-to-sort.gif new file mode 100644 index 0000000000..2171cac39c Binary files /dev/null and b/versions/4.0/zh/editor/project/index/drag-to-sort.gif differ diff --git a/versions/4.0/zh/editor/project/index/feature-crop.png b/versions/4.0/zh/editor/project/index/feature-crop.png new file mode 100644 index 0000000000..2e10729f25 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/feature-crop.png differ diff --git a/versions/4.0/zh/editor/project/index/joint-texture-layout.png b/versions/4.0/zh/editor/project/index/joint-texture-layout.png new file mode 100644 index 0000000000..14487d243a Binary files /dev/null and b/versions/4.0/zh/editor/project/index/joint-texture-layout.png differ diff --git a/versions/4.0/zh/editor/project/index/json-editor.png b/versions/4.0/zh/editor/project/index/json-editor.png new file mode 100644 index 0000000000..ef0d0e3a4d Binary files /dev/null and b/versions/4.0/zh/editor/project/index/json-editor.png differ diff --git a/versions/4.0/zh/editor/project/index/layers-camera.png b/versions/4.0/zh/editor/project/index/layers-camera.png new file mode 100644 index 0000000000..53b43216c6 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/layers-camera.png differ diff --git a/versions/4.0/zh/editor/project/index/layers-node.png b/versions/4.0/zh/editor/project/index/layers-node.png new file mode 100644 index 0000000000..f74809a8b4 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/layers-node.png differ diff --git a/versions/4.0/zh/editor/project/index/layers.png b/versions/4.0/zh/editor/project/index/layers.png new file mode 100644 index 0000000000..0f2791b44d Binary files /dev/null and b/versions/4.0/zh/editor/project/index/layers.png differ diff --git a/versions/4.0/zh/editor/project/index/macro.png b/versions/4.0/zh/editor/project/index/macro.png new file mode 100644 index 0000000000..38d7e70b55 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/macro.png differ diff --git a/versions/4.0/zh/editor/project/index/model.png b/versions/4.0/zh/editor/project/index/model.png new file mode 100644 index 0000000000..2d81a07a4b Binary files /dev/null and b/versions/4.0/zh/editor/project/index/model.png differ diff --git a/versions/4.0/zh/editor/project/index/path-protocol.png b/versions/4.0/zh/editor/project/index/path-protocol.png new file mode 100644 index 0000000000..f8b36d4e78 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/path-protocol.png differ diff --git a/versions/4.0/zh/editor/project/index/physics-collision-demo.png b/versions/4.0/zh/editor/project/index/physics-collision-demo.png new file mode 100644 index 0000000000..c8c9e93945 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/physics-collision-demo.png differ diff --git a/versions/4.0/zh/editor/project/index/physics-collision.png b/versions/4.0/zh/editor/project/index/physics-collision.png new file mode 100644 index 0000000000..8a8e7896ec Binary files /dev/null and b/versions/4.0/zh/editor/project/index/physics-collision.png differ diff --git a/versions/4.0/zh/editor/project/index/physics-in-engine.png b/versions/4.0/zh/editor/project/index/physics-in-engine.png new file mode 100644 index 0000000000..cb9ff8c3a3 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/physics-in-engine.png differ diff --git a/versions/4.0/zh/editor/project/index/physics-index.png b/versions/4.0/zh/editor/project/index/physics-index.png new file mode 100644 index 0000000000..e895e92815 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/physics-index.png differ diff --git a/versions/4.0/zh/editor/project/index/physics.png b/versions/4.0/zh/editor/project/index/physics.png new file mode 100644 index 0000000000..c1219e2a71 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/physics.png differ diff --git a/versions/4.0/zh/editor/project/index/plugin-script-menu.png b/versions/4.0/zh/editor/project/index/plugin-script-menu.png new file mode 100644 index 0000000000..6f8669f794 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/plugin-script-menu.png differ diff --git a/versions/4.0/zh/editor/project/index/project-data.png b/versions/4.0/zh/editor/project/index/project-data.png new file mode 100644 index 0000000000..c6f0ef3415 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/project-data.png differ diff --git a/versions/4.0/zh/editor/project/index/rigidbody-group.jpg b/versions/4.0/zh/editor/project/index/rigidbody-group.jpg new file mode 100644 index 0000000000..9601623a75 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/rigidbody-group.jpg differ diff --git a/versions/4.0/zh/editor/project/index/scripting.png b/versions/4.0/zh/editor/project/index/scripting.png new file mode 100644 index 0000000000..3c49b05416 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/scripting.png differ diff --git a/versions/4.0/zh/editor/project/index/sorting-layer.png b/versions/4.0/zh/editor/project/index/sorting-layer.png new file mode 100644 index 0000000000..3dc77c949d Binary files /dev/null and b/versions/4.0/zh/editor/project/index/sorting-layer.png differ diff --git a/versions/4.0/zh/editor/project/index/sorting-plugin-script.png b/versions/4.0/zh/editor/project/index/sorting-plugin-script.png new file mode 100644 index 0000000000..24fc1b265e Binary files /dev/null and b/versions/4.0/zh/editor/project/index/sorting-plugin-script.png differ diff --git a/versions/4.0/zh/editor/project/index/sorting.png b/versions/4.0/zh/editor/project/index/sorting.png new file mode 100644 index 0000000000..82e81660c9 Binary files /dev/null and b/versions/4.0/zh/editor/project/index/sorting.png differ diff --git a/versions/4.0/zh/editor/project/index/splash.png b/versions/4.0/zh/editor/project/index/splash.png new file mode 100644 index 0000000000..6f1f560e3f Binary files /dev/null and b/versions/4.0/zh/editor/project/index/splash.png differ diff --git a/versions/4.0/zh/editor/project/physics-configs.md b/versions/4.0/zh/editor/project/physics-configs.md new file mode 100644 index 0000000000..f713be6b59 --- /dev/null +++ b/versions/4.0/zh/editor/project/physics-configs.md @@ -0,0 +1,63 @@ +# 物理配置 + +物理配置用于配置各种常用属性,目前 __2D__/__3D__ 共用一个配置。 + +## 属性说明 + +![Physics](./index/physics-index.png) + +- `gravity` 重力矢量,默认值 __{ x: 0, y: -10, z: 0 }__ +- `allowSleep` 是否允许系统进入休眠状态,默认值 __true__ +- `sleepThreshold` 进入休眠的默认速度临界值,默认值 __0.1__,最小值 __0__ +- `autoSimulation` 是否开启自动模拟, 默认值 __true__ +- `fixedTimeStep` 每步模拟消耗的固定时间,默认值 __1/60__,最小值 __0__ +- `maxSubSteps` 每步模拟的最大子步数,默认值 __1__,最小值 __0__ +- `friction` 摩擦系数,默认值 __0.5__ +- `rollingFriction` 滚动摩擦系数,默认值 __0.1__ +- `spinningFriction` 自旋摩擦系数,默认值 __0.1__ +- `restitution` 弹性系数,默认值 __0.1__ +- `collisionMatrix` 碰撞矩阵,仅用于初始化 + + +## 碰撞矩阵 + +碰撞矩阵是[物理分组掩码](../../physics/physics-group-mask.md)功能的进一步封装,它用于初始化物理元素的分组和掩码。 + +默认情况下只有一个 __DEFAULT__ 分组,新建分组默认不与其它组碰撞。 + +![Physics-collision](./index/physics-collision.png) + +### 分组的概念 + +在编辑器中,碰撞矩阵分组的存储格式为 __{index, name}__,__index__ 是从 __0__ 到 __31__ 的位数,而 __name__ 是该组的名称,新项目工程会有一个默认分组:__{index: 0, name: 'DEFAULT'}__。 + +点击 __+__ 按钮可以新增分组。 + +**注:新增分组的 index 和 name 均不能为空,且不能与现有项重复**。 + +**注:分组不可以删除,但可以修改分组的名称**。 + +### 如何配置 + +以新增一个 __water__ 分组为例: + +![Physics-collision-demo](./index/physics-collision-demo.png) + +这张表列出了所有的分组,你可以通过勾选来决定哪两组会进行碰撞检测。 + +**如上图所示,`DEFAULT`和`water`是否会进行碰撞检测将取决于是否选中了对应的复选框**。 + +根据上面的规则,在这张表里产生的碰撞对有: + +- DEFAULT - water +- DEFAULT - DEFAULT + +而不进行碰撞检测的分组对有: + +- water - water + +### 配置物理组件的分组 + +通过刚体组件上的 __Group__ 属性来配置对应的物理元素的分组: + +![rigidbody-group](./index/rigidbody-group.jpg) \ No newline at end of file diff --git a/versions/4.0/zh/editor/project/texture-compress/add-format.png b/versions/4.0/zh/editor/project/texture-compress/add-format.png new file mode 100644 index 0000000000..7595797eff Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/add-format.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/add.png b/versions/4.0/zh/editor/project/texture-compress/add.png new file mode 100644 index 0000000000..aa1ecf7ef2 Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/add.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/compress-texture.png b/versions/4.0/zh/editor/project/texture-compress/compress-texture.png new file mode 100644 index 0000000000..ba2d602dfb Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/compress-texture.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/custom-compress-log.png b/versions/4.0/zh/editor/project/texture-compress/custom-compress-log.png new file mode 100644 index 0000000000..c78a629c6a Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/custom-compress-log.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/custom-compress-options.png b/versions/4.0/zh/editor/project/texture-compress/custom-compress-options.png new file mode 100644 index 0000000000..ee5a18accc Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/custom-compress-options.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/custom-compress.png b/versions/4.0/zh/editor/project/texture-compress/custom-compress.png new file mode 100644 index 0000000000..3d79a07ccb Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/custom-compress.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/custom-format.png b/versions/4.0/zh/editor/project/texture-compress/custom-format.png new file mode 100644 index 0000000000..3eebc6b364 Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/custom-format.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/delete-format.png b/versions/4.0/zh/editor/project/texture-compress/delete-format.png new file mode 100644 index 0000000000..fb2874a840 Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/delete-format.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/delete.jpg b/versions/4.0/zh/editor/project/texture-compress/delete.jpg new file mode 100644 index 0000000000..74dd320f9c Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/delete.jpg differ diff --git a/versions/4.0/zh/editor/project/texture-compress/delete.png b/versions/4.0/zh/editor/project/texture-compress/delete.png new file mode 100644 index 0000000000..debfc3a9f3 Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/delete.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/edit.png b/versions/4.0/zh/editor/project/texture-compress/edit.png new file mode 100644 index 0000000000..cf1ad8e67f Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/edit.png differ diff --git a/versions/4.0/zh/editor/project/texture-compress/overwrite-format.png b/versions/4.0/zh/editor/project/texture-compress/overwrite-format.png new file mode 100644 index 0000000000..a0a63019ad Binary files /dev/null and b/versions/4.0/zh/editor/project/texture-compress/overwrite-format.png differ diff --git a/versions/4.0/zh/editor/publish/android/build-example-android.md b/versions/4.0/zh/editor/publish/android/build-example-android.md new file mode 100644 index 0000000000..1da11208e8 --- /dev/null +++ b/versions/4.0/zh/editor/publish/android/build-example-android.md @@ -0,0 +1,176 @@ +# 安卓构建示例 + +本文将演示 Cocos Creator 项目发布为 Android 应用程序的流程。 + +请准备一个至少含有一个场景的 Cocos Creator 项目。 + +![project.png](images/project.png) + +## 发布环境搭建 + +要想发布 Android 原生应用,需要安装 Android Studio 开发环境,以及特定版本的 JDK(或者 OpenSDK),Android SDK 和 NDK 等。详情请参考 [配置 Android 原生开发环境](../setup-native-development.md)。 + +## 发布流程 + +接下来,在 Cocos Creator 找到 **项目** 菜单,点击 **构建发布** 按钮,弹出 **构建发布** 面板。 + +### 创建发布任务 + +1. 选择 **项目** -> **构建** 菜单,打开构建面板 + + ![cc-build-menu.png](images/cc-build-menu.png) + +2. 点击面板上的 **新建构建任务** 选项: + + ![new-build-task.png](images/new-build-task.png) + +3. 选择构建平台为安卓: + + ![select-platform.png](images/select-platform.png) + +4. 选择至少一个场景作为应用载入的首场景,当仅有一个场景时会被默认添加: + + ![start-scene.png](images/start-scene.png) + +5. 参考 [Android 平台构建选项 - 渲染后端](../native-options.md#%E6%B8%B2%E6%9F%93%E5%90%8E%E7%AB%AF) 选择渲染后端 + + ![render-backend.png](images/render-backend.png) + +6. 填入包名 + + ![game-package-name.png](images/game-package-name.png) + + > 名称规范请参考 [应用 ID 名称](../native-options.md#%E5%BA%94%E7%94%A8-id-%E5%90%8D%E7%A7%B0) + +7. 选择 Target API Level + + ![target-api-level.png](images/target-api-level.png) + + > 如果没有下拉框,请检查上面的 **配置 SDK 和 NDK** 是否正确。 + +其他选项请参考 [Android 平台构建选项](../native-options.md#android-%E5%B9%B3%E5%8F%B0%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) 进行配置。 + +### 构建与发布 + +1. 构建:点击下方的 **构建** 按钮会创建一个新的构建任务并开始构建 + + ![build.png](images/build.png) + +2. 等待构建完成 + + ![building.png](images/building.png) + +3. 在点击下面的按钮打开生成好的 Android Studio 工程: + + ![open](images/open.png) + +4. 找到工程对应的目录 + + ![find-proj](images/find-proj.png) + +5. 打开 Android Studio 的菜单: + + ![android studio open project menu](images/as-open-menu.png) + +6. 打开已经构建好的项目,`{项目路径}/build/android/proj`: + + ![android studio open project](images/as-open-proj.png) + +7. 使用 Android Studio 构建 APK + + 打开 Android Studio 后,会花一段时间进行准备工作,待 Android Studio 将项目准备完成后,即可打包 APK。准备过程可能会耗时较久,如果长时间无响应,请检查网络,或者切换到其他镜像。此时如果您需要中断目前的后台任务,可参考下列关闭方法: + + > Android Studio 有后台任务时,可以点击窗口下方的后台任务栏: + > ![background-task.png](./images/background-task.png)
+ > 在弹出窗中点击右侧的 × 以结束后台任务: + > ![interrupt-sync.png](images/interrupt-sync.png) + +8. 打开 **Build** 菜单选择 **Build Bundle(s) / APK(s)**: + + ![build-apk.png](images/build-apk.png) + +9. 发布成功后可以在 proj/build 目录内找到 Debug 版本的 APK: + + ![apk.png](images/apk.png) + +## 秘钥生成与使用 + +正式版本的发布,不能使用调试密钥,需要自行创建密钥。 + +### 创建密钥 + +秘钥可以通过 Android Studio 来生成: + +1. 在 Android Studio 中点击 Build 菜单,选中 Generate Signed Bundle / APK: + + ![gen-sign-apk.png](images/gen-sign-apk.png) + +2. 在弹出菜单的窗口中选择 APK,点击 Next 按钮: + + ![gen-apk.png](images/gen-apk.png) + +3. 在引导的窗口中点击 Create new: + + ![create-sign.png](images/create-sign.png) + +4. 在弹出的窗口中,填好相应信息即可: + + ![android-keystore-panel.png](images/android-keystore-panel.png) + + > 不同的项目,建议使用不同的密钥。可以将每个项目的密钥保存在项目根目录下。 + +### 使用密钥 + +密钥创建成功后,会在选择的目录下生成一个密钥文件,我们可以在 Android 构建面板上,将密钥信息填好。 这样不管是调试版还是发布版,都使用的是自己创建的密钥。 + +如下图所示,取消 **使用调试秘钥**,并从下方的 **秘钥库路径** 中找到自定义的秘钥文件,填好相关信息即可。 + +![debug-keystore.png](images/debug-keystore.png) + +### 注意事项 + +1. 建议项目第一次发布时,就为项目创建一个专用的密钥。 +2. 一些 SDK 的服务接入需要 APK 密钥签名验证,使用默认的调试密钥可能导致服务调用失败。 + +## 进阶 + +### JAVA 和 TypeScript 互相通信 + +引擎提供多样化的方法用于解决 TypeScript 和 Native 层交互问题。 + +常见的 SDK 接入时,我们需要通过 SDK 的登陆接口进行登陆操作,获取到登陆结果后再将结果传入到 TypeScript 层以方便游戏进行处理。 + +引擎提供了三种用于 TS 与 Android 原生通信的方法。 + +- [使用 JsbBridge 实现 JavaScript 与 Java 通信](../../../advanced-topics/js-java-bridge.md) :用于对接 SDK 中如注册、登陆、广告弹出等行为是非常方便的,可以较为快速的解决这类问题。 + +- [JSB 2.0 绑定教程](../../../advanced-topics/JSB2.0-learning.md):需要高频调用,或者批量导出 API 时,推荐使用这个方法。 + +- [基于反射机制实现 JavaScript 与 Android 系统原生通信](../../../advanced-topics/java-reflection.md) :在一些非高频调用时,非常有效。 + +### 导入第三方库 + +为了将应用发布到应用市场,通常需要对接某些第三方的 SDK,通常这些 SDK 会提供给您 JAR 或 AAR 格式,您可以参考 [在其他项目中使用您的库](https://developer.android.com/studio/projects/android-library?hl=zh-cn#psd-add-library-dependency) 将本地的库导入到工程内。 + +### 扩展发布机制 + +参考 [自定义构建流程](../custom-build-plugin.md) 通过扩展系统对发布机制进行扩展。 + +## Q&A + +- Q:发布时的错误如何调试 + - A:点击日志按钮,进行查看 + + ![show-log.png](images/show-log.png) + +- Q:提示缺少 LIB_EGL + - A:更换 NDK 版本为文中所述的版本 + +- Q:文件命名 + - A:由于安卓是基于 Linux 的,因此 Linux 的一些文件规范在安卓依然适用,如: + - 1. 安卓是大小写敏感的,确保路径的大小写是正确的,否则将无法正确加载 + - 2. 文件夹或文件名中不要含有空格等字符 + +## 特性与系统版本 + +不同的特性会依赖特定的系统版本,请往 [特性与系统版本](./../../../advanced-topics/supported-versions.md) 查看详情。 diff --git a/versions/4.0/zh/editor/publish/android/build-options-android.md b/versions/4.0/zh/editor/publish/android/build-options-android.md new file mode 100644 index 0000000000..a65cee0db4 --- /dev/null +++ b/versions/4.0/zh/editor/publish/android/build-options-android.md @@ -0,0 +1,124 @@ +# Android 平台构建选项 + +Android 平台的构建选项如下: + +![build-options-android.png](./images/build-options-android.png) + +## 平滑帧率 + +**平滑帧率(Enable Swappy)**:是否启用引擎里集成的 Swappy 功能。目前支持 GLES 和 Vulkan。 + +该选项会主动配合屏幕刷新率调整渲染时间,以达到稳定帧率和减少无效绘制。构建参数为 swappy。 + +更多信息可参考官网 [Frame Pacing Library Overview](https://source.android.com/devices/graphics/frame-pacing)。 + +## 渲染后端 + +目前支持 [VULKAN](https://www.vulkan.org/)、[GLES3](https://www.khronos.org/registry/OpenGL-Refpages/es3/) 和 [GLES2](https://www.khronos.org/registry/OpenGL-Refpages/es2.0/) 三种,要求至少勾选一项,默认使用 **GLES3**。 + +若选择 **GLES 2/3**,则默认必须勾选 **GLES3**,不允许单独选择 **GLES2**。 + +在同时勾选多个的情况下,运行时将会根据设备实际支持情况选择使用的渲染后端。 + +## 应用 ID 名称 + +**应用 ID 名称**(Game Package Name)通常以产品网站 URL 倒序排列,如 `com.mycompany.myproduct`。 + +> **注意**:包名中只能包含数字、字母和下划线,此外包名最后一部分必须以字母开头,不能以下划线或数字开头。 + +## Target API Level + +设置编译 Android 平台时所需的 Target API Level。点击旁边的 **Set Android SDK** 按钮即可快速跳转到配置页,具体配置规则请参考 [Android 原生发布环境配置](../setup-native-development.md)。 + +## APP ABI + +设置 Android 需要支持的 CPU 类型,可以选择一个或多个选项,目前包括 **armeabi-v7a**、**arm64-v8a**、**x86** 和 **x86_64** 四种类型。 + +> **注意**: +> +> 1. 当你选择一个 ABI 构建完成之后,在不 Clean 的情况下,构建另外一个 ABI,此时两个 ABI 的 so 都会被打包到 apk 中,这个是 Android Studio 默认的行为。若用 Android Studio 导入工程,选择一个 ABI 构建完成之后,先执行一下 **Build -> Clean Project** 再构建另外一个 ABI,此时只有后面那个 ABI 会被打包到 apk 中。 +> +> 2. 项目工程用 Android Studio 导入后,是一个独立的存在,不依赖于构建发布面板。如果需要修改 ABI,直接修改 **gradle.properties** 文件中的 **PROP_APP_ABI** 属性即可。 +> +> ![modify abi](../publish-native/modify_abi.png) + +## 使用调试密钥库 + +Android 要求所有 APK 必须先使用证书进行数字签署,然后才能安装。Cocos Creator 提供了默认的密钥库,勾选 **使用调试密钥库** 就是使用默认密钥库。若开发者需要自定义密钥库可去掉 **使用调试密钥库** 勾选,详情请参考 [官方文档](https://developer.android.google.cn/studio/publish/app-signing?hl=zh-cn)。 + +## 屏幕方向 + +屏幕方向目前包括 **Portrait**、**Landscape Left**、**Landscape Right** 三种。 + +- **Portrait**:屏幕直立,Home 键在下 +- **Landscape Left**:屏幕横置,Home 键在屏幕左侧 +- **Landscape Right**:屏幕横置,Home 键在屏幕右侧 + +## Google Play Instant + +勾选该项即可将游戏打包发布到 Google Play Instant。Google Play Instant 依赖于 Google Play,并不是一个新的分发渠道,而是更接近一种游戏微端方案。它能够实现游戏的免安装即开即玩,有利于游戏的试玩、分享和转化。 + +> **使用时需要注意以下几点**: +> +> 1. Android Studio 的版本要在 4.0 及以上 +> +> 2. Android Phone 6.0 及以上。Android SDK 版本在 6.0 到 7.0 之间的设备需要安装 Google 服务框架,SDK 版本在 8.0 以上的则不需要,可直接安装使用。 +> +> 3. 首次编译的话需要用 Android Studio 打开构建后的工程以下载 **Google Play Instant Development SDK(windows)** 或 **Instant Apps Development SDK(Mac)** 支持包。如果下载不成功的话建议设置一下 Android Studio 的 HTTP 代理。 +> +> ![Google Play Instant](../publish-native/sdk-android-instant.png) + +## 生成 App Bundle(Google Play) + +勾选该项即可将游戏打包成 App Bundle 格式用于上传到 Google Play 商店。具体请参考 [官方文档](https://developer.android.google.cn/guide/app-bundle/)。 + +## 其他选项 + +安卓有部分 SDK,NDK 选项需要在偏好设置里设置,除了在偏好设置以外还可以通过构建参数传递指定。 + +**sdkPath**: 指定 SDK 路径 +**ndkPath**: 指定 NDK 路径 + +可以通过导出构建配置,并在安卓对应的选项内添加参数即可。 + +## 构建参数接口定义(用于命令行构建修改参数) + +```ts +interface IOptions { + packageName: string; + resizeableActivity: boolean; + maxAspectRatio: string; + orientation: { + landscapeRight: boolean; + landscapeLeft: boolean; + portrait: boolean; + upsideDown: boolean; + }, + + apiLevel: number; + appABIs: IAppABI[]; + + useDebugKeystore: boolean; + keystorePath: string; + keystorePassword: string; + keystoreAlias: string; + keystoreAliasPassword: string; + + appBundle: boolean; + androidInstant: boolean; + inputSDK: boolean; + remoteUrl: string; + sdkPath: string; + ndkPath: string; + javaHome?: string; + javaPath?: string; + + swappy: boolean; + + renderBackEnd: { + vulkan: boolean; + gles3: boolean; + gles2: boolean; + } +} +``` diff --git a/versions/4.0/zh/editor/publish/android/build-setup-evn-android.md b/versions/4.0/zh/editor/publish/android/build-setup-evn-android.md new file mode 100644 index 0000000000..5fb02cb536 --- /dev/null +++ b/versions/4.0/zh/editor/publish/android/build-setup-evn-android.md @@ -0,0 +1,108 @@ +# Android 原生开发环境配置 + +## 下载 Android Studio + +开发者可以从 [Android Studio 官方网站](https://developer.android.com/studio) 下载对应的 IDE。 + +参考 [安装配置原生开发环境](../setup-native-development.md##android-%E5%B9%B3%E5%8F%B0%E7%9B%B8%E5%85%B3%E4%BE%9D%E8%B5%96) 搭建开发环境。 + +## 下载并安装 JDK + +参考 [安装配置原生开发环境 - 下载 Java SDK(JDK)](../setup-native-development.md#%E4%B8%8B%E8%BD%BD-java-sdk%EF%BC%88jdk%EF%BC%89) + +在终端中输入 `java -version` 查看是否安装成功 + +![terminal.png](images/terminal.png) + +> 需要配置对应的环境变量,请参考 [如何设置或更改 JAVA 系统环境变量](https://www.java.com/zh_CN/download/help/path.xml)。
+> +> **注意**:[OpenJDK](https://openjdk.org/) 和 JDK 仅是开源协议不同,功能和配置方法没有本质区别。 + +## 下载并安装 Android SDK + +### 自动下载 + +在 Android Studio 中下载 Android SDK 步骤如下: + +1. 启动 Android Studio + +2. 通过下列菜单打开 Setting 面板 + ![as-setting.png](images/as-setting.png) + +3. 在 Setting 内找到 SDK 下载分页 + ![download-sdk.png](images/download-sdk.png) + +4. 选中对应版本的 SDK 并加载,可以选择使用 [下载发布 Android 平台所需的 SDK 和 NDK](../setup-native-development.md#%E4%B8%8B%E8%BD%BD%E5%8F%91%E5%B8%83-android-%E5%B9%B3%E5%8F%B0%E6%89%80%E9%9C%80%E7%9A%84-sdk-%E5%92%8C-ndk) 中推荐的版本 + + - 以 Android 11.0(R) 为示例,选中 Name 前的勾并点击上图中的 OK 或者 Apply 按钮。 + + - 在弹出框中选择 OK + ![confirm-change.png](images/confirm-change.png) + + - 下载 + ![component-installer.png](images/component-installer.png) + +### 手动下载 + +如果出现网络问题无法在 Android Studio 中下载 Android SDK,可以通过手动的方式下载并放在 **Android SDK Location** 目录内。 + +下载完成后,可以在 Android Studio 内配置 SDK,如下图所示: + +![sdk-location.png](images/sdk-location.png) + +无法下载 SDK 时,可以修改代理,配置镜像: + +在 Android Studio 中配置 HTTP 代理: + +**自动配置代理**:在 Setting 中找到 HTTP Proxy,勾选 Auto-detect proxy settings,填入下方描述的镜像源,以腾讯源为例: + +![http-proxy.png](images/http-proxy.png) + +**可选的镜像源**:安卓 SDK 以及下文中提到的 Gradle 都会有一些镜像源用于帮助您解决无法从官网下载的问题,如果下方表格的镜像也无法解决,也可以从搜索引擎获取。 + +| 镜像源 | 地址 | +| :--- | :--- | +| 腾讯 | | +| 阿里云 | | + +> 如果出现镜像不可用的情况,您可以视可用性更换不同的镜像源。 + +## 下载并安装 NDK + +根据 [安装配置原生开发环境 - 下载发布 Android 平台所需的 SDK 和 NDK](../setup-native-development.md) 推荐的版本区间下载 NDK(推荐使用版本为 **r18~21**)。 + +### 通过 Android Studio 下载 NDK + +打开 Android Studio 的 **Setting** 窗口,找到 **Android SDK** 分页: + +> **注意**:需要勾选 **Show Package Details**:
+ +![show-package-details.png](images/show-package-details.png) + +![download-ndk-by-as.png](images/download-ndk-by-as.png) + +勾选合适的版本进行下载,如果无法刷新出所有 NDK 版本,请查看下文手动下载部分。 + +### 手动下载 NDK + +您可以在 [历史版本](https://github.com/android/ndk/wiki/Unsupported-Downloads#r20b) 找到下载。 + +在上述地址中找到对应平台的 NDK 版本并下载,此处以 r20b 为例,选择您的操作系统所需的安装包,下载并解压到本地。 + +![download-ndk.png](images/download-ndk.png) + +## 配置 SDK 和 NDK + +回到 Cocos Creator 中,在 **文件** -> **偏好设置** 内找到 **程序管理器**,并配置好 Android NDK 和 Android SDK: + +![program.png](images/program.png) + +目录选择如下: + +**NDK:** + +![ndk-dir.png](images/ndk-dir.png) + +**SDK:** + +![sdk-dir.png](images/sdk-dir.png) diff --git a/versions/4.0/zh/editor/publish/android/images/accept.png b/versions/4.0/zh/editor/publish/android/images/accept.png new file mode 100644 index 0000000000..d11260cc42 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/accept.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/android-keystore-panel.png b/versions/4.0/zh/editor/publish/android/images/android-keystore-panel.png new file mode 100644 index 0000000000..76f1b51ac9 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/android-keystore-panel.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/android-sdk-preference.png b/versions/4.0/zh/editor/publish/android/images/android-sdk-preference.png new file mode 100644 index 0000000000..838577f99f Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/android-sdk-preference.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/apk.png b/versions/4.0/zh/editor/publish/android/images/apk.png new file mode 100644 index 0000000000..349353c1c0 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/apk.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/as-open-menu.png b/versions/4.0/zh/editor/publish/android/images/as-open-menu.png new file mode 100644 index 0000000000..b1f57b5d4c Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/as-open-menu.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/as-open-proj.png b/versions/4.0/zh/editor/publish/android/images/as-open-proj.png new file mode 100644 index 0000000000..9eb58e9f8e Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/as-open-proj.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/as-setting.png b/versions/4.0/zh/editor/publish/android/images/as-setting.png new file mode 100644 index 0000000000..cb72db0d6a Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/as-setting.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/background-task.png b/versions/4.0/zh/editor/publish/android/images/background-task.png new file mode 100644 index 0000000000..0977698682 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/background-task.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/build-apk-success.png b/versions/4.0/zh/editor/publish/android/images/build-apk-success.png new file mode 100644 index 0000000000..0fb0f4a4b9 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/build-apk-success.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/build-apk.png b/versions/4.0/zh/editor/publish/android/images/build-apk.png new file mode 100644 index 0000000000..b5692a05ec Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/build-apk.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/build-menu.png b/versions/4.0/zh/editor/publish/android/images/build-menu.png new file mode 100644 index 0000000000..3d4968bcbb Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/build-menu.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/build-options-android.png b/versions/4.0/zh/editor/publish/android/images/build-options-android.png new file mode 100644 index 0000000000..894aabe3e4 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/build-options-android.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/build.png b/versions/4.0/zh/editor/publish/android/images/build.png new file mode 100644 index 0000000000..a81485fb5b Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/build.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/building.png b/versions/4.0/zh/editor/publish/android/images/building.png new file mode 100644 index 0000000000..427d0a9987 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/building.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/cc-build-menu.png b/versions/4.0/zh/editor/publish/android/images/cc-build-menu.png new file mode 100644 index 0000000000..83935002d6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/cc-build-menu.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/check-err.png b/versions/4.0/zh/editor/publish/android/images/check-err.png new file mode 100644 index 0000000000..4978158a95 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/check-err.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/component-installer.png b/versions/4.0/zh/editor/publish/android/images/component-installer.png new file mode 100644 index 0000000000..f57a8b2f40 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/component-installer.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/confirm-change.png b/versions/4.0/zh/editor/publish/android/images/confirm-change.png new file mode 100644 index 0000000000..3e6d57aadb Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/confirm-change.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/create-scene.png b/versions/4.0/zh/editor/publish/android/images/create-scene.png new file mode 100644 index 0000000000..7fa7f6560d Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/create-scene.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/create-sign.png b/versions/4.0/zh/editor/publish/android/images/create-sign.png new file mode 100644 index 0000000000..5f633747ed Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/create-sign.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/debug-keystore.png b/versions/4.0/zh/editor/publish/android/images/debug-keystore.png new file mode 100644 index 0000000000..c0f2c237bd Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/debug-keystore.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/download-ndk-by-as.png b/versions/4.0/zh/editor/publish/android/images/download-ndk-by-as.png new file mode 100644 index 0000000000..40ddd3dc32 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/download-ndk-by-as.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/download-ndk.png b/versions/4.0/zh/editor/publish/android/images/download-ndk.png new file mode 100644 index 0000000000..37b1acf917 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/download-ndk.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/download-sdk.png b/versions/4.0/zh/editor/publish/android/images/download-sdk.png new file mode 100644 index 0000000000..07b8a8bcc7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/download-sdk.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/downloading-complete.png b/versions/4.0/zh/editor/publish/android/images/downloading-complete.png new file mode 100644 index 0000000000..70f6c65565 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/downloading-complete.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/downloading.png b/versions/4.0/zh/editor/publish/android/images/downloading.png new file mode 100644 index 0000000000..398f61dcaf Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/downloading.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/find-proj.png b/versions/4.0/zh/editor/publish/android/images/find-proj.png new file mode 100644 index 0000000000..ac741c9f4d Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/find-proj.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/find-required-version.png b/versions/4.0/zh/editor/publish/android/images/find-required-version.png new file mode 100644 index 0000000000..f284735b3e Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/find-required-version.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/find-sdk-version.png b/versions/4.0/zh/editor/publish/android/images/find-sdk-version.png new file mode 100644 index 0000000000..8fc2e931a9 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/find-sdk-version.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/game-package-name.png b/versions/4.0/zh/editor/publish/android/images/game-package-name.png new file mode 100644 index 0000000000..f838d433cd Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/game-package-name.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/gen-apk.png b/versions/4.0/zh/editor/publish/android/images/gen-apk.png new file mode 100644 index 0000000000..df8ae2fbab Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/gen-apk.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/gen-sign-apk.png b/versions/4.0/zh/editor/publish/android/images/gen-sign-apk.png new file mode 100644 index 0000000000..e3249799ab Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/gen-sign-apk.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/http-proxy.png b/versions/4.0/zh/editor/publish/android/images/http-proxy.png new file mode 100644 index 0000000000..ffa66da5ae Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/http-proxy.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/install-ndk-by-as.png b/versions/4.0/zh/editor/publish/android/images/install-ndk-by-as.png new file mode 100644 index 0000000000..81e07c3974 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/install-ndk-by-as.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/interrupt-sync.png b/versions/4.0/zh/editor/publish/android/images/interrupt-sync.png new file mode 100644 index 0000000000..f10fdbe28d Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/interrupt-sync.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/java-ts.png b/versions/4.0/zh/editor/publish/android/images/java-ts.png new file mode 100644 index 0000000000..e6e0a28319 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/java-ts.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/manual-proxy.png b/versions/4.0/zh/editor/publish/android/images/manual-proxy.png new file mode 100644 index 0000000000..79b87fac49 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/manual-proxy.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/missing-sdk.png b/versions/4.0/zh/editor/publish/android/images/missing-sdk.png new file mode 100644 index 0000000000..4e3de76a2c Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/missing-sdk.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/ndk-dir.png b/versions/4.0/zh/editor/publish/android/images/ndk-dir.png new file mode 100644 index 0000000000..adb9459b76 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/ndk-dir.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/new-build-task.png b/versions/4.0/zh/editor/publish/android/images/new-build-task.png new file mode 100644 index 0000000000..6ac624a9b3 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/new-build-task.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/open-with-as.png b/versions/4.0/zh/editor/publish/android/images/open-with-as.png new file mode 100644 index 0000000000..42cb349157 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/open-with-as.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/open.png b/versions/4.0/zh/editor/publish/android/images/open.png new file mode 100644 index 0000000000..cbc97681ab Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/open.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/package-name.png b/versions/4.0/zh/editor/publish/android/images/package-name.png new file mode 100644 index 0000000000..0fd4824c87 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/package-name.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/preference.png b/versions/4.0/zh/editor/publish/android/images/preference.png new file mode 100644 index 0000000000..850cf3a115 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/preference.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/program.png b/versions/4.0/zh/editor/publish/android/images/program.png new file mode 100644 index 0000000000..f734f91b7b Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/program.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/progress.png b/versions/4.0/zh/editor/publish/android/images/progress.png new file mode 100644 index 0000000000..53048055b7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/progress.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/project.png b/versions/4.0/zh/editor/publish/android/images/project.png new file mode 100644 index 0000000000..2bd2bef86c Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/project.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/render-backend.png b/versions/4.0/zh/editor/publish/android/images/render-backend.png new file mode 100644 index 0000000000..b5e88160a7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/render-backend.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/sdk-component-setup.png b/versions/4.0/zh/editor/publish/android/images/sdk-component-setup.png new file mode 100644 index 0000000000..b3dd272f98 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/sdk-component-setup.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/sdk-dir.png b/versions/4.0/zh/editor/publish/android/images/sdk-dir.png new file mode 100644 index 0000000000..d127103c27 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/sdk-dir.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/sdk-location.png b/versions/4.0/zh/editor/publish/android/images/sdk-location.png new file mode 100644 index 0000000000..86a8844d48 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/sdk-location.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/select-platform.png b/versions/4.0/zh/editor/publish/android/images/select-platform.png new file mode 100644 index 0000000000..d160c93e08 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/select-platform.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/select-sdk.png b/versions/4.0/zh/editor/publish/android/images/select-sdk.png new file mode 100644 index 0000000000..44b4ab4328 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/select-sdk.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/show-log.png b/versions/4.0/zh/editor/publish/android/images/show-log.png new file mode 100644 index 0000000000..9e9cb4d6cb Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/show-log.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/show-package-details.png b/versions/4.0/zh/editor/publish/android/images/show-package-details.png new file mode 100644 index 0000000000..832618e2b5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/show-package-details.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/start-scene.png b/versions/4.0/zh/editor/publish/android/images/start-scene.png new file mode 100644 index 0000000000..7ac450b1ad Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/start-scene.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/target-api-level.png b/versions/4.0/zh/editor/publish/android/images/target-api-level.png new file mode 100644 index 0000000000..b6006ef0ee Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/target-api-level.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/terminal.png b/versions/4.0/zh/editor/publish/android/images/terminal.png new file mode 100644 index 0000000000..2d152f9bb0 Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/terminal.png differ diff --git a/versions/4.0/zh/editor/publish/android/images/verify-setting.png b/versions/4.0/zh/editor/publish/android/images/verify-setting.png new file mode 100644 index 0000000000..cd75d0091a Binary files /dev/null and b/versions/4.0/zh/editor/publish/android/images/verify-setting.png differ diff --git a/versions/4.0/zh/editor/publish/android/index.md b/versions/4.0/zh/editor/publish/android/index.md new file mode 100644 index 0000000000..853060b111 --- /dev/null +++ b/versions/4.0/zh/editor/publish/android/index.md @@ -0,0 +1,21 @@ +# 安卓平台构建打包 + +本章将说明如何将 Cocos Creator 应用构建并打包到安卓平台。 + +## 主要内容 + +- [Android 构建示例](./build-example-android.md) +- [Android 构建选项介绍](build-options-android.md) +- [基于反射机制实现 JavaScript 与 Android 系统原生通信](../../../advanced-topics/java-reflection.md) +- [使用 JsbBridge 实现 JavaScript 与 Java 通信](../../../advanced-topics/js-java-bridge.md) +- [特性与系统版本](../../../advanced-topics/supported-versions.md) + +## 相关阅读 + +- [安装配置原生开发环境](../setup-native-development.md) +- [构建发布面板](../build-panel.md) +- [通用构建选项介绍](../build-options.md) +- [原生平台通用构建选项](../native-options.md) +- [原生平台 JavaScript 调试](../debug-jsb.md) +- [构建流程简介与常见错误处理](../build-guide.md) +- [原生平台二次开发指南](../../../advanced-topics/native-secondary-development.md) diff --git a/versions/4.0/zh/editor/publish/app-clip/cocos-proj.png b/versions/4.0/zh/editor/publish/app-clip/cocos-proj.png new file mode 100644 index 0000000000..81f1e52e74 Binary files /dev/null and b/versions/4.0/zh/editor/publish/app-clip/cocos-proj.png differ diff --git a/versions/4.0/zh/editor/publish/app-clip/other-proj.png b/versions/4.0/zh/editor/publish/app-clip/other-proj.png new file mode 100644 index 0000000000..652e667ef6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/app-clip/other-proj.png differ diff --git a/versions/4.0/zh/editor/publish/app-clip/ui-build.png b/versions/4.0/zh/editor/publish/app-clip/ui-build.png new file mode 100644 index 0000000000..c7eefa7e6c Binary files /dev/null and b/versions/4.0/zh/editor/publish/app-clip/ui-build.png differ diff --git a/versions/4.0/zh/editor/publish/build-guide.md b/versions/4.0/zh/editor/publish/build-guide.md new file mode 100644 index 0000000000..99dda0cfc2 --- /dev/null +++ b/versions/4.0/zh/editor/publish/build-guide.md @@ -0,0 +1,256 @@ +# 构建流程简介与常见问题指南 + +## 构建基础结构介绍 + +构建流程主要包括以下两部分内容: + +- **通用构建处理** +- **各平台构建处理** + +由于 v3.0 在构建机制上的调整,不同平台的构建处理均以 **构建扩展** 的形式注入 **构建发布** 面板,以及参与构建流程。各平台特有的构建选项也会以展开选项的形式显示在 **构建发布** 面板,开发者可以通过 [构建扩展](custom-build-plugin.md) 将自定义的构建选项显示在 **构建发布** 面板上。 + +![build-engine](./build-guide/web.png) + +## 通用构建处理流程 + +Cocos Creator 的通用构建流程,主要包括以下内容: + +1. 构建选项初始化 +2. 构建数据整理 +3. 将构建后的资源写入文件系统 +4. 整理 `settings.json`/`config.json` 数据 +5. `config.json` 中的 UUID 压缩与文件写入 + +### 构建选项初始化 + +这个步骤主要是将构建时传递给构建的 **原始 options** 初始化为 **构建内部的 options**,完成部分构建选项的格式转换、项目配置初始化、构建资源数据库的资源数据初始化、查询最新的资源信息并分类。 + +### 构建数据整理 + +构建时,编辑器会先整理出当前参与构建的场景以及所有 [Bundle](../../asset/bundle.md) 文件夹中的资源,再通过引擎的反序列化查找出这些资源的依赖资源,层层递归查找出所有需要打包的资源列表。这份资源列表汇总完成后,便会根据 `Bundle` 配置对资源进行分类,并收集脚本编译任务、图片压缩任务和 json 分组信息等。 + +> **注意**: +> +> 1. 引擎在反序列化之前会先加载所有的用户脚本,脚本是否加载成功会直接影响到资源反序列化,所以如果脚本编写不合法,会直接构建失败。
+> 2. 如果资源整理过程中发现有依赖资源丢失会直接报错,通常情况下会继续进行构建。虽然允许继续构建成功,但并不意味着问题不需要解决,如果资源丢失不解决,很难保证构建后的游戏包允许没有问题。 + +### 将构建后的资源写入文件系统 + +构建后的资源会根据 [Asset Bundle](../../asset/bundle.md#%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95) 的配置在构建完成后打包到对应平台发布包目录下的 `assets/[Bundle 名称]` 中,目录结构如下: + +![build-engine](./build-guide/bundle.png) + +更多关于 `Asset Bundle` 的配置、构建、文件说明等内容,请参考 [Asset Bundle](../../asset/bundle.md) 文档。 + +在整理完基本的构建任务后,构建将会循环所有的 `Bundle`,依次执行完每个 `Bundle` 需要处理的资源打包任务,生成这些 `Bundle` 资源包。 + +每个 `Bundle` 都会经过以下资源处理流程: + +- **脚本构建**:编辑器内的脚本分为 [插件脚本](../../scripting/external-scripts.md) 和 **非插件脚本** 两类,不同种类的处理规则不同。 + + - **插件脚本**:直接将源文件按照原来的目录结构拷贝到构建后生成的发布包目录下的 `src` 目录中,所以插件脚本是不支持任何需要编译的脚本形式的,比如 `TypeScript` 或者是使用 ES6 写法的 `JavaScript`。扩展脚本的资源信息会写进 `settings.json` 的 `jsList` 数组中。 + + - **非插件脚本**:将会全部打包成 `project.js`,放在对应的 `src` 目录下。勾选 `sourceMap` 选项将会生成对应的 `.map` 文件,根据 `debug` 选项来影响脚本是否混淆压缩。 + +- **自动图集处理**:查询项目内部的自动图集资源列表,根据自动图集资源的配置将图集下的 `SpriteFrame` 小图打包成大图、生成序列化文件等等。这一步骤也会修改 `json` 分组信息、asset 资源分组信息以及根据需要添加纹理压缩任务。 + +- **纹理压缩**:根据整理好的图片压缩任务,进行图片资源的压缩处理并写入到构建文件夹内。 + +- **引擎脚本编译**:根据顶部菜单栏中 **项目设置 -> 功能裁剪** 的配置,剔除没有使用到的引擎模块,打包到 `cocos-js` 目录下。勾选 `sourceMap` 选项将会生成对应的 `.map` 文件,根据 `debug` 选项来确定脚本是否混淆压缩。 + + 引擎编译的主要步骤包括: + + - 获取 **项目设置里的引擎模块信息**; + + - **检查缓存** 中的引擎编译选项与当前需要编译的选项是否一致,一致则直接使用缓存; + + - 如需编译,则执行打包引擎的任务,之后拷贝编译后的 js 文件,并保存引擎的缓存判断信息文件; + + 编译引擎时,可以查看 [输出的 log 信息](./build-panel.md#%E6%9E%84%E5%BB%BA-log-%E4%BF%A1%E6%81%AF%E6%9F%A5%E7%9C%8B): + + ![build-engine](./build-guide/build-engine.jpg) + + 关于引擎文件的复用规则,这里有必要阐述下:
+ 打包好的引擎文件将会放置在编辑器全局的临时目录下 (在构建进程内使用 `Build.globalTempDir` 可查询到缓存地址)。缓存文件按照会影响引擎编译的参数生成的 `hash` 值作为名称存放。 + + ```bash + global-temp-folder + |--CocosCreator + |--x.xx(3.0.0) + |--builder + |--engine + |--1dc4a547f9...63a43bb8965.watch-files.json + |--1dc4a547f9...63a43bb8965(引擎文件夹) + |--1dc4a547f9...63a43bb8965.meta(文件夹) + ... + ``` + + 只要任何相关的引擎构建参数发生更改,就会重新编译引擎,具体影响构建引擎缓存使用的有: + + - debug:是否打开调试模式 + - includeModules:引擎模块设置 + - sourceMaps:是否开启 sourceMaps + - platform:构建平台 + - 引擎修改时间 + - 是否勾选分离引擎(仅微信小游戏平台) + - 使用引擎路径、引擎版本号 + +- **json 构建**:序列化 `json` 时会根据 `json` 分组以及所属 `Bundle` 进行合并写入文件系统(`assets/xxxBundle/import` 文件夹内),如果是 **release 模式还会对序列化 json 内的 UUID 进行压缩处理**。 + +- **普通资源拷贝**:一些原始资源(rawAssets)会直接从 `library` 拷贝到构建后的 `assets/xxxBundle/native` 文件夹内。 + +- **md5 处理**:将 `assets` 文件夹内的资源、项目脚本、引擎文件全部加上 `md5` 后缀,并整理数据准备记录在 `settings.json` 内。 + +- **application.js 文件生成**:根据用户选项动态生成 `application.js` 文件到构建后生成的发布包目录的 `src` 目录下。 + +### 整理 settings/config 配置 JSON 数据 + +主要是根据之前资源整理的数据,准备游戏启动必要的配置信息。 + +#### settings.json + +`settings.json` 记录了整个游戏包的基本配置信息,会直接影响到游戏包的运行初始化。 + +关于 `settings.json` 配置的说明如下: + +```js +{ + debug: boolean; // 是否为调试模式,取自构建发布面板 + designResolution: ISettingsDesignResolution; // Canvas 分辨率设置,取自项目设置中的数据 + jsList: string[]; // 插件脚本信息 + launchScene: string; // 初始场景 url + moduleIds: string[]; // 所有用户脚本组件的信息 + platform: string; + renderPipeline: string;// renderPipeline 信息,取自项目设置 + physics?: IPhysicsConfig;// 物理模块设置(仅在勾选了物理引擎模块时生成) + BundleVers: Record; // Bundle 的 md5 值 + subpackages: string[]; // 分包信息 + remoteBundles: string[]; // 记录远程包 Bundle 的集合 + server: string; // 记录远程服务器地址(注意:v3.4 之前该项存放在 ‘application.js’ 文件中) + hasResourcesBundle: boolean; // 是否含有 resources 内置 Bundle + hasStartSceneBundle: boolean; // 是否含有初始场景内置 Bundle + customJointTextureLayouts?: ICustomJointTextureLayout[]; + macros?: Record; // 项目设置面板中的引擎宏配置值 +} +``` + +#### config.json + +每个 Bundle 资源包都有一个 `config.json`,里面记录了整个 `Bundle` 的资源、脚本等基本信息,会直接影响到 `Bundle` 包的加载。 + +关于 `config.json` 配置的说明如下: + +```js +{ + importBase: string; // Bundle 中 import 目录的名称,通常是 'import' + nativeBase: string; // native 中 native 目录的名称,通常是 'native' + name: string; // Bundle 的名称,可以通过 Bundle 名称加载 Bundle + deps: string[]; // 该 Bundle 依赖的其他 Bundle 名称 + scenes: Array<{url: string, uuid: string}>; // Bundle 内包含的场景信息数组 + rawAssets: { [index: string]: { [uuid: string]: string[] } }; + // 存储 resources 下加载的资源 url 与类型 + // 示例: "bba00d3a-2f17-4511-b47c-0d584b21b763@6c48a": ["test/right/texture", "cc.Texture2D", "bba0...@6c48a"] + // "bba0...@6c48a": ["test/right/texture", 1, 1] + packs: Record; // json 分组信息 + versions: { + import: Array; + native: Array; + }; // 勾选 md5Cache 后才有,数组部分以 [uuid_1, md5_1, uuid_2, md5_2, ...] 的格式存储,其中 uuid_1 如果是个简单数字说明存储的是 uuids 数组内的 uuid 索引 + uuids: string[]; // uuid 数组,仅 release 模式下 + types?: string[]; // 资源类型数组,仅 release 模式下 + encrypted?: boolean; // 原生上使用,标记该 Bundle 中的脚本是否加密 + isZip?: boolean; // 是否为 ZIP 模式 + zipVersion?: string; // ZIP 包的 MD5 Hash 值 +} +``` + +这里的结构仅列举了通用流程下 `settings.json`/`config.json` 的结构,实际上不同的平台构建后这些参数会有所差异。 + +### 压缩 config.json + +在生成 `config.json` 之前会根据是否为 release 模式来压缩文件中的 `UUID` 信息,了解这个规则对查找资源构建后的文件位置会有帮助。 + +构建时会对 `Bundle` 内使用到的 `UUID` 进行整理,出现 **两次及以上** 的会存储到 `uuids` 数组中,并将之前使用到的 `UUID` 的位置替换为索引。 + +所有出现 **两次及以上** 的 `types` 也会存储到 `types` 数组中,并将之前使用到的位置替换为索引。 + +#### 构建资源 + +该步骤是生成除了脚本以外的其他项目资源文件,因为脚本是作为特殊文件另外编译处理的。资源在打包过程中执行反序列化后会重新压缩序列化,以减小打包之后的包体。会根据之前数据整理的 JSON 分组信息将多个序列化 JSON 合并单个,例如 `texture` 资源的序列化文件会全部打包成一个 `json` 文件。 + +## 各平台构建处理 + +构建提供了构建生命周期的钩子函数,方便开发者在构建的不同处理时期参与构建流程,影响构建结果。同时构建也支持开发者添加自定义构建选项的配置,通过简单配置可以直接在 **构建发布** 面板显示对应的新增参数,详情请参考 [自定义构建流程](custom-build-plugin.md)。构建扩展注入的构建选项将会存放在 `options.packages[pkgName]` 内,因而目前通过命令行构建的选项参数也需要遵循此规则,对应参数的规则可通过点击 **构建发布** 面板右上方的 **导出构建配置** 进行参考。 + +### 各平台的编译/生成流程 + +自 Cocos Creator 3.0 起,所有需要支持单独编译、生成的平台的构建流程都已经拆分出来,可能会有部分开发者疑惑现今的小游戏平台为何新增了 **生成** 按钮,事实上之前这部分逻辑也一直存在,只不过合并在 **构建** 中,无法进行单独控制。 + +编辑器的 **构建** 类似于一个 **导出对应平台游戏包** 的功能,主要是完成引擎对各个平台的接口、以及游戏包基本格式兼容,并不代表完成全部工作。各个平台通常还会有自己的编译流程,例如微信小游戏平台自带的开发者工具的编译上传功能,以及各个原生平台相关 IDE 的编译运行调试功能。如果开发者需要针对特定平台进行定制化打包处理,就需要编辑器先支持流程上的拆分才能更好地接入。 + +## 常见问题指南 + +构建的整个进程是在一个单独的 worker 内的,所以如果想要查看构建过程的日志信息或者查看出现报错时完整的调用栈,可以点击主菜单的 **开发者 -> 打开构建调试工具** 查看。构建时其实会输出很多的日志信息,但是为了不干扰用户,默认只有错误、警告和一些重要的日志信息会被打印到编辑器的 **控制台** 面板,调试工具中的日志信息才是最完整的。 + +> **注意**:在构建之前 **请先确保参与构建的场景是可以正常预览的**。一些场景的资源丢失或者其他脚本问题,是在预览阶段便能暴露出来的。在保证预览正常的情况下构建能更好地节约时间以及排查问题。 + +### 资源加载 404 + +这种情况下,请复制报错资源丢失的日志中的 UUID 到 **资源管理器** 中查找对应的资源,查看该资源依赖的资源是否都正常。资源加载 404 通常有以下几种情况: + +1. **在脚本内动态加载了没有放在 Bundle 中的资源**。 + + - **原因**:通过上面的介绍,我们知道只有在 Bundle 目录下的资源及其依赖资源,以及参与构建场景的资源及其依赖资源才会被打包到最终的构建文件夹中,并且 **只有直接放进 `Bundle` 文件夹中的资源 url 才会写入到 config.json**。所以如果在脚本中使用了某个资源但这个资源没有放在任何 Bundle 目录下,加载的时候便会出现 404 了。 + + - **解决方案**:将使用到的资源移动在 Bundle 文件夹下。 + +2. **加载的资源导入有问题,未能正常生成数据到 library 中** + + - **原因**:构建时所有的原始数据都是通过读取 `library` 中的资源文件获得的,如果导入失败将无法获取到正确对应的资源信息。 + + - **解决方案**:通过 **资源管理器** 找到对应资源,点击右键,选择菜单中的 **重新导入资源**。 + +3. **资源丢失** + + - **原因**:在前面的构建流程中介绍过,**资源的构建会经过反序列查找依赖资源**,而最经常出现问题的就是所依赖的资源在项目迭代过程中被不小心删除而导致资源丢失。这些资源的丢失可能平时并没有注意到,但一旦执行构建便会暴露出来。 + + - **解决方案**:通过代码编辑器查找该 `UUID` 被哪些资源所引用,修改对应资源。 + +### 脚本资源加载报错 + +在前面介绍的 **构建数据整理** 部分内容时有提到过,构建时需要配置脚本环境。如果报错信息与脚本相关,请参考报错内容对脚本进行修改。如果不清楚是哪个脚本的报错,可以在报错信息调用栈中找到对应脚本的 `UUID`,然后在 **资源管理器** 中查找定位。 + +### 如何查找到小图自动合图后的大图 + +自动图集在构建过程中会打印出原始小图与合成的大图的 `UUID` 信息,在构建调试工具中便可以查找到,然后用查找到的合成的大图的 `UUID` 在打包后生成的 `XXXBundle/native` 目录中查看即可。如果合图太多,可以打开构建 `log` 文件用搜索 `UUID` 的方式查找。 + +![build-atlas](./build-guide/build-atlas.jpg) + +### 如何解压缩 UUID + +在 **release** 模式下打包出来的资源 JSON 文件以及 `config.json` 中的 `UUID` 都是压缩后的,需要将其解压才能找到原项目中的对应资源。构建进程中内置了一些工具方法在全局变量 `Build` 上,直接点击主菜单中的 **开发者 -> 构建调试工具**,在控制台里输入以下命令即可查询到原始 `UUID`: + +```js +Build.Utils.decompressUuid('425o80X19KipOK7J1f5hsN'); +// 42e68f34-5f5f-4a8a-938a-ec9d5fe61b0d +``` + + + +### 引擎编译失败 + +如果引擎编译失败,请检查安装包是否完整、修改的内置引擎代码是否正确,以及若使用了自定义引擎,路径是否正确等等。 + +### 使用自定义引擎并重新打包后,Android Studio 依然使用默认的引擎文件 + +需要在 Android Studio 中打开 Build 菜单并点击 Refresh Linked C++ Projects 按钮,之后就可以使用切换引擎目录后的代码文件。 + +### 使用自定义引擎并重新打包后,DevEco Studio 依然使用默认的引擎文件 + +需要在 DevEco Studio 中打开 File 菜单并点击 Sync and Refresh Project 按钮,之后就可以使用切换引擎目录后的代码文件。 + +### 其他报错 + +如果遇到的错误无法自行解决,请附上 Creator 版本、构建选项配置、构建任务中的构建日志文件以及可复现问题的 Demo 到 [论坛](https://forum.cocos.org/c/58) 反馈。 diff --git a/versions/4.0/zh/editor/publish/build-guide/build-atlas.jpg b/versions/4.0/zh/editor/publish/build-guide/build-atlas.jpg new file mode 100644 index 0000000000..204ab96a23 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-guide/build-atlas.jpg differ diff --git a/versions/4.0/zh/editor/publish/build-guide/build-engine.jpg b/versions/4.0/zh/editor/publish/build-guide/build-engine.jpg new file mode 100644 index 0000000000..f60f377f3b Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-guide/build-engine.jpg differ diff --git a/versions/4.0/zh/editor/publish/build-guide/bundle.png b/versions/4.0/zh/editor/publish/build-guide/bundle.png new file mode 100644 index 0000000000..a0591ef16e Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-guide/bundle.png differ diff --git a/versions/4.0/zh/editor/publish/build-guide/web.png b/versions/4.0/zh/editor/publish/build-guide/web.png new file mode 100644 index 0000000000..83d9c4e310 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-guide/web.png differ diff --git a/versions/4.0/zh/editor/publish/build-open-data-context.md b/versions/4.0/zh/editor/publish/build-open-data-context.md new file mode 100644 index 0000000000..d055876e15 --- /dev/null +++ b/versions/4.0/zh/editor/publish/build-open-data-context.md @@ -0,0 +1,127 @@ +# 开放数据域 + +目前,**微信**、**抖音**、**百度**等小游戏平台为了保护其社交关系链数据,增加了 **开放数据域** 的概念,这是一个单独的游戏执行环境。开放数据域中的资源、引擎、程序,都和主游戏(主域)完全隔离,开发者只有在开放数据域中才能通过平台提供的开放接口来访问关系链数据,用于实现一些例如排行榜的功能。 + +开放数据域目前只支持 Canvas 渲染,在 Cocos Creator 3.0 中,我们废弃了之前的 Canvas Renderer 模块,使用一个基于 **XML** + **CSS** 设计研发的轻量级 Canvas 前端引擎来替代。并且将该引擎整合进了 Creator 3.0 内置的开放数据域工程模板中,开发者只需要掌握一些基本的前端技能,就能在该模板的基础上定制排行榜功能。 + +## SubContextView 组件说明 + +由于开放数据域只能在离屏画布 sharedCanvas 上渲染,所以在项目中,需要有一个节点作为渲染开放数据域的容器,并在该节点上添加 **SubContextView** 组件,该组件会将 sharedCanvas 渲染到容器节点上。 + +SubContextView 组件主要包含 **设计分辨率** 和 **FPS** 两个属性。 + +![SubContextView](./build-open-data-context/sub-context-view.png) + +### 设计分辨率 + +若将 **SubContextView** 组件的 **设计分辨率** 设置为 **640 * 960**,则在组件加载完成阶段,sharedCanvas 的尺寸会被设置为 **640 * 960**。意味着构建之后,开放数据域工程都是在一张 **640 * 960** 的离屏画布上做渲染的。那么,在定制开放数据域(参考下文)时,`style.js` 中标签的样式最大尺寸为 **640 * 960**,否则渲染的内容会超出画布。例如: + +```js +// style.js +export default { + container: { + width: 640, // max width + height: 960, // max height + }, +} +``` + +同时为了避免数据耦合,标签的样式还支持设置宽高的百分比: + +```js +// style.js +export default { + container: { + width: '100%', + height: '100%', + }, +} +``` + +在实际渲染过程中,引擎会采用 **SHOW ALL** 的适配策略将 sharedCanvas 渲染到 **SubContextView** 组件节点上,避免渲染时因拉伸导致的 UI 变形。例如在以下两张图片里,我们使用了不同尺寸的 **SubContextView** 组件节点,开放数据域的贴图都不会被拉伸: + +![adaption](./build-open-data-context/adaption-1.png) + +![adaption](./build-open-data-context/adaption-2.png) + +### 设置 FPS + +**FPS** 属性主要用于设置主域更新 sharedCanvas 到 **SubContextView** 组件上的频率,避免因频繁更新开放数据域贴图造成的性能损耗。 + +## 发布流程 + +1. 打开项目并双击场景,然后在需要渲染开放数据域的节点上添加 **SubContextView** 组件。 + +2. 场景设置完成后保存场景,然后在 **菜单栏 -> 项目** 中打开 **构建发布** 面板,选择需要发布的 **微信** / **抖音小游戏** / **百度** 平台,勾选 **生成开放数据域工程模版**,然后点击 **构建**。 + + ![generate template](./build-open-data-context/generate-template.png) + +3. 构建完成后点击 **构建任务** 左下角的文件夹图标按钮,可以看到在对应小游戏平台的发布包目录下生成了 **openDataContext** 文件夹(例如 `build/wechatgame/openDataContext`),该文件夹就是 Cocos Creator 内置的开放数据域工程模版。 + + ![build output](./build-open-data-context/build-output.png) + + 开发者可以基于这个模板定制所需的开放数据域内容,定制方法参考下文 **定制开放数据域工程** 部分的内容。当再次构建时,如果发布包目录下存在 **openDataContext** 目录,则会直接跳过,开发者不用担心定制的开放数据域内容被覆盖掉。 + +4. 使用对应平台方的开发者工具打开构建生成的发布包(例如 `build/wechatgame`),即可打开小游戏项目查看开放数据域内容,以及预览调试游戏。 + + ![show in devtool](./build-open-data-context/show-in-devtool.png) + + > **注意**:由于百度小游戏的开放数据域只支持加载百度返回的玩家头像,所以构建后生成的开放数据域模版工程可能会因此无法正常加载本地的头像贴图。 + +## 定制开放数据域工程 + +在定制开放数据域工程之前,开发者需要先了解一些基础信息: +- [minigame-canvas-engine 快速入门](https://wechat-miniprogram.github.io/minigame-canvas-engine/overview/guide.html) +- [doT 模版引擎使用](http://olado.github.io/doT/?spm=a2c6h.12873639.0.0.36f45227oKu0XO) + +对这些基础信息有了大致的了解之后,我们来看一下构建后默认生成的开放数据域模版,目录结构如下: + +![folder structure](./build-open-data-context/folder-structure.png) + +- **render/dataDemo.js**:模拟随机的排行榜数据,开发者可以在这里请求平台方的关系链数据,并传给 **doT 模版引擎** 生成相应的 XML 文本 +- **render/style.js**:记录 CSS 样式文本信息,可参考 [样式文档](https://wechat-miniprogram.github.io/minigame-canvas-engine/api/style.html#%E5%B8%83%E5%B1%80) +- **render/template.js**:记录 XML 文本信息,默认使用 **doT 模版引擎** 生成 XML 文本。可参考 [标签文档](https://wechat-miniprogram.github.io/minigame-canvas-engine/api/tags.html#%E6%A0%87%E7%AD%BE%E5%88%97%E8%A1%A8) +- **render/avatar.png**:开放数据域模板默认使用的头像贴图,可删除 +- **engine.js**:小游戏 Canvas 引擎源码 +- **index.js**:开放数据域工程入口文件,在该文件中通过将 XML 文本和 CSS 样式传递给 Canvas 引擎,即可渲染开放数据域 + +## 推荐做法 + +1. 由于项目构建后生成的 build 目录默认会被 git 排除在版本控制外,所以如果开发者希望将定制后的开放数据域纳入版本控制,可以将 `openDataContext` 文件夹(例如 `build/wechatgame/openDataContext`)放入项目的 `build-templates` 目录中,具体可参考 [定制项目构建流程](./custom-project-build-template.md) + +2. 在开放数据域工程中,如果需要监听来自主域的消息,则需要先判断消息是否来自主域引擎。 + +3. 微信接口示例: + + ```js + wx.onMessage(res => { + if (!(res && res.type === 'engine')) { + console.log('do something...'); + } + }); + ``` + + 详情请参考文档:[微信小游戏-开放数据域基础能力](https://developers.weixin.qq.com/minigame/dev/guide/open-ability/opendata/basic.html) + + 当主域向开放数据域发送消息时,建议附带上 type 信息以避免处理错误的消息源。例如上述代码中的 `res.type === 'engine'` 表示消息来源于主域引擎。 +4. 抖音小游戏示例: + + ```js + tt.onMessage(data => { + if (!(data && data.type === 'engine')) { + console.log('do something...'); + } + }); + ``` + + 详情请参考文档:[抖音小游戏-开放数据域基础能力](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/open-ability/relationship-chain/open-data-base) + +> 其他平台的使用类似,具体细节请参考相关平台文档。 + +## 参考链接 + +- **Cocos Creator 小游戏开放数据域范例工程**:[GitHub](https://github.com/cocos/cocos-example-open-data-context) | [Gitee](https://gitee.com/mirrors_cocos-creator/OpenDataContext_TestCase) + +- [小游戏 Canvas 引擎简介](https://wechat-miniprogram.github.io/minigame-canvas-engine/) +- [小游戏 Canvas 引擎源码](https://github.com/wechat-miniprogram/minigame-canvas-engine) +- [doT 模版引擎](http://olado.github.io/doT/?spm=a2c6h.12873639.0.0.36f45227oKu0XO) \ No newline at end of file diff --git a/versions/4.0/zh/editor/publish/build-open-data-context/adaption-1.png b/versions/4.0/zh/editor/publish/build-open-data-context/adaption-1.png new file mode 100644 index 0000000000..1bb192df80 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-open-data-context/adaption-1.png differ diff --git a/versions/4.0/zh/editor/publish/build-open-data-context/adaption-2.png b/versions/4.0/zh/editor/publish/build-open-data-context/adaption-2.png new file mode 100644 index 0000000000..4883df8447 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-open-data-context/adaption-2.png differ diff --git a/versions/4.0/zh/editor/publish/build-open-data-context/build-output.png b/versions/4.0/zh/editor/publish/build-open-data-context/build-output.png new file mode 100644 index 0000000000..c64328ddcb Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-open-data-context/build-output.png differ diff --git a/versions/4.0/zh/editor/publish/build-open-data-context/folder-structure.png b/versions/4.0/zh/editor/publish/build-open-data-context/folder-structure.png new file mode 100644 index 0000000000..def7574cfb Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-open-data-context/folder-structure.png differ diff --git a/versions/4.0/zh/editor/publish/build-open-data-context/generate-template.png b/versions/4.0/zh/editor/publish/build-open-data-context/generate-template.png new file mode 100644 index 0000000000..8899376e1d Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-open-data-context/generate-template.png differ diff --git a/versions/4.0/zh/editor/publish/build-open-data-context/show-in-devtool.png b/versions/4.0/zh/editor/publish/build-open-data-context/show-in-devtool.png new file mode 100644 index 0000000000..41f98ae9c4 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-open-data-context/show-in-devtool.png differ diff --git a/versions/4.0/zh/editor/publish/build-open-data-context/sub-context-view.png b/versions/4.0/zh/editor/publish/build-open-data-context/sub-context-view.png new file mode 100644 index 0000000000..e08694bff2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-open-data-context/sub-context-view.png differ diff --git a/versions/4.0/zh/editor/publish/build-options.md b/versions/4.0/zh/editor/publish/build-options.md new file mode 100644 index 0000000000..29947f5dce --- /dev/null +++ b/versions/4.0/zh/editor/publish/build-options.md @@ -0,0 +1,199 @@ +# 构建选项介绍 + +## 通用构建选项 + +**构建发布** 面板中的通用构建参数默认是按照平台区分的,如下: + +![build options](./build-options/options.png) + +### 发布路径 + +发布路径中包含两个输入框: + +![build path](./build-options/build-path.png) + +- 第一个输入框用于指定项目的发布路径,可直接在输入框输入路径或者通过旁边的放大镜按钮选择路径。从 v3.1 开始支持切换使用以下两种路径: + + ![path](./build-options/path.png) + + - **file**:指定的发布路径为 **绝对路径**,也就是之前版本使用的方式。 + - **project**:指定的发布路径为 **相对路径**,选择的路径只能在项目目录下。使用该种路径时,构建选项里一些与路径相关的(例如 icon 图标)配置便会以相对路径的方式记录,便于团队成员在不同设备上共享配置。 + + 默认的发布路径为项目目录的 `build` 文件夹下,如果您使用 Git、SVN 等版本控制系统,可以将 `build` 文件夹在版本控制中忽略。 + + > **注意**:发布路径中不允许包含空格、非法字符以及中文。 + +- 第二个输入框用于指定项目构建时的构建任务名称以及构建后生成的发布包名称。默认为当前构建平台名称,同个平台每多构建一次,便会在原来的基础上加上 **-001** 的后缀,以此类推。构建完成后可直接点击输入框后面的文件夹图标打开项目发布包所在目录。 + +### 初始场景 + +设置打开游戏后进入的第一个场景。可以在 **参与构建的场景** 列表中搜索所需的场景,将鼠标移动到所需场景栏,然后点击右侧出现的按钮,即可将其设置为初始场景。 + +![start scene](./build-options/set-start-scene.png) + +### 参与构建场景 + +在构建过程中,除了项目目录下的 `resources` 文件夹以及 bundle 中的资源和脚本会全部打包外,其他资源都是根据参与构建的场景以及 bundle 中的资源引用情况来按需打包的。因而去除勾选不需要发布的场景,可以减少构建后生成的项目发布包包体体积。 + +### Bundles + +自 v3.8 起,开发者可以根据项目的需求,来决定某个 Bundle 是否要参与到构建中。 + +![bundle-option](./build-options/bundle-option.png) + +- 全选:所有的 Bundle 都会参与构建。 +- 取消后可以从下拉框内选择需要参与构建的 Bundle。 + + ![bundle-select.png](./build-options/bundle-select.png) + + > 正常构建模式下,主包和引擎内置 internal 包不可取消。 + +### 在 Bundle 中嵌入公共脚本 + +![embed-script-in-bundle.png](./build-options/embed-script-in-bundle.png) + +是否在构建 Bundle 时嵌入公共脚本。 + +> 该选项目前无法修改,只会随着构建面板上的 **正常构建/仅构建 Bundle** 切换而切换。 + +- 未勾选时: + 在构建 Bundle 时,会将不同 Bundle 之间公用的一些 helper 之类的内容生成在 src/chunk 内的 bundle.js 内,减少整体脚本的体积。但这样构建出来的 Bundle 是和项目相耦合的,无法跨项目复用。 +- 勾选时: + 不再提取 Bundle 依赖的公共 JS 库内而是直接构建在 Bundle 的内部。这样的 Bundle 可以跨项目使用(因为所需的脚本都在 Bundle 的内部,而引用相同代码的 Bundle 可能会有重复的部分),缺陷是由于脚本资源都在 Bundle 内部,因此最终的 Bundle 体积会增大。 + +### MD5 缓存 + +为构建后的所有资源文件名加上 MD5 信息,可以解决 CDN 或者浏览器资源缓存问题。 + +启用后,如果出现资源加载不了的情况,说明找不到重命名后的新文件。这通常是因为有些第三方资源没通过 `assetManager` 加载引起的。这时可以在加载前先用以下方法转换 URL,转换后的路径就能正确加载。 + +```typescript +const uuid = assetManager.utils.getUuidFromURL(url); +url = assetManager.utils.getUrlWithUuid(uuid); +``` + +> **注意**:原生平台启用 MD5 Cache 后,如果出现资源加载不了的情况,通常是因为有些 C++ 中用到的第三方资源没通过 `assetManager` 加载引起的。也可以通过以下代码转换 URL 来解决: +> +> ```cpp +> auto cx = ScriptingCore::getInstance()->getGlobalContext(); +> JS::RootedValue returnParam(cx); +> ScriptingCore::getInstance()->evalString("cc.assetManager.utils.getUrlWithUuid(cc.assetManager.utils.getUuidFromURL('url'))", &returnParam); +> +> string url; +> jsval_to_string(cx, returnParam, &url); +> ``` + +### 主包压缩类型 + +设置主包的压缩类型,具体内容可参考文档 [Asset Bundle — 压缩类型](../../asset/bundle.md#%E5%8E%8B%E7%BC%A9%E7%B1%BB%E5%9E%8B)。 + +### 配置主包为远程包 + +该项为可选项,需要与 **资源服务器地址** 选项配合使用。 +勾选后,主包会配置为远程包,并且与其相关依赖资源一起被构建到发布包目录 remote 下的 [内置 Asset Bundle — main](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中。开发者需要将整个 remote 文件夹上传到远程服务器。 + +### 资源服务器地址 + +填写后,将会在对应的远程服务器上下载配置了远程包的 bundle。 + +### 使用构建内置服务器(实验室功能) + +勾选后,无需再填写服务器地址,构建将会自动填充内置服务器地址,可用于快速验证 bundle 的 remote 功能。 +需要注意的是,**此选项仅作为开发测试使用,不可用于生产环境**。构建内置服务器是本地搭建的 HTTP1 服务器,一些需要 HTTPS 服务器的游戏平台请关掉开发者工具域名校验或者不使用此功能自行搭建服务器,同时手机连接请保持和电脑在同一局域网下。 + +### 调试模式 + +若不勾选该项,则处于发布(release)模式,会对资源的 UUID、构建出来的引擎脚本和项目脚本进行压缩和混淆,并且对同类资源的 json 做分包处理,减少资源加载的次数。 + +若勾选该项,则处于调试模式,同时也可以配合勾选 Source Maps 选项,对项目进行调试,更方便定位问题。 + +### 压缩引擎内部属性 +此功能从 v3.8.6 开始支持。开启后会对引擎 TS 代码的内部属性进行压缩,能够有效减少代码体积。当项目构建后会在根目录生成 `engine-mangle-config.json` 配置文件,用户可以在其中自定义添加压缩属性。此功能暂不支持原生平台。若项目启用分离引擎插件功能,那么构建时将忽略此配置。详细使用方法请参考 [压缩引擎内部属性](../../advanced-topics/mangle-properties.md) + +### 内联枚举 +此功能从 v3.8.6 开始支持。开启后会把引擎 TS 代码中的枚举值替换为具体的数值,也会禁用枚举双端映射( Reverse-Mapping )功能,能够有效减少代码体积。如果你需要为某些枚举生成双端映射,那么可以用 'cc.Enum(your_enum)' 函数修饰对应的枚举动态地生成双端映射。若项目启用分离引擎插件功能,那么构建时将忽略此配置。 + +### Source Maps + +如果需要生成 sourcemap,请勾选该项。构建时便会默认压缩引擎文件和项目脚本。 + +由于 JavaScript 脚本正变得越来越复杂,大部分源码(开发代码)都要经过编译转换才能投入生产环境,这就使实际运行的代码不同于源码,导致调试时无法定位到源代码。而 Source Map 可以将已转换的代码映射到源码,也就是将转换后的代码对应到转换前的源码的位置。这样在出现问题时,便可以直接查看和调试源码,定位问题更容易。详情可参考 [使用 source map](https://developer.chrome.com/docs/devtools/javascript/source-maps/)。 + +### 替换插屏 + +鼠标移动到该选项时便会出现 **编辑图标** 的按钮,点击该按钮打开插屏设置面板,数据编辑后将会实时保存。可手动指定插屏图片路径,或者直接将文件系统的图片拖拽到图片处替换。 + +![splash setting](build-options/splash-setting.png) + +插屏的详细设置可以参考 [项目设置 - 插屏设置](../project/index.md)。 + + + +### 擦除模块结构(实验性质) + +若勾选该项,脚本导入速度更快,但无法使用模块特性,例如 `import.meta`、`import()` 等。 + +### 跳过纹理压缩流程 + +默认为 `false`,若勾选该选项,则构建时会跳过整个纹理压缩流程,以减少构建时间。构建命令为:`skipCompressTexture`。 + + + +### 引擎模块配置 + +自 3.8.3 起,允许在构建面板上配置部分引擎模块配置,方便针对不同的平台或构建任务选择不同的物理后端等。 + +![Engine Config](build-options/engine-config.png) + +目前支持以下配置的修改: + +| 配置项 | 可选项 | 说明| 默认值 | +| --- | --- | --- | --- | +| CLEANUP_IMAGE_CACHE | `与项目设置一致`,`开启`,`关闭` | 是否在将贴图上传至 GPU 之后删除原始图片缓存,删除之后图片将无法进行 [动态合图] |与项目设置一致| +| 2D 物理系统 | `与项目设置一致`,`Box 2D`,`内置` | - |与项目设置一致| +| 3D 物理系统 | `与项目设置一致`,`Bullet`,`cannon`,`PhysX`,`内置` | - |与项目设置一致| +| WebGL 2.0 | `与项目设置一致`,`开启`,`关闭` | - | 与项目设置一致| +| 原生代码打包模式 | `Wasm + AsmJS`,`Wasm`,`AsmJS` | 主要影响物理、Spine 等原生模块打包格式,其中 Wasm 模式能够在一定程度上提高游戏的性能。由于不同游戏平台的支持程度不一致,请根据运行环境官方的实际支持数据来判断选择。 | Wasm + AsmJS| +| 启用 Wasm Brotli 压缩 |`开启`、`关闭` | 目前仅抖音平台支持,启用后将会减小 wasm 包体,由于需要运行时解压,加载 wasm 的时间会略微增加 | 关闭 | + +以上选项会根据项目设置实际模块的选择情况显示或隐藏,如:`项目设置如果没有勾选 2D 物理模块,则不会出现对应的选项`。 + +### Cocos Service 配置集 + +该项用于显示当前项目在 [服务](https://service.cocos.com/document/zh/) 面板所集成的所有服务。 + +## 各平台相关构建选项 + +由于目前构建机制上的调整,不同平台的处理均以扩展的形式注入 **构建发布** 面板。在 **构建发布** 面板的 **发布平台** 中选择要构建的平台后,将会看到对应平台的展开选项,展开选项的名称便是平台扩展名,在编辑器主菜单的 **扩展 -> 扩展管理器 -> 内置** 中可以看到各平台扩展。 + +各平台相关构建选项,详情请参考: + +- [发布到 Web 平台](publish-web.md) +- [发布到小游戏平台](publish-mini-game.md) +- [发布到 Facebook Instant Games](publish-fb-instant-games.md) +- [发布到原生平台](native-options.md#%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) +- [发布到 Google Play](google-play/build-example-google-play.md) +- [发布到 Google Play On Games](google-play-games/index.md) +- [发布到 HarmonyOS 平台](publish-huawei-ohos.md) +- [发布到 HarmonyOS Next 平台](publish-openharmony.md) + +Cocos Creator 支持自定义构建扩展,处理方式与平台扩展一致,详情可参考 [自定义构建流程](custom-build-plugin.md)。 + +## 其他参与构建的参数配置 + +编辑器菜单栏 **项目 -> 项目设置** 中的配置都会影响到项目构建的结果,详情请参考 [项目设置](../project/index.md)。 diff --git a/versions/4.0/zh/editor/publish/build-options/build-path.png b/versions/4.0/zh/editor/publish/build-options/build-path.png new file mode 100644 index 0000000000..5d65899222 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/build-path.png differ diff --git a/versions/4.0/zh/editor/publish/build-options/bundle-option.png b/versions/4.0/zh/editor/publish/build-options/bundle-option.png new file mode 100644 index 0000000000..1cc28e07c3 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/bundle-option.png differ diff --git a/versions/4.0/zh/editor/publish/build-options/bundle-select.png b/versions/4.0/zh/editor/publish/build-options/bundle-select.png new file mode 100644 index 0000000000..5ce18986fa Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/bundle-select.png differ diff --git a/versions/4.0/zh/editor/publish/build-options/embed-script-in-bundle.png b/versions/4.0/zh/editor/publish/build-options/embed-script-in-bundle.png new file mode 100644 index 0000000000..d2885d3322 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/embed-script-in-bundle.png differ diff --git a/versions/4.0/zh/editor/publish/build-options/engine-config.png b/versions/4.0/zh/editor/publish/build-options/engine-config.png new file mode 100644 index 0000000000..1199c7cd77 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/engine-config.png differ diff --git a/versions/4.0/zh/editor/publish/build-options/options.png b/versions/4.0/zh/editor/publish/build-options/options.png new file mode 100644 index 0000000000..f2a3714619 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/options.png differ diff --git a/versions/4.0/zh/editor/publish/build-options/path.png b/versions/4.0/zh/editor/publish/build-options/path.png new file mode 100644 index 0000000000..28dc222608 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/path.png differ diff --git a/versions/4.0/zh/editor/publish/build-options/set-start-scene.png b/versions/4.0/zh/editor/publish/build-options/set-start-scene.png new file mode 100644 index 0000000000..3749b39e45 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/set-start-scene.png differ diff --git a/versions/4.0/zh/editor/publish/build-options/splash-setting.png b/versions/4.0/zh/editor/publish/build-options/splash-setting.png new file mode 100644 index 0000000000..80297e911f Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/splash-setting.png differ diff --git a/versions/4.0/zh/editor/publish/build-options/start_scene.png b/versions/4.0/zh/editor/publish/build-options/start_scene.png new file mode 100644 index 0000000000..0d3583c299 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-options/start_scene.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel.md b/versions/4.0/zh/editor/publish/build-panel.md new file mode 100644 index 0000000000..3b21e2700f --- /dev/null +++ b/versions/4.0/zh/editor/publish/build-panel.md @@ -0,0 +1,132 @@ +# 构建发布面板详解 + +点击编辑器主菜单中的 **项目 -> 构建发布** 或者使用快捷键 `Ctrl/Cmd + Shift + B` 即可打开 **构建发布** 面板: + +![build-panel](./build-panel/build-panel.png) + +若已经构建过某一平台,则打开 **构建发布** 面板会进入 **构建任务** 页面。v3.0 各个平台的构建是以构建任务的形式进行,类似于下载任务: + +![panel](./build-panel/panel.png) + +## 构建发布面板 + +在 **构建发布** 面板选择需要构建的平台,然后配置 [构建选项](build-options.md)。配置完成后,点击右下角的 **构建** 按钮即可跳转到 **构建任务** 页面执行构建流程。或者点击右上角的 **关闭(X)** 按钮也可以进入 **构建任务** 页面。 + +**构建发布** 面板上方有三个功能按钮: + +![build-options](./build-panel/build-options.png) + +- ![doc](./build-panel/doc.png):点击该按钮即可跳转到当前平台的官方手册文档。 + +- **Import**:点击该按钮即可导入保存了构建选项配置的 json 文件。 + +- **Export**:点击该按钮可将当前平台的构建选项配置导出为 json 文件,用于 [命令行构建](publish-in-command-line.md),也可以在项目成员之间共享构建选项配置。导出的配置是按照平台区分的,并且带有版本号,请不要删除版本号,以免在跨版本导入配置时无法进行数据迁移。使用命令行构建时,将构建参数 `configPath` 的文件路径指定为导出的 json 配置文件路径即可。 + +> **注意**: +> +> 1. 构建没有场景的项目是没有意义的,所以如果当前打开的项目没有场景,则打开 **构建发布** 面板时会提示请先创建场景: +> +> ![save-scene](./build-panel/create-scene-first.png) +> +> 2. 在构建之前,请确保当前的场景已经保存,否则点击 **构建** 按钮时将会弹框提示,可以选择 **保存**、**忽略** 或者 **取消构建**。选择 **保存** 和 **忽略** 都会继续执行构建流程。 +> +> ![save-scene](./build-panel/save-scene.png) + +## 构建任务页面 + +在 **构建任务** 页面可以查看当前平台的构建进度及构建结果。 + +- 正在构建中:进度条显示为 **蓝色**。 +- 构建成功:进度条到达 100%,输出实际构建时间并显示为 **绿色**。 +- 构建失败:进度条到达 100%,提示构建失败原因或者报错信息,并显示为 **红色**。 + +![panel](./build-panel/build-page.png) + +该页面上方有三个按钮,包括 **新建构建任务**、**打开构建调试工具** 和 **清空构建缓存**: + +- **新建构建任务**:点击该按钮即可返回 **构建发布** 面板,选择新的平台进行构建。 + +- ![debug](./build-panel/debug.png):打开构建调试工具,点击该按钮即可打开构建调试工具,查看在构建过程中产生的全部日志信息包括调用栈。 + +- ![clean](./build-panel/clean.png):清空构建缓存。为了复用可被重复利用的构建结果,以便重新构建时加快构建速度、降低内存占用等,构建过程中的很多处理都添加了缓存管理机制,例如压缩纹理、自动图集生成、引擎编译、资源序列化 JSON 等。
正常情况下这部分缓存数据是不需要手动清理的,但如果是在特殊情况下需要避免缓存干扰,可以点击该按钮来清空缓存数据。 + + 项目相关的资源缓存会存储在项目目录下,引擎编译相关的缓存则存储在全局目录下,开发者可根据自己的需要选择清空项目缓存、全局缓存,或者全部清空。 + + ![clean window](./build-panel/clean-window.png) + +### 平台构建任务 + +各个平台的构建是以构建任务的形式进行,类似于下载任务。平台构建任务的名称取决于 **构建发布** 面板中的 **发布路径** 选项,具体可查看 [构建选项](build-options.md) 文档。 + +![build task](./build-panel/build-task.png) + +每个构建任务都配置了相应的功能按钮方便使用。 + +**构建任务右上方的移除(X)按钮** 用于移除当前构建任务,可选择 **仅移除构建记录** 或者 **删除源文件**。**删除源文件** 即删除对应平台构建后生成在 `build` 目录下的项目发布包。 + +![remove build task](./build-panel/remove-build-task.png) + +**构建任务左下方的按钮包括**: + +- ![folder](./build-panel/folder.png):点击该按钮即可打开对应平台构建后生成的项目发布包(默认在 `build` 目录下)。 + +- ![editing](./build-panel/editing.png):点击该按钮即可返回 **构建发布** 面板,修改对应平台上一次构建时配置的构建选项,然后点击右下方的 **构建** 按钮重新构建。详情请参考下方 **修改构建选项** 部分的内容。 + +- ![setting](./build-panel/view_build_parameter.png):点击该按钮即可返回 **构建发布** 面板,查看对应平台上次构建时配置的构建选项。 + +- ![log-file](./build-panel/log-file.png):点击该按钮即可打开对应平台在构建过程中产生的日志文件或者日志文件所在目录,可通过 **偏好设置 -> 构建发布 -> 日志文件打开方式** 进行设置。更多内容请参考下方 **构建日志信息查看** 部分的内容。 + +**构建任务右下方的按钮**,主要用于各个平台在构建完成后根据平台要求执行生成、运行、上传等发布流程。**Build** 按钮则是用于重新构建。 + +各个平台完成构建后,与构建相关的构建选项配置信息都会保存在项目目录下的 `profiles/v2/packages/builder.json` 文件中,只要没有在 **构建任务** 页面删除对应平台的构建任务或者删除 `build` 目录下的项目发布包,就可以在重新打开编辑器后查看上次构建时的构建选项配置,以及继续运行预览等。 + +各个平台具体的发布流程,可参考: + +- [发布到原生平台](native-options.md) +- [发布到小游戏平台](publish-mini-game.md) + +### 修改构建选项 + +点击构建任务左下方的编辑按钮,即可返回 **构建发布** 面板修改上次构建时配置的构建选项,以便重新构建。因为只能修改当前平台上一次构建时的构建选项配置,所以页面中的 **发布平台** 项为置灰状态,不可修改。 + +![edit build option](./build-panel/edit-build-option.png) + +当前平台上一次构建时的构建选项配置可点击编辑按钮右侧的 ![setting](./build-panel/view_build_parameter.png) 按钮查看。 + +修改完成后点击 **构建** 按钮就会清空上次构建后生成的项目发布包并重新构建。或者点击 **构建发布** 面板右上方的 **X** 按钮返回 **构建任务** 页面,再点击平台构建任务右下方的 **构建** 按钮也可以重新构建。 + +> **注意**: +> +> 1. 原生平台为了避免误删除已定制的内容,在重新构建时仅更新项目资源,不会覆盖原有的原生工程内容。因此在返回 **构建发布** 面板修改之前配置的构建选项时,原生平台相关的构建选项为 **禁用** 状态。如果需要重新生成工程请新建构建任务。 +> 2. 从 v3.3 开始,原生平台在重新构建时,构建选项 **游戏名称** 为置灰状态,不可修改。 + +如果修改配置后没有点击 **构建** 按钮重新构建,修改的配置也会被保存起来。若当前 **构建发布** 面板中的配置与上次构建后生成的 `build` 目录下的项目发布包中的配置不一致,**构建发布** 面板的上方会显示黄色的 * 号键。 + +![settings](build-panel/settings.png) + +### 构建日志信息查看 + +由于构建过程会产生非常多的日志信息,默认情况下只有错误信息才会打印到编辑器的 **控制台** 面板中。 + +如果需要查看所有的日志信息有以下几种操作方式: + +- **打开构建调试工具** + + 通过点击主菜单中的 **开发者 -> 打开构建调试工具** 或者点击 **构建任务** 页面右上方的 ![debug](./build-panel/debug.png) 按钮,即可查看在构建过程中打印出的全部日志信息包括调用栈。 + +- **打开构建日志记录文件** + + 每次构建过程中产生的报错信息都会被记录存储在项目目录下的 `temp/builder/log` 文件夹中。点击构建任务左下方的 ![log-file](./build-panel/log-file.png) 按钮即可查看。在反馈构建相关问题时,可直接附上该文件以便定位问题。 + +### 构建队列和取消构建 + +![interupt](build-panel/interrupt.gif) + +构建过程中依旧可以添加新的构建任务,此时新的构建任务会加入到构建队列中。 +在构建的过程中,可以点击构建任务右侧的 X 按钮来中断当前的构建任务。 + +### 构建和仅构建 Bundle + +![build-dropdown.png](./build-panel/build-dropdown.png) + +自 v3.8 起,Cocos Creator 支持仅构建 Bundle,仅构建 Bundle,可以一次性构建选中的所有 Bundle,和单独的构建 Bundle 不同,仅构建 Bundle,会根据 Bundle 的优先级,将公共资源放在优先级高的 Bundle 内。 diff --git a/versions/4.0/zh/editor/publish/build-panel/build-dropdown.png b/versions/4.0/zh/editor/publish/build-panel/build-dropdown.png new file mode 100644 index 0000000000..d281ef19b0 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/build-dropdown.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/build-options.png b/versions/4.0/zh/editor/publish/build-panel/build-options.png new file mode 100644 index 0000000000..2b6d7d4032 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/build-options.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/build-page.png b/versions/4.0/zh/editor/publish/build-panel/build-page.png new file mode 100644 index 0000000000..27601c7e69 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/build-page.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/build-panel.png b/versions/4.0/zh/editor/publish/build-panel/build-panel.png new file mode 100644 index 0000000000..bdcdef7e19 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/build-panel.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/build-task.png b/versions/4.0/zh/editor/publish/build-panel/build-task.png new file mode 100644 index 0000000000..b4f9c55041 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/build-task.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/clean-window.png b/versions/4.0/zh/editor/publish/build-panel/clean-window.png new file mode 100644 index 0000000000..c5c1f14847 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/clean-window.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/clean.png b/versions/4.0/zh/editor/publish/build-panel/clean.png new file mode 100644 index 0000000000..dbb3eb80e5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/clean.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/clear-build-data.png b/versions/4.0/zh/editor/publish/build-panel/clear-build-data.png new file mode 100644 index 0000000000..18787d3ee9 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/clear-build-data.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/create-scene-first.png b/versions/4.0/zh/editor/publish/build-panel/create-scene-first.png new file mode 100644 index 0000000000..00fb132de7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/create-scene-first.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/debug.png b/versions/4.0/zh/editor/publish/build-panel/debug.png new file mode 100644 index 0000000000..a79e2c802e Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/debug.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/doc.png b/versions/4.0/zh/editor/publish/build-panel/doc.png new file mode 100644 index 0000000000..93174e837e Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/doc.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/edit-build-option.png b/versions/4.0/zh/editor/publish/build-panel/edit-build-option.png new file mode 100644 index 0000000000..fd2a7a2eb9 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/edit-build-option.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/editing.png b/versions/4.0/zh/editor/publish/build-panel/editing.png new file mode 100644 index 0000000000..d1f1672fd5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/editing.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/folder.png b/versions/4.0/zh/editor/publish/build-panel/folder.png new file mode 100644 index 0000000000..ebd78f5c37 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/folder.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/interrupt.gif b/versions/4.0/zh/editor/publish/build-panel/interrupt.gif new file mode 100644 index 0000000000..53fd0ba02c Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/interrupt.gif differ diff --git a/versions/4.0/zh/editor/publish/build-panel/log-file.png b/versions/4.0/zh/editor/publish/build-panel/log-file.png new file mode 100644 index 0000000000..114b419cfa Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/log-file.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/log.jpg b/versions/4.0/zh/editor/publish/build-panel/log.jpg new file mode 100644 index 0000000000..4a08f8346c Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/log.jpg differ diff --git a/versions/4.0/zh/editor/publish/build-panel/options.png b/versions/4.0/zh/editor/publish/build-panel/options.png new file mode 100644 index 0000000000..4142fb45ba Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/options.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/panel.png b/versions/4.0/zh/editor/publish/build-panel/panel.png new file mode 100644 index 0000000000..d30372f059 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/panel.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/remove-build-task.png b/versions/4.0/zh/editor/publish/build-panel/remove-build-task.png new file mode 100644 index 0000000000..2df63897c9 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/remove-build-task.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/save-scene.png b/versions/4.0/zh/editor/publish/build-panel/save-scene.png new file mode 100644 index 0000000000..b9b16afaeb Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/save-scene.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/setting.png b/versions/4.0/zh/editor/publish/build-panel/setting.png new file mode 100644 index 0000000000..3f1796e726 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/setting.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/settings.png b/versions/4.0/zh/editor/publish/build-panel/settings.png new file mode 100644 index 0000000000..9b358ab52a Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/settings.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/subpackage/subpackage-config.png b/versions/4.0/zh/editor/publish/build-panel/subpackage/subpackage-config.png new file mode 100644 index 0000000000..4e4f2ee0cd Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/subpackage/subpackage-config.png differ diff --git a/versions/4.0/zh/editor/publish/build-panel/view_build_parameter.png b/versions/4.0/zh/editor/publish/build-panel/view_build_parameter.png new file mode 100644 index 0000000000..db83c1cf16 Binary files /dev/null and b/versions/4.0/zh/editor/publish/build-panel/view_build_parameter.png differ diff --git a/versions/4.0/zh/editor/publish/bytedance-ios-optimize.md b/versions/4.0/zh/editor/publish/bytedance-ios-optimize.md new file mode 100644 index 0000000000..77ce25a6cf --- /dev/null +++ b/versions/4.0/zh/editor/publish/bytedance-ios-optimize.md @@ -0,0 +1,176 @@ +# IOS 抖音小游戏内存与性能优化指南 + +## 前言 + +由于 iOS 端的性能表现一直差强人意。 同样的程序版本,与有 JIT 加速的 Android 比起来,相差甚远。抖音小游戏提供的高性能模式,使得在 iOS 上的小游戏,也能拥有 JIT 能力,大幅度提升运行性能。 + +## 性能数据 + +从内测阶段的数据来看,接入高性能模式后,小游戏的 80 分位 FPS 有平均 69.01% 的提升(32.53 -> 54.98)。从原理上来说,高性能模式大幅提升了 JS 代码的运行效率,因此 CPU 压力较大的游戏,在接入高性能模式后会有较为明显的性能提升。 + +> 因为内存问题在 Android 端表现良好,在 iOS 端反馈较多,所以本文仅对 iOS 端内存优化做介绍。 + +### 苹果手机运行内存限制 + +#### 有关高性能模式的介绍以及使用方法请详细阅读抖音小游戏开发文档 [高性能模式](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/performance-optimization/runtime-performance/high-performance-mode)。 + +1. 高性能模式下,游戏将拥有更好的渲染性能和表现,但是它对游戏的内存要求更加严格。 + +2. 在高性能模式下,小游戏运行于浏览器内核环境,所以兼容性、内存消耗、稳定性等方面需要单独进行测试,不能复用普通模式的测试结果。 + +3. 在 iOS 设备中,iphone 6s/7/8 等 2G RAM 机型的内存限制为 1G,iphone 7P/8P/iPhoneX/XR/XSAMX/11 等 3G RAM 机型的内存限制为 1.4G,一旦应用程序的内存占用超过这个阀值,就会被系统杀掉进程。因此开发者务必保证内存峰值不超过该数值。 + +4. 如果游戏没做好内存优化,不建议开启高性能模式,否则在 iOS 低端机容易出现内存异常退出的情况,如有内存问题,可参考本文的内存优化技巧,充分优化内存。 + +#### 如何打开高性能模式。 +1. 在 game.json 中,配置 enableIOSHighPerformanceMode 为 true 即可打开高性能模式。目前局域网预览尚不支持高性能模式,请在预览时不要勾选「开启局域网快速预览」选项。 + + ```json + { + "deviceOrientation": "portrait", + "enableIOSHighPerformanceMode": true + } + ``` +2. 如何确认是否已经打开高性能模式: + * 高性能模式下可通过访问 GameGlobal.isIOSHighPerformanceMode 判断是否为高性能模式,高性能模式下该变量为 true,普通模式下为 undefined。 + * 通过 vConsole 的颜色进行判断,普通模式下 vConsole 为绿色,高性能模式下 vConsole 为蓝色。 + +## 运行时内存结构 + +![2](./optimize/2.png) + +从上图中可以看到,运行时内存一共由 6 个部分组成。 + +### JavaScript Heap + +在高性能模式下,小游戏运行于浏览器内核环境,因此 JavaScript Heap 包含游戏逻辑代码内存。 通常我们可以打包 web-mobile 端,使用 Mac 平台的 Safari 浏览器的调试工具来远程调试手机 safari 的内存情况。 需要注意的是 JavaScript Heap 通常无法看出具体物理内存使用。 + +### WASM 资源 + +为了提高 JS 模块的执行性能,比如物理引擎的计算,我们会将一些 C++ 代码直接编译成 WASM 代码片段来达到优化性能的需求。 比如 Cocos Creator 部分引用的第三方物理库就是 WASM 版本。 + +### 基础库和 Canvas + +基础库可以理解为抖音小游戏的黑盒环境暴露的 API 封装,可以防止将浏览器内核环境 API 暴露给开发者,实际测试基础库内存占用在 65M 左右。小游戏环境第一个创建的 Canvas 是主 Canvas,也是唯一可以将渲染表面同步到主界面的 Canvas,即呈现我们游戏的渲染表现。Canvas 的内存占用跟 Canvas 的宽高大小成正比。 + +### 音频文件 + +音频文件内存是指加载到内存的音频实例。 + +### GPU 资源 + +比如顶点数据缓存,索引数据缓存,纹理缓存和渲染表面缓存等等。 + +## 内存问题诊断 + +下面给大家介绍一些常用的 iOS 内存诊断工具,它们可以辅助我们快速定位内存问题,找出解决办法。 + +### 常用 iOS 设备内存查看工具 + +- Xcode 自带的 Instrument 分析工具 +- Perfdog 工具 +- 抖音开发者工具 + +> **注意**:iOS 端小游戏的进程名称在不同模式下有区别。 +> - 高性能模式:含有 WebContent +> - 普通模式:含有 Aweme + +### XCode Instruments + +![3](optimize/3.png) + +XCode Instruments 是 XCode 自带的应用程序运行时分析工具,它同样适用于抖音小游戏进程。 + +使用 Activity Monitor,选择对应的设备 all processes 捕捉,等进程列表刷新后,输入 webkit 进行过滤,即可看到所有进程的 CPU 与内存情况. + +![4.png](optimize/4.png) + +### Perfdog + +Perfdog(性能狗)是一个 iOS/Android/PC/ 主机平台的性能测试和分析工具,具体使用方式请参考: + +[https://perfdog.qq.com/](https://perfdog.qq.com/) + +选择对应的设置-进程名,即可看到相关性能数据。 + +### 抖音开发者工具 + +* [性能测试工具](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/performance-optimization/performance-testing-tools) + +备注:只能在真机上使用 + +* 主要使用开发者工具自带的调试器来跟踪内存数据,进入调试开发者工具界面,将 【Profiles】勾选.主要有四个功能: +1. Record JavaScript CPU Profile:主要用于查看函数运行的时间 +2. Heap snapshot: 构造函数自身占用内容以及直接或者间接引用对象的大小 +3. Allocation instrumentation on timeline: 主要使用 JS Heap 的峰值曲线,用于观察内存增长变化 +4. Allocation sampling: 可以查看JavaScript执行栈细分的内存分配近似值. + +备注:此四个功能只能在抖音开发者工具的IDE上使用 + +* 抖音开发者工具主要使用它的调试器来跟踪内存数据,操作流程如下: +1. 进入调试开发者工具界面 +2. 将 【Profiles】勾选,然后刷新游戏 +3. 点击下图左上角的小圆圈按钮开始录制 +4. 结束录制后,就会显示下图界面 + +![5](optimize/10.png) + +当我们要确定某块 JS 内存的来源与释放情况,就需要用到下面的内存泄漏检测工具 - **实时内存诊断**。 + +![6](optimize/11.png) + +具体操作步骤如下: + +1. 点击左上角的小圆圈按钮,会进入下面的录制按钮,柱状图的出现表示某个内存块的创建,消失标识内存块被释放。左上角的垃圾桶按钮是主动触发 JS 引擎的 GC 的按钮,点击后可以加快内存回收速度。 + +![7](optimize/12.png) + +1. 再次点击左上角的红色小圆圈按钮结束录制,这时候我们可以选中蓝色区域,然后会显示该内存块包含的对象,这些是在内存中未被释放的资源,选中某个对象后,可以在 Retainers 界面看到对象的内存引用关系。到这里你可以根据代码层的逻辑关系来推理内存对象是否应该被释放,从而确认是否内存泄露。 + +![8](optimize/13.png) + +## 内存优化技巧 + +常见抖音小游戏项目的内存由下面几个部分组成: +1. 小游戏基础库 +2. 引擎脚本内存 +3. 业务脚本内存 +4. 音频内存 +5. 字体内存 +6. 图片内存 +7. Canvas 内存 + +知道了内存组成部分后,我们就可以针对不同的部分做一些优化。 + +以 iOS 为例,常用的内存优化技巧如下: +1. 小游戏通常基础库的内存 ~= 65M,常驻内存,不可优化。 + +2. 引擎内存占用加载是确定的,由于引擎加载会初始化渲染器,所以通常主 Canvas 内存占用也在这个时候确定,这块内存占用可以通过配置渲染分辨率的倍数来优化。运行时根据引擎模块需要,会动态增加一些缓存内存,开发者可以根据功能需要通过编辑器项目设置里面的功能裁剪来减少引擎内存占用。 + +3. 脚本内存包含引擎和业务代码、配置表数据, 根据游戏的开发体量,业务代码和配置表数据内存会有几百 M 的大小,只能用户自己做优化。 + +4. 单个双通道的音频实例可能在 20M 左右,音频播放完后做释放会减少这块内存损耗,也可以精简成单通道音频减少内存。 + +5. 在国内,一般使用的是中文字体,加载后内存占用至少大于 10M,所以尽量使用系统字,使用应用内部的共享资源。如果开发条件允许的情况下,可以使用 Bitmap 字体和 SDF 字体渲染。 + +6. 图片内存是常用资源,根据加载需要,可以选择填充纹理后释放,或者缓存于内存中以便下次重新填充纹理。在iOS端上建议使用 astc 压缩纹理格式,同时禁用动态合批,这样可以释放 image 资源内存。压缩纹理本身也比 png 的内存占用小超过50%,但是 astc 的文件大小会比 png 大,所以会增加包体大小。通常为了减少首包大小,尽量将图片资源放到小游戏分包或者远程分包。 + +7. TTF 字体文本渲染时会创建 Canvas 对象,Canvas 对象使用完会被回收到缓存池中,文本渲染的字号越多, 缓存池就越大,目前引擎没有提供回收机制,必要时可以修改引擎来释放 Canvas 缓存池。如果游戏运行内存占用比较高,可以使用 Bitmap 字体替代 TTF 字体。 + +8. 还有其他的 JS 内存对象,比如 JSON 文件的释放,根据引擎提供的能力按需释放。 + +9. 合理使用对象池,建议设置一个最大数量阈值避免创建过多的对象。 + +10. 按需勾选引擎模块,如果有必要可以定制引擎深入优化引擎内存。 + +11. 3D 游戏尽量简化模型。 + +## 参考文章 + +1. [抖音小程序性能优化](https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/tutorial/experience-optimization/overview) + +2. [抖音启动场景上报分析](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/performance-optimization/startup-performance/startup-scene-analysis) + +3. [普通小游戏高性能模式](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/performance-optimization/runtime-performance/high-performance-mode) + +4. [抖音IDE使用技巧](https://developer.open-douyin.com/school/course/243) \ No newline at end of file diff --git a/versions/4.0/zh/editor/publish/custom-build-plugin.md b/versions/4.0/zh/editor/publish/custom-build-plugin.md new file mode 100644 index 0000000000..8b64572cef --- /dev/null +++ b/versions/4.0/zh/editor/publish/custom-build-plugin.md @@ -0,0 +1,533 @@ +# 自定义构建流程 + +对构建流程进行自定义的前提是需要对构建发布的整体流程有所了解,不熟悉的开发者建议先阅读 [构建流程简介与常见问题指南](./build-guide.md)。 + +为了能更好的理解本篇文档内容,在继续阅读本文档之前,推荐大家先阅读 Cocos Creator [扩展编辑器](../extension/readme.md) 文档,了解扩展开发相关知识。 + +为方便书写,本文中我们约定将用于自定义构建流程的扩展简称为: **构建扩展** + +## 构建扩展模板 + +Cocos Creator 提供了快捷方式生成 **构建扩展模板**:点击 **项目** -> **新建构建扩展包** 菜单即可生成 `cocos-build-template`,如下图所示: + +![cocos-build-template-menu](./custom-build-plugin/cocos-build-template-menu.png) + +> **注意**:生成此扩展模板时会选择其作用于 **项目** 还是 **全局**,建议选择 **项目**,除非真的需要将此扩展作用于所有项目。 + +下面我们将重现此扩展包的制作过程,并逐一讲解细节。 + +## 创建扩展 + +对扩展的创建和开发不太熟悉的开发者可参考文档 [入门示例](../extension/first.md),创建一个名为 `custom-build-example` 的扩展。 + +> **注意**:为了方便与 `cocos-build-template` 对比学习,我们使用另一个不同的名字。 + +## 扩展包定义与自定义构建入口脚本 + +在 `package.json` 中添加 `contributions.builder` 字段,此字段指向一个 `JavaScript` 脚本的 **相对路径**,作为构建流程的入口脚本(以下简称**自定义构建脚本**),如下所示: + +```json5 +// package.json +{ + "contributions": { + "builder": "./dist/builder.js" + } +} +``` + +> **注意**:`./dist/builder.js` 是由 `./src/builder.ts` 编译后生成的脚本。 + +## 自定义构建脚本结构 + +`builder.ts` 完整结构示例如下: + +```ts + +import { BuildPlugin, IBuildTaskOption } from "../@types/packages/builder/@types"; + +export const load: BuildPlugin.load = function() { + console.debug('custom-build-example load'); +}; + +export const unload: BuildPlugin.load = function() { + console.debug('custom-build-example unload'); +}; + +export const assetHandlers: string = './asset-handlers'; + +export const configs:BuildPlugin.Configs = { + 'web-mobile': { + options: { + opt_1:{ ... }, + opt_2:{ ... }, + ... + opt_n:{ ... } + }, + verifyRuleMap: { + rule1:{ ... }, + rule2:{ ... }, + ... + rulen:{ ... } + }, + hooks:'./hooks', + }, + 'android':{ ... }, + 'ios':{ ... } + '*':{ ... } +}; +``` + +部分属性与接口描述如下: + +`assetHandlers`:string - 用于替换部分构建方法的脚本文件的相对路径,详情请参考下文 [自定义纹理压缩处理](#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BA%B9%E7%90%86%E5%8E%8B%E7%BC%A9%E5%A4%84%E7%90%86)。 + +`configs`:BuildPlugin.Configs - 构建面板相关的配置 +- `web-mobile`:string - Cocos Creator 支持的平台名,与点击 **构建** 按钮后生成的文件夹一致。 如果平台标记为 `*`,则里面的配置对所有构建平台生效。 +- `options`:{} - 构建面板选项,每一个选项都会在构建面板中新增一个显示项,详情请参考下文 [自定义构建面板选项](#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9E%84%E5%BB%BA%E9%9D%A2%E6%9D%BF%E9%80%89%E9%A1%B9) 。 +- `verifyRuleMap`:{} - 自定义参数校验规则函数,请参考下文 [参数校验规则](#参数校验规则)。 +- `hooks`:string - 指定一个脚本,里面可以包含构建生命周期内的一系列钩子函数,详情请参考下文 [钩子函数](#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0) 。 + +## 自定义构建面板选项 + +目前有两种方式可以添加构建选项到界面上,一种是 **options 自动渲染配置方式**,构建将会自动将 options 里面的配置的内容渲染到构建面板中并完成操作后的一些数据更新操作;另一种是 **panel 自定义面板配置方式**(>=3.8.2),添加 panel 配置放置一个自定义面板的内容,构建将会使用这份配置用 ui-panel 来渲染。 + +为了方便测试,本文将以 web-mobile 为例来展示如何在构建面板上新增选项并显示。 + +### options 自动渲染配置方式 + +新建 `src/builder.ts` 脚本文件,并在 `builder.ts` 中编写如下代码: + +```ts +import { BuildPlugin, IBuildTaskOption } from "../@types/packages/builder/@types"; + +export const load: BuildPlugin.load = function() { + console.debug('custom-build-example load'); +}; + +export const unload: BuildPlugin.load = function() { + console.debug('custom-build-example unload'); +}; + +export const configs:BuildPlugin.Configs = { + 'web-mobile': { + options: { + testInput: { + label: 'testVar', + description: 'this is a test input.', + default: '', + render: { + ui: 'ui-input', + attributes: { + placeholder: 'Enter numbers', + }, + }, + verifyRules: ['required','ruleTest'] + }, + testCheckbox: { + label: 'testCheckbox', + description: 'this is a test checkbox.', + default: false, + render: { + ui: 'ui-checkbox', + }, + }, + }, + verifyRuleMap: { + ruleTest: { + message: 'length of content should be less than 6.', + func(val: any, option: IBuildTaskOption) { + if (val.length < 6) { + return true; + } + return false; + } + } + } + }, +}; + ``` + +在上面的 `configs` 中我们定义了 2 个参数: + +- `testInput`:string - 字符串变量,采用输入框方式修改 +- `testCheckbox`:boolean - 布尔变量,采用复选框方式修改 + +构建选项配置的参数各字段含义如下: + +- `label`:string - 必填,此参数在界面上显示的名称,支持 `i18n:key` 配置 +- `description`:string 可选,简要描述信息,用于鼠标悬停在 label 上时显示提示,支持 `i18n:key` 配置 +- `default`:any - 可选,参数的默认值 +- `render`:{} - 必填,配置渲染组件相关信息 + - `ui`:string - 必填,UI 组件名,详情请参考文档 [UI 组件](../extension/ui.md) + - `attributes`:{} - 可选,UI 组件所需的属性,详情请参考文档 [UI 组件](../extension/ui.md) + +- `verifyRules`:[] - 可选,参数检验规则 +- `verifyRuleMap`:[] - 可选,自定义参数检验规则函数,参考下文 [参数校验规则](#参数校验规则) + +执行 `npm run build` 编译此扩展并刷新后,打开构建面板,在 web-mobile 构建任务中可以看到末尾多出了 2 个构建参数,如下图所示: + +![custom-build-example-options](./custom-build-plugin/custom-build-example-options.png) + +### panel 自定义面板配置方式(>=3.8.2) + +新建 `src/builder.ts` 脚本文件,并在 `builder.ts` 中编写如下代码: + +```ts +import { BuildPlugin, IBuildTaskOption } from "../@types/packages/builder/@types"; + +export const configs:BuildPlugin.Configs = { + 'web-mobile': { + panel: './panel', + } +}; + ``` + +新建 `src/panel.ts` 脚本文件,并在 `panel.ts` 中编写如下代码: + +```ts +import { ICustomPanelThis, ITaskOptions } from '../@types'; +import { PACKAGE_NAME } from './global'; +let panel: ICustomPanelThis; + +export const style = ``; + +export const template = ` +
+ + + + +
+`; + +export const $ = { + root: '.build-plugin', + hideLink: 'ui-checkbox', + link: '#link', +}; + +/** + * all change of options dispatched will enter here + * @param options + * @param key + * @returns + */ +export async function update(options: ITaskOptions, key: string) { + if (key) { + return; + } + // when import build options, key will bey '' + init(); +} + +export function ready(options: ITaskOptions) { + // @ts-ignore + panel = this as ICustomPanelThis; + panel.options = options; + init(); +} + +export function close() { + panel.$.hideLink.removeEventListener('change', onHideLinkChange); +} + +function init() { + panel.$.hideLink.value = panel.options.hideLink; + updateLink(); + panel.$.hideLink.addEventListener('change', onHideLinkChange); +} + +function onHideLinkChange(event: any) { + panel.options.hideLink = event.target.value; + // Note: dispatch the change to build panel + panel.dispatch('update', `packages.${PACKAGE_NAME}.hideLink`, panel.options.hideLink); + updateLink(); +} + +function updateLink() { + if (panel.options.hideLink) { + panel.$.link.style.display = 'none'; + } else { + panel.$.link.style.display = 'block'; + } +} +``` + +#### 自定义构建面板选项的规则 + +以上代码来自自定义构建插件模板内的简易示例代码,可以根据自己的偏好选择其他的前端框架来开发界面更方便。只要注意以下几点约定的规则即可: + +1. panel 的文件内容规则是 ui-panel 来渲染的,需要遵循相关的组件规则,基本是参照以上示例在代码内暴露 `style`、`template`、`$` 等变量或生命周期钩子。 +2. panel 的 `ready` 函数会在面板加载完成后被调用,可以根据需要在此函数中做一些 dom 元素的初始化、事件绑定或者是一些前端框架的初始化绑定。 +3. panel 的 `close` 函数会在面板关闭时被调用,可以根据需要在此函数中做一些事件的反注册、对象销毁等。 + +4. panel 的 `update` 函数会在任意构建选项发生变化时被调用(包括自身发送的 update),可以根据需要在此函数中做相应的处理,比如**需要根据其他构建选项做一些交互联动**时,可以添加此钩子。同时当构建面板有导入配置的行为时也可以在此函数中做自定义面板的一些界面更新,此时 `key` 为 空字符串。 + +5. 自定义面板中的交互如果涉及到构建选项的更新包括初始化过程中可能做的数据更新,都需要使用 `panel.dispatch('update', key, value, error)` 进行更新,其中 `key` 值需要遵循 `packages.${PACKAGE_NAME}.key` 的格式。只有调用后才会同步数据到最终构建时的那份构建选项,发送 error 的作用主要是影响构建按钮的禁用情况,error 为可选项。 + +6. 自定义面板中的交互如果涉及到构建选项的读取,需要使用 `panel.options.key` 进行读取,其中 `key` 值需要遵循 `packages.${PACKAGE_NAME}.key` 的格式。 + +### 特别注意事项 + +1. 不同进程中的环境变量会有所差异,自定义构建脚本会同时被 **渲染进程** 和 **主进程** 加载,所以请不要在自定义构建脚本中使用仅存在于单一进程中的编辑器接口。进程相关详情请参考下文 [调试构建扩展](#%E8%B0%83%E8%AF%95%E6%9E%84%E5%BB%BA%E6%89%A9%E5%B1%95) 部分的内容。 + +2. `configs` 的 `key` 有两种配置方式: + + - 针对 **单个平台** 的配置,`key` 值填写为 `平台名`,参考上面的代码示例。各平台对应的构建名可在编辑器主菜单的 **扩展 -> 扩展管理器 -> 内置** 中查看。 + + - 针对 **所有平台** 的配置,`key` 值填写为 `*`,参考 [构建扩展模板](#%E6%9E%84%E5%BB%BA%E6%89%A9%E5%B1%95%E6%A8%A1%E6%9D%BF) 中的 `source/builder.ts` 文件。 + + > **注意**:优先级 `平台名` > `*`, 若某平台有对应的 `平台名` 配置,则会优先使用对应的配置, `*` 中的所有配置对此平台无效。 + +## 参数校验规则 + +在 `testInput` 参数中,有如下配置: + +```ts +verifyRules: ['required','ruleTest'] +``` + +### 内置参数校验规则 + +上面的 `verifyRules` 定义的 `required`,是扩展系统的内置校验规则,它表示此选项为必填项,不能为空,在界面上会有 `*` 提示,未填写时也会有红字提示: + +![custom-build-example-options](./custom-build-plugin/custom-build-example-options.png) + +完整的内置参数校验规则如下: + +- `required` - 参数为必填 +- `array` - 参数内容必须满足数组格式 +- `string` - 参数内容只能是字符串形式 +- `number` - 参数只能是数字 + +`verifyRules` 数组中还定义了一个叫 `ruleTest` 的自定义校验规则。自定义构建脚本在进行匹配时,会优先从 `verifyRuleMap` 去寻找匹配,若不能匹配到任何一个自定义校验规则,才会从内置的参数校验规则中去寻找。 + +自定义规则属性含义如下: +- `message`:string - 当规则校验失败后,会在控制台窗口打印出此消息,支持 `i18n:key` 形式。 +- `func`:function - 自定义校验规则函数 + - `val`:any - 当前值 + - `options`:IBuildTaskOption - 整个构建面板的选项信息,详情请前往 `d.ts` 查看定义 + +本例中的自定义检验规则函数,会对输入的字符串长度做检查,当字符串长度不小于 6 时,会打印 `message` 中定义的提示信息,如下图所示: + +![custom-build-example-validator-errormsg](./custom-build-plugin/custom-build-example-validator-errormsg.png) + +## 钩子函数 + +在项目构建过程中,引擎提供了处理一些特殊事件的钩子函数,如下图所示: + +![build-process](./custom-project-build-template/build-process.jpg) + +参考上图,所有的钩子函数都是在构建流程中按照顺序依次执行,不同的钩子函数接收到的数据会有所差异。钩子函数中可以直接使用引擎提供的 `API` 和 `Editor` 相关全局变量。 + +关于 `Editor` 详细的接口定义请点击编辑器主菜单的 **开发者 —> 导出 .d.ts** 获取和查看。关于构建进程的说明请参考下文 [构建进程](#%E6%9E%84%E5%BB%BA%E8%BF%9B%E7%A8%8Bhooks-%E8%84%9A%E6%9C%AC) 部分的内容。 + +要实现钩子函数,需要先在 `builder.ts` 中加入 `hooks` 字段,参考上文的 [自定义构建脚本结构](##%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9E%84%E5%BB%BA%E8%84%9A%E6%9C%AC%E7%BB%93%E6%9E%84),并创建一个 `src/hooks.ts` 脚本文件写入如下代码: + +```ts +import { BuildHook } from "../@types/packages/builder/@types"; + +const PACKAGE_NAME = 'custom-build-example'; + +export const throwError: BuildHook.throwError = true; + +export const load: BuildHook.load = async function() { + console.log(PACKAGE_NAME,load); +}; + +export const onBeforeBuild: BuildHook.onBeforeBuild = async function(options) { + // Todo some thing + console.log(PACKAGE_NAME,'onBeforeBuild'); +}; + +export const onBeforeCompressSettings: BuildHook.onBeforeCompressSettings = async function(options, result) { + // Todo some thing + console.log(PACKAGE_NAME,'onBeforeCompressSettings'); +}; + +export const onAfterCompressSettings: BuildHook.onAfterCompressSettings = async function(options, result) { + // Todo some thing + console.log(PACKAGE_NAME, 'onAfterCompressSettings'); +}; + +export const onAfterBuild: BuildHook.onAfterBuild = async function(options, result) { + console.log(PACKAGE_NAME, 'onAfterBuild'); +}; + +export const unload: BuildHook.unload = async function() { + console.log(PACKAGE_NAME, 'unload'); +}; +``` + +目前支持的钩子函数列表: +- `onBeforeBuild` - 构建开始之前调用 +- `onBeforeCompressSettings` - 开始压缩相关的 JSON 文件前调用 +- `onAfterCompressSettings` - 压缩完设置文件后调用 +- `onAfterBuild` - 构建完成之后调用 + +钩子接口定义如下所示: + +```ts +type IBaseHooks = (options: IBuildTaskOptions, result?: IBuildResult) => void | Promise; + +export namespace BuildHook { + export type throwError = boolean; + export type title = string; + export type onBeforeBuild = IBaseHooks; + export type onBeforeCompressSettings = IBaseHooks; + export type onAfterCompressSettings = IBaseHooks; + export type onAfterBuild = IBaseHooks; + export type load = () => Promise | void; + export type unload = () => Promise | void; +} +``` + +> **注意**: +> 1. 传递到钩子函数中的 `options` 是实际构建流程中 `options` 对象的一个副本,仅作为信息获取的参考,直接修改它虽然能修改成功但并不会真正地影响构建流程。若要修改构建参数请在自定义构建脚本的 `options` 字段中修改。 +> 2. 钩子函数允许为异步函数,构建流程在执行钩子函数时默认会 `await` 等待其执行完毕才会执行下一个流程。 +> 3. 详细的接口定义可以参考扩展中 `@types/packages/builder` 目录下的内容。 + +编译、并刷新此扩展后,再次执行 web-mobile 构建任务,可在构建日志文件中看到相关打印信息。可通过下图所示按钮打开日志文件: + +![custom-build-example-hooks-log](./custom-build-plugin/custom-build-example-hooks-log.png) + +## 自定义纹理压缩处理 + +Cocos Creator 提供了自带的压缩工具用于处理压缩纹理资源,但因需要兼容不同的用户环境,通常压缩工具会选择兼容性更高的,而不是性能最高的。 + +因此 Cocos Creator 开放了资源处理器扩展机制:**允许用户自定义对应纹理资源的压缩处理函数,构建时会在相应的处理时机进行调用**。 + +具体操作步骤如下: + +### 添加资源处理器脚本相对路径 + +在自定义构建脚本 `src/builder.ts` 中,添加如下所示代码: + +```ts +export const assetHandlers = './asset-handlers'; +``` + +### 编写脚本 + +创建 `src/asset-handlers.ts` 脚本文件并编写如下代码: + +```ts +import { AssetHandlers } from '../@types/packages/builder/@types'; + +export const compressTextures: AssetHandlers.compressTextures = async (tasks) => { + console.log('compressTextures'); +}; +``` + +### 运行测试 + +选择一张场景中使用到的图片,在其属性检查器中开启压缩选项,如下图所示: + +![custom-build-compress-tex-option](./custom-build-plugin/custom-build-compress-tex-option.png) + +编译、并刷新此扩展后,再次执行 web-mobile 构建任务,可在构建日志文件中看到相关打印信息。 + +> **注意**:测试时请先点击构建面板右上角的清理缓存按钮清理项目缓存,否则会因为纹理压缩的缓存机制导致不会再次执行压缩任务。 + +### 执行流程 + +在 `assetHandlers` 脚本模块里,我们开放了 `compressTextures` 函数,构建时便会在纹理压缩处理阶段调用该处理函数。 + +处理函数会接收当前剩余的未被处理的纹理压缩任务数组,处理完成后从原数组中移除。未被移除的纹理压缩任务视为未处理,会被放置到下一个相应的处理函数进行处理,直到所有的处理函数都处理完了,若还有未处理的纹理压缩任务,则放置回 Cocos Creator 原有的纹理压缩流程中进行处理。 + +当有多个扩展都注册了纹理压缩处理函数时,按照扩展启动顺序执行,如果前一个扩展处理了全部的纹理压缩任务,则后注册的扩展的处理函数将不会收到任务。 + +代码示例如下: + +```ts +type ITextureCompressType = + | 'jpg' + | 'png' + | 'webp' + | 'pvrtc_4bits_rgb' + | 'astc_12x12'; // 详细格式请参见接口定义 +interface ICompressTasks { + src: string; // 源文件地址 + dest: string; // 生成的目标文件地址(后缀默认为 PNG,其他类型需要自行更改) + quality: number | IPVRQuality | IASTCQuality | IETCQuality; // 压缩质量 0 - 100 或者其他的压缩等级 + format: ITextureCompressType; // 压缩类型 +} +export async function compressTextures(tasks: ICompressTasks[]) { + for (let i = 0; i < Array.from(tasks).length; i++) { + const task = Array.from(tasks)[i]; + if (task.format !== 'jpg') { + // 跳过处理的纹理压缩任务会传递给下一个处理函数,直至最后进入 Creator 原有的构建时纹理压缩流程 + continue; + } + task.dest = task.dest.replace('.png', '.jpg'); + await pngToJPG(task.src, task.dest, task.quality); + // 处理完的纹理压缩任务,将其从 tasks 里移除,这样构建时便不会再次处理 + tasks.splice(i, 1); + } +} +``` + +> 通常来说,我们建议用户优先使用 **项目设置** -> [自定义纹理压缩](../project/index.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BA%B9%E7%90%86#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BA%B9%E7%90%86%E5%8E%8B%E7%BC%A9%E8%A1%8C%E4%B8%BA) 来配置项目内的纹理压缩。在上述功能无法满足需求的情况下,再使用扩展进行纹理压缩处理。 + +## 调试构建扩展 + +构建扩展参与到自定义构建流程时,相关代码会运行在以下三种进程: + +- **主进程**:执行自定义构建脚本(`package.json` 中 `builder` 字段指定的脚本)及其依赖资源 +- **渲染进程**:执行自定义构建脚本中注册到 **构建发布** 面板上的部分字段 +- **构建进程**:执行自定义构建脚本中 `hooks` 字段定义的脚本 + +### 主进程(入口脚本) + +主进程主要执行用于参与构建流程的自定义构建脚本,以及扩展自身的入口脚本(`main` 字段中指定的脚本)。 + +当修改了运行在主进程中的代码时,必须要重启扩展,然后再刷新需要更新的进程(这一点会在之后优化,尽量通过一次重启便解决代码更新问题,但刷新依旧是最彻底的重载方法)。主进程目前没有比较合适的调试方法,可以使用命令行打开编辑器查看主进程代码日志来辅助调试: + +```bash + +// Mac +/Applications/CocosCreator/Creator/3.0.0/CocosCreator.app/Contents/MacOS/CocosCreator --project projectPath + +// Windows +...\CocosCreator.exe --project projectPath +``` + +### 渲染进程(构建发布面板) + +自定义构建脚本中,有部分字段是注册到 **构建发布** 面板上的,例如 `options` 的显示配置、`panel` 字段,以及 `panel` 脚本本身,这部分内容会在渲染进程载入执行。渲染进程其实就是窗口自己的执行进程,打开调试工具,可以调试 **构建发布** 面板上的 DOM 元素、样式、脚本等。 + +如果是修改了注册到 **构建发布** 面板上的代码时,只需要刷新面板即可,无需重启扩展。 + +- **打开构建发布面板渲染进程的调试工具** + + 点击 **构建发布** 面板,然后按下快捷键 **Ctrl + Shift + i**(Windows)或者 **Cmd + Option + i**(Mac),即可打开 **构建发布** 面板的调试工具。 + +- **重新载入(刷新)面板方式** + + 点击 **构建发布** 面板或者 **构建发布** 面板的调试工具后,按下 **Ctrl/Cmd + R** 即可。 + +### 构建进程(`hooks` 脚本) + +构建流程的实际执行环境是单独的一个 `worker` 进程,确保即使发生异常崩溃也不会影响到其他窗口的正常使用。在自定义构建脚本的 `hooks` 字段中定义的脚本也是在这个单独的 `worker` 进程中载入执行的。 + +如果仅修改 `hooks` 字段定义的脚本,刷新构建进程即可,无需重启扩展。刷新方式同上文的 **构建发布** 面板一致,打开构建调试工具后,按下快捷键 **Ctrl/Cmd + R** 即可。 + +### 打开构建进程的调试工具 + +下三种方式均可打开调试工具: + +1. 在 **构建发布** 面板点击构建任务窗口右上方的 **打开构建调试工具** 按钮。 + + ![dev_tools](./custom-project-build-template/dev_tools.png) + +2. 点击编辑器主菜单中的 **开发者 -> 打开构建调试工具** 即可。 + +3. 在任意控制台或者扩展的代码中,执行以下代码: + + ```ts + Editor.Message.send('builder', 'open-devtools'); + ``` + + 可以在这个消息方法的基础上,根据自己的需要进行加工处理。(例如:可以在自己编写的构建扩展代码中捕获错误,一旦有异常就自动打开调试工具)。 + +完整示例代码请参考本文 [构建扩展模板](#%E6%9E%84%E5%BB%BA%E6%89%A9%E5%B1%95%E6%A8%A1%E6%9D%BF) 中生成的扩展项目。 diff --git a/versions/4.0/zh/editor/publish/custom-build-plugin/cocos-build-template-menu.png b/versions/4.0/zh/editor/publish/custom-build-plugin/cocos-build-template-menu.png new file mode 100644 index 0000000000..879cbd9f23 Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-build-plugin/cocos-build-template-menu.png differ diff --git a/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-compress-tex-option.png b/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-compress-tex-option.png new file mode 100644 index 0000000000..bcfb711f1d Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-compress-tex-option.png differ diff --git a/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-example-hooks-log.png b/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-example-hooks-log.png new file mode 100644 index 0000000000..0adba31ae8 Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-example-hooks-log.png differ diff --git a/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-example-options.png b/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-example-options.png new file mode 100644 index 0000000000..6679e0a73e Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-example-options.png differ diff --git a/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-example-validator-errormsg.png b/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-example-validator-errormsg.png new file mode 100644 index 0000000000..da777f98ec Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-build-plugin/custom-build-example-validator-errormsg.png differ diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template.md b/versions/4.0/zh/editor/publish/custom-project-build-template.md new file mode 100644 index 0000000000..60dd5b64d7 --- /dev/null +++ b/versions/4.0/zh/editor/publish/custom-project-build-template.md @@ -0,0 +1,77 @@ +# 自定义构建模版 + +Cocos Creator 支持对每个项目分别定制构建模板,只需要在项目路径下添加一个 `build-templates` 目录,里面按照 **平台扩展名称** 划分子目录。在构建结束后,`build-templates/[platform]` 目录下所有的文件都会自动按照对应的目录结构 **复制** 到对应平台构建生成的工程中,这个功能是全平台支持的。具体的 **平台扩展名称** 请参考最下方的 **特殊自定义构建模板平台支持表**。 + +结构类似: + +```bash +project-folder + |--assets + |--build + |--build-templates + |--web-mobile + // 需要添加的文件,如 index.html + |--index.html +``` + +这样如果当前构建的平台是 **Web Mobile** 的话,那么 `build-templates/web-mobile/index.html` 就会在构建后被拷贝到 `build/web-mobile(以平台扩展名称为准)/index.html`。 + +除此之外,目前构建模板支持的文件类型还包括 **ejs 类型** 和 **json 类型**,这两个类型不会直接拷贝而是会过解析处理,或者数据融合。各平台对这两种模板类型的支持情况,详情请参考下文的 **特殊自定义构建模板平台支持表**。 + +## ejs 类型 + +随着 Cocos Creator 版本的升级,可能会对构建模板做一些修改和更新,就会导致不同版本构建出来的包内容不完全一样,开发者需要手动同步更新项目中定制的构建模板。例如构建时勾选了 MD5 Cache 选项之后,以 Web 平台的 `index.html` 为例,里面引用的 `css` 文件地址会带有 MD5 Hash 后缀,可能会和原先模板里的不匹配而导致无法使用。
+因此为了优化这个问题,Creator 在主菜单的 **项目** 中新增了 **创建项目构建模板** 选项,用于生成对应平台支持的构建模板。 + +![build template](custom-project-build-template/build-template.png) + +开发者只需要在生成的构建模板的 `.ejs` 中进行定制,构建时会自动将编辑器构建模板的更新同步到定制的构建模板中,经常改动的内容都会同步到该模板引用的子模板(`.ejs`)中,这样定制构建模板便可以不用频繁手动更新。 + +以创建 Web Mobile 构建模板为例,生成的构建模板目录结构如下: + +![web-mobile](custom-project-build-template/web-mobile.png) + +> **注意**:构建时,拷贝定制构建模板这一步骤是最后执行的,也就是说,假如该目录下同时存在 `index.ejs` 与 `index.html`,那么最终打包出来的是 `index.html` 文件而不是 `index.ejs` 文件。 + +## json 类型的融合处理 + +许多小游戏平台都会有类似 `game.json` 之类的配置文件,若这些 json 文件在对应平台的定制构建模板中,构建时便不会直接将其拷贝并覆盖到对应的发布包目录下,而是会将编辑器最新构建模板的内容整合进来。 + +## 特殊自定义构建模板平台支持表 + +大部分文件放置在 `build-templates/[platform]` 目录下都会被直接拷贝到对应目录下,除此之外,各个平台还支持一些特殊名称格式的模板文件,部分文件各平台对构建模板的文件类型支持情况如下表所示: + +| 平台 | 平台构建扩展名 | 支持的特殊文件类型 | +| :--- | :--- | :--- | +| 华为 AGC | huawei-agc | `index.ejs` | +| 支付宝小游戏 | alipay-mini-game | `game.json` | +| 淘宝小游戏 | taobao-mini-game | `game.json`、`mini.project.json` | +| 抖音小游戏 | bytedance-mini-game | `game.ejs`、`game.json`、`project.config.json` | +| OPPO 小游戏 | oppo-mini-game | `manifest.json` | +| 华为快游戏 | huawei-quick-game | `game.ejs` | +| Cocos Play | cocos-play | `game.config.json` | +| vivo 小游戏 | vivo-mini-game | `project.config.json` | +| 百度小游戏 | baidu-mini-game | `game.json`、`project.swan.json` | +| 微信小游戏 | wechatgame | `game.ejs`、`game.json`、`project.config.json` | +| Web Desktop | web-desktop | `index.ejs` | +| Web Mobile | web-mobile | `index.ejs` | +| 原生平台 | native | `index.ejs` | + +| 平台 | 配置文件 | 配置说明 | +| :--- | :--- | :--- | +| 抖音 | game.json | https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/framework/mini-game-configuration | +| 微信 | game.json | https://developers.weixin.qq.com/minigame/dev/reference/configuration/app.html| + + diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template/build-process-public.drawio b/versions/4.0/zh/editor/publish/custom-project-build-template/build-process-public.drawio new file mode 100644 index 0000000000..d620eaff70 --- /dev/null +++ b/versions/4.0/zh/editor/publish/custom-project-build-template/build-process-public.drawio @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template/build-process.jpg b/versions/4.0/zh/editor/publish/custom-project-build-template/build-process.jpg new file mode 100644 index 0000000000..e83a768d8a Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-project-build-template/build-process.jpg differ diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template/build-template.png b/versions/4.0/zh/editor/publish/custom-project-build-template/build-template.png new file mode 100644 index 0000000000..25501118a4 Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-project-build-template/build-template.png differ diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template/console.png b/versions/4.0/zh/editor/publish/custom-project-build-template/console.png new file mode 100644 index 0000000000..fcd6c19463 Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-project-build-template/console.png differ diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template/dev_tools.png b/versions/4.0/zh/editor/publish/custom-project-build-template/dev_tools.png new file mode 100644 index 0000000000..5b1f4122e6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-project-build-template/dev_tools.png differ diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template/enable-plugin.png b/versions/4.0/zh/editor/publish/custom-project-build-template/enable-plugin.png new file mode 100644 index 0000000000..60735e197b Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-project-build-template/enable-plugin.png differ diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template/plugin-template.png b/versions/4.0/zh/editor/publish/custom-project-build-template/plugin-template.png new file mode 100644 index 0000000000..03a30f79f0 Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-project-build-template/plugin-template.png differ diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template/reload.png b/versions/4.0/zh/editor/publish/custom-project-build-template/reload.png new file mode 100644 index 0000000000..cae0cc7dd7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-project-build-template/reload.png differ diff --git a/versions/4.0/zh/editor/publish/custom-project-build-template/web-mobile.png b/versions/4.0/zh/editor/publish/custom-project-build-template/web-mobile.png new file mode 100644 index 0000000000..24d68d3a46 Binary files /dev/null and b/versions/4.0/zh/editor/publish/custom-project-build-template/web-mobile.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb.md b/versions/4.0/zh/editor/publish/debug-jsb.md new file mode 100644 index 0000000000..02481f4c76 --- /dev/null +++ b/versions/4.0/zh/editor/publish/debug-jsb.md @@ -0,0 +1,132 @@ +# 原生平台 JavaScript 调试 + +游戏发布到原生平台后,由于运行环境不同,可能会出现在浏览器预览时无法重现的 Bug,这时我们就必须直接在原生平台下进行调试。Cocos Creator 可以很方便地对原生平台中的 JavaScript 进行远程调试。 + +## iOS 和 Android 真机调试 + +如果游戏只有在真机上才能运行,那就必须用真机对打包后的游戏进行调试。调试步骤如下: + +- 确保 Android/iOS 设备与 Windows 或者 Mac 在同一个局域网中。注意在调试过程中请勿开启代理,否则可能导致无法正常调试。 +- 在 Creator 的 **构建发布** 面板选择 Android/iOS 平台、Debug 模式,构建编译运行工程(iOS 平台建议通过 Xcode 连接真机进行编译运行)。 +- 用 Chrome 浏览器打开地址:`devtools://devtools/bundled/js_app.html?v8only=true&ws=设备的本地 IP:43086/00010002-0003-4004-8005-000600070008` 即可进行调试。 + + ![v8-android-debug](debug-jsb/v8-android-debug.png) + +>如果端口被占用,端口会做+1自增处理,若连接不上,请查看 App 启动时控制台打印的端口号。 +>构建时,建议勾选 Debug, Source Maps 选项,这样对应的 js 代码显示才是展开的。 + +## HarmonyOS Next 平台调试 +### 使用 V8 引擎 +与 IOS 和 Android 类似 +1. 编译工程,运行工程,搜索日志: + +![jsvm-debug-info-log](debug-jsb/v8-debug-info-log.png) + +2. 把这个链接拷贝到浏览器中运行即可。 + +### 使用 JSVM 引擎 +1. 在 项目工程目录/native/engine/common/Classes/Game.cpp 中配置调试信息: + ![config-debug-info](debug-jsb/config-debug-info.png) + + port : 配置端口号(这个配置的是端测端口) + + pauseOnStart : 等待连接调试开始 + + +2. 编译工程,运行工程,搜索日志: + +![jsvm-debug-info-log](debug-jsb/jsvm-debug-info-log.png) + +3. 检查端侧端口是否打开成功。hdc shell "netstat -anp | grep 8806"。结果为8806端口(注意端口号,是与步骤1中的端口号一致)状态为“LISTEN"即可,如果2、3正常的话,可以忽略这个步骤。 + + +4. 转发端口。hdc fport tcp:9906 tcp:8806。转发开发者个人计算机侧端口9906到端侧端口8806。结果为"Forwardport result:OK"即可,个人计算机侧端口可以与端侧端口不一样,默认是个人计算机侧端口与端侧端口一样。 +(建议使用个人计算机测与端侧端口一致,也就是都写8806,这里只是为了演示不一样的情况) +![hdc-fport](debug-jsb/hdc-fport.png) + +5. 获取端口连接信息,在chrome浏览器地址栏输入"localhost:9906/json"(这里的端口是个人计算机侧端口,如果使用的和端侧端口一样,可以拷贝步骤2的日志连接),回车。 +![jsvm-localhost-json](debug-jsb/jsvm-localhost-json.png) + +6. 拷贝"devtoolsFrontendUrl"字段url内容到地址栏,回车,进入DevTools源码页,将看到在应用中通过OH_JSVM_RunScript执行的JS源码,此时暂停在第一行JS源码处。(注:"devtoolsFrontendUrl"字段url只支持使用Chrome、Edge浏览器打开,不支持使用Firefox、Safari等浏览器打开。) +![jsvm-debug](debug-jsb/jsvm-debug.png) + + +*注意: 每次重新运行之后,需要重新拷贝devtoolsFrontendUrl字段,因为devtoolsFrontendUrl字段每次重新运行后不一样* +### NAPI 暂不支持调试 + +## Windows 平台及 Mac 平台调试 + +在 Windows 平台及 Mac 平台下调试游戏,步骤与真机调试类似,将工程用 IDE 编译运行之后,此时便可进行调试。步骤如下: + +- 用 IDE 将打包好的工程编译并运行(Windows 平台请使用 Visual Studio,Mac 平台请使用 Xcode) +- 在游戏运行时打开 Chrome 浏览器,输入地址:`devtools://devtools/bundled/js_app.html?v8only=true&ws=127.0.0.1:6086/00010002-0003-4004-8005-000600070008` 即可进行调试。 + + ![](debug-jsb/v8-win32-debug.png) + +>如果端口被占用,端口会做+1自增处理,若连接不上,请查看 App 启动时控制台打印的端口号。 + +## 使用 `lldb` 查看当前的 JS 调用栈 + +通过在 C++ 中断点我们能很便捷地看到 C++ 的调用栈,但并不能同时看到 JS 的调用栈,这个割裂的过程常常会破坏调试的体验。而 `lldb` 提供的功能支持在调试过程中进行很多的操作,包括查看调用栈。 + +**Xcode** 和 **Android Studio** 都默认使用 `lldb` 作为调试器。详情可参考文档 [LLDB 指南](https://lldb.llvm.org/use/tutorial.html)。 + +### `lldb` 的全局配置 + +`lldb` 在启动的时候会加载 `~/.lldbinit`,例如下面的配置: + +`~ % cat ~/.lldbinit` + +``` +target stop-hook add +po se::ScriptEngine::getInstance()->getCurrentStackTrace() +DONE +``` + +设置了 **每次断点** 后的行为,执行以下代码输出 JS 调用栈的信息: + +```txt +po se::ScriptEngine::getInstance()->getCurrentStackTrace() +``` + +关于 `target stop-hook` 的用法,详情可参考文档: + +但这种方法也存在着明显的缺陷:会对 **所有项目** 生效,若其他项目不存在相应符号,就会导致出现报错。 + +### 在 Xcode 配置 `lldb` + +#### Xcode 在断点中编辑 action(只对具体的断点触发) + +![xcode-brk-point-action](debug-jsb/xcode-brk-point-action.png) + +在 **Debugger Command** 中输入命令: + +```txt +po se::ScriptEngine::getInstance()->getCurrentStackTrace() +``` + +关于 `target stop-hook` 的用法,详情可参考文档: + +#### 设置 stop hook + +断点触发后,需要在 lldb console 中增加回调。可以针对具体的断点进行更多的调用: + +![xcode-brk-point-lldb](debug-jsb/xcode-brk-point-lldb.png) + +同上,也可以执行以下代码查看调用栈: + +```txt +po se::ScriptEngine::getInstance()->getCurrentStackTrace() +``` + +### 在 Android Studio 配置 `lldb` + +在 **Android Studio** 的 **Run -> Debug Configuration -> Debugger** 界面进行类似的配置: + +![as-brk-point-action](debug-jsb/as-brk-point-action.png) + +Android Studio 也提供了和 Xcode 类似的 `lldb console`。 + +## 进阶调试指南 + +如果需要在 Release 模式下调试,或者需要调试定制后的原生引擎,可参考更详细的 [JSB 2.0 使用指南:远程调试与 Profile](../../advanced-topics/JSB2.0-learning.md)。 diff --git a/versions/4.0/zh/editor/publish/debug-jsb/as-brk-point-action.png b/versions/4.0/zh/editor/publish/debug-jsb/as-brk-point-action.png new file mode 100644 index 0000000000..ffa52225f1 Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/as-brk-point-action.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/config-debug-info.png b/versions/4.0/zh/editor/publish/debug-jsb/config-debug-info.png new file mode 100644 index 0000000000..e07f8ecedf Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/config-debug-info.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/hdc-fport.png b/versions/4.0/zh/editor/publish/debug-jsb/hdc-fport.png new file mode 100644 index 0000000000..897091cf32 Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/hdc-fport.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/jsvm-debug-info-log.png b/versions/4.0/zh/editor/publish/debug-jsb/jsvm-debug-info-log.png new file mode 100644 index 0000000000..1924d6091b Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/jsvm-debug-info-log.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/jsvm-debug.png b/versions/4.0/zh/editor/publish/debug-jsb/jsvm-debug.png new file mode 100644 index 0000000000..78e22bddad Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/jsvm-debug.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/jsvm-localhost-json.png b/versions/4.0/zh/editor/publish/debug-jsb/jsvm-localhost-json.png new file mode 100644 index 0000000000..52dcc86974 Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/jsvm-localhost-json.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/simulator-run.png b/versions/4.0/zh/editor/publish/debug-jsb/simulator-run.png new file mode 100644 index 0000000000..b365e75e36 Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/simulator-run.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/v8-android-debug.png b/versions/4.0/zh/editor/publish/debug-jsb/v8-android-debug.png new file mode 100644 index 0000000000..c866cd1813 Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/v8-android-debug.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/v8-debug-info-log.png b/versions/4.0/zh/editor/publish/debug-jsb/v8-debug-info-log.png new file mode 100644 index 0000000000..d49d197cb1 Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/v8-debug-info-log.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/v8-win32-debug.png b/versions/4.0/zh/editor/publish/debug-jsb/v8-win32-debug.png new file mode 100644 index 0000000000..7061257b84 Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/v8-win32-debug.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/xcode-brk-point-action.png b/versions/4.0/zh/editor/publish/debug-jsb/xcode-brk-point-action.png new file mode 100644 index 0000000000..bdf435936d Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/xcode-brk-point-action.png differ diff --git a/versions/4.0/zh/editor/publish/debug-jsb/xcode-brk-point-lldb.png b/versions/4.0/zh/editor/publish/debug-jsb/xcode-brk-point-lldb.png new file mode 100644 index 0000000000..43c623776c Binary files /dev/null and b/versions/4.0/zh/editor/publish/debug-jsb/xcode-brk-point-lldb.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/build-and-run.md b/versions/4.0/zh/editor/publish/google-play-games/build-and-run.md new file mode 100644 index 0000000000..89e57a72dc --- /dev/null +++ b/versions/4.0/zh/editor/publish/google-play-games/build-and-run.md @@ -0,0 +1,59 @@ +# 运行和启动 + +![build-and-run/run-on-hpe.png](build-and-run/run-on-hpe.png) + +本节我们将介绍如何通过 HPE 模拟器对已有的 APK 进行开发和测试。 + +## 准备工作 + +首先您需要准备一个由 Cocos Creator 发布的 Android Studio 工程,如果对此不熟悉,建议您参考 [安卓构建示例](../android/build-example.md) + +## 启动模拟器 + +首先您需要在操作系统的工具栏内找到模拟器的图标并选择 **Launch Emulator** 来启动模拟器: + +![build-and-run/start-simulator.png](build-and-run/start-simulator.png) + +启动后: + +![build-and-run/start-simulator.png](build-and-run/after-start-simulator.png) + +## 安装应用 + +我们稍微回顾下如何通过 Android Studio 来构建 APK,点击下面的菜单即可: + +![build-and-run/build-apk.png](build-and-run/build-apk.png) + +构建成功后可以在项目目录内找到对应的 APK 文件。 + +### 启动 ADB + +接下来需要将 APK 安装到模拟器上,找到 adb 命令并输入: + +> adb 一般在安卓的 SDK/plat-form 目录内。您可以考虑将该目录加入环境变量中,以便在任何地方可以使用。 + +```bash +adb devices +``` + +用于检查模拟器是否正确连接。 + +![build-and-run/adb-devices.png](build-and-run/adb-devices.png) + +如显示 `localhost:6520 offline` 或不显示设备,可重启模拟器或者在命令行内输入: `adb connect localhost:6520`。 + +### 安装应用 + +找到之前 apk 的目录,并在命令行内输入: + +```bash +adb install C:/yourpath/yourgame.apk +``` + +![build-and-run/install-apk.png](build-and-run/install-apk.png) + +## 启动应用 + +找到已经安装的程序在屏幕中通过鼠标上滑即可: + +![build-and-run/show-apps.png](build-and-run/show-apps.png) diff --git a/versions/4.0/zh/editor/publish/google-play-games/build-and-run/adb-devices.png b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/adb-devices.png new file mode 100644 index 0000000000..96ae1ac74d Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/adb-devices.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/build-and-run/after-start-simulator.png b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/after-start-simulator.png new file mode 100644 index 0000000000..bd3724b294 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/after-start-simulator.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/build-and-run/build-apk.png b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/build-apk.png new file mode 100644 index 0000000000..2eb37c9faf Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/build-apk.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/build-and-run/find-adb.png b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/find-adb.png new file mode 100644 index 0000000000..08cb108dd2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/find-adb.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/build-and-run/install-apk.png b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/install-apk.png new file mode 100644 index 0000000000..61e2bf1876 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/install-apk.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/build-and-run/run-on-hpe.png b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/run-on-hpe.png new file mode 100644 index 0000000000..56ff530221 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/run-on-hpe.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/build-and-run/show-apps.png b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/show-apps.png new file mode 100644 index 0000000000..78220a4587 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/show-apps.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/build-and-run/start-simulator.png b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/start-simulator.png new file mode 100644 index 0000000000..1f7cd19b8b Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/build-and-run/start-simulator.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/external-storage/external-permissions.png b/versions/4.0/zh/editor/publish/google-play-games/external-storage/external-permissions.png new file mode 100644 index 0000000000..f1b69f96ac Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/external-storage/external-permissions.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/external-storage/permission.png b/versions/4.0/zh/editor/publish/google-play-games/external-storage/permission.png new file mode 100644 index 0000000000..16f75be236 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/external-storage/permission.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/index.md b/versions/4.0/zh/editor/publish/google-play-games/index.md new file mode 100644 index 0000000000..a1ba3abfc3 --- /dev/null +++ b/versions/4.0/zh/editor/publish/google-play-games/index.md @@ -0,0 +1,86 @@ +# Google Play Games on PC + +[Google Play Games (GPG)](https://play.google.com/googleplaygames#section-system-requirements) 是由谷歌开发,可以让您的移动端 APK 在 PC 上发布、游玩的相关技术。 + +自 v3.8 起,Cocos Creator 将提供发布到 GPG 的支持。这样将有助于您安卓版本游戏在 PC 平台发布以获取更多的增长。 + +为了顺利在 GPG 上发布,我们建议您先阅读 [GPG 官方网站](https://developer.android.com/games/playgames/overview?hl=zh-cn) 以便快速介入 GPG SDK。 + +## 接入指南 + +为了让您的应用可以顺利上架,请检查下列的步骤是否都已经设置。 + +1. 为了让游戏可以在 Windows 上运行(包括 intel 和 AMD 芯片),您需要采用 x86 架构进行构建。在 Cocos Creator 构建时,选中 [APP ABI](../native-options.md#app-abi) 并勾选 x86_64: + + ![ap](./index/app-abi.png) + + > x86_64 同时支持 x86 和 x64 建构,为应对未来的需求变化,我们建议您勾选 x86_64。 + +2. GPG 采用的 OpenGL ES 版本是 3.1,请不要使用高于 3.1 版本。Cocos Creator 支持的最高版本的 OpenGL ES 是 3.1。请参考下图。 +3. Vulkan 的版本不高于 1.1,对于 Cocos Creator 来说,要支持 Vulkan,在构建选项中需要勾选 Vulkan 即可,请参考下图。 + + ![index/render-backend.png](index/render-backend.png) + +4. 需要去除相关的移动端平台的特性和权限,根据 [功能测试要求](https://developer.android.com/games/playgames/pc-compatibility?hl=zh-cn#unsupported-features-1) 以及 [质量测试要求](https://developer.android.com/games/playgames/pc-compatibility?hl=zh-cn#unsupported-features-2) 的要求将里面相关权限删除。目前 Cocos Creator 默认的构建工程并未涉及这些特性或权限。 +5. 删除安卓应用的权限对话框,[详情](https://developer.android.com/games/playgames/pc-compatibility#permissions-dialogs)。 +6. 删除不支持的 Google Play API,[详情](https://developer.android.com/games/playgames/pc-compatibility#unsupported-google-apis)。 +7. 当应用需要读写外部存储时,需要启用分区存储,示例如下: + + - 在工程目录中找到 AndroidManifest.xml: + + ![external-storage/permission.png](external-storage/permission.png) + + - 在添加权限: + + ![external-storage/permission.png](external-storage/external-permissions.png) + + - [详情](https://developer.android.com/games/playgames/pc-compatibility#scoped-storage)。 + +8. 缩放 UI + + Cocos Creator 支持自适应 UI,对于绝大部分移动端游戏来说,分辨率是在应用启动时就确定了,因此不用考虑适配的问题,但是在 GPG 上,由于用户可以通过外界来调整窗口的大小,因此需要单独去适配分辨率。 + + 我们建议您可以选择 **Widget** 组件用,并确保其 **Align Mode** 为 **On_WINDOW_RESIZE** 或 **ALWAYS**。 + + ![scale-ui/scale-ui.png](scale-ui/scale-ui.png) + + - [GPG 界面缩放](https://developer.android.com/games/playgames/graphics?hl=zh-cn#ui-scaling) + - [多分辨率适配方案](../../../ui-system/components/engine/multi-resolution.md) + - [Widget 组件参考](../../../ui-system/components/editor/widget.md) 对子 UI 进行适配 + + 更多详情请查看 [UI 实践指南](../../../ui-system/components/engine/usage-ui.md)。 + + 对于大屏幕的设备的适配工作,您可以参考谷歌 [适用于大屏设备开发的响应式布局](https://developer.android.com/large-screens)。 + + + +9. GPG 要求支持 16:9 的长宽比。为了获得理想的玩家体验,游戏还应支持 21:9、16:10 和 3:2。 +纵向模式的游戏只需要支持 9:16 的长宽比。如果你的游戏缺乏横向支持,Google Play Games会在全屏模式下渲染黑条。同样可以参考上述的 **Widget** 组件部分。 +1. 适配窗口变换,GPG 游戏渲染的分辨率将在游戏启动时、窗口大小重设时、全屏和窗口模型切换时改变游戏的渲染分辨率,[详情](https://developer.android.com/games/playgames/graphics#dynamic-display)。同样可以参考上述的 **Widget** 组件部分。 +2. 为确保用户在移动设备和 PC 之间切换游玩时,游戏进度不会丢失,请检查您的游戏符合谷歌对连续性的要求,请参考[连续性概览](https://developer.android.com/games/playgames/identity?hl=zh-cn) + +## 发布流程 + +GPG 的发布流程和安卓的发布流程类似,您可以参考下列文档以获取发布支持。 + +- [安装和运行](./build-and-run.md) +- [安卓构建示例](../android/build-example.md) +- [原生发布](../native-options.md) + +## 内容 + +- [集成 Input SDK](../gpg-input-sdk.md) + +## 相关链接 + +- [GPG 官方网站](https://developer.android.com/games/playgames/overview?hl=zh-cn) + +## 接口说明 + +如果要在运行时判断是否运行在 HPE 上,代码示例如下: + +```ts +if( sys.hasFeature(sys.Feature.HPE) ) { + ... +} +``` diff --git a/versions/4.0/zh/editor/publish/google-play-games/index/app-abi.png b/versions/4.0/zh/editor/publish/google-play-games/index/app-abi.png new file mode 100644 index 0000000000..806c016d67 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/index/app-abi.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/index/hyper-resolution.png b/versions/4.0/zh/editor/publish/google-play-games/index/hyper-resolution.png new file mode 100644 index 0000000000..5d733ecc7a Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/index/hyper-resolution.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/index/render-backend.png b/versions/4.0/zh/editor/publish/google-play-games/index/render-backend.png new file mode 100644 index 0000000000..84eee6f6be Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/index/render-backend.png differ diff --git a/versions/4.0/zh/editor/publish/google-play-games/scale-ui/scale-ui.png b/versions/4.0/zh/editor/publish/google-play-games/scale-ui/scale-ui.png new file mode 100644 index 0000000000..2d22bb6147 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play-games/scale-ui/scale-ui.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/build-example-google-play.md b/versions/4.0/zh/editor/publish/google-play/build-example-google-play.md new file mode 100644 index 0000000000..21d9cf4a54 --- /dev/null +++ b/versions/4.0/zh/editor/publish/google-play/build-example-google-play.md @@ -0,0 +1,143 @@ +# Google Play构建示例 + +本文将演示 Cocos Creator 项目发布为 Google Play 应用程序的流程。 + +请准备一个至少含有一个场景的 Cocos Creator 项目。 + +![project.png](images/project.png) + +## 发布环境搭建 + +要想发布 Google Play 原生应用,需要安装 Android Studio 开发环境,以及特定版本的 JDK(或者 OpenSDK),Android SDK 和 NDK 等。详情请参考 [配置 Android 原生开发环境](../setup-native-development.md)。 + +## 发布流程 + +接下来,在 Cocos Creator 找到 **项目** 菜单,点击 **构建发布** 按钮,弹出 **构建发布** 面板。 + +### 创建发布任务 + +1. 选择 **项目** -> **构建** 菜单,打开构建面板 + + ![cc-build-menu.png](images/cc-build-menu.png) + +2. 点击面板上的 **新建构建任务** 选项: + + ![new-build-task.png](images/new-build-task.png) + +3. 选择构建平台为 Google Play + + ![select-platform.png](images/select-platform.png) + +4. 选择至少一个场景作为应用载入的首场景,当仅有一个场景时会被默认添加: + + ![start-scene.png](images/start-scene.png) + +5. 启用 ADPF (可选) + + ![enable-adpf.png](images/enable-adpf.png) + +6. 参考 [Android 平台构建选项 - 渲染后端](../native-options.md#%E6%B8%B2%E6%9F%93%E5%90%8E%E7%AB%AF) 选择渲染后端 + + ![render-backend.png](images/render-backend.png) + +7. 填入包名 + + ![game-package-name.png](images/game-package-name.png) + + > 名称规范请参考 [应用 ID 名称](../native-options.md#%E5%BA%94%E7%94%A8-id-%E5%90%8D%E7%A7%B0) + +8. 更换应用 icon (可选) + + ![custom-icon.png](images/custom-icon.png) + +9. 选择 Target API Level + + ![target-api-level.png](images/target-api-level.png) + + > 如果没有下拉框,请检查上面的 **配置 SDK 和 NDK** 是否正确。 + +10. 开启 Google Play Instant (可选) + + ![enable-google-play-instant.png](images/enable-google-play-instant.png) + +11. 开启 Google Play Billing 功能 + + ![enable-google-play-billing.png](images/enable-google-play-billing.png) + + > 不勾选这个无法使用Google Play Billing接口 + +其他选项请参考 [Android 平台构建选项](../native-options.md#android-%E5%B9%B3%E5%8F%B0%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) 进行配置。 + +### 构建与发布 + +1. 构建:点击下方的 **构建** 按钮会创建一个新的构建任务并开始构建 + + ![build.png](images/build.png) + +2. 等待构建完成 + + ![building.png](images/building.png) + +#### 通过 creator 生成 aab 包 +1. 点击 **生成** + + ![make](images/make.png) + +2. 等待生成完成 + + ![making.png](images/making.png) + + +3. 点击下面的按钮打开生成好的工程: + + ![open](images/open.png) + +4. 找到 publish 目录 + + ![find-publish-dir](images/find-publish-dir.png) + +5. 找已生成的aab包 + + ![find-release-aab](images/find-release-aab.png) + +#### 通过 Android Studio 生成 aab 包 +1. 找到工程对应的目录 + + ![find-proj](images/find-proj.png) + +2. 打开 Android Studio 的菜单: + + ![android studio open project menu](images/as-open-menu.png) + +3. 打开已经构建好的项目,`{项目路径}/build/google-play/proj`: + + ![android studio open project](images/as-open-proj.png) + +4. 使用 Android Studio 构建 aab + + 打开 Android Studio 后,会花一段时间进行准备工作,待 Android Studio 将项目准备完成后,即可打包 APK。准备过程可能会耗时较久,如果长时间无响应,请检查网络,或者切换到其他镜像。此时如果您需要中断目前的后台任务,可参考下列关闭方法: + + > Android Studio 有后台任务时,可以点击窗口下方的后台任务栏: + > + > ![background-task.png](./images/background-task.png) + > + > 在弹出窗中点击右侧的 × 以结束后台任务: + > + > ![interrupt-sync.png](images/interrupt-sync.png) + +5. 打开 **Build** 菜单选择 **Build Bundle(s)**: + + ![build-apk.png](images/build-apk.png) + +6. 发布成功后可以在 proj/build 目录内找到 Debug 版本的 APK: + + ![apk.png](images/apk.png) + +## 其他 +其他基本与 android 一致,可以参考[安卓构建示例](../android/build-example-android.md) + +## Google Play Billing +[Google Play Billing 使用文档](./google-play-game-services.md) + +## Google Play 游戏服务 +[Google Play Game Services 使用文档](./google-play-game-services.md) \ No newline at end of file diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/achievement-infos.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/achievement-infos.png new file mode 100644 index 0000000000..8fc2220527 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/achievement-infos.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/application-id.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/application-id.png new file mode 100644 index 0000000000..dbefa9be71 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/application-id.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/config-application-id-en.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/config-application-id-en.png new file mode 100644 index 0000000000..e3990558ad Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/config-application-id-en.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/config-application-id-zh.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/config-application-id-zh.png new file mode 100644 index 0000000000..20c3732c14 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/config-application-id-zh.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/create-achievements.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/create-achievements.png new file mode 100644 index 0000000000..33e0276d15 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/create-achievements.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/credentials-infos.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/credentials-infos.png new file mode 100644 index 0000000000..ae681feeb5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/credentials-infos.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/default-achievements.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/default-achievements.png new file mode 100644 index 0000000000..a71db97f2c Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/default-achievements.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/google-cloud-new-oauth-client-id.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/google-cloud-new-oauth-client-id.png new file mode 100644 index 0000000000..e5a8593df7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/google-cloud-new-oauth-client-id.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/new-credentials.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/new-credentials.png new file mode 100644 index 0000000000..96e574b7d5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/new-credentials.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/no-config-application-id-error.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/no-config-application-id-error.png new file mode 100644 index 0000000000..f265a4f699 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/no-config-application-id-error.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/game-services-images/publish-achievement.png b/versions/4.0/zh/editor/publish/google-play/game-services-images/publish-achievement.png new file mode 100644 index 0000000000..131c24c620 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/game-services-images/publish-achievement.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/google-play-billing-example.md b/versions/4.0/zh/editor/publish/google-play/google-play-billing-example.md new file mode 100644 index 0000000000..227aa4bae7 --- /dev/null +++ b/versions/4.0/zh/editor/publish/google-play/google-play-billing-example.md @@ -0,0 +1,93 @@ +# 使用 Google Play Billing接口 + +## 导入接口 +``` ts +import { google } from 'cc' +``` + +## 初始化 BillingClient +``` ts +import { google } from 'cc' +this._client = google.billing.BillingClient.newBuilder().enablePendingPurchases( + google.billing.PendingPurchasesParams.newBuilder().enableOneTimeProducts().build() + ).setListener({ + onPurchasesUpdated: (billingResult: google.billing.BillingResult, purchases: google.billing.Purchase[]): void => { + // TODO: 购买更新回调 + } + }).build(); +``` + +## 连接到 Google Play + +``` ts +this._client.startConnection({ + onBillingServiceDisconnected: (): void => { + // TODO: 连接失败处理 + }, + onBillingSetupFinished: (billingResult: google.billing.BillingResult): void => { + // TODO: 连接成功处理 + } +}); +``` + +## 展示可供购买的商品 +``` ts +const product = google.billing.QueryProductDetailsParams.Product.newBuilder() + .setProductId("productId") + .setProductType(google.billing.BillingClient.ProductType.INAPP) + .build(); +const params = google.billing.QueryProductDetailsParams.newBuilder().setProductList([product]).build(); +this._client.queryProductDetailsAsync(params, { + onProductDetailsResponse: (billingResult: google.billing.BillingResult, productDetailsList: google.billing.ProductDetails[]): void => { + // TODO: 产品查询回调 + } +}); +``` + +## 启动购买流程 +``` ts +const params = google.billing.BillingFlowParams.newBuilder().setProductDetailsParamsList( + [google.billing.BillingFlowParams.ProductDetailsParams.newBuilder().setProductDetails(productDetails).build()] + ).build(); +this._client.launchBillingFlow(params); +``` + + +## 消耗型商品 +```ts +this._client.consumeAsync(google.billing.ConsumeParams.newBuilder().setPurchaseToken(this._purchases[0].getPurchaseToken()).build(), { + onConsumeResponse: (billingResult: google.billing.BillingResult, token: string): void => { + // TODO: 消耗回调 + } +}); +``` + +## 非消耗型商品 +```ts +this._client.acknowledgePurchase( + google.billing.AcknowledgePurchaseParams.newBuilder().setPurchaseToken(this._purchases[0].getPurchaseToken()).build(), { + onAcknowledgePurchaseResponse: (billingResult: google.billing.BillingResult): void => { + // TODO + } + }); +``` + +## 查询已购买商品 +``` ts +this._client.queryPurchasesAsync( + google.billing.QueryPurchasesParams.newBuilder().setProductType(google.billing.BillingClient.ProductType.INAPP).build(), { + onQueryPurchasesResponse: (billingResult: google.billing.BillingResult, purchaseList: google.billing.Purchase[]) : void => { + // TODO: 已购买商品回调 + } +}); +``` + + +## 查询用户的结算配置 +``` ts +this._client.getBillingConfigAsync(google.billing.GetBillingConfigParams.newBuilder().build(), { + onBillingConfigResponse: (billingResult: google.billing.BillingResult, billingConfig: google.billing.BillingConfig) : void => { + // TODO: 获取用户配置回调 + } +}); +``` \ No newline at end of file diff --git a/versions/4.0/zh/editor/publish/google-play/google-play-game-services.md b/versions/4.0/zh/editor/publish/google-play/google-play-game-services.md new file mode 100644 index 0000000000..f3afe32897 --- /dev/null +++ b/versions/4.0/zh/editor/publish/google-play/google-play-game-services.md @@ -0,0 +1,170 @@ +# Google Play 构建示例 + +本文需要在 Google Play 平台才可以使用。参考[发布 Google Play 示例](./build-example-google-play.md) + +### 自动登录 +当玩家启动启用了自动登录功能的游戏时,他们无需与登录提示互动即可登录游戏。玩家可以在 Google Play 游戏应用中或在游戏中显示的初始登录提示中启用自动登录功能。 + +#### Google Cloud 后台配置 + +- 创建凭据 + + ![new credentials](game-services-images/google-cloud-new-oauth-client-id.png) + + 需要在 Google Cloud 创建 OAuth 客户端 ID ,才能在 Google Console 里配置凭证里可以看到。 + +### Google Play Console 后台配置 + +[参考文档](https://developer.android.com/games/pgs/console/setup?hl=zh-cn#add_your_game_to_the) + +1. 新增凭证 + + ![new credentials](game-services-images/new-credentials.png) + +2. 填入凭证信息 + + ![credentials infomations](game-services-images/credentials-infos.png) + +3. 创建成功即可 + + ![application id](game-services-images/application-id.png) + + 红框选中的应用程式 ID,就是下面需要填写的应用 id + +4. 构建 Google Play 平台后,需要用户配置应用 id。 + + 如果是英文的应用,配置英文: + + ![config application id](game-services-images/config-application-id-en.png) + + 如果是中文的应用,配置为中文: + + ![config application id](game-services-images/config-application-id-zh.png) + + 也可以同时配置中文和英文。 + + 如果没有配置,会有如下错误: + + ![no-config-application-id-error](game-services-images/no-config-application-id-error.png) + +#### 使用实例 +1. 初始化 SDK: + + ``` typescript + import { google } from 'cc'; + // 初始化 Play 游戏 SDK + google.play.PlayGamesSdk.initialize(); + ``` + +2. 获取登录结果 + ``` typescript + gameSignInClient.isAuthenticated().addOnCompleteListener({ + onComplete: (isAuthenticatedTask: google.play.Task) => { + const isAuthenticated = + (isAuthenticatedTask.isSuccessful() && + isAuthenticatedTask.getResult().isAuthenticated()); + if (isAuthenticated) { + // Continue with Play Games Services + } else { + // Disable your integration with Play Games Services or show a + // login button to ask players to sign-in. Clicking it should + // call GamesSignInClient.signIn(). + } + }, + }); + ``` + +### 成就 +成就是在游戏中提高用户互动度的绝佳方式。您可以在游戏中实现成就功能,以鼓励玩家尝试其通常不使用的功能,或者在游戏中运用完全不同的玩法。通过成就功能,玩家还可以相互比较游戏进度,轻松开展趣味性竞争。 + +#### google 后台配置 + +1. 创建成就 + + [create-achievements](game-services-images/create-achievements.png) + +2. 填写成就信息 + + ![achievement-infos](game-services-images/achievement-infos.png) + +3. 发布成就信息 + + ![publish-achievement](game-services-images/publish-achievement.png) + +#### 使用实例: + +1. 显示成就: + + ``` typescript + import { google } from 'cc'; + // 显示默认成就界面 + google.play.PlayGames.getAchievementsClient().showAchievements(); + ``` + + 显示效果如下图: + + ![default achievements](game-services-images/default-achievements.png) + +2. 解锁成就: + + ``` typescript + import { google } from 'cc'; + google.play.PlayGames.getAchievementsClient().unlock("achievementId"); + ``` + + 如果成就属于增量类型(即需要执行几个步骤才能解锁),请改为调用 AchievementsClient.increment() + + ``` typescript + import { google } from 'cc'; + google.play.PlayGames.getAchievementsClient().increment("achievementId", 1); + ``` + + 完成的步骤达到要求后,Google Play 游戏服务便会自动解锁成就。 + +3. 加载所有成就信息: + + ``` typescript + import { google } from 'cc'; + const achievementsClient = google.play.PlayGames.getAchievementsClient(); + achievementsClient.load(false).addOnSuccessListener({ + onSuccess: (data: google.play.AnnotatedData) => { + this.addLog("isStale : " + data.isStale()); + const achievements = data.get(); + for(let i = 0; i < achievements.getCount(); ++i) { + str += `getCurrentSteps : ${achievements.get(i).getCurrentSteps()}\n`; + str += `getState : ${achievements.get(i).getState()}\n`; + str += `getTotalSteps : ${achievements.get(i).getTotalSteps()}\n`; + str += `getType : ${achievements.get(i).getType()}\n`; + str += `getLastUpdatedTimestamp : ${achievements.get(i).getLastUpdatedTimestamp()}\n`; + str += `getXpValue : ${achievements.get(i).getXpValue()}\n`; + str += `getAchievementId : ${achievements.get(i).getAchievementId()}\n`; + str += `getDescription : ${achievements.get(i).getDescription()}\n`; + str += `getFormattedCurrentSteps : ${achievements.get(i).getFormattedCurrentSteps()}\n`; + str += `getFormattedTotalSteps : ${achievements.get(i).getFormattedTotalSteps()}\n`; + str += `getName : ${achievements.get(i).getName()}\n`; + str += `getRevealedImageUrl : ${achievements.get(i).getRevealedImageUrl()}\n`; + str += `getUnlockedImageUrl : ${achievements.get(i).getUnlockedImageUrl()}\n\n`; + } + console.log(str); + } + }); + ``` + +4. 显示隐藏成就: + + ``` typescript + import { google } from 'cc'; + google.play.PlayGames.getAchievementsClient().reveal(); + ``` + +### Recall Api +若要使用正确的信息与 Google 的服务器进行通信,您需向客户端 SDK 请求 Recall 会话 ID,并将该 ID 发送到游戏服务器。 +``` typescript +const recallClient = google.play.PlayGames.getRecallClient(); +recallClient.requestRecallAccess().addOnSuccessListener({ + onSuccess: (recallAccess: google.play.RecallAccess) => { + const recallSessionId = recallAccess.getSessionId(); + // Send the recallSessionId to your game server + } +}) +``` \ No newline at end of file diff --git a/versions/4.0/zh/editor/publish/google-play/images/apk.png b/versions/4.0/zh/editor/publish/google-play/images/apk.png new file mode 100644 index 0000000000..759cfb4137 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/apk.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/as-open-menu.png b/versions/4.0/zh/editor/publish/google-play/images/as-open-menu.png new file mode 100644 index 0000000000..b1f57b5d4c Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/as-open-menu.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/as-open-proj.png b/versions/4.0/zh/editor/publish/google-play/images/as-open-proj.png new file mode 100644 index 0000000000..80996bac67 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/as-open-proj.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/background-task.png b/versions/4.0/zh/editor/publish/google-play/images/background-task.png new file mode 100644 index 0000000000..0977698682 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/background-task.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/build-apk.png b/versions/4.0/zh/editor/publish/google-play/images/build-apk.png new file mode 100644 index 0000000000..393c5647a9 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/build-apk.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/build.png b/versions/4.0/zh/editor/publish/google-play/images/build.png new file mode 100644 index 0000000000..ac71db0127 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/build.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/building.png b/versions/4.0/zh/editor/publish/google-play/images/building.png new file mode 100644 index 0000000000..c4244578aa Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/building.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/cc-build-menu.png b/versions/4.0/zh/editor/publish/google-play/images/cc-build-menu.png new file mode 100644 index 0000000000..144808df77 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/cc-build-menu.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/custom-icon.png b/versions/4.0/zh/editor/publish/google-play/images/custom-icon.png new file mode 100644 index 0000000000..7677b19263 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/custom-icon.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/enable-adpf.png b/versions/4.0/zh/editor/publish/google-play/images/enable-adpf.png new file mode 100644 index 0000000000..82f80cf178 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/enable-adpf.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/enable-google-play-billing.png b/versions/4.0/zh/editor/publish/google-play/images/enable-google-play-billing.png new file mode 100644 index 0000000000..0c7853f8ff Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/enable-google-play-billing.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/enable-google-play-instant.png b/versions/4.0/zh/editor/publish/google-play/images/enable-google-play-instant.png new file mode 100644 index 0000000000..ecd1d16bdf Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/enable-google-play-instant.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/find-proj.png b/versions/4.0/zh/editor/publish/google-play/images/find-proj.png new file mode 100644 index 0000000000..afd2da6796 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/find-proj.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/find-publish-dir.png b/versions/4.0/zh/editor/publish/google-play/images/find-publish-dir.png new file mode 100644 index 0000000000..9f22dd0b5c Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/find-publish-dir.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/find-release-aab.png b/versions/4.0/zh/editor/publish/google-play/images/find-release-aab.png new file mode 100644 index 0000000000..c3b0bfb178 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/find-release-aab.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/game-package-name.png b/versions/4.0/zh/editor/publish/google-play/images/game-package-name.png new file mode 100644 index 0000000000..07a0862ebb Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/game-package-name.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/interrupt-sync.png b/versions/4.0/zh/editor/publish/google-play/images/interrupt-sync.png new file mode 100644 index 0000000000..f10fdbe28d Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/interrupt-sync.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/make.png b/versions/4.0/zh/editor/publish/google-play/images/make.png new file mode 100644 index 0000000000..d4986a7f7f Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/make.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/making.png b/versions/4.0/zh/editor/publish/google-play/images/making.png new file mode 100644 index 0000000000..1919eb39f2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/making.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/new-build-task.png b/versions/4.0/zh/editor/publish/google-play/images/new-build-task.png new file mode 100644 index 0000000000..db9de33d78 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/new-build-task.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/open.png b/versions/4.0/zh/editor/publish/google-play/images/open.png new file mode 100644 index 0000000000..8c4841ae51 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/open.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/project.png b/versions/4.0/zh/editor/publish/google-play/images/project.png new file mode 100644 index 0000000000..2bd2bef86c Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/project.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/render-backend.png b/versions/4.0/zh/editor/publish/google-play/images/render-backend.png new file mode 100644 index 0000000000..651d515276 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/render-backend.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/select-platform.png b/versions/4.0/zh/editor/publish/google-play/images/select-platform.png new file mode 100644 index 0000000000..c9df8f1978 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/select-platform.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/start-scene.png b/versions/4.0/zh/editor/publish/google-play/images/start-scene.png new file mode 100644 index 0000000000..e45fbe8434 Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/start-scene.png differ diff --git a/versions/4.0/zh/editor/publish/google-play/images/target-api-level.png b/versions/4.0/zh/editor/publish/google-play/images/target-api-level.png new file mode 100644 index 0000000000..b6006ef0ee Binary files /dev/null and b/versions/4.0/zh/editor/publish/google-play/images/target-api-level.png differ diff --git a/versions/4.0/zh/editor/publish/gpg-input-sdk.md b/versions/4.0/zh/editor/publish/gpg-input-sdk.md new file mode 100644 index 0000000000..c64ab330f6 --- /dev/null +++ b/versions/4.0/zh/editor/publish/gpg-input-sdk.md @@ -0,0 +1,150 @@ +# 集成 Input SDK + +Input SDK提供了一个统一的界面,让玩家在 Google Play Games For PC 上方便地找到游戏的鼠标和键盘绑定,从而提升玩家的游戏体验。 + +## 准备工作 + +如果你要在一个全新的 Android 构建上使用 Input SDK,只需要在构建面板上勾选 Input SDK 选项。 + +如果这个 Anrdoid 工程是之前构建的,就需要手动在 文件 `native/engine/android/app/build.gradle` 中添加下面的代码: + +```txt +dependencies { + implementation 'com.google.android.libraries.play.games:inputmapping:1.0.0-beta' + ... +} +``` + +## 添加 InputMapProvider 实现 + +新建类文件 `MyInputMapProvider.java`。 + +```java + public class MyInputMapProvider implements InputMappingProvider { + public enum InputEventIds { + JUMP, + LEFT, + RIGHT, + USE, + SPECIAL_JUMP, + SPECIAL_DUCK, + CMB_MOVE, + CMB_DASH, + CMB_WAYPOINT + } + + @Override + public InputMap onProvideInputMap() { + InputAction jumpInputAction = InputAction.create( + getString(R.string.key_jump), + InputEventIds.JUMP.ordinal(), + InputControls.create( + Collections.singletonList(KeyEvent.KEYCODE_SPACE), + Collections.emptyList() + ) + ); + InputAction leftAction = InputAction.create( + getString(R.string.key_Left), + InputEventIds.LEFT.ordinal(), + InputControls.create( + Collections.singletonList(KeyEvent.KEYCODE_DPAD_LEFT), + Collections.emptyList() + ) + ); + InputAction rightAction = InputAction.create( + getString(R.string.key_Right), + InputEventIds.RIGHT.ordinal(), + InputControls.create( + Collections.singletonList(KeyEvent.KEYCODE_DPAD_RIGHT), + Collections.emptyList() + ) + ); + + InputAction cmbMove = InputAction.create( + getString(R.string.key_Move), + InputEventIds.CMB_MOVE.ordinal(), + InputControls.create( + Collections.emptyList(), + Collections.singletonList(InputControls.MOUSE_RIGHT_CLICK) + ) + ); + InputAction cmbDash = InputAction.create( + getString(R.string.key_Dash), + InputEventIds.CMB_DASH.ordinal(), + InputControls.create( + Arrays.asList(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SPACE), + Collections.emptyList() + ) + ); + InputAction cmbWaypoint = InputAction.create( + getString(R.string.key_Waypoint), + InputEventIds.CMB_WAYPOINT.ordinal(), + InputControls.create( + Collections.singletonList(KeyEvent.KEYCODE_SHIFT_LEFT), + Collections.singletonList(InputControls.MOUSE_RIGHT_CLICK) + ) + ); + InputGroup movementInputGroup = InputGroup.create( + getString(R.string.key_BasicMove), + Arrays.asList(jumpInputAction, leftAction, rightAction, cmbMove, cmbDash, cmbWaypoint) + ); + return InputMap.create( + Arrays.asList(movementInputGroup), + MouseSettings.create(true, true) + ); + } + } + +``` + +## 添加按键的描述 + +在 `res\values\strings.xml` 中定义用于国际化的文本,以供前面步骤中使用。 + +```xml + + ... + Jump + Left + Right + Move + Dash + Add Waypoint + Basic Movement + +``` + +## 修改 `AppActivity` 注册 `InputMapping` + +`AppActivity.java` 位于 `native\engine\android\app\src\main\cocos\game`,需做如下修改。 + +```java + ... + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ... + + InputMappingClient inputMappingClient = Input.getInputMappingClient(this); + inputMappingClient.setInputMappingProvider(new MyInputMapProvider()); + } + ... + + @Override + protected void onDestroy() { + InputMappingClient inputMappingClient = Input.getInputMappingClient(this); + inputMappingClient.clearInputMappingProvider(); + + super.onDestroy(); + ... + } + +``` + +## 测试效果 + +在 Google Play Games 提供的 HPE 模拟器中运行,通过 Shift + Tab 调出页面。 + +![key codes](./publish-native/gpg-input-sdk-keys.png) + +更多的细节请参考 [Input SDK 官方文档](https://developer.android.com/games/playgames/input-sdk-start)。 diff --git a/versions/4.0/zh/editor/publish/huawei-mini-game/build_options.jpg b/versions/4.0/zh/editor/publish/huawei-mini-game/build_options.jpg new file mode 100644 index 0000000000..cc37f85696 Binary files /dev/null and b/versions/4.0/zh/editor/publish/huawei-mini-game/build_options.jpg differ diff --git a/versions/4.0/zh/editor/publish/huawei-mini-game/rpk.png b/versions/4.0/zh/editor/publish/huawei-mini-game/rpk.png new file mode 100644 index 0000000000..7fbbf875ab Binary files /dev/null and b/versions/4.0/zh/editor/publish/huawei-mini-game/rpk.png differ diff --git a/versions/4.0/zh/editor/publish/index.md b/versions/4.0/zh/editor/publish/index.md new file mode 100644 index 0000000000..3e096d5374 --- /dev/null +++ b/versions/4.0/zh/editor/publish/index.md @@ -0,0 +1,48 @@ +# 跨平台发布游戏 + +得益于 Cocos Creator 的双内核引擎架构(C++ 内核用于原生平台,TS 内核用于 Web和小游戏平台),使得使用 Cocos Creator 制作的项目可以在原生平台和 Web 平台和小游戏平台上都运行良好。真正实现一次开发,全平台运行。 + +## 发布平台 + +目前 Cocos Creator 支持发布到以下平台: +- [发布 iOS 应用](./ios/index.md) +- [发布 Android 应用](./android/index.md) +- [发布 HUAWEI AppGallery Connect 应用程序](./publish-huawei-agc.md) +- [发布 HUAWEI HarmonyOS 应用](./publish-huawei-ohos.md) +- [发布 HarmonyOS Next 应用](./publish-openharmony.md) +- [发布到 Google Play](google-play/build-example-google-play.md) +- [发布到 Google Play On Games](google-play-games/index.md) +- [发布 Windows 应用](./windows/index.md) +- [发布 macOS 应用](./ios/index.md) +- [发布 Web 移动端 - H5](./publish-web.md) +- [发布 Web 桌面端](publish-web.md) +- [发布 Facebook Instant Games](./publish-fb-instant-games.md) +- [发布微信小游戏](./publish-wechatgame.md) +- [发布淘宝小程序创意互动](./publish-taobao-creative-app.md) +- [发布抖音小游戏](./publish-bytedance-mini-game.md) +- [发布 OPPO 小游戏](./publish-oppo-mini-game.md) +- [发布华为快游戏](./publish-huawei-quick-game.md) +- [发布 vivo 小游戏](./publish-vivo-mini-game.md) +- [发布荣耀小游戏](./publish-honor-mini-game.md) + +## 准备工作 + +在项目正常开发,预览效果达到要求的情况下,可以将游戏发布到多个平台。在发布之前需要做的准备工作包括: + +- [熟悉构建发布面板](build-panel.md) +- [了解通用构建选项](build-options.md) + +## 进阶 + +如果对构建流程有了一定程度上的熟悉和了解,就可以自定义构建模板、自定义构建流程等。详情可参考以下文档: + +- [构建流程简介与常见错误处理](build-guide.md) +- [定制项目的构建模版](custom-project-build-template.md) +- [自定义构建流程](custom-build-plugin.md) + +开发者还可以通过命令行发布项目,详情请参考 [命令行发布项目](publish-in-command-line.md)。 + +## 更多资源 + +- [Cocos 技术支持](https://www.cocos.com/assistant) +- [Cocos 官方论坛](https://forum.cocos.org/) diff --git a/versions/4.0/zh/editor/publish/ios/build-example-ios.md b/versions/4.0/zh/editor/publish/ios/build-example-ios.md new file mode 100644 index 0000000000..628e863f77 --- /dev/null +++ b/versions/4.0/zh/editor/publish/ios/build-example-ios.md @@ -0,0 +1,85 @@ +# iOS 构建示例 + +本文将演示 Cocos Creator 项目发布为 iOS 应用程序的流程,需要以下准备工作: +- 一台安装了 XCode 的 MacOS 设备 +- 一个苹果开发者账号 + +## 发布流程 + +### 注册开发者账号 + +首先,需要拥有一个苹果开发者账号,如果没有,请先前往 [注册页面](https://appleid.apple.com/account) 进行注册。 + +### 确认 macOS 系统 和 Xcode 版本 + +Cocos Creator 打包环境要求: +- Xcode 版本为 **11.5** 及以上。 +- macOS 版本为 **10.14** 及以上。 + +> **注意**:默认情况下 AppStore 中对应的 Xcode 与系统匹配,如果要使用特定版本的 Xcode,可前往 [Xcode 下载页](https://developer.apple.com/xcode/download/) 下载。 + +### 准备测试项目 + +打开一个已有的项目,或者新建一个测试项目。 + +### 构建 + +![project-build-menu](./images/project-build-menu.png) + +如上图所示,在 Cocos Creator 顶部菜单中选择 **项目(Project)** -> **构建(Build)**,可打开下面的构建面板。 + +![ios-build-panel](./images/ios-build-panel.png) + +#### 配置通用选项 + +上图中,左边蓝色部分为 Cocos Creator 支持的所有平台都需要配置的公共参数,可以前往 [通用发布配置](./../build-options.md) 查看详情。 + +#### 配置 iOS 专有选项 + +平台(Platform)选择为 **iOS**,面板向下滑动,可以看到右边红色的部分,这是原生(Native)平台和 iOS 平台特有的配置,详情请查看 [原生平台通用构建选项](./../native-options.md) 和 [iOS 发布选项](./build-options-ios.md)。 + +#### 填写包名 + +包名(Bundle Identifier)是必须配置的选项,一般使用 `com.mycompany.myproduct` 组合,比如,本示例用的是 com.cocos.testios。 + +#### 执行构建 + +配置完成后,点击 **构建发布(Build)** 按钮,即可生成 Xcode 项目。 + +构建成功后,可以点击如下所示的打开文件按钮,打开生成的项目路径。 + +![ios-build-open-path](./images/ios-build-open-path.png) + +如果未更改过生成路径,可在项目目录下找到 `build/ios/proj` 目录,内容如下所示: + +![ios-xcode-folder](./images/ios-xcode-folder.png) + +### 在 Xcode 中编译运行 + +双击 `build/ios/proj/项目名称.xcodeproj`,即可用 Xcode 打开项目。 + +![ios-xcode-showcase](./images/ios-xcode-showcase.png) + +选择如图所示的 **项目名称-mobile** 作为编译目标,并选择一个适合的模拟器或者已连接的 iOS 设备,点击编译并运行按钮,即可启动项目。 + +![ios-run](./images/ios-run.png) + +## 进阶 + +### 脚本与原生通信 + +有时候项目需要从脚本层调用一些 iOS 的系统功能,或者当集成了某个第三方的 iOS SDK 后,调要通过脚本代码调用其 API,此时就需要用到脚本层与原生层的通信机制。 + +通信机制请参考: +- [基于反射机制实现 JavaScript 与 iOS/macOS 系统原生通信](../../../advanced-topics/oc-reflection.md) +- [使用 JsbBridge 实现 JavaScript 与 Objective-C 通信](./../../../advanced-topics/js-oc-bridge.md) + +### 在原生环境调试代码 + +有一些问题,只有在特定设备上才会出现,如果能够在对应设备上调试代码,就能快速定位问题,找到解决方案。 + +Cocos Creator 提供了原生调试机制,可以很方便地在设备上调试游戏代码,请参考:[原生平台 JavaScript 调试](./../debug-jsb.md)。 + +## 特性与系统版本 + +不同的特性会依赖特定的系统版本,请往 [特性与系统版本](./../../../advanced-topics/supported-versions.md) 查看详情。 diff --git a/versions/4.0/zh/editor/publish/ios/build-options-ios.md b/versions/4.0/zh/editor/publish/ios/build-options-ios.md new file mode 100644 index 0000000000..8930beb39e --- /dev/null +++ b/versions/4.0/zh/editor/publish/ios/build-options-ios.md @@ -0,0 +1,52 @@ +# iOS 平台构建选项 + +![ios-build-options](./images/ios-build-options.png) + +## 配置选项 + +### 可执行文件名 + +用于指定应用程序主执行文件名称的字段,这个字段被存储在应用的 Info.plist 文件中。如果未填写,系统将根据应用名称字段生成默认值。值的设定只能包含数字、字母、下划线(_)以及减号(-)。 + +### 应用 ID 名称(Bundle Identifier) + +包名,通常以产品网站 URL 倒序排列,如 `com.mycompany.myproduct`。 + +> **注意**:包名中只能包含数字(0~9)、字母(A~Z、a~z)、中划线(-)和点(.),此外包名最后一部分必须以字母开头,不能以下划线或数字开头。详情请参考 [包的唯一标识符](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier)。 + +### 跳过 Xcode 工程的更新 + + +在默认情况下,每次构建都会执行 CMake 命令以生成 Xcode 工程。然而,如果对生成的 Xcode 工程进行了修改或配置,例如通过 CocoaPods 集成了 SDK,这可能会引起问题,因为这些修改在下次构建时会被还原。 + +如果选中这个选项,后续的构建将不再更新或覆盖 Xcode 工程的配置。 + +要注意的是,与 CMake 相关的其他修改,例如添加C++源代码,也不会再触发 Xcode 工程的重新生成。 + +### 屏幕方向 + +屏幕方向目前包括 Portrait、Landscape Left、Landscape Right 三种。 + +- Portrait:屏幕直立,Home 键在下 +- Landscape Left:屏幕横置,Home 键在屏幕左侧 +- Landscape Right:屏幕横置,Home 键在屏幕右侧 + +### 目标系统 + +默认启动的系统类型: +- iPhone OS: 真机运行。 +- iOS Simulator: iOS 模拟运行。 + +这个只是默认值,可以在 Xcode 中随时修改。 + +### 渲染后端 + +**渲染后端** 目前支持 **METAL**,详情可参考官方文档 [Metal](https://developer.apple.com/cn/metal/)。 + +### 开发者 + +该项用于配置构建编译 iOS 工程时的 Development Team 签名信息。若使用 Xcode 编译时,在 Xcode 中手动配置了签名信息,则以 Xcode 中的配置为准。当执行重新构建时,该项的值会覆盖 Xcode 中配置的值。 + +### 目标版本 + +该项主要用于指定发布 iOS 平台时的 iOS 软件版本,默认值为 **12.0**。构建后版本号会记录在发布包目录 `proj/cfg.cmake` 文件的 `TARGET_IOS_VERSION` 字段中。 diff --git a/versions/4.0/zh/editor/publish/ios/images/ios-build-open-path.png b/versions/4.0/zh/editor/publish/ios/images/ios-build-open-path.png new file mode 100644 index 0000000000..5c8fb969ce Binary files /dev/null and b/versions/4.0/zh/editor/publish/ios/images/ios-build-open-path.png differ diff --git a/versions/4.0/zh/editor/publish/ios/images/ios-build-options.png b/versions/4.0/zh/editor/publish/ios/images/ios-build-options.png new file mode 100644 index 0000000000..1a3368fa05 Binary files /dev/null and b/versions/4.0/zh/editor/publish/ios/images/ios-build-options.png differ diff --git a/versions/4.0/zh/editor/publish/ios/images/ios-build-panel-a.png b/versions/4.0/zh/editor/publish/ios/images/ios-build-panel-a.png new file mode 100644 index 0000000000..0054002269 Binary files /dev/null and b/versions/4.0/zh/editor/publish/ios/images/ios-build-panel-a.png differ diff --git a/versions/4.0/zh/editor/publish/ios/images/ios-build-panel-b.png b/versions/4.0/zh/editor/publish/ios/images/ios-build-panel-b.png new file mode 100644 index 0000000000..e4148b4fdc Binary files /dev/null and b/versions/4.0/zh/editor/publish/ios/images/ios-build-panel-b.png differ diff --git a/versions/4.0/zh/editor/publish/ios/images/ios-build-panel.png b/versions/4.0/zh/editor/publish/ios/images/ios-build-panel.png new file mode 100644 index 0000000000..bb063c6fd6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/ios/images/ios-build-panel.png differ diff --git a/versions/4.0/zh/editor/publish/ios/images/ios-run.png b/versions/4.0/zh/editor/publish/ios/images/ios-run.png new file mode 100644 index 0000000000..f995fcb15e Binary files /dev/null and b/versions/4.0/zh/editor/publish/ios/images/ios-run.png differ diff --git a/versions/4.0/zh/editor/publish/ios/images/ios-xcode-folder.png b/versions/4.0/zh/editor/publish/ios/images/ios-xcode-folder.png new file mode 100644 index 0000000000..e1a7dd0e68 Binary files /dev/null and b/versions/4.0/zh/editor/publish/ios/images/ios-xcode-folder.png differ diff --git a/versions/4.0/zh/editor/publish/ios/images/ios-xcode-showcase.png b/versions/4.0/zh/editor/publish/ios/images/ios-xcode-showcase.png new file mode 100644 index 0000000000..ae439ebaae Binary files /dev/null and b/versions/4.0/zh/editor/publish/ios/images/ios-xcode-showcase.png differ diff --git a/versions/4.0/zh/editor/publish/ios/images/project-build-menu.png b/versions/4.0/zh/editor/publish/ios/images/project-build-menu.png new file mode 100644 index 0000000000..d5cec7521f Binary files /dev/null and b/versions/4.0/zh/editor/publish/ios/images/project-build-menu.png differ diff --git a/versions/4.0/zh/editor/publish/ios/index.md b/versions/4.0/zh/editor/publish/ios/index.md new file mode 100644 index 0000000000..9e8958d850 --- /dev/null +++ b/versions/4.0/zh/editor/publish/ios/index.md @@ -0,0 +1,21 @@ +# iOS 构建指引 + +Cocos Creator 支持发布为 iOS 应用,但至少需要一台支持 macOS 的设备以及苹果开发者账号。 + +## 主要内容 + +- [iOS 构建示例](./build-example-ios.md) +- [iOS 构建选项介绍](build-options-ios.md) +- [基于反射机制实现 JavaScript 与 iOS/macOS 系统原生通信](../../../advanced-topics/oc-reflection.md) +- [使用 JsbBridge 实现 JavaScript 与 Objective-C 通信](../../../advanced-topics/js-oc-bridge.md) +- [特性与系统版本](../../../advanced-topics/supported-versions.md) + +## 相关阅读 + +- [安装配置原生开发环境](../setup-native-development.md) +- [构建发布面板](../build-panel.md) +- [通用构建选项介绍](../build-options.md) +- [原生平台通用构建选项](../native-options.md) +- [原生平台 JavaScript 调试](../debug-jsb.md) +- [构建流程简介与常见错误处理](../build-guide.md) +- [原生平台二次开发指南](../../../advanced-topics/native-secondary-development.md) diff --git a/versions/4.0/zh/editor/publish/mac/build-example-mac.md b/versions/4.0/zh/editor/publish/mac/build-example-mac.md new file mode 100644 index 0000000000..ecd79041f4 --- /dev/null +++ b/versions/4.0/zh/editor/publish/mac/build-example-mac.md @@ -0,0 +1,105 @@ +# macOS 构建示例 + +本文将演示 Cocos Creator 项目发布为 macOS 应用程序的流程,需要以下准备工作: +- 一台 安装了 XCode 的 MacOS 设备 +- 一个苹果开发者账号 + +## 发布流程 + +### 注册开发者账号 + +首先,需要拥有一个苹果开发者账号,如果没有,请先前往 [注册页面](https://appleid.apple.com/account) 进行注册。 + +### 确认 macOS 系统 和 Xcode 版本 + +Cocos Creator 打包环境要求: +- Xcode 版本为 **11.5** 及以上。 +- macOS 版本为 **10.14** 及以上。 + +> **注意**:默认情况下 AppStore 中对应的 Xcode 与系统匹配,如果要使用特定版本的 Xcode,可前往 [Xcode 下载页](https://developer.apple.com/xcode/download/) 下载。 + +### 准备测试项目 + +打开一个已有的项目,或者新建一个测试项目 + +### 构建 + +![project-build-menu](./images/project-build-menu.png) + +如上图所示,在 Cocos Creator 顶部菜单中选择 **项目(Project)** -> **构建(Build)**,可打开下面的构建面板。 + +![build-panel-mac](./images/build-panel-mac.png) + +#### 配置通用选项 + +上图中,左边部分为 Cocos Creator 支持的所有平台都需要配置的公共参数,可以前往 [通用发布配置](./../build-options.md) 查看详情。 + +#### 配置 macOS 专有选项 + +平台(Platform)选择为 **Mac**,面板向下滑动,可以看到如右图所示的原生(Native)平台和 macOS 平台特有的配置,详情请查看 [原生平台通用构建选项](./../native-options.md) 和 [macOS 发布选项](./build-options-mac.md)。 + +#### 填写包名 + +包名(Bundle Identifier)是必须配置的选项,一般使用 `com.mycompany.myproduct` 组合,比如,本示例用的是 com.cocos.mac。 + +#### 执行构建 + +配置完成后,点击 **构建发布(Build)** 按钮,即可生成 Xcode 项目。 + +构建成功后,可以点击如下所示的打开文件按钮,打开生成的项目路径。 + +![build-open-path-mac](./images/build-open-path-mac.png) + +如果未更改过生成路径,可在同目录下找到 build/mac/proj 目录,内容如下所示: + +![xcode-folder-mac](./images/xcode-folder-mac.png) + +### 在 Xcode 中编译运行 + +双击 `build/mac/proj/项目名称.xcodeproj`,即可用 Xcode 打开项目。 + +![xcode-showcase-mac](./images/xcode-showcase-mac.png) + +选择如图所示的 **项目名称-desktop** 作为编译目标,点击编译并运行按钮,即可启动项目。 + +![run-mac](./images/run-mac.png) + +### 修改分辨率 + +可以通过 `native/engine/common/classes/Game.cpp` 中的 `_windowInfo` 进行修改: + +默认分辨率为 800 x 600,我们以把分辨率改为 800 x 400 为例。 + +```C++ +int Game::init() { + _windowInfo.title = GAME_NAME; + // configure window size + _windowInfo.width = 800; + _windowInfo.height = 400; +} +``` + +修改完成后,在 Xcode 中再次编译运行,效果如下图所示: + +![run-mac-800to400](./images/run-mac-800to400.png) + +## 进阶 + +### 脚本与原生通信 + +有时候项目需要从脚本层调用一些 iOS 的系统功能,或者当集成了某个第三方的 iOS SDK 后,调要通过脚本代码调用其 API,此时就需要用到脚本层与原生层的通信机制。 + +完整的通信机制请参考: + +- [基于反射机制实现 JavaScript 与 iOS/macOS 系统原生通信](../../../advanced-topics/oc-reflection.md)。 +- [使用 JsbBridge 实现 JavaScript 与 Objective-C 通信](./../../../advanced-topics/js-oc-bridge.md) + +### 在原生环境调试代码 + +有一些问题,只有在特定设备上才会出现,如果能够在对应设备上调试代码,就能快速定位问题,找到解决方案。 + +Cocos Creator 提供了原生调试机制,可以很方便地在设备上调试游戏代码,请参考:[原生平台 JavaScript 调试](./../debug-jsb.md) + +## 特性与系统版本 + +不同的特性会依赖特定的系统版本,请往 [特性与系统版本](./../../../advanced-topics/supported-versions.md)查看详情。 diff --git a/versions/4.0/zh/editor/publish/mac/build-options-mac.md b/versions/4.0/zh/editor/publish/mac/build-options-mac.md new file mode 100644 index 0000000000..a2c580e761 --- /dev/null +++ b/versions/4.0/zh/editor/publish/mac/build-options-mac.md @@ -0,0 +1,34 @@ +# Mac 平台构建选项 + +![build-options-mac](./images/build-options-mac.png) + +## 可执行文件名 + +用于指定应用程序主执行文件名称的字段,这个字段被存储在应用的 Info.plist 文件中。如果未填写,系统将根据应用名称字段生成默认值。值的设定只能包含数字、字母、下划线(_)以及减号(-)。 + +## Bundle Identifier + +包名,通常以产品网站 URL 倒序排列,如 `com.mycompany.myproduct`。 + +> **注意**:包名中只能包含数字(0~9)、字母(A~Z、a~z)、中划线(-)和点(.),此外包名最后一部分必须以字母开头,不能以下划线或数字开头。详情请参考 [包的唯一标识符](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier)。 + +## 目标版本 + +该项主要用于指定发布 Mac 平台时的 macOS 系统版本,默认值为 **10.14**。构建后版本号会记录在发布包目录 `proj/cfg.cmake` 文件的 `TARGET_OSX_VERSION` 字段中。 + +## Support Apple Silicon + +该项用于更好地提示一些已知的引擎模块在 Apple M1(Silicon)架构设备上的支持问题。 + +## 跳过 Xcode 工程的更新 + +在默认情况下,每次构建都会执行 CMake 命令以生成 Xcode 工程。然而,如果对生成的 Xcode 工程进行了修改或配置,例如通过 CocoaPods 集成了 SDK,这可能会引起问题,因为这些修改在下次构建时会被还原。 + +如果选中这个选项,后续的构建将不再更新或覆盖 Xcode 工程的配置。 + +要注意的是,与 CMake 相关的其他修改,例如添加C++源代码,也不会再触发 Xcode 工程的重新生成。 + + +### 渲染后端 + +**渲染后端** 目前支持 **METAL**,详情可参考官方文档 [Metal](https://developer.apple.com/cn/metal/)。 diff --git a/versions/4.0/zh/editor/publish/mac/images/build-open-path-mac.png b/versions/4.0/zh/editor/publish/mac/images/build-open-path-mac.png new file mode 100644 index 0000000000..d9c17541cf Binary files /dev/null and b/versions/4.0/zh/editor/publish/mac/images/build-open-path-mac.png differ diff --git a/versions/4.0/zh/editor/publish/mac/images/build-options-mac.png b/versions/4.0/zh/editor/publish/mac/images/build-options-mac.png new file mode 100644 index 0000000000..751908d1eb Binary files /dev/null and b/versions/4.0/zh/editor/publish/mac/images/build-options-mac.png differ diff --git a/versions/4.0/zh/editor/publish/mac/images/build-panel-mac.png b/versions/4.0/zh/editor/publish/mac/images/build-panel-mac.png new file mode 100644 index 0000000000..85e69f6810 Binary files /dev/null and b/versions/4.0/zh/editor/publish/mac/images/build-panel-mac.png differ diff --git a/versions/4.0/zh/editor/publish/mac/images/project-build-menu.png b/versions/4.0/zh/editor/publish/mac/images/project-build-menu.png new file mode 100644 index 0000000000..d5cec7521f Binary files /dev/null and b/versions/4.0/zh/editor/publish/mac/images/project-build-menu.png differ diff --git a/versions/4.0/zh/editor/publish/mac/images/run-mac-800to400.png b/versions/4.0/zh/editor/publish/mac/images/run-mac-800to400.png new file mode 100644 index 0000000000..1c047e2bb6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/mac/images/run-mac-800to400.png differ diff --git a/versions/4.0/zh/editor/publish/mac/images/run-mac.png b/versions/4.0/zh/editor/publish/mac/images/run-mac.png new file mode 100644 index 0000000000..831e2e222e Binary files /dev/null and b/versions/4.0/zh/editor/publish/mac/images/run-mac.png differ diff --git a/versions/4.0/zh/editor/publish/mac/images/xcode-folder-mac.png b/versions/4.0/zh/editor/publish/mac/images/xcode-folder-mac.png new file mode 100644 index 0000000000..0afdda5796 Binary files /dev/null and b/versions/4.0/zh/editor/publish/mac/images/xcode-folder-mac.png differ diff --git a/versions/4.0/zh/editor/publish/mac/images/xcode-showcase-mac.png b/versions/4.0/zh/editor/publish/mac/images/xcode-showcase-mac.png new file mode 100644 index 0000000000..8f2b05b756 Binary files /dev/null and b/versions/4.0/zh/editor/publish/mac/images/xcode-showcase-mac.png differ diff --git a/versions/4.0/zh/editor/publish/mac/index.md b/versions/4.0/zh/editor/publish/mac/index.md new file mode 100644 index 0000000000..f7d706dc89 --- /dev/null +++ b/versions/4.0/zh/editor/publish/mac/index.md @@ -0,0 +1,21 @@ +# macOS 构建指引 + +Cocos Creator 支持发布 macOS 桌面应用程序,但至少需要一台支持 macOS 的设备以及苹果开发者账号。 + +## 主要内容 + +- [macOS 构建示例](./build-example-mac.md) +- [macOS 构建选项介绍](build-options-mac.md) +- [基于反射机制实现 JavaScript 与 iOS/macOS 系统原生通信](../../../advanced-topics/oc-reflection.md) +- [使用 JsbBridge 实现 JavaScript 与 Objective-C 通信](../../../advanced-topics/js-oc-bridge.md) +- [特性与系统版本](../../../advanced-topics/supported-versions.md) + +## 相关阅读 + +- [安装配置原生开发环境](../setup-native-development.md) +- [构建发布面板](../build-panel.md) +- [通用构建选项介绍](../build-options.md) +- [原生平台通用构建选项](../native-options.md) +- [原生平台 JavaScript 调试](../debug-jsb.md) +- [构建流程简介与常见错误处理](../build-guide.md) +- [原生平台二次开发指南](../../../advanced-topics/native-secondary-development.md) diff --git a/versions/4.0/zh/editor/publish/native-options.md b/versions/4.0/zh/editor/publish/native-options.md new file mode 100644 index 0000000000..5615a4ad0e --- /dev/null +++ b/versions/4.0/zh/editor/publish/native-options.md @@ -0,0 +1,128 @@ +# 打包发布到原生平台 + +点击菜单栏的 **项目 -> 构建发布**,打开构建发布面板。 + +目前可以选择的原生平台包括 Android、iOS、HarmonyOS、Mac 和 Windows 四个。iOS、Mac 只在 macOS 电脑上才能选择,Windows 只在 Windows 电脑上才能选择。 + +![native platform](publish-native/native-platform.png) + +## 原生平台通用构建选项 + +![Native 选项](publish-native/native-options.png) + +### 加密脚本 + +该项用于加密发布后的脚本。会在构建后的 `assets` 目录下生成 jsc 文件,这个文件是加密过的。而 js 文件会备份在 `script-backup` 目录下以便调试,打包时不会进入 APP 中。 + +**脚本加密密钥**:在 Native 平台上会使用这个值作为加密 js 文件的密钥。项目新建时会随机生成。 + +**Zip 压缩**:勾选上的话可以减小脚本体积。 + +![encrypt js](publish-native/encrypt-js.png) + +### 原生引擎 + +该项用于展示当前使用的是内置引擎还是定制引擎,点击后面的编辑按钮即可前往 **偏好设置 -> [引擎管理器](../preferences/index.md#%E5%BC%95%E6%93%8E%E7%AE%A1%E7%90%86%E5%99%A8)** 面板进行设置。 + +### 任务调度系统 + +该项是目前引擎内部功能模块使用的功能,用户暂不需要关注该项。如果确实对任务调度系统有需求,应当注意以下两点: + +1. TBB 与 TaskFlow 会因项目情况和环境不同而产生表现差异,应根据实际项目情况做出选择。 +2. TBB 或 TaskFlow 在原生平台上的应用会有版本限制,具体请查看下文 **版本支持** 部分的内容。 + +TaskFlow 依赖特定的 C++ 版本和操作系统版本,请往 [特性与系统版本](./../../advanced-topics/supported-versions.md) 查看详情。 + +### 自动执行下一步 + +![native-build-buttons](./publish-native/native-build-buttons.png) + +构建面板右下方,有 **构建**,**生成**,**运行** 三个按钮,默认情况下,需要点击对应的按钮,才会执行相应的动作。 + +可以点击两个按钮之间的 **链接** 按钮,按钮会变为 **箭头**,意味着箭头指向的下一步会在上一步操作完成后自动执行。 再次点击箭头,可恢复为之前的状态。 + +## 其他构建选项 + +发布到原生平台需要安装配置一些必要的环境,详情请参考 [安装配置原生开发环境](setup-native-development.md)。 + +如需查看所有平台通用的选项,请参考请参考 [通用构建选项](build-options.md)。 + +如需查看某个原生平台专有的构建选项,请参考: +- [Android 平台构建选项](./android/build-options-android.md) +- [iOS 平台构建选项](./ios/build-options-ios.md) +- [Windows 平台构建选项](./windows/build-options-windows.md) +- [Mac 平台构建选项](./mac/build-options-mac.md) + +## 构建 + +构建选项设置完成后,就可以开始构建了,点击 **构建发布** 面板右下角的 **构建** 按钮,开始构建流程。 + +编译脚本和打包资源时会在 **构建发布** 面板的 **构建任务** 页面显示蓝色的进度条,构建成功的话进度条到达 100% 并显示为绿色: + +![build progress](publish-native/build-progress-windows.png) + +## 原生工程目录结构 + +Cocos Creator 构建的原生工程,包含 原生公共目录,原生平台目录和原生项目目录,详情请参考 [原生平台二次开发指南](../../advanced-topics/native-secondary-development.md)。 + +## 编译和运行 + +在 Cocos Creator 中,可以通过构建面板上的按钮,编译和运行项目,也可以通过各平台对应的 IDE(如 Xcode、Android Studio、Visual Studio)打开项目,执行进一步的编译、运行、调试和发布。 + +### 通过编辑器 + +构建完成后,继续点击旁边的 **生成** 按钮,成功后会提示: + +`make package YourProjectBuildPath success!` + +> **注意**:首次编译 Android 项目或者有版本升级时,需要通过 Android Studio 打开工程,检查是否有缺失的工具和错误,确认无误后方可使用构建面板按钮生成和运行。 + +**生成** 过程完成后,继续点击旁边的 **运行** 按钮,可能还会继续进行一部分编译工作,请耐心等待或通过日志文件查看进展。各平台的运行结果为: + +- Mac/Windows 平台会直接在桌面运行预览 +- Android 平台必须通过 USB 连接真机,并且在真机上开启 USB 调试后才可以运行预览 +- iOS 平台会调用模拟器运行预览,但建议通过 Xcode 连接真机执行 **生成** 和 **运行**。 + +### 通过 IDE + +点击 **构建任务** 左下角的 **文件夹图标** 按钮,就会在操作系统的文件管理器中打开构建发布路径,这个路径中 `build` 目录下的 `proj` 里就包含了当前构建的原生平台工程。 + +接下来使用原生平台对应的 IDE(如 Xcode、Android Studio、Visual Studio)打开这些工程,就可以进一步地编译和发布预览了。 + +- **Android** + + ![android studio](publish-native/android-studio.png) + +- **iOS** 和 **Mac** + + ![ios xcode](publish-native/ios-xcode.png) + +- **Windows** + +![windows xcode](publish-native/windows-vs.png) + +如果要对生成的原生项目做修改,请参考 [原生项目二次开发](../../advanced-topics/native-secondary-development.md)。 + +关于原生平台 IDE 的使用请搜索相关信息,这里就不再赘述了。若要了解如何在原生平台上调试,请参考 [原生平台 JavaScript 调试](debug-jsb.md)。 + +## 注意事项 + +1. 在 MIUI 10 系统上运行 debug 模式构建的工程可能会弹出 “Detected problems with API compatibility” 的提示框,这是 MIUI 10 系统自身引入的问题,使用 release 模式构建即可。 + +2. 打包 iOS 平台时,如果开发者在项目中未使用到 WebView 相关功能,请确保在 **项目 -> 项目设置 -> 功能裁剪** 中剔除 WebView 模块,以提高 iOS 的 App Store 机审成功率。如果开发者确实需要使用 WebView(或者添加的第三方 SDK 自带了 WebView),并因此 iOS 的 App Store 机审不通过,仍可尝试通过邮件进行申诉。 + +3. Android 平台通过编辑器和 Android Studio 编译后的结果有些区别: + + - 通过编辑器执行 **生成** 步骤后,会在发布路径下生成 `build` 目录,`.apk` 生成在 `build` 目录的 `build\app\publish\` 目录下。 + + - 通过 Android Studio 编译后,`.apk` 则生成在 `proj\app\build\outputs\apk` 目录下。 + +4. 在 Cocos Creator 3.0 中,Android 与 Android Instant 使用同一个构建模板,构建生成的工程都是在 `build\android\proj` 目录中。针对该目录请注意: + + - 如果是 Android 平台单独使用的代码请放入 `app\src` 目录,单独使用的第三方库请放入 `app\libs` 目录(若没有这两个目录可自行创建)。 + + - 如果是 Android Instant 单独使用的代码和第三方库请分别放入 `instantapp\src` 和 `instantapp\libs` 目录(若没有这两个目录可自行创建)。 + + - 如果是 Android 和 Android Instant 共用的代码和第三方库,请分别放入 `src` 和 `libs` 目录(若没有这两个目录可自行创建)。 + + 通过在 **构建发布** 面板点击 **生成** 按钮来编译 Android 时,会默认执行 `assembleRelease/Debug`,编译 Android Instant 时会默认执行 `instantapp:assembleRelease/Debug`。 diff --git a/versions/4.0/zh/editor/publish/optimize/1.png b/versions/4.0/zh/editor/publish/optimize/1.png new file mode 100644 index 0000000000..18a77a2148 Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/1.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/10.png b/versions/4.0/zh/editor/publish/optimize/10.png new file mode 100644 index 0000000000..e4e79b9d37 Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/10.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/11.png b/versions/4.0/zh/editor/publish/optimize/11.png new file mode 100644 index 0000000000..83fc456d01 Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/11.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/12.png b/versions/4.0/zh/editor/publish/optimize/12.png new file mode 100644 index 0000000000..67697fafff Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/12.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/13.png b/versions/4.0/zh/editor/publish/optimize/13.png new file mode 100644 index 0000000000..f563be6e0b Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/13.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/2.png b/versions/4.0/zh/editor/publish/optimize/2.png new file mode 100644 index 0000000000..684f5953e2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/2.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/3.png b/versions/4.0/zh/editor/publish/optimize/3.png new file mode 100644 index 0000000000..f91656a82c Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/3.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/4.png b/versions/4.0/zh/editor/publish/optimize/4.png new file mode 100644 index 0000000000..facaf7af20 Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/4.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/5.png b/versions/4.0/zh/editor/publish/optimize/5.png new file mode 100644 index 0000000000..82e2bf1fc0 Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/5.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/6.png b/versions/4.0/zh/editor/publish/optimize/6.png new file mode 100644 index 0000000000..cb4cd516bd Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/6.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/7.png b/versions/4.0/zh/editor/publish/optimize/7.png new file mode 100644 index 0000000000..4cdb4b7a80 Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/7.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/8.png b/versions/4.0/zh/editor/publish/optimize/8.png new file mode 100644 index 0000000000..629e922f28 Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/8.png differ diff --git a/versions/4.0/zh/editor/publish/optimize/9.png b/versions/4.0/zh/editor/publish/optimize/9.png new file mode 100644 index 0000000000..d318fad26b Binary files /dev/null and b/versions/4.0/zh/editor/publish/optimize/9.png differ diff --git a/versions/4.0/zh/editor/publish/publish-alipay-mini-game.md b/versions/4.0/zh/editor/publish/publish-alipay-mini-game.md new file mode 100644 index 0000000000..94168aab35 --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-alipay-mini-game.md @@ -0,0 +1,52 @@ +# 发布到支付宝小游戏 + +## 环境配置 + +- 桌面端下载 [支付宝小程序开发者工具](https://render.alipay.com/p/f/fd-jwq8nu2a/pages/home/index.html) 并安装。 + +- 下载 [支付宝](https://mobile.alipay.com/index.htm),并安装到手机设备上。 + +- 支付宝客户端在 Android 上支持的最低版本为 10.3.70,在 iOS 为 10.3.70。 + +## 发布流程 + +使用 Cocos Creator 打开需要发布的项目工程,在 **构建发布** 面板的 **发布平台** 中选择 **支付宝小游戏**,然后点击 **构建**。 + +![build option](./publish-alipay-mini-game/build_option.png) + +通用构建选项的设置请参考 [构建选项](build-options.md)。支付宝小游戏相关的构建选项填写规则如下: + +| 构建选项 | 说明 | 字段名(用于命令行发布) | +| :-- | :-- | :-- | +| 初始场景分包 | 勾选后,首场景及其相关的依赖资源会被构建到发布包目录 `assets` 下的内置 Asset Bundle — [start-scene](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中,提高初始场景的资源加载速度。 | `startSceneAssetBundle` | +| 设备方向 | 可选值包括 `landscape` 和 `portrait`。| `deviceOrientation` | +| 资源服务器地址 | 用于填写资源存放在远程服务器上的地址。开发者需要在构建后手动将发布包目录下的 remote 文件夹上传到所填写的资源服务器地址上。详情可参考 [上传资源到远程服务器](../../asset/cache-manager.md) | `remoteUrl` | +| polyfills | 构建支持一些新特性的 polyfills,主要是在打包脚本时会做相应的处理。目前仅支持 **异步函数**,后续将会开放更多功能。| `polyfills` | + +构建完成后点击 **构建任务** 左下角的文件夹图标按钮,可以看到在项目的 `build` 目录下生成了支付宝小游戏工程文件夹 `alipay-mini-game`,其中已经包含了支付宝小游戏环境的配置文件 `game.json`。 + +![build](./publish-alipay-mini-game/build.png) + +使用 **支付宝开发者工具** 打开构建生成的 `alipay-mini-game` 文件夹,即可打开支付宝小游戏项目以及预览调试游戏内容。 + +![preview](./publish-alipay-mini-game/preview.png) + +## 支付宝小游戏环境的资源管理 + +支付宝小游戏与微信小游戏类似,都存在着包体限制,超过 **4MB** 的额外资源,必须通过网络请求下载。 + +当包体过大时,可在 **构建发布** 面板配置 **资源服务器地址** 选项,将低加载优先级的资源上传到远程服务器,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 + +游戏启动之后引擎会自动下载远程服务器地址中的资源,资源下载后引擎的缓存管理器会记录资源的保存路径,用于在缓存空间不足时自动删除部分缓存的游戏资源。请参考 [缓存管理器](../../asset/cache-manager.md)。 + +## 支付宝小游戏的限制 + +不支持以下模块: + +- WebView +- VideoPlayer + +## 文档相关 + +由于支付宝小游戏相关的文档目前只对内开放,开发者如果有需要的话可直接联系支付宝对接群的人员。 + diff --git a/versions/4.0/zh/editor/publish/publish-alipay-mini-game/build.png b/versions/4.0/zh/editor/publish/publish-alipay-mini-game/build.png new file mode 100644 index 0000000000..f049bc5d2b Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-alipay-mini-game/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-alipay-mini-game/build_option.png b/versions/4.0/zh/editor/publish/publish-alipay-mini-game/build_option.png new file mode 100644 index 0000000000..ae797b3883 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-alipay-mini-game/build_option.png differ diff --git a/versions/4.0/zh/editor/publish/publish-alipay-mini-game/preview.png b/versions/4.0/zh/editor/publish/publish-alipay-mini-game/preview.png new file mode 100644 index 0000000000..993bd1334b Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-alipay-mini-game/preview.png differ diff --git a/versions/4.0/zh/editor/publish/publish-baidu-mini-game.md b/versions/4.0/zh/editor/publish/publish-baidu-mini-game.md new file mode 100644 index 0000000000..c669660d61 --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-baidu-mini-game.md @@ -0,0 +1,100 @@ +# 发布到百度小游戏 + +> **注意**:由于相关合约已到期,自 2022 年 11 月 30 日起,将不再支持百度小游戏的发布和构建。 + +百度小游戏是基于手机百度 app 上的智能小程序进行扩展的小游戏,它不仅提供了强大的游戏能力,还和智能小程序一样,提供了大量的原生接口,比如支付,文件系统,位置,分享等。相当于同时结合了 WEB 易于传播以及 Native 功能丰富的优势。 + +百度小游戏的运行环境和微信小游戏类似,基本思路也是封装必要的 WEB 接口提供给用户,尽可能追求和 WEB 同样的开发体验。百度小游戏在智能小程序环境的基础上提供了 WebGL 接口的封装,使得渲染能力和性能有了大幅度提升。不过由于这些接口都是百度团队通过自研的原生实现封装的,所以并不可以等同为浏览器环境。 + +作为引擎方,为了尽可能简化开发者的工作量,我们为用户完成的主要工作包括: + +- 引擎框架适配百度小游戏 API,纯游戏逻辑层面,用户不需要任何额外的修改 +- Cocos Creator编辑器提供了快捷的打包流程,直接发布为百度小游戏 +- 自动加载远程资源,缓存资源以及缓存资源版本控制 + +具体百度小游戏的申请入驻,开发准备,游戏提交,审核和发布流程可以参考 [百度小游戏注册指导文档](https://smartprogram.baidu.com/docs/game/)。 + +## 准备工作 + +- 下载 [百度开发者工具](https://smartprogram.baidu.com/docs/game/tutorials/howto/dev/) 并安装。 +- 在手机的应用商店中下载并安装百度应用 +- 登录 [智能小程序平台](https://smartprogram.baidu.com/developer/index.html),找到 AppID。 + + ![appid](./publish-baidugame/appid.png) + +## 发布流程 + +使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板,**发布平台** 选择 **百度小游戏**。填入 AppID,然后点击 **构建**。 + +![build](./publish-baidugame/build.png) + +通用构建选项的设置请参考 [通用构建选项](build-options.md),百度小游戏特有的构建选项如下: + +| 构建选项 | 可选 | 说明 | 字段名(用于命令行发布) | +| :------ | :--- | :--- | :--- | +| 初始场景分包 | 可选项 | 勾选后,首场景及其相关的依赖资源会被构建到发布包目录 `assets` 下的内置 Asset Bundle — [start-scene](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中,提高初始场景的资源加载速度。 | `startSceneAssetBundle` | +| 设备方向 | 必填项 | 可选值包括 `landscape` 和 `portrait`。构建时会写入到发布包目录下的 `game.json` 中。| `orientation` | +| AppID | 必填项 | 百度小游戏的 AppID,构建时会写入到发布包目录下的 `project.swan.json` 中。| `appid` | +| 资源服务器地址 | 可选项 | 用于填写资源存放在远程服务器上的地址。开发者需要在构建后手动将发布包目录下的 `remote` 文件夹上传到所填写的资源服务器地址上。详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md) | `remoteServerAddress` | +| 生成开放数据域工程模板 | 可选项 | 用于接入开放数据域,详情可以参考 [开放数据域](./build-open-data-context.md) | `buildOpenDataContextTemplate` | + +### 运行预览 + +构建完成后点击 **构建任务** 左下角的文件夹图标按钮打开项目发布包,可以看到在默认发布路径 `build` 目录下生成了 `baidu-mini-game`(以具体的构建任务名为准)的百度小游戏工程文件夹,其中已经包含了百度小游戏环境的配置文件:`game.json` 和 `project.swan.json` + +![package](./publish-baidugame/package.png) + +使用 **百度开发者工具** 打开构建生成的 `baidu-mini-game` 文件夹,即可打开百度小游戏项目及预览调试游戏内容。**百度开发者工具** 的使用方式请参考 [百度开发者工具文档](https://smartprogram.baidu.com/docs/game/tutorials/howto/dev/)。 + +![preview](./publish-baidugame/preview.png) + +> **注意**:预览调试时若出现了 **当前版本的开发者工具无法发布小程序,请更新最新的开发者工具** 的提示,说明填写的 AppID 是小程序的 AppID,不是小游戏的 AppID,请重新申请一个小游戏 AppID。 + +## 百度小游戏环境的资源管理 + +百度小游戏存在着包体限制,超过 **4MB** 的额外资源,必须通过网络请求下载。 + +当包体过大时,可在 **构建发布** 面板配置 **资源服务器地址** 选项,将低加载优先级的资源上传到远程服务器,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 + +游戏启动之后引擎会自动下载远程服务器地址中的资源,资源下载后引擎的缓存管理器会记录资源的保存路径,用于在缓存空间不足时自动删除部分缓存的游戏资源。请参考 [缓存管理器](../../asset/cache-manager.md)。 + +> **注意**:目前百度小游戏在真机上只支持通过 HTTPS 从远程服务器加载资源,所以必须将资源文件放在 HTTPS 服务器上,否则会出现资源加载失败的情况。 + +## 分包加载 + +百度小游戏的分包加载方式和微信小游戏类似,其包体限制如下: + +- 所有包的总大小不超过 **8MB** +- 单个分包/主包大小不超过 **4MB** + +具体的分包加载机制请参考 [小游戏分包](subpackage.md)。 + +## 平台 SDK 接入 + +除了纯游戏内容以外,百度小游戏环境还提供了非常强大的原生 SDK 接口,这些接口都是仅存在于百度小游戏环境中的,等同于其他平台的第三方 SDK 接口。这类 SDK 接口的移植工作在现阶段还是需要开发者自己处理。下面列举一些百度小游戏所提供的强大 SDK 能力: + +1. 用户接口:登陆,授权,用户信息等 +2. 百度收银台支付 +3. 转发信息 +4. 文件上传下载 +5. 其他:图片、位置、广告、设备信息等等 + +## 接入百度小游戏的开放数据域 + +类似微信小游戏,百度小游戏为了保护其社交关系链数据,也实现了一个 **开放数据域**,可以获取到同玩且双向关注的好友信息。这是一个单独的游戏执行环境。开放数据域中的资源、引擎、程序,都和主游戏完全隔离,开发者只有在开放数据域中才能访问百度小游戏提供的 API `swan.getUserInfo()`、`swan.getUserCloudStorage()` 和 `swan.getFriendCloudStorage()`,用于获取相应的用户数据。 + +详细的百度小游戏开放域发布流程,请参考 [接入开放数据域](build-open-data-context.md)。 + +## 百度小游戏已知问题 + +目前 Cocos Creator 对百度小游戏的适配工作还未完全结束,暂时还不支持以下组件: + +- VideoPlayer +- WebView + +## 参考链接 + +- [百度小游戏注册指导文档](https://smartprogram.baidu.com/docs/game/) +- [百度开发者工具文档](https://smartprogram.baidu.com/docs/game/tutorials/howto/dev/) +- [百度小游戏 API 文档](https://smartprogram.baidu.com/docs/game/api/openApi/authorize/) +- [百度小游戏分包加载](https://smartprogram.baidu.com/docs/game/tutorials/subpackages/sub/) diff --git a/versions/4.0/zh/editor/publish/publish-baidugame/appid.png b/versions/4.0/zh/editor/publish/publish-baidugame/appid.png new file mode 100644 index 0000000000..513e32cf6f Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-baidugame/appid.png differ diff --git a/versions/4.0/zh/editor/publish/publish-baidugame/build.png b/versions/4.0/zh/editor/publish/publish-baidugame/build.png new file mode 100644 index 0000000000..027d437075 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-baidugame/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-baidugame/game-json.png b/versions/4.0/zh/editor/publish/publish-baidugame/game-json.png new file mode 100644 index 0000000000..8581558bb6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-baidugame/game-json.png differ diff --git a/versions/4.0/zh/editor/publish/publish-baidugame/package.png b/versions/4.0/zh/editor/publish/publish-baidugame/package.png new file mode 100644 index 0000000000..1f3f50af1c Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-baidugame/package.png differ diff --git a/versions/4.0/zh/editor/publish/publish-baidugame/preview.png b/versions/4.0/zh/editor/publish/publish-baidugame/preview.png new file mode 100644 index 0000000000..8bb7ea1b9e Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-baidugame/preview.png differ diff --git a/versions/4.0/zh/editor/publish/publish-bytedance-mini-game.md b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game.md new file mode 100644 index 0000000000..11510a1acb --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game.md @@ -0,0 +1,110 @@ +# 发布到抖音小游戏 + +抖音小游戏是基于一种不需要用户下载,点开即玩的全新游戏类型。 + +小游戏的游戏提交,审核和发布流程等,需要遵守字节官方团队的要求和标准流程,具体信息可以参考 [抖音小游戏接入指南](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/guide/minigame/introduction)。 + +## 准备工作 + +1. 下载 [抖音开发者工具](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/developer-instrument/developer-instrument-update-and-download) 并安装。 + +2. 参考 [抖音小游戏接入指南](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/guide/minigame/introduction),在 [抖音开发者平台](https://microapp.bytedance.com) 完成账号注册、登录以及申请小游戏。 + +3. 小游戏申请通过后,在开发者平台的 **开发管理 -> 开发设置** 中找到 **AppID**。 + + ![appid](./publish-bytedance-mini-game/appid.png) + +## 发布流程 + +1. 使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板。在 **构建发布** 面板的 **发布平台** 中选择 **抖音小游戏**。 + + ![build](./publish-bytedance-mini-game/build.png) + + 通用构建选项的设置请参考 [通用构建选项](build-options.md),抖音小游戏特有的构建选项如下,具体说明请参考下文 **构建选项** 部分的内容。 + + ![bytedance-options](./publish-bytedance-mini-game/build-options.png) + +2. **构建发布** 面板的构建选项设置完成后,点击 **构建**。
+ 构建完成后点击 **构建任务** 左下角的文件夹图标按钮打开项目发布包,可以看到在默认发布路径 `build` 目录下生成了 `bytedance-mini-game`(以具体的构建任务名为准)文件夹,其中已经包含了抖音小游戏环境的配置文件 `game.json` 和 `project.config.json`。 + + ![package](./publish-bytedance-mini-game/package.png) + +3. 使用 **抖音开发者工具** 打开构建生成的 `bytedance-mini-game` 文件夹,即可打开抖音小游戏项目及预览调试游戏内容。开发者工具的具体使用方式请参考 [抖音开发者工具介绍](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/developer-instrument/development-assistance/mini-app-developer-instrument)。 + + ![tool](./publish-bytedance-mini-game/tool.png) + +### 主包设置 + +| 主包设置 | 说明 | 字段名(用于命令行发布) | +| :---- | :-- | :-- | +| 初始场景分包 | 勾选后,首场景及其相关的依赖资源会被构建到发布包目录 `assets` 下的内置 Asset Bundle — [start-scene](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中,提高初始场景的资源加载速度。 | `startSceneAssetBundle` | +| 资源服务器地址 | 用于填写资源存放在远程服务器上的地址。开发者需要在构建后手动将发布包目录下的 `remote` 文件夹上传到所填写的资源服务器地址上。详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md) | `remoteServerAddress` | + +### 引擎设置 + +Cocos Creator 3.8 中,将引擎相关的构建选项统一到了**引擎设置**中,方便根据需求选择不同的引擎配置。 + +| 引擎设置 | 说明 | 字段名(用于命令行发布) | +| :---- | :-- | :-- | +| CLEANUP_IMAGE_CACHE | 是否在纹理数据上传 GPU 后,删除内存数据,删除后无法使用动态合图功能。 | - | +| 物理系统 | 选择不同的物理系统,发布后以当前选择的为准 | - | +| WebGL 2.0 | 选择不同的 WebGL 版本 | - | +| 原生代码打包模式 | 此选项会影响 Spine、物理等 Webassembly 模块的打包方式,将统一控制所有的 wasm/asmjs 模块,默认为最优值,若无特殊情况,不建议更改。| - | +| Wasm 3D 物理系统(基于 `ammo.js`) | 用于选择是否启用 Wasm,默认为开启,使用 **bullet(ammo.js)** 物理时生效。详情请参考下文 **WebAssembly 支持** 部分的内容。 | - | +| 引擎原生代码分包 | 将引擎的 WASM/Asm.js 代码放入子包,减少包体。| - | +| 启用 WASM Brotli 压缩 | 开启此选项会减少 WASM 模块大小,但会略微增加加载时的解压时间,根据需要开启 | - | + +### 构建选项 + +| 构建选项 | 说明 | 字段名(用于命令行发布) | +| :---- | :-- | :-- | +| 生成开放数据域工程模板 | 用于接入开放数据域,详情请参考 [开放数据域](./build-open-data-context.md)。 | `buildOpenDataContextTemplate` | +| AppID | 必填项,抖音小游戏的 AppID,构建时会写入到发布包目录下的 `project.config.json` 文件中。 | `appid` | +| 设备方向 | 可选值包括 **Portrait** 和 **Landscape**。构建时会写入到发布包目录下的 `game.json` 文件中。 | `orientation` | +| 引擎原生代码分包 | 将引擎的 WASM/Asm.js 代码模块放入小游戏分包,减少主包包体。| - | +| 开发者工具打开方式 | 默认为 Lite 模式,以加快启动速度。| - | + +## 分包加载 + +分包加载,即把游戏内容按一定规则拆分在几个包里,在首次启动的时候只下载必要的包,这个必要的包称为 **主包**,开发者可以在主包内触发下载其他子包,这样可以有效降低首次启动的消耗时间。若要使用该功能需要在 Creator 中设置 [小游戏分包](subpackage.md),设置完成后在构建时就会自动分包。 + +抖音小游戏需要特定的版本才能支持分包功能,字节产品的版本要求如下: + +| 产品 | Android | iOS | +| :-- | :--- | :--- | +| 抖音 | v13.6.0 | v13.7.0 | +| 头条 | v7.9.9 | v7.9.8 | + +抖音开发者工具使用的版本请大于等于 **2.0.6**,小于 **3.0.0**。调试基础库则要求在 1.88.0 及以上。 + +> **注意**:若产品的版本不支持分包加载,则引擎会将分包作为一个普通的 Asset Bundle 加载。 + +### 普通小游戏包 + +未配置分包的场景下,每个小游戏允许上传的代码包总大小上限为 20MB + +### 分包后小游戏包 + +对于配置了分包的小游戏,默认限制为: + +- 小游戏整体包(小游戏包整个目录)大小不超过 20MB +- 单个主包不超过 4MB +- 单个分包大小不超过 20MB + +具体可参考 [抖音小游戏分包加载官方文档](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/framework/subpackages/introduction/) + +## 小游戏环境的资源管理 + +抖音小游戏与微信小游戏类似,都存在着主包包体限制,超过 4MB 的额外资源,必须通过网络请求下载。 + +当包体过大时,可在 **构建发布** 面板配置 **资源服务器地址** 选项,将低加载优先级的资源上传到远程服务器,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 + +游戏启动之后引擎会自动下载远程服务器地址中的资源,资源下载后引擎的缓存管理器会记录资源的保存路径,用于在缓存空间不足时自动删除部分缓存的游戏资源。请参考 [缓存管理器](../../asset/cache-manager.md)。 + +## 参考链接 + +- [抖音小游戏接入指南](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/guide/minigame/introduction/) +- [抖音小游戏开发者文档](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/bytedance-mini-game) +- [抖音小游戏 API 文档](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/api/overview) +- [抖音开发者工具下载](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/developer-instrument/developer-instrument-update-and-download) +- [抖音开发者工具文档](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/dev-tools/developer-instrument/development-assistance/mini-app-developer-instrument) diff --git a/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/appid.png b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/appid.png new file mode 100644 index 0000000000..c08ca70335 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/appid.png differ diff --git a/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/build-options.png b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/build-options.png new file mode 100644 index 0000000000..10449da610 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/build-options.png differ diff --git a/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/build.png b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/build.png new file mode 100644 index 0000000000..4076798011 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/package.png b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/package.png new file mode 100644 index 0000000000..20c2f02073 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/package.png differ diff --git a/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/performance.png b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/performance.png new file mode 100644 index 0000000000..19bcbba796 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/performance.png differ diff --git a/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/physx-options.png b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/physx-options.png new file mode 100644 index 0000000000..8e1770e9cf Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/physx-options.png differ diff --git a/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/tool.png b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/tool.png new file mode 100644 index 0000000000..44d774b723 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-bytedance-mini-game/tool.png differ diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games.md b/versions/4.0/zh/editor/publish/publish-fb-instant-games.md new file mode 100644 index 0000000000..681f9ac73d --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-fb-instant-games.md @@ -0,0 +1,160 @@ +# 发布游戏到 Facebook Instant Games + +Facebook Instant Games 跟微信小游戏相比,本质上的区别在于 Facebook Instant Games 运行于纯 HTML5 环境。因此它不仅可以在手机上,还可以在桌面浏览器上运行,开发和调试更加便捷。 + +**目前 Cocos Creator 为用户完成的工作包括:** + +- 集成了 Facebook Instant Games SDK,并且自动进行了初始化,用户能直接调用相关的 API +- 在 Cocos Creator 构建面板中提供一键式打包流程,可直接打包为符合 Facebook Instant Games 技术规范的游戏 + +**用户需要完成的工作:** + +- 调用 Facebook Instant Games SDK,访问平台相关功能 +- 将 Cocos Creator 打包好的版本上传到 Facebook + +## 发布流程 + +- 使用 Cocos Creator 构建游戏 +- 上传到 Facebook 后台 +- 测试游戏 +- 在 Facebook 中分享你的游戏 + +## 使用 Cocos Creator 构建游戏 + +1. 使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板。在 **构建发布** 面板的 **发布平台** 中选择 **Facebook Instant Games**。 + + ![build](./publish-fb-instant-games/build.png) + + 通用构建选项的设置请参考 [通用构建选项](build-options.md),Facebook Instant Games 特有的构建选项如下,具体说明请参考下文 **构建选项** 部分的内容。 + + ![fb-instant-games-options](./publish-fb-instant-games/build-options.png) + +2. **构建发布** 面板的构建选项设置完成后,点击 **构建**。
+ 构建完成后点击 **构建任务** 左下角的文件夹图标按钮打开项目发布包,可以看到在默认发布路径 `build` 目录下生成了 `fb-instant-games`(以具体的构建任务名为准)文件夹,其中已经包含了Facebook Instant Games 环境的压缩文件 `fb-instant-games.zip`。 + + ![package](./publish-fb-instant-games/package.png) + +### 构建选项 + +各平台通用的构建选项,详情请参考 [通用构建参数介绍](build-options.md)。接下来我们来看一下 Facebook Instant Games 平台特有的构建选项。 + +| 构建选项 | 说明 | 字段名(用于命令行发布) | +| :--- | :--- | :--- | +| 设备方向 | 可选值包括 **Landscape**、**Portrait** | `orientation` | +| vConsole | 插入 vConsole 调试工具,vConsole 类似 DevTools 的迷你版,用于辅助调试。 | `embedWebDebugger` | + +### 上传到 Facebook + +在 Facebook 后台创建一款新应用,在 **添加商品** 中添加 **小游戏**,设置游戏类别,保存更改。(详情可参考 [设置应用](https://developers.facebook.com/docs/games/instant-games/getting-started/quickstart?locale=zh_CN#app-setup)) + +点击应用面板左侧的 **小游戏 -> 网页托管** 选项卡,单击 **上传版本**,将 `fb-instant-games` 目录下的 `.zip` 文件上传到 Facebook 托管服务中。 + +![upload](./publish-fb-instant-games/upload.png) + +当版本状态更改为 “待命” 时,单击 “★” 按钮将构建版本推送到 “生产” 环境。 + +![push](./publish-fb-instant-games/push.png) + +### 三、测试游戏 + +#### 在本地启用支持 https 的 Web 服务器 + +- 首先打开命令行窗口,进入构建好的 `fb-instant-games` 目录,通过 npm 安装 http-server 包: + + ```bash + $ cd fb-instant-games + $ npm install -g http-server + ``` + +- 接着通过 openssl 创建私钥和证书: + + ```bash + $ openssl genrsa 2048 > key.pem + $ openssl req -x509 -days 1000 -new -key key.pem -out cert.pem + ``` + +- 当私钥和证书准备就绪后,可通过 SSL 在本地启动 Web 服务: + + ```bash + $ http-server --ssl -c-1 -p 8080 -a 127.0.0.1 + ``` + +- 用浏览器打开 [https://localhost:8080](https://localhost:8080),跳过浏览器显示的安全警告,这一步仅仅是为了让浏览器把上面的私钥和证书加入白名单。如果后续重新生成了私钥和证书,则需要再次打开确认一次。目前还不能在这一步直接预览游戏,因为预览游戏需要初始化 Facebook Instant Games 的 SDK,需要通过下面的方式。 + +#### 在 Facebook 域名下预览游戏 + +如果要使用 Facebook Instant Games SDK 的所有功能,需要用浏览器打开 [https://www.facebook.com/embed/instantgames/YOUR_GAME_ID/player?game_url=https://localhost:8080](https://www.facebook.com/embed/instantgames/YOUR_GAME_ID/player?game_url=https://localhost:8080),注意要将链接中的 `YOUR_GAME_ID` 换成你在 Facebook 后台创建的应用编号。 + +然后可以看到游戏成功运行: + +![game](./publish-fb-instant-games/game.png) + +### 四、在 Facebook 中分享你的游戏 + +点击应用面板中的 **小游戏** 选项卡,选择 **详情**,在 **详情** 页面中拉到最下方可看到如下图所示部分,选择 **分享游戏**,即可将小游戏直接分享到 Facebook 动态消息中。 + +![share](./publish-fb-instant-games/share.png) + +详细的内容,可参考 [测试、发布和分享小游戏](https://developers.facebook.com/docs/games/instant-games/test-publish-share?locale=zh_CN)。 + +> **注意**:Facebook 托管存在多项限制,其中最重要的是: +> +> 1. 不支持服务器端逻辑(例如:php) +> 2. 每次应用程序上传的文件数量不超过 500 个 + +## 自定义 Instant Games + +开发者可以根据自身需求在 Creator 项目文件夹下创建 [build-templates/fb-instant-games](custom-project-build-template.md) 目录,然后将发布后的这几个文件复制到该目录中,即可对它们进行定制: + +![file](./publish-fb-instant-games/file.png) + +- `fbapp-config.json`:这是整个包的配置,可前往 [官方介绍](https://developers.facebook.com/docs/games/instant-games/bundle-config) +- `index.html`:这里可以修改引入的 Facebook Instant Games SDK 版本 +- `index.js`:这里可以修改 SDK 初始化与进度条 + +## SDK 说明 + +Creator 已集成 Facebook 提供的 Instant Games SDK,并且在游戏加载时自动进行了初始化(`initializeAsync` 及 `startGameAsync`)。用户可直接通过 `FBInstant` 模块访问,使用说明详见 [Instant Games SDK](https://developers.facebook.com/docs/games/instant-games/sdk)。 +此外,Facebook 还提供了 Facebook SDK for JavaScript,用于访问 Facebook 的社交功能,可通过 `FB` 模块访问。不过该 SDK Creator 并没有集成,需要用户手动引入,详见 [官方文档](https://developers.facebook.com/docs/javascript)。 + +## 在游戏中创建 Dom elements +请参考示例代码在 facebook instant game 中创建 Dom elements,并显示在游戏画面中 +```javascript +var buttonDiv = document.createElement("div"); +var button = document.createElement("button"); + +const frameSize = screen.windowSize; +const imageWidth = 195, imageHeight = 57; + +button.style.position = "absolute"; +button.style.left = `${frameSize.width / 2 - imageWidth / 2}px`; +button.style.top = `${frameSize.height / 2 - imageHeight / 2}px`; + +//load image from resources bundle +resources.load("btn_play", Texture2D, (error, res)=>{ + if (!error) { + button.style.backgroundImage = `url(${res.nativeUrl})`; + button.style.backgroundRepeat = "no-repeat"; + button.style.backgroundPosition = "center"; + button.style.width = res.width + "px"; + button.style.height = res.height + "px"; + button.style.backgroundColor = "transparent"; + button.style.borderColor = "transparent"; + button.onclick = (()=>{ + //DO SOMETHING + }); + buttonDiv.appendChild(button); + var body = document.body; + body.insertBefore(buttonDiv, body.lastChild); + } +}); +``` + +运行效果: + +![](./publish-fb-instant-games/use-dom-element.png) + +## 参考链接 + +- [Facebook 后台](https://developers.facebook.com/) +- [Facebook Instant Games 文档](https://developers.facebook.com/docs/games/instant-games?locale=zh_CN) diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games/build-options.png b/versions/4.0/zh/editor/publish/publish-fb-instant-games/build-options.png new file mode 100644 index 0000000000..7f7a9fabf8 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-fb-instant-games/build-options.png differ diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games/build.png b/versions/4.0/zh/editor/publish/publish-fb-instant-games/build.png new file mode 100644 index 0000000000..7a6cc23c07 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-fb-instant-games/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games/file.png b/versions/4.0/zh/editor/publish/publish-fb-instant-games/file.png new file mode 100644 index 0000000000..81880f764a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-fb-instant-games/file.png differ diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games/game.png b/versions/4.0/zh/editor/publish/publish-fb-instant-games/game.png new file mode 100644 index 0000000000..ed6def3ea7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-fb-instant-games/game.png differ diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games/package.png b/versions/4.0/zh/editor/publish/publish-fb-instant-games/package.png new file mode 100644 index 0000000000..ffd5762aea Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-fb-instant-games/package.png differ diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games/push.png b/versions/4.0/zh/editor/publish/publish-fb-instant-games/push.png new file mode 100644 index 0000000000..1af0b924dd Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-fb-instant-games/push.png differ diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games/share.png b/versions/4.0/zh/editor/publish/publish-fb-instant-games/share.png new file mode 100644 index 0000000000..682b8f73b2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-fb-instant-games/share.png differ diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games/upload.png b/versions/4.0/zh/editor/publish/publish-fb-instant-games/upload.png new file mode 100644 index 0000000000..9b5ac56d75 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-fb-instant-games/upload.png differ diff --git a/versions/4.0/zh/editor/publish/publish-fb-instant-games/use-dom-element.png b/versions/4.0/zh/editor/publish/publish-fb-instant-games/use-dom-element.png new file mode 100644 index 0000000000..e2ff4728a6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-fb-instant-games/use-dom-element.png differ diff --git a/versions/4.0/zh/editor/publish/publish-honor-mini-game.md b/versions/4.0/zh/editor/publish/publish-honor-mini-game.md new file mode 100644 index 0000000000..5743a6d60d --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-honor-mini-game.md @@ -0,0 +1,90 @@ +# 发布到荣耀小游戏 + +Cocos Creator 从 v3.8.6 开始支持将游戏发布到 **荣耀小游戏**。 + +## 准备工作 + +- 参考 [荣耀小游戏接入指南](https://developer.honor.com/cn/docs/game_center/guides/miniGame/miniGameGuidelines),在荣耀开放平台完成开发者入驻流程和应用创建。 + +- 下载 [荣耀小游戏调试器](https://developer.honor.com/cn/docs/game_center/guides/miniGame/miniGameGuidelines#开发+调试指引),可安装到荣耀手机设备进行小游戏调试。 + +## 发布流程 + +使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板,**发布平台** 选择 **荣耀小游戏**。 + +![build](./publish-honor-mini-game/build.png) + +通用构建选项的设置请参考 [通用构建选项](build-options.md),荣耀小游戏特有的构建选项如下: + +![build option](./publish-honor-mini-game/build-option.png) + +| 构建选项 | 可选 | 说明 | 字段名(用于命令行发布) | +| :------ | :--- | :--- | :--- | +| 游戏包名 | 必填项 | 游戏包名,根据开发者的需求进行填写,例如 `com.example.demo`。| `package` | +| 桌面图标 | 必填项 | 点击输入框后面的放大镜图标按钮选择所需的图标。构建时,图标将会被构建到荣耀小游戏的工程中。桌面图标建议使用 **png** 图片。 | `icon` | +| 游戏版本名称 | 必填项 | 游戏版本名称是真实的版本,如:1.0.0 | `versionName` | +| 游戏版本号 | 必填项 | **游戏版本号** 与 **游戏版本名称** 不同,**游戏版本号** 主要用于区别版本更新。每次提交审核时游戏版本号都要比上次提交审核的值至少 +1,一定不能等于或者小于上次提交审核的值,建议每次提交审核时游戏版本号递归 +1。
**注意**:**游戏版本号** 必须为正整数。 | `versionCode` | +| 支持的最小平台版本号 | 必填项 | 用于兼容性检查,避免上线后在低版本平台运行导致不兼容。 | `minPlatformVersion` | +| 屏幕方向 | 必填项 | 设备方向,可选值包括 `landscape` 和 `portrait`。构建时会写入到发布包目录下的 `manifest.json` 中。| `orientation` | +| 使用调试密钥库 | - | 若勾选该项,表示构建 rpk 包时默认使用的是 Creator 自带的证书,仅用于 **调试** 时使用。若 rpk 包要用于提交审核,则构建时不要勾选该项。
若不勾选该项,则需要手动配置签名证书。| `useDebugKey` | +| **certificate.pem 路径**
**private.pem 路径** | - | 如果不勾选 **密钥库**,则需要配置签名文件 **certificate.pem 路径** 和 **private.pem 路径**,此时构建后生成的是可以 **直接发布** 的 rpk 包。开发者可通过输入框右边的放大镜图标按钮来配置两个签名文件,或者也可以参考下方的 **生成签名文件**。 | `privatePemPath`、`certificatePemPath` | + +- 生成签名文件 + + 有以下两种方式可以生成签名文件: + + - 通过 **构建发布** 面板 **certificate.pem 路径** 后的 **新建** 按钮生成 + + - 通过命令行生成 release 签名 + + 用户需要通过 openssl 命令等工具生成签名文件 private.pem、certificate.pem。 + + ```bash + # 通过 openssl 命令工具生成签名文件 + openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out certificate.pem + ``` + + > **注意**:openssl 工具在 linux 或 Mac 环境下可在终端直接打开。而在 Windows 环境下则需要安装 openssl 工具并且配置系统环境变量,配置完成后需重启 Creator。 + +### 构建 + +**构建发布** 面板的构建选项设置完成后,点击 **构建并生成** 按钮。
+完成后点击 **构建任务** 左下角的文件夹图标按钮打开项目发布包,可以看到在默认发布路径 `build` 目录下生成了 `honor-mini-game`(以具体的构建任务名为准)文件夹,该文件夹就是导出的荣耀小游戏工程目录和 rpk,rpk 包在 `build/honor-mini-game/dist` 目录下。 + +![package](./publish-honor-mini-game/package.png) + +若需要修改生成的 rpk 包,在修改完成后点击 **构建任务** 右下角的 **生成** 按钮,即可在不重新构建的情况下重新生成 rpk 包。 + +## 运行 rpk + +有以下两种方式可将 rpk 运行到手机上: + +- **方法一**: + 在 **构建发布** 面板点击 **运行** 按钮,等待二维码界面生成 + + ![](./publish-honor-mini-game/qr_code.png) + + 然后在荣耀设备上打开之前已经安装完成的 **荣耀调试器**,点击**扫码体验**按钮,直接扫描二维码即可打开 rpk。 + + ![](./publish-honor-mini-game/honor-instant_scan_install1.png) + +- **方法二**: + + 将构建生成的小游戏 rpk 文件(位于打包出的小游戏工程目录下的 dist 目录中)拷贝到手机的内部存储目录下。 + + 在荣耀设备上打开之前已经安装完成的 **荣耀小游戏调试器**,点击 **本地安装**,然后从手机内部存储目录中找到 rpk 文件,选择打开即可。 + + ![](./publish-honor-mini-game/honor-instant_native_install2.png) + +## 荣耀小游戏环境的资源管理 + +荣耀小游戏与微信小游戏类似,都存在着包体限制。荣耀小游戏的主包包体限制是 **5MB**,超过的部分必须通过网络请求下载。 + +当包体过大时,可在 **构建发布** 面板配置 **资源服务器地址** 选项,将低加载优先级的资源上传到远程服务器,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 + +游戏启动之后引擎会自动下载远程服务器地址中的资源,资源下载后引擎的缓存管理器会记录资源的保存路径,用于在缓存空间不足时自动删除部分缓存的游戏资源。请参考 [缓存管理器](../../asset/cache-manager.md)。 + +## 参考链接 + +- [荣耀小游戏接入文档](https://developer.honor.com/cn/docs/game_center/guides/miniGame/miniGameGuidelines) +- [荣耀小游戏调试器下载](https://developer.honor.com/cn/docs/game_center/guides/miniGame/miniGameGuidelines#开发+调试指引) \ No newline at end of file diff --git a/versions/4.0/zh/editor/publish/publish-honor-mini-game/build-option.png b/versions/4.0/zh/editor/publish/publish-honor-mini-game/build-option.png new file mode 100644 index 0000000000..2878c2e6fc Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-honor-mini-game/build-option.png differ diff --git a/versions/4.0/zh/editor/publish/publish-honor-mini-game/build.png b/versions/4.0/zh/editor/publish/publish-honor-mini-game/build.png new file mode 100644 index 0000000000..4abfda8ea0 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-honor-mini-game/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-honor-mini-game/honor-instant_native_install2.png b/versions/4.0/zh/editor/publish/publish-honor-mini-game/honor-instant_native_install2.png new file mode 100644 index 0000000000..d36a540955 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-honor-mini-game/honor-instant_native_install2.png differ diff --git a/versions/4.0/zh/editor/publish/publish-honor-mini-game/honor-instant_scan_install1.png b/versions/4.0/zh/editor/publish/publish-honor-mini-game/honor-instant_scan_install1.png new file mode 100644 index 0000000000..138de3dd5e Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-honor-mini-game/honor-instant_scan_install1.png differ diff --git a/versions/4.0/zh/editor/publish/publish-honor-mini-game/package.png b/versions/4.0/zh/editor/publish/publish-honor-mini-game/package.png new file mode 100644 index 0000000000..52b7f9d16e Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-honor-mini-game/package.png differ diff --git a/versions/4.0/zh/editor/publish/publish-honor-mini-game/qr_code.png b/versions/4.0/zh/editor/publish/publish-honor-mini-game/qr_code.png new file mode 100644 index 0000000000..edeff21c57 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-honor-mini-game/qr_code.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc.md b/versions/4.0/zh/editor/publish/publish-huawei-agc.md new file mode 100644 index 0000000000..617bb6fc81 --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-huawei-agc.md @@ -0,0 +1,92 @@ +# 发布到 HUAWEI AppGallery Connect + +Cocos Creator 支持将游戏发布到 **HUAWEI AppGallery Connect**,帮助开发者接入到华为的应用市场。 + +## 准备工作 + +- 进入 [AppGallery Connect 后台](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html) 并登录,需要先完成 [开发者注册](https://developer.huawei.com/consumer/cn/doc/20300),然后再 [创建应用](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-create_app)。创建应用时,**软件包类型** 选择 **APK**。 + + ![app info](./publish-huawei-agc/app-info.png) + +- 通过 Cocos Service 面板接入所需的 HUAWEI AppGallery Connect 相关服务。目前 SDK 仅支持 **Android** 平台,具体的操作步骤可参考文档 [HUAWEI HMS Core](https://service.cocos.com/document/zh/sdkhub-plugins/sdkhub-hms.html)。 + +## 发布流程 + +使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板,**发布平台** 项选择 **HUAWEI AppGallery Connect**。 + +![agc-build](./publish-huawei-agc/agc-builder.png) + +通用构建选项的设置请参考 [构建选项](build-options.md),需要注意的是请确保 **包名** 与华为后台设置的包名一致。 + +- **agconnect-services 配置**:用于配置华为参数文件 `agconnect-services.json`,具体的配置方法请参考 [配置华为参数文件](https://service.cocos.com/document/zh/sdkhub-plugins/sdkhub-hms.html#%E9%85%8D%E7%BD%AE%E5%8D%8E%E4%B8%BA%E5%8F%82%E6%95%B0%E6%96%87%E4%BB%B6)。 + +- **Cocos SDKHub 配置集**:在 Cocos Service 服务面板开通 Cocos SDKHub 服务之后,**构建发布** 面板中就会出现该选项,用于帮助游戏快速集成渠道。 + + ![sdkhub](./publish-huawei-agc/sdkhub.png) + +### 构建编译 + +**构建发布** 面板的相关参数项设置完成后,点击 **构建**。
+构建完成后点击 **构建任务** 左下角的文件夹图标按钮打开构建发布包,可以看到在默认发布路径 build 目录下生成了 `huawei-agc` 目录,其中已经自动集成了 HUAWEI AppGallery Connect 相关服务。 + +然后点击 **生成**,或者使用 [Android Studio](native-options.md#%E7%94%9F%E6%88%90%E5%92%8C%E8%BF%90%E8%A1%8C) 打开项目进行编译,编译完成后在发布包目录下会生成 HUAWEI AppGallery Connect 的 APK。 + +![apk](./publish-huawei-agc/apk.png) + +### 上传 APK 到 AppGallery Connect + +有以下两种方式可以将 APK 上传到 AppGallery Connect。 + +#### 1. 通过构建发布面板上传 + +Creator 支持直接将构建编译生成的 APK 上传到 AppGallery Connect 后台。
+点击 **构建任务** 右下角的 **上传** 按钮,即可打开 **上传** 面板,然后填写相关信息。 + +![agc-upload-panel](./publish-huawei-agc/agc-upload-panel.png) + +- APP ID:填写应用的 APP ID。登录 AppGallery Connect 后台,选择 **我的应用 -> 应用信息**,即可获取应用的 APP ID。 + +- 版本号:根据需要填写。 + +- APK 路径:选择之前构建编译生成的 APK。 + +- 登录方式:包括 **OAuth** 和 **API 客户端** 两种。 + + - **OAuth** + + OAuth 登录方式只需要在点击 **确认上传** 的时候,根据提示登录 HUAWEI 账号(账号需要有足够的 [权限](https://developer.huawei.com/consumer/cn/doc/development/AppGallery-connect-Guides/agcapi-getstarted-0000001111845114#section797720532313)),然后勾选对应权限的允许框,窗口会自动关闭并自动上传 APK。 + + ![upload-oauth](./publish-huawei-agc/upload-oauth.png) + + - **API 客户端** + + - 若首次使用 **API 客户端** 登录方式,需要登录 AppGallery Connect 后台获取相关配置信息。 + + ![upload-api](./publish-huawei-agc/upload-api.png) + + - 选择 **用户与访问 -> Connect API -> 创建**,创建一个 API 客户端,并根据需要选择 [权限](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-help-rolepermission-0000001155345429),然后点击 **确认**。 + + ![create-api-key](./publish-huawei-agc/create-api-key.png) + + - 将 API 客户端的 **客户端 ID** 和 **密钥** 填入 Creator **上传** 面板中的对应输入框。 + + - 配置完成后点击 **确认上传** 即可。 + + 两种登录方式的详细说明可参考文档 [AppGallery Connect 使用入门](https://developer.huawei.com/consumer/cn/doc/development/AppGallery-connect-Guides/agcapi-getstarted)。 + +#### 2. 通过 AppGallery Connect 后台上传 + +在 AppGallery Connect 后台进入 **我的应用**,选择应用,切换到 **分发** 栏。然后点击左侧的 **版本信息 -> 准备提交**,找到软件版本,点击 **软件包管理**,然后点击 **上传**。 + +### 提交审核 + +1. 在 AppGallery Connect 后台进入 **我的应用**,选择应用,切换到 **分发** 栏。然后点击左侧的 **版本信息 -> 准备提交**,找到软件版本,点击 **软件包管理**,选中刚刚上传的 APK,然后点击 **选取**。 + + ![apk-list](./publish-huawei-agc/apk-list.png) + +2. 其他的配置信息填写可参考文档 [发布应用](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-release_app)。填写完成确认各个信息没有问题后,便可以直接点击页面右上方的 **提交审核** 按钮。华为应用市场将在 **3~5** 个工作日内完成审核。 + +## 相关参考链接 + +- [AppGallery Connect 后台](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html) +- [AppGallery Connect 操作指南](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-create_app) diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/agc-builder.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/agc-builder.png new file mode 100644 index 0000000000..823ba820b0 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/agc-builder.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/agc-upload-panel.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/agc-upload-panel.png new file mode 100644 index 0000000000..da5cc56f4e Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/agc-upload-panel.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/agc-upload.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/agc-upload.png new file mode 100644 index 0000000000..9bf5a6d6b9 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/agc-upload.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/apk-list.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/apk-list.png new file mode 100644 index 0000000000..77a52ca568 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/apk-list.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/apk.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/apk.png new file mode 100644 index 0000000000..1ea9419243 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/apk.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/app-info.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/app-info.png new file mode 100644 index 0000000000..2e3c0d9ee2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/app-info.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/create-api-key.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/create-api-key.png new file mode 100644 index 0000000000..95463cc1a0 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/create-api-key.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/sdkhub.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/sdkhub.png new file mode 100644 index 0000000000..91f41db797 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/sdkhub.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/upload-api.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/upload-api.png new file mode 100644 index 0000000000..8399dacf27 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/upload-api.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-agc/upload-oauth.png b/versions/4.0/zh/editor/publish/publish-huawei-agc/upload-oauth.png new file mode 100644 index 0000000000..7cbacf2b99 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-agc/upload-oauth.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos.md b/versions/4.0/zh/editor/publish/publish-huawei-ohos.md new file mode 100644 index 0000000000..f58980456a --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-huawei-ohos.md @@ -0,0 +1,138 @@ +# 发布 Huawei HarmonyOS 应用 + +从 v3.2 开始,Cocos Creator 支持将游戏打包为 HarmonyOS 应用(`.hap`)。 + +## 准备工作 + +- 进入 [AppGallery Connect 网站](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html) 注册 [华为开发者联盟帐号](https://developer.huawei.com/consumer/cn/doc/start/registration-and-verification-0000001053628148)。 + +- 登录后 [创建项目](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-harmonyapp-createproject) 并 [添加 HarmonyOS 应用](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-harmonyapp-createharmonyapp),**选择平台** 为 **APP(HarmonyOS 应用)**。 + + ![app info](./publish-huawei-ohos/app-info.png) + + > **注意**:**应用包名** 需要与 Creator **构建发布** 面板中的 **应用 ID 名称** 保持一致。 + +- 下载并安装 [HUAWEI DevEco Studio](https://developer.harmonyos.com/cn/develop/deveco-studio#download)。安装完成后 [配置开发环境](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/environment_config-0000001052902427),下载 HarmonyOS SDK,SDK Platforms 中的 SDK 包请勾选 **Java** 和 **Native**。 + + ![dev setting](./publish-huawei-ohos/dev-setting.png) + + 记住窗口上方所示的 **HarmonyOS SDK Location** 指示的目录,稍后需要在 Cocos Creator 的 **偏好设置** 面板中填写这个 SDK 所在路径。 + +## 发布流程 + +### 使用 Cocos Creator 构建 + +1. 使用 Cocos Creator 打开项目工程,点击上方菜单栏中的 **Cocos Creator/File -> 偏好设置 -> 外部程序**,配置 **HarmonyOS NDK** 和 **HarmonyOS SDK** 路径: + + ![preferences](./publish-huawei-ohos/preferences.png) + +2. 从 **菜单栏 -> 项目** 中打开 **构建发布** 面板。在 **构建发布** 面板的 **发布平台** 中选择 **HarmonyOS**,根据需要配置 [构建选项](./native-options.md#%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) 然后点击右下方的 **构建** 按钮。 + + ![build](./publish-huawei-ohos/build.png) + + > **注意**: + > + > 1. **HarmonyOS** 展开项中的 **应用 ID 名称** 需要与在 AppGallery Connect 后台添加 HarmonyOS 应用时的包名保持一致。 + > 2. 由于 **HarmonyOS** 已全面支持 GLES3 渲染后端,因此在 v3.4.2 中我们移除了 **HarmonyOS** 展开项中的 **Render BackEnd** 选项。 + +3. 构建完成后打开项目目录,可以看到在 `native\engine` 目录下生成了 `ohos` 文件夹,该文件夹就包含了构建生成的 HarmonyOS 工程。 + + ![package](./publish-huawei-ohos/package-ohos.png) + + > **注意**:因为 HarmonyOS 暂时不支持多目录构建,所以生成的 HarmonyOS 工程在 `native\engine` 目录下,与其他平台生成在项目目录 `build` 目录下的不同。 + + 然后使用 HUAWEI DevEco Studio 打开 `ohos` 文件夹即可执行进一步的编译运行,详情请查看下文介绍。 + +### 通过 HUAWEI DevEco Studio 编译运行 + +1. 打开 HUAWEI DevEco Studio,点击 **打开项目**,选择上个步骤构建后生成的 HarmonyOS 工程(`ohos` 文件夹)。 + + ![open-project](./publish-huawei-ohos/open-project.png) + +2. 准备签名文件,签名文件的获取方式请参考下文 **签名文件** 部分的内容。 + +3. 配置签名文件。点击 DevEco Studio 菜单栏中的 **File -> Project Structure**,选择 **Modules -> entry**,在 **Signing Configs** 页面 [配置签名信息](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ide_debug_device-0000001053822404#ZH-CN_TOPIC_0000001154985555__section19238119191816)。 + + ![sign configs](./publish-huawei-ohos/sign-configs-debug.png) + + 然后继续在 **Project -> Signing Configs** 中配置签名信息。 + + 设置完成并保存后,配置的签名信息可以在项目目录下的 `native\engine\ohos\entry\build.gradle` 文件中查看。 + + 根据构建类型(Debug/Release)的不同以及是否带签名信息,开发者可根据需要自行组合配置,详情请参考 [编译构建生成 HAP](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/build_hap-0000001053342418)。 + + ![build variants](./publish-huawei-ohos/build-variants.png) + +4. 然后点击菜单栏中的 **Build -> Build Hap(s)/APP(s) -> Build Hap(s)** 项,即可执行编译流程,生成 `.hap` 文件。 + + ![build hap](./publish-huawei-ohos/build-hap.png) + + 编译完成后可以在项目目录的 `native\engine\ohos\build\outputs\hap` 目录下看到生成了带有签名信息的 `.hap` 文件。 + + ![output](./publish-huawei-ohos/output.png) + + 所有带/不带签名信息的 `.hap` 文件则是全部生成在项目目录的 `native\engine\ohos\entry\build\outputs\hap` 目录下。 + + ![output](./publish-huawei-ohos/debug-output.png) + +5. 使用 USB 连接 HarmonyOS 系统的华为设备,然后点击菜单栏中的 **Run -> Run ‘entry’**,或者点击上方的运行按钮,即可将 HarmonyOS 应用运行到设备上。详情可参考 [运行 HarmonyOS 应用](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/run_phone_tablat-0000001064774652)。 + + ![run project](./publish-huawei-ohos/run-project.png) + + > **注意**:若运行到设备上时发现找不到图标,可检查一下项目目录下的 `native\engine\ohos\entry\src\main\config.json` 中是否有 `installationFree` 字段并为 `true`,将其改为 `false` 即可。 + +6. 若需要上传并发布 HarmonyOS 应用到华为应用市场,具体流程请参考官方文档 [发布 HarmonyOS 应用指南](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-harmonyapp-releaseharmonyapp)。 + +### 签名文件 + +HarmonyOS 应用是通过数字证书(`.cer` 文件)和 HarmonyAppProvision 文件(`.p7b` 文件)来保证应用的完整性。首先需要通过 **DevEco Studio** 生成密钥和证书请求文件,再通过证书请求文件在 AppGallery Connect 申请用于发布/调试的数字证书和 Profile 文件。 + +#### 生成密钥和证书请求文件 + +在 **DevEco Studio** 中点击上方菜单栏的 **Build -> Generate Key and CSR**,生成密钥(`.p12` 文件),然后再生成证书请求文件(`.csr`)。详情请参考 [生成密钥和证书请求文件](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/publish_app-0000001053223745#ZH-CN_TOPIC_0000001154985553__section7209054153620)。 + +#### 生成并下载数字证书(`.cer` 文件) + +- 进入 [AppGallery Connect 网站](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html),选择 **用户与访问 -> 证书管理**。 + + ![provision profile](./publish-huawei-ohos/cer-file.png) + +- 点击右上角的 **新增证书** 按钮填写信息,其中: + + - **证书类型**:根据需要选择 **发布证书**/**调试证书**。 + + - **证书请求文件**:选择上一个步骤在 **DevEco Studio** 中生成的 `.csr` 文件。 + +- 信息填写完成后点击 **提交**,便会生成一个发布/调试证书(`.cer` 文件),点击证书后面的 **下载** 按钮将其下载到本地。 + +更多内容可参考 [申请数字证书](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-harmonyapp-debugharmonyapp#h1-1598336089667)。 + +#### 生成并下载 HarmonyAppProvision Profile 文件(`.p7b` 文件) + +- 进入 [AppGallery Connect 网站](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html),选择 **我的项目**,选择之前创建的 HarmonyOS 项目和应用。 + +- 点击左侧的 **HarmonyOS 应用 -> HAP Provision Profile 管理** 项,然后点击页面右上方的 **添加** 按钮来创建 `.p7b` 文件。 + + ![provision profile](./publish-huawei-ohos/provision-profile.png) + + - **类型** 可根据需要选择 **发布**/**调试** 类型,但要与上一步骤中的证书类型保持一致。 + - **选择证书**:选择上一步骤生成的 `.cer` 文件。 + - **选择设备**:请参考 [注册调试设备](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-harmonyapp-debugharmonyapp#h1-1598336280693)。当选择 **发布** 类型时不需要配置该项。 + +- 设置完成后点击 **提交**,将生成的 `.p7b` 文件下载到本地。 + +更多内容可参考 [申请 Profile 文件](https://developer.huawei.com/consumer/cn/doc/distribution/app/agc-harmonyapp-debugharmonyapp#h1-1598336409517)。 + +## 已知问题 + +- WebView 不支持 `touch` 事件,触碰无响应。 +- VideoPlayer 播放远程视频不可拖动。 +- EditBox 全屏不可缩小,键盘收回后不重新布局。 +- 重力/加速传感器延迟 500ms 左右,跟手性差。 + +## 相关参考链接 + +- [AppGallery Connect 网站](https://developer.huawei.com/consumer/cn/service/josp/agc/index.html) +- [HUAWEI DevEco Studio 使用指南](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/tools_overview-0000001053582387) +- [HarmonyOS 应用调试](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ide_debug_device-0000001053822404) +- [HarmonyOS 应用发布](https://developer.harmonyos.com/cn/docs/documentation/doc-guides/publish_app-0000001053223745) diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/app-info.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/app-info.png new file mode 100644 index 0000000000..750f8137c8 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/app-info.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/build-hap.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/build-hap.png new file mode 100644 index 0000000000..d052e7a13e Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/build-hap.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/build-variants.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/build-variants.png new file mode 100644 index 0000000000..d061788cbd Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/build-variants.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/build.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/build.png new file mode 100644 index 0000000000..de2251f352 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/cer-file.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/cer-file.png new file mode 100644 index 0000000000..b78094b28d Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/cer-file.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/debug-output.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/debug-output.png new file mode 100644 index 0000000000..bf8a94b6a8 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/debug-output.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/dev-setting.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/dev-setting.png new file mode 100644 index 0000000000..8f1e26a010 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/dev-setting.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/open-project.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/open-project.png new file mode 100644 index 0000000000..32625f32f3 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/open-project.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/output.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/output.png new file mode 100644 index 0000000000..032c121905 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/output.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/package-ohos.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/package-ohos.png new file mode 100644 index 0000000000..99a0833314 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/package-ohos.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/preferences.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/preferences.png new file mode 100644 index 0000000000..7ff73f0f8a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/preferences.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/provision-profile.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/provision-profile.png new file mode 100644 index 0000000000..c29307f397 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/provision-profile.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/run-project.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/run-project.png new file mode 100644 index 0000000000..f7904309d7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/run-project.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/sign-configs-debug.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/sign-configs-debug.png new file mode 100644 index 0000000000..fb6f817556 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/sign-configs-debug.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-ohos/sign-configs.png b/versions/4.0/zh/editor/publish/publish-huawei-ohos/sign-configs.png new file mode 100644 index 0000000000..dfd3f7fe15 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-ohos/sign-configs.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-quick-game.md b/versions/4.0/zh/editor/publish/publish-huawei-quick-game.md new file mode 100644 index 0000000000..0f54bc040b --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-huawei-quick-game.md @@ -0,0 +1,91 @@ +# 发布到华为快游戏 + +## 环境配置 + +- 下载 [华为快应用加载器](https://developer.huawei.com/consumer/cn/doc/Tools-Library/quickapp-ide-download-0000001101172926#section9347192715112),并安装到华为手机上(建议 Android Phone 6.0 或以上版本) + +- PC 端全局安装 [nodejs-8.1.4](https://nodejs.org/zh-cn/download/) 或以上版本 + +## 发布流程 + +使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板。在 **构建发布** 面板的 **发布平台** 中选择 **华为快游戏**。 + +![build](./publish-huawei-quick-game/build.png) + +通用构建选项的设置请参考 [通用构建选项](build-options.md),华为快游戏特有的构建选项如下: + +![huawei-options](./publish-huawei-quick-game/huawei-build.png) + +| 构建选项 | 可选 | 说明 | 字段名(用于命令行发布)| +| :----- | :-- | :-- | :-- | +| 初始场景分包 | 可选项 | 勾选后,首场景及其相关的依赖资源会被构建到发布包目录 `assets` 下的内置 Asset Bundle — [start-scene](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中,提高初始场景的资源加载速度。 | `startSceneAssetBundle` | +| 资源服务器地址 | 可选项 | 若 **不填写** 该项,则发布包目录下的 `remote` 文件夹将会被打包到构建后生成的 rpk 包中。
若 **填写**,则不会打包进 rpk。开发者需要在构建后手动将发布包目录下的 `remote` 文件夹上传到所填写的资源服务器地址上,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 | `remoteServerAddress` | +| 游戏包名 | 必填项 | 确保与原生应用的包名不一致,由 **数字**、**字母**、**.** 组成。必须以字母开头,以数字或字母结尾,同时必须包含 **.**,长度不能超过 255 字节。例如 `com.example.demo` | `package` | +| 桌面图标 | 必填项 | 点击输入框后面的放大镜图标按钮选择所需的图标。构建时,图标将会被构建到华为快游戏的 rpk 中。桌面图标建议使用 png 图片。 | icon | +| 游戏版本名称 | 必填项 | 游戏版本名称是真实的版本,如:1.0.0 | `versionName` | +| 游戏版本号 | 必填项 | **游戏版本号** 与 **游戏版本名称** 不同,**游戏版本号** 主要用于区别版本更新。每次提交审核时游戏版本号都要比上次提交审核的值至少 +1,一定不能等于或者小于上次提交审核的值,建议每次提交审核时游戏版本号递归 +1
**注意**:**游戏版本号** 必须为正整数。 | `versionCode` | +| 支持的最小平台版本号 | 必填项 | 用于兼容性检查,避免上线后在低版本平台运行导致不兼容。根据华为快游戏的要求目前这个值必须大于或等于 1035。 | `minPlatformVersion` | +| 自定义 manifest 文件路径 | 可选项 | 华为快游戏扩展功能。使用时需要选择 json 文件,文件中的数据类型要求为 json 格式。
**注意**:当 json 数据的 key 值为 `package`、`appType`、`name`、`versionName`、`versionCode`、`icon`、`minPlatformVersion`、`config`、`display` 时不可用。否则在构建时会被 **应用包名**、**应用名称**、**应用图标**、**应用版本号**、**应用版本名称** 等数据覆盖。 | `manifestPath` | +| 屏幕方向 | 可选项 | 可选值包括 `landscape` 和 `portrait`。构建后会写入到发布包目录下的 `manifest.json` 中。| `deviceOrientation` | +| 是否全屏 | 可选项 | 若勾选,则应用运行后处于全屏模式,全屏模式下状态栏也会被覆盖。 | `fullScreen` | +| logLevel | 可选项 | 日志等级 | `logLevel` | +| 密钥库 | 可选项 | 若勾选该项,表示构建 rpk 包时默认使用的是 Creator 自带的证书,仅用于 **调试** 时使用。若 rpk 包要用于提交审核,则构建时不要勾选该项。
若不勾选该项,则需要手动配置签名证书。| `useDebugKey` | +| certificate.pem 路径
private.pem 路径 | 可选项 | 如果不勾选 **密钥库**,则需要配置签名文件 **certificate.pem 路径** 和 **private.pem 路径**,此时构建后生成的是可以 **直接发布** 的 rpk 包。可通过输入框右边的放大镜图标按钮来选择对应的签名文件,或者也可以参考下方的 **生成签名文件**。
**注意**:这两个签名文件建议不要放在发布包 `build/huawei-quick-game` 目录下,否则每次构建时都会清空该目录,导致文件丢失。| `privatePemPath`、`certificatePemPath` | + +- **生成签名文件** + + 有以下两种方式可以生成签名文件: + + - 通过 **构建发布** 面板 **certificate.pem 路径** 后的 **新建** 按钮生成 + + - 通过命令行生成 release 签名 + + 用户需要通过 openssl 命令等工具生成签名文件 private.pem、certificate.pem。 + + ```bash + # 通过 openssl 命令工具生成签名文件 + openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out certificate.pem + ``` + + > **注意**:openssl 工具在 linux 或 Mac 环境下可在终端直接打开。而在 Windows 环境下则需要安装 openssl 工具并且配置系统环境变量,配置完成后需重启 Creator。 + +### 构建 + +**构建发布** 面板的构建选项设置完成后,点击 **构建并生成** 按钮。
+完成后点击 **构建任务** 左下角的文件夹图标按钮打开项目发布包,可以看到在默认发布路径 `build` 目录下生成了 `huawei-quick-game`(以具体的构建任务名为准)文件夹,该文件夹就是导出的华为快游戏工程目录和 rpk,rpk 包在 `build/huawei-quick-game/dist` 目录下。 + +![package](./publish-huawei-quick-game/package.png) + +若需要修改生成的 rpk 包,在修改完成后点击 **构建任务** 右下角的 **生成** 按钮,即可在不重新构建的情况下重新生成 rpk 包。 + +## 将打包出来的 rpk 运行到手机上 + +将打包出来的 rpk 运行到手机上有以下两种方式: + +1. 点击 **构建任务** 右下角的 **调试** 按钮,会弹出一个 **快游戏调试工具** 面板。在 **手机列表** 栏目选择手机(如果连接了多台手机),然后在 **快游戏调试工具** 栏点击 **运行** 按钮。 + + 这时 rpk 会被推送到之前在手机上已安装完成的 **华为快应用加载器** 上(如有读写等权限弹出请允许),即可在手机上打开 rpk。 + + ![play](./publish-huawei-quick-game/play.png) + +2. 将构建生成的 rpk 包拷贝到手机内部存储目录下。然后在手机上打开 **华为快应用加载器** 后,点击手机的返回键会弹出一个列表,选择第一个选项 **本地安装**,选择路径为放置 rpk 的路径,即可将 rpk 运行到手机上。 + +## 分包 rpk + +分包加载,即把游戏内容按一定规则拆分在几个包里,在首次启动的时候动的消耗时间。 + +若要使用该功能需要在 Creator 编辑器中配置 [小游戏分包](subpackage.md),设置完成后在构建时就会自动分包。构建完成后,会在 `build/huawei-quick-game/dist` 目录下生成 **.rpk** 文件。 + +> **注意**:目前华为快游戏不支持同时下载多个分包,需只下载必要的包,这个必要的包称为 **主包**,开发者可以在主包内触发下载其他子包,这样可以有效降低首次启要下载多个分包时请按顺序下载。 + +## 华为快游戏环境的资源管理 + +华为快游戏与微信小游戏类似,都存在着包体限制, 华为快游戏允许上传的代码包总大小为 **10MB**,超过的部分必须通过网络请求下载。 + +当包体过大时,可在 **构建发布** 面板配置 **资源服务器地址** 选项,将低加载优先级的资源上传到远程服务器,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 + +游戏启动之后引擎会自动下载远程服务器地址中的资源,资源下载后引擎的缓存管理器会记录资源的保存路径,用于在缓存空间不足时自动删除部分缓存的游戏资源。请参考 [缓存管理器](../../asset/cache-manager.md)。 + +## 相关参考链接 + +[华为快游戏开发文档](https://developer.huawei.com/consumer/cn/doc/quickApp-Guides/quickgame-dev-runtimegame-guide-0000001159778255) diff --git a/versions/4.0/zh/editor/publish/publish-huawei-quick-game/build.png b/versions/4.0/zh/editor/publish/publish-huawei-quick-game/build.png new file mode 100644 index 0000000000..65a08f4d71 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-quick-game/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-quick-game/huawei-build.png b/versions/4.0/zh/editor/publish/publish-huawei-quick-game/huawei-build.png new file mode 100644 index 0000000000..5a9c45b64c Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-quick-game/huawei-build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-quick-game/package.png b/versions/4.0/zh/editor/publish/publish-huawei-quick-game/package.png new file mode 100644 index 0000000000..4b0014268b Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-quick-game/package.png differ diff --git a/versions/4.0/zh/editor/publish/publish-huawei-quick-game/play.png b/versions/4.0/zh/editor/publish/publish-huawei-quick-game/play.png new file mode 100644 index 0000000000..f46eec43af Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-huawei-quick-game/play.png differ diff --git a/versions/4.0/zh/editor/publish/publish-in-command-line.md b/versions/4.0/zh/editor/publish/publish-in-command-line.md new file mode 100644 index 0000000000..53a4594c11 --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-in-command-line.md @@ -0,0 +1,121 @@ +# 命令行发布项目 + +命令行发布项目可以帮助开发者构建自己的自动化构建流程,通过修改命令行的参数来达到不同的构建需求。 + +## 命令行发布参考 + +**例如**:构建 web-desktop 平台、Debug 模式 + +- Mac + + ```bash + /Applications/CocosCreator/Creator/3.0.0/CocosCreator.app/Contents/MacOS/CocosCreator --project projectPath --build "platform=web-desktop;debug=true" + ``` + +- Windows + + ```bash + ...\CocosCreator.exe --project projectPath --build "platform=web-desktop;debug=true" + ``` + +目前命令行构建除了必填项外,如果不传递一律使用默认值来构建,具体参数默认值请参考下方描述以及平台的参数介绍。 + +## 进程退出码 + +- **32** 构建失败 —— 构建参数不合法 +- **34** 构建失败 —— 构建过程出错失败,详情请参考构建日志 +- **36** 构建成功 + +## 构建参数 + +- `--project`:必填,指定项目路径 +- `--engine`:选填,指定自定义引擎路径 +- `--build`:指定构建项目使用的参数 + + 在 `--build` 后如果没有指定参数,则会使用 Cocos Creator 中 **构建发布** 面板当前的平台、模板等设置来作为默认参数。如果指定了其他参数设置,则会使用指定的参数来覆盖默认参数。可选择的参数有: + + - `configPath` - 参数文件路径。如果定义了这个字段,那么构建时将会按照 `json` 文件格式来加载这个数据,并作为构建参数。这个参数可以自己修改也可以直接从构建面板导出,当配置和 configPath 内的配置冲突时,configPath 指定的配置将会被覆盖。 + - `stage` - 指定构建模式,默认为 'build',可选 'make' | 'build' | 'bundle' 等 + - `logDest` - 指定日志输出路径 + - `includedModules` - 定制引擎打包功能模块,只打包需要的功能模块。具体有哪些功能模块可以参考引擎仓库根目录下 **cc.config.json**([GitHub](https://github.com/cocos/cocos-engine/blob/3d/cc.config.json) | [Gitee](https://gitee.com/mirrors_cocos-creator/engine/blob/3d/cc.config.json))文件中的 `features` 字段。 + - `outputName` - 构建后生成的发布包文件夹名称。 + - `name` - 游戏名称 + - `platform` - 必填,构建的平台,具体名称参考面板上对应扩展名称即可 + - `buildPath` - 指定构建发布包生成的目录,默认为项目目录下的 `build` 目录。可使用绝对路径或者相对于项目的路径(例如 `project://release`)。从 v3.4.2 开始支持类似 `../` 这样的相对路径。 + - `startScene` - 主场景的 UUID 值(参与构建的场景将使用上一次的编辑器中的构建设置),未指定时将使用参与构建场景的第一个 + - `scenes` - 参与构建的场景信息,未指定时默认为全部场景,具体格式为:`{}` + - `debug` - 是否为 debug 模式,默认关闭 + - `replaceSplashScreen` - 是否替换插屏,默认关闭 + - `md5Cache` - 是否开启 md5 缓存,默认关闭 + - `mainBundleCompressionType` - 主包压缩类型,具体选项值可参考文档 [Asset Bundle — 压缩类型](../../asset/bundle.md#压缩类型)。 + - `mainBundleIsRemote` - 配置主包为远程包 + - `packages` - 各个扩展支持的构建配置参数,需要存放的是对于数据对象的序列化字符串,具体可以参考下文。 + +Cocos Creator 3.0 各个平台的构建会作为独立的扩展嵌入到 **构建发布** 面板中,因而各个平台的构建参数位置也不同。各个平台的构建参数会配置在 `packages` 字段中,例如:为微信小游戏指定构建参数,配置大体如下: + +``` +{ + taskName: 'wechatgame', + packages: { + wechatgame: { + appid: '*****', + } + } +} +``` + +之后在构建扩展支持对外开放,其他扩展的配置参数也会通过同样的方式嵌入到 **构建发布** 面板中。具体各个平台的参数字段 **请参照各个平台文档中的参数介绍**,最好是通过 **构建发布** 面板的 **导出** 功能来获取配置参数,更加方便快捷。目前依旧兼容旧版本的参数进行构建,但之后将会移除该兼容处理,请尽快升级配置参数。 + +## 命令行执行独立发布 Bundle + +1. 打开 Bundle 构建面板,配置好选项后,导出配置(自 3.8.2 起) + + ![export-config](./../../asset/bundle/export-config.png) + 导出的配置大致如下: + + ```json + { + "buildTaskIds": [ + "1699344873959" + ], + "dest": "project://build/build-bundle", + "id": "buildBundle", + "bundleConfigs": [ + { + "root": "db://assets/resources", + "output": true + } + ], + "taskName": "build bundle db://assets/resources" + } + + ``` + +2. 在命令行中执行以下命令: + +- Mac + + ```bash + /Applications/CocosCreator/Creator/3.0.0/CocosCreator.app/Contents/MacOS/CocosCreator --project projectPath + --build "stage=bundle;configPath=./bundle-build-config.json;" + ``` + +- Windows + + ```bash + ...\CocosCreator.exe --project projectPath --build "stage=bundle;configPath=./bundle-build-config.json;" + ``` + +命令行发布 Bundle 与普通的命令行构建类似,不过 `stage` 参数需要指定为 `bundle`,同时将在 bundle 构建面板上导出的配置指定为 `configPath` 参数。 + +3. 在命令行中执行“只构建脚本” +- Mac && Windows + ```bash + --build "buildScriptsOnly=true" + ``` + +## 在 Jenkins 上部署 + +Cocos Creator 命令行运行的时候也是需要 GUI 环境的。如果你的 Jenkins 无法使用 Cocos Creator 命令行运行,一个解决办法是:确保 Jenkins 运行在 agent 模式下,这样才能访问到 WindowServer。详情请参考 。 + +如果你的 Jenkins 在 Windows 下无法编译,请在 Windows 的 **控制面板 -> 管理工具 -> 服务** 中为 Jenkins 的服务指定一个本地用户,然后重启电脑就可以了。不必单独设置一个 master-slave 模式。 diff --git a/versions/4.0/zh/editor/publish/publish-mini-game.md b/versions/4.0/zh/editor/publish/publish-mini-game.md new file mode 100644 index 0000000000..ba84920ccc --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-mini-game.md @@ -0,0 +1,16 @@ +# 发布到小游戏平台 + +- [发布到支付宝小游戏](publish-alipay-mini-game.md) +- [发布到淘宝小游戏](publish-taobao-mini-game.md) + - [启用淘宝小游戏引擎插件](taobaominigame-plugin.md) +- [发布到抖音小游戏](publish-bytedance-mini-game.md) +- [发布到华为快游戏](publish-huawei-quick-game.md) +- [发布到 OPPO 小游戏](publish-oppo-mini-game.md) +- [发布到 vivo 小游戏](publish-vivo-mini-game.md) +- [发布到荣耀小游戏](publish-honor-mini-game.md) +- [发布到百度小游戏](publish-baidu-mini-game.md) +- [发布到微信小游戏](publish-wechatgame.md) + - [启用微信小游戏引擎插件](wechatgame-plugin.md) + - [接入微信 PC 小游戏](publish-pc-wechatgame.md) +- [开放数据域](build-open-data-context.md) +- [小游戏分包](subpackage.md) diff --git a/versions/4.0/zh/editor/publish/publish-native-index.md b/versions/4.0/zh/editor/publish/publish-native-index.md new file mode 100644 index 0000000000..afaaea800d --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-native-index.md @@ -0,0 +1,24 @@ +# 原生平台发布通用基础 + +Cocos Creator 支持发布到多个平台的原生应用程序: +- [发布 iOS 应用程序](./ios/index.md) +- [发布 Android 应用程序](./android/index.md) +- [发布 HUAWEI AppGallery Connect 应用程序](./publish-huawei-agc.md) +- [发布 HUAWEI Harmony 应用程序](./publish-huawei-ohos.md) +- [发布 HarmonyOS Next 应用程序](./publish-openharmony.md) +- [发布 macOS 桌面端应用](./mac/index.md) +- [发布 Windows 桌面端应用](./windows/index.md) + +以下是发布到原生平台时可能涉及到的通用知识点: +- [安装配置原生环境](setup-native-development.md) +- [构建发布面板](build-panel.md) +- [通用构建选项介绍](build-options.md) +- [原生平台通用构建选项](native-options.md) +- [原生平台 JavaScript 调试](debug-jsb.md) +- [构建流程简介与常见错误处理](build-guide.md) + +如果要发布到 [小游戏](./publish-mini-game.md) 和 [Web](./publish-web.md) 平台,请参考对应文档。 + +如果要对原生平台进行二次开发,请参考:[原生开发指南](../../native/overview.md)。 + +如果要对引擎进行定制,请参考:[引擎定制工作流](../../../zh/advanced-topics/engine-customization.md)。 diff --git a/versions/4.0/zh/editor/publish/publish-native/android-options.png b/versions/4.0/zh/editor/publish/publish-native/android-options.png new file mode 100644 index 0000000000..a429379f98 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/android-options.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/android-studio.png b/versions/4.0/zh/editor/publish/publish-native/android-studio.png new file mode 100644 index 0000000000..1916313261 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/android-studio.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/build-progress-windows.png b/versions/4.0/zh/editor/publish/publish-native/build-progress-windows.png new file mode 100644 index 0000000000..cbe07db4b5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/build-progress-windows.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/cocos-console-log.png b/versions/4.0/zh/editor/publish/publish-native/cocos-console-log.png new file mode 100644 index 0000000000..0aaa10ec77 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/cocos-console-log.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/encrypt-js.png b/versions/4.0/zh/editor/publish/publish-native/encrypt-js.png new file mode 100644 index 0000000000..ef3cfbea4a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/encrypt-js.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/gpg-input-sdk-keys.png b/versions/4.0/zh/editor/publish/publish-native/gpg-input-sdk-keys.png new file mode 100644 index 0000000000..14a0370fef Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/gpg-input-sdk-keys.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/ios-options.png b/versions/4.0/zh/editor/publish/publish-native/ios-options.png new file mode 100644 index 0000000000..1a3368fa05 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/ios-options.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/ios-xcode.png b/versions/4.0/zh/editor/publish/publish-native/ios-xcode.png new file mode 100644 index 0000000000..b447d27cbc Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/ios-xcode.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/modify_abi.png b/versions/4.0/zh/editor/publish/publish-native/modify_abi.png new file mode 100644 index 0000000000..3cbe4c3419 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/modify_abi.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/native-build-buttons.png b/versions/4.0/zh/editor/publish/publish-native/native-build-buttons.png new file mode 100644 index 0000000000..fbdea7ef80 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/native-build-buttons.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/native-common.png b/versions/4.0/zh/editor/publish/publish-native/native-common.png new file mode 100644 index 0000000000..8da739db02 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/native-common.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/native-directory.png b/versions/4.0/zh/editor/publish/publish-native/native-directory.png new file mode 100644 index 0000000000..fd70851982 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/native-directory.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/native-options.png b/versions/4.0/zh/editor/publish/publish-native/native-options.png new file mode 100644 index 0000000000..44d552ff0a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/native-options.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/native-platform.png b/versions/4.0/zh/editor/publish/publish-native/native-platform.png new file mode 100644 index 0000000000..80c20c5ec5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/native-platform.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/native.png b/versions/4.0/zh/editor/publish/publish-native/native.png new file mode 100644 index 0000000000..1798ef85e5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/native.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/sdk-android-instant.png b/versions/4.0/zh/editor/publish/publish-native/sdk-android-instant.png new file mode 100644 index 0000000000..50b1fb5d62 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/sdk-android-instant.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/windows-options.png b/versions/4.0/zh/editor/publish/publish-native/windows-options.png new file mode 100644 index 0000000000..667fad34d7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/windows-options.png differ diff --git a/versions/4.0/zh/editor/publish/publish-native/windows-vs.png b/versions/4.0/zh/editor/publish/publish-native/windows-vs.png new file mode 100644 index 0000000000..8d6f2fce38 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-native/windows-vs.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony.md b/versions/4.0/zh/editor/publish/publish-openharmony.md new file mode 100644 index 0000000000..7e8f62b966 --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-openharmony.md @@ -0,0 +1,173 @@ +# 发布到 HarmonyOS Next + +自 Cocos Creator v3.8.5 起,支持发布到 HarmonyOS Next 平台。 + +> 1. Cocos Creator 3.8.6 对 HarmonyOS Next 性能、功耗做了深度优化 +> 2. Cocos Creator 3.8.7 对原生通信做了调整,更易使用。适配了鼠标键盘,支持鸿蒙PC。 + +## 准备工作 + +### 安装 Cocos Creator + +1. Cocos Creator 下载传送门(版本>=3.8.5):[Cocos Creator](https://www.cocos.com/creator-download) + +### 安装 DevEco Studio + +1. 进入 [DevEco Studio IDE](https://developer.huawei.com/consumer/cn/download/) 下载页面(需要登录华为开发者账号),选择对应平台的版本,点击右边的下载按钮,如下图: + + ![](./publish-openharmony/document_image_rId3.png) + +2. 解压目录,双击 deveco-studio-5.0.5.310.exe 进行安装,点击 Next,如下图: + + ![](./publish-openharmony/document_image_rId33.png) + +3. 选择安装路径,点 Next,如下图: + + ![](./publish-openharmony/document_image_rId34.png) + +4. 根据需求配置,点击 Next,如下图: + + ![](./publish-openharmony/document_image_rId35.png) + +5. 点击安装,如下图: + + ![](./publish-openharmony/document_image_rId36.png) + +6. 等待安装,如下图: + + ![](./publish-openharmony/document_image_rId37.png) + +7. 安装完成,如下图: + + ![](./publish-openharmony/document_image_rId38.png) + + +### Creator 构建 HarmonyOS Next工程 + +1. 使用 Cocos Creator 打开一个项目,以下以 [cocos-test-projects](https://github.com/cocos/cocos-test-projects/tree/v3.8) 为例,如下图: + + ![](./publish-openharmony/document_image_rId53.png) + + +2. 选择标题栏中的 Project-\>Build,也可以使用 Ctrl+Shift+B 的快捷键,如下图: + + ![](./publish-openharmony/document_image_rId55.png) + +3. 点击新建任务,如下图: + + ![](./publish-openharmony/document_image_rId56.png) + +4. 选择 HarmonyOS Next + + ![](./publish-openharmony/document_image_rId57.png) + + +5. 配置工程名称、配置开始场景与包含的其他场景,配置 Debug/Release,如下图: + + ![](./publish-openharmony/document_image_rId58.png) + +6. 配置 Javascript 引擎,目前支持V8、Ark、JSVM,如下图: + + ![](./publish-openharmony/document_image_rId77.png) + + > **注意**:建议使用 JSVM 以获得最优的游戏性能。 + +7. 目前 Make 与 Run 功能还未实现,请使用 DevEco Studio 打开工程 + ![](./publish-openharmony/document_image_rId59.png) + +### DevEco Studio 编译运行 + +1. 使用[DevEcoStudio](https://developer.harmonyos.com/cn/develop/deveco-studio#download),打开工程,如下图: + + ![](./publish-openharmony/document_image_rId62.png) +2. 找到工程目录(native/engine/harmonyos-next)并点击打开,如下图(下图是以[cocos-test-projects](https://github.com/cocos/cocos-test-projects)为例): + + ![](./publish-openharmony/document_image_rId64.png) + +3. 配置签名,如下图: + + ![](./publish-openharmony/document_image_rId65.png) + +4. 插入设备,点击运行,如下图: + + ![](./publish-openharmony/document_image_rId66.png) + +执行成功之后,就能看到效果了。 + +**注意:如果出现安装失败,可能是以下几种原因导致的。** + +> 1. 包名冲突,请卸载之前安装的包,或者修改自己的 App 包名。 +> 2. 签名失效,请进入 File -> Project Structure -> Signing Configs 配置签名 +> 3. 签名更新,由于配置的自动签名,有可能签名会变,请卸载之前安装的 App 再启动即可。 + +## 原生工程目录 + +发布成功后,项目目录内容如下: + +- **AppScope/app.json5**:应用的全局配置信息。 +- **entry/src/main/**:应用的主模块,编译构建生成一个 HAP 包。 + - **cpp/**:存放 so 包导出的接口描述文件和依赖配置文件,比如 libcocos.so。 + - **ets/**:存放 ArkTS/TS 相关文件。 + - **cocos/oh-adapter/sys-ability-polyfill.js**:一些系统接口的Ark调用方法。 + - **cocos/WorkerManager.ets**:worker 管理类。 + - **common/PortProxy.ts**:worker 代理类,用于主线程和 worker 线程通信。 + - **components/**:Videoplayer、Webview、Editbox 的 ArkTS 实现。 + - **entryability/EntryAbility.ts**:ability,程序入口,管理 ability 生命周期和窗口。 + - **pages/index.ets**:page 页面用于渲染页面,初始化 worker,系统接口的调用通常在此调用。 + - **workers/cocos_worker.ts**:worker 线程文件,通过 worker 接收主线程消息。 + - **resources/**:resources 文件夹,存放 cocos 游戏 js 文件和资源。 + - **module.json5**:应用配置文件,包括权限、旋转属性、启动设置等。 +- **entry/build-profile.json5**:应用级配置信息,包括签名、产品配置等。 +- **hvigorfile.ts**:应用级编译构建任务脚本。 +- **oh-package.json5**:工程级依赖配置文件,用于存放依赖库的信息。 + +## JSVM、V8、ArkTS 的选择 + +> 推荐使用 JSVM! + +由于历史原因,目前支持 JSVM、V8、ArkTS 三种脚本机制,区别如下: +![](./publish-openharmony/document_image_rId78.png) + +1. 不管如何选择, 项目中的 ArkTS 代码均在方舟引擎环境中执行。 +2. 选择 Ark 后,引擎中的 TS/JS 代码在方舟引擎环境中执行。 +3. 选择 V8 后,引擎中的 TS/JS 代码在 V8 虚拟机中执行。 +4. 选择 JSVM 后,引擎中的 TS/JS 代码在 JSVM 中执行。 + +不同的选择,也影响 JIT和热更新功能 + +| 引擎 | JIT | 热更新 | +| --- | --- | --- | +| JSVM | 支持 | 支持 | +| V8 | 不支持 | 支持 | +| Ark | 暂不支持 | 暂不支持 | + +因此, JSVM 是综合来看的最佳选择。 + +## Cocos 与 HarmonyOS Next 原生通信 + +详细的通信机制,请参考 [基于反射机制实现 JavaScript 与 HarmonyOS Next 系统原生通信](../../advanced-topics/arkts-reflection.md)。 + +## 几个注意事项 + +另外,因为 HarmonyOS Next 还在不断完善当中,因此有些已知问题。这些问题都会在后续的版本解决。 + +- Ark 不支持 Restart,JSVM 与 V8 是支持的。 +- 编译失败时,可能是内存不足导致,退出部分应用,重新 build 试试; + >> + >> ![](./publish-openharmony/document_image_rId72.png) +- 更新IDE,编译报错,如下图: + >> ![](./publish-openharmony/document_image_rId75.png) +- Mac 版 IDE 编译报错,报错信息为: + + ```txt + npm ERR! Your cache folder contains root-owned files, due to a bug in + npm ERR! previous versions of npm which has since been addressed. + ``` + + 解决方法: + + 在设备终端中执行命令 `sudo chown -R 502:20 "/Users/修改为你的设备用户名/.npm"`,之后重新编译项目 + +## 构建流程简介与常见问题指南 + +前往 [构建流程简介与常见问题指南](build-guide.md) 查看。 diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId0.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId0.png new file mode 100644 index 0000000000..f527723c36 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId0.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId10.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId10.png new file mode 100644 index 0000000000..0a0964143e Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId10.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId11.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId11.png new file mode 100644 index 0000000000..d93d3a5573 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId11.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId12.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId12.png new file mode 100644 index 0000000000..c0fb8b31c4 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId12.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId13.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId13.png new file mode 100644 index 0000000000..caa00a79b6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId13.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId14.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId14.png new file mode 100644 index 0000000000..7851cd7f61 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId14.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId15.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId15.png new file mode 100644 index 0000000000..9c6e014e20 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId15.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId16.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId16.png new file mode 100644 index 0000000000..2f52708290 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId16.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId17.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId17.png new file mode 100644 index 0000000000..4bad29dd3f Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId17.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId18.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId18.png new file mode 100644 index 0000000000..5fdaf5a4ea Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId18.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId19.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId19.png new file mode 100644 index 0000000000..eeaacdc850 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId19.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId2.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId2.png new file mode 100644 index 0000000000..a8433b8f09 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId2.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId20.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId20.png new file mode 100644 index 0000000000..e75e8b1e98 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId20.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId21.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId21.png new file mode 100644 index 0000000000..47f919b00a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId21.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId22.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId22.png new file mode 100644 index 0000000000..2d35034ecb Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId22.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId23.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId23.png new file mode 100644 index 0000000000..23ca956428 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId23.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId24.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId24.png new file mode 100644 index 0000000000..c9685d9be3 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId24.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId25.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId25.png new file mode 100644 index 0000000000..c0f6545e3a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId25.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId26.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId26.png new file mode 100644 index 0000000000..9cb099d520 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId26.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId27.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId27.png new file mode 100644 index 0000000000..f80550d487 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId27.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId28.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId28.png new file mode 100644 index 0000000000..9cb099d520 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId28.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId29-1.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId29-1.png new file mode 100644 index 0000000000..a8b8118cf2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId29-1.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId29-2.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId29-2.png new file mode 100644 index 0000000000..afe23781db Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId29-2.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId29.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId29.png new file mode 100644 index 0000000000..f3ff7a55a6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId29.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId3.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId3.png new file mode 100644 index 0000000000..25a989895a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId3.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId30-1.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId30-1.png new file mode 100644 index 0000000000..9d3f01f926 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId30-1.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId30.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId30.png new file mode 100644 index 0000000000..bcb0cd3ec7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId30.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId31.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId31.png new file mode 100644 index 0000000000..a9a9b561e7 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId31.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId33.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId33.png new file mode 100644 index 0000000000..b312bc8507 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId33.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId34.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId34.png new file mode 100644 index 0000000000..70578bb06d Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId34.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId35.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId35.png new file mode 100644 index 0000000000..3446f3f1c3 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId35.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId36.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId36.png new file mode 100644 index 0000000000..43858de3bb Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId36.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId37.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId37.png new file mode 100644 index 0000000000..9f702d491a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId37.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId38.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId38.png new file mode 100644 index 0000000000..2f5c90c94d Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId38.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId39.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId39.png new file mode 100644 index 0000000000..a5477c0e3f Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId39.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId4.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId4.png new file mode 100644 index 0000000000..f400e4c485 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId4.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId40.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId40.png new file mode 100644 index 0000000000..bb01238f5a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId40.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId41.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId41.png new file mode 100644 index 0000000000..3f0d64fefa Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId41.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId42.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId42.png new file mode 100644 index 0000000000..b186cb3e87 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId42.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId43.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId43.png new file mode 100644 index 0000000000..05297a509c Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId43.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId44.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId44.png new file mode 100644 index 0000000000..fbf4dc8015 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId44.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId45.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId45.png new file mode 100644 index 0000000000..59a65796f2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId45.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId46.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId46.png new file mode 100644 index 0000000000..22bf97320a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId46.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId47.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId47.png new file mode 100644 index 0000000000..45929a45f0 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId47.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId48.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId48.png new file mode 100644 index 0000000000..8e7aa50436 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId48.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId49.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId49.png new file mode 100644 index 0000000000..8df815e286 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId49.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId5.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId5.png new file mode 100644 index 0000000000..faae9d67ae Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId5.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId50.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId50.png new file mode 100644 index 0000000000..1d085d2806 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId50.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId51.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId51.png new file mode 100644 index 0000000000..7c8e55f452 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId51.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId52.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId52.png new file mode 100644 index 0000000000..07f65727ad Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId52.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId53.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId53.png new file mode 100644 index 0000000000..07f65727ad Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId53.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId54.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId54.png new file mode 100644 index 0000000000..08b42ce2b6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId54.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId55.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId55.png new file mode 100644 index 0000000000..9d5cc3ce7f Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId55.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId56.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId56.png new file mode 100644 index 0000000000..d511fafe39 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId56.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId57.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId57.png new file mode 100644 index 0000000000..56fca7b2af Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId57.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId58-1.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId58-1.png new file mode 100644 index 0000000000..2c26b34bd8 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId58-1.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId58.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId58.png new file mode 100644 index 0000000000..b6f96d26a4 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId58.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId59.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId59.png new file mode 100644 index 0000000000..d815ba8581 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId59.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId6.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId6.png new file mode 100644 index 0000000000..4b0b654c4f Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId6.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId60.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId60.png new file mode 100644 index 0000000000..f0327c46a6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId60.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId62.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId62.png new file mode 100644 index 0000000000..33e0dd421f Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId62.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId64.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId64.png new file mode 100644 index 0000000000..672068b131 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId64.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId65.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId65.png new file mode 100644 index 0000000000..322037a1cc Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId65.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId66.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId66.png new file mode 100644 index 0000000000..21d74841e4 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId66.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId67.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId67.png new file mode 100644 index 0000000000..dcdfd9b3f2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId67.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId68.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId68.png new file mode 100644 index 0000000000..c30ccd18eb Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId68.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId69.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId69.png new file mode 100644 index 0000000000..9c12758dd5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId69.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId7.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId7.png new file mode 100644 index 0000000000..64121595b4 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId7.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId70.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId70.png new file mode 100644 index 0000000000..c4cd799ad3 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId70.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId71.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId71.png new file mode 100644 index 0000000000..b797d42486 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId71.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId72.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId72.png new file mode 100644 index 0000000000..f9e3f48cbd Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId72.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId73.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId73.png new file mode 100644 index 0000000000..a0a1a1eeee Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId73.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId74.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId74.png new file mode 100644 index 0000000000..aad7a07845 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId74.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId75.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId75.png new file mode 100644 index 0000000000..a737193afd Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId75.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId76.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId76.png new file mode 100644 index 0000000000..4e525d5e35 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId76.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId77.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId77.png new file mode 100644 index 0000000000..830548c88c Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId77.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId78.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId78.png new file mode 100644 index 0000000000..2a1a80dc28 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId78.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId8.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId8.png new file mode 100644 index 0000000000..50ef7a0da5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId8.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId9.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId9.png new file mode 100644 index 0000000000..2884b7fe8c Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rId9.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rld_n0.png b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rld_n0.png new file mode 100644 index 0000000000..94e6f08002 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/document_image_rld_n0.png differ diff --git a/versions/4.0/zh/editor/publish/publish-openharmony/video.mp4 b/versions/4.0/zh/editor/publish/publish-openharmony/video.mp4 new file mode 100644 index 0000000000..4d8606e831 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-openharmony/video.mp4 differ diff --git a/versions/4.0/zh/editor/publish/publish-oppo-mini-game.md b/versions/4.0/zh/editor/publish/publish-oppo-mini-game.md new file mode 100644 index 0000000000..e5f1be6b78 --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-oppo-mini-game.md @@ -0,0 +1,104 @@ +# 发布到 OPPO 小游戏 + +## 环境配置 + +- 下载 [OPPO 小游戏调试器](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use?id=_2-%e5%ae%89%e8%a3%85-runtimeapk-%e5%8c%85%e5%88%b0-oppo-%e6%89%8b%e6%9c%ba%e4%b8%8a),并安装到 OPPO 手机上(建议 Android Phone 6.0 或以上版本) + +- 全局安装 [nodejs-8.1.4](https://nodejs.org/zh-cn/download/) 或以上版本 + +## 发布流程 + +使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板,**发布平台** 选择 **OPPO 小游戏**。 + +![build](./publish-oppo-mini-games/oppo-build.png) + +通用构建选项的设置请参考 [通用构建选项](build-options.md),OPPO 小游戏特有的构建选项如下: + +![build option](./publish-oppo-mini-games/build-option.png) + +| 构建选项 | 可选 | 说明 | 字段名(用于命令行发布) | +| :------ | :--- | :--- | :--- | +| **初始场景分包** | 可选项 | 勾选后,首场景及其相关的依赖资源会被构建到发布包目录 `assets` 下的内置 Asset Bundle — [start-scene](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中,提高初始场景的资源加载速度。 | `startSceneAssetBundle` | +| **资源服务器地址** | 可选项 | 该项用于填写资源存放在服务器上的地址。
若 **不填写** 该项,则发布包目录下的 `remote` 文件夹会被打包到构建出来的 rpk 包中。
若 **填写** 该项,则不会打包到 rpk 包中,开发者需要在构建后手动将发布包目录下的 `remote` 文件夹上传到所填写的资源服务器地址上。详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 | `remoteServerAddress` | +| **游戏包名** | 必填项 | 游戏包名,根据开发者的需求进行填写,例如 `com.example.demo`。 | `package` | +| **桌面图标** | 必填项 | 点击输入框后面的放大镜图标按钮选择所需的图标。构建时,图标将会被构建到 OPPO 小游戏的工程中。桌面图标建议使用 **png** 图片。 | `icon` | +| **游戏版本名称** | 必填项 | 游戏版本名称是真实的版本,如:1.0.0 | `versionName` | +| **游戏版本号** | 必填项 | **游戏版本号** 与 **游戏版本名称** 不同,**游戏版本号** 主要用于区别版本更新。每次提交审核时游戏版本号都要比上次提交审核的值至少 +1,一定不能等于或者小于上次提交审核的值,建议每次提交审核时游戏版本号递归 +1。
**注意**:**游戏版本号** 必须为正整数。 | `versionCode` | +| **支持的最小平台版本号** | 必填项 | 推荐使用 **1060**。该项用于兼容性检查,避免游戏上线后在低版本平台运行不兼容。具体内容可点击 [使用说明](https://cdofs.oppomobile.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use) 查看。 | `minPlatformVersion` | +| **屏幕方向** | 必填项 | 设备方向,可选值包括 `landscape` 和 `portrait`。构建时会写入到发布包目录下的 `manifest.json` 中。| `deviceOrientation` | +| **分离引擎** | 可选项 | 该功能是通过共享全局引擎,来减少每个小游戏的首包大小。启用后,如果引擎在手机中已有缓存,首包下载时将会自动剔除引擎文件。如果手机中没有缓存,将会加载完整首包,完整首包内会包含完整的引擎文件 | `separateEngine` | +| **密钥库** | - | 若勾选该项,表示构建 rpk 包时默认使用的是 Creator 自带的证书,仅用于 **调试** 时使用。若 rpk 包要用于提交审核,则构建时不要勾选该项。
若不勾选该项,则需要手动配置签名证书。| `useDebugKey` | +| **certificate.pem 路径**
**private.pem 路径** | - | 如果不勾选 **密钥库**,则需要配置签名文件 **certificate.pem 路径** 和 **private.pem 路径**,此时构建后生成的是可以 **直接发布** 的 rpk 包。开发者可通过输入框右边的放大镜图标按钮来配置两个签名文件,或者也可以参考下方的 **生成签名文件**。
**注意**:这两个签名文件建议不要放在发布包 `build/oppo-mini-game` 目录下,否则每次构建时都会清空该目录,导致文件丢失。 | `privatePemPath`、`certificatePemPath` | + +- **生成签名文件** + + 有以下两种方式可以生成签名文件: + + - 通过 **构建发布** 面板 **certificate.pem 路径** 后的 **新建** 按钮生成 + + - 通过命令行生成 release 签名 + + 开发者需要通过 openssl 命令等工具生成签名文件 `private.pem`、`certificate.pem`。 + + ```bash + # 通过 openssl 命令工具生成签名文件 + openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out certificate.pem + ``` + + > **注意**:openssl 工具在 linux 或 Mac 环境下可在终端直接打开。而在 Windows 环境下则需要安装 openssl 工具并且配置系统环境变量,配置完成后需重启 Creator。 + +### 构建 + +**构建发布** 面板的构建选项设置完成后,点击 **构建并生成** 按钮。
+完成后点击 **构建任务** 左下角的文件夹图标按钮打开项目发布包,可以看到在默认发布路径 `build` 目录下生成了 `oppo-mini-game`(以具体的构建任务名为准)文件夹,该文件夹就是导出的 OPPO 小游戏工程目录和 rpk,rpk 包在 `build/oppo-mini-game/dist` 目录下。 + +![package](./publish-oppo-mini-games/package.png) + +若需要修改生成的 rpk 包,在修改完成后点击 **构建任务** 右下角的 **生成** 按钮,即可在不重新构建的情况下重新生成 rpk 包。 + +### 将构建出来的 rpk 运行到手机上 + +1. 将构建生成的小游戏 rpk 包(dist 目录中)拷贝到手机的 `/内部存储/games` 目录。 + +2. 在 OPPO 手机上打开之前已经安装完成的 **OPPO 小游戏调试器**,点击 **OPPO 小游戏** 栏目,然后找到填写游戏名相对应的图标即可,如果没有发现,可点击右上角的 **更多 -> 刷新** 按钮进行刷新。 + + ![rpk games](./publish-oppo-mini-games/rpk_games.jpg) + +> **注意**: +> +> 1. OPPO 小游戏调试器为 **v3.2.0** 及以上的需要将准备好的 rpk 拷贝到手机的 `/内部存储/Android/data/com.nearme.instant.platform/files/games` 中,如果没有 games 目录则需新建。具体内容可点击 [使用说明 — 新建目录](https://cdofs.oppomobile.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use?id=_3-%e6%96%b0%e5%bb%ba%e7%9b%ae%e5%bd%95) 查看。 +> +> 2. 若使用了分包加载,则分包 rpk 需要拷贝到手机的 `/内部存储/subPkg` 目录下。同样的,若使用的 OPPO 小游戏调试器为 **v3.2.0** 及以上的,则需要将分包 rpk 拷贝到手机的 `/内部存储/Android/data/com.nearme.instant.platform/files/subPkg` 中。详情请参考下文 **分包加载** 部分的内容。 + +## 分包加载 + +分包加载,即把游戏内容按一定规则拆分成几个包,在首次启动的时候只下载必要的包,这个必要的包称为 **主包**,开发者可以在主包内触发下载其他子包,这样可以有效降低首次启动的消耗时间。若要使用该功能需要在 Creator 中设置 [小游戏分包](subpackage.md),设置完成后构建时就会自动分包。 + +构建完成后,分包的目录在 `build/oppo-mini-game/dist` 目录下。
+这时需要在 OPPO 手机的内部存储目录下新建一个 `subPkg` 目录,然后把 `build/oppo-mini-game/dist` 目录下的 **.rpk** 文件拷贝到 `subPkg` 目录中。 + +然后切换到 **OPPO 小游戏调试器** 的 **分包加载** 栏目,点击右上方的刷新即可看到分包的游戏名称,点击 **秒开** 即可跟正常打包的 rpk 一样使用。 + +![run subpackage](./publish-oppo-mini-games/run_subpackage.jpg) + +分包 rpk 需要拷贝到 OPPO 手机的 `/内部存储/subPkg` 目录,未分包的 rpk 需要拷贝到 OPPO 手机的 `/内部存储/games` 目录,两者不可混用。 + +> **注意**:OPPO 小游戏调试器为 **v3.2.0** 及以上的,需要将分包 rpk 拷贝到手机的 `/内部存储/Android/data/com.nearme.instant.platform/files/subPkg` 目录,如果没有 subPkg 目录则需新建。而未分包的 rpk 则是拷贝到手机的 `/内部存储/Android/data/com.nearme.instant.platform/files/games` 目录,两者同样不可混用。 + +更多内容请参考 [OPPO 小游戏 — 分包加载](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/subpackage/subpackage)。 + +## OPPO 小游戏环境的资源管理 + +OPPO 小游戏与微信小游戏类似,都存在着包体限制。OPPO 小游戏的主包包体限制是 **4MB**,超过的部分必须通过网络请求下载。 + +当包体过大时,可在 **构建发布** 面板配置 **资源服务器地址** 选项,将低加载优先级的资源上传到远程服务器,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 + +游戏启动之后引擎会自动下载远程服务器地址中的资源,资源下载后引擎的缓存管理器会记录资源的保存路径,用于在缓存空间不足时自动删除部分缓存的游戏资源。请参考 [缓存管理器](../../asset/cache-manager.md)。 + +## 相关参考链接 + +- [OPPO 开放平台](https://open.oppomobile.com/wiki/doc#id=10445) +- [OPPO 小游戏教程](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/quickgame) +- [OPPO 小游戏 API 文档](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/feature/account) +- [OPPO 小游戏工具下载](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use) +- [OPPO 小游戏使用说明 — 新建目录](https://activity-cdo.heytapimage.com/cdo-activity/static/201810/26/quickgame/documentation/#/games/use?id=_3-%e6%96%b0%e5%bb%ba%e7%9b%ae%e5%bd%95) diff --git a/versions/4.0/zh/editor/publish/publish-oppo-mini-games/build-option.png b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/build-option.png new file mode 100644 index 0000000000..5a8de90dab Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/build-option.png differ diff --git a/versions/4.0/zh/editor/publish/publish-oppo-mini-games/oppo-build.png b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/oppo-build.png new file mode 100644 index 0000000000..71da63ee91 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/oppo-build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-oppo-mini-games/package.png b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/package.png new file mode 100644 index 0000000000..48cac32ce8 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/package.png differ diff --git a/versions/4.0/zh/editor/publish/publish-oppo-mini-games/rpk_games.jpg b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/rpk_games.jpg new file mode 100644 index 0000000000..b7e9ddb43c Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/rpk_games.jpg differ diff --git a/versions/4.0/zh/editor/publish/publish-oppo-mini-games/run_subpackage.jpg b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/run_subpackage.jpg new file mode 100644 index 0000000000..57888b1756 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-oppo-mini-games/run_subpackage.jpg differ diff --git a/versions/4.0/zh/editor/publish/publish-pc-bytedance.md b/versions/4.0/zh/editor/publish/publish-pc-bytedance.md new file mode 100644 index 0000000000..b371e2938c --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-pc-bytedance.md @@ -0,0 +1,28 @@ +# 接入抖音 PC 小游戏 + +抖音 PC 小游戏即支持在​ 直播伴侣 打开抖音小游戏。PC 小游戏将具备移动端的大部分能力,暂时不支持以下功能:keyDown/keyUp 键盘事件、录屏/录音​、​广告​、转发分享、收藏、群聊、侧边栏、支付、以及强依赖移动端设备的能力,如罗盘、加速度计、相机、麦克风等​。 + +Cocos Creator 支持将游戏发布到抖音 PC 小游戏,下面我们来看看,如何通过 Cocos Creator 将游戏发布到抖音 PC 小游戏平台。 + +## 使用 Cocos Creator 接入抖音 PC 小游戏 + +### 准备工作 + +下载并安装最新版本的 [抖音直播伴侣 PC 版](https://streamingtool.douyin.com/)。 + +### 发布流程 + +1. 参考 [发布到抖音小游戏](./publish-bytedance-mini-game.md) 的流程,将项目工程发布到抖音小游戏。 +2. PC 端小游戏需要代码运行在严格模式才能保障正常运行,因此请将代码使用严格模式进行编译,并且修改因严格模式造成的问题,否则可能造成运行时报错。​ +3. 在直播伴侣调试前先保证在 IDE 上能够正常运行 + +## 常见问题 + +Q:抖音 PC 小游戏支持 Mac 系统吗?
+A:暂时不可以。 + +## 相关链接 + +- [抖音 PC 小游戏接入指南](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/guide/open-ability/pc-game/tutorial) +- [抖音 PC 版下载](https://streamingtool.douyin.com/) +- [抖音 开发者工具--IDE 下载](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/developer-instrument/developer-instrument-update-and-download) diff --git a/versions/4.0/zh/editor/publish/publish-pc-wechatgame.md b/versions/4.0/zh/editor/publish/publish-pc-wechatgame.md new file mode 100644 index 0000000000..db35faba5a --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-pc-wechatgame.md @@ -0,0 +1,33 @@ +# 接入微信 PC 小游戏 + +微信 PC 小游戏即支持在微信 PC 版打开微信小游戏。PC 小游戏将具备移动端的大部分能力,包括但不限于虚拟支付、开放数据域、触摸事件等(广告目前暂不支持)。同时 PC 小游戏还支持键盘、鼠标事件及自定义窗口等功能。 + +Cocos Creator 支持将游戏发布到微信 PC 小游戏,并完成了鼠标、键盘相关接口的适配工作。下面我们来看看,如何通过 Cocos Creator 将游戏发布到微信 PC 小游戏平台。 + +## 使用 Cocos Creator 接入微信 PC 小游戏 + +### 准备工作 + +下载并安装最新版本的 [微信 PC 版](https://pc.weixin.qq.com/),使用微信开发者工具绑定的微信号登录微信 PC 版。 + +### 发布流程 + +1. 参考 [发布到微信小游戏](./publish-wechatgame.md) 的流程,将项目工程发布到微信小游戏。 + +2. 在 **微信开发者工具** 中,点击上方工具栏中的 **预览** 按钮,选择 **自动预览** 选项卡,勾选 **启动 PC 端自动预览**,然后点击 **编译并预览**,即可在微信 PC 版预览并调试小游戏。 + + ![WeChat PC preview](./publish-wechatgame/wechat-pc.png) + +## 常见问题 + +Q:如何通过引擎接口区分微信的 **移动端** 与 **PC 端**?
+A:可以通过 `sys.isMobile` 判断,PC 端返回 `false`,移动端返回 `true`。
+> **注意**:微信开发者工具中的模拟器,模拟的是移动端环境,所以这里返回的是 `true`。 + +Q:微信 PC 小游戏支持 Mac 系统吗?
+A:是的,微信 PC 小游戏目前已支持 Mac 系统。发布流程与上述一致。 + +## 相关链接 + +- [微信 PC 小游戏接入指南](https://developers.weixin.qq.com/minigame/dev/guide/open-ability/pc-game.html) +- [微信 PC 版下载](https://pc.weixin.qq.com/) diff --git a/versions/4.0/zh/editor/publish/publish-taobao-creative-app.md b/versions/4.0/zh/editor/publish/publish-taobao-creative-app.md new file mode 100644 index 0000000000..005e9d88f8 --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-taobao-creative-app.md @@ -0,0 +1,74 @@ +# 发布到淘宝小程序创意互动 + +Cocos Creator 从 v2.4.11 开始支持将游戏发布到 **淘宝小程序创意互动**。 + +## 准备工作 + +- 参考 [淘宝小程序创意互动接入指南](https://miniapp.open.taobao.com/doc.htm?docId=119114&docType=1&tag=dev),在淘宝开放平台完成开发者入驻流程和应用创建。 + +- 桌面端下载 [淘宝开发者工具](https://developer.taobao.com/?spm=a219a.15212435.0.0.11ef669aIQNlnI) 并安装。 + +- 下载 [淘宝](https://market.m.taobao.com/app/fdilab/download-page/main/index.html),并安装到手机设备上。 + +## 发布流程 + +使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板,**发布平台** 选择 **淘宝小程序创意互动**,然后点击 **构建**。 + +![build_option](./publish-taobao-creative-app/build_option.png) + +### 参数项配置 + +相关参数配置具体的填写规则如下: + +- **主包压缩类型** + + 设置主包的压缩类型,具体内容可参考文档 [Asset Bundle — 压缩类型](../asset-manager/bundle.md#%E5%8E%8B%E7%BC%A9%E7%B1%BB%E5%9E%8B)。 + +- **配置主包为远程包** + + 该项为可选项,需要与 **资源服务器地址** 选项配合使用。
+ 勾选后,主包会配置为远程包,并且与其相关依赖资源一起被构建到发布包目录 remote 下的内置 Asset Bundle — [main](../asset-manager/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中。开发者需要将整个 remote 文件夹上传到远程服务器。 + +- **初始场景分包** + + 该项为可选项。
+ 勾选后,首场景及其相关的依赖资源会被构建到发布包目录 assets 下的内置 Asset Bundle — [start-scene](../asset-manager/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中,提高初始场景的资源加载速度。具体内容可参考文档 [初始场景的资源加载](publish-wechatgame.md#%E5%88%9D%E5%A7%8B%E5%9C%BA%E6%99%AF%E7%9A%84%E5%8A%A0%E8%BD%BD%E9%80%9F%E5%BA%A6)。 + +- **资源服务器地址** + + 该项为选填项,用于填写资源存放在远程服务器上的地址。开发者需要在构建后手动将发布包目录下的 remote 文件夹上传到所填写的资源服务器地址上。 + +### 运行预览 + +- 构建完成后点击 **发布路径** 后面的 **打开** 按钮,可以看到在发布包 build 目录下生成了淘宝小程序创意互动工程文件夹 **taobao**,其中已经包含了淘宝小程序创意互动环境的配置文件:`app.json` 和 `mini.project.json`。 + + ![build](./publish-taobao-creative-app/build.png) + +- 使用 **淘宝开发者工具** 打开构建生成的 **taobao** 文件夹,即可打开淘宝小程序创意互动项目及预览调试游戏内容。开发者工具的具体使用方式请参考 [淘宝开发者工具介绍](https://miniapp.open.taobao.com/doc.htm?docId=119188&docType=1&tag=dev)。 + + ![preview](./publish-taobao-creative-app/preview.png) + +## 淘宝小程序创意互动环境的资源管理 + +淘宝小程序创意互动与微信小游戏类似,都存在着包体限制,超过 2MB 的额外资源,必须通过网络请求下载。在包体优化方面,建议剔除掉没使用到的引擎模块。 + +Cocos Creator 已经帮开发者做好了远程资源的下载、缓存和版本管理。具体的实现逻辑和操作步骤都与微信小游戏类似,请参考 [微信小游戏资源管理](./publish-wechatgame.md#%E5%BE%AE%E4%BF%A1%E5%B0%8F%E6%B8%B8%E6%88%8F%E7%9A%84%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86)。 + +## 淘宝小程序创意互动的限制 + +淘宝小程序创意互动暂时不支持以下功能模块: + +- VideoPlayer +- WebView +- 自定义字体 +- 暂不支持资源分包(替代方案是资源包放 CDN) +- 暂只支持竖屏的应用 + +## 参考链接 + +- [淘宝开放平台开发指南](https://miniapp.open.taobao.com/docV3.htm?docId=119114&docType=1&tag=dev) +- [淘宝开发者入驻文档](https://miniapp.open.taobao.com/doc.htm?docId=121648&docType=1&tag=game-info) +- [淘宝小游戏介绍](https://miniapp.open.taobao.com/doc.htm?docId=121646&docType=1&tag=game-info) +- [淘宝小游戏API文档](https://miniapp.open.taobao.com/doc.htm?docId=121112&docType=1&tag=game-dev) +- [淘宝开发者工具下载](https://developer.taobao.com/?spm=a219a.15212435.0.0.7892669alqxNjY) +- [淘宝开发者工具真机调试](https://miniapp.open.taobao.com/doc.htm?docId=119194&docType=1&tag=game-dev) \ No newline at end of file diff --git a/versions/4.0/zh/editor/publish/publish-taobao-creative-app/build.png b/versions/4.0/zh/editor/publish/publish-taobao-creative-app/build.png new file mode 100644 index 0000000000..1d783a3d5b Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-taobao-creative-app/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-taobao-creative-app/build_option.png b/versions/4.0/zh/editor/publish/publish-taobao-creative-app/build_option.png new file mode 100644 index 0000000000..4efb5c65fd Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-taobao-creative-app/build_option.png differ diff --git a/versions/4.0/zh/editor/publish/publish-taobao-creative-app/preview.png b/versions/4.0/zh/editor/publish/publish-taobao-creative-app/preview.png new file mode 100644 index 0000000000..0c4b1a6225 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-taobao-creative-app/preview.png differ diff --git a/versions/4.0/zh/editor/publish/publish-taobao-mini-game.md b/versions/4.0/zh/editor/publish/publish-taobao-mini-game.md new file mode 100644 index 0000000000..c5a9696d40 --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-taobao-mini-game.md @@ -0,0 +1,84 @@ +# 发布到淘宝小游戏 + +## 环境配置 + +- 桌面端下载 [淘宝开发者工具](https://developer.taobao.com/?spm=a219a.15212435.0.0.6a14669aEQ2g6k) 并安装。 + +- 下载 [淘宝](https://market.m.taobao.com/app/fdilab/download-page/main/index.html),并安装到手机设备上。 + +- 淘宝客户端在 Android 上支持的最低版本为 10.22.30,在 iOS 为 10.22.30。 + +## 发布流程 + +使用 Cocos Creator 打开需要发布的项目工程,在 **构建发布** 面板的 **发布平台** 中选择 **淘宝小游戏**,然后点击 **构建**。 + +![build option](./publish-taobao-mini-game/build_option.png) + +### 参数项配置 + +相关参数配置具体的填写规则如下: + +- **主包压缩类型** + + 设置主包的压缩类型,具体内容可参考文档 [Asset Bundle — 压缩类型](../../asset/bundle.md#%E5%8E%8B%E7%BC%A9%E7%B1%BB%E5%9E%8B)。 + +- **配置主包为远程包** + + 该项为可选项,需要与 **资源服务器地址** 选项配合使用。
+ 勾选后,主包会配置为远程包,并且与其相关依赖资源一起被构建到发布包目录 remote 下的内置 Asset Bundle — [main](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中。开发者需要将整个 remote 文件夹上传到远程服务器。 + +- **初始场景分包** + + 该项为可选项。
+ 勾选后,首场景及其相关的依赖资源会被构建到发布包目录 assets 下的内置 Asset Bundle — [start-scene](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中,提高初始场景的资源加载速度。 + 构建完成后,初始场景及其相关的依赖资源会被构建到发布包目录下的 assets/start-scene bundle 中。这个 bundle 不会放到远程服务器上,而是放在本地,引擎在启动阶段时就会自动从本地包内加载这个 bundle,从而加快初始场景的加载速度。 + +- **资源服务器地址** + + 该项为选填项,用于填写资源存放在远程服务器上的地址。开发者需要在构建后手动将发布包目录下的 remote 文件夹上传到所填写的资源服务器地址上。 + +- **分离引擎** + 该项为可选项。
+ 勾选后,可以减小游戏的首包大小。使用方法可以参考文档 [使用说明](./taobaominigame-plugin.md),其中构建平台选择淘宝小游戏。 + +### 运行预览 + +- 构建完成后点击 **构建任务** 左下角的文件夹图标按钮,可以看到在项目的 `build` 目录下生成了淘宝小游戏工程文件夹 `taobao-mini-game`,其中已经包含了淘宝小游戏环境的配置文件 `game.json`。 + +![build](./publish-taobao-mini-game/build.png) + +- 使用 **淘宝开发者工具** 打开构建生成的 `taobao-mini-game` 文件夹,即可打开淘宝小游戏项目以及预览调试游戏内容。 + +![preview](./publish-taobao-mini-game/preview.png) + +## 分包加载 + +淘宝小游戏的分包加载,详情请参考 [小游戏分包](subpackage.md)。 + +## 淘宝小游戏环境的资源管理 + +淘宝小游戏与微信小游戏类似,都存在着包体限制,超过 **4MB** 的额外资源,必须通过网络请求下载。 + +当包体过大时,可在 **构建发布** 面板配置 **资源服务器地址** 选项,将低加载优先级的资源上传到远程服务器,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 + +游戏启动之后引擎会自动下载远程服务器地址中的资源,资源下载后引擎的缓存管理器会记录资源的保存路径,用于在缓存空间不足时自动删除部分缓存的游戏资源。请参考 [缓存管理器](../../asset/cache-manager.md)。 + +## 淘宝小游戏的限制 + +不支持以下模块: + +- WebView +- VideoPlayer +- 自定义字体 + +## 淘宝与其他小游戏平台的差异 + +- 全局变量的访问,需要挂载到 global 变量上。例:$global.my = my; +- 'global-variables.js' 说明: window 变量是 global 的引用,需要先确保 global上变量已经存在,定义的临时变量才会有值。若使用自定义脚本或使用第三方插件,发现全局变量不存在,通常是加载时机问题导致脚本还没被加载,就使用到了脚本内的全局变量。 +- 淘宝小游戏平台目前使用自研 js 虚拟机(后续可能会更换为 v8),当前平台版本的 js 性能要稍逊于主流的 v8 引擎,所以在此平台发布游戏时需要重点关注游戏 DrawCall 数量对游戏性能产生的影响。Cocos 引擎提供了动态图集功能来降低游戏界面产生的 draw call,在构建淘宝小游戏平台时开启 CLEANUP_IMAGE_CACHE 选项即可。 +- 淘宝 IDE, 模拟器和真机的 JS 环境是不一致的;若是真机无问题,直接把问题反馈给淘宝平台 +- 由于真机调试模式会有额外的性能开销,因此在验证帧率的时候应该使用真机预览模式,而非真机调试模式 + +## 相关文档 + +- [淘宝文档中心](https://miniapp.open.taobao.com/doc.htm?docId=121719&docType=1&tag=game-dev) diff --git a/versions/4.0/zh/editor/publish/publish-taobao-mini-game/build-options.png b/versions/4.0/zh/editor/publish/publish-taobao-mini-game/build-options.png new file mode 100644 index 0000000000..bc4777b6e6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-taobao-mini-game/build-options.png differ diff --git a/versions/4.0/zh/editor/publish/publish-taobao-mini-game/build.png b/versions/4.0/zh/editor/publish/publish-taobao-mini-game/build.png new file mode 100644 index 0000000000..abc43ec3b3 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-taobao-mini-game/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-taobao-mini-game/build_option.png b/versions/4.0/zh/editor/publish/publish-taobao-mini-game/build_option.png new file mode 100644 index 0000000000..bbf0a22cb6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-taobao-mini-game/build_option.png differ diff --git a/versions/4.0/zh/editor/publish/publish-taobao-mini-game/preview.png b/versions/4.0/zh/editor/publish/publish-taobao-mini-game/preview.png new file mode 100644 index 0000000000..bc3f7aae11 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-taobao-mini-game/preview.png differ diff --git a/versions/4.0/zh/editor/publish/publish-vivo-mini-game.md b/versions/4.0/zh/editor/publish/publish-vivo-mini-game.md new file mode 100644 index 0000000000..564dddddda --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-vivo-mini-game.md @@ -0,0 +1,129 @@ +# 发布到 vivo 小游戏 + +## 环境配置 + +- 下载 [快应用 & vivo 小游戏调试器](https://minigame.vivo.com.cn/documents/#/lesson/base/environment?id=%E5%AE%89%E8%A3%85vivo%E5%B0%8F%E6%B8%B8%E6%88%8F%E8%B0%83%E8%AF%95%E5%99%A8) 和 [vivo 小游戏引擎](https://minigame.vivo.com.cn/documents/#/lesson/base/environment?id=%E5%AE%89%E8%A3%85vivo%E5%B0%8F%E6%B8%B8%E6%88%8F%E5%BC%95%E6%93%8E),并安装到 Android 设备上(建议 Android Phone 6.0 或以上版本) + +- 全局安装 [nodejs-8.9.0](https://nodejs.org/zh-cn/download/) 或以上版本 + + > **注意**:安装 nodejs 后,需要注意 npm 源地址是否为 + + ```bash + # 查看当前 npm 源地址 + npm config get registry + # 若不是,重新设置 npm 源地址 + npm config set registry https://registry.npmjs.org/ + ``` + +- 全局安装 `vivo-minigame/cli`。确定 npm 源地址后,安装 `vivo-minigame/cli`: + + ```bash + npm install -g @vivo-minigame/cli + ``` + + 若 `vivo-minigame/cli` 安装失败,可能是因为 nodejs 版本过低导致的,请检查 node 版本并升级。 + +## 发布流程 + +使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板,**发布平台** 选择 **vivo 小游戏**。 + +![build](./publish-vivo-mini-game/build.png) + +通用构建选项的设置请参考 [通用构建选项](build-options.md),vivo 小游戏特有的构建选项如下: + +![build option](./publish-vivo-mini-game/build-option.png) + +| 构建选项 | 可选 | 说明 | 字段名(用于命令行发布) | +| :------ | :--- | :--- | :--- | +| 初始场景分包 | 可选项 | 勾选后,首场景及其相关的依赖资源会被构建到发布包目录 `assets` 下的内置 Asset Bundle — [start-scene](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中,提高初始场景的资源加载速度。 | `startSceneAssetBundle` | +| 资源服务器地址 | 可选项 | 该项用于填写资源存放在服务器上的地址。
若 **不填写** 该项,则发布包目录下的 `remote` 文件夹会被打包到构建出来的 rpk 包中。
若 **填写** 该项,则不会打包到 rpk 包中,开发者需要在构建后手动将发布包目录下的 `remote` 文件夹上传到所填写的资源服务器地址上。详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 | `remoteServerAddress` | +| 游戏包名 | 必填项 | 游戏包名,根据开发者的需求进行填写,例如 `com.example.demo`。| `package` | +| 桌面图标 | 必填项 | 点击输入框后面的放大镜图标按钮选择所需的图标。构建时,图标将会被构建到 vivo 小游戏的工程中。桌面图标建议使用 **png** 图片。 | `icon` | +| 游戏版本名称 | 必填项 | 游戏版本名称是真实的版本,如:1.0.0 | `versionName` | +| 游戏版本号 | 必填项 | **游戏版本号** 与 **游戏版本名称** 不同,**游戏版本号** 主要用于区别版本更新。每次提交审核时游戏版本号都要比上次提交审核的值至少 +1,一定不能等于或者小于上次提交审核的值,建议每次提交审核时游戏版本号递归 +1。
**注意**:**游戏版本号** 必须为正整数。 | `versionCode` | +| 支持的最小平台版本号 | 必填项 | 用于兼容性检查,避免上线后在低版本平台运行导致不兼容。具体填写的值可通过点击 [更新记录](https://minigame.vivo.com.cn/documents/#/download/engine?id=%E6%9B%B4%E6%96%B0%E8%AE%B0%E5%BD%95%EF%BC%9A) 来查看最新的 vivo 引擎版本号。 | `minPlatformVersion` | +| 屏幕方向 | 必填项 | 设备方向,可选值包括 `landscape` 和 `portrait`。构建时会写入到发布包目录下的 `manifest.json` 中。| `deviceOrientation` | +| 允许分离引擎 | 选填项 | vivo 从平台版本号 **1063** 开始新增了 **游戏引擎插件** 功能。此插件内置了 Cocos Creator 引擎的官方版本,若玩家首次体验的游戏中启用了此插件,则所有同样启用此插件的游戏,都无需再次下载 Cocos Creator 引擎,只需直接使用公共插件库中的相同版本引擎,或者增量更新引擎即可。
使用时勾选 **允许分离引擎**,然后正常构建发布即可,无需其它人工操作。详情请参考 [启用微信小游戏引擎插件](./wechatgame-plugin.md) | `separateEngine` | +| 使用调试密钥库 | - | 若勾选该项,表示构建 rpk 包时默认使用的是 Creator 自带的证书,仅用于 **调试** 时使用。若 rpk 包要用于提交审核,则构建时不要勾选该项。
若不勾选该项,则需要手动配置签名证书。| `useDebugKey` | +| **certificate.pem 路径**
**private.pem 路径** | - | 如果不勾选 **密钥库**,则需要配置签名文件 **certificate.pem 路径** 和 **private.pem 路径**,此时构建后生成的是可以 **直接发布** 的 rpk 包。开发者可通过输入框右边的放大镜图标按钮来配置两个签名文件,或者也可以参考下方的 **生成签名文件**。 | `privatePemPath`、`certificatePemPath` | + +- 生成签名文件 + + 有以下两种方式可以生成签名文件: + + - 通过 **构建发布** 面板 **certificate.pem 路径** 后的 **新建** 按钮生成 + + - 通过命令行生成 release 签名 + + 用户需要通过 openssl 命令等工具生成签名文件 private.pem、certificate.pem。 + + ```bash + # 通过 openssl 命令工具生成签名文件 + openssl req -newkey rsa:2048 -nodes -keyout private.pem -x509 -days 3650 -out certificate.pem + ``` + + > **注意**:openssl 工具在 linux 或 Mac 环境下可在终端直接打开。而在 Windows 环境下则需要安装 openssl 工具并且配置系统环境变量,配置完成后需重启 Creator。 + +### 构建 + +**构建发布** 面板的构建选项设置完成后,点击 **构建并生成** 按钮。
+完成后点击 **构建任务** 左下角的文件夹图标按钮打开项目发布包,可以看到在默认发布路径 `build` 目录下生成了 `vivo-mini-game`(以具体的构建任务名为准)文件夹,该文件夹就是导出的 vivo 快游戏工程目录和 rpk,rpk 包在 `build/vivo-mini-game/dist` 目录下。 + +![package](./publish-vivo-mini-game/package.png) + +若需要修改生成的 rpk 包,在修改完成后点击 **构建任务** 右下角的 **生成** 按钮,即可在不重新构建的情况下重新生成 rpk 包。 + +## 运行 rpk + +有以下三种方式可将 rpk 运行到手机上: + +- **方法一**: + + 在 **构建发布** 面板点击 **运行** 按钮,等待二维码界面生成 + + ![play](./publish-vivo-mini-game/play.jpg) + + 然后在 Android 设备上打开之前已经安装完成的 **快应用 & vivo 小游戏调试器**,点击 **扫码安装** 按钮直接扫描二维码即可打开 rpk。 + + ![vivo instant scan install](./publish-vivo-mini-game/vivo-instant_scan_install.jpg) + +- **方法二**: + + 将构建生成的小游戏 rpk 文件(位于打包出的小游戏工程目录下的 dist 目录中)拷贝到手机的内部存储目录下。 + + 在 Android 设备上打开之前已经安装完成的 **快应用 & vivo 小游戏调试器**,点击 **本地安装**,然后从手机内部存储目录中找到 rpk 文件,选择打开即可。 + + ![vivo instant native install](./publish-vivo-mini-game/vivo-instant_native_install.jpg) + +- **方法三**: + + 利用 vivo 小游戏打包工具命令生成网址和二维码: + + ```bash + # 先把命令行指定到编辑器安装目录下的 resources/tools/vivo-pack-tools 目录下 + cd ${CocosCreator}/resources/tools/vivo-pack-tools + # 生成网址和二维码 + npm run server + ``` + + 然后在 Android 设备上打开之前已经安装完成的 **快应用 & vivo 小游戏调试器**。 + + 最后点击 **扫码安装** 按钮,将第一步中生成的网址拷贝到浏览器,然后直接扫描网页上的二维码即可打开 rpk。 + +## 分包加载 + +vivo 小游戏的分包加载,详情请参考 [小游戏分包](subpackage.md)。 + +## vivo 小游戏环境的资源管理 + +vivo 小游戏与微信小游戏类似,都存在着包体限制。vivo 小游戏的主包包体限制是 **4MB**,超过的部分必须通过网络请求下载。 + +当包体过大时,可在 **构建发布** 面板配置 **资源服务器地址** 选项,将低加载优先级的资源上传到远程服务器,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 + +游戏启动之后引擎会自动下载远程服务器地址中的资源,资源下载后引擎的缓存管理器会记录资源的保存路径,用于在缓存空间不足时自动删除部分缓存的游戏资源。请参考 [缓存管理器](../../asset/cache-manager.md)。 + +## 参考链接 + +- [vivo 小游戏开发文档](https://minigame.vivo.com.cn/documents/#/lesson/base/start) +- [vivo 小游戏 API 文档](https://minigame.vivo.com.cn/documents/#/api/system/life-cycle) +- [快应用 & vivo 小游戏调试器下载](https://minigame.vivo.com.cn/documents/#/download/debugger) diff --git a/versions/4.0/zh/editor/publish/publish-vivo-mini-game/build-option.png b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/build-option.png new file mode 100644 index 0000000000..2bc655c0f8 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/build-option.png differ diff --git a/versions/4.0/zh/editor/publish/publish-vivo-mini-game/build.png b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/build.png new file mode 100644 index 0000000000..5078eebf95 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-vivo-mini-game/package.png b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/package.png new file mode 100644 index 0000000000..37941b61aa Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/package.png differ diff --git a/versions/4.0/zh/editor/publish/publish-vivo-mini-game/play.jpg b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/play.jpg new file mode 100644 index 0000000000..a7d5998549 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/play.jpg differ diff --git a/versions/4.0/zh/editor/publish/publish-vivo-mini-game/vivo-instant_native_install.jpg b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/vivo-instant_native_install.jpg new file mode 100644 index 0000000000..f0a0175bdb Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/vivo-instant_native_install.jpg differ diff --git a/versions/4.0/zh/editor/publish/publish-vivo-mini-game/vivo-instant_scan_install.jpg b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/vivo-instant_scan_install.jpg new file mode 100644 index 0000000000..729e92eb9c Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-vivo-mini-game/vivo-instant_scan_install.jpg differ diff --git a/versions/4.0/zh/editor/publish/publish-web.md b/versions/4.0/zh/editor/publish/publish-web.md new file mode 100644 index 0000000000..d33208f67a --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-web.md @@ -0,0 +1,82 @@ +# 发布到 Web 平台 + +打开主菜单的 **项目 -> 构建发布**,打开 [构建发布](build-panel.md) 面板。 + +![web](publish-web/web.png) + +Cocos Creator 提供了两种 Web 平台的页面模板,可以通过 **发布平台** 的下拉菜单选择 **Web Mobile** 或 **Web Desktop**,它们的区别主要在于: +- **Web Mobile** 会默认将游戏视图撑满整个浏览器窗口。 +- **Web Desktop** 允许在发布时指定一个游戏视图的分辨率,而且之后游戏视图也不会随着浏览器窗口大小变化而变化。 + +## 构建选项介绍 + +各平台通用的构建选项,详情请参考 [通用构建参数介绍](build-options.md)。接下来我们来看一下 Web 平台特有的构建选项。 + +### Web Desktop + +| 构建选项 | 说明 | 字段名(用于命令行发布) | +| :--- | :--- | :--- | +| 资源服务器地址 | 用于下载远程资源的服务器地址,详情请参考下文 **资源服务器地址** 部分的内容。 | `remoteServerAddress` | +| WEBGPU | 是否启用WEBGPU作为渲染后端 | `WEBGPU` | +| 预览分辨率 | 游戏视图分辨率,默认为 **(1280, 960)** | `resolution` | +| Polyfills | 构建支持一些脚本新特性的 polyfills,在打包脚本时会做对应处理,开发者可以根据实际需求选择需要的 polyfills。暂时只支持 **异步函数**,后续将会开放更多功能。| `polyfills` | + +### Web Mobile + +| 构建选项 | 说明 | 字段名(用于命令行发布) | +| :--- | :--- | :--- | +| 资源服务器地址 | 用于下载远程资源的服务器地址,详情请参考下文 **资源服务器地址** 部分的内容。 | `remoteServerAddress` | +| 设备方向 | 可选值包括 **Auto**、**Landscape**、**Portrait** | `orientation` | +| Polyfills | 构建支持一些脚本新特性的 polyfills,在打包脚本时会做对应处理,目前包括 **async Functions** 和 **coreJs** 两种,开发者可以根据实际需求选择需要的 polyfills。 | `polyfills` | +| vConsole | 插入 vConsole 调试工具,vConsole 类似 DevTools 的迷你版,用于辅助调试。 | `embedWebDebugger` | +| 预览二维码 | 用于扫描预览,详情可见下方介绍 | - | +| 预览 URL | 用于预览的链接,详情可见下方介绍 | - | + +- **资源服务器地址** + + 该项为可选项,用于填写资源存放在服务器上的地址。 + + - 若 **不填写** 该项,则发布包目录下的 `remote` 文件夹会被打包到构建出来的游戏包中。 + - 若 **填写** 该项,则不会打包到游戏包中,开发者需要在构建后手动将发布包目录下的 `remote` 文件夹上传到所填写的资源服务器地址上。详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 + +- **预览 URL** + + 构建支持同时预览多个 Web 项目,因而构建的预览 URL 不再是统一的而是每个构建任务都会有一个单独的预览 URL,互不干扰。点击 URL 即可自动打开浏览器进行预览,具体的预览 URL 拼接规则为 **${偏好设置中的预览 IP 地址}:${编辑器预览端口号}/${构建平台}/${构建任务名}/index.html**。 + + ![preview-url](publish-web/preview-url.png) + +## 构建和预览 + +配置好构建选项后,点击 **构建** 按钮,开始 Web 平台版本构建。面板上会出现一个进度条,当进度条显示“Build success”时,构建就完成了。 + +接下来可以点击 **运行** 按钮,在浏览器中打开构建后的游戏版本进行预览和调试。 + +![web mobile](publish-web/web-mobile.png) + +上图所示就是 Web Mobile 平台的预览,可以看到游戏视图占满了整个浏览器窗口,而 Web Desktop 的游戏视图则是固定分辨率的,不会撑满屏幕。 + +### 开启 WebGPU + +Cocos Creator 自 v3.6.2 起开始支持 WebGPU, 构建 Web-Desktop 的时候勾选 `WEBGPU` 即可。 +构建 `WebGPU` 之后,先定位到构建好的资源文件所在文件夹,可以点击构建面板上的文件夹按钮直达,需要一个 http-server 或者 nginx 服务器来访问。 +在此之前请先检查浏览器的兼容性,下章 **浏览器兼容性** 会有详细介绍。 + +### 浏览器兼容性 + +Cocos Creator 开发过程中测试的桌面浏览器包括:**Chrome**、**Firefox(火狐)** 和 **QQ 浏览器**,其他浏览器只要内核版本够高也可以正常使用,对部分浏览器来说请勿开启 IE 兼容模式。 + +移动设备上测试的浏览器包括:**Safari(iOS)**、**Chrome(Android)**、**QQ 浏览器(Android)** 和 **UC 浏览器(Android)**。 + +对于开启了 `WebGPU` 的 WebDesktop,现在只支持指定版本的 chromium 浏览器。[这里](https://vikyd.github.io/download-chromium-history-version/#/) 有历史版本的 chromium。chromium 105 的最后一个版本是支持比较好的。下载浏览器之前不要忘记选择正确的平台。打开 chromium,地址栏输入 `chrome://flags`,搜索 webgpu,开启 `WebGPU Developer Features` 并重启chromium,这样准备工作就完成了。 + +## Retina 设置 + +可以在脚本中通过 `view.enableRetina(true)` 设置是否使用高分辨率,构建到 Web 平台时默认会开启 Retina 显示。详情可参考 API [enableRetina](%__APIDOC__%/zh/class/View?id=enableRetina)。 + +## 发布到 Web 服务器 + +如果要在互联网上发布或分享游戏,只要点击 **构建任务** 左下方的 **文件夹图标** 按钮,打开发布路径之后,按照当前构建任务名称,将构建出的对应文件夹里的内容整个复制到 Web 服务器上,就可以通过相应的地址访问了。 + +![web mobile](publish-web/web-folder.png) + +关于 Web 服务器的架设,可以自行搜索 Apache、Nginx、IIS、Express 等相关解决方案。 diff --git a/versions/4.0/zh/editor/publish/publish-web/preview-url.png b/versions/4.0/zh/editor/publish/publish-web/preview-url.png new file mode 100644 index 0000000000..6dbaf408c2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-web/preview-url.png differ diff --git a/versions/4.0/zh/editor/publish/publish-web/web-folder.png b/versions/4.0/zh/editor/publish/publish-web/web-folder.png new file mode 100644 index 0000000000..3486a1b160 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-web/web-folder.png differ diff --git a/versions/4.0/zh/editor/publish/publish-web/web-mobile.png b/versions/4.0/zh/editor/publish/publish-web/web-mobile.png new file mode 100644 index 0000000000..a67bd441b2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-web/web-mobile.png differ diff --git a/versions/4.0/zh/editor/publish/publish-web/web.png b/versions/4.0/zh/editor/publish/publish-web/web.png new file mode 100644 index 0000000000..89a2cd9f4c Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-web/web.png differ diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame.md b/versions/4.0/zh/editor/publish/publish-wechatgame.md new file mode 100644 index 0000000000..de66e95a9e --- /dev/null +++ b/versions/4.0/zh/editor/publish/publish-wechatgame.md @@ -0,0 +1,133 @@ +# 发布到微信小游戏 + +微信小游戏的运行环境是微信小程序环境的扩展,在小程序环境的基础上提供了 WebGL 接口的封装,使得渲染能力和性能有了大幅度提升。不过由于这些接口都是微信团队通过自研的原生实现封装的,所以并不可以等同为浏览器环境。 + +作为引擎方,为了尽可能简化开发者的工作量,我们为用户完成的主要工作包括: + +- 引擎框架适配微信小游戏 API,纯游戏逻辑层面,用户不需要任何额外的修改 +- Cocos Creator 编辑器提供了快捷的打包流程,直接发布为微信小游戏,并自动唤起小游戏的开发者工具 +- 自动加载远程资源,缓存资源以及缓存资源版本控制 + +除此之外,小游戏的游戏提交、审核以及发布流程,和小程序是没有区别的,都需要遵守微信团队的要求和标准流程,具体信息可以参考 [微信小游戏开发文档](https://developers.weixin.qq.com/minigame/dev/guide/)。 + +## 环境配置 + +1. 在 [微信官方文档](https://mp.weixin.qq.com/debug/wxagame/dev/devtools/download.html) 下载微信开发者工具。 + +2. 在编辑器主菜单的 **Cocos Creator/File -> 偏好设置 -> [外部程序](../../editor/preferences/index.md#%E5%8E%9F%E7%94%9F%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83)** 中设置微信开发者工具路径。 + + ![preference](./publish-wechatgame/preference.png) + +3. 登录微信公众平台,找到 AppID。 + + ![appid](./publish-wechatgame/appid.jpeg) + +## 发布流程 + +1. 使用 Cocos Creator 打开需要发布的项目工程,从 **菜单栏 -> 项目** 中打开 **构建发布** 面板。在 **构建发布** 面板的 **发布平台** 中选择 **微信小游戏**。 + + ![build](./publish-wechatgame/build.png) + + 通用构建选项的设置请参考 [通用构建选项](build-options.md),微信小游戏特有的构建选项如下,具体说明请参考下文 **构建选项** 部分的内容。 + + ![build](./publish-wechatgame/wechat-build.png) + +2. **构建发布** 面板的构建选项设置完成后,点击 **构建**。
+ 构建完成后点击 **构建任务** 左下角的文件夹图标按钮打开项目发布包,可以看到在默认发布路径 `build` 目录下生成了 `wechatgame`(以具体的构建任务名为准)文件夹,其中已经包含了微信小游戏环境的配置文件:`game.json` 和 `project.config.json`。 + + ![package](./publish-wechatgame/package.png) + +3. 然后点击微信小游戏 **构建任务** 右下角的 **运行** 按钮,打开微信开发者工具。 + + ![tool](./publish-wechatgame/tool.png) + + > **注意**:如果之前没有运行过微信开发者工具,可能会出现:`Please ensure that the IDE has been properly installed` 的报错,需要手动打开一次微信开发者工具,然后才能在 Cocos Creator 里直接点击 **运行** 调用。 + +### 主包设置 + +| 主包设置 | 说明 | 字段名(用于命令行发布) | +| :---- | :-- | :-- | +| 初始场景分包 | 勾选后,首场景及其相关的依赖资源会被构建到发布包目录 `assets` 下的内置 Asset Bundle — [start-scene](../../asset/bundle.md#%E5%86%85%E7%BD%AE-asset-bundle) 中,提高初始场景的资源加载速度。 | `startSceneAssetBundle` | +| 资源服务器地址 | 用于填写资源存放在远程服务器上的地址。开发者需要在构建后手动将发布包目录下的 `remote` 文件夹上传到所填写的资源服务器地址上。详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md) | `remoteServerAddress` | + +### 引擎设置 + +Cocos Creator 3.8 中,将引擎相关的构建选项统一到了**引擎设置**中,方便根据需求选择不同的引擎配置。 + +| 引擎设置 | 说明 | 字段名(用于命令行发布) | +| :---- | :-- | :-- | +| CLEANUP_IMAGE_CACHE | 是否在纹理数据上传 GPU 后,删除内存数据,删除后无法使用动态合图功能。 | - | +| 物理系统 | 选择不同的物理系统,发布后以当前选择的为准 | - | +| WebGL 2.0 | 选择不同的 WebGL 版本 | - | +| 原生代码打包模式 | 此选项会影响 Spine、物理等 Webassembly 模块的打包方式,将统一控制所有的 wasm/asmjs 模块,默认为最优值,若无特殊情况,不建议更改。| - | +| Wasm 3D 物理系统(基于 `ammo.js`) | 用于选择是否启用 Wasm,默认为开启,使用 **bullet(ammo.js)** 物理时生效。详情请参考下文 **WebAssembly 支持** 部分的内容。 | - | +| 引擎原生代码分包 | 将引擎的 WASM/Asm.js 代码放入子包,减少包体。| - | +| 启用 WASM Brotli 压缩 | 开启此选项会减少 WASM 模块大小,但会略微增加加载时的解压时间,根据需要开启 | - | + +### 构建选项 + +| 构建选项 | 说明 | 字段名(用于命令行发布) | +| :--- | :--- | :--- | +| 设备方向 | 可选值包括 **Portrait** 和 **Landscape**。构建时会写入到发布包目录下的 `game.json` 文件中。| `orientation` | +| AppID | 微信小游戏的 AppID,必填项,面板中默认的 `wx6ac3f5090a6b99c5` 仅用于测试。构建时会写入到发布包目录下的 `project.config.json` 文件中。| `appid` | +| 生成开放数据域工程模板 | 用于接入开放数据域,详情请参考 [开放数据域](./build-open-data-context.md)。 | `buildOpenDataContextTemplate` | +| 引擎原生代码分包 | 将引擎的 WASM/Asm.js 代码模块放入小游戏分包,减少主包包体。| - | +| 分离引擎 | 是否使用微信小游戏引擎插件,详情请参考 [启用微信小游戏引擎插件](./wechatgame-plugin.md)。 | `separateEngine` | +| 高性能模式 | 是否开启微信的高性能模式
请参考 [高性能模式](https://developers.weixin.qq.com/minigame/dev/guide/performance/perf-high-performance.html) 获取更多信息 | - | + +## 微信小游戏的资源管理 + +在微信小游戏环境中,资源管理是最特殊的部分,它和浏览器的不同包括以下几点: + +- 小游戏的主包体积不能超过 4MB,包含所有代码和资源,额外的资源必须通过网络请求下载。 + +当包体过大时,可在 **构建发布** 面板配置 **资源服务器地址** 选项,将资源上传到远程服务器,详情请参考 [上传资源到远程服务器](../../asset/cache-manager.md)。 +- 对于小游戏包内资源,小游戏环境内并不是按需加载的,而是一次性加载所有包内资源,然后再启动页面。 +- 不可以从远程服务器下载脚本文件。 + +这里引出了两个关键的问题: + +1. 远程资源的下载、缓存及版本管理,这部分内容 Creator 已经帮开发者做好了,详情请参考 [缓存管理器](../../asset/cache-manager.md)。 + +2. 首场景的加载速度。当主包资源放到远程服务器上时,如果要提高初始场景的加载速度,可以在构建时勾选 **构建发布** 面板中的 **初始场景分包** 选项。
+构建完成后,初始场景及其相关的依赖资源会被构建到发布包目录下的 `assets/start-scene` bundle 中。这个 bundle 不会放到远程服务器上,而是放在本地,引擎在启动阶段时就会自动从本地包内加载这个 bundle,从而加快初始场景的加载速度。 + +## 分包加载 + +微信小游戏的分包加载请参考 [小游戏分包](subpackage.md) + +## 平台 SDK 接入 + +除了纯游戏内容以外,其实微信小游戏环境还提供了非常强大的原生 SDK 接口,其中最重要的就是用户、社交、支付等,这些接口都是仅存在于微信小游戏环境中的,等同于其他平台的第三方 SDK 接口。这类 SDK 接口的移植工作在现阶段还是需要开发者自己处理。下面列举一些微信小游戏所提供的强大 SDK 能力: + +1. 用户接口:登陆,授权,用户信息等 +2. 微信支付 +3. 转发以及获得转发信息 +4. 文件上传下载 +5. 媒体:图片、录音、相机等 +6. 其他:位置、设备信息、扫码、NFC、等等 + +## WebAssembly 支持 + +> **注意**:该部分内容在 v3.3.1 有较大的改动,v3.3.0 的内容请在页面右上角切换旧版本文档(例如 v3.2)查看。 + +从 Cocos Creator 3.0 开始,微信小游戏的构建选项中新增了 **Wasm 3D 物理系统(基于 `ammo.js`)**,当编辑器主菜单的 **项目 -> 项目设置 -> 功能裁剪 -> 3D -> 物理系统** 设置为 **bullet(ammo.js)** 时生效。 + +**Wasm 3D 物理系统** 默认开启,构建时会自动打包 `wasm` 模式的代码。若不开启则使用 `js` 模式。 + +> **注意**: +> 1. 微信小游戏引擎插件目前仅支持 `js` 模式。 +> 2. 微信 WebAssembly 要求微信版本为 v7.0.17 及以上。 +> 3. 微信 WebAssembly 要求微信开发者工具的调试基础库为 v2.12.0 及以上。 + +## 微信小游戏的限制 + +微信小游戏不支持 WebView。 + +## 参考链接 + +- [微信小游戏开发文档](https://developers.weixin.qq.com/minigame/dev/guide/) +- [微信公众平台](https://mp.weixin.qq.com/) +- [小游戏 API 文档](https://developers.weixin.qq.com/minigame/dev/api/) +- [微信开发者工具下载](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html) +- [微信开发者工具文档](https://developers.weixin.qq.com/miniprogram/dev/devtools/devtools.html) diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame/appid.jpeg b/versions/4.0/zh/editor/publish/publish-wechatgame/appid.jpeg new file mode 100644 index 0000000000..e77e31bc85 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-wechatgame/appid.jpeg differ diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame/build.png b/versions/4.0/zh/editor/publish/publish-wechatgame/build.png new file mode 100644 index 0000000000..9d7d3fcc98 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-wechatgame/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame/details.png b/versions/4.0/zh/editor/publish/publish-wechatgame/details.png new file mode 100644 index 0000000000..66fcc0abe2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-wechatgame/details.png differ diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame/package.png b/versions/4.0/zh/editor/publish/publish-wechatgame/package.png new file mode 100644 index 0000000000..58f9333f98 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-wechatgame/package.png differ diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame/preference.png b/versions/4.0/zh/editor/publish/publish-wechatgame/preference.png new file mode 100644 index 0000000000..e075cc62cf Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-wechatgame/preference.png differ diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame/subpackage.png b/versions/4.0/zh/editor/publish/publish-wechatgame/subpackage.png new file mode 100644 index 0000000000..4f0678bd8a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-wechatgame/subpackage.png differ diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame/tool.png b/versions/4.0/zh/editor/publish/publish-wechatgame/tool.png new file mode 100644 index 0000000000..09bc196496 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-wechatgame/tool.png differ diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame/wechat-build.png b/versions/4.0/zh/editor/publish/publish-wechatgame/wechat-build.png new file mode 100644 index 0000000000..703948657f Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-wechatgame/wechat-build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-wechatgame/wechat-pc.png b/versions/4.0/zh/editor/publish/publish-wechatgame/wechat-pc.png new file mode 100644 index 0000000000..fbe5f592b6 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-wechatgame/wechat-pc.png differ diff --git a/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/build-option.png b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/build-option.png new file mode 100644 index 0000000000..cca71f608f Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/build-option.png differ diff --git a/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/build.png b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/build.png new file mode 100644 index 0000000000..fcbb794a8c Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/build.png differ diff --git a/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/certificate.png b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/certificate.png new file mode 100644 index 0000000000..3e15f57cfe Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/certificate.png differ diff --git a/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/debug.png b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/debug.png new file mode 100644 index 0000000000..7510fe7da3 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/debug.png differ diff --git a/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/package.png b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/package.png new file mode 100644 index 0000000000..b337da436a Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/package.png differ diff --git a/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/play.png b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/play.png new file mode 100644 index 0000000000..bbfdc3a1e5 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/play.png differ diff --git a/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/play2.png b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/play2.png new file mode 100644 index 0000000000..67e219f335 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/play2.png differ diff --git a/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/rpk.png b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/rpk.png new file mode 100644 index 0000000000..cf144ddb67 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/rpk.png differ diff --git a/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/run.png b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/run.png new file mode 100644 index 0000000000..1d138f3f98 Binary files /dev/null and b/versions/4.0/zh/editor/publish/publish-xiaomi-quick-game/run.png differ diff --git a/versions/4.0/zh/editor/publish/setup-native-development.md b/versions/4.0/zh/editor/publish/setup-native-development.md new file mode 100644 index 0000000000..fb8b213e54 --- /dev/null +++ b/versions/4.0/zh/editor/publish/setup-native-development.md @@ -0,0 +1,121 @@ +# 安装配置原生开发环境 + +Cocos Creator 支持将项目构建为符合不同平台规范的原生应用工程。在开始构建发布原生应用之前,开发者需要先配置好相关的原生开发环境。 + +### 下载 Java SDK(JDK) + +编译 Android 工程需要运行设备上有完整的 Java SDK 工具,请到以下地址下载与设备操作系统和架构匹配的安装包: + +[JDK Development Kit 17.0.7 downloads](https://www.oracle.com/java/technologies/downloads/#java17) + +通常 Windows 系统设备安装完 JDK 之后,还需要在环境变量中添加 `JAVA_HOME` 系统变量。通过 `右键点击我的电脑 -> 选择属性 -> 打开高级系统设置 -> 环境变量` 等步骤查看和修改环境变量。需要添加的变量名与值如下图所示(变量值为 JDK 安装路径),添加之后重启电脑即可生效。 + +![windows-java-home](./setup-native-development/windows-java-home.jpg) + +在 Mac 终端或者 Windows 命令行工具中输入下方命令行验证 Java 环境是否配置成功,若执行后能够显示版本信息则表示安装成功。 + +``` +java -version +``` + +如果系统中使用的是 JRE,则需要安装 [JAVA SE 运行环境](http://www.oracle.com/technetwork/java/javase/downloads/index.html)。 + +> **注意**:[OpenJDK](https://openjdk.org/) 和 JDK 仅是开源协议不同,功能和配置方法没有本质区别。 + +## 下载 Android Studio + +不同版本的 CocosCreator 引擎通常兼容不同版本的 Android Studio。用户可以根据下方版本推荐信息,前往 [Android Studio 历史版本](https://developer.android.com/studio/archive?hl=zh-cn) 下载对应的 IDE。 + +| CocosCreator 引擎版本范围 | Android Studio 推荐使用版本 | JDK 版本 | +| :-------------- | :----------- | :----------- | +| v2.4.12 及以上 或 v3.8.0 及以上 | 2022.2.1 或 2022.3.1 | JDK 17 | +| v2.4.11 及以下 或 v3.0.0 ~ 3.7.4 | 2020.3.1 或更早的版本 | JDK 11 | + +## 下载 Android 平台所需的 SDK 和 NDK + +安装 Android Studio 完成后,参考官方文档,打开 SDK Manager:[SDK Manager 使用说明](https://developer.android.google.cn/studio/intro/update.html#sdk-manager)。 + +1. 在 SDK Platforms 分页栏,勾选你希望安装的 API Level,推荐选择主流 API Level 26(8.0)、API Level 28(9.0)等。 + * [查看 API Level 对应的 Android 系统版本](https://developer.android.google.cn/tools/releases/platforms) + * [查看引擎支持的最低系统版本](../../advanced-topics/supported-versions.md) +2. 记住此时窗口上的 **Android SDK Location** 路径,后续将用于在 Cocos Creator 编辑器中配置 SDK 与 NDK 路径。 +3. 打开 SDK Tools 分页栏,在安装所需工具前,需要勾选右下角的 **Show Package Details** 显示不同版本的工具。 +4. 在 **Android SDK Build-Tools** 里,选择最新的 build tools 版本。 +5. 勾选 **Android SDK Platform-Tools** 和 **CMake**,如需安装 Android 支持库,请参考 [官方文档 — 支持库设置](https://developer.android.google.cn/topic/libraries/support-library/setup)。 +6. 勾选需要下载的 **NDK** 版本。推荐选择 **r21 ~ r23** 范围的版本避免意外的适配错误。使用 Apple M 系列芯片的用户推荐使用 **r24** 版本。 +7. 点击 **Apply** 之后进行安装,后续如果需要删除已安装的工具,也可以在此处进行。 + + ![sdk manager](setup-native-development/sdk-manager.png) + +### 无法下载 SDK 或 NDK 的解决方案 +因为网络安全的原因,有时候国内用户下载 Google 服务器上的软件工具会出现下载失败或者下载速度慢的问题,可以尝试以下方法: + +* 在 Android Studio 中配置 HTTP 代理: + + **自动配置代理**:根据菜单路径 `Setting -> Appearance & Behavior -> System Settings` 打开 HTTP Proxy 页面,勾选 Auto-detect proxy settings,填入镜像源后点击 Apply 即可。 + + ![http-proxy.png](./android/images/http-proxy.png) + + **可选的镜像源**:安卓 SDK 以及 Gradle 都会有一些镜像源用于帮助您解决无法从官网下载的问题,镜像源存在更新滞后以及失效的可能性,如果无法解决问题则用户需要自行寻找有效镜像源。 + + | 镜像源 | 地址 | + | :--- | :--- | + | 腾讯 | | + | 阿里云 | | + +* 在 Android Studio 中配置 VPN 代理: + + **手动配置代理**:根据菜单路径 `Setting -> Appearance & Behavior -> System Settings` 打开 HTTP Proxy 页面,勾选 Manual proxy configuration,填入你的本地 VPN IP 地址与端口,点击 Apply 即可: + + ![manual-proxy.png](./android/images/manual-proxy.png) + + > 注意:如果遇到开启 VNP 代理后反而下载速度慢的问题,请关闭 VPN 代理配置。 + +* 手动下载 NDK 并为项目配置 NDK 路径: + + **手动下载**:在 [NDK 历史版本](https://github.com/android/ndk/wiki/Unsupported-Downloads) 找到所需版本下载。(可能需要开启 VPN 代理)下载并解压到本地目录后,在 Cocos Creator 编辑器中配置 NDK 路径。 + + ![download-ndk.png](./android/images/ndk-dir.png) + +## 安装 Windows 与 Mac/iOS 的发布环境 + +- Windows 下需要安装 [Visual Studio 2019/2022 社区版](https://www.visualstudio.com/downloads/download-visual-studio-vs)。在安装 Visual Studio 时,请勾选 **使用 C++ 的桌面开发** 和 **使用 C++ 的游戏开发** 两个模块。 +- Visual Studio 安装后可用于构建发布 Windows 平台游戏。 + + > **注意**:安装 Visual Studio 时请勿勾选 **使用 C++ 的游戏开发** 模块中的 **Cocos** 选项。 + +- Mac 下需要安装 Xcode 14 及以上版本,[前往下载](https://apps.apple.com/us/app/xcode/id497799835)。 +- XCode 安装后可用于构建发布 Mac & iOS 平台游戏。 + +## 配置安卓工程构建发布环境路径 + +原生项目构建与编译环境安装成功后,需要到 Cocos Creator 编辑器中配置开发环境路径,不需要编译 Android 平台的话这里可以跳过。
+在主菜单中选择 **Cocos Creator -> 设置**,打开 **偏好设置** 面板,我们需要在 **程序管理器** 页面中配置 Android SDK 与 NDK 的路径: + +- **Android SDK**:填入已安装 SDK 的 `Android SDK Location` 路径(Android SDK 的目录下应该包含 `build-tools`、`platforms` 等文件夹) + +- **Android NDK**:根据已安装 SDK 的 `Android SDK Location` 路径,在其中找到 ndk 文件夹。ndk 文件夹存放用户选择下载的不同版本 NDK。在 **程序管理器** 的 `安卓 NDK` 一栏填入你下载的 NDK 版本的路径 + + > **注意**:新版本 Android Studio 的 NDK 可下载多版本并存放在 ndk 目录下,但是老版本 Android Studio 只能下载一个版本的 NDK 存在 `ndk-bundle` 文件夹中。 + + ![preference](setup-native-development/sdk.png) + +> **注意**:这里的配置会在编译 **原生工程** 的时候生效。如果没有生效(一些 Mac 机器有可能出现这个情况),可能需要您尝试到 **系统环境变量** 设置这些值:`NDK_ROOT`、`ANDROID_SDK_ROOT`。 + +## 注意事项 + +在之前的版本中收到了很多原生打包的问题反馈,这里补充一些可能的问题原因。 + +1. 包名问题 + + 检查 **构建发布** 面板中的包名,具体命名规范请参考相应原生平台中的 [构建选项说明](./native-options.md#%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9)。 + +2. Android 编译成功,但运行时提示 `dlopen failed: cannot locate symbol "xxxx" referenced by "libcocos.so"...` + + 请检查 NDK 和 Android SDK 的架构和版本是否和测试用的 Android 系统相对应,另外可以尝试使用本文所用的 NDK 和 Android SDK 版本来测试。 + +3. JDK 版本问题 + + 升级 Android Studio 和 Gradle 的版本后,需要将 JDK 版本升级到 17 或其指定版本。这样可以确保你的开发环境与最新的 Android Studio 和 Gradle 版本兼容。 + +最后,如果依然打包失败,请附上详细的复现步骤及报错信息通过 [论坛](https://forum.cocos.org/c/58) 反馈给我们。 diff --git a/versions/4.0/zh/editor/publish/setup-native-development/sdk-manager.png b/versions/4.0/zh/editor/publish/setup-native-development/sdk-manager.png new file mode 100644 index 0000000000..4d36367000 Binary files /dev/null and b/versions/4.0/zh/editor/publish/setup-native-development/sdk-manager.png differ diff --git a/versions/4.0/zh/editor/publish/setup-native-development/sdk.png b/versions/4.0/zh/editor/publish/setup-native-development/sdk.png new file mode 100644 index 0000000000..876584b26f Binary files /dev/null and b/versions/4.0/zh/editor/publish/setup-native-development/sdk.png differ diff --git a/versions/4.0/zh/editor/publish/setup-native-development/windows-java-home.jpg b/versions/4.0/zh/editor/publish/setup-native-development/windows-java-home.jpg new file mode 100644 index 0000000000..0cfdde10d4 Binary files /dev/null and b/versions/4.0/zh/editor/publish/setup-native-development/windows-java-home.jpg differ diff --git a/versions/4.0/zh/editor/publish/subpackage.md b/versions/4.0/zh/editor/publish/subpackage.md new file mode 100644 index 0000000000..8de787ca5a --- /dev/null +++ b/versions/4.0/zh/editor/publish/subpackage.md @@ -0,0 +1,101 @@ +# 小游戏分包 + +部分小游戏平台支持分包功能以便对资源、脚本和场景进行划分,包括微信小游戏、百度小游戏、抖音小游戏、华为快游戏、OPPO 小游戏和 vivo 小游戏。 + +Cocos Creator 从 v2.4 开始支持 [Asset Bundle](../../asset/bundle.md),开发者可以将需要分包的内容划分成多个 Asset Bundle,这些 Asset Bundle 会被构建成小游戏的分包。在启动游戏时只会下载必要的主包,不会加载这些分包,而是由开发者在游戏过程中手动加载分包,从而有效降低游戏启动的时间。 + +## 配置方法 + +Asset Bundle 是以 **文件夹** 为单位进行配置的。当我们在 **资源管理器** 中选中一个文件夹时,**属性检查器** 中就会出现一个 **配置为 Bundle** 的选项,勾选后会出现如下图的配置项: + +![subpackage configuration](subpackage/subpackage-config.png) + +除了通用的 [Asset Bundle 配置](../../asset/bundle.md#%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95) 之外,小游戏分包主要需要关注的设置包括: +- 将 **目标平台** 设置成需要分包的小游戏平台,**压缩类型** 设置为 **小游戏分包**。 +- 小游戏分包只能放在本地,不能配置为远程包,所以 **配置为远程包** 项为锁住状态,不可勾选。 + +配置完成后点击右上方的 **打勾** 按钮,这个文件夹就被配置成 Asset Bundle 了。 + +## 构建 + +在构建时,需要将 **构建发布** 面板中的 **主包压缩类型** 设置为 **小游戏分包**。具体说明可参考文档 [Asset Bundle — 压缩类型](../../asset/bundle.md#%E5%8E%8B%E7%BC%A9%E7%B1%BB%E5%9E%8B)。 + +项目构建完成后,这个 Asset Bundle 文件夹会被打包到小游戏平台发布包目录下的 **subpackages** 文件夹中。该文件夹中包含的每个文件夹都是一个 Asset Bundle。 + +例如:将 Hello World 工程中的 `assets/scene` 文件夹在微信小游戏平台配置为 Asset Bundle,那么项目构建后将会在微信小游戏发布包目录下生成 `subpackages/scene` 文件夹。`scene` 文件夹就是一个 Asset Bundle。 + +![subpackage](subpackage/subpackage.png) + +## 微信小游戏 + +在微信小游戏的构建中,Asset Bundle 的配置也会按照规则自动生成到微信小游戏发布包目录下的 `game.json` 配置文件中。 + +![profile](subpackage/profile.png) + +> **注意**:微信小游戏需要特定的版本才能支持分包功能。微信 6.6.7 客户端,2.1.0 及以上基础库开始支持,请更新至最新客户端版本,开发者工具请使用 1.02.1806120 及以上版本。更新了开发者工具后不要忘记修改开发者工具中的 **详情 -> 本地设置 -> 调试基础库** 为 2.1.0 及以上: +> +> ![devtools setting](./subpackage/devtools-setting.png) + +### 分包加载包大小的限制 + +目前微信小游戏分包大小有以下限制: + +- 整个微信小游戏所有分包大小不超过 **30M** +- 单个分包不限制大小 +- 主包大小不能超过 **4M** + +具体请参考 [微信小游戏分包加载官方文档](https://developers.weixin.qq.com/minigame/dev/guide/base-ability/subPackage/useSubPackage.html)。 + +## 抖音小游戏 + +在抖音小游戏的构建中,Asset Bundle 的配置也会按照规则自动生成到抖音小游戏发布目录下的 **game.json** 配置文件中。 +![profile](subpackage/profile.png) + +**game.json** 中具体的字段含义,可以参考文档:[抖音小游戏配置](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/framework/mini-game-configuration/)。 + +### 分包大小限制 + +目前抖音小游戏分包大小有以下限制: + +- ​小游戏整体包(小游戏包整个目录)大小不超过 20MB;​ +- 单个主包不超过 4MB;​ +- 单个分包大小不超过 20MB。 + +> 抖音小游戏开放数据域文件类似子包,该文件 / 目录的大小不超过 4MB。​ + +详情请参考文档:[抖音小游戏分包加载](https://developer.open-douyin.com/docs/resource/zh-CN/mini-game/develop/framework/subpackages/introduction)。 + +## 淘宝小游戏 + +在淘宝小游戏的构建中,Asset Bundle 的配置也会按照规则自动生成到淘宝小游戏发布包目录下的 `setting.json` 配置文件中。 + +![image](https://github.com/user-attachments/assets/3842a436-fe43-4042-97cc-a2e070b841d8) + + +### 分包加载包大小的限制 + +目前淘宝小游戏分包大小有以下限制: + +- 整个淘宝小游戏所有分包大小不超过 **20M** +- 单个分包不能超过 **5M** +- 主包大小不能超过 **4M** + +具体请参考 [淘宝小游戏分包加载官方文档](https://miniapp.open.taobao.com/doc.htm?spm=a219a.7386797.0.0.feff669aqaekOs&source=search&docId=121604&docType=1&tag=game-dev)。 + +## vivo 小游戏 + +在 vivo 小游戏的构建中,Asset Bundle 的配置也会按照规则自动生成到 vivo 小游戏发布包 `vivo-mini-game/src` 目录下的 `manifest.json` 配置文件中。以配置 `scene` 文件夹为例: + +![profile](./subpackage/vivo-profile.png) + +> **注意**: +> 1. **快应用 & vivo 小游戏调试器** 从 **1051** 版本开始支持 vivo 小游戏分包加载。低于 1051 的版本虽然不支持分包加载,但是也做了兼容处理,如果使用了分包也不会影响游戏正常运行。具体可参考 [vivo 分包加载-运行时兼容](https://minigame.vivo.com.cn/documents/#/lesson/base/subpackage?id=%e8%bf%90%e8%a1%8c%e6%97%b6%e5%85%bc%e5%ae%b9)。 +> 2. 与其他小游戏平台不同的是,项目构建后 vivo 小游戏的 Asset Bundle 文件夹是生成在发布包 `vivo-mini-game` 目录下的 `src` 目录。 +> +> ![](./subpackage/vivo-subpackages.png) + +### 分包加载包的限制 + +目前 vivo 小游戏分包大小限制为 20M(主包限制 4M 及分包大小限制 16M)。 + +具体请参考 [vivo 小游戏官方文档 — 分包加载](https://minigame.vivo.com.cn/documents/#/lesson/base/subpackage)。 diff --git a/versions/4.0/zh/editor/publish/subpackage/devtools-setting.png b/versions/4.0/zh/editor/publish/subpackage/devtools-setting.png new file mode 100644 index 0000000000..cdc27d98d2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/subpackage/devtools-setting.png differ diff --git a/versions/4.0/zh/editor/publish/subpackage/profile.png b/versions/4.0/zh/editor/publish/subpackage/profile.png new file mode 100644 index 0000000000..5aea3b02d1 Binary files /dev/null and b/versions/4.0/zh/editor/publish/subpackage/profile.png differ diff --git a/versions/4.0/zh/editor/publish/subpackage/subpackage-config.png b/versions/4.0/zh/editor/publish/subpackage/subpackage-config.png new file mode 100644 index 0000000000..d36609025a Binary files /dev/null and b/versions/4.0/zh/editor/publish/subpackage/subpackage-config.png differ diff --git a/versions/4.0/zh/editor/publish/subpackage/subpackage.png b/versions/4.0/zh/editor/publish/subpackage/subpackage.png new file mode 100644 index 0000000000..f86af19eda Binary files /dev/null and b/versions/4.0/zh/editor/publish/subpackage/subpackage.png differ diff --git a/versions/4.0/zh/editor/publish/subpackage/vivo-profile.png b/versions/4.0/zh/editor/publish/subpackage/vivo-profile.png new file mode 100644 index 0000000000..58b5b92a34 Binary files /dev/null and b/versions/4.0/zh/editor/publish/subpackage/vivo-profile.png differ diff --git a/versions/4.0/zh/editor/publish/subpackage/vivo-subpackages.png b/versions/4.0/zh/editor/publish/subpackage/vivo-subpackages.png new file mode 100644 index 0000000000..effe4ea371 Binary files /dev/null and b/versions/4.0/zh/editor/publish/subpackage/vivo-subpackages.png differ diff --git a/versions/4.0/zh/editor/publish/taobaominigame-plugin.md b/versions/4.0/zh/editor/publish/taobaominigame-plugin.md new file mode 100644 index 0000000000..aa48bd2606 --- /dev/null +++ b/versions/4.0/zh/editor/publish/taobaominigame-plugin.md @@ -0,0 +1,32 @@ +# 淘宝小游戏引擎插件使用说明 + +游戏引擎插件是淘宝新增的一项功能。此插件内置了 Cocos Creator 引擎的官方版本,若玩家首次体验的游戏中启用了此插件,则所有同样启用此插件的游戏,都无需再次下载 Cocos Creator 引擎,只需直接使用公共插件库中的相同版本引擎,或者增量更新引擎(3.x引擎更新)即可。 + +例如,当一个玩家玩过了由 Cocos Creator v3.7.3 开发的 A 游戏,里面已启用了此插件。然后他又玩了同样是 v3.7.3 开发的 B 游戏,如果 B 游戏也启用了此插件,那么就无需重新下载 Cocos Creator 引擎。即使 B 游戏使用的是 v3.8.2 的 Cocos Creator,淘宝也只需要增量更新引擎两个版本的差异部分。这样就可以大幅减少小游戏的下载量,提升小游戏启动速度 0.5 ~ 2s,获得更好的用户体验。 + +## 使用说明 + +Cocos Creator 提供了强大的集成式游戏开发环境,使用引擎插件非常简单。 + +## Cocos Creator v3.7.3 及以上版本 +在以上版本中已集成此插件。只需在 构建发布 面板中,勾选 Taobao Engine Plugin(Seperate Engine),然后正常构建发布即可,无需其它人工操作。(此功能仅在非调试模式生效) + +![构建面板选项](./publish-taobao-mini-game/build-options.png) + +## Q&A + +Q:引擎插件功能是否支持自定义引擎? +A:不支持,构建时只能使用编辑器原版内置引擎,否则编辑器将会弹出提示。 + +Q:项目开启了引擎的模块裁剪,要使用引擎插件的话需要还原为完整版引擎吗? +A:无需修改,项目可以按原来的方式继续裁剪引擎。引擎插件提供的是完整版引擎,能兼容所有的裁剪设置,不会影响原有项目的包体。 + +Q:启用引擎插件后,是否仍然会把引擎代码算入首包包体中? +A:根据淘宝的规则,目前仍然会计算在内。 + +Q:开启引擎插件后,是否可以在编辑器 模块设置 中移除所有模块,减小包体? +A:若随意裁剪引擎可能导致游戏无法在低版本淘宝上运行。 + +## 参考链接 + +- [淘宝小游戏引擎插件官方说明](https://miniapp.open.taobao.com/doc.htm?docId=121623&docType=1&tag=game-dev) \ No newline at end of file diff --git a/versions/4.0/zh/editor/publish/wechat-ios-optimize.md b/versions/4.0/zh/editor/publish/wechat-ios-optimize.md new file mode 100644 index 0000000000..d10c890acd --- /dev/null +++ b/versions/4.0/zh/editor/publish/wechat-ios-optimize.md @@ -0,0 +1,169 @@ +# IOS 微信小游戏内存与性能优化指南 + +## 前言 + +由于微信小游戏架构是基于 C++ 渲染层模拟 Canvas 渲染功能的实现方案,导致在 iOS 端的性能表现一直差强人意。 同样的程序版本,与有 JIT 加速的 Android 比起来,相差甚远。 + +微信小游戏提供的高性能模式,通过转用微信内部的 Webkit 运行游戏的方式,使得在 iOS 上的小游戏,也能拥有 JIT 能力,大幅度提升运行性能。 + +从微信小游戏官方文档的水族馆测试中,我们可以看在,同样的场景,在 iPhone11 Pro Max 上,高性能模式下达到了 49 FPS,普通模式下却只有 13 FPS。 + +![1](optimize/1.png) + +本文将从以下四个方面进行讲解: +- 高性能模式 +- 运行时内存结构 +- 内存问题诊断 +- 内存优化技巧 + +> 因为内存问题在 Android 端表现良好,在 iOS 端反馈较多,所以本文仅对 iOS 端内存优化做介绍。 + +## 高性能模式 + +有关高性能模式的介绍以及使用方法请详细阅读微信小游戏开发文档:[高性能模式](https://developers.weixin.qq.com/minigame/dev/guide/performance/perf-high-performance.html)(可长按文末二维码)。 + +### 开通方式 + +1. 登录微信公众平台 -> 首页能力地图模块 -> 点击进入"生产提效包" -> 点击开通高性能模式。 +2. 开通成功后,通过配置 game.json 的 iOSHighPerformance 为 true 则可进入高性能模式。 + +> 通过去掉此开关可以正常回退到普通模式,以便两种模式对比。 + +### 注意问题 + +1. 高性能模式下,游戏将拥有更好的渲染性能和表现,但是它对游戏的内存要求更加严格。 + +2. 在高性能模式下,小游戏运行于浏览器内核环境,所以兼容性、内存消耗、稳定性等方面需要单独进行测试,不能复用普通模式的测试结果。 + +3. 在 iOS 设备中,iphone 6s/7/8 等 2G RAM 机型的内存限制为 1G,iphone 7P/8P/iPhoneX/XR/XSAMX/11 等 3G RAM 机型的内存限制为 1.4G,一旦应用程序的内存占用超过这个阀值,就会被系统杀掉进程。因此开发者务必保证内存峰值不超过该数值。 + +4. 如果游戏没做好内存优化,不建议开启高性能模式,否则在 iOS 低端机容易出现内存异常退出的情况,如有内存问题,可参考本文的内存优化技巧,充分优化内存。 + +## 运行时内存结构 + +![2](./optimize/2.png) + +从上图中可以看到,运行时内存一共由 6 个部分组成。 + +### JavaScript Heap + +在高性能模式下,小游戏运行于浏览器内核环境,因此 JavaScript Heap 包含游戏逻辑代码内存。 通常我们可以打包 web-mobile 端,使用 Mac 平台的 Safari 浏览器的调试工具来远程调试手机 safari 的内存情况。 需要注意的是 JavaScript Heap 通常无法看出具体物理内存使用。 + +### WASM 资源 + +为了提高 JS 模块的执行性能,比如物理引擎的计算,我们会将一些 C++ 代码直接编译成 WASM 代码片段来达到优化性能的需求。 比如 Cocos Creator 部分引用的第三方物理库就是 WASM 版本。 + +### 基础库和 Canvas + +基础库可以理解为微信小游戏的黑盒环境暴露的 API 封装,可以防止将浏览器内核环境 API 暴露给开发者,实际测试基础库内存占用在 70M 左右。小游戏环境第一个创建的 Canvas 是主 Canvas,也是唯一可以将渲染表面同步到主界面的 Canvas,即呈现我们游戏的渲染表现。Canvas 的内存占用跟 Canvas 的宽高大小成正比。 + +### 音频文件 + +音频文件内存是指加载到内存的音频实例。 + +### GPU 资源 + +比如顶点数据缓存,索引数据缓存,纹理缓存和渲染表面缓存等等。 + +## 内存问题诊断 + +下面给大家介绍一些常用的 iOS 内存诊断工具,它们可以辅助我们快速定位内存问题,找出解决办法。 + +### 常用 iOS 设备内存查看工具 + +- Xcode 自带的 Instrument 分析工具 +- Perfdog 工具 +- 微信开发者工具 + +> **注意**:iOS 端小游戏的进程名称在不同模式下有区别。 +> - 高性能模式:含有 WebContent +> - 普通模式:含有 WeChat + +### XCode Instruments + +![3](optimize/3.png) + +XCode Instruments 是 XCode 自带的应用程序运行时分析工具,它同样适用于微信小游戏进程。 + +使用 Activity Monitor,选择对应的设备 all processes 捕捉,等进程列表刷新后,输入 webkit 进行过滤,即可看到所有进程的 CPU 与内存情况. + +![4.png](optimize/4.png) + +### Perfdog + +Perfdog(性能狗)是一个 iOS/Android/PC/ 主机平台的性能测试和分析工具,具体使用方式请参考: + +[https://perfdog.qq.com/](https://perfdog.qq.com/) + +选择对应的设置-进程名,即可看到相关性能数据。 + +可以参考【开发阶段内存调优: 把一切都控制在最开始 | 微信开放文档】: +[https://developers.weixin.qq.com/minigame/dev/guide/performance/perf-action-memory-dev-profile.html](https://developers.weixin.qq.com/minigame/dev/guide/performance/perf-action-memory-dev-profile.html)。 + +### 微信开发者工具 + +微信开发者工具主要使用它的调试器来跟踪内存数据,操作流程如下: +1. 进入调试开发者工具界面 +2. 将 【Memory】勾选,然后刷新游戏 +3. 点击下图左上角的小圆圈按钮开始录制 +4. 结束录制后,就会显示下图界面 + +我们主要关注两个波形图,一个是 Main 波形图,用于查看逻辑帧调用栈,一个是 JS Heap 的峰值曲线,用于观察内存增长变化。如果我们观察到某个时刻 JS Heap 值增加,然后我们就可以查看逻辑帧调用栈大概确认数据来源。 + +![5](optimize/5.png) + +通过上述步骤可以分析出游戏的 JS 内存分布,然后当我们要确定某块 JS 内存的来源与释放情况,就需要用到下面的内存泄漏检测工具 - **实时内存诊断**。 + +![6](optimize/6.png) + +具体操作步骤如下: + +1. 点击左上角的小圆圈按钮,会进入下面的录制按钮,柱状图的出现表示某个内存块的创建,消失标识内存块被释放。左上角的垃圾桶按钮是主动触发 JS 引擎的 GC 的按钮,点击后可以加快内存回收速度。 + +![7](optimize/7.png) + +1. 再次点击左上角的红色小圆圈按钮结束录制,这时候我们可以选中蓝色区域,然后会显示该内存块包含的对象,这些是在内存中未被释放的资源,选中某个对象后,可以在 Retainers 界面看到对象的内存引用关系。到这里你可以根据代码层的逻辑关系来推理内存对象是否应该被释放,从而确认是否内存泄露。 + +![8](optimize/8.png) + +## 内存优化技巧 + +常见微信小游戏项目的内存由下面几个部分组成: +1. 小游戏基础库 +2. 引擎脚本内存 +3. 业务脚本内存 +4. 音频内存 +5. 字体内存 +6. 图片内存 +7. Canvas 内存 + +知道了内存组成部分后,我们就可以针对不同的部分做一些优化。 + +以 iOS 高性能模式为例,常用的内存优化技巧如下: +1. 小游戏通常基础库的内存 ~= 70M,常驻内存,不可优化。 + +2. 引擎内存占用加载是确定的,由于引擎加载会初始化渲染器,所以通常主 Canvas 内存占用也在这个时候确定,这块内存占用可以通过配置渲染分辨率的倍数来优化。运行时根据引擎模块需要,会动态增加一些缓存内存,开发者可以根据功能需要通过编辑器项目设置里面的功能裁剪来减少引擎内存占用。 + +3. 脚本内存包含引擎和业务代码、配置表数据, 根据游戏的开发体量,业务代码和配置表数据内存会有几百 M 的大小,只能用户自己做优化。 + +4. 单个双通道的音频实例可能在 20M 左右,音频播放完后做释放会减少这块内存损耗,也可以精简成单通道音频减少内存。 + +5. 在国内,一般使用的是中文字体,加载后内存占用至少大于 10M,所以尽量使用系统字,使用应用内部的共享资源。如果开发条件允许的情况下,可以使用 Bitmap 字体和 SDF 字体渲染。 + +6. 图片内存是常用资源,根据加载需要,可以选择填充纹理后释放,或者缓存于内存中以便下次重新填充纹理。在iOS端上建议使用 astc 压缩纹理格式,同时禁用动态合批,这样可以释放 image 资源内存。压缩纹理本身也比 png 的内存占用小超过50%,但是 astc 的文件大小会比 png 大,所以会增加包体大小。通常为了减少首包大小,尽量将图片资源放到小游戏分包或者远程分包。 + +7. TTF 字体文本渲染时会创建 Canvas 对象,Canvas 对象使用完会被回收到缓存池中,文本渲染的字号越多, 缓存池就越大,目前引擎没有提供回收机制,必要时可以修改引擎来释放 Canvas 缓存池。如果游戏运行内存占用比较高,可以使用 Bitmap 字体替代 TTF 字体。 + +8. 还有其他的 JS 内存对象,比如 JSON 文件的释放,根据引擎提供的能力按需释放。 + +> 本文由 **Cocos 引擎官方技术支持团队** 提供,已同步到引擎官方开源仓库中:[微信小游戏内存优化指南(IOS 端)](https://github.com/cocos/cocos-awesome-tech-solutions/blob/cdee10f39c7704724392c76d232d21b8338ccc2b/summary/Cocos%20%E5%BE%AE%E4%BF%A1%E5%B0%8F%E6%B8%B8%E6%88%8F%E5%86%85%E5%AD%98%E4%BC%98%E5%8C%96%E6%8C%87%E5%8D%97%EF%BC%88iOS%E7%AB%AF%EF%BC%89.md),欢迎大家 Star、Fork。 + +![9](optimize/9.png) + +## 参考文章 + +1. [高性能模式 | 微信开放文档](https://developers.weixin.qq.com/minigame/dev/guide/performance/perf-high-performance.html) + +2. [微信小游戏的内存调优指南 - 知乎](https://zhuanlan.zhihu.com/p/297665769) + +3. [性能测试实践 | PerfDog助力微信小游戏/小程序性能调优 - 腾讯WeTest - 博客园](https://www.cnblogs.com/wetest/p/13346501.html) diff --git a/versions/4.0/zh/editor/publish/wechatgame-plugin.md b/versions/4.0/zh/editor/publish/wechatgame-plugin.md new file mode 100644 index 0000000000..33da7d75df --- /dev/null +++ b/versions/4.0/zh/editor/publish/wechatgame-plugin.md @@ -0,0 +1,35 @@ +# 微信小游戏引擎插件使用说明 + +游戏引擎插件是微信 v7.0.7 新增的一项功能。此插件内置了 Cocos Creator 引擎的官方版本,若玩家首次体验的游戏中启用了此插件,则所有同样启用此插件的游戏,都无需再次下载 Cocos Creator 引擎,只需直接使用公共插件库中的相同版本引擎,或者增量更新引擎即可。 + +例如,当一个玩家玩过了由 Cocos Creator v2.2.0 开发的 A 游戏,里面已启用了此插件。然后他又玩了同样是 v2.2.0 开发的 B 游戏,如果 B 游戏也启用了此插件,那么就无需重新下载 Cocos Creator 引擎。即使 B 游戏使用的是 v2.2.1 的 Cocos Creator,微信也只需要增量更新引擎两个版本的差异部分。这样就可以大幅减少小游戏的下载量,提升小游戏启动速度 0.5 ~ 2s,获得更好的用户体验。 + +## 使用说明 + +Cocos Creator 只需在 **构建发布** 面板中,勾选 **分离引擎** 选项,然后正常构建发布即可,无需其它人工操作。(此功能仅在编辑器使用内置引擎并且构建时使用 **非调试模式** 时生效) + +![构建面板选项](./wechatgame-plugin/build-options.png) + +## Q&A + +Q:引擎插件功能是否支持自定义引擎?
+A:不支持,构建时如果版本不匹配或者启用了自定义引擎,虽然编辑器在出现报错后会继续构建,但是构建生成的包实际上无法正常使用引擎插件。 + +Q:项目开启了引擎的模块裁剪,要使用引擎插件的话需要还原为完整版引擎吗?
+A:无需修改,项目可以按原来的方式继续裁剪引擎。引擎插件提供的是完整版引擎,能兼容所有的裁剪设置,不会影响原有项目的包体。 + +Q:启用引擎插件后,是否仍然会把引擎代码算入首包包体中?
+A:根据微信的规则,目前仍然会计算在内。 + +Q:开启引擎插件后,是否可以在编辑器主菜单中的 **项目 -> 项目设置 -> 功能裁剪** 中移除所有模块,减小包体?
+A:不可以,因为微信从 7.0.7 开始才支持引擎插件,如随意裁剪引擎可能导致游戏无法在低版本微信上运行。 + +Q:启用引擎插件后,在微信开发工具中提示 “代码包解包失败” 或者 “..., 登录用户不是该小程序的开发者”,但真机预览正常?
+A:构建面板中默认的 appid 为通用测试 id。根据微信的规则,如需测试引擎分离功能,需要开发者在构建面板中填入自己开通的 appid。 + +Q:启用引擎插件后,在微信开发工具中提示 “插件未授权使用 `添加插件`”?
+A:点击提示中的 `添加插件`,选择添加 CocosCreator 插件后重新编译即可。若添加插件时出现“可添加的插件信息为空”的提示,可尝试在微信开发者工具中选择 **清缓存 -> 全部清除** 后重试。 + +## 参考链接 + +- [微信小游戏引擎插件官方说明](https://developers.weixin.qq.com/minigame/dev/guide/base-ability/game-engine-plugin.html) diff --git a/versions/4.0/zh/editor/publish/wechatgame-plugin/build-options.png b/versions/4.0/zh/editor/publish/wechatgame-plugin/build-options.png new file mode 100644 index 0000000000..d80679187a Binary files /dev/null and b/versions/4.0/zh/editor/publish/wechatgame-plugin/build-options.png differ diff --git a/versions/4.0/zh/editor/publish/windows/build-example-windows.md b/versions/4.0/zh/editor/publish/windows/build-example-windows.md new file mode 100644 index 0000000000..c3fbc86c83 --- /dev/null +++ b/versions/4.0/zh/editor/publish/windows/build-example-windows.md @@ -0,0 +1,92 @@ +# Windows 构建示例 + +本文将演示将 Cocos Creator 开发的项目发布为 Windows 应用程序,需要以下准备工作: +- 一台 Windows 电脑 +- C++ 开发环境 + +## 安装 C++ 编译环境 + +Windows 下需要安装 [Visual Studio 2019/2022](https://www.visualstudio.com/downloads/download-visual-studio-vs)。 + +在安装 Visual Studio 时,请勾选 **使用 C++ 的桌面开发** 和 **使用 C++ 的游戏开发** 两个模块。 + +> **注意**:在 **使用 C++ 的游戏开发** 模块中有一个 **Cocos** 选项,请勿勾选。 + +## 发布流程 + +### 准备测试项目 + +打开一个已有的项目,或者新建一个测试项目 + +### 构建 + +![project-build-menu](./images/project-build-menu.png) + +如上图所示,在 Cocos Creator 顶部菜单中选择 **项目(Project)** -> **构建发布(Build)**,可打开下面的构建面板。 + +![build-panel-windows](./images/build-panel-windows.png) + +#### 配置通用选项 + +上图中,左边部分为 Cocos Creator 支持的所有平台都需要配置的公共参数,可以前往 [通用发布配置](./../build-options.md) 查看详情。 + +#### 配置 Windows 专有选项 + +平台(Platform)选择为 **Windows**,面板向下滑动,可以看到如右图所示的原生(Native)平台和 Windows 平台特有的配置,详情请查看 [原生平台通用构建选项](./../native-options.md) 和 [Windows 发布选项](./build-options-windows.md)。 + +#### 执行构建 + +配置完成后,点击 **构建(Build)** 按钮,即可生成 Visual Studio 的 `*.sln` 项目。 + +构建成功后,可以点击如下所示的打开文件按钮,打开生成的项目路径。 + +![build-open-path-windows](./images/build-open-path-windows.png) + +如果未更改过生成路径,可在同目录下找到 build/windows/proj 目录,内容如下所示: + +![project-folder-windows](./images/project-folder-windows.png) + +### 在 Visual Studio 中编译运行 + +双击 `build/windows/proj/项目名称.sln`,即可用 `Visual Studio` 打开项目。 + +![vs-showcase-windows](./images/vs-showcase-windows.png) + +选择如图所示的 项目中包含了 `cocos-engine` 库以及项目代码,点击编译并运行按钮,即可启动项目。 + +![run-windows](./images/run-windows.png) + +> `*.sln` 为 `Visual Studio` 项目解决方案文件,推荐使用 `Visual Studio` 打开。也可以使用 [Rider](https://www.jetbrains.com/rider/) 等支持 `*.sln` 的 IDE 打开。 + +### 修改分辨率 + +可以通过 `native/engine/common/classes/Game.cpp` 中的 `_windowInfo` 进行修改: + +默认分辨率为 800 x 600,我们以把分辨率改为 800 x 400 为例。 + +```C++ +int Game::init() { + _windowInfo.title = GAME_NAME; + // configure window size + _windowInfo.width = 800; + _windowInfo.height = 400; +} +``` + +修改完成后,在 Visual Studio 中再次编译运行,效果如下图所示: + +![run-windows-800to400](./images/run-windows-800to400.png) + +## 进阶 + +### 脚本与原生通信 + +有时候,项目需要调用一些 Windows 的系统功能,或者当集成了某个第三方的 SDK 库后,调要通过脚本代码调用其 API,此时就需要用到脚本层与原生层的通信机制。 + +JS 脚本与 C++ 通信机制请参考:[JSB 使用指南](../../../advanced-topics/JSB2.0-learning.md)。 + +### 在原生环境调试代码 + +有一些问题,只有在特定设备上才会出现,如果能够在对应设备上调试代码,就能快速定位问题,找到解决方案。 + +Cocos Creator 提供了原生调试机制,可以很方便地在设备上调试游戏代码,请参考:[原生平台 JavaScript 调试](./../debug-jsb.md)。 diff --git a/versions/4.0/zh/editor/publish/windows/build-options-windows.md b/versions/4.0/zh/editor/publish/windows/build-options-windows.md new file mode 100644 index 0000000000..1692e5d429 --- /dev/null +++ b/versions/4.0/zh/editor/publish/windows/build-options-windows.md @@ -0,0 +1,18 @@ +# Windows 平台构建选项 + +Windows 平台的构建选项包括 **渲染后端** 和 **生成平台**。 + +![build-options-windows](./images/build-options-windows.png) + +## 可执行文件名 + +用于指定应用程序主执行文件名称的字段。如果未填写,系统将根据应用名称字段生成默认值。值的设定只能包含数字、字母、下划线(_)以及减号(-)。 + +## 渲染后端(Render BackEnd) + +在 Windows 平台上,Cocos Creator 目前支持 **VULKAN**、**GLES3** 和 **GLES2** 三种**渲染后端**。默认勾选 **GLES3**,在同时勾选多个的情况下,运行时将会根据设备实际支持情况来选择使用的渲染后端。 + +## 目标平台 + +设置编译架构,目前仅支持 **x64**,只能在 **x64** 系统上运行。 + diff --git a/versions/4.0/zh/editor/publish/windows/images/build-open-path-windows.png b/versions/4.0/zh/editor/publish/windows/images/build-open-path-windows.png new file mode 100644 index 0000000000..519ddb02a4 Binary files /dev/null and b/versions/4.0/zh/editor/publish/windows/images/build-open-path-windows.png differ diff --git a/versions/4.0/zh/editor/publish/windows/images/build-options-windows.png b/versions/4.0/zh/editor/publish/windows/images/build-options-windows.png new file mode 100644 index 0000000000..06253e18ed Binary files /dev/null and b/versions/4.0/zh/editor/publish/windows/images/build-options-windows.png differ diff --git a/versions/4.0/zh/editor/publish/windows/images/build-panel-windows.png b/versions/4.0/zh/editor/publish/windows/images/build-panel-windows.png new file mode 100644 index 0000000000..aa63b2eda9 Binary files /dev/null and b/versions/4.0/zh/editor/publish/windows/images/build-panel-windows.png differ diff --git a/versions/4.0/zh/editor/publish/windows/images/project-build-menu.png b/versions/4.0/zh/editor/publish/windows/images/project-build-menu.png new file mode 100644 index 0000000000..225f5c1c1a Binary files /dev/null and b/versions/4.0/zh/editor/publish/windows/images/project-build-menu.png differ diff --git a/versions/4.0/zh/editor/publish/windows/images/project-folder-windows.png b/versions/4.0/zh/editor/publish/windows/images/project-folder-windows.png new file mode 100644 index 0000000000..d1cdaa85f2 Binary files /dev/null and b/versions/4.0/zh/editor/publish/windows/images/project-folder-windows.png differ diff --git a/versions/4.0/zh/editor/publish/windows/images/run-windows-800to400.png b/versions/4.0/zh/editor/publish/windows/images/run-windows-800to400.png new file mode 100644 index 0000000000..352915cb54 Binary files /dev/null and b/versions/4.0/zh/editor/publish/windows/images/run-windows-800to400.png differ diff --git a/versions/4.0/zh/editor/publish/windows/images/run-windows.png b/versions/4.0/zh/editor/publish/windows/images/run-windows.png new file mode 100644 index 0000000000..acb3fc3312 Binary files /dev/null and b/versions/4.0/zh/editor/publish/windows/images/run-windows.png differ diff --git a/versions/4.0/zh/editor/publish/windows/images/vs-showcase-windows.png b/versions/4.0/zh/editor/publish/windows/images/vs-showcase-windows.png new file mode 100644 index 0000000000..55f9fe6850 Binary files /dev/null and b/versions/4.0/zh/editor/publish/windows/images/vs-showcase-windows.png differ diff --git a/versions/4.0/zh/editor/publish/windows/index.md b/versions/4.0/zh/editor/publish/windows/index.md new file mode 100644 index 0000000000..79fc9831cf --- /dev/null +++ b/versions/4.0/zh/editor/publish/windows/index.md @@ -0,0 +1,18 @@ +# Windows 构建指引 + +Cocos Creator 支持发布 Windows 桌面应用程序,但至少需要一台支持 Windows 的设备。 + +## 主要内容 + +- [Windows 构建示例](./build-example-windows.md) +- [Windows 构建选项介绍](build-options-windows.md) + +## 相关阅读 + +- [安装配置原生开发环境](../setup-native-development.md) +- [构建发布面板](../build-panel.md) +- [通用构建选项介绍](../build-options.md) +- [原生平台通用构建选项](../native-options.md) +- [原生平台 JavaScript 调试](../debug-jsb.md) +- [构建流程简介与常见错误处理](../build-guide.md) +- [原生平台二次开发指南](../../../advanced-topics/native-secondary-development.md) diff --git a/versions/4.0/zh/editor/rendering/lod.md b/versions/4.0/zh/editor/rendering/lod.md new file mode 100644 index 0000000000..8bc096c176 --- /dev/null +++ b/versions/4.0/zh/editor/rendering/lod.md @@ -0,0 +1,139 @@ +# 多层次细节 + +多层次细节(Level Of Details 以下简称 LOD)是大场景开发中常用的一种优化方式。LOD 的核心在于对于远处或者不重要的物体,降低其显示细节,以达到提升渲染效率的目的。 + +通常的 LOD 的做法是对于某些离屏幕较远或者不重要的物体,使用低模进行代替。 + +在引擎中,如果要启用 LOD,可以在 **属性检查器** 内选择 **添加组件** 按钮并选择 **LOD Group** 组件。 + +![add lod group](lod/add-comp.png) + +添加后,场景中会显示 **剔除(`Culled`)** 或者 **LOD** 字样。 + +![enabled](lod/enabled.png) + +## 属性 + +![property](lod/property.png) + +LOD Group 组件的属性描述如下: + +| 属性 | 说明 | +| :-- | :-- | +| **Recalculate Bounds** | 重新计算包围盒
点击该按钮后,整个组的模型包围盒会重新计算
该按钮会调用 `recalculateBounds` 接口对包围盒进行重新计算,同时会修改 **Object Size** 属性 | +| **Object Size** | 物理包围盒大小
计算物体的包围盒并取 X、Y、Z 中值最大的轴
点击右侧的 **Reset Object Size** 按钮会将此属性重设为 1 | +| **LOD(0 ~ 3)** | 多层次配置,点击左侧的 **>** 符号可以展开进行配置,展开后的内容请参考下图。
LOD0 表示最丰富的显示细节
LOD2 显示低模
当物体在屏幕中的屏占比小于 LOD2 的 **Screen Ratio** 值时,物体将被剔除 | + +点击属性检查器上任意 **> LOD(0 ~ 3)** 展开后会显示如下图示: + +![lod0](lod/lod0.png) + +其属性及描述如下: + +| 属性 | 说明 | +| :-- | :-- | +| **Screen Ratio(%)** | 最小屏占比,取值范围 [0, 1]。
开发者可以在后方的输入框内输入自定义的屏占比,也可以通过 **Apply Current Screen Ratio** 自动计算屏占比
当屏占比低于当前配置的值时,LOD 会自动向下个层次切换;如没有更多层次,则物体会被剔除 | +| **Apply Current Screen Ratio** | 应用当前屏占比
按下后,LOD Group 组件会将当前显示状态的屏占比作为 **Screen Ratio(%)** 的值 | +| **Mesh Renderers** | 网格渲染器
请参考下方文档以获取更详细的信息 | +| **0 Triangle - 0 Sub Mesh(es)** | 该属性显示了当前层次所配置的三角面的数量以及其子网格的数量 | + +### 网格渲染器属性 + +网格渲染器可以通过点击下方的 ![+](lod/+.png) 以及 ![-](lod/-.png) 按钮进行添加或者删除操作。 + +添加后可以通过拖拽层次管理器内带有网格渲染器的节点进行配置: + +![drag mesh](load/../lod/drag-mesh.gif) + +## LOD + +将鼠标放在 LOD 的右侧显示网格信息处可以显示添加或删除层次按钮。 + +![add-remove-level](lod/add-remove-level.png) + +点击 ![+](lod/+.png) 时会在数组尾部添加一个新的 LOD。 + +点击 ![-](lod/-.png) 时删除当前鼠标所指的 LOD。删除后,后续的 LOD 会自动向前位移。 + +## 预览 + +配置好 LOD 后,在场景管理器内通过鼠标滚轮即可以预览不同的层次。 + +![preview](lod/preview.gif) + +随着鼠标滚动的变化,场景相机逐渐拉远,此时 LOD 的层次会变化,如当前物体的屏占比低于 LOD2 所配置的屏占比时,场景管理系统会将其剔除(Culled)。屏幕中会显示当前该 LOD Group 的层次状态 LOD(0~3) 或 剔除。 + +## 脚本的使用 + +开发者可以通过 `getComponent` 方法获取到该组件: + +```ts +import { _decorator, Component, Node, LODGroup } from 'cc'; +const { ccclass, property, requireComponent } = _decorator; + +@ccclass('LOD') +@requireComponent(LODGroup) +export class LOD extends Component { + + lodGroup : LODGroup | null = null; + + start() { + this.lodGroup = this.node.getComponent(LODGroup); + } + + update(deltaTime: number) { + + } +} +``` + +更多 API 可参考 [LOD Group API](%__APIDOC__%/zh/class/LODGroup) + +## 美术工作流 + +在实际项目开发中,美术工作者可在 DCC 软件(Maya、3D Max、Blender)中进行减面操作,并将模型导出。 + +通常可以导出多个不同层次的模型或者在同一模型中增加不同的子网格来代表不同的 LOD 层次。 + +![export](lod/dcc-export.png) + +### 自动匹配 + +通过 DCC 软件导出的模型如果其子节点拥有多个以 **_lod + 数字** 作为结尾时,LOD Group 组件会自动匹配这些子网格或网格渲染器。 + +#### 自动匹配示例 + +请确保已按照上述的规则导出相应的模型。 + +将准备好的 FBX 或者 glTF 文件导入到引擎的 **资源管理器** 内: + +![import-asset](lod/import-asset.png) + +此时资源导入系统会自动识别这些节点并启用 LOD Group 进行自动匹配: + +![auto-match](lod/auto-match.png) + +#### 自动 LOD + +自 v3.8 起,我们优化了模型导入时的 LOD 功能,如模型未带有 LOD 信息,可以在网格的 **属性检查器** 上的 Model 分页,找到并编辑模型的 LOD,勾选 LOD 的单选框则可以启用自动 LOD。 + +![model.png](./lod/model.png) + +鼠标移动到层级上可以点击 ![-](./lod/+.png)/![-](./lod/-.png) 来增加和减少层级。 + +![hover.png](./lod/hover.png) + +- Face count(%):面数比例,优化后的网格的三角面数量除以原始网格面数得到的比例。如 25% 则会优化掉原始模型的 75% 的面数。 +- Screen Ratio:最小屏占比,和 LOD 组件的 Screen Ratio 属性一致,请查看上文。 + +每个层级和 LOD 组件的设置类似,可以修改不同层级的面数占比。 + +## Q&A + +- 我是否可以添加多个 LOD Group + + 可以。他们之间会工作良好。 + +- 我能否在 2D 场景内使用 LOD + + 由于 LOD Group 组件只会识别 MeshRenderer 而 2D 或者 UI 组件通常没有 MeshRenderer,因此这样做并没有什么意义。 diff --git a/versions/4.0/zh/editor/rendering/lod/+.png b/versions/4.0/zh/editor/rendering/lod/+.png new file mode 100644 index 0000000000..eb5321a9ba Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/+.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/-.png b/versions/4.0/zh/editor/rendering/lod/-.png new file mode 100644 index 0000000000..197fef21f9 Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/-.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/add-comp.png b/versions/4.0/zh/editor/rendering/lod/add-comp.png new file mode 100644 index 0000000000..564267d0ad Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/add-comp.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/add-remove-level.png b/versions/4.0/zh/editor/rendering/lod/add-remove-level.png new file mode 100644 index 0000000000..bc75de91e0 Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/add-remove-level.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/auto-match.png b/versions/4.0/zh/editor/rendering/lod/auto-match.png new file mode 100644 index 0000000000..2fc09c02c8 Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/auto-match.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/dcc-export.png b/versions/4.0/zh/editor/rendering/lod/dcc-export.png new file mode 100644 index 0000000000..ab6024b2f2 Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/dcc-export.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/drag-mesh.gif b/versions/4.0/zh/editor/rendering/lod/drag-mesh.gif new file mode 100644 index 0000000000..a49516d4ac Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/drag-mesh.gif differ diff --git a/versions/4.0/zh/editor/rendering/lod/enabled.png b/versions/4.0/zh/editor/rendering/lod/enabled.png new file mode 100644 index 0000000000..82515eaab9 Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/enabled.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/hover.png b/versions/4.0/zh/editor/rendering/lod/hover.png new file mode 100644 index 0000000000..66272d83da Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/hover.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/import-asset.png b/versions/4.0/zh/editor/rendering/lod/import-asset.png new file mode 100644 index 0000000000..bf1b14fbc0 Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/import-asset.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/lod0.png b/versions/4.0/zh/editor/rendering/lod/lod0.png new file mode 100644 index 0000000000..135bb35848 Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/lod0.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/mesh-renderers.png b/versions/4.0/zh/editor/rendering/lod/mesh-renderers.png new file mode 100644 index 0000000000..838b1ce296 Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/mesh-renderers.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/model.png b/versions/4.0/zh/editor/rendering/lod/model.png new file mode 100644 index 0000000000..06e7a51b1b Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/model.png differ diff --git a/versions/4.0/zh/editor/rendering/lod/preview.gif b/versions/4.0/zh/editor/rendering/lod/preview.gif new file mode 100644 index 0000000000..054a1e8d7f Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/preview.gif differ diff --git a/versions/4.0/zh/editor/rendering/lod/property.png b/versions/4.0/zh/editor/rendering/lod/property.png new file mode 100644 index 0000000000..0a88e716af Binary files /dev/null and b/versions/4.0/zh/editor/rendering/lod/property.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/2d.png b/versions/4.0/zh/editor/scene/bar_img/2d.png new file mode 100644 index 0000000000..84cc636e1b Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/2d.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/3d.png b/versions/4.0/zh/editor/scene/bar_img/3d.png new file mode 100644 index 0000000000..ee60de495a Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/3d.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/align-bar.png b/versions/4.0/zh/editor/scene/bar_img/align-bar.png new file mode 100644 index 0000000000..66ccd1a4ff Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/align-bar.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/align.png b/versions/4.0/zh/editor/scene/bar_img/align.png new file mode 100644 index 0000000000..b197746bfd Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/align.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/all-use-x.png b/versions/4.0/zh/editor/scene/bar_img/all-use-x.png new file mode 100644 index 0000000000..46094417cf Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/all-use-x.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/anchor-or-center.png b/versions/4.0/zh/editor/scene/bar_img/anchor-or-center.png new file mode 100644 index 0000000000..9903360e0e Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/anchor-or-center.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/center.png b/versions/4.0/zh/editor/scene/bar_img/center.png new file mode 100644 index 0000000000..dd020cc8ec Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/center.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/desig-resolution.png b/versions/4.0/zh/editor/scene/bar_img/desig-resolution.png new file mode 100644 index 0000000000..7ff551fe0e Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/desig-resolution.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/distribute.png b/versions/4.0/zh/editor/scene/bar_img/distribute.png new file mode 100644 index 0000000000..3b3e9fed63 Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/distribute.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/increase-snap-config.png b/versions/4.0/zh/editor/scene/bar_img/increase-snap-config.png new file mode 100644 index 0000000000..1b8ce6eb2f Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/increase-snap-config.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/light.png b/versions/4.0/zh/editor/scene/bar_img/light.png new file mode 100644 index 0000000000..b4185acb22 Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/light.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/local.png b/versions/4.0/zh/editor/scene/bar_img/local.png new file mode 100644 index 0000000000..5c9dca142c Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/local.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/moveing.png b/versions/4.0/zh/editor/scene/bar_img/moveing.png new file mode 100644 index 0000000000..02d58a2a35 Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/moveing.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/rect.png b/versions/4.0/zh/editor/scene/bar_img/rect.png new file mode 100644 index 0000000000..b9ffec6a63 Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/rect.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/rotating.png b/versions/4.0/zh/editor/scene/bar_img/rotating.png new file mode 100644 index 0000000000..0d12b188a9 Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/rotating.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/rotation.png b/versions/4.0/zh/editor/scene/bar_img/rotation.png new file mode 100644 index 0000000000..bfb625cf85 Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/rotation.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/scale.png b/versions/4.0/zh/editor/scene/bar_img/scale.png new file mode 100644 index 0000000000..779187653b Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/scale.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/scaling.png b/versions/4.0/zh/editor/scene/bar_img/scaling.png new file mode 100644 index 0000000000..658e8f9d7b Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/scaling.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/shaded.png b/versions/4.0/zh/editor/scene/bar_img/shaded.png new file mode 100644 index 0000000000..66248a09fb Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/shaded.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/translate.png b/versions/4.0/zh/editor/scene/bar_img/translate.png new file mode 100644 index 0000000000..d20ca60753 Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/translate.png differ diff --git a/versions/4.0/zh/editor/scene/bar_img/world-or-local.jpg b/versions/4.0/zh/editor/scene/bar_img/world-or-local.jpg new file mode 100644 index 0000000000..fa42d1277b Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/world-or-local.jpg differ diff --git a/versions/4.0/zh/editor/scene/bar_img/world-or-local.png b/versions/4.0/zh/editor/scene/bar_img/world-or-local.png new file mode 100644 index 0000000000..5cf195b379 Binary files /dev/null and b/versions/4.0/zh/editor/scene/bar_img/world-or-local.png differ diff --git a/versions/4.0/zh/editor/scene/camera-gizmo.md b/versions/4.0/zh/editor/scene/camera-gizmo.md new file mode 100644 index 0000000000..dc7fff2f54 --- /dev/null +++ b/versions/4.0/zh/editor/scene/camera-gizmo.md @@ -0,0 +1,15 @@ +# 摄像机 Gizmo + +**摄像机 Gizmo** 用于显示摄像机的裁剪范围,摄像机相关的信息可以查看 [摄像机介绍](../components/camera-component.md)。 + +## 透视像机(Perspective) + +**透视摄像机 Gizmo** 显示了由远近裁剪面和 FOV 构成的四棱台图形。可以使用方块控制点来编辑像机的远、近平面距离,FOV。 + +![camera perspective gizmo](images/camera-perspective-gizmo.png) + +## 正交像机(Ortho) + +**正交摄像机 Gizmo** 显示了由远近裁剪面、正交高度,所构成的长方体图形。可以使用方块控制点来编辑像机的远、近平面距离,正交高度。 + +![camera ortho gizmo](images/camera-ortho-gizmo.png) diff --git a/versions/4.0/zh/editor/scene/collider-gizmo.md b/versions/4.0/zh/editor/scene/collider-gizmo.md new file mode 100644 index 0000000000..1ce221e158 --- /dev/null +++ b/versions/4.0/zh/editor/scene/collider-gizmo.md @@ -0,0 +1,11 @@ +# 碰撞器 Gizmo + +有关 Cocos Creator 的碰撞器信息可以查看:[碰撞组件与基础属性](../../physics/physics-collider.md) + +碰撞器的 Gizmo 用于在编辑器内展示碰撞器的位置和大小,在 **层级管理器** 内选中任意带有碰撞器组件的节点,可在场景窗口内观察到其 Gizmo: + +![box collider gizmo](images/box-collider-gizmo.png) + +对于所有碰撞器来说,通过鼠标选中并拖动这些方块,可以调整碰撞器的大小。下图以 **盒碰撞器 Gizmo** 为例,展示如何调整碰撞器: + +![edit](images/edit-collider.gif) diff --git a/versions/4.0/zh/editor/scene/images/auto-snapping.gif b/versions/4.0/zh/editor/scene/images/auto-snapping.gif new file mode 100644 index 0000000000..60bc7c28be Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/auto-snapping.gif differ diff --git a/versions/4.0/zh/editor/scene/images/bar.png b/versions/4.0/zh/editor/scene/images/bar.png new file mode 100644 index 0000000000..9ef7fed5ba Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/bar.png differ diff --git a/versions/4.0/zh/editor/scene/images/bar2d.png b/versions/4.0/zh/editor/scene/images/bar2d.png new file mode 100644 index 0000000000..819d072ea8 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/bar2d.png differ diff --git a/versions/4.0/zh/editor/scene/images/box-collider-gizmo.png b/versions/4.0/zh/editor/scene/images/box-collider-gizmo.png new file mode 100644 index 0000000000..f0e3fc021f Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/box-collider-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/camera-ortho-gizmo.png b/versions/4.0/zh/editor/scene/images/camera-ortho-gizmo.png new file mode 100644 index 0000000000..7b45da27de Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/camera-ortho-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/camera-perspective-gizmo.png b/versions/4.0/zh/editor/scene/images/camera-perspective-gizmo.png new file mode 100644 index 0000000000..3cfed133f5 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/camera-perspective-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/debug-preview.png b/versions/4.0/zh/editor/scene/images/debug-preview.png new file mode 100644 index 0000000000..252f6f32a3 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/debug-preview.png differ diff --git a/versions/4.0/zh/editor/scene/images/edit-collider.gif b/versions/4.0/zh/editor/scene/images/edit-collider.gif new file mode 100644 index 0000000000..4bac5bb1fd Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/edit-collider.gif differ diff --git a/versions/4.0/zh/editor/scene/images/edit-resolution.png b/versions/4.0/zh/editor/scene/images/edit-resolution.png new file mode 100644 index 0000000000..21bfc1daca Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/edit-resolution.png differ diff --git a/versions/4.0/zh/editor/scene/images/enable-snaping-rotating copy.png b/versions/4.0/zh/editor/scene/images/enable-snaping-rotating copy.png new file mode 100644 index 0000000000..a636c9b3b6 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/enable-snaping-rotating copy.png differ diff --git a/versions/4.0/zh/editor/scene/images/enable-snaping-rotating.gif b/versions/4.0/zh/editor/scene/images/enable-snaping-rotating.gif new file mode 100644 index 0000000000..a4580d5321 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/enable-snaping-rotating.gif differ diff --git a/versions/4.0/zh/editor/scene/images/enable-snaping-rotating.png b/versions/4.0/zh/editor/scene/images/enable-snaping-rotating.png new file mode 100644 index 0000000000..a636c9b3b6 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/enable-snaping-rotating.png differ diff --git a/versions/4.0/zh/editor/scene/images/global-rotation.png b/versions/4.0/zh/editor/scene/images/global-rotation.png new file mode 100644 index 0000000000..576a99543d Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/global-rotation.png differ diff --git a/versions/4.0/zh/editor/scene/images/increase-snap-conf.png b/versions/4.0/zh/editor/scene/images/increase-snap-conf.png new file mode 100644 index 0000000000..bed3de572d Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/increase-snap-conf.png differ diff --git a/versions/4.0/zh/editor/scene/images/light-off.png b/versions/4.0/zh/editor/scene/images/light-off.png new file mode 100644 index 0000000000..e6e5f93211 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/light-off.png differ diff --git a/versions/4.0/zh/editor/scene/images/light-on.png b/versions/4.0/zh/editor/scene/images/light-on.png new file mode 100644 index 0000000000..f87cfb06e4 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/light-on.png differ diff --git a/versions/4.0/zh/editor/scene/images/local-rotation.png b/versions/4.0/zh/editor/scene/images/local-rotation.png new file mode 100644 index 0000000000..66d69c05f0 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/local-rotation.png differ diff --git a/versions/4.0/zh/editor/scene/images/multiple-select.gif b/versions/4.0/zh/editor/scene/images/multiple-select.gif new file mode 100644 index 0000000000..7f9642f752 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/multiple-select.gif differ diff --git a/versions/4.0/zh/editor/scene/images/particle-box-gizmo.png b/versions/4.0/zh/editor/scene/images/particle-box-gizmo.png new file mode 100644 index 0000000000..94832637c9 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/particle-box-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/particle-circle-gizmo.png b/versions/4.0/zh/editor/scene/images/particle-circle-gizmo.png new file mode 100644 index 0000000000..3a39cd2ab0 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/particle-circle-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/particle-cone-gizmo.png b/versions/4.0/zh/editor/scene/images/particle-cone-gizmo.png new file mode 100644 index 0000000000..86fa6f8650 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/particle-cone-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/particle-hemisphere-gizmo.png b/versions/4.0/zh/editor/scene/images/particle-hemisphere-gizmo.png new file mode 100644 index 0000000000..bf106f3dd4 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/particle-hemisphere-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/particle-sphere-gizmo.png b/versions/4.0/zh/editor/scene/images/particle-sphere-gizmo.png new file mode 100644 index 0000000000..7f86d8ed2e Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/particle-sphere-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/position-gizmo.png b/versions/4.0/zh/editor/scene/images/position-gizmo.png new file mode 100644 index 0000000000..c9f0b9d98f Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/position-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/rect-gizmo.png b/versions/4.0/zh/editor/scene/images/rect-gizmo.png new file mode 100644 index 0000000000..8b23b7c45f Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/rect-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/rect-tool-snap-config.png b/versions/4.0/zh/editor/scene/images/rect-tool-snap-config.png new file mode 100644 index 0000000000..83d1192eae Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/rect-tool-snap-config.png differ diff --git a/versions/4.0/zh/editor/scene/images/reference.png b/versions/4.0/zh/editor/scene/images/reference.png new file mode 100644 index 0000000000..b0da445ca9 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/reference.png differ diff --git a/versions/4.0/zh/editor/scene/images/remove-light.png b/versions/4.0/zh/editor/scene/images/remove-light.png new file mode 100644 index 0000000000..e75dc8473c Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/remove-light.png differ diff --git a/versions/4.0/zh/editor/scene/images/rotation-gizmo.png b/versions/4.0/zh/editor/scene/images/rotation-gizmo.png new file mode 100644 index 0000000000..87ccdda345 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/rotation-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/rotation.png b/versions/4.0/zh/editor/scene/images/rotation.png new file mode 100644 index 0000000000..7573ea4848 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/rotation.png differ diff --git a/versions/4.0/zh/editor/scene/images/scale-gizmo.png b/versions/4.0/zh/editor/scene/images/scale-gizmo.png new file mode 100644 index 0000000000..3e3086d9c0 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/scale-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/scene-camera.png b/versions/4.0/zh/editor/scene/images/scene-camera.png new file mode 100644 index 0000000000..aec1780690 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/scene-camera.png differ diff --git a/versions/4.0/zh/editor/scene/images/scene-gizmo.png b/versions/4.0/zh/editor/scene/images/scene-gizmo.png new file mode 100644 index 0000000000..a02467b623 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/scene-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/scene-grid.png b/versions/4.0/zh/editor/scene/images/scene-grid.png new file mode 100644 index 0000000000..96a5af68d9 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/scene-grid.png differ diff --git a/versions/4.0/zh/editor/scene/images/scene-light.png b/versions/4.0/zh/editor/scene/images/scene-light.png new file mode 100644 index 0000000000..c88b9f2b5b Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/scene-light.png differ diff --git a/versions/4.0/zh/editor/scene/images/scene-reference.png b/versions/4.0/zh/editor/scene/images/scene-reference.png new file mode 100644 index 0000000000..01d84a02bf Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/scene-reference.png differ diff --git a/versions/4.0/zh/editor/scene/images/scene-resolution.png b/versions/4.0/zh/editor/scene/images/scene-resolution.png new file mode 100644 index 0000000000..b09b353749 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/scene-resolution.png differ diff --git a/versions/4.0/zh/editor/scene/images/scene.png b/versions/4.0/zh/editor/scene/images/scene.png new file mode 100644 index 0000000000..46d8f00f7e Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/scene.png differ diff --git a/versions/4.0/zh/editor/scene/images/set-camera.png b/versions/4.0/zh/editor/scene/images/set-camera.png new file mode 100644 index 0000000000..9c1badaa89 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/set-camera.png differ diff --git a/versions/4.0/zh/editor/scene/images/shaded-config.png b/versions/4.0/zh/editor/scene/images/shaded-config.png new file mode 100644 index 0000000000..2d9d9abe61 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/shaded-config.png differ diff --git a/versions/4.0/zh/editor/scene/images/shaded-debug1.png b/versions/4.0/zh/editor/scene/images/shaded-debug1.png new file mode 100644 index 0000000000..4df41fbf90 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/shaded-debug1.png differ diff --git a/versions/4.0/zh/editor/scene/images/shaded-debug2.png b/versions/4.0/zh/editor/scene/images/shaded-debug2.png new file mode 100644 index 0000000000..752d63b8a4 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/shaded-debug2.png differ diff --git a/versions/4.0/zh/editor/scene/images/shaded-debug3.png b/versions/4.0/zh/editor/scene/images/shaded-debug3.png new file mode 100644 index 0000000000..e725095e97 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/shaded-debug3.png differ diff --git a/versions/4.0/zh/editor/scene/images/snaping-surface.gif b/versions/4.0/zh/editor/scene/images/snaping-surface.gif new file mode 100644 index 0000000000..4cc2d61a29 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/snaping-surface.gif differ diff --git a/versions/4.0/zh/editor/scene/images/sphere-collider-gizmo.png b/versions/4.0/zh/editor/scene/images/sphere-collider-gizmo.png new file mode 100644 index 0000000000..5c86b3a974 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/sphere-collider-gizmo.png differ diff --git a/versions/4.0/zh/editor/scene/images/transform-bar.png b/versions/4.0/zh/editor/scene/images/transform-bar.png new file mode 100644 index 0000000000..ad9597cc27 Binary files /dev/null and b/versions/4.0/zh/editor/scene/images/transform-bar.png differ diff --git a/versions/4.0/zh/editor/scene/index.md b/versions/4.0/zh/editor/scene/index.md new file mode 100644 index 0000000000..d979e57672 --- /dev/null +++ b/versions/4.0/zh/editor/scene/index.md @@ -0,0 +1,321 @@ +# 场景编辑器 + +![scene](images/scene.png) + +**场景编辑器** 是内容创作的核心工作区域,用于选择和摆放场景图像、角色、特效、UI 等各类游戏元素。在这个工作区域内可以选中并通过 **变换工具** 修改节点的位置、旋转和缩放等属性,并可以获得所见即所得的场景效果预览。 + +## 菜单介绍 + +场景顶部为菜单栏,在切换 3D 和 2D 视图后会略有不同,请参考下文。 + +- 3D: + + ![bar](images/bar.png) + +- 2D: + + ![bar](images/bar2d.png) + +## 视图介绍 + +**场景编辑器** 包括 **3D** 和 **2D** 两种视图,3D 视图用于 3D 场景编辑,2D 视图则主要用于 UI 节点等 2D 元素的编辑。 + +- ![3d](bar_img/3d.png) 表明当前是 **3D** 视图模式,此时视图的相机为透视类型。点击后可以进入 **2D** 视图。 +- ![2d](bar_img/2d.png) 表明当前为 **2D** 视图模式,此时的视图相机为正交类型,再次点击后会回到 **3D** 视图。 + +### 3D 视图 + +在 3D 视图下,可以通过以下操作来移动和定位 **场景编辑器** 的视图: +- 鼠标左键 + Alt:以视图中心点为中心旋转。 +- 鼠标中键:平移视图。 +- 空格键 + 鼠标/触摸板拖拽:平移视图。 +- 鼠标滚轮:以视图中心点为中心缩放视图。 +- 鼠标右键 + WASD:摄像机漫游。 +- **F** 快捷键:摄像机聚焦到当前选中节点。 + +### 2D 视图 + +在 2D 视图下,可以通过以下操作来移动和定位 **场景编辑器** 的视图: +- 鼠标中键:平移视图。 +- 鼠标滚轮:以当前鼠标悬停位置为中心缩放视图。 +- 鼠标右键:平移视图。 +- **F** 快捷键:摄像机聚焦到当前选中节点。 + +## 变换工具 + +![transform](images/transform-bar.png) + +上述工具为变换工具组,负责节点的 **平移、缩放、旋转、吸附、锚点/中心、本地/世界坐标系** 等功能。 + +- ![3d](bar_img/translate.png) 切换当前场景内变换工具为 **位移变换工具**,快捷键为 W + + **移动变换工具** 是打开编辑器时默认处于激活状态的变换工具。 + + 选中任意节点,便能在 **场景编辑器** 中看到节点中心出现了由红绿蓝三个箭头和红绿蓝三个方块组成的移动控制手柄。 + + **控制手柄** 是指在特定编辑状态下,**场景编辑器** 中显示的可用鼠标进行交互操作的控制器。这些控制器只用来辅助编辑,不会在游戏运行时显示。 + + ![position gizmo](./images/position-gizmo.png) + + 移动变换工具激活时: + - 按住红色/绿色/蓝色箭头拖拽鼠标,将分别在 X、Y、Z 轴方向上移动节点; + - 按住红色/绿色/蓝色方块拖拽鼠标,将分别在 Y-Z 平面、X-Z 平面、X-Y 平面上移动节点。 + +- ![3d](bar_img/rotation.png) 切换当前场景内变换工具为 **旋转变换工具**,快捷键为 E + + 旋转变换工具的手柄由三个相互正交的红绿蓝圆环组成(2D 视图下由一个箭头和一个圆环组成)。按住红色/绿色/蓝色圆环任意一点拖拽鼠标时,节点将分别绕着 X、Y、Z 轴旋转。 + + ![rotation gizmo](./images/rotation-gizmo.png) + + 当鼠标悬浮在任意圆环上时,圆环显示为黄色,点击即可选中圆环,同时还会显示一个黄色的箭头,表示当前节点是以哪个轴为中心进行旋转。拖拽圆环上任意一点即可旋转节点,放开鼠标之前,可以在控制手柄上看到旋转的角度大小。 + + ![rotation](./images/rotation.png) + +- ![3d](bar_img/scale.png) 切换当前场景内变换工具为 **缩放变换工具**,快捷键为 R + + 缩放变换工具由三个头部是红绿蓝正方体的坐标轴以及中心一个灰色正方体组成。当鼠标悬浮在任一正方体上时,显示为黄色,点击即可选中并拖动: + + - 按住红色/绿色/蓝色正方体拖拽鼠标,将分别在 X、Y、Z 轴方向上缩放节点; + - 按住灰色正方体拖拽鼠标,将同时在 X、Y、Z 轴方向上缩放节点。 + + ![scale gizmo](./images/scale-gizmo.png) + +- ![3d](bar_img/rect.png) 切换当前场景内变换工具为 **矩形变换工具**,快捷键为 T + + 需要注意的是,矩形变换工具只适用于 UI 节点。 + + 矩形变换工具由四个顶点控制点、四个边控制点、一个中心控制点组成。 + + ![rect gizmo](./images/rect-gizmo.png) + + 矩形变换工具激活时: + - 拖拽控制手柄的任一顶点控制点,可以在保持对角顶点位置不变的情况下,同时修改 UI 节点的 `Position` 属性和 UITransform 组件中的 `ContentSize` 属性。 + - 拖拽控制手柄的任一边控制点,可以在保持对边位置不变的情况下,修改 UI 节点的 `Position`(`X` 或 `Y` 属性)和 UITransform 组件中的 `ContentSize` 属性(`width` 或 `height` 属性)。 + - 拖拽控制手柄的中心控制点,可以在 UI 节点的尺寸大小不变的情况下,同时修改 UI 节点的 `Position` 属性和 UITransform 组件中的 `AnchorPoint`(锚点)属性。 + + 在 UI 元素的排版中,经常会需要使用 **矩形变换工具** 直接精确控制节点四条边的位置和长度。而对于必须保持原始图片宽高比的图像元素,通常不会使用矩形变换工具来调整尺寸。 + +- ![3d](bar_img/increase-snap-config.png) **增量吸附工具**,点击该按钮后会出现 **增量吸附配置**,详情请参考下方 **增量吸附工具** 部分 +- ![3d](bar_img/anchor-or-center.png) **变换工具位置**,用于修改变换是来源于对象的 **锚点** 还是 **中心** + + - ![3d](bar_img/anchor-or-center.png) Pivot:变换工具将显示在 2D 对象 **锚点**(`AnchorPoint`)所在位置或 3D 对象的 **世界坐标系** 中 + - ![anchor](bar_img/center.png) Center:变换工具将显示在节点的中心点位置。若同时选择多个节点,则显示在所有节点的中心位置 + +- ![3d](bar_img/world-or-local.png) **变换工具的控制手柄方向**,切换变换工具手柄是 **本地** 坐标系或 **世界** 坐标系 + + - ![local](bar_img/local.png) Local:变换工具的控制手柄以节点的旋转方向为准,如下图: + + ![local-rotation](./images/local-rotation.png) + + - ![global](bar_img/world-or-local.jpg) Global:变换工具中控制手柄的方向以世界坐标系为准,不受节点旋转的影响,如下图: + + ![global-rotation](./images/global-rotation.png) + +## 增量吸附工具 + +![icon](bar_img/increase-snap-config.png) 增量吸附工具主要包含 **增量吸附配置** 和 **矩形工具吸附配置**。 + +### 增量吸附配置 + +增量吸附功能可用于在 **场景编辑器** 中使用移动/旋转/缩放变换工具时按照 **设定的步长** 对节点进行操作。可通过以下两种方式触发变换吸附功能: + +1. 在使用变换工具的同时按住 Ctrl/Command 键即可触发变换吸附功能。 +2. 在变换吸附配置面板通过按钮开启对应变换工具的自动吸附功能,详情请参考下文介绍。 + +该功能仅在 3D 视图下生效。 + +![increase snap configuration](images/increase-snap-conf.png) + +| 按钮 | 功能说明 | +| :-- | :-- | +| ![position snap](bar_img/moveing.png) | 用于设置是否在使用 **移动变换工具** 时开启自动吸附功能。X、Y、Z 分别用于设置 X、Y、Z 轴上的移动步长,默认 X、Y、Z 统一使用 X 的值,也可以点击 ![position snap](bar_img/all-use-x.png) 按钮分别设置各个轴的步长。 | +| ![rotation snap](bar_img/rotating.png) | 用于设置是否在使用 **旋转变换工具** 时开启自动吸附功能。右侧的方框用于设置旋转步长,默认为 1。 | +| ![scale snap](bar_img/scaling.png) | 用于设置是否在使用 **缩放变换工具** 时开启自动吸附功能。右侧的方框用于设置缩放步长,默认为 1。
同时缩放时,会影响缩放的倍数而非绝对值 | + +如下图所示,启用旋转吸附后,每次节点只能旋转固定的值: + +![snap rotating](images/enable-snaping-rotating.gif) + +### 矩形工具吸附配置 + +![rect tool config](images/rect-tool-snap-config.png) + +使用 **矩形变换工具** 时,默认会开启智能参考线对齐功能。在场景中拖动 UI 元素移动时,遇到可以对齐的元素,会显示对齐参考线并自动吸附到参考线位置上。 + +![auto snapping](images/auto-snapping.gif) + +| 选项 | 说明 | +| :-- | :-- | +| 是否启用智能对齐 | 勾选该项即可开启智能对齐,默认开启 +| 吸附检测阈值 | 设置吸附检测的阈值(世界空间单位) + +## 对齐和分布 + +该工具为在 2D 视图独有工具,用于 2D/UI 多节点的对齐和分布功能。 + +![align](bar_img/align-bar.png) + +- 对齐:对齐选中的节点,至少选中需要两个节点才会生效 + + ![align](bar_img/align.png) + + 从左往右依次为: + + - 顶对齐 + - 垂直居中 + - 底对齐 + - 左对齐 + - 水平居中对齐 + - 右对齐 + + 对齐逻辑描述如下:选中所有的节点后,会根据当前非子节点(如果选中节点中有父子关系,那只考虑父节点)的位置,算出这些节点的 AABB,当点击底部对齐时,则将所有的节点都放到底部;当点击左对齐时,则把所有的节点对齐到最左边; + +- 分布:用于均匀分布多个选中的节点,至少需要三个节点才会生效 + + ![distribute](bar_img/distribute.png) + + 从左往右依次为: + + - 按顶分布 + - 按垂直居中分布 + - 按底分布 + - 按左分布 + - 按水平居中分布 + - 按右分布 + +## 吸附操作 + +在 3D 视图中,通过吸附可以更准确的对场景内的物体进行编辑,目前引擎新增两种吸附模式 **顶点吸附** 和 **表面吸附**。 + +- **顶点吸附**:顶点吸附的主要作用是更精准的通过一个物体上的顶点,对齐到另一物体的某顶点。快捷键 v +- **表面吸附**:对于有碰撞体的物体,表面吸附会吸附在碰撞体的表面;而对于没有碰撞体的物体,会吸附在物体表面;快捷键 Ctrl/Command + Shift + +操作流程: +- 在 **层级管理器** 中选中要移动的模型模型 +- 在场景内按下相应的快捷键,模型下方会出现白色控制块 +- 按住鼠标左键拖动控制块移动模型 +- 移动鼠标,移动过程中,物体会根据吸附操作的类型不同而移动 +- 松开鼠标左键确定物体位置 + +![snaping-surface](images/snaping-surface.gif) + +## 绘制模式 + +![shaded](bar_img/shaded.png) 主要用于对场景的绘制方式进行配置。点击后会显示着色配置窗口。 + +![shaded-config](images/shaded-config.png) + +支持基础绘制模式(Shaded)和 **渲染调试** 两种模式。 + +在默认情况下,采用基础绘制模式模式。如需对场景进行调试,可以将鼠标滑到 **渲染调试** 菜单(目前只支持 [表面着色器(Surface Shader)](../../shader/surface-shader.md))选择不同的调试模式。 + +![images](images/shaded-debug1.png)![images](images/shaded-debug2.png)![images](images/shaded-debug3.png) + +以 **世界空间顶点法线** 为例,演示 **渲染调试** 的用法: + +![debug preview](images/debug-preview.png) + +- 在材质 **属性检查器** 的 **着色器** 属性中选择引擎内置的 Surfaces/standard 着色器 +- 在 **渲染调试** 下拉菜单内选择需要调试的项目 +- 观察视图内的渲染结果是否符合预期 + +## 渲染输出目标分辨率设置 + +![resolution](bar_img/desig-resolution.png) 可以根据需要选择场景相机的渲染输出目标分辨率,会影响场景相机的可视范围,方便跟最终预览时选择的分辨率有相似的显示效果: + +![scene resolution](images/scene-resolution.png) + +可在顶部菜单栏的 **偏好设置** 的 [设备管理器](../preferences/index.md#%E8%AE%BE%E5%A4%87%E7%AE%A1%E7%90%86%E5%99%A8) 中添加/修改/删除分辨率。 + +下拉到最下方也可以编辑分辨率: + +![edit resolution](images/edit-resolution.png) + +## 场景灯光设置 + +![light](bar_img/light.png) 按钮主要用于设置在进行场景编辑时是否使用场景灯光,默认使用。 + +若按钮显示为蓝色,表示使用场景灯光,将使用场景中添加的灯光照亮场景,如下所示: + +![scene light on](images/light-on.png) + +当场景中没有任何添加的灯光时,场景为全黑的状态,不方便进行编辑: + +![scene light on](images/remove-light.png) + +此时便可以将按钮切换为黑色状态,表示不使用场景灯光,编辑器会自动创建一个与场景相机视角对齐的隐藏的平行光来照亮场景,如下图所示: + +![light](images/light-off.png) + +## 视图相机 + +点击 **场景编辑器** 右上角的 ![set camera](images/set-camera.png) 按钮即可设置场景相机(非用户创建的相机)的属性: + +![scene camera](images/scene-camera.png) + +| 选项 | 功能说明 | +| :-- | :-- | +| Fov | 设置场景相机的视角大小 | +| Far | 设置场景相机的远裁剪面距离 | +| Near | 设置场景相机的近裁剪面距离 | +| Color | 设置场景背景色 | +| Wheel Speed | 设置当滚动鼠标滚轮时,场景相机前后移动的速度 | +| Wander Speed | 设置场景相机漫游时的移动速度 | +| Wander Acceleration | 设置相机漫游时是否开启加速(v3.3 新增)
若勾选该项则开启加速,相机移动将会越来越快
若不勾选该项,则相机会匀速移动。| + +关于相机可视范围的说明可参考 [Camera 组件](../components/camera-component.md)。 + +## 参考图设置 + +点击 **场景编辑器** 右上角的 ![scene reference](images/scene-reference.png) 按钮,即可打开 **参考图** 面板,主要用于辅助开发人员在 **场景编辑器** 中拼接 UI 时作为对照参考: + +![reference](images/reference.png) + +| 选项 | 功能说明 | +| :-- | :-- | +| 添加 |用于添加 UI 参考图,可添加多张参考图 | +| 删除 | 用于删除当前选中框中的 UI 参考图 | +| 偏移 X | 已选中的 UI 参考图显示在场景中的 X 轴位置 | +| 偏移 Y | 已选中的 UI 参考图显示在场景中的 Y 轴位置 | +| 缩放 X | 已选中的 UI 参考图显示在场景中的 X 轴缩放 | +| 缩放 Y | 已选中的 UI 参考图显示在场景中的 Y 轴缩放 | +| 透明度 | 已选中的 UI 参考图显示在场景中的透明度 | + +### 网格设置 + +场景中的网格是我们摆放场景元素时位置的重要参考信息,可通过 **场景编辑器** 右上方的按钮设置: + +![Gizmo](images/scene-grid.png) + +| 选项 | 功能说明 | +| :-- | :-- | +| 3D 图标 | 场景是否启用 3D 图标,后面的输入框用于设置图标 Gizmo 的大小,取值范围为 0 ~ 8。
若勾选该项则表示启用 3D 图标,**场景编辑器** 中的图标 Gizmo 便会是一个 3D 面片,有近大远小的效果。
若不勾选该项,则图标 Gizmo 会显示为一个固定大小的图片。 | +| 显示网格 | 是否显示 **场景编辑器** 中的网格,后面的颜色设置框用于设置网格的颜色。 | + +## 场景 Gizmo + +场景 Gizmo 在场景视图的右上角,它显示了当前场景相机的观察方向,可以通过点击它来快速切换不同的观察角度。 + +![场景 Gizmo](images/scene-gizmo.png) + +- 点击 6 个方向轴,可以快速切换到上,下,左,右,前,后六个角度来观察场景。 +- 点击中心的立方体,可以在正交视图和透视视图间切换。 + +### 选择节点 + +在场景视图中点击鼠标左键选择物体所在节点,选择节点是使用变换工具设置节点位置、旋转、缩放等操作的前提。 + +### 复选操作 + +在场景内按下鼠标左键并拖动鼠标,会同时复选当前框选范围内的所有节点,通过变换操作,可批量对所选节点进行变换。 + +![multiple-select](images/multiple-select.gif) + +### Gizmo 操作简介 + +**场景编辑器** 的核心功能就是以所见即所得的方式编辑和布置场景中的可见元素,我们主要通过 **Gizmo** 工具来辅助完成场景的可视化编辑。 + + +- [摄像机 Gizmo](./camera-gizmo.md) +- [碰撞器 Gizmo](./collider-gizmo.md) +- [粒子系统 Gizmo](./particle-system-gizmo.md) diff --git a/versions/4.0/zh/editor/scene/particle-system-gizmo.md b/versions/4.0/zh/editor/scene/particle-system-gizmo.md new file mode 100644 index 0000000000..989b279d3f --- /dev/null +++ b/versions/4.0/zh/editor/scene/particle-system-gizmo.md @@ -0,0 +1,33 @@ +# 粒子系统 Gizmo + +粒子系统的 Gizmo 主要用于可视化粒子系统的发射器 [ShapeModule](../../particle-system/emitter.md)。 + +## 盒子(Box) + +**盒子 Gizmo** 显示了盒子的长宽高,同时可以使用方块控制点来编辑长宽高。 + +![particle box gizmo](images/particle-box-gizmo.png) + +## 球(Sphere) + +**球 Gizmo** 显示了球的大小,同时可以使用方块控制点来编辑球的半径。 + +![particle sphere gizmo](images/particle-sphere-gizmo.png) + +## 半球(Hemisphere) + +**半球 Gizmo** 显示了半球的大小,同时可以使用方块控制点来编辑半球的半径。 + +![particle hemisphere gizmo](images/particle-hemisphere-gizmo.png) + +## 圆(Circle) + +**圆 Gizmo** 显示了圆的大小,同时可以使用方块控制点来编辑圆的半径。 + +![particle circle gizmo](images/particle-circle-gizmo.png) + +## 圆锥(Cone) + +**圆锥 Gizmo** 显示了圆锥的形状,同时可以使用方块控制点来编辑圆锥的半径、角度、高度。 + +![particle cone gizmo](images/particle-cone-gizmo.png) diff --git a/versions/4.0/zh/editor/terrain/images/create-terrain-asset.png b/versions/4.0/zh/editor/terrain/images/create-terrain-asset.png new file mode 100644 index 0000000000..376f68b679 Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/create-terrain-asset.png differ diff --git a/versions/4.0/zh/editor/terrain/images/create-terrain.png b/versions/4.0/zh/editor/terrain/images/create-terrain.png new file mode 100644 index 0000000000..4f699e65ce Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/create-terrain.png differ diff --git a/versions/4.0/zh/editor/terrain/images/terrain-inspector.png b/versions/4.0/zh/editor/terrain/images/terrain-inspector.png new file mode 100644 index 0000000000..c89c89be7a Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/terrain-inspector.png differ diff --git a/versions/4.0/zh/editor/terrain/images/terrain-layer.png b/versions/4.0/zh/editor/terrain/images/terrain-layer.png new file mode 100644 index 0000000000..b156f9e1fc Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/terrain-layer.png differ diff --git a/versions/4.0/zh/editor/terrain/images/terrain-manage.png b/versions/4.0/zh/editor/terrain/images/terrain-manage.png new file mode 100644 index 0000000000..afda42b5d4 Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/terrain-manage.png differ diff --git a/versions/4.0/zh/editor/terrain/images/terrain-paint.png b/versions/4.0/zh/editor/terrain/images/terrain-paint.png new file mode 100644 index 0000000000..a180a19ecf Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/terrain-paint.png differ diff --git a/versions/4.0/zh/editor/terrain/images/terrain-panel.png b/versions/4.0/zh/editor/terrain/images/terrain-panel.png new file mode 100644 index 0000000000..671051da6c Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/terrain-panel.png differ diff --git a/versions/4.0/zh/editor/terrain/images/terrain-sculpt.png b/versions/4.0/zh/editor/terrain/images/terrain-sculpt.png new file mode 100644 index 0000000000..1cf62ab955 Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/terrain-sculpt.png differ diff --git a/versions/4.0/zh/editor/terrain/images/terrain-select.png b/versions/4.0/zh/editor/terrain/images/terrain-select.png new file mode 100644 index 0000000000..d68bdbf9ab Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/terrain-select.png differ diff --git a/versions/4.0/zh/editor/terrain/images/terrain.png b/versions/4.0/zh/editor/terrain/images/terrain.png new file mode 100644 index 0000000000..2ad48159d0 Binary files /dev/null and b/versions/4.0/zh/editor/terrain/images/terrain.png differ diff --git a/versions/4.0/zh/editor/terrain/index.md b/versions/4.0/zh/editor/terrain/index.md new file mode 100644 index 0000000000..7b88631f34 --- /dev/null +++ b/versions/4.0/zh/editor/terrain/index.md @@ -0,0 +1,127 @@ +# 地形系统 + +地形系统以一种高效的方式来展示大自然的山川地貌。开发者可以很方便的使用画刷来雕刻出盆地、山脉、峡谷、平原等地貌。 + +![terrain](./images/terrain.png) + +## 创建地形 + +创建地形需要以下步骤: + +1. 在 **层级管理器** 右击菜单中点击 **创建 -> 地形** 来创建地形节点(地形节点可移动,但不支持旋转与缩放)。 + + ![create terrain](./images/create-terrain.png) + +2. 在 **资源管理器** 右击菜单中点击 **创建 -> 地形** 来创建地形资源。 + + ![create terrain asset](./images/create-terrain-asset.png) + +## 使用地形资源 + +**层级管理器** 里点击选中地形节点, **属性检查器** 会显示 `cc.Terrain` 地形组件,将已经创建好的地形资源拖拽到地形组件中的 `Asset` 属性框。 + +![terrain inspector](./images/terrain-inspector.png) + +| 属性 | 说明 | +|:-----|:----| +| Asset | 地形资源 | +| Effect Asset | 地形特效资源 | +| Receive Shadow | 是否接受阴影 | +| Use Normal Map | 是否使用法线贴图 | +| Use PBR | 是否使用物理材质 | +| Lod Enable | 是否启用地形 Lod。若启用可减少渲染面数,提升渲染性能 | +| Lod Bias | 设置 Lod 起始距离 | + +## 编辑场景地形 + +**层级管理器** 里点击选中地形节点, **场景编辑器** 会显示 地形节点的可编辑面板。 +编辑面板菜单有: **管理**(Manage)、**雕塑**(Sculpt)、**涂料**(Paint)和 **选择**(Select)几部分。可通过点击各 Tab 标签来切换面板内容。 + +![terrain panel](./images/terrain-panel.png) + +除了编辑面板,也可以通过 **场景编辑器** 左上角的图标来快捷切换地形画刷等功能。按顺序为: + +- **管理** 功能 +- **雕塑** 功能中的 **Bulge**(隆起)画刷类型 +- **雕塑** 功能中的 **Sunken**(凹陷)画刷类型 +- **雕塑** 功能中的 **Smooth**(平滑)画刷类型 +- **涂料** 功能 +- **选择** 功能 + +### 管理(Manage) + +用于调整地形的各种参数。Tile 是地形的最小单位,Tile 组成地形块(Block),目前一个 Block 由 **32x32** 个 Tile 组成,一个地形至少由 1 个 Block 组成。 + +![terrain manage](./images/terrain-manage.png) + +| 参数 | 说明 | +| :--- | :-- | +| Tile Size | 地形 Tile 的大小,目前一个地形块由 32 x 32 个 Tile 组成,所以一个地形块的边长是 **32 x Tile Size** | +| Weight Map Size | 权重图大小 | +| Light Map Size | 光照贴图大小 | +| Block Count | 地形块在两个维度上的数量(**注意**:若该值设置过大会造成顶点数过多,导致卡顿) | + +### 雕塑(Sculpt) + +用于改变地形的形状。 + +![terrain sculpt](./images/terrain-sculpt.png) + +| 参数 | 说明 | +| :--- | :--- | +| Brush Size | 画刷的大小 | +| Brush Strength | 画刷的力度 | +| Brush Mode | 画刷类型,包括 **Bulge**、**Sunken**、**Smooth**、**Flatten**、**Set Height** | +| Brush Height | 画刷高度,只有画刷模式为**Set Height**时生效 | +| Brush | 自定义画刷样式,通过选取样式图片生成自定义画刷,使用R通道来作为画刷蒙版 | +| Brush Rotation | 画刷的旋转值 | + +| 画刷模式 | 说明 | +| :--- | :--- | +| Bulge | 隆起地形 | +| Sunken | 下凹地形 | +| Smooth | 平滑地形 | +| Flatten | 整平地形 | +| Set Height | 使用**画刷高度**设置地形高度 | + +通过 **鼠标左键** 来雕刻地形,当使用**Bulge**模式时,还可以通过 **Shift + 鼠标左键** 下凹地形 + +### 涂料(Paint) + +用于描绘地形的纹理 + +![terrain paint](./images/terrain-paint.png) + +| 参数 | 说明 | +| :--- | :--- | +| Terrain Layer | 设置地形的 Layer。详情可参考下方的 **Layer 编辑** | +| Brush Size | 画刷的大小 | +| Brush Strength | 画刷的力度 | +| Brush Falloff | 画刷衰减度,决定了画刷边缘的锐利程度。
**0.0** 表示画刷在整个范围内都有完全效果(全部被当前层纹理覆盖),具有尖锐的边缘。
**1.0** 表示画刷仅在它中心具有完全效果,在到达边缘的过程中效果逐渐衰减 | +| Brush | 自定义画刷样式,通过选取样式图片生成自定义画刷 | + +#### Layer 编辑 + +![terrain layer](./images/terrain-layer.png) + +点击右上方的 **+/-** 按钮可以添加/删除 Layer(最多支持 4 层 layer)。选中某个 Layer 后就可以对 Layer 及其纹理进行编辑。 + +| 参数 | 说明 | +| :--- | :--- | +| Terrain Layer | 设置当前 Layer 的纹理 | +| Normal Map | 设置当前 Layer 的法线贴图,需要勾选地形组件的 **Use Normal Map** 属性 | +| Metallic | 设置当前 Layer 的金属特性 (主要指光滑程度) | +| Roughness | 设置当前 Layer 的粗糙程度 | +| Tile Size | 纹理的平铺大小,值越小会在同样大小的区域内进行更多次的平铺 | + +### 选择(Select) + +切换到 **选择** 分页后,在 **场景编辑器** 中选中地形块,便会显示当前地形块的相关信息。 + +![terrain select](./images/terrain-select.png) + +| 参数 | 说明 | +| :--- | :--- | +| Index | 当前选中地形块的索引 | +| Layers | 当前选中地形块的纹理列表 | +| Wight | 当前选中地形块的权重图 | diff --git a/versions/4.0/zh/editor/toolbar/img/2d3d.png b/versions/4.0/zh/editor/toolbar/img/2d3d.png new file mode 100644 index 0000000000..d98908f33d Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/2d3d.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/browse.png b/versions/4.0/zh/editor/toolbar/img/browse.png new file mode 100644 index 0000000000..dfe05fd423 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/browse.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/build.png b/versions/4.0/zh/editor/toolbar/img/build.png new file mode 100644 index 0000000000..d15ed5a884 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/build.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/gizmo_position.png b/versions/4.0/zh/editor/toolbar/img/gizmo_position.png new file mode 100644 index 0000000000..ec87099ade Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/gizmo_position.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/open_project.png b/versions/4.0/zh/editor/toolbar/img/open_project.png new file mode 100644 index 0000000000..e7b10a9c2b Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/open_project.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/position-snap.png b/versions/4.0/zh/editor/toolbar/img/position-snap.png new file mode 100644 index 0000000000..2c4706faae Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/position-snap.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/position-snap1.png b/versions/4.0/zh/editor/toolbar/img/position-snap1.png new file mode 100644 index 0000000000..874772802d Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/position-snap1.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/position-tool.png b/versions/4.0/zh/editor/toolbar/img/position-tool.png new file mode 100644 index 0000000000..d7fa645952 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/position-tool.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/preview-bar.png b/versions/4.0/zh/editor/toolbar/img/preview-bar.png new file mode 100644 index 0000000000..601f124d7b Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/preview-bar.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/preview.png b/versions/4.0/zh/editor/toolbar/img/preview.png new file mode 100644 index 0000000000..30d7b78dcb Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/preview.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/preview_url.png b/versions/4.0/zh/editor/toolbar/img/preview_url.png new file mode 100644 index 0000000000..50d9aa5b56 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/preview_url.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/rect-tool-config.png b/versions/4.0/zh/editor/toolbar/img/rect-tool-config.png new file mode 100644 index 0000000000..367c4391a5 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/rect-tool-config.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/rect-tool.png b/versions/4.0/zh/editor/toolbar/img/rect-tool.png new file mode 100644 index 0000000000..4031c6a011 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/rect-tool.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/rotation-snap.png b/versions/4.0/zh/editor/toolbar/img/rotation-snap.png new file mode 100644 index 0000000000..223f24c4c8 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/rotation-snap.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/rotation-tool.png b/versions/4.0/zh/editor/toolbar/img/rotation-tool.png new file mode 100644 index 0000000000..d7451f3e70 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/rotation-tool.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/scale-snap.png b/versions/4.0/zh/editor/toolbar/img/scale-snap.png new file mode 100644 index 0000000000..ac32b83238 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/scale-snap.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/scale-tool.png b/versions/4.0/zh/editor/toolbar/img/scale-tool.png new file mode 100644 index 0000000000..7ad09bed53 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/scale-tool.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/small.png b/versions/4.0/zh/editor/toolbar/img/small.png new file mode 100644 index 0000000000..787e94d176 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/small.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/smallSecretary.png b/versions/4.0/zh/editor/toolbar/img/smallSecretary.png new file mode 100644 index 0000000000..567fd61c35 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/smallSecretary.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/toolbarEdit.png b/versions/4.0/zh/editor/toolbar/img/toolbarEdit.png new file mode 100644 index 0000000000..705e65a603 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/toolbarEdit.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/transform-snap-config.png b/versions/4.0/zh/editor/toolbar/img/transform-snap-config.png new file mode 100644 index 0000000000..112131acc6 Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/transform-snap-config.png differ diff --git a/versions/4.0/zh/editor/toolbar/img/transform_tool.png b/versions/4.0/zh/editor/toolbar/img/transform_tool.png new file mode 100644 index 0000000000..2ef8d1f61b Binary files /dev/null and b/versions/4.0/zh/editor/toolbar/img/transform_tool.png differ diff --git a/versions/4.0/zh/editor/toolbar/index.md b/versions/4.0/zh/editor/toolbar/index.md new file mode 100644 index 0000000000..5fb48379b4 --- /dev/null +++ b/versions/4.0/zh/editor/toolbar/index.md @@ -0,0 +1,183 @@ +# 工具栏 + +**工具栏** 位于编辑器主窗口的正上方,包含了多组控制按钮或信息,用来为特定面板提供编辑功能或方便我们实施开发工作流。 + +![toolbar](./img/toolbarEdit.png) + +## 预览栏 + +![preview](img/preview-bar.png) + +预览栏目前允许多种预览方式,包含 **编辑器内**、**浏览器** 和 **模拟器**,详情请参考 [项目预览调试](../preview/index.md)。 + +## 构建 + +![build](img/build.png) 按钮可对项目进行构建,详情请参考 [跨平台发布游戏](../publish/index.md)。 + +## 手机端预览地址 + +![preview url](./img/preview_url.png) + +这里显示运行 Cocos Creator 的桌面电脑的局域网地址,连接同一局域网的移动设备可以访问这个地址来预览和调试游戏。将鼠标悬浮在局域网地址上,会浮现一个二维码,通过扫描二维码的方式也可以访问这个地址来预览和调试游戏。 + +## 浏览项目 + +![browse](img/browse.png) 按钮会打开对应平台的文件管理器,用于查看当前项目。 + + \ No newline at end of file diff --git a/versions/4.0/zh/engine/event/bubble-event.png b/versions/4.0/zh/engine/event/bubble-event.png new file mode 100644 index 0000000000..66c998144a Binary files /dev/null and b/versions/4.0/zh/engine/event/bubble-event.png differ diff --git a/versions/4.0/zh/engine/event/event-api.md b/versions/4.0/zh/engine/event/event-api.md new file mode 100644 index 0000000000..3dfd14cbaf --- /dev/null +++ b/versions/4.0/zh/engine/event/event-api.md @@ -0,0 +1,65 @@ +# 全局与节点触摸和鼠标事件 API + +## 鼠标事件 API + +| 函数名 | 返回值类型 | 意义 | +| :------------- | :---------- | :---------- | +| **getScrollY** | Number | 获取滚轮滚动的 Y 轴距离,只有滚动时才有效。 | +| **getButton** | Number | **EventMouse.BUTTON_LEFT** 或 **EventMouse.BUTTON_RIGHT** 或 **EventMouse.BUTTON_MIDDLE**。 | + +### 全局鼠标事件 API + +| 函数名 | 返回值类型 | 意义 | +| :------------- | :---------- | :---------- | +| **getLocation** | Vec2 | 获取鼠标位置对象,对象包含 x 和 y 属性。 | +| **getLocationX** | Number | 获取鼠标的 X 轴位置。 | +| **getLocationY** | Number | 获取鼠标的 Y 轴位置。 | +| **getPreviousLocation** | Vec2 | 获取鼠标事件上次触发时的位置对象,对象包含 x 和 y 属性。 | +| **getDelta** | Vec2 | 获取鼠标距离上一次事件移动相对于左下角的距离对象,对象包含 x 和 y 属性。 | +| **getDeltaX** | Number | 获取当前鼠标距离上一次鼠标移动相对于左下角的 X 轴距离。 | +| **getDeltaY** | Number | 获取当前鼠标距离上一次鼠标移动相对于左下角的 Y 轴距离。 | + +### 节点鼠标事件 API + +| 函数名 | 返回值类型 | 意义 | +| :------------- | :---------- | :---------- | +| **getUILocation** | Vec2 | 获取当前鼠标在 UI 窗口内相对于左下角的坐标位置,对象包含 x 和 y 属性。 | +| **getUILocationX** | Number | 获取当前鼠标在 UI 窗口内相对于左下角的 X 轴位置。 | +| **getUILocationY** | Number | 获取当前鼠标在 UI 窗口内相对于左下角的 Y 轴位置。 | +| **getUIPreviousLocation** | Vec2 | 获取上一次鼠标在 UI 窗口内相对于左下角的坐标位置,对象包含 x 和 y 属性。 | +| **getUIDelta** | Vec2 | 获取鼠标距离上一次事件移动在 UI 坐标系下的距离对象,对象包含 x 和 y 属性。 | +| **getUIDeltaX** | Number | 获取当前鼠标距离上一次鼠标移动在 UI 窗口内相对于左下角的 X 轴距离。 | +| **getUIDeltaY** | Number | 获取当前鼠标距离上一次鼠标移动在 UI 窗口内相对于左下角的 Y 轴距离。 | + +## 触摸事件 API + +| API 名 | 类型 | 意义 | +| :------------- | :---------- | :---------- | +| touch | Touch | 与当前事件关联的触点对象。 | +| getID | Number | 获取触点的 ID,用于多点触摸的逻辑判断。 | + +### 全局触摸事件 API + +| 函数名 | 返回值类型 | 意义 | +| :------------- | :---------- | :---------- | +| **getLocation** | Vec2 | 获取触点位置对象,对象包含 x 和 y 属性。 | +| **getLocationX** | Number | 获取触点的 X 轴位置。 | +| **getLocationY** | Number | 获取触点的 Y 轴位置。 | +| **getStartLocation** | Vec2 | 获取触点初始时的位置对象,对象包含 x 和 y 属性。 | +| **getPreviousLocation** | Vec2 | 获取触点事件上次触发时的位置对象,对象包含 x 和 y 属性。 | +| **getDelta** | Vec2 | 获取触点距离上一次事件移动的距离对象,对象包含 x 和 y 属性。 | +| **getDeltaX** | Number | 获取触点距离上一次事件移动的 X 轴距离。 | +| **getDeltaY** | Number | 获取触点距离上一次事件移动的 Y 轴距离。 | + +### 节点触摸事件 API + +| 函数名 | 返回值类型 | 意义 | +| :------------- | :---------- | :---------- | +| **getUILocation** | Vec2 | 获取当前触点在 UI 窗口内相对于左下角的坐标位置,对象包含 x 和 y 属性。 | +| **getUILocationX** | Number | 获取当前触点在 UI 窗口内相对于左下角的 X 轴位置。 | +| **getUILocationY** | Number | 获取当前触点在 UI 窗口内相对于左下角的 Y 轴位置。 | +| **getUIStartLocation** | Vec2 | 获取初始触点在 UI 窗口内相对于左下角的位置对象,对象包含 x 和 y 属性。 | +| **getUIPreviousLocation** | Vec2 | 获取上一次触点在 UI 窗口内相对于左下角的坐标位置,对象包含 x 和 y 属性。 | +| **getUIDelta** | Vec2 | 获取当前触点距离上一次触点移动在 UI 窗口内相对于左下角的距离对象,对象包含 x 和 y 属性。 | +| **getUIDeltaX** | Number | 获取当前触点距离上一次触点移动在 UI 窗口内相对于左下角的 X 轴距离。 | +| **getUIDeltaY** | Number | 获取当前触点距离上一次触点移动在 UI 窗口内相对于左下角的 Y 轴距离。 | diff --git a/versions/4.0/zh/engine/event/event-emit.md b/versions/4.0/zh/engine/event/event-emit.md new file mode 100644 index 0000000000..1df3454b94 --- /dev/null +++ b/versions/4.0/zh/engine/event/event-emit.md @@ -0,0 +1,119 @@ +# 监听和发射事件 + +Cocos Creator 引擎提供了 `EventTarget` 类,用以实现自定义事件的监听和发射,在使用之前,需要先从 `'cc'` 模块导入,同时需要实例化一个 `EventTarget` 对象。 + +```ts +import { EventTarget } from 'cc'; +const eventTarget = new EventTarget(); +``` + +> **注意**:虽然 `Node` 对象也实现了一些 `EventTarget` 的接口,但是我们不再推荐继续通过 `Node` 对象来做自定义事件的监听和发射,因为这样子做不够高效,同时我们也希望 `Node` 对象只监听与 `Node` 相关的事件。 + +## 监听事件 + +监听事件可以通过 `eventTarget.on()` 接口来实现,方法如下: + +```ts +// 该事件监听每次都会触发,需要手动取消注册 +eventTarget.on(type, func, target?); +``` + +其中 `type` 为事件注册字符串,`func` 为执行事件监听的回调,`target` 为事件接收对象。如果 `target` 没有设置,则回调里的 `this` 指向的就是当前执行回调的对象。 + +值得一提的是,事件监听接口 `on` 的第三个参数 `target`,主要是绑定响应函数的调用者。以下两种调用方式,效果上是相同的 + +```ts +// 使用函数绑定 +eventTarget.on('foo', function ( event ) { + this.enabled = false; +}.bind(this)); + +// 使用第三个参数 +eventTarget.on('foo', (event) => { + this.enabled = false; +}, this); +``` + +除了使用 `on` 监听,我们还可以使用 `once` 接口。`once` 监听在监听函数响应后就会关闭监听事件。 + +## 取消监听事件 + +当我们不再关心某个事件时,我们可以使用 `off` 接口关闭对应的监听事件。 + +`off` 接口的使用方式有以下两种: + +```ts +// 取消对象身上所有注册的该类型的事件 +eventTarget.off(type); +// 取消对象身上该类型指定回调指定目标的事件 +eventTarget.off(type, func, target); +``` + +需要注意的是,`off` 方法的参数必须和 `on` 方法的参数一一对应,才能完成关闭。 + +我们推荐的实现方式如下: + +```ts +import { _decorator, Component, EventTarget } from 'cc'; +const { ccclass } = _decorator; +const eventTarget = new EventTarget(); + +@ccclass("Example") +export class Example extends Component { + onEnable () { + eventTarget.on('foobar', this._sayHello, this); + } + + onDisable () { + eventTarget.off('foobar', this._sayHello, this); + } + + _sayHello () { + console.log('Hello World'); + } +} +``` + +## 事件发射 + +发射事件可以通过 `eventTarget.emit()` 接口来实现,方法如下: + +```ts +// 事件发射的时候可以指定事件参数,参数最多只支持 5 个事件参数 +eventTarget.emit(type, ...args); +``` + +## 事件参数说明 + +在发射事件时,我们可以在 `emit` 函数的第二个参数开始传递我们的事件参数。同时,在 `on` 注册的回调里,可以获取到对应的事件参数。 + +```ts +import { _decorator, Component, EventTarget } from 'cc'; +const { ccclass } = _decorator; +const eventTarget = new EventTarget(); + +@ccclass("Example") +export class Example extends Component { + onLoad () { + eventTarget.on('foo', (arg1, arg2, arg3) => { + console.log(arg1, arg2, arg3); // print 1, 2, 3 + }); + } + + start () { + let arg1 = 1, arg2 = 2, arg3 = 3; + // At most 5 args could be emit. + eventTarget.emit('foo', arg1, arg2, arg3); + } +} +``` + +> **注意**:出于底层事件派发的性能考虑,这里最多只支持传递 5 个事件参数。所以在传参时需要注意控制参数的传递个数。 + +## 系统内置事件 + +以上是通用的事件监听和发射规则,在 Cocos Creator 中,我们默认支持了一些系统内置事件,具体的说明及使用方式请参考: + +- [输入事件系统](event-input.md) + +- [节点事件系统](event-node.md) diff --git a/versions/4.0/zh/engine/event/event-input.md b/versions/4.0/zh/engine/event/event-input.md new file mode 100644 index 0000000000..c09b41b60a --- /dev/null +++ b/versions/4.0/zh/engine/event/event-input.md @@ -0,0 +1,242 @@ +# 输入事件系统 + +`EventTarget` 支持了一套完整的 [事件监听和发射机制](event-emit.md) 。在 Cocos Creator 3.4.0 中,我们支持了 `input` 对象,该对象实现了 `EventTarget` 的事件监听接口,可以通过 `input` 对象监听全局的系统输入事件。而原先的 `systemEvent` 对象则从 v3.4.0 开始废弃了,未来将逐步移除,建议使用 `input` 对象作为替代。 + +`systemEvent` 和 `input` 二者的差异包括: + +- **在类型定义上的差异** + + - `systemEvent` 的触摸事件回调的类型定义是 `(touch: Touch, event: EventTouch) => void` + + - `input` 的触摸事件回调的类型定义是 `(event: EventTouch) => void` + +- **在优先级上的差异** + + - `systemEvent` 的事件监听器会被节点的事件监听器拦截 + + - `input` 对象优先级比节点高,不会被拦截 + + > **注意**:我们在 **v3.4.1** 中降低了 `input` 优先级,因此二者从 v3.4.1 开始在优先级上已经不存在差异了。 + +--- + +本篇文档我们将介绍在 Cocos Creator 中对全局输入事件的处理。 + +全局输入事件是指与节点树不相关的各种输入事件,由 `input` 来统一派发,目前支持了以下几种事件: + +- 鼠标事件 +- 触摸事件 +- 键盘事件 +- 设备重力传感事件 + +## 定义输入事件 + +上文提到的输入事件,都可以通过接口 `input.on(type, callback, target)` 注册。可选的 `type` 类型包括: + +| 输入事件 | `type` 类型 | +| :----- | :---------- | +| 鼠标事件 | `Input.EventType.MOUSE_DOWN`
`Input.EventType.MOUSE_MOVE`
`Input.EventType.MOUSE_UP`
`Input.EventType.MOUSE_WHEEL` | +| 触摸事件 | `Input.EventType.TOUCH_START`
`Input.EventType.TOUCH_MOVE`
`Input.EventType.TOUCH_END`
`Input.EventType.TOUCH_CANCEL` | +| 键盘事件 | `Input.EventType.KEY_DOWN`(键盘按下)
`Input.EventType.KEY_PRESSING`(键盘持续按下)
`Input.EventType.KEY_UP`(键盘释放) | +| 设备重力传感事件 | `Input.EventType.DEVICEMOTION` | + +### 指针事件 + +指针事件包括 **鼠标事件** 和 **触摸事件**。 + +- 事件监听器类型 + + - 鼠标事件监听 + + - `Input.EventType.MOUSE_DOWN` + + - `Input.EventType.MOUSE_MOVE` + + - `Input.EventType.MOUSE_UP` + + - `Input.EventType.MOUSE_WHEEL` + + - 触摸事件监听 + + - `Input.EventType.TOUCH_START` + + - `Input.EventType.TOUCH_MOVE` + + - `Input.EventType.TOUCH_CANCEL` + + - `Input.EventType.TOUCH_END` + +- 事件触发后的回调函数 + + - 自定义回调函数:`callback(event);` + +- 回调参数 + + - [EventMouse](%__APIDOC__%/zh/class/EventMouse) 或 [EventTouch](__APIDOC__/zh/class/EventTouch) + +指针事件的使用范例如下: + +```ts +import { _decorator, Component, input, Input, EventTouch } from 'cc'; +const { ccclass } = _decorator; + +@ccclass("Example") +export class Example extends Component { + onLoad () { + input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + + onDestroy () { + input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + + onTouchStart(event: EventTouch) { + console.log(event.getLocation()); // Location on screen space + console.log(event.getUILocation()); // Location on UI space + } +} +``` + +### 键盘事件 + +- 事件监听器类型 + + - `Input.EventType.KEY_DOWN` + + - `Input.EventType.KEY_PRESSING` + + - `Input.EventType.KEY_UP` + +- 事件触发后的回调函数 + + - 自定义回调函数:`callback(event);` + +- 回调参数 + + - [EventKeyboard](%__APIDOC__%/zh/class/EventKeyboard) + +使用键盘事件的代码示例如下: + +```ts +import { _decorator, Component, input, Input, EventKeyboard, KeyCode } from 'cc'; +const { ccclass } = _decorator; + +@ccclass("Example") +export class Example extends Component { + onLoad () { + input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this); + input.on(Input.EventType.KEY_UP, this.onKeyUp, this); + } + + onDestroy () { + input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this); + input.off(Input.EventType.KEY_UP, this.onKeyUp, this); + } + + onKeyDown (event: EventKeyboard) { + switch(event.keyCode) { + case KeyCode.KEY_A: + console.log('Press a key'); + break; + } + } + + onKeyUp (event: EventKeyboard) { + switch(event.keyCode) { + case KeyCode.KEY_A: + console.log('Release a key'); + break; + } + } +} +``` + +### 设备重力传感事件 + +- 事件监听器类型 + + - `Input.EventType.DEVICEMOTION` + +- 事件触发后的回调函数 + + - 自定义回调函数:`callback(event);` + +- 回调参数 + + - [EventAcceleration](%__APIDOC__%/zh/class/EventAcceleration) + +使用设备重力传感事件的代码示例如下: + +```ts +import { _decorator, Component, input, Input, log } from 'cc'; +const { ccclass } = _decorator; + +@ccclass("Example") +export class Example extends Component { + onLoad () { + input.setAccelerometerEnabled(true); + input.on(Input.EventType.DEVICEMOTION, this.onDeviceMotionEvent, this); + } + + onDestroy () { + input.off(Input.EventType.DEVICEMOTION, this.onDeviceMotionEvent, this); + } + + onDeviceMotionEvent (event: EventAcceleration) { + log(event.acc.x + " " + event.acc.y); + } +} +``` + +具体使用方法可参考范例 **event**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/event) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/event)),其中包含了键盘、重力感应、单点触摸、多点触摸等功能的实现。 + +## 3D 物体的触摸检测 + +3D 物体与 2D UI 节点的触摸检测不同: + +- 2D UI 节点只需要通过 `UITransform` 组件提供的尺寸信息和节点的位置信息,就可以实现触摸检测,详情请参考 [节点事件系统](event-node.md)。 + +- 3D 物体的触摸检测需要通过射线检测来实现。具体做法是通过渲染 3D 物体的 Camera 到触点的屏幕坐标,生成一条射线,判断射线是否穿过想要检测的对象。具体代码实现如下: + + ```ts + import { _decorator, Component, Node, Camera, geometry, input, Input, EventTouch, PhysicsSystem } from 'cc'; + const { ccclass, property } = _decorator; + + @ccclass("Example") + export class Example extends Component { + + // Specify the camera rendering the target node. + @property(Camera) + readonly cameraCom!: Camera; + + @property(Node) + public targetNode!: Node + + private _ray: geometry.Ray = new geometry.Ray(); + + onEnable () { + input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + + onDisable () { + input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + + onTouchStart(event: EventTouch) { + const touch = event.touch!; + this.cameraCom.screenPointToRay(touch.getLocationX(), touch.getLocationY(), this._ray); + if (PhysicsSystem.instance.raycast(this._ray)) { + const raycastResults = PhysicsSystem.instance.raycastResults; + for (let i = 0; i < raycastResults.length; i++) { + const item = raycastResults[i]; + if (item.collider.node == this.targetNode) { + console.log('raycast hit the target node !'); + break; + } + } + } else { + console.log('raycast does not hit the target node !'); + } + } + } + ``` diff --git a/versions/4.0/zh/engine/event/event-node.md b/versions/4.0/zh/engine/event/event-node.md new file mode 100644 index 0000000000..56c032c016 --- /dev/null +++ b/versions/4.0/zh/engine/event/event-node.md @@ -0,0 +1,243 @@ +# 节点事件系统 + +`input` 对象支持了全局的 [输入事件系统](event-input.md),全局输入事件包括鼠标、触摸、键盘和重力传感四种。而 `Node` 也实现了 `EventTarget` 的事件监听接口。在此基础上,我们提供了一些基础的节点相关的系统事件。 + +本篇文档着重介绍了与 UI 节点树相关联的鼠标和触摸事件,这些事件是被直接触发在 UI 相关节点上的,所以被称为节点事件,使用方式如下: + +```ts +node.on(Node.EventType.MOUSE_DOWN, (event) => { + console.log('Mouse down'); +}, this); +``` + +> **注意**:我们不推荐直接使用事件的名称字符串注册事件监听了。例如上述代码示例,请不要使用 `node.on('mouse-down', callback, target)` 来注册事件监听。 + +2D UI 节点上的触摸事件监听依赖于 `UITransform` 组件。如果需要实现对 3D 物体的触摸检测,请参考文档 [3D 物体的触摸检测](event-input.md#%E5%AF%B9%203D%20%E7%89%A9%E4%BD%93%E7%9A%84%E8%A7%A6%E6%91%B8%E6%A3%80%E6%B5%8B)。 + +## 鼠标事件类型和事件对象 + +鼠标事件在 PC 端才会触发,系统提供的事件类型如下: + +| 枚举对象定义 | 事件触发的时机 | +|:--------------------------------|:-------------------------------------| +| **Node.EventType.MOUSE_DOWN** | 当鼠标在目标节点区域按下时触发一次。 | +| **Node.EventType.MOUSE_ENTER** | 当鼠标移入目标节点区域时触发,不论是否按下。 | +| **Node.EventType.MOUSE_MOVE** | 当鼠标在目标节点区域中移动时触发,不论是否按下。 | +| **Node.EventType.MOUSE_LEAVE** | 当鼠标移出目标节点区域时触发,不论是否按下。 +| **Node.EventType.MOUSE_UP** | 当鼠标从按下状态松开时触发一次。 | +| **Node.EventType.MOUSE_WHEEL** | 当鼠标滚轮滚动时触发。 | + +鼠标事件(`Event.EventMouse`)的重要 API 请参考 [鼠标事件 API](event-api.md#鼠标事件-API)(Event 标准事件 API 除外)。 + +## 触摸事件类型和事件对象 + +触摸事件在移动端和 PC 端都会触发,开发者若希望更好地在 PC 端进行调试,只需要监听触摸事件即可同时响应移动端的触摸事件和 PC 端的鼠标事件。系统提供的触摸事件类型如下: + +| 枚举对象定义 | 事件触发的时机 | +|:--------------------------------|:---------------------------| +| **Node.EventType.TOUCH_START** | 当手指触点落在目标节点区域内时。 | +| **Node.EventType.TOUCH_MOVE** | 当手指在屏幕上移动时。 | +| **Node.EventType.TOUCH_END** | 当手指在目标节点区域内离开屏幕时。 | +| **Node.EventType.TOUCH_CANCEL** | 当手指在目标节点区域外离开屏幕时。 | + +触摸事件(`Event.EventTouch`)的重要 API 请参考 [触摸事件 API](./event-api.md#触摸事件-API)(Event 标准事件 API 除外)。 + +> **注意**:触摸事件支持多点触摸,每个触点都会发送一次事件给事件监听器。 + +## 节点事件派发 + +Cocos Creator 在 `Node` 上支持了 `dispatchEvent` 接口,通过该接口派发的事件,会进入事件派发阶段。Creator 的事件派发系统是按照 [Web 的事件冒泡及捕获标准](https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Building_blocks/Events#%E4%BA%8B%E4%BB%B6%E5%86%92%E6%B3%A1%E5%8F%8A%E6%8D%95%E8%8E%B7) 实现的,事件在派发之后,会经历下面三个阶段: +- **捕获**:事件从场景根节点,逐级向子节点传递,直到到达目标节点或者在某个节点的响应函数中中断事件传递 +- **目标**:事件在目标节点上触发 +- **冒泡**:事件由目标节点,逐级向父节点冒泡传递,直到到达根节点或者在某个节点的响应函数中中断事件传递 + +当调用 `node.dispatchEvent()` 时,意味着 `node` 就是上文提到的目标节点。在事件的传递过程中,我们可以通过调用 `event.propagationStopped = true` 来中断事件传递。 + +在 v3.0 中,我们移除了 `Event.EventCustom` 类,如果要派发自定义事件,需要先实现一个自定义的事件类,该类继承自 `Event` 类,例如: + +```ts +// Event 由 cc 模块导入 +import { Event } from 'cc'; + +class MyEvent extends Event { + constructor(name: string, bubbles?: boolean, detail?: any) { + super(name, bubbles); + this.detail = detail; + } + public detail: any = null; // 自定义的属性 +} +``` + +![bubble-event](bubble-event.png) + +以上图为例,这张图展示了事件在 **目标** 和 **冒泡** 阶段的传递顺序。当我们从节点 c 派发事件 `“foobar”`,倘若节点 a,b 均做了 `“foobar”` 事件的监听,则事件会经由 c 依次传递给 b、a 节点。代码实现示例如下: + +```ts +// 节点 c 的组件脚本中 +this.node.dispatchEvent( new MyEvent('foobar', true, 'detail info') ); +``` + +如果我们希望在 b 节点截获事件后就不再传递事件,可以通过调用 `event.propagationStopped = true` 函数来完成。代码实现示例如下: + +```ts +// 节点 b 的组件脚本中 +this.node.on('foobar', (event: MyEvent) => { + event.propagationStopped = true; +}); +``` + +> **注意**:在派发用户自定义事件的时候,请不要直接创建 `cc` 内的 `Event` 对象,因为它是一个抽象类。 + +如果希望将事件注册在捕获阶段,可以给 `on` 接口传递第四个参数,例如: + +```ts +// 节点 a 的组件脚本中 +this.node.on('foobar', callback, target, true); +``` + +## 事件对象 + +在事件监听回调中,开发者会接收到一个 Event 类型的事件对象 event,`propagationStopped` 就是 Event 的标准 API,其它重要的 API 包含: + +| API 名 | 类型 | 意义 | +| :------------- | :---------- | :---------- | +| **type** | String | 事件的类型(事件名)。 | +| **target** | Node | 接收到事件的原始对象。 | +| **currentTarget** | Node | 接收到事件的当前对象,事件在冒泡阶段当前对象可能与原始对象不同。 | +| **getType** | Function | 获取事件的类型。 | +| **propagationStopped** | Boolean | 是否停止传递当前事件。 | +| **propagationImmediateStopped** | Boolean | 是否立即停止当前事件的传递,事件甚至不会被分派到所连接的当前目标。 | + +## 触摸事件的传递 + +如上所述,注册在 `Node` 上的触摸事件,引擎内部就是通过 `dispatchEvent` 接口派发的。下面我们将介绍触摸事件在 **目标** 和 **冒泡** 阶段的传递顺序。 + +### 触摸事件冒泡 + +触摸事件支持节点树的事件冒泡,以下图为例: + +![propagation](propagation.png) + +在上图的场景中,假设 A 节点拥有一个子节点 B,B 拥有一个子节点 C。开发者对 A、B、C 节点都监听了触摸事件(以下的示例默认节点都监听了触摸事件)。 + +当鼠标或手指在 C 节点区域内按下时,事件将首先在 C 节点触发,C 节点监听器接收到事件(该阶段为目标阶段);接着 C 节点会将事件向其父节点 B 传递这个事件,B 节点的监听器将会接收到事件;同理 B 节点会将事件传递给父节点 A 。这就是最基本的事件冒泡过程。 + +> **注意**:虽然触点可能不在 A、B 节点区域内,但 A、B 节点也能触发触摸事件,这是通过子节点 C 的触摸事件冒泡机制传递过来的。在触摸事件冒泡的过程中不会有触摸检测。 + +触摸事件的冒泡过程与普通事件的冒泡过程并没有区别。所以,调用 `event.propagationStopped = true` 便可主动停止触摸事件的冒泡过程。 + +### 同级节点间的触点归属问题 + +同级节点间,触点归属于处于顶层的节点。假设上图中 B、C 为同级节点,C 节点部分覆盖在 B 节点之上。这时候如果 C 节点接收到触摸事件,那么表示触点归属于 C 节点,这意味着同级节点 B 就不会再接收到触摸事件了,即使触点同时也在 B 节点内。 + +此时如果 C 节点还存在父节点,那么通过事件冒泡机制 C 节点还可以将触摸事件传递给父节点。 + +在 v3.4.0 中,我们支持了 **事件穿透派发** 功能。在上述同级节点的例子中,如果需要把事件穿透也派发给 B 节点,则可以通过调用 `event.preventSwallow = true` 来阻止事件被 C 节点吞噬。 + +> **注意**:如果要让 TOUCH END 事件可穿透的话,对应的 TOUCH START 事件也需要设置为可穿透。事件的穿透派发会降低事件派发的效率,请谨慎使用。 + +### 不同 Canvas 的触点归属问题 + +不同 Canvas 之间的触点拦截是根据节点优先级(可在 Canvas 节点默认自带的 Camera 节点的 `priority` 属性中设置)决定的。在下图的场景中,左侧节点树里的节点 Canvas 1-5 对应着右侧场景中的图片 priority 1-5。可以看出,即使节点 Canvas 3、4、5 在节点树里并没有按照顺序排列,但根据 Canvas 上的优先级(`priority`)关系,触点的响应先后顺序仍然是 **Canvas 5 -> Canvas 4 -> Canvas 3 -> Canvas 2 -> Canvas 1**。只有在优先级相同的情况下,Canvas 之间的排序才是按照节点树的先后顺序进行。 + +![multi-canvas](multi-canvas.png) + +### 将触摸或鼠标事件注册在捕获阶段 + +有时候我们需要父节点的触摸或鼠标事件先于它的任何子节点派发,比如 **ScrollView** 组件就是这样设计的。这时候事件冒泡已经不能满足我们的需求了,需要将父节点的事件注册在捕获阶段。 + +要实现这个需求,可以在给 node 注册触摸或鼠标事件时,传入第四个参数 `true`,表示 `useCapture`。代码示例如下: + +```ts +this.node.on(Node.EventType.TOUCH_START, this.onTouchStartCallback, this, true); +``` + +当节点触发 `Node.EventType.TOUCH_START` 事件时,会先将 `Node.EventType.TOUCH_START` 事件派发给所有注册在捕获阶段的父节点监听器,然后派发给节点自身的监听器,最后才到了事件冒泡阶段。 + +### 事件拦截 + +正常的事件会按照上文说明的方式去派发。但是如果节点身上带有 `Button`、`Toggle` 或者 `BlockInputEvents` 这几个组件的话,会停止事件冒泡。 + +例如下图,图中有两个按钮,分别是 Canvas 0 下的 priority 1 和 Canvas 1 下的 priority 2。如果点击两个按钮的交汇处,也就是图中的蓝色区域,会出现按钮 priority 2 成功接收到了触点事件,而按钮 priority 1 则没有。
这是因为按上文的事件接收规则,按钮 priority 2 优先接收到了触摸事件,并且对事件进行了拦截(`event.propagationStopped = true`),以防止事件穿透。如果是非按钮节点,也可以通过添加 `BlockInputEvents` 组件来对事件进行拦截,防止穿透。 + +> **注意**:按钮 priority 1 和 priority 2 分别在 Canvas 0 和 Canvas 1 节点下,两个按钮并不是同级节点。 + +![events-block](events-block.png) + +## 触摸事件举例 + +以下图举例,总结下触摸事件的传递机制。图中有 A、B、C、D 四个节点,其中 A、B 为同级节点。具体层级关系如下: + +![example](example.png) + +1. 若触点在 A、B 的重叠区域内,此时 B 接收不到触摸事件,事件的传递顺序是 A -> C -> D +2. 若触点在 B 节点内(可见的绿色区域),则事件的传递顺序是 B -> C -> D +3. 若触点在 C 节点内,则事件的传递顺序是 C -> D +4. 若以第 2 种情况为前提,同时 C D 节点的触摸事件注册在捕获阶段,则事件的传递顺序是 D -> C -> B + +## Node 的其它事件 + +所有的 `Node` 内置事件都可以通过 `Node.EventType` 获取事件名。 + +### 3D 节点事件 + +| 枚举对象定义 | 事件触发的时机 | +| :------------- | :---------- | +| **TRANSFORM_CHANGED** | 当变换属性修改时,会派发一个枚举值 `TransformBit`,根据枚举值定义修改的变换。 | + +变换枚举值定义: + +| 枚举值含义 | 对应的变换 | +|:---------------------------|:-----------------------| +| **TransformBit.NONE** | 属性无改变。 | +| **TransformBit.POSITION** | 节点位置改变。 | +| **TransformBit.ROTATION** | 节点旋转改变。 | +| **TransformBit.SCALE** | 节点缩放改变。 | +| **TransformBit.RS** | 节点旋转及缩放改变。 | +| **TransformBit.TRS** | 节点平移,旋转及缩放都改变。 | + +### 2D 节点事件 + +| 枚举对象定义 | 事件触发的时机 | +|:--------------------| :----------------------------------------------| +| **SIZE_CHANGED** | 当宽高属性修改时。宽高属性位于 `UITransform` 组件上。 | +| **ANCHOR_CHANGED** | 当锚点属性修改时。锚点属性位于 `UITransform` 组件上。 | +| **COLOR_CHANGED** | 当颜色属性修改时。颜色属性位于 UI 渲染组件上。 | +| **CHILD_ADDED** | 添加子节点时。 | +| **CHILD_REMOVED** | 移除子节点时。 | +| **PARENT_CHANGED** | 父节点改变时。 | +| **SIBLING_ORDER_CHANGED** | 兄弟节点顺序改变时。 | +| **SCENE_CHANGED_FOR_PERSISTS** | 改变常驻节点所在场景时。 | +| **NODE_DESTROYED** | 节点销毁时。 | +| **LAYER_CHANGED** | `layer` 属性改变时。 | +| **ACTIVE_IN_HIERARCHY_CHANGED** | `activeInHierarchy` 属性改变时。 | + +## 多点触摸事件 + +引擎有多点触摸事件的屏蔽开关,多点触摸事件默认为开启状态。对于不需要多点触摸的项目,可以通过以下代码关闭多点触摸: + +```ts +macro.ENABLE_MULTI_TOUCH = false; +``` + +或者也可以通过 Creator 顶部菜单栏的 **项目 -> 项目设置 -> Macro Config** 进行配置,去掉**ENABLE_MULTI_TOUCH** 属性的勾选。 + +## 暂停或恢复节点系统事件 + +暂停节点系统事件,代码示例如下: + +```ts +// 暂停当前节点上注册的所有节点系统事件,节点系统事件包含触摸和鼠标事件。 +// 如果传递参数 true,那么这个 API 将暂停本节点和它的所有子节点上的节点系统事件。 +// example +this.node.pauseSystemEvents(); +``` + +恢复节点系统事件,代码示例如下: + +```ts +// 恢复当前节点上注册的所有节点系统事件,节点系统事件包含触摸和鼠标事件。 +// 如果传递参数 true,那么这个 API 将恢复本节点和它的所有子节点上的节点系统事件。 +// example +this.node.resumeSystemEvents(); +``` diff --git a/versions/4.0/zh/engine/event/event-screen.md b/versions/4.0/zh/engine/event/event-screen.md new file mode 100644 index 0000000000..d7c5477877 --- /dev/null +++ b/versions/4.0/zh/engine/event/event-screen.md @@ -0,0 +1,55 @@ +# 屏幕事件系统 + +## 简介 + +正如之前所讨论的,EventTarget 提供了事件监听和发射的功能。Cocos Creator v3.8.0 引入了 screen 对象,它实现了 EventTarget 接口。该对象允许注册全局系统屏幕事件。 + +## 支持的事件 + +以下是当前支持的全局屏幕事件概述: + +| 事件名称 | 描述 | 支持平台 | 支持版本 | +|----------------------------|-------------------------------------------------|--------------------------|-------------------| +| `window-resize` | 监听窗口大小变化 | Web, Native, MiniGame | 3.8.0 | +| `orientation-change` | 监听屏幕方向变化 | Web, Native | 3.8.3 | +| `fullscreen-change` | 监听全屏变化 | Web | 3.8.0 | + +## 事件使用示例 + +```typescript +import { _decorator, Component, screen, macro } from 'cc'; +const { ccclass } = _decorator; + +@ccclass("Example") +export class Example extends Component { + onLoad() { + // Register event listeners with the screen object + screen.on('window-resize', this.onWindowResize, this); + screen.on('orientation-change', this.onOrientationChange, this); + screen.on('fullscreen-change', this.onFullScreenChange, this); + } + + onDestroy() { + // Unregister event listeners when the component is destroyed + screen.off('window-resize', this.onWindowResize, this); + screen.off('orientation-change', this.onOrientationChange, this); + screen.off('fullscreen-change', this.onFullScreenChange, this); + } + + onWindowResize(width: number, height: number) { + console.log("Window resized:", width, height); + } + + onOrientationChange(orientation: number) { + if (orientation === macro.ORIENTATION_LANDSCAPE_LEFT || orientation === macro.ORIENTATION_LANDSCAPE_RIGHT) { + console.log("Orientation changed to landscape:", orientation); + } else { + console.log("Orientation changed to portrait:", orientation); + } + } + + onFullScreenChange(width: number, height: number) { + console.log("Fullscreen change:", width, height); + } +} +``` diff --git a/versions/4.0/zh/engine/event/events-block.png b/versions/4.0/zh/engine/event/events-block.png new file mode 100644 index 0000000000..9fb6664e4e Binary files /dev/null and b/versions/4.0/zh/engine/event/events-block.png differ diff --git a/versions/4.0/zh/engine/event/example.png b/versions/4.0/zh/engine/event/example.png new file mode 100644 index 0000000000..d38c8f2985 Binary files /dev/null and b/versions/4.0/zh/engine/event/example.png differ diff --git a/versions/4.0/zh/engine/event/index.md b/versions/4.0/zh/engine/event/index.md new file mode 100644 index 0000000000..1d000a67a2 --- /dev/null +++ b/versions/4.0/zh/engine/event/index.md @@ -0,0 +1,13 @@ +# 事件系统 + +事件系统是游戏开发过程中需要涉及到交互常用的功能。使用事件系统不仅可以将输入行为(例如:键盘、鼠标、触摸)以事件的形式发送到应用程序,也可以将游戏过程中的发生的,需要其他对象关注的事情通过事件的形式回应。例如:游戏胜利后需要打开结算或者奖励界面。 + +## 事件使用 + +事件需要通过注册获取监听,详情请参考 [监听和发射事件](event-emit.md)。 + +在事件监听和发射的基础上,Cocos Creator 支持了很多内置事件系统,包括: + +- [输入事件系统](event-input.md) +- [节点事件系统](event-node.md) +- [屏幕事件系统](event-screen.md) diff --git a/versions/4.0/zh/engine/event/multi-canvas.png b/versions/4.0/zh/engine/event/multi-canvas.png new file mode 100644 index 0000000000..3004dd4647 Binary files /dev/null and b/versions/4.0/zh/engine/event/multi-canvas.png differ diff --git a/versions/4.0/zh/engine/event/propagation.png b/versions/4.0/zh/engine/event/propagation.png new file mode 100644 index 0000000000..4c3cd1170f Binary files /dev/null and b/versions/4.0/zh/engine/event/propagation.png differ diff --git a/versions/4.0/zh/engine/renderable/create-model.png b/versions/4.0/zh/engine/renderable/create-model.png new file mode 100644 index 0000000000..86412ef46a Binary files /dev/null and b/versions/4.0/zh/engine/renderable/create-model.png differ diff --git a/versions/4.0/zh/engine/renderable/mesh-renderer/view-model.gif b/versions/4.0/zh/engine/renderable/mesh-renderer/view-model.gif new file mode 100644 index 0000000000..75bb20c8a8 Binary files /dev/null and b/versions/4.0/zh/engine/renderable/mesh-renderer/view-model.gif differ diff --git a/versions/4.0/zh/engine/renderable/meshrenderer-properties.png b/versions/4.0/zh/engine/renderable/meshrenderer-properties.png new file mode 100644 index 0000000000..9e09f1eba5 Binary files /dev/null and b/versions/4.0/zh/engine/renderable/meshrenderer-properties.png differ diff --git a/versions/4.0/zh/engine/renderable/model-component.md b/versions/4.0/zh/engine/renderable/model-component.md new file mode 100644 index 0000000000..e050d8661c --- /dev/null +++ b/versions/4.0/zh/engine/renderable/model-component.md @@ -0,0 +1,124 @@ +# MeshRenderer 组件参考 + +MeshRenderer(网格渲染器)组件用于显示一个静态的 3D 模型。通过 **Mesh** 属性设置模型网格,通过 **Materials** 属性控制模型的显示外观。 + +在 **属性检查器** 中点击 **添加组件 -> Mesh -> MeshRenderer** 即可添加 MeshRenderer 组件。 + +![Mesh Renderer properties](meshrenderer-properties.png) + +## MeshRenderer 属性 + +| 属性 | 功能 | +| :--- | :--- | +| **Materials** | 网格资源允许使用多个材质资源,所有材质资源都存在 `materials` 数组中。
如果网格资源中有多个子网格,那么 Mesh Renderer 会从 `materials` 数组中获取对应的材质来渲染此子网格。 | +| **Dynamic Shadow Settings** | 动态阴影配置,详情请参考下方 **动态阴影配置** 文档 | +| **Bake Settings** | 烘焙配置,详情请参考下方 **烘焙配置** 文档 | +| **Mesh** | 指定渲染所用的网格资源,详情请参考下文 **网格资源** 部分的内容 | + +### 动态阴影配置 + +| 属性 | 说明 | +| :-- | :-- | +| **Shadow Bias** | 阴影偏移 +| **Shadow Normal Bias** | 阴影法线偏移 +| **Shadow Casting Mode** | 指定当前模型是否会投射阴影,需要先在场景中 [开启阴影](../../concepts/scene/light/shadow.md#%E5%BC%80%E5%90%AF%E9%98%B4%E5%BD%B1)。| +| **Receive Shadow** | 指定当前模型是否会接收并显示其它物体产生的阴影效果,需要先在场景中 [开启阴影](../../concepts/scene/light/shadow.md#%E5%BC%80%E5%90%AF%E9%98%B4%E5%BD%B1)。该属性仅在阴影类型为 **ShadowMap** 时生效。| + +### 烘焙配置 + +烘焙设置指的是网格如何使用或写入离线光照信息。 + +该配置包含:[光照贴图](../../concepts/scene/light/lightmap.md)、[光照探针](probe/light-probe.md) 和 [反射探针](../../concepts/scene/light/probe/reflection-probe.md)。 + +#### 光照贴图 + +| 属性 | 说明 | +| :-- | :-- | +| **Bakeable** | 是否会被烘焙到光照贴图内 | +| **Cast Shadow** | 是否投射阴影 | +| **Receive Shadow** | 是否接收阴影 | +| **Lightmap Size** | 光照贴图的大小 | + +#### 光照探针 + +| 属性 | 说明 | +| :-- | :-- | +| **Use Light Probe** | 配置是否使用光照探针 | +| **Bake To Light Probe** | 配置是否烘焙光照探针 | + +#### 反射探针 + +| 属性 | 说明 | +| :-- | :-- | +| **Reflection Probe** | [反射探针](../../concepts/scene/light/probe/reflection-probe.md) 的类型
支持 **PLANNAR_REFLECTION** 、 **CUBE_REFLECTION**、**BLEND_PROBES**、**BLEND_PROBES_AND_SKYBOX**
**PLANNAR_REFLECTION**:配置为动态反射探针
**CUBE_REFLECTION**:配置为使用烘焙后的反射探针
**BLEND_PROBES**:配置为使用烘焙后的反射探针并且反射探针之间会进行混合。
**BLEND_PROBES_AND_SKYBOX**:配置为使用烘焙后的反射探针并且在BLEND_PROBES项的基础上增加了和天空盒混合的功能 | +| **Bake To Reflection Probe** | 配置是否烘焙至反射探针生成的贴图内 | + +网格渲染器组件相关接口请参考 [MeshRenderer API](%__APIDOC__%/zh/class/MeshRenderer)。 + +蒙皮网格渲染器组件相关接口请参考 [SkinnedMeshRenderer API](%__APIDOC__%/zh/class/SkinnedMeshRenderer) + +### 网格资源 + +Mesh 资源是渲染网格的必要资源,目前网格主要是在 [导入模型资源](../../asset/model/mesh.md#%E6%A8%A1%E5%9E%8B%E5%AF%BC%E5%85%A5) 到 Creator 中时,由 Creator 自动生成的。 + +Mesh 资源中包含了一组顶点和多组索引。索引指向顶点数组中的顶点,每三组索引组成一个三角形。网格则是由多个三角形组成的,是 3D 世界中最基本的图元。多个三角形拼接成一个复杂的多边形,多个多边形则拼接成一个 3D 模型。 + +Creator 提供了几个简单的静态 3D 模型,其中包含了立方体、圆柱体等基础模型,开发者可根据自己的需要在 **层级管理器** 中创建几个,以初步了解。 + +![create model](create-model.png) + +## 模型分组渲染 + +分组渲染功能是通过相机组件的 [Visibility 属性](../../editor/components/camera-component.md#%E8%AE%BE%E7%BD%AE-visibility-%E5%B1%9E%E6%80%A7) 配合节点的 [Layer 属性](../../concepts/scene/node-component.md#%E8%AE%BE%E7%BD%AE%E8%8A%82%E7%82%B9%E7%9A%84-layer-%E5%B1%9E%E6%80%A7) 共同决定。用户可通过代码设置 `Visibility` 的值来完成分组渲染。所有节点默认都属于 **DEFAULT** 层,在所有相机都可见。 + +## 静态合批 + +目前静态合批方案为运行时静态合批,通过调用 `BatchingUtility.batchStaticModel` 可进行静态合批。
+该函数接收一个节点,然后将该节点下的所有 `MeshRenderer` 里的 `Mesh` 合并成一个,并将其挂到另一个节点下。
+在合批后,将无法改变原有的 `MeshRenderer` 的 `transform`,但可以改变合批后的根节点的 `transform`。 + +只有满足以下条件的节点才能进行静态合批: +- 子节点中只能包含 `MeshRenderer`; +- 子节点下的 `MeshRenderer` 的 `Mesh` 的顶点数据结构必须一致; +- 子节点下的 `MeshRenderer` 的材质必须相同。 + +## 动态合批 + +引擎目前提供 instancing 动态合批功能。
+要开启合批,只需在模型所使用的材质中对应勾选 `USE_INSTANCING` 开关即可。 + +> **注意**:目前的合批流程会引入一些限制: +> +> 1. 同一批次的透明模型间的绘制顺序无法保证,可能导致混合效果不准确; +> 2. 合批后没有传递逆转置世界矩阵信息,带有非均一缩放的模型的法线会不准确; +> 3. 只支持普通 3D 模型和预烘焙骨骼动画控制下的蒙皮模型(实时计算骨骼动画、2D 物体、UI、粒子等均不支持动态合批)。 + +### Instancing 合批 + +通过 Instancing 的合批适用于绘制大量顶点数据完全相同的动态模型,启用后绘制时会根据材质和顶点数据分组,每组内组织 instanced attributes 信息,然后一次性完成绘制。 + +关于蒙皮模型的支持及相关设定,参考 [骨骼动画组件](../../animation/skeletal-animation.md#关于动态-Instancing)。 + +另外 instancing 还支持自定义额外的 instanced attributes,可以传递更多不同 instance 之间的差异性数据(比如不同人物间给一个漫反射颜色的外观差异,或大片草地中的风力影响)。
+这需要自定义 effect 的支持,更详细的说明可以参考 [语法指南](../../shader/effect-syntax.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%87%A0%E4%BD%95%E4%BD%93%E5%AE%9E%E4%BE%8B%E5%8C%96%E5%B1%9E%E6%80%A7) + +### 合并 VB 合批 + +考虑到合并 VB 合批在每帧合并顶点等操作会引入一部分 CPU 开销,相当于是用CPU开销来替换 drawcall 的开销(大多数情况下前者反而要大于后者,在 JS 中尤其昂贵),未经严格测试的滥用反而会引起性能降低,**此功能在3.6.2已移除,请优先使用 Instancing 或静态合批来替代**。 + +另外需要提醒 drawcall 数量并非唯一的性能指标[^2],最佳性能往往是 CPU 与 GPU 负载均衡的结果,所以在尝试使用合批功能时,请一定多做测试,明确性能瓶颈,做有针对性的优化。 + +## 合批的最佳实践 + +通常来说合批系统的使用优先级为:**静态合批 > instancing 合批**。
+首先要确保材质统一,在这个前提下,如果确定某些模型在游戏周期内完全静止不会变化,就可以使用静态合批。
+如果存在大量相同的模型重复绘制,相互间只有相对可控的小差异,就可以使用 instancing 合批。
+ +## 预览网格 + +鼠标悬停在 **Mesh** 属性中下拉的网格上时,可以在场景视图中对模型进行预览。 + +![view model](mesh-renderer/view-model.gif) + +[^1]: 注意目前使用 uniform 上传合批后的世界变换矩阵,考虑到 WebGL 标准的 uniform 数量限制,目前一批最多绘制 10 个模型,所以对大量同材质的模型,开启合批后 drawcall 数量预期最多会减少 10 倍。 +[^2]: 关于合批与性能的话题业界一直有不少探讨,比如可以参考 [这里](https://www.nvidia.com/docs/IO/8228/BatchBatchBatch.pdf) 的 slide。 diff --git a/versions/4.0/zh/engine/rendering/sorting-2d.md b/versions/4.0/zh/engine/rendering/sorting-2d.md new file mode 100644 index 0000000000..f5c1911541 --- /dev/null +++ b/versions/4.0/zh/engine/rendering/sorting-2d.md @@ -0,0 +1,80 @@ +# 2D 渲染排序组件 + +使用 Sorting2D 组件可以在不影响节点的原有层级关系时自定义 2D 渲染对象的渲染顺序,可以用于解决因为相邻渲染对象使用不同材质而导致的 DrawCall 增加问题。 + +![sorting2D-component](./sorting-2d/sorting2D-component.png) + +在 **属性检查器** 内点击 **添加组件** 按钮选择 **Sorting** 即可添加。 + +![sorting](sorting/sorting.png) + +## Sorting2D 属性介绍 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| sortingLayer | 设置渲染对象的排序层级 [**修改项目的排序层级配置**](../../editor/project/index.md#排序图层),层级越低的越先渲染。默认层级为 Default。 +| sortingOrder | 设置渲染对象在排序层级下的顺序。如果排序层级相同,则数值越小的越先渲染。如果数值相同,则按照节点层级越低越先渲染。 + +**注意**:当 2D 节点树递归收集渲染元素的过程中遇到 Mask 渲染器,会使用自定义排序规则渲染已记录的 2D UI,并重置渲染状态、重新收集渲染元素,等到收集完成时再继续使用自定义排序规则渲染其余 2D UI。 + +## 优化示例 + +1. 在项目中配置排序层级 + + ![](./sorting-2d/sorting2D-layers.png) + +2. 勾选 `项目设置 -> 功能裁剪` 中的 `2D 渲染排序` 模块 + + ![](./sorting-2d/sorting2D-open.png) + +3. 给 2D UI 节点添加 Sorting2D 组件,并设置排序层级和顺序。 + + ![](./sorting-2d/sorting2D-setProperty.png) + +3. 构建项目,运行并查看优化效果 + + * 优化前 + ![](./sorting-2d/sorting2D-no-optimize.jpg) + + * 优化后 + ![](./sorting-2d/sorting2D-optimized.jpg) + +## 在代码中修改 Sorting2D 组件的排序层级和顺序 + +```typescript +import { _decorator, Component, find, Node, settings, Sorting2D } from 'cc'; +const { ccclass, property } = _decorator; + +const sortingLayers = settings.querySettings("engine", "sortingLayers"); +const default_layer = sortingLayers[0].value; +const autoAtlas_1_layer = sortingLayers[1].value; +const autoAtlas_2_layer = sortingLayers[2].value; +const autoAtlas_1_1_layer = sortingLayers[3].value; +const label_layer = sortingLayers[4].value; + +@ccclass('NewComponent') +export class NewComponent extends Component { + + start() { + var testNode = find("Canvas/test"); + if (testNode) { + this.changeUISortingLayer(testNode, autoAtlas_1_layer, 0); + } + } + + changeUISortingLayer(sortingNode: Node, sortingLayer: number, sortingOrder?: number) { + if (sortingNode.getComponent(Sorting2D)) { + sortingNode.getComponent(Sorting2D).sortingLayer = sortingLayer; + if (sortingOrder !== undefined) { + sortingNode.getComponent(Sorting2D).sortingOrder = sortingOrder; + } + } + } +} +``` + +## API 参考与使用范例 + +[Sorting2D API](https://docs.cocos.com/creator/4.0/api/zh/class/Sorting2D) + +[Sorting2D Demo](https://github.com/cocos/cocos-test-projects/tree/v3.8.7/assets/cases/ui/other/sorting2D) \ No newline at end of file diff --git a/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-addComponent.jpg b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-addComponent.jpg new file mode 100644 index 0000000000..46a6bc74b3 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-addComponent.jpg differ diff --git a/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-component.png b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-component.png new file mode 100644 index 0000000000..cb0979af7c Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-component.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-layers.png b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-layers.png new file mode 100644 index 0000000000..81e7b4c9cc Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-layers.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-no-optimize.jpg b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-no-optimize.jpg new file mode 100644 index 0000000000..80f0933999 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-no-optimize.jpg differ diff --git a/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-open-en.jpg b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-open-en.jpg new file mode 100644 index 0000000000..95aae69f4a Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-open-en.jpg differ diff --git a/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-open.png b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-open.png new file mode 100644 index 0000000000..6d7d975207 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-open.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-optimized.jpg b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-optimized.jpg new file mode 100644 index 0000000000..f53624aaca Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-optimized.jpg differ diff --git a/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-setProperty.png b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-setProperty.png new file mode 100644 index 0000000000..e63c264b60 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting-2d/sorting2D-setProperty.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting.md b/versions/4.0/zh/engine/rendering/sorting.md new file mode 100644 index 0000000000..2b05dbf84a --- /dev/null +++ b/versions/4.0/zh/engine/rendering/sorting.md @@ -0,0 +1,87 @@ +# 3D 渲染排序组件 + +对于大部分渲染情况来说,默认的排序已经满足需求。但是实际上由于半透明物体的特殊性,我们可能需要手动对这些物体进行排序。对于这种情况,可以使用 **渲染排序组件**。 + +在 **属性检查器** 内点击 **添加组件** 按钮选择 **Sorting** 即可添加。 + +![sorting](sorting/sorting.png) + +> **注意**:该组件只对持有 **MeshRenderer** 和 **SpriteRenderer** 组件的节点生效。 + +## 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **Sorting Layer** | 选择 **排序图层**
通过下拉框选择不同的排序图层,这些图层需要在 **项目设置** -> **排序图层** 内设置后才可以选择
![sortring-layer](sorting/sorting-layer.png) | +| **Sorting Order** | 在相同图层内的排序优先级 | + +## 示例 + +- 在 **项目设置** -> **排序图层** 内添加排序图层 Sorting Layer 1。 + + ![setting](sorting/project-settings.png) + +- 按照示例中搭建如下的场景: + + ![default](sorting/default-sort.png) + +- 立方体和球体的材质需要选择使用半透明渲染队列: + + ![technique](sorting/tech.png) + + 添加 Sorting 组件且属性保持默认。 + +- 图片需要使用 **SpriteRenderer** 组件(注意不是 Sprite 组件),着色器需要使用 **builtin-sprite-renderer**,并添加 Sorting 组件,保持属性默认: + + ![layer1](sorting/sprite-renderer-layer.png) + +此时可观察到这些节点的排序方式是按照默认顺序排序。 + +### Sorting Layer 示例 + +调整后方 SpriteRenderer 节点的 Sorting Layer 如下图: + +![layer 2](sorting/sorting-layer1.png) + +由于自定义的层级是高于立方体和球体的层级 default 的,因此可以观察到下图的结果: + +![result](sorting/result-sorting-layer.png) + +### Sorting Order 示例 + +Sorting Order 属性可以调整同一个排序图层内物体渲染的优先级。 + +场景和上述的示例类似,但是将立方体的 Sorting 组件内的 Sorting Order 属性调整为 1: + +![order 1](sorting/sorting-order1.png) + +此时,立方体在渲染队列内的优先级是高于球体的,因此可以观察下图的结果: + +![result](sorting/sorting-order1-result.png) + +## 脚本示例 + +下面的代码演示了如何在运行时修改 Sorting Order 以及 Sorting Layer。 + +```ts +import { _decorator, Component, Sorting, SortingLayers } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('SortingSample') +export class SortingSample extends Component { + + @property(Sorting) + sorting:Sorting | null = null; + + start() { + this.sorting.sortingLayer = SortingLayers.getLayerIndexByName("Sorting Layer 1"); + this.sorting.sortingOrder = 1; + } +} +``` + +**排序层管理器(SortingLayers)** 可以在运行时获取排序图层的名称和索引,但无法在运行时修改。 + +## API + +渲染排序组件的 API 可以参考 [Sorting](%__APIDOC__%/zh/class/Sorting) 以及 [排序层管理器 SortingLayers](%__APIDOC__%/zh/class/SortingLayers) 。 diff --git a/versions/4.0/zh/engine/rendering/sorting/default-sort.png b/versions/4.0/zh/engine/rendering/sorting/default-sort.png new file mode 100644 index 0000000000..affb0e08e2 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/default-sort.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting/project-settings.png b/versions/4.0/zh/engine/rendering/sorting/project-settings.png new file mode 100644 index 0000000000..d1d0734fa1 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/project-settings.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting/result-sorting-layer.png b/versions/4.0/zh/engine/rendering/sorting/result-sorting-layer.png new file mode 100644 index 0000000000..747cd800a1 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/result-sorting-layer.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting/sorting-layer.png b/versions/4.0/zh/engine/rendering/sorting/sorting-layer.png new file mode 100644 index 0000000000..cd9572b379 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/sorting-layer.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting/sorting-layer1.png b/versions/4.0/zh/engine/rendering/sorting/sorting-layer1.png new file mode 100644 index 0000000000..b353823258 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/sorting-layer1.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting/sorting-order1-result.png b/versions/4.0/zh/engine/rendering/sorting/sorting-order1-result.png new file mode 100644 index 0000000000..df467f408c Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/sorting-order1-result.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting/sorting-order1.png b/versions/4.0/zh/engine/rendering/sorting/sorting-order1.png new file mode 100644 index 0000000000..d4b871182b Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/sorting-order1.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting/sorting.png b/versions/4.0/zh/engine/rendering/sorting/sorting.png new file mode 100644 index 0000000000..44415270cd Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/sorting.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting/sprite-renderer-layer.png b/versions/4.0/zh/engine/rendering/sorting/sprite-renderer-layer.png new file mode 100644 index 0000000000..0db65490b7 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/sprite-renderer-layer.png differ diff --git a/versions/4.0/zh/engine/rendering/sorting/tech.png b/versions/4.0/zh/engine/rendering/sorting/tech.png new file mode 100644 index 0000000000..dedcfba542 Binary files /dev/null and b/versions/4.0/zh/engine/rendering/sorting/tech.png differ diff --git a/versions/4.0/zh/engine/template/native-upgrade-to-v3.5.md b/versions/4.0/zh/engine/template/native-upgrade-to-v3.5.md new file mode 100644 index 0000000000..32b6c862a0 --- /dev/null +++ b/versions/4.0/zh/engine/template/native-upgrade-to-v3.5.md @@ -0,0 +1,155 @@ +# v3.5 已构建工程升级指南 + +从 v3.5 开始,Mac 和 Windows 平台的 `AppDelegate` 已移入引擎内部实现,可以通过重载 `AppDelegate` 的方式来兼容之前版本的用法;`game.cpp` 也进行了调整,已有工程需要重新构建进行升级。 + +## 工程升级 + +检查工程目录下 native/engine 目录是否存在。如果存在,需要删除文件夹,删除前需要做好备份(这个目录如果存在,重新构建时不会自动更新);不存在,则直接构建即可。 + +## 自定义代码迁移方法 + +之前在 AppDelegate 添加的代码,可以通过下文定制平台和 AppDelegate 进行升级;自定义的 game.cpp 可以通过接口更替即可升级。 + +### 平台与 AppDelegate 的定制方法 + +以 **Mac** 为例: + +1、自定义AppDelegate(参考文件名:MyAppdelegate.h,MyAppdelegate.mm) + +``` +@interface MyAppDelegate : NSObject + // 定义需要重写的方法 + - (void)applicationWillResignActive:(UIApplication *)application; +@end + +@implementation MyAppDelegate +- (void)applicationWillResignActive:(UIApplication *)application { + // 注意:调用父类的方法 + [super applicationWillResignActive:application] +} +@end +``` + +2、自定义平台(参考文件名:CustomMacPlatform.h) + +``` +#include "platform/BasePlatform.h" +#include "MyAppDelegate.h" + +class CustomMacPlatform : public MacPlatform { +public: + // 重写平台初始化方法 + int32_t init() override { + // 调用父类的方法 + return MacPlatform::init(); + } + // 这里进入 oc 的消息循环,直到程序退出 + int32_t run(int argc, const char** argv) { + id delegate = [[MyAppDelegate alloc] init]; + NSApplication.sharedApplication.delegate = delegate; + return NSApplicationMain(argc, argv); + } +} +``` + +3、加载自定义平台(参考文件名:main.mm) + +``` +#include "CustomMacPlatform.h" + +int main(int argc, const char * argv[]) { + CustomMacPlatform platform; + if (platform.init()) { + return -1; + } + return platform.run(argc, (const char**)argv); +} +``` + +### game.cpp 迁移方法 + +- 设置js加密秘钥:jsb_set_xxtea_key -> 设置 `_xxteaKey` 成员变量; 或 调用 `setXXTeaKey` +- 设置调试: jsb_enable_debugger -> 设置 `_debuggerInfo` 结构, 或 调用 `setDebugIpAndPort` +- 设置异常回调:setExceptionCallback -> 重写 `handleException` 接口 +- 运行自定义脚本:jsb_run_script -> 调用 `runScript` +- 可以通过使用 `engine` 来添加需要监听的事件, -> `getEngine()->addEventCallback(WINDOW_OSEVENT, eventCb);` +- 自定义的游戏 `CustomGame`,需要注册到引擎 `CC_REGISTER_APPLICATION(CustomGame)` 进行加载; +- `game` 继承于 `cc::BaseGame`, 而 `cc::BaseGame` 继承于 `CocosApplication`,因此可以重写部分实现,增加自定义逻辑; + +## Native 文件修改 + +- 替换引用的头文件:`#include "cocos/platform/Application.h"` —> `#include "application/ApplicationManager.h"` +- 使用方式变更:`cc::Application::getInstance()->getScheduler()` -> `CC_CURRENT_ENGINE()->getScheduler()` +- 有自定义 jsb 接口的情况:`native_ptr_to_seval` 替换为 `nativevalue_to_se` + +## Android 升级指南 + +### JAVA 修改 + +- **game/AppActivity.java** 以及 **game/InstantActivity.java** 的 `onCreate` 方法中删除如下代码: + + ```java + // Workaround in https://stackoverflow.com/questions/16283079/re-launch-of-activity-on-home-button-but-only-the-first-time/16447508 + if (!isTaskRoot()) { + // Android launched another instance of the root activity into an existing task + // so just quietly finish and go away, dropping the user back into the activity + // at the top of the stack (ie: the last state of this task) + // Don't need to finish it again since it's finished in super.onCreate . + return; + } + ``` + +- **app/AndroidManifest.xml** 执行下列操作: + - 删除 `application` 标签中的下列代码:`android:taskAffinity=""` + - 在 `application` 标签中增加下列代码:`android:exported="true"` + +- **app/build.gradle** 修改下列代码: + + ```html + "${RES_PATH}/assets" -> "${RES_PATH}/data" + ``` + +#### CMakeLists.txt 修改 + +- **android/CMakeLists.txt** + - LIB_NAME 变更为 CC_LIB_NAME + - PROJ_SOURCES 变更为 CC_PROJ_SOURCES + - 增加 set(CC_PROJECT_DIR ${CMAKE_CURRENT_LIST_DIR}) + - 增加 set(CC_COMMON_SOURCES) + - 增加 set(CC_ALL_SOURCES) + - 删除下列代码: + + ```cmake + ${CMAKE_CURRENT_LIST_DIR}/../common/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/../common/Classes/Game.cpp + + add_library(${LIB_NAME} SHARED ${PROJ_SOURCES}) + target_link_libraries(${LIB_NAME} + "-Wl,--whole-archive" cocos2d_jni "-Wl,--no-whole-archive" + cocos2d + ) + target_include_directories(${LIB_NAME} PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/../common/Classes + ) + ``` + + - 增加代码: + + ```cmake + cc_android_before_target(${CC_LIB_NAME}) + add_library(${CC_LIB_NAME} SHARED ${CC_ALL_SOURCES}) + # 此处添加用户依赖库 AAA target_link_libraries(${CC_LIB_NAME} AAA) + # 此处添加用户自定义文件 xxx/include target_include_directories(${CC_LIB_NAME} PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../common/Classes/xxx/include) + cc_android_after_target(${CC_LIB_NAME}) + ``` + +- **common/CMakeLists.txt** + - cocos2d-x-lite/ 修改为 engine/native/ + - 文件末尾增加代码: + + ```cmake + list(APPEND CC_COMMON_SOURCES + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.h + ${CMAKE_CURRENT_LIST_DIR}/Classes/Game.cpp + ) + ``` diff --git a/versions/4.0/zh/engine/template/native-upgrade-to-v3.6.md b/versions/4.0/zh/engine/template/native-upgrade-to-v3.6.md new file mode 100644 index 0000000000..2ed8139781 --- /dev/null +++ b/versions/4.0/zh/engine/template/native-upgrade-to-v3.6.md @@ -0,0 +1,68 @@ +# v3.5 已构建工程升级 v3.6 指南 + +本文将详细介绍 Cocos Creator 原生构建工程从 3.5.0 ~ 3.5.x 升级到 3.6 的注意事项。修改仅针对项目工程下的 native 目录。 + +## 构建 + +使用新版本的引擎打开旧版本的工程,待升级完成之后,构建目标平台。为避免升级失败,请先备份好工程,然后依据下列步骤进行升级。 + +## Android 平台 + +### 文件修改 + +- 删除文件:jni/main.cpp +- android/CMakeLists.txt:删除 `${CMAKE_CURRENT_LIST_DIR}/jni/main.cpp` + +### 编译修改 + +为减少包体大小,更改了 `CMAKE_C_FLAGS_RELEASE`、`CMAKE_CXX_FLAGS_RELEASE` 编译参数 `visibility` 的默认值:从 `default` 改成了 `hidden`。改完后 arm64-v8a 下的引擎动态库可以减少约 3.5M。针对这个修改,若 release 版本 `jni` 出现接口找不到,请先检查接口是否有添加 `JNIEXPORT` 的声明。例如: + +- 旧代码 + + ```c++ + void Java_com_google_android_games_paddleboat_GameControllerManager_onMouseConnected + ``` + +- 修改后的代码 + + ````c++ + JNIEXPORT void JNICALL Java_com_google_android_games_paddleboat_GameControllerManager_onMouseConnected + ```` + +## 代码修改 + +- 有自定义 jsb 接口的工程:须删除与 `NonRefNativePtrCreatedByCtorMap` 相关的代码 +- JSB 手动绑定的代码需要置空 `_finalize` 函数, 可参考[JSB 2.0 绑定教程 C++ 对象的生命周期管理](../../advanced-topics/JSB2.0-learning.md#cpp-命名空间namespace)。 + + 代码示例如下: + + ```c++ + static bool js_cc_gfx_Size_finalize(se::State& s) // NOLINT(readability-identifier-naming) + { + auto iter = se::NonRefNativePtrCreatedByCtorMap::find(SE_THIS_OBJECT(s)); + if (iter != se::NonRefNativePtrCreatedByCtorMap::end()) + { + se::NonRefNativePtrCreatedByCtorMap::erase(iter); + auto* cobj = SE_THIS_OBJECT(s); + JSB_FREE(cobj); + } + return true; + } + SE_BIND_FINALIZE_FUNC(js_cc_gfx_Size_finalize) + ``` + 改为 + ```c++ + static bool js_cc_gfx_Size_finalize(se::State& s) // NOLINT(readability-identifier-naming) + { + return true; + } + SE_BIND_FINALIZE_FUNC(js_cc_gfx_Size_finalize) + ``` + +## 脚本编写注意事项 + +由于 Native 引擎的实现和非 Native 的引擎在实现上略有差异,开发者须了解这些差异,现整理如下: + +- `Readonly`:在 Native 平台上,为减少内存使用,获取到的属性是新创建的对象。 + + 关于 `Readonly` 的说明可参考 [开发注意事项 - ReaOnly](../../scripting/readonly.md#readonly) diff --git a/versions/4.0/zh/geometry-renderer/enable-geometry-renderer.png b/versions/4.0/zh/geometry-renderer/enable-geometry-renderer.png new file mode 100644 index 0000000000..30942212ec Binary files /dev/null and b/versions/4.0/zh/geometry-renderer/enable-geometry-renderer.png differ diff --git a/versions/4.0/zh/geometry-renderer/geometry-renderer-demo.png b/versions/4.0/zh/geometry-renderer/geometry-renderer-demo.png new file mode 100644 index 0000000000..75b9530656 Binary files /dev/null and b/versions/4.0/zh/geometry-renderer/geometry-renderer-demo.png differ diff --git a/versions/4.0/zh/geometry-renderer/geometry-renderer-features.png b/versions/4.0/zh/geometry-renderer/geometry-renderer-features.png new file mode 100644 index 0000000000..fc5c233abd Binary files /dev/null and b/versions/4.0/zh/geometry-renderer/geometry-renderer-features.png differ diff --git a/versions/4.0/zh/geometry-renderer/index.md b/versions/4.0/zh/geometry-renderer/index.md new file mode 100644 index 0000000000..b6dddafa9d --- /dev/null +++ b/versions/4.0/zh/geometry-renderer/index.md @@ -0,0 +1,85 @@ +# 几何渲染器(Geometry-Renderer) + +几何渲染器是引擎提供的一种批量渲染各种几何体的功能接口,主要用于调试(比如显示物体的包围盒)及 Cocos Creator 的 gizmo 批量显示。 + +几何渲染器的效果展示如图: + +![geometry-renderer-demo](./geometry-renderer-demo.png) + +几何渲染器的功能特性如图: + +![geometry-renderer-features](./geometry-renderer-features.png) + +其中: +- solid:是否支持实心模式,如果不支持则显示线框模式 +- depth test:是否支持深度测试,如果支持则被遮挡部分半透明显示,未被遮挡部分不透明显示,如果不支持则全部不透明显示 +- lighting:是否支持简单光照,如果不支持就使用无光模式 +- transform:是否支持变换,如果支持,开发者可传入一个变换矩阵,变换矩阵会作用到几何体的顶点上,方便显示任意坐标空间的几何体 + +## 接口描述 + +几何渲染器位于 [geometry-render.ts](https://github.com/cocos/cocos4/blob/v4.0.0/cocos/rendering/geometry-renderer.ts) 和原生 [GeometryRenderer.h](https://github.com/cocos/cocos4/blob/v4.0.0/native/cocos/renderer/pipeline/GeometryRenderer.h) 内,开发者可根据需求查看。其支持的几何体描述如下: + +### 支持的几何体类型 + +| 类型 | 接口 | +|:--|:--| +| 虚线 | addDashedLine | +| 线段 | addLine | +| 三角形 | addTriangle | +| 四边形 | addQuad | +| 轴对齐包围盒 | addBoundingBox | +| 交叉点 | addCross | +| 视锥 | addFrustum | +| 胶囊体 | addCapsule | +| 圆柱 | addCylinder | +| 圆锥 | addCone | +| 圆形 | addCircle | +| 弧形 | addArc | +| 任意多边形 | addPolygon | +| 圆盘 | addDisc | +| 扇形 | addSector | +| 球体 | addSphere | +| 环面 | addTorus | +| 八面体 | addOctahedron | +| 贝塞尔曲线 | addBezier | +| 样条曲线,包含三种模式:折线段,多段贝塞尔曲线,Catmull-Rom 曲线 | addSpline | +| 网格线框 | addMesh | +| 基于索引的网格线框 | addIndexedMesh | + +### 使用方式 + +由于每帧渲染完这些几何体后会清空顶点缓存,所以需要在 update 等函数中,每帧往 geometry renderer 对象(位于 camera 中)添加几何体,除此之外不需要额外的操作。 + +在使用此功能时需要在 **项目设置** -> **功能剪裁** 内启用 **几何渲染器**: + +![enable geometry renderer](enable-geometry-renderer.png) + +> **注意**:请确保您的工程是基于 3D 的。 + +代码示例如下: + +```ts +import { _decorator, Component, Camera, Color } from 'cc'; +const { ccclass, property, executeInEditMode} = _decorator; + +@ccclass('Geometry') +@executeInEditMode(true) +export class Geometry extends Component { + + @property(Camera) + mainCamera:Camera = null; + + start() { + this.mainCamera?.camera.initGeometryRenderer(); + } + + update(deltaTime: number) { + this.mainCamera?.camera?.geometryRenderer?.addCircle(this.node.worldPosition, 1, Color.GREEN, 20); + } +} +``` + +运行结果如下: + +![result](result.png) diff --git a/versions/4.0/zh/geometry-renderer/result.png b/versions/4.0/zh/geometry-renderer/result.png new file mode 100644 index 0000000000..9e4deb3dd0 Binary files /dev/null and b/versions/4.0/zh/geometry-renderer/result.png differ diff --git a/versions/4.0/zh/getting-started/attention/index.md b/versions/4.0/zh/getting-started/attention/index.md new file mode 100644 index 0000000000..a63d0ec6f4 --- /dev/null +++ b/versions/4.0/zh/getting-started/attention/index.md @@ -0,0 +1,5 @@ +# 注意事项 + +## 语法格式 + +Cocos Creator 3.0 不同于 Cocos Creator v2.x 的语法格式,Cocos Creator 3.0 已全面支持 ES6 和 TS,因此只支持 ES6 和 TS 的 `class`。此外,我们还支持了 TS 的语法提示。针对 Cocos Creator 支持 ES5 语法的 `cc.Class` 形式已不再支持,请各位开发者悉知。 diff --git a/versions/4.0/zh/getting-started/dashboard/index.md b/versions/4.0/zh/getting-started/dashboard/index.md new file mode 100644 index 0000000000..cef30c871a --- /dev/null +++ b/versions/4.0/zh/getting-started/dashboard/index.md @@ -0,0 +1,101 @@ +# 使用 Dashboard + +启动 Cocos Dashboard 并使用 Cocos 开发者帐号登录以后,就会打开 Dashboard 窗口,在这里你可以下载引擎、新建项目、打开已有项目或者获得帮助信息。 + +![Dashboard](index/home.png) + +左侧导航菜单栏包括:**首页**、**项目**、**编辑器**、**商城**、**动态**、**学习**、**设置** 和 **账号**,具体说明请查看下文。 + +## 首页 + +首页集合 Cocos 的产品动态、版本动态以及用户最近打开的项目,可在此快捷打开项目、新建项目、安装编辑器、获取最新编辑器版本等。 + +![Dashboard](index/home1.png) + +主要包括以下三部分内容: + +- 轮播图:展示 Cocos 最近的产品动态; +- 最近打开:展示用户最近打开的项目,**双击** 可打开项目;右上方的 **新建项目** 按钮可快捷打开新建项目页以创建项目,**查看更多** 按钮可前往项目列表页面。 +- 编辑器:展示 Cocos 已发布的产品,可在此快捷安装编辑器版本、新建项目,以及获取最新发布的编辑器版本。 + +## 项目 + +项目页展示所有创建/导入的项目,**双击** 可打开项目;第一次运行 Cocos Dashboard 时列表是空的,可以在右上角新建/导入项目; + +![Dashboard](index/project.png) + +右上方的 **导入项目** 按钮用于导入其他本地项目;**新建项目** 按钮用于新建项目,点击该按钮会打开 **新建项目** 页面,详情可参考下方介绍。 + +项目列表支持以下功能: + +- **1**:通过项目名称直接搜索项目; +- **2**:可根据最近打开时间正序/反序排列项目; +- **3**:可选择其他已安装成功的编辑器版本打开项目; +- **4**:操作菜单,也可通过右键打开。支持 **打开(项目)**、**在 Finder/资源管理器中显示**、**从列表中移除**、**选择项目的图标**、**重命名项目**、**设置项目的描述**: + - **选择项目的图标**:自定义项目封面图,目前支持 BMP、PNG、GIF、JPG 四种格式; + - **重命名项目**:重命名项目名称,同时也将重命名项目文件夹; + - **设置项目的描述**:可用于备注项目相关的描述信息。 + +### 新建项目 + +Creator 提供了一些项目模板,包括了各种不同类型的游戏基本架构,以及学习用的范例资源和脚本,来帮助开发者更快进入到创造性的工作当中。随着 Cocos Creator 功能越来越完整,我们也会持续添加更多的项目模板。 + +点击左上方的 **模版** 和 **案例** 可切换项目类型;**编辑器版本** 用于选择创建项目的编辑器版本,下拉框中包括所有已安装成功的版本。 + +![Dashboard](index/add-project.png) + +选中任一项目模板,页面下方可以看到 **项目名称** 和 **位置**: + +- **项目名称**:项目名称只能包含 **a-z**、**A-Z**、**0-9** 以及 **_** 和 **-**; +- **位置**:点击项目路径输入框后面的图标即可选择项目的存放路径,也可以在输入框中手动输入。 + +一切都设置好后,点击 **创建并打开** 按钮来完成项目的创建;Dashboard 窗口会被隐藏,然后新创建的项目会在 Cocos Creator 编辑器主窗口中打开。 + +## 安装 + +安装页展示已安装成功的产品编辑器,可以点击右上方的按钮导入/安装编辑器版本; + +![Dashboard](index/installs.png) + +需要注意的是,第一次运行 Cocos Dashboard 时,这个列表是空的,可以点击右上方的两个按钮导入本地已有的编辑器或者直接下载安装: + +- **添加本地版本**:用于添加本地已有的编辑器,支持直接将本地已有的 Creator 编辑器从操作系统的文件管理器拖拽到版本列表中; +- **安装编辑器**:点击该按钮会打开编辑器安装页,显示所有已安装和未安装的编辑器版本,可根据需要下载安装: + + ![Download](index/download.png) + +## 商城 + +Cocos 游戏开发资源商城,可浏览、下载和安装官方或者第三方插件、源码、资源等,具体说明可参考 [Cocos Store](https://store.cocos.com/document/zh/)。 + +![store](index/store.png) + +## 动态 + +动态页主要展示 Cocos Creator 的一些官方信息、活动和 Creator 版本更新说明等,包括 **新闻** 和 **更新日志** 两个模块。 + +![community](index/community.png) + +## 学习 + +Cocos 官方推荐的优质教程和精华文章。 + +![learn](index/learn.png) + +## 账号 + +展示当前登录的账号信息,可前往账户中心,以及退出登录等。 + +![account](index/account.png) + +点击下方各网站的图标可访问 Cocos 官方获取更多教程等信息,目前包括: + +- Cocos 引擎官方微信公众号 + + ![Cocos](../support/cocos.jpg) + +- [微博](https://weibo.com/cocos2dx) +- [Twitter](https://twitter.com/CocosEngine) +- [哔哩哔哩](https://space.bilibili.com/491120849) +- [YouTube](https://www.youtube.com/cocosengine) +- [GitHub](https://github.com/cocos/cocos4) diff --git a/versions/4.0/zh/getting-started/dashboard/index/account.png b/versions/4.0/zh/getting-started/dashboard/index/account.png new file mode 100644 index 0000000000..50f92e2c03 Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/account.png differ diff --git a/versions/4.0/zh/getting-started/dashboard/index/add-project.png b/versions/4.0/zh/getting-started/dashboard/index/add-project.png new file mode 100644 index 0000000000..8ab0e3edde Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/add-project.png differ diff --git a/versions/4.0/zh/getting-started/dashboard/index/community.png b/versions/4.0/zh/getting-started/dashboard/index/community.png new file mode 100644 index 0000000000..ad9686643d Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/community.png differ diff --git a/versions/4.0/zh/getting-started/dashboard/index/download.png b/versions/4.0/zh/getting-started/dashboard/index/download.png new file mode 100644 index 0000000000..19849483d4 Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/download.png differ diff --git a/versions/4.0/zh/getting-started/dashboard/index/home.png b/versions/4.0/zh/getting-started/dashboard/index/home.png new file mode 100644 index 0000000000..47f9fd587d Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/home.png differ diff --git a/versions/4.0/zh/getting-started/dashboard/index/home1.png b/versions/4.0/zh/getting-started/dashboard/index/home1.png new file mode 100644 index 0000000000..26c8c3b62a Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/home1.png differ diff --git a/versions/4.0/zh/getting-started/dashboard/index/installs.png b/versions/4.0/zh/getting-started/dashboard/index/installs.png new file mode 100644 index 0000000000..9fa220c6d6 Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/installs.png differ diff --git a/versions/4.0/zh/getting-started/dashboard/index/learn.png b/versions/4.0/zh/getting-started/dashboard/index/learn.png new file mode 100644 index 0000000000..10c1baa49b Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/learn.png differ diff --git a/versions/4.0/zh/getting-started/dashboard/index/project.png b/versions/4.0/zh/getting-started/dashboard/index/project.png new file mode 100644 index 0000000000..2beeae0f8a Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/project.png differ diff --git a/versions/4.0/zh/getting-started/dashboard/index/store.png b/versions/4.0/zh/getting-started/dashboard/index/store.png new file mode 100644 index 0000000000..2fb9e56eb1 Binary files /dev/null and b/versions/4.0/zh/getting-started/dashboard/index/store.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/40-on-y.png b/versions/4.0/zh/getting-started/first-game-2d/images/40-on-y.png new file mode 100644 index 0000000000..1527286f03 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/40-on-y.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/add-animation.png b/versions/4.0/zh/getting-started/first-game-2d/images/add-animation.png new file mode 100644 index 0000000000..b9a85fb04e Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/add-animation.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/add-keyframes.gif b/versions/4.0/zh/getting-started/first-game-2d/images/add-keyframes.gif new file mode 100644 index 0000000000..959649c47d Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/add-keyframes.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/add-player-controller.gif b/versions/4.0/zh/getting-started/first-game-2d/images/add-player-controller.gif new file mode 100644 index 0000000000..44566e934b Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/add-player-controller.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/add-position-track.png b/versions/4.0/zh/getting-started/first-game-2d/images/add-position-track.png new file mode 100644 index 0000000000..9d3c20c6dc Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/add-position-track.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/after-layer-setting.gif b/versions/4.0/zh/getting-started/first-game-2d/images/after-layer-setting.gif new file mode 100644 index 0000000000..04814334f5 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/after-layer-setting.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/assign-body-anim.gif b/versions/4.0/zh/getting-started/first-game-2d/images/assign-body-anim.gif new file mode 100644 index 0000000000..04e0bc4040 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/assign-body-anim.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/assign-box-prefab.gif b/versions/4.0/zh/getting-started/first-game-2d/images/assign-box-prefab.gif new file mode 100644 index 0000000000..89467804fb Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/assign-box-prefab.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/assign-clip.gif b/versions/4.0/zh/getting-started/first-game-2d/images/assign-clip.gif new file mode 100644 index 0000000000..ec4eaf5721 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/assign-clip.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/bind-manager.png b/versions/4.0/zh/getting-started/first-game-2d/images/bind-manager.png new file mode 100644 index 0000000000..c1346f519e Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/bind-manager.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/box-layer.png b/versions/4.0/zh/getting-started/first-game-2d/images/box-layer.png new file mode 100644 index 0000000000..4e33b59b69 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/box-layer.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/cavans-camera.png b/versions/4.0/zh/getting-started/first-game-2d/images/cavans-camera.png new file mode 100644 index 0000000000..38c451d8b8 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/cavans-camera.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/click-event.gif b/versions/4.0/zh/getting-started/first-game-2d/images/click-event.gif new file mode 100644 index 0000000000..dfa7beb7d9 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/click-event.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/click-event.gif.gif b/versions/4.0/zh/getting-started/first-game-2d/images/click-event.gif.gif new file mode 100644 index 0000000000..f2dbd6faae Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/click-event.gif.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-2d-empty.png b/versions/4.0/zh/getting-started/first-game-2d/images/create-2d-empty.png new file mode 100644 index 0000000000..5fd75021d1 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-2d-empty.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-bg.gif b/versions/4.0/zh/getting-started/first-game-2d/images/create-bg.gif new file mode 100644 index 0000000000..224aeeda9e Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-bg.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-body.gif b/versions/4.0/zh/getting-started/first-game-2d/images/create-body.gif new file mode 100644 index 0000000000..ef9fcbd914 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-body.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-box-prefab.gif b/versions/4.0/zh/getting-started/first-game-2d/images/create-box-prefab.gif new file mode 100644 index 0000000000..b9219f67c4 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-box-prefab.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-box.png b/versions/4.0/zh/getting-started/first-game-2d/images/create-box.png new file mode 100644 index 0000000000..e5c77f69a1 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-box.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-clip-onestep.gif b/versions/4.0/zh/getting-started/first-game-2d/images/create-clip-onestep.gif new file mode 100644 index 0000000000..3dab75afff Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-clip-onestep.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-fist-script.png b/versions/4.0/zh/getting-started/first-game-2d/images/create-fist-script.png new file mode 100644 index 0000000000..017cad1339 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-fist-script.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-game-manager.png b/versions/4.0/zh/getting-started/first-game-2d/images/create-game-manager.png new file mode 100644 index 0000000000..cf4bc1bab1 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-game-manager.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-player.gif b/versions/4.0/zh/getting-started/first-game-2d/images/create-player.gif new file mode 100644 index 0000000000..1e093cf010 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-player.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-scripts.gif b/versions/4.0/zh/getting-started/first-game-2d/images/create-scripts.gif new file mode 100644 index 0000000000..bc4388c7c0 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-scripts.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-sprite.gif b/versions/4.0/zh/getting-started/first-game-2d/images/create-sprite.gif new file mode 100644 index 0000000000..d2a1169d39 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-sprite.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-sprite.png b/versions/4.0/zh/getting-started/first-game-2d/images/create-sprite.png new file mode 100644 index 0000000000..556d5e9b98 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-sprite.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-start-menu.png b/versions/4.0/zh/getting-started/first-game-2d/images/create-start-menu.png new file mode 100644 index 0000000000..223d097f78 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-start-menu.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-tip.png b/versions/4.0/zh/getting-started/first-game-2d/images/create-tip.png new file mode 100644 index 0000000000..6fe17d3f8d Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-tip.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-title.png b/versions/4.0/zh/getting-started/first-game-2d/images/create-title.png new file mode 100644 index 0000000000..f92e851d1e Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-title.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-twostep.gif b/versions/4.0/zh/getting-started/first-game-2d/images/create-twostep.gif new file mode 100644 index 0000000000..8ef64b1d2b Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-twostep.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/create-ui-canvas.png b/versions/4.0/zh/getting-started/first-game-2d/images/create-ui-canvas.png new file mode 100644 index 0000000000..253fdb4094 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/create-ui-canvas.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/download-editor.png b/versions/4.0/zh/getting-started/first-game-2d/images/download-editor.png new file mode 100644 index 0000000000..da4b9b54ac Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/download-editor.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/enter-anim-editing-mode.png b/versions/4.0/zh/getting-started/first-game-2d/images/enter-anim-editing-mode.png new file mode 100644 index 0000000000..d883e38f51 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/enter-anim-editing-mode.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/gen-road.png b/versions/4.0/zh/getting-started/first-game-2d/images/gen-road.png new file mode 100644 index 0000000000..ffb0698839 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/gen-road.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/jumptime-with-duration.gif b/versions/4.0/zh/getting-started/first-game-2d/images/jumptime-with-duration.gif new file mode 100644 index 0000000000..daac617871 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/jumptime-with-duration.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/layer-default.png b/versions/4.0/zh/getting-started/first-game-2d/images/layer-default.png new file mode 100644 index 0000000000..3309f5e20e Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/layer-default.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/layer-error.png b/versions/4.0/zh/getting-started/first-game-2d/images/layer-error.png new file mode 100644 index 0000000000..030f6d4113 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/layer-error.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/preview-anim.gif b/versions/4.0/zh/getting-started/first-game-2d/images/preview-anim.gif new file mode 100644 index 0000000000..638dfae475 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/preview-anim.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/preview-menu.png b/versions/4.0/zh/getting-started/first-game-2d/images/preview-menu.png new file mode 100644 index 0000000000..720237c7ea Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/preview-menu.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/preview-oneStep.gif b/versions/4.0/zh/getting-started/first-game-2d/images/preview-oneStep.gif new file mode 100644 index 0000000000..7b0d6242f6 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/preview-oneStep.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/projects.png b/versions/4.0/zh/getting-started/first-game-2d/images/projects.png new file mode 100644 index 0000000000..67d24d345e Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/projects.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/save-prefab.png b/versions/4.0/zh/getting-started/first-game-2d/images/save-prefab.png new file mode 100644 index 0000000000..96a1aa8e7b Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/save-prefab.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/save-scene.png b/versions/4.0/zh/getting-started/first-game-2d/images/save-scene.png new file mode 100644 index 0000000000..13cc9541a1 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/save-scene.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/saved-scene.png b/versions/4.0/zh/getting-started/first-game-2d/images/saved-scene.png new file mode 100644 index 0000000000..046f908f42 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/saved-scene.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/scene-dir.png b/versions/4.0/zh/getting-started/first-game-2d/images/scene-dir.png new file mode 100644 index 0000000000..2adf590e57 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/scene-dir.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/scene.png b/versions/4.0/zh/getting-started/first-game-2d/images/scene.png new file mode 100644 index 0000000000..25ae7f80b0 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/scene.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/scroll.gif b/versions/4.0/zh/getting-started/first-game-2d/images/scroll.gif new file mode 100644 index 0000000000..4fae1fe790 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/scroll.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/setup-scroll.gif b/versions/4.0/zh/getting-started/first-game-2d/images/setup-scroll.gif new file mode 100644 index 0000000000..37db457cca Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/setup-scroll.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/start-game-without-result.gif b/versions/4.0/zh/getting-started/first-game-2d/images/start-game-without-result.gif new file mode 100644 index 0000000000..4d504eb771 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/start-game-without-result.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/step.png b/versions/4.0/zh/getting-started/first-game-2d/images/step.png new file mode 100644 index 0000000000..e027d29b62 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/step.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/to-red.png b/versions/4.0/zh/getting-started/first-game-2d/images/to-red.png new file mode 100644 index 0000000000..fd01b478b1 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/to-red.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/ui-canvas.png b/versions/4.0/zh/getting-started/first-game-2d/images/ui-canvas.png new file mode 100644 index 0000000000..c58ab38791 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/ui-canvas.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/uicanvas-camera.png b/versions/4.0/zh/getting-started/first-game-2d/images/uicanvas-camera.png new file mode 100644 index 0000000000..2fdb71eff7 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/uicanvas-camera.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/with-scale.gif b/versions/4.0/zh/getting-started/first-game-2d/images/with-scale.gif new file mode 100644 index 0000000000..ed7408564d Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/with-scale.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/images/without-scale.gif b/versions/4.0/zh/getting-started/first-game-2d/images/without-scale.gif new file mode 100644 index 0000000000..e0935b59f1 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/images/without-scale.gif differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/index.md b/versions/4.0/zh/getting-started/first-game-2d/index.md new file mode 100644 index 0000000000..b6e9da74a4 --- /dev/null +++ b/versions/4.0/zh/getting-started/first-game-2d/index.md @@ -0,0 +1,1481 @@ +# 快速上手:制作第一个 2D 游戏 + +平台跳跃类游戏是非常常见且热门的游戏类型,从最初的红白机到现代用复杂的 3D 技术制作,平台跳跃在各种游戏平台上层出不穷。 + +本节我们将使用 Cocos Creator 的 2D 特性,制作一款简单的平台跳跃类游戏。 + +## 搭建环境 + +### 下载编辑器 + +访问 [Cocos Creator 官网](https://www.cocos.com/creator-download) 下载最新的 Cocos Dashboard 即可以对引擎的版本、项目进行统一的管理,安装完成后打开 Dashboard。 + +![dashboard](images/projects.png) + +### 安装引擎 + +通常我们建议使用最新发布的引擎进行开发,以获取更多的特性和支持。 + +![download-editor](images/download-editor.png) + +在 **编辑器** 分页中,点击不同版本的安装按钮可安装编辑器。 + +### 创建工程 + +通过 **项目** 分页,找到 **新建** 按钮选择新建一个项目。 + +![craete-2d-project](images/create-2d-empty.png) + +Cocos Creator 提供不同类型的模板和示例,您可以点击 **模板** 以及 **示例** 的分页来选择不同的项目类型,本节我们选择创建一个空的 2D 项目(Empty(2D))。 + +接下来就在上述图片中标红的地方输入你的项目名字就可以了。 + +这里我输入的是 :cocos-turtorial-mind-your-step-2d,让我们做一个和 [快速上手:制作第一个 3D 游戏](../first-game/index.md) 一样游戏。 + +如果您没有阅读 [快速上手:制作第一个 3D 游戏](../first-game-2d/index.md) 也没有关系,本节我们依旧假定您没有使用过 Cocos Creator,让我们从 0 开始学下 Cocos Creator 吧! + +## 创建主角 + +在 2D 游戏中,通常我们会使用图片来作为主角,Cocos Creator 提供了一系列内置的 UI 图片,您可以在 `internal/default_ui/` 这个目录中找到它们,也可以随意的在您的项目中使用。通常我们在制作原型时,会采用这些图片。 + +在 **层级管理** 中点击右键,可以创建不同的节点类型,请尝试创建一个 Sprite 类型的节点: + +![create-sprite.png](images/create-sprite.png) + +接下来我们找到内置的 `default_btn_normal` 资源并将其赋予给 Sprite 的 Sprite Frame 属性 + +![craete-sprite.gif](images/create-sprite.gif) + +接下来再创建一个 **空的** 的节点,并将其名字命名为 'Player': + +![create-player.gif](images/create-player.gif) + +如果您没有在创建时输入节点的名字,仍然有两种修改名字的方式: + +- 在 **属性检查器** 上找到节点的名字并修改它 +- 在 **层级管理器** 中选中该节点并按下 F2 + +我们可以通过拖拽 **层级管理器** 中的节点,来调整节点的父子关系,这里将 Sprite 节点,拖拽到 Player 节点的子级,并修改 Sprite 的名字为 Body。 + +![create-body.gif](./images/create-body.gif) + +> **注意**: +> - 层级关系决定了显示的关系,如果随意调整,可能无法被观察到 +> - 2D/UI 元素必须在 Canvas 下才可以被看到,请务必确保您的 2D/UI 元素在 Canvas 层级下。 + +接下来将 Body 节点的 Y 坐标调整到 40: + +![40-on-y.png](images/40-on-y.png) + +调整 Body 的颜色,在 **属性检查器** 上找到 **Color** 属性并展开,将颜色调整为红色: + +![to-red.png](images/to-red.png) + +## 创建第一个脚本 + +通常游戏都是由不同的脚本驱动的,Cocos Creator 使用的是名为 TypeScript 的语言,它是由微软开发,拥有简单易学的语法,强大的类型检测以及广泛的应用。 您可以在网页开发、应用开发以及游戏开发中遇到它的身影。 + +在 Cocos Creator 中创建脚本是非常简单的,您只需要在 **资源管理器** 窗口上点击右键,选择 **创建** 并点击 **脚本** 项即可。 + + + +通常我们会选择创建一个新的目录来存放这些脚本,接下来我们将创建一个名为 'Scripts' 的目录并新建一个名为 `PlayerController` 的脚本用于控制角色: + +![create-scripts.gif](images/create-scripts.gif) + +这样由引擎模板创建的脚本为组件,它的代码如下: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('PlayerController') +export class PlayerController extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} +``` + +**组件** 必须要挂载在某个节点上才会生效,因此尝试将 PlayerController 脚本拖拽到 Player 节点的 **属性检查器上**: + +![add-player-controller.gif](images/add-player-controller.gif) + +> 您也可以点击 **Add Component** 按钮来添加不同的组件。 +> 由于 `Node` 这个类名在 TypeScript 内置库内也有同名的类,因此需要注意在导入时需要确保导入的是 `cc` 命名空间下的 `Node`,代码示例如下: +> `import { _decorator, Component, Node } from 'cc'` + +## 制作地图 + +在 2D 游戏里面,地图同样的也可以用图片来代替。实际上,在 2D 游戏里面绝大多数的可见物都可以用图像来描述。这也是 2D 游戏比 3D 游戏简单的地方,所以通常最开始学习时,我们可以考虑从 2D 部分开始。 + +我们根据上述创建角色 Body 的步骤创建一个地图块,并将其命名为 Box,并使其大小和角色一致。 + +- 在 **层级管理器** 里面点击右键创建一个新的精灵(Sprite)节点并选择将 **Sprite Frame** 属性配置为 default_btn_normal +- 修改其名字为 Box + + ![create-box.png](images/create-box.png) + +### 预制体 + +预制体是引擎的一种特殊资源,它可以将节点作为一种资源持久化的保存在 **资源管理器** 里面,这样就可以复用到其他情景。 + +制作预制体的方法也比较简单,我们只需找到刚刚制作的 Box 节点,拖拽它到 **资源管理器** 里面。 + +场景内的 Box 节点,运行游戏之前可以将它删除。 + +![create-box-prefab.gif](images/create-box-prefab.gif) + +> 一般来说,我们会用不同的目录来存放不同类型的资源,保持您的工程目录干净整洁是非常好的习惯! + +### 保存场景 + +引擎必须要一个场景才可以正常运行,目前我们编辑的场景是未经保存的,在 **资源管理器** 里面创建一个名为 Scene 的目录用于保存场景: + +![scene-dir.png](images/scene-dir.png) + +按下 Ctrl + S,在首次保存场景时会弹出保存的界面,之后我们输入 game,并将其保存在 Scene 目录下: + +![save-scene.png](images/save-scene.png) + +此时场景就保存完毕,我们可以在 **资源管理器** 内看到场景资源,以后任何的修改都可以通过按下 Ctrl + S 来保存到 game 这个场景内。 + +![saved-scene.png](images/saved-scene.png) + +此时就可以观察到整个场景的状态,红色用于代表玩家而白色代表地面的地块。 + +![scene.png](images/scene.png) + +> 记得随时保存你的场景,以避免在断电或不可预知的情况下的内容丢失。 + +## 完善角色 + +虽然我们角色已经制作好了,但是它完全不能动起来,也没有任何代码可以驱动它。因此我们接下来将从这两个方 +面努力去完善角色。 + +### 让角色动起来 + +对于角色,我们的策略是: + +- 当前鼠标被按下时,角色开始跳跃 +- 当角色跳跃一定的时间后,结束跳跃过程 + +因此我们可以在脚本中添加一些方法,用于完善角色的行为: + +- 监听鼠标输入 + + ```ts + onMouseUp(event: EventMouse) {} + ``` + +- 根据步数跳跃: + + ```ts + jumpByStep(step: number) {} + ``` + +- 根据每次的更新来计算角色最新的位置: + + ```ts + update (deltaTime: number) {} + ``` + +接下来我们来完善这些方法: + +#### 监听输入 + +Cocos Creator 支持鼠标、键盘、触摸以及游戏手柄等硬件,并将其封装在了 `input` 这个类里面,我们可以通过如下的代码来监听输入: + +```ts +start () { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); +} +``` + +> `input` 和 `Input` 是实例和类型的区别。 + +上述代码将监听鼠标弹起的事件并调用 `onMouseUp` 这个方法。 + +在 `onMouseUp` 这个方法内,我们通过判断鼠标是左键还是右键被按下,来确定要跳几步: + +```ts +onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } else if (event.getButton() === 2) { + this.jumpByStep(2); + } +} +``` + +`getButton` 方法会在鼠标左键被按下时返回 0,而右键则是 2。 + +#### 移动角色 + +对于大多数游戏角色来说,动起来的概念就是将其位置发生变化,对于匀速移动的物体,它移动后的位置应该是如下描述的: + +```txt +P_1 = P_0 + v*t +``` + +> 也就是 最终位置 = 当前位置 + 平均速度 * 时间间隔 + +因此我们可以通过计算上一次物体的位置,再加上速度和时间的乘积即可。而时间间隔我们采用 `update` 方法里面的 `deltaTime` 参数。 + +```ts +update (deltaTime: number) {} +``` + +> `update` 方法会被引擎以一定的时间间隔调用,比如帧率为 30 每秒时,则每秒会调用 `update` 30 次,这个方法的作用是为了能够通过特定的时间间隔来尽量模拟现实中时间连续的现象。 + +这里我们整理下角色移动所需要的一些信息: + +- 是否开始跳跃: `_startJump`,用于判断角色是否在跳跃状态 +- 跳跃步数:一步或者两步 `_jumpStep`,用于记录鼠标的输入,并将其转化为数值。因为我们规定角色最多只能跳两步,那么它可能是 1 或者 2。 +- 跳跃时间:`_jumpTime`,这个数值类型的变量用于记录整个跳跃的时长 +- 当前的跳跃时间:`_curJumpTime`,每次跳跃前,将这个值置为 0,在更新时进行累计并和 `_jumpTime` 进行对比,如果超过了 `_jumpTime`,那么我们认为角色完成了一次完整的跳跃 +- 移动速度:`_curJumpSpeed`,用于记录跳跃时的移动速度 +- 当前的位置:`_curPos`,记录和计算角色的当前位置 +- 位移: `_deltaPos`,每一帧我们都需要记录下位置和时间间隔的乘积,我们将用它来存储计算结果 +- 目标位置:`_targetPos`,最终的落点,我们将在跳跃结束时将角色移动这个位置以确保最终的位置正确,这样可以处理掉某些误差的情况 + +在 PlayerController 中添加上述的属性: + +```ts +private _startJump: boolean = false; +private _jumpStep: number = 0; +private _curJumpTime: number = 0; +private _jumpTime: number = 0.1; +private _curJumpSpeed: number = 0; +private _curPos: Vec3 = new Vec3(); +private _deltaPos: Vec3 = new Vec3(0, 0, 0); +private _targetPos: Vec3 = new Vec3(); +``` + +那么我们要做的事情很容易这么做: + +- 在 `jumpByStep` 里面计算出角色要移动所必须的信息 +- 在 `update` 里面执行角色运动的行为 + +那么代码就可以填充为: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; // 标记开始跳跃 + this._jumpStep = step; // 跳跃的步数 1 或者 2 + this._curJumpTime = 0; // 重置开始跳跃的时间 + this._curJumpSpeed = this._jumpStep / this._jumpTime; // 根据时间计算出速度 + this.node.getPosition(this._curPos); // 获取角色当前的位置 + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); // 计算出目标位置 +} +``` + +Vec3 是 三维矢量 Vector3 的缩写,这个类会提供三维矢量的存储和一些计算的方法。其中 `Vec3.add` 是它提供的静态方法,用于计算两个向量相加,并将结果存储在第一个参数 `_targetPos` 里面。 + +不是 2D 游戏吗?为什么要操作 Vector3。虽然我们在编辑器看到的位置信息都是 2D 的但是在引擎中的计算都是实际上以 3D 为基础的,因此在计算是都会采用三维矢量作为运算位置的基础。 + +接下来将计算在跳跃状态下,角色的移动,非跳跃状态我们什么都不做保持静止就可以: + +```ts +update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; // 累计总的跳跃时间 + if (this._curJumpTime > this._jumpTime) { // 当跳跃时间是否结束 + // end + this.node.setPosition(this._targetPos); // 强制位置到终点 + this._startJump = false; // 清理跳跃标记 + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; //每一帧根据速度和时间计算位移 + Vec3.add(this._curPos, this._curPos, this._deltaPos); // 应用这个位移 + this.node.setPosition(this._curPos); // 将位移设置给角色 + } + } +} +``` + +此时如果点击 ![preview-menu.png](images/preview-menu.png) 已经可以看到角色的运动了。 + +![without-scale.gif](images/without-scale.gif) + +需要注意一点,在 2D 世界里面,如果位移一个单位,那么这个位置不会很明显,这是因为我们的 Cavans 设定为 960 x 640, 因此横向移动 1 个单位,它相当于移动 Canvas 的 1/960。 + +因此我们要对移动的单位进行放大,这里可以在 PlayerController 上面添加一个用于记录放大比的常量: + +```ts + +export const BLOCK_SIZE = 40; // 添加一个放大比 + +@ccclass("PlayerController") +// 其他代码略 +``` + +注意这里我们添加了一个常量 `BLOCK_SIZE` 并使其等于 40 和角色以及方块的大小一致。 + +将 `jumpByStep` 修改为: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); +} +``` + +再次启动游戏可以看到正常的移动速度了: + +![with-scale.gif](images/with-scale.gif) + +此时 `PlayerController` 代码如下: + +```ts +import { _decorator, Component, Vec3, EventMouse, input, Input } from "cc"; +const { ccclass, property } = _decorator; + +export const BLOCK_SIZE = 40; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + private _startJump: boolean = false; + private _jumpStep: number = 0; + private _curJumpTime: number = 0; + private _jumpTime: number = 0.1; + private _curJumpSpeed: number = 0; + private _curPos: Vec3 = new Vec3(); + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + private _targetPos: Vec3 = new Vec3(); + + start () { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + reset() { + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); + } + + update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` + +### 制作动画 + +Cocos Creator 支持多种动画效果,比如常见的关键帧动画、Spine 以及龙骨等动画格式。 + +通常我们在制作 2D 动画时,有几种办法: + +- 关键帧动画:通过引擎制作,常用于如 UI 动画、序列帧动画等 +- 骨骼动画:通过第三方 2D 动作制作工具导出并使用 + +本教程中我们会使用关键帧动画来制作角色的跳跃效果。 + +首先在角色的 Body 节点上,增加一个 Animation 的组件: + +![add-animation.png](images/add-animation.png) + +在 **资源管理器** 内新建 Animation 的目录,并创建一个名为 oneStep 的动画剪辑。 + +![create-clip-onestep.gif](images/create-clip-onestep.gif) + +在 **层级管理器** 里面选中 Body 节点,并将 oneStep 拖拽到 **Clips** 属性上: + +![assign-clip.gif](images/assign-clip.gif) + +在编辑器下方控制台处切换到 **动画** 分页并点击下方的 **进入编辑模式** 按钮: + +![enter-anim-editing-mode.png](images/enter-anim-editing-mode.png) + +在动画编辑器里面,可以添加不同的动画轨道。 + +![add-position-track.png](images/add-position-track.png) + +添加完成 postion 这个轨道以后,就可以添加不同的关键帧,添加方式也比较简单,我们可以在编辑模式下,只要在场景中或者属性检查器内修改物体的位置,此时如果动画轨道上没有关键帧,则会在轨道上添加一个新的关键帧。 + +这里我们将指向当前帧的指针拖拽到不同位置,并改变物体的位置,此时就会创建新的关键帧。 + +![add-keyframes.gif](images/add-keyframes.gif) + +布局下列的关键帧: + +- 0 帧:位置信息为:[0,40] +- 10 帧: 位置信息为:[0,120] +- 20 帧: 位置信息为:[0,40] + +> 记得点击 **保存** 按钮对动画剪辑进行保存。 + +可以通过点击 **播放** 按钮在场景中预览动画。 + +![preview-oneStep.gif](images/preview-oneStep.gif) + +参考 oneStep 动画的制作过程,制作 twoStep 动画。 + +![create-twostep.gif](images/create-twostep.gif) + +### 播放动画 + +在制作好动画之后,我们可以驱动 PlayerController 来播放动画,播放动画的代码很简单: + +```ts +animation.play('oneStep'); +``` + +- animation 是 Body 动画的动画组件的 ‘引用’。 +- play 指的是播放动画的方法,它的参数是我们之前创建好的 oneStep 这个动画剪辑,在 Cocos Creator 中,如果要播放对应的动画,必须将该动画配置在 Animation 组件的 Clips 属性内 + +在 PlayerController 中将如下的代码: + +```ts +@property(Animation) +BodyAnim:Animation = null; +``` + +添加的位置如下: + +```ts +@ccclass("PlayerController") +export class PlayerController extends Component { + + @property(Animation) + BodyAnim:Animation = null; + ... +} +``` + +注意:如果发现 Aniamtion 无法拖动或不显示,请检查该文件的 import 段是否已添加 Animation 的导入,代码示例如下: + +```ts +import { Animation } from "cc"; +``` + +这里我们给 BodyAnim 添加了一个名为 `@property` 的属性,这样的语法被称为 [装饰器](../../scripting/decorator.md),这里的 `@property` 可以帮助编辑器,使其将 BodyAnim 在编辑器内视为 Animation 类型。 + +如果这里代码没有编译通过,请查看是否有 `const { ccclass, property } = _decorator;` 代码,这里的语句将会正确的将 `property` 方法导出,完整的导出如下: + +```ts +import { _decorator, Component, Vec3, EventMouse, input, Input, Animation } from "cc"; +const { ccclass, property } = _decorator; + +``` + +> **注意**:TypeScript 的内置库和 Cocos Creator 都有名为 Animation 的类,请确保上述代码中 `import { ... } from "cc"` 包含 Animation。 + +在 `jumpByStep` 方法内,添加如下的代码: + +```ts +if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } +} +``` + +此时的 `jumpByStep` 看起来是这样的: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); + + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } +} +``` + +回到编辑器,此时可以通过拖拽的方式添加 BodyAnim 到 PlayerController 上: + +![assign-body-anim.gif](images/assign-body-anim.gif) + +点击运行游戏,点击鼠标都可以看到角色正常的跳起来: + +![preview-anim.gif](images/preview-anim.gif) + +如果仔细观察的话,现在我们使用的是统一的 `_jumpTime = 0.1`,实际上两个动画的时长并不一致,因此可以看到如上图奇怪的动画效果,可以通过获取动画剪辑的时长来动态调整 `_jumpTime`。 +这里举个例子: + +```ts +const oneStep = 'oneStep'; +const state = this.BodyAnim.getState(oneStep); +this._jumpTime = state.duration; +``` + +twoStep 动画和上文代码类似,最终的 `jumpByStep` 方法如下所示: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + + const clipName = step == 1 ? 'oneStep' : 'twoStep'; + const state = this.BodyAnim.getState(clipName); + this._jumpTime = state.duration; + + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); + + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } +} +``` + +> 这里使用到了三元表达式 `condition ? A:B` 相当于条件满足时调用 A 反之调用 B + +![jumptime-with-duration.gif](images/jumptime-with-duration.gif) + +## 游戏管理器(GameManager) + +在游戏中,我们可以通过手动布置 Box 节点来生成地图,但是这样的话地图就是固定了,为了让每次开始游戏的地图有变化并为玩家提供一些惊喜,可以选择通过动态生成方块的方式来创建地图。 + +这样我们就需要将生成的过程和结果保存起来,一般情况为了保存游戏的数据,我们需要创建一些类来辅助这类工作。这样的类我们称之为 **Manager** 管理器。 + +在 **资源管理器** 的 **Scripts** 目录内,点击右键创建新的 TypeScript 组件并将其命名为: **GameManager**。 + +> 在 Cocos Creator 内创建组件时会同时确定组件内根据模板生成的内容。 +> 如果您在不熟悉的情况下输入了错误的名字,可以选择删除再重新创建一个新的文件。 +> 如果只是修改文件名,不修改里面的内容,会导致类名与文件名不一致,而无法在 **属性检查器** 内找到对应的类。 + +创建好 GameManager 之后,我们可以将其挂载在场景内任何一个节点上,但出于清晰的考虑我们一般会选择创建一个同名的节点,并将 GameManager 挂载在它上面: + +![create-game-manager.png](images/create-game-manager.png) + +首先我们需要让 GameManager 知道它应该用哪个资源作为地图块来创建,因此我们可以在代码中添加 `boxPrefab` 来指向我们之前已经创建好的 Box 预制体。 + +```ts +@property({type: Prefab}) +public boxPrefab: Prefab|null = null; +``` + +> @property 依旧是装饰器的用法,如果你不记得了,可以回到之前角色 **播放动画** 部分。 + +将上述的代码添加下如下位置: + +```ts +import { _decorator, Component, Prefab } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('GameManager') +export class GameManager extends Component { + + @property({type: Prefab}) + public boxPrefab: Prefab|null = null; + + start(){} + + update(dt: number): void { + + } +} +``` + +之后回到编辑器并将 Box 预制体拖拽到 GameManager 上: + +![assign-box-prefab.gif](images/assign-box-prefab.gif) + +我们可以用一个数值类型的数组来存储当前的位置到底是方块还是坑,但实际上有更好的办法,我们声明如下的枚举,用 `BT_NONE` 来表示坑,而 `BT_STONE` 来表示方块,这样的表示会让我们的代码更加的易读。 + +```ts +enum BlockType{ + BT_NONE, + BT_STONE, +}; +``` + +在 TypeScript 里面您可以将这个枚举放在类的上面,这样可以确保 GameManager 可以访问它,同时由于没有添加 export 关键字,这意味着这个枚举只有在 GameManager.ts 这个模块内才可以访问。 + +接下来我们需要生成并记录下地图的生成情况,可以声明如下的成员变量来存储它们,同时如果想要在编辑器里面配置初始化时道路的长度,可以声明一个变量 `roadLength` 来记录: + +```ts +import { _decorator, CCInteger, Component, Prefab } from 'cc'; +const { ccclass, property } = _decorator; + +enum BlockType{ + BT_NONE, + BT_STONE, +}; + +@ccclass('GameManager') +export class GameManager extends Component { + + @property({type: Prefab}) + public boxPrefab: Prefab|null = null; + @property({type: CCInteger}) + public roadLength: number = 50; + private _road: BlockType[] = []; + + start() { + + } +} +``` + +> 用数组来存储这些地图数据是很好的主意,因为数组可以进行快速的访问,我们可以通过索引很快查询到某个位置是方块还是坑。 + +填充地图的流程是这样的: + +- 每次生成时,需要将上次的结果清除 +- 第一个地块永远是方块,保证角色不会掉下去 +- 由于我们的角色可以选择跳 1 个方块或者 2 个方块,和某个戴红帽子穿背带裤家伙比起来太弱鸡了,因此坑最多不应该连续超过 2 个,也就意味着如果前面 1 个地块是坑,那么接下来的地块必须是方块 + +接下来为 `GameManager` 添加几个方法: +- 引用 + ```ts + import { _decorator, Component, Prefab, CCInteger, instantiate, Node } from 'cc'; + import { BLOCK_SIZE, PlayerController } from './PlayerController'; + + const { ccclass, property } = _decorator; + + ... + ... + ``` +- 生成地图的方法: + + ```ts + generateRoad() { + + this.node.removeAllChildren(); + + this._road = []; + // startPos + this._road.push(BlockType.BT_STONE); + + for (let i = 1; i < this.roadLength; i++) { + if (this._road[i - 1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + for (let j = 0; j < this._road.length; j++) { + let block: Node | null = this.spawnBlockByType(this._road[j]); + if (block) { + this.node.addChild(block); + block.setPosition(j * BLOCK_SIZE, 0, 0); + } + } + } + ``` + + > `Math.floor`: 这个方法是 TypeScript 数学库的方法之一:我们知道 floor 是地板的意思,这表示取这个方法参数的 "地板",也就是向下取整。 + > `Math.random`:同样 random 也是标准数学库的方法之一,用于随机一个 0 到 1 之间的小数,注意取值范围是 [0, 1)。 + > 所以 `Math.floor(Math.random() * 2)` 这段代码的意思很简单,就是从 [0, 2) 中随机取 1个数并向下取整,得到的结果是 0 或者 1,恰好和 枚举 `BlockType` 中声明的 `BT_NONE` 和 `BT_STONE` 对应。 + > 顺便说一句,在 TypeScript 的枚举中,如果你没有给枚举赋值,那么枚举的值会顺序的从 0 开始分配。 + + 通过 `spawnBlockByType` 来生成新的方块并将它通过 `setPosition` 方法放置到合适的位置。 + + > 在 Cocos Creator 中,设置节点的位置需要使用 `setPosition` 方法或者 `set position` 这样的读取器。 + +- 根据 `BlockType` 生成方块: + + ```ts + spawnBlockByType(type: BlockType) { + if (!this.boxPrefab) { + return null; + } + + let block: Node|null = null; + switch(type) { + case BlockType.BT_STONE: + block = instantiate(this.boxPrefab); + break; + } + + return block; + } + ``` + + 通过 `BlockType` 来确定是否要真的创建这个方块,当然只在 `type` 为 `BT_STONE` 的时候我们通过 `instantiate` 方法来创建方块,其他情况下,返回一个空值。 + + > `instantiate`: 是 Cocos Creator 提供的实例化预制体为节点的方法。当然它不仅能实例化预制体,你甚至可以用它克隆任意类型的对象! + +此时如果我们在 `GameManager` 的 `start` 内调用 `generateRoad` 来创建地图: + +```ts +start() { + this.generateRoad() +} +``` + +运行游戏后可以观察到地图的生成的情况: + +![gen-road.png](images/gen-road.png) + +## 相机和卷轴 + +2D 横版游戏中必须要处理卷轴问题,所谓的卷轴就是相机随着角色的运动而运动,导致看到的场景不太一样的情况。 + +为了实现卷轴,我们需要允许 Camera 可以移动并不在强制和 Canvas 对齐: + +1. 取消 Canvas 节点上 `cc.Canvas` 组件的 **Align Canvas With Screen** 属性 +2. 将 Camera 节点拖到 Player 节点上,使其成为子节点 + +![setup-scroll.gif](./images/setup-scroll.gif) + +此时运行游戏就可以观察到相机的跟随情况: + +![scroll.gif](images/scroll.gif) + +## 菜单制作 + +对于大多数游戏来说,UI 都是比较重要的部分,通过 UI 的提示,可以让玩家知道某些游戏内的信息,让玩家选择不同的游戏策略。 + +2D 游戏类型下,我们本身有一个名为 Canvas 的节点的,但是这个节点我们将只会拿它来作为角色、地图和游戏逻辑的父节点。因为 Cavans 的相机会移动,如果依然使用 Canvas 的相机,会导致 UI 无法渲染,所以我们必须创建一个新的 Canvas 来作为 UI 的容器。 + +在 **层级管理器** 中点击右键选择创建一个新的 Canvas 并将其命名为 UICanvas: + +![create-ui-canvas.png](images/create-ui-canvas.png) + +![ui-canvas.png](images/ui-canvas.png) + +在 UICanvas 上点击右键并创建一个空的节点命名为 'StartMenu',并在 StartMenu 节点下创建一个按钮将其子节点 Label 的 **String** 属性修改为 Play。 + +![create-start-menu.png](images/create-start-menu.png) + +之后可以添加一个背景框和一些文本提示用于提示用户游戏的操作是怎么样的: + +选中 StartMenu 点击右键创建一个 Sprite,将其名字修改为 Bg,从 **资源管理器** 的 internal 目录内,找到 default_panel 资源并赋予给 Bg 的 **Sprite Frame** 属性,调整 **Type** 为 **SLICED**,并调整好 Bg 的 UITransform 内的 Content Size 属性: + +![create-bg.gif](images/create-bg.gif) + +在 StartMenu 下方创建一个名为 Title 的 Label,并修改其属性如下所示: + +![create-title.png](images/create-title.png) + +继续创建一些 Label 用于描述游戏的玩法: + +![create-tip.png](images/create-tip.png) + +同理添加一个 Label 用于代表角色走了几步,注意 Step 这个 Label 不要作为 StartMenu 的子节点: + +![step.png](images/step.png) + +接下来我们就可以完善整个游戏逻辑。 + +## 游戏状态 + +我们游戏有三种状态,初始化、游戏中、游戏重置或者结算,和下棋类似,大部分游戏都可以粗略分解为这样的三个状态。 + +因此我们也可以定义这样的枚举来描述游戏状态。 + +```ts +enum GameState{ + GS_INIT, + GS_PLAYING, + GS_END, +}; +``` + +将上述的代码放在枚举 `BlockType` 附近。 + +这里我们为 GameManager 添加一个 `setCurState` 的方法提供给外界,使其可以用于控制游戏的状态: + +```ts +setCurState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + break; + case GameState.GS_PLAYING: + break; + case GameState.GS_END: + break; + } +} +``` + +添加一个 `init` 方法用于表示进入到 GS_INIT 时游戏的处理: + +```ts +init() {} +``` + +同时在 `setCurState` 的时候调用它: + +```ts +setCurState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + break; + case GameState.GS_END: + break; + } +} +``` + +为了在游戏开始时不让用户操作角色,而在游戏进行时让用户操作角色,我们需要动态地开启和关闭角色对鼠标消息的监听。在 `PlayerController` 脚本中做如下修改: + +```ts +start () { + //input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); +} + +setInputActive(active: boolean) { + if (active) { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } else { + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } +} +``` + +此时的 GameManager 看起来是这样的: + +```ts +import { _decorator, CCInteger, Component, instantiate, Node, Prefab } from 'cc'; +import { BLOCK_SIZE, PlayerController } from './PlayerController'; +const { ccclass, property } = _decorator; + +enum BlockType{ + BT_NONE, + BT_STONE, +}; + +enum GameState{ + GS_INIT, + GS_PLAYING, + GS_END, +}; + +@ccclass('GameManager') +export class GameManager extends Component { + + @property({type: Prefab}) + public boxPrefab: Prefab|null = null; + @property({type: CCInteger}) + public roadLength: number = 50; + private _road: BlockType[] = []; + + start() { + this.setCurState(GameState.GS_INIT); // 第一初始化要在 start 里面调用 + } + + init() { + this.generateRoad(); + } + + setCurState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + + break; + case GameState.GS_END: + break; + } + } + + generateRoad() { + + this.node.removeAllChildren(); + + this._road = []; + // startPos + this._road.push(BlockType.BT_STONE); + + for (let i = 1; i < this.roadLength; i++) { + if (this._road[i - 1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + for (let j = 0; j < this._road.length; j++) { + let block: Node | null = this.spawnBlockByType(this._road[j]); + if (block) { + this.node.addChild(block); + block.setPosition(j * BLOCK_SIZE, 0, 0); + } + } + } + + spawnBlockByType(type: BlockType) { + if (!this.boxPrefab) { + return null; + } + + let block: Node | null = null; + switch (type) { + case BlockType.BT_STONE: + block = instantiate(this.boxPrefab); + break; + } + + return block; + } +} +``` + +接下来我们分析下在每个状态下所需要处理的事情: + +- GS_INIT:状态下需要初始化地图、将角色放回到初始点、显示游戏的UI,因此在属性中下列属性: + + ```ts + @property({ type: Node }) + public startMenu: Node | null = null; // 开始的 UI + @property({ type: PlayerController }) + public playerCtrl: PlayerController | null = null; // 角色控制器 + @property({type: Label}) + public stepsLabel: Label|null = null; // 计步器 + ``` + + 在 `init` 方法中需要做如下的处理: + + ```ts + init() { + if (this.startMenu) { + this.startMenu.active = true; + } + + this.generateRoad(); + + if (this.playerCtrl) { + this.playerCtrl.setInputActive(false); + this.playerCtrl.node.setPosition(Vec3.ZERO); + this.playerCtrl.reset(); + } + } + ``` + + init 时我们先显示 StartMenu、创建地图以及重设角色的位置和状态并禁用角色输入。 + +- GS_PLAYING:在状态下隐藏 StartMenu、重设计步器的数值以及启用用户输入: + + ```ts + if (this.startMenu) { + this.startMenu.active = false; + } + + if (this.stepsLabel) { + this.stepsLabel.string = '0'; // 将步数重置为0 + } + + setTimeout(() => { //直接设置active会直接开始监听鼠标事件,做了一下延迟处理 + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + ``` + +- GS_END:暂时没有什么好添加的,当然您可以根据喜好添加一些结算用的逻辑让游戏看起来更完善 + +回到编辑器,绑定好 GameManager 需要的属性: + +![bind-manager.png](images/bind-manager.png) + +### 绑定按钮事件 + +在 GameManager 内添加如下的方法,用于响应 Play 按钮按下的事件: + +```ts +onStartButtonClicked() { + this.setCurState(GameState.GS_PLAYING); +} +``` + +回到编辑器,找到开始按钮,并在 **Click Events** 属性后的输入框内输入 1,然后找到 GameManager 节点并拖拽到下方的 cc.Node 属性内,之后从第二栏的下拉中找到 GameManager 脚本,再从第三栏中选择 `onStartButtonClicked` 事件。 + +![click-event.gif](images/click-event.gif) + +此时已可以正常的开始玩游戏: +> #### 注意:如果你运行游戏后发现 UI 出现重影,请查阅本文的 `层级` 章节解决问题。 + +![start-game-without-result.gif](./images/start-game-without-result.gif) + +接下来就来处理掉到坑里后游戏失败的情况。 + +### 监听跳跃结束 + +在 PlayerController 里面添加一个属性用于记录角色当前为多少步: + +```ts +private _curMoveIndex: number = 0; +``` + +在 `reset` 方法中重置这个属性: + +```ts +reset() { + this._curMoveIndex = 0; + this.node.getPosition(this._curPos); + this._targetPos.set(0,0,0); +} +``` + +在 `jumpByStep` 中将这个步数增加,每次的增量是输入的步数: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); + + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } + + this._curMoveIndex += step; +} +``` + +在 PlayerController 中添加一个监听跳跃结束的方法: + +```ts +onOnceJumpEnd() { + this.node.emit('JumpEnd', this._curMoveIndex); +} +``` + +该方法派发了一个名为 `JumpEnd` 的事件,并将 `_curMoveIndex` 作为参数传递出去。 + +并在 PlayerController 的 `update` 方法中调用: + +```ts +update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + this.onOnceJumpEnd(); + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } +} +``` + +回到 GameManager 并增加以下的处理: + +- 增加一个 `onPlayerJumpEnd` 的方法 + + ```ts + onPlayerJumpEnd(moveIndex: number) { + + } + ``` + +- 在 `start` 中监听 `` 的事件: + + ```ts + start() { + this.setCurState(GameState.GS_INIT); + this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this); + } + ``` + + 可以看到这里我们使用的 `this.playerCtrl?.node` 也就是 PlayerController 的节点来接收事件,在 Cocos Creator 中,某个节点派发的事件,只能用这个节点的引用去监听。 + +- 增加一个用于判定角色是否跳跃到坑或者跳完所有地块的方法: + + ```ts + checkResult(moveIndex: number) { + if (moveIndex < this.roadLength) { + if (this._road[moveIndex] == BlockType.BT_NONE) { //跳到了空方块上 + + this.setCurState(GameState.GS_INIT) + } + } else { // 跳过了最大长度 + this.setCurState(GameState.GS_INIT); + } + } + ``` + +- 填充 `onPlayerJumpEnd` 如下: + + ```ts + onPlayerJumpEnd(moveIndex: number) { + if (this.stepsLabel) { + this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex); + } + this.checkResult(moveIndex); + } + ``` + + 上述的方法更新的计步器并检查角色是调到坑里面还是跳所有的方块,如果满足这两个条件,则重置整个游戏逻辑。 + +## 层级 + +在 2D 中我们需要小心的规划物体的层级以确保显示正确的内容。 + +此时如果我们启动游戏,则可以看到重叠的现象,这是因为 UICanvas 下的相机也绘制了 Canvas 下的内容: + +![layer-error.png](images/layer-error.png) + +为了解决这个问题我们可以做如下的处理: + +- 将 Canvas 下相关的节点层级修改为 DEFAULT: + + ![layer-default.png](images/layer-default.png) + +- 将 Box 这个资源的层级修改为 DEFAULT: + + ![box-layer.png](images/box-layer.png) + + 双击该预制体就可以进入到预制体编辑器界面,修改后记得点击场景视图内的 **保存** 按钮保存预制体的变更。 + + ![save-prefab.png](images/save-prefab.png) + +- 修改 Canvas/Player 下的 Camera 的 **Visibility** 属性如下: + + ![cavans-camera.png](images/cavans-camera.png) + +- 修改 UICavans 下的 Camera 如下: + + ![images/uicanvas-camera.png](images/uicanvas-camera.png) + +再次启动游戏则显示正常: + +![after-layer-setting.gif](images/after-layer-setting.gif) + +## 更多功能 + +接下来您可以处理更多的游戏功能,比如将主角替换为序列关键帧或者通过龙骨/Spine 制作的动画,亦或者增加一些玩法和特效等等。 + +## 总结 + +至此,我们的游戏核心逻辑就全部完成了,最后我们稍微梳理下一些需要注意的地方: + +- 2D / UI 节点必须放在 Canvas 下面才会显示(实际上是 RenderRoot2D,因为 Canvas 继承自 RenderRoot2D) +- 小心规划物体的层级,需要调整相机的 **Visiblity** 属性来让不同的 Canvas 分开渲染 + +到此为止,如果您还觉得有困难的话,或有任何意见和建议,欢迎您在 [论坛](https://forum.cocos.org/) 或 [GIT](https://github.com/cocos/cocos-docs) 联系我们。 + +如果您想让您的项目在移动端设备上运行,我们也准备了 [监听触摸事件](touch.md) 可以用于监听触摸事件。 + +## 完整代码 + +PlayerController: + +```ts +import { _decorator, Component, Vec3, EventMouse, input, Input, Animation } from "cc"; +const { ccclass, property } = _decorator; + +export const BLOCK_SIZE = 40; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + @property(Animation) + BodyAnim:Animation = null; + + private _startJump: boolean = false; + private _jumpStep: number = 0; + private _curJumpTime: number = 0; + private _jumpTime: number = 0.1; + private _curJumpSpeed: number = 0; + private _curPos: Vec3 = new Vec3(); + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + private _targetPos: Vec3 = new Vec3(); + private _curMoveIndex: number = 0; + start () { + //input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + setInputActive(active: boolean) { + if (active) { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } else { + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + } + + reset() { + this._curMoveIndex = 0; + this.node.getPosition(this._curPos); + this._targetPos.set(0,0,0); + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + + const clipName = step == 1 ? 'oneStep' : 'twoStep'; + const state = this.BodyAnim.getState(clipName); + this._jumpTime = state.duration; + + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE/ this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep* BLOCK_SIZE, 0, 0)); + + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } + + this._curMoveIndex += step; + } + + + onOnceJumpEnd() { + this.node.emit('JumpEnd', this._curMoveIndex); + } + + update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + this.onOnceJumpEnd(); + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` + +GameManager.ts: + +```ts +import { _decorator, CCInteger, Component, instantiate, Label, math, Node, Prefab, Vec3 } from 'cc'; +import { BLOCK_SIZE, PlayerController } from './PlayerController'; +const { ccclass, property } = _decorator; + +enum BlockType { + BT_NONE, + BT_STONE, +}; + +enum GameState { + GS_INIT, + GS_PLAYING, + GS_END, +}; + +@ccclass('GameManager') +export class GameManager extends Component { + + @property({ type: Prefab }) + public boxPrefab: Prefab | null = null; + @property({ type: CCInteger }) + public roadLength: number = 50; + private _road: BlockType[] = []; + + @property({ type: Node }) + public startMenu: Node | null = null; + @property({ type: PlayerController }) + public playerCtrl: PlayerController | null = null; + @property({ type: Label }) + public stepsLabel: Label | null = null; + + start() { + this.setCurState(GameState.GS_INIT); + this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this); + } + + init() { + if (this.startMenu) { + this.startMenu.active = true; + } + + this.generateRoad(); + + if (this.playerCtrl) { + this.playerCtrl.setInputActive(false); + this.playerCtrl.node.setPosition(Vec3.ZERO); + this.playerCtrl.reset(); + } + } + + setCurState(value: GameState) { + switch (value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + if (this.startMenu) { + this.startMenu.active = false; + } + + if (this.stepsLabel) { + this.stepsLabel.string = '0'; // 将步数重置为0 + } + + setTimeout(() => { //直接设置active会直接开始监听鼠标事件,做了一下延迟处理 + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + break; + case GameState.GS_END: + break; + } + } + + generateRoad() { + + this.node.removeAllChildren(); + + this._road = []; + // startPos + this._road.push(BlockType.BT_STONE); + + for (let i = 1; i < this.roadLength; i++) { + if (this._road[i - 1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + for (let j = 0; j < this._road.length; j++) { + let block: Node | null = this.spawnBlockByType(this._road[j]); + if (block) { + this.node.addChild(block); + block.setPosition(j * BLOCK_SIZE, 0, 0); + } + } + } + + spawnBlockByType(type: BlockType) { + if (!this.boxPrefab) { + return null; + } + + let block: Node | null = null; + switch (type) { + case BlockType.BT_STONE: + block = instantiate(this.boxPrefab); + break; + } + + return block; + } + + onStartButtonClicked() { + this.setCurState(GameState.GS_PLAYING); + } + + checkResult(moveIndex: number) { + if (moveIndex < this.roadLength) { + if (this._road[moveIndex] == BlockType.BT_NONE) { //跳到了空方块上 + this.setCurState(GameState.GS_INIT); + } + } else { // 跳过了最大长度 + this.setCurState(GameState.GS_INIT); + } + } + + onPlayerJumpEnd(moveIndex: number) { + if (this.stepsLabel) { + this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex); + } + this.checkResult(moveIndex); + } + +} +``` diff --git a/versions/4.0/zh/getting-started/first-game-2d/touch.md b/versions/4.0/zh/getting-started/first-game-2d/touch.md new file mode 100644 index 0000000000..6f4bbece32 --- /dev/null +++ b/versions/4.0/zh/getting-started/first-game-2d/touch.md @@ -0,0 +1,191 @@ +# 监听触摸事件 + +触摸事件可以通过监听 `Input.EventType.TOUCH_START` 来接收屏幕或者鼠标的事件。 + +监听方式有两种: + +- 通过 `input.on` 的方式监听,这种方式会监听屏幕上所有的触摸 +- 通过 `node.on` 的方式监听时,可以监听某个范围内的触摸事件 + +考虑到我们需要操作角色跳一步或者两步,因此我们选择将屏幕的左边的触摸用于处理跳一步的输入,而右边用于跳两步。 + +在 UI 层级里,也就是 `UICanvas` 节点下方创建两个空的节点,并分别命名为 LeftTouch 和 RightTouch: + +![create-touch.png](touch/create-touch.png) + +参考下图设定好他们的位置和大小: + +![left-touch.png](touch/left-touch.png) + +![right-touch.png](touch/right-touch.png) + +回到 `PlayerController` 在里面添加下列属性可以在角色上配置触摸区域: + +```ts +@property(Node) +leftTouch: Node = null; + +@property(Node) +rightTouch: Node = null; +``` + +并将刚才创建好的节点拖拽到上面去: + +![config-touch-nodes.png](touch/config-touch-nodes.png) + +在 `PlayerController` 里面添加下面的代码: + +- 监听触摸输入: + + ```ts + setInputActive(active: boolean) { + if (active) { + this.leftTouch.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + this.rightTouch.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + } else { + this.leftTouch.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + this.rightTouch.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + } + ``` + +- 添加响应触摸的回调: + + ```ts + onTouchStart(event: EventTouch) { + const target = event.target as Node; + if (target?.name == 'LeftTouch') { + this.jumpByStep(1); + } else { + this.jumpByStep(2); + } + } + ``` + + `target` 在定义中是 `any` 类型,因此我们需要通过 `as` 关键字将其转化为 `Node` 类型。 + + > 通过 as 关键字可以进行类型转换,前提是您得知道他是什么类型。 + + 取到触摸的目标后,我们就可以通过 `name` 这个属性来区分用户点击的是右侧的触摸区域还是左侧。 + +之后运行游戏就可以观察到触摸时的跳跃情况,此时可以通过手机应用扫描下图中的 QR 码来在移动设备上游玩(注意要在一个局域网内)。 + +完整的 `Playercontroller` 代码参考如下: + +```ts +import { _decorator, Component, Vec3, EventMouse, input, Input, Animation, EventTouch, Node } from "cc"; +const { ccclass, property } = _decorator; + +export const BLOCK_SIZE = 40; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + @property(Animation) + BodyAnim: Animation = null; + + private _startJump: boolean = false; + private _jumpStep: number = 0; + private _curJumpTime: number = 0; + private _jumpTime: number = 0.3; + private _curJumpSpeed: number = 0; + private _curPos: Vec3 = new Vec3(); + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + private _targetPos: Vec3 = new Vec3(); + private _curMoveIndex = 0; + + @property(Node) + leftTouch: Node = null; + + @property(Node) + rightTouch: Node = null; + + start() { + //input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + setInputActive(active: boolean) { + if (active) { + //input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + this.leftTouch.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + this.rightTouch.on(Input.EventType.TOUCH_START, this.onTouchStart, this); + } else { + //input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + this.leftTouch.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + this.rightTouch.off(Input.EventType.TOUCH_START, this.onTouchStart, this); + } + } + + reset() { + this._curMoveIndex = 0; + } + + onTouchStart(event: EventTouch) { + const target = event.target as Node; + if (target?.name == 'LeftTouch') { + this.jumpByStep(1); + } else { + this.jumpByStep(2); + } + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + + const clipName = step == 1 ? 'oneStep' : 'twoStep'; + const state = this.BodyAnim.getState(clipName); + this._jumpTime = state.duration; + + this._curJumpSpeed = this._jumpStep * BLOCK_SIZE / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep * BLOCK_SIZE, 0, 0)); + + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } + + this._curMoveIndex += step; + } + + + onOnceJumpEnd() { + this.node.emit('JumpEnd', this._curMoveIndex); + } + + update(deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + this.onOnceJumpEnd(); + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` diff --git a/versions/4.0/zh/getting-started/first-game-2d/touch/config-touch-nodes.png b/versions/4.0/zh/getting-started/first-game-2d/touch/config-touch-nodes.png new file mode 100644 index 0000000000..e17e6da77b Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/touch/config-touch-nodes.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/touch/create-touch.png b/versions/4.0/zh/getting-started/first-game-2d/touch/create-touch.png new file mode 100644 index 0000000000..00ea30da62 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/touch/create-touch.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/touch/left-touch.png b/versions/4.0/zh/getting-started/first-game-2d/touch/left-touch.png new file mode 100644 index 0000000000..95eb7743dc Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/touch/left-touch.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/touch/qr-code.png b/versions/4.0/zh/getting-started/first-game-2d/touch/qr-code.png new file mode 100644 index 0000000000..94ea7d80f3 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/touch/qr-code.png differ diff --git a/versions/4.0/zh/getting-started/first-game-2d/touch/right-touch.png b/versions/4.0/zh/getting-started/first-game-2d/touch/right-touch.png new file mode 100644 index 0000000000..88c7515a77 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game-2d/touch/right-touch.png differ diff --git a/versions/4.0/zh/getting-started/first-game/advance.md b/versions/4.0/zh/getting-started/first-game/advance.md new file mode 100644 index 0000000000..6d5712ba51 --- /dev/null +++ b/versions/4.0/zh/getting-started/first-game/advance.md @@ -0,0 +1,450 @@ +# 进阶篇 - 添加阴影、光照和动画 + +在本节中我们将向您介绍如何完善 [快速上手:制作第一个游戏](index.md) 中制作的原型,如何使用第三方资源比如动画资源等等。 + +## 光照和阴影 + +光影是描述游戏的重要渲染特性,通过光源和阴影,我们可以模拟更加真实的游戏世界,提供更好的沉浸感和代入感。 + +接下来我们为角色加上简单的影子。 + +### 开启阴影 + +1. 在 **层级管理器** 中点击最顶部的 `Scene` 节点,然后在 **属性检查器** 勾选 `shadows` 中的 Enabled,并修改 **Distance** 和 **Normal** 属性: + + ![planar shadows](./images/planarShadows.png) + +2. 点击 Player 节点下的 Body 节点,将 `cc.MeshRenderer` 组件中的 **ShadowCastingMode** 设置为 **ON**。 + + ![model shadow](./images/model-shadow.png) + +此时在 **场景编辑器** 中会看到一个阴影面片,预览会发现看不到这个阴影,这是因为它在模型的正后方,被胶囊体盖住了。 + +![player shadow](./images/player-shadow-scene.png) + +### 调整光照 + +新建场景时默认会添加一个挂载了 `cc.DirectionalLight` 组件的 **Main Light** 节点,由这个平行光计算阴影。所以为了让阴影换个位置显示,我们可以调整这个平行光的方向。在 **层级管理器** 中点击选中 **Main Light** 节点,调整 `Rotation` 属性为(-10,17,0)。 + +![main light](./images/main-light.png) + +点击预览可以看到影子效果: + +![player shadow preview](./images/player-shadow-preview.png) + +## 添加主角模型 + +做为一个官方教程,用胶囊体当主角显的有点寒碜,所以我们花(低)重(预)金(算)制作了一个 Cocos 主角。 + +### 导入模型资源 + +从原始资源导入模型、材质、动画等资源不是本篇基础教程的重点,所以这边直接使用已经导入工程的资源。将 **项目工程**([GitHub](https://github.com/cocos-creator/tutorial-mind-your-step-3d) | [Gitee](https://gitee.com/mirrors_cocos-creator/tutorial-mind-your-step-3d))中 assets 目录下的 cocos 文件夹拷贝到你自己工程的 assets 目录下。 + +### 添加到场景中 + +在 cocos 文件中已经包含了一个名为 Cocos 的 Prefab,将它拖拽到 **层级管理器** 中 Player 节点下的 Body 节点中,作为 Body 节点的子节点。 + +![add cocos prefab](./images/add-cocos-prefab.png) + +同时在 **属性检查器** 中移除原先的胶囊体模型: + +![remove capsule](./images/remove-capsule.png) + +此时会发现模型有些暗,可以在 Cocos 节点下加个聚光灯(Spotlight),以突出它锃光瓦亮的脑门。 + +![add cocos light](./images/cocos-add-light.png) + +### 添加跳跃动画 + +现在预览可以看到主角初始会有一个待机动画,但是跳跃时还是用这个待机动画会显得很不协调,所以我们可以在跳跃过程中将其换成跳跃的动画。在 `PlayerController.ts` 类中添加一个引用模型动画的变量: + +```ts +@property({type: SkeletalAnimation}) +public CocosAnim: SkeletalAnimation|null = null; +``` + +同时,因为我们将主角从胶囊体换成了人物模型,可以弃用之前为胶囊体制作的动画,并注释相关代码: + +```ts +// @property({type: Animation}) +// public BodyAnim: Animation|null = null; + +jumpByStep(step: number) { + // ... + // if (this.BodyAnim) { + // if (step === 1) { + // this.BodyAnim.play('oneStep'); + // } else if (step === 2) { + // this.BodyAnim.play('twoStep'); + // } + // } +} +``` + +然后在 **层级管理器** 中将 Cocos 节点拖拽到 Player 节点的 `CocosAnim` 属性框中: + +![assign cocos prefab](./images/assign-cocos-prefab.png) + +在 `PlayerController` 脚本的 `jumpByStep` 函数中播放跳跃动画: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); + + if (this.CocosAnim) { + this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; // 跳跃动画时间比较长,这里加速播放 + this.CocosAnim.play('cocos_anim_jump'); // 播放跳跃动画 + } + + // if (this.BodyAnim) { + // if (step === 1) { + // this.BodyAnim.play('oneStep'); + // } else if (step === 2) { + // this.BodyAnim.play('twoStep'); + // } + // } + + this._curMoveIndex += step; +} +``` + +这里 `_jumpStep` 时间是 0.3 秒,如果动画播放的时长和 `_jumpStep` 不匹配可能会导致如下问题: + +- 动画还没播放完毕,出现动画过渡不平滑 +- 或者动画播放完毕但跳跃时间还没有到产生滑步现象 + +一种处理方法使我们直接通过动画剪辑的时长和 `_jumpStep` 来计算重新计算动画的速度而不是使用常量: + +```ts +var state = this.CocosAnim.getState('cocos_anim_jump'); +state.speed = state.duration/this._jumpTime; +``` + +开发者可以自行尝试,或者手动修改 `_jumpStep` 和 `speed` 到合适的值以控制游戏的节奏。 + +在 `PlayerController` 脚本的 `onOnceJumpEnd` 函数中让主角变为待机状态,播放待机动画。 + +```ts +onOnceJumpEnd() { + if (this.CocosAnim) { + this.CocosAnim.play('cocos_anim_idle'); + } + this.node.emit('JumpEnd', this._curMoveIndex); +} +``` + +> **注意**:当跳跃完成时会触发 `onOnceJumpEnd`,详情请见 `PlayerController.ts` 中的 `update` 函数实现。 + +预览效果如下: + +![cocos play](./images/cocos-play.gif) + +## 最终代码 + +**PlayerController.ts** + +```ts +import { _decorator, Component, Vec3, input, Input, EventMouse, Animation, SkeletalAnimation } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + @property({type: Animation}) + public BodyAnim: Animation|null = null; + @property({type: SkeletalAnimation}) + public CocosAnim: SkeletalAnimation|null = null; + + // for fake tween + private _startJump: boolean = false; + private _jumpStep: number = 0; + private _curJumpTime: number = 0; + private _jumpTime: number = 0.3; + private _curJumpSpeed: number = 0; + private _curPos: Vec3 = new Vec3(); + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + private _targetPos: Vec3 = new Vec3(); + private _curMoveIndex = 0; + + start () { + } + + reset() { + this._curMoveIndex = 0; + } + + setInputActive(active: boolean) { + if (active) { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } else { + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); + + if (this.CocosAnim) { + this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; //跳跃动画时间比较长,这里加速播放 + this.CocosAnim.play('cocos_anim_jump'); //播放跳跃动画 + } + + // if (this.BodyAnim) { + // if (step === 1) { + // this.BodyAnim.play('oneStep'); + // } else if (step === 2) { + // this.BodyAnim.play('twoStep'); + // } + // } + + this._curMoveIndex += step; + } + + onOnceJumpEnd() { + if (this.CocosAnim) { + this.CocosAnim.play('cocos_anim_idle'); + } + + this.node.emit('JumpEnd', this._curMoveIndex); + } + + update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + this.onOnceJumpEnd(); + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` + +**GameManager.ts** + +```ts +import { _decorator, Component, Prefab, instantiate, Node, Label, CCInteger, Vec3 } from 'cc'; +import { PlayerController } from "./PlayerController"; +const { ccclass, property } = _decorator; + +// 赛道格子类型,坑(BT_NONE)或者实路(BT_STONE) +enum BlockType{ + BT_NONE, + BT_STONE, +}; + +enum GameState{ + GS_INIT, + GS_PLAYING, + GS_END, +}; + +@ccclass("GameManager") +export class GameManager extends Component { + + // 赛道预制 + @property({type: Prefab}) + public cubePrfb: Prefab | null = null; + // 赛道长度 + @property({type: CCInteger}) + public roadLength: Number = 50; + private _road: BlockType[] = []; + // 主界面根节点 + @property({type: Node}) + public startMenu: Node | null = null; + // 关联 Player 节点身上 PlayerController 组件 + @property({type: PlayerController}) + public playerCtrl: PlayerController | null = null; + // 关联步长文本组件 + @property({type: Label}) + public stepsLabel: Label | null = null!; + + start () { + this.curState = GameState.GS_INIT; + this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this); + } + + init() { + // 激活主界面 + if (this.startMenu) { + this.startMenu.active = true; + } + // 生成赛道 + this.generateRoad(); + if(this.playerCtrl){ + // 禁止接收用户操作人物移动指令 + this.playerCtrl.setInputActive(false); + // 重置人物位置 + this.playerCtrl.node.setPosition(Vec3.ZERO); + // 重置已经移动的步长数据 + this.playerCtrl.reset(); + } + } + + set curState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + if (this.startMenu) { + this.startMenu.active = false; + } + + if (this.stepsLabel) { + this.stepsLabel.string = '0'; // 将步数重置为0 + } + // 会出现的现象就是,游戏开始的瞬间人物已经开始移动 + // 因此,这里需要做延迟处理 + setTimeout(() => { + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + break; + case GameState.GS_END: + break; + } + } + + generateRoad() { + // 防止游戏重新开始时,赛道还是旧的赛道 + // 因此,需要移除旧赛道,清除旧赛道数据 + this.node.removeAllChildren(); + this._road = []; + // 确保游戏运行时,人物一定站在实路上 + this._road.push(BlockType.BT_STONE); + + // 确定好每一格赛道类型 + for (let i = 1; i < this.roadLength; i++) { + // 如果上一格赛道是坑,那么这一格一定不能为坑 + if (this._road[i-1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + // 根据赛道类型生成赛道 + let linkedBlocks = 0; + for (let j = 0; j < this._road.length; j++) { + if(this._road[j]) { + ++linkedBlocks; + } + if(this._road[j] == 0) { + if(linkedBlocks > 0) { + this.spawnBlockByCount(j - 1, linkedBlocks); + linkedBlocks = 0; + } + } + if(this._road.length == j + 1) { + if(linkedBlocks > 0) { + this.spawnBlockByCount(j, linkedBlocks); + linkedBlocks = 0; + } + } + } + } + + spawnBlockByCount(lastPos: number, count: number) { + let block: Node|null = this.spawnBlockByType(BlockType.BT_STONE); + if(block) { + this.node.addChild(block); + block?.setScale(count, 1, 1); + block?.setPosition(lastPos - (count - 1) * 0.5, -1.5, 0); + } + } + spawnBlockByType(type: BlockType) { + if (!this.cubePrfb) { + return null; + } + + let block: Node|null = null; + switch(type) { + case BlockType.BT_STONE: + block = instantiate(this.cubePrfb); + break; + } + + return block; + } + + onStartButtonClicked() { + // 点击主界面 play 按钮,开始游戏 + this.curState = GameState.GS_PLAYING; + } + + checkResult(moveIndex: number) { + if (moveIndex < this.roadLength) { + // 跳到了坑上 + if (this._road[moveIndex] == BlockType.BT_NONE) { + this.curState = GameState.GS_INIT; + } + } else { // 跳过了最大长度 + this.curState = GameState.GS_INIT; + } + } + + onPlayerJumpEnd(moveIndex: number) { + if (this.stepsLabel) { + // 因为在最后一步可能出现步伐大的跳跃,但是此时无论跳跃是步伐大还是步伐小都不应该多增加分数 + this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex); + } + // 检查当前下落道路的类型,获取结果 + this.checkResult(moveIndex); + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } +} +``` + +## 总结 + +恭喜您完成了用 Cocos Creator 制作的第一个游戏!在 [GitHub](https://github.com/cocos-creator/tutorial-mind-your-step-3d) | [Gitee](https://gitee.com/mirrors_cocos-creator/tutorial-mind-your-step-3d) 可以下载完整的工程,希望这篇快速入门教程能帮助您了解 Cocos Creator 游戏开发流程中的基本概念和工作流程。如果您对编写和学习脚本编程不感兴趣,也可以直接从完成版的项目工程中把写好的脚本复制过来使用。 + +接下来您还可以继续完善游戏的各方各面,以下是一些推荐的改进方向: +- 为游戏增加难度,当角色在原地停留1秒就算失败 +- 改为无限跑道,动态的删除已经跑过的跑道,延长后面的跑道。 +- 增加游戏音效 +- 为游戏增加结束菜单界面,统计玩家跳跃步数和所花的时间 +- 用更漂亮的资源替换角色和跑道 +- 可以增加一些可拾取物品来引导玩家“犯错” +- 添加一些粒子特效,例如角色运动时的拖尾、落地时的灰尘 +- 为触屏设备加入两个操作按钮来代替鼠标左右键操作 + +此外如果希望将完成的游戏发布到服务器上分享给好友玩耍,可以阅读 [发布工作流](../../editor/publish/index.md) 一节的内容。 diff --git a/versions/4.0/zh/getting-started/first-game/images/2d-view.png b/versions/4.0/zh/getting-started/first-game/images/2d-view.png new file mode 100644 index 0000000000..d07cb8ce34 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/2d-view.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/add-animation-from-assets.gif b/versions/4.0/zh/getting-started/first-game/images/add-animation-from-assets.gif new file mode 100644 index 0000000000..bc1cd0836e Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/add-animation-from-assets.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/add-animation.gif b/versions/4.0/zh/getting-started/first-game/images/add-animation.gif new file mode 100644 index 0000000000..b360b5b13e Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/add-animation.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/add-cocos-prefab.png b/versions/4.0/zh/getting-started/first-game/images/add-cocos-prefab.png new file mode 100644 index 0000000000..87619e8ff1 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/add-cocos-prefab.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/add-ground-base.png b/versions/4.0/zh/getting-started/first-game/images/add-ground-base.png new file mode 100644 index 0000000000..92e74efdf9 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/add-ground-base.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/add-keyframe.gif b/versions/4.0/zh/getting-started/first-game/images/add-keyframe.gif new file mode 100644 index 0000000000..5c2ed5d241 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/add-keyframe.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/add-label-title.gif b/versions/4.0/zh/getting-started/first-game/images/add-label-title.gif new file mode 100644 index 0000000000..c13788d4c2 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/add-label-title.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/add-player-controller.png b/versions/4.0/zh/getting-started/first-game/images/add-player-controller.png new file mode 100644 index 0000000000..ea9759e4be Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/add-player-controller.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/add-steps-to-game-manager.png b/versions/4.0/zh/getting-started/first-game/images/add-steps-to-game-manager.png new file mode 100644 index 0000000000..57e864dfd9 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/add-steps-to-game-manager.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/assign-cocos-prefab.png b/versions/4.0/zh/getting-started/first-game/images/assign-cocos-prefab.png new file mode 100644 index 0000000000..85c103828f Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/assign-cocos-prefab.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/assign-cube-prefab.png b/versions/4.0/zh/getting-started/first-game/images/assign-cube-prefab.png new file mode 100644 index 0000000000..72068ab151 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/assign-cube-prefab.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/camera-setting.png b/versions/4.0/zh/getting-started/first-game/images/camera-setting.png new file mode 100644 index 0000000000..0f5fc9a5ea Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/camera-setting.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/change-spriteFrame.png b/versions/4.0/zh/getting-started/first-game/images/change-spriteFrame.png new file mode 100644 index 0000000000..4053ba21a0 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/change-spriteFrame.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/cocos-add-light.png b/versions/4.0/zh/getting-started/first-game/images/cocos-add-light.png new file mode 100644 index 0000000000..f0146933bd Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/cocos-add-light.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/cocos-play.gif b/versions/4.0/zh/getting-started/first-game/images/cocos-play.gif new file mode 100644 index 0000000000..0d829a51ca Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/cocos-play.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-bg-sprite.gif b/versions/4.0/zh/getting-started/first-game/images/create-bg-sprite.gif new file mode 100644 index 0000000000..fcefda381c Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-bg-sprite.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-button.gif b/versions/4.0/zh/getting-started/first-game/images/create-button.gif new file mode 100644 index 0000000000..e41a65d52b Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-button.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-cube-prefab.gif b/versions/4.0/zh/getting-started/first-game/images/create-cube-prefab.gif new file mode 100644 index 0000000000..7c35aa6f71 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-cube-prefab.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-cube.gif b/versions/4.0/zh/getting-started/first-game/images/create-cube.gif new file mode 100644 index 0000000000..f163df010f Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-cube.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-folder.png b/versions/4.0/zh/getting-started/first-game/images/create-folder.png new file mode 100644 index 0000000000..fdfd12e71a Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-folder.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-player-script.gif b/versions/4.0/zh/getting-started/first-game/images/create-player-script.gif new file mode 100644 index 0000000000..e75ad1abda Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-player-script.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-player-script.png b/versions/4.0/zh/getting-started/first-game/images/create-player-script.png new file mode 100644 index 0000000000..63e9ea4788 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-player-script.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-player.gif b/versions/4.0/zh/getting-started/first-game/images/create-player.gif new file mode 100644 index 0000000000..0754205ded Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-player.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-player.png b/versions/4.0/zh/getting-started/first-game/images/create-player.png new file mode 100644 index 0000000000..7a4941aacf Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-player.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-project.png b/versions/4.0/zh/getting-started/first-game/images/create-project.png new file mode 100644 index 0000000000..914b6f9613 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-project.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/create-scene.png b/versions/4.0/zh/getting-started/first-game/images/create-scene.png new file mode 100644 index 0000000000..e307de90fc Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/create-scene.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/drag-camera-to-player.gif b/versions/4.0/zh/getting-started/first-game/images/drag-camera-to-player.gif new file mode 100644 index 0000000000..0da4c53eda Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/drag-camera-to-player.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/drag-to-animComp.gif b/versions/4.0/zh/getting-started/first-game/images/drag-to-animComp.gif new file mode 100644 index 0000000000..e9696b5f8a Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/drag-to-animComp.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/edit-second-clip.png b/versions/4.0/zh/getting-started/first-game/images/edit-second-clip.png new file mode 100644 index 0000000000..19ae8d7339 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/edit-second-clip.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/game-manager-player.png b/versions/4.0/zh/getting-started/first-game/images/game-manager-player.png new file mode 100644 index 0000000000..63390d17fe Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/game-manager-player.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/hierarchy.png b/versions/4.0/zh/getting-started/first-game/images/hierarchy.png new file mode 100644 index 0000000000..8bb03eba5a Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/hierarchy.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/main-light.png b/versions/4.0/zh/getting-started/first-game/images/main-light.png new file mode 100644 index 0000000000..83f594862d Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/main-light.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/main-window.png b/versions/4.0/zh/getting-started/first-game/images/main-window.png new file mode 100644 index 0000000000..c1325dae0c Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/main-window.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/model-shadow.png b/versions/4.0/zh/getting-started/first-game/images/model-shadow.png new file mode 100644 index 0000000000..5810745dfa Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/model-shadow.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/planarShadows.png b/versions/4.0/zh/getting-started/first-game/images/planarShadows.png new file mode 100644 index 0000000000..6e9ee1972a Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/planarShadows.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/play-button-inspector.png b/versions/4.0/zh/getting-started/first-game/images/play-button-inspector.png new file mode 100644 index 0000000000..03028061b3 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/play-button-inspector.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/play.png b/versions/4.0/zh/getting-started/first-game/images/play.png new file mode 100644 index 0000000000..35a468cf9f Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/play.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/player-move.gif b/versions/4.0/zh/getting-started/first-game/images/player-move.gif new file mode 100644 index 0000000000..5d235463ae Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/player-move.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/player-shadow-preview.png b/versions/4.0/zh/getting-started/first-game/images/player-shadow-preview.png new file mode 100644 index 0000000000..c7cf7b9491 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/player-shadow-preview.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/player-shadow-scene.png b/versions/4.0/zh/getting-started/first-game/images/player-shadow-scene.png new file mode 100644 index 0000000000..455545762d Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/player-shadow-scene.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/preview-with-jump.gif b/versions/4.0/zh/getting-started/first-game/images/preview-with-jump.gif new file mode 100644 index 0000000000..bc92c7659e Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/preview-with-jump.gif differ diff --git a/versions/4.0/zh/getting-started/first-game/images/remove-capsule.png b/versions/4.0/zh/getting-started/first-game/images/remove-capsule.png new file mode 100644 index 0000000000..659fac63e9 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/remove-capsule.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/start-menu.png b/versions/4.0/zh/getting-started/first-game/images/start-menu.png new file mode 100644 index 0000000000..a010bc532a Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/start-menu.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/steps-label.png b/versions/4.0/zh/getting-started/first-game/images/steps-label.png new file mode 100644 index 0000000000..b629572b20 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/steps-label.png differ diff --git a/versions/4.0/zh/getting-started/first-game/images/title-inspector.png b/versions/4.0/zh/getting-started/first-game/images/title-inspector.png new file mode 100644 index 0000000000..e1f71adac9 Binary files /dev/null and b/versions/4.0/zh/getting-started/first-game/images/title-inspector.png differ diff --git a/versions/4.0/zh/getting-started/first-game/index.md b/versions/4.0/zh/getting-started/first-game/index.md new file mode 100644 index 0000000000..b6afc7d845 --- /dev/null +++ b/versions/4.0/zh/getting-started/first-game/index.md @@ -0,0 +1,1181 @@ +# 快速上手:制作第一个 3D 游戏 + +本节我们将向您介绍一些 Cocos Creator 的特性,并展示如何使用 Cocos Creator 制作一个虽然简单但是完整的跳跃游戏。 + +下面我们将跟随教程制作一款名叫 **一步两步** 的魔性小游戏。这款游戏考验玩家的反应能力,根据路况选择是要跳一步还是跳两步,“一步两步,一步两步,一步一步似爪牙似魔鬼的步伐”。 + +可以在 [这里](https://gameall3d.github.io/MindYourStep_Tutorial/index.html) 体验一下游戏的完成形态。 + +## 添加主角 + +对于绝大多数游戏来说,都需要一个可以操控的角色。接下来我们将一步一步的制作游戏中的主角:胶囊体先生/女士。 + +为了方便制作,我们在这里稍微回顾下编辑器内是如何创建节点的: + +![hierarchy.png](images/hierarchy.png) + +引擎左侧的名为 **层级管理器** 的窗口显示所有当前场景内的节点,并且您在里面点击鼠标右键后可以弹出创建新的节点的菜单。 + +### 创建主角节点 + +首先创建一个名为 Player 的空节点,然后在这个空节点下创建名为 Body 的主角模型节点,为了方便,我们采用编辑器自带的胶囊体模型做为主角模型。 + +![create player node](./images/create-player.gif) + +分为两个节点的好处是,我们可以使用脚本控制 Player 节点来使主角进行水平方向移动,而在 Body 节点上做一些垂直方向上的动画(比如原地跳起后下落),两者叠加形成一个跳越动画。 + +### 创建跑道 + +我们使用立方体(Cube)来表示跑道,创建三个 Cube 并依次摆放,作为跑道。 + +然后将 Player 节点设置在(0,0,0)位置,使得它能站在第一个方块上。效果如下: + +![create player](./images/create-player.png) + +### 编写主角脚本 + +想要主角响应鼠标事件来进行移动,我们就需要编写自定义的脚本。如果您从没写过程序也不用担心,我们会在教程中提供所有需要的代码,只要复制粘贴到正确的位置就可以了。我们也会对所有的代码进行讲解,以帮助您尽快上手 Cocos Creator。 + +#### 创建脚本 + +1. 如果还没有创建 Scripts 文件夹,首先在 **资源管理器** 中右键点击 **assets** 文件夹,选择 **新建 -> 文件夹**,重命名为 Scripts。 +2. 右键点击 Scripts 文件夹,选择 **新建 -> TypeScript**,创建一个 TypeScript 脚本,有关 TypeScript 资料可以查看 [TypeScript 官方网站](https://www.typescriptlang.org/)。 +3. 将新建脚本的名字改为 `PlayerController`,双击这个脚本,打开代码编辑器(例如 VSCode)。 + + ![create player script](./images/create-player-script.gif) + +**注意**:Cocos Creator 中脚本名称就是组件的名称,这个命名是大小写敏感的!如果组件名称的大小写不正确,将无法正确通过名称使用组件! + +> 关于重命名:如果输入名字时不慎输入错误,需要同时重命名文件、类名以及装饰器内的名字,因此如果您不太熟悉操作的情况下输入了错误的名字,可以考虑删除文件重新创建。 + +#### 编写脚本代码 + +在打开的 PlayerController 脚本里已经有了预先设置好的一些代码块,如下所示: + +```ts +import { _decorator, Component } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + /* class member could be defined like this */ + // dummy = ''; + + /* use `property` decorator if your want the member to be serializable */ + // @property + // serializableDummy = 0; + + start () { + // Your initialization goes here. + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } +} +``` + +这些代码就是编写一个组件(脚本)所需的结构。其中,继承自 `Component` 的脚本称之为 **组件(Component)**,它能够挂载到场景中的节点上,用于控制节点的行为,更详细的脚本信息可以查看 [脚本](../../scripting/index.md)。 + +接下来我们来完善 `PlayerController` 的代码以让角色可以真正的动起来。 + +#### 监听输入 + +在游戏中,我们需要通过监听计算机的输入(鼠标、键盘或者手柄等)来操作角色,在 Cocos Crateor 中,您可以通过监听 `input` 的事件来完成: + +```ts +input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); +``` + +上述代码表示了 我们通过监听 **输入系统(`input`)** 的 **鼠标弹起事件(`MOUSE_UP`)**,当接收到鼠标弹起事件后,这个脚本内的 `onMouseUp` 会被调用。 + +通常我们会将这些初始化代码放在 **组件** 的 `start` 方法内,以确保当角色初始化后,可以正确的监听鼠标输入。 + +> `start` 函数表示该组件已正确的初始化,您可以放心的使用。 + +因此我们可以看到下面的代码: + +```ts +import { _decorator, Component, input, Input, EventMouse } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + + + } +} +``` + +#### 让角色动起来 + +为了让角色动起来,我们需要给角色添加一些额外的属性来描述他,回想一下我们在中学阶段学习物理时处理的小方块或者小球的问题。是的没错,在游戏开发中,我们要让角色动起来也可以通过描述他的位置、速度等因素的变化来实现。 + +在当前游戏中,我们要完成这样的行为:用户按下鼠标左键/右键 -> 判定是位移一步/两步 -> 将角色根据输入向前移动直到到达目标点。 + +因此我们在脚本中添加这样的属性: + +```typescript +// 是否接收到跳跃指令 +private _startJump: boolean = false; +``` + +通过 `_startJump` 这样的布尔值变量,我们可以标记角色当前是否在跳跃中,以帮助我们在 `update` 中区分不同的分支逻辑。因为很显然,当没有接受到输入时,我们是不需要移动角色的。 + +> Q:为什么要在 `update` 处理? +> +> A:在游戏开发中,`update` 会以特定的时间间隔进行,如我们游戏为 60 FPS(即每秒渲染 60 帧),那么update 每秒就会被调用 60 次。通过这种方式,我们可以尽可能的去模拟现实中连续的行为。 + +因此我们可以将代码写成这样: + +```typescript +import { _decorator, Component, input, Input, EventMouse } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + // 是否接收到跳跃指令 + private _startJump: boolean = false; + // 跳跃步长 + private _jumpStep: number = 0; + + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + + + } + + update(dt: number): void { + if( this._startJump){ // 处理跳跃的分支逻辑 + + } + } +} +``` + +很好,我们已经有了一个角色控制器的主体框架了,接下来我们只需要思考下,如何在 `update` 里面让角色动起来就好了! + +在牛顿力学里面我们学过,如果要让一个物体匀速的运动,那么他的位置一定是这样: + +```txt +P_1 = P_0 + v*t +``` + +也就是说,我们需要将物体当前的位置加上速度乘以时间,那么就可以得到物体新的位置。 + +在我们的游戏中,鼠标按下那一刻,如果跳跃步数为 1,角色会向前移动 1 个单位,而如果跳跃步数为 2,那么移动 2 个单位。因此我们可以计算出角色的目标位置(_targetPos)。 + +``` +目标位置(_targetPos) = 当前位置(_curPos) + 步长(step) +``` + +可以看到,我们上述的公式中有 3 个信息:_targetPos、_curPos 以及 step,记录下这些信息,这样我们可以在 `update` 里面去使用他们。 + +在 `PlayerController` 内添加如下的成员以记录这些信息: + +```ts +// 跳跃步长 +private _jumpStep: number = 0; +// 当前角色位置 +private _curPos: Vec3 = new Vec3(); +// 角色目标位置 +private _targetPos: Vec3 = new Vec3(); +``` + +此时的代码看起来是这样的: + +```ts +import { _decorator, Component, input, Input, EventMouse, Vec3 } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + // 是否接收到跳跃指令 + private _startJump: boolean = false; + // 跳跃步长 + private _jumpStep: number = 0; + // 当前角色位置 + private _curPos: Vec3 = new Vec3(); + // 角色目标位置 + private _targetPos: Vec3 = new Vec3(); + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + + + } + + update(dt: number): void { + if( this._startJump){ + + } + } +} +``` + +接下来我们要处理的是,如何通过计算得到这些信息。 + +首先需要判定下,玩家究竟是按了鼠标左键还是右键? + +这样的判定我们可以通过 `onMouseUp` 这个方法中取到鼠标的按键来得到。在 `EventMouse` 中,如果是鼠标左键,那么 `getButton` 方法会返回 0,而如果是右键,则返回 2。 + +那么我们扩展下 `onMouseUp` 方法如下: + +```ts +onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + + } + else if (event.getButton() === 2) { + + } +} +``` + +我们在给组件添加一个用于计算目标位置、速度的方法 `jumpByStep` 来协助我们的计算。因为输入可能是 1 步或 2 步。因此给 `jumpByStep` 方法添加一个参数 `step` 可以更好的实现复用性(Reusability)。 + +> 复用性? 这样的概念听来就很复杂! +> 不用紧张,这只是一些工程上的术语,我们初学的时候可以忽略掉他们。 +> 这里只是为了能更清楚的解释下为什么我们要添加一个名为 `jumpByStep` 的方法。 + +这个函数应该看起来是这样的: + +```ts +jumpByStep(step: number) { + +} +``` + +我们用 step 来表示跳跃几步。 + +我们的游戏中,跳跃是一个完整的步骤,当这个步骤没有完成时,我们不接受任何输入,因此我们也通过 `_startJump` 来跳过跳跃过程中的用户输入。 + +之后我们将计算,在特定时间内,角色要从当前位置移动到目标位置,那他的移动速度应该为多少。 + +这个流程看起来是这样的: + +1. 计算移动速度(_curJumpSpeed) +2. 计算目标位置(_targetPos) + +为了在 `update` 里面我们可以正确的将 `if( this._startJump)` 分支退出,我们还需要记录下当前开始跳跃的时间(_curJumpTime),因为当跳跃的过程超过这个时间时,我们认为这个跳跃过程结束了。 + +因此 `jumpByStep` 看起来是这样的: + +```ts +jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; // 表示开始跳跃 + this._jumpStep = step; // 本次跳跃的步数 + this._curJumpTime = 0; // 重置下跳跃的时间 + this._curJumpSpeed = this._jumpStep / this._jumpTime; // 计算跳跃的速度 + this.node.getPosition(this._curPos); // 获取角色当前的位置 + // 目标位置 = 当前位置 + 步长 + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); +} +``` + +- `Vec3.add` 这是什么? + - `Vec3` 是 Cocos Creator 提供用于记录和操作三维向量(Three-Dimensionval Vector)类 + - `add` 是将两个向量加起来的方法,注意到我们这里并没有 `this` 是因为这个方法是一个 **静态方法** + - `Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0));` 这段代码的意思就是将 **当前位置(`_curPos`)** 加上 `new Vec3(this._jumpStep, 0, 0)` 并得到一个新的向量,而这个向量的结果是存放在 **目标位置(`_targetPos`)** 里面。 +当然我们也可以采用简单的直接操作位置信息的 X 坐标的方式来移动角色,但是在制作 3D 游戏时,通常会有多个轴的移动,尽快熟悉 3 维向量的用法可以让我们更快的上手制作 3D 游戏。 + +此时我们的代码看起来是这样的: + +```ts +import { _decorator, Component, input, Input, EventMouse, Vec3 } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + + // 是否接收到跳跃指令 + private _startJump: boolean = false; + // 跳跃步长 + private _jumpStep: number = 0; + // 当前跳跃时间 + private _curJumpTime: number = 0; + // 每次跳跃时长 + private _jumpTime: number = 0.1; + // 当前跳跃速度 + private _curJumpSpeed: number = 0; + // 当前角色位置 + private _curPos: Vec3 = new Vec3(); + // 每次跳跃过程中,当前帧移动位置差 + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + // 角色目标位置 + private _targetPos: Vec3 = new Vec3(); + + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + + } + else if (event.getButton() === 2 ) { + + } + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); + } + + + update(dt: number): void { + if( this._startJump){ + + } + } +} +``` + +非常好,到这里我们已经得到了角色在每次移动时所需要的数据,那么接下来就是在 `update` 方法里面去移动他。 + +还记上我们上文说到的牛顿问题吗?接下来就是真正解决这个问题的时候了。 + +我们需要首先需要检测下,整个跳跃的时长(_curJumpTime)是否已经超过我们预定义的时间(_jumpTime)。 + +这里的检测很简单,我们在输入时,已经设定了 _curJumpTime 为 0,那么 `update` 只需: + +``` +跳跃时间 = 上次的跳跃时间 + 帧间隔 +``` + +就可以了。 + +而 `update` 方法恰好提供了 `deltaTime` 这样的参数作为帧间隔。 + +因此我们就将 `_curJumpTime` 加上 `deltaTime` 即可: + +```ts +update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + } +} +``` + +> 可以看到我们使用了 '+=' 这个运算符,相当是 `this._curJumpTime = this._curJumpTime + deltaTime`。 + +当 `_curJumpTime` 大于我们提前定义好的 `_jumpTime` 时,这意味着跳跃结束,因此我们就需要两个分支,分别处理跳跃结束和跳跃中这两个状态: + +```ts +update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { // 跳跃结束 + + + } else { // 跳跃中 + + } + } +} +``` + +最后我们针对不同分支,完善他们的逻辑: + +- 跳跃结束:结束跳跃过程,将角色强制位移到目标位置 +- 跳跃中:根据速度将角色向前移动 + +因此我们的 `update` 方法会变成这样: + +```ts +update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { // 跳跃结束 + // end + this.node.setPosition(this._targetPos); // 强制位移到目标位置 + this._startJump = false; // 标记跳跃结束 + } else { // 跳跃中 + // tween + this.node.getPosition(this._curPos); // 获取当前的位置 + this._deltaPos.x = this._curJumpSpeed * deltaTime; // 计算本帧应该位移的长度 + Vec3.add(this._curPos, this._curPos, this._deltaPos); // 将当前位置加上位移的长度 + this.node.setPosition(this._curPos); // 设置位移后的位置 + } + } +} +``` + +太棒了!您已经完成了本游戏的核心内容 -- 角色控制。 + +如果您觉得这样的说明仍然有些困难的话,请点击 [获取帮助和支持](../support.md) 让我们知道。 + +#### 完整的 PlayerController + +我们已经拥有了完整的 PlayerController 代码,此时只需要将其挂在某个节点上就可以使其生效了。 + +如果还是觉得有些困难,可以尝试将下面的代码复制粘贴到你的项目内的 PlayerController.ts 文件内。 + +```ts +import { _decorator, Component, Vec3, input, Input, EventMouse, Animation } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("PlayerController") +export class PlayerController extends Component { + /* class member could be defined like this */ + // dummy = ''; + + /* use `property` decorator if your want the member to be serializable */ + // @property + // serializableDummy = 0; + + // for fake tween + // 是否接收到跳跃指令 + private _startJump: boolean = false; + // 跳跃步长 + private _jumpStep: number = 0; + // 当前跳跃时间 + private _curJumpTime: number = 0; + // 每次跳跃时长 + private _jumpTime: number = 0.1; + // 当前跳跃速度 + private _curJumpSpeed: number = 0; + // 当前角色位置 + private _curPos: Vec3 = new Vec3(); + // 每次跳跃过程中,当前帧移动位置差 + private _deltaPos: Vec3 = new Vec3(0, 0, 0); + // 角色目标位置 + private _targetPos: Vec3 = new Vec3(); + + start () { + // Your initialization goes here. + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } + + onMouseUp(event: EventMouse) { + if (event.getButton() === 0) { + this.jumpByStep(1); + } + else if (event.getButton() === 2) { + this.jumpByStep(2); + } + + } + + jumpByStep(step: number) { + if (this._startJump) { + return; + } + this._startJump = true; + this._jumpStep = step; + this._curJumpTime = 0; + this._curJumpSpeed = this._jumpStep / this._jumpTime; + this.node.getPosition(this._curPos); + Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0)); + } + + update (deltaTime: number) { + if (this._startJump) { + this._curJumpTime += deltaTime; + if (this._curJumpTime > this._jumpTime) { + // end + this.node.setPosition(this._targetPos); + this._startJump = false; + } else { + // tween + this.node.getPosition(this._curPos); + this._deltaPos.x = this._curJumpSpeed * deltaTime; + Vec3.add(this._curPos, this._curPos, this._deltaPos); + this.node.setPosition(this._curPos); + } + } + } +} +``` + +现在我们可以把 `PlayerController` 组件添加到主角节点 `Player` 上。在 **层级管理器** 中选中 `Player` 节点,然后在 **属性检查器** 中点击 **添加组件** 按钮,选择 **添加用户脚本组件 -> PlayerController**,为主角节点添加 `PlayerController` 组件。 + +![add player controller comp](./images/add-player-controller.png) + +为了能在运行时看到物体,我们需要将场景中 Camera 的参数进行一些调整,`Position` 设置为(0,0,13),`Color` 设置为(50,90,255,255): + +![camera setting](./images/camera-setting.png) + +然后点击工具栏中心位置的 Play 按钮: + +![play button](./images/play.png) + +在打开的网页中点击鼠标左键和右键,可以看到如下画面: + +![player move](./images/player-move.gif) + +更多的预览功能,可以参考 [项目预览调试](../../editor/preview/index.md) + +## 添加角色动画 + +从上面运行的结果可以看到单纯对 Player 进行水平方向的移动是十分呆板的,我们要让 Player 跳跃起来才比较有感觉,可以通过为 Player 添加垂直方向的动画来达到这个效果。有关 **动画编辑器** 的更多信息,请阅读 [动画编辑器](../../animation/index.md) + +1. 选中场景中的 Body 节点,然后在编辑器下方的 **动画编辑器** 中添加 Animation 组件并创建 Clip,命名为 `oneStep`。 + + ![player move](./images/add-animation.gif) + +2. 进入动画编辑模式,添加 position 属性轨道,并添加三个关键帧,position 值分别为(0,0,0)、(0,0.5,0)、(0,0,0)。 + + ![add keyframe](./images/add-keyframe.gif) + + **注意**:退出动画编辑模式前记得要保存动画,否则做的动画就白费了。 + +3. 我们还可以通过 **资源管理器** 来创建 Clip。创建一个名为 `twoStep` 的 Clip 并将它添加到 Body 的 `Animation` 上,这里为了录制方便调整了一下面板布局。 + + > **注意**:若发现无法拖动到 `Animation` 上,请检查 `import {...} from "cc"` 语句中是否包含了 `Animation`。 + + ![add animation from assets](./images/add-animation-from-assets.gif) + +4. 进入动画编辑模式,选择并编辑 `twoStep` 的 Clip,类似第 2 步,添加三个 position 的关键帧,分别为(0,0,0)、(0,1,0)、(0,0,0)。 + + ![edit second clip](./images/edit-second-clip.png) + +5. 在 `PlayerController` 组件中引用 **动画组件**,我们需要在代码中根据跳的步数不同来播放不同的动画。 + + 首先需要在 `PlayerController` 组件中引用 Body 身上的 `Animation`。 + + ```ts + @property({type: Animation}) + public BodyAnim: Animation | null = null; + ``` + + 需要注意的是,如果发现 `Aniamtion` 无法拖动或不显示,请检查该文件的 `import` 段是否已添加 `Animation` 的导入,代码示例如下: + + ```ts + import { Animation } from "cc"; + ``` + + 然后在 **属性检查器** 中将 Body 身上的 `Animation` 拖到这个变量上。 + + ![drag to animComp](./images/drag-to-animComp.gif) + + 在跳跃的函数 `jumpByStep` 中加入动画播放的代码: + + ```ts + if (this.BodyAnim) { + if (step === 1) { + this.BodyAnim.play('oneStep'); + } else if (step === 2) { + this.BodyAnim.play('twoStep'); + } + } + ``` + +6. 最后点击 Play 按钮,点击鼠标左键和右键,可以看到新的跳跃效果: + + ![preview with jump](./images/preview-with-jump.gif) + +## 跑道升级 + +为了让游戏有更久的生命力,我们需要一个很长的跑道让 Player 在上面一直往右跑。在场景中复制一堆 Cube 并编辑位置来组成跑道显然不是一个明智的做法,我们可以通过脚本完成跑道的自动创建。 + +### 游戏管理器(GameManager) + +一般来说,场景内会有不同功能,不同类型的节点,这些节点存放在场景里面我们可以将其视为我们游戏重要的数据部分。而很多情况下我们需要动态的生成、访问、删除这些节点。虽然在 Cocos Creator 里面我们可以通过 `find` 方法来查找这些节点,但实际上由于 `find` 方法需要访问场景内的所有节点,查找的命中率很低,这会造成大量的性能浪费。因此在开发中,我们一般会做一些单独的脚本,使用他们来管理场景内的 **某一类** 节点,或 **某一些** 数据,我们可以称他们为 **管理器**。 + +举个例子,我们有很多角色,我们游戏可能需要不断的创建新的角色,删除某些已经死亡的角色,查询某些角色的状态,那么我们可以创建一个名为 ActorManager 的类来作为角色管理器,使其支持这些功能。 + +因此在一个游戏中,我们同样可以声明一个游戏管理器来管理和游戏相关的节点或者数据,我们将其称为 GameManager。 + +这样我们可以将跑道的生成、删除等功能都放在 GameManager 里面,这样可以很方便的对数据进行集合。 + +这种聚合的方式在游戏开发中非常常见,如果您遇见其他如 ConfigManager、NetworkManager 之类的脚本,请不要感到奇怪。 + +当然更复杂的游戏会有很复杂的结构设计,这些复杂、庞大的设计理念可以更好的提升我们代码的健壮性、可维护性。 + +那么我们接下来就去创建这样的管理器。 + +### 创建管理器 + +1. 在场景中创建一个名为 GameManager 的节点。 +2. 然后在 `assets/Scripts` 中创建一个名为 GameManager 的 TypeScript 脚本文件。 +3. 将 `GameManager` 组件添加到 GameManager 节点上。 + +### 制作Prefab + +对于需要重复生成的节点,我们可以将它保存成 [Prefab(预制)资源](../../asset/prefab.md),作为我们动态生成节点时使用的模板。 + +> 预制体是引擎内置的一种资源,其主要的作用是为某些节点提供克隆的可能。想象一下如果游戏有 1000 个相同的敌人,那么制作 1000 个这样节点是非常耗时的,预制体可以帮助我们很好的解决这个问题,我们只需将这个角色的预制体克隆 1000 次就可以了 + +将生成跑道的基本元素 **立方体(Cube)** 制作成 Prefab,之后可以把场景中的三个 Cube 都删除了。 + +![create cube prefab](./images/create-cube-prefab.gif) + +### 添加自动创建跑道代码 + +Player 需要一个很长的跑道,理想的方法是能动态增加跑道的长度,这样可以永无止境地跑下去,这里为了方便先生成一个固定长度的跑道,跑道长度可以自己定义。另外,我们可以在跑道上生成一些坑,当 Player 跳到坑上就 GameOver 了。 + +首先我们需要定义一些常量用于表示当前的坐标代表的究竟是坑还是石块。在 Typescript 里面可以通过枚举来定义。 + +通过关键字 `enum` + 枚举的名称我们定义了如下的枚举,其中 `BT_NONE` 代表坑,而 `BT_STONE` 代表可行走的地面。 + +```typescript +enum BlockType{ + BT_NONE, + BT_STONE, +}; +``` + +同样我们需要定义下游戏管理器使用那个或者那些预制体来创建地图。 + +```ts +// 赛道预制 +@property({type: Prefab}) +public cubePrfb: Prefab | null = null; +``` + +`@property({type: Prefab})` 可以让编辑器将 `cubePrfb` 在编辑器内识别为预制体,这样我们可通过编辑器来给这属性分配前面已经制作好的方块。 + +> `@property` 这样的语法被称为 [装饰器](../../scripting/decorator.md)。Cocos Cretor 提供不同的装饰器,您可以用来修饰脚本以给编辑器添加多样化的内容。 + +为了方便快速查找到当前所在的位置是方块还是坑,我们通过一个数组来描述整个赛道的情况: + +```ts +_road: BlockType[] = []; +``` + +这样一个基础的 GameManager 就定义好了,他看起来应该是这样的: + +```ts +import { _decorator, Component, Prefab } from 'cc'; +const { ccclass, property } = _decorator; + +// 赛道格子类型,坑(BT_NONE)或者实路(BT_STONE) +enum BlockType { + BT_NONE, + BT_STONE, +}; + +@ccclass("GameManager") +export class GameManager extends Component { + + // 赛道预制 + @property({type: Prefab}) + public cubePrfb: Prefab | null = null; + // 赛道长度 + @property + public roadLength = 50; + private _road: BlockType[] = []; + + start () { + this.generateRoad(); + } + + generateRoad() { + + } +} +``` + +接下来我们根据一些原则来生成整个赛道。这些原则包括: + +- 第一个赛道必须是石块以确保角色生成的时候不会掉下去 +- 后面的方块我们将随机生成方块或者坑,但是考虑到我们角色贫瘠的跳跃能力(最多只能跳 2 个单位)因此我们不能连续生成超过 2 个坑。因此我们在生成新的格子时,需要判断前一个格子是石块还是坑,如果是坑,那么这个格子必须是石块。 +- 根据上面两个原则,我们生成一个记录路况的数组 `_road`,用于快速查询某个位置到底是坑还是石头 +- 最后我们根据当前的路况信息 `_road` 将方块实例化到场景内,完成地图的展示。 + +因此地图生成方法 `generateRoad` 可以写成这样: + +```ts +generateRoad() { + // 防止游戏重新开始时,赛道还是旧的赛道 + // 因此,需要移除旧赛道,清除旧赛道数据 + this.node.removeAllChildren(); + this._road = []; + // 确保游戏运行时,人物一定站在实路上 + this._road.push(BlockType.BT_STONE); + + // 确定好每一格赛道类型 + for (let i = 1; i < this.roadLength; i++) { + // 如果上一格赛道是坑,那么这一格一定不能为坑 + if (this._road[i-1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + // 根据赛道类型生成赛道 + for (let j = 0; j < this._road.length; j++) { + let block: Node = this.spawnBlockByType(this._road[j]); + // 判断是否生成了道路,因为 spawnBlockByType 有可能返回坑(值为 null) + if (block) { + this.node.addChild(block); + block.setPosition(j, -1.5, 0); + } + } +} +``` + +> `Math.floor`: 这个方法是 Typescript 数学库的方法之一:我们知道 floor 是地板的意思,这表示取这个方法参数的 "地板",也就是向下取整。 +> `Math.random`:同样 random 也是标准数学库的方法之一,用于随机一个 0 到 1 之间的小数,注意取值范围是 [0, 1)。 +> 所以 `Math.floor(Math.random() * 2)` 这段代码的意思很简单,就是从 [0, 2) 中随机取 1个数并向下取整,得到的结果是 0 或者 1,恰好和 枚举 `BlockType` 中声明的 `BT_NONE` 和 `BT_STONE` 对应。 +> 顺便说一句,在 Typescript 的枚举中,如果你没有给枚举赋值,那么枚举的值会顺序的从 0 开始分配。 + +生成石块的方法 `spawnBlockByType` 看起来是这样的: + +```ts +spawnBlockByType(type: BlockType) { + if (!this.cubePrfb) { + return null; + } + + let block: Node | null = null; + // 赛道类型为实路才生成 + switch(type) { + case BlockType.BT_STONE: + block = instantiate(this.cubePrfb); + break; + } + + return block; +} +``` + +可以注意到当格子的类型是石块时,我们将通过 `instantiate` 克隆一个新的预制体出来;而格子的类型不为石块时,我们什么也不做。 + +> instantiate: 是 Cocos Creator 提供的克隆预制体的方法。当然它不仅能克隆预制体,你甚至可以用它克隆别的类型比如某个对象! + +完整的 `GameManager` 脚本中的代码如下: + +```ts +import { _decorator, Component, Prefab, instantiate, Node, CCInteger } from 'cc'; +const { ccclass, property } = _decorator; + +// 赛道格子类型,坑(BT_NONE)或者实路(BT_STONE) +enum BlockType { + BT_NONE, + BT_STONE, +}; + +@ccclass("GameManager") +export class GameManager extends Component { + + // 赛道预制 + @property({type: Prefab}) + public cubePrfb: Prefab | null = null; + // 赛道长度 + @property + public roadLength = 50; + private _road: BlockType[] = []; + + start () { + this.generateRoad(); + } + + generateRoad() { + // 防止游戏重新开始时,赛道还是旧的赛道 + // 因此,需要移除旧赛道,清除旧赛道数据 + this.node.removeAllChildren(); + this._road = []; + // 确保游戏运行时,人物一定站在实路上 + this._road.push(BlockType.BT_STONE); + + // 确定好每一格赛道类型 + for (let i = 1; i < this.roadLength; i++) { + // 如果上一格赛道是坑,那么这一格一定不能为坑 + if (this._road[i-1] === BlockType.BT_NONE) { + this._road.push(BlockType.BT_STONE); + } else { + this._road.push(Math.floor(Math.random() * 2)); + } + } + + // 根据赛道类型生成赛道 + for (let j = 0; j < this._road.length; j++) { + let block: Node = this.spawnBlockByType(this._road[j]); + // 判断是否生成了道路,因为 spawnBlockByType 有可能返回坑(值为 null) + if (block) { + this.node.addChild(block); + block.setPosition(j, -1.5, 0); + } + } + } + + spawnBlockByType(type: BlockType) { + if (!this.cubePrfb) { + return null; + } + + let block: Node | null = null; + // 赛道类型为实路才生成 + switch(type) { + case BlockType.BT_STONE: + block = instantiate(this.cubePrfb); + break; + } + + return block; + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } +} +``` + +将上面制作好的 Cube 的 prefab 拖到 GameManager 在 **属性检查器** 中的 CubePrfb 属性上。 + +![assign cube prefab](./images/assign-cube-prefab.png) + +在 GameManager 的 **属性检查器** 面板中可以通过修改 roadLength 的值来改变跑道的长度。 +此时点击预览可以看到自动生成了跑道。 + +## 跟随相机 + +可以观察到 Camera 没有跟随 Player 移动,所以看不到后面的跑道,我们可以将场景中的 Camera 设置为 Player 的子节点。 + +![drag camera to player](./images/drag-camera-to-player.gif) + +这样 Camera 就会跟随 Player 的移动而移动,现在点击预览可以从头跑到尾地观察生成的跑道了。 + +## 增加开始菜单 + +用户界面(UI)是游戏中不可缺少的一部分,通过 UI,我们可以给玩家提供到某些关键上数据信息、状态等,这样玩家在游戏时可以思考自己的行为策略。我们可以在也这里加入游戏名称、游戏简介、制作人员等信息。 + +UI 是游戏开发中的 2D 部分,您可以通过查阅 [2D/UI](../../2d-object/index.md) 相关的文档来了解。 + +下面我们将以开始菜单为例子,简述 UI 的制作流程。 + +1. 在 **层级管理器** 中添加一个 Button 节点并命名为 PlayButton。 + + ![create button](./images/create-button.gif) + + 可以看到在 **层级管理器** 中生成了一个 Canvas 节点,一个 PlayButton 节点和一个 Label 节点。因为 UI 组件需要在带有 `Canvas` 的父节点下才能显示,所以编辑器在发现没有 Canvas 节点时会自动创建一个。 + + 然后将 Label 节点上 `cc.Label` 组件中的 **String** 属性从 Button 改为 Play。 + + > 您可能会注意到,在您所使用的 Cocos Creator 版本中在创建 UI 时会自动添加一个相机(Camera)节点。 + > 这个节点是新版本的 Cocos Creator 的特性,这个相机将负责渲染整个 UI/2D 部分的内容。以此引擎可以通过更高效的渲染管线来处理这些 UI/2D 的内容。 + +2. 在 Canvas 底下创建一个名为 StartMenu 的空节点,将 PlayButton 拖到它底下。我们可以通过点击工具栏上的 2D/3D 按钮切换到 2D 编辑视图下进行 UI 编辑操作,详细的描述请查阅 [场景编辑](../../editor/scene/index.md)。 + + ![2d-view](./images/2d-view.png) + +3. 在 StartMenu 下新建一个名为 `BG` 的 Sprite 节点作为背景框,调整它的位置到 PlayButton 的上方。 + + ![create bg sprite](./images/create-bg-sprite.gif) + + 然后在 **属性检查器** 中将 `cc.UITransform` 组件的 `ContentSize` 设置为(200,200),同时将 **资源管理器** 中的 `internal/default_ui/default_sprite_splash` 拖拽到 **SpriteFrame** 属性框中。 + + ![change spriteFrame](./images/change-spriteFrame.png) + +4. 在 **层级管理器** 的 StartMenu 节点下添加一个名为 Title 的 Label 节点用于开始菜单的标题。 + + ![add title label](./images/add-label-title.gif) + +5. 在 **属性检查器** 中设置 Title 节点的属性,例如 `Position`、`Color`、`String`、`FontSize` 等。 + + ![modify title](./images/title-inspector.png) + +6. 根据需要增加操作的 Tips 节点,然后调整 PlayButton 的位置,一个简单的开始菜单就完成了 + + ![modify title](./images/start-menu.png) + +## 增加游戏状态逻辑 + +对于大多数游戏来说,我们可以将其粗略的分解为 3 个不同的状态:初始化、游玩、结算。就和我们玩任意的棋子游戏一样,摆放棋子的过程我们称之为初始化;下棋的过程则为游玩过程;最后两名棋手下棋结束结算胜利/失败时的状态,我们称之为结算状态。 + +因此同理的我们可以定义如下的枚举来代表游戏状态。 + +```ts +enum GameState{ + GS_INIT, + GS_PLAYING, + GS_END, +}; +``` + +- 初始化(Init):显示游戏菜单,初始化一些资源。 +- 游戏进行中(Playing):隐藏游戏菜单,玩家可以操作角色进行游戏。 +- 结束(End):游戏结束,显示结束菜单。 + +这样的作用是我们可以比较方便的通过读取 `GameState` 的枚举值就可以知道现在游戏处于什么状态;或者当状态切换时,我们的游戏需要处理些什么。 + +为了在游戏开始时不让用户操作角色,而在游戏进行时让用户操作角色,我们需要动态地开启和关闭角色对鼠标消息的监听。在 `PlayerController` 脚本中做如下修改: + +```ts +start () { + // Your initialization goes here. + // input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); +} + +setInputActive(active: boolean) { + if (active) { + input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } else { + input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this); + } +} +``` + +然后在 `GameManager` 脚本中引用 `PlayerController` 脚本: + +```ts +@property({type: PlayerController}) +public playerCtrl: PlayerController | null = null; +``` + +完成后保存脚本,回到编辑器,将 **层级管理器** 中挂载了 `PlayerController` 脚本的 Player 节点拖拽到 GameManager 节点的 `playerCtrl` 属性框中。 + +同时,为了动态地开启/关闭开始菜单,还需要在 `GameManager` 脚本中引用 StartMenu 节点: + +```ts +@property({type: Node}) +public startMenu: Node | null = null; +``` + +完成后保存脚本,回到编辑器,将 **层级管理器** 中的 StartMenu 节点拖拽到 GameManager 节点的 `startMenu` 属性框中。 + +![add player to game manager](./images/game-manager-player.png) + +### 增加状态切换代码 + +增加状态切换代码并修改 GameManager 脚本的初始化方法: + +```ts +start () { + this.curState = GameState.GS_INIT; +} + +init() { + // 激活主界面 + if (this.startMenu) { + this.startMenu.active = true; + } + // 生成赛道 + this.generateRoad(); + if(this.playerCtrl){ + // 禁止接收用户操作人物移动指令 + this.playerCtrl.setInputActive(false); + // 重置人物位置 + this.playerCtrl.node.setPosition(Vec3.ZERO); + } +} + +set curState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + if (this.startMenu) { + this.startMenu.active = false; + } + // 设置 active 为 true 时会直接开始监听鼠标事件,此时鼠标抬起事件还未派发 + // 会出现的现象就是,游戏开始的瞬间人物已经开始移动 + // 因此,这里需要做延迟处理 + setTimeout(() => { + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + break; + case GameState.GS_END: + break; + } +} +``` + +可以观察到我们增加的 `init` 方法,他初始化了整个赛道,同时激活主界面并禁止玩家输入。之后在游戏状态切换到 `GS_INIT` 时,我们将会调用该方法。 + +而首次出现的 `set curState` 这样的存取器可以让我们在设置游戏状态时更加的安全。 + +### 添加对 Play 按钮的事件监听 + +为了能在点击 Play 按钮后开始游戏,我们需要对按钮的点击事件做出响应。在 `GameManager` 脚本中加入响应按钮点击的代码,以便用户在点击按钮后进入游戏的 Playing 状态: + +```ts +onStartButtonClicked() { + this.curState = GameState.GS_PLAYING; +} +``` + +然后在 **层级管理器** 中选中 PlayButton 节点,在 **属性检查器** 的 `cc.Button` 组件中添加 ClickEvents 的响应函数,将 GameManager 节点拖拽到 `cc.Node` 属性框中: + +![play button inspector](./images/play-button-inspector.png) + +这里可以注意到,Click Events 属性 [0] 分量内分为了三个栏位,分别是: + +1. 节点:这里我们拖拽 GameManager 这个节点,意味着按钮事件将被派发到 GameManager 这个节点上 +2. 组件:拖拽 GameManager 节点后,就可以通过下拉选择 GameManager 上的组件,这里选择同名的组件 `GameManager` +3. 事件:通过下拉菜单,选择按钮的响应事件,这里要选择上述添加的 `onStartButtonClicked` 方法作为 **按钮按下的响应** + +> **注意**:上述的 3 个步骤相互依赖,且顺序必须为 1 -> 2 -> 3。 + +现在预览场景就可以点击 Play 按钮开始游戏了。 + +## 添加游戏结束逻辑 + +目前游戏角色只是呆呆的往前跑,我们需要添加游戏规则,让它跑的更有挑战性。 + +1. 角色每次跳跃结束都需要发出消息,并将自己当前所在的位置做为参数发出消息,在 `PlayerController` 脚本中记录自己跳了多少步: + + ```ts + private _curMoveIndex = 0; + // ... + jumpByStep(step: number) { + // ... + + this._curMoveIndex += step; + } + ``` + + 并在每次跳跃结束发出消息: + + ```ts + onOnceJumpEnd() { + this.node.emit('JumpEnd', this._curMoveIndex); + } + // ... + update() { + // ... + // end + // ... + this.onOnceJumpEnd(); + } + ``` + +2. 在 `GameManager` 脚本中监听角色跳跃结束事件,并根据规则判断输赢,增加失败和结束判断,如果跳到空方块或是超过了最大长度值都结束: + + ```ts + checkResult(moveIndex: number) { + if (moveIndex < this.roadLength) { + // 跳到了坑上 + if (this._road[moveIndex] == BlockType.BT_NONE) { + this.curState = GameState.GS_INIT; + } + } else { // 跳过了最大长度 + this.curState = GameState.GS_INIT; + } + } + ``` + + 监听角色跳跃消息,并调用判断函数: + + ```ts + start () { + this.curState = GameState.GS_INIT; + // '?.' 是 Typescript 的可选链写法 + // 相当于: + // if(this.playerCtrl != null) this.playerCtrl.node.on('JumpEnd', this.onPlayerJumpEnd, this); + // 可选链的写法更加的简洁 + this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this); + } + + // ... + onPlayerJumpEnd(moveIndex: number) { + this.checkResult(moveIndex); + } + ``` + + 此时预览,会发现重新开始游戏时会有判断出错的问题,这是由于重新开始时没有重置 `PlayerController.ts` 中的 `_curMoveIndex` 属性值导致的。所以我们需要在 `PlayerController` 脚本中增加一个 `reset` 函数: + + ```ts + reset() { + this._curMoveIndex = 0; + } + ``` + + 然后在 `GameManager` 脚本的 `init` 函数中调用 `reset` 来重置 `PlayerController.ts` 中的 `_curMoveIndex` 属性。 + + ```ts + init() { + // ... + this.playerCtrl.reset(); + } + ``` + +## 步数显示 + +我们可以将当前跳的步数显示在界面上,这样在跳跃过程中看着步数的不断增长会十分有成就感。 + +1. 在 Canvas 下新建一个名为 Steps 的 Label 节点,调整位置、字体大小等属性。 + + ![steps label](./images/steps-label.png) + +2. 在 `GameManager` 脚本中引用这个 Label: + + ```ts + @property({type: Label}) + public stepsLabel: Label | null = null; + ``` + + 保存脚本后回到编辑器,将 Steps 节点拖拽到 GameManager 在属性检查器中的 stepsLabel 属性框中: + + ![steps label to game manager](./images/add-steps-to-game-manager.png) + +3. 将当前步数数据更新到 Steps 节点中。因为我们现在没有结束界面,游戏结束就跳回开始界面,所以在开始界面要看到上一次跳的步数,因此我们需要在进入 Playing 状态时,将步数重置为 0。 + + ```ts + // GameManager.ts + set curState (value: GameState) { + switch(value) { + case GameState.GS_INIT: + this.init(); + break; + case GameState.GS_PLAYING: + if (this.startMenu) { + this.startMenu.active = false; + } + + if (this.stepsLabel) { + this.stepsLabel.string = '0'; // 将步数重置为0 + } + setTimeout(() => { // 直接设置 active 会直接开始监听鼠标事件,这里做了延迟处理 + if (this.playerCtrl) { + this.playerCtrl.setInputActive(true); + } + }, 0.1); + break; + case GameState.GS_END: + break; + } + } + ``` + + 然后在响应角色跳跃的函数 `onPlayerJumpEnd` 中,将步数更新到 Label 控件上 + + ```ts + onPlayerJumpEnd(moveIndex: number) { + if (this.stepsLabel) { + // 因为在最后一步可能出现步伐大的跳跃,但是此时无论跳跃是步伐大还是步伐小都不应该多增加分数 + this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex); + } + this.checkResult(moveIndex); + } + ``` + +## 总结 + +到这里您已经基本上掌握了我们本章的绝大多部分内容,接下来您可以通过提升美术资源的品质、更多游戏玩法来完善游戏的内容,为此我们也准备了 [进阶篇](./advance.md) 供您选择。当然如果您对引擎更多的特性感兴趣,也可以点击文档左侧的索引查看不同模块的内容。 + +如果您有本章有任何建议或者意见,欢迎访问我们的 [论坛](https://forum.cocos.org/) 发起讨论或者访问 [GIT](https://github.com/cocos/cocos-docs) 并向我们提交 issue。 diff --git a/versions/4.0/zh/getting-started/helloworld/index.md b/versions/4.0/zh/getting-started/helloworld/index.md new file mode 100644 index 0000000000..303b593dcf --- /dev/null +++ b/versions/4.0/zh/getting-started/helloworld/index.md @@ -0,0 +1,116 @@ +# Hello World 项目 + +了解 Cocos Dashboard 以后,我们接下来看看如何创建和打开一个 Hello World 项目。 + +## 新建项目 + +在 Cocos Dashboard 的 **项目** 选项卡中,点击右下角的 **新建** 按钮,进入 **新建项目** 页面。选择 **empty** 项目模板,设置好项目名称和项目路径 + +![empty](index/new-empty.png) + +然后点击右下方的 **创建并打开** 按钮,就会自动以空项目模板创建项目并打开: + +![editor](index/editor-panel.png) + +- **资源管理器**:显示了项目资源文件夹(`assets`)中的所有资源。 +- **场景编辑器**:用于展示和编辑场景中可视内容的工作区域。 +- **层级管理器**:用树状列表的形式展示场景中的所有节点和它们的层级关系,所有在 **场景编辑器** 中看到的内容都可以在 **层级管理器** 中找到对应的节点条目 +- **属性检查器**:用于查看并编辑当前选中节点及其组件属性的工作区域 +- **资源预览**:在 **资源管理器** 中选中资源,即可在 **资源预览** 面板中显示资源的缩略图。若选中资源所在的文件夹,即可显示文件夹下所有资源的缩略图,方便查看。 +- **动画编辑器**:用于制作一些不太复杂的、需要与逻辑进行联动的动画,例如 UI 动画。 +- **控制台**:用于显示报错、警告或其他 Cocos Creator 编辑器和引擎生成的日志信息。 + +关于编辑器各个面板,具体的内容请参考 [编辑器面板](../../editor/index.md)。 + +## 新建场景 + +在左下方的 **资源管理器** 面板中点击鼠标右键,选择 **创建 -> Scene**。 + +![scene](index/create-scene.png) + +或者也可以直接点击左上角的 **+** 按钮,然后选择 **Scene**,即可在 **资源管理器** 的 `asset` 目录下新建一个场景: + +![scene](index/scene.png) + +## 创建物体 + +在左上方的 **层级管理器** 面板中点击鼠标右键, 选择 **创建 -> 3D 对象 -> Cube 立方体**。或者也可以直接点击左上角的 **+** 按钮,然后选择 **3D 对象 -> Cube 立方体**。 + +![create-cube](index/create-cube.png) + +即可创建一个立方体并且显示在 **场景编辑器** 中: + +![cube](index/cube.png) + +## 添加脚本 + +- 新建脚本 + + 在 **资源管理器** 面板中点击鼠标右键,选择 **创建 -> TypeScript**,然后命名为 “HelloWorld”,即可在 **资源管理器** 的 `asset` 目录下新建一个脚本。 + + ![creatr-typescript](index/create-typescript.png) + +- 添加代码 + + 双击新建的脚本,脚本会自动在脚本编辑器中打开,前提是需要在编辑器菜单栏的 **Cocos Creator/File -> 偏好设置 -> 外部程序 -> 默认脚本编辑器** 中指定好使用的脚本编辑器。 + + 然后在脚本中添加 `start()` 函数,`start()` 函数会在组件第一次激活时调用,并输出 “Hello world”。 + + ```ts + import { _decorator, Component, Node } from 'cc'; const { ccclass, property } = _decorator; + + @ccclass('HelloWorld') + export class HelloWorld extends Component { + /* class member could be defined like this */ + // dummy = ''; + + /* use `property` decorator if your want the member to be serializable */ + // @property + // serializableDummy = 0; + + start () { + // Your initialization goes here. + console.info('Hello world'); + } + + // update (deltaTime: number) { + // // Your update function goes here. + // } + } + ``` + +- 绑定脚本 + + 在 **层级管理器** 中选中创建的 Cube 节点,然后在 **属性检查器** 面板最下方点击 **添加组件 -> 自定义脚本 -> HelloWorld**,即可将脚本挂载到 Cube 节点上。或者也可以直接将脚本拖拽到 **属性检查器** 面板。 + + ![script](index/script.png) + + > **注意**:场景设置完成后,切记要保存场景。 + +## 预览场景 + +简单的场景搭建完成后,就可以点击编辑器上方的 **预览** 按钮来预览游戏了。目前支持使用 **浏览器/模拟器** 进行预览。 + +![preview](index/preview.png) + +以使用 **浏览器** 预览为例,Cocos Creator 会使用您的默认浏览器运行当前游戏场景,效果如下图所示: + +![console](index/console.png) + +可以看到在浏览器的开发者工具中的日志信息中输出了 “Hello World”。 + +## 修改场景中的 Camera + +在预览中我们可以看到立方体似乎有点太小了,这时便可以通过调整场景中的 Camera 来调整场景运行时显示的区域,Camera 代表的是游戏中的玩家视角。 + +- 首先在 **层级管理器** 中选中 **Main Camera** 节点,**场景编辑器** 中便会显示变换工具 Gizmo,以及玩家视角的小窗口。 + + ![camera](index/camera.png) + +- 然后在 **场景编辑器** 中拖动 Gizmo,或者修改 **属性检查器** 中的 **Position** 属性,使玩家视角窗口中的立方体显示得更为明显。 + + ![camera-position](index/camera-position.png) + +- 然后再次在浏览器中预览,可以看到场景中的立方体就很明显了 + + ![preview](index/preview1.png) diff --git a/versions/4.0/zh/getting-started/helloworld/index/camera-position.png b/versions/4.0/zh/getting-started/helloworld/index/camera-position.png new file mode 100644 index 0000000000..71290c3aea Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/camera-position.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/camera.png b/versions/4.0/zh/getting-started/helloworld/index/camera.png new file mode 100644 index 0000000000..e7c1bb6662 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/camera.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/component.png b/versions/4.0/zh/getting-started/helloworld/index/component.png new file mode 100644 index 0000000000..29d9f6c39e Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/component.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/console.png b/versions/4.0/zh/getting-started/helloworld/index/console.png new file mode 100644 index 0000000000..20c5070816 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/console.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/console1.png b/versions/4.0/zh/getting-started/helloworld/index/console1.png new file mode 100644 index 0000000000..7f93df290c Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/console1.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/create-cube.png b/versions/4.0/zh/getting-started/helloworld/index/create-cube.png new file mode 100644 index 0000000000..eab97424f2 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/create-cube.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/create-scene.png b/versions/4.0/zh/getting-started/helloworld/index/create-scene.png new file mode 100644 index 0000000000..39ec8f1ec7 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/create-scene.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/create-typescript.png b/versions/4.0/zh/getting-started/helloworld/index/create-typescript.png new file mode 100644 index 0000000000..034608897e Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/create-typescript.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/cube.png b/versions/4.0/zh/getting-started/helloworld/index/cube.png new file mode 100644 index 0000000000..bde3f255f6 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/cube.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/dashboard.png b/versions/4.0/zh/getting-started/helloworld/index/dashboard.png new file mode 100644 index 0000000000..bf08d6f37c Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/dashboard.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/debug.png b/versions/4.0/zh/getting-started/helloworld/index/debug.png new file mode 100644 index 0000000000..d2b9cc53d0 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/debug.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/editor-panel.png b/versions/4.0/zh/getting-started/helloworld/index/editor-panel.png new file mode 100644 index 0000000000..fbfa3dc7c2 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/editor-panel.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/engine.png b/versions/4.0/zh/getting-started/helloworld/index/engine.png new file mode 100644 index 0000000000..1d53cbaa7e Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/engine.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/move.png b/versions/4.0/zh/getting-started/helloworld/index/move.png new file mode 100644 index 0000000000..223a276de5 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/move.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/new-empty.png b/versions/4.0/zh/getting-started/helloworld/index/new-empty.png new file mode 100644 index 0000000000..1bf12b43bd Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/new-empty.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/new.png b/versions/4.0/zh/getting-started/helloworld/index/new.png new file mode 100644 index 0000000000..34a9d86506 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/new.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/preview.png b/versions/4.0/zh/getting-started/helloworld/index/preview.png new file mode 100644 index 0000000000..696d583850 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/preview.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/preview1.png b/versions/4.0/zh/getting-started/helloworld/index/preview1.png new file mode 100644 index 0000000000..f93ff322a1 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/preview1.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/run.png b/versions/4.0/zh/getting-started/helloworld/index/run.png new file mode 100644 index 0000000000..90de96d712 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/run.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/scene.png b/versions/4.0/zh/getting-started/helloworld/index/scene.png new file mode 100644 index 0000000000..1299117f00 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/scene.png differ diff --git a/versions/4.0/zh/getting-started/helloworld/index/script.png b/versions/4.0/zh/getting-started/helloworld/index/script.png new file mode 100644 index 0000000000..7d368327e5 Binary files /dev/null and b/versions/4.0/zh/getting-started/helloworld/index/script.png differ diff --git a/versions/4.0/zh/getting-started/index.md b/versions/4.0/zh/getting-started/index.md new file mode 100644 index 0000000000..6fb9fc267d --- /dev/null +++ b/versions/4.0/zh/getting-started/index.md @@ -0,0 +1,19 @@ +# 新手入门 + +欢迎使用 Cocos Creator,在学习使用之前,请先参考 [安装和启动](install/index.md) 安装好 Cocos Creator。 + +在安装完编辑器之后,可以通过以下内容来熟悉编辑器,包括如何创建项目、项目结构,以及编辑器界面介绍等: + +- [使用 Dashboard](dashboard/index.md) +- [项目结构](project-structure/index.md) +- [编辑器界面](../editor/index.md) + +对编辑器有了一定的熟悉和了解之后,便可以通过简单的示例来熟悉 Cocos Creator 的开发流程: + +- [Hello world!](helloworld/index.md) +- [快速上手:制作第一个游戏](first-game/index.md) + +同时 Cocos Creator 还提供了很多的范例和教程,并支持其他第三方工具和资源等。开发者也可以直接反馈问题给 Cocos Creator 开发团队: + +- [获取帮助和支持](support.md) +- [注意事项](attention/index.md) diff --git a/versions/4.0/zh/getting-started/install/index.md b/versions/4.0/zh/getting-started/install/index.md new file mode 100644 index 0000000000..fa73bd39f5 --- /dev/null +++ b/versions/4.0/zh/getting-started/install/index.md @@ -0,0 +1,70 @@ +# 安装和启动 + +Cocos Creator 从 v2.3.2 开始接入了全新的 Dashboard 系统,能够同时对多版本引擎和项目进行统一升级和管理!Cocos Dashboard 将做为 Creator 各引擎统一的下载器和启动入口,方便大家升级和管理多个版本的 Creator。此外还集成了统一的项目管理及创建面板,方便大家同时使用不同版本的引擎开发项目 + +![Dashboard](index/dashboard-editor.png) + +## 下载 Dashboard + +您可以通过访问 [Cocos Creator 产品首页](https://www.cocos.com/creator) 上的下载链接获得 Dashboard 的安装包。 + +下载完成后双击安装包。 + +### Windows 安装说明 + +Windows 版的安装程序是一个 `.exe` 可执行文件,通常命名会是 **CocosDashboard-vX.X.X-win32-20XXXXXX.exe**,其中 **vX.X.X** 是 Cocos Dashboard 的版本号,如 v1.0.11,后面的一串数字是版本日期编号。 + +> **注意**: +> 1. 如果当前 PC 上已安装的版本号和安装包的版本号相同时,无法自动覆盖安装相同版本号的安装包,需要先卸载之前的版本才能继续安装。 +> +> 应用的安装路径默认选择 C:\CocosDashboard,可以在安装过程中进行更改。 +> +> 2. 如果出现 “不能安装需要的文件,因为 CAB 文件没有正确的数字签名。可能表明 CAB 文件损坏” 的弹窗警告,请尝试使用管理员权限进行安装。 +> +> 3. 对于部分很老并且长期处于内网或者很久没有升级操作系统补丁的 Windows 系统,运行时可能会出现一些 dll 缺失引起的相关报错,可尝试安装该系统补丁解决: +> +> 4. 如果安装过程中出现 “拒绝访问” 的弹窗,请确保本机安装的操作系统是微软官方的正式版本,而不是经过定制或精简的第三方版本。 + +### Mac 安装说明 + +Mac 版 Cocos Dashboard 的安装程序是 dmg 镜像文件,双击 dmg 文件,然后将 **CocosDashboard.app** 拖拽到您的 **应用程序** 文件夹快捷方式,或任意其他位置。然后双击拖拽出来的 **CocosDashboard.app** 就可以开始使用了。 + +> **注意**: +> 1. 如果下载后无法打开,提示 dmg 或者 app 文件已损坏、来自身份不明的开发者或者包含恶意软件等。请在 Finder(访达)中右键点击 dmg 或 app 文件,选择 **打开**,接着在弹出对话框中再次点击 **打开** 即可。然后请进入 **系统偏好设置 -> 安全性与隐私**,点击 **仍要打开**,这样以后就可以正常启动了。 +> +> 2. 在安装过程中如果出现“已损坏,无法打开”的提示,需要检查是否有类似 Xcode 的软件占用了 Dashboard 安装目录下的相关文件。如果有的话,请先退出,然后卸载 Dashboard 并重新安装。 + +### 操作系统要求 + +Cocos Dashboard 所支持的系统环境是: + +Mac OS X 所支持的最低版本是 OS X 10.9。 +Windows 所支持的最低版本是 Windows 7 64位。 + +## 运行 Cocos Dashboard + +在 Windows 系统,双击解压后 `CocosDashboard` 文件夹中的 **CocosDashboard.exe** 文件即可启动 Cocos Dashboard。 + +在 Mac 系统,双击拖动出来的 **CocosDashboard.app** 应用图标即可启动 Cocos Dashboard。 + +您可以按照习惯为入口文件设置快速启动、Dock 或快捷方式,方便您随时运行使用。 + +### 检查显卡驱动 + +对于部分 Windows 操作系统和显卡型号,可能会遇到 + +``` +This browser does not support WebGL... +``` + +的报错信息。这是由于编辑器依赖 GPU 渲染,而显卡驱动不支持导致的。如果出现这种情况,通常只要确保已成功安装显卡对应型号的官方驱动即可解决。 + +## 使用 Cocos 开发者帐号登录 + +Cocos Dashboard 启动后,会进入 Cocos 开发者帐号的登录界面。登录之后就可以享受我们为开发者提供的各种在线服务、产品更新通知和各种开发者福利。 + +如果之前没有 Cocos 开发者帐号,您可以使用登录界面中的 **注册** 按钮前往 **Cocos 开发者中心** 进行注册。或者直接进入下面的链接注册: + + + +注册完成后就可以回到 Cocos Dashboard 登录界面完成登录了!验证身份后,我们就会进入 Dashboard 界面。除了手动登出或登录信息过期,其他情况下都会用本地 session 保存的信息自动登录。 diff --git a/versions/4.0/zh/getting-started/install/index/dashboard-editor.png b/versions/4.0/zh/getting-started/install/index/dashboard-editor.png new file mode 100644 index 0000000000..40d45eeb74 Binary files /dev/null and b/versions/4.0/zh/getting-started/install/index/dashboard-editor.png differ diff --git a/versions/4.0/zh/getting-started/install/index/language.png b/versions/4.0/zh/getting-started/install/index/language.png new file mode 100644 index 0000000000..94587fe7fe Binary files /dev/null and b/versions/4.0/zh/getting-started/install/index/language.png differ diff --git a/versions/4.0/zh/getting-started/install/index/path.png b/versions/4.0/zh/getting-started/install/index/path.png new file mode 100644 index 0000000000..4f3d854e81 Binary files /dev/null and b/versions/4.0/zh/getting-started/install/index/path.png differ diff --git a/versions/4.0/zh/getting-started/install/index/startup.png b/versions/4.0/zh/getting-started/install/index/startup.png new file mode 100644 index 0000000000..83abb9fae5 Binary files /dev/null and b/versions/4.0/zh/getting-started/install/index/startup.png differ diff --git a/versions/4.0/zh/getting-started/install/index/waiting.png b/versions/4.0/zh/getting-started/install/index/waiting.png new file mode 100644 index 0000000000..4b4c9c1cca Binary files /dev/null and b/versions/4.0/zh/getting-started/install/index/waiting.png differ diff --git a/versions/4.0/zh/getting-started/introduction/index.md b/versions/4.0/zh/getting-started/introduction/index.md new file mode 100644 index 0000000000..0c35c44560 --- /dev/null +++ b/versions/4.0/zh/getting-started/introduction/index.md @@ -0,0 +1,92 @@ +# 关于 Cocos Creator + +- **Q**:Cocos Creator 是游戏引擎吗?
+ **A**:它是一个完整的游戏开发解决方案,包含了轻量高效的跨平台游戏引擎,以及能让你更快速开发游戏所需要的各种图形界面工具。 + +- **Q**:Cocos Creator 的编辑器是什么样的?
+ **A**:完全为引擎定制打造,包含从设计、开发、预览、调试到发布的整个工作流所需的全功能一体化编辑器。 + +- **Q**:我不会写程序,也能使用 Cocos Creator 吗?
+ **A**:当然!Cocos Creator 编辑器提供面向设计和开发的两种工作流,提供简单顺畅的分工合作方式。 + +- **Q**:我使用 Cocos Creator 能开发面向哪些平台的游戏?
+ **A**:Cocos Creator 目前支持发布游戏到 Web、iOS、Android、各类"小游戏"、PC 客户端等平台,真正实现一次开发,全平台运行。 + +## 产品定位 + +Cocos Creator 是以内容创作为核心,实现了脚本化、组件化和数据驱动的游戏开发工具。具备了易于上手的内容生产工作流,以及功能强大的开发者工具套件,可用于实现游戏逻辑和高性能游戏效果。 + +## 工作流程说明 + +在开发阶段,Cocos Creator 已经能够为用户带来巨大的效率和创造力提升,但我们所提供的工作流远不仅限于开发层面。对于成功的游戏来说,开发和调试、商业化 SDK 的集成、多平台发布、测试、上线这一整套工作流程不光缺一不可,而且要经过多次的迭代重复。 + +![cocos workflow user](./work-flow.png) + +Cocos Creator 将整套手机页游解决方案整合在了编辑器工具里,无需在多个软件之间穿梭,只要打开 Cocos Creator 编辑器,各种一键式的自动化流程就能花最少的时间精力,解决上述所有问题。开发者就能够专注于开发阶段,提高产品竞争力和创造力! + +### 创建或导入资源 + +将图片、声音等资源拖拽到编辑器的 **资源管理器** 面板中,即可完成资源导入。 + +此外,你也可以在编辑器中直接创建场景、预制、动画、脚本、粒子等各类资源。 + +### 搭建场景 + +项目中有了一些基本资源后,我们就可以开始搭建场景了,场景是游戏内容最基本的组织方式,也是向玩家展示游戏的基本形态。 + +### 添加组件脚本,实现交互功能 + +我们可以为场景中的节点挂载各种内置组件和自定义脚本组件,来实现游戏逻辑的运行和交互。包括从最基本的动画播放、按钮响应,到驱动整个游戏逻辑的主循环脚本和玩家角色的控制。几乎所有游戏逻辑功能都是通过挂载脚本到场景中的节点来实现的。 + +### 一键预览和发布 + +搭建场景和开发功能的过程中,你可以随时点击预览来查看当前场景的运行效果。使用手机扫描二维码,可以立即在手机上预览游戏。当开发告一段落时,通过 **构建发布** 面板可以一键发布游戏到包括桌面、手机、Web、小游戏等多个平台。 + +## 功能特性 + +Cocos Creator 功能上的突出特色包括: + +- 脚本中可以轻松声明可以在编辑器中随时调整的数据属性,对参数的调整可以由设计人员独立完成。 +- 支持智能画布适配和免编程元素对齐的 UI 系统,可以完美适配任意分辨率的设备屏幕。 +- 动画系统,支持动画轨迹预览和复杂曲线编辑功能。 +- 使用动态语言支持的脚本开发,包括 JavaScript 和 TypeScript,使得动态调试和移动设备远程调试变得异常轻松。 +- 底层由 C++ 内核和 JS 两套内核组成,在享受脚本化开发的便捷时,保持原生级别的轻量和高性能。 +- 脚本组件化和开放式的插件系统为开发者在不同深度上提供了定制工作流的方法,编辑器可以大尺度调整来适应不同团队和项目的需求。 + +## 架构特色 + +Cocos Creator 包含游戏引擎、资源管理、场景编辑、游戏预览和发布等游戏开发所需的全套功能,并且将所有的功能和工具链都整合在了一个统一的应用程序里。 + +编辑器在提供强大完整工具链的同时,提供了开放式的插件架构,开发者能够用 HTML + TypeScript 等前端通用技术轻松扩展编辑器功能,定制个性化的工作流程。 + +以下,是 Cocos Creator 的技术架构图: + +Cocos Creator structure editor +

图一

+Cocos Creator structure engine +

图二

+ +从图中我们可以看出,编辑器是由 Electron 驱动再结合引擎所搭建的开发环境,引擎则负责提供许多开发上易于使用的组件和适配各平台的统一接口。 + +引擎和编辑器的结合,带来的是数据驱动和组件化的功能开发方式,以及设计和程序两类人员的完美分工合作: + +- 设计师在场景编辑器中搭建场景的视觉表现 +- 程序员开发可以挂载到场景任意物体上的功能组件 +- 设计师负责为需要展现特定行为的物体挂载组件,并通过调试改善各项参数 +- 程序员开发游戏所需要的数据结构和资源 +- 设计师通过图形化的界面配置好各项数据和资源 +- (就这样从简单到复杂,各种你能想像到的工作流程都可以实现) + +以工作流为核心的开发理念,让不同职能的开发者能够快速找到最大化自己作用的工作切入点,并能够默契流畅的和团队其他成员配合。 + +## 使用说明 + +在数据驱动的工作流基础上,场景的创建和编辑成为了游戏开发的重心,设计工作和功能开发可以同步进行,无缝协作。不管是美术、策划还是程序员,都可以在生产过程的任意时刻点击预览按钮,在浏览器、移动设备模拟器或移动设备真机上测试游戏的最新状态。 + +程序员和设计人员现在可以实现各式各样的分工合作,不管是先搭建场景,再添加功能,还是先生产功能模块再由设计人员进行组合调试,Cocos Creator 都能满足开发团队的需要。脚本中定义的属性能够以最适合的视觉体验呈现在编辑器中,为内容生产者提供便利。 + +场景之外的内容资源可以由外部导入,比如图片、声音、图集、骨骼动画等等,除此之外我们还在不断完善编辑器生产资源的能力,包括目前已经完成的动画编辑器,美术人员可以使用这个工具制作出非常细腻富有表现力的动画资源,并可以随时在场景中看到动画的预览。 + +支持 2D 和 3D 游戏开发,其功能可满足各种游戏类型的特定需求。深入优化了编辑器体验和引擎性能,并内置了对中间件的支持,如 Spine、DragonBones、TiledMap、Box2D 和 Texture Packer。 + +最后,开发完成的游戏可以通过图形工具一键发布到各个平台,从设计研发到测试发布,Cocos Creator 全部帮您搞定。 diff --git a/versions/4.0/zh/getting-started/introduction/structure-editor.png b/versions/4.0/zh/getting-started/introduction/structure-editor.png new file mode 100644 index 0000000000..c2356e5520 Binary files /dev/null and b/versions/4.0/zh/getting-started/introduction/structure-editor.png differ diff --git a/versions/4.0/zh/getting-started/introduction/structure-engine.png b/versions/4.0/zh/getting-started/introduction/structure-engine.png new file mode 100644 index 0000000000..9d84d78813 Binary files /dev/null and b/versions/4.0/zh/getting-started/introduction/structure-engine.png differ diff --git a/versions/4.0/zh/getting-started/introduction/work-flow.png b/versions/4.0/zh/getting-started/introduction/work-flow.png new file mode 100644 index 0000000000..d76c12f392 Binary files /dev/null and b/versions/4.0/zh/getting-started/introduction/work-flow.png differ diff --git a/versions/4.0/zh/getting-started/introduction/work-flow.pos b/versions/4.0/zh/getting-started/introduction/work-flow.pos new file mode 100644 index 0000000000..6e90fbee17 --- /dev/null +++ b/versions/4.0/zh/getting-started/introduction/work-flow.pos @@ -0,0 +1 @@ +{"diagram":{"image":{"x":-39,"width":1381,"y":-19,"pngdata":"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s3QJRERERERERNUa2xcQEVUTsoOjpUugCk62s7d0CdWGf4C7pUugCs7bx8XSJVAheA9TUXgPExFRUbhSloiomtDWbWTpEogox5jxPS1dAhGVAu9hIiIiKi2ulCUiIiIiIiIiIiIqRwxliYiIiIiIiIiIiMoRQ1kiIiIiIiIiIiKicsRQloiIiIiIiIiIiKgc8UFfRERERERERJXE7EG99V9ztqOioixXDBERlRhXyhIRERERERERERGVI4ayREREREREREREROWIoSwRERERERERERFROWIoS0RERERERERERFSOGMoSEVG1kZWWhu/f+A++nfAKMpKTLF0OERERERERVVMMZYmIqFrISkvDqnenIuFcNG7HXsaKt6YwmCWqAPhhCRERERFVRypLF0BEVFVERUUBAP68n2bhSuhxuYFsYswl1KhRAzY2NoiLi8OKt6ZgxJwFcHB1s3SJRNVS3nsTAO9JIiIiIqo2GMoSEVGVljf08fPzw3fffQcrKyuMGzcO165dYwhEZCH8sISochF1Onw3aTxux14u9DjPwCCMX7wcSiurcqqMiIiocmL7AiIiqrJMBbK+vr7w8PDAkiVLEBISgnvxcWxlQFTOHr83ly1bhqVLl/KeJKrANFlZRQayAHD/egKyM9LLoSIiIqLKjaEsERFVSQUFsrkYzBJZBj8sIarcnJyccPz4cZMvNzeucCciIjIXQ1kiIqpyigpkczEEIipf/LCEiIiIiEiPoSwREVUp5gayuRgCEZUPflhCRERERPQIQ1kiIqoyihvI5mIIRPRk8cMSIiIiIiJjDGWJiKhKKGkgm4shENGTwQ9LiIiIiIjyYyhLRFRG2rdvj/bt22P2oN6YPai3pcupVkobyOZiCERUtvhhCRERERGRaQxliYioUiurQDYXQyCissEPS4iIiIiICqaydAFERESl8dN705AYcwkAkJiYiL59++Y7pm3btvjqq69Mzn/nnXewffv2As9/Lz4Oq96ZhlcXLyubgomqgSf1Ycm4ceNw7do1rHhrCkbMWQAHV7cyrJqIiIiIqPwwlCUioiovOjq6wH1Hjx4tcr4kiWVZDlGVxw9LiIiIiIgKx1CWiIgqtVe+/LbAfVlpaZg9uL9Z55m2ZgNX3RGVI35YQkRERETVGUNZIiIiIipT/LCEiOjJmb5xCwCgv6eThSshIqLS4IO+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AAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAAAAAAwECEsgAAAAAAAABgIEJZAAAAAAAAADAQoSwAAAAAAAAAGIhQFgAAAAAAAAAMRCgLAAAAAAAAAAYilAUAAAAAAAAAAxHKAgAAAAAAAICBCGUBAAAA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R36hnf3H03j/+1V8vmAjBYUV37ywrji1RNbQYF8nRyIiIudDSVkRERERERGpUY6lZvGPKQudHYaIiMgFozVlRURERERERERERKqRkrIiIiIiIiIiIiIi1UjLF4iI1HHz5s1zdggiIiIiIiIi8heaKSsiUkeZ3j7ODkGqmelVz9kh1CkRjeo7OwSR8xbawN/ZIdRZ+hkh56LxJyIiZ6OZsiIidVRhXFtnhyBSq9157+XODkFEajD9jBAREZHzoZmyIiIiIiIiIiIiItVIM2VFREREREREaolXhg8q+nrydWJiovOCERGRStNMWREREREREREREZFqpKSsiIiIiIiIiIiISDVSUlZERERERERERESkGikpKyIiIiIiIiIiIlKNlJQVERERERERERERqUZKyoqISJ2Qm5nJRw8/wMT77iI7LdXZ4YjIWWi8ioiIiMjFztXZAYiI1BWJiYkAzErOdHIkF5/czEymPf0oh7f/CcDUxx/i1lffwDsg0MmRiciZNF5FRERERDRTVkREarm/JngaNmxITEwMSfv2MvXxhzQDT6SG0XgVERERESmipKyIiNRaf03whIeH8+GHHzJ58mRiY2OV6BGpYTReRUREREROU1JWRERqpTMTPB988AFhYWEEBQUxadIkJXpEahCNVxERERGR4pSUFRGRWqesBM8pSvSI1BwaryIiIiIiJSkpKyIitcq5EjynKNEj4nwaryIiIiIipVNSVkREao3yJnhOUaJH6oLtK5bx/OX9WD7jf+ese3DrFp6/vB+2goJKna9KGq8idZPdZmPifXfx/OX9znq8e+do7IWFzg5XRESkxlJSVkREaoWKJnhOUaJHart18+dQv2E46+bPcXYo5abxKlJ3FeTmcnTXjnPWSz6wn7zsrGqISEREpHZSUlZEpIokJCSQkJDAK8MH8crwQc4Op06pbILnFCV6pLbKzczgz+XLuPL+hzi2Zw9Hd+10dkjnpPEqcnHw9fVl9erVpR6BgYHODk9ERKTGU1JWRERqtPNN8JyiRI/URht//YWQqCiadu5CbPsOrPt5brHzuZkZTH/xOf497HLevHkUu9eurtD5qqbxKiIiIiJSPkrKiohIjVVVCZ5TlOiR2mbd/Dm0HTAQgHaXXc7GhQuw22yO89+Pf5nczAwenPoFd7z1PjtXryzW/lznq5LGq4iIiIhI+bk6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4riCIiEiIiIiIiKilowzZYmIiIiIiIiIiIjkiDNliYiaydoxHo06rr4Zto1tl/2xP/bH/tgf+2N/7I/9tY7+iIjo5ceZskRERERERERERERyxJmyRERNRF9fH3l5eS/czigDnTr3r33hHtgf+2N/7I/9sT/2x/7Y38va36j4ePGzDHR06o6LiIhaLs6UJSJqIkuWLIGenp6iwyAiIiKiV4COjg4WLlyo6DCIiKiRlBQdwIsIOJ8mAoAVdp0VHQoRERERERERERE1wMm0x4jLKACAxSvtTb9SdDzyxJmyRERERERERERERHLEpCwRERERERERERGRHDEpS0RERERERERERCRHTMoSERERERERERERyRGTskRERERERERERERyxKQsERERERERERERkRwxKUtEREREREREREQkR0zKEhEREREREREREckRk7JEREREREREREREcsSkLBEREREREREREZEcMSlLREREREREREREJEdMyhIRERERERERERHJEZOyRERERERERERERHLEpCwRERERERERERGRHDEpS0RERERERERERCRHTMoSERERERERERERyRGTskRERERERERERERyxKQsERERERERERERkRwxKUtEREREREREREQkR0zKEhEREREREREREckRk7JEREREREREREREcsSkLBEREREREREREZEcMSlLREREREREREREJEdMyhIRERERERERERHJEZOyRERERERERERERHLEpCwRERERERERERGRHDEpS0RERERERERERCRHTMoSERERERERERERyRGTskRERERERERERERyxKQsERERERERERERkRwxKUtEREREREREREQkR0zKEhEREREREREREckRk7JEREREREREREREcsSkLBEREREREREREZEcMSlLREREREREREREJEdMyhIRERERERERERHJEZOyRERERERERERERHLEpCwRERERERERERGRHDEpS0RERERERERERCRHTMoSERERERERERERyRGTskRERERE9P/t2rEJw1AUBEEErlv9ybFLUBVKnlowxuxHMFPBxcsBABASZQEAAAAAQqIsAAAAAEBIlAUAAAAACImyAAAAAAAhURYAAAAAICTKAgAAAACERFkAAAAAgJAoCwAAAAAQEmUBAAAAAEKiLAAAAABASJQFAAAAAAiJsgAAAAAAIVEWAAAAACAkygIAAAAAhERZAAAAAICQKAsAAAAAEBJlAQAAAABCoiwAAAAAQEiUBQAAAAAIibIAAAAAAKHX6gH/sB/n6gkAAAAAAF959FN2Zt6rNwAAAAAAv5prtu2zegUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADzaDZNuQRXBQj4xAAAAAElFTkSuQmCC","height":1407},"elements":{"elements":{"17bcec15920f16":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"创建项目"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":18,"color":"255,255,255"},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bcec15a4857f","category":"default","type":"number","value":""},{"name":"名称","id":"17bcec15a48dac","category":"default","type":"string","value":""},{"name":"所有者","id":"17bcec15a48bcb","category":"default","type":"string","value":""},{"name":"连接","id":"17bcec15a48b5b","category":"default","type":"link","value":""},{"name":"便笺","id":"17bcec15a4801b","category":"default","type":"string","value":""}],"props":{"zindex":157,"w":136,"h":53,"x":333,"angle":0,"y":503.5},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bcec15920f16","category":"basic","locked":false,"group":""},"17bbf8b20f7e74":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf8b3c23885","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c23f9b","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c234ab","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c2360a","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c23f6a","category":"default","type":"string","value":""}],"props":{"zindex":131,"w":39,"x":607,"h":24,"y":1080,"angle":4.71238898038469},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7e74","locked":false,"category":"basic","group":""},"17bbf8ad6342af":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h","x":0,"y":0},"text":"编辑器内工作流"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":28},"title":"文本","dataAttributes":[{"name":"序号","id":"17bbf8af35f043","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35f7ce","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35f6e8","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35fc0b","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35f064","category":"default","type":"string","value":""}],"props":{"zindex":91,"w":202,"h":41,"x":541.5425257731958,"angle":0,"y":316.5},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"text","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad6342af","category":"basic","locked":false,"group":""},"17bbf8b20f72d2":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf8b3c2332b","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c23eba","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c23ebd","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c235b6","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c23ca9","category":"default","type":"string","value":""}],"props":{"zindex":135,"w":59,"x":948,"h":24,"y":1194,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f72d2","locked":false,"category":"basic","group":""},"17bbf8ad634f6f":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"资源导入器
db
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":18,"lineHeight":1.5},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bbf8af35fcf5","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35f099","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35f429","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35f093","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35fe23","category":"default","type":"string","value":""}],"props":{"zindex":85,"w":136,"h":59,"x":333,"angle":0,"y":377.5},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"184,184,184"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad634f6f","category":"basic","locked":false,"group":""},"17bbf8b20f724":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf8b3c22034","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c22729","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c22ce4","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c22b65","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c222e1","category":"default","type":"string","value":""}],"props":{"zindex":119,"w":85,"x":235.5,"h":24,"y":1143,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f724","locked":false,"category":"basic","group":""},"17bbf89e731ced":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":""}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"流程","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf89e731ebb","type":"number","category":"default","value":""},{"name":"名称","id":"17bbf89e731f9a","type":"string","category":"default","value":""},{"name":"所有者","id":"17bbf89e73154","type":"string","category":"default","value":""},{"name":"连接","id":"17bbf89e731ed8","type":"link","category":"default","value":""},{"name":"便笺","id":"17bbf89e731e3a","type":"string","category":"default","value":""},{"name":"成本","id":"17bbf89e7315c5","type":"number","category":"default","value":""},{"name":"时间","id":"17bbf89e7314f4","type":"number","category":"default","value":""},{"name":"部门","id":"17bbf89e731e0b","type":"string","category":"default","value":""},{"name":"输入","id":"17bbf89e7312ae","type":"string","category":"default","value":""},{"name":"输出","id":"17bbf89e731dbd","type":"string","category":"default","value":""},{"name":"风险","id":"17bbf89e731869","type":"string","category":"default","value":""},{"name":"备注","id":"17bbf89e731c2d","type":"string","category":"default","value":""}],"props":{"zindex":75,"w":1270.9691929768042,"x":0.04252577319584816,"h":1297,"y":20.50390625,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"125,200,227"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"process","fillStyle":{},"theme":{},"id":"17bbf89e731ced","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"flow","locked":false,"group":""},"17bcec08f3ea94":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"打包发布"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":18,"color":"255,255,255"},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bcec0904f931","category":"default","type":"number","value":""},{"name":"名称","id":"17bcec0904f695","category":"default","type":"string","value":""},{"name":"所有者","id":"17bcec0904f12a","category":"default","type":"string","value":""},{"name":"连接","id":"17bcec0904f9f6","category":"default","type":"link","value":""},{"name":"便笺","id":"17bcec0904faba","category":"default","type":"string","value":""}],"props":{"zindex":155,"w":136,"h":53,"x":802,"angle":0,"y":555.5},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bcec08f3ea94","category":"basic","locked":false,"group":""},"17bbf8b20f7117":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[],"textBlock":[{"position":{"w":"w-20","h":"h-20","x":10,"y":10},"text":"Resources"}],"title":"注释","fontStyle":{"size":14,"lineHeight":1.5,"bold":false,"vAlign":"top"},"dataAttributes":[],"props":{"zindex":117,"w":136,"x":316,"h":140,"y":731.9999999999999,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]},{"fillStyle":{"color":"r-50,g-50,b-50","type":"solid"},"actions":[{"x":"w-16","action":"move","y":"0"},{"x":"w-16","action":"line","y":"16"},{"x":"w","action":"line","y":"16"},{"action":"close"}]},{"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"uiNote","theme":{},"fillStyle":{"color":"238,197,145","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"id":"17bbf8b20f7117","locked":false,"category":"ui","group":"17bbf8b20f77e1"},"17bbf8ad6342b3":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"设计
场景制作
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":18,"color":"255,255,255","lineHeight":1.5},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bbf8af35f5c1","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35ff8f","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35fbe4","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35f2ff","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35f7f7","category":"default","type":"string","value":""}],"props":{"zindex":90,"w":136,"h":59,"x":561,"angle":0,"y":377.5},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad6342b3","category":"basic","locked":false,"group":""},"17bce6d70e9e66":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"textAlign":"center"},"title":"","dataAttributes":[],"props":{"zindex":146,"w":59,"h":59,"x":111.5,"angle":0,"y":464.5},"path":[{"lineStyle":{"lineWidth":0},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardImage","theme":{},"fillStyle":{"imageW":1535,"display":"stretch","imageH":1537,"type":"image","fileId":"http://cdn.processon.com/6008f0476376892e7dfd667b?e=1611202135&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:ElZqH3OMsWF2_j6BLj28-FuYSME="},"attribute":{"container":false,"rotatable":true,"visible":false,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"id":"17bce6d70e9e66","category":"standard","locked":false},"17bbf8ad63443e":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"fontStyle":{},"title":"右箭头","dataAttributes":[{"name":"序号","id":"17bbf8af35fe0e","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35fc1","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35f55d","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35f13d","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35fd0e","category":"default","type":"string","value":""}],"props":{"zindex":89,"w":59,"h":24,"x":485.5,"angle":0,"y":518},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad63443e","category":"basic","locked":false,"group":""},"17bbf8b20f79e1":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"Programming"}],"title":"矩形","fontStyle":{"size":18,"color":"255,255,255"},"dataAttributes":[{"name":"序号","id":"17bbf8b3c22597","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c2250a","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c227fc","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c227ba","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c2274e","category":"default","type":"string","value":""}],"props":{"zindex":115,"w":177,"x":538,"h":53,"y":1128.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f79e1","locked":false,"category":"basic","group":""},"17bbf99fc681c9":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[],"textBlock":[{"position":{"w":"w-20","h":"h-20","x":10,"y":10},"text":"

"}],"title":"注释","fontStyle":{"size":14,"lineHeight":1.5,"bold":false,"vAlign":"top"},"dataAttributes":[],"props":{"zindex":141,"w":136,"x":336.5,"h":140,"y":108,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]},{"fillStyle":{"color":"r-50,g-50,b-50","type":"solid"},"actions":[{"x":"w-16","action":"move","y":"0"},{"x":"w-16","action":"line","y":"16"},{"x":"w","action":"line","y":"16"},{"action":"close"}]},{"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]}],"lineStyle":{"lineColor":"255,102,102","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"uiNote","theme":{},"fillStyle":{"color":"238,154,0","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"id":"17bbf99fc681c9","locked":false,"category":"ui","group":"17bbf99fc687d4"},"17bbf8b20f7abc":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"title":"矩形","fontStyle":{"size":18,"lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bbf8b3c21852","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c21d72","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c21517","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c21b9e","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c21d8d","category":"default","type":"string","value":""}],"props":{"zindex":109,"w":136,"x":1016,"h":86.74609375,"y":1157.25390625,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7abc","locked":false,"category":"basic","group":"17bbf8b20f782b"},"17bbf8ad634368":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"fontStyle":{},"title":"右箭头","dataAttributes":[{"name":"序号","id":"17bbf8af35f2ba","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35fc42","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35f7b8","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35ff3f","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35fde","category":"default","type":"string","value":""}],"props":{"zindex":100,"w":39,"h":24,"x":610,"angle":4.71238898038469,"y":456.6545586340207},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad634368","category":"basic","locked":false,"group":""},"17bbf8b20f7d84":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf8b3c2298c","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c22ff8","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c22438","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c225ce","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c2217c","category":"default","type":"string","value":""}],"props":{"zindex":120,"w":59,"x":463.5,"h":24,"y":1143,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7d84","locked":false,"category":"basic","group":""},"17bbf8ad634a93":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"预览和调试"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":18,"color":"255,255,255"},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bbf8af35f177","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35f13a","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35fd41","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35f888","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35f594","category":"default","type":"string","value":""}],"props":{"zindex":92,"w":136,"h":59,"x":802,"angle":0,"y":377.5},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad634a93","category":"basic","locked":false,"group":""},"17bbf8b20f79ce":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w","h":"h","x":0,"y":0},"text":"Add components"}],"title":"文本","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf8b3c23ec8","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c23974","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c23407","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c23faa","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c239dc","category":"default","type":"string","value":""}],"props":{"zindex":132,"w":85,"x":538,"h":41,"y":1071.2677788214369,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"text","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f79ce","locked":false,"category":"basic","group":""},"17bbf8b20f7787":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"Build &
Publish
"}],"title":"矩形","fontStyle":{"size":18,"color":"255,255,255","lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bbf8b3c221c6","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c2239f","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c221e1","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c223d1","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c221a3","category":"default","type":"string","value":""}],"props":{"zindex":124,"w":136,"x":803,"h":53,"y":1179.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7787","locked":false,"category":"basic","group":""},"17bbf8b20f7bb5":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"New Project"}],"title":"矩形","fontStyle":{"size":18,"color":"255,255,255","lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bbf8b3c2232d","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c2233b","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c22475","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c222b4","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c22335","category":"default","type":"string","value":""}],"props":{"zindex":114,"w":119.76471002791345,"x":331.1176449860433,"h":53,"y":1128.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7bb5","locked":false,"category":"basic","group":""},"17bbf8b20f7865":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"title":"矩形","fontStyle":{"size":18,"lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bbf8b3c21819","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c212cf","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c217f1","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c213f7","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c21d3b","category":"default","type":"string","value":""}],"props":{"zindex":110,"w":136,"x":1016,"h":86.74609375,"y":1157.25390625,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7865","locked":false,"category":"basic","group":"17bbf8b20f782b"},"17bbf8ad63414d":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"fontStyle":{},"title":"右箭头","dataAttributes":[{"name":"序号","id":"17bbf8af35f9f4","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35f1ae","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35f93d","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35f5da","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35f121","category":"default","type":"string","value":""}],"props":{"zindex":88,"w":79,"h":24,"x":238.5,"angle":0,"y":518},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad63414d","category":"basic","locked":false,"group":""},"17bbf8ad63458c":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"fontStyle":{},"title":"右箭头","dataAttributes":[{"name":"序号","id":"17bbf8af35feb9","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35f4ad","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35f4f8","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35fb99","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35fa4f","category":"default","type":"string","value":""}],"props":{"zindex":103,"w":85,"h":24,"x":826,"angle":1.5707963267948966,"y":482},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad63458c","category":"basic","locked":false,"group":""},"17bbf8b20f74ff":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"Web
Native
Mini Game"}],"title":"矩形","fontStyle":{"size":18,"lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bbf8b3c223f5","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c22d13","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c226d7","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c2247","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c227e2","category":"default","type":"string","value":""}],"props":{"zindex":125,"w":136,"x":1016,"h":86.74609375,"y":1157.25390625,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f74ff","locked":false,"category":"basic","group":"17bbf8b20f782b"},"17bbf8b20f7223":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"Preview &
Debug
"}],"title":"矩形","fontStyle":{"size":18,"color":"255,255,255","textAlign":"center","lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bbf8b3c22b97","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c227a2","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c222b9","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c22f7c","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c22fb6","category":"default","type":"string","value":""}],"props":{"zindex":123,"w":136,"x":803,"h":59,"y":1003.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7223","locked":false,"category":"basic","group":""},"17bcebfbd67fd6":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bcebfbfb0edf","category":"default","type":"number","value":""},{"name":"名称","id":"17bcebfbfb0cbe","category":"default","type":"string","value":""},{"name":"所有者","id":"17bcebfbfb0e02","category":"default","type":"string","value":""},{"name":"连接","id":"17bcebfbfb02c5","category":"default","type":"link","value":""},{"name":"便笺","id":"17bcebfbfb068","category":"default","type":"string","value":""}],"props":{"zindex":154,"w":59,"x":718.5813871176003,"h":24,"y":395,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bcebfbd67fd6","locked":false,"category":"basic","group":""},"17bce6d70e9d37":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h","x":0,"y":0},"text":"
下载 Cocos Dashboard,
并在 Dashboard 中下载
指定版本 Creator
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":15,"textAlign":"center","lineHeight":1.5},"title":"文本","dataAttributes":[{"name":"序号","id":"17bce6d76203b7","category":"default","type":"number","value":""},{"name":"名称","id":"17bce6d7620613","category":"default","type":"string","value":""},{"name":"所有者","id":"17bce6d7620e84","category":"default","type":"string","value":""},{"name":"连接","id":"17bce6d762089f","category":"default","type":"link","value":""},{"name":"便笺","id":"17bce6d7620334","category":"default","type":"string","value":""}],"props":{"zindex":147,"w":186,"h":86,"x":48,"angle":0,"y":518.5},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"text","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bce6d70e9d37","category":"basic","locked":false},"17bbf99fc68201":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[],"textBlock":[{"position":{"w":"w-20","h":"h-20","x":10,"y":10},"text":"资源数据"}],"title":"注释","fontStyle":{"size":14,"lineHeight":1.5,"bold":false,"vAlign":"top"},"dataAttributes":[],"props":{"zindex":143,"w":136,"x":327.5,"h":140,"y":91.626953125,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]},{"fillStyle":{"color":"r-50,g-50,b-50","type":"solid"},"actions":[{"x":"w-16","action":"move","y":"0"},{"x":"w-16","action":"line","y":"16"},{"x":"w","action":"line","y":"16"},{"action":"close"}]},{"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"uiNote","theme":{},"fillStyle":{"color":"238,197,145","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"id":"17bbf99fc68201","locked":false,"category":"ui","group":"17bbf99fc687d4"},"17bbf8b20f75d4":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"Design & 
Scene Creation
"}],"title":"矩形","fontStyle":{"size":18,"color":"255,255,255","textAlign":"center","lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bbf8b3c228f8","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c22a01","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c2265d","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c227f2","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c22b3a","category":"default","type":"string","value":""}],"props":{"zindex":121,"w":177,"x":538,"h":59,"y":1003.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f75d4","locked":false,"category":"basic","group":""},"17bbf8ad634a6f":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"fontStyle":{},"title":"右箭头","dataAttributes":[{"name":"序号","id":"17bbf8af35f813","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35f6cd","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35f4d8","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35feb6","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35fa42","category":"default","type":"string","value":""}],"props":{"zindex":98,"w":85,"h":24,"x":358.5,"angle":1.5707963267948966,"y":297},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad634a6f","category":"basic","locked":false,"group":""},"17bce7251c186d":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{},"title":"","dataAttributes":[],"props":{"zindex":149,"w":59,"x":106.5503995578421,"h":59,"y":1083.5,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"standardImage","theme":{},"fillStyle":{"imageW":1535,"display":"stretch","imageH":1537,"type":"image","fileId":"http://cdn.processon.com/6008f0476376892e7dfd667b?e=1611202135&token=trhI0BY8QfVrIGn9nENop6JAc6l5nZuxhjQ62UfM:ElZqH3OMsWF2_j6BLj28-FuYSME="},"attribute":{"container":false,"rotatable":true,"visible":false,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"id":"17bce7251c186d","category":"standard","locked":false},"17bbf8ad634753":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"fontStyle":{},"title":"右箭头","dataAttributes":[{"name":"序号","id":"17bbf8af360c0e","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af360142","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af3606bb","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af3600fc","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af360eda","category":"default","type":"string","value":""}],"props":{"zindex":106,"w":39,"h":24,"x":380,"angle":4.71238898038469,"y":455.5861704324939},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad634753","category":"basic","locked":false,"group":""},"17bcec1225f146":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"开发"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":18,"color":"255,255,255"},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bcec12381695","category":"default","type":"number","value":""},{"name":"名称","id":"17bcec123814b5","category":"default","type":"string","value":""},{"name":"所有者","id":"17bcec12381c6c","category":"default","type":"string","value":""},{"name":"连接","id":"17bcec1238157a","category":"default","type":"link","value":""},{"name":"便笺","id":"17bcec12381b39","category":"default","type":"string","value":""}],"props":{"zindex":156,"w":136,"h":53,"x":563,"angle":0,"y":503.5},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"51,102,153"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"51,102,153","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bcec1225f146","category":"basic","locked":false,"group":""},"17bbf8b20f79bd":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w","h":"h","x":0,"y":0},"text":"Workflow within the editor"}],"title":"文本","fontStyle":{"size":28},"dataAttributes":[{"name":"序号","id":"17bbf8b3c22063","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c225c1","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c22bef","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c22318","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c227c","category":"default","type":"string","value":""}],"props":{"zindex":122,"w":345,"x":454.54252577319556,"h":53,"y":938.8444971073933,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"text","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f79bd","locked":false,"category":"basic","group":""},"17bbf99fc6851a":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w","h":"h","x":0,"y":0},"text":"TexturePacker
Spine
BMFont
3ds Max
Maya
"}],"title":"文本","fontStyle":{"size":14,"lineHeight":1.5,"vAlign":"top"},"dataAttributes":[{"name":"序号","id":"17bbf99ffccc1e","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf99ffcc3ac","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf99ffccfbe","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf99ffcc338","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf99ffcc9a3","category":"default","type":"string","value":""}],"props":{"zindex":144,"w":136,"x":327.5,"h":101,"y":127.5,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"text","theme":{},"fillStyle":{"color":"255,255,204","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf99fc6851a","locked":false,"category":"basic","group":"17bbf99fc687d4"},"17bbf8b20f7057":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"title":"矩形","fontStyle":{"size":24,"vAlign":"top"},"dataAttributes":[{"name":"序号","id":"17bbf8b3c22b84","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c22a4d","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c22f1e","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c22a85","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c22769","category":"default","type":"string","value":""}],"props":{"zindex":111,"w":669,"x":293.8760871092038,"h":330,"y":922.6770935058594,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineStyle":"dashed","lineColor":"59,59,59","lineWidth":4},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"178,235,242","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7057","locked":false,"category":"basic","group":""},"17bbf8b20f72ab":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf8b3c23b83","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c231f7","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c23a38","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c232d4","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c2338f","category":"default","type":"string","value":""}],"props":{"zindex":129,"w":79,"x":351.5,"h":24,"y":930.1649389266968,"angle":1.5707963267948966},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f72ab","locked":false,"category":"basic","group":""},"17bbf8b20f7f":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf8b3c23e29","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c230ae","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c236d3","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c232bc","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c23f26","category":"default","type":"string","value":""}],"props":{"zindex":130,"w":59,"x":463.5,"h":24,"y":1021,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7f","locked":false,"category":"basic","group":""},"17bbf8ad63421d":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":18,"lineHeight":1.5},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bbf8af35e68b","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35ea02","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35e1c5","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35e583","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35e334","category":"default","type":"string","value":""}],"props":{"zindex":79,"w":136.0000000000001,"h":86.5078125,"x":1015.9999999999999,"angle":0,"y":538.74609375},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad63421d","category":"basic","locked":false,"group":"17bbf8ad634829"},"17bbf8b20f7052":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w","h":"h","x":0,"y":0},"text":"TexturePacker
Spine
BMFont
3ds Max
Maya
"}],"title":"文本","fontStyle":{"size":14,"lineHeight":1.5,"vAlign":"top"},"dataAttributes":[{"name":"序号","id":"17bbf8b3c22b91","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c22f96","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c22009","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c22203","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c223bb","category":"default","type":"string","value":""}],"props":{"zindex":118,"w":136,"x":316,"h":101,"y":768.5,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"text","theme":{},"fillStyle":{"color":"255,255,204","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7052","locked":false,"category":"basic","group":"17bbf8b20f77e1"},"17bceb96ee8c09":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bceb9709c1b5","category":"default","type":"number","value":""},{"name":"名称","id":"17bceb9709c5ed","category":"default","type":"string","value":""},{"name":"所有者","id":"17bceb9709cd9b","category":"default","type":"string","value":""},{"name":"连接","id":"17bceb9709ccb4","category":"default","type":"link","value":""},{"name":"便笺","id":"17bceb9709cca3","category":"default","type":"string","value":""}],"props":{"zindex":153,"w":59,"x":485.5,"h":24,"y":395,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bceb96ee8c09","locked":false,"category":"basic","group":""},"17bbf8ad634b17":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h","x":0,"y":0},"text":"组件挂载"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{},"title":"文本","dataAttributes":[{"name":"序号","id":"17bbf8af35f38d","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35fca1","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35fb4","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35f4b","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35f393","category":"default","type":"string","value":""}],"props":{"zindex":101,"w":85,"h":41,"x":541.5425257731958,"y":451.5,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"text","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad634b17","category":"basic","locked":false,"group":""},"17bce87ce7eea2":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"浏览器调试
模拟器调试
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"流程","fontStyle":{"size":18,"lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bce87ce7e2e2","type":"number","category":"default","value":""},{"name":"名称","id":"17bce87ce7ee44","type":"string","category":"default","value":""},{"name":"所有者","id":"17bce87ce7e11c","type":"string","category":"default","value":""},{"name":"连接","id":"17bce87ce7e0cd","type":"link","category":"default","value":""},{"name":"便笺","id":"17bce87ce7e1ad","type":"string","category":"default","value":""},{"name":"成本","id":"17bce87ce7e46d","type":"number","category":"default","value":""},{"name":"时间","id":"17bce87ce7ea1","type":"number","category":"default","value":""},{"name":"部门","id":"17bce87ce7ef1d","type":"string","category":"default","value":""},{"name":"输入","id":"17bce87ce7e671","type":"string","category":"default","value":""},{"name":"输出","id":"17bce87ce7e6f","type":"string","category":"default","value":""},{"name":"风险","id":"17bce87ce7e66c","type":"string","category":"default","value":""},{"name":"备注","id":"17bce87ce7ea38","type":"string","category":"default","value":""}],"props":{"zindex":151,"w":165,"x":786,"h":86,"y":135,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"158,158,158"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"process","fillStyle":{},"theme":{},"id":"17bce87ce7eea2","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"flow","locked":false,"group":""},"17bbf8ad634cd1":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"Web
Native
小游戏平台"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":18,"lineHeight":1.5},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bbf8af35fada","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35ffcd","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35f9bb","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35f833","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35f8a9","category":"default","type":"string","value":""}],"props":{"zindex":94,"w":136.0000000000001,"h":86.5078125,"x":1016,"angle":0,"y":538.74609375},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad634cd1","category":"basic","locked":false,"group":"17bbf8ad634829"},"17bbf8ad634ed2":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"fontStyle":{},"title":"右箭头","dataAttributes":[{"name":"序号","id":"17bbf8af360f33","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af36059","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af3601b7","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af360fcb","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af360524","category":"default","type":"string","value":""}],"props":{"zindex":104,"w":59,"h":24,"x":941.7087552572034,"angle":0,"y":570},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad634ed2","category":"basic","locked":false,"group":""},"17bbf8b20f7b63":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[],"textBlock":[{"position":{"w":"w-20","h":"h-20","x":10,"y":10},"text":"

"}],"title":"注释","fontStyle":{"size":14,"lineHeight":1.5,"bold":false,"vAlign":"top"},"dataAttributes":[],"props":{"zindex":107,"w":136,"x":328.2352899720865,"h":140,"y":749,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]},{"fillStyle":{"color":"r-50,g-50,b-50","type":"solid"},"actions":[{"x":"w-16","action":"move","y":"0"},{"x":"w-16","action":"line","y":"16"},{"x":"w","action":"line","y":"16"},{"action":"close"}]},{"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]}],"lineStyle":{"lineColor":"255,102,102","lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"uiNote","theme":{},"fillStyle":{"color":"238,154,0","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"id":"17bbf8b20f7b63","locked":false,"category":"ui","group":"17bbf8b20f77e1"},"17bce95a241688":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","x":10,"h":"h","y":0},"text":"Browser
Simulator
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"title":"流程","fontStyle":{"size":18,"lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bce95a40ff98","type":"number","category":"default","value":""},{"name":"名称","id":"17bce95a40f85f","type":"string","category":"default","value":""},{"name":"所有者","id":"17bce95a40f672","type":"string","category":"default","value":""},{"name":"连接","id":"17bce95a40fde9","type":"link","category":"default","value":""},{"name":"便笺","id":"17bce95a40f53e","type":"string","category":"default","value":""},{"name":"成本","id":"17bce95a40feec","type":"number","category":"default","value":""},{"name":"时间","id":"17bce95a40fe96","type":"number","category":"default","value":""},{"name":"部门","id":"17bce95a40f232","type":"string","category":"default","value":""},{"name":"输入","id":"17bce95a40fb8b","type":"string","category":"default","value":""},{"name":"输出","id":"17bce95a40f9c6","type":"string","category":"default","value":""},{"name":"风险","id":"17bce95a40fcfd","type":"string","category":"default","value":""},{"name":"备注","id":"17bce95a40f6b1","type":"string","category":"default","value":""}],"props":{"zindex":152,"w":165,"x":788,"h":86,"y":768,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"158,158,158"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"process","fillStyle":{},"theme":{},"id":"17bce95a241688","attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"category":"flow","locked":false,"group":""},"17bbf8b20f76cf":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[],"textBlock":[{"position":{"w":"w-20","h":"h-20","x":10,"y":10},"text":"

"}],"title":"注释","fontStyle":{"size":14,"lineHeight":1.5,"bold":false,"vAlign":"top"},"dataAttributes":[],"props":{"zindex":108,"w":136,"x":316,"h":140,"y":741,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]},{"fillStyle":{"color":"r-50,g-50,b-50","type":"solid"},"actions":[{"x":"w-16","action":"move","y":"0"},{"x":"w-16","action":"line","y":"16"},{"x":"w","action":"line","y":"16"},{"action":"close"}]},{"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]}],"lineStyle":{"lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"uiNote","theme":{},"fillStyle":{"color":"238,173,14","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"id":"17bbf8b20f76cf","locked":false,"category":"ui","group":"17bbf8b20f77e1"},"17bbf8b20f7837":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"Assets
Database
"}],"title":"矩形","fontStyle":{"size":18,"textAlign":"center","lineHeight":1.5},"dataAttributes":[{"name":"序号","id":"17bbf8b3c22f44","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c2278","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c22082","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c2252e","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c2212","category":"default","type":"string","value":""}],"props":{"zindex":116,"w":124,"x":329,"h":59,"y":1003.5,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"184,184,184"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7837","locked":false,"category":"basic","group":""},"17bbf8b20f7716":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf8b3c2373e","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c23779","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c23e5c","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c2398b","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c23546","category":"default","type":"string","value":""}],"props":{"zindex":134,"w":85,"x":828,"h":24,"y":1109.7205066091005,"angle":1.5707963267948966},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7716","locked":false,"category":"basic","group":""},"17bce7251c1a8c":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h","x":0,"y":0},"text":"Download Cocos Dashboard, and download the specified version of Creator in the Dashboard.
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":15,"textAlign":"center","lineHeight":1.5},"title":"文本","dataAttributes":[{"name":"序号","id":"17bce7253412af","category":"default","type":"number","value":""},{"name":"名称","id":"17bce725341114","category":"default","type":"string","value":""},{"name":"所有者","id":"17bce725341e4a","category":"default","type":"string","value":""},{"name":"连接","id":"17bce725341fe9","category":"default","type":"link","value":""},{"name":"便笺","id":"17bce725341e5c","category":"default","type":"string","value":""}],"props":{"zindex":150,"w":195.8992008843158,"x":38.1007991156842,"h":91.17408560286844,"y":1143,"angle":0},"path":[{"lineStyle":{"lineWidth":0},"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"text","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bce7251c1a8c","category":"basic","locked":false},"17bbf8b20f7446":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bbf8b3c23cb9","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8b3c23d1c","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8b3c2304","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8b3c231c7","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8b3c23131","category":"default","type":"string","value":""}],"props":{"zindex":137,"w":39,"x":371,"h":24,"y":1080,"angle":4.71238898038469},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8b20f7446","locked":false,"category":"basic","group":""},"17bcec82f27eb5":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"title":"右箭头","fontStyle":{},"dataAttributes":[{"name":"序号","id":"17bcec83007708","category":"default","type":"number","value":""},{"name":"名称","id":"17bcec8300712c","category":"default","type":"string","value":""},{"name":"所有者","id":"17bcec83007622","category":"default","type":"string","value":""},{"name":"连接","id":"17bcec83007c2e","category":"default","type":"link","value":""},{"name":"便笺","id":"17bcec830076d1","category":"default","type":"string","value":""}],"props":{"zindex":158,"w":59,"x":729,"h":24,"y":1021,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bcec82f27eb5","locked":false,"category":"basic","group":""},"17bbf8ad63468b":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"fontStyle":{},"title":"右箭头","dataAttributes":[{"name":"序号","id":"17bbf8af360e56","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af360cc5","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af36090c","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af36089e","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af360459","category":"default","type":"string","value":""}],"props":{"zindex":105,"w":105,"h":24,"x":817.5,"angle":4.71238898038469,"y":287.10937881469727},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad63468b","category":"basic","locked":false,"group":""},"17bbf8ad634b49":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":24,"vAlign":"top"},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bbf8af35ee63","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35eaee","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35e00b","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35e7f5","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35ee92","category":"default","type":"string","value":""}],"props":{"zindex":80,"w":669,"h":330,"x":293.8760871092038,"angle":0,"y":299.4548568725586},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineStyle":"dashed","lineColor":"59,59,59","lineWidth":4},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{"color":"178,235,242","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad634b49","category":"basic","locked":false,"group":""},"17bbf8ad6345e6":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w-20","h":"h","x":10,"y":0},"text":"
"}],"anchors":[{"x":"w/2","y":"0"},{"x":"w/2","y":"h"},{"x":"0","y":"h/2"},{"x":"w","y":"h/2"}],"fontStyle":{"size":18,"lineHeight":1.5},"title":"矩形","dataAttributes":[{"name":"序号","id":"17bbf8af35e9f6","category":"default","type":"number","value":""},{"name":"名称","id":"17bbf8af35ec38","category":"default","type":"string","value":""},{"name":"所有者","id":"17bbf8af35e3be","category":"default","type":"string","value":""},{"name":"连接","id":"17bbf8af35ea99","category":"default","type":"link","value":""},{"name":"便笺","id":"17bbf8af35e4e4","category":"default","type":"string","value":""}],"props":{"zindex":78,"w":136.0000000000001,"h":86.5078125,"x":1016,"angle":0,"y":538.74609375},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w","action":"line","y":"0"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"action":"close"}]}],"lineStyle":{"lineColor":"161,161,161"},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"rectangle","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bbf8ad6345e6","category":"basic","locked":false,"group":"17bbf8ad634829"},"17bbf99fc68bb6":{"parent":"","link":"","shapeStyle":{"alpha":1},"anchors":[],"textBlock":[{"position":{"w":"w-20","h":"h-20","x":10,"y":10},"text":"

"}],"title":"注释","fontStyle":{"size":14,"lineHeight":1.5,"bold":false,"vAlign":"top"},"dataAttributes":[],"props":{"zindex":142,"w":136,"x":327.5,"h":140,"y":108,"angle":0},"path":[{"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]},{"fillStyle":{"color":"r-50,g-50,b-50","type":"solid"},"actions":[{"x":"w-16","action":"move","y":"0"},{"x":"w-16","action":"line","y":"16"},{"x":"w","action":"line","y":"16"},{"action":"close"}]},{"fillStyle":{"type":"none"},"actions":[{"x":"0","action":"move","y":"0"},{"x":"w-16","action":"line","y":"0"},{"x":"w","action":"line","y":"16"},{"x":"w","action":"line","y":"h"},{"x":"0","action":"line","y":"h"},{"x":"0","action":"line","y":"0"},{"action":"close"}]}],"lineStyle":{"lineWidth":0},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"uiNote","theme":{},"fillStyle":{"color":"238,173,14","type":"solid"},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":false,"markerOffset":5},"id":"17bbf99fc68bb6","locked":false,"category":"ui","group":"17bbf99fc687d4"},"17bced0da68aae":{"parent":"","link":"","shapeStyle":{"alpha":1},"textBlock":[{"position":{"w":"w","h":"h*0.34","x":"0","y":"h*0.33"},"text":""}],"anchors":[{"x":"w","y":"h*0.5"},{"x":"0","y":"h*0.5"}],"fontStyle":{},"title":"右箭头","dataAttributes":[{"name":"序号","id":"17bced0dda89e1","category":"default","type":"number","value":""},{"name":"名称","id":"17bced0dda87ea","category":"default","type":"string","value":""},{"name":"所有者","id":"17bced0dda848","category":"default","type":"string","value":""},{"name":"连接","id":"17bced0dda8f5","category":"default","type":"link","value":""},{"name":"便笺","id":"17bced0dda8a2c","category":"default","type":"string","value":""}],"props":{"zindex":159,"w":105,"h":24,"x":818,"angle":4.71238898038469,"y":914.1093788146973},"path":[{"actions":[{"x":"0","action":"move","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.33"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"0"},{"x":"w","action":"line","y":"h*0.5"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h"},{"x":"w-Math.min(h*0.5,w*0.45)","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.67"},{"x":"0","action":"line","y":"h*0.33"},{"action":"close"}]}],"lineStyle":{},"children":[],"resizeDir":["tl","tr","br","bl"],"name":"singleRightArrow","theme":{},"fillStyle":{},"attribute":{"container":false,"rotatable":true,"visible":true,"collapsable":false,"collapsed":false,"linkable":true,"markerOffset":5},"id":"17bced0da68aae","category":"basic","locked":false,"group":""}},"page":{"padding":20,"orientation":"portrait","backgroundColor":"transparent","gridSize":15,"width":1316,"showGrid":true,"lineJumps":false,"height":1453}}},"meta":{"exportTime":"2021-12-07 14:15:08","member":"5a801cd0e4b0874437c6723b","diagramInfo":{"creator":"5a801cd0e4b0874437c6723b","created":"2021-09-06 11:10:01","modified":"2021-12-07 14:14:59","title":"work-flow-en","category":"flow"},"id":"61358689f346fb0715603d19","type":"ProcessOn Schema File","version":"1.0"}} \ No newline at end of file diff --git a/versions/4.0/zh/getting-started/project-structure/index.md b/versions/4.0/zh/getting-started/project-structure/index.md new file mode 100644 index 0000000000..ae03265443 --- /dev/null +++ b/versions/4.0/zh/getting-started/project-structure/index.md @@ -0,0 +1,69 @@ +# 项目结构 + +通过 Dashboard,我们可以创建一个 Hello World 项目作为开始,创建之后的项目有特定的文件夹结构,我们将在这一节熟悉 Cocos Creator 项目的文件夹结构。 + +## 项目文件夹结构 + +初次创建并打开一个 Cocos Creator 项目后,开发者项目文件夹的结构如下: + +![project-file](index/project-file.png) + +- `assets`:资源目录 + +- `build`:构建目录(在构建某平台后会生成该目录) + +- `library`:导入的资源目录 + +- `local`:日志文件目录 + +- `profiles`:编辑器配置 + +- `settings`:项目设置 + +- `temp`:临时文件目录 + +- `package.json`:项目配置 + +### 资源文件夹(assets) + +`assets` 用来放置游戏中所有的本地资源、脚本和第三方库文件。只有在 `assets` 目录下的内容才能显示在 **资源管理器** 中。`assets` 中的每个文件在导入项目后都会生成一个相同名字的 `.meta` 文件,用于存储对应的资源配置和索引信息。`.meta` 文件需要一并提交到版本控制系统,详见 [资源管理注意事项 --- meta 文件](../../asset/meta.md)。 + +一些第三方工具生成的工程或设计源文件,如 TexturePacker 的 `.tps` 文件,或 Photoshop 的 `.psd` 文件,可以选择放在 `assets` 外面管理。 + +### 构建目标(build) + +在使用编辑器主菜单中的 **项目 -> 构建发布** 使用默认发布路径发布项目后,编辑器会在项目路径下创建 `build` 目录,并存放所有目标平台的构建工程。 + +### 资源库(library) + +`library` 是将 `assets` 中的资源导入后生成的,在这里文件的结构和资源的格式将被处理成最终游戏发布时需要的形式。 + +当 `library` 丢失或损坏的时候,只要删除整个 `library` 文件夹再打开项目,就会重新生成资源库。 + +### 本地设置(local) + +`local` 文件夹中包含该项目的本机上的配置信息,包括编辑器面板布局、窗口大小、位置等信息。开发者不需要关心这里的内容。 + +### 编辑器配置(profiles) + +`profiles` 文件夹中包含编辑器的配置信息,包括各目标平台的构建配置信息、场景配置信息等。 + +### 扩展插件文件夹(extensions) + +`extensions` 文件夹用于放置此项目的自定义扩展插件。如果需要手动安装扩展插件,可以手动创建该文件夹。如需卸载扩展插件,在 `extensions` 中删除对应的文件夹即可。 + +### 项目设置(settings) + +`settings` 里保存特定项目相关的设置,如 **项目设置** 面板中相关的配置信息等。如果需要在不同开发者之间同步项目设置,请将 settings 目录加入到版本控制。 + +### 临时文件夹(temp) + +`temp` 是临时文件夹,用于缓存一些 Cocos Creator 在本地的临时文件。这个文件夹可以在关闭 Cocos Creator 后手动删除,开发者不需要关心这里面的内容。 + +### package.json + +`package.json` 文件和 `assets` 文件夹一起,作为验证 Cocos Creator 项目合法性的标志,只有包括了这两个内容的文件夹才能作为 Cocos Creator 项目打开。开发者不需要关心里面的内容。 + +## 版本控制 + +Cocos Creator 在新建项目时,会自动生成 `.gitignore` 文件,用于排除不应该提交到 git 仓库的文件。如果开发者使用其它版本控制系统,或者需要提交项目到其它地方,应该注意只需要提交 `assets`、`extensions`、`settings`、`package.json`,或其它手动添加的关联文件。 diff --git a/versions/4.0/zh/getting-started/project-structure/index/project-file.png b/versions/4.0/zh/getting-started/project-structure/index/project-file.png new file mode 100644 index 0000000000..4a1db43820 Binary files /dev/null and b/versions/4.0/zh/getting-started/project-structure/index/project-file.png differ diff --git a/versions/4.0/zh/getting-started/support.md b/versions/4.0/zh/getting-started/support.md new file mode 100644 index 0000000000..8c614377c0 --- /dev/null +++ b/versions/4.0/zh/getting-started/support.md @@ -0,0 +1,57 @@ +# 获取帮助和支持 + +## 官方媒体 + +- Cocos 引擎官方微信公众号 + + ![Cocos](support/cocos.jpg) + + Cocos 引擎官方微信公众号,第一时间为您送上引擎动态、精品教程、达人专访等干货。 + +- [哔哩哔哩 — Cocos 官方](https://space.bilibili.com/491120849/dynamic) + + 不定期直播、以及官方视频教程等持续更新中。 + +## 提交问题和反馈 + +除了本手册里提供的信息,您还可以随时通过下面的渠道获取信息或反馈问题给 Cocos Creator 开发团队: + + +- [Cocos 论坛社区](https://forum.cocos.org/c/58) +- [Cocos 技术支持](https://www.cocos.com/assistant) + +或者参考 [如何在 GitHub 上向 Creator 提交代码](../submit-pr/submit-pr.md) 来提交你的修改。 + +## Cocos Store + +- [Cocos Store](http://store.cocos.com/):各类美术资源、插件、源码、学习DEMO以及实用的解决方案 + +## 实用的第三方工具 + +### 代码编辑工具 + +- [VS Code](https://code.visualstudio.com/):微软推出的轻量级文本编辑器,支持 Cocos Creator 代码提示和语法高亮 +- [WebStorm](https://www.jetbrains.com/webstorm/) + +### 图集生产工具 + +- [TexturePacker](https://www.codeandweb.com/texturepacker) + +### 位图字体生产工具 + +- [Glyph Designer](https://71squared.com/glyphdesigner) +- [Hiero](https://github.com/libgdx/libgdx/wiki/Hiero) +- [BMFont (Windows)](http://www.angelcode.com/products/bmfont/) +- [snowb-bmf](https://github.com/SilenceLeo/snowb-bmf) + (来自论坛用户 [哈库拉玛塔塔](https://forum.cocos.org/t/topic/147487/18) 分享) + +### 2D 骨骼动画工具 + +- [Spine](http://esotericsoftware.com) +- [Spriter](http://brashmonkey.com/) +- [DragonBones](http://dragonbones.github.io/) + +### 粒子特效制作工具 + +- [Particle Designer](http://particledesigner.71squared.com/) +- [Particle2dx](http://www.effecthub.com/particle2dx):免费在线工具 diff --git a/versions/4.0/zh/getting-started/support/cocos.jpg b/versions/4.0/zh/getting-started/support/cocos.jpg new file mode 100644 index 0000000000..78f241d0ad Binary files /dev/null and b/versions/4.0/zh/getting-started/support/cocos.jpg differ diff --git a/versions/4.0/zh/glossary/index.md b/versions/4.0/zh/glossary/index.md new file mode 100644 index 0000000000..47333ae327 --- /dev/null +++ b/versions/4.0/zh/glossary/index.md @@ -0,0 +1,107 @@ +# 术语 + +本章旨在搜集和整理 Cocos Creator 以及和开发相关的一些术语和缩写,并提供解释。 + +## 联系我们 + +我们会持续更新此文档,如果您发现有未经解释的术语或描述不清的情况请点击屏幕右侧的提交反馈或 [获取帮助和支持](../getting-started/support.md)。 + +## 编辑器 + +| 术语 | 描述 | +| :--- | :--- | +| Editor | 通常指的是 Cocos Creator 的编辑器界面| +| Gizmo | 场景编辑器内的一些控制器,比如用于调整节点的位置、旋转、缩放或者光照等等。| + +## 图形 + +| 术语 | 描述 | +| :--- | :--- | +| VB, Vertex Buffer | 顶点缓存,通常是指的一个模型内,顶点的合集,常用数组描述上,数组内的每个元素为一个复合结构,包括位置(Position)、纹理坐标(Tex-coord)、顶点颜色(Color)或者法线(Normal)等属性 | +| IB, Index Buffer | 索引缓存,内部存储的是上述 VB 中的顶点的索引,通过这些索引组合出特定的几何形状, 示例: 如 VB 为 \{ (0, 0, 0), (1, 0, 0), (1, 1, 0) \},IB 为 [0, 2, 1], 那么此时绘制出的三角形顶点和顺便如下: \{ VB[0], VB[2],VB[1] \} | +| FPS | 帧率,指的是每秒中可以渲染的次数 | +| 层级,Layer | 指的是在渲染时,节点的分组情况,和 [相机的 Visibility 属性](../editor/components/camera-component.md) 配合可以处理某些节点是否被该相机渲染 | +| 多层次细节,LOD | Level of details, 通过不同层级拥有不同细节的模型,来提升渲染效率 | + +### 光照 + +| 术语 | 描述 | +| :--- | :--- | +| GI, Global Illumination | 全局光照或者局部光照,指的是在计算机图形学中用于计算和模拟真实光源的一组算法 | +| LDR | 低动态范围光,通常指单个颜色的色域在 [0,255](整数) 之间 或者 [0,1.0](浮点数) 之间的颜色范围,也就是大多数显示设备可显示的颜色空间 | +| HDR | 高动态范围光,指的是颜色色域超出上条所述的颜色空间的颜色,显示中的颜色很可能会超出这个颜色范围,所以在显示设备显示时,需要使用 **色调映射** 将其转化到显示器可识别的区域 | + +## 2D/UI + +| 术语 | 描述 | +| :--- | :--- | +| 2D Object | 通过在 **层级管理器** 右键选单内创建的 2D 对象 | +| UI Object | 通过在 **层级管理器** 右键选单内创建的 UI 对象,通常是封装了某些用于交互的 UI 元素 | +| Sprite Atlas | [图集](../asset/atlas.md),通过将一些碎片合并成单张的大图用于提高渲染效率,引擎提供 [自动合图](../asset/auto-atlas.md) 功能 | + +## 动画 + +| 术语 | 描述 | +| :--- | :--- | +| Spine | 一款 2D 动画制作软件,其输出的动画文件可以被 Cocos Creator 识别 | +| DragonBone | 一款 2D 动画制作软件,其输出的动画文件可以被 Cocos Creator 识别 | +| Animation | 动画,引擎内置的动画,无蒙皮功能,支持不同节点可以通过编辑器内的动画功能制作,[更多](../animation/index.md) +| SkeletalAnimation | 骨骼动画,带有蒙皮功能的动画组件,[更多](../animation/skeletal-animation.md) | +| Marionette | 支持用户自定义状态图的动画系统,和 SkeletalAnimation 配合可以实现丰富的功能,[更多](../animation/marionette/index.md) | + +## 物理 + +| 术语 | 描述 | +| :--- | :--- | +| 力 | 使物体可以线性移动的力 | +| 扭矩 | 转矩,使物体旋转的力 | + +### 2D 物理 + +| 术语 | 描述 | +| :--- | :--- | +| 物理后端 | 引擎封装的物理引擎的类型,Cocos Creator 提供两种物理后端分别是 **内置** 和 **Box2D** | +| Box2D | Box2D 是有名且高效的 2D 物理引擎 | +| 刚体 | 定义了可受力、速度影响的具有质量的物体,通常有三种类型:运动学、动力学以及静态 | +| 关节 | Joint 2D 物理关节,用于描述物体之间的物理连接情况 | +| 物理材质 | 用于调整两个物理碰撞体碰撞时的摩擦力、弹力处理情况 | + +### 3D 物理 + +| 术语 | 描述 | +| :--- | :--- | +| 物理后端 | 引擎封装的物理引擎的类型,Cocos Creator 提供多种物理引擎包括: **内置**、**Bullet**、**PhysX** 和 **cannon.js** 等,可以在项目设置中修改以适配不同的平台和情况,**注意**:引擎抹平了大部分物理引擎的差异,但某些物理特定仅在特定的物理后端支持。| +| 刚体 | 定义了可受力、速度影响的具有质量的物体,通常有三种类型:运动学、动力学以及静态 | +| 约束 | 物理约束,用于描述物体之间的物理连接情况 | +| 物理材质 | 用于调整两个物理碰撞体碰撞时的摩擦力、弹力处理情况 | +| 力 | 使物体可以线性移动的力 | +| 扭矩 | 转矩,使物体旋转的力 | + +## 资源系统 + +| 术语 | 描述 | +| :--- | :--- | +| Bundle, Asset Bundle | 一种处理资源的方式,引擎会将一个或者多个资源通过算法整合到单个文件内(通常是某种压缩文件)用于提升存储和下载的效率 | +| Resources | 特殊目录,引擎会将该目录打包为特殊的 Bundle,通过 `Resources.load` 等方法加载| +| Cocos Store | Cocos 的资源商店,开发者可以在上面出售和购买资产、源码或完整的商业游戏 | + +## 脚本与编程 + +| 术语 | 描述 | +| :--- | :--- | +| 组件 | 继承自 `cc.Component` 的脚本,可以挂载在节点上 | +| 事件 | Cocos Creator 基于 [Web 的事件冒泡及捕获标准](https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Building_blocks/Events#%E4%BA%8B%E4%BB%B6%E5%86%92%E6%B3%A1%E5%8F%8A%E6%8D%95%E8%8E%B7) 实现的事件系统,用于响应硬件输入或者节点间的通信,[更多](../engine/event/index.md) + +## 原生开发 + +| 术语 | 描述 | +| :--- | :--- | +| 原生| 指的是 Cocos Creator 提供的,得以不依赖浏览器,在各桌面原生平台或移动原生平台运行的能力 | +| CMake | 一种组织 C ++ 代码的文件格式或者工具 | + +## 其他功能 + +| 术语 | 描述 | +| :--- | :--- | +| XR | Extended Reality。适配 OpenXR 的 Cocos Creator 扩展程序,可以协助您制作增加现实功能,[更多](../xr/index.md) | +| AR | Augmented reality,增强现实。将虚拟世界和现实联系起来的技术,您可以使用 Cocos Creator 实现自己的 AR 应用。[更多](../xr/index.md) | diff --git a/versions/4.0/zh/graphics-backend/overview.md b/versions/4.0/zh/graphics-backend/overview.md new file mode 100644 index 0000000000..cac2e96038 --- /dev/null +++ b/versions/4.0/zh/graphics-backend/overview.md @@ -0,0 +1,15 @@ +# 总览 + +渲然后端是GFX提供的API的native图形接口实现。Cocos Creator支持的渲染后端有: + +- 原生 + - GLES2 + - GLES3 + - Vulkan + - Metal + - NVN + - WebGPU on WASM + +- Web/小游戏 + - WebGL + - WebGL2.0 diff --git a/versions/4.0/zh/guide/unity/index.md b/versions/4.0/zh/guide/unity/index.md new file mode 100644 index 0000000000..ea9ad3a9d6 --- /dev/null +++ b/versions/4.0/zh/guide/unity/index.md @@ -0,0 +1,351 @@ +# Unity 开发者入门 Cocos Creator 快速指南 + +随着游戏平台和渠道的种类越来越多,开发者希望自己的游戏能够一次编写多次发布到不同的平台和渠道,而 Cocos Creator 恰好是很好的满足这一需求。 + +本文将从一个 Unity 开发者的视角,从以下角度对比,帮助 Unity 开发者迅速上手 Cocos Creator引擎。 + +- 安装和版本管理 +- 编辑器 +- 资源工作流 +- 脚本以及调试 +- 着色器 + +## 安装和版本管理 + +Unity Hub 可以使用来管理 Unity 的编辑器版本、项目以及各种模板。在 Cocos Creator 中,同样您也可以通过 [Cocos Dash Board](https://www.cocos.com/creator-download) 来管理引擎,项目以及模板。 + +| Unity Hub | Cocos Dashboard | +| :-- | :-- | +| ![unity-hub](./index/unity-hub.png) | ![cocos-dash-board](./index/cocos-dash-board.png) | + +同样您也可以在 Store 分页中找到大量可供使用的插件、资源和源码,以及在 Learn 分页中找到更多可供学习的素材。 + +## 编辑器 + +作为一个 Unity 开发者,在绝大多数的情况下您可以无缝使用 Cocos Creator 的编辑器,他们拥有接近的编辑器布局以及使用方式。 + +| Unity Editor | Cocos Creator Editor | +| :-- | :-- | +| ![overview](./index/unity-editor-overview.png) | ![overview](./index/cocos-editor-overview.png) + +略有不同的地方在于, Cocos Creator 由于使用 Electronic+Chromium 开发,您既可以通过浏览器预览游戏,也可以直接在编辑器内运行游戏。 + +## 工作流 + +Cocos Creator 的 2D 和 3D 工作流与 Unity 类似,您可以阅读 [场景制作工作流程](../../concepts/scene/index.md) 来查看 Cocos Creator 的工作流。 + +### 贴图资源 + +贴图资源的导入和 Unity 类似。 + +| Unity | Cocos | +| :-- | :-- | +| | | + +也可以在项目设置中配置全局纹理压缩 + +![cocos-texture-compress.png](./index/cocos-texture-compress.png) + +### 模型和动画 + +在 Cocos Creator 中导入 FBX 和 Unity 是一样的,将文件拖拽或者复制到工程的 Assets 目录下即可。 + +| Unity | Cocos | +| :-- | :-- | +| | | + +同时 Cocos Creator 也支持 glTF 格式的文件,以及 Maya、3DMax 等 DCC 工具的标准材质。 + +### Spine 骨骼动画 + +Cocos Creator 内置了 Spine 动画组件,您可以直接通过 `spine.Skeleton` 组件来使用他。 + +### 动画和状态机 + +Cocos Creator 支持关键帧动画、骨骼动画。您可以直接在编辑器内编辑和预览这些动画。 + +![cocos-keyframe-anim.png](./index/cocos-keyframe-anim.png) + +和 Unity 的 Animator 类似,Cocos Creator 也支持动画状态机的编辑,您可以在 [Marionette 动画系统](../../animation/marionette/index.md) 中找到他们。 + +![./index/cocos-anim-graph.png](./index/cocos-anim-graph.png) + +### 音乐和音效 + +Cocos Creator 同样支持 Audio Source 组件用于播放音乐和音效。 + +![cocos-audio-source.png](./index/cocos-audio-source.png) + +### 资源包 + +和 Unity 类似,Cocos Creator 也支持从外界导入资源包的方式进行合作开发。 + +| Unity | Cocos | +| :-- | :-- | +| | | + +### 发布和构建 + +除了和 Unity 一样发布在各种原生平台外,Cocos Creator 也支持发布在如微信小游戏、抖音小游戏等小游戏平台。 + +| Unity | Cocos | +| :-- | :-- | +| | | + +## 脚本编程和调试 + +和 Unity 的 GameObject 不同,在 Cocos Creator 中,场景中的实体被命名为 Node,但是和 Unity 类似的是 Cocos Creator 也是 ECS(Entity-Component-System) 架构,您也可以通过给 Node 添加不同的组件来实现游戏的功能。 + +![cocos-add-component.png](./index/cocos-add-component.png) + +### 组件的生命周期 + +和 Unity 类似, Cocos Creator 的组件也有自己的生命周期,系统将通过回调组件内已注册的方法,方便开发者处理业务逻辑。 + +![组件生命周期](./index/script-lifecycle.png) + +### 自定义组件的编写 + +在 Unity 中,我们继承 Monobehavior 来实现我们自己的游戏脚本。 + +```csharp +public class Player : NetworkBehaviour +{ + Animation _animation; + + Start(){ + _animation = gameObject.GetComponent(); + } +} +``` + +Cocos Creator 使用 Typescript 来编写脚本。 + +下面的例子演示了如何使用 Typescript 实现自定义组件。 + +```ts +@ccclass('MotionController') +export class MotionController extends Component { + + animation: SkeletalAnimation; + + start() { + this.animation = this.getComponent(SkeletalAnimation); + } +} +``` + +C# 和 Typescript 都是微软开发的编程语言,其易用程度是类似的。 + +### 调试和日志 + +#### 日志调试 + +Unity中使用日志我们可以使用 `Debug.Log` 的方法。 + +在 Cocos Creator 中使用日志,既可以使用 js 的日志打印 `console.log()`,也可使用 Cocos Creator 的日志方法: + +```ts +cc.log() +cc.debug() +cc.error() +``` + +#### 断点调试 + +Unity 可以使用 Visual Studio 或者 VSCode 进行断点调试。 + +Cocos Creator 使用 VSCode 或者直接在浏览器内通过开发者工具调试。 + +![cocos-debug.png](./index/cocos-debug.png) + +## 材质以及着色器编写 + +### 材质 + +Cocos Creator 的材质和 Unity 材质拥有类似的预览和属性面板。 + +| Unity | Cocos | +| :-- | :-- | +| | | + +和 Unity 不同的地方在于 Cocos Creator 在材质中可以比较方便的查看和定义管线中的渲染状态。 + +![cocos-pipeline-state.png](./index/cocos-pipeline-state.png) + +### 着色器 + +和 Unity 支持 CG、GLSL 以及 HLSL 不同,Cocos Creator 仅支持 GLSL 作为着色器编程语言。 + +下面的表格对比了他们所使用的文件格式以及 DSL 的区别。 + +| | Unity | Cocos | +| :--- | :--- | :--- | +| 文件格式 | *.shader | *.effect | +| DSL | Cg/HLSL/GLSL + Unity Shader Format | GLSL + Yaml | + +Unity 采用自定义的 shader 文件来作为 DSL, 而 Cocos creator 使用的是 Yaml 作为 DSL 的文件格式。 + +#### 着色器语法规则 + +Unity Shader 语法规则 + +```shader +Shader "Transparent/Cutout/DiffuseDoubleside" { +Properties { + _Color ("Main Color", Color) = (1,1,1,1) + _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {} + _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5 +} + +SubShader { + Tags {"IgnoreProjector"="True" "RenderType"="TransparentCutout"} + LOD 200 + Cull Off + +CGPROGRAM +#pragma surface surf Lambert alphatest:_Cutoff + +sampler2D _MainTex; +float4 _Color; + +struct Input { + float2 uv_MainTex; +}; + +void surf (Input IN, inout SurfaceOutput o) { + half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; + o.Albedo = c.rgb; + o.Alpha = c.a; +} +ENDCG +} + +Fallback "Transparent/Cutout/VertexLit" +} + +``` + +Cocos creator effect 语法规则 + +``` +// Effect Syntax Guide: https://github.com/cocos-creator/docs-3d/blob/master/zh/material-system/effect-syntax.md + +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: general-vs:vert # builtin header + frag: unlit-fs:frag + properties: &props + mainTexture: { value: white } + mainColor: { value: [1, 1, 1, 1], editor: { type: color } } + - name: transparent + passes: + - vert: general-vs:vert # builtin header + frag: unlit-fs:frag + blendState: + targets: + - blend: true + blendSrc: src_alpha + blendDst: one_minus_src_alpha + blendSrcAlpha: src_alpha + blendDstAlpha: one_minus_src_alpha + properties: *props +}% + +CCProgram unlit-fs %{` + precision highp float; + #include + #include + + in vec2 v_uv; + uniform sampler2D mainTexture; + + uniform Constant { + vec4 mainColor; + }; + + vec4 frag () { + vec4 col = mainColor * texture(mainTexture, v_uv); + CC_APPLY_FOG(col); + return CCFragOutput(col); + } +}% + +``` + +### 着色器语法对比 + +本小节将对比 Unity Shader 和 Cocos Effect 的文件结构。 + +### 结构对比 + +定义Shader对象 + +- Unity shader: + + ```shader + Shader "" + { + + + + + } + ``` + +- Cocos Shader: + + ```yaml + CCEffect %{ + + + + + + + } + ``` + +### Pass 结构 + +- Unity Shader: + + ```shader + SubShader{ + + Tag {} + + Pass + } + + ``` + + Pass: + + ```yaml + Pass{ + + + + + + } + ``` + +- CocosCreator Shader: + + ```yaml + CCProgram %{ + + + + + + + function vert(); + + function frag(); + }% + ``` diff --git a/versions/4.0/zh/guide/unity/index/cocos-add-component.png b/versions/4.0/zh/guide/unity/index/cocos-add-component.png new file mode 100644 index 0000000000..f170c79d1c Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-add-component.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-anim-graph.png b/versions/4.0/zh/guide/unity/index/cocos-anim-graph.png new file mode 100644 index 0000000000..ff794b9a1c Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-anim-graph.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-audio-source.png b/versions/4.0/zh/guide/unity/index/cocos-audio-source.png new file mode 100644 index 0000000000..3ddb1ea0fe Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-audio-source.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-build.png b/versions/4.0/zh/guide/unity/index/cocos-build.png new file mode 100644 index 0000000000..ecc75fef5e Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-build.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-dash-board.png b/versions/4.0/zh/guide/unity/index/cocos-dash-board.png new file mode 100644 index 0000000000..d5b0266a1e Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-dash-board.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-debug.png b/versions/4.0/zh/guide/unity/index/cocos-debug.png new file mode 100644 index 0000000000..80d85fdd75 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-debug.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-editor-overview.png b/versions/4.0/zh/guide/unity/index/cocos-editor-overview.png new file mode 100644 index 0000000000..80e8d330ab Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-editor-overview.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-import-package.png b/versions/4.0/zh/guide/unity/index/cocos-import-package.png new file mode 100644 index 0000000000..f1a2f52fe6 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-import-package.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-keyframe-anim.png b/versions/4.0/zh/guide/unity/index/cocos-keyframe-anim.png new file mode 100644 index 0000000000..58a4bd2573 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-keyframe-anim.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-material.png b/versions/4.0/zh/guide/unity/index/cocos-material.png new file mode 100644 index 0000000000..40b1eed64f Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-material.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-model.png b/versions/4.0/zh/guide/unity/index/cocos-model.png new file mode 100644 index 0000000000..ddacf4ea1e Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-model.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-pipeline-state.png b/versions/4.0/zh/guide/unity/index/cocos-pipeline-state.png new file mode 100644 index 0000000000..bdff7da249 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-pipeline-state.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-texture-compress.png b/versions/4.0/zh/guide/unity/index/cocos-texture-compress.png new file mode 100644 index 0000000000..fdb27975c5 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-texture-compress.png differ diff --git a/versions/4.0/zh/guide/unity/index/cocos-texture-inspector.png b/versions/4.0/zh/guide/unity/index/cocos-texture-inspector.png new file mode 100644 index 0000000000..1345ec3503 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/cocos-texture-inspector.png differ diff --git a/versions/4.0/zh/guide/unity/index/script-lifecycle.png b/versions/4.0/zh/guide/unity/index/script-lifecycle.png new file mode 100644 index 0000000000..84ea880faa Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/script-lifecycle.png differ diff --git a/versions/4.0/zh/guide/unity/index/unity-build.png b/versions/4.0/zh/guide/unity/index/unity-build.png new file mode 100644 index 0000000000..2090f596a9 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/unity-build.png differ diff --git a/versions/4.0/zh/guide/unity/index/unity-editor-overview.png b/versions/4.0/zh/guide/unity/index/unity-editor-overview.png new file mode 100644 index 0000000000..e53330762a Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/unity-editor-overview.png differ diff --git a/versions/4.0/zh/guide/unity/index/unity-hub.png b/versions/4.0/zh/guide/unity/index/unity-hub.png new file mode 100644 index 0000000000..cd1c6e61d4 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/unity-hub.png differ diff --git a/versions/4.0/zh/guide/unity/index/unity-import-package.png b/versions/4.0/zh/guide/unity/index/unity-import-package.png new file mode 100644 index 0000000000..abafe53bb8 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/unity-import-package.png differ diff --git a/versions/4.0/zh/guide/unity/index/unity-material.png b/versions/4.0/zh/guide/unity/index/unity-material.png new file mode 100644 index 0000000000..3610223813 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/unity-material.png differ diff --git a/versions/4.0/zh/guide/unity/index/unity-model.png b/versions/4.0/zh/guide/unity/index/unity-model.png new file mode 100644 index 0000000000..10b64c569f Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/unity-model.png differ diff --git a/versions/4.0/zh/guide/unity/index/unity-texture-inspector.png b/versions/4.0/zh/guide/unity/index/unity-texture-inspector.png new file mode 100644 index 0000000000..2c625dc303 Binary files /dev/null and b/versions/4.0/zh/guide/unity/index/unity-texture-inspector.png differ diff --git a/versions/4.0/zh/importer/materials/cocos-viewport.png b/versions/4.0/zh/importer/materials/cocos-viewport.png new file mode 100644 index 0000000000..cb7e2ec5f2 Binary files /dev/null and b/versions/4.0/zh/importer/materials/cocos-viewport.png differ diff --git a/versions/4.0/zh/importer/materials/enable-smart-conversion.png b/versions/4.0/zh/importer/materials/enable-smart-conversion.png new file mode 100644 index 0000000000..03eb767d4c Binary files /dev/null and b/versions/4.0/zh/importer/materials/enable-smart-conversion.png differ diff --git a/versions/4.0/zh/importer/materials/fbx-materials.md b/versions/4.0/zh/importer/materials/fbx-materials.md new file mode 100644 index 0000000000..86a0bf5668 --- /dev/null +++ b/versions/4.0/zh/importer/materials/fbx-materials.md @@ -0,0 +1,130 @@ +# FBX 智能材质导入 + +FBX 智能材质导入是模型导入器中辅助转换材质的一个功能,它可以将各种 DCC(Digital Content Creation) 工具导出到模型中的部分标准材质直接映射到 Cocos Creator 的内置材质中,尽量还原美术在 DCC 工具中看到的材质效果。该功能为 v3.5.1 新增。 + +该功能当前支持以下材质: + +| Software | Phong | PBR | +|:-------- |:------------------- |:------------------| +| 3ds Max | Standard(legacy) | Physical Material | +| Blender | N/A | Principled BSDF | +| C4D | Standard | N/A | +| Maya | Lambert/Blinn/Phong | Standard Surface | + +以 Maya Standard Surface 为例,材质导入 Cocos Creator 效果对比下表所示: + +| Maya Viewport | Cocos Creator Viewport | +| :----------------------------|:-----------------------------| +| ![Maya](maya-viewport.png) | ![cocos](cocos-viewport.png) | + +您可以参考以下工程文件设置,以确保导入器可以准确的导入 Maya Standard Surface 各个通道的贴图。 + +[Maya Car Demo](maya_car.zip) + +## FBX 智能材质导入配置和流程 + +1. 在 Cocos Creator 主菜单依次进入 **Project -> Project Settings -> Model -> Smart Material Conversion** 确保该选项出于开启状态。 +2. 在 Cocos Creator 中选中 FBX 文件,确保 FBX 文件的 **Inspector** 面板中 **Smart Material Conversion** 选项处于开启状态。 + +设置结果如下图所示: + +![img_6.png](enable-smart-conversion.png) + +## 不同 DCC 材质支持说明 + +### Autodesk 3ds Max + +- 材质: 支持以下材质类型: + - Standard(Legacy) + - Physical Material (建议使用) + - Multi/SubObject + - Multi/SubObject 的子材质类型只能是 Standard 或者 Physical Material,不可以是 Multi/SubObject, 否则,导入后部分材质会丢失 + +- 纹理: + - 对于 Standard(Legacy) 材质, Cocos Creator 支持以下纹理贴图的自动导入: + - Diffuse Color + - Specular Color + - Glossiness + - Opacity + - Bump + - 对于 Physical Material, Cocos Creator 支持以下纹理贴图的自动导入: + - Base Color + - Roughness + - Metalness + - Bump + - Opacity + - 纹理导入简化要求: + - 在导出 FBX 前,除 Normal Map 可以用 Normal Bump 节点矫正外,请确保证材质右侧输入的纹理节点均为 Bitmap 节点。 + - 关于如何将简化纹理简化为只剩Bitmap节点,您可以参考 [Convert a Procedural texture into a bitmap image texture in 3ds Max](https://knowledge.autodesk.com/support/3ds-Max/learn-explore/caas/sfdcarticles/sfdcarticles/How-to-convert-a-Procedural-texture-into-a-bitmap-image-texture-in-3ds-Max-for-fbx-export.html) + +| 简化前 | 简化后 | +|:---------------------|:-------------------------| +| ![img.png](img.png) | ![img_1.png](img_1.png) | + +渲染说明:对于 Max Physical Material 材质,需要在 Max Viewport 中开启 High Quality 才可能获取相对准确的预览效果。为了获取最佳的配对效果,您可以配对 Max 和 Cocos Creator 的渲染环境。 + +- 关于 Max Viewport 渲染更多设置内容,您可参考 [教程](https://www.youtube.com/watch?v=82hhg8Q1nus&list=PL9xXzsdQ6pbZGBnVSKMBO_BCYjzmFTj0R&index=2) +- 关于 Cocos Creator 渲染环境配置详细内容,您可参考 [Cocos Creator 官方文档](https://docs.cocos.com/creator/manual/zh/module-map/graphics.html) + +### Autodesk Maya + +- 材质:支持以下材质类型: + - Lambert、Blinn、Phong、Phong-E + - Standard Surface (建议使用) +- 纹理: + - 对于 Lambert,Blinn,Phong Cocos Creator 支持以下纹理自动导入: + - Color + - Normal + - Transparency + - SpecularColor + - Cosine Power + - 对于 Standard Surface Cocos Creator 支持以下纹理通道的自动导入: + - Base Color + - Specular Roughness + - Metalness + - Normal + - Alpha + - 纹理导入要求: 在导出 FBX 之前,除 Bump Map 可以用 Bump2D 节点矫正外,确保有贴图输入通道的输入户节点类型均为 File 纹理节点。 + - 关于如何将简化纹理简化为只剩 File Texture 节点,您可以参考 [Convert a texture or shading network to a File Texture in Maya](https://knowledge.autodesk.com/support/Maya/learn-explore/caas/CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-0F504570-CB7A-49D3-A7A2-83438C353A9C-htm.html) + +| 简化前 | 简化后 | +|:-------------------------|:-------------------------| +| ![img_2.png](img_2.png) | ![img_3.png](img_3.png) | + +渲染说明:使用 Maya Viewport 预览时半透明材质时,建议开启 Depth peeling 和 Alpha Cut Prepass 以获取正准确的预览效果。 +为了获取最佳的配对效果,您可以配对 Maya 和 Cocos Creator 的渲染环境。 +- 关于 Maya Viewport渲染设置更多内容,您可参考 [Maya 官方文档](https://help.autodesk.com/view/MAYAUL/2022/ENU/) +- 关于 Cocos Creator 渲染环境配置详细内容,您可参考 [Cocos Creator 官方文档](https://docs.cocos.com/creator/manual/zh/module-map/graphics.html) + +### Cinema 4D + +- 材质:支持以下材质类型: + - Standard Material +- 纹理: + - 支持以下纹理: + - Diffuse Color + - Specular Color + - Glossiness + - Opacity + - Bump + - 纹理导出要求:C4D 导出 FBX 前,若一个模型有多个材质需要保证UV集的面和材质有唯一的对应关系。 + - [Example](https://github.com/cocos-creator/3d-tasks/issues/11267) + +### Blender + +- 材质:支持以下材质类型: + - Principled bsdf +- 纹理: + - 对于 Principled bsdf 材质支持以下纹理: + - Base Color + - Roughness + - Metallic + - Normal + - Alpha + - 纹理导入要求:在导出 FBX 之前,除 Normal Map 可以用 Normal Map 节点矫正外,确保材质的其它贴图输入通道均为File纹理节点。 + +关于如何将简化纹理简化为只剩 Bitmap 节点,您可以参考 [Baking Procedural Materials to Image Textures in Blender](https://www.youtube.com/watch?v=AB24ITZHtuE) + +| 简化前 | 简化后 | +|:-------------------------|:-------------------------| +| ![img_4.png](img_4.png) | ![img_5.png](img_5.png) | diff --git a/versions/4.0/zh/importer/materials/img.png b/versions/4.0/zh/importer/materials/img.png new file mode 100644 index 0000000000..f100c6ec7e Binary files /dev/null and b/versions/4.0/zh/importer/materials/img.png differ diff --git a/versions/4.0/zh/importer/materials/img_1.png b/versions/4.0/zh/importer/materials/img_1.png new file mode 100644 index 0000000000..cbb4486c3c Binary files /dev/null and b/versions/4.0/zh/importer/materials/img_1.png differ diff --git a/versions/4.0/zh/importer/materials/img_2.png b/versions/4.0/zh/importer/materials/img_2.png new file mode 100644 index 0000000000..ae4592d89f Binary files /dev/null and b/versions/4.0/zh/importer/materials/img_2.png differ diff --git a/versions/4.0/zh/importer/materials/img_3.png b/versions/4.0/zh/importer/materials/img_3.png new file mode 100644 index 0000000000..3790b3180e Binary files /dev/null and b/versions/4.0/zh/importer/materials/img_3.png differ diff --git a/versions/4.0/zh/importer/materials/img_4.png b/versions/4.0/zh/importer/materials/img_4.png new file mode 100644 index 0000000000..b5ede6560e Binary files /dev/null and b/versions/4.0/zh/importer/materials/img_4.png differ diff --git a/versions/4.0/zh/importer/materials/img_5.png b/versions/4.0/zh/importer/materials/img_5.png new file mode 100644 index 0000000000..478f2aeb00 Binary files /dev/null and b/versions/4.0/zh/importer/materials/img_5.png differ diff --git a/versions/4.0/zh/importer/materials/maya-viewport.png b/versions/4.0/zh/importer/materials/maya-viewport.png new file mode 100644 index 0000000000..ce49d01b8e Binary files /dev/null and b/versions/4.0/zh/importer/materials/maya-viewport.png differ diff --git a/versions/4.0/zh/importer/materials/maya_car.zip b/versions/4.0/zh/importer/materials/maya_car.zip new file mode 100644 index 0000000000..56b24fb8c7 Binary files /dev/null and b/versions/4.0/zh/importer/materials/maya_car.zip differ diff --git a/versions/4.0/zh/index.md b/versions/4.0/zh/index.md new file mode 100644 index 0000000000..409c23bbac --- /dev/null +++ b/versions/4.0/zh/index.md @@ -0,0 +1,50 @@ +# Cocos Creator 4.0 用户手册 + +欢迎使用 Cocos Creator 4.0! + +Cocos Creator 既是一款高效、轻量、免费开源的跨平台 2D&3D 图形引擎,也是一个实时 2D&3D 数字内容创作平台。拥有 **高性能**、**低功耗**、**流式加载**、**跨平台** 等诸多优点,您可以用它来创作 **游戏**、**车机**、**XR**、**元宇宙** 等领域的项目。 + +本手册包括详尽的使用说明、面向不同职能用户的工作流程和新手教程,可以帮您快速掌握如何使用 Cocos Creator 以及其相关服务。 + +您可以将此文档从头读到尾,也可以在有需要的时候用来查阅。 + +如果您是第一次使用 Cocos Creator,可以从 [新手入门](getting-started/index.md) 和 [示例与教程](./cases-and-tutorials/index.md) 开始。 + +如果您已经熟悉其他引擎如 Unity,您可以查看 [Unity 开发者入门 Cocos Creator 快速指南](./guide/unity/index.md) 来快速上手 Cocos Creator。 + +## 用户手册主要模块 + +- [场景制作](concepts/scene/index.md) +- [资源系统](asset/index.md) +- [脚本指南及事件系统](scripting/index.md) +- [发布跨平台游戏](editor/publish/index.md) +- [图形渲染](module-map/graphics.md) +- [2D 渲染](2d-object/2d-render/index.md) +- [UI 系统](2d-object/ui-system/index.md) +- [动画系统](animation/index.md) +- [声音系统](audio-system/overview.md) +- [物理系统](physics/index.md) +- [粒子系统](particle-system/index.md) +- [缓动系统](tween/index.md) +- [地形系统](editor/terrain/index.md) +- [资源管理](asset/asset-manager.md) +- [本地化](editor/l10n/overview.md) +- [扩展编辑器](editor/extension/readme.md) +- [进阶主题](advanced-topics/index.md) + +## 更多内容 + +- [Cocos 官方论坛](https://forum.cocos.org/) 可以提问、查找问题答案、与其他开发者交流 +- [案例与教程](./cases-and-tutorials/index.md) 可以获得教程和官方示例项目 +- [Cocos Store](https://store.cocos.com) 可以获得更多素材、学习案例以及源码 + +## 产品线简介 + +Cocos(雅基软件)多年来不断发展,已经发布了多个与 Cocos Creator 密切相关的产品线。为避免混淆,以下是对这些产品的简要介绍: +- **Cocos Creator 3.x**:发布于 2021 年初,是当前 Cocos Creator 的最新版本,已经过大量商业项目验证。3.x 完全摒弃了 Cocos2d-x 底层,采用全新高性能跨平台 3D 内核,标志着 Cocos Creator 正式发展为全面的泛移动端 3D 游戏引擎。由于 3.x 底层已经完全重写,Cocos Creator 不再被视为 Cocos2d-x 的直接扩展和升级版本。 +- **Cocos Creator 2.x**:发布于 2018 年,2023 年停止更新。所有能力已被 Cocos Creator 3.x 继承,因此建议新项目使用[最新的 Cocos Creator 3.x](https://www.cocos.com/creator-download)。 +- **Cocos Creator 3D**:2017 年立项,2019 年底以 Cocos Creator 3D 的身份在中国进行了一年多的小规模测试,后正式合并至 Cocos Creator 3.0。由于已被 Cocos Creator 3.x 替代且不再单独更新,提及 Cocos Creator 3D 时通常指代 Cocos Creator 本身的 3D 能力,而非此特定版本。 +- **[Cocos2d-x](https://www.cocos.com/cocos2d-x)**:发布于 2010 年,2019 年停止更新。这是 Cocos2d 社区最活跃的分支,Cocos Creator 2.x 最初采用的底层运行时便是升级过后的 Cocos2d-x。 +- **Cocos**:当 Cocos 作为引擎的名字单独出现时,通常代表 Cocos Creator 3.x,而不是 Cocos2d-x。 + +经过多年的快速发展,Cocos Creator 3.x 与 Cocos Creator 2.x 在用法上已经有所不同,二者的 API 也不完全兼容。因此,在查阅文档、API 和教程时,请开发者注意辨别目标版本是 2.x 还是 3.x,以免因版本不一致导致错误。 diff --git a/versions/4.0/zh/material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md b/versions/4.0/zh/material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md new file mode 100644 index 0000000000..1465a5eef4 --- /dev/null +++ b/versions/4.0/zh/material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md @@ -0,0 +1,159 @@ + +# Cocos Creator 3.1 材质升级指南 + +> 本文将详细介绍 Cocos Creator 3.0 的材质升级到 v3.1 的注意事项。 + +## 1. 着色器升级与变化 + +### 1.1 内置头文件变化 + +原来 v3.0 的标准着色器头文件 `shading-standard` 在 v3.1 改成了 `standard-surface-entry`,可以使 effect 同时兼容 forward 渲染管线和 deferred 渲染管线。 + +原来 v3.0 的 `cc-fog` 头文件在 v3.1 改成了 `cc-fog-vs/fs`,拆分成了顶点着色器与片元着色器两个部分。 + +### 1.2 顶点着色器 + +- `gl_Position` + + v3.0 的 `VS` 主函数名称 `vert` 在 v3.1 改成了 `main`,并且新增了宏 `gl_Position`,用于给返回值赋值。 + + ```c + CCProgram standard-vs %{ + precision highp float; + + // include your headfile + + #include // 注意这里头文件名称的变化 + + // fill in your data here + + void main () { + + // fill in your data here + + gl_Position = fill in your data result; + } + }% + ``` + +### 1.3 片元着色器 + +- `CC_STANDARD_SURFACE_ENTRY()` + + 加载标准着色器头文件 `standard-surface-entry`,使用 v3.1 的标准着色器输出函数 `CC_STANDARD_SURFACE_ENTRY()` 替换原有 v3.0 着色器输出的函数 `frag()`。 + + ```c + CCProgram standard-fs %{ + + // include your headfile + + #include // 注意这里头文件名称的变化 + #include // 注意这里标准着色器头文件的名称变化 + + // fill in your data here + + void surf (out StandardSurface s) { + + // fill in your data here + + } + CC_STANDARD_SURFACE_ENTRY() // 标准着色器输出函数 + }% + ``` + +## 2. Deferred 渲染管线 + +### 2.1 Deferred Render Pipeline + +v3.1 的材质系统与 v3.0 最大的区别在于 v3.1 支持了 [deferred 渲染管线](../render-pipeline/builtin-pipeline.md),引擎自带标准的 `standard-surface-entry` 头文件,可以同时支持 forward 渲染管线和 deferred 渲染管线,用法如下: + +```c +CCEffect %{ + techniques: + + // fill in your data here + + - &deferred + vert: // your Vertex shader + frag: // your Fragment shader + phase: deferred + propertyIndex: 0 + blendState: + targets: // 关闭混合 + - blend: false + - blend: false + - blend: false + - blend: false + properties: // your properties name + + // fill in your data here + +}% + +// fill in your data here + +CCProgram standard-fs %{ + precision highp float; + #include + #include + #include // 注意这里头文件名称的变化。 + #include // 注意这里标准着色器头文件的名称变化 + + // fill in your data here + void surf (out StandardSurface s) { + + // fill in your data here + + } + CC_STANDARD_SURFACE_ENTRY() // 标准着色器输出函数 +}% + +// fill in your data here + +``` + +### 2.2 渲染管线判断 + +在头文件 `standard-surface-entry` 会判断选择了哪条渲染管线,光照计算在文件 `shading-standard-additive` + +如果判断是 deferred 渲染管线,会先调用 `deferred-lighting` effect 文件,随后调用光照计算文件 `shading-standard-additive` + +```c +#define CC_STANDARD_SURFACE_ENTRY() +#if CC_FORWARD_ADD + #include + + // fill in your data here + +#elif CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_FORWARD // 判断是否为前向渲染管线 + + // fill in your data here + +#elif CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_DEFERRED // 判断是否为延迟渲染管线 + + // fill in your data here + +#endif +``` + +## 3. 参数传输升级 + +v3.0 顶点着色器往片元着色器传递 shadow 参数的宏为 `CCPassShadowParams`,v3.1 则修改为 `CC_TRANSFER_SHADOW`。 + +v3.1 顶点着色器往片元着色器传输 `FOG` 参数时,直接使用 `CC_TRANSFER_FOG` 宏。 + +版本对比: + +- v3.0 + + ```c + v_fog_factor = CC_TRANSFER_FOG(pos); + CCPassShadowParams(pos); + ``` + +- v3.1 + + ```c + CC_TRANSFER_FOG(pos); + CC_TRANSFER_SHADOW(pos); + ``` diff --git a/versions/4.0/zh/material-system/builtin-material.md b/versions/4.0/zh/material-system/builtin-material.md new file mode 100644 index 0000000000..d57af9a682 --- /dev/null +++ b/versions/4.0/zh/material-system/builtin-material.md @@ -0,0 +1,35 @@ +# 内置材质 + +Creator 在 **资源管理器** 面板的 **internal/default_materials/** 目录下内置了几种常见的材质,其使用的 Effect 为 [内置着色器](../shader/effect-builtin.md) ,内置材质的属性都不允许修改。 + +![内置材质](img/builtin-material.png) + +| 内置材质 | 说明 | +| :------------------------------------ | :--------------------------------------------------------- | +| default-material | 非透明物体的 [标准材质](../shader/effect-builtin-pbr.md) | +| default-material-transparent | 半透明物体的 [标准材质](../shader/effect-builtin-pbr.md) | +| particle-add | 标准粒子材质 | +| ui-sprite-material | 精灵的标准材质 | + + \ No newline at end of file diff --git a/versions/4.0/zh/material-system/effect-2.x-to-3.0.md b/versions/4.0/zh/material-system/effect-2.x-to-3.0.md new file mode 100644 index 0000000000..92480bb6bd --- /dev/null +++ b/versions/4.0/zh/material-system/effect-2.x-to-3.0.md @@ -0,0 +1,256 @@ +# Cocos Creator 3.0 材质升级指南 + +> 本文将详细介绍 Cocos Creator 2.x 的材质升级到 v3.0 的注意事项。 + +## 1. 材质系统基础设计简介 + +### 1.1 Cocos Creator 的材质系统框架 + +材质系统自上至下由四个核心类组成,分别是 Material、Effect、Technique 和 Pass,它们的关系可以通过下面的类图来理解: + +![effect](material.png) + +#### Material + +Material 资源可以看成是 EffectAsset 在场景中的资源实例,它本身的可配置参数有 effectAsset、technique、defines、states。 + +#### Cocos Shader + +Cocos Shader 以 *.effect 作为后缀名,一个 Effect 文件表一种材质类型,是材质系统中最重要的核心资源,它决定了一个可渲染对象的最终效果。 + +#### Technique + +我们把完成一个最终效果的方案称为一个渲染技术 Technique,一个技术可由一个或多个 Pass 来融合完成。 + +#### Pass + +一个 Pass 就是一次 GPU 绘制,一般包括一次顶点着色器和一次片元着色器,在 Creator 里 Pass 有很多可选配置参数。 + +### 1.2 Material 材质实例面板 + +Material 材质实例是所有开发者接触到的最直观的材质编辑窗口,所有实际材质实例的配置都是通过它完成的。 + +Cocos Creator 3.0 的 Material 实例面板如下: + +![effect](material3.png) + +Cocos Creator 2.x 的 Material 实例面板如下: + +![effect](material10.jpg) + +由上面两图可以看出 v3.0 的实例面板相比 v2.4 的会复杂不少,一方面是由于材质配置复杂度有所增加,另一方面也是因为面板功能有所增强。 + +#### 材质面板的可配置项主要分为五种类型 + +1. Effect 资源:即 Cocos Shader 文件,下拉框会列出当前项目中所有的 Cocos Shader(*.effect) 资源文件,开发者可以选择当前材质使用的 Cocos Shader。切换后其他属性也会同步更新。 +2. Technique 渲染技术选择:下拉框会列出当前使用的 Effect 资源中所有的 Technique,资源中可能会存在多个 Technique,每个 Technique 适用于不同的情况,比如效果差一点但是性能更好的 Technique 更适合用于手机平台。 +3. 宏选项:通过这些宏来控制某些代码是否可以被启用(对应逐个 Pass)。 +4. 属性列表(根据宏定义动态开放),并且使用不同的输入框适应不同类型的属性。编辑器的可编辑属性一般是 shader 中的某个 uniform 的映射,从 v3.0 开始也可以指定某个分量的映射(在 CCEffect 中使用 target 参数) +5. 在 v3.0 还新增了 PipelineStates 选项,主要用于定义材质依赖的管线状态,比如 DepthStencilState、BlendState、CullMode 等。 + +### 1.3 编辑器体验 + +v2.x 与 v3.0 在材质系统的编辑器体验上也有一定的区别。 + +v3.0 在 **层级管理器** 中选中包含模型和材质的节点后,**属性检查器** 面板会显示各组件属性以及详细的 Material 配置面板: + +![effect](material-v3.png) + +而 v2.x 在 **层级管理器** 中选中包含模型和材质的节点后,**属性检查器** 面板只会显示各组件属性,并不会直接显示详细的 Material 配置面板: + +![effect](material-v2x.png) + +而是需要跳转到 **资源管理器** 面板选中 Material 资源才可以在 **属性检查器** 中编辑: + +![effect](material-panel-v2x.png) + +## 2. Cocos Shader 资源 + +Cocos Shader 文件后缀名为 *.effect。 + +这个章节主要介绍资源在 v2.x 和 v3.0 的共性和差异。 + +### 2.1 Cocos Shader 格式和内容 + +在 v2.x 和 v3.0 中,Cocos Shader 的 effect 文件均采用 YAML1.2 标准的语法和解析器,两个版本之间的差别不大。按照 [语法格式](yaml-101.md) 编写,可以定义以下信息: + +- Technique 渲染技术列表 +- 每个 Technique 的 pass 列表 +- 每个 Pass 中所暴露给编辑器可编辑的属性列表(包含编辑器内的数据类型指定、分量映射关系等) +- 每个 Pass 的着色器程序,包含顶点和片元着色器程序 + +在语法细节方面,诸如 Property 声明和宏定义的方式都是一致的: + +![effect](material4.jpg) + +![effect](material4.1.jpg) + +![effect](material5.jpg) + +### 2.2 预设材质类型 + +v2.x 和 v3.0 在预设值材质方面有比较大的区别。 + +- v2.x 的预设材质包括 2D Sprite、经典的 blinn-phong 光照材质、无光照 unlit 材质、默认的卡通 toon 材质、粒子材质等。 + +- v3.0 的预设材质则是基于物理的渲染体系,包含基于物理光照的 standard PBR 材质、Skybox、toon 卡通渲染材质、3D 粒子材质(CPU & GPU)、粒子拖尾材质、传统的 2D Sprite 材质等。 + +v3.0 默认的 standard 材质支持标准的 Physically Based Rendering (PBR) 流程,其中包含很多提升材质质量和真实感的贴图信息,比如漫反射贴图、法线贴图、金属度(metallic)、粗糙度(roughness)、环境光遮蔽(occlusion)等等。整套算法基于标准的 BRDF 光照模型,这些是 v2.x 所不具备的,整体画面表现上 v3.0 也比 v2.x 要高一个档次。 + +### 2.3 Cocos Shader 文件格式差异 + +尽管 Cocos Shader 的语法规则在 v2.x 和 v3.0 基本是一致的,但很多内置的头文件、变量名以及函数名还是有区别的。 + +以获取主方向光源的方向为例,在 v3.0 里是 `cc_mainLitDir`,同时要包含头文件 `cc-global`。而在 v2.x 中想要获取光源方向要用到 `cc_lightDirection[i]` 这样一个数组,同时要包含头文件 `cc-lights`。具体的差异可参考下方第三点 **API 升级指南** 部分的介绍。 + +一些默认的着色器函数是 v3.0 独有的,例如 `CCStandardShading`、`CCToonShading` 等,这些在 v2.x 上是没有的。同样可以参考下方第三点 **API 升级指南** 部分的介绍。 + +关于 uniform 声明方面,v3.0 强制使用 UBO 来组织,内存布局方面强制最小单位为 vec4,不再支持单独声明 float 或 vec3 等类型的 uniform。 + +在头文件方面,v3.0 的编辑器内置头文件资源就在 **Internal DB** 的 `assets/chunks` 目录下。引用时可以不加目录直接引用,主要包括一些常用的 **工具函数** 和 **标准光照模型** 等。而 v2.x 的头文件是内置在编辑器中,无法直观地了解具体有哪些。 + +### 2.4 新增 Pass 选项 + +v3.0 新增了一些新的 Pass 选项: + +- `PropertyIndex`:指定这个 pass 的运行时 uniform 属性数据要和哪个 pass 保持一致,比如 forward add 等 pass 需要和 base pass 一致才能保证正确的渲染效果。一旦指定了此参数,材质面板上就不再会显示这个 pass 的任何属性。 + +- `embeddedMacros`:指定在这个 pass 的 shader 基础上额外定义的常量宏。在多个 pass 的 shader 中只有宏定义不同时可使用此参数来复用 shader 资源。 + +详细的 Pass 参数请参考 [参数列表](./pass-parameter-list.md)。 + +## 3. API 升级指南 + +### 3.1 内置 Uniform 差异列表 + +如果要在 shader 中使用内置变量,则需要包含对应头文件。下表是常用功能 uniform 汇总表,有很多功能 v2.x 与 v3.0 是一样的,而有一些是有区别的。 + +| v2.x Header & Name | v3.0 Header & Name| Type | 用途 |版本差异性 | +| :------ | :------ | :----- | :------ |:------ | +| `cc-local.chunk` & `cc_matWorld` | `cc-local.chunk` & `cc_matWorld` | mat4 | 模型空间转世界空间矩阵 |无差异 | +| `cc-local.chunk` & `cc_matWorldIT` |`cc-local.chunk` & `cc_matWorldIT` | mat4 | 模型空间转世界空间逆转置矩阵 |无差异 | +| `cc-global.chunk` & `cc_time` | `cc-global.chunk` & `cc_time` |vec4 | x:自开始以来的全球时间,以秒为单位
y:当前帧的增量时间
z:自开始以来的总帧数 |无差异 | +| `cc-global.chunk` & `cc_screenSize` | `cc-global.chunk` & `cc_screenSize` | vec4 | xy:屏幕尺寸
zw:屏幕尺寸倒数 |无差异 | +| `cc-global.chunk` & `cc_screenScale` | `cc-global.chunk` & `cc_screenScale` | vec4 | xy:屏幕比例
zw:逆屏幕比例 |无差异 | +| 无 | `cc-global.chunk` & `cc_nativeSize` | vec4 | xy:实际着色缓冲的尺寸
zw:实际着色缓冲的尺寸倒数 | v3.0 新功能,v2.x 没有 | +| `cc-global.chunk` & `cc_matView` | `cc-global.chunk` & `cc_matView` | mat4 | 视图矩阵 |无差异 | +| `cc-global.chunk` & `cc_matViewInv` | `cc-global.chunk` & `cc_matViewInv` | mat4 | 视图逆矩阵 |无差异 | +| `cc-global.chunk` & `cc_matProj` | `cc-global.chunk` & `cc_matProj` | mat4 | 投影矩阵 |无差异 | +| `cc-global.chunk` & `cc_matProjInv` | `cc-global.chunk` & `cc_matProjInv` | mat4 | 投影逆矩阵 |无差异 | +| `cc-global.chunk` & `cc_matViewProj` | `cc-global.chunk` & `cc_matViewProj` | mat4 | 视图投影矩阵 |无差异 | +| `cc-global.chunk` & `cc_matViewProjInv` | `cc-global.chunk` & `cc_matViewProjInv` | mat4 | 视图投影逆矩阵 |无差异 | +| `cc-global.chunk` & `cc_cameraPos` | `cc-global.chunk` & `cc_cameraPos` | vec4 | xyz:相机位置 |无差异 | +| 无 | `cc-global.chunk` & `cc_exposure` | vec4 | x:相机曝光
y:相机曝光倒数
z:是否启用 HDR
w:HDR 转 LDR 缩放参数 |v3.0 新功能,v2.x 没有 | + +**另外,v2.x 与 v3.0 在光源和阴影方面差异很大,v3.0 相比 v2.x 有了很大的提升。下表列出了一些常用的功能 uniform。** + +| v2.x Header & Name| v3.0 Header & Name| Type | 用途 | 版本差异性 | +| :------ | :------ | :----- | :------ |:------ | +| `cc-lights.chunk` & `cc_lightDirection[CC_MAX_LIGHTS]` | `cc-global.chunk` & `cc_mainLitDir` | vec4 | 得到光源方向 | v2.x:单个模型在 shader 中执行一次绘制受多少盏灯光影响,默认最大值为 1.0。如果要获取位置信息,填写 0 即可。例如 `cc_lightDirection[0]`
v3.0:xyz:主方向光源方向 +| `cc-lights.chunk` & `cc_lightColor[CC_MAX_LIGHTS]` | `cc-global.chunk` & `cc_mainLitColor` | vec4 | 控制光的颜色强度 | v2.x:光的 exp,就是光的 pow 强度。
v3.0:xyz — 主方向光颜色;w — 主方向光强度 | +| `cc-lights.chunk` & `CC_CALC_LIGHTS` | `cc-global.chunk` & `cc_ambientSky` |v2.x:宏定义
v3.0:vec4| 控制天空颜色强度 | v2.x:这是一个宏,通过穿进去的 ambient 参数进行计算。而且还有函数重载,可以传入不同的参数。
v3.0:xyz — 天空颜色;w — 亮度 | +| 无 | `cc-global.chunk` & `cc_ambientGround` |vec4| xyz:地面反射光颜色 | v3.0 新功能,v2.x 没有 | +| 无 | `cc-environment.chunk` & `cc_environment` | samplerCube | xyz:IBL 环境贴图 | v3.0 新功能,v2.x 没有 | + +### 3.2 Shader 内建函数和变量 + +在 v3.0,如果需要对接引擎动态合批和 instancing 流程,需要包含 `cc-local-batch` 头文件,通过 `CCGetWorldMatrix` 工具函数获取世界矩阵。 + +#### v3.0 新增着色函数 + +- `CCStandardShading` + + 函数名 `CCStandardShading` 需要包含头文件 `shading-standard.chunk`,用于进行光照计算,构成 surface shader 流程。 + + ```c + #include + #include + void surf (out StandardSurface s) { + // fill in your data here + } + vec4 frag () { + StandardSurface s; surf(s); + vec4 color = CCStandardShading(s); + return CCFragOutput(color); + } + ``` + + 在此框架下可方便地实现自己的 surface 输入,或其他 shading 算法。 + + > **注意**:`CCFragOutput` 函数一般不需要自己实现,它只起到与渲染管线对接的作用。且对于含有光照计算的输出,因为计算结果已经在 HDR 范围,所以应该包含 `output-standard` 头文件而非 `output`。 + +- `CCToonShading` + + 函数名 `CCToonShading`,需要包含头文件 `shading-toon.chunk`,进行卡通渲染的光影计算。 + + ```c + #include + #include + void surf (out ToonSurface s) { + // fill in your data here + } + vec4 frag () { + ToonSurface s; surf(s); + vec4 color = CCToonShading(s); + return CCFragOutput(color); + } + ``` + +### 3.3 光影计算相关函数 + +v2.x 与 v3.0 的光影计算有很大不同,主要包括以下两部分内容。 + +#### 球面光 + +v2.x 中的 **点光源** 在 v3.0 调整为 **球面光**,有很多现成的功能,使用时需要加入头文件 `cc-forward-light.chunk`,常用功能 uniform 如下表所示: + +| Name | Type | Info | +|:------ | :------ | :----- | +| `cc_sphereLitPos[MAX_LIGHTS]` | vec4 |xyz:球面光位置 | +| `cc_sphereLitSizeRange[MAX_LIGHTS]` | vec4 | x:球光尺寸
y:球光范围 | +| `cc_sphereLitColor[MAX_LIGHTS]` | vec4 | xyz:球光颜色
w:球光强度 | + +更多详细信息请参考 [常用 shader 内置 Uniform](builtin-shader-uniforms.md)。 + +#### 聚光灯 + +v3.0 中的聚光灯有很多现成的功能,使用时需要加入头文件 `cc-forward-light.chunk`,常用功能 uniform 如下表所示: + +| Name | Type | Info | +| :----- | :----- | :----- | +| `cc_spotLitPos[MAX_LIGHTS]` | vec4 | xyz:聚光灯位置 | +| `cc_spotLitSizeRangeAngle[MAX_LIGHTS]` | vec4 | x:聚光灯尺寸
y:聚光灯范围
z:聚光灯角度 | +| `cc_spotLitDir[MAX_LIGHTS]` | vec4 | xyz:聚光灯方向 | +| `cc_spotLitColor[MAX_LIGHTS]` | vec4 | xyz:聚光灯颜色
w:聚光灯强度 | + +更多详细信息请参考 [常用 shader 内置 Uniform](builtin-shader-uniforms.md)。 + +### 3.4 阴影部分 + +v2.x 与 v3.0 的阴影计算区别很大,v2.0 加入了头文件 `shadow.chunk`,而 v3.0 加入的是头文件 `cc-shadow.chunk`。 + +**v2.0 头文件 `shadow.chunk` 的常用功能 uniform 和函数如下所示**: + +| Name | Type | Info | +| :----- | :----- | :---- | +| `cc_shadow_lightViewProjMatrix[CC_MAX_SHADOW_LIGHTS]` | mat4 | 在灯光坐标下绘制阴影贴图 | +| `cc_shadow_info[CC_MAX_SHADOW_LIGHTS]` | vec4 | 计算阴影偏移 | + +| Name(函数) | Type | Info | +| :------ | :----- | :----- | +| `getDepth` | float | 返回深度值 | +| `shadowSimple` | float | 阴影的硬采样会有锯齿问题 | + +**v3.0 头文件 `cc-shadow.chunk` 的常用功能 uniform 如下所示**: + +| Name | Type | Info | +| :------ | :----- | :----- | +| `cc_matLightPlaneProj` | mat4| 平面阴影的变换矩阵 | +| `cc_shadowColor` | vec4 | 阴影颜色 | + +#### ShadowPCF 软阴影 + +| 头文件 | 函数 | +| :------ | :----- | +| v2.x:`shadow.chunk`| `shadowPCF3X3`(**3 * 3** 采样)
`shadowPCF5X5`(**5 * 5** 采样)| +| v3.0:`cc-shadow-map-fs.chunk` | `CC_DIR_SHADOW_FACTOR`:直接修改内存中阴影颜色的数值 | diff --git a/versions/4.0/zh/material-system/effect-syntax.md b/versions/4.0/zh/material-system/effect-syntax.md new file mode 100644 index 0000000000..523fb7e35d --- /dev/null +++ b/versions/4.0/zh/material-system/effect-syntax.md @@ -0,0 +1,3 @@ +# 着色器语法 + +我们在 v3.4 对材质系统模块的文档进行了更系统化的完善和更新,着色器语法相关的内容请移步至 [最新版本](../shader/index.md)。 diff --git a/versions/4.0/zh/material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md b/versions/4.0/zh/material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md new file mode 100644 index 0000000000..d1705fbdb2 --- /dev/null +++ b/versions/4.0/zh/material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md @@ -0,0 +1,122 @@ +# 升级指南:Effect 从 v3.4.x 升级到 v3.5.0 + +## 宏标记和函数宏 + +宏标记和功能宏的效果语法已升级,为避免占用标准的 glsl 定义,项目中旧的 Cocos Shader 文件(\*.effect)将自动升级,但如果您使用的是没有配套 \*.meta 数据的外部 Cocos Shader 文件,或编写新的 Cocos Shader,则必须注意。 + +- 宏标记的新语法:`#pragma define meta` +- 函数宏的新语法:`#pragma define` + +有关详细信息,请参阅 [Cocos Shader 语法 - macro-tags](../shader/macros.md#macro-tags)。 + +## 模型级别的阴影偏移 + +在 v3.5 中我们支持对模型设置单独阴影偏移值,可以对简单或复杂曲面上的阴影效果进行详细控制。如果您有任何自定义的 effect 文件,您可能需要升级它们使阴影偏移值生效。 + +解决 **effect** 文件从v3.4.x,升级到 v3.5.0 后,shadowBias 不生效的问题。 + +> **注意**:如果禁用灯光的阴影贴图,或者没有在顶点着色器上计算 **CC_TRANSFER_SHADOW(pos)** 则 **忽略** 该材质升级。 + +### 升级说明 + +有 **四个元素** 要添加到效果文件中,如下所示: + +1. vs out varying 定义 + + ``` + #if CC_RECEIVE_SHADOW + out mediump vec2 v_shadowBias; + #endif + ``` + +2. vs shadow bias 获取 + + ``` + #if CC_RECEIVE_SHADOW + v_shadowBias = CCGetShadowBias(); + #endif + ``` + +3. fs in varying 定义 + + ``` + #if CC_RECEIVE_SHADOW + in mediump vec2 v_shadowBias; + #endif + ``` + +4. fs shadow bias 赋值 + + ``` + #if CC_RECEIVE_SHADOW + s.shadowBias = v_shadowBias; + #endif + ``` + +### 示例(代码片段) + +```c +// 顶点着色器 +CCProgram standard-vs %{ + // 头文件区域 + #include + ... + #include + + // vs 输出区域 + out vec3 v_xxx; + ... + + #if CC_RECEIVE_SHADOW + out mediump vec2 v_shadowBias; + #endif + + ... + out vec3 v_xxxx; + + // vs 执行区域 + void main () { + xxx; + ... + + #if CC_RECEIVE_SHADOW + v_shadowBias = CCGetShadowBias(); + #endif + + ... + xxxx; + } +}% + +// 片元着色器 +CCProgram standard-fs %{ + // 头文件区域 + #include + ... + #include + + // vs 输入区域 + in vec3 v_xxx; + ... + + #if CC_RECEIVE_SHADOW + in mediump vec2 v_shadowBias; + #endif + + ... + in vec3 v_xxxx; + + // ps 执行区域 + void surf (out StandardSurface s) { + xxx; + ... + + #if CC_RECEIVE_SHADOW + s.shadowBias = v_shadowBias; + #endif + + ... + xxxx; + } +}% +``` diff --git a/versions/4.0/zh/material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md b/versions/4.0/zh/material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md new file mode 100644 index 0000000000..5654bce021 --- /dev/null +++ b/versions/4.0/zh/material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md @@ -0,0 +1,80 @@ +# 升级指南:Effect 从 v3.5.x 升级到 v3.6.0 + +## Chunks 迁移 + +3.6.0将之前版本 chunks 文件夹中的零散文件分门别类的存放到子文件夹中,书写 chunk 的 #include 时请参考下面的表格: + +### 1、公共函数库 + +| 原文件 | 新路径 | +| -------------------- | -------------------------------- | +| common | common/common-define | +| texture-lod | common/texture/texture-lod | +| packing | common/data/packing | +| unpack | common/data/unpack | +| aces | common/color/aces | +| gamma | common/color/gamma | +| octahedron-transform | common/math/octahedron-transform | +| transform | common/math/transform | +| rect-area-light | common/lighting/rect-area-light | + +### 2、Uniform 定义 + +| 原文件 | 新路径 | +| ---------------- | --------------------------------- | +| cc-global | builtin/uniforms/cc-global | +| cc-local | builtin/uniforms/cc-local | +| cc-forward-light | builtin/uniforms/cc-forward-light | +| cc-environment | builtin/uniforms/cc-environment | +| cc-diffusemap | builtin/uniforms/cc-diffusemap | +| cc-shadow | builtin/uniforms/cc-shadow | +| cc-world-bound | builtin/uniforms/cc-world-bound | + +### 3、通用 Shader 主函数(仅限 legacy shader) + +| 原文件 | 新路径 | +| ---------- | -------------------------------- | +| outline-vs | legacy/main-functions/outline-vs | +| outline-fs | legacy/main-functions/outline-fs | +| general-vs | legacy/main-functions/general-vs | + +### 4、引擎功能模块及其他(仅限 legacy shader) + +| 原文件 | 新路径 | +| ------------------------- | -------------------------------- | +| cc-fog-base | legacy/fog-base | +| cc-shadow-map-base | legacy/shadow-map-base | +| morph | legacy/morph | +| cc-skinning | legacy/skinning | +| cc-local-batch | legacy/local-batch | +| lighting | legacy/lighting | +| lightingmap-fs | legacy/lightingmap-fs | +| cc-shadow-map-vs | legacy/shadow-map-vs | +| cc-shadow-map-fs | legacy/shadow-map-fs | +| cc-fog-vs | legacy/fog-vs | +| cc-fog-fs | legacy/fog-fs | +| lightingmap-vs | legacy/lightingmap-vs | +| decode | legacy/decode | +| decode-base | legacy/decode-base | +| decode-standard | legacy/decode-standard | +| input | legacy/input | +| input-standard | legacy/input-standard | +| output | legacy/output | +| output-standard | legacy/output-standard | +| shading-standard | legacy/shading-standard | +| shading-standard-base | legacy/shading-standard-base | +| shading-standard-additive | legacy/shading-standard-additive | +| shading-cluster-additive | legacy/shading-cluster-additive | +| shading-toon | legacy/shading-toon | +| standard-surface-entry | legacy/standard-surface-entry | + +### 5、仅供内部使用 + +| 原文件 | 新路径 | +| ----------------- | -------------------------------- | +| alpha-test | builtin/internal/alpha-test | +| cc-sprite-common | builtin/internal/sprite-common | +| cc-sprite-texture | builtin/internal/sprite-texture | +| embedded-alpha | builtin/internal/embedded-alpha | +| particle-common | builtin/internal/particle-common | + diff --git a/versions/4.0/zh/material-system/effect.png b/versions/4.0/zh/material-system/effect.png new file mode 100644 index 0000000000..9a9e72f402 Binary files /dev/null and b/versions/4.0/zh/material-system/effect.png differ diff --git a/versions/4.0/zh/material-system/img/add-material.png b/versions/4.0/zh/material-system/img/add-material.png new file mode 100644 index 0000000000..8ac156f4bd Binary files /dev/null and b/versions/4.0/zh/material-system/img/add-material.png differ diff --git a/versions/4.0/zh/material-system/img/builtin-material.png b/versions/4.0/zh/material-system/img/builtin-material.png new file mode 100644 index 0000000000..c37c64bc07 Binary files /dev/null and b/versions/4.0/zh/material-system/img/builtin-material.png differ diff --git a/versions/4.0/zh/material-system/img/def-material.png b/versions/4.0/zh/material-system/img/def-material.png new file mode 100644 index 0000000000..f74d87cdca Binary files /dev/null and b/versions/4.0/zh/material-system/img/def-material.png differ diff --git a/versions/4.0/zh/material-system/img/dump-material.png b/versions/4.0/zh/material-system/img/dump-material.png new file mode 100644 index 0000000000..95e8208a02 Binary files /dev/null and b/versions/4.0/zh/material-system/img/dump-material.png differ diff --git a/versions/4.0/zh/material-system/img/dump-result.png b/versions/4.0/zh/material-system/img/dump-result.png new file mode 100644 index 0000000000..327e58ec95 Binary files /dev/null and b/versions/4.0/zh/material-system/img/dump-result.png differ diff --git a/versions/4.0/zh/material-system/img/gen-material.png b/versions/4.0/zh/material-system/img/gen-material.png new file mode 100644 index 0000000000..35b84d6ab5 Binary files /dev/null and b/versions/4.0/zh/material-system/img/gen-material.png differ diff --git a/versions/4.0/zh/material-system/img/inspector.png b/versions/4.0/zh/material-system/img/inspector.png new file mode 100644 index 0000000000..5f3f6777c2 Binary files /dev/null and b/versions/4.0/zh/material-system/img/inspector.png differ diff --git a/versions/4.0/zh/material-system/img/locate.png b/versions/4.0/zh/material-system/img/locate.png new file mode 100644 index 0000000000..9328146a2f Binary files /dev/null and b/versions/4.0/zh/material-system/img/locate.png differ diff --git a/versions/4.0/zh/material-system/img/mat-inspector.png b/versions/4.0/zh/material-system/img/mat-inspector.png new file mode 100644 index 0000000000..53d3afa390 Binary files /dev/null and b/versions/4.0/zh/material-system/img/mat-inspector.png differ diff --git a/versions/4.0/zh/material-system/img/mat-show.png b/versions/4.0/zh/material-system/img/mat-show.png new file mode 100644 index 0000000000..c251d0d6f8 Binary files /dev/null and b/versions/4.0/zh/material-system/img/mat-show.png differ diff --git a/versions/4.0/zh/material-system/img/material.png b/versions/4.0/zh/material-system/img/material.png new file mode 100644 index 0000000000..10993f7481 Binary files /dev/null and b/versions/4.0/zh/material-system/img/material.png differ diff --git a/versions/4.0/zh/material-system/img/particle-material.png b/versions/4.0/zh/material-system/img/particle-material.png new file mode 100644 index 0000000000..3d39c5bba3 Binary files /dev/null and b/versions/4.0/zh/material-system/img/particle-material.png differ diff --git a/versions/4.0/zh/material-system/img/post-dump.png b/versions/4.0/zh/material-system/img/post-dump.png new file mode 100644 index 0000000000..14b7bc14cb Binary files /dev/null and b/versions/4.0/zh/material-system/img/post-dump.png differ diff --git a/versions/4.0/zh/material-system/img/preview-model-select.png b/versions/4.0/zh/material-system/img/preview-model-select.png new file mode 100644 index 0000000000..379e5b5b5f Binary files /dev/null and b/versions/4.0/zh/material-system/img/preview-model-select.png differ diff --git a/versions/4.0/zh/material-system/img/readonly-material.png b/versions/4.0/zh/material-system/img/readonly-material.png new file mode 100644 index 0000000000..e4edf0af44 Binary files /dev/null and b/versions/4.0/zh/material-system/img/readonly-material.png differ diff --git a/versions/4.0/zh/material-system/img/revert-material.png b/versions/4.0/zh/material-system/img/revert-material.png new file mode 100644 index 0000000000..0e245e1a05 Binary files /dev/null and b/versions/4.0/zh/material-system/img/revert-material.png differ diff --git a/versions/4.0/zh/material-system/img/save-material.png b/versions/4.0/zh/material-system/img/save-material.png new file mode 100644 index 0000000000..757ea1d565 Binary files /dev/null and b/versions/4.0/zh/material-system/img/save-material.png differ diff --git a/versions/4.0/zh/material-system/img/select-effect.png b/versions/4.0/zh/material-system/img/select-effect.png new file mode 100644 index 0000000000..9ab7319792 Binary files /dev/null and b/versions/4.0/zh/material-system/img/select-effect.png differ diff --git a/versions/4.0/zh/material-system/img/select.png b/versions/4.0/zh/material-system/img/select.png new file mode 100644 index 0000000000..975bc2b312 Binary files /dev/null and b/versions/4.0/zh/material-system/img/select.png differ diff --git a/versions/4.0/zh/material-system/img/ui-select.png b/versions/4.0/zh/material-system/img/ui-select.png new file mode 100644 index 0000000000..4013be7a8a Binary files /dev/null and b/versions/4.0/zh/material-system/img/ui-select.png differ diff --git a/versions/4.0/zh/material-system/material-panel-v2x.png b/versions/4.0/zh/material-system/material-panel-v2x.png new file mode 100644 index 0000000000..0d7dd7a7e8 Binary files /dev/null and b/versions/4.0/zh/material-system/material-panel-v2x.png differ diff --git a/versions/4.0/zh/material-system/material-script.md b/versions/4.0/zh/material-system/material-script.md new file mode 100644 index 0000000000..f747cff077 --- /dev/null +++ b/versions/4.0/zh/material-system/material-script.md @@ -0,0 +1,124 @@ +# 程序化使用材质 + +## 创建材质 + +材质(Material)资源可以看成是着色器资源(EffectAsset)在场景中的资源实例。 + +Creator 支持在 **资源管理器** 中手动 [创建材质资源](../asset/material.md),同时也支持通过 [IMaterialInfo](%__APIDOC__%/zh/interface/IMaterialInfo) 接口在脚本模块中程序化地创建材质。`IMaterialInfo` 的可配置参数包括: + +- `effectAsset`/`effectName`:effect 资源引用,指定使用哪个 EffectAsset 所描述的流程进行渲染。(`effectAsset` 和 `effectName` 二者必须选其一) +- `technique`:指定使用 EffectAsset 中的第几个 technique,默认为第 0 个。 +- `defines`:宏定义列表,指定开启哪些 [预处理宏定义](../shader/macros.md),默认全部关闭。 +- `states`:管线状态重载列表,指定对渲染管线状态(深度模板透明混合等)有哪些重载,默认与 effect 声明一致。 + +创建代码示例: + +```ts +const mat = new Material(); +mat.initialize({ + // 通过 effect 名指定材质使用的着色器资源 + effectName: 'pipeline/skybox', + defines: { + USE_RGBE_CUBEMAP: true + } +}); +``` + +## 使用材质 + +![设置材质](img/add-material.png) + +对任意渲染器组件,可以在脚本模块中进行程序化访问,代码示例如下: + +```ts +// 通过网格渲染器组件(MeshRenderer、SkinnedMeshRenderer、SkinnedMeshBatchRenderer)可访问 3D 物体的材质 +let renderable = this.getComponent(MeshRenderer); + +// 获取索引为 0 的材质 +let material = renderable.getMaterial(0) + +// 设置索引为 0 的材质 +renderable.setMaterial(mat, 0); + +let sprite = this.node.getComponent(Sprite) + +// 获取 2D 渲染器组件的自定义材质 +let customMaterial = sprite.customMaterial; + +// 设置 2D 渲染器组件的自定义材质 +sprite.customMaterial = mat; + +// 获取和设置粒子发射器的材质 +let particleSystem = this.getComponent(ParticleSystem); +const material = particleSystem.material; +particleSystem.material = material; + +// 设置和获取粒子拖尾材质 +const trailMaterial = particleSystem.renderer.trailMaterial; +particleSystem.renderer.trailMaterial = trailMaterial; +``` + +> **注意**: +> 1. 这里访问的是共享材质。 +> 2. 材质中存在共享材质和材质实例两种情况,共享材质无法和材质实例进行合批。 + +## 设置材质的属性 + +材质通过 `IMaterialInfo` 接口初始化后,只能通过 `Material.setProperty` 来设置材质的 `Uniform` 变量,代码示例如下: + +```ts +mat.setProperty("uniform name", uniformValue) +``` + +`Uniform` 对应了 `Shader` 内声明的由 `Uniform` 限定的变量。若要了解更多 `Uniform` 的信息请参考: + +- [Cocos Shader 内置 Uniform](../shader/uniform.md) +- [GLSL 存储限定符](../shader/glsl.md#存储限定符) + +若需频繁设置 `Uniform` 的值,请使用 `Pass.setUniform` 来获得更好的性能。 + +## 共享材质 & 材质实例 + +在渲染器组件中,材质会以 **共享材质** 和 **材质实例** 两种情况存在。 + +- **共享材质** + + 共享材质由多个渲染器组件共同使用,修改共享材质会影响所有使用它的渲染器组件。默认情况下,同一材质在多个渲染器组件之间是共享的。 + + 获取共享材质的代码示例如下: + + ```ts + // 获取渲染器组件 + let renderableComponent = this.node.getComponent(MeshRenderer) as RenderableComponent + // 获取共享材质数组中索引为 0 的元素 + let sharedMaterial = renderableComponent.sharedMaterial + // 获取共享材质的数组 + let sharedMaterials = renderableComponent.sharedMaterials + // 获取共享材质数组中索引为 0 的元素 + let sharedMaterial = renderableComponent.getMaterial(0) + ``` + +- **材质实例** + + 材质实例由单个渲染器组件单独使用,修改材质实例仅影响使用它的渲染器组件。材质默认为共享材质,当修改共享材质时,引擎会根据材质创建材质实例,例如: + + - 当调用 `RenderableComponent.getMaterialInstance` 或 `RenderableComponent.material` 的 `getter` 时,引擎会根据当前的材质创建材质实例 + - 当调用 `RenderableComponent.material` 的 `setter` 时,引擎会根据传入的材质创建材质实例 + + 代码示例如下: + + ```ts + // 获取渲染器组件 + let renderableComponent = this.node.getComponent(MeshRenderer) as RenderableComponent + // 获取材质实例的数组,若没有则根据当前材质数组进行创建 + let materialInstances = renderableComponent.materials + // 获取材质实例数组中索引为 0 的元素,若没有则根据当前材质创建 + let materialInstance = renderableComponent.material + // 获取材质实例,若没有则根据当前材质创建 + let materialInstance = renderableComponent.getMaterialInstance(materialIndex); + ``` + +### FAQ + +**Q**:修改了材质的属性后,DrawCall 增加了?
+**A**:可能是因为使用了渲染器组件的 `getMaterialInstance` 或者 `RenderableComponent.material` 的 `getter` 方法,导致新的材质实例生成,影响了合批流程。 diff --git a/versions/4.0/zh/material-system/material-structure.md b/versions/4.0/zh/material-system/material-structure.md new file mode 100644 index 0000000000..58a8cf83f9 --- /dev/null +++ b/versions/4.0/zh/material-system/material-structure.md @@ -0,0 +1,11 @@ +# 材质系统类图 + +材质系统控制着每个模型最终的着色流程与顺序,在引擎内相关类间结构如下: + +[![Assets](img/material.png "Click to view diagram source")](material.dot) + +上述图中的 **Material**([材质](../asset/material.md))和 **EffectAsset**([着色器资源](../shader/index.md))都属于资源。 + +- **Material** 负责 EffectAsset 声明的 `Uniform`、宏数据存储以及 Shader 使用和管理,这些信息都会以材质资源的可视化属性的形式展示在 **属性检查器** 面板中。Material 通常是被渲染器组件使用,所有继承自 RenderableComponent 的组件都是渲染器组件,例如 MeshRenderer、Sprite 等。更多内容请参考 [材质资源](../asset/material.md)。 + +- **EffectAsset** 负责提供属性、宏、Shader 列表定义。每个 EffectAsset 最终都会被编译成引擎内使用的格式,引擎再根据格式进行解析和应用。所有解析后的 EffectAsset 信息都会被注册到引擎内的 ProgramLib 库里,方便用户直接通过代码获取实际引擎所使用的 EffectAsset 资源。更多内容请参考 [着色器](../shader/index.md)。 diff --git a/versions/4.0/zh/material-system/material-v2x.png b/versions/4.0/zh/material-system/material-v2x.png new file mode 100644 index 0000000000..7b44fdd8be Binary files /dev/null and b/versions/4.0/zh/material-system/material-v2x.png differ diff --git a/versions/4.0/zh/material-system/material-v3.png b/versions/4.0/zh/material-system/material-v3.png new file mode 100644 index 0000000000..37abfacdbe Binary files /dev/null and b/versions/4.0/zh/material-system/material-v3.png differ diff --git a/versions/4.0/zh/material-system/material.dot b/versions/4.0/zh/material-system/material.dot new file mode 100644 index 0000000000..e3297fb808 --- /dev/null +++ b/versions/4.0/zh/material-system/material.dot @@ -0,0 +1,31 @@ +digraph G { + layout=dot splines=true compound=true overlap=false fontname="Noto Sans CJK SC" + node [shape=Mrecord fontname="Source Code Pro"] + edge [fontname="Noto Sans CJK SC"] + + subgraph cluster_framework { + style=invis + ast [label="aaaaa"] + cmp [label="Component"] + lib [label="ProgramLib | { shader instances | shader templates }"] + } + + mat [label="Material | { defines | properties | effect}"] + + subgraph cluster_effect { + style=dotted label="auto-generated from *.effect file" labelloc=b + shd [label="ShaderInfo | { name | glsl3 | glsl1 | defines | blocks | samplers | dependencies }"] + efx [label="EffectAsset | { techniques | shader list }"] + } + + ast -> mat [dir=back arrowtail=empty] + ast -> efx [dir=back arrowtail=empty] + lib:temp -> efx:shd [dir=back arrowtail=vee style=dashed label=" register to"] + efx:shd -> shd [dir=back arrowtail=ediamond] + mat:efx -> efx [dir=back arrowtail=ediamond] + + rnd [label=" Renderable | materials"] + + cmp -> rnd:hd [dir=back arrowtail=empty] + rnd:mats -> mat [dir=back arrowtail=ediamond] +} diff --git a/versions/4.0/zh/material-system/material.png b/versions/4.0/zh/material-system/material.png new file mode 100644 index 0000000000..10993f7481 Binary files /dev/null and b/versions/4.0/zh/material-system/material.png differ diff --git a/versions/4.0/zh/material-system/material10.jpg b/versions/4.0/zh/material-system/material10.jpg new file mode 100644 index 0000000000..c481c4628b Binary files /dev/null and b/versions/4.0/zh/material-system/material10.jpg differ diff --git a/versions/4.0/zh/material-system/material2.png b/versions/4.0/zh/material-system/material2.png new file mode 100644 index 0000000000..2dfd9d9ee1 Binary files /dev/null and b/versions/4.0/zh/material-system/material2.png differ diff --git a/versions/4.0/zh/material-system/material3.png b/versions/4.0/zh/material-system/material3.png new file mode 100644 index 0000000000..c6089edc81 Binary files /dev/null and b/versions/4.0/zh/material-system/material3.png differ diff --git a/versions/4.0/zh/material-system/material4.1.jpg b/versions/4.0/zh/material-system/material4.1.jpg new file mode 100644 index 0000000000..9e5b4d2d33 Binary files /dev/null and b/versions/4.0/zh/material-system/material4.1.jpg differ diff --git a/versions/4.0/zh/material-system/material4.jpg b/versions/4.0/zh/material-system/material4.jpg new file mode 100644 index 0000000000..02e7ba4089 Binary files /dev/null and b/versions/4.0/zh/material-system/material4.jpg differ diff --git a/versions/4.0/zh/material-system/material5.jpg b/versions/4.0/zh/material-system/material5.jpg new file mode 100644 index 0000000000..1264faf293 Binary files /dev/null and b/versions/4.0/zh/material-system/material5.jpg differ diff --git a/versions/4.0/zh/material-system/material8.jpg b/versions/4.0/zh/material-system/material8.jpg new file mode 100644 index 0000000000..c40362842e Binary files /dev/null and b/versions/4.0/zh/material-system/material8.jpg differ diff --git a/versions/4.0/zh/material-system/material9.jpg b/versions/4.0/zh/material-system/material9.jpg new file mode 100644 index 0000000000..4c81d019eb Binary files /dev/null and b/versions/4.0/zh/material-system/material9.jpg differ diff --git a/versions/4.0/zh/material-system/overview.md b/versions/4.0/zh/material-system/overview.md new file mode 100644 index 0000000000..cc9c549a31 --- /dev/null +++ b/versions/4.0/zh/material-system/overview.md @@ -0,0 +1,21 @@ +# 材质系统总览 + +![mat-inspector](img/mat-show.png) + +在真实世界中,所有的物体都会与光交互,根据物体表面外观不同,在光照下所表现出来的效果也不同。 + +Cocos Creator 通过 **材质** 来描述物体外观,例如一个小球是玻璃球还是塑料球,一个箱子是木头箱子还是铁皮箱。它们在光照情况下所呈现出来的明暗、光点、光反射、光散射等效果,都是通过 [着色器](../shader/index.md) 来实现的。而材质则是着色器的数据集(包括纹理贴图、光照算法等),方便进行可视化调整。 + +材质系统主要包含以下内容: + +- [材质资源](../asset/material.md) +- [程序化使用材质](material-script.md) +- [内置材质](builtin-material.md) +- [材质系统类图](material-structure.md) +- [Cocos Creator 3.1 材质升级指南](Material-upgrade-documentation-for-v3.0-to-v3.1.md) +- [Cocos Creator 3.5 材质升级指南](effect-upgrade-documentation-for-v3.4.2-to-v3.5.md) +- [Cocos Creator 3.6 材质升级指南](effect-upgrade-documentation-for-v3.5-to-v3.6.md) + +## 范例 + +Creator 提供了材质相关的 **material** 范例([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/material) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/material)),用户可根据需要参考使用。 diff --git a/versions/4.0/zh/material-system/standard-material-graph.png b/versions/4.0/zh/material-system/standard-material-graph.png new file mode 100644 index 0000000000..1e4afdd8f1 Binary files /dev/null and b/versions/4.0/zh/material-system/standard-material-graph.png differ diff --git a/versions/4.0/zh/module-map/animation.png b/versions/4.0/zh/module-map/animation.png new file mode 100644 index 0000000000..d829c82ba8 Binary files /dev/null and b/versions/4.0/zh/module-map/animation.png differ diff --git a/versions/4.0/zh/module-map/asset.png b/versions/4.0/zh/module-map/asset.png new file mode 100644 index 0000000000..9982559ad5 Binary files /dev/null and b/versions/4.0/zh/module-map/asset.png differ diff --git a/versions/4.0/zh/module-map/audio.png b/versions/4.0/zh/module-map/audio.png new file mode 100644 index 0000000000..89315008f0 Binary files /dev/null and b/versions/4.0/zh/module-map/audio.png differ diff --git a/versions/4.0/zh/module-map/component.png b/versions/4.0/zh/module-map/component.png new file mode 100644 index 0000000000..239ce54df2 Binary files /dev/null and b/versions/4.0/zh/module-map/component.png differ diff --git a/versions/4.0/zh/module-map/effects/index.md b/versions/4.0/zh/module-map/effects/index.md new file mode 100644 index 0000000000..2a0979f896 --- /dev/null +++ b/versions/4.0/zh/module-map/effects/index.md @@ -0,0 +1,8 @@ +# 特效组件 + +特效组件包括 **广告牌** 和 **线段组件**,可在 **属性检查器** 中点击 **添加组件 -> Effects** 进行添加。 + +具体的说明及使用请参考: + +- [广告牌](../../particle-system/billboard-component.md) +- [线段组件](../../particle-system/line-component.md) diff --git a/versions/4.0/zh/module-map/graphics.md b/versions/4.0/zh/module-map/graphics.md new file mode 100644 index 0000000000..767873066f --- /dev/null +++ b/versions/4.0/zh/module-map/graphics.md @@ -0,0 +1,19 @@ +# 图形渲染 + +Cocos Creator 提供了以下图形功能,用以丰富图形以及加强图形画面的真实性等: + +- [渲染管线](../render-pipeline/overview.md) +- [相机](../editor/components/camera-component.md) +- [光照和阴影](../concepts/scene/light.md) +- [网格](mesh/index.md) +- [纹理](texture/index.md) +- [材质](../material-system/overview.md) +- [着色器](../shader/index.md) +- [2D 渲染排序](../engine/rendering/sorting-2d.md) +- [3D 渲染排序](../engine/rendering/sorting.md) +- [粒子](../particle-system/overview.md) +- [特效](effects/index.md) +- [天空盒](../concepts/scene/skybox.md) +- [全局雾](../concepts/scene/fog.md) +- [几何体渲染器](../geometry-renderer/index.md) +- [原生调试渲染器](../debug-renderer/index.md) \ No newline at end of file diff --git a/versions/4.0/zh/module-map/graphics.png b/versions/4.0/zh/module-map/graphics.png new file mode 100644 index 0000000000..10edf1c614 Binary files /dev/null and b/versions/4.0/zh/module-map/graphics.png differ diff --git a/versions/4.0/zh/module-map/light.png b/versions/4.0/zh/module-map/light.png new file mode 100644 index 0000000000..4f8eadde07 Binary files /dev/null and b/versions/4.0/zh/module-map/light.png differ diff --git a/versions/4.0/zh/module-map/material.png b/versions/4.0/zh/module-map/material.png new file mode 100644 index 0000000000..908dbe9eb0 Binary files /dev/null and b/versions/4.0/zh/module-map/material.png differ diff --git a/versions/4.0/zh/module-map/mesh/img/batched-skinning-model-component.png b/versions/4.0/zh/module-map/mesh/img/batched-skinning-model-component.png new file mode 100644 index 0000000000..b2c6156ba4 Binary files /dev/null and b/versions/4.0/zh/module-map/mesh/img/batched-skinning-model-component.png differ diff --git a/versions/4.0/zh/module-map/mesh/img/skinned-mesh-renderer.png b/versions/4.0/zh/module-map/mesh/img/skinned-mesh-renderer.png new file mode 100644 index 0000000000..154488ca0c Binary files /dev/null and b/versions/4.0/zh/module-map/mesh/img/skinned-mesh-renderer.png differ diff --git a/versions/4.0/zh/module-map/mesh/index.md b/versions/4.0/zh/module-map/mesh/index.md new file mode 100644 index 0000000000..ebaafb3e05 --- /dev/null +++ b/versions/4.0/zh/module-map/mesh/index.md @@ -0,0 +1,9 @@ +# 网格(Meshes) + +**网格** 一般用于绘制 3D 图像。Creator 提供了以下网格渲染器组件来渲染基础网格、蒙皮网格等,从而将模型绘制显示出来: + +- [MeshRenderer](../../engine/renderable/model-component.md):网格渲染器组件,用于渲染基础的模型网格。 +- [SkinnedMeshRenderer](./skinnedMeshRenderer.md):蒙皮网格渲染器组件,用于渲染蒙皮模型网格。 +- [SkinnedMeshBatchRenderer](./skinnedMeshBatchRenderer.md):批量蒙皮网格渲染器组件,用于将同一个骨骼动画组件控制的所有子蒙皮模型合并渲染。 + +同时,模型若要应用于实际的物理碰撞中,实现类似凹凸不平的路面效果,可以使用网格碰撞组件,会根据模型形状生成碰撞网格。详情请参考 [使用网格碰撞](../../physics/physics-collider.md#%E5%AE%9E%E7%8E%B0%E9%B9%85%E8%BD%AF%E7%9F%B3)。 diff --git a/versions/4.0/zh/module-map/mesh/skinnedMeshBatchRenderer.md b/versions/4.0/zh/module-map/mesh/skinnedMeshBatchRenderer.md new file mode 100644 index 0000000000..6ecdf27e68 --- /dev/null +++ b/versions/4.0/zh/module-map/mesh/skinnedMeshBatchRenderer.md @@ -0,0 +1,27 @@ +# 批量蒙皮网格渲染器组件(SkinnedMeshBatchRenderer) + +批量蒙皮网格渲染器组件(SkinnedMeshBatchRenderer)用于将同一个骨骼动画组件控制的所有子蒙皮模型网格合并渲染。 + +![mesh batch](./img/batched-skinning-model-component.png) + +| 属性 | 功能 | +| :--- | :--- | +| **Operation** | 对属性的任何修改不会立即生效,需要点 **Cook** 按钮重新计算才能应用。 | +| **Materials** | 合批后使用的“母材质”需要使用自己定制的合批版 effect。 | +| **LightmapSettings** | 用于烘焙 Lightmap,详情请参考 [光照贴图](../../concepts/scene/light/lightmap.md)。 | +| **ShadowCastingMode** | 指定当前模型是否会投射阴影,需要先在场景中 [开启阴影](../../concepts/scene/light/shadow.md#%E5%BC%80%E5%90%AF%E9%98%B4%E5%BD%B1)。 | +| **ReceiveShadow** | 指定当前模型是否会接收并显示其它物体产生的阴影效果,需要先在场景中 [开启阴影](../../concepts/scene/light/shadow.md#%E5%BC%80%E5%90%AF%E9%98%B4%E5%BD%B1)。该属性仅在阴影类型为 **ShadowMap** 时生效。 | +| **SkinningRoot** |骨骼蒙皮的根节点,一般为 SkeletalAnimation 组件所在节点。 | +| **AtlasSize** | 合图生成的最终图集的边长。 | +| **BatchableTextureNames** | 材质中真正参与合图的贴图属性,不参与的属性统一使用第一个 unit 的贴图。 | +| **Units** | 合批前的子蒙皮模型数据,是主要的数据来源。 | +| **Mesh** | 当前子模型的模型数据,一般直接来自 glTF 或 FBX 模型文件。 | +| **Skeleton** | 当前模型的骨骼数据,一般直接来自 glTF 或 FBX 模型文件。 | +| **Materials** | 当前子模型使用的“子材质”,使用的是非合批版的普通 effect,不同子模型使用的 effect 应保持一致。 | +| **Offset** | 当前子模型合图时的偏移,以图集左上角为原点,范围为 `[0, 1]`。比如图中数据代表子贴图与图集的左上角是重合的。 | +| **Size** | 当前子模型合图时所占的尺寸,范围为 `[0, 1]`。比如图中数据代表子贴图占整个图集的 1/2。 | +| **CopyFrom** | 可以通过拖入 **SkinningModelComponent** 自动复制目标属性(除了 `offset` 和 `size`),方便操作。 | + +更多合批相关的内容,详情请参考文档 [骨骼动画 — BatchedSkinnedMeshRenderer 组件](../../animation/skeletal-animation.md)。 + +批量蒙皮网格渲染器组件的相关接口,请参考 [SkinnedMeshBatchRenderer API](%__APIDOC__%/zh/class/SkinnedMeshBatchRenderer)。 diff --git a/versions/4.0/zh/module-map/mesh/skinnedMeshRenderer.md b/versions/4.0/zh/module-map/mesh/skinnedMeshRenderer.md new file mode 100644 index 0000000000..d5f01febf8 --- /dev/null +++ b/versions/4.0/zh/module-map/mesh/skinnedMeshRenderer.md @@ -0,0 +1,26 @@ +# 蒙皮网格渲染器组件(SkinnedMeshRenderer) + +蒙皮网格渲染器组件(SkinnedMeshRenderer)主要用于渲染蒙皮模型网格。 + +[导入模型资源](../../asset/model/mesh.md) 后,若模型网格中带有蒙皮信息,在使用模型时,SkinnedMeshRenderer 组件便会自动添加到模型节点上。 + +![SkinnedMeshRenderer](./img/skinned-mesh-renderer.png) + +## 属性 + +| 属性 | 功能 | +| :--- | :--- | +| **Materials** | 网格资源允许使用多个材质资源,所有材质资源都存在 `materials` 数组中。
如果网格资源中有多个子网格,那么 Mesh Renderer 会从 `materials` 数组中获取对应的材质来渲染此子网格。 | +| **LightmapSettings** | 用于烘焙 Lightmap,详情请参考 [光照贴图](../../concepts/scene/light/lightmap.md)。 | +| **ShadowCastingMode** | 指定当前模型是否会投射阴影,需要先在场景中 [开启阴影](../../concepts/scene/light/shadow.md#%E5%BC%80%E5%90%AF%E9%98%B4%E5%BD%B1)。 | +| **ReceiveShadow** | 指定当前模型是否会接收并显示其它物体产生的阴影效果,需要先在场景中 [开启阴影](../../concepts/scene/light/shadow.md#%E5%BC%80%E5%90%AF%E9%98%B4%E5%BD%B1)。该属性仅在阴影类型为 **ShadowMap** 时生效。 | +| **Mesh** | 指定渲染所用的网格资源,网格渲染器组件中 [网格资源](../../engine/renderable/model-component.md#%E7%BD%91%E6%A0%BC%E8%B5%84%E6%BA%90) 部分的内容。 | +| **Skeleton** | 当前模型的骨骼数据,一般直接来自导入的 glTF 或 FBX 模型文件| +| **SkinningRoot** | 骨骼根节点的引用,对应控制该模型的动画组件所在节点 | + +蒙皮网格渲染器组件相关接口请参考 [SkinnedMeshRenderer API](%__APIDOC__%/zh/class/SkinnedMeshRenderer)。 + +蒙皮网格渲染器组件(SkinnedMeshRenderer)与普通的网格渲染组件(MeshRenderer)的区别在于: + +- MeshRenderer 组件渲染的是静态模型,由 3D 模型数据组成 +- SkinnedMeshRenderer 组件不但渲染模型还会渲染骨骼,因此除了 3D 模型数据外,还包括骨骼数据以及顶点权重等数据。
但如果 SkinnedMeshRenderer 上没有挂载任何骨骼数据,那么它和普通的 MeshRenderer 组件没有区别。 diff --git a/versions/4.0/zh/module-map/particle.png b/versions/4.0/zh/module-map/particle.png new file mode 100644 index 0000000000..65f350abd1 Binary files /dev/null and b/versions/4.0/zh/module-map/particle.png differ diff --git a/versions/4.0/zh/module-map/physics.png b/versions/4.0/zh/module-map/physics.png new file mode 100644 index 0000000000..ba59c5370d Binary files /dev/null and b/versions/4.0/zh/module-map/physics.png differ diff --git a/versions/4.0/zh/module-map/scene.png b/versions/4.0/zh/module-map/scene.png new file mode 100644 index 0000000000..100ffce3c6 Binary files /dev/null and b/versions/4.0/zh/module-map/scene.png differ diff --git a/versions/4.0/zh/module-map/script.png b/versions/4.0/zh/module-map/script.png new file mode 100644 index 0000000000..9f5c8d9138 Binary files /dev/null and b/versions/4.0/zh/module-map/script.png differ diff --git a/versions/4.0/zh/module-map/texture/index.md b/versions/4.0/zh/module-map/texture/index.md new file mode 100644 index 0000000000..e7b88293b6 --- /dev/null +++ b/versions/4.0/zh/module-map/texture/index.md @@ -0,0 +1,16 @@ +# 纹理(Textures) + +纹理是一张可显示的图像,或一段用于计算的中间数据,通过 UV 坐标映射到渲染物体表面,使之效果更为丰富精彩且真实。Creator 中纹理的应用包括以下几种: + +- 用于 2D UI 渲染,参考 [SpriteFrame](../../asset/sprite-frame.md)。 + +- 用于 3D 模型渲染,需要在材质中指定 [纹理贴图资源](../../asset/texture.md),才能将其渲染映射到网格表面。纹理贴图还支持在 [导入图像资源](../../asset/image.md) 时将其切换为 **立方体贴图** 或 **法线贴图**。 + +- 用于粒子系统,使粒子表现更丰富。与 3D 模型一样,纹理在粒子系统中的应用也依赖于材质。 + +- 用于地形渲染,参考 [地形系统](../../editor/terrain/index.md)。 + +## 更多内容 + +- [渲染纹理(RenderTexture)](../../asset/render-texture.md) +- [压缩纹理](../../asset/compress-texture.md) diff --git a/versions/4.0/zh/module-map/world01.jpg b/versions/4.0/zh/module-map/world01.jpg new file mode 100644 index 0000000000..3a7f31548e Binary files /dev/null and b/versions/4.0/zh/module-map/world01.jpg differ diff --git a/versions/4.0/zh/native/overview.md b/versions/4.0/zh/native/overview.md new file mode 100644 index 0000000000..d7242aada7 --- /dev/null +++ b/versions/4.0/zh/native/overview.md @@ -0,0 +1,26 @@ +# 原生开发 + +Cocos Creator 支持发布为移动端原生应用、移动端网页、移动端小游戏、桌面端原生应用、桌面端网页等。 + +本章将包含一些在原生端应用开发中可能会遇到的议题。 + +## 内容 + +- [原生平台二次开发指南](../advanced-topics/native-secondary-development.md) +- [Java 原生反射机制](../advanced-topics/java-reflection.md) +- [Objective-C 原生反射机制](../advanced-topics/oc-reflection.md) +- [ArkTS 原生反射机制](../advanced-topics/arkts-reflection.md) +- [JsbBridge JS 与 JAVA 通信](../advanced-topics/js-java-bridge.md) +- [JsbBridge JS 与 Objective-C 通信](../advanced-topics/js-oc-bridge.md) +- [JsbBridgeWrapper 基于原生反射机制的事件处理](../advanced-topics/jsb-bridge-wrapper.md) +- [JSB 2.0 使用指南](../advanced-topics/JSB2.0-learning.md) + - [JSB 手动绑定](../advanced-topics/jsb-manual-binding.md) + - [JSB 自动绑定](../advanced-topics/jsb-auto-binding.md) + - [Swig](../advanced-topics/jsb-swig.md) + - [Swig 示例](../advanced-topics/jsb/swig/tutorial/index.md) +- [CMake 使用简介](../advanced-topics/cmake-learning.md) +- [原生引擎内存泄漏检测系统](../advanced-topics/memory-leak-detector.md) +- [原生场景剔除](../advanced-topics/native-scene-culling.md) +- [原生性能剖析器](../advanced-topics/profiler.md) +- [原生插件](../advanced-topics/native-plugins/brief.md) + - [原生插件创建范例](../advanced-topics/native-plugins/tutorial.md) \ No newline at end of file diff --git a/versions/4.0/zh/particle-system/2d-particle/2d-particle.md b/versions/4.0/zh/particle-system/2d-particle/2d-particle.md new file mode 100644 index 0000000000..8610f9a5b4 --- /dev/null +++ b/versions/4.0/zh/particle-system/2d-particle/2d-particle.md @@ -0,0 +1,46 @@ +# ParticleSystem2D 组件参考 + +2D 粒子组件(ParticleSystem2D)用于读取粒子资源数据,并对其进行一系列例如播放、暂停、销毁等操作。粒子资源支持 `plist` 文件和图片,这两个资源建议放在同一个文件夹下。 + +![ParticleSystem2D](./2d-particle.png) + +点击 **属性检查器** 下方的 **添加组件** 按钮,然后从 **Effects** 中选择 **ParticleSystem2D**,即可添加 ParticleSystem2D 组件到节点上。 + +ParticleSystem2D 的脚本接口请参考 [ParticleSystem API](%__APIDOC__%/zh/class/ParticleSystem2D)。 + +## ParticleSystem2D 属性 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| CustomMaterial | 自定义材质,使用方法可参考 [自定义材质](../../ui-system/components/engine/ui-material.md) +| Color | 粒子颜色 +| Preview | 在编辑器模式下预览粒子,启用后选中粒子时,粒子将在 **场景编辑器** 中自动播放 +| PlayOnLoad | 若勾选该项,则运行时会自动发射粒子 +| AutoRemoveOnFinish | 粒子播放完毕后自动销毁所在的节点 +| File | Plist 格式的粒子配置文件 +| Custom | 自定义粒子属性。开启该属性后可自定义以下部分的粒子属性 +| SpriteFrame | 自定义的粒子贴图 +| Duration | 粒子系统运行时间,单位为 **秒**,-1 表示持续发射 +| EmissionRate | 每秒发射的粒子数目 +| Life | 粒子的运行时间以及变化范围 +| TotalParticle | 粒子最大数量 +| StartColor | 粒子初始颜色 +| EndColor | 粒子结束颜色 +| Angle | 粒子角度及变化范围 +| StartSize | 粒子的初始大小及变化范围 +| EndSize | 粒子结束时的大小及变化范围 +| StartSpin | 粒子开始自旋角度及变化范围 +| EndSpin | 粒子结束自旋角度及变化范围 +| PosVar | 发射器位置的变化范围(横向和纵向) +| PositionType | 粒子位置类型,包括 **FREE**、**RELATIVE**、**GROUPED** 三种。详情可参考 [PositionType API](%__APIDOC__%/zh/class/ParticleSystem2D?id=PositionType) +| EmitterMode | 发射器类型,包括 **GRAVITY**、**RADIUS** 两种。详情可参考 [EmitterMode API](%__APIDOC__%/zh/class/ParticleSystem2D?id=EmitterMode) +| Gravity | 重力。仅在 Emitter Mode 设为 **GRAVITY** 时生效 +| Speed | 速度及变化范围。仅在 Emitter Mode 设为 **GRAVITY** 时生效 +| TangentialAccel | 每个粒子的切向加速度及变化范围,即垂直于重力方向的加速度。仅在 Emitter Mode 设为 **GRAVITY** 时生效 +| RadialAccel | 粒子径向加速度及变化范围,即平行于重力方向的加速度。仅在 Emitter Mode 设为 **GRAVITY** 时生效 +| RotationIsDir | 每个粒子的旋转是否等于其方向。仅在 Emitter Mode 设为 **GRAVITY** 时生效 +| StartRadius | 初始半径及变化范围,表示粒子发射时相对发射器的距离。仅在 Emitter Mode 设为 **RADIUS** 时生效 +| EndRadius | 结束半径及变化范围。仅在 Emitter Mode 设为 **RADIUS** 时生效 +| RotatePerS | 粒子每秒围绕起始点的旋转角度及变化范围。仅在 Emitter Mode 设为 **RADIUS** 时生效 + +更多具体的使用方式可参考官方范例 **ui/25.particle**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/25.particle) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/25.particle))。 diff --git a/versions/4.0/zh/particle-system/2d-particle/2d-particle.png b/versions/4.0/zh/particle-system/2d-particle/2d-particle.png new file mode 100644 index 0000000000..dde6c50d65 Binary files /dev/null and b/versions/4.0/zh/particle-system/2d-particle/2d-particle.png differ diff --git a/versions/4.0/zh/particle-system/billboard-component.md b/versions/4.0/zh/particle-system/billboard-component.md new file mode 100644 index 0000000000..deb52af5b2 --- /dev/null +++ b/versions/4.0/zh/particle-system/billboard-component.md @@ -0,0 +1,14 @@ +# Billboard 组件 + +Billboard 组件用于渲染一个始终面向摄像机的方块。 + +![billboard](particle-system/billboard.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Texture** | billboard 显示的贴图。 +| **Height** | billboard 的高度。 +| **Width** | billboard 的宽度。 +| **Rotation** | billboard 绕中心点旋转的角度。 + +Billboard 组件接口请参考 [Billboard API](%__APIDOC__%/zh/class/Billboard)。 diff --git a/versions/4.0/zh/particle-system/color-module.md b/versions/4.0/zh/particle-system/color-module.md new file mode 100644 index 0000000000..c03b96565d --- /dev/null +++ b/versions/4.0/zh/particle-system/color-module.md @@ -0,0 +1,13 @@ +# 颜色模块(ColorOvertimeModule) + +颜色模块用于设置粒子颜色及粒子在生命周期内的颜色变化。 + +![color_module](module/color_overtime.gif) + +## 属性 + +![color_module](module/color_module.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Color** | 设置粒子颜色随时间变化发生渐变。点击右侧的 ![menu button](main-module/menu-button.png) 按钮,可选择使用不同的颜色变化模式,详情请参考 [渐变色编辑](./editor/gradient-editor.md)。 | diff --git a/versions/4.0/zh/particle-system/editor/curve-editor.md b/versions/4.0/zh/particle-system/editor/curve-editor.md new file mode 100644 index 0000000000..53472e1e9c --- /dev/null +++ b/versions/4.0/zh/particle-system/editor/curve-editor.md @@ -0,0 +1,101 @@ +# 粒子曲线编辑器 + +**粒子曲线编辑器** 支持设置粒子系统中部分属性随时间变化的曲线。属性默认都是以常量(Constant)的形式显示,点击属性框右侧的 ![menu-button](img/menu-button.png) 按钮,可以看到有以下几种编辑形式: + +![set-pro](curve-editor/set-pro.png) + +根据选择的编辑模式不同,属性框的显示也有一定的差异: + +- **Constant**:设置属性的常量值,该值在粒子整个生命周期内不会发生变化; + + ![constant](curve-editor/constant.png) + +- **Curve**:属性切换为曲线编辑形式,点击属性框即可打开曲线编辑器,编辑属性值随时间变化的曲线图; + + ![constant](curve-editor/curve.png) + +- **TwoCurves**:通过两条曲线设定属性值随时间变化时,在生命周期内某一点的上、下限,取随机值。 + + ![constant](curve-editor/two-curves.png) + +- **TwoConstant**:通过两个常量值设定属性值的范围,实际值会随着时间推移在范围内随机变化。 + + ![constant](curve-editor/two-constants.png) + +## 曲线编辑面板 + +属性切换为曲线编辑后(**Curve**/**TwoCurve** 模式),属性框会显示当前曲线的预览图: + +![set-pro](curve-editor/curve-property.png) + +> **注意**:若属性的取值范围包括负值,则属性框中的预览图显示为: +> +> ![set-pro](curve-editor/curve-property1.png) + +然后点击属性框即可打开曲线编辑器进行编辑: + +![curve editor](img/curve_editor.png) + +曲线编辑器面板的相关功能说明如下: + +- 当前进行曲线编辑的属性名称会显示在面板右上角。 + +- **横坐标** 表示粒子单位化的生命周期,由引擎定义,无法调整,一般为 `[0, 1]`。例如一个粒子的生命周期为 5s,则横坐标上的 0.5 代表 2.5s。 + +- **纵坐标** 表示当前编辑的属性的取值范围,可通过纵坐标上方的输入框调整纵坐标区间。 + +- 右上角齿轮图标按钮中的 **preWrapMode**/**postWrapMode** 用于设置粒子在生命周期范围内,曲线前后若有未定义部分时,粒子系统播放曲线的方式。详情请参考下文 **前后循环模式** 部分的内容。 + +- 右上角齿轮图标按钮中的 **Spacing** 用于设置关键帧间隔排列的间隔数(Spacing)。详情请参考下文 **间隔排列关键帧** 部分的内容。 + +- Creator 提供了一些预设的曲线数据模板,单击左侧 **Presets** 中某个模板便可直接将其应用到当前曲线中。同时也支持将设置好的曲线作为自定义曲线模板存储在 **Custom** 中,方便直接应用。 + + ![custom](img/custom.png) + +## 曲线编辑 + +在曲线编辑器窗口点击选中曲线,便可执行以下关键帧操作: + +- 在窗口右侧空白处点击右键,然后选择 **添加关键帧**,便会添加一个新的关键帧并连接曲线; +- 在曲线上点击右键即可添加关键帧; +- 单击选中关键帧后,可拖动关键帧改变其坐标; +- 点击关键帧左右两侧出现的线段拉杆,转动拉杆即可调整该关键帧所在的点的曲线斜率; +- 右键点击关键帧,然后选择 **删除关键帧**,即可删除。 + +更多通用功能请参考 [曲线编辑器](../../animation/curve-editor.md)。 + +### 前后循环模式 + +**preWrapMode** 和 **postWrapMode** 用于设置在粒子生命周期范围内,曲线前后有未定义部分时,粒子系统播放曲线的方式。 + +![wrap-mode](curve-editor/set-wrap-mode.png) + +例如下图中曲线只定义了横坐标为 `[0.2, 0.8]` 区间的部分,而 `[0, 0.2]` 和 `[0.8, 1]`部分曲线没有定义,便可分别通过 **preWrapMode** 和 **postWrapMode** 设置其循环模式,曲线编辑器便会根据循环模式和曲线定义部分决定未定义部分的曲线变化: + +![wrap-mode](curve-editor/wrap-mode.png) + +循环模式包括以下几种: + +- **Loop**:将整条曲线(包括定义部分和未定义部分)当做是无限连续循环的。 + + ![wrap-mode-loop](curve-editor/wrap-mode-loop.png) + +- **Ping Pong**:与 Loop 类似,但循环方式是以与曲线定义部分反向再正向(“乒乓”),如此往复的形式。 + + ![wrap-mode-PingPong](curve-editor/wrap-mode-PingPong.png) + +- **Clamp**:曲线前/后未定义部分会使用曲线定义部分第一帧/最后一帧的值。 + + ![wrap-mode-clamp](curve-editor/wrap-mode-clamp.png) + +> **注意**:若曲线前后没有未定义部分,则设置 **preWrapMode** 和 **postWrapMode** 无效。 + +### 间隔排列关键帧 + +点击右上方的齿轮按钮,可以设置关键帧间隔排列的间隔数: + +![spacing](img/spacing.png)。 + +框选多个关键帧,在框内点击鼠标右键,然后选择 **间隔排列关键帧** 即可: + +![spacingFrame](img/spacingFrame.png) diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/constant.png b/versions/4.0/zh/particle-system/editor/curve-editor/constant.png new file mode 100644 index 0000000000..37d6ff0656 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/constant.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/curve-property.png b/versions/4.0/zh/particle-system/editor/curve-editor/curve-property.png new file mode 100644 index 0000000000..9c61c01a9b Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/curve-property.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/curve-property1.png b/versions/4.0/zh/particle-system/editor/curve-editor/curve-property1.png new file mode 100644 index 0000000000..ebcd91f6aa Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/curve-property1.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/curve.png b/versions/4.0/zh/particle-system/editor/curve-editor/curve.png new file mode 100644 index 0000000000..b7b9c86ce2 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/curve.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/curve_editor.png b/versions/4.0/zh/particle-system/editor/curve-editor/curve_editor.png new file mode 100644 index 0000000000..777a2ab1e9 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/curve_editor.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/edit-curve.gif b/versions/4.0/zh/particle-system/editor/curve-editor/edit-curve.gif new file mode 100644 index 0000000000..80d0bc2030 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/edit-curve.gif differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/set-pro.png b/versions/4.0/zh/particle-system/editor/curve-editor/set-pro.png new file mode 100644 index 0000000000..c2d0434102 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/set-pro.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/set-wrap-mode.png b/versions/4.0/zh/particle-system/editor/curve-editor/set-wrap-mode.png new file mode 100644 index 0000000000..a4085c6bde Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/set-wrap-mode.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/two-constants.png b/versions/4.0/zh/particle-system/editor/curve-editor/two-constants.png new file mode 100644 index 0000000000..0cc203fcc4 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/two-constants.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/two-curves.png b/versions/4.0/zh/particle-system/editor/curve-editor/two-curves.png new file mode 100644 index 0000000000..b12e32d9f6 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/two-curves.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode-PingPong.png b/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode-PingPong.png new file mode 100644 index 0000000000..0584a9c617 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode-PingPong.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode-clamp.png b/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode-clamp.png new file mode 100644 index 0000000000..ceaa1ab022 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode-clamp.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode-loop.png b/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode-loop.png new file mode 100644 index 0000000000..cc2cd8b7c6 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode-loop.png differ diff --git a/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode.png b/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode.png new file mode 100644 index 0000000000..c267053adf Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/curve-editor/wrap-mode.png differ diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor.md b/versions/4.0/zh/particle-system/editor/gradient-editor.md new file mode 100644 index 0000000000..ffcd7a5582 --- /dev/null +++ b/versions/4.0/zh/particle-system/editor/gradient-editor.md @@ -0,0 +1,65 @@ +# 渐变色编辑器 + +**渐变色编辑器** 支持设置粒子系统中部分属性颜色随时间发生渐变。属性默认都是以固定颜色(Color)的形式显示,点击属性框右侧的 ![menu-button](img/menu-button.png) 按钮,可以看到有以下几种编辑模式: + +![set-color](gradient-editor/set-color.png) + +根据选择的编辑模式不同,属性框的显示也有一定的差异: + +- **Color**(默认):设定的颜色在属性整个生命周期内不会发生变化; + + ![color](gradient-editor/color.png) + +- **Gradient**:属性切换为渐变色编辑形式,点击属性框即可打开渐变色编辑器。属性的颜色变化由设置的颜色渐变图指定; + + ![gradient](gradient-editor/gradient.png) + +- **TwoColors**:设置的两个固定颜色定义了颜色的上限和下限,实际颜色会随着时间的推移在两个边界之间进行插值; + + ![two-colors](gradient-editor/two-colors.png) + +- **TwoGradients**:两个渐变图定义了属性颜色在生命周期内某一点的渐变上限和下限,当前颜色在两个边界之间进行插值; + + ![two-gradients](gradient-editor/two-gradients.png) + +- **RadomColor**:取 **Gradient** 模式下的渐变图中的随机颜色。 + + ![radom-color](gradient-editor/radom-color.png) + +然后点击属性框,即可打开相应的颜色编辑器。 + +接下来我们以 **Gradient** 模式为例,看一下渐变色编辑器具体的操作步骤。 + +## 渐变色编辑 + +选择 **Gradient** 模式后点击属性框,即可打开渐变色编辑器。初始界面如下: + +![gradient_editor](gradient-editor/gradient-editor.png) + +可将其大致分为上下两部分,上方的三条色带区域为颜色渐变图,下方区域为颜色选择器。 + +- 上方的色带用于插入 Alpha 关键帧;下方的色带用于插入 RGB 关键帧;中间的色带用于显示关键帧影响下的最终颜色,最终颜色的计算方式可在 **Mode** 选项中设置,详情请参考下文说明。 + +- 选中关键帧即可在下方的颜色选择器中设置 Alpha 或 RGB 值。设置时可在左侧直接选择颜色,也可以在右侧通过数值进行设置; + +- 拖动关键帧 **上下移动** 可以 **删除** 关键帧。 + +- 拖动关键帧 **左右移动** 可以 **调节关键帧位置**,也可以在 **Location** 中通过设置百分比数值进行调节。 + + ![gradient_editor](gradient-editor/edit-gradient.gif) + +- **Mode** 用于设置渐变色的计算方式,支持以下两种: + + - **Blend** 模式(默认):会按照当前时刻相邻的两个关键帧进行插值得到当前帧的颜色; + + - **Fixed** 模式:会直接使用当前时刻的前一个关键帧颜色,没有渐变。 + + 编辑器中默认使用 **Blend** 模式,可通过代码进行切换,代码示例如下: + + ```ts + // 切换为 Fixed 模式 + startColor.gradient.mode = Mode.Fixed; + + // 切换为 Blend 模式 + startColor.gradient.mode = Mode.Blend; + ``` diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor/color.png b/versions/4.0/zh/particle-system/editor/gradient-editor/color.png new file mode 100644 index 0000000000..639b1a2d2a Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/gradient-editor/color.png differ diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor/edit-gradient.gif b/versions/4.0/zh/particle-system/editor/gradient-editor/edit-gradient.gif new file mode 100644 index 0000000000..228f9bc8ae Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/gradient-editor/edit-gradient.gif differ diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor/gradient-editor.png b/versions/4.0/zh/particle-system/editor/gradient-editor/gradient-editor.png new file mode 100644 index 0000000000..122a04f240 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/gradient-editor/gradient-editor.png differ diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor/gradient.png b/versions/4.0/zh/particle-system/editor/gradient-editor/gradient.png new file mode 100644 index 0000000000..e112525a52 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/gradient-editor/gradient.png differ diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor/gradient_editor.png b/versions/4.0/zh/particle-system/editor/gradient-editor/gradient_editor.png new file mode 100644 index 0000000000..a024af24e8 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/gradient-editor/gradient_editor.png differ diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor/radom-color.png b/versions/4.0/zh/particle-system/editor/gradient-editor/radom-color.png new file mode 100644 index 0000000000..929bb6538a Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/gradient-editor/radom-color.png differ diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor/set-color.png b/versions/4.0/zh/particle-system/editor/gradient-editor/set-color.png new file mode 100644 index 0000000000..052e9eef20 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/gradient-editor/set-color.png differ diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor/two-colors.png b/versions/4.0/zh/particle-system/editor/gradient-editor/two-colors.png new file mode 100644 index 0000000000..4babe4f14d Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/gradient-editor/two-colors.png differ diff --git a/versions/4.0/zh/particle-system/editor/gradient-editor/two-gradients.png b/versions/4.0/zh/particle-system/editor/gradient-editor/two-gradients.png new file mode 100644 index 0000000000..33b5fe9489 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/gradient-editor/two-gradients.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/curve_editor.png b/versions/4.0/zh/particle-system/editor/img/curve_editor.png new file mode 100644 index 0000000000..e744907506 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/curve_editor.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/custom.png b/versions/4.0/zh/particle-system/editor/img/custom.png new file mode 100644 index 0000000000..e5fe956c31 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/custom.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/menu-button.png b/versions/4.0/zh/particle-system/editor/img/menu-button.png new file mode 100644 index 0000000000..e2d836084f Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/menu-button.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/menu.png b/versions/4.0/zh/particle-system/editor/img/menu.png new file mode 100644 index 0000000000..cbfb7f4c3e Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/menu.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/particle_dock.png b/versions/4.0/zh/particle-system/editor/img/particle_dock.png new file mode 100644 index 0000000000..b038f898f6 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/particle_dock.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/particle_panel.png b/versions/4.0/zh/particle-system/editor/img/particle_panel.png new file mode 100644 index 0000000000..84bf6128ca Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/particle_panel.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/particle_pause.png b/versions/4.0/zh/particle-system/editor/img/particle_pause.png new file mode 100644 index 0000000000..714a4e0260 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/particle_pause.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/particle_play.png b/versions/4.0/zh/particle-system/editor/img/particle_play.png new file mode 100644 index 0000000000..b37c8b571f Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/particle_play.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/particle_replay.png b/versions/4.0/zh/particle-system/editor/img/particle_replay.png new file mode 100644 index 0000000000..a97ab43642 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/particle_replay.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/particle_stop.png b/versions/4.0/zh/particle-system/editor/img/particle_stop.png new file mode 100644 index 0000000000..c189ccd9f7 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/particle_stop.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/spacing.png b/versions/4.0/zh/particle-system/editor/img/spacing.png new file mode 100644 index 0000000000..40513fa22a Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/spacing.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/spacingFrame.png b/versions/4.0/zh/particle-system/editor/img/spacingFrame.png new file mode 100644 index 0000000000..4d47258bff Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/spacingFrame.png differ diff --git a/versions/4.0/zh/particle-system/editor/img/use-dock.gif b/versions/4.0/zh/particle-system/editor/img/use-dock.gif new file mode 100644 index 0000000000..e26a3efc66 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/use-dock.gif differ diff --git a/versions/4.0/zh/particle-system/editor/img/wrapMode.png b/versions/4.0/zh/particle-system/editor/img/wrapMode.png new file mode 100644 index 0000000000..1119423619 Binary files /dev/null and b/versions/4.0/zh/particle-system/editor/img/wrapMode.png differ diff --git a/versions/4.0/zh/particle-system/editor/index.md b/versions/4.0/zh/particle-system/editor/index.md new file mode 100644 index 0000000000..cfc8a652bf --- /dev/null +++ b/versions/4.0/zh/particle-system/editor/index.md @@ -0,0 +1,9 @@ +# 粒子属性编辑 + +Creator 提供了 **粒子控制面板**、**曲线编辑器** 和 **渐变色编辑器**,用于控制粒子播放,以及编辑粒子属性等。 + +详情请参考: + +- [粒子控制面板](particle-effect-panel.md) +- [曲线编辑器](curve-editor.md) +- [渐变色编辑器](gradient-editor.md) diff --git a/versions/4.0/zh/particle-system/editor/particle-effect-panel.md b/versions/4.0/zh/particle-system/editor/particle-effect-panel.md new file mode 100644 index 0000000000..b51bbc1730 --- /dev/null +++ b/versions/4.0/zh/particle-system/editor/particle-effect-panel.md @@ -0,0 +1,25 @@ +# 粒子控制面板 + +在 **层级管理器** 选中粒子节点时,**粒子控制面板** 便会默认显示在 **场景编辑器** 右下角: + +![particle_panel](img/particle_panel.png) + +粒子控制面板可在 **场景编辑器** 区域内拖动,通过粒子控制面板可执行以下操作及设置: + +- ![particle_dock](img/particle_dock.png):不使用面板时,面板在 **场景编辑器** 区域中会造成遮挡。那么便可以点击该按钮选择 **Dock**,当鼠标不在面板上且面板在靠边位置时便会收起,避免遮挡。使用时,将鼠标悬停在面板收起时预留的突出部分,即可唤出面板。 + + ![use-dock](img/use-dock.gif) + +- ![particle_play](img/particle_play.png):在 **场景编辑器** 中播放粒子 + +- ![particle_pause](img/particle_pause.png):暂停播放粒子 + +- ![particle_replay](img/particle_replay.png):重新播放粒子 + +- ![particle_stop](img/particle_stop.png):停止播放粒子 + +- **Playback Speed**:调整粒子播放速度 + +- **Playback Time**:显示粒子播放时长 + +- **Particle**:显示当前粒子数量 diff --git a/versions/4.0/zh/particle-system/emitter.md b/versions/4.0/zh/particle-system/emitter.md new file mode 100644 index 0000000000..47fda3c915 --- /dev/null +++ b/versions/4.0/zh/particle-system/emitter.md @@ -0,0 +1,120 @@ +# 发射器模块(ShapeModule) + +发射器模块主要用于设置粒子发射器形状、粒子发射方向和速度。 + +![shape-module](emitter/shape-module.png) + +**ShapeType** 属性用于选择粒子发射器形状,可选项包括 **盒子**(Box)、**圆形**(Circle)、**锥体**(Cone)、**球**(Sphere)、**半球**(Hemisphere)。当形状选择不同时,可发射粒子的区域不同,相对应的属性设置项也不一致,详情请参考下文对应形状发射器的说明。 + +## 通用属性 + +各类型发射器有一些属性是通用的,包括: + +| 属性 | 功能说明 | +| :---|:--- | +| **ShapeType** | 发射器形状。目前支持 **盒子**(Box)、**圆形**(Circle)、**锥体**(Cone)、**球**(Sphere)、**半球**(Hemisphere)这几种类型。| +| **Position** | 发射器相对于所在节点的位置 | +| **Rotation** | 发射器相对于所在节点的旋转,可用于调整粒子发射方向 | +| **Scale** | 发射器相对于所在节点的缩放,可用于调整发射器形状的大小,即粒子发射范围 | +| **AlignToDirection** | 根据粒子发射的初始方向决定粒子的移动方向,可用于模拟类似水从水桶中洒出的效果 | +| **RandomDirectionAmount** | 粒子生成方向随机混合设定。
设置为 0 时,不生效;
设置为 1 时,方向随机 | +| **SphericalDirectionAmount** | 表示当前发射方向与当前位置到节点中心连线方向的插值。
当该值设置为 1 时,粒子由中心向外部发射(与 **ShapeType** 设置为 **Sphere** 时的行为相同) | +| **RandomPositionAmount** | 粒子生成位置随机设定,当该值设置 **不为 0** 时,粒子生成位置会超出生成器的大小范围 | + +## 盒子发射器 + +当 **ShapeType** 属性设置为 **Box** 时,我们称之为盒子发射器: + +![box_emitter_property](emitter/box_emitter_property.png) + +特有的属性项说明如下: + +| 属性 | 功能说明 | +| :---|:--- | +| **EmitFrom** | 发射器从盒子的哪个部位发射粒子,包括:
**Volume**:从盒子内部发射
**Shell**:从盒子表面发射
**Edge**:从盒子边框发射 | +| **BoxThickness** | 盒子每个发射方向的厚度,仅当 **EmitFrom** 选择 **Shell** 模式时生效| + +同时在 **场景编辑器** 中会有一个盒子 Gizmo,显示盒子发射器的形状大小: + +![box_emitter](emitter/box_emitter.png) + +直接拖拽盒子 Gizmo 的方块控制点可以调整盒子发射器的 **Scale** 属性: + +![box_gizmo](emitter/box-gizmo.gif) + +## 圆发射器 + +当 **ShapeType** 属性设置为 **Circle** 时,我们称之为圆发射器: + +![circle_emitter_property](emitter/circle_emitter_property.png) + +特有的属性项说明如下: + +| 属性 | 功能说明 | +| :--- | :--- | +| **Radius** | 设置圆发射器的半径 | +| **RadiusThickness** | 圆发射器发射粒子的位置。
设置为 **0** 时,表示从圆周发射
设置为 **1** 时,表示从圆心发射
设置为 **0~1** 之间时,表示从圆心到圆周之间,在一定的比例范围内发射 | +| **Arc** | 设置圆发射器发射粒子的扇形区域 | +| **ArcMode** | 设置粒子在扇形区域的发射方式,包括:
**Random**:随机发射
**Loop**:沿某一方向循环发射,每次循环方向相同
**PingPong**:循环发射,每次循环方向都与上一次相反 | +| **ArcSpread** | 设置可能产生粒子的圆弧周围的离散间隔。例如:
设置为 0 时,可在圆弧任意位置生成粒子;
设置为 0.2 时,仅在圆弧周围以 20% 的间隔生成粒子。 | +| **ArcSpeed** | 表示粒子沿圆弧发射的速度。点击右侧的向下图标按钮,即可选择打开曲线编辑器对该属性进行曲线编辑 | + +同时在 **场景编辑器** 中会有一个圆形 Gizmo,显示发射器的形状大小: + +![circle_emitter](emitter/circle_emitter.png) + +直接拖拽圆形 Gizmo 的方块控制点可以调整圆发射器的 **Radius** 属性: + +![circle_gizmo](emitter/circle-gizmo.gif) + +## 圆锥发射器 + +当 **ShapeType** 属性设置为 **Cone** 时,我们称之为圆锥发射器。在 **场景编辑器** 中会有一个圆锥 Gizmo,显示发射器的形状大小: + +![cone_emitter](emitter/cone_emitter.png) + +> 下文以上方图示为例,圆锥下方半径较小的横截面称为圆锥底部。 + +圆锥发射器的属性在 **属性检查器** 中显示如下: + +![cone_emitter_property](emitter/cone_emitter_property.png) + +特有的属性项说明如下: + +| 属性| 功能说明 | +| :---|:--- | +| **EmitFrom** | 发射器从圆锥的哪个部位发射粒子,包括:
**Base**:从圆锥底部发射
**Shell**:从圆锥底部的圆周发射
**Volume**:从圆锥内部发射 | +| **Radius** | 设置圆锥底部横截面的半径 | +| **RadiusThickness** | 圆锥发射器发射粒子的位置。
设置为 **0** 时,表示从圆锥表面发射
设置为 **1** 时,表示从圆锥中心发射
设置为 **0~1** 之间时,表示从圆锥的中心到表面之间,在一定的比例范围内发射 | +| **Angle** | 圆锥的轴与母线的夹角,值越大夹角越大,圆锥发射器的开合程度越大 | +| **Arc** | 设置圆锥发射器发射粒子的扇形区域 | +| **ArcMode** | 表示粒子在扇形区域内的发射方式。
**Random**:随机位置
**Loop**:沿某一方向循环发射,每次循环方向相同
**PingPong**:循环发射,每次循环方向相反
**spread**:表示粒子在某个间断发射,比如,0 表示可以在任意位置发射;0.1 表示每隔圆周的十分之一位置发射 | +| **ArcSpeed** | 表示粒子沿圆弧发射的速度 | +| **ArcSpread** | 表示粒子沿圆弧发射的速度。点击右侧的向下图标按钮,即可选择打开曲线编辑器对该属性进行曲线编辑 | +| **Length** | 圆锥顶部截面距离底部的轴长 | + +其中圆锥发射器的 **Angle**、**Length**、**Radius** 属性也可以通过直接拖拽场景中圆锥 Gizmo 的方块控制点进行设置。 + +## 球体/半球体发射器 + +当 **ShapeType** 属性设置为 **Sphere**/**Hemisphere** 时,我们称之为球体/半球体发射器。 + +球体发射器和半球体发射器的属性是一样的,这里我们以球体发射器为例: + +![Sphere_emitter_property](emitter/sphere-emitter-property.png) + +特有的属性项说明如下: + +| 属性| 功能说明 | +| :---|:--- | +| **EmitFrom** | 发射器从球体的哪个部位发射粒子,包括:
**Volume**:从球体内部发射
**Shell**:从球体表面发射(暂不生效,可通过 **RadiusThickness** 设置) | +| **Radius** | 球体半径。也可以直接拖拽 **场景编辑器** 中球体 Gizmo 的方块控制点进行设置 | +| **RadiusThickness** | 球体发射器发射粒子的位置。
设置为 **0** 时,表示从球表面发射
设置为 **1** 时,表示从球体中心发射
设置为 **0~1** 之间时,表示从表面到球心之间,在一定的比例范围内发射 | + +球体发射器在 **场景编辑器** 中会有一个球 Gizmo,显示发射器的形状大小: + +![Sphere_emitter](emitter/sphere-emitter.png) + +同样的,半球体发射器在 **场景编辑器** 中也有一个半球 Gizmo,显示发射器的形状大小: + +![Hemisphere_emitter](emitter/hemisphere-emitter.png) diff --git a/versions/4.0/zh/particle-system/emitter/box-gizmo.gif b/versions/4.0/zh/particle-system/emitter/box-gizmo.gif new file mode 100644 index 0000000000..2a1d8a3cf1 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/box-gizmo.gif differ diff --git a/versions/4.0/zh/particle-system/emitter/box_emitter.png b/versions/4.0/zh/particle-system/emitter/box_emitter.png new file mode 100644 index 0000000000..3ac3bca03e Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/box_emitter.png differ diff --git a/versions/4.0/zh/particle-system/emitter/box_emitter_property.png b/versions/4.0/zh/particle-system/emitter/box_emitter_property.png new file mode 100644 index 0000000000..342a6946c8 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/box_emitter_property.png differ diff --git a/versions/4.0/zh/particle-system/emitter/circle-gizmo.gif b/versions/4.0/zh/particle-system/emitter/circle-gizmo.gif new file mode 100644 index 0000000000..3b1569d7d2 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/circle-gizmo.gif differ diff --git a/versions/4.0/zh/particle-system/emitter/circle_emitter.png b/versions/4.0/zh/particle-system/emitter/circle_emitter.png new file mode 100644 index 0000000000..bfcd35e2f2 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/circle_emitter.png differ diff --git a/versions/4.0/zh/particle-system/emitter/circle_emitter_property.png b/versions/4.0/zh/particle-system/emitter/circle_emitter_property.png new file mode 100644 index 0000000000..c22282e1f1 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/circle_emitter_property.png differ diff --git a/versions/4.0/zh/particle-system/emitter/cone_emitter.png b/versions/4.0/zh/particle-system/emitter/cone_emitter.png new file mode 100644 index 0000000000..b118b613b7 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/cone_emitter.png differ diff --git a/versions/4.0/zh/particle-system/emitter/cone_emitter_property.png b/versions/4.0/zh/particle-system/emitter/cone_emitter_property.png new file mode 100644 index 0000000000..c5ce468b91 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/cone_emitter_property.png differ diff --git a/versions/4.0/zh/particle-system/emitter/hemisphere-emitter.png b/versions/4.0/zh/particle-system/emitter/hemisphere-emitter.png new file mode 100644 index 0000000000..56b36588f6 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/hemisphere-emitter.png differ diff --git a/versions/4.0/zh/particle-system/emitter/shape-module.png b/versions/4.0/zh/particle-system/emitter/shape-module.png new file mode 100644 index 0000000000..c84aca768c Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/shape-module.png differ diff --git a/versions/4.0/zh/particle-system/emitter/sphere-emitter-property.png b/versions/4.0/zh/particle-system/emitter/sphere-emitter-property.png new file mode 100644 index 0000000000..abefa558b8 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/sphere-emitter-property.png differ diff --git a/versions/4.0/zh/particle-system/emitter/sphere-emitter.png b/versions/4.0/zh/particle-system/emitter/sphere-emitter.png new file mode 100644 index 0000000000..dea03417a0 Binary files /dev/null and b/versions/4.0/zh/particle-system/emitter/sphere-emitter.png differ diff --git a/versions/4.0/zh/particle-system/force-module.md b/versions/4.0/zh/particle-system/force-module.md new file mode 100644 index 0000000000..09a79c6975 --- /dev/null +++ b/versions/4.0/zh/particle-system/force-module.md @@ -0,0 +1,18 @@ +# 加速度模块(ForceOvertimeModule) + +通过该模块可对粒子进行加速,以模拟类似风的效果。 + +![force_overtime](module/force_overtime.gif) + +## 属性 + +![force_module](module/force_module.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Space** | 加速度计算时选择基于 **世界坐标系**(World)或者 **本地坐标系**(Local)
(**Custom** 目前暂不支持) | +| **X** | X 轴方向上的加速度分量 | +| **Y** | Y 轴方向上的加速度分量 | +| **Z** | Z 轴方向上的加速度分量 | + +点击属性输入框右侧的 ![menu button](main-module/menu-button.png) 按钮,可选择对属性进行曲线编辑,详情请参考 [曲线编辑器](./editor/curve-editor.md)。 diff --git a/versions/4.0/zh/particle-system/index.md b/versions/4.0/zh/particle-system/index.md new file mode 100644 index 0000000000..0ec4a98b9b --- /dev/null +++ b/versions/4.0/zh/particle-system/index.md @@ -0,0 +1,10 @@ +# 粒子系统 + +粒子系统是游戏引擎特效表现的基础,它可以用于模拟的火、烟、水、云、雪、落叶等自然现象,也可用于模拟发光轨迹、速度线等抽象视觉效果。 + +![particle](particle.gif) + +Creator 目前支持 2D/3D 粒子系统,详情请参考: + +- [2D 粒子](2d-particle/2d-particle.md) +- [3D 粒子](overview.md) diff --git a/versions/4.0/zh/particle-system/limit-velocity-module.md b/versions/4.0/zh/particle-system/limit-velocity-module.md new file mode 100644 index 0000000000..e672bcde33 --- /dev/null +++ b/versions/4.0/zh/particle-system/limit-velocity-module.md @@ -0,0 +1,14 @@ +# 限速模块(LimitVelocityOvertimeModule) + +限速模块用于设置粒子的速度在生命周期内逐渐减缓。 + +![limit_module](module/limit_module.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Space** | 速度计算时选择基于 **世界坐标系**(World)或者 **本地坐标系**(Local)
(**Custom** 目前暂不支持) | +| **Dampen** | 当前速度与速度下限的插值 +| **SeparateAxes** | 是否在 X、Y、Z 轴上分别限制粒子速度。若勾选该项,则可设置:
**LimitX**:X 轴方向上的速度下限
**LimitY**:Y 轴方向上的速度下限
**LimitZ**:Z 轴方向上的速度下限 +| **Limit** | 速度下限。若当前速度超出该值时,当前速度将会与速度下限进行线性插值。该项仅在不勾选 **SeparateAxes** 时生效 + +点击属性输入框右侧的 ![menu button](main-module/menu-button.png) 按钮,可选择对属性进行曲线编辑,详情请参考 [曲线编辑](./editor/curve-editor.md)。 diff --git a/versions/4.0/zh/particle-system/line-component.md b/versions/4.0/zh/particle-system/line-component.md new file mode 100644 index 0000000000..4c4c9447da --- /dev/null +++ b/versions/4.0/zh/particle-system/line-component.md @@ -0,0 +1,66 @@ +# Line 组件 + +Line 组件用于渲染 3D 场景中给定的点连成的线段。Line 组件渲染的线段是有宽度的,并且总是面向摄像机,这与 billboard 组件相似。 + +## 属性与说明 + +![line-component](./line/line-component.png) + +属性| 功能 +---|--- +**texture** | 线段中显示的贴图。 +**worldSpace** | 线段中各个点的坐标采用哪个坐标系,勾选使用世界坐标系,不选使用本地坐标系。 +**positions** | 每个线段端点的坐标。 +**width** | 线段宽度,如果采用曲线,则表示沿着线段方向上的曲线变化。 +**tile** | 贴图平铺次数。 +**offset** | 贴图坐标的偏移。 +**color** | 线段颜色,如果采用渐变色,则表示沿着线段方向上的颜色渐变。 + +### 线段宽度 + +在 **线段宽度** 的右侧点击下拉按钮可以修改宽度的类型。其可选项如下: + +![line-curve](./line/line-curve.png) + +| 属性 | 说明 | +| :-- | :-- | +| **Constant** | 线段宽度由常量组成 | +| **Curve** | 由曲线组成,此时可点击 **Width** 后的输入框对曲线进行编辑 | +| **TwoCurves** | 由两条曲线组成 | +| **TwoConstants** | 由两个常量组成 | + +#### 曲线 + +曲线编辑和粒子曲线编辑器类似,详情可参考 [粒子曲线编辑器 - 曲线编辑面板](editor/curve-editor.md/#%E6%9B%B2%E7%BA%BF%E7%BC%96%E8%BE%91%E9%9D%A2%E6%9D%BF)。 + +![curve-editor](line/line-curve-editor.png) + +编辑效果如下: + +![curve-sample](line/curve-sample.png) + +当选择为两条曲线时,线段的宽度将由两条曲线共同作用。 + +![two-curves](line/two-curves.png) + +#### 常量 + +![two-constant](line/two-constants.png) + +当宽度的类型选择为 **TwoConstants** 时,可以通过输入 **ConstantMin**,**ConstantMax** 调整线段的宽度。 + +### 线段颜色 + +![line-color](./line/line-color.png) + +点击 **颜色** 属性的右侧的下拉按钮可以对线段的着色方式进行更新更改,其可选项如下: + +| 属性 | 说明 | +| :-- | :-- | +| **Color** | 使用纯色 | +| **Gradient** | 使用渐变色,通过 [渐变色编辑器](editor/gradient-editor.md) 可以对颜色进行调整 | +| **TwoColors** | 使用两个颜色 | +| **TwoGradients** | 使用两个渐变颜色 | +| **RandomColor** | 使用随机的颜色 | + +Line 组件接口请参考 [Line API](%__APIDOC__%/zh/class/Line)。 diff --git a/versions/4.0/zh/particle-system/line/curve-sample.png b/versions/4.0/zh/particle-system/line/curve-sample.png new file mode 100644 index 0000000000..f12004d3d2 Binary files /dev/null and b/versions/4.0/zh/particle-system/line/curve-sample.png differ diff --git a/versions/4.0/zh/particle-system/line/line-color.png b/versions/4.0/zh/particle-system/line/line-color.png new file mode 100644 index 0000000000..e46513e592 Binary files /dev/null and b/versions/4.0/zh/particle-system/line/line-color.png differ diff --git a/versions/4.0/zh/particle-system/line/line-component.png b/versions/4.0/zh/particle-system/line/line-component.png new file mode 100644 index 0000000000..b2e86002b9 Binary files /dev/null and b/versions/4.0/zh/particle-system/line/line-component.png differ diff --git a/versions/4.0/zh/particle-system/line/line-curve-editor.png b/versions/4.0/zh/particle-system/line/line-curve-editor.png new file mode 100644 index 0000000000..149a39171a Binary files /dev/null and b/versions/4.0/zh/particle-system/line/line-curve-editor.png differ diff --git a/versions/4.0/zh/particle-system/line/line-curve.png b/versions/4.0/zh/particle-system/line/line-curve.png new file mode 100644 index 0000000000..4f58e12693 Binary files /dev/null and b/versions/4.0/zh/particle-system/line/line-curve.png differ diff --git a/versions/4.0/zh/particle-system/line/two-constants.png b/versions/4.0/zh/particle-system/line/two-constants.png new file mode 100644 index 0000000000..28bdb2ea9d Binary files /dev/null and b/versions/4.0/zh/particle-system/line/two-constants.png differ diff --git a/versions/4.0/zh/particle-system/line/two-curves.png b/versions/4.0/zh/particle-system/line/two-curves.png new file mode 100644 index 0000000000..4008eb9162 Binary files /dev/null and b/versions/4.0/zh/particle-system/line/two-curves.png differ diff --git a/versions/4.0/zh/particle-system/main-module.md b/versions/4.0/zh/particle-system/main-module.md new file mode 100644 index 0000000000..442189325c --- /dev/null +++ b/versions/4.0/zh/particle-system/main-module.md @@ -0,0 +1,63 @@ +# 主模块(ParticleSystem) + +粒子系统主模块用于存储在 **属性检查器** 中显示的所有数据,管理粒子生成、播放、更新,以及销毁。 + +![main-module](main-module/main-module.png) + +| 属性 | 功能说明 | +| :-- | :-- | +| Duration | 粒子系统运行总时间 | +| Capacity | 粒子系统能生成的最大粒子数量 | +| Loop | 粒子系统是否循环播放 | +| PlayOnAwake | 粒子系统加载后是否自动开始播放 | +| Prewarm | 选中之后,粒子系统会以已播放完一轮之后的状态开始播放(仅当循环播放启用时有效) | +| SimulationSpace | 控制粒子坐标计算所在的坐标系 | +| SimulationSpeed | 控制整个粒子系统的更新速度 | +| StartDelay | 粒子系统开始运行后,粒子延迟发射的时间(勾选 Prewarm 之后 StartDelay 会被设置为0,此选项失效) | +| StartLifetime | 粒子生命周期 | +| StartColor | 粒子初始颜色 | +| ScaleSpace | 粒子缩放时所在的坐标系
**Local**:基于本地坐标系的缩放
**World**:基于世界坐标系的缩放
**Custom**:自定义缩放,不受节点的 **scale** 属性影响 | +| StartSize3D | 分别设置粒子 X、Y、Z 轴的初始大小 | +| StartSize | X 轴的初始大小,与 `StartSize3D` 属性二者只能选其一 | +| StartSpeed | 粒子初始速度 | +| StartRotation3D | 分别设置粒子 X、Y、Z 轴的初始旋转角度 | +| StartRotation | Z 轴初始旋转角度,与 `StartRotation3D` 属性二者只能选其一 | +| GravityModifier | 粒子受重力影响的重力系数(只支持 CPU 粒子) | +| RateOverTime | 每秒发射的粒子数 | +| RateOverDistance | 每个移动单位距离发射的粒子数 | +| Bursts | 设定在指定时间发射指定数量粒子的 Burst 数量。可通过以下几个属性调整:
**Time**:粒子播放多长时候后开始发射 Burst
**RepeatCount**:Burst 触发次数
**RepeatInterval**:每次触发的时间间隔
**Count**:发射的粒子数量 | +| DataCulling | 粒子系统资源剔除,详情请参考下文说明 | +| RenderCulling | 粒子剔除,详情请参考下文说明 | + +点击上述部分属性输入框右侧的 ![menu button](main-module/menu-button.png) 按钮,即可开启粒子曲线/渐变色编辑器,对粒子属性进行编辑,详情请参考 [粒子属性编辑](./editor/index.md)。 + +![set-pro](main-module/set-pro.png) + +粒子系统组件接口请参考 [ParticleSystem API](%__APIDOC__%/zh/class/ParticleSystem)。 + +## 粒子系统资源剔除 + +**DataCulling** 选项用于剔除粒子系统中无用模块的资源数据。 + +粒子系统中各个模块都是作为独立对象存在,每个模块都会存储一些模块相关的数据,因此对于未勾选使用的模块,其记录的数据都是无用数据。当开发者不需要在运行时动态开启这些未使用的模块,可以勾选 **DataCulling** 选项,对这些无用数据进行剔除,从而减小资源占用。 + +> **注意**:v3.4 之前该项是 **EnableCulling**,在 v3.4 为了与下方的 **RenderCulling** 区分,我们将其改名为 **DataCulling**。本次调整做了兼容性处理,对用户没有丝毫影响,无须担心。 + +## 粒子剔除 + +粒子系统从 v3.4 开始新增了 **RenderCulling** 选项,用于开启粒子剔除功能。 + +开启该项粒子发射器将会自动计算出一个包围盒,运行时便会根据包围盒是否在摄像机的可见范围内来剔除粒子发射器,若不在则剔除。剔除运算每一帧都会进行,适合一些耗时的特效,如果粒子个数少不建议开启该项。 + +包围盒的大小可以通过下图中的 **AabbHalf** 进行调整,调整完成后,点击 **Regenerate bounding box** 按钮即可重新计算包围盒。 + +![render culling](main-module/render-culling.png) + +| 属性 | 功能说明 | +| :--- | :-- | +| CullingMode | 粒子发射器被剔除之后的行为,可设置的选项包括 **Pause**、**Pause and Catchup**、**Always Simulate**。
**Pause**:若粒子发射器包围盒不在摄像机的可见范围内,粒子暂停模拟。若恢复可见,则粒子会接着上次暂停的时间继续模拟;
**Pause and Catchup**:若粒子发射器包围盒不在摄像机的可见范围内,粒子暂停模拟。若恢复可见,则粒子会以当前的时间开始模拟;
**Always Simulate**:无论粒子发射器包围盒是否在摄像机的可见范围内,粒子都会一直模拟,只是不在摄像机的可见范围内时不进行渲染。 | +| AabbHalfX | 设置粒子发射器包围盒半宽 | +| AabbHalfY | 设置粒子发射器包围盒半高 | +| AabbHalfZ | 设置粒子发射器包围盒半长 | +| Show Bounds | 在 **场景编辑器** 中显示粒子发射器包围盒 | +| Regenerate bounding box| 包围盒大小调整完成后,点击该按钮重新计算包围盒 | diff --git a/versions/4.0/zh/particle-system/main-module/main-module.png b/versions/4.0/zh/particle-system/main-module/main-module.png new file mode 100644 index 0000000000..3df45e5c30 Binary files /dev/null and b/versions/4.0/zh/particle-system/main-module/main-module.png differ diff --git a/versions/4.0/zh/particle-system/main-module/menu-button.png b/versions/4.0/zh/particle-system/main-module/menu-button.png new file mode 100644 index 0000000000..e2d836084f Binary files /dev/null and b/versions/4.0/zh/particle-system/main-module/menu-button.png differ diff --git a/versions/4.0/zh/particle-system/main-module/render-culling.png b/versions/4.0/zh/particle-system/main-module/render-culling.png new file mode 100644 index 0000000000..a72f725942 Binary files /dev/null and b/versions/4.0/zh/particle-system/main-module/render-culling.png differ diff --git a/versions/4.0/zh/particle-system/main-module/set-pro.png b/versions/4.0/zh/particle-system/main-module/set-pro.png new file mode 100644 index 0000000000..7ae63a1de0 Binary files /dev/null and b/versions/4.0/zh/particle-system/main-module/set-pro.png differ diff --git a/versions/4.0/zh/particle-system/module.md b/versions/4.0/zh/particle-system/module.md new file mode 100644 index 0000000000..56a8ec64dd --- /dev/null +++ b/versions/4.0/zh/particle-system/module.md @@ -0,0 +1,20 @@ +# 粒子系统功能介绍 + +Cocos Creator 的 **粒子系统** 存储了粒子发射的初始状态以及粒子发射后的状态更新子模块。 + +## 粒子系统模块 + +Cocos Creator 粒子系统操作面板如下: + +![inspector_1](module/inspector_1.png)
+![inspector_2](module/inspector_2.png) + +粒子系统使用模块来组织功能,包括以下模块: + +| 模块 | 说明 | +| :--- | :--- | +| Node | 粒子节点,用于设置粒子发射器的位置、方向、大小、渲染层级等属性 | +| [ParticleSystem](main-module.md)(主模块) | 用于存储在 **属性检查器** 中显示的所有数据,管理粒子生成、更新、销毁相关模块,控制粒子播放 | +| [ShapeModule](emitter.md)(发射器模块) | 用于控制粒子发射,包括发射方向与速度,支持预定义的发射方向,包括方块、圆、锥体、球、半球 | +| AnimatorModule(状态模块) | 用于控制粒子发射后的状态更新。已支持的功能包括:
[噪声模块(NoiseModule)](noise-module.md)
[速度模块(VelocityOvertimeModule)](velocity-module.md)
[加速模块(ForceOvertimeModule)](force-module.md)
[大小模块(SizeOvertimeModule)](size-module.md)
[旋转模块(RotationOvertimeModule)](rotation-module.md)
[颜色模块(ColorOvertimeModule)](color-module.md)
[贴图动画模块(TextureAnimationModule)](texture-animation-module.md)
[限速模块(LimitVelocityOvertimeModule)](limit-velocity-module.md)
[拖尾模块(TrailModule)](trail-module.md) | +| [Renderer](renderer.md) | 用于生成粒子渲染所需要的数据。包括 VB、IB、渲染状态相关的控制 | diff --git a/versions/4.0/zh/particle-system/module/color_module.png b/versions/4.0/zh/particle-system/module/color_module.png new file mode 100644 index 0000000000..4649b6ea9b Binary files /dev/null and b/versions/4.0/zh/particle-system/module/color_module.png differ diff --git a/versions/4.0/zh/particle-system/module/color_overtime.gif b/versions/4.0/zh/particle-system/module/color_overtime.gif new file mode 100644 index 0000000000..7f6e204423 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/color_overtime.gif differ diff --git a/versions/4.0/zh/particle-system/module/force_module.png b/versions/4.0/zh/particle-system/module/force_module.png new file mode 100644 index 0000000000..c859520882 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/force_module.png differ diff --git a/versions/4.0/zh/particle-system/module/force_overtime.gif b/versions/4.0/zh/particle-system/module/force_overtime.gif new file mode 100644 index 0000000000..8734abbbcf Binary files /dev/null and b/versions/4.0/zh/particle-system/module/force_overtime.gif differ diff --git a/versions/4.0/zh/particle-system/module/inspector_1.png b/versions/4.0/zh/particle-system/module/inspector_1.png new file mode 100644 index 0000000000..b552555a97 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/inspector_1.png differ diff --git a/versions/4.0/zh/particle-system/module/inspector_2.png b/versions/4.0/zh/particle-system/module/inspector_2.png new file mode 100644 index 0000000000..e32cbbb613 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/inspector_2.png differ diff --git a/versions/4.0/zh/particle-system/module/limit_module.png b/versions/4.0/zh/particle-system/module/limit_module.png new file mode 100644 index 0000000000..b284e86fa3 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/limit_module.png differ diff --git a/versions/4.0/zh/particle-system/module/noise_module.png b/versions/4.0/zh/particle-system/module/noise_module.png new file mode 100644 index 0000000000..ccdc1d0eb9 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/noise_module.png differ diff --git a/versions/4.0/zh/particle-system/module/renderer.png b/versions/4.0/zh/particle-system/module/renderer.png new file mode 100644 index 0000000000..70c83132ea Binary files /dev/null and b/versions/4.0/zh/particle-system/module/renderer.png differ diff --git a/versions/4.0/zh/particle-system/module/rotate_overtime.gif b/versions/4.0/zh/particle-system/module/rotate_overtime.gif new file mode 100644 index 0000000000..67fb69bb3b Binary files /dev/null and b/versions/4.0/zh/particle-system/module/rotate_overtime.gif differ diff --git a/versions/4.0/zh/particle-system/module/rotation_module.png b/versions/4.0/zh/particle-system/module/rotation_module.png new file mode 100644 index 0000000000..04e129826c Binary files /dev/null and b/versions/4.0/zh/particle-system/module/rotation_module.png differ diff --git a/versions/4.0/zh/particle-system/module/size_module.png b/versions/4.0/zh/particle-system/module/size_module.png new file mode 100644 index 0000000000..ba830e5261 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/size_module.png differ diff --git a/versions/4.0/zh/particle-system/module/size_module_curve.png b/versions/4.0/zh/particle-system/module/size_module_curve.png new file mode 100644 index 0000000000..9f1f28a41a Binary files /dev/null and b/versions/4.0/zh/particle-system/module/size_module_curve.png differ diff --git a/versions/4.0/zh/particle-system/module/size_overtime.gif b/versions/4.0/zh/particle-system/module/size_overtime.gif new file mode 100644 index 0000000000..14f5b58305 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/size_overtime.gif differ diff --git a/versions/4.0/zh/particle-system/module/texture_animation.gif b/versions/4.0/zh/particle-system/module/texture_animation.gif new file mode 100644 index 0000000000..5bf3bd664a Binary files /dev/null and b/versions/4.0/zh/particle-system/module/texture_animation.gif differ diff --git a/versions/4.0/zh/particle-system/module/texture_animation.png b/versions/4.0/zh/particle-system/module/texture_animation.png new file mode 100644 index 0000000000..6fb798ea36 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/texture_animation.png differ diff --git a/versions/4.0/zh/particle-system/module/trail.gif b/versions/4.0/zh/particle-system/module/trail.gif new file mode 100644 index 0000000000..285ba2161c Binary files /dev/null and b/versions/4.0/zh/particle-system/module/trail.gif differ diff --git a/versions/4.0/zh/particle-system/module/trail_module.png b/versions/4.0/zh/particle-system/module/trail_module.png new file mode 100644 index 0000000000..5ff524d8d1 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/trail_module.png differ diff --git a/versions/4.0/zh/particle-system/module/velocity_module.png b/versions/4.0/zh/particle-system/module/velocity_module.png new file mode 100644 index 0000000000..298f854d98 Binary files /dev/null and b/versions/4.0/zh/particle-system/module/velocity_module.png differ diff --git a/versions/4.0/zh/particle-system/module/velocity_overtime.gif b/versions/4.0/zh/particle-system/module/velocity_overtime.gif new file mode 100644 index 0000000000..ed5ca8286c Binary files /dev/null and b/versions/4.0/zh/particle-system/module/velocity_overtime.gif differ diff --git a/versions/4.0/zh/particle-system/noise-module.md b/versions/4.0/zh/particle-system/noise-module.md new file mode 100644 index 0000000000..39c2ea1278 --- /dev/null +++ b/versions/4.0/zh/particle-system/noise-module.md @@ -0,0 +1,20 @@ +# 噪声模块(NoiseModule) + +噪声模块用于增强粒子运动的效果,当 [Renderer 模块](./renderer.md) 中勾选 **UseGPU** 属性时不生效 + +## 属性 + +![noise_module](module/noise_module.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Strength X** | X 轴方向上噪声的强度 | +| **Strength Y** | Y 轴方向上噪声的强度 | +| **Strength Z** | Z 轴方向上噪声的强度 | +| **Noise Speed X** | X 轴方向上噪声贴图滚动速度 | +| **Noise Speed Y** | Y 轴方向上噪声贴图滚动速度 | +| **Noise Speed Z** | Z 轴方向上噪声贴图滚动速度 | +| **Noise Frequency** | 噪声频率 | +| **Octaves** | 指定组合多少层重叠噪声来产生最终噪声值 | +| **Octave Multiplier** | 对于每个附加的噪声层,按此比例降低强度 | +| **Octave Scale** | 对于每个附加的噪声层,按此乘数调整频率 | \ No newline at end of file diff --git a/versions/4.0/zh/particle-system/overview.md b/versions/4.0/zh/particle-system/overview.md new file mode 100644 index 0000000000..cb58ec69b7 --- /dev/null +++ b/versions/4.0/zh/particle-system/overview.md @@ -0,0 +1,46 @@ +# 3D 粒子系统 + +粒子系统是游戏引擎特效表现的基础,它可以用于模拟的火、烟、水、云、雪、落叶等自然现象,也可用于模拟发光轨迹、速度线等抽象视觉效果。 + +## 基本结构 + +粒子系统的基本单元是粒子,一个粒子一般具有位置、大小、颜色、速度、加速度、生命周期等属性。在每一帧中,粒子系统一般会执行如下步骤: + +1. 产生新的粒子,并初始化 +2. 删除超过生命周期的粒子 +3. 更新粒子的动态属性 +4. 渲染所有有效的粒子 + +粒子系统一般由以下几个部分组成: + +1. 发射器,用于创建粒子,并初始化粒子的属性 +2. 影响器,用于更新粒子的属性 +3. 渲染器,渲染粒子 +4. 粒子类,存储粒子的属性 +5. 粒子系统类,管理上面的模块 + +## 添加 3D 粒子系统 + +在编辑器中添加粒子系统组件有以下两种方式: + +1. 在 **层级管理器** 中选中节点,然后点击右侧 **属性检查器** 面板上的 **添加组件** 按钮添加粒子,如下图: + + ![new_ParticleSystemComponent](overview/new_ParticleSystemComponent.png) + +2. 在左侧的 **层级管理器** 面板中点击鼠标右键,然后选择 **创建 -> 特效 -> 粒子系统**,即可创建一个带有粒子系统组件的节点,如下图: + + ![new_ParticleSystemComponent_node](overview/new_ParticleSystemComponent_node.png) + +添加完成后,粒子在 **场景编辑器** 中显示如下: + +![particle panel](overview/particle-panel.png) + +右下角的粒子系统控制面板用于控制粒子的播放暂停等,详情请参考 [粒子控制面板](./editor/particle-effect-panel.md)。 + +## 内容 + +粒子系统主要包括以下两部分内容: + +- [粒子系统模块](./module.md) + +- [粒子属性编辑](./editor/index.md) diff --git a/versions/4.0/zh/particle-system/overview/new_ParticleSystemComponent.png b/versions/4.0/zh/particle-system/overview/new_ParticleSystemComponent.png new file mode 100644 index 0000000000..8f0a8f07b5 Binary files /dev/null and b/versions/4.0/zh/particle-system/overview/new_ParticleSystemComponent.png differ diff --git a/versions/4.0/zh/particle-system/overview/new_ParticleSystemComponent_node.png b/versions/4.0/zh/particle-system/overview/new_ParticleSystemComponent_node.png new file mode 100644 index 0000000000..c8fa3ec96f Binary files /dev/null and b/versions/4.0/zh/particle-system/overview/new_ParticleSystemComponent_node.png differ diff --git a/versions/4.0/zh/particle-system/overview/particle-panel.png b/versions/4.0/zh/particle-system/overview/particle-panel.png new file mode 100644 index 0000000000..cca024f469 Binary files /dev/null and b/versions/4.0/zh/particle-system/overview/particle-panel.png differ diff --git a/versions/4.0/zh/particle-system/particle-system/billboard.png b/versions/4.0/zh/particle-system/particle-system/billboard.png new file mode 100644 index 0000000000..8895d0d367 Binary files /dev/null and b/versions/4.0/zh/particle-system/particle-system/billboard.png differ diff --git a/versions/4.0/zh/particle-system/particle-system/inspector_1.png b/versions/4.0/zh/particle-system/particle-system/inspector_1.png new file mode 100644 index 0000000000..14659b9335 Binary files /dev/null and b/versions/4.0/zh/particle-system/particle-system/inspector_1.png differ diff --git a/versions/4.0/zh/particle-system/particle-system/inspector_2.png b/versions/4.0/zh/particle-system/particle-system/inspector_2.png new file mode 100644 index 0000000000..0fdf1ad286 Binary files /dev/null and b/versions/4.0/zh/particle-system/particle-system/inspector_2.png differ diff --git a/versions/4.0/zh/particle-system/particle-system/particle-system-class.png b/versions/4.0/zh/particle-system/particle-system/particle-system-class.png new file mode 100644 index 0000000000..6c86644ca6 Binary files /dev/null and b/versions/4.0/zh/particle-system/particle-system/particle-system-class.png differ diff --git a/versions/4.0/zh/particle-system/particle-system/renderer.png b/versions/4.0/zh/particle-system/particle-system/renderer.png new file mode 100644 index 0000000000..35a2d30735 Binary files /dev/null and b/versions/4.0/zh/particle-system/particle-system/renderer.png differ diff --git a/versions/4.0/zh/particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md b/versions/4.0/zh/particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md new file mode 100644 index 0000000000..ffb9c18469 --- /dev/null +++ b/versions/4.0/zh/particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md @@ -0,0 +1,103 @@ +# 升级指南:粒子 从 v3.5.x 升级到 v3.6.0 + +## CPU 粒子 + +v3.6.0 粒子系统新增了 instance 支持,粒子 shader particle-vs-legacy.chunk 需要做如下修改: + +原来的 layout: + +```` +in vec3 a_position; // center position +in vec3 a_texCoord; // xy:vertex index,z:frame index +in vec3 a_texCoord1; // size +in vec3 a_texCoord2; // rotation +in vec4 a_color; + +#if CC_RENDER_MODE == RENDER_MODE_STRETCHED_BILLBOARD + in vec3 a_color1; // velocity.x, velocity.y, velocity.z, scale +#endif + +#if CC_RENDER_MODE == RENDER_MODE_MESH + in vec3 a_texCoord3; // mesh vertices + in vec3 a_normal; // mesh normal + in vec4 a_color1; // mesh color +#endif +```` + +现在的 layout: + +```` +in vec3 a_texCoord1; // size +in vec3 a_texCoord2; // rotation +in vec4 a_color; + +in vec3 a_texCoord; // xy:vertex index,z:frame index +#if !CC_INSTANCE_PARTICLE + in vec3 a_position; // center position +#endif +#if CC_INSTANCE_PARTICLE + in vec4 a_texCoord4; // xyz:position,z:frame index +#endif + +#if CC_RENDER_MODE == RENDER_MODE_STRETCHED_BILLBOARD + in vec3 a_color1; // velocity.x, velocity.y, velocity.z, scale +#endif + +#if CC_RENDER_MODE == RENDER_MODE_MESH + in vec3 a_texCoord3; // mesh vertices + in vec3 a_normal; // mesh normal + in vec4 a_color1; // mesh color +#endif +```` + +其它 shader 代码需要参考 v3.6.0 的 particle-vs-legacy.chunk 进行修改。 + +## GPU 粒子 + +GPU 粒子 shader particle-vs-gpu.chunk 需要做如下修改: + +原来的 layout: + +```` +in vec4 a_position_starttime; // center position,particle start time +in vec4 a_size_uv; // xyz:size, w:uv_0 +in vec4 a_rotation_uv; // xyz:rotation, w:uv_1 +in vec4 a_color; +in vec4 a_dir_life; // xyz:particle start velocity,w:particle lifetime +in float a_rndSeed; + +#if CC_RENDER_MODE == RENDER_MODE_MESH + in vec3 a_texCoord; // mesh uv + in vec3 a_texCoord3; // mesh vertices + in vec3 a_normal; // mesh normal + in vec4 a_color1; // mesh color +#endif +```` + +现在的 layout + +```` +in vec4 a_position_starttime; // center position,particle start time +in vec4 a_color; +in vec4 a_dir_life; // xyz:particle start velocity,w:particle lifetime +in float a_rndSeed; + +#if !CC_INSTANCE_PARTICLE + in vec4 a_size_uv; // xyz:size, w:uv_0 + in vec4 a_rotation_uv; // xyz:rotation, w:uv_1 +#endif +#if CC_INSTANCE_PARTICLE + in vec4 a_size_fid; // xyz:size, w:fid + in vec3 a_rotation; // xyz:rotation + in vec3 a_uv; +#endif + +#if CC_RENDER_MODE == RENDER_MODE_MESH + in vec3 a_texCoord; // mesh uv + in vec3 a_texCoord3; // mesh vertices + in vec3 a_normal; // mesh normal + in vec4 a_color1; // mesh color +#endif +```` + +其它 shader 代码需要参考 v3.6.0 的 particle-vs-gpu.chunk 进行修改。 diff --git a/versions/4.0/zh/particle-system/particle.gif b/versions/4.0/zh/particle-system/particle.gif new file mode 100644 index 0000000000..21ff8003ab Binary files /dev/null and b/versions/4.0/zh/particle-system/particle.gif differ diff --git a/versions/4.0/zh/particle-system/renderer.md b/versions/4.0/zh/particle-system/renderer.md new file mode 100644 index 0000000000..34fa40a797 --- /dev/null +++ b/versions/4.0/zh/particle-system/renderer.md @@ -0,0 +1,31 @@ +# 渲染模块(Renderer) + +渲染模块用于生成粒子渲染所需要的数据。 + +![renderer](module/renderer.png) + +| 属性 | 说明 | +| :--- | :--- | +| **RenderMode** | 设置一个粒子面片的生成方式,包括:
**Billboard**:粒子显示为公告牌,并始终面向摄像机;
**StretchedBillboard**:粒子始终面向摄像机,但会根据相关参数进行拉伸;
**HorizontalBillboard**:粒子始终与 X-Z 平面平行
**VerticalBillboard**:粒子始终与 Y 轴平行,但会朝向摄像机;
**Mesh**:以指定的网格资源渲染粒子。 | +| **VelocityScale** | 当 **RenderMode** 设置为 **StretchedBillboard** 模式时,根据粒子 **速度大小** 按比例进行拉伸。
当设置为 0 时,可禁用基于速度的拉伸。 | +| **LengthScale** | 当 **RenderMode** 设置为 **StretchedBillboard** 模式时,根据粒子 **大小** 按比例进行拉伸。
当设置为 0 时,相当于粒子大小为 0,粒子会消失。 | +| **Mesh** | 当 **RenderMode** 设置为 **Mesh** 时,指定粒子渲染的网格资源。 | +| **ParticleMaterial** | 用于粒子渲染的材质。
当使用 CPU 渲染器,也就是不勾选 **UseGPU** 时,材质使用的 `effect` 只能选择 Creator 内置的 `builtin-particle`,不支持其它的 `effect`。
当使用 GPU 渲染器,也就是勾选 **UseGPU** 时,材质使用的 `effect` 只能选择 Creator 内置的 `builtin-particle-gpu`,不支持其它的 `effect`。 | +| **TrailMaterial** | 用于渲染粒子拖尾的材质,材质的 effect 只支持 `builtin-particle-trail`,不支持其它的 effect。 | +| **UseGPU** | 是否使用 GPU 渲染器进行粒子的渲染,默认不勾选。
不勾选时,使用 **CPU 渲染器**(ParticleSystemRendererCPU)进行粒子的渲染。
勾选时,使用 **GPU 渲染器**(ParticleSystemRendererGPU)进行粒子的渲染。
详情请参考下文 **粒子渲染器** 部分的内容。 | +| **AlignSpace** | 粒子对齐方向空间,可选项包括 **View**(视角空间)、**World**(世界空间)和 **Local**(局部空间),默认使用视角空间。选择不同空间可以决定粒子初始方向:
选择 **View** 时,粒子网格的旋转方向将会跟随摄像机的视角方向;
选择 **World** 时,粒子网格的方向将会使用发射器所在节点的世界空间旋转方向;
选择 **Local** 时,粒子网格将会使用发射器所在节点的局部空间旋转方向。 | + +## 粒子渲染器 + +粒子渲染部分由 **渲染器**(ParticleSystemRenderer)控制,渲染器分为 **CPU 渲染器**(默认)和 **GPU 渲染器**,可通过渲染模块中的 **UseGPU** 属性选择使用。 + +- CPU 渲染器(ParticleSystemRendererCPU)通过一个对象池来维护所有粒子,根据粒子当前状态来生成对应的 VB、IB 数据,持有粒子需要渲染的材质,并且保存相关渲染状态。粒子系统默认使用 CPU 渲染器。 + +- GPU 渲染器(ParticleSystemRendererGPU)目前是在 CPU 端生成粒子,只提交初始参数的 VB、IB 数据,但模块相关的计算则是通过预采样数据的形式,在初始化时提交一次数据。后续的模块系统则是在 GPU 端对数据进行提取模拟运算,减少 CPU 端的计算压力。
+可通过勾选 **UseGPU** 属性选择使用 GPU 渲染器。目前暂不支持 [拖尾模块](./trail-module.md) 和 [限速模块](./limit-velocity-module.md)。 + +## 自定义材质说明 + +> **注意**:如果需要对粒子系统使用自定义材质,则该自定义材质的名称有以下限制: +> 1. 如果要在 CPU 粒子上使用该自定义材质,则材质名称需包含 `particle-cpu`, 如 `custom-particle-cpu` +> 2. 如果要在 GPU 粒子上使用该自定义材质,则材质名称需包含 `particle-gpu`, 如 `my-particle-gpu` diff --git a/versions/4.0/zh/particle-system/rotation-module.md b/versions/4.0/zh/particle-system/rotation-module.md new file mode 100644 index 0000000000..86f9d78350 --- /dev/null +++ b/versions/4.0/zh/particle-system/rotation-module.md @@ -0,0 +1,16 @@ +# 旋转模块(RotationOvertimeModule) + +旋转模块用于设置粒子运行时在移动中旋转,可用于模拟类似下落的雪花这类随机旋转特效。 + +![rotation_module](module/rotate_overtime.gif) + +## 属性 + +![rotation_module](module/rotation_module.png) + +| 属性 | 说明 | +| :--- | :--- | +| **SeparateAxes** | 是否分开设置三个轴的粒子旋转(预计在 v3.3 支持该功能) | +| **X、Y、Z** | 绕 X、Y、Z 轴设定旋转角速度。其中 **X**、**Y** 仅在勾选 **SeparateAxes** 属性后显示。 | + +点击属性输入框右侧的 ![menu button](main-module/menu-button.png) 按钮,可选择对属性进行曲线编辑,详情请参考 [曲线编辑器](./editor/curve-editor.md)。 diff --git a/versions/4.0/zh/particle-system/size-module.md b/versions/4.0/zh/particle-system/size-module.md new file mode 100644 index 0000000000..d2484e960c --- /dev/null +++ b/versions/4.0/zh/particle-system/size-module.md @@ -0,0 +1,22 @@ +# 大小模块(SizeOvertimeModule) + +大小模块用于设置粒子在其生命周期内的大小,从而实现类似大小不一的火焰和雪花等粒子效果。 + +![size_overtime](module/size_overtime.gif) + +## 属性 + +![size_overtime](module/size_module.png) + +| 属性 | 说明 | +| :--- | :--- | +| **SeparateAxes** | 是否在 X、Y、Z 轴上分别设置粒子大小。
当点击输入框右侧的 ![menu button](main-module/menu-button.png) 按钮,切换使用曲线编辑时,表示是否三个轴分开进行缩放,详情请参考下文 **非均匀粒子缩放** 部分的内容。 | +| **Size** | 设置粒子大小。
当切换使用曲线编辑时,可设置粒子大小随时间变化的曲线。
该项与 **separateAxes** 属性,二者只能选其一。 | + +## 非均匀粒子缩放 + +勾选 **SeparateAxes** 属性,点击出现的 **X**、**Y**、**Z** 属性框右侧的 ![menu button](main-module/menu-button.png) 按钮,选择 **Curve**,切换到曲线编辑模式。便可分别定义曲线指定粒子在其生命周期中 X、Y、Z 轴方向上的大小变化(Z 仅用于网格粒子)。 + +![size_module_curve](module/size_module_curve.png) + +详情请参考 [曲线编辑器](./editor/curve-editor.md)。 diff --git a/versions/4.0/zh/particle-system/texture-animation-module.md b/versions/4.0/zh/particle-system/texture-animation-module.md new file mode 100644 index 0000000000..df768320e5 --- /dev/null +++ b/versions/4.0/zh/particle-system/texture-animation-module.md @@ -0,0 +1,21 @@ +# 贴图动画模块(TextureAnimationModule) + +贴图动画模块用于将 [渲染模块](./renderer.md) 中 **ParticleMaterial** 属性指定的贴图纹理作为动画帧进行动态播放,用于实现类似下图中的效果: + +![texture_animation](module/texture_animation.gif) + +## 属性 + +![texture_animation](module/texture_animation.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Mode** | 设定粒子动画贴图的类型,目前只支持 **Grid**(网格)模式。一张贴图包含一个粒子播放的动画帧。 +| **NumTilesX** | 贴图纹理在水平(X)方向上划分的贴图数量。 +| **NumTilesY** | 贴图纹理在垂直(Y)方向上划分的贴图数量。 +| **Animation** | 动画播放方式,包括:
**WholeSheet**:播放贴图中的所有帧;
**SingleRow**:只播放其中一行,默认第一行。可搭配 **RandomRow** 和 **RowIndex** 属性使用。 +| **RandomRow** | 随机从动画贴图中选择一行播放动画帧。
该项仅在 **Animation** 设置为 **SingleRow** 时生效。 +| **RowIndex** | 从动画贴图中选择特定行以播放动画帧。
该项仅在 **Animation** 设置为 **SingleRow** 并且禁用 **RandomRow** 时生效。 +| **FrameOverTime** | 设置动画播放速度。
当点击输入框右侧的 ![menu button](main-module/menu-button.png) 按钮,切换使用曲线编辑时,表示一个周期内动画播放的帧与时间变化曲线。 +| **StartFrame** | 指定动画在整个粒子系统生命周期的第几帧开始播放。 +| **CycleCount** | 动画帧在粒子生命周期内重复播放的次数。 diff --git a/versions/4.0/zh/particle-system/trail-module.md b/versions/4.0/zh/particle-system/trail-module.md new file mode 100644 index 0000000000..48a880aec4 --- /dev/null +++ b/versions/4.0/zh/particle-system/trail-module.md @@ -0,0 +1,24 @@ +# 轨迹模块(TrailModule) + +轨迹模块用于在粒子尾部添加一个轨迹效果,实现类似下图中的拖尾效果: + +![trail_module](module/trail.gif) + +## 属性 + +![trail_module](module/trail_module.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Mode** | 粒子系统生成拖尾的方式,目前仅支持 **Particles**,表示在每个粒子的运动轨迹上都行成拖尾效果。 +| **LifeTime** | 生成的拖尾的生命周期。 +| **MinParticleDistance** | 粒子每生成一个拖尾节点所运行的最短距离。 +| **Space** | 选择拖尾运行时基于 **世界坐标系**(World)或者 **本地坐标系**(Local)
(**Custom** 目前暂不支持) +| **TextureMode** | 当在 [渲染模块](./renderer.md) 中指定了 **TrialMaterial** 贴图时,贴图在拖尾上的展开形式。目前仅支持 **Stretch**,表示将贴图覆盖在整条拖尾上。 +| **WidthFromParticle** | 拖尾宽度跟随粒子大小 +| **WidthRatio** | 拖尾宽度。若勾选 **WidthFromParticle**,则拖尾宽度为例子大小乘以比例。 +| **ColorFromParticle** | 拖尾颜色是否跟随粒子初始颜色 +| **ColorOverTrail** | 拖尾颜色随着拖尾自身长度的变化而变化 +| **ColorOvertime** | 拖尾颜色随着时间的变化而变化 + +点击属性输入框右侧的 ![menu button](main-module/menu-button.png) 按钮,可选择对属性进行曲线编辑/渐变色编辑,详情请参考 [粒子属性编辑](./editor/index.md)。 diff --git a/versions/4.0/zh/particle-system/velocity-module.md b/versions/4.0/zh/particle-system/velocity-module.md new file mode 100644 index 0000000000..66719a1db6 --- /dev/null +++ b/versions/4.0/zh/particle-system/velocity-module.md @@ -0,0 +1,19 @@ +# 速度模块(VelocityOvertimeModule) + +速度模块用于控制粒子在生命周期内的速度。 + +![velocity overtime](module/velocity_overtime.gif) + +## 属性 + +![velocity_module](module/velocity_module.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Space** | 选择粒子进行速度计算时基于 **世界坐标系**(World)或者 **本地坐标系**(Local)
(**Custom** 目前暂不支持) | +| **X** | X 轴方向上的速度分量 | +| **Y** | Y 轴方向上的速度分量 | +| **Z** | Z 轴方向上的速度分量 | +| **SpeedModifier** | 速度修正系数,只支持 CPU 粒子。当 [Renderer 模块](./renderer.md) 中勾选 **UseGPU** 属性时不生效 | + +部分属性输入框右侧有 ![menu button](main-module/menu-button.png) 按钮的,可点击选择对属性进行曲线编辑,详情请参考 [曲线编辑](./editor/curve-editor.md)。 diff --git a/versions/4.0/zh/physics-2d/image/add-joint.png b/versions/4.0/zh/physics-2d/image/add-joint.png new file mode 100644 index 0000000000..b3cd00fba5 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/add-joint.png differ diff --git a/versions/4.0/zh/physics-2d/image/add-rigid.png b/versions/4.0/zh/physics-2d/image/add-rigid.png new file mode 100644 index 0000000000..562c845407 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/add-rigid.png differ diff --git a/versions/4.0/zh/physics-2d/image/adjust-points.png b/versions/4.0/zh/physics-2d/image/adjust-points.png new file mode 100644 index 0000000000..f5645eb777 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/adjust-points.png differ diff --git a/versions/4.0/zh/physics-2d/image/anatomy-aabbs.png b/versions/4.0/zh/physics-2d/image/anatomy-aabbs.png new file mode 100644 index 0000000000..1a656405de Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/anatomy-aabbs.png differ diff --git a/versions/4.0/zh/physics-2d/image/box-colllider-2d.png b/versions/4.0/zh/physics-2d/image/box-colllider-2d.png new file mode 100644 index 0000000000..e20620140e Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/box-colllider-2d.png differ diff --git a/versions/4.0/zh/physics-2d/image/btn-regenerate-points.png b/versions/4.0/zh/physics-2d/image/btn-regenerate-points.png new file mode 100644 index 0000000000..47f7b0c8bc Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/btn-regenerate-points.png differ diff --git a/versions/4.0/zh/physics-2d/image/circle-collider.png b/versions/4.0/zh/physics-2d/image/circle-collider.png new file mode 100644 index 0000000000..eab06deffa Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/circle-collider.png differ diff --git a/versions/4.0/zh/physics-2d/image/collider-inspector.png b/versions/4.0/zh/physics-2d/image/collider-inspector.png new file mode 100644 index 0000000000..ab870d84c2 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/collider-inspector.png differ diff --git a/versions/4.0/zh/physics-2d/image/collider-types.png b/versions/4.0/zh/physics-2d/image/collider-types.png new file mode 100644 index 0000000000..24ffebe59d Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/collider-types.png differ diff --git a/versions/4.0/zh/physics-2d/image/collision-callback-order-1.png b/versions/4.0/zh/physics-2d/image/collision-callback-order-1.png new file mode 100644 index 0000000000..cd4f019e73 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/collision-callback-order-1.png differ diff --git a/versions/4.0/zh/physics-2d/image/collision-callback-order-2.png b/versions/4.0/zh/physics-2d/image/collision-callback-order-2.png new file mode 100644 index 0000000000..cba1ff11d7 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/collision-callback-order-2.png differ diff --git a/versions/4.0/zh/physics-2d/image/collision-callback-order-3.png b/versions/4.0/zh/physics-2d/image/collision-callback-order-3.png new file mode 100644 index 0000000000..858ad64384 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/collision-callback-order-3.png differ diff --git a/versions/4.0/zh/physics-2d/image/collision-points-1.png b/versions/4.0/zh/physics-2d/image/collision-points-1.png new file mode 100644 index 0000000000..95c7a84ec1 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/collision-points-1.png differ diff --git a/versions/4.0/zh/physics-2d/image/collision-points-2.png b/versions/4.0/zh/physics-2d/image/collision-points-2.png new file mode 100644 index 0000000000..b8f1c12d18 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/collision-points-2.png differ diff --git a/versions/4.0/zh/physics-2d/image/collision-points-3.png b/versions/4.0/zh/physics-2d/image/collision-points-3.png new file mode 100644 index 0000000000..9113acbb0e Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/collision-points-3.png differ diff --git a/versions/4.0/zh/physics-2d/image/distance-joint-dis.gif b/versions/4.0/zh/physics-2d/image/distance-joint-dis.gif new file mode 100644 index 0000000000..846d1fcd77 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/distance-joint-dis.gif differ diff --git a/versions/4.0/zh/physics-2d/image/distance-joint-dis.png b/versions/4.0/zh/physics-2d/image/distance-joint-dis.png new file mode 100644 index 0000000000..091dfabb40 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/distance-joint-dis.png differ diff --git a/versions/4.0/zh/physics-2d/image/distance-joint-gizmo.png b/versions/4.0/zh/physics-2d/image/distance-joint-gizmo.png new file mode 100644 index 0000000000..968603f3fb Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/distance-joint-gizmo.png differ diff --git a/versions/4.0/zh/physics-2d/image/distance-joint.gif b/versions/4.0/zh/physics-2d/image/distance-joint.gif new file mode 100644 index 0000000000..5a661b1385 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/distance-joint.gif differ diff --git a/versions/4.0/zh/physics-2d/image/edit-box-alt.gif b/versions/4.0/zh/physics-2d/image/edit-box-alt.gif new file mode 100644 index 0000000000..6aba5358ce Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/edit-box-alt.gif differ diff --git a/versions/4.0/zh/physics-2d/image/edit-box.gif b/versions/4.0/zh/physics-2d/image/edit-box.gif new file mode 100644 index 0000000000..ab775230bd Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/edit-box.gif differ diff --git a/versions/4.0/zh/physics-2d/image/edit-circle.gif b/versions/4.0/zh/physics-2d/image/edit-circle.gif new file mode 100644 index 0000000000..6e200bef28 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/edit-circle.gif differ diff --git a/versions/4.0/zh/physics-2d/image/edit-input.gif b/versions/4.0/zh/physics-2d/image/edit-input.gif new file mode 100644 index 0000000000..755630b318 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/edit-input.gif differ diff --git a/versions/4.0/zh/physics-2d/image/edit-polygon.gif b/versions/4.0/zh/physics-2d/image/edit-polygon.gif new file mode 100644 index 0000000000..c4fcd8f563 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/edit-polygon.gif differ diff --git a/versions/4.0/zh/physics-2d/image/editing.png b/versions/4.0/zh/physics-2d/image/editing.png new file mode 100644 index 0000000000..eb5a9fe28b Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/editing.png differ diff --git a/versions/4.0/zh/physics-2d/image/enable-contact.png b/versions/4.0/zh/physics-2d/image/enable-contact.png new file mode 100644 index 0000000000..6cb8033f98 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/enable-contact.png differ diff --git a/versions/4.0/zh/physics-2d/image/fix-rotation.png b/versions/4.0/zh/physics-2d/image/fix-rotation.png new file mode 100644 index 0000000000..d65bb4cda4 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/fix-rotation.png differ diff --git a/versions/4.0/zh/physics-2d/image/fixed-joint.gif b/versions/4.0/zh/physics-2d/image/fixed-joint.gif new file mode 100644 index 0000000000..5158fef4ff Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/fixed-joint.gif differ diff --git a/versions/4.0/zh/physics-2d/image/fixed-joint.png b/versions/4.0/zh/physics-2d/image/fixed-joint.png new file mode 100644 index 0000000000..3144d1cddb Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/fixed-joint.png differ diff --git a/versions/4.0/zh/physics-2d/image/gizmo-a.png b/versions/4.0/zh/physics-2d/image/gizmo-a.png new file mode 100644 index 0000000000..a8fb81bf3a Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/gizmo-a.png differ diff --git a/versions/4.0/zh/physics-2d/image/gizmo-b.png b/versions/4.0/zh/physics-2d/image/gizmo-b.png new file mode 100644 index 0000000000..3e55f21dcf Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/gizmo-b.png differ diff --git a/versions/4.0/zh/physics-2d/image/gizmo.png b/versions/4.0/zh/physics-2d/image/gizmo.png new file mode 100644 index 0000000000..ee4612373a Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/gizmo.png differ diff --git a/versions/4.0/zh/physics-2d/image/hinge-joint-motor.gif b/versions/4.0/zh/physics-2d/image/hinge-joint-motor.gif new file mode 100644 index 0000000000..5297cf2bb1 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/hinge-joint-motor.gif differ diff --git a/versions/4.0/zh/physics-2d/image/hinge-joint.gif b/versions/4.0/zh/physics-2d/image/hinge-joint.gif new file mode 100644 index 0000000000..f66ad60ce7 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/hinge-joint.gif differ diff --git a/versions/4.0/zh/physics-2d/image/hinge-joint.png b/versions/4.0/zh/physics-2d/image/hinge-joint.png new file mode 100644 index 0000000000..a75659525f Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/hinge-joint.png differ diff --git a/versions/4.0/zh/physics-2d/image/joint-with-body-collider.png b/versions/4.0/zh/physics-2d/image/joint-with-body-collider.png new file mode 100644 index 0000000000..a3003be1b7 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/joint-with-body-collider.png differ diff --git a/versions/4.0/zh/physics-2d/image/module.png b/versions/4.0/zh/physics-2d/image/module.png new file mode 100644 index 0000000000..9213ee016e Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/module.png differ diff --git a/versions/4.0/zh/physics-2d/image/motor-speed.png b/versions/4.0/zh/physics-2d/image/motor-speed.png new file mode 100644 index 0000000000..97af421619 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/motor-speed.png differ diff --git a/versions/4.0/zh/physics-2d/image/polygon-add-point.gif b/versions/4.0/zh/physics-2d/image/polygon-add-point.gif new file mode 100644 index 0000000000..a167cbf959 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/polygon-add-point.gif differ diff --git a/versions/4.0/zh/physics-2d/image/polygon-collider.png b/versions/4.0/zh/physics-2d/image/polygon-collider.png new file mode 100644 index 0000000000..921f057495 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/polygon-collider.png differ diff --git a/versions/4.0/zh/physics-2d/image/polygon-default.png b/versions/4.0/zh/physics-2d/image/polygon-default.png new file mode 100644 index 0000000000..bb13ce0aaf Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/polygon-default.png differ diff --git a/versions/4.0/zh/physics-2d/image/raycasting-output.ai b/versions/4.0/zh/physics-2d/image/raycasting-output.ai new file mode 100644 index 0000000000..9a89e8d5b4 --- /dev/null +++ b/versions/4.0/zh/physics-2d/image/raycasting-output.ai @@ -0,0 +1,6376 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[6 0 R 7 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + raycasting-output + + + 2017-05-11T17:14:31+08:00 + 2017-05-11T17:14:31+08:00 + 2017-05-11T17:14:31+08:00 + Adobe Illustrator CC 2015 (Macintosh) + + + + 256 + 108 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAbAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FVskkccbSSMEjQFndjQAAVJJxVicfnbVru3iudP0dfQmUSRm9uf q7lGAKkpHFcUJB6H6c5XWe2GjwTMPVKUTR4R1HvIcmGkmRa2XXvNsqgxCws27hkmux94e0/V/Zqs 3t7iH93ikfeRH7uJtGiPUqMl15onH7zWPq5PeztoUH0fWBdZr83t7mP0Y4x95Mv+JZjRDqVJ7W+m Wl3q1/PXqVm+rHrX/j0Fv/n9Oa7N7aa+fIxh7o/8VbYNJAKDaDpci8bmN71T1F7NLd1+f1h5PDNd m9pNfkFSyy+FR/3IDYNPAdFTR7iDyteqkSLD5dvXVZoUAWOzuG+FZlUUCxSmiyU2VqPQVkbOy9kf aGWX/B80iZ84yJu+8E9/d5e7fE1WCvUGfZ37guxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2 KuxV2KuxV2KuxV2KuxV2KsZ893AlsYNDXd9Xcx3A22s46NckjurgrCfD1Ac0/bvaP5PSzyfxco/1 jy+XP3BtwY+KQCDzwt3LeKuxV2KuxVZNDFNE8MyLJFIpSSNgCrKwoQQeoIyUJmMhKJojcFSET5R1 WW1m/wAO3zs7xIX0q6kbk01uvWJmO5lgqAa7slGqTzp7V7O9tDXYLP8Aex2kP0+4/fYdRqMPAfJl edA0OxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVgn1n9J65f6pXlB GxsLHw9O3YiVxXu8/IVGzKqHPK/bjtHxM8cEfpx7n+sf1D7y7LR46jxd6Kzh3MdirsVdirsVdiqE 1GxF5CoWRoLmFxNZ3SfbhmT7Mi+PWjKdmUlTsTmw7L7RyaPOMsOnMd46j8dd2GTGJiiyXyzrx1ay b6xGLfU7VvR1C1BqEkpUMhP2o5B8SN4bGjAge5aLWY9Tijlxm4yH4HvDppwMTRTfMpi7FXYqgtZ1 iw0bTZtSv3ZLSDj6jIjytV3CKFSMM7EswFAMVY9f/mLZxWEt/ZWFzd29kzrqgdGtprZk9OkRhmVZ GnkWZWij4jmNwdxVVl2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV51/wArS1ONYXn0y04a izx6YIr7kVkS8isgbzlEvpIzzglk5lacaFqYqlWvfmr5lS6/RmmWMUmqXUqaTHwn52sV1It2zXaS +iXlSNbUFlIH7Q6rvjazVQ0+KWWf0xF/jzPIebOEDI0HeTdVu7p/qyQR2+lQafYSWcQcvKhlVwyu SoDU9Ola9q988h7f0UMQE5EyzTy5OI9DRHntz5eZ32F7TDMnbpQZTnNN7sVdirsVdirsVdiqBuvr djex61p0ZlvLdfTubVSB9atq8mi3ovNTVoiejbVCs2dT7L9u/k8vBM/uZnf+if53/FeXupx9Rh4x Y5hm+n39pqFlDfWcgltrhA8UgBFQfEGhBHQg7g7HPYgbdSiMKuxVLPMuinWtIk04Tm29SSCT1lFW X0Z0m+Hcbn06A9sVSef8u7CWWG4/SeoJdRXJvZbgSQlprn01hSSVXiZCY40CoAoC9QK74qyvFXYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq858u+a/If1XULI6QbVi0sc9sLSe4e5iN1LbxLI3o/ vJJnjkKxVYhQT9kE4qlLecfJmpeYbeHSYHjsdEj+rRwxWM8foXMkjRN60XpKbcRLyRS4UfHJ2Vs4 32xOXJhjhx16tzcgDLh/hAP1d+3cO9zNIADZUNL87+Wpb28ZbNra7tz9TCpbyNczCO6u4ESNI4+b oDaSvRSePxcuNDnFansfUcEf3nHGXqNyHBH0wPEZGVX6wO+Qojm5cckb5JtB5z8vz3K28M0sjNxH qLbz+mGeMyqjScOCuVB+EnlXalaDMKfYGrjG5RA8uKN0DRNX9IP8XKiDy3ZjNEqN750s42SOxtZ7 24Mc1w0Jje2YQW3D1XHrrHyI9VAAOpPUZfg7AmT+9lHGLjEbidylfCPSTXI2T9rE5h0Fou78z6Zb X+lWJkU3Gr8mt0LKhEapyLkMQTUlVCjck+ANMTF2TllDLMg1h57XvfL4bknoPeyOQWB3pvmrbHYq 7FXYqpXV1BaWs11cOI7eBGlmkbYKiDkxPyAyzDilkmIRFykQB7ypNC078m6bPY6DCLmMxXV28l5c wEAenJcuZTHRdqpy4kjqRXqc9+0WmGDDDEDfBED5B0c5cRJTp3RFLuwVR1ZjQD6TmUxSHUfzB8ia btf+YtNtm/kku4Fc9OiluR6jtitJR/yuX8vpB/oN9PqjGvEadZXl4DTwaGJ07H9rFNJZ+XH5u3Hn DzLqejyaQ1ilmjyxzFyxASQR8JlKji55V28DmFp9UckqIel7Z9nho8EMonxcW1fC7Hk9JzNeZdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirHB5C0WNJvq7z21xLIs0d1G6+pFKks0qvHzVlrW6kF GUgqaEYqway8nfojW7mxl1G9hvXllu5J0eOmo28lxJOGfkjcWjkuGST0ih3FfhZBnnPtcc2HIJyj DJil9JkL4JcNHrRurHECLHLZ2GloiuRVk8haTFcT3dvcXVvezStPHdRunOJnmuZ2CAoyFS9/Ns6t 1/yRTk49u5REQMIGAjRBBqW0Igy9XMeHHlX2uT4Q7/x+CjLXylo1tbm3jjZonlSeRZG585EQIS/K vLkBVq9Tjl9oNTOXESBLhlEVtQkb293TupRhiBSGm8g+WgqjTbVNFYB0kk0yOG2aSKYKJonpGfhk Ea1IowoOLA5DD25qY/WfFFgjxDKVGO4I35j5HqCk4onyQGtaZ5Lgu4rvVdbWzSJomkhnuoIYpFtp /rFtG/MAhIZmqnEjwbkNs2OHWa+WMxhgu79Qxyu5R4ZSv+dIc2BjC+f2q0n5peQVYpDrEd44pVLJ Jbxt/a3SU5ix9mNeRfh0PMxH3lkc8O9r/lYMMpIsNA1u+p0K2L26mvg14bYdsl/oflH682CJ7jk3 +wFHjeR+Tv8AEnnickWflFoh2fUL+2hHzpb/AFtsf5N0Efq1O/8ARxyP27BeOf8AN+11fzTuDsNE 01T4m6vmA39rMVx/1pj/AKvM/wCZEH7yv7zyQsnlXz1rOrWej3nmr04Zy11eCwsIoAkFuVIHKZrq paZkXiftLyqCAQeq9lcGkz5Tkx4ODwuUjOUrJ8q4eXPu2cbUylEUTzZiPyuSb/jpeafMOoV3ZTqB tUPSo42SW23zz0Jwbbj/ACZ/LQMHuNFS/kG/PUJri+atKf8AHzJL4Yrafad5P8paYANN0SwsgOn1 e2hi/wCIKPHFCb4qpxW9vE8jxRJG8p5SsqgFj4sR1yIiByDKUyaBPJKoPOPlq7N7Hp2oQajdafG8 txZ2kscswEezDiG68vh36HY0yTFMdOvoNQ0+1v7ev1e7iSeHkKHhIodajxocVRGKuxV2KuxV2Kux V2KuxV2KuxV2KoS+1jSbAFr69t7QDqZ5UjH/AAxHjirG7r83vyxt5PTbzNYTSHYR20wuWJ9lg9Q9 8U0h/wDlbegziul6Vrmqjs1rpN4qnv8AbnjgQdO5xWmn89+cpo2ksPImoLGoLGXUruwslAFd2pNc OBt/KcQrDPNjfmp588t2t/oFrptvFDcs1vNpmpGe4YKXhlCXDRW0YAIoeMnxe4ynWaOOSJhmiSK+ nbc9L4unV2vY2pxYswlkECO+YMgO/aN2a2Hcu0rTPzCvLMQXHmWKyntKW15DHYpJcpNGoD85ZpGR i1eSn0qMpVhUHfyntSGj0WY456YyPOzkIG/cAOXx73I1E45ckpYjw4zI0K6Iz/A1/P8A73+a9Zua j4kilgtFPy+qwxOP+CzX/wAuY47Y9PgA/pRMz8yWnwieci4/lf5Ok/3tt7jUSev1+9u7pTTfdJZW T8MH+ifWjaEo4x3RhED7l8CPXdMbHyR5MsG5WWhWFu/88dtCrf8ABca5hZO2tZM2cuT/AExH2DZm MUe4JyiIihUUKo6ACg/DNfOcpG5GyzAXZFXYq7FUT5GtVmguteYAtqjKto23+8UBZYKEdVkZnmU+ DjPcfZ7s78ppIwI9Z9Uvef1cvg6fPk4pEsozdtLsVdirsVdirD/L/ke7tfq0mp3iTPZRXkNlDDEq CMX0nOQtJ9qQ0UAbKPHkaHFWS6RpyabpNlpyOZEsoIrdZCKFhEgQEj344qi8VdirsVUri6tbaP1L mZII/wCeRgg+9iMVY7qH5o/lvp5K3fmfTI3X7UYuoXcV8URmb8MU0gD+cPk6XbTk1PVm/lsNLv5w fk4hCH6GxWnH8wvMFxtpnkbW5z2a6NlZIf8Akdcc+38uKuOs/m7dmlt5a0rTh2e+1OSY08SltbEf RzxVx0r84bs/v/MGj6Yh7WenTXDgb/t3Fyq19+H0Yq7/AAB5ouTXU/PeryCtTHZR2NktP5apA70/ 2dffFXD8ofKs2+p3Oras1an69qt9IpP+osqR09uNMVtGWH5UflpYsHt/LGm+oDUSyW0cz18ecodq /TitsjtLCxs09O0t4rZP5IkVB9ygYoW6lemx0+5vRBNdG2ieX6tbr6k0nBS3CNNuTNSgGSjGyByV ILKxvvN3lme2856NHZW97KHTShOzuLdGV4xcPHwHMstWVSRTY9xl0pDFO8Zuuv6kMjtLS1s7aK1t IUt7aFQkMEShERRsFVVoAMolIk2UsZ826TNbT/4hsI2eSJAmq2sYLNPbruJEUbmWCpIpuy1WhPCn Pe0PYsddgobZY7xP6D5H9R8m/Bm4D5IWCaGeGOeF1lhlUPFIhDKysKqykdQRnis4ShIxkKkNiHbg 2qZFXYq7FXYq7FUBqyzXCQaXbsUudUlFoki1DIjKXmkBHQxwo7LX9qg750Psx2d+a1kQfoh6j8OQ +Jr4W0ajJwwZ7BBDBBHBCgjhiUJHGooqqoooA8AM9rdQvxV2KuxV2KuxV5p+YP5rav5Y856VoNpo /wBehvljZpCXDuZJCnCGg48lp3r17Zg6jVShOgNnp+yuwcep0uTNKfCYXtt0F7+9NR5k/NO62tfJ lvZA1o2o6rECPmlpFdD/AIfM55lwtvznutpL7y/panr6NveXzj5NJLaKf+AxV3+C/PdztqPny8VT 9pNOsbG0G9ejSpdSDY/zYq7/AJVPpUwpqmua9qg/aW41S5iQ/OO1a3T7l/hitqtt+Tv5YQOHPlyz uZAAPUvFN45p4tcmVj9ONLbItP0DQtNVV07TbWyVdlW3hjiAp4cFGKEfirsVdirsVdirsVdirH9J 86aLrur6hpGkSTXDaepW61KKOtok1eJhWZvgeVa8ioBH6svnglCIlLr06rbflLyiugR3U0+o3Wra rqDLJqGo3bkl2QEKscQ/dxIvI8VQffjmzcdUBEDkAqf5QrsVdirBdTsP8PaoAu2h6lL/AKP/AC2t 3IamGvaOZt469Hqn7SLnn/tj2Dxg6rEPUPrHeP53w6+Xu3ztJm/hKJzzN2DsVdiq1nRF5OwVfEmg yUIGRoCytpe/mTy8svonU7X1v99CaMv/AMCDy75nw7H1kuWLJ/pT99MDliOoTTyjbtfatday6Ott bxiz04yIyc/U4y3EyhqVViI0U06o3Y56j7J9kS0eAnIKyzO/kByH3n4ut1WUSltyDL86pxnYq7FX Yq7FXYq0UQsrFQWX7JI3FfDBSbbwodirsVdirsVdirsVdirsVdirsVQ13qenWc1tBd3UVvNeP6Vp HK6o0slK8IwxBY07DJCBN0OSpPZw+d5/NM9zez2tn5btg0dnYQL6090WA/fTyuF9Lifsog8ak7Zb I4xChZn39yp7Bb29uhjgiSJCzOURQo5OxZmoO7MST75SSTzVUwK7FXYq7FUPqFhaahZTWN5GJba4 QpLGSRUHwIoQR1BG4O4wEWrGrbydr8aiKbXI5Yk+FJBZ8Zyo2BkczNGz06kRqK78R0zk/wDQXoTk MjxUT9N7D3UL+1yvzk6pER+SPirPrWoTKesX+ixr70MUCSD/AIPMzF7K9nwNjGPiZH7CaYHUzPVW XyN5eDc3W6mY/aEt7duh/wCeZl9P/hc2OPsnSQNxxYwf6o/UwOWR6lXi8m+UYpBKmi2ImFf3xt4j JvX9sqW7+OZ4iA12m0cUUSBIkVEHRVAAH0DCq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYqx7X/MurWWr2OkaRotxqd1dcZbi5J9Czt7fnxd5LhgwL0rxjUFjl+PFExMpSoD5n4Kif8I6 A3mVvMstt6+seksENxMzSCFF7QI5Kxcq/EUAr9JyPjS4OC/SqcZUrsVdirsVdirsVdirEvOXme70 nULO1XULLR4J4pJVvtQieaOeZGVUtIwskPxtyrQMWb9lTvRVjWree/NMmoCytzEsr6kttcaXBFKL yztI7oLHeyys3ForpY9l9NdpVofgaqlILHz75stvy9ja91FNO1ObTDPFd6jFNLNd3v1VP9GtwJIy k3L46AMatsmxxXqmr/mb53fVLnTLa0tUmWX6tDFJH6txAouFgS8uEjuPUaKYHkoMUY3FHamKvWoh IIkEjB5AoDuBxBam5AqafKuKF2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVLfMHmPQ/L2m yalrV5HZWcexkkO7N1CIoqzsabKoJOWY8UpmoiyqUa5pF55x0vTGtdVvNG0e5T19QtoozBeTxyKp SEyMQ8FN+dBU9MtxzGKRsCUund+1DKEQIioKkKAAWJY7eJNSfpzGS3irsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVacOUYIQr0PFmFQD2JAIr9+KsN1O48 wWGm+XLLU7bT9f8AMF/qLQpNKptbaCZbW6uxLH8Fy4MUcBjUgcjWu2WTmOI8Fge9QlJ/N6c6Y+qr pcIsoljilgN2Dd/WpbNbwBIljYNCodQXLBqcm4UXetURB+YPmybUl08aJZCaS+j0qNjfy8frL6Wu rMW/0X+7SEstRuWpsAahTSAu/wA3dSuPLia3pun28NjdQqsJuLoC8jnmsPrqN9WEbI6AMP8AdgJH xU44oel2kjS2sMr/AGnjVmp4kAnFVXFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FVKb6r6kHrcPU5n6tzpy9Tg1eFf2vT5dO1e2KpQf8EfpJK/oz9J/VG9Kv 1f1/qVDy4/t+jStf2cVTJf0V6w4+h63rbU4cvX9D7+fofTw9sVSmT/AH16L1P0V9f+rL6PL6t631 Wnwca/F6VOlPhxVP148RxpxptTpTFW8VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir/ AP/Z + + + + uuid:91432569-e28b-5847-9173-00da6ce9256d + xmp.did:9c6f38b4-6510-4b2b-9f49-88e1e65bada3 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + uuid:ac95c21a-360a-4ecf-b50d-18deafd85636 + xmp.did:3c521b3c-8e17-6442-b5cb-0001c5ab3e1b + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:9c6f38b4-6510-4b2b-9f49-88e1e65bada3 + 2017-05-11T17:14:26+08:00 + Adobe Illustrator CC 2015 (Macintosh) + / + + + + Document + Print + False + False + 1 + + 960.000000 + 640.000000 + Pixels + + + + + STHeitiSC-Light + 黑体-简 + 细体 + TrueType + 10.0d5e1 + False + STHeiti Light.ttc + + + + + + Cyan + Magenta + Yellow + Black + + + + + + 默认色板组 + 0 + + + + 白色 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 0.000000 + + + 黑色 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 100.000000 + + + CMYK 红 + CMYK + PROCESS + 0.000000 + 100.000000 + 100.000000 + 0.000000 + + + CMYK 黄 + CMYK + PROCESS + 0.000000 + 0.000000 + 100.000000 + 0.000000 + + + CMYK 绿 + CMYK + PROCESS + 100.000000 + 0.000000 + 100.000000 + 0.000000 + + + CMYK 青 + CMYK + PROCESS + 100.000000 + 0.000000 + 0.000000 + 0.000000 + + + CMYK 蓝 + CMYK + PROCESS + 100.000000 + 100.000000 + 0.000000 + 0.000000 + + + CMYK 洋红 + CMYK + PROCESS + 0.000000 + 100.000000 + 0.000000 + 0.000000 + + + C=15 M=100 Y=90 K=10 + CMYK + PROCESS + 15.000000 + 100.000000 + 90.000000 + 10.000000 + + + C=0 M=90 Y=85 K=0 + CMYK + PROCESS + 0.000000 + 90.000000 + 85.000000 + 0.000000 + + + C=0 M=80 Y=95 K=0 + CMYK + PROCESS + 0.000000 + 80.000000 + 95.000000 + 0.000000 + + + C=0 M=50 Y=100 K=0 + CMYK + PROCESS + 0.000000 + 50.000000 + 100.000000 + 0.000000 + + + C=0 M=35 Y=85 K=0 + CMYK + PROCESS + 0.000000 + 35.000000 + 85.000000 + 0.000000 + + + C=5 M=0 Y=90 K=0 + CMYK + PROCESS + 5.000000 + 0.000000 + 90.000000 + 0.000000 + + + C=20 M=0 Y=100 K=0 + CMYK + PROCESS + 20.000000 + 0.000000 + 100.000000 + 0.000000 + + + C=50 M=0 Y=100 K=0 + CMYK + PROCESS + 50.000000 + 0.000000 + 100.000000 + 0.000000 + + + C=75 M=0 Y=100 K=0 + CMYK + PROCESS + 75.000000 + 0.000000 + 100.000000 + 0.000000 + + + C=85 M=10 Y=100 K=10 + CMYK + PROCESS + 85.000000 + 10.000000 + 100.000000 + 10.000000 + + + C=90 M=30 Y=95 K=30 + CMYK + PROCESS + 90.000000 + 30.000000 + 95.000000 + 30.000000 + + + C=75 M=0 Y=75 K=0 + CMYK + PROCESS + 75.000000 + 0.000000 + 75.000000 + 0.000000 + + + C=80 M=10 Y=45 K=0 + CMYK + PROCESS + 80.000000 + 10.000000 + 45.000000 + 0.000000 + + + C=70 M=15 Y=0 K=0 + CMYK + PROCESS + 70.000000 + 15.000000 + 0.000000 + 0.000000 + + + C=85 M=50 Y=0 K=0 + CMYK + PROCESS + 85.000000 + 50.000000 + 0.000000 + 0.000000 + + + C=100 M=95 Y=5 K=0 + CMYK + PROCESS + 100.000000 + 95.000000 + 5.000000 + 0.000000 + + + C=100 M=100 Y=25 K=25 + CMYK + PROCESS + 100.000000 + 100.000000 + 25.000000 + 25.000000 + + + C=75 M=100 Y=0 K=0 + CMYK + PROCESS + 75.000000 + 100.000000 + 0.000000 + 0.000000 + + + C=50 M=100 Y=0 K=0 + CMYK + PROCESS + 50.000000 + 100.000000 + 0.000000 + 0.000000 + + + C=35 M=100 Y=35 K=10 + CMYK + PROCESS + 35.000000 + 100.000000 + 35.000000 + 10.000000 + + + C=10 M=100 Y=50 K=0 + CMYK + PROCESS + 10.000000 + 100.000000 + 50.000000 + 0.000000 + + + C=0 M=95 Y=20 K=0 + CMYK + PROCESS + 0.000000 + 95.000000 + 20.000000 + 0.000000 + + + C=25 M=25 Y=40 K=0 + CMYK + PROCESS + 25.000000 + 25.000000 + 40.000000 + 0.000000 + + + C=40 M=45 Y=50 K=5 + CMYK + PROCESS + 40.000000 + 45.000000 + 50.000000 + 5.000000 + + + C=50 M=50 Y=60 K=25 + CMYK + PROCESS + 50.000000 + 50.000000 + 60.000000 + 25.000000 + + + C=55 M=60 Y=65 K=40 + CMYK + PROCESS + 55.000000 + 60.000000 + 65.000000 + 40.000000 + + + C=25 M=40 Y=65 K=0 + CMYK + PROCESS + 25.000000 + 40.000000 + 65.000000 + 0.000000 + + + C=30 M=50 Y=75 K=10 + CMYK + PROCESS + 30.000000 + 50.000000 + 75.000000 + 10.000000 + + + C=35 M=60 Y=80 K=25 + CMYK + PROCESS + 35.000000 + 60.000000 + 80.000000 + 25.000000 + + + C=40 M=65 Y=90 K=35 + CMYK + PROCESS + 40.000000 + 65.000000 + 90.000000 + 35.000000 + + + C=40 M=70 Y=100 K=50 + CMYK + PROCESS + 40.000000 + 70.000000 + 100.000000 + 50.000000 + + + C=50 M=70 Y=80 K=70 + CMYK + PROCESS + 50.000000 + 70.000000 + 80.000000 + 70.000000 + + + + + + 灰色 + 1 + + + + C=0 M=0 Y=0 K=100 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 100.000000 + + + C=0 M=0 Y=0 K=90 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 89.999400 + + + C=0 M=0 Y=0 K=80 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 79.998800 + + + C=0 M=0 Y=0 K=70 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 69.999700 + + + C=0 M=0 Y=0 K=60 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 59.999100 + + + C=0 M=0 Y=0 K=50 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 50.000000 + + + C=0 M=0 Y=0 K=40 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 39.999400 + + + C=0 M=0 Y=0 K=30 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 29.998800 + + + C=0 M=0 Y=0 K=20 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 19.999700 + + + C=0 M=0 Y=0 K=10 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 9.999100 + + + C=0 M=0 Y=0 K=5 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 4.998800 + + + + + + 明亮 + 1 + + + + C=0 M=100 Y=100 K=0 + CMYK + PROCESS + 0.000000 + 100.000000 + 100.000000 + 0.000000 + + + C=0 M=75 Y=100 K=0 + CMYK + PROCESS + 0.000000 + 75.000000 + 100.000000 + 0.000000 + + + C=0 M=10 Y=95 K=0 + CMYK + PROCESS + 0.000000 + 10.000000 + 95.000000 + 0.000000 + + + C=85 M=10 Y=100 K=0 + CMYK + PROCESS + 85.000000 + 10.000000 + 100.000000 + 0.000000 + + + C=100 M=90 Y=0 K=0 + CMYK + PROCESS + 100.000000 + 90.000000 + 0.000000 + 0.000000 + + + C=60 M=90 Y=0 K=0 + CMYK + PROCESS + 60.000000 + 90.000000 + 0.003100 + 0.003100 + + + + + + + Adobe PDF library 15.00 + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 9 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 960.0 640.0]/Type/Page>> endobj 10 0 obj <>stream +HTn@Wx3;3J)U,q@4- gƻvL Pw͛c^֗=UE؝NGz?_}nR1'g mg0<L90"!4Q|\Q0EBm +KSc 5Iܔz_UFkb*,129&p?bT£*|NLgfBÚ=\l\CnZ-}Aup+:}a%_ +Uֳ_h&kIyBH(LKD`]&0@8OZpc*>:\\pMCI?TZ@4gFoN'>&bsaU-"Bb"q +[uR/&'~> endstream endobj 15 0 obj [/Indexed/DeviceRGB 255 16 0 R] endobj 16 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 6 0 obj <> endobj 7 0 obj <> endobj 19 0 obj [/View/Design] endobj 20 0 obj <>>> endobj 17 0 obj [/View/Design] endobj 18 0 obj <>>> endobj 5 0 obj <> endobj 21 0 obj [23 0 R] endobj 22 0 obj <>stream +H\j0ໞb!x>tNƩ*CI3`_*M|AlS)7M8φ)p9(M#6ʢ(8Q +|Cإߍ]G0W -A COPW qI +m JJ!aAGh !Ci d#9!]ė^h舼YRN㍘Z>+Z@ endstream endobj 23 0 obj <> endobj 24 0 obj <> endobj 25 0 obj <> endobj 26 0 obj <>stream +Hj`_`N endstream endobj 27 0 obj <>stream +HWkP>-eeEA "Adw#J] +^ +-:EemNMюMbB23Ldұhd줙vbf} qN'?:Ξ I%R>@ڷ7)SԮ={ſ!R-"04EكC%;׼=H*ѥ$k11'oǷَtoOgdO!.c6/q{ddgϺ|lCy, l-n<5bfJJXܮ((a+j8YနFm-5+X+[Rx6/dS2'('y#u!-, '+h0JkbW53 IsO3֫_ r%̃Uح#.1Boz \?|aTJ5{͜eCWƆ8:aw8%DXie6S8j#h`NGR!EsMYӫ?>uHp̣[™3~t}/6yAMȵv + m*tԴ(:ڮ%L]]4x|! h ]̜-{{}>$i=[YY8}T櫖clr:%J*A݈f|S#ˁϕ^Q`H$D +E*Yhcʥ.BNRe)h̍JLJfd!f (w3H +EFս+Zt#Չ[LĊ^^߼TE?f5>{Ui۸'O)B1' V*e2cS8`0;[\۰}Wo-{ڕm=vvm*L\:l1'OF\A;O?Ѕ}WPgz}x }ƹh+jD5-&ɨay׏ zٽ_췁##E|W/,h HQrJUݒEռnK,8MkE|6J)?MkZv]|ފ +`d_ r\R@ }A8wRy*3ݣM*cwP/S)N4`6EWdw~-a4?[{ ZAؼD.~c_&7/p2ᧁxM +/ .RX'A\9Zg;yyT$OY7&/eF*`[ht@^Zy.0si0_"x?)KgCױ~(j`Ppޤh5|ED¶T y䀤KK(S x9E3t܉-pa?o7uaٓl])QHHI]eɖc;TS Rh)UDQf߮ +]wsf sΜ9vdsO%/-ؿAv9IV/PL.>7d{Ss?N KzNӯ>Gs$yY/GXm&,ehanHxa|q4-[pZ:=|{`.e}M~$n ?ȳj\]=T),E>§}Z5q1 +wuȞ[~A  x^߄NJ_ K!;՜"~Mc2p߰ȝk.G2\6ks!|Y:WSW kX<:ʬq{׎§qw-a܏qo`cJљEZԢ:ʵ1_M7,zzǻid>}dܴ y/}޹~A,u6 "|meߝ?c✔6bo 2΅m8v>b>6k^G(<{>|of|+-Z"ӻ<ϳF=T}Uusu4Ym|ܺo=dΐt_o[eӵu⻊sg6߳:~$'}`(uZ>Y{JbZ>q^[3ιg&|hg}617iC;r[[e3SW9P<-k uw]D,fA"c [k'-fRfS'̰V5X/;EWݡue\/-jU\ 1qS~W,f vjR.=s}\<5~}U5lX"Cu{o[ Xyw=0-W@bܵmtteX鮧>>b1X>x\gcKuabX¯̧=R5>?o׳,f k˺;ʥ,md8'6ރKͥr`Qy>yjs>kf+d`Qw]T5IdX/vPCMg_3i۴16_xY֏#xaw~,e&sw]UV]hf6:'pa6O7- +r&{߯{8@a^g;Cj-;U>:aG}6KAXw&6̱n}{w-^'ż켎ZضENbd-i"~WIag=?'iqgp[-ݵM-fPS _]͢uࢊl5wli/w[Q?(Ww ](U4whphqn2'vV//Yw귺S,%Ւp9~n5'󸟥C^16\*(} _fSިz}jqorʡ,߱'ί̛94~C- + &{e1\Ǭoy\Xۼeћ_رe1ˮ(\γ_=Rػ10uY!-k$<֬~YԨEm[w4Ϛw,1|m#9bqG\fB\A^-mgs)[.Yys7"6 [e4 V3~k1~MbQ&SXycߕcKw%^{f:]qEJ٢_xZotT טk o^QREM5*v-`;;gB>2F+|XZRM]|gEj .:.Z ޶s]wbX}A>x>za=lبyBf}=ĮyWnX%-"PZ ųzCt7|njZԼ]F3e͕,'/> ,zfߩ,^p']xrU]O٢{Vբg^MVO+ @;m-aU_aL ;5:mQ4K8v^_˭ +贱9 BH !HY1ƍNq'mLp^w[w{>^w5!166 sniwsrQAw%'u=Krwmkwޖ?==aƢYq)uɓg"Gf䢿SE%=:%';D6Ⱦ%d~נ׼(oxۈ70Vȕy#D %O}|]2_|?Q\W.cM@|-cT6|wkb|&ryªb +Oo:1D +}"?S{u;XUE֤6_뮶*Mo+{WcZ_R7igYWl0u ۯZsy +.gEv&}~ ȧ1922Ze8Ʒ +s):ߠE8zyܱ)v=owA9Sym]O&:|7>1F^}09>tйZ(^e_YϪW3 %75]>~E~\#_QK5.x֔=3ܴ"~k`g{ x+{т"KZǶ&v*uRimy)9yD<.8&ʖa\ù*q<2; Ʃ>{w]Wt ?">E8ﻵT5% ?%fI_{%6oG߿Tou5X9tgx{ÎOs yus?cKۼq){"q>Ǻ Xl|.dw}_#.vQY |\:j{徴eVȕo?T\nme3]+Wi }k#y!W0%g򣆢{N\֋YwQQQ{rO # + Ƅy享^gYc{rQ_7-mggjcl;<@9-wy_>o!)yJwDN/Y:ls#匱WĽ&]qʹEnw]9b͐3Ŗ2a˹Z7@9w6y/?+l4]UΝrg Nqc?~4W!%xߵx+XGO1P1A|f#y=˺uLP9^WBK=BykiC%{)aB[rt@B ωr1+iSs9g} Y%7s󽩬Ǘ}\yHE[J,X)bz-`NΈ@]OW\# ??TbJu|L#vSO&8 fWJL.ܳB׎{JWa|ߛ8^?r|*gacV\[ b1M<申:!O7x>c1WO=ts^8ǽ*l_PSܚgEm?g_ۛ9F5Omârn"8fml9D1+dZ1o߇["cϗ}II/(Ĉc&+m-xV

Ul 1G\ICɵq[^({T_qJy v?euŷm:1d %ERDQ%QyiIL.hn]Ձ~Υ|3 ]==yV7_ӽ;GjwC~9"!J'x;<綐m,:F?)ga 6y ?R0+[e[_'UΑDYx^V|.(jqtE'=[4R浱wwy90Ce_ `ȘyE[=ttm1[KYe48obCXme^8V"r쯖GǮ| +-+gkuu4 +:ωr:V4.>z)mqݽqscq<=/zʞXbFg5yuvOCQUe4U71^eEl\՞ΉU@+5ǽ~ggLWz>YWSenx)w:"7ڍ|r_Q*9J]J쾮J0PJ<vOUOZ^}e^p/dkp^&8î 75?繈GJ1K~RʞζrOV +63d։gVE--xn/tWʺ%SΘgRqgۖ8{sh=C9?*мKų1w}}%D2~ϹYx¹%k)K8x?†!Z@]].Tb1(PO9ks{0u:7Uۺx9FC7ػYm+y rXϐ\c{ƿPC10&:辉.| O} 9/u@>8n?߸MƳ5x9zQW?79nbsxkIJiz.{ ]z\ػn>⃧a r z3;p!<7 +28G2&2q(qONÞAJ<w;PW9kyPJ`ҿk 糡WE_bPbKe.)kOL"[t^.rvAmcׯy2etysvmgs?Q Yy|?/8Saw}׿s$zY֊g}εĽ&6z/w= e{;QŲvy| 7ܙw3s}gYCcy|6XV֭=ZFhL~Qml^z~5Ŧ}ljr;n V5Aw1<997:V5g\G 79V'vOFfw_%]q]ٿ:>u:V ++ +z 礈{s=߼a2qgh;D|u)qA ϐ5wFzM X\7\cʞqNfrms[za&E~Ah(@Gcd?cbྼqs5nM% Fw}o[ʚ.>YEGπGk}]7?,x5iz_- Zjc>K&Pwݾr .[Gu\uot)!vA*:>1t/qI#l`{ĹEBgf +_n*ww"fׅ}o+|pܹ<y~!cHaq޵W8xbg}^Ct`'Jq=)lWbH9qw;loW9k\9ypsYΉ#;}B3֑u/1e=(vUSMm7v~Q7p/)P:x ΡyvK9_Yils5|;4>i.r\\@%#zS榱>~+q{Ro!/zIЩ&N;ö?,!)qr[ ȺP֗Zqn ~a]#ͳӸ^p:=B_ +;{LzOc}|)>!X5.8X<-zl` {]Ů<^dG },*󲋝5|s=ϜłFĨ/Aw~9j,Y܉J U|8_U!OOSl+Q:Qy ]^p3#l Ĝ]Bwk%nm}M;s-@1E|;DZ{>dyw叕{;B˭ѫ +B0$>N۱;vdL5Rڊ +B-1Ky^֞X{:wC=@:|+P7BȁW=UЧ>אo#Ϡo<2oϦ-|n}|O97v)?U +|`:YÝw)\r]za"wVxХǸցFcg㲮԰uLsODS9QQߟC %hohu}ј%ߏw ͑ dOĻBb97=cgNKE/. +ʽQPuFlGJ:GO.T}+*{}\ş}С; Ϟ#=.LN6[T{AwP|99'=%N(O8ozƗkc%Ƚ^X;qw{1Ο~Y62w*ʹ9zp>\#]K93ו0{G?y8ƦB粇5y;?ʞ6Л}i>a/%xnJǶN6 %[߹2=;Q1C#'2s@ \%!p0$]?@ιu{ |]FW='-O!]'a\Nhl_d;7~6uyy*{C/8s[Wccp?=Vseǿ;맺[J 2y<6qv*T ?9[фrO#utʝ{8GEvaw[E:gY>zu r/r/t}λȁo<.'? e>̋8; 9EoCG\Z%2wmz߯?lYoSmPf)}>.}=bCdw|t/o|"6?={SDžͮWxd l9P}%frCzNYg}rb–z*ۅuO^Sb nu=bwY[[y-#O~UA~3޵} +kxB"FH9O7s{eߴ~a9r;<;g}cxL v~GNUW<{J0TΔƤ#)?V%ζ[+dSk뮏ƨ'YB.u==\A=NaM8}KbL\QgFَeARkEN3T9(rE=xU3">Dϱ2W:ʼ.O_ב=W 6sĠiW8\#|Us[(zT:-dtm??seޗx9%^^9ryG96S1%=<[r-t;$vӵ7%=Ɨ7woC!r~}O\({" B^oeB`sw=8nϹ_y}ϏzClO)3}*D=| +rw05gDqVCo\@*Oˎm|H#dqL9SE{>2UϹƎP%pݻ_z2Gg +>7hGq-1>6x0^l=uxVފצSSd~s`ާ7|UwٶK%Ứ5(UPYJ,">oyČ"&5v=:Tւvq[<xgrscd!ޝ#׵ys'|9*zlSh7y=XYqvʯgiN{xg[Y>)^ s ÿK`r-öǜNk{nwDO@}s_]ue޿xM_nYhw cSδҵ`[p +ƫ3eo>gqwQ |_Sb.7JLv -w*k)2K*pt$/nVI$Ui(UJԃz c@`/`e#2<'4UꞾٜ/c!4j379~w#k/uO豊_Æf3ϼb-I̙Mb;gQ)vA> =7Q䙹rOxvKY/9wv5~p8 +أ;ٴQvL}˹zxGF&s~:&ytN> 73)"jXꟼȻ;{c *wcyo&xߍZIQ8WQޞ7cb{|_mٟFQ(<҉ړXen]# +oz~(Lg _ZVyxNG +QZoist*jvLnW_/w=u7=)e<+yf'}^nk ^Mw6_4]|6E,wwoM6?,1ZDqw{[ +w<9S s3p)gR}d[}R(iƯLT͈ o]tDq~ڽU6||Ũ~ ؅9#x܍){X6z̠_+g5FϾS|/9v̥Q;$^zWwgI#chs֦uۍŸꞓ^&(Ç|÷|_::r'qj6 eCGaFo9c%$'-;~& Sn/ +K|}ML|)r&jl]g[(gὨysKI7 +(իw-^^ Q^s!}|:a9#ao[CGg1i>𥾘RQ6ϡϐ5sǚ~Y/wbWrwt_D=tquoKut}O\`aIbmo7Yd- ^M-ޕ9,.Da3C5wDaꭨ2؅zbu&nb2EՀiapgo˻Q9ߦu^3NTڈW']u5pe{9ߥ ߜGٿ5ā}Ð;wpv3kQu疐N|KY1~T cME:-Eb+:g^{O{Xgo} }Aq} dXQX=0n-j;s6;vѤ@ypoQxa> +gr\SdK0ӜذeDaR̷qDեoMFչ{Tsn/<[ffz=jlN~Akͳ8\5HrUkc=jcMV7jӧ݊:d92/]|K2?wϠC/'Ga1{csk1*+w§Q]?GQ=U7x=Dm? +';kSэu|&.0:~2Jٙo@gן>>wlE2"ϼى®{ ?e.ߝ-è> V>ױuG5K=흆OS>~lww<Z%ֲwFQuijj߷O?NN; V_T{5lϬg.wOUSw0' }{2v2ܱ~px=G,3ĶEE {܍ߏlEQsFݮ0#(AT^1k y~TX l< +k?E xm(H;O~SQyemN̙+CgND>kv4kj{/eT-C|.>wOy̻ 2.sUdgqڔ5H?YKÛ;?|θݝţ1>GK`x34*7(L}Cwr;\1rO= t>@osM5FGQ}-b&^ˉ.:/NmhYNyGX!1+@/{?x#Cu wNG'v +Nok +eu:w.{WO>=p<7sx6z=gEMx|&ZXQH5-fLӬ۵d_[1XOSx3^NwS,9Pbr5w99żvN_&n;YI_[/3O+|&l6o5`ϻ'ܕ mGf}t>3ɮ+K8.9sfBi;!< :;g1nq_O!Α_<2rn ɺ}p|t./}?8OqUX*G6v}o n݃>1x-fUcBn ]lodp_z6wSE(G>WחOg?͗9c}ɾ7o2'|r +t8SnStu +G}oǼ-X֙Eu}\ sxT9fmiz/#E/w + >Yuâp 'I{inZ__՘S~iWcVmٝR~qlɦw8%z~dYԪ]< +/#t-j'ȹiQS#ClY#O{{=ݳkzufWitS/9dE/z~fQs#?9L<-z-rKF y[C5￰'s'{Aҫ}c;KGL}dwϓw>N \C//KV(,z%Wոtr&:9AC8lrd'>;-kmb6Fwz l#zXwr~vl1{ʷf>yJ9LYb6S-݄l>٩ES_vhO,ȢFcud\IE}zD RYX3 m^{w9w'1/^eߎ]㉅[[L8TՇV oz&w=C2,$.W;xܧ N-rrk8zaSpגoRMt߸/)O;)Lhg3ciיrEkO9E9\9 Cl\HݦENY/y[J ,Q/!o[䉖ET6Eqw~ՍdsV9@o?Ƚ'{@MՋ+XHv547 cеScŞpIyƻ&q'~ˊ0V=:h-c'ޝ;cgu=403/ !$ Rk%?y(sԩ嫯?gH>- +I#sIa+'{Ss|z} F:_:kpE>&?3 ^D=c|w=iq:k7,8mGKopﲿ.=5JZ#|>QӺIk69Gݢ/|u{"nO,#RY~e]I˺<; lْmiu_k,IU\dB^:>N3xD~Z)z|2^lpylYm<'u4K9O|N3GqM=s\нoށ%̗ l==~ׂGԲ[X s"\snϢW`2;ə-rme7pu]oVm (Y4 +>ZGɱVёm&ck,JZ+(I+Tva% N$b.8~I#mWd뻗O% >!q?;ʂ_̂kTB$#B˯.^?p;k<_d yCv/,5 ]Թsm^zJ]bwZ?虜~,'KQN,ԷWBRq[׺܂-|[/&s'3zbF|~=Z>N`]I7V"w:Ẁ2W#Z;=˶b׊/</Wt nwl\u9yoyF|)_B{ȻY?1NG҉ψ'Al'NR "םǺύ_ʟ{9H3x;&r 3i[8bݙK\e Ƣ&;-4v׊ܵH~lJ\{.} J,?s?2_#'?iHn:zKɵ Yk#ZJ̱-fE-51fvBz>|yhЂR|#9$cl/kŚ|Nk6VkߪdYYplhm"E_+,I]v`܂SKIqjdm1Sv-xDYwv?Kck]/ſw .qk_Ɇ,,f(+xXێ,֩0Yc ;gRbu߾m\v=p޸nf[_q{&,r@KQ !fiyY}j[9ڱW{Sū=ji]j^B i-亼4J=({ՑIE: + _j7G`|E }n6kD8ϸBw&ߥ{8Jng+WZܱcy(? +}wȦ"''zf6k/km|F'߇y<}o|=CX1Su0K߃Wn3NǢ7,pٔ ݫgGrlYlJ*[eql"3ёß] |+_tStN=Y܅ls|K[AվАmm}ocN,^XVբKlO0[注x>سG|gd9Oej7M^p7ӽehZ^[|?5SmstG|iZR$qu)Y1i:1]qE>W"=Hߩ# g3ZwMM/-Ǎ|tݎMOS 2 gKyW~y_%ߕ5]Po`.yjͤ=w|fܳb%>_77k=H,c3bVLUKۜѰyapvS3{o{80+PֶS(:Gn,l~c. =z97jzG5w7p&[W><|ǒEVRlzlZ`aE{8I=Ժn97xk+kTz]3Q#}:b#roeXvDpOͳЈЄM׈vwUw{co[o 7w}ys??#=mZ!nS򊜩YBEN,p:xKnyGI?Pgx0?l|k1s*s\+:(7DY=";o%t-z(]tgQgE'ɯ},XNWqs\b^,yj[<Š&_~y[뚲ΕtW%I-}iK~V_U-旺[H˂KRE0s^{̆Y.jK&]i9֮[6\3KfvW,pc@w8E0$Pw3#ҧ1~o7cua,fVꭕs .jy-꺟}aߥE[NZ(ܤεR'1[*=R?Mԕ |/R Ao51}u%^u L=jݝb56 bn.ٹP &~ejk;G)0TpsZ6F~58II{='c#Ft/"XP~RR> ,/=}g1;ÆƎ,ͮ9ws\z``6 z8bA0wEYp:XB_k-jүIqnk[ϙ׉sKy}=T^fxZ$o-][ <{3Z׺gc _n,7Fou^{Qk?sߒq+kXF6"J|ݮGE}_RؽM񽗜~^db6OqzԻ|oAg nyj{Me!#Փ_%?+ͭt|l?i}K6X=gYvӏt֍E.foVwzHlX'6]mտ|7ua/u޳ͫtxE#1Wʂ+=j' `X}fͦE]Zd{A -6-8eɎ̜90`'_0{AO/Xm3M%m.Y*KH{T,fOfE+깡4 ++k 49 m%z`[ L,fb޶Z=m%'e33aۂϔ"QUL+ҧ{Q#YwߢZoa3wj5U>ul?M,8Es.kn;eI'rHoAkgVM{AzO㫥G\[bN"m lx ,f;>pשbGd`rXz{k{O ؝b~%nojd!iaЅTԝ\׹d7-0U Q QC+>+'9̍|x +&2u,꧛r粷3ym=&{ثY5,r$zƾE~ 䓎V=J&X4ӹe6NKb_:Wu0FZNW~ȹdO~C4$s!+]3igg }hd[U }ksTz/:Wt?8ӱۘ!$rHv <$"OH1OXtSִAߤЃ{ӚGE_R)הkz3sߪ$c E}^.0\䮜 -sݷT `܉E:)>Kҏ,8;lxB.?~Z+R仏``xcrm"NOlu?.jwM Fu/Nv_/l_sZQgۭɢN%z/pI~)ka_pv.gq|;OW; Gk;̶g}ܾ~H1~| ZIUodgo5՝1uuclIscY,A-ɖ,y(.]UE7A6lذeo /2%z!y8x̬뇢Oe 'Y4g~nY˽3l`%6:mW۶\0S\:S9+cj<C4RZ۟ tVO˻t̻Vp3ozǰώ C͑WϹu|,k;+~Sߥ^uwXfDZ(1|=isVʽarc|s9w sͷR9;7&G'39<;Q$-H#-:g'J |1ezT<ߙr7&n[V#$z\7ʾ;sܛOmӭy~!w. ce Y 3S򁲖; CN:MoMUCbN-em9ukw:ͭWĬ3`Kro̪ce-X#o\E1#% +y_Q,0K> b3ɃyXmS3[31q/lq|m^(gڹ->&Eֽ+t_XFSMe}͟}ǝ }Q9}=PY##DYWZ} .;fE*s:D~;jܠ/"ܿE#*c?rj{J\sayJBu:7z^~Ic-kl :ϩ'InoOLGJ:'y*MGYO;;A39N; X?VsĜ*29C3#LwRpΞr~QWGCor?# sW}yqߎm+(zo;w۱{lw +LFʾ2er}A|^Wtėk֯eY˼%x\꾯]RmZf6 +۵<>#%nڞrWeossn6]:R9~sQW&ֹCeo?W%|nge]'ȓmyٳnx̿mi=:~G}){ۍ'aB'Ză`LU%&ZNG[`N2R aC7`k.q! +dfוy{m^){2=sU1̚i@b +>C|௕_CߑoQIBR|p <3ȏ]%پ2u<ȯ w~w45Rκ_(1x~a=׍֋ܡ2XQbްai,<֕}(|ό^ о~Ŷ2m67?z(Ϲayw|tL C7JB^3Q0.ٲ֝\Gx<& r :ږ}O`wc-ΝLM;zs۷f~`7=޽oa}y#7;J41 7$?rv=}xnU[K>@/f]d[ٯ2oا"7)gc=g>zRl(w9ݐTqVlgfD0w3, yQWolпrb{ȝf@zG}?o?cyAȨvwŇkz"!:;ݩð!sl*e<cagSx2ˮyr踅Q/+Բy3XyĘh؄H7Л>2UXd|]1ze^fya19k9*8ߓbe?辮0v'ZqEgj:SkeĹ17w8*vCM%a&3؛3G}:mǜf>~%f%fP9WוDo?S(@u 1Z7჊*[Q//l9]9(fc=󙖲 g}Y+*1 2q2[8WbS`{m_֕=huGZ5Lhl?G5Vs;+ki3>ϗqO?/~8}WɿcLw:0 c[h-)N;).(Y-ɖ!v4E 4hk5`D y; +{O!_,{d=$V_^9Fܥ}fU7NcFy/=,r^7\X}|g1G&)>$wlQw_KO~,;%{ ,ݵ䓾d"{Y܈׃?  \NfzO-u7zFO/CDk K~3nu;?H6vOGѧCrƉ݋>o<У8y\f[m]I`&3K*z3seķ P=;@gJgkbg;c ̕1zA&U .WhƭoB^$ZJ>Z` ė LѕnX+l?#y!)ɯHHo?K?1lJ:`gU< =wCp7s)#?;N ,?:]{$۹|P$`!z 忊ûnQz^DuɀOiJ߾>I {(9y% LN7,d8yǢ.uzfEWwq{MآWy^& }?5P~EoRRْt$kHWKoɗ7ҡbA#v2W}w ڣ6ֲ<{mNp3y@>4~́nd"a$>9s-ꓸ/}^٢-lKz?L0@r->Jž;uuj!ɝq|`KzOGTlm:JoQ6ki7y/-QH"6&8{!{9N0ř~7,y%ۙMZ71wէ;ŶteΏyWBWz:d{}YÌi;+>G8Ap8<0zM^4Kb˲ѭOfɓbF I.=Bl䣩1v:tZ`b:ۖ]HGqt{3\<)?īi{v ]'3== }yE/{((xehQo J~SKڙI<+2Nq&ߐ|vqzߵֲy&#]J,6;&)(s LzMn-zm[2_C5tiSW1G~\HfNwqP =.,r{עFZ-p|g uf(>t޵-v"g5S7ɇX=~|=yco`\.f'}:]ƎTŬmH&՞_sp[W7-ǂ+ ;@\Y̚R%KX}dgb~,ꊹ|;;G&ҁu 3aQZE6,ꥣ?[@yĖ1 iK\qlY`Ec$fBsĉ࿜Go;W<_suy[N:`NzWm- ܋>GTs!uNq]M2dc=~4oz ؇݄R;-?'bV[_obMX˻~M޷;1 ِklL[F|=٫e`+?udPg\J?Y`Z+xΑ̠I=9O~VO|u۝Y=ضbc$=³ē] bo^F2=_n.Smw@9IX6D[!߉i|D}Lݩθo>8N3}+}D bLJ\[Ҫ|le`6zw\{Ȣ9<-zU}m73eg  rȼ%9OσOKb+ʿc$xo fv[oF;97zV Jw] u#bv.dޭ-\bF]T$``[FP=}!A~7,QO<u Bllw7ٌ; p bf=*H;? p6g=mj5۝u}<,fH2O?v-叜+'zGIBgA>^0 +2?4Mc!}+DϜ Ŝ]E>Z`鮿LWϲN߈'X(q^[L7&_l+h?`^Ulw:[9̌Wb[,pvoR]vwsŒԽ?Gv5Q\'6 I@Budny~}[atw_W>V汶\{S< 2_OprY`G_v[]|vӫk%R>UrD_ N1[o Oj ck-&û_W:VËK-eM>jڎQszRӺ2SK|!> /÷X/v|>[b +]>{mv!w–'/|1WrkzDɡ'_eMq_bY){вʹvsVY|Xlj9|mpJö/Nbsϕum˵8o em;X]uQEVqNΕuUbUSYoӢt,P8V;=y^gJxiƶsc=/#:.+yrވnbބ,,,`w.Sþ14>fcע7><8>:FrΊ̟r XdfqR ,-׳̬^B+enT?W 'MovgDWj˾A06΃'\pvgyF0){N^⟋rwG_.R[?WC^!?s+R.~yxx1/zQ2E. yσG:J2Mgc1|ҵn)Q`P,0DGBSeϔ)"*54-uck7t'Le"OW,+yD'br{Aw|j ϥ2GGJ\]|o(1R؁cO[DYSĒywS%d#9ˬvcegRsܢ<~zb< ?9cr83Lop{93ۅ߯ Ν*1>z"sdm÷m/涞luBUщ8 7-W&3Dvgx}sc}VX w?:'Ur=pܶsc;(w.|g.q[mQJpg>̰crh\ks?Gmo1lOeu#%FW}h?x=61}A?( +t_;mz[6p}|g{=Z~gj=(닸\LY7o0/yE%f e])k {'A~ҷϭۓvkM3#egJNHɽ~f9Q %+s# [o=lX׺2vĚx5H)96M|?} O6.b̅ZrZ;Q~i;C׺י>;Cy9g̍En?g%.3zs}sS}?gώK?+%ng;#eHi_9kά1jw=*{~+발Wf!ʍ3~J3G?\rm=|w C~,tb:nr;.㐣>ȵo{m[WdFg7TW΄gEAuh){2:ʚϔ}n+{5{Wk)H8ԴU!=ѶۦÌ|XnmoX*Np ĭڕc7 X=,to3r!jmO{ŀE鉒'їKeM:_m O+y$W^g[ɥk 6s满Y999>RAL*s@ϦG7(N}7sj@AG rsF:B9=I|O;+37f=]~ёVD_V<-{Ķ0ց~A2^ɽ{'/8g~D,?X$xRN:|wraS([rm*q4=en=qK̭^anq_;W~.dngK߽޾W:0ρm+%G?[9Pmc})vE_zu_+9 3-\^YZt /ʜZ)y:>cr[?*g/|ߙ)ʞ.www9h|C9߂e%\O+uZO?Pً&~>q̛3 +ݞK̖:0\ c@lC, b"#q\q.rG3=R.23ݶC2~}V־zMr.fmXʼZ1ws7oL|/}Iԁ,GLL>\{kaY/$f +zIoeۋ kZd^5O"%;E_"9#b;7sדh>+ q.y|wIWpY{`ɅM@r}4?ǖUlPT;َZQtug"SE~ =$ϱAյiwZO#v&i |ݚa>8 p?)f;E^Ӥ_q{K+gfn̯aV+s;:xoj[Kiuՙ"?*0 %o,#2׉ILCe+o9Sw%7&ܙ1@]̝m^o~/9VzCev=M7}?W]E g3iGkvc[yr ):dyc#E΁ؔK=*Jy]ZQ1=4ߡ9V4YJULy?}`EOֻ2O*m3g۶o{QvU#$]V9D1w}Rq/xjM,?۲Mț{b9-VΗ\n>P̊gчަ=+ӧS1,gǼ^-5Uh^Ybf%f|N.4َu̯U~7ߜoXmcdg=Iz>J:v=mm8|J~[(6ӥa,ae1TƦ1نy 9~)C4B >gIQZ}/TD8[P)b N`x FB/0bR?Rw`Y8Ϗ_)bkwS)jL]Y(5޳75ObqcY7lKo5^QשI~PzECk#ݿ"GZQOϕ"!="~N_W3^۾~&KQP} >2;=Γ"2ش/fk-CE)0L` C۰ePd|pE'zbNK̃Y̎uZؿ<zpY2l{苉Yj-B/RBѧ/ې\ 陡Ao{"ǡyLk9о(dbFʸ>j&d^+4{gW˃cE܌d=Dt[ +]'kE^k?ɖtoM,ZQ'vCtx{g~zjuayíW+|R`L1NM1DkE/>r+ ]9;LwVI?M+tC^ZEl\'Q3U[;-XRt(ftYŏo Ο%?PoG]gEe*]j(z~6(bzKݠg'Nl^BtyLz" ǩ{A=Bۜya +CE>[ΓtoTe$cơޓ?sL `.޾>E⳦.2n'0$ۦql}}\QNo7LKߴZ~?r|w|<d8qh +lֶlM!Fɵzy:4F'{SBG}v=~v^]Wt]Y9fK5iS }Pe9#lUz74\̞?S=Ub穋#K'&^m8j?5zb[6#Uxo]{;X14oC6l*r<,G)~8P(ڲGzՁ΃o`!XR2#'L8ɏѥBQ7Ѝm"[? lOZQ惯 Wڑ󩫨UZGX#Z^8?:jۛs: 10EonfeF^I }gW6Nk"7Zֳ{P)p~CEJ=H Ff[g]|ox}/euasWh/ٖD-hE$EQ%ٴlNmvqA{?_Q>!ыsfNn̴N{3};wd%SSb~uGb,>uCHoYIzIox^攮H?oJnSaI,%MCz~$D˒-郾.=?%kIk:y&]~I>wvas}~U>'b;}6:w[U<^{ݵͮ><[ҹ<һKzӛ%݈7{,>/$Tk/Y\{O X̡t|+>IBw8gG̤-Bt~;#=r&Hkq.s=a.3=]~Leϧ7﫤\:On u,>35J1tu9yZfc*iKz勗}Ǯ3XG78y0p3sdl47Oq2YX#xt.wz+z.co\vw]o.щd_LwbZ<=]}.3xO,xH[;оnzL~LvޏS٭f :Г<`}&y`)0K٢?>ZI뗒GkW:%<^0X,mXq |rn5mi?/XVٖMOo۳\bZ̅õvŧ&%<v՗fi?x\ +n;L/ȆPomZZvdC/J .>/S''o`{^UQC-b[Ry7u}Mx.ێ$kWw@fFfHp&Xsۭl͘г?;8uy~-X䵯UYfV?; .,r~܃<6 QGU83:5yey,eu}u=fPcJm"w/?e1[̝^ķiQŧ~f[v.'Z/%9y+$Nj ev\ʺ+a%~*I._"n53Bҗw5]e3+|}ӞxW7cJI~޴JWG758/{sM}{ؓ,ߖ -j ClGdϼo?9'u8-I>ܻwuMrXG@{YI &]l;oiώx8q~q ^9Gv^A7 E×K^"Ww$sP{ڟq*e]CCJ_݆xqkCSԢ:QC*5ȭm[.ZaBZwcdX֒l_ܗ[\ї޾ 5dlo1۔-E~t)}A-PğA{h5I}0scrE~ҋϒMX>Q?g{:ؤlTOvϱXu`̉5/INboq;г'&q-^u?'XQ7٬Ԃ9I<>ᛚB| SDYώEӚ?\fӾdk5~H',搾E8Kgw"ɆδzI_^}-fI^wvl'^$_ߊoNyp1p{`s-~rNm'G:*17ZγǁEܒ7C_R<:5;Υ[[!ʹ|{3%3"Ỵ}},曢=љEoߔ.uzջNjc{gz>|8{X5,;v;^SUlg1gb fW`j@âѳy9}\+:O XtWg~ %E9ȳ5-r +;LJڏ\gbt`EL_ [gz q3U쵝m/53&5sL4ܚ։ v?@?"'ˊ` l"lI{wEk%|Ouv!mHV!eJS[xw`ZؘH-{bQ_ 5fCddM[9Emڑ$39^=}{ǀ?ntsZM[#n~KmjIεEL**9:\ofj}$Zw7lZަ|_]/w⫯s`12+od;z7l;ݙzїds6|ѻ:wd ocw=o.fVcU#Y@>PNZn Pү<>8x̼ĪoWvv`Eʆ\r0;o,rִ@݂xϝNt= O Z$dn! Nf'@_+'wd^S&׸[O2DRe#}_ ) zhQgFnm9r"t"҃^tlא̞|a%ܕE/}P3ŷֱ&y>jQ-N嗦EYVO2[nzNldk%[myܶsJ((nyx7ߪ}:ũ?Ӟq,gʻ]kˏ-贠zϵZnge/^ K`}`*}k\ >TJ;<Δf_噠ݵWp](c< (1>wc컥uv1"^X7 }S청]OGgO|#~FKeOZއYi̲&ƽXPMgLL킖zw Ε[48Tb-y:&zuv̛f1A»pw[{ &۲XJv» Ϛ2C'a.46^= ƞ_1COT ?/X!k|!㹲87D\G\>>1Y5w8h/(NWrm}̥J,kϔ8o1t/s_ycqݹ3etz߮wgbW C߭?Auߘsfr8U+D YGcxkcl>vCob߰cЗӂ_A󭲷 [3lȝ-Ywn~zue\b[e(kʵ;vDcV9UbƩ6\BwWX{GЏx;\ 뜻A?|{Υ2vٟ hgݳ68q5Q5Mdz]YSMww~ݎ73.}A-lf mȹQ輥̅vM%F=F>]Dx#%kBKWyG9#Gz\y,_^m޸.þgsMwaOۨ +yR݂. eowg`s:Wb*wvY|}]5͔u[j/2F/5cÅ 12B5c#ty +sDNƹR#1FJv?q|zل_Ĭc~nR۳SmQwt^2)s&x~\*{%v ?U\KWМ.G㊵ҷNx+x˺ސsKtnz!29&B㜳wJ]~Q;W[c92}ΔwE%wΝϔŽϞyz)E5kPv]yGȳ^@w +ñ2n,2F>#*][l̍F} +)_Kc}%^=BBNhKx<ѫ֑'θ{}f8l-{ʾ]hX®{忍LN8?6x]-_@s/TJ-J\e)k?d8?xe^!{\)|WΡeYCWJ1*k|yHُ,a_Sk!F8 ?2-sGc&}~b3e͵xУ]ueTb|{=S9] )׀-ly{Sl\弰9\;)sc5>5*l27Wж]OЮ1_)Io\Ͳ'LXұsG J,oP 8oذ l4`^U'6U]ݫQ̥yU +Z>D.6i)V~]wVSCe ?{0 +ηsw}a{su 8ǽs|>wN8ný}!hOׇ֏[ JrOYs?Ǧ!OĦ{ kWi+{ϰ}` =NXudunv8DO[=;%ַǜuXyx5˾`2O,3L yRc1+m-xq \!ϙ3as;=2YDѪA.~7QX=GmO=d>wu\,/ +OIyVT_{Ϋq#Kv89ʟ3s1b|3!qaT_GWFub%{~b?czؒ5z9;⑜,ůUut'؏+dZo;-GjuuUGQj/n3nD/( JٯE}rJ<'&=\Ms!vW>u{λ FM\MGY/7$~er7 + cr)䁳(r|}]2F'| &sňqǗǽ?miy72 +sؗuԝ7/[(N +ݷY{E{ɥNmCۻrOXA֋_I/7kn~ >/ƿ}l9wChی9}|5Ss[#dOo}ӨZ혠 YJ&-)!k_G=FZ5dʄ3䜗O:/YskkĤ9箙/?9m΢jqN{N\7OEG(i͙gԬO6CnTw7ˬ {DTu>P&/?¯)M s_Gzd\|/ 1:uQCc|o!cAߓFvNw8AGۼ?JJl;eM?Ʒ=R>. 9evۭ]~ll_D(T?>Kk<Ƭ=şG>(<{NrCgOTwOӷ#IN>QX^cEUQ~6GDEok~nܠ^?}=|)7OwYB Z#Gm',A% KOz_ΓWe8}lϟFݗ;F\( +siƺ^zэg8闝(*݊;컈E/{(. ~C~_]]xK\s~Zen%W}dߊGd5 +#8sU{=CUl^5NT-\!ǼVK("F縜gU_i3gQH-^Go_ ϻ*/2_Ekݬr({usμ)^51o>oG̶b->j_wˆsjs-]Bק(Y1onױm}Nczy5B&ȽalÞkl0_mc/~_'o:ޙƖ3k>e}6NQ[X&l+J'y˹wObMG_9ŢKYQu3nT slǬ/L^5ysj"˵mFwe9CsoU3GW_ \Da9 Qy3FNTݓ؛gΜg2>~1ށ7A͹(֏[=ԎF몶Fq2k%Kj!1|l;ϩnFI15Wij{Ӽ8&-74VG۬ߠ׍>7Y;v[oӟħ|u5{G<]!N);q#͐uYD|<^8S?_ld<-u}cmw5:y?D :Cby}nfoGՁO:Ql}γny!뾱~hUQ}X!y;8auua)bI HٲerbFҸu6rQH/P@V-'~̒/kϚ5攵~5e_uO}%wn P]ޫǷICoG vߵZ2\o\Smeo|.Vgi|sBcUY]ʙ TZϕ%|q>tre>rʙ/cטų֕3qa3tt|au*g1Oxb%cM}Zy#Q#]K]%OvzqmW.|=C,BO\wJl|랱ͼie}(s9t~yz^}|u%4.dܵVt=f~v*ξ낗}S| ye;oXq{~Y#G8|ED/egԢ'u;aPk̮Zğu|:XvB1>wz t3?sùGZΙ#x8׃&+8fC +z}+k9?q}8{Q(;טM"{M]ϔ)ZJ +E3k>YyyQ n*gvqUduYo6U-qP킶죡k{am){҅r5q- ?۬ (q{2U vŽΕxuHcr`ߘzث4e?(U8oB^ rW} Fq&]~*x7~2J\ogׄR[iĞ1B9ψmGq5󼏍+y%b +C=%\Y& tE>>6ǖi\[leJox7m{X®&ykC9W,(s'_)tEgs>F=B{?WW+ʾ5R1{9.IiGcbcpZfg])c;o*Y~٣g_6<пȉsmAM 6*{gX3z X.V_9?+F{)k>y\s1݈ 8xfSU6ww<ض [Vj;G8b!a=U{Ue;\YvM69YgUBV1s3zq[Am3;ͽl^#zv3}h|/c8=joc*w+|M*!{ e}?cc>t`}Ytsct֒;Q熵tDwctw^L_9v>P:{85>X7f4F+cr ]Ƨ;E |oG~X߅;ʘv) r [u þ/I8wue_r͝`Wm=>|8oKGz%xO")kbḰseE>#&]>A]%߅>Y+ =w ,lB.@6 +wW]*c(V)]US?t9y}v۲+2k>r3b]}6p"|''iiܷWmC%<'"?~ ]:.+m;g1r{}Cex֭2Ou?3n(ɳ_ž "81}-_L-%{?q=ų6ZYݟޮUa[ž'+cuﶲoOwqr}X w|p:]S +<Rְx5='lro{eëripK]9ٷ)k);Jc e?>s]xmD9Ǵ(tV|`clg=>zP.=쯆29Soz&K]θ#M_b9{{[Pb:x5v~ۡ3t?yEak-(J")x(JdӺ8رcAE"A&E(:>u@>\:;_vYWέAν o1`7,|WJ~ĕA5\! gϔ5d +ҹZŗ[s wcs籁rr_-;ܫ^"Wwan8RM cgO/l2ف*)qk(s3k|Cχ־?A&ڦ>gn(1Z%^R`_}p(DGYCJ\s͸X.#΄,+A8 4ou_wc +2<'>BsڪԶ?~ƺqAᗙv=hFs[9;ö 5c%>:fWs^Uus7ycho<ƞ<㚞O^6m]>_E^;|{Ƌ9?DŽϞ 2Vy}F N8}n;ue xϕ2g PA/wbsSqc y|| Ο#e_wMvt+b1{TJ]m)Ё/5~AϾ}sO_a~ ^}^Q/`ۆq%>0^[ ק7fs-""~]3eBYN|.~Ž!c~U}ً>ȥOMboH?.X{#h8sl5|96 /XsL8o^%}l)s+kmn9={c ?Pt1"O7v=snG A! s&2k+1\ 56O)gK+9}֎}.y=n+qZ#e-@u퐽W=,l9=~33TMe~ +}7>Vr]{ܺn`mq->Pd̙bmoc6 Suw>*3.W7>GSGʚ1V c 谪Ĥ+8 <@35SY<{ 8Uf^8)|q\1YY7n*1rzŗ|71oL2aϽ8)hn; n^9Wƚ{Vsfo]h?nSY\93(h Kvَr>+&)gGrc%vuO=ܹz5Vhqk +_א1w\c_vMU9>>ukrqI@rBYO'zw}5uģ_|]g\Owg?1نb̎DmO)tgϹ7*xosTn8;-80C)4;gc^{Qq:(G>S9@#to (NxXnwo@{m}yTⴑ2z:¶, +'|K"ӼйOYŞckwq:ZfjJ rWs_p=mlQֹ/XC-Vir +8I +g+)+gv||~|Y>msTP{t:q +l@g]v_rc{?ۺ]`¾gs;=Xə1w?7nw_+g3ٳgۦxM)WbnepoqMl0V$NO!'=~`?8K'H;?_~>YΏnϗh;m_܇Թk㇌hyIGsϵ_̿5g~Fnߧ/o<c5uĻ nei/xҖ{=soG{4~G1f:ɻ}8۬8>7ٱuw;1'V}>8\uK!sAg:Ej=o1QGGcӦ{F,8փ3`gsOwxboqtuOY/8I z#XVZU?wUq5>/]:4w,H$=؄>58`#j|7amp 39ByαvVӃt=)p,Y^|f Mhwf}Nꅜw_ԛ(]WMTl1i߭g+R?&rjvw)~CwQ,LmxZ3+~#WUf=! .=ytXȹgsm]^sY{rcH0s4c%umNoj c8b2e^Z/܃#k>f1 zZe;oS9 .S3yj3w5C}cUzb(~r1qзgj\+kAs_rH;qS%5\7ck=`5iǃ _[5kXy_s_ 38>*JߍmeǼZ{$Aֱ3x甚;PSNl{]La,7zb\֍oLKy}b|nyg뭱t^rl~9`Uq]/otS,GU~k"x}n߸c=jwѸE_<۠bx*\Gsb#f˨U/{ڇ=ϼz~*k\s[s_']~CM[]d9WU~!\%ggsoufnϯ=g})d^xr~stqMg>.̝\'4wp޽xQ9s½9yyϢ}TBGs`{q7XWsϼk=ɵ/־ou3dgUM9wd\d8\ʹ~ַy{owcǚA'q{5/\-=zLݬݤ>IT05pŵ 'ٻɧ;}ƽ>ɯzJ8|a66[WwhJ^د|c^z x~jn9by|A>b9On7{|w]n SgTyۀ'z֌'߫Mwq?X9BmG!FLz{g9d _ /޻h9}ߣ]_>~n;7uٱZ;0Ϙg7#o r>d'.ESz >a=Z{+X&OL,uSW#}:N]q6/әkyt ߣաgYW%&]ygOFf9>Ho+x/~6'N!qs&w_ #Z=1jo갓px"M3YmMunq5߮ z<-(3*\8HUk}#NژamWGp |}jϸM%&9pNx6>ŦMzlCzݳGq%2rh9 >G)S4~OM҃9c~T yNYN[D}>5B ҋMvײk;H&^]4bd!`~'i/ !*s?4Q ".]QY,,,,$S( 8RQ|d)ii: +ahh:ieTj|\c&>=sϵmۯNqfd{wPa?itދqH¿G<8If{W3 Igi)9oA }+EM5ݗio6;KO)ާ֯0g''j8N0/o+[TAm֨S`\ _Fjk=O)cp,p+mȫXW G*ДrIqbU,0zmcht8ur(?w>uF[g&Ap~kXN|5^wzʚrqcgX +KtݘOtv_ +ڱ΄/u5aѿ[`3<іnC;'O`s7?fw)g~ o~nt WًK,M/ӃRIuٱ>ftw#%ziIu<\&폘fIGjO( at?Q-2B7˱dVORN0b"~(˫rEҋѾ8e'EZ/-mVi^38M$2)o%,1Y?-̕AL3.=oLkT_> je9d42[E^/'t>_y z6Bfe-3g^~ҫniuȶwٕ9Nsdԧ-ĞJia6Gttѻؓ c/iրWH#W!ߔȌP/!^:I mWADr:*6)e78ԡ3>)T:C'Ff>?U/滙#egcoNDֵvS-k 6b {&)at汪w+=™Fsc+4iKܜȿ3-2eܫdvIn^&ryhǺS?(=׋}繇.` Ji %`I3?>?ڒU+Vt;i⳶Z!u 3* Ў?s-43|kF%}lZrґ0cΪ^f{i_-5sR<#]I}&^LrǶgh=w12gR9z9}~ +;\ȿ+nx/勬q}h o'Xd[\:X]("TF2"t+۟u>W4Q*G]"~ϧs9NyzLXΛ=Wjd qT32Vi+(R^#u229̞o +y'h?=E&c۝6wYd] +dڎ? Yûc J7}.W9Ƞ0%o;a㡽SϿ8`T$0К1F||yo467j6,Y1 x6ް18H*]U)RMEFݔJmB%h([M"*Km6isQ~:ケ|_k 9Z$)Oo`>_٠"b0'AW܊Ơʱ_=u1 :5}J3Cߙ1xNw}Gm/v:Zj4{)ƲOmʚ#n9 <&+plփujo5(aE{QeR^pRjL[lЏ2Wf1!)6[) +G_8VzKo=->9% : + +lRWIW/$,w8^{㇔^o6?O.糘5Zd\\B^KCcY|7w[׃>,A*@2%$@>WaO}Ӏv#/3#εaZ|FRr'*@Jc"!hކo3 V]J99'Uzi:_ja''n\O%Tsm{rReM@?19"YrFC{#ꏴtRIK&q關7٨vHiMJD h)hyR\~fgP vId%͚7 ec9Ɣ{(g};:zl\-ںƎu8{)];qs}Me޻2EG;ww>cz=ùSoIwqHzvWGt=y3l=ݶ[W7.5ܽǝ+jG2HYuPǺ^;TyZY{95J`8v(Y]s.QF/w^z8~\߮X_w3 U+Uv?}~ϖP}^l +joVL8}pvk/RJKW{C/6oZj^p]MWz:6o"լF\⾿Xct@aj\+S8Vy>+ֵu8Rv;Iq! d{vR@qOyz,q\Sa}Z.X-޷/O==lDqs>R ./,62|vW(Lqy6-Sd4,ғ;\ vhL9D6']O6{v!1M>y/;߫\ޗ!{(b7\ ܁gb݋[\HNMQx?wӼqzq_Qyu(W*.G]s>4v7ǽf[⻧ ޛܫo=>灻~|oRڨYܦλ4FEawve=;xKQ#?J߶H<}A׮U8kk"w," ^/{/$ۏ9aTϋ +vxCΕ{>;y .>\9|i >aq^ #'%v37: +F/.N8D.OSd,^1Csg\^ ^5+ Q 'q_6zRw&b97+_)s1Nϻ!&9A%BU\o&=[p ?_XuNЂOG^Kclҹ->JA6pb%9PcN q}}U÷2Ƹoq|ѻ ޟ$1je9yޮcX[&xCb΀? CVA> Oj ^W5h.ÖSy8G ۰TC{|fNصM1r|8 X.V"΄yYa)8΍5 !}$G\'!8۔ %@{k[ufTB8 č;JPN\8~kHTͼy3ߙyyw>N?VPǶ +h^Dr+c6TrPgr*nS7ruX_9|=LdLs:\v*m/e匭N5{RYR;1Njlf5|:9Z5mf:,d?7ӵ,lx1㼐d\zr/Z_\_~<s]sg}~ɵznf;g=e.s/g\wZ4K7fywmM?֕F90mh,uxj{1}\5o%uSs4[[7wҩyfsybۥ9x{~&9el{W#sm8x5Zo#A.~_kܷsP}6{:nW e̗[-f vK%ag=҅hs'JVt-%~uFNW}Jc\hG)~5·޿6t)%5¯KsOU) ^F))kt,e|Ow4Ky68#3^ϖϢecq7TǙ*k&(]#̟)SC3|I53QNT}~aypztܪZ;Gޣj=3UmTaAi=hulμ[Z'uR N:ljoDYYKGS^/'*eLD:V3wLEz.Gȵ(ی"=Vlq'ð5Uwn/1nFH1<螵"mEf붯DZ[s܋hg=|9j"È_ڪts]'[zw].<.lP97x7rRlx>1Yލoo.z'ǥQ=݃_|\V= endstream endobj 13 0 obj <> endobj 12 0 obj [/ICCBased 28 0 R] endobj 28 0 obj <>stream +H 4]ƾdɖ-Èc.j}[v*EF5-%Kxl"Jxt>}=nw.DQ*XThju 97M-6,f&:FH#]ËFzf6Fv&NfXKU5yv&]6\(VλT;Ʃ-~%A{!ܡ+Fa^1-Q۱qWa&ޘJLfJi(hZ6u`RȂ六E#%9J%hYVTTV>\٫;HNsC`ͦ'Zq?7jjdv}.M_/ }W0|};8>.px}Rʇ菄eԊ'U/Ӿ6P[0|{.{? E!##+* 4E]@sVn!ѓI$8jNpqAWxysN +, +vF"b+~gŃ$\"%ed&eȧ@`peE2ʒڀzF\$|(m{KE==!j,o"o +5+[hX"-lm=|NW\R]sJQNaU̶%._?ZXS7G&VtփɡlWDÔ #PAQe1q{8KƟOT~XPpZt0 l) !k8%J!).,UzDI\7TA& g(+ *7j83TӒ>/tA}y[UѠְ¨ԸؤдyEeUuMm]}=2jG.MOQ=AQiE̚׎/X&NoM@a9Ae!}W>#8"!QJz1NWs]#=I*v3+6L,XIO;m3~a]WY5}M.G^~ϡOT +2 Mޤ)TVzc& 3-s;K u +64v\y.<`%S`|{MxO " "E3SZ-,,}CYV^$7)_VIAi:e*Z=s*Z\ZG.!Ҵݻ8ס_jdH0r0FHr2[5贬ʷN s7sqTqutcFѠ}; K[>|`C< NT 6 q\!E'GEE5FwŌ.m\% /@Rdwőڒ2Iazɝ>^Ħ4\֯.]&\̻CA„ Tǜ2z_ЮQԤLءKܞA.:ELNPV2P> y_P]Ȁi>9[ل%}ߌ@}~V~Pz~w[S=׆pۉ/7|&L|Trő|˫K|}vv.~Z~֌=] zϦ{×{i@u{яe|t}5+Y~<_FzqA{ WH|;}EDۉ/~ )=}f]~|~3H~o~T7@vw:P' +mm'{ƅmSB79[ERքTZTkロl`RT6I8 sσўlJWNqĕk“pQ6FّVgRLтs{$|2jQ:]L5B⽂ϻDʪȈ{] vLu; !ZLYʗ&$Gf|a4',F6NoyI ]لf$ZZp~˔oISUD Л}KnjC(?l?[Nv>E^ =HK\kA.?&;bWF4Dn ,#7~IR3 (,euj`Fn%,3N&(J.O@E\>৤'漉ꂒ>̏p&K.ǪuL$]BV͟W(圴xBR̺* a +YfyN*>!-j潥{8Cpr +NĘRvne< Icwߩ!.tpmS' c,FM@~$iHfEt}f-hNd??{X y_>Fmq+cK{$K؇`&qT bnr,{ qU擄|ܱBb7pfsi#\%Q/Ί|_Ip}@F BWD#&O"{\gZ(RSN?$Q¿MG,rAUpX ʓ9Wixuz>/TƷ;kd 'e1Vńb4n_DX4. _/p0x'kfĶ*w-Nk.s +s7)iE6a?KߧK4;rg)x`pig4N&͊DZɴ$7a + +}VI.>w:0˖M@7o?nnU|/OB7M'`iMyvd&r#Dp+Q1fh2_Œ$gGл6'+ɩbX&Ca6-SqZWuD}`H:) +_rHh7zIMߊڍV-SC5}M;ŊIKp\GOCDg#H:Sί ߧP;^D*ĭKClDS"GD *} T<N๬ٞ/ :}=>HƦ]_}O<duw nfo-E?xG}1j3GEԅ:[X| :k{{ "?@(>C2=Aw|}eb#ڐz^Ofafs0O5&Z;M^9("JeP$.sÙ׸ +Z‹CP+6KO]S\Y[&-s2;6HpK-";L\p\8zب[m_;iB&}睾<3^`(b O e^SՈ2 ue3IM9OR3OjGWbSQ"jP 8Tv'*v_.` {g5Tj.dפ~@3PzF{v¨s~^ውsGl+T$0luk;Q1:D@@Ŵ0ZޏB 7DEREisfΙs4iwfIisǃq WO<|^z[5fުo; W M]>ƠNԫ)~=3ޓу2_ڴukk2sSw@=<+`T"F.vDz]#Z%J7Մ3zz::7/Ԇ/m/&mp]m4l:S8T?_P6[ f/U*yn_m5" KS:30Ny\:ƊEœna۝7: u:գSG+28qs U~.}nem)7 Щ׺#>[px͚?lTBZnpS7AeGl+z 7#GVz(\#ˉN>Y=GN~ËYPiA4z*Mg@Bb$[KxI|N:804SAkмÐE#>)6ח5ė d;ndi9gSwro5),eowgb"6R) "B $=M6 s zQ;@~] 3wezֆ] \YQi$ITpAG(@on[^3ӯ$~[:\,!a9g]JL VņOf/>Wɲ6- |1Mml!VSlϨP2-9?EoUD+$$žr&LmKčN;` +uco@4O-ҭDQS$Pȣ>sԷB,u)<-mMDAn0U ^B:O+=wȼjS+j0)#^1%>/ŒvrLxBT]ְ'm4z+Lsr5ct`1;n -Ӟ`'nC;A/ +O5:_*ЅGٹB Ȋa>δEgMgY>=OE=ʂކp|<;(lahp`G%ß,~H-h5ʑ2q*iEJ?Vŭ?<>$=p ?oV]@P|銣4˧mB;Y=In+6nS~$nѐ6`I2flOURS6 *ݰVD(/dS;]H4I;i5 lUR:e-ǥ,u&RWR7jFߩ!QAJq'XB3`GY-nd1EHVPw5攱Ly6'\~] q1-xs (1Emb̍2\AZdE~Q@(\T1"]JS24l )p)z[o}C=+DXɰYëwP}mY=(?T0Nmǜ{Ycg@;[&G@GeYkȣK2]*7 D6cv,& ,u-C}~R*4}G#֩߇\L0ԉ\fGubQ,A 6\Bk I/+>?ɩd=ۉ6cƺ( h˳ʩ&@+f :/}6y2 +Ԭ/˴jO}\iv vn'B^C:ccKno^~/63!"F]i;T߄3\M$GWDV16trKq`",]$!I^IH!-J)zXPVrz>=m4Ś2ᶍrtqUJ:fYS{ץx[+oeS`n&2jȟUzǂ?tfFZGj3]kwd-2 mKYFzޞ^J't"u~4>ʮ[Vל .Uqmxu +PX7TF{reɔ7t2x\ntNm![rt7O2d!-YZRC-=!97@@v1h$g5+EsUŪUBG\xE}1|kqgMŖʀl>y<?!xvJ8?v~Gfŷ۳]QtPnR: ŞUWM9 / rg *7[5{e.;$t~h3ts3i86@cQPy˅i +=1_: E*y͂-גhb1نGM~vL%j7ӊqo+gΞ'HRXrkSbiuX0Qz>i4t!.^ifK2k^F:/b[ `߱jq +9k&:p76WgK`~vit* oaY2»W(-}=7AրY'ikplY_Wz*Ka_^xGZi|mo2;Sҹ6YDnt 1V~_2;Pj?.רpijyr pxB׈n᭼fj|czNd-@<,T+jIrUOV@{x)m+4z )7Q +y3V!Yk,U[,T$ IsD#p *4*(Q;YZpm6&6,rEiEŏ ?uQرKmXo̳al36Z?A)V8+}Qj#yj-O+z#H44ۼtbjP V曖H@Q+;qy SS$>ڥnfTݠYl_H7HBދ*ӂ*(v\Q.(T»NCT-o8\ +|zzQTc]3sV.5NyNt*LĦ]a`31b|fU0ٷӇkѼsFaDYTToIlʨ~~*|P|]}w.WjmnBuR rFޔnb.N@lO30?SŨpbca$gX` W=C .{eTUhqEaIv_1oKw-BwYuBim b ҡF=3v򨬔hb rG׉HΖ6+beUb5,JVi>2M252]͐}ܵ^ AAhỈo;TA[?#Y4UOm%o_f( Ϫm: S+tq4W;Z9n?2PBpw |m=H 3O&h~LJx9g.%RjH"2M{׀L+С 9 tC4/<)H8ֈޘdКgj9 TG6UBX#3hYܜ(Va'8CnV˥] W)`0(Shwh dW~J ϋ7"jrQo@5wRX0@҅oTt9Do!^QYjZ#khq43y^x"SjϠ\ GVؙ5k6qEDnZmCQ IMWyz%:_x,ϳ%ƶ >&ˤnc YЅ7 hʬX!x<w&}JhOIA#dG57 _`Pۼ\,Qh6js&Fr E,{2~>{5±."!1% ;=J70 `90H ul(e4ރ/ӯ$ L,I>KduA!IhӠ=4,G*3_@W ӊabFb&3]sX &,:3}bTge5}5QH:mGe^;3'2bO߿NyѱG2^u!2@IO*t4d |3#'V" ii|!KO" %z +oW/\;<pQw;')gknP(w`aX76IjX\ txB8Ӓz)m5/F=pD>bQñT*aD Wie}I4*9VGm8S8"U 9Jud 7Gq5ua *E@!VhR* .ZG- B,@HnBr}7 "ت03U+ֺ{"Vp>=W\%E(‡RiO ~_cڪ]1BF(uă2a +1;ljE] _'³?gaذ~g;6d <{PY3ɷG|_X2YP 70Y\?up o?S4?5t7xy=q";>ޔk '8B1Ǽ.C(#(ǭx/ݖ/5Q~0Wd"pCi#`LWZ1ˋrt"Vީ6=$Ԃ7ćփBΐcl pU-p#Zm$=Mǿ:8g]N~rNY)U\gysnWq_+cj|XCb9bZb;o$R U(=]TI#% veI6^]ͯ/2'?KUJ&Ky Zjz,F VJCT:dIU?b(4g?<%1[/ J73n\I}ÈB6AZVh`탬TZ?+$VV&X7Dsr@pYy--L^/<Íh⤔{B'z@,E +YAՂE2i,keaJB*^F[R_@C|u>!tQ^H"qa)0%\fȏ>w订qIG? 87ȳ{;S҆ +JA꺓KsEG-"Q 0tW.MS|Ĕ/E  Է S/I +: *XomUnͲa#{J;H"5m5nDPΒ5:iLL@Ȕ&aZ$IIyNSgR~Ot>D(m!ߜHcCpX5I/]ﵝxI.ܢ|)Mmj~__$!-̩L1Ul\c_~K"‚<#[(a/j\2]׌ {vf1g{)aV#߸y@Xka=wq"H7ߧ~5F<#·kb3(=mm!}<(sC;ta>} `/jLKʵ~DM" +'ܶk;>+)2}+z%m-lS.4uٸ"\Y~ʹ|FVzg{Wդ|X;X# y8,C8,Uh"ww*k[w8M;+ț (/Q==!O2' :˂:&2V".}rR(XpCH2?ԡq< PqhLeP2u(" +B'9@a !jH]཈ZX}?y7jmܚqNkWSax*Y5]y0}bꘫ~yuԈJdфJ3C ͼzXʪoUVn|\)}pv5Lr.j9៥F$lS W~R|-*DPYh M_/"[/\kvY(<~JӠECw{kOC=N+婨G uK2KN T&Ѧ0;X:ITfw?N$?Rb9`%i~' + " +уuXڏLxyt 53gAgMU[aBKۍ@ހTe&s`:i+k,bZbgB]ܡۗQ@ YYV&[coܤ^mcӶdB+|܅w#Ȝ%_cS! ^k;oç.o"7VbXMW~9;XM/ ~m +R=W ލZ9 jD_2 mL*PJ}}{SPez +_n*Hq-=sq<4qD/(.D+ k&EH7 q̚G\8}Qoj+HG1jConHWq8`j`I)Kԓ"כ`ii![yQ q =СDÅ +hu+P?VMQm%Xc#ބ s TyWAhk7[F<L8A+a2|22}UȴQ>ͼkT,u(6^Q]d* +vBEw~K3cO+l?w>9%+ 5daB+\ Q G1iG˚O蹥 `sQpX.66OxyOꯐ $SF.J4pBqsq /g[jŒJ[' QdM|| `X_cs#h݂*o;=kKPoB9WI?ɡ\F7N3)6,vi͈3( s0DQ9 fJt^DO dy Ţnp_pCkh:0I6:Nw;Eݫy/dlDA z =FU%}C/~|­/>1/Qb,6lhTșE6R~|{ +пI~ "@Kk c2p-iJk_,p):+7; E; /̳++=UV0C#3[X7YfLf5 unϒ|Uux0SIl*p5@ZCGyܮejKS)@MA7%Lw>^ /sjԖA:-qf0X JQPtE@ %) 4x/yI^K}Ћ;.vZ`@qˎeDy9s~oY8%o~Kpؖb.Rq^պ)5GLS5/zY;va9Ŏky=DZ¹C8Օ@fW|dJ9sMy.a%fD&Ʈ Cz2Q9^x<ć' )*$v$/;XR%.>JowJ؞/1.v*J 97wNx{0/qy/YrJo4hbZ&z@(4g(aw-!Lv~XRہ; ј!_R[|0T,dNd8C B8++ *Zl76 A 5D+r [)}`,n`,.传ZڎƤmNBeV<)8`͛ǟ0Jiuڱ p !7I$$' |qs)눤=eZQĔB(xtQ/猁@DHlNk>5m;"C\R^t(R4 ^mhipUrYFlRT6 ZmԤVR߼㏎h/T6D21qUJZL1ŠUjV 0E-K'*a*cBp\(W/pVss},x)y؞"\t*G0'G)`vR~ +s? +OAL-pUJ+_Wf+rd8/I_II1L E*a{z`&\&\!0͏ *~rFIbp2UeyV8u| +:dM WVI-;w/Lscix6J񖜕):%: \-*h]~px\A}Y\WD%>טǔltc7`- v$ۣb5j1JYC EŬV$u%uVґĥR mG6lD܊ +l֊>VZ+FM{C3W1YkM|LXd6`K_or bN~H!s?^D63푮5aj>Xe\,Wx/ =ԅwn>)[~ GvKm!0S>Wud+vjgK^=QN$":]}N)`φf,ޣM;ŧhqJ6Zygaj6cO:Ԃo͏R FK\$r)b +L+kxj Fьv^UNRxQJPݮk_Ph=G* {<{`l`*]xn뱘&Mfڔ!f١Dp-G-7݊~C)elԟ}hAic[eqǁo%Kw/vJlW>/ʾe=ΝrloWۦʯ̪Ϟi`!͗l8gbLZ SYߴzN6m`].i`Fcl3l͹լglԅk*zcXy5|G q?7 McN6"/41S.ӆnL%MB`Olx%0-d+)\h=}Ť944ڏ,L%5kQE?*(a99mܡxIڋUsn |WkM=3/Z ſ0Х1*NJD6t(jllBuot2~I|Ƕ(fLTl-#4]CWm3l/r~={!j ]0U h+6rO=jΧ%CEiViu/4*D 8hV.wN5t\*ƥlS H6\" RJ6]mY3eǩۤ5g=[{j9s^(j g +ox#b_Gg8ltCדCr ZMQ{19~] Vc5O0c{B3O2,ؽn/Ev~.MΚͿ5n>< o[`H'S4%蚖 ;Xɯ,Oga-bV8oE휨[LmgpӯOx%0nͮZ^ْ/{撢:%9?*Qu :N + IrimŰݑ smS9`krOH'4=MMZՆ}}7ޭZeuƐ2W +-al̏?tc!#Pt]עk5/T'T +V9ՆRRpvhN=8ҵ/ί]ZݛS@>o#<ķC1JN>3} +_=Lu@&=+^#Bh'7Ѓ!=tTk_۩ow2/I{ <1ۇA㺇"lw:ʹʎ=@*w BO'^SW(7@>h aSmpxpe T\bGVXfHl$;i@#prQ-ukdõӫ!fUMfoi"44}#&q(9 (.aBbD8k5ѰuTp`[T *r`L0E0!|D1bVFhs_HjLczQrse0}=9kd m{GrraI}^}4 9WA#O)-#\H+=NRn5"!zZ.%h +< ,J@=GC&zP :\{?Lk#x4 ]1r+ɼJ%X)c XlS H^x[KuI%ZH" "eF0הOU2@gY7|9e`5|Ke@FKJѢӴ;#6Y?|amd0m\oMjYUKQ$˰H/ b#$a}rNIr' { +UDZp{T:kW\ZO>}߯1.B!96Mr2)= F}c^қGO_~WKqC['98֔]#y8rrQon&mDZ. +>k3%$t<иTDvk-HA i`Q,>ӜKX& y77 `Ѡ]I~cd7"y|rDR댏Vw:¬H :R [EqJD8;uMYu5wt'> I"h1 ?DQ`)a5itL]œ Q%qRN9xu]GvZPB?AxKwЎUt>Jxex]r"w`c;>r I(%_v sEg=rx.Mu-km@}pV׊r8?i]c\y3?)|t5,oQ$C%U +ah"M/tP/:#mJCVo/ +V4d˶c*Tt5T:\.!OK, +xu~ ̺C۰Ղu?W0zwpZ)h +,ZEۋp–oew.3@i?SqÂhIQfm}q:'%o@:0Ӟ%"Z&U ,&LK3*Cde?spIIޚ\Z svQIC:Qѝ'/۬MYyQ}66OuV ֆU4)sڕ&r~izy*Qy#Y1LFI݇S*]B-[B 'jRe{hw>J"DFb@I)&V4anv੄H\JmG?]L$ Qz+f_.?,Oޕ+/}} Ó8z7.( UxaB +&F*TaJ:EUTߑ/tb,زf~nFK~7CJ 䭦f)(Ʌmqe%]GSq;0LmQIP}Qr@C^AWqjYy)ugA,˪1vUAX_;">)Ϊz6B5⇖x܇V?T\SdM@:S_m49օW|)Q6.KJpn594K5y5E7f -j&ۿXhGmo-a}VՔc1mfΤ ۘS| ]L.9bCѦj.7˺Mr S|uBm1Z3 xj$kU")K5He_w?hJt_hē 5S\AD_+ҀP"4վ/[a0i +d憲*ꍺDQ٦(0yj,QTE3K1{tƾ,M#{aCX{3]ut *θCWڭ?֒줾oRm &gM*/d]rjS#+UH7%~yȷ"ο>8A"Z +DD1,jY+RP{$`H$de (VG.UEQ8ʴeEx=y~G +i-ahiSxӯ"&+tn!h]H9~yJi +kYڜPW)2;q3vۙ3~"S'+by'H D.u{ה@k="iP > t[F DbKKW`Cbh3=!W]D}oxmET{H+ibYƭ=S\=JuA>)S8b1Tl7U!j*N8mCu)ny^#,Oj-JC0?Hl$5[jU/_x*'a`F zN6o5+>\YZyPJq*eEv1"-!:u,Km9 { B{(m +O8z^V0LTӍE+#0\NjͱV[(/-2dL25~G]N#JBgPY/Vk?w\Cƛ'R_p,{q@]t*=.J.`Yae` l!`Աk;@{k&9[۠VB)'>!;1M`J+2!=]/\?=g%蒓\p'f$:)'B.usӍQL7~_syvX#Xբ vX!B~ SMH =@"s`h~Y)[Lç=׳+d#4{{2jW>dFibıf 0ޮ\5IlLO4)GR}US.W%w, +ٜtFo_I8@PydTߌENne˕f*\iTro^&a,ό8FwK?=oźFT`@}~d<зjSW].EhU*=jKl#2#p,i .Cܗ'b9I[ȂA5J_΀3 #E``G"D/#$nsw d%b7Hݳ&B{@at1R3eY EAl~EKQk&vt0 'o0ɁXO^Z$J LOK{O0H&0zɱ.I#l$ -2jFHQ`Ry$WEcԪ\^M{ b( *%CT6Y +@BofH"+ b^(rᪧzZDpPgmm<]hȔ)F.dڀ|U-o8՚=!idd@ +[@޲R$po@ MRZw&=v~\,n̿GE:{)"fm 7a6z>5siׁm=)$Xsi0Ph$`Rz,BJUvW۫dvGkǤt4Ez7;C0*'$H*`:j~F8! D'${C/JtZ.HdV T5* _3sw5Y9VQ/d%rZu.^(JcW;N@8=(}D)kDEt70&r"x#'Ph9톲wvl4"';h|@w+Vq`A?A< oqNi\*0Ҭ^\}FJp+5/c-o$Xc;v7xsM0kTonʡOļ _겓:3VGlG|'>w5'~%|ҭmȃx[>b(Kg22 :1!9~x>S`UګROھJ}3kwM{,㪖ե1EJaH "ɷe+?BtS-jPUOhs%'Bia! Z[YyfC:xeʀaP`5DɀIvOʆx BRet)DJ#O8(<y[#S9Xba Y餏P }V"0@qZ17pؖ9_b'U+%*%V`JxJD =Uu޼A ڒ'+*IOj'"ϫ֋6:UZ|S^M4,zg~e/ky()QJ_0Az* {:x/<,Ggv\ԬH[ϴxPy­Xl5rl{Of֐E5n +߄"$3v˜I*a kꩍ1^&,/1\&ďw琣1(}Dwz~n+yW_(EGTjC].SoH ȀRflke\< +^͸I펻}Q#F0׭h4t闉xHFJ$ĉŠT$og*^>֩D1gߪ*o(=¸8Aq&p(0[ݞrlҞUfey}2L0ӡG %mLvxRQ +T7*JZ6}Ԝ +-k-1ayמ?Cx0ۭ(V^g|bvEPEkriSkOi-`e| nE /]rU#uoƂ̺~ںlU@(!ũ¯,XX;SL j(i;k IZIܙ$_RZ` LHL>tYC?d{|2g#S%%^G&7CgE_. +sқ*gPc m+6&6\#W<\_'s\RsvEf0qHӘ49PPy4xTNp[MU35.RXè.bҡtnf--dSU\>y`nZ9Y`[wyZXj߈l|4j`ڻkGz:[?WA:)ܩ'y6aՂ?-֭nT,ZS`ws9׼Jy +(6Ċ `77Yiq/(4,ՉGTG(Wb@h~ҳ罃"TცEQOƈǰxrT*4'[h?b^,`m}r.G\ͯ]/d +f +љe$P&l2H"\)!S:2NO[uH;U$忕 ^ ?cD*ڒs8|$ J8m,n9]y%{柌 SxreT24  4+ԠbZkV +qTFbt\C =  B;\x#2 LΠٚ@KZ|oB4_eJ m{g<|3\i% 1b\q9g8R+CVPҁ![ѾWn7:۫CklE+AD:5-q\4ں2PK ɛ!͌qTW|Q vl plLO)2ـOO&ːd8AE1Ġ2c\y1OvWTqN7)=ߎ?\[H"jgY{Hfr Y{GXp56'}7Vo1SȄ!Py2uUKM~cjQ|wv)%)x&4Ɨܴ=[=:j =E!*e 0)M\AB$/#˞@ @uQ:V=;ъ⊢T֡'~;5|1+tUmhM(h֓zW߰J vi*Otwb +/6%;%SE+i#-FOo+ВȀ`X^m/"VIONP>U,?)O~Q~J &U1]RGX@." ]g>ct1=9譖 +tEWc {ſs~䯜tyrMުNIU' /g\"RCt/p.36q&XJ5'VmÄ /^&[4-/J6̘~^= di4DdNzI$wgFZ +Eqv=EXu+Hd1M`%(.~=pOU3m26ҋ'2sǪJb4`9Jkڕ$,w Sz[Ջw1ì-ְ9nB]#TĂ~[P")Ȇʪ5dx&;(7>)@x[_Fʚ2ؽ垼ܿN E_KPe_1gic9&OR@&e&WzGZ˹97TQ9V7nH<h/lv,Nz) + 9 q>B\ZM?aP-'Pn*p!۔&(k')wHE&,-Tw"}?'P^ztU yKj?mb^z^xW?'HNRYnW&bQ^b:d;$.x= +&꺽̬yqd$ڮW91ۯWer'>ηN׶P.͑oZ-%K#kxcEy1cT\3"l 1EWUx&12P<@Γx /dF"HH+ǟ, 4nWi٨.LT)u +~&X@E~ԚPyg&똫U$@-2o?eR=(ۦ +th=KkՏ]$#%Kr[b|k#/A最m?6زRZلKaZ4|{F"=k̊_Ǝd) +חVfo-sIh,ֆAjr&/8TPi4 'U`VwT̡z\wK)GK{@}ˢ)(\3SSX7H.26V, +嘓F\0kL[W +߰lXS6nj"}Mni*C; Xn\Ļ!fWiCM^>OXKֵCu0[ эSpyA`:N[ u܇!g5a6V )=s4!GK#jnܐ|T#}6>QUM~V sFn:DH1kGF琝| awWXkS.jj).E5hV,b)վZk6V]zeݫK&862u_$)NqFHůk_V] +3iϹLZ gd2ĹlFW15xz:A-TA~Օ^~Ḓ Vt'ݷ ° +hH&a:$! n:(A "tpE8p38&Eo%mZENIhP݃_G ;_IYug 3@o!-l?%=8y# 6l0F$`2rP9h?$/((jxgx|C,88^P}x,f?-Y.pOi',eϕEj|(ѐ@Ňo>4~nTִ uȶzFY@A-)]>Sg"hج;<]w__TŻh[M˔X3y? G(IqyEբi1JVg&&Z 2{y473l|"w +y PNbw dB6L-}߷< _M2X8=I"/ȯRM LP!d j0 Bs*/gX'w[)E +d.TSTl<.TB M]AI6EN{/EGQe0 y]B-+ ԓ)!HDJeۜhҡuՔF# N%܊P/JT IXzE_OzLROT:(dU<0S)6W;=q5#mҽNt+z*KqIzvkNzQZObr^U҇5;53낈|o|J szT,,j^vnEClk ` V9>JQ=ou* cXLmtu8o$t)ۭDu'NH[ +h-ڐytx(q !>b"jtCF6ogkP q ym|-+܊wH;8WUmz(Y;Q?;Ӯ&!`C^7g +~T% G.PAU=$ج.n#cL2z_iv&6? iZ}_dX>scH%G} qrK4xC~w6a^G2,٤B2%ų1sWeK+"@3eL62ƣ DܥUc_EbfstF]\:Br-2]iJ/r+tk"$rŮ,h6_N}gE&̵&Ms+[:kGqC})g–4(WX+ߝC܊fvh|edKKgonWDs'uVȭ[Wc5+j<يXo<|M+78ó[Fzƭ~O bDS׼<+䩱5^Ztڭ9ƄPٹG~^O0b+T-ɒj8/#'Umŷ$ T;]v+UYj+,>'5ee1岽ᾪP9w]xpZ-CE_$)e˦-߰%j5ǧRlh\dguHH`rCMHOʔ^ް9fTp.BgbEKl/̒ +{4,]gbs[U>M0u_Mwh`mC꜂%ȟQ3Uug[/| W9Oof4ջn_TI[X="ɺDѳu5oV֞-w4|e_D56Ά#x +sy'յιRyK+ (Uֲw)Ytڞ{u{0c–qͅ'Dx&d'WsbǸnWlxYfՁjH7*5",W}8:9OM|3g(t k'][aJϒ5J%-WI:u#Ul8E72g+SQkJ N=$֋"5sZsN//}|ŇTH֜AR:RqSm2K4If*L *+3[99o'TE#6dz-żj*}9"$p4^wK?Rۍ]=𦕸snNxB]j[M:Y80 q!NdϹBrm!w+ۇ^kI]i@fY*CRپ9TZҲEr{ Kv9s;g:s?G0u: &oVKd>A)Qn06$A}qw +A*QUlƢm֧r a}!hU<2wSh&;ef LB[uY( +7Nvc)I腦HC& 6l Zgj1<6`K`-#$k.='p9S8w%wY,Vo X\uhM都5QJUZ@A#Ȑޛ_&ل,!cdZVi핫QGETZzԪxպ^ ? 3]̽ 9f2j Y7bLb' + Oy=A\/LYe yZaO 3Ǜ3k5#Mم_u[PzX k_^!{Ɣi5Fvڄe!3 g 98]kui?o 5sUn@Z!FzU4mfelr@GD]ŚŞHظ^ċ{2IRUn7Tj+N≊^fʈ`f0G1BK `p̷Eqk-qpko1?HkHpB|]d”6z@8ڟ&UGީM*clAOYUХAטy7 +Zo*&Kx]Ls b_fN[=f!Fh\6D6GSIYnpN6;Fʳ +Ɏ,|$RN8l>)%#G|)e.PG[sLwlE nWͮ]d׬ȍا2p8l!% iFfw{VV9Tx;RΖ\Y3IĂ<̠@3B`i< +̶ [`/FZ/*͂.rH9򹽾o0&` F#]2*B,XX+!|+NJ1iyqOڤP])rVW%Ա^<*t<{Ds-%mzG٥ERISP@CYVW^S@|^Jl% +{MR9LRJt5;Pg )ZVr[A }J {auSJaȽwpxԇx\99Muf$pw 5UgJ`tqJ]=К̓l6\ŋ׭DT}iG^;#ն CR{e)%SW!sePL"LQ6($WʵqUۘ h?!ŤR뤡@H&ik:g*eq JгtmQA/?IH|vR^yn%nz^$BX- $sD,RY-:L-_bcЅX8뤸 n\ISpKQّP`\}R+r23QEpѩdU9` +`itn(臺|v}:+)?Taa )xU٘/6bȚBS`,oPd|Fޅ]QMYD MPYBX,DPv-,=,dO$$T0EǺQ8k)S=QTdE|0|{N`,j!ے,#,sS +kv\g3YsZsS{d[F6$f8f Y[̶=cٚN`)UMWs;GwƴUƃTF?s\q.9kH+RHjk` JLМe`3o.ĄYCK˻0ʟm^}!}k:|&П^$) T]9/`ir0l.0gg +ʙt&ócR.d/Pt/W{{Ҩw*n<M0%[e_IQpr]\;p(Ǖ]c jt-5S+mԄA(ʮql~j1!!%@$mt8 @k!%ؕ"\%edˍy=yu6a2* %TiP?.V3@>+3BOЕE̒=K[i!-UԾ S=Pv7<̱傑2 |g92C%ngR@ iYѷp/0?]̃bUFG +މS8]gXBRmSh @yjZo93ŕ)tJ+ܬBT`Z6\F0i0֌.-jSyn/ot>ծ2!riwb̄-ws,2(#Dqgͳ'#漥T^5ٱ?u5UX\Id&m8(y'gvHz܊_JelI +~GdU16o>WAq{CEgzIHQ(_aG\i'Qi$;}^UWp62z57׶Sb+t4]n^9ZɌ-,Ink}U[ +뇘%֠Z xMk{7 `[+xp wmz#/ 8)oȅy*Wڥ@%zѴ=zL %dIX[Ϊܖ05w* oRG +$ +MRXc%-EwJZ.5cKh18B/ .]{J#s)2βZZ0leS{.ς eA˔{ ѲgR +y[DscV7͟RW;'4sڪ׋8+oWo}V LXi/vt.i{AXzi J32Il?XOQ5_ӶR7QnN# 'YiQ:c#6dbh +*+4c}]g9Pp»*1Va-ń=ax;g~T\8Nx0'z +7Xtb,VU+Ac*ç4ʧP^ +)b< ^X6*M E1U vvs)e\\F'9A9i_{NSֻ ޝ~/K/\!6g8t4,;7-;Îs[N{eG]jyFM%+,~mOa~CבƼH_!cX?U@QAOBMCKDKEJFKGLHOIRJUKWLYM[N^ObPgQmRtS|TUVWXYZ[\]^`ab1cDdXemfghijkmno7pQqkrstuvxy0zK{f|}~Հ+He׊ +#  _3W0 rL(` A!$""#$%&v'](D),**+,-./0v1_2J364#566789:;<=>?s@iA`BXCRDMEIFGGDH=I8J3K0L/M/N0O2P6Q;RASHTQUXV[W_XdYjZr[z\]^_`abcdefhij!k/l>mNn_oqpqrstuvwyz"{5|J}`~v܃)?Vnَ3Ssۗ/Jeס1Pn˪ +'D`|д$@\x˾:VqńƗǫȿ#7LavӋԡշ0Jd~ߙ.ATfw_0Vi$^3  +g 8 + V*xQ)iD t R!1""#$%&y'](A)&* *+,-./g0N1623 3456789|:j;Y+?@AABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgijkl(m2n=oHpTq`rms{tuvwxyz{}~#5G[oLJވ*D_{Ȓ2Mi۝4QoǦ9VrŰ2Mgл-CYoÅĜŲ #:RiρИѱ6Spڋܴ۟ "7K^p7_"Jqk7SLASq' +M  x 3 aW] l2Nr =!!"#m$;%%&'u(D))*+,\-/../01U2+33456a7:889:;~>?@ABpCRD3EEFGHIJqKYLAM)NNOPQRSTU{VkW]XOYCZ7[-\#]^_ ``abcdefghijkmno +pqrs&t/u9vCwMxYyezq{~|}~Áт/@Rdwőڒ2Iazɝ>^Ħ4\֯.]&\̻CA„ Tǜ2z_ЮQԤLءKܞA.:ELNPV2_qUKwT\( =jˮr +R2<3ɓI!!D ld +"Dq "rTrw?7952LWּGx} nxN&Ą} ^~ʶ5&u\)j)P%gB=xȅN%-3 +[-X?癲.KGu%$${Ma[!πT aJ}sqҗ9ݬ|w;$ N| +~ Ь4t0t5f~%M`R\Cv+kCٴ85V0~5˝v5 +pCG>^}(S5H&-k[dLjKG.VoehjQu}C& i3-b Ujh=b[[3`A#X܅eX3H77#'sbP[5nG~} ](e;4 D-=vh&S)䛪ޠ Ia!*#$I,nM4!$vyvZt?wJ;j]wR^O{T X>f ƥ^ۍ n!-WאBk ܗù]i氩SO,^Nscn`x$*xynQ ەm)g@uG3PbCb(_u~ +x ޾Y.4<}ڕМⵘ!}&cJhpv#zY ) 6-H=20krOƁSy}Lo< uL@KzJN+MxV(/pr#x+32Qn$j3G,9I)pՑ* +ؔ`q<Cog,5\twa"[&  hMF!ш|J<Ï!=}Gěf4/԰7AKD +<:>_o/wK l% \ͽ)d]r(-o N.B~@%aqw/4Wpe>ˉ8dZr6~цoVx>⟕c#3s9;*,Ϭ(+$rR)L}M&^3 `ʅ..dqKSufCc84Vr[ӏ:b"6yM$H Ri#k#ZYDW/,f476t.1.YEEߛ_N\eAZU"<օɇJ #^NI'[dأ:k,qzU] %jP0@=ðX'y!P2ZDQ.,x=ᵐpn5uX%3fUV8Q +^?,ā9*l[>P6AzS`P{n ZJ|xcV $8|x9ݮT}񮑧)(U;dbyIݱPPlxEՄ7 GꥼwV'|,ثm;Fe H^( ^M}º5f !o^"#PR1KU%Wy$|ŋv 8/AYkygwޠ(XxW6ND |?q +y҇fBJytVbN 뾉TTɲ .+kVl>2~|Ǎ_QcM ]vg1L(eQf!/ЇUddUai,$ +I+C2ÖIoȸ6-װSNcZɺSd2y4ΌY|`Ǩ~CCj^=SD fMxWטrhۨ?J&Uo|NH4"5 ->܁-1?;vZQ蝍&{m@[7|(~Brnn z⿨.5\v2˱Neu7jnmMR @ %uP ŢfĉdlRT6agUo|IҪ;칊Cv< +KXԕv:Tr~Ƌ7S Ԁ" ++B|vSt"70RЂr0v ;,T̬Y ׉? `Vk˧$T6*tˑvK$s}3c0縉ȕ܄c+m%?=ųɦ=$:wJ@(RA7"4SAU@kB)v2uH5*&dWxf Fhɓ?\kP'LE.~5ch*Ayw 99JZƲWD}N q2nҒ+":%juxspDeI,`{x2O~*ݥO²ksBcOiAu#No?c6y=$z =:H a"1QhGwJA*0tݟO`,%1ư$ ]Q ؇?y)*/o?DF|NCkF!#͇'C; <+BZI[R^O Q;O\6x4̀͠YjtԸE[ϔB|^\0G6Z'Ez;PU\Ξg 6x %x 8zU}s4EN iQ\!k, +K8WQp&>%osGM"|rq Z^8gyWy]ym3ήJS\9dsfX,7=x>/t^p?H!i{iv7dgW}0SUi Pq 2yQ w͐c|U3/NH-dxm-dʗ F{Y3Afc%7_s;Wr(y-#&abS/Ju0T80cr*.F*zc⾚>/6Aę;O<<[@TsO:M%x0p])vDuOTqtPC:b8G?˷s]/.\nB4hQa=]]B NWM}\K輽#crSő'M ?=?v7̳93&uoeiJ70^/*$P\(wES&+V(ϋ\X<" +I~IYߨ^g/Aԁؖ+*@= o :N;#LhZM㬹mrnM;.d@SOO'P4VVb`Kӫ{6?GjUB h;[| ZWFf*r9~D@Wd7"~[ [!ꭟY5_x1C'uZ_>ਊx+eW}RӗI^p[EfYtWmSf{QeWYx'C +K#yYQmZ!4D>`)ɾUBb8 +7y\sðZk7*C"-n(sds94>`K0E)MSr)Ր^qI?Ss>DCA,V 8G&{@UdvqGe_]_%JJx9*="7S\=AaCZB*Fd62ab nIK8.P4xM4MU|>ղzߠ\'^*&^]wI&Jm]u2=ט}-6HhrҎ +кwC1T'nl}ԫ!wPo?ﴺ~pfK899շHJvj\ ƱLQVՠE!g1nJ9U:y7nD;sA&N4FQyxozҥڭ9*OW:+Q{,rGA!눅.7zk:&A՚ݢ,"}+̏A6AcAҠr3ks6Sj9a$QL5LjAqv<#omMr?{3v똆Rb*G=v `,4sg)AuIӴSSjQHU9ua~aN4o`I4;慗hJ9zdȈ2>wy[?s1yEև@ũaI d2-d>Ҷ1O(lXQaZ9n=O ;bZ'M VU|"kzBr2gYƳNj)37rI=ٰ~.XLH+\-u4͑w2R!;7Ⱊ fM!NJldL,wtGɁ[N:y +:5R!0 lQզ?ٶ¿]1&>#ϫ3\Y(*HV4TeuVgìY񦶟Lu?A3 (ߌkQwg3uju^4RvTQwEEex9)yT-z#.V|[}wo}kZ |~^|TQֳe|~EW|9@y}m-~ +#~+xk`\^SyQ޲ +Ej9jTm_oOq~rit訯Tw@Hy3-|klqY rŨt+ud}vh*Cx0RҀy=Y{j(}pс}S}+h}}<}}N}}v|7~R}f~~$Qx~=$b;)Bm{3κs{H{|Xz|e}8Q,~k<){yQz z}⎬{y{e|;P}v<~ӆ)€x3 xҜR?y_yyxzdD{!P |Ս1EZ+Zgݺeg';X܀ѵ z&r|Z}!c<ڃ Gې@":f8^ퟂssXx~(gq7ڐ`?+ܟj}fcHRDg9pkk|l\w,`NF}(p2*mם5ypp jguӃS 㢚щ!9|mNF G'A0~յH=n\V1^!(@LmY{hDMOdTBS|`= +fډA[zW4E`2DZ8*of?Vԭ%RmX|~ +Sj/)-e4ה7w)oE?)]q.* 9Cǟ(X*:4 )I@ gjtEZnI9YSrK]C[XD\|Ž}n4 EfלfN'3m#^|$z`pn@.R/qs%<Ϻ\ĶNxvq3vTC:Ho:ӣ;w ϲn  W'E̎p-}PԵ+ sW5g7GsINV?2-Qw<)h,QluG!ԬN m)re0(=,dMXXU5\Qtڷ [L+՜/L00NJ| rTbhr<}phej3fWڠSH9FM40W[@l/Uە9ʦϲ0S;ka+kmƒO7)q~m8 9WE˾g 06ՙ¤p֩¯?슊upt+BfvE*a8Nydf&>%dH}O\=ңlVmd(ۻЈ5./]p}ejO93p홈$l*(DP]:4Hm,}WxrUĚ{nz_Q:Ȗg \p@P{n#Ga7T`ɃG"K;^m/ӖN%Oizʱߎm7a ~TDljhG!;y~0 ҅[%$)zm֬j?`𧺐T(GaQ;-0=!%퐄 z(XzZmȫ͙O`ܧ#3Tu6GѐJ;/a0d&(ŋdzp΢mǫM`Ҧ"S}G_;Ɛ:0~,&Y6Z?zB&m#`}eSˡ,Gb ;~l0wӎS&gN\/n$o`bapNW:qhKr@-tS5v*Ww y{oJ*vcsvWwYKx@S +y5,z*ќ{H!z|}ob~ c}W̳}qL]}@}5l~!++~!o^/c~hWѱL+@:P5_+v"{e4:opcRW개3LJ݇@"6\v+왅g"蓯r'cĖX"BLpmA$6Z},n.#yi+o|dOX,LuKA'E6b,#˅omdX+y3Lj{A6`4,jb#Α ی@LrksmHtoꕪvqwPsj xupTwz|yb)~f{(oxaqx煮r{ysz(~pu`zhw +{Szx|s>X{}_*$}~smnʧpWsq}s:guRw>z W*^| Ϸjܐlˎnp^{rMftdR!v=yB.*|Hi1E2kE'om2;o{zq.fsq4Qu݋=x*{ƅ*gj lan%ypRe=rQu=C=Wx9*{Zfvi'ɡk?Rm_xodrPt=+wR*{ɟf7ĵrhrjllƫ~xo!fcq2PIt_ՁOX=i=biݖ>2uwe V9XSmɡ:h湿\#FxbPpD{MN]KU:񷅅.7<n|JTq{IR^h*}j1Hsr0 襮1ھ /z~ۢ͢ +OToq<:p| y[I%V +⍺mТP;!RmS U,!%ҷt\uz)p܉8C iۍn@L %wK3h,=LTlIA- _wh@H4ܷjߺ+y+mrh6J "f84s\gyjpx%1lw*iݔSGӹ<'n2M[Q¾`@O_su9;%\aY>mB?T/ME ]^%Lf\wQ-eݵBzlI E EYu=@x&(8sXU3/5=ߜ1?2(='- G ҡžhp"zx&h+l /bKⓘې56_?=_гE!D4:&%] _D<۬aM +cO+7ܓ>>~awdlМr1 yg])0D/0pXg3h̒uKUI$RN/B*R7--RD$p ql[eǤYӠMB7}-m҉$;@‰q@eyTYMʜd]B_N7wT&-lo$Lg=f ijPltEܬEnd;Op1#r'vu>woz;fʹpU[qBQxr^F%>s;u31Idv'xuLzz,|Ef׷w\xQ$xpFQry;,y1z(Fh{} q~"fĶ6#[4~Q*~sF~~U;~q1s~(-~ a.fɴɆ\݅Q>ȄFW<624Y6), yn=f߳\!يQT׉RFXզ +z+1\ qSQ#>VrdP'"䝭r(g7\RP)Pk;ZKvH1MQZߩ0)m#;rsbjB) "oKsMYĦ{=Yutl%)uj1 ^7jzvc8~$$E!_xOAAGf)>[FKV@MYXo=q/QfMlG/ݟĒ|.:䥞 ^V\ g_*CZňax%dΰ:$i/gPA Ƌ۵`Jí OSs_xZ蛤[KOhP9K}*}@$4o%n=TFA֊'ט[,x@AoE)]HŴKx  7a*).ybEXV=f1(EGdal^+bq%$UL~`s|u~(v`c^Fyc牋l)i> WQ?vh^'Xmdb +{~6vb\띊Qd9@2:cQu ɛчU7w=}ah#8:Vb6R2BXoߴMqAzo6QwMSea~B1|ݭY?8sPR}֒22뽥\2'|F:$3U^d5_Y|j0(K@<!4~P'ymgZ]Ęl0f/?iLtd:NTj.q'ðp92Nb7N] ]EΔƟfO"t龍̷Rq҈+WMZ#6]SCgs 5cgiҗ0D eœF&Wg)>7m-8q4 |ɗJOtɣmbnƺݲQdChكˠt E0`ɋQn gX`z>UU*{8RHpe)iefA$AܜUje +EE:޽_w +Nʒzx`CR5F=o*d$Շ}qb}Õ>l%2)N,ˎ0cߌ sy>0p(]Z?rMǘo)xc +bTuGc\ +YP *U;i?Kc1VIyk[=M*qbAChqI;/ʍzv"o)uFTX+?M- +WJ>F iCU[mz1^ 1d+p>Js4븪fĸ&QW!Bi׾rC-;0( [yneL#%]M=n&cʏgJ7=bYjin5i;-[F!St`j!B =EnuOJt|\ou:" jwOG@,ԮGZ({q`w[:A +9 ߋYäLvND2c3(v& 6ܨ$un9Dkkȁ:?aoaNRj#s6pLz ~x.p @A;Aqn.!5:4~ڋ #[G" qAҼNUĹ|/ZDoN _"oY>$FFn +da)p{yr2sUї_-"s2yeQu$A4I>FYl[>,3@KfA=6ǟD) >cl$I{Mv8xjZS ƪQGMG^֤S]H|٩k4HP{X NR #8$ؼ{=Llٍxy֡i,_g6 +T?:X\unTDw!F7 6F՛}#vL`LyͰ'A9Nt}/<ֹLHhmK"gȦZjʹںw٪r#ET.}&Ljw:wYav''? m4X${ 8m{'|l"r©u hO |Wjv8Yu-XG]hT$4| ?Q V@뫿)5T +Q\^TUp-PuAlyɉaU~F) i 0Nn"׼CӮY +uQ,FQp\>eP?.j}%ޑ^ ;~BOtwQ_/;R"g +'%[XѸx0R:V\3eTӜAL?i^>`W"p{NyW\0V)ek&Ɩb%M؊ M|-G0(<)H,O}FW/_ێÌސfĩ*+ҙߍ|8Qp/0wڨ%ܼ#29gr:Bh, 9.9<H DFuIHXxVx\B(59!!S!vՊ 5kΫ KσQ+ R{,!]>(>9SoOno׹ +#<3YLMHAUdq|XHsSCXU,^]),ϳs2#+FԮȩ3*tx%@y`{7 viϋ5Բ5d lUㅘb*N&ڠz>3g/A}ӵ[6,%6JakD[7ûWAnyʏNI}U8ҷ,s\8C h${+Mi% &%!ՕP6BûFs*房˨Y䡲 'oΥ2e!QAx$#‘N\L%y0'YͥM[l:9!¤qG`P$؃ ?]ԁث OlAqTdÏ%D84S Cd佼^ @V֪砞WxP` vmʠJ!/A(MAӻEFҠ{Lb/ѨbZG$c8 ώeзb,dW"#_(pl=nhI{ _yfڞW"5/Jr艢Bgdy +ayk':Ku]Jf'E22E-ZWشZ pFDq$2T +Q!CQQ9(4v {]~i/3r8BBRjGX ;K^d>VZxҹ9qN+:.JeQ\}ua]/oVT$~cQ %"iI|Y[~x/;o+S"-$P>*ZVTlrnǾ( 1D99b*xOaS5[$YJPI~H-9B`ct +P<*N\KJ0#vF$X7!iHsB! +ף.RTjʻ5>xأn}򩙢F**@b)Ir1"= F3)dK0w6ƽn.$ ý]ÁR\mތ#kz+ۺZal[%< V%I ++KtqdtPHΣ}#˦^G`5,nz6 +))13K/"|A7` +(W-T)wRZSU00l؈EeaoHSp}num"g*FGWlOD +_fpBk. '|NW3UjSK5 Eu{^r\T7GԴOI}'~q \rvL5Fc to|ܥjGHѬx 'Nap~"HϘMDrG=ﺜ]&p-yd~9 '^86Q5Z٤U:uKveTժtθYz;hXQv+6a<-LcGHVq`x3??U-$%9l*) UYb7ykRb L9^f2i[-%kHVDuyhMznttƎZ/7Geu/ҷc$WyiBf_Zt*\`&b{L.~xmJiW5  +jOUisOF3epey;zPeUG!dĥ5X|09 wM3B8/jx)͘J#6rT{J^pC̀w;_\GrGn~ +ؠn Z8T ~,MoU.eјソx[1lTwUy6 i'~:#կ5oquj,\AgNÒLw5kd)^i`aB&H)Zdax ( +"""9cvLjevtlSYMnf53eۣNfs'հ3; @H'n;!k e}#c%o\zc$!&Vȵou偉 zeDxI] eO"M?+1[~<ZD2 @=<NoDPu7 J'7{J 7v1c4('g`9MJaY'Xl9]+-ϫ5,nA{\Լ:f}sXZu]D3_YM7Kha]8-C'C~\(b/"gjI:9J[,t'mаU<;T~GkYی|N{;p|庘o&a}Diڻo*U?jlP?1MU;eLa̷>\|Aj2 T7)<* +21z>{ Hm~ 9QQbneW&t/Xn\sدԎ3uٚFsDNgwa!Ku DnhH3l1`L +:n`NjwQz'JHSE$c0:RxYa'uyIs[5 CIkH=j3DpYliJIqԖg9bP<:kuYtFŠuߎI:YkIly(؅͗~"5I=4IGǚW!<>mX:fack +Yxݘ/WVmE"Y51VEĦ:\7ѓUq]XOa'^FYQ7)xt`i>zؼQe{45j'j xEƑ񹞅4ԭ-3*GtS Po(Nk7mC6 CJ匇6 +:G"BBGWc;kC z酗yTn5l&qy9:^F|wi/ /fjϨN^x^A#ۙHUwx uc{cޕ8osßd M>e VMwK^~}s^xm[gaƱK^%WP|nQtȷ2B7MS4D55 +ÞڱRj*\G0uzl|MY'+xeAGrh)vLbyZ_ׄmq]py ej; y2r$ *uƯ |O2K, pVb>VtH>?#VE,6)hs­d.k6o 8ChN(AI=LfDͨlиU k60,1@|wi^8Z?d IiFuS P`Q'}5 +ש^ |s7-Ow!3JFGIWJ.opN*)$f]-P̏s/柕|MGhRW_%AS&eR,jSlF|>;wJϬh@-5Uo $LkQ柪າp4#4R^  -. 4VҭxBܥvہ Y evԔs4龍aߪZ`\4:z\hPY2?TW+S̎0eOg1׽'RύkOabSW$17O|5u V:%KN@e){ {fMH $`@~E,ny"WXρ(J-ρv?wߗKQ*=Kc <@] +eGt$3yo?jVڶCT3,~n OUGa}DPHm] r8 +ybH`P(|}9N˛dg5 T8R~Y)n( 1(.`cvd5KZ'滁DŽrKU޾3܋P">XX/3c5 OrxomJTt\j%e-4Cc6˴:%K1د&-ttGa`I,sL,^Pé^R~["OK$ !oyNU"9ltHwun%*NB 3*N2 y3@ΗۚKcɁungL_mC1vQUr4>@N%SK~<V92TL6I罫d>ٜ%w6%]( +V,Yn&R)Z3i?cwArE(!*yqB}gR6n .:.\GZ ?.^U>Ÿ'6 L)( nm (G#*\9[1[=j.PW-xv3D7$Rl&4w"1 KxP:TYMl7"R5e[~J,ʫkRdkIZIP!xDkJcՙJÜ+MVO3>7L'H#A_)7ҦZ }TV朽Pbl<a~pyfa4)%ӻ{d!+Z'KM_JgP5Oe;E -\iB"Q@&Ylrk +m Ium?.֤La1<ԘSծ*˥[~@`Yj*MQj+(.L!= FG[ZOqbi8LhehCJGr0\EE{ eT.@ˎǻȏoT ^1_# HEREF虸^> tU6+ ծ[ȮK8܋q&0IʧUXl TqZ-Veh^|"$0\LzS?1 +ӯt԰h'p-I$hh>myS'HtǪ/.T`NbBLZ0 g3 "Q1B(DjsոcTYh^ zIDs>{)r(&ޯjy*NzNv֍@:e\941 !U%ɧPMMS N1jI/+}m̜MpAMn29j|eQ6)V(ː1?Zxb09]&oP@)Hp!tEY4sÔWw7U[O5uբ+Tt!!r$`,$(GJ8  ?D) "ZZu<]Qx8"^TwǾwX) +SoS/d&yH“Nȕnui458\3DңEXZ߭[wUʫwĽRz:(A;pq1F,TuR5twMHyJz:9&t&8aW +^i~N 17 [^vHG ~>7a8K8S0s< xZ :l.S\!:HCP6+nP[Ĩ'MQĻL2ا ]GP^@8Lu <Ő]be?UoE$, 9h`c@ߠPV6_r'i;/$7ƟmӦ'N$ p`~IK8'W#9O}xYEt+l/ X#++uɎE5i7# +{dErrnfKRr +V]  Ҵ:B+*rlEFZMUtePɏzA)j^WWD?ImG\dVcmeTFɆϨ3VuLJ4Q-ݪ iXA%o$IN&FtQ~lF_ dtʸ?@Lډ@>,Nw,Ҡhc0%W tV0!ډB%3( 9 n$ YgGEhU&&Kdj>?dw +8 X{vA+ V3#,)BNhHBCckYNP. a!0-g߬h!=dǞݑHێKdϚ哬[/ɶ,ǽPnmd"mkkyqJ-[mUyavoB{U[ԾO xʫdL:?VGbv-K-SBm[}>xCJy&;j$1yȵV?0EV/gɢN/>chxӍ?TZIӺe's,2M$@_Ѕ5'DN޵;̿'.B1 Yq|efqnvyP&cIZ=z(%Ę8iM7P'Ĉ r0{?1DE<%[߶.򞺾k&D?As A6>gm뇿eJ D8E:9:\AuLT?d(2A{8,jٳ~ṢOإAC5e7/gHg2%RT֜ jT%rƦIUnx_!|dig^Ws-=,iBZ+Z7*ET <Jv|9aO1o\캧.HUuѴm͓שBkѐ GG/-|:M_W֊g ^\kCXꧤ!Wa2dJ3$?9ń~=3u"ILDQ)w+&.9r]ʍܔI'lp8@h9Okb:C)S,|kT)ԗ`uW 4ih#^ }OP6:tpD?#eU:OּĨz~EdN(ѥI }/o9uga?YQ"E!Ilb9) L j愍!2\|ĢTfqt0'ˋ/ZQ6 $r{Q yِk`6fUINq-LO~"K: iOV8爙j]imF&5@^T36+^ibHs;Sֹjꁍ*$h ʼnBZJdS@4WL"eB:{^6 p,f| Agtmh-܆E㕆Y6xH%\aN<, rŚB .![ :Xaw к.umZ5(7sQNuZYY +^i*yژ6 +~AƢ+V Շ_S`a4@,0$d^"a"D۫VړZC=O|_$<鬡N˓J=?9:`z{1*M]@^콠i)nqH'@'4BƬ@b9zH/R~>[Q&U˨n|C4?B̶Ld{}Voѯ酊5Os E?̉9\I6|q>=hZ듦:}<^K+MU5 +B4_^/^lP2cxFt>;UD,ľDgvpa"/wQ-a]"Yj ?+gS9U>=ׇG9QMFRGiIRCEJ) +C2\3^A*G8~LUhB0i U`0TTSv*sJ!<sK5&[NˁcV[|:h2Ks?<  W]Pd0k44!^kZ]zdȳt䌺.+X&5q SͿ)APʹ6Yhg-t z2ϖH(3 9L9vR)ar/_ZzJh?jyЛR.žua5@(Aez*Z:y=蠫zBzb/_P=G$,RmbQ=5\up+Nr 02r\]s+Q)u_ 7m,y>-u|/uYrg5mIQ+J'iDy //H^8l\ +!*aO ?CPP;lij=Y"uaV|REޕZ>}+[ݘT ŢĠ +tYΠn9 Xbm{j xi{"܂oI-o{AIZ>=>FUihs0^k 墴XXɩ#UAyA)](1܄plΈ פ.`%G6G4}Q#w+/( ڟǐY-#U$֥*ɸWvs's'(h +uMĂXrUDgoU1EZ[ OC"5'1{6K2&h}ŏnan,2T%QǓQ7_p9lM3d{v%4^n,"X,wK?2ΙyG6bzo46 +&pɨc[I⎄Kbٖ_ͤQU]afc$]>ui:xBs*X1ЅTu}XJ %ʈ9y[,qZd #iOУ₨@X( YKDlzScn]x ~R9B$bmxʸS)5)vL#uF =Dsz51\΢bP8_Z!} ='0f:*HEx3h GF!ށ(K;CA?02*W;sȏ]_O#2L{01/o!ҁPt3 ؉OGm9!q mϩ7ϵ>/xO4G#5$gIˢ7kLNց5Qޏy!݅E)~M{x|j|{'asdxr(2\Jص0l+oğ7w (ަs72u#?,JOS,w{̈wsgj1;TU;V`A*d}<sy5w&tl)׸'uM$ ǖ(;]ݬ+\u +MFzl1aۿsKBoG;83gF7|'5y_G'\ˮ.*f4UnJZ@]/u}I`cy-{tnF-fG(UI9>S#$$ON'D-(taJ̻Cb_נ=YͭxkE}MlR̼#(: -̐ !ܥKG4N3Xl]8<"$azUU ̤q;սu{D:}L7oɔ1׆ U-RwxxѢ_)=xQȆ@wW@j4A+zǓm-[ITYƭfO[9]ǣoyڠzO1Ka;ΏUטif9$Y^'DbL"TȰqEE“.|yGhB[ j-04D}럪`|SED5JwMj/{C +eκ-N2_PvU`o gA::ߕz\}ݼioCUГEv B~lk rKU6QT/,〲@2|&HEHFM* ETH)V5b+{PBsIh>abG=9T3}&` ;('WSʀ +t28YO-ȗɪ $7>nM[w}hPU|ѽ_VORI#E6Fo{:P~RȍZ~`$7pH6OX04HThV&I7T4Ļf8?Z5 ngߵ^PBf$C)nIqBC$rsy&)=؟'a:5wJ4-V + ߫˒5Zv^ܛ9o7lTخ[liy[C5 C_*Lu<н2}EgZ6~kv0xҨ1JLkX̹hfR&zi1lUXY W[]%1QRK7 Nf3fV8z䦑Wt¹Urri34rΒ⋡S8=_8{V>ت'4_ʹ# RvRt7\C2[pTq>nY-*55Y4:p00olbx)Z'( |9eIߊYF^^H Pj.:/C0治!`Wkr²ɺ#C gl3%bY$fm|O=)) /0(!CָՖ,> aLIs:ΔC(w8NpƊOxFVUu1zĥvlh{\"n Jv8%3*#W(j$8pKjSZ8.9.k=kDjY}uʖ-V?f#>&)_&Fc s/;u?q}P{PE9}ikK9DnT59[~BPNJ(vұce jhMhuNd{]F }DY?6xur֧\ZX\xږovp/dX`Ն &/s[rIi*v 7RSgi7QP(p5w[ cp}Q~.ݘwDDb˟lhvP7}s2g^X ,7 9|H1{`\2\ӊq^+g9;b:u[ B %䙪#SHAElV$^2j{'i=邭SxfB1onٝJbf\A, @'V_ AT^)*IDŏ;MO`7^mv@&þ.y"Zޔ.ZW8Z]խ#FWmm9&F\۲9/ҙ+7V6UV9 !tW2*{W}K lXkP:\si^uCAP畇Y `:G[䬄bDu]qw sLz+'׺Xc0aUQ{Qӄ-*$@7R(ɽ?q"'FOV' 2'~ +V@QHkFj"4 s{ܭB\Yi}ɘzF;jY֧N@GB(غA)> {N `'{ȾmT Ȓ p'r+!m@vb#19';>|rg`"^4HIfC$p1aIt~ljKuⁿ }$AFnכ(^/9n)ӆko] +k\p@xG-U+krǢUjغ @02 F!H$7&9}-4=CRHGD{ʾvw㐫 +L1=j?8E*r9,֛'3t| m끍VA6Le*bSm3F-gCx[K۽,u5o Xi xCtT}8!7r!}zs Y2ɱ4sfct=~@^l<49 ^BYzɔr?bC3'y{H́M˜ :bAjW!]!Fא>~v};A5计vP$3{1<1q+D,2fwrvqV"b\*7;LOº`hc(~Ha/7G  Q{z`AdRg& lz_w -[}]k>ݹL[g~??tG _gLiz.^2/_굟v;wC?,m={bN/]n^ >~PD1X@$CB)aᴈ(:#:&6J`'r<~RrJ -=#3+;'7/TXV^!KuycSsKk[{GgWwϛ^m_̻ٹ K+k*-[ >پ]Kkç;wؽg^` m`]=`&(*,.0٘6gc`޼uylg셣3BA!*-d9\~r -#hKƍ@& @*:0c@4nvv~o,5׊_ꬕzUlfj&cbJ萔'Qzby,A(N-UTKjeaaaaaaar(e\8T㭧 48xHFnRhGڋv0JWE^K ' x9;pSeZcPd-`O=[ o`[$SK"cĴ8nJs^w.-{0Ky75墒۩NXaD:ئTi 0[V8dζoh>_\6,zq^rCjP[Rn;pvߢPیj';iВm;C`?m+>ĊC-1es~<|Oh 5c +]|Y.ՆLWg}N"`6P/s{uWe‚u{.. MرfR"J.Olks~臟 !oLw6b*('j#5M*ߞm @W|п.`ggÈkQTa]2W(qSbU`^Zn]eF{>sAgSt y+G%]FL`pl1+5aNP$~Udݎt퇠?,͆NOD9AQLA4 8WLs 0x^D͓+"bMkXMF~eʶo/Ǧ;?N&9={qL'krS~:Ozx2iH)(4go +\DP$J;;Sf0T},,mt䂨LɍhYm_r|_}hdoG4Y>AZ>N\#@ cV^TV˙Q"@)ȶ տ> +L6AUئpH}Y5Bn X&O# +D^S{lNCr W2Bug`y +LQ"Y4BL q"Tҧfh8 ٲo_xҟǪ}CQʙO$ &6¡tʰIfh: t7.74xt/U9a+yD5'`Nᑴ ":NRhxRDLqI~3!)"fs!+僚Bɻ?O~߸Z{NQ+z-b/, +scBaT4CmOi&;Ab&yl۴̄n@b#ϕfZ+Y!XyUso^ů`Wp˄%R)jR\l)G,0* l[88 q*?*O0`eن]Gmn an7qa b~Sr)uc3?fo}ϼZr;W0 x, }A?@PuDԂ؋5 jeHT/g(z*TM=V~fkaEq fTq(c2=-B-wo'N'z$QF_k'm{=Þamk:c%޲Up4\~jGpAOOj1̷Q#|xhgdafLdJwێA /MGͧOإuĖ- v@#iRIHU(A~XV= +;&mA{4zn+=d~qŅ[,c3e LzR+ĕ +рT/ʨ0 nQoE^wrōY`n-zjIi2Z |NL#g^ yHjVnKij+[v Íkw\K^E/yP5%“n2n`Pz\NM,rܠnmI$j¨zm`ꆳUwE/ܼ83#B鴱9>PNNcS򰡭KD5 O#˞|ȳsƫ3!LD'YPcNۭN:vkV+QV:t]9tA+B/BB 9 !BB,7$ DDAu;}Њ;i EE]m7 zŬ[ .S&` C͎ ~d[?\;>)ILFC`7g0!'Ey0>TQa +2O(e~V3^0ݐmk5Wξxk# 1ܱ(9;MkYT +ALأRF-;ұzt64R~Ҝ|[P`s3&s͒߬PiQ&K6Eb^H!ʪ^.96&{OZ1ہ! (;:Mjgٷ_0Mĩ'T)rCBg@.bk%YV< +|-a!mWc{`'A~Fsa_ duEdQAq5,2T|^Hhզ{?k/h{mS%7ϡ/:d&[-S<7ZIfE4fO3cr&mTâٔ3j@ O_}/0!8<%KeTf)}\^ǕfhfWLq I +*1RM#FtDsǍoϵg~k`(q,@cy3d }~XUIp< A~@XY.0+,Hʱ1%SảeX XW$ޙ7ͫ'4[Š:gqrF]'-O)%CF.fW/{,(@%pT\ +9Pm;曾3Dz}P Ai6ȻHEWI*'oR֩wggU5Nb*MG`=`eXc,S7Ԣw)!~1dLaWb^1:)Uy&Y̡tJ|j8BJ_1|^WxПĽ^͘:>PN+39Zȩ;u*̣ɂZOeK6/79YvaEoxRua;vp7~_` ^o%VvŭfAPIU5*E^Q-,T4slzii6^̗~"fㆴ?Mf}4ҁr tIjr *ɢE*7˥ zX- +ZQCH!ҚuvC=m..wvpt(jTbTe7ۙ#QHvf*mJ^U 0NkooTK Pw >~H9S=A(&=jIu~ ߧr$^[K} F5w]Rt6=ݍGoyg˦QIRj=#\-fq,Ow,wөUloDp&n4 $(/Y|l)c\gDDeyra%ֳ)C.yGl (P ӫr-17rmXk'1l>\y>f;KH)`Bm*gL $(Bt+#R@z տQ6i;*_WqN%B|0ѐe&`T M1$ o}<x'@x2$Rs/MB0JνdžKVD~;!qw*AB$["⁰/JM>/ 8"@5rX @!?S? +-19I>jFh>[ +2jy] a2Ynf{ ye ×/ zAMd"Ak NsmIN-.cb~]0uA7Cv0E=5#y/,hӢB$'8mqv/vqC.c纹 CΊA}y=%a7XS>N,[pg̤O jN#=%SS>ƿr<^ xC&ޠuފ!/t5hsV(NYiځoJps;g!'"")nVrM>]A%]FKT@/ .|EӃmYǞ̔.OY)fF^Ry吔A]7 +ZS\@ +7Xb<+KbEʬW⿟.Q"Gġ~FNJp(N.-JjD͢I7V4S&rJ^7 1`S[Ҏ'5|5o5h%%]::TqZb:Zm=Z a KB (M#E@Dy/xN{|IS +6dAU)WTNEJU;}*_o ` vI;jIfG&Y'qG*~L=3Plh.YT/h*uBժq({%JX٧_ +l2guϦn2'xg.0aJMt\X -ח*˴RMXӣ+R{Յ>5ho(̖7)&]̃cnw~7bW=R) *f8<ؑcl*JSw +=]&_ۧWcƕ7_Ǽ?m@;f~kb; m]lo`E{j 1XwXⒼYQnj\:[kj{e0ە?-Ul8y{A+vF`w+Br5".4BIoUi͖zr[oњL^5hQ9h mX}w&aۄkI;E<Кz3E5ټ +eL/їZbVSZtʢګ7j{غ&ݤ;_P#H9!uv"-d;S5;B]6[]#WPII+je^2ǾiNꞖ!ц~rP=ՒNVbA+bnudMXVgf; rUfGYU˽*9jУUKkZR6 tu P7 28S.09]!MO|U>OţP +F+B䞄%V L;Lr1;}~y0@8xŜ3b}wcuǯmnujYG̟RC >&F$ fg҇TLސ,HctW;P3j-n_6ҎAc7׻|jS?/LSOhWFeM鸴 131 Ƣؔʃ~ j=,UA=֡ؕ|㔀iW 1H@5O&gafiQkJc!'MDD>2QB!Ri:`=՞D;9N!.™, ]"h$TGb3tARʔ=0U70A 2vz? qN`P6b}mJH<&Sr Գ,`]LeˌP qiIN|f-XJ rw#H&1fruKQ[S1D:X@8 +S4`H <@z.@{!̗H_cH_pu{&\à t*t`}IL  8 1`W/G:V ;¥#H.ή¥N!@>' @<ΗU ĀhÑn-C.G,×e! &\Hw5a!8H[ v`ԝׁI$0w]{Ai?F@5ӯІmCtpSd4=ҫ/2 EAA4q>7urRfؽiyQzx7ï ԐP5S`F &· 3ْkK"]̢0 (IW\)64K~qR>j;mO ٨Au~=7C.fX> 0S~xa`-y! r9*8'qFx?- zj>8:Zj{֮xڵ=Nݴ/r ry@ !$$r@p r$.`Qx8;|<<&>tw3u=Ei' 5W E4v0$Fux@:a] + y&ؘ=+.JA r8˝s<?QDAz%տ +bSPJEK&Ec򼚟0O2zB@l!O4qߟ71n]d"C>jPcC@Eҟ@U,هT=65oȻ8Sʋ%)B%//%I+(ĵ9$ax=IG8q&w!4!SOMYs3^7pE*bw~KVHV"4dBXL% F rP qo[fv9HCu5cV֕aFNT+IJ-Eab5ɪy.i^-rHpt\dGJDžřu@*.Wq=?|3#y+x7: M[@jVi.,k jYlϿ#J"Cj(C Uq?Mykacffo#9zq½nqerR}I1NUƶ++BJ&P f1)bG  +Qk`Ӻî]snV6#ӽN%eԗjuJD˩Rبt#eDI>/$[5(Df#ſMY̶Nzh:[\KDUQkmFN6 S7"nVu_-!b8^$51/_}ҐÖ췦=]]}q<٧<͔MNOj~ +!V A.+ XmZTIj?Ij(F &5,4>׆$ 3V2ޕ{^esU2\.1*C%;BGZ'-$J)jPGfgלLak'M vw`_==lbj݌KkʪiUTTDrs-ojHMÒr"ő jļ%ms@֖{ɻGR89onVjYqJEuLEo7 +[5 [݈:U~1bѐѿ̺' c7L6ܹJ//w2;)6~zdy!iPa'pڄ )hš-נDhp:>I$T3AxٛC䷝-Ӫ|.dPs14^$Pn֡COc- vG>H-g΢<R܏::z\Ԏ:T[VTl X& YH!$@а/B**R @Q3qTAK\ :ܠ} ܐR'D0I=981ι0f5Dz{9#..o4[AҏυL[E\ ٤?A6#1? 7<ĄTLTXBC+"ZH#܎X2r=.06${xAAGxr+HF_^wOըS0*5H +#<"GVKD]87A|/oz_xhm0eAy|dB.q9ILp27a׸u`L'8:(TrC#%-)#(K=hwt|a4caAԸ@xe`!3enOĔwdP+9ѯV)arst8%jw +[w΄X_Bp(Y4ly a4pCd#uBiBꦱ_G,ɟ ѷ=tuf?Ƙ¸R톤qMܜ&Hj&7CuP Z`" +KHkS73 ڒY8_jtdAw*MU6]|KZ%9)%ig]<`5k*lt字7p>`RҪ2Oc4iWO@#Wt=+sܬ4Q/+l MI zV}Z Vz[ϭH{u?8np + PL|kNZ>꠾;PHqߞZ85'kMZʒX(KPͩx?o|_/%?t` ֠yPJ\tI}_Q7ZU2ɈVEҘNU1ˑ%bEN#!nPo_( ;e'>$-|XX& {Gذ4bMn"[K/2홅W*[efUQ-W{ m c` 3 .x\I^!aMgu{*gi*-H%V] ;ziY e<ѿ)wGyp;k] s?[ҟ/K!r}df_u(e{$zdB7dծC ]P=ݘ`5k'ma4>;ƥ.b}__ıYLh-<] 7Qz&$̐gW&sT1O|W̑g( Q! >c=1&83\Z71=x\ŒqoSuhx)j4Cλ]S{/<z M,Pm8 GX g@tH +i'!1 IN!p jIbsxw-F/GmQ>D}I:H7ɁU;x-6@TP ա0Pۑ!yGP`b* tbT89H.fIL H6Hf Tk:0E +oB@!iWh@k9~Pn3U=n!TЊ7T؉;B +|ߕQ@2> z2DoF"6 H@6B7hxAVOf* +J0GI` -atl?<C2"(BV*"?O|i  /ѧ@fJK7HZsR֚ +h,GA<;]@n$P+H *-(v Uك ~3 :Zy#eqӌ8s76p}܅ t R̠}Ɂk 3i; m q";;TsDNW]ݍjk݆a{O|&Y>9Ak +! +31e $aT%h_Pl!Q`qSFis_i~k9#o9SYޘ99+A6GEao3ĺk9>K!bv0m|;ca..qF<%u{') u>zAYۘ.P0~ f˟%tg_fC4fa3Lz-Kՠ ȯ/14C3jTBD\xlOc+^oCf€3W0-̜!!6fI&h |̐>LH)Ua`a1fP;-RCil~dLtMK:ͣ-1˷5Lyآ( iW=PeᵲdSӑ&6O1E' Xa,$YN]e +mPӦ?gו%=]&ճEoJ jP QVDT+#ANV;EX5ȏbӋ 9/FN|:m˛"+m9g(].%{_P>w34 vY:DZzlQ Qz 1&%@)!$$R 86+H 㞳~osOĽ.Z};RC5]̲Q:bQ9P_dC(ِt0rg a%ms%'Ď<#١g֍DJ0ݹ:>ǣ2VN-]/gX%Q}le["z%~ǒEc#Mɻ9yǷݴq7C*y>{T +/m~r4dFYפS4lOuBOq\ CsU‘ 14f#cX!b?9s  ?lJU>Eur5Ō2gj֕TKIr")&*x [7͍%r8$>%dC$ِz3, &LV;Lt[1[㾺UM߮UJOk.D4/iJM"3s9\-|Mf G GoC>c`BqpjiOkvT[~}kC qJ~TYAh{8Y♗.J2SDfr=/]O ϣDz~#|ِ>ugZוZ ͤkJlU$/ȍs'{hVZz VKNkH#nr(ϗu, +4M^ಢϖ +ZgPYJevyN$ό Z8)VYռ~|V?!k' +G }O6 q{G).5V˳Z5uޛʫY+' +Ձab1PR擘Èϓsbs+& )i#%b{wL7tܳpqFUcw,2,r*!Y&,J񎓋BVda9!råoDd(G%gޛw+j3xԼڏzV5{Wh% ٭sU6ɕayKlig*'D).e7r9ANp;D !ȳզ; [ ie\wIvJn Lu-r +j .F8#@ ?r+|(nhXOW=PmMn϶virxBKyAsuDS]I7.7H<|屮x.*aʫQy5:}0`9y6@~r=Y:g@ӭ9 }#E^Y|6xs>箂z׈\T>W@8U1V?uIk:+6 KfXV2ޖߖ{}0x)-,Z3[j(8A+(x{mxm^ײuo&C]7nFF &N3siCU>C=ԁQKjN/^Ц2FC({(7t(佳2~O1Np04j6DG#vtA{8L]NtGu{פdnW&¤!h2fC ͇qȞ4_4}507] +#{=ylba C0j7Ĕ(늲Xf #>a6@E#K VH# fz(- ^BJ:iП1gW)PPt6l&d? ȵA nHB@L:͹H ߹#g+.qƀF`J;՗1 *(d?| \wN!`I:ZLWh? ]'逴YW99; +C[c+[ho-0kѫa8\{{p|5wqmuPKQFƥ!"d]BB$B!TPAE֕".kNx?w^ssEM#Z o=@ǗA7@ +[]@K"C$;ps3vTGtW`Or`KmR߭@ɺg979gŬSBq~WS| e8m>ȼ*yYk'ɓޏ> .&0 ¦%@qxac>r-@[2yEԟg &GFO \ieg_GK;֩f۶+VحI] +3ëD8 _. o eyBhFBpjixPR 2\4r-Umƈ?vR(fٶrэ +[\m)*sC*$>!T|P"'jůIYbDЈW G Y(uFƫ<:M?鮎D:FMcJVf{E!:ͩkۉj=Rd\=M*6U+xsҏ>;!F 2 4lkcW Fef=t] U`* +˔%M^)0׈/]')edUTl/[&)&9>8$]'UYd:!/Z+ۺCp]1H}Pw-dziղ"2Db*|^~ )7,{%˛eRD0D.#_2=Ey9EpZ-4sT.rVӨwdI)[a"}3i/Upv SȘ4ar=xwK+Uuԓz#^[Te%EH(& 5da(dYh""x?\ HsHWeۭ5b})DI}9u Ii˫|r AYa"}NPVKvIW7OkMsxo~>w{dy/~[͢N'5_nFе%2Ml|i\iVKai^5h} C i6-gkNRIEfi&{4Ȕ$ iAE}{}=}]?]v"8ZaXZRJ$]C_i/wT cӁ< 7#9zC-תF_-,#*8T{XbHͪ(ͦh鍮huet|Dt`/~%&Yng½3hJ[ εD,=D]Qޘ_/yU#~wJmީ`҃U»Ma0e6O +BlMzPH*[Õ?3{94kuSUxǴ Ħ\Oi)_qcAC?*}ȫWpjednY +@K.ܿ p.u^Pw3r^~'嫜,7u;⭒^';$^*Q/ROv{7هˇb'd_OkÃ[PYW[|JɾCssK2zLS{^xEXn#Ps)~v(\YKL_?0zm+^o lwML f́_]}Zt| E}렰~g+#+c%+_˔\JXS㨲,TǭBkĖ.UW;ƨ?OK swn<сpReyCplW+s4@X|fN֠}?z8LY48Bor<Q1BŇWj9'֏s-8F7GXU6ì:f{5󵃒ptg:C>&:)Ds J:F_Sfcr|LbLl:!M1gaXPiwZ-Uq*앜ZAv'vS?ǹYq.=,t}BO, "ؤwŴ1-KMj$H|HV-9a<(,((;?]svvs\q]r4o8[N $C0HgH$L۫!03ꋱ#%i<U|PcBS%/:)D]z]ܺO;|r3z5;0қ]QM^k~Cky@/Vb*Z8PpBDfHDD@@$ ! !sB L02ƨ(@l*Cժ,Z߿os~߳QIAsz0Ym8csgQK x<=JL}/ItFz:KmqN+D{ʈ6eܭ-eIp-+`H)bPICs>[x1a9޹9 +orw,C]dvR6D[IB|є£!G}FԦ;4f L&/D~O3g Wb'Z_ge1msKf&r !գR{אyHo30oeUm^<'3s`\xILJHC}[xRK&nZ-ؒ[ԘwϖT>U̧~|YOWBp2e's/;/.`(= X$rE nբ X2_'sD9m/aBg ,iB}32{+|, }G~ zEп:×b~h%n٩(r--(2/qW׌uGw^G㋆ o"_Ҷ$g|/ܯ=ʀ9WgE%lU*rͩtp"ċ_1\FW 1fA)P̖ܘb7viH@>Vè +W/ކlWZ]LZ}f6_2ō%qOQe +QF98S"ȔVYz= C:! K>3%Q(hVfI%<+[Aײm@(^ӕm[-M ۸uv |USBeV=3>i~ +=>UO?"0?c`Q&YЗ4,f7~e%4Pa! +LpZ&n %6H=@i;J㸦ET#;VX3ITM$Vc!?.`! K,ZLc){~q2=~@eUkבdӞ+KK'] $k.55VKq-c'㚧/5aF#/,D{r&AZnك︩o&0{#Vo^\KqGuM>kgܽI +tƒtG.49GXV/Ɔe^ع\+jKo( \aX t:v 8ܙ6#g{c/Ls=0gΧa*-tZ\ p @#880Q3 +qx^B8Ix/1!.ț(VsP߃X TRqL>Y3j/4BflG D^tH]Dz-6<Ȃ[06,1,0E%H%N[Xzyt= Li"G!;u r# + ,_Uȸ`uS^ }BoGё + L4P"}H L4'BnbȗbYeyi 2~ l\7ئ%$eCe RO ?@Á?݈oI%}3O0 cj"11D=r3W`pנ_UkصIV euO٩?60d|d&3} \OId;l#( pEi#?q4/ǩUܢ +Xph? F O> ?Gcq/MzXh +t'jmI: ؤo᫁Q(j}FhO/S -X7+|d0ĭ3m4Z?0t 7ጘL q 4'XA4Ј_sH Mc|1Qڣ"PxAA@9 F߇nb{h5o+!_̟e'7=%*p?ɳ_I E+tRa I:u}J^aQwx[Aٛkš73n|(=I=@:q-ė>I=67k89p@BȢ~hY$^;F::(Edٳ*N˧፛%Cn{w6M[ -*@/t@Shr ?Sk ɃN.J<:c3s: -7ߏYm l[">6Gh@_^$S&}M!_&Q ӽҏyZRS810^a|/.{3oŔXFW܌mjkl=o&]/W uQDy+9dRkJC>s#3_d#E]yvܤ5!âYw֦Qr޶!zo1ĭ{"wYWW1Q z&3y#BH"H1zNeS=<=Cz"ҜdzCv²!)۪>H/]ɡ:.A*ɔ :VOCD'x"#m}=tv3jmWif V6eDSMSS63ksmk +wXb|CU|SݥBkYKY<:ƣ*Їh`OHô|#| +=J[x:z̸[ٚZܤؐ-\ˌ3MmQYc[%Ϸ+OXTRF=I+N%QЕ +!InCHS:C{ ZXm}wEmPZ)6I8b]$]i_VpVQᜟzv:{nJ2u]4@_`($=ɠ@iC,66v|f^BJgyM`żh\E)uqvmaV}~fC^Fsnzti8h92TzT[ӞVB8L9CN(dɾ!'K$]4zTts]W>>zNF|]ruHS㽠$Ar%bf&'gNM7IJ0y {Vw1^'ؾ|2Q:xg5Un=h(? WSjTQ|Lk~anUYYgdFoI'Kv'ħJbSx)SZF|eg2 gכX8OwKVZ4Vbl%2nQNn+3rCץD%e'LJ3ͼhQjQg.i&yj&ym. dWc8 xjr5 P}ي#+W:IݵN.YZ&0bs|~ܬ=a9Ŧ_ ~ ~",2rdoWP2zpD׫梾f%kwBZǓ)=wn$U S*N/Kϭ+]5$ygh"fP0hPl> = O8ATe?.뗡V_3h=5ARwbߒؚ"/_V)2q۹ #K{.^12+# 56 1n*3eQ4ת5-Aa&#~FrZBVLөtCCV]ZsAѿ^ů>ɺ]>u-u=޵W>zאd +{p'h.jjio_kqi$O<3ꖻFMC:, {F +էyojhkֳͣepmʹ}2_N Z6Ƚ!ڹ];m6rݎC:u+h}zu[gjQgQokE7Ft1ObՠPQ;$.BX?p{q~iwg{U Dq]5.=:X0Xpp𙎰8&A"p=Òk >~mZ7v7NG9׏p:40q(DI8,NVdڎ>*]ug#5'= l]+V'?sd9F7g}|1ѯVpøp7wY7o#/oMSQԴl%ϰ%=G'@AN x8x~Vh\N ! ;҃ 0pb?’\#w3 +12K鲻@ֈ*䌨*?& 8L[t -m>2^2n!.:r6:-=:Mu(Bځn@X*\V3Lg فd40 SeYE6XNf) +b:E1!]@@V,XG@B9x˓0pGN'Ae*] +ٕ"~*g]8+kw?'Ζ4'>y0d˜,o+azɓc9 w$̘IyȾLzAmL*EQ#v4> ki9hD.a;W{@wavmk9)O +t~W昮2:PաEXYx]cTxK㓰[sX\pPpHpL +i=iۓDw2ld7Mc~ +\C\bf~qNQ씥7eWP*è("k@@P@D'iT;;!$@H  KRA7T\HUԊ`A +p\;t;g!d>]Ddžfo""nφO2 3|<Y Gc0v دc;gv(>S7{}LZm"hf&`tt\e6yVelD\x\˸q#gq/ Oflž096ݏAwcf]5q{XD|7RGzs˧ShFSƯfcr(5V OmG{7>&_{DiaM22t9_?.@{p}x,p'q'$ //D$ QG C }\[BVQG"¨lR]l%ʲ\WDC$*(HyK\у\a\BWH.q|'p ,pWh %pIa5۠6ZT߂RUjeZ"BVE(36s=eE2?60G1[– eL`CK C:'15; x1\ЇޚСrm?h{Ae镕1Ʋ:&զHXʯmϫy˾̮P[}$FpC6yGb)C@ƸkW@zxKe Zf4} =l9_|byYSMij-ϰWu\WNmVtC+ҫ{Ӫoc +^0XpUc=Զz- ڏ-+i0 4ۜf&vS Q薩xkvԍ)nfj +D^`\`Tt`h  ;O.Ө,?vEk]8wGE & I bAmq#uSuCQDP}׊,."ܹ~|{='OnvlF ^Ժm1zN^s!뇧:*qIY'Řu1ptlÙ)=S ,h5@! p䅽W`Wt(7hDk&]]陚z@ʕ+l”Ϙ脼ruyt&.DEPmn;89=*tǶ/XK`[*[ʒUM%k7{wg)..S߬x)NQpw ;8`. !@bd;07⻂. AF.nw +Z$h#A$2 gN4_ -IaMm$kG=ݒ$$y.uGa买]F*i6K:R6Hɱ^Jwr6Mg- +`,/bR>J\gAVֆ[m?3d +>N;eYYYcԱMT"5EdrɹVF.52r5M9mh]sLkv +Pd-K^nٮ3r~A␰Mq⴨U#jQ\ujV;5E9GT:)(>8U+ȥ2*"uy 3FsƀB9@P|=!;艉1ZwĬm`]ױ)꠨1Swwoy.u.ʇ׮Q.Qn({<^(e24}nw"LDp.KU0zRtj,4zMmfCzm.Qm.ժUn\ +ur=rgec=P1u'?f(윉Y[RGZ4YOѥ u+;_sy/ˏz<לL˾w5J5^%Onj熚|4hgїw0?S?'QcL4ږS/ Nx}aϻ*^P]اH[sM2@58_KCh7l;'o ߇v S4IAx)і/m˒V;1mr/MާĸMx_O89P=@_0$_7O_>aEazvAO_?<9$a,:VLBcr ެZWx:AjU&ǒT%m[bg|~fzuKnMs~Sh`n`_ճoճ'ؿS!7'6G:ŠK,wNK)*8tX څE¥&tEk(l\[iqCk9gk8cj#pm!FNt==r=pn u2}+ߊ~Y]OY g$$.l*.ZjJVrߺ"-_Xk 5z%jxAW(OMFf#o>EqI KyQ*vuͲtgSXh+je"PT)0} +J~\K\@щpɽu}v)sVaUmB*fbI'3n`|.C8tiGh)Vd+1*ϪZrܸ']g}4/~k%j~EmQћ 7 dgY%HW g$}Wbߵϑp={`XM8{Zx7ʦᄀs[V;1zri9u?0@/"@ҷH~y1w7 VCgߧb~?aͧkl` ͏&mz_ Ԑh! ůϟ̿$^b9x vLcۏ`˓uc|t+6>ƆgIX,stiXy)%#L}kiӥcTG2v$d Q¸Er?\;1K+ôj Ӿ}߁B XsHnfx WgmYm0}:/|߱Hh &P\e/%a,e`4ma0N`$gq$y 9&0FM1` +9ppL1O0&st9AN>Jȳ8P()v~})~#4H D]Z x '8WW2ׇLNFpL.HqΙFOZ<9bʳQ2 x/$yC:/}HEșrtcY]yI@>Jt,DdV +e녟dۄM=FYH^V&n]-]V@R['yQܷU/>%G :={!|&A dD@|Y'r4W?׈E;o$#7:W +j{:Ot}нU v[NƝh'9<;<ϋB"VV&|X%SlTlPd+ږ§}JE +%iŏ<ܢ{/pw(4PfTpz-g&U &W]nb YTt&{ЗϞ弓%#4SѠt_< cU~hPz! o7 6>hR7MYlvEy[zf˳ԣUIhR~z.y'A2U[: OåP uF{-\kp5lQe&KL/.vAu⼪򜪬Y?Ψ^,V5Y""VT.B7I܍Dm5Jo$Q z#5"VG7;lyze@=O~Ѷ(WA'zSӿvfpוZzfGLR_lT5=TFˢVQ{OF͏,}<ﱈ~y|Pdk(+cKx^h^0]P4d,9Tr&)RE  {b}4vm^Ln1Ŕ\p z/Ҁy{r߲Fc5N(OTXP.?gbv,Ik6zHoQb퉿;aWg] 4hg +sהsU,hn(_1%Z/Q X,=HZ١tܔUVI';)e%V ޑxq{Km%`5uF@-@!h4C}!&⚦*(]92{n\-'ﱌ3N'` QSb}9@]*PRbRy!|55teazGKƦ$&OL,LI(7+Wyb> hYL 2)~G"ePTZ@^5҃W{CX^#zvZuJJO⏋̛]QfazTyȊ_L#˟Fu4t0(:@-r~#!;<Z꜑^ _4ho>4>fDlQ{S넆Qu%w՞Zϧt$`n;Sr[WU +ꀒzNq&R9 5BjT[5BĨvVE6%$֔TlT;~{әAM?o3 +:i&38/)Ҩ?i=31g]}n9vED5Za=CBԉCizAjmj-1Sc/\Txec.*j#@fH=40D^4F쀐ˋ|ۛ!9ۮ֫Qih"Ԓ*=V=z믾o0 +1\2CQ r?`~a^7c@{3koamۥ{Y-ۯ|njdQ<:7sf h}|_?Xe,}%q<?I,zVgMlkGx<}'P+ɟF؋}o+`Yf/fóp /WZ8ulc ױX0rؾ=F\ƼwA4T#-,z xX5vs{{}17 o78aA sV3VSvhD5(kKp6_i8f}Kf ƣ0- 1$XƱX|D6 X1qxE*A$G?~/;>llȩKα@{2& Lw¬a#J2h`eF&2lLsl%`D(6l~&]6Q?>xoߊ^[wƾ:2t4̘l&C]ӖGk{ګM"! la!|;W:t +Nߡ˩N7 ^;uSڝ^00Y`V`Or,y9s@l'\{W1z\sD[:js?jqokp/ܣ <.bxB<^4 YXP0Gu20O?b <<37'9HitDZ;vۮkXDA4AMjD4 !$!$CVYMv뭕 +U뱶Lg~gyg%^,(xЁ' 8vancL|N$5|d 効x _;kq{enR*-.Lθ(dճFD-sQ7sRtsb%<$N"Iī7B4FG,X9?$}MÍd\Z#Eq&FjM)dNaq5縸;$n8*JwD HuKn>)%HI@>DOkYScT:#81SS0,f}(U=׻̫g˻[wl߿d]]{dwˮL쐍jHZLJC\" &Mqb<y27`\+9[{=6[;7;}wJkؙؾ'm.lOߖߜFMom&!gtux!S9'gb0c.3cp kz%ؗTve:2}鮀 -m}&Fhhih}: K'In<7:Z:6&/џw*ѭ\.ZQ:vE.5G=גmМؘUܐ_9.oJM[\Y)2 +uGҼ֑⍗Ԗ&w -C%"ǩE!̛3)i;v Is|WGp:?G3+aHaW8F3[_ӕh%jG$ceNANavٙшlOR9nH2p[F@wqP7nsUU` Ku45 0֬aj,}MWW婩VUV'*!ٮə)^AT{ixzAzy+u:y@?>z~he歟dy\OL s] 1.!m-:RZiHN}\2քZT2BȌ-kywu_s~;O;XΒhEt$Nk>o\9;s %lm75!wNqp'oq6hyfY82' +º=V2bS%ٱ8y }ܔ=k3RfMKYcSRֶ Imc| &:{ V_x(Cg~WfYf6It_;0fAfrff'SN\Z4S&iM9<]gy&-kZ?&mSݭƤmuMTZy6Q:pCb<k!U?)Av?i'As3}GqsgRO'd3FøDgX'*sz; +J2뇝ll\S}OH ^й`Jf01cO[uf0s4Dq6 ˍ%47DPZ۴ujtrtΪfˋ pP;E!Xu@x~sBTdˆ ~|-Ƨx4Kb.|]^ރ$ͥWhJLߚ*@}\lVh^ ?@~Mo\nV$Nc|'Aw0|%w7pom>þA4}⨜}2_we2\8'\: z`G]q0î†ҿU?OB_SE +k6cY8 /Q\f?[_~ +n*m=6[g 3_ZcWN|koIQtH緳n9F 1lHcI:4O&鯐\_$v o},߀YS L޷LJNtWzEYI`i1PU>R!P4WSi֠'HH#?/2}@0VHS>F-Tu޿hz2+e%irBW/"(SVC('T۾(#e,fO/"TbkFTmbL$&4d`ϻ%+'ԙYF*j_RZ:϶B:݄Cňib7W|=[h;dZo,xm+b^Xe%ՖuTiI_Eʮk,,dkv"XEYMz%xc6ye=Lmmsxf{T>_7`➝⮽3Ǿ캦='3e$<>؏n"v3yeٯ9l!jrL1JK{nҔ 3(ir9N1:y˨rYE:*\~$q \Szۮr-l#Jj@Fq^}F*e'ù'j%}M.=j1TO,x,&;5\kCqeJ=Y@ɰR.x=9ߑ7\룴ΊYgMwGy˚yN?#(i e\YEun(;9t +sȗس |)2F(ϩ>Qߢ Q^2 [Կ?~ΔxG@0c)1 #gS8r- ?'7x 9Zgi +>RrC'=JxcّP(v2MS;'#- š |ɉedG-̈u#tOD=hdޑȫzifܣ>8Hȍu[.=u縋ݺyJ EA.$$$&!! @ {""7 ZugtyVמvέ9:ߟ}}턣H(VKO@JzjeogwrVF x7g"ٌ q0E%- oHʹR;}RȘcEqNJ"EфLbLz-qXz=X|=P(J{ +/I}CURK6w/{tΕ|N̔f Yd(Sc@YGeN7>&kMud~\r-ND'\ t=jɼ;H{x`O?se/cLoÔ|/*dQ(i1>03yCBꕇyoR|-Y%)_s;_p +*iPI$ud'|& x8 )ŋ&[1܃#M{^?7ϯR5yI}K_${%4U +!fT/`B͈i21ُ"VUnM Ks(S`uAu MݵU='y>uA #.J'WL$-Jk]P0aJF?AT5;W kd5PƷlV8>m(Y۟ю񛴧 +E υn% PtP3~)p^5|1* ֡טK4*mF 3`4 V9M`J֏ +\9SMT[ajY#7 H\kH%q%L?BW6ӼCh;$E\NUfsMlf52q0aM:4fz,( r&%35!Q7gy WnA- +l+{lJFU`euzk#n p,<%&XNY-7f%"YZ^?NQgS}쯠ͱ ~N8ୗP_p94qcacdݗr~g j"RAG"C%$CۤyxpO |m{;bl(7v:'XܖDۙ\jI1Bz _:.ԺDZ׍4SPuR'H/y$H<&u@8JE[Ҵ +Mp{^ӻoEz-^z,בTr S{&UBgQjDX@PH-_Cҏ^5E0IN;doby%}/{v`HP( } &hu@SrUm˭||c$36\Y;$wn:?5掍CHPt!)Cr&TŨ +!kBe!Q +$œh,t+ ϕu|ĕکޮN<wdNL]н +C U8H*#2TD*i^13"҈UbK##IEž(NaWY\;@+I=$ZC/BQ9s(6|hQ6a)J(!iEQ; euǢ yل;臬!;H%.%,ăFKIUEiP6p/ʰ +/b%7_b^95WlyToTrPț2U CQH T(%X^6m2BƔ2dȐ"ZIe >Yzw<\yո]E1i;S;vɳ_4a)"NAP6\˺ ou \ +3О.Aߛ8DZ((o ]O;۱{T\Nv5ݮo 5A@rY0DڃIEɿ +΅`[lӳԚza?=D2/zHϠS:>YIX<=i7 yȾ/:? 9&uL܂[c–/hWBۗիZ8Q34ݎqar1z[v_H\ɿ CC[`*UyG&КiQߙfaON|vEO ?!_ïū6=" rY"fA>Dj#,w*m9A ͔&C}e.ItvI #@K@a%6 mQ&=PMZF @*d"P3&Ub#hz}|0KQޙE +ycj<7<5oHD^5J],wIO5|Ьf3o5IivS9H0o,xmK^hPyFE:;*wR<(ҐjM$ e=Hqe,xo9Skռ+˭E~J YPiu +YWk-+Jz(꩸F5v!{bk/[*dlsW{rGEwhHi:d.P}d?{-_w!Sxn*S;a5ﰍdr<ǣv̦*}KWɕ9梋"W\^n&r,pu\yΓxMLJpe!.K(r: +]7smn:0O7&gκgN{(N}1!e%v$-Mj1Ěvҗ)ý!(Mw-6tWFХ\a๕ɜF~∷FygG^{ugoJ=](`?WGN";\+gG- {YNN&p7860GGgs/ ԱU:_m.ޚm@Ȯ8qmFs~L0c#Y7c8꿄#d'= CI L`):ǗLwO{ Ů>F(/˨2Vȕq} șޜ +$3(#ɘ0 I OjbRq d5Ct;4S/94Wogh~JMT[EwS#û1FPg1\ 4''؆Pg2'zqxXM +%uR$)3&eS'9,^ggzao ;hkF7 6?1HRci2z(O˓1oR؈3&wXx_ҧ %uhGL`oD8#b9I:"&FE?aj7N=xuQ F1\ZK|=Zj }uyd$3ŠD fQ$O dI$EG-:Yl9z%zW鯏d.fG51cNf2q|+V*[j]BjP W'CN8lFFt'Rb'֍]G4}[EA6MڙlY]VF Vj0ZwxY&KJMWdIGѷW=R]aQi͉pՖںRnkV i)J<*T30'`f8" b-EUnWVjgJv5#}K ~潮y}x/=_كˡmE;ȶ'iHGi25T^c)uͩTm5,j[3b0]_d>n(4_60,(z!'dNKG%vM +ԟF0jӟ#"U3YgZbXSnIg,Ky,E@hMjȷ3Y?ϵ~o̵5Z{{ 7kK]?U~Y `zh*1/̾*fP9\BGv;Cs[.>chxbx;'cC1( }p U|8dCVL]Y7O u/^F;,w*ӝp bq׆[ iwfyќGP 'dHp\lsw慰?ŅC),K~\h=sĒYBw%vYe:VK&o>{=+\5$y~}RQo. 9hwېUBy/%&t٥dMQ6[8,|)}VɗIj}A|)}˰ۂXExPz`$*V[A y9vh̓fѯ+U!ZMƇXY u#XV7%'y:q1,eQ֯$!xkSmܭi|W=::f:Eӓ.E.@T* uxzXEOܛXm(۟o̼mKL"^m^+ac֎QG2h#|÷٩bOn>Bvv^9;0明Q't@Ft Fq ?9a'1^ygx"?} 1~rlP&h;=(GD8> £B4wx}ݗ_AtE~rߐC i뻅%ol+%= K$yc.u\?}/! h;a0R/LDp\r t +rػ%˧M~&HHS%h%hՏu$EEyT0V(ȅȅ T"PɉR +CUu+(@.gK.{ +n5*^W13D{ ߞxr/ a-xLe6/,Ͳ<\rߺ{˸cֵbVnuV3\jbLJ +Nw*8 C[Dw%Nۍ&66<}#wmRm-|nsݦV%TZs;.6q4]w|*8,9dwDJʵIoY_3zz$څp.vqؙje.?leQɞiqW3zLw ~4_.#zuDky׉{Nj>\>39cIfroq_)/`j XǏFʋsol$hpl{ ѭk?F;g'}BG$[=l3:L6za\Ǖn\kZ͚Ɵ X^Y%z 1HpQUEH5Gؘ95ư˛z@&Nchj'Ʊvb"kX*V~]·_WR>u,^դ,bTb6~K7h?쮢i|Gv{ǑIl:_?RJ͊,ɹTĿ+ XϢf假/- ( lAA"/a3> S̚@VPpY4SFŔlʃ ( .4laZ! &7C( ^H L("d 2ܥ!_+1]ꃺSlOMPCǰ,t"KC "LKyX eaY85,:i ̊-V̟V( ߢߧ~V*7D(sM.KyBj~c~[c}hWj~ƊiXFexRˆDDMDE?}sȏ,2Yȋ\֫jYڛꬨg( wyo8 =g[ Ӱ&3{4ҁ +ʴh}(R5¨( +̋27#(TdG+W273tt5st74i4x-b$32a49vF(X@ՌO@n8 tc)M@<](yf2wIdde+cJsUi nMy+9R)q%*)VhN`8f`fn4<^K-d Wd/"ۤ\7ĴlV7㾠Dqd|020ZRgI7)>Gh,V&Ƶ"U~ⅺ-e/\c!kr֗,8+NAFsIM'91HL1$I0om*R*UqIձI5z#Dr[YY&;1H#)1Sؙܤd% =y8cIN”GBJ0r8[axiCT(͐u3A"*ZhE -SJYZZ H"lYlC3g9J\{>o@Wzˢe, +o 8V'xwpWpgQᆶgPN3H Dx8-u( H)"[~$a2 "u 6&x5b_X8:.,OgIyỴÏh_r {VP4s~odL x!2Xz6ῢKVw- #F1xE3̋]5*AcNKT欨R?4gEϸ*2y +OziC/h@Ds X9>1}GŎ=f*O\V2kU3Vό&5pR9zSgd/>wxĻv,ǪZxcځ^; 0焩Hw}O?SCd~!i?IgO}دU>B2(ĻAƲXqܵm]K\wyӓ-SS옒2)8lt~l&m`F_&1>5qIMbL>Fd:FdL˜O> Ŀ9d w}d4##4S6akW&mdBz?ƧǸǘ GFeLLWϜLgmaX.laYmsl JER<>KkYw f}9?lgvX1kA˳}hq[87'3Ѫ +BjT2hrP8 k?*zGK-ngYDqu.NsZˠQ `*ܔ߇45 ܑ]yK Ֆ H fI?(~q[D܆n[ͯ:j;Zr5=#x`J*MꓱP/MS9 q\B.y~}7_YY>T,O@7q뉻U5hm@(yzmqkKci%׃P M"/b%JHP0}W*& .X!?Y+yA2D=r+yNQCJO%VNSFs+[{^p򞅼eC!XF$Tqnh+\+Qq]zM\\PՈ(QJ&ͬ(v(Z?^ W^B3Upy*{djU*|P +Tu~ɩf +'+T~puPԆ($=P޶ëV>%.J'ɤ|Zp0 FHouzӸM=o-^8YU_) Tl6* (XHY2w@Q-BP`ʗQt[tibbڕצ=7^ס2.Tuəns9i&?QaDi8eQ1{"pl+mgy>E%XfovzoigCSf(fjyӀԚ[rl,9c(M'%V?{Sl/4 +ÏT0]XPC:NNpرX^M^dKdɒlɒlY,ɶ˻8,b,8544 @)L% Mi)0PvCgHI~<|{9/&fkX^3˩88םaisuc! W8tM*;BY-oWW Ikyzݧlr ^_+7xdS9q)1R92э,n¦ٴ¡f {7=W&kajk'“)9)GSX\-b-vYLkf!#Qr0c)ga߃ e.'Ɂ&؛\N7ٽm Ӛ~۟ ڋk?bL0z_ϲL,Βf S9ͼ}rFf6v5+t^Q{I0Y.1{ ċbC@Iol%s=%l\Rޭ,f0}LfuZfJ.2BdƊ;-fdxiҽ e1ӫ+=N3{Bx5,|̕&3S2 2 +FF6 } 1PI}TZj.ót jx?B[(ROH)S?_3Wq'*1UDU6U W1Te`BAC!@:Lwu/]5Dkf6*dzMnzO6r9_̳$.W쪾DMc56f2ho,XIhhHL6D6FyyvAڬZO +QZ_ +-koD{YCXuv|)ӭW7o`NEC̢Ӣ'j!laqziVE6Dm-fI~^UK\+_7 .h=)0gc^Ÿ'~A>btٲ鰕nZZ=Էw s&:\'T9zGv~r;y2RGEa HTa,^7︋n:pftpV2w6؏=A{vy^V;>U9˸(EVjpZ.W#wiL"N! zjxp7hlۣS}g66'*W7e/Nb?g  6\G駴yWK/ol쥾 {K [`nTU֗U斏EenVd'\2I)9aߡzn,djc1l'CO1U=TR@룬7Di_%}@׷,m GDBO"wMlr]a9kd+Eh%")R1P,1cc%kJP)3H̆afc1w.t};坶6j/(rU+bhAmЬ A ĔOhLd@dsg+kٌe-pwmzL57)ownwl.)TF߉;byT3y!Zڄ/7: +uq-H1^L3x\_XϬBS:?H{yH:}rBuDW;NSeJT8P#[9BS&S\J +\@s}ޮh;odžΊs 箓=03.b:2i6nPt_Bqh +{Rc=93r۫1r/;2= zpYFf&mr};As!-"crǐ6d#CI`~&ѿ힀G`szURGoHO{v#ϛɮI iH )AKH f[p ![2,a6 ?'zua {0zqr_\C[%ă:P3)aI{İw6>l#bX?r3FZ6u5XeO+`q5:DwPu @^Cʨ@G$$6'"g]\">b%Č^Ś1X=&豻YYȊc,K$0X;0T[&`3tZ4ۑ:cI;l`}"$6b:j6=n+ƭd8O`Ʉ o=*:㟵jzڪin%5CME+*00 3 0 w7@ED0 RMeXV㩭.~?>}3~"E J-4\$AScy1:EknjFKL1~.>Zj ;ZE?"L*U'=]]=`ԤL*e6)(5Gl$hP`vxȷVkm ٶ'='-%q~~R//)g&@'bVjRRiLu&EZ(ƐoM ϚϖFn4!Uxr8C;qL{3sfyPOr !u5HCIB(Go_B=\{<^ OwFYA\8tڃՍt׷;0陿kãT谪D~UڇR1̻wOs9Ex]+rqRqfe%.YGgiؼz_%9ˤ} CbN[ +׳*J#5.r=Szg."3;fҳ帱斐[ׄN)}3s%9'˪a΃T8Y};[νoμ2"7a0H(!)cAH(8S 1XrpH1w{#'9'h{H NZ8KB̅I2J (CQ!EU)n 1V'ebOStO"LL᯼^<tI{W~'[}+ᓾ,;`l%##l2PC*֔Wtb+ܬ]Q N*_`Y)U\`Y,+ s\98Uۥ[W mwKR1S$VA|Ollbj"6,5Uʲ:'K|,+aQZmfLG_7מc~%ۥT U>Oү:yu7a+6#j$G~.a% 0!y m21jf5nafc3h|DO+"G)GCP KP^T`߲i$rxl3%h[ߒԖ<3I;$Z_cB&4˄m/}Tw2@6LyYq[`Y33v\nfQLi3cR[wEr%ܳkwJd|=mqgGc:Zc;35~*IՊP^iI;Q+;afL^'nOOdL8FwNbTtF3BCwxۺ ۿpc .}?x\Nڨ>hT; v,~+ܷO`! }f7v1 {r]σ Y11.#*Q"ݯ qZء7mVlYvj;E5N}RAvѠ"n%7Zh)>츆'5tzu{ + ^yJ?hQ,(Ŝ\|3җ;E84d$ZJ'jG{Dh!P.T7 |?%8}ʁ]6I{-pp0_MxO|(>j>EK2 giY}8y%D/(V9/rn筒/4 V >Ag^t}}K^l%(2mb+V\嫔o|.ߛr=f xKO`Vh4F﫧)vWu5) 9#UlJ+K\ +V_ΙUb;oljop[]X9G?p{Cv6ZxJ؅]Ӻ?[Z\mGmx]:X981w:-9y%'^.h|cpnm9 +!\ v>cЧmtknmۥvx: /OR7wK'i|ɱNClPU]́eS>u{]NR2uvmj>`@Abt_G}7ӧߣ%ve^=S&r|L Kàl11}/.fϐ%î!]Mv v6jcF^qרuQMl~`(]s1{K[sv`N _HwޗkёԍejǤP3&cQ=ױ2*US1n7\ߠ|96 ?P wu~5Qq4kdQ? 3>av 㵉QlՉIYOʣlj6N.tʫLxaUָ*_)0`# zJ7ߞzs0Oue4gSf`=,ydֿ9M_ŬU3k)T,iOJu9C5gWxcH6w:E>1ʴBS4+LqIf r3w56ԯLd(.LIT8Y@>pWmw|;iN/Jh+kQE\X2+9dO$k~ rX%%VFOr?I +#Eo*;t}ml׎R-̪1L&/`d40@ 6҃0,.&9U;I4#|o-48Cq}J $B212L"Z(((R:F * + +"+ &*׍E\ .6dfiSSMNN&Tv;t{8p{YGgZLa1{sИF z&,VjNuW΁Zt ы‚hOgΒZ۱37h%x'7H<#(bZ"(q^ysK cH&"78I&wN~&&gτRƧT3.)|w-V.hV*7+A(|ju_$5gz3˺ELY>഑LJĴ`Ӧ1>}ǘXͨȌ#g\?6i?WarZsp@+ɻf #qr/JRϡv1< B2#Wy0!ӛd cLhFgM`TV##ϞLjhNfXNCr628A9fr.} e>+GUZɽ.䎖{+`ڣ vY̨ndD~1`8C 2`tװ Ka kg0c𾸉wO3+fw]"w^ rG=S\aZ6B7^/b`Q |.oq8ū8ϒTzd3%{QrV|I{,zB߬OZ{}*d˝*L'wܓԖ(wkk:YF>xnqnZ> TkE&+ +qM8U\ǹ.팏hWu.fLZ +;AHg=n74«ۡN\k Rjf8GSMhei9vؚ`cMMlk?[1*lI䎑{nj +jk>piM. 6 f]̺1kÙs +d CqSܱ)OgA5X@teQU'~oWC =FMhХ.ħt9 ]"rV69M9~n. +qJ:X5ПḱA#z ڜfg?/[;B]/>pe\qM/?b^ ʄ~8oŠ^Q L2W桇=qڟMBBqCҧgNj.7uAf|q;~wh5j +Xo%wQ ZC].KFߊDrWM=]xM.(4z)>8O؟XpD{& +?1JafV3]+qRj:^r+e UUg5Ow9q&6mW`fy7@]R5` !jZ vk:IfٙdF&cjF2t +TZP:WzћFOVYteO+g`I:sB*?ҦV"-g~UZ n,Gv\Reb*;seOn!}zB&UVv\t ]=Jzւ5hּJS]FM>_)ɞsᨬsٰ/g1cPOZA 5]Z4f:4v5 +h)_8Lvcxui]mJާN]>CಸIeJDMF,$3J<(cشGd?6!t(M4V}6~}Mzrz 'uexZ-bw4.aƳ8L/c7=lO{X(=\Uh)Œutr¸Vf%2,xSU(VQ_QBOɃԌ2üy-g,oǘH~U oA'K%s%o aɌUc⫌kNΜN9YRbnbZ*k8f<aRa +HrHyLr2n;̋[X Zp[pڲXmZT٫]T:!*c(s9z]wޡyRG$r/_b ŹW2A~+xmq\͕BKŝ]mXcAYm=VnJ=#x;"uoPE#j"Qۻ(⟗wJaB]N]ճ8*aO>  FJ輵h5vQ8q?}?#!oK"Q^w|\jGY_h]M)mVPҜ֟OB~7->ZZi!YϓzMIf?Afs$/IGl!CF>q6͒{ZEiJ(،#ug:9uj 0ph@lgg$;I ~"xECR;?'#E?+K4wDNqKp +zjEozk%;̞=Jv*&-TFjE!?ɽl#w޳$^!C _E~\|% +햺[-ϊjq%H<eR6F%<ʶ,$?lb˰F6 "6|g ̺3>1~^>$Iq! n6['=$&."a;lXɧ4[JSLiX'*fǧY=3ʙ,zwQN5[w &r6 @A2 Eb▽~:]\sOzn#X1l~0_Y`瑅F^҅1, ,&xh( 'LyX?"َ"CeR +MeKM$,EE$%ı29c8csY4z{~=}c垣c# ߤSu1X]ӄ? - 03OtUh+\,vZՠ& +i;Rrp +ZB]Ъ +L?NtrVVFp.mB֬Uj q bFr wܳT5ئ=P +C `[N49=q.\ǻ '᤽:kҟւVOk:R.OXN]mhūr~Zz츼D8+Wq~.*UTq qEUxWٯ)kZ{~Lu{V}@{@?*g;'"eqE\ŇZ s;U8BQ)5*wTW%<&oIV_6M|bJ|~_T?Sz{䉦B06q#F3^t}!Ɲ1# 9勑+A$B+K6rA1R.^VU%K^} ?`TkD]䷒{pKOo*PzFS\ZeP<2K\m< +UJ m8,iZEuJSlĨ%7KzmUh+L(yȓ(2B4]lmuz]*W߫8GPYCZF_}&ZZrVvI/9,L.O< 8ym_-h}q_2^rR\+ZxZ&W*d^b*A3-SJEYȶ7׫*nLqwP=b`TLcmAUhƳt1˩hFy,Sڢ;r?ŭNeZߡljFyqwC%5L̨mbmt+z} &aByPvvS4%;<" +lbsV16tg]xYca$n騭ױZw۶S0ʻ/-QlgXNrlC~z%W*뭲Ŭy>GX}FClg7(^z-QkpoSvYSfՇ>uړL``m9X/~dO&:rY5ItIr$',ma_Աzb(;lڳ%?h(FkZ@'~*Hw*(V'uRR$q 74t'KUةDX5 _x'#_u;c[Q& +.oIXhMPAiNHuDsΡ,w$ix,K'%D$pD ns%:1v.~"]+]EtT)FW6QyLuLI1$y-$ ,qB %ad#`rFg3zۈbI"\%b=1?ilrP=U#M#VgH Ve$yÃE~$x98b=CIZ1%ΊK'cŤTTxL@g)4#F32 +LeU`VV?A~{6LYy=oF&aW-IrQF|9]Y=HF(4gBr&jz! +G*nM~Y[)y49Z6jB^|O{M>oCMw|p%2hBmf%EF3fh +T@HoM-0M*B| +Jl+rhl +)Z/UH +jT]y~~jyni}@Iۘ 6P뤩\5i M?\>%5dƕiliFFʻ4NzacF8hNC쐧MCg5Mc/&.&.vC6/Ma +Up+s';v +M`y\ER-3d$b:v{@KZwQ5w&lkFg Kp_lnV{>u΋zd ;@tx?8p|C om<ɏ<9-ay%˩N˶ye ?D Ra@ 1G:1p`t8 4= fϰoCv~BeY! MR8}w҃$zFgKos&ٗ\_hD/΅; z=0i'/ ta\eo3ޤx%xACa_4zg3rGC8O 2-H%E*uMoU+ՓW7j&aIX1>/>,>μ]P'z(o}r` K5 riIν2Rl%j`V#N󜡀W:弧?Ay)y;  >x7 Mpq K` +V- puB-:ô ZRmT(>NxwmZO=M_0hf 'N.8;x 3̄e8YɇRL:`kVF +xFvp:랬Qgg;8+V,Y.;3'P +ϑrBnrh-l{; }'[%r2rQ pIr-LPq̅0t3ZŠ8VdX5q`k3mض~m~Y^QË^}%.4_֔*Uk1ԆFK$NI^]6\i!2H$v*)ߑxGKJ} >^IRGf⮓FUvגoۂi;8rnN'Ӯ"î#n" +eK#)'Ɏ~v:Q8z;~#&KߑhUڧߣ헚]mdu-*mh%=Tg")N%tpHrI#b;G/}S%:[aK|.|C!UC֐fq[Z ĭemUAJ6 fg;8†  dXCH!GԳqIw IyOo5{/eRTMdGAMEQMTJ)fJ6fX>jRqN98ItN~{w%%ةcaGHa`lk5RֵdYAuX磗MRߺ2OTL_*3W3D11~˨Z`.](gK; +eR(lJcĈfJh4YSdba <|.}n $y"&1H.!ǦaWHK\ةqje,IfX^o# ȦșKbo("Mh/kmnnb$|gNL0>>FKW5z s +f&E"oU!tCA_0ܦ^bo, `1= ooNSd`䄼Q2h=b}le +ԩ"f2*f&}͌m&/)ڛx7$!Por>~y49_̠p'qdXrw//o1 ψV56c&@?p/ٴDrVwI#DңwgضF>x;zf4.42N128UC +Ryu"~No1\uG{K C p9xHSp'? 񒇇Ŭ^ WqDja:XXeGNp.dG@{tOư_0v郯Pxä́g~`,#q8HcqU# 0Pddp;Mߑk3u"cc}zrqRş3W&šhV' N*LZ j9TX-0^N sItrDEV8=0u, x`Mx8IP`ejQ +d\UZBVSՙQM%U*NU>?.rk8O~ǔgȌeƎj33駖|#'FkkM$~YHu/f@V SATnʬ꒖YT4 Rt{^Ormَs[z[8`F@S)\0;l}ve3S5 j[ٍRp_Uۇ>BSaJcU☨b*%NTTw^<帼lr;Lp7ztg粒IKA ޅWrNΞ*u2*rMS[ +>OyilBey,W:eYەuP^guU)R#67nk]_ו\f壬UᨢѣT8Gc3\yS,-JPwҽ366_>[df%۪~?Q]_T~]d> Փ[j@+P},T8Nc]l_?- UƸ-7Uifi_R쟮yyPjҜ͊ ޫ!PLE=DOܥs(+>R#mi @%|-\ +-Qj`RÔ4Y"DŇ).4GK5{|'4**MS'EA)2'୧hQ)btQ>_̔l!ꪤP/ SP S;!J1aOQTxfM,Ԍ +ENZ6[PFD P6;ɐ0##8GPi-uju 16:hӤcژVm-}is#}}o%U d4|*c#ԭn?0i EUUpN?E*']Q^ʈUZtl!JTRLcd1(!Hje49gjSc1ZOW1=D:J^f69C Xk,0Zʍ0Rj(8SVc,śbe6'd&T*ҨhjEY7)ҺCs("­7=EXnbQMXkQXqxn [p%%%ad%%Lkbf$&+*i"5'\ +KiRhJBll; Yt]A)_*8+of +h y*i|,I2'1SŤ+H[">7Qa +MUpZ0K+5+Eu!w5E@Pej + (ۊL6'ťRLHEל ogPhl WмXfZ4;srU,E\ӳkZv|'f_O|3~39/RQ.t,c2ee5rQXBr4K(`h(5izn|2W)y՚L/ib@wG^hbcmp+ɹp.=mmɔ נ;vէYGjt0s׈:/ [\C5>V[5~6ȥFV_C˩ԟS 9QߺH+ɻ. ߛ#$$؆),ؾ/H/JÖ8iв5>\.vOT?dR3v[>h-g.81n{j?Ε{T6ÎĖž^/M0lM5,:k^|^|.}G7\3^al;$:nZIu "Gؙ;f3 X4vfX6m`8G|Zx[x6F-ږr17mQtaN[oj ܚ2G `;7ᏅwFmm&N$b!-ıxxHZt~It]U=jj@`Gdy2-Knp](#K_~UcCe\{:<w=t 7n\Lk*STéupINVQqahOS4>w1H5@'yN'`1,RdO>O9-q8N N3 0h*qJ C׫/՗|]uUcaN"H>IN>90a(1rDp&8߃:rPe:C䱾cEH-G7")FrɇQUǮZ gJtq3lj6umv1Cߎ}_0=k2,?X'N + 9A)Sk +-LF'W.Co3-2VߠǨo<:SAb/s[,Xp p,pRdK6<-SQ- +Mѕveg}!!1$3$%˹.˱\%5jN<1^j3U[XرvNiNj~w y^椲w\8W>y#Iޑ$Hr+)VbWLqzˀX#bM5#VXNsIOڱ aE}8#\ב&$= ݦ;%^x%bUr*5^G9^n];ȑuAMu˄ןd~9e&UBo2b>@W UGGkoRSOo ܱ68!c-F;XbysxY-|0sn;Ewg*.&f<%ф7I|Oٟt]oj~V]ý:ߺmIّR\ 6'q(z&Ijc:~&f,0]uû`0; Log}knXL~ݻ4i4ZIѨ8SRebfwӻKJu2ZxZ#ci F f(}93Зy=Y6>I%:L&`t*ĿGZQiYZyC$ғ9xL ,*r3hoEi1| SGk~@Kޯh/?P LpbM),ek4w62n'^#|EћSEwN=>:rі?Bk4-4[Nd9zElKoQot ~|?x/{flҜ4uB F13)0U`N{a-Z=4ن8LCqJ?JyjW_T?b=/)G +:!HHYc%a3n3ZOKQ2UxJh,iIcweTn\KT:O5οH,\~ǭ5+Z6wgFq92:rh,PWj]V)rU*ƩZu'eUqT]^ qr~V+Ǥ!Z;snLblj*,*UCEgu} 9jg׮P>KaluZs,u~_kER+$wHDU/)#O]lO3Ԉ]+}-soƾX2%/ Cj'vŖ1D r4Ɨ?˺K{ȱ׵źONr~PZ]-vYX G)3() ӛ#:M 6D %"3`&,[88dBߕ>rֿP-{>N;kBs +f9:p.V ᄳ$om. y)I&1 *+b*Qq{tUΫt}'8 *,/ʊ|劆[wHjc̣0{;"d?c_APDEK"n1&c%5&mZ$UklFx?̝;w@k6r9`$T#ldFZ16GMtG{D;v8WJnlO(IJCBkJf[l˶B6maі&Ĭ0ُ+̓[2\I y(؁<ϣpv Z^CcC/D<6҈l"uBiD$[+neză[."nq 1/caqyrR?f` KF]NSGM~!?iڹ9x$\{",fȳ<vl|vl&9lC:Eo%151qIw3.yqBoӚ߹<v ~`&7qwus;Yox@@H>*tӰ'&h>ĎKe*jtm:{2x!=6SKVl nt{@5JًMrK9WI8~K7d-r#wJ`*KcJf&=橻SU1brR ՟~4tĹN\^]eƻL>`;Li'YuQ3aprGS([츊W.aw3#aϒ4i֞dL:֭_,w~tןuy]x"dT|)bfR/Yv-kM+۸H!eϱ7k$9'3}0m `X1# _t;aQx;N5Z8K,gU 7yhdݡosd=/!'ˠw'FG1 ?ԎlQ9j8pvy ާMs"O#렯Er{cfuX#9 80`L40raA g6r8p4~3ƻ7ɔdXwrh=E;cgt,0SFs!}eQ0Ʊi 'ND6PN=BvŚmD`gԓUVI{W9]vrNU8!pÙ' J&NDJaUi1{RǷ<(j2u*\N\GOȿfoUJkӆ:e%Q;jٗif*Ug*͚UnYsQqQzT}[>Dx+p%W!q v%AY/tbsož g*Ik*zMSyB)SijWq*I/S1PyAhG)gU +jL7@GuiA_{UsUU_U`TU U٠=hJOV )P 4ݺIS7(gneۼ)6isS#~Pg: \ukvh:k+Uќa*k*SMGj,MQEU)/-TrM_mJs8TRt j?Z:ڐs9W_mS(;;oh}r3:^SF'+!]98Cʔ:FF\w(ƻZ nS!m6 mGwϧ=l }#sf8 RHMurTwMLg&;G(%Vi.JuMU[>[J|Y^Moj9@Cx?ѡv]k>\|j6\vJwwV&z QJx%xgh4R_b(" [a*,w +3<@u=B})r4 b5ӫҽ(V>Jqx__i_8XOR!]Q ,RD`‚+ԸR!F\2.z c íJi tlwi=lxQk%+.Ec`PT`"bn4g+$Pr)0l_xߓo'ᅰ_O~'ª E rxR跍fJ0U\PńRdC<꧐P"e WPx"d,E'Iћ.Ϙ3򈹆n3GĻ~ 2Ƥ`^RQR4.~L P-py* ( R@eXPv=EDA%QZ&Z$i;5itI8vcI5h&<,~{|{R<:_sSYiњJLOQB|MU|Iqk)ư^S):O+*5Ef>R&R?|÷ë ^׿B=O2Iط 7(0A HMϊմDcٚH1Q+N9W9/*,U\A³j?5؁ :K&&yp xl3M?Z@(&gNQttE-E3jb^_v+PHnSP52{ +}^'_%l 2p H s5{w&/cQ ++ՄD/ +.^R1UkTIKP+-9%ч3}*frH+`W5s1gN=;ER$1lƗ(,XAeS +,s 5\U(ϊgQ/Srcyo$հkna6}M̀=vXǠJW,eFX2I,1NG\WZ/r f?QyԼgjn Subw=*b.! ;L"W῰(uwܬ~r +CJsY9Vj%+I3mx+ߵqKa!+7(]صpp3a'Neƣ>ecp aȰ3hr;NCUvPfi'?AE;莚a[V--f6NDSk_Ik$FɥKG ^n7򣘼t6ku \Z+䶑6hkBԣ:0E7n6΁{&X>%l݊6 N>fvh݆͢0YD=v/mc+K aO=.6IVURKZ.ݣc/>NGE C4:j9*{! 9mud\y:/\ ;.$Qr} 0xI 3 ћUK΃;ig]El;N-`}8.. .'}S\Ox?:H@8.όĝ ;v(_s%zq^0.osy-IEXK*ދ(Dˌ&fCNO ti"ƌ~9MqFIBFk{̍C |ح!X=|{{/usV\u{8xѴ;z]g^lUy qm㫾@BЩ0RuK `)Qc9Xi#-pt$m/RӔ7wQwo̻ 3GI\J.(xu_Jx}U QLv8-ptNڦ9B&^񞥬Wh;z|"ktDEG 7xx:F.~)W褖`sDZlf 59J.@˛Co9+C (X 'li0?0*aY>viVvХ|Xq+A}9r~ Xpb$˜Ê0?FLG.jlb\ 6SfN:N~sO?1^ cH8qp%tV7C' N9Ufj_ZV[C=VCYũYI=lS`WCe α um!1Bi2+Ùg. 8F8 +``aY`gCt Uw>~'OƠce>k ma˝|Y)^8)LX)Ĕ+x +¥T]ZոŵYUntQG̞'Tx^UmxGƯ#bwZ|<\ <ָh[l1HrYLU{-;OUÊU9̬ +jTوu*"_UpBS~ QA_z~Xën;ײ6^3Q#c~M[>T7|jFF*DU'" M.3?7 ,r, ..7산hxEH<ɨ*cښњv miҚImƶ416$MI?Ȧ?q{^uѩjZ-jVC죪="OID5o}kY K 5` Wp_lF,ѪRmRgt1j-R-qjkTcO =&Q]Iޥr<* 9 /a(GO#=z5u/!0u&Dȗ4$e))O EO.'ťj rvɕ֧A9;T3t\UbMY,>vrlJ'#h߻i7QaRcU"OZjӲU^ wzNUfajRSe^37$' +-T`yA7П&(yk;&IU6ƌYjsir3U"et[%gd^œ *.kވ\3ʶ GRvm=E Ra l`%5J, u|w[Ҳ\KʲSTEE9UW|WVk{S^مd)ܫ,Wi{Z2]zOfۿtgىe[+uVߵ|Xy*.R5JE*OW~A +[XSٶ:(jQVq2KTEe*-ڟVQnŇQ-*R])8\yq)1Rb֊edTV-cYW*_MJq +qH %8x︪ߕ +`ƯկynŬvvβ9/UVE̎$e82:V(͙T]).%W+XLp*ubݻ>hQEUO*SEME>!fpT݌ZrӇg#5ȁo1ؖT~U!p-P;Bx%W*&S 5)VlS1uEյ+ҳZឍZ١Z6BzhoR݇^gKBsAجj)8;WK$E5hQxc5kIS5hAS@a5yHsRh|R!/yIn(o xmķ9@bPZwm>ƶ(-Lõ=N }̚\RA<0~ϭvaMۋ料fr_w7zمwسRZv)G/蚫9KҬ@@ P P@-3i0 B@Z( +\@_z}7RFs'y㝋U2N)[Z +墽g! bcyij?h]~~n~xwJVτFۍ7Rx( +jl~/F.ڍ/ / $AGcE@u ݋ogLx#%NFCXr6S-,[rG#z ѓ!^!C$0|7t 1/wߞf$z݈KH qFKg + XN] q s&=\~{9G8pF;CGHb<ٽl{$\V/3Fuxm۴NJ$MmA]h7,哾/Aqrҍѓ1;F-FIdFAlӣohY#ޕ܆w怔DCҼ}=Q4\xkf!:s q`6&E=uW-du]J +Y\E-q {\~5JAQކi?+?#A_SGZgFyW%*].ՁG^R^=nq^!яhiy}s)O)g-tC6J.ºØ7%Ic>8>4`a lNs4HB!a%MD9tm4Zj[6iVEIݥ-i6u[RemS폭pL諟m{>>σsp25O8I'm.KN&wuH7_>zUD-m6ªėn Y#؟!Uf!Xx,XA +".WY WG} M_ѯvR;[)ybS]u0'8200a,XLg?.b{/ϓY9;ΑE,n*N +,`4pQލ0>cJhGa,8LF'u_⭓xp{ :FVo?rZDh޹o])b8U0 `"cp&9Q8p`{UakxJI:_VεxX猜Q>2 3X# +0X"gُ}_ 7ż9ڎute$)''ٰVN v8>8p)°ɈX{a*Npz.ϰ[N '{ZoG[1Εy=,ɈqXπWŰ*`YV >u k7aXXѭL$^D84if ucb>`cpk e(ZI,&f:uUlDUth:[S}Dܘg4e64|N)W5S/V`׻íRp t"˔Gm+| 䕪?R|z )hUwG"ZNKܖTZr.LjjU_!կZېj"sϨY[d@)^=`L #/X;VlKR-S uՕZ_zݪlVE̎q5|H%T켨" *t~U)Sa,18AW/FAZv¸K :T0dTeY52;29[UUIk*j]O( qEW].'aa3tN|dofiQUk۲dreRJE:;ީ at(˳LϺ2潦]/+]x~T[w%։(qx0V3AFo ++WZew9VFwҺ3%)Z翥X3)`>|= 38 ní`H&T#Co2W>R,JWR_*!.8P K&b%Du_#p``7y% myYTĨbJ JJW|(Wq\ 1!:D҆p$ċ^6A6̌3,aSn%LuKGy5I/5K>fعRF3N1LpOXu ^`ΌL}5€CP)'lj<>O krc +|@*fDOxS4͜;C{a\Q;JREqf͎"cv%Ewt{~A|9@ veH*a\ϸHh)l^fA. b˾r;pn,~qqDN,ұgX*ゥm +f%F.vոP?(@..˱ˡ rWP)rT^XMLĦ4m5dbD iI3d&m2j2L1IN𵓎}~<Ú#fɛQ{bSGW?6nGNzd,ý,w%"=5&D0Zol@~>DN4vFM<c ~lÏ~ Ģ ?;t覷]R^ÝK\ة#Ys\op7*MDӹ v7^c?:Hm$'CzB ] ] {M-ay]{&l'Rk<6h|ҺO4ć=hLQc?~Jm0k>*A3v1u6=Mm}'F{Bf=܃urܜ;etzacnC#yAyc`Oh0ό&=@#*Q1FM + ^hGٷWٳW+[W\W,2/?5GI̭9icnIu78&E]C;#'t|H }Ü7f/n1|lf*׻1`Jy ?q?^g~) +e%y$%qys + '}L16_3̷c6=3U)f.R-X&ahAitQ"06S2;)9zx6?t qzÔ._%/11S%'NدL`}f+atnl<<9D R<߭0bx{ +&Nf֑# FJ_دga,F׳62zo]X inqEۉ21寧ί"0YQ _J˱_4[jhkwk9›rH>*J;v>a5G7 0,X}'kȁQq]F5y숅pBa˥O KDn-UFNmv|pq 6%XG*F kr.( ++SuщkK{;V_a}mʚ&vy#n|8bNzp_O}Ģb^pɀ N12*Vd10Vb݈WES2w o?fw}?n׏I>~ENbF~`Ea55dMp + *'54i~Qx;*G@kGZNԎܭ/^4, dX`屦BXϾV˛"QZUƪ/Q~MvjJ T2TEU@ӖiFpB6(7t\ +Vfi9F=Г{X r5֣1j]<ş`UNS`͙Ҡx4+8U!YbU('NeE*#KJ3Pͯ+t}!yw-zC kG|21|oi$0"\3#ʏ*7.WSQ9ʌ.Ti͵JY9:e,[>%YVyY-d\Gwd!b=t%^xQJ8%RA(=ؚ)SdEBV+^!9 +qjjnrWy"p]AQg?!FDQ"uaaaY!VЈUT`4Eڑƨ$M ֻiL5u8ZffL;1ԉiL?v\9~Y~$S +`1pCXT,) m} s.z Y +Af#[b+B]9EU+Қpkp:Z*(]}Unq#Ip ܦ؟k= 2b] >!XMMpŏux}xd!0ڏ_GO= r=}lIjApgDѤ1bys,?c?60ʠo%[p;`>a6͈MTm;/W}ߐl3%Ȼ r NzcS4[( ~{Q|5pÝ w +i1sy MYE9c{+ ՗鑽~e*C*C5AQ'>%&3'rO3!3 =;f,\Ǚc Q8CQ)x{|γ§=c\}k6q_}+kkjwхUda7v#Oo _s~q50b~*3#3a5p8\4sӁ^M60ū]d\wt[ࡇ8n\#ifdPSOipdau(:? fxX'ude'/tIn/?3WzKH#lbRx*1xԩEp oV t3t8N0QG<lW「WoEXA..M'dxf“ O><J&.',k1Ky1Et(ӃL |nvWZx,1a=fW>\* +iy);V:e}[̨ʢOTN.v{W>V._|`/h_*\Ė Wqe!9FTЭ^TbQq. : s)]Q>]kj#|k"VŠ YYYB!G@ LUfTY* 4Aš* +9Ws#9ݚ5ʎȿ(+ꁯ[uhV`VVV&b-sl54Va*KWixJ"UQy*).OmU޴.*{(#vfN*=b{ƨf\<ֿ/J5]Ǟ?)UST45NT͝i5'Py˔kSv\[@'VzҒ+%i(1]%$]U|'>+ 1cō`N֟^Jq&EhN\f'*'~bL*=٩fv(9m.;$ PDRBC !!@@Q*ŖY=2+ZW[[ֶjs]N^o9kcvmvK~@}aT2 $x@Ҍ'$~%Ɇgن\{a"b-L3}͜+bҧQCVdk-24,vY}G$ټUZ|3reњߖ$Ǣ;-[^|muyP@1pX1,Պޔ*t6ʒ\I7;%ԜJImo]'ZIMHm̵ۋ͈z]_[`FX˽ mf~7 kʺDI.4IeI, 2?HvsJE$>P✛e=Tvr;y&b0@Op"6`FH&+r]$/w;r3M2`-J%Wq˻ ,ͅ,""BBB4>r +&xB.\t˂[tZ\ ȭyIP"qJz0xѻ^vvC Ⱄ)\ʍXT,9 dXW\ep'[킟nd֫2aVNSD0!"0H +p;X]% P܀  +Lp1{%𗣁詊𪢨U5&ռb6ޭ&ASPBl j ZW)yLK RpMK![-å^ƀki0Nq(̋atf|;5V +#, 6҃+^VB A>ȁvJ)x˥-Фa. V"BrۈEF#6Dl^@ TVk|!|C&D4Zl6bd;ZrP&Gwܐk.zc ݐVxs%9. f5s&ϻJbҕQYrD.ա?HN(!kgwXϦ4@MA#Oy. Hc>'PyN yuQ.ܓajtsr1dqbi9H>6X/ ?mxaGآ,\c`(KmNS8jc `?F]™z'3,0A)m;!wLڢ.=%6[qpL/9Q?ES&Ob&ya5s%pu*sJ%Y8xc)X G;^vS<} )>J{ӼrCg)YiQS{mcbq=sy +-s@G#GaT!TQd&P94^4nv. Gpr{kN%XLuDcNJ/A|%ڍp,)j9Gh[ b5JՍ¯F?^bG^,ln{:_ChUp~Fwe' +Dj=:w0ūSyL ߋl/2ov0Q GO+D 'JtO>͵sok0-G1CMʈ ^t9ppၣ'D OYk4?B's?7?ҟC#k%=ge#9N]SkwI%5t[ &$9PF?卭\]X#_‘’Q OQa8"tZ*wOP'KdZf%*khx& xd+?p+O=TRJ>TJ.&ED!ܫ@pFXϖF֣e!zTQh>Q. +*અ 7,T<QV܎&%p~s,窫5ը8m>|ipv|N)/'OsZŵUNr7%~B {Ŕ2m;qbljs:NHJ҆WI途 +ʠ:ֆѮl +hJKSwh +iM*vx7~Ol=ߒ~ o'|>M:N,) :@+E왑LS>ٮdjR| +TR0A +dt,OaXk\>*ʾ& +%@W?ރ,3Y]"gScIUuf2,R ˭,JZˎʛ)wn\r,WQ춧eP~uen))>/3 :$YTbWE~ϵ$@<<>MϯV[DŅ1se+RA29*DZ]VsrLe8Wcp_ۈz ˚ҁhq?u ij +PMkO۞!}vUX\&#g\m暣郲+ӳZ鞭JSaoi31ϓF_7AkԃjP2.7p"Wl.۔O,:eQ2Ki%R:d*ɿ 0 P?UzWJbh(S塙[0Re2GTK鞖?<O-L'E +kY!,Qh%8NQ/G%.FMĸ bJ1vv66ֻZ || O >APg>|RltVǤ@A,f1Dp$X !=V=|=rm݅rϴI2kՑ|.gk3o5xK,qrq) u1H,?d6@ 0wQOXhnZ ]#Z>JvKܸ9ϼ%' c!FcQc82c;eN,B)-B5П S7oGc =Ҏ :đS'!6,ǎ89Y0y ?N,;U|HcvkQo)v 1n/CŎ cvlOP[(b34?:qUx&p;!%&5%xo)¶D;s^rgHnT-Wη#ϭ.1UO\Lm]J,{X +KSCee06I=S(Ibiu0N#{;nOBXrհhhJhV,UbSb!1)e>XH5 =s;1oS8yFcG4suHK1V>o1}u|Gu\Eu@u\\ ט?_ +uqޝT#y1Zgrs$z&IpoFJ +%kQ7q9wm-Er?ǖ)|oF xq&  +?`JQW)K;"Iz 3ngˈ%`kW;#~v5#tu.v\Žwz.8hة8g u +C@sCBTIi)߸ܷ7g^&#fvz??ð>'c^cXgz6=xE(줄a,9AI]4N +=@]ON#7a|h&~Dx6[l` 77h~jg.,;p?O>abAYu!gTs~h9dw4hXA{KSXv('cqw~/Ϛ-Kis_ V ]p̅c>Cpq +F"*##`%z gwQASy:.JPt8ZA<*N*hTc-8"XqdHO86p`(X iU+8?Eb2*;LGfz4j9 DK)#M^d$l6K*E< =RhK)$>SL*d5;M\ g.v"mXl$vX̶byNL7$.|r~x);gM*r y;lz)1J%K-Nɷe Z&J(6{J.vJs\,zѹ"ND=+\:;h`EH"X}煘/O'nQ\vfKnnnlW\kng=R>|taGg>xsfdjFy"Xg~}zqbw$'?K,nd]b[,_LtI4B^@nDw8svBuY 3rS5"ǫq a07'+'=/)m_XT6J+8"&9YJjc= ($|p` #~T\gkaUXE.ہ6fhbc&|(;kF-!OsbZpdhx,'v"מz%u~y05<%vI/a+418ǧFje `1J77"Qf737 툸΃2sL w&25,5kXJm¶HFf 7e/|w'ȋ=8նy4Nj>hj]am"Bee,Ej9ًQ%+r{r5\>|M7l޺m7y筻v{۾>w|S==rc?C_<~O~WN?䙧~7֋}{/\_}Go?/~_w|}Ye>ەxSn)#8U'^o `bGQ  aa"b-LWTl-(6n*>>FTܭ8 |V9hyD"0UE3C =`NJ7G?(h[oXN1K˓ +3|ew'(/]KN7i-Аc vt '+4/b.K4/2[4kNE,[jͺoڼfbۺ$$$IH<vcvlv~ujU X?FƮ0OZsZL3 wFܞsIavm܂o߹sf޽||>Ow/h<*`:9_ݘ-Mg4(9*M,h)1'R%Nkk có = 'g7k7j%0H` hHr\N[ =徾ʹFmfdq` A@ +S,||m ߜ̉{W`P 0p`` !&#GH?4,a JFd6h)Y:0`Y9?0#t>t* 44gώVkC4Ն`yO͠YvAB dMUdI?̗|QUsV?=ZQT8R$L ,dJP!"s3׎^=_m+׆'%#cM/aL bt | ſ ߞZ?|8sͨ^ aJtE EIx'16P6DLjN[~Ш<73#Y^*kVd`Ip8_T L4iϐym6{[3ÿ5G.je*Ѧye:]$Q\1M8,"N[R4o!o\V'5k]>˨V4Lc +I.<(z"~ +[㝶CA?6(/*mT3[x LQʼndO#.);pgv}8wjžśɚ-O-niGs.Ս:T_`Z mJʳ/"FsIK2gO.jDe.߰%kL\ƴQtxnUztgѱӛ?E$/Ů|`vrtXs䯍[6n;c 7' NY=U(czKiI<%(< #00,Z<0Vo(OH 2J't $, + Kۃ6 Gabrg9:0tM)T(%Ib6Vah] +T0(g\. / M2~0]" NQL i +|@ -0=OaM|ahE>Wz\8eՆr#Lhe +qΟt gLp!hGd>T,.+_}mzYe?YZs4 U΄ʛ0"gLX b$!(K~`SՋ.\=Tt)OXA^0YтE̡9GIa)'߁J_[ZgvXLiTy҂db. VS +8/.oLE`8S+Mw)}}ZBˬFcMFj6LNƉ +bVPLˤř6|瑇47:&?:523\ZY6 .YEN"1t'Y ~{W]6N>iykˎscݗ7 lD{ktykyk;t{)xplWl{yz7we-S h5K^nc2ͱ8K4eê61dekq%HRO7(L=7<7<7<7? s)L1_SCq7wu-ڥNS ah&NIQ"8E4![`I;7slg v6ըfQ !~>`4,X,3p:$*BL⼨쮷}gaGʐѸԼAzp7cd/06! T Q&SBc87.m}ft`yj%5ŔjU`($1w6A3"9> KAw<KNR}CGvR5$Gj˜fQV TQx*(Ƙp@b +f蔏K\B xӯ =qThsXӊs"O8Fx p,+L=IѸQ ̸~0 sfz $p8= b|o4RMG6SR{a˚нL\ep7k +,b1'^6Ii R<"h~,ۿ?90lӛb׵Ukp=T2'xteUEH|h2 +I%H!]}aCn}eS~AS-^oupvQR e̔7 9\ +2R"RM9=y 1SB=~ |m+/+/oL3۹q2l:$7`ޜX(: IJ +Π2Dd\Ef)Rw_xOc ʷ[O>H_|ݷrs-][|e)MB捘CM2 BZ3( ;40)ڨ3Dp_ijBsgSK=HEkݝْv V[by f8`#Y͔ Y'Ϻj7宝(4./Kw3;쫎يl/Ozh#E3@[+j!oty]wP^L% +wpk}QtvL Oku`2P2lѕd 'J6H[zH|XQ,م ?76tH !vb ހM-vaŶ!mMs9ˎ9=GvN4}.羛}}裟3?Y{>34 }ǃ=ҵɃa}hL NeSKE? ˨`ڽR@|Ty5kC+WEejsubccl}s k䱨{g葬CEC`M30Z60>@W>=۬;C{Qσ2fkǟw^9w&{w{vD `c׶siD9}x6,Gg.}_G s>SQ鷃L{ȅ[ŎO_/\n{.zK19> 0'?6N]Y6Ɂ +)< d +\#%*."6K*RR4E_SEТ4iqkkk%iPK )\.1ޫO| cWW}}N&>q|QʨA]BHQmETn݂2ج,Fy X5yGHӺsj +Vm +).b@%=QB;XsjaNgIcF6Ҳ!>\b4xxXf58LDeכsZ+>xL3pC)s\"ό_ø_w(;yFֽኗ5EThSJ$,:n4SckTJX#ůH݊,%F+ÉBa5Yvኟ5De(_t*M', +8e=vDa[69YK ̬}5ya'pi=05\ Pe\]tA9;P9G quQwPC;՟vC}5[O#wbS h9- 2YZCa#bsf266!it##ְ~n?½՛5ztAzB\28V4B2zm*hFGc iޔ$uK $8{6Ngс'`Zrh~~TPヹ Tx8ިnc.sŃnxtڡ%J.12z?>qv/ێv}A=Jfs|y7#| ɩpإvđ`*eg k]<i8?'Vt**ie4'ELAF2ppcWG6ȗ r+8_<m<k=KLٛrXxGc@Ըr~ڈ%Pc8n&!N)&- +c5|j+|fn{I733\ legAd:, ahJ)JM$EnB`,mxa{OI7h\gKV[#+]q{VΈ!"XNj*-PF),M }80p1~ 8q .Mo=J`~'镖hV]rbVf@-`8DZiPYNfq$kщ0cpz؊\&\i<]nj]1cyrZ0 sb|OR( ER + PX#I 5T27_;O5,fi5rc{4؋<VKIP )ӢL$,*ES :priLjITiʤIӪm&-RMٚkӕ4dIڜK\ ͍c/ 4@!Bj+n/GoF -vfGm. +vܞJY\Λ4q Zጠ ׇqB;ChB[÷;g?7 `xn 4Ԣ'F圊4o Z0 lAH:x}7,Js8{gP}cP0lzҠ' E=jVˤO̜BĝJ1ԂQG<#>MaNR8iА[4i0zl XY5ʝPHu(f N<-쑤!?.%~ 4nFʞjъ4X3ff뤑 +frtT͒4݆İp@~.^Ro@uݠ% / i0S٨Eͥv`# .+kyZpL)F5d"<*pDD>ALO\$3 $ gu4X)WE'idM9E7`F*J+UaQ]ȯ #am/Qw c +0.s=Ͼ1H噡x;Qv8iIzEAF FIFUR%$!Cگ U:8aa2pm₧ʮn۫fH-2a\qqZ;UZ8aD%dx>4H4X kNʻ]Y͖If2h*a#G9\.̢ʈ =:({Io=5i:3m2Sf4xyhڃHRMԮ.i#4K"j-kD $$ ~nnXEo?|g{ۚ7sA&7FA,^;`US4ĀZ%)xBoɺ(-x]󁏶䜿N^?jiFd`0{`6&dT* +yT)eC@`' =Wr=Ppu"ؒN0bL~0K=b{ .V62taLQIq"uz8eyn_;~jl@]x<5RWm PƀԨd>CI!np}Dc|!xf9NnYu_x~o[sa㉚C#KLm?&9+&P&$IY0ߌu#,5*0},`=h}zB_/+~N]-f*rէzk+\9:6ziH$#B\IѨDr^ĤD\ +vį`br'~=uXIBIt2&tMk׉]'6x75^ubclv$ Zru $d؄Yn $#6ـ `N~I_? 9s@,y!d.ԗR^N>񏦙ˮtT61Gp_i5>Nkazao^ۮt{CU +AS]I?Kxc"ݦI=Zslc0Y,8H9"CbL%$" +Gu2ܐ$J뀟ʳ < Y # ܋l+"ȁTߟBơd>_ PUAp,`Gck4(cW)F Ÿ{'Gs/d…W xHMP@ξ(`uGBсKE@FpP! 17 PEP*(v:%He.g+ạe9`_˞5rfminoq^7 o n B # jҠ! Mmi0¶Tqk +a}Bk3kNdc`ia-oJ(ۄ~^/:b>/: 8% iА-iВCR0I+_Uӗ9'3H)qՠhhB.ܕ =~O%>NM9iО i0D3i(snĦ4K R?#7rdIC4!qnԋw> IQ g@@AtA*%1JI%7b$?aĥ,E9pZlB搌Kj1Ql CxAꕵ5zH# '=i0(-|JX)PM(cVO4ZDi4bނ򤏶-'-t`S3iRb:tTU&תIحjŃ*@HnUQOlTgZX,g(gB`&>*}Z1I{ ~]ԭ*%ċ~4I4Gޭ+šZ[T͝u +v 6fQˆG2u˻- DF֧w> 4b'T\ݳQ[W7G-7nqn̹*T8-f]EYVSR!3w56b@UJxWH !D̟?֋?ۮ+JC'My\lfE.4^kPM!]K:}a5;nMk,UkKbŠ,kbPv7)-7>^law6ۖ;Nd tp ;zZ_i얖rZ1:M> +Cϓ4hn^zyw[hZ!3y'YF݅/3]؊I*zfvԗ6}qYonN_43cuYήUۭ[W֝]3nvvu]֣* /BB!ʡX@P W4r>3wE_gtV=,o+Z-VUU{L]qu>Wx^aC6\<^y)`>aiG7aYcec랗|'`Kp>ѬCi@!NMUn^U&0ZnAIQ+>{o\^j]𨇸C螗G}a'$M)䱻 DX߆2kJT3!*]ȟ\Mfڞ ++W 0G;AI*O' G͡ sp«Wf.^-[w6BfT45{uv85"!, Jn*$KсxC+?t × 78DM^Elټ]ۄK25,{(yh〹#\ [(?΋~/S-krL,A`R`/fá48|AĚX ( ?^ƟgS> +ρjYHr҃NCǧ᷒ +rJ%H֣Bɰ`B"cI,[޼aC0ĿsS|$ք'@hi}0dm߀w^:_/erPͥ $5}(x UIZls«1E_J 3iN]Kg RFÔAtsGjHa 5V `ؠm:>a{3 ן-FJq3ⴠ$q<ɫM'qO wuf_.6HA ꐷ@ FH=z1Ј6R NKn&ŷ&qA-}ߑ 0nf`-b6 YB߀^ )6A Fl0cCaus?jUjSܴ2+aR~=iBZL<1}N/ts< ^<ĭ6hA @Fo6dR@8藺4”FPQϥlTO[P/*_O-6y`=f dwB6R$1O,Iܔʛg2ި+vz%}6ـ^T*nI-#E 6C~ I1sB,:L0 y2W qK]NyA/<;O-V(nGUucu;j붵ޭQ +*r&r<  + +T !EA>g~m'j&jrw# b " +@I'zPJ;=ci2=WʨR sBd4pzy \ Q=MF8s ' @z> "7ک2bvs-'N(ԩ78vCԄ9:)2aNs D. }O!%a|ߕP>m}Skrp֘rru~ʐ:QXQU.E1*6 n;1IdEvqȁENw K.`PA*r\MSҁiT-w:=h2+jP/شryiG73ZqjGkQuFIگZRT/KQԉK]BwA rP*׼2]0]Ot)eϓ=nal3(`Ֆɸ"ШQЭ֪U\\K4*Xt4 +'.\0(.Tq!{%.Ʈ^W?wu[&3epu|IaAPis +(6Zm9\]o r.ߒPgGh[\w}wI?$;*-Fsqe^)5 +Rk +m8WYN0,rwga zԙGoK~᠜lOlÊ)J)lf)~D.0YT~BZbˊlR.Q:pwArMl$a(8*<–uV !y(+{)u4я~f7 +>E'478M6&Vؤ"]".vNAB4Ѽoє%c+]^gҪAGéhxv8:`F3Ȓ~Wի4b鷝XŁsn:p!;1aK.,#AJWD[WoG{t Rִ42L׏1. ;g4YP@ҶwoO_=J FgO$IhfHuZpd3fn gYfRb@ krYFݮ`6q4IQ>M9MQ9t^4C `EsbXO5%y\4֐8zN'?xDOIoe K7/Pq~Ʊ?>%m*S&$lP/f̱Y3aصߔ)nĒ-I+)a99NJu"]-&Z[Ѥe=q2r 5xLE.ڤڡ_ P$dC0R3AҰؗ1/#ע#F&EFA3FOG}i-`qՅQ1́ [PЀ~" اȐs&DgcA _B|{@kۥO. 1Bt˼f!pa .y psngANDI>2$:E~8TGA{< 'B|2:~ +)w 2 T׺_԰)",j6{ +nIm=?+2w`/f%(q]=7C|D؈7(z02*J H +A` o 8ҷ\ d@9(,nwW!CB{`ʄu[p F p8$B%Z|F5uq.(SGku:.g,3z:eڙZK]Ԫ *(}Yna a7EYD(օE<՞9"˾>!eeП?_|> ^@H_ kvAầU݄a. K006)#v6b$xFg ;C08 DҢ8!Zz'`|v+/Zf*ڛԍ` ё@Į"U3p4[g~?0.zaFmPG"gQӴɨQt9b=~? EO ;!ؕ{YwO S S? T7EAH H#V@N{ $ R| +xNv85aH`~ sc3qscMi]46w:_@C d!ȠYdPC Xy@P<6E1`Θ3GOFc.D=^uID>2{A :dHOx tw@ xi{@l>’TG܎9έY {g4L_Cu~{l` k G` 2@# +U(H@R "; 'gy=qqG)X5XRp<]47` m^62Ƞڳ A%0kJy2XAwFpY#RCAO4`0NtFS.GSc-s:d 4!ugMy ^O{E̪rH'fG´%iRx=e wS]A[2L0ZQF31Foﱁ \d#.xd? P2$ G #iYquaܨLҰ!uHE}dK-av#|U 7y@ !8r×%o16X&D{GM#FCO֜Nq*e>=72/]]+u!W!Y0_~ ^PL~trK9!|Y+[s?LNWBU;7;x‹7+s*FϪ}xzF=cȠ@ _' Wpxa}-';mҿٹFaꚸYi#zvWT6O/ m=TA荿{~l1+g^:ʈwYUm$n]yK\Fߘ^+Oj4̮sI+tn#.yEe:/.b"C rv͇Á`Yf6zg=ݾ'7.۬lfw*}®靊rSVwIKnI#.{zlh"CޮyP~x\u̲Zʪ&+9oKI\ ^ag6+K2o3ENEaKV&Fje. ܌e<:b5SWw5fu}MRMXUb` +r%y2ky|SoWg(MNy~K鑠Dzb@{S 0(ʰjcG(Kmw/ٚZ_+:V]l3n), jU&U)_;489f4'-1yRtA M@s%Or䆶䠆V%W+RcC^ҩE)sIr<#A 2dA6x}q؋O|%isM;#,m ,uɑ3TH*]PjK"LouGlȰ)2AgïB h{G:Nx;W],Od7 F" MFԸd3.괛Дz$iE^ pd!/h󽋁cl~'ݺ]֟{)/;$gn+>h%=]ޚTx/ϰ(,  D6ظk$kvW & RD0Lcf0C nplA`")<^O~<}}y.d4ggi;ْyVaykfQYzacA':^Tӣ[.T(>Y4+>oXŕڀ]")]}pBqEog\n0^h3dUrkȫh[9cY/0ҏX?.Ӌɵ]yB5RsOgoUpff-)+qALnvaXP!4\V5)4PjuN2fT֏?t]nrmٝM>%HiOreiƧ!0=?^mۗj+ONk!E@$)T>nuK5u/Sk{d@USX/LޓTՖj:йUTn]|I8I<{)uvbyazMN:c^k7OuT#6ljz[㥭/{n%Y.uUT H'{VSEu9v¡i{ EHu΁rZx)WƖ DP2jx4* ?6 Z6L]oPٰ;Q>KN*AfgCHKӐ钊nb wMqQ:X4Ƣk]?dZGj*F[:9Vnpw`Ә"!fOGP͓@H&pq/WKPy, ǵm]< YKD/ V/h>-Q&XI$ `.%b I֡( ?!{cTv>Џap'y88a%ߴD?BJ읉NpO7 g. w< AoO .)ֶ,og79$0D n!Sz|o^ ϐv`WAVe$!nBӺ0$v&Yi,o&cO\kC푓l# m2hAY)BVG\Ho,5Ȇ#~X*jC2s3dB7e|K?@z!2PH?ʁH=a@gclpcbh3pPjx}pmHM e^sHK-c +yaKD%MC58#Ɲ8*!@vIܛҕ6Ң64ja҇xCu9Y0TO9lƶP8%bGlM[-zb;mB vǞI#{3w|ֹЫ 6=4tFğ}K,\*{0l;e=1sHքWG=g[G+jm-SAQ @vHF+#!,a7@ +©guBzo~s;ٯɭb +?Ԣd|;U]3D +E^wb({O`}(% 1fZ;\;T6dtJ +)FAU/ *4d:ݓ&*;/.%8]ث1h&zS^gՈFiP U+ +҉8ЈC@٫L‚5g?6~i1y}DiǕJ8=65^hf5Ŀfď*LmQ`&~d&@ߐs3Ʌ0Wy4j?Pz4luUn.yGTӜZ ׊[Չ׹Iq/&8ebD<*^$YLM&qP(v1iLQ?qmgwkBVh[.mbO\[+ͣV1/4r$x%'|d8H2UKLldU(13 +%5?jrom$[`$cz?`tBgThjr\ uһo ^NK1?~ܟ20Q7o Aħij +ـk2nNG!(~zJs%gՒM O~x`A5 nLõ/NMx0ߞbu;ѿ? +aX[6 M*/v@#2;37s(pd(tZ/4 v cӘwf8b6s y.(m5ʩ0Y 3o + + +Mg9 ٣Ӥo$-/g>7[ΘHP5h!m^|^;S+t +z~ѶpGp@D޿Px5#HT\}r֋U-$\p==y2]mxjz Rfc,F\/ܯw]dešI>OȦ#|b?qFt'd~ꈂ(eٛq3k\I T> pLeRa|,dX8XöØ9#083{3[x2dl"4̑6LP2y&y:5Qʻ}ާcL|0y@9kul#Gfl!2}0=x00mJHre-BZ3aL7N_vPi@Մ,p&d1_"0uy .º[O+ZޯACAwD"N;'1l'mMB쪁`?MJf!4jBRF[IR6WCxk u!x۵MCTa1%0PM : 7/d.Dx"""BŐU-G`6m:\ЅZׇACǍ 8Fo0ĺ^֫|)!YQ>FtDD+\UnЁU}^1Af=ģ0(w l iw 812Ne8oM$g#!D qq &D/GDj!B#z6zaLt$p ̆d0yo,Ca{zO G2N8HFJc ab)V"DQh6o3`:v|$p+ˡ!|M=az u|x,:D:odZɉK=\;Wi<SC1᨟,G^]}^w6/͞Q$lFF4Ĩ2}K ?+E'e CTdDT Y |LM_=o0䭼| 7qc/nc"Aqpb+[; shR-V2RƉL>y9#߼ω]6sΛtaj體/mz䭶oyqSl7V\.õ3p=+:G %S?BNiː6GDR(^ԗ7'Brǩu:k?q>gў pW'9 >pڍdOhzOR >{{YJwmUj*l~5⎮仜/9?xu=^'vx*8-tՉ1|C;Iy323׊|.L|Y)"l#UEs,+W+~ΨsLk<û#3SsO.M44K')eXtCvlT5/UϿ[*JQ奂| *玜j3Yާ2N_8ג>;1k@@c& τ:A(ʼA(gYЧ>ldn=}>iϼW'FutsE +{\*=[-9;YMY ゃ쉀ldO AH ti&rtݾe69C3n5q.|YgUaGEe*Dז2ΡZܣz9Q;+ŘZ1.RLؿ\uD7f$L(tX:6P4سGG\[8^;,w1tIGmA{UEkEM%zev!Au_ʂQIe"\P7ȃ:AUE*[$?JEɧ'AC8sqG.fsSS5ZM:qgFeX-Ģ%69m iǽN+jkDD#ėX +u d:YHbAhҭj]m U!.v9Reמg::n-\6-~sJZr\7zl:s/rwj<~@([i3Ir Z?TmK?m_<Ϳ#3̹21%w}ЙYq37.7κGh~3+vxk_kI35vQ;랪F 3J};R>{y~?$~ߎRիs1ch7:#-pS"sPo.QǏDuD5WXPgĞr4ZiҹIqQ&1I63m /*_!Ơ7c6`KpՁX=I'u͠͠t(Ք̞2^H}ɝ80K14`ĢX㈡4ֲW5$ۊXbs3?cU ]'hcD9҄R^7vΕ;׊Ό 6<Г'H3& +Lf<wwV~a{{lNTMtunamYBWR pHNSi"L[YB(4>PAyg^ =#u( 蜊w%6!XMDbC7`$x\H4}<S; ZȘmgT؎˳p{#8`l:ˈ.3(neE' +4XJXA>vDpMD9mCmE$;ٚP5.r6ezfl{dψik/'̘5ӱ%{%;6o6l N{ -Ĵ +!7D d|DZw a]"""{)Lwm"gJaAkaôli5dZi%cX1?Lf̏ |2@ #ZrO[X[Opdםu=oo{$ELGOn<χ9]Wsk||30|7qa+y똧oۙ a4iC:J=ۼbc#p2[q;4sa2̃Ñ=%u^o7Yq ӂ8vBBBAPc~!׌^#?L#yX:̇٠υX!yr65n:/ׁemOmȃ yfBrR38oQL!Z +} `oD0bK, V,C93߱Lv›u^!r?yʧ#4j6B#(]\N#)uf6OHn.Xx+|-rPbX?`)laβ 7N+gWjN ثAx"~o44 .3 }'o֯eX\6/9Cw5aeahXK]LJUQAEDPAD ;hՖqA!B!’ 6A@eeԱU\Ǎ[}:,|۞'Ahyy'w +]@n['BIdN/pߙ,Ïт5FD*gM 2b6w/C> }i$WP00 ǽ#=!5 <rC ~gK\;+^)*Z?ϣ̞Evz1{:"[Lh=W#Ѻ3%i$c4!dHѝZ8Zi*GSvǬbKw̬5nqѿoq&(B~Dm'&4@4P:=t}qIa†m㼬 q]a_:7͚:RoNmqxN l'2f\a85y@J+="DC٩7ܵ3ZAJ-]I #7pICf%e#L`*g FeՃWғLm-#I]y]nucԚǴ_WЭeIBpKYԃ ~n'셧 +;_ T +d07lu蕢Di9H ,Rx*=AwZhk2#PEeKKϯQGnTֲLeՉmZE\I_(ʚC>>\WB zTH "p;Ё!.@[Jhh7_qKUVzYsk.Oޟr1>Vĭ)VմK3GDjtэ[~3酅pG +p-dv;/}UM}T]Uq}eϙavEouě&_X$89GfK3yh0?P Ha"%V. +t aF0!TCoՍ! +( +4H ^e$]•$SvxOl:&ޞ3@~@sDGz;:F6cTvx#$ٳdFAe'ɟ@ym䌙C#d\'~J?_AMXgō'ndLdfbːAkOBF>X<҃6]h +X@w @s-@U@1ɟ9#z 5ooԻvr +賧—N2DLMe2yOS*:M"%*Ko^}SiOC1q`dcd-K9 )R:Eڗ<׹fy󽿿c[D7]lz`&MD懿}D="5^#3ZAt2 yHT\/G{ iyN9QǭA +)*I)Y1Q%7T134Yp{Y}I,[`)D87N]Dt? Q aL]6Jɟ){ecz䣾)Fij 4y)~}c~__Q1JDGcgDi*RR1 ZQԐ#xPZ +Rи?ICe;A~VHq3'T|Q&c hX q=BT#+Dr%"UpQR6,"@*W9?눒~! +m"@uh3f%kggZwZ5,rE GiJi>N2F  GWKwHM|Vb]U>y0'7ذ=9L&k#+bX2a/48TH3Pp#>=7m~G3J|Vb]YZ 5r>Y7&s,p S3Lb5M=i!TMJH!HKx]{=+ggo +ks˒HR*BpZ KP*?3jC0w|6 uFcҀ;o.a!{XHXGfLByN%˕! +55o36h@t3ÚBh +1w4 KbKw% Y;֚yt9l-PD!*SD5 #-Z_'m4,¤AfA t{EГN\y,`}/_On v! o"+*!ЄR ⛓ ц^gLܬ3Kwpۀn5EɟA1cJ{x66p&6{|x#"8IJګR 5X=x^gTrUwDrGoXR7yPҨ'"@KN >mԎ*4;93c ;k["2^cGK¾ ;zF/khGC~ `k1t|j[k``eBa* ,g˸gbXs;&2F;Dv,.Za(.Ue 6_k~/>juʰ+:"G>:]͑&o"0!_cγ̭VNq.9X3(فpޔ$lͮ"ݎăŗƝw]اfc[f,fi ,X=`J,+ϳ\M\J#i>ԗU'#D3#^ΝZizR S7&3ytz֋ʹ-j-$4Y=Nz0d}?&S\ON+kGv/zgBk=,9^Aέ=du#2+眪2/x&=ng3v&?fbLgYX|yX_p9uP^'j,gԖ*hٗjX]crpU/wBn˹*3w+_-;ݵ<{ls~2ne9p=I>3L{YAyfQdAŶ;Օ +tn$^ݟiRmť}6 KvyE}eae(X!k"Ql(RGbŠ`AlJ)Cg3 0tADE@hu%MbuƠ9{·ss}7.qr +iKYL[iL$q_T$%ע$TKɹPJڠv {܏ x;Oq[8+o%:6j5tVƌiRc)I62]iƹ$ε(U\{ͣ _qUq$Rœ+Be93NkXYӟ3o 2_ν $^z=t'f>P[[kX=ZSZ}" fVJ6?[npNш=/,M3!DEl'f[\A^G?ν!#ux/@MsP_)±5ʪCJ+vMPi¦GYeK/LM)uL/vWչ*%RmTrQ3\r!! յ;FjVgVDXZ'$hcK˜ZQdQ{D-u{đU@1JjsgE]B v Cf}TxL|=;1,=Pt9 2p{`-]ydOfbϋO߶bM<Зh/wC־l1~y{w޿F_?aڸA3A{@Kv<҅_)6Ͽ`NXoVb͛uz؅U]8)?OJwszI޿֗#}[)@{0HWۀ}|?_4 +h!"'H bZ7 +g +#žXLr2ϰH+d*yك*`Dֳ+L @#p-k3)t4:ƜTj`A3zbk[{g`]@HYs4e"M\V\%-f-gfI䅿q_LhЍH T ##f D؃[n^wǀS?X̦A?c ds-iY5F a(f0->D/ +gx)#h.8Xڀfq֜-n@N tΦJadZ$e~.>蒘~1vyF5fqBDE H1q36FD%V@:=PB&" bWtAVg.EdE9sowy@`F=ԇz@mTp' zq[ +y);z +1H<`sbLaiJ2&N@0'Pq6ЊВ<Ӛ\s:_~#\,gNGc t."q.0OS..ߘ_'>sJ2E噠 `ihE}<6knLu]. 7<( 5[]% tT]/LuJqzyn7&ݞg}rd}܍Q!T޺l-:rL;S:?pHuoPz ]Șv/Tr/g~)wס4Q»<λ7vʯG|P?I-@5(S >jqCO\g{#ʟ2q6m aOu"IC0)?z+V~[ʧ^O)>}O}ni<|Jsw|?}Pjmoit7k2sS^F~{a,2^V P~$T5}>ؿW =?=Y?%pU +qip=p&tYx +KV +<"18T%Lu,g[ɵ̃o1su:W[jP]m$+4--YIDmEsp,~E|/ aEp +=t_v ΠP2XQ]“TZ_6HInUq +qaiQiqԢUA~m^^CNޠ(%1e5ELKilKrpF:BG8^Ņ +Ujd*+btIK3VKrM +-$6K|AXwGa(^Xufr2 =tUoIr> r<^ yWhZ5h3z0jj}e5eQUE"rYVQdVe""}FLڥH6 NZ1gn4r0N{IA_yW)yO4BC=6:AѽX ºxݼ#YšBӴrFNbIA ;AS;쫭@ 2J@vb9 r9wU2P(92%thoRB206Ԥ&JSBtQҠLIfq<%ňNvG/Q9ta×F9BkWv&h /2 +1(4fѬX#篿끧<;{{3(@LJ58^J҂Tw_ޤyyfD.m脧9%nƔɅ; /ApHH"MMłC@V4 Hu:8fY؝% +P +k#C3Fo8>nCF3.O +(QxC/ +M Lg> 8gs K:a5ݳ`3,ļ^ksĜ>}`1b:EcB:L^>#@B:?wfs`a`F*0%M% a@s1,dDr8`E+: +MJH<>K.^7qwd ;E|7UH.MM6` d U>2@B=pO=]`2?,-o~:|Ob,)c44AZPP!](>D cX3Ό/??Ĵ9n,6c04I sdV0L( z4hz"hF* 2,b3@36|Ieg}k$κZ39SϡYsgb0\?k͎a΃AW9܆*Lޣ:Ms #h2a9gN寁 ׷v\ۃgv4{7&dz4?Ι5)C7Oc +fh3C9B lg;i|g176ǵ@f[x̻@ dΡoQ"z-Scnq.w)~6qމ[vq/^[ъ LCSts k»aX8dq,;!X}V;c.c=vx-MWj"A]aY}֞D5$WmO/ҟȳfu=u~Բ@*Y[h/\6/=e?0 !\{zu9bHAeDϑTUݡUܤJvk8rȣNmVd2+KL!ƴmZrx"i,!D=l1݅u伻F8$9t +_OaլAv+Q\It͒x2Ӽ+ժ媜DYjP4 #o)4",WB`9xBOp֍siPMB&mٟE +9[8u9{̺1)ToFS 0dr 3C~kNu~F uV VxD=l9֙}aiߝns’uPS=*_Z'r/͕yr2||n5Ys3ԚRkְpO25,#c p; (]thR8f2Tm@a稲x}BgAAL]^n&$$3WX+sf(rn}бpO4xr͠O te十EQUkQj`(/M_"S,5|3t*aBo +K/<./<Zx3";|yY=, J?ݨJ- +IK%%_߆K3Baww#35򛋀Z*&z_Ȯ1ǏV*kMrSdYuAR":V]V +rBLOtzrȻ(셼z>t94,Ef:qM1N\)% jKRdi<`&X_pXpB-&#Ȟ4C4Q~ (%it Yh4jo=AھI.t9.sK*9 >X_\ Vn=P H9R~m `$im@juMCY +Q:{!IpLtx-fV;~ggt7=kq6aW6@ 4V@HI'`Nľy;svÞsɜs2Q; \p:yL1Qgu2rI3{xkQ@AOv^ߩ-S",$$IH +Ɍ]jZTҡזJJQRGԱ\ÛK)$k{/s?Zc[a0o޾&cG}9! :a~p8Vˀ-`C0iLIad!d4d 1Bf@B7~ +)*^NBC^ T᜿+ +R#L`)AԡG:gyC84!M&[3>tdg?/M1g?wq""gLu(#f06Y8ȄH ˷Hk;u4CY @:{n>;;*+=bM"nGwC}f@hb-qhnӜ1I@A,mddC? +vdѨ mFV^֡ pty. L72'Ζ׿ DzA#|1=Nlv%ZHks_F5H^#sbR6s6|O?eOy`QvSh{KhזdيV O  CylFau1W Ad hي֟wvbL,Ix5,'[x:ضT={v  2ȷ3c"8ǔYvj/jf4Mێ{N/@G}_)VK} }I+Owd׻TTu-F㲁=Vؠ &KQ)>"S+[$=^(^ m'k+9,[F_wh<j2:B*{u_cJP P2PŒG+LR- +P@Zl9 h @ls&y +Kw+^:!,p&'Cx2]'( P*X+?tc}sBvgdj)>%\/nZ[=!$J !m4wFw4$ ʵ}~ pE*ob aSPO}ra2pktI/g$fL#{7/\Cr>rLo80^ؗ觘V%%!FmDmh&ǧe%o0w}І: H2A +\{:U7ՕGCW (0t""FW7'`I &"D> uiR uU1vd׍"DP,9g?s{{ޯ{AE(&}(LÖInJ*yAVrazR2QaHT''lJHPm?mN bE,śohc/ [G>:NI2xH!"W4fVJ }2"5(YlȒƥYŦlI+N>e|E% b$,śUsC=&N*$(IFA''@1Y󑑵DC M ^\F ȴ#mH;) O) W> W + Ja!UX7yݏ_X@4s@jT ?9YnTH͝EB$HrurFE椙D +uz5mkڄe LaI,u?^:1)WAk`uߪWh$ԑ+ zA w\}HJ˿#ri栉V>9~)U@6zz0tD./#vk.u!XV!,Wj//]X_&}+u]~E?o׻(ISQ>ڛԧ IJCK`IX0 w`:C1 s46Ujij6]9X2mھᄒf۩Ȫw+7_4aaqGGcV$|<O/じѲ[1SOnŔL9S['DŽd3@A kPҋ>>IW|pv& }1?] 01r6FR 4v/ +@e?{`W!;cb!y#P0FSïZOל:0po z#.70V΃s{ .8p.*Q?a졌]~``C? +!{4`1 },Yj>à| 쩃 F]:fKXL>v a3Oޗ_6@<ׇ SH__#&C"fs&( .:`'>3~/c_s mloSBzZBylq'dP.> ` .a X~O; c잌}0cl/8MCSbM.>uN7PAHq=90^sBK/T(Ǎ$(<#Td9DNK Ё^&Ἆ% +>"T]di$-<^\SxbЋ~:XĎL"< s HwǑ)R_x%=\RWfL:<6Wڌg-mE?=Kܱ%v} ‘tc_ ?Wv$E,e +ʲTRtWˡ09p>n9=Í!mCZ] +\} amavpc]GqHf@+uYuxN$t[.{ہ;ny _q ^G +8֌ +b‹zx#'s}߂>ĝ>eS<.G_0Kf0|F_~Ju~ݏYә+?³{`J1*wV|AV.Ȏø~7`vl| +fpL`ޠSR/ҙֽg帿n#Zpusqnk~3!-q é3860k|N%#+ػ`K(s.>`^Ƿ?Ɠwpoqs\ َK8#3;p6g~SS嘊4D~LF1Ջ#O8c91wԃH +e=wWw|0C Fovp%pr4<[%֭Ic,6ʏMgZ9șxυr:ާnҞOϦОKg,CO܍p䅣C{~ +Ze|)_ͳ*Fٳ^&0)Dc>ՊsU} +&"B"c^ )]g꟢9B_"(>DgzؕaƢY%F*_5T. *n*) ʣ?_eJMDDW2B/WhRޢgVl`0 =?AGR֢EhX`H9 9N+M^fAWuZ?j-b""$?4 wh/RP; ѮCM/Af,%[ѨD.&]*4[U2wK +uZOFZ{_]O]|75L<Dzԃ)9prπRqЪ!`.]zfQcC!FCS!`(rJt*ϒ2ûlȷ촿캿R7?(K7B Jsz0Az@Ϯh0 Sy+6Ҹ`܋4NiY[Y袩ՕFUe@i>yƓ~kon!r&${NHN;%(5 +Z_.BM"?@yM!FAkJƔbSS)߹Фq-0)LuV;v' +yYü3`hp.8QPe@eІ +]|Lo~ JV1EPwCiNF%r,E5Rii0 +ByN$5E(m|!60yq84ﮤTS}ʁ'h,Ce] eS#9-ѐ$"ۖ,[6GfS:eti*gqO.{[y71wt,zWEs`(j6r?CVR:V!#ivB#Ց}<$ws]ܤ.sb%k|xqNf(~S$n-@жPgHz@ʁ~o"gb@bJ3Xlj;E v<_2$tLbQ4qDAQ T 2@ l:6i8Yi"anJd% )=+;'qfÅ{s{M<<*K!KOU?2lNxKٓKa[BoZNtO@ڝhL8[K5q8_Jۚz6SuݧəU_ra>y!x?Ck^m u<_ߑgt\ ťQ4W:kbu-˵p*\1p-zLSTS2:*g5 mjU-mg"kokhAԠEHn墐ǃzyz+nU{j_wg/mXn?؋tFhM&t}Y>MqPGt'jWvKesIn<$\:?^MFY z& =tw-$D"%^ޖwe,HdINGG)Qn맑[:Cϥhp %d,$Y+PvebeW(}\ё?K7tzX{:*Qf\@-ѲGxhIAxVYrc)T͇2kjPpoZ4S: YqyzQ(}a4}ܵ[$nٯ}*3f+\ϏǨ\FN5z/G 9c_tƴiqnqոipNS 8[N\:}eE\t^ΏΫsI..{id |՝9pD*;V;Q^kLg'LWG7GWm(wz\<.32Qug.t$Nu[ˉn{zQݣ***=Suލ26KSnbP/zxM/?.y3I93̧')>8/~|eIYÔpU +_IS󘗵hrç>} D|;R7Cfq`{!)a[)Ϟ'7";yA֧N-w=*`<ז93cP9d#( Lqt +dwb3r!C7jR \.%{1ԑ1>c %M6iʸ:_lŝ[.n|53>R:/D?vfk,vam+̈42& }B#yȻQƒi,G={Ǐ;C|ָAVTڒC!EAXyLb{Tۢ$뵙dNÖ')iѫjIfYS`Si.[ɱzuZߌ?bj:1 +/8̉=z͏ +&X'%6m6MEꔿaG[b<.j]\L|lWşMa2جxzy\S08jMu* /-916?S?S0m*)7asY0ߒꄕ֫$%=1ي͗'~|YKM%vq¯ldz^SI;+<3agw]Ū*=`Vq0ֵCQ;UڏN7KQBacQ#OogH\\,}vE~"]Ri_SZDQF"$@u/P`/E3&u!ĩ(Aa"$@a5r j 6xfm1n,ҍ52Ma1R'&" REH_F68>8>8-g(K"(0BnBR!۴ +(8M:8 Eb3gz[ͅBJf2(RL;FU@aHɩ,9E^Y `$s/;F^Չ Q: +yIpg +-NKPdX#nՖa%f+V 5Z+dk"ɺ]]hLW$XDyB(#8`mIdm\/lHsaX WlW#ՑG;ʅG\VhS9Ue2"ĥ?Ew pvnN&94&it۰:œ R HΎBRz#eĻ\^ZW4U/][1^y<:,:K3"vGɳ0|YbfW>ftb8uELH^砛{VH +f?֝RGf l~'0sӺ_GFm~`TL @L 0#vN'U᷻~~ߎ'#bdv&z^Śs׳  tvf]^ 71}pFZ0GLޛa ѽ{?C"f] 6`܃MBjP{^``O<0z |>'w_O+fcEPLgl-pnNB8{3"ugjP[C}jP}jM׏CN>y ^rÁ c9x^q(s@^a1ߝ#w؏&Wrfz? =ÿ$߀Z2GuE7\q ܩ!,. {~N /[Yv_R00cXPj".PD0s`} 6#0˨`#'yx9 ^E?8 P5n3a6\#ӥ]Jpiduqqt+\hn &PoD@BH}s7nc|t<\=q? •Q-MԢcR.NmJ6OCB|=-kpf148S3ljYwg86~.0A6 p8ܙ0]5)B0ΕV^ sд(*4|Rc?@m.yKjjb_7K\8qt*ml6x<ƕY1!/B˧pzIN.DG L8܆8+ +q賽8y<QՆjP *VF* \I{21J<*m?.9ퟌY98|=} PZCJzq`צzmKTFźCu .3ΧG%ͥe@R!ZVit4qGjEa߆8TmРr6g<~^=U(nQ +E(=@Ol1u@wVh14cӾqa5TOqh"ľ?T`&16KQ9{QłtŮ](@~anF^e8B#7r bEj-}o jPi*,Gy:P/QE6H;az9"t$J3;QCyсpĄ#7FR=ddґ.spib.ȏ B"&H1,cY8S/C.A&9C(aoTƣ$b8 +%SyROl"G ٱȌFF\*ԫ[tNZSVk=pxMppr,\BF4PAЬ-mVEijoa!Y#E+EV .&]2 ,.Q +#Bk'Sa f;Q.^G4doYJ)vEv&:w$W¢CRf`4D`ިh֘yL4*L-8S0Oa b߃qxBpFIƾJPT Ӑd\ 3h.)ڤHhb`1CiMCYKrk@fPj&<$!m&L{C/*9A#QLM>Zw ,0Z&!:eؼmDBJ )O"ΞX2{ +I-%ӂNA "5/n8q{P.пpFJqPQ.hߠa }#ڻlCB,(.5^ 3 ٲ%!PX/3c=TRFiޒ-vƌecf1"|zw?ϽƓk|f S'111`_`iL^ӦM׼0ZmQqM=X@@{Qވʁ܋uRsǚ*|Аik1%ȞIAn eܺ/ i èE Y͈0n7t>%xwsx8xHpm l^5wʽ@tBhmַ`̆Ό +s`dwgxF'b"G`X|F+2O#_qx{)QU,Hkh{nL ã*㽩âcg1xnvc'>ŎaHfP\?sMc?F79o]X?T^)(5yjlm$'u9ّ~.8=cG8̣gJzajBRbg~DdIb;wQޠL=5F\>:vfkfWds@:ey1ۗi3ŴDCyH,Mtʑڧɟ|E*Wৱ;lK@<*Mգ44wA; |[89)ZI:GKU7S =Yʁb^abn v{f?4-ъXEFT,nKNt5]5S\%yj?7#E9OPOwUzAjG(f]Uhdqz +L߂&8SSM.@/2tIe>Wh*W%zPx-G)B=]r{hN`9 +'f)X|'9qQ\WMpnnAu wu]K^{ +x[ub_3ъSq;UrW( e⦸%~ŝTVG?sRx|RC@^Bnʽȵ2_B!hS<*33ǼtQ4tQ5ڀ6Bs.>5 \01VLsbRP^ bxN"H)vk=$@0 HLJI\<Io= g=:Wr0~7GwJIB׶dvnˢ3$mI.ZiILn9@_ƿRc^Q&$nh)VSy1 z0IXHOR$]kLD1tZRMӊhNsД136s_x3/Qydp?Z9"uiO$8 :ѿ;tÎe?IKF4eʡ!ۂ?N}N956Aun?UxLp.RnW) 3;̷ūwbUiOVCZ owtf̥5k rӐ),j +滨RUЈy/a\p|҂~$/̗ԃoȃKՎGtfM%wMy4䯤`-f>s"^K:UEx +(/*jiݍ, Joc^[!EK̈́Y:Cn'P+EF-KeqT*s(Y(,(^BV-hE7)̶ba sCo:s`\9[&h0OhT< JRVÑEÆYN3HaY7g0'u>#4D=u=u|YXYCx(w.Y ++ˀPG/ݝvy j'*sWކz| +jW8ȩLv]"ud哮Z_I}$GI%'"Vry_yp\K{]Zm]5`l =($7őd$)mͅ7;k5> abLLӗ60ou:1i)vArKmW({2mo]J\*=X ĶӑƎwxX sOwbM"U9{0.GT^EvR@|'\Q@-5]jT/Ve_ޭtJQ2`"vp888( rQD7Լ!^JHԬk|f&sK̬ɯWgwkoWPLNTå#m7i= ݏ)+'tψPh5JC|tPs& ?kLJmm{cg-J]+n~Bi;q] ~J5wZov54CMP2nl snWMmi޴94}-{j?:N`_ v浿XΧQmۻPۣ=噥lzv ]6e5=Yƪ^wY۠ACͤ'a:޾ۏvxSWge,;Om0>gf '/o5}RݯQl`ՀݬxgX6 +]u,e1t|>QϣMґb{l0W5X;8!d V4aT +e,^b: /S2En0-yc]̀;=Vg8t FP|9J=,`ɨ,IPܯ?'U]Z޽+U/j0*<ǰˏ%c(Eٸ8z'Rd JPSH" +|OL>r}O%پ?1 kAO#bpYiFjP R'6ƣk<;Q97KS32_JND3E̛L:gnBrV0GMdϬ/Ō{7s7]?6h=o5z^#*w` e( DD7ܠ$rҘIvpY! `f2B_'=8C/rgn` CO8kfNQJxb[J{P2AC^S ;l*md2+b&3#rQBz2E֐ԨcD'9p\YG׼#J8+a} JN(#}Ȋ +$3*Y&2̈I&=&i9H5-!ży'v簙ncȷ3Q;{&)Af P ?3QɎy,dIL;8a+'RRrxY,qN`jU-ZcyY'T=nՄSGdf@fĹ3EZ R#INaMĞ$1![B>V[jO 1Y bⅥKOW-sfHXTW1׌knjLK6h'-DB=}&q|0- sw>] +p/-X~׼0yD&hNv͎, = ruj]o)&f'LZaEQ}p:Bc5/z[5>[rjۖ.ƀ`f͒osV3ěiVf$jz%Ԥ)<[hbR )%4iLjS/9停^9!Zy;P"qGN?ɤ4cS35cfKxΚ*笹%,9fCfg'jxNT.l.2 M7qWsQ(#HrEZK qچ?Srɑr;~g/ 9Hd?R6e]0UC\ +V* *ܤѲ,V2+:EeQTt;!Zbߏ7o2ިX){Ilg)(35VSb).uTRW(lYFosYLwcqݒԩԠ%-T.2w|"9H,`4=_UKA˥$J#2S* [EQ= xثsi jPJd(ǹ3Z!m̖VJ^D3Fss;C7Rra8_@C1{Fl]/e۝9F&k/7>{ Cr1x%zg +)~V0s.5J"v=?[91w pJAH~ 0 f"X "JzɌWf̺.=Wmg1I'O`P9נ=wXqfBLQx$3ѿk7r֭&='y?f;p1Q^-;No|+k@z)uL]M c?D%̃.0#"J4cEp0!dlv7d7c96dM. IP`h9Ro[pTFthvhVڻG2cL63}~4oy?Y4/r6]`b]vH?fcn]Cws$v=px"42e?.쮠'F:tбQbp&h4|pzWb7۳_ш{G/i쟒9)1߉W1C2'>qeo32iKl%^ٛEuKTf~GPj ސ従Sb"Nb0V$ѝ !eDbL)3ƬY4LnLf7QEuA*sR3;rߣ4dW&Id-]:;'%I_H]JgݴgnŴp9sS嚨5[񚋨+*J^*,-YPj=<8,QdNa&yڇw {:U}=P^ѩ,ؖܵ6ךD-*OU4PZЎO}}B8Oqi@ꮗv:%"q MIn +!9fq)ZLĶ؈iqf"[{ o`W;[_% +a-!)@X")=ii.nE&i{U*{@|2b&c=DuExW<w+Bv!!~wwg-=3Ȧ3;ľ^Ps>Q/* +S Hڹ͐ =_+ ⡾ lP D<#?ࠋ?CzY3kq_}wZ'TWQ P+RT[/>+Y?ڑ 7̚P~0=fVkj㗹c UÁoxF{}L=$ivh$!luʀG嬜\Skmj˦vpT4L%qtK|kZMA#bV(@N)N)NaFcn>v퇼~APMG? Ê'`2 ؓ|O|jj̩tTܜ67%΋u4)vK (8=Ы~ AtםI}<#8-ܷj ]32`gӄy5j^׏_/Lm0G#ict?%s~^(^//%^コte_!{SqA.ڋj* +tV]qU|y^ٯ}.Wlif~ +6˾}Y&%x[s \!/HpL6L]@_]e ۮ١1icO ?L TTG\" C)Q (DdSYaaf`e@vYPh$.Ւf7ZXk1դnhbL39z8a}b6c 9O€c8&&sr{2q !A>:ANQϏs*#`  xiiw"_;4 6(EPuLph d&AfďXL$p,̹3wivE7'<9¢ ri'w &_͛)9ZD~>3KO16G=,|fx=u\:Σ&zvгn|A|~ 'pԏWb\IjPѓʻZOc(rF\XoFpO1CAxwcV{h{]Ã{챟Bz?鋡KƘ4ɠ'UF&3ڻ}ܕ.VS8tdg]ω1OgNbHg}'ƛ7!סgR1^{Άɕ{[5 SS5*ZgC󬟰}mc3_Imsks6p@˼>(?@%j=~D0F5_g&xipqC >xm%ehXhAg= hDW.څ:nxBϻ͋Bab6<}xQs̓n^wW=ǞhZޡhB *CRenYjJP|,CVl^a?4JGq">GܓK8s+[#tyhEӊ@l@]@,j^z +[SQhFŪl +*By jlގҐ=( 9( Cob]w ,  +>#8قtNh)hz [Q0TFasظZd4<%e(؂ˆ&v#Oԏ\o`E d##Bn\"c~#={0{\^WAQꊪ՞CyD0l"ơ42 %jGPeAAt.K.f3rc`#K܋ qXg.40~M0C=ywmᵁԬJ,l[/JcQ"GQl +D^ȉ*)BzX.)->A:R 9%J}alQÁW8\Z^+"LGY;J}P( @4 (5QD-2e&dȲ`I*@z Z;*^~:)h:!T2Nk!|3{>˙G2E s/BnrXȒ%YsJL yHU!EU >ՇTȓ$sm6{I9Y϶ i)\aM^LRXT0 M&m2RzftCn;A @ ܇}tmh}.lrN̂E4bHM CJJ$t)ЦʡIBeH`P$cd&$ 1!>bW 6܇8U8> 3ލɜHG {_`qИJ2M Ez"* H4gBj.ļ q- ".Z.AdQQ19aGQ[_ XRЧM +eH\0$dEA%A|qz-CՆzDXn=aZtG|q+APcID, f(ڃDVY.#Mr#lAUb Z +S֩vRѡh;&i'|NMrysI~B~+[jPKݷs G?K˘=5}Q@ M 䰑>^'kBt>W5."Dc" Qz9ZGyQw|Br!M䝋;ȰX +ý f) +^W?]cc5&Oc&hTlFԈ]!oCR4`PV 1|(wrQ19Xb; +w +iJ0ij41FJ#:kD| L8Ms䖴T咔YNɵrL>/Ǥ"9zoŝH/2 +&p܁~#K>'fA>P+qr̘c/cz iTRu#̳7Vg4H rrƛ*ESF=b- 9Kriv렮yβ]:u7v%jk)ELjizOVpM-UYr]HxFw q_6_/ dduȗ +%6.SR6W.WʋV:h2Zrٻp32<sxsX{0yOҨli beM-̔[mµ 6FUU4Ul4x5N"`>7cx/TD»NR9Fx6KH;aqn{{%}'~6E-8p_-Wo^ڿD"_怷R{|\י 1.ǹ|¦s*X'z|Osi{@:u`S7˥W+Axƿ_>N)8 Ol \[5+##47+$q/]n([ًzձ&&y h/@P5k.[\pI8rߣ7xIxi*gR8::ͼ79/ԍ;XbF)`,]z"N/8K7Sx3b|aB_`̀w y4i1Z)Nϔ0u&=UR0=b{@gZQ"=O'h7-V^ >0s6EVuJrM4DmktַF^~k7g8q#QTF &,~vmֽqle#:Dn^8kY<ʥh-Q; KFqTM2'( 3GdYrre]&$Mm$ks^'v H9Ez_;Eݲ-eP9{N(6{,viXs擞W9Hr G5cC7Qr<^KsY=PLj( grM~> {6|XDV B+ H**TTCBQwS+_%"EQ "Gy[s?&=.^!݆,+ٕ۲ ƓQ4٘KI)]AriXL$c,P^Dlyf"=;!9LA,z*w-nKzhR,įy:aվKuiKwC]$Li)((k`~dev\~LǯLjOO*?aZOxgRi oo&7I}<ҷ }K`ܖ˥nqE'U\f9rP)UVvIVPڙNҟGn?%M*1QTdӯ߀^Q"itTܠ8*ֿ)<|$aP{ԭ MꁴNvlm~0e~ I'z{t=:ǐ>~C kهVʘ{uud\L<-6ҭ]`x<sG/JwHQ2P;\8s9D/٣QyT8Kחu}OkB.6ikVKu[f@5Gҝ_>Oz!qW/XLIoAS2oȄV8>Z*zqN:wFOi1Мͪ9yXzv_z|E&NxSNh̼vCc$Sq)^FM??k?/kG#zT[s.e愽̐z̘C ![P蔆,HR_Dq]/򻡙|oJK=uv͹zzhy} +&sqL-Xh|:nۚ2wdAVq_ @MN93E<"ƋYb"]E p)G+UνKFKk%*y}ezSy]wCO&>b7r!Q]&QgUA #!dIksl6n6n6ɒ q!$` @ [TePj=Nmkk^a>!3{{tY'Ovqʼnϟ54?h20㸈(<+LOK]gO)!zU;MW G/F%(FX>"Zx_xO9)%8›@6+JE|Q|A]"<'={(sՠXtVҡYE8,1yy\a~ [s jҔYbcåQ1:ֱ~q ;٧p{JV.EVwJS_rMl yS3Is"1czyiE!NX'52,֨8ŹZCʊJ+uIfSs|jWt^9gD4J|e"O'NtuX*NnWy%"3*gQ#;Eаc)0M^oe fF̗29 'i`~Ʀozo]eyóαmz[nύeBsu*u=5KWlzޭ53~  Ekfύ6Fձ{753 z^ǶEZt``lY(>Od.5=4q#31]8D^gf|{atq1b0[n]`r7[W\1bȪ:AO27Bq':/N*.tս@סA%S:ǥUv{BFV2kd(.BVWпJ_ "ܜƵI;i1:"h/4f'S2t eL}9touKd,%Oo:z2^_D(,+uК97?w=xݏ;{F'{K>aNj t͎fJz)sY gM3/`~>TR P؀(@SQ'^<ŃGqYr')y[o)E.[qʅ* ? c٪s_-RDɽP|: +^@kq%M%J4-sP]֎g]TZq/UO`xs'˿\Ti״OL{*]ŪKRj P|Yx̅4TS_Y҉ai^Ս,U0WVSo0Y*%sZsi=$nCbnV9ڣgI}Jy&MTJUA5Z B́@uM3>*)d?E?آX'uAkp6K ~1GTK[ծ-^Gmu5+'`wbs`-֌i쬧OY]'Mn5F$GvD#ASZL|iZ}*鳪穁:5؝VgވٝI2wjJy)"ۻo=KER=wRIGy:WgTQ1,nnj۬n̤ܻƕ4%RܔF/_1yJrv[I ú1em>UMRc'ĿG⏈/nءt*b;Ŷz\-lk1GVk22h'v+).! &>8Ϊbϲ2 b?'-JlO(67fŮ۬XkNΙv-d] +5$RX! E`&~Uݍtw"ew$Y.6šYܥ +ZzHqy[M/eyEBP.D<UTDDD[`Y`Qf}Yq mJ㵍dvlǎʡJ[ n+ \Zѻp?UVIlP9`VJyl:noF A{QhA| /IwN܌xn!N'3w,>n|y3~DZ߃萵'&{s {f8 l8zNBt'9UKˈwN=1y)`;Vn6z@f֔hNsh /b%.l+jtktn + +w:_M'9_l q>.a +j>A$?#n9p}I᯶2Pe7>jq?q/X[q3 +Yw4i}B?gֳN1ǟͽ=G(%9h+WO/0;GLMըLUdO~d5=v ͷc[/3t]%=P +3$Nt?6j`/v%=p3W,xX3ɰO&ETB!(ڨoWqU_;*wVeu؎Xqx%n\?s7rכX.DdvUݦjED-wJQm2RM|U*Us^)+TgJ]UzBEWT>V^_K,/<E`&XݝƩ̽i+s -Fũ=AIZ?U dUGJVijNEkUUy^o(r,;Z82Yݥ\wަ AK 5zuiLU=_^cT)à*!3U2tW,(ϧ\M],e)]Pm{T?K|- sfNZF +юYZ:C/\E1*iZ<"I#S32S٣kR-VzYiAE􎒃?[J +~YA.rgc!m6_Z]ZOE# pB=:^ Tfek~"+5R)a֤J ?g~ShZK'uBk5<>2R/Sh-RN‚S7[sǥ)%2[ɑJPb +͈ڨ]}PguKS,Yt6 9>?iKy-Byω襅c= GQh91S5;&QI1Jk:Ǯ.3?ƃ(Ifh)*xCweWceU`mAZRz&zI4U3cSM:MMMC9F#j1{C&%'UbJHR|Gq.{&-f5od-JS5fߎRQ#5-aCe%dL)6ThB%V*fأXqE./򝢓]uxL~;fxb񽬁v~ӆm)H#KKeCd6Fʔ:U)JMː1-O)EJJRBC&ŚV(ڴASLa:py_WxRx*of771)8}g9}L@e1#Ls͉JLSBf +UZEeWdv²;&gQH +ɺ +1)`{ 6c6P4 u3g>*c%g+1'X q!ȨLMɟRVA,K5ѲN-;5rXAs +\yXlp MRFdQeb WQ2}"a +(̚&)Ц v+Wp.ZNg5[{qK WP&Oî&4ؙصT,ltQ_?/P&ق4$DJ"54NFfktY˪P6Ge-QoA'ҫZ|K\򵹴ߚ߂]KR;v:$1ibEoW>1U]c0#T#gDɯ:IOj|ej}W VyhT_[zʥ6ɽRo{5PؙV)TXא*]a[;\>u\7^4.VO8 vdH^hPoGz90n̡RZj6rOup+w4lb/1Ttsu_%?B ~2=Z3sh7Cm@9RȻ j"GEv;QmBʭPjBGuQ 'r cWȟ9Ek@nMoeRZfHz[zu:ڤSA*-߇4'Q`<wq3d9}D-Ú{?gs\c4r % P=#;D(s֣)Qg(Cސ$] 8: uiH_#t%!$ạb #[EVIt%_OAa #[9&d{[pاGϊt|~!ɷј~ x8g .edO=L9 KEv V1o x"@6H CRPqA  ub'F>85QƼ#aڠqr݌vDKӦ$!.F!bs X!&6ĤHI gi&-m2 `8Ϝ/=EY@v*|@5٥|UɰRfl+O,5VzE<#n *@"#D)NF(/8C,'k~z'mdƁ 0qH\ bzq^n6l:+ln+{.7Z,ʅQ~xό&?}QS!wQau@l9ٵd2 $ܳy^/25u6 \ .sֹ;rr^F7vsdNr?`Z0s@Qx+8\-ߢ6+)9ݼ;56F2*wsoश|0:kMDY]. ~2|wy-C^y|7SEoUc޵}gBc[\/wٌ60wԻW8H2#LEdXEE.}_1jGb2Fm4hN8m15M4ccF{iJ]$}v@ov@U<ة׽2Yd0eè]AW-6+=qGMx!{!}tߞ{FCQDVD4g4 ؉?Gq5p9wpXi 6nsAҮ+8ڏ5yӇWy[po3Nwu_j2ެSbXMu5?OpFih.,pQI9V=Bg>V+Y_iqYk,zM0&1?IL)b%p6.> ,+~PN 8{_էxݏKx̋x1:`t1bxzR!qH~HV@ j?k81-0SțatNQ;i0fc ^W8HR`;@^4E7%c7O1 3+R.>aY. +cy)FS-|H6Pg +h6z~xF.(V"dmT F% z2c"1 N7_@W"Ud|%]YF;i)%'ƑsQvbU)ē B.u;eWS[88ޖLLJ O ykpV] fW==l5M54XPQĖ/aYٍvXUxfxYY]tS =ouKz?PS@i:(+x-ZŵKB|m>ѷL]O4_WgH&I9tBqkz[AU۪ {ڰjzA: w+:F\y|_fWWh2kJX:mhZë^I٢ȩj6[E=>T}I;:S#wĵQh1c\ F֮^hG%krHjp& /US5y!Y MŪN\+wҋr%˙k9n,1 +O I/ZǨȕ?cڝvH5'$)1]IfK)V}jjSQ'ψ&w "}Tj:i.9z@N1 +dYwq*#w|ԁZ<)GSȟ+_zLfʓaS2UeTY#רq"gVʲ$)ͼOS3',YPGǩ9Ѹt2ױsj>3TQQd%i$r.ИgWȑSs'8w +-sU`Y<Z^Q̖ ʲPV2d (9ψ 99P3߾,suv9rƫ2BμQr[T_ TXP/u򭝲f+ǶBf&H lnt}etlcYGw!_ՠ*,ØrZ6\v[ G0G"ť/vRUNx.iW[e27*;[7lWIϔTrO } xm&ƯTi z$Ta&(4]yef)Qю +e9=tSU3ZDXK[~N14yW~9j\ uق-%̻xO܊peW<&EYcF*ӕ MJ*ZS{^U]Cê*K u_UxES+Fػ>vX^_!Y\[c46Ji QYI|%z슯q)V1fEy*»@CkݦP Lk>ր/5OV~[G}VpMzU;d{0ԅ(~|1%+ƗE6*~ +whWz_g(2xßHbL>0"r ܛXD%ފ& 1F֤8Subk25UjML4V$xd[Okg:'r{{{nb7"EV-C `gÞY?~b5>򫝠gcDj=[By+8XX;Ύyc;Že=R/nʅ;vykXFoc5Bu"6b|Ёt L8W^ʦ} +HȻT_LwY5@YVzt6ؔ-V.66L>XNdA';Kfɔdbicricl}Ḯ]4n yN#$Q4/|Z`y薌@wϜ0`#l+' ɰI,3z e2<1\xIc5q%u~Þ=,ӌV~OOǴnf]5-vV4Ű!G)=LCz|g3VFۉc=es{Ym@==úB}Eϳ׳aR8>%#_bQZpiq D'C\8lY <:bz:@OL-Ժ_WmmvIa=$~ca<Q<<'Nt'( ĿL%ew2G99J#{zCG<:N6ӟ3Y\xHɝp.+rוaŽ"P.Ӟ!1'cm:aJhqLr_7a?.JA.A 3J-dȔ$o3OQcF?1 ֯g E ]AA5lt8>?n(θK[esSoW(Ţ(zy +%Ns&Lpw9,q%ݤI?Y>$k'/һn[o5 +H}ztU8,<]Q0j ` *Z^s6= +Ml- HO蓡!IzO+M9r)$ yTj G`>f0:QYc kT1}en1߈'ƀ+x#5N d8鬔Co! +i~zFpG?qKt{v׻Xm'AI=4rl318I-G'tȀ1 +aQ`nm:6KdG_# z۸bLƑK(ᤒ˳0ra(Ӆ68udDBkWrV~vCsm.]>4qF뱏ZlD8™ +g&L*S j=f8NvĮ%-Tf/tIG{Hiʢ~&xc)V'dAɃSK'A.8V'DT:h"OrS=Еwڸ"{I^a&BEtX鰲a*jV3-6r01KNu"K!7;4\~ ?/AQRHwb=iaO' YiPR}DjTԼݪ4'黲xR>PѨ;!r=p/-\ky:I[Y[ f 5"Q)?YɓͧXUO[U[1mn۠q}*;|7r{1rWyjwȳQ3q-k#I8f=ߧT;A?QUgbB':@J'Ud|hN2^eRf)e_Ьr+==,vz1: -`*U!kp,!* ISIhTVʛlWΔʞҩ5ߦJ8o+%}D|i!3qs3Up;'SprgT#_W00Y&OTIx"bT93ܨʉ.RvU15ʈqhVlfƮحJ;q2ǝWBUtWodq#nj"9/ɜ(XfCۋU`E*'.Aŧ(+aMVyf-hӌFMKlԤJLڬ}M:褳LTTE'>V٭S\=SA+v1ڣ + *1N̤)ʘIJKNLHӴR%2<7†! Bpn%,.$lH@́JU`pT +JH*-NOZ@ԱzӠX&?|'9f].Q a֣iJWm*/% |?oYÚgE\,৓q=j,[23̲eZ4'ӪYs/KG43FwW*žN`߫)^)~Yq>toP*a1z-b?g}͐O#dJ;MI]9JҌ\̎B8JWļ)޹U݊q> yg4.;51!]=Fw\XȖ0S\)!sRMҌy 25=ߢi6%*W񮥊u7Mp?1hLSr2RtAHɷ-ĺ =\hgֆ;ʔh꒦ϏRJ,dM.LU|lSb5ShO<٩H/yE*b-/9~ v^n΄{|+ F+$FK%5ĢQ%2-p_"}5c4X|,#>^oрɷ{9 + 9m.fJGh4' ?3,KVAβ T^ ~aW,s6 [ʀmm;>JT%L5 採fZCC!> K\1T`J)#sڌߛa\"`πqVkydhhjC=H) Ę  Bd 2g[Slmϧ7îa*Yns`Ϭpil4f9; +# 5Whk7[hw+ƵbH haRj92=5j] ZAN6س'&JkJVtjCz~tG'IL:q&!37Tv0)v,?@#DI2R`gö:b07V1lfm!.[G2.tn} FM5*|]*Ei;'5I94l0za!?sh|_ǁx}4+~_Xög3nLanm 1c OagMot{Q}q}D:292Z:[AߩN<6Go1}R,81㈒~`,H/DIgۗx^xb;O[p9:K}jb텝; vVi÷=OC s+]XZ~:)׈%qLL/.܋X,9"s63ی'2lit ƃK92#ھ~~‹JxaW]#_K?9O'㋏reBȌQ`@3&P0`g#y=-x\1k0!B2n; CP",0 +|* x{{=~=^`/ӼJF{p0DžW>&|!Bϣ>5w>(g+4=qox`%llՄ +PZmNsSlj?pHP'O>nu ߤs QJ!w@^T*90\>NUz-vҼȎ#OqI>R%Snq[?}~/N*N + pQ"VJ(0V6Z(-{)Cx=<~5D } +]>i{FD옊wH[ + h2Fz_٭NDߦ} J/on?men{ 'S`̄1;`8``A,% Zy\C]˭" +"n`Fهnbcd)2 8?gNeprvMVqJN dQYVOՑu^mq!!@r$&l6ٜl]@"A x!"@ZNǖb; TTGHhCR;N'fw{}l~d}w\ExdIghG=$AgX l51LG6hLܳLRUFQy&ƌ8f +bsA1s G  B-I޷q;+T-Q1jN'&U͋˓;X5񕪚]'WBԫbS +4oSyG0WyLvu^oy܅4 :8Wbz]xA3k$KZSjU%JVERʒUVќ&Zt+׺JrXn-Rge >_)Av EDfY ցFԼd|UYĩܚҔ*NTQSsK[\[m󕙶Xi+fD۷jߣ9CJLʜ64۩ч]B̷{ApT|EnR~UyQvF2.e8dʖyR˒VYe~J (.b.kRq~y=+醧{ВN>nȁ R^Vr╕,GM9˖[ԼZYZ4٩D]29U|Êߥ*yJWHy_(2eIg1Z Y +\TĢj5C}.Y%*dBK(yM/>Ppg +)ZO g+|9h}8A)G!޳XJ-.KiX,SM˳_^ +*jS[ +q )ȵ]\7Sů{<pK3kC5gq%LHr)Ʌ +UlubjLIQdMk]Z[uFGײyk܈nn U筞Vf`W"x}pYM+p;N; 9-EMWXCf4+!YA 6Mk ?%ʃ`WLsAC4!ff>B~ g-.r.X +t?+ՀjRP+X^%/x ؇|G_P>S~r >?w6ipρ;3X \* 8X 9wj:I.y= oag+JP +N7+XQ>Bnn^9je$9ĪI:H WZ.u1H뙍,!z2`mI`?ܰ 0Aw>\4i]=;n[''m +bgBk!z7=IJc>/[c7°0Tۨ6fs+>sx;8K[8|Zz]ҙ9Q姈)|#; 1Tg8xsܽ$2F{NJQZzz] +>'!fRi|7x nC5 1e +!59I!g|ӓc4( =¾:C{]U-|T w.yڃc}D(M9 5e8~:0z8^d \/@rntg^U2.FqIӷc~?1x48 X9^ĨM^;?0' #1x5]赙Cw~#@]9r;.7|2?,SqV8wP]Q\_08@0__Be9nO8,x8k Qߥio+⤧8ߎ!p`ҿx +դ/'x8, k +"W8tVecx(gO#? o%@YS,Т+_Wc8׈w[%%\9)9$|>GC(Q&C㌎iqlI'1("yc$x.cK9&p3堶pOwL^<ި`Tϧ~0b'Q8\_,ɡpp䅣;cubyOJu'B?9>1槍#pXfjG9QZh n8?`x(6!2ـ]OgA/e]G4v$ùv;؉pj&6GiXWZvk1: 1nch& +EPU$u0!DI\*ELZ);P;$@k$}{KUGG9s=aSJ#u qx@ pDrUtxj/'Mwѕ < "XQ +0#J +QJ,^xG6t?|A"ο>8G";-*GH8CS+͠чGb6q>wW?ѪzJ,VǪ2'U-N!5)G5M*6:ˢ#g= +yYc&]al irxw &m!ЇDg]HNk:V+jR@.5*ƯƻԐѢnjN6#}Q#rWTjZS[ࣘ<8A U@#{A'G3ElijUP:Ye˪Rmv@5Utȗ;܍8GŎU8|9xEَ}+ %B!9, 49q`nsר֑-^][P]GEZ(=P%K @0kA9C-¿㏶@;W$k,>0`Ї=Jc̣pt6i/GP)#(ag@{} cbp۰HiU?` K~#`MRij2M`St )m< ؊֛^7y;60}%R +)x.ϰQJ?KPG+tClZ9 49YFc>Em,aIX>N쎣l84KxdwN &!|[CZv5O&b8MoeU!8xAG;c8=,: z.OnDޞZ֖Z^<(_pLJ7" +OXO?<#snj}ʹ|· I3$5"ѨmbFVQȩfƧ j|O 1F^(?#<AN [}0#=4-\$i?ѸqMul?B,ƹ :bp3x22]D/F;G;J>Z-Ӗdbc*V$u]bⓎSprˋ/F6>9=FʓDyWOH$I-*ui5`p)VW)Ys>|ME%“0օR(?~xZ"ùnjMF58#w߫|o43G,`gl3̿OnX o~OSNĮ{SlĚvO֢+㬄C +{Mܠ{-bUҜ^  fTHcF3=34H!$uYmaw q(bg!PŢR+7(1e${a0\qB?`7hQM/0tiTiPLRS&\P7Uk^8$ gt@Lb4|w$cQJOAb(f)"H(ho< 1T"ERU:ZKiK<.&I(eOKd$_IFm,]'}y71L6_0+z@{UiNY{Il2T-)KQEoVɶNlܕ/Hi_'-X@pjP lHr[ ^"ŕV19&ɭjW%ùINn%I#`YȪR-]Y ;&AG i' T/bɑ Q ^l'?pW@ "9>( +6@D.ߤ +-)D +l(6FqEPxZq .j>; Ys"`B40j5ɌpPF8 VDkt0yq3"fs ߗ9y n4/p'] +wa#HNCI ڡjqtP1Xh#FQ1UЉ@xCFvBԂ` #00 +M,\{(~58G4>_ !g} }5jx-|Z*ޮd +c)5;zr)88/G51p7ֳ1;3#9W'1krM5CxN q11Eoȧ)iS4PIF6؉db +_3`:[Ļ(?>5QsB[neC3=;]. +y'ǻY[feG]8vi˗jNcQja0}b ܁wyk'h!~p3nDՖ)wdՖym.hßkǚZ +2uzw?COs2Μ'/]lcĻsZ-,wBƻO[h\Ey% USuI/ +2~Dm2m;5]Ot3ʿ+;V|!m(׆}7Z.5mbrgvϡ#80:.} w-#}o)Rm19HxО,M, p?D'J;񮧥V*JmsEʝ<~(2.+ԎK t1/>ȿd-=0J rM +?Ik_\"1 ÷l u7KQ&6h#60N^1 +0eX + /gKsQ +qZD ju7BNS>7rZ@Wnop "lVo}#kcNztfiЕl JXW'F [_-51p8 7KUCb7r^SH^xv'kbcdH (-滺3$l\ !cM.|+ +(&┸KQ2( 6z~M,ۤ,9)4Ob(ӂ6An֕V+36-?A X ~KXY"B`&^%V۠,پNbo} $6*]~!Wf46=_'yԃ0]]/.QvTUTHE[lVª%RhbG#bpNI] HN"Levp*PuQСb>8z(ʖ >.].C5R\'&,e^2K20{TC`5wfr_o/SIqb3.\c""1+9>P_A U.H A* gpYCwq3p,#ʚnVSs@/Z0 aE0c/Fuu]9Lf 󆾔hp8`c/p4>.P 5 R*YIA [#bXdNjSYT8(NGc=wk &1awÑ5<^޻A94,bb,"8bnDL |AaI% d)N%I&Wq6Cy?4];-؞DCRMH) ImAPmQqh@y0#;Fit+ut,31qlžxPjy3mȬ5 w xZH!&Ĥae0*/2\8NaʌհcY}E.\y9|ixo:ռ 6ۭYjl˹X=ƈ*=c8lQSS0Oy {/Wbl.lUk=%oLp)s^8xrZn8&!@5gxhQ] =ZCUG4֪aKR +-0 +U!18,vTvp8646ݩv< ~{}1o?M}?BkWCG-J($6H_m(-(-oKNЎkncL;-).LB[B#]-Q> ur`̮yDhy7h-ڿJ֜Wms! Qp < /߃7g衯dPU^9uk>|n۬v~Gvp7%;DVA2ɇ$9 ?/g~U+Wƿ% V4P6h9\A_B=OžEfwH7I?W؞^i촯/N9 YSW?4.$Oղ!^#=Ѡ^p^Ii0Q/b'R̯+ੇ [8=I>^;hROpǣl &by9iePJf0C& ~6RGimEs[̽4?U9Ӱоi1q8>KofvíbDYm~B,Uy6֨ɗ" v-ptMFfhRV1&2v+mOfe_TyP4jͪESf^Ϟy1ܘ"G:q-n'Dpq +a9X p0"Ed"u[MWl5蠅s +@2xᩃYF0A-l?ūdN$ &T V@Ē(PlA5tV3“jxi!3:gLZh#|+H(aK[D/׼J0VMdTMLȍ. UWUW+\pe8{OƹzI$,5TUWE$WɛW%LNKB* 5rmfau.eި oq˛6o+0@J)d3F?P/6p*bl)J6/RSb#JF09"9$:w~iI#%4]4E-x_cG!+mvls_W$Mp ȍҁt >CV.2e pv;#% y<;KbN8ϗb{@"b 9J%訒"GsIsDHnqӵSs|RlXΰ03A lAob}K&@D\srgK0#!cRonyĝ+aqzy7xE`yϯDrfO16I\  6 wϛ-z_$Qq&K,vYT^!gC!-sfMN/b-XO324źTJAA}YRs|_x. x`XB,!v0KB +"T\ È)H,[ ~$4?úW$@ lRK~!"B∸Ŧ t|I$ƨX?8Yc=b4y#`8/xZ95?̒| e+bGa@), tk͘Hh +:LݮWrRAL2PZ_ ` g԰%@ >auI2 ('*sD<#O8):PiD ̫UܡL(USZb6gE{=A+jIQK6F|Mģx45W0 6XnC6\R A syJk@89p6U47|iJ4&Ki&d!g-Wd!~ }1^a5)+yq3"wrnrdH >C<@@? T=sfH,cmJ^˰5gAb m  0f{ KQ;Ge`,c ӧXY]axyS˸KBj2yPv+k$>IOе5tUЧVlؙ䵊Q^iujW~*_QQ_i$6t%dUPوŀ% *DPD"u6TpRC4;x?g?d>f899}}/lq &uٞ u!Ї>ч|@U> |П. o`ڲ[]uuFd^Ĉ`{*l+Kz0,mmS1b Nj*5bf8jA  ! אp(+aQ@}8y`=lἼQv@0,mˠ>*&U uPeE"#U c/"s|XPX;B0}CH"^ @m Mj]HDl l7SLPJ.`Hp />C/umB%UC嶠8 U7ʗH#lT6y8VyvA C{Ihqha{"'Ξ3y|E*,[⫕|vEmݦcw{^8tX~:~Suhg70426hjvk׭ott͝ear#Qx1 SR33e>~_PXTyeՋ54mmkx79/q?y~Y$ ,I/!/$j$L ٷ*$? $>p8Bh¯YO8\ . +X <  L&%P,IKd4))!t*WjB-Q'aqi=g-YwI)$> ?9/$!1k0DFFVS0GNn.'/?h|X,Y +L +P,23hl>3ys*X1RNRjQtFbGꃇx%5O7Pi%KN;㚮۶Z&^c!QeQ﯆ij)ۆJB^,Boi6MvpC r{h`^y=0KU8mCeMHj&b݅*6ה]>y'KǞܱ~Wp;0uQ5PUU AQG k˳٥pvi vSr e'SqA9`.Q"_ +/AUQ>" Ĵt-w+ÚZ{7b6o YTP8*#w1I-_R|';&peDa2Ϧ7;ĭa$m*/1)1..WMxU\VG% <x1!]{90g8Q`wPi}wޚďԧ߫NJKM+.7upݭݵuv3ֵ:n gVEwE⾑@G8B};!H H90\}%k;}I^/gM e_x9T^}o&d|}8i[ `V܊GQ ]z6C#:r]\5T9Va[Tャ roߴ=ĄtFWc!ۍ CǕZ+%u*]^[4@ L u[flM8bN>N:Ԟ1ؙiT-1izB +YiA?joqnD;7I{فZ,Ē |Z#)T5ܬ 2a9KGw?#w')/# *ZKFRM\PTYi ҳ^zwgK Ljδ?Cc:J('irNufk^KuS?ty;!PZÍ|T/wbHxA +ETnhX 1a6z-\Y7'ww`ı#i!dzuoZ/k+$ +UX~Ћ+kGZ{(hXxHV?/WSuH6"sh\>I&&4VI<핁ʏ;h;M=:F:b/Տe&)ܹHQoac>)#R۩TR+wJ8ycв44fW >mxp6s"%T5'IjC">aJqi 8AtZП;. c8luaBybJH "7.HY|Ni)bp1nq߉Д$yjy,lWܽꖣ ;6s&'kBmOA%ꉠ7% DfI@]*Pp^Cr7ٷ POO !azj|B n<-'A\Psـ,@= XwӀch̴n r[L7E aH?H$2~=eG2h:`O從ٛ C|pv\X@Ї l+z#]; d5}K%$@<xZq5d^ C\pC8pn \^遼AD%~9 ƂOb@_b@hJ=Ͳl{ /wwT~JzN:+ÄHIq"v(_=Q݌*l't;hE6†2@ 4 (aEm̡ym7+MʉRfP?ȕcY2HV 4c;+ 6`ڦ;; чf )YQ5ᰒaRBOŪfw&B1Vh8"l`ؠis3]|z˜y}5e hh /~EEyTI'i"DIlcpkiҘ=-m4. +"00l̾+3ð̀0AM@@@}i^{7>;jS)YO;|k@f.i<]9_m6+LMF9oA޿ duÖþ:w=͝SבIlJjYjhu<$TH +bssހYA1A ܽw 3юi6ZkyCRVt2(. Jko1w~YϢɛˆW86P{?)CLtM\+R֋ZF4yr9SѢp "d0Þ,k_\ +1]ȵV %FX =Yٺ4-DRARm6jXM:Fc6M2HEa/6SytP@Αw_=]6p/7s l؊5Ŝ#Kʦ>5@ȟG 8v"H)9󌠬ODYy(aF:m2]|o ]f:lb-^9&^+i?C 7v ;2d#8F <r%C$((Vb n.[҉XKVsZDl@$HK*w{w2$t_"' (h=Řo1WqB8 ?K$Q:ĮM$d lL7Y<з?łXᮣh,k09xEt8Z-'@ۉQ ~u,$p kb w1Q4P~G!ĸXY]0'>ذ><K<~Qo]/A; 0 +.B֊P {+rqd4(h>PMb/ /95c%7pF.qć1NnFIݚџWWWS@Ci>[s½Lq<1Mk) +}܄8i\d'eI:(3`~eʉ} +=GRt lnn󳴞!SL< #ԛJW>. [c,h 1hB/$ +oYM*YI`+]Bkv9:2"`/g|͹fOVFL_F[$tj9Qܔ%5"ZXs]* Slf h0A EKϼ7]u{Ʊ|oyvl%75GI5*JnVHJ6SU"lSyR#n/ڃ{A'g9Ck#w]?_zN%)UJOo0sN]Q +mJD^HE6QL! F [}x\5u3{!b`S޺NGfl^[̦Tl^.jt"YYkLfN.׈:Exzi AJ|ߘuxNԇ|9b}WqwcR6#B.aRlf>j + uI4mA+N1 t7C N>Sxs}׎w&]h'^&6Zc&JZ]FU.T!  Cӡs__aQ_id1$1Dͣ}S]5hLKDD4i 3 {/ 2WK "tH|lw/8;߹juh׶SW]ag~U[O +/O'rbyAJd&e<%ꛓR[- kB`8a5WraD!E/xx4i%XW)H>DbfIOIa&ө|v\g-nQ |; A -@:{u՘){i?0mtz*E1IO2yxq >hq77Y7aga;4Yv3l5͙^;VχSǜ ]ᾠ^p=>)} a2~4$f:Ԓ@{'_;Tۻ2N9d,mJ*\a!N2H92.@qjg/iŬ1 ע- b;-/_6{7h}w-KfA~*z(X 8 xH<)ѐy, TT_ lE9Q)k5[%+kpu -|Bx ϢFlmP폀/q`*_[Ңn5/},CY3"#9pa \x͑m?A8쭡!a-~zjFf֣)`!WH<.6f0 ܟ!j} ľw7{MoɦxϒJ24!H"Bd8p݆֡,k+}g !k<Db\;^w׀- +YM${#n63xԽ;&Ic:pϜ+t%,`W+@wn/rsnȎ瘁4>Q+:4mtf($(4u' +f42(K oW 0V,85zM7F|;K8?EsڵPcIx"}aJ*)洋8ݜlv?m41 ; i|ڙBOۉ]EBŞO{׬6je2J/EJi7ɲx]{^R@ cؿiߜ*/ 5T]_I̥cY,\^ YNHRKzA߯T(-wȊr~@:LYANhŬ W ٭MpRf>(ͤh´B9@)(/HrH!S ;q +w1`糗#Ot@]]zl}Rs+;Hʽ]X PPURU}2Mw 󤽙% 7J{6~qoMnzmr1urEYV1(_RnKyU.Is{ydpqDfS<_x --DW]#9=FtU|Z\&fU¼RH#5No*;;{OXl6#y+دmq7:zTY1 +r9_^JnݒJje9%z@)z%y? uh,ӻ[ ty}O'Y9Mk_>%DjlVWdgW"ʪ*e"nITfFC8hͲ!GcVw$n5-A0#E +]xO+xΝ*C1KȬ +;dZ< yUÄ!Δu_.zF )hce-l,nc>t m~k*;/;v#(ᦟӞs˚|VFjFBӮX ;P=z#Ҏi Wl+m)ܯ +?.$M /8۟iġ4"jPGQ U1$ÃĄC<;| M4lFϷ 1YTU|χse]ҍ,WwfyRc/Xdl $[ +d}9z[)j{d/BuGT>sޢU +^/9;gHtB@ w:'­ Pw.f*5Iҵ[Z>T:n.X䁻. -bw!i78%AƁD!ć!nebx 40ڢeHhj?E%s %8 96ˆsVXkR h"7&Al"?2_E(ȳծ(wJ +!fn1CMH 8-c ZM+Ol̬r4xd=gXWiɠ>TPy@tܳZ -Fn]%흓f:dKSRI߉ m8媁:>1ٓ:)e"5y:8ÐqM{vP\BA6c~9贔sjR$tVxR24DdGψCiC{h#W)Q)ad(K_da9(=ߩTa(㿙RPsYcRQD +y+I}鏓^4FI:x}T)eɁ!A9.X/ͨCO}?4 + mNǺuڣc]0Z[7"u*"[Q@%!}O a !HE e_S7}{rҚ˹<(2Csc%H>~Sc^'`2[YF4"9P?8qƑBx7~^Kq+`]+3U2-,1:Eu i s8cj\!P?8PgI-r_1geYȮXqC됎ad̬y>]J*Y-n̫#2$&;ii8㈰C&vgoVZ0Sk2it_Os1*[jJSQ/+JxRl +"T-@b^u~õT~.hX]@Li.k1rRCZ0%~MhL{"K:C+6w`%=3~{~\۲>&w 7E:Yj gz*(bVBatC^::[Ic*%w*ѽGBـH6K;Ti!޹n׎>x?P_c])mF"~tuA;w!ɽ*Ve)'ʠ'fP,f^^>OS,Tk$YReE;̽{A~{4pnG=~[z!Mc][HjhHQHe\]&3[ Uby~4#4=g'cб;pU`rW4nU͏uƜoKthײ"Bn4EU`d\=@+H$O$!8oa @佃`%U lp^gA_7?r6ZStM:^ZBά3JUlQJ’F1#x|A%sF~7{_Z-A_=eLځ{?pM|j9{]y}iȍO8^w''0dBMFϢ[6CkFY -k# {Шrx5M9Tv4 =$^Wu&7QRdz`<"۫KUn&td)8ArwNALݩx(?ͮL A3%T5t-FD߾EpNೖ  +ɐD`Ax7?DCHhsöVi: 5,DHߐ +#98# [ą+tXEI!qS,| ­y8dn P(廂i-55 dX4}7J}G\P +\XnKkY?AG!@X mM0wm2P:C?(h F#B +XD 1K|!D(I\\n/񇠥~nbB²RVx}Wz`';6QQPAB"ć~5yl{t[fvqص;:vkZoA9Pnxs'"!!$pA.  Cgq~~ͼy? 6o`N$p  A pU ܮw?~Ļe[/jo+@'p};$iԧ@7p+v{-yG8'OO-μA_{x0 cd=B^C{"M&t^r 5Ȱ_)+ o+R|߿$,8ȷ_5綞g?|rxf]F}W+Ô˃pw9tB +MMEwX <@ZyR*gTo^q|rNXz5{w 4? G7!C?WIЁ[[OTkAzNJ%oisrYČ9RT\5Mpۮ2n >\cbHm+9$][O[O~ _]tK Z75w3ʔNy iϸ8/`Lh>ʫ 4Gc^TCt!]qCj&|a`{M UÞ3 bKR[J%&7( r-o:7g-Dd89d9sk8oo$lQ@V+W h6`&ĸ*,!,g<fJ~7Q~kHY pOݫ lϔ+cLL܌$մ +q &~ZSH4-Ż :ag;>+K? +Pe~+"gl7-Ji$k^".fdU Ipfl+:ai#+٧9˥ՓyMgIP<[wYjl&PʣYzfmNusctSoL`_ +[OeIdx_o~a5`[_+0Icz%֒#PsŌU ˤl8(O$XW{tA8 ' nW5|c]=us %U/jZi|`֙&m^k9:3N3Ԫ'IZ4(;kd8Z5ro5["5&W֔ӯsCE1f9&Ǥ!|4#N5q4qպ,vJJvwU=cxyE}uKSu34+fES r]nEU2V0tqa\?Ng:x@m\;´d 2;흷6[VM>e^)d0 D]Qrrs)KjH̝q[bHN+qngb'2U~n:?rzU}{[sI]:4PŋwVNQ%,&Ȃ +jYv6p-rxWtU[`@ vh'̪^OO6Uu|[ڎ=Bhl_5pnkEqj9VRNH)":e1Ye]:AfO%С:Hߊ̕ y,rqm_Ʋѻ +&~@ҵ١{(]vGШI6B2Jk4z$^9s`o&8|foa3cٿ7 6psC#jGN_¡̧gՏ.V !fmYZk!Z'HSI$l+ +x{6"#됮ϐ{C{ob7?7GnU~Lu/Q?^?+ "(fL|hG0ZDZڱEMȒ@B%JB ,您Xh '&y>緼}^Q_]%z`='ꢦċ h||o{A9 לjH\%5ۓSSx\w5Lfv.³M~Zo-Dto-<.i=%#~Z#ES⿓8[$Z@pi?E. ڊںCqFOWa贀|,ѓjjgYjP٘#:7q73Tt}BO]!3׈|p#GblH>pr ː-pa;Pkkhclv;>E BP}-F%ztָ D%±y_ą%ɐ,&2 w] C\&lBT谙3=FB~1Ci؈4`T࠙l͂P+&P2 v>Ѐ($5җP 4JC xb:/"t& +ʁe( g %^ĂCf GC +䙑@1s€eIq| +C `P t^TϟUC1  FIY0a #>8B A،(@"$Y@7fzAԬ騽9 ^ b4Crڠ؊3W0#8A(:a (hb EɄ 8l:"aގƽ{A? 5_(g5ׁ}k߱5k<2:ڍ'aH &)r@]C8vQ #<R_ 9(.`UW77Om=cn.-{a ԟ a)>:PCM&n^u'Td+P2@*gQ![O\?%˷\n=nx1Qo+ [BÄ׉2zY3Gbl +`~ra%9[d?ׄWZŁRPj߽Z)n- +7@L FdJ@ +UѢ@p|?@.q$ {![Ca5}cwwA罹߳_Pi ؖw/KtDv:5BBeblkE!ת†!NMh|+`"OY]Ӡrh ONR*-y*iVs"'D|wMt%4PCQ-訦;n:CqO~g7m`!'Ho-Y#_C_͏ 9TZ .xLX#o_Ceԃȓ'Xy +gN_+O{μ3ƺ= ^@Gsh +5p~(~c7.į}$o{5|)4#p0`0w0ˊ5XhxQu6pC[ڳ![SMIkm`|VPx +ꔮtvFӼ^bU4$T%"TElSf21UUV 2@혥ФV0ZeMU=(~>ndNҘʹ8H0߷"5kE:x5Osq-Gc$n`hM-4MV;?AgF|Ҋ& V>@K @u@Q?rFVAVH xЊ,za#}>*E4ɥEJ*x^h7'k=Vf4(~h@joH x8pњ@)4tԙHE= 93vK:aBGT~=QP[P$'ȺĆ?/o@;]7tW?S;.`;fg1w YcV+J[d=~ %@CCSB3pý`[6 +gN/H9,ݓ(=!0r(1FN^G?Og\Kjzf8cl/pgc:ta# vvaVIОIOJY_22Nrc*1P:'Yܥ-[ڕi6@NdN~z5~6a'&d3qp.2{$= qbT20X4b [_ +yի' 'ʵ;?siدLXFXc?p&n&g6y#q'}N2kDt1efaH<E/Fad`"q~c׳i;yO[?GgOۇظ/j]Ӻ]aM_i3ֶV[qV;Պ[}M@B! BHB!%@!.{RAQE|} (Sly {=1&02VҺtƷ47)w/{c fH^ywSQb2~4Lf ;.&c +!k'zdlzmW=/5fsCzmIGgbƏOOLF<0 "-bac@,reèG9M }r%nv1{^0r&7<&u=7A:?q( !#qP<L[ԹL`RZԈoR>&{?͔XLQ5R:. _y7ҢSTx4K@>yF]\6mռvVx}Ì4/adRDzsM0J-qR2Li[ܠk[7{y3:6:14`0yI ȟ\ Xp]5'_;# 8%&0!&I2zmZG'W[-q{&thHgn@2u8q7x'[=xY,g͔qB㞈rpDl{b.r;K,hfx7|C~uԇ5Y;j&tҳyc|pi@}3 [ux!w^1Z=o'9,%'Xu m鎷.MI +zƫUW4zpq cұ!4DIC"ol[0|coFwH:%tlXIyjrNWX *bM}\Kƀi@]/ A6W0'? =к_}<FIm(^ Ĕ^ΰ-8j%|LXj\^Z^o d1>hEО⠹ ^那` r^]鷮(b_:W~F+g;d0*S'+q46D! HID4\ۂ#\4?ټ`R{Qu :PϚ{+41jBA%OvIeI$ +\ff^Q"G3̷Ln H@+u^*˔dx #uGAOuޟ7Tu{JS*5B^>%'/C* iYTik(Oz''}Bg-aӲ1`I4`){'xR YMmf"]$d^>sY⻋ž {3(% +ȇC9nklzw ?[|xT4lAK[k*n* u*򰢂lSFDE<״B7fv@ZVY* IPNXE,[ =Ð"1l`V W~mm@cݲvOZBvGՐ$UT[aˑK${pJb%- iTm0EO+Z`h'ckH-txR70P +t]f躯|R^]rc@6RlR繵\*'MKї+pWIp rp0Kt'О `Jٰ4u}~t6Umt_->g8jIJ, +CW'i'q'a3boC;CJY45/O@5P:L҇v3gl[8zH==KݥMCWo?rk?9LQ)5ˀ}}9wSqJ6K24LDDȾDBq8qp[I)iqjJih5wZmtB8u_|G(ou*m!oeme4W6KHr֋l4u7;s+o_u*\o[`&(Tɯw.\pA ~0>cpD]HQ ʖ˸)D$tHm.J,9SkYVLY5Cְ>Nf)s&_gj@ 58!XBQCNz|fD)D9 |p+x'x/S7 WgdfSa#ǔr:zN7@ B8Πs/.c:먆pc>``!#` F%Y\@!<}E9V5`8EwAWGTYSU2 YyNU3yc2g#dAFKw17fP?PW XA}REo"rR'#';d_}v&/FVd O]dc01 r&1аs }e?e XV;Tߍv+@^*Ƴ'bD9ӐHy'g!Bƹ5c;džWFÜVAyr|~k+ MznwHOlr _p=&9 )(BAѬqqG GWy7 ^,඘sd {99q ?sN.WP_@~aGJ}hߩIq$uFDG O&5\^P_ԬCsn߲N'..v.n㣩20v(?ew[L7^PE{n*Jh +HgJIOTWXkBͲWt[t?Zq~jEK<.o4SQt6tT0=Xu,3jP&OK'1rд[r̬K\n.Kp9I)neY)(v;v9b>YWg <"Lz{~3~&CGe`eE)>G*tiXW_vܭ֎Knh*Q@$ʾFPAAdd  @" C@`DקhmQc;Ӫ@9<3K;l;܏*?핝\%];g,֐cKj2NUΕz7UZgyWiʰ2uĽΩ42F<b"Λh*mp5U:^WN -?VYhQFm[%w:)痹fzdh>rCߢg̷Y&[}w돬OELccb3XV]h8S~d^2yy4FMߩJU]q>[ƿ[ȿW4L@2!AOfHa,ËUx\{^`wkRܶKJj7? +2v*9< Hp +՞9M>b%|77?9\"+%;}!3f/eUwj7`=O 4U.֔E*ݨ'mUm$S(.rX=U"0ŃjS ֭C_nt6Ol̮YT]yr2JHeKdyFdsk$ 63thwY2^ʝn +ZPV\QVZENέ%(q*K>I%W}&z$оT%+h qz<.Uh/N-~Vr|锹>vHCv &>J씬V$VV'TzUxV {U%T'g71MNĭR#k> -QcK{ǩzw+(xtL0$5@! e Mb-P>`P|D0K2~is^w†˳qvtE#[Np"8m{#T&dT=llmj{  e -7Ssn˼fz-jExg7G__dѯ o 뿶'نҎK]tjާ- F(3 o ek pFgG.wέ:u/sMPMCU[ i-C[ +3fr" }P!drA=T8,Zd=F ):OD]x]]'QNxKˎ_T=m^}iyaǣ恏,pVc,@ t&8?`]#_ ܟpbF"$bSBGg9:57hTihj88π z"Pw,@Pwˀ&{6 IK["_kBx9`a~5IR&^|~7u[37Ƙ^,U@u+)w3'h5Biv.qKtЋ"yP;%R wх H;C^,5@ a i8LOkx5h\h/ Ntt{(;.:[ؐ`HԦ: HdC!? O2e+a>ɂle6L;kڏm䎭K + +aEcz;b-Ez8}8<Ȑs*;|iV0-3X:|Oye=d߱r |b+ +ǔB ;Lg#y;7ټ7܀ď#.x5+-BQڥrSG頢Iu)0e,c0f0Ìa:%IZꞺ-E"sN~>@Lb*PʧF( ӁҤ3mth SFu( @R@R8as@ {1ӻc9Ig}? xMjPS1 rM*J ޝԧ3ƨgPuSRGtRA @W88dIĬI˙ +4&rVs3iu3hthz>}3{ȧk7~~/>07z5Ǟw܎l~'1Mz'j=Mk?{Ƙވa+n{8w|,^C|a~/Qþ q8cӁ X> 8l8=}1=(_0l| hZOoGo3߹ߗ~-p<a܌gam0cӆڃa\Ќ9!9 0T]7 S_+ޮ {U җ X7b>`: +{o#W2}Zc(:Ps5/2I7"ma3c¨;l凐5;6 j5|=㦶gqqh GLp35v,`^X!2>)=3RmxfɫP:!m!-?nesK{a.5*d:5x`Ď叻jΖ@#Jp'}=tq׵LE)̞D[X> blv/_ַ͡CXdX6EYc$ވcd: v#xgcNDNBFv\W?6: Ή2|1ax\E[YmlnF6noxz++=v#4DKѰR "&6p'{;= &$'Atxb"-@)?7~$?fYqۮ*mT}yw={.Du9Բٝq`Yf&u!pŷ`^Ě$ Ϸ!"&iO2f6 bV%\̯&6$X׳vWmrmw.>UL*~| T `a:}o{^OyfWhE:%= ZYf/nV]5iH_䊬j9Y5IvU eϝq`_s*g8+_:ݥL[elQrRp5xb dk7)aA:ms y ?>Y=Q8rU(Bjy ?ժ'Uqs9ʤ}% .ʼnw\ ^)>$ ;&ÎB.l+䁕:j8=/o&g5O3&KVҪp¼{lMUYA4|e8֤*=\Ʒ*oWe%Bn"[nr9I]rC{<5:o-#sjVfL),HK^r3EU *IyY:ϪD$^&W;˜sSj]e)yOܥR9e `,lei`5xce%d|WyIkS@JL\tzfoakCR;X8?k>_@knǤ,K"[M #?@}{yup#q'][%:\@AٞNGM?`5?e[i9{mn.q9\cd7΋&lrFghȻ Zs8S2MFN# \4`ۊT yR^sz}Yѧ +U*WzuIukfIXkbof{pȻdW&"t2ʥ;]RԈƬ m=tҽWdD(Q|fxQCDQMd!Y10!Ӏ@YPPe}Y97g"DOύ7'Wq퍘7G\8Nu-}b䵽"*VXT\ +xi|*ʷiWf&hNvRοw[#z + AxTD`QmThOPVWZS[m=AYEq5T$8U dTj!Z O#oH|iW/ՉQT`HuQ:_:4HGzE~g}kGz1ҧy.s4{$7\Μy{UZHz*Dsľ?" "V'WDz!ЯaC/߆tލy75UȚ6~$qgppg3W=b4-_@Ѳi]yBۻ-TG֮ҕ'vlJ:yu{v*3pü1XjS#W =V $a}9cMr2rZOI|O +E`!`>%iR ӞKtDuV#Ź@u>?u*d4y5 4.4Gle&bϵr-6 kJOK:Δ25r,pHDY;Eb,"yc0&&Î8ӹ?i.r_AI$o䍉_/PP*F&S#Gyv=L  Uhș|\ϘBrgM8;z~z̹smA֔ku[R_ ?flRf5*7( (1;ŋ^)>Pt> Oc%y$#Mlw\T^;{-BgT5"PsJ)<Ѱ>ls;4y|7A' +*TxihMa/O?UHih`2w0~Y wƛ;;1 Q U%j^ԩV߯&r}ڕLk3,ǡg^AȍC +~=VPӘʐ7Biص0\NT4kpeǓo wbTtウx| 3x|jbE(},GG~Ú@kl;6]٧*\PuTڤ`X,E7 +1הjv6p3u,-qV=ut|W88v*?x?Wl]8Lj3aDw_͊՝*$ZbeQK iZA{F`?=QT KQPF\w:xRSim(vÀq?ztK|mjFe6j 2EVZR'1ŭJq_B4#F= +] +)JhWC8;{/z;Xv) .=t7|xCF-idRBJAT}mMZIQMi{f7K*#K-7U*G29u: V"?M2{hC0wú:,Yf7jj(Zg6m2꫶̐:~J#SU5JcDy!CT*s{yWقwiB%JP+_x]pja@p4%CG|xisSfkZQP1w( N+& ;ak/m%Q+1O&V +x tn{Bost㡥Shn9n2^(Xm>i$j-3;L п/P;x]6p|ios8}%6J4GAi)i?J,qL*<{1\7&vWbg4܁`w`XяA>҉ rW-L{2'U+8[H@*d!H[ۑ/# ޅD@9S]-O_i?τcLw@ʍ62[<[!I!:Y닝""rSD(p;Gwytۭ(CpB,`D;c4aS݀'T +w|/HsQ7|byjywa]ރ7p/w_{hC]{\ .Y:QUY7(@(~ E?f/ۅC8[OKV,=tyXW5ʩ8MR 1&)j+[Miv2.2eH7!Gǹ]WDG%|>k]yV ۔%MU4wVW[Smu#}]@I.T1pٟ@Po6i΅Ő|tW;rc +^Qs+|q'u{$\$pk|t"Fٕ-@6wm9 ٬u@nyVEܓ- +_=v+yz=N(~W_T$.v +UNqMF"n$|4zbw ksYZ|zW($((TvJrE3vI65VmJ3wFFIkgz )R,`\yxp/~^zs'% D4F!MSlfZ{Z'UIϫ6I ~+1ѧBfhk:VZO}hȿ4Txhg`\p'|s'|] /SlO5oV~!KaA|[4ηD=g5:[t~Ҫ#J?RGjh3?4=\nA3j%x/[(o +FFC# {]v`DehnE BJ* VpIipqIW롤{-Chߑj΄Scu^{&_8ND #F+ޒu#"ߤ4iI1] {iv9٥^4HFz"i3?h$=1荚8K4$x⁇)Ea%j%ȵoog.1٣7rTŨE'F>X {gq>Y~4s1w.tCy[+ MIP''iQ>;i7S5KRt3$fǤ(И¸cZ3lSx*ƞ' d=\YpцhK4kX'c*6-@fWmn +P!Ӽs15UPŨ elGƟNҮDvF}tQIg&R-%2Lecz~9)6kɊGdkAJ>92|I/wIq<]NF]Uxr9J++BF)ʢe)dR"嬔"aA-A{=AJ&$52(Hdz{{G稬 ŵˠ]2\^sLNUɡ$SQ<<2\aXVs՞vFkz]k^hhVlbd5m.C8ZJJGfB]em nY1.HqԩuYyV15%v55{kN:Et9~༧cT"+f9Ǔh$ZJQp y'iAv +dlMof|"j\҉8MbMɧG5͌lh:iehݮWd@ d>?'|ZBDkp36ig"} oKl1^D?d +T7:z0! ԭ l=bIL䠅jVjqQ2jM7e'#iα33T@ D5B !];rl*bUPNt>/a:p^B&?:fyo՟0c%@!ۀۍ 9b41>lp']0 2d +^O&GYN wY; 0`n>l.nK9ݢ70NyCyz26D N}v8l0=~r%.K_'q؉؉a><>mlx# +e2QE}ѠѦѭѯ?8kq>cp6O ? zא _3{ nF="W ov?s&$JD=dVg31a54ɼlz9f6|fvbԼp1> Lbp +=F Zs>"6̯W]][y4&X `=|;"Û2*QVHtC .G_x)zB ˡ Ӗs NI1`,b :&]# +|bOBA|$M'^M_]/.k:׾L\`iSWlbL~>G,đļ'Bg~/LB\.dG;G7g +'AwЮX Wɠ6( k%셕rbJ&)e%EjbiwOZx.)/.|'OIqEBPEm蝅~= +PCkw\ ) U&ʬe +]iI^"w{~>zAA=~e9*7d.d+ұ{S~͹N9JmM 5u (}&XNPTpJleBGJ,.S ڥeo<Fq?_cPW TkAS +;ʴ{Jk̊3,r%Uat@tT{p=~Y1:_VżWqYS<%itϣQ Ь5zjXkq(")E &Ezm~1ԙ[[鞥makk׼a{kkFٮ?ijB~UkBj$54SjM0&g:Z?ZU" iruCMH%xﻇc59yS%gpG?y"s{(!ǘ"%J7ȱX~xj-C~[moO}Ȥ@_A=1@d^sO'",m:6||Y*X լljmv[Uo=%Z2 hM.֘ @=q{k<d sx`2+K j_X,/Ze^]I,gg'eο,9f8'N"GkȮ5RdR7EuˠE<'G{Ӏ-uYw7 w! ʼX~/XEA0 +C0\gaѿɋEw3D>w} +E>"||Wx Rxr__B[B!7Po<=,}2^ŋ]m0ܟg,ϣ0珋p.+\Zٶy"ҁm׀sm/z-sf͂/8YJL+CE(`6voc0*S݇mU5l+3|C|oΟp +~uV z|p&}נe:m ǘO0s4F7i`TqsM;Hs|\0lZ{`8Mha0<07S +F_ +CڃaB^r=N\Fpcɀzq) 3K?6I0tt盏C! ^tOqM2{p Ηcyg +\17Aѝs9tɊS¤LƼߗ0~ %Ȍ!cAC@#l@bG'15hTɢYKa|˞U\&4[YWP?1=42f2bҝ-d+l,% &Zl-+`Y:"ԲVmqA (sh+BuhH!\ -_O=h rxO8Q&uhtڄN;QgF㨞y +U x뜊Jk(^^DVg&W9?~Ȇ|-㵙-L>Msh9PU?m/~F{$^G{^xGAmwdKו 7^O^/;|k.Q^{ts|c}uuɯ wjݛOrfY$%G~[uz͸@ɨ?onx`!Jvxd)+ lѹKr^(KyL?Cy5eR+ti]S9EIJ%)I=9=Cn=) Aʉxl.BJ[@dt]Qѿ蜮O4 TU\ FTST$(&aEM?.Ө3?a [A@LQ멵3*ZqBXC $!!!!%eqgә:.Emg{Lo_OP;~{ygw:֕θ.E08{wsbe<܀٩ɱ.r:-u9rһfxpǹ=1N7- ̯۹/}ڸ Xnb\D{X;6^<a:]ۆX%0+p/aݎ{r>~=m9#r.Ys<nΝoe|6A/FYtG‹tdp7< [0RÅ (p-wY]{'_{+0Z [xMF޹ލ03_ASWx6f }'Luģ ;v|7\,Äp5FDbx7Sfo+q XmBUXn)4 -¶`?N0n\]R#xZ]hu!`,b| 0[H{|F}u=f8+ɒlDd'q蔦طIVXX"c[>ŕ:Q]IR-:f-^~#\/O>BK$MڵaWm@|;:qLd"tj,Ef]+UHtj) nw'#*QZKQ.1\BmOԹh?KoDѥXv6X1hVkPuWԖ j%FyO\˩Ud!pMHud􋥊o#?(eLG.U^.yP\S*}_ѮZ5Gf/,vfMIRTŞ*Nh@R+ʆ#e/--&Jx*S0[~`өb<(}[OZͨF jt) +seUzjK5*?2P1mH|X}wioKWab $00{4JH+ߘjxlQN'93^{!s=̾@E(o%6z¾Po`-6!wp#k0ܡ d : ɝSt![PG)CWه=>}1_ҽ8Mwe` t ]! {ѕH]ԱHۃ8$' |8#;*F p!| o7*!寓H_OgOH?iaXX }>\aX8yOjNsX+gΓ h4]q1{?M35ME("UaXXRmaٌm4鶧%rRH(Br'K{Z"aKE~wQ~L}~sduF1K6Ma C?)ҵ 3#5MKr9|! 7%c|;Kg9 +b')vO֨ ]F=xÃo7&˫ǜ=524ڙ<dd˟ lKw\vrvͪάfaւצh5#^d̹X&g072.*(7d^˽xgVGcUn3贺GK4Yu5kdOG#@cA4q dKA,qxkݶIKC]60^G)_K[hMh؉GnA>9ƍM8pr`&=?9@C:ba71͎hr܋Nxt Ϝ<D:<M?%[Q [Tnj9aTz]tB@T,mHIPJÁ!kڜo!hF? .e8e,_8)Alw,Q,WtLVqTeq!s|YAd$d2E\"%xf fɜp+7 Q&W\yZ|QyH]T(ߥyH#.hOHio/oW.ɑx4$b¾h{ Z"-Η: ++( qR HH +C"B!ECh)RĹ,~E^1يj݊G63] +E3gýy_7p1&t CHX`*aX{9۵Cӵw --zM?-RHFb9s',7{ k_ jEqL*QDȐ/"\='"JLg*JҕҔ{襄$+/ڡg<(Iv6%i3-&fsb˟{N5j8 %Aj:#ݐ}Q˰'j%vG +2To4vEFnuUYUڪ0xMGoS$0Ib$iEsx poP<ͯ!(F~rc!' ٱRdƮ@z*AZR36F#91%I1udm5]l]9dCC MOL{} Isیr xϽg +8=ߌE"'n!#>i8w3*3 e #8XVc[LDDfFEAcA1ƨd7 JFQΛG w9wܛi+NLRXg(e:e]r⽵]|+Vt#$dQ0ȿA-_lܽ&?Ipߡ,ƒX\p|GTX/[P+LV&7L~b2J9Dv{q`?Vvvɕ@ wCj OC1X95Ƭ 0f)k1&lgRIm!>݅ig:^5Fx:ՙ栊`zƝF#1bEL8.MKQw \ƢOjFBpQI9-}(:{̻ }UG` s+ls7_Cbd/W`j_ρɛe0gF +FcӉR멟B}P-3ꀉ?nW\2a K ?_w2xIdC&1OtAAqEQ?uW }cwg=y]9 ǔXĉ&nnHC8pF(DQ'Daփb%wM|g!$$hI +,!|MfߑzMڤ& +#ZM-m { Qwg܇!KDH^L]ڜkeb F<MF +4w댐SBѝ5yhHRI6 +=(JZQWxj)jLqO=@yO}S嬥x'3163dp?w ֬jܜUfm_;q-W>OK AqnKCprr-@&C 4doALƋx:EܵԸ1?Wr\\hǗj0Ë[q~q\1Y2SK1 yz(oXshiپ ~MGz^NӐxd6Kq%4cda ++^p7N7؊vYы D puGH= S}@tIA~,gvt\ _/#"q>rF,_ÑE\m@ S1۱? b/z.'bW,A's 1}Hd^w19W&a8zƆx\$t38ox{וw{\IhBwBK)?mW9 3?<*!n_`xD^7 G!8~5$ʱ7Q jlТ;EW;TTVAbpb@ؒ9kQc +¯W^+kbW,HlGwJ2vБʠ=ՀZv6[Ьl6*w3:u/ĥFI[_$7*rsPϫfUY`6[RɴHmno 3(`Fd]_3OoC$!BOH0iygc4wSdN#Df1j"QYMiҪjyvm!R[&im"vB$5kweF羥Q?NNG8-h"?$xFm-jqگ?QxM!g>!pl©O]J}&lzgNXnM* -5tp~bg~E[~ቌ5"aD Ƣi:VQڇ4Zul3ipCual4*Xٍ*0|#[,,cE디[El-(:g(gk%xB߁ڿ.h<\* PXKP)`6F)pEb$b9>>_wO}~iWR G$"%y:< },ApU:βߣ1fS$5(5m@IbSL^PP`.͕-՚[}2|7|7Hcz)1I=yN-:#Tu4sil5e4s [G(|e!8K8-1`rZ7`UAoexV_ky6Hr-R'vRi.˰~a}a!ȓƛw=,Zwizn7L?g"\8B# &9rBg8ҝ=MiUWο{+/$J;SD<{2)ڃC">`6)Dі0L&W0 +b^,w +2ݙpAU6՚iNajm([\{TD~(Qj"dTAQz98޵Y>)S䖶.BW-4g 2 g1q; ]U[ttu;pe|3kM17#vk]oX9L;|˛oZI7H}S| 9zLOI~ >}/u5Xo-Ub^ُ?W #O?㩘U/M +51IM(ՊQc$Ծ:ܣ|J8_k`}:U@оѾ茦4:g&dc(Fy7M(1VemG>,rp +v`~*x#j0]pI}~c"3%%a%_ɍdVvJL8I>R)}WjU]-t^ 6#!mQvbͲ PɃ@m{[QEiUΙ(1^LbJ6[ HY#O4M%4Ps^~Q³N^m媷Gλ7y#BlP,ߊSDbtIG2ɾ*%O_Ոߡ'}T;iGr*c@(mX*Vx7oxfm,~ӦR{mMyfz'&yJ6m)~qwe)t4A>E|ui}KztyA.:^juFUK$/*Zf~ꬸn*-Βߥd[lQelQFz:&4tKZ5T[V[CX%q ևc}ޣK6OgeTP%/([sT.~Pe<Nj>*Eܷ_F7uXmMtε>'K};frޱ(w5pj☓h@@l6((g!V;SF@;pwLn8ƕAK8\CsffJxX%r ]r8:$E/Z}C"w-(v( 3phU.U׉9A/8%'WrlFmc g293]Dy261o90F_-(-I +WS<:R?ʂʑVEyrjL(Τp\$yr|Cwdn$w~{HKe)F1?%5 ōU W ^ZFqCDžq)cȟ@iAƤ,%5+RVs {IɾX$grkĆTĞ`YďP$_/dy:*uτrvГ@gr< +'-4YI–읲)ۈ?= j7=UkoXc+~W! $!wr%W $- Qn +)-:n^Vj3OO=ںvS/O~MԍV1Y<$ŷs+pWs3S޸LfbgpLF݅{%(!1]bE[%ؗ[fiM$NAvaلC^\9sq;a w(ȝCy'ųpTKqP]hК}y4癐w1 A)Pgl\ƔyժQJ^X VrP<Aa =o$ݷ${i̍P;.V,ArZ[O**)T+07RTLTVjzx0B;!(׾'i?5a)pν:`Λrʽ +`rk`|t_@fРFV1:3:'*~DLXg+ ͼ4çk>ínP(VSWN\Ӑ4dθ5,Č"TPiT#l,DELT͖Rso>&DŽŖSB|c&ηH+Yw'.(=:FZc!}QZ o=+@֒c.L%-|cIP:"ԗ^J>&OXcsɇ -`%@O1~ A߈?G/{޲ klEQ րA7cUP3OO +/ T[UK*ȱL]80iw(( X+a o)cDCD}D ]mRD*dn^~$?/z//'p,1ӹI>Y`vGhBҷ!X0< eBu<x.5J(j ȯ#Y" i.FR;Ȋk/ć柌(A}M;0!lC;aK~ +q WB޸,H{ Iʱ78iɍM!ڔ@Ns;aWt;#CVÃLR FԋTl'Zg"m>ӰgvFNfd®ٮ=wa[G)vVcsg 6uql| )5qX?tGR;TKH-@yjt@nn!ڛͽ˰w 6ml>߃WXٟįz|2;Xayi@?j@QuCg?rc3/B,LG௰lp52`P^a p^8@X|jißa 't$?HnWst&ݽGmIWIEڹxn <3/S_X6ĸw KL.- ܠfH~Er]5itqg8طjlEkQZ2" $^In6$Bd! B,ڥLTfDZ Z[Qwi{9{>}}~$D{b>gDH_~K{KK^o/ VOPc5pAM7@ +mqGE^ +B V)9,ߣ!J_?u<]ݦ +tFW7O▝4p)WLjP~4~d\J&߃qF(dY~vKZ(Bax"[~m6׀pҌBMP2^~c QtD1X"PDHbX*V<')dqvZ&^ Ywmqhm]2@|$ ?aDH]"\-b~a4?><]NJ4e/U+&Zvt}p"E3,ӥ-O~#@>)Q8O~.f|̗RZ'6qU~|\s}y:T^bC **͔Gc[dqe- wh/[xkSɑvW8:FyG °'t&Ff(dx)k!N(@x{QnWkbp; Evrt>cG6}d㓑)9QɬQX3zEװjV +R˝/3K rŒZ^Ri^rXhAn|:z[Gnf@ֺX)rbK,X9n,we*6^F,EwvF;n/1r _=ΫW:kG(s~.lvG](Ix? +&|B8&ĐOG,!g +M,!k'^Șt~bSҼ &ھB9x\Yv[&}ԭ&tس7ER5^ɟ4I,dew$cN kr*Xu 0~UI} |JMn1Z;;SCsImY=>I+}B pNFxOI!!y ("%pɁI +:mJ 6%>fvZp8'4zlW0.E~-YߍX0쀱dz ЗAZI bnP)$ĐU̱lfeiSxuL#B bkQc * γSf]FA=K;Hf,qfe<oZIL%b#q LM 4f[o]ЍrSlI즇>d _TU;_{j眢s]âh} 9M7!ZGbu%F|X 3f>,fV0ݶ4-.&+舫vQHAD_jp[yl {V~qhC23?'smH} m|"6ʌ(GdZT +1QDG/#*."s.<), ȗY80UyޫHh3#̤E&%;<,|7Ke8ZN30$dK(KHv{)QHAXJ9hdP`.s9c,cɱΈ|qɜ?>o==_&7Y뿔eA, fI@"fQ`iA>r3|xſu.Of;"YW<@BL@HƱ,ȝ3X, + `AH$C ]Ǽmx1 ;av4; ^!o::Tǔ?7PFlzlFxh úoY>cY1̏O#Μ̎J+j3 }4=xF3nhNςR}oAHCąCZ(+|;dyO13 +ؙ̉x0+v3V0}E,qxmbj\.SV ++19Wn9:ʕ"? gGC +բMŒ8 毲c^s{̄aHg LKE+yq[iLXIKkC\jk|4Wwi ٶR됨+IsjX&LZӌHԵ}q_;#2)Oɸ\S251N]˺F+fĺ HfZa0<*DkPҙd] +ħʊBG/-SƦgt\\6i>a̐8g✑C8e)>NiphഡZg&LoFØ4Arh&Fl_lkːLg9s5YIyۣlzgoW>O./qfPG?GUr/t ,(6#Kg6 ט8`'|ۊ>'C?cLz,ġ nt-Lsa ++Tp5td[~.3{IFm͟ɻ`T8@ t>ܘEv/}'+KAؕ`[F/ZYJ#4/MŦtGaUz4+#oFVX+w [)9, tZ& hE=4MJ'W)7L9BRUPշxx*Ja(fbK/Т>: Ρz)蒚# !xEzhs]S׊ߓ \+>jPn1~ڞ3`qM\ ԐjH1QcvW =5B~@ZUօxO/{RDqOWTrk؞A(~Ba!^>^>jZ[[rgiAʹ"wo}- +-+x*OmsxРfPj10D42c^tw'bX DI!lWJ-Yb'/)CpZgX~_K<𛻍w!>Cb-/^5r+Mo/+>pDr.@ vOw~u[W%n>7wK(\{E(r%ʕ,׊-Clo|w[2V|_]钸(F4߭MO 2"W\+jRZx6TۥݧU(繪;Ϸ*]RTFkzzmHz4p0c^-T +kb[%W\g|[8Gg*4Snv׻KY57(*eê!Fs+6x٤'O-|s܃+sh󖁜kΙb88SP+ʛfp*ֹYI48Ъ} F3 mkքNhgM]ʝf\.kFUTpn)Q6b9.dsf{ +;#mvwyή:E[gt6Q۹![q8۾/xWvJI7/w_A_/eyOIfN64Q/Tr ."..,r- rʱB V$N66LӦMMLڤimlg0|rzS!>d 'BG9G/07^cn{lA&0qꯑF@x֊oqm +^ 8mJtŰ"Nq"ƱnpdK3v0m#l 09Q0WDdDCBїkf&X Z؎}@N12É?c0OMEpwKpAo/DZq%{~E?bjopBL'$kO62\tOib$Pj~f`}K*~BwJNw%xgȳ[ѿ!몌sm\\fv2He4MF L7z2['k=Yљ>+?U- C0TFxɜ2_/RO1Ѭ(I )3اT1җc'Bwn:w{=w/m1ZUGhQś:MyӠ;񨖸RޯfJ֑sFlzɜ9{êSӯVУa<=<y67Мߎ&4sS#괿\24Apkod1{?Տ2pz5tiRd^B#B Z ZExz+¥;Sk/[X$,:V ERɠ㲖a hW]hڊi)˥Q^ǣ3S­UҌӰ0i籗BU;T~x!%>^>x]jpItO$Nwet+YO!!AېPXFMR'&eT4Ob3/`-LEXt)lXt;/H[;|D,`@C>AKZM[pRp1qF+,fijQQu&ۋޢgJ+7>ܐ{UjpNBCY4?Fe01T[[3bjJc1Wz0UQVO} EJ7?BWo!Hq}.dc6ɽUrB*jVRm߈ݾ=CšܑɡDiu%7>t΃)r]@)?qBSDS.}RSZ2SzQOh&v2l,k0զPZĭE6s[)sRFO0yQ_G幉_։Su/ I/X1ɠV*뾆qL 16Q14M96i*!B\oGiE|7ߓ'MA2xSegr7$Erg+4y f},?G@G+S **~ZZhǖs쐸=o6kSrUw=[oIv8qK!؍`wq:lIfyd[DzelHmƆoXFHnUM 2Wycna;XrΎZF.hMV2 %HҚdR[LcC٬oMJxֶJ"FVNR?O]}ɒbq \{R  9-u{2Hkۗ`ֵkۏ&Vw%C+;FB;-ci:ge/q]Nωq0`H}Rn]wRHGٺnkEfugc8!pBp9%$t cqHGb{&)螹yיDGzbVi2$njtC~G],hǺ-Xԉ={ܗg{ "PIl`bzO!%.Q>+,D2 +p7oQyzlW{E~dNJ$4!O;v'͕X7/b2=yÈ;9}'2#HY췀~LW8k^G-^>,-ڟw9Uw-*u\MHrfG'Ӂh^`5#fzP$l@4S$d,B|1ɯ~F*i~HO@zW_quВy:1ljH7fz3ןp |7)~!L8sĀddxq c`t7j~8ޥ{Wzr0P~$ϖ͈oO7Ih/0I8 Rb"tcW)(ř=kV_L,!e*2.C|E*,vB +l)[*(>8%I}/ſ!Vz'^%̢%%I^XYdN:Yqe VxAa1 +ORHB(ȶ\uX'ದizV.zrފy`$(lFA1 =da2$)"i’|4UdfmiO "kZʻ:VsK+?C~ɭ\R:FT^;Ș"S\gNTwzjffP@ZEijZ/=[mjdlw$;J56s3>$^bx'CZ^`D˘ Q|2B3 Pᚖ)əS(Mʊw] 35qa&,,}Hٟ=6<{ǨA@*JŌf94>oK0v_|ϒE^9c3QojBt"\467CrWQy䚷_#.jd42F.q<=WyW)#Y|zO3L7 zȣјBW.tE^U/עYr)Ј"oHfh +9Wk=T'\xnz`&xV1{.<@rf/Ҙ?H.%Nzв!ReT歁S4;j@EvQϾj$7C:70#p8q.' .&q_c p9kds8lb ^Q{\ܖҬZiGCRǣxOh?\FP>)]b)H_Ǘ]9}^eOϛ(9tܱهGaR?çqW`X[\nǿC;4= +p|jV/y!LgjݛzVjs?n^+@)uJ8 G ~pexO\ 8D^n [0BYD?(_OLOTZ0>F=Tcúfїv?y&|7-8 -B J5W6\=P^knk=͸p5)Cu}Me"r}. !0\ c ypZ@f)m)5-qmSOa>WO'sgL ue?~M\'G?A"qŒS29ij<7 q|r\kqmµi'plZhK<ѫPC-qP}&.'"q8i8B\+qUk z4t=܁~B=ybxVLw$p\fS9`c 8,qp],>/ch${BښIΗg`cܜ+Wh-N_/)28p`#\adww!8ʗ:;>`_+OI/RynxMr>0)[j )f61f3״3al_x/q_ѽ3"'%Oe +gVB[hzD$w]&CvE# +G;[Bz6x0LWQ<ϱEG;)[ - +-uZyv_wfhiYq Ʀ1PUӫ0-8+]qVj2|?3ļ&9PYY ћִh:7ёF[ +wF>F6+4*2.G>jTU/QM}*WX +`"7^M:ϙ#9})O@ z d#+X٬ĥTWSS&{Nչ}4{27(~%K|1k&X/$?,ϊ=#wixTЖ06n!7zM:M>v[^UZV6* +z(/L7YR?Ĥ:$ܗsxKj +.IE'pK394i>?ڂ8 +hR+\oRXM.Lƣ]X*0=Z|Sdu Ta=}2oyܤSGavCUX* IX9:JLU`*"1N +K/=6_9ڒ57"J s-ki),AMlT°6`.I$S|J0-6+$2rhoSkX9ΕZXg$ +V[C=h5iX``DQT<,y* WVȳȵvS5jdni%ʪ~+PVNrOjGao]ScWa +Bo GgH~u" 49u%VkɬiCY%v?i R7HqKrI#>뒃 bF`HrK-jgs,@[ M](9ѨɮO%әҩ%YD n\=$uk(FE7(xMrpE}V)wIjE%Eg$ +b:S"ڣ%S:Ok:YӵЮ1^fu[Y5>LrMlysQ= ^B]6BBtD-"o9}yY'ԛFW~=Aewd"~w?;e9Ӳck@aŢd4 +*!Q;!d׏X)=Krװd8xyTwXpH0X*#"[!PDPBP8D((nEM4Q%eZ8[51q q r}{g<0D݋&[Q%S2u(.Eq̊OOE7e^,Vc'_#p5:6SpY# *I=7SR3u(.sS_IL#֦ji853id$70¿)U\G' +]snlqEY!Y/ڂf(>f dBO0 |Ax|b>ŋͦ#u<3@SH|X񶧔M~wPҗ\Q:cppxax*5 Ȯ_vB^{><<{g'>fsXFm|.w+8_e +׹$ptg/=`ߥ1K>'ɢΩ= }q;kbGZ׽=R .w-߼K2v:Bo ` +(\q,g9z}\&\xv9ngOΓe4n z-8w 0+O< !R,mQBZ]ej+=# /_!%}Jf ʠ_`iýWMm}=/N|8L4xOoXqNYDGZvڸL|«t+Eb8 9XD lFԮ?JG/'iœ'EXǧ1U-}[}|cEwPiKDpw~Vl0_|cq4σk"]?i.D\)JKE9 +U{RXzlhzQWI ޢV2T~N2`D+=R' ܽutv4ҶUj&VmU6Uۥ}6׺WתĬR +;\T~L+ʅF}UNlَ9nږ]kkwUtѦv]#,tI*ꑦZ3G+-gYjQe:ejI'l}8z8(tM-dz}TI QaPV3g=[闬~ ieoka[?EOkf޳3 +)QsYv~nةRiM?WA7|sr킔=`Sp-1Z<(ARhp Y!%JT%;|7 s|#r͚oG1G~)eXcAz+oeS:icҝ&h$-p4h:*eh-aYJ^rŻUIź]f(Fs^ @ݸsrΌ킟;W2*c]ܴ[*5Dv |)JrsOT[iLsG(ƣL=wi Ey]Le3óg[S3xs-`-.ĭ߲ւ1\JU{WD9^ъYީ^Q) EtH5h [F^_ƾKXyeG3-ԑmDo'ŏrWjOf*wfF*of'+PxzM ڦAi),PX`>nF-P٬%+5Ub]4+SQ~ 8Iӂ">:FSG'iʘtM + +[А#z'+MV>C0·񯂑gxf췥e%-PJ +lR]&PUi MS̤X` +"e" ,^A TtLM5s\ʥqfڜ42͎w~=e;?=~EeQZR 8Cb IWthBg)"l^0>U aO0k(=k67p%_:DJ%<:8Z!J %sh%Q|hŅy+&OEG(/1USYȹ2DTpFEU@UO;G/{35\Wgu" &;*1L(6r#G*2Sƨq + +RXQq2LTpLbW.`x4tZ+V>$p4{k3߅-#I]$c).G +U@|KT*k /nf> 3gy&Z^z`;ZuhWs]N/ыlf"BtB':*4hvU@(MH_M4wb|&&;%C^):G5&FviT }#{rKƑ@z؊Zl2Lsf&9)0Ӻj|z_f'c3+@ydeTehb Ta4$A\'$,\3p4RzDwh4i6Jim5<~=U-K#r4<'DCsc4$7Ys'kP^\h@^crZιw/Sx?eB{i?\.sS&Ě6siG$tMUxk1MDoڎazO 18Qd'4Ocy^%)?ʥ(E.]K/fJљ"}u{AtsZB;NŎ1Q8M6~o%T'> o> +SnFe%e1:ߙGCEcJV:}Tvû ۢ/!x +񓎟l<])FT5Щa'{Y̷2Vu3JJ+jqZ|l^&ttEg&gUSoӠZo%]jUOmk}gL=8:/]Xm3V.Z3*ʩ@Z+ѪAc;fO;Zblshi>z>T uŧ3CoYeZ2ˌVZ٪j܃,0K4 hE  ;MXXXXX {5rYr#Y1F52oVڪuSg۱c1nv;=>^>ɩXы81|'x|:3bG)?lo~|7k yif +^Btc Wjrf뜜ş5o7pQx6af%PL1w|h9(~~m5m/]Qxi}m瞍}l(XP G*TgS3wv063+=Pt#,V[@ʋpw`'PdOVIA|G!yr,u䖶a.%|cd:'s?ctc'^=WY/*IyY:͢U.QG˂G_ՙi$ŒRL5dVQՃzG0Ԝ% +5&#}j)%^%zZ=x\뿠ا%;5k/)ܡguʫj9UU[ sM4Yj dԚHbC}Hm8HkD\o|s}I\G|5WG{]~GdB.yQ|k5&W!"KZS*Im!' - ķv:LL,mQm"Q-뼠~_ T=^͂ .vDҾ$I|gqvuq[v]KdW;ۻbk"[!%@H [:ԃUŝ58x@0$%[W Mݻ} '?d a@!4s~' j!&R3aN[͝/ߑzi6BC^2k+չ10}D<tsSĢCG!u׹p`q:~n s ZvN ա :?qrs#iAs,?Grs9dc/}6Zo`u> Wѷoˆ]/^='C3V10ًuWvĎlj=mBuIa֟<'Cgt;3bцovO (x0H}즅0ar$-s,"ETs&Je;L8:XgW|~XX ʦG!b(e__%+5ev̧ G)%]71 ÉIO?D=X%a*jc-Ţ%,-_zB֬ mᶝ_ujzIx&XXIR!dW6>O"5'\9X8̿I4go&U`4wv8k `;S7"/kyآM!U ~yٚ㘫M 4$U6fiz%*oQ-ה tKZ?Qqkk&"[ .+ oLk|9۱V;8kyZK [׼V!Tzf.UdM9SsJۖ n_Y C +;S w}|8 +- +`Ὡ>^-Tԫ}j\o/A},de S__EJѠJ +D _ ;wfY3V`KIf ߦu֘~ӯF,e(?BCb!iJ1 `X +74p%T).d,ƣ5~=dUL3f &v*ƍhO9` +lQNapQ]AJ Ӑ %ipY$(1$EJ0Q\h,a>Okd6mS"".+tY)ܪCsc 7|-E? it+J3vQJh %(>,RqYLkJWt(# Y92W)̼UчehYe89fj1xU"{L R]gRLI%b".9FQ$ED)&[1 +-2K,+EAq+0Kݖ! kP{{`lW0~Oxy2[RZ1.X/E*" %TqfDv@U]q(h"" &)ŌȎ pdWɅ +*S9%Bd'rjs;9f:ΝGө.{{ ҴDMP +ДZME}Pџk +_DYރ-djߋ;[ReVd_Q^ +UM'LUHS +ДxMMS`l&ŕibJ=_/6O8 g4.tW,Y'70ї2ؓiFƓl%clg8MQz"a&'((!X8Kb4!)Ess4nn$/y|SZS>蔯5:|ZK-zx8Zbʔx Wi)+` q4&-\~iQM+,yg52cFdl_QyY7ZFƐu #*sFpl +Ʀ "z0R>16LԨ`ȞfG3'^9[!Kk֠r+50\sn-"lvĸ ?©g[$ՔJfy|NYH0l +,`*ш>*pGzkh8 . +{T˵(Z.\~J5Yݲ7}E9ߔcENJ_PN-,H%(P]Аr)DnWG+|ԧ_NSPHRJzT2T2[U6 wlDʹ[Ak˩B*wjU-M$䋆 r-[S6;7pMvD1LxѴ!ƚX-tO^* X +4y+s\+٘l+\~CL7"9 d3yɼ11] |7|ezj} 419ɩ._Ԉ)xtEn"M;RGj~Y߸#M'ymTΈڧ6"z'qm%ģu;ڸqh''4vN?nm +N JX `J ny5ޠ>}\B4.ꢋXt慮c캪>DC/GC :a."J^Wѵu.7D6Grrb n'sVOdc;`&(%ty]mU&[T7g)7$k/ǬuU.,:>PvD(f8hXdL00t ;pVW2%6&s)ϙ\?#y9>:ÿ3EO!թ^b?.sׅz 2 +p)$i0 0_pJTY:8[X:Ʒ[B,C܆Z ׭|A;`pCͅYp]%Tb6 +o USmlw>-u\F *" +bm9n8^C|n瘵WgUgU즫wGjC;&1UNY*@%}K2[Uܪ|p\9.= eY~cwEk&uWjziY??-vP`t5R ]cU6Wn*AuR2 ٤̡-?SÎ(u9,JsZ~tsчߗ{hULytņTE 2I2Y&d6wHd5(T5J(XզI*7$yʖrSLf('5W"e9dIoTFz3VɜR-}J<#S֯gB.獼{ ͧ?wa 1cۘqF 3m-!ϑ5}3dHRfYJd*WjVRʔR hݡܧe=+COrVαh-d+g0]IJꉥ4G*5WYفWzN9 J(9"Sn*1V Kd,蒡1*6hK] =w u\:=ƾ|.z 1]1뒝?Id.P +#ThPͤ[reXTj*xnͷoWD!ܒo +pGe Q c=P#r +3e*VO J"$V)5+Ŋ,[yE7+Sت`AV)7Z/Zq1؊4cĔM1>dT(r"B5*RUUZה)ANͪ[6kf~jzeMu[ GyH[:ǝF6` J)L8^a +iUpcM o2kfU~vhOR=غJnҏ[hJI\Wy7{t6jaih ;`{} xb tf`Mkq$:lR= 81PN i k\GC;}%g{b-w}5 9]5k&5HESuQ.HEb.^>1s. 3u;zۏ޶n|L}5^z7nwČ6x7Ѹ)27 &1vcນ0{y[~Bo!MƍRز$\O$W8`yf}~ςæC顰{XЃĔ }3A})C6z@4qg4gqGq~?ͷOg-)~ZwR;?k/uKų;H8Ji.i:)Lރ>`I4G94d}9H!jcCx(ya?GvS*hlMӂN^Rњt-J +02T8[|̰VѺqLKmó3ѧ8[z ryMs}S{r@0DdyPǿ4h@Z]sY`YaDAQPh 5;Qc4Z M8c&11ii;M;mMmI&M?. y}7s#f/>W .0V& i=psTw/3Qi?A }#O iiCéѦ!>>|,w(,9ݦ A&G3Œ $xY,N%eHXBVXM& z8}c(9gN FvB+1u|qQ|njpA"J 4K\VW5FXI}8.뱒u +`w/wye3S=\7W#׳+zc hxFX`YaٵΥZ3V<֥a~J+fZ^Sq%u99ǩ_Ҋ܁=ǕȜqM1sX3~Ana< yXӮ95xI˼ZݣyꙿV Yuc+j]Z~f58ԏZnJr?-HzK>fu׈kO|Sla.,P_z+_΀utʱ_8hw1CΩ.jB9U]_\+g->Gh35?_%AFu#GEm6-RsXjU}JFWuNUEWEK@ee3|,[#:Ujp ցbt0m8-NMYA橮pG-2N$A=Z2IapSGͱjV]\jMREBJUX%I%I*NZ䵲lSAMk=eBNeT#mh!%u?A=|誆DoTX%'49]%)XUZ.^fӖ*/mD9[W'yUiYwe̙_(-9 Ip?f-vͼ_tFrRePilUdNT٤l[QZeg*3WJ,sWJ%彋JuM\$#}y; +:rRd*?+RYqNQVv2s[jZd_UJ.(e,_tYEߣgqNi"sx_d<;TI]Ù@;ZK/^ZRdwU~r +2)0W)*e6)ڥ+ C.EQUE(;E]NEKyr:걑z_ +>/S U% ^2J+8H)%QJ*IԤl,e(kPCESX +8ʋ +UVA!˝BΒoцQ -R7kO9ϒ|*W&CA1IPdU«l +UHMjvT;Ww@uȧO[%b{z쒶Ђe4ZI=v0w'E1uUx}Bܐ5(X~UmZͽ<"'5es}}se̋\3:bf6fB6%=[!R(dkS .pw:T<{ji:/0 8 . CB.,4{m6 -}YXsGZ~l6½mb.ԓ6ҏ$z' ЋY˿ EorX|4bR@VJP֝Q"lqRU @)iR@6h~%kq<6ral$چvj"mIk@@"n]¹Ks4s.vE3| { v uEM4M{%~(2d^lO/;\rH)w~1r +uzŰ8A2lc1p mGE_ zVUWv35߸Ò/+K%N5@!JX9y~\Res!p&A@MtZ[YpiM?q|!@΃ 2:һ䆻X~E݆F?:x+]oq=ZIzpBCp|%^/03ЫiGcq5oaD dEM[c#}7h*L_l`qX>ZK\G&x\1xg誧hJ'<,a دF=Z u?Hޤy~Zѐo^'.LJg"3Ђp“[ܛ'/~%TQ ZJQ5P;ѮuAќC![a9^]?>| < 1- p%q3# 9~.yUjt?iTڭXhV<8Guӏah?tO F)U>Zm"@"$xNkdGOEp,1Jg)-!};5j/q?u2hit6cvr&NW g|atֲ Z'IK䜮Y*rQK +߬,I9h#;䜲\Sϕtv-~ݛp5\3 ^8c~E.ZKU<РŃZ IRk \q[yE;\ QܥY-J8 4)U𷲒65u_mT0-9^F&*c5C29_G(ݫLi+4{R4*eM=)RߧJgg7YQ}\d eėR䀧r=5he{(;L4gLfLROftJSIS4%p&ݡAtR CN;u'(=s` l}1Y ٱ4w2Fj4# B1J ichjL%ejrp&+1Vm54+.cUEG| nI}C;'QnToed"GY!cjUQ}BcŒwn|s ѝ:܂$8:UVKz9ap4  AبDŌ&cte,Pd2*(AP H,* * E\RB+ +b$EHg:I'MRcb54xjcJd,^ mx`c4btbdi%hhb)*N"R~ԋr."px~dݪs,i/cdHP\BF$F*6qbFhXh MNUtIQy:UcaRx:OۡK? |Th +MAr> hdߌT̞ͧ'g{b>aƾb W1JO 4"ӓ5px3"N~ZdjWz>M.C|{wwTs.ːIS' Re$DgxkPF"L +7Eji2tVS +̞>OWNrc+oQy+|r7]\a~l*LLg6Y*^D'p|uShnOU +3q'//S>,T&O|[VR/ KdnF +߿UI߫NB둆+\s9=U܌OF1/d/Rς>-'HyGr+LR +" +)Pb6"کo%y}|BiOI?*ZjCrYVjeX9dVⰕ`fT:FYWVt7)}j IFwP$oich Eyy\64orq6ƱL6aV 4"V po@ԬL9+RhB&O A,c)*V5pT1$W|F+VlHKؖ=.ĸz^e?rk8 `σ,Q$I.jK?r&5ԤW&4s5z3hְ>V}fbmrm썜 E~Q  $C ^?a ;N=k=Uh&]ƭ"zHCl4N4= +wBD]˶3B_-Cl=tMly`{(fLİThAxubiOhMC$$#sO-<$Ms_^mur:xKv!x(pC# +cQb>99μ88A':y +|:8Wȇs#gp^uرcNAE ai;N>9ef|LJpJ+[L&x F=w@q,s |0CRk, ߘM-6w^֛?00rPEurps tqeUC}Ihuw>D}Xg-p̩y> Dߡ)ݖ <]E~2ƾ0~?QCp`Ϣ +OOG{8|μ 8#+hOsba6md!pLմzc eOD=&8* ;p\_'a7,xdc#(q?^Ïq@upl z>pE>mſ&oDC,[*E3'ccZʱ}ͽh6xa<uCmLͼK۩ԳAte!g˟D?- + 38 +z k<̇g p D2`&< < ZOi{xrHet})'icޕZ&Ɖ`@lQb=$2•W6\yT-Kgp-Z2n{i:tJ>]t @o F؇Q_ճRDZ:h~BlĖDlFbˀ/> |E*rGVVBUN5q@/wy:r2=GSTXUUiPEpBTRPA\G,UneGu++j2_R5bB`Xg/? ݛbvr>N>ev,"J RudU,ܨ"RU%[MyQeʍUQVӣʌYUmTj.%Qb`>S)QT>6yiԤ4JTd *2i*}yS[xjU9ZsRhUrR*!|k1vs,r;؎I8S4a^9f&!v6c .c]aTO$c$pulG4`;6@C;ܓ9 u4_s&mmu +r>iTآB> 8x |>wpR 'p4&s2N8Ṋ:҂!j}~]y+W&|jj4wfj#9po89x܇jhm4qoE⭖7m]ƾS\O2yE?0#\;ɣ9dN2'klk鋵Ԣ|tx@_9Fӡ\V_g]ij ;o#%I3D?"ztSnffS沉=$i'1ˍ( /K6j+iX=Ă@~⒄ ?z'D<\.ֈpԽE@Kb ;R5 6<^.}Y#~RCAd`Mhu:Ho2уd>Ph@;Kwm>=|<ŸRb|8qcP#RO^q<>M-^ Wmk%\gƹ9#>7njBaBHg [!"Y+ ޥ?//pI/u{Mp^nJ"y:z&=M;u,yx?|9 p}(3|J7Q5%LO+iZ ʹvxhhh2g5l/p9|%8N랣o(bQM7shp,fLxg=<[h^x o#g>|f{56n . R6x3^iYЩ8 O#<ӡ11E|~~[cܲFn؆!<0 x/k%2/Ѣ3tR&reG/'z p .K h]$j.c;N~T37ϖZ ]V}Ra3ے̌ s݊^vpj:C]E'V܎G +zU?Ռ D(;m8LS/V#̀/+͉n.ko| 7x=/ mAc7.hU?Ŗ2OeODES/V#'&5)[ !:&wΨfu9o2=G<#9j6#]8_O'h^ZYƏ㌆S3,C]hu',5i]˵%F-lQmTwj^UL{ZTSQC>_jXջp|v#3,q'kDktbTVmlcKURq.7aЫ҄T8ylו}Y)r.#[/3_/&)bEad.kJ CH0ˑ+{UeIvnVqrF +=OVnYN*+ݫLӫ2U?D&Ӫqms\`=8d3hҖ|arcTfLTIJ)Y*JWAj*VETFLYǕ[Jڎ|[ˤZd6Y'%V$sdVZnRl2W+QIJ,쓡hTqV*LE/^RT O*0C`["cb=ug]ܽXy4"K5v)E5X(d,2*(CI<%P\--hCYm{iQT㮂?Q +Mmt\Fp^n&'XD+ 3(ېyBJ+Q$khêhG" +hUXeB*w+ά N: 9x` k-&ԦC.#S=U g~2Cj"S:K5 ++VPNbuX,ne׏ӂ3<W;se3ϰ!Fy:9#ri8Òa hczj{e&/GѬxylȒ,`[w4vbEuh]sA {apimbY2gH/s58.wzd:@=̊R:P hxC[ bA +  zvs騟em#9D,sOirry)'%"S8|ύ cQndУyFOQȍ,{XF>R7pՕ)98K= >yrgCd Q13 34|2MM4Fi.>Ą682Gƽ," 3-S$=(<ɡQo xs +uOu<~8dO^a]R^/w&7.p]daR%\ +Yհ~#y4sTgmgG`lft6 +aAR^##Ck{^Sxgad0jh N'@^\&AX//wWSعd:Mw7 +^`0cK+E>SM>28md VXzU`lnXU{_';;Qp7GF.6ȀK8TSLr +V'X ZV?k0;@V7hnXo߃ka;1@-+ h0X1 ,'ɮ@) V-X8-i>Hs'a+U8;w>Qh\A_#& ?3VXN\`&:7vzء%`k6h*@w3)j)LgrpqFmØDAk0YgU &QG!9.Pȭko6xݰk?f yJ9%fEN! =A w| ~ +V%?+xv1|\WM^ZLJbۮBC*0S C ZuO}~B}I >ȑ.4/j6z&gW_* 4P2CJjUؤIAmܣe r¶)+2)=lO qxs<܍ڒA$egpՇ*(\A*SIHB5)4Ga*+3ZS=n)#jV+Re~Kה`RiXIt)|װH;s9x ^W g.RiR |KjҸM|ҤOUNT&80)#RebS,ZeNM)-WF[nEOUDF3(Ա\ +#CzTYwdp6!wH KWJ!`9$$I rN#STEm=:6ֱRvL? a>}^,!%bd[I"n*+G ĺ[":>z^.^.^ꢗ"{DnfiFZR` X-n`!|O } `aҁt\Wn٠ +dF{H?2Nq"OFc愒!ggxvb<ˢgρkjg_M쭌Ks82]|3S G4·q'1sFRwa2΁~woη oۯI-qEo88WeE<ͯq/P)_qh;0g?6Os>rళPQLe?GFGFgqȧi1 +(Qf=`ÿW\JӠ:ُEQcX"WTJeGֳW3^.8]\bı8تnDTF}Btw9!փ[],WzaEBybGpVN{O8qp JuDVNI +N;iQg8|pj+`?p9qTS'kısi_Vn5RV\LfiJrƗqAȘ̼ 3Õʞl\u9rT^&UpΥƅ|;_%SpٞUo9!^5c5)5oѬh+LgÓHd_0ny J#k TһM#Yryc35FV 0:_"\ipeÕW \*bxZX:b 'rv(>0@-q  +$f8c`HdtgG-f2'8Kc8rVEER%tw/? +'H39.G,'dYt S"|eWHM:ȷnळ9,z4So:1xRr\|  J,yzW?xI>cK/Cḛ쬘 +&s92gXL5JzR)=J2LW|u/ҫӅMYK,u䧆Oe!1RqLVgFx*T,o2|&*wRf+V+.p,tZA/)Q /q|&ʔ/alt`Ce}wtuX$ljs=1ERSI<2h^/ʦsr`\c.>/-Q~룼-LLTsS,w-y K"x %3A=8`JJ!PtGy8;җNk%js{M~ \K"7dgeFӴiV@9r/g!Bc܊N`8?h)-E(" GN*" ȡLIu3tnn:\tfed˶}e!˖Is` eЧ +T}7[=ٌb6Hʊ [|:ztD +H5 +CB>*pWJn/V`vJl% 520b;0\Zsؔzb:\(5úZJ pC>Ί+UbQUJW XUSfPKؕTD> νWRࠕf=&=Z,\ק2;D,&Ť|LT<D8 xJa1 w螀BJ|6+QpO(x709%'lLuQ-L|T ^g2;t3b8|[=lW-KPEǥY$l9vBX"`7~o|pa5 :ל] g5RUG<}6Qkg˓ssN}Ku.zd&^)e 2uY7SK{VǵzB.:3|KH|2nMR`ziXonw֫zW7)u?RZ+EK9l19̳>L˻Ee/k0;oP KlX8ͼlx޻'b2cdF~["eItJ. ۝gmo\3M7C}P,HAQ^>.dSv Ȳ~$8Wk7. !{:i q,s4/z(CapLSb1jKP;#=3x4-_ ggMKhZ:! kuo{>/ ǩMqķd#)hO쏩_v޷mm[ + [U;.0/{r4D=O3K08_[v*24v)1U\Ο0ne´6}vI@=!Z@Qc_4O혯Pl //P?9xֆ )] <$mj4^żۧ]/vvls_ؗd^kLq&;36l*O&}k-oÆCxm7b[gH͛!ױ5s7GK=Om,/|';2sH-{%_xHÙn|8Yi"fi;l),d+=lD?-UU.pz(K9_P0cጋjd%K],S clry~7b^!¾9i$$8՚'a)O3?C3*2q,'VnڜnPS'쵤LhN5v^>npJ)s)!|3FU>NLb;UM"c{U n\߈88bgcFz3ns'-$z ++3UpkåuYԫuJ̈ѩE^t,m;fVEvhh75sk"5hiO <{V{Ib]?=i]]\Nv\9}p)0/5ڴn!uܒh'iFDJG:E(~S@<pT:g埌=Vmzd7<ճpP\KO9,GmӣEh1/:FJ~hV¡]TrW:HnDr7Ⰸ+2>(?}5$r6gzL=Z]GY.% O=."]8nY1BVbА%c !_.ns;FVl)>e8Ŧr7F)ռT~yUu%q (@FV v+'6%BTPM| %b7ꂚhXqu&k[b +;=޽/n|;7w;?3sfjRP 60; 2u Q.ݸZ3xab}K,&?oN.Phj0ȧ5I!878jOfU֝i@s@;2zz(y2{rC{B t +};`_|j?x)B9>xdqM92y!6H/^2{yq+6cAh;4[#dRg?9˚Oy~V?wahK#luGrBQ:rcԱXpKfhu@f&8m1_8ړ;Л>UJ^!.Eg4Ç,=NK=3eaĤԑi!gwI$3o?E.Hgo7 +#Ϣ5`ҷHnb Sp?_8:G$vgIzqdY>on"9H9Ȃٟ>ɡ_qm2ߝ@!mq1:^P5Ҭc:ڬj#c("b# 4N^6+^I m(۷h0Cd(wSZH@$N@JN /(nCoGgFL Mx2LY]|l]9p~ M)IJ=S#L>Rۀfɼs4ŵC*hQ@'nZq_O˧KhӸOȍt$ƒil:/OO!y 6bpE`4[S.r8YMךuʴ0li35Jɀ z1o_Wcg/Rbڟ؍r%´LV?ҿ7.II88,T+Ŋ(Y̫.e˕XrV ZԭݣaMy5o٪u:tӹK7|gpޡa}G8(&vaF=6~w&&&N쀙=L9*9!~I`貤6$ݗ,=4=5Ssu2(ėaV)Q'_e*)/PcA#E};9RK!{]`l9PF ȋd2-ޔD%1 *:fȰQcƽcH},G_M4DM4DMHo. /W]h&<Į[ٳ|@Csްs~ qu~b׮&yr;#R2 _={YHSU6ugc]|@JϨάڱkN^6?au;m}'4)WsYo5riA GTۖν?R,v±ӂ* ;*u]4O8>\hFX%רsĭn* %:@yd׮&}r=Mɱ7*ڡf~DMJL=,^h{En@]lUw2U܌Jf@mʀ"ͯh0#s + Ƞ&%߯=GX8CjLjDnnصd\LcUE)OUS_v\ltǂPA93de1J)V$EJ/.*)I{mTf+}5ު{Ў%>ߪ7/_RQM~dXL͘ӣ:b?6sʢAyH³'yT,-h|^TjvQJI$[v1P0ƾ.mEϿ{eyTZ#lM?n]97)1ҋ5F|}=UU4Zb>>q(bV3'q$Y``j弲(3W"_e+ K/"~F~M@E9kp48dm?dY6˚%e5 PCkJKqݰ8B4MЭJYj{l:u/}ybvzcʐ gXVneo훣_M!/i +=euBT$@g֪⍝6~SҖ5ڞniրV8k@_+@5 +d i<ti]Ni 5ᱱ#z :{3Mi~q-cE(TtR !ʾdO%)K%ɖ*I!"K46K3? 潿\\s\ws_&Sݩ֍ +7=`% Og+10v5ۃ~u`5,[o:?P G$6 `>`jI )c`wMa._!4<"za3GSsrע~ZUmiB~eyZŀ]qlu-iU Z]VlklB)Sd#"6c`nX z/F%kmR|ׇE8t3o^M|ߔ~ㇳA:%%wm#?``{4ȓЎ++IF@` A@C[Z7P$g_{D D8܃6"HF +b)S.LE;^N 7To7ꁵ +cؽ̩e<_!ׯޗwi5/A" vJqt`9l<Ђx+)D`e=e`GC#HS#81D6;ijK$E>|&޳hUׂ +VhgR`8#W6/CpHp6C.:OAoR  I1`Kr:iW`x!(/4 +n23J[LC l*SXDb\7Q]~iX{.EZ#\/j@Fg5@>QK3@s>H1zDP #9 +FH~}ӗ/؛7X4oig̚dgn`cam0/4zH̦ӏw? wəq: -~ms:x˓W7|#; +F( |gO>eSO5wS)Y<+Vla97˷OjXt)NG߲K|xrg2 +?``0`&!; endstream endobj 11 0 obj <> endobj 29 0 obj <> endobj 30 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 17.0 %%AI8_CreatorVersion: 19.0.0 %%For: (youyou) () %%Title: (未标题-1) %%CreationDate: 17/5/11 下午5:14 %%Canvassize: 16383 %%BoundingBox: 215 -357 587 -200 %%HiResBoundingBox: 215.5 -356.9794921875 586.83203125 -200.873972506743 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 13.0 %AI12_BuildNumber: 44 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%CMYKProcessColor: 1 1 1 1 ([套版色]) %AI3_Cropmarks: 0 -640 960 0 %AI3_TemplateBox: 480.5 -320.5 480.5 -320.5 %AI3_TileBox: 84 -626 876 -14 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 2 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 2 %AI17_Begin_Content_if_version_gt:17 1 %AI9_OpenToView: -415.364781760912 206.383230838457 0.6667 1114 681 18 0 0 78 133 0 0 0 1 1 0 1 1 0 0 %AI17_Alternate_Content %AI9_OpenToView: -415.364781760912 206.383230838457 0.6667 1114 681 18 0 0 78 133 0 0 0 1 1 0 1 1 0 0 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 76 %%PageOrigin:174 -716 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 31 0 obj <>stream +%%BoundingBox: 215 -357 587 -200 %%HiResBoundingBox: 215.5 -356.9794921875 586.83203125 -200.873972506743 %AI7_Thumbnail: 128 56 8 %%BeginData: 5144 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FD57FFA8FFA8A8A7A8A7A77CA0A8FD6CFFA8FFA8AEA7A87DA77CA6 %7CA6A4C6A4FEC6FEC6A6A8FD69FFA87CA7A0A6A5CCA4FEC6FEFEFEC6FEFE %FEC6FEFEFEC6A1FD69FF7BFEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEA47DFD67FF7DFD17FEA5A8FD65FFA8A6C6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FE7CFD65FFA8A4FEFEFEC6FEFEFEC6FEFEFEC6 %FEFEFEC6FEFEFEC6FEFEFEC6FE7DFD64FF7CFEC6FEC6FEC6FEC6FEC6FEC6 %FEC6FEC6FEC6FEC6FEC6FEC6FEC6A57DFD62FFA8A6FD1BFEA6A8FD61FFA7 %A4FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC67C %A8FD60FF7CFEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FE %FEFEC6FEA4A7FD15FFA8A8FD48FF7DA4C6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE7BA8FD10FFA87D7DA8A87DFD48FF %A7FD20FE7CFD11FF7DA87DFF7DFD48FFA5FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE7CFD0FFFA87D7DA852A8 %FD40FFA87DFFA8A8A88452C752C7C6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFE %C6FEFEFEC6FEFEFEC6FEFEFEC6FEFEC77DFD0FFFA8FD44FF7DA8527DA87D %4B527B7CA4FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6A0A8FD53FF7D7DA87DA8847DA4A67CCCFD21FEA7FD53FF %A8FFFFFFA8FF7CFEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FEC6FE9FA8FD57FFA8CCFEFEC6FEFEFEC6FEFE %FEC6CCA5FEC6CCC6FEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFEA7FD %0FFFC99AFD42FF527DA8A87D7D7BC77B7CA4A6A4A57BFEA5A57B7CA4FE75 %FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6C67DFD0BFFA8A87D7D8C %FC99FD41FF7D527DA87D27C8A5A0527CA57C52A67C7C51A6A5C8A57CA5FD %13FEA6FD05FFA8A87D7D527D527D527D7DC28CCAFD41FF7D7DA8A87C7C7B %C77B7C7BA076A6A5A6A5A57B7C9F7C517CC6FEC6FEC6FEC6FEC6FEC6FEC6 %FEC6FEA4A57B527D7D527D527D527D7DA8A8FD4CFFA87DA4A6A4CCA5FEA5 %CCFEFEC6FEFECCA5CCC6A6C6FEFEFEC6FEFEFEC6CCA5A69FA67C7C517C52 %5252A8A8FFA8FD54FFA87CC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEA4 %FEA5A57B7C51525152517C7BA69FC7A4FE7CFD5AFFA8A6FD0BFECCA5A67C %7C527C527C527C7CA6A5CCFD08FEA5A8FD5BFFA0BAC0FEC6C79FA67C7C51 %765152517C7BA6A5FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6A6A8FD5AFF7D %9A6F99517C527C7CA6A0A6A4FEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFE %FEC6FEFEFE7DFD19FFA8A8FD35FFA8FFA8A87D7D527D5252527D779475A5 %A4FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %7BA8FD16FFA87DFFA8A8FD30FFA8A87D7D527D527D527D7DA8A8FFA878A9 %FFFFA8A5FD1EFEA7FD17FF7DA87DA87DFD29FFA8A87D7D5252527D527D7D %A8A8FD07FFA8537EFD04FF7DA4FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6A5A8FD17FF7D7DA8A8A8FD21FFA87DA87D7D52 %7D527D7DA8A8FFA8FD0DFFA9FFA9FD06FF7DC6FEFEFEC6FEFEFEC6FEFEFE %C6FEFEFEC6FEFEFEC6FEFEFEC6FEFEFE7CFD18FF7DFFFFFFA8FD19FFA8A8 %7DA859FD04527D527DA8A8A8FD12FFA828A2FD08FFA8A0C6FEC6FEC6FEC6 %FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEA4A8FD2FFFA8A8A87D527D %527D527D7DA8A8FD1BFF7EA9FD0AFF7DC7FD19FEA6A8FD28FFA8A87D7D52 %7D5252527D7DA8A8FD1FFFA87EFD0EFF7CFEC6FEC6FEC6FEC6FEC6FEC6FE %C6FEC6FEC6FEC6FEC6FEC6FE7DFD1BFFCFFD06FFA8A87D7D527D527D52FD %04A8FD25FFA9537EFD0FFF7CFEFEFEC6FEFEFEC6FEFEFEC6FEFEFEC6FEFE %FEC6FEFEFE9FFD1BFFC28CBC5252527D527D52FD04A8FD2BFFA8FFA8FD10 %FFA87BFEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC6FEC67DA8FD1AFF %BCB093A8A8FD32FFA929A9FD13FFA8A4FD0FFEC6CCA4C7A5A6A8FD1CFFA0 %CFFD34FF7EA8FD14FFA87DC6FEC6FEC6FEC6FEA4A5A4A67CA77CA7A7A8A8 %A9A8FD50FFA87EFD18FFA8A6A4C7A5A77CA7A1FD04A8FD58FFA84D7EFD19 %FFFD05A8FFA8FD5EFFA9FD7BFFA829A9FD7DFF53A9FD20FFA8FFA8FF7D7D %A8FF7DFD52FF7E7EFD22FF7DA87D7D527D52A87D7D7D52527DA8FD4CFFA2 %53A2FD21FFA8A8FF7DA87D527D7DAF7D52527D7DFD4DFFA9A8FD23FF7DA8 %A87DA8FFA8FF7D84A8A8A8FD4BFFA229FD7EFF53A8FD7BFFA2A9FD7DFF7E %297EFD7EFFA9FD7CFF4DFFFD05A8FFFD04A8FFFD04A8FD71FFA87D7D7DA8 %52FD057D84A852FD72FF7DFFFD047DA8A87DA852A87D7DA8FD74FFA8A8A8 %FD05FFA8A8A8FDCBFFFF %%EndData endstream endobj 32 0 obj <>stream +=հnͫ1N.aB_V!De׽l=&.G)4 {-=/9UW>aَٝ}z[Cr4'[Y>ݖ}!?cc6 3:}Io2ApEG+ rj|;jdAX/FbE'X[->`zNԿTF,E!j|܃ʾ2a{.Q`AQfńLUAv<=4˦U#,=4Dߚ @'dU nkY[6N}Rޛ_9Av= BV\겕YТFY<ѾC qEA;.t"׬)!zIZSMZ1݆.BcP:f] ?ٱAR MyCDbPm} OCl:ZkW?臤Oe t24튺5Gc蓨WL+Aevd{JkGLJ#vȒ,چ +rLoSM7ljE؈n|.GF+yYO\y$hwɲʹ!bJ˪Uk*nr>zs*|^˃uY%+c328LoC=IH zn@e4[3M+`ňN]6brMU;?;و=u0WiiS6'!qk/D>O}uq1GxYG7TӼS +x_VV TS U:3O=n9iӟzk_l@'1r n+4;&q2컗 3 +7SҲvQmlAܟ i܋:T_|qA=#<NVFB|sbDT*|Lqʨ䴿haI+/tQw +wv\C99$rfO QT}М覩SW 1e]s0pg @e9fqDQz4|]_Pƚ}WtctaZӦ]|ৠ~̲aLX 7hė X/ B"w/^Ʈ5A]yiP<3@I腽Ցs0ge|JlrBo.*a͎ P9s2PĖX3Oӱ{~.mB;( NNɝAt9+ŒѶ$Lc ;lـIraםYuӇf,ƶg}YĎϓ VFjZ1tb~Ǣs?4?&E=?9/c,Ҏ\АCG >'J*ůcc^]o|1?KGF4=LK;j{)*\RX[|CYAbհ;ʣo{5;hg x{ޝQmB-_5R䉎gNAu51SyVwL/%ixB>Vq[pEêTW ISKLN-{2z?ۍw!Ps?2US%{AN59i+gV܌~8.^a]3o *5ɒa.UyWw2+qHS>vT=^zb ++P_^5%iQjbm[yԁwQRcD)~8@D_s$ OƦi&J:c}8g kgF$ ϧ{ߨpi7tnbzM8\q/ 'ɥES2>&9I|s + (|?:2',<權qY76w=! r+~#zqG?~s+M:/ɗARA]˳ޮϣ;:_xߝԽـ=37HȡV%\{dˀ )FHܖ꓎}K\:Eѿ_d?3 ?` NpnצMȄ j %R?Lտri{~:`ȝ6}NxEw5ȴq)Rjxd'^Yxm߃/[RC>|(~ISOP%=wc/M | p]Y)v\18$S,Gc:fr+gI{2_5]R5E}#6Ԝ%5rwXXձkj|dO@gɮrd쬒ľ|ܴPS^Ake 6?_46JPn>/m೏^w36αlCXxW9!kéfIKk~]YTQK׬b6J71[][b043}~.!.kV? ~8;q>!H)[qN}D[21GE>J1dL#HCh#_8wS]i1*/?1Q;JJmfjxG6$-eA36gSlh6fyQϵYu뀼⨧1æT5f9mBPɕf3/+mɻ(ƞi;B$>U>>H-nʍ?ڪ?]’[ɗLקi9u>JIu J#фgD[e;+取=pW(,-'K|vBO9ON +s<ײ_7|}JO~C_}{q5y '3soS4ҮQjtU>"ՠ3/gTcj=)+.=nxvJPxok_ ?J}yRe =/BmX#wJ6C*^_ ߦ:_g:lgUdV8X`Ém솧=$=[RCSQ9U+Lƚ43OVn ЈF"A%wMkrx!}:S~}R_4e`獴 59g*.9:['4"P)q fB }!Tm9;6LxX0Bo_T gls9\\j(Qq;Nr _=@C}w 1^(m>Ыu+i \Ćiu!F#03 +pY,fFL] +G)0~t5"/8UkI܌!RL/.IzR2LH?{ *fi%1mD. +:#vĨ吿[FH;* MA aIk1P3zBZlW`sbzjYC+ZS +gOp̭~dĕ+vFQ1t?)kJxS*r恇6b.Ysd_bׯ9|xO㛕ƨocDToՃ۳RU% $=Z3c*fetιؙ-၎:lID-n ĨCVlW4:z}OGzБ!+WB#Z|ƩxKƆ}^=vnY]5b2%lo %)s#C*񬾗%XZ,4TjO1+ڛS΍^/ ?Mu?=gm9~rR6F95'bՅ+Z2&XҞUDPlFPROomhjx1u}Dҫ-c,ʗ WhRb%,뉹KZESC:lxݜ.}<}m^~k,;# (頃=geS߰dtxe€{`ҷG(m9j6ܽQb- s +aCeШT  yԃ{[sLlвٰ`Uh=/m f.h}}Oy9+2dt-;&#yZ/GE5 d`Tʈ)r!mTFNE) Ձz`h.%p!ZՌ +4eɾC ХRQϺTgSC.?)5]tu˯q)dĖNRUJ(^0sJRڂZu ;ցzA+Qo9AhKP'ί6aI(n=C-)fTᚅP;; +u!6h?ܞM )X絘ZL-<-5F>z$DXbBt̪ARF"u1#oV0eG|AK[51Ӱ9"\qviIq˻}2_r|e96kB]3GU1 +6w7!/'Տ[R.M=\+mB²6"zXȐ ZfVlxJZaCzPtЊXҚFYLxUlVĉM#aǬY ZZ3lкK(lY8 Px{:lv3aK&BD2rqQhm]U/bVY47\z_s7IՁWҬU?7I w&iXՓ/3-eo]}vMC? l9 eM bՌ/E;hM;es1>p\$-;WINNY4׽QEs*ze ^qn8dW95VႁY5.ezJٜ i=F4*E  WYH,] >.pw/m8Ͷiv'5 gWGa7lڊ00ʯQy+.yL*xÊTl&/M[XOԶ`4nۤ{-?"rbUV뷙V)lo;2"A^ł^tM5A#*c~ĎI#T2vG[GYkWKćYi DfVӲjlyyM/4}A<搀m;}Bᦧo-Q\ʀӴBFXSjA2+m9i-H+j~dDPQB9>OUc! 6<\򚛃⳧A@į[VvhXD[f#6 %5(ɆH !>FQ#6<0e"Y*m^&l{c{x]j'.j"gUmlrj œOh\bA\fIyAGTQ)*p+Xvr1 b[XEs)~OLښr1QV|PS$6.jo hMWBhjɣ1 eyrbʼ\wBޖR}!8i +Q6n@)Xvp[s 8% tu/1oc'L6kB*p4886X6{ـ0>g%lhi`cLв9ݪܚk̉RC$&@mW=o j톗Y Ԛ M5 7 7oj>ACr{\3ez#|8eu`kUb笄e~uC\ G@`S`4Ԍ!a_P v`ֶ]5r~aE;ڝ +Vr2{0! %S@ыrVGNU\Pֺ}'QvtOxȮR +g%^lH_6˗;0Nz(~M;4,[pF gΞ5qFAzÜ^k~ޒ4؉0H!]pɜ֡ULj=/uwxj^".jh]~E 5w/[hrRΦV6F9u3'Hc7rDG9m?qmDޙn [w&M:n!){~ mB*[d* +帤6GtK 핻S4= iyC"{oKA}S:t;bيOiy7ǮjL(cXe%ntsF~]@Qeei~_x-)iz-pV}1Ìi_jKZ^#zDR{-&i*elEtTxYx'D*WgOZ3 +<*{|SQn͛31+u%3b%~׈V3(4 !ISJxЂ31a!{?Ц1n| 1,:uѲuD,ы0eD-n^l,OHsJzI6d6=BNpX@ Ӻ6JT;gU-9%YFGhR16Y%[z{,2 ܫg-NR'1 tЂ.f#,vIoX1P뀖y%7՜ܨN?OY?k՝Io\]yPz(kFMXS +VxL +V.`xHV!NnAW8i(3'x!$I yD,p y;=BqJM#2td{r %'fF7`.0:rM# +(h^/l;9;an(4X+E%jK0dv@b۟]BN)Wٝy@X!{^74) ІySq_s3gJ<| v'է3Qg8h=mG{渦 3iQ( 6jhD" |hHA|ͺ.iR_+YVq\^wr(?C]mcFl% ƴ-0NW̞J{.AyQl]kaIe {OnNX{̱Ha7Tzyۈӥ8H-m$u&λx\ޡ.8sL{42!XdS<UIPg Fa7"Y[I&.n0%G daa<]G5n55RKwt7u$KV=]0D)H^U//ҽnԂTwQԉ ᯪ?^r2D[NY'/.]M r#+]ԜV= :)sl72~mJFQvs֬uY$c6fp9P5US\ChDެi4+9q"kaO«? +x=(qByhADm(kkJ)_|?=X|xR7Ѩ.?2 +yZD,%ڃ򰧱cUYu@TwlX_VfN?C9P^X!w>^xCؽa/>OH`ʖȪ5_ǞnM ̌j _W&E e_V~Ke|xt<ثHB' awX Oݎ7 y_2JyhB.E.E{MP6ZOiፘ;`쎟=tܥ<;1},sۣD!YoTf#d\ +1^ 5:t2" N*΋:dzydS##[AJƨd媕ʦ>?JՏhJfTEgeNe~|C88_['CI9ƔY1=hU/XI#(cȤ29VY^c(PZՅ _kcDBDj!ςbcC>trs-4QX .-Ab)r&6 o|mM"Գ(챧j]X:{u";곆K@z( /u*Y޲hlS]r+]R^oݻQ\O}^dPި#ۅaW=| *aҲE' +gxw}%szFŒZoǾ5 >Ѽe;V٘ +IaZMSZ{+HXر<Ʉ UHCB{r=;X c\TK88v(TWFeS +KN?(ixYz'y7uưF)'Rh*Jvδ7g$4!F]"w*٢HV II(_rJkzck+듁"FtJFtgQ ;x(2[4LqE5Jne4C[;P&s]Z@OZ%*6E.mIpKVHdTHڭ)4sH,1 -<oBI[򱣧?I)q*Se%"02oJ[]FMŨiVs4m{Ryc +DJAoQ'#1?*j9PGv< +QAvlYF)De0+U1cv-aYH8Dt$Aex{|r9 I :FբMJ mQ)v kRMyX JuF."}X{ju_M X;0{,y:ev9pLKͳzEoTӆzU]iޞt逾FЁ{%}QI3jmofJO/ vfƅ:?t]~Kftf:t^*8- F\ ^h[@HB<8Y{ow'><$}!dw Ɩ5U{f=rkPX/N1G kde$,/&O`M=]#Sj54wmԻ֣5~PɆCՃ#c^QaSn&c\ͯ츑XX0k~-(g{PYF4Ռ_wU&۠ѮJ5>5!.kWFN }ձeVuͷu855qf]4;UIMvd*d'ȧG&z3]-Isc-%nfaM>м}SlwlBsÑѣ.^/}`ݷH9kBVt4󞢟)X2g;y:QSDh dP[bc,L2g:YPW]R Ip"bƌf Hw4ei+0YWˇJ? чY(=n[S!xVG@Pe{nd֦liؕ+ mO:'6k\/1T>:D#w>48  Ģpo5 Bh3%(v. |lQ5&H<7T /9_%Ů&f1ϊ"gIzi |ja=MO7kG qdb\:z4#oMiu׻[6m Ϯk]3)ݐU6ui +[3}bQPS>=|!uk9-9$$oK1Jv\; tX'?R87UeDEMFe'sMՀݜG:L-ǛcNҦWk';g{[YW4*Gj +>4 ]Xx7xJ,jՌJ"(I.j)ϲ+ͭޥm!,r5|OiܦZ&(9dlM*6o%*%XM)q49t `ňQ3*%$Ҳ6EIN^Dy6֯&d\7P6-5W ׶rZ&%.@AJ^-g"%,\-tQ@=_NDV>F]$E }Q"V4-wKDIv(YV !LZ7ǪӽJJ?pFÁ5-kVi @̕Z9aO22ya'8Ҙ6Ͷ)-sv ez3{-#M5'm}g)-6<r+k)>CSoѦ1:&Z;%gq6+TWK+ik ^Aw ƮG%J#8\ =Ԅ+>YĂrLKM\9_8eHSQGE. g4C?CWDZq6teuhٵTsnuu2n `cݫM-޵w]{,lk8h3KjĈi\RE+*NKꖰAc(e7AXBXy/4shˆe8 f|M}k==͢bID`UJ_NWGZ6HAYۃvG!nyuOM+Sm"T{h8>*"n#_z-bWK}'1?c_ghM~М{Cה"V&_9n)>)~fjOOblNqK,rgKбYbufCXɚ}؈ﶇ盛OL'm V?\)':RRS3:C^G<ϧ^ό^9HW]5 Z< +9]y$d$ѪW[Uρ~)ͳQ#cq>_Vgex 8*, O=uqB8:l_K0V;^1PzoJ_ݳ6"EOm#-DC-PEմ@8(!w_3v5q RV"=)ޟ$&u>ߟF.=4_G+xWND 5>B >`wE5z}:\Pטߔk!h+9:je܍x| $` QqtZrW+ {Dw*^ow-O=48\`i ]HHXxwu-钦&钱>0=_{ JqJҷRv +AC*^e<8kcRR'}}=7RS,'eZa8/v4@ѭmZ!ac;'K3؟t{쵮%QdO0| D Cۅ//T1*WG`aSܬRjhۣiۄ Ds2lӴ8&6)*"rkk*kK@؄0-+񽖟xM&|pm~$ݪiʫNK;ӯo ?]4^ki`>ӫ^§!^(S=`g$hΑKL?4 -߫ gl9.%%)#%;İPDe5 -_JGLb[m4L]ЇC9zw/e:n>XlOy5# p`;ňh, +o|}iȸ!տwX 痁9Ksn-I>2:Y14dֱPgW!Е]$(AdڧNͱ%.0VU;uS%&^Pb;Ѳ*}3~ 3\pKm/„~6d}/&m"aTŷuWU[+MRԭty\2r(9:Jxad93,*b'n/ω[Fc j|{Q=wo@/&d)--AG,vS֤b]RxsGXiA v&9ELsI6>!ǘGJZ.zs;E˽݇L9P勝ҧ RU|i]߶@/6ob]ZlH#E\hGjfF?{CLvP骐ۦC09K u]!6_lb\J1+=-೥6a4MjĔS5+%g˛m ٪ڂ$$MQ׏u˸+9E|kxwFw-tVݱ-)F䫥v~ !nsD<_E94 l3$65;'AtV+ߎ;r+ÄwCP~o &zs<"[F26Ԗxi'纩9Pzu6x9Aiȗn]+[}UO{!/ewR tZv.B >P#.]*FGC%]+7Ec =I^x%%/u?Ob_wy7=*6db.!޹%%Y@.U<%}ۍJ: 1}uf"ͣ#<"C{qt0N?dRL]U;駕E݁Vfi.ŏbuj{ւ=%[!~,ksI7T;g ⧫qwW;38d^]H̻dǦ~^=s1:i%}~8"o䱷En56ihƮ˖ix\s~37ȭy<*T 80Ttۧ'd퍡",ķ)5PǁH()yu4qBث9b걊^aCk '.wT^Xuok@ߚ>ssWO?_xv0kb1O@_mZE$ֶ^0W<1^F)FKD5u宲ۛBܛ.r/a;ĄYx8KH>l E~*֯#f>[x!&gk0iG*tB5|ȩwE Luy>Tb(= uH>]bկO 9IW-Rd{5_94jJЙ6]Ȁ0 C#`IO?l K\} esOc=hcC4HTGͅ|6{/M)Ä +o+2o}(C >CΔ5R{yB}Eɪ+8ެ~#]շ89-9.x^Z/*,D-w8`uM)?-g\:0=ce8;#WvOT0KA%"lA` y>#>G6.%-?^|_r&lStͅnT=z'$tn?2+ qA`C.@kI'j&#瀔@_K<3Y3e"| +JSJL2x.exfBE7"T$J_ckcjODNٙ$7}ZZu# 9;ǩOR.he:r+[<֦C#VMͱg_o[5)>(({&ypTucRSxaj)kNXj/&n =PEe U,A>B49PKVl("g %^BҏN9VSKvH 5)/u0\' aNC#p#Ԭ2,%}{6z3EM[ܝ$5()Id )P;/t*6tBϷ)O wQ_hp_b!,"dh o 躶6O9߭ /1E.ug_1ɩed-}Ľҷ@X5/LL[Okl]OW +Z>mlZ@|5'j)E+ckn=%BٚF +/wlmouբ}1#5 ${wƥ^ yI nmyppѭ{ l\GCbsJs50DϷ\5flY! $ +6=MS+Uz1K*s*40 Q}:b9hG nG(%gZѣd =;hkg864qW.ГrGsՅ7XL02j} ~6_ǶVasH;>%L<Ѽ2}r6R<7!~ 6mkwu1e~ui!-11ZlCZ!#҇;_7Mveu. `Y#=ťX'+B3x14ʛ ý*ү"E&|ܗRV,֮u\uKFnci~= %ե嶢 6xMi3*z顉uj9N9&:Q<8Oր+vi |m3IͱIbL `cmSUM}N@ם=%{#qJC_M+"b䖜g u\6c}(4ͮ2! }d5 ^e-u +얡IY>5*cCͩJe䵩?+Snob]}is~mVq-W~-ʡC8dTQOMu{zg/Vh/l3Y%MQ+~}TP;<[‵'?omxv hWBp)nUC+we:l\iv'aIybaR>إyqaO36p&FM + ۝(y!"Ot# >>{c "V ,we=DfC#Qъs-gv"RmleŬnIjZ/BK/3o+'J|B`W܌]gӶI jenu7-;k PE7O'h.e }kN|@?g+C'b#-BT[^pX YNmkW `5IٙDM2-S \uC?g쓈hck>aHYdZrxYrgڭWFYBj +~ubFB]S$2n6WcGm;๖ѱ۶)HT34CI6 qΊG mPsW,>XPjg*ҫ\,1 km)>έ}WѰGZb= +zT!-X}wDNcNW4RäYʃ?]@J;66fe«)|OE-0Ru6pO h{KD^ElW*CRBީQ76h듄XQ@ϳBhU?2|::t^yCP{м h9&V/U\CSÁ4DI\?lO5N]lK42xcuhÂ.5ڎ5k,KuK|K|ZOmG&.hpzly<*r_1itg좪Wn1PG=6Ǡ3ic/Y O< ph&m[(3N !'"֭moۜm} toŽ86yC"b\殨-jÙ%}12AcM={Y/l;} ʥA9DS0%Xu䕽ǧ*3lt+U[c,}⊰yze`f⦈xWV{bVUh@ECV]7ӷ!H:6!&إȈkm7mp 2te7iǚM~oHYהq:Ov8E'䅺"DfK]Ʃ ?~@E\.}x1 Cc̟}j>d_}#m!,vV ܥok1 aMwcrO$4mz4DjZsN@#mS8>lO;[푥}_~%glŎ;P~H-7;^ml5˸[b>,W,|?3B߸rf ) l͆,]TWI`n6٭.9ȭIl_O.X:zՉ<_`c{ё] ೌݠc7pMGO,?Yםo +NL,OGQy:<ֻfiQ3bxjѭar7G(2P2z9~_LJradIo8]9#̤;ʠC ,$nJ!aW.y=*ku +lLyW? +`qq\C5]1=]R~O'` ⟭<"3X\Xma̓6<ԯaވO:260rj£ĥ BZ*nhWZ}Ocz־]xsN1G+OVu0#&w/,-%R_eu{oU>e#>Sޟ$:erb~{ B.4=z4;.AG gݞi~!Fi}`?0v"(yѪg{fʽ3y۫d.p6)),Ɲڨk>6A:|MTb{8kh٩ynU31WOUF@Jp(9rGǂeC=0>%)H*D>iM=Ցޢ +(U-lعc"-.SwD,~/,ylbk|>[h[ybjlY`:"AF`!>fMퟖj "@>o4oKm*7Sחxؐv{}|oT%l8u[2pd ^N=8@.WҘq^Ç?Zl}*d(Oφ:60]jj:2M|l#xgpI'*Jᅱp +gᛥ~^r 'T8ѐ +?ӱNpѿX` ,u!E$\qN%%HO+쓣blW~%4}lhFX$b ` +_ Yk:YН +r:Ћe^%@vRQ<]-}v[4cWb#G:as lTM' ӅĥA3 'LẀ9U,ЎV1sj9^ UKRTcp<π8SJ]c]\[}_CȴNW.Wį?o4u k?N# . hGF\k8H|GLCK fyR h7> 攕94=^47:ԭ o,RJyDgk"PQ5<EŸgoO s;[͍[bvZƾOl86~ړ",kJ߃ɯcN蕁zΡ.˭hrNZ= -lKh ?7k _K*pȐoI 4@B +[ BZS۟ScrN-l o"Xx+O"ȅzƱ>[vne`oo < +.GEn0 +G ͍gKFJOI+{lN)=3Q!6jk |i?gnAn*ګD'OUڛy5Zk*RWKJ*"K9XihXrSN.ٝƙǫ^Ya/9t;? ޙa! >Sb-d|_\L"fn2NWYVjjET{ |٬;_45;BDf]SC))E>=9ߥ?"p c]lQpN.l!3G:Z} i'lz>Yq=u{L<Vl"i 4f~7?N&ǧ5=7 +>aS թr[/fm S,brhkfk**F~ s+y6* \=6r+DU]ꬸ }b$V|j0@Q\DEt+5A2ZƲoZ'rnڦ{$ȭT>=p[:]!9faYL‰O>h 5K)'K]Wq.d)`oXg#bJW%-55KɀP6z0+r<ѩ@fcmcsvERLZC +PO->+jB_C)=1ֱ@e +_ǩ 7*TW2/ʯ|<Toii@ĺԘ" {&h<Ǟժf{LT K=l#EF8~ܨ68KM#M]_xc.ߧ|99K:7ѱ_]=RvxUժaA6D$#5-2aG \}A>|;PcwxX}}X GM,8ꪋuڿ3MEZ=կig@+_ _VNTl"^O\Jاj)r gЀ,',U\*RGEdShutk%@ ԍR %3GL*);ds=eXKm`V4$ƩC䔬9>i{ *ƽu+ywFA xGIyuhGoT=Y ZM^i6cgfqu 4yreWUP3\*J$6ܡ.+L@֩ǎhyڧ!f:?3,0 OhU!(;?{ƹ >L͜3sO>w{۲e˶d+)"ER9$r,Y93'DJ{S_̬֝{fT  EYjo_h42D<{pkU"tՒu3ffu+g<ݽc79g{SJLTo7F/;,Bk")?b`'FRPua{3wI}pd{v=-Ϩ-'?y,:Ԕb|^0OZ6uOK!Eف WƪXy]q0\R3'nҰMHu3T}-ׄˆ 7QOإ)H"NHU ?@gee=19cVcׯϊ1y\u/IDi#j׭O^s I$m;jA@'. Lz¨ݴ>I[^ܜa‡ Q25+x:sc:By'fYʵRvOO6sTG)~y{s}m3WjyIͺ +fwNH9$ZeoLP |z ^9i?5vQμ;#>읡Df%8ωo}TMpSW:%6?d%oc* fyfKMoؼ4+_y o%ЃPƕwYN_/%i^;ҙIF{WZz)bWi +Ͳ+k1 ~xUpʇ6MZ6Ǚ+oIga v83ꛥ?ND]7qFlbJu?t&ad5fkoH&}gD"`.mysT֋H [25ځk4Wl +%WG(InmpH'=tQK>MQG:!z>5) v%Yv Z^1amĮXa-qa!s[˪:ISK+/[mkRzonL'D%lѷF̈́q tk!tW' y<7"mZ6FԼqC/ێR44ҧ}< \݊}qtKE|in- [Bsj8V xaFFMbVeptue{+o9a1Q6Q/{k8񧈆X.ԝ[SɗaԫX \>)*0E- Lq*GH?݅A:ɼ(!mVتnL lt? ۯ#+o}Ӳ.40n [ƹO~Ye } +:jDn[{Ӟ$^ eAܧeUt +~ԦTM0ȗu$110;:jeXˬx{c8uwd]q)D®QDD`_Cb‹C=!&Ou]BV xgJ*FlwF">3lV\,/ܿ72^&߂YY5bR6&i%~=zms^PM.Zplh) !N/<  +Rf6&?rR{C&ψ[8plL9z< u5J>{|ߗ8v~!l|3A5ִK0/f[' VFڏE &L?u# z+f];5 z @!8e¤MD^ -lvYۯ[O |I'xe9u!кɖca|\ǪDM|79)rHq!%\ u }3̈V܁D `vPͩ9uߎZ%0aɗobF`<%^4z{L԰9Lw-b 7LȍΰSuJ8qTֱcVRi@S{"0Syh}P j%7nU'{^D˹~ ZOH#h?h =zkfo)UZԺ6J<[1_NL."bjz_}h9:1s2⢀eYx+)8՘U++'Y1[=7;}/q LFʸABޜb5r_ h{y e*vg˵)Gw/bSu߆Yq^6n}~%wƭJճ[rDkK UHF]ʾ(`3gFf%\TǕձ+%WBrKy=ϚSGSsYVYmKٮ _}_oLLϨ ꥤA0z~[9݊ou/AsBBHǭj>|Pz0iOHeR<6rRX?1;&:9iRpwNb?3sCzf3Ǣ1^IoDQĂ7KV1-ؓru+7FmR}h-އ]*H9!'鹁옍ӚpY3h׷&y-u)ck*Ve@˨] 35ҕg_}x($zDŽ|ӻ}O&!#1/1w[EԄcפC9Ғr6[OlM΅usi7>!mkr'Ndb1`wR*C.%Ş~:-=SװCgmC?C$йwP矵ݚ]ڥQRk;y))eA#ga?}XRnW'M挭˙Y5㺃/p'Ӕx%ce7R"Dm"* +/#f/"⤍Q=)CAG)/ۏj:e>xY3 ڱ&;~^X>xu}k_Uw3ϼ95|V(6 zs_(jc_1HYq'GG^?>~>n]15-mq:QWTˬo==;P$b:vmXE%毾ÝkutKp rcBX:*XzN8 Q?N>]xXCL)K9mA $<_STG(OڛSVEb a9/Z"*raBO+nYşjx-︕֯|ux{ҵ UTH1Z.1SN>-'UČ +&a_ͪ#Yx\s~;-Aj*h3󊾤YLB 9?ctĜKZy>-ԯIPW܄\M.ϺYiEU욨^Ws*qGdZ?߽v 'BJխLym+cG]O +=0 +2VNK."E Oho^rjV9<27avԌ)EHD-N×˃Ыwr|)a}ܠƍ1u[,J{ܔG@] 8mWcQQ+aO+Ϻ'0W<ƏI1B9\A-.(ļoۦ#Q.@߀/2+AMh~ušѶca=.bT7GINC\+"5gF5zvr 'Hw*EI+>de]퉶K_=ĭ2!p%#Fg8O&m=}[3ה H!RL~?xZ7tSA-*d`4urt%jfT#&N#҆xvn{LΧd+*ڵS +9qu=*"*x[6h`+Sfvbd7;Fkr6ƺKxzo@l4䲤Mh>iq;3žF֐R A+ʲs \MoKIHx{%b0}sr7˸ގ8 ˖1 nOĝ; F╸xӏq'S=*s\⿯yΤ^>~jEOivutV] /;_tQvp&aWP'j8u)+*m<Vm?e]$- +buGmo7 cDiku)bfLK*Z~]^ O*a8l#N1z+FO&IyYA\o9$QiN9-(_\\dV|ÜTn>9"v]XoHZWkE+jEtN5Xoq9 cc[~K9[P)ȋcsoH9IZ`ɧm^,q ! 4QKrZYQ~ɧ%'݂S!Glb)X N^ouۦ4>Ҟ*}U&Tb G^kxk>)s1 +jSъZz9b3j$J$-Yˏ1eiχ9.%$/εΜ1%v zIBuЪcK,b‚>X("3aGo5?!FnEÒXX8-w +BڎGov雿3vCdʇEpgY׼/kAB-ÇQ#N'>jOװ4̲{zkUvY;^>4u\wa٥1Z2VnkĮOYHEb<1떇Ik5A"rv$t\׷Z-%?mTp/%yH1^aEx;}iS4K]Ӓ_\WK}cmAŮM1 +ޑάwp;E) U5UWFpDrRko"*VE$cr_ϩA,Rz1p;1;G-:"\LbO9x]i'Sn4̎WX^q?H{N(^;abo64=w[?Oy|w+k=qtFێMuH>58>'bV15afEtҥ7M_:w,Un3vm=z-4!:ak m]9YLu ijAH'hG,"R%'C&a[M+{ +3Z.>|E)'(I)zN1;jeI }y^/us[ ^UK={\dkڻ#c'bWE;; +2vs2E<\DߙoX'Mi91vn+GqaU]4/#c?=,?H>b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od~"{-k^DZ'b?Od.?H$~w`蓃yNWu'el9ϧt),fWr OOA?py:PΥ(L:гc~Aɑ_rNt_>y_9/GO9~LΉc:өSGO:q'бߝAH"\XrG>HNαc-'8~؁cGN ;|O>9x)b{s>iEf$G2JPůpj}VcjL9up-"F ʨۊ%쐎Ӵ1EҮfXeq5IhQƌjEQ=}+ƴq}YX1LϐDGQwMŔAS?+jM0ms4Qۓ&9gx"fU]6p 7QKP;6"^ +ڤ츁UKzDnuvw'-BLk%̤SʋY[c1$,a2.<##o u(8 :FZO%4ZT'!&MHu^Y{rbUwɎ\ȡ2jVEDíG"bʪ&ʞW)[$t̆M@9ittꈆVrjS03jFj~Xk84*3e)2>{>N9A-|Ī\5J:W"YEM5g "D|b@GHy$̼v%N>1a瓒^W@.iW&J bbjw!6v[i h#FF%fm& +Y"}u$Br*-5jmG~Ny{^7Q(Qw2hS}:~)A5/zV )i7MXM ;#PxNDêBL"J'r83M8]1 fRIHC.ؚ&oU|H 6qPp7AZr'l꘎T0k@zܠU6A<6Eɏ؄1 219q3#!|Vв0¿ ހow5 +|~hUA-fUJ":Q(ﷆqB3 54uF +oz+nvfsW^X巈|-iPXt"c&PH(qlʤZqZ6n[)fdM·bu,&a#P)7U|NF,$曖iex9\ '/~ dߑɸiܙ'_yUUBGБQ&c#V)M8q[U"F*B-Ҟޛi?a~6k#jvU\ŮNM)+!mg5,ꘚא48 *%_K۠,BB!'mF.$qԉpX|-'<"V!f$α^;mNʻɅI\S\2qmKZqrI,MaKۅw چD8`NȬIUi&dE [S31|u|pHAt[C&qg$PoOnAC65jELҰKcS\:!1TvK7e7SQ456\)cV)S휎馟6؜n<Sy4(`7EæW/>j:]K;%W*L8x]Hgrq%a6~-osq5h橁RL +n63WU1\]Ho|$'YDޘM+1a fp9+ZoPfsm^ wd5bh\XU1ǵ,h/[X}Gы:vDƓ1 $d!FV #e￙t#f93n$ Jam䲘ߑ0+E!?%nDRbBko=zp7Fב19)0-lm^ \@T­îG%Ͽlx+ϣ(fF&V_5츜2SJSKRM*A-ueY\㕈Vf]uYV<ך0۳V)MLukv\Ē00Y46iLzZrIpq^oKK̎[ĸE΋^YYFτ NޛJuR~-ȅ>葛.,(o}wXYr3\qɯk<׵b/° Xm)Sv2`tѺ"jVS~iw3jBzNS[9q#+P\. H{^=pPRjeJ9XY +G_$-siq+>fe;Z3da_H)C)jB&\òoV +{cҕ7]Ki+1mפiJ墤G*xq:a[c"ͭ t2Cj|>jd4dm: blY\H@815&e%DaZbu"`17/h7"ot.0I͏jYUq[g_4`t^Z-i`UH@] &`z}w !mGnDOʏie ST} +$~?yv]aH/[>;`3Fx/3goei8)kzs{+Su_~Shws~5ZĈMc^S&rI G. ͜F"XŔ eF+ϸE [9t(xۉTeP+1娑Hqjsҋ;Dz"ԣ腾J8j+,Ȕ; IzNOBR7 @W6V}ǧ]Nڥ"I}7PQӰ^23*NnG+d%^iQzlGIύ7Q\4Ap)6/SMJ[_ R&^kFk[ϮT}:Vsp {eZaA:dD,XCFGso[/K%.Z)pT3úS˯+XysT#!v1kct>eՆu9[9K/ێX~ytTb3\pQ$ Mu Rf)ɮ3q4%\hIҊ:ga/WŭI᳝_lY{kDL4B,JHE)͒VO؟>F=&!)cǥ/縆ױ㒊f2&97cQS6 3dMfbμgb5,sZЁΞE +ܣn͉ũe6O8{y`3ZG0N2[@:MP%vr%6 |#|Bk|q*iWN򀽮 ntx`Iˣdb bc!vf+v$BRnç\]":s۳Bnz^>XJ{%$hňVR >)/l_al0">RJ)owo#..agI*%=헙HQ<摺{N$ z6^ֳ0o:}J xkX)W!bjVv+3Z'=4X8  ̚~-?E)-I +:׵RǓkoP_2#oRܴFT>h;z4t]á1R!-dm(0i蕅U]5NLJBTì@w'wk)&DC-B@'|Bmm1tۓI:5!!/8x$m=6 +Rf$m OPE/jMEHIejɨh5Y!M1?#N;ԡ:V% +ӫ6.7F ":j0 4]HL~N +XhecܘQBOy~C|bP +6nRSb1:'CmҒ$I"jh<5F⸓^΃5YTgCfڭŮYb&kyik6F.u#d VNKH- g X =p7P5a'.qtD_uݙB$J tQ#2fⵡnOk:/c?]zMFbZ{rgu4w96 u_zǦgd +V M՗Yc%G*LؤLlU/#Euۙ9ݍ:Y-S++#*l03x1!k~U$DV(AJFJ1bS&, +'q-)SMۚ&151Ӥ+i^=*iYI4bjimqB^$c4bl(>rtu^2+DIs_ mL#jGraו"*ŕem:"!ZQH/o[O&bj$&Fኯ!4_ͩIY$ԸF-lK.EgjN zo i+ׇ;Υrqơo-B|"`ƌ소 iCt"B'\xTLM/CZi΀ٞ^ /̘$]/M!`?v-65< _ Jof)3ae3d"6f.fcfݑvz*AAMw^꽵;IE̤]ʂ^ڝWL:{LN<*">7F Ѓ +ԩ%uP8.bbnq1pouFԺ>żXy;s.e@zNȊ9@ػ1ۯh h2)d<".݊C!He3Ivx+!=. ,n T0?E,S.fE`OR}wkmRY)^F̬ a=$6t@D'lͲ2Aژ 7B_G}!ݑS*Ӕme햡 78?m/UrjQsʦ2:8M^ٍiHz5<î{ 5MҪe`(6m71 y%!Q +)w68vß٘`-=:baTGTv'M֥5,> g,=zmOi|~4î [%إf䐵*i7}{O)µQعx:㣆^yP +3Zm%Io dPۭ(!@֑&AWLE5w 2j.5擳 ÊP8iAAĪC+DYoM 0~Ay;S#Lۢnn\'!n!V[;[ǚ +%]!ԉH7K1]?^r[Fe'4%!G~#5dpw 7x%{|B"Br(gEU)M:zP{O_ݷ(e}iQ r5܈g = 0& Z ޞ]~v˄]EwI?=@Fݷc{ GG*~g] +ٮ[iY5. N=[n褤zt.ĭbVpzK.q+Z椠NczT 뽋c54W9VDLQ?Y|^ڋᎼG zu:o/D|2-cU3q#4ncXm +%bImkis1aQzfĸY>@k808í4*hevm۸pd}ukx%b`VNuh(`F@# +르H1[;Ÿ*$ԜZz%+AKOoˊAvE .l qj)<1&9tHyb\1.2 !ǵ +&jAvA1B?61syY:y!msAP!5iE 7qp!7 +zfE&BJ>݁sEd#As +z +z |9vfc3W&#CM~My{n$=E/+f-|B)"= R.) +dU}(.3-dVܙ}Za-2u"f 5bf"N +ғ Vӂ7na5ve0Vm8[vݺ^OlWFbf4IQ3FȺQr 31p4!Y疧rA\LQxnM.UI;\(N< +Es#z?@<"uIxh"x'%+#fAG]L{ł%grVģeIB2*FZ/ŴF#b S-G#F )fUlO @gYk/'j~C8-9- BӜN üXh@QM)*NuHî +L/E#jn](eb~sƃON&ԠmR.vohG~۝(VMT]w Y;bk(xI[ߝG7J13)؟cwMص*ܫH@LG9v|UċSHPcab{oQ{oiG V 1)bfTY,x:ptûv(!neZVEv~{Mx2[~Sb(q.;FbQ-$d=&JIvB}%t!3?I·-茣PQN{ W izAv}8_B8g~eOf3mFtFi|=P.쉖C9g9)u rԃv0Iu#0i_磊s^|SM?UbCz­C6%71FctɧOr:TEO t$ȇ]S0%pBg%l QLgPה8s_B#$@K8H@#vW҇COTL䑜w0F9`"3G2ȹ }?1Lĝ0RʀMp jNg'SIϾ w$AOUB8, 7uk#9G +bj1b @|#q,K4 :=ybcMR DD;\ D*IF%ȴ7)lޙۛ/ߓ?RWOE8W'5Q)>R甞?7~bbNĘC6 5M*0aн !DԴg;RT؁{dc_IuW vU<^%dy&XEi꜒mzz0IK:Rc ㄰A0`Z8ˡO8TȁNl*d&l& i-X+L1Q?D T0ٿsM?_[\J*#Gb5PߟQt2#H?01#qsug95@\k. h}i48s@w&?@o1dx:wg&q!i2:4M 5nyoHjTAMڗ]Kmt=)ncЖ +s+]/nH R؉`V$v?HE@x^WmuJk8vt}p(ԣӷ:#_:c>N\⁌M1xbt#,Z)87p=3yI&0FK4Iel )hgzʯj/- kM>"'Ё{㤈C3ȅCCe |d{7$}$+?LB艜G=2J~՟h2/㏔,́x$h)fy_]Y t22Lk?u>%8U8Ld=8ri?\?s,?sLa"}k8jk?J7 ۑpǓgSepw"7Ci%s64 ;Y5{*9(&:ťdԘw6#O?R }8K?*;ԖIf/} ?X/w1PEJ8RRODeKH(18H[fNf[fҳ\H53|P#;98|I0e:>T#gB>R1y>>H%|V Qrv>#U_au02Εy?~3oN[nG8(qtORӹF)~m6%>bqrg$;ăoТ Sl `wQ.g*B/ F)eGceV k}PlSg{2.O G +6v_Bm\Al4陦5Vs'[`NT 21@ 9Q02Rb=i#qK5/ϼ09K't2*t嬛vT $ 4+TZp.'o}]L9*ԍAN;h_3Y? +ఊzdM뽪=/<ӈ'&mM ݙ9 -+Fey0I?P1QJ6lA? l6Дb =&<7; x&g{R@Y, # +[@ES}UV&x:ׄR^Wp93L!zOIߕiSXQRnlf۟K9ͧ|)x=Ӛ(c c@zNS5q>&g? <ה{RR0$#ŴH#hNfsjOfxtK8N\<gg˸]9 pB;3sϧ2ϵ陿|o >8Yt6l^Q=kyLQŝOgM7 +ΕYǓ"iԿt8I !X)6ʊjČ\#:tR~)+Nn5D +^ hkф3,)b拏bƈZ(jra D6D>_}=ŧ>[jH.8ʭ:xʫ8Q8`)%ꑊx +0Ytw}ܖ0QB^hW5*,kPp[RZ$+uc [F 艒9V3\)7Hk`G$S 78 h Xg[;ʯ;ɩQ}f4הp>{4&Z`XL{>++˨*l>*dLZ>kGMFW3OǨ+=y~ôvy>U~xEgN\x QU*?+4Ne[2XD&'m|~O_PWrFQNދxii>͏=i\pnZ7̈ĦedBa78B@9TH'pIc4;Fږ38!&/ jy@ƀNT`|2DSv6QsؕbM=@MeTfӍo'P#>(!O5E*S~w4ӡK6o9a's5 issv"]9t>X=Sg]dq瞂sILʹ&?=Of8Rv H2E6TĞj8ԟEŌӹ#Y?2~-e <(%qΑ&tO%d* L he:Du +|׾*3נ-3Kz5G̒}aMx<â$ڮ ]etGYtwpܳ@iMFd@W}4̈ Jԯ)6ԗsΫ/6AQ":mqr 8jJN4f.@؛ ) "~iIfʑ<,i]lN(879ws,OgmX+c0J%Og؄?\֮ZzCNYeFEf=nX!l]İ-I<⮄Ì8:蘓I6zgw IRcz:Uz4UbZ<ͩ?1?d<%r;R;bA?6/UB9y\{h.a}$VNkgցXhH.~R}36Y׏ IMC)5zoM6WCV[;ÌnR=cZڂEجARbjrag74t}$NFM<GPn{/۽Q.G 5zP3 a XqZacI]Da|?9н!l ~m-vig;6LkWIrLOYa21ϔB桌8S3Q&6qqb!g +c8qQƱtQ 3l+ew3=HH!8%ԈAJ \lt"6;zTz9'\qd&p.km({C.lr0E%Fyof 6O*>}h)/c >#wS4}`8ȦIZ>h7AGgN\sC>TPOY)rOFYj#V>-ԑJkڗ3g~%h B5DNmù @>I' +. +(3ޤvE$DxW]hb 뽼^Z([}Hǭ^'f7T /@kX; k-OHۍ6j3#`DҔ_M,[ ߅6ZX^ÍQŒ':YiA:buf/_]l#ˇ>Xj6)!q4U?"B$Tw܄ކs>0NtO ݍ +}ڔd x`#r!`0r\ VLiMx 0M>a&ۃ鸍~VtU4b6&ZTL=>Թ0%x ?Vp0{[ӚҙZ9Rg|mi;z~Ԟl8NXl~EFh!^юv\kA~O}\b衅o +xmpo%X׋4#yK=/yQOf~ӵ]RR-o?fqN:2zm'8F76GtMUbщZjxZ@F3jiw +j/!L{M@},{*{ۇ 0a$gQG9+mԗ"S8ڼxu$.])(!oz< }U^X[j{v]gFA~lMG~HC-PDž#)5a{aÇ+(VԗZ6Ba`cw uoˍ[MiKyn˥zby +yye@rDٮ7㝷>[W2"Dƴӵ3n niS/7"AM>[j&8/5g_6D=ה_nLv+옽>B?Ȍ[n!4JC9[lmI ֚mT}/T@z͕4rŔK +-[(ԵG +ZkC~5 f(m5WCq|*Y,ԦZ:>{vw/ՐwX֧@! n|n,tIkmym9b-&V'0Ȉ[0_+}.>[m&E 7ut貝!jn.LUdQnUaF3}&r)8ٸ00ꎦ(Ri/=ݙȆ><'ov`]tDO0oKok]1nK&ۆ0nV'OwRđ҆Jt8Nh{mw3Sc[}\|`3i bDtLIc؜s՝a#v#Vܫ;Ko%Sw<̖NH^KNz;j*yڌ@;vu=I]-}))jgڒh9WHYl֞0YhG>V +ۃԈզd@V?֗Y!fhF8兦(b +AUlot#_&cg7ݯ"^0 $j'Mqҳ՚ ?b,kS="a$WŊSQeV焛ja擅HZ,rCjSRCbL}W]rXrRvzٰnZj;ٗ%' BA}"VQr3kJYPYXWoi ҊrXvfJ,$ |0Si=;Q{#.WK&GۃZyֈum\o:4 O2^NÙɑ[ ܘcAwU8_3v@m=&sЂ66NWI}]ktcYaz~f>~/֧W=Dޒd=Dzˉúq6(up +>cjށ繿̈́d0UDzXNj#R^{'4׿nO! kqnNE>1w@$G"'~zaLHP+_ U1Ojm pIA‡Xicebtҽ/rk뵖WWZt9s j'm.j(\^xw4=xvbWV1SI׻AWFD1>[zA0O.JD[ x{yAp3Ifn!QZrȓWZG@r\UG֦ /+2}ojl|%oJ=z j1[ZPm-}_rRMucJ}V ?j.J"oVS?6-t6j+RSbC"D^xvrDrFڪaR+i~AqJϕfͥ&JGY}2m 9@P=ZGڗy+|5FM>xw 0`!zr}b-dۙwk-g^D0]?%dpiP|O};[v,7&4Wq*CXMB+KR^6ֺYQ&5|k*[MݎwW|ZrH}g0qFUڧ!aKi^b#-h27buc=$5S$=.|H|AV"0П%p H@k"?u[E,5=5C !h 5W|kޝ@YJ ZoΙD(K_D?=O[4^Vcs3ut_iqCYq44b q427J;*l4/dNЗQ_ U) ϙkO) gr1w=lu|5賴wCUll +x~Z=ȯ/v= + /uwkzV?1{,;ݱ\@>sDɼHsUqE .4PV(Չ \"lt\' +^$zͷ`7h;4dk'ߣ)F:V;nXlH6Z/Xh(q{Qu8?'nwQOd鬃,z&5 Q.fZ%m ~:|P8\j#_<&g@ R~Ɏ*[ʳc #t3͔WZ32,/,+XFmyvݵǭ~FDU^mw^lŻj aBی6*;`n,+,7J 3TY9=Z.MxVFaC s2Ҿ_z.ůϻG +v{԰nf4V+'j!;]dxXl|'k<=l +8 xPR-.H&3#ĠnBu؋ɬoWg*c^ @'L&XlLC Boq!}QCq3QD]`1@ɅigG ;݌p[hHw{ZQIZZy{luT+yAcEUv]iZ[wZl"zo̶su ߃:L6Xq!>b| r +Uy"xE\;!Bx&Z M߇ 4PF 9r+k;l0I"x.7>dG# +}-5x'|Ht6BE!T@W$ۂvsGVojWJB0'!sweF h'Vr;O7(B֛X ynCuG2>ngIWUQw3tֶf;Os\h߫dy>) _\PҤ75H{mݯ ٦t]jLYo#{-`a[ qEQkMЕa +#ȧuX.t!+\oK'HJ_ː+g=XjF}d' k#K9uvn<T`h`*ŘVľZkq[fIxJ*=)3ZTdJE|Ί9~w<$HtlC=|#ڴOz #xПvKf6gJ@9mQALu~aßuH[^V8M3~8,GċnCxs +@zzg&|[|sڧ.j%˛:z$WÊєTӽnucjQtCb֠AVC?.'_hL],yX7}7>xАy(#мw7胂ga09\Eotӂ竑 _~SuDcuzz7)|A]^j%zm6aq>Ze/Jtk0K7򲖇x6TNjE!t"=, yo~I[0Uj1慪2쩼$JcS5L0rw$L㧬B>:RQ.森 N.#-'[tC"Ƙ/}|f'Å q~jr+mƏAKf@lĭ Li.l5w'i%?ʁ$muGcNBm<*彾?؀s,|xˉz6,~R`g% Po`7%3ENih=n9vM?g S"/9}Ţzn$# { $;̀{7REu'?D@OXw_oI-LJ<"vjK +. 6=usy/Ѐ_;CBj7%p:HMpP[ʂIq@:v1ۀvZjX)w /曐_p^b0JUd-I['$l0D>[b99FyVZ7!pYY{ue{ R}in"*$3J=z;>z{X@Oxɠޓv)u)v#<[BGw;LyΈ/o zbk+VTb[3u?S/V7?uTώo"/UDf;i\q,Ġ̯X3p&wyݑ ؇\äXm#R]w¢v~,<X߁ \Fc7IJ@7Bɪޮ WCy̥6QH'} 1rL@c-kRotnyi@~\fymg:2Rje-yP?dĻ_lܞȠڬԤGC'NTFߑOH*}Aq*,kA ˿C-Yw*ڹ1UY"#=Bט\Ȍ%3kĥ&vf_L׍ry[l4jH@[!i,!iKخ 2qt cJcmc5S+t̴͖z;)zw2( u11CZ^nBnHTmEIk6k(׭6rN7ޔsg2t}k.a2pB$Q/ff }AAh:8*f|e`bVDe𫵘[^[y^o&mY[I'+ e{n3ao"#c{O14䯚 +L=vP7+GF>y{}mY2J w#4SOI\i&l(7u{8vS[3RmE`T?3UE[sTm%{Dvf!/)~@ʾGUU&Tnr&n+J *K@{žWĽX+3ʜ^b|"-9^{(-(A*~PF^1Zx'ĴYxpOa߅߅~WοQ#h|y`0m3 Y5T/m5eIemjD +r7$9iq#nTH\F=9ŒSV g$=#؏琿d'\:|wt@r'3 +%7"쬯΋H6G ę +0ۆcTu iso&z%drfF|VU׿_r\fׯ@,܀$|Sz~:ԍ5 1vceVF-^XNZ nK)Cb7qG{d?ExF$_NN~QSm!X+*g=I% IN}ye~ ț{w!݂\;A,- Nvʐ7%YO c-).o!:ɚblU1!8% +7. W@BmA!NB,Zؿy ~2 oo/&\>Hz)FoT|Bc%  jay~/ȧ!ϟ@>>2ܾ7r`!!`ž+M@m{.~BЙò-#b ̿7a^IZ[AlC^xܽqoA (B|kU 4|b'/ I \ʻb|ȠKho h7G)C3=ȭ+W!7yq:1x."=&J.S*җ,N>zp)obc~-[nBn_r8 f&~ݓɬl'@#)%8]D/ߋvw!o܆8=} +| +鍛{!f׮A.] ^'綤y%U9>AiF 4 +rБ7) &nYBA>=~q~˗!Oou*"y7p6 + ߿ <.CRT#W^11#&]e&.#.B~szލ[; ܅~m9V|{즪 ~ӿ5 kYH Dž/!AvvG!] yp*}G KxqiBmXANڮ\Q2!eKCu +/(wa">~XZ6|+߃ܹ>O O>X>zIpuk6ZH: \*U2|'kX4rfL%7gG w&%^<~|r1[-&|#H-3s}L|"?k[;͓k<7B,\|ry3ZQrhY֖ϰiڷgzD*u. ;+>~+˯CMV2;mN箎LdE\<>6}&>NY)RSoS"oCnpUqv#Hm5pa;L\.rTewXS!: z 72Ke#PAq<&7 O=?9] N"D>ob߫*讛٬!lk@ۓdd9| {`&$9:o !ܺv26iB,kϝb_\8)tjo:j= 9\DlcOI)if >!ÜG_ֵCmOB44i8㘙-bfKfffb&KYdHڞw|C0lii9{9>~}?{>=эw?*L(e]蠺VXqQKjlK01tj*fNИoqe_KJ~=q>|q{ꊊ>%5]'̱na'-j:Ōejfz0I^ĉGXO_<]ҿ %9g%|zڕ|ٕf~YbMX }*b3^mzb&ЇTwwd0M'_ϻ?|QuZ_k>{cXeȖd,b!16! `{H۫h9M3~6dύ1 NW*9\]l)L~X5t9LM9\ޖ|/X=JKю1sVxKqybi9!eF%Lp/ꑒkBz/Xv{\T@=1IZ$2ӤoP-pSSe^O8|T ze_ЇI:ҾR2QariAe;}{Rhå.0jW`^6Y(ﯖ fysDOaOpZO٘J:.)mkq# +7KJX[ﶅ\pr=uqh|ex4By9͘s(iIN[.C;ߧj{G=鰱k-ѱ/YC[i--'}Cl֋Nޫ!NKp}h]五Sft [V"[A'~o«m2r>S x*g5v4lLth:d`}0 t|>ܞcZ7-&ŏЃex!9%h ˽y|]e)geť:/S1-Z';$}|ݟgLL!'yd*Cؓ6ma^`WhV:*[cWQ+\".4I2\CĆ[& OK]k!݈8:vUlQOӛ/"&"dkn_Ez@JA%6:rFk@vX{F.K Єd + 'vnH zإ5F5d2jSP;Y\CƆoQ-Ӽkb<.`n{q6:jj۸;C +٭{wDGީ Z2lKڐR^9Puv$\g3wW0i{+4ȾuI-ife_Lm|Z2PknS3OS1.)wy=S}|trOMۜ'5;NMJ(#=qb[#U}j:0?^Gt&4(jĝ̛s\HՎl|Ѱj*"Jݎu{&AGڇca]9撓EݥVIk+9UbuGJm jgo C5lρusȴ fWp(66)n^@NZQ94+,jI +[,B|"byTecѶg?zKW k݆E6EӔhR1ѭYLT(a=G .;OzM]5 N1-^kEtl3{+mI_7j7YLV OĆ\v@GĆSꗠlgM#ꁙtl6 &Jj_/t`37ŘxS@m.˵ +('FUX.f+80JB]ɱ n B6P~{qtc4TXjz\i}3 KEVc4 A ?Yw6ye[#;YnDGNrzd`=bZtk!as4B@:J.I3ya!U;+(کA`i%nj\nX9-ůpVK5zԝv)%zh-գN͈Wb*b]lE(x]n_V ˭৆n!- :Fl +RmtLg%80!=u{nȀi}17bh[;gbF)bUQT݃'9зO^lO5ݕJ56q, ~068O;xwhhv +WʂJR +(8Q>1.$ N븬 @ֹrTqH6I.0К%dR+ze4 &9V^%=!ts +%Pbxv\+m U7U|JLfe Uv5`HyzTXIQy{[@nĴlΑ<2`jN c=3|/O7 endstream endobj 33 0 obj <>stream +{ +¶h\BrH >{OlZG=RdN?AgW- +1Q5D9xƭ@5944.pK5z3|V&3=@;=Ud:$%>Xt0Hr'׋jEm/Js;zo Zҝ9pxÈ +n 4c_m;?xC Etʽ'ևel2*9dNK?!䭶269\5UVrA~dXX 8ܠ6C)2Z}}{ i&k{c^D:7ݒ}l$!2pc-¼Kxİ x?܍s8"4J;Զ;LjDo%3L[-q&Cu/ZHn@+86b~n iæ~Q@!*ఊS=Km ly5OypZSX/FnXwg!oF)gXyw| A^޶ޜpxF.g_d_ޙ9{fyrdwW@VͫKXwekiꪼl(A/5ĻF(U 44WHiH +HuupTMZsBDKD]藡JXfL RkN6> ؠo-wĖGd ]P`"TmioX'_dZ +} 6XEjiq}<6p{^<x \a5*) m/kcF&! {Ę y9_g%P|MhDF #%;ؼ)3=2T0)"xTer&"bDC,Hu~_[f=j 6Um).Q+rZk4+De6eUtOB'W;VYٶw! `Y!`˟L[{֎6A╡"|Gu~l[kZ0vC#CG `}4ӻ؞JVn61`*DKZ)~qmF(U!S{W'V]pԈUنƑ!-F&H%hxo%a`_k[q%!fW<<RtG5BLN 5lup!S}K˜Z?x1:ZsG<+a(+<=B:ȽG|M+h 1墎_'XHheX_|{s񉪿.98*1Ep/*E_[C:H3;,x~!(fabr +$ӱz5Yq(g%igV!(9(A7螰[{+3qA-xsfwfXDnpm0۶OLԱK %Dm +̃QGm\cG~Js8^wmGlkXAy B}4YI(]~5kz3ώꙘ_8J"ɽJw-A^4 +~ (@u]# z40B ʞHQ VK}"xD5/݉G)p}m ȇێ٦g^hο9WO]~92?.AfAZGcTR6[c onWuS}ӱȨ'gș5ǟd8f`'ZHۋϭ _sg WZ`fX'$ r{vLFB{VPrӿ3X+ozWYG+uDmڻu/rl i˃I3g~0 THmlzj[eżBBʧNUsr Rc©eBĮL>ѳac}!es2q(ug[zݙƯfΡ(pvo/Sҿo_ M6NV|SSY꺒/q Pk1|OLC7!1,チ>SQ-z0o&b:|ÑUJ.4?)h-"rB&hg +l_@xY5Tmz0}(ai~q<k}lnųH{{봺QȓEtiO>>s1MCҿCϡl쮽,Z~mH,9(8ґ6Nꗂ3m+M 椨 +XUuC.հGz:p`1H?U!)L̿,gVS_zD4*^^!@pB +xoឆ`ӽQ- 2/n-+Btoz7#շzYmu"@O9Nok]BRqXNmkz_PҳZ7+8C_,o4 'F|z[3yVDG5LKB;X4pTkaZ"D$2: [8=n5-n@_;EȂ]ul|j:%E:HcELwV\fZGoxWK:"M۞ԙC~q5Ȼ#=i3v Q~b$?ld&pcVё+"`9rQ9s\q.5|gVYfm m]'iKȹYG;kTP xCH@oŔr&۾H*9E0RQM#5?o_[uBmlv8~К티Ľ)s Ɣ8/>es8u +}T'+MK#fg5l) bXI>:r@Gzbllm?pd`*|eHDp3ZsU}ug D~y}agƵLr-saUlv` s8SWkx/MXaF%7k}W̓&"H6@%F՞Ds 4,c&W߳}r-z{H+;n>̻(`Qiis߷zW4sT{-␜kҀ cmirDO)_;V~p:ZR^\Hܛm|ѢJ="pзrʾֻ;Ko5=m$].d*)j+ kq ˠ~-!ӵ~c*ܯ"GeJO&.XkظZAOpM7?ɩ-E;rMз+4PN.S*&ҧĖt9)~mYQHEz(--&imz k(`~Ma3iuO?\eLwѯ?['|d'~NJ9s8є͵=-:+fCR|YXιZT[p>H`85x^ Ŧ^Wy!'tXoElf|d +`gT(^ЊJS9L%mP1 +Z{t4xLKGf}0N+'%@[Xtq%A%tluxaGkoHhk9l޲`:Zr.u%uWBw.7{0ݖ|>w(%oN_FnP>ӈ6lRGBx^l~bkN~0bjC +H63-<6S'5R + +*qG7Ÿ]GMP=t4|s3T}g\k_EDVH@Ƕ<S_AnGF L])qA߭S@O/#Jtky*<+T4:8z)]5OV)yg%?YomsJ2.,#.^OBqUMNuګOCLiS=(e qFV5B81KʟV)MbT|s@˿'wdmF}ۚ܀- ̷ɉgڇ+2dTG% m1q앢 +ij{?doֳ.Ip%+ uwÍ5d֗%pvLOlU_Qt~*1M<wLcb\ػ[־s>cDjZ28%6гxT*<:^y:\{GZ;gA֙k؆ٛizn4Ru2^v'n|a͊x,RTPN6yݿozbg%f_5:#f4aOB +h큺N"'Kʿ +CCi1.``dPU'zhcq0i(܇i`-BRI0rc蘒Δuֿm""jl80Ha"xsB[ "ާ5z+IM~95O'%TӃk#?W{8 r 2 +s,B6gf>Ѓsmsw:BuJGLk cr|2d6_3kxՠ ( +o5ub"9HixczmS,}4S;T!6UԵL*6=2tk|5\q[p1_~+0L֑KM&D'!U7ҶtX]cD#G wV\土?fo +5J|3H"D,BPIprG+=8<ָ>1)# +c7+7!< 1Bk4k#\g\ YTWt33'V&}J+L:.&5x%ݩi]5Z޵9XޝEfUhhEHTc\%n,^(QJfQ@ȅ' i~YpbP:5OxPBD\vTnj#w9yÍ(pc+]|fv_pѽH$s}5?7 !kxUB~3H(iz~1abzL[ID}qI:3l.۱+ *~"~9'F#9sb#3j` +tk1ޔ+"V@j:,y-xKmmLrq$(ѿB.]{؏8;ڱt2y@ +z k! SG[=5׆4USӰ66:Ƕ?7˼z|<::9Kܞ'׹/ܛ,`ҭ]kYXyR #N;VSu |·xH -MqA2]By +Ŋ9lSl)Y=+|5yq8Pt''wFͯBw 9 yJ*q-r wzTM JJc_"s~#HUV/Kڛ*.U~[8檟u(u6!,w@r,l{4;ĝ;smxG&6+eBZ.aOLҟ +="Ằ^l5݃AM;'wEMjD Х~4ǹҹ{ xZn['Ȕu^ L$fBk̀*:/ǖ8#fߛ'N\֩C?tSImҍa-Sjo묪_g!Y_Ji̲n@.6?  YN^~㪏5GJ,!o\蜃%D[+Y*@wmk氎21ѻƋ>ֱQ! wl ^o}D h%B6`V^KkgYdIDf_ݭ[$K܂F 9W< zvःEӀ~sj혌YC~%~OxZGIpŭ |Upy +uHERcA$nVh(awsҰ̮*TZW_WaJ\[+ꟄͼɘUʝ"\GFj;b~xV:6wk*ޒ w!9ƾg;-I y֔C!:o] 9crR}X:%gBm A^o/s`rrc r0bwtwe`;aĊ ۓ/=oL#2.mԿRW&h6`ڷW9ƻ. vb>1Gƶ""qO>;O`e-brLS4M}oik͊[ +q[+'֞c#۵ζ_;2~e,;Vy=8tkk,0' !_٥7YOp*n}('х|Wi5;">1ulaDI$yX2יu?uԽdUz1mOWug-?,mOmGj:̻ +9 2"hs%Qޙ}{3RA ,#n}㗂bZ?fR6m=tGL瀒K;R.4 MB}= N?+]E]K܋:X n><Cd[^m-w WpV#|JjkLoе!KTOCmi鰈c 8o%b;LXХBVϾezs +Z>+c|m-=Ւ0:-wtK&&%cnNH="<4`0x툁81*ϱܒ5Vz_-xiGZAQK뚗aŷՇk5rdoNf ok%@k~/L17>Wh,m hٝL{ݺ3xQpQQ}2{)<ݜ"Ux|P@tTLǢnrxG&<6AC*lO)+mG[ M~_ǥ,@%ordv:˽'UȔo^~#Ľa4>jHPsVœ/stPTP31J|uP>ڡ}FBU̜ystk>j߈7YNIŀ6B:j"SnM4'ڦp[/uSNkLq! VM۳pAup iĽEb_ͦĔZYGqXid%I )Իs pq@AޤQf29B58<+a`!1j!%Uۤ4! }!voX5BM2om;C_ݠlt4 v&A)ZƦ;f&nmV]ӿrK.xsۏ5p.#3桯7#MoVـ?@ljFbl_3씏;e=A ulwE \e=KU㫝ԝaR\D'}A}qFٜYFfmh,~)^&Zt}m飕!):ņ%Xy%X pe># l(;Yywo=@)eo=SzYKnUO/yE7? / Y7YU?jJٞ< ׸{ 𷈐Q!%&xXܾ&tN艭 ,,\s-K߃2Rc=n!\^Gٖ)Ls䜟6x^VvEīFȀI(%BU<#vǔK!:7PM#k<lKwY(2˺3)[Y1Iěd]H,ޝB;%G˜g^EGW@+芯mp-'W| [|׎RWne;k{ZB>WhM+#oJgyZlEtcc>9z.<  +5B_y|~ +[9NR8CL zW#Xw[C,T/^'L/э^}T5je~}bwfO͏ڞ֛o}׾H^0󾱎4؟'l~}C*A5 l˾/k_SAP=mQS̱31qݱF)jozȪzf[~XJ,3 ⩾rGDi(g4pL}ΒWS k;YFN-CHګoJ\;L#% %,-jK~obccH<_" +4V[^NAZf0%_+yRkKPoO#WP)fD6|&_Ia +7ilVP`f_lߝӶ\ۛly* +RW}8~ WyEBx#C;3 [\RbGN5\Ϋ0Z.-rDEǬ 5j agi| ":ˑWg{\=JUƼTD}S^zR|W/5O^j&֔K95^W+Z',PM26ݶ-’oJn|JJ~zD#0U扆s +Z<+mgUoSǔ[@栯7Z~vn /a -3ǁ2bF%eܺ??=vUu҃2|}P-:R TTǯat ;7ЅֹG.=ȄikJKXbp: `xĔ@U]G% ?rP ԾnN~ճ/"::H3rGq~˨~9 ؒM)l䠁A}}8F.Mӊf3MM5%gB|+}wiثm@_ sn 2Duh̀oUz?Cn))•.`c#j6t}{oŽߟU]xY3rTr}YoիT. +ޟGnk +ҽ"Yj mQIQ3dRJ*L6\s2xW {,[ãMxM". s V=}xcL݈U7 NgXa˨O䴌3LJTs_]m{y_5]&eAbnw Ӈ}fWZ`2Ψ?+ץ +~[c *i-ud} ^'א`ٵIezkr,=K.1zgpVb\MT*{Ө#](Kc-%(#T#p{{,F>W~Bn1YYuuᶧ&~/B?l6?U%VhIA/g?jNC!H>X%tV]lk}n]i{9xOmvMD-'=-(B"4<ŵJM^ڧ'k_V>+XiMhEQ_.S3c{d}D}^O|GeW!)j~5}g b]~̀>dO, CWj䈂 z(冞[{-i6`_W= Dܶ;\eyt_Nh.aY{޹ufכCտONoa5_Gfz~1zdta*6ѱJ +٤O/ ϭi'7nL"^,ѫDKkԢ[Cul5ښ%/&3If&Lڤ^LLb4{CA)J」`{7}~?yps*Z9toku}r8->2z|ipX3ࡂxY'휚/a49 -} +cX6?W99(| ,vRo? 2@^q5ڧ S ! .f#]~h3C /657 yqL~9p^EړB^Z֥nW˞#mulMN*i^3*//ܘqj 9FI*iʊj_D4MkO>^ I% $}͊*\5 J +dᆕ6xo*P9FlYAnJ1^$+LEHTۻG_tt,ꞮXswGeb>35Y"g 1~͈_f}ii"CYexWm{Pm5y3|Ԓ Dž5XPYNեaHgG53?(AgL} 7?1Y~L;KkHɿ1}EYRu5>JoEOOa@57zAjv3Q{~6aÆ\ӗ5_EE?^dwZo;}K[.F%!ϱVr˚ՀnjZ_/jik. ?xEG6R<9.i~yjxcvIhQjӓ}U-eEM4AmM,;Θ }D~b` hyZJqL墪3)"2\;ʮN-;YwKS`_bgad?8BE~Wx*/!ſfXtsoy:)ed2r-1 n{=?-SrMHQ~;A3>tòZp)G?ys jkʮl:;RN.@)cx+:x~vpÈ-}uĘv,.h5/=~>DH;fjMp@rW{>JLoe_{]5S~$cy2_&4v,[q42M"ʿMT7/ő7fu=-3di'a@d| Sy{ |OOh0oʾ5h{髻+>98k[2Sg'V=v0(YG+2Al{-eIMU_!OOao3t_Af`K+{Y:6+j}o4l0d+L .~2{Qu^Sv$Hnoc0XWwmLI֮ + ^|㚕аl"4,+FbdʽЬ<&,f~^O,mM} 6E!; Y~xqcPa6b`bhk [_-k/V4mb(m|xVi%z -5SOhgAIK!~6\,St~2o`kU;4gŁ^e2d?|?MRg~W.ZO+j[& ÷:Z7O,|MAE;7.l;#qq/gh-qd64aT^S~NHl[aT̤1>ܞV/Y[^Dɺ3xlEhu+ e;Q~厏ZԁXrP  Bq+@#oS|ݷI^i `pޡt'ZXI kjՙ1n١GZT4arZ*j -Paqq~~J׎15a6{wEBל1z/a;c,ҪXj&D䈌 Q[|ZT\|J"i}}L Iڳw\YiEAvtDP,_!c 1j:}G7`W̐\^ЀswDC'a]HJ`¢jATUc,hMhIHiٌaۨfa¾s&rO!Fq83o;6| 9Hs+Zdpӣ+ږ׫xά [cU+|N y{0N~ٽY>-&i#Xy{#Xa:fs5n\A׵Di $!vL~Fb0Mx?aw\؊U*g-OS?ʮU`Rp_z1/k}n~4+o}k=vwVάIٕSe=z`RgN-OǙ͘O(M aB=&6FQKWƺаAvf>p.J˵Arɘ^?3눷1:^{Q:wBׂ qb3!FzZ c16kMڴc4=Dt? +AN*l@s*XqӇM3aRꬽ5G o 3A=?J{}]"0& +{Zg2#& y˪bT w'dW :^N +r3(aj\isS#撏cX5<3 u<%i~ |rN)$֧^wW=&ޖ ^lO5mW wO8Y8 +] +igBgynZ۳mKdڅiE!lC94uF(y8ԕ@V.k is"^,ذ2 `A_Ta!PES;-<6%40pzŖ$c 3J'9`B<ݍT=sõ51JY5V-ʉ~qQ% (mӆP_ Yk6HUqr5~f>OɟisGX[t?S g==؆!پ{sO"WDZmApQCus)zSTΞo0)$&* #ȗ3ljbSx:S|)8b;v|LNWR2X[~/fr>HuOWF9BꪞT1Ow3.G;/Ps *b՚]@YՆ3)W~eB\rqAd˅YwS3à+1hdRc_lAʺ^O?^PVh&T9mf!5/Z-jfc VJ2b t\3`y8SbDڴ*kvjOp) UWE/~te~o +DzDZY9@&@9;5O{jZ =o6myc#pV,M[[ҼHw9(DۻT_&OBjbEGQ//oxnCu_ 23@@*qvkLBIoN}vVs kB*kN:iv=U=s">9-@?zˆ_%sT>*zH(x7Bm]9(97x==&P3\?l |}"vU;8{T\wK95+ޞ{t9b6*0\~ N.FʍQt=IXMo!6KI! {K._f,Lݫoٱ/S3"L.nmxm}]ԘPւdn>Xῂ"wAZ!u;sӈflL- 򮬓ӽ-ϖ-vTswG(y3jB/\sޞ urw\Sc|c\hBL8x\&hy3ۤ֞I%ݎߴEW} `_'I 3j k,^tmptm:/p|oZw}dzLAuMAiVNC1(& C_JDT5pnƒ?3Ja_Ya&ob{kcV;WL+ <fR;Fq+NX^L3tDmuu/@16b\8,ZƊK:RP6De VfÝU"? mesRJo /pS +j 7*R$Iuт̟A+e@x۞^}oM]w ++r~kD9cT]s#*B-CⶇӃd )*sAh5ЬU 4}Xs̩O?k`•RLfEU\ӲΌt> -3Z-r,GU~ݼ=~ݎ,]g]Y1 #JPɚ緜X,2D3ܔ긨u[d } +斔0tr?@rh?+f|YlXqVkU6Vã=/|nJ^5#زE;,$)k?/*qǗKb麩+}@&vf6.< COܟso9 eRK(]Hc WE/9`ʶU 8}XKfLi2Gǩy@w7, a{q1q}CГ- 4§ pдbǖ.E:RŒ + cBMXS:BW{1 0b,DQ!501R?g݅k ҉^,eTϚ^rk.ҲYjAlzpvLEĄ. ++q; rv=Ç}v|޾sqm/*$ +vxn<ʷmVXٚ 4i-뭋*gٿqsZzD)="Vl?c`4.y %+6&p@Wrj/WQdžHz +`{,ژ&1ūJs,Ǵnvs#f׼|rq[@-#9=9ӕ$GoMNj瘊X(@Vc6eGᎻ;۫v|킃Y0FyU? Vt؊e# 1j䮌:F|jWyd~M3uF٨~R窗XpRZ#v:`ls74T,xxm{~2xQL 4aAK_01}|-wݐQ*&pΆL QN\CxiHX돡֬  ;$aRM4-4~V|S[>p#} +`} iè!k#+qōo_ >\]"VlDMp#&jT`$W sf:hΧϻzyFfOx7iV M=_Q3jI{aoi۝d :Fϸ(~*|#H'xiV78p KVlܾ͒kEi{ޭ 5rlU?YLWdgrafRͪM@eJajPrx<.錟SApk..ewo{Tꙵ6pAL^I9*5ju,YUGdkE嬹u\D$eo2la -gJi )Rr֓;^ zE at&>6|˧g| ]0EEc"~^З<ҪPP{TmT0uX),)5{2d/]k#T?`G{Dۣ}/ģUk{ڂ5aVt:llԈX&Tx" 3Ý :pRh+lϷ=X|Hu3jB}Ԍh?=2E}`rbet)-5)N{#ڶ\[f/]<[؎jKEI-`<[?AA!Knt劗 +]{"nC4G4BS,X+6(,'XQU[~:.:؍BfvjKԆ-7u +rd8gezN졭Ǯ=ivZ>[=VWפ٘6b1wVhݝQVM)uc$B7̻ ۠8ҭ SAEWRr%:; hbZ o{;!$7:BoIfgJۍz&)+w /0pVzw$U/xĆo${b֔мˢ#άSB¾_oq>t(b7DBN4auJ)7ڶCBvۻ"VRø9lOph6ʡLwvǹ=;؎0 wHYnYz T2򨔙c&=rҧ%j-ϯe&ەa\d d+~6>#BR7h2$E܅fK;^AϦ7rQ]0X`dd؃<;5٦OPz :%Ҷ^m j{E3|TնaFStu2"ۆ魡< 2,$ϛsV6"|ŷY%c 7p|Q%C7^z뚷HQ/ b !(XS}1VpPc[kLC`_e+)x}T,{>wtJ4c.riuw8WSfF]ebްG@f6LZ6)#yHTqĨq5A5"ko6BCc3 |o+4!ʐ;Q~N1!&e+tۅIs.l!he3n}gXt#e@췭=l؄[rt)CL_1dx-@HcUi*JXxU_` Ɔ9cl-0[%R%bVIΝޖaVw7+@4^5iS +ZiDKvFqkPfj52M^SXl m6N)h٫R\՜s{\畩o-zp3D^{2q،q5,CqDuХX]?UOq=^YIOsŀ Ը:}.Byi;WPSYյK?y >¸'G]өq/[~`^ %Y3r5rRѩD,Yʣ2bѼMRY1>Pse:"2çKY[~fٯ +-V݀4K]P'[p9\)M'`p'KPgsӻ~xp;YrC9%45ɲCa'f`aKBHӆKMuq"U{ӆV/\X=ýG qό;w)?e +;q^!!C8E7??dB|)K0$L.渒?Qw˫'sJ߿~O:&Ov0InWoRfPek,2g*6߄0xS(B_3[M+.F<Լ18Ѡ".BZq[MSGr ߽xwQy܏>}8U +1s`ܹqYB{;}r>ȯ~E7n'T}ԯahw֪w$q+S;s1ȋW$u)p _qP~^aX[Kܥwk>ͬ>q5HJhz.牰^r+ qnOFɨoE +[ݚ1juo0(C38p1o4OTz؅GС1ǵ9+/ǤʹGl);G,(E_eя|hb +L{q6b2I7j<_ڿy4Xcnm WQx@(镘BԗjλhLTШUUk]\2LID O)wa*[o\ˮ^ϯ]Sک%S%5z~4tͻixΌ)9ZJB͔+Lut:MUS +/%ㅻMGCO)R=G9br5aN)L}j-W}6aJ͟V7\#G9UDM|OHGw`&Z:79Gf7bhBv ^-}\8p֍tb$PA^(vU\ +ۚj{M 2/oӑgrKW0(IEt(c ~3x(^CgɒN7Mr9;T< *.#кD6S],5;foۼ?p  E#:'E&erAMhUa\ڧZ+L}?x ܄" 1$E'y+JWJ 65lf WIfpǝo3i2K{~z;>}}y_ cO(3boHYu`t„[UrbqVbEG4Bu}GG.^+{uMdy o0HvEпIj&IeԊ( <`)%Cf񀫡Wb+[R]M@kI"_~;F ̾'H%~LuaΓZ?1zb0D4FP{}鰱Q0 k$cy҃4Y@N{>e$;8'`vy{ϗl6ee˘v^COn:l[… KH=NfZR%Lb1L%$jio:Vu/mo[7-,iVYZ; +RPH p<_:0WRKɷ +ODvbZ +wĘ +[ o-%/q{^}GLȕ>"&eW(5탒Pr+<^W+OKBî8 1bzaʛaq͍e`Z͚10Ö\2e'lB_Ϫ:+mpwS1f贈Etl{>6VlK:Z(͋AwP?aAF2RluWˤ,MV"h.ǿFK޲zTya`ªmk..cɄ8%$t{.q GW]1 WHPQVގQ01 +Nf?S +5.$.(Lݶi +j|f_ FF=ߘy:`E6!Xd&-0&wgXO1F~ c:Yh/ڥiG/}gu?'.:qU |嬝qKnjYQp|يи섗-s#x9yviN!#֤%~Gr:@)$}ȭ@Yc.!f9ma.OG׭STޱsb$7⢂\rFKN.1iQZJ W+ v=zĆ[dB vD*?WS7=R\x@Sߌ +zo/i<$W*G[Vc@8%H[  V 2oNo;x9;NDQ|0pj8Qbb_l|d`j|QBkĄ[uPۿSBDȗqRצY1$OA^M)׳zlټY8ou=WMΔ; 9%,{/;8>b]ùU#d誓ҶL)PrdŜ2.m{6.+Щf>$vəEZBs@XpkWy{k/|g]4qQ,~\siK^-.7Zm5+aYA>20v=03Ħ4eLT|{В%%7ɡg&I̽ l 4k^Hkw`Wl!",f + 0еuAK3GLf|iHM,2^3NknxWg9G]fL]T&4ϩZM U +\0PPyK5^ KFuO>N_$ȧiWotrAU0+/Q<D53 #|ժر?ϊ3ܡq:eŅkCS'1_$w3NZ{Fi%>㑟F]ϣگQsaHWelJDڂ+iZxa%]33v*tE̟w1 ?zMK77<17oxp Cm{W殇ԺcW-ƨv}/`P|AQp%G ZE=<ӸmX SmT7[vTP1a͕E=4cZ\7BԀ)A_sefjoŞEd9h?/iZR$"{]Zn{L:C2lY~jˌ.6Τ}!-fǎ(6Cڀ=@-yGdn9cCۜHs4_3%|5P>B/f6\TpDI_5Tvd[c4d~fQڶ7aAq~K]74U$kÆZL0eGnQΑi5.t0oG5u+8}ҙofu/!`Qs:[r>΂v8L09_܈UTߕc~ 2t;f`Ƣ->%oXQ;.l m[ٕ3'),x:_z{AVzIoSxQv{m#/u{GZ|seO U +K˒wݐ/! +8}~?[P آDx vF%v^;2mlM0t&ZOY,hφ)l{0?xgQtY|ՠrEm&%Ot?^&[,oZގp<ɿ۱kF`Ⲳ޾ u8w5iv>ar6NO>px)>2{nI^|mr w5=厍c8RǪ=Ց[}W--]|Js]x}^ך7`'6ѥڎ䨼~}GO;3mBPquیXRw,yԞc\UXW/ۓSLt_@wʹs~HGj/?umiH¸QluB>aƇ3wW3 mj_Q? /Gl?[iϳn{|N;3B)@wZZJ!$Zk{|`B:c%Q3P*¯墎,5!$8f`'Ibu>tc=em3yr0Or>-Uvi +l#&!橪>$3X>5W~2=248 Bi2{`cb~Wy_ᕓ21+IpI[̣Uq{}lՑ ? \.׈5Ւ +*ty= ϑ2=Zz[ͨ? ,P'uBEG-nAĺuU.1@5WH_{[|#'`;C-Ӹ8cϢ'1MHuiU^9Q0JC3!qi#sW0B(9yђc.CR6W%BМb /W&k?OL[9bm[" Sqk47yQw}sH(fx)2ڦĜ?j"~ե$g٤첍r^OMҟ+hW`on_\`#&n+_yzKӘ ;Goo?Tf7tsFPvaoZF耉X'Ls*Yۃp Hm{sd`"7ޕ|4I~5)ZG,ݕ}2Vʈ  e*ͣ_f_Ɔ<]S 8ܿ g|X`VG8IgIߪIBk !C"NVؔ[M.)5߯ڀ.ejpNtꘄqR]&"T큅̏:&2˻"̟;ͽ淇sOì/R.5VNh\WYO˧LiBQdnt\t)(v-&!XňǦ0M[Fw4+~S6U[_#EPbɱ];1Bu_vm=v 7B\uNswjČ 41p:Q~% B3-.@6zwG隨!ݝ[Avޙnlכ]H4*gn)sOI1_uC7_ Wa#ڳ(⿟%|n.|8ů85S,4-9%GA)<\tV#ږ3`#UK揇hY摪;ƴwF7˯> +^uX&^5,"-ȵG!:ˮw~x飡AͱE>mzrvsΙ4fk{ױ`iL83[Z)5ohN+ ӓk=;KrQOmh6 uS9u0HI0 gq6#-cGwo-(:{(36Dt5G'g>~T{RWǠf+.*0Ly_@CqyҴ&rUYmWyԭEG)߼CSӄ Ɵ ?I)Vyi]Y.fs7ȻݍN[v<\f/M~M߶z!6ޔ\%܏|bcN1"D()ؕ=gFX;gbINV=2_Zj/ׇ==14[gy{#>Ze/n/]reM̈j4lr&`@/ +QzSU YelvC+:riŏmnt½Z 4{5O-!w:?LjiZ0#TC]o@VaOw'1xEoP˂9K_d´SJa)XįxC5桌Uib)kψ scS7'}+F~e(!Y$SUywVgO /󯮽=>V+VʯXgЉy˗ QOG i#l#u|ڋ! 1v 25ɰK1ɀi1'_:*y?XšyR߳' ew4npWS4#Fo  Kѐz-03e2?O]G}зg^5<"R{+!WvFp77jwW Ϯ?E޳Nto +.M2Z{{l╨[ +O-*}kkus+o`H_9y퍒4лȨt09_i +.u3P?GF`f} cX'p=O +.8Xv."oEC̹n/?E5׎qr8%gs9q⪹cݕ`=᪪1gShU?@P#WF;(ELs +s8y*[QpIV+laj% ?>K\3ҍ.D4;)J$ oo)r]՘~M ?CN))"8֗=24LLXi/hh:Ԗ>Ŷӻm'M$^ t>ye-yC啷SORA$=gc5KXf&b.k׎WElyFoߴ YLDSV3BIP5\P4wEoWD|\ U~Ywljqy hX? ZnMߔsКM\m&vk 0)9/dLҥ&%qKBLbJZBp*P̾[|ţbZCJY bPCh|j+㔃l7 +_몼_f!'DȝIf>vb;.4E],d|?2vzDٜ9~d]L*iJ%F?>e#+q3!J7㴈?`~٬0Lh"[m ߍÔD0:C9J0߄~`hA37ר+]g~ezKW[o+)y9JJcH;㐐u-i?^$}љRKic_-"-P=5O^ =dhy'd)me.ۋ]%mUwJzDC/YxNo)zݰkg?$W?曊h0{k>175g9VÞG))>984]G&13Øȭ~TNx29q>7Ǖw7s+3tm;C )_v}=_7:K/QJ:[]XaCccF8¥SId\KAu^CCvTʢ-Qa@/5H0):My)ZSJ<7x#R xwpQJK类l @iڒNkqy!f~{A@.@9qBeY(f?il `ֲsʆ_ڡv88>A[(eh/䚣إ|(6Z S0&9E+#MQa)j{`E+o[?٦))>5` `61ֻy+k_&Y?~Vyme*-9me@.51ʦk+K3N3sT8x3xH%.uV#0O? s̳ ОIfW9W\E*^Kï#~-o,L0.=X|(@}LMZ4\,=3jRӓBc/үn>y&g /yQp5)%$sS#*o)jݗT݄&^f|=em)wmo;]F--{;u(4|;67F:( _"¿n ;KL9n>LTFz5q5)G Wb}rLq'(iY3I|z_ս1V…AfC 6;Ƞ QT0/q]~mӧm4 7'Yxu< }2ᑈ@"nl6izȸ+E6FAao`d8v=HShcV\Z-g9{A3P_)9r hқN7oafMJJY'Ɇ[l<.e18?LNuHx[ak~׷d<Ϗ&_3̒zz&, $5,SN-I0I.eeg`nZ99=rJp l yMLJ=`fw2~ jbdFk`w +>doݛcxYZf 1m?r ٥G+bI;Q #' Ok|w3\mߞ` =ZlqɅ(cpO ,6 +C{ $h2D*ɥ-_# :2RJo_$<3-̹d$pԭdħ^fߓ$\3t_~ x]>xO!ۊ Oyle%iο({Qw ѻ+r/Y?/ʹr#Wr}iaѥZMO5vJ"/DVݒ5eYyyu*)tXHe{)'f{4jSs&[5 +2j "AT1Vt_B{ÕO׻(ֳj[nm&. |/k&lwdMc!Cusꏆ3{|j֩`M3Lrg(EOLPIG "shޘ +,N1",Ӕ巰/Kϛ'[TuKUw@=\]xycY|0B}Xag)#dMP!N)"{Q;,uZYeADiiR.s  w5WDMJ7_ͮڟB7*Fi |# %5ǃZ&i>dKrZ怶Iy֯!F`{ 7MӔµi>S*@ZƩY1Bsj&'j"3x6H]xxڅw{+O䪀Ѿ):ʧlJ8^zDd5)ъp,:( Mʨ[K<ґRӷaw{E׻v)1#"*n_A/Nab,uV'1GT,CҲ]2j_ũN?<u2C-8Yij́8>F12VJDr8m'alG1vroᛣy70}?:j}qZ&ʻƁ2ۖd^D{yYem )Bk%.}kH>DgL:{jf l_˰NC.ͺ~m7E*k-֋kECiX8,>4}$8%4Q0XpYR5U1+l}0MN]Gf):B}sx0f@,3Pl~ovףL.z⫻Ca9\K +,76ZYƅݣrbeqfQPvekg[:.8 EnOGYAm30 :ޮ"_Vj봝p\| :_XKK*= ԲN]godڴ?NK[&i+@*(ȣcAB1H;^)2=GLs˅h3˼K./de;RnԤϻXnd溔,j^n%gy}O9m[rQᚧo~xu =p*Pq K]G+Mm{SԌ4b춲1t[NRc쭎6ٕdV{O=*H:ͫnkV7Eϴ7 6we&hRi#=r]۸r޴f׼|nzu[+2&qy.Mh&VydoCzP3:Nu8>1L"W{ +o-wd_~?fs]3CZ x7J3Or!7oZF=e y{5 3k?L@F;Zh[p˃œAn+8JO2c͙?"C>iĘQF]V^i>}_o17nz +bI냸ƑH<4"1[&. ukk3}?F2S2=*J 3-c][/[wA>ٰp e27\:FWU+յAv=ya/)zs{8X>O4ֽ^2ĩ.ǫ߱N[ؔ\5lc/fzjOβko+ V=u +O lɚHd=Mw9\OE**Iۃ{69*8~IlbpEhi9lq"lmeCGiz_(8e˛Κ n9-Ǧ|YC1\fo=g1HNZ|d&u"GԼ6[#yt"GAm R|[̈́7DuG+- JB}A-)yylCD/qj=K}^]1M}جm:ڷ&GK_^"œv/Sݫ۵ 5ۮRA0 "UvymuQ}pn~_" i jkqizo;E }3+C{&nNВW^;duD>q%:!'g6zOj$'_}pUUsQV .2Կ>"y2¥#7G=toA+E'qhR7gj$#e?5td'&3[MZ]#D~ԼV)zdP +J"Iɪk^.as~fb6!pRǫ@fphUGMu[(y#- +G".٪ۥ,y*wXc7NSGˍMm~ŖXٝBŸ٥ +7`YbK"GDؕԪ)DCKq(N 5Kh(e'k|Gͬ +h8.PA֦",3l!3]f]ZjwtjH5a1!t8:ʣgVֳpn=ZZfEBϴ=)'j^݁f0s) brM|✨8Vqo]/|ah;yk]杂=M6is#RWoUiG?5Y~%}wI;_Na̳a|Uʁ8u=13CZqE-9}8~䙥f5}Iٶ\M*B 7p!%5-RzD= +6)5{v_U,$:(pk^ +;Zܡ&\`ݱ)r1jccc7 wr>-GZu)5o?.DžVOOQ\}dj`] =_8Wè +.m ⢖;aY&Նs^64Hf%;/3YsQ':y#5;e>ͻzu.3рḟw>n6h SvǍFC>Z,&UBHs*IxPT ?312|Pŭ%C%Ա'[: +^h>֩H-vx@BVxJ>"XK絛[**..?t'UbMS)f⛢sTM?,_,pF3 $HEl S6eMLL-`dJGJ-T vCµ5 +n_Aj2tuTsCX$Ds*Y0}#~0d8#xso: [* s&9[%M j4r-,|]MSrڭmv}60&2p`N8ܟ@H=hͽUvvX:C(@,1џ\IX<rZKbsS`\ehdY\eM 91ةm}nS\J!ʥ`Yk5~=x˫g )~u蹶:ezܙj7G'}h~]'zN)"z "OYG(_uμ`Z㱼LUNǤڮkz\*Bc&1hY;M}KӖ[r0E/k:? + 3v<4Ea}m4[%{x¥Ľr]3W2 +!$ᕃKyhrJªCY][WGsEߟ"<u;1NvyYFLp |X`SP.Ǚ&)I12#(3f192~S +\Z .vo&/n_M/Q$++T[5:3GzFA 'N 7!<)HEwJ{9-;V1*2FIiYBG͂zc + Z~r+#';U"!M Q t\\`lxGE;$Z֚_:k,cS Ze`蓧9JѱW +-:;t f`FiQ)r>zkU>HMi[ M Aܭ&o ߕ$T<>ZWب,IB51.&``W' 7DlPK+YPLSᲰu/+'+ulRJ<~w.d4*)y~d Xhjr/ 9 ]jyH<>B}XQ8[!ӽA/ 9B5c #h}'B +K?v"X* +MgiƟ ?VP`\&S)zÆ4H`I +,m +>pWaCĜjQL8co m V&sy.RکhTV9~eB&QU WoȡUrTspI- +I^ !9Xe5g8XշYgC9M7R\cYPs"F:)n,aŸXgOĀVH9h 5;MK di)V"Aac@ĐG ʽv& +8$yq \ +7VyoԣshOzy-zo73Dxj\rr0EJV0N9 ֤p8+¦jΊ9YxtlPm؟d7Q4M:Ykl +^ ħbVzVs> Dz{/oU%Kd_= _NtBzP;"-e` d8XD_v[q9N&4Kf"Z\44:rs^ g^ כ;>m x~5>GɲᓏO W+n5}aӧvB*nRr7&h69Zo->㎀@qG(8^e ؤi\Mɬ9Y}hp JId,q\Ls]sE(" +\UuAi@i1hn L2-obzBCs gEP|_x~KON9=ÒeƖ[Q1KA M4hPN9GJsh9@3ZƱF@вve٥l RZ=~`2z>AXq)#g>/7Mvk"6niRLӤ sjA}+f n`NC\/+[吳Klirpyzn 1yL73'cn%)xjg]GuUHkg+Z=b@go--ֱj>y ,J>or)6JM,fV?1OܰLAFЙkZW@-AVr)jmRt^b{ϛ!lrS)$,`Oesɘ~1#%x@=o#9xW[z-d^XS>PA 6:]r8ZxFC۞Z"yUI~`\XLL_S4gfxG,:. +h"h\Od _lr֧]QQTJBhUp*2~Uh._Mo/].z!mȡ2,ED{YX$}'KBqTqlx>=>G|[\U.0&ƣU9efq+RU'z>o>Q˶z+YI~=3p^}KM,mL2LL03q m]s3 A.^.>rK0 k˩yyj龘j*o 6ڛegkc"θz*jddLCS46إ[gYj{(>%7S1 +2 xXF/FC)> v8C5O.q/riR|G-557,s_6/BѡQ=DN +}~5qѮ~5ʣba? \ME/󭴼=^m~yg#bON4WfeƺvD=wV t:EEBuc Ֆw w=q7u|C) +-&?%¿3e{[S㎨d%j8^qm2lRWͥKbM!w5du2:*{  jؠV~l4TDu<:ΧYp:f]NVE}^W,fSͷRJ5v_[UܳA|q ħb4̥dCs$lH<` zP\X~Xcz6<ء\whHVE2 D+ϙJ 4)æUj[J*0 .~0!x-KWK>e̱6'ʗM]=<6)`Z94xU7=s)L@ua +ja0=J.a"<>`?~l>ȹЀZDL7{ vJ\OMv+iViUyP7.խ Ao954aFx5Yn-ؙ7z:k/G+@*-nv\eG[הzF3蛓栾y 8~>-dKX'}=Yod\K\*0p||P')by}8C)k?7w923!r%ޣo8Ͳ]JF5^_|{Qb;5^-2=;ȽQxhoU55|2<b'Wx_VE%>$' W(b~Tbr>qp_6E/["*Y5 wKQ96.9 瀎]aE@"L[MqYNV7q4J +XlY0˃z ${@s.pДrʯ#g¼j.֩dW%i>p& |V$67(N=YRW +ܣ \syAt&^,mbO J[S0$jMn%z rs+ʑF**msqbM@q)զ)||siA61%p@@|k0,6ჁHR`-?oro_~uI7'!ۻSH+LSBe]ڦ٢h6q츁#@ zo@;yNYЌvg3\̽9)wog7t8wٝԶV80$#W[wtOk[g;/w;凁=zë+O wo^qG/k; ;;yq߲?׿|oVg-?Z5gޟM5fʹ<4Ӌ;Z?޽|+ݔgϓ7>tOoO^otҥ_ܮ_>~3;]3CCkF37}oɢ ڊ:^1/Zw~ ?:(Wvjüq'wN>n~tO}|Ә;gx^Ư' ;^\0q/:[~ۿZvOL܎sxԮG;yYW[Ƽ4q䕦;pánoUcG뗶ٴ /]ҋ9l@w>i載nЫo:sW秷}OC/ +Om%_o{nļwk>rO+nms_1|lc~Q?]sjvN8J?떵GNl{7r3]aSmmvK33|;X44 =솇N\sofm?2Ks߇>[ml⾁_gv-9s̳uشp ^=ZFt˹}=pk:g|qL[V6ytcZ_6}<K.0tɧw,wv?={ţ6qp,gʃx|/wo.hh,˳ .gcvwul^__|1ޚL=ggؘƓw]᫭lylQ_3]K\9ZAVrlqa ;W,=e{j;~1]?z獿{3K{ZOoϸ۴̆_79O͑״=Ҏv?΅΁wbcm^3[8ɥ@?iaG}zvwOw-X]6xjkl?|yn=wV|EЋ_i'=Ѕ5o{}?[~_~C?~}ţ'=}wJߴt^q{/S{7v=ÛOm[4Es>\` Ong:k? wjck[n>js stA_냗ۿn{ziؿxoٔo>w[n_Az{ywl}#5۶uS3GYvM'录4?x>זơF]r͗,h;a-Gֶ?6O>zuמhzf^nO}w՜}7gA|qnxqvww>}sG;0}:|v替>vSo:MK\m?7_hĖ%=\9o쑗L?aaseCB_t7==oԻ6>Rͻo\v]\=4~Ҭw<>~ޱtkgvrcm7|8>|ϴlckn>i͇_7؍_ }tmtE7vNyNow<}y]3~oOXІv/]v뿞 γz?=u}㗓'[۲W)ug%+~eg/{ +\30m'[϶<0oNi[ >ygCkn|f'~w|inɘK&;78R-䍾6qǟ6;o=y?z^?/ϼ [o;3xvg״w~zO9_[7M:iI_?\؋[/x^\wqws;Zo;#k'Z~S~T7o {vܝo[ Z/n 9}\س+c?11w_sfm.{YM پG7{}sGV7eOX3h\x|Mw]׬[kvutOv/_ǶvLx]=[֮{>\y_oј{q[/u v̿̔襆Y\%CM͋ZX4춞i'6v{}i|m'{V3з~;q`mrs &{{.: r5n9 [?~m٢?uuGwzr}{ӷ=xqYs㭛?sWB*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*->B*3Lȍϟ_s-]McjVsl}wCo_sfB5Ր_@NPnn]W|{k{'76t4_6\: +qrnΡ/=OI ?iH-ċ|U'@)'/B0 _y$ +C狊WEEk殚0wuLM|} p{&5tE9{Bwפ|{W_{Wkm-4V%P3:n^{W맏7s0*wΎ.S-L+>5PpEc[{GS .r ӷZq.wԮFOv/hkۚ[ꮆNb/v%ia{S_[K2Wknӽ}%/25fȗ:٣;zf ̚!_JXxL,dQEcqXlӭ6ן\Vw>gVUzͽ3{#kK]܆|uwNlkh.' º'5^F륫wԘR8, .ByJ$|cƒetW'Uᜰ2Z\nt4%/(0lSW\y\ܒTPe& Laa-Ќ;&tfF`%{,3f6`ff6`ff6`f~h',ฉtU ~_G~m!͝%kpprǰr7tޞ1 NZ24,)}-s1Mt˃4t,lX\kȗe)CYh%.`Ԑ/d8 %//0+Pz\r!yrрa0K?UԐg9wpr:9w|?J]4}+];YTi<ހ*4Ⱦ +8O܎˩G-gMdΔx>%Z5WŋqkCoo{CטˮBY'Rs 8UuM \88+/u/K>tMr%']BJgh󇃟~" &U -St ڐokl+]+VIZ2* \іbH7[q37BY: ANb,`x6Q!h @71BYz(4N B gTYgvⲰsvWp";:z&X m('[8rC6LQ[Y5wԥ56wwfU:(7Eɒ?cyǗT,EA&\U\"-2\չt\pC*]Mp.}ݨ|ss(.Gko57*4;uP{jQ2mX_F!F55w|NVr]l3· + tCkE=pfx,eS/*u +:A۷zWT Gj4J` YR^,S +rK*2=K>Vl9K ]HЊU-eT܉dY.Y).9i.;|ONsCIs$YQMeg};HܩQ$г$JYH$2},Vm\KK!|;J9qUϹdF\qG*|+ij_yN.+=/sizKtfT#)?B6.?2G%?G5e +t<{vLjiRJF+ deYT-/nWNɒr*گ] xZ6qin2m?v44cPwOCc{2jR[ ZT;+*qd| {aHdyUܠaS8q6Æ\-E,5rfzTvqCĬ \heXZW V WLۑq2v򒓫@ժ#ePV.[%LE^|]16ӞL{ʔU*og킫&^PY"6O2N2ZgɬC+x陵 Dhu?_։d"2WK+23 IꐠκTf5߿2T +Z:&tn>ҕLhj,%W)99{*z#d㡫s"@s=C|e]ݓ/tN֍S;X`j̰ Rɯ|gܪ"ݜc[)utW tv+ |U5ۂ%`%2@c#~BVwKSdқ j41[ᜠlg\cFf> )Ь졚J/^BJy`rM%QVIU>.Esk[WSJ#ڧLF`]wWSqVWnm01ÜBvUom)UWTw-d&|`sgQ6莎 +8Jْ+, +1s9*UU2^h]U?){VF̻#Un]顆^f}U ۫&shu/窤|O[wGwk4:]IrE_RW]-MK Jg%ja}UoUagV|dCW4Tےon^Rrĵa8֒IKŕ.]\FiKDJ/k^-y-s[OXذ5kȗ!_W +Ch%`Ԑ䷢66t4.n 1`Wo&kjjk(*}]O%',̆(}e$eQJ, (AfV:F8RG Xr:M EV^KhxgWSͯݗ@nv%[I*e쭚[G2-coކwi-S2񷌿, +ϳO}C_eF>R_)9Y?[UK|sgNWk9"/YZWڻ[ڻ.@Oʹ憾eX!1b7^1R_P[ []ٻ YoYu~, MgOll Z0k)g[FDeQ&H$p5owٷt5EĄ &-. WLfBC&/Qsz^ɍ//52/ [S3\ZN )UVuA\g |s z:З.\un \PKW+k\{@u<wqp_0+> N\SO:u$ޑ%> @"r!px~f*y XyM0=;O9蕓ڦڇ|E5z<ĕcƌFCO^3~v['CZGS{AΫ 8]y:᷀]6) M8(L@}"?Y$/W g !I /d\ xPt\+<#H~$'\-7]xUp +* +79 L6{Æ(ih> /nlU&Fms= +CH[xHk:& X!å>cȈn L q@_Ny$QSq4j>AD@3, 6">MNq?4qSHXf|UPs ~=d@ĮdaED'3 a` \&h=  +/У$FbJgLHࡈ@v }Iw <8Upp]@ " +0qd@@8a4c1>wQT ]b!p_ + SWuyۦ]~6 +<5XCB $A`"> Dp>`Hȿ AaBel(ԐQ7^dA0F (@*")M,}/$=CL@;2Ќ|ZB{]R Kt[# -y"0q\A qJ&xwSP.@;E@&)|F<凨cqHs@r\@,A `3F`qQ#/ .sUL>%t4q@SLJՀt& ph'Q΂!{HȧI!- &˄rI] E>!#9EKqQ=D'"׮ 6N| `H9(ɉFAGp"e/gy w.@  /`#".K"CdVt4,#}=8_3oThH@Ԉ# +o ,62 a' 詊QN @9[,I8P +D:`\DebLYBh`mF PicEU!RIn@J 9 +6VHⴑ7 V8p|c]LvLV .&<8x +x'ʻ6{#K Q:' *CR b@i K@HᮑNjR(ס"t|SI0[7 Jlw!*T q %0Nqxp> +l0%E !` +XPH٧+ D HFZ ("銒 $i)XP!Ey璪| + 7)4C˄=J<& QyxuPDwF[63 PdQ"WBŃ ;?ds=3"A㮂r X EXI{>A@kP+>m(ĐfmPGpϦ1k,EH~`JH .@ ca_AVR"L)22xsڬB2pQFB8"lCD/ 33d  +S[⩁ ֈbI4 zGwY} %+IFi(yd1Ff6J!!"e.4( fs̢'brQ Q  )$ @pZ$25\{ R,=w]&W&e!. +4X?k'J?@-*YK<ߜ ̫jqFKq2݄Ps8"D A)2b# 'V|Þ^Ydkt? IQ S "PcTV$"}lTL<þd uB1X[-R- % fA`+!BzJ&4d؍Y2(^lk ,[c%X{"`O;ӌ4<$*Y 3""0@`4@ŀq1/# \HJ&jC/d,V#u=F0URIΑ~*A\UCEL]R l;+Hr o&lNQ&م,ifi {-',SZb ch!ȇ[e@MA$g6oLE.[fC3Œ,>"OH'd{bZk%E +gE&YÎy 0y\g HqaV:^h͇vfZ&73K@ڝ(tj_Aь46b>:i]hKqِy˞ڬtA>z-4ND^hc"M;Qj#rE d!q̀IDž f84rnB0rn,k6@Oyi'C7DqKR>$!9.9i-h ȱ޺5s6*{2y+K5>y45K"̻b +}y<`%槚117eӛjܵW 2kGGW~/M 4ocjP ZtKFtB(gX)ڗÏi\E/O%l9%꧁!` +jهf! ]‘Px8B?;/QujK妵E촶4 +-h]Lq">Pc^tB]+ `,`lJ ĘIYnҌT]S249 e!&mP2GwrŠOC~aXjf?y4PG4?m%i&ѓh[(>j=60h$@ M{g!; , )J@#HzJ06o+C/\꓋%S5FV-*op) InE{I}&cTc;Z`7X^3͍9p.e>L[ŠTj~h|DŽu&C)_β +zXBp b,W\\-mPhF)!/P}I+xBzvcӒ|8(r%jwzW*wNFW*ܐ0}Ewy-2)" FBu#3GAVZKhbD%Q(+y!n0 (QX +!@P~ߗє?`v +E? ]L-z:0@Lb,8188'ip1TMR< Y+_PG:A!S\c07fL ~@2&9QX^R2߅V_S[ѵ=h"EsMX YS̐aZL&)L̉פ8ka=hE16uD flzJ?l 0v>. SPl)(ˍ"6ڞ,<^~`_a\˿fm@PW~-|3\h|C\-R.QSa y1e\)_p y&y)\a$ ԨP8B4KQ&F0D8Eu\ITZ!.$>eD 4%ʬbU5Tb"wǨ=iTF$WT>pAs5,&+5@$ Lוx'k0ui*LQQ64<ʪ. -Q<=D8+N"IyX}9_9#SFk|y!$= TQՁqm9OĸXIȶX4A)Kcp`BE\| +f)#K{hƒv~H(|QLt5~H$0.yXK(OAAЍ(﹈2jV 5 J;\@GY .21a6k NXT[㊞G !$P'V$ Ҩa1N( ˧C,m|>e@Ҡ ãB h-YtJ SCҧr+B +Me!X\i1vrsH-!Fˤ +jmSp)viCMMzrB A}M}+ (-`mkIjyʬF VFCExO[@_Cw[6U]8kb0 JG)buR 4 (rDZK. h'4!&YלHN" YX bblc@r q)rYBD5sCpX>]"#7Gs`@rCz;G%Jj@Gq ik&F>& + RPq'G5MQh +x%m%ESt;)Q(\Fqճ:Q>9C#=b OAIq90j22ib!AH'T߉A9An#K dj*vWGMd t X܄u]S%w4JۏMa/:x1hdj}kwC|.gؓdM[ E~ .؛;#\78[%.{Q@8]NpC"JsƮL<k9o!6>ZIhx(=V #Eߧ듯\;COZ G'O~Sa +│t\پq{XKz>톇mvGÀ7 +q뙌>nggRTܣ~ .\HPQ_!.:m^3B3H ԰$0;iA?}1wAqš FrщSg(jN^ؿGCVꄩKcR\̐c 8 `wbnt6Hb$Y|R[ ~FSG.T? *7*%y wu:V1a`Hg> ӊC)(Vݩ+:[i{=z=hbƈ"ZEKl(N#:C6 ʍHU,go;hR5O"W!p:>bp5֢ȭrY5&0)\HV<pq0wҜBͺ:=>stream +ɯ>\R;ft#vŒQDI>'`(!S(1Q!z '.QϧޚJ F$D z& žd!&wlD 5 ImqH#֩´'V|7M1grr% OdgR{ig!j} +Q ,C(] +~N_'68$]u2Ed_jԏYTﳺN!\A)]XWmȲq0bڵT1;uzr0R1>=67/sURB+r2 -0 +5?e8BcbHP>5.l=~LqcS4ɎtІ:c37TSMh)| I#L_@l[t3+""aKF9ԘkX"]Jʎ澛ԄB>^ةQ>/gZBTѻdp+-RH(9gd& W",B6@4$JG: d.#1d01Nswc6Am) ZdKNLՔ^pTs&`I틚;&5[8Qs7/; . ;A(=\,: =0]anɵ1/߾J'w76Onlhj5t}./OmR/?o r_z8-VVnzzMwS.sKoT|N]tehDpԷ.ۉhhvmDfnH lH%TW\ +}|r\Jޛ(^BwhUQ'yJMȝD[)K ONhR) dQpX5###M|(UD@1@sZ6c4n&0ޖ!pgref ^v u L-gAi:3iC 8UP^ۅ@z&# +xmb#A= DKyOTsar6 r.p d &ӗI P&Z@{ԌNcffh}% ȼHeߛh&xy c +AƼ&)%ND rB@v0Gʅ:4iTV {gXJ$@KB7π7xC`Zb ^r"Zz$0_(I,qҖxJ{ʾAj +!mRkeɈ fCج2&TC݈y~&fɧ[vxZJݢW[@Go (nA ] +dW0C PL2KPe7BCR0B# R4#U2ɮE|\5,910i|27+lYߪ +~ s#]*i$x*}rUeDr;K+0@JD`$x.R +\[VC("bz{HveT%5߾E%\TL4}.?,0b"LX1&1ϟ@a rQ|SHeW@1?KVsYC9 k'ֈJ" Ao3馆4 +DR1 +@"NZE;㥐Sl[uMR41)~ITL' SLʼ5_Z-$ 7'xFtBޥeIbIJgˠ+Il&b]"BQ$<7IP$@#%%VZ*MdpdrD9I;5W*PB%cRP6*2hY#-K#).(SG+s(ӣbߪhRY MuNē$2(!o +_-ɜ4> \hz1Zh|۲]Y<PB$@c [QaRrtYm;F3%39K2GV^ j,sbt!PRwZYȅ4'(a:4L9%6%F'vV ،v <,ZGHRd$T_.Wz}=,Z6Lc0QlD eM#PQ +)ί&ۭ@ +U;7gN2 `2 +xLd@oZX߄#eLT +-&.c!(EH+6Q1qe0؀EyfsmXF.* +`]òaeTTVH7P;4~ XA&,"T8,C}|!([F- 91}`#8ln^qdFS4sn62Dd&A7) P1o>:X1Q1f:Yk2L_8@Qh) DBW<QTe!홒zD\j- +b@:VxHE (c"ǒ&؂"Tûm2sel?)"HImXFNDa pM +2&20':D9PQXFq; 2Om<3D$2+,Ȓ KSq=h|LUs)j`З&.qڸ6(Fqm%"0/0VL%*I^!mGĸm,%Acxe&Q`2/i2\V"UNh'i`HfDPxk¼7ǂh)P(<`VeS.2AArH+zʘD8V}oD+lgظula91/3H4-.=0 bϛʽB D<6̑y2kU)^.LV2rA\H +D'ڨ͟(!["0'Q*DT[oA"5(S39Vr٣d\4o  eFJ6buqw @rΤɵ6l1VmW2R\8uҤIj[ZH7r2HِN >bG6HD#$" kFY_Gm_w:l$b2dq-3Krhfd`5pȂ3& + +QR(!M‚(y⧍m$A 11M'4AYb*8(eIasql#&(Gж2zB7,$܈`V<kRLZ> ®)n0'GjSL[3V\K]G{G{dW(+ihDp#%0V> 2S"Qq 4NxUR9"0p yeJs5k_RiT:mijj̸8%P2`_7*pA^D16 5 bOHYSeQ!*hbQTBLܱQ('ʼ,BxĶ,(2S<mHzDKr"#(1M%FMj`S( hacP b`D8A \P&ITNHVAEsD؂"P0zW%G+R cn1 A y`#,ی IjVY{ncyGFϥx#ot0dы< =U0YӏJ"\B<HԢ3@y1F1*BӖA|H1F@iPs(T#ӦV[&uF%Jי]J?W1VJU $:ߵ%vKJ m^˔hcE* 46N;mˤĢ>ImXj$%6YH(9ʂ6& Bs;桍`i]f )W$3IJ7$F^ʩajS#l#rjĐS#xEf?P ;9jb`X3: i~HtMjRr*+6K,,JkF@,{)U.5+ +(ژ)a3.˨LH. $.۰+ D4ElJ!NG:V qNJ l*&) \X[J6]reT)6$K#S/=I҈xZM5AѦ$BqMWbmL(q0&zW+c}@A:gk]g)<܉V$z0 2a ! W+Ȣ0ZLt:2q&ʲk˾&t]!tQBE B!taBE BБαsK't=HBE B%t=HBЅ B&t !B[!tQBЃ$t]!taB{TeJ(C袄IH]-L蠌]!tQBE B!tQBE B!tQBE BБ:.Jz:.Jj +:*Lj:*J  ӗل:D$tU!TQBP$tD^'F$-U!TqBPE B%tXP^Pp'tDؑ'TqBPE B&t5HBPE B%tt)'tUЁ*LSʆP$tU!TQBP$tU!dQB$tPE ڄ:*Jj :.Jz:.Jz:.Jz:.JV3mclaJS\GVPV3,VשZB b5#jjl)95#Lw5՜ Kx7+S{nX9ul5[Pj4p'i5[Nj.V~XBYoX`5*5Uh5#j&L[*IK[DZiقӛRl5Y"izV3-#m5bVsjS9jNGfZقVs +'Ռ,j.B6*. AVsjl $YʹiN"m5[$e5N6ZB\Pl5ڍ@輅VsւX͑(>%AEVsbZͩsH̭jN%p\l [Xd5[X "i5q+7nh4p˴˼w!~O"3y55!Dr a$/"wR'>"a3CPd/k;*Z6T4iΔ(2pI[ڔ"(2m" A #YYGqee.a f 7PHY x/^<"eCrK+:e/rc+%edR6r"{y2;3ຑ k7a+{)z7;(Nk+{>Y[9m('4Ho4'ьTP={2!eASfx^IJdVƝV&Օ\(Jmee+\ʧ+StL2)Rd˻%ݲ!猛]dSGZKjE)tG'Sbb6%,c`ԉ_&:M7uu NlAv M| 9R-M&;y5Ɔw:vMU4,0d$Pg@NkGFFKfTJ:Mw%cRPM7 +7< %dL6m%W$R8:ײ8Ή\dSJ+RN`QCCɌ:n[;YhįäYh$[;IvL3r*n٤|nSq?7)dqxMvwvNռ­$܂f>Z" $:&;NdmDʳyiP"`zM e)DEYS4Rt?{oqwIw_}_\X%L6;tO&L={&$_ww:_ƒo1uNս Gɒ%O +SNN2[y<|t[wlmh*x:O7/4$<זx:]"UAm)plj-%G[JfOMc9x쀛Gș2{ v\T1.Z"Eֈ2Ќe~Tij0R<a[= O>2Y>gETA +Aۦfu\`6]s^ZB`\,dÞ`h*X\9ZK,p0Nx bi-R`%m[p!D{ϝu#(#ph.e,5eRZici=# +7$d^Legy̡n4g >+-%YPr7ѩ]G 2 XoB@$[ 1 +-Dfy͌5QvvCq(;_8ytJx0 +-3ZNw_iv>XOz,3Ѐ: $=}F!$4æ7c~̝&葇9D h6G [+!dwQZ"-D#35J퉬Ȑygl%@[e&v!/Q4&,VǛ|"y%d{jFflNB˼VԴrlu5:Tdy(G^#-0?  oy$(3YH5 im.oeuF o`e+n{FSij)Be~9يkJ R4)oy|󲹠MeșoS;A Sr"+]QQ9WR8Y744)6sEn vi,74법xZFı#$D_Q&T3$rrKFO!fsXYgz,f~,+#TqԫHkY0H#  |. %*>&G4|ذ=l%O +\NlT$*O +^^Mk>NaSS#yd;shvpWFhS~+yS,O% +,"3zJdX_X$?O,vx8xKS<<DA԰ 6l# ݿBD(x =1JKn8\4Yb@!O7`+A4`|6ERhx-J`aḫZ4ύ9Sy$j*p?O~bV[}Uz벂oT m| +cm mtѷӊ2n23rZ\ǾluGQ9VB' I>Rn]IY=;nR`"eEe+%]cѽ`*(L7I\.ʤw u,lem|v~Eଡ଼ r&\-d U!R*llt%0Hx[8 '.sv>9Jd`?U%QJӢ*><"&vi[jŐ U( G1F^ER롷 +,]B#u&"v!j*Ә^]j=2UGCl@ bda?|$̗qR;Gg,tAOM[jt $j ꩈ"[螈mEw q/؎/`&pI0Eadl셔H.vXЅP_x &嬸h/ۅ᛹ި_|1_ڎ/SWë4!*:J9:H<5|,"~d= ba1 or THB% ˊ"a9?Bۓ^Ɣ'I rH1 +/Ь ᾼWèW$Λp"psSc"b.iCq=$څu lx/ ӘOTTr}4c 7C<W!%eX\ j@FB(jyc@d&D+ O E@ J?,. b5wS5 B+dv++dH4-" OxRȌu\}mմ$t:u1Hy1Z`_UI|l!4Ni?'\i@",ci"r$[!9c@)<]Nc 01KG + ^C!Q± +JE^Yv4Fص畼;l3JƳǃ30B0KtQmLbdoĐS(C *a\VOL׫z 5ksL"!* ̤VWOA(^@*@ OeRu]]smz\pa̅UppLn3͐Dȓ;k9| -c\Vweeu[b龗(hNIԧ߁r}gB->x}>faah\>mЧN1&D >Ró/D<.K.C yhi + O8#;Ö", Lt4Xhb`|p + t`zC{TET•u\dv v;8X8Ĩ&Ф` YRTG aQe\E+# )[_7}'..,` bE{(py8TDRe5(j'AA4*|\3,`Y .ϩi<^CU`cR~*4I8#%\| 1 |m@^DZ{%*3lHL,R%)4 (Ea Ɍ@BLL$=dk`Rfx 8d 4,+зldjFTNWtTÎȠh -l`ACTP: {Yys^iJn +q5X>y>по,t亜N0ŊvX/rkkLT0"O2K"+d4 +P,PY%4/(q]يI95с񇶰`##bAx.4@Bݚ0"LBԿ%tseL>f!0nBMc=*Ͼd鎅pkE ΐOǻ7pSg͇J.aAHI"fxS˸[dx&tqC %ԋjn+*9[ٳQ+#-H\" UfJ= @6chٽ L {:!J ljzRDVxf9Jyq. œ2K +1A;sby)A` g=3 Gb̕C3Ⱦ츠U! U0.%'6ާL:EHDZ剖 ݈Լ"ʎ$"*;EcVTDgWe#>.! lQ +8q]Ppo8<!3$ 6 W]4[!`8;Xzv̓L4PIc>nҹ*TAA0FL@ * ,rȢhW#RKኵpFZ`N4Qe 1FO5:[J=3h)PA}7aICcGar.,i))#S?񤒍$ +z!@p/jrc),9M<=VCK>` ~n@@*} E^kS[e:ʹH2e$@7%YgD rGԋ+.:CLwf/Pp*SqBxoR57?o@MWɘA< [F@Zv`8j +5=F휗}{:!8rhIs`pARF*o#e {!jqَGÍJ@uP FX4*]&3GŇ@< 3pZ̰SP9ʜAgΰ&` H^MϢP0g5, +7skrmo]ecz EXtٹΕ0s4ѰBax>Mkzk6{[BH4*} +Q8ϋ"""idb0&fsĻ:1 # 4n] +lGF  `D+#΅{Z3Z X/_31=[9$ⱔt%4q=,w9&hF1~ERz[aj#fsE\Hz%QŃ,>zq$E NCE \~A3h"fx齲.Fb^<iwtѳ'Dp#w8Zr2g.D J''_mlntɡڡi( Zq[.WWOd-8q+f`{ +TE^3< 5ؤ"vt(b(ɒe WKNBU>w(,yŋQLCdS\xPwO}_@T̑dbj ^cb~l%pl5aK18fĢZ5z[ +FSs}h"(slI48" Hd{h,#(D4H[ vFb2{#6r̩I{IFq5x"Uq_PNPMyx2/RfhB' t,m$-i\%%1?żDfԼ#ξIi<7T񺼈 Ԓ ̂Ø7Kl`l4t݃x{ty +Map;P;+Ge(s8¾qL0oB;Ϡ ap`5wr/YZ3zH.5IeA&Aκ2愤L`.A<2Ҋnk±.^PD+iPh3DqxPÈRPbHdT49=an=XœZ (,-Ejm)|#|ԞHoXa9B5q2^fR2ZO;Q; B>PI%g'̬i$}R6PȤIO\t!m9޸'a+Su*ߚu 0nZD gt$M @*ňLP#_ 'A ݀hIu :/ nfT4 R,K*pwbLj/%ۛgԳq6x6yz=fƭo^cu;d! 5AgF?]UGBfހg#b3؁-%mn575,ʨݢ[5B1 `NE@V$6vCd-d +8Qfdky +fjv@SDZ;X9mr)H6? pʭ۳5̄l~>_~Ap;OזΉ%0!q,޵sҲ>eWO3tՆh:a4n2b_b}!';??|!oi0-D|kK X8+:‰[^p y}dD7~A/og~OpLDq #\N6O_\"AbC C,Z6KŇN!+Z~kO<`~!R`HNP ݻ ]5Gpiu{uyw^ +@K!!,w'FtP֛Op}mZ0f[zl}<]Ja:>> +eƬ"i*vsHH8ijp"%dx Xo_5!6tҾz= KG®N_DvbOS?l2XQ&mFzD0Q&BaQ FS@PUd7,Jq*FY- 8cI80>ª8NFfY]ڄ{As,T'EOoy'0o +D%|¥4ik2.FD| ~n` ]$yy]u>H5oݸ/H55:$ckD6 7 |A@$"14bz^"'EAdaUWUWm|,\8O[~Z ǭwy#x1,q,ܮe(,Y? +AگAZ*L~9_:sy3cq 󇫟7?B?qgn_4.7O;[w0}y0/W 5^hy#5^}E$ +4JS-$!Wp&owc|ÙK>I@ξ>G/zЯAGVq\LǕ W#fm}~F,O/qnN5wfNI3n'L?M^;z{VoNcloq(Qwvi<9hXO;hhjm{@eˀ̹/1ΨuqY?2u/J}xG#o~u. ͂ VAش½M +c'tHyO? n{-6 1㥔G5z3,0Uw1r1dc:pTo:MB2la8d|EB! yO??k_/%^ϋ2m OEwA7e_7w7U<~8 ۯٝĚpTlwMޝK:.::( vV:LJO)83yOLʻdE-Ht|_3G߬(Ƈ8 Ш Bbj%$k+,H`B?O>soF:r!C,\ձ+a ֕0ɘ+K?OxuKi_vȴvYi&YE֜ؗҗu徠?]|oWx{^ +ӒcXe y |y.>oq)~Ŗ_㳒WS«,idWDUbWZK,|MX}/<{sg W%?ͼw?{ xlElӲdɿbK)QOLڈ g&?-,_r +y[Z?7';fæbЩ7A>D(x>)Jϡ gnҰ1;l Ad^0; r(u +o,8b/([a0 HN5S)bTd"Xy(m}ݫV|`tCsFPX@suMS՚C 8Og +&Pݗ-p<͔QrR-S},02F$.等u=-0d;itV +6J&A@JVHW!a%hU9Y>BΊCT((0AA`dĀ-ÕLPW; CPm!h4x龰A--ZU^s@udFj9Ts*o{|[oi>og?Ϥ?O~~m ZK?Ma'^X :x-,gb/CyggޛX`? @hztC.t;J4 hMuv0 Nq!d;%;E;;ySXO~c.\<ڟ]L:U^dH"^|F[Î50őD~( dæXjHMdw2s2*R8n~) ]R5_cg/~?~o%($%?~OoO{^ϤnO~?o~t~2lD>OНݻ,z|H%V{ᏻލ9PNlJ3|޸iKh|Z_ӏGm`r>Z0[A{3'/Ll߾ 7 7l>[oΫ@7̍G=M$??+ &W1w^-U6mwo5S4L9L}ut3CnH8{+9~o|cK'_6o;/V$M4[6 _]ymAnψ~2ƽ4pJd~wN򖙹||}&uO# [f7GWv_QO{"/:JBN{BT [L'tkC (eD}"3JZn&Qx6zɴ=f;{{DaΦx8 +ڝ {iO1{{GQ{t32i icXñ}4l~Lm֌)Ȃ$h/foxadžBȲ( [^P kWDM4! X>xWL<ˎۓ][l4zN/D;=16Ia֘gtd+nPy{߃Y1[]«z>h.JIdP#c2wLj΢0 6Fe[焜np.!a3wu5IzSY29»w6꟣pdolz'x~k@uǓYnT]&3yÌ{ӏ&m΃ a[=oÎoM߆yNLuۭ1: _m>dc26wmâ%3Dnj'mLm>dnm2so@!l԰A̞AdR6vc!G`;h{'QAxJ8*,萛աkoc/ g/ȨfYX+Cڂ#e2JBOOxU&"]Ԅ%=DZ+"XJ9~^?NiНTnީXxg}0z W%=Cn DЛ ןgQlOrMq= LCw;9qN'[›oEg6W_DV#3z粛N:[45^0A*%f-7Gk3wpd,'5y,Ǵ7&&ސHh֛XaKik8x8:&ç\"bbsҾz#"^N|鄊>ß|c! 5A!“i|ŽTit '] +8^Ť*l;d v  nqt`8ו[нRVwַPE6]=ÏaFԅE@0/懠&l^lPES$BwlL԰CAf{O ߃'Eaߋ);bTQኵ@{sz6#Ƅ9:rA_^ϾPA:ؘHI7~9LjGsȨb5n|؈ D[|e06cㄮN`f1-uF>7xF77N |43{;x3R>%nl`oG;e~_'OOO{};;k:2{FZWsP:h'" sQ)C4~}fe[{`_hn 8)^@R MƥUa!v xOLm 5bkvaQ[ +oms4G/ +iVb'̴۳q^Y^}~<0W>(q^7KN9|c!)ܚg#28&"-xt4{@@=`4V^ݨ'|ONeX9'>7ZhVuGaF5~`Y؏5VⅫ8 6 +2hws"9nhfwKp:ۯ` S0 :.6/ܫ-8!_c?p:d + `W۔+\&8f/ 4`2!y};m O>ThU `fWg p>řJ v vn_M'&Z2FEmsKlQm!']C+?.Db1M>iKK͡<]hH)zέ$+gh's4g;ww?KK__?PsXȇ sp4xɷ&Gs˒ er=_WTF"|wc1l7sjq)vw.ܧH=-Iwe0:?{{Qƿ],NW ++ˡZbTXnW+Xھq7X˴ˋXxmcm)Th7~!Z/}qs\5I'|4[}Cenp#i7gմ$pLl\K܅tQ!79g\X);:Z f4iPMvW9^I^PBvnDO[dkiQ6uT;VN3wpD"xn"'R쩼θ8-n&jKOJ'e1YXhBzyH_eNn=U[If;M..r,}+wft˝˧ɇRek{[$`l*HJyL}ښ|'f::~f +oK~S_w٫[q4['͇C+uk LW)NGotz7t_ow; zu{{ٞ|8΃z?Vã7lpzh$UQ㶕yޑ疮jq|pZV@kKST>>,lwEG+wUvwƚǷvr5^]{N^OIX=(Kr`]OO۝| ޗW!Yd >xPcW8<>zd :'5wy9o(K+J%Uɾ-j_olݿoNo"qJWZ,QnTYZnSv]i$:wzJ~H6Y$r@K>+KWG[M9Õzm>OgpGi-*kU_}=RnFyKtSovb.ᐫX5R %:C{.uqP7*_5AZSWޚ+]fkeYV>L*Iucneᴣ>ᖳjJ/ʧvnWc{\'V/kwOAK-D#enzT5Oĕv`pĿ=4g{ -7ͳV0uK=E4mғtŇ(QefɽVjK<^g~&P n,\4NZtY8<[bח]+ɲ6 ʳJ{r}U88_njծԸgWڲ?|5ݾ]%p~oemup~Kٻ^N[Yט coˁpzѾ SX#\tF烫rkߺ>oH[mE"q}78ġmm}5z|\6\zN`' +˓}vtICK}fy.Ql%CH_\m7G>Zh&kmq};uy|{vgFQ~ަ_7jYv6RX=ەE XO.GAaw!qE$2*\zzɇM-G݁+d6ξ˨eiKy.Tzn-1++;]eTjwv˴쬝͵fI$|O +a^RrY+vR+EBƨvtV]6N;;WPS;Ǯ7g`+\ +;ݾiՃ"Ã=9=_x2AgyDe=t c?~ [jy RhOZryǹiV(o.帿:56+1|eۻZ|#WOעWm+tܹRGuK> Fr_?>F; Bmu/q,Ⱥ|Ri%,NwMcV73y4|Nv:X?\}ZSi4|/EJ4c|-Yt>;T'Igݗׅxwɣ+>sEtkvQlVT_i?9̥brډ-Bnw9/JOdL_H uu;WrM.nKn"|.OT>X˻(Y}_&́jj+4|Jmw[m~U؉KdrP*].N&QN~!Jn~2"sJ\=6IEeR8kd.=%Zr1ɵ^#Q,Mw{i&9غ:n41oD~+t=[\KzKrQ(^NoqDV&ٌOGY$l]f %և˫PmEĊxZʴ}hv|=ҹp9&kB7ZH|jjЎx<|("Y6g]_ x䓊X>57?wGz&Ǒxng|h:'NpYE.Hdr~O2_olr7a,gO'Kg^%xKOb/5(|h˝Zt/q7G\JFcFO +^KL&==̉tj92Od3;&gwjzsc +Vw:*]9^KqF'X),?SqU+ Bc,6=ʩNZ/8u/[J< +3w3s{W[I ഞ`+0oa/UܔR_bdwLF@'yظijkW*QRݻfj|ljBQ_)zfdTh'ѐhIAmm֓Ń/h\OR'{FՈdKE]oKRWq-[W'RւfUyP|^I)ZBeV"c4ڙcYP+wјpS{8=B/&z}~n&.sB$$RI6=fqBޯǧԒ)9!W)#nuWh<ߪVѻNzg\qj09f +g7^9^n-;ǽ -NSj*s6ZAjU$^eOw+rftO?JWueglmquS$cԫ~)7b.wPR>\?? +!Ku->P+j,l3A=>NW/l:{=QsZko%2pD^ZôϷ'rO +Jw۷i%n[1ۭ<y1Y3-J]r~-'zXpxYϏoz+zᄜܹ\gz #O^uUW-28nS~ T/i~9.fcTe+GvH熁ӽG5Y[)7oRڲ<pNVciJtE//gGhBhn/ڼzFґlLf$/MTa$7KA<9ȅl+m>U5dw/FzuYXMLrqJ:2J1QO?{GSPc0M`9x-Gw\bkZדb9wRFtyPKK+t~^dV{l,pt5HV3rdVvЫ ! +BvK,ʱ!RY.f֓s+tvg̉˝z$8ެzQ:Ke+!!oD`Y-)a.?ۉ>jn`;Rl j.Uz⳾㻔2Zv)'0&S\v-p+i_%2CW6F"Sthp]xPW7Kn>{Y:n=\_vvdi||q3p5T[T|7 ʬpSӫ\(nFJihi<)Tڕ6';epv1]^^ˑRNZ@4RmJMni;߼GdP>?O3ݻ@tzXZv? +yO]I|mƯ. Fv35U +l8:%")xẵV?y%p9_fWT(J#a9)(qU$=Q[ݟ T/C2R3oYO2ۙVWO3]<>&}pVV/7+Rcp;םS0۾v^wsZ.^*vwݻ}D.j_]&.&|xx߮.|\0k}DQ9}u1tI_nr }Y\*ߪj2[W+\u +K")er6=ʇ&ahWʅ¹ZǛ٭_&lTb&wXvuPs|i5'Jk=o]U!Hǧ뗑xQ*+Kˌ,ŋ{XcKt؊/ޙki-ws5Bg]>qv% JDuvVZ+bu_ngOAҏ\Doy4]wt㿗 +S-V;}]XOs@1NdH]|pT)w%/kLVbtvtS<o2ʴZ,ME.LrpFTڭTۓݫ29Q>ؾ'GˣU7*9HW,>nFi{ڨ6KgSA.6O;ݻĖSL `q+ĻbH6ϳb\ԮGwI4ҌwFNA?XLt}{(ϒ+ыQ"Iv7:$@}&;";i +rWjE;gӇI>}x$v^&ۚ!897ZtwNU}-NU 8y_ GW+u'\vRZva'9݊\y[S'>>*\L*+2e{mx]^mY@۰ɓ~sq1d2/P&ye@qK͍ڇ|W+,/N qCm%gCk0ŬZcM\V@7~_ E9-[vt\Y@FkBIzhas}w_i])pgo_QH/ϾcR;`=sb]|G;2Q]DψΛz)^CcK'2H8/@'}wKXxK,-qҷ68&2<nE,Yu[sѳmm<b|}QzU!0eO0ॿ˖t98tRdSx$VAOjS_J*֤5KV_t|4'S0 1ܹU"ǻ?2]+31E:?8o*gް眣.tՈ +׼f鿨۬ͣߡkVA?hfb(gOTNܫ5ˇ/vicPeR$k&~yb5mcZur|i]K$85զ}ns奲&çWmtW)253%?"!AC0i[,Emi-rfPV3wA;Fne4  薍g +1iEk0\ B(kh5 Vr[rrfoÉfFw={w;+/ Xtp[_߹n=H'f>y0,OڝE߷a~Y5%%|n<gs6g:׷:\of"ɨ3uk(ADz-jkT#Xw:7UTn^F Dgv40C$; [>\ ^`YeOuܾ><`0}5ۋ(HXJ+׼#͠Ws۬!xe}}~ {ӘR.J7ޞ vCIJxŠ!6.ۧ\HKy6VkBXj_kgJT@|m#"!{cg/cWɲcp[Vy@Ĺ"f +E6I&,{WRņ|ͅ}Ϧze)3UU8gݪ+uJ'v00z^GԔj,]^:}r¬-JAwV1 I} YD)/6=CmNxed rXWIVM+N1j=ppZ ^y1N:bZ.Y4Dr6,~z]gR _<߼\}~ØM0*:_r &I-K.k#KP[mUSioxJ{PXwnT5d&h4"|.6~?ډŏHܮ{nM:[*4{e%2߯WЩSU9"@Oqva?1~Ge/tv)]>?X9 .k6@?fuو{:{keF_{aMsInmk-3Gv!֞{F&f8irR}y,uMȚ,N|,b Hos,{ c +a(0!DhؕZڠQaB^WlΉ.46|~U5;$y$4`dpezpJ zL{(ݎ|+;8}nXQǕUݧ+a1rs,/E;Jk44Z~"[]W> ˷cFc{Xy]8l_{Kŷ#`n$lwO5&6TE" iآvDh0}\rTx|EnX">6G^LIDEq$ĊM>{\݈u*)&!;+^ֆI6 x}uO9* lt"J?Ū-2Nf1yUkom.=_=o\^ϐ2vea +M}TewbNI}r:-iwjCJ84dPl/*|3TߣhӦFFg_] Dmf4Bc=PFtYձd{QU +Ύk@]W},n2`J*{zU8,82o3)7sNY,*t9-߿ni!WRB )+rV}4I/:miܸ޸hV2 +3⾔]վ&!֯wE+H]p9 &/d|Np0>.=QVvb,/Np7tИĊmU5%lJ6y.2|&W֏R7_uinMY+c,_!Q2۫2.Y?`lS=荍f Qz7m4ޜ8{S*%wxeZktx' tMO~Dކ٢6|זG3@q+02Zr .cPbcA,sRjͥ%W=8mm_C@n,rWl"#+)(\ɠRچOGY/Љ3QM]{E[k?Qsn9mQ>vYɘ[T;G7"1I,g\QI5t?ݼnֺ.RMpa-U{NLH:PIZԍ5:΢ފ\p$3ъSPy,դuVK+VE:w6[Cu Vͱ.nU4V!srڐruGw=AC f;,y]ͧv*_./Q&U h4h'>Lt Մ!($LM|q*&umA>s]1[+YKgT_/B]H<4TysXb?>L20D||(Y=(+|=Xzt 5J ;Y^ 㮵BZWaySK?OΛ| t}a[鯎 Z]ZϬ~/[NĴf-R˄[(=+>UbxmdM7|4PwrfN4cx!l 0Xs/9W/ba(Ւ3֟Uyjc8Fª?|1Yc½ w^ ++D;^?M`d*vjJQH#ml}p_};9lkH)9t|+Z ]}#`ccX:yGjx55>x ZH[mSi?,9ĜR x V@жD_w 7=b<".ȕl)sxyƖԫG؋778ߧUI,g@ѕbn QwQ48"ʇBcPcV,3 k~s2*[tJ +Rs騋)S"@uy)}c0t)j^5Xwb`(I\z-u/g?^7Nɛn4Z87eܬJͻ9s8跱Ɨ^כֻaӇ@>v\pWȫ16=tAw7B%7v(RŜȉohc^PrqQ?.7NXI!>J>gppM nX#՞Kv3eH(/ TXcckTA1J)yځd AC*t%LyA|F#ξnOrYnm`ȓ8bQD0`Ks,k\-6(vcx2//h/&+a嚍YQLr*>1+Ψ}tIk^ +tzJ!s5 ؋u*ׇRۍ)o8=djZD6;f?[Oc/yPHnؐàK"{즁 !>v]R|z8gS1~tAlN&+Lpz-q+XXt@餺V{T2Gj!v;_+ Q ԭgwgk>Gt\Y![x>s.;tdQy`Z;fƪSE^ +MoV8MvEۓN]'Mj"ߋ?Cxg7Ӏ_G{>wO#3u'4ts9M^}S)cn rec?)ץ[Ѣo7^|;2I_ԇRu6iiS& i'+{*%V:j0Ɩ7P!8Pڝ )޺/l&ղ05uQ]μA;3{]1e4g~OJlbyd3ETٙʤ1Y{;|Q|ڄ 4.虜 G2d I)[\97K&U4$xuzqfvΠ'߉u>yuLnr!^2'`vo)ےUÇ'F*@ע'=py}b +QjW5u{Vht*Iay$66s~Pbb6UaX''5r/V$rG/|CQ8uqE`UJ}wZggܿ γgϞZuEKK;s]b]i'1b߅K:K}0FImIXԵI']XH|3F.2ەiN5ORCJ1e g&RP[1"*ʑ1ʸiG+)l'>I b:BZcZt`ߒ!?s~:6O'Fju*qs#} +5׵Ryrߓ\* =,yAQժ/gHB|լB,;ÞU:dA(A3ѦJNI+ lKu?49n-^mS{=8W^ G{WdS[WB (%Vƪ~ [}Ԍƛ(j_Q vڮRhd}JlͮVA;EI4rI֞Ju{! 376ӛL^Z.ӂOk~.nhъh D:((0n;MO@<㰆Cj fn{0$0[BƇͥ|}fkH!T%ie0lŢҢeGe'obAe;v7at+< Zj7Vx FTȱI?.]Op %K-ol~)Tr-25 4YǗbBznG` [Sޫ\SBapTZmAH@n*MfnO]gݿ3Xz)P5fnmk}æPc :;wnykߔɱ4@"{ˣ͏Q37aUc1]QKڎiP`1g@[bD;.$;otFR)y!)5qdJsru9^vf€?\pk>K|ar ++Y7ZeP-ܸΖ]YᄒxH|0X&<\>gQYAd7XMuz+f;,םRČ.-aw8e@Hb4]ݠ]u0 +*.SbUW$z'yϯ-WmʟSU +*f<YUbyL {¬~bz{3|D 8*P:M"oMZ.Rӽmp%jSZW_y$B1 *oSx~}=ޘY5;`I驸d _-޴b/ύeWGHK G r϶fƞq}L +^ +chbe +^@EޟBB=7dR $W7 +euJ/@Ҡ w +1;ԯc58s3ViKFvfgO/bT Y.|0R̭)5N+oQ +U'OM$ZXO +)P_sMװ|N}ͺdw"/GW&z"5{%y.XE/%d~Tӆ`6@4nƥxdCmtxn5Y`Z$,9 \31ȩܟ~bИ>V@ۉ={zV&Z+w,+Asc‰W叭\=(q`/UKI-k 6Ǽݰ7|C6}xߣ +&+q;Ol8~4F] r+|}pm POH&m>0Q>걨ͰےEX l9T #u+z|T-!ݻwα*AmrܪR~X2mg"GYh J9y;HXm +>gXFڣ4AѠ-a$7Yjr*:⳺BEB(~ 8-hFJɬǖ; T_FGhoybe[#38Q Ak͡K} h ,Zb:~).𖬛dve{z}ţLgJi$dwz9= u ̔KgA!uiR8&3)"F LΐhT GJŜ,58V.0@f,/gUYoe \c1G<^g2;7O4} +UO:nYIK ]{lwZ, W˸YqG8 vS#tJ@ +>'q 7cw2K9c_s==!!es6jWӥٰ Q^|vt}cL!Υz"@5Vlsdqkwc:g'H j~s{nOzQٜk fQ7ncZM["Qsg"JSHI2\<~z)T$󴮈Kk}(wʄPu*EXJStI8ַ@ϓUJ9- St{b_ZMl$л-Y 5j@eeF9_.OeZϛ0 nwyu#r{{'L\"hzb}fr)8+H+_+t>x\}AĿ.g\iYsX ݶx&!ܢIwxt i9EUj)_OZNJ25'vϩ4(Xp7d +5[ge}kDN8)p3k+=AqFD񐙄{^Jw|]hNU8:a/GKys͏K+*9Z|ncR~l;V1x{ƀ^ `&O)byʫ>5.ЫJ?vӿoIzMFx ]-:Q~]0t)G9i&+%ZLj|_C鐤sb wrH + A MCȒ4OJ8-i',/ =IydSR^v<@%-RNgBMtJUR@X<ҘTz3QarUOn%] fH7`}?jtueYWs,ΣUrSTex#~#QK̄8 ؈3);)&`Hz~ 6o 4$wjΠ!Jϔ@vR(ٹRx=5u\| B+w$Î?%}$`\:?H;X/kNC"镢[Rf[NG0{֠ j(¯|[Sb`2@ nګ +\xo#Ԭ4֗qcJ/ +oID>7Tb<ԐTVT4ހr)Tķ+vup:VFL5&}fa )9WE 1K3\F?мӺ #ZU(oOl\-V}ng趣Y|Gs"Мڕw;;S;: I9cdٷ`0HE{x) W9]"#քDyjO8aU>, -%)e%塏udk5߯:PLiϟ2޼VDzFtuXmX3WYS5jh*}jXF{4"]C7*O)^Z*zu!Ʒ^ӷ\ +FCsD)MƱbXlHa?5e`{t[?fХp:e?&{I!,@JoNObK:r)TdzډVWsRI֫F~y&E/hkt5̏oz32OI <Bc6|\X+mmEzgcI_uN77s0t7.Q}Z2mph jVg0W}~>% +"PFrAbi.@?T\Kxľ. 8\Fv[oKG7g`\J[Q^^TBM6cRQJB셄Ѣ1?L;]%DXͲv0ǣ߸k5S5KZ a~RY.^c;׻Oiu1P;lz^7PAWR)k%M2BJOt19c֕-vP *T +b fkFp6x[ʡP;+)u`lh[+j7a5ir5-gS @6(e23LՓd,)üa5 j&ϰ\mM=+ v۟. ǐ +:9n?E̬cwo]۝@N@Q1k:W>qϭ(jUhPUhcCuB;h>c*oHH Yiow2`)FZ3yoso"( +Lm/Z JI~뀴B6~*%y+UU9Y5NYZ;&W߹4l`39gL9^3{>`R$Nwt(XF*d&6_@)0OxHUWLoaCf}dM5 m"JDX#^>ur(S"7F6 nG[ۺN䫮UtSo1%!qX`_meM̻DVik< Qm*P'2IـgF`QaI~%>G}ypA! nbu$y@owmIBr SSi(&Vnx[n #pzθmz՞z#VELӵ]/t쁝:JdsT{>62oHPQ>KWZTo+,%5t@:5 *u:!WՍQ0lj΂N Olc4 xnr*ؘ51ܤ$5=72XNV`1 VbфCBNCΩ&ѭuŬlwCXco}Y'v?-U_zIÕs޴ۂ^ѫO[C?q 9#5>۴uS,@M5-7 m^jb[sSJ]i][g1xˋ{ʖ<yX̲s0<-O#O҃$b +Y  + wnS57j +s Yi:;{^.f%ء ؛Pg-ejU;il)rs|HjΑ}BwAdi U/恧Z1vy^MJAc(8kX8z'N +GJEslq=#Wf~KZ03\ACAh-oq[:Os\=A2kéuYc^!7U*2W:^fye"8Vۜ(vJ#BXk3y" .f|wTϤ_<8N^"ÎW::{9q}ԯugV ^G]Z>UQߍ[L%ZoJHzp&w-xWm]}}i}SAi(LO:Ko@@I56%R [z~Un7x` +H#}i]W{0!V~@zTTs$ƻ{T + 4KT$3P>Ț𲇪hkud`==q3GCW5$ӫ2enssfhu$;w%f}Pg*iU(p +) l^X{N-*)WbCYi=-"z>jjiמ،]z;CQmftZp=OtndӚ|\ϬWF[=F޲V:#I}`fu&%! ikQvBw#rЎ XWd21Z\B4l2``_!.cC ?-j>Mhw9_+ }4kۮͨ1p go䒮f +tnw Z}4Z 3m) pL6KaIqe)ѧ + xscWB2Q!5;A~Xꞯ6䍶d ԽIm8Y1f !֊[3%xD3R6,tѺ4)Q7= O*taڝ.1Wp]^rB8(6l)/ +?v+7q4bj8fʾsS#s1?opz;h;@%rWܫ[Sgl^@]u #tjtWʂ;0)g"dꫡ/Ta(C卙w0~3Xgh?#ePoԬ`?Zڜ^_*b}X'G8յt<,knQf'oe},.3Թ}* ݠtcC +J?ҦJHv{m+=k'ղwLZX-m)t&‹e+6W֞v<8qq=zk큹޸[`կmȰ +^B| mepP~7WIQ*%Z TYdNk~ldɒ5 RcyEBLt>^\RfJ +췞fX2Nvu.((:5(bvl;`=Ǽ~pm2iAm(yL ʙwsӒ?DәOqar|P1e8O1O˼{/K.DxWhfO?{do<Nνt aن++^kή貟}^BvEU*>ދX$tѓLjqüG/:Su#[rz.!kS[jyx 0* o E8^% EqO9l7+{n"RA'(_ir\yٚ8>㸏0A,OvRL?eq>K2pgrT.Pv/j{YBse1q.XW-Ql|wمՕE,}7? $a.Rz3Lb鴦}yhvBY&1ېQe\ov=?䞹:dfL 4~ƅgzЇe͐J'i!k I)Rͨ`]_40{'C!3:Kn< <ЏoXVt>"i +d Ъ*_7Xano~Bu4>xoYAO? ˾ ctEIr5UR\vT2LZV#ӭ1@>=&E/<^+\R;)Eړ'8Kզ=e{;?-k0zP~3McճW-0;pAw(!j/A7C/gMFaдgǤ .9b^ı~/A#^gf=6Rqf oiIEMkd=P&e-͹ѫ?.}VۓvuZN|V9H TS*wp UBi%ࣚ+ BT#U%=VNۚb*DrOE`{qjCq'tt 2!+؈V_#%_yˊu'HB(}T%FL~naM8]/vv*0*Fަx2IōN`!m |JaFbqngG`å`YTи%-uRE|ͼ%c}7B'j]5ZS%|M@4lvcAūn6T5.ޣ^Cܱ쾰eʋT]'1~!±9Vlx|,w} SZSqXh1 4iPsJ[RM*~T"><<ΕNU:E^㡏J$A4Im7G҆W2w (@_TZ*EFD esY+/L%%luu;19njnC$H.`Ě7eӓp;5F^bU/U?w46O2GmGs +V1*-3Qn>U@T%O1o;WݻWM`ۀn`K) ꎹ(J7Ww;reLSv ȍ}]~#Kv34cm&oSP,,S+oT2y>HqG +#E/WxǽM%[?5, qV-M8a^-Z2G>i*l= I9S]P>eWy*r,\>9ǚ|2p\669hLVAc  RGuEjT)65a~%35XڻfqWtx@}xzVO= E٦fAQ1MyY[p!z:OѰh4+3lG3sWJ\vWx3 1Z[ 9uI^|M?s=JڮB;gVūPµYRRӫL[!fol l57QO7hͯEhOư=hJϺRBQkXk;l|SQ Azk/ (*-8~|L2C'Σ%a#,xOFptL:RTI^z| ȝggB‚Fcz*JW4"MètT?sbvd"od-hlHS7*Qs(BӾ=4?QR}9ӟa>/m5d΀ ! J㫿VwVy;"[J#y}Gm`,VkxՌG5}jBCLrϠ^*W 8ϳ>|mUZ< @r.;a"uj<$P4Uݨe3L\y_̾]O'%فݺ/ #4ZH-lWFmqޭ`A/.:I*I3o`8^#蒯|MuaB=眪?3p1858'LcfO.OR& 9'_?β +*G~߯=_ |dYRM.L5RQl"jCڶ/toS -xB/F! ފNZB4]ICc;4ˢ v>R>J,XwMI1(%PI6:OCo_fƣbgQ@_];NLy+Kx(6".P)+ةvZca; +b 4GP޶!Tcx^kYUF%4l׋7kB|2ulAdM_@ձ)=+8"70=<X(ey[Z<jL֠Aڠ +ZX, +%$֖~ߓc с׭˵Z/G +]jVUU-=_оCpblLaIu>El,t3xI%$:vb&w_j}8ޏ{>7}YUY;عaicإӪAB +8o)ٮ3|<)6ȫXq8yuL4\TIk%Q~, +$!bYhtP#S;X1kC +j +o% 9ۻ*rB_yof),5c(UFzw_d)57hG1*Ń$8d;RͦyTNO-A1ITboNBwr=^8mf>_reu)tx&/?6U=]BH(Xeٕ?s49WmmcP_ $8 + gd5]ʿ@|!m0M_Ϊ@Sau2SHf)~O\NY> erFqF>w"`wsU 2|ziڥ,ȬS-1}CF9Ä>v *U=˨3ko۔˥Cg /7skN9 RXwd72۷m:`@.3E/ܮB:dtXU-?>V_ Xcȷ@ЍW_0?-VJ6?lFV^v-kXz\ȎbuY"LG&q7k| q;w9nc!u^xrf(OmӃ |-+gOZ|DŽ:{Keo3ګ^X҃ rBKrQ zԉITOJxi_zhaL|_sAi"o$tI&,pyMKvY=rnׅXf9&Ϲ?(y&^cSGۗNps(W6Yu/rf]E>~ m/=2V5IGz2NM'$VOOM3OŚ2Ő-⽏8 w2']mHžY(tɑM*H*I/[RD-lG3jZ2,eFItz[W?<8ڦXxGu/e *.N0߅IAϊ7{*a'ؾ8NoU;iGYu>F|d \{ARc|lSJ!i 1faRuC5d&W.'v4jRNhPKl .C9-ua~-b0벝l BhSfawS'OΞkPx|:WsnWt$GvSk*-0>LB-qAx͹nǔ"+dQ{Ub>&kd O7N@Vp%ɶB. >ЃOI˳%c-m_2&.ס(aZn >}|^g~Gm =Bw]D-׶a_N;O"{Gqu( nȗ*>6 5[ہUF5?{a&3o%ϺBB}ֵaC<ðV F.JBnywEx3e;Ro #j#2Nmz? +Ͼ*D"5ĭ#v{oԤOLoAy9M`͐ZA-@hɰ~Xd{5 ~OqFO9Yoڧ},tgnB^ǭ;Y+IO*+j-br!'7|Ⱥ^%κWH[5_8ZV5uf(]S +c6gO.s- -xp$ [ו\j}`-ZLiShy-~4UVEY}K:iRmXӼ6xCK?JmuCR+N&GӐ!QT/H(?*Wѣv3s1哹[|$ | +߫ڪdQoZYV|+5 Pp G'˛ OM<)Qޝd9݊a+ŤBA ?co*`F^_q%+,c.AH;gU&⌬.~C9?Yjjr gW3uW1wKKv-1fJ&.[(&&p}íERjI#֢J8ss|&z T; '_kN :z]h5%VX~7&0XE}N{R +c-e.LH4ZY7yk m]J^`nr)o(+î +ӯAD֢>^.KG;!ά;J Fsx~34nUo=zUqAi@_͔ 4ܐ"!| ?[5_+j]#}家 )H}rW$6dPZ(t bkP#.;}M;mwԶ_{0t +~|.<%VHוhQST?H@R鮸~NRmn~(ց,ݪJ=M v抭-u7aյ"n{FE8˪iGkv2zG5QpYh}\7ְ,nn^Zfj^Ԏe\Aʲ]g4g}D {hlkzﱗS'Je1@g@wm[*ƉB6t +6VeKSX}T܍kӥWlmMo[q|R-RޗXjU"~y7rJc~+|gmyI㏻8Xv/Cޓ}ۏ%)v)U>{P_?lx6Qij[yvQ3[wP.HE7?/}̾[Kru'1>Q tUAWK^m4hi^ڜ3g-,bSEJ<n]y%lr8pMyuMkCJ@ E/P^w^D1zR޶`Du j=o*iTMa@ʢX›Kwj_V½Jv'+V ,ҚEzbX3a>yWc Z Rފu\+ɓ9T3%M{\ ]Z15_(q ג[[m{N0&Qu=ms7$W3l i0:]Hz_'-@P<M_f<%!.'cFvF;v)CU!`A Zqa\;<" -6>3o`#,eL86S +FLilߖ*q_b~:ώNa2 ML )/Z5:RLq'UZ17n2;ҟ8ާ_mcHGZ{p52Yla(%z@ (.m=]rEyHuؽ~.j`[ ڧ:;?v_23ᰓLIpk5WzXuua]9p.D[R3uyk @VwgBOsQK&x|8)LDU>1^̗tP`ujcϩT;ų}ڍpiv K޳XoL홿\cV,mʮUzU XF(PWT ՗ns[\ OzX^38F-havDl6޶'c͹3p؝PdP:)>ʕs !S+v5.0LHu0h 1~QQEqu3f׷7RX +̠Ez8dw=?mI/Bߙ^_ML\>XyմIZMbQx !bZ\PS`m?:G[ͪko|Y g%loSmZh.K;9>+f^~_(зߖF탵٢9K\ E/@7d7Wu构 t!^gQtoߓ! ;5dwpioHwԌj( v$~<\C.\(L$A-zXdUdwڻ5a|"O;T>97>Sh\}L +QHs@2°/l1C%Ըv^!dz$1@$Y?WR8>H*n`֭bR={M\ +?=.gX4RwOvkQQOm U%-\$f aHGsYժvo!5w}~1:SDQ7}|GoQ@4|J"c~ }3ˢݗnಞ zZ*QQ }@V."&vkwh5ڣ_`‚ W`8ހªYʅ9\Ӽ>XOe +TaՄL* Iu[CYSe+g=[ċw'0{~_cľSU'*bFA&pE18ɸ:8Cy} #V&B v-w%kFoQ|0k 7yNx?رm+6"ijܴ|xL'ń1`2YuStorvDM,*iGsL=`Bց!~qȍـlnvS83#(+/}G7KgU3Y,.R4nsQkʴ` +PP$N}T䜍wŸ&P|fx0Ĭ*"89pߔ6Ӑ+G~^WF+o`5*{ _yO_)=߸C%e-;}hǬЗ9 /#SvXgv]bRYlv}?w$Д}2n/s5Z-c|.jh"YTM{&8e*zKBB۷ +_n^q\nHZ)< 9|$kK;Awb}[ѥBu-X 5uPҷ +^D|Z7P[h6fЪdAUkQyq7)PJ3*ȍr4rFǨ,~.@[sܥfdؓV,PsvG}f >* E5ai^ꈇv;bX~IbeB→[sgF<Y(a:Jk =74#6/#|^n3sr눓A77Z~zbN, ih+/MF0!ZR:,_Q^: +.0n+S5( dORar-rvo6i[}8ZGgg_`:(] w-CU^ 2\W}RZ> +tdA +m'xk%JpnN)#C&+%(Qxc7/>m[N/ uP[LiABpWa .*ض9[z>dn)"4ԡYLma{MOCN|DUlHFdkګɘa'5)5S0p4%n&ܔeUjYnf6FULdo=M΋x?'L.GJwyX +BHi?PLrvw C '~4FPW}9)7 1WZS] i_h V}EYb-޼MBa>Zt\R!xBu9}մ.}7.'bs[2&ȡxwǒHhM=z;GGrGpno51qc//l?[@tҁA?wⒶ1}v4fDUhހTj1r ^уTM`q5,w{Ԍ) ^<[r6 s5c`J1Ho~E3(_Һ5Xjr.wYgܐ1R. +VYsm4 b!(,\>HˡUw7VqtA:F!V0}]c!9LvMi96~m 'E"6ZvgM @.^TPbձK]8}3VYt@q_sÙc VbO&9sX%&6V|mc&+<%t)nֽoEgqRNzB]z =;y{S&wjpY$k8OmI]cK㦞-[s՘mY\_m^_6B7v]Pр]-l/U&RXn]u}>V!j}msRQ/uj>%6he"BChИ5dB𿂈'8{hփ(R=놬ţ;5PE€omdņIc["(h@ƒ7RNc +"j/w xl:=g&^--pG7N`[V&^GÓF? Zjg.ݤe}I^]Z|XNQn;G{}Z[!1 :~7؃*Iy䱍urt$JA&c35C#n!v"jnUl"e^bx4/NV&)?PS-y'IM@УHC. +/Ž N>R"GKׄ⭅Y;\llۣ ,th^3`0P}тE冴n/lEKrszu +AWQcsteU1+F +-Oq+PY|vڬPkh-M7j;IS'+HRV +p("pLY?kvUH*Cs) +*mP/A:gJwq\utiV %bT2腾 eعP&|Tvm +Κs}TʦSޟ?2Va񳜸1 V*}Vs[Q.`W~bI[-OJn_(c/ZG#E%c#1[*b}BW3rvb~Nm.@roUf!"\gF%okնՒҰ +M295ˆq;gO +^{6ISkTBՀdd/ፚj_Cpo>LWJ)t#9A*LRBZRF*tV7m 7ka5P\,Uqe`a.6 Iߢ#^O+1ioc+`GM [-bB>F)^F"Sm6ۈVB\m] pO?`qj7BGk'QHR ?zG +Tt +E.feOm_c +;V9ףm +۵\?d **n}j i쵴h|71Itϻz[@k>sfEr5;SKiW9cCd[UF$ )nڕ Dn?P1C?:I/lSq+_\a-tiNiΦC2oDj/Kt9YYSE:@Ôjrt@gTPWz(2'X]hl]qv8n;ZBƙQ,D=~OZS[3}f>c=UTZ]Q+s*{ȋ%3M_Rs*t&0EuE(#p[ϛbXE;PjI+Ko~;Oc2n6!}q(FeNv9 Ec.E*6#-tb_o`c8'Iwa8I0&2 ;Q/,ppZToV M6.9x9zvtI莠ꐷ5@?ZϛV33RtpuĒ(Бbk-@Wy¯KI-ךC|*RYav6ݑmv'YO[wPS=OHhE`5FΠF }W&uglXZhX,fѝ'Q#PIIx^o`Zˍ^/}-L}Ʒ?Ո+^ _J2Ut5?heQk /[aYq:%WK>_$RIa:BrX< jpgdP'3q/F':}=`E"F^6M~G$վ%0ozT)(mKa֥ rTS#nw *LW'ʧ[]@*Mt;vqUrŹq$؁}%Be_)1lw' c4gDŽ6B +%l;. HN,L:0eBw +O4_эw +M=XyC""'>sBj3an/Ey˴u^1ڃqff `δ{*GwĢr#ۼ@<-Ja\G2=q.+{\uP?e)7]5;Bi:DƓk靇NM3nF2ehMo<Fߔ8g9ZV]Yk-&a gv{x Gݜn»(wc5RE&ת7L(ȑ]74X-E;7T(w&"diwFn>?[y%#~}0cBio2H˚icK.KQ)ExԾ6A lb +%% endstream endobj 35 0 obj <>stream +Ԩ5!B2X{a-c{suy6\ 'J 7[ G'tՙ% +kNpѹn@7H-+w6iV6-eJ̯NFuc=>+ i¢I7_9n ];L9P{W[2Irjn/(_Ⱥ,9V׉qFG䎾gJ︓L?u<2o7oR!]Co!uQ/,Yhu=,>}}z| z[3Mrk3+5ls" g=C|4ŬxtVP(&V>fr,w6!o 虢#g :vɩLLPBLG+sիb\U7/kʛPAQu7v'f& cLj-{iډu۝P6%77F[ݚ-n35 )3 ~>ӽʴs8S=7\8p]~Q~CV6kf{P-ؐlstP&DXDe}<4Dy\Bྒྷ й*,5ł6GvR_2E/m-k"k-Q,m UZ'݋w\"x!qAiEБ]aT)ފ3W&T+\!mFrBɾ΄UEHkҦ4e`2 Kzd5^N][ UuɠP0>BߜWKm^D{<`7\rà aЯپDDDW$:H*M  Sbe:SfH_quجÚqJZ,l0׹R:Je/BN먮U^] %eʆ }vUX_oR`3M[pqtm鐇óT IkxAir><,>?O4%g<+Z#,G_'w`nx`UWnq|a!΅k;/oz"="/, JV$AF@d;J(&-V/-7Lm:P#Z1&ES=VAkў"3G\,ml`CgL('xZ8:H$E\z>{^\j%f_ ':Vʞ{5c?UӘ"ڇ^sic62V`fۭnV,P1˖]}y8!v_tXV4R\h]>l3Y2=lx!ty=^>|_yV,0f4ُ`/)T,nSC%\D'p7QWrnVR?ƮuYP'5߈ sVy0;Z]^6pޠuOddxrxꡀ`C@؆tMtˁ^뤖'W 7Eto^'mD#aN%)ky?|d1{X( ָU)uoc[p¹N=`C ټ7gXφ0j@T/ߪ:-7ZѺ|4\j.H;MK>um:y01b!Adc$F-y| ІUE54+($}V#`5L%8FezR|d +63zֺwthP݁]SCЇWƷ8- ǔ؁ӎڰ2*zO1B OUTMƈ] f04M:@6) [m55gV,s"lYnxV/lK_Zdt>њupS{܌!jxEZ'PJ_gߙ=>˸=sy J^[LQPY8=CXfYps]ͬ@hj;ZݺMz`2ѮQOvP)@8ZK37Tlw'r;$I;t{a:)ٚlЧzm޻hMOvps'K.HG|pI<:W7kqQ;+ 2kM_^&Pz˛1W |f\!:J5Kkqc SSXkuj{"|۳-Bw)<ەΎ?"9֭|者͹}_7;E墓װaʸt%vxk JжFܫhQOp"vpV 2!zîgR> O|4{ӆ5wKv*ݛ^ˆ䎯=kk0y<\e.]FӳX`_v]y]MCgv{*OaɽΩWqN{h_{lI?vkSK( [O.gc1FБʦ HCy )ևSK?.>2ez{J%1d,wX&ҧfsQ6/b߶xWscUFcig5biL 6: #Lk#B6Z*Ǫ =2d姕Ognh3a5>Gp-S.|s zxml;`no6]h?Ҟ$V+Zы4S{BO!t{{lgUoH[f3^ ZW'u:2H$ڴ7FV:;9[~~ V"=pG 2E6e:j32}6M!/{$§}8| 5%tm>UXzf7=6цsnŏszp/3W-<ab\ɑ9&{kM"\yRe*y+ʭ0 ~Pڳ?tk3Z(OGPwhѱr"`xVÓA~Jx%c#{)vw5`RŊ綽0:i/AQrlt3i܊s4@u=.&C|7Z +X_jDDk]lZ}Ic3_< sÌYyuJp50nj5NyҺ԰{zϫ<0}k=b\$d?T47vV0@O ZA?m\]Ť/" $o`sw[|1zK% j,O `'6^%2 fBb_z#<)^t"A_ξPfHev{T΋YkZ?]taŢ>5]Iw߾::Ko7}am3ui|6ufPPF;21L;wVW w~*q]7}Kz"B5`vwm[8(K:sIC~1<#DjRheg-w(2x5VϺ(>d;BCm!0εG8Tِq|to39W^Qe;yyU"|7Ut>')7Ö竭9P{fŗy]O7P*J'[0#Ёb[k{q!3(DQ=H=DG0t˗1DYd662Wsd!_?y`8ܸl_99荝-fo+v̂ag68`:G:#?Vg 6UW:x'6gFxԅMY8|L Oo4NyZ8&F&wyjm <{IݽhRB6+q*&(Vƀ.}#:փ*;N3So)`^8>=luJn=iKo\~4ytLr/bꕟ caL:̎Gΰiy Xjo3g%(ҒY=e2מ.ý nkuzqHb77lRv] ,Z%e&ipDz7s =ƽ\/Xr{3atFV{٧YЅ .g@IkZtQ4;׃h2^nӵqYfo UQmnu{h9Jkg 'aZX+nSYUʹ@CnEW({43lFYvp WXt K6_-$nHKCm>%0:[A7rwl]sϗ&h;էBlTEt5h,fSŷx 7~ЗުnBNƠ헯Z>Yߔ3;cN$pm 'vݐ?f:Cy#"+?㍭)s˷.bvZgKӏF*5*^ՕD5o.>OhgөgCcҷMD)[k@aVPvBSD4n{"^-LKUV~kE5e*Tz[NZ}1r[ZSvmʵ.S%hm7~oK7k0ȝ|b좿.փm'"/Vqg]uWeb<Ʃ[5 Okn QÆ q,'U_ 8V-P|=snOpQ GKF ]屳 sVߦLS͋7R]"2R$G݇j=~XDBUU)$j^L s9ƑO?©dW%|梾Gu3uHUTb o{ds@ JKQ\Z~buD{\ +ZqDŽu;Rf5 2s Kjuo :\,{0 +ᙳ; }S_a|B)U}`WW"1vcmKл45Yhw(u7d᳟T5(d|"-Rpr5+91,p Q\FLs#@@耕ƻ{lj % +Ap4FyM!Z_ "|?G0>Vgڳ@EGĽRd؞}d\zẍOntۯ*I\yV<w][&&mU/Szorva}/p&u8FX̰m}wg+6ypv։ $%] >To/ǛpydHww$S;a_ +r3+K@9Z!ˮ,{*#u2R>.棯SVLkmE!b#Hylwxݒ(m50kZuс.QYՁGM& \=5 8It"$kdðubZZOO +fo4w㫽[Lz#! +K usV#ݞ2 M +7bw~3 *0#YW mkΉs8-ds2C*vj n>߳UT~Qh|5|Ҏ&ї^pw\ꏨ].kWh@̐14LëʈwըBvI ϔ.fM+cMrN3PѠaSڦJZ߆yqrfF X]|,'>,NCSbFO +1NenXIղ ޚV&{+I*Zm#~=XjVq,t1K~0]QiFZ[slE s=k/o?hGsL#$o0V3$;dJ[7>l1l~V_c1NQ]]lXbT;\o4>Aw]kR]\PMd#.M+ ͺkXq:K,~RNփ~==6ُI{s, (j5ryxW5w%Nq+@s(5(a#p;ni,<;ӻ$&8طN25H+Upi1GݚPV6ߪsL%u͕y@1"AjevvŜ-K;Fht,y쬝_O9N5Ky\}6-ano6zSyDl@"+-|m+)9^=_YwVv/MC˽Q7)^.Fz[u2R\&Agqw]"EC(nz +Ugj5`7ǃy\)e 1=MlSw0^V[TR <}XJIwdqSx{솓h'D2͜ka5Ys/]=thW<祡W],ƁjzIzwYޛ;rhaݟm۽OZw;^wl಻w_uA3U^Ķ^}=^؁8K.AFUs0Jv}+%Zb "Nf{!9,_vk ~s3,9/,G26UӊR8ԙ0ۍ{Gi혱@'EqeZ-mwݽ4Y(,:b?H[E{ +4|}ooa)uLˑ;M*V`ϳŹM}g:/=?x6߅ω@ۓwێ>m_ 75b5$VKn9.m rZ3S9;?ǃ0rM`1WGvOņc;!;c-GUo}7oZV5\PMEr@!]c-ՌKF[ `)vdw`Yo`9p5{Rj0w,\wxOƓa@P.ƴ1_݆=OZU-l,&v')iMS󂵘qp-7;wB8@b=3y7gYNwɸ$TRTG̴#ڤ,l)57n=~m(E3C(~—O9Q{v,l{kAIdBYCʋމ|dmrX#yF y>L1:u,u)f7fmM5}1lF{~GdgH=w@oQO4ֺ]nݔK%/IS]ĕ +dexJ]/blܞUet[*k$5-A#r]ξKu㒀>+a5r%,.֫Z#9Ȧb.[bԓ 8ƕZ5|\q XVm3*ķ純=u0=EQHM;TvoQqWylGϪ F\U͟dC?H>%N\oڥ]N2rvB>u歱s +ݠ?? ^ȸ tr,--s_X^IMG}<>g"l:,9xd2xiQŠ?hz!mj؂%|#-I} מ?|wѠI.5QoL \?C|BŰ_uH3-reGEM tq` koK> 伴QaX3_k:]Wi1s&Jʳw^BW3`$6*AbS^h\T #lED)'j|\UX8b8@ORṵuNԬi5,P4@X9bfAw[t]כT Gu*M^=|&7xU^ qlJQjeo8}Z!*­% >6dĖ8n YNt(D02'KҐT]rP~M5BhNtwy?ՏO>l*TZk\N{i}ZN3:5d)ĨO5z1KʎpjƁnvo? n?/fzp1t<}#x^>N ji;y{֫\m:s&5$QBr'zsBT܌?Li\7Ksm&+ۆټX`]_$Z;jlًԵV5ܼ﬑a}!fmwU=VBmY3G>Nƺh3G%yAfXVe~/Rc0HKQ,lA}c;O9mءz6<2X̲@ڜSp'iK4~c6`OCv\nH:FݢS|˱0KF4O]yʹX;6I^V5Fe@JPkhp7 ^L/ 3v>w.TپӉP6~nIׯqW2V=chfXѬɶ[hhsD;xʚd?MAizmG7РɱL~"g,9nhchKl@3擲 +2n?j%ݨҒs(alAs|.\O"_\QJM<=42$FnZN]aCZ#@uȎx֙^_*lԫKUD? vO/S +?lvl# g^3G9fvU5a \)/vm'y6`.}ǽq=L5,XY,/o E$cT^Z͆azd-ՙh6isZbbtͱIeSxI@2֌ vAێ }Xoo+;_X>彟>ImhY  +o#<1rq`FoW_3⭣i Fe[=R"aOs\=]aPK>]m0`9vU. N܆9yuنȬ6ꝏ&rT¿Uq?vDEVn2+j'{~IQӥӼV KĿ>6z2W{N;?cO}׏JdO֋Lįա{\?fk&ƀqx\yf{“FCBD6 _=1X8 vO:WrYo8wy])QTr6C}"'T !)pYqCL}\:yל_*bcrk7VDm~PMչA{B*}Sk]k-\Ĺs҃E"Zib*Zպ^4gc:amtUJ F=]@&ۛ +yEru ,'`S@[uSaI1*ȥYkp(>P_)| oܮJBLqHcep4$/.|K[yv8a;KcXf&wQ=IHFcAk׳[rD]揥a_-&(V/Ʈ-vN0wXvLZ(y\lFxt17}[2c$lkue]t|\!vRBZ-@[dY2*M6{/ښcYT_F#*+}IJ)/l_KHvk߫=RWM\3ǚҿ@lӸ.v}ԳlۛcdG{:g|yApc[3 E~Tg`~EV{s5owZ/ġGSyOȞڵ7sO3`& +l^gxCX,s[ >"I9Y&&knvf?o;3_n4cw`ʵXvVͭI=#Mwg,bҫZ"t^1o{!J\[Mfic%%5Cl6:tlּEQAYe&2 ̐% MY Hϙy(Ca 2dsj<=*B~`;f 1oe6~lt1܃]3gM( +l} }U 1ս Z%C)G<4ɹ+ؾi7ٵ?(ts2}TC, +!ѽnYMeijeUƿ854/$ޘM K_t3 +Z)7}a Պk&_̰.*znC<^TAe Yee AMSᴲl;YJ};B 'gi'Dl^k xFE5ﱉ^ {ͬm/RTm%끌L׺]b:^i;M(A8ka}xޤV<@O h6%N^- <."JG3vGIC@zb5.u~&69J ^<|$aů^ٮ;3n~ z +z޹C7m[G?`CvҙSN+\]+N%VO7՘;z£gKS.;[$j7gb5;ɡjl?{#n `)sI2(t6祱lZceRDZ M"=mr|;ArZ8{bs +pT)t.jIVjّ9XsA5OzZiScq*>#}4fS$Uݘh-ݸ`1Hgj4DTxlVL/JL>&{dR4G i5ω۲I; :Wun+9~n#;]hFsL+rћty}b3t#ݬd[Y^g}X8*0il3geJE9]?8HE +H٘HN)( b FL p]ҕaiLL˅]kbpkG Kޝa+n6[M_\]Xc״l69 l\| +_E2璘K2SW ."F+",zO%&_y*jSO&(QVJi )C&;ȕt.ހGYo6b;Rʺ`%]>)y*2"_>sg+ ZFH pj!;'UË{Eb_ڥPem/W!r&$ƿ?@<̧s {zWhYl@RwDla6#ΪYm٫W[oaE#<%uusuw>WGëh#X>s:/{U98|7\wOq%,gķJu=x珤7"n#dc;+tM3.h>Ox DFzc V+'@+wk8M*,d=MW(Eg}߇=Q]}*g7KI[;v߲9nf_pE,H=\U0mOc@B|ʼn@tô\=<}ܰ ۢn<j/(W"`窓&z 9ŪbICzVҾhc$od3Z!NHYOR4%vB:L`]h$n3y{8xjzu4m=YR:NFNJ̟N1P˄;4@rݥ|3-y#7P%MҩSA[A^!(&cyll `*Š;Wܭ Bϩ1EAmzt9 +WanP PNNmhu%[ j@wY*o153jKn_)foY׃?ľsr(O%&~Ykյ \r|34LoW," +<"iP1e5 +x_J!+ȬENx@hN|~_ ˳ڑ.T,\@_Z'xC[&nvĤ\5\ +5LMfʔMnat] +EQ6YoRtHa'assї<1^%dWܔo?%6Ws'}ު;, A_j4zS%%IRR7 5UdEEsݞ7-&4q)b\' +U/ChF+Pu}*xX;Buqw/s#d }CIű^]~-Hތ Iiz\Js>`,qڪ tƞVw̨TMnl'q=n( /-v2+DLo>5,?k1n+0ζjXswp@{v0>_(*=M#NE3r[[q0j`إd\+G? U |g$9( 6JYf̲)lӏ mmrl`:MLf*̅oZ!@wL|{gΕw/-]ȡ868H/ +RT0+4[BE.j.AJ4WA޸Yj "f(Rm{ j|4\eS[Sߜ]oCH_u('G@kz~tI]ljD}+h~9Od^·xٹ]pwgۄ\ׂay]XGR цMԵW}ܶF_K-zXy|Vy%w2lX}7gB5p!auq>;gxR뗫|ÒϊųNߴvZ҆*Z X[bQds{ L7WM=XSgCX:.`׌ a,{w]z`z:PAcd#Պ[ϵ%Mٵ]Urc)1-cF{V4,p ]xLp"g}:.6\/QO5]GEvl]][qMym\>9)S؁f?\ž}+umq;EȤCY73F-{+|{:fsAV39 FyQvUcYUOoZ܂WvߺkIjX;>Mѵ[ߧ0=2p5uy@e06!p[teeu%6sNǐBx=`3EBDQq۳-1Z֙ܳl Ʋ"GjMq!(BBE[D2b]oT^q;l}c:.[۾Z}CBw7NԑRiN!yM\uf"-qV)?oޓn_Nj#yF FWW6xACd"#3UatU(h ګՉ+89{8%Q06[l9?(V`ӻrbf+{( rY1uuY,^Q;F}^kipo[d }"xKOV=AjK=[S1e;;7Ӡe*hNi)hípUeWVG;=dU{>J,ᾮF|4*$v]񡘱ӧ>z"'7PMѼ'Y_LɎ%|4W%O dP# eQ#F1ok37&o^Q7H2;*1'UY0՝pJCtHzb4W;$m.oF-/ J;.-nWCTb +{j_^|߱~\,{"b_=Z͔VE5 @k@65bɯ{]˓sG‡dļ +/zml8ť`ϑKk+~.wswOLbw4rlؿ1|W{cm6ȷ;.ң>au;4DcÏ*'cbbv_r zi]mj)Jm`QȾ]ޛmk|\4/SI- lÎ.O&5͍P#nkpnSm?PW?Q*{f{x Sʧ;삁eg-i¾Xys2a9+Yx/)c3 +@'պxV?S{'wkz^*: T U\ y<%4]* +sx\yfAn$֍@v_s +PSǴJŞ)B4ja*vt >M'4${K!Qt}ԉz7Sc +jt9xVw6cߥW㊜6"Z_a| NUiHmډ.六ka` YF2϶bA_H#x:2{.tfi08ĴY͔^o&ڃ47sUF?Q¬MFjl,bLT57$%Ni,eh?tw^:їumoK[ѫ4CfFAOaPe:maA0nZ"UPrlM Lv۹3+WƌrNw%(:DE `a@EA@%I90+*|_TWr[晹'F(ǴNhO~hr +&m8SMC;Y\M}Ucqwm"v^Bc=J@"?׈WL~ҊnJczpҴR_˜2ǖ|/R%Dkّ4t'T'*oFM_m]>DCRpgq>ߑھBygt!1Atjj zvT">SN~ wHu"G=,A,P^+!7ܞ^Z6oonq0l\JjNV{#ݭgu[YBsW^D{ l:L#tdz*磌|ǝ-vS;m\JXU%ENd1>C4կsz0[2U?3@ς`5Bw03~e` +@0;qUv7\=-@6&@]j M)NYy,4pQΕ\3wP!kljjϦ=j`o=u &oϣ2Q^h%YVeHPyUV,p>: Htkvh JK{3H>,{I̚ YO5륯ܥ?3,ΪGtZ/wJs5GѡzTq C\\tk ]tnsôixLt5QM`*c\ZT _ҧgYq&cW>L~%Ϲkx 殥Vw QJ^%c݈[,zkO$< >!Zʨy1g#E3Ur|zM,sTV k +d*6:/2`zC~ZHZ8|j5[Xs˅,/fqA djD䶡ݝ=?o#e>!grOTs5d1HHDuteS Nz݊?##=JjjeCު{QuhaͯK>}01Q rwxhAAilPQ.dP`<2$ӹ.7u9{|}uSLş<:'Wd`YC+9 kGѐ<Μ6^鵩ZjKYȂ'Nzύs0=CB0C˪Q:rTτ6a+U5 *=1M,F?)-z&W71mu>6+`m_)){]91'N`ȘaW<] ז"cW0N*ȶmm{yPaNp4ny8ٓbYEPbn$'kLKqλ;9jv'/ ܙ#{Y5uk5NkU7ؽȭ[ls}rkH9!j0gd +V9ttglӷ#B8e-ilΜاya9acM*F~xoK!FtY듳JtpFG{bA.i>=]fSe{if]FZ%++-3X[=~> $6??`:soWSk'žkI]ZݜݙUXR`p'XĴ*ũmSt 57mtm[ꩈ~` ˴UƊ J4. (W?QW0?ۑڸTZ[clʗr<>$uz+?FWyuii#dmSͫ#"p Na-G}9f8Xk6=o45e7۴j2xHNi˩+L 9)srh碠mZabWPL fmڥ(%jih}w8tcaJw_^)rRD/neg si kmo+zi҇9Q/;:ܮ6Q_G9)uL;g9Xa#}E3+gq#dVk+,#YKe`Cd;>~@6=ӔÒ`_!N,3$NyC-y]wn ryzhB Ou2jծ-R9Ә|7{k6%^\g3i'.B4K zq8 j65z0gWu '#ʷz#|tiCwuJ7Vȣ|wo1yW7`-'߿oMB "Ɋ: Ԣ@!G{Iث7K9)BIq•=5dh4$ä^۩;M$Zow\9N|M>L@FEx qĕa֟}'"|̼4 ӉJqEêv'<,Un.6,va["7>D55OWO[4_p8(x0(;/$f ߸塔dL9tG]:a!1<+bI1lsE)2G3Iejhk\0=Z&1#s[ bO{N,Aζ;aB,ۦ_YzY8wLfIfo|lh1ߓwSihı9Yn$fJ YwI~Gk5ڡXq*qre _ g^l%MΦR6x8՜W h=h徒WNq58/#N][]NX,0jKH>3RˆCuNj +p@Wzr M'G;QBCK==M+E]:6j3bC%p9N~ YYu^2J!B$t}C_Xֳ Hu:7U"׆ uVDn&9ܨ 6U53)T"Tf?l4qyOSXgoc)EOY^&wQ.`ƫvyx* +"%6ɲR/>Ӵ[9M{ <` 2\5vh~4-UwA<Qt'JȎ+7AU(?( a( #$rpKEY8ۤtn/^;WⰀ  F}㧋dsޏV#SQٯLG+n6{a)W}yYJK7G$Z~W^CRU()Ocƞq}(9BFqtZy m&j:9*Ш0-x25lr\zTfw!tײ +%O֫DNd?S+c\jVf=GFEI!n/Wk8cy4~hEc*h`M4Ƣ)#\T%|cvvۄr#CRZ˪*eVKˑ.\M:|ŏ?@xe-.,a%UkňeVOѾǭُVc>_a4.i:yȿO=㍨S/u1FR} o[$.^VBϞvʝg,p^ o%(G!1.¾W];޷l93u:VOOk=4h'A;\8c"u9\ hI Y)>D,9'|5b+ +|3}s"]C3fvRϒd;N䓹RDM;FaӨ'4L؞BueN>XRT2G^Oש#<{upyMMLIurܱď);f8㭼Vlhm13X3Lk}UA5L7[9 'P- DsJxcV^ ՅTw/<^:-PP:ϜDY̒:F?t]0_Ĥ4ܞs71׈5yZQ\__R@;~@h;tU8HUINMp1-)c6#d9OdmO<GKVN[x^Js~mO5cÖ"j>cD;@V MZj%wjd[3wP܉kӑ0`⬴yf]C8kpTeN`,֩ w JsFs99{|CҬ۸Ù{uݪܱOZWQ `%o-uCZ.vpp Cş\i?5G喊с^aQ$ɟhV؂YŜV^x,͘I +7,YOmS*Bn <[>)ZV3yDã%..xٌn)%VB^V}ͥ'*Rp{xjWuӼU/:y +sGCLpDD}t9T<۟QYT:ũ$߼ J&OHּC/zyDC-G͖uK3O՝['woXp Y7ħEovzBab6mb)Ii~Wؗ%]`KMs4wȀ飡qZٙ#k|n{î(p~p^5KQ /x`1h/B^řvE{ `g6|{Z Rg=^)+P21pH` iv2G%7Z rrrpιi{j^c8Բ Ci!Z5lm0:4W]G^l6=MG>l8ſ(F7Uv7/S*%]p>ߚpk؍D}. s[g}Y閷NkEGlmiΞus,d'yTO NeL)i5]\Օ?-\e݊RiMVM&x(>+{܄A ?@ 7Ht{5ܝ~yާ. ՁX 4e%(gDjZ[dmK{֞rاN0BcnUQ`Wo.)%8\R;oE{g$IG ;eZLdSD.fi`m?1u;;i~Sl-ԩ]Uk hA33]^liqOek~}߹u<[:H]KP#}~6U7jwiNNW;SvJm_ +A {LB>3s'^_3 cAC̈FR; 6{/3z\n m˯a) aq .'|5q.C@Y-sy1ۗ!=$fmhЯE nu}~׏e8L;_$4B6qd̵Chuڟ~O{ul6Oy[Gjd1D{'ZدM΍{:Eo"|+ZWVނ|zYnmec[\\#6![TsbL^Pνa!mʻM 8vjT9jy?]u&ň)X<ٟN H!nU lP5cNWN!q6}{`w =~vgOۿ[$/n8ݤ'mRAn93{˖7k1w7yϰt*v>bcMfAnwbOJp)~"Cw,H>HZ#\-gICНT}ܠhʜ;fM])i2c7h_RptF5e􂰏O{0n}mݴ.e*; q"+ |!Ϯ5vV:@-<}wUw}^ߑZxuO~gVjCȢ vg3͘DN['k#FI׊]㮫5PUd'q #Vߠum] JG3DN^"H%iryۥϳkvmX r_Sw,mG3OҴ&Cg p蒇+?1.[W}Ij|O{NvW"eR?*YqXh>E;狅6ve9Q+U{vԑ5IO-p|jʼVn+jzP=mdy#vuNC,T7Kgrީlm,>o۲DqO1S3`^Va3QO1꽗B4i:UKJlj.ug.]&sݶ`w,e%m \'ϧ^XM{ݡBREw2N6\{v>ʮUAYX6[= +;cK(qKRU񹿷/BAݖQx2뉏3WCF}S #j1K=߻wC*s GfN\s"~<ţM0^`*@uD(WxiCs^Zo韅}0a_{)4q3 +1ZV{`D5CvfŧT.Wzb$Ka{S@{ĜLT>G)6QX-;ǶZMeiD2\{-! 7Z7N4vllsKyq˪#S*)ά\X&# )lmz@$oƠٓ$7ڻÍh3kaH:"2{l:X~ˮVݒ֎soٍV7QW|ݻX/scB9l0 yyT[Jm\9zڼ:sNtްBc-#ooiiܗւ1KzxB*/Y0^*'YPoüwADVA}fyW֖qR=-nIQϾl qΤtaXϦ5էa[7m>VZ$Uߧ}195~o{f؋-Ըq@!gl?&P"b7Nܼ?}BZ4\oӓN?/1[ל7iqeB|n`(y嫼9gno&H0aI S I.xȸ\Ȩ(CU[t/͙ܺL]+Ӄy[oo`+PhN] hٖLM7^#LjTᮄoP_L]e`mhI?Jϋ#Q˷vgg<ٹJYoA-9=QMm^_B䴢7_AJV)}wE0E72y%=@_mhrJHz?RɾT^UBc@E6o7sx,kWSͥoq +J* _KZ5uⓌNJk21ژqx/qT҈+׵-fmrّ[2>+Yu/3dxg`3G;Sm6~t]S; fV ᝏXcRѪ.cwg +?E[IOko?pe.յ}ݖl$.^Sn2XI\k?@`^?o: MPƿhxG"mK̘Q/ʀ'_>ًFlDYTD6SiJO?iQ[Xr7׵1;ҧɜ}Yay`oˠodBzujykan)=mm:@e].!ܒ^]X/;F)m< /ƛ08Gl.(nAўwUvGҙv[ά#n?uZbfRk;^_nT.w-tό.wtA^>u:2uC‚""*J֪oD-M;I *===m}`, cbIv.:38h Yn*=mC_X-C ǧ9` n(TvwVTjpl2U~ȁf{XkEJ2A((Й2cc4jp?,IأtdgiP`2ҳңi#'7}Sϥ?c^+MV݂l\ۍKq2+mQ9+Y!QOM /2oww\9Yo%ޡ^xr|M) +mx]s傝/S˒/ 6nCnN%{1`E#",޽k?}>#qVjʥޑn 5?1ޭTRud`I`=s-F孡ӿ= V+L{a̬vTe@l#/zCMϑ;wguvԛME5]?ЩS6%wry{cy&9s5<_ED+F[kI7N[}$\ y'5hryҜʶy:`\%u㍧mjϕJn~;0;ԃ˄4t6Ԫ ۏMkW=c@wEVnia^,vcԍ{91 iONrY=|<]r7wQަ9m +qAxiBcb@r2NǮ ݭMQqq x=[phc>Ij KPNts5/*ٽ;&۪_X`@g؋85+oR8_ +<|߁56JP  $ZY꠽ڪh="ٷ?o'nӐ8˦.Qިke v&Uzn !WiWJجE5963-SZ$yگPOAPXY5SwƗ~82ӗՏ[J,$77KF~P(~T;>n8B@)7fc.޿ySAwpTVg0ˍ/0<=nJ,ԮXj5 S)z%>}?qN=p&BT zK&̑Įӿw񄗯S_<1[Y_.9v}QJޡbŗ!zƤ<:}zV:|M1q^ڤRAoY(qysm* UOۛn%[O0QĊ +EDuiy$tLJvVNUA{[,]={_a\CD[aqR9VДw}DEBs7ܵXXXlE^ +R:7]tU-8poTH^y+w35S*rfV_%uo QQT*VՎsWNѲRA~P%J6kes?.w*ѨݷO@?/UI#HD41pK8 j^'VދBm +5'wO9iPƠوU)h*9 5IurUw #>Ebˁ]{T[Uěrws@<)fw$HU(}0^4Zл1J3`l)wq'AJ}ˮ)RL\}U_Ds 2yY߅h.rE@[<%F ۓ) p|]D'H䆼 j[MAΪ\8Dtz4UH}f2ј7\*B%oZQ›K(y}r`RrTi6"3;9XFi^Zu46K( j*"/.TniY*`wT^|fъ^p տS*<ب:(RjO{!4jHx_wXo0NgOgT#d"LF.ge@_b/N>B04@5ٝ+>Y$) $]o/ zxtZ r^($;814>1r[MyEit.`.rD=BFY:7lze.ǎ|%K18vbnݍ#`˙i#}ͽJׯ[]d$eBv6;X^?G4-'.\\?}KUihlKzUxBw~NYuȅmYkWgO ʴ eU(X[(u.YBrW %`3V9򢋧$ +VULJok-LR{Z)qC%W{^(AGgcGcB` +YG$ʔM,+%aKTkOV*Fů|ZA^uHmv\T Cs"Oy&NJl,j@}yө܃ QԎac!+j~鹡hRAr6জZs][NqZkČl%%pAO|y=݇gӬVOYj~rm=S=>;:xh6{䪱,^筒Y(8>ooSB,X.WDƺKOZk + + tnKGo'񲞕w&Rz\MKS|løvHΝVr .M.r7 :o+>G2 ?o4@NK;Wi҆t3PEC{ j4BrE}B" 0vى{DT'lz%0qfKaz.7S:LЕbmu@+޵"*Qo /ŜwIkK{C&cϝz_*RjM6v~.,Yނ4MltM*#Q7r^(YW$ ܔωzXK5@h&p;04ULyXg}7²e9M5k}7IQwcejTݹW&`h$LUA=_pF S.6)R Մih(3uZl7M4] j0orhjbS +Pv\(p۳ +1'k[h ^>?fdOT]K=ѻCX-ꈮ%ŨL#wܱ @X}K@> 6wطAnY)PK5H:}=Yu4zk +IkD^fd)ᅅ0xn; S;V kxګ/LSS .z"dSb]iL%mTD +C,Kߚ2沼Mȕ&94'͛ƍG/;n q)w'6 9Dr5%Zy [:&:vf`GBHlO2ԀCOu:5~xl l2i/]xp19Z:P JS-sh.TRmKgG7.u*(ŢVVRIy^lluSf !F>644-RjYA陳7E00׈;}tThs*˽B|ќmY&$[0͝::ЮblYV 'g: #`]szzKCvKrN.veu74sEtc͘1](|\c=ml/ͬm]zQo;gDEE5+=nr~[X"PuDT? rh_0O ˏkU&(Y *uhZ=K?+k.[`Dz% %9ZT +tZys}?KuX=$J@e{!n_b10U|XmҿZNd`*AV'wwzE.7eh<.1`1㠇nC|׍E+Jrd})+z It2i"WzW~ǽ^xK+UkQc)d*A傡NJ.[IU*T**X;ءii+˪hpkEy?w^Vg.?zγ4vq䶯U,^VB{^H^n3Lnvptv72ú) dΰ/SPV9H^Zހfؠu j1x6ܴQ +u R:d<p[Il[l+Rs[@2h{Z^ irt mOmǻgXc H9VDWklq#O(X~@z +ʲ.__: ~g#Rut~(%XbJ K;5T* wM7J`jdΦ`^V8|3x5\Wc&p!;d"">ЃKUU#m_; @.7i%6lRxpܠ՞~~TrgVjv0ƳJ"mFV'wRO*4vS􍫮L2X֋)Rù؄>+U |-.?pY 1X5U~ËqދG#yކMl|7u w"*1 t׽szIVw^ZuңWOV*YMu 1Q$=|dxcV,U&J^v5au!J;uړrwiXw(wQ|=~I/ g,Z pUW`/*DN3;zC GVLQ^{`:=}bcdžvkn'Y@n!WT>VˑȻp^ +a^xƸ~ +|LR]H 5pJ6j2'Ϻnhc.vd[ς퉤NͨrĹ_odNǗ6pky#1]3bx~UN Gcf1ZiԢmEG{9ő-[]y)b8iX U[9EgW0%_l%h5eG:XO1`|T(]3f4|HRPڲO%Q\"pm"@؂,QX7EݒHI9E< ,rK\h P%,S[u'hPe͋-襜oaՍFr3`)w/;u#]NС|>kYfAQ^_Hlцrڞ9!ՙ5yn*5mXjZ+ϦZӎ0׉Rl7$)ps_[ڪ"tD#JLO~'ua,-Og'{2kߐ^aA Q/9~s߅וJC^.[bF5|%^%\_0%w' tdrjb7W(7\Dlұex4.bEaN VrCLs/o3=`#u2 ӁlLt^-ה&ӈ1ͣhS Vbs΢vs;_oofm.J> >oJj{++6k䝠KVCC\):5`ʍ;Lh`OEO!ԔRs7Tehj~RV dM@g>ʪ>3J9T ~?HXpC5g~m^hi)*Spk헼TQRUQًԫacA%҇,&OapTNis *FʈVl]ɚZzxY_*ʹ%T/Vx>KCV +i-kr3tCeYk#ڪJQIA!;-_="Sir#jUΓ<ӞݰB'rOW;1njZY`jRxVqO g) +8]6ʛ=I~KWXg4J?F8,TEpgblox,)SC}XU¿1~J`hZ­u_ ӷR5=:4+[,CeURx%.J .24z)+^"M~8>&N"ypU +-2n fȗG-%B ^/2_ث3 +PXV1h~Ǔ%iYjg7)}| i`qyjf"iKЫ-]/A?Zrr gr/~rp9__+sao?<*1JσQYEVQu*o7q(;;n[E^KsƮ@QQTc+mKʱ߅;gVnyG$^BR)פF˒ecrwwdimfQT%LumXS1[J5q)x2;3Q̼c ֨Bܖugmw'&+^vҽ Հâk8^Las?%sAf$\ebo+<]=" 伧|,o)\Z!{T;H~s|[6[*\88Ze“թ0ɡC{2Sp%~i>bOS6ur׏CO:z.4u0Im<ֺؗ<>4jaiRN c-3w,W]vq EC=e_bŽ_V|a\Ӂq/7^C2@/`ss#]c[ +B~Uv$>:_,џ2џ2џo,DfcW3Q4c%{~YXn/7 w kf~g.E_?U}W8_s}{Y{=,s]7M'.3:k@˕/NpVi<!饃r;JZ 2gV|/;c} z߾#V/ /g#xƎCTun?]yd_`Pꝑ&$u{(r B+j%NUu7d4LgcqQȈqr-y{N ,7?y"؃zq~CD:fshb|:hfJp4bx U'HS8MHe."әlbNwuDh(dj(?/O^x]<{Ő>;?o'Mrtگpy42k| BfϾEU +9urF4%ZQf-EqX]K+-7 9Yqk:XW3=x +_SxgUJ=sMݺx}[m bc|/^MR1 Bj<5 r7>ѯ>Of1 `W,λ?ӤeC [7MƮ\1k4ܘW!R{-2. V<ݣR^+M+k=wW5y%VOr,}3+X^( {-S=ds?1:XgyƳ)2S=\>6CDش٩\k͹$LgCRep{Wxo2׹[͢{ r䭱 |˷h=kPhy}cIlK<ٷ|Fx{yή/r{=\ ˾Q φU(YuZN}WŦ3O*Rq9^Oǒ]CM.Rf":#0ǻ+ӧia߀%r㈁ chr~Uc\UKpxL> ?wNIfkNt\B?ϭSVorZg?ۇxhQ;uzvN|8eU%v?/hؽvnhZjkqOx;Y ?hq|`?Jžzdhwle.g O)Q{M3w2#8ϫaIF`!R:mi5[,׶Bt\ 8{a[Q}a4{wou{E ajQ\7d_$mOǻf!E??5_Dj.YKfj.= +UAgV^7A[*f\ f꿰j/{?ЌfW\n&Da1.j H;y,pv67}gen (U.!d{6w2 +j\_"6U8Jb%0?^(~avmpJ>k7έ?huDX/ ܙ_KI"zV~J8w6?w'8lvb"wMO /2D4h9ch vJg;MJqW;hYmtenx_kӯ&~YL3SZ%2% 4NqdӔd4G+u7:G}|\jֳxP4?c# uzF3\7~HZ<#4en2JIؕ^$eo[Yt鴽U!_*ׁh==|?U7<\K?oOdjry'CI| w%WUYsXͣ"jPP2 qxa= ha.Rڜ*t޲J?kfWv80/sxQ^'rYPLv ׳iʒ'B2AfJ4-O}XwrЛU" +ٚK ְ>6+9ޥMI`ϧb ~CLk)LyFYrIۘf=kdpG7 MO igR1#v[ݽ`"7|WJʰ+KgEJ; 2;߯>mkO5Yˏ`!|SH`ԧ's/!ͽ2]>l2ҁYڻwj+o^aL7>|:N]{-O&Mߎ0+?~P@ib)#{öS!*m"ڲZfNӇݛζDdž(߅Џ^A_[,6$[M2$iM3}$r\p],dG<}s_piRwocyvUuKQ~.̢Prɱ2sj7wԎc>_A`ᦾ#mWOדrfý3۱CdpA`0)== u'1.г*ݸ؏{"/Vtw,ux8gQc($q}̞&mHzXho ۿ_z{j+M"G!h6޾L7Sr*a"Zfx)C'a`4s69vxj+5{ &|vq>`AΓ۹I:sXߖ +*ϸW +|4"ER1奧oU0ffe +?Sxh{Bo CYWwVrssgkku},$y-/༆I5}-5:L,9M[pi {z +gR>o*~:9hո??D\~Ͻ[W=tװ8rҺfqjjZp\3 6-ן0S04 ]u?!ajhvgY @>]C:].}A1mPo-$α&Ǯ{ֶ4q DIXN +~_ł' lr*g1GsF%:0)R)D&nEZzE|yO͹d>/Y+93F^KSppsVve2vY\q6ࣆuSX0d4GU uy{/D=tw-4v:Ņl !_bwsYc8#.k=`^C>BoC%ȲR7D݌(wwiV/iʗ Ik1 L[J]Q ),qU}&ד|*3S% / 4+K} +s--NfL4̺L(I.;~_~4ͱ!#rYpj/D65DEӜ^A$t/bODs+#ϳT/gS!?P{5/?ƿkuJ.Ȃ +[Gw<^okuxݧS.S%&8ǸE}## +vKqLv[6veS_=ɰrQk^u7Kw1}wܿ$_FFzR^ +”O;[Vah 4G3% +Ԙf=-~$:/#4j͂stib'Yan2Z?)/8t +]Y"2SF=2z{1 G[|9z#rrt:}fp]6 -AMv ,9Kʜe!r(AH{v>jCK!ZYU\wM.O8 54ֽ]҈j^lkx(\>uq--zjp)lKA,-K3v +׻SznU""lAcXKc&TW? Ns5Ov:jfبe1ޟ5:ۛ_$t*0Rhێ `^4D9X NL89 +Mi$M_ޟk~6zmg(%+"pFyqdFdF ϛU]Uɪ? 0t;4lNޘQ~?g:4N޺Ѿ(❮=]v9.a +ʸN:m'57ELׯriJaU҅5KWWc "yfy7{[@ivlw)%Oo*ۺ/l*_OW#S#g8Fq +1WdW (U:`Y1돮#ssmgжO6:_mVW2{bnvHwoK ߰]oh&, Oz&_ __|i 'jv?A% Q=+Uwl;pψP?l.Xڎ|'Om]=KlomTJ,YC׼|dSV&~h"?L]|Tl5%2*/džRGO@9[?K&Ѹp7oeͪemԪ4U*.!W(KH]te s. +8r%oZ@斯S?bCl! A޸Us<;yX[׊rʜl/!vCkN6z\+vˡY.S}| ox2M i!V#kM҇]J**$RJ&~)quKv (%JAx}k{d4_hUP-iHcj.Bܑ3{1T ܕ:Pu_5Q :(t~=fnfg+|]#cׅQqxrڙWuoZUǡ42ٶuɹ8X`ݿr e&&~i*ˏ.zϳ;23U-h-Dۢi>0# +Z󤮆E 736^< +B; I O_ %~ 郩=`X8l+2GgnngLyvxIdi +@񫚽-p,bV-+LQi1@cWYt!7aE"XټTGAa@ċS Z5M&EI5}sWߡ Dx0qW.9p+)˄Z VQ?- c:K9!_K_×]I@f2h>e;uA ߴ@s .ر!0K>3K+d_#Kq塞Y6W9̛7tjB/c:Xg,_{uFֹ^Cq[t5 hOvI!G2:E:5"zM&@j-~y;_ :Z^™|rЎ:/3^]UQS.2^'&B޽N _"FWfo!++C3[Jx(tDgh~%[]DXoXty\U"2:q$;|2T6mD݂E6ozȆA*jM'fV|,Kms{'SƩifQ<i2=2.=]_l;/ϿZb`HӬU 0y LIdUz*C֞}21>Kb;1FwNG 54NcqiGoq  MyJkt_k Íp;'m-+|UA\9W͓*L&^lzXYF(^m%tKV%~;fD$+<;w-w{q,I21EyZjlr>h7*|62˛B!UP-U3DBűtuN|Ն*ٴ:%ٻ8Fq"7ޒqeC7g:5#m *:uyy$ 1ܚMb-<؀*c$ёcf>'>xGջLm/Æ."dn&}^y5lܝ@fKH@_" M0'Soh~( =OD *7?>˿//KV#Z)Rt#:)EJ-R]6>QlfZDeơaJK$K"S#bTWhܑFD fquZ?JKO [s zUr~#:`='&nf?[9Rzs?{|t?9t:DJuQЋ{\r\ZȞ (t5ҢG^+҉;MGZC'WU\=^gʃ-gM O_ 1!epq8JW2Q,jBCpoɿ7;{^X?7MQѷ.>yا{ϷK*9aqV u +#؊kn%t4,J'P[ӝ6N"cN^(5\_)X$1h\lvb."C SHA>JDwJöf`1y\N,y ffV2-S:k$0+X!G@YQsZ/5_|f9wDVhaϊ{VѤ$Yv QEfK~̱ܜQ1L:"5Uo׌Z2B1#ܮ:SU!qur,7cJiy^@p}a>^B|u#@:}~ ܱNKz) NybdkAjҶU,- :{g/(])\HYwi3vl#ez:2xY٦m[(v5^«7Nm\1+_$?em44)qX]_ne&KV>WS- :({4n ̍x t}bL +/+0'1_ncዙ/ȚZn7{jsҘ(duZlYڌ>̯5QPHv0whؿ[l¹>'Пlyry k,GE}`s>d᏷٬g[OӳvR43IR-s{Nǹ*,dRۺRs&-&BD@걷 ^:4?ϰ#v I>Hd318%vWl="=2 cW*BQ;5ۜ|5n =69MسQZ?E ?5ПJyI*=? RY +/SmlJ}.[O%tԅױx`R=ZIxjAx +Ht"\2 +縇TtFSGF9!^+-މܮkXX?ze +pꕀrߴe{S[z=~=V`1`1bQsQտ*+MޑnV +F*]$b{ә2]JU%\TDYeE525ү~ĕMno1t"\e~\2.F٤ +{mL~ +exg 8I٤]Y2&L}ubӚ#˵qG]u|2 t;1ԁKO;CqդG$7ZvOÕ̲luoaJNIGr~/}!5,F0gg^{RS,W۝V.KS7]'(",j1BsN8.l8xΌ_:cESuydJ 1Y:uWQ}U_'MlҜEzMn4Jp!A ARr߽qŴ]JsFz(e޿vd\,`Cg^jhp3Y_=2MV91V<^J"c&N7~Dt>TCkH@R*s(Š/ SZ; }%ouQ]{dr7{9\a mB57©閬2m>n7tHnZpu}k /1Bb#<+O};lSzlNGLs;q@*0}ѶP~+֭0qЍi/Y[wðV9uyv/G3%ArN&Ѷgfɣof1a|fmsK ' 5~ ;*:5yмe.I[N2𯑀~*,6+U&[8tZ@6"DH/ a~Wxrmd63.xXN2-6 q>܌Xd~g_G\q<1K\Fp\9,{\Iv3AI҇qĕ4,Fx|DQX*8W5haۯx~ +7xgF=.vyU?bkuIdnt^J e(i4@ybD̀(̳۰t-J  t˝\)kμ?р at;ϓ>0UwqyF"1\b۠u1YZq"+pa bnL;/}Z{קFn-[w{fn9վEth#EaM*r-7$?4 @o=q#N8;'B~mқ̽ j`76's69x<'-!tYbe=7Zpo _2F#?D1GP4쨙y~K{f*]N_֌kc}ǏH5pR#Vp/~ 6SJރ]Cs`Nʸ` &zh1Ni .?\ TdJд1ݥ),K9n&;{Sa +F@Yoˇsh~n!&5S8[uj>ȗ{& siMG+g0*1q*yS.}euԿI'__—g}mT=0ط]0|6Y)oV=#lV ՋgTݓyU576lpzȰm9cx`K.t0?Rhf,2%8 ~gUI>9yjxpc~~ q׿Db jG/M\OvNYL2% S=:\3A0Ns˫|ˑ>JxizdDz3;)_߭?D +W,^{eѩc{i36TeV/{˩\@\Nedz1&Ɉfn}0sY9t?@ X_j|6!KN]Oڰ|;ڞ]R}60)" ◨ܲH@hf#3ݾg[. Ǒ[<4,k$Wb?ӱB<*y9W7,EW _qCr?".E@81~f6뛼unp*=pBWe6qCwLq!4JT6ZAp !=p]KʅLMwg:L@_d!iPʯ;uG_7WP0DW=\ZȣHJoG9,uAI퉓?8wj֣ϖMfMy oN\!K\낗56X]WzKI&PL/]ťrJ*ZzY-zi(\zpx805ްFCqn{ܠWBeNtp˻53iu#(%e.[xbb+Ń tMT +2}˵#=Vb3Ƃ}Ƕ&&s7m~;ksg V75R:qі2P.<1 ]b&xd؉j_ۼ OK]-aK)IfMkk/u+}nWk&M-YKffmr;IF2 '?e;$"派%ơF b2Ut8X%H7P|)e n+f 0іg^2F%5(r [qC_>ȯ᭼Nj~vo!5^7|G`YcA{9t- }\d6nnA Yfqu`V2oXN22-U…,KkenrZG1ˠ¡̏nt]}ydD.طӘ>_T^mrm_s-reZ\kȪ͕`?_p[r-2"v+j*cY)K bY0ìIŋqRչt-XNRrg|jU|Ufg::϶IiV~L)^o#Si'=Ld$^Eqܶr;İ0J!ŕHN)wT$C$H@ KF"UGq kSjء#+;v%!*ws]|iL韊~C3coh~*( E KCR=1a6֍Lqe4Qc9T'".+& !>d)-㌶1ε$cz͟[XοTt&q11ܖR>wtwv&Nic?khksl(׽tO;fKKC|$p:8AGn5dɸ?޲icL4j{0EC[U@NV-ijQ_d 3o~-gMPeJ縲ھcd>C6x^ENql^Ϫ?On#֥u]ZNl%$c K{sMՖsF<.j2.# +%s~\m鰭d?X WY*uw^nٴ/Tr,BAb +C>dnʽj:0a&/?Ww{vW!#"kFMK+`yٙ*5*HK^gwƦ{&/(L=&CzTW1u*q0/?4ߗkTdO=t0A-R^txQz8K=]!-]vԴ~r s}{hQbuO_QykVGb/H5!3 p7{г{-﷣l黈KabkWf^:b.Ic79^q@OzOV¦}7ɾ+$ K"Lt#B3I]9)ur؎timYx/wfr֕8KNѹfb~0X_ZB꾵f!~8$Hl j *z62Fp6+ˏ~q0׈IUzMIU]s w_<q%1oo'LDxrX1ءtٚJGS[ԯm3$$W*t+.k[xY9|*ǹCʰu^o|mg;Nɝ/SMe6 FEz7]4 ϗSơ7ZIoi{E bT3—!\4p[1B)OͪG(]{5}'*´Btr궾u6[g>4u=347UgFv"m(k7Xܮw XxiMi7{ʬTUM13-  kr?%?y ^K^*Z"'kxky`)M1>̗h2oPl5?ۡvxI$>_?s @b-R_\ I4f:~|Fr] tx_xF` K<7؏!L@lVޚmF(Oon# F͕5vn(&OMow!{eCFV֌R>ʮsLČEI8L@kn>Y>X ^{ɏޙedY++?fǢ,f:ɸzq>DRMM wTLQ\."<+؛YKZX&=<\p~]Vjau#8O -) 8"LpV$x/ADZ5ع^*]N:P,>p|޽]2+ {&ɇ +N:H:03e k&J g|CU)!LR4fó?B+5дahgCv,[,U 9a.s[LLn3WSЖVReI-WL\"IJϥߐ&% f<'(]1 \m/; +R,rs,PM=K셯ni +^jsD/}VlAQ& $bUÛ,_pxܫoteY+sE ד1ҟ}nV+f(7b.mXt{q%U)nd"[ro#9tZ' J@sQbQHvj!K\/cX>q0ґ#;Aۛ{9TIvJ vo|e}i)we^J./'ev#z6%V1[&y#))W*ǭfkH 3N\t|\V1@4Q^UL}v4Ͼ)Y~6)7qN a8m]Tm(dfВy5BU+vrxX|~Wm(1^~za,읈b 5hN59r\Gl(LWzܺp^6^ݍ=#;RU쉗 Wݸ q]JU? H5^F:[^d2'-1Du<&Q_H @v~&  SA˔޴8P lFj%Qꭺ+=!;/Ɯf\31BD~ދcgP2#r6i,7bpR!(ez.n/[pZeyQȱ uršQQ#|A X&]ΟZ9rx6۷Ɂ+*at+_)jRxa$p%jD"﷟bfrK'H/;-_IRSo:^9yϋKL)[2W+12:i?R)ה;J"ő]Q1OB~a\%g +S{[(3ۂ3dJ/bo}ye\~hp97 II +s虙i9Wh"}i#n{z~2Ʃ= +N2U#KEd6UmuS$T\n +yD!2Q 0a`Nj18J5)\ZWZJk.uNU?}17v-I1~].OoF:s* +g +,*{q;rj*H.)NPTŮgw'?ޞط|`jz1z!<.(~`Yrkp|W|t%PR+#&4rReP[8;%IB~r~QqU)IAۯG s 6m_=-qp~70qaYq<.LCXd95Aq&V\CY-m誟U!)y]>2Y/ͳyNA=|~_sj$cCwj.U 8Yk^Ccv5D^DݢAfkooI|#=)Mt룺(aZ'o:ʼng|8z_Hv|WAL=vCóz ++0 [ .[=oJli庱*yD!7Bntŏ7k*&f5Pm90.LO(FHnJ +8I\| s/JS Ep{Ggж~][inws>7^69|!v3~D&>*]x\[^ܛ-v?NMnM CCYrfp'+yV'){2gV##/_asNGv9nTqq}1tK+,<[FG*Gڛ.-4Gp<3 ̢3H{x/?UUDo%oۓp_?ꗘڻыd6?Py->PMXquK= g-1 #EпOxy*fZR +.l̊6dġUSȓ9&Z'G=}o9-P^}(/=&7'oZn9y<+3_f#LJ LG߸ L6Gy!Usvl}8f ;/kWn2ZvޛzX*ܧ 땺C@ W2 \ +|9Q>7GKՄ!fWbE q-n# Ϩ?&;ȿ?d~!"iPcPlN +0F%P@C槷z6% У'֧J%7bkO fF. Z7 +KSf :/jl@Ma(pzeP@VeaUUvhC'YDR:_tt_*›AbJH1s] p $}RT snU)/eОf *=Vll + +vɦ~^QQ/" +X~1j +p9MAjK ܅]yrMОضD_@S Pxj0FAJU +޲IkݑC퇯8n I+cT.M. 5Hć˫5uq/ڕhݏu~\V= `s䇄MXvpXAr;%{nkosdJ %aC}O7-oPɶtզ#P@_aaټ(e}]u'νZ;Y`?x:<2&M>W#BrXVXc:!С@1;@(f/aZYyl-۴Snq{7֞:{ktvޛ{tq&ץ{=brot?BG\PeRwR]q¡i|#岕ݢ"\lTp_:M>U eQŚFMSz6ӝvR}}_:~/Y e=o)f4$mH UriotWzS/ۚ5Hi[Yhkr;֘<ײ gZ{@u3F.P"W@AiPL3;zh+>nyEme#O3 ˤwVrf)ꮛˎxzkRm^b$jnp`Ąj z: +<ͤzQ#AN CZmk"oEץ.O12lyܵ!֓;}']\J,V` T12О->iPHA sWppai%xTvj>o.̟+n`k* Bp=ʼnLU_\nDA$UAEoB%I#*c+nvi;Xgw-evd~{?U + +mP"fx{s?[sµ8Nw/0:ӕ}V&ASIhhKKlZ* endstream endobj 36 0 obj <>stream +{b> !n%!7e 3_`| k*&a{niH7kY9ym#J2S?6KM>% E,c>B|B/5n@xa|yn??rm_'83xE}˾--zs j'Gz]_(ICq⾻y +$ ӥb0YG{ ^οq8U}95WJo_\fݴPjmVU4cUC-*.dwK >cg4~'c#[o%_n|;:9'pxr]3r"؇cR;]Bzg_(vC]\2l[<I +AG}θu~(`f.-άcOL]dK^̉ ؍tVH-GđdtsfCc$EqQ _0j ]C/Ɯ3A{:s_8?GLGV] 鮸;-dzܶ˯k4B ҠY[a;4(6>W#M||vtӠ`Ht>H`siI+_'z>$jI~0ܦ'_L2g$ d%g̐ +7:^Hfe 55Vk4'(L (U erTa c]+M;6rR{qgF]^{ {;,m}][ii{#ng*xelճ9 Tqt5Wbz^b܌xlK:wJ+~"E J.*"cUNˬd|;Z4=] Vj|dQ4X.UF|3dwY-~aS(Sp9cnE;}Ńd1yvGL>7{^A*wafdsy˵933ާ*y\2(md;Io3j:)(hZ\F?oЪp}!35|13բ5AC!6D]ڽTV+6ۖ(<'.jzJi\o[+ca[saەI%J:C|bR}PfߒowxC# &Z~@yՆ:QN,$Ts$ǦzD8~mez&L$js( N@ʵ)~? ?`О + ޼ F=U?6WM@dD 6ܺSR\#|G/2JYyTnBeqeoFlZ>} zUN^/ P^ߝM0n=`(ktBU'<Ͼ ^ފD :yI}+ZmFI +Lyv< J{wR7o6ɽP5$h?&zX0kU@, k([^޲0=k5d?:ȇ +E,w74zهY{y9f3sht(mZPL\@xvyf[omhV^(8pp~w XBc3~9W=j?@Յ);]ʸB=!$dT-ؕ|qw7`l=j$^쉽_fj3@9{zlg͹ kNF_OlG<@Ρ$A&3{ߡք1hB7UX,cu4W_}e1~5Y=@|puܥšxm*MrgZw +ʹY:\g|p2wTsY>ӛ=riRVC9.ެ!֔gGfw~̀BI/͊Wsi:ЭKOM|ΞHF[;jI,Y|RpTavL"ӡRGGќ;2fPIFoPN*~/W]>ҕjo&uZe}SWSTߛ\eLxĬk)>ahcr;7Hll&믍u#6&qmL8(GG:6wUK p6Nw5-zC4u[|F.gSd"Jw)=?7i8ۉ*-p>ʩkj rp`3/ pgP(a6f +h sg͑o逃u|\7yͮ٫ϖx{po}lip ,K]5{{JϓvCzeUmu>g٩"1)*)L>WOrgv;YS+wte&'vg\ 98P 6S_1nj|gT߇;M&ȑ} J u(]o.$ΰ>R%!;y¤_cƵWm=a7uJ G!Ռ =1H݃M#zAeuڷW\ -ƩAu>CZeHM-\M 1L)L4ju( )gL<lsK}u53y1ב&ɋ^["KJ픲(yF۴p?8cY#0yEjP QŁ47\3Wq;^q^vʕX}A:T3an3j(ٺv'6sAc%vW=ڎVvGt~rzp%ұv)ԽMfϖ'bnyMnT LfO=1ٺ`- ۨr xϒ@uuJpa1`< 0&cHp tap]0=6@a !f*%KUx &NQC/1Q`p/jʼ H ]·Lv]0(I Fm0b@ɺ %&@\+YU6لpkC޼qEѾQچ|݇A^}[\oP8AG}^{%\X20*rR(+ݱ׆|EͲy|3.qĽe)t B.uWG蛁=4!u`/j`?](9BAC6wM2_efeĻ9ko [lGΛ @n>[>X/`R?ܾjo 䯿&t`rW.$\Di,":q5F +" ;*ةEfgF7}7ŵ* Jp_~ R \{Al~d ߯J qaFa\Wpu4$oAM~g8V;QsC5Nk9#Op {⏀i(t~B +6aNP:l38:؃lxT@AJОͽy%7ױp(r >qs[Ofw>8I޻=Ik} cB{g@>GxTZX7p0ʢEicCˇKKg]2s;wz2{˷TCXc=^gM]cygq s 3_N_ ꜂"yFԨػyW0Z{Y.[ YYpQt兀INrF⹳:o\j;-|?G, tMܼ᠔QYS;s%y=O6Jf׹:Xf<`Lm/2}vof1\%[2ntыS;%{ gO p_u^!IJh}1*3v)v+7WVDv/%Sw#exh0r:4S!O_?^9pNtHߨ)~/^60ZyƱ?LI.eE;8HL#|[Zfo}?nv{ΝHJtPM64oܾSg&.Z /;h; _}q+|??~ +u|ȿZVsKvڭStKF{6BM)uft0\&eh1jjM]5a{_RMc k:b:t +q^ SS8W K6sT7˂S݅0\b2#Tԃɾن[=''KQtCd?R[;$^q7|8vP'1Ă:_;p"QrxNR07{?-V %[lk,K##urlxGr)/=S>xԽrs^!7[`X evg(+u]k2=nzaKBWuN˕LMYzwdB+?05@ e@w7siOp/_Q=, cPiriEjUNL8MDέ"U? vZfw*&j1T#1VoA,/AgPHeu]֘̓7yN [3,ٚXPQR>ol ɯZ|gϦOZc2RɃKPG1ꈎ :/^ôi"WI];)c-S]o{69C֎59i}xxębbbo {N1f]BZ1)m͏bk<1RS/Hq}=h3:15Kz/"wZ%Mw'􋇡U;~^``M )oԹSGc1e}ے/bZ @t~,n"-؇Gvml{H's +7jq{݌Rz3:wdQ3V>\(t} ?XZ0D_z8? QCۮHFv{Jx\t8vvy1:k9 |۽5z`{&FzX eby@]>FuGkq^ms]khF!i;Fq\nU0Lci}^SνW<\y 9{bы/һ63\~MWX92ٴ^nlYƻAXoq5o65O+(neoLRU.A;PTYdqqOΧVf'9'Lʞ #IRYm.Abvin_"-QSKqT+uP%ͩgn?|y^gi sxk;:ja$ա7 :輯.t?c ެTzfujt1+77N*UJ*2 ;qtJ<=vv+nĵF󮩃A`;-3^qOcSM]LBhœՓ+K+HԸTjK]%5ċ7fdfLR +|тaﴸͺdHv*n9b=ˍ}.%-[T2\yWl W./.=LT`s°d!v9F-@ī3̶3ڷrl +o"3 &*R"| hsb#Y6׍WQ +q%Pj;v,vb>/Hv+{Xh8(?@9Aw#n +~} ZΌ!Tb^,1!pݼk!Lq_"FƒwGu \ZdCs#C[u !b@; Pm br-YdyG AV;^c^k#Jۈh۳G^ΆaF]zur\CH& CS e~Rh7FR8.|❕G#?W C> sb^I +DqWV{s幁C$}g*}{Sc%#ȇwv`s:xÿCzø@Žk0[gj}%y. ==Lk^C݅ty9+E9(>O >yeع%[dyBE8]Æ043h|"g#hfhRD;gsUܛՉ{YØKcZWNAm3c=?^?'/uKXAykxm+Ci~`f?a U0vߓ1 |+)"3pՎY.; eskVtܴǒ8374- TؾΎ5_R&M hN3/ 7C +tę橵6~ ` +r[o!LJi}20^mt>@=} +Z&˚Tspڨ4tSK~L&A] lvfu)5q>Oખ(vS>Ən]rYU3uwnކѿOy}wPެ66t-e:DZbP2o-J>6~ VgphbG_-pNm\W2Kfaߒvd0}Toi;d"})blJ,z[qxPB:|t--;*[|zR?)C 2 JGMf-`{S /$vr^EAQ 1JyB9e|.)́=!{ֿ ߢaQE +YZ^ٽvgvZLɗ*?񷮐UBCw[YFT.-G&X&GʃUS܌Omq1g5{`BKJAտQ߯s`ӗ{{Ʃ]n3^#4KfMUsCt7cza㓑gD\&t8L͉c)`έ=;&wenU[epZx//VߜXR^a=W}Uڙ1|}HATWtD7R3]Շ?@v5mаa׾Œ~swOͫ+1v ^7zfRU2p}#1QIlf%UYlutNv&E~}w6|m| q@!*ݓo)jˀ8gӟt]o]ӅJ6yQ~CbN([l@9erȽ5E 31.;>QkI| +Mb3WntUQenqusn|>= +={h\Q܈?U=#eYD o<:҇8ׄ4⾤* 滰l_en4cinﹳF3(ZwSgm/=]nr%v/ hXlwOƍLUա}";AP@FkS$5kV1HBҰb]dOԨ_HiO GݝEK7ڻ[MUc-iug\U~M5H`oõ{u|m]݆,w;~wj?JsܻuoQmyX;KErž8Gv~6vs'Υ_6S:CZ}|h6+@x;ޯ.oqu}{%,H ̝a,!ʼnm]r~^Z.̶2l̚c,iGS|QqޏIX?r:iܽj¢[.- 3y[U WW%.5H:M9kxۢJA&YIBLJvԹЦ` bon@r}U{Tb1D*qF>(aoH>S~ڽ཰P;cc4GXy mj xK7thckgcמ箯~ * 0VRh椦=>WjN4<62Ҿ0īj@6ML_Crח%fG\__ZmjiXzZ?H9fur̒ )=#bRN6>[hn1dʟ!^ +¢ݣnG?H;rL[?ob cѰQ xDFqp[3+JyXqK^'UΈi~9oU oCy;>{鈹^x{L{2n[ r~)Z>VKq'Cm)eƕOx50 \ {'D7P87lkajU||S䛨BR$䋍\b;Zt')U2v;鷨=p\AP (**qNj{vRg;/KvEM䥆9L>K|61yG;quK^xO|5y#H|>?V7d_ = +es9K|:V@|ڀ9ekC9j{)dЌiiu, I>#@~דş4Sp8Ce={yo i ) _;A^X~@*,ד&6K6(]ð<}jH6%y˗;O(ቴWrnm&!k ߛN8N hTy@dA43q5 .ahkr_s0Ḟ͗US,.=){ָ+[Aޥ +/뾫ۍkfkzy_u%㼙QړF>@pJ%g*4BGBιm*.MVY%V~G1w^mXJ[ Pf++<((0! e83z>@wTe B*-9y;0Իa;O6ۈOkj^+mpyj/e2j s5bxX|kɁ4'T|/^z;A湬BG-Tڿ7u @ {haݑjOQ׉q9cjz/Zڤ9N')lx0*\vrw| +mJJ]?ޱӊyna\&g?˅]kw6VV;T Zh`g4TTj33 +ft,ʲ<Ն%/xqsܔp߼msh$7R3Im|L|]?EJ̄\Y3"Xh +ݨWMrTV:#Mng >+;U^.{ `PE}L.] +&j+J[8 Pl(>{6f*;? k_;8NpMdL~ t9,,nn_$܊rff-ϪX|;?m@s1q=SLKy#_b9{J{Aem dV[zu% VP`+gS ?ȟ"cP8hPjǗx +ww.ix-1(C]v8W)%\rxM3CQMU{ͦ>:w!G [1W.=4tVc3{0" ԭI׃L6~&L䶪y+Jy05tJS 4'{&Jk L%K|o' M:[e -]7oyeߕ?Ocx$Tj(QػMZ@bTK89e1w><7@pV<@M6NK|nFov]u}z[6hÓL׍?tE_8$Fy/Eyrdh)g[ɵ}b[ %8tx/u+kMɹC-x%޹뇚LfHT&_̫J_~(?[; T E +`y9CVLF&|&b?>9uFfge;^o(m;H3ioҘDK*7EU&K9] +j^ P8ܦQ;qs̛QB4=fӄ[G̣Y71 u8K\>ʰ]+wvB<EͲ1;ۄCCDqT$*bu +QY'BT2m~C5Y_P޽(|Rii߹;a +m.WQ3F 6޻>8۠i8y1;z,YbvG {q!88Y^,1O{mv'\$!]t^fΕ\Ng݅㵲I/=y^ukxn>&#6BϾb[ :\{/U8?t/0?owQ6~ +mDwD얰ʻ>_̳KŻAҡ?Ssݥ)lB B[aK +6Oeyl#WΛ6i"o011oؔ%əS IZ3|7ҟat.Θ% HtnY=^D.3֙Ő_r:/})H9 XZZOi~|S'~8"D΢}Is g6g=g#AV1W# /Yfբ$q=+rnaڭX Z+d7ʬ~0ځ5<#3}T*tg[vـ#ɷ A``;Mgex˱fx!Nn'U5:c3mcg6FI0r=HjR/RJW8t˻5ˌ͍ hX~OC |YevDD{Gx0Ϙ9ɧz[7SvYLxJqLOxge#cnBٿfV}u8(ǟҿګ}bBO۸_"޲_'KߠSyY(q‹cf36^YiC6i 銝{LOTЧ!O=ky/fNdd0Oun}JK^ zxL`Uu+~plAvB-݄2jjZ:a96XE6ʝןCs>!=f@{t1XE-vW-lSmn=mB\sRv(FhW۹ʽǯJ$""UO~(tp|NNwqAُ]k''ik/4ܰ9(R^=^֪̬S업f9:#Rrgp^O'#g!4 +#3$r [ CUuv106s,@b 5n A6XZL8b 4|N1R0qS6,fRPA|2lܶ/*]|AxslSY dSmc[| i u +ItĢQ/Y $)  +Tlm*bحJ S0@}O247ʩ+u .}=FBTK.`22O+4Ť8h9N\d׭܎^&=r5͑hwh: 3l~w_M]v3Z@,T$RCdSQl Zqs7'E,Xާ[%z1}bS3@GkpLAm[~b7O/=P=zxvzgR]-@v_I (@w5+%ҹIu lwˇjOF>*ɟfJ\³3ai㒑y߇wu5N}ҴcWM͌MtNGȹSkO/R_ kb3Gah]~že){B(:^q٭D~5˽]͙i$:Gu 7x3X} ך; ~ɗGg?\i\k V|:AkX>D^FYՑYYS(*V2 YֵYq3m%4JFC)9g> &Ϟp[ R{wNn<.r9“g} ;gDh'?)UQ9k-|/Z2ܙ; nNZFYzh`Dd}<@Oquoqp?zUe\VZac;HC`]?Mê}}5~ۭYs v%~*AUf oPpހBFqz|K+r-f;gݤ$'2.E>Miuԣ5g郫jǜ'(~֫:k6elO<7/q6a@^?ˡ58AB'=5Kf0:ȷ/ۢ_ɼMc~!Q{lO|Th*Se$;!:v$w"~Y]hj>%gk/>Y6 By?4Zz352xֆu:V4%Z̝-m<2n4­#0:}|YKu&1BIqŠ'X?ȟ7dR|_BB2*EwzAï/ 'aX ȝl}tSCW!HSQFl;U4 +$걜35e:^k(ua谕ٹ[R XNV%,H//m|.,$Ww88INNjQ +i/d̿炟J1z;WR1{=,)IlYM|a/r6G]s ̪˽5z2N5oS +^Jr厚sZKj8гea|pU)#?CM*Sw#C3 'JY[ V.md5˜]\+i>R쭷kîY$?_Br>Dz4UŋpvsrEH=v#v-OZqv;ݍv-l\|/zE(鋁ѽm٪$]//=s*6dR}as 8 B<: |pr>KN#&LtH[ZbSyJK]O^ }@M|-W.hر%PSRZ6[dž1K݊B:2Y{siʲѠVViPx6(C$B==+2+BP12–B1MB i۠DvA|0+WS6:n;nM +NYT-j0Gi_67nFg-W[ l헢)=B7ʨm3 |8cxӤYSHfM_ әю)ȜΘ0f[)o戢85ϲyTyb\1g9uxePzOΈʮf[҂#,Wbo<ϵrLka~ Pu_&1A3<_>/V9{}hQJL] o[!#Ӿ(7'S绺!*[\S紮Ww\]#jˬ +:pi(Jkt6zFNynUmioEхrTzRpJ6yDI|usUgXDʪ.Wݠå`UQ򧈺~!?<՗V-,:,0}HƜ#IG7iSL ndBb>s,IRu +'u{얣fQ{b^d2^`|br_1Ã\٫jZö8vt#䲕@47K^!~iHP0"jw[Fn~6Α]VųI.QE0,tuX UEi~Z+GWK$3GL!SI# ޕعOI?ǣLeUM"ka"+sc1#7b=FS̪-ޜTo,Fq0Aq6+EA}uG| +fּ&aC67/y9GoxMLؤWt  +*h o?d`=P->2#f5)~䅬 t[s9aoG4R>*:wg!GÎ8A564h5!6j|q8ؾC p~ pJ JrPKoд )Ak=Igg(GC#x X8 ֹZ7ٯe\?^^кޣvNN_`mry=zVZou5 Je ҫfK6 cSN +ld5M~*AݧjԺd܎ɬ=&UKCd UB 2HGFQ$~m~jtwZ5 >>Ue\eVo7}tng˪vBtB~Fѻ5+p WXnܭ2m,ZEyX%n/ br sOA|* y[itQTZ;;ld) +X{!L {7~mQߩ>߻k)뗒?g-%)`;,r30̬3hO{#_f>NtSCadO2loY'2d5PTlR4PVVtYډao V$z qtΕ/ώ#zc!3kLd&-F9~,B|j!Ot)T@ z}:1.PHqNJVu)ɋtbU4kQc z҆'>²Oyf~?_H>@&Ra%>!p$xX^B~ +<©x'?Ƨ%sG.[}4c3֣p#j›W.l)<+O6Y?eeyf+[Ӣ 9!>0A + vm@4`8ן8GG3@q€Bym8r ,j-2|VLF;1@^(2:g*v *XôjH*Lt{7Ap_XşCc$D^{I Q zGo( |S)h 7 㣗, \!l'bs&oںcgZoSy$kkXM4vw5Er]ahr,C~Uk~fn=8nl8,h2w5 49!^Й QϮ2/vd+歏ky7pKKzfW1DFwAڷ`Ze^|9M[?LIXnxk>u-eK l@)h%w4q:e^7]R{WŠL⁼~cMGC. RRDVsC18'@T#Ι4 +܂]s ჩ w SA@._@ gg^6 67E8t@Zs#u!'"O+cpS{Dbn="yVubw+15>Ik9[.{l9h`/65~|ӟƙ)dnLN{ZpUg)|<*FEa-$Î?tc9<˻%(b6]fioAl;_CmЃܮKȒ5@NASj<"d ao^͓/,G&b]Ъj0ƫC +'sNV:dv28bL֯ n6ĩ[) +dxmbyMYo{e]}W|9_LUJpL[Ot)xMx V(A{yoR=+՘ٺ^qeJݭPC ]Sof(ei?fQ05Ud0QyxMe:.Cy׫ߊ͵PJF=tx<ЌBCi}kKU`)Uq3_{x]SA.б,Z.Z;X ufz g%geWgeicV8{߈*wzuqYVhj2*r: =ϭ0+lvBbFHfVYN[akL:31#6[?oo>[/\( PO~ +m_S'_K_oK.@Qkclul)WӹrxIfat+hoƯzn21>8K6BuP[4MD Lf̽ +K1_ G>֓CbQ[>yg;#`r5rt`iTAAܿg,(f/Q愈i(§z^>`BLmj㏕jg_L;rFtdo/=:D9> wx'[0D-103hOY.HFYoek.^QɴE{[jizB̴Y4V&|4䆹.^2jO aR_oz˭S!EP rrI$e#~v&lM^^nۛq:aL;%XXd']sU{6R=7 +Rqv%ZUnUwZ244 [sHbݺQiIM-xhx F 飮}.޴!6vouY@-"$Ęx̐F6HU)>z⩇͆9Ɲ<1+Scp !|ح69:qnxz$Fh\~]Z֗N }۞]Yx(/iȟ^~}W>tr|'֥ήI/ݿ2F%p:S=_5 Pu=)ǁ򃄝f^lysf@92Wպ5w}񵣞vח~*؝Ck$*5~^uϻ!MQk^Aj[ɮYUOl54><,&4\E׻rCxp{B3Yy*:'њr4WaoٷaUo 1εnl_/]rԕV}fk1/D0#kO)%){k)2nqOX;m[_4 ˱ydy Ix`lhfA+jb˨WU9멙:!urȤY}Y0qa~SE4t?a>av[,2FO}Efá҉Ѿ˰7{{ M.MU`buRE5@ge۽eSOJ丞:@#rTM:F>sbڼR'IIF?rU`mA@B]`5!BV?WS3SEOXYh]v=p3 "ªeܕoҷA?W59#<5: EVS4U&|KڗO/ wP{㉟`]S-5cX Yi`C!}*0wlǖqǨ~Vu~|DD+ڰ>tLgȊ%G1+ib3M &S'wcŗP^>9RB=X fc]ތIAﭖuV +2,%m8c%enwY ʠ(VZu?' N܂ޒ,i9 >K +[2*1eFy2ڍOy"7O?Vtm+fc8,IN'>$JhQ!o۟]ꥶ?=q՝m{=}UP@{h{~yMX?[*l#|}Uȣfd-rQ^M\H ]4p $> Zk]z*?dPmJl~YpMx_2 lmLzMRR}- :Kb, + +\D 5;TW5pفp@ QhFȟ/$m}&{AJIHQ_hnE7m=;y-4(s tpBBF½,k%tHm׋Dӏ@exJ,>+ 8 ~~7sޓ_ƿ!C3+ePm-k.лV)`WMZ()8TiG8܄H)̟NΛv#ހ7aXV1ĎǔH#89O2(Z/[#/`.϶=ښSͤ[Qi`#{$0]KeDsXZϣՖh̉>4*Ϡɼ)`7Y798/?П$+M^cp>.NU+We6>ņM)/⡸[؀?2+6 N:>yM +y/3/suczh +&qa*a>eyנsCjU5YEIDFhs.6VV_`h n+I/@dEg'b2i1ob9 YBTn@nFݸ8.et~iFCIۮ{MO+'KFœRmʼНύ +ʒP gessRdӫ5|~J׋$DV/ކȦ(DD̲Цq*61eyT+ȉמ& 6ɗ޲[H^vBgxxp `lkl5B<(RhdT}8Dyos?=[Y%%醫iJj7ŋgg{QNƽY Sgz)uJ3Bt_O]{8 71j` W2bS!l<B5gE@ÉϿom;wѹ.N |)Jm M6mgӞ'Go\zJsVhaZe2&;FG#_-gY3@koRV!W鮈 +UեJVU3-Y3O5)thp+-/Q)r QX&|"VAis.x4UC%]9/N&]ps e |as;j +p՗du@*:;-J-Y9K2a ^^ї=m6`fu9S[36LlZɁ A8-sͫA"M"\Õ)/u4Ẅ́:{v괯Z~OŸ8AM}R:ZqȜcbZC fFEQs[4ڽ,ž]aL#^.(_'p8&H7FO}5Xt?#wAw< pAA~F?ص +? BnucmnD@AC,۳bfxLNo .ʭg:u!nyUf5_]to5o3V &,{NۆyA5Vׯ(k/^Xn?2Pȋnn-I9dN&C[4O87;WTL>~MTW]q?MZ{y\Yڡ5tz +( 3{ö0*{Mxxj4xBsohEjԎd 3j̼l=:kq}VomT5xʹ]&&РDS4Fێ;[2`%XIxR3帷_i+_l"O$]۟o^Q}Es &OX<yIxqCԚdڡWӲ*WxW@\\P?<{t «}iN5 Yev3j]{e|bb8uUt.Wc7qk4./H.{v.XTǕ~f~8̀v|΀fO^IӜ7ny|w\Ob4~yx3bn#D0VWή \D_ b+GM~#? +={նd)wi<(6cv +e +"+ƛwO=J^o&>]~-Tw Bi_<ܳv}ScULИ]=܆$GoJ)4ZMI(%a1,+$#&cy󀲀/Ya{}l! qӐ6t-K9RמaGr\4Fp޿2|eNs r +V vCF0JڦE[ Aۋ e'+BF=+2ѻ$ۓĂ%hsFue{̝.Fz^#:5z n7:Wgn+wqТϽKR nrՑ :@.8Fμ!NqdE7iFC2Y\3|3 =/[}u9כK +\7v%] 580(ʑL= *sɌz$nC8A#c:WT8FEnpUc?dnϋJ2Hvz6Z!ϻ=| 5rvqwA,llxs3knM.`:WfSc*AEReK HE^yK`pˠK%ȍv]ݛRh"<<1 Wlv]uZt$`y`:؋Z2٫lm`BUK:&H1TQ]#6`y9SB?@QRX yύH1#dn}3wwia٫`cažjS-FkmXJ:y9dSdOÃ7 Y 9Sq'X,ael!T?dŕ},wn]A!t(cgYC ʉq<FU:ꄿÊq=̕2)2'/*yߑh$E,  ʽ"Xh1c;!+:8q7o8ѳ] ]cRc䙍 |~k󦡘=/;#n"/_RJC*n{Twtc鷿-s-pŒ޷갷}eo3~)C4뗼ɹٹ:{UL{t2Sl }mT=\UWEWBP wH@ +W]Y1r|go٬ʖeGdc^h'䜓|\6z!,u`ѶD^H׸5Ip]](R!#GI*c`Sm>=1@o][(wt)N1{*%+bG +LtΘB[Yq1>9/ '/s`djyacDײ:u6pY +[ܔ&~u:|'=sf^dǥ™j^. +֒ZFr7C&rN2(_mC2R&oQN  +p:/;ym _S +'L}>S-Пރ{Eqˣ7vS'1w(G.32 +s/)ާC֯Pqa\,z>Y)RpM |"$\2A0327?>z孶 boīfGn8'`lD7WZgo(NkGGEPKB'X?J +Æ [;C|i;V%nS޾7Rdu6}&K/@8xFC<ޤ Cⴜ8xhnTd%]LEJId!>s1&Q%ۇHdH "8h Y+&/=TfMfjp@B?@h ը>Q9EQ~o{ bjwd#0lg+t +V'^Sj6D!W5 +kxp.K|=>za+g$zV{릔t<5R/WhYRٹ{mv=3SZA㣨Qejg +!.Sz waZt}qnr(S\6v>~V4 -+7Zwڤ[|)/ n37{,b5䨏F,[զ˄-w%l{3`Ą0R|NQ\M+jcީ1MzonwRk.r*K\ƪ g쭊'!+/^ǝn3X0`W`v* <YՌX gTku Y^fr؊O=q\K!_'(vBaXBOu|q`L(Ol sZ;͹y\`\VA1']OAJ.x9] +aGŨ`[uj1ƽ1 #`8E 9T3g?Aa3'r 93ݸa3)G:%M<ί2MDId皤w 鞫sY߹{1Mez彪w`d/ /?jzս 䆵da3]6'9ǥf~ZҌ[[*r4A)͌ftfW7~~]{By~/`+*T}7v|wwMZͳ۰H.T\e<΋m=鴍y/e< tл ɳVoKxD~q/u] +sv[Дۍ01DA2yS?γ88yY5gkso:.G秳{})Sd q(~%--|EϮgw44 9i#-^v1ǏTgj kŝK*g ,ۧ)ZXbߌܸx9\0{Zwﹾ$B-qPm 0(ػ['hB}^>,c莄赃f[lklfdDdk@ h] vV2笰|>ܳ藘W{-:#'!z[[zuC+xzoItq^'J +C5?IdQ^*-{bVi|~1,1,X: 8>|Ϛn^U՛U*t}l}5f}Gƒ9&^|4r\1:_kt|f5CaV2k258YZh[i|(;>rW+q -H@2)ۖz{VNPyQ*'|!#QzmJ&oNxZl쭟'Pu z[ZU)ʋ[,K4QhZ(M8u"D*&(N'bP6y\zx' +\x3,-mϼQgv+zzܵAE +AmlteJbIn\YKkv$ u|aWN&&x:|jyD9n39y%} 讽a 9[[$?/3]2AʒW]H#bnaa'@ 3m[ z&^X dVz;gvXY!Rt{kF|aW_ȀkgzM*T9eKH}Ֆ)Jڮ +$k `r·[Ҋ;=D`o@b%wck<ۃ:^"LK@?~7ph⓾hc]3C۲2_x\hoR%4/8{on5mWl̞җ 湒XF+ufrnEtsz~;R_)RDsfli{SۃZ0[TƑj䑸&IsT1K^. )}271.= g\$X+a4mU2bMnIpWUoT +_bрHD #Q8su]7έL;P;oDγϮ:QӅ + M=T{l\X=Kw;NtVjkf1 tKj%6TqdPuU=bp0usӆpN,8S#Qb}iLPR 0k +KhƤWrFŲN +?-(y _zKӁwV>c6=P>#Oʼʘ^FAOVRܐp- iL[DKnn{#T6atSPA{u~kk;="kk;:ls?XѲ=m +6q(qlg<[Wh0[rih;JbpFQN^"bbJcYCը /_YeUK%eep 7 m+.lUMWp /}g#Z>7S/=?x$uL:__uqszXq֒ѲfaQ f|9'.^is# s&u4hvNMQYΘ1;VgCޗ +tpŔ!V 0_aA+|a/; ܥyzwb:&_T Gx;}3S=Q RA/G*Hrs6 GtNRB\a-ہ{f.maxMliZ9_s2yN8_d_9/~KeV2}t[=yaoiPSǙLQZQa>!ڝFen^LuD{;KmQ}R28`/q}þh%Rnv={]DKZ:\>y !Z6DZĆPfu*E+68Jֿ>ߍzfS쾇5ޝV5oPmoo.͙U&6d؈qf`{[Ѿ=h>kuz 3+EغAX.+P9PW.U +UJߗc۶x|Kz낥LhnVP +XTh#AMEz+S +< +]WMi@i2& `R\ s4/x5(0rqr])Bc'^{Z{s[j,c+} Y|`. R`C%`o:yiqEcLSSvSfW`jJ9f/jL(S6כwզl6RCy}o?P=ܸ~xw@0]|D ]0Gʢ .IL?YDY[Gfx ́#Gw7OB ƆAWN60t=`0F`zkm`iP\ݚI_X)p3%sn:OQ۴ H\Hb; ijsv}C/_~_%ne=v4x4OӝӛJ Z\Fscur܋f_/{09G5`_A@Φ ypN兾5+_%M?+=z@8>V{?ts =n|~ԭw-bfcyuZJFA5s~G%qܒ+&Tqs)4Ep@1͑c{Ao*҉{s}79n>ϷЁK@X8mJVA r>uީ5<ռQQ~ {:"ldD3y^eZ=:h~q!pB'VHA;;bGQɫ hz6|nUb9QcoU㤴&Ef(f\>p=ݻ#~Br᩾hN@_CyM}fX[ygz<:$!T\GU/;-7;ḽajǒu4p~C}U鴉Bqz\us +ȸlxB}Vro̢/2xnDxelFƨ,^gj98yuѺ&:Px+ʽ-+cei PtCS{W'S"\{7脨4X̀u@Kl6؛4L,dƮ:Wch-iqqVDUѢs#+TIڒEE +j>kß +/jbb? Ǧ +% m|DF^vxZ*5@6[ #Me"-买ťq`HFىL !Y DCj|=7!o=J +x1G =ٶCCrF%umzC:WE{/9U_ywIQhKCi p&c­J /-[7*ܩt96\g#9^ ka2cN향|vd.{iᬰ0 FIVp/u(9M*/ѿQXQͺPÜ#|Sl8Ȉ\+?<̙Ќ"0 +6_;7u74*s"uGN pk}d;zb0IJM!n㗓)qnp݂g?cE11-Y̘N)h<3>ѨOcCVJεh&vOœ}hd;jӬM%:AeycdRmސ^gH "=Ȃܑ%lp fH%a$K8יp,317䳥.Tء/guӻJ buS6 \<\*V푥~m ^TIDۻw>A&u wpJLK* /w^dXy ,VʐgDw&B2Ϛ+i9};Tat8>$d nVTC6A8ż oǜɢJn{uC}e=ֻӒ?XqRheoj 6Ǜ|=F9B"ѢlBoBӃVh%<M1fvLp_Oz6{F[V}Z'BF)p||em6zX}TS(4 sV}}TZr.n}l%({lf13h`}wFfEG'A^F} +kn#N#"F[oXUoG -}Mlx) ‾yT4u8]{]Qe#q} "ǧڠ̦4zA<Di &5yx/nnMe +X{$r_&^Ժl""r2ŒJ%/%T tl~|2=vAA-Bly"/uò+9ٲ 2OK>WN$#>e@_A_" (JΒ((b9wzs3 KJV. s-!uO UwlBnWe/K::} ƇJLwQ 䏄l4$egWz*-ng'k.Xy_7Eh){,#I1³F0*2^V_UA _W3Sk" A~.t*cv;`fM[[yay0ZDwLGLݡnxB u(P*J7 +;y.IĨv _V~ %`vjV Ur;R"μ. Xv5(RtA_4ۥ{qHsB ۜ^+s^6*UO>#.f y!DaJ^y:5\U(yGkM/46 ruI2 xb5B]|F{hxif̱2ᡭ \PBOlj5ӵjNI_q!2MLa)Hp;UB_閘9^JLO%mm$^[ͶgmWDDݥl ^#WGdB'KL,J7_i[Ѭ#42rhg$xT"oH}*\Fz(Eзk}ҫgI^no+s4w>>[A +vkn^I/m軓/PqzdA2QRN:*!_lTSф+͑d%Oƾ7[$z`X<*~]]TR*'0=jztgкep[?-%1yήZ< *N6qޘU:s&?zE[dBfU_{/_knIQۏ湖{|=C|S:V "O~2-F:@5# 5vf`w7MSVcIwT_z +GDrU;2NB'~`c]fgQ@,p} FHat^eO%t?ƥՁp/رrSVN몽@-^% kv>pNjY'l@"U"q%RBTOS x[YIFwqe1rԲru:jD_lj ÷|>~Fu:%@TǰL$ANڃ2+ r͊/1jږv6ȢiZIW?09f,Bq=@)̂J?6Nl"<sxUl˝F=D]FB 7+:ZoEtFfq2=- gP}hbLz K#(&ߏ|iz\[~lnc[>H+N]bח)x0|V8˿_OŦ11.7o=yJff=}m. +IN0;tvQf/zw:p6K^^Y&pt{ڈ<a=睱Es\{A|W)A%jּp7kFrM/-m>:sgRr* +:UƴUtZM8qbM} c^9cȍ(c6kQy?|I}ڳϏ+klR=cӳvۀAB[R_C}:]Mim'˦+J5GY>|#)ι1Gw(Y^Y!,Qhg[TƆnAuPhV/-tKSDX`I3YY3Odtvcb{==~O,nβ`neMi>@}!T:G`oIU`#T# 0m|+hxCHNlEC0|Vj 8˹ĭ6 +U3|rp> Hr .Q\l +> nR8[[6}|lWyN.5 +i%9+bظ1?v?Zc5f[iy%e­c=ᓃD}PY"{$@,O1Co364^LjRmdr?ǽb(cO7S#'`9L>.6!jp̢G.Ŧ 5jfSpW9 aOV9^Zbgl޲ gmȤfaIcՓg h֏eMCVNu$֥ *WtH9y?B?RJޭuimnu +,S2xsdwzjhzi%`f5'Byw,j2j% P5I3633iM1lܟcrrZ#=/k@sv泲&f㔗ݭm$}tuW14EvѦiJSTi +MHs^2RHN81ϑm~2GX=;y8{ZgwX]¼uxlpԓPi9cjQtŧi!=J}˱ޞlr=ϸ9@9QPꊾRԦMjRgả=Tx@W;zs90leUQZ4PQWV$gW2WFj-JrI.(29BTǏ@$ ԟh?D L.~9{ 5.F"[ ՝.v{4yCjU0ѡë͡`*vnɨ;ghǵyhNnK!'$rCZĨ!DEfYdƠnWE!Iѱ2hŀ/Wf ~ m+ +ʖH$x^u=K!s<FS ZJJr UNؼtGDcK|Y6s,RAˣ'?-=|gE|1܂hX/98A܅1T9Oc5\婓cfc(xd'}kv1W*5W@Mv@K##\ S;rc5ی–̍]ȚzSi0N͘&f}2e2x/Kp){- +mOkzO%Lxs1^USxg iǼU 5 ِKq }R'گTtUkFR}J1(Kh cwGl5N?l9 ]`-?);DY)?Swgn9)ͼHC' *ڟ +)kO&0$5#{G|@FO. S|LA{ <çz5vsm9j/3>&f/MM*4h#l q;E82āDf=y6 $: b1%cEHGmGx8[n~3]4۱I֪iLS;D5X 'ӂ^/4I6;ZlZ+ܰ'h1f9+@"@M|8`J>'ɸ.C{dt_s~};>sF)7Ŕ`V%v[`CMs zCxw*s_,mhA`vCR' +Np2p8 2V+ j=X3^/=fccZ9#5YShqǼLŕ {/I`/þ|<@0E) G˕ g4- +]Eħ rIl- @ uס!kUmf69d'r06[ i|掵mvEN';7 U (H=ȉyV(ETFG@]@J$@ k(c8 R#, + {я uǚpxAuu0 i)*S*n|L%8@?*`lb0#0.0Ú0q0<Y~L*Ȩf(.=%v^ i~;W*jj_6/[t/ֳ\h08{@/>tz +xSv#z=]c1z`F]J]aX Uy߻_nt8SYhJ(e'bosĂ_=qn|`\򌎭,?_8aSsɾz K#ԇ1lVy>Λꤚ_:Cz72/Fh~ߚ:Z dyb3:_['(ɅW=DXvkTl:`Mu k;~W˳N֘,|?ܜ٘ + +Ypv=-g[?ڻ a:aX{έVÏM5v\ea[닰GOD̙󛟍:tw*}t.iyF&FYxuE"ËTT=orP.֭ə5JFpuG'Kl՟/š?w^u}|2ocR{) \[U $lHlm{+U*#|Yw_d3ϗA_AL]zc /(k͟%48Nu0,]:d ?}%纅' b׮ס;$zKC)WGv?מoŸK]FxXج[A~y9{23 KhZn6pHg6cy5Y(DUqIJ蓰hl܏a@5'LװN}k}zG)|Y=fFTOdtޘdx|ʟUSc//SE cL",S#@C:k|^հ^Z}grZJ{t^ʦ7}KywY`w_}␋o!c'9,^݁VXkGΩwJ{k z\~gP~ˏ֪'-0XQ|+W>GI^]13ԙaɩP<s6Mb/M +nTfMGA3} p֫+ 2Qu;7-UiYѵ$ðm,d7l-ӟ5 x1zi5(_Xd 98uõPVEg&7EnM򊮳rLrp\1m Ͷ(u\];,6D^Q&!}CnZͯ|r).]*uԡH[M +V^daq58{A)wvnz/Uh&sv1üg `f0IZGyIql_7te~nsPrc(% gꕂߧ]+^ ۭ=dgäy]&Yݾ Oy u{kIK+(jU+0+B}Vԗ{8E=vZI!+to?>!>y<,:;Slit+F@-YA aӷF?>zS/:>.Z&sZaԹtTj++|W gxFJ+{H}9ѫ3z1:ӺjBܣ|k[Q~vŭQ>Pc:siӼIc$[`.zMKϮցP+D +C:ňUq_'q$[&7:ܐ;XX2!^Q^kj况C=~*nFnA(O=E,L +jbQL'!:Q+[SU#UcpR;Ty XMtW:C]^'}FbGkկ@Ȭq5id{acf 'P֦hE%VURxWjZGє!;%+7ΣN;Yߴ]-`fP$8 "M^P[,O8ū֞i{ԈQ>Kk5UĦMyq:]uJڗz8M*a^G6\Iա$p^"$iSȱ͑y.bIpr}\]޸5ص^`o,vLtY Tm{\*V mvAs,p8LRxNE,@% @|a9>pJyseMʙaU`` +Qge8[.IyOTcV8vBYc&SP@ hoS}IJ"bMT#9?& _8$Z_f+!F]މځgc+Mq|KO}L)[v-AiJ%8r mNhًȁ_qI'0MHJtO&.f%nCfWvaVOQ}!)^vŴ&h1YҧOEiN]"& I>eb. wED3T%Llɍ˾iWɣ땖M +A~G?PVigƗ HW|7bs}`L@j9BZ[|nYB`ʨ++lɴKFyȳ[ȳ]~7.70}4$ƮcYMS![3>EzK6:ke=myi&ލލ{{ 5ɽ:p ک''u^Vgg**<]Uإ.UuW٥%wrmom8*~ߋ[#6n;ٓJF=yabĤ6tV9AKŊ^pH9MIɻ3.D]ݴ~!&gx{GxP@+);4+5(1e|濻7d+Jm2ǧs2ߥmq7 VBUBܵگH $[hRqb38axSH!~sKq遲Y1G\Wp+P+rZU3P;BؿWA ܁ق~|d ;Zpm 0ZPu:(ZvtqsIv]GZ+r u.ܝqڏGpN:[>7۬\_#/Ϛ,#-ȃϣ4}zd?;i&j\* +Z_V_ڿH7e'$gpwh6t[z˵T٬yi `贸˂={ϙ'gcS2f:1*-41#7$q|kcx/0oy9BLhTj Ϯ +k$Y:wO{]8(d8J4"Ut3YĀVdl cn= +Po-c̣pv ?˿E+W؄b*N~MksEg`O#̦G&+ASӬ n0`]OGxco1xE[.n}8~0Πsg/Fr!|#m(PNZgmVxg +<2wizRÓ&{K?ꍚʈ5;f7n?5zQIˡ;nlׁh}eC]aͶ#TKt;s)Uѿuƅ/w=R'{rWf8X}.%"'}+OrcTgcFnOFS +R2j ʛf,bVܸYVaz- +.f)UZtc䓽.e^^¥'²إ/&2xgsl5%&qүHh)YXh>ߙCߪpcZ\V~@ԮC&>Tׇw9üws!pgew ubvBeoTсm _-H2ͮ~,mnN(|aXzM{>TnW!Lr'{kݑXR=֏YieSTy)goYrB:Љkc|a 굒l_V>WX>cW]~i&^F,Efz?Ύv,:_gw^\5q.r:WAy}yn}I޻ybp[@iۻA ~ ey!<qW1;='O+%-\AxNU;=H-?NTU]}a67B}q順X~k:مϷS v T.oT[ muY0tNP]=C-&ĀH~,ռ,j[#U̹Q_yil底?ӗOlX֘{Z!l] /nj+$A΍L;Z;^엢膭F-_YHѱRْ;4hgV䎛*NOgtz?Y|~qr4w,Cf=@~T^uwY'^}v4Gr;NbY0BUK/9Ŕe0JfVH:1I:5@ +"7g7{CC g+_v:kӗ[Jz+Л[cA:I6S:ظ\BR4)"+U BeĪIptnfyRкkm~;jthV '%res_6U{Xu'ygiC{ǚtԖgLuQpn2r:1pRI8Mo#=)q^H~"^k#l\ 1 [)rnἹ=)@aY\}}^;l)x 5|?#:z¹L[NKR0s,ׄG !H>p:r>O75{6\i0".x좻߰FX2/J ^R*$_36ޝ@Q[ZiXdqtc%깣xwKw XAz}ϟf''+KCVs Ԯ?􊌹nPL\9L]WL= ӻ#SH +2=w }X3QQ&Um-.Y^{Ɩ.+7Q6^VͅOQ<7}\eXmk1̆v󵷘{ݜnuwwN#NaR~ SFT_KH^V3bz9.$5ГRVC_rG-?HOPXK攢r`]`6m[AiW+4tu +6y+0$}'洈ֈ6%3咱eVyCh,gI7dS6]?V4Xn%a$\s[~FNx =;2RU%NUa>$j:q.8.t~'^`?Tj=L"Vt V[=a֚*jQGw0Dw`תiG7/Y6Klhbk[aSm׷Df\иJL]>_XB\l<— {z Jc]h][3hSqmBҸVC(䁊]W 5 5|s |;k74o ^ RU_.RHL8OYl O9+o]vB{1ɢyJv/u5]@ {vXpQT4 kaU+ 7h^Ĵ:t=DKiʮUf#eO{*q$I7n |{湌b rHtaˠ>Q]禵ԩk&}}QU1eb> nTNp'mJ `.ӏJIn wՍ/M^1y?vc~;$C*z!D]+Z*Z1awf(qv-(wr\Yg*}f]7V +aTżl|ޜ +L#`0@Ũ:]հfۉ.|< #TZ/Z5uv̭0Y7}DPrz,Ux60 p@[ @9aʃjp}g/ Pd6♳bzhPwlC,~c MXy~#rhFy![4%+#I,Gd9and9}OJjȝ[o#5D%JIt8?|j#Y +SiČO.}.ucEAixi}j!|@v1/ȹB#l +pn}J=N +{nZ2DT:CGDu["TqDAAAdEQEQgCu*ȌxW f  ) ` `% XIK?F]pizӪig)V;ޓ"iqdb .7ۤw3[ҭLV3`\WO~q 8_n8ݢ_1}o%A~u}VЯJj9~I)u1K|,[8Onn9q[<}P/>}= $ɶ&isk_ˊUB|MoW]}afN +fV~BQ"YGgvE哈n [C7HO/7YkdW^~EϾZ|2"vk,?^%Яdnor$ݥ8\z`_̛W0;souxv1ڇ}fϻ޶1{oAw`}Vǹ[W7cZY19ؚZ~SZ/>FboP>V~FH mb?wP/b^'.߇z[؟?P|hhMcȯQğxdzeP0y,9ՙU5S2R{zv)yΓې~O<>>-?u,>k[xl!1UeڋtϮK9?*^~-gLz2uNg +Z@plx^B;Ef\a{8i%%7:)zR8/"?tZYq:&1`۱0u4%cersL-9X}Ɵi:>4Vq7/Eʸ1k6 qj +-w;<` {BeP_OyoL7C"2=sri;aKN]\ED1 +O1Mao O]l彡6烝0]}s#9ٿO`C{cLql#zɋ3uMO4o*s܊-h$ߝӊ!9j34ɤ9{4VO~za-zOtB-gmחe7ҩSWn޾xA>.̱u4.jƬ~Z?Ja9R֟KUfݳ.UJLWJ=k0zQd;9y~n$~Ui<2*&c̷iv8i?V!sn]מn(u' ڒlE]UI}68ֽtKĄߤOeM_̸ԜX~pmu+5T6exoKԬ{gXy}I51oRwt||^bZ*Lgqw<]Cz$7sC45#v.-?Z@J)YfSjn܀2oUz] t(gk㵾 rT#bU gK8V@<@pXL +wZZmOLsQzBm:fn˦ ˯awg7)T=Rәd4%7;IOiv^_CVjR$orYY˩\UjG{W2_bvnLosR^Oc%س7 e?~v^.lL]x'aY礛:8lsV?!- Z;9|]S˕P"QQ+-wF_t\E4_陝DTD;Nh>>Q4:rs޲n@@{u+qYLS:! JE6*ߵ52JN7ͦ\!W[i69DžV8zW_lyvYԷ/H/j{E}Mͧإ4iM|wZBuףRW=^iw|]ɪԻ Dlu+vŇKı8Y|zIEVB3;Œu}Ix +_vvD VFZ{}G.ϴnhD: ZMu ei\[]Ukmxdj ۢ_II|8ȓ(|kC*{wiJAшjuṱ|._|YbC7 l +SvGW2WܳT>J6VR[CN >| +W'/ð*d Q,MumDB-LɹQ +|)o3 s1ӢeI:-&iBZ';K/ _6ͫ'擟tyg 53MEyld3bx/[4Vtc(o _#׭7'8zYT2A2M&=c#S1.dS@&!>&M\R dȷCǽZ_[-Fp֝"0:Xz-+,;G\]ޢ'̵^?9B/$w<.KZ޼ XhH5*[)E}ɥ u2pW)<#T- Ln:qB>)5@J䤘E0O@xz +_(o ?9lsiq$8}5[ +]dTtNp5\'J2 GtKj ȼٴoOoi=\hG@>vP(d58 +m?{ddko Ts|%[,>@puD*yр3_|o3Wݒ/W[ ] @YPs Fh +5hpe0r۟0>VLofղs)^32-S8QQ +ghk*W\BV_=fxw;ZKwJ=Y.p=ɻ@\\uRX틿ގnGO|IqK[ܔ]P$P c=?c@Þ8zEF<2kA>iA?`텣9Mgp1>&;};6'u}62)ڕ }|$ɾeitjZ]c\̻t8cS|+óSm9*(UmGv14vnG^aW} {ZW.~aM*:u5!)lpDΫz whYy2??VG`5/7 4R8zY=CvrpV*0WI +ӹAϴd&U7볜tV7-2e@x2;t iMyߙ*aisrYh=pܽT.Fѓϱ}~{%}v!<7B/=V).,BH3X,sxQgVjϐuQ ,)ZӳUMce0\kv1ykaq/RmOT+m06}8ԑO:mc*ɨ,?6\H6,MN9wN&u'\6SZ{[95}h&fq}zyx '[-Vy w4eiIcޱoZ8w*6fhxyuqvxe2_wo?(@ֲC_9wR dzdOrbԍB-YJ .MTZy4AԶL5Uv&i8μDS[PqJ|qxiiswsK>'%Fjֵθ珆o;aɵǖ]a-oO3`3ks7vf=YC,=YF38w&9imu^X*4ʲb>䵳CdpTΖm(E?Pzo[;tD]~0^,}{mZ.sg*oz@{ΩRSRA Q]NdmI swF]rnU"\np:we +a"֧EM7;*k LzS}K-:Oj6N˛ݛIGXNQI)ݴ {.N1F7\:Lɦg%uNrGR'WыwId w(jB´T]9Փ*/Φ4 +JŶ1葜΍Z,`&E*pӴ}Z JSƲ/[vwÅтw[LCnŬ œ91wPģo:u47 9pD3?kI3:~0#Ӫk H6݊3[7ɛk,ߐLOg35C& dŶ92}ב4[VV(߳\ + b'YHa`0 SVsimrahw6ӸZkI,Ϭy]6oUgu_m=ΐ4~eZ%z2c(&3oJ@{QsEʭ/+Z&AYXUh_Q;b|DAS Z4a9~`TK1۶Z˂e*Q*<1ajQQ/{إ&r@`3Z}K,l:u*㕚,`j88\껫w #X]Sc64aJyt -ؒS>ۺ9z<[~!L7? [\H9`r[Y[,*Y UJT'+dmw"rw; m]:/F5奌s0H፺V?WUU +kpa$*+*d$7J+W;FzPOD!.r@:yLN]5 f =erowz:&>WZAF~e6 ?·x~jphT3ĽxR\h\tK:? PfssssU6[_'J` h@tu~3P{*wP;h +&z2N 4H r.M)eZhpNd^ 5+ˣdSIwوy3q6 Qb!9T# *KgW|b P@n& +Ȯ"wF6ځ Ơ]]1oj ̨H0du`O+S\#Oltn:)qЭSJl +EJz@=7Z:TA=s)]P/K_=E[)f9M犕$t*갗ޢZ!b%#^#{S,9O{PӟVBzUۣX?~`P?am0y/:`fI1+bh#5`&@JaZW"ʮ$ZffG60p[tL3ǀ_l,JdpY',:A +u%T,nw3lv}s+-B*kCKB}?} +g,­z"3zoYȃ_I=7!w!IDG6#g?>(8bnY{ ~?r7H!j]stIlΫ9$G& +\A͹B]񽍹{ O3`ce~{{b'ϯXOx\_iLev[, ͱ֢'tP z½]Z%1 ozx*>n&i̇5ϊ9]ta"zvW?}>Y +7WGf<|lf(88i9=%”_+_i$~̳Lw!rlZ"Ŭ\~v~N#, k^,Y"oF3dm7业`7%O#i?.;d6.(~俕Pe{uMnyօOߧr`報N4W6ʇˤWZaACxJr{{~yG=/??:j6-ҷt|V1Ԋ.ci@۬:͙w'蕛zUu(AEpm~W +01HSP/K?83QO9gn3a|ط[eln.ª+[QUOsIrfT Bpd  4q@^auzu)* }Mb{}G<џ +rTLkVpO,q+LqaύD.nT ,mPG)_LDAVơDn L:9Mvs +N#WRlwRwfͨbB䬗pn\.6"^+,&YJ ;Qvb$N"WpނѮG7I [-y.ŋX Ex:0aV}aT fxc H5tF}:oVTV>M͵{Fx#Aj~v=VMEf:[Oajg3djf9uɘWmeϗl/_ 3hX/op/ԽruZ`X6:xywWJ2=K;tT]-iis +YB5E6NW}@n|t2UnNW;\uzNvl@,eC`шC< q4'̝x?qp*]o9V{o /E1m7/5sTk5 +WϗŐH܆33W=l>l zC1Nfz8d/o'KM OQt/vN#y/py59 -mT$Kz,RG0XH (Dȗ7X3jX:D!۵3W Gkm>Y#hIԣS㊷oI١žGqKh\9i~ҍSp7+L-׶ƫu"^p@:bYRWWv7+ya!rIӻǎ;ŗ>stream +,ft[/np2ׂ! 1*-l21`2\W@M*S .SLӳ̥+5>XZ#jjBu 7QeiY 1SP| _ᛔ,f!CkUueޤ?ܜa'?P]+=@0b+h`)@l M{D"ӯV& +鯡CθhuYȒ~۾ +bsaRbSƫC󮶴㭼%cd: <+LSPkB9PR0@`ْvLTӯ@n%Sjd.ŖXn'֪8m41Z- oQޝaZ(xu7>qr/x1ҳ9@g%@/ +Az%{I!P'"7(k b(z`LR/7 WaWM*P_,󠾤/ mp[>/n3?%}2ֲPNEP"] hsGk`ilOo?s)rlLS4+ pSx)Ηg_ 4o1B͵{= TY0i}yJod_m q +?W fXEK@kCdMq\p$ H14FT7̺gAC'\lml iso3~b+wJ|JրAO@]5@gdΊNFʀ6!MЕGGƪ]l[jUDS;57?oM~n Eo焇9D}gKn(`i7N\vc#0>F@?z!D]ڜ>^aw~ MZ㭭̪]<<;:|>@>.?@4.[8 c몽vauYcMh5qx|Z!wbu+u] +߻e1jfm̎voy;e}VۚϕW#AI(|ePR}E~TC5Wbw%"5v_=]cԨg =Z>kD 2+uX޴bh4X~z-BƝ.QËt~XexY P =e:Gϓ d|_=,yi~G,^vҁoXX1VZ +Zev@q Obf?. +9W=2VXk0 +bm/COx+=^Ae8>4e7z2۟Be_:~@ӳf+L<-O_}E}sQ(xQ;H|hZ8tq6I>$ :dbwJ/EofJm^$x3 gv\T;8(e}V s|yOfW]M4ѧWP/׳CxC\y{u;6}$YcFgzjnk $~<@=Xq%N>y}zcd^N#؟9R +Hs[;,Oi+*[gȢHDUwO}_=!:Ά]. +MdKm 3mS7ﮑ<_#Ú?~B٦KŅiA}t,a/7X2 j. +y˰ -u {F&JMF+<*<Рb>]SO?N$ؙݻYeQ'ݜ:"Kz7::,o\FC{tk[Q"(sAń9aY^~N1nʪjU=8,깵g;i{OHiE%wq\q*;bS%l(k\NMmxb&Qs1\6F.+h7һE#7KUrKi6:.ҪJ5lq#[咆t>A5b=Q隔*Ma?^KTƐ +/o8xAY0Fی&%_ROsѓ3yGk݊͢7%`%,/ +?|(R++XΚWb7Ee  K `1cԹYژq%p%=˥LnMru;ϗP9-6דof.E*lLMtg4o(.U"O +Pr#SHmwX\^( k]0(PS q?HG 7B;1Y12$;x2b0YzǶڒU[ "M\"ƫxmfY3H°0UvTnǵԮg_!0(?$_x*WZ \u إTh>썘*1MlN#%h]RUN[V*Pp;ldG۵VxRܧ2GbQI!=óŇiEa$3Yd(ӢNGuΊ ϔPfɛ)'i_VÚLvbLA?B;^Zwx٩/s0]rVj7":`"j:'#lw]3'SV3 OP4;8T {%mj`⹸O 6[qK8bl:p2Aqhp +sɩ֓=lUAfpj7l=TmՋJZYs Q+&W!r&3:O;|\)d<v;Paz[6\,v o(x%r=OD\wW)vo@k .6Ԧy <}^Ϭ,MӪd%V^MLwAM XH|S]M4ֱ٢kh0tQ4_\݁zJA"` T a S!`," ԧa{9J{S2mQ_ 8b襟R[:[ٻSaHUxo7^y!\s:gi2uw Z3k +~Uz\ء[\X.)vZOuc>= YYb(EK/Q'Gj6iNVW ;_[|q:y&4֌x01Lq#4oKOq[Kily[?fyl*\A,18lv{*i4OC**'VQpb +DQJ?X\@@+w1seh{SDžFޤ@  {pzG5Kk[f[R>\y|{նQ (Ru z`@F +[BVgWl.ջ?Ԃ栏_o},*_ݔ=Da]#!TW,|Zܪ w!vDfOKg0KQiYDpfDtL7)Slݛ:Zohu(n&+v>-[d"&&YԻ|O=P<̐(72gfrp}/q1?G)9(럲'=ެjn|git,V$/--g%ڱ4M?4)::6&+z鍷?Dv=< +ǡpR^X(jPUGGȬ7ꗞ==j{x7off >5 0gCYcFF`Ռ`}zng~Vwj]'L͜bte::Vmo#0jצs,WxJ: 1!aH[m?ɹݒ{o$4eScw{&c4t(vK!m@s;6vӸ-PSo^ׯ*< >jó}ʸ=3|L3N|7FҨ꼽 e;$mڕ1-T7ʻ)\иu!9ґYJ}2VԦJl0 zLNSXI-6 +RΡF ځ|HbLYmyՕbg־EX:3ը#8:HR`|])LzjU]ռ\5`Fte<NC "-]Σ9췊$f;_i>ѶgZdH];x[JxkZ|g4Pt]9&z. ͥQ ].6z +5 *`'m.|1]qq$x}=MBq-fRt~)۱^L;+©=?ҾMʷRhAVʶ^Ocv(VO "qb:w} ʷ^Vk;׮w3+tϖC;ɩk9^n7vEectlCK3Z/6YtNkP6ȋbmVQ\(BNpNQ@;κY4m"{ Yp#ݠ 6,]G8y2/ :qTzR~h!]Y׮rŦQGuÉ\3o27~1=ghג:nZ'+hC˃6!#{X4ٛ>Cu9U؄\1ћ{oj佯I3µ *~wO%()Fp_n_ZhZ[όOhn6};`G.X-t'KF q񧚺)/BWYmˀ't(u4vhcS\:?@57U0^D1wQ >{mF6{jAj]=)Ri[ʸj[:t*:xr 0Gzr [ͤTHc{')\K_Xٛɻ3z`EzjƀJƎSC`(WTz(ﱰ#׮۩trޗ +"^gB옮)Zxp##<r;rt;*w޾/^w%La֎GFY똪rMWZi+UAʆ +W  Vf E:EI/x=p-ӏ$GnahD<K +0 +/ 7ŻJK 4hʻl\EciN<5t"DVJݣ n2R3C ^Q}We'[2t'8F,y,qNAs堲Iǭ')Td=&t6L@iK=a\f84LzHRTHϙD:NIM轥-0?:mX*76稍Mx-Q:ǘ51Up= *sq( C꾘{_Y61PGvO%e$kXLW,<9v_|?9A<4 &"N&ܘ!s3>iVd eK?)9/P+/fQqЕ+]Gإͭצ^C5|X=Vk PC>[n9gY yrd Y{SutGk өF(`5S@>+E@R +#}P,6xf}ؤZ09|0T`6 +o>7)q[N?񭞫Yseg}vj FlyT7m0HgE(vJh~=Dp'p(0"A  tVn`(^-%[9gPjtТʣ+nm˓QD?ۯ-7Px3_M/!]MV4\ h6 :ɼ{i&V^X%)k0IQq$R,@"λ=~2-uE!aOle5Pr19FC<"L9|^z)c9d)]@.g3@ (4~}[' b2R!@Y5PE1L%wR(9=z]֘1\(5:|J?S7ŧfP|t CyH7*5:uc@u]@#@ s@=20@Wt x^tX+/h)Vѯ;=2nvmzep A+)K»ϛ>+aM{t7o.uL26i-n3@zۄ2pLsrfl )"`z XQ:8T01/Τ?꜎>s|@n#ۙ &F[ۨ? IŷpY6 +ftA*\~p7}=1 +xdP|.'ZBH*-N#?O}䶥ox*jf!t" l? ;g <|D"m~=҈b7 #a#7|KI@Y?_Ӌ% +e? tg$2})q@@@o@~P3YE#DݧsKHst[|P8o7{?<{ݔD䞛}峦&M6ep"D;g3d> .T|O: 裕p7eJ=%sMh΅沅- y>s27l*V3M={}Q9j }P\u");oW6/$sFR;?}^nM7KN8sYq +NlC]*Tu纬yY,dS⏦PCX 6 (-p,WH5tpp'ߓ4Fe4GYxm>ҽ8!_q0ܱI6*u2.߼5&ަc /Sdtn06` яe5覊v VK!S7ܒ"gҖRe U }ݓTS'#R9Vg K^A8lTczӑo:0jynG\nڔ4Ϫ{EU!U4ydZդh b1b"bB0M}Ah A1㏠3h/;ffL}jhp ZF'w^dhH..FAd +Y1co2?LW왭Z`#馤s-;?+**E -^$E(gFEf].Ȼr}q8[bJ1K,|sg8%Z.6q%2kẫPWDþB,N6Zt%7 +TGwFc1eZgϝ0ˠ 3>l)lSuBd!TZ6mPs)Fcc0"(--1_q HޒxGR 5߲OIMlpC9xC1Ki +Jri08ouqSH՗IN|]?SXݩ2ziT=x^`u-5}CK23H:Gj&sw5jg˺E,ڋ{-DfSNE<$QyGG3mM逸ŷq2杏]rXKF9v;Z+UV;VQt͊sv݉tψU0.[&^@S+AػS ϳ)O\!ѤG(f7G$niKbWڇGꗀt>TԑZzMCE Gom=/xv$O'mpVlb .ƅ|6/-_ _+h\}&1\i#D@#,p1c2^ז~!?nLiJbpwesuu ZCE^stՌ('K*X3_ nnƹzDNy&;^6T%hK94${y{dEgeI9i +)`aes'4٭̜?·&m:/$CKE2^A]]|@qՃcC ύeuA7I2fAH9 +*⭃V A/LRn@$|tXT^2p +'q.v:nዱ1BFG(̒(̡8aYx\A!1f&Kӵ1S03*}k]uAa#R 6ip)X]c~gV%M.\c$ ]UR8i8YI#+9Rf) +\Xm0{kOd)Gy`1ɥz `}*} h̥;7S Z~)̖^qmTxZ5jB!sGi ?ptde=c/oO)7,E7VM?f)WC5L^KL`=j.0CVK۽܌ŕ`KbEW/J$nvf +{'@P w +3S"<^Qm"A_753JE[S"h+ߔs>}#Wn`aN(w Ys:q" &yI&Oĵ2Ϸ)FK@rG@' W 4"6Aƀ'ȹg ~g.1ɐp,3?^Q94s-f|m_Q|ryb-SjJU:NU6 ˯@#U44=i@w@5NqkY yșr,?"x.l6=;εN7]afJeF}?:.}Iv@Wa0 fy*8ue&4n1`fR=j%F͂C[nߩXEa]n)T㴋*6Z)VV^i) .W$-ǵ p\RlU k]{r"~ H2|ioM[ӿ=EZ[P+ ݀U~MސH.L1g@åֻ 4|5x4jT$FkUn߳!+v^>nozGmIJ] %KH4NE@v*6i=|HtsH| +FǛL=ߏb7e.m"ֿJc`0e@^1&: ͻwݘ&6Eg$@@HuPjZ}n ^/[^ldO˜QO7o~-6W,[<ðhG/x>:zsf?4ѹsI㭫eYQDṶ3- bqTȏAYQV;շw\rDN su͐ 9MzPa2աpW@SZz#mAʹ>N\vVEPwOwu)̇ybé5&xaՃzLEL;FXب vHV:֭!w--4so +n/- &.GPCޮ9Ր`& =B4 +_|kF_կ6.2Ԟc!S֡p-( +ۈ~={Z}F}dEmuhԦѸ,dza_]K?>_IoFe9:jUix)6 +]Iӝ9SM/5WYaLi<\jmK5Yug:Z_*%9z) x^sj)DMP4??zg̍Ⴓ˖W9}>lnL8p?tenUS6'#1$pJ_CΏ8㽯E)yţ]XŹ.VW&'JcNj+ +YuY]).[\z̢Y2eS_Ct%?kS%MSL3&tI~&w3YYuT GGjmhdPkGC벼w@k%^Vۏ h⥷nPaAWePT5<]rW}QCkmjO͆ɓdSn$mCDLs +k.66j3 &[.!fz&2G%6u/ݸ54Т'pM-ܲ\;U5Am >SR48R>qF4|?\Ǫ)gk7Wxc#P)`(T.ϸ&szE8_9KM>>ng:qqK(`hA[+=NR%GEgU>3 + #M@ +nO':繃?yf<424W) MzXE?_f sanOrah}#ihsas|c|R9յFfh eA=QnSaISgqB Yfbw _ɶ^W$,Mn'\xe F^Uv0^ʧCV_*erVa( 'h}Ze-#P>gHQ)`XYYr5 sLn# >/mV-a,Lc42|L씣3030k+ :|Y Ig1e[rm|={.4VHl',r5 ++B7-Bn8$w`G>\ʪDXݨA~4[w7!"ESVK1x=UU#QsF(bYb!cgw?V[aJ:/|y_m?݅976 +Fn ӪPK[{[??.dj2VxbPqIDS$dL*ם)T?08a/Ks?weQ6X@Tu@pK Q d@R,~і FĘnzH-UoaQ|.A#:U+$>f,ҹ`d$" WO2Xbgr~,f,! s Hc l, w HLYd+@q!dۏlX9d`Lg>ftד.ͽ$"Wm6ɂ_Sq0 ě>@A{ݎ%(vb'LB{^( +Kɱ4™n, ?~|(& / ݭʙsw|\]k OC#>ҍf"1PАrdj#6u N!SBk#~-4 JzpT|1M3Y>呵pTO*`/fUHah6SMEO]ـ/O䪀G@G1`&08RfcyY! +`Ht0r4MDmzOw o^Ucgtk %n^Mx̺5TqQA, ڀh09LSt[̎.Kug,C YIfAԈV +C5v?$Dd{swCN@m~[5`|Ms`h/O6?FMK 'o`x8|8InW΀"%X]-F踭n~ξӼZhJb 8HK25_LuRWRҷ_2f/mm@Hg ߾!8B>!=ā9V,: +H MR|(Yʊ]1W2zu+I9[o|_ k#6n >N3 +@_f-c=9 JH$!c?M-nCcQF`4~MMme~nInI$'mf5L(n j|WOK(^%}k'O,a\J84_B7nsUx۾kXtX|b@z Az-AzAzb7MɎk[ȌK7k\*VtXo;pj٪E.SYjeJo<*fafO׹4L4GA7h`tnlԠCoD}xsD4+aL](s:rJYߖY=&/ƙKw'Y+NNßj0 jckF-~Pa@/3diUQ$14ivn+%l,Y/b&mVxϩ`x\'#tt.r᭶9u, {YCsuQK,?O+ہmZQk9j<' N/$& #e74j'}tɰ ׮f/k'_E湅ָنޯ[⓯9dW6_nrW慈O=t)]bNz+Pmy<7v%oO9jt5{ОE}`˺r8jRVgګpHmkW@˳erk5y;?si*ge5HhhnUg{'2y1e̫[6*twˠ]ď:*Բ{ImZYoޠx]=Ie7kcC#aՁkd:uxTVYw5kXU2w9/sjeZIyf6*8Ů]{Y NY[qq;,@G17l<8׋/?K~n݇X"HhZM(/ AՏ~tG6 A-v󩨃d?oB10o)Ĉab\=0T%Ϊ[|6qw$ N[UnyR%jjlhv|8|3ϔ ^nUow4K7 [8ܺqu.lSYN=b9VѼzBF' kPF{s!sU&t:}CNoʣ,)@˦GZ83}AF?Ӊ} +/V|t\.֠x'eaB'3f͊f=D[x3^JF,aCcPz4`(Ǣ6䚑[? 5[,wwynx}-[ZL[qlms\ st]|yg+WzuOz@<5߲n+rS,A\(| ߱'q9{ ͱ\ɊS"n}1ӣtFlkPqgvb6\XIR}$ݥztUwʥz~E}M6[Q.̮"qrɥl$(ro'T1/]F!Tr&9z3y쟆5*9E˜gb`5սOVʰоMWrA=(Me7i+q(?\ԽnMq81,aZr}~uy nYknA,~6j,*BaPSv`6<&eֆb.sN,UDO6qJSMV4}(&_ hhKP=\DV1*bSw_dm{˜wdU`y1\Coiu(G>-k2,kܡ1NUnTJFϕ.As+/H_{lM+.(Oll=\ȡ¨\MsE5i1O t &\ur{XuP4gg/OEVϟ|ڽsDqN#E".&녭 agUp9#jRg.&;guvO[moz2'Y*^6lv*>y $F|X:s{8Rfɢh,3^ +I1iN+R53%js)iL[lF}+н]kSvBeJ +a3nxpj|c۴`[M-;fPW&_:V,/n Tҕ-ayFE  '7+̴u }zSun&]r~|2{T"ST?W/Z*v2Z .ctrψċ!EiT+&q$t[f,y8v4͔% |u08>L^y&E2 FNs C"*m|5p lgqS4"k{Tlobp4eX۬Ѐ:-O#* I~Lav#ن:LűOrVESլ$WjӏxO9̞1Ok1Rӭ;Uw )^] -U-Uuއ]Y*zLŋrMfzUc9>Lt6k +OjR?)r@>}L)"Ps=K\,ʧNcJ,${Jw' ׷iVT[B=M(X9ALׄHj>1|4NwqO:' +r>1x9^rMBwG2*?wʀ::u]Wiz(ci\byÀnfX6[~e]^+ɀv +ݕ^m&*QpM+Уk!|uj@YDlCZb +!2pf֚`*`fosgoٿb?-`6 QJeSLY[0^],R-ca"Iz.Ԃ*+ҿ䊖r| Ʉ6&`W`DLer, p*BdǺ-F?[M@صXdüz,6~Zlɉ?t} 79Y)BA! ЕTp#?_y9d6$8›NSr*ls iu>zg@L +O#q9Ӏ8UF汴/Zk$M# _й,>.6ܝ$)gznӄovG1P (8+F+dj=_'HLLaDܴd2lE܉ O_U>eORn3~mҵ+H H&H7T{1Fxxg[捨kH'erGbK91FνZ{gQkqV7?0鏭MVPE?YsWxNpQqZԖ¶Ew5GY3bNi3G^tn#<ڙx-w2~yz3fz +u3:ϵ*z/eܩ}60Չ:otI̫Ifo@r'% 7ϋ#e'CLfVu qg]HO;Z'6_AfTnOC{ Saji_9Ίln w x"~u{oGV C{yA:kwEhs[ks\PuhYj__AqZ<C>n]N^Jۆ?+pZ͚ۢ&'/Iğ]>bTsR{u냪*^#—~هVծf9NY> +HJvϝ7ȗjx-}-@`?Uޕhޕs+._q-`# +ř Tu*-۷2YKB3:6XaYT6?uW E1C㽘vN<)MB%#ZN3UwcXEw; TNۯp𙝬x 98>gw2s6ЯC"geL-&th+3z5t>uoYލt`Gj4{z*zVZ#vBV1Uz3& Q[F6f6IsBʈ1/?k4zجt5_o 3R~4Zj=7T ]e*(*y]u.Ց0,+ǛYt:)H= Npe_RLOʍ'3íߊQ7Q|2ocMfٗL77MC +6>HGNZAajh;>s^dip4s rYǟ o B@=s(űhB^gw烾K_y"xgkhk̹ FWOJQ |_.r,NjZTJ;B)z5s۷Z4AʤUu]d0Rbdk+\*3ݳ˖5N+V.:hu& 魜ZރF})} tȫer:-rC&vM="EûEvi`_PϛV;}:ʔ[|7XnQ1eDpǣcþ~ۆSGH}!4ȵY2@>@s*͗-~Iha j%4g@yZzh=-g/Zž҇im W2{ #_v"a33 UF@(tnRqr5Wk 0( GqcYjp;mQ7fQ;/U]PJ$wPoHG=)܈B9a;hy@av k&8N0by)#L>I ` {7:֤?xl`mJ1~Kf\m<2Hbz^8~C(Hy4pzųI9vq`!G8bX ܤpnO70M"~8.eGK,{Kzjߚ=J45Ta} <'hٻatA|u?xG&8<^N@<\&wc9K, +=~Dc}?\g)4hC2{w!/cGdkWn, C{oKygދFr0qcw0>g ozze]PˈĪYͱ4P"(R: 2+BRKH ( < `.ܻ9@[pag˖+xo4r& ۫:fծ&;տ[$=4m5 !%[׿1s +P+֗'oE@fhh9x4u@ R,{pԃ>}#PLG==.jJ;q\_#{Pij:-OeE%P9Dt`c*J&^q, ` Zpc5)^)U!O\"lVM6[ 4vON% _7!If} 8ѿrDGUiv{XvW~vΊ틀t{bI̷lIEs8_0toe%% 8N }2_n|ݶ;逿K߳ QIwKy:(ic wPlBWCV$Յ| 7#A3wG __]&a^VFS (->z@ă!s w&+Z}d_; mO| NI +[Cb5+i/M2:C훠$$ȒDۚ_5t2t94 T[Z:ލKz ^w|Tg&Jeu孴`P c~{{Zݙtտfyt6H? +î:~m1d?v$rsu^礛 ǝk|gt٥Sy%fPMz+ψAz}#Ŭî0J+s~>4 =7Ń˃+zSf[ɭX՚0T5QC/Av&-ߨ}Ӱ搣~eo$m[\'i4U*` (ȽY3{AzTmC6k[uM`mɕokijok3ikyTgTEx,Tֵ[Gk@*Qݭ$GCO, O u:O׺*E_^M@ڽJ&Mc>QseOԲ}sfr ~ԾFnKz)*vK{LxgQW᳛\D8m0β^JX7+p"UDJK7$o#T)vmZ3.,,E;(@B+I7htX]"]32Iw2suLXmeCKcYsouמgѽi}g$G= 9=󕣒u/F60#An> [K +Lo2r{__OhBkK NPc~V|qhqo'R"V]tr;<:"h SH s[YnH-jHF'5e1Wņ1kmfx}XNk}{'T,0AIq?ȿH(w'9#'[,oкL}6ֱg߲hhl~vtxtiBGng2w4Ы=KiAݷJz7 ׬y _rRU/}sLwBoRcn]ۉq]bdc2_*:C_xirMN恹SYJH +uHǔXiP4Sqsmr +\H]rt<BAd<XeB&:/&\>9Ⱥ^SjqlBsK?pUڬvm#Jzd~*8ܽuf ,%Ƈ=œ`qKiHϳUeԯ⊾h+F?8u/*գN%Je˽D˨?sASLz~j'>^!ۣ&f1;0ϐ B י'# i9CA4F:otrtTo"3eJL-@̧/@ǵ[R5L|}U.RZ*zzkSѣP;1pBo1Ɯ-sO.t4yi唢0NPFUȷQ{'-F3 +'DJ-Gwp\,#g?qֶL6#jGVfM (i߲'dEq9gNqB_{'yAg9 +2ub:<J9|q8X8,cmxtp+ی;v!MShzmP.&^|RSa,DRe -܅aFa}Hw)41C&Dn]!8m?ckMxb>Z1:?Xg|*b2-T]M fC],!5b, @rlR(rkW^V5fx 6 ~3>Cq<4:8iy"o_kNAj=*Wɜgmjkjg/fz^zs`z3L=Wte]ل '"ΕnX pcI8⭼Fk{kfi^'h +зP8VSXɃb!c`CkKt6FuUߩ\`d^OjpIKQMpBo1W;9M'|+bH}ƥp`y%~^)` ltx"4X.ez7p rx~MPn/6[C$QZu9s?QB&O:QeK;Q-#[6y-:+ؗqx5TD&+81Qy@z5- YW#@0`ϕz9uRWF#f5BO:fO,+KTxlT?qI$̙7@x; '#Ӏԏi@V%'|MִY,# =e!2)FTW@2-Y{u bora>}y!q]_,$n`5̊ oC +PKq&VmFCJ7&9F- `]Zokq/oz!|4"&'ϱ6MkN/spoM>xv|!}?j+j3_F'eT>s-llgyg8`LgHLM Eu)5OZ ԵFNυb7Cκ$; mD m*ޢ>pqC[9թ9 K"Fg_j &F u_qK,O흿yݪ [ynr{ ;}fVmw5hͳU*U+^u ÓޕO׃Dgz&-1:{͖[{4GX?Vg8Ug0O?t;U|OwM)s]}oK +e׺ --@5J~^ͷ'װ}9MH tp>۔Zٷ^W%Q/O'*[:מ c?~hM]I+'%U7pJ#+q&m=xм2w~sqv$ ~<9[EY%4dϷ{D)ӑCtt+ߎЯ2n5A9峁ZWY>֜w:u9uS c6햣ul6n^etܞYU.,:1?tń3X;gFtQR^3׶@|I-2+-ͧZϭlUBrEE%EQDzFLV\wLeEXSou:fP~;x:1*;->J_iS,AJ[3/  %8n4Qv=k@e!x΍xnp~Fr 2cfCA9*׳mʯrϥNJ@#ㆺ=Һ N)Z/>pІy< 4g< 4 1 u1ȓjތYk0 5tuu.?2e}@zT4秦,J5܄ +f2#()h-ͻ#~.H^jEvJ9y93T8kլk8{;!L TLib\OGu:l:`;vVzhfTû%$IɠJnd" 6qzWW,g;o8.={;j]uLD F(1ΦH{0fTJIQ|HH`X wk"?%y@ܓ,W*vbp]B\N|q"dawV.!׌YoS?f|]$ӣDs4 gjNjT.fѬ4z(TM܄SH_1楟M1.7#'.0\x%t,+Ow\yP~"t䮙G]mz)RvM +'KMf )F*&3Ǐybz*ǮH\ab~E-]u4ؗsTezG(YeWh~4%Thndz(y85Mc#p }*2ڂkFi p7-~Zx +-j6Ev/ +<F(Ac(vc [ʄwfA35,wf%04^}sVq#OU1\ SgMC3!nXd|%&~ṶzX +]U`'a?@(߀ ;/  \5ZvsU:WwKx56CLp㞜sWM*3h:q\RUpH.Uؘ]ܚdֱ[U]EHT=$)(^KP'P␃XfʓIP +ůi(>UڽAnXkmQ,JBE(yʹv22E1GSBC W&u 4D ߫'(q Cō yx­ *g1UPPkUD)_mVBwwT|y'Mͫk2鰋y:g54@{.n/JIJ +pdg @BIw?dOH +JLf1{@H(yPr+cթ#i2o>bK9;[~3 8ƒǯ}2¡d*F,MpeJRtP6SP:Z.}4iȍR^NR*}%Z8kJ z!;h|_t|p%/?%_rwk_ Ň3 +f$:)'@guc*I(mm(MOwPCS_P %0M-Ep>GJ /d ?#1oo 0%S盯 vϡ𹇲e e[9( l*y2Q2kj0 jkwRH>by 5 ؛*>xׇ9|4\_ɫ\Yr|k& +2qAsfJzW F3 9(fĎjt~ypg$ rEcoR*Vzw__ۯiu^h ft@nPaeh.OTHB(ohe(?.*咏/e|( n~_wXw?/ݶ͞Ds*1T.PP֠\T_|?D$GzjP1r&fS/y7Ooxѯזnߩ;,}}o裻 bPuh6j\K+>u*|U*s2|j **+kygy?ƵǡUöC/]ong}|MmJ[i:G;}Zp5r:|#tջT{C&NPmp|?lhY>xk j{H/H]k`zAYB;΁NVg{uP[}? x3^+4s3k3cZb-::b ?yiNl)i+gr5Cu#D0z:LZ \=r"O5 8v̊zŽ%o%sxqS $":=*) O4$ [Cۉ]R}*#ٮifb,k p:8Xx#gJ [X!k`F~k{n"|EmZ+EmZ~{j>mfӖW(5D'߯W2xjC=ѠX7fi_M16$Lԇ5qwrV륭G VT:n2#qrUéM9,ae`:(w]Ag$Kܘc?7g7!NHXvL )\>ңmc +첾dMo]Rt^:ՇցFs(?yF%g1L\O~T¼Y +'w0x#)׿]nĚ)tGi}mOL}Dt2[gbyS):TܷrI~Gm7V~=VCs5q ܤ223eti8;fcuf)5CZdh3ZlVɽT]ĕa% +ܺc>Ђlԕ:}KsNƵzӎo#gc0cꯀ3\l2QP/S]uCf aV/{3'|S1x\^#SW^z/@:Wzy';zTvu-l"=')p%Z829BpRbp )1?QCQO(zLk܉f5ZVcPƖ&皳?nJ5^ѣkk$m\"&FZJ]|=]w|.F.|51j|_Qkx Oz@NִV,2դ$3w%TSBM=+fet'A? g }soL0^˾3j_~o Y?#./DcF?\_pc_~l6?RAUf0sv đN fۢ cbN$J#L7TA/we4~Y"7+9GFS`e'U|udSȓGʓW!z]Y:IAO]žx/Fo7ack+$"nQì<ۦ+>IL SbaUdۧY+[+AX#Z9'ܾAuK2DE kB̈́Rx/SR/Ι8 s>K_g}{;Z;t?!sP;YYxh% V.a mT;ӏ'یaV=(Gz[yI fOT+?S8KPHeO`1~̑S2'0AlߘS?Al}'RÅcF?\_ F}/ + + (,{HX:fξS3.o +@`+WCnFˢtHvDcIP^g@H汧D?G\AW߬|Yaa&柲nLieI赻CE`cADӒ,U)ّC8,C(6&G|$3n$sL3V*/Qi.(N5' ˀP +Y$F=Fߟ%QYABs *"a /]>0ɦ9WCϰQYiޭZ+gj'< +2S i𔒻c.h}]:b7_恟9%:iT +YH]NEJE< +rĥèʳ qQ-Or1X0]#'DM8@ ԺX={[BRUP6ыI)1) %̡MEfHX05 a" a* W +]5 ;BN<21iS2kWlanu{u7@"]ػ5Mh_>>P^6K4K9 cX/"Fn J#NN}/RlOJX'p426* ΊOW$Gp|猝)Ѹi){qs먐֡ +\gwBZCN.38Bl?fcz84ӈ_/?)Җh\Qk%( KJPyX)]Q08޾1v9Su}'v?fw7P$}/H!4aW/Eݹ*cTkX8s#Mp>Pސ&ʥev1W%w^|5-nT+u|d$Z B墨Xj< =6VԽYX:dn92š#r"j+jlOLowJZ!qN8i;CwQ4[?8Z)&YoTm!F}Dey`N$֑m gD֍n:]eCwJZ{dce/%irOт 3y2@dpK<^Ķ, )yt>+YNZ.آ+ Po˜Kx?gh' c ^nR=`™'rDoV|B$>Ǝ;N8| ~+`]-پx=:^w3Ďn–5mq;] +O!߬ו!`&t咨9}muGw˧]g^ǰjs7-?Oi.,f;; c90`\J %W$,6NX[z`0E2@ĹCThXx~k~3Dvi`3="fW ~nvKU{sL%{2Rl'y)qveզEB&IS}ڰH^k4a0ti{}Ҵ&B8,~֮ y2УN:&kN!bIihuO {v1x/Ѥ9Q 3tm:DW85-r!> V˟(e{Mo3qԈK3U;}F8~E$ݡl0W8/$N%$NQ<9p5^">+_@ +nOS9ys]v^)c2QKVI1Nr2j5g `~V{wF`2wx:pnZ~Rf|v9.>Qbȫio^іآlfSItr7Qvg]&$X~jwc}|{0fT[_{=y(˦!eCO8ǔ<[aQmTÅRJ. +cI%b= yoi5rD>S&v +厫KAjg7ϛ"9tjƖ)㻃9cXQY(t'3ﲣFPr+z8Po钱y{d.oa<O:ZX?zN^F1J̯Jo]BdGCNW|@c'v;`u/'sbݎN9q@6}>d};j#tU"o, JhnRs +u +u+bss|^!8wT=ƣy!e;+۷mʴmeL7YƸH!,E`%'^LjlV+BƷ$沅k霻;Aލ^n6ͪ*%t60Xosx]TJRաDmVC8Fॵn?)mIXLb:[:@%uKcpOO``ld;d߈ 2(Pu0sՌ/붓Z'5¢yEsDC%1,zt~X0+An +63[VO %F˛[zoksF}ZBbnr"&률b8KQQHbȿ ˠ L/ + LP?[7o; ?[W?,$TX/h,,=:xNR.n5bR']cPݭar{xt=1IPr+&bj%˒, 'Nk䟘I(?%=*M=^ovPNQ<~"z fv\U;bEWt= 跀˟Ph?Ee>U\%Pd~kI66;i@I߇^鹘fSA*JZm?ϻhc(ź Fɑu9,tXl/ x|FÒZDk4ߵ?33JJ9K\Q+^=r4wh$ +LsflL7spHB3~m#~s>[Bp\BtonfL#_r½"pIm>E[~8/V<_^()UPbD-aa~+='Vsq s0Ⱥ(|hv}WVߚ񺂞@iQAʲΤ3~/1(_?+ttr"k3n/<9``izc򝃫mX,;a诞:.'F C0a=U_|7\$@,f.7cgT+2"CqFw1(AAkWyc$,bϵq K'&+z;EiC{ݾ2f 5OzjBjg1P \ͬʃQ4R89!f[~ƆXNz{sxq~w:ga Ϟw+Gp7tlt*1c䌬^t>~x$,l/̮1q4 0N}5[fav(kzJfz.ῲZ%^B&{u[ 3_@ˬ;p3`{Y5ղ0XbK JkmEp0ßU%(55N!DXK8 &ÏKX=O:ba<'n<ݝ,1[730jIhuyDGXH,%ӞTD_=3I"cFһa{q l,gy94+[gv^Fe2x ur:ߵ֌md؞NQqĶf1 }jX}XQPhN*_7rV.8Z>;˕= S^O֟j_$1Z- ]W{ū#{e, :gOyxW 5|cFJfjy<8|wjQ+ `Yo+C H 1#67liڍ̵ۃ(⷗\H‚NwXwV>g 250s + 0;Ԗ9}؇٘3#P YmkٔD6|4+;Xq L;ܢ}ڰρ;{4Nt_aVf֑z9=Nwx7'{0=pצk࣍=scvŕ=Ak\/m\F&8uM`!C90Rx˯i'ؗgK$Ehףbf2hԨܥV%ksX E [R .k*+.lo.t>KFΛ>80jzCT(Y-_cϲ=o_/ZmDф3 78ߘHfsО7F[:W,qk=/9AQ$EoSa;Z~x Oۇ+OPkNq-Jl`[O {s)\2.[CwW2C8c\!uy.JYQ]tRB՚{=+-u I܎kcsQA<rCtOdJOCn-{tZۤ6?!v}pMYE2f!YTWj{5cp@ٖofۙ\orF>lrVb6^Mqm擒Voiq05{Q5d-Q~+n$<~6tzE_X~yU1ulZ;½W@vD-=Yǹ6V(y縫a] o|+i+st7^V}oĉ]+ ̒E"V".E\흲p/;c<@뮟F${e6J~pW뀐r5ݾ~vX ra=ܰ[aU|+=]߁S1+6ݸv-.LKs1Ǐ8"h3VFӎ& 1/}KE=e +aqr" ?,zX7,şHXo-g0j6.VhMͅC +Gc="F!f\_sQBºCyiy_ qT [JJ?,*xXj|.lXw;7Lng(TLW׮gK͋IiDE`!|]H|s( K+ 5&ɍ*\ ?mY344شc{W]M2ⴺc ynpqdV/zJXd[X^rq+~uT=۶yzlNݕv^O[ Em6DSd1D$.J%rR6l}Y kR^x/oPBҙlM mzVqi6Q߃чf]N;iKPg Y/k C:!X9E%}yPigLƼQvh-2ٹ<vzeĵ:3HZ`ӥWn:iY^DZSw&sӑ+V{N/HY}%$.-&^{\Bn:xY1 ^?.?m繵-+x/€J61! +HD {޻Z}=<Ь5^&T՜EG itM^L# +,x4x +$ۅ=:<|c5v.F(9\VO6.}N:˝Zغ%>vwE{{eˮ˸s|_ȯE) + [:g4}m$>ӡɆKGbޞ^\*zJ71k<2H&.+ͤTW4MzOSLU}U=[ g;ZzksM> ,d89Jlt659{wGĊ~h]|!?`~݀m(y({mj%l~<Ϙ4C:Kc^ό* +Mz nYݙAƀZ=@/61# 5'ҟo@2%h-FLq"unpWg+ڰh(tӭjilϚfwn| |glyYdxP*˿t0Eݸ}zΊJKA4ܹƦ;W6IKay#+M3L2I#>SOO fPذմO\?HRBǩ ީzYuͶcQSZJ|2IX#s>p:swc"L_P|6/lCnEwiNI?~]6O[j\x&:/SvDY 8:ÔfӜdJ]wGkh5W?7@HnIϋAGBO_w5 "t#>"3.?fpc̓K + &z&Vvfo븸Rto|r|2Za[Qw3wӓfKH'IhN$dŸe2g.=ܶbK*OE +_f38uW=$Y(^dcƔ\Ba9 .reEqXwbMRsuݡϫ-UzFS5Ze|q-* =/䯶@X.6$zmWTi1"վQ>Ts'`Ů{DuvYl+z~g/dՀHq\*΃102``½#ȗ3hr'$NEY%VzyB\QϤٺ}<^Yels9A݅>q 2Xi;_;Wa^dX%ͶsUYG0fy:}c[)ּ@GBfZ VæJk7Y xb#ȼ+#ԬSOq;餝n {"EE>OAt=ij)v[o494KW2,Eq~ +/dZp!yzn㷂JrƩQ=1ܺ +7qZoT奝.ö/ ncOWVl݊uJ^Bghcgͼ(\+94rh[ǽ]iC=f2͌`Y 4v]B_߆Qkk~CR~I.[ch̼Ցփ BDv~[hO:Om}͙4?Q_Y7}__b)肋e< I@h4dds a +(A0j*_<$@VAETJX:A *l"WbD{xl] 'X|0= D +s +{0 @j( 5T̻s+*QO4ZV!8O3nQ{M֎%)JDosf6Vkξ/sE4 Hswf<8,I(Auv-q"E#6JeaA6A[=48T$x01 uwodj1;4~TI.r{;h;ƫeދd*M"hpry8gpodnh.z\M.tm_ݰ\8/DKկ&JlP`Zڌ0/u0U0 g2 + \^sl bWOfW{&{8p7.Ԟ9;0H(JlaY=@7{@?Cqd= aҁp4ZX teяۣ:?K- 7ld/b7c[XWQp?SArg\F>ظ ,CoFrn]V\̩3_B~Q@,J'g'lx̳Uʔ U +xHU#la2Գ:?5o4\^Ҭ)y톝s%'_"VZGpLp]@͍9*:Ηj0 VWjGeT+hT?^*Tri :1o?|/; MLԳTRi7MRkRQZ'Ļ7V=r];CsثPePHЉ+;Z&xPBS?GTRU1J)ͼ~FNgL~LQU:QX9@-oBj=3Zoo.<9 J\%_a4KσdHqYRֲ|ڽBn3k-*1.TMȷїSs+I ECШ.BtDヘ!x^?;EdZY29͹u =vx¹<$8Kfgׇzd:OQ)?4|@ȿ]5(H?~e>VGs-ηŰj7h^ aIz@I JtG,s::X؏k +)z{-u Ox,%xQ̬27M7? mm s1\^PGcBdas8b숕3+Hڵ7{k׺O$Cs%^>a+J'iJA}ԉQwyF=62 M8DV;fk]^T9PB |B|(y&B#p);oD]n2oW>E=>G@Gt#׀_B؆ZxŝKGa3ѥ궉VB\%vp .wJ9A1GeI*#HYqszo۸’÷d}Yݩ`+[orB @/ }yx:ꖾ*~e\VÞ1 [<%g[*F[3uqSbBvٙ.,0,X@(=?VugqwAÉ{5|ψ<|-oaPwYop63z UحhhqZ e֟Z$f;jƣSTE6.w\\p3E4N+coy#[`}i:׷JRt֍->ZSN%jJwXmw{[s/`J=:ۢJT3ao.weem-;͋KS~6  +Ť0AEZj0b? 'oN@vGd8 +v:J!wúUNpDpˑ&ƕź[*#S0aMl_ ɇTs ̌ J|>3UfajqYB6$˶TWIarP$as%W]wawّ%:߲Ґuub*\m򌽥t|V?PsUaU9u;si8ZJikB%Ҵ~h3>:Lh%ۛtƕUvϧiis'nkoݻ͜ qj-yR[\^U;8f'=2,/rn P㢏:K[s{>gU| 5 4i_ߌSgU.Bzi=FTP\ͧBf~ z33V(/U& ]a+Q+W0i^DZ}])BF\9܊h}@Dړ幞NoXވ;Z#f['/dcuC;㊝`W+ɡIdo%Ʃc 3^yx!j;9u+`.Xb\cv+`4> Nh} ]z[Ovx 6-z{q.W :34y//[smPjܗfAi˪i4P.+* pL3V$4}E@+=\$Y+02 0;RCva`li=j,, =u/Ѥq _74_EnёE?E5?yQKi>nj(aEgS‡T .Ҁ8#0F< D1{mnC; *;63 +ad1!R&znIo[ +-E~|#LB8yBVTVgW o|P<[S +LLUZ5uG6v-3o`2{L;W6'Hf5Q>I9@2 ڵ ] 9o=Ř9a(+ +=Ӌmg~;雭Įn$Z)c.ޏ<1 ~N$O4! }Sӝ: . &0D1k𘉧*&vX<.'fW VIjRgV`7A$]> ++ +бOȣsz¾ʀ B"^ jDLS]DCeQ:xrj)&[ . ̕$Ԭ>#W@aPW$?"a?~ N8V{T͑ȫ`p9uߙ?;[sK^pQ80:ث}!6]$^AAS ·1cZ*~]ɓXnJdWJU@ #.&_&Ϸjk;5C4' {(Ciz^nh-ZUG􅨽%(|?E['U +q OއAec'a@Z4yXʂU],GMN+fo^S1&g%tT4VSð עJ? xqxE/m'g +.{sMcɜA$X(HԠ_;VD(6\d<}k#OM8aOLZ+[o2i5˼|t:Zx Ǚj#_ȯW>=! h;2Ͻ൭L}-;&Tn)ݯ>ISlK|4ʑ~.܃ݐ٬-<ۂ䛯5 \ѷC {/˩w)H{WKVGG(_/UiUݽ YwtwV[3-y[R*ޒò.uKAاR]!uhe@T +6#W/tƩ[uHֆW|[L&ڰ-oFߔ +[H҅-9~-R$[!Kܗ '`456} a-k1r>,x>/}nD?(~!lin-%՚/ա6WzVYb)3Yv-go2 w㩴 +H^wyZp [~-O=mBxt?GT-Y/V %/_/RlanjM*U4IkNᛒ@ 5S~t ^,k ՛(h8m;4yvezE@|6dγ\qT[+|2"gBF*/N}iK wH\!/3Ea+w|`W"Â)ϟz9NYW}?4'?&)v`k7*~o:c!XӧdrL[a + P.늁z+mmԨTM׭wϛ^'<6Հm;1Fg0/&YVO=[өJbs+ranD7:Nӡ4VXxqFv^{5 +ѿr/8^1J]ztE|w[o7i2ޜyҜlehfUpz*3qԙoHk2 cSFڀVF q[/ 즢]&;tuIn3NLӣhCZ|f<-ӭV7Fb_I+Z㔲FUn@32Pthv6RX1itjY@@(0ׇ 2m[k *?ERkZm ~5B/$fh+3xe?QРIhjMܟPztwr`Y&#r\1W prw(<&D=,`-V`XFBnD +S᷏{*uvh+G $>W?@" |Rhvj@e;LF{z5VX}G99U8~dL[&t^F4|dt2qݏ+.u٩_AQvPp{P% j+137#5¨gm\ +n9OJvEX\a{萩8x?վ?٪~,m.2, \3z8jOT`j"v T%Ic o+[XK~Аg{^BozMݳo\ȫ,=lMY\).Ɇ>@r8[&k-%{P {{ m |6(1w:=\'sVlߦ*T)UV'm/ѣV3껙M2_B~P]݆i>PX&yʡ|wg8ԸTٮ_g%[ohg_bTY+1Qrj5)$ ;3^4)xAvt1yg\Fpٶxj)YnV?*ĴW ϶8\F9UNս}nK|Y0)o``ùl*Al'tՙ~!NM@f|='QΎ/KizlwԽ,VI!'OLFfZo),QϾ(`aDLJ* qp E]GRyhowu;V|ޖJ-*Ա/W?3@Ɏ_+yv4@K"'Tap@fL]>ɤ^ <Ӝw wײY@bq_|] +e@OFj2RoEG- C=DeL*wjױlfŐk?ƣXw|u{7nGrE^/d׿"Nπ[ . +h0i`TQg9Xg{[0:*ΧvvYglc4uc{lg7Պa8/:R{m ࢑d}*謱pWtՙTz-d$q8 +qFw/>~^}瓫\1a0yla Y6^W_z{ͦ%,-kFǀ5-4vHR:2fttӛ^ٍ~<7XKbLi7b\[wmk캞KN/.;6^JbךSxS|0b 5X;T^n)x=ژ\+{vT#w*)eĮ(hwx>I:mu|zBe ɊZ&WRxxl'w*괓糞Vskɥ,FZ׻zxo]!0=o-6ez-\aN3xB=̍s--AosEߕ?9g$i[_tElo/jBˤ.;r~QPku"x엣3acrSߓkZ _L`QjZ-M3ӭ.gr7v7Muvͻ`( p./ يIF2[z nqy#2jKhY3 +fͶΚ,G|,GT Z訶ё1cI!?࡯Xe|>D{\O 1jX?z?D&G,O뉵R/SM3Z,~i=fxUJebUPYܸ#D?Z h韄ias"d?+"xـ 6Z٬"mKCg2L[)me5a/}?{:c!wI3 1휫+q6m;pLw=tU]-xhcll{Ҋ4'Gj4L?$̩ /~gW|:L_7}ݷ}Ujs: e]5PoUϫ`^ڻ_B~GuhK黽fg g^h3 |θަx;va}nLoˎV꺎C|B<@ R7Nq-2p=)+7ϐWjd-'ȽRI4F#]/Ix*py}97.>/LmY4:lœrpFJmܔM-d\T peKA3@Ǎ@ m)g䶻@ Жwhh{r: rrbbKQrM+Ǹw)^E6윇`2zVk kVԄZ`:%-`UPܽ9mOʃ:Aѳ?'NdI+||t5.r55 Z.@#[ݵQ`(whJ.(14u,\@k{<$7;/}ОZE7TO h?EHQkgQbzX,;#\[XY„}"TT0+& :?dj̇3u>]Y"l O BLF;=oO(0r\WCu/8@g\a9~P1s:%S^>yz)Ial[ˁm ݡB~WY9c ٘8|&ן #V`Y*dn#WQL6ZS!T+Vqo]fv[fc Pނ,3MSsPi'9%WpWu7'q*cIcF'eP'B'߭v:uYS.Mv/Ѿ=S[J94~f +jX^"b{ˀhᣘNp=!<[ .S_JFt}a?_{3yIQi'Ljz?%ٷӼ_ 5 أSKT} ,F3T9T~X0|*U _ȼ{: 7jLL~ +I}besF ƿf>Obv +X} q<ṘhTn!OE7R Ʊ<sx/_;\g֘,.=ʵjtFgzuFB,z [ۧ+ ?[N2P=50=b)oS;g)i:$V,{X&=(,& yc=K o`Ojgyي=M۪b1K.UN>U].ۅכM^s<[Jq.٩(ھG8GqUnY(!'%+?!( C#紺3V%o.{49 Uõi/׹/4g^|V֊Gj +`I:V=$I:]E/6|jgN#mȣ1hڷ3];D2Џq|շ!'Ք(JU=Wi{?i:ЧCG' y&%𠦒c$.o nUmvc3eLћNf rpdf|񒑤kqV"^aYET;K9΂ם?Z gWvE5c\m׊].iDđ벱-+k\Q^!,K (p̐̇ˇ{ZӄԤ?4?c%q}\ӀxkQIq,ǙChEcT7Rvg?IZpcOqK>XxY + 曉*j|wߎ/d߆o;"w߷9sߖEm^Ҁߕ *}_Ɩ1a׋sIQyeqC,e:0~쥷ԇq'lryFlr'ռҾk҆ ?Lgj* 4ep^bߊu30i:|ksr)Vkx\xQݖ{no5ʚ#e˚㱾[s{dc{C fnPÜX9 +`\%I"3U(ݨp4TL="gX +iWʷ}ʺKܥ3vXzLnSzSa6YKy ek5(]bj:aջl.mQY4-Ѻ+$)iwQK~O?7yk–\MNkNʿV~_5ŪVevm./ wJ“ +Ov^SȩZGmgm/=#1h8Fq}nA֪*p?s[q]i>y#9cr48L26[pkz%ۥRIUyu'n1J;=\jѽpIxMC7.  J!(lzѕeeU;Ww?AaޡlfBȘ2i+2vW~ņ_esyȎǽ3ɹ q!mPgpl]cKe0󭴼n,YtRkXڅMi0&kEĴjU5C d^^"=#|qqYkwe8.!p_g֎ 99Phl҇^1[[+}X'`$89Akаd %lB7?G6)ԥrV!\G<-f)e3b3\J.YgFEz\7 7YN<¬l0wxS}ZꞧexzJ܋)>]=,jd=6_9v{XG׏RZB{."Ami 4:y'?m%5U{: Z̍m@Qϩm c͔FJ-3zKң.bMa9q`n)b_sŢgV~ќeOH'08e].>b՗2G n]bPnJXtcZlK՛aM#Ovqb G*<![̣yE'N}6;tή"Xš/ya5&+Xf#;t#7siޥs'ьڰ%y "D2ej- g}O'h8Ͼo^;^х-F& FSHm^o&~ۣ͜JgR QRÚs S)-~dS! ތx-0zwfG1TV>fnrRTn2e>,?0vP0/ ꧊.=L?8  ݏe,7}eVkHjHfLT?dP\a2\NJK\9/fRMA~!Gj/(8uN ]& ,@ P+k`L8|X|CDPBA|bXd0몲xU~t<A+K)Dߌo!&T@>^|@(PWB{?n/y~'qIG|yo'@@j,p]K*f,5m@HyB`\xTwa{)=aBSn+^x1}` $9cVq̯|;sf +ОtPB +Wx)s$IY N0x2%eK4 ZBĎĴB%Gb>Ir׆a(wgkw[fs4kwaE qw@ Y(Rwaojio@" XȜ2<"*BTrv$wS1/N^BGzGy>TE +3$ ug w8۱0>ޢ͂^m)7C;'y?< _pNUNƛ<+b ځ>ݵ;Lnkث9LV[NW=fjO(3+=C'ƚ{ΟY*3b}yDYfxGR8N0Ϯ9껫jzrx<_؆B +6p&Ccj I7 Vg]A*YxE:N-:^b\tޮ}Dھtlٶh /^Y% zk 狵B We4,_'g\<$^ԭO/rM-RBՍljU~S4%;=*9 8!4iAc#ߧmE0} W ]_-uM/^ˇL.EEG+*F/kYs7M@]]^5je)W|HC ?C'$)h,֩_^Ք \4Ԟf)8=9#gj"}9ޝ`}3JsU̾Wfrv3o \ZJo d%$2U?]3ʊ*48{>޷)vr徔?Ζ+˼}ݧ .|Kk8&5ܜFS+B$VeNprI +AC:]:;#c]\} lִc)Q]xVtuIz2쌚b\1@t $Bau:g9= i~zҗ5@}yP?pl'utr'vJUirv3z^^)™YQF'G]+Bl +.t^\J΁ȯ*M;1HZ-?޶+ڱc/i.57(S endstream endobj 38 0 obj <>stream +++#kl5G9ICw UJYn+KP&]@6?2u:Ū +d~*H܋۾~M \ў{|ɪC*֕Gk*M'a=w֊1M>Gȫx>U@W0/NKZluf +<.Vs^-VOn޶ewG[clE֕{8)c $|ΊoJ> " +^xrȋϊ<2tMk+-{lA|n1'yʃˇ +u pz/Ml֎rp22^֞|GL_橝|A\׾±~vEʹ\y,˿w HY\eD*^ϛ<8.ѾnM>uƞa~Fh=SZw;}( +` |?+wx-՟r=%Ci0b۩ɸ%fK`-%-X6 {}ENqϛD1w lB(f<FI]:ZWcXʼnLfo5;cwrK#f#[\<ꣾe2塦̄+r_e]3_|,Ov%PcM`zZN7*3i)\mEi8 ԛL8:!m[. `ܯ?϶ß7f,Y/N27+'C#*5ͩcR[vHR4'v-66POGIW֎^7ȍ <1*\[>BAr֫r;,M*eF;o_<<̊;>!}[332M8ZRk6Pk~+ +zH]DEB.ӜZK\ݾ'̩dQeļ!jdzʳM?ni?G󽜚r +l +DlW!f R|̰fVCj7jo=e~H>LvJh0{9,jntS`NYJZa'[Z',N&Lh+1->'xCn* W,5)*ii]dsMg +qbL +gh[eLw j1QgiNQ;Ofk|h|tm DBh~pByWFe{5cԪKȥh;$722Ě–Tw]b}Z6өʿ6E{p,bg.=t`QYizK|84"oK6.܏u$w\Z4{firG5il?Lxs&_fuXj澔*ZT\~?hzs[\_zYkL:{ԧVkUjYsiVsh:Z5c6[T%T\:#(#(SSǘ|S;ܸlVB?q7YC+"y#alQ<A^R% Z{7xɓyxGoUp.CP&cfpeyl<\+Z!de[j`= s S{ B@.YGڻ,B`VllA =La)`J'Scc{QE2KBB_c#~ L &}`+ nMh#9c>aXP!iyMxDAJ,>}QRxYj6@X~AWt!k шD3bșr B*xi%$s&XO'݉&~Fǀ|>Y,Ggho$7jBOݷIhe-(2K*T"@`D񹸀xdj0EnnKcleLC)Zս &x脇W +N+sխ}z/S?U(؍V "\>z " \VʼnKw{ A֗;Oj=L~t'M1|}if3gK΍k|t/VkF hW?G<~L^$Uh[o_BQ|r5nӯG>a_ +\zD={'u[cwGzQL:=^vþHwT+uhTi{p{lb>aN-jy}졕g'(!{p?TTp~cnһ"dлg(8bi?{C!ږ6-ē0~QzX% 8l$4 &(Cz5(s\euя$!gns" ZO}VU5>}aYJ[m*:; vvz]jYKcb`Ddu`Q\V[:5􊲫jWMi} hW} 1S|u64Mvayv "n5rٟeigIr^$1YԬ~.dg7-`>K)VOJ}BCUz_0|ۂs܃9[S~3Vӵ_m9fm[٠#.{kҾ&ߛEB{juaWhGk +{*UUQ%IUnwۊ~S^C鎪/Ax- `)L]l}n@Z' +K-!my=" vs 7tc?ek*T)1Q7? 4ĥ.n~_ +8P5C ޅ[c{X>&ᯔB}E4tPl/5N*yjRգU uUj gA฻]/h8_%̼T += +een7pϩi`67i'qf̹g3S2U +09.#zY6{lUīBPu!T\_y6 iAZjs.Y>z2q^*!krOT3; 8kk:xOA8ّE t4]/' ',Xt}?p{窽6XZ OԗqmϮOyZYq+ZЬ".3?(9 +E6*];r?}sMR/XU6_B +}NV[_y`r~SB]Q}gG5Rb9XQ*(<ׄ;;"Vof9nR &,W-/ ]T}Q|Y,GeInſΡf dsV9>Gz(64C*N3;gՒK,X_Kt%#a_%NR2H򸐣ř3mu+QeN'4TkUz!eze< +7YԳ}O泔ۗj1v\Y]2*gt}I:+E?< +ً?E4,5bqpKo{Pv;]ۧfIt3.Tn>+5kΚi2l(ͨf6? +HO1sy.&i)5jފT kgv$4qÏaMpe 0s/iZv2O<̜Y_hDNk{sb{hb[Qn2*XQv_0Χ)J#n6n>i1;]8dZ a~af7탎:9-i`Jduf0Z8qk27/㽪~tPgy6F2Z%|1b?_q &=wgPΒfii=6# 0bbDѤ73 AZ"eh|)x}gdT4~ȋWcXu?+U*/};4n-g*IuD;c/+:`2zq5"r?cԧ2O/DOƕ^wjv<ʪ"3^LW{:Z{:a{?o'lu}Ƙ%O봼8\s*@z@^'NZ8w:Д!1TzB >TʘHH; E`2=MUL+Ά@Ďo-w +AD @>P4(nw@zHj*Կdw rrd*a5j͙$)d%ph{Alڢ<Pam@OɮVV:s:w_/?A Ce +u5QyIM?}e9@Fp֮  (h +xNv9#q*Un֟Q!gRT(@k(S+ЌU,uSTX}6!`W +`Z%MfqNHJ[Uf}Dz/uo 3Bl Q01,|2iL{R+.ޤI6#>brMK>}]y|TTgs@~ S;2I EɿACy7[kx1@߆ 0apX2FS6$9e2EU9VPS6g3qyhIp3`?lh|n'I +Wp+H'̎֡L%e4DNJp9ɼkzk)|ْC~/2z悑^lhw:O-hjscʿʏK}O1A:4޵֯"V^Tr(^| | 8{MPFCzyi_֮w[1ns:&dN{pp^)>Z%2+1'ZmGكkCoOFѹMfAq?z|cw^Sߜ|pȱ-jwmEtuqNw씲GondϜYns(w솉SILZ~I+;uԏ7(̡LIȡf$3]Vas߯ޑ|&] s9Qon~öFJdN|9ʩMm挓.RF/t~p +~j=Vbk98&VO).@\A&·Z1mpN4ZgvL :*u={0ς+hΊf qnZ7ȻOVf| M\J܌ϏXhN'4Ș`FMI)b|)'=/oܻ1K-mmCxsHmռNӍqH~]J /z0|J(y^G6er!< ̷>9m?u-b(\h~e]_bt?NzV]W,;9rjo@fF|f΄HF~'YΫ6R6X +mi^YqZx-(!Bmy}yi{Qw'&^̇'68;$?: Yrp3]h.޳ / l&mܺRr֔'V!B*&ΛD9\"U+x/ηQ& + 6jGܛkbŋH) 8FeY\d1A =ytGzP4jT2-"c9>nMěu#Px|t9=\X%l3#ȝH_4uid=K)=ۚ 6nEiG<0Lّ'e&u^U buG /O]ٰx Vӣ \`q4kx &opY@>!)mF5aKSιbѷZs)ӴV\RU RLzyÕ2v,iZ.r_n 6/gy97 KQ5/]FO-?~pܖД:6];R2y_b7PQx|Zn|?>H=֘J.B/N"ίIo慮W?,t&O +>grx-+`~a~MJڡg@5k} |#^GuΜ `঱E.!k܂u\0>,BueFn~ēOqW|J hobib??˷ۏ֛m.JX߿tl!ԕ dܺ=8n*߸΂F?Q\#@UgmyAF,&gNy31._dccጧSC=dO?ƃC hO<˽=L^{5nSn4d=&t)Ե;Z7GT0ߥg5=JH:LfXwl3 oFloM4[j~#L th ۰i + ׬Yܵy#XFP+ wv\xT8#m4cs_.i Lܱ[JZ1}ЬG~ֵܸDxu֨cmLf2ݚdUK?h$r;OȖҫSI>Xz pGa(=5\1[ejXIliB5sU+ؽ(,UZR~8BpW~fkoK~NZR +!MEfGf#\C{ ?p!Fb pN޿^)r5bn0`V3ͦmɓ6"aoϼ4iE9u^L~dIs8q(7ZnXOGJ2e=գT ⟠NzU f,JP]2% + +w5uƄp)-2ϵI +֚V$yDEdHK+9(Z#zMJjPCtS wU( QI#CR8T̓h3 u#H'iPv4VzҼoAZ_@z oL =6qVϡƵ? KIGGL(o #+ <@zTAٚ ͖Y)i.EA@02V!Af 3 >u ל$;'ijf-C;!B R 2cqfC?~|PW<8ɅioEs~h1MMM#ؼHE he>f&lu  " /+8k%SG99-Σ*&HQ5Q%l|A'u,kXjgfd#@:~KLyt@]G[Z;%-FX>Γƫ t1]|6x}P%4*0;;2"h>!FSm֌+g8.giv3N'u)m pH? YXثR͜0kk Jh/p3lK}}O|7t`S}+NN\\'\+I=?:${e簽'Qc RKcmm[i?!h(rQF̐)CدT^?\ѫ@ųR7Oz}>LjJ{iJ;Z+@}<9XmobjrYvqv.nڱy|az~L Y*#}AQ}ba_3l{cǺY:,{nw5t{ƶL`r, Q/YeX2o}jU0zE@FC:V-~?Mxd׭r÷q;/anx?NDp ?dv!{ԆmC=mPUŲjJbe(opV?tQXio_l84@Sf1˳Q#ӛX?VsYX<{ی]9bI%$زnBHqmNo-oCO?/ڄQ@܎ A/W[CDIpm}?}T +.3G}+tysF1 D9":{^9eu7J<7F^/к{qs{zPT~Zq4~iM1lk\;.J4;AiK5)iM1FK7zda|ZݴdԠLshIs&ܽ|+߾r'dis3?;}i,(ADdIR"b}Xc0A}jv>Qa]&=H+%Orz6&5~>/7R000G#D]Ŝe//. [q]L%Ϊ3EXhG؝;L=Huo]涪L)H-jI` c¸5 b$ ([][\v~=c@^]zRl|ǂ1މV6WGmdV@$ ZP% 9)0H^#v/=*=tf/ִ(uq򋠴֯\ͼ;hs.S=söWmm<^wZ$l +/6S45^vbﻍ# 6FB#c"}QF˕;:屿"5٫%8OARV.+>,sL.CDz6+uO2/H S-02M0MWIR_߻!W }}Pu>f=#-i3#zuͰC,PۗH[uꁖfRF;\(C8l._"\ԝ')bmnnv!>Y?*Ɛ'{zYg J^3_#o }{s3t=nu&h6h٬>][q'!A<̪&.<,T[C0^Խ2254hf~)`̜Gp9~r0#ȼVoОp.Km2[~5lzye& /8:S2!nљuֻ/a@@x438oMcwO oYɣ{J_˧`{Vc[G.9Ę^Z}no<|ɻp&9.Mrv$0Gek0\!oG.~^>(e2ڞhkfϊ3^u)3dQrrf>'±l&Xᵛ>؋`؆V.m (/M/^==x]mzvgĆy7תjl]ze1g?௿0Kk㷤фk7J{mVV-1',엃.>GYQ^ͩhTZ{Oh]~ƭ:^4I<46q/~}`'YjkҤ6%M{Xs[\>P%&ej{r{Bihv.z,\7hī>,T&pk\;UdP:(Y_Xv"vxX5wĆ{y e>}3#ݞi<~MNfPm5$y7 u~U{D5/}O.n*Zr.m40XOQо¡08tPu~oHMrjp睏Ecvk\T0oT:A^ay~@9]p??~J=FYbqE 3Bv6PjπF9C$g2r֒t3{o+t'4}(J3|VE#c{xW_v]~Оj;śzo E~@,%knNG&'ce~Ffz!SpAmR waR _SN3A + a@UH>#2~?,EcbTawO6]el!DIvLM!c5jsGΰTXHLfGsKFB5c~6!nu0}ţHdJQr[G +PWji?k4G\[44.쇐G1QP;YQk6rb׮ v57z[[x]0+P-mۢY0=4!;i~S[ddCN|L35L~9=c34zR, +mdԫUp="ޡͳ|VPP+NQĴV~#L zp%A]bLmhZ*- .-@4ʍO-WĂl`E: kiκ ,>4x6l^8J']Υ)K4]7k)@wʗJ,|L'#qac6Wo+Ocr_qD?"IoyXšbY_ l_who9}=ӻ3yz{R{&wK9ϗNOQ=cOn3ߜY%E$$W+|iNqӦ好 z F 6Pg##朾C =wߊ\ycwr[qڥ Z>؟l߸׎mYSflKdsF "F/uT)p_{+eO %7i`_Dsip8ק?6|TRiw?r+8jpr7|A5ov V5Rp|M ҜdטDzݶ +BC_Z++ەNbiMBaͥJq Uwsmo&Gϛ0˕es uCZNxO-c>FudRkB>RT\i [2#%/_ ;#뻙xJBu*kbOѥc!]^ٜבּPMQ=Ƥ}30ՑgQ][f=S.k2>qJkZ?7w+5/Q48^7T///2.Y7ô +inM|z[;3SbaHGKUNO],Pī#XUt?v$tQZWUA%-I//EK +GZnfa㞔1d̾Ū0MªKתaD=)F~ȷ%]˂JIzL4š(RH;.!|N! ,qթ}|AFZh0Uhܜq?G̙ypߡfÄyYwPzgT%oF&Mn^b +1 : ;眕.q%k{*9klտm3N EZXLIfɳ;0c)N qQ8W@ L{Yl.a\J怜"`n/9_S|NMU?;J&xlZmNgk;oƿ +Cs~?H [k/bS0aӎWPB#Lxƅ N{Sз糐;Y9Ⱥbka#sY"{:_ FzۓM}sHragqpNt3[KmDe!|~"*`#|:$+#rw*}1a-#7:s=LbTdMj rHH|A4}Y/Rڹe J{xht;kn,l6:T94n*T?0}VvF"}Y9ӕ|c{[ %E06i};bn]#bZXP© 2בះn 8vTh ߫ԨbJep{u O B)\&Y@JΊC׃Ck3vVY}\-4$D}&FV@EDqiGG oDO3 &hYך^ S*vV_L!hMQJk%AW6Vi=e܁1Hld6/oQaU}:~L+u)>FckȢF#s!si +m^>->z=y**$Y8;::Ped7ϝwx룐V)ެhxs, )%ZA#Lh/J֢'*6ΘuS><t'T OrΎ +i-N +Hv +|.K'K:Tn+-[^4r3?,fNʼyUp= 8I网Q7/j'ōM%L*1ףbD=l6MO +<ۧsӱ냓/5'p1>Ν f A.@>O\y";x% +ƶtM{梃]f57GDža[?61l4x8:|i(fذqŮ_9&zS. :Y{h>k~\h{b yFnjt%W{# f!꿰ԆCP]X5yPMy&):c\hx:GThutٶk]D.{jM*uVfaiëA]~J\@=(Tֈ +w%-#qQnXu7t"ȸ1N6߇ s<y]zl# ۰ٸD^z ՄR6z ?`E}UE=M +ҽfO< +2F7mDƒ8w?;#\`1wEh/)IBPNw%-{I$1$Ie9Ip$:?<I$$Er)i&e笓Jt7nC3LjxHR,&Xz9#j%)f`h2ƫE[[35cqݞ_dqЌ8ٙ'o&GITOgu')͍KV;re2|7ɼl&9oY< 8<]cwH<s.}`Iʡ3V~#9Gt(d /8x)#&;K>.'=1cA=^aX7W ,hDq'q+G0=oZov&׃Bd֮ծ(VI>>C;wd\Lx>pvɲW^%cհ=2h +}E6{Xzgǰ*OCYozQnUrEv$Q:_O)|+*{78<$bl47?Ҝ/͈fi߾~Y9WQ^ew]0i-(.)ZϋP`Y"5W~UwGm7dr`$8(9o7 +ޫ^ىVsw?x({im +3\)vw\?)>YS5|V %lD*"DIs3IM4sI H uCm>qt'GL]-O>>{|2u.vP~ڻolse Ĕڄ:` |NVZJ]U=\_YqYjZh)i6e~#7kxx]4|DkQܹ5}zF5d1njgل,fdp.dW4eYiΪkqRA$4R!Z.BKi;iiYB7߯(|_U/gus;q;k(9)C"AC_:f +˧Vq9W4)gP+wv[bBb%F! +/.V^4) 5潄^sC*zN;K9ՇFn]I~*rZ۫qޫWW))O#3AYdW&L7) ^v"(w|c^" +#;;Kp=mUhi4Ӱ6ԇ ׷XWDr͒|EI]o<ƛ{jPw#LSUsI_0& )% ĝEK>nI`'#_|>mhYA6ɕ\zį +Nzx5+Ȼ1"3#"3xUAfs^4C<(Wu h֧bhVS?,ǿ!8[krZnaɓ^wQuxR&q(C3C9/cReQdvq 8r@*>3hCTQvKY\unU>ſFhd3&W')u5I8ܢy-՟FKcԁR?hvB#$/^Ӂ7Km^vd-1U*[%#OZ%S(2ӪˑNQ0zÖe|Ã?.n;|k\y?JX.Bi%Wcy:O5 +6l t,o=ϣsLsu^0"0/m]w"u vGwrhˬVn4:l7ĊeI4CUeY[[r.ιp)7m`:‡t,)9 )IB LQ##ULU%[͂)￐םs\A߄1 +D trܬ\[ͥ~}| 3fF0~ mBU'akx"пFrקR?H˂0_{FVmW6-B{ݻPfE"/7OlhBoFeEd>He0TIfptYLya޿9E-2i,Mx*I)Ce\$Yi9QKdk~c$:"ϊ\#ol?`M,(Xs,f,fs!Ȃ Z` [eK[^ B 2>d0~0}}6k~Jʱ7;zk$ھ,qڣe` z+ܿ$C~?(d(6nu,P/+YNI'LtѼ"zqoxD|ɑ3i!HRE|TFQ%?qz`dNJW?+~袒z:s`Cd !``ݐά_KVbϻvpptCߥK}wp_0 ruy۷϶tEðd +KB=47GvtxY^\l6Y/c<3-j'M}GK9S|@05\mjNY֎Bs8FdoH"(>|^2ŸӜ=&]NuҁsPvv||4`4֙F9UϚG#Mg붣?;W͇m YkvzE53Xr4~Ϥq)74 g +<%kXMQލ ŕƔ ܩZQ}u?Պ_t;BdjAR<.R[}w7~aݹfLf7SҼ?wWR6?l{Hcq7Wj"#Һ6 +MqR7D*[ +nҠ'"6;䥞iaM{kl0fw[kYŵ#hW-@/zl: +QM!l6ĤaHf'J⊣q.#L6Fo3r/rs>N>D,JHs'}lvYWp3ҙ a 9j +g4^kВY-jC1:(|ҵ823/L ׻ݍA! Tgkuʮs~Wom͒]-yF_;Os,%w g0Bmr:4Ѐ5TCaLRΠI\Vi.~]d!r9aî2Vۦ7kWKgtK'f\_գy8 o_]m»w7mn>#rҢLAyϮQڐ^kl;^F % +8] zG=<ώ@=Rz[D构1?gn?i^ZaV|N-!lD/5SC^+')]q{_|M6 .[=cPe 7"]rVDyq3ZFFb|!}Ϥ΄N,=` aELccKD6KRSq,qKGK _ȑ{Hݩ38<2}qCpg|cd|Z43qs):nķhآk[:ےVy⟇Ӛd/mp=qHbڡ?[ʻ|$$ ;n.ki+ͮ=Ej4IyMwW_LaK%gn{Ey,a +jPj{9Exs:%ї/^@(l0#4tGb>wiv_1(>=!z^%f|3tHl.sYoˮbݜs wkfTPL x~rg;M²ʱ<gtpB? 5 M- $x +7OSRJgmԆiBLzMVIZ=+r_E]!<܁, 8S]SIcBzxo_"C5n +mr~@~l6,oZUsp_f]}nr1VPK ɭG2@:!pwZ~_a;̞d/W8NW£vjsSD/YGԸQTnhlC1ޙUg[(ctsWVYj#r8B4_ bKHcz\8#X%֢E +y( /Dz+dܠ8)lLR#¨y~|A;w\A^ɓ&. sX/h5^InlurlyҋٞflL&QD?_JDN<:%=:E^ # +T 2+'OJ\—=#K&zxtA9}?$+Sm@Bbf,d8>lɶ}~O?oP 62J,e4e^hEj|~'~u~R#K#hAGQ Q'${ݓ*ZZt}( }GJӯ(sGQQ~O.ǽb¹[ ˵[W][>y}xlu(Pf2bmY8dX2 ;R3 +LcT pQQ+˾VWUyo9FKw)f1BwmMEN!@/eϰGZbrɷ` 38?_f ɩOfv5體ˑ, -Mld}Y~iYSa>ywPttS81Nk\5^)ƽA_E&}̸SM_YHN_k'8}u^g<V*_߼&Rl~z4 9^EoOHy[I +#8p'NH7IBK`Ymy}')VKistcŰ}cU_7N_2ohIcয়|M~i-GP'LÄz1%v^Joe/CZLtNE>\&>n &TX%jR~v9]&OsAEO3N?ϫeGZ|ps1DMTl5U>/ +Ȣovd |sfK@NP?ӕsvʋji/hFL9lh.YqΗ] -?|n8-֝ et} &Sk = s&z?F~EQNdwzѯZ4{xMQ/pJPT.a#̓ +ž7y͕8=^;N|1ޙmʷj]T_A?+_n&LآQٷ^]ںZլ ԆYW`Wdkd{=5pʭZy,zQ;>=}֣uZ/(jmajcF!zȾytkϿ@:]WSYZaOrJ:_ ~~3^cɒar,wn >;<9 (RicXX{AbU/ZY?] XYQZU+Q:!E!%VsTBQ ? +mAT v$đnIm-٪~wMYHCGxiRXv!PUM+ߔvF#?N,Qkz UyTSz\{9 R㻕ѫjLފȠŃ|~1ި%@ b8{V__T Šg,lψ=?kr|`#@y8C;aMmsFۄށXIT<8'y"{%]>w| +Յ^;3_"?&*ƒJHuT6~JEvpޙv| czSPmUY3Ħt ]bDk{!|ۏh\t">`ݷ؇/0ni$Y){%mj&ұczSYk׷rt7yf" U@ky![]*pksb~3[`49\Ԥ<:>ڌn=h`Me yuyli~p~6(iլ +܂cOž1je] SBᙣd qLpbxݲMq(s18>(l.<!df%d'=2ߦMӃ%':͍K9 {8Hh۲^ z6d5}|w չw#[^G(Wn?tS˷ BTVGKMU/7Ek]%:ytZsSFH:j'OE{r}$K56KkdߠA6.) + yўM۝0`pTt>b)kSb{ ”ܯ{R.L!qV-g ivpeE?1&Y3E5בWOפ.L*ה̈́a aQ|x!f惆`;ĉ(2xǯ"HӰ DCݦ CM)ɐlby:}gMP.JTz>#G04օ%BN=pk>gM0g0k9n + ڽ{WwG] |EMӔ2r32?7Yd;|h$}뼖}?t\^ٮ&ؓ@h.}gb Wp(p }_pi6pfbFXWj{ZB[U\ +v i'*48~sxbѪ!% @&@<cS;@&m}_hG +|k`rMD#hB-ηڭ+rZ +IЙbyyYoOm۟3_iH՘?%Gl1JhRt'v )]SBm/N?%bWIm3/G 'UbdgxlZOVB]K Bb'[ }* U4zƃ9ى(*#u^1CnrrWݫ] ]=q6։ wT? =F?7uy~!R{u(M W)ˑK˺b<`9'iZ-_mKG# u̳'򎲫ݏ/eIj ϴ%vZwUJz~*eXc<><19{BH6&y}b'iZ]qwlwgFY"&re1gVgy1C2}sprċ3OhtktC<'l}trށhNޘ-K/cz畏> +acݰT X$TnM9]H㴞F'K6}wZi#qYOxسTO>>Kܓύnr)mR^vih0\= +h/ʼ x|/7o.CF{wz~FR%T;c՟ WF'lkчu'_%E>Db,F+Wb"i_&l=BpҢ=|ӪnhQdLBb8>p4?H_LTl|&ϤL==rImS|ZV 9 mTE|KJ>/{W}7 2G:KXw\|V0Wש]Hf* a* y; aV ,V{#W_r~$mJIB8 i=c4<_;RtϢљMN*:w#nSөܶ"wѫTydH߆ +Y~ 8Zp7qXIr]ԷOc<._;,Iɬ7Go SzNve{ e]5i`eݘ㖑=$-t6&L̹=7n.fA, via +lMNt4DY:-ObMxcΔx*w󡏑o<5qzǘ6+c5n?6 eO4OIY>cJ֊t!Y2>$N U)2]|C^vՅ2owLq9<8svhg,|*oȫ,$z1/Z$^Z\ȫWUFtȈ"{F>>̀t' Hq; ExƲ:}&\Af*Unڳn&1/!yx]Igq8'xUɡ//g!˭h&ݰޛk<֖OaCǍ1_`®z99`!o8לplL?5}4>Ql|tԧ'q3_M" ",>Ä(Pæ%cgn;֎[BR6n@?ɾ'@OZ~Okr]Aх@#/pYү=*OW>\X7dXH;c*U`44\t !h딻و2wٙ~NѪ]G-Zzr X_M0 =?Ez/;}"CiK UV`ӭGꑻ&%7dr V6.eb/G bc{eN'K5s9vǓ^찤vsS8dJ2Uãfʊz4:rId j u^.fK)>+ C?' }^1}n{,\4&c^xj*bkMsN$;lƻVå$.@7侹}-V&4iݭfsN*`wcptˆ+%dH}8XYRIy.zQ٥r<NXe_Ǥ\р6}%F-J; +P v);Y,& quou;Cqz +:WfIޮFw\jD/8DGg;c#o(j=^,/..iNY7v~ޅ+%nҷSN|G$ۓGz4,'Ї8 +Ȯhx2V$szbTsRq +@NZ٤@7 @Щ(Bx@nz7cn"ϢUG=Irr}:huRy@\۹r7U~6.<@p0cl] _Gu[/h~ϟ3 G_{zfayAOxeQ{E[nuտM]発{ o,}Nh G <}YOed'V.b||U\ݞ_?z+5F6{0[ɿWTisr +d4-ici#Ӻߒ$ 聹 +~wuy[V&ͅ ?蹗\Hn>=^Slh F/iBO^:y0Q= <_=tglp(P4.~0GFHl̪_E6g(U݈lЕ0 rbXq'`ogGk_Bn ֡_2Y2rO>}/G%>ameo5]0;Mtج|vN=|0pmuS ԋ-N&hwsv?&z: ZDz}L)OW^Tě 4 ('s[&z{߈ 伜tS9¶!u9)9. 7} +E}ч7jⶩVoXڑ4KVV( ,JJu auB+ræ5o?#ݟ +k[ޡS 4ʖUA&v} :͊S-*6)NVדZcKOEJj +.  xkF a6`~[cqvro{;ԇds/*݌tھہWP|[WD+o˲7!"M@Nbq#(w]2G&h㞈y}x`s|eZ!9f!8FxL^r)cr=P[ndnh|N[͈nbUvVZ%/D>HfKĻ2ދ[ +,C; +0e+`{b Q6?nfo cٍ[Oϡ9X˷Z7>5 (ɤxV YN7vnrH;~S1 Fw]dLX}b/zA0ZB@"p ܧ8*J~ )krP+Nw\xa@ Q8^}o ۆ]dk{8*Úґ>ڣ{J, ]|@Mƥ<ӆ`J/sEى7_`X^4}aP ue Hzm^As>XYIԚ̑$= ؒ JD9h$>+/T +![/nV)UK 1`p HS5iÏ| >^ 7uS=AƦ'&^!uu";+q^}yJz%>e#\&Dg#Wh`Ը0 8"#Es'IJj72YPmP|sQ=!Kچ67%S=x)U99*m*@O|LD޴වӥ^x6J^4(56zz>f,<[&ی&"Zmr\(!z7 3usIozSLi4vtZ?Ųf>ausOa9*mPͰ-PUkd?a~ʘe;?%s8Jpe5GǭQP!G:\o}ʞ|gg\F //Llmm%=x!IP"wG@ ]+LzƋ uif]<{kL,Nl1hqroqD01a6ӂ#@ 2sֹq ]5lXd|?M>u*lɿ2ye$Ϯyf9C`%*Q1;yv.k+L_lC^> XTO su\uA7 |*MJ]|`6heZh-tX&?zoa! ,umq&_|%t$?l3Oj.,8Wt}GmK{ta_t;Sx.^POsL%xt$G?%Sw Aƿ:6S-=~FZGQ? +UJOq9h,@ + 4-a&GI uRf, ܶJ%֪:<rv1~^PY?)t_еe X#(Ɓ9mk,jnx^O=,.=ua8w5-rwOǻ軲g%__Wҝ=6f]lm֍]yvo ~JȤm#4,2岾\V<:ZCxx{y[O}./f?ΆY:ˇ Ɠ/ks~J>\7ElRkE]nŰ$%e=!ߪu)!#~wL4'yp'ęOuw_9eWNiYցsR5z~kL[OfQw9ڰfjEۊޟ(Gp)_R,,x~~8 ZPsV$a{`/imZ^߷+WZܛVs}73o@P5/P wG-> +ǶHSm^^qX,e|yhƬg#˻9٥0m\ B>/мUi]f4cVHlGVz촥 40 ~ʘPxȻD-Yt*/~n#d;C:.{Wئ[P#թNe4Lq͘yiqrZ3lY+yU+.Ps*s"T˚)eLuIZKFO?/ì O*m^ϟ{Wk8L+}y/LP9ęxúVFشFU lʼnҐ[|hܞ3t 6U:DU~ 9_ZsAp W?l(ok0.9kv8̆sCk󏯯f=BYƻ'TC.ky6s>ے\R]ٶu"a{!Nx?*ƜgξQd*j٦},?Hg ͭwƷF@ؑ̅GGGڜNË\͕ؖakn-2U})mLx c5 6Q߷[`TըLV_=oB<#yhɻIHCG涚xVf{T2&zpz9>S~Ve-\ #V5s#l5[]o-v\ANKjUUASJDlj%ETk"2_BGe 7Bu#l5QPFG~sFž:fBN8쩓#u&X} 5B_՚lERW2+Xe+.4P| [^zjj~ +N߿@JFg7WuݬGoײ}oηCJT6Ce.*2EEl%вV:`d ++?P\#54|盝2͝ǵ-ײ GeWźBzVuij|OR_U{.0i]@/Cj#( +֏dwoĖeR Yξa-t@gveo< "tK-g,?g?2?KX{6M% ^*qb6zmq4d22#M›͑ⳏQj%B~y +WU!&kp-l0U-i|L"VMGi.s:WxWVw"_kf_ٍU2X$BHN< d-Z@"Yd͊;Jeݔ`]"&BxT^l.62Z-1 ~n7h@!KPJoGyg@=J x+>H2_{C?|6@ ~?qm]?r!߰\۰<(|m#`?OHh6*ߝO@@G,DYxHH 2 D4N1:`UHb_%{_;ӯjTm@e1#.in}X0x{+~s/ˎ 2Pu#/ܷ;Bl;,{ȫL>mf[)̰5YpR.;f^'&Gt[Dz6NO7s;>(*CC;|hlPBn_td}ȋmy8p{u 'iйç@>{JmLb$ %wcl8 +6,qjWXזlRa@Ŀ/;a]ӏI7sW4#GvƊOB>ut}6݂k|>j_4ޫv,ou/d^y1[ӞͦB9](F<-ڴx"kW yHzn u5Oymd-o-\<]g!vyLGlȘq9vvZ=^&.ywV5?F6"6?J7wD]0b!` v/|)#O}{>gymh1@ x><35?0Y+:g93FVRc|gD6j Gԩ1Fdg=xvǧy;8eoy<ơttG:06ww('ug;a#GIiЗ?כ*Tsf?ZiTZMv?Pׅή!;>Ƕj˼P8g-ޚSm_&Ѱwé_*/a&Ǎz؛~1ox?VP)2<<5"X/ڶiũW5'"S2F97 Ea3AG~=poYd p.&{%tO^N8E SBx4Zs]gEc1rIHkwiU(\Z uo%Xa^D߫wӎ4~^Gz}[|rY8u9B3ѓYq|`N\i~r‡jÌ$Y]L1özy' +~L3cߋw%yzb zV|nMg{ fv<\X!VLj > ׬?jI9ܙϱ1fKƸozlfن>z0 >r^={+\:/ a9@[v.LF#0'ҟ6 UoX;Y iľv=5 Iav7= 9vVݾ' -BcdoCpNO;uwQs"j͖Q@ un!,[ +h_ Ou̧9&cNV4-$M*JV"\L I`߃[ +c Yj-̉D?d:%0~`߻{/&Q#އzY(S^"gBw…nkaOkS$o`""~qtZ7}=ߡd{5%51Ȕ#[wZ2Tzi\U2F~"( ({9uoTZJf̤֓+حl.af_ST +L"}ZiD JsMsҐ`Zy~=N2Z̀0\a5d Ҵ XR`9)N&~~rAP0,xV4qM]Qd[J:Cyw[\Ff+x>BG& kSN7^ +,b_L1 pq$b)~}S +-i_mUl4kUcKS Vu:zB ds}0^|<=~;fQ*ݶS  Ӵ! }āq|?;@G[@$W +ԌA@]g`'YG-u&YX TR%" ydQ䀡ST@.^u@Pd(޼StE*`<@ s7V1uKm1VlAֳdB^2- kզ㖏;V͏K(wͿz2?OWs +z\@7 +`l[LL<K +ZU8?Eedr|foO6ELGy&ep?q': +~y)Q[r Դ|LO`=_S)68ᏨSqD4FmR9np[nퟳ5B1svt +D65m8oWVJӲvߗ!fc,Ŋ'u} +_=(k]H\\%Y?'#흩UsW(ᷝDߗ?>u }Zn [~\epQI3Wω27ܡb7viZo`]}S<S+&k]&r}ٌXk?'o)uD掃"[k[u|ltzEDqQugo^7w&~5#WVA_G #j\<{9a FË_o%9S>X}d7mݛ`Z <*c`qPQ ̨h0"c^Z`1jca>gì޻^~.=#4*wu‡quvAyA.}T77?*M֦[8Kv9lKhMKRCIՈߎW/^!JF `Ғk[ѻn%wvޠAa; Vn-($+!^@̿G_|#w?Йy͎SKĩmQ,a.Ӂκ}{:X.uuK[gȴ+L'5զ1EJ +F +e3¨CSirY(5 +9m;ˇV7⯨b5/d3a4K'{S>-7.?ovѣ>ѵYV-{|.*T F4'!4wGo̝(s=1wwkm7[m%KGF7_egjY{e + hY<Lu0[ֹ>(k@n4k\ڮJ76¡kw)v׏Ы&rۻҥDj~GcݥomrwOWe@)hbthvZw +i +4coίCN~u2?p;LMsxO~ {x vF,,uB_-ȻƊ}F%VIБ[ogr?|ygWsZ'dlGkX(@NfaXR 3|ol<̚L-pݺW/sv#tw +8BYYeٙDŽ4ZyV7͎5xnVÊx5I_Ca}/jRW_Ej?A`vnZ"7ɢڨVկu~"t'ŎlgtfMW~sDʦUP4å7kj'j/.+8)l(7ϥeq' Ms"F(YajN<ݣczMࡥ+*T{/j1γiIMG{ESR/NR;Ե)6Qwx +TRƦIj鶑O"٭/ڝ|>GB8zzK0Vd=[57AUV>Z4Mv43g|[IDzit Fq^HOBydA}͔n gHdգhXTЎM>Mg_㟳. c EƃX7ҍ +@Św\:flOIn+y!Yj+U")&K'MlXM;VCh/8R?o${ܫ֒_Q6 7F+tĄ@,UJW3j:tlHUZj cϕ,&W4YE)O8ČXbv,J+kG wDf8d[<f[bС8}L6ؖGSʚM_5"u*z3ϗ5 +8yY#(4AdyO)$08 ޖc{p#eۂdn>pz@? s+i?-Ou}gGDaޗ:34%{RjS}&kT(43QlP,}йŭPZg~o?C"9O%PƶgUcD]~Ȝ{XR:ER ?`f>`$.nh5G!aa޲lOkIVUqbW-5F<m\/8bڱ~1=΀իCiC`W(U,iP•XFac:a1a} Jst.^\hX!ɲl VoZ?GY[A"ߨt)=(iO XܯL5uL%=\t(sP[)BʼnU'oeׂ k\2au5zk4j/~) @ߞ-wA$ղVI֏+Q:Sժ,TOT , +Ï)Jg:~&!9WiVx?;),v` NjlJ&J.V{l[caiȟ0B-Ώ|p E:ئ~ZVe~ 97Tp3|d(dr™,+ o6vJMyc#4ѵm+fOHZU=jׄUci.WZ+SY1m1x$Ss]s%zvOG TPjV;] Y74_quendp$GMxx٢|*=_۵CK_%kg*Z8` [?2 {P)_yA=G}dͧs[Tug%p<ݴx{|ZS|po{-;󎾶;-. ]ЧF-9uƌp|~5wg^yE%{Fz?ac9gL{޶Ӆx;NKm`ԯ:Z$6YIFO6x&}>`dڅdݔa4];-Ř8qs5e3jYU`^ۨ#ɏ쵱 <2}% 6S{ҏnYO_^+sO_ oObP} gSj2tOcdѽQ?_<><mD?l?;7jOh|kv|tl[o[Ԩgۨ1Qt3}YN'q+3?FވC\[y֗_uo*Uz|][w7!]x;~tܠx.6=[⇑PJs@شRi]n7QBw;dW~%yj>DȃprlNN|pt!9G:k&u}[]C=iz]h~&Z]^~~#tn<,j֑W=Qף{^*Q7Q2* M;b-X6~u0b~[XJ͸}ZJkk}(us7[{=19fݻ fF?JP}IEtuP][I3sC6Ǧ7s0뎿?#2ϼmxpޭWo|_]HέT{̌,9i~ sKf~׌t ]O=ys('>Z]Vhw>+ -WSݑFT}X ϻw~:~^ݶ\8Q>:!JxbiGʜajZgHQEA4 }T;_A=qTs>8ZJ iv*AAsE;x6J=Z=~YUup=ԃ-ө5W*%{s\L/=NsK^ HMdnTr>(WTTʏ֥-WO×苃wsщWGl}2P5ܨ{AON^z=n МKb,nED[}%:R>>õp-N˧.?Y/ ^3YiJײαۑzvMʡ4sƊ^3wϻWY8GwΚhnhe8WS!#tEn}&uRmp{Q y(-@6zk',0$۴L[zAm#s޾vJ5GZSO-gPK<COMeKmǯrDJ~^| -)aK KŲl%|uk".*v=L* @^eݮ UiZ&qά`g"0c.^hukv)]Q7y"Y*nbiJ8΢a2rBan.&[-6,i4?Lg-?ӉMz^`vۍ!t1!.i;bF?rrLiRsUDjԤvm8o+kZZ0k*bR(V(svAx‚Q=-kmu8YjMTVA>Dɟ+BNUL NX_׃ ".8+quqdܞNNoTun5QI%ioY<|к NGH皟ʶ7D&kϵuϽ;|F- Yz-_,^U0g>z*g4P4,,4?1%ñ^}Lj?#׌^v`%y>9=иr0Et[NO)ӵF!sI,RB.AIq44 'dkZO_U׸nԸdתJV-`vCBEկJ5|GR }r^/Ԙ@Yb9WJx+px51Pl &n%19|ֵ'5~f#hQfV2G*ˊq|+Ezf0TTܴ5|7+HM*1%Uܻ, w5_L])ODE8ftA[x1\X*6^{lߘ)?P}XU:̽Ѕ5T+w[)Q{ +;D9q\kR]l$_<,^0x6Y>n|'u4-RAsءOeJΠa?熻ZW\W6t,4P)-ZښerJn(!O+)7)Cv$t 8y"Z촛7)JJ=MP:< +XM'Z + \\: +vn9zC^dݰf:՞MEWҾDsez^=Аpn,M\nrF>M6̔T_'_-6ςDNKc6ZZ'(UIX75%L*j VHa8?Ao`*7RgGL kd4k<lcK [!< &" ցMKsm*~+ůA-eSdǟhUTahC;QC,ۈX[ؐ 2 E.ޤ0WL134 9Rx9!@.d~4X> i42f6YG~b*GɼRz)ITG"@~إp BV}"o,#$9 0V0^z faF>`0LO K;f4BJ~y)|jBwRbӬ};LfAupzwRq$xr8F + + c Y1.U@2"bqŊC37ܲVbO~b|FuI0J}E5E$ sG)sHrOP`<-QDFV$9!!X_iv{oWea.r?? x/D&4&Sl^' ャ{::8? 9Xʿ̵_Wwzy3Dž3{p)Ą\U*CRтgװaϠT>KGͼGDԔDb!_jlkbM_Q e!~iǘLӜ:Лh4:;h3Ocm04~X^tŒEGuAKf+ps ۝yӺ!{N5/R/Ƿ,N_{9A9Z6S7* zt?G[((6Ipi؋un|"m:x07}.Vn|xam~rSɢvZ6^})[rvv6l +vHIvmL'?OK ~ы3Zr>ٻ9mlҧdrfL9b&4G6`L G ճn搸&c:~w<>gwhsPnHrBb;;BRټ2ChL熘쯋?p3Z +M)P;Mب#{uȻbG0m[F^syw#w!˿Hɲ|uS.3H7R> M `86GW-V[yҫn*᫡e智YnI.r~5(Wmo5KۉW2K%,QNpmTuCeSs[OI{ +^VI 6H'gTFFkNN@msʁCC/F^Aѵptz\5n~7!i=62$YɪXdv= s׼/Ă7v1D]V>B:℔KIfk0CVguJ BjO6bU(0umFy(g_Vj +OI΢$gEg]h0GΌǷ8{+~ڜ8K}aGY_*).fI!PB. +9+f*׷q1b.P QxlS!gx!;8,i9F)Ǔ'Ŋ,:>NaCvs6_p)z[Y^#T!B;]tm/h&]Ƹkz?݂-c˕c000o^湑J\/He)Q9n (e=TT:6^Q~yZ0؆Ym; M Iln0d*V:yH 7~ Zg2ft}>ŮJT?܍"a Ky:oF~PaT̔^傱Lk}:T`qOs!!V6Q,-:4K PTSr,:06ߖՋuc]}We%[ZuɓTmW,4B>7XDyƛ=S['o +p_% X֗72l/y4"5Uu7GCHC\ ]-vʣo] ٜA 3){\յ̎UBi%ÉSMKO'$a +R;^sgۈ璓7<`RDzYtAtң̖JRE7RNN [ +*kJeeH=4Va[6'+z'fdw:)ە @Hj<c*5wՄLT0xp5 Ӑ:9{E{}Kmst0v,ӟmMXQ<'4VXߢsqw Vd/J^WE?,hC^,eڳ 3t1K8/g>e +px jYV;{Qb'lˎx0(W4?=XT7Xi,88"cOXJ, +#@ ^bY?7VʀHyzI̷^*>ӷhYªWRl+*=YtUZv&6"<@jXV>- Ec1XU@f, +IJ ɻ7@ +.Ha.5kw33SM+ґ Y!yM] }F Vn y<ddr7瀂P<r<=MKz P@5dƀj/@uraP50oM"+TZԉfIh'Qf|<^ _j} 6ob hs%= +eﻲE@6?+ ^IX` 0<=ً弰=ЛK|ȒN.yٝ(.k)=%O5KtN|IvܮPH3YPEPPLpFlA6H"~QXΩ6ᠬm^8k|*(fD1x{WD iī}WLgfRlyqpkqe?fR߮!-ǃ8h:Pĵ l$#<>#St3>O>:u si +trOVqr香M$^r#//:X)mx{XſYXI߅y@P;\bm\>U;֑3ԓl"!7\iy|:o,~\/ڍ-i,@V#OM=|jh6W_8OnFͦbǠ6?pWqˉeY;$!1)X7E%Tm|ZI@r3NM% -{MQŅ.vTVzȫs<~;enӱe Yrh>m”d|W9KO7ȤFL=f]ش;do'fZYe Dԑ:ămFs +U=זb˪|r3&nUP 8 T\K4;64t3.WcYeP{.\+[rymѻȕQPe˕_Z C{3`$ô{eTnSsvG+-w(SK5;) 3Ϝ^JN1e0* #]e"\CPY-Xhk1s襔bDcYGeLYPw9;,̱MP]FƬǫWݸ֚Cfj]>EU UP.ĘUXܥRq<ğ D9sIqE3ݞn4^~J^+\Odl h3gR)ˑe$0w~ZAF+sm sJyݭAیw U0ՖBM+w(j Y^yiR2hW#N1HPͶn $ia+SIՠ;,YV!8z5"T!fz +{ˌ/Lw҄!7J!swOxHׇr5Qev{lڦ6rҮBov3`{=2x/+W|^isyR2\ +aYA VANexYJ<{ Ze뇾]jF*NaT˾)bD"̞z= )uY +Ld_RaYP{lfp}Qc&ƛO_zcYo4UR"àII"R+is<~`mNYW3+Ck(n_ĞbO/T'Rgrw a_"]HRQ4ʅ ݲXGxHiS =(OE<Јn R~]sOr#.- wJƃoX#C}uT|ܔΜIfy=O{W)ءcYp>Uq*¹R,oc ujl[m$dqmJPvc& xv;F=_'OjH֣z~O\Y] g#(].[b_M +fVlؐ7ZPV}d[uq 6A< +:4Iyr:y󆂠y.Z7ihA镐ᰔThkqg|` 9xS-IR|&I{N5<m^čx >`.UA͑G/o¹ #Xsu\fo9r8֓ι ӣh&0hV /sxP~=M6~.N>AL9g!',kP~r^0Ht;.BZjvP |gsKhRiyi ~_EO]v[ᓴ]. &_s( ۼOoR;j[:CzlKųe}A2%2M_˥MN35G@SJ .ʮ$B-YҤzbpeT|?܅? zݼ1}:0]o:!ܰ2=vNO j!B:5T.'D,tW|(C ХJt4m V3 ty$=g)OUٸTcق1UUkzk"D0ezF!u>0<t!<X{H^J X#M#$˻ +0=X?zYf8zL[̓5RflE SX_+p)?T Սqj,uNO@^Mo kk./hlLQ6..M%V[Y=H{V]oc+pۄ&4q֧/̰`x:B*(0: +:`otnP:#:ID74if:@tIkdﺾ%Vn%iꖄ& "PnX}t5X`W(~Zܨk>'Ii`zw':uzm߮innOu2(^^Je2ʝAO,^hÓW7GôcM)}h 6# Hu2r+ +Hr߀1閕&$o2ex5}`{%o ]!ރ:!/W- 98ULb~n:czުMT>V4#f}2V;R}om֫b=Ԩ=7c?X_;Dx:WA#<zWN=R[nlTE-V^\"cW57 %w}_4~FE:~Z*XE%pknEpmWh&GX֟8;]1+1Š +F3՝.g/-7 ?NΏ,)̔mUa~ѳvYX<1iUuwjcU+'J)[+W3ggLqeVQ7e:Ejz"G'˻mtFˉX钇RC#gsGЎ\Ɔz:R- [E[%neP^Jz9TO㲎ЦAK`sEӼBu>_y['Ba:_:+M%R㱫_&}[Rrh~e[hExeQFC] sZ7x1"iK\H4D^X!Ut]Ԓ&5{\CVcq8^CB8 +p~`4<éQ>w*Q=嚡j^*ҭs6y\N宕pʠ!ח|i&.FDjߝ1gK']%ZJS|7b4gz0h4W4߱Dw\'Moi˓E *p)DCVc\KP]^ʭx:6'^kzz3>VmY|\_Pa-#7 )Lq9('ٜyeNھ\^l*JOP)ExP.]k_\1=Gpq⛢T/总ަ36kvpYi>c(Cu6S)/dZn1SԕFcTK֊8ƅl۸A[B_ +!6Dm哎@Τ6 ta׻l6^3T'E%e/ {,_Ez2IBhDHxg(d6 ^onUU|䊎A";)Rk7qU +ot2$\ q]L%vw,~Qa-=@Bz'#ڹGjWR5wgy]V1,mtXhL:7!AUR2mꪗwJ7c}VVeSLZX@tt xŗ;, 6ڍ.E!j3HDw<s;.i9&NGas֧X7uC,OIl~,Te,J+jKZX>+))Cw90 endstream endobj 39 0 obj <>stream +oYL,u&Fke_.>]Cl$K- 7#okYZ6>tMZI@Vyœ{~M,0J'|bXr,}M gP`xE⫩2ED, n\ww])',yr__:㿙 SwdLXoPX5P-(]Qx&P9[raS3-1B{pss[Bm#no͂Jm6@x7@D2ʝ8) .6)/tɶM8j'_:Z!oO)<~ +*{ϸ8LkTQW;*^Ǡtn~4+ǛnkG~sz x>zF#/e}I,l&O +MR~f1δG׈aË{,i= ^#ڏ 5z[^>"/z:y7 7=ssiJZPl}^~j 1N߷^E.IQ. +9㼙,tBiT ÅӨCޑCnA 1g1Ŵ5a? {ـ#Ylpu5Nm<V\ٲu ށpGxOh繙T3sX sm|xl|Iݻ4\m!Q?j3mkOlicYzpڎ +cb9]w|Q4ZQ4ˢ_ ͔P06jyX\6ծszcM"=mmlBVZ>gp3[r= eجB*^諧q)ͣ"?FXX6XNdim7 R,yנٜAZ>esݫ1Vee:33PPml22fe· !O69H//uBeA; ]m&iVLU}eXYK#7^BӖ,]}]~Bĩ{$YS%jmkLPyݥoZHìgu7.#ӆ[Bt̝rscޖ#={MNZBfQU)/JTdqsX`ٛ_˦3{g:2Dz04re[xv˳lo2y{煲cy[ی:5X jP۲gnMvDTj+{+]|_[v@ƥ--$fm\x=ƳRjUZ'[]yVEV9[ow^$9/2mŹ=R5mx^͛ki;6cY5+XU#;5ۊɃ3S-(!i!f^JX5R}/ݷ&W%<{ +&+sF;ntOm0Ի'h)=t{7N>=};T4o=?qH ʃ823D2 +kND|7Z +v/PoF7_v?on1N m k:Ӟ|=kU&P$2dyux пDA(ms]wR7/s9]Xl3j,<uم1g+^r +.᷸1⹢1JN6уo7x9GA~_#*>6<<Z#|M%aPQX1ڄ:'NaIYfzt41̪B|_wǟu\gϓKUˈAD?ؖ3҅E:N@DW=Z[ +k1Q{hmء\j/G~GX0vYk,{ii\,aUj|S +W]eAl˖˴v4(zhMș5Xj}G\@8qXLK:ZϮ}kFРzA`Ifj CIP]$#7i7'7uqV,ϖ(ψeaG8V6hp.]2[iY&(e70,5*Ka i'KP*RMso$Μ\HKC0m;XgX<cٙe`R[Vl}i0USDN$ +eBO{iw +݆c}AHf0__`8 ز)2n Xg>aiMbI/$s0z{&FER6V({Q^? /@֠\%s%᜼91ðBgcv]ROf EŲ7q͖yud@ @Dʹt(":[5^W7pyAgJ^1B90"[GyFnu&Pi@m nK   ȫG:fyZsuAeB1ndi><4'/wœ(rR_- ,M,P۰4~Vjy 4{N)y@SO5x+ N>tN_9?''`;ͶwfCnɒl`́V?d`|V!0A{ L- o338oJHW{>dӳw}3jqeFԊxQCgKY)_~ԗstvՉ6$:W+q}{wP$59ӟ̤WZ_LPЅY{fmuŸ َOg?;vP~r?bn*6w[['[2OPæQ_DSJ̛c{9x[<&ք&8<턞=sxfZ.Zwusio'<ܽ3cOsznUq] a}4ջˁ~=^֋Bz#gfi\p9VZM/MW+e\3~,QHDsghL6Y4ylDާs=KIEcl{UmymqGz٬O荦t~WjHNLGl'\gt_ȰIA}>V>Nګ\aѦWu+dһ֚4΍ҟz!LTO2{ +-Rŧ_Hڏ0vFHnDj PEIy?8iͼֶ3-n4an(~?j/ja3 +;XYةLa]6'anF!nۻ"[t +Sܴe]=v }8M'rN?Ϡ{z2,w0W!~ZP|{*ϽT|R_H6b;1}SnTr%$?jsYs6 \6jQm_5J*:[Uh\N2)>oxEBb$ L}PȍM%FvZYt?Q\ 8Џ6Zwx|fI$$$4bi:)f%pBǚѥ07ʁY#uv0Ϩ߄"`3JKo;ū9c=OYW@չ-cPI`J>eK{kܽIğ|Y&<9 +_Zid$H赛޶q:;W{GnmN.U6SnYpM&408ħ뫱AIJ<ʼnH2U +sKh'7)SKՑ[{AeWsm/#ۦŹZk㭽9[gӗ/#j7axNi=ϊiGU=D5 NQ]QQ@:$N9qS'; ^r™8x{Go$Abf0mr _ ]a=ڗZvȰo[͉AW;m'ޮ?&ZGTBTkWMsU5oL$o4d4W(Ӽ=A#yCR}"UsR̰vH4ێm07]&;*ꥴv{UzT=ͥ*7G¿"܎_ˬT'=yW.P/GRQt_YaDEO]e!*YecYvt5ka1kGd-OSUrm`I^̈3,[z*,nK '_BSnW;8: b.QK]MmP8 +]l8U.Yaeh!*1}Ai%INox]mI/ؙ,fDׄv,GJ_qt0bl%bxOw?;. +,>L"|1v.q_aCݹw*Pvb᫠xzV-:&댡tG]YN8'6CJXw 5j(-.-'BxT.Q]YV gًBp"XEF8tV*Dχ1.BXOP/3?ܱ 'jE)/R2*>ׯq-vFucOS-I?BYZ=R7%asRRyƯB?#t8ŋvҗ6AO +P3W¶EuQK +g} l:G`iv 0~xN0ׂۯ3tE\e]x=`82t1d~nxo5P_s Uc:wkW kT!ovRtfj +w'.;OJdJomI#m`3_ k~OSz4qRn؜'zEت+ٮ.oT9 %FNM_)8%d'uu!Y%rX=|=Wx5;xmPI[4he$؊R^ʳ<Иq<5mUf,!ƈgP)AX4!:  bPVc@. Z mbBOak@65Ļ;?_8紵fʟ('iVy+ +lzc hC|7 ) 3~cWhg<:гX@ҀLZU%~ʣlnRMrlV;\{/&x~[nRbF_%^Ax +~d8yi. w  b?""U}/t8\Kt7,}nnn,OK i+7S,gXnZT捊-^sC{u0 ˓(eʃE@5=Pէhi\NMQX*@8hcpzB PKO_7kii H{}wUD}!53_4W8@Э9i:s~6jAO/50,cVX 8 T}>8MmnVa&꣔JDPGE`&TSxN~W&f`F/;/VA-Vƀ7=X(`/Ʒ[vc;Ӄ-ـ}7Vs2anv~yk9c*-!QߴZF\Ԅ5%CRSH"+Zm _  fs{)JyxD{߀r,%']w,DXC؎Ss0=:Aj'1m%h+Fc 3gȣJ3@`WdcX,UgG^Y`j*KKNWڿ\)hhQj~|WjYs/! '%L}e_'*?Ö7$;d`r'SORe'ᛠd#|;83$jY`10Sׇ ,.թ_ +mh}J޷sYnqASq|mF6jz +JMad}}6 +Ձp9gP*3ky-stODyx1n*ܭA/d0Mӑȑ)r//.:Q@>C{1}4{37:Ṽ/ õ2/V#_Hod 4]zv;y +4ʶ:&6\w3I#S ʥ9԰3Wq6Y9a|?[ߺmע̌ьmBjmKGlS0FS [s`I9(B*RSAJ'mki V~M'rX=t3Syj{=5m~`N'p]`SO,i8$!b8hj.M/lA}>nOAfUw,ҡn39vU$-cK'5f5gC>.%#F4$}f=ÿHhi}~H(}Pjޡ{~s q]wUXl9>Eef۴F{2D͔קA6seH|2y|֫6&31E<&npQd,# FM_kFbF^S{h7׶F Y=̢*w\i$v'w*Ϻ;!/i#j#RRY-t͢zZ<(tf5oR6QK)@\T)}IVM7`x"7|5/bb&BF]d1փP:cZT)֕ 7w}-&U:Ֆ@_|i)J=o`W*uv_YAL:32W b)$=5#v;NcMBw(jYJgOWTՔz|!TI)H%ȦӤlKNkd^T]x$sSg͕s ?!H# ToL'R Hw9h'sʮ7E s |AU Sghxgh8}4=qhB۷DyQX- M +toJT(%D[ ܃gL΅CGFR3W_a-Cڟ"`txLܔ ^wT`h&J2f-P!SGxj_}*xX^Ϟb.5-tp:a=>_o+.nL+|#Ѧ,`bTd8$brDv~6s-z +n3zT:L*sw׉2R{.,޳(fBsQsIVg_b&/9%bO K* B\!Aw+5+9ao_w /jmzq]@EƮדڂ?,I&[(j2@kPb(eP{O׆7qZU[գ*;Y!Z{@"Uf+77mw*]\=˳t;DSCiV;XA Yn>>.@'<S% +S@kkuDdmfz.85G^⼰ώ =eG/nrՃD=3ioP=4奇Ľ0MR)r񗱜s$0uRVbC|az` J*WIޑ@sԛ7"U\ۜ|Cf_3lX/#߾܂biO"K!~ú#k؛{1d<]RXGmf,"fDeU7% v'+|KYWsRfOPSW=w15 ki-+Xywz,it|b~>'x= Ҟp(HVNk-FxE[䪗"NyF!%a*x< c<.  7Qꀨ2(A,V,) # 3RQD"?a6/]@H/cN_}c+^kjG~76eVe3,5EQHI&v.._)\Q<_q?2 M`~ۂ sE,+ .ODȢ1G@jH_J鹰qׂ}]O I||#' "Z_xIܶH@,LWKy5[ǀvjٶTJT*-룀>Pqmmf04;+ Jgyֲ72WH44ZQJo74_ (eMv0-cH;&s +`0s W S +$z?=vw$;T/;KC@ǣoU*BgG=됹f[V`aM]׷[70y]lQRv#[2t`)(WUY}*iT?q Z{Rls7M"3Xt}q>)jyAi>E't$V4_HMhmC }I2bۻ0|\')>xx͍0bq+exi.IP/dJ ℞qQ*&Z.2(=Smw}7D9^ M[V(ٛ$an@$!:X=49,ΡԛZaB6ꎹa<<@?Qߺ0v=h~Jt j$jmK,Tfcp׬ٰz2-&2}T0/\Z'84霔xCݽOsA3EGM<~*+wsփfƹNʕh;:ک=Pf}>̪쳬W S*T+qwx/L/rEd ϕ%$׀П_U5Zj\ L} "&mBqJJ ^Tx|fɃs6:FyLVN+B֗CBuTSJo8k'8 L=fn%5ZM<VT^qT91|?OdӹQsڗ>Yt؏WU3dU̟J= *ya_c?,iqxPg ~n&Q'zJ( N bIYew;|wbj[@OuZ2׉|]|<>񄣖r8& f6͢IU-]=˦EJHtG*zKQwHw_$Ó ^Cke+f9F +#KeF39Xw 2~D\϶N]Jlqdxg8dL#!'3b:`PTOT%훔:gٞœol^ 8B1Jcwð†~!LC yiZjkpY\q +8:hy+w9gA!^Г[/ +>vHi!#o>7W'V҃=̆zeWZ [bFn^xu!1f]]CV9EYp+en-B;9?@@HJ2||ok#ZPGlݛXih@_ؒO"-2轹DX=:w[Q/&`Idt|s]7TT>t!X+eprVR`nHmҝDHmM)- ޕH:k[~/vW}CuA!CY֩ەQo]6DXY3 s[7ܧ޳%'4@ng wQ(ܶbyd[\"@1%.wr$gEM;[VB{42NV-Ggp\6ݭ'j$"7xa]:pYA1SZ9{ry}h,Ųw:6K̾x_=:9v!?qhrSηthg.'!2/\Y F΅L=,ζ9+W)f9@`0Y' `X \{S3i ` #⛓^5T٤`("K\\待GRB &zsWo,Y;ll#GQٌ^8oq8N +>Aw kضYH<˙ +vP_ϙ$|%dO-PjX>\ϸ`!t<2Y^vo/lR'TV|yUk~ϱ0?lG8;K|odeMnT(muGz  +w}qeXk|Yž}QG0@,J ^GoƏ%rcb< 24 .q˓ ^ scGPy͌Vo`Mvbw K +TKN[tKW@ + 'xՊzG;wˡKnyOr h~&IO);}nZB&+ x'tcnpp pK. rnsRk] +äs?V_I6bD2&#TauͶ~X*@̾@!|K k LTgfaN +"9S@Zw gpCNz H@eam i\ RyJ'jIXWOFiTbOW'6_ӚVNwQ3q׵xI}.9u#LkYmuuM/ú>Mĺ+"t `-"(((sgN/7UIꆃE:k_wtz+K +xU%v4ǻs\XsvT4IX8d^J)mՔhYMZ@گt^Z:#]RB l6i9!W `;i;j':vmԻ/G۲sB7\5rُiL]](}(i;+nYFnskDiy֥'0bAD[j |# +}_(CI#kj5@79Uj K-8%˯[(ռ6[I]g]rjM2eMS"dJKJP ? +N^xЂs+(!,^,4~SmmtH ]G.Qr+#;G%(VwRQCQ8a]\Ae3ǕC ׮݅;΂`;ToKEf@(gF=U|T]}yuFjqv7p5r-*2֧Oe7H-nMvy9ៅ|[*gKlz|FB] 5sVo[Z7/jk@W9;P}_4(4CjPT\{mWvۖ"&荒[ڬңugz+>|[WdcUSZ$ ^I"Q$ú*uH)LE;=;!.L!cwЊ&A@~'[lb :iOZ5CXjIi:<Ԋ-ߴʻoQX J6씤fŠ_d#uXc ekg;߫>OV0-Ax: +IGާH\ 58et?L#rVۉb1* +j_*$5ATSUϵ25de i2aJ:-4_M9t/E^*s@@PoS?+yr$q9]V:c*e|-c.3̠\m:J ޔwvCۛm=Zf*ǡwnbj'^nu +M 9jAL^B fIU̠8d8N UH 5/{`3)-&STFVtp64jqwVeam$vP2Q#2a= +~K Z-bMy{8WJ'їK鈾OI9= Wg뒌$*|N nsT[yUV8*vnLqQ`cIy[ a8.tzt7PU˸rIZ3/pH#b['^}$ϫroڌ2,w^ a\8 +NHn ֦bv8rf$cM#nuͥu+(G^JTWYEwd@Xd>wۮ#K7Wƪ]~)ہFXz|Skn/֩pT#uiv}ܴAch3~xe£EFFq& /jird' bǵ5>Gݭ]LvWP?T/.9ԣJ7v*)xԹhZ06h/bnl9Y3`k&OXѻDw}@-[ +/L1Ո,Ǐ\Mw^d9BXxnqs"I2FDZXG K-Ncu\ኾm z:3ϣi:$08‹C n#4(bzc#XE'ƣNYKTႸ#N$($͞!he6(_6|LaoI K4UfPS{fb6̪[P~N\ k-1BgszYfWIuFdqE2;b\6;:9e(le ddڨj|)'sjNK[/N^v@E%f* +FLM=q9Dt3%>hvvX Tm}nHS-<@| Zh7hź:fMo -?1nV-\(o4uD@ يI*FJjѕќ05)* Ƕ>Uֱ\raUHg u:@(5!o*f"tʅT3#֓[#мQItb P(Y_DHƭ /S^<'pK]CP˾% X~ɣ eU؍%VrXXhKlU(ǧ(DYẛ#& }OrfTlo ]GZ~pWޙ"sMR ~ #rpm(U^Edžs8w91`[Vf=,uJ[Κ?a:Ak_sܕ`S褺5 k4^&w%G%K֓'3K7 p5Aܘ <+?4Ǯ p ֌<id:rc3JBǩ ֓Yjl 2t<4VsI3 s "R6__1->S^Jgpq!|:O?p]6yDϏm;Z!nb jg;~vC,|L' "$vAzA߁ȰÀHaRdu "*Dl^mVnv8K%:LYfG}sp;Ghdab|驪_bm-MGtiHxߦ@yk*&< ! @Yr軔0y=q.NP$YWSQA@"@H@/X:9UCaj/*x Yq~(O\]aʽ9H) (`GPPt1X(|xʞ6|/r,\__l"CtA#$ylGNx/{r@>??#Xkro%hҬ];:5MGh@#2+S'=DQP842}>L.(Dqx7 4qnY`g`\lbbb(3?#ڀ%``R`8( Kjz=jX5yIEQcfwuWrJ .@, ,Zπcr~UU> b^Nu Ĕ/!aw$7,ttvXgx'1Z>3ߺ?kFWg͆t۠@VH,H E( A ~H_Awe@ NU/OѩփVI>!jI;&h4bgjY(S, %c`Vkg?>6!K=H-HUjEB&HKN笘PԎ7g.Bɏ/zre&0ooPRC,kQF6@0A N?} 4鹛vHU;EJ_}m?+ھ"J_o S <'zTDRvQڀ<2@ޢlP^QPȈ9!ȷ/1E/* Gukmxi?rda-hUQ/w-:ߌӻ3z*k]9TQlwRbKEV¶U.zg픽oC-4@} zsuɺM^ aΥ޹8CV| 9t܎ӽ4ܾ]aSM/uKjY4ՄU/ɂR|.!Г]wQ?P}Dzqk=F'U.m5Z7{e칦5*yho҃|*DDX^l| rL2 :E:#t"cNs{=7AT"w[(eȒ/?Z#+D +%BWjhu-8 0[ϘN㶑&N HI3g3d%G;7O}:͒kg$h.PʴE+Y)l}yyx^%c-d 4n&bQ| +&38xTq|ƿT; P;G'ߴ =E, vH˒LDm꺎 +rc_O$3fn%A@x<N + EcqtAVT +XR쑚o l?oI>Gz"n##&*}#d7GaC17Cy] ٬pˊt/A +\m $>ho *tGd# gM//DK^/cl4:o<J95)\': >v2:Q2;lzHvaGej*(_aZ@V5E@1: V# yYҞ)_N_lnm{&jš1P^<ʳw-5HS} Dg s0rNkpvA%1:H@(AD'@Su +h܄pKMfRoeA#0r$T6).eH$%vW[5NEHAN}Tm$Vp13}khhs5#*E x`;z^~؉+,ab谌U-u"-wu 8 +szᆪDKREeQE& QNtI(N-zmnW}> `YEXwۥ Bب?".4JJ`l 0fkW+7{<͛HXm^>\|{CHX%qO#䕟bn1>0n Ӹ\b `7E8Nx8<#xrSVD)aRtGnFK>) `F_ʲ.o $*do")׎ /xQrQ'ȹ<]lǃB'AnKNtѣ@ 6$o3PKU&AȳF/U$"Lc: $=A4*^8/)p1* 3qHDl|ikw<6u +XSTNMoo=C)#reBP WRǡp*١y;+4@ + [}:ĶޱE3 BmᴃH5\oE@Lʟe&>}N~$Ө`+, 'U$ D +QЯ&3T/'. >|1xj~ x.6w376=f?E$ޅw1`GyF7rd]$sdif!_r(A;ہ">ootN\~KF6`%`6 bsإĚ$bT%A>X7SPcXya0 j~ISm?K|Iemb- sW}x>ֆSNǀko/MRj[2F׉;Arg Gm@n"1f' egK]^WUS$5ou+|S?6wݾ !Gvq"S +aӕHyn[ew/Ce^æЭM; fv2kT=ۅcEqޕY㷝Qn߫x]Y +洞wG-(n;j!Gqը~%j!g_ZٻG +.sɑA/ɕ\":r= 'F|BH)=o{ ןJq#Íhrm#)͍EތGOEy"! JWyQyѬѪF}L ;JZۭ^lR;~^)Y ,X.&SLJb)YJ|”z|.%Me_f&7&>kg gp|-;^U57xxGluZBE|U񼶝Y p.k*xtiݟ跴&mQ_*bk%ZCC'GVnQwI_&O1{~mfU[Q["Hq -w%}^dyI kU;W֊DU܉Iuʷjf뫶h.-^$FOZAS?Q؆ɇu:\N>y>[>zVݚ_T|v:MZ]&K`eA!6:LnІ_<)[d1'UHshɐcfu߶*]8da+-)Ⱥpe2B,/ik|yY->೫­-{a.1ҵ:RtzlZ}T˾S2>ťګE!JMҎa?:곇$t{ক48#4eu78YܪM~"}j<~:3|M8a"|4K&+/Vz1Bm~oR${EVsl7=lĚ^{[er^IE !'>\<򹅾wga 08vZFk}RݦR8 {wy>gmuʶk1kPލoaq/Cϵ#?Nxv[y&VњiLQl-y׈CԬj@>p=g/LP"\Y ao;9tE^sp'y53ۙQߦvBzj, MIwHQ٧k#d~B~2Ǎ:ŕ%kV+/)DFjzʔ:>V +7ΔFeetAnxWĠw嘯Mw-W"5|teOV993 +TL쳔 7*?ܰRZ*FFC݈{Um15?&'~fDP3{pdQ˧n-WuiM.G8ѵȺYF/J߱}d|(%4a?DZ~OJ DMyWz#UOm]Ń+qws}ӱayhTsV<şZ˞#{ tx9z 9םzr^F$)6.Z8I]dnב"et츯GxObd9țZ mKl特ovΒQ./=6ڷ4'*^Kzu+Û~M[,̡w<\OU;NɌPҤ3Pj:歾n.'glwwt7؟#Va2l/loɶ)+Aeao^l.[M %K| XUׂ}9A쪳 $lkE7ٖ*O 6ϏoGFQsJ:ҡWVT麊Ib:J>xјj C +9:CfPD!(ٻBN6uΜr1&Cg65=/Xܮ%/Y2ά]8ަ=O֟G5>N7<Cj;_T }RX5U9?k%DYMVRsf4߉'h01${OME{gׄ0px4h|ފ G,5Ug:SC1a<:c)Ur+h?zqku.¾;gj_x/";Yk)≹R-4~!#7 5 7)W[h^R?*a,jP+u +U +^5 VzE]w8Y[#'/*J7`yt8{\mԬ!J RF|L~W y^*٫]gn/EϑVG!{Y"#1>=q: ^afP+WŠqv}݈{׵gvq7`(ѿ 0ʽ窟ʋkka:@㧱ۗ .Aq7cb[ծ݄}t܇ 1!GG}f>s9}ՆCR< t0OT.=sVp|.\f4ebvj';O]v7 _ +|GFE?A32џ~O_w'>pO&d?lr-$|42O?o%>~W|g#p +q{ͦo ydgw1OR.X2[E}G_K-Gw٧Ȱ|cjOO QO&xa?.Uk{epsSv#8V2m<(VvbQO5f-;`"KUEs)d Kۏ% EHl~y<)okw=w:Uzm>OIeWVa1ՈM Xȿf6"XYV4;vʏymrهcD¶6 { y5>VTfj|oJI =?\tZB? Ѭv ;ӎȏٵ仯mxi)5CkvX"׃ ; ƺnh 媲XW )D s._i`7-=]u/݈Jmt\G'Wikk7G;zR 6Hby._g›uEm[/YP_hM:Mc)尿 +vm3Mb=}U)89,TvŸFf`ZhASs4beO@S!:sWb3!PF7(5.񧂟Ts%}Δ7qVXijQ[誻jnR^ @YpU~s+zm 峏l8svZ2w4EUƆC4Lgvov-i5ueoD nrRI.y!xk60vTV]4g'>مWO#fOY$wrNu({[.1/}%5)]m',B5?ک><;y5'v^FrvUoܚ<ËzìvKP/È9q&՛?\z+ٷҔ{D7~.rNy/|>>n[;W$NNƊEYܫ"jKy6rf'x&ߞWp0> +.?8_GȬ +b +[꒎5R!-G(3+盕Ƽ*kwFh@~JͿ݁/DG>5Q v]p/_)9#ݓVEQS^!7MKCclkǗ)U$g/LbNykUY)dV~,߇44?QvJzܾ̖Yg݁Y}nKJd]jg5N)r/2#T'ۺJFSˍyl܀}!GGXcn'UǍh I+rt'YEL][dN\/\=3J|UM)^̌xZ=oO !엉+b'!'I|@alT{= a8p-1M?@ϔ/w\ؙ9m#k.<{zz*CʞOnNbite7o\OgV{f0g+l1 HK{ +R9gR(8{%[w kZ;GJp"r H\,{X9[ÓcfgI1ȱ #yX (D1잟%4[$^ qZHYۣ6..U{¬j\ b]<&/4[ [J'l;ژ,:/cD)l [eTfn,~un4fMh&n%\≎=9; fTtM[@׵@"E4bHgRʘHk{<0T?{l۱o#47^;}'lzQvpm| Veّ(z4!gR|>o|=H?όݯ|&>O>{a][ uIκHG`SSWo_4[IdhqfƩ\q2sHakΪxaB_gU8jD]6p-_تj ځvB2ǽ[eGYy;23OO9%ÙI`>xq/2$lO}.l_7w_]8nz}҅sIud[}z{@9B&'>{ZggHDT f3$trɽœ&R>E*NJ5]Ũs4 hq$!Y%Z_"xnsPΠ6~uAcDv:&Xyjt7ډB^^fؼ2[pyP'85Ap6Oniz:g|AROB'PrR׋\~ڵ%~rۅ_\3`cT, :t'MtlB~5'zDrCI/x.ѬVw[[uo2%Ubwrڃ}lcjerg@8T^6wCRd_H#&WTS*Y~I(uPYKP+w`F?T~Tߊ 'Qƪp FcwrLdN(6ȁ]u DzdYCp9m{VPbVͮYJr~ED)s[Vz{e/~"Y4gǶqzALHr~}!ϓP&>>klȅ%@^5ANa\/{Q[I`!RlU%=* +Џ D 8,E[-島֪/TVNWB 9pSso<34۴OX5_={I1'|jX<j4JDV-b=1; џvbG4i}s[\nWp/ūQd:,9OLߋ.{mauDekvW}/0LF ATK/XW)!{Ƀy7=H)WsJF}*FV!ed4퍻4n.dӋfz_53͞ ΂rl^x/rXPdZ& Ygρݣ9C]ڶ>)7=W$2_(\{˜R4 + yӆr[<p/ixQPq,D<TTETlLlD'Q/{$z adOŇ*eeGYkOŤц5?U4+/~;[3v{g_oE+DtDsr4Gt9K|vT/ YBJ~Y4C|/#1DދMc9@ *MRgc[83Em3 v {\(Xx[)s{zSp֛lYևWhwHz.z-2Ӡ$S5}GI'^.IK4b/ȃp.׽_/NਦeǛUylXa-/Q2 TuyˉpʝQ!LۮıϨy(.сʴC~ASf=}|'Kk241T脌Sp@f}hw):sՌtufc=VYnLhԎIwRυx? ǗN۳xjKTs6n!C"3Ӳ:˲|m!̸=n#-s[Pۑn&ž.n#9}b8}չmGpBqe4[mmڝ3|R +K- [-#9 +Yͪ}q ,t7g$kiM9hNDV_mÃqox+羹\׹x,kCE=ڕ-5 +2'c,@r$h 7NvmӔIH4xZ O4>"@kn_^ۣ'ke+hlI, ķLtlJ/*_vmVfFTL2$]0Dp7^}aL_ +Yn, 3_a48?$/Q!$w\v9mS2ǧc(L:K:i*)171K:f b7=XTdӺЌM5DSnWtz`0$yMVe}%빾JIKҹǽcxZq:+ rt]ktM.ԙ]:(~w26b7P ax6}q޹Q|Ga̵i'ןQq,r̗]^cq'o0c]|:9T:)?ϓ`DWqy}Ts(O5hA9O0y(B*LzκpPOˌOOX<.d#\$3ꠠC}DS\Z1Œ{ )VU=77 ¦r5(0N3=CdvKSI()IX]FDJ4gHs5l!r;"@QP;ykVl|~g#Z9=rn%Ki--J#_r[̓s&e%ۛ}RIS +^ރZSƎQ sF9<+^_o>r=0@szT~σlDТk`^>Ң +6>c<5j3N (cGIb@x5pR^So٣uyY#kB移J$6o_]_%冟=OdԶ@t;1țZTެV#t-ݽ,w\vT"K#ĸ+Fps87e]by~Ob1'^jҸp$Ďo$5.Ŕsy%Ĥ9aDLbVL={X>>* PI[q}7A_c}%+_O>Nݭ~:\jҝg"r?.u^k[[[=6g= ZT/}j'^AٮnlTxTF T#TZ̧R3qɷcG㚂b?A\JQBWYCjL`Q~^!L}=G|A[?kh&oE-h~Awk~AGO +4AtvxTaTk2tq,ۍl6FVz;dvFF;2a~L(zG#ҧYw$b߇ ,%JbI5ܒPNHTsM5"goc$$Ѱ[1؀^fot e-4̼v ituVs鋥 -TL6eDB$T$[X-?ЏB1=a|ySu[m.delp7S<摩~ &S.i=@4A,IHd=Gmi.>'‚tu,GҰXOߚdBqiT4X3@YDKO!J.ȵs؟V0jE?s ܴ@$++îU?fsu;ˁ*n}hn!WO}~ZHFMjĖ(6'ZT8|c0q49f5M7{XN#"`mA=Y =$-H"-G4-?? +mQ&wy +Rf{.^vKZwfct.SGuOU(U֪ʘKdrC21ڴ+|O|Ai;h,&ShlE^ugqU;Nen5=bi(-+z8v>S;^.rz񵑐]xm~gQj +kdpvc_XRު)ێ~a;^zv?ڮ({*JSL3ĨWlSf}rʆms^-r$K&|~5ȇJclQl*n`g3OJ(%wsŅv%R߃J/ n9 |d [ k¹~xܕ*Иp\U>C솶,W8|,_ӕZIrx>&Wedѷj 'xp9u/\>0fgBꤠo>MݿQP#nb}(X[YK j ;˄SD4W ppo{rf3nĕ +odhNa_2CP)蔯e )&wK٧녇L-Ōt.+es{5f\8_.KWz^D!0OPҷeX8WҿDGIAP \'=]&zgb[ +~I7k.)tb8~O[=5ys Vq)׾܎ѹ+j' Ѓ S ASznCs? KC{vIDhiI{^VDJ>܎sQs ҙЭKGU@7kw_x&؜-g@xwQ=R\zy +wIM c0x,X^ lw)2ޡ~A߇~3UottϪjziI6J rõRIC2!l$?\7EnYmCRg^E1nENpoP]EsG?@^< 4 *|ә#%X4aVo|Z1y`E |A'=ɟN;jg%o ,Ogm[Q +g"_zSMTC]=;'ާ`Ed.+MV5kZ>4jJM-~bv3NG^Rzr*\ ׉u/R:U5B<)(MV76]dCц- 3e6o.cӋ:f53NU~g[nxoܕVd;.N׽H C.qxn.o.$ nV՛zx5̽.\NoAp +ɶu"$SW1Gs1j|vu|Yq;99nȭ y=WJ_[k9l 4h$ k? NZkw^K.b?&&>ڣP16bvׅ?j.Мם=1{/p6bJʹᇻ{ j22B2WHJS>(+@HȌ'gH$G%e&?t5WYlf%vЍyH1l$bjJA(llM݅;Vu{ Iy@u + Mi%J*cV]Yi3xEί37;_Ϊw\GuD?NOK%;k?@xP?}O!FUl0(P7ݨ+9Ý[БS vZޮܱK"umuP%^;|A +Jj%m[ {lpYK1qB?F.A w ?^$sଙ`k\6LWi p^Li劅~CkZqN)]ND@GT9 {r߳T)򓎳eV,aMr YKӎBjhs++Ⳳּ~w{U%$.R5 +9rbda7x"n_Μ*v,sH.V`|:Y}d뎬3kj\AFKݽ[!"__@5`h,/y 6U +ԟ>gW) +?"efT. a*,j%{MX0MOx'35HU4y]h+k2뱶*,yOzK(L}X7KSZB+E6׿'%󗨼\lU.P ]ʞ,;< |4]RDjd&׿)l*^6ٽ0;NIĖ1)FJʫ"zyuH I_+V'_[+}~1sT [ie24Q'H%}nv&كc5(Y*'ECD%y˨F E v?T \Oc_[c0\TDu8 +hr& +*EgqYmQG{cˑbtӺSJi# מs+v|\rșgMdİiҜ_-n-]T iҙ!Vo%qc a]͢ՎSR. |>G3|4O$//ѕ+JD爘 ͚e71ί+!7~Gaָzڥj|m3s6ܝML*稑A* :CM8`^wQaX)@YZ;ޏɄN /WwBziibnSav/P:mfPhϕCSp q0|.Tck{/89ɒj6 +P61~{ݤ++G#cj4IdYy丆}I AD+{ld͛~՝;zvQޕD~W>EfV/Z&KP`~W#T vSF7o\G5FQ\]zB.EHzC+cLXCTg_3tH;d޺F~[ϠVCp/( !M|U!_>}[i|}Lԧ ZiURf{[l%Y웷0C{ 2vv0G͖xPK>~3uQ)|PY}l_ƹzKPAGLUfXzt +{KGn==#]kOdOMV\^ݳ-]ȡh nO \w dAMtOeIeI8k]ZCWc}fTRgs]U;u!a=大 O&TW-/C6y1bRks!ӂ4fTu6X }35)@X?@Pqjʛ _'Ro|V<`?߱*{5x2>6(ߐYxgN,V 0l&lD 껼YsfrOoE)/Q  +P9(Kb5yCi".udŧ B +b"5~ 1q(N%߫xmo' n90},JQaDe6A +x .Gǥ_a!^ =X],e)nj4q~>g'3bPz MPVk DI2*aF>O+zYZ ubgMrɗj)3↥Wi3X]J"z9xϿ@9i88s*)=OSЅxoi:uE(XK80N[f6JG :.UiM)҉fX{hedC>K}#?<X ApѱNmfÃ֣0jE8?)t=qcX+YEi'#hw"dI^qTK+*.([V,yFj̻g~)>3*eZqM +îpd0 >#N[EM**WGϯ'*+5}}n1*~oJ<)):vK^I`hhWw+e^Ngl.t%Y``V*ҪRe}Wj@h,֯:H=nSX7ih F٫=k/ڻ/u\C !:9p(mןPH\I{b>4}Z> PfJϧ 0{$ىtvvڽ߶Rs)J>˷E7~ɻBnQeY"[_vЧ}8Ber/B\2Q!@iKTFJaἼ?lbxU+L~~beőWUQ%(7W)k;]\ezJ<֫;ZqOӋe`4D4lr]Q˸X:?:iT6fd~L)Ck[ |.[7ϙb|wI֕Z5'%)7%Z!%mZJۓ˟IwSaˢٯ.퇬9:z9wb:_Һs<3 +];T\Ϝ=Z:-RQkG·gVT{:u2-7)h)SmW=YN͏ΉS굻,ͤOyJR|)-If=PI%bҺ9(D= +|PsT#^FMGI퉊c>j71Hx#؆H2׳ JmqwBYlv8-S3orHϋL4F_BJ9baqw }yx-ʎsLtL$CZG"ha/5?&@ǸJ.0Pr %>q9&.k/x)=SAU7L7_NOe)5Pv4IJ ?QV¦u_;}.ɝLݺp0ϭ/J/[p7xTٻ1v+Ź0lɽ<{(Iw)p삺vN {S^TkWn3Vhnx͢5rh֋/P%]WZ w6<`"e0򦳝|.Uk +G"XK[T+~,;[̣o-7ڒS6qWm0얰,}erhGF;|zo٭†;:% +N8tD|j[nv a-w@mkYl}ڜnxӉYW{ǯZ3]"WjWCGiw.-)b̙39 WTj$x˝خڳO(xMm۞32lmRէ%XDTN@i`%uM߭_ezC~~)qq XQ\;Pkͣ kPRqg6ͥYmz _3Cmqϲj +M_tORu|t_5J-e[n1ڈY24<ڀx c~v%&ʷuLOn\+\M3*#NKq)h!o9ZS (Xf0M\L{0is@dg{f9=# s{4K<.VJr̀e~̵,7{bҬjS5nN3z8`e4ba1:A;bȯ6x3p,EM=?o-~HU26>]UcVp].?8q)S_>b&Gsp'& A]Ek>ԃ$ºrMrدwEذMM +[6;Iɝ(Y.(U%(PN;&(>1{V6d|V$Ev(]?OT2~4)2Fva&/vOFrDy# tj4?+R_ߋ6G(AV4c֠hPn,P $kr$f)|XI+>.]ƈ0=DX`BJa +Iӿ8IJRg> -P 6( &q$XP*IqEP(3,?BیwDĕb(* +-8!%%lr{l^\wPMI_6z⠜nʩ8 ہ@5\p`P~>9Έr"~2z2jB'ozaxj}){#v\z +, h8yol4z6o(Ϥ9P:uz<.@6'k&ҕXwgiɨQckXhQ>}SȮ0/Ys+]%KQڳvxd@n$R̤70 w!D4 "݂^/{[8(hy;vGfκ0fͽb+ܲ˞o!661 '61Dtu% +v6(Mү0ݒxqDQ%I/$b,z|jo7r! ):˰t +Kdlo]t(kGf=F3^{z衾>1Ⴣ(<fDt +g]PePf1W ٍGv[r+͑akX:;[[7`h zG ` Ԡ6R'g(P$ /]h'@PV =ƕ=AߎܼWG璹d FfWzpb~moUi<9(j(*Uȕ+~7L}|ARW|guND' SZf +kN +6iXt rc`YkoՂ~2izd)rg*Gw;Myj%n$bc)>_4D߽zY[9jKTNV%`Ū3L U>خl}wCϨiKBSOߡ;96v$Yag8n> x+ +|}}gB|M4=M7_?%3rͮog,2V[=:*(H${< @޷X~ɩ +3 ukυӕ!Cs=-1vJ*޿" Ds QP$IR@swvz~POV0ؔRS{C6N9I$|V޷ًhѮknϷ+}*_Rx2q603C%6e*uk]D6iR&ߕSw +4x"|3Xċ\8]cȮԇk?ި@{S|{USw bWLeR+X@޽|<>9=:ԶE$ژgI1.y+EXĬgts 9nJ;9Jy)~eF$ڮ:(ų+^P멂y{fO' &6Yej bzeaO2:GQ }%9ۘL04L4"Ma8ֽʿ~I\jکy]bu~ncȾj2Ӛ=gSzE +$p" ,e˥)9l!,n!=l 0uSsjψw@=2[=4uaսEru>᥊qP8xNqZ=_u lLeO_ȤBzI>'OZAP qM.":U{O+Y3˱^K\К#}Wg}XXڱ(\8{7J}pg~znwbgœ+,ҾoM˻A~>}V+jn$⟛y0ģJdTʯ.5w'4\;pGlMY8^Zp^ +9B*-^gHLFr׵UÕ]B+ S?Myuʩinj aǨV^qVEia|5X_Ua,6笲7:e7wFNy_^ Oݗ)D4Uf;gDo]qn8h+{HԴM['.egZRMw&|->>o~qN.֚1f\ؚV}MEҧꌖ_ +^R_alM +e:/~/ +ǻe6 H/T_KK/jrQܓٍ^F9YoG~[,ѫyj{Za%)=xۄe e;.GX2ˢwEov b'-Cfn N7PZ!+X ~Yp\0L4 p߫#DiOݣl)efr:bPZANqLoMf [-~n٘)a49 (?R㹴7u/[۵0vzTvl5';ץބD?[B,Mp<>$¦ Xyqpϱ:TkBa:„z1ZZHNs%Gxٍf$irmskU<3;m]6?kSN+Aw0qrutԓby3<0>5q.}G˅UYAH$C <'N,{C}zcN)Sߝ ׌Mq+QW[P-^}qBgG v, |rumU~r ksklvڜ6ozHa3a9 rַ(8o7Ci0v&6ѹI3SI(j3ߓ)%ZfC[AAj1g1O\H" % &0'Dkߥ sKǁN%Om9]|i)n#/~C[xqsI/F4YxcE;7y`dbyOFخ[Ml }9rL<&V_)60hUuP6'WE$|WM$xu>E?BI*tt޵6hS]`.k;}!&\KaRݱ0Rh +Jh@'_|[9"F݅ |3h+`zg/Mq5=*0f'p̥H'5}&>fbSӿObWȗ=6jy̓vCe$҆ ي~]~TpK>*;~jrj^V_tKMavN ?PRj[dD/f_ZN;(Db +V+Cxw$:ɰ)jiLep1]Jlcu_5cWŜ׏ƒEAA-FymG^ {&ذnB;?A'A'Rzl=;@VZqܲ&@] ödހx _ɵ(^D^^͊M5v +ÞШ"- rJdTع%L?Jk?IKXt"NR]N/;Y;WfE l;cA)Fn]zEIt-557u?ֽav? -YΥ@L ,ո:2EQCw,|/ +>4^9C\nzuWp薴M;kV+CTg:D*޹? +[_xPX5/Vݷڼx1lA9j͔}絃pV\VqMuBۆ*83f'uCMSt3t$ʺ?@O*A%0`l[OrG+G 9h^?A]Km 3my?pi]ؓcww!jw q m 'Y,?#5%tI %B2s/T|R5sͼ~%![d +n:6LL0SfG9#?@~3;wvE>[*JnZLNV1ӒgGWƨeP4v͸pW<,>]^!I{ea4wv34:׉/X^'*ץ3YE,gJ+47Hd%@¿! PK?mH*4Lڑ2{>zGudgt!l K0fOӏ]ɪZuS~ |xr%%RrǒƵVr)ȲdY)*L ~|PK(>tXv^]aO\:s`%t5o +|Y'w{_]ykzW'|)1;pl{:3Ad2Z'FҦ_otL8l2V):L]Y|r)jjw_ 䔄ug:b׀(a;em:j+V^o-y=-SigXPy6(Ym75^^h_[EY?fl _ݴ%.8!flWhO̘0tV~)EHSWZTu,g ]gυ/}7܆k Yֻ1e<"i<߭%lL;freMkbW&NӴ{/Tu*K%ٓ%K$ITŸ6,yi5Z3tO+ j@2dsv;X i\:M +7NQ[PDVf'nZlv=g_.ПnTqpLɜ`rGwPvo3g5sn&!VC*>Yڋ +Tc61Ӌ޲ZC!3;;ϙgHZK@))w.Z+<Wbf)=f.fc=G\Vʕ!`O=/gVeJA7Q{d< Kv9tCr!G U91q>]+R}('o#`# *nZJ؏'.].|%X&&k-Xanv +EFgά^0;޸f49Vk7~tZMzJ]5X2Xh*ٓN`d [T3f~ݴ6L+Oz;ZwnŞ}'E!2zdHn֟͟q+-}IUv:-*ZXQJj%i1xe5Yz +v^4=>~6cҷpчM4y=Vv.μŸALR}s|²?0Ft6YkD<3yTa{xwm[lii^G^>6ef71>)óC]H? +|JR;s_Lԧu"ObH7wv2YIE>2uE/x@&"^χ7 "0Q`i>>S||kK]΄Ҝ 6:FPo- ]cP/`_mP&ɺvxl|y6*W~lS&}pcK[?EJk|d G6"gv!͎wU7$[xNez*sq~n6PPh,:J@`ݣxFnᙬ]ޜ۹dā u4w:턿9^4xgϱV[g6?:D4:'oG}cj&s|@'wcA][<[wo!r_q I=J4cmm>V8Vf,q۶33cc$McteFV{_tOߝ)O@czHS2z%;f} 8P:GAX7@ke#+{ t3fwXN?Ğ3=9,ajOh:~Oc/2lk?qII{_mםmyE:=>bɩjs߅CYJȰ=z_vTҮĸcW[GSz7Rħv;1OIN|=ŭ;(XUݻi^9Tt C~_*@F0nPLs;J5ZՇ:㊰?ua:U@7Gڇh7FƢ 0"EE=t=rՋI3UMipLpaԥNZ~L*;kgM!Sj]EuCimsnJ$%U0{*GVsH.R{r3.WpH!oz@7?rTODZS-ͷ.[Sh~_+V]0MEd{m6풓嫔短^ZԶ>LҴPN/@@LjQ{F]*;rA>uQݚ:O.iԴb&9B080c}s/r+n9QU*ޕ{b&Uq3[Q-KODWDb0NoL$͵.(uwlA}lh#Nc60g}yݕO@}Vطf?A[ȇv( 4~ SG +Ok&"GvD&V1?P(v]|t[_G,mO~ 3Swn}z+FY-㐴{{ŭMp 9'<a]ܦ+p^×Г_!gnDSw +a|SafF5;@5̉S&%OzTޕ`G$7*;?6AeۄMZ,YcCv(o /9CTV~|}붗Y)Xnz?QnM2kWyVm/_iM{cT(3l\XoXMe^Ψ1k2+7H?܄h'^P@Lʪxɹ8G4;mf+iOl5NK l x_BXqQnm{hhMe=uWV\]bHΐ?FIq{=tu=/s/& +E ]KST348 ++=kU=>>l:ټ-WĽ)ѧ֡~?O]>ъ`pq6͑c}~ +=> ( m;z(j=Ey|z%x E"BeҹgoLf\Y? sڟTy:Se/%PJQ׌3[Zf)"g=A]@Wz;iwd1{t$Pl.U0N{%wl]kK㕽]i8mSX֟;~BlG˺|7@.u: +A+k/vGICtwB |9PnvؾXo}gg:%3} tJțwhf{W_loBƟ*G*}&Eه[}R[B]մA֋mPu; x_ߧVmu#ͧxcf[mmc+F䶔K]'\t}%R|id tvdƏNitavrlmf՜fbKiėRc a9M6ܨ +Ż]tuY](˰p/R{oý<^3W0df-MO͘VƎi>Nv~׬XqK?㖱FQJ-$֭{dx⛌r2̤D#?>ڙ@.SOR<(ݑ<쏇1n߬gr޷VOb>wgR Cٵj4ATn on.k w 2rVe%e,Yc$p8 w}0{zMN嵨]Ug'ɺՆJ3:fdo;_*RIm薡[4X±I٨i<}[.ntVhVnjgf m)7o?Pb~;*r+QfLB ꖴP [|?& :|] +7#=5íYN}R5 pXQӒNj8Ly,b x4c %zo.9%~sp}֮M3@g]`H@Rwg=# b0K!R$v, y9++/dΥO@ VH5S/[ǝY𨚜FsCZ/&c"ouŢƑÀ$r*-Az"Ss*zB;ab } RйN .N1$']&O + Y_Pe%<`$(t"KX~f{$+@֣7@g hN ^)48Yv9) keyUVq)L-_w0,2>@w[wqG +^?  ovR@O +UH(d*WJ(VWu=9~׺>HڝnE"j9> ŖJ)xmΑnY?+Gv#g إ*[՝+S ٲPsUΰIqb*)7͔8 +\ud4b#<瞥*ڍҨ{}#7p7@>vp +-ΰ1N#ZNkqi-RSz$V9;0(67~wwǻCcݻ[3nxdS71 oŔ?\ ?o .6PNfm'_ao+]p8 +UduYTc=#[,vt:|\`o|W f +2o~]t:#)PgV˔kE`މ7ȣ+Aluv`yƸ&t:d:=Ͽ;7n]a)ZyՍM(a* *ۂPH]8GeaՀLk~Aƒ ,d@?ɮTǐc;KR(Q6/R$n'x~-RKpl#×- /R'n7:B7s:T:)G[5L1XݭP-Z HXK5d.8NkFs/1kUّrwe*UO m:[cR*֥+q}Hy9J2sӱ򽻵ؒz@)I9mQ. +KL-]jM8litJƙUdm8u7Du>K>W̜ztn..pVU_c-~Je8Ŵwoa{v|v1yHE D-bp1A ܓhLX7*r 3ҕz\6MGTo/7bYnw-7]*[l|rj_aQO+/[%,gma,pOrlT+1zv߬IT[nuYiĵ/B0 2蕔<^IID 2t"siq +EEK7J?ѳ 7\8+YrH-,o xMsҫiAn m;{>c򧤰:EŪCo|(l٢;R'?0c +歗poWߥь<'%oFNYd{+w؛{kRR9/RܦNµޟNॿӋ(JGp|@y7j، d]NOkҠqvb2Sxy)Z4ƒ9J6q>6f-/|.|Xϥ + UO,32DyVteɣy.:hz=WVJcN#MZq|!c:^|(i\ٳQb÷]:70±S۰Sy𶻿ڮNpa!KyU2B1R}by k,aN[Ҧgpy[[m?~4)BO*R▝Ѹ̿=Go/IxW,LFԊNyFkk8w5g{M-tnPks]2.ZpoiKUM8˗o-W3]6^ *ɣ5.ú7@.ʫbo8 +r#L׉ޟ Xhsӌ{cXa=wҟ*Ls3Eʕ-ijPfYZ`:GPk;k!:>(67*'2u!y]ϠrG<]!J]@m' :1fּ"8HnSpQ{@j!ŢzH^upu{~]"8v~ f%u:ȱa4hDS!A5tyMt1 +/"/9K$.KD)L\P Js6_É+Ѝ  + +zuߐ>%W'S6B=ǜ s|nn   N`ɤf*"ouM^YRX9fJq5 Ŗz#ݠn~EZ2ȑf+ +}am][- ԍ-YP܂̋ Uۛ^ձOE^~:;mpA|C~?='=|ٽF6z|4Aޜt2HeH\ȯ˸aJ7oL:&G/~"yBF@;aTGA[P2U7KRna=䑄u}5qc^ i*_~Y2R75<o_0'3P\} _GMvs^OQM}~ na@< ?sIU`mA("&"$DEi=wj\PmnZԦ)דdu v&e%@r=-  xe@V6+^ !I>(S#&,^߁&rBDzXp"`%X6P%- Q6S&e蹫_$8@ZүR mxUzX%n1Q^ Yl U T&,X +~]t|yCKtu^˭}s{qV-G0w텞} +^ ۩mM*Q6~<f?~L.#7V7{^N:e(ܬ?[ygWͳ#iЎVH 2oUmL\,!h0BrTB}':zzqT 6l8:G7Oa/7I%SWպ6k3N;G2vUuK}C]MͱH_?(^@͐9euz;fQ~+y >d*?{njӹ/wNt<I[zT62[R\fzF`xgdx}8^o,2O`Ѭ]; ]w.φ\;क؟WlzXq㛣k嬭g' k s2g鯍eY0;{~U &*국tu]r^WY/_Nä f8@ū + + Λ@þٔ{۵s\5.*&m'FqƱzhUP%SzQ:pd#k*4YV,R^s҇MJGUO/xf5}+5-כDO3 e6i!X)V[tYϒΗ>).:AZu"Nf,]fݹ'N1*EG'@ `\2 ,H7^WOA ?,(oʹlR7y=_tC@'( Y|F^tE(3#L1lY*ϟS) \6>5X٥ +d~^#?"]f\Iheaf6q +~v]z,*-&݇#2sC}vͣC{0wr -z?L,=iz;LHYX 7M))*wô4t.õkKۛR5/u%W^5c~n˺y2Ȯ ymmoYTD1:͇Q፲vsCUgRVJa5$Rt> m[WWr/UA95Ӫs=0cX@'N/ޟdZ^ xy,g&.UYjon9)%Ǧ*fios"䦬i&uUg!Q#ػv$S I5urpĦKti1r >˿wqz*G0?Q'˜iʨV5%J2mٚ$'>}s|[M6d^sFPCޱi8qe&yM̧DgxyXT:}z]iwJ]hwE{7ޮ9.x }v0m1h-Ub, )8P?5U˭o/GKx2"P'e0Zp馲`aqnˢ0|9,Jf<{nÜ~ { \Fzt#Or)ս{*^=//QTRgɒÈ8Ņ\{>|.7o>!ٽ{?8 wgO@ER+YV@aEmjt~L%%wve-Xpb!d*lڽ%&y$rw3/kO5џCUo早 +ӗ_<-oc\-֦^ii>n']yvp@3tEп-OB~Ѩo3$خD=`)eԘ +;݉c؟&'NS9EX:yoQGe9,\o;lrjj0eqa(Kx5{}Dq>!Qk֠HM&&dL,h0>ͥǧc0f`w8œk8F5c ‰JzTݐ^07nL'9JH݅,n,~z?qm<5fx(acD"*. /ܴmeeUuZŽ^w17EZKl s{=VvBiJnA'>9Xte \e=a݅݅(nO'&YiY̼N]Ov._=#/=6d6螻;nv:g4u*֭KZ-g7WmvkRF +Q6q~wvRfN,,ܷNMBByg|1#$GE{ƪ1S7p4^ݯznwr]J $},%Ka YYero<ƪ xt0HJlh518,A3m>stream +n@_! +1J}=K~͇m]ssDI :(H,]߫ Shfh5Z!~JtXx"so<ɮ$_dQb.ǕkM>V N00F'cb,]1:d=PXlP ׋I%ق}ykP'cPxec#ϛCpSn= 2*}SqNM$Y  0%n^5_˼$gOۀcg9Yd@7*zzm,2oDEYhPF[J]NZG/@|u lI>{7!Jl}fsAS7u"U'{,q=n!vG˿{d{/M2Bvwj8/pgEo]PTNM/@ZHphl +ŝx͑wT2o,kٌC=|ܞ{]9<(Jn~WuJ p!6;ILQD[7PA u{;84m *Ȕd&={ǝƫw_݆$"{-35;q<~Y;7sI)&>?,;Ѽ鲥zbQ/&!/_R99fHxdȖ2np/ytFJ64խ@eVn,:qԊcSz.y<MjឺuSݭ:/ڑ P&w?`^ Y/ug0 k;"{>]xV8XL0_Ftz$Ǿȩ? w_̭.K[AteN&crFUɨ5X>Sma2l&fi2f}lH +h>k*Fm'O)k3B fy^y֛h xnAi%S*[^_= +C3Droik.ħܚ/ʩzoT9e@\r%Z01SZtУV?+u?JN<.)\>N6QSwPX̽'3{{n&P뺝;BWoLQ'$sՖ,Ƹ&,9MI(\DŽGzʎՏ;K0I11czeMU:KpO W_`eg2w!*"tTn:"/ C%^VQXQ;o~%3I{C1C:c=8z^n ]WxLu>q1mB=Tz7\9P&AYHNXd6|u;V9FVVsݖ}5=݅+^=Sy0X,LV*DR ےd>jd?]Xů9Zw6$<fݰ>5q}"k/kGwn?}XZds >0t-˥j;6~U3Zy05Ϩ<BC? o=?$XeYi0gvˇ@ty᪕7(UUoQ_qiqgǺ2=KA@/`Ro,8U!a/V(DM㷐LsԞ*⛞*TGIJXJ'|& {_?]ɇ< Zݕn9{ti?oT1W,Fʜ$ysʕgAN_IMTSơPCm 0T-KT _6qm눏8؉.0,1QeI{f[p(eQ+ +|6=o8i#{k&_4nS-~OkF_8LC-9;?ndnubNGvW~}ea&0Emჾ$Y;{-f̺~JtiX&c&I<}2w 6]͍K'5ZU|jTxѩ4ܔpzWJe='QBYG J^;_Ò541*put"N dR6hD<’ vIlD -?7G6}GGZꢏ=g?lXI|s8nOsfz9['f" 7A飩OQl'qwkn~:BVr߾tK{_v̵imF ,V}K7M4<47pίRƹGԞqHvǹB)uP$X~uc9a\|fP5N6;ʢ\}IQmjVjx1UV&UQY[hJQA]j$WiD ɺ$!W?d^ ?!)g4o}"IT7Fŗ@R|~J̊b?#K}"3}EHkeyM6/swoD301^+5ˋOkèy}jUϩ_y/#Vlg +B~r>Yyagg^D@D{ ~J71]jS{'YߜtkPQJA˫?ė6fӹERfT%&2PI+"Z]m #J2-@.봧e IH>y| RzmR0i,ˁk1u=TظCTY)kA$;dltq\3Hcvw?= 2.3r5 "He Nq~{i:i/*7 OiW]Vb9V!H7'Ncas6{;=G wj0<<ҟC  'dXɇ MgvwNԕ.&,51+'w1\evyS +ŭ{Qfyo^laF!@qhA  Eǯ; ғƳ4 +v1aN-!m`Gf٭vpXX+r| Aa"Z+;6y|1E^;tqf33,I'ސmxΩxoT)J[I_ 0W+]^~~ΉMŽ%-J˥ag>yO&{A[0S؁IJEo#ͽGEu'[MJ#6([-v$  IlөNPMrQ7Zv6bH#^vZb=![rڂUIs& +lMY]&Lfxv*nI,i5LYl@FV\䅻fk5\ӓa~0/{2&.讎 0ÚwmU O\t $I.).ٗLM `9Bhsb7U1$QvIMXo9?ۻ=<:"m~d|Xݳlnmg c̶ ȍ8W]/n͎V5,)uպP?k ~78ŪQm/gS#nc"4sYc^WutYfqFO;T3c#L~}@Z 䍀L^݋ѸFXs۳ת[GN3~ʦ,b,z2bf'iԫUwCWOm8ʜh#)IDL#`p/`=|b.Fx)9?P}X)[F37ѳfa^/%Fj^969H9"?iX#ۇ,OΆ5a;]7/>t`-]mX3~r|M2b2k U pfF)6] uTb^2V݆:t"Y̅'´r7޲y,&٦ԗ?@)Z ԍ@mR}I3Ӧ:?F:m%U׾A~ +W=zP>⬈5oa=![QŦ}=D {{g Sd*FJ?άˮE,-u :vX\Q.j+mYm$bfi&fiH||YJ*im5WBz_5//^},uv]6>v?y(݂V :J[RrR8*HqhGʑ+_%#iJ^lפ2\E^5?uξrV;wS_lkcrЯ^<ԷM1U[n𦬷[dGlYQ,Xz:= swZVV55d[Iiug>=l6ibv ɿ8}'_ N, +lWbZօsMbB0*ƿvMJv,Wm,YLc>g9AɁvhCSX93(a y9_Vqxp'䌳=`NumU\)͇J"Լ(xM9vԦz}}~V_65l;?31;n4v `k_^E}ȉ sz2q;:NݑAos +TfS-֠f_$po2]6= 9:)喞՗pͯh} ,6<~rU8N85mZ=h̽TSvKS+6}e^E摞h1EdUhgmLJ>}pqe`9ݒ02xCF&cl\ Lyk)(0IZj3{cbp3tn[,i-[p2|ـlm0#J|v WG n}+mvzꟗ~b]Q2;qaIvdZ੄"q!4]]o&i;gI|¾rv5Qdm6Mr;aYʻ9d—.:::SHuT*aYN|s @ nNa 0@ 0dl7AoAf"$|Ĩ_y ۂ=ոyH-_ƞX 6ѐcE:<8`8gBlSn'?Xw:gl_wD,|)0Gԑ;^Cmbf"f|8dI?8|afϗvJ/yq)jCu%ѪpXjׁ;Շu#YYJtuƲST +~\R7*R.6έp\AQ8n_a%r_~job_ږpatt(IgNfwƕq:[nוؖ٬W +GͰxbL +G%d͙uQ3&7M  myCNiE))s{?}5K] K* *;i*_2oV8/pig\JD=W[6,4tr^ZYN)^o tQݵCcSD7!B"z2^|SC&vD\*`W[lxjALN rU`\njpdF嬟U+Dd yj@7vc|G9| <$y}J>)꒼T|!H>y14j͒OQ$M>hQɧN%r'YU!^ɧҀQ r 6d$[^f>,ŇSYK|%E\&kLX^]' |7?%S? d?rߛQ@( +3|⊑M+~s63a}Sku00$@Wy@jA +krHi05R|> ߵqAz݄LkǚkZ9 Lџf} .Q;uNnߊ7Ҍmq}|SWܮ !H#J +n%H[ l_ З8_YZ}&γiަb6?^>뎩`+"w=,. rTzhJ'vƭ W,50o$!*]O*@6c/Y*|Yt\`/F{bto8uY.=Q\/#>^Jj^Q,~ 3W~ jItc<h@+Cx@Pj}ْsh9'H]0V+[JN3~H2XQVK)jNJ;_)=.Y,{X\.XeS1~&lG!@VLPvy_u11t(8Trj s@'!W)b~<_Άa=;l'fx⼟-wowEy՜V5+zeYpT`eXrXT]vaKԚVl\$NT.m!7Vets\fZf[^nA-A؀2GDn'K43\#KpbyXւEJz[k)z"֠ese]4wgMԯ/%q[(*YÕżSSgb9~ٹ'urZ2o Ys#cߑ?x4C]K Z?E,bDY$Y{%8 y`YK(*PZݪ0z2^v:o[/6:s +X\kUK]VW#QEnv,ORr\%}NCq)Hx +T82魶;h6g;?)}{+|9|4㱟7[ BMŜo;E ;*ʽ,Y=W$#ҕ@D'XjJTa3|-/O5wΝ5k_몂*vWΧ|&ǩ/nhݗvryu[JNWduIdJ/dׅs wo-3L/zuՏl)OU#j$ڗ2guvoyu˩{o_;uG►;DfYŕN>&zҐ)乥;r-sfMx=`}uvWY]5۽'ʚ *{y;}WjY//_~vsz-b~AvڛfZuS0oP*Ha$ڕL[XJfq ,5~uMΫ͛GkpŀqK{ts--V/@//CO܀L;烓W]JGv:-@J"֕h3ǂ0 '7E\Y鼰%m%L84N\ؾ0ooY{Li3Ff =#yȤuFl&Y'oa\Ν,XQXQ =^z'8aO!7Xd6 ::[X; }T}l|z7m?Y2˙A )Ɛ}vj_\R?\!}Mqlڪ[u-8YCZϕwJ+q4lq 8flrV,$dw S v!?RR,|~Xt~ڜ!9W3OKK~PX$_[ÇlKc`j)3zWduYW p#Ȳ3VO2wOgnRT׊5<aMM*{\R(:Nϓ[zMGn.i+r23O I^/|CX+祮GG. ['Emr!3t=-*׷n:R]c>Թ@a_\*^Q)JEXzy`$daUps`eSaY +˧; uױH'{.z/>B>44 D^Cg:;.? <}谦sh;,PݹGHo4̮6T*>J_/p^Hr]Yc >ɫ3~(NocO/q*9D +?&£ 2<*K+@6s!iu^OF#4u8j>0Kbs{f:^9^ӋkT +Cu=vyS.ƛ`8#GBΫBVBE8xgㅳڽ8Z ,.](3yJkH V~48.0 U:#Oʠ N1`Vj2jvLObC=r:ܺ]]][|])t_p/gF^gvSv{|lEd +@zw̲KkANj66/], EXWVenҤ5Y%q$ B6-m6ZV5%٘ R[,%z,պz+*z0j6Nh+P +vۚI՗UW'SY\/u 64;ARY<͊A?r<68O[O܌j*,"Ptl48qwւhVkVWvVy_ L%/b*}` Sn}^~ݣ^)$9iSpNV1&/`^ui3ݒc\#bjP~Œ}^ Ln<>xtF +JMb + "*q3vR}X +։aKnt4^d㧈 b< pZ{n6ZNP`1W*XRH:W}97nͳ+|8+8f/"߀j[;|O2g!rYMCF($0$$$؍$$ m\>Iy#v:d{[&G(!T3NrPlGh&$5ck^X:<+=+] ! oJލ,m7c7V&|$ytR2>I%I78Ceٱw+I0Yi2+؞IOL~NB|.Yp[;yS7y7m;;:ɇe{3|O8GvT~6y ?I6#q9v,cNvF"JY%-`~Apj>$Qb |[Oi{|m7(pj)0 S@Sp瓵K]$qoZR|7'mF;=8~=k{o_ܔ5!܆}1!-6X.9wd +p"/w? oK>;F<99VLs4Mso2wM)&YN}K5\3VuD]e{dS&ȟwl} nL|P^{"!pMh# +\$i UɰO !|{D5/d|:Gϻ+=w#bA6p{4yp=~>uj~{ҹKxC[rw:;XT|ӝvy[p:vT7o~S{ԩlR474O0riA=Up7!Xq.Z>p\WUظWyB6j2{+'w]_ctʕ.Q@KM5еz'հ:VS%}?sO;zJU6R`Qw +=Ie^Ƽ&(|`Oe +S_5I1εdtأ_[E_{q6Ӌ*or*ASr$6;_drJePҮo_h 27?4My/pp3;kGya宅l.fi_]MɼDU5|W(* 5"5.c<.KFa-V;p +%' +w?ݬÝSr6G$ܰ~!-A04Ne ^CX{9_\ec~3 m,iSVNS'l,-)GiNN +m>z\W-eѬ>7O ;OwMyY~ʇDϮCƎ bMtqFaicz{RN+$KUcn$T.M;4/9ũŵ {vFgǬ[邙^\%ygR6?doI c.-6ˆnoB{n'orb(UZB;9 |x)SܨXٗ7Y*cp}afӇQF>N9nCmϙl!$_M0$ pټš+sޤBWoUnDrOR! /yB{Twֺ q~̻^֙ܨvLi^)ŠTedP$O$I_i|Mu_vS61Q7Da*:EbM:Tަ _1=naZ ,O!h>p4ֈ5j'UudYס/Ì瓐wZx~yo YY`r[Vꈒ9 s`^e[w /Le6?q^;me|?xJE#>fma)KcwMUgQ@}yj˶4/..39fc ^,n?4[C36M/gg4:Uw?y!"+nљmDz"b*{&˽ +LoltɳU \H&r$FOR{-l}_mP((s.H-L;K&FY~MݎkggloT{Vҥfw划Ka{l'ڞ5 Pʹč>B Cf@8@nx +^cFzxkZ\3v]|&aYOQ|-O^F8^oyOm߇ؗL 5c&14`+br_hNlŽ]wο|?wtO{Wpj=MGOךq`*rr%wJ2%@ZgY`vKC>; ፳ +B`5`ۉ?kgIm +X$h W[QFC?y)sZuV#|d|/qpir˸@\i="6i1w:MvD°l00T|[YӻJ4>ҍ| Q=@30ьq^lL&0HYEt 2*IN9u&߻ak? y~( xL~?)? 靌俼Nl3G-Z/a}Sd8?$L^,cTf$AF^ҼYP)$0Ō*I3V+I"D5)9l/, +O*ϊ6TBu9ݏS?pKW\\6Ypk;L$8\1?V>fA졛8̓%.z6/ҺEGv!yfjI%+7JvOuz&0-$ogJެ>NP'ޑQ#]bV?7ow$ZOUN>##Zz)0S=OײI^;_kqg]VToJY1z/fNxxKh˺yrg\VPוּbe~>Ut &D쯯L .cm r˶{,5|G?$]Mp˦6cY݊+\WufLApw=QJ +xЉb+@]%^[*qzn~k4uV`[OwE`1ӊͻKs룕')$.yi +@/0H|ӟ>NJR,bBnhw]Wvt8=kBp2peZh3U½I ,3Y \NDM1qOۣ72y csّMm|iC?ᦶM2G ܼGeNs*۵SkPgn|,]ain4e&;Qv/Yu4WY]m8.uxxe8:p=oCW͂kK5FswQBÅU,Y֪|}/FK>zZ_!DSjE}r +@1i?2~gn/㮝Ia}nVv' qh!4Y4)Y7+[M2-bp#B"S^gh +w!WcfZQqLELΖ; C g Os择7àQ'ν|;uIdmhz +r8~֚zO0k \} Tǁ(*zYamh'-Ʌ=QQe$bB {ubˬ[m+ 7)aX-]a}qWU'@!9Xkl#Rۄ m/!i'E$]>/  +Md,C ?dOI_Ч{q%>wX<7abJ#+ee'Ge:ҮIeqTdĚWEB3O|Q~npWwJq2~[sw7`VjH _g~UJ7 B p]S9z}X+YN[KRi)'( <!yAxH|wڷCn l-8Yn: >}Ƣ/iz7ccmO%iLjf= jaACA'{2=Ejuϭ(*/l-+Tܭ M9.\Yp^й0fy2]?}uI\Fc@lƹ)ފ_z9WǤ$t]薞}5iHq Uz#Gp\ټh6'1) V}.c&VP1vV:>=B!PQR%fXw:n2ñMR'ձ"Cڳo'֍*کMF֫d~qE2J`6g0!1g4('(h~fL|saC6UfuB+b6咠CUwGliԠHؘ čW:r:~80($ش3*+YC-i2wbsq^*{RFuܑ1f{5kFf/ ٭@gMvUwЖC0.Iࢇ*-LtQ٠iͷ}4'r1ŸTZcJpxY?=iwvS%J41X>4wnpoWv UV7T_p!cO +Ņ_ |{v[2Cj D͘3%Ϛ7ع|E;X! ړc$0 pm3hdhv / pů,rgR[7^Z^zZ#u +jwlm^>>S>'ړܥưIЇ*fyvP^#wGXGŠR.PwX9A;ՎZG&G Gh`٤eӃȺ1p-pJ+Aew*<ק]S0p1??!o{jOqAn/]17)u&vF{Ƣvog6۞u{?orRT$i5䕾oݟ>oչsQl9&iո c`\BFn>0R&:[WPz.s1j]d|5A/rE}r2Mm.k +EIU{:Bwzˣrqo#ԠIPU+SR^W/@yտ2уlnKKy X|+> :*4k1zqʈjDo.;B=vKI~(QfBQE/U||~'s9qa.0 /LJFLU aCo X GO9ILKF3En=7}ğQ9ntOv}'Z\&ȭCYPdLi pثS8`E`ߔ΀⿬#' t3K˹ q' :$2;;?P` 챗Ԟ1jigm>k/,^Eq1{.hOre7[_ᵰ:1Ȣ,F0{y?e,Fev|+"bdm_n# +^E>:>Nr[%~ Aq"v;AoѼMPO^vۋ^]$!xFIieJI~#'\p@y4`|rM'ށH{UM޷Vo~$劣 NIG  ^]b ^a|O> w/듟F;b~O@/0 U|jvzHiZFp(ܟ^D\<@E?Q?adzToc?v)KM /jbݩ)}f +8 '3O!;9]r^`0>jnw@ϦCpjOvd8t%)uç"ͱrΥs:TbKO1g=bbz:˪鐔h߫cHCIgm|6uz6[nmysQgKI=j9?ԎӅԁ}+=X쥤iIn`qZ#bj0 +<kaXӏfWCet~dcOvnhE?W֒d*ir=lƀ5 ftk+:57A/1600OjcsI(zGqu iZ=m?TVX~&#gտs.9!NsG.E8㇞.ttOGpn֯j n8ȹ|S]*L"MXTM*Τe ?}Ғ'GKmE_\!gsBW`^/wMأmr'%!e-=6*|u_P"J[2!c=3Kr/S"iac%saq"Viq]vСv1X9M-*׳OyPϾYɎ-Q9ܡU/(R^D > p<<8Ws{j$è3%7SŽ{|ULǺW͖ ͗kr|rJ"0* 42uz`N? AgՍ㰣yčC1v J+'^[/OA]>^c6c3뾺j'JڗmE%b""pʞ?]̈;wyx '9i} +^HZBL0Ž dfVLvzCzrs: &ʹ#UClBOAd֖FR(˜eF۴#˓56]Ųl᪉um0rJR=!AJ^^2'OSЗ0p8ȨySΨRpQ^&=ȅao}K:a:-Xw+9 R0ݨm 7G"$;T +!3ỐسL44;:? C +/Tq=vд$ wWyr닳ɭ&z]E}JVѲn`2RZ'ppXcre\X2"y9_NzMbF#;=GynaQB7yOm2~ݱhXI2|hBTMd3s_宬~bHK#4&Y,xEP&tWyri>m",A~wr7ɨV%fƓ\H4YOwpJld;,CBOc H= jc?:khPA3[e0guy9UrUE-4yARyɮK8듙iO:+o +Ýad>[R'jN|z ^'+[^ٰvWH_`"sf)[i[I /24;Uz|Q{;lH>x:S +oӲ"Pmʼnҡ&2 +'?rӧzX}XNqƖ\!/ڨ`g +WGK\C.־> p4f}\{[oʭG;Bz ;U9G++Z+XetjpLJUB:VK}KG(mjaMݻq_]m]uI*c?;^ k{;Zn۵sZaG-A*4Mֵq#n\9QrOJn!|ClCH_ 0$JF:He%N[LCK(PI*H&7hKZWw<]xSڔCB/E^7u1 0Z_HѺFTΈvԕd+ܜZ$c +}e +sQ}͑_'c^ƻfŪ쫊sJʟLVF hPH 4zl_8_`t/"c(pƏr Lr0gE=i-`Ҭ2{EEŶ@^8Vcp=2'X.fNYP(?I,?LSc`q| +p}gG(E"R"% z.tLƍ"Jl׬f/OG`-ɫ}4knk;yAjQ8j?ydBvtN9 0IA3eB31E'ClUK3vk/iΎޅųJ8Sb!~/%3*2I2.qᤜ䓁ibElUtmv^E^G񞋫uxA=ףnwݫ"d9uhyUE럼lp+2[m˿M#(c?/ڟ7F7pkxm4Pj!뭋%Mke7_/_S`wa'_~B^rSPVjTx%y 3.Ճ>V\Ƭ;FNi|/k;? +AUR_j=o)CnKS "eu+*Sk ZFDlKuN?$o'K>4$*_k(w˪M gP,~]u 뼷e~}AbΒsA byks 5MWotp^FcY? \Wuݜ o|f5nZٸ-\ IXIi+EVU8B6J]9fQoncVvw%D[yaf2$[@Xq:^3]gZAp O(^>&yA36:.ed y-e}Y8<Of~XU`Jgee0K)` _|^$~&/N*]30SZY菺~ +5}ƨÀPV?L?EGNs"ۨ[N" b~z6/TdӚ]2VW%Gvޟb9U6F~|5ln3 C$.~T5{ k,Z(d-`nc\ ((}@<ԣ\=naQeR(_,qRƸqF^?jpm%> U{:67pkY?x暘,L| vڥ3h!8JbTΦ)}oAY:ϔ"/p6,)u:~Y]Uuxg{ vٕ=؝͗%A\Ϸ,SWìd2;hѧqc5vWq:$t@jtrpH*ב>]^]k]t&Y/׸7G"jͷ$977ڬ`j\-I% Tk]JpiG}OZk?FG9'Z4w`R 9_TM:֥c9"k}ժlˬp`v`U~ݬnGޤqi^QÒ穳b}OEYLvkEsY)#/52R&Yz.tΔ%NɆ^+>}/:صd$MH6Wֲi.:ޓXтQS$3YSQ ]QJF^|PVx#I[KoMJ3GYO0:"6íy/ؘE?Sh%oY;frgq'P d$Tgj9V98rSDaQ{E^]:=Y9,iWR(祦UD@XEq0#͹t()+/ё5d)&dŀLOG )jkleu*L2u񽕕U,l%IQs܋x4$>@l tlIq󠶻ENS?':b@j +Ҧ:-VmXV;ikqZ,$^K(D4y-PL:7hOxL4+#@) )|)]m⸮&U4"nLymg9VJe ȋc!\, Lς'9\glقȟEy2%-L%!Z~ND=j+=OT)ޣƈ7͉W}C%A;Ď-wBEUMoZjk&sp/r ggjc:0 O0"+ +_ҵz@mjyx=:`ʰd{)V <+e#F@Ul'A5!d\m)N@9G YuR\RKW \Ky`b \ /՘WTWO0_މv.?w z01ԙ8v.?1PSHrz, DyXރ\5 jw^;Bsݳt{йנ3lޝw;%~/_D,L |0( 'v.i SAWn"7k<Ԡ:^c r3w[#I◻iu'/ab IűMpݢʣ9'/&(dcɗ܆Ʋi6A& bF6앜!٦'*lc#Q@jmnӚ٭<7]o,ڣ!AQ.tyuCY=jAҮr_>?`)1c]ʞVڗ\2Rg,S1˯&F?=rKk6)AϤ́7@Fw*aKYרr^GjyLOe-mV^Sѻ +(Na*EQ~^[ avܠ/f{ 9΢iG#*+-`,@nnD8mԪ +wż. + a[o G4 w}࠰U7x+x3 ?c}h/::HI:B|hQ JJ8E~h@vu+"8@V  7 PК(\p2+e{.:5A@ +"a%vT=`ӲjF>ʈбcSw/aV6u W~2x.r c=9}|+p#p~/~@W &aۢjލto!uҢ2Dž:#WӯA:aZ,@` @@vD ɶHJReZ_Xs% ۔LZ :&w8vzE d,WO&ڳ @ݟT[B 1P5P0}~J+"0,ΰg_wf>-KW2,4 +ُ9J20G<ȕ,v %/I&ϨE3,?n}T-A{}r +Q^joq+8^gXe#iXol¬R +'(֯6ӛ< >*Շ=7}(\и%~_61셓#ss_ F3W].O#,+>ݽ[ޣb簻z[IU֮1/W^eg|*PxJ5">v>s$NwL*eu^_jϠY ;G(qs? +B8cRe?:歘;ېݳ9p]5?V^ũ㵖/Ɨ×-񯅥wvgQRr81/crZtbc{!ii,WMWJZ}H;YN@~v+,N2&/f2aB((w$C8̣ƼQ<3îs鮚aϨR +vIѽ44= |aLQŵʭCdZ2鋅rD5>]|ϛA83k-p:c:Ūa;Hy2 yRtea,=o!O{^}{E6^%}3Z&¤Q$57ny[*zlJFi|n;4/vװNH_U}.d~m˹wrS j`Ƭ7< Y4l՟M +VDxDq@Vޮ,]GGOGo^D>.=2bL~_Ͱfۚ/&ŋŲE^Oc6ًEW6W~ųcX z)"+}U>xRlYn^ _9#3؎9yy^غXP8:<rG}hXtձm e(~ 6nsШKߓa]/odiwjYԵuvD;\|~.7DpJ7Dj1pvJpv,(o8-1raEƠcY4~1~3*k +CZ@|~&wE'P xn}O^[_(sqmG}YsD7{~bX,kg_ %Mv؍d9r%(LZ|x\7Tf 95̛щ&CiPλyD},_>iɞK5+ӵ«*W ^R%CeI/Z"ObO_fFو`Im=xdw#g`:Ga"GAh&;rlܫy%PW-H\Zro[e0q/_D+Z6b/2yK/̜#\_t2ѵV6bߓ3e=v,}l*̼:h0v0lE C^\O"&C;>6ѽy.X<Ղrdu%}oXWb4<;4Qq IsĹ>);57fny7]|c'Įt9j6/[.ϙ6&4> +qs,%[VI'CJX^óPjz HA\#:PvD6,4f}9 7|1-މ(Դ>ǝ,!N\7LN/v~/7ځ% wkHy"ϐ'5r>p<cN09eVrfRZ5e_l&`AtP&zD=T7 hn![pT5, +A::LkP ₒJ!ZM$ B'C1 qbO y`ƬxvRi'üRIU vYbL)~A)ڭAuƎ)Uڪx$Wz.P㷽=mbd1;3 B'h=|E@ E+,V nX Zm_,UD1o2ݐ 5yځ1|ujY܅{ y4g%z= RDj\DsGiOuz]ā=zkʭn[I˚w.ݹeoEt4* prh()8y8vk?µfwwEirOTkTz\)eYˇ ++cZJswD +Mib:` R*'*(,tR\a9T/Zmai Y`ee5lsTEBBRouqEwlڅƯ sea` b*ZT 匕J +<$7z.am=B=g~zZbbNDAWDCR&WTf3@ +rCܺ{u7Ylb];t.zr+\Za+g/m`t~de +\p^ _\2_xϺg+*{*S +},rȸ60;2B&S~ {|E!>9O@lYÊ2چv/%i( U>gԬLE3[D5?*^׮>W<N\ͩz*-g_2_pLڏ ,o{D:^7yy,={%$wJxʑ9U뇨Wjcmmog ~k=8gd]%ED7ڋ G2Aey~񯍿J;] ɇS`fR/eR99Pt˝W?5m68_y޷vHIˏS^DD(N[!yk%]djρޕ.4/M_=k_jߴ.Ǐȭy+i߶Ҝe~y >Ղ(YT  +y}i~QIҰ{Xnn>YM>tI:F8;G +_<5i~y:D-;bW0&ODmB(&k|5~\f2a)^l;^l wQ}OؗX/:.:s\كC +_e9H}lRUqlǾϷ(?zy 2E/ Lf=LLNxZdK?W0;l}rHl pJ" )WT)-Pӊ_d~O0]WLG>Tk7]&S X΁t5m ̺ߍzb:R]d,}֭7Cf_3{ۯYjMfjR2P|භ?♸-m>owN~(9M\=谭!eX%%:M_&s֡/.GS`=uyCHn?k];R,_JC (WSj,Tt]#úXٚќ'C .։k6cQk~>Q58W̊Q1vrbi=2ZV?闛}j#=3OECQ[UCEsg 4}=1:-ta?}Ψ+c+`W3%?v~/8ff@MpchMyn}ړ²}L÷Y}wJ[ 2ShW)xn׶ v36[nE-i5:j"?cϬ/I^v,١VPR뛍T{.Z%UɣpX/)WSo-Hd{ +_Qy[û[89uzvߥAKS}/[%Į\~*3^V{-/JV6y~+<|uakΫlU`S6)q6BtKDkџK09oDWw].- ׆ }5F2ۭϛϗ,yiSU6E}vŖD3Ei%*^cK1zeXRYy,PS&_^p+qE[SW-6Q)Ou9!^tX9'JrLO҆>Ľ5ۼ(XhK_']H$ 8o6 YX̬Fu~E{ы2B i/GV`֞;>S/oOxQ 5y*r5ΤFqzq +_⼫`_ȨRe20g ^t)- B3/?ȭȭ!cqyμ*i\ohkewg'*k5*O H~{C v⇍ȍ;ɝ}ppl:#^L4-2.7::dF6x͢N6K 18n`": _OǼPtEeդL@)ߺ8Hm7s]\}za8oxSjR:S5O%>H Zv(pEaM4FtF5SF ĀPΤō+eDc6QSMN&3g*FpLZ惪&r7Vq_#|B 1w"{.6:7h +)"JFcL?2ftX6wGo8X]Iڮ1mFI-u&/˻ SbU{9*/U&[esc1l`'JU[2 aeG>xAC~0 YvLtߐܡ_r̈Q-w?=7yK2oE*ig^:H6}!S*R^4\(""Nz_xa:۾ڝV;g8u ":m2) 1ѯ&6,(cA-a矫^ ~ ,J9)XTVh;GUwP ǯ~2,tn>wS~1GH595 U8-d6?'.7RBnY [b]S ?`rM-{{ژlZie|nM6ۤi\riҬ5$kns.zjS}\ۤ?Cp~<9rkwqmJa9t+umhQ (+Z]vJϟ;Z_bX?NV**+=i,Ţ2شH] +PPS_ +)J-9BNlIw%[;YdLiU_^6KGZ׸ r{|\XP]~ Ws5}] D;I, 1б8c䰛iHivCK+v>t_hw~9vi'fQq}{}nt'j_zb8+G4+G[ gTWgPGܶG( 4=n{,t_Ov~n˕o\ s*0Ǐ_+~cw_5 +(?G%t5aVYԡ̋=*7|ܾvnZ!WSvǰ ǼM:wЍ7VeSX|g0Iz]ap_C -UBOX,cY6}ٻ='<]]iƲlDn9Z!{I;̼ȃIU6Uvkk8j3(K&QEDnQ̵QeiWiՙ7vkהo3uWLEWhPs/&YhT'kxLW^t@ԯ~(4̩\+ՙQ5Ytqt=ۛ,0Fo|* -M!>~|qĶkx]T~kz{f+!@*aПkű3k΂fS=|}L>Z1>1JaJzUv8mzg/Q3Zl[v i g{'kgo§.x`7}UGɄ&! &qkb.50RmhH >W EdY[kX۞|)ߘ OZehe;u5˧\ + +rmRp1 oŸ%hkD¨_a;q/yuB\yϞjMǷPs~&7dkFQ+g<4^xld]UwYSOi᳝pm\~?W/#ݡS^ Y#\8XyQ['ֺK+O>xH١~.q>Yf{I^)wPoWaYb:;]AޔIQjcu+e_у7<6Y%ac>0wkzR TDdJ:; P7—fw<}[f:Vͭ]d0ak1I n9Zqٱɫ~fI~g>5&Q`| +QfhljTŷY< Tex1$W4'CA}>b{EƲzga +~J?%[<3:v)T596bM}ĒtZ>'&%twߴP +Tli5J] +1,ht)ٌ/YM-P͓ &>ў"nWa m'36j^0$g@i;:_o_6׈:Ya +9`N|59m~} abظ¶Pr-ٛ%q"HĭCFjǡI3@ wwwD! "{ +tGQ*FQ "fE1xg/RY̋SQCmMN/, Xluyvi(qUgNKVMh-5vc`8aM\"94X6Lz@';[ВSPjd#OֻJ,'EoJ6n] l y<9_L >#B%l^Oh;zd[ݾNy\ORl<ރF-B2;F6!YX"4W|oM81WF YCOA8F9!EB&4{$Z='x㝾VEPjyT K}+ X\:{0)i('o4ļUF;yU:zނ^-+#WμIz +RaD{XZxMiHH> :1G@@jA>`2S͋kKw]5L +.q,ajdJ e9,<.-u|Q2}q>}+x,8J\eXαpNu}2w1Chu翭K~:9GZB+6=:ac 'W!Vʓ"[dঙJx\ޯ { -SCtUt1Rq)9%)Sf?P*}#NjȞ1H-Dfv~,h>}5hnREF)^2Y!O agx}RM>/@KRK628j 0~%X!d~urG~RwGs.H OޡOj]Jl{Yw+$Xr zNDU3DwDDj3sNͺHIM n |LMW=rr.! +V83D#sUgͷ`'-o_LUxv6l]}EpVwwY2:\9 AR\. +fOl@tAstLM3  v{pMq5q"@,w-|]{^|QuSf8’>t (޹^(ʙ r-@y`AЧV$3=\@+|||U,JHwre㍔nz'AR[3zSpM5@K h:׀)OG0 * ]q̹-6) t!o ] WTs']Mٚps*UO[ϸ>C'eRZn8|uo4As9캱 gp;|)u<.jAM?lPOT:nWг=w\_?+%\aۛ'[n|D`@u Mp흁Xn"@dh-Drhk,CЯ>;ݲǾaZ)-=9Ѹud.z6mZƷl+_#,Z_ܫg5~zjԧaJUB? +n5/Rÿ)AQTMae:E?cwpV6>\FxNiױ4s##b:-:U!L]۴xOdw<^~n{7r .i?#|/n]W깟0BhWf{.Oh˂=ŨOm;7|:BKX毅VM;oƙݣ lvfyb&5<+;:nV,OzVk B:z8B.Njy.Tb64&iaM|o7/GЀ#p)|)ׂ]|+kv:fY5o}ENԯ@t gN#ќLuFXqWxv?5·s Emf{4#ycb9/ ϫ-XVw8n +i6*S;Xe jK/vO_νѾqF9wgw¾Hp$azz5 +UȪ݅Ĭc򦃞S^_z`lnA*2FzM>Z¸w*wjO|>UW_v0Y@>vJ>>6kj3O5Vl)]l=[z.L +I6%lSwܲk)? r4L&mݦƍ3 XImѕ|lg|?-h9m񈁗X͞>]T]#z}chrm;k766JG.]~&2fGW &S )gbc0)t uHXՖR6 J'Ԉk뚴'BIN8V*[/d[?X~϶5d=rTwT|.S]<[gp:ڍL>eQ`mJt/ Vc?Ŭmm@&ۆ aOiZu'lpח]airΧ)uߦ$եS9M=o)+R)9uhSjNɂ~ύԟ?]~O-uFXB}Yv3S`:z~[{f7q0stL^Z:J]:lSrjjFtf}䔺fDf@=H%\9A2E^5O/e.8lu<~ԫ* qEm<ļ2}Ƭ6I0Z ^:۩ya~|-ٹ4CD Ӳ1jaUVYm̘,|)C,睈@TA?VDZ*6#!NżW;YK+kʯ-# ^۵z ywb}VƝ爯nY>vWmΥcT˞Enj*0(ȓ;>0kIN~2cqN=vmīluRB8.d[fMvۑ9!:̶W*R`)u;`ZeR\qRza8wisNVoYo>rkM.]s|Q"$)oF UUmdmZC ]V}M#žRK\b؛aH{>'n?(Ezf&$d~lv8jNk10ރ^>R"Vu:8qJ)?PCRLsSڔםҁ;s2 WeؠAQeaKe(GeI ?@Q*dOQCr( cX#C m +WTj-mk}4qDqnci0,6f8gQK^}Zx(5KTUhrF,427tBL+bő6'~ulեr#A ~1WLwώSYQd,g q>̭) @plZ|'[< )|Q]~Ejy$shҪi=J竓{:/ 줘YcnTV+"FG[Gc4%hё {RTo"쌬 L@rGϦٰ=CiL6q> +WUO\5Xj`'@MIpV@U PN5L Ѹu{]3vŌ)u~̱iRQ*cT t++wT8m@ENf |*vVH,=Pia8X*R1v #* g'wPij>L_H@.ŭ@!] E')xS* +N(0G}` )*A[Jp46b: 3"alN6k<2ɱ~I=wCp7OCA`ѺVARŊmbq'rx5Eݽ|:O~. J ;$P=@ǟL'~B.A n1rcTwߑne½l4Qmjs!|;QI,}6NΖw{VIق'_3p CI ŞHWrq]h=y"|Od@Tn:Qv('| s' ҁ_ 4$~Y}#> dX'㠶>&o'3P +~-ЫK4=L9} }Ba`j#`[VsBqœ_QuLn m_ +mZfT_[pr^jrݽ̥kMQzia'fP%|o'^`pZ/V0or9oH$s[u-6Y݌,(F^>Ow1ˆ޶&&lom8Fȅ>C< ?%\p$0=z|v.KcLfv;TMdG1>9 v5'w{Z9 fsnv]VVV}bs>4I5.Y>o0O#!F/|29͊"zS7iQ_1ۛbwSR)GxKn '(%:&:K;4֡Ђzh:zite{A2dtOijC9M}]nSճEtÑvIv? }܂]Y>U d^!L:Ѫdþ[v4]1Oh`hbfapӺ< Q*цzY%; WaJZN %wdCaDž~e=LwCV ڳw ƴd\MwzfMQ\6πS >u63ڴ{OZ?j-SCTQ[{U1w_W)#_y,w9+#3/*ݗ2uu|&x; gA/~*69%8zX-fSOFWu׵Җ̹sP[`XCV r B:9Y%UʔlUW5J]M[/AV<'/U(Yo]I͏smmƔr[Zil"!6mS%/ٛ3/C/qA_]͒X=&Y xf9{{-Ҟy|zC{\̥]I 8V߭ʳ:⛹?,Jbdc_ z}s/`M"dӬtmG\6 +yr=~6]gaWýl}~;*> +(rcfDKHސ\aFN̕VW!2>mUUe+KVٸqDdbɋ%R 0l[Ԛ24`\L{f*2YONѺV3k-e[X_}OZ]ͬw2yV)sXóu6XLg=3&F ?`Hq~zyk.q9w'wPmu5n'ﱛMI + +W$N3\JBq&7py˵ݘÈԒ؁%sAvE#4}:ͯs=!7PC';!v&0FYĹ~͟p`Գq4jA7[D9ߔTeOq6n +%D|*bɑ30v8Nb+G IL_8o52 ;d6=~1/}k>uOzf`iLY vZP + .a[c|e.Qa}ߜL:}kq3g^SJͥ2,lwy=SKiö+^izdlJ7\Q]"eG^b<#nVO)l3E̶w*pL3S!]Jp~ +ׂw5و|ٛߺzoճ$r`n:(i\!s CVN$(Qp/xY=l[LjJs?X|_!')FE^c'=yy w9&עs 8 n7w?}جz37UlcK71Y;9sW +;&Ucɳj̜h(7/Vq-p:Y+iJ7J|>,ֽjtI(:ْnd+LTrvYhCU*CnZ?}TVg^e ro`?*1e)a57pJso+թ⼸E87Ƞyr#wE䜜W\vgR-F/kv[Ҵy7CR᛬yEvu OqgU^L1&B8F2?PR~_?yXrs2%D $"\fxL&}YigHO(ᮐzfD]adž^z^e@nZY\*FqE:4ls|h[,Eg9z=X}COBK1'2nħkS-NQ`e-W|(%{f򱩁I +'>  +a#mSx7WO_+3Ŷh^eu:o7};,9/2U'oO@2 2Fa+ĹQmsl))@ifs.wdPLW NLdӪXڝ5\X)}!O_ٌNՓ*?'2U@c rx4Ce9@[3. y69:y @// &vi-ioNpz[b/<m?zK@ѓC~ g/BxY0wFC{+هc&'NpR\9~Wi?>*4تwm)"a!L]Zh^^\b^zl :9:s<*fRpՌځ]Zƾ;ݨ|vm +,2Wo |M5̟Fn¾qo8^óu}LO`F +Pr/ske^ʅ0zH8'r>i$^}{щhߔԡO \Yߣ?Q&x|'e}eU} ; +5ˆ<7-0qbf0em.KO⁁Y#|8CRj^)lO6)Q+PGo܋lARң]x:~1[Gфt{=֟LMc4FH - SPi{.J/CҾD 蚥Xqu{滤plSM= ǥWk*^ (ׯNbQ\l r3`A_g~_z޴Z9\{v:6t]>xn|MJ>UӞj^V_:N0 4b3~8ra:\*A$OX:Iva8nd8p鱏}AxrFJxs՚cO߄;xՂOٴ]܇qWu Q줕[\uuKh?ht(݆ \^55cdr3m.ŞЊmnbڢMGUO? ZĒj<Ԫ zz,t`P},غRj̖}uS`͕;l\ږ !}R( w~y$0RjrUuCinٖެD<3$*0:ʢo0m3*Vz#?TP2]Wk\1`u˪TL5 ٽhqo,qx!mS +} BLMlLpFWg AiXptut`ճ`ECZ}x58nT^~^V׉:̮eZ[iJ2,:inバ$5LTKg8B6<ftNݲ>T}ZhQ 5bjf䩛WK~`eV>VW>B*^Ts+{*h+K+GE<u|؇&3*G9cџzP@&l0ֶDlWmyZrǚ{Ik\}:4NՔ:rHEkB=u&xӼ!<_z?8hbahbð`.[m,H,eP&oV'[[>%(쾤+U*{VxZ= 8Q$H8|.Eh/W7:YΊzMvOSֹ<^,>I$؋S`zZ \OvzVȼ:R#MZiݨD8ɗ W_;\'(̷*/iM46t}ٙ;;R9C#~ӜhjR4'uQIfٟΪ~~Ot\86'N z w2[no&}Ŵ5?YB=y1O#9nRB1 +iL_,G|o+bD_|-g wh7kKt߁SjCͤɽ,49 ~gNV7QsB=(+:9WlRk]*NyZ(*%Oe1d:cȬbHX:)ć!p;]y7:z]' H=FˏCtN7s{}^ j+Jļ -'ߚxi㥹<,P}TV[DۮlP"*6/&Des(:}zh~ ('a"7fgN ގ+ o})\q8ðR$ < ɫ_eG[nRstrA[Y/̗᷻E/s[>1_&c#MҜzYJ-Qu;35fDE$oʞAOR \pU~,zAY$sx\dJe,娑YnTvTʡyVRMmVa [oqU;8x:hb(+.e荜 R +cFiq}Dn%!UH{8_ +AqX-͐^!Qm M4*JUsf;wrs||fJ֏go3%k<:vn׼Qc;cRa'- +Ͳ&$XQ&"Pl$Kl܋m.6ZQ:n".C)yeY?P;NEYf]!~9geRȤZv&lNj3\ɼ8dr*1_ +գwSSRFa^WeتFrk¤n+"g\\a~7'yǹ::(tsv +Ƿ\ IJYot?U0U p 1>Y0JX `86 +; _^Wݣ^v":+Ѣ +aJh=weާV;]~*ZzI͟ҍě+(cd6A)2 N=P&5(W(s)e2RP\uK`v-:-Kj;XfrH<4򸎼((T0qKxp*lCkZ?hU Ag`]$@E LJ`+ d>J:f3@p%oR&S“c6敠:JrseuTʰ:Sm +2dٲ[tou,F1AHp*&*lj2ԼT,e䯰R򻜀}R(Vjv7ݞ=6k)6op5.b#!6 {)\dfT.S PQ*Pi]Zo ϳԜGů  ׀!?a =N88@܎. ɲDcq^oQ|T13c4rHyV;TҰ# +ȿߏD*+!6Ꮜ[=ڀe#@͆7@2y5"t.83ˀN>EoXf+Jt~2z*׷K-AnWO?7*+~KJxg,Lu=9c.[jժm +P-óC$V>8Gbj-!OR9oQ7Mv1g~ ʫ~@pMmH"?a߶^dߌX>kL>+=m(m +7Aڵ5KLzoPӾ +Ǔ;HK?,?ͼZ?9WetnpKtSW/8oG>'= ~`'z/:I;ioZ¦3Q/(TR|bB'wkc +E>{^c-']c8[W;\p_,v#$تmQG4+;8{U;xܖ~\\Dԑt3ٔ(HNyLTŶmSFQE +EQ/+wɳV|Gl7\8🬁C+ ~ lݗû#W61fmt=zksϗ x, }'>2S),aEr:b;q8~ Mc,SACݾ<>a_Q%%e x8}d:oQ33k^h=f\ԮN\Y?x\uFUWK7._%7?y? 8v):zY uCǣ0f0j1=F!͟15<ɾ? wZ cV+u9/;d=sjDK?Pպd,j +T;5Zm.yr=mrEFNpN|g/Y6gr< =w{lM+mѮ֖yu6rSy2lLƶT!'c9CMW(Z5"s'wL2B>~{>1dEl==VPA+YQ . d֛µǁ,?#i}qO6¶83Ut+^`x'Fr܃˜۸t z@,|1(F/=j +p5|ibP'˷T3¯/n*NK׍\QwCwX+$M89cT +JgFbX䇥JONZ 2JNLp08f"7',N +!M{IӮ@s]Mbk9$׹s'yMC=G+I()~>p:2܃UUYZ@L ژBqEՎKwۊ:N1UH/We%vFŬE"]kw~jk=0'k쓮^5kt~rwrJz1qRW3#NKжOKskv6zӃ6B}]ʵ,[SVA,*5l.v3Q,߲>ڞWsTU:d{Ľŋxo4 +yؼ9`B"A;djG.c:~Yq {eoinں1_ 5Z %)@A8{.N!(.sA=^s@ꊿn .C5s 8 V`|+?G#Nrb22fQ4d6vv 꾫.aO-E؆t'7lh*űwdrg\{:Ӯcj٠I:H8P$t 1k| >?=ЖО_~8i2Inǎ{ˌ7y*9^յ7c|͓({R}ǷU`_;].[i´29DnoY//m9zgz&̲4$CC~gn3|zٗә3jE}V44#0]6{oQbfqXT:\ t9xkA +Kً H+Z4yL+j\5M-ih?=XkcHpW]'һ~`Q[تlM6\ـ,*ntnGx~xu I#@ü4bH=g{U'h>&V/v LP=$>MA t}^ |E#ُVҀֹG{e≲E?]# [(U+AEZ<A4-dF4oѓ7* $Oߞրܵܓ@.E9)@N#xZeXɷ:hgm(oi1cddx"]2~vB|_<ހN> Jp:ɥzLPzہjo;bxJ+:<\ +#XʴGw{1/Y;@?-__v /f:A|/$ȷ,ȷM ymN{YZ]9Su2-Z8[F/8<wߠ0BGnp?`<۲XmYr,.G7 +&X6V\mMVٟsJY!7ð#>}:]PXnkXn?8*ڥ((;mch/+nD`6i d}VŒNɫ,+KÊH𘍟:ĖOGNǍ@xG_yW.gү[mgeQd)΁e5fPL/V`)oLʐRB_ڈnʭd7KP̬n_m3AXSrV +rU J(\%d P(To͝RI' +ě(Yyj7JѰs(RJP%X9{R{݆KfWJ$ORn{mm{M|VGGzp rZ5&뵵pg#KQ,.䓧_l`2[vu޴.RtA[5EpKc) ܖCBzӛZ5>[VI($YmL=E㕠fN%LYeyM%bĶ"`X 7Z;#d;'W'wVMnXL +3-Ȋ. a`!vf!Jp"X}L^voVw"ʥl1õ-ר{sW :˛SKRvGD;+Ӡt4\).L/WJ!Uɵ>djh3nn|4~ti'0-;;ٔHizTVEb +ޯkrF\r]֢09qs>׌ӕ |Hiu= +dq܎7 Qv9&@+jU +^9]m nP.obڨJť2$x42欴tm{) @p*]-WrO~m!@qCMjqUr(1l/`NN\1luKpLEDJ-NGax6y%xl|{y]tm&`ynP6ZGyhiqF +Ulu\+ j :7K  /@( r [O;@ؼt'u ;vyWp7V']R1wJu_*47)NQ@v?|hqm'_]M@V0'A}`$qN# Ȓ & @8)yK~(&0 .yrJ1'Y<ɯ-aPNNzq,CJUΟz!ICƫuPP&,"L({p5 [$\h7T{ph TΌpsLm@l "6ֿgT.x|3uoD'@oWM(:j n!ݑ4îgeW3)&Xs_ +dә\ Sp. +g'P+Wʓ;}߉BSgtF<`F`pIf5.f]3׀0[o`]}}GU"^ϡb;]xO2$?RRߪ ?D܏^((L)(L(#Σ +(\PGPp5-BoDlyzH༆FѓCLwp̷+6]%¢t4ߠ4J,Pn%p*+bRK??;dW?R~~ܶwwD7/r+@X7@x 2Hz BgLk^_ Z_Vѽj![^ J5Ҿ7s$izYi'nѮGsjG49TN;ן;NHn~aofRzѴkwR_^<9G?9qptag=tItH嶇s jC;~#\*'/`Yy 쑿`W@o~x]I'~3Cewk1a\l\|g3L@:k-|>X,qua5 fԆQEUt&ǥ{Yr1P+"90(u4kc_C*步o@`s#fT~ ٤wSn>M6ssv/` OhrC?2Ia]d) ,[ڄڧb˱Yc;e{׮[)1ﵾs>4<}={/Tú1]RDrNm趌W_nP.>/}񣎞Kܫ{\,PIۏI-gSMjH ZfЮNL$Faoܢ2C[Zv-?syk K&7_j8KNԎjݸ]XX{c+_xL mZf%0yŦ#Fs^bGާf_{_=ykAX۝{jU2.i_d:jV.:VaPivvIoQaD}c0wt$mrpN?ý^mng -u5Q'^b*r::!H@ڝ_ ĥ*a<sgnue))1A[gkgSF{ͳ!Mx`B*q@z~5OT6ҮSuV~3ji%5Y^9QU,+>=_Qyz^w3c~-}ځLmuEu-{}\Ng]%g'H- -՝s#)|U>hUY:Ye68ǦKcK +I[.as_ ;[FaLI仙xz!htJՇFvɉ66Uoh\?),Ǖ{Dv5+[~dl9;*RD5U)$Ru]G~G?POjen.b{C,_ _]axHOcNNZ8n֫fb(٭8aӕoŞ' ѲHI!tZX)Th,MeBoeWF$Kq@ r/SGSwT;TgI:n6_i^%Y͝Z!͎UҮV@-X'aJ # +sq"qnjm&ݰ|ݰfIpx}1Onc2C,U+/y^b-+ Fw}9l jlZn%8k"}tKoSp\bx>VE.J8Oga2BR6J>S˼MF?[t t+Ӽ'jiWڝ]rAp*ow"ЖR*@8d0TUXƴZe綛:#ǻs0{4:ܩr.BCQٺ!5Unr: +[CkQV(\bX]XAlҭhdӧbJ%y3{ouڅGV%1.=t;B/Eʩּ9wr CIxZ❪J uTܹ/'P0Nr|hrRõNX%yH@K匽YUf U = $(.l]Π|(?-9\hQ5T7 {qvSҍQ"K׎y3o;8K+ ʔCN.QLkđ5yeĥš-hUι +Unѥ\.L7j=xf[E9ѡd  :ԥͱ#cέخ uÐd#I^뱩lEnoRcԚ<.TMH`TkQVGf A 4TrfvseνONOZ/?=iףQN]{%{T'i gma5=yiF1[*iv>.iΦˈ"F6흐k jv?l>҇67' #FpxSe +IUg"XWԣc r rFe)²^Zgvsj7 fv+JMK̽Ae5$fgķS ׽~ +:EЊ 5AlB &j$Ra@ ^=tuU^1<ԉacRӿ rt'߿ ݦ{u#!-)|iynn*Me*(7"krXN|vsI܋4@EaFH T.@G8As`1J0: V@J(:oھEnږSɉܠ:VXvwA2yyR jlXY``/$ 'yklfORh[ԸqkP7aڒEOG0;6Sk#h̔ Ve'|@$c `!c vSxs3x}F`NVMaK}]m~f]eܒ.qP2cho0^ @\{: SWuJp".L/L W@쫙'/Y9AD b@$ N9~9y^+d{ +;SFZ}tTMSĻ%xN_WOzZ-TNo4 &51Hp +U +a PTTF.m'x|%W6Zljlo=JXϟ0CL+1o _7s[@%@FНZ!h;#&ݘFnZ8Y k :}zL_"wNi}Q/+9K_0\O k}tKdN)d*`0V RdCX;fO)E endstream endobj 41 0 obj <>stream +*]4ș."_徯' )nJo[[q$`/bЛ +&x3P\P&߫ROB=mG{eVgU& 9٥&ᴂ2r?ߎ߿bU +.U-QpmwAA_AqNAqbAxhA1F2OQD瓏5[}n}~%݊'-Wz@o&6i@Li % LЇdiu}[CXy>?B zӹ"''Ï*dG#v$F܆{1!n9>78G o lYBUQ=}njm\ftg$؆B}Ʌ緡 Y'?КېȽ1K/ia_m3@{dȁ_#+uo94ֶ/'γ(j!m2mQkn]W阩./gcQZ~ ]:~rm<3{ =fOnSO\nFCZߴ | |t/SEmU Xڋ|̍44i䣎n!4iى˼q)-ޣtmITs8k/xsM6ྙ?05k~aз+~{ٮ(!+x̕#O`􄍆jANN>-A]뛤T}NdU97S*x02H:namy#aP騤o| +ZWN]#no4J!M.z`86m {59ߍֲ?jogYj) Af"m UiӢd1l +7:X]O?>[Λ!4^ Tlڲxln(_5~m3h:^;l?m.(-k+-m/6Y>ߠdԆDý-%xwTxʵ GvV/ߢ=ΰ<R{J#ZjCHuz9;@טv;k|"W+[3{t;U=I]]GH< vv G_K3K˩h'`QOg4 uo)ꭏktF{DVSP$WT)~S)% +Nxfe<2KUB |!|n}"|\w ܳvܣ83Ӽӯ˪.[9n{Iz~=ު&*x |+-:T,?6\.rC#G_TKEFTfU@Sܘ-ooNJr4 +3h"75sa{CMZ[jJ.ˬ֮˾#yGrd!7ؔ\3w)}L6E~ !iyRoȚV͗NbAn ys0krź9ኝ8Q.M8g<J[:ĥeYנ1JَbZtȮjlJږJEV"|QB0gJo]iDN]p`9T +*et҃NT۳6Su˳n8GjyehJNѩ%k[͚D]Ey3S9(9"/y.-~IX!:3oeVҾ@%uIe%MǕlP用 ;;n96`YwaLԓھjNlj o8;k; :,ޔy "Z=jj,jfҫ@-*'`R-]}~Y=ԭ?)8)϶)Shў\t*z¨D"RJzd. YwIs)$a.B9XVcdqOH1$Wh7z0EI8n p׽f-R1MK/ фo.oʃ}i9w3qCoX`p SD%.j|B_.6q3LC[tvcz SZlsn %,%#mSQv[.7VEOVj"u`%!g\ +/ϯF_^6'0&[OR|XC@.ErsunݾsؓOgۍi>Ĺ*tzֻ3Ie^K:ch6YI5Ӏ'K0T/C.ϘVܤ6Ӓӱ:Px'MƦ2 +AP2VxF#皭ɰm.3gEhѯ im<қ=>Uv6 qvz Vl kZB|7SsrƊշ\WNbI/KkP~6|W$7byޠ]qCfMppeD95[Wə˕(Ly ?Y@ + h b^ dk{d  9엠ۭLn]`k߶β#e^E5ͱ3RTٷCH؀b}74v֪s{FՇ_j'2/0CȐ\a`'RFЋQ!= X" +S֩ +;̚b;`8"q@<'e@bs 5@l$})=w:]*͜[}7d𵂨rTPdU+44(~A7, "N&p  n!HA̦  +HonZ|4XiR_uWVŇ kVY澳e +2/wpt`%t2GD4]`@$!*& (+CMPqq(cTVB^كTJ, N4(7ȬlS&'9ڙ!ѵJHn6)5UwpGFh,b6< X'-:xs ]@SDhI FD4np+oBvmX`7^̚Gw˟>]-Pm\+ ڗ=H.J`8` ^?1X-C]:,C`hb>N^J/2` ̼4s{D܊:5z/C׃b.dqHT+w4'RCL+91 J2q%mBTJMLa85XJc p8|szVZNf1p8tq`떐ǜX>jºb aⶥaʳeZ;M0 ;=ʽkq=lb<Ql_g 6Įmr9|V7@`@ asCit2OӎN΁2׍2˰ Wi 'o qL^ !WRk -!~}6~bۢ8)%iz}A_ #m rBEP@h8:Bb9°BKR5 w[ +Y?L.xR,#d9sd%y;?F_nۯ̆GJn\|`%c$/:-&kC@x 8V,|5vD<=d\ ŏwo۷6J@ #G%/l$n[\p ev6>uQEVDx_zW_6kʷUva*@]!P[AjGVGJW}t.x43{Xrf'jQ'EhY劻&)=W(x?DaAԓ.`u7p'kL]2J{hE/{; Vcn:- ++/F%O^c]0(cl7՜̧IHVD Eʲ!E)r5.k奥i4I9mj4BT'A +GTEA|toٸ{WhJ͏ %:+YQyשvnQg{fj;Vl8Ɯ2yn i5I#]LV퉕\΍,!Ib\Y㻅Qzwk*xM{W59c1VlAS洒r6/΄5ku7m~*p&L6䢻'gxMT(M$6ϥӼn٦ݥt;Qo1IKvy;êQ}`~m3B ++UfY:(PÒirjhQ HM*lǙ~dU+)$f)>DU]Po]sl6Vf)+׉"yV4rä9[ 206Cr$~)t2Mҥ'ws,m+VݧTm<ʻUo_u*Ž,=_z3oв$6#L>XJ\"q7WRI)qee ݣ}[ҺsWR玈6N$2bJr&lg,*hڀÐ=% L5)urBO͹jEݬ&ʙtN@MlPN]&eETv aN<ԕS>ŶXv?!]D6̬ڽXGb#fP88}֌ӡ_\(Y.xB~ڹX +Cu1k+jUngTԩs1rF³ czMZsggN42=R#I=i +"96:e`LR+uLPH3C&z=3t^W؄sGc3s'=5N{SFDm&wg4zZ[FqqBneO!"n7 Gxltcuᱱ /xY壎Uh'S88ɷ!=@90{dE>uSgNGZZ8ݭg6\!s,J?M6@Sc\" +iIv#e7<=Ц˳"abr NrEnٽPE8-tD \yi޳^ib W7 StNh[Hz (b "HP>z_|`\h3tNhu +mbi8eSmxkxZ:b^n&A[pPh8:VD;W+mgNTmVd1'zX !6 SV 2 tk 5 ry +j, [j5zE˙QlVKֲ, M|ggEj?bc}'~`m(!A1Ib F%yĖBD +ES< n'wI&;T%H:$8|D.BfүǮv޽Ґ 2@6ŧ&Ԙ`GøTbpj$ :sr@ =;0J!:d6! 7"@U$]șO 5@z.AZu/x|1gp86B9@67T/ vP3(k*4st5W$5dIQPLP-knN4esxљUjR&cDPi\2]ʋHLzv|KsK^ IA@Q 10M@w<n3.\ns@pVAgaw}%M޾jݔk~Y6I~4^c (>c_%Ú`PS~Q!l7^z",} ]0#VL;6`0΂&әFurT;; +;Ait|7c>wpCyӣb 9#YFk{fGo(U7!3+( 1LEׇ`Gzm>W_0f#i);|[2kҲ`4S¸e~i~Ё!['& +A[pg|! qr}gӨQ5ˏ+͜!7(+*_[aI̹:HԿ7x7f=;>B9& +_ZϕNIb5Y.W]]alBv Li ؀?Z o^xyq܎@`\,@t- p r=c|5A5O46>9Py 3 ̀ (2DFD'?,,]luO2ۗK}K|'m<18s (~ {r-zqsS7:n4|i] {ީ^ݿK[~Yn߲wӂi_r +JHF=5 Y-JF$%Hx⡃}*@͏ uS nߜB8Np;:`t"ɾW绛e>]zimyb=Zr?5Wq14}ʖ'4YPO(FNk9EaLn%}Tv3#YXK]1ϙ|}_-TBl f0p]Bt_ٕKfUl?}mA|'ؾmQ ('3h?W?WH޼|kr:kSznRP;\ 8/wO;PsUղG"iijtkMo; ]rguYqS$tP֧uxmvrA^=X/{$YU=5vk8-v>o6x#8JO& <|]O&;&'Wۛ3ȆK-T _RbՊUft\oݖۿz;կorhަmV\^W'Ŕz;AFjK5Di U%ET=/We4(g_ѫ;dQ.[]xq!Kݷ>a_7-_*cT?XW~έfK;xŎ)E n<'⥔J^.)Ql%Ӻ园Z9zd&zdoRg=+)l noPȽua w[}WaW¨%!Ka9YjiX(<)ʛQS&/ڹGadG a W\x}=NVv)n뵤i¨~<_ 3L8NT_RpyzPM>OVR7Hr'ٵM)+.ѕk%Ώ]/vnɪboO[o-B*lx˟l7n^X<2y|sq*㈩eTB5KS>0u\F +tEw"]>H$7KR_{jb'ʌd"w1[?2uDkw!'0M l"HSt)P#SbC̏W*D\*=uMvn[Z]*rr#:=KHko~&RtRH +cN{̵_ӏ fBG]@z[p&$esDi?k17^1K[=)eDv+er.G6Dķ \#&~2نZ#fPR%w>ztLՄÊ,#H +7 (v[,^ +&EL&6HC m; t\m%u yVpVUJێĽ>fVHnwux`&ed,VwLX #|=ib?f@.t(ܽɠ<:r!ƨiHb +.$ Wdg5WcU]խ+ZI1/{ڎR--gM}ʔ"^ݗ;AYr:]+:-"58Hfk xX'8zlC8)"b T Op6|2|:L+~FudMyGr2 )rTGJ:. shϤ]tɱ d9zGPuUVY"" ƞiC/ !fu@cC@ԧ{@44B#kQLc LBڮyڱNL˫Ptn+wEG-N~]YMK&7ˋ%G@ BD 1"Jm +{(@)@jJ :mjZ~]wozpE,-31U:91=YMbWB$D?Q@+WJ?!̰b`f +w 8jѺ*G!r PDl  +?ir48 .gpvzuLm3!f\TNhv@7TS Qc`ڀl@ӗۖ4M!} LTuۦ:N8EmV.`&znfea-g ٷx<\x̀}{TDc"`X a 0Q-&gB.  1+?qOHGУ-h?=I3Y)A\r'6,vq#SY TN{ii%!J j`3b ƍ:`X^sto{6|`)82EVNs&!n8 ܳ>bn Q!=_XU +?bTύFUX"?\{Jj: b)Ɯ[pn z\)3pYwbk6 ^-;%Y +rj՗~7?q`!N'fY|?< +1c+'[;46|K1E\:K;%Lڂ_*MW3oΰ)xyz߂OhV 6B ֪ SuSrOKlz-5jhT8:٦ç XrLP\ +n)%Warn +ļ}閵Dm# w:s˺dI}u+'}?leqmIvUW+oᅭd?RpҭO +Ly OQ + Ò٣/h=ɣƘt>`nq}T۟vz[? NY"]tľA*]@RI Y+"ؾasu~5 N|[ݷKsSv{@hS8ZB\gb}䱚{c7ksDc_^y>3m{Gշ;ux>VdFKvpZ;#{$!,H|`S?&iM箽ڣѫq>`W rև}Krձ}^@sw{oz% ^jDbVxdjYpOK@XѾm07o ~w]?tLdoO]Mvfiޮ xI52mCyt&o;sj!k|gjN4^^ȿ[Փv?uL0maMkQ[6<57㻷mO<Ȇ\)C] /u$kFRSqcUvKi $|PRSjz_#yOJ O+=lnx6[q<3" IfUj#>*`}#,yFX 'RlJeo[%|W?^Nѩz;^]?SkLW-FNUUU]PJR?e_n_ +V08f:՛g2oo[m n]Ư<N ]VrU]]cpZD*ș,>\ .*^QhJ*Jfy/"C0mbZvQExERdl<o1t` JV]d@0VB^&'=x ޟ0;Y= =SC{Rd.ĈbuVjޣַ1rN/OHb"Fl>8L'!gaZ>H;=675{l$eSC'rE5˟8[T]2ޤOOȈ Q[|y)X`X± $J"y86vH X^ `LqDۜ,$Hq&:.TK)Zlv\8'ضս҉gbQ~= XNiZPj{=6^`Vc䦲HzfR` @Fuȅ g\,@nD> PMnti/ +VtxωFO{0MᘛZ,bҠM`/QZD݉*D;IC00$oPQI zuw-ZzS6Qzsh,Zͷ-!O@j5o)K@91:{dOx$ۉc^/O) :#~`l)"LLPF4}jԒFu%О:Yx$@"@L`x 0Z{R -^r y\rSFš'QȞ-3Ԑuj)_m(Z% ScBa .DgHŸd\f83ZI8-`! +X̛+^(_,785p͖d=JiX6"pL"qh\cqje) h8LhGְ!8;C ē8W[eH9KON;pO|A?x +P7 _mopקe೘52 gAY,z/nȪZ-1 +fKW팅qt$QC; +y Hte237@Lү>FLZp/ӄ+bFw!wbĢ0D \Q$Q#erآ֬xΩa|frR^;iT(T~{ap7GEr PR (:{ȇ~܆5`goG0 1IUtUuT*4%F.mhi a8BJ&'!o}s+i&wOFYvрE&J'b +>yGKe!-@7mtTttʋ+3aG󘮼~))Ά)Ԏ]5I@q-{p soP Qg %$*#Yz97^00>+N)NJ|y/V"pF|}m)6Fl4-,jC2R+~%dC {̘bqXvvT(1 * .vX.g -iCnhp}5کhuB|`UOiCbi4Fð1frڂg`\gW'yN]h2n'γ^d{>#ZGuOWwߩO{j?mpuL ڶGCDkFiFi%Wlsg{]UH #l'U:d/F,F҇Axάs 01Dbס^[˝JרjK]~, 2^&v|-}qz>_5{U>VIp|=Ҟ`ە&R,qj[Ql=bylv.:{O|%+fYV^Q:/߼sѕUo?K^*%_jV\,[9+dN_]6i'כ1 w߸@++,ڕSj7L+fΠq_'?[S/5>|+Dz4CH۲[|z-~a^,盍1T&_4)gxhwLM&Q2(\/v\zMd?fsƦׯV~򿹕{/W_ Bξ +8l$s%vӦaaurMxI,f_,ݼۿϷ?AQ7@GrI; /.ׂA}A\id~NWGZbfs"eq1ȶϏI/7{5 <=W+ZJwfv6+[R_uv-vt'TUd2j7Elwq@ 4wS;Pk<]C6J%wDՅS@ݬEHIWƬ h,<ᒻ@m&0RB:GFDnq" jf(ʁAQ x(+T21 + Jβ pSԍw|8[J~`²S&UBa11EEgŲ|`e1:ZPΓCRzI1v 7O+E"svRII {Ca"h]氽S2:r,¡A!4E;dF39RANn!XAaXntBǭALj!F|w2LPx_:W֧]{V)AUkL懚B- n4oݷ.ixN歴U0;#k??:3{MƼP];`U-Ōy8-8V&vG8.V5\]W`ߚNŢqzJ +D5 k'B믆r+x +b5xl R]^>zU)d]:v}#7sZQ(sC(_LraPIP0z^/~+%4- ٤mK6c$ X(sW{U Io-|=/Dr#i-RY$Jq2AHiuj.M50etC߹ٷ:?I-laEyXf:GSfNw팅u |Z2#$NI5 _XQ< $K~64-e&lmٗlS3ٕLz.±}HmM&ADlu/8;CO0Cì/KCrUrb=p6h,L24JF +lo5C$ݢI$Х-y<$CIvP67PZ+Hl1 +BDGgɹ 5ǝNr(^iܖ-?m,Wk# -[CޠT!kS~.Í ʸZh ٞf_1Ki:RzrOy=iW(̙9 dH$p6}B)&ë%|'j6ж.Ô Fxa9VH|?h̶|^7{$Yj:p`! tip|+vJipMk\6RJaˆ3GazXKzy49S,NpUZf;t-~{eI0:5lwzh +pЉ}[ I7q!*(I0rܮdXoU6N)IqggJQn-HlM2ݘ +9ۀJ FuJfhm|wXgf'V;] ֦)JX5q0ϧeͥI3Dt!\"Mk 2ՠnwUsB6SBH񽨠ۇb:(OjB 0j?%}ȣ VV)3mF`씥s)\,Cr {|P㬗C=B%l0i؁j[VG uG0P"UYmVJ?J}֢8@`OW(H]g8OW D2r>C&+!j.$1w3;wRFYb0_dm^p=i0M(LBh2Vm֔$=BB,y©̷0ۺnF<'d=tg5cc.[Ô Aq$ldK&΢Pŵ֞HR>uus4Βʓ¡"jEK+NdnV^P t56DbʺaY$+)I]X62lHuG3obtIhn[jNۦ7gK: es~ƱB93Ag6KΌwc̍5nJj gd8$"( 3LpͰ(% 6Y`F-#DȅQ[P@w4,j67|ofc&ίJ"ې0H}iԑb4MJ0?A'etݦmSmyub\Q8_ӗ-Cʯ't +yysP&ISIld>#>罙dpT*=w@G2&(}.M2r8I{Qԕr911Ӕ*z/ādx]ᄧdՌ,S +AmuCNu;CiMwV'.<<feN\b +ÌA5{B ټSuҀE͔rDlP/K3fa\5 tOPI3Yw(( +s3CD=Y ɥB\9lh$P #u;jvb5*q ޤl1QgSϝ :Nc=S4CJiU +~(]6liH8Gcsb=#s_e;'[=~:?E,5${N-zNfN䙆Z:yv;k5&F3UiSy31O:3U1U͜ΐrX5UR 9W%T ɶEtJҮi#Ϥ4 Ie&I}3 .ւR렂R,?~/|3 \ƴQUla&]'[?SƲXm垚Saevj=b|a&:Y[y6μZyURʂr2Ƒk,ٖV⨋-a9ӕ#ڒFJj t +u?e,ՏPvBWS1."KAfְ~v#Ŕ91tl$dD#xu2p}Ug@i,P^)nd?^'>{]{rrRe76[6uQn?:!iMd ^4LJt^MT ΋ET~a>ʯv2z̓9D(&}^0^DR~cn!]xSsj*˹ͩtz%,Vq:-ǔj?pI%Yasxm% C'YT7'OOO`r.9_orQi|LÒUl+[ހ`0Ɍ pi'U'm.^btT(O"*y::صx̻z^E{:ͭ v)[~ ,ItU:LeRq\;$D< IOS;{n@5vTZs匧ί:|YEQ|Sc==xS&xXŢ\mUŶrUkΩv8EI~a +H8=dq̓:$}K+S>FtUMtÉE]^NBUsYI\ď*פu97*]*cS/eќ3X0oK;n[-y#Uqfi#Luw?z7o,{_M"ms6V_xnv/)k%wV'p,RUJ~/OqE2G ''MH %`S>bfXg(Kk2 jⲘǦolZ^=Ȝs[VŗzQXFA'ۆx[@JQ’R}l'?#u(7.۶V)LXΞ`YJd" Xr;yZi0[^e{Yz2E|-皣a1,4Uw5A 5X& 5,omصGx(cJh.RsV^ɺhZcKo!*q|=[,?\>t},m +3՜p)*9+C|?,_u1'fgHnanf'6v33YF= ԁ&6P[ڷU76 5L:G`8ԧR74:`ːBD'̪,?5,z>--dy6t|zMXW tDz/c5mU{Ҵ@O)K`sFG m4Hn>ss>ZZlW.?>9#DX3hf?^}7u8m|mGdsV:f-Eț./eNY lJtbjA>g,8Sz;ӕӜ8dD&oD>VƋ"65eX`r{꬗:ojPGJ ns4NqN)0z!966UhL#StlxWF> $0c7m7>%Cer pQ|d/s[ +J"{'BQU-zF6F% ܜl6BY2JT9(p73(z7ތTkŸ74a6K +qlNٰƞE8kz$r Z9E(q~8dgt#|qcʺl` +82$GQ@:"KCOf}3|8)}XAAA(LCiچkijQ]fiYҀu&",Oi}ku^!b"2|ibCK">"ulr m(\}1*y1EE.D#ش؇]E}mǸz F?pӵbNf@Yoo\:.DL3Β}D=95y'i9ScQFB4 +BNJo!+b߻/ܡ^+&e {YsQ ̅NL.g̈c|POWm[4s=I;Mt(/Dΐq*jJ| 94jSiyj尳miנZɸ^7tĽd/p5?& c\k-z^[L-S1)m!w6/04ٝPG +`NuiF>NudoyS@d+]\}$zg'\Ug]eA椶Ħv;'`L]T8@V߄ +=6btd c!vm,ջE—4i]4{ MYMs\7n; )d9ÛT8]7}=#gSw=KE :9e>Õsv{^z a:b+;ux%:ec rFC AcXi"pPH +af:d6Ҝ ~9|Z|ttR"^BE_FmJ/|,\(>ebHbRw:1Aw//s fx"fI oS![ua hKrYXK,b+ą9×DEA3E+س]A0GCRr)G"mV$ Du@ P Τ./Mrj7.7΢bC$tŐ$CG MNiizv&U`O- ހrK[Dki2.^,2ԐfE{% 14L _1yq o$ Ujwt~.c>BP? 7B;  +L,g^ Y9fd(n?wB,TLx|01tN:t 6TI+~=ۛG`IܔZN;NEopYLd?)NmW@ǠYZۃ+<)/fBq&z],ߜTZ,q9GLT -蹥^+uk_4>JsyMa<&fJ*Yʒ0S_wu4sf񡱽`0HcwN׹ܻ7>k|}`x™j#%.[e!_׋N^o%mq.WE,T!71(WvUpwUJ/SbS@%2j7WGL勭rL-bO&M+B*ƶ›@z!. +Ν' y#;M̧3ɡ;~Mx{rR8`yA (dv<T0M㒀a1{WNwشt;2*Ѧdh;KDCrJ7n2%իA;F4L4&kw裸$KܢѢ\T)i18)u@QƏ0L_ է3 v9 >yXҌsQuH(ur}ؒq>_H=ɑ*c qF^l#B嗒l_6f5s60ժشzΞiI`}O*l baX75gxLDAM|(N%M0Yy,PLv1Q}&q:oPG4c_ +g^NyZe?x*J~ ex1FEQvjxخϖ0s@{ K'IM|dzC,UY'mxcϺ@dyUD2؞\>ƿM' Md &JMQ`Ret +Rϴ34]Y9CrJPxԞ({S:Sz"Ҙ"xmCOE)Ax5S <1)+Kiy ,HOFrH]ا9({k-@c hۈ 1EqV g}5~G +87%_]Ҵj\<>ʠ诌#9] }ꌾ5Gģ\95t́YP'G<]݌% . RۚV[mH`1͋±7+ޮ$`ʹ:Ͽ":@? اa/B`]f  b,]z{h3ؖ㱣^]`4wRaICf}a1gK [fD,h]7Jx 5Mi,Ev8z9/yoF!o G`/HO#骚.:cGʩ(* )0nYo}{[p3@[jnn"hkoxNwsmB~Bt՘ 5ux3gY=c[k RV:ߞ<Wkve#fn0B ELǰD"*_5]G>]+ tiOsAz5(Y‹c*2 +)x\:kA:XC*I0x^E".yQ+t>( KNhNm|#yfܤK `gy;of[êֱ~s33]h@^?]d4FAn=s| <4ҫh,7YBۺ-m8LJոpHQ!iQLVӘs&()&] XF25݆wyӅTiֆ`.e"VE4< @F;<"OH2T=GUSZ#wG#KG'8FFF=햌gflopMF&bܯ̷Z/qJ{ݞʓ0/PPqp:ir4.6dmϛ [z[O P0?ϫbvp?= +ml&oimZ (_ofm0XȂe. <'SofLlϧb3v<Qwq$<:lSnsRlv)淛ml{` H^fpKbe'Z(?͟//wH0ڮVI[MF1m,q:C(ӷΧO6'*=.nWS6xqvb_V'*|496eW#ysnWCtXw3>،BA3X V_S  HmvG]Zm³@Qc./fsJ5DјFٞNGܢB[S$ ~}Y{90'" +l@LOz゗g8M3,vu+T KJj %?BE y9n9!%D 4fXDžU;NLEs3/o,4jnpl. +d:_誇1 Uw;@+O!)Of9WI s?bK}@/A4Z-vF[8 +?;қYCF}nMYM&0Ŭ )(UHǃw!HQ,n"*f L#ElB5 +ea}1=W~7?ÿ˿__[x/3iQ//_o7G9?՟__ Q7w8~?}I\6~W_/____- W?o/"'}utXZ3v7|F3C TxWnG\mX࿂?>_]ћ9za< އ&е429wvnB}8  D2Xһ(mPЏVwŋQt; kfvݝwLZ^e8!Pߌ/a(lG<{A@ >ߝD>4D^O'+fJA =ZS$6u')Y?ǔS /;by;ZB$*Q}6gOBl&ɬMce+`D5l]&$BZKⶏ˸r`9$D[} `T]^fmxY=6$J.ѻ&nQ[j U)Q:`Nya4XPvԒ[6rP6<-0K'Af\g"(mG1[( ƣ+i9އEXyjbyH'5-XxRIh̼ۤ !1-Bj/Y>TA:PBGWÌ+D%O<nۋ7Wgx:dzՎGjkMB +J.-␚QbkM` ,Զ嬪jU*֚Zu! XUz"pWֵ 5=.Ex5yeH^=Gs(E, JA#iZwU{!hC Ԕp!W/VהؙJGbiWnH_4Ij2 H}#0rR[Jh_AgH ܱsFZҖ^Buim`̴2:q )r5?:4=,xhiG8sfoDw"^^oLj~:ڨjHsse6H9鋢4&W|s͚;kiPS5XP{C +օ1Kd(u]$M> SBC'|"/ +\zRWJx5H) ۃ`!Xd^M黶,.ZR\iS;EPH ,#HuP9MH#[߶niPױ+ ' +*i01FxS DkšY)?ęj(9T-P5Xsua4&w+S) Z8njc=v!Rj gJ'yp,pO +$5.QD1ˍL)mαQ9<4-~Ιؤ8I \b*q]-y )v8%kVL-tΥ(b_mpHR(k@+|>2k^D+7$z*p5g(j<*gvh'_㩄fc޵QWy] wE*3.C ѤhdjԢ< Ȭ*mz:˛¥#iJ==R-tx9}鋂BqsL[\ZH])h]&WLvҫ=f.4cRvC9w,pse"&f&^AAxǘsH&/RdS"yBG`LJ֚uZ@ѷ^c7 +ao/) KpoujZX|B .M0^%>Y(*uGД9%Ѧ+ӥ +NՉRc#`m`WoE5DIL?dWZ:,tbfm#_2bO@q>ЌѫX%E +5ېMB> 30\FdUcD`hM8O֊J~g!qCLy֡d1IFt>2#_Мݛ4cz5ouycH;?w^bWZde>2X}{f7MU&?Y[@J!?)9 "ibTOKAߢ;:\^W[ɺɬ20NA]?:Tb"qgiAWXN Hܛ&Ʉ H$o(ɠ_;+ +쐓[s܄b_aLAɴHs ,T$ԣnPm&i<:7Pƒe{ΗHr7H\_YI]\G~p*]PrJ047^9ͼ-g ˲D k"j&$K׎$෵NӒ6 V$$Lg[[$Sr 6Vw7&P»-\Xן7[;hpCb.Ae| +dV] ^2,gX{X<\7O0sGƪAK8pN.z`9^<ń)ԵOM>E1*E1%l陎k +J}:>b Z^=6K[yR\OR9KYHVx4:y"NVѼ(xĻ}Iwt2ͦ[= 4rL=W \}114aږ j]{g8jfٷXa}CXdt}:.^'nڶ%WZ7pH@GZX0xLX)?9?3f)/,-,b~/V0=`7#m!Kbe`)G͙'e"p,_X*0 =hvH]4? 72mc!3qx|B NÊ={,].aG"m ÌCSHӔ4,c.Sw=yOƏ (n,s#m*/e4KIV$LGY49)O.+x%̽p8[)ٲ5jVnR;{}ąJX{`]/"v5Űx+ez^'G=CbWƫhPn$-sp8 '7(<.xʆmo¿۩ȸvW +`xIYE'&1Fb4ZKFcFH5qyG C`>jBE׾D } +FyI|+T3R\ meCB15pZVR+크\C.֍~QpA:Ug۸pH53UqF̎uϚcß!8,1;4Y~xEgTޓn.0(Q|sI@bBd:H/.ڶ1I $"s>;/^$DKaH|-8v&n' 0RT-#O@-#b S7sL "r'0kr: +:jG~Nݩ5t8 + ŞNZ-d pO5t`ݵZ: +:Nj0*w^h &(_ A}6: !3f4mjwZpۀ@aŰ>sw S4ӂEp>os@].{+fV nM\*Xo/H"m%fi^H8+I*R't 4"{!-$ Lu*zt-#[EgKW 1SR8^}i)xGq*2" ӥpϕ n-As2x]2 ,9| VlqÂgI"4q`qs=`&#vsx4`]9P3釚\:3N+!)z[cM2XTw%=U4|K-4FnF0ˌ'SnAz?3V*W'i#b&o1"5%L<)sβW&Xa#M5FYJ 3b7@[n!]9Vb7rSw&de}E{+v V%\)dmfHZJr7٫5jyCR̺ٞⷭec\p0iTծa7ńӛ^E\nb.=(@DՃp/$.]:$'NK\ Ecn}N g`g3;Č\A=L)0$z)ykq+p#Ĝ&#kYwͻо|F:' in}1 + ZZ_#? Fg|ppBJ('*@C̘ +y3#q.ɍ muz ¹왲(`Dc&{D[A#mXyJ$S?QiHJ|2*"AKh\TnIbw G(vɏI()ß_cIU1લXUbk"_(ˆW(^qUbɳW[(G[xM +:άӋcO_ޔO ' iG#6hܦ|~I(Mx}/mXM0w#4cbHM,%tl皸'ro\_?ew{n(z_?U O+UwOP v{Rp +&lW?NHtd%pIw,~O-EK wz/jɏ` c $Uc}S`W?qq~ς'Ǟǀ)d?<~|@*Jt_?1~){} +X>CJZW;"MuzUi POc'l{~|ߏ_WՏnzUi뇕kW`_qQ}D{^ =܏S7{`=/~V|l`L >PnOء&oox߽Gvct.~ʭ\_?>`v{hyY~\}o*ww:[ 'bBپ~8R޲j' O T_?m_W6E8/kD=\_?aH[婾~vc6D_?(bO>aPȜ~|յ>\{;Ⱦ~ ݆kwl搳a|%w{r + +a.y\ a";Ōa)b^^@1w1LR墇I"LhQ7XsnpP2 q{R[RIesx䕆,Xn*9d;*}߹f<;ݯo}beyi%sԨ(yQ|uNwD;t:(*j`K֨0^M;7n"i(vy%RXޑ-dJSH)YxEq8J:SP}^;pI$;,#3 :UX;%aKH*/㉵2*|mk *͢]E^b%QW~[wPLoq3lA$DZ/9,FXY[K(]|+F|4'xk6[`Nv[iqR\emdC MDL8(T:m0~3|Q0J5ָd,5VU=}-` x=0ӆi.MK*7?"1 9&!//[cOE$mf/p,_ |ewWB zDܒnĂezDJK5IG|_`fj7߬o6DܻMA ߶Mof:Y-eä6iRp3r-ڞߠ~z(/]dKo6 +8!`ww\luM >QD4٫5dC*U-I1nvQ 02Z:ցDm{ Wm'W"Xf+t".9H0H,좕G~RZ t:H`~S5 VF!>@yϒ $cGy%A~B7>V‚hxy.7{'1< +y4]Tй՟b)/|NC+F[ ); +At^ZRֈ^m{XD7L*1K MNH XIikmE`pMBӘh<(.b.8 Z}J5i>.&v )y̅}Ò8W2@lR\DF[&7@+y8TBcNcboZI7(b̫;e能V]W?k™CA[:L$,w\ynHGfMcL^U( +eYIFC ;vZ""젷yxS MIHؕ nʏHo ei݅Nbt2`WVsC +70BE67DPVCgyoʈHۚBF\Vgna>R_"gU*@DK!MvVϝ2#U\t#=ro<(ij^Gv- &1H[Xֈ[q7Tm fKy1EJ*E5^%%LYdUj})bjRQ\_{WGB)H(6MAY%FIY"8Ӹ9߷1γϱҭ&y 0C$esK~CJQ/$ivuR^oz?]lYX?4'@}Էv6kG9'Fn9k`10MAւCO;mf]>o1e,y ֣4emFдao UMz4uBK4wqJ^"g_mx4ySzԹ/skѨj_:V>و +F,F5xu^Z!O-tF<&+HҸ$a)f+^")ycv >G% bd uaL2˯87qɶ5s &A^.#]naAD4c.w+dDZ `sa6=Ch'n/N-.fzn:}7fjNMY&͸˪v*UmhWyhePz)X0˯sf氫Y4^=Ip}خ(_̄o/zA%*r -|wuw}c`vM_p#2;a㒆Q=i_h*2JBƓPdhu'GƉ$f*!;ZH_porTT|y:@oen.ˉʍd/Ix R]irFKc뙊:"K,Nn3| Ýg!;to`p[ZMY{PL|=_һHvT> +(躣~EÉ.N'h +_ +^Z/WA/tQ!<ءK?/|/M5jg(Ak }?s#C3GEqmO;. 0X\_o1H4~]qת~/^~UMYW0 ^0m-RIO,u9rRZ>%s|9*Bf4 1ɴ)<u!AkR,E"sny|}̽T}jzԘSc%HG9kfLaJ7$zJF +VĆueg&ۣ$]V=q_=P&V2Cf:ycSoZ^3էo_Jz̽Sk*g'{eN/nY<ʰwA??,[Oǔf.iRdkRΥ9+հo8}_XƖ"*`麡V<nj;_V .q8=qS5\΀%nRAPJG (]*J3. .\ 0 lݕ?X?^;g7zmB!kV7Jwy}sOwl'v?;^2AWU #~VlwY5؊3ٽ2ȳz&]P/rׇ k|]82"q V3Q>S)o-8QqjaȸO`||:Y37ʿX^|-[NfVKDi,oRӎyHQUjXL4y;y81~V~&Y~tmS%W6Ũ ˛VV/Tm,w?H1)ըSRUI(wg{[+hWzaٌcJU:x:ll#V9 aj'c4J\rZmr͸ .kj49Yggrwj?Cs~#=Ko qDi<|i̸Q?qB;o}ߑX6;݌' r=;ӺՇIOf y_XjyYIT_rUO;NeQVNW_ۣr '~e2{RdAx/W=9aEx̛>@> :")3L} erSNy``"xO(P䩄R$\7A+}trngD }.OƟ񿬾"dim,H{_v/-3uM6-ܜlÍګ~r 7jgPL0 o-ayf!/UCbeDX_ q޲|3W\14,:ߌ#2΢b茳|wgf/ybE1)c9Qgf,?,: 3?cob,:c|3|3q,",: Ȍ|3uqo5ocEE_cX(Yt2{2΢3΢\\Ќ 2΢A3΢͖A-ھ87[(z9,:cl||qoy7& +7c"k:81zX|'cr:iuI`i;zP0miCѽAiL>\gDJ9}(3K3oCմc7(o;70׉;9ח0*`hux[郎NwB7C!B25raD9cD9cqeqeD82QNPqe4 Ts(9r2B8g('63{;K$ʩ1{r׸oc9BdS5x}K?oNHsBe: ogHJx{>oոJ>}x1ڇ~ế2Ϻ@fw>å_C; l')KxЭïKyd ~i ~ΖzG>JsDO/w]h5g.70EXJ"c)yKyK]0JvbUY i2_ˊ@T((p%'b^dI+Q( +!"ԩx(yQ"C"**)Cz3v&x(S9 +2VncgR\ԹK%Da@7w¿D伨B4"!ZTEFaJoQEI%)($X& EXouaiDAKEB(/bRLJ +^x]ҶȋjѸT~ _X0;)D߳@ 'G!}Te4V&i,ZP`!qephʩ1;x/Ez*a endstream endobj 42 0 obj <>stream +%AI12_CompressedDataxٮ%ɕ%GFŹn;plq3Ybl$.#n&7+[P~CA$ߨ ̏m;w*«qak~gynַ_\}ݝ|ŋwo޾Gwv&GtyOn^y~;3G׷};o_ÿ';][|kI81f_k%>[µ9`Y?_/SׯD?8g7/z( l?ʽ7y<ݔ)?,ͯ滻ܾkpy{q's"?*| 2y ]-ŻBS}{_pi&}~vWxϫϿי?wMڙ<~u?< =}LlΎqBnA>ƈwqVL24a2nXsW؁:/_=g_W/a#_?a@L& 1fl~2˟M$/-&ݼ8n_qO1L%) iG ~gOۛ7x֋/__fgtrk|5>9z'O߽9[ۆ,η\N>ŋ??>ܓ-Z~#NnOoV?Y~yYyzw/^ܼ=5W_/_= klqW_&o=4{vCvǰn/NN/O~_G_A +|߯o^|~<o_?zyGٻ7߼:/__A]ϟ^_}Ɨ/n^B~\Ͽ|u4oi!˓[ȫgK2XZ):|?|I ˢ-k߾x}w[e/ypsL|s"{}8g~s P[pr"O3e\.KI5~IW|O?Y?'g)կF~)]?t>/߽/߽9kknRxs c]j.}wyF8I{IqD5>w~w_ ?3є NF7Og׫u8`&gϿ|)׷={x'%/o f'sz6?_ϧﯟ?q-!?c: ë^MoG 5KSӧBG!ZL7Ry#f6?N4g_xrTIx!^>:eg _NY} ̂vۑwyϧ~{7{es>O\ɯn05x~=/kKmö<`J"yz!8### +/n>3>=g؇$? 놦lʙ9Dz<=|۟503O={8<67taL˺~"BOI;{ /O^~ ehg3>gPuprry\V.#ט+.庐\g55+ +rr9\F1_WWR ˕ߑrMJrE\^.eyW应Bs$bJq?r|(Ϻߟ/of;j.s,)5MiS&;iLW2]`_LeNSJ)|r $x/ S1D]1\p<0b7L3'|{ݕt3&pDs7+{i/@grH> d͕4 32fl`4ƌx9^#Ofy,oYta?W./,>rܷ?0AqK1 [q\1Ei}J>nyd8Nb8@;:XDEcW +1#^R犝 f.߹l*sțKiuֹm 鱁gQϬz|M-T1(08TRLMY8E)\`*,(B*Z[\Yt"B,1;5QQeyv.ȁ2R-5J6mlo"(Ⲑ;q:@QeiGy%eH={Y]90 H $ d;8Bҙ EJ,)|4IYNBREXNlգ㖣0yYHPZL*v(W?<~;+/ݥňƋˋs S@ /…Dz"D&T92 GADg'~A!(BPZ<?}6? > Z> !,!~37| |o1z\]u\g%q1G{g;3*,E˕rUm+++kyp8"p |]zqL3̩(c*H+k]y3`sURTx9]A,f1?c/+A؄HؔsMf]Al;v2bG')v#D}d>_{- ߡ[9*4 &=A)?aht aZ@ ģX΀&+@dV`Kb">Hx|5e8UrCkl i򻐋'1s(V7 +rSR#A1e[1bQr] hu$k?kN=k; 5,ae<* ǩ*28,z5g?p5ByYq_qop2n{ͽkXzzߥMxAy>}Mp I +\`%g!yI|~z~v~~~q~ 8^^8H. + \Kw!"dt9_^]_^h>XB9dE0.x^e`/7eVl;t=z)ԉ"4A$+|܎|rf,,ccGy߃1 xF~k}//p=_Ape r.S$\4;O+5U`͚bFgmvfm&DcDk7υ\u%h.krv/{aIARZT i!luW_#\:BU*z<#:ŠdEUPڣʹIoa=o}4w7Nѹ=+עT MMye+O#sx'/7Z;o`o+uI] qn~z#~~xE-2OA @ S%͂ҢP(JPR) +Dj +AE +"zq\R29K,4. 5,z(jaQpTQX,L"h.$檀%#7L` a:MSAOg❿ u)6jY54a͙P yES\5V[.җ++.W5Lk^uvu>!ٕf+H1seW6Oj`6U!%3%%³” $\XK{NSqGaQ7@Qe9wqUUvPZa6 ޠ8T۪wN ,+lb`5:ܪX/mJ]sgם ɧj(Lo,ϔC[}3/T59ȡ +.?kbW]e wYS?r| /+>Jk\n0s WW9謇09$@3ӌE73n޵Otqэээ%zW\ndqt:[R/6>%r<W>mʫulUK ~IW 3yIXֹn1 +w*:?+Er2M2rǢM >Ve#ǢM-5+QU,/wPA$HjJdJVR7/JyUV::*\DNU.Ɋ=8V_!>x-B<, |wHޡ'zWѲw ߎ5ѿCw 9<Y~PgD(gdMy8=$镓w:#(]Lls?Zc_WǮcoй5*A+u.aSGjѻζWO1,ew2sVv?rkؠV(6dK԰1G)Ԓ(ҷI&9RQkҖ(!ֶ¶B.~ U$)/X(]3JJ<, aӳdҚJzՄwHړA֙{ܽu޶oae[B& M&7m}V*i1ubCni,xR{ߋnk6q%b$I%JԨ\2Zlϒ:Ink{I-h\ \JE!IdET4TTQ9;RAE*P@EuM aUF-RKE **i8WWMw)paS{e]^[`SP͞E`2} x3<lE=*݄H)0SƷIM!u&kyM!WU q4o[ t^`+t&g.(BjpQW$պz~A`0*DQ*" Ɇ妖٦iE9Ke'@b8tu:dl4CU)) )qkтwP1">Ohoy2\k| +qC@aλSM=s;wcVzw:7^,<ij6&|Lm1p 0q}L1c 0>&}&>ʯQ1&^`VAt0G]TڰK3Xk=CoUK9dj(&pX1(T[b>kƸfa=v~+_~9x`bZtU.#ZN <۰j7(NViHmMI]i)J<4 [JxgK;v_'gPErtvxn(r-NYZ%NZ m)+ָod8 :^(q +8A5hr̚-%(Bdp'gCE w5q[6|}sof#=j }XK",omiMxY8\4τ8a^`5M!c%]S~$1-*99W='a~N|(wKKO /ukNDKaA_䃺>>?>1VeӺkL4C:j6f Y,Ap;@Z3wXrx_l#8M$ceь5F4NGk+CiҴX) ****ʰꤲR^ڮ)F)(׹e{qA}U?r 8Dc${:݃Fɾt'wW+GfQ%CiRYzĀ/Sm91m'<hgǽswer]mE"N&i3wVoVFoz~fJ1Rn8^[7hby6*] M`? ^B?$z8B^_cױ&[k%mMh|=qZYAZC7d})`cC՞nYK9z|pzxLߏ&ךZ_X'>@I]`[#O%vT7n#a͡{On-ηJJXɋ}~]+A*V'0M&tO?8]*Um /Mrj2}}_vіu1(Pd|zQ + +̥@X + y) 0 K̀Z/`0R4BJJ^ӋuO UjAWZ.PjT95NŶKAK .嵇MqƷ}W 5PLӿۿ~*춟^ +z `Q3<nݷ XnK~ /}>W~w:y3~(/S6fgC'a{ki :ʷ3eۥz9>J9wt&pGz՗Ofۛׯ/;hgH&3fڃf ; #s*?䯦~-ćw_g?-CKuvUދWo/?}wsݷO?)oo_~vWؐ?{<}1]溓Նaw,Y3l7#t$'Os ; (>$;80g$t3Gvܔ_Vxv<{$F~sovfpGG<׺S c-$q2] Ȩ~1[OMn1|'h5'8GWO/c ݴD<LLX[7ٽſf +]0i 6Z(;kӈTb;( (֘T.'12Sq +IiRbi7R$Aqngg] .nSyQT1 +*~N||tC +ЩXp2L)SLXIuYL A;06p hL 1۲:>'`DZ D<C!X8%NlȄ3'$A|S4 +pr0Y#^lq-s/'CtƜ8Zf<qGa9q:eʣy`p^A:̕OJwTd..5Z.Tvc‹F-š'?a]ŝH48PPpgU+21a!x)V ۆ-f2TaXe!QbbKhi5$8G&`bEߎL ATq|8[!`σ@m 8?L3nd)jCL(z=n&{N3:ġ!wi4*>żT¥d6e& 848񎉩$ +" J΍A 4Hb@{KdB(۠H@„X +9Ӟ _}w<G:+UKa  Oi>gAz'A + +XZ#81*UzS'8a[!]茣.'#E&#Jqktj`<'EMdy4c=snۉFmwaKHܘvoñ sB Y;[@@IVɏo_|w>?=}qeg`gs ӷ_X^kJw_q~?y'sOx#`3NLmx +[)`/+R[9R!Ai@̧ثbDЄJ*(ت18{FXZ1U<2rztAc]V:#Wz mJfyZ< x;Γ81i'/'vL9~VIJCpʫ;l/"58>> "] ڥ79&+ *0^]Ac31q,zn9Eix݁\@ Al=X{J$3\ُy x_~KL,/\Ns}^xT>(Dگ!a+7Gz XP<OVe>Hb|:L XMbk>K" +a +ÇaU [M4j+Π %֜%䫠T/F['Dq; eŗ2}<8YxBRZcicҖ(l(-m2 GQ,=T&0,@Si8-!fvkTROE@ Z!(`CqL ʣNy8bD: _ƇpD_8 +:Lh$(0]4_m:ӂ5 +,pif5T_VJK=DNU-^Xh~9tGX3 +*n)A9(u# D8j `y27,J 4`  eL8ҚEC(dҪR p{1GZ 0/jYĽӱUωHz ׆4E*pq $Ȃ1n +#E寰tJ2B I` ?MfB&Pj:9N<{i»nTF@bBV ./Bs/4Ե$Awb0gA3hL?`¬j6!{c"XPdi@ZJ)l3tYJ6܁(;+ʵ~Dlp.2:>͖d=jdO(ȞvW™ [-{e2h ϤO8vG"%ȲH NɁc8PB-9zj9qaKaCҞfJgƹ6p#O^ 9b?̈! +;uZمwSc3Cch΀i9N ]e$Kah&@d5'a& ǀAX8R_2AzTPР@ݮtCу&sٰy%ɢ ) A UW:d + veQ6R QzH"C#1։4N|&D*mk/)`01bh1L`,ٴ.# ~R0g*n4R9SRdLrQw n)jqcYT`Gߙ9eF3}w&R :ⶾ#$Ǻ1:34U =hKʣ; TUR9vVlEyI9Sr*gc^kW_t%(hE(KG0$h%h +Xnъ~ +gLLHt +Q8LnO(Y|*T!A +*UaH +6ZD1)]rWR=@Z)=BEi)]Dyuu#t#^+}GOOw5^SbB7wJiU1{7wjnl4&R0G6BR teϖ3Cq*Ӊ`T2SeZWWF$lP*HVTm +Bt5M*YTqkTՠB{H]*ZYAku׈Fk+[l Vf\޳jJnCЕZL*]U2x1^+u&蟷N * Qyʿs+Z׌x".mbj\E2kcɡkMDUur:^H2Pz|z1If@KiHI"NCYcbr<C2YqgAdtɴżI@X`p塌S̒S͜c~4a)#-oW+DhNDWW eh]z9[׷DےnvM="(^QjO⸙V Мr窱 FCVh+6aڗ3\Vufa`U/BXӬL*)D4 Jh[ Fj !Ht {m 1:8L`Wp3TI)F#'= :$,5|Xy稵#(duD(29 VC ?1ps1^@\xJ-yʠ, X0i_ LU:J8";?]t谎(id d K c1*X=<]K=GbVK0#R8.va(Pr9 ` (M=.,իDt'mDuñZfgzEsԪlrFo_mcae"e 宇_5= +&ojGC7k[.#k-6F˱e'H@N$ }5baMAAYNj0:jVNsQP*&kqBG1eH51# f)&Kz)ޡՁxIYL&RI1,qt!70Z_R1'V5YPʣ'Xɭdi&hSMTa!ƲBU_`rDϧ%V:4jX~$`bQ]y؋|.)Lg$3f{0Lv$ lYw]?z<:=WR9P-&Yv]Sn`@,t ͵*0vo}++H("lMi6c+ 5!$ ;pt`ށ/H-} xlIR*;)ۉS@+t ZSLw%N1j=,f ?N<= CHM;+B@SIoxY. cY5]qI?74܂1"YC[VuQidwpٶsS .Ke5¡-`mԂGZԤfKZvJCI%I?hᠧ]+ͦ}!lvH?n?|D-܎6歌td?( ?8F"~G +#]UtKNb%#dAY:N-b,YGF2ЃJ8Jc4$5'T66&6} 6Njg i c$vJkdgBEҤ,=+%&-k&ݏȖ}oF' ʤ!s)%\]Q9(I|0xF,+ N􁲞D/"L`.$^xi=#z p eHk*u{! {*͖6 Kdq`Ƣd_ĺ,/i\Q3{ 1:{s`]}-;H&$D-.'[^IєZHRC* a,oP.(IkFLo違:$`K}8Pعd6r3+AVB %κgn ~@|hvO@6ƉO,9CZV!ï,("cbz{ͬjK9KU=#S$Fbߑ#gpkEO$fx.| B, ,ɺ-̐/q' S3rG%u9{0W_ " AlMc p'o)D<341ch71Y~bU1s(:2!$j n*Tj O٪+)u.k51@=x <x?fgU@ؘ|UA3YS27 =|1PO)6)ˮ5cE C[aJc J8 +t# %`c*Z>L(ɣ$0&yt=FSoX2E, ~Hޞ%#`[$SET0 Z*!RHu& MH$~1T yQJٌNpz5)[-RS?21gl֒jk2f#03 U{2-ά914u6 %iѶdɰ{Qe +&.fNa޾gd:'Íj紇T=9BMm򶼪KV|Sы=|ߎh@+7iɛ|ۛg18Y7J50^B  +)yMUؠ(z=xoydt"Uxl{&wMIjwz&3κgeTRMu +k8I1 xj6Mt\m`|#~N3e3 v`Cymk˘_9o`yME0 +n`9[Fb-lbPHtF4̌ڶ3 :V7>(Ą~k5\<-n70Šdq-{-l( 1v5 ҨD4ٙe:?zR%@,J`YZ%XOi'H&Mťe X6`C2AHl9px8QM 4`H h^"+.*r$Je+\-}oɍ#=JpVRԲ47d6I&z%?xJjж:cnbNDqΘV${Yt3M4Vgp3' "NP |8o> +r(O,#$eK4?ا0C<>z915t81Iȇ*;3Qoc(}N`~Re<~jPWFqum6}sGFrp=+bk$rta+1y#H.{,Nu_p{2ܔ9&' XPR.+@,VH@"sP42v]rۯ:q2cR +[ MK4bNL^ti ^& YtӠcC7Er+dr)Xm`^*eZqh`ۖb;0R-6RK $`CK16 +6ݒ{d0x[ʩmƭMӡӯrX;,aLFwR❢Oߕ Kԯdj-?Oy7,:jz#EJ٦=@_%dZ0"ߩ"zica8Y͓~%<CthX5 .!Q&{h(@!kKY&P_/nɲ,FQ nPA60"t6 3U5;j'іG..ŠjfE\2m]XhgP %t-7N=@eU7 )CO_QzhP '_A*땮LDVt7 .%w<щ.R[YyztM0^he31u͹9೨:\~ȯi]#F  qG3Ê a E3#JgQ 9|aC鳨,M]O,0Q_TFP;2;I9cQ|3{F[TG{3XށUM+ZT=\g;-V~n@--= #֤i ;ҵݥrh=92odgb$d;A|QܥAfH?+Iݡ{(yMFsUŀN*'0G*\1:3-%vb∸lzͧ"'9EYGZMMzO7gmT}UGkL,n Uײ-@T6)oTh[.Ѝ*Z[OiB\`9`9GJҪ[ӾK!)E/:\U]d_$&Վ6M Au>ib\XLSCki[<(JS2˨ՐUTOE1Րk(SA2^FU[(XH$׶6i x(vYfæ;KkqvvQJF:kOe 6QUFmۦ8^s}W;aePֱh!.Hٱcq\DѝKLk׫ۚR5X6q3hh]}2SX[׈"ֱ=4[F֡oƛpyM곬Xr)|n=ϻMzJ4gtޫ4wy|;&s}{MICbs!Y GL - AᲡÈ1Gzu:4w:dS"nmSA{_.oJR9rUemBmX7W _ªYa:ώ澫<Wծ# C5Ekۦ<4gS*c~0R92iAgd t4ih <4Udt"g#?S"JJ_R**Y`m)F;XHKml -MG W&)mvNi/ǽf4) ,a߬e,,Ht]b]Mvx+&v%Y}HyjЪsPK +gTni{K` '30\; [9DM2;0i(JCA7"5E- +kPUWڶjԽzmm#={ж1J%JKF,jmEmu(fԙhilCkCJҪmbjˏZ~F([V-}YVlU'JmU"KJ4*SUQg]V]ASXXԹ?u` ~]z SoK.=HJaM'`(8Xk)R7*@5e F2 }DT|#f K18x᧣rR9^ ⤏v`\0!3NkF) +JFaV !:$P?8ܕew)IP5 FT4\Nր.c +;w;EgfioE>1 G8Ett6۳W}"N;p16Vs3c ھr̸ TKer!X hW.D@J8W.7Vr] l `-^ޤQ{ +8siUc9M srlOjgZl8sTh^GQK葼IusX3i6'S4+:s>_9VOL* 8qR +G1WJ +-}.6F{y1 +Ibl}x3~㭥gU^'_hmvOX{&Mۜ8GI,~ddm.wZotcQ~[S\͍o12ӷ,I|_rӧO߽k.6=4]M`,hxl640hAhlex?IDgL,T=4OlB##3Yy(D58X̄ƛɂq!!1) %/Yv3%0kb#g 6MEa˂0hJ2psNqbD:FS1IL8d2|ԑ!8d:41YϱRNu\17yȽbx'cXس1#n$hG53}P+Gs;IzL aYw<ďElˆOVꞴuSF3Ia/ޞ&lO(PߤS,%' *m9IUE[CZlz0c5HgJȏLo/-Y6m qD6f-8!,=3$P q`܅u#*$J̷b<;⑩9'eg7[ڛJ.6 Q&Y^31{g5e#A +^wpT#qAR| d@%Yx /}\ ZTYkd9skXE|iYD'v!*u})/.#`Ü-$s=1,:- eTN:%E&e@ LDpjX<.+AaI\DK#NbmG Kɐ2w㇓uxs':pqy !8wE[~E?D#T|b3 8(?~<#WB[ "Ҙ blɲdcS*"&B[r`!HLy˼ɍMQXh|aEfػh,MA#g)CLUb.Pp#wmʊaz[RRgn˃>/tRtd'NrO߹CbH)ATwḊdRI\Tc1bbbzx0&5HVʦڒḏN>f'XN2 (yz)%:eE`LyYG$Cg'ĂځG:\-5@f{Q11P8}xm,̊:˛ff3&еx}xJgՔYz[R1ڜL8 +ks&QْO +\2qDD%d%;(4$%)s4 C ]b*ݱmuS@^Is+c&& 1! b≘,sy#ʓ&`ig\Xq'2eƇlh^d$m,J@ +[12&00Tk`^$8Q)Hs6EaG)M01Cra*[)cʆ&K2",i5l!-NP'%$9bd:XW3.4 [JLRo¦0Lʛz(#HFX/($S"a9p5_aG:3A.(z@'=28ꐣ2Y@ +:tsb:&0}3[6g[--Si'&&fAwjG$}2$`rK[H,sERg@J$@ +TJ/IM$≞_&+i1!Gv"Ɛ{kB5 Ls`K(ƤqѓX 7aGIAna+EXp jH|GA>4}qcO"j#eϱҘwd*Q\(‰-UF:IAN`8s4uhi:VY5P: +idvZp).e"_!D[뿖{E9<4L,A+y)ϚUU:ZĢ[T.taJV}CF2wը@2 iE쨚%,@iӶFojIZiBg8]椦F:DJEĎxs/ߵz<& b 3}̉tص)phsX!t +I`uIB0DfN$:b|Lõ#{Hy[˓XKD_7`q }_t0dqc9 Oǁ,pH9cJH">,S7*WS$k ε1ɑhxP%,~]s'ãATp*Ubg$*8ӱzfVCN; cp|dOsKcǞLA\M$}R&l +EcsW26mϐ( 7'euӌI(<}]Kop՜O.%72r]Wȅœ𡎅8#]2Sr!vpٻ3ވQv:ڎL&֦Ku$ax萎d'T"?G9:l o ++#&pKU2/՞| +X[3``)0n<̜ ~?*nE +t?LlϊJxyXi\fe,Tk4L QReu:G^G,U siCZhVD +MxGV X71º1{b-<Z(\%kb`&Rq H,`Z83pg"0m>b;"Vb@|#0I0.òQ +Z rbq6 y  ћ" PLa +h^PUa Ȭv@A طܰ O*`p Op!- ~CR79 2k= n +$ j 1Knc]C QX,U*'-,1rך_CYBBmmu,6GͱD( ^x-34&^aVZ,z ͯEQ2{sℴW`scP DjnS5+pVu*r|d-hZCZ_ZdmE  a޿?eB'[w"Sp2Ю6W׶8θ@LMt+@l(AM4|w +##"rT'\p(}ꤒBT$/fWJy 1GqY撴k4"łr6g T/98CԦ<,NK>u j6&l3Q.NӂT6l]⣑Ϗta}uA, +d JIU!fj}@v@%3&b(Y|j^HdJEq!cU˸O0}|7D|Wq]i;wbE"S|ښ30P/gwƥnyz`>v`{'RHQZFJj]AmUT@kt>p09 2MIKc?FXg3E RB gg\WfOZ'i؏TlguTLh5T.MZ (5ZfIYgPTzB&.D P.Ue*eR):CZ#IZD]\L +XITwUá&Ҕ) +Ji:;v9㑲c(1NdDsgٖ(lZ$a |jvD\N L.잎$祢D39XG9\/\͓ ]꤃]T\AXoROi<=䦔:4riФ`nͨ) +$wY](BI3wwBHƝi t~3}tQb,df/['2!N.r5' zc1Ả̒2|n u(VIh Ns7êxc>NxR'kgܯrvw:@c|].(|FhAm%oݟd|%ӞLVZ=Za&lK|[ $l MN%9# LA.G=˟855D_K0R-j`9)zC0HW{u5:!k<_?u}`;qY_܉)7 :LEoӣa *R[SY-x;zM?Ќ@m Т/0xl^h{/ :HgEU&$%DJ^! 5=e8B$qU0pb/p8S +8,v̴XTM.NڑJP˨ўHY +[>#KdJt}IЭX2$vu]WT*{s?͂G!(g!Uc#}lk,p~(Qs L*9Ow(/ -".یWv*| 'uAjҤF5/ +ˮޙ$mvД XyLA# +2eHDh TpGsMh!{!t%d#* "j%Ȯ/x?|ˑ|Uj}p + 1'XtEڥݑH* KyuU߿ez$WE:!e{zGkmFe +#ͼJ +K I s+y > +J"c+ĶZ +NC/Q$r:Nٹ+HnT d{|c( ci5@u@B#S܆d2T5 ҚBkm> {LYbxyG]_>u4sH[BZlȈ7*X\s-"$u +|? -mV11^O.B55tع=&Sz?%銙/ ''XeC{`*yg=W ]G Wrp'e9K-3;~V=X8T"@Es~=Ѣc6"CJR>BD>v h}d2$`7RRiFvtl^.6ZșqG(UAG{6avSH~:r9`5DH^f&30nuşfs9s ]9gså 볠 +yZNϠjRlU&HeA57AL hX8Js/Le<-{yEiv붙lx d{:j;o?_eoZ8ZW /|`47Nhٺ6r?u\"8%]Nd6W>TmȻӮI f% $Ķ4?|[ԱO;|UPnN"}\$54!sp79kY,C86-/ IY 39qjA]>zɧ[s WOԞ)-,OY o t1bJSɁb +4y'SI0 v tv:eH-LhVbf7SŬnTi9mC+zA] 4)VWHL+Owv +v^dWmk\DH(!-e@Mg5?6۸5n\ 0.[hľQmt|ZvqI-O窰LaP_`KUpr{Jŀ~%ϊ?s\?IM|)RSu,U6QVBPcN^ Ycf]DܦHm1x"j":T(UԘj{jM3ƣ943,|ԾRG-FroVF7^Is]- +숊ɽpkt}EJI_ fwՎ[]IUՓ!""#H~j>Hnu^ x-b2nFO)Ji/ +V;Vp* '.t0vx&1=TFQFSp< CA ?td"ׇYO7(M:nAѶ3xQ`/G|XX P΀r^T-*.lk =8 #A?фU,-ԉl>M?u~)7<8+ɌGXN@E:"˒6O0wQ8 $ +ÙFxo Ya\V +۸+ +SzPXnLqt466dg-`c!\חph͛O}wi|OOixwg M?Tq Ǖͨds/yR3DBs ? & .&1 O)Q/_' =  +wnX$/,/ pRhujT, Nd@vf&C-쩼 nĈiKwy$)wjriiQDVZdCG + ů!UeQ`,|NP$6yYT(!ZGՇ7 ,{wov|,]H<ߴ;3]M#Zu=֋^o4}2Fߜ07CP}Zu_#*͈}|qP + +9BvFJVk8%3p {לwSgE#nQGܬHq>pe{MN>"# }t>n㤎^F8A6u RiD;i2۝@</8lXJʼ)iYsܞ y/z728μaO@FV޻}' UB:k#9%y먛^./VG)Sgt!+ YG;LvOozYnC*~{K1M'438.RV|/tP-!crt?Mo2DMf[q9HuF/]Dd';Gs貛1ҿ=uxz5 "w:ѣ{.3t5 0@VGBQڙ3 隨nMTRxI(kj0ք4c3SE$6'_o iaO okx/fuq(5mS4R_m(ΉSY3N'{)$z +s:ZzVlR=`%3Mۭ`Ar,6sh ٖYd`SBX$&bZVc}ݵ>X"j,oס¯[nvy޶)Ի_GZwh I~Ȝ=~EAZiaHe/vbC`QY+jNlT~yI{1R/*,9YuE;4$J`VB$oT~uaJ /M xk%<07^F'\kx0 +LfcI&րB 6ov;E,G;%`c8z㤰*k +ܣyl0]lݏ zd3l,}"<[ɛghDx8OGpHO 5"!\p7ڜ)3 ׳wd2Y3C Լσg X#n6QLwwI[ |Զ?DƬädZ+zRgOfBG&Yo=$ɟ_uM2a+$>+x3nO )5-pPf:Tߛ/,ADPdH"[[Q3Vlgt8)=-\EW<8d!HpBqJ8+fXev<46\'.PԚDCTyĆ"ɓRcXz.o6^ d_9G͑`e/a.wXpՉSx̽ߖDB"LPd7@ЈG<`$ZMvVoB*FL'm0J P'dumvg-lFrTM%A`E@-l!"ʸ|Rt.bs$ V'#{S50B;/?s[fsB&T;HLxiO7#f Y3H Jm|xF#)7Jr7$NJyMГhQZċ ޡ$[B_.."qI&Y[Ѳg,9)miˁ-5X'I?g4dwс+3|KG]R] +ɦ)cm3UӼ2[i( S O$ kr /id5|rwL%GB1aq{k r(*w2 bٝr3JsS]gЇxmvW$J0yM帑=5Xv(TRb7_u!.tO],@#CIT)1$o PZP'2xJ!aRۙ82rM4lV?ۈ\Pl)K53hdfafJ +vL2*leIGDݴfU9q* f1KDkhf4a`k7>4~.2?|RoGRd8Xμә(tw )K탇{M #ҾsnVEd;:|F +أl=$ m2%pO3yzm E輙m73D 6A>vA"5,,Jl(>&6ۅ=&`(ꓬF1LAޮj^ D ngFY$؛iZSܟ] 5eFDcO J:7 &3/.n>6'uيe: r& PoC @P8' OrtUn"Sx6qkD rEߢI`gPEqGmUCG-S-@yiv^׹(`tQޛh<;M۔E{kqoJFΠa ~+|0듭ͤaont9DX +/zNq%tݫ4 ! ظ|:!k-15Of6K"J6T߆V)/bm'lhϸ+؅m;AB0PMj kw6:XmL_ބت!M( +!9LJe]E0"@pIVzf !mvsue][rG@4rl&*ΠFl#]Zg=o’X[>=R +$fv'PxdKoKy`$gߤxlK N8; z/ ęulx-d6;]9:qJ\" VJ Kߦ31zp=fC룁Fsͧt)B"Q}^/)2zLA]bM  MeBAabfNEV 0<,;E;E6}r(0߫8CniekʳX%k+\_._f*e֚#x +P6^3e[}Q+E-zSEV*M%H+J>v@tY%boT/{Kcq +5`L1A=t,{ ,a-JzyWP6Mhd8\6XJ~V]]2azx3aTP'l^W.lӗ +Lǀ{}zM%Q櫶UMARy8iU.כ&[єK_yAt5㉵[ !rH_ DcgCS1v`?eQ'ZHsb ҝ(\0 +jMiGG}7$ > `{EOu3Kɀ+×3z1PaWzxxAKR0#numI=ϝbYIӉFzўfKnQӻBAH0T;PƼtmCuֶT#jyw +83t4mcz>P-HdFk4Dzvwl/DIi u㈷&`Z(`3צ%i`H5F83kDHI-m>;V'w + ہءm5aJ?ઢZi +.1, Nh8_:S.'f9d剭ޜݤuln[^|Jzᰰg>D`w *3۽]É|\]suefSƍY=|ᄩ?08R +tTnRS摚Cp-Xf}$I@hE]b,{ 8<ði pGKܸ"?L ׬=p˹ag7Qx:34 Ti;e{m2PT !exz->V3S)nnS4`=t gfИa=4l8Sމ=Biviƨ,8)Tpe2bi]zhx9$+y9N*!;7\irj5>9K`rt${,o:cZ>OZ`o?oGf-x?u-/&|?Ž]?N@'As|ekeD*>nh*WNo+퍠 XkgsVtό1:;uE*oz}e %ap$x:n풘~*_ ++OGV~U.Wj9lK-UbZؼz9q /pv9?(!tDm"OWٿ#+=s5I5|=@jJ?E% q"uQr-'U@!2wSd dR>vHFK(W7z v& mdCKoLXFDx;8%ǁ`lK@yZ;ub5P\FES;]bレA|ݶKJe_|ozY4:)> U:<5T phWDOЃD+bƵ>ͯ3zw W]<5z*g|22!g1 h}qG|oW, \ {_ d-Aa8DHRNs`)زI"<;!Ϙ٠rVJ$*\˪mSNrLBL`T:JTtK֝D(kmØBzu@4Ι.aP`aEV]h~L30XwYfrʬIj}}zhU8ACMo4]`*'x +7rO?nfPO }r&P0I +уdvXAN"I,>#ˠXewl)LʳolA|4/Mh==w_%<²K~!*kL 2wx: BN05tRWpLm/qTiIҝۮlR@m/D8!bs?JS(<,*nad +ln-@K +*'mll$yHPIRCM'6BᙥN52XyU(<DmZJ`S~U:d&pH=HtC!^5,mނ8A} 㵨CAQk"הFJ fy&n<ӱ`,}ʉc@}C,Nf=qM"m ֗UN24"{MFOVN=ʂ o@Y,mACL\+g{ ԠRoR3PM:QtEuߋw^ -)nE&P]7Uq7qAqwݰEݹ^%w׽.J.{]TuQqwE^5x׽.j{]uQw,}vwۃ7و-3<%^dkEy-܂u;tBw%.F^Ĝ +i+O)\x}AM \-6 c-RSVy'R}S"rb=:Dº+m)ăMlx5u/S]ݻJ" +2t|E]Lv|γS~FoDD[PQ+w}G!;|_Mohjnыg۸-G>;_"P[$#*|Fw?=>u$*Au$7p,jIsjؘc>%h cK<aXu* waA:E xnS4\#"7.q]ڏr؇`倐? Q't\:~n}2oF“ 堮>Sk6omugߥ|Y-yX7d쇟HI[o># u*#,v;3\5`=`&IODaa6\JKE{zQdaU|^፜J/qLjjQߦI؞@>-p紽]:;lUĹe vs_gYr$/~Lno.QrSVLིu~fX" C2a(~`§ 3Lḫn)M:?9J(#MQ$E!VsË +y/hH!2! jw9EfX\SLiNOBRg 8̓ ["ZC(Z0 QgG}'Iʆ@%XFlT#A )ֺxJw*Me3e<_.dẙQt @G}(`W,">XqQ':D yʮ槵mi>"Cܜ3"\UCvL`#$B6V+ +eT0N:{VYM/VNB~ap#MI)6CGm=괨|;L"D]G|SZY$]΅Db-:Fz;T5Ͼ [ AI` +TwӔ[|A~}a?zxPtyGOoz;ƹ|cxeKp$dLeGS\üHcs0۾F_?.o~?>/~NG@e@~Þhg?tT!l_5aAY:'aw5/jTdi-*'̚mYW_QH\A~Ņ.N ,ߒLbZtB-"8JXAYL=e!{' jΙzKBP:X\[}eĮ91T}Adl\*&Ayȼ+ H%ùtV@kjb^sGm,(YjTն$~A URNvQ47Jb=9B} :J廳bzb7Ya1Ad',!kSun8κQpNaY‡ԟsWj"/e`^H+ET/oܴKuT\p( ǟRM"Mly5ӒZ^LI< TQb}TӐ\7Y6XՌbG4MP'뀪ON8*ۜ[,Z׻3W Q}Y9_omR6iDr{x Tb3:S!8t,;~0 :- ca4ՠUWDW32&+ +yZnjl>il+ v:r˛9o[\y*Av"L k%ؚHk^d{q1 p暕mlP:D,6삀 `j QW*L`Oqbтgo2Gkp?59&g;{¤]H;,E[7dPnW;%RT[XX>jߕ8ğ}N2&7B +rd3{?hpFa輢fvs]SjducmHEgm&NtDIĚiXIjo3%9I 7WfpK dMj?9b]fI1C;%Ma]!xd51_~v\'g)jŲ53iVDsV14'Ww˖6w:10s5L$VC] ]6mCa,I}ۮ%6Kl/H[IY68X2 A&|hWokWx!x; P.BD ح^aޓuds3<$.MI&lB.9G )f_G'Jώ%t@<}ЍtcS5s=blE c.͉(mobUg|}G2K^9 dm='%(y{"  T .Ov$O+#ۄoHe*"PI[$'TkB6oHuBi+YrsAȐJ XX+nK%S\L){|۸S[1!{7@l;GCe8rϚO$|lbhqIAxbŽE}Z0m;Mn5V`Ϲ4v< +sGI(Bjz 7B^_m:4@ ~# 2u4 Vn$Q:Ȓ. 鹋oHIOz&n~X tWvdVTр郝%8^kPXpY>9ۛ,;>S2Vtv3ȋKYb9h7E.$?bS~@)ßr$iY> B*QS@X[BG4DXZNiMw9DCj'!h3lKqGtԵdi᳘glvq6< x0X] ,DO>8HvA[LH+QjM=@ +B^R7-̤پO1Nt CUz{Z;;AmWv;"ʸvgabEl sC^Vq if~[ s,u9V%:,3_QuXGC[pbC0fZ2TknO;Akէ͐rzVⳄ3 +cVչ§]9Sj-Mƙ7^ˤU[b[ qz_l%hqJnz4v@fV BP&!lE"lЀ7($Ou%8"yN'8S+fhQp|)\owp蒮zh]A^!bKe[G<Ԃn1X!sbE܅dbb(sFBH}Pcpb$m|ZC<Wj +d+6 Vlnc}>d +ǼJ̊Zfx&ytL1M61!½CB,w:y\_3z}H'\0}`͋mV50Ҹ{b]!7shئCHt$},{L}3!nнh=r$8+Hs3"]R\cI'yտǵ^+\cZpex7oV+oE+@=qov`ZU|{wo8;3E4C!1 ogA4(\Nv.-UE`q|L|]^ i0OJDBFqۖ&v##K/<'̵o{=:q䮹Ť/*,*5_*=4By5/LO o›/%~AÁy?dY4ٓb֪3l,$މs lA푔 )̱Έ~f(Hڷ6\ PkcQogz2F9Ff ޼@?5<.Ż{-Ɓ?6s/EucwƥnMn^o8}>7vRL ѵ!Q~wb9Vot1ٞQ W #㝱ަ-s(Hg3; [Hƪ) +<4H;?"%CLS9uw0IBIT |R di.G4"9q)Vn4\duRє/Lhq!=>Д4m.|$x/IPEp=\b ~Ztd=ڪRDJ<#* #0x%ܲ5a6")uNYUSOUtpyL[_XP7ky. PoZ3,>1>(w `sbtmQY>69ܢWp8>Ǭu Gﮞ)P=ofd36yY]HSxf[}gqTׄr:\{EU $G-kq}F$ afnZqQn NT7묏kRI$W|!g=ԶZE4vF"(c=vi]E֝ryəu-4iZ\HWM 9^aJ,h7AVYG=.0Qk/aeBXNMXףObӾ@CڝrW%cCt̊[c.xdG wFnr[0FaY?hس”[vf-4ϰ ÷" +>CX ;0>|F}zn zFI]%iV0 ~%qJ pϩ +Ғ43v<,1HSw1`B:3OJ]H: w#%ĹMޕw RUA^JvK(˒1l ~FuΣ*Z3,g.?]ٙ =*G{@ȧO'Θb$O#E iZzJJO\Bj~2Slo cva*,FRM6/{)9HnON{* jJ0D;N]xL.* һ9vT?1MB>Nuy r^>6i^UR_IRSrڮx/Q +«t,0X,3KmweWW3 (L]&!,w/cȜ]'A JN2З!1[T8'5)a8Q"8ɣm!?F:F/ 0hPٙsqpl_؜Yׯ O,z,aK=FE {\.f*LÒ[\ۑ]R.m)xAP-o,H%oTXKew-3.Bg/IJ:j؆DhHOa8P͹%51dh&Y- T@g7dd*v*[FQN :1 ODml c̽r~ua&@~r@6gs2l܈`Lüi:jvFj[UN#qI}$0<@jb5uk!Eܘuqa`^"W9{eͫgt&vJ5^osnCoJ՞P-mvxٚ1b6 UB i DoO/`Vex1΁Ƴq yۍ pAfQӑI2T W-⸆sBn-E+`눬_)/9L Y芹YS]4ޚOuTmfӡyd`+8rHfyuY{8v%aOoq)A !ͤ +9h^qGG=W?軿'*Ww^~l|s%SN_|WwO?mܠ߽:'m/?lp??o߻_?r]s }??տ.OW=6Oo.T E}_O>?o>zmN~{P?+~?;?o//B[齗LmtHEP҇JN wY=Hr4u'y#Oǿ~|g?noeTy^~;+׏67ۜu3@b_-~1|ՊI/ֶwյOKg,CCB?m[`"+8<88@t4u듨op:gX2`aibqNH?o͟ۀTGlfhVsu:=fJd?gKg H^#k 5O'!TŻyݭ!V ];?PF4π3hZHБ(Y 3D?EAK+ĔnzVmdaDǤhq뎆̰& Pn7_ +x@ p{;gB )δ/0P>UNgR\B 5C ?@OIgW`"A*sRhHt<ȉL59).9Zkg8z<_"I-zJьͫ@z/| A}'H}\,a?Yv|!$:v޳Z0?Yg nfIΩ!i4;I?%֨ :h~FG 󗺻Һ}IH8]r?W`>`ґ%8pz/M_auABiؗ; t. bKh7RxO)okW~s yBJ%> npݱ>5m `؜EŭW ړsl^[BcrF^e@ryl_kЎNy ^lɷ,g~ġYܲ 8QK\s36"2ge,Lg4k\B7.al +HTְM}s&"~mi,f(Yӈ[iA'$3u{Jrv+Li5e!]rw;}ĕ}1B>){Bvaٝ󤋛absdr g,۠ouPAu3iO Zb;'hpQ"O3SA\r Af6:@2  C' xh6N-/0L܀_`D +tKw!(|Ӿ@ܣRBA }1*Hv9ηJ +F9(R3 ItNL:<w]DD[ioNUVP.3@Pa弜dQ<T]%S凎9^%`6F +`vB +r'L6Kha,F Oz7HkN-f3ntv4?YeTsQ? xJ h~U \20al+3֢ǩAdqC!}p8\?< trQFTtD[iA[''b~&\7X.est63IW U 8h;A"$a.j1[OJ6.D`ۅ' +\5!>&j.TZث <M͢"p +If K~VCf 1-N":Q4^ *=$'q0.udkA) +WSN)&ӸhPm`lK{p6d"4`>~U1i*Q3^[LCA wz=yRp`QSOV%+6- +͇Cp#qO8 +NM<~\tWlQhKvxIxIs' ,0H lvs- fmQϺ6 +8"ˮpæ)s>B!-c.F3Vna~$փv_PV6Á4u&4N $˒˳XBPk 9C" lK9TBhA2`< :D+XWBY}My 2vX=WP+>>5GyOL쐂&.ΩhQ0а{h$B0pNbq~q×8m5Ë^T덯0u@Wez/;,6&*0T*K,ԅu@Eqe:Z Jr۷wA37U쳁D"Ā͟Nۦ_Ɗ +9 ru4mzH*jq8~} {"![ 6NpAcjdam 3:aE N%I,1aA +=Lq18[}*~{#Ҡ~:Y]ǿO)ob5ܝ칶fك5^ir𠡶D mʞvɗXw3FHRYL$8?~*T= +f н/4#% \h $ͣpv .EGg.h`pU :=]O4"T“̛9l+3A)%L<$7]Q^ <<\wbׯ:uʁl+ވ 4TjhGׁ %qocqjHި=E1`ӹ`Z\9 tW$ dz@> 65Al;~6- h ߫Dy]yV6|zN0$0$1˃ N ALN|PKqgk>HF o' +A̵xkaAgOhX= 4r1,$U,? +{q69\v?/'i6 Ҏ $@ FɿCZl~]HZK ,#.޿ rZtDE H9@$j۞ r1tlri@YX32,TIepf8lp@_ /E[<t2"T @ +xopE5S2ȚV?y ҠWvT5ɭ.wYf2d n5M0YH8vf iHI*F))))~}8hBA_@K\@ t9KΤC;auB +OmX!؎zIEUL,%Jph3҅!Z (=3w}VeLHazJt;x1իxBt:˅fCsw]gQ ,2꭯~'D̹\ɍ(!Gd/@)KsYs=jݧ+ j,9#gnco(zX&}SAHHPvDͿ?>๰$v78 R1tt$crZF* +!BV4\-ܛz9ÿ$3fa\G|IZXx{='@ԭ(A:gϚD69 ܟ8ʳSu .*_4Q, +^"wI^=i;6?\WkFq'*lݟGjh44ӹMmIog<wr{%KbCP- x2n%-@>:ѠAd&޴꺼@/8RM6qSaOx*GYYX1V,􁜆FUt|O7(Upzl$0..L&>#tGùMd-eqy1otʓIK]_cǕ<ي:( B!:)D!M +7y69f97qLbSU=oU8 70%=m^|j2Ens5rd]_&0a)ًp8᫓xX_%"!x3 +e11A:>l2-gJwzXhfTginhL!Ģ ܂ g_bq|@k4JZ31㌃ e{Ӹ}0 - w 3$+Uѽe6HzKsΖW_tXI:l= VxH,̘~NA1纜Q,þ?H{{!f12.dՀ CD; |,!kV,dbCTfqi$dkXD4K+=j{<Pa79gM۔Ռs=c{W]QvؔnEP,&e!_!?R A)~ɲ2I%?B)tLgĨą S>'# +dt'Ѷ.MV$vyZnNwcntee;xx:m.:z)m*zVjPsl 톾$׈-[* QT B&DYAΟ5 -O^ +7xmZ+(;&m8zi7Bz'3yT`MNu{gTj=PgUJ -0[଑5SivIp&%2ͷ z4%Dj/?Ԁ {mAh$s,( +J®?dڻʖdyOwؗo9b^e@Æ|!Ai&V~{\VbVsfC8?沓ӕQ̾Mض x,Q/fπCy윘'9.Vg28D Γn̋ +D-=pnDESWO.J / +lXum:v:]ݜ]SVzwm=ج|\meeS#VN =G=)^ZnW )h@F80o`PXEk=-3Z$:}Ãd:etE7PB +3xUL!ZT:8Aib稛84QY`Π6,wPѥ(5 cP3vP5S{3gQ)ߴCTź}}p@˼ +*3]6T7]+vfhơKٱ*?==dqbJ*RwB4!!X +n& +Zk[1(YZ4W^MJzN ԤtQĽV핱蹋T%jCPsY6GJO.EK5,[PA3JAA }dFcb&]d]8NDaz#3/hpM Jq+-c 3An#wFăbOw+Xg $u>3sQ@ZpPl"jdqgo9 rAp M쳣 +k vrwG#.XvG+!Z +?A;iwoo??s!%CL)gΎaI~V4O`.Ha m2'E'+Zv?@"?R51==_J C /hstN s`C =iaw߼ʸU@' +FcΜ_@C,,UӪ(wЬ}ƛZ.M} 9`4 +!y8?RK kvbh@ Da @;>RP5vp?~rBYY;(Y3f5aH9nQ0a;Ew'#,āR$ɴh+cm2Lo㫌8ɜQ2_q@3uElH^ʾw=sb|v{0xNa qjs-pA6j +o邰X$Ԏ߭`<-y^B(nXKZ91ɡ-*G "Pt ؑiRmgUb5Ex<B] y;IbGfKД}i/*Ofu +ɇ0M }CI砮 .be_=쥭l#;(ZVN hUky4{M17^"ǃ8n^֏mp:*ȥ[nI>{u呜^=-ݩVMTZbh &>.VOgre)};""#k?oәr ɷT;3w˙T;G`<=رK|dԯn{FXxMA%)B/W!Od#O%)P_9?sK_%p*]KB7"vJf}[i";{~Бl/Gv}U_bj I8%`B!^hKʏ C +DJX0I,BpPpPԳ (\tʬE)ymxRl~4 XY;NPӨ( u $<60 E'@PY̝x4@hVw…uͧPxk@g zq EcxXFٻ~~XePEr)H^{D:dz_FG$ٛ,4Up>H| J\VSԏKݢFzS<4{.-5g¯ pG*0B%Y/̵ZhwD@vč[6 q+K`O1jU, {eّYJ#$Bڕ+@!乧KI"sH 20(B@p@`D y P|;9Ƈ|~[EUKzt(ڞna|I S#ZD  T@KETE8 + +F1/:~$u"?~L!)F2yO`ϊ/uz8r@-hՂ3iTEX .Ї1X/ RľotAvȰ2uXӳx>8" |A`W^Vd Wwr 0w- +uë"wAXOv/(v[=k^"gL `/U3!A/U\|jl}Y#BvBQFVC  e~Gv!ASwڍ C7|67^Ѐ|Agh.EJ=JI. 8ž? %JamN{lOT9g)Q(&yU=]֬&*Jxd AwRHLNEPe(ٸ♴|9^̛w& D`o#\,%64Ȼ̧u(YvC|(!b#8S*)a""A!Wm9dЕo5IwWsGWk*HT z/G1R!Dgp:R}LIlEl 9QfX '?/7E'Ϳa v#~§U͟$5|kˣ^hr,+-jmq"SfvZ]bY,ԜfI9IAn€$v2XA,!bJ>qx9$2:=k/V0KP 9x&@}hFa[Q`D 3OP["j?KRzy5瑦C9E`ݏ2^>(coS9 1&y CئjPr)l*H8tk$Ȓ#Ü|<#^&D2o-z % ӟhx +$,,nȓvU 25#"hzLXؑWO~8V +UF x"GVfsC2(Y; IH|#KviNi\sFi$b‰B +-38YY!_M~cF|YD3 ӱfM&ߞuuo(Sc)ߗl=G + ̲jN2"3*-&PG#s#8Un?%pJ'J(f80$Tn~@Kǎа!H"0'HB[vp;w +ۿcZGSA"Is;RXrB']"]AѸlTt%zǯ zPD0kJ3nܢt= +*U}.*OHyV0~hlLsZX&gh +\wSI7\0 88M)"lFy|ה9+]hu$`:S^(Y+R^R\{5yJ5wg\f`]0?%口_?\O")}F/MNU߭u^g3[>c( MR@j *ļj *t@8?g)wp34c5C f`ZgXR8`M U/R58@ήjU5p^f{G2Qw^2ZXFAv3^MG ۼnlJ^x:ϋEoM0%d*0T>O_H+f~?_yLJ))b endstream endobj 43 0 obj <>stream +s xqz\YrF\XIBY{3a('Hk ox@sOe$m_Fq)h +>1l^( נu,T|`@STC5{Boq8r:{bSq X@^QJþ10%^_ Uui9?KۋSqƦ3MkHWmۚ vzn Ud@ ƣ<|UZEPO+/>DjN,ҵ9zh$< +{dOY"t@ۀ 炽 o`!޲--H} =퀥Й 8tԁ''F3/oC' 8|@M?6Nh_pƠ0mAHnD͋9`Tdl\§S)1ԬA>escwZ|Y Y3OۇS IlIw0n\b{|`L-^ 0ADzmS$A`΃HC ON\\&˔YCu]!Ĉo`;G)ZN~YݺOc?i-G1Qv3﵄.WycĀ!yvAwJe)IQDMBSfJgjfNa%k-^;dS]Qe\c7@1J4pofQZO$o؋PU }{C 3B*IQ#3#1"!\GO:4gm"Ѝ~W:*BPu +M +;}'8ȭFzW}7ņ_be42 BltMJ+D?XOAxW +*'ʼJ>%?4~/Մ XY0:pARodMRW11m-I@CԾ5G|`,Y뗪nd;>tUZhȩd;<x:Ou>dFs +M). +@|45)`0갽/figԩ=>3FQhc`̙ƫ]rGԪcp&ZR)1S]R !=~KیLy@,B&v컶:!1dy ZNq,N +] XeEY"{q\=B@E0$0Hɬ!qHoa<`եꑄH̼Wʖ6m*MSbn#qq +TOJPKUIR>:~+kw[:B.tYNݕpiZuty ,x Nlv܄cO5 da @ôpk.?Tq^qɕ|d)C۟@WDO5k넴KQdeܼ Wo^ğ4kgȿ[<~x"ME儠įccOoa6m8P^Aʞ;;[VX]ޖA:DD%fއ8|u P*э^@W,x71Vz; 2oZH8l#)f>=(SIϘ7x⻄/zUMj(raf:hČK*#p@~;F= +4IQCo$װ~]Nʅw78ǖ@vQ4َ3*uG1_=ͤ|2+q"^OmN5=]1?Sq[ߋR,{|йK3;nQ?K͏Jb$T:ovF7 Տ}RP%W hj6s mXdBIO"| BbEX(+̓U< %Jvf3;>`61}.鲢vvs .x? g2sF?eQ?;#:mXtE70QR[] 1@<0fƜ4QNZJEFV=0 8Do:J"~T= +nWa@ڐ/G~^opfT%Q@9 'mJcjgל;*2elxS Rg& qfrC4d,.xeaKai| -@3#WB=XLe4y=eW[G8g(}1)}]O'a!ZX[WAټ?팷1 /p?'IgPF|>U9"嗡L]eg\C@ *1XX "Ho*|J=N秺EKuf34`!m6\׼*("b\<3C$3Q>$ iնqq/Zql0sI#nx|pP._Lߏt4'Ѽ~%١(BĶd3ŋ3₡W_'C|T:i[!{ٹKw ~2/Taݯ~c,M}} +-J,e?%|1rS@~$bcہ9EBh2$@lhD!>1gĕʁHN#v^#,ʸ(+c~:d@P +؝'y( +0Kq%kMW4\j4NG!>NQV&Sq$oCƪ^TךP*N oay :"]p@F +^A1Ȟny)g5Ck jG Jm ޛ~yDd/CLufMMuAcMkE>ԢE{xSI3 OȨ o}lE˪HtҚ%DS30k X#Nd}ϷTo{j_~_68GuyA/f|g(E!腯jL9aDH+M:ˡRAӻ8Ǘ2WOmFр6@dsgIYZRXZ?pE]1GA)pamϧvծFςWQ2$788e!| V3e@tRIӠS*IcX0#=gƌƌ3bQ3=qOQLrYwgBUȹ#bR'4?etq:bѐrM-:gDf}J?ZMCW 4%2>qXQ7uk8C~:*S|/փ$аM ^G + 6_4ᢱg ˒ ؃:l5: txBaF*\O x*>C +e͹ۃT#L&WNRQ_21]"}*$S*ʅTu$>:n/MqKku,f#rGH' 8+f Ljri`O5٤@SoW4 +43_*X|.%M#dK[q:"3ݛ?>6W'V׆uO!C dd55AM"2EqZ]_ڊ[.J?Ů 4 -0[n&p贛q~ U뛸@RS]n´Ԅ蠮)Fs>/fS|"pjl"<7B;muO'kR @({g땓?_@hUGc͛@dl`= )D[VU~rDLy/Jg<:eb?@"4!At <-B {쯃MD3-vdȻ)_pXd^FBHV2lG[3VS pR[P=2rU%@s׮bm=)oJhC7TĢYFZA-N +}61xj ?K +|W*p^YsLJ­TDBT/@jNQ%LAc8BjD:{ +εfb< EfSAIg7tk)9ĨAwn?!UN;HI[ Ε@hGN+ԋi-}Fb_)[13C 221xby2 7q{O9wP* Qa?y +bhH5x,h&=ĎG:G*8SĪ<=|XC4@;82͂MW_۩0ر@& +`ZA9PKf V`л3NS}…H/X|BIԜ 7æҲ>.o5]WF{/Sh*vr@SH@NyNLNjC vi`euAz4I[Tr(`[#yM2Y3F .բ^a#otb;@cY2/1*C"mpuE hǵ@W*P؏"4]Y;vWB 70~0)̚'6BW%h"X]ؒHcVDf,% OzEw"_ߐKépۀz=tU`~@*`h4ev,KzuO@5l" +vRVhZ5~Ʌ C&Դ,a"ە-H ` #o_Oow????_?_?%N'??ݟO/ows?vnj+~Ų GӤ@1(g>蛼P$ e3<{.n!hR:rOco+PLQ$аc?G*C%q%``)pE~(vId@R5H)fJ=c 4{ i6NP +ݷ"R4pS< L@j9t2FJ}2n%<>A@ltuVԱFZw/8Y@ 1'/62o0%9[eU!ŌܷIXwh@ mߑO?ńO5P{K0ns\yOqQ^d؃g1+OLR8 +!񾀊^_N)k*d5Z&>3@%A cQX<{|hp8сw;)$!aѯgS8o㫏: 7 NZ(uqLpB0Z9t1N] b2+r]LGFEQ5nH+hdpD&xoxih%+ +Ǣd-GX-Gk9 +CSZ(`'/m!s)/6guAyPX"R\j$330`4g3'z<`Bj&.ZҠ2RH=LE$c5!#K14"]5P)X(kLt:oŽT)7 muﵕ^323K (/` )`$} p;hNdxQV MMo 3Xce߅:3X)ۅus8rAAhDσ̈́@Aҫ}kLez~:NEʇ= L(P>eMA &?c?$W&E5F;41بAHc~*!]Դ_̒ͣJ/ &[dC XAcRu1#i{dLm#!V;͘C<`?Tߋh9ȫlh  ~=?t KWI !`GU(R3Ȉ2oJ(3 )͗ +gA"jGrj"<p#t(Apyw||n`..ah&uk*Z"@)"6c/|kI j p1^s^· A~r8PNemP(a()9g=h,8HDc+a/=]d deQOEW|JS!`BӜp,ޭl]JuYsw=Da`;"Ul@0+:i;3">p +:%씭Ԛ>a 2Froa`l}O4'vRTJ8Ey:fAj{l3~%Jyfe _AM(*u6@4*$m%/ۀ-XJ>3xq\>jYk ؂;hx> a4L#\*ʰD4u6sxdf(5ѪNcqy{5k~g1jRѓly; +cK*H݁H,AUl| +#9>  +4KMC?vYV_<7+QD[+v]D[1^S|ɨ9,|~ G!lО /6-|r2W$&h1yzr/"c˻1^X=0kqM^>qOFBjʯ>JoT!0ƻ۪jOCguO{ o (7O=Qğm``dQAR,#?(tCFg7&cCm8[JUJ4YaAy{we,] C+@MmקZx, )F캄ʅmO+]Dm䦩~.}!kWJwLHV#@<ɰʺwOc}]rKu0?cF1E4Z&bcP>[  ?AGE:p@SrJ|_ 6@"tĄO4ܿc3[sף|~(H3 0'G5 qJ~C u:v{,T;BU׿W$x,BQ=0YmZT8GVPɤxQkT%}n:hMdގ^> յ;3u8S/"M3= D+1;4͆ +b,X2[{Nb]2+ʳcH l&T a~+OQdv__=A1KM +J!f9y뵕 `/@&#a8_e5:!()~/d]Q TՁ~HL%xlyKP +_G.& g1C 9Y QĐ;*:h6)XXSsv-Zo3oЯ\Y8Pҵq޻H +G =2灟OT`%&~l/(+ b!ǔ)G"j8]O=PW;:P+YOO3^ ]Sٿ8L#oxϊa ^-|-&cVYaҽW;A8 'de q7D!9fyӶP"'c $\yW*+gº;W)8(KS@@C(<$ucBi.8={:>wcոiKԦێD@O"8zA<,WG I8z*73#0@n>Ltyr2"sPNxf47+pp,l#׼Eq6k5m!8e{ϺJGMq'fU0aσ?0nE>*b홛h}sr;.k6?"`.湞Ixc %, 5 pa7^X:ϑ3je$jRu"IUW3 +gpBe--჎{ r|X* -M!;^ڗ3X`i\˅tɯݾ{lFlW=_})7ݜ%u9,3,7qeAb.^g,+$Q,f !xX)+F~5 +k)ݔBr>ς^F"Bޥ؈Cо"QG4 '>EpYVD}Zs +CroY'G$sDxb5y@F +f!\]TUlDunh+S B>~Dv4)I]RC{:! `WpkDm"Ć+?{)MtqDwIӂYBb.fF;a.ÏW!}G0ۀ`IK¦J2)XP%DU p5L( E|&Z1r츻 +<U`+m&0Jv8q;;?u$OZ޶EL!Дƣoi(O3vJS)e*j*ì5P=p<|4ZBH$4) mH'ȂоW6Ͼ Q=ZT;!ZbuASAʌYY+Os$§UܟV ~5`aZ]F[P&3K] 8|FJ㌩$+mK;NO\w AkNΖBQg +6t +&hI'o+ʇ%(4\T+(wVh=zd{v$ĒY!ǠDAܻM;, OT<axཆ4c?PI%iO/ lo x dSm +KNo' HBil:@8Dk0St+`9X^m i;,ŜSSIh@B7lQ!?ef~'̒0d0(rf/fQ%֬XuWq!\H0c);/Lt}&zu'X3b{0a{<5Nj 7>CP萭x(8<N7Spm0 +qְc ;Xңew5V}a!7 QY9QOC={dVv#Ѕ=8$kOgr:{ZÇ Qw x5.xW^7`KBS΁*jݢFXzܫ8 γ=SI\$ӠGi6X tSlj8e4>'R~;>ɔP&pFLxWzCJa9{KY+[K.-a! +@;j51Ayz+d|د:s`<_&5BՖ&nG̏R ׺|@P O2Y!AKL RkϺ~UtmY.8pj{Ȥc3 1@΄ &׸PH^GF=:̠Wnj}3M)᜽H^4^x#zBZo34i'BdI~^Cvc.yKٚL L5Sd)z5ҧvC5 n&]+ KqA - v Bfiۃ %Qmc-O wGqc3-XZqTvtJЕt>G5y[`*<2.]r;x20hƆE7X835_!@ ezM00:/z=}%Hlzrsh20HEϻ f'יXa'Sd#S.E_U]C{-$(Ol_f +D$ + +~ ?1؂Iٮ^N-[Xx9H)}ңDX`Ib=Qc mjrfZPς!IAf-WM+%TV]-6 ,UGH-O>`Ħ`0U@Ve-lq OPIgq/L0w$U$/ +b5,8Oub.cT=JT*H#dd˨Nyz=pR~{)|6!s dijc::NR"fpdJ}II8u_~&ܸaSXQ࢜ߊbW2| bsp16FOЅ;U4$%0 +tnR4cˀ  ]nM1m"9| +kF. 4.s[n-ɪn4 Hhۯ" G`{"^.Xa Na>xr$boL0=< F@|RR:T"‹Tv*ZDGl8,N>) "ziP^hBBD~a4!N,f? PN/@%\7eCdR,LutrTW82FYzx#.N#aaH |D48Oj/nGY:2ÌK{|uf6lY y{h-PpF/f H 13<δrc7DEҐ]C878!VT!RQEa!Ew='kvtF[ H:1B1ρr6ffA4a%E)Zmv7%mTIt`CǩA~kL\°V`:{0i@6?JIχnp,FPeNRz.;B_@,퇎 vInLz+Bv+pUqXk;;Kf0qvO—f/KTGvdZIXp70+ juAv  Z$ieGe*)yTlJ|炋d`=Dn$<3P陨q&U:)/~ &Ydi3R(WIz{3t'*_=IYjMTaM޲|yʂй5wꡫ{[f"?W0N':V/IjB:Gx6UNa R84fWy ,:Ӎ:& ¡9~z¿QK/@bjF_c(mN\E@(y N^n{o3T5!!RA u;|ȁZUxzQGĪu^p-83 +ƌdV!MBE4;?+*+[lZO1Q[XՒ@kHH")lH]-B}/$x%'a+ H>JTSPL!bRGZ\R uLHCÊt$XYBγ4(4jVHynE #J8)Ǐ#c;}ux{>^G /,{4 }991! N fP` +r*JA eLiz:\ :)SĐ"`UOa@= ꈷlp15  +XA@Ǚ&TJ@ܴ0M@T١ӕZ WM^=W!ze4ziauB,Lj]uV1no*H s(n,* +u'Iz⾼v.u*\JIp@j/S:TJkxmR~+K8vaZa$NMV"S@ړ9IJ9?q'*.ӣ:s zr <79Y2mM!3v!!s75XIB[T+l6К#-͓@2h ,;a."["x5bD5ɶS,+E=4nxVRMQ aG.%Fמ~GXخq{U|Y}?`HxxNTE> +tG#x(/ +Ba_]@sf3F +ETЛei' { +A`"J>)6! C#sԖi~`qǀ (xTi)K%=D)SMJ$Z=GT"MWg_8ofFP }U^*<j YAۛ5$-mPpoU34v}UܣΝ +G'؆ *BƺȢ2c7+k78+0R`1}1RTC Q3 n"^g^.w8+< A/y\;Ȫ.\咧'8Q m-1a 4S/ d6:{\FU2! Km#dT,$c̐ڳJ0]Y&XT9dOZp:<`N\GBJu';@!pGA)1ۅz%a +&6*eHᒌO S xFEC]UdGs5kiV؟gAGewj(mXOGqڅ%`<#cچq 9)ۋV6>C/K`m6vwtwYg~`*^ +#YeV1ĀUUjC-6: zH+e6d/?? A?d5합=p3}ĈT` By\qQP'TzR`{BKE_D5^yfZq.&-!2{Iyzݒ"vܭCKwB}Y'vi0 =֐Lx6S95vϓ*T_ ؔ=EΜoA L/X+`=i)Z>1oN0ymO7AksiaD" zJ`a;F= +*4Ț,nfȁEKd@u{ᐷҤ4ObR;`G}S5h)`F j(j3u:kI[Q3M +_I8OP-GΝEU1FhK _@Q g~ku;C^k?Gǿ ))E e%D`@ GhBUVPثC |Ryw ++sCQ"E䍍!3NάvR&_))]NLPM!xfʉR2A8-F/U7/x_̂3Ĭ",=Fp5p?Ec_6i0齷DPsV F!87@Lzմd^(7ACr5,%2@ziI!{T)sOaut.@f舼#:y]%5?"zJNa>9G Ug 2!wC_SeFi{ 0ء?z0CǙq C lU |EN CS[gF:3.Xj}R ]mfA +wkn`N<=6Q7CI(G"y X'rԄA8Dm}H=V/8(q( a Bϡ4~1 E'>RsU4>A[g j\Rwr)J" !a@RJrፂ35u +)A C6 +䏱D?ݪab?b339jsYsY#n0cqHwPѿ#,fA妖*A +n[ #wGuD@^R\8ǟoSP./SugpԀIzovJXX#pz~H&Ք`L[xT8N ,tnULW{؇c3'#nc{!0`X;獬MR̪̈iNVR9/%dXة͈uD=N唶Dr%IϴG5,= ;NuNLC{0M a;r.|CHm {bD?G x%i<{9cF:5մFYdfXh-Oi%V@V[ ~㗏*KZwRNR$4@ 枢o7Z,3EwAf3_9ڋ1;wKSbm9 y͉$sp +:޵Vn?א,-BzqGP0GwY>ȢksJ|Q,Ea + L~0D&\VCEIJC7وej]GW[R#_! rpc@ Nz;KY zk z&vb|BJn *Ca^yC({;lZcäzR)1N"n<խN|;ڠ Z2+q YyǨ`\A /ǻmA1}ht}QS0?Ct0(q>1zRf@ჼLtR*/3o/"uo犋&d(xJv< 'LtBF<Aa{wVq}#e@HԦ?H. y=_gtǛ&ph& k= 0oHGQU<P Ţ'Ck'Z4sG獈7DNwK$Df=I+pqXm +#L Fބ[B{Z?vt>gcWuvPЮQB + O &i N`2z+[w8YW^Ɍo"y(2AELrE'vk/R9?MhKDLɣPiV5Uiu?-  OB>}3pN)[j I o|.r`6#kR3r֚VN[ "@S YAÎ-:fpdcJ}vYQ׌+3MpUkGwЊڃrgo=4"`)@ [! ||z^ͷ\bա˗^+Rw{D1H]g$O͔@)f(x?3kdnz,Dֿ)+ c|7xGb?1uR2izxKͭ~p!tji0*0W?Pw, %"!*"':"^:wf`X0떔A91;?jDt¶(_y. Yid =F)ySgeף&.ݷb!/.$+;+tNY륑rD&]w~)Eb+ E6뱳7buOZB@osoց)k]kc<.ch3([!ؔϸۑv@%հUTb^Agl,DsǑ@%*zr^/Ebڂ6ghXݦ~I` ~$>H lCpISkm.6bՎ\) i~2 74B?ґ_]pFu?jegZ aqd/?r +CN X1fŘ.I" NYpjL$*RH +g\.(;:tp$~FD #R̺Za3~[}Zn "2ꅿ>qmxx%+Q<*Nӑesʾ6&yotr +ymw-^EpFۗ>#ջqx~k;Ώwͨu.D:+ oLxvGXglD zL3A4Y=BH0 c>""6p/3, &kqR8,}ܤL<垁h1|MF) |F;:V T:C VVx.UGw#ק5H^!X}@͈{( \GJy~> "{9w&A<?wh н|Lhd+ALo~RO*Jw po3|/࿛^-읐yt2Tmdzlq[6Ptw w~Mbr;DZuK&hpѣ +0Z]51> u-(潘 Y-wpaylG| ubFg;Ap1PwHG[9]n\fzݟFsHb!վmPgi6ql'8!cmhQ !^#^$^: O\^-a=MH|_cs8LoIxXw ֺkylWkO񯨎vb^.w5^WTܪj ~fmWJ$ܸB-RHɇX\ +)u3i"KY) VRz8)40w@ZA2+*'W EV5w^im-AcШAE" gv&_L3wJ@Pp->i{2U}k+a;֟(;hU0{W-z~#™m^2ߢnV  [i-ޕt,a 5ׅ$6>Ōzk-#mWDٞEi~`+U[A*H kbEv d-,Wz5&9Tz~Z~wrq`g!G-~Emy?YA'{[cì@~1kva(;˚єâJ:go3k10ew>tb\c_WT#MWGpk8=#˵ədk;w) +V>Zt&8o}5E g.)q M\yT@MBDJx"AHxP- hZ"CsCElwz⎿L&max D fWwaQU ^^wΙEvE58AhǕ`T{q2 ޕPoH! J 0тK&Jd wW#4Y STHMAwhvmM`+-hyjb=%*԰ NṪ՝=&T] ]Bdf߮jY#R+@*ݓM\@cuyG(OyWAPI*ZG|WPFGA%µ+<@.I){Y V-$]b<qD7jlu,Gֺŷ.>)37[:5]Y8+<$EPPeE7GR7]6ܥ rlEMCcOO^qޠ|ϫѯn+`(@'[SWUCTqǨvq"D6@oז"Z迢 B`k+J9Q5XqP/wH&ZN~y̠Ny=01K3]Z7F\|-x ᣸fz:g:_-Tș8 6@́ dvRkE3( r <@Q3ݹ {mjEO*ۢ LK){=ƳoAϡ=AEU@[k"=1nL 3*o/lZv+H}*TARq&EYu@{/z. @Ũ=4DYRk悕6QW^ +e;VoэXw9 hOK] VH'\Q+O袁w\)TMJJS;bocY2;)3l_d"Ct4~ k潗ae)YCU3GQȵ{{E#3H'V?Z=% PGcwv_I_豸p@./Xڝ=S^*A<"utt +7#0$n]zf} 3uq {JVPcSC=(mdE jDx&Ռb7p+'g-A,i*Ȩ3y\Eh)%fb9dʝ׃YfȎ_7[àbv(fex &\QR.-Pk0WI&Gͧ%slz l'Kwݼ6Je`%3hcou?aQoq9~G{aaуA򧢹\ 솠 Gv "/bnXQ֞[@ٗIq=y\QCkFrշҗwϿ t2%n!cjg c N(҂lȵ,FVXWF]P/#Hq܌\ l;pv@}5S)_ȐwAq܁=ݯ~g=˻uc* Y,J 10T(#R25 xd8,E:ndb?}j-3]a +hO ̇!E߫2 蕕 QY+[XS/dmi/?sS<$Q*Oاj{q9zNA70p J@ijxQg!7E%p}C3PR[ֲ~Po6Q"jZgm4KBQPU.m Xf`P1ATZ+5Tq)uw]yZ˼h,;'vm/-A 9vWҰvGlkh qIzp(=SLݑ åȠ?tT<=R5tn^Fum9b: JD +ovXG.k!lG +GmPo;ՊbWCF|n#m9VΓu. "SVy `_qozvFF0wG +u{[⾍M M[# +N@z?6YA%_M)눭6IpDi*%_bΛ7ą _9? 8G;yL;}³:x)CSW^_MBNozI?0"cB zo.`^[C fj˅2 |=DtHCTDe`1_=rdl(M^r~P̪0:TƞN'\B7u69+zCDH!yp};bɪc2Ԓ:˃"'yfD0kٮ]XН꼵֒igOBZ:$]k+'=KlkKjŦaK)_:RN e\bkLuk?[o:ǩ=kƊmtn2@ߩS(IVlMI¬|ns ,Ύ+4 '۶k#} E}P娎]^H"iIvw13umK @w.FEC'E`vzQ["b]1i9(zXYӱ0r1]!..逮z~&a0MWpT$#fg}to?|[Si"Zz& zϑc 8:*f: w 8ȅWcA0NL~*g0膇``ѳ,@Ϧ{(DS|$ _tr<7c ڎITҥ'td>\c81ѤZyqx؇yBkͭ^Uj݉HP_}>ؐߓr]#p}Y[%,N1Ifծ⊺ґ7N +k5 ET$.p,-( KeJndqP,F^gfAbMs6 6FP LH$5nC2>/ts=*aK]Ƅ(A@VĮ-4X3VxR$Rt) \6,Wo;,ǦHHB.̭^@`W>@iƸQ0Pr`5|\+H@!MܲR;~*CA$%2wld3l(H!?aznN4w#97GN*n<9ѾnKLɛY0uXjmtPQoBTf =cc|NYq֬:F8Mޚ +Ȅ 6@{:jP|Th۸yM*mg"̌HBNOE-`l9tƉ7EGD#: afgsp)C؝52qU&dql+ <0 嵋%z$mt=j>DW3 +GQQ(&s溸PAxƃ0 l: z}*UtAgDSg] A=Ӑ#,@TpX@b<$u >i7"~ +g|掬W:X+3A>s_0 M _jq!b'r˅ A\ "&hyPEa%[*'CkCo֠zDWGE{ s:&*3鷣ȣйEuSt`"-:_31‹$:xZL#n5nPl[Cpdk'KE8Kb,<$xH d5%[uR8Cop<èGÌ{Zamu6V1~3B2y&WuJv +؏Ȱ|9Szʷwnh!@ii >Q3Ʀ2{"e|Rn-nl_(PfQIPG?>S>9ߝɖY[@, h'~N60JSGwZSpU)aO^vtځ_ JyELH*(Y$X9j~ +"D 4 nzz ifM/p5B9"\^wF-}9SbErI7|DYbXqaRKU<[m"cme`˙"Hs;|V roXA6](Za:cpE-jXݶ*M,o-xDK/_ )' "`&7,"l0JlWߨjYй"3;h(}%tڐ=Hkrb(R SC(&W:g&Q?~b )hL4ouw7;]&G+XDl"2;ݟ(!mѠc5sGA;P:& [[f{E IDhsAz eq7l3egmeX` c#f\[ώE)<+(+b;B!@Jm UVLV4aU|ߵ<f\ɠ^Z7"ULm9ܕ粃Sǜ[5{B8D@mZ6瞩YP&nYcK2d!ɾdsdlWqeڋN xcTwmQ +h2&r<Ї +_ ly:R'o5t6A x6>U)^N<-Esu5PQFiװx= Z`puK@} x8q'ӺE~p^ }=M@)ur[aZ(<?{{p:q@] F$qi;?A[c9fF!O6> 5fIdM{ Yևd@9#BTt䃻n,$c6ZLJ[`E,2~cZ+;g#O|ӊ}X1)Eu*?p?|@X?x!H~hb\q ǹOXw]wS:K!$]]#M#j2Rr({~^o@{ak.u":Q{LpEvr[J i38ĕة8\u3sWh#C9Ek\JY2fVe˛"WȄ!cMRiO/nbtg۴?ʗNɅYIU~QMQ ߯;Y\y5rY۝8wd/"ݙHS,ʰ)uZu+(l9Z֐D3Pe긃?Z +&_}8aɨA: +d+G -klA8GjA D^:0ֹDōXbpkӠ麁RTP`noR&;ZgIZ7$4[x^%w;8Ҿ^6rh{=hgg*l:~#jg}73jǯ6̂:Sk{t!h +6aB.2}l Q0~@uSl:>I69z/ e@Mml +׉&I@Hz~(&˴bV+I!)q|fx +=~Ѣ< X|L)'P}3-.?Ptyz8:-aխ^pESk~Z +)dP $H!nB 3D*;B*1CРe vMUIyiCuf4 m̅J\vQ48fx^7¥@M(lz֍iHωaIg3TM;O=]be[q37E͍y\a5w̢qlNU}Ff\D[[ '8^~mߦ랚3xv\(!M\zu֥oՌB"ר2ymz}{S\Ng4 ++ +k}Ȃ,a>^uq_ mtn=d猓ḺmU~,i + +;RZX~ +–{QRbu +-J+nnX˓>ĊzP"Ƭ-QgM$4)9 +z̸Kt&SGzEmn-cCTH9[iLOtAYP2=z 4ƞWw_A’TcuwӲo= /d:@姻sBy)r|T-Bd3O @z`-cAU-[v>آu]D`K@OoNh?_V5%`p]}|o{_sɎQ;ע5SCc}]^fKl`[}*ܪӈxnÌRw/&[pW~T.sU̯'OK zdqy͢yxŽ|E{dӊ$Ms;PĸN)ʠ;emS UBհR؜表=m9a)4OKB/smf[4qCsr5=պy@i^??aVbĻrIq>8H牌(w׀26"5hqO/襳( $ +CKby๋#Qo('_G@Sq/&ڂ&?xύrR|cv#+asǔ +;*#oi{G!O/\^R1/鱀,A\Du7A@m`|1я +DE{ۖ^hl +^[5.'dhwe쌺oߔ^0*z+c烥cX4MoCa]|;nQ4Ĕ8oC5't3-$V?]9ܹ))Au;h5K~\:5 H& +촒B?F!Q*tx6m)A#AۦB,7c f9v#?q!TA5[4s6k\.PXl6+A8}O}ی} f/ԗWo"ʈș7UssV;S$^ h4E8t(F臵낹Sni.>1rL?<Ôfnor \!(ap$!3l$gi5MȝTI~;>&L8R!bgh 9QxȺ@5ᣟR' +#~;뙡5d[Qρ=( 0Mß!+MU]+z&Gj]zS3E ?4b?d35é_xQAoP PYZkVx}o8$ȟnjX02t&jKIwݲ xZ۷l#֜`@K6d{DQddBQJ9]:뾿qZn!eT7b[DH}EEK VwoYxm[j \v@gXS&tON[*v%T+jN&Jx%Hz5WHNϼǵ1eATIYzEڂX@@sIaoIrv8pHj0O[_PK]񤨒4xkvU_6A#_cxUi4?Y)= ضzU@݄/w&QH~؞F!]W]6Pzu3|[:ndM-oEbe>&@t=zT\YyOFr9-pZB#.D!׷5G+|7y[ 톰U5N-oaE|p>\Z9 +Y4tDk{8^Yy9Mp&C>9aW䉋?E(a.M/\2H7.~yD :,Ggk`NpeDIv;8 5. GKnAԷCq +{ iAx θB#͸ޏwR&t75E"SG-b2"*"~o̷`vr^Haſ}Y'Xƈ30N܍yn~f7ةY^oৱ*-u`X`UECqjEBieQzd>#9a=D@ Fʷh.zG QT 7ti[OfRP76`l~+Q^o"d>_fwޣI-H8=l¡>^ic\i+1}.vO;m$ZpBxK`5A[y~R.j-Fd2tfg`fD{σ멌'6n[0Z^RVWd6l<`ɞGk +qW ֑Bw? NWhgr_eHghD8P=_u/sg$&op?wCP)<7tfgQYŌNժE <02CS}3öò#(ooE2`N1X7rx❰ymaye|iV``hzcЭ r-)k}@JߜmruIߙqмtFA!ƚo ki2y&^sZ 52<a@DQaz GO7? *B` +@磔azҥ vr#ыaok]sWPp  o5~+kUK}`%G7$!o*ylصҬe uۅCUz-=llua."D,x,bI:ѱV5F4.{ kG}Y!qey_'ZK!'Bn:L`_ |^L00Zᓘ <Ok~#F==zTx6(w6[uڛz]CN%S+*-@}11d <_jZ?bfSAR;\a埣YP}X3wf\UC@sHk^A+kۧof 69-2hs͘E`$UsP3+:ӏ*?4bzϟ{7VY0l>!>"jQp+ܝHv[R*ILGʊ>xZQN踠Vh>z̄ \ R_'Fd Y?'3F;[I=\x5}?SyOAz|HC |՝6pQj{~sxeX{*uRBd`UvC qn}<~<:iT fg(-Up6pp&aR^"zC(:2C.VHZN"Mu:Dwk6鲽n nڨ];Q,ߐ|ε@fa' S#v?vs13&-=0UI_jbg_\3&Lp*og3 5v: %PW;/ˣ3Ø3֧XW]+uLbdsK`]TCmۜ_}~ޏMCHaBc6{ր@{V<I}qblkV_.E -/]~O7E]5<14RR Q0n7$֋;yYM(&㧳GNƃue kP=9Ch=:kW_GB $Xlas ћ9ݸ9BN≅޷ +h]ty;WPBE^7Z1b!cwD]8+-S;@fztDNCYMQらR &VDum$/TJlYyZ'v +^Ҽ)ٳR#5+wf꫇otLU>;l}B6# yp q8v-8QS iܬf(=.0K٤rn21f_3rug殳ſR 8nFYꏮg%XS0Ϝ`a=ʾ~`c2׉JxHIeK8X'-Չ yKl"W׎d +" O"=dݴ&E +~O 3ͺCPjڣ%1S;CR;:qmF)!Fv PV} +r3>A, +h@60A/TP>!<EC.#Vl0`Yuq]n[ .R'z>_'63qZN4Gꈞg{mHҟ&լ!cҳ  i*GZPWљI)x;-P[Y4֬fq +%{x5jsDK2A>ۚXj,8ܸHG[ܲkǸWwe gAvVy +:nj\iY_2Ë yI̬I=`)t1=ʱǙl@rrm˒m`K} E3EN鷘߻sHxӓ hnٶ yy7:n_FP)]aO57Jrm ]Hafd=LP0VM/Hc W5Cy u|hbL/t9Ik!HQ6LJmׇ=EwL>SImGA]ŵ ku4MdaM7W4\楰| :8yL7l-pטtl-9Rs  xLj~Q{D{gыqta3VaT=7{c-Kg[̶0G|W~{H +w_=91nL%bC8طJ7ցJ))o3]ѫqIAr Oc^eqЊAzrϺAD2 ۧ7zØt5jHN" \VS1#1Y\|Iq3*Sb0Hy;欩j/}F+9~ÏavT- +=H:f7~4WJWG(?tE~`Œ3mCH<B,XU ++>D]]C^m2Qȡ[vQŌvc@ꠠPv=p.f!F?y"կ's(q6g-2vuC Zl=uxd^޺DjE{ +N5F]r&U`NwҎ`]XRpCZKNfrgzg8h罼4" 3z|qb641d y׼vBڳ| F̷a›Ȥ#;ϗ\v6z}E'&uov`XJQ-#F<ͪ#Qh+ap#Z=iԐe8ɴ'<(eķA +8CQȅ'_L[}g(#0~_. rhOJ"q>u䜍`Eoqw:(q;MLʙS^CMǮwCRTt9`3AMkwA8ڈgͧQnm#MG(ؽr5Ү +֯Ht~tUc'8H+ڳPywQ&@ixWo#nB=1<{IQ'}Ef s$vBJ"W0oĊ:?˳ '>#oEf!qѴЏ:(2Ƽ"B)u,E%yH)ƕAX`Gf:G*Dh(\Is Է(`ã*ғ;YXӡQZ|Du +$o+9Y%ohFD'v}Cb3ף LqpE5(S7Nm10 EW̿MZiNbeȂٹV'PV$׾~w+Wu|ۧSwYy&,iW3'@JÀd?-K,يc&!wi}ʼ Lh/4 -IˁQ7)svi(=oX+Ҡitol$% V`fǸbph E#K8| %uXf@x0 ;(NE_%#ٹTPAg2B3?gIߘheqի(u"0 LҌߑ0I[yf:ɒ(5RCT<Aܝ8󹖂q{EL4f7$Đ?R ;1M<$헪z{#;vvfHeɌ?ښY e psq^'@L֝NgZ 0Yg,~P@O󛾿Lqja`ylOH[vvM%`}D=uBǦM,߯R>sFsah\ yT*g(-.xFﳤ4aWEAk5cnTUIj}@J{emp }ϮK1.lM[/!дv5N3~Ղ⹢O>3*0>>.R^{c(f HϙmZmӷK'4ډÒ .p JK*Q,DHëapW-+[w'%kogܺf+K,=2gMt _$Z?Y3XPY|T'b`&ى\ANsz"OTGڝ9>`ʯ B1+iTdÙ;A"sf`}E@~YcT0^1@i*DwP㠇Ҏ +|z,)7c|u)^l `:Pȩڇ?~uT.0W2ju@Hmjl_q4`HClH6xe4obruD@U|w=4 .j3C +=sL +'JCWH ϙ,1rbl +ۡ|o!>jJ{z.[>Y7}?0սΆ}u :gvl ꓴߎ`4o+.3=+ʀ|glX.M ΘQ>7u̓TQ"N.;V; i1(.WzLmˏ<C<1w+ +!Q5y;Cdޓ{N"C>c,,/.?BfQ7g`i, e tގ-ml,3SHww5ܧ)mt']1Ǟ׌]#յ6i•3 iϙ^T'_J*  Wd C%3(֟afQ5&C-(g%d J,W9Y' pkYnjXAлw" ~Do3rF:Q?T{i}&Rc{>y bNke;6a8˵Awj- +?25+DY n.#[၀)v;E{}?v(lg|I Q"w,6*?00j 9%Imh"PBHx/MGˌ̳jFCeތx62#w-;C1"kdB* +cH)጑׉wd{xXSjѭN̰: }ܫ ̠f̧ +RNO<:B5|\XKk]q(`XA9L|uEeImBCٲoӆ +;K!H2!$`Dddf9!apRid!Nƻ a\IaHqʋ},MIKri8QR>re[IҕB׌sܶ*bRtfF!R\˕8T5[eVxγnjmAI%B/BYg["ȟv?`3͡c Uif)MK8pGb]6~1/́ز  +dmi ;U V*Td$Z:srTyx]xpeT"Pk q%Ǥt}jƠVP/DքYT7}; $G/~ > p|4%Þ,Y4@N1M|.@Y>[tCqh5c =LO8%H ڶ NiCP$[ +f +q*0ֳ5:Dي5qAv` qCwIQvO+jJa Ѓ"a k N!f\k GbZ$Pmr˨4\iw|le"9WZ9VCha$ wICm}J@V |֮_O+ {k*ewEfQlfG^MOEh"O_:g»y 5b:{:>NN%0wnu\.W,wӎc}\E~uKU!N_Pڛ8Gؐk\x:fqܴ, ҊE56xfU tx&z#=xp1<(24n'8(DۃR:6N` ~]2U!G`.HXgnJ _⥺OxQ֦TZI[P^Ozkc$J"tNY1Mm)&͑H ޮHsҍ9{B_=l@Q#T+u15v\ͦ.=W~3d_<h7l|@TDw0XFTP{Q;ҁ:Dwu?!N 0kcJhN ykb>_PzhdN(ĚQ#wDv3hR 򀫴( SslRJ`~o\IJ#~hUyU7,ڀ)I !>q@ 3~̘qH/lo!753qrh{ o~A)Tac;3,ݭ@L:lu1k`@,vv3][vC_+x@}HUSK/ $R0D+3*H@˙Pz̍Ur"[1rn~d@PUwd'GO4X,cDjJk9 KxmH]S#TM]wH +T-)%:ݦiId? ڛ|K*>'VƖը + Z|4ٯ@%$`6NMYP1 ґY&Nӳ2YKs 7,Rd&Շ4 6x#}v^<^rCu{%~ڜ#sv{ h X+4:YH]6^pUW.,ps\ hT՜P];HĞa5'㰧 +n%s=~ e@4!d + r!mn +ӏ];&WjVn\$*:#xB)SuHVl&T֭y<]PRlۦwT!Y!-q{s^_(J%B@ 2Q^:p ǢoUCevhtf'H!-أq`h)$,ޞ2&]Ctew=d$Ɉ{H(ǀ9%ϔ uЬG#32kc+˖"^= @'M?8Ф4i1-#οSy :_6MXb.zG X}L_w~̇o.H]+1ž՝KK]X߯S-浵<.Vk!43,_QPg/'5(>SHaV xYkn,~RqcR<!fU(/0܄[H/#>ۚ3؈1U噰`h-LN`>UCt~)cqP]j7kzEona.%]=~R[:pe Ӧ`=qZy^pc$#*JRܷpNm\cWF^H$ҷ:Y<ڇQ?|qΌh(]QVS&/@g YڻК&ҶVC{@șC H/nPeCԶؿRaU?9M"eMXDMVܟq]m˗_.iQs +k͵ +ٽ7Ӵ Z<'vhK5qCn&ڠglMBFuMtBeke?<Ϳ؜'D/^HZz:Zpg'ԥS6WYi\wsVh+q{PTW@>~뱙~ +Lcsnr".d۟BcR!SLJmv E,uzLrbw +VQB hïb?ǂކe˃ZBm!u\犩[BA/HZ.RZˀj)m۴V0ym6Y:A3lgykּ+Y9UM2!ܻ٫2Hd[M:_fh FFy*%=Ōr֭mG&Oѧ#BK(mJRKj=CS/?"TQ@݅uB.Ho. +՚"NמGN 5|$-wȝΎ 90'\HZ{ +_iuXo(WR R~D ȕnƵ^r#(T"fwԣIG(vo(u˅JU٨GVT2X5X VMCW>ga`m'T&P*'% 4Aـp˻q"[Wyah|7=f/8àlV [ϛ(3?Z3beH8G 72*XD52xW +c5Xk^v{q@] +<}ԍ;4З .3g~f7٘EoYU€v#7 4TB{XmR;CZ.YXPpZi@մ|`^ieRD1VYLܳWѵc4uFvVT.#gntg=4Z_{\6C!)U(PrRrX?q}d+0~ۮJzwu@aXd! (HG!+2E@>A(Zhlǒ#b)־54w({5ž&>а"EUE::1c&AjpA[ +%y_N߯4r 3D$Wj4I"L4*9 vxǩds?ndg앨J +@ UQDXMXs +޼Qfc HʠlwDn(0h1$M/烗 +r en/3}>FH{$1Mˉp|> +fLǙPVZӇW;cp  ͑p +eɮFK^%D, %*|7Z|_ +Wx=V!& `Β2rH@5ȁBm\%>3]o-<疽'G('(4SmꌢdŴmd*z5x7 GşUS@g!n"υqN$A1'{c)A@(DXϲ `"gE-`|D\WHWd0U瑣2t/)0b*3oIR6Rh4Afih<]51"h2wa%e\Jfs#^a|h㘹#.F>E<!1A-= Ih+tt<}ꔀ;kȖBlSٍ| sZhi[4 +S: Ԯ|1"`Yơv};ϙBmG >݀bBAv5QWz>,p~dNi;T)=)$q:.hpr]Eiy:1ba=|X(*VqlT~3^% gqU E",͖'B_І]cA=71Ft? _V៎]I" )Pɗ&+N5b@{N)̬s&TL/%"R^ ES9_Pэ0I$07/eQuNoD7EԹggF + d^As!(O +` U>ʞԪ}^27  齨NЋpziY&@hǨT=nX B,KQ{,,xYO~ׯL +5;H##A;hxvDzt [ +Ơu)kK_)M1 [W ہ"}^]5[/))Ƌw7˶Ā9! #*9Em؀/\o†SZ=+|F^3aSf\ðJd13د7i0@tBGA[ukΦ:ynQ.;|[,s5tJ2,S'.獹f謲"HL-.gʋߌPYZ_+wt\T)wM|G;J.S*%#II +~5Hxp&U^lgNF'@PlodL/Ѷcr "_3%B{d%p_9;?C?*GT<1 ^@BR=Y%0*e2[b1FW\j$$.Ru!Zc_nȉ,:wI"R& 5nbא3|DJCguDPIV-%1pӼ\( N\De  {L@hc#Lh{%pa sgdO=6Z9 XDܩq`HGĴ|H"0z-ܓo|ib{Gxxe9)!z~35.}OD#YıxBtɨYL=™($9Fv( /M"=[taML9/R&Պyh%ZQ}Hbs/W,TX+curƄ%0IU`ǧOHh8ޥ/aqxt>ϕ'#80Uo^K#D(yX'v:DQQVHe8. WYP`eGD¼Iִ0s|O(s"tC8[Qx)W/D?DȽS4K[&ِV[!2ow=g'X.^DZCJR-Y;L3}gީHVgC#3R0P[}>EN)7x +vV@n _ß +Wq71 +TC~1X5  Zy@ l"'3e v};џ8(ňszh W&#T_cBPП\ OX(QH/_}r,Tˉh'e v37|k_ NອCOOgT#N3`{Y(Ӊ дϚ Ep#Y2KͅAau Ԯ54]GjFn.9D*C>`wjW2^BlfHEU=.aNb@2Џ3\2ar8S_-~K|\#fO:9SDZ-R}mSax1"`M;^jR7e=4ؚ%5`چ_Z51q/z=f]t<+ N/d|nNRU ՝nҔ#хGڊ~yÍ%gIJÜ||3 C..kA{տrt`CSG6=i:   zGiKK iQdm7B#%ƯRrLf Q|>H_E+j`AuϡC9yu3u'Q!^YWg$Qo=D.m19(Hs|hؿQmqASvmGrNdz9a̾T/׉ӭ@| -'ޥ@y[nIIyizeM״/+'d3%{44-7(g.s$^qỲ ;ۼ:H2aYE.#n9~Hix l@T^>,O(׮4j8Q*T1>`XBI!Oe(Yڠ%GYSp+k ?їl޿, ?*vEsiw{h W4րkgM.'ɥHHG,@(: 5B-'eՂ f粣daKvTԅT+M-Nܶ>v_*Җ\p$SuL%JW; hQny95u:ʦONwn#8u_!WDiD˭y$:ˆW_jwR~\| ևc?0'>Od;}Qp5ig0gUM1-rSe`GpH_oc8F#bF @oBvNphZ9iޅu}flwO:׬0ȾxmJVh" d<]y] +dȈޟC@nIu8XKE$|ѤX0)Xt{w X'Hf%9K|m*lDC+nBt70JP܃!ؠ%sݡlIV)H٠F;<_# *^DCOUqYN, L3{c= + (̀'\qwȿ_694)aP)([ w?@;2(<JQGg,22fyWO + +#\42uT4+ >"g)-q)twqǎ"CO@^?{Rxmʁc!FbxwJBDWTŸ\`GJrDCQߛF_"4yf^?9FҧïR'=X@rt = iuugDnDA~3 "ARm%ȽNy!+BX@H̐ Np5L&=ߋdM8W '`D\?BGCd*!e鳾NE,gha3}*;q"U%>j?X@ҬZ~CP2h {z+DGj)C',Z]~ nwJ B`G|> +}b(!}m/XJ`W(h5?HV1a~Omm:=i¾eWy,"fW +f"™Y^ƒYɳj,&眪vB d^L_S}dQFW!;첏(L+uQ[}0j"(¿~QYuk<&"f{H =8].7E\_Ͷ&}LjtS8B8v.iJ# A=Mי" 9[~7JXC$$ػ>Ρ}|OÔE -+ѩBge`*֭6;RʆJ[dz?i&"Jy h480'b2OLډ 9rУn/DBM6z&N)I.,ͪ<:^@Fo,?~0UZưRRʫ'~5GINu=|{̘N{{?15f g pbHͯ~H 0&jTQl=\]gߤqX6\({nI%} 9R*Qa/>ٔ͟:Х +g?4cpKUU8n׮v)ۆBlvD":/.9Q66ҀL5d5a1D}3hX ݫvE 9)Gy?1fQQ~ ("E +ou`#XHUH&@FNa,*+W$\gf]zJQvP⾃4 {?Jr. 7J`@SVP kr Q=";ٻgD+>T򡿈OBZ%y~5_5oQ /vj-h)n{J$SжT(Htф + xm1v3k1?&!u)Q&$G>P}(S 1HZJ0 NJ|Vvγ!l.Gvcs ܈llNJ|Ց8y.Qݙh=2_A ψGab>*V]@G7t:u\9W`@n"5Ys@;RDۃ89j.;7xh  #pI3?zz E@ƅ|v$ּzaK#wtS%zsvd><*8d}ɞnW5È@[ hu%EV UZQ4ң +9!.׉HהЙ?$u{e+6j|(nC*6b(xBvKX)\:#2P{>$C2ХQQUpGe*nĖq4#E>G\QL=h9H91 "~zn?WEmaW}(ɾ%LnKP6qi׉!{񧋻(L+^^gMdB=eUwgv5>6T`̂҇4?W xaD9,`:h ug,Nod_ &HSYq7 b8z;̎]뜉 Ie*!][ +D?,go U.%mεkl /P/K$n\&=3Nr1Hwq%j+&%*<'> {29څ^6U넬# +"ֱM>ܷ̒}z`i\kjfq!]:kM~@o3w5tQER  WP9+;FLLڮgŰDzSVA)5WJbs7פ \a )WR0RO],ѠjlK-ȧ#YkxCǝo.Lcݞ.W| \IF\:a4n]vw>Jd9;)O0S٢v@zʉ6qέO}_Cؖwv6Ei(QٜV7C p%* B$_!0(ŝjOt21#5]C) Տ''hp+*^zVW`kJMz'`B+/=RKpuc@ͼ'^S{@f#MLQ-2#...xmTYK#-aD9ƸpA+^tw.Y6 ->O)Y0ByM=TGd>m +\8bLH +sa߁'Aj%iފAWcK'e9H?ne/| H 2ފQwCaqQMɩj5KtL,'I:c3̳ia8]ψYm؝i?gE]#ًQ'qx/A@8Wihۙl+ fcM +dG Hr7(  ]qPdPX>@N%jdJ+%jgʉtT]\N +C. i ](wsUW>stream +ήXp?E}~{=8CQr髊N]qDkXaL[g'L#bwgX&.ٌaӈ M@1r_XNmW,NE2]_g_>s ̲Ȭ3S4M`YH^aޔj!?臷-D:F C퍎FU50Vhv\u#g5QЁҳ%Q~lRأߗĬoc`G 1yW|`iЌҝ1%D6PF[k&&7 n1_?ELк%wR|Or}%7"!CLlT*#Ұ\#V(va'O;53>l /3 +0!"(E[pn*-"Vk!U& X8S'TcAӲzZ=AF%Qrح!ڈ @b| ow##-Hfq%x zDfz;GphV!:HS-v"j<6b]M( +hs, >9P?yu] 6AD|>3 p@o# A}#G(_+(*g+ =q+;>ۃsN!=8@D@ؖ_PzIo.½po@\)yvA-S({v5mQ؀/K{GG86J}(+ND%B*&dDxPuG :;/ӂZV @Q7,<NrUw 샊:pPӵ22 )1E1 +˝zgn + #-^2TK{:Yyi'S\QP~pZȢw6|ˋe9a*'e.߼_+ 9>̑.T63vob4IèQMnsÖD%BE(ŀSΉLje(Gw5Yu\dAH%{Dx0NJ E/Li96^Q@w!6 hjظ-fHr;ss;B@gckmY.'JzwC /ۋi_>B\͸ `aBIsTAT~N^ 8=\sK2\ }{_H-zߔF/ k1k0qŮ#:zxte Vuz5w(f‘k?|!?9ly0gm9rA=5Q&dx"fQ~y(kM!"DgOwl"eoYWw١;c&3wX0,hp臅N>-죫6 {FTiN~.L*U;*Dh@aPxX띊u&XD7(YgcD (#2~`q&:6GyZTo/a?<:S93F +esHX \`1*SNUǎIbQA Dl8F.vr_r{ ls;WST-ʰׂZ=lՙ<]*>R#=#_eȠ\kH\%RE1JBi:8Lo%2\ h˸O|6ɼMr6 Ӑ(¿}\y% 8?e釬!~o U{p8k2G +"-{,Lő`=W\Yig?ȁz2̰Q"3{xswytoxvQKԲlahPa{=x;U j)AGB +P57 +_ѹ\J W˙v6dDwiSOBh :^pf*An"1M7*f==v^/%,:h7a[2{HXGygɸbXX=S2##b%eա`W BAca@=N4m!$^p0S R|vUI^SVVa%jÚgR@Y +rֈ-0 b?ݱ n{-'BhK;,(KcQ#_-8⛆cW{4 +k7zAhVs(p:>b0K-w~cKGKhYsFGbÿUS|$Vg4TJa&fRnH*j@L=n&F#0J/&`oF{#Y + á,cox}4wgD4P̀o~RbZc :i)Q|פ~OjEVmqO=h rMY]iitu&(vt9A;D:h]h+~b_~z"@';r&3p4If:Bt=7DNKF#jB ͦ[UK}7rb,{20 `hyGx +z3]v!G"@;+\žJ,Le%@" + M2EbCvGF:OS9+" ʧ`w$#Hw\ +4MR\`F}S8Ii"+~?p!)0$; 2yGyEtuy {~ᶀ UIW ݀=* w@Fó?ji&CAIݓzP +eȘF"{K:Z'j(Rs4;>EpEcy%v{Rl +`_Nk4꧆[DIs S˻FsR# +;@W'!D`2pk P}m^s~?"w!t$~U@ꁎ^Ꙧ5?ά +gP_}^Z2b\_"S"3_]ד+<-]jPΑ]sl&Yg|r_Jﴮy WLyHQz@yvCe?֪f|R3]O޾Km$63+ H;4lG`ٵ٩Cȴ(_溸ܨ辎 ڤǒLhS"y&Ȏ S:Ucn/G`\Uu?|Ā`.ׂ:~X!bߣ$ ?p48S mDM(1F͌=&n1u|{<>tavcvLjN^}of[KΖ&SF~,ZG* +_>V{ُ?^2;^G_U7gA@a']Ȃ3lW_e [!l.<ʂY/ +h:Af^a2aFc]r+ջs&jW9i2tzd pGD͍;vA1zl6ѐBZcw$ c-bφ-eD܈4f3i+)OMhXL=VWpYC)/I×Ԯݫ|b09W3 ;9%OIJ2=G=Q8ly:A@2IiH# +Xw-fp R;gJ3:'Bz2hG~Y^gNu2 L*IL^zWj$7mpڞO2QS#hЙ?)8ʽk'qKC +Wqs`2S׉ޫ"b+5>RZ%q}e*pcЋR} "PW/%kg'~]boNΎ dSihf~E:rft/ BR.>q>GU +iyXEKUءX DȿZ`> &:/&%Lp5:kfV)}0M@043=^CAa ~u &;!U84Q-6b"tMә$ߢqQȻG# ̊֨n53KA P-|Y{ ;/dKkQBͪxTE=l]T7}e)Y~ nfu2sz+,5;k*ҐzT{2͇ F={Wqpؿ*P!wVtDb$+#i Óc󞈶ةŭ 1 *st@G?*"Y`ro*0%\p* P5pp +V?e:J"tDoRqy/_'QjХ%kz8R~c$zM;@ v}({~4xMNLlG\(ݪ(K6,ɴGh7( M2S]p#qvUi g&Ũv;͇>`NYzsuA. +_z9ё@I(٢NEhĮoTvzU;#xQ5^쇾ݏ cܹ *dϾb:1KM7c[w+I!l7*qj XkDafa{CUo'9<O^V2a *"lyأFhVPdZX. "0=:z6˿慂vX){ OI",-3!JI9StO1uJx%xs8+C*_n,g+4;z7TXR91Yay }T,gɍS~Tz`pP +18 +ptIj:NvBðrsf'=\ `aJGII@Y%i"Rѓiՠ"2e#Cv{ Qh.5mK}"p-E}z[HE/=m}XK, + r>);L8 ĄZ]-'jLWZ*' +/_0=o``%W*`ps+mVkZB|޻sx+xaΪ ^3a-;Er-'4h!u+ P#`4 xb>(PA NѳO;V'ooUhɟ\¯ /QkU@sVlMuRT2M7U˜Ǝ 4nhc?[z-u3䱻-J?zD:: + +=ij +GJuMK^yD/h@Ǖ"m2ՙ6Gj,}er2fA[K#Y-m*as;IYF0lf 0G3Fp(Uz#ea`vۀW#۬AIA,: j%A^5eXS>}<$T?ۜ_+IfDcI#ҏy3*yةHSujL768+Р=m<\x80p ?ū#_4 .OP3"#̫SK/itM +,7S?ZĀDRo6טʈsbK -%?Q;Ip4eؒ RkgC"ڈW3jL )oQ XЀ&=jxO~—V:>T}sdT5x`XZ򞒛I)ϳ4Ĭh04aUpK3x/fXu~EZx|Ӎ7©Nٴ)tbYYF$R0VXsV0/t'KcȒڠ, 1vU( T~K{(U'yo +Mc0U Rs'd/^י 2jУ a!o nW*JBMLydcld1^Q!'KR@@rᓱiwf'3&9ӄ= tܹ;-Z:hm?2׌GwRճS0- ?W⾓vV^_ڡ@J:4m,r?3(CUXǸ^\^ + "=$~f)YakO^ŐQHzVzS*#u )?V{E>S+Ai5 +~J?%2؇ ]e_Q|oy4`jl7[l?DIt)}^O[3 HݷpK0Х@;+t=aڙWA,^Z}½..hqԦG·Fb6,Hr+ jԌ/_a"Q:Q6_;ta] hr |4Mu^J/O#t苢t#ǫpq2E72qp)U~RS +(#4~ 83)ti + Q-2s|Ѳ2"nnBU3=(NS|2~ YwO2~ ײOJn,3GX+TefE b Iy[XATqMn +HV8Bfl1na_{uq?ٕ.K:?%JRwA/!w;s4! Ɓ#~ [|FعCG)`BR썷+C Ʒî'j}h6Bu/ʐ-t6`kI5>?X +ܾiui:Qu&W?W(<؉QODa%Țl^o 8__1@AkUqF8Gug]$z|$RQUФԫ]OGjŋU9!a(RǢJ,vqi?9; 4a+VuE!n="Ot9%ٽFR=r!*@{; mk/ձcjqa^idt.G=gDŚnu:jD_xϜ)?# +FjQz4 ʹN1SLͲ +@J[6dP􈫅҅5 +1+SU2zQ `м]-y$T p"w ŕa⧪7 \ޜO!~Z{)܏H,Dw~x~tЦZO@MbSTe~ ~F홃ns$)?D)"w[vRտ$Ⱦ>$e;?">j4#i.5!7f*b=ᎪA߬F&':#RmPzEor@؎Ċ*|گN顯fk/V9=:`wJPF\6r?Bs๻ +"U-ڿt3?]??׋ ?Ejjegy>Љu~a? |%)O,!i5Uf~(]aA] K3zFz LJ5fĔ|w('H)J&ŜT8Z p|]Z z.i~:{z z#)󎻀`1SΒ,U}UA":}AZ龃)YiqfeY GnU;LXۃ ƙMW(!-;lzLeQ1aZҮ @6)_F@U85Fͺ'vw*T,tP ࡰz*}"X x|&z$3Kq̞7_̎e +B}^5bnWP{;pq=R0ǼL_\)RN#HL A-X&wK iuN< +Mף|0񎸝rn+M8BD@o/:h "Mߡ)v~u๢mfsc,LX0bF%h&M$/.Iji0#[NpES|IXahlR `o-+jϐR#[pʾ:=/J=ZFwZ0٣FK˕f?"iNßFi2c"~| \̴PtilBsL>'ij׾;aAbFz< Z]HICڿ6&!%EPl|p&%5֋2e@QdН6t.&ODMĴJ^2qZ@MDyJD+q&}AjF5&-ȅ$ +u8FZpoXþ㼬|F3^e3K21O?-}X{OX?EvɼJaQp(ZEJ: ~K(Cqgi(I?['Mޙꀏޏq,i`vC {4+WXjEKĈSTIͪWk4JQr[֚@ݺ/b(|-dNE,1 3"U~H28ΫQJv +q$i%& {Uy^^,v``{ DCM5q?vݮ_'бӝ9.C.7*t2 l'_%sr.kFnʍ=C~% }oOtNZ_u!w}Kl)Ų2UAWF =Jd{G}UrEO 7w@X1ߙ쨡(UvzCs,6y:]/$g4[fSg;j6@C]I]cyL*%k]IE%#<.YLΊ'"&nIVӈ oY/a1|ks7(f4b-#&J1(5x6<[xMġ8宭D۷cCT,R(t&aKg@i%Ԃ&\B[sa@ce` =^@Vvs+'{Qb]1y^i֨+):b:1B0mޯjU}\=pVBN7MhkGC&Ɓ/Z֎RoQu܎WV`a#PZg׹# LSݥ>ֺ9Aߨ?W +q.֎yU|2U%gUcm۳%uk߰uzH +V(>o ";8,X; ,m+9-˱Y=3uKoP)L.vK2&Ui̸r֍pdh5h!\ysyH34UMCl+TM5(aGxoG((1#s\hυXw}NS}/ V`@ivUbE=0F]BA~Ԫ d]#lzkWDYA/86?"Ɗ]6u\AwQ+%(s`wɀgSb3uG}1T5 ΋DohN1ehz/5Zwo 3"kAG0%J%y R|E}s$N!=Q;s +uc=ke ӗnep*_&[׽}a^3'uŃ(:rx 0$SbLDŽ]Lш{:rfdDiKx8Q@!0B -_2_OIG2]ƆU v؈r|uP#i3fQQf|^bwb߷~ U'(=~(1(v20(G$W5!x@0Buc OZ8rA,ȆN& Z2B~p1*QgmvG+8dqh C gb{Z2 V r[2wwA@CFJ FXծ8,Xa@:Mmqe3TךXW9زI)iuQ"+DouDWB4cRV /$FNhFzPq>!HJ3k="R>?bNG~>Dz(AamJAB![w8d>뀲܈q '֒ؠi:٩J R7tU>B}d0j+(*DQ}?wi{g0(m_B +G{RCfMu"N-t띻g3cOɹICzD 玸v2=jJ7TT'T,;5'a <&oo*%l__n%-g uf.х{SnD 6j(ٯ\däLE@] ͕ vї[vlTGZZɤE}SѮk]HqF<T?NZ [ -Plp5GyV-('anuUfWcsڸˣn#|;eC .[us'4dJ$PU htEU_Kb$IRd.tyֿ7_鋕W߿OOwO:op3ֺɾo8X -xjC7&3tAθG FfcVmgsl+UwW=Jl EwQ}?:W]gŌ'k6ApN`N֒m2:XSeCQQ,u*+}=#P|Ũb'~E2H6JATnT~v|F+JC.bLE F̆;m}/XOٓA _;'A3>(HP$ewQn[z`-y=A#=/DCD间U $QyDq8l5)fcow3=:RyI#a^Txe#=Ljs2sȬPq@i-"/x"8G~*Ojl~vwڌj5wZUz:P m ?lyxsΔh}1fsSZ)" ӭkNչЈ6$=Xey0ib6}C"pGRweœNbTuhqW6X^4핾Uo=K^JfŹɴsõ +r[?{|ap!wFCbcp]Ai[0?,D]lzĦd:jDtN6طBWuƖ!&"vEBkhhާvw畺~F+&4:Z-z\o)k4z<%a#Dl Z1L톽,sPmAf.3n#j7"z!lx~"Oy z_H_Fx61_6,*ˑ/L#Z vui"K +=dzIʶrg4l>"OwS;50 Zݻ >Ӳ!΄~ ']쨫Y= Ao` Cq y i$ mMzs#SZ:J<%!Z,k{w݌tn]j۾WslQAZ|sശL{ז;V{5.DCj婝jqZs : &qДvJ<{u@+W;q`iMO]hAGřvy!Sd ˮ_G=ru}EC5F\,ujcf׊9b 8 Re/Oߌ͊-2KiIyϯ22zQhcQv0*HwdRJ@gieX͕Ls˓ugC +@Y v״nvI~W,Wʀ #t8E(o{H1RVZZ*Z6NrVp鼿~{i~b `!0Th=I(uhexbYLpdmjdյߗGG=;KvhjcH:]Dh9 gp$ 4cXת B\cω]QdO#LXRpO+žz.Xgma%AP߃>_E.3&ߩ]Y6r5+E"5O5=ͻ+(*iuW| _ +a \~T՞G^J$ΪOW)Kk Nt;'R$U7>R *1^c**yR= zn\`,2u]n`~aYm"TqeDc:0[`D5!3‰ay_w5PTNuEtMܓMp gEcWxn[qWް/DŲ{i{ &"; foQE1pz5q^;s||ׯFѻw;$"ҩGSߥ +o9[{F$îlp֞<NoF dx {:(wtgok;I#Nlzݔx5/~sPڭFR70$ĝƅ>66y4QL5M1S=o(u6x3?m⻢cжBڛE[o!` y2#9{GaK ̕-rﯔJTcݮZy|'a=i-+CYRV)ydH 0|W)~]CsAoXã՗8p4h@ʲ2~P&QX +_ZysV%ڸ8)E̓7o[įo?NM)ߤul;dGyz6ǁ<7"PitZwfvfzo +Dxm=\QUDT)jD~ȧ&E]ܴsP *SꋙɧMx;XsbF3o|:=TɿQ%1e Mhu 3刞͇ġx JbAvo"ޓ'}QokiT{!-ӃxtN3 痀˱5ez5( zj5&UId{5kD 5iۼ*  9ep +5%&V? ;޵V_~4 +GPR#Ub ׺f S7Ƚ!: BUq|}L]:z 0ΩiB˛ +>Z ,Ut`kvhĴ*Y5]8g̩`p9aJ~9rzPTtG~:!8Rܪǣdb8:r=A?G{liۍ]=c#)'w" +?U|ZBl*aBr1i9U[';fܠYL~مо7?upP0{ f%p +!hkcu#v'4JM=zҷVˣDzP#9a0QR\Wɜ 81Ӛv˶`ĶoL>ZxC d!XxsЯ2~ܔKT>d\?<^%8bXF:';bfViBxEi0΄p }:ʡOQ?Slx}zwC6)kDTΣyX_7RGreZ'}*}A0 ʝ q +.tmE4VJR(v c NIvo=x"H. J~7e)֎@0@ݪuͧ>O"@߈k +u0ho UP5C&VrDy!;=֘u۶?@nbjJ?hQ=dQ-M8D%`?6uGQIco  ~ldI+{;v}e *P>rLN +-5h;o#\5b} \FL[2Bx>o sX:/5tZ)qM±z){J 8udROxoшֳs ن?;P.$hKRUlցT:͐4ke;=Gl|j}< 6>cY.b }ߩĦ(j)ш`敻ϣ]" U xZӇ+__)kZz88peSPi`;"IU!35>VG-;>v $nq{n5~4cYc'&$4F01ͽuS3E4ځE"e bNUh r腢%S5F$gT0cej0JG:*Ы tJ+*fWR_ raؔb({A@$ϓ$_ʀHp΁X'g\zpȄU.TͮxD6fMH2~3."xY_KQS\aWJ^Ur%ɶ?-;J 4T$?11_W3T.dF!0PKv9WQ9@9ta3?=)޷Z嚎Ud"sZH Yc;R3t?*z܌b{VՁm6:14V3lF#;lKNԻD"~t\)`l1Ӈ<9wnUZY);h?[4ȩo$TۜNH\hf5l [4|~%lٳvlZTҙ`5on,maiJ"Oî3KۏI\IyKsgP|r~⮴p@<v +ˏo !^b +&L%Gn;n_ފh4s!8qۭ@+h .Ї.fβ&{*P_/goULwWas*;,$+J2lTO@DZ;{j~pFDHOU72H:ЉqL'M&%n7ʝdVWymi<#x[弴b cYU+][A-(b5´&JPj_+OfO' PtFG[見Isg=ץ+d aR.~yZ #(n|AP1u*%I21?S5b v+5N*~Jw)k^{W ?H^Y +4m<-?asO4HF Q< hso,NC8 D@H ̱vrw&XSbfSN$~Ku?ZX2kݤVu+97yA-sº]×\ e|*6q޹g2?<PXbdwgo~?V v'_;Z?l1Tʒ{өl${E.L>>v0Q՛b>Տi' cW8P +F3qCScCȅ$">gUdz <];kH 2K BZ[k6P3,M5KTr'ߞ6c`O_" a{`Q}NԳ[ +j}uV2#U^~ֵ:[94VÜŅe j3Zֿ{r_f0(s*;44k*Uضͥv j`a`-(}ݝT64kBRߐ^? -&~ +!?݌QX7~ԨLtV$rzu>F.:[t wdKƚ"JEޯyb)IǰvN-?]1U{CT2oҼEg53&rk:GzơOwHi*o,->s˖ 󲱄],ZRAh˹Թfy",_N G'a,9$ky؈|ܹ/F3qJğ>U? TQzJ񩁀%_73ZV1T";F֒1nm`uۜОj@BiŇ-WakD967f +y<`rC KN#R7LI(;>-KY_-),O J ^X_;۶>sS92 Qi? P} <{D|Kë a1+րfVQo}b>[5AdThBg7 pN3p$\S>0PWD.޴Rt

V Pl _e%PT[fY8v0MhAMBN7aټڬͰ-ќS`989[.{U?i[%u|&.XIfX&Y"4_kײp׾R|u (\O+m4\@ZA0:W{ZeBSWx-]Kʵ%Tl5FZ &3lT篡},{mNE"Lx^o(@E +5k=y+ c J W>odFTcȄb +35; aG# Sp [MqCGqY1[6<@;p*xmYdѬ_z=;~a#-OTTG}s_Wb3k$ mT^٠=Oc 3QyҗAk 魙Z,*PtGSzPʫ3|/ z}>hJθC޼̎{2X%uEV%+Wz 8e٪dzEigAkwdKxuq!-eMA ,VXFŝ+T2G Z)a5< 6"ā<S_=ԆROBi'MtAb(ykGa5%k.߰g5ZTrJdҍ{u4٢} k;>*4:V+E΁A|ԅ 4i * F5oRG XE@mVɡ5s6V@}"[d(,ߏWy~lO~G{Ԥ/ؐGBϽAm|1xZm'SPLǀYu9` ":hWEױdxY91vh"86?4^ Zz ҁ,dO }'MUD/3\YWCR.܋ +!6As7Zbj/]bHuo*uK&*q~w;w:&&wS('^f)/hk) |i9hԷzd#tv׋v<"0@SxMEJ*3"]m&"dP=n]V1.j=|Uz]׃ʜ'o#l +fs3wTpcH]/rlԹXHZ[ ȓگyҶ;{+d7C{- W1姽Ea;}m7%bp\6LT뛑+VK*ojB+y?#ki}z@.9|໷.ɚlθrFLZxET`ZYʆ̻߯̓QN ĞRZrWꒁJ1*Sš}MfaiozUm"PsZ 6c=s鯤.tSJNոpK>$!BtюE5a^G>ީ8gӔ"*$kI?oAZaQhLk" !0/dXglwo;*[@(vDea3G~\9?/Juڠ3?rGiUb@RzzbywzVLlA*`S1})y׀J&++o5VDJድUj۶7KDH=Pb aٍB*Q|\ +lMgE5S&Z;k#I]ɼQ@,B\LliZOIEUCXV[][F~ힲO +Q؀ EPkZM@1@6~C2&qn.`{EHŊ$YD*;"zUs'%Z,ڰ\OTi5 ڝ< ;'5H3' cG[dk m뼦}K]6U*[T 5&]Ѥ7וH!?[v'&_L5^O^O5ۙcN=̂iƸ!j"tj< 5%3&~ᦦ}+/Ad?ZۼjA `|n-5pv }@tZUɫ0]?(R S\QXn ?KzXz"AD*]怺zފ=]˕El]1(UZ{Z&Nϝśl\je,؁v;6׿8$zDĩ6)O]I +nJ#ڊ|A,>}]9^l/8pԗٌl5e݆sd7G VΚ+ B$vYZ3Ϟ ҔlmPmՀ]Ǖ%I1]/П)F + v5y^!e\GέݟРƬ?NNZ?Rel.JBT  yVn_وZkoS?]|?fVz'mGtMt9ː&H^(AlQ0`_{'_$}x^tXBёb!|W8K͙cM>4#(O]&n# R +Yk:r + 5M8+ɇXHH$/ 2{Da.R=88+֐~^-H ߣ|J½*Cգ_ܳ+#qs`ٞ +XJN5j`UNG}W}8Ou" +xFG2jxGS2Xav== R_Wr)nduӯLhIY(01BDr 7O:ύX zD;YAy'q3Wuw+l|gv~o~tJJ+ӸOk +ڷE0ouZxbo{N:H\XN(۹i;Ktp\*>"zf1c uٝG$gdfiLMT Og9\/H?v @J&BտM#nj :3V3ҏ(8jX'~Խs\Jko^:Nkr0 +eLTkқ}w$kG?k&Gӯy6P;Ca7,N@Cy/emGj8f$V/8-g!9cggcE/ oWSmn\[>.]zYߑv7$~_gH'Ϗ$ $߽)9mId"7 ++۞L.Sf]6W:o(\+wfu5dz`%h~S|ʕAG#+#x4ngMB߾ؒGqeW:~cS[9mw/#Y?T3Ut&=p2~(^kptvREǧoaX3E7sc (E+$PNQT&%:A),cQd)ƶ-Qt=gEtk]LRg] +)* lnU'D[[^ɳ<{%i1U9rXǛ# +ʑ ;<&.by# Wq́Lf.ޠ +~8 TMs7{|CeA>jVDma@/uP ++BzHn i7?[Gg(kt΍HXѐ@}W&i Ir͍mp>?Ds 5f[me5FNvlv +:\j))QK>وSH1Lּ =2PmTx,r3rmz{F3L Ǐ8i\ ]k_MڙJ=e^$UT0`~ҵWdd-bAd;57w*}cE]wkb|.є,Ⱦ&P{;(OФpᕮgs2JTeD1<yn{lD$ ~4gr0^2EE`?"u mmvi:'?R;.&}%mk<ιU]V⯐BB3$׹eO_NW5TQ_CVKdt=/K>?w8STʁNM) f} d1<*}Z?8y<7T+b8#PARRg[y-P[6 G@]9*Ni6;S>#j b ~G<Ԗ ckEWnJ׀]sq ^)=Xט[Q7Wh_{_PCCk? 9W +.4u@BRʎǁg+5PEKҚr4%JU_oLKTY~2*ws> +?B@Y(#@ >vXKk=}seWĻYFtz34`Ȁ5gk\*q(Jz{*;I{{Oҷ9΋Ft -HmdNx8m޶gA&p6&d@ @[:XQA?ATw+Rª: OB7da#RrڲC/px`̶ $oz`(H@% %!W&9L3jh&kmacl +cg1Z(av}Wohg{L`w`2iٺ4 hqh$3C/R("W@˿qqLB7/$ H6Q~b69'P1LUjV]{j,>H\gN΂eòi .Ku}?HDv$˚<q} 0|D;}||M*qSc;G*+.]Z3!O\+u>Q@KP[Wڄ_Ly@8h"<.i]F dy&oML`GƔ鹀xu}"GМ@(S'*:LRklCh$lUIU4spPN6)Ukk.la޻#h{;#{o2QH"Ԋ-b>xرFCiatjɖUwt B "@)m>rζ^YTw񇡘Dɓe@v #nжF(jH@` ^@(ughi1R u\[&lYh3"_cF[C#2(Lď +F5%f'ܮ@"F7z|6sx_KBq_dd7HXYݤZxMZJG4k>M$a +?|u]c.Oj3$5ذ3`~٬[9QdCggZ"늒C053 uOm`1sZnVTmxnFDT c7Węj=+VV1Jev9dmu3k׶l7uc/gDLݎyfݿb[;bUEys*߱iYy-=8\&35Ϩ󅱰6l-4b-:zпyj̏H9AsCmپ +2[sn2BR2ϨUdX +w9O٪+U4F Nk>?ÎOw>#XM?V{Xpm?AWdȶT Bq2[ P)6QlPF6&@\I>/~N<2[JTh<(!-W#(Q҂quY8S Z?ǁ܃w{j[ùk])í Z"pi3"To}r[BDk 87:?4ESD=wDNzPxWJ_Rx.fRxքj (k;MJRV>k\@J@<@m2[ OrE-+v01K+. MJp?}cC#[nD"ןD%@p$_yKwk|/V_/????~?????%~us~25b֗럙/ *at}QٺX:/1,B(6|DFuF\ňF94[CrϖoDJ)3ů50*\,WViQv(9z}PSeCP7a"N9 [ƻXFʅ'/bvk0ó^y(#V@\ 2ڠ5zR?!g +L@ e#^+P'_g(䬗,\_GRHvvDd2RqZ!%8^g@؎@i9@2BKJɢ(а[?|okdjb +NĊ4jc5>86ڞE6.kP3hh߿݄?U3 QUsixk= fr' by}cblȢ3H +Dψ|+VEP쾑kn2C4Fty~]+MYs!T `ɲYA]d&#mÈ5CY1kACgc495ȤA_LD2397g@5F̕nsz?$4l*q=q@H +mu*<z?kE>Ah F揨+Z'_ʀMȘ"9qj +u}6tXVKA&4 IPU6E5l`Ql=,<8v@AdπMF1@*wwLL˚tM֕K<@4@CEx +s'd qs!b*m؎n@\퍠B#~?nG @>E[xCH 7( ! :o&J./m,C 9:׾^krAXL4D䵌GT=X,!] +#I ܶ|eBS>uR=ߑ*I~@$P>{R0Y6N^_bJ +r]m nkp 0Z"Ph. ]U:?^cBgTbr>pVt3v+DP&=%:`QS+C#Q6h1834yޙ;dp\c7PVnH_^ R7)9!p(}rI]ڵ*Uwnc¶~*/ΕKdFQCfRU[cwI5o| +ˠOg^cLvٌ_pq2Q$H5-0Us'j&)U )zק;S1RM-__iX\ T0CþI!(]uiyH*Y׷$MQD@%R f=C|Z?KG|z* 񪲹>!a2hW{lU4Ƞȁ˪Ěܫp `VM XqJPROdDk1%i&BOuw9uYn)ȵoA'MTc_EBhyY9aU [{I^(N<(hwڣ╴s5za4w8SQ \I٣6Z;rnҿ'k7%?+el1/؎ulu%.8-+Ǫ83g\w5%^C?݃iv_LfW\(l@7A̰&忔H6UakPC +JHP<*p[긶*d4MJ jRW AVUgHՉPh +4a# +]X#ʫ'Tcx蠌mBug?2=;LfoP;Zeֹɭ'IP=Ss ,I'كAzΆyrM6G%m;k%ى)(6y,v.zs%iO+>& QsUVlҳF{\Š8ޘγ/D-pon8vwX[dJ/ #췞D`u_.; aTB]fj~2?7X2G.NkbpYU'¯e.G+xl]; vq6̟,+vS]RcR'RhVt#cX?#.;BAI,|`XРTb>%4;RbeV|{*ZLQ(q7Gޒ[HFFU"jF[G({2 +iCD3 \lRRrI΅*-$H" *x6 +|C'2 +f%0U/7 ȅXvNCkK#hM6><{ϨHIJgMzr! +֭3G?gA+{}s jƛ6Q`p׮EU7I7c"T(.| I<瘟w0"*;hvÅ3!C⩄GA] ]3U1#z":TRW\3 +HӁGD4Da$P89(0]f2e#zTU]U{;>2QJ8UmS*}'W0Q&~]$FtϫZN^Sו4hMIIh'5Xq$⮉N$?a+#.;g6W8uSĨ8Ҧ" +rmj6%K*zF黋*> 90OGqlKwx*E[ +Գ6Q潂니DGp y{06Hnjڋ3Xc'}t; pד^ >0)Qt=ǽj$#UX#&'R{B+ L=ܯ+5[2뾟ټة~gM0M =}:D?WJܛOلˠ\PБM[) +$u5ΖSW}*W1pQ=eP-(hA D#[a:[RQ*J=d.sr%׹(*HB7ٯ\ jkwL3zDmUp :s|L(`ֶ8my47k&S}C7Ok"7+!q} Y|J3΃: o9ϨyYi=0[Ih$WߝG]9ױ`kک#M[WQWb{zvxůY}- +xT._!7A~EF-F-'Z3QGeCA\IO?)9BmɆpjn8݀\YН;/fu YUu ƿv5r3w(q+@uQP\aۊ$}g0VZup=GM8 ǾcJ&UhʹcY>#~qu6m>9+rW}˳Bd'EHSBmx޸ b n Ϗ3OYՔG SWK-]k@s3{" +]Xܺz"\w=DM߆:6갗M8CĚ(2 ڞ(Ր 3;RF#Q7褎Vӂ#2t78'"x%ʹ!+DLƵ|WGˇ=Idx0`]4ߛvfi|C]ū5(Z(5ؘC*n‘TUb2q;ôT;'&UZ`eܰ c`k*L]. rA6ߖg{xM-0`}q<3*;jjW?u\I_az7z'!D4kh&HDBG\ 'g"ؗI{7Sx$jxӧDv 3%SCӗܭ ՞a_|hQϊi=&au;G0X <{(hkńW#}>"3IӄXNۉs,E=I@ 4?F_' 4{sT<{R7ү!,nz3{"EN5.zFfX$3kWs\J& 1nkYrrWcԕtcez)%4%3gD Uޣ.xNsL7u,لuJQ4z76As:§l:0M~k{'Pz3@AQrx{0cxl(`Y bRc\וCq%$ե]p/$ ++B9",tX=s/ h7\RX a-$uOWv M{o}$u`[q1~zG9K` M~A+B{KMRT>?D#s -x^ؗAPra76 tP]Y8˃2rtE"s=ҍ`EN=ۤ(|HC$D5 }/W0QI$AfgC o37tЁA*rw(2}F|,^ߣؑӕZ +ZLp +o&i= /+5v!qۋTje~e^c -'gJXv#nqFe*D<oHL-5B> +S:UU鵿HѭϳlJv?X&F}0KQeO#lQ7@i-&OdtGh4r7  U^mH(^Nx0 {Ueӟ6nhC=(\M y8csպb%)Mf{^dv7Sӎ59ҬNŒaE uE ieћ|R/HX?3%$֣f494Rі3]p6~Er]Itq!D5PFIEDy~@B]SYFP`uui;Srlo{+9/MrVٜ Y]PfzB["P +k}os. _jl冚 aAͰkT]s@f-dI +rȪ瓣L}wE큞똲^Y`@}0ϱ#N Rvɸ pgcr^~.aYcWm^sw:)i&ٙU,KXeot +QՅ#_62-k@J6&hӘ[% OCQ!ug[bUkN; \jg_vAB@)7嶖 @҇lq9dDF :,0Aa1C +:uqjޥGYr%:c[|$5964,D08ǚ@BV?i xGY +@H< pv!q~cCr wr-PRVa;Z_+ EʌRzetdjwܞOǑaP@zЀʟ ͹OFM=~@ѭvFï# >d]&sujievǮU@m:YT!Q`g-@u_6+.IIb26Ɋ峻~uqŤ~paz\/N1χNrvnA ɲ\j<=CX +u +%_;_,c'y(H?@?h]>נ7")n$7'QB&l 2'WNw$ EU㡓4MXP=o*>#W?R#J6Lk9O7iU0ʝ'=I;W t/i] "r+v^} VZ#"6 S857!M8S76D֜0Kϵ쁗2T^q<fgVHV!{5vdxLUflg\dBj)r˼w Hbs_iL"J00>ͳ@e %kGj'?!E)$Lل"S}5ΩNպ ]#bYU@S֘kqZɂ4|lp*..Dlrv2r(6Vk B5?ADb;*upQn{pՔd++Wo&P["픞N ) T2JcΦ3|v{6Agf3TaՠLB+hqP\GUf;z;*G +q# W`Ǘ%Eyٌ&P;w7:H#V` +dW(gA6Ojkd(`/Ve}ex V*EB t>hK~{*BTGk #`N'>zf!>Ch7k 4A*tjS2 DQ#w4㏥MO\ J¼V`6S kaD}tF->6(xE+W"&KU}4< +bSav:qҫ.Tv rC+lox3R]Pxn*(mš_ JKRU}aku H=L~2-.1d@X +0Q#z_b2Wz5oQ`4Pq^n'Y{Vk<ﯬCJWWa Gl+`,(u]{)-c;ݣGwq,FDM 3\Vx_ +< +-j達rc'˶~` qe}[t4X{ ;ܶPKЧBq1 +<> =+Ip5c5`4QI,b82 rLqc-('TsSǺj% .:|2<Y; bTBBwTt$鯘1!o?Os3ٻKl?k? +Hwڤ`k!9MF)sOձpڵ7O=%g{/+pAb|AL;<(W"tGt|pN54a2ʮ(1` 칒^bg#)GĪQ,OMO>6/b¶`h6ڣA`JCj +a{`TjCX S}c.z^ZB@h59PgwdY8JgVag(c{Z[0QlWD5I/t"#¢͚0%B>Y1k*\j0RE;r[$%,)Xчq_N>\"e -;"6n'+޼U;B;5KCo# ez? +aS%ݻ)z/d[y244v]"[~C4|Tj}yGDւl5hBwCeG$.\oBo s}Ϯ'P0|ݯ4\Y,}i8%.`\@_δwqT@cU U|Ļx,i@~İnwA! ͣRtcrT9x*Za6Wx}#~0t8gzdW*@h1QI90xR!H31LOOD.D31"VҊ@|;#/Iӎ@,V +J¦oOb-pЙy`M("l ߃&bIc(7[m[gH@lWQ5(ujPh0v҂%ktܻn3U%y ^W<Ĩ8wnf|eu;$Q<(F@THnVEh{J0E3NK3r.#CִO!S]9A4u 'IrX^6#5k$M׾0Wh nQPk-[]6MU(Ě~UdM,s!pګ?lm"έ(pk n7WV nޛ9ipE2g_^; vi)DaX[6<:S̒q; Qzé.光iSFEОhhP_04OnFߠ/{+s"2}ѨJ؅45"$^-zɫK +^yv _gI!UV54C4?f$ంft 9bdj)g%]uOm_v.LU؏QlzD\0N>:n'lˬCDFdq?,`H08N:H#ʌzr"WZ H/CΚ*9 q΄3~\#Hp[#4|`2ӭL< 4D, 3"(Us1o W? f$h { as>x+45> 3K:Dh/\$DW0 gڅ+8_d^XA'); ZZGS87 ]0ɻv nh﫾tUfUFe#XC:>XRiH;ȡk2e˃nHwja8 ܧ%5܍ڐDPȭ}- +yC?-$Js&"h(05+b~,)FNpF"Qo7 +u`dLco$ bD|*odxXǁ>T<5TTpQdv\Dh#zfc |fH*Mds{cHST`߿'^sTi#pfQc AE {V%ׯ2x#"p, +_mJ!݋8C>@^?Wl9@E@_ +_[YiDnW2S_/}dWl`mJ'2>@nM9qN ''8'ĈMD.lQtɆ:-$\`8i)೦iZ/_*̄V3}Si6V,>Hks`)m"2{.,<)VVa˝$ꪷ16]OlCDMCGXy@p<"wl`&t-j~z)Z I#^V(|އxmQ%Y:*(<+v1aKu`@@xH|7-nbΙ|ɒ,0$%AT RnGEv&zx\+;Ԍ̢ۼasW"O?(E8wUh& Bu +Wu,) Tܣ }X*/+@6d?.2r螯Z٤W_(36X]r/-Cӫ9{4fD9z_-!@*H? +X.c0Ԩ`e2Q KN[HA c x킴{`@l1?]K@ؐ*d"*U O[G>jS*WDo*MV >-ˈ~[ %L@:#R?Sr]I^ 0^>#g#nN u8N9N1Pזe)0 x*w2B3> F`w:@͉5⹔l:+gn?p|l{[+pvv$wkjj>}nݟ "%Cc +璩E.)@x|FL`L:LQ9:PIBqu'Ǵ~UZ PU}q(L\޿җ~Ǡ,{7k$p'`@Ƈv0[U5FPq@rAwd[a!GƥlSdwqV湌]T}Α@6E>"w;<}h_jB?|eD86CR6Ȯ}ۥ1gH/'pՖϐfg ^Gm đg9X]_+5Ĉ󰨻?O/=2.J60AB/9(fM ̎>4 ioVK7[^I_T vOEJ"=;Mu}kR`c9?I\/u0 +_(,A~"\6y ԭD@R +zFj +4jT0Ga|TȪ}iְZ_VQY+-̮)bWc " ei<yeiJ;V@EϰOLR.cG {&SxA=` +.f8Rw/7Sf Fɯ1-p~}K{%|rP _]-9čdh-YYwj D4#jP8uJX4 [ApPM?"^I09F&]1D}0Xd#RGCRJ.$rm6)t@@т):/,WPj] ]4ណnDg"kV┈{W5ol\9$H@1'4;Zj[Wfb4VyvJպ +瀁ɻ(2~[#4k3J<~[FzxT̡{WBi%g8~]^2AQf}wnfG FʅFWq3}Cg/dr׫_X#l^N"'@lCTi~iܝ}_Ř.xA@GxuAsbDf:[všdP>i7n8_L@?ɵ+Lz$Qhk?JZ1zٟK#';P@Sfu: F% @M$/qe}{rBt7+L +Ig7LmU:ව1fe_ڑv>US//4bXLFk%ץ6F<+{q;AiNve#eC6a]CS&q+Lm +~B!?]oN}^9ov{9JrYMD#Z^E[wxE*~)CD^׳oܴx(/&!⩙4g{mɶqЅ!'y%͈ ]bvFsuFh-f럧jE@'ׅ1/s:#B> #TYI+HzU/QJvJ]pGm%vuJ3e~^DBHjB|(|~9>I 0 h+vQЂcM.2:0)_otk `a #y[c׸sunl Nԕ7w"J@(UUNc(Qywp]A}y͌1˹a::u)xHi3D-ޖ4=~8Jt]A\UҾ.a,(b/aiVf?봍jN~`;x9kڡӥQ3$' *D_㐺k:l~DBNHK#&;]5hs.M3)iq'wO G0W+Zs^j#=(T=C_zx*҂ner dL~fT74z紅s̴jm5=a?@U- POxp@W8-ysF0OȨ?`#̷Fl~1Yf8 ; iQ@""o>1i lu"d>"aG~$%[3:ǡQ&/` ]n0gTTZ"˱N9 f o Q?bZ%0 #S+r5̜폈:b[Pxgj!M_[$G(?kT^ 7!1=j{n}E&zG]蚨#P"bSQ ũDG-HfŔiT w!ڡ"ˁ~]ľ]ϯYU}O`ߪDOL;p_̢+prm(ݲl;ci2cOOCiR J ߣ𡧈O9nЧ4yj<MviN-Gru\R>\,cT(isurO, ۨfch[03-Ed:hFAdGX_B\1wgFm3 pg׺jۅ^s:eNjϗm@v(Ī +U|`/H4.3 Mof"vҪ+t=x,TY@>#R1Ǐ?/O4;?5(~*O*6i&}dOoNg1DkYsx>G܏-փ +cIVP8!mCޣ֩Luw6ԥ2rU~8`ʼnؿ/58H"8o[@x3xɩ c|DUm8VUBl) uV +r~\P(|w|Є5F( q +zfdz_A[JQHbhPw eAɛ=ǡA- M- -t=u~!:tAPGFuJǀLߣxj1KH/놆w"n9SM ~yǹ:ł]x.Z=:4ز%P6VH{=Q;Sz~#$³gKeeH-hfZA];Z,pF)6P݃E=xI[R[|Bki:N- %[#։.*ŕaK%'|֣-Lu?Ǯ`U}+}M#LnTlv VdH{SѣjRU:Ôn!yWĉ.6дL5\N<6Dhj=:n3Vr]+/)MoB01@ʈ@`T<(qy]_ĝegPS>\,2QpcuLk'Vc=ķR^vM&8aHψ::Igʃ]ُiooǿoooOIqq +s̽/˿+ '!:OC;[SҌKG}RJAAOr[Eov-6E:IgxL"w0#sjRcVt[؈7>UaQE(Vh4Pe3#wҲ#툥}"I0[ϑ6Bd` ?h:#HߘV3:4}jI;v!;.葈<X5"CD͞W0[Ugv+gl,coRPhQ]D<2w9^ Jv9"M8Q@SSΝPM2ߝ.TLdoot` t,wZSsFgX[޵s>W9 kd#X? V,0ww_(]B<w(0!3L&`@C8׍(;^ح@(O ql4z%.n"${<"0ehJqbdu{eT8DP9q| 7,nȜ}3"<w8_2>-X)mM 옑߿s o9e;sù<^$KtݜLG!G#h3ޒWzd2畂l+uG Wh̸%UPPRLop]5SAL00:O<` %oN]\&=1}I,a9Z^!2sG t&1/%蓣b.Wha m"|yK}~X^ל'n;bejiO^5f +.r+9KerTXB>o56J7\"*`XgN/ao>N>oCJZ--\jm#iEџ8S70K@h_BƆv@?87D ةH6ZW19ϊ7e #TeFĐNKT=( #'==6>  aҚdKV[.9Dc?R7lWLY *$\'72UT NjsڸS"AL`+ŖX0D7t3~'Ⓘ3:*r-u!<룃GĭߜzE@>D,ϋ.緘!Kz+>@N#hWΈA.\ȷ-t{\ygs(#*GS?Rp8W9TGfݺe؉1 K"W(L6c@dDW'wGfP1bH$f#NBwնWV yڨXмG hv{ }| .aZSHɼ(S]k0A5Li`PkETAtӈ/ϔf"C;IDEI5֤N/ȫǑ1ef)m\A8-"U˽-7UlzkU[@"0 +У@ëMf%47P"lNcXKI'jqe 漶^ItfCO}QkFGUQ ^X޾-n}{/~!pgw<]EƑki5{2e(ݯ.q+8eiʆ;9!]kAmSeӔA4X2tN'M#ǩ4sҒ + Y"~0G̨60.\zE30>ǯ,2Ao%ȑ~%}~%y^_{ϫǟЪD +QRJ*{i)o^z~<.`ED[\) .KGs>DQX"k';빏<| wTu.Ngv\l̴3#vwvXk=ag+8k\ #X&fxQ4{gţfYkdKDdDD9-,㈡Hp0H/z( Dyth\q1,by +`<-./Myn0=Nμ >"#~Z | t|+5OSKTX;myO 0"e1G}$Ckψܳ!SqV&!18?7])3d3u-J[F=/H%t{7Ihvψe-O^U <ܪT#5ȑ>y/KΈAؾj}ȝzTwtJ_㐙(*;'?'?3)bA<}[W9Cw#0ֹVX59U*mj3).œ-&M2Mv@MM7F^Gݛ;+Ne1>BδdeoLiIp$(R>͙X>!-)o_&-IxֿBS + eVDv- +wDQD|A_ 8_5ڍC{@cԸƛj|IOڗ(]=9"'>  Ð6%fUK.=- D~_Rl-LO,2"WIgA֜ 1#]fx?zp<+;?'UYXf@8~Qͻٱ_=֝ml.&˭(m6ȱa`S"qoˡշ?õZ#Wߏr̫qB`Byi|̟?ߣHNZ@fv7K/ҏI[+1ph"y45aC8fbڱtd*Wʱa8ib- &)bLCHUvmLtxoJ Բzׇ'cTB'me'DŽ +Q.]#h[>2"wNAFDܶq_'RФ5xe/DgV~H$vqȜp}?vPd4\_(Qj{_Xȷ|vܙ)_ UX}کl㣈g33!tQ\3B"o+QmK(\gqvD/ݍrzԻCvDfQMӮkD}:W"( P_;Ѐ=[\oAl GJsWc@RotC)}y$U,nBnխ ?ҭÿGul!iQt[Ehs@ +BjA:y~gF:ٵBF$AEa4Q߯:*B8wYx(:s4#fz沲uEF$URcX{q KMLD)eW@&z/VAv*^ Q7 +^hnIڇʃG :_P^ b{|7 +Fnye:3 /ˈG|_L/_|4@WMGf/xd1)guE:?h}a-f@΃wͻ/G3[! ]Xԟ#`z-T}  !$&jE?] H>Rb&7Rg@ Fp;Kqh,ʼ`1="?BqZ췏G 1߅b@sHF1*uYiW뫪/e^^66(0?MgD=!H28"D-mPc眩FG{r0M< ؚК, ԍ]`=_G7N3A `"u5f |3b&߷4r@1PӼPMi"|yh3j+u+(< KGB柛n5>#|@ul6〾JL%/lkX&(%fm?{ + Ͼ+~]Uۛ18=ueA 6un:(Kahx`2*Bo{?##\%Q"3OjD梾FhVM'Ln T@7}F\} 4 "!ڻIe6u"7%[W8} e8)4-tIyx qFJKrPERG,íߕ}-1L&' tJ'%{wQ,Hwf?)٭ݿ]]˯8Q-#~RuL2kC"qG-=Nf;#,}(թ~ʹ`6/6HLs%p'cēGA25p. 1G\كK@ ُl"{*GeټOAvp79U j{*QFom4삧bꈨ< _\۸DM-&5UqJrP;X 2+4v#rz@,UFxLB1qk{t U2YwClFK)TS0)q&4Vm0W{+^/S{s16d'`J%ly %ɮ@Հk +0-A43 JJ}M@UsN[`] #)T]!x0GDV b+#"f"ttk_ Ё@UEqøJ.JygAz4Joe.nf?F(GbaWD +*y % e=L$uln˔bx3tNnG- Q\EZ玅0K~0owb0kugهd6aW/#F;I_7+-x}cy(@G: +c8܁@1"X4jz5Pc%7$,A"1Dp5>)F}l9Jc!}(Ic^h4í]5<E_1.P͖7$|Q g݁!loA1gHG+OA;` gQ+;f2q/WEGVc-_va`{^pxyAƽΠ"@S?>k_c9ЦQ`y4kgrvJDLs~ 4?y{7KpuENcq$J+:#,#k{UVK P! MrruW~d}wݱu1T{@~Zmjg6=" -qK>P~PPc;:BJN +ffKӍ:w3W"&e$ƭoԆ)f碾6b%ؤI(^MD"2>֢u.C޷iٱɬ:нձŷ/WuR1h>Q|"JIu15k!%%w/1_Z'QHLuX~j=% "bVKmoYG%K%GBq$ldd23lBB?7Th4=)uU\.2L(5_q"F* $Ny2&ǎA$O(OtɅehґg#E|ܟdt|}#jM˖?T뉨* 9ד%*ʔ׻Ԩi3Ki3ʨ[+v4,zM[ ͵rP".Dמ"+& .ZgCdPߟ}ck4F`㰇BXݴ 8G*A򫫾(ΏIQTy*1*={K UN4Dqzz)7vz&޺qb:]|]k8֒n:+CRqt?6oU]5h+WFw΄uYLpK_}TF˓Ż`sEnG+% #Qe^SЄ>USΟOhCY_ω̑.~vDkyf׊ l <]-]ЪSOs:OF +\t䴾N=h3)]}AuAw\~.@qA]p7gpjΟ>{(@dK!к~`Q^ˁzՅd(8@Mؙqq;ɔϬ1w{6;c'Se1!aydẆ>d9$w F}+dYhi z, @,:RCSd$A"s@x-h^9#*4:V40gŵGK 89& OΗPcKUBe + +kA),P? vCրjTu81CsJl>Sdqf&3߫N#O%t?0w`OGߢy0MAFJW F˹,v{/s螯H>(!aXt#7üYTFȅ@alireF iD;œh8(Iި, &67M6;F$ 0KtQ"BuMReu:/\ +~RQ((n!sdݜ_gb2JTw@W`A7~W\9e~$$vgeA^ +%֛GWoDL[P vx!30ҬFZ`kPŊ~Fg)^%iJe(pQ6j~>r#Ό-@OSE>Ie +:OH{)GD.}߻Ċ0=QCZ,2])bf""揩/}j,r +9(mɽt[D{@iХش_+$f) +$,`g %"0&@U@)%쁔I{ѯ أe[㳋;}qh; =V}?MAfvz11aSv.D'ddwقGS4dGrmerrjIבhEfrw9Z(ǼRd>stream +]&Ki3w'K{.T@ '\1ls ʍm01N$qJK~M<[L8u|FqK]~X]. 0ZZܦC@ZofӫɠDm-&};W+eQЏg?',2U?i4zקk +oL*}?x"Q)5d\|W4eY7UwZR@\&jQH3]mR4u+e!F ɴ'bx{rv-CVl;wޏhauܙi= +ktg?BY>E_"v(`sG)b5&vBT˦{FI,KU9g51]Ұ^pxßjP,d1 颙+ئ*^+H|'3絩rf P}#7E75Ś hS'C7+,UV}}znH̝LޯY"K(݈/)[8i~;AR ?3|z""j!} Q&(N9q{Df7BiVe|"$yw!†(qi5co'wO[bLPHQ):{[S!AnF^:0w΃%9ԋJUEczI/Ro)JȈ +uv\zdPBv! |hjJL_%Cs0k&I_vZYVU>8X1 d N5XO 6^U&αzneFoXlgw{ZN\|vqwiBdIam3䞅dDCQDAi&dl=Hf{s臱Cw`V6:0DFr":d>& +&>nIÉOTףv;sKpFzΤȨǸ I$*tpd*(FjICF#gg>gηbRE5o@'VNp U\zFA#edZY/C f_'QJХTEn30~91ʱvFD9;z~؀Z +bU)ȓ ȯ3&hSquh; +/[trZ͹VNe4z+8ϘE_yo{РezvL!XIW–ڜy@%M*t/5ibCQ O]&3D 2_:.|H0]62yIgI)i,~0I(=5u.yίC^xA޾*`0gOT2`#?:7H碗eeb47<)?s؈f{l,k r1ūFtF$ B]Lr{^=:>T5%EIn)UM׹ZQ:qϐ5xt4{+bvNnzWRn_0irMh(B ^+aQBR8ܨ)l<i4Leo%,Uە\3h`CfiQI'Vᗠ WJ]iN!m3.tE f + =Aa*ko?]BW}U:^eQ\ИEg=o0P_ qÉ/ ׺F[Hsn" 3BDsi$ D-ؕw'$Q\-)4S)]ip&s5"׊TQ~N*w*P%^$ʒHS"qGU}O̥GzGh?Tj:"m({[TuLBAeK|svʧqd-.:iGz7?<} GFFBlB0r/,[ILPAfJ뼤J8G|vw?F{HsFZ?4 +ኡ11Ujm=>gȂwL!8I?<0;b_cIDkߛAPky_;S4?RaE +U ]`.Xr@Ёy/ p% ~X,5tkX0]hr#1 SZP.䷩>/01fW5]ҡ^!7OWϯ3 ,ŸD2$^bN!r|e+STK}H0( 20!6(?(_h|:oqSC{A:|n#( &wC$NM?Kc?rĝYFy |vp& VJ Ҍ%i2Ǜb1ؿMzDMtx(P0`7[E?#^ѱDR$(H {x/[%z{_z3fm_h;}m[:4 u)q>lj8,uRWdSO&<=6#H&}l/~\h6j<גu?h!>8wϨd9ݬm{o|g|m (ĥ bgDI0z% wv DLo"fa%|ohc}w]mEUEz]T3!C>V3̑2uc7b?0o"±Sg`9:GN# D1ȜDT*Ev9u;ctAQ& 8JڴHǦ,AG9]'t,7]*@-h'z](' K2A~}~'# +?`aTPx+o̓9}L(߁C3H֛֫wДO渨u#͹r9'uji~!Q$ +_݉ +4mWKDS$oæn:ÇxD֡/Ws_-,Isڬ&lDODS'9Ot>O*m-" o7< +P=_dv@'FKo4~8u,W~9[045K6KKE\ՙ'` ?4Hړ?Ѩ,_L92_a{^%ϣksP* RեTR0ddz0NQ`nUή1F6hF9p|P `Ht!s/ha,LTϙrPاBIQi$?*@Mj Wz"W^gi3vJU u{eX IGOPGg jZ g4=lO65f%QiNHeLREuUv1Q !FWKݶޯQYS5+bF;ti0F=}z>l650~m4iiL$lNi=oKWχ3?i&@X7i)gby%L\)gğDv]a?sИB"]b(,_-y$Ngml3]X:#&u*ox@.bW0z$j'"3}2?TO߆ܻ ڊʢ|pNԮy?!3٣JsÉINr!ZnCn:;B_Xme>o]?a~8&kuO{?lh+FmZ)MV[Dc]Qz|?{Ohў4yx^?\Q~W뚙82bTNQ9(±okn:n0|??KPx@ezO}O FՖlgw+%CyLB1]LZ ڂ6m!~6~Fs)|eZ~ cM;mo4SD~f]TBOP^vq#9F(?6[fQIHJLAj7x[YүM;jo@=XDs B"A wb oCBFBמqžӠ9N6<^FRcn@/"17m-r zV$qnڎvgQв%?>ޛF^SmOvA]u|xCR*G ovVDmFĦ-8Vi$u$K7Y6 +.C}w7<;ܥk9291",J^d+'rb=baG2Q9Qrԝ\ +VD q8k$® }t!Q1 +=t;_ztIP +B7,B> 8IT':?X%u62F'qQ: jYF٦ b9JSן(۩i, u ߡ94a5]@}FD:Ga]'T@!m#ʡgU̢s:(R[ ӏI] I8-[[p8[KK,|t|VY`אHmvaM >0R`S}|O۔T$я-ls=DBpFpƠ)*v%2z=0aRt"f"F3OD#Q֗u7PH_#m݊KڻږgzOwX7XR-iou BR0 +4YjVUdIs5gUA-v\9Ȉ ?Fzer ֹ$f⪂ކuԭ$*d9/浍Y)[o-T jMS6jŶ$t\7ޅ&ͨZ_bv0!Bř2 3PF8tkR0ًæAzwk>2r1QC~{Cg[Hj_0%c^?Y" QlG rÖHCxU@@[\ZD߫A=}yɉ2nBrx/"a{R]M2bYl|Ļ顴HH;gL#c)j+Dn\Loӳ(lI0ÕƏ׷h*@l@Y7nA/:i4^"?GԋK4!AipC֌KKvϺzH4rM"',ٟ5Ԣ͏UQ HX-ۆ4$4۔o\% 'I߫Iö$-E6DA +E;V=O GI==^O⯾/o׿}__o{;o~ͯ_S|qq^pnj?~ (dV!}k)>HhCFIO|'"[QicG% Iߤ..6QR [*,+!1^ R;6LY)RXHlKU->k-മMCFF&e:Fe4ZN+LYȵRPp +bjFNmھ `2ra!D<|S9SCV^gjJC&][( NSGs *"+D <):`n'>+8hƋ5t]B0GAal櫚!ƙq OV8IIdJ RkŒf5\P wz,WX:N\86!Kl/Gvz/0I_֩n>~T&ߝhYeV:!w>I6)S/rr=V ,D0~*A:ЫM}l*3Sr)ޘbTzl6qw6$I{t|8(he$T&o|!d2&sHJӋ"0l4CL>~hLގo6;IF@30]05sKg%-K^"4TQ\F<6TB4s,֒TyAew☜G-7!P0>af&bq(ܝ, lxdV|p 359zcJZb<^HjzكU;1Yֻ׫d1FrU(&ۈ\f~Idbs)-3[3H}D ӿRFK?Hbӑq}XɐA0b:PVl$qQX$ L6>'=rvl3.ru8g*M83*+ľ nDt =*1- 'bbi`'&yI_rl9h ~9Q3 id!-p Io)cAd%)#RgTfnͅhu@HB֕+{wzKi$ԭ9&.At +@8F:7 +aH%]B硍wZdp^D>"[-kdհ;[o4nD5-*oF/=۟m2U.V;5@с +fUn*ȃ@u\ũ{Ɗd:=6_{}c4\]6t3ҩR3K eQg斘ZJ@x_,Uэ; &eHŏekl5WUtl SQ;JJd{+np]I"r9p:-*=1@xºBK J.LXoPWy +dR Ez8i'vs2C ьD_ AHri&} PX]]?KxE -48@m+dl/BDE|޵F[h!Α + [*j%'o3M<+i_mJkȤ- +]n\*Gc_~˅/5uR:2K`r/S;a=sɣ}|m1=LtKfs @BE}<@AV3y:/WVnSv|%y8qZ *G76L`W@ۖ*瑑nY|њ? @M +' nua3/: +4C>K38}f 7%+Fg,jJXp@#%p.) - \B/I*:POC*OM\/$(dM*N c= +l>sjR z篩)EE5THHHzW#EP 1$FANz 6jwnʾ@֨v${ϯCBD[b + NTi Z;08l4-%, +NUq>ӘF*ǵ|n-I%n89禂X^Zszi6\ZZSzә\",p U@D5=;.,E-WvD "?}x$L`O@>:-l!Z+])U)g&C˂ӫvH僂ͬz&oi|txuI(/F~NhPC{OlU]e\e(m+=8/=T(BTgKq"Oz5@5ܖ0GCrz'|<+%5syB&\)kxa/% +7D LR->[EU%OȞqutIs =?8htp{/56٥#ҴM; "qʋ,_=gjڨ4B&:'K x|[ +, ^J..F[b yp1WW*Qvp~eNȟ(ܦ2ܩRjg4Ta@"0vi]o(S$&||G9)Z*ݶ'F}c/-T>?#Ysw6Gx 85xF"-ݗ$eb/&k% ӣ97%'{Pwt(8cGLXO.8d6S͏cY!02YߍfGXI*fL?XYܗh~`QchͲƑ!R^϶(@i27 Ii8IJXۊ)74̦+SCNԯc.7~+ tʤcp1"ӓ1o;0<7805S;Ȼ k}v@ +J6JH{Zث<&$RKXѲ?V*ڵ\Srxdi3tR =su@@EC(Rzͣ y8X^w}#)SVG)2I&#H-&j0T[FgRbA&NJ3_ͦ6&mTSS̋]@#tG-l>l[JO~uf8@x!}գ{Ϧ֫~t2~:U9>p95넀‚,&z(0u mEs05-N6I&;eb҄)s"łl9Ǯ^f'ߞ{3< qLe(򢇞~蘧^lݯxR dzPѡ Ii +qNbP%e"^&(L ٨!pJ1$aTI\YY ϽF[hP<8~kMh2:݈}CCx &1 aˢr*BեVL+vࢀ]JRF~qg6)$bd(\YEbήmWJ2Df1\g饌gzA: "/*Xi H2b;5M\+a<$9^$ +:&qӖPaEtצPA SeI\%}Z.SQ~l\v5`Xֿ0H PUv!W1^8,k + O)H,UsdJKq +i&4eOw>ɳj2;R6U$ +jƗ0Ws @[̐s\βch%Wq|/%K-Wc:ܕnIP؉ɲ)oη(5\2v%"6VPGzLy2FP)=dtpNщ ,NyZlyHz:bf|SgqHkI yҕS/k#'hEaQT=W(EHBIr<(ZrRz^xv`5^h9 Bc%#9absIq6f"Ő3u`AX=2:ZF|*@Y6[փr+0zH@FgBFyLi?9J$})47agG.(@6 K*<,+`|IDFkpS 4lQOZ!B@@G;PxPAeR1o TkKQ1ZF?sz`U "W +_W}"ͮJ ,}6+I!pxhwk$J܁ w-Ю-=<.@10=g隼 `!?y茶ƥyMh73./;V;roɁtbI/0 NIJܩcP:Nz@qz)碫askU;zr,IH+^%$+r@$hkU7DZnLi"ܯ_nw PB0|GɃ9WHi7 쒻FZuٮgEQʖ &R½n/{֤Wg"2 +1YDy/-3| 67d?#՟aSAP';"s.#Mϝ: %N֞cr%7T(ljl$IͨE +G6JAccbC1 +\9U;4 } -'M EfM%V(l62;PBc'dnC\tKIEl1;+gɈT%zBœXM=``#AX(FAR@+>9 O< +3))mXkCR$u=W/yKlt%{sȧ?g6%O0R$Sզ%Dp&fe +<$$h'bN28k@D'uAK E=r$"/OʧHopqL%2lwV27ś`ﶤh>! *ޮ"b!Ye@ @)e+LF"2$vb5,K` ;2ru2Ļp2.!-U;lCu` \%׋zQ'+w`%%wni3{T'ׅS$I$tJY'x/œH29>1[C0 ƆgS u+6Mu$r͊@"P;n+/stRs6""99!23?B{`rKI*#NL?YH@`d?QC$N$P)Ѩ5$ +kBn"`7*l3ɛR^{^I4/߭rb;XٴѓP^D'M1 KDgmXKf$!cࢂ@@=SaF&)q-3?5ם]UgyV^USFUi,u4HL +xǸZmWF#$diw317 D[MF~5a$\cd:5CE2dWڈ{T2Mfyé&)<لu*IΌZɆqq_@L3lPiTie9w@:Ͷ܆0u. !H /$!UkekPCX"#RAAXNnio<pUI4Q[79@r}Of7 0-l%:Bܓ@IQ3i<븚Bl\h`pPJԮ@/u—SDŸҊl.f E\NOG ;mE;/<)mWd,=,S믤n& +H#MՑ˴p?j[HL"RrL&J +05B2^/vB:)a0'SP4+EtA@bpX +}R]piiܭ@CT>,du^ 0)xWKDEЬB a+hYA+AܓM )>m^ s^~F , ( +9Nð]5VΦƎٽN2)i!N/91|rF@ E׊I"EÖ4280ID2 `G8PrXK(W{xeJQ򊪯8Yu'{poϣm wB#s[]Ⱦ-١ܺ 4VMM@o4nG+DH僈Qo +9n 30 XwYL&0!b2y^c >1Ď=^Khvp;X $KC=Gc0}9JwQ{ppkN^S +)~>J0J<2[42pf`  17N0Ui.T >P7#_)M^ơ + bR6먘$ÄVIԔ$9Ah7M}4[H2̎$9̇i(i`#r]MW:jdͲK$XJn |.m:HWofM'i|@!/Sv;?-#N4k\v-d7wn7mȔPC]'m LjR&WH f&':/s’/Ζlޢ#Ga'7FKN:.iC[tS7,\/ύW z=mpiqWxT=(f$Dql!(H8o<2!UJ [8=0u`S\(ڥ8L;EJGb]j@^eÐ81R,BZEM? ,&Fk%l[R aWl /ZD.QULa>3ոr7ʶhTy5EbqF-Al8>L°KpQ9leh r*Elے~0Rf%`e~/׺[g3dBz,Q&g,Q)6@-RIH]ƜuEW`a͆y[",DQd2I90ʡO3Ua} +2B1>#810Næ-K? `}zWfP=/35^lԣٟۜ +i2yɮJCx}鉚u$LɈjVm4dMTQ[.JP:V%H%lwm +Qt4;rB=T{aC',C0]5Yo}$i"^jZ3SEQ A"6hoR +iY4K(U\I{ tB=A)Y!i^AA,$ƄEԴd PtbK#7(d;gn!E;/TF3""zv-*zZm3Brs~ݣ.ф7u R + V$Z25Z!:lT+Gw- +B4/. +ܦxNKf*S6`^u5~U8 3+.B{"bVyeLBث7WjDXP`F[/L6gg:Γ*5FE59y"+#A=-OԔ?CBl/RY_(cXBxqؖ+vd䥊]^``mLۯ6 +2"%(N g=L V+8 Bm"xx1ۡʫ wbfHuVηJ +,JIxkc2orE]Q@!eNR :]|P>I3II̾Eln<`gi&q$Wm蚜X6TQ'4rKNB8PXE(#k9ҚELQCXl:IX}U,:()FGm/zU5+|z'͆]IaZfF6k&zdWئ~hʀks@t=~B$ iq ɄV/\O .k'&GRi +ZI~O L!! S!%/ԋz-nKx):Xc!ʪǰj=|ER=B&-&YX N +f\\3N[?_$V\/ ^VjANFٲgLgj'l5سgY'"\Sz(mt>~uGqo .;gi~t<H>OR”,;h|7xu睕J/К6 c ]J+iS{ +IXL@ɀH {nYxе"NjZ/J]sI]< ienϿcUs҃} F}R_N +ʝYz2ԐW+Lj^$il PBH!ӣF4NGCzfD<)H "%cޝM'[spah54y๡E@쉱W_t*IE*aNV!w^>Ԫ83H'#L<2Y) A׊x|X$0|8#.RoBkq9@ zW2p.^Iz`N~JK@*hAc3@d KIz|2}*wӟOP~k1%rVVeAxy F.Wa2]fHlb-d)8I^Y ++`YoPT~5tz -NhY굈1FjP XxNByZ`p6b*^Qf-]( d7# U:s6Clb}vHHgvcf(JJZ w3KM01fV 10 +TiHn {Jr)!!y잓cHjva!R,6(Hjٰ!Y:Τ&liۜoR=a5-A5t2Z`\-,i8=JyCD%24QǓN I~iUlXFٝIQ[.W^&>ʄʿ}ÙrKݰys^ "XcSý20+)~<`3;evBs JQuYG^2ycCEmWfm&хil];'j&Gw2z&+X \iˆ`z3} 1{Qi2g\?xctٰBG*/ؿZl!xaؕr-K)JSj K CFG:t1PA*7#e[ j4MANg=0 a0l,6j0i{!c6iL.)]EvoVT +4AgCg,!kGnżlcgSuy.yO;j!qvAW:$J()[zx-#F MU4U/%1iZ" 9Z :b hvI?*+p06 +k Pp۝[^]cD*֙R☸4%Urӱ͸rxVHA@ԖlL۸!* +.!ADbʀzmUMtr4l;rmd&lq8л/ lS쮫vjC#|Rdc %@|7Q(tE.dT!YU*]B%!qJcY [ `ViBo X +}x]\NZr!I,k)Hݐ /uwQqNvi=ȞŒe.<FYҢÁ BpIe Cj>u4;=VBy]o$b%^נ,Wz$l;%pOxNm^9)T)-(N<fLό*; \B],-e'SBW!5KFo/B"^ؾ`t17 crTt@nh;m<])6<;_evGD;[^؊jh q6_oD'Lnq-*4||aׇ$8%.5v6zO#I^d'UlJM&%hž@plz\6U}n&r!^.h9 !+hAkd+ KRgJoA>: '&v*o:SsnZ㱵Q]"MUM8Wm$WxG̺<}mNEJ3:@7.PMiG"/3A sAesӤ.TP^#)SEtHM[$n^G-EtU+˪>Tjp{a>YQHI%_<ƙ2'|ثl yi\P EsX,U OEsFek Bp]>)/L7\FaۇM](M8fg}D3!E.i_]"'Z, ]&843K&am'0(J)Ti?G7"/U#I_2j'H4cFxVl#N]Xlzj#,`%O.q;KRĊC^&gP݇ن*B +ߜ!yJ`MS +Lhܫe7 4кLw)hQɡD Sm#KE~@ (\s俷긺&υpꯤV+Jԣf +s/}D.Q$n^.td[b.BZFtuU/G՟w}E@W 8qK_LO zb%۸rKx)h& U0(}?XO<.GsCE%OA"df)Xb^zp}OW׫d ̑T;Z+S,bJځnL34eֆ:jʉ<`c#ojٴcHZ!vwhNY7l_P"+`p[URX'>`8N|+i + N &haM.>y|JB>(7]VZ|˫(Ttwi%0Z "%%^f"mRRTsդskxMˡ> +la8 ,<+9=Ʒ;SҦb" u"_ztTBatO Q3Cm?w? TJ5)YסBAЃS;wy4^ ^ipƼ1|2 R?vj8DjX &U 0A3f ]24$ec@elU_zL{)evf5RR~Cbd_Lp/_q@kL:X kq!֩z')5SMU-=g"&yZN xEJC{$4;^xp4>!MaJ +a. pT"?= ,IB!cti9AK;.!p;Pن@%*ܔhZ^nc8&*mrfQFsvpҮ|h=8!E&;dZi?8P:U8d +H,:A d>t֧s9ud#lr+ȼxbK0I Drtkr%]lzED)rග{"sij?D^@™ݮ,轙W{cR|SRnp>BT;3IIz,Aj^(>;-f^6]{&I@=O`PZ' +ٚ\bc%eT}&M݇$8<%y>9>hDž\@WH8>KܨVLԹ%K"K]fR/GfX=AL!ϮBm" +$̞D5!6UǙקiIh iAp,y(b4,n jc>%ҍv~w` U-??Y~~w__ͯן?~׿7~}N퟽ͯS~ooomo~xћ矿 ol7o a??|R1ND[5kcndJNtWɯcۖwmWʋ7#ͤ^t 뷉4^~Q4z37k'ҧ7п[o~+W4GxVZcO]K*ﲣmğ_[Y]ˮ'/x iNomD*ך;jY;!=}!lpҽkyH44Al|"$ϸ/Ї {\6{'RUJ:x>*d@6[ +Ow;q5ȣI_$8ߗ'+n홢9z߽bYY{/x_5GħTo1b֙<7W4bs'ojaQ}V3U7kK"L_דL T_q*gńf.+.mGٗ ~w@̷\[5¢A ĥ7^:g >t#ś2Y#Ms-XM[ԳvQeH@~LM>ӯRf^^^sI@ROVƛ |Q1chlYh4u蚢+NO~ʝW)E_#"6 +3wԸ~W#qz+O\1oT[OlQ;"c}E”{ulǼY1o cŝp7^cka!=::{:`<16fqcXPzocI!bB--;JֶW̗cm=?}~_^ҷo}}"ԏAqJ^Y%"t绛 +L7ß')>9w{q |E4?q 0jocϱ?pQ.XgǛF!o v½3pG3P~X杹ǂo+D؀7C>YCQ}>;X?\{ˑ@WЩVswhmFc}2~elE|y?{in J=>e(Ŋ۔sȦgo3{7-O4|=}@(z?[L^TgoLDϙ}.Bk,g>;th@^bD,ݬw(4{dQ S!{^]L61h@,Y=5cr >G~gBir|s|ͭd4λUχ~Q}<_OrK~ɌwJ]& WqGTV Wk57cX # 2ْ\m{c#vS^quEܬ9z yWԤ-׃{TnhQF06>{;ܶ]nA|tfp蘎/.ջ7m ^ +ۂqfao8h"k 弽Ey\?! +vz$[$Tެ=)6n +OkCiǰ}ܭ ^0WPiB45g+VzȱEA蕱ߣ])k:Ǡ~E135^6rv V8 h!$boɷqq oJbó_ӂ\Gں'=_?p$J 4$5r|ެ;4o, ԅo%(v }d7<>(w%d﮹ytok\>Ÿ7:<|FLQ=ʋrM֛{f5ihBs[l?6~eP"oKMg%4nc~8EFr+$yߜf?!n}hrTeW~1WlTkܞ:xNޠZdò1oW$~Ur^>Tm&1nUcYјm/Bk4üg^Η^ˣq!jN=Eanπ7<ѽK{D~9!Z4_XmQ=~. xn}l;CnرY$O?=?}+ +ݷi{sjwϯdt*ɺ;w}[c3iD&>Ҍ @%;E};L#J>,_ u%9d]^W%ǫL+5>n@kֽGO xcԜRbu:٭~DOVwaZ.~ I ğG +4-MӸ?M'6m +wpjq ( C3e#k{|o쟗 +|?*K 7;F'}qc=[l&,go7#ʙ V򴏹c{'mu=.y+>blHk +O`db.Әq߂R3&.(X{ +͢4tg~,DwDCrGUÚû'HX(Ij@K+,{%Yy.&iK[yZQ`Ǐs@m0挴reuO5ʖMXw^9߷[̏Q/:Z;+jV.1Ge/>~43Ĺe45k ;nV`^,av&>uh\=ǠůH-εN ?aiF`(a[4IK="qzGe1pPg/f0+WfU5YO9"ԋ6/[bU2?lҸi#l|7nm˝Q#kOO&P3f)lDcV||j}fc%is}kD{mK ~gowiXx<%qgFg;?tݡ/b>ڽ3քQ*b*s,+nS̎}42(c Zq/Q5QEԋ%Py1MpzYȨDJf^żPFF-_3T("G4.AD+E5Evባʾ\_#m +Kl<A&.]| Xٵݾ3/-kM|wrİ@)GKM XJ/Kpzc1u A$hgz w$dǃs׌B|&1.y*!4CA-jJNhx7io@ǩXU" +R\tvw FMn7G 챣n].6Ep"R/Ұˊ8j#:{i2~]Bi7WyHr] 2_B%ѷ$S5,#hS7Q2.DZE}vTez12iwK~.D܂7"3沙W61r蹳-Jkb;ڹ3*p&"\瀲7Xx]HXN{Pnf4/]0r]%4}+/beo|PJj5B{.Bd~ZD܂KH @m9>fAƾt +i,FV_>0Z2 B4+5v?`)wIqDt|ڐwď%ErQ%aOlX[ء[.gxc-95=IX6%v{GPg ucmi%S* D~XcSe#DXæ_UFAZ%:˨Pϛj Ly_@L-廕-wIcWp[ILoy|p|X1PGn款L\+AC2,w +F162 lVɲ32F,h nB|h8$F)$U$raߥnn\!VOYo,#2j}հEJ@9=Ъ oo\U}'3zhbwEɾk{cWUG{֨ f7v&)Y8|S2Y awɄ+z s=y{oWx2=T֌)Dr~hq*E1@qZL{DC~Vo="J\lVrTCq=fyHǣjB51>9? +cXnI;n*Xਭg( UT~Lk.nUȋpdqbA ^ +,jƮQw97/mPO ;H1i͏Ny*oy[q.tj8+/Ktwܭe^v>L%/[;O> ĸ& ?l@AKqx8!079o*^ uM\kԣgl_Mj#"\ߙ̚~ouwuU\ePr(Js@9YYm9ȒLLʌ)Ww>t3k>p޾8{xny8eI*0#Vɞ<JwRŸI7||+m*)+P)]]ZhIISP (#]HH[bRG謒=V9TR(0CvLDbd&=:"ɐ)JdFxY~3埤xIF\^ȠsYTgg*ym7J*fNG$.+!*%UN RtAB2U 9ь./ +QJ?Kr7WW5̅.d:9b}SVRa/,% (2d+jRQYT٪RJs"!<(QVnì*RׄdTB ʊTB*դUR4zy#O*%ZCGЉfGi +(pҐ "S_NݛP^E+* '2G~V^R +lFRHȤ | @^@+8WTtQ\ ꤸpE{ +L jHUL:!jb:/b K*W/"V*eEϲ(GVF?Id^\EQ~Phu¨5VYZV%+Y('/<%}I򜦒 +cK6C/d?mx@YtKʧ^L |B{4*>J +xz$],TR<{G +T1375TUDlɟYE +%qR=Yk- +\&᧓tJ8ɂ庤 +vF䮲V'j>O0Yw aT +( +G8M,=FȢI?WP>媬(P~)QYƠj[Ӊ%L27AHJHYealy򍎔tAN] ]Nui#\F_cSn<#?ʭ//T:xQIJ\ͫ @B,J)Qԭ)TJ_>*ePu2:ƽpB@2B5pM6WT`UQTMXُERWBVE**zYN%PFJ {HiFjl^Uֵ9Pʠ#bV,RΨ(JFu#˘ j&YCRMTu +CTd`@&[{C{+-Sj~دC2?lp^tNJ^V΢rRʊv0*QtT;jڰ/U"]>y +sǨC|yo*JY^D)ˆtj8JݤS_=T*tc모Rdy3E̟tlǨ`,V!"ťtn>$$(`nS(t~FEp 3mŁ2BK*ސQѨ(VT!A5BŊ(TX Q${U󚷀ug9eUQsZ?$!YUEURI`QVzyړQ,F|B*&4c*T$QB)E]eŜ V] w/kȂ?bL}Z}&CjT.9B^[KiVY0,.([U(ܓHOR2xqO&fL9b %moT(Rx/x1 uᬕUPtϾ*<^Fv`kDLb4)1}V"sgLrt2 UYY|_WŢ4a+i5瀪,TŪ6ȑ.)K + 7"RL[ +9R!;Wң'Do5@?oQ5G}??>/}o-Ғ];cnBd0RB^iQ\C1- M[%"mt#(t0xU| +(I(mT\[i^tdyj *\ k%WWUW? +.;zO:GE8B~Y>/*/Dȏ( G,3?zMyg4S|6ZFePXQgA`aJH_Tg +բ=+{[lugpz3GgUo*R@{Z{EtBk6G7#$f Oˏ-,a{{1=O#ު6\~P,(`b0qFHʼ +LtK-QQ(ppa@ QlsT^9)+TgM0 :~,NMcJ*_]g9?[q޲':o}&#bQeλb4@~%g K H=/@KNQbv+Q^~ 8,j +#Xx5~siԏ Pow1QCeU +?.4B@?.n1 )q>Jw!!aѽ_3벲1إoڕ +42jeL$"QY\h@{Y&RGhFQ__[vS[8Vx/oImt>XZR? ,fo|O+wwp{۰lʘo5c Va eƜ94~af?aDg.y;I#VY˫ ΁i)IMrA݃;tn5ekԦhW族gVeyu}8:=RN-/fp9c@9ix">otĤ6L;“/IkvEL''*_osl} + +g؇ [ه{ҤMr>>' OfM~9R']'NKٽ6?~G#'qIMʦ4])wl|[껦>/3RsUN;t:+7yqwʟݮx>0:+1MM2m56gLIuƸ w @zNqSƣ7T&؜:6/oؚ/ΧxTv +f]ގ_x J cO'sTS3#Q_%$:u­ᾞԆicv%{*%B:]vr9P q*9i7nOsSf7k%Wًy #bEgq/O{dHT0/WMyɵYфCމ=;͢.72Yٲm7M d܆o6(p *ؘGXHZR^yw&S؜R+)^ E`/Mg0.䜊qMrkf/8b2\#/Dx4冱 X@їVɔMx~'sɷs/?|1;=b=%;Z54ʒNewts$2>9j[~!n]J,_o|*ζ_>׿ аwnեC*~6fx6)ũ_.}G}Zqtړn>H8yՄ;dm``-fs.`u[ܐ} Z+!9RΜ[/ޣ;xcIbRL8繧enؠi$J.5;2*.-74TtqX2!"Wj +G.fo4:p-l ]']|>aɦ|ڱ%7 +s9KAĭsrAGw^lH֛8I井wrc^rtop/:_I1Pvd/D +gĮy0Q6s{WpZnA}!8js哶E}1,}ݖpE)Xâځ(J8D8vAM +t{&kܞU6nLJk7E/'6{>Xجw̭'c/E(jҁ}^9Y#0 QbZp<:^|lt_xxw=n +Nf} %+Q `O֍{UIwzn-/ cdS7'k~ $k퉞w_Y2ZIZmINtЄS\ZYfr_ԩ^L/R>e_k!,,;:]fQLY:4YUt2UvjY.Ub" X`] FEG+]FwH۳`o$ *0e|:iګ$\6/'<ʞU˵̒NG.ʭ,;c6az#ˆemn3᫻΢F 聴[p0^?n-y1S rh:8x)p |4{?u̲Esd0&jt#³`1ucwqk~Ĵm3Vi}bTGŋG6v-$Hf/p)Iظb šS|~}o{J]`ڭVȂǼwUKbؗ5S龏_~zKZٞ&M{ i1I{csG:*3%Ws׏vGv0oM[3}Ffz?AGٓuiII֞tުկr[{pQ߾^|diѥAgd<3 msqmYEԼ=VY-?΄ܴ_џMN;Ny`ظ[ѝ ,kEYo&$jY?DR&0t͊VfY؂9Z+"3a؇IcګtLǼCc)o 48Eg~y7VVp |: 0jw7+%2Ug°g`-ޡQ7z]~ \-u%1W{iFrkx9,]Rz3'.QݶwQs*NLhFvqZn8j,)XXLV' +/yr0*@8xXxJ.l``!8]֔Cgs % ~].1mjǃ,]2XY7L:F/!t}{;GXos}M8|]X*[#fGGiVdj3QpVM:8Qk靳zv9UJ^=|ZAw3Wx}]0ݹ%t]S#FyG#Ӽ#UI+::4nԦ}Ad^pr}ͩNs}ƁO:.囀w'(6"3#i΁g:˽_7h +\lUt629aWoc0o{Xt%/ z 2ŁDR.E7vW `{2`-o{2:=]fO ^YUq򞤋w*f~E٠e4}ҎWpVqN).v@Ogrc&moۅ5۰WlcǾ k4lEL)piIxx+8ΪNsaH;b}, KڈF!a0`ʄC\hA&k:Dg3A Dh,9G`GEX3x`g.G`o~lwܴ>!Tʒ] kVE/1S&4)`;K M̡@]J7_*Q3t!5`>݈ٺ>zv-}u&?ذϨ5jVU[tNt&2#Y{,>>g_wWkO6ic\KxKib!hpVqY%swF?/kܿl|+#:~/ﴗ]C֌A?_?ly]o@߰1)=2/ +O1ޟΥݲքдwIÎS}gN)wzMa&70ʳ.: 4JvLISm|p`urmgSi?V`auq!*a'j5+nh4$-DeIϛg7~1@$;`v٘KҰ8sq"Kra/*Op0jDt>E<AO03gT5LHLz N N Fl33QtdN1I#MD<x@޿7f!Y\.xHяH恴ߤK%BWGI/.H!n(z6lSa#£_eV->$jm N%fK;0ۢ0V`ӡksW\&m9mWcYy[گFݪjߗ1Q7&ǧEgbqm4g^>4u¾KKW5>:wJZsy=ci۠!:JϮ=ޚ~?ωkyy{+'w?Z9q p8my.pp7fܩFflO5ogdGg4ۜ8*`wy\P'ƭ蜼347&D'~d؜J⠓ s';?xzOM#)frRvm?fUd0Y6ֈ7lv|dWN5XjE֦ 2RQmeqGǾs2ς-X4ћosJavW aře,o2!2csNt:5Ά"-kY\W `G᫙DHڙF (w 8Hm+8ptl &mn؜8LDxGWJ^}ZA&hss(>hF"ƴդvҋ `iZI|V~G4iΡpĜ_0yu+f?S[5/tW^vjIyu+7OsajY=658Elwdϴc oamf}Aܧd>i{.m`L\ qUӟ_ٞx]6u0lArZe'Gf:.p/-AO|ڤykN x):^^}Iq]M9ìgȆ5R<(i좴;Q.\}j<k xii Ԉ^$xiwr1\r[Æynw4~! Ṙ Iڤo)M ̭Yԩwa~0YTK>!^1t6{0ɲ!taٔfs*)2 _^% k.ع9ِ^rޘ\})Q̊KC"_ےq^P=9%MYlp\Lh`ဘUݱ1-FjR^?}al2 klڗ#FsxtـtTɪC[欦4נǽz|a\>`|y_^u cv4Y+XsJxg9s0ᒶ>6;&|z5Rv{^^/գWODW-MHb37)y߬|U؂=斷mDga%7}iWG:]K*\ ?Ru>57e765=ۢOpP.dYjr!K 2z-sdHfiFfy4`i( 9hP?=~p o2ҫ0߹ɰʐOE sJ'O'_Z}:=(esb# qVtk%wI3ᑷ=ʮ$:vD)9d.88lɭhpqgkNZiz_/H;Y{ i]=;ؼҭwg/=PKg` VEΨ]Ѳ>-}v6.8x:kw5;AZ2i%{=Z\Y6f4m͉j_f6 + qÛ̕ojVKI~/$CZ]oqGZ%u]UI;:vn6uI;<՛𠜍Gޱ6'}S %]իoilJpbro3CWDmdWb6mjq[s^\}} őp9Yٹ謾/b=XҮ..4 +D +=;" Zj]s.|;"t~:8v ]ExuWqɯl:nr$ -F1qIqVt\\xKq>2\pz`qCfEol:d]FzŦ]ջ Mx|أzOfOwI7/qSǀgZWz'Gƀ|J.e c.lor>9;8~4 An!fEM{mKvok"Kw^Xfěcvqf67FҶ)ޱKLmY` 9v=񶀗M΀ bʬe}O>OǻO9!i-\ڍ¨$;'yu7fr`N</v˪ZWGm,-Es¦YlW=ȇt#$>9zgWME&3n$#s C)E2 +ʦ،zwʶSw;n"7q_ׁ. y5Ģg}`nP1lBwڭo5/}3Йs e|"N;S؜M{Q.ݴwxG`L pi 8Ǟ~0ADQIwh99t, zI֊vCג.eگc($wr;Rs+b G&;g[M.hz0HڳAuGo&g`04^o'47^dk6ia܃6uC' YFI/^n[ѵ5'>|>iE:^Hx/!YFA1AOV>&3ҋKQa)X#mƃ3q\ګ[MA!) +} +67+zw;ᑛ``+cNUw­\ɄUgFt֊,Ñ/qEIb^ 눝U 3qј]ٞԮ ٞNxGgM1q{Fv66lݚQ6L})_Ҫ&@dNٱ9#Im=f )^;Oz?X/~y"5 Xۣʬ{М s;$m]ڵɦLgJ1`H„βJ,B-q!M̦zlv?`K'} k,:];8({7z?8 WMx-16H®磳iٹMgϟ4,~Z]^\bKĽna>KxI蜾?0[G3]'q_eg*>tp%I6-OxM8:\qc7bO'q9p: [7`x(>Y2v +6ԧqK|2W, 1bnnNYUҰt"6x$IGG[މl>89/lL{{]`ob?ӖUv~}wlk]XCA燍M~Ag)f2bN-o}YO/?ee|D'#>w(b:ól>/O2sow?EG7甽 k6#)E׭/Hљd©b<]8eҞ7ݰ֘D&M9ȴ36ΈkSCـF%a*ݚ`nfsw| 4Jr"7ntۘl/'ϸۤM:t<ܫdc.Qs'nMKZq0 +%@W}XO{rA%qʻ4m<8jKJ, AFa_~ p+íyAu-~>9DqKikXP֓ pc>aonytd!`|v|^g*pSK썱N 5M}q aܧuvrKzj:Gc=,Zg; a6'~ o~~5#n@O6&2hSoXO'.(3'sI9k +x{tì߸ k!%f=P\vF9 [<ڣ6&4p(A楍 &`@>ݞ蓿*}:beJϐg1{Na7~|ۉylw0\Pm:%-}l܈֖Uޒv eoCb6n櫇nMwKyiuƔ8xc',O@])+>@̣lYM/fFQt@Ύ{> 4*j_wfc׳=U)P ]'"FASA, kY<;,x~˩aG]6. PxX?0 ¦ݰN x(.8 :>ifi^ 8*;;4\hF64v+F5[҉9Eq+d1`N8/}`Gf֜/p0JqVy(n}FxiibsJnl1k6jmNߛE񅾃ӽ.:˯ژ9ٜ T[7pI |~Fx$37>1Yi}v&l4w>x~aXkpV!$<]ˉ{Ԝ iíZs+^Hjw`lw^;r)ZkS㯦EY3o~ `]ݟ 5<858Fv]\2TU4Ag+Z5::p 8'i366m8S"`ҿ5n$=W&|7{̺$ )x`w6 ]BF)#~Y6qiؓtH̭;ntzR5v!iI,ߤ](s^]>!3KwSޱ/P֖]ڀ8|:߆9sXp* EI}.GT#&v G1N ΪaԦhB>$[ Xsz_?U)2~'PKvV̗3OWrO-2˖FwV>ȬXH+ۘ{<|y38xa,`rkR`xW70\O'ၫIh?'ᅳ0mxEUW*\eٰAWe8FobA>".I۷}#I͒i௫oz 47  z6炲./i?kՙEWYp'Ma,"Df5[c9Y}0/O9Dg3){k\rj!;A +0: {x;n5FX3}K:5\ZnԩCyNbaN-wzs[n*O8sϟHOx(>1;rj#qڔִrwك68UҸ5jwf}ګ]?WEMQQ[0pw_-,k`QS[RYU/焉j`4fSҀم緧s֬` +6ṤCp ^nmҥ}&&mFSy1{ +5`N_HuLР 6X )p{ w"&`mQg76 N#/"Lk@j\xd1X5_ctp+brXswVDo/x 9elw`Ypetzfx eK1j`mڇRA9r.5Cnv&W+b}y<=U<[\$-G:icAOgA~]A?nMq>y~Ճl0a6Q]tg_b pU";XqO7eE!ͅ}i9?5?q'$68SsR^QÖt!oE{' _Ojcvi# 8wʚKQ.*Kg :so0ILJ>%'Rr0o +N.ݜ$dͅ:/~f ̢^!"mۆD10.D!@vYoOx4``ϠƂ#-YTJ+e)k,fUu<H8G.&<3+xWt^P4 +Vu׋',­Lb !8qPifq8x%sKdan-sj9׏*xz5>Η>"4jy((ׅ=&xwք؋`dJQ[]ߴ.2;rַl>ft,4a] PY-md'{g'٩&1$jhAR.]J9<>4Yg gZl,!:E= ŬuB>XĄ_K5"L=;#SkYf.jydBLwps:,%\Ɵ)f >RUT~Ζ!,9LcP u:MX?|H̲ω}.fṭX`JGڮQ3s513 ؈ m9b^ȶhYss g?{ bilr 9DAJ +_䚣 yt{\o C_ +/TK!"7MJ:SR2Nِcy,B,2O$356_ %7?t¡dq6- zBʉd_w7?t٠#B ߅}_k)DYg, +``~g  7c.n!h5_q6?@9B>KL1L𙂒 "%D-.+gyvot:'\UN5B=Dʊd 'XX )'مZT ࿋_n76 ;Ax=aZynD5ޭ7@Gq!bR%^[H,r| h`d E[HUP ~u p9jgaW;>{n{P eiLqMM0ypƪ6F0~cZYV\)R҉9XZ{4 +v5 ~2:p$.jS^`TDwF?w\}b+C}*+1GrFFWՖBbW}z?@pu"%Z~-Pgo ڟ)c1ޖreSaNj<@'IAtYBI?WsXeh$!h0|/v7a|')iomJRU 7Ob/5RK|gk26۠Ql^_>n/honuc|LsVZ1K۽H/{.T @Otu[%e]E^6xa7"TE0ȻԔ~߹ Ŗܗ+͹NSx;_Hx_v1'ϰcⷫ A)FsS@;BvIſ1'gߟ@s^;ߙ ekK=]`mrz c9%;v?FGNsw{9#'O#ϵ_KK7߽^1\M0 (@YŨ8ac𭮺~?-zՉۅ׍ݎ0: 81s/2ZEBM:LJ|{/ߗsSﯵC c[]oLZblӜR3L))蹆\&a2TVG+20J2Om"78&-~}_SJ쵴;Xb[`$2 +&#O~J\]UfKvɑ=Nԗnog}6M<DŽccCװ?j 乄_K T6-s v?u5Fs{};am2A9W2%o#5VLøП:>D.>w|ىa_t+x~fA%(w_LvGzXq=DII6ƁRO}{ (iiQe2=[i1q{Tߟ;ꪳi.7tab<|86|*$%06&3F7.ho\xoEN~)*H}TJƉu89BSRJ^Rc3/힚q_SӇ=bIH ;}O;|6wLJ&n|ALM=VUĴ ]]IRm%!w/A[ܯ*nN3ȨS%=Eާrd-!{,ky-Aӷ#mWv{<Χi/9^~[k mæFsӷ$;v羻PSqAO_4M`Cw>-68͔ߟ~ш[i]^I-%ޔSnۣ[~.\eFptwsC~KLvꂆ1Re帹Rhꅺ8 DZחD1]JlN'5xc?v+y轮*ټ(Qߊ`oWۇ2R{0W}[ Ҝ<7ېnjifEx @?;r$:V>You!pFZ |mK ϩ}cwoa#E/׃fDY2v=(BmզW_ qx, f,皯NwXyIVz=^֗?d$Jj `fg+./f+CO blZ**'% wB{5{C[ +ȖvS0L\i-4W ~ }5@<22>kFtosYi<86l yV[]3J5O{"nb#곒TWjeFXb{GpRV#1)N⽥kmbOyׅ>P$$oP㤤d3`Qh tVw +vM/⎾51qZPUxO'rwmT9}3B+bXd|?Yy1_XTE}Yk-u_i V]`o֛K>aFNϯȘI^'Sܒ.)^S圈?[Fh?>NW{ru%;(y[B [ɇF| ,gA<{!rM1Sï3p >6?PUdyPb']׺J>\Iw䥽=7c @[J<lT c|uܮx1m']P߶:K?P0pv¿:z(p!gj?4ğeI0(;k߇AL ^]$NPKY${ M}n=G2| *#gZl +&1rҭ!|Ӭ{qu-LxKI~scwkFy+)K| Hߵ7k7[%_Va^k% +!,bW[s_shqwɸI[qO[1q,FW[j&$3^}aD+s./"&$LQπr$<D-mĦF=RLb.0 +<.l{ޞQLot7K "cQ](&{v EO9@[2/Tt.aq$$}0W +^m4:59w-b4y4*8VG6ȨjrwowfӤIO[M~nrsћ2^ky9ynsmQcӟ-4&;d9[z35Р?;aaWak +^.VXoZz;&* LkN6|.ӍYHWq`?xeas7@ +Mcq mx8aY-;Zmykv\j{9[4Jx0M~߇ +* ۂtq \wOn=Cgø-U #M@ 8B+!G zTLX~(fw]Aix47a t4NMGh8:.J;O[$%k/Tlo+]H^l8HXD +=&3>2R]olgK$T$ǝ+\رdto: R~N?#v_u MMΗ^DX7=m79a:I)wI&)G)iXYnK=/_?ße!l5A?}ޒ֔j rJ&5V hEO*cWܞ}?'_:.񧎍5Ock2WܤGW;KB,5COg&}$FgnCG8a+ @}>(pfYI@祦tŦWi&ty]mdQRz>b# ۮyD^;^jĸOr20.V<^(g|0هZw^mx<nCa^1rV'5zՋ6F =_l$~1E, fz+EWT|,cf<ҳ^m`VbB BEK02EN˜kEZNUeȽQl;AN;RJØA9a6"Bu{=e݉9!F`MCؐQZѹX%? WZx,?ɬG=RX)bTbS\wpN_']]ڋbf£g}6poqd ń) +\%,w6#<ȵ)9;X@\L#?[i/ru}( e9o+2^@}FJCmSs5v!fHJ᷷JCլbaBx+RԂ2^̫ah6OXks 7CWQ&>A>B~И`g'. ~:K,t19Egr™EqVi!z68ӟ\s9Q1'(q+]dSUX) q6\QLQU^oxbN6wDhx/?s1 |[ +?lCXY] !Jm0)piA B #`?s߭6d: RbjAAYA>t؉/44XVRgEO~ +f8R.rZYoKhk(_}ko)n⃕wxݟgڪBnRPS9grʏ%lC:ih(_-UUd5hVygZYZ7e-"sX)'c%5\ df P{Q\x+@P, ~(]*Ḙw+ )OTVOǓIJ!Z?]WiS3 > "L .f_mKcb: b~ 4pXpi#%aq&k%2~qj)*X']/Dif)10 67,=ŋ|2L ytdzh1qk +⡌99JYK[dV ߌ(v/ֺJP̈́T^[RyHR`.v'0:gm [ޟ(16r%(%$'Q7.^\njdΧ }K-{+ 𥆒oa6z.=imcL,2>6SeV漘Fݖ^N{`K9.XC!Ìh$f&j꒞,zD{fefeY62_nk*}_tM9/5Y/[-Sl@'Ti/x )*p]jpZm3Jcf'+H1Fjk!\tbǰ U'ݦ#lSi7H^#ͰQ_C N !GRSJ>H-𧾧SRldNT^J0HqI|Α\}ϵj81XYW~(Wp>Ut!k$-IBE wA N`aOլHC;ims6jҀq+)ba~M\knu#eU/¼sBV@o)t_jyy!٪WZn z`n1D Zf(6HV dmEΗ!ek;:PpKgI3M-Ƈt|M/>stream +DcB~+ _7;+D?X?x,j|?N +PFNX++Ybo~#j3 _џBh)IN ULߟXߗJ&2Y;2R®+F) B[iZL \zYNȇЯסaW;}qIH~+йj:Ýx33Ĕ=SjzrWJXb5[EO zs}{'=9kϮ{~ ScwdQ ( +殘W9 ^lў+}7YEs*v' {¼ns p8ܾr~x(Dz_u6mMt¢**JKƩĄc z"aD^ (NzYWU^-\L|ۢ Vc9?~KϯCК v:A*0Zat$(>]ЇǏ@ݼ;ŝ@.Π@+{s-2τF ÜvAH%S#.qv r%5PN7WP(KP7 /@o\E|j9[ ͘/w\әJ쇕Nt=׿Fu*+D^ x}9;^ (sЃ~@6#ߤlj'~ƪ;U V^pk.hĄo{$N.Dma-|$<}7ܼqs߿#$67OW߽z@E}FL˾׈A v-jجc尥beO]H] Ę++A @_##ۻ#PW\~$<ĥ>~\%wDwu󛺉]wj"Iy]>|޾~ȑ@<^!o5>Bx'Y/0}zX略9VnWa2Gx:>9usӻ@Nx=wp ̎. +Jx}Y$e%Ѻ2<(1`w7g'P;ӳg@ A9(jǗkǫ)F1jkҁյS8=l +8.8"J7P'(3_׮޼ WgЃ|qJv1nE*9 Nɢ)oOPKH_jŏ a!ҿz>Xv=_'@?>u|(?$wbJL=ocaիFl7%^q-[g WΠ@>{yCo@.Ϝ@Wk In b kظY!}{ݐ!,:!8OWb?{} rsz uwг} +䯾WQAڦԱZx5kVKtiϺ((+ljhd-l +>=~!nIߙh*ۛ$g6UmLWvP ýF"Ϛ|fyq\}HlmRF]br]Vj.,S]_"'lz "J%/5ѴC +tʽ:T q]u|gu]$M{K+u@$ Ϻz>PO@N<1D/m(c'* +\N}X)?IDlǵrf@qyz p{9ݽFx^MO +I*W:!fU%8ɃXy3|6[-2k⡆Tu3pWOP0+W[@_CsRK/mBZc;|0ʑ&Y9bk8@Pu +j LK!?:zjP ?/}'7PZhoTc'<~}̤(C䌜y!lq|tyGEk)Q.M ufef-MMχO(/PĠ@d!hrԁ}0UI1y;e9w]/3~c*(O%q&yȎx'="O7@_ߺ^IsHOx./DcH^F̷)AҠ劆Uyj!٨T԰!#|nQ_Dy¢Aщb{y +JKw3—+Sc g--ٞl`nLԓVXѳ$1!C7ц6d#E5(|yo^hWj3|잜WjPH E9ju<܈vr"k]cU&@|ww6Gh+, L6X=FUC\DUTLU{9Iފbuo3T32 T I ,km)^+JVY9D6+jlVx<'Is"m1N[Fm +w'PCUE4EwstGK5v>z7 :Z3 )}guUltsaRi.lOF}56Of1\Ć 6rG+Fr9 Tn@UglWE ]/֏䤴# Ī255v5,u~2fRRYj*ez~Cm埚ʫ=&[ViOyoUן6OmYjO_iG)a5eqOPm>;Ila>GwQtwb&+5CuY%:'kYT3 3ԏ#vj:0 'GCnIvE&9,sgGZe=miGEhM2L7UaL*|/٪-JFY+oM#ʌ8Yk]T7L1ЀPwe13ie4D]Z"aJ[AڭwO͚J~x JkCqݓ)>6SNߗPRRB>H`ucbNtյ*ªp7.=[h\,4t-W<4=kc-&lJـW~_mFv9Yjxr,+4N +ŠeacIhaj9bc+i)x fw=&m0ĩpʾXe>jJ F)xo_xTTUS[k*+弒M19zKL6$4O[MɎs,PGJ:,]- +\򏕊jlR鶅ڶņMMcL[wKQYGzi-nON52wuG.Wo_b_BK~ױV5jrZ/qH+GuJĪ2ņQ'uGΖ5$RejJmI*<l]p!iwދ:T!ms0]*x~fcB/|2J4~XrğLCQtyscN<咐w<%퍕woH7͏pwUFliCq𾢌|֢6u11b0:cꅖ\hF1?+*s-jJZĴPy4yW0aiZ֑ ?+/qݭ굯q{:Ɯ- ?V #9DÀ/sJ6ؾئV94ރkFzH9.RMǞYh]՟'~{Y^c'8^` 6n:2LWtXx\j +lʪfv>β[c z&pq:_?^//?ߪXj1iЩTQlq2Kf%;JyfwRrIN4*b 1lQr\]Q%dشBE>"bѝ4L~s1ˀfQjyھ tK7H!Atr?!t{+AELSyc-xֶ,>[9ۨh\\3kABUE/?A;qO"ŦɡƱwұvhY_5':$'&$z!+{QQ^Yyt,e#f w110~i#rf %*)+ޗ7𑛃 ~M+9gBלϗYa$HF,t,]O:UHA{bL:`uc-ej߿KNLYc#pS]MuV`Va>#!T?TVE/5jd"ń'ёY;PYpPzgc5o" ""4DIՅF?{գO#X/h]~ <],#֪[wR՝٪ht5^GxSvlj3<쏅ꚳT[UIasO)w&ʳߪSZԴ+|<ߕAsm}2jjgNS%Ӗ_ 2d6TYí~x"oU" ` ZQES8YNT$x Pc-pHG3Lwʐ':z9 +TK+馆>r~7ڹޑB,s$,6r Fe RU[SmN4es-p xg[kȟ3բiN񉔖av:.f-z :y<#pw9T>v]xx?"<(Lp&\YlC%{-rps6hTq.IWb*bUPnGD$|8D>Q3!ȍFIIfQ0wql̢$&`ε8 +5 +ylS߁ -JΩ(IV5%`7Nk;o=0c%(ƅ~9ޙraϲqY$.a} {o˨i6$e$MؓG=_ӭjFѡmU2K2Fum]j&tJt8]1ɩ)F)%y&;ǧ98ɪae@9Q@O3L2Lȥ5sתZstiýdI >2Z)<=ͬĸAd,*)9wV{?$U-6 }$hAkOBNS3%Bv]3& Z9z433LI=11TcT{]l&]ks>ss~R6{zkM’µhS+;3ZOt[Q!p + >Gx ۟zlzȾ +9VZ[Իd4|E=H% Lg~1LA?^Y9 |ty-j.HN.u)9\{u\SZV/uW\O$&o,x=^~k}c![E Ĉm",4f=e 晆9+KftuT/Y v͡SQv߱2oKXR=Sde?wWGOB>+2K19Xy7"-zz@ϳjWG}gK,GsI)d<QHBcQ >ͯ>@oO6ʟ p +Aov +_ldq5 +|o-%[faIlձ>r{ +WY@jYS@_X%"821NOՅoǺ'j`M}O^=7w?46f^LFƫcw'*O\@6ֻeԺHdMPYqK+4tjh:ץ2ַq(Z~)P. :Ӱ0VzԚEL4lc m_mOQ^]KSDowZa_EdžYt,.ˣm["ZR$vNa@o3@]^:Ykp{rde=EI`]/1ݣ{-]$!ϵHX,~O1~k + C,w, U}t.bsCP~_ͻf^#Ϗ4G;C+&8& . ey%į:2DKC.si'Z. +(# XC:T\o͹ۗwHLuXaTecs L0rcAiҡxVpzͧbԝ,.֞zoݩS< k}EnQ]<2Yv!+ޙ{B;j?{4 +)&n ae[).`S}XTCCĈDxC,XN@kAG!9dⳮr Gm[yMM5e2w<)Wy[o.P>W:Ĩ O|1r8 BrkUcP|¦>lW4>O&y)Ͽ?;8SW֒ y75yz˞zc:|X7/F N #n{Ue>!.R!j6 `Ss75t-'{BF١)սb2~g?"Bu(.A҉Km4쥦}$YPH*҃<֐kW:ӯo pˠI3UaV!8pV48`P|57kqx(,:L8S1^ >Mʸ#g~pOPاyrtƟJ 7ﶩ ,}k\w7vGɨ#qk4?@'HИz:]lA>XfY:B.P;9ה_7;E>hraȽaO^zTP+5?bWի_$dW!\R +-"L޷A-MOq!{]'xzeQfs䴪~ %#)ħk}2.[L|b(ϋߍy|*l_إ9|[Ij %l^CNLV m }5uv|IP)d5|ޡsҋmS$s虊_r1E.N"3n2nk!H8 7pk9,Y1AL>T1z2*L!Jv+6us.u(]"L* w)ޙ#dM-r S)\jg;WhrK=9/특LShLooֺ>uX)!9oSCWEzK3wxSLR4M;uR^VP/O ÓƈS %'JJ=V${R!b-z*g-whX3#]E*;s@UtUA籎EWJ''ϣsv a}/%aS ~iAh}Rl=*ug/l"(@K( %/908싁8ְ.9v,dg.P7L#3XR'%]cg$:VE|H sSCgBį#rZm=^lA[v'˂NtLG%(y *k|`M̹9b=No&`r}'㧆|Y*toMҶ|K!5/קCZ5+;ޓ}ԝ:XWnMCB˸fK?nW%jZ,'j޳gP1͑VmcvYl +0 S0"xSL)JU,Kj588&2O4 E~Yı[x g3ЈƜ +Ve5<@3K` 7t=2CMݐrEDML}g*싎;NwHuN v #jblmw,د3=,6R1J.tu:aB\Pue>IΒrnI9d +Ni0&)pmj'?3>FG/|G aj~}5s@\/; zo8\ bb4tCWR ':~W 'W R/t=3bSo@"hNl*lӕoU i[A~d $/8DmkuF/Z}ab|50I^%ȽI;PK +6awPN-A>v<쮊>O~f+ /QQTW?TR*OlWs/eZDsWXAR5\T'hʁqKFN1fw cڶʷي\u4$asBMh(3CG$is/RR:e]>0u/\!Gzegоe^E]hEtbaNWSW K5Owebem:V Xitzwmp#xy=]L.3[{` + +J%:ʧcwZK{5@n_a׉汚;[i< 9X$͠vG}/:d8ٙ!R>0w"> + !"K2꒒JsW,)bt csֆkOy2<^g@A;㠗YXu'ܚ=؞VH61T/!߷UD|OMEv] FA/$ckYWJ90cwV~]}>{WD-WjOW-[${uc'aVz +.XF_I`g˟!-~R1St%* ^f}7~3_tS-,:]Vߕ|Н|j}Ǿs!m"rљT$}10GJrWR31[mT7C}}㱉I>P =6Mֆ"c +r^ģm2Jr[NӉliw*hgNo,"GYx{R'awgIYt "*E9}-~i3jlJ%Έ7ԣ^ OqIKS_[]s]8(d-u` l82+,걁AzپL7?ujRP+VP۞z}04bu߃s2xM-J:Iaz/߃ŷJc U2xcA%l8%_< i Sk9:>I]#$,2c )?}C䑑E;5@;svfn3ȕN)w lA'Xx,1k_g=jgEvŕ{[зf +2nlA#g}oqf"--~n{.x#9xIw8m{ +B ;n̯&M:uL0'b} x}6a/se6Hͦ*Yn&|(>}uK׾}%KF"0yXMXbi.ojך+\e*zf}33 bB~Z81p_Ě#)n9 /3K,Cਫ਼>s2)iU~\4,_%!+XAeLN\bo. D(ؗNe]]IݞB:9듸4e;|Ԥ*"$)E8硩x1'6Pg|WŸ 'z.߷޷01{g=P_(w)1J9L[izULLO-Wy\:2[ +bmH=]equ51.mu%!dԥ>TGmO֤fkbwD)NQJړ :RT`Q*<6.gv穥9 _kkϝRJu +=Tiac.2Ϫ5Ha7ֆ*BAk[ʎϮyx_YTb'}?5ɘc}mSZH{fNj;cM]*X(9K)tUz6QKs L !͹𪉥D}+ ]ef8ĸ,Ev.q ; lu_L4>'DLj,0sg}MK%SWqDie6xw3"JG΀D,6W!oJ(Hl bJdO +OؓDeeCc +ccO7oT]!$_^>2W<7Uk׳H;0ia궢#\yl4J1%1DG,[)vw VcoaWJ+(G3]}[ά[d +YkYBfp}9V-Q}W<.gSC*/I=.wO̰JvTh{wƘ!=zۂOE3ħVXS)( U74z,cؓ@Rk^%lٔ#c˨GlSw6V3N5fA.9kx[ǖ-r &8gh-p/`\TKNNWG^j"_*}=MȔ5^nȿ tyfBͩS3W-G}7sM\<$=]@ +M8Q@̴#t,BJMB-|⮔X`:GF6ދMpLsjև昙7KLyk݇arឲI`KL>Ұig+\;Pc`R`JJjͭ'aɛҚ9*} OlF펳MABTŞ"|-5'e|ECUsgE8P-9^S\zbYJf>hu9p}.ws +mF1c˰IE▪NEcgٿe9 ہ'pkt Pֳfi%3һvT_3_p@bE^~ hM=yt.LEWEGjy\,]Dϼ:I}`6⠒Qg$l0@N%-pZKj +k]:a\M:/ +1G+o>mt]ny:@+8!e +/Ϻ)e_4VlTp*FݾOTjSdn_ѹ%cs;\_]yy6@F)ۛ[FnmKjM9*:N7Chro +kB׆+߬ +#GNEWIKs%>]n.x"$_}6bsGhT91!d, F[3#xb N ĺ#=@rX_ +e`EGpz7#;4O\psXx W,K0yJse|({>Z" He{rέ1N^12GTt葙 <,|#@T !$t}b ~SPt[;:/(!\*"P9АN=^)#G K1{*ߙ $۫3쩉6d=!|c>GM:RaS$ܠ3GO4%w/׏VCv)yd(l@GT$XV.Ъֺm ;)h1d!|&GNוpJq桒瓴 ]!rqXJw Y3i'*8.kU5K'⽩OHX?jEo(xc\i̻ R\X/FYTZa41Yq[zslVwFvK#E׈GܓPK>̝5Ԃ;2\,o>5eCFk^wUl??i`XV'ym꤯}xcڨ,!s7&!oG9O`)/ٴ8pUevgNTj[CZ}>%d]ʾw!ŽtW ^C϶- +.~{VQpp%lk-~jɺ %>VCkQȹ ]a> 5]rZ(@Kp{TX +*|hWX4_)}i9h68xp5g+$ET["as%E$Z7G+^|s[7˫NQKsrzw;8c{BH'coajOScOzW@}Y3:Υe|.EcСFP2G2vݑF"m#"}sl*^#ౌ{]!(ڏKt-qN%zW!18$Q\SHi+M=6N%h{ *tPAkZ_,SE@|\$D/w P@w5M'1zj?!2 n?zk# + C)\Bym+;HE:ԷԢEmӾYgapIYAy}wKJNvIeXFq;#5bVVh 5U8Qn95IsK0n n_FK9ѧj  <թ{TƆvegh}9PCE?wK=ϗqFk"Ə_ܓBh}&r_j{l[ r~g.~_ꘆEo C?j. lF?>dڍqdÐ_T@ B鬎Rycl_?)d $̫!J09`Lm[7. { +EلCd%(9adm{բ > ٞb+,rnѲty(@Y_j&~QJN{☥{+/ ʞZ 7!!{3c5v?3 gk]?51_V*DK=N,U(b3^h9mv&m"`=Ofs؄{M ?&%o%7MȲ= \ә`rAHx}Qzems/a7Im>Ӱ +\MXo{`'q}nQ`yl]7*xח+B\:f^P30:Ȣ%IЇ͉W'o;ɻ3il.cÔM| )Zk^l U+"jC~׋|)Ado KX5)KTEa n ;TH9,s\}ViC=ԵPyS!+#<b}c[1"B\Pxg}[ JzskZ^s.O-ϵ@;E-{$>X]PK?Rcnp~"<['/z{{gD|eh}^'7_J68@B͹f6e=q#R/-7g=tU9F,_ss5T>Yrk CXQR +Pɧ:D2D+ɠi.),gON;QSk|дk9YSS䕔,9o T VҪ4 4u2ԷL(7V7=ٚD眭78$Ă!d :^-}:H2'&=[kms)lܼ[gn~S- pf8ϏT$_ܖ= ;Y|}(SZsfouifce}3ǿwڣ XZʗBb*)ӯ?-w}vtk:SQ\+kYpܞ; U22t!lȠy FeDYȍ8Z׸MtOBPnQ@;ӄP9/롌t5W[l)}(ؼ#VP*bjeZ-$YghE["V^ق). "&ɿW}a +vV@.ۘc>Kx}"4x”#-锳ЖyZ|}hwfW(-~LVN@(V7 ANhuў3njT%'Ux,!dOy<9QrnqL 澊n1:H"'P=)hgR`rXDmE*p#qfd^َ`GF&{WBݑ0!L\p4ҰizEEedR-~tcg(L +}= +,7][ژPMվ^j>Y|_ג{ckODxTihx8zr_C4u6]^`F3o(a3k<2hZ_>e2DG}YastanMS4c +"$D^SlBqqF6ͫ[1r6вI|{Oet]eM;h:Ywj#->|04:|nJg +0klIrlbR82ܧv?[FXg"Lw :4 ;> ak|:eiY-@,<вهz.mE;Y&<2Rmlԋ +V */ ]U+%Ѝ1~=z6 ~u@ }JeQ߷_;R>̰I ھ7: MƬvpMWZ]"!oMҭBlV7<\;$Gz?#{ y ve<~WHHpVݝ q16>LtEVI~|浶 /e}h]UIwinR0Ȍ97PWG^}O\bc >/*#<89&+oӸg? "%PZ(dVG?j6h봿@GS{U)#(:8NŽ> >Z@|@4R2 }<zYl ZYͲ'L] hM45SyvORZmQKW3uTSs⚤yn̝ \Ցԗè˳} 2]3X'4]1R~);=g&17Iq9}nXq^-h0Gaإ4pGJ!KMQ;c9'`s*B3CBC-&B@?!w&`y|W"o{_pcRNLW#Kme?Y0Wu~_Pĭ|$'o TXm}t#A# Zb6ͩܝ ${:o;o "F)K^lSG?, 2.k)׬3VQXm}nBG: [ &"^Nlj+晄Ux]ryIP`Zx"݌#ظYZѮ_R%vX 8_smma<`u_Ӹ:9O.ۘfZ [E +<$bcRS֕K3M߇$SL2>XK콣bG) +* w{M1y#<u +XOV!?6.ōq{1Кw{3-Άl)#&1ViԽici(r>ai!-'w?s8D~kQ1q>:6).ycG@~7Dm8_mx#=1!L_y$VNN{'i]hOTkfLLqgWV4ce»/󓓓pwkq7?=r{CUa~8Mo!Tyz~oF$NvھNw*gu^>. piԄ' h̲x9m\b3% &amڴD}Cd@\rLSH=9Ylf@Տ'k"ԚCTQ0Ƞ1ps&Z 9gƧ.8;Ƶ†%G`#`n[;fL'rˉܱcsw-)@u4\nG'?9Lj9Hn^1&ӯذq't>16F6ȅG)#SpYG,PK{ W-g{:(nQ_5a3 no[JenO!5*񌡇;#.?YTX7"g̊~zc ~ԇɮӲ{.H֖5j'45o mcӼ&zEvt@}ԪE1FtҔ僞+/;h5fxJo`.::Z}0Ƣ|n~ +sSWlĂE1wAK+^zPge>ݘ*}87Pt_l8i:n+lgab7tȾSiX@m꒵>XpH;*SI>) `_LU.7iEˠfB*񎇅:^=\Ynݰhaօ4B9 =d6ڷmFXs-6BAQkVf(S) !mDN8zI-Ӂ~2SC&)krZK3dϡ{RϺeS=S6c{ al•qnR岍P26Hi!a+rlگGپ1M'~T+*0q.ii`VmmJٴ ׬u$ 5LDS+k۽ pܬ򩝕pJM' Fռ3 PX=rNM`2y=hL #kOV͌c6[!;1&̝BRWY߆[T6uiDEJ.sS"-HA5T_U|. ,8k–U lg67dUٱˆ%2ce8!d1WpYQ7M̺-;femhMJüQ=uszan˄/ 9㽨y rȅxE VÀ TG-j)Ia$pE@uHρQz/KECrxBM-2n:ݪd-Oc]'Vo{I2%&( Õl e;+_4OםƝ abBP[QVIΒZ5o5GMYʘ!׽$ȲbImXqNaSnʙAZ^6c͚ذY=NvI4_eK&b麃s[{-­3N3"OAFfɀ,Q6mBc +tފO^7ݰb'-DТuBѼQ=*;FO0 &M\556mv86yR f xAe hBR2pR]23԰`/4~L5BsלH[=c.y5;TZ[O|ȗ +|⒅ոhigڻ[C-!XRq4/)EM_{WZt  :JW)32zіOъt¦Nql"сęA`}/K^rpSI .i 06ުV]l䦛gWHVh +^Fӑ>yA 1ʃIhc n2'ƔZ IںU)[-Zy!@M);?@_ O;8/ݔmCzZŴRdBΞNRџƹE7>W<]y{*;er0ʠ}P>i0oĦi [&z\4F/qQ;λ3it$6^G$w u0M()s:rz/JI?BFRppu\a'E'h5//X_gVׇx9;7nn mTfzD' "2P!=.{ш1P~>g%l g:[S\usm`kVdؙNpx>rF5ܠhSZ2ޮ|;m!+zc]bB.jw)cUuk>@{(U^~omr,v@60420ׇy@Kx[XlDJ8o'.Y}::>N]i *A_ h(=5ԁ⳶,ܪ^wto6luoG[*c@S.UD]9ceQqͧLE +VON܆³m5 _ܽ=Bx3yfR%*Zݔo{͓h!M0W)vXPKYgLRά=c6ː jzݬA0>9ޚoe} ddN ʷcĆ!ךҐs絴M~Gj쌆;kfC"ڄ6*iծؘu/a– ){ [n( oRD{'%l;$5Xb2x_cB:bWu;ylmvoeW fƬ] 猈գ̡Q~cf-u?}Lǯ9ufb%3uYؙҋ:>acB0wݲ;Ӧ^Ё-40*6F +r; s9^Iէ]'Mr+u@ MttVZ{%ihz' ۙw. &~{B<3):=[ ZȌȘ3a +8q GOqRuؘ7 >iaƄҽ)ckݴɗ xD-ۻ6`V͠aLnnI!gDn;6-d4Y7v{F4X3k|T|wLж3Nl +]f@Q>)1uv^h]4 #3jbѦOYrs^V '`^?k( כXH^`,Ȼ!֞\˴X.+AhyBY;hK켏O4삛^g/c"Vs#zCKc}vyomH$)cdc|"^Z{sJV=@| =TL%GWbf\F;:n1c™ +k7!瞧t1ǭ2[)qNK~-4@jlK@oFv;m"ބ@e)Dgk#{?uVп;9ogH06 p%dL8 .\ٔPwlv[GSnz%xM&Q6Uǐ7ODӿce$Øн'w MjuK Y)wg[;D# yc^I-kR*pb+AyH9MhZjmZ#mƲ>=CV#@-eJZ{ʚW5?cѼ +N]r +Ɣe+Ñ&7 +LJ:| \@!ӣ"'47ZuW$e pw]WLL7OP]7ZZٕSH`3ft4(1)DSa$9W%8-WsmҶN4꼶͚7&&\j [' ,"3*^_YG9+'^z:$y05K5=E_2IXJVAHC+xϠP^-Cɍ1z ;%h-x#ISCYp u+=|qrUcIG'om nRմ0 +j7;6P Mçirs ZV!:1;{abO461u3 +IøEJu+FD(}f@3{{UvU\.X;vsf(5aS)Iya^:jYvfH~+z҈Ik- QdwuNп?iٙ7'l+iDwHtJ5^!Qp:b5~-N.DKk>nnlqb& /S>X/Qy U\)=LR/_W3'9]]G{f@+NXJѮWib0!ӄdF +i w-lcjٍ➳yҟ#@an%%R~l%ڀ _H~㘮+x.{͞FRU5Y/gZXs\u:Dkkwr"b=G +멧aj.-ԵKS|ܜ`U%|w.!R\L{>EP ,1 + W\/̣rzoĄG.!P#&̤^p,O2SlfGId@{$Z 1GEHbO9_+2??[N|n<ȸ$(N!SmzKA!y@>\AuMӜxӒy%9e=??zx%% cWԄEblO{I>kn{Ez;K 3q䟞>:KQdzǷ= +{G8>v^u؝|X>kC*YD)V:Di#l%9 CIGќR ؄),]h@,xK1ae=~$ĉ;awש.HetZz5y{_҈cw}K =:CYgi{ADBOfMM8y;]Ƽא-;vIm݈jÞ6<&[v!Q!?ѕ/ح|6<\T ~I{M.*paaǽdz*Kʼ9'>oFL"[0&)'0zm0*p8/aOxK:vǰ=Sl"TYO߃ yOLnPΉRp6k.'J:c?yvY Po[~kuwZ9q&@Ԕ9ӭf 4Bm8FgqMࢆ=.Y&5.[|Y縏?kxcU;=eFOL_!+5a֛=Sn: +q4k$b{#TG&}d,Buҫ6yu:ؒSH ٚxNi:]rNLb ((PUǮ/Lb]yRvgIRO {d3Ǥr; Ő]R0rVTg6vTo8^$­°+K?] x-;,-XZxAd IQ2k`/ةUv+*5C+fL!c5ƞ+GVKj܀L)|?sTx-' zĬ3UjWfl8Kxb{g5XjBoɸ U'6.uPF&ł[oHoK_5o0hX/J`n1/MOTYFZF) +y6 m6u|<{Vֱ^ >Y]-[4&gN(,ҐQ9obz~ŀ_W#q;BIZ,ӂZŊDDFfA;[ju {hց \drc:j]G5t M]t)Sz{e*R׀RCQku=`m夁^4g|좝t_.Z)5kC<ʈ3=zm9[sM*S MŽz%ў%Zm V{VCd0C-ѮaJq݃oUMI܋#].SG)ڻuQ(ݗMzUL,Tq_N J~>-^ehm.ZlЁִVީT *D~Gd(Wu!:oohr@Yֈ`;.1*!3*BּT7L䤑^U%C;)׋^iwCd0F +m%Fy]4!+. Sjb괞S4a6a󸵝ӵ (74b}kkC8gjBvΦKn|mG}̒)vZx?cf,zM^ݸIְ +*FMZ- +YWp +LxH nؖ6UcL=nQm5h$nm; t@3GNI]{7"ʦ(evTP^Oc[0鍪A1V^@lSI]/u^(v*]Da&zꝋ|+NST=8"bsrUyI4xf1 !IC&Am(ȧ_v8d|Ŗ} > M>|9w>l 1hsMP}V %36!r%xZi~6a ͙Ʌ+.JղSmU="ϊM[Gߵ{s6ZeP2-:IvvSeײr]XhNrqvvfP 2+5WLΑO3dœ +NSApp;U'Xkq5HЩHNP6d%fN{/yf]KJϪ_Xmߍ tV_ԱCF!|x^2@mxe?GuC]̂xA ~j'lXq:Lww.o? z89l0aK3kVڽ ~7D(*)u!#>s΄1T O 'o&UpN˙a7Ċ U#Â)aXEi9픚7;Nsb +x՟ UN\!:j_1@J|ƄW<;JGoqW7(SJ֚Gl ]Ć};;DP xkϷH(g X~db[8~6ẃۛKbR&ez4|j],EKS t(p= uj0ґwE_zT S +d62]M R {n h~j +]6?|P1g-4u=QݮUi2vgN7 jj,K|8!p ntW=ٳc > &+6n(5g}%ၮ[3»6C;k%:7|`оu/iM + TD|oh/SVyߧآ KSɼ8zF}+8Ń'6=lY쐙\6Og:ӂ16cIԡu•_Gǜ_p KVF0u2LD|raʾʿ-}ttzB)]me/ `U)E+ -=mcVtiI#&[Ƀ[z_??16S)zПueC_tۆHuU3jI}B}*{U_r^Ytأy Qafo3hdž6\rgf;lHڳ?s^|RåZXN&‚]GmYЖ;&q.[6Y!R,2з-="yGM1A?.(룖4 xI,w"3 6|&V~v g9/ @|$W@S澻~^pl*:sݨ̕,Ɩ5W&(+u!k Ʒ1dXaTA߯/SvEw/]/zCډcv]bA~zvcoJe`Po>6=e&=?03w=+`\_Ճee >}لž }xZ;O>7N<=scc51GֆΣuq3}=:E|j> P_EE C:cߍ+ڵRt wT@Wuo f(1% vA~_Q4^E3. "z%9m]K&_dV 5KAQ/;/츸ֶPn^cE_4\VC4+=Sڴ-6!FXz~ `]4a}wa*?p7hpH4Uw]^뇼ԑWIs=uOw &qM xWCOK٥^؛/NlYٳ2] _ ^=ڵR,D3^?,ã9jxh[֤3zҙv֕9P沐&2Жz}M z5Xd^-v @xyBݙ5OG`kvnLݿOѻA2X35_ˊ%(U &Di/-|/&v>zst ōt=ʜ<;-jNb":Jg) +|ӺԟlMރ醝<2*!=2餤a +(3Бf9P3y7ܨm'&{˂L4DV<&3dw5FV]p.;C*vQOwW?^b6uM1!yɽ1Q?X7qD,oxem6t5G{pܢJN/m#fzk#U/L oWO滫8%!BO㇓&bRԿYб'{L +R,/VrٶM@q~JpRr{H)6¡l[&LƮ +ħڈk7Vü ^Ab71^g+qR.MKrˣfzJLt<;ubzּ=tó>9/kAσ؇RH،GsWQO.^5WEshoɉ2 ^7AbӘ_?~^ɟ'eo@5e Lv$]rߝV #~\\5` +^aCo 틋Pcu\"~RϚյq;ּ_p_F7Xj 5uٛmSQqe1Ҿ]8z7љ}uU]^V5D8Sd<, G3mME3}gzA}2n[孛c= Ҝ7O$~(0Vt;mOZL(i:g_Q\(E횫\u #$2(귱η{6tঋ[GS_1 (拗\K7_x0E9Cؓݐ?7FR6c2O(ewUؤ9%nm *cE۷6y ñւKqkچ I깑Jcskޏ^!󫣀-  UN>1oh71q'=|Ps'(6O>U{b 3 +ޟ VDnmcsi-(JZ.vVrnz?~t|oɣ5UMus=}zAf:n(.k_$_5DÕ|v*fdw8#A$o>l+_D7{5@ +pXVt΢zі/ ,{n ֯ C;3G:7Q [o oYftltWٮIμkʣ—~a3On:-ap/U17KXML.;=UoN}>L,q|7w`WyxIKM>硥;p (e/vl8+V9z|-_u%"-}7jɌɜ4b"7ܻS]ٗCMt4' +E~>3 X6 5G(S^Ytyj{_~~G5\PTpqtg^ >oxy2)ʿ4%,[lS0ҙwSeJ 6|s[:rΟs{%5suq2$*"}{\RG2'=OCKnJ:. -316uP{Կ +nLv\YVݶ@GXcw9^ 蟞C>z}{ZJ֏L]с"CݩAч^NգVrd(75ÍONLUg5nh Ȼ)n̸Bt<}홱35oM7}3/z½kEzh׆3)g='DŽio'q+ +~@CgѶ.v )Szj_ب +zl !~t6pl7>r =ǜb~"ՄTo sjxܙQueꗫh^M#4aKC̫QnvcGIl_Y䦶ߣO Zx0 yi%몟u; Ⱦe(yqNSAȺd8,_Fq{Ƅ5CUĆ>(u\b`J3K#b*77,ye;^\ޱa;8bA}F@|rKm8P9]>'oxF&nz|ꢮ)nɘtgI } U#,nBRrs:[@q<-=-䵑L桛ִhBexgMK,3w#.tv}Jy}E1~B)Ҳ/mZHes}Ș5{|ֲ + +]@|m|pp#'sgk +RP{ݓnȻ/ƚn)AZb$rNVvgDEW&JEV˜dg!>l_c "9 ;d/l5̌$J~?ᖮ&5jp(ysK̿:і{at{Xwsvk[zEZc{sgzN8oN eoowp 5ʞ +yYh/1Ͻ"UMMԼX7³mB@z4%lHo}$_ZV'/(?ߏf)uzHܰ(BHjNF&YYt' +}bFwouսX7#V%ᓢ+?g$Wu/|k{vTף9t*z4)~{=ަ i8Ո3 2hct!2tI E{*L>PI!2&~sOL) d2t$[yyѾ;C<d3[ ^8_xs4Bl| # 0N]ɎHoI jO.ty8.9UygN7$L\!-ꠗ]s.ikvem=-o^jcPRwHI0~h}ez*"VO>kNF[[].!زeMU~߮*>LG@?/˺5y{eZz7Wjlܘt+'ܤگ>tѮ 4"ʺBHN+x$'z3˓]"=Na?{oh{]ՕUYYP  rmْ̌p0̲eYeI,CDfuo1s=Νy5/fKKs{^uOanN[|UϭO_C_Z"|B")?(ԄgHy;}W]c-C*LFHG.3O +0$y@{ZBsحC##F 7c\}ȩqjO89t,s zd5ڿĥ_YuC"K치YD ~#愔"9QrgBZtHE-OYpnCN:[{&[cW?uM2KØ+ؙZj*v r +ߡn>_=T5 2.,d0SIWO BG02 br?40',%z8_=;PP oҢ)6)JUd 9%y~w΁sڃ%} !cXۄI+PsOn< ;%*5N)(Xp^԰A_J+{[~3zRE퉵wK (ZED HU9 k5+KYxtɚ욦H+SޯI+k[ko[;7룲wG}Ș|=M( wu/Zx=*`W5V'~M'/_`~5ʸWTҳ[.f[GtUy%h`bf. ƕ~T=!}CJkY3[/K +E,-tM|ʲN 6+$Wsnmux⒮;ϖ]z1z!1,-1]j/JR *'a8SXA)`TE4"jaKv~Q~R?pQ[񕦫drC,i]Q\bіTCһcoO@=J& h/!'WVz^<(ig}ek1.uK0kܚ vwoU +zRejL9>m;uU7K}_gF9u Ko.M*lmr1uND}򣦻o~) G?9Ƨ ?agv:b^؟4~J0QkB*Br?'5?_fD{ejyШQY{R=>9 Z}zi@I+jPTj3eG]Ӵ|WG$c{9d'_ɅA-nOgQC`g=""'E䴚0 +ԇa %L<c^5*nR"& ]~~\s3R1ZC|CMgrBi$$ǧfhQE"Tst +ِ[k[HP/O΄eA9 ]=ZjGp 7\{[ARޗȅwFSF)ޚXy<夶 MGgb ec^f}?aAzg{|3Ft'i9̮]*G\B\ ^yOΟKPÄfwiE[a548 vSItP$R_㏲]߯#,T&pyKIjKmGΧMFQW_5 +uq PsA1 +IKb/'Ŕ⨶d{MLWT߅R=D]A_Њr!Tw*)tz|J_7k6 DkڶFf 5yE$9X[ȩ E `21uo}V H䊸s)̫8(&8>5_9>\'Et&f/&QmOYx ?RݰW1kSZzڞ6]YG8y͙kN#o%򴹷iV*y) sd}o +عÌAmuw 7-+VXEZ%^fyb4wJx>ϱ?|[/"C>԰)5Le¿/םAuG(wVq#QjqHK=X&.5mW@mr5-44YXKXla}\˚ .sJ9T +L2*c;+Qxs)oQ.\ ]W3q.omLgWNiFuq볆-W:iᐒ ϕ)bO{ͯ"ZruXC\釜P9iL掅N"f@oqMϫT=O]1ͪz.1f_/Sr_\L*F 7d^cVGH!2qQ+C)tֶPԱ YVޢ-1ٳ4ɶ ڰZW}K:s[;z , 9M. O%EX9ļH6!*ƫ4h{"/p9^-£-"VbȜg|@$7M;ɯT {=z65蓒v)si~FKmWƧZO|o@ "OqN j Οè {.֤akWO +js{51A(Yk'M#oM6C䌹gMkl~D]Z'^ߚB*1=Eʙ{z-*NgP'CiV_)B@ϛc*N&MԜKcPǛG}6`٧[|Ӝ6ƹS쪰rKٮ)u/q\ hȥ!N^٥¨FLp!4Ǚw~.1Nǭi #坁[9sLO'™o pJr6Qc&-aRjj"c Cnνe)ˎ*p1 =/ 1e FZ^ŷaxs"f*Q}t+cCG\#-W wTܖKX;Fm]֤ dmRl~;QgHQ3xy?5nHBz54$Դz^Ԇ䂀ұ1=cA_ :'1F-R?ThfKYЃ`E(@kib)fŇB@ ё~=) jYt渔[EX&6廯b2]4Gw +ѵ߯o:7n)|}5AL Z/l4JvjW-“f4!/M^U=aeEhZE_Qv~f}x& +Ȟ=؞ 677 2.ħi#OtM?$eo/fh%Q}j^drgIPKiko!ǝc+! 1vr\2FXyP2 tʻ34Y3=)m)" +j$`qtcqr#u|k^)=Iɦ!5$Ou6(5)>i 7:M/?o3| +VqUOݹMP*vkXl^]7] xĄO%iBQ&~Xm޵0HV68Њ!#eδ}-5kv1asКF6 ^rY +oiAs >gEsO +H]v=r5&|l&˦R~Q#o`68e\\PP:vK|O*ɅRKʮ {6FWZ;K'\:0/h 6h&O-"k{ca?2}ƻm6>F7>FV +K3άnM8nS֞)J>3ak]lYXk8WWN.{zX;^}q}Z{qP1UFK +];AUnfm4^N<(qIQm;٫. /-X"ޞխ Q^:A\XbN@5 89JMj;Cl9W ~g!gS؜ߺ܇:$e@>>]v cL|ޜU3jc6)-'~ Lrc*zM(&AzŞUt07@O!=̯$f1:.̣5]!sWGH#E \cv-C\wN"6.O%"g6@2XŐ؛ ixQ'뛓T-8u)(릓I$f< +j%f{};c\[~.a=MA̱ B^"- \͗CzJB׃lwWƋSh8!Lb@FxT\o*f509Lvn53ļ.u_[G[HC*qCXTH+ -v?/HwkҾ| 3bY+@nk(v=MgE\F=9@x7j?5V5=]n˃SҨEĉZ8!'ggYh^ ڤk7Zbos73Kv@ [z} ^$׍Wvgcv9`{Vuis:e߷_9> F]Zw)MY>.\rȸuNlmtNBtIR0"aqF 7ӉY =jj)qjkBP68&X Jk }9bVi~9S8_5 uKR[Sc&ab6ȕ~9&smqa (p:&msy%! +rIBKK}e]Ð3 ,U <3ܐ Y-x߷q 1 +Vyl^hsY(~-6;>K,,!--E1Qjf&І+!O +kmǹqc'h2VRq+C1ĕAE0Nңrj͎ՉV6fh;sm`(LI2_%@/Ok+aA5.wy'+9Fb[[,"GBƮ{n!>v@m1z9lRv]#f +ARw-bFTn(e%1|}e\<LJ|rGF_g B/I;Vi\k ϶c ;",΍quo {ƞq[WO$Q9֒IK)FL_~=:XjzzmPOiU0Ns'6|GAIXب];c#@oEE,?;rbq>9s*G1 >5skB),SuwgElu?U| x~Ns@ZÀ Iر91ã&Bм5\oNPA~9zX^@/!O)l;m'*>u5j +d} w +qO#j:. +9>'DfEa=Я1SpjNc /6ʾq DSoi:[#@dxո|,Xt~XyaCrϡLj-%S+Su?jI}dYȏUA p9#'̱kOԏ ?u_6D]@o +Ep[[=z "faoU(%[ wlLĶ>hXӄpЯ&Woȕ!# !o|=]{֧%a`3B_0;f.*a`6킱sH{Q)^TuBcu g$Yz[PЫU9bΞ iY j.[E$( JzEn)hG/$:@[J8>٫i4bBߖKeܕEOɨ+[퇝F^+)ŀ䜵A̵A4iXN1Eix00&hEKKCti녩kyPSVKJjWE/__s+q`T|?鞆_]A\ +j%S3wĴݠ ]l>99voem{ƍ<,bP[k#mN8Xd昙^wģ/vttU{hwGZ:~vMBέ5^;ք bnD$"J5&#CZ;?oKo!=RJ_#"ξ'v~^|>dFդ vk2Hr jg 풘Y@ݷQq==Uwfsbǽu5N[àٛ>.j:+ +[@<ȉ܄Ű&!lL< Kqv+10VT|&-"z$+JRsϫ:&!#:RyETgUF߲v}Tb\g4 2kcbCj>ԯWUpC&7>Qs4wOs&VsEM=NY`""kUяn~\u|MU t u-{=*Z_Aߘ 滀q:Ȁ 31|iD]hÖz,O/G;;rN ۵[ˆZ7Pki * z 5xXɨ]΅Iy==M/%܌hqsm >JXyr焤 哷L!3 J<(ⶌP ΤOE-͖ZDZ쇟ݚ ̰ gw4uW7SWA9s;&!q#|X>$Jf%'11H1!-?/v/h=ZFCdAA҈9ؚf6͝m; ]rPM\΂֬v+bfn9 z^u7-}7 +>)xu͹7U)*Q&uoUқ X{˒;Qj^֞{VT${K\oKXvWUZ!Ěo[ M 1o5*#6:ˁFVWr@oXɹBg+]ӤZa0>eyaK"Z֫  ▃: %g+XU /oO`cfZ6q@UBOFvqN{-uWᗎsA=E.-J1byBܲ9o904> .d(l%[rNWuqAWpNnYV띡VLbJ.8Su>S(4OW^{ppƹ bᒲ*C +eVF5P519Kx1#.Zo "Z\nP.pˠYibO۝R\һ2r_ƪL~WfzIhS}}]2bwQ=욖^o+r + *k㌠Qڝm!׷&iρ. p_+rW˨ڹNV"f'%; QtV舎\ҫcfvgx{ ۄ8נ_:|ʯC cZH) 0ΤM{2[x0CrBݠQέ1vpr_UgY~"<'f(eks\*Cr̽Eew ԝ׵ʿk|ry0=*lBȉ؀9Im&gy%"*ji@NyȤID #2bFbg X #٧>FBlh(׼vV,I0!=&+GMlW, kȀhEϪ?$h졑fXF녴f 5?V av/+0Yk5+­S7j +L, 8 J(a}]96ۅ5}k Du1$fиQL9ۓ_?+:vlmA= WqS^fD ZoDa|o~s d ZP1]aFn@.8&+:F]a:x AV贰N19~ &fe!vC $.qDQ vh7I[gw<</ba@lh;rI[{R",>/xKYA3d[z9 =IrcomOSzŶ{&, +4|i2{5r@4ֽ~+~PsOٜ@ ~ R WᷗF5 +6&li1BzR>m6 ?9' :7n`Keh ҂2 +1jvL\dsiٝvE jqyѯ`.WMkI Pj#zVcEXDWeOs/+/Q#x,#yqJiwqܭYvl"Vl1cE-r_Qs9x* +X`!Ihw?-WHawZ zP[#+[RF`j} 9DŽTgᠢ:1*Xm6J:1IUt}1%W578 uk0 .k" MUۯlM3V~myTq`,7wtǭb31ۇkvrtNP!:f.вSsw{-|X\3?' |:',QtТNa޶(nu! ȥ!ÊXFZ=ЪeA5zFXPl֨T1 =f}\iOyk|lߙ}1tA,D͞>}'/,`hQ#&|Zf]Ȇy*3=lS3K(p1PO,||ho-g777<,;_^}aG,|XwT帆k9crakv+'D ̺KBep* FM/y V 7ipv >ۂJ *Q-<Ԭqlcn_#ַLSu1\Ӭ7irQhTJx;>qg'j{oh9~'EJ7=2T&ϣvN|]ksR0m`r<@;Gd0FU.WɨLWb2m/J=/;jz15l3};֧QuEߛ{>ͽk:ve;G**bCB6BHnG;O>û!CQ3ӡ]SjLji5)'mEfIJEHܳ;2'f:<1"SRǬ +:B2 n2sļ ~'T[3%/wLl|\~nkW޿Uuoq7>nL|\j}33 +hE@NHwR n!t;{vr*ӝ3@˳Wz^IΦwjr" {*u=Y-9虆Yn=w?KE[ƟלV4o +^yAVXŬ]+1d +7rf cwS0m+7ȋW AuM:Xdc\BĆݽoǵ*䕐-L`ƘxG :g`:4RF{QwSٞA~T~6I(GC;{枮InKm Č'@@Ћ[M3㨵ODB[.DnO*c:&nO^Ȑj|^RJ|2#֥t7n%oMqmO`?Irs>_}U&i&UYY\gkp>9@iZTNމ:!v}ҏJhWzJU薭Ե c-p61EUĊ +peewcmy53v];n9q}z6tku=Vw橰_7xŽMsh)y8uvqYHFT t^,X_5Z?Rb06so,`G|g^ݘhCF.-aecJM؞PRnQK6FⰙem6.%'TG,0>S3j-}ޓ4 ;FfSDAߟe>.Y{\dAxrF;BJ̷ٵ 瞂{"Ko:1cdsU4g.-ٻ<*e쀉_By'!~1Pkv\>&kt/'0q#bu$s<`(7>5*YT +64(bk$<-7Iri^{9N&ay|{N)!懈XЛl  ³-~X~MY$g":B1h[%>5##ɹݰ2LGQ="ۧhK$Z@K h@{ }:ZWCȋiuqp)$juQo&\J{P}*JɾMgt7^U[~S_)eg!暢~53[F&'G;&]S;~-:ֹ8 +t_CLγ1>IR3!f# YA-zoZQrLCةZ>FSrZ&>eY.@NߖrJZ<0հ?;{:!Ѕ`! #>/fFg9[@}Xu9ԯ\f}AG'MIĵąYQ +JI@͵^Ӗƛ@?|6w1ֱw-ڈRHU&D+!V0&jsr?/rD7@~۳ٻV!=%o _|Wc=4ӎ {!NT2v=݀IU !ǯ5}X`aQ‚&,N_ȰR\'#,n2 {v.qC!5\%o5w (299mc9.ډkSBwR"|qI"AO +zyDIDktug7ܲρ4wlm9!eKFY胦ESImM¯i>@JZ:mU91ׯHi [.9KYz9;bB+bWE) hUY!`J8`ggHy1aag_ _ҡ,}XbSFoN _Pz9hPW_0B&u]eRs +`lUXuM!j%K V>WgW_Hh ~H]WFMhn)zoA%[cn\{윬;!dy}oaMYLv܀7nn9x1*L[ͽ;J3pӢT]aTFd͓v,b {/gT:% }f|Q#%u jCF+Z#2Fok,<Ƨ=9YsiE]WW9FjOU7YY1uoQE;'Nm pYIsR,Ws0!9>k] uO4̇y&j릸k&HNMۓ-ily힙 HecNn䑢n08/f{ilz $$-Brl #-6O؈l 0VԱu?ka5%& w)~V`䂠 + m$x?h;Ӛk>B]Z>`UocYҾ.G[_]ӓiHA~tv. !R9N~u?`Rf٭=ʗޢT}4I'ܯ# cʎ9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G!EÉtl>Rɉk؟T~rq1'a$޺tckq(S1ѱ*6@70(*s`IC{o7Q\}NGpc{ }k?t,< Ox)إADE) Kt1ljOΧKv^\~B|]sσ'܋^|$*sg[:ڦ蓋8> ˫h.;zo'?v׏|) u=,gR?>?o +v#o{}oɉh*nqO %9иIBh%SFsh޵QF\IH/h5Yvk*buSRJ +FeLGDQBh M-%7;Uk8>? GĂTVn'BRqE%)^)3fEJHXCR:jTmcbX/۱R"*|^LOKX؝913f`4D4ZäLG[GGr819Y.-'oӼS 2a"2Bx\-F0]#;+]5w!}oIwI'fY3C13JdYf&$bf1שCU%93Ǽ'jy-YR}쳏1SYm?-__}^ir0|J¦u(G|ƞ 9ӊؤt\WΆpɻ:cf,ļJ&\њ/4|O(> 9gޚ:\ƈydD@Ig>3 +u*٘:Ha՛qQ/:b^NY綧Ye*ܥMY{:v~n%hlwX:4(a^~5be`v^-uD$iVO0Of +䌸AtK֙63ϥgyJmA&S7CqwavYίHXUI s{ytIۯ;f:cRҪ%ͣ3jz;Tt%fʿ%-]PV5ns9I[-*QdPCUڔ`y; +]}Q^Gsw㯴d(Orlhr~{@sfwebol%Nҫħ;~>_>ĬӘyZQ2h_1w#A2<*Ԃ06 >lfEU9Q54aC'l ݂jd^@8bɭYX^NU<«>sn8iUvdl:Vœ_'Ω^Dmo_G7ŞDDGWxȠVEx$mWsO|UvO;NRG^%+uj0*y~{'&C_L9;f~*T2vwwDXΡ>zJN)89i#O` dh}Fӈ +! +rI>$>%p ysygk4 :"s>qt7S_lM7~/_4}1Y'96E?Eat4R>Kp s*tBP-½7S[I=5Ost%Eɔv ^UӔEFCڥdmNr` @S\G؈ٙb3V_~3ùvi듼;H+s+9 bzyfs۵?bؔ v{hLLmCXB 㓾!c@]:A)k\B弬2!\FeʦOQ٤Mg|W>ʼ1ӊQީWWXW_xV-pJߞ]{Cb4b]NWZ. bPٍGPZyހS^h?{U=?f7ȸt_HLLIW)ia]Nt.Ve}> +Fέ" [ʹA:`$^2a!ocu!=2_4\@qfax-'݂08:Oƭֶi̫`nI4>f cF>Vif^T]}Dz.=e7eGܞs;a)rqi>2<1eJ.g~Ne!,&Bbf(ٱKbi,hE+n#GOиc\@?S% I<=ic!n~֤+Sw>^{<ܜy#*]}}0><n^M'j&ҧS#Ks. /k6bVEKI9תޏOJ1'pMښrpS󴳸PRGie 8f 8u#󒚜_9c[څuW;M`Xz={*3+HL0&&9gaU)jh۷ >vu*q%h4wE?tؘg myY}lZTC_.'lj*>:/xݱo%PbBpa5LjTZSS +3bדNԱent? !:.m} +nD!.FbT. ⎔[Se*Iҭ*>֣N+j-$mºGyzyZD /"^Yf~A&˽ qr 1#W/a[nr$bT)tp+QG&I[nᡞ"d>'#}wS0_$f'H'PK!eV\aū/G]<֌>a'Zn*A,oc3CXf =xPn%3mUglvօW;3Ϻ!=DPG;8I+|ƻ5>=9yq@z-'dRGk~Izv|}Ug O>IN AD9x_9ڏɘI~HuC\ҶMikqj8ؑas#yѶ'z}K®d퉶}Ok>\~\l2.as.{9 3LS{:J[!)HYD@JǼ0քskꅱci'ռ]TJQqw>oڞޞ ]Ri.Pg|0q + ^z_ dN.^yů$dD'*1h؋B2S.otR#гEPˤw"HpqQ/(D8)_ ^%(E_pzqA@sVyԧSWJT|N!jI/HhH.)dJ!CC#oWi'"=׋C? +uRV ՁV6cNQ}8e+WД-YN+M}?}\NNI95*1RNi+U-GFui])ҳyA9ǚU֜U4lji +AtQdM d^DikVGڿ͚O%cXi+hdgm_{n:SW}̨;S˩IT,|j}.>S.M[vnWrSnTבL;|L;= 9|vɟCw]s!6:cK2Oi{\y x.CjWg7ǚ\~Z8ګ7هwF @j _٭eY4sJ9 '̎y8WȀF_VHp>+f_N,tg_ZrAa}6b_:%[:d`y!z xR¹tʒ+ ;\#moIkDA_>[ Kf+Dux1/ZGavt{ZesQAoSnQ= +BB6#0(0%Tc^sqcɯ̇TR2Ւ.AG8#|Tg$5zx{􃅨aY6e=M;3GQFu9jVi71}>{Θ|q{Q%kf,r>JW|R w[}u/ J=y$fԯ)7bԜl{\p:n+QݰF9ͻ=-(]k~gl)sӾd>LՐ n71>ūZKNŻpU)Lï*hcuGA:ō(\X/k?Y{Q1)3ĦyP+H3>ц|w/n!^= y O}R󼒝I4D"WfºR%ň]'_ xH|F|gh$_J#A%tZ$%$ *Y'jG6G?Ix%|?1F%e`IK~xWXPkN =D@&g0JDbq{&Q+ٗ![x%#۸_)L;xq3rc0|=}Y%m_'7T̸ $킚w) |b_KZȻ6,RO_0^#6M;gIU eƘzXw9ȥ˯>KNȀ>a?"& }p.Ov=ƹ@M-5/gMLq/''8pH7^\/MsO2[:#t;ӄ[n]*Vj~*9uz0#]m1'9+ɚeM[&A!9 }i=kbSY܎uUX&!6^t׍_v%wjE]*:jf)XF5JA-[y88<29W9C.TxTa Vkը]7~uz%[\n}xP ]}VxEvu1"> +!Z&|X-K~+_z]+Gܒvp.!$ +ڦ ||~MnR&aQ;AѠ{JSS8d"d0' 7fJ)m=UB;7w㓾[] I*n>bY!u|d u$._>*QGzr5F -,,Fy58mF.轹##7aꨢ*6ۣ 'YW+E\GR·Sޅ|<aCbװPQz[=naݚgڞ]*W3ii=jOYu洴bjie뾝q)©{&젵>:onu 2+ Wo1+qZmԺRfvbu;cM_Ps}󇍗sY 늏\cjrt99;xͼL>,;Syvi?j򖂑l:ΣyNUM_V!j(aL8Ԫ9MߴDyeMX`i‚Sۃ Ҕt +l.'w2KI w!$=C9)Y }Ta.s RxPX:QtorjdCJS K{ !"ӏR\Rܣd<7fs^K}V ڌucJp Hvj+|T!D!>ޞOc'Z\+E{ z̯W̲LOK->$}Fc&Kۀ!|rFqtXk,~{zNfEaNȅ޻{ZۂFqJdQcX <,^=dج0J洼5mrN<7Fs*aUlLXtmW ]%h*dބ(+I^IruˑFŐ9*mgal^Yq/+?OwFܢgI )3h1^(W_u|Cisqa̔2:3ǼPkw2ᔴ.|oMԚ dig0EϹnosm_'GYNhXm֯<.Yhߟc^,*ޜ}*[qe!37.j<5#L{Îo7JQ.|G/ϻ]Waƙ)%ġZ}13e&F U#@\};˄wV۟r(#F:2)F,Fi|brqF"$cⷫJϫ +9&QQ /+!J1VQ}h&2N}b[a:.0(]2 d`¦#>f˪d^N"Y>#rꑴ$J""ce\<Ӣq5QAwX=~=w[JH9t 54zzaCJY\Bf,7^L{ w/`%"sHXx`kM7L5w1TbjM}zɘYdӪaf#xO%yT”SY#iG֬W֑K|?7k’b4/.)eXPFn֡JEY{&jw7|=*lًB p,<$9*]]}0]C==Yviάb$iqw_vCz$)UoM5<*Z 3by!ܢd./kWf!ǧ:-=xiwם ya/p0X(dUXtsJs^Q[A'F$ jJڶgh'&h'0wCl>Q1r{rUJy`rFRV73K=ܚ$BWGvi+jmXl[f$̪Mż7F1=kRQ0Sԫ#`73yE?}"‚ܺd϶MZ5#W_Dh)Ay ꎌx)CjV΢Ϋl}w>+p`+zUIAixGs!pnH͒FB>;k)FwȻ6AUpG5OOwr.%M`!<=9LF%d̜OHz65 y- dd*(,M#bDϳԧ?͛3{1ӜZ>w >풘ED- ~\|(Ky[Ts=GW?(D4UԲ=þ ynI#j)Zys|q ۳rKB hhY>K%+|?O ўkivE rEщ:TLKʋ-&Uªؙ:,M+:2V s_>D1!z Ƒ[};٨OJ۵S/:U"u/, \8uV?fUP͜[F' +Je߅BH5VY12*>\r!W F gGҚ*s6eRsRI@93<4 :6:=[؈p]2`#mncW2:+1IeP'DthUGڑ0 Ghv&͔f!U7>stream +މxdҮ|D%[R~ac_ʦej涅_8m $b6B1NYA@ɂG539/ؙ)K +;G֞e >9'Xd͉9YkԴZJAq4 9JLzZ.=)->'NEU X43/[G?Y{XQulX۫P{ePfaML;sscEb{;L?똳Gshx x +gwQ[1w#RVLzu)5zdy;8}iZ5~8n}>+뎙e̳[B,+j؁\}CágUqгijCsIw)J.3b+[WHA![P@h@FG]bX܅z՟cAC>hQcTQw2v1,څڿ'g907ԫ`hC X- y.ҩS9} 0"?,rjExXCFry-fq˘h *ye꽁(y7p'2h7-ŠbVz-n[{PS/۱o5G"2fHjbQ۳d_..=]:M.nA5Ņ<^!b͇zsqQ$""Z˪.a\C_֩n qPojo2P&ey%5$n_1lN (I곔9b*l#f!VeGrBZQwgf[CMYvpf 4q.=qr&u5电gJܭR,伜 + jMFRgN}{ ރL)zt#nڄu81$x=n.h!)qk0/n߲`^FYqhdPZJQB-lAZ'gB^1w\Vگ%Zy|UY? [n}o+@=f˯9e[]G7YѰa[Pq"|} u'$*GDt4$.z`0{K}EboI!,2O@Tb~c_/ +` ~=~52\QbXnhjoq>d- k5yZ">jA/hJ9 O5ExKqu]!:Jf;\.Zwº̜= +sc/W_]8>vIXWqA)+,?#wbXyI)(]r/b-`6VYʪ`OMd-I{g^\3y*թcnb%zߑ+A$qO&TN )YYwoY'Dt$ȫ#b:k$BlX'z+ޮ uDD%C +> G X1< GeGn)tgsԾK<#J)w催]\GF!Uw;!KaxPR1XN 9]oa^ ~%<ưlAD4q!6#05PUY; +KZ7p{7`ܹ`bNPGO|J9!JĭUI[v;S܋m^lT QUvW: l0NgiG ~NTAMC4@Qsy5O<7],j+g2Z"06A}fzuze33/UJz%]i$'X%KZ#,T.aStv$PR~-pzE_Ռ[=yhsla̫n*c3:c/j ?_ҁ7d +n*j,?T$Ŗ-hYC.I8e6 FKjM#GR a, M_L(ۓ0I|d8돳#3}IFjS΋+K(ѐ77 cAU"O0&9RR\{FLSq.VI.®kSBϵjH~Emõ#3Ԛ(0Ӥ_}bHyQ!?*^Tj6owy$m=rAvf<=5Qs~JLī%*f֫`ƜGֱ푶l +valw!>9ŨRD ,2UR &`ƜCA'|LFBnԂIЛ VŐ9{rQqQ&xKݛ^R)h@- 5#VCx$7,kuꝜCנuaLL&?4ns!4Ӣ,-^9ڄ3{ފ+c%ӝrm",]4 H4߱cWǐƦ]͎T "( wCΝ9=D9N-OlMOƦQ{1|+h 3~4 ^PƩ}/QZ%eQV) jX@ŃlF'jc>)~{C'ش]E㵷J 7=t91+aV`A^H3a2|@҉uWe]D̲s"p | Ϋ9[X9$xuO4g\~?佌ߋ$=Z)1`r ҪL,Vϫ!"j.擴S zȺɂtF;KYԽ0N/CtwwU9 9;ˆ/{4b"4\4^'->ne++f +޲ָ81@ku3y)Y <*& xM[\jdHL|ΔCڒ - 黛A +[r^ƕ[znO/cpbcrAߛu۰GRj_bf>{qTX@)^a'O-ŌݐR|4|m>sg7Ru^a~a4f5l*UMSЮ +3̓k#]mZ9q(3',q/Ӭ_i|IǤSNEMSB@~xWsH33'K[4 ĥ` *34,c83Siجj}asq8keO[2P;|gu}㺏&ZPP#pP)meNuܼEPZ[Ȣgy)nLp%'R3KPC>Uʋ~]y4k^!fwR5okF\qp>d^^P{mK,쭒PϢR҅EHs+j>Ծ­~0o#R&Ug'.bD-R:! =PQ +SEԒS{h5`\Cڞ¢FW9x/ +4z>*2KJY%nJKo[mK̉^ +. L宴[LyUܴUѱ>!('cHGK[wJZ=&5ݠ% }O +0GF]*f&8ʫ2JNs(njLb}2xӵ̃)ޅ)Ԍzs}rigZij}zf=ag80. u1Kvƹgҍ#yI8$,6x]A' |I疠NnDסѮ)3$cc_B켲֘\^K]сy93xfdǁ4(n^AIΙ's򖍗-_oe<,ba_ͫsfU;nW2̜e ޏƫǒ3i2֮D 4ï٥j% &i-R%*4ʯ)B,Ҭ>Siu)iM/FFe|^R8]C֖\SkV^(hrI"H +.6i~9c^;c#YP {!IX$<,7+a|7#Ug-iUjWv!VyƑ}!uIO)W)sk w%ee_N9ܢƷa)S0:Խ0#jH%kScax7c^A#;`lV_q-g_~;D| +2YOJ?:8ai+̬!J?4"(mɐϥSZ֤u{Rt2TIs\q;[GV}uC,K;Ĭ#1=:8uhn4䝊8,rjZxTrMo:3F=G:*+#?ޏn<}tqǗˏ>_~KBn)kXy?Y|DسgԴb Lr[eБ"̀@&i)`hBAY}ΪB{<@sS#9tDB:aҢh]9[ZVCcK +ߨ +A+\tVI +}u~}ϑ{ ~OPv8Rv'U 䴨;"Ov~ ]+† d.%%퇕G[8̓U_WB퟈2~yM;濹Drv=΁+XоY;pH5l rEveRR{#{bYGL/vZSמuNBO_Y}q~GOy֞Į5+1+[{9N1֯3IƩY{Ai%Θ$19Uu񎃉C]fW~Ȯ[#jiYg/Zӎ&G׉IdU윖5vuτ +E1 lpQǗQ̌j5P_< CRÂZZݪKF';mi#\vVR|#*zqPn_oj8։SVe63pYjyjcǝ[?\|o\v_CunpO,=llyǷếصk~էá#Ӛ?h~m >]eqO5:i r8df.neߦi4g!ϧ*F̪h[{-8o4j#G?'W0[-9'*e:Ż/VW)a5ӈŠ_,>u;/?mcDvwO,ܥ\{9n(xomO/5/jߟT%&'֞wZGGoͻ ;~?yUGiSp_6mv͇[:zF?Ą5*,wwe,>ldJ[g#ȭO֞}էm_o|d3T!0׌+OhGssZAjR]x>}ίBmU,=X/uk#{-w־&u36)(߂zyC?vpviW͟ys\c~yҫf̊73ߵlxI׷G>3̸$5qiw; nzs5tlNKۚQ5ǝ*-36օIcû?% 7}tuiۯ'=}j}WOYJ^ΑIֿ%)otןTGP_&MFjkQI|\tyI?kcTjsRQcԔa/@~y9|ʌK[6&RѵQqY׷1*t_̿v[xprsn߼ +ݪ|?kIuOOӏc6^PMO4]ב ׼쌰z߃5GA~}bJ?^}w}Ew8G+UCon/=nsʻK/[aڑZ_71N427_KYWq_d CooV]71]vc)L뜮Ϗo;/' r?XƣJkINrNz/&W5{^EE7)."(((b+M]ss?n 3bi͜GQw 0]%*x/gh~lh.|~dZ~ +:RU9?o}#n7C;쫡Mqk)]3W[=-ähfKv>ڳV'T<^m#ؐ.rJ3䝮&ue{80Quӂ;S's5iO^Z%%~^)e_L_=~ +qzԭaJN旿&XTlS_N{^ֺbT3EiId4h ݉Yi,VȖ>hiR1+?[l*zяʨ>kZxwgwxQTB#Ӝ(Fkft|SqrꪋhX}Tq3zas gt"99*XWeĨj𛢸OW|_;RBe#Nt{yCJ Sͅހ|N?4r8: ~k7g$\_z8*%q Ok3^jdIZCXp~yf|Rm˵+.).`]n+9Iz{dOOqEw7;2_9q'͘ݑFV;mtU N+E 1F;*T-u iO\3>tc!oEEW;aWu/櫲6j7DO;>m\%}X[ǰ|*]J}=傘a}OR)&aLĕb7̣%̜2L@rK>e1LUIzivSc/GUI(@I)Z@\6eh/x7#J{܇~MA zޝ5H|;NςfOAH7dCyY,&-:W~?Uޕ^r0"phJvVXm<͎ a&ov!BFQ<uDCZҞD(smk`縌S6=L )WQ3RO)9񦚛qݏۗX}C֛[GCn)||Y:zOYX%m!IQs˨ɷ;G#ԗbzBwyhqA`{~Йj9e1f'1 RtWx@J +U8d'm:!Jcmez6a]r_+؉IQo/v>^hyiH2 +pSIBf2_"Bnt`"lQBolbbځn( /И|1sV8U1Nwz +?YvIWmzJnOQd-4 ?2\zW+^K UPf] js뽘 O4GRJX%C$&$kE6:HQ=pKb\ ]Uӹ|ٰi6qQwh-P7}e;-r<"(]l.2'}W?C:ٖ}Kq࡚Y3 +1|650 e=LYİ%nk ˃Eݟkpeg^@'xPgpۃnf1!6*,:~bbxv ׺sЯ +  Q q_I?P)F]NGj)mU7g[Oz;LNtHXywc ̾Q1Ta2=:PR/'<Fg+sp&l|gK>sJ1jSmv}MerJ JPI#Ġ؇]zA76HihƲSϱ5Gjy TZm;Ziq|c ۛbmPSg*~ސ1_3u"}.KKۉQe~Tܮ5dUvH!>V%ǰ 4-Jy5Qs"CEOPTROYS#8Y6t~(*{:* +<"tOJ

7ڴB #Va~N w]BLw{GT7\jǵ7K)/M)^U|PNj4ʞ['gYF;䨕^Bh+649@4Zp~m@c7!:BJqe6-d^Y11΀9H$=߮ FabQhE>lvIazE)4Pbu"軍.\y\Y$MWRUA>v`bc,oiCEmxeZ,wAuGqO&/<&DX1C=$r.߅E""].lVrƧ/4'[ >FGبcG̴Ͱ]+0~MA蝿g= l:4rܕ q6&90 :E~#M.s1OR\ƑZ\Eoyz.6΢%8ge.^7blPpe;Բ΂ILy؝ܼV/`Z6M&xHInX΢c(cif$.#Va_22p\- ]F#Œ"|]Gdݛ<nV|=9hyIj[~uPo3&8}#N|9ZH=+ckEs +fl *&0g3oQJGrN ;U"CA/6zxO &2gU ^kᥬ51w:9 ZGHŏxaAf_F_s +y8C/ -:\߄^hkԑsĭ0Z͠IZ+a9 Ͳe~SRs>=ͮ{X?YjNpKI46KZ,QNjLz?&t2YMC.߆Ϳ)O GIޠ7@/ݾr zO/]]w01,qW8t3Չ*o"~|_31;0%k(ذ 5irRcPb 5(&Xֳxsd2nB c{M I=$ī6LjHUFr%dDBo1xW~~( (S8BGyõfE"(ߕW3p},其P1 ;)y$&b/2^tmP)ׇi4ni==aVR seʚr񁶊6ȐKY/-Ko1R..x J sa SWؚoPx~l| 9H[CS}Xen=炼Я/vpEjB؃l[ 7'ЋٟÜg#!;ب#h,v{f>3U8ʼ`߯1Q~ ?R"4OWslS<}ZHۖp?;bO]{k|nga=ZMBM7^߿ sWku9\ֶ{,)zaYp()awT #} QԔ _0ӢnV_GG !(v'>Rp3^ѩoW0"e']\*{޹=sCЫGwA׮ zv&(5(7B ,FXKwhP0&hn89\Ih@_a^F\ssP;(-P7 @7\E|j9Q~&M}qvѓJԇvVd;Lz,^'\5՝o^=}u}w/ _usCƀ<_|7 "T;7nՂAZcbV;ʿ,wbƄ50 T|ܥ`PgP O{tuЕK>JA8VHG:erR>B݀҄_xt.>/?%\j֝dI!!h1E5\a *Vu VTόHP}!=8S+!(˧ ?= &@ > u]@U T-T#+ {ވG<{y<@o߃^z|N73?>zrwtϫ w@/߃7r?С>jJitkei*X*dMlfϠ.O@:ry +|+{y>p{Y5O2 *:xuQɢ ^fH-[5RǏnﯟ=?<>*֟>pݿy+(bחKG؈]95wK1 ]jH,n  p} +y2ɵk~<}}wAn/d|-fg{:C=ycq 9F+}F?\J >]pGo9' 7ח?qktY`SՈ{Ԉ.6JxRĥOoA|^{uw@o,޾z8Cgo@nO +\$5¯ޟcwgMy%8U[&Fx7|hD|b>y{ |t򟠧7nSk,]d3/tk\g)稈3Z#!1䧝D{Rz(2:*z ;tP;J|ejm&3Y /ZSyڈrűw!17陷qI ))(T30H_)FՕn!,r!"N-<V! o rB3/{? c{O@ɩmHAEޑr +r!4.D- rBD +H5)!0sX$kjQRpo<@tkOЏ.F| z+~5A|^j'ՕT8 4㹪ڽ4U 7 煙B||wE| tݽ~52"B-źxZEP5ƅ喉Q!l~Nj)4 lQɃ)Qj>[=AS.䊸jE,(:>haxEvTմf#4ğ>fl7OxU)z@ʾȸ]z'[_<@?}|A?.&$-*uj[e5 hVpF3-Zx I^Xb\Ɋ p+-"כo/e>HKp +.Gus>S r^֣Ea2)>ZWVm"!&'_|.Dxzrâ@Q .{yJ +MZRa:`Gܼ zX~5ӂ׫NH:QC)E,B1ʸPkkSՍi.jG)5 +<^]jk;X܄Nᚸ0U4؈Xmb&#_m%sWI[elcڡ56RܒWV{Xar_}TOSf%KG]t{M)N r*m}`W6V)T5i.@ +X{ӕAFB'0`{ +JRIN ldƠn`ݢV^`0£B5*iX{U}{zQrpnhO_U~o6Ây+br?EKOU7l~hQ]=;XS2M d73Ǹ):mqA^D%9+`gQ BEՑEG [bnI.gJ]!r̆Ю[}쬙qDw+D.Hƪ]59VW|;^[{s]5ϮS6&+e哽m؉Dy+;L__hǚpZ)jaԏsh4^F|7?ޮs]vMMIUI`g+Z_r@MWPRbY6a~ڔV'{l^TcUsʜjwmV\}Ѣ4A')kȠM 6z]B\`jj=!RdY7м;^w*DޜMjse-C~B%f[\ ZܦX]%d)!sW~>ClU~kuNPN tw 1˸;bnt%c_Sűy82Y'U +Nɺ!G&(i1be .wUm+Y: +PC`wkI(O%7Wj]4mNl;z6u!J҈lՠwlȡ9p|VAMQZ޹XU py?ty?-L@9E& Aq;+`Y.`^sب9j4!1O)*{]rmw*;^{oYPSMbbsg1U}(&YqijscW4 5V\'|y6* F>:IQ5gOc~-Vw59"殌h%ek9E1h"ggy4u9[l*.z#fsg#d(jq:L֮`ygZs,cEAOSxȑ:-t!vlԾ@SSw8US1JFP;y`1VtWA/+%Z`ulMM .EPkŞja}CijGZeo;>qt/@S5T53,o\EtIY{4Ȗ_h>G;ƩYƾN 6loWBx>e['WsQrvP mW4|d- +,"㱭J>Ʀ&T( +k$@$!}CF\ŇY|Byv'Y$A=yyӄ/˝s?% ٔBoGGؕ) +lg'K W=|'QK =%^ k~6sZ`wJTaQq@}6X:&'8mS#-ldX&ys\Syw:Ɵς9ё g2R|e$/6+1idžF5i*lg ld.e=l/lF +|iyJoml)*Ŧ( +03GO/?[{@OGX$S5}%.gg8"(cG)2jM4~إ%k+YRlWr6N)(%11ndo(h;kc]qbҖi\%:qAkDU5g3Tx. ʃw$Ȑ1":I;p?lJ!`n_˂;vRAu'~4tP_]!@l +DЖ}֤ai}0xa`o~N*2dya95 Hisu]g@;5Pd%AbݍβYwG44eA]EއLފe[*5Ii& 6:lm{g= ?~͉C T'C֥=%F$\z%uL-q9Ζ+8-=,rִ/Z'H:oU@N[YCk˙q7`4S40ɩ0!`#8#s|e*roMa9_K\桖\E +}g^mR^>n:bKw"3yB*;g Nmho?0ˎ]qyЮGi}[iAG &Y2rQfj! Bډ*<9M `n };/o ~s`i?E^[P7)+@fLH8ҏK>.>Wg?J<_|2X&dN#ܯ!?dI.no8AvkҒY١"biP +t0="cC`ハCܛi쵪o%NU" +"Y帘Qll6QV W tq{GS x~z7Xр9AJշ&e +vgxKqNQ w,Vbf٤@c&τV u U),#&m}5;_'95tM,=́9ְPV&$fhS}U]jT>SK49,pg,-H?l2\s;ORsfWMt@y ,~{pYW'U{¦%4 kꏾ#%$mKHF)]`x rZ<$ZԟYzĩ$YfE"5ew]|~BeVEm׫ڎɥ{x@|{YP':2NT?[eq~@.2a+Ϧ R)Cʹ)Z*fc5 PӋRbCJ?u bŤc9pazv=j~lQRl+jkQBdR8~YQQFȖWEzjuWy EE4M%oL]dS/%ftvWN7W hX3$eWyqM{ - "$- ^I{=; 5 MRM|ρh3 1O["|x/-w0 +JQ B_E^N>D w920zc+m3i/dXv0DJ@;&)2pP|J2Pbo:hK8IA]ĄZp1"x$X-Lyz3g9U @l W!oS:̓ȝ"y>g+J]b. _)p_hWϕ9[Z;]dZ'V*.Na쀎ZG`m 8|e{*|;}#7Jaߏ|pP,k'/$&'|^l{P4[Ǩ bLng}SKee# +VʈYSF>Ao@FGzh+D#dJ|ܕBn `Ն?#aa2خ`a}*o= ~:s|EcDG)>"e96\kOӑXIɸ^lqiY!ϭ^lu$ ~.d<԰}ܛf gEUrlW[hdN0rrbz7k#!CQႯZQBK_ tQe9՘D $f;hScFany_q0ϲPO*u* d@Y.dmvQTw\tTٶTĘ1oH*PQV%19+ڀl +j6N3au?&925i%~ra Agg*&d*'X}=m":M>%ou7[_*%.V0%^bϥw[/!lPE3z +.f{s%lMS}}YQS]uZ{aCn$AAEӘ4$:ˡ%V-0+rl >tMkVUnQ':!|ޑbbjM#e aLle򓅚gcإ9רQbϟ>pg4+4߫t"6{iuac$4}s>.`<]Obsssu[JNs3ߪ9bVᮘ\qMx$>\m~6KQ?Z>,wz/y4!(%Gh,t,j0JoWڳ_dm3B@lw#'Y%ԏ^ߤy<@I!H_LNӋM_ƽI1_YsRb/ ٦tSkYXT]C-89 fm @4,?+U'z.eOCݟՔ \vOi9_Љ +3O (=2*@9LjN16a4UXJrt \mǭޒ TKY1'ùrLy(c=Uc^q®W>6)&PJ6̶sftf:RRmqw!]pw'XB( !xۙ9|d͝.^hiR/sRf/2j[5_7)lWɪ)x]pW)mj;C57t|^ +}Jņkz1A)>/8UpM cW'LBH YB# j_qE;bRMR/ƭAj윛Ĭko D,p-W Ooq1V1z8\brwLWA G†m򓽷➒r N?Y6Ol_>MlSRS/t^} +8M-wk=Wj:5)!-$0Ւ.1I *h5ǀ vI!oV:nu|2p! vJ暃F-rX_-M!5 1TЯ&(Z{`9r0OF M +a잩z_LUTp=IZ/b#x9\\;t@m_ߜ< jh+U2~s®KkPZsRIc|]W4uvNZ/,D8.wGK,:rS@A,76wȥNs,thsSؤծ?CEck :5Fo!Ee4`Z5{"Mtdq;F:vF@ek|=6 +R ٝ'oOcwkl\Xx25Mʜg: Ͽi@}pQKȕp5 * WRʽŏVz-4]|cH/8Xg<>4\Elf)|WveNXrss^#:mk FAo’1ʙJb^`mʹ7pYBuW_uQw<&{slkbO/le3W8vwLj_o ^?lp~%1so7Im{v`tY5 L] WP. И"OkQYgMJ)(X== +F5oH%{:!5W޵tVܒ1rϋNb.G!Ip{lʗE@K)` [#O3U |f_5Z] 8hsDm:8zcsRkjFN&`K[[=y73=]G՝ ZѶt`S3d4*%E4kl__j=~D_|Ss`I-Wu"6OY!m5w ¼7. "X"o ʍIRkclceO7dm:vSOcҀx5*Tk*[B{u)n)"Suguc6e\r7-J40OU G}_מ)jbS5(j5kёgoHP&/sf9e<:2PzߧMVS}l辺Grh> jm |u-0!x6jWuwL R ":TB+*%܃#t)c#uy $_뒒ve,ǥ˅GIɗl(6K)̽d}`0m1v" 4p2V從ZS"ֆ;7a/õ\#[cq|4]p5 +O؛CA +FhuJ_K[Oε#t`6F!#Ew6-wuJ@?-٣܂=ĜGWZ&̢Խ^ Rl{n!qYV6xԗk]Z2ލTs3{R[k܏ro _e.Q)κ1 +ٙ'~6-$ȁ[MBpu*/80Q~:3h"x b )WB-v3mϿi)~_3C6;7) 7&q;*~W#&|wΣl,Pl4pG +?^c-KKlbP+@nYGRsl|cSt6R?4dwS&ӴYCvLaR_."l㒜SȠ +#| p:\ih#"d@ RI1\CKwLzP9.' 2c *Z..#m}9O {fȏ'ort*ąʖ@]m>׋4\ڒxl]GI. ;kr< +g)d3{RG# +00kٜlrm p }= GDj鉿!^]~=Ι,Fm#ʸG悛ܢ pEw׺!U:x@vg3աձ`\d__,y}5 ~X|^dXȰEljBQ! gsy: _Lϭe,݈h]o*fqSamXV~e/AH=;Lů ;j!٧ ﳬp雓x:AжA8fi]y@ Cb} +PowdB֞I 8ģ%a_[5DH}*Mód\_Dg,NȣU\GA8^5n{ok2^R$&~sZX]s6"(їUQsXsOSG򕕑w9d_k\}>z3ǖc wTS>0 !isWO.^ !tҟ6|ՎЊRnμp:}lk:Z7{qiֶ[<@: o @Xyҁy[¼ +.ϱR/v_/~#;޷W'4)'g Rs(Z;C ܱ9{Z/է瓶\kF[e87*?` .02Q|؜vY|s} m}9ԳbOJ+ +Y#ܳucڵyqYDu R JdҶPS[^~ d6 +ZiZmWS nD6T7J\e4 +0rpyZ%hjl7;򠶞`o(j u)ZSdgd9fHK=Լ?5]QP &)h^,#KhY%,jVU,%F^%:.4 >lܿW[{:Z {LEزg5E=-R0{ A5g0/u3,%?GNsoX{( m_Gq oM3aR̒Zﻦ i6Sk/65RJfv`i .7մJu1[TUCkj HUzo.}Z#9 sKEL@+0i~65[+x=E\,9EV5P_x tny6xW5[, ?mMcsT]%<15Xl/2F +{#%+露oꛛGIdb:3=$:~9ξOFT2 Zowdž^>I)޷t·==k!w :s NkMDɎL\j^) jg!.{ +9x +gy1T1^?CH>C,!}P@C[;oi1Gfqޢc&G@2 :aJ*;M-}\^/䌷Sԕނk ''/H(WWMS6f_L +ed֎.yKR=S%lOI,g=loP~|[wB}2Lwpu.}kѱ$6cr+%S7>MCSp)^5,ۮU ~lΫq3iz۫Wv'8Kՠ{)>>tcQ2(`9簹 ιdQ骏!o-ÈL)3R/O/Y]6~?IZ jYu J4 Ӓ2x2ҥs/ܻkABV6O7Bb3_3pv0Z'rK~F{#e=qNUJNi[O~)UuÈڴ?jwË~}b:rdZ;gCسY*Z.9BNv_՗{1J ؘ!U?tIЩ#8>D(EewۘcHU4 ylbT,$rOC(Q`sZdkw|6]C*uOelL2Z/KwWqzQ9z,~Wm |m&b}vW*D|\Dl?^8+_􊕡L9& d<4!Ժc+ Ӽڏ_\޾A$:Zj ˞=S*^ŵ!>uOX9<'S򮊃4~}1?0Gf*L;X"VI5~3 _f2rmX0!uG-iasODٷv] J.,ԉ{監k۫#Տ2XGEV31KU>cll5ᜣzU0)i[ݙVʈm_ڰ Q N3ςtܮH>2 yo+8 ;DSK/8.-T3.tGUi/zPPa +-AgLP1 2{›ut, YN8)}0McOmz!sk..(ŧ)A2mke'~!N0{Nu煀~LG<_+dI=6*j;z_ΨwVGCh-¢QqR:t,JP̈́J`٣ F!r)K_C7̫͏wrxfٞ^_j~n:1Zav>X3]5!/0r!/wA ;09Sڍ~`^:P>RҼv3(d}o䅦҇Ɔk)2igT$ew>;JZJe0sE(} whWw(MpPV>l7 W ⬫;*B=8w7X[/:{ }p~35e@d?RhYE6ywMkMԣ& /ywi/LPMs}A8ƫޙsn #҃xۍKOy{}HF,FB)*! <>qsQ{z2+@1RXלsQ5BͿ3Ow-%Q'+cNtVGqt𞂓w5K+XOV` +r PW} a8 |wxVJ}O%z BV<}^`ҺNx==piZzǖ)Q I9>l{FsM2 6qz^)l 53WH˭~xɂ_N*Xzk)]~]pksu`Yx_V쐙IX=3 EKQ,+5;rVՁ[wWa^R4>W V\tG˿GH >-am` ^%$wu%U<;cl-O?Lza- "~WC.=0uآm)~5-<46?ZAw%@|xh }7c^JM@7&p'k_$), XnGíJ+289'cgySWٵWLBc6d g- IK]Ohy~%e:5E/pH*&4$#n e +a2N <2*hw R sPPvTzzccZ#}|aPG~wX}9̀׃iF:jk .䧟8;Mcc h<\S_~\ `JG3eٜCC}H!nM%˽S8(/M[o؋θ4Ny󝚒haĻ:qgkb/  K#㽟d}qs2a.:MƆNn*ѯ`-/¾+T\5 0*3Mt A5q[{K^ja" k2socސS2~@%,A}QӒ(clqD{mI86;S25fJrEeG<ޱ5|ސM:4YmOCQ +|ņ'snhΚOC{ ("K(jɮGՊY)Z:Z΋Z!WQRm}M9',#rWC ӊ=(l;8wT?ҋ+.6C^@ L~̀hMY..}ulf⾬бGfL"Є8\ +ytkHq=UVZKə)']f1.,{.lCWl+hƎsSҳӆw}r3DK__妼*>/KXRs# 7j {$ȏht^#ȾVW0qsp 8?[8̀^~>{[: Y2deQg&NS2/~}e0:\)p6IHܭozPn Is c*7$l;Z㷄Egه+~:46Q + /;‚Y$y K}[KSJt#%WOK^|3N=}bv$>217ڹq/!w~ŋo'Fޯ V=n: +#v瑹f2HgZQ0+bH'oQ\sq+s[ڤ_xQ2j_k{ +zY-?窩1箊{uyf)V@ƪ̲`qD\> 8jcgFS*:Ť4 VHJIyYtPgm6@)6@Yo8&iynS%/jI.o"?6*o}撰K[c?&.椥궽 ~k0ˆHg-]t5A{[M8"%eL=؀51KTa R智6jWA.YD`U#FqRH&&xKF+SO9qW„ko=p iP|x}#?U}5-E4~Yr!:/ .3F@#hSSI|o߶<֖{kOKtαّw~U>:Q񛏖(D٣>eQWLshEyw>yРRҜO%tJ۟ѩX84]FTt '3 p¶ iـj<^iWs6rha1< 9,x0Sh1+ x¤_#3T 2*hk>PN-ȹៅ~㕂3rf)Rzh LRķ3{ڈ?2n~}8žm}*yP#詐'$ȥv<[́inq|n@- X5[ %W܂sBS+sRRyƒ蚽8b&3Jq%7m)ve@B8 Xiμr8 "xOJF:+uG&z;s<3u1>*~[K Yu!=R]SbRrs2Yʧs~@<`@'\nyijx9>ieEAyЍͼ C!Vxw}KIuwJCH,2[؜ s@Y "=Džy"Wޡ["B衹 [r>|}- v|sX?o.`j+M 6}S뜀vC^(~Έhɶ7ˎ=&366uּVbm(g_rJqVaO-AbrLKA)hInY0ow-C ;hK~2\z4~eVӪ؇ZZ͎&7Q6)DjAEg*^V J\qms84b 莥9}fP$̼%o62R] jz] +h)RnP'b{^[j[cq?Jigig#\*9{$O Œ K- WGq'E? >sbUu&UxWtOB}S_3gy7RGK +JY?hz"$E;^Z{Kn(*nNuS~SMȃr/,!gtrᓶ7T288fc5U'9H6}uޏ4c&٠ +Gi63\UаgdקyyfO+3ѱ_%6;(>ob 9M[\G5z(\*Tms6@OYAe]'"c\SҀ 6qqCx[ {DSZ)UAh}"L涆Uj~1\rVY[o y`-X䶒R5Ok^5">^)#䆴\WF* 8X&*떠x6סn)ؐ)jKJ-:h%Ye>\R&!g]Bt0uY:oGx#tS)>P_py6AL\D.4޳Vz۪WzsGKutlTPGZǬ#EfhgA +9\\BF0dj .9F{Q>O\nz]ŗf&؋̹!c^G}77?* >}nهS֎GڏMFK=<\jFٻU-7Ngq5kCg֮;=A^~k*5)oQt!^nHEHo/gQ7BPoo}k+0m6*ZJ1'~7zv;U:05R:;C-ܑsJ1;"3ABKpmmRы +.^k}ahg۠I1x;}j/,%{  jxxS/3vD}^"I/lk 0̵qdڎN$2j^Xc9DHpO5`Fye)A[@TR M\wLB! pqUp߸5S\ȣh`lm̶Vk +VqF&ٛO Iם5Qn).>C$--20_ +V1ixW +0S_7f& RbԊQr>N𓕞ĔS{.J̐D]ms/)aV|APz_b|Ϣ+ҘԴ˜§Lb&0itI#,"zIxtd\Som畬#5줾{@VT'Ⱦ_d'f?.Wm?r"wfqS-~P2E*uyQج˔= 6'?]'_^OI2tv|sx 9@R1m enc':scR_8b[{gNh.2%?G0,Ь~ux2p k֋Κr.hř\ӨQ +8Aaq 8iE.5#UҶR7ܓQrr7*J/]rsd頋҉s.n2=g۲sW+#~;φo\41 8Ǘ; *j lc >[z,ģPj1#e6Ø@GSc^0󌃌Y⾄\{sz#X;+-ط8^\us^|8*Tx,FC&DZBl"Sfya2i }監VMwqս5Т.)7Y,o97wbr(biQX+\$2q1Ix^yqu'<݅NvӋoů8661Rd9炒[cŃot nh,,IfVw_ ܱ?@Ω_~OkKѓIn ?L3߄,D@G!>E~Ir9{ js?mϫc+CCG rI+|!نA)cs@.~P?H~5V}6 +_D&/#L& ֱ%$} xv##[?#lc,B<#<G$o褀Pfi"Ui!^Χ4,K-ECWk^8lb ؑ96;z6amY=/dE&1ɛrb~`p̳j0o Q~g)7cka!~0|wmCش˽u{ ĵi+#u)@mu)|gO;Z5 TKM4CcM3%[rVYCX5Ǭ@ŖY5MϷt})LPy-g[MSS.*yZoVaNOfZGi{:&lJ L4)%cո|K#"[ꆼtA/R֞7C%ԬKèS^u,rzN..~.u[B +(!am]#oХ:jmWE x 5MK쳬ְ^U /9g En)!קV8Ry.Bzq4jµ}WݞʼnvIRb;Al8ė8;WgB|2lg\mk- v'8fDAqOӓ''Iz!cWm̊pkz%9|sS6FM3"ͽSQvB?3>Ɇ|}㖲A@ +jM@HڡqleiN +@&T!7G*9u BEAu.rck?ߘt~[N+PԓM|Aq1Na]j!gVxL44!GZ9JDxo+Dx1:tCyP^XoD 0u<$c!w \B@AnJY}ȍ vƌm5V>Xx +SH+ 2u[è1{`B lWI4/f_:X'mcKϚY?ȭ1WZlf6ͯEf5  6Hh˽'i\ + E~5dgbenҏAKCFv(e+JBVICɨڐ uwEksih +sIR)LOT>é37Y%iz,*o歩V5fzev<3м1LJU☥{LQ_n '{d"j0Gs/q6Msԩ1ϴ&jWZI^V_%&L2 &+1oG%z4@dgVs$o4OX$eZ'@{M"o`yhM -sxn=b&%4#xUR7S7٥!# Ӷ(^?g5o9Ȁ-`9'6J2Xi<_֜bKή<pHaҵv))q N 1D w{y3f<̜:`Iu+>[:!5S>Y)hEx|Vs<мlnt@w,EcO߬YfWGL*!l†^cY]EmCCm# 9ʑi,cJD[PPrƚ[]ՑVN92k\eomYm]eᧆYz26UZHsЄԴa6StT!lr@!\Ʈkߴ5ùnLCDsL|joe`iFG-`5Wi:(qɷoWhjښ#tvmshū#LXs*xRRaZeL5u~]j^ ذ:8@nڛm6ޢ9'f.[zҢr|G59.m}SLKˌ,vE.lHsݮe]<-=&kiׁ{~pGjkյе&┤dRGwrZڲ?7t4 Uv*rV(W4:T +0_5w,I[C=~K];I]N*F5GsMj(o6*tB.]10+2ZUkO %/ut膉ٴ7-af:hsK|#G=cwZ6,MK{kd誏S v9掎5]=C[[ެi;6٥ _8UGVdkXmԈYBWt!d̡V6܃v7J EsEǞҰs^߫kc%ߝR4:$[ujڤ fU­f @~ėYiu?G-h`D݅FdmަG;-sWF-G-~wcpoǺL1. 2iAZ'OjYP gE9%MWGX%sbz 3s0ޙT WFEc-zuwiyJ1:ncp٣V]Gஓݽ`]Z:dB7S3tb]aofʹ +'m{}'f,|9^׫A +$O>b^"dy VgkwšlF*k ZC83w<ѥstlM19#(nHbTAA^ki=KG۶n*u&n?e({o{[fdܯw B>B/VULK7pe& 4<]Q^(dHWdM7=jZ,}`3ꫳLCEVȮ]hu wnq-c^S[0.i5Hm󋶖~ְa1ʅV켶 bbosjL=ZvjT-؏B)u6m흫1!3C`ZЂ|l꜔mcׯI F،įꊖM% +x!fm\#JڅY|RZ-WU7ԫ&Gt ?]}҈]5S dfm1k6/jE.YfnƏ*U|L/Q ~(]n3){JIG[ɬ [16GjLf嶳v@Zv;hU/N-}aa/tJ[;㵛\/yf7cj2x\_3)ۣ}-t[;.:bsNz] ?WقMGs9;S6SFddϚZX{SuK&hFE]5ut6G:fM5&NU FD(N5 Hh s('{+gFEaSGY.U[ߺ`K'-j)Z,BE = X}[HGi[P} ћc!֐PO}g& U13n/iVQ}AȭqUH!όsmP\#YNLV TԻ(rEK^Ob~H+J4L?Z3/tk!Tn2C0Kb5aOQz/ 6TSc| THkhe^!?|w9#j;OqnG.=xS6waofeU[[W<ΞM1< ѳrd8LOAK/İ.=xUw~R?EW{3ul`{k;)lGT5^]~RR{'*w9 v{*/Ɠ<˗/A$#kxjB"ᱰ_2u8ڿ9@T- +UFa}B'#hd4O' W5b?@ HJ"]Jp +V-sjH."wk32#D|52CtGj9nq{IswOuE_f]2U]e7%D@j+$&"kWBNLfۏ~!p )?Eܞzue[_~ +ط,Hr<θiH +x?(6r-p52Tn/G1/}"=~_ |7wA~uW@= Zɔ1!p|̟#S~OB?D1 +Ka_$~n_滽z SF(98ж3RŜYx{ЪeRI9ziqfFvq)]?~\0";.~<*f`}-iYvcx&sXt]0wAKb?x=ҧ@30C0¥"RH3ˣ=A9ɟ%:ײ3peiv&'ʟ]zҫ'3gTρSJᔉ,~f7LȚLMDM gVx2c1\^ 3/]͐KɍDa2mJ.nB %<S@7tOSZ B~TZ˾1n{_g@1~Hm"0˅=RF%d.}p`loB&*~x1-5dԎ˞_Cc7KJE/Hx{Ig#nNIJ\1!.]zJPℼΤn{"GyczO8&Dr;{Bb?GͺU()~XUS-_L|BsǤafE;w+:xstJ$"Ot/q€z(uw(w42GyF"ͿVh_Qijp뢡mb&"}Q$)c[P*mpZbNwg=xGǩnAK -?gtHÛ%XՉӈa~Memܯs.Ե gͅUKzWW` +zt9*v|Q۫*̛ `+ۚh`N.Tfdw 8iu=c KH +S>!"2GrAۋZ>4HB" Q 3JjƼ2f +Y-B-N+~U oK.}] 꿙y +ɪfIGV: HKWM{sSEW(9g/#jKp PwS ,8ǯ 6C=vv"#ҵJ9GH~ck0$18-+}W=ͮ3$w')Yq%C=Shբ%3oh>&+ Ur1Ok_X-ST/iBR$A a-~%ȃ"D+4Ў/Ƥ-* K*B sTJ)sP&A x^<ӭͭoP(oWhl Ȋbɥn[ZGL 1g^Zש2V_CѾ/t9.wȚ؟"3~i% +v-\|hǛTQIS嶕_ƁV D8:i$ 8N|x_qpu$d|\{K^d#/}#z4RA>xJJIW|Yhh) z~|5B{t'vY˚RI/G4c/-ef?kD/ؘ%`~e3]zYQWjAy +mOX|IT/!u5"rKKW`Rds}eaGfD06mwqd'UlX 5kEYKz?wU[5?!XEvjkn<`b9h :r{;; +%H;&hNA^Pm#b"bI^gV +2j죋wh,c;x׈5?aׇK +&xZOrёr ")&DMQcΪQQC1:Z6M%eїEAq48l~u.LYaIK%A{Z̡pհ1\Y4g d"k;~ :覎U%@{f,Z0x3g봒E9:r+iaj+9785jo 3|q8B,^BV5 mF1i.f˾YR{ ҤҲjx|_۩vKKCl։\R! JxZ:/zN<x/$TwQ|u|:ع"/2ݙљ5Pu2I/6 v'*?MfX[EIn(K +|>|$U=ˬc4u3p}pkpߙ`1׌i:jRA^,ٺLzh՜MPpv؄q_WڇvD3:P1ϔ{*r榬&vWK]F/J3 +?-=ၺE)p)!Lډ{lX,7| V1 hCfI`KYqW7u xE sB&ЂN]W,?{(C,)0qc+xH)e>}%/;z|olIVm1 +z 0@"6N@#+@35mk.9*f]kO0p;u8*E/Ύ< = 4d_W؄~VDoɻV*tIJ>!;ɰ=6kK:4Tˉ<XFXK˞7bʰM-"f]SgBlqjXA9ahE:jπI>uvɫWs[ĎaKr4GYꜳ xS_kFg\\6]ѢSN^vؾ Uz`2N*J" X:n"9ouv=sM8|J,ٳPdo}0ܵ $|\ڸc** RV9Pϖ+JΧ# do~#fXY_V]S=U,wK;:hHs-;+DsíAd^߯*Nes)nNe^'Vr鉍R*/u/2^l5If\ξ GZW \rO@/>iM}>ѕtWBBoa;2D)vevu)1Ƃ,āa1w=5!kSV"ylOp6_}&.KSs !%~>P#p%&=tf&BfzގFޟ hP)'fLs1aTסc;UvRfOl59 ⏳RJ\xbD$O]E~>#W 2̉Oocu]M2bO ]m*<֡g$EIPuԁ\o,K +ܧMqA/NQҠ5%{c(Ċ9`×wB8s|M ,iOOR#J}6emUeL(^e TDh lekG^pfEH +fNG#vlGf[ˣ6Ԑk@&ٱ6hIRlSt-}JZ,єuod8|p[mC\oUqӦh]V0E&Ȍ˽vIaO> +r *z[qOIYa6dJJ> '{KZAၧF&|U >УM5[ +Dw3<$R@~83Kq9kO?WN.AfAzԀ=}GgY*"dw[o/ AS|dwٛ-)*|YT"ZUI*}g8]^RBLO__ˢ+&Z#g[U!ݾEDi_ 7/rޥ{,kkBaD #-)03O,=-:}ԵU)2mSQ2?UO7FX8),=1oJ^UDwA:wRʨ5 O'@O6."r70@ۑkb @+s ?I7%12EaG?ͦ4Q,ӢR?95q2J[ dC +3XPՁ/ AEڐ@Y\}ɏ7eGٶ.?ɑSYӔ}mu m7![)^54fwk_!-W`Dx'c>EDkpI?5d\:љ`A|×Ș]#y`ml?g (zS +Yy8[whg@<N lC 2>Ae. +r?Nsة苙'#q0Nxz`F1;ZT̶w}HK- .U&y{H3葥tU De!*ʼ=W~]VTy&n\_k_M̪}5){QPiGKQ#"^ۊwZHq.񆁑hWO|ZBV%PuEb_[*&VE1W&em.Q.&en4-">.~sIK8Q@*h=鎙~MGfk{5D}navԘ!xw'nH*u2e{S`W!֦[`6EGr?I |S>֎MEkoo ) :7#RWONNq2"Jk7x}_s*:'Cc( 㣮b<~x" +uU897PzT$߀v?UILwE32\~i¦-ϭSpOwݓ~㯫i񷖥=}uhk\(KVߐw{iGb5q"`o*neC_ger:gzzUYȮoxƦ:IF|fhhI7ifZHؚ$@ߊ컯):2Ew7v)`?'-vZN %q3#ϮW̍oND8|jYI# &̃aD̢0;䏾?|̈;0WTe 6zAᕾ75<3e313nNXa"-m w51:b]r +j3`SVۉ֬{[*hp)N?,Ԩ'Rn>!,#~k.jC>8;2 #6 +YKI1Ń~Rf>S3.*&:ݍΎ'6vA|̕Ґ# 0o%3 9{KQ#G27m5$tIr!"T-=KP؟nCRq[ZĈ&G\跅~\wS1,wm>4 wb&`_'{vV8ӟ93_- Jǧ~wmjҖ0%&rST'XfOvoHfHG\eLuyY7cmٯw]MM<j7|oqlwuls}c3ej}j%p3>eQE6-\{NUU"qSIo2=^Vc|B,Ⱦ-Qq7[z­#" +WfH(\"YwT8@C5F]5~T{"cͤ⹾s |U~a 3oL<@S}ۚ1kOFᩀXUA~T췙ց#+ +B hӔE19ぁ}QV|02Yʪ9nx% +͍P- hA +2֝u򋙕~{KQ^EMubEįkO|fd}hMlϺ; v_BmoA.A{3-9Jxhw3%.%֔yk#﹂]z]Tz Ci䤴G˃Р9e~?IP(1ῌ29kР;j| 10{9:2Nw=XV@elo.1-]nPsœW{ +/Zuw"Jʠx)E<=6bJ MePyȖ WgUIO%5]h/O9l\_i{uk$ڋ?5=u=鏧s^zh_Lgg?ő^ "s:Zn9[[hIx%"˯Q'6l6mYoMS=OB5%6etCP _{OBM xȣrKR$ޒ"B͵gl+JC Gn[E):ybfe 烼 Ǒ%!䈰a;=[xu.giLki"z0fzoJ?'YQRL7pQWWM+=-5OGOs1h[K-Q|gX7xgU :r :dmD 7{9K>,y9aT_SK#sCܐBvT ̯^g/z^鷥O  𠾿-ܧ_4B1B +qwѦ֨ksΦ۫Է;ҸƫkF\30|q4ug3MY+m)xQ藷`ϭ57C؀k\ȫ?U!AAn}W<ٞCֺ2'3rYn!D*3wȴ="0:{cPO1)4*-YǜP.2Բuee=KR , ^ANFEjR)7z][Ѡ]e75Ȕuat{ɖ̇s'2#$Sj׀N=I3jv4MIλӝIȘm=>k~m{Q{6Bm*ѯmNnчYIY(RVN|3#2#P@&5ܷ3ǣ]VPCSF3HM(t ^4櫳p@p2P*Ao6J&v__0NZul@ƝrqlbVQf{DW#-Q;;bX0r䝫ufQ[rx6֖|ik^XWTGoi+;ナ^vIPyCۗw4gW[G{γ&cWK_YUq Egz>[e%)k&m(gA.QxkC&O;jt̉X&+Yp}v+s]%!uBm0S"ls{GI8t 4<fr6 Uy9o㍬VZ$@^ߙ@> Y.fz˼֕cKsݾ[>iKX=My1і|{+ټK:v[m"PK:!b% \MEa"ȆL[`[NP_RV +gEMx6NG +M5;ʨ##,Qrd؎ qp1Kj }9!n(B7Ş;*dE&ܸ9KZ]?5S=ڝRDaҗk"e!%KL? q'ALE,BJ*gF,uW/KV5)zK)i.r&Z L2cWdeާv*l~U4co0m d}ˉ<3)( 瑁 +6Ƣ}AQxg6rgXEDq$.qYجj\|%}Fehͅw82K$-N%+^(lkhO,*:ѪGhU\➁R(`hkMS]ܟ&f"ʢ37.C-!cSTfXȥjtdD[ 6uA5eݝlgDse^&/K?StUCPuY]@kQ)gf"x_OZA } <h͵ +Zc2v%fbE!DGuE 5-x;ڒF,ZQceȈC+.w]\g_N90P +6tЦ +VMÃ}SK㙽t.|vo5g濗R2iMyMҶ ?]WE**64c{-fFJ, +*pF[+RC9`{wΓ v-ܯScE PW!tf"WBc8~d.JC/4Y%*/' Iǣd8 k&0 .eeL?,E96~SiYFm 1*)ŇFdҪ<`_O'6Bў^i,0֞p4]4_`- ZU^m-ebe?֦><4V~7H;uTd!3'̋ ɎjvK-)큥!w' ̺fȨM @ljj2O548T&_T _L侑s$U!G]C;gH6p*ĀH81dܰ:L ˃f _ț +ê(<76' u2ZK^U,f'Y` W+RCu1*+EM]Mbթ.IziHx{yf㦱9_tjgGHl_W.+=TFkQqz>ㅾ)D)?Qˏx$FN ˽/O&Z.3Ԥj1)<ĕqy~ȇRdk˥;Xy6Y_ ԊSPvGcT؞ .2iZ +rgBuAB9w4^uMyz6[v2Ũs +M`w`]kOIT&ȜyQ{~NR :qǎ+<Wt<Aـ>J9hw^W B(xw?C C|SO3؎ +[Y>Z|:W[jtFYaS75UU/|cmLga%tݙZQ`R&{K5?iS~9 cA]zݘ'샏3vˏ +IwIʍ?8^\YzKC]_[_"sCJ\jXå4\rHEhqS_4vٔ[}&>-c]ycY0p` 1So_fuhdoTԦiZwq@I$ tFښnC:BGjU(`cF??L E.IZ 7ήls@/o!rc*FkLo_ӈ֟NW|?w'ee캖@l C/[_x(uMr:O mIc۶gĻ6mL bՖwaf(hkpC>Po/SwQ 7VG%!|ݪO!Oߨ>a=ju7O5,wgv'h|Rוw'rA~)m9F'We.#ccڻ< !]ǬO{r2VgڍkGe_-?o=Ӵ:.]%85 7ί@s_ T_{rNy&!s뜥Zj;ks؆EHD"Mد "JbCXA[}qvW(ښ]uϢn&, $0nCjS5~/wfhuQ#e?qoe sJf1lYFK:deVmxTdyuqwP׊!]m^k#"zHF^v o**FG, dppyLJ6/(Maֿ)g{/њɎO"Ki7pݝN;QSe/l##f1as;a9S/rR/N̪/ncJl4 + [ߝw0>W?̛+C2̭<:3 MDy"n:+>UaE$=3  }kצ~tlxT?F>{]L?r{Zк=Ʃ_[55s3ՓUnLBV'֧[3Z^@w ._ݞw{eBgPdzs ֧Ôܿl!nwr2Koս2DYdS:FWvD#ߒOADHUk/g_vz(͛:=w;3a 3ȫ{Uzu%YUm?!ˢ +!C]iFڞuS 0aVNrL'$|rPNn^}+ҩ`{L1Uƌ,CZƻgjYHTwktBs]Л|} '"UǼ${R`gHYr'j>k_tO : A{AwwHe 0|ҽ9 +ɰ}rJ_EaBjrkcTL9X0WS4csn};x9Ѻ541#{۳11 pm +=>>ҳa3 P&*ܯ&lޤC$ؤYG^ =:&;/bAr垣U9֟zC7(߶Tfc|k{T;93~cgyxlݷ╷[]gӐ0q˲uߘwSn_+KKe6?m; ?3#moz>1(W U3R*ĪYlayKxYk~)`m$ E{V=sֿ+ iyJ㫯vxA11ߒ3K/q Y$WIÄk# tϐnG\wT時_~qtِ\QJ8~dsʡsä%!f*,ϻߚ-Ƨ,u|'l>jq\ 蝎QU͏xoa0s rwWkw3~P6~'Q+\{sS*B?gvjZɟU'o ]:ynM/W{z& e@?XR rLs r=Rp!uU1-hXrBEBKj [N9ͨڞfT4~ [eMlk=4Yά4Iĥ_ ^0(Wҿ. .3ܳB8hw]vՁ_bnLn|_v&PeqDW16'9g +td_byZv|RtW΅9ix΋)PjIu>c'ǥd%bޙW`Rz.:aBJzy9nQ-կEbU`TU1.D,sK E9fsX=Կ91ujAR[RhЋy`lk>9 i7U_Ri~hB#wG;K|])NjeׁELE\%̃\3 ̈́Uܗ^jQ5zvMb82U&Xu> dzG9Q-*,! < +ݓ}=/  .ZRKHm1;,/ӫRbm㎗jGS.8&8a~\?uu&r c "V.|ZK0fCJJ4bPCm8[/7gi [3ܞ̲߷-4Dp拢h%e_"nԯ-$4BDSl81}eeo/j+>۔<94eȲwSA5TE^gٸ>Jdv5ze/ѫ8Fl?Ao'ؑrߓnٞ.lH#:5N,r]S,:;u/C5y-=3袠۳q5Mxl~EqӚo9[]?V}fq11`䊠Z1 yu,2͐XW&h ]t!}_H!D5lsr3Ꚇm\ppKڈ޶o1Σ]CK鐘R̉j<^^ݚf4ekwC{Ao= x[ƆkCr+&rD4 cr=y1&5&mÆ^̉~5z"t,|@΄kS&fήbP9=*HQQ fXMmJ񷣪{m}␂Pd~ZW!%Yxxw>SBDEf%! +.ң{:{"o]1q) ;v=!7Y!\ߚVEVB%m|Ve GFc܀_}Rx^LL6t+SiZ//vksr5 9g}Za}C+4Ȭ\=8 +L?unSz2|r9ml-sZt,QAS99M> x}wckV>­I%踾0WN\$xMY~sQ"'#:r}@Mmؙo$~uN2+Rq{9ےbus=Kԙq"騜1 /+JR^{g +q5f`])8bvlٹ^\&fo|5MNy$]7?8}R}a!\Ɩ\{Ye#|961[@rE Ջئ Vj@;x/nd{Yi-[])3&g +!umkW +jKPMm +^9>$`jk ]%LD%',lEi1y.s-4:-p @3o'uDn)@JXg~~Rvߏc )uOU?ǯau쎘IČ]9m,:u)%[Sfʻ3 쵼ZD(y0/#*%{5"< 5o:ڿ +?P18Li]f- +Jc4=8HW)؄M$9X|u)\V,T®kk3Kk3ȫ' 5fmz6jxFUr8ib`v|TQ~D\SY|fO p5>`[4YV:lG)ژU0P)' s|ذ3O i));ڟ7fNϛLZH}5(ǖ%tHhi{?%=D7h@C۞$z^S5Yxo2\WF*huiv ӫ!3Oڙ#;py%Zدz(TysX>x}28OJ%}AᚧT:g} +į|~ t z~vve@+9>ųH,+2jgV7 +) xYs,BPAzjMj0q2zHɭg [piЃ^jkS fU<2 ov}7,?swM6;s5{ dEԚFk谄^@n#6^CTU9R_! ?zM8XbD儚؀6SkTemS6?xr  +9KpMɎSiEQ ۯ6g#gxپQ )oe'%e?35=lfb&y2 8[R14rp%mNo,R7&YW4,ɐyQ󌎍NH6V)\Q>]8Qjz6'uJYde×O$ Ժo'v&+Jɹ%j{X]{Y-)>ZW^ :FgFuݥ\@η$'Wsl.h:V&%Զ[5zܻ,h0rQ +oj^9G}mLI=e`ˀs*} ՠ}Kgw:aGO,(h"=P:< +b@g\gwP1=%mC:<ȎD\uO6e:Zqb۵$L a9 ޴::~< F.W^A4|镒kc:!)e\ fO2r7߶:LH endstream endobj 48 0 obj <>stream +1 +cԅ"w^UX&7$tT"{Xsa4R|-,اt9Lu7ybѸ;CvMnK!D4 \,3 #5 #W - a5eA"Jz{Lˀ% lLHhh>9)CCΕIuӶo 광LNhz =hObmD* QӚ *@Ы +< ctDrc>zX%&|JH xջ \вq# gz{ǃ:ܽƿ>xne1?oPO=j.2[߁fYJ}kwt(;)Jk }Kۣ//MQQU6>);RbjV4YgTH:,aՌx:JSpZDMn xa>^88԰%ru`鞦UoҊ BsYKD[ c2588)J2'0AL`!9 \_Am +0ywܸ7 t:5G+ÖMȇegM*}0~ dk@뽗|?/viGߝ2^xx1)1#56U,\L8Ln&y;䶨CyY|7Y%8Io7CϽZ6 pޓq%Ezg_)&k +к1^86 +6n,2Qa/ b=ŗԄ=1*L;w5 24IƇu} +r5my:r)퀂%d6;%3&426"D"aBd_+q&}9qn{r޻/h|!?m᣺K i2Xi3Om`KĊXVP}rF$pm՝ tqDvv^vkޙ=iYFK,<4K(xzVƿƛI#I;1ŵ-ޘj_DWy9y]W^^.dea\}۝=*9&%ֻ1%Z]JτVOϪJףjJc@-y9[rb go}JR~i#T1[Z*`%iλڝ'e̎\ kH2RΕ︜](g‚jj7ЏVsLо!7s!nߊ #ؓ01 El=9ᙃgeYzU +Y7Q'aj/cI9l_.MZ؄3Nѯ4LTBτ{PEgjlOc{wdyڇ7!#Dܮ9fSPPCsݳ=< +]4%^\:"LʐIh *{ _Y3r VgZc +zsp^?96Yܜ j5r#2͉kYi6ID-:d| ?o~zg/kL2ggb:fݤZL+kڎB {{HN[8Lx$|a»סR&J;IQpڝvvH %@\TTǁe?z`>oxV둲">j7ӎoU&_g˒~u^d5BZ G:\S緧ϺΣyOFE|gߙaTd`"Fʧ"}rZ ګ?t{hgP7<Žo:}ow0Zz{d_g` > OY0\S }8POA\_o2.q80["rrMLlO<VR.a'IYЭ Z$4=#1pa5+}?IOpq3br_] WքQsA9v-oP4pInuz5>0|Xh8. yn{L#ƴF-,se2f@ǀ.xyknE$OAnLYl}Ly$!߿@4pPq-i5xYQ]DE)F v=%.4wo|=pbgRS^4/etĔ<\LEB\5/*fexc \sBo"r|EHmXi};-4>f5:7fq63nC"*\Ӽ6#)Sgٺ:J 4.(CFհBb5սOWJRSPC܋SQ +툞1:.AAwf6au-ý>rʣpp1c+/P?>!yR!R%ƴһ} +tK{WR6doyQ7_0Q>IsݗzRkFF,tʣ%АYasWPvrWNژ X]QxY>;buX¥F +FہQsHhcz-lO^ywVSj>0f}ؤ"Srkvekv.@o,`~)2.Z!.xeIu; mi#) +,h!q^5lOuVnUql8B/c~mX̴O*q-.{5$ECFgk<.bieW_iE (bdt' H2N=t<ao@= BrvGt_ +[WԈh\d{l㄂ f_֋_a֮rRR M*{`POԚBJ#\ivǀK^L8k8n⓶qE79u6ƻNmuvMRFɝ,f2rUGRVOhs Nj=ts}t^HHU%>nSW{JǁvycGJ 1m 95rpxIgus5/\~xANYRАA& ǫ rBZ.17F{>_sq-P_70 52V.`ڬ-hn|^@e=zeJHwq3{z&ŵ9 ?vf_RP: 5!cl<ีI}߾Dt~)֞ӱ {FfOZGoOpI0A'LԶI\s؄kL\rS\1wP2ØYAg u"P2B}҈ͣ z]1J|?_>cT>Mo~0:}3[e"|Wsx\Fk׉S3Z!9cz> Ht͑cy̭^VruqfUr/8{G^iЎQcx]<Pio=}&] i^j~$]vK$нyyDWWZ m ZEߚUyg +?62Ҟ\DY Rr0WqLzv IDY$74\X,1(bQ- 50^ᳳCRB Nwq>9IGOFkhUߦX9_kl0!#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfG6;rّ#͎9lva#G9rȑfp!VwTt EW`MELv &ШXхs/;߄A-3 8 cN;6>]8s¯:ͽzҹ+9\pc.]>wܫWQ]~Rnūsr\_7"g7grEѕ_ʹsrkr8GQ?j'u01s9Y~2c ۔t%0k;u}ԝ;;ױU^=s⅜c9fGG^c)\/{=lv܏:*{XDRDmEFWAhi+-Td10{f=btqE%RVSsI׊%QҺj=- -K"KҸP#n$|BX{QHǀ_[x稕\VtZa)\圠7D U3:afrkDSSF6:icZkDA )U Tn\N`K8 +)5b$4;- Jiu QE3ɇ3~]+ 5`S:q{ +veUq0sri})"ИR !9!`ǵ砢*ZsXA +m~96f&SΙ)=q#X܌YĎ9qwU17r|JRW/ 1JY9Cy91u>aZ6֯"7UɣKNZ؄KYX_U:Bu\K`dziI37ѡQԐjZJcB ֔KXzE  2s #Uabͮs˫ta<[Jc0EO3:\w5sfwXҋN)b-{t+8mȨ1%"F^U?ll{{3*zgT]Ҡq-%r' +> (i)S_\/tlHBOn޳hăCiuQ-KxA5wBlB\=/Aٟ۾ ψqnhwx?A=gl/\<}KHnR˧F{ﻮⴞP֡ +B}W7K}{_߾M,X-6luƊ*K/Di@@,<HޡOؘÅ51_6+j߽J`.d'\K/rAf>&y>GK-!%dÇ LB8+$V>>hm8'ݓg'aU{#0bbcV+f +m +N=:j[GXؘOڅ,;VEm,bDRΌTqNZ3#&fzj9jmڢ%1+f0)_^FGj %Lu-q t Lr@OVMRGAo+i=;kZKc3JA+oLd5w}\PlmC}-ʀ-uc)VFe,OeQӒRBfJnHNF-2Q'mNmk+[jC8FD}E`wV"O8\`/A -% $,FŽ7a0;"KyIM,jIs"0qT3 (Q>c?{ү熌ȳY--;fr$Jciz >"9O&ކ}ƸSWCUĊ jx135q0^徢#/uF-B AG{瞦g')øk[ -'b`PCԴA h%+忙RY8чx:`vbAa&5*z4jjJG ri"d6]4p1 +u2L.y5vԶ,5ݜ h3&dm%drfƣ̴Y%vRrУ,"#"Ƭ5MQkuGC'ܠ% w9'A# hmR$A6lՍa1sQ+U@Mltbώ  101~|]ĉ+zՔT00[Mܜ ~D"4~sw;bdWQ&bV6ng^,Դ"Q4Ł3"q!>3d$yu"EgvDYXhd#S&Bu4+GCuMQR"n)ei+u +ɕ^=уm5=ok |!=M"cUL?o|(oUl K>2Weh K{x&wȀ&FɿlB=MN]O~QH<r$3{}xk=D~gم+=s')_/wU\( Nc\3r6U?00nD/c:!==V|20:mg{▄|R/a㑣ZTě#\ b0h_EYE_[->V aT !T3Hb`c}SMX_kDԨeoO BPvJP]" +@%Si˯ M_8kŭujx5K8,)7``1YD&]u~h!#p:nӂVGCN@f*Q𕵥7j{sa+3_+r֕3}H2A&W'P!4lS6ȅ;[a폼k7g@f!%y,Du4k똣y_"ϫOJd "jy(Vm|3#b~(#6G]kxURۺl:j,>~x5?b}2 +%I[npiyAdگ:0U2'iIðVH4(oST5ֻqQ('kjiufcw'0=FԠ&yhoe췥s1jjT'Dtu~ȶ):҂rd];ؕF NpZY\\FVMV{̽N_Ջ5x$< +;Yu=Zp77Qr.Bl؏:ҋ3Hq lf&ʯaŭ0&zzr\P)ELuO'HɫJ/'A#?fƁe6F Ƶ".d7ϠS0=jf>ri4(jjjQ{us22Oǯj> +C:1= 3lcVzqĜlSS ԴxSV ^ڝQ4G,A{C:'lapE&}^˅s}Ҙ2BO8;NI}g!&o|6& [D]EX&2[KpDt"N5)nֆ 0ٚ Ǹ],鞏-asuu"{Y0'љZیXqVqQw:FQ0nkhO؛cJ~8##nW(Q7457/F݆}Xf6u]]\6!ԠWnM+خIAi(DM:9r!]1fWS-]eyƠal" c5 #Ϻ>3K'kx{Ỷ䒐Q&ܞW-T]Z^Y=o:a@z'bd펋h٨CsoULr|GSQyB'<{ ,-==ȝ,?G +f1qf!dQ5-f?C 6!2Z~a{1xӫoi8AƋ$Sjz\_ï\_N>u4 Ր[1 +FlLS#Ɔ6ޭoH6!*zWK(a#>. +/yA %z0hPH%dY~UD\WWmX#gRJP##3WU&kT"$)m914˶մ숃Kr)aT47ri8(AUl1sq>=ؒ5!T +q\6U&ڝ9 1+Op. >]}W'cE`%Yg4\)?a^5dkż8w*ڄ,dMKIMeRLGQ&!+`2Z.HFlȭ ץ"ZH_R) [Mά>1'|_+4ʭ)fO-ZS1ږ}]'KM=pv]X'yi3y'ic؇}{"S͏jhhS1Z^Z$:yw3vl2YQS6RzzY?)Z{̥ >-y54בuh7[/+=V]K;`|~%!y+ 1-`d[_IJGIFvVH+ T9!>}94BPrPn53~=ikhq9͜X|Ҳ*KZ1ǥUT,O?ɑ>݁cDS{ Kc{x +z r4@&zqVݰ:)&.G!<:"zDpHĮIbndMr4`{sԽ;~8"FL̲wg3܆]:D5ݥƝԭd"$%%``Ł{Z10_'"mA._ge?ٙ,8WH3Z\2=+>a&Տ=)գ1;i>&`ΒOIzG*RUqj~W)T^zOb&YCDTֆ+ξ): "F@/$ U5AvU *![9谆U杠) + ɶQӳ# 'a;ſmOV\^.%!eD n@#FZ<1. 9c{#0%EA37P~V<|oWT(;PgY9G +nRB"tØD-2Y#[MHADQKh + ZXzvAWq!mVsq DψZܙw<=p4On ruuÎS^|6Qifv@% D䈕(dY:"&-oFy cuM@(x{݋E.C[S +̂ j snurAq6+j|f!kTlM3g6fm5M~c]s.M*ۅl ?oaIe!wE EC##d}cK׆Z=ڃZI|Q`@QQ!R▻&X)fVX;.iČ]׆9_^j/886`GnP#e)Fo +Yw&g7&bho.GOBjI@#uS0?;<,PF= jW6X~v=z+lQy" +Ԙg{;lUSU7|j}5~#īn5b2rm5q- |{b$āKqg3s I.eoG!{rP75AC +ZY*F#dѐ^454-Hv\R5؇}l`nA#8Cjې6mfWA1dqV ۥP"d$Z#!mNHxVgP&dU xVӰ N1ۀAtr4#+m#1x{G 記K+n(7‼>'k9U - Q&Ef%իZRjbRht-l!\Hc_ژ]р7'fEY%OΈyO+R_/Aё=hA 6皢e}Oim^CMXh1!}u(W ܡbXHN d}c̬Hf9ܢ FlZɅl&(iY-ӧW|c9\})?mA9Ua= 5dcu`Pgo7(q G4R.j$SK gn.BB&!NoLTFJ^F+(A.@ Ƈ10٧'y&*'fr>%=;Q ,?[DEaڴmk{FæƗ #5}*FAo-uQzz3#82Rw];,dglk^@K>ڡ3ʂZZ~#@^(ܘHd{m,n%<@)Qp0+轥i! 9A4l䌵뫓A14g=8ت1yw6C"kyTq;jA\kd=cgVߝsnٳ}[P+!ṇ|` f~S,9;>aTbm|XyjwXK1X:dAL/qk OZżY9x1 u8] +:51:V%rJFLwBL܅-xŞCija'TR&^-%+cpkWC͈Z@R@ɛC *kA`NHF_rNۻܝ*k}7GYDîsirA(u|p9{5ŭZf0I! G2SS1k~ح;wʏNFŮKuhԫ%{ԬNHp QQg2aŭT`-{K֕q1.kd9=2V!'nMnE0&.>ҐY;#Hk'}R?r>Mŭceas\*b['D ͯW1LQq˶UIV(muȖ~Ksg. m[+d`"zɩ6ݧc:vj'hcUBZ}QS$ژ(: Y.:ʤH]ʠ7N?f +pa3# Y@A0wO#ح? /ebTs]ㄩَEQ [yak^O| h!e^"6㠶!l(7F()3!Do1s|Ԝj +rg{=n&B-B_)5SMCx ٗވ( qtã}j aF!`b7@׼°*Fllݕ'2f|[2%;'EJZ!;9.9n3Ԗ]ƴ- SmWM\\Z/>521^AcY|E̠fvEĩ#fEȧl)EV(E96RP3;slr|^$AMuO ]9glģl)fJ- 0*)HMWSWQ?y=6*~$++lkRY& kZB֚$= +\ݛCȹ^9ӣ#x$X~Y-18v?d/& +>Mݨ}"`,ʄG}BS(@j֫fMV^ 5U"Zkۢ3uo3 p!7 aL,urq9xNx.xJrXR]U7&]HHȾh|,6S">' 3!z! k~C@Lk퍝qKMSTS1H:()d`:ZOINh~mʹ[S"l@+ a#6ݭga{Q#'?ڜCSML8x1{Z[W +"fTػ3?)16q[hb"jp>-#l/*೟ -c3 |sAӜ{@Rt2lfDyQ >#emN#kvhGBV)`">F}:NKmFY$qdߋS*GsAS~oC!.b`SK/B*ӧc<*-(x'9we Dl0*C5vRUC,BA |Woz)ǭǘYjT57CdW ,yy1LkW|(>1( E&N@:܄ӳ) +sPA 9);VB*fa8:n:v r)jӈi1S]sR+v|GKHFh1"y?aڋ66r:C:d;dj|0 qY@CoȰں]}nL0pl3'&{3/ 5YyYdϨYv@΋e^_qb!f3v-Ă56x +5R3_ݶx˃v!ks ~\ֵzXSq4v>6! &dUQ0Wm~ဵe0~ ceZ&mnzW.;$bUQ6 O Hm%[{3K?Ȫ3hX!k𣶸U@OXa 4cs*Z&6mmޔJ6Dm{5B{by!--3A=N}1[Ìԍ~M ΎMGG }jVO*ΈȺW/$zt">'zT+eDuBrXϫB޷Q2:.fs4LY?#%,4-k[E]%oHsݤ{}A|n%p$∉U~:H+hlJB1)U<8+^a}sC[LdfVHCٵ +qO-0facB:FAPC'>OqK#FjjN@KɎ)9a1+T2I&P +-S|Ք_(pWC=fjՉ12!ۋ(!n,AOytUw7U;13*\ \fDwwd=9ԤA7/o'L̊DgON6qg3 ّY>kw^ؙmz 8hzlr ccabr\ڐܕ^N%#;Ay%^jv)!4,8Gpp) zgV;jZbNoF2 lDž졵BրqTgҰa8C~VI#ǝ\sG*^W~]}X]1*X]X(3<ӔMȤ!Q+8YqpLùZbrXKɌieӘ5yK&f:0 ={mbo [c{eY‰ؤ?CA#=FHm_.lP.w.Gwc>N{kw%H{ַ{%aH(A]{{[CkIv b0>6 73o*Ov.'))A-%ceR_5ū"% Z?= -9Ww}q{w{=܍ao ^JwɕþqY$O&I!:0%DBw_WVzOϹ<R_]1@ϱ-'f(a#׃*6*npB:^e"~x曢9y:֯Z5kxA 3A~BԴ>쥕~u4#=NO8%y]x ׬W]'\O6Yu2d ٥IZF`9@7FON0=cQayP-+y!5aA  B="z1mKETaޑ=!=Kx8r-,h`mH)K#^ eƧ⨝K U^#frc[굅=m]n8}ӯ jic4G/LwYދψ 3|4a#ZJkjB0=)! Qtɛ#U7綧mMTD~1$(kꁧQ_'Z=Qujzwhy}lZ7Z=iHPz_)$A_PuapOG1L}hy:k,ٷ ZѰ>[:l{Չ7G77IqIU%i a=ZZX.i.^z{|g{zwѕǖ0gȏ4_/uIr2_7N{{@}TH#ă&&eqq}oUqZMQG{1"9g1p#UrKo5E XlAܭI^oC^ڣw+,ߔw Q{qW?Ts2Qqr-odm zkx{x;Tn.ta.r~t>+?&]_x[q|m7ݸkg3OP|s7c Y`O_BGYY{ۓ*4j ~a-``+=ثI 0Vkܛbh-G~:"5R4Cik8hy\RiO5ů$g^1SK?8^^v !vE,bG'lA}l23#2Qusd-2p}*RZH/ {nί Jnh{ Շы=򔵵b[Ù^>ǞZ^y=EK 7 2`Rn-Ü)<,uSmShVP'6{g+.l W]<3&K|smG/#4򇲓Ck.ajjO%߄nJ1wg7/meTw{ǘ9kaC0$7,)*s#0=Jm*1(9jk+gZzs/ o6`6Iqv3e%o4[" ,RP \ YC6s +u8A wQn͵,8mj)اd~|i}\~pxrmT\호KMmO(Fẫrb-mIMȒ~5dxs͹1m}E?u`MזV +0ǫ#'?Z +r5 ¯jluAWmm%$)1hqə7{ʯIŧPVʡ +k\(5 {B_K|9sju=V/p$]C㞪M-{V{1 m'G=Zy:6۞)yO7NJڞ K--Q_O[z3ms[윭!~ /ktq}YCSڼ30'n}2):NdwR:_Y[JMٵw '?o Iqz7{ 1Ksmڿ,u{, {fKH2Ͽau^,?bg1sk<+KK^ͅm2WzI7.οŜKBgr1lWK]ΗeLMߙ +~Pe|?,}0=s icwu{avUCKچooDbw_6@] G+bG#;pgW#S66׺;+sn +b|EzI''=̯OQGh\C仞oT\U񭱹p߰\RDZe Ɩ&%OP|_9nGE乷cs/@s?[r.= +wS2_S/2>v!?| s㟔?7EW +Lo֖ow3Q܃: Y=$4JL]r<Ǟt<+ܒ|;ol*:]۪[#_/u^њgu!397^b^xsts-q^}sWӲG_k~h^_W^%:Nd)i޿?&< [): 5myA:oASGY !F_'y??YEEr|bn޻Ĥ(^?>w"֒ͽd=]Y@m+?ӊ:N[F8G4U xi-F7+yxܨ?O~-wmp'V>VzVcN-tw?iH٫yvy=SֲCƢ?k w{M%?NW>U1;˓}}}{YwnMY=@ N:O-{:|YxbǃԵ7؏MR\u Ӫ#gO*~yYu)U}>ȖmGY5Uԗ`zZ~xzwuptu ̭#'Խ['%ufU! +snBRxPP~l3P?rЂ?>!cl*g?=(>J>'c仃ԯw6-JPYّ~5)k9+ҥ?i2{R}Gw E IAmm}",;4.ɤ?mY9Z[K~v:5Y_|jT^v=.UBg}~}jEpy-k;!wX{؊:PEN=qU/Ö'3q)˯n V%e(7ߡO؞04McK3,y>mm`{tǂ0/{D"\ʟGEw6ȼwG ױ収 {gg3?`ӄ mugÜ0o}6}S_ST|6b{aJ]0#y}>wb/Hk2u?7.jBťwֶ֧%f^cN[Zc.zAI;eK[>=쇟aq߶]1B7~%\z/ܔKRz|t/p:lQ#,/ I_jjn|sWS+}wk]Ǘ=ycIvu% 3o+V_\|`xR~X~OQw-9 Zr~XDLk0?- +pxqǙ{97?=tqCq n}LN@ +!)꓿}4-s6[S+%e Cez:0]ðQz{fơdmNגܚv醲} XWm(PIA4_;_]yTs05Rz`s~uűx3 QW_=G8Qv~V~(r*.q^%Qŕ/'1Hə=ki^8oIO꺼}sV}_V`.NjCo:?\l/:{.rVXӸ5^os3X@=.9<Ռ:(>+\}Uqj0ڸ8}Yյ^V Uח?g_nlj%+Y?'X7+)ҕQq!nֿl-iRdK܌?gWW`cRwEQ[[ۃԭC}Kҳʹَ3o0kʆ7[9_3R-L( +~6"H|u73s_?w_7!5M9ǫm{O;1a_{_^|2P7G^t}Β G]oTG#z1-6)IԯXKoOnLLUz9g3CJ)|>r71:QǍO~4<㕛_cm +z oPWޗ+?seOBmV̿['D9{P[aT'5>9zGv\(oiV)z7U7}vGɯttL( +u>VWwx'Ԋ=5xakloOqM1 +̥=n--Uȼ#a]%;[Wǫ0UŇ>ЮU»}#P2,"\fbnX'eԜ%ec}yuC͕O/T0|u`qRoR?۩?k_RӔhVe//wLp3Ac6qweo/5Óc3 N.uU\{[tLߔUť䯴 % )wV![lg늏sOBos6F[9˹#+*ρ>2?Gп@V(=Kd^~qCͭdWҲW0z?yUy7ů Yz=7Uiy֞)yÒ}]w&.)/QSu ;uTHڟ?k)< +οT' ǣϿ9HY2wBBX&\8귶f:3uoЖ:5X F$!  A#ù}^#kYFQ1nj oi7 ib"J!vx<]k; ڝ, +u+ʂ.O VηSK-w\,}f +/u] LcNUn#Hz?eϜu׶pV GZ^I^m/㪽+,u!Mu%o#\#睬7ݐa:C 3ͅז9e\j*ؐys>/CD<%?5bu. è]IEGb1eթw(5g>V彔R-SS!ӀQ, ‡suW5Ry=<L<伲&?rGK)2?$E^6g=oI!%N% >짿̯򟬏}X,{#m#`]}:16NZh-}Y4I %Fݖ=fsWlOwRa,P!']tL,Lrzr[NfT KA}`4t[R9&?k]5Z\|h&Xhq\|3ە!wXrKX`%g_G** ,`2r.l9}AwKO6^&I; +bae7yy&k@w>9s؈>ߏKۖ^s.K1L5 >X+xAwcs#U-AF6% BΎ3c/w>ލ +9ILA}hK+(x:Bk]t+?T8.5)ߙ17)cd+'$K6HP!게wcg's]eVxUM) +5J]BG>:x @1=߬Ø(86FL$^nY%U}>éf@ǐ]VTӐ[(聼67dWeޜm.z钓ՈSorb}[q>|sޓa|59.F(Y͐7[D +lJWIq n d"J +x[ûTlU^,x[rնcl"2ۑ! +xF_{5?9쌌胚;{r ;BKYEb;_K>. bc{Б| ^zbkTaR +tm001wҞiԡZԌDb_GȕgEBHz Tcj(8.ŎkYSnSU?rii&QW' 8;rfmOZ`oLaok/oqy =LLq9= 5 x^@~ܕ3!1zk^հN%-ߩZ1a!B3wZ',~ֆ Chk<\F[DžbdU]rc 2j0h5BGهM5J>A˸oIaU<Go;#/rmZFeÇ4_%^z +}c+dzML((buaع_SB֎`yiURa2uP˙qV?Jv9?F?ڇojb FXFZ)ӫf|ͪp6&Φ&hٚN٪-c>h 2ڭk [Fa7\[|rcqV/6gOǥ5xF~&dE}P-Com+VaUNېS3U2$?(XAtz{ً1|ܾ I9v2ψZ߄ y~=*؟nߛn*N%ԡ$|711eaE +ZՖ^j-O/,"F"Vq_]h +,5Z1ۣ읤zT*Rfعngҟ#AkZ &losmNe?2O͍Ƞ''syo8+_K6k"t['d{fd{ U{tܚ][}ܖs߮@r d맡1i?ߝh[ݛeVa=e}7b+& ?wtce! ߿ s4㠀ӿߺx9+?`sMj9y#D58vgՎ. gIvs:37vjIrc/"<}p· S@.ϴVeOVqFh(~tٕY? +zQt8\qn\9QZJ_C»:x($,c*2^{!!z"=Cʂ\3w&z>⃆ {T82*DGG E|mQSѮI:m'p2dĆ ) yR;&遄hiI6 󖄚)!Ļ,!8َ@ TAXtÛ#%bN "?%AN< xؽG҂^DĆʍ;OLtiy礐!a!Bg; +>s=pcruy9~nIJjQ]wU`t$ųρ7'љ }YAs4,,%&ؕxZPz Ko3ҢNrBE>ꔠ0-񊂓q{kSvU؍G BXIja¨HPg΃n; z|2?Oy4(=PnHБs:a==)e!Af}{W~z$oz  -(mKg@/> :u(1˟GejDAOV"ϵ1vOXrʑPPO; zw*[7@N:~)ۀk `[Q1RƉQ^57Z0k ̔_>69(5}`>.= 1sEZ#"m:D@kS#jjі'qꑥg ǡC@ÂAϠȧA/o\vt䟠sݿx +t8''j#.M5=#WFKZv1!A r2>xYP@.]8zzx2 ۷A~t9+WA t1MxɯĄ Z!xK*vjv%2JOSCJDs`NŽ|%л/wn;s6p;Ne${}}&*5Nނ@,c-0e5 $<*7/<q ?ttX @|'|D&;=Y&DΏ+)Y';qؗ{m3Q~Z]FU (sЇ@w/^])O\\:S@<U|K<K[c&{*.<Qqq"KЃkA8{u2󽇠@^+5;|fŞPaJFUM+X@~k/zaSҗFp ]s(!X˧n_߸3zp)t9Gc^>^Ype aSrGWasmV&ns[X>[~${PBP(+O'0WAo=]8}d}}wvRv[УUcݰ?Q1?Ue7kJ▶zI&@%'by + P۠Nq4p7okrV91,LG}CVa2RM +ߎV »mhҍFL) \r[~Q+n'506x'SbdhIYVs!U]L+1 P EhpRsq1pgI gi))HD<_ꆼZDX5M@wOU8/sq|: +b}yUɅ*ʞVaDsMMWW`j01jf6ׇ 955Q#^S]SW]iQWm9FSԖ+z+w&-)!*Do;T@*);,,ְ+ Ze>StTXl䖒4Y`W,@+**]f@۝"n)qjFd}}V 56CYUL鐒mĘ>|R?9fC]*X vmDPGMpxZ!.=4=n-1 v%lqJ7G8JG'b;4<[W]mS +Zᖂ]"GJpQ r!ѦOrR;Eeτgf9ECPN2zcKJ1';'yxO1U8zVtk-ݫUU-sv57,(XU6U,𮙎7 ؇f>5278:-9ߩgܳ zDzr񡙎tk@Q )}2 -;*jΜ^sa^nuoFh +vg*[[Vh.o]`m'P$1FX솾Н֥>7sKXq#|Ng<#NM|DW^搕]9lЃ>cd6G>l\lOiK{D\[ Nj<4tK{FxG@-@Bsʘv,NJ1S5XbUZᮑGpdU 3优)|r\ 8SwZsYHR3t)qIÐkwM}nUG[5uL2\ +REBm*{gLu74=!qqIJ[Kq*^ܣq7ݢ35I9?g['[ +|– $娛Mc^j555b]FHJ*>kwhZ07:gm;چfoz ޔ3ը8]-:.97V{SXql{u'TC'<خa!=JWW]deM4|oZ D|?l+XVT`+]gv4.{s#4h[Ƅ:vf6Bvwv粸VlDXzbYܰ7-5Wy``jbPAz] :B/v!Bc]#YrV&Z,oLy0tOP )R١_r+ kE_vR^lj̽JؑpGIq*&*vouz>*QaWbdAx]K؛,Snfre]ks( ^ 9ǥoJʹ&ߒG,yٵ +tUg{[ٔ4xg8HQUgMTz޾̥:~䧥@0󒹾ejEn˰6JߺQQ?e& +w LO`^XEtiY;TWkN34:|k[-NrqNiƇRm\Y@m2\SMMe ]KTdX*>ڡ:(kUY$J|9ۃf{3u: +6>NJI!+yOF^:'pdm_zC(rhqTSM]S>r;coߟfBсY.{R0Q?fZ6zs]ԋvkS1@|N~0 5!mG)e}0n-:ymuw`b`½)J>UF؟R94ڪB-7h=%wMR7 +X'ʾ횉dGhQkc䌽F΅b#աS}t@%]r_:6v `y&E?~v RZ7brOm8me)]oE+:AG\GVg}0jsF\m֍◖QLԀ0!?jށSp"S`H4o]zkm +lUKtp):|GFm2rDFnaCRg 6_j3UTPs n)tKJJ[AG#c vَ);05t͞ xERe.DZ'@,Ļ&4N,ڝ>4PJ]MYȶ eS9VvlC%yk=z& ߿KMG"#/ +[wUun)ߩA$P#xuZʄjqQL}h' nG LQa6)5Tyx=Ah9h v+EV 1qx~"j: 8uivgAܼbB,R,]gU?k]#&wu\oE(!vc;F0|vkMզ]%D_ Xf=ЈsK<<ҳbTulm1w=R{]5f_JtuU9#joy4V"-P zåR3l4Ow}@ĎsLRa@f6"sՓ:aW]쟋~eO)xۀL9T4*$1*24Į Q?'Yꕑw$N ]RrJ?k! 7) [pcye@.gZ_  /W@ޖY,~'e~;H߄-`C9Iq eOfjrooDWElRr^Dc^jWr;Zܫȥ'ViYRQv1wtD;h`5ch0݂M:$d.5.í%m˱1Ƃ{ØD}+$aGM+JP߶! $e{__sO+l~x b5)gU1k9dRdWjjsQ!gBwD]smʦ &[e+gc `[G+w(}x5VS-rL]ME5믙 j9e̔4&/9Z*~I6=AʲbJJ87o6yOC-pihRBKJ:@0ߝ#=9h乥$hJ{QÏ>=]j}6Rf%-2d{rKϖW>ZA|bծyFQQ٦wƲ@vλ+Nlcߘ䤝װR/DetJ1-#>n׎rR>^QpV .Z/ i[aCoZqʃ%o@xD8e rGu뙕'^96.>c`tSAAm"?y-}Fk~mC+/%A9/VzS^-iww)1D,tӑƎҠAT¢.öǰѶ/aCsm#4XY;-^>?Wr= Gn<~ҥ×JZ*— u?q}g{TsLӍAM{>B{̵ +;:&`ZT?S۽>[#u"k~M(C>ԗ}tc׺KlKQ.W|cSi4`׈J55 H$6__m[RtB9̊]qXYo`ZT}Rsw<0*ңc`ZKU= ]NڒbkpR0hn#'uɺZ>Ւ2rTrWs0JWٛXVfM,j|ZOMsx wk]>ޡbC?m W!7A0(so1[^6ݝbf-tb4~c,}k]/͍ywTW$̤3/-wl1~X ,ܭ$gȩKeAuTrIAi ;bc,n-%A76o#8$b)nލ`߯k|AMW¼[2r~y}HIȏ)~h첒8r;tKdbgm!sc_c ->`V:ңOW9&vNQqϽzxml2Sgkoqؔ0 +{CqMR= +d/3v`b}S|Ɓ9 ௞3R|cr];{*tCƁ,`·Бq\MM}_A|~Vզdx2p<Ʈ?0Z6bg=VyĢ 9ܮkph+ z[NH^Zj}wȈ7zqԵ;?fh 6C5D|݀ot(`1ÈooZ~?z)&I +h@,CNqsSYq|e\e pbݐ@Fm)xP7+jpkx8>:f{e7S=8:O0W,d?((_$ͺyf`+k~ܐ#19-JoKIy\Q8Wp &lyWAo b7ޞκa˿?%μa Xj+|A\BA"|1: _ȹ?zrG/L 92ZOMfrp{a) 򠅎ڒGaVOS_gq1߁~Yъ&lD#\3o7nt!F9n%%GEUk}Y͢s3U -,wam_MaH,.yP~usJMk1; sLX+|[.~i]h.}z7EEy̍ ô-.YQO49j+u"@O9Rܚ`dtQQ(L>Ɋ;7tb#ږ,?aṿL!ׇ\ށx75'\K]#clJ:x]JLpM2a>#䯹 {4!Uy~ݶ\m@_[嘔.`EtYpы*WY}p1fu^T'̿=B[QSgok!(x7#M<Ζ ]$بi*kF>o:fIH37QNN.ȅĘ?6N5?kYd!-4=Ym/~¦j3hJ0VVY3-y;ڊK7ޒ㷆T;/ne3mڲ,6gS89cso.х(e[G|$O~Χ?{W>1I8?`VjnH h>F i}r~@N5?;0rh?Un u}=g!kOD j7s;:RgՐw&ݐȐFka]QZmTt#.T2YCՂܯ^L +Λj2SÄ +G˥!%h*WQ~@tEEHWɯa#gUQ{_l |pH):d6l iFPQKLu0ZOMP͡f6ӭSRZ[ŁhG*"NJɑgWZ_h,#Q0!,asWKNa3rX!з'*GI1RKg:LUӏiСX/ӶӜ2@*! &YX.%!,QS%3~E+xփ-gtXy\K ηô3^=4W+[+jLzOɫ7l6,0 -ފwoq\SIXcŏXRkL~ԒV*$Ȉg7۲PXkS4M(0!>N 1Tb'\sqFڕSm׬ž`VA-)xWG~k?i˻;ߙu˩(k5 +2zΎ=$(e&9WfoKqv܊|iW5肼=)'cǟsolD/O[qk~M?ؼC"2f9.`%ʯƝmHhQs̈lO3 7I-(*oen}{I0kr.7]Y?Ϧz7ͭCА>tQ?zc]JJvMRK]=u 6{J_othKC蚊Ѳ +rs 5ٔ,2DCI11^c}$MT O.0N,%'6 ةdob\#|C#0Rk0"nl FlWT>7=283.vأ]Z6|kTc_|dgޱþN_/]H{ؖzD'~Hj_õdzMKBY$@|jܝfoۈ,ԍTa;:AlzW3=p暪>X:^g]yutg{7R˶$h0.:FN]Yj XhJ_mH{ s]((Z?,tGM1C,1G3S$MWڟ>x?:i@'M\cC&l + AGr^S#ouSL}frnkkC^1oτV5۔Ղ>J12AyaB&@3NiS"tmgfZ\m$:^Qr\rn5E-ۍ~YT+4|}I!B̕qm9^j\\%:啄A\([b_zPfU0P սnbH*n6ϩgZ:k7帄!QBt*1-wǃMr㒬ХnT\PP|v>^IqՑOrtĩv7e]Sz" "icmJr\: Ӈ^#79T*NE9#\ә馴{msL=LdR&W${ +"HcbGETi*`$3g9? ۾:lc + VڎYԿcw-+c`b +l-H=#,憤9+[/D^s79n&oJi +n1/W@l4G'|hCV:'T:c&C4a~iGrw (͖yr5 4Y=՞Zgw }gm#yC +Iݐ>ֵd?xC` n!'&w9<#hz!JDO7I,Vu5:Tuvo˃n1BܣM2Re\lY` =c;_^@z.X_\`oC<ݞ/L_3'߱+o|M/~3"./BFB?rvw&-hh@?:ĨT1[2.xm`_ֿJ,u{^z*/ZD9']|/]mO3mG&,*D夊m5gEU"(Yי?T`1飓L [XBqBh31JoI ~6pH{NQT:/Ǒ?B2O8WD駜Y|{`u=iEjWQ+cԼlImgӫ- 3PV\Ӂ Ukv̏)2 pG/9{]va&p.I=`-@}8|F,#6O +_-ssN.~tt^^NO*8ewV"# [FR3V f 萈Z5 ?m3uXob5l>,^"DTKswE,9&69Y~ 9Ցm<20P%RCC_m2u +JA>lθ' + /ۇZK#GmoMOJLO΁{f/ <S5MEKgqT(۳خ)zUyed3^VI fjݷQGNxYx}c!5 :FGdֱ&4Ҿ%ü^$ǔBGkӚ*%Ajӈ쓋SΞL|56p! }oI:t&mTbw=ys]Vb]{0a96&j<zɧ*Fibe_9uq(F<(=Rv$HYCz~b>4Mk=0vunC p5;蚣U*)f7{wcK=)>xf}9HrL)Ft$lӸ䚈W~B:jk)Vv^ 4{Fc̡XhȽng|mFFǖ6YP7~5nI7 ++83x ޚَq_BJHɡ8{J9pHFdch.GAߞU0TQ1&:<%vA:ٿ2.i@/)Ja s?Ne Z47x_}AQ)=?l-ƩO~ #6fM/䲫1lw..;of<.ъ˙UיN{j 4yM,GFڰ^VA6dMO} ;w(]/|ᓖ&[RM7hm+K0хm9AZh>w5yTw#?I|,xa-Rq-S j(7@TH%F7WK}sb(:#f9dmV`ʧQk{YЕmd]DG>ynx1^f\!ކ߬\V3Cw<υΊ&ơb?炊7FjǁvN@Sa苨wp;&oyudx][/==ͅ; [4>ptOؼC"tYrj`OKۚEe&Y|-8M(w>tꗂ_xxGklo5++=R=? ~ud-2Jo;IzLYo?/z\Dӳ|{*O R~uuhiVNfӰW% e}:ؘNhXz`=zsvXQ0P0"׹U@lܜcv~p߻ИQ`<ޱs(/f2t߀ σ^9*jYgơ] J5]ghXt{_|OY{Z x2I;cЎmsc*: _R?2)hgrgJ_y}= rOOlPqk.Q/JRuT,r6>:x; k u[: +&nQ#_#ZGTȡ<@Jby_WO2W۟HQY_ny+o{e)N)#f$ 52P{BK4яWjqeΉg+[~E 昝Ƞ4q k37zBkuQn""nZxc=KېR$t<Ȩߜe9{@7? 3jKbӖ\6_$~hOݖsJĽA,gsS`žW\R~]fypܱkac1d!j/@Ei> +}Srr+΄oWgp?mD_ܚd29R/NRJ_^;!#f\{]7 @>0ct]OS:YC8|SW +W+ڮ}ӘŸcQ`_:/}#%NFf:oZe̝%\ÎS|d${k髾nWߌ)aCJTgn.+ȁnBI׎kC}ĶFHRkua93S{Ւys~o4g^la"?Ǘ(-q+e*8wP;6:~F'{7mͶ>\>JԳY1d#=?**<{{l{Wt;9=Z-jqCC-8–l솒RgaM,^&;|}QTt|XzEIWwUϿOr_KKxԌ_J eF-5|KM!8ɘ%`kfuk܂y +B@S&<d1MGVpoξ YwtGElQGЩ&VůȌtԬ"6G!Ξ;˼31=9Ty=X{9XvycI|Z'>]6/`.Ɂϋi;*Zá~bFoV{0) V tgS_qE*=WdŖ ++Ex!)^HYu7/NZks\⹰P:y)ՇflY%UZ{[nN ˏ,a$jc7c7*qoG ̖ꋳ۞|6MzWt +Bt<1%W&] W+=4,XޝZD/w ԵidG^h+Ȫ],>xT;\6 ܑ +U`7'i +FM57 `?Ƞ9ax-2Ȋˉ1%*/FՄ-Ei]潙EDS\ˍF"O`pHXJ뒠َx`!wlN^kع'!O#[ZUǷ~1/- MD yVSv`o)@; 6d쐊Xyh +vu l{62M5ύC~k쪹مȿ\o[?2ё- }>P)Qs%v %~Փͦl,ܵ~55 N,~btGFLMGtD3ܑn"Q~)䕱DGZxo}hNݔԽ W=3X 6[_"W_THFk9켂~I=X=.Wm)'B3Ȝ=/l,݉k*ڐ5+,{W!wda߫9}=iGI(.+l6a% (Dy)5۞Wb*sX7fBSXNY`d2Ps5272QOb:xȣXظ.hz_u<2M`f,BPF3{>-0O*{YxwxzsU1G6/%)7Voy蚁Wy= 0 yMN]H`P-M7|kL$ Pa[7*~*꣛p]zЙ>?f[ ˛<פmIKJ{?;_U:KT(EGYe ?X==4$'Ee̬q@OJa +oƵ$hD. Kao4SxfyLME)59y1Zew~p2q54?7x7q8I`'en +lAh]l) ]g56{Z\%zea xwrp[Չ˟uUwKkвq;x-N܁{LN~{xn&{M11U%Te\m]h +MQ gG设9a9\ǯ2^"Vk䑷ĊAq|iӖ-gV^嚚\r.:o,eN.w]+hE?MMIZKg﬏ߍH^nzJˁKq%ExֱS4В-2Ln@]2s`] +1Nn-H:klҫV ~ϯENQIvgZ3=f9)ۨ Qb6h@X )qgD(e\cNXwu}YaU0Q+6ړ}[2Cˮ>ّu¯Sj:15=) *H5Z^O*Fy 7k١9\K$/S'yX ?:wKrxq=GkķZ/͡g֧\}@Ik)L%1֡[tl'@5V%8XaжPr3ޑcw|R|.7_.׽t}~j쪸:ґ-?eaYK헵<5%wj= N\ᾅ͓KKԶUo%#?zC;YBƌ(E\_,J|7iHҎ|dZ&Ho61!ؓyR)tO^ ,98OMQ&cF&*n|Xm +ȑ㘎аmf v`%4̮C3Q{Zr]qiVz3p=@Zx "6N6e謘]A3m/ +L (p&=n%B |Orǡmjz<0('V_\Y=;%~t +?#m2|RR~ 1PFMI쫼dŐ[z^%5eAihݵYlmv3īp4iStG_Eh}#cHhCf`+̥M DX/^fv-|U4̥"7XiW!FD-zVtQA+IE?Zéf@;F`0k^k~_ Fai+H' vwF+<'}M)|q"j^j2zHύ]5sSlЇ&Ae7),%}{'~۔E޳˟B cOݍVqy)>̷!g9gtOOFDdxsJ_T%T>rM{$M)+↤am`S-oT=MO>Iynt(䤗?\;\ByOֳaLYX|h6}X!Bm+tBp03 Hq)?)|$$$ic"6X5yN[H.į0}St:e3Vǐج6)^ٝ37Qu6i{_lK2\<&aYLX]sgl&) )eW6 +EL[I.@"r]unl:D߷;B,xb;C+g*|垕<\_+ 2i[K(cÀ?IF~Å%l(=^ ::IZlL7<4\sü + )-0GZKm_O= +/ BjXsuÌ>AQmd]#G+g [&\_ޑђgpA):Cũ87K5 -xz +IXn{fLMܭ5{oc\VsqS6f4L=BN"'% %w.ŷ]wO]}4*P3ڮ kN &ɕO1qn tOF+U¤@T3'zyg:믹'=A/JbS7,j*؆5yMRqhʗ_M3[ko&܏_ٚe;.Y9^*V~zzۇ2+_֠ytd͝Qi 3κpΝԂPPM$ȡo{=x›p+19鮉q[/gçyH,]/*ཋJ1B*;s_Y| *u:9̯e:Wc?3u]ͷnB.D|^ٜoc<\ᑇ#8Jӑ*BpCJ-T0A!S89?0/ ߝ7&j9Nu%]fZxψ*:7g/Æ$:&)-n&#d{'o6liQ˄F9dƖoy>+;>3ܡDUYnYM'Tyfm[4,c;Q?6*Jm@- w4☞K4v- !JjCFJRR{nYBX BKJ. db?/g,DLa9=ٳ);]j$%D]+гYvwV[z:TϪ> +""$Guĺ +_ +w>2ێD_=| /wvf{V0J^oBA?&jLξZWgFҰ%}YF+Z3rD539s1< ZWy5rIPɁl3֦9!Tcl}Tf-RKstP|&BrzcZNOXNy/0jЬ=9N#ysU#F@8i://ח:3q] H5j~Sܐ Ա:*j]X,^U𜠉f1:rb]d`[zJe=2~LD4zHpޕ+eE& 9MiSA,!&o*՛ +8jDl=aPƭ~oJFuJ\5v|{&]ɞ'M1 x ns[[zq{X_DMCS#[)x궉 k宁yXUu=<'9D`yL S7Q 1r[f.u۫9-3W;N6/dTj,1oI\P)@<ޖI))[&:hba|Kޱe&^LV L쮓aK6P-k= v, +X#bqvνkx]XU72&m/Cǀn;S׬E;/*Ozy&>3n`bB +ZKf? ͖ +U1t&UtHg?ߖS0+Îmk̯㓝28ʲL 8lJx0n撷Lx}[;ktLCwm Uo.29o\$[PRz٫nWi3xi- \ܸfTuwmLbde{6hDG=fZ +YRẔZ]1=ɯU9 QwVQ㥨^b"66g@zso< +lZJo.*z"udu;sq@g醬kaXTaKCg[39Bp>p9_ܲp(%.ʯb45YP琕߹4K^*z9c4ɛu൝rAW> X(X_s|S<ob5iV[|ڮ qkZ1{E9[ Cő>@d@֤Po2¨mhth-;wg]_>S}򎜵ET&(XQZn1{|8`b6tLKCl]w&^ ZD*Z"`~?6`C\ǔ*߱S1;+♨_-[CлmgQ} +jm#5T:'df>k)ٱ Wm;6h[&Q)V9 GLvJr{Qꔄfd+; tX{zVhj:Ke|S#3mKF"i'8I jah"\R1(Łm+j`yM:x$7NW1>$[nd&ôQhfl:7չ?25\ZDg3i|XɜJk%B+ + +~p3!{}3|o7Gw$:MxRDahUΨqϣ36Ě~m;] +u^*@]TK֞C4-| (;pq{gi2O2*;nɣeE H~B$Zc&f;FiBY(`5%uf݆PӚ+3ݭHRKʡ&DP -0eJF/q䵸{8:б\O^vCGkX%FFdž`5<04ejHH&MoaOcuY?jO]yįc6MLwܰ%z_Q4s֞^NL/ `%}ʱzd٩`(E:T?:O>EG9itVpI@.oGwkY⛥ۆaIX?3_F,͌ r{9/e#c>k33~b\3ł"N$q1;TyLC&7.\^&m:bgk a ealWUss|l!,io:UՊN1>/}ǕO^|EE J; (i-ĥ5n(-FF*{5B&^A(KoWo`oeCUW~0&y]ճ1~vz l +\<^mO<P-CТ웛ƎW{^N6ώI1a_PV1֙ed;qGzoE<z' ^/^G[&!g-C?ecLK0srkOazӧO*E~FqεKNĒL=?5kKpWL8[H}wrѱ=qKڪ9& KǃX\f#IP[[3^)eqF|Y%?;UC?VnՑ y(( Q񺦹fkz`l2fTIZ[cֹnXg6Ԍ' +F*&[YSc~* hO#~O r'wLS!9^bJgv3 Z3V$r$|HϐroJuiÓ~O?h?P)*^38*X5v.W *ҳ.%կC"9?CoFQ MW`8 7y?S''!_XYǯ(*VUmSQ3/6=ƚn#շ"2p :{ vx)vxqxIp]ݧe^m_Ҵ6,הF;jJ=aߒJC01ᔔO_RxRU]x_ɵ7nLrËwy)_n~ߦd^sRYkoV6 puxAmC`J7<Ki~38flr>_(7>q qpr+txױ}'lXig]jG$y*Z\s,x Ə)7"jN;#nj|BϜ.1c7 ـ6%H;IؘhY;LJlwk"T,wpA7?ށnvE8D:sG83~p,! d3V8a+%5+J8-q.~.J6io=]PDōLOHǏPh1o|=rFM.2]s""1V,Y%YlY;8lZȦ'7#s68a/AxLobbW5eȽJB}YK4-I\p=\dsR~Rܲ7x*]&WՉBC݅i$m'j+e3}v`͵9#ubF#^J JlG؍a-w#@=W ćLCq2SqQJAOQ7~ `3;8 4ɧ;0=аzN66*inyas&I(VXx 7:&hČPtk`8A>ΝV {MxAE>fYU|XHmoM1K2p3zU|"7)1G2+m2lQG&LrFɖĂ!=1֪̩8=&'[S=.RecL [ZR7+{Ci!ÍQ)٭c~l7|?³5NA z)V>0!FqhS̈́Lsͬ |[K1 Σ77p"C5\wJ{bNҼ뷩dYd5ueRVS0ۺ4>%{\4Y&A?ڑv/oW;YCC=jyvVB=)c\QxRQ;-奫e9[(CwI AgDvA[ Uu im$[c\Z=Q[m,P]ȮJZ^qAYpB(3QXfW(^WëچB[Acթ=3=PGs£6DǐV*{͝)ly1]g^,29ZϫjwS4cmI=~9Gvc=9ʞM[fI=L'l)ЋPak.pq I(uУCHCOxa %iٴM5ǓM&f+ ą\*g+ofGdF3AR vzձv 5죩ꊳ٪J=ߝ4~5LYdi$cAUǘkU&JD|?W>.LSFeb_?vd.953QA#KRBnQ{s:YR U2/!t(}LvےQRSkOL_w{ʜRذoVl 3Cיؤn'M[;u'G%դV{^3SツSn, +U]}`o?D%g=aAYpeAͥ^mesCScuR^QȂzp&GJ9k-scUE!^h7+T +DlMu/U5[j/WJgST,y6,>4eEcKY]n-M5v ]cyGԜeUqIݽ['˙V:r[㔬ᢠ!!}]iR\nVUY112 а ԍ4Ӌ:C=\2uҪ M3sQ[]QgkW-5dTp:qfWZ48@Ef$cÉ.+B>dP8”Ddc%Yf*-bxd]ɶܰ1Rb.lx+/937e:y$wW~lo:lvȠu!e] q*{c)%YI܉Ls=lqjUc`CWեmCul)-eq81?2 \edU/ԯ3<4?NY$Ds9pYZRW} zu>\ +@R?1pQK_$i(Y'ߡs`f&PC5;6ԺT^h9+Cnt7>pVȈS4'͘ʴE 7ygKTe +:ڔSU\}l)6q}hӔRUǓ;dȚnbϪK*/C쎕( eqMGhcC ^l%<0)b% w9C)<2\|/kt_y4'o)"ŭ(c= Ň(_y-6ܜfz0]CkC-1/roGZ,8~| u=RS3%uz*ei}7y#rwֻf,VPx2A/:V[tsU͛ð/ ͩg3X <˵i>|Kzby㞲8wM<"Cͱ0. *'-<9o:^n?^=.lkذ))rNڞh[7j+e1#_LshfTK]iWkNRZM2f{+!*h)Ԩ5!dqWO{vJIRZR&6BI=QsN!6i]ڒO~v$UX<6T0l sBv-t'Xek50v `O]":Q."ܗ=k; ~.%,I;"G+Yϖ;WKq[Cq>v+%ǭwCNTأ1b>^zہubN MEM9T~t4zJ]B;(Yd- 6HK/ܒ6:N7;F/Zj&*#Q \ĺtbb"/g5sO1`㈄,heqMMI6WYsX^klJ qG2з!&Fؘu!gS SslXCLkU*<'$ڙ~? ug,N:1P&|lq4¤H3W. ޼PiXgA -YDgՇ3eC3lӑΧ( 9x-T-ִlga#>@ܶP9:ؿr8t ^W('dTҎƘ=(^u!eK'L1oJW[5I6hG l >is'!˽]:!= lS5 &>H'Tc-ۮHlc<@1V)>ҏrS}ʛW݅3]ؐB\6Sh!W%Tu3cUad ܋~xl +F92h(z8Qwg)͡!.NY^:6FPE[bt$i7K" ؑq>BGȓc HL>QT2¾$m92;N;3WnK]ЏV1jS 4϶s9藬IvHy߹:Y:#'ٔp($*ݕǯ<E5w{8Ȇu Q~rOBg.۫y6qKܖvF + =5I:?Kƙ^sŦ\TVT=).:ޕ@< Tǀj o)q]Y;C} σa endstream endobj 49 0 obj <>stream +)_s:7 'F.LHP; +eu&$9Fh]|1GAPʷؘ!T|{(<\Rz>WVu@/[U_":ܒB +޹s +6E:oK@. M)lQs{bb&hS\-o>ms-bFb;Ʒ27-/lCۭ~'.nGrEݽy#OG֗Dݞ(O~|>]t.r}Q~U~އ e'ݦ$=T%&1]]k*ܖHOLGƋ.rNЋw;T؇Y9jSg&z4)H?2R&bZ_B[֛33t=3tYhG㤄WG:Rʡr$F>֡7S]QZ/[t}kS 蹾dl#7!K0/-:M7ޛ6~oW"]Z+8=)s_H KXZ?sK>PnB\:-!3a&וN/&XPJr] xv TԇJft(G4&>ԕ6VႮ&*#%>.UQQ/얄w\w-):Ppwou⎸Tx-u[ܕ8ߚ}ihjٕ6O7hvx|: +{̶:wﴣC9S.αC|}z}UR,=_T dWGޢ:LvQ ip +%cy ||'$@SřZkJ zd}dS:ۥe?Kga*~/bSOԧ;7 +'7G +8TM5%<Yb֔V1s[1#n|ܝLt*^y~|8V7.}wHx8מ5< ™7ʔWsl9gv(/RIš-J}mGa'ZdV9tM 8}Z|PEXf֖R7]U‡&Nrm tn/ Q=%q|Z+Sma|~[5Qn)w"jԪ wVf֔? +ݔP"tuiOm!)*9Ä+{q>9"UA깑L%&lKy, qmx9DUpw!-xgZ@O79jJ64fGiza-=^{RL0!p#%+ !-2@ tbN/m+p_#*pC=%ѷ]j}5{{"]f/ +KF> +g^6f8m|(#n~}m8y?i+q+gvGE&rϵΤϷ = ;39&2^(酇.RGJRk z~Zd]ڳNשy?p3["L@Q>-f.m{[|®$%/p pSAea .eԠÞp243 v55k#kTÏh\Nrԇ= :H՗$=XyKyW +$DXlն<ϭނhf-v<&yl$;GQSu5, J&p)ߒ#|W\#DŽ*aGjt +x1]x_'Z@ާ#lcu7RNZhȝ'U !LKy:J}1D556"hCKD QF TxP]s/;Ӕj50ݡ:юT&?xL@te2ߘj.l]}Yuo)GU|poc(Hp>j!<|;S6h[FMb&#=57@W:~B\KS roDj??5*@I/Ƈ^n+y¼R5H&][z5Sj%,f1zB̶8I)9(.bWi)BsӾgnwcc.)0qrtdm!guI ?-Rt;զg"{cqI/&Z"o!tcsDO8_ ,-p/ 9X%"+ F|ocWdƉឧjB칖?ZSO)rRGT\@ 9Z`, -iWqAR|dC!J?*- 'gwaF֡:ےq1~m lH0Am7C}b;Aې2;^J?2H =ӟYz >t Er-uRN2F]57ΙPcT%)g.=5rƊK@/e9ϷF;jB0 +fjש+3#=\௓U_gz +V=T_SfW :zALU[0%.xo7:8- +_R Z蹞֝f"RS;KoZ/6O,9ѱFr]hL~!">‰{|kx ?Gm|&.f_m?J}БPC(IwLqNru_AhPrW醤s-yWQ_ZH^c%3t 'J OE'FBl}9)\;ӢSN_X-))P,S0ۣuYqR_RHz$]Nȗ3!dxKfI_ͪiN5':򿜙 0xԧ_x`VOS$:D]r<QF:PRZ} :q !o? ;Sa#K=KNqfu܅r異51g%+귕vHWs@E./%Fԃ?HIqZΡu=Cg bSD)9y07ƹu-IzfgoKh)>ȷkJtxaB\.M\ԉm4;RLag~ 'lKI]3BwGh֡" \Em{5˩7FIaG]CO7<Za^G뀸;T 'f:Or .dNZqe_MЊcU!sk [s{ QZ')3utHG)Az +uVR7sWByg0YB+l\3'j.d9\) rCEn |Zs0LzWF;Ԕc=hCC:i-tng-ud9b,ܱpzcCo)yvܵAD} W@OfDeU+M}]Á qANu)/匐GsMɎ_'6ygdhcDl/-~T&>j< v$cMʷKm*fjGNz:L yhNL hHL7/c3r4"D. CJrEmCTԙd<))9Y,ݟW켃k?K\,r[#}ZӞI`^ƪ3 7gFnaS6P |{ڲߵ@_!#*;yc*zڱ;P`Sl70ݐbGJMYbՙ +] zlt|m1.lz1aBGV !5t lWqd=󩚞cE^򿹶-1x~ Dx%91agMYϬ" s+D]ӹY)%ŬIZU>JJ>Rswe|Ю .m2\{DH!-V)cQE +\Ĝv;LNܗ#0&|KZ3 ;5RĔfB'&ln)4%L,!ͭ/;grޞ=SsMQx tuRF'>΁VjUw笉#/Oi~RD34đ cU"(뼭uYO )ώ5ŀ 3 }Kd$s%!#iT }d>W6,gUP"ok +jXf3Fы $GJ|'erfbVE_VB׹ָ{[7sZ%}ei+s[H>j[Rdņci=E e.RW΋WKY;Cذ355뫑}dWɵKw KoL>vQ~}HwDo鉺z*])yG +*$uoׁo1"mA>xE0'gC$ehV̷K]1~ڃC>F؅:j:dCT2ےXMMH0)K[0ߍ $i)Vz#Qr5G0AbU0r ]oEᱹm\:pzǟə7l3!A\d{W \r߭t9k6#M +(z(`W3zƭd9mA.Go\c䤿&܀WL'36?i}jWC[)<7 X&NJc|Qt]I+xL#-ubN t4 ,ϦZ?ɇJJ2w3.4?s4䉎; s끸XE +NˎBsY i6bb>ruLr6->nW2eKJƚ3=6-2#<7(*@HX*k8/)5xMXߖ@U +.nf ܵa|)L+X17)Q냤]%y}||=B|Xr9U!c-+'ڴY@J9PaBC)ϷE0ceDS/U (kjʎ-Zz U]wa7D=蹈{h!@J֗R1SX ]0q؄4mˆ5Xl ?`%>Rk҃'aM:u!g$+p:Nuҟ*7^"t䷹R +})PPy{S=}<5/g# ~+):W]UiZ^]h0 RB7.@W~x}= `= -AyռB}+&ˁ?y=D(SV-)/vdP?#@_lʡ^{JD("b]u,6.hӍarʑ*nm`#LjsMq{jeJS;뢢˽r] sKxjncTbUAOuٯnWz~ U2B*x ̡9#`Ё +-/~ 3>ε$9o_JyjҲZec +"cwdlXkxk햲.F/v">Mզ<GF?R*j윐a6שN Q^\sܫw. +IbjG#48dOXs8 ؑԥi&_kj|3 sٗAL)1q[ 3-i/Դ +=O[: ?Rs(uL(=luUJM*_# + djQJK]CO&I"Z=Uq±,(dOk/YZR}aS]g/Q檊=}[l%r + ԍRRyԷe>t:cyٚ&CKO~q#Zr<4`S>jz,r6@r5_3o2]KJRGؖrHbBlV}#B~^Gxڒ}>rO 2HxٗrV!(=ٮlf`-iUܕ+\L\R"雚mR<}B/f6-1iܓ$goXizӞ~jd#؈]>\}qߑX3uy+ݨs}I?B3U3V%jNGPpIM)ov%>vhYg&j|n]~T;e]r"/79b^{bÁɅ#Xmӑs&jU<1r-r'Fzᅉ:{9tgޔRA p!$m"\ +p-IF"Q\D+  {:,>2Og)X}]T\̲F*d}x2I=gbfb* rBw֫~?~Y]NPaRu GڥP= }#ꩩ.i.~:gw:@_*p;|"+LŠmI{;Yy*/K `Fh*͵jG=%BX`!kAFAA9t=t0 =P >z/iG93o'MT0wUm)ݮ);1T$K/k_MK?67]LIl잲(e&r'%KG˃e1@96rP +b=T>L5D:Ot3Cnoj=6GJR73!D|S|g--6Nϙi2㡺줭vRˀi+Csuy[U$w5!q\,\GhkA.6-r hœ fw6VL]v{WXJN?5殼$G׵i|K}NM˹U!6p v4F{s:LU Uv8 a&sMgNu b aqRNc&'(ñ^s|rꩉYrYMB!۲_͵WbzB࿊N,)N}\n~5 UAP7Ջ:mcQ>y=e7p^(WԱ U2#6R[jv}Tw ГGӭ/Ԃ Qwf^poTّ3J -|u< K򋙮ǚJh d{ys6)`7V?ӯ{Ң˩ +TNߔSqaB35.f-Dm#:PǁꠟAz dķ|fc#;϶>>}9WQjӕw{#zlҷ)̀hN'%'!nQqB81(51|䮒}jFn +bT»*k ZDt<]^Q<рǫZEL4#&{jEXx@[򑿬KL'1+pD 螑n(\pK=b#&0|j}7Q+v'4L V&6 DW#:2 >p_XbB40i ϢJ v.mV/a& ˿@^dLf U9~nm%a7Cn@XY]׳HװS=g:JK9x'fL [\R!Ul_^d#~0?*V470A btfB ]6 I5}gm*xs,1D01;͏@ߩ%EŒ@m;j[G9fnߎ *p768=ke czj[UOukty}^8 q +9i'3>%I082Ja *8) !ztMΘmp4hN6- >ɹxw[a'NmˠێI~IT\oae1_dN]3vJ?SVP%Bm;U_bNos۶D};RL O%MÜq8^yG>Xyï ɱ)Я\9A-ݞn;6'Y[3q#)7KV_+%(+;ݜ`@I=1\[y8~9 t:h} y6!b 䚴EX}Q} I3X WeQ-;ibB^p) ks +~p֋m.mPB썔XacwltTCi͢3FJKq6*/\_CS9p:FPqKoIg7yr1dVDs{VJy;SM__?-&?R +vf\?<  Έ +W}po|3]gVF-FM"r$d\.`{N?3)ݸI (x9P{bJJ]xP9saMq{=|(k^nSWV.yԬZWS/E3\T48ʄQ#1t e"$Z9g33 acp8R1{fmlR#e\j1巨i_Mo)5Ad0w'H4mar.>5o"VMLyبEP]ƍ +e+}E(Q?sQ{Շ7?Q`A^H-x UNZ JYw=ƥIjՎ}C}w|s~:IHa\T԰F/mM({uwK/6(<-s*"24#V1  ә0[ ˂ +VkP (-qD7y9\`w#Q3GнFWI9 9~,F][3]]zDZw'Ԗ~5o5ձ+ @?C4q{^[@\/ yu5$"Ӗ)MGT,xHkt8;e 0v$HOjtڈԘQ,9^%ׯޜƕf]@mq 7"EAu y j\?~yw|k +o7~UbZBKdP5zwm"F'Դtz1<8Հ +W4siD2loMrks_2p0P/ 5ӈ ~}n',еM^&\C +^!?%4E@+5~,ce +ThDeTs29I@/ {Hq0;*\4_d#Ӷ3!yg5.66j ? Ԑ،BDL}3r >C.OY$ {ʽP'7&(7BXD9ȊBs8}<ϚPPRBJFc*b칇) հA DTt )B܎\^<ҫ2:!s)ZJ G|g^3O,[z>9I(.2_C -R>ܜ .2O5Oٜ& yg+*Ki[E +vJNo^[VzoQY;yo}3s+c5XǍ(}+oz[}5 +򁓷 Nmt֯UCտY~4Piry.Z [*Hvf_HKoѧvm- + -~Zx[Khۨ[K[@ll$ϳVo}\pqo>?ndnK&wh ; ˶``- kzdWqq7rYbeT5I(~Pa6Oŵ*}9V շ]frSAu{BG9x1Q/Hoڞ%Wy]N;8~_(~'*Ni 44ad-\t8,/>5?4|"8j~מ_X̀Ziݘ絙~ž|7 %!@k_$o~S6[4ω1 vr*g}[_duub|.TpڽsjnQ+j}JOm7lX$9Mkc0f\ǁ.|XJGq='1 *Mdł:Җ +Ii4r,;6%yrw6bW#Nj:!mg]#A%]3ZsD=RވJDr1IY)~*R 6quӥr6-ؚDt\d($l<žG[IvmK)ɏI|q:߯eE5l\.v+bFG>85r)6AX_avLri9tw[j8t W4} +ĕYrw1#]#I'q47.aafߑ0*u\ߥ]?kzS+W=۸=C SLB) .v+zN03K6@؞E_9>$WV;I=dcF<؎RdJߞԮ$+?k?Y TU~wmQN{'=1_s==~|2lS6 %D@}g0j1QŖ͟ Iu[Pb-hE2>t]ˎ@gx= 8Q1gYQe6&Ԍr x bTIj%qrc+zNWQ+|}׷feA ?jmnx7~'WY-I_l<ȋYHߘDrqij}_iXEl<=m g*~ys-QrfzwxVZ/n9-#+1a)B(M@sS+۳򍰂٘ <<ھM[a%6nNxȬQD)i0"cZVG `8 *x'p : +)u9 XihOrC܈aC h@0kcj2du5*#{m_1fL+%tBZTxHu~hٔ)GM,P0ׇ0W޴t?-? `3OGCh-BpՅfH ӍlO*ưw>woޚXB9$ R1-L!.9W15q>Ne҈Rqq^fHk_-A1z N4 Z#z.2JG{`zɎw/%!HE:d9HgCRv i.-iᒒFL3Y') l 3Q?mM[ [3zkUK굨OYmX;_7d}Q3z!BO,_[VؘE:7:Re]sHY9K(]~ˎϦغ]'QF "KQ%jRh礁ӛ:'grv-iduG4a-3`#An`aQ)VZ:VgEd,"JI3o +x,BjN_iO w&(宇c L ̦$ЩY3qXI;3e%,lhDˀ$mį:7;,$k=jM, ho[O _uJhi}S ++0gIjX%ߜ"- UvZXIjY耤ԭ qps!e!*j^7~I9iHqeEhWFJox4l^c' 9kB+î?'Y+rzqgYVKA0;lLY6r\R6173 nV쬇ܚ E.@쒌5lhwe+XsJoz>"Er;aCZ(&M2i-. Xղ{⤝h퀫ET會ss(y?#5?$ڽo:.lf=*,c@,E"~a}W2t{Fo-'S W3FZO$'t|Pl04I㟌o| +iϘ 5)#< i'e]. ]=_VGX='H$⌍_NY)5>;;ةX3=BW,Ą>t`R8tEьO8 ^JZėNv -`낳,uscsӯB:'e r.1 ˾1Lp縎 rܴ=+ ,2&6r ŽKʫ0V};g"Zvm OGjDyJh ,j󔦈UI Qݭ͢K0;%LlDDGz{Ӽ9&Zt18:eF^OuiؓsȀn[̭b-z=o YUʻlBfOۅN)YyJo_`+2v.)twzSh ih-I6ILqSH@9ķ=Õ__~5qPق  pE@NG 7=7,j%ykwEg@ɚ;I-5faw㠅P:Hm+e-HG˖c;s9=k +/]!vmbVF( IY咰x9ky:bqݫ|XO~wo}6 +%p$$d ̮`fBVEf-D@C%xb)+dq_sv] D',Cq%='U쮴R͓V7~祇5GbjRm'_;|^lPQ`yq{ O"`<o,F@AkXi 42;m#*ZKT1bzqs~Sx*isob`Qڞw%r~0$Yf5ɻιyTa# ~, +L TmX8ڒqpP}IRIHA10) i@%TI A[^{dQ`Kv])xQTGd)ryh՝I5 uyWyI7+ؚ%ܰ=h2"Vg-u0Y /ϱ ?Sv&,%T9-/8AZ} 쎥wk@"J썄q#sI{ͩa 5% jiM5fDkKY=@v'LN%崍K]} ,澇Jj߭=i8}ZSaEvU(¶)5`[r&fo *P՛S1%! 9 {|`Bjbc' ͘(I)k3#Rs(Ґ.jw@E]'9 #R =ʺ yRY`:"eӜo`gjVa +{1v ILIPJV_ h^665::kvmM!ίF)mE"4G /`OPK>]{Ͻ{.0n#>& bZVgBkkzNG߇CV)y@07&{~ jY t]`D3@bv&'4wu_W~!:'FU¨^LA5u֘RqJx\`PZuyG~fA-@Z뜎-R]7O+%͌K,x8Uqf5ߤ806#/nMt]Ƕtu}]Rʈ+b`R&V;. {.=$@圬qPxQ%S%dMC&MYyBE_Q:Oz~ؚDg-2isCJj-#Y=?=揞'7?z榤YbjPRjjS,.FvR@;ؼ3G4HI3z !a&xI7Ha!ݹ9>M~xAK8};߽ 2i3OO7 Ƙsɔ"*,tOOs 8@U$|w+)07YD[yum5"n tjE} ~IMoL90|Sp6dLV^)ۚj64G^0#VsFfWLΘ17RztIJ\ oê۲S_f2^bsPcgA]. +1>i/o'5e,<8`4%NhKo?AcLؽoPq1J;9;%n=>U;sE^8C MG6Z]KyA=eSy𽘉ޚ:Hhw=k<;)3˲*WUbS/Aa,_GԦ%u`!N];S''EgWyAgu@|Ye3{Vw.fTŘ^HZ䴉RWz< 69 W}йk17gMMx?m.:$4 +rEDC®S*̂#FίB{.ΧaW^6络_潲CLB߷ya'4 $i[)ͣ/% Rf2r+ +{e@"K 7y{xD5ૠN*?Rm ;2@U]zV@ ^Y%קޙfw*>x\[Ө PZVW{L* +6/^,4\L.=z?' Wȅ·DH;eKpť~0gA%xY۟'9Et[c}?'19hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG:rБf4;t١#9hvACG!CXIWGVб(.O֜G[Q.I'GJrKbQlёbg9ՊCQ۞GNG$^là^'9n''.^|ً\8STtܥ#/]<}/^<{KGh;=w/^_8Bv˧O_:*:{ҹs/;w O蟴|rȱtw}_=v 26ClFqb:Ea||xș3T +n ￝Gj2xIp#}`[/A#.]8sⅿ]_}c_FQcxT=+`*>U*I͆#ݍ0?d?(:"S7KG΁]8sG7яC»ȥg\8})HHa%3=q#'v _Zfv&bFyBipRZ`4&u|t( z\#@u>tmgk޴_=/.ĊTSMBRp~xǰqP\f5lqxjPRJ!"kRk7Ǚ ?ؙG^؜=S26jG\.6c3zFG\M[㘋){105p -?iѳ1'a| +r(0c4bZ\-@F$D߄R s1̙o6^5}?V:2Z>k7&Wu| ;vvm۷9T1%S*FsLJEOa+} +>,0ǂe,`<褉ޝH`#fM8n-g\AHh L_*A#rۯrEXu'޴ʻD xOLhM}A!fba+J;ؘؔ&B]DM){-Op,mJ*Jʴ̥K&M99ش֖jzj2Z7g%򘕏[_ /Z:#&)+i⢢jBED/jxInC<42 4 䕌WczZsHCo kE.i(d;|dUVQsZ:gM1-"77M@ɚȌILNYԔYBO1eF,"%&:rmʂelx!z%U1Čj9EN{C@%c-G?7h,8.!'TrFp|sg ?l>ė%դ0{_tJ-rQ9T /{Wvmi-,%gdf" $ <9 yݼ[>|3;b*fkRl˙ؽ9 ;octӝ5JXi#5! |\.f"Z&eCI +12˸Dv IqB'N.O=YT@MrN0kaófjs҈+;y-j_ 7t)76jkIi;hq=7pλov!'ո-4i?e<2, ;rz +q31f$Gdj:QsP&flbfuλM@Κ)ȝ[ 5?y巓6jG{r{ET))nќGu`W +5^y{4"V#t':‚SXiJ2!&fhgɕ9k=m_oOt~pai| j` ''TQ9G^~Z:VbcEMrAD'=)qz}3?nmw:MK !wgZ'_*TqӺϗ`x)b f%o{/Pe9>gȘIMP# o 0]T zgM!B $̜صYX};}ω;0U 'g +kudq!{$⿮FN!3if)'B,$&Mj<ҡw`8s.>uKI +rn~yn>*,]@12fz{T,N% FPs6C=` V>!efsNz_Fو6:۝9ݸX1=+ia!F*2.>= pywyusm>8 +(i;1g',L"$ZTpV؇F,uMIZ] #^{l4ψABȹFR1yԘ0zJ{Hi؞G;4~Pw<18MZ}+m&?(3g$T' v~]˟!NU]@h(y2^.XG?8Y= 2-&W7#JREdP>8k{42 \ jdDV_981>tCXV" YִjhYPyPV!)&FU$rC!Wmh4cUqI3.xD׷9Z}%t2Y;k@,Huq#6m=L.ɜu~ +4UŠАjcJHcH)P/Kv=B.^ҋQWZv׆ߥ~gc -UӚ"Jzo^ 5s$s +jOAz>;Wt,v~Qzj(4?YΟ'3^/a.kSg;Ϳ1>髭wg,ldIqL|qtckPJ! +jUCf{RםgqQr@E4ƈZ] (דZkKA -R3 3i`nMOvgY^j҂C[5޸@UŘ_[c2imݜfk4aheM9zARZb#C@]f?Sz>/8dҼsN9 k5ZbfFՙ%r ΔޝqIeq(qY;s>PNR66bw/0n!7ic""NpŘRjH{pވPJzjCdPndlL ebqGP}/+,7ՙ%H# f2QٜG"vQF1+Гꀶ1$"*1c0j[PMKZH\ˆE5`@]Ϻ!]3!<!5& UpTWaY[Zy1(㋴B {M[S2 S@we)Ѐ6F_)o#(f#&80O-f!RjVgFhLJ設; +ĕ1J)6q)=L6>/Kpc=4M59eJ;&>9"f)MJ-]VKD)+997‹BmA(];c9[v]wz2m֐wɇsйI\^-F~մֵR,j Ef m&'ry7q2sn&" =:9iRQ`>4A5 < +txO@oI9e@`Kjuh9z' 5&fBJ3,:Σ9o?{nYN.KdPKZDPsU `% V1jn}aVovÏwrq3O>o ,syV 'az.b|nyMD˸d{`0S&0e2P@k;*BoZ1 +9TnhdQN , +N" 4k0(dBi +Կ .g ж Ii) W)5?{<2O(MiIԅ&!9iBDW+~B]jϙE"\(::+{Ϧt@Oٚ@-Ra.Zs;?a`v`N,D踔VF 1ct2~2ta( KogS`=ʣzf= r'ti`Pvyp:~sMM=ncri6 +@3<f8=/ +8˃w:@bz{^`%gm".KKYh +=WM2N"u?e2I'%y@>XokRz+8eM2 ;!'Io׵mk߳Ɇ$;$Nb;13e13K%ILd13s1P%sx[֚kޟ> <vd=zlbS>Bf :!ǡ@'[犞\+RjS +Xd0 +U?xluާj zIh#4׶8٥">%9ᱱ+l>2@zQC&2:m֡ny3:X0U54wEKM@~0}ʪJ۲f]d4BvHg_<*'Đ\s vFi9P`lj!9F.Yg"<*JF?o z!9e|sxwtwkw͐A!fбGjh*hEKݙ"1ެ{ù\#* s{4fOm9W|>8Rp( a~hkƐs)Q"W""T['U*j|deMEzBޞ6x$#~df|jZ]FL 5>`}^Ps@w~#JHYؒұ0~jgb^PWQЖWMʓu;&V4u.4WV+A9}x ښ6o&`l誚Ɛ<;l?9ThNKՑX͡쎤2_Aە.-UsTY|ū<Z[(B*.ѯ.[dMX^g3֣rvmᖱ{R +Lј{y\q}E5쪀Gt-V2/0s1FR}OS^V3N9)#(u$30&(juQ2f +_֙OBε-SwJ٨CpKin3{S/m1 :en. +hN 9þ\+$z5bCgc 0`yRV?x7xWqV&8Ay> z9Z!*m9-OͻP te:htŐdҗ ~MӘ_WTԺ!PЎEjspH*%<҉\熰 og; {;$.^^\72Տ}!Se% W%J\Ox0[UkxMXlYV>[!mLWHUКKd=?8 Cl@>*%[K +*g­RRGA}zdc]7 o]S@ l%h4.0S,# |5aAY1܈+ŊjW[pdj ~c j)OPuඤPN*+՝f!ac*rsUS +k1_(XOօG,*W}p8/3V1]X!u\^)op\HK ,J}r<)vׁ +M*9X$%:elY]RzG*qIx(ew7{K9tKC):joZwM >H%{UcV +s{> V2:, ߅y7{|Pӊ˔4]SrqAȦ@: k:k,3;)y-{bj EBp{2jξ@HܘDe,8G)bL5z +Cr%%R 4̀ΣI\4 +& 2ܘB@ +V5h##Ԁi<v~]MЪpkߏ- +1)n!=e r=&k,kVׇ x?yub\^R2K=jBzّhC~&Z}&qC:\21ummƳLu䖋g}r.ھHN=&DIϓ;ǑϷ\c_Fe1KMSeaA r5Av)w,1 %6y,Zeնy",9io h`RdBt=ښP=|zҧ +e> 2ֺq/'*(kjz(װ'+ lDC䈘ݱDlH> \V<5 +5:ztu~ CLP\*Hİv tF@{tM35{MhW*F;gUi>mMU).S+-kgԤ#cuU!dHlq4hy}]X{dfӂ6ūcmrTl +lBxdn6V0p!.$ʫǶŕG&q 4^㲃f +­.A,Rh' +6§0<* ڃSSa{w%b$bE6kPoNĜ#-:iw*庥?睁ܫ^;7c +!)^5 o#p$ +'f○r:lg,xCQTSRb[_‚үY' +nA3}rzwܻTNsikl0c=s iVD!#FDz耊3LYIn >ErqyVQ@ n%ȣ!s.i0-AE9JOtL4U0`n )-! /jpo~-!ˣ!iCC]UR׶Tfvpa RSRrvԛGW. +諚C*_xbBZ|_N81 ?H~ *.$|f Z +Aklr@E.)YA- BRK/*[:3|+\[ȅ=4hzj +^ ħ4, 7_bw3j,jL~ghSNlUZh:^? +HG 83^ gqYY P1kP+i~ʦc` 7tB hEPYKٷk=U]OW}\2yGеEUО|wltȥ$͗>*eеH٫## g]d*~]&5,e-!3 KhP1E+7 V5>su v'{4챹3p-Txc3eĻchK=Olϼ_eixe.xp,K !#{$/gRJ&Q2Q958t+?V% &1h$8WrЎy65-s.Rw`61{X%T<P6Buk%Tr{lZ*؀y~\0> +* Ub!JOb14}^a@|`WK+?Іc(ïePN5In%9ӧabW-\ah*ҿJ5X4Hhh1Q3Vd" 9 E B͢lTu܀Rrdƃ,ڜ-=jZ: {XV33R˼LKG*ۃ&8xGWp=X}d*?A(}եF +ud=!@Bt?s9*c:<^>ZMA3PbgʞtBt AeyCX[D/tkm OU/=*>)`?zd *`6; fg >9) P:\GP ᑈxUE}P[QJqfBTg1N܀jkeJN@ ,le@O t$2>ͧ$42y~2q_:_ aUV%9e*ga:P[Z~PQQyh k+\2|Lkg~ ?*8cfc|\h=1ӐAaįx^ܥamJ.7r4>ɡF@͜p. jţzV3^jROGGLⱉF&0%|u*fn +TTUN *Gڔ7c$Jo !o Y6 6֫aUݾ:ee5c&hXLxzxfWt +hrxDHG}¦ea#-YXԯCk( o0>gl VuVk]Fգc&2 x0{?oSQNxd!aa}yuX^6 ?## >V ?>5CC=J6lLTzl;WԅA̓Bj2̧$%\Q6t٥"C!bؖ9c42Ia3Nm iX8<.p`k +s{L_ HOsKyվ_v "ё˴hccuGhbXW +X^pn)r@G@-n82  ;co>g\T5 9Lo&ڣ3lrڭaC5d_M/] =y4e:5¦@?Gݝ'&<в @ +y^9 T@kkcmOh F>vf&~ma(&% MsX:Oϥ _wdGO3##}zbUFt1y{lիijZֈ^p]>_98hUQK^Ce$\COYk +S)9o{Rն c[xv<^?DA}ES*Ս{M2.PA tXpb$,`"?LW԰VQs@wogQoXe 3|ZGw/Csо )T(Ye s !j xjt/RVSJl@{:.<Ҡ'A^N,š +ثfA8Yu<蹬зcCy$ex#υ0sTrMsU@XA؜.Z&gɅ&QPރrW%酼0я+lFxV +7h%vTы|r<4Rr€\OIˇ֙B:p}rH[UWu'+,ShABъp5%r($M@(Q1:䑡aدkl`c0N~og +ê+2'ƣ hMjRqSzL2)qUPiNMEe\;bc>2+@ʫ݆ikh ˛ B\aP֦nvY֩mS8ރ>\vl+>ʡP 5> j,QDC( +"nc*;ؤZГ |P>6sXG+FVY zS hKHG;Iɶbs +il}뗳Jm䄃q+$퉑 + QsrjU. 7r8 s"rHTL8s6N 9ӣd.Sҷ(9l_Ou,SRR16/Įcm=q-wH9'ZD-t%IbS.|̉V,Ϛ567z(6OaF&=x:FLslezǺ0U 2BWO -&իtB~x&s g]?A A9\ͧT"? ^5tdsUVǀ/ +A d{.ޯ DBu' +JxġHGς€הu $ȑlr8;N'q"?kO4tC+4::ӎ40X*W15ky#%jҭJx x^mBF*¯:Bx]1,Umnȅ^ *ɫN]譣Ъҭ lB̥CrvJ, +T6+= +ǯgB5Ґx +а:1ɀjO, 5֓ |ԧ!zԤ̀ӐfJ98h +`8N@9;cȧۣ7}Ǯrk߳b쌔,ٛs)ĥT|^-%$yR1J] l2Q +h~ᑰ`Eju:ALv- h,<=}{;z`l ԵKFL9D:R~6d_ڛ/zC +(ILO̰ʙyRWJ<lTRrl *mȾӟf'#f8˞y2)dw.L`=fgN ilsNqCA69Lzױ~-1&)}Ⓜ +Z;57GMuI-dBG}<94,*4/}煡= o{0&26:Gu y63ah \瑱>z\xxtPfB{h kq !(#d'!gLYzw ›;9Ȁ^DzEB>»ȁy8pͳ] "{;#ŷaKwN96(@A51:sizGJ+:~%XOG@~ yg^"&{Jtk)TH(l"-#gb. +=H[c;VKT<)!(c>qÃqs57lA x2HL.'0/vG͑\ 4>)e `nܵ93eJO΄@=yu\0CFyU5%.o ^Tk#אB&o]Jr!}KF/pc ˸~z$x8GN4ެ`^)٠w! U/S& x"D̶ c|4h2p 4}m +HWQ x* +w`$o,;q!^B -`=@04. +/ ^,#XiMOc =pk>BUӁ8l_fm^K~7䟓2N>55esN=\ZH%A5&}sX՚uܞ{` :} +yT m>쳔sDzwMmW{w,ChO +(ȹ9ԐrОaģn䃽\m#arS@"l24uÊYm=t`ltb"nm^#n b#Ap/NPG7Kos.ϊc%mh-f= zs|sd#N_/eL6O½pk +=zږq~ktk`o"8ݳMڽT9Zk]+ 1\k7y1<CBaOZ<0n9%)Mxϯ_575\6fnэp垧'({C'{CǦ_?3{P  d: ̱~3Sz芺!Iƛ/_w](m>/|'kh(u؁>+q2us0~aǿtstw>Ȓ5o jԫޢku g{+RGz}9cc9.: aIzGZ[{8.\Cm=3_\5EJSUCn\'&ld[cŠ/*+!eUKew򮬵mL8;ZPݒw>CjyT<9ugZW9e]ܩa@^dM>jV?肥u[X|i(-IZm?Lbm ~Wf^(9@|"'coXV:1 wr~|ȼ]ro>۔^M0VaWu*W$Zʿt"-ŗo +k.(e|ouwj»'9QKg,=7 %uA'2Ro'+0;ÂArJ{%m]Oں3䟧1NR@=L m#0w\Q7q#}%VKew Eru5I?7~Wvܚ{aKMqKDG2[7r5ftrϑS$u}߆Aoꁿ|5}yw(뒹93gъ{u3}StMݐtZUݲ0kYeF/rsYpy-IzgKo&ɑ_~6%$[m{3ʔSo _N X1Qaji,z0M KQzJ]ЃS/课g47o[|k^ѭklFUfseR }A&K}{ _ޒVg Ak]Ewk3ڦ˫}IF:Y _o#aWtSb_k_WV_Pr yFG~ίkxuuхNcu#?딦༩u{Ywwu!\$+1RĿ_ij>^9,{aS.u`h(3VI6uQ"~%%8JH<5όjE6s|c=ѝyiE/-?N1wo-Vݚ^s >)ElunYJr6t3EW4E2R`Yg7:~Tg]ֿ/P=r4;wg_9i0~Ȼ:L8JιH^65*W ƦߍDyg_̄{^`3$%3s#űxxo㴴NW䷟,>U'~9M|;Q/D).>Vk݅ia&}ы~ɒ0!n2?AŊsʬ3}?JW~~^j;#A;7nZ-c<؍-N AH z)n`F v}־j>jo9;{ ͩ% `L\B?'_ %u(\Q}JSq~+;=76OMWt+z'aG 1J:xlFNVJIOK-UՄ?/LnAvo飭~#CKECSV=Ck#缘t0/<ѷd7f_|Ysy ?7c~lĜ:FU[t^YyfU}0J#!W\p蘡@nٟ,~hhV^s(kSwFl9Aq1;c~Գ[=9iQjn͸\t=66\ED^X:G=T/NIC۟s\t±ug` ċ'_~;oj>khH9=M11CO|g>&%^긯3~9\&LV&m gRME2L. P/aj1'eci]sYsso]'5V?W;5`f&*}Aܿu?Y:[^~Lom2X%b>X,މ|Vzu򝪦B3wO5~|L5˖=!~(yqϷGKsK~ɾ$>ҏoYՐyZ>眬*+e^O=UolFF&/: +VpIVC/gVro 0"~ĥi*jmmM Ry1_mt_l:/miÉҧ_藟ߞZU|?BIuK߬]׼O=~q:,_:KN1F(IEwV]twO֋-LM#7t&_s rLH)dd1lkD[?7^7(d}?AM{[#$iu/HK]7_IY? 1Ń{y>qz83q43Izf2m 񅲦"T2>YZRRԹAn c_{,EVytY\iEC$m3Z;0Ƚ[Yǧ@zyl/SrvAlӸ>5ON>Q~M)ڼKCg|џv~F2aE䫟mvc2J+;'ꊧg^KdHucE^W3w_t|Ez.鬾9X {(3kOvLRƦN"#6nXrj꒿_m/9MG|7_[:GIC=WϽڙ}skzO5U]MG!fw.쳍]-lal܃Fm[zWᕝ@.Znڊn +ެ2Ƹig6KdGBL۝(}1[ecεCлj%~ӝ2be 9~[\ |0(.:/@nb.SZ$bqJĹX[n_p6pr lmy9{^};=澶>4Շxm.gFO'-;H!Fl }Iߐ~zw=/>~yIvnYqN&Ja17ua(`wF8Iq 'e.+qs>s,`U>}eUVݜ%PP,{fu0+w{6ËFהuiw 8ynҪ N[PSY!ůs] +.ɱUe+@ |'G|R~`R[ýQ8ޟZ`op8͝f_f%ng l`ѵA 83dP8N%JZr~KK57'xy 25ꕥ/xʙt I?, +|9zzo5}2@,?z./¶(R46HaF%CI;e[1Aܙ9r!ׇ}ǟr_~k}}` t) +'7X$>zpsz{y8ŅS%u ?lQю%s8ewVy:ρ[zngdѶ磶QE7-vg1Q6 ok6Mp̢#-}qG课d>K?^aݲ*Bp j?IIXT5^|DP/MRV-yB,>yg/:yfܪ}iM:yf?J@hD1Jڛ+O *MVӼ5+ n;.g^Ե'S5%ԉ~ZfZ)}\S j+f9rwl] <d^3ܲqvXcXy䔭a«!J7.ߒڈOqC~]Z$FmOU 嚢_ +oч\t@ZFZ Z3~1tߴ͊q}gYEMh #]Eғweg ? 07{ͣWa/TYwm7֦ѫBc#lrkkvY,go1JoW"y#(yX}aXa z/1T$"f{ +?K.i"[K[U(;!LWnO u엛*f[y2rO9#촊vy4۬j]j.Tsgk m&2|i7MM7 a*|`thu1nm}K[ch悛; /$Zp] 򆾡.؍~bc7 Wղ؆jV7;QZ}`[#;ub ;2؝ڗ$ϲlYKɱ>jgBP҂HiwMS,ewqum[]RV̱K.XlrI%;U2XЖevk[9 +OY*"^fF*0-](X a["*=H86vݺKJqww $X>G\ =}G23砢]2rZU?nLЃ/u(k*}9|6?V!Kp%詒^T 16[ҟζߝe ~.K-VE茙ފ-5nWuQ1cpe]c[փm vJ/rr~GF~?VHk@w;yw7ۗ`>3r6Q2)?@7NU#Nr whҋ?> ?0cTL:I(ã! 7d<>JuVԅ [0Ӧ;Yb>U7zYq518,4hGQ9!'g{{$#PG7Ay_i_Yl%Xxmqum 1vt@.OVe#ĝnD~dy?|Vx(|Qȧo^ݽpQJRC{:h2e$A-2b_Vt}Ppib: }>)7Mg?#NDx|.ILfj1n9PQN=6]C _n` j).8Y7:6/'RqE?ry~\nG%<ce,@JJSD9FN: WٕĬW +n-]V\p:ӻ#_?EE}> :EL; xGR_Eʎ9ޏw(98O^c.f:>@PB q_vqzynvIRrkvIK@9^6)~8m{s}{ȯ,mKѱn5&g7ƨ˃I58_qF'"۹oN#SJXiwɿX5`vq+"su;1н g|sbDY¬jX@[C#rrᦜQGԸ# h7Pس;7/^^ +pߏ\: OS塗uO͓ђ TB;\8Pv<4<6xiH (kPǠW/_zv :yp0oorx꞉_r^=m1>eGm\G <KAwB=ιgW.'c[߾Q#.kWxB-"G)z~Fz +I@/]} +wt'@wcܽtt痯ၿkjrȈVY<ᒈ04vc#̊ȋ cq^y +6r"$T|tg,lnS_|L*āA-8L9^pGKW@o|q'cڡJR]͂/G%򮚬*?01GqG*#~s~< +nsrWώ_lG$3bojke I+݀^BbsrNy +xcнKA|`ӫA~/v +w<Ǡq6g?"zI1K}c//{,<%(/<-UA} } \v?ЍsA|/@.EJ˷)y —V !ui >T!Q 㢎~x6(kЋ79>:o@Μ=t4yqWPbR:|e<ۆll#uQQwBC$~E~zwt Ѝ?ggxq΍02_9dRCۙ0&ơbe%uUВ+?Oy%A\I o~t3k޸ s]PЉU/}TD&u"M]Pm'.XrzVrM`YVERWWY}$PPnD/_Oyp(<;T2χn0뙼 yS: `V'ȋ㵘J~ y҉+Y` 76n"V]%d rb7$2Y!26dn +53XѴtH`i=;]+_#G{,/-mɺ*kԢ]P2!mM,.t㗺Ysu%"PWCkwR(ϫ +7`w*)C X%ksɫn ga<:ݥd,P;Cqdn@P(oNu6Jۚ'ꚣĵ|֗6MXa +aSe*UƁ[ Jb'u \؎EXA.0!fr=PcepKKXksfƨ'hbp*s%KCܜ- &٦:Ѧo5jёS L⥤2@Bd6Ķp_Z l&FX3Ϫڝ1|(]i 9%g~O7~fƺtե.bv4D tpLlQp*N 4[U`k!l+,5%N3]+ 2nΎ2/z橮Q?Z+,k m:T/ |؝fq.-'5\⊔ U=xq ㇬B>Ԍ7gllC#\,z̈́[&1uՕfy%q Imɻv,K&nN,"T82&rUJtzKp*ӵ=C]wFXcV2Rb,`u <BсVUq]z v^->ǣ#lJ1[rZKUߴ=Yux8X *S%&A|Z6t>^*x zuWG8%-=0=N%TʶU1w*niSpNuuUZۖ7%1l&3H-2FHEx#J_Dk01A z8_== H;"'~fG?AsƩʊ99kVL-X!;z_!ﴰq[LWε꽳V\ ]Z`mUtCŬe4KWۺ؇al{8 "TKP ܴGXI^ɫ˃mk`U`;r&xtOړoRIYח:aUTW)aM]@K#eʨHᲠ6]5՘`Z{&ߣ)^[^v̰Yځ `v cqz|@*r)&ac`۟f)w[{.Vc虦[l䣶>:qHN%>.A﨩n%){]Sl4ˁ.Ӣ"mJq"L얔%o|>ōzs8S14:Bڶo)yo{ݦ`BgF(ECtݭnt*mrb=jwA跍 bEfl+@ծe/ +iTb +-~V0nu%{G]]QV\rj0[":أpMWm(9^gv環?l6*r^"Qذk{> X`=4SO[{֟ģ@WFH ]q|GOCaނɪ!#+35]kS<$@G(=0j6N Yis"Do˘йNzZ5gO]|`dS|}~ŞW +~ec} (%2 #*>l wN""e$=Z:'vq§; \6Z+ 5+ljR.1IxCEP4cC1{ ?<[oE@XEL v i UIToq[2.Ҧ&mU +mI+"rLU̥N oIIBu ܝts4]MӶ뀄xU-> V6Ц 'v:ןvLR ]7[Ys3b}lCBG ؔL$ȮBUDY`}<-CzvՐJ {Z`Jd@Ccs(&fz :R:EO3ɿ|i!Sv;Tw#ho_dj81mwѮa s!rwdeRٯDdgUS$ +5Jwg*($K90&Rؙٞr{椒`OK*w8a|]uCxMR?4p:YnI9^ jq;_]7/3]G;\_Oql;z&k.U\ֹ\;bp6d,Ȗ%KT-oSg 5=аۿꟀ~vOVnf|!+I?5l)<&)jI{CCZdW)P``jLGtYa^F/->JwH)tf:>CG,wfܱrj?.9A+ٜ , Öa|ԥìr/ ճHH5O&Pa)2abW ]UT|A{["p4`KJ)0l2>eGW]!N]tl?"zconZB]?&:nq8(嗅\ Cw]m@;}=UD$4̩>6 Ns j\t1)kN)2_Xb?a@.}i0Y1Юya^f#gQϒ~V-=*{elo(.z}Q1aWţz,SCWrcvam` jP!L|PJgY̿996ӡa"קhEN]uLm>{=b{Cid/-bkTpML[o"#rHy`aSã9|41$crH0;F&p9ˢy4N|Uc$B)5Q“Mآ+:!FSS%t5bWC)l9XmsfDOKvDT&vE1u v[%}my @. +kqWؼ~ng8l o@,}8Hӄg؆ 1s Oko2oLċց b^XZ-e]JtGI.؞-?LxttCFEwI9:aG] +p`ާOn96l)hnA'p9!Ǥ9-1:jck̻2״-tq)1ď[N]-ŧYF'|uO&> cPQS<kwM,SI*ܖ`3rr}a|R|ztXTY%DfĞw"uU;&9ՔrVx ÷$d-'5̝4e3FRK ˿mL "dlcRRpnv9!<,1\7:BތɥBmrtELHoM@Q6& c r1*뉹y9em"d-Bd^"1b jdUVh~9H'n+I9]%}#ӕ5;?X\zM2_Ԗԭci*n,}p܉N4ܛ*V#A pX-*Y<|#Fr[1:"c>԰j.!fm 7߀X^+{+~Osrx!tr{엶b˽% =q 6aўlˡ%KX=YB^@o##@?鑱6SLueG_Y1y@ " +V%]qC,aEM\VU:dTRWwPEbwٮܧvTuyWjvݚo!v1G<5\6 + O׻ZcR6!nx`96ֶL*0-+tȾIP7]۽6Y%;CWS*i˾Mח~`Wm#<^dD'(8 #j_1_c\"ܩB뻾2še Jz,To+PN%"a{ +=FwiV-Ū @&lN"ᡫ7ll#&8NU]SF\QbDRW ?rW黅撷HfFMy%lrlJOM hqL5k1NG`!+eׇ?jm- `P:ŘG grM>53s;)Q +VMK?Q>ծƜԫc􄳭ߖ:1!v Xg1HS/rJ;Kh_O'*^3BcS8$f}%`s o,xFqC^׵n'z]+곩QD}n.1΁H9EİSZnx/[#: + 쌘r`pE ^CQLwSSKBg-.}}_á;-^`; R#T;'ܱOra1a/v4l0>Q/,_s.O 1lc֋"%tc +~ɐ} qi. JٮP'xt/lCRonݕ!l"VR*dsfX'q]U擅V)s3qh5J&̥M*N(Xsʷ5u 6e%}{ +,g?w!.1zt,l=#U6zf+[FK4e- owFTpHIleX0+>o06XВts{uwTq@NC*k$+>\Aö&Q9, btu%O&93l.zqlm֣KAv-J‹c;r}>ל}GWqX@'LJ[lAL| A")|eБ`1[-q-%+hC@5Ri) ɹrÌ|6I/Xl~$IOMDnEJС3-e MՙoCMYIwgZI{z2x)rOK 1V`. iP59OĜG^-kw@j:-GIQjery x^Bɺߚk~KX%g}}FTe<1 +"OW]20;:6m;z|pk*Ri9)ϕ{2B1~Qc@pZ*d赖sVup)Y"L_3./>Y}{zȑJ#*"(h%+%*Fėǧh!7J 3u~7q@^؜}#rVٖg=Ҳ0V)`mP!^=ૹ4.*dSpheKU;sܺ- 1[QI.-&gkPZ*W==U`qze^LϽ5B.0!#t7oj!H87a8M NN|s|yF[X-z9X6Lәi@ӌ "׏i`՞[桼CoLS%[&(N +;xtU [n|$51Ge?gx_Co!цL@JӖKT5TZ'8ƽͺYdyZ[Rk\K}}upEޥ+\ k!jr+fߟqul쾖w1TfOkoVnFnt!9Ʊ . :6aĜIJ$-ץf*\[OpjK}ۮUCL4 Ir~~ОexЦZ AJVe-M/G2tUӡϷI- 5Jp)vS%x },,v'- |?)=zU{ރ2B(\3.7| h_z:bW@O~V75ZV[\(x1T<ɲslڒ\*Jپ_= ]l"G +.4?[FT(췅H 6)2ZN-UW,' )%,C4ԕ[n+뙘9_L[Ev1;*bC&lbJ%}]mO͹ZJBN"'=96#L|2|WE)ײn56ݡ&m5Uod1~t՞vIy侞U|>E]|M}TP{b/zґ1^9Q=ZҙQ :Wd~^ AՑY_ߜzRmȻ%mW +sn.mRhL|wȪ]035QKq{79$<}U 1G\o i,(:r%fOC@ۅ;ad[Dgcs SCI)>Gy,~6Pn\z*ڥ/Ty|N\Qq35]Fț91k{  ܅ 2fB Ԅ#i?6ۣĂ9'fy CǥڥWt'N]#wlb>^{CDr)i~ߋ:Tj|b]i}5ݖ0Kn% u=ySS]eO И[4RA3~L/SM/ӈ90ṚYL1~-1WW6ƷED>虤FfWE.rK*_mki15UV1;J|!p@2 m 6AHt%NK=%o;w#|pMֆ(|c9CE`2;.ik??UWwВ*|UA-+h?6Xb䱖z}5y3Yv/,] +VG\[} NRs%"Bt(Il]'Ydh:kեacJ­[@zuL,lR/"<ŨXdʹRӕ2rε bE0!E.]:JΜk/ygIc(:ߚb>y04<ɣf#Bǰ1{c̰{ V92a+MVlrmxO6 by?끡!z/ŕH:v~힎MhD *%SUfg׃7Knc=TؼwcȕRw`hF|7;^1잠Y1qb~~BM^G'J)7Sx,qߎ>Nq(yIFWC.I=Eފ]>p=åOkX{`1R<:wT۹gV{LSINX&KBl"RC._!'IإQ7:OC/E]fqQ' +QQ{ vq%n}9Z)+ +^n-b{սN¥'仵TcqJ*jfn1qC룸BS"6.MƃNpNb,nDXWJ/;iuNP![. +|u85h'8rov8Dղ)*9*+$We`+åmvi+nkT}h$Ҏ,_-#@@gܛ՚+x%]n~fXFMvaUthd~+vLc +\n3tmׁ>4q{cMK\h%\(5l+,hsNFdep'dr s]V \ +>&g nĭ öԥ!\tG/hI%Nyw86Ƌ׳^=19n- o M\oGhbz.rC\8;A+Wͅ&鱷ԧ#[ĸ{v\ zAkÐ᲏N4":p{wNS}ׄILႠsov3-w$ytsuO){} ه iξ3ݔ~k<2I-dݓUcKOS4}~ؙw.lJbEOޖ,XQ9|s$mI1K8SCൡxEغ0|Gᖂ9Iʴ(N=`U`SԼ-o.k`ۤ*le4fsU.n,v~ݞ*>u7WM՘ka'̮\Gɛ_Ytlm,gؤoio~cm0re7݃Rrh?gVwwJk[Râ;)Y%FMZ&vxȷ{ُ|=L%PBJ ꛆ~ss,7)fړcNn b7616ѭ%9դ}#O7[6u,45KXPβ6 .fIV'7.xBS=}m쥹+NApОm"JS(KQ61,2Q#$\*sJHeRx䶤C3Kgl` %~vxMuMOz Z!Gڂ˳蘵!b|[R's2=}5Et;IdŁRѮU^#&߭GqNkivjڝUzyoaciց.{cj-|al)yЃIo]Gٖd{I 0]t<si x>:ylwu_ 5LpVfr&dKRukD:Z ccdOwumN9tK_,gߛY%䢽YN;ۏqyӡcz*Zٝ9 ՍۚjᆔT zaݡڡ`BG)IìAZ8!Vo\ +¡Dl!}M͙7p+"ra^Kteն -C&듥q }lRT;0t~l5ŧ ï>9-qnZ{ <L,R6rkS]~tS^dUQ8CVU +b:ɡ`wl 7Mh10WlWb* +xovhgFPKCik7j FaT֒EP?-'f_gdfg}8g`y5N KEu鰹{KtD,YIkS^ }W ޚG Av.FHqUޛvT @m\%"ׇ͢n H%.=3ïvnS*̮mYVI !˫to-4=sWi=Ы)Hꮑ5{NR(n52Q|i 0KuL4ȹ2z9&a M"j);,wOi=s-Ն0 k]ZtSgf`*^ 1Q\ybs]'򂌗v]x A^-(*jmh/~Vl`s8.q|^!C+vh[G-Aڥ5#5׏X'NIqdR+䃘x~3/g եQۣ; ݙT1zO n/v}w0@O2bRAև[ ?{T}=o3vU)_&gOK,te<[~2@~\?^‡-VoWFJS3oOYZrδ-XۚYE$6^ HRPPnpCme8DwЍs @0Kр5'l[i6J;Sigөwꮴ-!] q' w3~>[#s:yYc +y 퀦;LXѡSfA%)U WE1əw,=Q?U95Rp8pq$ ~\{ō)5?S32~⪅ v)[fden%F~\-&3#C. !SB&bqԏ-gՎ+3տqJmD;֣F'֝mm@0ڏJrK|֬ZT$pfb.?򓾥$r8ƥ, s++= ~r&l-¸a+L g8#pMo 3M7dN_& fW>aB1 +]no)QGʛb~j?sd?}~ᢢٖy#xдf:sw-o0Ю:0GcH)_c/Lt6;UC䐒:Սxok~j^s2 |-ׂYy7+D:cp;DE&_|8Ўܸa 5|  <}y–PbwV _Z5g-YbD5 \?&n{BI 0s?a7'2\WzjcmGB㧥gk!}-+jZޔmqyeߧHߦ-,c#b_[z`=pcKt%+r)%PAyl)z#ԤgjB]@?;P@*1zTIk 4[f(/|F1Ψ3 +l/w,ĄKܻ 2PľP"Xyw%r7_zޜY ^s6Th$Uoz8Q.c<S! _j4+)t7D\ThU;·<ƐS6Llz}a_M* \AȍCq6sMԆ/5GSɶRLE]Jir]9Yj>5#zYO=rm;G$PFJ+&cۂJ942?.^I(y>D%LI{'(=VBFkI:׉=Rg%.jI^ɕ5"aNd}:N㤁JlaI3CKrBꟓ}OX][𬁔迋m`C|r5Ң*|Ekǟm)O7 hnB[mA'AyWț~`_S=>|0^[]Yaɫjxhk ;ٵcxG%˟lazV-輙{G/L.PG,gDc16lʣ>QA΍]U! AF)"flNz=LmvP 4ct? g߳E5V +@3]^=gc1!eC,/o˻}85 Ag>̿u#@?>mU+h-Y;.6&|3¾,i˻1^I*/$^ᄸ?%*dQ(?1 .ɥ@K4#7l ;i*pݝ`RCn*l)|pZسeGxh)25H q]fNo+e/C]'i7 \V؇KPɧוo!IGeWiEMdoG=Ewni4atAwV١O߲3wVg;7߇Ʈ*e㭠zLn=侃]x%({5ni2"75G9$sJl]T/_{e)vFS@>,k.B\Ua—;^,+P_|mR8_GɠaBْ 'xk2^vQOyk)==^h/Z|@ +=Sygԧh`+\˃$Ii4hK7?so? ؏ Tٞ2]\w|vem⠁e8{b,f&Ս>\DnH[tֶ m#6: vvEYC6uVX S!{C?6 vC}ܴs2| G%Z]τGWO* ~vaį kp -hj%~ LPs0r]g_CixҊ̾u)" "1愓|I0ï] mW9E-5?\=߶<:f:7d8GnB,4RJ DM-̺=䳒?aizE7c-ؒo8n>`Xf/)NtWɤm۠ G'+a][V#̸U&?l +T'f_W;9Ƚ>*z@*h"K6= t{_TZ13vdfm:sG5]g`~t?O8T8riw]5N(S,M}u'_3\3ዝӜ·Ќ~. }C<7hgi3)6OC˪.^=IP^? N]&h\ܞOٰJV5褐? @Gӎ؅޺Y m nN K'hkm3~Hи kC_kjw=ͣFDWڲC 6̨#Mpдl1ZrTO ;;㜁~N6xcfeW-M/W4GjT𓒺wg'jv>.+93JZya:ĭ_Q!6 u hXbT8 +hj(`_U=XB|*m%Uŝ 0;Jvp*)|[VbE 6g[!E+frzʌa%OIs>{ϣ0)dw=q^jRRrIdY2Bgvq(1vn_VZyrPp)\X9O}WԄnPXP4wvW>?3R鋛M%5.` ZRɊ)N& endstream endobj 8 0 obj [7 0 R 6 0 R] endobj 50 0 obj <> endobj xref 0 51 0000000000 65535 f +0000000016 00000 n +0000000161 00000 n +0000047552 00000 n +0000000000 00000 f +0000049891 00000 n +0000049511 00000 n +0000049585 00000 n +0001663591 00000 n +0000047603 00000 n +0000048002 00000 n +0000495496 00000 n +0000113801 00000 n +0000113688 00000 n +0000048569 00000 n +0000048950 00000 n +0000048998 00000 n +0000049775 00000 n +0000049806 00000 n +0000049659 00000 n +0000049690 00000 n +0000050027 00000 n +0000050052 00000 n +0000050436 00000 n +0000050697 00000 n +0000050766 00000 n +0000051037 00000 n +0000051125 00000 n +0000113836 00000 n +0000495570 00000 n +0000496129 00000 n +0000497454 00000 n +0000502818 00000 n +0000568406 00000 n +0000633994 00000 n +0000699582 00000 n +0000765170 00000 n +0000830758 00000 n +0000896346 00000 n +0000961934 00000 n +0001027522 00000 n +0001093110 00000 n +0001138887 00000 n +0001204475 00000 n +0001270063 00000 n +0001335651 00000 n +0001401239 00000 n +0001466827 00000 n +0001532415 00000 n +0001598003 00000 n +0001663620 00000 n +trailer <<3663182427704D6BA6D0BFF289E29A48>]>> startxref 1663820 %%EOF \ No newline at end of file diff --git a/versions/4.0/zh/physics-2d/image/raycasting-output.png b/versions/4.0/zh/physics-2d/image/raycasting-output.png new file mode 100644 index 0000000000..d39d1a66e5 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/raycasting-output.png differ diff --git a/versions/4.0/zh/physics-2d/image/regenerate-points.png b/versions/4.0/zh/physics-2d/image/regenerate-points.png new file mode 100644 index 0000000000..857a26544c Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/regenerate-points.png differ diff --git a/versions/4.0/zh/physics-2d/image/relative-joint.gif b/versions/4.0/zh/physics-2d/image/relative-joint.gif new file mode 100644 index 0000000000..5ce163d1ab Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/relative-joint.gif differ diff --git a/versions/4.0/zh/physics-2d/image/relative-joint.png b/versions/4.0/zh/physics-2d/image/relative-joint.png new file mode 100644 index 0000000000..509003eb8d Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/relative-joint.png differ diff --git a/versions/4.0/zh/physics-2d/image/rigidbody-2d.png b/versions/4.0/zh/physics-2d/image/rigidbody-2d.png new file mode 100644 index 0000000000..a383d0970e Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/rigidbody-2d.png differ diff --git a/versions/4.0/zh/physics-2d/image/rigidbody-type.png b/versions/4.0/zh/physics-2d/image/rigidbody-type.png new file mode 100644 index 0000000000..c46c7e06fb Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/rigidbody-type.png differ diff --git a/versions/4.0/zh/physics-2d/image/slider-joint.gif b/versions/4.0/zh/physics-2d/image/slider-joint.gif new file mode 100644 index 0000000000..9ddbd11eb5 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/slider-joint.gif differ diff --git a/versions/4.0/zh/physics-2d/image/slider-joint.png b/versions/4.0/zh/physics-2d/image/slider-joint.png new file mode 100644 index 0000000000..616ddac01e Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/slider-joint.png differ diff --git a/versions/4.0/zh/physics-2d/image/spring-joint.gif b/versions/4.0/zh/physics-2d/image/spring-joint.gif new file mode 100644 index 0000000000..3965e87877 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/spring-joint.gif differ diff --git a/versions/4.0/zh/physics-2d/image/spring-joint.png b/versions/4.0/zh/physics-2d/image/spring-joint.png new file mode 100644 index 0000000000..a7505970ff Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/spring-joint.png differ diff --git a/versions/4.0/zh/physics-2d/image/threshold-1.png b/versions/4.0/zh/physics-2d/image/threshold-1.png new file mode 100644 index 0000000000..96e7324e98 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/threshold-1.png differ diff --git a/versions/4.0/zh/physics-2d/image/wheel-joint.gif b/versions/4.0/zh/physics-2d/image/wheel-joint.gif new file mode 100644 index 0000000000..d0c3d62366 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/wheel-joint.gif differ diff --git a/versions/4.0/zh/physics-2d/image/wheel-joint.png b/versions/4.0/zh/physics-2d/image/wheel-joint.png new file mode 100644 index 0000000000..03df0c7157 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/wheel-joint.png differ diff --git a/versions/4.0/zh/physics-2d/image/world-manifold-normal-2.png b/versions/4.0/zh/physics-2d/image/world-manifold-normal-2.png new file mode 100644 index 0000000000..1a28862d0b Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/world-manifold-normal-2.png differ diff --git a/versions/4.0/zh/physics-2d/image/world-manifold-normal.png b/versions/4.0/zh/physics-2d/image/world-manifold-normal.png new file mode 100644 index 0000000000..cd4f019e73 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/world-manifold-normal.png differ diff --git a/versions/4.0/zh/physics-2d/image/world-manifold-points.png b/versions/4.0/zh/physics-2d/image/world-manifold-points.png new file mode 100644 index 0000000000..57b1c99ab0 Binary files /dev/null and b/versions/4.0/zh/physics-2d/image/world-manifold-points.png differ diff --git a/versions/4.0/zh/physics-2d/physics-2d-collider.md b/versions/4.0/zh/physics-2d/physics-2d-collider.md new file mode 100644 index 0000000000..575a679df6 --- /dev/null +++ b/versions/4.0/zh/physics-2d/physics-2d-collider.md @@ -0,0 +1,122 @@ +# 2D 碰撞组件 + +## 添加碰撞组件 + +目前引擎支持三种不同的碰撞组件: **盒碰撞组件(BoxCollider2D)**、**圆形碰撞组件(CircleCollider2D)** 和 **多边形碰撞组件(PolygonCollider2D)**。在 **属性检查器** 上点击 **添加组件** 按钮,输入碰撞组件的名称即可添加。 + +![碰撞组件](image/collider-types.png) + +## 属性 + +不同的碰撞组件都拥有以下的共同属性: + +![inspector](image/collider-inspector.png) + +| 属性 | 说明 | +|:-- | :-- | +| **Editing** | 是否对碰撞组件进行编辑。勾选后可以在场景内编辑碰撞组件的位置、样式和大小。详情请参考下方 **编辑碰撞组件** | +| **Tag** | 标签。当发生碰撞后,可根据 **Tag** 区分不同碰撞组件 | +| **Group** | 碰撞组件分组。通过 [碰撞矩阵](../editor/project/physics-configs.md) 可设置不同分组间碰撞的可能性 | +| **Sensor** | 指明碰撞组件是否为传感器类型,传感器类型的碰撞组件会产生碰撞回调,但是不会发生物理碰撞效果。(一般和rigidbody2d一起使用时,开启Enabled Contact Listener可以监听碰撞的回调事件。如:onBeginContact,onEndContact, 通过搜索文档可以了解如何监听这两个回调事件) | +| **Density** | 碰撞组件的密度,用于刚体的质量计算 | +| **Friction** | 碰撞组件摩擦力系数,碰撞组件接触时的运动会受到摩擦力影响 | +| **Restitution**| 碰撞组件的弹性系数,指明碰撞组件碰撞时是否会受到弹力影响 | +| **Offset** | 碰撞组件相对于节点中心的偏移 | + +## 盒物理碰撞组件 + +盒碰撞组件是常见的碰撞组件,用于模拟类矩形的碰撞组件。在 2D 节点的 **属性检查器** 上点击 **添加组件 -> BoxCollider2D** 即可添加。 + +![box-collider-2d](image/box-colllider-2d.png) + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **Size** | 盒碰撞组件的大小 | + +盒碰撞组件接口请参考 [BoxCollider2D API](%__APIDOC__%/zh/class/BoxCollider2D)。 + +### 编辑碰撞组件 + +对于所有碰撞组件,勾选 **Editing** 后都可在场景内进行编辑。 + +![editing](image/editing.png) + +在碰撞组件范围内按下鼠标左键并拖拽可以调整碰撞组件的偏移,在 ![gizmo](image/gizmo.png) 上按下鼠标左键并拖拽可以调整碰撞组件的形状和大小。 + +![edit-box-collider](image/edit-box.gif) + +按住 Alt 按键拖拽时,在拖拽过程中将会保持 **矩形中心位置** 不变。 + +![alt](image/edit-box-alt.gif) + +在 **属性检查器** 上输入也可以精细化的调整碰撞组件的大小和偏移。 + +![input](image/edit-input.gif) + +## 圆形碰撞组件 + +在 **属性检查器** 上点击 **添加组件**,输入 CircleCollider2D 可添加圆形碰撞组件。 + +![circle-collider](image/circle-collider.png) + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **Radius** | 圆形的半径 | + +圆形碰撞组件接口请参考 [CircleCollider2D API](%__APIDOC__%/zh/class/CircleCollider2D)。 + +### 编辑碰撞组件 + +按住 Alt 按键拖拽时,在拖拽过程中将会保持 **圆心位置** 不变。 + +![edit-circle-collider](image/edit-circle.gif) + +## 多边形碰撞组件 + +通过多边形碰撞组件,可以编辑更为详细的物理形状,从而更准确的对物体进行物理模拟。 + +在 **属性检查器** 上点击 **添加组件**,输入 PolygonCollider2D 可添加多边形碰撞组件。 + +![polygon](image/polygon-collider.png) + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **Threshold** | 指明生成贴图轮廓顶点间的最小距离,值越大则生成的点越少,可根据需求进行调节 | +| **Points** | 多边形的顶点,顶点可以通过勾选 **Editing** 在场景内进行编辑,也可以在 **属性检查器** 上输入数值来调整 | + +多边形碰撞组件接口请参考 [PolygonCollider2D API](%__APIDOC__%/zh/class/PolygonCollider2D)。 + +### 编辑碰撞组件 + +如果是 Sprite 组件,引擎会根据 Sprite 生成轮廓: + +![default](image/polygon-default.png) + +通过鼠标拖拽 ![gizmo](image/gizmo.png) 可以调整轮廓点的位置。 + +![edit](image/edit-polygon.gif) + +通过调整 **Threshold** 并点击 ![points](image/btn-regenerate-points.png) 按钮,可以调整轮廓的形状和点的数量。 + +![threshold-1](image/threshold-1.png) + +鼠标移到多边形的线段上时,该线段会高亮并且鼠标会变为添加样式,此时点击鼠标左键可在线段中插入新的点。 + +![add-point](image/polygon-add-point.gif) + +## 详细说明 + +Box2D 物理碰撞组件内部是由 Box2D 的 b2Fixture 组成的,由于 Box2D 内部的一些限制,一个多边形物理碰撞组件可能会由多个 b2Fixture 组成。 + +这些情况为: + +1. 当多边形物理碰撞组件的顶点组成的形状为凹边形时,物理系统会自动将这些顶点分割为多个凸边形。但需要注意的是该算法无法处理凹多边形自相交的问题。 +2. 当多边形物理碰撞组件的顶点数多于 `b2.maxPolygonVertices`(一般为 8)时,物理系统会自动将这些顶点分割为多个凸边形。 + +一般情况下这些细节是不需要关心的,但是当使用射线检测并且检测类型为 `ERaycast2DType.All` 时,一个碰撞组件就可能会检测到多个碰撞点,原因即是检测到了多个 b2Fixture。 diff --git a/versions/4.0/zh/physics-2d/physics-2d-contact-callback.md b/versions/4.0/zh/physics-2d/physics-2d-contact-callback.md new file mode 100644 index 0000000000..e6ae5e789f --- /dev/null +++ b/versions/4.0/zh/physics-2d/physics-2d-contact-callback.md @@ -0,0 +1,187 @@ +# 2D 碰撞回调 + +当物体在场景中移动并碰撞到其它物体时,物理引擎会处理大部分必要的碰撞检测,我们一般不需要关心这些情况。但是制作物理游戏最主要的点是有些情况下物体碰撞后应该发生些什么,比如角色碰到怪物后会死亡,或者球在地上弹动时应该产生声音等。 + +我们需要一个方式来获取到这些碰撞信息,物理引擎提供的方式是在碰撞发生时产生回调,在回调里我们可以根据产生碰撞的两个碰撞体的类型信息来判断需要作出什么样的动作。 + +> **注意**: +> 1. **Box2D** 物理模块需要先在 [Rigidbody](physics-2d-rigid-body.md) 中 **开启碰撞监听**,才会有相应的回调产生。开启方法为,在 **Rigidbody2D** 的 **属性检查器** 勾选 **EnabledContactListener** 属性,如下图所示: +> ![开启碰撞回调](image/enable-contact.png) +> +> **Builtin** 物理模块只需要有碰撞体组件就可以产生碰撞回调。 +> 2. 回调中的信息在物理引擎都是以缓存的形式存在的,所以信息只有在这个回调中才是有用的, 不要在你的脚本里直接缓存这些信息,但可以缓存这些信息的副本。 +> 3. 在回调中创建的物理物体,比如刚体,关节等,这些不会立刻就创建出对应的物体,会在整个物理系统更新完成后再进行这些物体的创建。 + +## 注册回调函数 + +注册一个碰撞回调函数有两种方式,一种是通过指定的 collider 注册;另一种是通过 2D 物理系统注册一个全局的回调函数。 + +**注意**:Builtin 2D 物理模块只会发送 `BEGIN_CONTACT` 和 `END_CONTACT` 回调消息。 + +```js +@ccclass('TestContactCallBack') +export class TestContactCallBack extends Component { + start () { + // 注册单个碰撞体的回调函数 + let collider = this.getComponent(Collider2D); + if (collider) { + collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); + collider.on(Contact2DType.END_CONTACT, this.onEndContact, this); + } + + // 注册全局碰撞回调函数 + if (PhysicsSystem2D.instance) { + PhysicsSystem2D.instance.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); + PhysicsSystem2D.instance.on(Contact2DType.END_CONTACT, this.onEndContact, this); + } + } + onBeginContact (selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { + // 只在两个碰撞体开始接触时被调用一次 + console.log('onBeginContact'); + } + onEndContact (selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { + // 只在两个碰撞体结束接触时被调用一次 + console.log('onEndContact'); + } + onPreSolve (selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { + // 每次将要处理碰撞体接触逻辑时被调用 + console.log('onPreSolve'); + } + onPostSolve (selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { + // 每次处理完碰撞体接触逻辑时被调用 + console.log('onPostSolve'); + } +} + +``` + +上面的代码示例演示了如何在脚本中添加所有的碰撞回调函数。回调一共有四种,每种回调函数都有三个参数,详情可查看下方的 **回调参数** 说明。每种回调函数的作用如注释所示,开发者可以根据自己的需求实现相应的回调函数。 + +## Box2D 物理模块碰撞回调的顺序 + +Box2D 物理模块模拟碰撞的过程是比较复杂的,我们可以通过拆分一个简单示例的碰撞过程来查看碰撞回调函数的回调顺序和回调的时机。假设有两个刚体正相互移动,三角形往右运动,方块往左运动,它们即将碰撞到一起。 + +![anatomy-aabbs](./image/anatomy-aabbs.png) + + + + +碰撞的过程 + +
+ + + + + +
+碰撞 1
+ +
+碰撞 2
+ +
+碰撞 3
+ +
+ 当两个碰撞体相互覆盖时,Box2D 默认的行为是给每个碰撞体一个冲量将它们分开,但是这个行为不一定能在一个物理周期内完成。像这里显示的一样,示例中的碰撞体会在三个物理周期内相互覆盖直到“反弹”完成并且它们相互分离。在这个时间里可以定制我们想要的行为,onPreSolve 会在每次物理引擎处理碰撞前回调,可以在这个回调里修改碰撞信息; onPostSolve 会在处理完成这次碰撞后回调,可以在这个回调中获取到物理引擎计算出的碰撞的冲量信息。

+ 下面给出的输出信息能使我们更清楚回调的顺序。 +
        ...
+    Step
+    Step
+    BeginContact
+    PreSolve
+    PostSolve
+    Step
+    PreSolve
+    PostSolve
+    Step
+    PreSolve
+    PostSolve
+    Step
+    EndContact
+    Step
+    Step
+    ...
+
+
+ +## 回调的参数 + +回调的参数包含了所有的碰撞接触信息,每个回调函数都提供了三个参数: + +- **selfCollider**:指的是回调脚本的节点上的碰撞体; +- **otherCollider**:指的是发生碰撞的另一个碰撞体; +- **contact**:是一个 `IPhysics2DContact`。包含碰撞最主要的信息。其中比较常用的信息就是碰撞的位置和法向量,`contact` 内部是按照刚体的本地坐标来存储信息的,而我们一般需要的是世界坐标系下的信息,我们可以通过 `contact.getWorldManifold` 来获取这些信息。注意,在 Builtin 物理模块这个参数为空。 + +### worldManifold + +```ts +const worldManifold = contact.getWorldManifold(); +const points = worldManifold.points; +const normal = worldManifold.normal; +``` + +`worldManifold` 包括以下成员: + +- points + + 碰撞点数组,它们不一定会精确地在碰撞体碰撞的地方上,如下图所示(除非你将刚体设置为子弹类型,但是会比较耗性能),但实际上这些点在使用时一般都是够用的。 + + ![world-manifold-points](./image/world-manifold-points.png) + + **注意**:不是每一个碰撞都会有两个碰撞点,在模拟的更多的情况下只会产生一个碰撞点,下面列举一些其它的碰撞示例。 + + ![collision-points-1](./image/collision-points-1.png) + + ![collision-points-2](./image/collision-points-2.png) + + ![collision-points-3](./image/collision-points-3.png) + +- normal + + 碰撞点上的法向量,由自身碰撞体指向对方碰撞体,指明解决碰撞最快的方向。 + + ![world-manifold-normal](./image/world-manifold-normal.png) + + 上图所示的线条即碰撞点上的法向量,在这个碰撞中,解决碰撞最快的途径是添加冲量将三角形往左上推,将方块往右下推。需要注意的是这里的法向量只是一个方向,并不带有位置属性,也不会连接到这些碰撞点中的任何一个。 + + 你还需要注意的是 **碰撞法向量并不是碰撞体碰撞的角度**,它只会指明可以解决两个碰撞体相互覆盖这一问题最短的方向。比如上面的例子中如果三角形移动得更快一点,覆盖的情形像下图所示的话: + + ![world-manifold-normal-2](./image/world-manifold-normal-2.png) + + 那么最短的方式将会是把三角形往右上推,所以使用法向量来作为碰撞角度不是一个好主意。如果想要知道碰撞的真正方向,可以使用以下方式获取两个碰撞体相互碰撞时在碰撞点上的相对速度。 + + ```ts + const vel1 = triangleBody.getLinearVelocityFromWorldPoint(worldManifold.points[0]); + const vel2 = squareBody.getLinearVelocityFromWorldPoint(worldManifold.points[0]); + const relativeVelocity = vel1.sub(vel2); + ``` + +### 禁用 contact + +```ts +contact.disabled = true; +``` + +禁用 contact 会使物理引擎在计算碰撞时会忽略掉这次碰撞,禁用将会持续到碰撞完成,除非在其它回调中再将这个 contact 启用。 + +或者如果只想在本次物理处理步骤中禁用 contact,可以使用 `disabledOnce`。 + +```ts +contact.disabledOnce = true; +``` + +### 修改 contact 信息 + +前面有提到我们在 **onPreSolve** 中修改 contact 的信息,因为 **onPreSolve** 是在物理引擎处理碰撞信息前回调的,所以对碰撞信息的修改会影响到后面的碰撞计算。 + +```ts +// 修改碰撞体间的摩擦力 +contact.setFriction(friction); + +// 修改碰撞体间的弹性系数 +contact.setRestitution(restitution); +``` + +注意:这些修改只会在本次物理处理步骤中生效。 diff --git a/versions/4.0/zh/physics-2d/physics-2d-joint.md b/versions/4.0/zh/physics-2d/physics-2d-joint.md new file mode 100644 index 0000000000..6eee34008f --- /dev/null +++ b/versions/4.0/zh/physics-2d/physics-2d-joint.md @@ -0,0 +1,189 @@ +# 2D 关节组件 + +物理系统包含了一系列用于链接两个刚体的关节组件。关节组件可以用来模拟真实世界物体间的交互,比如铰链,活塞、绳子、轮子、滑轮、机动车、链条等。学习如何使用关节组件可以创建一个真实有趣的场景。 + +> **注意**: +> 1. 关节组件在 Builtin 2D 物理模块中是无效的。 +> 2. 关节组件都需要搭配 [刚体](./physics-2d-rigid-body.md) 和 [碰撞组件](./physics-2d-collider.md) 才可以正确运行。如下图所示: +> +> ![with-body](image/joint-with-body-collider.png) + +## 添加关节 + +在 **属性检查器** 上点击 **添加组件** 按钮并输入 2D 关节的组件名。 + +![add-joint](image/add-joint.png) + +## 关节的共用属性 + +虽然每种关节都有不同的表现,但是它们也有一些共同的属性: + +| 属性 | 说明 | +| :-- | :-- | +| **ConnectedBody** | 关节链接的另一端的刚体
**注意**:在 v3.7 版本中,如该属性没有被设置,则会默认取原点 [0, 0],以与 **3D 物理** 中的约束在实现上统一
由于目前 2D 世界的坐标原点默认取 Canvas 所在的左下角,此时关节的 **ConnectedBody** 会连接到左下角。如果觉得这样对开发不太方便,可以通过将 Canvas 进行偏移的方式来处理; 而由于 Canvas 组件本身无法修改其位置信息,可以在通过给 Canvas 组件所在的节点添加父节点并偏移父节点来解决 | +| **Anchor** | 关节本端链接的刚体的锚点 | +| **ConnectedAnchor** | 关节链接另一端刚体的锚点 | +| **CollideConnected** | 关节两端的刚体是否能够互相碰撞 | + +每个关节都需要链接两个刚体才能够发挥它的功能,我们把和关节挂在同一节点下的刚体视为关节的本端,把 **ConnectedBody** 视为另一端的刚体。通常情况下,每个刚体会选取自身周围的某个位置设定成锚点。根据关节组件类型的不同,锚点决定了物体的旋转中心,或者是用来保持一定距离的坐标点,等等。 + +**CollideConnected** 属性用于确定关节两端的刚体是否需要继续遵循常规的碰撞规则。 + +如果你现在准备制作一个布娃娃,你可能会希望大腿和小腿能够部分重合,然后在膝盖处链接到一起,那么就需要设置 **CollideConnected** 属性为 false。如果你准备做一个升降机,希望升降机平台和地板能够碰撞,那么就需要设置 **CollideConnected** 属性为 true。 + +## 调整锚点 + +在场景中可以通过点击并拖拽 ![gizmo-a](image/gizmo-a.png) 来调整 **Anchor** 属性,通过 ![gizmo-b](image/gizmo-b.png) 来调整 **ConnectedAnchor**。 + +![gizmo](image/distance-joint-gizmo.png) + +## 距离关节 + +在 **属性检查器** 上点击 **添加组件** 并输入 **DistanceJoint2D** 则可以添加距离关节。 + +![distance-joint](image/distance-joint-dis.png) + +**距离关节(Distance Joint)** 会将关节两端的刚体约束在一个最大范围内。超出该范围时,刚体的运动会互相影响。 + +![distance-joint](image/distance-joint.gif) + +低于该范围则不会互相影响。 + +![dis](image/distance-joint-dis.gif) + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **MaxLength** | 最大距离 | +| **AutoCalcDistance** | 是否自动计算关节连接的两个刚体间的距离 | + +## 固定关节 + +在 **属性检查器** 上点击 **添加组件** 并输入 **FixedJoint2D** 则可以添加固定关节。 + +![fixed](image/fixed-joint.png) + +**固定关节(Fixed Joint)**,根据两个物体的初始角度将两个物体上的两个点固定在一起。 + +![fixed](image/fixed-joint.gif) + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **Frequency** | 弹性系数 | +| **DampingRatio** | 阻尼,表示关节变形后,恢复到初始状态受到的阻力 | + +## 铰链关节 + +在 **属性检查器** 上点击 **添加组件** 并输入 **HingeJoint2D** 则可以添加铰链关节。 + +![hinge](image/hinge-joint.png) + +**铰链关节(Hinge Joint)**,可以看做一个铰链或者钉,刚体会围绕一个共同点来旋转。 + +![hinge](image/hinge-joint.gif) + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **EnableLimit** | 是否开启关节的限制 | +| **LowerAngle** | 角度的最低限制 | +| **UpperAngle** | 角度的最高限制 | +| **EnableMotor** | 是否开启关节马达 | +| **MaxMotorTorque** | 可以施加到刚体的最大扭矩 | +| **MotorSpeed** | 期望的马达速度 | + +启用马达速度后,关节连接的刚体会尝试逐渐加速到期望速度。 + +![speed](image/motor-speed.png) + +![motor](image/hinge-joint-motor.gif) + +## 相对关节 + +在 **属性检查器** 上点击 **添加组件** 并输入 **RelativeJoint2D** 则可以添加相对关节。 + +![relative](image/relative-joint.png) + +**相对关节(Relative Joint)**,控制两个刚体间的相对运动。 + +![relative](image/relative-joint.gif) + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **MaxForce** | 可以应用于刚体的最大的力值 | +| **MaxTorque** | 可以应用于刚体的最大扭矩值 | +| **CorrectionFactor** | 位置矫正系数,范围为 [0, 1] | +| **LinearOffset** | 关节另一端的刚体相对于起始端刚体的位置偏移量 +| **AngularOffset** | 关节另一端的刚体相对于起始端刚体的角度偏移量 +| **AutoCalcOffset** | 自动计算关节连接的两个刚体间的 angularOffset 和 linearOffset | + +## 滑动关节 + +在 **属性检查器** 上点击 **添加组件** 并输入 **SliderJoint2D** 则可以添加滑动关节。 + +![slider](image/slider-joint.png) + +**滑动关节(Slider Joint)**,两个刚体位置间的角度是固定的,它们只能在一个指定的轴上滑动。如下图蓝色方块虽然受重力影响,但实际上由于关节的约束,只能沿设定的方向运动。 + +![slider](image/slider-joint.gif) + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **Angle** | 滑动的方向 | +| **AutoCalcAngle** | 根据连接的两个刚体自动计算滑动方向 | +| **EnableMotor** | 是否开启关节马达 | +| **MaxMotorForce** | 可以施加到刚体的最大力 | +| **MotorSpeed** | 期望的马达速度 | +| **EnableLimit** | 是否开启关节的距离限制 | +| **LowerLimit** | 刚体能够移动的最小值 | +| **UpperLimit** | 刚体能够移动的最大值 | + +## 弹簧关节 + +在 **属性检查器** 上点击 **添加组件** 并输入 **SpringJoint2D** 则可以添加弹簧关节。 + +![spring](image/spring-joint.png) + +**弹簧关节(Spring Joint)**,将关节两端物体像弹簧一样连接在一起。 + +![spring](image/spring-joint.gif) + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **Frequency** | 弹性系数 | +| **DamingRatio** | 阻尼,表示关节变形后,恢复到初始状态受到的阻力 | +| **Distance** | 关节两端的距离 | +| **AutoCalcDistance** | 自动计算关节连接的两个刚体间的距离 | + +## 轮子关节 + +在 **属性检查器** 上点击 **添加组件** 并输入 **WheelJoint2D** 则可以添加轮子关节。 + +![wheel](image/wheel-joint.png) + +**轮子关节(Wheel Joint)**,用于模拟机动车车轮。 + +![wheel](image/wheel-joint.gif) + +在模拟现实中四驱的情况下,可以考虑将前后轮的轮子关节都启用 **EnableMotor**,而非四驱的情况下可以考虑只启用驱动轮的 **EnableMotor**。 图示中仅前轮开启了 **EnableMotor**,从而模拟前驱的情况。 + +### 属性 + +| 属性 | 说明 | +| :-- | :-- | +| **Angle** | 轮子震动方向 | +| **EnableMotor** | 是否开启关节马达 | +| **MaxMotorTorque** | 可以施加到刚体的最大扭矩 | +| **MotorSpeed** | 期望的马达速度 | +| **Frequency** | 弹性系数 | +| **DampingRatio** | 阻尼,表示关节变形后,恢复到初始状态受到的阻力 | diff --git a/versions/4.0/zh/physics-2d/physics-2d-rigid-body.md b/versions/4.0/zh/physics-2d/physics-2d-rigid-body.md new file mode 100644 index 0000000000..8e56d3eaf9 --- /dev/null +++ b/versions/4.0/zh/physics-2d/physics-2d-rigid-body.md @@ -0,0 +1,256 @@ +# 2D 刚体 + +刚体是组成物理世界的基本对象,可以将刚体想象成一个你不能看到(绘制)也不能摸到(碰撞)的且不能变形的物体。 + +由于 Builtin 2D 物理系统只带有碰撞检测的功能,所以刚体对于 Builtin 2D 物理系统是不生效的,本篇设置只对 Box 2D 物理系统产生作用。 + +## 添加刚体 + +点击 **属性检查器** 的 **添加组件** 按钮,输入 Rigidbody2D 即可以添加 2D 刚体组件。 + +![add-rigid](image/add-rigid.png) + +## 属性说明 + +![rigidbody-2d](image/rigidbody-2d.png) + +| 属性 | 说明 | +| :-- | :-- | +| **Group** | 刚体的分组。通过 [碰撞矩阵](../editor/project/physics-configs.md) 可设置不同分组间碰撞的可能性| +| **EnabledContactListener** | 开启监听[碰撞回调](./physics-2d-contact-callback.md) | +| **Bullet** | 这个刚体是否是一个快速移动的刚体,并且需要禁止穿过其他快速移动的刚体
请参考 [Rigidbody2D API](%__APIDOC__%/zh/class/RigidBody2D) 获取更多信息 | +| **Type** | 刚体类型,详情请参考下方 **刚体类型** | +| **AllowSleep** | 是否允许刚体休眠
[物理配置](../editor/project/physics-configs.md) 中可调整休眠的临界值 | +| **GravityScale** | 重力缩放比例
仅对 **Dynamic** 类型的刚体生效 | +| **LinearDamping** | 移动速度衰减系数 | +| **AngularDamping** | 旋转速度衰减系数 | +| **LinearVelocity** | 移动速度
仅对 **Dynamic** 和 **Kinematic** 类型的刚体生效 | +| **AngularVelocity** | 旋转速度
仅对 **Dynamic** 和 **Kinematic** 类型的刚体生效 | +| **FixedRotation** | 是否固定旋转 | +| **AwakeOnLoad** | 加载完成后立刻唤醒刚体 | + +刚体组件接口请参考 [Rigidbody2D API](%__APIDOC__%/zh/class/RigidBody2D)。 + +## 刚体属性 + +### 质量 + +刚体的质量是通过 [碰撞组件](physics-2d-collider.md) 的 **密度** 与 **大小** 自动计算得到的。若需要计算物体应该受到多大的力,可能需要使用到这个属性。 + +```ts +// 获取刚体质量 +const mass = rigidbody.getMass(); +``` + +### 移动速度 + +```ts +// 获取移动速度 +const velocity = rigidbody.linearVelocity; +// 设置移动速度 +rigidbody.linearVelocity = velocity; +``` + +移动速度衰减系数,值越大物体移动越慢,可以用来模拟空气摩擦力等效果。 + +```ts +// 获取移动速度衰减系数 +const damping = rigidbody.linearDamping; +// 设置移动速度衰减系数 +rigidbody.linearDamping = damping; +``` + +如果要获取刚体上某个点的移动速度,可以通过 `getLinearVelocityFromWorldPoint` 来获取。比如一个盒子旋转着往前飞,碰到了墙,这时候可能会希望获取盒子在发生碰撞的点的速度。 + +```ts +const velocity = rigidbody.getLinearVelocityFromWorldPoint(worldPoint); +``` + +或者传入一个 `Vec2` 对象作为第二个参数来接收返回值,这样你可以使用你的缓存对象来接收这个值,避免创建过多的对象来提高效率。 + +**刚体的 get 方法都提供了 out 参数来接收函数返回值。** + +```ts +const velocity = new Vec2(); +rigidbody.getLinearVelocityFromWorldPoint(worldPoint, velocity); +``` + +### 旋转速度 + +```ts +// 获取旋转速度 +const velocity = rigidbody.angularVelocity; +// 设置旋转速度 +rigidbody.angularVelocity = velocity; +``` + +旋转速度衰减系数,与移动衰减系数相同。 + +```ts +// 获取旋转速度衰减系数 +const damping = rigidbody.angularDamping; +// 设置旋转速度衰减系数 +rigidbody.angularDamping = damping; +``` + +### 旋转、位移与缩放 + +旋转、位移与缩放是游戏开发中最常用的功能,几乎每个节点都会对这些属性进行设置。而在物理系统中,系统会自动将节点的这些属性与 Box2D 中对应属性进行同步。 + +> **注意**: +> 1. Box2D 中只有旋转和位移,并没有缩放,所以如果设置节点的缩放属性时,会重新构建这个刚体依赖的全部碰撞体。一个有效避免这种情况发生的方式是将渲染的节点作为刚体节点的子节点,只对这个渲染节点作缩放,尽量避免对刚体节点进行直接缩放。 +> 2. 在物理系统每次迭代(物理系统是在 postUpdate 进行迭代的)的最后会把所有刚体信息同步到对应节点上去,而出于性能考虑,只有当开发者对刚体所在节点的相关属性进行显示设置时,节点的信息才会同步到刚体上,并且刚体只会监视他所在的节点,也就是说,如果修改了节点的父节点的旋转位移,是不会同步这些信息的。 + +### 固定旋转 + +![fix ratation](image/fix-rotation.png) + +做平台跳跃游戏时通常都不会希望主角的旋转属性也被加入到物理模拟中,因为这样会导致主角在移动过程中东倒西歪,这时可以设置刚体的 `fixedRotation` 为 true,固定旋转,代码示例如下: + +```ts +rigidbody.fixedRotation = true; +``` + +### 开启碰撞监听 + +![contact](image/enable-contact.png) + +只有开启了刚体的碰撞监听,刚体发生碰撞时才会回调到对应的组件上。代码示例如下: + +```ts +rigidbody.enabledContactListener = true; +``` + +## 刚体类型 + +![type](image/rigidbody-type.png) + +Box2D 原本的刚体类型是三种:**Static**、**Dynamic**、**Kinematic**。在 Cocos Creator 中多添加了一个类型:**Animated**。 + +Animated 是从 Kinematic 类型衍生出来的,一般的刚体类型修改 **旋转** 或 **位移** 属性时,都是直接设置的属性,而 Animated 会根据当前旋转或位移属性,与目标旋转或位移属性计算出所需的速度,并且赋值到对应的移动或旋转速度上。 + +添加 Animated 类型主要是防止对刚体做动画时可能出现的奇怪现象,例如穿透。 + +| 刚体类型 | 说明 | +| :-- | :-- | +| **Static** | 静态刚体,零质量,零速度,即不会受到重力或速度影响,但是可以设置他的位置来进行移动。该类型通常用于制作场景 | +| **Dynamic** | 动态刚体,有质量,可以设置速度,会受到重力影响。
唯一可以通过 `applyForce` 和 `applyTorque` 等方法改变受力的刚体类型 | +| **Kinematic** | 运动刚体,零质量,可以设置速度,不会受到重力的影响,但是可以设置速度来进行移动 | +| **Animated** | 动画刚体,在上面已经提到过,从 Kinematic 衍生的类型,主要用于刚体与动画编辑结合使用 | + +通过代码可以获取或修改刚体的类型,代码示例如下: + +```ts +import { RigidBody2D, ERigidBody2DType } from 'cc'; + +const rigibodyType = this.rigidbody2D.type + +this.rigidbody2D.type = ERigidBody2DType.Animated +``` + +2D 刚体的类型定义在枚举 `ERigidBody2DType` 内,请注意和 3D 物理的 `ERigidBodyType` 区分。 + +### 碰撞响应 + +不同类型的刚体之间,并非都可进行碰撞,其结果整理如下: + +| -- | Static | Dynamic| Kinematic | Animated | +| :-- | :-- | :-- | :-- | :-- | +| **Static** | | √ | √ | √| +| **Dynamic** | √ | √ | √ | √ | +| **Kinematic** | √| √ | √ | √ | +| **Animated** | √ | √ | √ | √ | + +## 刚体方法 + +### 获取或转换旋转位移属性 + +使用这些 API 来获取世界坐标系下的旋转和位移会比通过节点来获取更快,因为节点中还需要通过矩阵运算得到结果,而使用 API 是直接得到结果的。 + +#### 本地坐标与世界坐标转换 + +```ts +// 世界坐标转换到本地坐标 +const localPoint = rigidbody.getLocalPoint(worldPoint); +// 或者 +localPoint = new Vec2(); +rigidbody.getLocalPoint(worldPoint, localPoint); +``` + +```ts +// 本地坐标转换到世界坐标 +const worldPoint = rigidbody.getWorldPoint(localPoint); +// 或者 +worldPoint = new Vec2(); +rigidbody.getLocalPoint(localPoint, worldPoint); +``` + +```ts +// 本地向量转换为世界向量 +const worldVector = rigidbody.getWorldVector(localVector); +// 或者 +worldVector = new Vec2(); +rigidbody.getWorldVector(localVector, worldVector); +``` + +```ts +// 世界向量转换为本地向量 +const localVector = rigidbody.getLocalVector(worldVector); +// 或者 +localVector = new Vec2(); +rigidbody.getLocalVector(worldVector, localVector); +``` + +### 获取刚体质心 + +当对一个刚体施加力的时候,一般会选择刚体的质心作为施加力的作用点,这样能保证力不会影响到旋转值。 + +```ts +// 获取本地坐标系下刚体的质心 +let localCenter = new Vec2(); +rigidbody.getLocalCenter(localCenter); + +// 获取世界坐标系下的刚体质心 +let worldCenter = new Vec2(); +rigidbody.getWorldCenter(worldCenter); +``` + +> 上面代码中用于调用 `rigidbody.getXXX` 返回的 out 变量,建议缓存下来,避免每次都 new 造成 GC 问题。 + +### 力与冲量 + +移动一个物体有两种方式: + +1. 可以施加一个力或者冲量到这个物体上。力会随着时间慢慢修改物体的速度,而冲量会立即修改物体的速度。 +2. 直接修改物体的位置,只是这看起来不像真实的物理,你应该尽量去使用力或者冲量来移动刚体,这会减少可能带来的奇怪问题。 + +```ts +// 施加一个力到刚体上指定的点,这个点是世界坐标系下的一个点 +rigidbody.applyForce(force, point); + +// 或者直接施加力到刚体的质心上 +rigidbody.applyForceToCenter(force); + +// 施加一个冲量到刚体上指定的点,这个点是世界坐标系下的一个点 +rigidbody.applyLinearImpulse(impulse, point); +``` + +力与冲量也可以只对旋转轴产生影响,这样的力叫做扭矩。 + +```ts +// 施加扭矩到刚体上,因为只影响旋转轴,所以不再需要指定一个点 +rigidbody.applyTorque(torque); + +// 施加旋转轴上的冲量到刚体上 +rigidbody.applyAngularImpulse(impulse); +``` + +### 其他 + +如果要获取刚体在某一点上的速度时,可以通过 `getLinearVelocityFromWorldPoint` 来获取,比如当物体碰撞到一个平台时,需要根据物体碰撞点的速度来判断物体相对于平台是从上方碰撞的还是下方碰撞的。 + +```ts +rigidbody.getLinearVelocityFromWorldPoint(worldPoint); +``` + +更多刚体的方法可以参考 [Rigidbody2D API](%__APIDOC__%/zh/class/RigidBody2D) diff --git a/versions/4.0/zh/physics-2d/physics-2d-system.md b/versions/4.0/zh/physics-2d/physics-2d-system.md new file mode 100644 index 0000000000..bf257f3a0b --- /dev/null +++ b/versions/4.0/zh/physics-2d/physics-2d-system.md @@ -0,0 +1,162 @@ +# 2D 物理系统 + +物理系统隐藏了大部分物理模块(Box2D 和 Builtin 模块)实现细节(比如创建刚体,同步刚体信息到节点中等)。 + +可以通过物理系统访问一些物理模块常用的功能,比如点击测试、射线测试、设置测试信息等。 + +## 物理系统相关设置 + +### 开启物理系统 + +物理系统默认是开启的,代码如下: + +```ts +PhysicsSystem2D.instance.enable = true; +``` + +### 绘制物理调试信息 + +物理系统默认是不绘制任何调试信息的,如果需要绘制调试信息,请使用 **debugDrawFlags**。 + +物理系统提供了各种各样的调试信息,可以通过组合这些信息来绘制相关的内容。 + +```ts +PhysicsSystem2D.instance.debugDrawFlags = EPhysics2DDrawFlags.Aabb | + EPhysics2DDrawFlags.Pair | + EPhysics2DDrawFlags.CenterOfMass | + EPhysics2DDrawFlags.Joint | + EPhysics2DDrawFlags.Shape; +``` + +设置绘制标志位为 **EPhysics2DDrawFlags.None**,即可以关闭绘制。 + +```ts +PhysicsSystem2D.instance.debugDrawFlags = EPhysics2DDrawFlags.None; +``` + +### 物理单位到世界坐标系单位的转换 + +一般物理模块(Box2D)都是使用 **米 - 千克 - 秒(MKS)** 单位制,Box2D 在这样的单位制下运算的表现是最佳的。但是我们在 2D 游戏运算中一般使用 **世界坐标系中的单位**(简称世界单位)来作为长度单位制,所以我们需要一个比率来进行物理单位到世界单位上的相互转换。 + +一般情况下我们把这个比率设置为 32,这个值可以通过 `PHYSICS_2D_PTM_RATIO` 获取,并且这个值是只读的。通常用户是不需要关心这个值的,物理系统内部会自动对物理单位与世界单位进行转换,用户访问和设置的都是进行 2d 游戏开发中所熟悉的世界单位。 + +### 设置物理重力 + +重力是物理表现中非常重要的一点,大部分物理游戏都会使用到重力这一物理特性。默认的重力加速度是 `(0, -10)` 米/秒2,按照上面描述的转换规则,即 `(0, -320)` 世界单位/秒2。 + +如果希望重力加速度为 0,可以这样设置: + +```ts +PhysicsSystem2D.instance.gravity = v2(); +``` + +如果希望修改重力加速度为其他值,比如每秒加速降落 20m/s,那么可以这样设置: + +```ts +PhysicsSystem2D.instance.gravity = v2(0, -20 * PHYSICS_2D_PTM_RATIO); +``` + +### 设置物理步长 + +物理系统是按照一个固定的步长来更新物理世界的,默认的步长是 `1/60`。但是有的游戏可能会不希望按照这么高的频率来更新物理世界,毕竟这个操作是比较消耗时间的,可以通过降低步长来达到这个效果。代码示例如下: + +```ts +const system = PhysicsSystem2D.instance; + +// 物理步长,默认 fixedTimeStep 是 1/60 +system.fixedTimeStep = 1/30; + +// 每次更新物理系统处理速度的迭代次数,默认为 10 +system.velocityIterations = 8; + +// 每次更新物理系统处理位置的迭代次数,默认为 10 +system.positionIterations = 8; +``` + +**注意**:降低物理步长和各个属性的迭代次数,都会降低物理的检测频率,所以会更有可能发生刚体穿透的情况,使用时需要考虑到这个情况。 + +通过 [物理配置](../editor/project/physics-configs.md) 也可以在 **项目设置** -> **物理** 内修改重力和物理步长。 + +## 查询物体 + +通常你可能想知道在给定的场景中都有哪些实体。例如,一个炸弹爆炸了,在范围内的物体都会受到伤害,或者在策略类游戏中,可能会希望让用户选择一个范围内的单位进行拖动。 + +物理系统提供了几个方法方便用户高效快速地查找某个区域中有哪些物体,每种方法通过不同的方式来检测物体,基本满足游戏所需。 + +### 点测试 + +点测试将测试是否有碰撞体会包含一个世界坐标系下的点,如果测试成功,则会返回一个包含这个点的碰撞体。注意,如果有多个碰撞体同时满足条件,下面的接口只会返回一个随机的结果。 + +```ts +const collider = PhysicsSystem2D.instance.testPoint(point); +``` + +### 矩形测试 + +矩形测试将测试世界坐标系下指定的一个矩形,如果一个碰撞体的包围盒与这个矩形有重叠部分,则这个碰撞体会被添加到返回列表中。 + +```ts +const colliderList = PhysicsSystem2D.instance.testAABB(rect); +``` + +### 射线测试 + +Box2D 物理模块(Builtin 模块没有)提供了射线检测来检测给定的线段穿过哪些碰撞体,我们还可以获取到碰撞体在线段穿过碰撞体的那个点的法线向量和其他一些有用的信息。 +```ts +const results = PhysicsSystem2D.instance.raycast(p1, p2, type, mask); + +for (let i = 0; i < results.length; i++) { + const result = results[i]; + const collider = result.collider; + const point = result.point; + const normal = result.normal; + const fraction = result.fraction; +} +``` +注意:在 2D 物理系统中,PhysicsSystem2D.rayCast 使用的是世界坐标来进行计算。如果传入射线的起点和终点是基于本地坐标(如this.node.position),它们可能并不匹配物理世界中的位置,导致射线无法命中。无特殊需求请使用worldPosition传入p1,p2 + +射线检测的第三个参数指定检测的类型,射线检测支持四种类型。这是因为 Box2D 的射线检测不是从射线起始点最近的物体开始检测的,所以检测结果不能保证结果是按照物体距离射线起始点远近来排序的。Cocos Creator 物理系统将根据射线检测传入的检测类型来决定是否对 Box2D 检测结果进行排序,这个类型会影响到最后返回给用户的结果。 + +- ERaycast2DType.Any + + 检测射线路径上任意的碰撞体,一旦检测到任何碰撞体,将立刻结束检测其他的碰撞体,最快。 + +- ERaycast2DType.Closest + + 检测射线路径上最近的碰撞体,这是射线检测的默认值,稍慢。 + +- ERaycast2DType.All + + 检测射线路径上的所有碰撞体,检测到的结果顺序不是固定的。在这种检测类型下,一个碰撞体可能会返回多个结果,这是因为 Box2D 是通过检测夹具(fixture)来进行物体检测的,而一个碰撞体中可能由多个夹具(fixture)组成的,慢。 + +- ERaycast2DType.AllClosest + + 检测射线路径上所有碰撞体,但是会对返回值进行删选,只返回每一个碰撞体距离射线起始点最近的那个点的相关信息,最慢。 + +#### 射线检测的结果 + +射线检测的结果包含了许多有用的信息,你可以根据实际情况来选择如何使用这些信息。 + +- collider + + 指定射线穿过的是哪一个碰撞体。 + +- point + + 指定射线与穿过的碰撞体在哪一点相交。 + +- normal + + 指定碰撞体在相交点的表面的法线向量。 + +- fraction + + 指定相交点在射线上的分数。 + +可以通过下面这张图更好的理解射线检测的结果。 + +![raycasting-output](image/raycasting-output.png) + +#### 射线检测时分组与掩码 + +2D 物理系统的分组和掩码和 3D 物理系统一致,开发者可以在 **项目设置** -> **物理** 分页中找到碰撞矩阵进行修改。更多信息可以参考 [分组和掩码](../physics/physics-group-mask.md)。 diff --git a/versions/4.0/zh/physics-2d/physics-2d.md b/versions/4.0/zh/physics-2d/physics-2d.md new file mode 100644 index 0000000000..c3e0d6f44f --- /dev/null +++ b/versions/4.0/zh/physics-2d/physics-2d.md @@ -0,0 +1,22 @@ +# 2D 物理简介 + +Cocos Creator 支持内置的轻量 Builtin 物理系统和强大的 Box2D 物理系统。Builtin 物理系统只提供了碰撞检测的功能,对于物理计算较为简单的情况,我们推荐使用 Builtin 物理模块,这样可以避免加载庞大的 Box2D 物理模块并构建物理世界的运行时开销。而 Box2D 物理模块提供了更完善的交互接口和刚体、关节等已经预设好的组件。 + +你可以根据需要来选择适合自己的物理模块,通过编辑器主菜单中的 **项目 -> 项目设置 -> 功能裁剪** 切换物理模块的使用。 + +![feature cropping](./image/module.png) + +## 内容 + +本章主要包含以下内容: + +- [2D 物理系统](./physics-2d-system.md) +- [2D 刚体组件](./physics-2d-rigid-body.md) +- [2D 碰撞组件](./physics-2d-collider.md) +- [2D 碰撞回调](./physics-2d-contact-callback.md) +- [2D 物理关节](./physics-2d-joint.md) + +## 2D 物理示例 + +请参考范例 **physics-samples**([GitHub](https://github.com/cocos-creator/physics-samples/tree/v3.x/2d) | [Gitee](https://gitee.com/mirrors_cocos-creator/physics-samples/tree/v3.x/2d))。 +s \ No newline at end of file diff --git a/versions/4.0/zh/physics/character-controller/index.md b/versions/4.0/zh/physics/character-controller/index.md new file mode 100644 index 0000000000..f5215b7bd7 --- /dev/null +++ b/versions/4.0/zh/physics/character-controller/index.md @@ -0,0 +1,137 @@ +# 角色控制器 + +> 自 v3.8 开始,Cocos Creator 提供角色控制器。 + +角色控制可以为您的游戏添加简单易用的角色控制功能。 + +## 添加角色控制器 + +Cocos Creator 提供两种角色控制器类型:盒控制器和胶囊控制器。他们都继承自 `CharacterController`。 + +需要注意的是,角色控制器仅支持 **Bullet** 以及 **PhysX** 物理后端。请在编辑器顶部菜单 **项目** -> **项目设置** 中的 **功能剪裁** 分页中找到 **物理系统**,并将物理后端修改为 **Bullet Based Physics System** 或 **PhysX Based Physics System**(默认的物理后端是 Bullet)。 + +![backend.jpg](index/backend.jpg) + +### 通用属性 + +以下属性为角色控制器的通用属性,您可以在 **盒角色控制器** 和 **胶囊体角色控制器** 的 **属性检查器** 面板找到它们。 + +| 属性 | 描述 | +| :-- | :-- | +| Group | 物理分组,更多请参考 **项目** -> **项目设置** 的物理分页中 [碰撞矩阵](../physics-group-mask.md) | +| Min Move Distance | 角色控制器的最小移动距离,每次 move 时如果低于这个距离则不会移动| +| Center | 角色控制器的中心 | +| Step Offset | 最大自动爬台阶高度 | +| Slope Limit | 角色可以行走的最大坡度,单位:角度 | +| Skin Width | 控制器使用的皮肤宽度。请参考下方 **皮肤宽度** 获取更多信息。| + +### 胶囊体角色控制器 + +若要添加胶囊体角色控制器,在 **属性检查器** 面板上点击 **添加组件** 按钮,并选择 **CapsuleCharacterController**: + +![add-capsule-charactercontroller.jpg](./index/add-capsule-charactercontroller.jpg) + +#### 属性 + +![capsule-property.jpg](index/capsule-property.jpg) + +| 属性 | 描述 | +| :--- | :---- | +| Radius | 胶囊碰撞体的半径 | +| Height | 胶囊体末端两个球心的距离 | + +### 盒角色控制器 + +若要添加盒角色控制器,在 **属性检查器** 面板上点击 **添加组件** 按钮,并选择 **BoxCharacterController**: + +![add-box-charactercontroller.jpg](./index/add-box-charactercontroller.jpg) + +#### 属性 + +![box-property.jpg](index/box-property.jpg) + +| 属性 | 描述 | +| :--- | :---- | +| Half Height | 盒碰撞体在 Y 轴上的高度的一半 | +| Half Side Extent | 盒碰撞体在 X 轴上的高度的一半 | +| Half Forward Extent | 盒碰撞体在 Z 轴上的高度的一半 | + +## 驱动角色控制器 + +如果要驱动角色控制器移动,可以使用 `move` 方法,代码示例如下: + +```ts +const movement = v3(1.0, 0, 0); +let characterController = this.node.getComponent(CharacterController); +characterController.move(movement); +``` + +`move` 方法会考量行进路线中的碰撞体,内部使用了 `sweep` 的算法去检测,检测碰到物体后,一方面会判断控制器和物体的夹角,如果小于最大爬坡角度(Slope Limit),控制器会接着沿物体表面走;另一方面会考虑碰到控制器和物体的高度差,如果小于最大自动爬台阶高度(Step Offset)也会接着沿物体表面走。如果这两个都不满足,控制器就会停下来。 + +如果要重置角色的位置,请使用角色控制器的 `setPosition` 和 `setWorldPosition`。 + +需要注意的是,角色控制器是不受力影响的,因此开发者需要自行处理角色控制器的受力或者速度。 + +## 判断是否在地面上 + +通过 `isGrounded` 方法,可以判定角色控制器是否站在某些碰撞体上,代码示例如下: + +```ts +let characterController = this.node.getComponent(CharacterController); +const isOnGround = characterController.isGrounded; +``` + +## 监听角色碰撞 + +当角色控制器和碰撞体发生碰撞时,会派发 `onControllerColliderHit` 事件,代码参考如下: + +```ts +let characterController = this.node.getComponent(CharacterController)!; +characterController.on('onControllerColliderHit', this.onColliderHit, this); +``` + +碰撞回调如下: + +```ts +onColliderHit (hit: CharacterControllerContact){} +``` + +回调参数说明: + +- contact: 和其他碰撞体碰撞时的接触信息, API 参考:[CharacterControllerContact](%__APIDOC__%/zh/class/physics.CharacterControllerContact) + +和触发器发生碰撞时的事件包括以下三种: + +| 事件 | 说明 | +| :--------------- | :------- | +| `onControllerTriggerEnter` | 触发开始时触发该事件 | +| `onControllerTriggerStay` | 触发保持时会频发触发该事件 | +| `onControllerTriggerExit` | 触发结束时触发该事件 | + +## 细节说明 + +模拟角色时,角色通常不是完全的物理对象,也就意味着角色其实并不展示出完整物理特性。在碰撞发生时,角色控制器无法和普通的动力学刚体(Dynamic Rigidbody)一样模拟器受力情况,如果要实现受力效果,可以在碰撞回调中改变位置、速度信息来模拟物理现象。 + +如果要模拟完全受力的物理现象,请使用 [动力学刚体](../physics-rigidbody.md)。同时需要注意的是持有角色控制器的节点上,如果再添加其他刚体,可能会导致无法预知的错误。通常来说并不建议这么做。 + +如果场景中有多个角色控制器,控制器之间并不会产生物理反应。该功能会在后续版本中添加。 + +### 皮肤宽度 + +皮肤宽度允许控制器和对象之前发生碰撞时,可以产生轻微的穿透从而避免抖动或者卡住。 + +通常为一个较小且为正的浮点数。 + +如果出现频繁卡住的问题,可以尝试调大 **皮肤宽度** 属性,来避免数字精度的问题。 + +## 示例 + +角色控制器的示例地址为 [GIT](https://github.com/cocos/cocos-example-projects),下载完成后打开 **case-character-controller.scene** 场景运行即可。 + +## API + +角色控制器的 API 请参考 [角色控制器](%__APIDOC__%/zh/class/physics.CharacterController)、[盒角色控制器](__APIDOC__/zh/class/physics.BoxCharacterController) 以及 [胶囊体角色控制器](__APIDOC__/zh/class/physics.CapsuleCharacterController) + +## 支持情况 + +该功能在抖音的小程序 wasm 上不支持。如要启用,请使用 Cocos Creator 自带的 wasm。 diff --git a/versions/4.0/zh/physics/character-controller/index/add-box-charactercontroller.jpg b/versions/4.0/zh/physics/character-controller/index/add-box-charactercontroller.jpg new file mode 100644 index 0000000000..31744850d0 Binary files /dev/null and b/versions/4.0/zh/physics/character-controller/index/add-box-charactercontroller.jpg differ diff --git a/versions/4.0/zh/physics/character-controller/index/add-capsule-charactercontroller.jpg b/versions/4.0/zh/physics/character-controller/index/add-capsule-charactercontroller.jpg new file mode 100644 index 0000000000..1f3db4ced6 Binary files /dev/null and b/versions/4.0/zh/physics/character-controller/index/add-capsule-charactercontroller.jpg differ diff --git a/versions/4.0/zh/physics/character-controller/index/backend.jpg b/versions/4.0/zh/physics/character-controller/index/backend.jpg new file mode 100644 index 0000000000..eaf6892e9b Binary files /dev/null and b/versions/4.0/zh/physics/character-controller/index/backend.jpg differ diff --git a/versions/4.0/zh/physics/character-controller/index/box-property.jpg b/versions/4.0/zh/physics/character-controller/index/box-property.jpg new file mode 100644 index 0000000000..cfe6b691c8 Binary files /dev/null and b/versions/4.0/zh/physics/character-controller/index/box-property.jpg differ diff --git a/versions/4.0/zh/physics/character-controller/index/capsule-property.jpg b/versions/4.0/zh/physics/character-controller/index/capsule-property.jpg new file mode 100644 index 0000000000..062d9cbc34 Binary files /dev/null and b/versions/4.0/zh/physics/character-controller/index/capsule-property.jpg differ diff --git a/versions/4.0/zh/physics/example-img/arm-apple.gif b/versions/4.0/zh/physics/example-img/arm-apple.gif new file mode 100644 index 0000000000..c6c4cff4a9 Binary files /dev/null and b/versions/4.0/zh/physics/example-img/arm-apple.gif differ diff --git a/versions/4.0/zh/physics/example-img/arm.jpg b/versions/4.0/zh/physics/example-img/arm.jpg new file mode 100644 index 0000000000..e747d6023b Binary files /dev/null and b/versions/4.0/zh/physics/example-img/arm.jpg differ diff --git a/versions/4.0/zh/physics/example-img/bow-pulling.gif b/versions/4.0/zh/physics/example-img/bow-pulling.gif new file mode 100644 index 0000000000..65e252211f Binary files /dev/null and b/versions/4.0/zh/physics/example-img/bow-pulling.gif differ diff --git a/versions/4.0/zh/physics/example-img/composite-shape.jpg b/versions/4.0/zh/physics/example-img/composite-shape.jpg new file mode 100644 index 0000000000..5a3c4809e3 Binary files /dev/null and b/versions/4.0/zh/physics/example-img/composite-shape.jpg differ diff --git a/versions/4.0/zh/physics/example-img/config.jpg b/versions/4.0/zh/physics/example-img/config.jpg new file mode 100644 index 0000000000..052093f23c Binary files /dev/null and b/versions/4.0/zh/physics/example-img/config.jpg differ diff --git a/versions/4.0/zh/physics/example-img/convex.jpg b/versions/4.0/zh/physics/example-img/convex.jpg new file mode 100644 index 0000000000..06cfa4ea9a Binary files /dev/null and b/versions/4.0/zh/physics/example-img/convex.jpg differ diff --git a/versions/4.0/zh/physics/example-img/recycle-area.jpg b/versions/4.0/zh/physics/example-img/recycle-area.jpg new file mode 100644 index 0000000000..896d4d7759 Binary files /dev/null and b/versions/4.0/zh/physics/example-img/recycle-area.jpg differ diff --git a/versions/4.0/zh/physics/example-img/set-raycast.jpg b/versions/4.0/zh/physics/example-img/set-raycast.jpg new file mode 100644 index 0000000000..ed4e5ec524 Binary files /dev/null and b/versions/4.0/zh/physics/example-img/set-raycast.jpg differ diff --git a/versions/4.0/zh/physics/example-img/set-step.gif b/versions/4.0/zh/physics/example-img/set-step.gif new file mode 100644 index 0000000000..c2a2f18fac Binary files /dev/null and b/versions/4.0/zh/physics/example-img/set-step.gif differ diff --git a/versions/4.0/zh/physics/img/add-rigidbody-in-inspector.jpg b/versions/4.0/zh/physics/img/add-rigidbody-in-inspector.jpg new file mode 100644 index 0000000000..0ee4f52347 Binary files /dev/null and b/versions/4.0/zh/physics/img/add-rigidbody-in-inspector.jpg differ diff --git a/versions/4.0/zh/physics/img/apply-pmtl.jpg b/versions/4.0/zh/physics/img/apply-pmtl.jpg new file mode 100644 index 0000000000..f68c3d32c3 Binary files /dev/null and b/versions/4.0/zh/physics/img/apply-pmtl.jpg differ diff --git a/versions/4.0/zh/physics/img/box-all-in-parent.png b/versions/4.0/zh/physics/img/box-all-in-parent.png new file mode 100644 index 0000000000..f251bb091e Binary files /dev/null and b/versions/4.0/zh/physics/img/box-all-in-parent.png differ diff --git a/versions/4.0/zh/physics/img/bulelt.png b/versions/4.0/zh/physics/img/bulelt.png new file mode 100644 index 0000000000..292a4e5908 Binary files /dev/null and b/versions/4.0/zh/physics/img/bulelt.png differ diff --git a/versions/4.0/zh/physics/img/can-collider.png b/versions/4.0/zh/physics/img/can-collider.png new file mode 100644 index 0000000000..01f0ccf4f8 Binary files /dev/null and b/versions/4.0/zh/physics/img/can-collider.png differ diff --git a/versions/4.0/zh/physics/img/cant-collider.png b/versions/4.0/zh/physics/img/cant-collider.png new file mode 100644 index 0000000000..8a8ec6aaef Binary files /dev/null and b/versions/4.0/zh/physics/img/cant-collider.png differ diff --git a/versions/4.0/zh/physics/img/capsule-explain.png b/versions/4.0/zh/physics/img/capsule-explain.png new file mode 100644 index 0000000000..f880e9c47a Binary files /dev/null and b/versions/4.0/zh/physics/img/capsule-explain.png differ diff --git a/versions/4.0/zh/physics/img/center-add-comp.png b/versions/4.0/zh/physics/img/center-add-comp.png new file mode 100644 index 0000000000..8f06448556 Binary files /dev/null and b/versions/4.0/zh/physics/img/center-add-comp.png differ diff --git a/versions/4.0/zh/physics/img/center-add-cupsule.png b/versions/4.0/zh/physics/img/center-add-cupsule.png new file mode 100644 index 0000000000..7f1c5ad688 Binary files /dev/null and b/versions/4.0/zh/physics/img/center-add-cupsule.png differ diff --git a/versions/4.0/zh/physics/img/center-of-mass.gif b/versions/4.0/zh/physics/img/center-of-mass.gif new file mode 100644 index 0000000000..c87feeb091 Binary files /dev/null and b/versions/4.0/zh/physics/img/center-of-mass.gif differ diff --git a/versions/4.0/zh/physics/img/center-of-mass.jpg b/versions/4.0/zh/physics/img/center-of-mass.jpg new file mode 100644 index 0000000000..a24e112f06 Binary files /dev/null and b/versions/4.0/zh/physics/img/center-of-mass.jpg differ diff --git a/versions/4.0/zh/physics/img/center-result.png b/versions/4.0/zh/physics/img/center-result.png new file mode 100644 index 0000000000..6e906cbf40 Binary files /dev/null and b/versions/4.0/zh/physics/img/center-result.png differ diff --git a/versions/4.0/zh/physics/img/collider-box.jpg b/versions/4.0/zh/physics/img/collider-box.jpg new file mode 100644 index 0000000000..dafdeedf83 Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-box.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-capsule.jpg b/versions/4.0/zh/physics/img/collider-capsule.jpg new file mode 100644 index 0000000000..d159e26bdc Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-capsule.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-cobblestone.jpg b/versions/4.0/zh/physics/img/collider-cobblestone.jpg new file mode 100644 index 0000000000..c7fa31761f Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-cobblestone.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-cone.jpg b/versions/4.0/zh/physics/img/collider-cone.jpg new file mode 100644 index 0000000000..2fcd309f9e Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-cone.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-cylinder.jpg b/versions/4.0/zh/physics/img/collider-cylinder.jpg new file mode 100644 index 0000000000..37eb25e97b Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-cylinder.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-matrix.png b/versions/4.0/zh/physics/img/collider-matrix.png new file mode 100644 index 0000000000..7ce07bd6c1 Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-matrix.png differ diff --git a/versions/4.0/zh/physics/img/collider-mesh.jpg b/versions/4.0/zh/physics/img/collider-mesh.jpg new file mode 100644 index 0000000000..2e979869ae Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-mesh.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-non-uniform-scale.jpg b/versions/4.0/zh/physics/img/collider-non-uniform-scale.jpg new file mode 100644 index 0000000000..9a472a4d4a Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-non-uniform-scale.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-plane.jpg b/versions/4.0/zh/physics/img/collider-plane.jpg new file mode 100644 index 0000000000..2e1fbe473a Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-plane.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-simplex.jpg b/versions/4.0/zh/physics/img/collider-simplex.jpg new file mode 100644 index 0000000000..bf41e2e2ae Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-simplex.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-sphere.jpg b/versions/4.0/zh/physics/img/collider-sphere.jpg new file mode 100644 index 0000000000..a57c691b99 Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-sphere.jpg differ diff --git a/versions/4.0/zh/physics/img/collider-terrain.jpg b/versions/4.0/zh/physics/img/collider-terrain.jpg new file mode 100644 index 0000000000..156159392e Binary files /dev/null and b/versions/4.0/zh/physics/img/collider-terrain.jpg differ diff --git a/versions/4.0/zh/physics/img/compound-colliders.png b/versions/4.0/zh/physics/img/compound-colliders.png new file mode 100644 index 0000000000..807af5434a Binary files /dev/null and b/versions/4.0/zh/physics/img/compound-colliders.png differ diff --git a/versions/4.0/zh/physics/img/configurable-angular-driver.png b/versions/4.0/zh/physics/img/configurable-angular-driver.png new file mode 100644 index 0000000000..aee220bd04 Binary files /dev/null and b/versions/4.0/zh/physics/img/configurable-angular-driver.png differ diff --git a/versions/4.0/zh/physics/img/configurable-angular-limit.png b/versions/4.0/zh/physics/img/configurable-angular-limit.png new file mode 100644 index 0000000000..b962dbbcce Binary files /dev/null and b/versions/4.0/zh/physics/img/configurable-angular-limit.png differ diff --git a/versions/4.0/zh/physics/img/configurable-linear-driver.png b/versions/4.0/zh/physics/img/configurable-linear-driver.png new file mode 100644 index 0000000000..946996e30a Binary files /dev/null and b/versions/4.0/zh/physics/img/configurable-linear-driver.png differ diff --git a/versions/4.0/zh/physics/img/configurable-linear-limit.png b/versions/4.0/zh/physics/img/configurable-linear-limit.png new file mode 100644 index 0000000000..a8217fe306 Binary files /dev/null and b/versions/4.0/zh/physics/img/configurable-linear-limit.png differ diff --git a/versions/4.0/zh/physics/img/configurable-main.png b/versions/4.0/zh/physics/img/configurable-main.png new file mode 100644 index 0000000000..fce12ab601 Binary files /dev/null and b/versions/4.0/zh/physics/img/configurable-main.png differ diff --git a/versions/4.0/zh/physics/img/configurable_constraint_axis_config.png b/versions/4.0/zh/physics/img/configurable_constraint_axis_config.png new file mode 100644 index 0000000000..852d37f6be Binary files /dev/null and b/versions/4.0/zh/physics/img/configurable_constraint_axis_config.png differ diff --git a/versions/4.0/zh/physics/img/constant-force.jpg b/versions/4.0/zh/physics/img/constant-force.jpg new file mode 100644 index 0000000000..14a514a306 Binary files /dev/null and b/versions/4.0/zh/physics/img/constant-force.jpg differ diff --git a/versions/4.0/zh/physics/img/constraint_angular_coordinate.png b/versions/4.0/zh/physics/img/constraint_angular_coordinate.png new file mode 100644 index 0000000000..2bd9c0929a Binary files /dev/null and b/versions/4.0/zh/physics/img/constraint_angular_coordinate.png differ diff --git a/versions/4.0/zh/physics/img/constraint_coordinate.png b/versions/4.0/zh/physics/img/constraint_coordinate.png new file mode 100644 index 0000000000..3ab4541780 Binary files /dev/null and b/versions/4.0/zh/physics/img/constraint_coordinate.png differ diff --git a/versions/4.0/zh/physics/img/create-pmtl.jpg b/versions/4.0/zh/physics/img/create-pmtl.jpg new file mode 100644 index 0000000000..4f2231d30b Binary files /dev/null and b/versions/4.0/zh/physics/img/create-pmtl.jpg differ diff --git a/versions/4.0/zh/physics/img/cube-inspector.png b/versions/4.0/zh/physics/img/cube-inspector.png new file mode 100644 index 0000000000..14b7d13146 Binary files /dev/null and b/versions/4.0/zh/physics/img/cube-inspector.png differ diff --git a/versions/4.0/zh/physics/img/cube.png b/versions/4.0/zh/physics/img/cube.png new file mode 100644 index 0000000000..f2e8823a74 Binary files /dev/null and b/versions/4.0/zh/physics/img/cube.png differ diff --git a/versions/4.0/zh/physics/img/fixed-constraint.gif b/versions/4.0/zh/physics/img/fixed-constraint.gif new file mode 100644 index 0000000000..8153438a59 Binary files /dev/null and b/versions/4.0/zh/physics/img/fixed-constraint.gif differ diff --git a/versions/4.0/zh/physics/img/fixed-constraint.png b/versions/4.0/zh/physics/img/fixed-constraint.png new file mode 100644 index 0000000000..235c730c44 Binary files /dev/null and b/versions/4.0/zh/physics/img/fixed-constraint.png differ diff --git a/versions/4.0/zh/physics/img/game-script.png b/versions/4.0/zh/physics/img/game-script.png new file mode 100644 index 0000000000..e283203701 Binary files /dev/null and b/versions/4.0/zh/physics/img/game-script.png differ diff --git a/versions/4.0/zh/physics/img/goose-soft-rock.jpg b/versions/4.0/zh/physics/img/goose-soft-rock.jpg new file mode 100644 index 0000000000..c7fa31761f Binary files /dev/null and b/versions/4.0/zh/physics/img/goose-soft-rock.jpg differ diff --git a/versions/4.0/zh/physics/img/hinge-connectedbody.png b/versions/4.0/zh/physics/img/hinge-connectedbody.png new file mode 100644 index 0000000000..401b09bb68 Binary files /dev/null and b/versions/4.0/zh/physics/img/hinge-connectedbody.png differ diff --git a/versions/4.0/zh/physics/img/hinge-constraint.jpg b/versions/4.0/zh/physics/img/hinge-constraint.jpg new file mode 100644 index 0000000000..eafaf6a6f3 Binary files /dev/null and b/versions/4.0/zh/physics/img/hinge-constraint.jpg differ diff --git a/versions/4.0/zh/physics/img/hinge-joint.png b/versions/4.0/zh/physics/img/hinge-joint.png new file mode 100644 index 0000000000..bb9f39c62f Binary files /dev/null and b/versions/4.0/zh/physics/img/hinge-joint.png differ diff --git a/versions/4.0/zh/physics/img/import-geometry.jpg b/versions/4.0/zh/physics/img/import-geometry.jpg new file mode 100644 index 0000000000..a43acd6621 Binary files /dev/null and b/versions/4.0/zh/physics/img/import-geometry.jpg differ diff --git a/versions/4.0/zh/physics/img/induction.png b/versions/4.0/zh/physics/img/induction.png new file mode 100644 index 0000000000..9f9531e4ac Binary files /dev/null and b/versions/4.0/zh/physics/img/induction.png differ diff --git a/versions/4.0/zh/physics/img/isTrigger.jpg b/versions/4.0/zh/physics/img/isTrigger.jpg new file mode 100644 index 0000000000..8380f05a4f Binary files /dev/null and b/versions/4.0/zh/physics/img/isTrigger.jpg differ diff --git a/versions/4.0/zh/physics/img/mask-all.jpg b/versions/4.0/zh/physics/img/mask-all.jpg new file mode 100644 index 0000000000..9a75730d7f Binary files /dev/null and b/versions/4.0/zh/physics/img/mask-all.jpg differ diff --git a/versions/4.0/zh/physics/img/material-create-pmtl.jpg b/versions/4.0/zh/physics/img/material-create-pmtl.jpg new file mode 100644 index 0000000000..4f2231d30b Binary files /dev/null and b/versions/4.0/zh/physics/img/material-create-pmtl.jpg differ diff --git a/versions/4.0/zh/physics/img/material-create-pmtl.png b/versions/4.0/zh/physics/img/material-create-pmtl.png new file mode 100644 index 0000000000..a4b0779ddd Binary files /dev/null and b/versions/4.0/zh/physics/img/material-create-pmtl.png differ diff --git a/versions/4.0/zh/physics/img/material-panel.png.png b/versions/4.0/zh/physics/img/material-panel.png.png new file mode 100644 index 0000000000..e820aa498f Binary files /dev/null and b/versions/4.0/zh/physics/img/material-panel.png.png differ diff --git a/versions/4.0/zh/physics/img/p2p-joint.png b/versions/4.0/zh/physics/img/p2p-joint.png new file mode 100644 index 0000000000..33648ff11c Binary files /dev/null and b/versions/4.0/zh/physics/img/p2p-joint.png differ diff --git a/versions/4.0/zh/physics/img/physic-material.jpg b/versions/4.0/zh/physics/img/physic-material.jpg new file mode 100644 index 0000000000..26edf9613d Binary files /dev/null and b/versions/4.0/zh/physics/img/physic-material.jpg differ diff --git a/versions/4.0/zh/physics/img/physics-add-boxcollider.png b/versions/4.0/zh/physics/img/physics-add-boxcollider.png new file mode 100644 index 0000000000..0f1bab8f4d Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-add-boxcollider.png differ diff --git a/versions/4.0/zh/physics/img/physics-add-cube.png b/versions/4.0/zh/physics/img/physics-add-cube.png new file mode 100644 index 0000000000..7922cc8fd6 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-add-cube.png differ diff --git a/versions/4.0/zh/physics/img/physics-add-plane.png b/versions/4.0/zh/physics/img/physics-add-plane.png new file mode 100644 index 0000000000..e5a950f843 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-add-plane.png differ diff --git a/versions/4.0/zh/physics/img/physics-add-sphere.png b/versions/4.0/zh/physics/img/physics-add-sphere.png new file mode 100644 index 0000000000..9d51142f52 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-add-sphere.png differ diff --git a/versions/4.0/zh/physics/img/physics-ccd.jpg b/versions/4.0/zh/physics/img/physics-ccd.jpg new file mode 100644 index 0000000000..d37e7aab0c Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-ccd.jpg differ diff --git a/versions/4.0/zh/physics/img/physics-collision.png b/versions/4.0/zh/physics/img/physics-collision.png new file mode 100644 index 0000000000..abc95eb175 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-collision.png differ diff --git a/versions/4.0/zh/physics/img/physics-config-index.png b/versions/4.0/zh/physics/img/physics-config-index.png new file mode 100644 index 0000000000..a0cffe0ed2 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-config-index.png differ diff --git a/versions/4.0/zh/physics/img/physics-configs.png b/versions/4.0/zh/physics/img/physics-configs.png new file mode 100644 index 0000000000..121d6ebb87 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-configs.png differ diff --git a/versions/4.0/zh/physics/img/physics-debugdraw.png b/versions/4.0/zh/physics/img/physics-debugdraw.png new file mode 100644 index 0000000000..7a3406c9cf Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-debugdraw.png differ diff --git a/versions/4.0/zh/physics/img/physics-element.png b/versions/4.0/zh/physics/img/physics-element.png new file mode 100644 index 0000000000..797214e8ac Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-element.png differ diff --git a/versions/4.0/zh/physics/img/physics-hinge.gif b/versions/4.0/zh/physics/img/physics-hinge.gif new file mode 100644 index 0000000000..c755a4a22d Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-hinge.gif differ diff --git a/versions/4.0/zh/physics/img/physics-mat-panel.png b/versions/4.0/zh/physics/img/physics-mat-panel.png new file mode 100644 index 0000000000..80b1f842e0 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-mat-panel.png differ diff --git a/versions/4.0/zh/physics/img/physics-module.jpg b/versions/4.0/zh/physics/img/physics-module.jpg new file mode 100644 index 0000000000..d37687df56 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-module.jpg differ diff --git a/versions/4.0/zh/physics/img/physics-p2p.gif b/versions/4.0/zh/physics/img/physics-p2p.gif new file mode 100644 index 0000000000..ee00fb91a9 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-p2p.gif differ diff --git a/versions/4.0/zh/physics/img/physics-pipeline.png b/versions/4.0/zh/physics/img/physics-pipeline.png new file mode 100644 index 0000000000..4b51975a53 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-pipeline.png differ diff --git a/versions/4.0/zh/physics/img/physics-scene-movement.gif b/versions/4.0/zh/physics/img/physics-scene-movement.gif new file mode 100644 index 0000000000..d7af1c83cf Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-scene-movement.gif differ diff --git a/versions/4.0/zh/physics/img/physics-system.jpg b/versions/4.0/zh/physics/img/physics-system.jpg new file mode 100644 index 0000000000..4e3c6d7ced Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-system.jpg differ diff --git a/versions/4.0/zh/physics/img/physics-system.png b/versions/4.0/zh/physics/img/physics-system.png new file mode 100644 index 0000000000..48f865b29b Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-system.png differ diff --git a/versions/4.0/zh/physics/img/physics-type.gif b/versions/4.0/zh/physics/img/physics-type.gif new file mode 100644 index 0000000000..b3567a28f9 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-type.gif differ diff --git a/versions/4.0/zh/physics/img/physics-with-rigidbody.gif b/versions/4.0/zh/physics/img/physics-with-rigidbody.gif new file mode 100644 index 0000000000..dde116ab7c Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-with-rigidbody.gif differ diff --git a/versions/4.0/zh/physics/img/physics-world.jpg b/versions/4.0/zh/physics/img/physics-world.jpg new file mode 100644 index 0000000000..2ac073fa53 Binary files /dev/null and b/versions/4.0/zh/physics/img/physics-world.jpg differ diff --git a/versions/4.0/zh/physics/img/physics.gif b/versions/4.0/zh/physics/img/physics.gif new file mode 100644 index 0000000000..8d5237800b Binary files /dev/null and b/versions/4.0/zh/physics/img/physics.gif differ diff --git a/versions/4.0/zh/physics/img/physics.jpg b/versions/4.0/zh/physics/img/physics.jpg new file mode 100644 index 0000000000..ebcbddeedf Binary files /dev/null and b/versions/4.0/zh/physics/img/physics.jpg differ diff --git a/versions/4.0/zh/physics/img/plane.png b/versions/4.0/zh/physics/img/plane.png new file mode 100644 index 0000000000..d3d45cb42c Binary files /dev/null and b/versions/4.0/zh/physics/img/plane.png differ diff --git a/versions/4.0/zh/physics/img/pointtopoint-constraint.jpg b/versions/4.0/zh/physics/img/pointtopoint-constraint.jpg new file mode 100644 index 0000000000..f5742f1541 Binary files /dev/null and b/versions/4.0/zh/physics/img/pointtopoint-constraint.jpg differ diff --git a/versions/4.0/zh/physics/img/raycast-mask-setting.png b/versions/4.0/zh/physics/img/raycast-mask-setting.png new file mode 100644 index 0000000000..8bdf9b539d Binary files /dev/null and b/versions/4.0/zh/physics/img/raycast-mask-setting.png differ diff --git a/versions/4.0/zh/physics/img/raycast-with-collider.png b/versions/4.0/zh/physics/img/raycast-with-collider.png new file mode 100644 index 0000000000..f9b719d728 Binary files /dev/null and b/versions/4.0/zh/physics/img/raycast-with-collider.png differ diff --git a/versions/4.0/zh/physics/img/raycast.jpg b/versions/4.0/zh/physics/img/raycast.jpg new file mode 100644 index 0000000000..87c23077e2 Binary files /dev/null and b/versions/4.0/zh/physics/img/raycast.jpg differ diff --git a/versions/4.0/zh/physics/img/rigid-body-player.png b/versions/4.0/zh/physics/img/rigid-body-player.png new file mode 100644 index 0000000000..2f992b286c Binary files /dev/null and b/versions/4.0/zh/physics/img/rigid-body-player.png differ diff --git a/versions/4.0/zh/physics/img/rigid-body.jpg b/versions/4.0/zh/physics/img/rigid-body.jpg new file mode 100644 index 0000000000..e6da70d713 Binary files /dev/null and b/versions/4.0/zh/physics/img/rigid-body.jpg differ diff --git a/versions/4.0/zh/physics/img/rigidbody-prop.png b/versions/4.0/zh/physics/img/rigidbody-prop.png new file mode 100644 index 0000000000..af3e6434aa Binary files /dev/null and b/versions/4.0/zh/physics/img/rigidbody-prop.png differ diff --git a/versions/4.0/zh/physics/img/rigidybody-type.png b/versions/4.0/zh/physics/img/rigidybody-type.png new file mode 100644 index 0000000000..6cfed30e52 Binary files /dev/null and b/versions/4.0/zh/physics/img/rigidybody-type.png differ diff --git a/versions/4.0/zh/physics/img/sample-phy-setting.png b/versions/4.0/zh/physics/img/sample-phy-setting.png new file mode 100644 index 0000000000..b9cfd9fad5 Binary files /dev/null and b/versions/4.0/zh/physics/img/sample-phy-setting.png differ diff --git a/versions/4.0/zh/physics/img/scene.png b/versions/4.0/zh/physics/img/scene.png new file mode 100644 index 0000000000..8bc0c36bb3 Binary files /dev/null and b/versions/4.0/zh/physics/img/scene.png differ diff --git a/versions/4.0/zh/physics/img/servo.png b/versions/4.0/zh/physics/img/servo.png new file mode 100644 index 0000000000..178904c1ab Binary files /dev/null and b/versions/4.0/zh/physics/img/servo.png differ diff --git a/versions/4.0/zh/physics/img/set-collider-config.png b/versions/4.0/zh/physics/img/set-collider-config.png new file mode 100644 index 0000000000..52cdde9670 Binary files /dev/null and b/versions/4.0/zh/physics/img/set-collider-config.png differ diff --git a/versions/4.0/zh/physics/img/set-group.png b/versions/4.0/zh/physics/img/set-group.png new file mode 100644 index 0000000000..131542d3a2 Binary files /dev/null and b/versions/4.0/zh/physics/img/set-group.png differ diff --git a/versions/4.0/zh/physics/img/sweep.jpg b/versions/4.0/zh/physics/img/sweep.jpg new file mode 100644 index 0000000000..84591ea11f Binary files /dev/null and b/versions/4.0/zh/physics/img/sweep.jpg differ diff --git a/versions/4.0/zh/physics/index.md b/versions/4.0/zh/physics/index.md new file mode 100644 index 0000000000..3a6b383faf --- /dev/null +++ b/versions/4.0/zh/physics/index.md @@ -0,0 +1,9 @@ +# 物理系统 + +Cocos Creator 的物理系统提供了高效的组件化工作流程和便捷的使用方法。目前支持刚体、碰撞组件、触发和碰撞事件、物理材质、射线检测等等特性。 + +![image](img/physics.gif) + +Cocos Creator 内置了 [2D 物理系统](../physics-2d/physics-2d.md) 和 [3D 物理系统](physics.md),开发者可以通过 Creator 主菜单栏的 **项目 -> 项目设置 -> 功能裁剪** 中根据需要配置 2D 或者 3D 物理系统: + +![physics system](img/physics-system.png) \ No newline at end of file diff --git a/versions/4.0/zh/physics/physics-ccd.md b/versions/4.0/zh/physics/physics-ccd.md new file mode 100644 index 0000000000..3b35f2bc4c --- /dev/null +++ b/versions/4.0/zh/physics/physics-ccd.md @@ -0,0 +1,33 @@ +# 连续碰撞检测 + +**连续碰撞检测**(简称 **CCD**)是一种用于避免高速运动的物体在离散运动时出现穿透现象而导致碰撞数据不精确的技术,可用于实现类似高速运动的子弹与物体发生碰撞时,不会直接穿透物体。 + +## 开启 CCD + +CCD 默认为禁用状态,若要启用,将物体 RigidBody 组件的 `useCCD` 属性设置为 `true` 即可: + +```ts +const rigidBody = this.getComponent(RigidBody); +rigidBody.useCCD = true; +``` + +## 解决线性穿透问题 + +穿透是由于物体离散运动而导致的现象,三维物体的运动可以拆分为 **移动** 和 **旋转**。Creator 目前仅支持解决 **线性穿透** 问题,指的便是因物体离散的 **移动** 而导致的穿透现象。 + +穿透现象通常发生在高速运动的物体上,例如子弹类的物体。这是由于计算机的模拟都是基于离散化形式,当物体运动速度过快时,就会导致单次积分的能量过大,便可能导致物体穿越了本应该碰撞的另一物体,如下图所示: + +![physics-ccd](img/physics-ccd.jpg) + +可以看到小球从 T0 时刻运动到 T1 时刻,错过了本该在 T0.5 时刻碰撞的黑色方块,这便是典型的因线性速度过快而导致的线性穿透现象。 + +## 支持情况 + +由于各物理引擎支持情况的不同,以及出于性能等因素的考虑,Creator 对 CCD 的支持情况如下: + +- 仅支持动力学刚体。 +- 仅支持凸类形状物体。 +- 不支持解决旋转穿透问题。 +- 出于性能因素的考虑,CCD 的事件仅支持以 `onCollision` 开头的类型。 +- 当物理引擎设置为 **Bullet** 时,仅支持单个形状物体(只包含一个碰撞组件),并且碰撞组件中的 `center` 属性需要设置为 0。 +- 当物理引擎设置为 **Cannon** 时,仅支持球碰撞组件。 \ No newline at end of file diff --git a/versions/4.0/zh/physics/physics-collider.md b/versions/4.0/zh/physics/physics-collider.md new file mode 100644 index 0000000000..1437ccc8cf --- /dev/null +++ b/versions/4.0/zh/physics/physics-collider.md @@ -0,0 +1,251 @@ +# 碰撞组件与基础属性 + +碰撞组件可用于定义需要进行物理碰撞的物体形状,不同的几何形状拥有不同的属性。碰撞体通常分为以下几种: +1. [基础碰撞体](#基础碰撞体)。常见的包含 [盒](#盒碰撞器组件-BoxCollider)、[球](#球碰撞器组件-SphereCollider)、[圆柱](#圆柱碰撞器组件-CylinderCollider)、[圆锥](#圆锥碰撞器组件-ConeCollider)、[胶囊](#胶囊碰撞器组件-CapsuleCollider) 碰撞体。 +2. [复合碰撞体](#复合碰撞体)。可以通过在一个节点身上添加一个或多个基础碰撞体,简易模拟游戏对象形状,同时保持较低的性能开销。 +3. [网格碰撞体](#网格碰撞器组件-MeshCollider)。根据物体网格信息生成碰撞体,完全的贴合网格。 +4. [单纯形碰撞体](#单纯形碰撞器组件-SimplexCollider)。提供点、线、三角面、四面体碰撞。 +5. [平面碰撞体](#平面碰撞器组件-PlaneCollider)。可以代表无限平面或半空间。这个形状只能用于静态的、非移动的物体。 +6. [地形碰撞体](#地形碰撞器组件-TerrainCollider)。一种用于凹地形的特殊支持。 + +> **注意**:在某些(如 Bullet)物理后端中,由于计算精度的原因,应该避免使用比例很高的尺寸,这里建议低于1000。如某个盒碰撞器,其 **Size** 属性的 Y 值为 40 而 Z 值为 0.01,此时他们的 Y、Z 的比例超过了 1000,此时可能会出现浮点数计算不准确的问题。 + +## 添加碰撞组件 + +这里以获取 **BoxCollider** 盒碰撞器组件为例。 + +### 通过编辑器添加 + +1. 新建一个 3D 对象 Cube,在 **资源管理器** 中点击左上角的 **+** 创建按钮,然后选择 **创建 -> 3D 对象 -> Cube 立方体**。 + + ![add-cube](img/physics-add-cube.png) + +2. 选中新建的 Cube 立方体节点,在右侧的 **属性检查器** 面板下方点击 **添加组件** 按钮,选择 **Physics -> BoxCollider** 添加一个碰撞器组件. + + ![add-boxcollider](img/physics-add-boxcollider.png) + +### 通过代码添加 + +```ts +import { BoxCollider } from 'cc' + +const boxCollider = this.node.addComponent(BoxCollider); +``` + +## 碰撞组件共有属性说明 + +| 属性 | 说明 | +| :---|:--- | +| **Attached** | 碰撞器所绑定的 [刚体](physics-rigidbody.md) | +| **Material** | 碰撞器所使用的 [物理材质](physics-material.md),未设置时使用引擎默认的物理材质 | +| **IsTrigger** | 是否为 [触发器](physics-event.md),触发器不会产生物理反馈 | + +刚体获取请注意以下几点: + +- 在自身节点无 `RigidBody` 组件时,该属性返回为 `null`。 +- Attached 对应的真实属性名为 attachedRigidBody,attachedRigidbody 是一个只读的属性,不可修改。 + +```ts +let collider = this.node.addComponent(BoxCollider)!; +let rigidbody = collider.attachedRigidBody; +``` + +有关碰撞器的编辑可以查看:[碰撞器 Gizmo](../editor/scene/collider-gizmo.md)。 + +> **注意**:在使用碰撞体前请先查阅 [不同物理后端碰撞形状支持情况](physics-engine.md#不同物理后端碰撞形状支持情况),确保当前使用的物理引擎支持。 + +## 碰撞组件 + +### 基础碰撞体 + +#### 盒碰撞器组件 BoxCollider + +![盒碰撞器组件](img/collider-box.jpg) + +盒形碰撞体是一个基于长方体形状的碰撞体,可用来实现木箱、墙壁等物体的碰撞。可以用来组合成复合形状。 + +| 属性 | 说明 | +| :---|:--- | +| **Center** | 在本地坐标系中,形状的中心位置 | +| **Size** | 在本地坐标系中,盒的大小,即长、宽、高 | + +盒碰撞器组件接口请参考 [BoxCollider API](%__APIDOC__%/zh/class/physics.BoxCollider)。 + +#### 球碰撞器组件 SphereCollider + +![球碰撞器组件](img/collider-sphere.jpg) + +球碰撞体是一个基于球形的碰撞体。 + +| 属性 | 说明 | +| :---|:--- | +| **Center** | 在本地坐标系中,形状的中心位置 | +| **Radius** | 在本地坐标系中,球的半径 | + +球碰撞器组件接口请参考 [SphereCollider API](%__APIDOC__%/zh/class/physics.SphereCollider)。 + +#### 胶囊碰撞器组件 CapsuleCollider + +![胶囊碰撞器组件](img/collider-capsule.jpg) + +胶囊碰撞体是一个基于胶囊形状的碰撞体。 + +> **注意**:`cannon.js` 不支持胶囊组件,建议使用两个球体和圆柱拼凑。 + +| 属性 | 说明 | +| :---|:--- | +| **Center** | 在本地坐标系中,形状的中心位置 | +| **Radius** | 在本地坐标系中,胶囊体上的球的半径 | +| **CylinderHeight** | 在本地坐标系中,胶囊体上圆柱体高度 | +| **Direction** | 在本地坐标系中,胶囊体的朝向 | + +![capsule-explain](img/capsule-explain.png) + +胶囊碰撞器组件接口请参考 [CapsuleCollider API](%__APIDOC__%/zh/class/physics.CapsuleCollider)。 + +#### 圆柱碰撞器组件 CylinderCollider + +![圆柱碰撞器组件](img/collider-cylinder.jpg) + +圆柱碰撞体是一个基于圆柱体的碰撞体。 + +| 属性 | 说明 | +| :---|:--- | +| **Center** | 在本地坐标系中,形状的中心位置 | +| **Radius** | 在本地坐标系中,圆柱体上圆面的半径 | +| **Height** | 在本地坐标系中,圆柱体在相应轴向的高度 | +| **Direction** | 在本地坐标系中,圆柱体的朝向 | + +圆柱碰撞器组件接口请参考 [CylinderCollider API](%__APIDOC__%/zh/class/physics.CapsuleCollider)。 + +#### 圆锥碰撞器组件 ConeCollider + +![圆锥碰撞器组件](img/collider-cone.jpg) + +圆锥碰撞体是一个基于圆锥体的碰撞体。 + +| 属性 | 说明 | +| :---|:--- | +| **Center** | 在本地坐标系中,形状的中心位置 | +| **Height** | 在本地坐标系中,圆锥体在相应轴向的高度 | +| **Direction** | 在本地坐标系中,圆锥体的朝向 | + +圆锥碰撞器组件接口请参考 [ConeCollider API](%__APIDOC__%/zh/class/physics.ConeCollider)。 + +### 复合碰撞体 + +复合碰撞体指的不是一个碰撞体类型,而是由多个基础碰撞体组成的可以简易模拟游戏对象形状的碰撞体组合。 + +![compound-colliders](img/compound-colliders.png) + +### 平面碰撞组件 PlaneCollider + +![平面碰撞器组件](img/collider-plane.jpg) + +平面碰撞体是一个所属平面模型的碰撞体。平面模型可以通过在 **层级管理器** 面板右击 **创建->3D 对象->Plane 平面** 创建。 + +| 属性 | 说明 | +| :---|:--- | +| **Center** | 在本地坐标系中,形状的中心位置 | +| **Normal** | 在本地坐标系中,平面的法线 | +| **Constant** | 在本地坐标系中,平面从原点开始沿着法线运动的距离 | + +平面碰撞器组件接口请参考 [PlaneCollider API](%__APIDOC__%/zh/class/physics.PlaneCollider)。 + +### 网格碰撞组件 MeshCollider + +![网格碰撞器组件](img/collider-mesh.jpg) + +网格碰撞体是一个基于模型网格而形成的碰撞体。 + +> **注意**: +> 1. `cannon.js` 对网格碰撞器组件支持程度较差,只允许与少数碰撞器(球、平面)产生检测。 +> 2. **Convex** 功能目前仅 `ammo.js` 后端支持。 + +| 属性 | 说明 | +| :---|:--- | +| **Center** | 在本地坐标系中,形状的中心位置 | +| **Mesh** | 网格碰撞器所使用的网格资源,用于初始化网格碰撞体 | +| **Convex** | 是否使用网格的凸包近似代替,网格顶点数应小于 **255**,开启后可以支持动力学 | + +网格碰撞器组件接口请参考 [MeshCollider API](%__APIDOC__%/zh/class/physics.MeshCollider)。 + +### 单纯形碰撞组件 SimplexCollider + +![单纯形碰撞器组件](img/collider-simplex.jpg) + +单纯形碰撞体是一个基于点、线、面和四面体的碰撞体。 + +| 属性 | 说明 | +| :---|:--- | +| **Center** | 在本地坐标系中,形状的中心位置 | +| **ShapeType** | 单纯形类型,包括四种:点、线、三角面、四面体 | +| **Vertex0** | 单纯形的顶点 0,点(由 0 组成) | +| **Vertex1** | 单纯形的顶点 1,线(由 0、1 组成) | +| **Vertex2** | 单纯形的顶点 2,三角面(以此类推) | +| **Vertex3** | 单纯形的顶点 3,四面体 | + +单纯形碰撞器组件接口请参考 [SimplexCollider API](%__APIDOC__%/zh/class/physics.SimplexCollider)。 + +> **注意**:`cannon.js` 对线和三角面的支持目前还不完善。 + +### 地形碰撞组件 TerrainCollider + +![地形碰撞器组件](img/collider-terrain.jpg) + +地形碰撞体是一个地形表面生成的碰撞体,具体形状与其附加到 Terrain 属性上的对象相同。 + +| 属性 | 说明 | +| :---|:--- | +| **Center** | 在本地坐标系中,形状的中心位置 | +| **Terrain** | 获取或设置此碰撞体引用的网格资源 | + +地形碰撞器组件接口请参考 [TerrainCollider API](%__APIDOC__%/zh/class/physics.TerrainCollider)。 + +## 自动缩放 + +每个组件都会绑定在一个节点上,有些组件会根据绑定的节点动态更新数据。其中碰撞体组件会根据节点信息自动更新相应的形状数据,让碰撞体可以更贴合渲染模型。以模型组件举例: + +模型组件会根据绑定节点自动更新模型的世界矩阵,从而实现改变节点的位置、缩放、旋转等信息,可以使渲染的模型有相应仿射变换。 + +但碰撞体的有些性质导致缩放的处理不太一样: + +- 碰撞体一般用几何结构来描述 +- 碰撞体大部分都是凸包类型 + +这些性质限制了切变、非均一缩放等变换,以球举例: + +假设绑定节点的缩放信息是 **(1,2,1)**(非均一缩放),由于模型和碰撞体描述的结构不一样,球模型使用多个基础图元(如三角面)来表示,缩放后会形变成类似于鹅卵石的形状;而球碰撞体是根据半径大小来描述,缩放时会取数值最大的维度来缩放半径(这样是为了碰撞体尽可能的包围住模型),**但缩放后还是一个球**,因此无法精确的包裹着类似于鹅卵石大小的球模型。 + +![非均一缩放球](img/collider-non-uniform-scale.jpg) + +### 非标准形状 + +对于像鹅卵石这样非标准形状,可以使用 [MeshCollider 网格碰撞体](physics-component.md#%E7%BD%91%E6%A0%BC%E7%A2%B0%E6%92%9E%E5%99%A8%E7%BB%84%E4%BB%B6%20MeshCollider) 来代替基础的碰撞体。 + +**注意**:若需要支持动力学刚体,则必须开启 **convex** 功能。 + +![鹅卵石](img/collider-cobblestone.jpg) + +## 复合碰撞 + +对于单个节点是很容易看出是否有物理元素的,但如果以节点链(由节点树构成的物理元素)为单位,则很难看出物理元素是由哪些节点以及哪些组件组成的。 + +对于节点链的情况,目前有两个使用方案: + +1. 节点链上的每个节点只要有物理组件,就是一个元素,这样父子节点之间的组件没有依赖关系。若节点需要多个碰撞体形状,往该节点上添加相应的 **Collider** 组件即可。 + ![box-all-in-parent](img/box-all-in-parent.png) + + **缺点**: + + - 层级结构不够直观,多个形状只能往一个节点上加,而显示形状需要增加子节点模型,并且难以支持碰撞体的局部旋转。 + - 对节点链调整参数时,需要同时调整两个地方,分别为子节点的位置信息和父节点上对应 **Collider** 组件的数据信息。 + +2. 从自身节点开始往父链节点上搜索,如果找到了 **RigidBody** 组件,则将自身的 **Collider** 组件绑定到该节点上,否则整条链上的 **Collider** 组件将共享一个 **RigidBody** 组件,元素对应的节点是最顶层的 **Collider** 组件所对应的节点。 + + **缺点**: + + - 增加了节点耦合,节点更新时,需要更新相应的依赖节点。 + - 在节点链被破坏时,需要维护内容更多,节点链在反复被破坏时需要处理复杂的逻辑。 + +> **注意**:目前 Cocos Creator 使用的是方案 **1**,后续版本也有进行调整的可能,请留意版本更新公告。 diff --git a/versions/4.0/zh/physics/physics-component.md b/versions/4.0/zh/physics/physics-component.md new file mode 100644 index 0000000000..ba6f076129 --- /dev/null +++ b/versions/4.0/zh/physics/physics-component.md @@ -0,0 +1,19 @@ +# 物理组件 + +Cocos Creator 目前为用户提供的物理组件分为以下几种: + +- [碰撞组件](physics-collider.md) + + 碰撞组件用于定义碰撞体形状。可在编辑器环境下查看其形状,运行时不可见。其形状可以根据需求设置,无需和对象网格完全相同。 + +- [刚体组件](physics-rigidbody.md) + + 刚体是组成物理世界的基本对象,它可以使游戏对象的运动方式受物理控制。 + +- [恒力组件](physics-constantForce.md) + + 恒力组件是一个工具组件,依赖于刚体,会在每帧对一个刚体施加给定的力和扭矩。 + +- [约束组件](physics-constraint.md) + + 约束组件依赖于刚体组件,可将某个对象的位置、方向或比例约束到其他对象。 \ No newline at end of file diff --git a/versions/4.0/zh/physics/physics-configs.md b/versions/4.0/zh/physics/physics-configs.md new file mode 100644 index 0000000000..566936b754 --- /dev/null +++ b/versions/4.0/zh/physics/physics-configs.md @@ -0,0 +1,84 @@ +# 物理系统配置 + +物理系统模块(PhysicsSystem)用于管理整个物理系统,负责同步物理元素、触发物理事件和调度物理世界的迭代。 + +## 物理配置 + +有两种办法可以配置物理系统,一种是在编辑器中配置,另一种是通过代码配置。 + +### 通过物理配置面板 + +通过 **项目设置 -> 物理配置** 可以对物理系统进行相关配置。 + +![Physics](./img/physics-config-index.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Gravity X** | 重力矢量,设置 x 分量上的重力值 | +| **Gravity Y** | 重力矢量,设置 y 分量上的重力值 | +| **Gravity Z** | 重力矢量,设置 z 分量上的重力值 | +| **AllowSleep** | 是否允许系统进入休眠状态,默认值 `true` | +| **SleepThreshold** | 进入休眠的默认速度临界值,默认值 `0.1`,最小值 `0` | +| **AutoSimulation** | 是否开启自动模拟, 默认值 `true` | +| **FixedTimeStep** | 每步模拟消耗的固定时间,默认值 `1/60`,最小值 `0` | +| **MaxSubSteps** | 每步模拟的最大子步数,默认值 `1`,最小值 `0` | +| **DefualtMaterial** | 默认物理材质 | +| **CollisionMatrix** | 碰撞矩阵,仅用于初始化 | + +> **注意**:目前 **2D** / **3D** 物理共用一个配置。 + +### 程序化配置 + +程序化配置目前可以通过直接访问 `PhysicsSystem.instance` 对物理系统进行配置。部分代码示例如下: + +```ts +import { _decorator, Component, Node, Vec3, PhysicsSystem } from 'cc'; +const { ccclass, property } = _decorator; +@ccclass('Example') +export class Example extends Component { + start () { + PhysicsSystem.instance.enable = true; + PhysicsSystem.instance.gravity = new Vec3(0, -10, 0); + PhysicsSystem.instance.allowSleep = false; + } +} +``` + +> **注意**:物理系统是单例类,通过 `PhysicsSystem.instance` 获取物理系统的实例。 + +也可通过 `PhysicsSystem.resetConfiguration` 接口来重置物理配置,代码示例如下: + +```ts +let conf = game.config.physics +conf.gravity = new Vec3(10, 10, 0); +PhysicsSystem.instance.resetConfiguration(conf); +``` + +更多 API 内容请查看物理系统接口请参考:[PhysicsSystem API](%__APIDOC__%/zh/class/physics.PhysicsSystem)。 + +## 碰撞矩阵 + +碰撞矩阵是 [分组和掩码](physics-group-mask.md) 功能的进一步封装,它用于初始化物理元素的分组和掩码。 + +![Physics-collision](img/physics-collision.png) + +碰撞矩阵默认情况下只有一个 **DEFAULT** 分组,新建分组默认不与其它组碰撞。 + +点击 **+** 按钮可以新增分组。新增分组的 **Index** 和 **Name** 均不能不填。 + +- **Index** 代表的是碰撞分组值, 最高支持 32 位,即数值范围为 `[0, 31)`。分组值不可重复。 +- **Name** 代表的是碰撞分组名。此处在这里设置的名字只是为了用户进行碰撞分组配置方便,无法通过代码获取,代码能获取到的只有分组值。 + +![collider-matrix](img/collider-matrix.png) + +图上所示的就是一个飞行射击类游戏碰撞分组的配置。从图中可以看出,当添加一个分组的时候,面板上会出现横向和纵向都有分组的名字。我们把横向的部分称之为 **分组**,纵向的部分称之为 **掩码**。假设我现在进行了如下勾选: + +![set-collider-config](img/set-collider-config.png) + +此配置代表的意思是分组 **SELF_PLANE** 可以与分组 **ENEMY_BULLET** 和 **SELF_BULLET** 产生碰撞。这里的分组 **ENEMY_BULLET** 和 **SELF_BULLET** 就是分组 **ENEMY_PLANE** 的掩码。同样,对于分组 **ENEMY_BULLET** 来说 **ENEMY_PLANE** 也是它的掩码。 + +配置完成碰撞矩阵之后,就可以对需要产生碰撞的对象添加 **刚体(RigidBody)** 组件,设置碰撞分组 `Group`。 + +![set-group](img/set-group.png) + +> **注意**:分组在 v3.5 之前的版本不可以删除,但可以修改分组的名称。 diff --git a/versions/4.0/zh/physics/physics-constantForce.md b/versions/4.0/zh/physics/physics-constantForce.md new file mode 100644 index 0000000000..45ff36bfef --- /dev/null +++ b/versions/4.0/zh/physics/physics-constantForce.md @@ -0,0 +1,14 @@ +# 恒力组件 + +恒力组件是一个工具组件,依赖于[刚体](physics-rigidbody.md)组件,每帧都会对一个刚体施加给定的力和扭矩。 + +![恒力组件](img/constant-force.jpg) + +| 属性 | 说明 | +| :---|:--- | +| **force** | 在世界坐标系中,对刚体施加的力 | +| **localForce** | 在本地坐标系中,对刚体施加的力 | +| **torque** | 在世界坐标系中,对刚体施加的扭转力 | +| **localTorque** | 在本地坐标系中,对刚体施加的扭转力 | + +恒力组件接口请参考 [ConstantForce API](%__APIDOC__%/zh/class/physics.ConstantForce)。 \ No newline at end of file diff --git a/versions/4.0/zh/physics/physics-constraint.md b/versions/4.0/zh/physics/physics-constraint.md new file mode 100644 index 0000000000..655f3c39ee --- /dev/null +++ b/versions/4.0/zh/physics/physics-constraint.md @@ -0,0 +1,184 @@ +# 约束 + +在物理引擎中,**约束** 用于模拟物体间的连接情况,如连杆、绳子、弹簧或者布娃娃等。 + +约束依赖 [刚体组件](physics-rigidbody.md),若节点无刚体组件,则添加约束时,引擎会自动添加刚体组件。 + +> **注意**:目前的约束仅在物理引擎选择为 Bullet、PhysX 或 Cannon.js 的情况下生效。 + +## 铰链约束 HingeConstraint + +通过铰链约束,将连接物体的运动约束在某一个轴上,这种约束在模拟门的合页或电机转动等情形下非常有用。 + +![铰链约束](img/hinge-constraint.jpg) + +| 属性 | 说明 | +| :------------------ | :--------------------------------------------------------- | +| **AttachedBody** | 关节所附着的刚体,即当前关节所在节点下的刚体组件 | +| **ConnectedBody** | 获取或设置关节连接的另一个刚体,为空值时表示链接到静态刚体 | +| **EnableCollision** | 获取或设置关节连接的两刚体之间是否开启碰撞 | +| **PivotA** | 在自身刚体的本地空间中,约束关节的相对位置 | +| **PivotB** | 在连接刚体的本地空间中,约束关节的相对位置 | +| **Axis** | 在本地空间中约束关节的旋转轴 | + +![physics-hinge](img/physics-hinge.gif) + +铰链约束接口请参考 [HingeConstraint API](%__APIDOC__%/zh/class/physics.HingeConstraint)。 + +## 点对点约束 PointToPointConstraint + +点对点约束是一种简单的复合约束,可以将两个对象,或者一个对象与坐标系中一点连接。连接的对象可以在共用一个连接点的情况下,相对对方自由旋转。 + +![点对点约束](img/pointtopoint-constraint.jpg) + +| 属性 | 说明 | +| :------------------ | :--------------------------------------------------------- | +| **AttachedBody** | 关节所附着的刚体,即当前关节所在节点下的刚体组件 | +| **ConnectedBody** | 获取或设置关节连接的另一个刚体,为空值时表示链接到静态刚体 | +| **EnableCollision** | 获取或设置关节连接的两刚体之间是否开启碰撞 | +| **PivotA** | 在自身刚体的本地空间中,约束关节的相对位置 | +| **PivotB** | 在连接刚体的本地空间中,约束关节的相对位置 | + +![physics-p2p](img/physics-p2p.gif) + +点对点约束接口请参考 [PointToPointConstraint API](%__APIDOC__%/zh/class/physics.PointToPointConstraint)。 + +## 固定约束 FixedConstraint + +固定约束是一种最简单的约束,它锁定了两个刚体之间的相对位置和旋转。连接的对象不允许相对于彼此移动。 + +![固定约束](img/fixed-constraint.png) + +| 属性 | 说明 | +| :------------------ | :--------------------------------------------------------- | +| **AttachedBody** | 关节所附着的刚体,即当前关节所在节点下的刚体组件 | +| **ConnectedBody** | 获取或设置关节连接的另一个刚体,为空值时表示链接到静态刚体 | +| **EnableCollision** | 获取或设置关节连接的两刚体之间是否开启碰撞 | +| **BreakForce** | 获取或设置约束在断开之前可以施加的最大力 | +| **BreakTorque** | 获取或设置约束在断开之前可以施加的最大扭矩 | + +![physics-fixed](img/fixed-constraint.gif) + +固定约束接口请参考 [FixedConstraint API](%__APIDOC__%/zh/class/physics.FixedConstraint)。 + +## 可配置约束 Configurable constraint + +可配置约束是物理引擎中最全面的约束之一,包括各种游戏引擎中常用的约束类型。通过配置,可以对6个自由度分别进行控制,并通过设置不同方向上的约束参数实现几乎所有物理引擎中常用的特殊约束。不同自由度可以有不同的约束模式,如free、limited 和 locked。其中,free 表示不做任何约束,limit 表示限制刚体的运动范围和过程,而 locked 表示限制连接的刚体必须相对静止。例如,将所有6个自由度的约束模式设置为 locked 时,相当于使用 fixed 约束限制刚体。在 limit 模式下,可以对刚体的运动进行一定程度的限制,如限制该自由度在某个区间内运动。对于 limit 模式下的刚体,在运动到被限制范围的边界时,会发生回弹。此时,可以通过设置不同的回弹系数来调节回弹的力度,并通过调节软约束的参数实现约束的弹性,给运动施加一定量的阻力等。 + +由可配置约束连接的刚体可以通过马达驱动产生期望的相对运动。启用马达后,刚体将向指定的相对位置运动或逐渐加速到指定的相对速度并保持运动。马达有两种不同的模式,伺服模式和普通模式。在伺服模式下,驱动器将使刚体向指定的位置或角度运动,并在到达目标位置或角度后停止。在普通模式下,驱动器将使刚体逐渐加速,达到指定的线速度或角速度,然后保持该速度运动。另一个可以调节的参数是驱动器的最大驱动力,该参数决定刚体能够以怎样的速度加速趋向指定的目标位置和速度。当最大驱动力很大时,外界的干扰将很难改变刚体之间的相对运动状态。 + +![physics-configurable-main](img/configurable-main.png) + +| 属性 | 描述 | +| :------------------------ | :--------------------------------------------------------- | +| **Attached Body** | 关节所附着的刚体,即当前关节所在节点下的刚体组件 | +| **Connected Body** | 获取或设置关节连接的另一个刚体,为空值时表示链接到静态刚体 | +| **Enable Collision** | 获取或设置关节连接的两刚体之间是否开启碰撞 | +| **Axis** | 获取或设置约束的主轴向 | +| **Secondary Axis** | 获取或设置约束的次要轴向(与主轴正交) | +| **Pivot A** | 在自身刚体的本地空间中,约束关节的相对位置 | +| **Pivot B** | 在连接刚体的本地空间中,约束关节的相对位置 | +| **Auto Pivot B** | 从 PivotA 和两个刚体的相对位置自动推导 PivotB | +| **Break Force** | 获取或设置约束断裂前可以施加的最大力 | +| **Break Torque** | 获取或设置约束断裂前可以施加的最大扭矩 | +| **Linear Limit Settings** | 获取或设置线性限制设置 | +| **Angular Limit Settings** | 获取或设置角度限制设置 | +| **Linear Driver Settings** | 获取或设置线性马达设置 | +| **Angular Driver Settings** | 获取或设置角度马达设置 | + +示例:当设置约束体主轴朝向为依附刚体的 y 轴,次轴为依附刚体的 x 轴时,约束体各个轴的朝向如下图所示。 + +| 附着刚体轴向 | 约束体线性轴向 | 约束体旋转轴轴向 | +| :-- | :-- | :-- | +| ![configurable-constraint-axis-config](img/configurable_constraint_axis_config.png) | ![constraint-coordinate](img/constraint_coordinate.png) | ![constraint-angular-coordinate](img/constraint_angular_coordinate.png) | + +## 线性限制 LinearLimitSettings + +![physics-configurable-linear-limit](img/configurable-linear-limit.png) + +| 属性 | 描述 | +| :----------------------- | :------------------------ | +| **XMotion** | 获取或设置 x 轴的约束模式 | +| **YMotion** | 获取或设置 y 轴的约束模式 | +| **ZMotion** | 获取或设置 z 轴的约束模式 | +| **Upper** | 获取或设置线性限制的上限 | +| **Lower** | 获取或设置线性限制的下限 | +| **Restitution** | 获取或设置约束的恢复系数 | +| **Enable Soft Constraint** | 获取或设置是否启用软约束 | +| **Damping** | 获取或设置约束的阻尼系数 | +| **Stiffness** | 获取或设置约束的硬度系数 | + +## 角度限制 AngularLimitSettings + +![physics-configurable-angular-limit](img/configurable-angular-limit.png) + +| 属性 | 描述 | +| :-- | :-- | +| **Twist Motion** | 获取或设置扭转轴的约束模式 | +| **Swing Motion1** | 获取或设置摆动 y 轴的约束模式 | +| **Swing Motion2** | 获取或设置摆动 z 轴的约束模式 | +| **Twist Extent** | 获取或设置扭转轴的角度限制 | +| **Swing Extent1** | 获取或设置摆动 y 轴的角度限制 | +| **Swing Extent2** | 获取或设置摆动 z 轴的角度限制 | +| **Twist Restitution** | 获取或设置扭转约束的恢复系数 | +| **Swing Restitution** | 获取或设置摆动约束的恢复系数 | +| **Enable Soft Constraint Twist** | 获取或设置是否启用扭转约束的软约束 | +| **Twist Damping** | 获取或设置扭转约束的阻尼系数 | +| **Twist Stiffness** | 获取或设置扭转约束的刚度系数 | +| **Enable Soft Constraint Swing** | 获取或设置是否启用摆动约束的软约束 | +| **Swing Damping** | 获取或设置摆动约束的阻尼系数 | +| **Swing Stiffness** | 获取或设置摆动约束的硬度系数 | + +## 线性马达 LinearDriverSettings + +![physics-configurable-linear-driver](img/configurable-linear-driver.png) + +| 属性 | 描述 | +| :----------- | :-------------------------- | +| **XDrive** | 获取或设置沿 x 轴的驱动模式 | +| **YDrive** | 获取或设置沿 y 轴的驱动模式 | +| **ZDrive** | 获取或设置沿 z 轴的驱动模式 | +| **Target Orientation** | 获取或设置目标位置 | +| **Target Velocity** | 获取或设置目标速度 | +| **Strength** | 获取或设置最大驱动力 | + +驱动模式请参考下文。 + +## 角度马达 AngularDriverSettings + +![physics-configurable-angular-driver](img/configurable-angular-driver.png) + +| 属性 | 描述 | +| :-- | :-- | +| **Twist Drive** | 获取或设置沿扭转轴的驱动模式 | +| **Swing Drive1** | 获取或设置沿摆动 y 轴的驱动模式 | +| **Swing Drive2** | 获取或设置沿摆动 z 轴的驱动模式 | +| **Target Orientation** | 获取或设置目标角度 | +| **Target Velocity** | 获取或设置目标角速度 | +| **Strength** | 获取或设置最大驱动力 | + +驱动模式请参考下文。 + +## 驱动模式 + +线性马达和角度马达都可以分轴调整他的驱动模式,驱动模式有 3 种: + +- **DISABLED**:禁用模式,该模式下,对应的分轴不会受到马达驱动力或者扭矩的影响 +- **SERVO**:尝试移动/旋转到指定的位置/角度。 + - 线性马达: X,Y,Z 的 Drive 属性分别和 Target Position 的三个参数对应 + + ![servo.png](./img/servo.png) + + 以上图举例, **XDrive** 为 **SERVO** 且 **Target Position** 的 X 分量为 10,则表示约束将尝试移动到 10。 + + - 角度马达:Twist Drive,Swig Drive1,Swig Drive2 分别和 Target Orientation 的三个参数对应 +- **INDUCTION**:驱动约束到指定的速度/角速度 + - 线性马达: X,Y,Z 的 Drive 属性分别和 Target Velocity 的三个参数对应 + + ![induction.png](./img/induction.png) + + 以上图为例,**XDrive** 为 **INDUCTION** 且 **Target Velocity** 的 X 分量为 10,则表示约束将尝试将其速度移动到 10. + + - 角度马达:Twist Drive,Swig Drive1,Swig Drive2 分别和 Target Velocity 的三个参数对应 + +有关可配置约束接口,请参阅 [ConfigurableConstraint API](%__APIDOC__%/zh/class/physics.ConfigurableConstraint)。 diff --git a/versions/4.0/zh/physics/physics-engine.md b/versions/4.0/zh/physics/physics-engine.md new file mode 100644 index 0000000000..45bdfaf97f --- /dev/null +++ b/versions/4.0/zh/physics/physics-engine.md @@ -0,0 +1,116 @@ +# 设置物理引擎 + +点击编辑器上方菜单栏的 **项目 -> 项目设置 -> 功能裁剪**,在 **3D -> 物理系统** 中可以根据需要选择适合项目的物理引擎进行开发,也可以在开发过程中随时切换。目前 Creator 支持的物理引擎包括 **Bullet (ammo.js)**、**cannon.js**、**PhysX** 和 **builtin**,默认使用 **Bullet(ammo.js)**。 + +![物理引擎选项](img/physics-module.jpg) + +## 物理引擎 + +### Bullet (ammo.js) + +ammo.js([GitHub](https://github.com/cocos-creator/ammo.js) | [Gitee](https://gitee.com/mirrors_cocos-creator/ammo.js))是 [bullet](https://github.com/bulletphysics/bullet3) 物理引擎的 asm.js/wasm 版本,由 [emscripten](https://github.com/emscripten-core/emscripten) 工具编译而来。 + +**ammo.js** 模块较大(约 1.5MB),但它具有完善的物理功能,以及更佳的性能,未来我们也将在此投入更多工作。 + +### builtin + +**builtin** 内置物理引擎 **仅有碰撞检测** 的功能。相对于其它的物理引擎,它没有复杂的物理模拟计算,如果您的项目不需要这一部分的物理模拟,那么建议使用 **builtin**,使游戏的包体更小。 + +使用 **builtin** 进行开发时,请注意以下事项: + +- 只有 **trigger** 触发器类型的事件。 +- 3D 碰撞组件中的 **IsTrigger** 属性无效,所有的碰撞组件都只能用作 [触发器](physics-event.md)。 +- 3D 碰撞组件中的 `Material` 属性无效. +- 3D 碰撞组件中的 `Attached` 为 `null`。 +- 3D 物理 [刚体组件](physics-rigidbody.md) 无效. +- 3D 物理 [恒力组件](physics-constantForce.md) 无效。 +- 3D 物理 [约束组件](physics-constraint.md) 无效。 + +### cannon.js + +**cannon.js**([GitHub](https://github.com/cocos-creator/cannon.js) | [Gitee](https://gitee.com/mirrors_cocos-creator/cannon.js))是一个开源的物理引擎,使用 JavaScript 开发并实现了比较全面的物理模拟功能。**cannon.js** 模块大小约为 **141KB**。 + +当选择的物理引擎为 **cannon.js** 时,需要在节点上添加 **刚体组件** 才能进行物理模拟。然后再根据需求添加 **碰撞组件**,该节点就会增加相应的碰撞体,用于检测是否与其它碰撞体产生碰撞。 + +目前 cannon.js 支持情况如下: + +- [刚体](physics-rigidbody.md) +- [Box/Sphere 碰撞组件](physics-collider.md) +- 触发和碰撞事件 +- 物理材质 +- 射线检测 + +### PhysX + +**PhysX**([GitHub](https://github.com/NVIDIAGameWorks/PhysX))是由英伟达公司开发的开源实时商业物理引擎,它具有完善的功能特性和极高的稳定性,同时也兼具极佳的性能表现。 + +目前 Cocos Creator 支持的 **PhysX** 是 4.1 版本,允许在绝大部分原生和 Web 平台中使用。当发布到原生平台时,推荐使用 PhysX 物理,可以得到更好的物理性能,特别是发布到 iOS 时。 + +但由于 **PhysX** 目前的包体过于庞大(约 5MB)以及自身的一些限制,导致部分平台无法得到良好支持,包括: + +- 各类有包体限制的小游戏平台 +- 安卓 x86 设备 + +部分较新的平台和设备,例如 HarmonyOS 的设备,将在后续支持,请留意更新公告。Apple M1(Silicon)架构的设备已在 v3.4 支持。 + +除此之外,抖音平台提供了底层的原生物理功能,因此抖音小游戏中同样可以使用该功能,详情请参考 [发布到抖音小游戏 - 原生物理](../editor/publish/publish-bytedance-mini-game.md)。 + +### 不同物理后端碰撞形状支持情况 + +| 功能特性 | builtin | cannon.js | Bullet | PhysX +|:--------|:--------|:----------|:--------|:----| +| 质心 | ✔ | ✔ | ✔ |✔ | +| 盒、球 | ✔ | ✔ | ✔ |✔ | +| 胶囊 | ✔ | 可以用基础形状拼凑 | ✔ |✔ | +| 凸包 | | | ✔ |✔ | +| 静态地形、静态平面 | | ✔ | ✔ |✔ | +| 静态网格 | | 极其有限的支持 | ✔ |✔ | +| 圆锥、圆柱 | | ✔ | ✔ |✔ | +| 单纯形 | | 有限的支持 | ✔ |✔ | +| 复合形状 | ✔ | ✔ | ✔ |✔ | +| 射线检测、掩码过滤 | ✔ | ✔ | ✔ |✔ | +| 多步模拟、碰撞矩阵 | ✔ | ✔ | ✔ |✔ | +| 触发事件 | ✔ | ✔ | ✔ | ✔ | +| 自动休眠 | | ✔ | ✔ |✔ | +| 碰撞事件、碰撞数据 | | ✔ | ✔ |✔ | +| 物理材质 | | ✔ | ✔ |✔ | +| 静态、运动学 | ✔ | ✔ | ✔ |✔ | +| 动力学 | | ✔ | ✔ |✔ | +| 点对点、铰链约束(实验) | | ✔ | ✔ |✔ | +| wasm | | | ✔ |✔ | + +## 不使用物理 + +若不需要用到任何物理相关的组件和接口,可以取消物理系统选项的勾选,使游戏的包体更小。 + +> **注意**:若物理系统选项处于取消勾选的状态,项目将不可以使用物理相关的组件和接口,否则运行时将会报错。 + +## 物理引擎的性能表现 + +主要针对各类小游戏平台和原生平台,并对使用 **Bullet** 和 **PhysX** 物理时的性能进行了对比: + +- 在原生和抖音小游戏平台上,使用 **PhysX** 物理可以得到更加良好的性能。 +- 在各类小游戏平台上,使用 **Bullet** 物理可以得到更加良好的性能。 + +## 物理引擎的效果差异 + +不同的物理引擎,其内部的设计和算法都不相同,因此会出现一些参数相同但是效果不同的情况,这些差异主要包括以下三类: + +1. 刚体组件上的 **damping** 属性 + + 由于 PhysX 物理使用了不同的阻尼算法模型导致的差异。但此差异已被内部消除,如果需要同步之前在 PhysX 中设置的阻尼值,可以参考以下代码进行转换: + + ```ts + const dt = PhysicsSystem.instance.fixedTimeStep; + const newDamping = 1 - (1 - oldDamping * dt) ** (1 / dt); + ``` + +2. 刚体组件上的 **factor** 属性 + + 由于 PhysX 物理只固定刚体自由度,没有提供刚体速度的缩放因子导致的,即刚体组件中的 **Linear Factor** 和 **Angular Factor** 属性对 PhysX 物理只有固定效果。之后将会在内部消除此差异。 + +3. 物理材质 + + PhysX 中的物理材质支持静态摩擦系数和弹性系数,与 Creator 中的物理材质资源相比,缺少了动态摩擦系数。该系数目前与静态摩擦系数保持一致,这部分的差异目前暂时无法提供转换方式。 + +除了以上提到的几点之外,也存在其它算法的差异,例如数值积分方法、LCP 求解算法、求解精度等,因此始终会有不同的效果,不过这些在实际项目开发中不太容易感知。 diff --git a/versions/4.0/zh/physics/physics-event.md b/versions/4.0/zh/physics/physics-event.md new file mode 100644 index 0000000000..efc2d39d6c --- /dev/null +++ b/versions/4.0/zh/physics/physics-event.md @@ -0,0 +1,123 @@ +# 物理事件 + +## 触发器与碰撞器 + +![img](img/isTrigger.jpg) + +碰撞组件属性 **IsTrigger** 决定了组件为触发器还是碰撞器。将 **IsTrigger** 设置为 `true` 时,组件为触发器。触发器只用于碰撞检测和触发事件,会被物理引擎忽略。默认设置 `false`,组件为碰撞器,可以结合[刚体](physics-rigidbody.md)产生碰撞效果。 + +两者的区别如下: + +- 触发器不会与其它触发器或者碰撞器做更精细的检测。 +- 碰撞器与碰撞器会做更精细的检测,并会产生碰撞数据,如碰撞点、法线等。 + +## 触发事件 + +触发事件目前包括以下三种: + +| 事件 | 说明 | +| :--------------- | :------- | +| `onTriggerEnter` | 触发开始时触发该事件 | +| `onTriggerStay` | 触发保持时会频发触发该事件 | +| `onTriggerExit` | 触发结束时触发该事件 | + +其中可产生触发事件的碰撞对为: + +| 类型 | 静态刚体 | 运动学刚体 | 动力学刚体 | +| :--------- | :------- | :--------- | :--------- | +| 静态刚体 | | ✔ | ✔ | +| 运动学刚体 | ✔ | ✔ | ✔ | +| 动力学刚体 | ✔ | ✔ | ✔ | + +> **注意**:接收到触发事件的前提是两者都必须带有碰撞组件,并且至少有一个是触发器类型。当使用物理引擎为非 builtin 物理引擎时,还需要确保至少有一个物体带有的是非静态刚体(只有碰撞组件没有刚体组件的对象,视为持有静态刚体的对象),而 builtin 物理引擎则没有这个限制。 + +### 监听触发事件 + +```ts +// 此处的节点添加了 BoxCollider 组件 +import { BoxCollider, ITriggerEvent } from 'cc' + +public start () { + let collider = this.node.getComponent(BoxCollider); + collider.on('onTriggerStay', this.onTriggerStay, this); +} + +private onTriggerStay (event: ITriggerEvent) { + console.log(event.type, event); +} +``` + +## 碰撞事件 + +碰撞事件根据碰撞数据生成,静态类型的刚体之间不会产生碰撞数据。 + +目前碰撞事件包括以下三种: + +| 事件 | 说明 | +| :----------------- | :------- | +| `onCollisionEnter` | 碰撞开始时触发 | +| `onCollisionStay` | 碰撞保持时不断的触发 | +| `onCollisionExit` | 碰撞结束时触发 | + +其中可产生碰撞事件的碰撞对为: + +| 类型 | 静态刚体 | 运动学刚体 | 动力学刚体 | +| :--------- | :------- | :--------- | :--------- | +| 静态刚体 | | ✔ | ✔ | +| 运动学刚体 | ✔ | ✔ | ✔ | +| 动力学刚体 | ✔ | ✔ | ✔ | + +> **注意**:接收到碰撞事件的前提是两者都必须带有碰撞组件、至少有一个是非静态刚体并且使用的是非 builtin 的物理引擎。 + +### 监听碰撞事件 + +```ts +import { Collider, ICollisionEvent } from 'cc' + +public start () { + let collider = this.node.getComponent(Collider); + // 监听触发事件 + collider.on('onCollisionStay', this.onCollision, this); +} + +private onCollision (event: ICollisionEvent) { + console.log(event.type, event); +} +``` + +> **注意**:目前碰撞事件以物理元素为单位,所有该元素上的碰撞器组件都会接收到碰撞事件。 + +## 触发事件和碰撞事件区别 + +- 触发事件由触发器生成,碰撞事件根据碰撞数据生成。 +- 触发事件可以由触发器和另一个触发器/碰撞器产生。 +- 碰撞事件需要由两个碰撞器产生,并且至少有一个是动力学刚体。 + +## 连续碰撞检测 + +相对于连续的真实世界,物理引擎的模拟实际上是离散的,这意味着物理引擎需要经过 1/60 秒或者其他定义的时间才可以采样一次。因此对于速度较快的物体,物理引擎可能无法正确检测到其碰撞结果,若要解决这种现象,可开启连续物理检测(CCD)来解决。 + +在引擎中开启连续碰撞检测的方法如下: + +```ts +const rigidBody = this.getComponent(RigidBody); +rigidBody.useCCD = true; +``` + +参考 [连续碰撞检测](physics-ccd.md) 以获取更详细的描述。 + +## 触发规则 + +碰撞事件的产生会根据刚体、碰撞体或触发器的类型不同而不同,这里将结果整理如下所示: + +> **注意**:此处标记的刚体都携带有碰撞体且在碰撞矩阵内配置为允许碰撞。 + +| - | 静态刚体 | 运动学刚体 | 动力学刚体 | 碰撞体 | 触发器 | +| :--------- | :------- | :--------- | :--------- | :----- | :----- | +| 静态刚体 | | ✔ | ✔ | | | +| 运动学刚体 | ✔ | ✔ | ✔ | ✔ | ✔ | +| 动力学刚体 | ✔ | ✔ | ✔ | ✔ | ✔ | +| 碰撞体 | | ✔ | ✔ | | | +| 触发器 | | ✔ | ✔ | | | + +> **注意**: PhysX 物理后端不支持同时为触发器的碰撞体间的碰撞响应。 diff --git a/versions/4.0/zh/physics/physics-example.md b/versions/4.0/zh/physics/physics-example.md new file mode 100644 index 0000000000..ad1974560c --- /dev/null +++ b/versions/4.0/zh/physics/physics-example.md @@ -0,0 +1,73 @@ +# 物理应用案例 + +## 射箭案例 + +1. 用基础形状组合十字架——复合形状 + + 下图中用两个盒形状组合一个十字架,节点上所有的碰撞体组合成了一个十字形状,这是实现带有凹面形状最基础的方法: + + ![composite-shape](example-img/composite-shape.jpg) + + 容易误用的地方是在多个节点中添加碰撞体拼凑出十字架后,希望它碰撞后可以保持整体结构进行运动,这在目前的结构中是无法做到的,只能往单个节点上添加碰撞体来实现。 + +2. 射箭与回收箭——运动学、动力学、事件 + + 射箭的第一步是拉弓,箭需要完全跟随弹性绳骨骼一起运动,不希望箭受到物理规则的影响,此时应将箭的刚体设置为 `Kinematic` 类型;第二步是松开弹性绳发射箭,这时希望给箭设置初速度后,可以按照物理规则进行运动,因此将箭的刚体设置为 `Dynamic` 类型。 + + ![bow-pulling](example-img/bow-pulling.gif) + + 回收箭的大致过程是在箭射出去后,一旦触碰到触发区域就将其还原到弓上。这可以通过制作监听区域来实现,首先利用碰撞体组件拼凑出区域,同时将碰撞体组件的 `IsTrigger` 勾选上(下图中的地面,也就是蓝框圈出的部分为监听区域)。 + + ![recycle-area](example-img/recycle-area.jpg) + + 上述容易产出错误的地方有: + + 1. 希望修改变换信息来操作动力学(Dynamic)类型的刚体运动,应当通过速度、力或冲量等物理层的数值来控制。 + + 2. 利用静态(Static)类型的刚体来监听事件,静态刚体只会和带有类型为运动学或动力学的刚体产生事件。如果两者都为静态,则不产生事件。 + + 3. 监听事件时仅监听了触发开始(OnTriggerEnter),但是误以为包括了触发保持(OnTriggerStay)和结束(OnTriggerExit)。 + +3. 瞄准——碰撞矩阵(过滤检测)、射线检测、静态平面 + + 瞄准是射箭前的步骤,准心处于箭头指向所在的射线上,在十字架前面加一个静态平面碰撞体,然后利用射线检测就可以得到准心的位置。 + + ![arm](example-img/arm.jpg) + + 静态平面只是用来做射线检测,给它专门建立一个分组,并且不与箭、苹果等等物体进行检测,这是最通用的性能优化方法。 + + ![config](example-img/config.jpg) + + 调用射线检测方法时,设置传入掩码为仅和静态平面检测,即 `0b10`(二进制表示法)。 + + 上述容易产出错误的地方有: + + 1. 分不清刚体组件上的分组和节点上的层。这两者的概念类似,但是使用者不同,分组的使用者是物理模块,层的使用者是渲染模块。 + + 2. 对掩码的理解不到位,不知传何值。这里提供一个小技巧,以一个能够筛选出 `Others` 的掩码举例,首先Others的index值为2,哪么只要让二进制掩码从右往左的顺序第 `2` 位为 `1`,就能让 `Others` 通过筛选,也就是 `ob100`(这里强烈建议不要随便更改分组的索引)。 + + 3. 误认为射线检测接口的返回值是击中的数据。获取结果有专门的接口,此处设计是为了强调这是个复用对象。为了减少垃圾内存,每次调用接口只会更新它们的数据,而不是重新生成新的(若需要持久记录,哪么可以克隆一份)。 + +4. 射击苹果——静态网格、凸包、多步模拟(步长调整) + + 一般的苹果都带有凹面,处理好凹类或带连续平滑不规则曲面的模型都非常棘手,这是因为目前成熟的理论和技术都建立在离散、凸包的世界之上(微积分中用差分近似表示微分就是最典型的范例)。 + + 在实时物理引擎中,对于这类物体只能支持到静态或运动学类型的刚体层级,对于动力学就束手无策了。然而不幸的是,真实的苹果运动表现强烈依赖动力学,这种情况只能给苹果填加凸包形式的网格碰撞体(需将 convex 勾选上),再加上一个动力学刚体,用近似物体去参与模拟。 + + ![arm-apple](example-img/arm-apple.gif) + + 运动表现与模拟参数有非常大的关系,穿透是最具有代表性的现象,这可以通过缩减步长和增加步数来实现,调整步长有个小技巧:输入分式,即 `1/Frame`,其中 `Frame` 表示帧率。 + + ![set-step](example-img/set-step.gif) + + 上述容易产出错误的地方有: + + 1. 在带有未勾选 `convex` 的网格碰撞器上添加了的动力学刚体,与其它物体产生了穿透现象,或者说完全没有反应,这是典型的错误使用,只有勾上 `convex` 的才能支持动力学刚体。 + + 2. 对一个顶点数极多的模型开启了 `convex`,过多的顶点数会使凸包的面数增多,这对性能有很大的影响,而且实际上并不需要面数特别多的凸包,一般建议模型的顶点数应小于 `255`。 + + 3. 开启凸包后,模型的凹面处的接触不贴近,这是正常现象,现在的实时技术是将模型用多个凸包组合来解决,如下图所示: + + ![set-step](example-img/convex.jpg) + + 4. 只调整了步长,但未调整步数,这两者需要相互配合才有效果。小技巧是,步数可以随意设置较大的值,步长根据最大的速度值进行调整,值越大,步长应当越小。 \ No newline at end of file diff --git a/versions/4.0/zh/physics/physics-group-mask.md b/versions/4.0/zh/physics/physics-group-mask.md new file mode 100644 index 0000000000..53da500f81 --- /dev/null +++ b/versions/4.0/zh/physics/physics-group-mask.md @@ -0,0 +1,123 @@ +# 分组和掩码 + +分组和掩码是物体之间能否进行物理碰撞检测的必要条件。分组可以简单的理解为一个碰撞对象所处的分组,掩码可以简单的理解为该碰撞对象需要与哪些分组对象进行碰撞。 + +## 碰撞检测原理 + +在 Cocos Creator 中是否进行碰撞检测采用的是二进制 [按位操作](https://www.w3school.com.cn/js/js_bitwise.asp),通过将分组值与掩码值进行“与”运算,从而判断是否满足条件。对象间允许进行碰撞检测的计算方式如下: + +```ts +(GroupA & MaskB) && (GroupB & MaskA) +``` + +从这个公式中可以看出,分组 A 需要满足在分组 B 的掩码列表里并且分组 B 也需要满足在分组 A 的掩码列表里,这样的两个对象之间就能进行碰撞检测。如何将二进制操作与允许进行碰撞检测计算公式结合起来,这是下面需要了解的部分。但在这之前,需要用户在 **项目设置 -> 物理 -> 碰撞矩阵** 处先配置 [碰撞分组](physics-configs.md#碰撞矩阵)。 + +![set-collider-config](img/set-collider-config.png) + +根据上图的配置,Cocos Creator 会将数据解析为以下值(此处仅针对讲解的部分做展示): + +- **DEFAULT**:**Index** 值为 `0`,分组实际值为 `1<<0=1`,二进制值为 `0000 0001`;掩码值实际值为 `1<<0=1`,二进制值为 `0000 0001`。 +- **SELF_PLANE**:**Index** 值为 `1`,分组实际值为 `1<<1=2`,二进制为 `0000 0010`;掩码值实际值为 `(1<<3)+(1<<4)=24`,二进制值为 `0001 1000`。 +- **ENEMY_BULLET**:**Index** 值为 `4`,分组实际值为 `1<<4=16`,二进制为 `0001 0000`;掩码值实际值为 `1<<1=2`,二进制值为 `0000 0010`。 + +根据数据就可以对分组之间是否碰撞做计算: + +- 分组 **SELF_PLANE** 是否与分组 **DEFAULT** 碰撞: + + ![cant-collider](img/cant-collider.png) + + 根据以上计算最终得出的值是 `0`,因此两个分组间不会产生碰撞。 + +- 分组 **SELF_PLANE** 是否与分组 **ENEMY_BULLET** 碰撞: + + ![can-collider](img/can-collider.png) + + 根据以上计算最终得出的值大于 `0`,因此两个分组间会产生碰撞。 + +> **注意**:`<<` 左移运算符,是位运算符的一种,通过从右推入 0 向左位移,并使最左边的位脱落。 + +## 动态设置分组和掩码 + +### 定义分组 + +通常,在游戏开发中,需要在碰撞发生前设置好可碰撞分组,在碰撞发生时处理相关的逻辑。在 Cocos Creator 中,所有的碰撞数据获取到的是数值,这样不利于开发过程中的判断。因此,可以通过定义分组对象或者枚举的形式,清晰的知道每一串数字的意义。 + +在可以采用左移操作符(<<)对分组或者掩码进行设置,并且无论是分组对象还是分组枚举,对应的值都应该和碰撞矩阵定义的值一致,否则可能出现数据不一致,从而导致判断失败。 + +方式一:定义在一个 **object** 中 + +```ts +export const PHY_GROUP = { + DEFAULT: 1 << 0, + SELF_PLANE: 1 << 1, + ENEMY_PLANE = 1 << 2, + SELF_BULLET = 1 << 3, + ENEMY_BULLET = 1 << 4, + BULLET_PROP = 1 << 5, +}; +``` + +方式二:定义在一个 **enum** 中 + +```ts +enum PHY_GROUP { + DEFAULT = 1 << 0, + SELF_PLANE = 1 << 1, + ENEMY_PLANE = 1 << 2, + SELF_BULLET = 1 << 3, + ENEMY_BULLET = 1 << 4, + BULLET_PROP = 1 << 5, +}; + +// 如果该枚举需要在属性面板上显示,需要引入来自 cc 模块的 Enum 函数,将定义好的枚举注册到编辑器中 +Enum(PHY_GROUP); +``` + +> **注意**:由于历史原因, **Enum** 函数对 **-1** 有特殊处理,如果不熟悉,请勿定义值为 **-1** 的属性。 + +### 设置/获取分组 + +```ts +// 此处案例使用的是上方 “定义分组” 处定义的枚举 +const rigid = this.getComponent(RigidBody); +// 等价于 rigid.setGroup(1 << 1) 或 rigid.setGroup(1) +rigid.setGroup(PHY_GROUP.SELF_PLANE); + +const group = rigid.getGroup(); +``` + +### 添加/删除分组 + +```ts +// 如果当前分组并未在碰撞矩阵中定义,也可以动态添加 +const group = 1 << 7; +const rigid = this.getComponent(RigidBody); +rigid.addGroup(group); +rigid.removeGroup(group); +``` + +### 设置/获取掩码 + +```ts +const rigid = this.getComponent(RigidBody); +const mask = (1 << 0) + (1 << 1); // 等价于 1 << 0 | 1 << 1 +rigid.setMask(mask); +rigid.getMask(); +``` + +> **注意**:此处需要注意操作符的优先级。例如:3 + 1 << 2 和 3 + (1 << 2) 所计算的值是不相等的,操作符 + 的优先级大于 <<。 + +#### 使用掩码 + +掩码可以根据分组进行定义,例如: + +- 定义一个只检测 **DEFAULT** 的掩码 `const maskForGroup1 = PHY_GROUP.DEFAULT;` +- 定义一个可检测 **DEFAULT** 和 **SELF_PLANE** 的掩码 `const maskForGroup01 = PHY_GROUP.DEFAULT | PHY_GROUP.SELF_PLANE;` +- 定义一个所有组都不检测的掩码 `const maskForNone = 0;` +- 定义一个所有组都检测的掩码 `const maskForAll = 0xffffffff;` + +### 查看分组或掩码二进制 + +通过 `(value >>> 0).toString(2)`,可以看到二进制的字符串表示。 + +![查看二进制](img/mask-all.jpg) \ No newline at end of file diff --git a/versions/4.0/zh/physics/physics-material.md b/versions/4.0/zh/physics/physics-material.md new file mode 100644 index 0000000000..ed6e912a34 --- /dev/null +++ b/versions/4.0/zh/physics/physics-material.md @@ -0,0 +1,94 @@ +# 物理材质 + +物理材质是一种资源,它记录了物体的物理属性,这些信息用来计算碰撞物体受到的摩擦力和弹力等。 + +## 创建物理材质 + +### 在编辑器内创建 + +在 **属性检查器** 内右键任意空白处或点击 **+** 号间都可以创建物理材质: + +![创建物理材质](img/material-create-pmtl.png) + +### 通过代码创建 + +也可通过代码实例化物理材质: + +```ts +import { PhysicsMaterial } from 'cc'; + +let newPMtl = new PhysicsMaterial(); +newPMtl.friction = 0.1; +newPMtl.rollingFriction = 0.1; +newPMtl.spinningFriction = 0.1; +newPMtl.restitution = 0.5; +``` + +## 属性 + +物理材质属性如下图所示: + +![物理材质](img/physics-mat-panel.png) + +| 属性 | 属性说明 | +| :-- | :-- | +| Friction | 摩擦系数 | +| RollingFriction | 滚动摩擦系数 | +| SpinningFriction | 自旋摩擦系数 | +| Restitution | 回弹系数 | + +当与其它表面接触时,这些系数用于计算相应的摩擦力和弹力。 + +## 应用 + +目前物理材质以碰撞体为单位进行设置,每个 **Collider** 都具有一个 **Material** 的属性(不设置时, **Collider** 将会引用物理系统中的默认物理材质)。 + +应用到 **Collider** 同样也分编辑器操作和代码操作两种方式。 + +编辑器内操作,只需要将资源拖入到 **cc.PhysicMaterial** 属性框中即可,如下图所示: + +![应用物理材质](img/apply-pmtl.jpg) + +在代码中操作: + +```ts +import { Collider } from 'cc'; + +let collider = this.node.getComponent(Collider); +if (collider) { + collider.material = newPMtl; + collider.material.rollingFriction = 0.1; +} +``` + +## 共享材质 + +在物理系统中,物理材质拥有共享材质和独享材质两种状态。 + +- 共享材质:不同的碰撞体,共用同一个材质,对该材质的修改会影响到所有持有该材质的碰撞体。在默认情况下,碰撞体的物理材质都通过引擎的默认物理材质初始化。通过 `sharedMaterial` 可以访问,代码示例如下: + + ```ts + import { Collider } from 'cc'; + let collider = this.node.getComponent(Collider); + if (collider) { + let sharedMaterial = collider.sharedMaterial; + // 或 + collider.sharedMaterial.friction = 0.5 + + collider.sharedMaterial = newPMtl; + } + ``` + +- 独享材质:只供该碰撞体使用,修改该材质不会影响其他碰撞体。 + + 若一个碰撞体的物理材质处于共享状态,则在调用 `material` 的 `getter` 时,会生成新的物理材质。 + + 下列代码演示了当物理材质处于共享状态时,调用 `getter` 后,该碰撞体的材质变为独享状态。 + + ```ts + import { Collider } from 'cc'; + let collider = this.node.getComponent(Collider); + if (collider) { + const material = collider.material; + } + ``` \ No newline at end of file diff --git a/versions/4.0/zh/physics/physics-raycast.md b/versions/4.0/zh/physics/physics-raycast.md new file mode 100644 index 0000000000..95cdac4678 --- /dev/null +++ b/versions/4.0/zh/physics/physics-raycast.md @@ -0,0 +1,182 @@ +# 射线和线段检测 + +本文将说明如何通过射线和线段对物理世界内的碰撞体进行检测。 + +## 射线检测 + +## 射线检测 + +射线检测是对一条射线和另一个形状进行 **相交性判断**,如下图所示。 + +![图解](img/raycast.jpg) + +### 构造射线 + +射线 **ray** 由 **起点** 和 **方向** 组成,构造一条射线有以下几种比较常见的方法: + +1. 通过 **起点** + **方向**,**ray** 的构造函数或静态接口 `create`: + + ```ts + import { geometry } from 'cc'; + + // 构造一条从(0,-1,0)出发,指向 Y 轴的射线 + // 前三个参数是起点,后三个参数是方向 + const outRay = new geometry.Ray(0, -1, 0, 0, 1, 0); + + // 或者通过静态方法 create + const outRay2 = geometry.Ray.create(0, -1, 0, 0, 1, 0); + ``` + +2. 通过 **起点** + **射线上的另一点**,**ray** 的静态接口 `fromPoints`: + + ```ts + import { geometry, math } from "cc"; + // 构造一条从原点出发,指向 Z 轴的射线 + const outRay = new geometry.Ray(); + geometry.Ray.fromPoints(outRay, math.Vec3.ZERO, math.Vec3.UNIT_Z); + ``` + +3. 用相机构造一条从相机原点到屏幕某点发射出的射线: + + ```ts + import { geometry, Camera } from "cc"; + // 以脚本挂载在 Camera 下为例 + const camera = this.node.getComponent(Camera); + // 获得一条途径屏幕坐标(0,0)发射出的一条射线 + const outRay = new geometry.Ray(); + camera?.screenPointToRay(0, 0, outRay); + ``` + + > **注意**: + > 1. 首先需要获取一个相机组件或者相机实例的引用。 + > 2. 相机组件和相机实例两者暴露的接口参数顺序不一样。 + +### 接口介绍 + +Cocos Creator 提供了一套基于物理引擎的射线检测功能。 + +目前接口由 [**PhysicsSystem**](%__APIDOC__%/zh/class/physics.PhysicsSystem) 提供,有以下两类: + +- `raycast` : 检测所有的碰撞体,并记录所有被检测到的结果,通过 `PhysicsSystem.instance.raycastResults` 获取。接口返回布尔值,返回 `true` 表示射线是否和碰撞体相交。 +- `raycastClosest` :检测所有的碰撞体,并记录与射线距离最短的检测结果,通过 `PhysicsSystem.instance.raycastClosestResult` 获取。同样返回布尔值,表示是否和碰撞体相交。 + +> **注意**: +> 1. 检测的对象是物理碰撞器,在场景面板上与之对应的是碰撞器组件,例如 **BoxCollider**。 +> 2. 检测结果返回对象是只读并且复用的,每次调用检测接口后会更新相应结果。 + +基于相机位置和屏幕坐标进行射线检测最近物体的常用示例: + +```ts +let ray = new geometry.Ray(); +this.camera.screenPointToRay(eventMouse.getLocationX(), eventMouse.getLocationY(), ray); +// 以下参数可选 +const mask = 0xffffffff; +const maxDistance = 10000000; +const queryTrigger = true; + +if (PhysicsSystem.instance.raycastClosest(ray, mask, maxDistance, queryTrigger)) { + const raycastClosestResult = PhysicsSystem.instance.raycastClosestResult; + const hitPoint = raycastClosestResult.hitPoint + const hitNormal = raycastClosestResult.hitNormal; + const collider = raycastClosestResult.collider; + const distance = raycastClosestResult.distance; +} +``` + +下列代码演示了如何检测多个物体: + +```ts +const worldRay = new geometry.Ray(0, -1, 0, 0, 1, 0); +// 以下参数可选 +const mask = 0xffffffff; +const maxDistance = 10000000; +const queryTrigger = true; + +const bResult = PhysicsSystem.instance.raycast(worldRay, mask, maxDistance, queryTrigger); +if(bResult){ + const results = PhysicsSystem.instance.raycastResults; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + const collider = result.collider; + const distance = result.distance; + const hitPoint = result.hitPoint; + const hitNormal = result.hitNormal; + } +} +``` + +#### 参数说明 + +- `worldRay`:世界空间下的射线 +- `mask`:用于过滤的 [掩码](physics-group-mask.md),可以传入需要检测的分组,默认为 0xffffffff +- `maxDistance`:最大检测距离,默认为 10000000,目前请勿传入 `Infinity` 或 `Number.MAX_VALUE` +- `queryTrigger`:是否检测触发器 + +#### 返回结果说明 + +射线检测的结果由 [PhysicsRayResult](%__APIDOC__%/zh/class/physics.PhysicsRayResult) 进行存储,主要有以下信息: + +- `collider`:击中的碰撞器 +- `distance`:击中点与射线起点的距离 +- `hitPoint`:击中点(世界坐标系中) +- `hitNormal`:击中点所处面的法线(世界坐标系中) + +## 线段检测 + +自 v3.7 起,Cocos Creator 支持线段/采样点检测。线段检测分为两个方法 `lineStripCast` 以及 `lineStripCastClosest`。 + +### 接口描述 + +该方法内部由 `raycast` 实现,通过定义 `samplePointsWorldSpace` 参数可以很方便的检测曲线是否有击中其他碰撞体。 + +```ts +lineStripCast (samplePointsWorldSpace: Array, mask = 0xffffffff, maxDistance = 10000000, queryTrigger = true): boolean; + +lineStripCastClosest (samplePointsWorldSpace: Array, mask = 0xffffffff, maxDistance = 10000000, queryTrigger = true): boolean; +``` + +#### 参数说明 + +- `samplePointsWorldSpace`:世界空间下的采样点/直线段 +- `mask`:用于过滤的 [掩码](physics-group-mask.md),可以传入需要检测的分组,默认为 0xffffffff +- `maxDistance`:最大检测距离,默认为 10000000,目前请勿传入 `Infinity` 或 `Number.MAX_VALUE` +- `queryTrigger`:是否检测触发器 + +#### 返回结果说明 + +返回 true 时,表示曲线和碰撞体相交。结果存储在 `PhysicsSystem.Instance.lineStripCastResults` 以及 +`PhysicsSystem.Instance.lineStripCastResults`,请参考下文: + +### 使用方法 + +调用后如果方法返回 true,则可以通过 `lineStripCastClosestResult` 和 `lineStripCastResults` 获取检测结果。 + +```ts +if (PhysicsSystem.instance.lineStripCastClosest(sampleArray, ...)) { + const result = PhysicsSystem.instance.lineStripCastClosestResult; + ... +} +``` + +检测的结果类型为 `PhysicsLineStripCastResult`,`PhysicsLineStripCastResult` 继承自 `PhysicsRayResult`,属性描述如下: + +- `id`:发生相交时线段的索引 +- `collider`:击中的碰撞器 +- `distance`:击中点与射线起点的距离 +- `hitPoint`:击中点(世界坐标系中) +- `hitNormal`:击中点所处面的法线(世界坐标系中) + +`lineStripCast` 检测的结果是一个 `PhysicsLineStripCastResult` 类型的数组,属性描述如上所示,代码示例如下: + +```ts +if (PhysicsSystem.instance.lineStripCast(sampleArray, ... )) { + const results = PhysicsSystem.instance.lineStripCastResults; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + ... + } +} +``` + +如果要投射其他几何体,请参考 [几何投射检测](./physics-sweep.md)。 diff --git a/versions/4.0/zh/physics/physics-rigidbody.md b/versions/4.0/zh/physics/physics-rigidbody.md new file mode 100644 index 0000000000..918823a79e --- /dev/null +++ b/versions/4.0/zh/physics/physics-rigidbody.md @@ -0,0 +1,218 @@ +# 刚体组件 + +刚体是组成物理世界的基本对象,它可以使游戏对象的运动方式受物理控制。例如:刚体可以使游戏对象受重力影响做自由下落,也可以在力和扭矩的作用下,让游戏对象模拟真实世界的物理现象。 + +## 添加刚体 + +### 通过编辑器添加 + +点击 **属性检查器** 下方的 **添加组件 -> Physics -> RigidBody**,即可添加刚体组件到节点上。 + +![add-rigidbody-in-inspector](img/add-rigidbody-in-inspector.jpg) + +### 程序化添加/获取 + +```ts +import { RigidBody } from 'cc' + +// 添加刚体 +const rigidbody = this.node.addComponent(RigidBody); + +// 获取刚体 +const rigidBody = this.node.getComponent(RigidBody); +``` + +刚体的 API 接口请参考 [Class RigidBody](%__APIDOC__%/zh/class/physics.RigidBody)。 + +### 什么情况下需要添加刚体 + +1. 配置碰撞分组并让其生效。 +2. 物体需要具备运动学或动力学行为。 + +> **注意**:物体需要具备完全物理特性的前提条件物体同时具备 **刚体** 和 **碰撞体**,并调整好其质心位置和碰撞体的形状。 + +## 刚体属性 + +![rigidbody](img/rigid-body.jpg) + +| 属性 | 说明 | +| :-------- | :------------------------------------------------------------------------------- | +| **Group** | 刚体分组 | +| **Type** | 刚体类型。
**DYNAMIC**:动力学
**STATIC**:静态
**KINEMATIC**:运动学 | + +> **注意**:碰撞体间定义碰撞发生的可能性是通过刚体的 **Group** 属性,而非 **Node** 的 **Layer** 属性。 + +以下属性仅在 **Type** 设为 **DYNAMIC** 时生效: + +| 属性 | 说明 | +| :----------------- | :--------------------------------------------------- | +| **Mass** | 刚体质量,该值需大于 **0** | +| **AllowSleep** | 是否允许自动休眠 | +| **LinearDamping** | 线性阻尼,用于衰减线性速度,值越大,衰减越快 | +| **AngularDamping** | 角阻尼,用于衰减角速度,值越大,衰减越快 | +| **UseGravity** | 是否使用重力 | +| **LinerFactor** | 线性因子,用于缩放每个轴方向上的物理数值(速度和力) | +| **AngularFactor** | 角因子,用于缩放每个轴方向上的物理数值(速度和力) | + +刚体组件接口请参考 [RigidBody API](%__APIDOC__%/zh/class/physics.RigidBody)。 + +## 刚体类型 + +目前刚体类型包括 **DYNAMIC**、**KINEMATIC** 和 **STATIC**。 + +![rigidbody-type](img/rigidybody-type.png) + +- **STATIC**:静态刚体。可以是手动设置刚体类型的游戏对象,也可以是具有碰撞体而没有刚体的游戏对象。如果一个节点默认只添加了碰撞器而没有添加刚体,那么这个节点可以认为默认使用的是 **静态刚体**。静态刚体在大多数情况下用于一些始终停留在一个地方,不会轻易移动的游戏物体,例如:建筑物。若物体需要持续运动,应设置为 **KINEMATIC** 类型。静态刚体与其他物体发生碰撞时,不会产生物理行为,因此,也不会移动。 +- **DYNAMIC**:动力学刚体。刚体碰撞完全由物理引擎模拟,可以通过 **力的作用** 运动物体(需要保证质量大于 0)。例如:斯诺克游戏击球后,母球滚动与其他球撞击; +- **KINEMATIC**:运动学刚体。具有碰撞体和运动刚体,可以直接通过移动刚体对象的变换属性,但不会像动力学刚体一样响应力和碰撞,通常用于表达电梯这类平台运动的物体。它与静态刚体类似,不同的地方在于移动的运动刚体会对其他对象施加摩擦力,并在接触时唤醒其他刚体。 + +**示例**:通过一个简单的物理模拟说明各种类型刚体所具有的表现。下图方块中白色用了静态刚体,蓝色用了运动学刚体,黄色用了动力学刚体。其中白色和蓝色都是操控的变换信息,很明显的看出几个表现: +1. 白色和蓝色之间会出现穿透现象。 +2. 白色的静态物体也可以运动。 +3. 两个黄色方块表现不同,白色上方的静止不动,蓝色上方的会跟随着运动。 + +![physics-type](img/physics-type.gif) + +以上现象的原因是: +1. 静态刚体和运动学刚体都不会受到力的作用,所以产生了穿透,这是正常现象。 +2. 静态物体的确是可以运动的,静态是指在时空中,每一个时刻都是静态,不会考虑其它时刻的状态。 +3. 与静态物体不同,运动学物体会根据附近时刻估算出运动状态(比如速度),又由于摩擦力的作用,因此带动了黄色方块。 + +## 刚体质心 + +在默认情况下,刚体的质心和模型的原点是一致的。 + +下图演示了当模型原点不一致时,质心位置的变化: + +![Centroid](img/center-of-mass.jpg) + +这里用一个示例来说明如何调整碰撞的质心: + +- 新建一个空的节点 **Node** 并添加下图所示的组件: + + ![add comp](img/center-add-comp.png) + +- 在 **Node** 的子节点下添加一个胶囊体,如下图所示: + + ![add capsule](img/center-add-cupsule.png) + +- 调整 **cc.CapsuleCollider** 的 Center 为下图所示,则此时胶囊体的质心则在胶囊体的底部: + + ![result](img/center-result.png) + +通过下图可观察到当质心不一致时的运动表现,右边的胶囊体质心位于胶囊体的底部,左边的胶囊体质心位于物体的中心。 + + ![result](img/center-of-mass.gif) + +## 控制刚体 + +### 让刚体运动起来 + +针对不同的类型,让刚体运动的方式不同: + +- 对于静态刚体(**STATIC**),应当尽可能保持物体静止,但仍然可以通过变换(位置、旋转等)来改变物体的位置。 +- 对于运动学刚体(**KINEMATIC**),应当通过改变变换(位置、旋转等)使其运动。 + +对于动力学(**DYNAMIC**)刚体,需要改变其速度,有以下几种方式: + +#### 通过重力 + +刚体组件提供了 **UseGravity** 属性,需要使用重力时候,需将 **UseGravity** 属性设置为 `true`。 + +#### 通过施加力 + +刚体组件提供了 `applyForce` 接口,根据牛顿第二定律,可对刚体某点上施加力来改变物体的原有状态。 + +```ts +import { math } from 'cc' + +rigidBody.applyForce(new math.Vec3(200, 0, 0)); +``` + +#### 通过施加扭矩 + +刚体组件提供了 `applyTorque` 接口,可用于改变刚体的角速度。 + +```ts +rigidBody.applyTorque(new math.Vec3(200, 0, 0)); +``` + +#### 通过施加冲量 + +刚体组件提供了 `applyImpulse` 接口,施加冲量到刚体上的一个点,根据动量定理,将立即改变刚体的线性速度。 如果冲量施加到的点在力方向上的延长线不过刚体的质心,那么将产生一个非零扭矩并影响刚体的角速度。 + +```ts +rigidBody.applyImpulse(new math.Vec3(5, 0, 0)); +``` + +#### 通过改变速度 + +刚体组件提供了 `setLinearVelocity` 接口,可用于改变线性速度。 + +```ts +rigidBody.setLinearVelocity(new math.Vec3(5, 0, 0)); +``` + +刚体组件提供了 `setAngularVelocity` 接口,可用于改变旋转速度。 + +```ts +rigidBody.setAngularVelocity(new math.Vec3(5, 0, 0)); +``` + +### 限制刚体的运动 + +#### 通过休眠 + +休眠刚体时,会将刚体所有的力和速度清空,使刚体停下来。 + +```ts +if (rigidBody.isAwake) { + rigidBody.sleep(); +} +``` + +唤醒刚体时,刚体的力和速度将会恢复。 + +```ts +if (rigidBody.isSleeping) { + rigidBody.wakeUp(); +} +``` + +> **注意**:执行部分接口,例如施加力或冲量、改变速度、分组和掩码会尝试唤醒刚体。 + +#### 通过阻尼 + +刚体组件提供了 **linearDamping** 线性阻尼和 **angularDamping** 旋转阻尼属性,可以通过 `linearDamping` 和 `angularDamping` 方法对其获取或设置。 + +阻尼参数的范围建议在 **0** 到 **1** 之间,**0** 意味着没有阻尼,**1** 意味着满阻尼。 + +```ts +if (rigidBody) { + rigidBody.linearDamping = 0.5; + let linearDamping = rigidBody.linearDamping; + + rigidBody.angularDamping = 0.5; + let angularDamping = rigidBody.angularDamping; +} +``` + +#### 通过因子 + +刚体组件提供了 **linearFactor** 线性速度因子和 **angularFactor** 旋转速度因子属性,可以通过 `linearFactor` 和 `angularFactor` 方法对其获取或设置。 + +因子是 `Vec3` 的类型,相应分量的数值用于缩放相应轴向的速度变化,默认值都为 **1**,表示缩放为 **1** 倍,即无缩放。 + +```ts +if (rigidBody) { + rigidBody.linearFactor = new math.Vec3(0, 0.5, 0); + let linearFactor = rigidBody.linearFactor; + + rigidBody.angularFactor = new math.Vec3(0, 0.5, 0); + let angularFactor = rigidBody.angularFactor; +} +``` + +> **注意**: +> 1. 将因子某分量值设置为 **0**,可以固定某个轴向的移动或旋转。 +> 2. 在使用 [**cannon.js**](physics-engine.md#cannon.js) 或 [**Bullet (ammo.js)**](physics-engine.md#ammo.js) 物理引擎情况下,因子作用的物理量不同,使用 **cannon.js** 时作用于速度,使用 **Bullet (ammo.js)** 时作用于力。 diff --git a/versions/4.0/zh/physics/physics-sweep.md b/versions/4.0/zh/physics/physics-sweep.md new file mode 100644 index 0000000000..b1c7db593c --- /dev/null +++ b/versions/4.0/zh/physics/physics-sweep.md @@ -0,0 +1,97 @@ +# 几何投射检测 + +Cocos Creator 从 v3.8 开始,支持扫掠功能。 + +几何投射检测,会沿着指定的射线,发射不同的几何体,就像使用某个几何体沿着射线扫过一个区域,因此又称扫掠。扫掠会对几何体扫过的物理世界区域内的碰撞体进行检查,并返回特定的结果。 + +和 `rayCast` 射线检测发射的射线不同,`sweep` 允许物理引擎投射不同的几何体,并返回特定的碰撞信息。 + +目前引擎提供以下 **盒形**、**球形** 以及 **胶囊体** 扫掠。 + +## 方法 + +- 盒形: + - `sweepBox`:沿着给定的射线投射一个盒,并返回所有命中的碰撞体 + - `sweepBoxClosest`:沿着给定的射线投射一个盒,并返回最近命中的碰撞体 + + - 参数说明: + + - worldRay:geometry.Ray:世界空间下的一条射线 + - halfExtent:IVec3Like 盒体的一半尺寸,三维矢量的 xyz 各代表盒在每个轴上的大小的一半 + - orientation:IQuatLike: 盒体的方向 + - mask: number:掩码,默认为 0xffffffff,请参考 [分组和掩码](./physics-group-mask.md) 以及 [射线检测](./physics-raycast.md) + - maxDistance: number 最大检测距离,默认为 10000000,目前请勿传入 Infinity 或 Number.MAX_VALUE + - queryTrigger:boolean 默认为 true,是否检测触发器 + +- 球形 + - `sweepSphere`:沿着给定的射线投射一个球形,并返回所有命中的碰撞体 + - `sweepSphereClosest`:沿着给定的射线投射一个球形,并返回最近命中的碰撞体 + + - 参数说明: + - worldRay:geometry.Ray:世界空间下的一条射线 + - radius:number 球体的半径 + - mask:number, 掩码,默认为 0xffffffff,请参考 [分组和掩码](./physics-group-mask.md) 以及 [射线检测](./physics-raycast.md) + - maxDistance:number 最大检测距离,默认为 10000000,目前请勿传入 Infinity 或 Number.MAX_VALUE + - queryTrigger:boolean 默认为 true,是否检测触发器 + +- 胶囊体 + - sweepCapsule:沿着给定的射线投射一个胶囊体,并返回所有命中的碰撞体 + - sweepCapsuleClosest:沿着给定的射线投射一个胶囊体,并返回最近命中的碰撞体 + - 参数说明: + - worldRay:geometry.Ray:世界空间下的一条射线 + - radius:number:胶囊体的半径 + - height:number:胶囊体末端两个半球圆心的距离 + - orientation:IQuatLike: 胶囊体的朝向 + - mask:number, 掩码,默认为 0xffffffff,请参考 [分组和掩码](./physics-group-mask.md) 以及 [射线检测](./physics-raycast.md) + - maxDistance:number 最大检测距离,默认为 10000000,目前请勿传入 Infinity 或 Number.MAX_VALUE + - queryTrigger:boolean 默认为 true,是否检测触发器 + +详细说明请参考 [API](%__APIDOC__%/zh/class/PhysicsSystem)。 + +## 返回值 + +函数的返回值为 boolean,用于确定表示是否有检测到碰撞。为保证扫掠的性能,扫掠方法的结果都被存储在 `PhysicsSystem` 内。使用时请先通过 `sweepCastClosestResult/sweepCastResults` 获取到检测结果,下次扫掠后,之前的结果可能会被覆盖或失效,因此建议在获取到结果之后,将结果内的内容提取出来使用。 + +- `sweepBoxClosest`、`sweepSphereClosest` 以及 `sweepCapsuleClosest` 方法的结果被保存在 `PhysicsSystem.instance.sweepCastResults` 内,其类型为 `PhysicsRayResult`,代码示例如下 + + ```ts + const result = PhysicsSystem.instance.sweepCastClosestResult; + ``` + +- `sweepBox`、`sweepSphere` 和 `sweepCapsule` 方法的结果存储在 `PhysicsSystem.instance.sweepCastResults` 内,其为 `PhysicsRayResult` 类型的数组。代码示例如下 + + ```ts + const results = PhysicsSystem.instance.sweepCastResults; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + ... + } + ``` + +`PhysicsRayResult` 的描述如下: + +- hitPoint: vec3 在世界坐标系下的击中点 +- distance: number 击中点到射线原点的距离 +- collider: Collider 击中的碰撞盒 +- hitNormal: vec3 在世界坐标系下击中面的法线 + +API 参考 [PhysicsRayResult](%__APIDOC__%/zh/class/physics.PhysicsRayResult)。 + +## 示例 + +以盒型扫掠为例,使用方法如下: + +```ts +if (PhysicsSystem.instance.sweepSphereClosest(this._ray, this._sphereRadius * this._scale, this._mask, this._maxDistance, this._queryTrigger)) { + const result = PhysicsSystem.instance.sweepCastClosestResult; +} +``` + +更多示例请参考 [GIT](https://github.com/cocos/cocos-example-projects/tree/master/physics-3d)。 + +下载示例后,打开 **case-physics-sweep** 场景,运行即可查看扫掠的结果。 + +![sweep.jpg](./img/sweep.jpg) + +示例中扫掠的结果标记为 **红色**。 diff --git a/versions/4.0/zh/physics/physics.dot b/versions/4.0/zh/physics/physics.dot new file mode 100644 index 0000000000..27d632c0ac --- /dev/null +++ b/versions/4.0/zh/physics/physics.dot @@ -0,0 +1,5 @@ +digraph G { + + lateUpdate -> SyncSceneToPhysics -> PhysicsSimulation -> SyncPhysicsToScene -> emitEvents; + +} \ No newline at end of file diff --git a/versions/4.0/zh/physics/physics.md b/versions/4.0/zh/physics/physics.md new file mode 100644 index 0000000000..d887aae940 --- /dev/null +++ b/versions/4.0/zh/physics/physics.md @@ -0,0 +1,80 @@ +# 3D 物理系统 + +## 3D 物理简介 + +![physics-system](img/physics-system.jpg) + +物理系统是游戏引擎的关键组成部分,因为它将真实世界的物理性质(如质量、重力和摩擦力)注入游戏世界,使游戏世界的运作更加真实。物理引擎负责根据物体的物理性质模拟游戏世界中的运动和碰撞反馈。值得注意的是,物理系统是一个复杂的系统,不仅与游戏逻辑相关,还与渲染系统、动画系统和音频系统相关。通常,游戏物理系统仅涵盖了牛顿力学的基础,如碰撞检测和刚体运动。其他物理现象,如流体力学或软体物理学,需要使用其他方法实现。用户可以使用物理引擎提供的API控制物理系统,在游戏中实现各种物理效果。 + +Cocos Creator 支持以下几种物理引擎: + +- **Bullet**:默认物理引擎,[Bullet 物理引擎](https://pybullet.org/wordpress/) 的 asm.js/wasm 版本。具备碰撞检测和物理模拟的物理引擎。 +- **builtin**:内置物理引擎,仅用于碰撞检测的轻量引擎。 +- **cannon.js**:具有碰撞检测和物理模拟的物理引擎。 +- **PhysX**: 由 [NVIDIA](https://developer.nvidia.com/physx-sdk) 公司开发的游戏物理引擎。具备碰撞检测和物理模拟的物理引擎。 + +开发者根据开发对物理特性需求或应用场景选择不同的物理引擎,详情请参考:[设置物理引擎](physics-engine.md)。 + +> **注意**:在低版本中 PhysX 不被支持。如要使用 PhysX 请确保引擎升级到最新版本。 + +## 物理世界和元素 + +物理世界中的每个元素都可以理解为一个独立的 **刚体**,在 Cocos Creator 3.x 中可以通过为游戏对象添加 [Collider 碰撞组件](physics-collider.md) 或者 [RigidBody 刚体组件](physics-rigidbody.md) 让物理元素具备物理特性。物理系统将为这些元素进行物理计算,比如计算各物体是否产生碰撞,以及物体的受力情况。当计算完成后,物理系统会将物理世界更新到场景世界中,仿真还原真实世界中的物理行为。 + +场景世界与物理世界: + +![场景世界与物理世界](img/physics-world.jpg) + +> **注意**:此处的 “刚体” 指的是物理世界中,在运动中或受力作用后,形状和大小不变,而且内部各点的相对位置不变的物体。 + +### 物理世界的流程 + +当所有组件都完成 `lateUpdate` 之后,引擎会将持有物理属性的节点(刚体组件、碰撞体组件)同步到物理世界,并驱动物理引擎进行模拟,模拟完成以后再将物理引擎计算出的结果同步到场景的各个节点上。整体流程如下图所示: + +![phy](img/physics-pipeline.png) + +## 添加物理元素 + +![add-element](img/physics-element.png) + +在游戏世界中添加一个物理元素可以分为以下步骤: + +1. 新建一个节点。此处新建一个立方体模型 **Cube**; +2. 添加碰撞组件,此处添加一个 [盒碰撞体组件](physics-collider.md#%E7%9B%92%E7%A2%B0%E6%92%9E%E5%99%A8%E7%BB%84%E4%BB%B6-boxcollider)。在 **属性检查器** 面板下方点击 **添加组件** 按钮,在 **Physics** 目录下选择 **BoxCollider** 并调整参数; +3. 为了使它具有物理行为,接着添加一个[刚体 RigidBody](physics-rigidbody.md) 组件。 + +这样,便有了一个 **既有碰撞器又有物理行为** 的一个物理元素。 + +## 射线以及几何体检测 + +游戏中可能会需要对某些物理特性进行检测,如子弹是否击中目标,可否将某些家具摆放在特定位置是,检测方法一般是发射一条,多条射线或者某种几何体(通常为模型的包围盒、胶囊体等),可以参考下列的文档来进行投射检测。 + +- [射线检测](./physics-raycast.md) +- [几何投射检测](./physics-sweep.md) + +### 绘制物理调试信息 + +![debug-draw](img/physics-debugdraw.png) + +物理系统默认是不绘制任何调试信息的,如果需要绘制调试信息,请设置 `PhysicsSystem.instance.debugDrawFlags`。 + +物理系统提供了各种各样的调试信息,可以通过组合这些信息来绘制相关的内容。 + +- EPhysicsDrawFlags.NONE = 0:不绘制任何内容 +- EPhysicsDrawFlags.WIRE_FRAME = 1:绘制碰撞体线框 +- EPhysicsDrawFlags.CONSTRAINT = 2:绘制约束 +- EPhysicsDrawFlags.ABB = 4:绘制包围盒。 + +想要绘制所有信息,可参考如下代码: + +```ts +PhysicsSystem.instance.debugDrawFlags = EPhysicsDrawFlags.WIRE_FRAME + | EPhysicsDrawFlags.AABB + | EPhysicsDrawFlags.CONSTRAINT; +``` + +设置绘制标志位为 **EPhysicsDrawFlags.NONE**,即可以关闭绘制。 + +```ts +PhysicsSystem.instance.debugDrawFlags = EPhysicsDrawFlags.NONE; +``` diff --git a/versions/4.0/zh/publish/publish-wechatgame.md b/versions/4.0/zh/publish/publish-wechatgame.md new file mode 100644 index 0000000000..276dd8c853 --- /dev/null +++ b/versions/4.0/zh/publish/publish-wechatgame.md @@ -0,0 +1,7 @@ +# 微信小游戏发布 + +由于产品迭代,此文档已过期。请移步如下地址: + +[发布到微信小游戏](../editor/publish/publish-wechatgame.md)。 + + \ No newline at end of file diff --git a/versions/4.0/zh/release-notes/animation.png b/versions/4.0/zh/release-notes/animation.png new file mode 100644 index 0000000000..0f20acb89d Binary files /dev/null and b/versions/4.0/zh/release-notes/animation.png differ diff --git a/versions/4.0/zh/release-notes/assets-preview.png b/versions/4.0/zh/release-notes/assets-preview.png new file mode 100644 index 0000000000..eb1a6cbaa1 Binary files /dev/null and b/versions/4.0/zh/release-notes/assets-preview.png differ diff --git a/versions/4.0/zh/release-notes/build-panel.png b/versions/4.0/zh/release-notes/build-panel.png new file mode 100644 index 0000000000..9e9e6732f0 Binary files /dev/null and b/versions/4.0/zh/release-notes/build-panel.png differ diff --git a/versions/4.0/zh/release-notes/build-template-settings-upgrade-guide-v3.6.md b/versions/4.0/zh/release-notes/build-template-settings-upgrade-guide-v3.6.md new file mode 100644 index 0000000000..d687efc725 --- /dev/null +++ b/versions/4.0/zh/release-notes/build-template-settings-upgrade-guide-v3.6.md @@ -0,0 +1,437 @@ +# Cocos Creator 3.6.0 构建模板与 settings.json 升级指南 + +> 本文将介绍 Cocos Creator 3.6.0 构建模板与 settings.json 升级注意事项。 + +在 3.6 版本之前,引擎的代码中都包含了一部分无法被剔除的内置 effect 的数据,这部分数据极大的占用了引擎的包体与拖慢了解析引擎代码的时间。对于大部分项目来说是一种资源浪费。 + +在之前版本中,构建模板中的 `application.ejs` 与 `game.ejs`(某些平台是 `index.ejs`)中包含了不少引擎启动流程的逻辑代码,这部分逻辑代码较为复杂,定制过程中容易出现问题,需要更加易用与稳定的引擎启动流程的定制方案。 + +此外,在之前版本中,游戏的主要配置数据存储在 `settings.json` 中, 在游戏运行过程中无法访问这部分数据,对于一些插件来说是极不方便的。 + +针对以上几个问题,我们在 Cocos Creator 3.6 中,为了优化引擎包体,更好地维护引擎的业务代码,同时能够提供更多的定制空间,我们重构了引擎的启动流程并提供了一个新的 `settings` 配置模块 。在重构的过程中,虽然我们希望能尽可能地保证之前版本项目构建模板与 settings.json 的兼容性,但是某些方面还是引入了一些无法兼容的情况。在这些情况下,你需要手动升级项目构建模板与配置文件的定制内容。 + +- 对 **美术策划** 而言,项目中的所有资源,例如场景、动画和 Prefab 都不需要修改,也不需要升级。 +- 对 **程序** 与 **插件开发者** 而言,影响主要体现在原先项目中定制的构建模板和 `settings.json` 需要调整为新的方式。 + +以下将详细介绍这部分内容。 + +## 需要手动升级的情况 + +- 你在项目中有定制过构建模板中的 `application.ejs`。 +- 你在项目中有定制过构建模板中的 `game.ejs` 或 `index.ejs`。 +- 你在项目中有定制过生成后的 `settings.json`。 + +## 升级步骤 + +### 将定制过的 application.ejs 迁移到新的模板上 + +application.ejs 在 v3.6 版本中改动较大,主要有以下两个方面的改动: + +#### 提供了一个更加简单的 Application 类型的定义 + +```js +class Application { + init (cc: any): Promise | void; + start(): Promise; +} +``` + +我们提供了形如以上的 Application 的定义,我们在其中提供了两个生命周期回调: +- `init` 生命周期函数会在引擎代码加载后被调用,引擎模块将作为参数传入,你可以在 `init` 函数中监听引擎启动流程的事件,并在对应事件触发时,执行一些定制逻辑。 +- `start` 生命周期函数会在 `init` 函数后被调用,你需要在此函数中以你需要的方式启动并运行引擎。 + +下面是我们实现的一份最简单的 `application.ejs` 的模板与其对应的解释: + +```js +let cc; +export class Application { + constructor () { + this.settingsPath = '<%= settingsJsonPath %>'; // settings.json 文件路径,通常由编辑器构建时传入,你也可以指定自己的路径 + this.showFPS = <%= showFPS %>; // 是否打开 profiler, 通常由编辑器构建时传入,你也可以指定你需要的值 + } + + init (engine) { + cc = engine; + cc.game.onPostBaseInitDelegate.add(this.onPostInitBase.bind(this)); // 监听引擎启动流程事件 onPostBaseInitDelegate + cc.game.onPostSubsystemInitDelegate.add(this.onPostSystemInit.bind(this)); // 监听引擎启动流程事件 onPostSubsystemInitDelegate + } + + onPostInitBase () { + // cc.settings.overrideSettings('assets', 'server', ''); + // 实现一些自定义的逻辑 + } + + onPostSystemInit () { + // 实现一些自定义的逻辑 + } + + start () { + return cc.game.init({ // 以需要的参数运行引擎 + debugMode: <%= debugMode %> ? cc.DebugMode.INFO : cc.DebugMode.ERROR, + settingsPath: this.settingsPath, // 传入 settings.json 路径 + overrideSettings: { // 对配置文件中的部分数据进行覆盖,第二部分会详细介绍这个字段 + // assets: { + // preloadBundles: [{ bundle: 'main', version: 'xxx' }], + // } + profiling: { + showFPS: this.showFPS, + } + } + }).then(() => cc.game.run()); + } +} +``` + +- 此模板中在 `init` 函数中监听了引擎启动流程中的事件,可以在对应事件触发时执行一些自定义逻辑。 +`game.init` 在执行过程中会触发各个阶段的事件,可参考 `game.init` 的 API 说明,其中不同的阶段的事件可以使用不同的引擎能力,触发的事件顺序如下所示: + + ``` + -PreBaseInitEvent,不可以使用任何引擎能力 + -基础模块初始化(logging, sys, settings) + -PostBaseInitEvent,可使用基础模块中的能力 + -PreInfrastructureInitEvent,可使用基础模块中的能力 + -基础设施模块的初始化(assetManager, builtinResMgr, gfxDevice, screen, Layer, macro) + -PostInfrastructureInitEvent,可使用基础模块,基础设施模块中的能力 + -PreSubsystemInitEvent,可使用基础模块,基础设施模块中的能力 + -子系统模块初始化(animation, physics, tween, ui, middleware 等) + -PostSubsystemInitEvent,可使用基础模块,基础设施模块,子系统模块中的能力 + -EngineInitedEvent,可使用基础模块,基础设施模块,子系统模块中的能力 + -PreProjectDataInitEvent,可使用基础模块,基础设施模块,子系统模块中的能力 + -项目数据初始化(GamePlayScripts, resources, etc) + -PostProjectDataInitEvent,可使用基础模块,基础设施模块,子系统模块,项目数据中的能力 + -GameInitedEvent,可使用引擎所有能力 + ``` + +- `start` 函数中调用了引擎的初始化与运行。你可以通过在调用 `game.init` 时传入自定义的参数来控制引擎的启动。需要注意的是其中的 `overrideSettings` 字段,此字段可用于覆盖配置文件中的一些配置数据,从而影响引擎的启动。第二部分将详细说到此字段。 + +新的 `application.ejs` 比之前版本更加简洁和易定制,你可以参考此模板实现你自己的 `application.ejs` 并在其中执行你的自定义逻辑。 + +#### 引擎启动流程相关的逻辑代码迁移到了引擎中 + +我们将之前在 application.ejs 中与引擎启动相关的逻辑迁移到了引擎中,包括 settings.json 的加载(`loadSettingsJson`),js 插件的加载(`loadJsList`),项目 bundle 的加载(`loadAssetBundle`)等等。所以现在你 **无法在 application.ejs 直接修改引擎启动流程**,但你可以通过 `game.init` 中传入的 `overrideSettings` 字段来覆盖配置文件,从而影响启动流程,另外你也可以监听引擎启动过程中的事件,并执行一些自定义逻辑。下面将详细介绍引擎的启动流程定制如何迁移到新的机制上。如果你的项目有对引擎启动流程进行定制,请参考以下升级方法进行迁移: + +- 如果你在老的模板中引擎启动的特定阶段插入了自定义的代码逻辑,例如做了如下定制: + + ```js + function start ({ + findCanvas, + }) { + let settings; + let cc; + return Promise.resolve() + .then(() => topLevelImport('cc')) + .then(() => customLogic1()) // 自定义逻辑 1 + .then((engine) => { + cc = engine; + return loadSettingsJson(cc); + }) + .then(() => customLogic2()) // 自定义逻辑 2 + .then(() => { + settings = window._CCSettings; + return initializeGame(cc, settings, findCanvas) + .then(() => { + if (settings.scriptPackages) { + return loadModulePacks(settings.scriptPackages); + } + }) + .then(() => loadJsList(settings.jsList)) + .then(() => loadAssetBundle(settings.hasResourcesBundle, settings.hasStartSceneBundle)) + .then(() => { + return cc.game.run(() => onGameStarted(cc, settings)); + }); + }).then(() => customLogic3()); // 自定义逻辑 3 + } + ``` + + 你可以在新的 application.ejs 模板中通过上述所说的监听引擎启动流程中的事件来实现自定义逻辑,例如: + + ```js + init (engine) { + cc = engine; + cc.game.onPreBaseInitDelegate.add(this.onPreBaseInit.bind(this)); // 监听引擎启动流程事件 onPreBaseInitDelegate + cc.game.onPostBaseInitDelegate.add(this.onPostBaseInit.bind(this)); // 监听引擎启动流程事件 onPostBaseInitDelegate + cc.game.onPostProjectInitDelegate.add(this.onPostProjectInit.bind(this)); // 监听引擎启动流程事件 onPostProjectInitDelegate + } + + onPreBaseInit () { + customLogic1(); // 自定义逻辑 1 + } + + onPostBaseInit () { + customLogic2() // 自定义逻辑 2 + } + + onPostProjectInit () { + customLogic3() // 自定义逻辑 3 + } + ``` + +- 如果你在老的模板中定制了物理模块 wasm 的加载,即老模板中的下面这个方法。 + + ```js + <% if (hasPhysicsAmmo) { %> + promise = promise + .then(() => topLevelImport('wait-for-ammo-instantiation')) + .then(({default: waitForAmmoInstantiation}) => { + return waitForAmmoInstantiation(fetchWasm('')); + }); + <% } %> + ``` + + 此方法已改为引擎内部使用,如果需要定制,请自定义引擎,并定制引擎目录下的 cocos/physics/bullet/instantiated.ts 内容。 + +- 如果你在老的模板中定制了 settings.json 的加载, 即老模板中的 `loadSettingsJson` 函数。 + 此方法已改为引擎内部使用,如果你对这部分存在定制,请分为以下三种情况考虑: + - 如果你需要定制 `settings.json` 的路径,你可以在调用 `game.init` 方式时传入自定义的 `settingsPath` 路径,例如: + + ```js + game.init({ settingsPath: this.mySettingsPath }); + ``` + + - 如果你需要读取 settings.json 中的内容,你可以监听 `game.onPostBaseInitDelegate` 之后的事件,这些事件中你都可以安全的访问引擎中的 `settings` 模块,并通过 `settings` 模块的相关 API 获取对应配置数据,例如: + + ```js + init (engine) { + cc = engine; + cc.game.onPostBaseInitDelegate.add(this.onPostBaseInit.bind(this)); + } + + onPostBaseInit () { + const property = cc.settings.querySettings('MyCustomData', 'MyCustomProperty'); + } + ``` + + - 如果你需要设置配置文件中的内容,你可以通过在调用 `game.init` 方法时传入 `overrideSettings` 字段来覆盖一些配置文件中的数据,也可以在 game 的事件回调中去调用 `settings.overrideSettings` 覆盖配置文件中的数据,从而影响引擎启动流程,例如: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'profiling': { 'showFPS': true } + } + }); + } + ``` + + 或 + + ```js + onPostBaseInit () { + cc.settings.overrideSettings('profiling', 'showFPS', true); + } + ``` + +- 如果你在老的模板中定制了加载 js 插件, 即老模板中的 `loadJsList` 函数。 + 此方法已改为引擎内部使用,如果你需要修改要加载的 js 文件列表,你可以通过在调用 `game.init` 时传入 `overrideSettings` 的 `plugin` 字段来修改,例如: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'plugins': { 'jsList': ['MyCustom.js'] } + } + }); + } + ``` + +- 如果你在老的模板中定制了加载项目 Bundle, 即老模板中的 `loadAssetBundle` 函数。 + 此方法已改为引擎内部使用,如果你需要修改要加载的 bundle 列表,你可以通过在调用 `game.init` 时传入 `overrideSettings` 的 `assets` 字段来修改,例如: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'assets': { 'preloadBundles': [ 'main', 'resources', 'myBundle' ]} + } + }); + } + ``` + +- 如果你在老的模板中定制了 macros 与 layer 的初始化, 即老模板中的`initializeGame` 函数。 + 此方法已改为引擎内部使用,如果你需要修改 macros 与 layer 的列表,你可以通过在调用 `game.init` 时传入 `overrideSettings` 的 `engine` 字段来修改,例如: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'engine': { + 'macros': {}, + 'customLayers': [], + } + } + }); + } + ``` + +- 如果你在老的模板中定制了初始场景的加载, 即老模板中的 `onGameStarted` 函数。 + 此方法已改为引擎内部使用,如果你需要修改初始场景,你可以通过在调用 `game.init` 时传入 `overrideSettings` 的 `launch` 字段来修改,例如: + + ```js + start () { + cc.game.init({ + overrideSettings: { + 'launch': { 'launchScene': 'MyFirstScene' } + } + }); + } + ``` + +- 如果你在老的模板中定制了传入 `game.init` 的参数, 即老模板中的 `getGameOptions` 函数。 + 请参考最新的 `IGameConfig` 接口定义,并在 `game.init` 时传入该参数。 + +### 将定制过的 game.ejs 或 index.ejs 迁移到新模板上 + +game.ejs 与 index.ejs 相较于之前版本没有太大变化,主要变化体现在以下几个点: +- 我们将 `loadJsListFile`, `fetchWasm`, `findCanvas` 等引擎使用的接口迁移到了引擎内部,如果你对这部分内容存在定制,请自定义引擎,并修改引擎目录下的 pal/env 的内容。 +- 正如第一部分所说,我们整理了 Application 类型的接口定义,而 game.ejs 与 index.ejs 作为 Application 类型的调用者,所以在 Application 接口调用的地方也发生了变化,比如在某些平台我们改成了如下形式: + + ```js + System.import('<%= applicationJs %>') + .then(({ Application }) => { + return new Application(); + }).then((application) => { + return System.import('cc').then((cc) => { + return application.init(cc); + }).then(() => { + return application.start(); + }); + }).catch((err) => { + console.error(err.toString() + ', stack: ' + err.stack); + }); + ``` + +- 我们将不同平台上用于适配的代码进行了打包,合并成了单个 js 文件以减少包体,所以在运行代码的地方改为只运行一个 js 文件,例如: + + ```js + require('./libs/common/engine/index.js'); + require('./libs/wrapper/engine/index'); + require('./libs/common/cache-manager.js'); + ``` + + 改为了 + + ```js + require('./engine-adapter'); + ``` + +如果你对 game.ejs 与 index.ejs 存在定制的话,只需要将定制迁移到最新的模板上即可,需要 **注意** 的是如果你的自定义逻辑依赖于 **字符串的匹配** 进行插入,例如: + +```js +const gameTemplateString = readFileSync('game.ejs', 'utf-8'); +gameTemplateString = gameTemplateString.replace('require('./libs/common/cache-manager.js');', 'require('./libs/common/cache-manager.js');\nCustomLogic();\n') +``` + +则有可能出现匹配字符串失败的地方,你需要在新的模板下进行插入,或者你可以参考第一部分在 application.ejs 中进行定制,新的 application.ejs 模板更加利于定制。 + +### 迁移对 settings.json 的定制 + +我们在 3.6 中增加了 `settings` 模块用于在游戏中对配置数据进行访问,你可以在 settings.json 中增加自定义数据,然后在运行时使用 `settings` 模块进行访问,但为了避免模块之间的配置数据互相影响,我们将之前只有一层的配置数据结构,改为了 `Category` 与 `Property` 的两层结构,下面是具体的变化: + +之前版本: + +```js +interface Settings { + CocosEngine: string; + debug: boolean; // 是否是 debug 模式 + designResolution: ISettingsDesignResolution; // 设计分辨率 + jsList: string[]; // js 插件列表 + launchScene: string; // 首场景 + preloadAssets: string[], // 预加载资源 + platform: string; // 平台名称 + renderPipeline: string; // 渲染管线 + physics?: IPhysicsConfig; // 物理相关配置 + exactFitScreen: boolean; // 在 web 下是否让游戏外框对齐到屏幕上 + + bundleVers: Record; // bundle 版本信息 + subpackages: string[]; // bundle 在小游戏上的子包配置 + remoteBundles: string[]; // 远程 bundle 的列表 + server: string; // 远程服务器地址 + hasResourcesBundle: boolean; // 是否存在 resources 包 + hasStartSceneBundle: boolean; // 是否存在首场景分包 + + scriptPackages?: string[]; // 引擎内部使用字段 + splashScreen?: ISplashSetting; // SplashScreen 相关配置 + + customJointTextureLayouts?: ICustomJointTextureLayout[]; // JointTextureLayout 相关的配置 + + macros?: Record; // cc.macros 相关配置 + engineModules: string[]; // 引擎模块,预览时使用 + customLayers: {name: string, bit: number}[]; // 自定义 layer 的相关配置 + orientation?: IOrientation; // 屏幕旋转方向 +} +``` + +新版本的 Settings.json 文件格式: + +```ts +interface Settings { + CocosEngine: string; + engine: { + debug: boolean; + macros: Record; + customLayers: {name: string, bit: number}[]; + platform: string; + engineModules?: string[]; + builtinAssets: string[]; + }; + physics?: IPhysicsConfig; + rendering: { + renderPipeline: string; + renderMode?: number; + }; + assets: { + server: string; + remoteBundles: string[]; + bundleVers: Record; + preloadBundles: { bundle: string, version?: string }[]; + importBase?: string; + nativeBase?: string; + subpackages: string[]; + preloadAssets: string[]; + jsbDownloaderMaxTasks?: number; + jsbDownloaderTimeout?: number; + }; + plugins: { + jsList: string[]; + }; + scripting: { + scriptPackages?: string[]; + }; + launch: { + launchScene: string; + }; + screen: { + frameRate?: number; + exactFitScreen: boolean; + orientation?: IOrientation; + designResolution: ISettingsDesignResolution; + }; + splashScreen?: ISplashSetting; + animation: { + customJointTextureLayouts?: ICustomJointTextureLayout[]; + }; + profiling?: { + showFPS: boolean; + }; +} +``` + +字段与之前版本几乎保持了一一对应的关系,只是迁移到了某个具体的 `Category` 下。有几个字段略有变化: + +- 新增 screen 下的 frameRate 字段用于设置目标更新频率 +- 新增 profiling 下的 showFPS 字段用于控制是否开启左下角的统计数据显示 +- 移除了原先的 hasResourcesBundle 与 hasStartSceneBundle 字段,改为统一由 assets 下的 preloadBundles 字段控制 +- 新增 assets 下的 jsbDownloaderMaxTasks, jsbDownloaderTimeout 字段用于控制原生环境下资源的下载并发数与超时时间。 + +如果你对 settings.json 中的原有数据字段有所定制的话,你需要参考新的格式,将定制内容迁移到新的格式上。如果你有自定义的配置字段,我们建议你将自定义的配置数据放到一个自定的 `Category` 下,避免与其他模块的数据发生冲突,例如: + +```ts +interface MyCustomSettings extends Settings { + 'customSettings': { + 'someProperty': string; + }; +} +``` + +------------------------- +以上就是 3.6 版本构建模板与 settings.json 的升级说明,如果在升级过程中遇到问题,欢迎到 [Cocos 的官方社区](https://forum.cocos.org/),[GitHub](https://github.com/cocos/cocos4/discussions) 中向我们反馈,感谢! diff --git a/versions/4.0/zh/release-notes/build.png b/versions/4.0/zh/release-notes/build.png new file mode 100644 index 0000000000..b6c85acd86 Binary files /dev/null and b/versions/4.0/zh/release-notes/build.png differ diff --git a/versions/4.0/zh/release-notes/editor-cmake-path-mac.png b/versions/4.0/zh/release-notes/editor-cmake-path-mac.png new file mode 100644 index 0000000000..7702e6d3e1 Binary files /dev/null and b/versions/4.0/zh/release-notes/editor-cmake-path-mac.png differ diff --git a/versions/4.0/zh/release-notes/editor-cmake-path.png b/versions/4.0/zh/release-notes/editor-cmake-path.png new file mode 100644 index 0000000000..ffcdd3798c Binary files /dev/null and b/versions/4.0/zh/release-notes/editor-cmake-path.png differ diff --git a/versions/4.0/zh/release-notes/engine-common.png b/versions/4.0/zh/release-notes/engine-common.png new file mode 100644 index 0000000000..9b884eb61c Binary files /dev/null and b/versions/4.0/zh/release-notes/engine-common.png differ diff --git a/versions/4.0/zh/release-notes/extension-plugin.png b/versions/4.0/zh/release-notes/extension-plugin.png new file mode 100644 index 0000000000..9e71f7e082 Binary files /dev/null and b/versions/4.0/zh/release-notes/extension-plugin.png differ diff --git a/versions/4.0/zh/release-notes/import-menu.png b/versions/4.0/zh/release-notes/import-menu.png new file mode 100644 index 0000000000..80ac57629c Binary files /dev/null and b/versions/4.0/zh/release-notes/import-menu.png differ diff --git a/versions/4.0/zh/release-notes/import-panel.png b/versions/4.0/zh/release-notes/import-panel.png new file mode 100644 index 0000000000..b6d699ffbe Binary files /dev/null and b/versions/4.0/zh/release-notes/import-panel.png differ diff --git a/versions/4.0/zh/release-notes/import-select-project.png b/versions/4.0/zh/release-notes/import-select-project.png new file mode 100644 index 0000000000..9932a5fee6 Binary files /dev/null and b/versions/4.0/zh/release-notes/import-select-project.png differ diff --git a/versions/4.0/zh/release-notes/index.md b/versions/4.0/zh/release-notes/index.md new file mode 100644 index 0000000000..f26750c1af --- /dev/null +++ b/versions/4.0/zh/release-notes/index.md @@ -0,0 +1,16 @@ +# 升级指南 + +- [Cocos Creator 3.0 升级指南](upgrade-guide-v3.0.md) +- [v3.0 材质升级指南](../material-system/effect-2.x-to-3.0.md) +- [v3.1 材质升级指南](../material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md) +- [v3.3 动画剪辑数据升级指南](../animation/animation-clip-migration-3.3.x.md) +- [v3.5 材质升级指南](../material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md) +- [资源分包升级指南](../asset/subpackage-upgrade-guide.md) +- [资源管理模块升级指南](../asset/asset-manager-upgrade-guide.md) +- [v3.5 已构建工程升级指南](../engine/template/native-upgrade-to-v3.5.md) +- [v3.5 已构建工程升级 v3.6 指南](../engine/template/native-upgrade-to-v3.6.md) +- [v3.6 构建模板与 settings.json 升级指南](../release-notes/build-template-settings-upgrade-guide-v3.6.md) +- [Cocos Creator 3.6 材质升级指南](../material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md) +- [粒子系统着色器 v3.5.x 升级到 v3.6.0](../particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md) +- [v3.6 之前版本升级 CMake](./upgrade-cmake.md) +- [v3.8 Android 工程升级](./upgrade-3.8-android.md) diff --git a/versions/4.0/zh/release-notes/native-engine.png b/versions/4.0/zh/release-notes/native-engine.png new file mode 100644 index 0000000000..8493e50117 Binary files /dev/null and b/versions/4.0/zh/release-notes/native-engine.png differ diff --git a/versions/4.0/zh/release-notes/project-setting.png b/versions/4.0/zh/release-notes/project-setting.png new file mode 100644 index 0000000000..0b9527c46a Binary files /dev/null and b/versions/4.0/zh/release-notes/project-setting.png differ diff --git a/versions/4.0/zh/release-notes/texture-compress-setting.png b/versions/4.0/zh/release-notes/texture-compress-setting.png new file mode 100644 index 0000000000..83f41d08b5 Binary files /dev/null and b/versions/4.0/zh/release-notes/texture-compress-setting.png differ diff --git a/versions/4.0/zh/release-notes/unzip-cmake-mac.png b/versions/4.0/zh/release-notes/unzip-cmake-mac.png new file mode 100644 index 0000000000..98b32722e8 Binary files /dev/null and b/versions/4.0/zh/release-notes/unzip-cmake-mac.png differ diff --git a/versions/4.0/zh/release-notes/unzip-cmake-win.png b/versions/4.0/zh/release-notes/unzip-cmake-win.png new file mode 100644 index 0000000000..3e5e94a8bb Binary files /dev/null and b/versions/4.0/zh/release-notes/unzip-cmake-win.png differ diff --git a/versions/4.0/zh/release-notes/update-setting.png b/versions/4.0/zh/release-notes/update-setting.png new file mode 100644 index 0000000000..a98f12ea56 Binary files /dev/null and b/versions/4.0/zh/release-notes/update-setting.png differ diff --git a/versions/4.0/zh/release-notes/upgrade-3.8-android.md b/versions/4.0/zh/release-notes/upgrade-3.8-android.md new file mode 100644 index 0000000000..3c3f8d98a0 --- /dev/null +++ b/versions/4.0/zh/release-notes/upgrade-3.8-android.md @@ -0,0 +1,108 @@ +# v3.8 Android 工程升级 + +从 v3.8 开始,构建产出的 Android 工程默认支持新版本的 Android Studio(Flamingo | 2022.2.1)。由于 Android Gradle 插件的 [要求](https://developer.android.com/studio/releases?hl=zh-cn#jdk-17),开发者需要将 JDK 升级到 17,同时升级 Android Studio 到 Flamingo 版本。 + +> 如果没有对构建生成的 Android 工程进行配置,可以直接删除 native/engine/android 目录和 build/android 目录,然后重新构建。这样不需要对工程进行一步步的修改升级。但是请注意,这一步操作是有风险的,如对接 SDK 的代码可能会被删除,请谨慎操作。 + +对于现有的原生 Android 工程,开发者可以参考以下步骤对工程进行升级: + +## 第一步:备份当前工程 + +在升级之前,我们应该先备份当前的 native 目录,以防万一。比如可以用 Git 保存当前修改. + +## 第二步:升级Gradle插件版本 + +Gradle 插件版本是 Gradle 与 Android 构建系统之间的接口。因此,在升级 Gradle 之前,我们需要先升级 Gradle 插件版本。在项目的 native/engine/android/build.gradle 文件中,将 classpath 中的 Gradle 插件版本改为 8.0.2。 + +```diff + // jcenter() // keeped as anchor, will be removed soon + } + dependencies { +- classpath 'com.android.tools.build:gradle:4.1.0' ++ classpath 'com.android.tools.build:gradle:8.0.2' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files +``` + +## 第三步:移除 package 字段 + +移除 native/engine/android/app/AndroidManifest.xml 文件中的 package 属性。 + +```diff + + + + +``` + +在 `native/engine/android/app/build.gradle` 中的修改 applicationId 为 namespace +```diff + compileSdkVersion PROP_COMPILE_SDK_VERSION.toInteger() + buildToolsVersion PROP_BUILD_TOOLS_VERSION + ndkPath PROP_NDK_PATH ++ namespace APPLICATION_ID + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 +@@ -17,7 +18,6 @@ android { + } + + defaultConfig { +- applicationId APPLICATION_ID + minSdkVersion PROP_MIN_SDK_VERSION + targetSdkVersion PROP_TARGET_SDK_VERSION + versionCode 1 +``` + +### 第四步:升级 Gradle 版本 + +接下来,我们需要升级 Gradle wrapper 版本。在项目的 `build/android/proj/gradle/wrapper/gradle-wrapper.properties` 文件中,将 distributionUrl 改为 8.0.2,如下所示: + +```properties +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip + +``` + +## 第五步: 更新 Proguard Rules + +添加下面的内容到文件 `native/engine/android/app/proguard-rules.pro` + +``` +# This is generated automatically by the Android Gradle plugin. +-dontwarn android.hardware.BatteryState +-dontwarn android.hardware.lights.Light +-dontwarn android.hardware.lights.LightState$Builder +-dontwarn android.hardware.lights.LightState +-dontwarn android.hardware.lights.LightsManager$LightsSession +-dontwarn android.hardware.lights.LightsManager +-dontwarn android.hardware.lights.LightsRequest$Builder +-dontwarn android.hardware.lights.LightsRequest +-dontwarn android.net.ssl.SSLSockets +-dontwarn android.os.VibratorManager +``` + +## 第六步:安装 JDK 17 + +在 [官网](https://www.oracle.com/java/technologies/downloads/) 上下载并安装 JDK 17。安装过程中需要注意配置环境变量。 + +安装完成后,可以通过在命令行输入 `java -version` 来检查是否安装成功。 + +## 第七步:升级 Android Studio + +在升级 Gradle 之后,我们还需要升级 Android Studio。如果您当前使用的是较老版本的 Android Studio,请先下载最新版本的 Android Studio(Flamingo | 2022.2.1)。下载地址为: [https://developer.android.com/studio](https://developer.android.com/studio)。 + + +下载完成后,打开 Android Studio 并导入您的项目。Android Studio 会自动检测您的项目所需的 Gradle 版本,并提示您进行升级。按照提示进行操作,即可完成 Android Studio 的升级。 + +如果在 Android Studio 编译时遇到下面的错误报告: + +![error](./upgrade-3.8-jdk-bad-version.png) + +您可以参考以下截图,在设置中修改 Gradle 使用的 JDK 17: + +![fix](./upgrade-3.8-jdk-version-as.png) + +完成上述步骤后,您也可以在构建面板中重新执行构建。 diff --git a/versions/4.0/zh/release-notes/upgrade-3.8-jdk-bad-version.png b/versions/4.0/zh/release-notes/upgrade-3.8-jdk-bad-version.png new file mode 100644 index 0000000000..535bd5d6bd Binary files /dev/null and b/versions/4.0/zh/release-notes/upgrade-3.8-jdk-bad-version.png differ diff --git a/versions/4.0/zh/release-notes/upgrade-3.8-jdk-version-as.png b/versions/4.0/zh/release-notes/upgrade-3.8-jdk-version-as.png new file mode 100644 index 0000000000..20ef2df6f3 Binary files /dev/null and b/versions/4.0/zh/release-notes/upgrade-3.8-jdk-version-as.png differ diff --git a/versions/4.0/zh/release-notes/upgrade-cmake.md b/versions/4.0/zh/release-notes/upgrade-cmake.md new file mode 100644 index 0000000000..01e3a88a66 --- /dev/null +++ b/versions/4.0/zh/release-notes/upgrade-cmake.md @@ -0,0 +1,29 @@ +# 升级 CMake + +在 v3.6 之前的版本不支持指定 cmake 路径,可以使用覆盖编辑器内建的 cmake 目录的方式实现升级。 + +### Windows 平台 + +1. 到 https://cmake.org/download/ 下载需要的 cmake 版本,并解压 + +![unzip cmake for windows](./unzip-cmake-win.png) + +2. 打开编辑内部目录 `resources/tools/cmake/` + +![editor cmake path](./editor-cmake-path.png) + +3. 用下载的 cmake 内容覆盖编辑器内建的 cmake,目录结构保持不变。 +4. 重启编辑器后生效。 + +### Mac 平台 + +1. 到 https://cmake.org/download/ 下载需要的 cmake 版本,并解压 + +![unzip cmake for mac](./unzip-cmake-mac.png) + +2. 打开编辑内部目录 `Contents/Resources/tools/cmake/arm64`(或 `x86_64`) + +![editor cmake path](./editor-cmake-path-mac.png) + +3. 拷贝所下载的 `CMake/Contents` 中的内容到编辑器对应目录。 +4. 重启编辑器后生效。 diff --git a/versions/4.0/zh/release-notes/upgrade-guide-v3.0.md b/versions/4.0/zh/release-notes/upgrade-guide-v3.0.md new file mode 100644 index 0000000000..0ba63dfe97 --- /dev/null +++ b/versions/4.0/zh/release-notes/upgrade-guide-v3.0.md @@ -0,0 +1,574 @@ +# Cocos Creator 3.0 升级指南 + +## 版本介绍 + +Cocos Creator 3.0 集成了原有 2D 和 3D 两套产品的所有功能,带来了诸多重大更新,将做为 Creator 之后的主力版本。同时 v3.0 还延续了 Cocos 在 2D 品类上轻量高效的优势,并且为 3D 重度游戏提供高效的开发体验。 + +- **对于 Cocos Creator 2.x** + + 为了保障现有的 v2.4 项目平稳过渡,我们会将 v2.4 做为 LTS(长期支持)版本,提供后续 **两年** 的持续更新!在 **2021** 年,v2.4 将继续更新版本,提供缺陷修复和新的 Cocos Creator 小游戏平台支持,保障大家的项目成功上线。在 **2022** 年我们还将为开发者持续提供 v2.4 的关键问题修复,保障已上线的游戏平稳运营!因此: + + - **现有的 v2.x 项目可以安心继续开发,无需强制升级到 v3.0**。 + + - **新项目建议使用 v3.0 版本开发**,我们会不断优化 v3.0 的开发体验和运行效率,支撑好 2D、3D 等不同品类的重度游戏顺利上线。 + +- **对于 Cocos Creator 3D** + + 原有的 Cocos Creator 3D 做为 Creator 的分支版本,已经面向中国进行了长达一年的迭代,成功上线了 **星空大决战**、**最强魔斗士** 等重度项目!Cocos Creator 3.0 发布后,Cocos Creator 3D 也将包含在 v3.0 中,现有的 v1.2 项目都可直接升级,因此 Cocos Creator 3D 后续不会再发布独立版本。 + +Cocos Creator 3.0 使用了面向未来的全新引擎架构,将为引擎带来高性能、面向数据以及负载均衡的渲染器,并且无缝支持 Vulkan & Metal 多后端渲染,未来还会支持移动端 VR/AR 及部分主机平台。 + +关于 Cocos Creator 3.0 的详细介绍,请移步 [官网更新说明](https://cocos.com/creator)。 + +## 如何迁移 Cocos Creator 2.x 项目 + +虽然 **我们不建议开发中的项目,特别是即将上线的项目强升 v3.0**,但是我们仍在 Cocos Creator 3.0 推出了 v2.x 资源导入工具。此工具支持旧项目资源完美导入,以及代码的辅助迁移。 + +### 资源导入 + +开发者只需要点击主菜单中的 **文件 -> 导入 Cocos Creator 2.x 项目**。 + +![import-menu](import-menu.png) + +然后在弹出的文件浏览对话框中选择 v2.x 项目的根目录。 + +![import-select-project](import-select-project.png) + +> **注意**:旧项目推荐先升级到 v2.4.3 或以上版本,然后再导入到 v3.0,否则无法确保导入结果的正确性。 + +v2.x 项目中所有的资源便会自动呈现在弹出的 **导入 Cocos Creator 2.x 项目** 面板中,开发者可以再次确认要导入的资源,然后点击面板右下角的 **导入** 按钮完成导入。若开发者想要切换导入的 v2.x 项目,点击下图中的搜索图标按钮,即可重新选择项目。 + +![import-project](import-panel.png) + +面板左下角的 **使用说明** 按钮可跳转到导入项目插件的 GitHub 仓库,用于 [更新导入插件](https://github.com/cocos-creator/plugin-import-2.x/blob/main/README.md) 或者提交反馈。 + +### 代码迁移 + +当导入使用 JavaScript 进行开发的 v2.x 项目时,导入插件的代码辅助迁移功能会先将 JavaScript 转换成 TypeScript,再进行代码迁移。 + +例如,导入的 v2.x 项目 JavaScript 代码如下: + +```typescript +// AudioController.js +cc.Class({ + extends: cc.Component, + + properties: { + audioSource: { + type: cc.AudioSource, + default: null + }, + }, + + play: function () { + this.audioSource.play(); + }, + + pause: function () { + this.audioSource.pause(); + }, + +}); +``` + +由于各个项目代码的写法差异以及不同的复杂程度,目前导入插件对代码的迁移仅添加 **组件类型声明**、**属性声明** 和 **函数声明**,组件在场景中的引用都会得到 **保留**,并且函数内部的代码会以 **注释** 的形式迁移。
+另外,v2.x 的原代码则会以注释的形式完整保留一份在迁移后代码的末尾,方便开发者手动转换时参考。 + +上述示例代码在经过导入插件的代码辅助迁移之后,结果如下所示: + +```typescript +// AudioController.ts + +import { _decorator, Component, AudioSource } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('AudioController') +export class AudioController extends Component { + @property + public audioSource:AudioSource = 'null'; + + play () { + //this.audioSource.play(); + } + + pause () { + //this.audioSource.pause(); + } + +} + + +/** + * 注意:已把原脚本注释,由于脚本变动过大,转换的时候可能有遗落,需要自行手动转换 + */ +// cc.Class({ +// extends: cc.Component, +// +// properties: { +// audioSource: { +// type: cc.AudioSource, +// default: null +// }, +// }, +// +// play: function () { +// this.audioSource.play(); +// }, +// +// pause: function () { +// this.audioSource.pause(); +// }, +// +// }); +``` + +> **注意**: +> +> 1. 如果是从 JavaScript 转换为 TypeScript 的。需要在 TypeScript 中声明 **所有属性** 并设置默认值。 +> 2. 如果 **属性检查器** 面板数据丢失,则需要检查属性类型是否与 v2.x 相同。 +> 3. 如果 JavaScript 代码使用外部类型,TypeScript 会提示:通过导入外部源文件或声明进行修复。 + +## 旧版本开发者快速上手 + +### 材质升级 + +在 v3.0 中我们持续改进了材质系统的设计和内置 Shader API,所以从 v2.x 升级到 v3.x 时,部分内容无法自动升级,还需要开发者手动进行调整,详情请参考 [材质升级指南](../material-system/effect-2.x-to-3.0.md)。 + +### 引擎 API 升级 + +#### 针对 Cocos Creator 3D 1.2 用户 + +1. Cocos Creator 3.0 资源加载相关的 API 与 v2.4 一致,都对 `loader` 进行了重构,v1.2 用户可参考 [v2.4 资源管理模块升级指南](https://docs.cocos.com/creator/2.4/manual/zh/release-notes/asset-manager-upgrade-guide.html) 进行升级。 + +2. 组件类名更改 + + 为了符合 v2.x 的 API 规范,Cocos Creator 3.0 将组件类名包含 Component 后缀这样的命名方式舍弃了,并做了数据的自动升级和代码的兼容。 + + 不过建议开发者还是要在代码中搜索所有类似命名方式的使用,并尽快更改为无 Component 后缀的类名。可以使用下面正则表达式进行全局搜索(打开大小写敏感和正则匹配): + + ``` + ([A-Z]\w+)Component + ``` + +#### 针对 Cocos Creator 2.x 用户 + +- 节点上 UI 相关接口变更如下: + + - 与坐标变换计算相关的接口(例如:`size` 和 `anchor`)变更如下: + + 需要先获取节点上的 `UITransform` 组件,再使用对应的接口,例如: + + ```typescript + const uiTrans = node.getComponent(UITransform)!; + uiTrans.anchorX = 0.5; + uiTrans.setContentSize(size); + ``` + + - 其余接口变更如下: + + - `color`:需要先获取节点上的渲染组件(例如:`Sprite` 组件),再使用对应的接口。 + + - `opacity`:如果节点上有渲染组件,直接设置渲染组件的 `color`。如果没有渲染组件,则可以通过添加 `UIOpacity` 组件,并设置相关属性。 + + - `skew`:从 v3.8.6 开始需要在节点上添加 UISkew 组件,之后调用 setSkew 接口设置 skew 属性。 + + - `group`:变更为 `layer`。 + + - `zIndex`:变更为 `UITransform` 中的 [priority](%__APIDOC__%/zh/class/UITransform)。 + + > **注意**:从 v3.1 开始,`priority` 属性已弃用,若需要调整节点树的顺序请使用 `setSiblingIndex` 方法。 + +- `CCSpriteFrame`: + + - 移除接口:`copyWithZone`、`copy`、`clone` 和 `ensureLoadTexture`。 + + - 变更接口: + + - `setFlipX` 和 `isFlipX` -> `flipUVX` + + - `setFlipY` 和 `isFlipY` -> `flipUVY` + + - `getTexture` 和 `setTexture` -> `texture`(此处的类型是 Texture2D/RenderTexture)。 + + - 其余 `get` 和 `set` 对应的方法在 3.0 中都直接对应同名属性(例如:`getOffset` -> `offset`)。 + +- `CCTexture2D`: + + - 变更接口:`genMipmaps` -> `mipmaps`、`initWithElement` -> `image`。 + + - `initWithData` 整个方法被移除,类似的使用是将原先要传入的 `ArrayBufferView` 数据,传给新建的 `ImageAsset`,然后再用 `ImageAsset` 传给新建的 `Texture2D`,从而获得一份图片资源。 + +- `cc.Action`:相关接口全部移除。 + +- **物理**: + + - 2D 变更组件:`cc.Collider` -> `Collider2D`、`cc.BoxCollider` -> `BoxCollider2D`、`cc.RigidBody` -> `RigidBody2D` 等。 + + - 3D 变更组件:`cc.Collider3D` -> `Collider`、`cc.BoxCollider3D` -> `BoxCollider`、`cc.RigidBody3D` -> `RigidBody` 等。 + +- **tween**: + + - 变更接口:`cc.repeatForever` -> `Tween.repeatForever`、`cc.reverseTime` -> `Tween.reverseTime`、`cc.show` -> `Tween.show` 等。 + +- **动画**: + + - 变更接口:`addClip` -> `createState`、`getClips` -> `clips`、`playAdditive` -> `crossFade`、`getAnimationState` -> `getState` 等。 + +- **相机**: + + - 移除接口:`findCamera`、`alignWithScreen`、`main`、`cameras`、 `zoomRatio` 和 `containsNode`。 + + - 变更接口:`backgroundColor` -> `clearColor`、`cullingMask` -> `visibility`、`depth` -> `clearDepth`、`getScreenToWorldPoint` -> `screenToWorld`、`getWorldToScreenPoint` -> `worldToScreen`、`getRay` -> `screenPointToRay` 等。 + +- **音频**: + + - 变更接口:`getLoop` 和 `setLoop` -> `loop`、`getVolume` 和 `setVolume` -> `volume`、`getCurrentTime` 和 `setCurrentTime` -> `currentTime`、`src` -> `clip`。 + +- **材质**: + + - 所有相关改动都需要获得 **MeshRenderer** 或其子类身上的 **材质实例** 来完成。 + + - 移除接口:`setBlend`、`setDepth`、`setStencilEnabled`、`setStencil`、`setCullMode` 和 `define`,其中除了 `define` 是调用 `recompileShaders` 完成更新,其余的都是调用 `overridePipelineStates` 完成更新。 + +- **sys** 下的平台变量变更如下: + +| Cocos Creator 2.x | Cocos Creator 3.0 | +| :---------------- | :-------------------- | +| `BAIDU_GAME` | `BAIDU_MINI_GAME` | +| `VIVO_GAME` | `VIVO_MINI_GAME` | +| `OPPO_GAME` | `OPPO_MINI_GAME` | +| `HUAWEI_GAME` | `HUAWEI_QUICK_GAME` | +| `JKW_GAME` | `COCOSPLAY` | +| `ALIPAY_GAME` | `ALIPAY_MINI_GAME` | +| `BYTEDANCE_GAME` | `BYTEDANCE_MINI_GAME` | + +- **全局变量** 变更如下: + +| Cocos Creator 2.x | Cocos Creator 3.0 | +| :---------------- | :---------------- | +| `CC_BUILD` | `BUILD` | +| `CC_TEST` | `TEST` | +| `CC_EDITOR` | `EDITOR` | +| `CC_PREVIEW` | `PREVIEW` | +| `CC_DEV` | `DEV` | +| `CC_DEBUG` | `DEBUG` | +| `CC_JSB` | `JSB` | +| `CC_WECHATGAME` | `WECHATGAME` | +| `CC_RUNTIME` | `RUNTIME_BASED` | +| `CC_SUPPORT_JIT` | `SUPPORT_JIT` | + +- **动态加载资源**: + + 在 v3.0 中使用 `bundle.load` 或 `resources.load` 动态加载 `sprite-frame` 或 `texture` 时,需要将路径指定到具体的子资源: + + ```ts + // 加载 texture + + // v2.x + resources.load('background', cc.Texture2D, () => {}); + // v3.0 + resources.load('background/texture', Texture2D, () => {}); + ``` + + ```ts + // 加载 sprite frame + + // v2.x + resources.load('background', cc.SpriteFrame, () => {}); + // v3.0 + resources.load('background/spriteFrame', SpriteFrame, () => {}); + ``` +##### `JSB` 接口相关 + +- `jsb.FileUtils` + - `getDataFromFile` 返回类型由 `Uint8Array` 变为 `ArrayBuffer` + +### 编辑器升级 + +#### 构建发布面板 + +Cocos Creator 3.0 中所有平台的构建都内置为插件,因此 **构建发布** 面板也与 v2.4 的不同,各平台独有的构建选项会单独放在一个可折叠的 section 控件内。 + +![image](build-panel.png) + +点击 **构建** 按钮后会跳转到 **构建任务** 面板,所有构建后的平台都会显示在这个面板中。可以在这个面板中修改构建后工程的构建选项再重新构建、可以查看构建日志、打开工程目录等。如果需要返回 **构建发布** 面板编译其他平台的话,点击 **构建任务** 面板左上方的 **新建构建任务** 按钮即可。 + +![image](build.png) + +另外,构建时支持构建成文件分离的多模块结果,便于多模块并发加载、动态加载模块,并且微信引擎插件支持选择不同物理引擎后端。构建完成后生成的 `settings.js` 也改为 `settings.json`,并放置在 `src` 目录下,允许作为资源上传到服务器。 + +#### 资源缩略图面板 + +在 **资源管理器** 中选中资源,即可在 **资源预览** 面板中显示资源的缩略图。若选中资源所在的文件夹,即可显示文件夹下所有资源的缩略图,方便查看。 + +![image](assets-preview.png) + +#### 动画编辑器升级 + +- 支持节点树面板中对节点的搜索与显示过滤 +- 支持使用系统剪贴板复制粘贴节点上的所有动画数据(节点、轨道以及关键帧) +- 支持多选节点后批量添加属性轨道 +- 优化关键帧选取和取消选取的操作体验(Ctrl + 鼠标点击选中关键帧可取消选中) +- 支持在动画编辑状态下继续编辑节点属性,包括粒子和模型材质属性等 + +![image](animation.png) + +#### 项目设置面板更新 + +分成 **Engine 管理器**、**项目设置**、**构建发布** 三大部分。 + +物理碰撞组独立使用 `PhysicsSystem.PhysicsGroup` 类型,不再与 `Node.Layers` 共享分组配置: + +![image](project-setting.png) + +**压缩纹理配置** 修改为在 **项目设置 -> 构建发布** 中配置预设。在 **资源管理器** 中选中图片资源,然后再在 **属性检查器** 中选择预设的方式。
+旧项目升级后,编辑器会自动扫描项目内的所有压缩纹理配置情况,整理出几个预设。由于是自动扫描的,生成的名称可能不是想要的,可以自行在此处修改: + +![image](texture-compress-setting.png) + +#### 编辑器插件系统升级 + +Cocos Creator 3.0 拥有更加强大的插件系统,编辑器几乎所有功能模块都是以插件形式存在。你可以在扩展菜单中快速创建自己的插件,从而实现自己想要的效果。另外,Cocos Creator 3.0 还提供了扩展管理器,可以轻松管理所有扩展插件的运行和卸载。 + +![image](extension-plugin.png) + +### 构建目录差异 + +Cocos Creator 2.x 不同平台构建后生成的目录与 Cocos Creator 3.0 也有着一定程度上的差异。接下来我们以 v2.4.3 为例,和 v3.0 分别在 Web、原生和微信小游戏平台上进行对比。 + +#### Web 平台 + +v2.4.3 构建 Web Desktop 平台后生成的目录: + +![image](web-v243.png) + +v3.0 构建 Web Desktop 平台后生成的目录: + +![image](web-v3.png) + +从以上两张图可以看出 Web 平台构建后生成的内容,v2.4.3 与 v3.0 大部分是相同的,不同之处包括以下几点: + +1. v3.0 将引擎相关的代码,例如核心模块、物理模块、插件脚本等都统一放到了 **cocos-js** 目录下,相比 v2.4.3 分散放在构建目录中看起来更加清晰。 + + ![image](web-cocosjs.png) + +2. v2.4.3 只有一个启动脚本 `main.js`,而 v3.0 则有以下两个启动脚本: + - `index.js`:用于做一些预处理工作 + - `application.js`:用于启动游戏 + +3. v2.4.3 中用于管理配置的 `src/settings.js` 在 v3.0 改为 `src/settings.json`。 + +4. v2.4.3 中的首屏图片 `splash.png`,在 v3.0 则默认存储在 `settings.json` 中。 + +5. v2.4.3 中的 `style-desktop.css` 和 `style-mobile.css`,在 v3.0 则合并为 `style.css`。 + +#### 微信小游戏平台 + +v2.4.3 构建微信小游戏后生成的目录: + +![image](wechat-v243.png) + +v3.0 构建微信小游戏后生成的目录: + +![image](wechat-v3.png) + +从以上两张图可以看出微信小游戏平台构建后生成的内容,v2.4.3 与 v3.0 大部分是相同的,不同之处包括以下几点: + +1. v3.0 将引擎相关的代码,例如核心模块、物理模块、插件脚本等都统一放到了 `wechatgame/cocos-js` 目录下。而 v2.4.3 则分散一部分放在 `wechatgame` 目录下,一部分放在 `wechatgame/cocos` 目录下。 + + ![image](wechat-cocosjs.png) + +2. v2.4.3 将小游戏的适配层代码都编译到 `adapter-min.js` 中,而 v3.0 则是将适配层代码全部以散文件的形式存储在 `libs` 目录下,没有进行编译。 + +3. v2.4.3 的启动脚本是 `main.js`,v3.0 的启动脚本则是 `application.js`。 + +4. v2.4.3 将所有动态代码的引用记录在了 `ccRequire.js` 中,而 v3.0 目前暂时没有这个功能。 + +5. v2.4.3 中用于管理配置的 `src/settings.js` 在 v3.0 改为 `src/settings.json`。 + +#### 原生平台 + +v2.4.3 构建 Windows 平台后生成的发布包目录如下: + +![image](v243-windows.png) + +v3.0 构建 Windows 平台后生成的发布包目录如下: + +![image](v3-windows.png) + +从以上两张图可以看出 Windows 平台构建后生成的发布包目录,v2.4.3 与 v3.0 差异较大: + +1. v2.4.3 的发布包名称是以 **构建发布** 面板中的 **构建模板** 命名的(例如 `jsb-link`),v3.0 则是以 **当前构建的原生平台** 命名的(例如 `windows`、`Android`)。 + +2. 因为各个原生平台(例如 Android、Windows)构建后生成的底层 C++ 代码是完全一致的,所以在 v3.0,我们将 v2.4.3 存放在发布包目录 `frameworks/runtime-src/Classes` 中的底层 C++ 代码单独提取出来放在共享的项目目录下的 `native/engine/common` 文件夹中。这样在构建原生平台时,如果检测到已经存在该文件夹,这部分内容便不会再进行处理,加快构建速度。 + + ![image](engine-common.png) + +3. v2.4.3 发布包目录中应用层相关的文件,在 v3.0 都统一合并到了 `assets` 目录中。 + + v2.4.3 应用层相关的文件包括: + + - `assets` 目录(资源) + - `jsb-adapter` 目录(适配层代码) + - `src` 目录(引擎相关代码、插件脚本、配置管理脚本 `settings.js` 等) + - 相关配置文件(`.cocos-project.json`、`cocos-project-template.json`、`project.json`) + - 启动脚本(`main.js`) + + v3.0 `assets` 目录结构如下: + + ![image](v3-assets.png) + + v3.0 在合并的过程中也做了相应的调整和改动: + + - 原 v2.4.3 全部放在发布包目录 `src` 目录下的引擎相关代码(例如核心模块、物理模块、插件脚本等),在 v3.0 都放到了发布包目录 `assets/src/cocos-js` 目录下。 + + - 原 v2.4.3 中用于管理配置的 `src/settings.js`,在 v3.0 改为 `assets/src/settings.json`。 + +4. v2.4.3 会将所有原生平台的构建工程都生成在发布包目录 `frameworks/runtime-src` 目录下: + + ![image](v243-build-template.png) + + 而 v3.0 则是将构建工程生成在发布包目录 `proj` 目录下,且只生成当前构建平台的工程: + + ![image](v3-build-template.png) + + 同时,v3.0 也做了代码和配置的分离,将一部分代码和配置放入源码管理,位于项目目录下的 `native/engine/当前构建的平台名称` 文件夹中(例如 `native/engine/win32`、`native/engine/android`)。开发者可以在这里集成 SDK 或者做二次开发,删除构建后生成的发布包目录(例如 `build/windows`)不会影响已经集成的 SDK。 + + ![image](v3-build-native.png) + +5. 一些编译时需要用到的资源,例如应用图标、应用启动脚本等,v2.4.3 是存储在构建工程中,而 v3.0 则是存储在项目目录的 `native/engine/当前构建的平台名称` 文件夹中。 + +## 升级常见问题(FAQ) + +### 升级后项目脚本在 VS Code 打开时,绑定组件定义等操作出现报红现象 + +Cocos Creator 3.x 开启了 TypeScript 的严格模式,会对代码进行更严格的审查,排除开发过程中可能会出现的因为疏忽而导致的问题。 + +如果不想使用严格模式,可以在 Creator 顶部菜单栏的 **项目 -> 项目设置 -> 脚本** 中勾选 **启用宽松模式**。需要提醒的是,我们并不鼓励关闭严格模式,因为严格空值检查能够减少代码运行时的一些低级报错。 + +关于严格模式下的书写规范,可以参照官方案例 快上车 3D([GitHub](https://github.com/cocos/cocos-tutorial-taxi-game)。 + +### Action 动作全都失效 + +因为 Cocos Creator 3.x 移除了 Action 动作系统,统一使用 Tween 缓动系统。 + +### 修改 2D 节点的 `size` 和 `anchor` 不生效 + +需要先获取节点上的 UITransform 组件,再使用对应的接口,例如: + +```typescript +const uiTrans = node.getComponent(UITransform)!; +uiTrans.anchorX = 0.5; +uiTrans.setContentSize(size); +``` + +### 修改 2D 节点的 `color` 不生效 + +需要先获取节点上的渲染组件(例如 Sprite 组件),再使用对应的接口,例如: + +```typescript +const uiColor = node.getComponent(Sprite)!; +uiColor.color = color(255,255,255); +``` + +### 修改 2D 节点的 `skew` 不生效 + +从 v3.8.6 开始需要在节点上添加 UISkew 组件,之后调用 setSkew 接口设置 skew 属性。 + +### 无法获取分组,但 Creator 的项目设置面板中仍有分组设置(Layers) + +v2.x 的 `group` 分组管理从 v3.0 开始变更为 `Layer`,如下图所示。在 v2.x 中通过 `node.group` 获取到的是分组名,而在 v3.x 通过 `node.layer` 获取到的是 **分组值**,并且分组值是以 2 的指数幂设定。 + +![update-setting](update-setting.png) + +User Layer 0 的 layer 值为:20 = 1。
+User Layer 1 的 layer 值为:21 = 2。
+User Layer 6 的 layer 值为:26 = 64。 + +### 通过 `zIndex` 设置同级节点失效 + +从 v3.0 开始 `zIndex` 接口已经被移除,若需要调整节点树的顺序请使用 `setSiblingIndex` 方法来替换使用。 + +### 通过 `getComponent()` 无法获取到节点上挂载的脚本 + +请查询对应脚本的类名,而不是脚本名,因为在 v3.x 中脚本组件是以脚本中定义的类名为准的,而不是脚本名。常出现因为大小写而导致脚本找不到的问题。详情请参考 [创建脚本](../scripting/setup.md)。 + +### 动态加载 `resources` 文件夹下的图片时提示找不到 + +图片设置为 `sprite-frame`、`texture` 或其他图片类型后,将会在 **资源管理器** 中生成一个对应类型的资源。但如果直接加载 `testAssets/image`,得到的类型将会是 `ImageAsset`,必须指定路径到具体的子资源。 + +例如一张设置为 `sprite-frame` 类型的图片在 `resources` 文件夹下的路径为 `testAssets/image`,那么要加载 `SpriteFrame` 应该这么写: + +```typescript +resources.load("testAssets/image/spriteFrame", SpriteFrame, (err, spriteFrame) => { + this.node.getComponent(Sprite).spriteFrame = spriteFrame; +}); +``` + +若加载的是 `texture` 类型的图片,则将 `spriteFrame` 修改为 `texture` 即可。 + +### 物体产生物理碰撞之后,原有的物理碰撞回调没有了 + +从 v3.0 开始,碰撞体回调需要在开始的时候进行注册,与原先 v2.x 会直接产生回调不同。因此开发者需要在物理回调的脚本中增加对回调函数的注册。例如: + +```typescript +let collider = this.getComponent(Collider2D); +if (collider) { + // 只在两个碰撞体开始接触时被调用一次 + collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); + // 只在两个碰撞体结束接触时被调用一次 + collider.on(Contact2DType.END_CONTACT, this.onEndContact, this); + // 每次将要处理碰撞体接触逻辑时被调用 + collider.on(Contact2DType.PRE_SOLVE, this.onPreSolve, this); + // 每次处理完碰撞体接触逻辑时被调用 + collider.on(Contact2DType.POST_SOLVE, this.onPostSolve, this); + } +}); +``` + +### 升级之后,物理碰撞分组不见了 + +目前导入插件还不支持物理碰撞矩阵,因此暂时需要开发者手动设置碰撞矩阵,可在 Creator 主菜单 **项目 -> 项目设置 -> 物理** 中重新设置。 + +### 音频系统的 `audioEngine` 接口失效,无法播放音频 + +从 v3.0 开始,移除了 `audioEngine` 接口,统一使用 **AudioSource** 组件来控制音频的播放。详情请参考 [AudioSource 组件](../audio-system/audiosource.md)。 + +### Button 按钮无法点击 + +排除代码和渲染层级问题,请查看 **Button** 节点的 `Scale` 属性中 `Z` 轴的值是否为 0,如果是,将其修改为 1 即可。 + +### 升级后对脚本进行修改,出现编辑器卡死的情况 + +检查升级后脚本中定义的组件类型的属性装饰器 `property` 是否未定义,如果未定义,则是由于导入插件太过于老旧导致的,请参考 [插件升级](https://github.com/cocos-creator/plugin-import-2.x) 对导入插件进行更新升级。更新导入插件后,需要 **重新进行项目升级**。 + +### 升级后在脚本中修改节点的 `Position` 时,直接通过节点(例如 `node.x`)修改不生效 + +从 v3.0 开始,`node` 节点上不允许直接访问坐标位置,需要先访问 `position` 再访问坐标值。并且 v3.x 中的 `position` 为 **只读属性**,若需要修改,请使用 `setPosition` 方法。例如: + +```typescript +// v2.x + +// 访问坐标轴 +let xAxis = this.node.x; +// 修改 X 轴坐标 +this.node.x = 200; + +// v3.x + +// 访问坐标 +let pos = this.node.position; +// 修改 X 轴坐标 +this.node.setPosition(200,pos.y,pos.z); +``` + +## TypeScript 参考教程 + +- [Cocos Creator 3.0 TypeScript 问题答疑及经验分享](https://forum.cocos.org/t/topic/106995) +- [TypeScript 官方网站](https://www.typescriptlang.org/) +- [TypeScript - Classes](https://www.typescriptlang.org/docs/handbook/classes.html) +- [TypeScript - Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) +- [TypeScript - DefinitelyTyped](http://definitelytyped.org/) +- [X 分钟速成 TypeScript](https://learnxinyminutes.com/docs/zh-cn/typescript-cn/) +- [TypeScript 源码](https://github.com/Microsoft/TypeScript) +- [开发者回避使用 TypeScript 的三个借口 — 以及应当使用 TypeScript 的更有说服力的原因](https://mp.weixin.qq.com/s/7QQJxErt2-e4jLK2_4GUFA) diff --git a/versions/4.0/zh/release-notes/v243-build-template.png b/versions/4.0/zh/release-notes/v243-build-template.png new file mode 100644 index 0000000000..609649601b Binary files /dev/null and b/versions/4.0/zh/release-notes/v243-build-template.png differ diff --git a/versions/4.0/zh/release-notes/v243-windows.png b/versions/4.0/zh/release-notes/v243-windows.png new file mode 100644 index 0000000000..1bd8d4b7e1 Binary files /dev/null and b/versions/4.0/zh/release-notes/v243-windows.png differ diff --git a/versions/4.0/zh/release-notes/v3-assets.png b/versions/4.0/zh/release-notes/v3-assets.png new file mode 100644 index 0000000000..9c4fc24a48 Binary files /dev/null and b/versions/4.0/zh/release-notes/v3-assets.png differ diff --git a/versions/4.0/zh/release-notes/v3-build-native.png b/versions/4.0/zh/release-notes/v3-build-native.png new file mode 100644 index 0000000000..b3b3182e51 Binary files /dev/null and b/versions/4.0/zh/release-notes/v3-build-native.png differ diff --git a/versions/4.0/zh/release-notes/v3-build-template.png b/versions/4.0/zh/release-notes/v3-build-template.png new file mode 100644 index 0000000000..99b2b22c62 Binary files /dev/null and b/versions/4.0/zh/release-notes/v3-build-template.png differ diff --git a/versions/4.0/zh/release-notes/v3-windows.png b/versions/4.0/zh/release-notes/v3-windows.png new file mode 100644 index 0000000000..8c454efcbb Binary files /dev/null and b/versions/4.0/zh/release-notes/v3-windows.png differ diff --git a/versions/4.0/zh/release-notes/web-cocosjs.png b/versions/4.0/zh/release-notes/web-cocosjs.png new file mode 100644 index 0000000000..a08cb3d527 Binary files /dev/null and b/versions/4.0/zh/release-notes/web-cocosjs.png differ diff --git a/versions/4.0/zh/release-notes/web-v243.png b/versions/4.0/zh/release-notes/web-v243.png new file mode 100644 index 0000000000..2464ee06f6 Binary files /dev/null and b/versions/4.0/zh/release-notes/web-v243.png differ diff --git a/versions/4.0/zh/release-notes/web-v3.png b/versions/4.0/zh/release-notes/web-v3.png new file mode 100644 index 0000000000..f66a653005 Binary files /dev/null and b/versions/4.0/zh/release-notes/web-v3.png differ diff --git a/versions/4.0/zh/release-notes/wechat-cocosjs.png b/versions/4.0/zh/release-notes/wechat-cocosjs.png new file mode 100644 index 0000000000..8675726308 Binary files /dev/null and b/versions/4.0/zh/release-notes/wechat-cocosjs.png differ diff --git a/versions/4.0/zh/release-notes/wechat-v243.png b/versions/4.0/zh/release-notes/wechat-v243.png new file mode 100644 index 0000000000..df618a8b6e Binary files /dev/null and b/versions/4.0/zh/release-notes/wechat-v243.png differ diff --git a/versions/4.0/zh/release-notes/wechat-v3.png b/versions/4.0/zh/release-notes/wechat-v3.png new file mode 100644 index 0000000000..6e09558a29 Binary files /dev/null and b/versions/4.0/zh/release-notes/wechat-v3.png differ diff --git a/versions/4.0/zh/render-pipeline/builtin-pipeline.md b/versions/4.0/zh/render-pipeline/builtin-pipeline.md new file mode 100644 index 0000000000..c9e636f6d3 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/builtin-pipeline.md @@ -0,0 +1,62 @@ +# 内置渲染管线 + +Cocos Creator 3.1 的内置渲染管线包括 **builtin-forward**(前向渲染管线)和 **builtin-deferred**(延迟渲染管线)。渲染管线可通过编辑器主菜单中的 **项目 -> 项目设置 -> 项目数据 -> 渲染管线** 进行设置,设置完成之后 **重启编辑器** 即可生效。 + +![setting](./image/setting.png) + +## 前向渲染管线 + +引擎默认使用 **前向渲染管线**,前向渲染管线的执行流程如下图所示: + + + +前向渲染主要包括 **ShadowFlow** 和 **ForwardFlow** 两个阶段: +- **ShadowFlow** 中包含一个 **ShadowStage** 会预先对场景中需要投射阴影的物体进行阴影贴图的绘制。 +- **ForwardFlow** 包含一个 **ForwardStage**,会对场景中所有物体按照 **非透明 -> 光照 -> 透明 -> UI** 的顺序依次进行绘制。在计算光照时,每个物体都会与所有光照进行计算确定是否照射到该物体,照射到该物体的光照将会执行绘制并进行光照计算,目前场景中只支持一个平行光,可接受的最大光照数量为 16。 + +## 延迟渲染管线 + +目前引擎提供了试验版本的内置 **延迟渲染管线**,对于光照数量比较多的项目可以使用 **延迟渲染管线** 来缓解光照计算的压力。延迟管线的执行流程如下图所示: + + + +内置的延迟渲染管线主要包括 **ShadowFlow** 和 **MainFlow** 两个过程: +1. **ShadowFlow** 与前向渲染一致,用于预先进行阴影贴图的绘制。 + +2. **MainFlow** 包含了 **GBufferStage**、**LightingStage**、**BloomStage** 和 **PostProcessStage** 四个阶段: + - **GbufferStage** 会对场景中的非透明物体进行绘制; + - 然后 **LightingStage** 会对输出到 **GBuffer** 中的非透明物体信息进行基于屏幕空间的光照计算,再绘制半透明物体。如果有非透明物体并且设备支持 ComputeShader,还会进行 SSPR 的资源收集与绘制; + - 若还开启了 Bloom 效果,**BloomStage** 会对已经经过 **LightingStage** 处理后的图像进行 Bloom 后处理; + - 最后 **PostProcessStage** 会把 **BloomStage**/**LightingStage** 输出的全屏图像绘制到主屏幕中,再进行 UI 的绘制。 + +开启 Bloom 有以下两种方式: + +1. 点击 Creator 顶部菜单栏中的 **项目 -> 项目设置 -> Macro Configurations**,然后勾选 **ENABLE_BLOOM** 即可开启: + + + +2. 通过代码开启,示例如下: + +```js +@ccclass('BloomSwitch') +export class BloomSwitch extends Component { + bloomEnabled: boolean = false; + + setupPipeline() { + (director.root?.pipeline as DeferredPipeline).bloomEnabled = this.bloomEnabled; + } + + switchEnable (toggle: Toggle) { + if (toggle.isChecked !== this.bloomEnabled) { + this.bloomEnabled = toggle.isChecked; + this.setupPipeline(); + } + } +} +``` + +延迟渲染管线依赖 GPU 的 **Multiple Render Targets** 特性用于绘制 **GBuffer**,目前大部分移动平台应该都支持。WebGL1.0 环境下可以使用 **WEBGL_draw_buffers** 扩展支持,不过部分 WebGL1.0 平台可能不支持该扩展,那么就不能使用延迟渲染管线。 + +另外延迟渲染管线对于 **卡通材质** 无法正常进行绘制,比如引擎内置的 `builtin-toon` 材质。 + +引擎内置的渲染管线后续也会不断优化性能,并添加新的特性,为开发者提供更加多元和丰富的渲染特性。 diff --git a/versions/4.0/zh/render-pipeline/code/PassUtils.ts b/versions/4.0/zh/render-pipeline/code/PassUtils.ts new file mode 100644 index 0000000000..a2131fe9f6 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/code/PassUtils.ts @@ -0,0 +1,1148 @@ +import { _decorator, gfx, renderer, pipeline, rendering, + Vec4, macro, geometry, toRadian, cclegacy, Material } from 'cc'; +const { ClearFlagBit, Color, LoadOp, + Format, Rect, StoreOp, Viewport } = gfx; +const {Camera, CSMLevel, LightType, ShadowType, SKYBOX_FLAG} = renderer.scene; +const { supportsR32FloatTexture } = pipeline; +const { AccessType, AttachmentType, ComputeView, LightInfo, QueueHint, + RasterView, ResourceResidency, SceneFlags } = rendering; + +function SRGBToLinear (out, gamma: Vec4) { + out.x = gamma.x * gamma.x; + out.y = gamma.y * gamma.y; + out.z = gamma.z * gamma.z; +} + +export function isUICamera (camera): boolean { + const scene = camera.scene!; + const batches = scene.batches; + for (let i = 0; i < batches.length; i++) { + const batch = batches[i]; + if (camera.visibility & batch.visFlags) { + return true; + } + } + return false; +} + + +let profilerCamera = null; + +export function decideProfilerCamera (cameras: any[]) { + for (let i = cameras.length - 1; i >= 0; --i) { + const camera = cameras[i]; + if (camera.window.swapchain) { + profilerCamera = camera; + return; + } + } + profilerCamera = null; +} + +// Anti-aliasing type, other types will be gradually added in the future +export enum AntiAliasing { + NONE, + FXAA, + FXAAHQ, +} + +export function validPunctualLightsCulling (pipeline, camera) { + const sceneData = pipeline.pipelineSceneData; + const validPunctualLights = sceneData.validPunctualLights; + validPunctualLights.length = 0; + const _sphere = geometry.Sphere.create(0, 0, 0, 1); + const { spotLights } = camera.scene!; + for (let i = 0; i < spotLights.length; i++) { + const light = spotLights[i]; + if (light.baked) { + continue; + } + + geometry.Sphere.set(_sphere, light.position.x, light.position.y, light.position.z, light.range); + if (geometry.intersect.sphereFrustum(_sphere, camera.frustum)) { + validPunctualLights.push(light); + } + } + + const { sphereLights } = camera.scene!; + for (let i = 0; i < sphereLights.length; i++) { + const light = sphereLights[i]; + if (light.baked) { + continue; + } + geometry.Sphere.set(_sphere, light.position.x, light.position.y, light.position.z, light.range); + if (geometry.intersect.sphereFrustum(_sphere, camera.frustum)) { + validPunctualLights.push(light); + } + } +} + +const _cameras = []; + +export function getCameraUniqueID (camera) { + if (_cameras.indexOf(camera) === -1) { + _cameras.push(camera); + } + return _cameras.indexOf(camera); +} + +export function getLoadOpOfClearFlag (clearFlag, attachment) { + let loadOp = LoadOp.CLEAR; + if (!(clearFlag & ClearFlagBit.COLOR) + && attachment === AttachmentType.RENDER_TARGET) { + if (clearFlag & SKYBOX_FLAG) { + loadOp = LoadOp.CLEAR; + } else { + loadOp = LoadOp.LOAD; + } + } + if ((clearFlag & ClearFlagBit.DEPTH_STENCIL) !== ClearFlagBit.DEPTH_STENCIL + && attachment === AttachmentType.DEPTH_STENCIL) { + if (!(clearFlag & ClearFlagBit.DEPTH)) loadOp = LoadOp.LOAD; + if (!(clearFlag & ClearFlagBit.STENCIL)) loadOp = LoadOp.LOAD; + } + return loadOp; +} + +export function getRenderArea (camera, width: number, height: number, light = null, level = 0) { + const out = new Rect(); + const vp = camera ? camera.viewport : new Rect(0, 0, 1, 1); + const w = width; + const h = height; + out.x = vp.x * w; + out.y = vp.y * h; + out.width = vp.width * w; + out.height = vp.height * h; + if (light) { + switch (light.type) { + case LightType.DIRECTIONAL: { + const mainLight = light; + if (mainLight.shadowFixedArea || mainLight.csmLevel === CSMLevel.LEVEL_1) { + out.x = 0; + out.y = 0; + out.width = w; + out.height = h; + } else { + const screenSpaceSignY = cclegacy.director.root.device.capabilities.screenSpaceSignY; + out.x = level % 2 * 0.5 * w; + if (screenSpaceSignY) { + out.y = (1 - Math.floor(level / 2)) * 0.5 * h; + } else { + out.y = Math.floor(level / 2) * 0.5 * h; + } + out.width = 0.5 * w; + out.height = 0.5 * h; + } + break; + } + case LightType.SPOT: { + out.x = 0; + out.y = 0; + out.width = w; + out.height = h; + break; + } + default: + } + } + return out; +} + +class FxaaData { + fxaaMaterial: Material; + private _updateFxaaPass () { + if (!this.fxaaMaterial) return; + + const combinePass = this.fxaaMaterial.passes[0]; + combinePass.beginChangeStatesSilently(); + combinePass.tryCompile(); + combinePass.endChangeStatesSilently(); + } + private _init () { + if (this.fxaaMaterial) return; + this.fxaaMaterial = new Material(); + this.fxaaMaterial._uuid = 'builtin-fxaa-material'; + this.fxaaMaterial.initialize({ effectName: 'pipeline/fxaa-hq' }); + for (let i = 0; i < this.fxaaMaterial.passes.length; ++i) { + this.fxaaMaterial.passes[i].tryCompile(); + } + this._updateFxaaPass(); + } + constructor () { + this._init(); + } +} + +let fxaaData: FxaaData | null = null; +export function buildFxaaPass (camera, + ppl, + inputRT: string) { + if (!fxaaData) { + fxaaData = new FxaaData(); + } + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + let width = camera.window.width; + let height = camera.window.height; + const area = getRenderArea(camera, width, height); + width = area.width; + height = area.height; + // Start + const clearColor = new Color(0, 0, 0, 1); + if (camera.clearFlag & ClearFlagBit.COLOR) { + clearColor.x = camera.clearColor.x; + clearColor.y = camera.clearColor.y; + clearColor.z = camera.clearColor.z; + } + clearColor.w = camera.clearColor.w; + + const fxaaPassRTName = `dsFxaaPassColor${cameraName}`; + const fxaaPassDSName = `dsFxaaPassDS${cameraName}`; + + // ppl.updateRenderWindow(inputRT, camera.window); + if (!ppl.containsResource(fxaaPassRTName)) { + ppl.addRenderTarget(fxaaPassRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(fxaaPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(fxaaPassRTName, width, height); + ppl.updateDepthStencil(fxaaPassDSName, width, height); + const fxaaPassIdx = 0; + const fxaaPass = ppl.addRasterPass(width, height, 'fxaa'); + fxaaPass.name = `CameraFxaaPass${cameraID}`; + fxaaPass.setViewport(new Viewport(area.x, area.y, width, height)); + if (ppl.containsResource(inputRT)) { + const computeView = new ComputeView(); + computeView.name = 'sceneColorMap'; + fxaaPass.addComputeView(inputRT, computeView); + } + const fxaaPassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + clearColor); + fxaaPass.addRasterView(fxaaPassRTName, fxaaPassView); + fxaaData.fxaaMaterial.setProperty('texSize', new Vec4(width, height, 1.0 / width, 1.0 / height), fxaaPassIdx); + fxaaPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, fxaaData.fxaaMaterial, fxaaPassIdx, + SceneFlags.NONE, + ); + return { rtName: fxaaPassRTName, dsName: fxaaPassDSName }; +} + +export const MAX_BLOOM_FILTER_PASS_NUM = 6; +export const BLOOM_PREFILTERPASS_INDEX = 0; +export const BLOOM_DOWNSAMPLEPASS_INDEX = 1; +export const BLOOM_UPSAMPLEPASS_INDEX = BLOOM_DOWNSAMPLEPASS_INDEX + MAX_BLOOM_FILTER_PASS_NUM; +export const BLOOM_COMBINEPASS_INDEX = BLOOM_UPSAMPLEPASS_INDEX + MAX_BLOOM_FILTER_PASS_NUM; +class BloomData { + bloomMaterial: Material; + threshold = 0.1; + iterations = 2; + intensity = 0.8; + private _updateBloomPass () { + if (!this.bloomMaterial) return; + + const prefilterPass = this.bloomMaterial.passes[BLOOM_PREFILTERPASS_INDEX]; + prefilterPass.beginChangeStatesSilently(); + prefilterPass.tryCompile(); + prefilterPass.endChangeStatesSilently(); + + for (let i = 0; i < MAX_BLOOM_FILTER_PASS_NUM; ++i) { + const downsamplePass = this.bloomMaterial.passes[BLOOM_DOWNSAMPLEPASS_INDEX + i]; + downsamplePass.beginChangeStatesSilently(); + downsamplePass.tryCompile(); + downsamplePass.endChangeStatesSilently(); + + const upsamplePass = this.bloomMaterial.passes[BLOOM_UPSAMPLEPASS_INDEX + i]; + upsamplePass.beginChangeStatesSilently(); + upsamplePass.tryCompile(); + upsamplePass.endChangeStatesSilently(); + } + + const combinePass = this.bloomMaterial.passes[BLOOM_COMBINEPASS_INDEX]; + combinePass.beginChangeStatesSilently(); + combinePass.tryCompile(); + combinePass.endChangeStatesSilently(); + } + private _init () { + if (this.bloomMaterial) return; + this.bloomMaterial = new Material(); + this.bloomMaterial._uuid = 'builtin-bloom-material'; + this.bloomMaterial.initialize({ effectName: 'pipeline/bloom' }); + for (let i = 0; i < this.bloomMaterial.passes.length; ++i) { + this.bloomMaterial.passes[i].tryCompile(); + } + this._updateBloomPass(); + } + constructor () { + this._init(); + } +} +let bloomData: BloomData | null = null; +export function buildBloomPasses (camera, + ppl, + inputRT: string, + threshold = 0.1, + iterations = 2, + intensity = 0.8) { + if (!bloomData) { + bloomData = new BloomData(); + } + bloomData.threshold = threshold; + bloomData.iterations = iterations; + bloomData.intensity = intensity; + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + let width = camera.window.width; + let height = camera.window.height; + const area = getRenderArea(camera, width, height); + width = area.width; + height = area.height; + // Start bloom + const bloomClearColor = new Color(0, 0, 0, 1); + if (camera.clearFlag & ClearFlagBit.COLOR) { + bloomClearColor.x = camera.clearColor.x; + bloomClearColor.y = camera.clearColor.y; + bloomClearColor.z = camera.clearColor.z; + } + bloomClearColor.w = camera.clearColor.w; + // ==== Bloom prefilter === + const bloomPassPrefilterRTName = `dsBloomPassPrefilterColor${cameraName}`; + const bloomPassPrefilterDSName = `dsBloomPassPrefilterDS${cameraName}`; + + width >>= 1; + height >>= 1; + if (!ppl.containsResource(bloomPassPrefilterRTName)) { + ppl.addRenderTarget(bloomPassPrefilterRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(bloomPassPrefilterDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(bloomPassPrefilterRTName, width, height); + ppl.updateDepthStencil(bloomPassPrefilterDSName, width, height); + const bloomPrefilterPass = ppl.addRasterPass(width, height, 'bloom-prefilter'); + bloomPrefilterPass.name = `CameraBloomPrefilterPass${cameraID}`; + bloomPrefilterPass.setViewport(new Viewport(area.x, area.y, width, height)); + if (ppl.containsResource(inputRT)) { + const computeView = new ComputeView(); + computeView.name = 'outputResultMap'; + bloomPrefilterPass.addComputeView(inputRT, computeView); + } + const prefilterPassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + bloomClearColor); + bloomPrefilterPass.addRasterView(bloomPassPrefilterRTName, prefilterPassView); + bloomData.bloomMaterial.setProperty('texSize', new Vec4(0, 0, bloomData.threshold, 0), 0); + bloomPrefilterPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, bloomData.bloomMaterial, 0, + SceneFlags.NONE, + ); + // === Bloom downSampler === + for (let i = 0; i < bloomData.iterations; ++i) { + const texSize = new Vec4(width, height, 0, 0); + const bloomPassDownSampleRTName = `dsBloomPassDownSampleColor${cameraName}${i}`; + const bloomPassDownSampleDSName = `dsBloomPassDownSampleDS${cameraName}${i}`; + width >>= 1; + height >>= 1; + if (!ppl.containsResource(bloomPassDownSampleRTName)) { + ppl.addRenderTarget(bloomPassDownSampleRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(bloomPassDownSampleDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(bloomPassDownSampleRTName, width, height); + ppl.updateDepthStencil(bloomPassDownSampleDSName, width, height); + const bloomDownSamplePass = ppl.addRasterPass(width, height, `bloom-downsample${i}`); + bloomDownSamplePass.name = `CameraBloomDownSamplePass${cameraID}${i}`; + bloomDownSamplePass.setViewport(new Viewport(area.x, area.y, width, height)); + const computeView = new ComputeView(); + computeView.name = 'bloomTexture'; + if (i === 0) { + bloomDownSamplePass.addComputeView(bloomPassPrefilterRTName, computeView); + } else { + bloomDownSamplePass.addComputeView(`dsBloomPassDownSampleColor${cameraName}${i - 1}`, computeView); + } + const downSamplePassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + bloomClearColor); + bloomDownSamplePass.addRasterView(bloomPassDownSampleRTName, downSamplePassView); + bloomData.bloomMaterial.setProperty('texSize', texSize, BLOOM_DOWNSAMPLEPASS_INDEX + i); + bloomDownSamplePass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, bloomData.bloomMaterial, BLOOM_DOWNSAMPLEPASS_INDEX + i, + SceneFlags.NONE, + ); + } + // === Bloom upSampler === + for (let i = 0; i < bloomData.iterations; ++i) { + const texSize = new Vec4(width, height, 0, 0); + const bloomPassUpSampleRTName = `dsBloomPassUpSampleColor${cameraName}${bloomData.iterations - 1 - i}`; + const bloomPassUpSampleDSName = `dsBloomPassUpSampleDS${cameraName}${bloomData.iterations - 1 - i}`; + width <<= 1; + height <<= 1; + if (!ppl.containsResource(bloomPassUpSampleRTName)) { + ppl.addRenderTarget(bloomPassUpSampleRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(bloomPassUpSampleDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(bloomPassUpSampleRTName, width, height); + ppl.updateDepthStencil(bloomPassUpSampleDSName, width, height); + const bloomUpSamplePass = ppl.addRasterPass(width, height, `bloom-upsample${i}`); + bloomUpSamplePass.name = `CameraBloomUpSamplePass${cameraID}${bloomData.iterations - 1 - i}`; + bloomUpSamplePass.setViewport(new Viewport(area.x, area.y, width, height)); + const computeView = new ComputeView(); + computeView.name = 'bloomTexture'; + if (i === 0) { + bloomUpSamplePass.addComputeView(`dsBloomPassDownSampleColor${cameraName}${bloomData.iterations - 1}`, computeView); + } else { + bloomUpSamplePass.addComputeView(`dsBloomPassUpSampleColor${cameraName}${bloomData.iterations - i}`, computeView); + } + const upSamplePassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + bloomClearColor); + bloomUpSamplePass.addRasterView(bloomPassUpSampleRTName, upSamplePassView); + bloomData.bloomMaterial.setProperty('texSize', texSize, BLOOM_UPSAMPLEPASS_INDEX + i); + bloomUpSamplePass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, bloomData.bloomMaterial, BLOOM_UPSAMPLEPASS_INDEX + i, + SceneFlags.NONE, + ); + } + // === Bloom Combine Pass === + const bloomPassCombineRTName = `dsBloomPassCombineColor${cameraName}`; + const bloomPassCombineDSName = `dsBloomPassCombineDS${cameraName}`; + + width = area.width; + height = area.height; + if (!ppl.containsResource(bloomPassCombineRTName)) { + ppl.addRenderTarget(bloomPassCombineRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(bloomPassCombineDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(bloomPassCombineRTName, width, height); + ppl.updateDepthStencil(bloomPassCombineDSName, width, height); + const bloomCombinePass = ppl.addRasterPass(width, height, 'bloom-combine'); + bloomCombinePass.name = `CameraBloomCombinePass${cameraID}`; + bloomCombinePass.setViewport(new Viewport(area.x, area.y, width, height)); + const computeViewOut = new ComputeView(); + computeViewOut.name = 'outputResultMap'; + bloomCombinePass.addComputeView(inputRT, computeViewOut); + const computeViewBt = new ComputeView(); + computeViewBt.name = 'bloomTexture'; + bloomCombinePass.addComputeView(`dsBloomPassUpSampleColor${cameraName}${0}`, computeViewBt); + const combinePassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + bloomClearColor); + bloomCombinePass.addRasterView(bloomPassCombineRTName, combinePassView); + bloomData.bloomMaterial.setProperty('texSize', new Vec4(0, 0, 0, bloomData.intensity), BLOOM_COMBINEPASS_INDEX); + bloomCombinePass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, bloomData.bloomMaterial, BLOOM_COMBINEPASS_INDEX, + SceneFlags.NONE, + ); + return { rtName: bloomPassCombineRTName, dsName: bloomPassCombineDSName }; +} + +class PostInfo { + postMaterial: Material; + antiAliasing: AntiAliasing = AntiAliasing.NONE; + private _init () { + this.postMaterial = new Material(); + this.postMaterial.name = 'builtin-post-process-material'; + if (macro.ENABLE_ANTIALIAS_FXAA) { + this.antiAliasing = AntiAliasing.FXAA; + } + this.postMaterial.initialize({ + effectName: 'pipeline/post-process', + defines: { + // Anti-aliasing type, currently only fxaa, so 1 means fxaa + ANTIALIAS_TYPE: this.antiAliasing, + }, + }); + for (let i = 0; i < this.postMaterial.passes.length; ++i) { + this.postMaterial.passes[i].tryCompile(); + } + } + constructor (antiAliasing: AntiAliasing = AntiAliasing.NONE) { + this.antiAliasing = antiAliasing; + this._init(); + } +} + +let postInfo: PostInfo | null = null; + +export function buildPostprocessPass (camera, + ppl, + inputTex: string, + antiAliasing: AntiAliasing = AntiAliasing.NONE) { + if (!postInfo || (postInfo && postInfo.antiAliasing !== antiAliasing)) { + postInfo = new PostInfo(antiAliasing); + } + const cameraID = getCameraUniqueID(camera); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + const postprocessPassRTName = `postprocessPassRTName${cameraID}`; + const postprocessPassDS = `postprocessPassDS${cameraID}`; + if (!ppl.containsResource(postprocessPassRTName)) { + ppl.addRenderTexture(postprocessPassRTName, Format.BGRA8, width, height, camera.window); + ppl.addDepthStencil(postprocessPassDS, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderWindow(postprocessPassRTName, camera.window); + ppl.updateDepthStencil(postprocessPassDS, width, height); + const postprocessPass = ppl.addRasterPass(width, height, 'post-process'); + postprocessPass.name = `CameraPostprocessPass${cameraID}`; + postprocessPass.setViewport(new Viewport(area.x, area.y, area.width, area.height)); + if (ppl.containsResource(inputTex)) { + const computeView = new ComputeView(); + computeView.name = 'outputResultMap'; + postprocessPass.addComputeView(inputTex, computeView); + } + const postClearColor = new Color(0, 0, 0, camera.clearColor.w); + if (camera.clearFlag & ClearFlagBit.COLOR) { + postClearColor.x = camera.clearColor.x; + postClearColor.y = camera.clearColor.y; + postClearColor.z = camera.clearColor.z; + } + const postprocessPassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET), + StoreOp.STORE, + camera.clearFlag, + postClearColor); + const postprocessPassDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearDepth, camera.clearStencil, 0, 0)); + postprocessPass.addRasterView(postprocessPassRTName, postprocessPassView); + postprocessPass.addRasterView(postprocessPassDS, postprocessPassDSView); + postprocessPass.addQueue(QueueHint.NONE).addFullscreenQuad( + postInfo.postMaterial, 0, SceneFlags.NONE, + ); + postprocessPass.addQueue(QueueHint.RENDER_TRANSPARENT).addSceneOfCamera(camera, new LightInfo(), + SceneFlags.UI); + if (profilerCamera === camera) { + postprocessPass.showStatistics = true; + } + return { rtName: postprocessPassRTName, dsName: postprocessPassDS }; +} + +export function buildForwardPass (camera, + ppl, + isOffScreen: boolean) { + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + const cameraInfo = buildShadowPasses(cameraName, camera, ppl); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + + const forwardPassRTName = `dsForwardPassColor${cameraName}`; + const forwardPassDSName = `dsForwardPassDS${cameraName}`; + if (!ppl.containsResource(forwardPassRTName)) { + if (!isOffScreen) { + ppl.addRenderTexture(forwardPassRTName, Format.BGRA8, width, height, camera.window); + } else { + ppl.addRenderTarget(forwardPassRTName, Format.RGBA16F, width, height, ResourceResidency.MANAGED); + } + ppl.addDepthStencil(forwardPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + if (!isOffScreen) { + ppl.updateRenderWindow(forwardPassRTName, camera.window); + ppl.updateDepthStencil(forwardPassDSName, width, height); + } else { + ppl.updateRenderTarget(forwardPassRTName, width, height); + ppl.updateDepthStencil(forwardPassDSName, width, height); + } + const forwardPass = ppl.addRasterPass(width, height, 'default'); + forwardPass.name = `CameraForwardPass${cameraID}`; + forwardPass.setViewport(new Viewport(area.x, area.y, width, height)); + for (const dirShadowName of cameraInfo.mainLightShadowNames) { + if (ppl.containsResource(dirShadowName)) { + const computeView = new ComputeView('cc_shadowMap'); + forwardPass.addComputeView(dirShadowName, computeView); + } + } + for (const spotShadowName of cameraInfo.spotLightShadowNames) { + if (ppl.containsResource(spotShadowName)) { + const computeView = new ComputeView('cc_spotShadowMap'); + forwardPass.addComputeView(spotShadowName, computeView); + } + } + const passView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + isOffScreen ? LoadOp.CLEAR : getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearColor.x, camera.clearColor.y, camera.clearColor.z, camera.clearColor.w)); + const passDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + isOffScreen ? LoadOp.CLEAR : getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearDepth, camera.clearStencil, 0, 0)); + forwardPass.addRasterView(forwardPassRTName, passView); + forwardPass.addRasterView(forwardPassDSName, passDSView); + forwardPass + .addQueue(QueueHint.RENDER_OPAQUE) + .addSceneOfCamera(camera, new LightInfo(), + SceneFlags.OPAQUE_OBJECT | SceneFlags.PLANAR_SHADOW | SceneFlags.CUTOUT_OBJECT + | SceneFlags.DEFAULT_LIGHTING | SceneFlags.DRAW_INSTANCING); + let sceneFlags = SceneFlags.TRANSPARENT_OBJECT | SceneFlags.GEOMETRY; + if (!isOffScreen) { + sceneFlags |= SceneFlags.UI; + forwardPass.showStatistics = true; + } + forwardPass + .addQueue(QueueHint.RENDER_TRANSPARENT) + .addSceneOfCamera(camera, new LightInfo(), sceneFlags); + return { rtName: forwardPassRTName, dsName: forwardPassDSName }; +} + +export function buildShadowPass (passName: Readonly, + ppl, + camera, light, level: number, + width: Readonly, height: Readonly) { + const fboW = width; + const fboH = height; + const area = getRenderArea(camera, width, height, light, level); + width = area.width; + height = area.height; + const device = ppl.device; + const shadowMapName = passName; + if (!ppl.containsResource(shadowMapName)) { + const format = supportsR32FloatTexture(device) ? Format.R32F : Format.RGBA8; + ppl.addRenderTarget(shadowMapName, format, fboW, fboH, ResourceResidency.MANAGED); + ppl.addDepthStencil(`${shadowMapName}Depth`, Format.DEPTH_STENCIL, fboW, fboH, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(shadowMapName, fboW, fboH); + ppl.updateDepthStencil(`${shadowMapName}Depth`, fboW, fboH); + const pass = ppl.addRasterPass(width, height, 'default'); + pass.name = passName; + pass.setViewport(new Viewport(area.x, area.y, area.width, area.height)); + pass.addRasterView(shadowMapName, new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + ClearFlagBit.COLOR, + new Color(1, 1, 1, camera.clearColor.w))); + pass.addRasterView(`${shadowMapName}Depth`, new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + LoadOp.CLEAR, StoreOp.DISCARD, + ClearFlagBit.DEPTH_STENCIL, + new Color(camera.clearDepth, camera.clearStencil, 0, 0))); + const queue = pass.addQueue(QueueHint.RENDER_OPAQUE); + queue.addSceneOfCamera(camera, new LightInfo(light, level), + SceneFlags.SHADOW_CASTER); +} + +export function buildReflectionProbePasss (camera, + ppl, + isOffScreen: boolean) { + const probes = cclegacy.internal.reflectionProbeManager.getProbes(); + if (probes.length === 0) { + return; + } + for (let i = 0; i < probes.length; i++) { + const probe = probes[i]; + if (probe.needRender) { + for (let faceIdx = 0; faceIdx < probe.bakedCubeTextures.length; faceIdx++) { + buildReflectionProbePass(camera, ppl, probe, probe.bakedCubeTextures[faceIdx].window!, faceIdx); + } + probe.needRender = false; + } + } +} + +export function buildReflectionProbePass (camera, + ppl, probe, renderWindow, faceIdx: number) { + const cameraName = `Camera${faceIdx}`; + const area = probe.renderArea(); + const width = area.x; + const height = area.y; + const probeCamera = probe.camera; + + const probePassRTName = `reflectionProbePassColor${cameraName}`; + const probePassDSName = `reflectionProbePassDS${cameraName}`; + + probe.updateCameraDir(faceIdx); + + if (!ppl.containsResource(probePassRTName)) { + ppl.addRenderTexture(probePassRTName, Format.RGBA8, width, height, renderWindow); + ppl.addDepthStencil(probePassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderWindow(probePassRTName, renderWindow); + ppl.updateDepthStencil(probePassDSName, width, height); + + const probePass = ppl.addRasterPass(width, height, 'default'); + probePass.name = `ReflectionProbePass${faceIdx}`; + probePass.setViewport(new Viewport(0, 0, width, height)); + + const passView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + getLoadOpOfClearFlag(probeCamera.clearFlag, AttachmentType.RENDER_TARGET), + StoreOp.STORE, + probeCamera.clearFlag, + new Color(probeCamera.clearColor.x, probeCamera.clearColor.y, probeCamera.clearColor.z, probeCamera.clearColor.w)); + const passDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + getLoadOpOfClearFlag(probeCamera.clearFlag, AttachmentType.DEPTH_STENCIL), + StoreOp.STORE, + probeCamera.clearFlag, + new Color(probeCamera.clearDepth, probeCamera.clearStencil, 0, 0)); + probePass.addRasterView(probePassRTName, passView); + probePass.addRasterView(probePassDSName, passDSView); + const passBuilder = probePass.addQueue(QueueHint.RENDER_OPAQUE); + passBuilder.addSceneOfCamera(camera, new LightInfo(), SceneFlags.REFLECTION_PROBE); + updateCameraUBO(passBuilder as unknown as any, probeCamera, ppl); +} + +class CameraInfo { + shadowEnabled = false; + mainLightShadowNames = new Array(); + spotLightShadowNames = new Array(); +} + +export function buildShadowPasses (cameraName: string, camera, ppl): CameraInfo { + validPunctualLightsCulling(ppl, camera); + const pipeline = ppl; + const shadowInfo = pipeline.pipelineSceneData.shadows; + const validPunctualLights = ppl.pipelineSceneData.validPunctualLights; + const cameraInfo = new CameraInfo(); + const shadows = ppl.pipelineSceneData.shadows; + if (!shadowInfo.enabled || shadowInfo.type !== ShadowType.ShadowMap) { return cameraInfo; } + cameraInfo.shadowEnabled = true; + const _validLights: any[] = []; + let n = 0; + let m = 0; + for (;n < shadowInfo.maxReceived && m < validPunctualLights.length;) { + const light = validPunctualLights[m]; + if (light.type === LightType.SPOT) { + const spotLight = light; + if (spotLight.shadowEnabled) { + _validLights.push(light); + n++; + } + } + m++; + } + + const { mainLight } = camera.scene!; + // build shadow map + const mapWidth = shadows.size.x; + const mapHeight = shadows.size.y; + if (mainLight && mainLight.shadowEnabled) { + cameraInfo.mainLightShadowNames[0] = `MainLightShadow${cameraName}`; + if (mainLight.shadowFixedArea) { + buildShadowPass(cameraInfo.mainLightShadowNames[0], ppl, + camera, mainLight, 0, mapWidth, mapHeight); + } else { + const csmLevel = pipeline.pipelineSceneData.csmSupported ? mainLight.csmLevel : 1; + for (let i = 0; i < csmLevel; i++) { + cameraInfo.mainLightShadowNames[i] = `MainLightShadow${cameraName}`; + buildShadowPass(cameraInfo.mainLightShadowNames[i], ppl, + camera, mainLight, i, mapWidth, mapHeight); + } + } + } + + for (let l = 0; l < _validLights.length; l++) { + const light = _validLights[l]; + const passName = `SpotLightShadow${l.toString()}${cameraName}`; + cameraInfo.spotLightShadowNames[l] = passName; + buildShadowPass(passName, ppl, + camera, light, 0, mapWidth, mapHeight); + } + return cameraInfo; +} + +export class GBufferInfo { + color!: string; + normal!: string; + emissive!: string; + ds!: string; +} +// deferred passes +export function buildGBufferPass (camera, + ppl) { + const cameraID = getCameraUniqueID(camera); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + const gBufferPassRTName = `gBufferPassColorCamera`; + const gBufferPassNormal = `gBufferPassNormal`; + const gBufferPassEmissive = `gBufferPassEmissive`; + const gBufferPassDSName = `gBufferPassDSCamera`; + if (!ppl.containsResource(gBufferPassRTName)) { + const colFormat = Format.RGBA16F; + ppl.addRenderTarget(gBufferPassRTName, colFormat, width, height, ResourceResidency.MANAGED); + ppl.addRenderTarget(gBufferPassNormal, colFormat, width, height, ResourceResidency.MANAGED); + ppl.addRenderTarget(gBufferPassEmissive, colFormat, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(gBufferPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(gBufferPassRTName, width, height); + ppl.updateRenderTarget(gBufferPassNormal, width, height); + ppl.updateRenderTarget(gBufferPassEmissive, width, height); + ppl.updateDepthStencil(gBufferPassDSName, width, height); + // gbuffer pass + const gBufferPass = ppl.addRasterPass(width, height, 'default'); + gBufferPass.name = `CameraGBufferPass${cameraID}`; + gBufferPass.setViewport(new Viewport(area.x, area.y, area.width, area.height)); + const rtColor = new Color(0, 0, 0, 0); + if (camera.clearFlag & ClearFlagBit.COLOR) { + if (ppl.pipelineSceneData.isHDR) { + SRGBToLinear(rtColor, camera.clearColor); + } else { + rtColor.x = camera.clearColor.x; + rtColor.y = camera.clearColor.y; + rtColor.z = camera.clearColor.z; + } + } + const passColorView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + rtColor); + const passNormalView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + new Color(0, 0, 0, 0)); + const passEmissiveView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + new Color(0, 0, 0, 0)); + const passDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearDepth, camera.clearStencil, 0, 0)); + gBufferPass.addRasterView(gBufferPassRTName, passColorView); + gBufferPass.addRasterView(gBufferPassNormal, passNormalView); + gBufferPass.addRasterView(gBufferPassEmissive, passEmissiveView); + gBufferPass.addRasterView(gBufferPassDSName, passDSView); + gBufferPass + .addQueue(QueueHint.RENDER_OPAQUE) + .addSceneOfCamera(camera, new LightInfo(), SceneFlags.OPAQUE_OBJECT | SceneFlags.CUTOUT_OBJECT); + const gBufferInfo = new GBufferInfo(); + gBufferInfo.color = gBufferPassRTName; + gBufferInfo.normal = gBufferPassNormal; + gBufferInfo.emissive = gBufferPassEmissive; + gBufferInfo.ds = gBufferPassDSName; + return gBufferInfo; +} + +class LightingInfo { + deferredLightingMaterial: Material; + private _init () { + this.deferredLightingMaterial = new Material(); + this.deferredLightingMaterial.name = 'builtin-deferred-material'; + this.deferredLightingMaterial.initialize({ + effectName: 'pipeline/deferred-lighting', + defines: { CC_RECEIVE_SHADOW: 1 }, + }); + for (let i = 0; i < this.deferredLightingMaterial.passes.length; ++i) { + this.deferredLightingMaterial.passes[i].tryCompile(); + } + } + constructor () { + this._init(); + } +} + +let lightingInfo: LightingInfo | null = null; + +// deferred lighting pass +export function buildLightingPass (camera, ppl, gBuffer: GBufferInfo) { + if (!lightingInfo) { + lightingInfo = new LightingInfo(); + } + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + const cameraInfo = buildShadowPasses(cameraName, camera, ppl); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + + const deferredLightingPassRTName = `deferredLightingPassRTName`; + const deferredLightingPassDS = `deferredLightingPassDS`; + if (!ppl.containsResource(deferredLightingPassRTName)) { + ppl.addRenderTarget(deferredLightingPassRTName, Format.RGBA8, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil(deferredLightingPassDS, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderTarget(deferredLightingPassRTName, width, height); + ppl.updateDepthStencil(deferredLightingPassDS, width, height); + // lighting pass + const lightingPass = ppl.addRasterPass(width, height, 'deferred-lighting'); + lightingPass.name = `CameraLightingPass${cameraID}`; + lightingPass.setViewport(new Viewport(area.x, area.y, width, height)); + for (const dirShadowName of cameraInfo.mainLightShadowNames) { + if (ppl.containsResource(dirShadowName)) { + const computeView = new ComputeView('cc_shadowMap'); + lightingPass.addComputeView(dirShadowName, computeView); + } + } + for (const spotShadowName of cameraInfo.spotLightShadowNames) { + if (ppl.containsResource(spotShadowName)) { + const computeView = new ComputeView('cc_spotShadowMap'); + lightingPass.addComputeView(spotShadowName, computeView); + } + } + if (ppl.containsResource(gBuffer.color)) { + const computeView = new ComputeView(); + computeView.name = 'gbuffer_albedoMap'; + lightingPass.addComputeView(gBuffer.color, computeView); + + const computeNormalView = new ComputeView(); + computeNormalView.name = 'gbuffer_normalMap'; + lightingPass.addComputeView(gBuffer.normal, computeNormalView); + + const computeEmissiveView = new ComputeView(); + computeEmissiveView.name = 'gbuffer_emissiveMap'; + lightingPass.addComputeView(gBuffer.emissive, computeEmissiveView); + + const computeDepthView = new ComputeView(); + computeDepthView.name = 'depth_stencil'; + lightingPass.addComputeView(gBuffer.ds, computeDepthView); + } + const lightingClearColor = new Color(0, 0, 0, 0); + if (camera.clearFlag & ClearFlagBit.COLOR) { + lightingClearColor.x = camera.clearColor.x; + lightingClearColor.y = camera.clearColor.y; + lightingClearColor.z = camera.clearColor.z; + } + lightingClearColor.w = 0; + const lightingPassView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + lightingClearColor); + lightingPass.addRasterView(deferredLightingPassRTName, lightingPassView); + lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, lightingInfo.deferredLightingMaterial, 0, + SceneFlags.VOLUMETRIC_LIGHTING, + ); + lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addSceneOfCamera(camera, new LightInfo(), + SceneFlags.TRANSPARENT_OBJECT | SceneFlags.PLANAR_SHADOW | SceneFlags.GEOMETRY); + return { rtName: deferredLightingPassRTName, dsName: deferredLightingPassDS }; +} + +function getClearFlags (attachment: rendering.AttachmentType, clearFlag, loadOp) { + switch (attachment) { + case AttachmentType.DEPTH_STENCIL: + if (loadOp === LoadOp.CLEAR) { + if (clearFlag & ClearFlagBit.DEPTH_STENCIL) { + return clearFlag; + } else { + return ClearFlagBit.DEPTH_STENCIL; + } + } else { + return ClearFlagBit.NONE; + } + case AttachmentType.RENDER_TARGET: + default: + if (loadOp === LoadOp.CLEAR) { + return ClearFlagBit.COLOR; + } else { + return ClearFlagBit.NONE; + } + } +} + +export function buildUIPass (camera, + ppl) { + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + + const dsUIAndProfilerPassRTName = `dsUIAndProfilerPassColor${cameraName}`; + const dsUIAndProfilerPassDSName = `dsUIAndProfilerPassDS${cameraName}`; + if (!ppl.containsResource(dsUIAndProfilerPassRTName)) { + ppl.addRenderTexture(dsUIAndProfilerPassRTName, Format.BGRA8, width, height, camera.window); + ppl.addDepthStencil(dsUIAndProfilerPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + ppl.updateRenderWindow(dsUIAndProfilerPassRTName, camera.window); + ppl.updateDepthStencil(dsUIAndProfilerPassDSName, width, height); + const uIAndProfilerPass = ppl.addRasterPass(width, height, 'default'); + uIAndProfilerPass.name = `CameraUIAndProfilerPass${cameraID}`; + uIAndProfilerPass.setViewport(new Viewport(area.x, area.y, width, height)); + const passView = new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearColor.x, camera.clearColor.y, camera.clearColor.z, camera.clearColor.w)); + const passDSView = new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL), + StoreOp.STORE, + camera.clearFlag, + new Color(camera.clearDepth, camera.clearStencil, 0, 0)); + uIAndProfilerPass.addRasterView(dsUIAndProfilerPassRTName, passView); + uIAndProfilerPass.addRasterView(dsUIAndProfilerPassDSName, passDSView); + const sceneFlags = SceneFlags.UI; + uIAndProfilerPass + .addQueue(QueueHint.RENDER_TRANSPARENT) + .addSceneOfCamera(camera, new LightInfo(), sceneFlags); + if (profilerCamera === camera) { + uIAndProfilerPass.showStatistics = true; + } +} + +export function buildNativeForwardPass (camera, ppl) { + const cameraID = getCameraUniqueID(camera); + const cameraName = `Camera${cameraID}`; + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + + // Resources + const forwardPassRTName = `dsForwardPassColor${cameraName}`; + const forwardPassDSName = `dsForwardPassDS${cameraName}`; + if (!ppl.containsResource(forwardPassRTName)) { + ppl.addRenderTexture(forwardPassRTName, Format.BGRA8, width, height, camera.window); + ppl.addDepthStencil(forwardPassDSName, Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + } + + ppl.updateRenderWindow(forwardPassRTName, camera.window); + ppl.updateDepthStencil(forwardPassDSName, width, height); + // Passes + const forwardPass = ppl.addRasterPass(width, height, 'default'); + forwardPass.name = `CameraForwardPass${cameraID}`; + forwardPass.setViewport(new Viewport(area.x, area.y, width, height)); + + const cameraRenderTargetLoadOp = getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.RENDER_TARGET); + const cameraDepthStencilLoadOp = getLoadOpOfClearFlag(camera.clearFlag, AttachmentType.DEPTH_STENCIL); + + forwardPass.addRasterView(forwardPassRTName, + new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + cameraRenderTargetLoadOp, + StoreOp.STORE, + getClearFlags(AttachmentType.RENDER_TARGET, camera.clearFlag, cameraRenderTargetLoadOp), + new Color(camera.clearColor.x, camera.clearColor.y, camera.clearColor.z, camera.clearColor.w))); + forwardPass.addRasterView(forwardPassDSName, + new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + cameraDepthStencilLoadOp, + StoreOp.STORE, + getClearFlags(AttachmentType.DEPTH_STENCIL, camera.clearFlag, cameraDepthStencilLoadOp), + new Color(camera.clearDepth, camera.clearStencil, 0, 0))); + + forwardPass + .addQueue(QueueHint.RENDER_OPAQUE) + .addSceneOfCamera(camera, new LightInfo(), + SceneFlags.OPAQUE_OBJECT + | SceneFlags.PLANAR_SHADOW + | SceneFlags.CUTOUT_OBJECT + | SceneFlags.DEFAULT_LIGHTING + | SceneFlags.DRAW_INSTANCING); + forwardPass + .addQueue(QueueHint.RENDER_TRANSPARENT) + .addSceneOfCamera(camera, new LightInfo(), + SceneFlags.TRANSPARENT_OBJECT + | SceneFlags.GEOMETRY); + forwardPass + .addQueue(QueueHint.RENDER_TRANSPARENT) + .addSceneOfCamera(camera, new LightInfo(), + SceneFlags.UI); + forwardPass.showStatistics = true; +} + +export function buildNativeDeferredPipeline (camera, ppl) { + const cameraID = getCameraUniqueID(camera); + const area = getRenderArea(camera, camera.window.width, camera.window.height); + const width = area.width; + const height = area.height; + if (!ppl.containsResource('Albedo')) { + // GBuffers + ppl.addRenderTarget('Albedo', Format.RGBA16F, width, height, ResourceResidency.MANAGED); + ppl.addRenderTarget('Normal', Format.RGBA16F, width, height, ResourceResidency.MANAGED); + ppl.addRenderTarget('Emissive', Format.RGBA16F, width, height, ResourceResidency.MANAGED); + ppl.addDepthStencil('DepthStencil', Format.DEPTH_STENCIL, width, height, ResourceResidency.MANAGED); + // Lighting + ppl.addRenderTexture('Color', Format.BGRA8, width, height, camera.window); + } + if (!lightingInfo) { + lightingInfo = new LightingInfo(); + } + // GeometryPass + { + const gBufferPass = ppl.addRasterPass(width, height, 'default'); + gBufferPass.name = 'GeometryPass'; + gBufferPass.setViewport(new Viewport(area.x, area.y, area.width, area.height)); + + gBufferPass.addRasterView('Albedo', new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, ClearFlagBit.COLOR)); + gBufferPass.addRasterView('Normal', new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, ClearFlagBit.COLOR)); + gBufferPass.addRasterView('Emissive', new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, ClearFlagBit.COLOR)); + gBufferPass.addRasterView('DepthStencil', new RasterView('_', + AccessType.WRITE, AttachmentType.DEPTH_STENCIL, + LoadOp.CLEAR, StoreOp.STORE, + ClearFlagBit.DEPTH_STENCIL, + new Color(1, 0, 0, 0))); + gBufferPass + .addQueue(QueueHint.RENDER_OPAQUE) + .addSceneOfCamera(camera, new LightInfo(), SceneFlags.OPAQUE_OBJECT | SceneFlags.CUTOUT_OBJECT); + } + // LightingPass + { + const lightingPass = ppl.addRasterPass(width, height, 'default'); + lightingPass.name = 'LightingPass'; + lightingPass.setViewport(new Viewport(area.x, area.y, width, height)); + + const lightingClearColor = new Color(0, 0, 0, 0); + if (camera.clearFlag & ClearFlagBit.COLOR) { + lightingClearColor.x = camera.clearColor.x; + lightingClearColor.y = camera.clearColor.y; + lightingClearColor.z = camera.clearColor.z; + } + lightingClearColor.w = 1; + lightingPass.addRasterView('Color', new RasterView('_', + AccessType.WRITE, AttachmentType.RENDER_TARGET, + LoadOp.CLEAR, StoreOp.STORE, + camera.clearFlag, + lightingClearColor)); + + lightingPass.addComputeView('Albedo', new ComputeView('gbuffer_albedoMap')); + lightingPass.addComputeView('Normal', new ComputeView('gbuffer_normalMap')); + lightingPass.addComputeView('Emissive', new ComputeView('gbuffer_emissiveMap')); + lightingPass.addComputeView('DepthStencil', new ComputeView('depth_stencil')); + + lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addCameraQuad( + camera, lightingInfo.deferredLightingMaterial, 0, + SceneFlags.VOLUMETRIC_LIGHTING, + ); + lightingPass.addQueue(QueueHint.RENDER_TRANSPARENT).addSceneOfCamera(camera, + new LightInfo(), + SceneFlags.TRANSPARENT_OBJECT | SceneFlags.PLANAR_SHADOW | SceneFlags.GEOMETRY); + } +} + +export function updateCameraUBO (setter: any, camera, ppl) { + const pipeline = cclegacy.director.root.pipeline; + const sceneData = ppl.pipelineSceneData; + const skybox = sceneData.skybox; + setter.addConstant('CCCamera'); + setter.setMat4('cc_matView', camera.matView); + setter.setMat4('cc_matViewInv', camera.node.worldMatrix); + setter.setMat4('cc_matProj', camera.matProj); + setter.setMat4('cc_matProjInv', camera.matProjInv); + setter.setMat4('cc_matViewProj', camera.matViewProj); + setter.setMat4('cc_matViewProjInv', camera.matViewProjInv); + setter.setVec4('cc_cameraPos', new Vec4(camera.position.x, camera.position.y, camera.position.z, pipeline.getCombineSignY())); + // eslint-disable-next-line max-len + setter.setVec4('cc_surfaceTransform', new Vec4(camera.surfaceTransform, 0.0, Math.cos(toRadian(skybox.getRotationAngle())), Math.sin(toRadian(skybox.getRotationAngle())))); + // eslint-disable-next-line max-len + setter.setVec4('cc_screenScale', new Vec4(sceneData.shadingScale, sceneData.shadingScale, 1.0 / sceneData.shadingScale, 1.0 / sceneData.shadingScale)); + setter.setVec4('cc_exposure', new Vec4(camera.exposure, 1.0 / camera.exposure, sceneData.isHDR ? 1.0 : 0.0, 1.0 / Camera.standardExposureValue)); +} + diff --git a/versions/4.0/zh/render-pipeline/custom-pipeline.md b/versions/4.0/zh/render-pipeline/custom-pipeline.md new file mode 100644 index 0000000000..f3f54affb0 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/custom-pipeline.md @@ -0,0 +1,317 @@ +# 可定制渲染管线(Deprecated) + +本文档适用于 v3.8.3 及以下,使用可定制渲染管线的用户。 v3.8.4 及以上用户,请参考文档:[编写渲染管线](./write-render-pipeline.md)。 + +Cocos Creator 3.8 中正式开放了新的 **可定制渲染管线( CRP - Customizable Render Pipeline )接口**。 + +**可定制渲染管线接口** 位于 `cocos/core/pipeline/custom/pipeline.ts` + +**内置渲染管线** 的实现位于 `editor/assets/default_renderpipeline/builtin-pipeline.ts` + +下文中,我们以**新管线**作为上述两者的概括,以便于统一描述。 + +## 介绍 + +对于游戏引擎来说,渲染是最重要的功能之一。而引擎的渲染效果,由 **渲染管线**(RenderPipeline)决定,取决于不同的艺术风格、运行平台、硬件设备、渲染技术等等。 + +如何在引擎中实现各种各样的画面表现,是一个非常复杂的问题。 + +这需要引擎提供足够的灵活性,让用户可以自由定制渲染管线,以实现各种各样的效果。 + +Cocos Creator 的 **可定制渲染管线** 能够在不修改引擎源码的情况下,在不同的平台、不同的硬件设备上,编写最优的渲染管线,以达到最佳的画面表现。 + +也能够在不同的平台、不同的硬件设备上,为 2D/3D 项目编写专用的渲染管线,以达到最佳的性能表现以及跨平台性。 + +## 启用自定义管线 + +勾选 **可定制渲染管线**。 + +![cp-feature-enable](./image/cp-feature-enable.png) + +通过填写 **自定义管线** 的名字,选择注册好的 **可定制渲染管线**。 + +目前支持 **前向渲染管线**(名字为 Custom 或 Forward)和 **后向渲染管线**(名字为 Deferred)两种。 + +![cp-pipeline-selection](./image/cp-pipeline-selection.png) + +### 编写可定制渲染管线 + +新建 Typescript 文件,编写一个类,例如 `MyPipeline`,让该类实现 `rendering.PipelineBuilder` 接口,通过 `rendering.setCustomPipeline` 方法把该 pipeline 注册到系统中。 + +## 概念 + +可定制渲染管线以数据流(Dataflow)的形式概括了渲染的整个流程,用**渲染图**(RenderGraph)描述。 + +**渲染图**由不同的渲染节点组成,比如**渲染通道**(RenderPass),**计算通道**(ComputePass)等。每个渲染通道有数据输入与输出,比如**渲染目标**(RenderTarget)、**深度模板**(DepthStencil)、**贴图**(Texture)等。这些输入与输出会在计算节点间构成链接关系,形成数据流。 + +在**渲染通道**、**计算通道**节点下,可以有**渲染队列**(RenderQueue)子节点,用于控制渲染的顺序。**渲染队列**节点下,可以有**渲染内容**(RenderContent)子节点,用于控制渲染的内容。 + +**渲染内容** 可以是 **场景**、屏幕 **矩形**,也可以是计算任务的 **分发**(Dispatch),不同的通道支持不同的渲染内容。 + +**可定制渲染管线** 的【**渲染通道**、**渲染队列**、**渲染内容**】构成一个森林: + +![cp-render-graph-1](./image/cp-render-graph-1.png) + +**可定制渲染管线**的【**渲染通道**、**渲染资源**】构成一个有向无圈图(DAG): + +![cp-render-graph-2](./image/cp-render-graph-2.png) + +我们可以层叠(Stack)以上两张图,得到 **渲染流程图**(RenderGraph)。**渲染流程图** 描述了 **可定制渲染管线** 的全部流程,引擎会按照用户定制的流程图进行资源分配、流程优化、渲染执行。 + +## 渲染管线类型 + +引擎的渲染管线根据硬件能力,分为了两种: + +- 基础渲染管线 +- 标准渲染管线 + +### 基础渲染管线(BasicPipeline) + +基础渲染管线提供跨平台的基础渲染功能,适用一切平台。 + +接口主要分为两类 + +- 注册资源。在管线中注册的资源,可以在渲染通道中使用,资源读写状态由管线托管。 + + ```typescript + interface BasicPipeline { + addRenderWindow (name: string, format: Format, width: number, height: number, + renderWindow: RenderWindow): number; + addRenderTarget (name: string, format: Format, width: number, height: number, + residency?: ResourceResidency): number; + addDepthStencil (name: string, format: Format, width: number, height: number, + residency?: ResourceResidency): number; + } + ``` + +- 添加渲染通道 + + ```typescript + interface BasicPipeline { + addRenderPass (width: number, height: number, passName?: string): BasicRenderPassBuilder; + } + ``` + + **渲染通道** 代表了一次光栅化过程,将物件渲染至**渲染目标**以及**深度模板缓冲**。 + +可以通过`BasicRenderPassBuilder`构建。 + +```typescript +interface BasicRenderPassBuilder extends Setter { + addRenderTarget (name: string, loadOp?: LoadOp, storeOp?: StoreOp, color?: Color): void; + addDepthStencil (name: string, loadOp?: LoadOp, storeOp?: StoreOp, + depth?: number, stencil?: number, clearFlags?: ClearFlagBit): void; + addTexture (name: string, slotName: string, sampler?: Sampler | null, plane?: number): void; + addQueue (hint?: QueueHint, phaseName?: string): RenderQueueBuilder; +} +``` + +### 标准渲染管线(Pipeline) + +标准渲染具备更丰富的管线功能,目前支持GLES3、Vulkan、Metal三个后端。 + +标准渲染管线在基础渲染管线的基础上,提供了**计算通道**(ComputePass),**渲染子通道**(RenderSubpass)等功能。 +并且可以使用 `StorageBuffer`、`StorageTexture` 等资源。 + +```typescript +export interface Pipeline extends BasicPipeline { + addStorageBuffer (name: string, format: Format, size: number, + residency?: ResourceResidency): number; + addStorageTexture (name: string, format: Format, width: number, height: number, + residency?: ResourceResidency): number; + addRenderPass (width: number, height: number, passName: string): RenderPassBuilder; + addComputePass (passName: string): ComputePassBuilder; + addUploadPass (uploadPairs: UploadPair[]): void; + addMovePass (movePairs: MovePair[]): void; +} +export interface RenderPassBuilder extends BasicRenderPassBuilder { + addStorageBuffer (name: string, accessType: AccessType, slotName: string): void; + addStorageImage (name: string, accessType: AccessType, slotName: string): void; + addRenderSubpass (subpassName: string): RenderSubpassBuilder; +} +export interface ComputePassBuilder extends Setter { + addTexture (name: string, slotName: string, sampler?: Sampler | null, plane?: number): void; + addStorageBuffer (name: string, accessType: AccessType, slotName: string): void; + addStorageImage (name: string, accessType: AccessType, slotName: string): void; + addQueue (phaseName?: string): ComputeQueueBuilder; +} +``` + +根据不同的平台,用户可以针对性构建不同的渲染管线。 + +比如在移动平台上,用户可以通过**渲染子通道**(RenderSubpass)利用芯片上的高速缓存,减少内存读写来降低发热。 + +在桌面平台上,用户则可以使用**计算通道**(ComputePass)编写复杂的图形算法。充分利用平台特性。 + +### 渲染子通道 RenderSubpass (实验性质) + +渲染子通道表示渲染的一个阶段,该阶段读取和写入渲染通道中的一部分附件(Attachment)。 + +渲染命令(Render commands)被记录到渲染通道实例的特定子通道中。 + +```typescript +export interface RenderSubpassBuilder extends Setter { + addRenderTarget ( + name: string, + accessType: AccessType, + slotName?: string, + loadOp?: LoadOp, + storeOp?: StoreOp, + color?: Color): void; + addDepthStencil ( + name: string, + accessType: AccessType, + depthSlotName?: string, + stencilSlotName?: string, + loadOp?: LoadOp, + storeOp?: StoreOp, + depth?: number, + stencil?: number, + clearFlags?: ClearFlagBit): void; + addTexture (name: string, slotName: string, sampler?: Sampler | null, plane?: number): void; + addStorageBuffer (name: string, accessType: AccessType, slotName: string): void; + addStorageImage (name: string, accessType: AccessType, slotName: string): void; + addQueue (hint?: QueueHint, phaseName?: string): RenderQueueBuilder; +} +``` + +渲染子通道支持输入附件(Input Attachment),可以通过 `slotName`、`depthSlotName`、`stencilSlotName` 指定,这个名字需要与 effect 中的注册的输入附件名字一致。 + +```glsl +// .effect +#pragma subpass +#pragma subpassColor in albedoMap +#pragma subpassColor in normalMap +#pragma subpassColor in emissiveMap +#pragma subpassDepth in depthBuffer +#pragma isubpassStencil in stencilBuffer // isubpass 的 i 表示输入附件类型为 int + +void main () { + vec4 albedo = subpassLoad(albedoMap); + vec4 normal = subpassLoad(normalMap); + vec4 emissive = subpassLoad(emissiveMap); + float depth = subpassLoad(depthBuffer).x; + int stencil = subpassLoad(stencilBuffer).x; + ... +} +``` + +### 计算通道(ComputePass) + +计算通道是对一次计算任务的抽象,可以调用Compute Shader,执行计算任务。 + +```typescript +export interface ComputePassBuilder extends Setter { + addTexture (name: string, slotName: string, sampler?: Sampler | null, plane?: number): void; + addStorageBuffer (name: string, accessType: AccessType, slotName: string): void; + addStorageImage (name: string, accessType: AccessType, slotName: string): void; + addQueue (phaseName?: string): ComputeQueueBuilder; +} + +export interface ComputeQueueBuilder extends Setter { + addDispatch ( + threadGroupCountX: number, + threadGroupCountY: number, + threadGroupCountZ: number, + material?: Material, + passID?: number): void; +} +``` + +更多内容见[计算着色器](../shader/compute-shader.md)。 + +## 渲染队列(RenderQueue) + +**渲染队列** 是 **渲染通道/子通道** 的子节点,有严格的渲染先后顺序。只有一个 **渲染队列** 的内容完全绘制后,才会绘制下一个 **渲染队列** 的内容。 + +可以通过 `RenderQueueBuilder` 添加绘制内容,**渲染队列** 内对象的渲染顺序是未定义的,可能是任何顺序。 + +```typescript +export interface RenderQueueBuilder extends Setter { + addScene (camera: Camera, sceneFlags: SceneFlags): void; + addFullscreenQuad (material: Material, passID: number, sceneFlags?: SceneFlags): void; +} +``` + +可以通过 `SceneFlags` 标记场景渲染特性,比如渲染不透明物件、渲染投影物件等。 + +```typescript +export enum SceneFlags { + NONE = 0, + OPAQUE = 0x1, + MASK = 0x2, + BLEND = 0x4, + SHADOW_CASTER = 0x8, + UI = 0x10, +} +``` + +## 渲染数据设置 + +我们可以通过 `Setter` 设置 Shader 里用到的数据和只读资源,名字是 Shader 里的变量名。 + +注意这里的 Shader 是管线相关的,而不是普通表面材质的 Shader。 + +```typescript +export interface Setter extends RenderNode { + setMat4 (name: string, mat: Mat4): void; + setQuaternion (name: string, quat: Quat): void; + setColor (name: string, color: Color): void; + setVec4 (name: string, vec: Vec4): void; + setVec2 (name: string, vec: Vec2): void; + setFloat (name: string, v: number): void; + setArrayBuffer (name: string, arrayBuffer: ArrayBuffer): void; + setBuffer (name: string, buffer: Buffer): void; + setTexture (name: string, texture: Texture): void; + setSampler (name: string, sampler: Sampler): void; +} +``` + +这里用到的数据和资源是管线相关的。材质相关的,需要设置到材质上,不应重复设置。 + +资源必须是只读的。如果需要读写数据,需要注册到管线中,由管线进行管理。 + +### 数据更新频率 + +Effect中,不同的变量有不同的更新频率。由低到高大致分为: + +- `pass` +- `phase` +- `batch` +- `instance` + +effect中需要在变量声明前加上`#pragma rate`指定更新频率。 + +- `batch`为缺省值 +- `instance`暂不支持自定义 + +例子: + +```glsl +// copy-pass.effect + +precision highp float; +in vec2 v_uv; + +#pragma rate outputResultMap pass +uniform sampler2D outputResultMap; + +layout(location = 0) out vec4 fragColor; + +void main () { + fragColor = texture(outputResultMap, v_uv); +} + +``` + +### 节点类型 + +RenderGraph中的每个节点描述符集的更新频率,由节点的类型决定。 + +| 节点类型 | 更新频率 | +| --- | --- | +| 渲染通道 | pass | +| 渲染子通道 | pass | +| 计算通道 | pass | +| 渲染队列 | phase | +| 计算队列 | phase | diff --git a/versions/4.0/zh/render-pipeline/image/BloomEnable.png b/versions/4.0/zh/render-pipeline/image/BloomEnable.png new file mode 100644 index 0000000000..65eef4b3d7 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/BloomEnable.png differ diff --git a/versions/4.0/zh/render-pipeline/image/DeferredPipeline.png b/versions/4.0/zh/render-pipeline/image/DeferredPipeline.png new file mode 100644 index 0000000000..6cf897e2a2 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/DeferredPipeline.png differ diff --git a/versions/4.0/zh/render-pipeline/image/builtin-bloom.png b/versions/4.0/zh/render-pipeline/image/builtin-bloom.png new file mode 100644 index 0000000000..6420e0ce4a Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/builtin-bloom.png differ diff --git a/versions/4.0/zh/render-pipeline/image/builtin-editor-preview.png b/versions/4.0/zh/render-pipeline/image/builtin-editor-preview.png new file mode 100644 index 0000000000..e1f8e67034 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/builtin-editor-preview.png differ diff --git a/versions/4.0/zh/render-pipeline/image/builtin-enable.png b/versions/4.0/zh/render-pipeline/image/builtin-enable.png new file mode 100644 index 0000000000..eaeb9ab500 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/builtin-enable.png differ diff --git a/versions/4.0/zh/render-pipeline/image/builtin-fxaa.png b/versions/4.0/zh/render-pipeline/image/builtin-fxaa.png new file mode 100644 index 0000000000..ea4f178cd7 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/builtin-fxaa.png differ diff --git a/versions/4.0/zh/render-pipeline/image/builtin-pipeline-settings.png b/versions/4.0/zh/render-pipeline/image/builtin-pipeline-settings.png new file mode 100644 index 0000000000..ae8b221fe6 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/builtin-pipeline-settings.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-add-compute-queue.png b/versions/4.0/zh/render-pipeline/image/cp-add-compute-queue.png new file mode 100644 index 0000000000..fd2b0cad68 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-add-compute-queue.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-add-compute-view.png b/versions/4.0/zh/render-pipeline/image/cp-add-compute-view.png new file mode 100644 index 0000000000..bfaacb51ac Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-add-compute-view.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-add-raster-queue.png b/versions/4.0/zh/render-pipeline/image/cp-add-raster-queue.png new file mode 100644 index 0000000000..9b5cff64db Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-add-raster-queue.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-add-raster-view.png b/versions/4.0/zh/render-pipeline/image/cp-add-raster-view.png new file mode 100644 index 0000000000..94f59aef02 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-add-raster-view.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-compute-pass.png b/versions/4.0/zh/render-pipeline/image/cp-compute-pass.png new file mode 100644 index 0000000000..80be0d197d Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-compute-pass.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-compute-queue.png b/versions/4.0/zh/render-pipeline/image/cp-compute-queue.png new file mode 100644 index 0000000000..867419a1f4 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-compute-queue.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-compute-view.png b/versions/4.0/zh/render-pipeline/image/cp-compute-view.png new file mode 100644 index 0000000000..a4813e3991 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-compute-view.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-copy-pass.png b/versions/4.0/zh/render-pipeline/image/cp-copy-pass.png new file mode 100644 index 0000000000..8f36166070 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-copy-pass.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-data-structure.png b/versions/4.0/zh/render-pipeline/image/cp-data-structure.png new file mode 100644 index 0000000000..761e6f41fd Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-data-structure.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-dispatch.png b/versions/4.0/zh/render-pipeline/image/cp-dispatch.png new file mode 100644 index 0000000000..867419a1f4 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-dispatch.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-feature-enable.png b/versions/4.0/zh/render-pipeline/image/cp-feature-enable.png new file mode 100644 index 0000000000..309e343598 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-feature-enable.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-move-pass.png b/versions/4.0/zh/render-pipeline/image/cp-move-pass.png new file mode 100644 index 0000000000..26af0d7d78 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-move-pass.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-pipeline-edit.png b/versions/4.0/zh/render-pipeline/image/cp-pipeline-edit.png new file mode 100644 index 0000000000..976dad4ed7 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-pipeline-edit.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-pipeline-selection.png b/versions/4.0/zh/render-pipeline/image/cp-pipeline-selection.png new file mode 100644 index 0000000000..a667841d73 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-pipeline-selection.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-quad.png b/versions/4.0/zh/render-pipeline/image/cp-quad.png new file mode 100644 index 0000000000..03b0b17edf Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-quad.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-raster-pass.png b/versions/4.0/zh/render-pipeline/image/cp-raster-pass.png new file mode 100644 index 0000000000..32a445bb4a Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-raster-pass.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-raster-queue.png b/versions/4.0/zh/render-pipeline/image/cp-raster-queue.png new file mode 100644 index 0000000000..8edb470e74 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-raster-queue.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-raster-view.png b/versions/4.0/zh/render-pipeline/image/cp-raster-view.png new file mode 100644 index 0000000000..472c2766a0 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-raster-view.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-render-graph-1.png b/versions/4.0/zh/render-pipeline/image/cp-render-graph-1.png new file mode 100644 index 0000000000..68a41eeac2 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-render-graph-1.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-render-graph-2.png b/versions/4.0/zh/render-pipeline/image/cp-render-graph-2.png new file mode 100644 index 0000000000..f55bbeea85 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-render-graph-2.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-render-queue.png b/versions/4.0/zh/render-pipeline/image/cp-render-queue.png new file mode 100644 index 0000000000..9b5cff64db Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-render-queue.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-scene.png b/versions/4.0/zh/render-pipeline/image/cp-scene.png new file mode 100644 index 0000000000..ab6e84b6f9 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-scene.png differ diff --git a/versions/4.0/zh/render-pipeline/image/cp-setter.png b/versions/4.0/zh/render-pipeline/image/cp-setter.png new file mode 100644 index 0000000000..31f33f1641 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/cp-setter.png differ diff --git a/versions/4.0/zh/render-pipeline/image/customPipelineBloom.png b/versions/4.0/zh/render-pipeline/image/customPipelineBloom.png new file mode 100644 index 0000000000..8e2c325ebb Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/customPipelineBloom.png differ diff --git a/versions/4.0/zh/render-pipeline/image/deferred-pipeline.png b/versions/4.0/zh/render-pipeline/image/deferred-pipeline.png new file mode 100644 index 0000000000..b1ecc434a5 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/deferred-pipeline.png differ diff --git a/versions/4.0/zh/render-pipeline/image/effect.png b/versions/4.0/zh/render-pipeline/image/effect.png new file mode 100644 index 0000000000..f43a02d8b5 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/effect.png differ diff --git a/versions/4.0/zh/render-pipeline/image/forward-pipeline.png b/versions/4.0/zh/render-pipeline/image/forward-pipeline.png new file mode 100644 index 0000000000..9576379c51 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/forward-pipeline.png differ diff --git a/versions/4.0/zh/render-pipeline/image/lut_cocos.jpg b/versions/4.0/zh/render-pipeline/image/lut_cocos.jpg new file mode 100644 index 0000000000..acef07938f Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/lut_cocos.jpg differ diff --git a/versions/4.0/zh/render-pipeline/image/postprocessOutput.png b/versions/4.0/zh/render-pipeline/image/postprocessOutput.png new file mode 100644 index 0000000000..2d689ecf3b Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/postprocessOutput.png differ diff --git a/versions/4.0/zh/render-pipeline/image/postprocessPass.png b/versions/4.0/zh/render-pipeline/image/postprocessPass.png new file mode 100644 index 0000000000..f3506bf816 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/postprocessPass.png differ diff --git a/versions/4.0/zh/render-pipeline/image/setting.png b/versions/4.0/zh/render-pipeline/image/setting.png new file mode 100644 index 0000000000..d911881719 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/setting.png differ diff --git a/versions/4.0/zh/render-pipeline/image/testCustomPipeline.png b/versions/4.0/zh/render-pipeline/image/testCustomPipeline.png new file mode 100644 index 0000000000..7f34fb9935 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/image/testCustomPipeline.png differ diff --git a/versions/4.0/zh/render-pipeline/overview.md b/versions/4.0/zh/render-pipeline/overview.md new file mode 100644 index 0000000000..be61515058 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/overview.md @@ -0,0 +1,44 @@ +# 渲染管线概述 + +![lut_cocos](./image/lut_cocos.jpg) + +RenderPipeline 用于控制场景的渲染流程,包括光照管理、物体剔除、渲染物体排序、渲染目标切换等。由于每个阶段对于不同项目来说可以有不同的优化处理方式,所以用统一的方法来处理不同类型项目的渲染流程很难达到最优化的结果。可定制化的渲染管线用于对渲染场景中的每个阶段进行更灵活的控制,可以针对不同的项目做更深层次的优化方案。 + +Cocos Creator 3.8.4 之后,拥有两套渲染管线:新渲染管线、旧渲染管线。 + +## 新渲染管线 + +Cocos Creator 3.8.4 推出了全新的可定制渲染管线,CRP - Customizable Render Pipeline。 + +全新的可定制渲染管线主要有以下几个好处: + +1. Cocos Creator 3.8.4 版本及以上,引擎内置管线基于 CRP 管线构建,使引擎内置管线和用户自定义管线拥有相同的渲染机制和流程,兼容性和稳定性更强。 +2. 基于 CRP 管线,开发者能够在不修改引擎源码的情况下编写全平台兼容的渲染流程。 +3. 基于 CRP 管线,开发者能够根据项目的需求定制渲染流程,删除不必要渲染过程,节省开销提升性能。 +4. 基于 CRP 管线中的 RenderGraph 渲染架构,开发者可以很方便地复用、增加渲染流程,实现项目所需的高级渲染效果和后期效果。 + +相关文档如下: + +- [使用内置渲染管线](./use-builtin-pipeline.md) +- [使用后期效果](./use-post-process.md) +- [编写渲染管线(进阶)](./write-render-pipeline.md) + +## 旧渲染管线 + +旧渲染管线是从 Cocos Creator 3.0 开始,基于传统渲染架构构建的一套渲染管线。 + +内置了前向渲染、延迟渲染。不支持自定义渲染流程和后期效果。 + +旧渲染管线会在下一个大版本移除,推荐大家在新项目中使用新渲染管线。 + +相关文档往下: + +- [内置渲染管线-旧](./builtin-pipeline.md) +- [自定义渲染管线-旧](./custom-pipeline.md) +- [后期效果-旧](./post-process.md) + +## 新旧渲染管线的兼容机制 + +1. Cocos Creator 3.8.4+ 新建项目默认管线为新管线 +2. 旧项目如果未使用自定义管线,则在升级后使用原渲染管线。 +3. 旧项目如果使用了自定义管线,则在升级后使用新渲染管线。 diff --git a/versions/4.0/zh/render-pipeline/post-process.md b/versions/4.0/zh/render-pipeline/post-process.md new file mode 100644 index 0000000000..a8af062f23 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/post-process.md @@ -0,0 +1,5 @@ +# 后期效果 + +v3.8.3 及以下版本用户,请参考文档:[全屏特效后处理流程](./post-process/index.md)。 + +v3.8.4 及以下版本用户,请参考文档:[使用后期效果](./use-post-process.md)。 diff --git a/versions/4.0/zh/render-pipeline/post-process/blit-screen.md b/versions/4.0/zh/render-pipeline/post-process/blit-screen.md new file mode 100644 index 0000000000..35ae5bfadc --- /dev/null +++ b/versions/4.0/zh/render-pipeline/post-process/blit-screen.md @@ -0,0 +1,22 @@ +# BlitScreen 组件 + +BlitScreen 组件允许用户定义不同的材质来管理 [自定义后效](./custom.md)。 + +## 属性 + +![blit-screen.png](./img/blit-screen.png) + +| 属性 | 说明 | +| :-- | :-- | +| **Materials** | 后效材质的数组 | + +分配 **Materials** 属性的数组长度,可以添加不同的后效材质。添加后属性如下: + +![blit-screen-material.png](./img/blit-screen-material.png) + +| 属性 | 说明 | +| :-- | :-- | +| **Material** | 后效材质,从 **资源管理器** 中拖拽自定义好的材质到该属性。 | +| **Enable** | 该后效材质是否被启用 | + +后效材质会根据添加到数组内的顺序执行。 diff --git a/versions/4.0/zh/render-pipeline/post-process/custom.md b/versions/4.0/zh/render-pipeline/post-process/custom.md new file mode 100644 index 0000000000..93e58158bd --- /dev/null +++ b/versions/4.0/zh/render-pipeline/post-process/custom.md @@ -0,0 +1,135 @@ +# 自定义后效(不再维护) + +自定义后效有两种方式,简单的后效可以直接将后效材质添加到 [Blit-Screen 后效组件](./blit-screen.md) 上,复杂的后效需要自定义一个后效 pass 。 + +## Blit-Screen 后效组件 + +参考 [设置后效流程](index.md) 添加 [Blit-Screen 后效组件](./blit-screen.md), +将自定义后效材质拖入 **Material** 属性中,Blit-Screen 会按照 **Materials** 数组顺序依次渲染后效材质。 + +**Materials** 属性中每个自定义后效材质都支持单独开关,方便开发者管理。 + +![BlitScreen](img/custom-1.png) + +![Dot Effect](img/custom-2.png) + +具体可以参考 [cocos-example-render-pipeline](https://github.com/cocos/cocos-example-render-pipeline/blob/main/assets/cases/post-process/post-process.scene) + +## 自定义后效 Pass + +如果需要自定义更加复杂的后效流程,你可以创建自定义后效 Pass 。 + +![custom-pass-1](img/custom-pass-1.png) +![custom-pass-2](img/custom-pass-2.png) + +1. 定义 PostProcessSetting 组件,传递参数到 CustomPass 中 + + ```js + import { _decorator, Material, postProcess } from 'cc'; + const { ccclass, property, menu } = _decorator; + + @ccclass('CustomPostProcess') + @menu('PostProcess/CustomPostProcess') + export class CustomPostProcess extends postProcess.PostProcessSetting { + @property + blueIntensity = 1 + + @property + showDepth = false + + @property + depthRange = 30 + + @property(Material) + _material: Material | undefined + + @property(Material) + get material () { + return this._material; + } + set material (v) { + this._material = v; + } + } + ``` + +2. 定义 CustomPass + + ```js + import { Vec4, gfx, postProcess, renderer, rendering } from "cc"; + import { CustomPostProcess } from "./CustomPostProcess"; + + export class CustomPass extends postProcess.SettingPass { + // custom pass name + name = 'CustomPass' + + // out out slot name + outputNames: string[] = ['CustomPassColor'] + + // reference to post process setting + get setting () { return this.getSetting(CustomPostProcess); } + + // Whether the pass should rendered + checkEnable(camera: renderer.scene.Camera): boolean { + let setting = this.setting; + return setting.material && super.checkEnable(camera); + } + + params = new Vec4 + + render (camera: renderer.scene.Camera, ppl: rendering.Pipeline) { + const cameraID = this.getCameraUniqueID(camera); + + // clear background to black color + let context = this.context; + context.clearBlack() + + // input name from last pass's output slot 0 + let input0 = this.lastPass.slotName(camera, 0); + // output slot 0 name + let output = this.slotName(camera, 0); + + // get depth slot name + let depth = context.depthSlotName; + + // also can get depth slot name from forward pass. + // let forwardPass = builder.getPass(ForwardPass); + // depth = forwardPass.slotName(camera, 1); + + // set setting value to material + let setting = this.setting; + this.params.x = setting.blueIntensity + this.params.y = setting.showDepth ? 1 : 0; + this.params.z = setting.depthRange; + setting.material.setProperty('params', this.params); + + context.material = setting.material; + context + // update view port + .updatePassViewPort() + // add a render pass + .addRenderPass('post-process', `${this.name}${cameraID}`) + // set inputs + .setPassInput(input0, 'inputTexture') + .setPassInput(depth, 'depthTexture') + // set outputs + .addRasterView(output, gfx.Format.RGBA8) + // final render + .blitScreen(0) + // calculate a version + .version(); + } + } + ``` + +3. 注册 custom pass + + ```js + let builder = rendering.getCustomPipeline('Custom') as postProcess.PostProcessBuilder; + if (builder) { + // insert CustomPass after a BlitScreenPass + builder.insertPass(new CustomPass, BlitScreenPass); + } + ``` + +具体可以参考 [custom-pass](https://github.com/cocos/cocos-example-render-pipeline/blob/main/assets/cases/post-process/custom-pass.ts) diff --git a/versions/4.0/zh/render-pipeline/post-process/img/1.png b/versions/4.0/zh/render-pipeline/post-process/img/1.png new file mode 100644 index 0000000000..637056fa58 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/1.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/2.png b/versions/4.0/zh/render-pipeline/post-process/img/2.png new file mode 100644 index 0000000000..d348cb3407 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/2.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/3.png b/versions/4.0/zh/render-pipeline/post-process/img/3.png new file mode 100644 index 0000000000..84b7dcd231 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/3.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/blit-screen-material.png b/versions/4.0/zh/render-pipeline/post-process/img/blit-screen-material.png new file mode 100644 index 0000000000..cbebdbf0fc Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/blit-screen-material.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/blit-screen.png b/versions/4.0/zh/render-pipeline/post-process/img/blit-screen.png new file mode 100644 index 0000000000..0590828f3f Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/blit-screen.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/bloom-disabled.png b/versions/4.0/zh/render-pipeline/post-process/img/bloom-disabled.png new file mode 100644 index 0000000000..f2dfa611e2 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/bloom-disabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/bloom-enabled.png b/versions/4.0/zh/render-pipeline/post-process/img/bloom-enabled.png new file mode 100644 index 0000000000..f46b3a78bf Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/bloom-enabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/bloom.png b/versions/4.0/zh/render-pipeline/post-process/img/bloom.png new file mode 100644 index 0000000000..1d48e7e450 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/bloom.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/camera-post-process.png b/versions/4.0/zh/render-pipeline/post-process/img/camera-post-process.png new file mode 100644 index 0000000000..98024dc00c Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/camera-post-process.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/color-grading.png b/versions/4.0/zh/render-pipeline/post-process/img/color-grading.png new file mode 100644 index 0000000000..f812646959 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/color-grading.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/custom-1.png b/versions/4.0/zh/render-pipeline/post-process/img/custom-1.png new file mode 100644 index 0000000000..f1d5d87e9b Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/custom-1.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/custom-2.png b/versions/4.0/zh/render-pipeline/post-process/img/custom-2.png new file mode 100644 index 0000000000..2290b8b378 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/custom-2.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/custom-pass-1.png b/versions/4.0/zh/render-pipeline/post-process/img/custom-pass-1.png new file mode 100644 index 0000000000..638bdaac60 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/custom-pass-1.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/custom-pass-2.png b/versions/4.0/zh/render-pipeline/post-process/img/custom-pass-2.png new file mode 100644 index 0000000000..013bd119ac Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/custom-pass-2.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/enable-post-process.png b/versions/4.0/zh/render-pipeline/post-process/img/enable-post-process.png new file mode 100644 index 0000000000..c6b4141e81 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/enable-post-process.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/fsr-disabled.png b/versions/4.0/zh/render-pipeline/post-process/img/fsr-disabled.png new file mode 100644 index 0000000000..55813932fa Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/fsr-disabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/fsr-enabled.png b/versions/4.0/zh/render-pipeline/post-process/img/fsr-enabled.png new file mode 100644 index 0000000000..0ef53486ec Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/fsr-enabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/fsr.png b/versions/4.0/zh/render-pipeline/post-process/img/fsr.png new file mode 100644 index 0000000000..81f646bd33 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/fsr.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/fxaa-disabled.png b/versions/4.0/zh/render-pipeline/post-process/img/fxaa-disabled.png new file mode 100644 index 0000000000..3c17273f69 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/fxaa-disabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/fxaa-enabled.png b/versions/4.0/zh/render-pipeline/post-process/img/fxaa-enabled.png new file mode 100644 index 0000000000..009d15aae5 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/fxaa-enabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/fxaa.png b/versions/4.0/zh/render-pipeline/post-process/img/fxaa.png new file mode 100644 index 0000000000..96ba4d2633 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/fxaa.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/hbao-debugview.png b/versions/4.0/zh/render-pipeline/post-process/img/hbao-debugview.png new file mode 100644 index 0000000000..4420bab474 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/hbao-debugview.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/hbao-disabled.png b/versions/4.0/zh/render-pipeline/post-process/img/hbao-disabled.png new file mode 100644 index 0000000000..8c2734904f Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/hbao-disabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/hbao-enabled.png b/versions/4.0/zh/render-pipeline/post-process/img/hbao-enabled.png new file mode 100644 index 0000000000..e6d7f3f4d4 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/hbao-enabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/hbao.png b/versions/4.0/zh/render-pipeline/post-process/img/hbao.png new file mode 100644 index 0000000000..5243e7df91 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/hbao.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/lut-disabled.png b/versions/4.0/zh/render-pipeline/post-process/img/lut-disabled.png new file mode 100644 index 0000000000..c95d451805 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/lut-disabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/lut-enabled.png b/versions/4.0/zh/render-pipeline/post-process/img/lut-enabled.png new file mode 100644 index 0000000000..c990448df3 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/lut-enabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/order.png b/versions/4.0/zh/render-pipeline/post-process/img/order.png new file mode 100644 index 0000000000..01a0df28fc Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/order.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/post-process-component.png b/versions/4.0/zh/render-pipeline/post-process/img/post-process-component.png new file mode 100644 index 0000000000..d3bbe8eae1 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/post-process-component.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/post-process.png b/versions/4.0/zh/render-pipeline/post-process/img/post-process.png new file mode 100644 index 0000000000..50f42bf0fc Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/post-process.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/taa-disabled.png b/versions/4.0/zh/render-pipeline/post-process/img/taa-disabled.png new file mode 100644 index 0000000000..16ef6f94a3 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/taa-disabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/taa-enabled.png b/versions/4.0/zh/render-pipeline/post-process/img/taa-enabled.png new file mode 100644 index 0000000000..7ae1594579 Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/taa-enabled.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/img/taa.png b/versions/4.0/zh/render-pipeline/post-process/img/taa.png new file mode 100644 index 0000000000..26e5a6a53d Binary files /dev/null and b/versions/4.0/zh/render-pipeline/post-process/img/taa.png differ diff --git a/versions/4.0/zh/render-pipeline/post-process/index.md b/versions/4.0/zh/render-pipeline/post-process/index.md new file mode 100644 index 0000000000..c4947d0a96 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/post-process/index.md @@ -0,0 +1,162 @@ +# 全屏特效后处理流程 + +全屏特效后处理简称全屏后效、后效等,指的是在相机绘制完成整个画面后,对画面再次进行一次或者多次的图像处理。从而获取更好的画面或者实现某些特殊的效果。 + +Cocos Creator 在 v3.8 版本新加了全屏特效后处理流程,并且内置了一系列常用的效果。 + +## 开启后效流程 + +[开启自定义管线并切换至**前向渲染管线**](../custom-pipeline.md#功能开启),如图所示: + + ![1](img/1.png) + + ![2](img/2.png) + +## 设置后效流程 + +1. 在 **层级管理器** 内点击右键添加一个空节点,命名为 PostProcess (可以按自己的习惯命名) + +2. 点击 **属性检查器** 面板上的 **添加组件** 按钮,并在输入框内输入 PostProcess,菜单栏会列出所有可用的后效组件,选择指定的后效组件来添加 + + ![3](img/3.png) + +添加后效后,节点上会自动增加 **PostProcess** 组件,详情请参考 [Post Process](./post-process.md)。 + +## 启用后效 + +后效可以针对单个相机使用,也可以作为全局后效针对所有相机开启。 + +需要注意的是,如果该相机要启用后效,则必须保持其 **Use Post Process** 属性为勾选状态。 + +![enable-post-process.png](./img/enable-post-process.png) + +### 全局后效 + +如果要将后效应用到所有开启了 **Use Post Process** 的相机,需要在 **PostProcess** 中勾选 **Global** 属性(默认为勾选状态)。 + +![post-process.png](./img/post-process.png) + +### 单个相机后效 + +如果只想针对某个相机开启后效,在想要开启后效的相机组件的 **属性检查器** 上找到 **Post Process** 属性,并将之前创建好的 **PostProcess** 节点拖拽到其上,这样的话,该相机就会启用 **PostProcess** 节点上的后效。 + +![camera-post-process.png](./img/camera-post-process.png) + +## 内置后效 + +### TAA (Temporal Anti-aliasing) + +TAA 是一种先进的抗锯齿技术,他会将历史帧数据混合到一个缓冲区中来使画面更加平滑。TAA 的性能消耗是比较大的,推荐在中高端机型以上才使用。 + +![taa.png](./img/taa.png) + +参数说明: + +1. Sample Scale:采样范围,推荐使用较小的值,该值过大可能造成画面抖动。 +2. Feedback:历史帧混合值,该值越大抗锯齿效果越好,但是画面可能会变糊。 + +- 关闭 TAA + + ![taa-disabled](img/taa-disabled.png) +- 开启 TAA + + ![taa-enabled](img/taa-enabled.png) + +### FSR (AMD FidelityFX Super Resolution) + +FSR 是 AMD 全新的、开源的、高质量的渲染解决方案,用于从低分辨率的输入产生高分辨率的渲染结果。它使用了一系列尖端的算法,特别强调创建高质量的边缘,与直接以原始分辨率渲染相比,性能有很大的提高。 + +![fsr.png](./img/fsr.png) + +参数说明: + +1. Sharpness:画面锐化度 + +- 关闭 FSR + + ![Alt text](img/fsr-disabled.png) + +- 开启 FSR + + ![Alt text](img/fsr-enabled.png) + +### FXAA(Fast Approximate Anti-Aliasing) + +FXAA 以极低的代价来降低场景中的锯齿。 + +![fxaa.png](./img/fxaa.png) + +- 关闭 FXAA + + ![fxaa-disabled](img/fxaa-disabled.png) + +- 开启 FXAA + + ![fxaa-enabled](img/fxaa-enabled.png) + +### Bloom + +Bloom 用于在场景高亮区形成泛光和光晕,可以让光源或强烈反射的材质看起来更加真实。 + +![bloom.png](./img/bloom.png) + +参数说明: + +1. Threshold:光晕的亮度阈值,大于此亮度的区域才会产生泛光。注意该值是人眼所见的 LDR 亮度单位,和曝光无关 +2. Iterations:模糊迭代次数,该值越大则光晕范围越大、越柔和,但性能也会有所降低 +3. Intensity:泛光强度,该值越大则光晕越亮,请适度调节 + +- 关闭 Bloom + ![bloom-disabled](img/bloom-disabled.png) +- 开启 Bloom + ![bloom-enabled](img/bloom-enabled.png) + +### Color Grading + +Color Grading 使用颜色查找表(LUT)的方式对场景颜色做美术向的校正,LUT 贴图可以从网络上下载,也可以在 Photoshop 等软件中自己制作。 + +![color-grading.png](./img/color-grading.png) + +参数说明: + +1. Contribute:贡献度,可在0-1的范围内调节其对颜色的影响程度 +2. Color Grading Map:颜色查找表贴图,支持任意 Nx1 的长条图和 8x8 的方块图,系统自动适配。内置 LUT 贴图路径为 internal/dependencies/textures/lut/ + +- 关闭 Color Grading + ![lut-disabled](img/lut-disabled.png) +- 开启 Color Grading + ![lut-enabled](img/lut-enabled.png) + +### HBAO(Horizon-Based Ambient Occlusion) + +HBAO 提供了一种简便的方式去计算全局照明中的环境光遮蔽,对于整个场景层次感的提升有很大的帮助。 + +![hbao.png](./img/hbao.png) + +参数说明: + +1. Radius Scale:环境光遮蔽的范围,通过调整该值可以让暗部区域更好的按场景尺度进行适配 +2. AO Saturation:环境光遮蔽的饱和度,该值越大则越暗 +3. Need Blur:开启可以获得更柔和的效果,噪点更少,但会耗费一些性能 + +- 关闭 HBAO + + ![hbao-disabled](img/hbao-disabled.png) + +- 开启 HBAO + + ![hbao-enabled](img/hbao-enabled.png) + +- 可以在渲染调试中单独查看 HBAO,在场景右上角的调试模式菜单中选择单项选择中的环境光遮蔽即可 + + ![hbao-debugview](img/hbao-debugview.png) + +## 自定义后效 + +参考 [自定义后效](./custom.md) + +## 后效的顺序 + +在节点上挂载的后效组件的顺序不等于后效的执行顺序,实际上后效有严格的启用和执行属性,开发者可以在引擎的源代码目录内的 [post-process-builder.ts](https://github.com/cocos/cocos4/blob/v4.0.0/cocos/rendering/post-process/post-process-builder.ts) 文件内找到。 + +![order.png](./img/order.png) diff --git a/versions/4.0/zh/render-pipeline/post-process/post-process.md b/versions/4.0/zh/render-pipeline/post-process/post-process.md new file mode 100644 index 0000000000..d28e34c2a6 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/post-process/post-process.md @@ -0,0 +1,21 @@ +# PostProcess 组件 + +Post-Process 组件是 [后效](./index.md) 的辅助组件,提供后效的配置。在任何后效组件被添加后,该组件会自动添加到节点上。 + +## 属性 + +![post-process-component.png](./img/post-process-component.png) + +| 属性 | 说明 | +| :-- | :-- | +| **Global** | 是否是全局后效特效,启用后,所有开启了 **Use Post Process** 的相机组件都会应用该组件下所挂载的后效 | +| **Shading Scale** | 最终输出时的分辨率缩放因子,基于当前的屏幕分辨率,最大为 1,最小为 0.01 | +| **Enable Shading Scale In Editor** | 是否在编辑器中启用 **Shading Scale** | + +### 说明 + +- 当 **Use Post Process** 属性被勾选,并给相机分配了 **Post Process** 后,PostProcess 组件的 **Global** 属性会被忽略。 + +- 当 **Use Post Process** 属性被勾选,并未给相机分配了 **Post Process** 后,需要在 PostProcess 组件上勾选 **Global** 属性才会时配置的后效生效。 + +- 假如场景内有多个 **PostProcess** 组件并添加了同样的后效组件,只有一个会生效。 diff --git a/versions/4.0/zh/render-pipeline/use-builtin-pipeline.md b/versions/4.0/zh/render-pipeline/use-builtin-pipeline.md new file mode 100644 index 0000000000..f27e827b30 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/use-builtin-pipeline.md @@ -0,0 +1,48 @@ +# 使用内置渲染管线 + +在项目菜单->项目设置->图形设置中选择**新渲染管线**。 + +![builtin-enable](./image/builtin-enable.png) + +默认的渲染管线为内置渲染管线**Builtin**。 + +## 内置渲染管线(Builtin) + +内置渲染管线(Builtin)是基于可定制渲染管线(CRP - Customizable Render Pipeline)的一种实现,提供了跨平台的基础渲染功能,适用于所有平台。 + +目前内置渲染管线支持以下功能: + +- 前向渲染 +- 多重采样抗锯齿(MSAA) +- 实时阴影 +- 后处理,包括(Bloom、Color Grading、FXAA、FSR等) + +几乎可以满足所有的常规项目需求。 + +## 内置管线设置组件 + +场景中每个摄像机,根据排序依次进行渲染,输出到屏幕或者渲染纹理。 + +我们可以为每一个摄像机节点,添加 `BuiltinPipelineSettings` 组件,对该摄像机的渲染进行设置。 + +通过此组件,我们可以控制摄像机输出的行为和后期效果等。 + +>对于自定义的渲染管线,建议复制此组件源码到项目,并根据需要进行修改。 + +## 编辑器预览(实验) + +勾选`Editor Preview (Experimental)`时,可以在场景编辑器中可以直接预览渲染效果。 + +![builtin-editor-preview](./image/builtin-editor-preview.png) + +由于场景渲染可能附带后期效果,因此在编辑器中预览时,可能会有性能影响,以及部分编辑器效果显示异常。 + +## 使用后期效果 + +关于后期效果的配置和使用,请看参考文档:[使用后期效果](./use-post-process.md) + +## 代码位置 + +内置渲染管线的实现位于 `editor/assets/default_renderpipeline/builtin-pipeline.ts`。 + +其中`BuiltinPipelineBuilder`类实现了内置管线的主要功能。 diff --git a/versions/4.0/zh/render-pipeline/use-post-process.md b/versions/4.0/zh/render-pipeline/use-post-process.md new file mode 100644 index 0000000000..6a718cd856 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/use-post-process.md @@ -0,0 +1,63 @@ +# 使用后期效果 + +> **注意**:本文档适用于 v3.8.4 及以上用户,v3.8.3 及以下用户,请参考文档: [全屏特效后处理流程](./post-process/index.md)。 + +## 添加组件 + +场景中每一个摄像机,都可以添加一个 `BuiltinPipelineSettings`,`BuiltinPipelineSettings` 用于进行当前摄像机的渲染设置,其中也包括后期效果。 + +开启对应的功能时,需要附上所需材质以及贴图,一般以 builtin 开头,如下图所示。 + +![builtin-pipeline-settings](./image/builtin-pipeline-settings.png) + +## 多重采样抗锯齿(MSAA) + +多重采样抗锯齿(MSAA)是一种抗锯齿技术,光照阶段通过混合不同深度的采样,消除图像边缘锯齿现象。 + +目前支持2x、4x的多重采样,仅在原生平台上开启。 + +Web 平台上,目前使用`WebGL`的`antialias`功能,通过`ENABLE_WEBGL_ANTIALIAS`宏开启。 + +## 渲染缩放(Shading Scale) + +`Shading Scale` 是一种渲染优化技术,通过降低渲染分辨率,减少渲染负担,提高渲染性能。 + +比如窗口大小为 1920x1080,`Shading Scale` 为 0.5 时,渲染分辨率为 960x540。 + +适用于渲染压力较大的场景,同时可以配合超分辨率技术,提高画面质量。 + +对于最后的UI渲染,会使用原始分辨率进行渲染。 + +`BuiltinPipelineSettings`组件上设置的`Shading Scale`属性,仅对当前相机生效。 + +## 泛光(Bloom) + +泛光(Bloom)是一种后处理效果,通过提取图像中的高亮部分,模糊处理后叠加到原图上,增强画面的光亮效果。 + +`Bloom Iteraions` 为泛光迭代次数,`Bloom Threshold` 为泛光阈值。 + +![builtin-bloom](./image/builtin-bloom.png) + +## 色彩分级(Color Grading) + +色彩分级(Color Grading)是一种后处理效果,通过分级贴图(Grading Map LUT),调整画面的色彩效果。 + +## 快速近似抗锯齿(FXAA) + +快速近似抗锯齿(FXAA)是一种抗锯齿技术,通过对图像进行平滑处理,消除图像边缘锯齿现象。 + +[builtin-fxaa](./image/builtin-fxaa.png) + +## 超分辨率(FSR) + +超分辨率是一种渲染技术,通过降低渲染分辨率,再通过算法提高画面质量。 + +目前使用的是`AMD FidelityFX Super Resolution`技术。 + +目前仅在`Shading Scale < 1`时生效。 + +## 色调映射(Tone Mapping) + +色调映射(Tone Mapping)是一种后处理效果,通过调整图像的色调,提高画面的视觉效果。 + +目前色彩分级(Color Grading)整合了色调映射(Tone Mapping),如要自定义,需要协同修改。 diff --git a/versions/4.0/zh/render-pipeline/user-pipeline.md b/versions/4.0/zh/render-pipeline/user-pipeline.md new file mode 100644 index 0000000000..1b44f32f92 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/user-pipeline.md @@ -0,0 +1,61 @@ +# 自定义渲染管线(Deprecated) + +要创建一个自定义的渲染管线,首先要在 **资源管理器** 面板中新建一个 RenderPipeline 资源,再创建一个 RenderPipeline 脚本,然后在 Pipeline 资源中选择对应的 RenderPipeline 脚本,即可编辑对应的属性。 + +RenderFlow 和 RenderStage 使用同样的方式进行创建和编辑。在创建出来的 Pipeline 脚本中,可以像其它用户脚本一样添加属性并使其可以在 **属性检查器** 面板中编辑,但需要注意的是不能拖放场景中的实体,因为 RenderPipeline 并不和某个具体的场景绑定。 + +## RenderPipeline 中的属性和方法 + +- `flows`:RenderPipeline 中包含的 RenderFlow。 + +- `renderTextures`:在 RenderPipeline 启动时可创建的 RenderTexture。 + + - `name`:RenderTexture 的名字,创建后可通过 RenderPipeline 的 `getRenderTexture` 函数获取。 + - `type`:RenderTexture 的类型。 + - `viewType`:RenderTexture 对应的 TextureView 类型。 + - `usage`:RenderTexture 的绑定类型,用于确定是 `color` 还是 `depth_stencil`。 + - `formate`:RenderTexture 的通道格式。 + - `width`:RenderTexture 的宽度,-1 表示窗口宽度。 + - `height`:RenderTexture 高度,-1 表示窗口高度。 + +- `framebuffers`:在 RenderPipeline 启动时可创建的 FrameBuffer。 + + - `name`:FrameBuffer 的名字,创建后可通过 RenderPipeline 的 `getFrameBuffer` 函数获取。 + - `renderPass`:RenderPass。RenderPipeline 中配置的 RenderPass 的 ID。 + - `colorViews`:与 ColorAttachment 绑定的 TextureView。指定 RenderPipeline 中配置的 RenderTexture。 + - `depthStencilView`:与 DepthStencilAttachment 绑定的 TextureView。指定 RenderPipeline 中配置的 RenderTexture。 +- `renderPasses`:在 RenderPipeline 启动时可创建的 RenderPass。 + - `index`:RenderPass 的 ID,可通过 RenderPipeline 的 `getRenderPass` 函数获取。 + - `colorAttachments`:ColorAttachment 描述,绘制 FrameBuffer 时对 ColorAttachment 的操作。 + - `depthStencilAttachment`:DepthStencilAttachment 描述,绘制 FrameBuffer 时对 DepthStencilAttachment 的操作。 + +- `getTextureView` (name: string),`getRenderTexture` (name: string):用于获取在 renderTextures 配置的 RenderTexture。 +- `getFrameBuffer` (name: string):用于获取在 framebuffers 配置的 FrameBuffer。 +- `getRenderPass` (stage: number):用于获取在 renderPasses 配置的 RenderPass。 +- `initialize` (info: IRenderPipelineInfo):用于通过脚本创建一个 RenderPipeline 时的初始化函数,RenderPipeline 必须初始化后才能使用。 +- `activate` (root: Root):用于通过资源加载一个 RenderPipeline 时的初始化函数,RenderPipeline 必须初始化后才能使用。 +- `render` (view: RenderView):渲染场景的逻辑。 +- `updateUBOs` (view: RenderView):更新全局 UniformBuffer。 +- `sceneCulling` (view: RenderView):场景剔除,剔除后可渲染物体保存在 `_renderObjects` 中。 + +## RenderFlow 中的属性和方法 + +- `name`:RenderFlow 的名字。 +- `priority`:RenderFlow 在 RenderPipeline 中的执行顺序。 +- `type`:RenderFlow 的类型。包括以下三种: + - `SCENE`:用于绘制场景,该类型对于每个 camera 都会执行; + - `POSTPROCESS`:后期处理,该类型对每个 camera 都要单独指定; + - `UI`:用于绘制 UI。 +- `stages`:RenderFlow 包含的 RenderStage。 + +## RenderStage 中的属性和方法 + +- `name`:RenderStage 的名字。 +- `priority`:RenderStage 在 RenderFlow 中的执行顺序。 +- `frameBuffer`:RenderStage 要绘制到的 FrameBuffer,应设置为 RenderPipeline 中配置的 FrameBuffer,或设置为 `window`,表示使用默认的 FrameBuffer。 +- `renderQueues`:渲染列队,用于控制物体渲染顺序。 + - `isTransparent`:标记渲染列队是否为半透明; + - `sortMode`:包括以下两种:
`FRONT_TO_BACK`:从前向后排序;
`BACK_TO_FRONT`:从后向前排序。 + - `stages`:指定渲染列队渲染材质中的哪些 pass,应指定为 pass 中的 phase。 + - `sortRenderQueue()`:对渲染列队排序; + - `executeCommandBuffer` (view: RenderView):执行渲染指令。 diff --git a/versions/4.0/zh/render-pipeline/write-render-pipeline.md b/versions/4.0/zh/render-pipeline/write-render-pipeline.md new file mode 100644 index 0000000000..422f87b907 --- /dev/null +++ b/versions/4.0/zh/render-pipeline/write-render-pipeline.md @@ -0,0 +1,774 @@ +# 编写渲染管线 + +项目中为了实现一些特殊效果,或者是精准控制渲染流程以获得最大的性能提升,往往需要对渲染流程进行定制化修改。 + +Cocos 可定制渲染管线(CRP),使开发者可以在不修改引擎源码的情况下,编写通用跨平台的自定义渲染管线。 + +这个教程以内置渲染管线(Bultin)为例,展示如何基于 CRP 编写一个自定义渲染管线。 + +## 源码位置 + +建议大家在内置管线的基础编写自己的管线,可以快速起步和迭代有。以下两种方案均可以得到内置管线源码: + +1. 参考 Github 上的 [Cocos CRP 示例项目](https://github.com/cocos/cocos-example-custom-pipeline),找到对应引擎版本的分支即可。 +2. 复制编辑器内 internal/default_renderpipeline/ 目录下的三个管线相关脚本到 assets 目录,再做修改。 + + - builtin-pipeline.ts:管线实现 + - builtin-pipeline-types.ts:管线需要用到的相关数据类型 + - buitlin-pipeline-settings.ts:管线设置组件 + +## 改写内置管线 + +由于管线的编写较为复杂,可以在内置管线的基础上进行改写。 + +用户可以复制内置管线的代码到项目中,然后根据自己的需求,进行修改。 + +需要拷贝的文件有: +`builtin-pipeline.ts`,`builtin-pipeline-types.ts`,以及`builtin-pipeline-settings.ts`。 + +拷贝完后,需要对类的名字、以及注册名进行改写。 + +`builtin-pipeline.ts`: + +```typescript +class MyPipelineBuilder implements rendering.PipelineBuilder { + // ... +} +if (rendering) { + rendering.setCustomPipeline('MyPipeline', new MyPipelineBuilder()); +} +``` + +`builtin-pipeline-settings.ts`: + +```typescript +@ccclass('MyPipelineSettings') +@menu('Rendering/MyPipelineSettings') +@requireComponent(Camera) +@disallowMultiple +@executeInEditMode +export class MyPipelineSettings extends Component { + // ... + +} +``` + +## 创建渲染管线 + +首先,我们需要创建一个渲染管线的类,继承自`rendering.Pipeline`。 + +```typescript +import { renderer, rendering } from 'cc'; + +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + windowResize( + ppl: rendering.BasicPipeline, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, + nativeWidth: number, + nativeHeight: number, + ): void { + // 设置渲染资源 + } + setup( + cameras: renderer.scene.Camera[], + ppl: rendering.BasicPipeline, + ): void { + // 设置相机渲染 + } +} +``` + +自定义管线需要实现两个核心方法 `windowResize` 和 `setup`。 + +### `windowResize`方法 + +`windowResize` 方法会在管线启动时,以及窗口大小改变时被调用。在 `windowResize` 中,我们需要将使用的资源进行注册。只在有 `windowResize` 函数中注册过的资源,才能在管线中被使用。 + +> 注:注册资源只是向系统添加资源描述,如果在渲染过程中不被使用,则不会分配系统资源,不会增大内存开销。 + +### `setup` 方法 + +`setup`方法每帧都会在摄像机渲染时被调用,用于设置此摄像机的渲染流程。此方法用于构建 RenderGraph 节点图,构建完成后,会交给渲染管线进行渲染。 + +> 注:只有在 `windowResize` 中注册过的资源才能被使用。 + +内置管线的具体实现,在`builtin-pipeline.ts`中。 + +## 管线注册与使用 + +用户实现的渲染管线,可以在脚本导入阶段,注册到渲染系统中。 + +```typescript +import { rendering } from 'cc'; + +if (rendering) { + rendering.setCustomPipeline('Builtin', new BuiltinPipelineBuilder()); +} +``` + +其中 `Builtin` 是内置管线的名字,你可以更换为任意你想要的字符串。 更换完成后,在项目设置->图形设置->新渲染管线中,将管线名称切换为自己的管线名称即可使用该管线。 + +## 渲染资源管理 + +Cocos的可定制渲染管线(CRP)基于`Render Graph`理念,但在资源管理上与以往`Render Graph`有所不同。 + +对于需要写入的资源,必须在管线中注册。一般在`windowResize`方法中注册渲染资源。 + +注册时需要提供资源的名字作为唯一标识符,所以不同的资源,不要使用相同名字,以免冲突。 + +如果有多个相机,可以在资源名字后面加上相机的`Id`,以区分不同相机的资源。 + +```typescript +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + windowResize( + ppl: rendering.BasicPipeline, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, + nativeWidth: number, + nativeHeight: number, + ): void { + const id = window.renderWindowId; + const width = nativeWidth; + const height = nativeHeight; + + ppl.addRenderWindow( + window.colorName, + Format.RGBA8, nativeWidth, nativeHeight, window, + window.depthStencilName); + + ppl.addDepthStencil( + `SceneDepth${id}`, + Format.DEPTH_STENCIL, width, height, + ResourceResidency.MANAGED); + } +} +``` + +在一帧里,请不要改变资源的大小、规格,否则会导致渲染错误。 + +如果重复设置,会以最后一次设置的参数为准。 + +## 资源驻留与生命周期 + +在管线中注册的资源,有以下几种资源驻留类型(`ResourceResidency`): + +- `MANAGED` - 管线托管的资源,会在合适的时机自动释放。 +- `MEMORYLESS` - 片上资源,不占显存。只能在一个RenderPass中使用,RenderPass结束后自动释放。 +- `PERSISTENT` - 管线托管的持久资源,不会被释放,直到管线销毁。一般用于需要跨帧使用的资源。 +- `EXTERNAL` - 外部资源,不由管线管理,需要用户自行释放。 +- `BACKBUFFER` - 后台缓冲区,不需要用户管理。 + +对于`MANAGED`类型的资源,管线会在GPU结束使用后释放,所以会延后几帧。 + +目前引擎针对移动端优化,需要尽量减少`Framebuffer`的创建与销毁,所以不会移动资源的内存位置。未来在桌面端,`MANAGED`会作为过渡(Transient)资源,进行更多内存优化。 + +## 渲染流程 + +`setup`方法中,可以设置相机的渲染流程,每帧会被调用一次。 + +传入的`cameras`参数,是当前渲染的相机列表,已经经过了排序。 + +传入的`ppl`参数,是当前的渲染管线。 + +```typescript +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + setup( + cameras: renderer.scene.Camera[], + ppl: rendering.BasicPipeline, + ): void { + for (const camera of cameras) { + if (!camera.scene || !camera.window) { + continue; + } + + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_BEGIN, camera); + + this._buildSimplePipeline(camera, ppl); + + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_END, camera); + } + } +} +``` + +以上就是编写一个渲染管线的基本流程。 + +## 构建相机的渲染管线 + +对于不同的相机,可以构建不同的渲染渲染管线,以实现不同的渲染效果。 + +以下展示了内置管线的简单渲染管线,主要用于编辑器`Gizmos`的渲染。 + +```typescript +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + private _buildSimplePipeline( + ppl: rendering.BasicPipeline, + camera: renderer.scene.Camera, + ): void { + const width = Math.max(Math.floor(camera.window.width), 1); + const height = Math.max(Math.floor(camera.window.height), 1); + const colorName = this._cameraConfigs.colorName; + const depthStencilName = this._cameraConfigs.depthStencilName; + + const viewport = camera.viewport; // Reduce C++/TS interop + this._viewport.left = Math.round(viewport.x * width); + this._viewport.top = Math.round(viewport.y * height); + // Here we must use camera.viewport.width instead of camera.viewport.z, which + // is undefined on native platform. The same as camera.viewport.height. + this._viewport.width = Math.max(Math.round(viewport.width * width), 1); + this._viewport.height = Math.max(Math.round(viewport.height * height), 1); + + const clearColor = camera.clearColor; // Reduce C++/TS interop + this._clearColor.x = clearColor.x; + this._clearColor.y = clearColor.y; + this._clearColor.z = clearColor.z; + this._clearColor.w = clearColor.w; + + const pass = ppl.addRenderPass(width, height, 'default'); + + // bind output render target + if (forwardNeedClearColor(camera)) { + pass.addRenderTarget(colorName, LoadOp.CLEAR, StoreOp.STORE, this._clearColor); + } else { + pass.addRenderTarget(colorName, LoadOp.LOAD, StoreOp.STORE); + } + + // bind depth stencil buffer + if (camera.clearFlag & ClearFlagBit.DEPTH_STENCIL) { + pass.addDepthStencil( + depthStencilName, + LoadOp.CLEAR, + StoreOp.DISCARD, + camera.clearDepth, + camera.clearStencil, + camera.clearFlag & ClearFlagBit.DEPTH_STENCIL, + ); + } else { + pass.addDepthStencil(depthStencilName, LoadOp.LOAD, StoreOp.DISCARD); + } + + pass.setViewport(this._viewport); + + // The opaque queue is used for Reflection probe preview + pass.addQueue(QueueHint.OPAQUE) + .addScene(camera, SceneFlags.OPAQUE); + + // The blend queue is used for UI and Gizmos + let flags = SceneFlags.BLEND | SceneFlags.UI; + if (this._cameraConfigs.enableProfiler) { + flags |= SceneFlags.PROFILER; + pass.showStatistics = true; + } + pass.addQueue(QueueHint.BLEND) + .addScene(camera, flags); + } +} +``` + +## 动态渲染管线 + +在`setup`方法中,可以根据相机、平台、硬件参数等,动态构建渲染管线。 + +首先可以在`windowResize`,`setup`函数的最开始,收集当前的环境信息。 + +内置管线的`setupPipelineConfigs`负责收集平台相关信息。`setupCameraConfigs`函数,用于收集相机相关信息。 + +```typescript +function setupPipelineConfigs( + ppl: rendering.BasicPipeline, + configs: PipelineConfigs, +): void { + const device = ppl.device; + // Platform + configs.isWeb = !sys.isNative; + configs.isWebGL1 = device.gfxAPI === gfx.API.WEBGL; + configs.isWebGPU = device.gfxAPI === gfx.API.WEBGPU; + configs.isMobile = sys.isMobile; + + // Rendering + configs.isHDR = ppl.pipelineSceneData.isHDR; + configs.useFloatOutput = ppl.getMacroBool('CC_USE_FLOAT_OUTPUT'); + configs.toneMappingType = ppl.pipelineSceneData.postSettings.toneMappingType; + + // ... +} + +function setupCameraConfigs( + camera: renderer.scene.Camera, + pipelineConfigs: PipelineConfigs, + cameraConfigs: CameraConfigs, +): void { + cameraConfigs.colorName = camera.window.colorName; + cameraConfigs.depthStencilName = camera.window.depthStencilName; + + cameraConfigs.useFullPipeline = (camera.visibility & (Layers.Enum.DEFAULT)) !== 0; + + setupPostProcessConfigs(pipelineConfigs, cameraConfigs.settings, cameraConfigs); + // ... +} + +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + windowResize( + ppl: rendering.BasicPipeline, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, + nativeWidth: number, + nativeHeight: number, + ): void { + setupPipelineConfigs(ppl, this._configs); + setupCameraConfigs(camera, this._configs, this._cameraConfigs); + + // 设置渲染资源 + // ... + } + setup(cameras: renderer.scene.Camera[], ppl: rendering.BasicPipeline): void { + for (const camera of cameras) { + if (!camera.scene || !camera.window) { + continue; + } + // 获取相机信息 + setupCameraConfigs(camera, this._configs, this._cameraConfigs); + + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_BEGIN, camera); + + // 根据相机信息,构建渲染管线 + if (this._cameraConfigs.useFullPipeline) { + this._buildForwardPipeline(ppl, camera, camera.scene); + } else { + this._buildSimplePipeline(ppl, camera); + } + + this._pipelineEvent.emit(PipelineEventType.RENDER_CAMERA_END, camera); + } + } +} +``` + +然后可以根据这些信息,动态构建渲染管线。 + +介于平台的多样性以及性能要求,算法的选取与判定会较为复杂。这是新管线主要的适用场景,尽可能简化渲染管线的构建,提高开发效率。 + +```typescript +private _buildForwardPipeline( + ppl: rendering.BasicPipeline, + camera: renderer.scene.Camera, + scene: renderer.RenderScene, +): void { + // Init + const settings = this._cameraConfigs.settings; + const nativeWidth = Math.max(Math.floor(camera.window.width), 1); + const nativeHeight = Math.max(Math.floor(camera.window.height), 1); + const width = this._cameraConfigs.enableShadingScale + ? Math.max(Math.floor(nativeWidth * this._cameraConfigs.shadingScale), 1) + : nativeWidth; + const height = this._cameraConfigs.enableShadingScale + ? Math.max(Math.floor(nativeHeight * this._cameraConfigs.shadingScale), 1) + : nativeHeight; + const id = camera.window.renderWindowId; + const colorName = this._cameraConfigs.colorName; + const sceneDepth = this._cameraConfigs.enableShadingScale + ? `ScaledSceneDepth${id}` + : `SceneDepth${id}`; + const radianceName = this._cameraConfigs.enableShadingScale + ? `ScaledRadiance${id}` + : `Radiance${id}`; + const ldrColorName = this._cameraConfigs.enableShadingScale + ? `ScaledLdrColor${id}` + : `LdrColor${id}`; + const mainLight = scene.mainLight; + + // Forward Lighting (Light Culling) + this.forwardLighting.cullLights(scene, camera.frustum); + + // Main Directional light CSM Shadow Map + if (this._cameraConfigs.enableMainLightShadowMap) { + assert(!!mainLight); + this._addCascadedShadowMapPass(ppl, id, mainLight, camera); + } + + // Spot light shadow maps (Mobile or MSAA) + if (this._cameraConfigs.singleForwardRadiancePass) { + // Currently, only support 1 spot light with shadow map on mobile platform. + // TODO(zhouzhenglong): Relex this limitation. + this.forwardLighting.addSpotlightShadowPasses(ppl, camera, this._configs.mobileMaxSpotLightShadowMaps); + } + + this._tryAddReflectionProbePasses(ppl, id, mainLight, camera.scene); + + // Forward Lighting + let lastPass: rendering.BasicRenderPassBuilder; + if (this._cameraConfigs.enablePostProcess) { // Post Process + // Radiance and DoF + if (this._cameraConfigs.enableDOF) { + assert(!!settings.depthOfField.material); + const dofRadianceName = `DofRadiance${id}`; + // Disable MSAA, depth stencil cannot be resolved cross-platformly + this._addForwardRadiancePasses(ppl, id, camera, width, height, mainLight, + dofRadianceName, sceneDepth, true, StoreOp.STORE); + this._addDepthOfFieldPasses(ppl, settings, settings.depthOfField.material, + id, camera, width, height, + dofRadianceName, sceneDepth, radianceName, ldrColorName); + } else { + this._addForwardRadiancePasses( + ppl, id, camera, width, height, mainLight, + radianceName, sceneDepth); + } + // Bloom + if (this._cameraConfigs.enableBloom) { + assert(!!settings.bloom.material); + this._addKawaseDualFilterBloomPasses( + ppl, settings, settings.bloom.material, + id, width, height, radianceName); + } + // Tone Mapping and FXAA + if (this._cameraConfigs.enableFXAA) { + assert(!!settings.fxaa.material); + const copyAndTonemapPassNeeded = this._cameraConfigs.enableHDR + || this._cameraConfigs.enableColorGrading; + const ldrColorBufferName = copyAndTonemapPassNeeded ? ldrColorName : radianceName; + // FXAA is applied after tone mapping + if (copyAndTonemapPassNeeded) { + this._addCopyAndTonemapPass(ppl, settings, width, height, radianceName, ldrColorBufferName); + } + // Apply FXAA + if (this._cameraConfigs.enableShadingScale) { + const aaColorName = `AaColor${id}`; + // Apply FXAA on scaled image + this._addFxaaPass(ppl, settings.fxaa.material, + width, height, ldrColorBufferName, aaColorName); + // Copy FXAA result to screen + if (this._cameraConfigs.enableFSR && settings.fsr.material) { + // Apply FSR + lastPass = this._addFsrPass(ppl, settings, settings.fsr.material, + id, width, height, aaColorName, + nativeWidth, nativeHeight, colorName); + } else { + // Scale FXAA result to screen + lastPass = this._addCopyPass(ppl, + nativeWidth, nativeHeight, aaColorName, colorName); + } + } else { + // Image not scaled, output FXAA result to screen directly + lastPass = this._addFxaaPass(ppl, settings.fxaa.material, + nativeWidth, nativeHeight, ldrColorBufferName, colorName); + } + } else { + // No FXAA (Size might be scaled) + lastPass = this._addTonemapResizeOrSuperResolutionPasses(ppl, settings, id, + width, height, radianceName, ldrColorName, + nativeWidth, nativeHeight, colorName); + } + } else if (this._cameraConfigs.enableHDR || this._cameraConfigs.enableShadingScale) { // HDR or Scaled LDR + this._addForwardRadiancePasses(ppl, id, camera, + width, height, mainLight, radianceName, sceneDepth); + lastPass = this._addTonemapResizeOrSuperResolutionPasses(ppl, settings, id, + width, height, radianceName, ldrColorName, + nativeWidth, nativeHeight, colorName); + } else { // LDR (Size is not scaled) + lastPass = this._addForwardRadiancePasses(ppl, id, camera, + nativeWidth, nativeHeight, mainLight, + colorName, this._cameraConfigs.depthStencilName); + } + + // UI size is not scaled, does not have AA + this._addUIQueue(camera, lastPass); +} +``` + +## 管线配置 + +内置管线的配置,由`BuiltinPipelineSettings`组件提供,会保存一个json对象至camera.pipelineSettings。 + +```typescript +export class BuiltinPipelineSettings extends Component { + @property + private readonly _settings: PipelineSettings = makePipelineSettings(); + + onEnable(): void { + fillRequiredPipelineSettings(this._settings); + const cameraComponent = this.getComponent(Camera)!; + const camera = cameraComponent.camera; + camera.pipelineSettings = this._settings; + + if (EDITOR) { + this._tryEnableEditorPreview(); + } + } + onDisable(): void { + const cameraComponent = this.getComponent(Camera)!; + const camera = cameraComponent.camera; + camera.pipelineSettings = null; + + if (EDITOR) { + this._disableEditorPreview(); + } + } +} +``` + +可以根据这个json对象,设置相机的渲染效果。 + +```typescript +function setupPostProcessConfigs( + pipelineConfigs: PipelineConfigs, + settings: PipelineSettings, + cameraConfigs: CameraConfigs, +) { + cameraConfigs.enableDOF = pipelineConfigs.supportDepthSample + && settings.depthOfField.enabled + && !!settings.depthOfField.material; + + cameraConfigs.enableBloom = settings.bloom.enabled + && !!settings.bloom.material; + + cameraConfigs.enableColorGrading = settings.colorGrading.enabled + && !!settings.colorGrading.material + && !!settings.colorGrading.colorGradingMap; + + cameraConfigs.enableFXAA = settings.fxaa.enabled + && !!settings.fxaa.material; + + cameraConfigs.enablePostProcess = (cameraConfigs.enableDOF + || cameraConfigs.enableBloom + || cameraConfigs.enableColorGrading + || cameraConfigs.enableFXAA); +} +``` + +## 添加后处理效果 + +这里以泛光(Bloom)为例,展示如何添加后处理效果。 + +```typescript +class BuiltinPipelineBuilder implements rendering.PipelineBuilder { + windowResize( + ppl: rendering.BasicPipeline, + window: renderer.RenderWindow, + camera: renderer.scene.Camera, + nativeWidth: number, + nativeHeight: number, + ): void { + //... + + // Bloom (Kawase Dual Filter) + if (this._cameraConfigs.enableBloom) { + let bloomWidth = width; + let bloomHeight = height; + for (let i = 0; i !== settings.bloom.iterations + 1; ++i) { + bloomWidth = Math.max(Math.floor(bloomWidth / 2), 1); + bloomHeight = Math.max(Math.floor(bloomHeight / 2), 1); + ppl.addRenderTarget(`BloomTex${id}_${i}`, this._cameraConfigs.radianceFormat, bloomWidth, bloomHeight); + } + } + + //... + } + + private _addKawaseDualFilterBloomPasses( + ppl: rendering.BasicPipeline, + settings: PipelineSettings, + bloomMaterial: Material, + id: number, + width: number, + height: number, + radianceName: string, + ): void { + // Based on Kawase Dual Filter Blur. Saves bandwidth on mobile devices. + // eslint-disable-next-line max-len + // https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_slides.pdf + + // Size: [prefilter(1/2), downsample(1/4), downsample(1/8), downsample(1/16), ...] + const iterations = settings.bloom.iterations; + const sizeCount = iterations + 1; + this._bloomWidths.length = sizeCount; + this._bloomHeights.length = sizeCount; + this._bloomWidths[0] = Math.max(Math.floor(width / 2), 1); + this._bloomHeights[0] = Math.max(Math.floor(height / 2), 1); + for (let i = 1; i !== sizeCount; ++i) { + this._bloomWidths[i] = Math.max(Math.floor(this._bloomWidths[i - 1] / 2), 1); + this._bloomHeights[i] = Math.max(Math.floor(this._bloomHeights[i - 1] / 2), 1); + } + + // Bloom texture names + this._bloomTexNames.length = sizeCount; + for (let i = 0; i !== sizeCount; ++i) { + this._bloomTexNames[i] = `BloomTex${id}_${i}`; + } + + // Setup bloom parameters + this._bloomParams.x = this._configs.useFloatOutput ? 1 : 0; + this._bloomParams.x = 0; // unused + this._bloomParams.z = settings.bloom.threshold; + this._bloomParams.w = settings.bloom.enableAlphaMask ? 1 : 0; + + // Prefilter pass + const prefilterPass = ppl.addRenderPass(this._bloomWidths[0], this._bloomHeights[0], 'cc-bloom-prefilter'); + prefilterPass.addRenderTarget( + this._bloomTexNames[0], + LoadOp.CLEAR, + StoreOp.STORE, + this._clearColorTransparentBlack, + ); + prefilterPass.addTexture(radianceName, 'inputTexture'); + prefilterPass.setVec4('g_platform', this._configs.platform); + prefilterPass.setVec4('bloomParams', this._bloomParams); + prefilterPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 0); + + // Downsample passes + for (let i = 1; i !== sizeCount; ++i) { + const downPass = ppl.addRenderPass(this._bloomWidths[i], this._bloomHeights[i], 'cc-bloom-downsample'); + downPass.addRenderTarget(this._bloomTexNames[i], LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); + downPass.addTexture(this._bloomTexNames[i - 1], 'bloomTexture'); + this._bloomTexSize.x = this._bloomWidths[i - 1]; + this._bloomTexSize.y = this._bloomHeights[i - 1]; + downPass.setVec4('g_platform', this._configs.platform); + downPass.setVec4('bloomTexSize', this._bloomTexSize); + downPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 1); + } + + // Upsample passes + for (let i = iterations; i-- > 0;) { + const upPass = ppl.addRenderPass(this._bloomWidths[i], this._bloomHeights[i], 'cc-bloom-upsample'); + upPass.addRenderTarget(this._bloomTexNames[i], LoadOp.CLEAR, StoreOp.STORE, this._clearColorTransparentBlack); + upPass.addTexture(this._bloomTexNames[i + 1], 'bloomTexture'); + this._bloomTexSize.x = this._bloomWidths[i + 1]; + this._bloomTexSize.y = this._bloomHeights[i + 1]; + upPass.setVec4('g_platform', this._configs.platform); + upPass.setVec4('bloomTexSize', this._bloomTexSize); + upPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 2); + } + + // Combine pass + const combinePass = ppl.addRenderPass(width, height, 'cc-bloom-combine'); + combinePass.addRenderTarget(radianceName, LoadOp.LOAD, StoreOp.STORE); + combinePass.addTexture(this._bloomTexNames[0], 'bloomTexture'); + combinePass.setVec4('g_platform', this._configs.platform); + combinePass.setVec4('bloomParams', this._bloomParams); + combinePass + .addQueue(QueueHint.BLEND) + .addFullscreenQuad(bloomMaterial, 3); + } +} +``` + +以Prefilter Pass为例: + +首先我们添加了一个渲染通道,标注了它的大小,和用到的effect pass name。 +```typescript +const prefilterPass = ppl.addRenderPass(this._bloomWidths[0], this._bloomHeights[0], 'cc-bloom-prefilter'); +``` + +effect pass name 是在effect文件中定义的。只有匹配pass name的effect,才能在这个渲染通道中使用。 + + +``` +// bloom1.effect +CCEffect %{ + techniques: + - passes: + - vert: bloom-vs + frag: prefilter-fs + pass: cc-bloom-prefilter // pass name + rasterizerState: + cullMode: none + depthStencilState: + depthTest: false + depthWrite: false +}% + +CCProgram bloom-vs %{ + ... +}% + +CCProgram prefilter-fs %{ + ... +}% + +``` + +然后我们添加了一个渲染目标,用于存储这个渲染通道的结果。 +我们还添加了场景颜色作为输入,绑定到了effect中的`inputTexture`。 + +```typescript +prefilterPass.addRenderTarget( + this._bloomTexNames[0], + LoadOp.CLEAR, + StoreOp.STORE, + this._clearColorTransparentBlack, +); +prefilterPass.addTexture(radianceName, 'inputTexture'); +``` + +effect中,我们定义了`inputTexture`,用于接收输入的场景颜色,并且标注了它的频率为`pass`。 + +对于UBO、Texture、Buffer,可以通过`#pragma rate`指令标注频率,控制资源所在的`DescriptorSet`。 + +如果频率为`pass`,Texture、Buffer可以作为输出,状态会被RenderGraph跟踪。不标注的话,为默认频率,多用于材质参数。 + +```txt +CCProgram prefilter-fs %{ + precision highp float; + #include + + in vec2 v_uv; + + #pragma rate BloomUBO pass + uniform BloomUBO { + mediump vec4 bloomParams;// x: useHDRIlluminance z: threshold, w: enableAlphaMask + }; + + #pragma rate inputTexture pass + uniform sampler2D inputTexture; // hdr input + + layout(location = 0) out vec4 fragColor; + + float luminance(vec3 color) { + return dot(color, vec3(0.3, 0.5, 0.2)); + } + + void main() { + vec4 color = texture(inputTexture, v_uv); + + float contribute = step(bloomParams.z, luminance(color.rgb)); + contribute *= mix(1.0, step(253.0 / 255.0, color.a), bloomParams.w); + + fragColor = vec4(color.xyz * contribute, 1.0); + } +}% +``` + +如果UBO设置的频率为`pass`,我们可以通过直接在渲染通道中设置Uniform的值,来传递参数。 + +```typescript +prefilterPass.setVec4('g_platform', this._configs.platform); +prefilterPass.setVec4('bloomParams', this._bloomParams); +``` + +最后我们通过`addQueue`,将这个渲染通道添加到队列中。由于effect没有开启Blend,我们将`QueueHint`设置为`OPAQUE`。`QueueHint`目前仅用于查错,不影响实际渲染。 + +最后通过`addFullscreenQuad`绘制全屏四边形,这里使用了`bloomMaterial`的第0个pass,也就是prefilter。 + +```typescript +prefilterPass + .addQueue(QueueHint.OPAQUE) + .addFullscreenQuad(bloomMaterial, 0); +``` + +对于`Downsample`、`Upsample`、`Combine`等Pass,也是类似的流程。 + +## 资源状态跟踪 + +从上述例子我们可以看出,我们设置渲染通道时,只需要关注资源的名字,不需要关心资源的状态。 + +`RenderGraph`会根据资源的名字,自动跟踪资源的状态,保证资源的正确使用。 + +在渲染通道之间,我们会根据资源绑定的ShaderStage、读写状态、资源类型等,自动插入`Barrier`,进行同步、缓存的刷新、以及资源布局的转换。 + +这样,我们就可以专注于渲染效果的实现,而不用关心资源的状态管理。 diff --git a/versions/4.0/zh/scripting/access-node-component.md b/versions/4.0/zh/scripting/access-node-component.md new file mode 100644 index 0000000000..68cacfaece --- /dev/null +++ b/versions/4.0/zh/scripting/access-node-component.md @@ -0,0 +1,251 @@ +# 访问节点和组件 + +你可以在 **属性检查器** 里修改节点和组件,也能在脚本中动态修改。动态修改的好处是能够在一段时间内连续地修改属性、过渡属性,实现渐变效果。脚本还能够响应玩家输入,能够修改、创建和销毁节点或组件,实现各种各样的游戏逻辑。要实现这些效果,你需要先在脚本中获得你要修改的节点或组件。 + +在本篇教程,我们将介绍如何 + +- 获得组件所在的节点 +- 获得其它组件 +- 使用 **属性检查器** 设置节点和组件 +- 查找子节点 +- 全局节点查找 +- 访问已有变量里的值 + +## 获得组件所在的节点 + +获得组件所在的节点很简单,只要在组件方法里访问 `this.node` 变量: + +```ts +start() { + let node = this.node; + node.setPosition(0.0, 0.0, 0.0); +} +``` + +## 获得其它组件 + +如果你经常需要获得同一个节点上的其它组件,这就要用到 `getComponent` 这个 API,它会帮你查找你要的组件。 + +```ts +import { _decorator, Component, Label } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + private label: any = null + + start() { + this.label = this.getComponent(Label); + let text = this.name + 'started'; + // Change the text in Label Component + this.label.string = text; + } +} +``` + +你也可以为 `getComponent` 传入一个类名。对用户定义的组件而言,类名就是脚本的文件名,并且 **区分大小写**。例如 "SinRotate.ts" 里声明的组件,类名就是 "SinRotate"。 + +```ts +let rotate = this.getComponent("SinRotate"); +``` + +在节点上也有一个 `getComponent` 方法,它们的作用是一样的: + +```ts +start() { + console.log( this.node.getComponent(Label) === this.getComponent(Label) ); // true +} +``` + +如果在节点上找不到你要的组件,`getComponent` 将返回 null,如果你尝试访问 null 的值,将会在运行时抛出 "TypeError" 这个错误。因此如果你不确定组件是否存在,请记得判断一下: + +```ts +import { _decorator, Component, Label } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + private label: any = null; + + start() { + this.label = this.getComponent(Label); + if (this.label) { + this.label.string = "Hello"; + } else { + console.error("Something wrong?"); + } + } +} +``` + +## 获得其它节点及其组件 + +仅仅能访问节点自己的组件通常是不够的,脚本通常还需要进行多个节点之间的交互。例如,一门自动瞄准玩家的大炮,就需要不断获取玩家的最新位置。Cocos Creator 提供了一些不同的方法来获得其它节点或组件。 + +### 利用属性检查器设置节点 + +最直接的方式就是在 **属性检查器** 中设置你需要的对象。以节点为例,这只需要在脚本中声明一个 type 为 `Node` 的属性: + +```ts +// Cannon.ts + +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("Cannon") +export class Cannon extends Component { + // 声明 Player 属性 + @property({ type: Node }) + private player = null; +} + +``` + +这段代码在 `properties` 里面声明了一个 `player` 属性,默认值为 null,并且指定它的对象类型为 `Node`。这就相当于在其它语言里声明了 `public Node player = null;`。脚本编译之后,这个组件在 **属性检查器** 中看起来是这样的: + +![player-in-inspector-null](access-node-component/player-in-inspector-null.png) + +接着你就可以将层级管理器上的任意一个节点拖到这个 Player 控件: + +![player-in-inspector](access-node-component/player-in-inspector.png) + +这样一来它的 player 属性就会被设置成功,你可以直接在脚本里访问 player: + +```ts +// Cannon.ts + +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("Cannon") +export class Cannon extends Component { + + @property({ type: Node }) + private player = null; + + start() { + console.log('The player is ' + this.player.name); + } +} +``` + +### 利用属性检查器设置组件 + +在上面的例子中,如果你将属性的 type 声明为 Player 组件,当你拖动节点 "Player Node" 到 **属性检查器**,player 属性就会被设置为这个节点里面的 Player 组件。这样你就不需要再自己调用 `getComponent` 啦。 + +```ts +// Cannon.ts + +import { _decorator, Component } from 'cc'; +const { ccclass, property } = _decorator; +import { Player } from "Player"; + +@ccclass("Cannon") +export class Cannon extends Component { + @property({ type: Player }) + private player = null; + + start() { + let PlayerComp = this.player; + } +} +``` + +你还可以将属性的默认值由 `null` 改为数组 `[]`,这样你就能在 **属性检查器** 中同时设置多个对象。
+不过如果需要在运行时动态获取其它对象,还需要用到下面介绍的查找方法。 + +### 查找子节点 + +有时候,游戏场景中会有很多个相同类型的对象,像是炮塔、敌人和特效,它们通常都有一个全局的脚本来统一管理。如果用 **属性检查器** 来一个一个将它们关联到这个脚本上,那工作就会很繁琐。为了更好地统一管理这些对象,我们可以把它们放到一个统一的父物体下,然后通过父物体来获得所有的子物体: + +```ts +// CannonManager.ts + +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("CannonManager") +export class CannonManager extends Component { + + start() { + let cannons = this.node.children; + //... + } + +} +``` + +你还可以使用 `getChildByName`: + +```ts +this.node.getChildByName("Cannon 01"); +``` + +如果子节点的层次较深,你还可以使用 `find`,`find` 将根据传入的路径进行逐级查找: + +```ts +find("Cannon 01/Barrel/SFX", this.node); +``` + +### 全局名字查找 + +当 `find` 只传入第一个参数时,将从场景根节点开始逐级查找: + +```ts +this.backNode = find("Canvas/Menu/Back"); +``` + +## 访问已有变量里的值 + +如果你已经在一个地方保存了节点或组件的引用,你也可以直接访问它们 + +### 通过模块访问 + +你可以使用 `import` 来实现脚本的跨文件操作,让我们看个示例: + +```ts +// Global.ts, now the filename matters +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("Global") +export class Global extends Component { + + public static backNode: any = null; + public static backLabel: any = null; +} +``` + +每个脚本都能用 `import{ } from` + 文件名(不含路径) 来获取到对方 exports 的对象。 + +```ts +// Back.ts +import { _decorator, Component, Node, Label } from 'cc'; +const { ccclass, property } = _decorator; +// this feels more safe since you know where the object comes from +import{Global}from "./Global"; + +@ccclass("Back") +export class Back extends Component { + onLoad() { + Global.backNode = this.node; + Global.backLabel = this.getComponent(Label); + } +} +``` + +```ts +// AnyScript.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; +// this feels more safe since you know where the object comes from +import{Global}from "./Global"; + +@ccclass("AnyScript") +export class AnyScript extends Component { + start () { + const text = "Back"; + Global.backLabel.string = text; + } +} +``` diff --git a/versions/4.0/zh/scripting/access-node-component/player-in-inspector-null.png b/versions/4.0/zh/scripting/access-node-component/player-in-inspector-null.png new file mode 100644 index 0000000000..74441fb7ef Binary files /dev/null and b/versions/4.0/zh/scripting/access-node-component/player-in-inspector-null.png differ diff --git a/versions/4.0/zh/scripting/access-node-component/player-in-inspector.png b/versions/4.0/zh/scripting/access-node-component/player-in-inspector.png new file mode 100644 index 0000000000..244e220a0c Binary files /dev/null and b/versions/4.0/zh/scripting/access-node-component/player-in-inspector.png differ diff --git a/versions/4.0/zh/scripting/basic-node-api.md b/versions/4.0/zh/scripting/basic-node-api.md new file mode 100644 index 0000000000..992956aa80 --- /dev/null +++ b/versions/4.0/zh/scripting/basic-node-api.md @@ -0,0 +1,98 @@ +# 常用节点和组件接口 + +在通过 [访问节点和组件](access-node-component.md) 介绍的方法获取到节点或组件实例后,这篇文章将会介绍通过节点和组件实例可以通过哪些常用接口实现我们需要的种种效果和操作。这一篇也可以认为是 [Node](%__APIDOC__%/zh/class/Node) 和 [Component](__APIDOC__/zh/class/Component) 类的 API 阅读指南,可以配合 API 一起学习理解。 + +## 节点状态和层级操作 + +假设我们在一个组件脚本中,通过 `this.node` 访问当前脚本所在节点。 + +### 激活/关闭节点 + +节点默认是激活的,我们可以在代码中设置它的激活状态,方法是设置节点的 `active` 属性: + +`this.node.active = false;` + +设置 `active` 属性和在编辑器中切换节点的激活、关闭状态,效果是一样的。当一个节点是关闭状态时,它的所有组件都将被禁用。同时,它所有子节点,以及子节点上的组件也会跟着被禁用。要注意的是,子节点被禁用时,并不会改变它们的 `active` 属性,因此当父节点重新激活的时候它们就会回到原来的状态。 + +也就是说,`active` 表示的其实是该节点 **自身的** 激活状态,而这个节点 **当前** 是否可被激活则取决于它的父节点。并且如果它不在当前场景中,它也无法被激活。我们可以通过节点上的只读属性 `activeInHierarchy` 来判断它当前是否已经激活。 + +`this.node.active = true;` + +若节点原先就处于 **可被激活** 状态,修改 `active` 为 true 就会立即触发激活操作: + +- 在场景中重新激活该节点和节点下所有 active 为 true 的子节点 +- 该节点和所有子节点上的所有组件都会被启用,它们中的 `update` 方法之后每帧会执行 +- 这些组件上如果有 `onEnable` 方法,这些方法将被执行 + +`this.node.active = false;` + +如该节点原先就已经被激活,修改 `active` 为 false 就会立即触发关闭操作: + +- 在场景中隐藏该节点和节点下的所有子节点 +- 该节点和所有子节点上的所有组件都将被禁用,也就是不会再执行这些组件中的 `update` 中的代码 +- 这些组件上如果有 `onDisable` 方法,这些方法将被执行 + +### 更改节点的父节点 + +假设父节点为 `parentNode`,子节点为 `this.node`,您可以: + +```ts +this.node.parent = parentNode; +``` + +或 + +```ts +this.node.removeFromParent(); +parentNode.addChild(this.node); +``` + +这两种方法是等价的。 + +**注意**: +- 在 v3.0 之前 的版本中`removeFromParent` 通常需要传入一个 `false`,否则默认会清空节点上绑定的事件和 action 等。v3.0 之后不再需要该参数。 +- 通过 [创建和销毁节点](create-destroy.md) 介绍的方法创建出新节点后,要为节点设置一个父节点才能正确完成节点的初始化。 + +### 索引节点的子节点 + +- `this.node.children`:返回节点的所有子节点数组。 +- `this.node.children.length`:返回节点的子节点数量。 + +**注意**:以上两个 API 都只会返回节点的直接子节点,不会返回子节点的子节点。 + +## 更改节点的变换(位置、旋转、缩放) + +### 更改节点位置 + +有以下两种方法: + +1. 使用 `setPosition` 方法: + + - `this.node.setPosition(100, 50, 100);` + - `this.node.setPosition(new Vec3(100, 50, 100));` + +2. 设置 `position` 变量: + + `this.node.position = new Vec3(100, 50, 100);` + +### 更改节点旋转 + +`this.node.setRotation(90, 90, 90);` + +或通过欧拉角设置本地旋转: + +`this.node.setRotationFromEuler(90, 90, 90);` + +### 更改节点缩放 + +`this.node.setScale(2, 2, 2);` + +## 常用组件接口 + +`Component` 是所有组件的基类,任何组件都包括如下的常见接口(假设我们在该组件的脚本中,以 this 指代本组件): + +- `this.node`:该组件所属的节点实例 +- `this.enabled`:是否每帧执行该组件的 `update` 方法,同时也用来控制渲染组件是否显示 +- `update(deltaTime: number)`:作为组件的成员方法,在组件的 `enabled` 属性为 `true` 时,其中的代码会每帧执行 +- `onLoad()`:组件所在节点进行初始化时(节点添加到节点树时)执行 +- `start()`:会在该组件第一次 `update` 之前执行,通常用于需要在所有组件的 `onLoad` 初始化完毕后执行的逻辑 diff --git a/versions/4.0/zh/scripting/basic.md b/versions/4.0/zh/scripting/basic.md new file mode 100644 index 0000000000..a74c8d2d34 --- /dev/null +++ b/versions/4.0/zh/scripting/basic.md @@ -0,0 +1,46 @@ +# 运行环境 + +Cocos Creator 3.0 引擎的 API 都存在模块 `cc` 中,使用标准的 ES6 模块导入语法将其导入: + +```ts +import { + Component, // 导入类 Component + _decorator, // 导入命名空间 _decorator + Vec3 // 导入类 Vec3 +} from 'cc'; +import * as modules from 'cc'; // 将整个 Cocos Creator 模块导入为命名空间 Cocos Creator + +@_decorator.ccclass("MyComponent") +export class MyComponent extends Component { + public v = new Vec3(); +} +``` + +## 保留标识符 cc + +注意,由于历史原因,`cc` 是 Cocos Creator 3.0 保留使用的标识符,其行为 **相当于** 在任何模块顶部定义了名为 cc 的对象。因此,开发者不应该将 `cc` 用作任何 **全局对象** 的名称: + +```ts +/* const cc = {}; // 每个 Cocos Creator 脚本都等价于在此处含有隐式定义 */ + +import * as modules from 'cc'; // 错误:命名空间导入名称 cc 由 Cocos Creator 保留使用 + +const cc = { + x: 0 +}; +console.log(cc.x); // 错误:全局对象名称 cc 由 Cocos Creator 保留使用 + +function f () { + const cc = { + x: 0 + }; + console.log(cc.x); // 正确:cc 可以用作局部对象的名称 + + const o = { + cc: 0 + }; + console.log(o.cc); // 正确:cc 可以用作属性名 +} + +console.log(cc, typeof cc); // 错误:行为是未定义的 +``` diff --git a/versions/4.0/zh/scripting/coding-setup.md b/versions/4.0/zh/scripting/coding-setup.md new file mode 100644 index 0000000000..b7515b072d --- /dev/null +++ b/versions/4.0/zh/scripting/coding-setup.md @@ -0,0 +1,163 @@ +# 配置代码编辑环境 + +在 Cocos Creator **偏好设置** 面板中,可以指定 [默认脚本编辑器](../editor/preferences/index.md#%E5%A4%96%E9%83%A8%E7%A8%8B%E5%BA%8F)。 + +在 **资源管理器** 中双击脚本文件时,会使用指定的代码编辑器打开代码。 + +我们推荐大家使用 Visual Studio Code,或者基于 Visual Studio Code 的其他编辑器产品作为代码编辑器。 + +## 安装 VS Code + +[Visual Studio Code](https://code.visualstudio.com/) (以下简称 VS Code)是微软推出的轻量化跨平台 IDE,支持 Windows、Mac、Linux 平台,安装和配置非常简单。使用 VS Code 管理和编辑项目脚本代码,可以轻松实现语法高亮、智能代码提示、网页调试等功能。 + +前往 VS Code 的 [官方网站](https://code.visualstudio.com/),点击首页的下载链接即可下载。 + +MacOS 用户解压下载包后双击 **Visual Studio Code** 即可运行。 + +Windows 用户下载后运行 **VSCodeUserSetup.exe** 按提示完成安装即可运行。 + +安装完成后,在**偏号设置**面板中指定默认脚本编辑器即可。 + +> 如果只是日常开发,那么代码编辑环境到这一步就算配置好了。本文后续内容可等有需要的时候再来配置。 + +## 智能提示数据 + +Cocos Creator 3.x 在创建项目时,项目目录下会自动生成一个 [tsconfig.json](tsconfig.md) 文件,里面配置了一个代码提示用的目录文件路径,用 VS Code 打开项目编写代码时便会自动提示 Cocos Creator 引擎 API。若项目升级,引擎 API 也会自动更新。 + +启动 VS Code 后选择主菜单的 **File -> Open Folder...**,在弹出的对话框中选择项目根目录,也就是 `assets`、`project.json` 所在的路径。然后新建一个脚本,或者打开原有的脚本进行编辑时,就会有语法提示了。 + +![vs code](coding-setup/vscode.png) + +> **注意**:当使用自定义引擎,或者切换使用内置引擎/自定义引擎时,若出现 API 智能提示不更新的情况,请执行以下步骤以获得最新的代码智能提示: +> +> 1. 删除自定义引擎中 `bin` 目录(内置引擎是 `resources\resources\3d\engine\bin` 目录)下的 `cache` 文件夹 +> 2. 在引擎目录下执行 `npm run build-declaration` + +## 设置文件显示和搜索过滤 + +在 VS Code 的主菜单中选择 **文件(Windows)/Code(Mac)-> 首选项 -> 设置**,或者选择左下角 ![gear.png](coding-setup/gear.png) 中的 **Setting** 选项,这个操作会打开用户配置文件 **USER SETTINGS**: + +![vs code](coding-setup/vscode-setting.png) + +此时在上方的搜索框中输入 **exclude** 搜索,找到 **Files: Exclude** 和 **Search: Exclude** 模块: + +![vs code](coding-setup/vscode-exclude.png) + +然后点击 **添加模式** 补充以下内容中缺少的部分: + +```json +{ + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.meta": true, + "library/": true, + "local/": true, + "temp/": true + }, + + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "build/": true, + "temp/": true, + "library/": true, + "**/*.anim": true + } +} +``` + +以上字段将为 VS Code 设置搜索时排除的目录,以及在文件列表中隐藏的文件类型。由于 `build`、`temp`、`library` 都是编辑器运行时自动生成的路径,而且会包含我们写入的脚本内容,所以应该在搜索中排除。而 `assets` 目录下的每个文件都会生成一个 `.meta` 文件,一般来说我们不需要关心它的内容,只要让编辑器帮我们管理这些文件就可以了。 + +## VS Code 扩展使用 + +Cocos Creator 在顶部菜单栏的 **开发者 -> VS Code 工作流** 中集成了 **添加编译任务** 和 **添加 Chrome Debug 配置** 功能,以便更好地辅助开发: + +![vscode workflow](coding-setup/vscode-workflow.png) + +- **添加编译任务**:用于在 VS Code 中触发 Creator 的脚本编译,详情请参考下文 **使用 VS Code 激活脚本编译** 部分的内容。 + +- **添加 Chrome Debug 配置**:用于调试网页版游戏,详情请参考下文 **使用 VS Code 调试网页版游戏** 部分的内容。 + +### 使用 VS Code 激活脚本编译 + +使用外部脚本编辑器修改项目脚本后,需要返回 Cocos Creator 以触发脚本编译。
+我们在 Creator 中提供了 **添加编译任务** 功能,通过一个预览服务器的 API 向特定地址发送请求来激活 Creator 的编译,这样在外部脚本编辑器修改了项目脚本后,执行 **编译任务** 便可触发脚本编译,不需要返回 Cocos Creator。 + +#### 安装 cURL + +首先需要确保操作系统中可以运行 [cURL 命令](https://curl.haxx.se/),如果在 Windows 操作系统的命令行中运行 `curl` 提示找不到命令,则需要先安装 curl 到操作系统: + +- 前往 + +- 点击下图箭头所示的控件,完成人机身份验证(若无法正常显示控件,请科学上网) + + ![curl download](coding-setup/curl-download.png) + +- 点击 `curl-7.46.0-win64.exe` 开始下载并安装 + +安装时请使用默认设置,安装完成后可以打开一个命令行窗口,输入 `curl`,如果提示 `curl: try 'curl --help' or 'curl --manual' for more information` 就表示安装成功了。 + +#### 添加 VS Code 编译任务 + +要在 VS Code 中激活脚本编译,需要执行以下步骤: + +1. 在 Creator 顶部菜单栏点击 **开发者 -> VS Code Workflow -> 添加编译任务**,该操作会在项目目录的 `.vscode` 文件夹下添加 `tasks.json` 任务配置文件。 + + ![task.json](coding-setup/tasks-json.png) + +2. 在 VS Code 里按下快捷键 Cmd/Ctrl + P,激活 **快速打开** 输入框,然后输入 `task CocosCreator compile`,选择 `CocosCreator compile`。 + + ![task compile](coding-setup/task-compile.png) + + 然后选择输出类型: + + ![task compile](coding-setup/run-task.png) + +3. 任务运行完成,会在 VS Code 窗口下方的输出面板中显示结果(根据 VS Code 版本及配置的不同,输出结果也会有所差异)。 + +这样之后在 VS Code 编辑脚本完成后,执行第 2 个步骤便可触发 Creator 的脚本编译,不需要返回 Creator。 + +VS Code 还可以为编译任务配置快捷键,在主菜单中选择 **文件(Windows)/Code(Mac)-> 首选项 -> 键盘快捷方式**,或者选择左下角 ![gear.png](coding-setup/gear.png) 中的 **键盘快捷方式** 选项,这个操作会打开快捷键配置文件。然后根据需要修改编译任务的快捷键,例如下图将其设置成了 Cmd/Ctrl + Shift + B: + +![set compile](coding-setup/set-compile.png) + +之后在 VS Code 中按下快捷键 Cmd/Ctrl + Shift + B 便会自动显示 `CocosCreator compile`,不需要手动搜索。 + +更多关于 VS Code 中配置和执行任务的信息,请参考 [Integrate with External Tools via Tasks](https://code.visualstudio.com/docs/editor/tasks) 文档。 + +### 使用 VS Code 调试网页版游戏 + +VS Code 有着优秀的调试能力,我们可以直接在源码工程中调试网页版游戏程序。 + +首先需要安装: + +- [Chrome(谷歌浏览器)](https://www.google.com/chrome/) + +- VS Code 插件: + + - Debugger for Chrome + + 点击 VS Code 左侧导航栏的 **扩展** 按钮打开扩展面板,在搜索框中输入 **Debugger for Chrome** 并点击安装。安装之后可能需要重启 VS Code 才能生效。 + + ![debugger for chrome](coding-setup/debugger-for-chrome.png) + + - JavaScript Debugger:开发者也可在 VS Code 的 **扩展** 内搜索 **JavaScript Debugger** 以启用新的调试插件 + + ![javascript debugger](coding-setup/javascript-debugger.png) + +接下来在 Cocos Creator 顶部菜单栏中点击 **开发者 -> VS Code 工作流 -> 添加 Chrome Debug 配置**,这个菜单命令会在项目文件夹下添加一个 `.vscode/launch.json` 文件作为调试器的配置: + +![launch.json](coding-setup/launch-json.png) + +之后便可以在 VS Code 中点击左侧栏的 **调试** 按钮打开调试面板,并在最上方的调试配置中选择 `Cocos Creator Launch Chrome against localhost`,然后点击左侧绿色的开始按钮进行调试。 + +![debug chrome](coding-setup/debug-chrome.png) + +调试的时候依赖 Cocos Creator 编辑器内置的 Web 服务器,所以需要在编辑器启动状态下才能进行调试。如果编辑器预览游戏时使用的端口不是默认端口,则需要手动修改 `launch.json` 里的 `url` 字段,将正确的端口添加上去。 + +调试过程中可以在源码文件上直接下断点,进行监控,是比使用 Chrome 内置的 DevTools 调试更方便和友好的工作流程。 + +## 学习 VS Code 的使用方法 + +前往 [VS Code 官网文档](https://code.visualstudio.com/Docs),了解从编辑功能操作、个性化定制、语法高亮设置到插件扩展等各方面的使用方法。 diff --git a/versions/4.0/zh/scripting/coding-setup/curl-download.png b/versions/4.0/zh/scripting/coding-setup/curl-download.png new file mode 100644 index 0000000000..e56a6085c7 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/curl-download.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/debug-chrome.png b/versions/4.0/zh/scripting/coding-setup/debug-chrome.png new file mode 100644 index 0000000000..550097ab9a Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/debug-chrome.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/debugger-for-chrome.png b/versions/4.0/zh/scripting/coding-setup/debugger-for-chrome.png new file mode 100644 index 0000000000..814ff4d533 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/debugger-for-chrome.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/gear.png b/versions/4.0/zh/scripting/coding-setup/gear.png new file mode 100644 index 0000000000..69a8056f44 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/gear.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/javascript-debugger.png b/versions/4.0/zh/scripting/coding-setup/javascript-debugger.png new file mode 100644 index 0000000000..222a73533a Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/javascript-debugger.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/launch-json.png b/versions/4.0/zh/scripting/coding-setup/launch-json.png new file mode 100644 index 0000000000..f3ea1b9ad0 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/launch-json.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/run-task.png b/versions/4.0/zh/scripting/coding-setup/run-task.png new file mode 100644 index 0000000000..7fd0ed5d22 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/run-task.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/set-compile.png b/versions/4.0/zh/scripting/coding-setup/set-compile.png new file mode 100644 index 0000000000..3098c72484 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/set-compile.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/task-compile.png b/versions/4.0/zh/scripting/coding-setup/task-compile.png new file mode 100644 index 0000000000..66da8ea244 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/task-compile.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/tasks-json.png b/versions/4.0/zh/scripting/coding-setup/tasks-json.png new file mode 100644 index 0000000000..52bc59aff6 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/tasks-json.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/vscode-edit.png b/versions/4.0/zh/scripting/coding-setup/vscode-edit.png new file mode 100644 index 0000000000..04efa7f5db Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/vscode-edit.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/vscode-exclude.png b/versions/4.0/zh/scripting/coding-setup/vscode-exclude.png new file mode 100644 index 0000000000..ffdca14360 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/vscode-exclude.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/vscode-setting.png b/versions/4.0/zh/scripting/coding-setup/vscode-setting.png new file mode 100644 index 0000000000..0b061340c1 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/vscode-setting.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/vscode-workflow.png b/versions/4.0/zh/scripting/coding-setup/vscode-workflow.png new file mode 100644 index 0000000000..de7c6bd4d0 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/vscode-workflow.png differ diff --git a/versions/4.0/zh/scripting/coding-setup/vscode.png b/versions/4.0/zh/scripting/coding-setup/vscode.png new file mode 100644 index 0000000000..562464f358 Binary files /dev/null and b/versions/4.0/zh/scripting/coding-setup/vscode.png differ diff --git a/versions/4.0/zh/scripting/component.md b/versions/4.0/zh/scripting/component.md new file mode 100644 index 0000000000..051a86a326 --- /dev/null +++ b/versions/4.0/zh/scripting/component.md @@ -0,0 +1,216 @@ +# 组件和组件执行顺序 + +所有继承自 [Component](%__APIDOC__%/zh/class/Component) 的类都称为组件类,其对象称为组件,实现了 Cocos Creator 3.0 EC 系统中的组件概念。 + +**组件类必须是 cc 类**。 + +```ts +import { Component } from 'cc'; + +@ccclass("MyComponent") +class MyComponent extends Component { + +} +``` + +## 组件的创建和销毁 + +组件的生命周期完全由节点操控。与普通类对象不同,组件不能由构造函数创建: + +```ts +const component = new MyComponent(); // 错误:组件无法由构造函数创建 +``` + +相反地,组件必须由节点来创建,通过如下方法将组件添加到节点上: + +```ts +const myComponent = node.addComponent(MyComponent); +``` + +当组件不再被需要的时候,可以调用 `node.removeComponent(myComponent)` 移除指定的组件并将其销毁。 + +```ts +import { Component } from 'cc'; + +@ccclass("MyComponent") +class MyComponent extends Component { + constructor () { + console.log(this.node.name); // 错误:组件并未附加到节点上 + } + + public printNodeName () { + console.log(this.node.name); + } +} +``` + +```ts +const myComponent = node.addComponent(MyComponent); +myComponent.printNodeName(); // 正确 +node.removeComponent(myComponent); +myComponent.printNodeName(); // 错误:组件已被该节点移除 +``` + +## 组件执行顺序 + +### 使用统一的控制脚本来初始化其他脚本 + +项目中一般会有一个像 `Game.ts` 这样的脚本作为总的控制脚本,而其余脚本,像 `Configuration.ts`、`GameData.ts` 和 `Menu.ts` 三个组件,如果要在 `Game.ts` 里初始化,那么它们的初始化过程是这样的: + +```ts +// Game.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +import { Configuration } from './Configuration'; +import { GameData } from './GameData'; +import { Menu }from './Menu'; + +@ccclass("Game") +export class Game extends Component { + private configuration = Configuration; + private gameData = GameData; + private menu = Menu; + + onLoad () { + this.configuration.init(); + this.gameData.init(); + this.menu.init(); + } +} + +``` + +其中在 `Configuration.ts`、`GameData.ts` 和 `Menu.ts` 中需要实现 `init` 方法,并将初始化逻辑放进去。这样就可以保证 Configuration、GameData 和 Menu 的初始化顺序。 + +### 在 update 中用自定义方法控制更新顺序 + +同理如果要保证以上三个脚本的每帧更新顺序,也可以将分散在每个脚本里的 `update` 替换成自己定义的方法: + +```ts + //Configuration.ts + static updateConfig (deltaTime: number) { + + } +``` + +然后在 `Game.ts` 脚本的 `update` 里调用这些方法: + +```ts + // Game.ts + update (deltaTime: number) { + this.configuration.updateConfig(deltaTime); + this.gameData.updateData(deltaTime); + this.menu.updateMenu(deltaTime); + } +``` + +### 控制同一个节点上的组件执行顺序 + +在同一个节点上的组件执行顺序,可以通过组件在 **属性检查器** 里的排列顺序来控制,排列在上的组件会先于排列在下的组件执行。可以通过组件右上角的齿轮按钮里的 `Move Up` 和 `Move Down` 菜单来调整组件的排列顺序,即执行顺序。 + +假如有两个组件 CompA 和 CompB,它们的内容分别是: + +```ts +// CompA.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("CompA") +export class CompA extends Component { + + onLoad () { + console.log('CompA onLoad!'); + } + + start () { + console.log('CompA start!'); + } + + update (deltaTime: number) { + console.log('CompA update!'); + } +} + + +// CompB.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("CompB") +export class CompB extends Component { + + onLoad () { + console.log('CompB onLoad!'); + } + + start () { + console.log('CompB start!'); + } + + update (deltaTime: number) { + console.log('CompB update!'); + } +} + +``` + +组件顺序 CompA 在 CompB 上面时,输出: + +``` +CompA onLoad! +CompB onLoad! +CompA start! +CompB start! +CompA update! +CompB update! +``` + +在 **属性检查器** 里通过 CompA 组件右上角设置菜单里的 `Move Down` 将 CompA 移到 CompB 下面后,输出: + +``` +CompB onLoad! +CompA onLoad! +CompB start! +CompA start! +CompB update! +CompA update! +``` + +### 设置组件执行优先级 + +如果以上方法还是不能提供所需的控制粒度,还可以直接设置组件的 **executionOrder**。executionOrder 会影响组件生命周期回调执行的优先级。executionOrder 越小,该组件相对其它组件就会越先执行。executionOrder 默认为 0,因此设置为负数的话,就会在其它默认的组件之前执行。设置方法如下: + +```ts +//Configuration.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, executionOrder } = _decorator; + +@ccclass("Configuration") +@executionOrder(-1) +export class Configuration extends Component { + + onLoad () { + console.log('Configuration onLoad!'); + } +} +``` + +```ts +// Menu.ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, executionOrder } = _decorator; + +@ccclass("Menu") +@executionOrder(1) +export class Menu extends Component { + + onLoad () { + console.log('Menu onLoad!'); + } +} +``` + +通过如上方法设置,Configuration.ts 的 onLoad 会在 Menu.ts 的 onLoad 方法之前执行。 + +**注意**:executionOrder 只对 `onLoad`、`onEnable`、`start`、`update` 和 `lateUpdate` 有效,对 `onDisable` 和 `onDestroy` 无效。 diff --git a/versions/4.0/zh/scripting/create-destroy.md b/versions/4.0/zh/scripting/create-destroy.md new file mode 100644 index 0000000000..73eca80686 --- /dev/null +++ b/versions/4.0/zh/scripting/create-destroy.md @@ -0,0 +1,113 @@ +# 创建和销毁节点 + +## 创建新节点 + +除了通过场景编辑器创建节点外,我们也可以在脚本中动态创建节点。 + +以下是一个简单的例子: + +```typescript +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + start(){ + let node =new Node('box'); + node.setPosition(0,0,-10); + } +} +``` + +需要注意的是,在上述的示例中通过 `new Node` 创建出来的节点并不会主动添加到场景内,直至用户调用 `director.getScene().addChild(node)` 来添加到场景内或者通过 `node.parent = {a valid node}` 来作为某个节点的子节点。 + +## 克隆已有节点 + +有时我们希望动态的克隆场景中的已有节点,我们可以通过 `instantiate` 方法完成。使用方法如下: + +```typescript +import { _decorator, Component, Node,instantiate, director } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type:Node}) + private target: Node = null; + + start(){ + let scene = director.getScene(); + let node = instantiate(this.target); + + scene.addChild(node); + node.setPosition(0, 0,-10); + } +} +``` + +## 创建预制节点 + +和克隆已有节点相似,你可以设置一个预制([Prefab](..\asset\prefab.md))并通过 `instantiate` 生成节点。使用方法如下: + +```typescript +import { _decorator, Component, Prefab, instantiate, director } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type:Prefab}) + private target: Prefab = null; + + start(){ + let scene = director.getScene(); + let node = instantiate(this.target); + + scene.addChild(node); + node.setPosition(0,0,0); + } +} +``` + +需要注意的是,在使用上述的代码时,需将持有该代码的脚本挂在某个节点上,并在 [属性检查器](../editor/inspector/index.md) 内配置好 `target` 的值。 + +## 销毁节点 + +通过 `node.destroy()` 函数,可以销毁节点。值得一提的是,销毁节点并不会立刻被移除,而是在当前帧逻辑更新结束后,统一执行。当一个节点销毁后,该节点就处于无效状态,可以通过 `isValid` 判断当前节点是否已经被销毁。 + +使用方法如下: + +```typescript +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type:Node}) + private target: Node = null; + + private positionz: number = -20; + + start(){ + // 5秒后销毁节点 + setTimeout(function () { + this.target.destroy(); + }.bind(this), 5000); + } + update(deltaTime: number){ + console.info(this.target.isValid); + this.positionz += 1*deltaTime; + if (this.target.isValid) { + this.target.setPosition(0.0,0.0,this.positionz); + } + } +} +``` + +### destroy 和 removeFromParent 的区别 + +调用一个节点的 `removeFromParent` 后,它并不会从内存中释放,因为引擎内部仍会持有它的数据。**因此如果一个节点不再使用,请直接调用它的 `destroy` 而不是 `removeFromParent`,否则会导致内存泄漏。** + +总之,如果一个节点不再使用,`destroy` 就对了,不需要 `removeFromParent` 也不需要设置 `parent` 为 `null` 哈。 diff --git a/versions/4.0/zh/scripting/decorator-group.png b/versions/4.0/zh/scripting/decorator-group.png new file mode 100644 index 0000000000..90d2be2e2f Binary files /dev/null and b/versions/4.0/zh/scripting/decorator-group.png differ diff --git a/versions/4.0/zh/scripting/decorator-group2.png b/versions/4.0/zh/scripting/decorator-group2.png new file mode 100644 index 0000000000..03af05d6a8 Binary files /dev/null and b/versions/4.0/zh/scripting/decorator-group2.png differ diff --git a/versions/4.0/zh/scripting/decorator.md b/versions/4.0/zh/scripting/decorator.md new file mode 100644 index 0000000000..b44251120a --- /dev/null +++ b/versions/4.0/zh/scripting/decorator.md @@ -0,0 +1,431 @@ +# 装饰器使用 + +## cc 类 + +将装饰器 `ccclass` 应用在类上时,此类称为 cc 类。cc 类注入了额外的信息以控制 Cocos Creator 对该类对象的序列化、编辑器对该类对象的展示等。因此,未声明 `ccclass` 的组件类,也无法作为组件添加到节点上。 + +`ccclass` 装饰器的参数 `name` 指定了 cc 类的名称,cc 类名是 **独一无二** 的,这意味着即便在不同目录下的同名类也是不允许的。当需要获取相应的 cc 类时,可以通过其 cc 类名来查找,例如: + +- 序列化。若对象是 cc 类对象,则在序列化时将记录该对象的 cc 类名,反序列化时将根据此名称找到相应的 cc 类进行序列化。 + +- 当 cc 类是组件类时,`Node` 可以通过组件类的 cc 类名查找该组件。 + +> **注意**:类名不应该以 `cc.`、`internal.` 作为前缀,这是 Cocos Creator 的保留类名前缀。 + +```ts +@ccclass('Example') +export class Example extends Component { +} +``` + +## 组件类装饰器 + +此类装饰器是只能用来修饰 `Component` 的子类。 + +### executeInEditMode + +默认情况下,所有组件都只会在运行时执行,也就是说它们的生命周期回调在编辑器模式下并不会触发。`executeInEditMode` 允许当前组件在编辑器模式下运行,默认值为 `false`。 + +```ts +const { ccclass, executeInEditMode } = _decorator; + +@ccclass('Example') +@executeInEditMode(true) +export class Example extends Component { + update (dt: number) { + // 会在编辑器下每帧执行 + } +} +``` + +### requireComponent + +`requireComponent` 参数用来指定当前组件的依赖组件,默认值为 `null`。当组件添加到节点上时,如果依赖的组件不存在,引擎会自动将依赖组件添加到同一个节点,防止脚本出错。该选项在运行时同样有效。 + +```ts +const { ccclass, requireComponent } = _decorator; + +@ccclass('Example') +@requireComponent(Sprite) +export class Example extends Component { +} +``` + +### executionOrder + +`executionOrder` 用来指定脚本生命周期回调的执行优先级。小于 0 的脚本将优先执行,大于 0 的脚本将最后执行。排序方式如下: + +- 对于同一节点上的不同组件,数值小的先执行,数值相同的按组件添加先后顺序执行 +- 对于不同节点上的同一组件,按节点树排列决定执行的先后顺序 + +该优先级设定只对 `onLoad`、`onEnable`、`start`、`update` 和 `lateUpdate` 有效,对 `onDisable` 和 `onDestroy` 无效。 + +```ts +const { ccclass, executionOrder } = _decorator; + +@ccclass('Example') +@executionOrder(3) +export class Example extends Component { +} +``` + +### disallowMultiple + +同一节点上只允许添加一个同类型(含子类)的组件,防止逻辑发生冲突,默认值为 false。 + +```ts +const { ccclass, disallowMultiple } = _decorator; + +@ccclass('Example') +@disallowMultiple(true) +export class Example extends Component { +} +``` + +### menu + +`@menu(path)` 用来将当前组件添加到组件菜单中,方便用户查找。 + +需要注意该菜单是添加在 **属性检查器** 面板中按下添加组件按钮后的下拉框内。 + +```ts +const { ccclass, menu } = _decorator; + +@ccclass('Example') +@menu('foo/bar') +export class Example extends Component { +} +``` + +![menu](./menu.png) + +### help + +指定当前组件的帮助文档的 URL。设置完成后,在 **属性检查器** 中就会出现一个帮助图标,点击即可打开指定的网页。 + +```ts +const { ccclass, help } = _decorator; + +@ccclass('Example') +@help('https://docs.cocos.com/creator/3.5/manual/zh/scripting/decorator.html') +export class Example extends Component { +} +``` + +## 属性装饰器 + +属性装饰器 `property` 可以被应用在 cc 类的属性或访问器上。属性装饰器用于控制 Cocos Creator 编辑器中对该属性的序列化、**属性检查器** 中对该属性的展示等。 + +属性装饰器的各种特性是通过 `@property()` 的参数来指定的。完整可选择参数可以参考:[属性参数](./reference/attributes.md) + +property 装饰器写法参考如下: + +```ts +@property({ + type: Node, + visible: true, +}) +targetNode: Node | null = null; +``` + +接着,下方会罗列出一些常用属性参数写法。 + +### type 参数 + +选项 `type` 指定了属性的 cc 类型。可以通过以下几种形式的参数指定类型: + +- 基础属性类型 + + `CCInteger`、`CCFloat`、`CCBoolean`、`CCString` 是基础属性类型标识,一般仅用于数组属性的内部类型声明。非数组类型不需要显式声明这些类型。 + + - `CCInteger` 声明类型为 **整数** + - `CCFloat` 声明类型为 **浮点数** + - `CCString` 声明类型为 **字符串** + - `CCBoolean` 声明类型为 **布尔值** + +- 其他 cc 类型 + + 所有的 cc 类型 **都需要显式指定**,否则编辑器无法正确识别类型,序列化也无法写入正确类型。 + +- 数组类型 + + 当使用基础属性类型或者 cc 类作为数组元素时,可以被通过数组类型声明被编辑器所识别。例如 `[CCInteger]`、`[Node]` 将分别以整数数组和节点数组的形式在 **属性检查器** 中展示。 + +若属性未指定类型,Cocos Creator 将从属性的默认值或初始化式的求值结果推导其类型: + +- 若值的类型是 JavaScript 原始类型 `number`、`string`、`boolean`,则其类型分别对应 Creator 的`CCFloat`、`CCString` 和 `CCBoolean`。 +- 其他情况下属性的类型则是 **未定义** 的,编辑器上会提示 `Type(Unknown)` 字样。 + +> **注意**:当声明 JavaScript 内置构造函数 `Number`、`String`、`Boolean` 用作类型时将给出警告,并且将分别视为 cc 类型中的 `CCFloat`、`CCString`、`CCBoolean`。已经初始化的数组属性修改类型后,需要手动清除掉原来的数组数据,重新赋值,否则会因为数据类型不一致,导致数据错乱。 +> +> ![property-changed](property-changed.png) + + + +> **注意**:需要在编辑器 **属性检查器** 中展示的属性,属性名开头不应该带 `_`,否则会识别为 private 属性,private 属性不会在编辑器组件属性面板上显示。 + +下列代码演示了不同 cc 类型的属性声明: + +```ts +import { _decorator, CCInteger, Node, Enum } from 'cc'; +const { ccclass, property, integer, float, type } = _decorator; + +enum A { + c, + d +} +Enum(A); + +@ccclass +class MyClass { + @property // JavaScript 原始类型,根据默认值自动识别为 Creator 的浮点数类型。 + index = 0; + + @property(Node) // 声明属性 cc 类型为 Node。当属性参数只有 type 时可这么写,等价于 @property({type: Node}) + targetNode: Node | null = null; // 等价于 targetNode: Node = null!; + + // 声明属性 children 的 cc 类型为 Node 数组 + @property({ + type: [Node] + }) + children: Node[] = []; + + @property({ + type: String, + }) // 警告:不应该使用构造函数 String。等价于 CCString。也可以选择不声明类型 + text = ''; + + @property + children2 = []; // 未声明 cc 类型,从初始化式的求值结果推断元素为未定义的数组 + + @property + _valueB = 'abc'; // 此处 '_' 开头的属性,只序列化,不会在编辑器属性面板显示 + + @property({ type: A }) + accx : A = A.c; +} +``` + +为了方便,额外提供几种装饰器以快速声明 cc 类型。如果你只需要为属性定义 type 参数,那么可以直接使用下列装饰器替代 `@property`: + +| 装饰器 | 对应的 property 写法 | +| :-------- | :---------------- | +| @type(t) | @property(t) | +| @integer | @property(CCInteger) | +| @float | @property(CCFloat) | + +```ts +import { _decorator, CCInteger, Node } from 'cc'; +const { ccclass, property, integer, float, type } = _decorator; +@ccclass +class MyClass { + @integer // 声明属性的 cc 类型为整数 + index = 0; + + @type([Node]) // 声明属性 children 的 cc 类型为 Node 数组 + children: Node[] = []; + + @type(String) // 警告:不应该使用构造函数 String。等价于 CCString。也可以选择不声明类型 + text = ''; + // JavaScript 原始类型 `number`、`string`、`boolean` 通常可以不用声明 + // 可以直接写 + @property + text = ''; +} +``` + + + + + + + +### visible + +一般情况下,属性是否显示在 **属性检查器** 中取决于属性名是否以 `_` 开头。**如果是以 `_` 开头,则不显示**。 + +如果要强制显示在 **属性检查器** 中,可以设置 `visible` 参数为 true: + +```typescript +@property({ visible: true }) +private _num = 0; +``` + +如果要强制隐藏,可以设置 `visible` 参数为 false: + +```typescript +@property({ visible: false }) +num = 0; +``` + +### serializable + +属性默认情况下都会被序列化,序列化后就会将编辑器中设置好的属性值保存到场景等资源文件中,之后在加载场景时就会自动还原成设置好的属性值。如果不想序列化,可以设置 `serializable: false`。 + +```typescript +@property({ serializable: false }) +num = 0; +``` + +### override + +所有属性都会被子类继承,如果子类要覆盖父类同名属性,需要显式设置 override 参数,否则会有重名警告: + +```typescript +@property({ tooltip: "my id", override: true }) +id = ""; +``` + +### group + +当脚本中定义的属性过多且杂时,可通过 `group` 对属性进行分组、排序,方便管理。同时还支持对组内属性进行分类。 + +`group` 写法包括以下两种: + +- `@property({ group: { name } })` + +- `@property({ group: { id, name, displayOrder, style } })` + +| 参数 | 说明 | +| :--- | :--- | +| `id` | 分组 ID,`string` 类型,是属性分组组号的唯一标识,默认为 `default`。 | +| `name` | 组内属性分类的名称,`string` 类型。 | +| `displayOrder` | 对分组进行排序,`number` 类型,数字越小,排序越靠前。默认为 `Infinity`,表示排在最后面。
若存在多个未设置的分组,则以在脚本中声明的先后顺序进行排序 | +| `style` | 分组样式,目前支持 **tab** 和 **section** 样式。 默认为 **tab**。| + +示例脚本如下: + +```ts +import { _decorator, Component, Label, Sprite } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('SayHello') +export class SayHello extends Component { + + // 分组一 + // 组内名为 “bar” 的属性分类,其中包含一个名为 label 的 Label 属性 + @property({ group: { name: 'bar' }, type: Label }) + label: Label = null!; + // 组内名为 “foo” 的属性分类,其中包含一个名为 sprite 的 Sprite 属性 + @property({ group: { name: 'foo' }, type: Sprite }) + sprite: Sprite = null!; + + // 分组二 + // 组内名为 “bar2” 的属性分类,其中包含名为 label2 的 Label 属性和名为 sprite2 的 Sprite 属性,并且指定排序为 1。 + @property({ group: { name: 'bar2', id: '2', displayOrder: 1 }, type: Label }) + label2: Label = null!; + @property({ group: { name: 'bar2', id: '2' }, type: Sprite }) + sprite2: Sprite = null!; + +} +``` + +将该脚本挂载到节点上,则在 **属性检查器** 中显示为: + +![decorator-group](decorator-group.png) + +因为分组一未指定 `displayOrder`,分组二指定了 `displayOrder` 为 1,所以分组二会排在分组一的前面。 + +若需要对分组内的属性排序,也可以使用 `displayOrder`。以分组二为例,目前是按照在脚本中定义的先后顺序进行排序,label2 在 sprite2 的前面。我们将其调整为: + +```ts +// 分组二 +// 组内名为 “bar” 的属性分类,其中包含名为 label2 的 Label 属性和名为 sprite2 的 Sprite 属性,并且指定排序为 1。 +@property({ group: { name: 'bar2', id: '2', displayOrder: 1 }, displayOrder: 2, type: Label }) +label2: Label = null!; +@property({ group: { name: 'bar2', id: '2' }, displayOrder: 1, type: Sprite }) +sprite2: Sprite = null!; +``` + +回到编辑器,在 **属性检查器** 中可以看到 sprite2 已经排在 label2 的前面了: + +![decorator-group](decorator-group2.png) + +## 内置类型界面 + +针对一些常用类型,引擎内部提供了默认界面,开发者可以根据自身的需求使用: + +- 颜色界面: + + ![color](./decorator/color.png) + + 代码示例如下: + + ```ts + @property(Color) + color:Color + ``` + +- 曲线:用于保存曲线类型、样式以及采样数据。 + + ![color](./decorator/curve.png) + + 代码示例如下: + + ```ts + @property(RealCurve) + realCurve:RealCurve = new RealCurve(); + ``` + +- 曲线范围:可以通过常量、曲线、双曲线或双常量进行控制。 + + ![color](./decorator/curve_range.png) + + 代码示例如下: + + ```ts + @property(CurveRange) + curveRang : CurveRange = new CurveRange(); + ``` + +- 渐变色:记录渐变色的关键值和用于计算渐变色的结果 + + ![graduebt](./decorator/gradient.png) + + 代码示例如下: + + ```ts + @property(Gradient) + gradient = new Gradient(); + ``` + +- 渐变色范围:通过颜色、渐变色、双颜色或双渐变色控制颜色 + + ![graduebt](./decorator/gradient_range.png) + + 代码示例如下: + + ```ts + @property(GradientRange) + gradientRange:GradientRange = new GradientRange(); + ``` + +## 参考链接 + +- [属性参数](./reference/attributes.md) +- [脚本进阶](./reference-class.md) diff --git a/versions/4.0/zh/scripting/decorator/color.png b/versions/4.0/zh/scripting/decorator/color.png new file mode 100644 index 0000000000..7687ae15b1 Binary files /dev/null and b/versions/4.0/zh/scripting/decorator/color.png differ diff --git a/versions/4.0/zh/scripting/decorator/curve.png b/versions/4.0/zh/scripting/decorator/curve.png new file mode 100644 index 0000000000..c1eeecff50 Binary files /dev/null and b/versions/4.0/zh/scripting/decorator/curve.png differ diff --git a/versions/4.0/zh/scripting/decorator/curve_range.png b/versions/4.0/zh/scripting/decorator/curve_range.png new file mode 100644 index 0000000000..caf7505f02 Binary files /dev/null and b/versions/4.0/zh/scripting/decorator/curve_range.png differ diff --git a/versions/4.0/zh/scripting/decorator/gradient.png b/versions/4.0/zh/scripting/decorator/gradient.png new file mode 100644 index 0000000000..46081d87a0 Binary files /dev/null and b/versions/4.0/zh/scripting/decorator/gradient.png differ diff --git a/versions/4.0/zh/scripting/decorator/gradient_range.png b/versions/4.0/zh/scripting/decorator/gradient_range.png new file mode 100644 index 0000000000..07b3ee2320 Binary files /dev/null and b/versions/4.0/zh/scripting/decorator/gradient_range.png differ diff --git a/versions/4.0/zh/scripting/external-scripts.md b/versions/4.0/zh/scripting/external-scripts.md new file mode 100644 index 0000000000..bf2fb824a8 --- /dev/null +++ b/versions/4.0/zh/scripting/external-scripts.md @@ -0,0 +1,75 @@ +# 外部代码支持 + +> **注意**:Cocos Creator 3.x 推荐使用模块代替插件脚本的使用! + +## 插件脚本 + +当脚本资源导入到 **资源管理器** 后,在 **属性检查器** 中设置了 **导入为插件**,此脚本资源便称为 **插件脚本**。插件脚本通常用于引入第三方库。目前仅支持 JavaScript 插件脚本。 + +![import as plugin](plugin-scripts/import-as-plugin.png) + +与项目中的其它脚本不同,Creator 不会修改插件脚本的内容,但可能会插入一些代码以适配 Creator。特别地,Creator 将屏蔽全局变量 `module`、`exports`、`define`。 + +### 导入选项 + +许多第三方 JavaScript 库以全局变量的方式提供库的功能,这些库往往会写入全局变量 `window`、`global`、`self` 和 `this` 中,但这些全局变量不一定是跨平台的。为了方便,Creator 在导入插件脚本时,提供了 **全局变量别名** 选项,开启后,Creator 将插入必要的代码以模拟这些全局变量,其效果类似于: + +```js +(function() { + const window = globalThis; + const global = globalThis; + const self = globalThis; + + (function() { + /* 原始代码 */ + }).call(this); + +}).call(this); +``` + +默认会模拟常见的全局变量,如果是特殊的全局变量别名,可以在输入框内添加对应的变量名称。 + +### 执行时机 + +#### 执行环境 + +开发者可以控制插件脚本在某些环境下是否执行。 + +| 选项 | 影响平台 | 备注 | +| :-------- | :----------- | :----------- | +| 允许 Web 平台加载 | 浏览器、网页预览、编辑器 | 默认启用,禁用时会连带 **允许编辑器加载** 一起禁用 | +| 允许编辑器加载 | 编辑器 | 默认禁用,如果编辑器中的其它普通脚本加载过程中会依赖当前脚本,则需要 **手动开启** 这个选项。
开启后,脚本内不在任何函数内声明的局部变量 **不会** 暴露成全局变量,所以全局变量需要用 `window.abc = 0` 的方式定义才能生效。 | +| 允许 Native 平台加载 | 原生平台、模拟器预览 | 默认启用 | +| 允许小游戏平台加载 | 小游戏平台 | 默认启用 | + +#### 执行顺序 + +插件脚本默认将会在引擎启动后,项目脚本加载前执行。插件脚本之间的顺序默认按照插件脚本本身的命名来排序。自 3.8.3 起可以在项目设置内指定插件脚本的优先级顺序,指定了优先级顺序的脚本会在默认排序后再次调整脚本的位置使其符合指定的顺序要求。 + +> 具体项目设置的修改交互方式可以参考 [项目设置文档](../editor/project/index.md#插件脚本排序) + +![sort plugin script](plugin-scripts/sort-plugin.png) + +例如假设有插件脚本名称分别为:`1,2,3,4,5,6,7,8`, 默认的排序顺序将会从 1 - 8 排序,如果此时项目设置内指定了部分脚本的优先级顺序为:`8,3,1,7,5`,则最终的排序结果为:`8,3,1,2,4,7,5,6`。 + +### 可用性与跨平台 + +插件脚本几乎会原封不动地拷贝到构建目录,因此插件脚本的可用性与跨平台性不受 Creator 保障。例如,当插件脚本使用了某些平台不支持的语言特性时将导致错误: + +- **目标平台不提供原生 node.js 支持** + + 例如很多 [npm](https://www.npmjs.com/) 模块都直接或间接依赖于 `node.js`,这样的话发布到原生或网页平台后是不能用的。 + +- **依赖 DOM API 的插件将无法发布到原生平台** + + 网页中可以使用大量的前端插件,例如 jQuery,不过它们有可能依赖于浏览器的 DOM API。依赖这些 API 的插件不能用于原生平台中。 + +### 交互 + +插件脚本与非插件脚本无法以导入形式交互。举例来说,即使开发者知道其目标平台实际支持 CommonJS,也不能在非插件脚本中强行通过 `require` 相对路径进行使用。 + +因此,插件脚本一般以全局变量的方式(又称 IIFE 模块格式)来通讯,但需要注意以下几点: + +- 谨慎使用全局变量,当开发者要用全局变量时,应该清楚自己在做什么,我们并不推荐滥用全局变量,即使要用也最好保证全局变量为 **只读** 状态。 +- 添加全局变量时,请小心不要和系统已有的全局变量重名。 +- 开发者可以在插件脚本中自由封装或者扩展 Cocos Creator 引擎,但这会提高团队沟通成本,导致脚本难以复用。 diff --git a/versions/4.0/zh/scripting/index.md b/versions/4.0/zh/scripting/index.md new file mode 100644 index 0000000000..89531bd84f --- /dev/null +++ b/versions/4.0/zh/scripting/index.md @@ -0,0 +1,22 @@ +# 脚本指南及事件机制 + +Cocos Creator 脚本用于实现用户定义的(游戏)行为,支持 JavaScript 和 TypeScript 两种编程语言。通过编写脚本组件,并将它挂载到场景节点中来驱动场景中的物体。 + +在组件脚本的编写过程中,开发者可以通过声明属性,将脚本中需要调节的变量映射到 **属性检查器** 中,以便策划和美术进行调整。与此同时,也可以通过注册特定的回调函数,来帮助初始化、更新甚至销毁节点。 + +## 内容 + +- [编程语言支持](./language-support.md) +- [脚本基础](./script-basics.md) +- [脚本使用](./usage.md) +- [脚本进阶](./reference-class.md) +- [事件系统](../engine/event/index.md) +- [模块规范与示例](./modules/index.md) +- [插件脚本](./external-scripts.md) +- [WASM 支持](./wasm-support.md) +- [只读属性](./readonly.md) + +## 更多参考 + +- [添加引擎内 Log 信息](./log.md) +- [推荐编码规范](./reference/coding-standards.md) diff --git a/versions/4.0/zh/scripting/language-support.md b/versions/4.0/zh/scripting/language-support.md new file mode 100644 index 0000000000..27ab72f4d1 --- /dev/null +++ b/versions/4.0/zh/scripting/language-support.md @@ -0,0 +1,120 @@ +# 语言支持 + +Cocos Creator 支持 **TypeScript** 和 **JavaScript** 两种编程语言。但需要注意的是,**JavaScript** 只支持以 [插件脚本](external-scripts.md) 的形式导入使用。 + +## TypeScript + +Cocos Creator 支持 TypeScript 4.1.0。在此基础上,做了以下限制: + +- `tsconfig.json` 不会被读取。每个项目都隐含着如下选项: + + ```json5 + { + "compilerOptions": { + "target": "ES2015", + "module": "ES2015", + "isolatedModules": true, + "experimentalDecorators": true, + "moduleResolution": /* Cocos Creator 特定的模块解析算法 */, + "forceConsistentCasingInFileNames": true, + } + } + ``` + + 隐含的 `isolatedModules` 选项意味着: + - 不支持 [const enums](https://www.typescriptlang.org/docs/handbook/enums.html#const-enums)。 + + - 重导出 TypeScript 类型和接口时应该使用 `export type`。例如使用 `export type { Foo } from './foo';` 而不是 `export { Foo } from './foo';`。 + +- 不支持 `export =` 和 `import =`。 + +- 命名空间导出的变量必须声明为 `const`,而不是 `var` 或 `let`。 + +- 同一命名空间的不同声明不会共享作用域,需要显式使用限定符。 + +- 编译过程中的类型错误将被忽略。 + +编译时不会读取 `tsconfig.json`,意味着 `tsconfig.json` 的编译选项并不会影响编译。 + +开发者仍然可以在项目中使用 `tsconfig.json` 以配合 IDE 实现类型检查等功能。为了让 IDE 的 TypeScript 检查功能和 Creator 行为兼容,开发者需要额外注意一些事项,详情可参考 [tsconfig](./tsconfig.md)。 + +### TypeScript 参考教程 + +- [Cocos Creator 3.0 TypeScript 问题答疑及经验分享](https://forum.cocos.org/t/topic/106995) +- [TypeScript 官方网站](https://www.typescriptlang.org/) +- [TypeScript - Classes](https://www.typescriptlang.org/docs/handbook/classes.html) +- [TypeScript - Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) +- [TypeScript - DefinitelyTyped](http://definitelytyped.org/) +- [X 分钟速成 TypeScript](https://learnxinyminutes.com/docs/zh-cn/typescript-cn/) +- [TypeScript 源码](https://github.com/Microsoft/TypeScript) +- [开发者回避使用 TypeScript 的三个借口 — 以及应当使用 TypeScript 的更有说服力的原因](https://mp.weixin.qq.com/s/7QQJxErt2-e4jLK2_4GUFA) + +## JavaScript + +### 语言特性 + +Creator 支持的 JavaScript 语言规范为 ES6。 + +此外,以下几项更新于 ES6 规范的语言特性或提案仍旧在支持之列: + +- [类字段](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes/Class_elements) +- [Promise 对象](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise) +- [可选链操作符 `?.`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Optional_chaining) +- [空值合并操作符 `??`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) +- 逻辑赋值操作符 + - [逻辑空赋值操作符 `??=`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Logical_nullish_assignment) + - [逻辑与赋值操作符 `&&=`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Logical_AND_assignment) + - [逻辑或赋值操作符 `||=`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Logical_OR_assignment) +- [全局对象 `globalThis`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/globalThis) + +以下语言特性同样支持,但需要开启相关的编译选项: + +- [异步函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function) + +特别地,Creator 目前支持 **Legacy** 装饰器提案,其具体用法和含义请参考 [babel-plugin-proposal-decorators](https://babeljs.io/docs/en/babel-plugin-proposal-decorators)。由于该 [提案](https://github.com/tc39/proposal-decorators) 仍处于阶段 2,引擎暴露的所有装饰器相关功能接口都在以下划线开头的 `_decorator` 命名空间下。 + +### 编译选项 + +Creator 开放了部分编译选项,这些选项将应用到整个项目。 + +| 选项 | 名称 | 含义 | +| :-- | :--- | :-- | +| useDefineForClassFields | 符合规范的类字段 | 当开启时,将使用 `Define` 语义实现类字段,否则将使用 `Set` 语义实现类字段。仅当目标不支持 ES6 类字段时生效。 | +| allowDeclareFields | 允许声明类字段 | 当开启时,在 TypeScript 脚本中将允许使用 `declare` 关键字来声明类字段,并且,当字段未以 `declare` 声明且未指定显式的初始化式时,将依照规范初始化为 `undefined`。 | + +### 运行环境 + +从用户的角度来说,Creator 未绑定任何 JavaScript 实现,因此建议开发者严格依照 JavaScript 规范编写脚本,以获取更好的跨平台支持。 + +举例来说,当希望使用 **全局对象** 时,应当使用标准特性 `globalThis`: + +```js +globalThis.blahBlah // 任何环境下 globalThis 一定存在 +``` + +而非 `window`、`global`、`self` 或 `this`: + +```js +typeof window // 可能是 'undefined' +typeof global // 在浏览器环境下可能是 'undefined' +``` + +再如,Creator 未提供 **CommonJS** 的模块系统,因此以下代码片段会带来问题: + +```js +const blah = require('./blah-blah'); // 错误,require 是未定义的 +module.exports = blah; // 错误 module 是未定义的 +``` + +反之,应使用标准模块语法: + +```js +import blah from './blah-blah'; +export default blah; +``` + +### JavaScript 参考教程 + +- [JavaScript 标准参考教程](https://wangdoc.com/javascript/) +- [JavaScript 秘密花园](https://bonsaiden.github.io/JavaScript-Garden/zh/) +- [JavaScript 内存详解 & 分析指南](https://mp.weixin.qq.com/s/EuJzQajlU8rpZprWkXbJVg) diff --git a/versions/4.0/zh/scripting/life-cycle-callbacks.md b/versions/4.0/zh/scripting/life-cycle-callbacks.md new file mode 100644 index 0000000000..e226de2f1c --- /dev/null +++ b/versions/4.0/zh/scripting/life-cycle-callbacks.md @@ -0,0 +1,111 @@ + +# 生命周期回调 + +Cocos Creator 为组件脚本提供了生命周期的回调函数。开发者只需要定义特定的回调函数,Creator 就会在特定的时期自动执行相关脚本,开发者不需要手工调用它们。 + +目前提供给开发者的生命周期回调函数主要有(按生命周期触发先后排列): + +- onLoad +- onEnable +- start +- update +- lateUpdate +- onDisable +- onDestroy + +## onLoad + +组件脚本的初始化阶段,我们提供了 `onLoad` 回调函数。`onLoad` 回调会在节点首次激活时触发,比如所在的场景被载入,或者所在节点被激活的情况下。在 `onLoad` 阶段,保证了你可以获取到场景中的其他节点,以及节点关联的资源数据。onLoad 总是会在任何 start 方法调用前执行,这能用于安排脚本的初始化顺序。通常我们会在 `onLoad` 阶段去做一些初始化相关的操作。例如: + +```ts +import { _decorator, Component, Node, SpriteFrame, find } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + @property({type:SpriteFrame}) + bulletSprite=null; + @property({type:Node}) + gun=null; + + _bulletRect=null; + + onLoad(){ + this._bulletRect=this.bulletSprite.getRect(); + this.gun = find('hand/weapon', this.node); + } +} +``` + +## onEnable + +当组件的 `enabled` 属性从 `false` 变为 `true` 时,或者所在节点的 `active` 属性从 `false` 变为 `true` 时,会激活 `onEnable` 回调。倘若节点第一次被创建且 `enabled` 为 `true`,则会在 `onLoad` 之后,`start` 之前被调用。 + +## start + +`start` 回调函数会在组件第一次激活前,也就是第一次执行 `update` 之前触发。`start` 通常用于初始化一些中间状态的数据,这些数据可能在 update 时会发生改变,并且被频繁的 enable 和 disable。 + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("starttest") +export class starttest extends Component { + + private _timer: number = 0.0; + + start () { + this._timer = 1.0; + } + + update (deltaTime: number) { + this._timer += deltaTime; + if(this._timer >= 10.0){ + console.log('I am done!'); + this.enabled = false; + } + } +} +``` + +## update + +游戏开发的一个关键点是在每一帧渲染前更新物体的行为,状态和方位。这些更新操作通常都放在 `update` 回调中。 + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("updatetest") +export class updatetest extends Component { + + update (deltaTime: number) { + this.node.setPosition(0.0,40.0*deltaTime,0.0); + } +} +``` + +## lateUpdate + +`update` 会在所有动画更新前执行,但如果我们要在动效(如动画、粒子、物理等)更新之后才进行一些额外操作,或者希望在所有组件的 `update` 都执行完之后才进行其它操作,那就需要用到 `lateUpdate` 回调。 + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("lateupdatetest") +export class lateupdatetest extends Component { + + lateUpdate (deltaTime: number) { + this.node.setPosition(0.0,50,0.0); + } +} +``` + +## onDisable + +当组件的 `enabled` 属性从 `true` 变为 `false` 时,或者所在节点的 `active` 属性从 `true` 变为 `false` 时,会激活 `onDisable` 回调。 + +## onDestroy + +当组件或者所在节点调用了 `destroy()`,则会调用 `onDestroy` 回调,并在当帧结束时统一回收组件。 diff --git a/versions/4.0/zh/scripting/load-assets.md b/versions/4.0/zh/scripting/load-assets.md new file mode 100644 index 0000000000..4cd6050a8f --- /dev/null +++ b/versions/4.0/zh/scripting/load-assets.md @@ -0,0 +1,78 @@ +# 获取和加载资源 + +Cocos Creator 3.0 采用与 Cocos Creator v2.x 统一的资源管理机制,在本篇教程,我们将介绍: + +- 资源属性的声明 +- 如何在 **属性检查器** 里设置资源 +- 动态加载资源 +- 加载远程资源和设备资源 +- 资源的依赖和释放 + +## 资源属性的声明 + +在 Cocos Creator 中,所有继承自 `Asset` 的类型都统称资源,如 `Texture2D`、`SpriteFrame`、`AnimationClip`、`Prefab` 等。它们的加载是统一并且自动化的,相互依赖的资源能够被自动预加载。 + +> 例如,当引擎在加载场景时,会先自动加载场景关联到的资源,这些资源如果再关联其它资源,其它也会被先被加载,等加载全部完成后,场景加载才会结束。 + +脚本中可以这样定义一个 Asset 属性: + +```typescript +//test.ts + +import { _decorator, Component, Node, SpriteFrame } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type: SpriteFrame}) + private spriteFrame: SpriteFrame = null; +} +``` + +## 如何在属性检查器里设置资源 + +只要在脚本中定义好类型,就能直接在 **属性检查器** 很方便地设置资源。假设我们创建了这样一个脚本: + +```typescript +//test.ts +import { _decorator, Component, Node, SpriteFrame, Texture2D } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("test") +export class test extends Component { + + @property({type: Texture2D}) + private texture: Texture2D = null; + + @property({type: SpriteFrame}) + private spriteFrame: SpriteFrame = null; +} +``` + +将它添加到节点后,在 **属性检查器** 中是这样的: + +![asset-in-properties-null](load-assets/asset-in-inspector-null.png) + +接下来我们从 **资源管理器** 里面分别将一个 Texture 和一个 SpriteFrame 拖到 **属性检查器** 的对应属性中: + +![asset-in-properties-dnd](load-assets/asset-in-inspector-dnd.png) + +结果如下: + +![asset-in-properties-dnd](load-assets/asset-in-inspector.png) + +这样就能在脚本里直接拿到设置好的资源: + +```typescript +start () { + let spriteFrame = this.spriteFrame; + let texture = this.texture; +} +``` + +在 **属性检查器** 里设置资源虽然很直观,但资源只能在场景里预先设好,没办法动态切换。如果需要动态切换,你需要看看下面的内容。 + +## 动态加载 + +关于动态加载,请参考 [动态加载资源](../asset/dynamic-load-resources.md) diff --git a/versions/4.0/zh/scripting/load-assets/asset-in-inspector-dnd.png b/versions/4.0/zh/scripting/load-assets/asset-in-inspector-dnd.png new file mode 100644 index 0000000000..fd5a3de39c Binary files /dev/null and b/versions/4.0/zh/scripting/load-assets/asset-in-inspector-dnd.png differ diff --git a/versions/4.0/zh/scripting/load-assets/asset-in-inspector-null.png b/versions/4.0/zh/scripting/load-assets/asset-in-inspector-null.png new file mode 100644 index 0000000000..33e18309b2 Binary files /dev/null and b/versions/4.0/zh/scripting/load-assets/asset-in-inspector-null.png differ diff --git a/versions/4.0/zh/scripting/load-assets/asset-in-inspector.png b/versions/4.0/zh/scripting/load-assets/asset-in-inspector.png new file mode 100644 index 0000000000..e646cfd0cc Binary files /dev/null and b/versions/4.0/zh/scripting/load-assets/asset-in-inspector.png differ diff --git a/versions/4.0/zh/scripting/log.md b/versions/4.0/zh/scripting/log.md new file mode 100644 index 0000000000..83e651ed4d --- /dev/null +++ b/versions/4.0/zh/scripting/log.md @@ -0,0 +1,51 @@ +# 在引擎内添加 Log 信息 + +本文档主要说明如何按照正确的规范在引擎内部代码中添加新的 Log 信息(包含 log、warning、error)。 + +## Log 信息机制和背景 + +目前 Cocos Creator 中的 Log 信息是以一个错误信息表形式独立于引擎存储的,具体存储在 engine 目录下的 `EngineErrorMap.md` 中。而在引擎代码中,不允许直接以字符串形式写日志、警告、错误等信息,必须以下面三个 API 来书写: + +``` +import { logID, warnID, errorID } from 'core/platform/debug'; + +logID(id, ...params); +warnID(id, ...params); +errorID(id, ...params); +``` + +这样做的主要目的是减少字符串在引擎源码中所占据的包体。 + +## EngineErrorMap 的编写规范 + +EngineErrorMap 按照一百位来做大模块划分,总共四位,从 0000 到 9900,也就是说支持最多 100 个大模块。十位数是用来划分子模块的,或者也可以直接以连续的形式排列,这个由模块负责人决定。 + +由于历史原因,目前没有按照严格的优先级顺序来做排序,新建的模块可以简单得往后顺延。未来我们会做更好的排序管理和整理。 + +具体错误信息的编写规范如下: + +``` +### 4 number ID + +Message in english. +``` + +比如: + +``` +### 8300 + +Should only one camera exists, please check your project. +``` + +信息中支持使用 `%s`、`%d`、`%f` 这样的参数接收符,运行输出 LOG 时会按照参数顺序依次拼接到信息中。 + +## EngineErrorMap 的维护 + +EngineErrorMap 修改后,如果希望代码中的调用生效,需要在 engine 目录下执行: + +``` +> gulp build-debug-infos +``` + +`EngineErrorMap.md` 的修改也要跟随引擎的其他修改提交到 git。 diff --git a/versions/4.0/zh/scripting/menu.png b/versions/4.0/zh/scripting/menu.png new file mode 100644 index 0000000000..636b0a6329 Binary files /dev/null and b/versions/4.0/zh/scripting/menu.png differ diff --git a/versions/4.0/zh/scripting/modules/config.md b/versions/4.0/zh/scripting/modules/config.md new file mode 100644 index 0000000000..c3e5e9f181 --- /dev/null +++ b/versions/4.0/zh/scripting/modules/config.md @@ -0,0 +1,75 @@ +# 获取 npm 包 + +## 安装 Node.js + +npm 包管理工具 `npm` 附带在 Node.js 发行版中。安装 Node.js 之后即可使用。 + +通过以下命令确认 `npm` 的安装已成功: + +```bash +> npm -v +# 可能的输出: +# 6.14.9 +``` + +## 安装 npm 包 + +在项目根目录中执行以下命令: + +```bash +> npm install --save protobufjs +``` + +会将 npm 包 `protobufjs` 安装到 `/node_modules` 目录中,并将对此包的依赖关系写入到文件 `package.json` 中。 + +`package.json` 文件是 npm 的清单文件,需要纳入版本控制。 + +> Cocos Creator 推荐将自动生成的 `package-lock.json` 也纳入版本控制,以确保多个开发者之间安装相同版本的包。 + +`/node_modules` 目录一般不纳入版本控制。 + +当 `package.json` 记录了依赖之后,后续可直接执行以下命令重新安装或在其它环境中安装: + +```bash +> npm install +``` + +## 拓展:使用 npm 镜像 + +npm 默认从 [官方 npmjs 源](https://www.npmjs.com/) 读取和下载包。有些国家或地区可能因为网络问题导致安装失败或安装速度过慢,推荐通过切换镜像的方式来解决。 + +首先,全局安装 npm 包 [nrm](https://www.npmjs.com/package/nrm): + +```bash +> npm install -g nrm +``` + +> `-g` 表示全局,会将 npm 包直接安装到当前计算机中,只要执行一次后续就不需要再次执行。 + +查看有效的 npm 镜像: + +```bash +> npx nrm ls +# 可能的输出: +# * npm -------- https://registry.npmjs.org/ +# yarn ------- https://registry.yarnpkg.com/ +# cnpm ------- http://r.cnpmjs.org/ +# taobao ----- https://registry.npm.taobao.org/ +# nj --------- https://registry.nodejitsu.com/ +# npmMirror -- https://skimdb.npmjs.com/registry/ +# edunpm ----- http://registry.enpmjs.org/ +``` + +可根据当前地区选择合适的镜像。中国大陆用户使用 `taobao` 镜像是不错的选择。执行以下命令来切换镜像: + +```bash +> npx nrm use taobao # 或任何合适的镜像 +``` + +> 该命令也是全局的。也可选择仅切换当前项目的镜像,见 [nrm 选项](https://www.npmjs.com/package/nrm#usage)。 + +镜像名 `npm` 是官方源的名称,因此,可通过以下命令切换回官方源: + +```bash +> npx nrm use npm +``` \ No newline at end of file diff --git a/versions/4.0/zh/scripting/modules/engine.md b/versions/4.0/zh/scripting/modules/engine.md new file mode 100644 index 0000000000..933a4eccab --- /dev/null +++ b/versions/4.0/zh/scripting/modules/engine.md @@ -0,0 +1,82 @@ +# 引擎模块 + +引擎通过模块向开发者暴露功能接口,模块以 **ECMAScript** 模块形式存在。 + +⚠️ 注意,从 v3.0 开始,将不能通过全局变量 `cc` 访问引擎功能! + +## 功能 + +模块 `'cc'` 提供了所有引擎功能的访问。模块 `'cc'` 的内容是动态的,其内容和 **项目设置** 中的 **功能裁剪** 设置有关。 + +### 引擎日志输出 + +示例: + +```ts +import { log } from 'cc'; +log('Hello world!'); +``` + +## 构建时常量 + +引擎模块 `'cc/env'` 暴露了一些构建时的 **常量**,这些常量代表执行环境、调试级别或平台标识等。 + +由于这些常量都以 `const` 声明,提供了很好的代码优化机会。 + +### 执行环境 + +| 名称(类型都为 `boolean`)| 说明 | +| :-------- | :------------------- | +| `BUILD` | 是否正在构建后的环境中运行 | +| `PREVIEW` | 是否正在预览环境中运行 | +| `EDITOR` | 是否正在编辑器环境中运行 | + +### 调试级别 + +| 名称(类型都为 `boolean`) | 说明 | +| :------ | :------ | +| `DEBUG` | 是否处于调试模式。仅当构建时未勾选调试选项的情况下为 `false`,其它情况下都为 `true` | +| `DEV` | 等价于 `DEBUG`/`EDITOR`/`PREVIEW` | + +### 平台标识 + +下表列出的常量表示是否正在 **某一个** 或 **某一类** 平台上运行,常量的类型都是 `boolean`。 + + +| 名称 | 代表平台 | `MINIGAME` “小游戏” | `RUNTIME_BASED` 基于 Cocos Runtime | `SUPPORT_JIT` 支持 JIT | +| :---------- | :---------- | :----------------- | :----------------- | :----------------- | +| `HTML5` | Web | ❌ | ❌ | ❌ | +| `NATIVE` | 原生平台 | ❌ | ❌ | ❌ | +| `ALIPAY` | 支付宝小游戏 | ✔️ | ❌ | ✔️ | +| `BAIDU` | 百度小游戏 | ✔️ | ❌ | ✔️ | +| `BYTEDANCE` | 抖音小游戏 | ✔️ | ❌ | ✔️ | +| `WECHAT` | 微信小游戏 | ✔️ | ❌ | ✔️ | +| `COCOSPLAY` | Cocos Play | ❌ | ✔️ | ✔️ | +| `HUAWEI` | 华为快游戏 | ❌ | ✔️ | ✔️ | +| `OPPO` | OPPO 小游戏 | ❌ | ✔️ | ✔️ | +| `VIVO` | vivo 小游戏 | ❌ | ✔️ | ✔️ | + +### 调试模式下的输出 + +示例如下: + +```ts +import { log } from 'cc'; +import { DEV } from 'cc/env'; + +if (DEV) { + log('I am in development mode!'); +} +``` + + + + diff --git a/versions/4.0/zh/scripting/modules/example.md b/versions/4.0/zh/scripting/modules/example.md new file mode 100644 index 0000000000..9a92cf4bae --- /dev/null +++ b/versions/4.0/zh/scripting/modules/example.md @@ -0,0 +1,248 @@ +# 示例:外部模块使用案例 + +本章节通过案例讲解如何在 Cocos Creator 项目中使用 npm 模块,如果不知道 npm 该如何获取,请参考 [获取 npm 包](./config.md)。 + +## ESM 与 CJS 交互规则 + +Cocos Creator 3.x 如何使用 npm,最大的问题在于 ESM 与 CJS 模块交互。如果还不了解这两个模块在 Cocos Creator 里是如何定义的,请查看 [模块](./spec.md) 一节。其实,ESM 和 CJS 模块的交互方式在 [Node.js 官方文档](https://nodejs.org/api/esm.html#esm_interoperability_with_commonjs) 就有提到。在这里我简单的概括以下几点: + +- CommonJS 模块由 `module.exports` 导出,在导入 CommonJS 模块时,可以使用 ES 模块默认的导入方式或其对应的 sugar 语法形式导入。 + + ```ts + import { default as cjs } from 'cjs'; + // 语法糖形式 + import cjsSugar from 'cjs'; + console.log(cjs); // + console.log(cjs === cjsSugar); // true + ``` + +- **ESM** 模块的 `default` 导出指向 **CJS** 模块的 `module.exports`。 +- 非 `default` 部分的导出,Node.js 通过静态分析将其作为独立的 ES 模块提供。 + +接下来看一个代码片段: + +```js +// foo.js +module.exports = { + a: 1, + b: 2, +} + +module.exports.c = 3; + +// test.mjs + +// default 指向 module.exports +import foo from './foo.js'; // 等价于 import { default as foo } from './foo.js' +console.log(JSON.stringify(foo)); // {"a":1,"b":2,"c":3} + +// 导入 foo 模块的所有导出 +import * as module_foo from './foo.js' +console.log(JSON.stringify(module_foo)); // {"c":3,"default":{"a":1,"b":2,"c":3}} + +import { a } from './foo.js' +console.log(a); // Error: a is not defined + +// 根据上方第三点,c 有独立导出 +import { c } from './foo.js' +console.log(c); // 3 +``` + +## npm 模块使用案例 + +### 案例一:protobufjs 使用 + +首先,需要获取到 [protobufjs](https://www.npmjs.com/package/protobufjs) 包。在项目目录下打开终端,执行 `npm i protobufjs`。如果这个项目属于多人协作,甚至可以把 `protobufjs` 这个包作为依赖写入 `package.json`,通过在上述命名行里加入 `npm install --save protobufjs` 即可利用命令行自动写入到 `package.json` 中。执行完之后,就可以在项目录下的 `node_module` 文件夹里查找到 `protobufjs` 相关文件夹。 + +![module](./example/module.png) + +有了 `protobufjs` 模块包之后。其次,判断模块格式。 + +- 查看 `package.json` 文件里的 `main` 字段,判定入口文件 `index.js`; +- 查看 `package.json` 文件里的 `type` 字段,观察到没有 `type` 字段; + +鉴别模块格式,可以推断出,这是一个 **CJS** 模块。顺便一提,在包里是能看到每一个 js 文件都对应一个 .d.ts 文件,说明 `protobufjs` 包里自带了 `TypeScript` 声明文件,方便导入 `protobufjs` 模块后可以通过代码提示获取内部接口。 + +接着,在 `index.js` 可以看到它导出写法。 + +```js +"use strict"; +module.exports = require("./src/index"); +``` + +确定了模块格式和导出方式。接下来,就是脚本资源里如何使用 `protobufjs` 这个模块了。 + +首先,在 `assets` 下创建一个 `test.ts` 脚本。接着,在脚本的头部写入下列代码: + +```ts +// 大部分 npm 模块都可以通过直接导入模块名的方式来使用。 +import protobufjs from 'protobufjs'; +console.log(protobufjs); +``` + +在 Chrome 运行后,控制台输出如下: + +![protobufjs-print-default](./example/protobufjs-print-default.png) + +可能有部分同学,在书写 `import protobufjs from 'protobufjs'` 时就已经报红,提示模块没有默认导出(`has no default export`),这是因为 **CJS** 没有 `default` 导出,而 **ESM** 和 **CJS** 交互的时候是将 `module.exports` **视为** `export default`,并不是代表 **CJS** 模块具备真实默认导出。因此,如果要保持原来的写法,可以对 TypeScript 进行配置,具体请查看 [TypeScript 配置](#TypeScript-配置)。 + +接下来,就可以直接使用 `protobufjs` 提供的所有模块了。当然,如果只需要特定子模块功能,例如 `light` 和 `minimal`,可以直接导入包中的子路径。 + +```ts +// 使用 light 版本 +import protobuf from 'protobufjs/light.js'; +// 使用 minimal 版本 +import protobuf from 'protobufjs/minimal.js'; +``` + +> **注意**:就 protobufjs 和许多经典的 npm 包而言,当导入包中的子路径时,后缀是需要的。详情请参考 [Cocos Creator 模块规范](./spec.md)。 + +#### TypeScript 配置 + +当在 TypeScript 里导入没有默认导出的模块时,通常会出现 `Module '"/${project_path}/protobufjs"' has no default export.`。这是因为目前 `protobufjs` 仅提供了 CommonJS 模块,而 Cocos Creator 是通过 “默认导入” 来访问 CommonJS 模块的,但是 CommonJS 模块确实没有 “默认导出” 这种说法。此时,可以通过编辑项目目录中的 `tsconfig.json` 文件,将 `"compilerOptions"` 字段中的 `"allowSyntheticDefaultImports"` 选项设置为 `true`。如果没有该字段,可以自行补上。 + +```json5 +{ + /* Base configuration. Do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + "compilerOptions": { + "allowSyntheticDefaultImports": true, // 需要开启 + } +} +``` + +### 案例二:将 protobuf 中的 proto 文件编译成 JavaScript 文件 + +本节主要讲述如何将 proto 文件编译成 JavaScript 文件。其实在翻阅 protobufjs 文档的时候,可以发现它自身有提供 [命令行工具](https://www.npmjs.com/package/protobufjs#command-line) 转换静态模块以及 ts 声明文件。本次以新建一个 3.0 的空项目 example 为例,演示整个流程。 + +首先,通过 npm 安装 `protobufjs` 并将它写入项目目录下的 `package.json` 的依赖项。 + +其次,在项目目录下新建 Proto 目录并定义几个 proto 文件。 + +```proto +// pkg1.proto + +package pkg1; +syntax = "proto2"; +message Bar { + required int32 bar = 1; +} + +// pkg2.proto + +package pkg2; +syntax = "proto2"; +message Baz { + required int32 baz = 1; +} + +// unpkg.proto 不属于任何的包 + +syntax = "proto2"; +message Foo { + required int32 foo = 1; +} +``` + +接着,在 package.json 中定义。 + +```json +"scripts": { + "build-proto:pbjs": "pbjs --dependency protobufjs/minimal.js --target static-module --wrap commonjs --out ./Proto.js/proto.js ./Proto/*.proto", + "build-proto:pbts": "pbts --main --out ./Proto.js/proto.d.ts ./Proto.js/*.js" +}, +``` + +其中,第一段指令 `build-proto:pbjs` 的大致意思是将 proto 文件编译成 js。`--dependency protobufjs/minimal.js` 这一块参数其实是因为执行时 `require` 到了 `protobufjs`,但是我们需要用的只是它的子模块 `minimal.js`。然后,将 js 生成到 Proto.js 文件夹中(注意:如果没有 Proto.js 文件夹,需手动创建)。第二段指令 `build-proto:pbts` 则是根据第一段的输出来生成类型声明文件。 + +根据以上步骤则成功完成了 proto 文件转换成 js 文件的过程。接着,可以在项目 `assets` 下脚本里引入 js 文件。 + +```ts +import proto from '../Proto.js/proto.js'; +console.log(proto.pkg1.Bar); +``` + +此处还是要声明一下,如果有同学在导入的时候出现报红现象,提示 proto 没有默认导出,解决方案有两种。 + +- 通过向 `tsconfig.json` 增加允许对包含默认导出的模块使用默认导入字段来解决 + + ```json + "compilerOptions": { + "allowSyntheticDefaultImports": true, + } + ``` + +- 增加默认导出 + + 在项目目录下创建一个 Tools/wrap-pbts-result.js 文件,脚本代码如下: + + ```js + const fs = require('fs'); + const ps = require('path'); + const file = ps.join(__dirname, '..', 'Proto.js', 'proto.d.ts'); + const original = fs.readFileSync(file, { encoding: 'utf8' }); + fs.writeFileSync(file, ` + namespace proto { + ${original} + } + export default proto; + `); + ``` + + 将原来的 `build-proto:pbts` 命令改为: + + ```json + "build-proto:pbts": "pbts --main --out ./Proto.js/proto.d.ts ./Proto.js/*.js && node ./Tools/wrap-pbts-result.js" + ``` + +最终,就可以直接运行了。完整项目内容请参考:[npm-case](https://github.com/cocos/cocos-example-projects/tree/v3.8/npm-case)。 + +> **注意**:打包出来的 js 文件即可以放在项目 `assets` 目录下,也可以放在项目其它位置。`assets` 目录下的 js 文件不再需要勾选导出为插件,请各位悉知。 + +### 案例三:lodash-es 使用 + +同 [案例一:protobufjs 使用](#案例一:protobufjs-使用) 方法类似,安装 `lodash-es` 包。得知入口文件是 `lodash.js`,入口文件里也自动帮忙将其下所有子模块以 **ESM** 模块格式导出,再根据 `type` 也印证了当前是 **ESM** 模块。因此,可以直接导入任何模块。还是以 `assets` 下的 `test.ts` 脚本资源为例,引入 `lodash` 内的子模块。 + +```ts +import { array, add } from 'lodash-es'; +``` + +此时,会发现代码层面会报错,但是实际却能够运行。这是因为,两者在语言类型上就有明显区分,JavaScript 是动态类型,TypeScript 是静态类型,因此,在使用 js 脚本的时候,是无法获知导出模块的具体类型的,此时最好的办法就是声明一份类型定义文件 .d.ts。幸运的是,但我们将鼠标移到报错处的时候,有提示可以通过执行 `npm i --save-dev @types/lodash-es` 来安装 `lodash` 模块的类型声明文件。安装完之后,重启 VS Code 就会发现报错消失了,同时还有了代码提示。 + +### 案例四:web3 使用 + +根据上述 `protobufjs` 案例,以同样的方式安装 `web3` 包。利用相同检测方法,判定 `web3` 为 **CJS** 模块。根据同样方式导入: + +```ts +import web3 from 'web3'; +``` + +回到编辑器后发现,还是出现了如下一大堆报错: + +![node-module](./example/node-module.png) + +这是因为该包是专门为 Node 定制的,内部引用了 Node.js 的内置模块,这些模块是无法在 Creator 里使用导致了报错。通常这些包也会兼顾 Web 用户。当我们直接导入该包的时候,包的入口直接指向的是 Node 版本,而把 Web 版本放在包下的 `dist/**.js` 或者 `dist/**.min.js` 文件里。针对这种情况就需要对症下药,导入包里提供的 Web 定制版本: + +```ts +import web3 from 'web3/dist/web3.min.js'; +``` + +这时候发现编辑器不报错了,但是代码提示:`Could not find a declaration file for module 'web3/dist/web3.min.js'. '/${project_path}/node_modules/web3/dist/web3.min.js' implicitly has an 'any' type.`。 + +这是因为即使这些包有类型说明,也是为那个入口的 Node 版本而做的类型说明。一般情况下,两个版本的接口定义是一样的,因此我们只要 “借用” Node 版本的类型说明就好了。就是任意新建个 .d.ts 到项目里,写: + +```ts +// 为模块 "<包名>/dist/**.js" 补充类型说明 +declare module "<包名>/dist/**.js" { + export * from "<包名>"; // “偷” 主入口的类型说明 +} +``` + +> **注意**:是不是每用一个包都得确认它是为谁而做?目前确实是的。但这种情况正在改善,因为在新版的 Node 里,package.json 加入了一个很好的机制来告诉用户什么情况应该使用哪个版本。 + +### 特殊案例:firebase 使用 + +这个案例是一个较为特殊的案例,接着,了解一下这个案例的特殊性。根据上述方式安装好 `firebase` 包,根据 `package.json` 文件对包进行分析,可以得出这个包采用是 **CJS** 格式。根据入口文件,可以推断为此包是为 Node 定制的(这个可以根据 [案例四:web3 使用](#案例四:web3-使用) 方式测试),因此,得选用 Web 定制版本 `index.esm.js`。神奇的地方就在这里,`index.esm.js` 是一个 **ESM** 模块,Creator 里将这个包识别为 **CJS** 模块,但是它又是 **ESM** 模块,自然会导致出错。 + +针对这样的案例,目前还没有处理,建议的解决方案是用户自行通过 `rollup` 等打包工具,打包成一个独立的 js 文件作为 **非 npm 模块使用**。 diff --git a/versions/4.0/zh/scripting/modules/example/MGOBE-no-module.png b/versions/4.0/zh/scripting/modules/example/MGOBE-no-module.png new file mode 100644 index 0000000000..9959604feb Binary files /dev/null and b/versions/4.0/zh/scripting/modules/example/MGOBE-no-module.png differ diff --git a/versions/4.0/zh/scripting/modules/example/module.png b/versions/4.0/zh/scripting/modules/example/module.png new file mode 100644 index 0000000000..a707792598 Binary files /dev/null and b/versions/4.0/zh/scripting/modules/example/module.png differ diff --git a/versions/4.0/zh/scripting/modules/example/node-module.png b/versions/4.0/zh/scripting/modules/example/node-module.png new file mode 100644 index 0000000000..18ab0c6735 Binary files /dev/null and b/versions/4.0/zh/scripting/modules/example/node-module.png differ diff --git a/versions/4.0/zh/scripting/modules/example/protobufjs-print-default.png b/versions/4.0/zh/scripting/modules/example/protobufjs-print-default.png new file mode 100644 index 0000000000..c264d61acd Binary files /dev/null and b/versions/4.0/zh/scripting/modules/example/protobufjs-print-default.png differ diff --git a/versions/4.0/zh/scripting/modules/import-map.md b/versions/4.0/zh/scripting/modules/import-map.md new file mode 100644 index 0000000000..cdd47bf8db --- /dev/null +++ b/versions/4.0/zh/scripting/modules/import-map.md @@ -0,0 +1,94 @@ +# 导入映射(实验性) + +Cocos Creator 从 v3.3 开始实验性支持 [导入映射(Import maps)](https://github.com/WICG/import-maps)。 + +导入映射控制 TypeScript/JavaScript 的导入行为,尤其是可指定对 [裸说明符](./spec.md#裸说明符) 的解析。 + +## 使用 + +通过编辑器顶部菜单栏的 **项目 -> 项目设置 -> 脚本** 中的 **导入映射** 项即可指定导入映射文件的路径。设置完成后,导入映射功能开启,使用的导入映射将从指定的文件中读取。 + +> **注意**:导入映射文件的路径是至关重要的,因为导入映射中的所有相对路径都是相对于导入映射文件本身路径。 + +### 别名映射 + +若有一个模块被项目中所有的模块所使用,而开发者并不希望其他模块以相对路径的方式引用它,而是为它起一个别名,那么便可以选择使用导入映射。 + +例如,某个模块真实的绝对路径为 `<项目>/assets/lib/foo.ts`,我们希望所有模块可以以 `import {} from 'foo';` 的方式来引用它,操作步骤如下: + +首先,在项目目录下创建一个导入映射文件 `import-map.json`: + +```json +// import-map.json + +{ + "imports": { + "foo": "./assets/lib/foo.ts" + } +} +``` + +- `"imports"`:指定应用到所有模块的 **顶级映射**(Top level imports)。 +- `"foo"`:指定我们要映射的模块名。 +- `"./assets/lib/foo.ts"`:指定如何映射 `"foo"`。`"./assets/lib/foo.ts"` 是相对路径,**导入映射中的所有相对路径都是相对于导入映射文件本身的位置的**,因此 `./assets/lib/foo.ts` 将解析为绝对路径 `<项目>/assets/lib/foo.ts`。 + +然后在任意模块中使用以下方式引用模块时,`'foo'` 都将解析为模块 `<项目>/assets/lib/foo.ts`: + +```ts +import * as foo from 'foo'; +``` + +### 目录映射 + +导入映射还允许映射指定目录下的所有模块。 + +例如,要映射项目 `assets/lib/bar-1.2.3` 目录下的所有模块,则导入映射的 json 文件如下所示: + +```json +// import-map.json + +{ + "imports": { + "bar/": "./assets/lib/bar-1.2.3/" + } +} +``` + +除了 `"bar/"` 指定的是我们要映射的目录,其余的与 **别名映射** 一致。 + +这样项目中的模块都能以 `import {} from 'bar/...'` 的形式来引用目录 `bar-1.2.3` 中的模块。 + +例如: + +```ts +import * as baz from 'bar/baz'; +import * as quux from 'bar/qux/quux'; +``` + +`'bar/baz'` 将解析为模块 `<项目>/assets/lib/bar-1.2.3/baz.ts`
`'bar/qux/quux'` 将解析为模块 `<项目>/assets/lib/bar-1.2.3/qux/quux.ts`。 + +### TypeScript 配置 + +TypeScript 并不支持导入映射,在使用时可能会导致出现找不到模块的报错,所以我们需要通过额外的配置来告诉 TypeScript 类型检查器额外的模块解析信息。 + +例如上述中的两个例子,可以在项目目录下的 `tsconfig.json` 文件中配置 [paths](https://www.typescriptlang.org/tsconfig#paths) 字段(若没有该字段,可自行补上),如下所示: + +```json5 +// tsconfig.json +{ + "compilerOptions": { + "paths": { + // 注意:这里的相对路径是相对于 tsconfig.json 所在的路径 + // 由于本例中 tsconfig.json 和 import-map.json 位于同一目录,因此这里的相对路径也相似。 + "foo": ["./assets/lib/foo"], + "bar/*": ["./assets/lib/bar-1.2.3/*"] + } + } +} +``` + +更多关于导入映射的功能,请参考 [导入映射](https://github.com/WICG/import-maps)。 + +## 支持情况 + +Cocos Creator 支持 [Import Maps Draft Community Group Report, 12 January 2021](https://wicg.github.io/import-maps/) 中的所有功能。 diff --git a/versions/4.0/zh/scripting/modules/index.md b/versions/4.0/zh/scripting/modules/index.md new file mode 100644 index 0000000000..9d69ba84a8 --- /dev/null +++ b/versions/4.0/zh/scripting/modules/index.md @@ -0,0 +1,25 @@ +# 模块规范与示例 + +所有的代码文件可以大致分为 [插件脚本](../external-scripts.md) 和 **模块** 两种,该部分内容主要介绍模块相关。 + +**模块** 是 TypeScript/JavaScript 代码的一种组织方式,按照模块组织的代码一般又被非正式地称为 **脚本**/**项目脚本**。在 Cocos Creator 中,除 **插件脚本** 外所有代码都以模块的形式组织,根据来源的不同,大致分为: + +- 项目中创建的代码,包括 **组件脚本** 和 **项目类(非组件)脚本**; + +- 引擎提供的功能,详情请参考 [引擎模块](./engine.md); + +- 第三方模块,例如 npm 模块。详情请参考 [外部模块使用案例](./example.md)。 + +Cocos Creator 原生支持并推荐使用 ECMAScript 模块格式(简称 ESM 模块格式)。为了支持对外部模块的使用,Cocos Creator 也在某种限度上支持了 CommonJS 模块格式。关于 Creator 中模块的格式及使用,详情请参考 [模块规范](./spec.md)。 + +从 Cocos Creator 3.3 开始,支持导入映射(实验性),详情请参考 [导入映射](./import-map.md)。 + +## 模块加载顺序 + +模块加载顺序如下: + +1. 首次导入 Cocos Creator 3.x 的 [引擎模块](./engine.md) `"cc"`。 + +2. 插件脚本:所有插件脚本将按照指定的插件脚本依赖关系顺序执行,不存在依赖关系的插件脚本之间是无序的。详情可参考 [插件脚本](../external-scripts.md)。 + +3. 普通脚本:所有普通脚本将被并发导入。导入时将严格遵循由 `import` 确定的引用关系和执行顺序。 diff --git a/versions/4.0/zh/scripting/modules/spec.md b/versions/4.0/zh/scripting/modules/spec.md new file mode 100644 index 0000000000..adeab0dfc6 --- /dev/null +++ b/versions/4.0/zh/scripting/modules/spec.md @@ -0,0 +1,223 @@ +# 模块规范 + +## 模块格式 + +本节介绍了 Cocos Creator 如何决定一个模块的格式。 + +Cocos Creator 引擎提供的所有功能都以 ESM 模块的形式存在,见 [引擎模块](./index.md)。 + +项目资源目录中(一般地,任何资产数据库中)以 `.ts` 作为后缀的文件都视为 ESM 模块。例如 `assets/scripts/foo.ts`。 + +对于任何其它模块格式,Cocos Creator 选择与 Node.js 类似的规则来 [鉴别](https://nodejs.org/api/packages.html#packages_determining_module_system)。具体地,以下文件将被视为 ESM 格式: + +- 以 `.mjs` 为后缀的文件; + +- 以 `.js` 为后缀的文件,并且与其最相近的父级 `package.json` 文件中包含一个顶级的 `"type"` 字段,其值为 `"module"`。 + +其余的文件将被视为 CommonJS 模块格式,这包括: + +- 以 `.cjs` 为后缀的文件; + +- 以 `.js` 为后缀的文件,并且与其最相近的父级 `package.json` 文件中包含一个顶级的 `"type"` 字段,其值为 `"commonjs"`。 + +- 不在上述条件下的以 `.js` 为后缀的文件。 + +## 模块说明符与模块解析 + +在 ESM 模块中,通过标准的导入导出语句与目标模块进行交互,例如: + +```ts +import { Foo } from './foo'; +export { Bar } from './bar'; +``` + +导入导出语句中关键字 `from` 后的字符串称为 **模块说明符**。模块说明符也可作为参数出现在动态导入表达式 `import()` 中。 + +模块说明符用于指定目标模块,从模块说明符中解析出目标模块 URL 的过程称为 **模块解析**。 + +Cocos Creator 支持三种模块说明符: + +- **相对说明符** 像 `'./foo'`、`'../bar'` 这样以 `'./'` 和 `'../'` 开头的说明符。 + +- **绝对说明符** 指定了一个 URL 的说明符。例如:`foo:/bar`。 + +- **裸说明符**(Bare specifier)像 `foo`、`foo/bar` 这样既不是 URL 又不是相对说明符的说明符。 + +### 相对说明符 + +相对说明符以当前模块的 URL 为基础 URL,以相对说明符为输入来解析目标模块的 URL。 + +例如,对于模块 `项目路径/assets/scripts/utils/foo` 来说,`'./bar'` 将解析为同目录下的 `项目路径/assets/scripts/utils/bar`;`'../baz'` 将解析为上层目录中的 `项目路径/assets/scripts/baz`。 + +### 绝对说明符 + +绝对说明符直接指定了目标模块的 URL。 + +#### 资产数据库协议 + +通过 `db:///x/y/z.mjs` 这样的 URL 可以访问某个资产数据库中的模块。该 URL 的组成部分如下: + +- URL 协议头 `db:` 是固定的。 + +- URL 主机(host)为 `//`。其中`` 是资产数据库的标识符,一般情况下,它即是挂载了该资产数据库的插件的标识符。当 `` 为 `assets` 时,指代项目资产数据库;为 `internal` 时,指代 Cocos Creator 内置资产数据库。 + +- URL 路径名是指定了模块相对于资产数据库根目录的路径。 + +#### 文件协议 + +Cocos Creator 支持文件协议的 URL,例如 `file:///C:/x/y/z.mjs`。但由于文件 URL 中指定的文件路径是绝对路径,因此很少使用。 + +> 值得注意的是,在 Node.js 中,一种访问 Node.js 内置模块的方法是通过 `node:` 协议的 URL,例如:`node:fs`。Cocos Creator 会将所有对 Node.js 内置模块的访问请求解析为 `node:` URL 请求,例如 `import fs from 'fs'` 中的 `'fs'` 将解析为 `node:fs`。但 Cocos Creator 并不支持 Node.js 内置模块,也就是说并不支持 `node:` 协议。因此会产生加载错误。当使用 npm 中的模块时,可能会遇到该错误。 + +### 裸说明符 + +目前为止,对于裸说明符,Cocos Creator 将应用 [导入映射(实验性质)](./import-map) 和 [Node.js 模块解析算法](https://nodejs.org/api/esm.html#esm_resolver_algorithm_specification)。 + +> 这就包括了对 npm 模块的解析。 + +#### 条件性导出 + +在 Node.js 模块解析算法中,[包的条件性导出](https://nodejs.org/api/packages.html#packages_conditional_exports) 特性用于根据一些条件映射包中的子路径。与 Node.js 类似,Cocos Creator 实现了内置条件 `import`、`default`,但未实现条件 `require`、`node`。 + +开发者可通过编辑器主菜单 **项目 -> 项目设置 -> 脚本** 中的 **导出条件** 项指定 **额外** 的条件,该项默认值为 **browser**,可用 **逗号** 作为分隔符来指定多个额外条件,例如 `browser, bar`。 + +若 **导出条件** 项使用默认值 `browser`,当某 npm 包 `foo` 的 `package.json` 中包含以下配置时: + +```json +{ + "exports": { + ".": { + "browser": "./dist/browser-main.mjs", + "import": "./dist/main.mjs" + } + } +} +``` + +`"foo"` 将解析为包中路径为 `dist/browser-main.mjs` 的模块。 + +> [多玩家框架 Colyseus](https://www.npmjs.com/package/colyseus) 中就为 `browser` 条件做了映射配置。 + +若 **导出条件** 项设置为空,则表示不指定任何额外条件,上例中的 `"foo"` 将解析为包中路径为 `dist/main.mjs` 的模块。 + +### 后缀与目录导入 + +Cocos Creator 对模块说明符中模块的后缀要求更偏向于 Web —— 必须指定后缀并且不支持 Node.js 式的目录导入。然而,基于历史原因和现行的一些限制,TypeScript 模块不允许给出后缀并支持 Node.js 式的目录导入。具体来说: + +当目标模块文件的后缀是 `.js`、`.mjs` 时,模块说明符中 **必须指定** 后缀: + +```ts +import './foo.mjs'; // 正确 +import './foo'; // 错误:无法找到指定模块 +``` + +Node.js 式的目录导入是不支持的: + +```ts +import './foo/index.mjs'; // 正确 +import './foo'; // 错误:无法找到模块。 +``` + +> 这种后缀要求与对目录导入的限制同时应用到了相对说明符和绝对说明符。对于在裸说明符中的要求可参考 Node.js 模块解析算法。 + +但当目标模块文件的后缀是 `.ts` 时,模块说明符中 **不允许指定** 后缀: + +```ts +import './foo'; // 正确:解析为同目录下的 `foo.ts` 模块 +import './foo.ts'; // 错误:无法找到指定模块 +``` + +另一方面,支持 Node.js 式的目录导入: + +```ts +import './foo'; // 正确:解析为 `foo/index.ts` 模块 +``` + +> **注意**: +> +> 1. Cocos Creator 支持 Web 平台。在 Web 平台上实现 Node.js 那样复杂的模块解析算法成本是昂贵的,客户端和服务端之间无法通过频繁的通讯来尝试不同的后缀和文件路径。 +> 2. 即使通过一些后处理工具可以实现在构建阶段完成这样的复杂解析,但会造成静态导入解析(通过 `import` 语句)和动态导入解析(通过 `import()` 表达式)算法的不一致。因此在模块解析算法的选择上,我们更偏向于在代码中指定完整的文件路径。 +> 3. 但我们却无法完全限制这一点,因为就目前来说,TypeScript 中不允许在说明符中指定后缀为 `.ts`。并且 TypeScript 尚且不支持自动补全特定的目标后缀。在这些限制下,我们很难做到两全其美,但我们仍在观测这些条件在未来是否有好转。 + +### 未支持 `browser` 字段 + +有些 npm 包的清单文件 `package.json` 中记录了 `browser` 字段,例如 [JSZip](https://github.com/Stuk/jszip)。`browser` 字段用于指定当该包在非 Node.js 环境下特有的模块解析方法,它可使得包中的某些专用于 Node.js 的模块被替换为能够在 Web 中使用的模块。虽然 Cocos Creator **不支持该字段**,但如果对 npm 包有编辑的能力,Cocos Creator 推荐使用 [条件化导出](https://nodejs.org/api/packages.html#packages_conditional_exports) 和 [子路径导入](https://nodejs.org/api/packages.html#packages_subpath_imports) 来代替 `browser` 字段。 + +否则,可以以非 npm 的方式使用目标库。例如,将目标库中专为非 Node.js 环境制定的模块复制至项目中,再通过相对路径来导入。 + +## CommonJS 模块解析 + +在 CommonJS 模块中,Cocos Creator 应用的是 [Node.js CommonJS 模块解析算法](https://nodejs.org/api/modules.html#modules_all_together)。 + +## 模块格式交互 + +Cocos Creator 允许在 ESM 模块中导入 CommonJS 模块。 + +当从 ESM 模块中导入 CommonJS 模块时,CommonJS 模块的 `module.exports` 对象将作为 ESM 模块的默认导出: + +```ts +import { log } from 'cc'; + +import { default as cjs } from 'cjs'; + +// 上面导入语句的另一种写法: +import cjsSugar from 'cjs'; + +log(cjs); +log(cjs === cjsSugar); +// 打印: +// +// true + +``` + +CommonJS 模块的 [ECMAScript 模块命名空间](https://tc39.es/ecma262/#sec-module-namespace-objects) 表示,是含有一个 `default` 导出的命名空间,其中的 `default` 导出就指向了 CommonJS 模块的 `module.exports` 的值。 + +该 [模块命名空间外来对象](https://tc39.es/ecma262/#module-namespace-exotic-object) 可以通过 `import * as m from 'cjs'` 来观察: + +```ts +import * as m from 'cjs'; +console.log(m); +// 打印: +// [Module] { default: } +``` + + + +## Cocos Creator ESM 解析算法公示 + +Cocos Creator 用于解析 ESM 模块说明符的算法由以下的 `CREATOR_ESM_RESOLVE` 方法给出。它返回从当前 URL 解析模块说明符得到的 URL 结果。 + +在解析算法规范中,引用了[Node ESM 解析算法](https://nodejs.org/api/esm.html#esm_resolution_algorithm) 和 [Import Map 解析算法](https://wicg.github.io/import-maps/#new-resolve-algorithm)(引用为 `IMPORT_MAP_RESOLVE`)。 + +### 解析算法规范 + +`CREATOR_ESM_RESOLVE(specifier, parentURL)` + + 1. Let `resolved` be the result of `CREATOR_STD_RESOLVE(specifier, parentURL)`. + 2. If both `parentURL` and `resolved` are under project assets directory, then + 1. Let `extensionLessResolved` be the result of `TRY_EXTENSION_LESS_RESOLVE(resolved)`. + 1. If `extensionLessResolved` is not `undefined`, return `extensionLessResolved`. + 3. Return `resolved`. + +`CREATOR_STD_RESOLVE(specifier, parentURL)` + + 1. If import map configured, then + 1. Let `resolved` be the result of `IMPORT_MAP_RESOLVE(specifier, parentURL)`, with parsed import map. + 2. If `resolved` is not nil, return `resolved`. + 2. return `ESM_RESOLVE(specifier, parentURL)`. + +`TRY_EXTENSION_LESS_RESOLVE(url)` + + 1. If the file at `url` exists, then + 1. Return `url`. + 2. Let `baseName` be the portion after the last "/" in pathname of `url`, or whole pathname if it does not contain a "/". + 3. If `baseName` is empty, then + 1. Return `undefined`. + 4. Let `resolved` be the result URL resolution of "./" concatenated with `baseName` and `.ts`, relative to parentURL. + 1. If the file at `resolved` exists, then + 2. Return `resolved`. + 5. Let `resolved` be the result URL resolution of "./" concatenated with `baseName` and `/index.ts`, relative to parentURL. + 1. If the file at `resolved` exists, then + 2. Return `resolved`. + 6. Return `undefined`. diff --git a/versions/4.0/zh/scripting/plugin-scripts/import-as-plugin.png b/versions/4.0/zh/scripting/plugin-scripts/import-as-plugin.png new file mode 100644 index 0000000000..767f44ae2f Binary files /dev/null and b/versions/4.0/zh/scripting/plugin-scripts/import-as-plugin.png differ diff --git a/versions/4.0/zh/scripting/plugin-scripts/sort-plugin.png b/versions/4.0/zh/scripting/plugin-scripts/sort-plugin.png new file mode 100644 index 0000000000..d6e399587f Binary files /dev/null and b/versions/4.0/zh/scripting/plugin-scripts/sort-plugin.png differ diff --git a/versions/4.0/zh/scripting/property-changed.png b/versions/4.0/zh/scripting/property-changed.png new file mode 100644 index 0000000000..1051d0299e Binary files /dev/null and b/versions/4.0/zh/scripting/property-changed.png differ diff --git a/versions/4.0/zh/scripting/readonly.md b/versions/4.0/zh/scripting/readonly.md new file mode 100644 index 0000000000..ac6f4cc16c --- /dev/null +++ b/versions/4.0/zh/scripting/readonly.md @@ -0,0 +1,31 @@ +# 开发注意事项 + +## ReadOnly + +由于 `Readonly` 是只读属性,不建议对其进行写操作。因此直接通过 `Readonly` 属性调用接口修改值的方式,不保证在各平台都会生效。 + +例如,节点的世界坐标系内的位置,是 `Readonly` 的: + +```typescript +/** +* @en Position in world coordinate system +* @zh 世界坐标系下的坐标 +*/ +get worldPosition(): Readonly; +set worldPosition(val: Readonly); +``` + +在原生平台,此处 `add` 的结果不保存到 `worldPosition` 上: + +```typescript +this.node.worldPosition.add(xxx); // 结果不会被保存! +``` + +为避免这种情况,可通过中间变量来传递,代码示例如下: + +```typescript +let ret = this.node.worldPosition.add(diff.multiplyScalar(this.speedFactor)); +this.node.setWorldPosition(ret); +``` + +常见属性包括但不限于以下所列的属性:`position`、`rotation`、`scale`、`worldPosition`、`worldRotation`、`worldScale`、`eulerAngles` 以及 `worldMatrix`,都建议采用上述方法使用。 diff --git a/versions/4.0/zh/scripting/reference-class.md b/versions/4.0/zh/scripting/reference-class.md new file mode 100644 index 0000000000..406abce591 --- /dev/null +++ b/versions/4.0/zh/scripting/reference-class.md @@ -0,0 +1,271 @@ +# 脚本进阶 + +在阅读到此章节时,默认您已经对脚本系统较为熟悉,包括装饰器等。否则,请参阅: + +- [脚本基础](./modules/index.md) +- [装饰器](./decorator.md) + +## 实例化 + +通过脚本定义一个 Foo 类和 Bar 类,Foo 类需要使用 Bar 类定义的属性,此时可以在 Foo 类中将 Bar 类直接 new 出一个对象: + +```ts +class Foo { + public bar: Bar = null;; + constructor() { + this.bar = new Bar(); + } +} +let bar = new Foo(); +``` + +### 实例方法 + +实例方法请在原型对象中声明: + +```typescript +class Foo { + public text!: string; + constructor() { + this.text = "this is sprite" + } + // 声明一个名叫 "print" 的实例方法 + print() { + console.log(this.text); + } +} + +let obj = new Foo(); +// 调用实例方法 +obj.print(); +``` + +## 判断类型 + +需要做类型判断时,可以用 TypeScript 原生的 `instanceof`: + +```typescript +class Sub extends Base { + +} + +let sub = new Sub(); +console.log(sub instanceof Sub); //true +console.log(sub instanceof Base); //true + +let base = new Base(); +console.log(base instanceof Sub); // false +``` + +### 静态变量和静态方法 + +静态变量或静态方法可以用 `static` 声明: + +```typescript +class Foo { + static count = 0; + static getBounds() { + + } +} +``` + +静态成员会被子类继承,继承时 Creator 会将父类的静态变量 **浅拷贝** 给子类,因此: + +```typescript +class Object { + static count = 11; + static range: { w: 100, h: 100 } +} + +class Foo extends Object { + +} + +console.log(Foo.count); // 结果是 11,因为 count 继承自 Object 类 + +Foo.range.w = 200; +console.log(Object.range.w); // 结果是 200,因为 Foo.range 和 Object.range 指向同一个对象 +``` + +如果不需要考虑继承,私有的静态成员也可以直接定义在类的外面: + +```typescript +// 局部方法 +doLoad(sprite) { + // ... +}; +// 局部变量 +let url = "foo.png"; + +class Sprite { + public url = ''; + load() { + this.url = url; + doLoad(this); + }; +}; +``` + +## 继承 + +### 父构造函数 + +> **注意**:不论子类是否有定义构造函数,在子类实例化前,父类的构造函数都会被自动调用。 + +```typescript +class Node { + name: string; + constructor() { + this.name = "node"; + } +} + +class Sprite extends Node { + constructor() { + super(); + // 子构造函数被调用前,父构造函数已经被调用过,所以 this.name 已经被初始化过了 + console.log(this.name); // "node" + // 重新设置 this.name + this.name = "sprite"; + } +} + +let obj = new Sprite(); +console.log(obj.name); // "sprite" +``` + +### 重写 + +所有成员方法都是虚方法,子类方法可以直接重写父类方法: + +```typescript +class Shape { + getName() { + return "shape"; + } +}; + +class Rect extends Shape { + getName () { + return "rect"; + } +}; + +let obj = new Rect(); +console.log(obj.getName()); // "rect" +``` + +## get/set 方法 + +如果在属性中定义了 get/set,那么在访问属性的时候,就能触发预定义的 get/set 方法。 + +### get + +在属性中定义 get 方法: + +```typescript +@property +get num() { + return this._num; +} + +@property +private _num = 0; +``` + +get 方法可以返回任意类型的值。
+定义了 get 方法的属性可以显示在 **属性检查器** 中,可以在代码中直接访问。 + +```typescript +class Sprite { + + @property + get width() { + return this._width; + } + + @property + private _width = 0; + + print(){ + console.log(this.width); + } +}; +``` + +**注意**: + +- 属性定义了 `get` 方法之后就不能被序列化,也就是 `serializable` 参数不可用。例如下面的写法,`width` 属性既不会在编辑器上显示,也不会序列化。 + + ```typescript + get width() { + return this._width; + } + + @property({ type: CCInteger, tooltip: "The width of sprite" }) + private _width = 0; + ``` + +- 定义了 `get` 方法的属性如果需要在编辑器中显示,需要定义 `property`。例如下面的写法,`width` 属性如果去掉 `@property` 就不会在编辑器上呈现,`_width` 属性会序列化。 + + ```typescript + @property + get width() { + return this._width; + } + + @property({ type: CCInteger, tooltip: "The width of sprite" }) + private _width = 0; + ``` + +- 定义了 get 方法的属性本身是只读的,但返回的对象并不是只读的。开发者依然可以通过代码修改对象内部的属性,例如: + + ```typescript + get num() { + return this._num; + } + + @property + private _num = 0; + + start() { + console.log(this.num); + } + ``` + +### set + +在属性中定义 set 方法: + +```typescript + +set width(value) { + this._width = value +} + +private _width = 0; + +start() { + this.width = 20; + console.log(this.width); +} +``` + +set 方法接收一个传入参数,这个参数可以是任意类型。set 方法一般和 get 方法一起使用: + +```typescript +@property +get width() { + return this._width; +} + +set width(value) { + this._width = value; +} + +@property +private _width = 0; +``` + +> **注意**:set 方法不定义属性。 diff --git a/versions/4.0/zh/scripting/reference/attributes.md b/versions/4.0/zh/scripting/reference/attributes.md new file mode 100644 index 0000000000..b43f8b12cf --- /dev/null +++ b/versions/4.0/zh/scripting/reference/attributes.md @@ -0,0 +1,42 @@ +# 属性参数 + +> 属性参数用来给已定义的属性附加元数据,类似于脚本语言的 `Decorator` 或者 C# 的 `Attribute`。 + +## 属性检查器相关参数 + +| 参数名 | 说明 | 类型 | 默认值 | 备注 | +| :--- | :--- | :-- | :--- | :--- | +| type | 限定属性的数据类型 | (Any) | undefined | 详见 [type 参数](../decorator.md#type) | +| visible | 在 **属性检查器** 面板中显示或隐藏 | boolean | (注1) | 详见 [visible 参数](../decorator.md#visible) | +| displayName | 在 **属性检查器** 面板中显示为另一个名字 | string | undefined | - | +| tooltip | 在 **属性检查器** 面板中添加属性的 Tooltip | string | undefined | - | +| multiline | 在 **属性检查器** 面板中使用多行文本框 | boolean | false | - | +| readonly | 在 **属性检查器** 面板中只读 | boolean | false | - | +| min | 限定数值在编辑器中输入的最小值 | number | undefined | - | +| max | 限定数值在编辑器中输入的最大值 | number | undefined | - | +| step | 指定数值在编辑器中调节的步长 | number | undefined | - | +| range | 一次性设置 min、max、step | [min, max, step] | undefined | step 值可选 | +| slide | 在 **属性检查器** 面板中显示为滑动条 | boolean | false | - | +| group | 在 **属性检查器** 面板中显示为分组,样式默认为 tab | `{ name } 或 { id, name, displayOrder, style }` | undefined | 详见 [group 参数](../decorator.md#group) | + +## 序列化相关参数 + +以下参数不能用于 get 方法: + +| 参数名 | 说明 | 类型 | 默认值 | 备注 | +| :--- | :--- | :--- | :--- | :--- | +| serializable | 序列化该属性 | boolean | true | 详见 [serializable 参数](../decorator.md#serializable) | +| formerlySerializedAs | 指定之前序列化所用的字段名 | string | undefined | 重命名属性时,声明这个参数来兼容之前序列化的数据 | +| editorOnly | 在导出项目前剔除该属性 | boolean | false | - | + +## 其它参数 + +| 参数名 | 说明 | 类型 | 默认值 | 备注 | +| :--- | :--- | :--- | :--- | :--- | +| override | 当重写父类属性时需要定义该参数为 true | boolean | false | 详见 [override 参数](../decorator.md#override) | + + + + +**注 1**:visible 的默认值取决于属性名。当属性名以下划线 `_` 开头时,默认隐藏,否则默认显示。 diff --git a/versions/4.0/zh/scripting/reference/coding-standards.md b/versions/4.0/zh/scripting/reference/coding-standards.md new file mode 100644 index 0000000000..9e4c35463c --- /dev/null +++ b/versions/4.0/zh/scripting/reference/coding-standards.md @@ -0,0 +1,560 @@ +# 推荐编码规范 + +下面是 Cocos Creator 开发团队使用的编码规范,收录在手册里以供游戏开发者和工具开发者参考。 + +## 命名规范 + +- 当我们为变量,函数和实例命名时, 使用 camelCase 命名法。 + + ```ts + // bad + const FOOBar = {}; + const foo_bar = {}; + function FOOBar () {} + + // good + const fooBar = {}; + function fooBar () {} + ``` + +- 当变量、函数和实例命名涉及到缩写时,缩写在开头全部小写,在后续单词中,则全部大写。 + + ```ts + // bad + const Id = 0; + const iD = 0; + function requireId () {} + + // good + const id = 0; + const uuid = ''; + function requireID () {} + class AssetUUID {} + ``` + +- 当我们为类或者模块命名时,使用 PascalCase 命名法。 + + ```ts + // bad + const foobar = cc.Class({ + foo: 'foo', + bar: 'bar', + }); + const foobar = require('foo-bar'); + + // good + const FooBar = cc.Class({ + foo: 'foo', + bar: 'bar', + }); + const FooBar = require('foo-bar'); + ``` + +- 推荐使用全大写加下划线来命名“常量”。 + + ```ts + // bad + const PRIVATE_VARIABLE = 'should not be unnecessarily uppercased within a file'; + + // bad + var THING_TO_BE_CHANGED = 'should obviously not be uppercased'; + + // bad + let REASSIGNABLE_VARIABLE = 'do not use let with uppercase variables'; + + // --- + + // allowed but does not supply semantic value + export const apiKey = 'SOMEKEY'; + + // better in most cases + export const API_KEY = 'SOMEKEY'; + + // --- + + // bad - unnecessarily uppercases key while adding no semantic value + export const MAPPING = { + KEY: 'value' + }; + + // good + export const Type = { + SIMPLE: 'value' + }; + ``` + +- 使用前置下划线 `_` 当我们为私有属性命名 + + ```ts + // bad + this.__firstName__ = 'foobar'; + this.firstName_ = 'foobar'; + + // good + this._firstName = 'foobar'; + ``` + +- 文件名我们采用 dash 命名法 + + ```bash + // bad + fooBar.js + FooBar.js + + // good + foo-bar.js + ``` + +## 语法规范 + +- 当类的属性声明没有初始化式的时候,应当声明 declare,否则可能面临性能问题。详情请参考 [Issue](https://github.com/cocos-creator/3d-tasks/issues/2848) + + ```typescript + // bad + class A { + public a: number; + constructor (a : number) { + // 相当于此处还有一句 this.a = void 0; + // 注意可能面临性能问题! + this.a = a; + } + } + + // good + class A { + public a: number = 0; // Ok. + constructor (a : number) { + // 相当于此处还有一句 this.a = 0; + // 但不会引起大的性能问题 + this.a = a; + } + } + + // best + class A { + public declare a: number; + public b: undefined | object; // OK: b 未在构造函数中二次赋值 + public declare c: object | null; + + constructor (a: number, c: object) { + this.a = a; + this.c = c; + } + } + ``` + +- 使用 `Object.create(null)` 创建一个字典 + + ```ts + // bad + const map = new Object(); + + // bad + const map = {}; + + // good + const map = Object.create(null); + ``` + +- 使用 `[]` 创建一个数组 + + ```ts + // bad + const array = new Array(); + + // good + const array = []; + ``` + +- 尽可能在 TypeScript 代码中使用单引号 `''` 来定义 string + + ```ts + // bad + const str = "Hello World"; + + // good + const str = 'Hello World'; + ``` + +- 多行 string 定义时, 尽可能使用 `+` 定义 + + ```ts + // bad + const errorMessage = 'This is a super long error that was thrown because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.'; + + // bad + const errorMessage = 'This is a super long error that was thrown because \ + of Batman. When you stop to think about how Batman had anything to do \ + with this, you would get nowhere \ + fast.'; + + // good + const errorMessage = 'This is a super long error that was thrown because ' + + 'of Batman. When you stop to think about how Batman had anything to do ' + + 'with this, you would get nowhere fast.'; + ``` + +- 使用 `===` 和 `!==` 而不是 `==` 和 `!=` + +## 书写规范 + +- 根据个人习惯, 和原代码作者格式, 选择 4 个空格或者 2 个空格作为缩进 + + ```ts + // bad + function () { + ∙const name; + } + + // very bad + function () { + ∙∙∙∙const name; + } + + // good + function () { + ∙∙const name; + } + + // good + function () { + ∙∙∙∙const name; + } + ``` + +- 行尾不要留有空格,文件底部请留一个空行 + + ```js + // bad + function () {∙ + ∙∙∙∙const name;∙ + } + /* EOF */ + + // good + function () { + ∙∙∙∙const name; + } + + /* EOF */ + ``` + +- 语句结尾请加 `;` + + ```js + // bad + proto.foo = function () { + } + + // good + proto.foo = function () { + }; + + // bad + function foo () { + return 'test' + } + + // very bad + // returns `undefined` instead of the value on the next line, + // always happens when `return` is on a line by itself because of Automatic Semicolon Insertion! + function foo () { + return + 'test' + } + + // good + function foo () { + return 'test'; + } + + // bad + function foo () { + }; + + // good,这里不是语句结尾 + function foo () { + } + ``` + +- 尽可能将 `{` 和表达式放在同一行 + + ```ts + // bad + if ( isFoobar ) + { + } + + // good + if ( isFoobar ) { + } + + // bad + function foobar () + { + } + + // good + function foobar () { + } + + // bad + const obj = + { + foo: 'foo', + bar: 'bar', + } + + // good + const obj = { + foo: 'foo', + bar: 'bar', + } + ``` + +- 在 `{` 前请空一格 + + ```js + // bad + if (isJedi){ + fight(); + } + else{ + escape(); + } + + // good + if (isJedi) { + fight(); + } else { + escape(); + } + + // bad + dog.set('attr',{ + age: '1 year', + breed: 'Bernese Mountain Dog', + }); + + // good + dog.set('attr', { + age: '1 year', + breed: 'Bernese Mountain Dog', + }); + ``` + +- 在逻辑状态表达式 ( `if`, `else`, `while`, `switch`) 后请空一格 + + ```js + // bad + if(isJedi) { + fight (); + } + else{ + escape(); + } + + // good + if (isJedi) { + fight(); + } else { + escape(); + } + ``` + +- 二元、三元运算符的左右请空一格 + + ```js + // bad + const x=y+5; + const left = rotated? y: x; + + // good + const x = y + 5; + const left = rotated ? y : x; + + // bad + for (let i=0; i< 10; i++) { + } + + // good + for (let i = 0; i < 10; i++) { + } + ``` + +- 一些函数的声明方式 + + ```js + // bad + const test = function () { + console.log('test'); + }; + + // good + function test () { + console.log('test'); + } + + // bad + function test () { console.log('test'); }; + + // good + function test () { + console.log('test'); + } + + // bad + function divisibleFunction () { + return DEBUG ? 'foo' : 'bar'; + } + + // best + const divisibleFunction = DEBUG ? + function () { + return 'foo'; + } : + function () { + return 'bar'; + }; + + // bad + function test(){ + } + + // good + function test () { + } + + // bad + const obj = { + foo: function () { + } + }; + + // good + const obj = { + foo () { + } + }; + + // bad + array.map(x=>x + 1); + array.map(x => { + return x + 1; + }); + + // good + array.map(x => x + 1); + ``` + +- 在 Block 定义之间请空一行 + + ```js + // bad + if (foo) { + return bar; + } + return baz; + + // good + if (foo) { + return bar; + } + + return baz; + + // bad + const obj = { + x: 0, + y: 0, + foo () { + }, + bar () { + }, + }; + return obj; + + // good + const obj = { + x: 0, + y: 0, + + foo () { + }, + + bar () { + }, + }; + + return obj; + ``` + +- 不要使用前置逗号定义 + + ```ts + // bad + const story = [ + once + , upon + , aTime + ]; + + // good + const story = [ + once, + upon, + aTime, + ]; + + // bad + const hero = { + firstName: 'Ada' + , lastName: 'Lovelace' + , birthYear: 1815 + , superPower: 'computers' + }; + + // good + const hero = { + firstName: 'Ada', + lastName: 'Lovelace', + birthYear: 1815, + superPower: 'computers', + }; + ``` + +- 单行注释请在斜杠后面加一个空格 + + ```js + //bad + // good + ``` + +- 多行注释写法 + + ```js + /* + * good + */ + ``` + +- 需要导出到 API 文档的多行注释写法 + + ```js + /** + * good + */ + ``` + +- 除了多语言 API 注释以外,代码中不允许写中文注释 + + ```js + // bad + // 中文注释不利于非中文开发者阅读代码 + // good + // Please write all in file comments in English + ``` + +## 推荐阅读 + +[Airbnb ts Style Guide](https://github.com/airbnb/ts#table-of-contents) diff --git a/versions/4.0/zh/scripting/scene-managing.md b/versions/4.0/zh/scripting/scene-managing.md new file mode 100644 index 0000000000..57970b958e --- /dev/null +++ b/versions/4.0/zh/scripting/scene-managing.md @@ -0,0 +1,62 @@ +# 加载和切换场景 + +在 Cocos Creator 中,我们使用场景文件名(不包含扩展名)来索引指代场景。并通过以下接口进行加载和切换操作: + +```ts +director.loadScene("MyScene"); +``` + +除此之外,从 v2.4 开始 Asset Bundle 还增加了一种新的加载方式: + +```typescript +bundle.loadScene('MyScene', function (err, scene) { + director.runScene(scene); +}); +``` + +Asset Bundle 提供的 `loadScene` 只会加载指定 bundle 中的场景,并不会自动运行场景,还需要使用 `director.runScene` 来运行场景。
`loadScene` 还提供了更多参数来控制加载流程,开发者可以自行控制加载参数或者在加载完场景后做一些处理。 + +更多关于加载 Asset Bundle 中的场景,可参考文档 [Asset Bundle](../asset/bundle.md)。 + +## 通过常驻节点进行场景资源管理和参数传递 + +引擎同时只会运行一个场景,当切换场景时,默认会将场景内所有节点和其他实例销毁。如果我们需要用一个组件控制所有场景的加载,或在场景之间传递参数数据,就需要将该组件所在节点标记为「常驻节点」,使它在场景切换时不被自动销毁,常驻内存。我们使用以下接口: + +`director.addPersistRootNode(myNode);` + +上面的接口会将 `myNode` 变为常驻节点,这样挂在上面的组件都可以在场景之间持续作用,我们可以用这样的方法来储存玩家信息,或下一个场景初始化时需要的各种数据。 +需要注意的是,目标节点必须为位于层级的根节点,否则设置无效。 + +如果要取消一个节点的常驻属性: + +`director.removePersistRootNode(myNode);` + +需要注意的是上面的 API 并不会立即销毁指定节点,只是将节点还原为可在场景切换时销毁的节点。 + +## 场景加载回调 + +加载场景时,可以附加一个参数用来指定场景加载后的回调函数: + +`director.loadScene("MyScene", onSceneLaunched);` + +上一行里 `onSceneLaunched` 就是声明在本脚本中的一个回调函数,在场景加载后可以用来进一步的进行初始化或数据传递的操作。 + +由于回调函数只能写在本脚本中,所以场景加载回调通常用来配合常驻节点,在常驻节点上挂载的脚本中使用。 + +## 预加载场景 + +`director.loadScene` 会在加载场景之后自动切换运行新场景,有些时候我们需要在后台静默加载新场景,并在加载完成后手动进行切换。那就可以预先使用 `preloadScene` 接口对场景进行预加载: + +```ts +director.preloadScene("table", function () { + console.log('Next scene preloaded'); +}); +``` + +之后在合适的时间调用 `loadScene`,就可以真正切换场景。 + +```ts +director.loadScene("table"); +``` + +就算预加载还没完成,你也可以直接调用 `director.loadScene`,预加载完成后场景就会启动。 diff --git a/versions/4.0/zh/scripting/scheduler.md b/versions/4.0/zh/scripting/scheduler.md new file mode 100644 index 0000000000..f3ffe264e0 --- /dev/null +++ b/versions/4.0/zh/scripting/scheduler.md @@ -0,0 +1,78 @@ +# 使用计时器 + +在 Cocos Creator 中,我们为组件提供了方便的计时器。 + +也许有人会认为 `setTimeout` 和 `setInterval` 就足够了,开发者当然可以使用这两个函数,不过我们更推荐使用计时器,因为它更加强大灵活,和组件也结合得更好! + +下面来看看它的具体使用方式: + +1. 开始一个计时器 + + ```ts + this.schedule(function() { + // 这里的 this 指向 component + this.doSomething(); + }, 5); + ``` + + 上面这个计时器将每隔 5s 执行一次。 + +2. 更灵活的计时器 + + ```ts + // 以秒为单位的时间间隔 + let interval = 5; + // 重复次数 + let repeat = 3; + // 开始延时 + let delay = 10; + this.schedule(function() { + // 这里的 this 指向 component + this.doSomething(); + }, interval, repeat, delay); + ``` + + 上面的计时器将在 10 秒后开始计时,每 5 秒执行一次回调,重复 **3 + 1** 次。 + +3. 只执行一次的计时器(快捷方式) + + ```ts + this.scheduleOnce(function() { + // 这里的 this 指向 component + this.doSomething(); + }, 2); + ``` + + 上面的计时器将在两秒后执行一次回调函数,之后就停止计时。 + +4. 取消计时器 + + 开发者可以使用回调函数本身来取消计时器: + + ```ts + this.count = 0; + this.callback = function () { + if (this.count == 5) { + // 在第六次执行回调时取消这个计时器 + this.unschedule(this.callback); + } + this.doSomething(); + this.count++; + } + this.schedule(this.callback, 1); + ``` + +> **注意**:组件的计时器调用回调时,会将回调的 `this` 指定为组件本身,因此回调中可以直接使用 `this`。 + +下面是 Component 中所有关于计时器的函数: + +- schedule:开始一个计时器 +- scheduleOnce:开始一个只执行一次的计时器 +- unschedule:取消一个计时器 +- unscheduleAllCallbacks:取消这个组件的所有计时器 + +这些 API 的详细描述都可以在我们的 API 文档中找到。 + +除此之外,如果需要每一帧都执行一个函数,请直接在 Component 中添加 `update` 函数,这个函数将默认被每帧调用,这在 [生命周期文档](life-cycle-callbacks.md#update) 中有详细描述。 + +### 注意:`Node` 不包含计时器相关 API diff --git a/versions/4.0/zh/scripting/script-basics.md b/versions/4.0/zh/scripting/script-basics.md new file mode 100644 index 0000000000..36df97e40c --- /dev/null +++ b/versions/4.0/zh/scripting/script-basics.md @@ -0,0 +1,10 @@ +# 脚本基础 + +该部分内容主要介绍脚本的一些基础概念、创建方式、运行环境等: + +- [创建脚本](./setup.md) +- [配置代码编辑环境](./coding-setup.md) +- [脚本运行环境](./basic.md) +- [装饰器使用](./decorator.md) +- [属性参数参考](./reference/attributes.md) +- [生命周期回调](./life-cycle-callbacks.md) diff --git a/versions/4.0/zh/scripting/setup.md b/versions/4.0/zh/scripting/setup.md new file mode 100644 index 0000000000..55b427e7ae --- /dev/null +++ b/versions/4.0/zh/scripting/setup.md @@ -0,0 +1,236 @@ +# 创建脚本 + +## 创建组件脚本 + +在 Cocos Creator 中,脚本也是资源的一部分。在 **资源管理器** 中创建的脚本,默认是一个 NewComponent 组件,我们称之为组件脚本。可通过以下两种方式创建: + +- **资源管理器** 面板空白位置或某个文件夹资源下右击菜单,选择 **Create** > **TypeScript** > **NewComponent**。 +- **资源管理器** 左上角的 **+** 按钮,点击后选择 **TypeScript** > **NewComponent**。 + +![create script](setup/create-script.png) + +在创建脚本时,名称不能为空,输入框默认为 `NewComponent`。我们将其修改为 `say-hello`,可以看到在 **资源管理器** 中生成了一个名为 `say-hello` 的脚本文件。 + +![say-hello-1](setup/say-hello-1.png) + +![say-hello-2](setup/say-hello-2.png) + +新建后的初始脚本代码如下: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('say_hello') +export class say_hello extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} +``` + +> **注意点**: +> +> 1. 项目中所有脚本的类名 `ClassName` (如上例中的 'say_hello') 不允许重复,即使脚本文件在不同的目录下,各自的代码里也不允许有相同的类名。 +> +> 2. 脚本文件名称和脚本的类名不同,在输入初始的文件名之后,文件名会被处理为类名,处理的逻辑详见下文 **类名的生成**。脚本文件生成后,对文件的后续操作**脚本重命名**,新的文件名不会再去生成并替换代码里的类名,不再影响了。 +> +> 3. 我们推荐用户使用 TypeScript 来编写脚本,目前 **资源管理器** 中仅支持创建 TypeScript 文件。但如果用户想要使用 JavaScript 来编写脚本的话,可以直接在操作系统的文件夹中创建 JavaScript 文件,或在其他代码编辑软件中创建 JavaScript 文件。 + +## 类名的生成 + +在获得初始文件名数据后,会生成两种规则的类名 `ClassName`,并以变量的方式提供给**脚本模板**。 + +- 下划线格式,变量名为 `<%UnderscoreCaseClassName%>`。这种格式是为了保持类名尽可能地与文件名一致,保持一致的好处是有利于代码全局搜索和替换。 +- 驼峰格式,变量名为 `<%CamelCaseClassName%>`。这种格式是为了保持与主流的脚本标准一致,首字母大写的驼峰格式。 + +## 添加脚本到场景节点中 + +**将脚本添加到场景节点中,实际上就是为这个节点添加一个脚本组件。** +在 **层级管理器** 选中某个节点,此时 **属性检查器** 面板会显示该节点的属性。以下两种添加方式: + +- 直接将 **资源管理器** 中的脚本拖拽当前节点的到 **属性检查器** 中,即为挂载了一个组件。 + +- 点击 **属性检查器** 最下方的 **添加组件** 按钮,选择 **自定义脚本 -> say_hello**,即为挂载组件。 + + ![add component](setup/add-component.png) + ![add component done](setup/add-component-done.png) + +## 编辑脚本 + +开发者可根据自己的需求,选择自己喜爱的代码编辑软件(如:Vim、Sublime Text、Web Storm、VSCode 等)进行脚本编辑。编辑器的 **偏好设置** > **外部程序** 可设置指定的脚本打开工具。 + +![preference script editor](setup/preference-script-editor.png) + +外部程序配置完成后,在 **资源管理器** 中双击脚本资源,便会用指定的程序打开该脚本。 +编辑脚本代码保存后,鼠标点击回到编辑器,编辑器会自动检测到脚本的改动,重新对其进行编译后使用。 + +编写脚本代码,可阅读以下文档了解相关内容: + +- [配置代码编辑环境](coding-setup.md) +- [脚本基础](basic.md) + +脚本文件创建成功后,再对文件进行重命名,或者对代码里的类名进行修改,文件名和类名均不会再互相影响。 + +- 以 `say-hello` 为例,我们在 **资源管理器** 中将其重命名为 `hello`。 + +重新选中该资源,查看 **属性检查器**,代码还是显示 `class say_hello`,不会变动。 + +重新选中 **层级管理器** 上刚添加组件的节点 **Node**,查看 **属性检查器**,组件名称还是显示 `say_hello`,不会变动。 + +我们继续双击当前的 `hello` 资源,将类名改为 **say**,保存后回到编辑器: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('say') +export class say extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} +``` + +同样的脚本文件名 `hello` 不会变化。节点 **Node** 里的组件名称变为 **say**。 + +> **注意**:这里需要记得将装饰器 `@ccclass('say_hello')` 中的内容也改为 `@ccclass('say')`。 + +![modify script](setup/modify-script.png) + +##
脚本模板 + +从编辑器 v3.3 开始,支持在项目中管理不同的脚本模板。 + +- 新建一个项目,新项目不会自动创建自定义脚本模板所在的目录 `.creator/asset-template/typescript`。 +- 可以手动创建上述目录。也可以通过 **资源管理器** 的右击菜单里的菜单,点击后才生成目录。 + + ![custom script](setup/custom-script.png) + +默认的 `NewComponent` 脚本模板仍在引擎内置资源目录下 `resources\3d\engine\editor\assets\default_file_content\ts`。 +文件代码为: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('<%UnderscoreCaseClassName%>') +export class <%UnderscoreCaseClassName%> extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} + +/** + * COMMENTS_GENERATE_IGNORE + * Use "COMMENTS_GENERATE_IGNORE" tag if you do not want later created scripts to contain these comments. + * + * Predefined Variables + * You can use predefined variables below to setup your scripting preference. For example, whether to use camel case style. + * + * <%UnderscoreCaseClassName%>, class name in underscore format, like 'new_component' + * <%CamelCaseClassName%>, class name in camel format, like 'NewComponent' + * <%Author%>, Who create this file + * <%DateTime%>, when create this file + * <%FileBasename%>, creating file name with extension + * <%FileBasenameNoExtension%>, creating file name without extension + * <%URL%>, url of this file in COCOS ASSET URL format + * <%ManualUrl%>, url of office help document, like 'https://docs.cocos.com/creator/manual/en/' + * + * + * Example: + * + @ccclass('<%UnderscoreCaseClassName%>') + export class <%UnderscoreCaseClassName%> extends Component { + + // class member could be defined like this. + dummy = ''; + + // Use 'property' decorator if your want the member to be serializable. + @property + serializableDummy = 0; + + start () { + // Your initialization goes here. + } + + update (deltaTime: number) { + // Your update function goes here. + } + + } + * + * Learn more about scripting: <%ManualUrl%>scripting/ + * Learn more about CCClass: <%ManualUrl%>scripting/decorator.html + * Learn more about life-cycle callbacks: <%ManualUrl%>scripting/life-cycle-callbacks.html + */ + +``` + +> **注意**: +> +> 1. 脚本模板中大量的注释并不会生成到脚本文件中,因为在注释里我们使用了关键词标注 `COMMENTS_GENERATE_IGNORE` 只要此关键词在某段注释里,那么生成脚本文件就会将该段注释忽略掉。 +> +> 2. `Predefined Variables` 我们准备了一些预制的变量,在生成脚本文件的时候可以作为辅助的信息,比如作者 `<%Author%>`。 +> +> 3. 特别准备了两种类名格式:`<%UnderscoreCaseClassName%>` 和 `<%CamelCaseClassName%>`。名称前后仍可以添加自定义的前缀或后缀,如加个 `Robot` 前缀 `Robot<%CamelCaseClassName%>`。 +> +> 4. 通过点击右击菜单的方式,项目自定义脚本模板目录下会自动生成一个文档网址快捷链接,双击即会调出浏览器打开指定网页:`Custom Script Template Help Documentation`。 +> +> ![custom script help](setup/custom-script-help.png) + +### 添加脚本模板 + +我们从复制上述内置 `NewComponent` 模板的代码进行修改,类名为驼峰格式,加 `Robot` 前缀,文件另存为无后缀名的文件 `CustomComponent`,保存在项目自定义脚本模板的路径下,即 `.creator/asset-template/typescript/CustomComponent`。 + +![custom script file](setup/custom-script-file.png) + +`CustomComponent` 模板内容修改为: + +```ts +import { _decorator, Component, Node } from 'cc'; +const { ccclass, property } = _decorator; + +/** + * + * <%UnderscoreCaseClassName%> + * <%CamelCaseClassName%> + * <%Author%> + * <%DateTime%> + * <%FileBasename%> + * <%FileBasenameNoExtension%> + * <%URL%> + * <%ManualUrl%> + * + */ + +@ccclass('Robot<%CamelCaseClassName%>') +export class Robot<%CamelCaseClassName%> extends Component { + start() { + + } + + update(deltaTime: number) { + + } +} + +``` + +那么最后的我们新建一个 `wake up` 脚本资源看看,效果如下图: + +![custom script menu](setup/custom-script-menu.png) + +![custom script result](setup/custom-script-result.png) diff --git a/versions/4.0/zh/scripting/setup/add-component-done.png b/versions/4.0/zh/scripting/setup/add-component-done.png new file mode 100644 index 0000000000..821ff5fc93 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/add-component-done.png differ diff --git a/versions/4.0/zh/scripting/setup/add-component.png b/versions/4.0/zh/scripting/setup/add-component.png new file mode 100644 index 0000000000..df2164a988 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/add-component.png differ diff --git a/versions/4.0/zh/scripting/setup/create-script.png b/versions/4.0/zh/scripting/setup/create-script.png new file mode 100644 index 0000000000..f7a5c1eb42 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/create-script.png differ diff --git a/versions/4.0/zh/scripting/setup/custom-script-file.png b/versions/4.0/zh/scripting/setup/custom-script-file.png new file mode 100644 index 0000000000..ddcb7d97d0 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/custom-script-file.png differ diff --git a/versions/4.0/zh/scripting/setup/custom-script-help.png b/versions/4.0/zh/scripting/setup/custom-script-help.png new file mode 100644 index 0000000000..a7a3d772c4 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/custom-script-help.png differ diff --git a/versions/4.0/zh/scripting/setup/custom-script-menu.png b/versions/4.0/zh/scripting/setup/custom-script-menu.png new file mode 100644 index 0000000000..ce620765a1 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/custom-script-menu.png differ diff --git a/versions/4.0/zh/scripting/setup/custom-script-result.png b/versions/4.0/zh/scripting/setup/custom-script-result.png new file mode 100644 index 0000000000..428fd11345 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/custom-script-result.png differ diff --git a/versions/4.0/zh/scripting/setup/custom-script.png b/versions/4.0/zh/scripting/setup/custom-script.png new file mode 100644 index 0000000000..3cd733d165 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/custom-script.png differ diff --git a/versions/4.0/zh/scripting/setup/modify-script.png b/versions/4.0/zh/scripting/setup/modify-script.png new file mode 100644 index 0000000000..d756d9fb62 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/modify-script.png differ diff --git a/versions/4.0/zh/scripting/setup/preference-script-editor.png b/versions/4.0/zh/scripting/setup/preference-script-editor.png new file mode 100644 index 0000000000..9ea71e9704 Binary files /dev/null and b/versions/4.0/zh/scripting/setup/preference-script-editor.png differ diff --git a/versions/4.0/zh/scripting/setup/say-hello-1.png b/versions/4.0/zh/scripting/setup/say-hello-1.png new file mode 100644 index 0000000000..ad4fb7725e Binary files /dev/null and b/versions/4.0/zh/scripting/setup/say-hello-1.png differ diff --git a/versions/4.0/zh/scripting/setup/say-hello-2.png b/versions/4.0/zh/scripting/setup/say-hello-2.png new file mode 100644 index 0000000000..2c7bc10bde Binary files /dev/null and b/versions/4.0/zh/scripting/setup/say-hello-2.png differ diff --git a/versions/4.0/zh/scripting/tsconfig.md b/versions/4.0/zh/scripting/tsconfig.md new file mode 100644 index 0000000000..915fcdcbaf --- /dev/null +++ b/versions/4.0/zh/scripting/tsconfig.md @@ -0,0 +1,60 @@ +# tsconfig + +项目中 `tsconfig.json` 的 **绝大多数** 编译选项并不影响 Cocos Creator 对 TypeScript 的编译。因此,你需要小心配置其中的某些选项,以使得 IDE 的检查功能和 Cocos Creator 的编译行为一致。 + +---- + +以下选项不应当显式修改: + +- `compilerOptions.target` +- `compilerOptions.module` + +例如,若将 `tsconfig.json` 设置为: + +```json +{ + "compilerOptions": { + "target": "es5", + "module": "cjs" + } +} +``` + +那么以下脚本代码在(使用 `tsc` 作为检查器的)IDE 中不会引起错误,因为 `compilerOptions.module` 设置为了 `cjs`。 + +```ts +const myModule = require("path-to-module"); +``` + +然而 Cocos Creator 隐含的 `compilerOptions.module` 是 `es2015`,因此在运行时可能会提示 "require 未定义" 等错误。 + +以下脚本代码对于 Cocos Creator 来说是合法的,但 IDE 可能会报告错误。因为 `compilerOptions.target` 设置为了 `es5`,而 `Set` 是 ES6 才引入的。 + +```ts +const mySet = new Set(); +``` + +---- + +对于其他选项,你可以自由修改。 + +例如,当你希望禁止你项目中所有 TypeScript 脚本对隐式 `any` 的使用,就可以在 `tsconfig.json` 中将 `compilerOptions.noImplicitAny` 设为 `true`。如此当你用 Visual Studio Code 等 IDE 检查该文件时就会收到相应的错误提示。 + +---- + +对于大多数项目而言,`tsconfig.json` 的某些选项是固定的。例如 `compilerOptions.target`、`compilerOptions.module` 以及 Cocos Creator 的类型声明文件位置等。 + +由于 `tsc` 的良好设计,`extends` 选项使得 `tsconfig.json` 可以是级联的。Cocos Creator 意识到了这一点,因此固定的 `tsconfig` 选项被放置在 `{项目路径}/tmp/tsconfig.cocos.json` 下,并由 Cocos Creator 管理。 + +于是,项目根路径下的 `tsconfig` 可以如下配置以共享这些固定选项: + +```json5 +{ + extends: './tmp/tsconfig.cocos.json', + compilerOptions: { + /* 自定义的 tsconfig 选项 */ + } +} +``` + +所幸,当你创建新项目时,Creator 将会自动生成这样的 `tsconfig`。 diff --git a/versions/4.0/zh/scripting/usage.md b/versions/4.0/zh/scripting/usage.md new file mode 100644 index 0000000000..d467b3499a --- /dev/null +++ b/versions/4.0/zh/scripting/usage.md @@ -0,0 +1,12 @@ +# 脚本使用 + +该部分内容主要介绍如何在项目中使用脚本: + +- [访问节点和其他组件](./access-node-component.md) +- [常用节点和组件接口](./basic-node-api.md) +- [创建和销毁节点](./create-destroy.md) +- [使用计时器](./scheduler.md) +- [组件和组件执行顺序](./component.md) +- [加载和切换场景](./scene-managing.md) +- [获取和加载资源](./load-assets.md) +- [tsconfig 配置](./tsconfig.md) diff --git a/versions/4.0/zh/scripting/wasm-support.md b/versions/4.0/zh/scripting/wasm-support.md new file mode 100644 index 0000000000..f7df897190 --- /dev/null +++ b/versions/4.0/zh/scripting/wasm-support.md @@ -0,0 +1,20 @@ +# WebAssembly 支持 + +WebAssembly(缩写为 Wasm)是一种基于堆栈的虚拟机的二进制指令格式。 Wasm 被设计为编程语言的可移植编译目标,支持将 C++ 编译成 WebAssembly 并部署到客户端运行时。 + +Cocos Creator 从 3.x 版本已经开始尝试在引擎里使用 Wasm 来优化运算效率,截止 3.8.0 版本,目前已经将 bullet,physX,spine,WebGPU 这些模块使用 Wasm 实现。 + +## 包体优化 + +针对 Wasm 带来的包体问题,我们提供了一些构建的编译选项,来缓解包体问题。 + +### 小游戏平台引擎 Wasm 分包 + +在小游戏平台上,我们支持了引擎模块的 Wasm 分包,在构建面板上开启 **引擎 Wasm 分包**, 构建之后引擎的 Wasm 模块会被输出为平台的分包,进而减少主包包体的大小。 +注意:由于 physX 模块的包体太大,目前不支持在使用 physX 物理后端时,开启 **引擎 Wasm 分包**。 + +### Web 平台剔除 asm.js 模块 + +Web 平台 Wasm 模块构建后,输出的引擎包括模块的 Wasm 和 asm.js 实现,这是为了在一些不支持 Wasm 的浏览器上,能够将实现回滚到 asm.js 实现,以解决 Wasm 的兼容性问题。 +但是这带来的问题是增大了引擎的包体。我们在 Web 平台上提供了 **剔除引擎 asm.js 模块** 的优化选项,如果构建的时候开启这个选项,构建输出的引擎 Wasm 模块将不会附带 asm.js 实现,进而减少引擎的包体占用。 + diff --git a/versions/4.0/zh/shader/advanced-shader/skin.md b/versions/4.0/zh/shader/advanced-shader/skin.md new file mode 100644 index 0000000000..142f6e4779 --- /dev/null +++ b/versions/4.0/zh/shader/advanced-shader/skin.md @@ -0,0 +1,41 @@ +# 皮肤材质 + +在游戏中皮肤如果使用普通的 BRDF 材质往往表现不够红润和通透,因为现实中皮肤结构比较复杂,我们需要引用 Separable Subsurface Scattering 可分离次表面散射,还原皮肤的 SSS 效果,并使表现层次更丰富。 + +## 使用皮肤材质 + +1. 使用皮肤材质需要开启自定义管线。详细操作步骤请参考[自定义管线的功能开启](../../render-pipeline/custom-pipeline.md#功能开启)。 + +2. 使用皮肤材质需要一些额外的宏定义配置。在 **项目** 中选中 **项目设置**,然后点击 **引擎管理器** 的 **宏配置** 侧边选项。 + + - 勾选 **ENABLE_FLOAT_OUTPUT** 选项。如果不勾选,引擎每次启动也会自动检测是否有皮肤材质并更改此选项以保障正确的效果,但会产生警告信息。 + + ![image](skin/define.png) + +3. 使用皮肤材质需要为模型添加 **skin** 材质: + + - 新建材质。新建材质请参考 [材质资源](../../asset/material.md#材质创建) + - 在材质中的 **effect** 选择 **advanced/skin**。 + + ![image](skin/effect.png) + +4. 指定某个模型为全局 **skin** 模型:**可选项,通常是指定头或身体对应的模型**。 + 引擎需要知道皮肤材质对应的模型尺度以正确计算皮肤散射光照。若无勾选任何模型,会自动选择使用 skin 材质的模型来进行计算。 + + - 选中皮肤模型的 **节点**, 在 **属性检查器** 中找到 **cc.MeshRenderer** 组件,在组件配置菜单中勾选 **Is Global Standard Skin Object** 选项。 + + > **注意**:场景中只能在一个渲染组件中勾选 **Is Global Standard Skin Object**。重复在其他渲染组件中勾选会导致上一次勾选失效。 + + ![image](skin/MeshRenderPanel.png) + +## 调整 **skin** 参数 + +1. **skin** 全局面板中的配置。 + + - 在 **skin** 面板中调整皮肤效果。skin 配置面板请参考 [skin配置面板](../../concepts/scene/skin.md)。 + +2. **skin** 材质中的配置。 + + - 皮下散射强度:该值可以控制皮肤的润泽度和皮下散射强度,会在明暗过渡区域产生泛红的散射效果。针对单个模型调整皮肤材质强度时,可以在皮肤材质的 **属性查看器** 中找到 **Sss Intensity** 进行设置,也可以为皮肤材质添加一张 **USE SSS MAP** 图,精确的控制面部各个器官的皮肤效果强度。 + + ![image](skin/material.png) diff --git a/versions/4.0/zh/shader/advanced-shader/skin/CustomRenderPipeline.png b/versions/4.0/zh/shader/advanced-shader/skin/CustomRenderPipeline.png new file mode 100644 index 0000000000..fc6fbe5402 Binary files /dev/null and b/versions/4.0/zh/shader/advanced-shader/skin/CustomRenderPipeline.png differ diff --git a/versions/4.0/zh/shader/advanced-shader/skin/MeshRenderPanel.png b/versions/4.0/zh/shader/advanced-shader/skin/MeshRenderPanel.png new file mode 100644 index 0000000000..5d29ca2545 Binary files /dev/null and b/versions/4.0/zh/shader/advanced-shader/skin/MeshRenderPanel.png differ diff --git a/versions/4.0/zh/shader/advanced-shader/skin/define.png b/versions/4.0/zh/shader/advanced-shader/skin/define.png new file mode 100644 index 0000000000..9aaf06ad11 Binary files /dev/null and b/versions/4.0/zh/shader/advanced-shader/skin/define.png differ diff --git a/versions/4.0/zh/shader/advanced-shader/skin/effect.png b/versions/4.0/zh/shader/advanced-shader/skin/effect.png new file mode 100644 index 0000000000..b77610243f Binary files /dev/null and b/versions/4.0/zh/shader/advanced-shader/skin/effect.png differ diff --git a/versions/4.0/zh/shader/advanced-shader/skin/material.png b/versions/4.0/zh/shader/advanced-shader/skin/material.png new file mode 100644 index 0000000000..3f6837e4f7 Binary files /dev/null and b/versions/4.0/zh/shader/advanced-shader/skin/material.png differ diff --git a/versions/4.0/zh/shader/common-functions.md b/versions/4.0/zh/shader/common-functions.md new file mode 100644 index 0000000000..7bead6fd63 --- /dev/null +++ b/versions/4.0/zh/shader/common-functions.md @@ -0,0 +1,28 @@ +# 公共函数库 + +可以在 **资源管理器/internal/chunks/common/** 文件夹下找到不同分类的函数库头文件。 + +公共库中的函数不依赖任何内部数据,可以当作工具函数直接使用。 + +## 使用示例 + +```ts +#include +#include +``` + +## 目录功能对照表 + +| 文件夹名 | 函数用途 | 主要文件 +| -------- | ---------------------------------------- | ---- +| color | 色彩相关功能(颜色空间、tone-mapping 等) | aces, gamma +| data | 数据相关功能(压缩解压缩等) | packing, unpack +| debug | Debug View 相关功能 | +| effect | 场景特效相关功能(水、雾等) | fog +| lighting | 光照相关功能(bxdf、反射、衰减、烘焙等) | brdf, bxdf, light-map +| math | 数学库(坐标变换、数值判定和运算等) | coordinates, transform +| mesh | 模型相关功能(材质转换、模型动画等) | material, vat-animation +| shadow | 阴影相关功能(pcf、pcss 等) | native-pcf +| texture | 贴图相关功能(采样、mip 计算等) | cubemap, texture-lod + +> Surface Shader 内部已经自动包含了常用的公共函数头文件,不需要再 include。 diff --git a/versions/4.0/zh/shader/compute-shader.md b/versions/4.0/zh/shader/compute-shader.md new file mode 100644 index 0000000000..ce86608e29 --- /dev/null +++ b/versions/4.0/zh/shader/compute-shader.md @@ -0,0 +1,259 @@ +# 计算着色器 + +计算着色器(Compute Shader,以下简称 CS)是 GPU 上执行通用计算任务的编程模型。Cocos CS 继承 GLSL 语法与内置变量,添加方式与光栅化 Shader 相同以 effect 形式呈现,**仅支持在自定义管线**中使用。Compute Shader 无光栅化过程,输入输出均为内存数据。通过多线程完成并行处理,处理大量数据时非常高效。 + +## Effect 定义方式 + +定义方式同光栅化 Shader,在计算着色器下配置 PipelineState 无意义。 + +``` +CCEffect %{ +  techniques: +  - name: opaque +    passes: +    - compute: compute-main // 定义 cs 入口 +      pass: user-compute // 定义 layout +      properties: &props +        mainTexture: { value: grey } // 注册着色器材质面板资源属性 +}% +CCProgram compute-main %{ +  precision highp float; + precision mediump image2D; + layout(local_size_x = 8, local_size_y = 4, local_size_z = 1) in; + + #pragma rate mainTexture batch + uniform sampler2D mainTexture; + + #pragma rate outputImage pass + layout (rgba8) writeonly uniform image2D outputImage; + +  void main () { +    imageStore(outputImage, ivec2(gl_GlobalInvocationID.xy), vec4(1, 0, 0, 1)); +  } +}% +``` + +如要查看更多语法,请移步 [着色器语法](./effect-syntax.md) + +## 输入输出 + +CS 输入输出包含内置输入变量与着色器资源。 + +内置输入包含以下部分,与 GLSL 语义相同: + +``` +in uvec3 gl_NumWorkGroups; +in uvec3 gl_WorkGroupID; +in uvec3 gl_LocalInvocationID; +in uvec3 gl_GlobalInvocationID; +in uint gl_LocalInvocationIndex; +layout(local_size_x = X, local_size_y = Y, local_size_z = Z) in; +``` + +着色器资源包含: +- UniformBuffer +- StorageBuffer +- ImageSampler +- StorageImage +- SubpassInput + +CS 无内置输出,可通过 StorageBuffer / Image 输出。 + +## 着色器资源 + +CS 目前支持 **PerPass**,**PerBatch** 两种频率的资源绑定,如下所示: + +```c +#pragma rate mainTexture batch +uniform sampler2D mainTexture; + +#pragma rate outputImage pass +layout (rgba8) writeonly uniform image2D outputImage; +``` + +其中 PerPass 资源可以定义为需要管线跟踪处理同步问题的资源,PerBatch 通常为常量数据、静态纹理,可通过 Material 进行修改绑定。 + +PerBatch mainTexture 在 properties 中绑定后,可在材质面板中配置。 + +PerPass outputImage 需要在管线中声明并由 ComputePass 引用,并由 RenderGraph 管理数据的读写同步以及 ImageLayout,详见后文示例。 + +## 管线集成 + +Cocos 自定义管线添加 CS 过程分为三步: + +1. 添加 Compute Pass,其中 `passName` 为当前 Pass 的 LayoutName,需要与 Effect 中的 pass 字段对应。 + + ```ts + const csBuilder = pipeline.addComputePass('passName'); + ``` + +2. 声明、引用资源,并关联资源访问类型与 Shader 资源槽位。 + + ```cpp + const csOutput = 'cs_output'; + if (!pipeline.containsResource(csOutput)) { + pipeline.addStorageTexture(csOutput, + gfx.Format.RGBA8, + width, height, + rendering.ResourceResidency.MANAGED); + } else { + pipeline.updateStorageTexture(csOutput, + width, height, + gfx.Format.RGBA8); + } + + csBuilder.addStorageImage(csOutput, // 资源名 + rendering.AccessType.WRITE, // 内存访问类型 + 'outputImage'); // 着色器资源名 + ``` + +3. 添加 Dispatch 调用实例,设置 Dispatch 参数,绑定材质 + + ```cpp + csBuild.addQueue().addDispatch(x, y, z, rtMat); + ``` + +## 平台支持 + +### 特性支持 + +| | WebGL | WebGL2 | Vulkan | Metal | GLES3 | GLES2 | +| -------- | ----- | ------ | ------ | ----- | ------ | ----- | +| 支持情况 | N | N | Y | Y | Y(3.1) | N | + +可通过 `device.hasFeature(gfx.Feature.COMPUTE_SHADER)` 查询。 + +### 约束限制 + +- `maxComputeSharedMemorySize`: 本地共享内存的最大字节数。 +- `maxComputeWorkGroupInvocations`: 一个 WorkGroup 内的最大调用次数,即 Local WorkGroup 体积 +- `maxComputeWorkGroupSize`: Local WorkGroup 三维数组限制 +- `maxComputeWorkGroupCount`: Dispatch 三维数组限制 + +可通过 `device.capabilities` 查询。 + +### 平台差异 + +Cocos Creator 会将 Cocos CS 转化为平台对应版本的 glsl shader,因此为了能够保证各个平台的兼容性,需要尽量满足所有平台的限制要求,包括: +1. Vulkan 与 GLES 要求显式标明 Storage Image 的 Format 标识符,详细参考 GLSE 语法标准。 +2. GLES 要求显式标明 Stroage 资源的 Memory 标识符,目前仅支持 readonly 与 writeonly。此外需要显式标识默认精度。 + +### 优化建议 + +1. 进行屏幕空间图像后处理时,可优先考虑 Fragment Shader。避免 RenderTarget 结果中途切换 CS 进行写操作。 +2. 避免使用较大的工作组,尤其在使用 shared memory 情况下,每个 WorkGroup 大小建议不超过 64。 + +## 示例 + +下文展示了一个通过 ComputePass 实现的单个球 1 rpp 的简单光线追踪演示代码,使用到了 UniformBuffer,ImageSampler 与 StorageImage,其中 Effect Pass 声明部分如下: + +```yaml +techniques: +- name: opaque + passes: + - compute: compute-main + pass: user-ray-tracing + properties: &props + mainTexture: { value: grey } +``` + +`compute-main` 实现部分如下: + +```c +precision highp float; +precision mediump image2D; + +layout(local_size_x = 8, local_size_y = 4, local_size_z = 1) in; + +#pragma rate tex batch +uniform sampler2D tex; + +#pragma rate constants pass +uniform constants { + mat4 projectInverse; +}; + +#pragma rate outputImage pass +layout (rgba8) writeonly uniform image2D outputImage; + +void main () { + vec3 spherePos = vec3(0, 0, -5); + vec3 lightPos = vec3(1, 1, -3); + vec3 camPos = vec3(0, 0, 0); + float sphereRadius = 1.0; + + vec4 color = vec4(0, 0, 0, 0); + + ivec2 screen = imageSize(outputImage); + ivec2 coords = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y); + vec2 uv = vec2(float(coords.x) / float(screen.x), float(coords.y) / float(screen.y)); + + vec4 ndc = vec4(uv * 2.0 - vec2(1.0), 1.0, 1.0); + vec4 pos = projectInverse * ndc; + vec3 camD = vec3(pos.xyz / pos.w); + vec3 rayL = normalize(camD - camPos); + + vec3 dirS = spherePos - camPos; + vec3 rayS = normalize(dirS); + float lenS = length(dirS); + + float dotLS = dot(rayL, rayS); + float angle = acos(dotLS); + float projDist = lenS * sin(angle); + + if (projDist < sphereRadius) { + // intersection + vec3 rayI = rayL * (lenS * dotLS - sqrt(sphereRadius * sphereRadius - projDist * projDist)); + + vec3 N = normalize(rayI - dirS); + vec3 L = normalize(lightPos - rayI); + color = vec4(vec3(max(dot(N, L), 0.05)), 1.0); + } + + imageStore(outputImage, coords, color); +} +``` + +管线 API 调用如下: + +```ts +export function buildRayTracingComputePass( + camera: renderer.scene.Camera, + pipeline: rendering.Pipeline) { + // 获取屏幕长宽 + const area = getRenderArea(camera, + camera.window.width, + camera.window.height); + const width = area.width; + const height = area.height; + + // 声明 RT Storage Image 资源 + const csOutput = 'rt_output'; + if (!pipeline.containsResource(csOutput)) { + pipeline.addStorageTexture(csOutput, + gfx.Format.RGBA8, + width, height, + rendering.ResourceResidency.MANAGED); + } else { + pipeline.updateStorageTexture(csOutput, + width, height, + gfx.Format.RGBA8); + } + + // 声明 Compute Pass, layout 需要与 Effect 中 pass 字段保持一致 + const cs = pipeline.addComputePass('user-ray-tracing'); + // 更新相机投影参数 + cs.setMat4('projectInverse', camera.matProjInv); + // 声明当前 Compute Pass 对 Storage Image 引用 + cs.addStorageImage(csOutput, rendering.AccessType.WRITE, 'outputImage'); + // 添加 Dispatch 参数, 绑定 Material + cs.addQueue() + .addDispatch(width / 8, height / 4, 1, rtMat); + // 返回当前 Image 资源名,用于后续 Post Processing 处理 + return csOutput; +} +``` + +Compute Pass 需要用户完成对 PerPass 资源的更新与绑定,PerBatch 的资源会由材质系统完成绑定,最终经过 FullScreen Blit 到屏幕的效果: + +![Cocos Effect](img/compute-shader-rt.png) diff --git a/versions/4.0/zh/shader/effect-builtin-pbr.md b/versions/4.0/zh/shader/effect-builtin-pbr.md new file mode 100644 index 0000000000..b5f0f91d83 --- /dev/null +++ b/versions/4.0/zh/shader/effect-builtin-pbr.md @@ -0,0 +1,197 @@ +# 基于物理的光照模型(Physically Based Rendering - PBR) + +Cocos Creator 从 v3.0 开始提供了基于物理渲染(PBR)的光照着色器:`builtin-standard.effect`。PBR 根据现实中光线传播原理和能量守恒定律,模拟出近似于真实物理光照的效果。 + +PBR 的优势在于: + +- 真实性:基于物理原理的渲染让最终效果更加逼真 +- 一致性:美术制作流程规范化、制作标准统一化 +- 复用性:模型材质与光照环境分离,在所有 PBR 项目中均可复用 + +## 使用 PBR 制作材质和纹理 + +在 **资源管理器** 面板中手动创建的材质,默认使用的是 `builtin-standard.effect` 着色器,我们称之为 PBR 材质,PBR 材质使用 PBR 流程中的 Metal/Roughness 工作流。 + +在使用 PBR 材质进行渲染时,为获得正确的渲染效果,至少需要设置材质的 **固有色(Albedo)**、**粗糙度(Roughness)** 和 **金属度(Metallic)**。这些都可以在材质资源属性面板中进行设置: + +![image](./img/pbr-maps.png) + +除了在材质属性面板中直接赋予数值以外,也可以为材质的固有色(Albedo)、粗糙度(Roughness)、金属度(Metallic)赋予 **贴图**,以更精准地进行材质表达。除此之外,可以为材质赋予法线(Normal)贴图以获得更多表面结构细节,环境光遮蔽(Ambient Occlusion) 贴图以获得细节明暗关系,自发光(Emissive)贴图以获得自发光效果。 + +接下来我们以下图为例,看一下通过设置上述材质参数获得的效果: + +![flakes.jpg](./img/final_alarmclock.jpg) + +### 固有色(Albedo) + +![flakes.jpg](./img/albedo.jpg) + +固有色(Albedo)用于表达材质在没有光照情况下所表达的颜色信息。美术上,可以将固有色理解为用肉眼观察时材质所表达的颜色信息。 + +在 PBR 流程中,固有色代表的是材质 **非金属** 部分的 **漫反射**(Diffuse)颜色,与材质 **金属** 部分的 **高光**(Specular)颜色的集合。 + +> **注意**:在 Metal / Roughness 工作流中,所有金属的漫反射(Diffuse)颜色都是黑色,肉眼所见的颜色表现是由金属反射光线所产生的。而在非 Metal / Roughness 工作流中,金属的颜色是由高光(Specular)颜色决定。使用非 Metal / Roughness 工作流制作的颜色贴图,将不能在 Cocos Creator 默认的 PBR 材质中正确渲染金属的颜色信息。 + +用户可以在材质属性面板的 **Albedo** 属性中直接设置固有色颜色,也可以勾选 `USE ALBEDO MAP`,为材质指定一张 sRGB 颜色空间的 RGBA 贴图。 + +![albedo](img/albedo-map.png) + +依据标准 PBR 流程的制作准则,为了获得符合物理现实的渲染效果,在制作固有色贴图的过程中需要注意: +- 固有色的 sRGB 数值最高不超过 **240**,根据材质的不同最低不低于 **30~50**。 +- 在表达金属的固有色时,需要遵循金属 **70%~100%** 镜面反射率的物理规律,其 sRGB 取值范围为 **180~255**。 + +### 粗糙度(Roughness) + +![flakes.jpg](./img/roughness.jpg) + +粗糙度(Roughness)用于表达材质因其表面细微的结构细节所导致的反光强弱程度,其取值范围为 [0, 1]。 + +- 当粗糙度设置为 0 时,表示材质表面绝对光滑,镜面反射率达到 100%。 +- 当粗糙度设置为 1 时,表示材质表面绝对粗糙,镜面反射率为 0% + +用户可以在材质属性面板的 **Roughness** 属性中直接设置粗糙度。或者为 PBR 材质指定一张 sRGB 颜色空间的 RGBA 贴图,通过贴图的 **绿通道** 表达粗糙度,操作步骤如下: + +在 **资源管理器** 中选中 PBR 材质资源,然后在 **属性检查器** 中勾选 **USE PBR MAP** 属性,将 RGBA 贴图拖拽到出现的 **PbrMap** 属性框中; + +![matallic 贴图](img/use-pbr-map.png) + +### 金属度(Metallic) + +![flakes.jpg](./img/metallic.jpg) + +金属度(Metallic)用于表达材质的金属属性,其取值范围为 [0, 1]。在使用过程中,通常设置为 0 或者 1。 + +- 当金属度为 **0** 时,表示材质为非金属。 +- 当金属度为 **1** 时,表示材质为金属。 +- 当金属度为 **0~1** 之间的浮点数时,通常用于表示表面带有非金属脏迹的金属。 + +> **注意**:当金属度设置为 1 时,材质被认定为金属。因为随着金属度的提升,材质会发生以下变化,进而使其表现出金属的特征: +> +> - 固有色比金属度为 0 时的明度和饱和度更低; +> - 材质高光部分的颜色混合了材质的固有色; +> - 反射光更趋近于固有色。 + +用户可以在材质属性面板的 **Metallic** 属性中直接设置金属度。或者为材质指定一张 sRGB 颜色空间的 RGBA 贴图,通过贴图的 **蓝通道** 表达金属度,操作步骤如下: + +在 **资源管理器** 中选中 PBR 材质资源,然后在 **属性检查器** 中勾选 **USE PBR MAP** 属性,将 RGBA 贴图拖拽到出现的 **PbrMap** 属性框中; + +![粗糙度贴图](img/use-pbr-map.png) + +### 环境光遮蔽(Ambient Occlusion) + +![flakes.jpg](./img/ao.jpg) + +环境光遮蔽(Ambient Occlusion)用于表达材质因表面的结构细节所导致的明暗关系。美术上,可以将环境光遮蔽理解为模型自身结构所产生的阴影。 + +用户可以为 PBR 材质指定一张 sRGB 颜色空间的 RGBA 贴图,通过贴图的 **红通道** 表达环境光遮蔽关系,操作步骤如下: + +1. 在 **资源管理器** 中选中 PBR 材质资源,然后在 **属性检查器** 中勾选 **USE PBR MAP** 属性,将 RGBA 贴图拖拽到出现的 **PbrMap** 属性框中; + +2. 勾选 **USE OCCLUSION MAP**,将 RGBA 贴图拖拽到出现的 **OcclusionMap** 属性框中。 + +![环境光遮蔽](img/occlusion-map.png) + +### 法线(Normal) + +![flakes.jpg](./img/normal.jpg) + +法线(Normal)贴图是一张用 sRGB 颜色空间的 RGB 数值代表模型切线空间的顶点坐标位置的贴图。其作用是将贴图中的顶点坐标数据叠加到模型自身的顶点坐标数据上参与 PBR 光影的计算,使顶点数量较低的低模也能够表现顶点数量较高的高模的光影变化效果。美术上,可以将法线贴图理解为一张表达物体表面结构细节的贴图。 + +法线贴图通常有两种制作方法: +1. 分别制作一个顶点数量较高的高模和一个顶点数量较低的低模,将高模的顶点坐标数据烘培到一张使用低模的 UV 的贴图上 +2. 将一张图片资源的类型转换为法线贴图 + +![法线贴图](img/normal-map.png) + +> **注意**:在从高模烘培法线时,请确保烘培器使用右手坐标系(Y 轴向上)和 MIKK 切线空间算法。 + +### 自发光(Emissive) + +自发光颜色(Emissive)表达材质自身作为光源向外发光的颜色信息。 + +用户可以在材质属性面板的 **Emissive** 属性中直接设置自发光。或者为 PBR 材质指定一张 sRGB 颜色空间的 RGBA 贴图,操作步骤如下: + +1. 在 **资源管理器** 中选中 PBR 材质资源,然后在 **属性检查器** 中勾选 **USE EMISSIVE MAP** 属性,将 RGBA 贴图拖拽到出现的 **EMISSIVEMAP** 属性框中; + +2. 然后通过 **EmissiveScale** 属性调节自发光颜色的红、绿、蓝通道的发光强度。 + + > **注意**:通常将 **EmissiveScale** 属性值设置为 **1** 以上来配合自发光使用。当 **EmissiveScale** 设置为 1 时,材质的自发光效果等同于 `builtin-unlit.effect` 材质。 + +![自发光(Emissive)](img/emission-map.png) + +### 模板遮罩(Stencil) + +![flakes.jpg](./img/leaves.jpg) + +当渲染使用了模板遮罩(Stencil)的材质时,可开启 PBR 材质的 Alpha Test 功能,将遮罩之外的片元去除。步骤如下: + +- 将模板遮罩(Stencil)作为 **Alpha 通道** 或 **红通道**,存储在固有色贴图中; +- 在 **资源管理器** 中创建一个新的材质,参考上文 **固有色** 部分的内容,将固有色贴图挂载到新的材质上; +- 勾选 **USE ALPHA TEST**,在下方显示的 **ALPHA TEST CHANNEL** 属性中选择模板遮罩(Stencil)所在的 Alpha 通道(对应属性中的 **a** 项)或者红通道(对应属性中的 **r** 项); +- 在 `ALPHA TEST CHANNEL` 参数中选择模板遮罩(Stencil)所在的通道(Alpha 通道或红通道); +- 通过 **AlphaThreshold** 属性调节抛弃片元明度的阈值; +- 如果有需要,可参考上文实现法线、环境光遮蔽等效果。 + +![模板遮罩(Stencil)](img/alpha-cutoff.png) + +### 透明材质 + +材质属性面板中的 **Technique** 属性用于渲染透明或半透明材质,当设置为 **1-transparent** 时,将开启 Alpha Blending 功能,切换为渲染透明材质模式。 + +![透明材质](img/transparency.png) + +当切换到透明材质模式时,材质所有的功能与不透明模式没有差别。用户可以依照上述的工作流程进行材质制作。 + +### PBR 主要参数组装流程 + +![pbr 组装流程](../material-system/standard-material-graph.png) + +## PBR 宏定义 + +| 宏定义 | 说明 | +| :---- | :--- | +| USE_INSTANCING | 是否启用几何体实例化 | +| HAS_SECOND_UV | 是否存在第二套 UV | +| USE_TWOSIDE | 是否开启双面 | +| IS_ANISOTROPY | 是否开启各向异性光照 | +| USE_VERTEX_COLOR | 是否启用顶点颜色。如果启用,顶点色会与固有色颜色相乘 | +| FIX_ANISOTROPIC_ROTATION_MAP | 是否使用各向异性光照旋转图 | +| USE_ALBEDO_MAP | 是否使用固有色射贴图 | +| ALBEDO_UV | 指定采样固有色贴图使用的 UV,默认使用第一套 | +| USE_NORMAL_MAP | 是否使用法线贴图 | +| DEFAULT_UV | 默认 UV,默认使用第一套 | +| USE_PBR_MAP | 是否使用 PBR 参数三合一贴图(**按 glTF 标准,RGB 通道必须分别对应环境光遮蔽、粗糙度和金属度**) | +| USE_OCCLUSION_MAP | 是否使用环境光遮蔽贴图(**按 glTF 标准,只会使用 R 通道**) | +| USE_EMISSIVE_MAP | 是否使用自发光贴图 | +| EMISSIVE_UV | 指定采样自发光贴图使用的 UV,默认使用第一套 | +| USE_ALPHA_TEST | 是否开启透明测试(镂空效果) | +| ALPHA_TEST_CHANNEL | 指定透明测试的测试通道,默认为 A 通道 | + +更多 PBR 原理相关的内容,请参考 [PBR 理论](https://learnopengl-cn.github.io/07%20PBR/01%20Theory/#pbr)。 + +## PBR 参数 + +在外部代码编辑器中打开 **资源管理器** 面板中 `internal -> effects` 目录下的 `builtin-standard.effect`,可以看到参数如下: + +![PBR 参数](img/pbr-param.png) + +| 参数 | 说明 | +| :------- | :--- | +| tilingOffset | 模型 UV 的缩放和偏移量,xy 对应缩放,zw 对应偏移 | +| mainColor | 用于指定模型的固有色或固有色贴图,在 **属性检查器** 中的名称显示为 **albedo** | +| albedoMap/mainTexture | 固有色贴图,如果有指定,这项会和固有色颜色相乘 | +| albedoScale | 模型的固有色强度,用于控制固有色颜色对于最终颜色的影响权重 | +| alphaThreshold | 半透明测试阈值,alpha 值低于此值的像素会被 discard 掉 | +| normalMap | 法线贴图,用于增加表面细节 | +| normalStrength | 法线贴图强度,控制凹凸质感的强弱 | +| pbrMap
**R**(AO)
**G**(Roughness)
**B**(Metallic) | PBR 材质参数贴图,采样结果会和常数项相乘
R 通道:环境光遮蔽
G 通道:粗糙度
B 通道:金属度 | +| occlusionMap | 独立的环境光遮蔽贴图
采样结果会和常数项相乘 | +| occlusion | 环境光遮蔽系数 | +| roughness | 粗糙度系数 | +| metallic | 金属度系数 | +| specularIntensity | 高光强度,该值相当于基准反射率 F0 的倍增,仅对非金属有效 +| emissive | 自发光颜色,独立于光照计算,由模型本身直接发散出的颜色 | +| emissiveMap | 自发光贴图
如果有指定,这项会和自发光颜色相乘,因此需要把自发光颜色(默认是黑色)调高才会有效果 | +| emissiveScale | 自发光强度
用于控制自发光颜色对于最终颜色的影响权重 | + +想了解更多关于内置 PBR 材质的实现细节,请前往 [内置 Surface Shader](./surface-shader/builtin-surface-shader.md)。 diff --git a/versions/4.0/zh/shader/effect-builtin-toon.md b/versions/4.0/zh/shader/effect-builtin-toon.md new file mode 100644 index 0000000000..aa000103b1 --- /dev/null +++ b/versions/4.0/zh/shader/effect-builtin-toon.md @@ -0,0 +1,118 @@ +# 卡通渲染 + +相对于 [真实渲染](effect-builtin-pbr.md)(Physical Based Rendering - PBR),非真实渲染(Non-Photorealistic Rendering - NPR)通过特性化渲染,实现与真实世界完全不同的美术表现。 + +卡通渲染(Toon Shading)是非真实渲染的常见效果之一。 + +通常卡通渲染的内容包含以下几个基础部分: + +- 对物体进行描边 +- 降低色阶的数量并模拟色阶不连续现象 +- 明暗色调分离 +- 阴影形状干扰等 + +![toon](img/toon.png) + +## 渲染过程 + +Cocos Creator 提供了内置卡通渲染着色器 `builtin-toon.effect`,以此为例,我们在材质资源中将 **Effect** 属性切换为 `builtin-toon.effect`,可以看到卡通渲染由两个渲染过程(Pass)组成: + +![渲染过程](img/toon-pass.png) + +- 渲染过程 0(Pass 0):用于描边,默认不启用,可勾选右侧的 **USE_OUTLINE_PASS** 开启。 +- 渲染过程 1(Pass 1):正常渲染模型 + +### 渲染过程 0 + +渲染过程 0 会将光栅化状态中的剔除模式选择为正面剔除(`CullMode = FRONT`)并将模型的顶点沿法线进行扩张,由此得到一个比原模型较大的单色模型,之后 **渲染过程 1** 会正常渲染一次模型并遮盖住 **渲染过程 0** 的渲染结果,由于 **渲染过程 1** 的模型尺寸小于 **渲染过程 0** 的尺寸,因此会留下一个纯色的边缘形成描边。 + +![cull-front](img/cull-front.png) + + **渲染过程 0** 可通过勾选 **USE_OUTLINE_PASS** 开启或关闭。 + +当勾选 **USE_OUTLINE_PASS** 开启 **渲染过程 0** 的描边功能后,效果图如下: + +![USE_OUTLINE_PASS 开启](img/outline-on.png) + +若需要调整描边的深度效果,可通过 **DepthBias** 属性进行调整: + +![DepthBias](img/toon-depth-bias.png) + +当不勾选 **USE_OUTLINE_PASS** 关闭 **渲染过程 0** 的描边功能后,效果图如下: + +![USE_OUTLINE_PASS 关闭](img/outline-off.png) + +### 渲染过程 1 + +卡通渲染的核心思路是通过降低色阶的数量,模拟卡通中的赛璐璐(Celluloid)画风。 + +在着色器中将色阶降低为三个色阶,并通过三个颜色组成: + +- **baseColor**:基础颜色 +- **shadeColor1**:一阶着色的颜色 +- **shadeColor2**:二阶着色的颜色 + +其颜色与编辑器材质属性的对应关系如下图: + +![toon-shade-color](img/shade-color.png) + +勾选 **USE_1ST_SHADE_MAP** 和 **USE_2ND_SHADE_MAP** 的情况下,使用外部纹理进行模拟色阶不连续现象。 + +![shade map](img/shade-map.png) + +通过着色器的 `surf` 方法计算表面着色器(`ToonSurface`)的参数,并由 `CCToonShading` 方法计算最终的着色。 + +![surf 代码](img/toon-surf.png) + +## 参数和预编译宏定义 + +### 渲染过程 0 + +| 属性 | 说明 | +| :------------- | :---------------------------------------------------------------- | +| lineWidth | 描边的宽度 +| depthBias | 描边的深度位移调整因子 +| baseColor | 描边的颜色 + +### 渲染过程 1 + +| 属性 | 说明 | +| :------------- | :---------------------------------------------------------------- | +| tilingOffset | 模型 UV 的缩放和偏移量,xy 对应缩放,zw 对应偏移| +| mainColor | 主颜色,该颜色会作为最初的色阶| +| colorScale | 颜色缩放,对主颜色,一阶和二阶颜色的 RGB 通道相乘 | +| alphaThreshold | 设置 Alpha 测试阀值,Alpha 值低于设定值的像素将被抛弃。该项仅在勾选 **USE_ALPHA_TEST** 后显示 | +| shadeColor1 | 一阶色阶的颜色,该颜色会作为卡通着色的中间色阶 | +| shadeColor2 | 二阶色阶的颜色,该颜色会作为卡通着色的最后一个色阶 | +| specular | 高光颜色 | +| baseStep | 一阶着色的步长 | +| baseFeather | 一阶着色和主颜色混合因子,和 **BaseStep** 属性配合调整一阶色阶所占比例以及混合的形式 | +| shadeStep | 二阶着色的步长 | +| shadeFeather | 二阶着色和一阶着色的混合因子,和 **ShadeStep** 属性配合调整二阶色阶所占比例以及混合的形式 | +| shadowCover | 阴影遮蔽因子 | +| emissive | 自发光颜色,独立于光照计算,由模型本身直接发散出的颜色 | +| emissiveScale | 自发光强度,用于控制自发光颜色对最终颜色的影响权重 | +| normalStrength | 法线伸缩量
只有在启用 USE_NORMAL_MAP 后才可以调整并对法线的 xy 轴进行缩放| +| normalMap | 法线贴图
只有在启用 USE_NORMAL_MAP 后才可以调整 | +| mainTexture | 主纹理,定义物体的基础纹理
编辑器内显示为 **baseColorMap**,需启用 USE_BASE_COLOR_MAP 宏才可以调整 | +| shadeMap1 | 一阶色阶纹理,若指定则会和设定的 ShadeColor1 相乘。该项仅在勾选 **USE 1ST SHADER MAP** 后显示 | +| shadeMap2 | 二阶色阶纹理,若指定则会和设定的 ShadeColor2 相乘。该项仅在勾选 **USE 2ND SHADER MAP** 后显示 | +| specularMap | 高光贴图,若指定了贴图,则会和高光颜色相乘。该项仅在勾选 **USE SPECULAR MAP** 后显示 | +| emissiveMap | 自发光贴图,若指定则会和自发光颜色相乘,因此需要将自发光颜色(默认是黑色)中的 RGBA 调高才会有效果。该项仅在勾选 **USE EMISSIVE MAP** 后显示 | + +## 宏定义 + +| 宏名 | 说明 | +| :---------------------------- | :------------------------ | +| USE_INSTANCING | 是否启用动态 instancing | +| USE_OUTLINE_PASS | 是否启用描边 Pass | +| USE_NORMAL_MAP | 是否使用法线贴图 | +| USE_BASE_COLOR_MAP | 是否使用基础贴图 | +| USE_1ST_SHADE_MAP | 是否使用贴图作为一阶色阶 | +| USE_2ND_SHADE_MAP | 是否使用贴图作为二阶色阶图 | +| USE_EMISSIVE_MAP | 是否使用自发光贴图 | +| USE_ALPHA_TEST | 是否进行半透明测试 | +| USE_SPECULAR_MAP | 是否使用高光贴图 | +| BASE_COLOR_MAP_AS_SHADE_MAP_1 | 使用 baseColorMap 作为一阶着色 | +| BASE_COLOR_MAP_AS_SHADE_MAP_2 | 使用 baseColorMap 作为二阶着色 | +| SHADE_MAP_1_AS_SHADE_MAP_2 | 二阶着色是否和一阶着色叠加| diff --git a/versions/4.0/zh/shader/effect-builtin-unlit.md b/versions/4.0/zh/shader/effect-builtin-unlit.md new file mode 100644 index 0000000000..e03f06a3ed --- /dev/null +++ b/versions/4.0/zh/shader/effect-builtin-unlit.md @@ -0,0 +1,36 @@ +# 无光照 + +无光照是最基础的着色模型,这种模型下,引擎的任何光源都无法影响其最终效果,适用于: + +- 不受光源影响的物体 +- 画面要求不高或性能要求高的场景 + +在材质的 **Effect** 属性中将着色器切换为 Cocos Creator 内置的无光照着色器(builtin-unlit.effect)时,如下图: + +![unlit](img/unlit-shademode.png) + +## 制作标准 + +技术选型时,若要在使用 **unlit** 材质的模型下使用光照,可将光照信息绘制在纹理贴图上,然后将纹理贴图拖拽到材质的 **MainTexture** 属性框中。 + +若要使用真实光照,可参考:[真实物理渲染](effect-builtin-pbr.md) + +## 参数 + +| 参数 | 说明 | +| :--- | :--- | +| mainTexture | 主纹理| +| tilingOffset | 模型 UV 的缩放和偏移量,xy 对应缩放,zw 对应偏移| +| mainColor | 主颜色,该颜色会在片元着色器内被处理 | +| colorScale | 和主颜色相乘 | +| alphaThreshold | 用于半透明测试,在启用 USE_ALPHA_TEST 的情况下,小于该值的像素会被抛弃(discard)| + +## 宏 + +| 定义| 说明 | +| :--- | :---- | +| USE_INSTANCING | 是否启用几何体实例化 | +| USE_VERTEX_COLOR | 是否叠加顶点颜色和 Alpha 值 | +| USE_TEXTURE | 是否使用主纹理(mainTexture) | +| USE_ALPHA_TEST | 是否进行半透明测试(AlphaTest)| +| SAMPLE_FROM_RT | 是否是从 RenderTexture 中采样,勾选后会对 UV 的 Y 值进行翻转 | diff --git a/versions/4.0/zh/shader/effect-builtin.md b/versions/4.0/zh/shader/effect-builtin.md new file mode 100644 index 0000000000..2f3f2af482 --- /dev/null +++ b/versions/4.0/zh/shader/effect-builtin.md @@ -0,0 +1,54 @@ +# 内置着色器 + +引擎提供了一系列通用的内置着色器,位于编辑器 **资源管理器** 面板的 `internal -> effects` 目录下。双击着色器文件即可在外部 IDE 打开进行查看和编辑(前提是需要在 **偏好设置 -> 外部程序** 中配置 **默认脚本编辑器**)。 + +Cocos Creator 将内置着色器大致归类为以下几种: + +- `internal`:内置引擎功能相关着色器,比如编辑器内用 gizmo,几何体渲染等等。用户通常不需要关注这些。 +- `pipeline`:管线特效着色器,包括延迟光照、后效和抗锯齿等。 +- `util`:存放一些零散的内置着色器,例如 DCC 材质导入和序列帧动画等。用户通常不需要关注这些。 +- `for2d`:2D 渲染相关着色器,如 spine 和 sprite 动画等。 +- `particles`:粒子特效相关着色器。 +- `advanced`:基于 [表面着色器](surface-shader.md) 制作的一些高级材质,如水面、皮肤、头发、玉石等等,引擎会持续迭代。 +- 其他的为内置着色器,详情请参考下文说明。 + +## 内置管线特效着色器 + +![管线着色器](img/pipeline-effect.png) + +| 着色器名称 | 说明 | +| :----------------------- | :--------------------------- | +| bloom.effect | 全屏泛光特效 | +| deferred-lighting.effect | 用于延迟管线中的光照处理阶段 | +| planar-shadow.effect | 平面阴影 | +| post-process.effect | 后处理 | +| skybox.effect | 天空盒 | +| smaa.effect | SMAA 抗锯齿 | +| tonemap.effect | 引擎预留 | + +## 内置着色器 + +![内置着色器](img/builtin-effect.png) + +| 内置材质着色器名称 | 说明 | +| :--- | :--- | +| builtin-standard.effect | [基于物理的光照模型 PBR](effect-builtin-pbr.md) | +| builtin-terrain.effect | 地形系统默认着色器 | +| builtin-billboard.effect | 公告板
公告板是一种使物体始终朝向摄像机的渲染方案,适用于树木,血条等渲染 | +| builtin-toon.effect | [卡通渲染](effect-builtin-toon.md) | +| builtin-camera-texture.effect | 相机纹理 | +| builtin-unlit.effect | [无光照](effect-builtin-unlit.md) | +| builtin-clear-stencil.effect | 清理模板缓存 | + +| 其他内置着色器名称 | 说明 | +| :--- | :--- | +| builtin-graphics.effect | [Graphics 组件](../ui-system/components/editor/graphics.md) 的着色器 | +| builtin-occlusion-query.effect | 遮挡查询 | +| builtin-particle-trail.effect | 粒子拖尾 | +| builtin-particle.effect | 基于 CPU 渲染的粒子着色器 | +| builtin-particle-gpu.effect | 基于 GPU 渲染粒子着色器
请参考 [粒子渲染器](../particle-system/renderer.md) 了解两个着色器的用法 | +| builtin-reflection-deferred.effect | 用于延迟着色中的反射处理 | +| builtin-spine.effect | Spine 骨骼动画的着色器 | +| builtin-sprite.effect | 精灵着色器 | +| builtin-sprite-gpu.effect | 引擎预留 | +| builtin-wireframe.effect | 以线框模式进行绘制 | diff --git a/versions/4.0/zh/shader/effect-chunk-index.md b/versions/4.0/zh/shader/effect-chunk-index.md new file mode 100644 index 0000000000..f16a2b86d1 --- /dev/null +++ b/versions/4.0/zh/shader/effect-chunk-index.md @@ -0,0 +1,47 @@ +# 着色器片段(Chunk) + +着色器片段(Chunk)是一种跨文件代码引用机制,使着色器代码片段可以在不同的文件之间进行复用。 + +着色器片段的语法基于 **GLSL 300 ES**,在资源加载时会进行预编译,生成目标 Shader 代码。 + +## 创建着色器片段 + +在 **资源管理器** 面板中点击右键,选择 **创建 -> 着色器片段(Chunk)**: + +![创建着色器片段](img/create-chunk.png) + +便可创建一个默认名为 **chunk** 的着色器片段,如下图: + +![片段模板](img/new-chunk.png) + +## Include 机制 + +在标准 GLSL 语法基础上,Cocos Shader 引入了 C 语言风格的语法扩展 — Include 机制。 + +通过 include 机制,可以在任意 Shader 代码(CCProgram 块或独立的头文件)中引入其他代码片段,如下所示: + +```c +// 引入引擎内置着色器片段 +#include + +// 引入自定义着色器片段 +#include "../headers/my-shading-algorithm.chunk" +``` + +相关规则和注意事项: + +- 着色器片段的扩展名默认为 `.chunk`,在 include 时可忽略。引入其他代码片段时可使用双引号或者尖括号,二者无区别,例如: + + ```c + #include "filename.chunk" + #include "filename" // 可忽略扩展名 + #include + #include // 可忽略扩展名 + ``` + +- 在 Cocos Shader 编译时着色器片段会被展开,且只会展开一次。因此书写时不用担心,每个模块都可以包含自己依赖的着色器片段,即使这中间有重复; +- 所有不参与运行时实际计算流程的函数声明会在编译时被剔除,因此可以放心包含各类工具函数,不用担心生成的目标代码会有冗余; +- 着色器片段引用可以指定基于当前文件目录的相对路径(以下统称“相对路径”),也可以指定基于编辑器 **资源管理器** 面板中 **internal -> chunks** 目录的相对路径(以下统称“项目绝对路径”)。两个目录下如果有同名文件,则后者(项目绝对路径)优先; +- Cocos Creator 提供了一些内置着色器片段资源,主要包括一些常用的工具函数和标准光照模型等,位于 [资源管理器](../editor/assets/index.md) 面板中的内置资源数据库(简称 DB)`internal` 的 `chunks` 目录下,因此可以不加目录直接引用。 +- 引用了编辑器其他 DB(Database)的着色器片段只能指定项目绝对路径。当多个 DB 在此路径下有相同文件时,DB 优先级为:用户项目 DB > 插件 DB > Internal DB; +- 所有在同一个 Cocos Shader 文件中声明的 CCProgram 代码块都可以相互引用。 diff --git a/versions/4.0/zh/shader/effect-inspector.md b/versions/4.0/zh/shader/effect-inspector.md new file mode 100644 index 0000000000..fd0ae8d336 --- /dev/null +++ b/versions/4.0/zh/shader/effect-inspector.md @@ -0,0 +1,78 @@ +# 着色器创建与使用 + +## 创建着色器 + +在 **资源管理器** 面板中点击左上角的 **+** 号按钮(或者在 Assets 目录下点击右键),在弹出菜单中选择 **着色器(Effect)** 或者 **表面着色器(Surface Shader)**, 便可创建新的着色器资源。 + +![1](img/create-effect.png) + +两种类型的着色器区别: +- **着色器(Effect)**:简单的无光照着色器,可参考 internal/effects/builtin-unlit.effect +- **表面着色器(Surface Shader)**:基于 PBR 的着色器,可参考 internal/effects/builtin-standard.effect + +我们以 **表面着色器(Surface Shader)** 为例,引擎会在 **资源管理器** 中创建一个默认名为 **surface-effect** 的着色器资源: + +![image](img/new-effect.png) + +在 **属性检查器** 中可以看到着色器主要由以下几部分组成: + +|属性|说明| +| :-- | :-- | +|Shaders | 当前着色器以及其渲染过程的名称 +| Precompile Combinations | 是否开启预处理宏定义组合,详情请参考下文说明 +| GLSL 300 ES/100 Output | 着色器输出,详情请参考下文说明 + +## Shaders + +如果当前的着色器有多个渲染过程,则可以通过 Shaders 右边的下拉框来选择不同的渲染过程。选择渲染过程后,可以通过 GLSL Output 窗口查看当前编译后的着色器代码。 + +![渲染过程](img/effect-pass.png) + +## Precompile Combinations + +一般情况下材质会在使用到相应宏定义的时候进行编译,当使用到较多宏定义时可能会出现卡顿的情况。因此便可以在该项配置预编译宏定义组合,用于提前编译所需的宏定义组合。例如下图中的配置: + +![image](./img/precompile.png) + +## GLSL Output + +目前引擎提供 GLSL 300 ES 和 GLSL 100 的输出。 + +通过选择不同的标签页可切换显示编译后的顶点着色器和片元着色器: + +![vs-fs-switch](img/change-vs-fs.png) + +## 代码访问内置着色器 + +在 internal/effects/ 目录下包含了引擎提供的内置着色器,这些着色器在程序启动后,会自动加载。 + +以 builtin-standard 为例,可以参考下面的代码访问并使用: + +```ts +// 获取内置 Standard 着色器 ‘builtin-standard.effect’ +const effect = EffectAsset.get('builtin-standard'); + +const mat = new Material(); + +// 使用内置基于物理的光照着色器(PBR)‘builtin-standard.effect’ 初始化材质 +mat.initialize({ effectName: "builtin-standard" }); +``` + +## 动态加载着色器 + +位于 **resources** 目录下的 着色器文件,可以使用 **resources.load** 进行加载并使用。 + +代码示例如下: + +```ts +resources.load("custom-effect", EffectAsset, (err:Error, data:EffectAsset)=>{ + //获取 effect + const effectAsset = EffectAsset.get("../resources/custom-effect"); + + //使用加载好的 effect 初始化材质 + const material = new Material(); + material.initialize({ effectName: "../resources/custom-effect" }); +}) +``` + +> **注意:** 动态加载的自定义着色器加载成功后,effectName 为 "../" + 文件路径。 diff --git a/versions/4.0/zh/shader/effect-syntax.md b/versions/4.0/zh/shader/effect-syntax.md new file mode 100644 index 0000000000..977b906225 --- /dev/null +++ b/versions/4.0/zh/shader/effect-syntax.md @@ -0,0 +1,186 @@ +# 着色器语法 + +Cocos Creator 中的着色器(Cocos Shader ,文件扩展名为 *.effect),是一种基于 [YAML](yaml-101.md) 和 [GLSL](glsl.md) 的单源码嵌入式领域特定语言(single-source embedded domain-specific language),YAML 部分声明流程控制清单,GLSL 部分声明实际的 Shader 片段,这两部分内容相互补充,共同构成了一个完整的渲染流程描述。 + +> **注意**:推荐使用 Visual Studio Code 编写 Cocos Shader,并在应用商店中安装 Cocos Effect 扩展,提供编写时的语法高亮提示。 +> +> ![Cocos Effect](img/vs-ext.png) + +## 语法框架概览 + +**Cocos Shader** 通常由两个部分组成: + +- `CCEffect`:用于声明渲染技术(Technique)、渲染过程(Pass)、渲染状态、材质参数等属性。 +- `CCProgram`:用于声明顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)代码片段。 + +此处以内置着色器 `builtin-unlit.effect` 为例,说明 Cocos Shader 的语法框架。 + +在 VS Code 中打开 Cocos Creator **资源管理器** 面板中 `internal/effects/` 目录下的 `builtin-unlit.effect` 文件,可以看到主要内容如下: + +```glsl +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: unlit-vs:vert + frag: unlit-fs:frag + properties: &props + mainTexture: { value: grey } + tilingOffset: { value: [1, 1, 0, 0] } + mainColor: { value: [1, 1, 1, 1], linear: true, editor: { type: color } } + colorScale: { value: [1, 1, 1], target: colorScaleAndCutoff.xyz } + alphaThreshold: { value: 0.5, target: colorScaleAndCutoff.w, editor: { parent: USE_ALPHA_TEST } } + ... + - name: transparent + passes: + - vert: unlit-vs:vert + frag: unlit-fs:frag + depthStencilState: &d1 + ... +}% + +CCProgram unlit-vs %{ + precision highp float; + #include <...> + #include <....> + + vec4 vert(){ + vec4 position; + CCVertInput(position); + .... + return cc_matProj * (cc_matView * matWorld) * position; + } +}% + +CCProgram unlit-fs %{ + precision highp float; + #include <...> + #include <....> + + vec4 frag(){ + vec4 o = mainColor; + .... + return CCFragOutput(o); + } +}% + +``` + +## CCEffect + +在 Cocos Shader 中由 `CCEffect` 包裹的部分是由 **YAML 语法** 声明的渲染流程相关的描述信息。对 YAML 不熟悉的开发者可以前住 [YAML 101](yaml-101.md) 了解详情。 + +`CCEffect` 的整体结构如下: + +```glsl +CCEffect %{ + techniques: + - name: tag + passes: + - vert: vs:entry + frag: fs:entry + + + + ... + ... +}% +``` + +一个 `CCEffect` 中支持定义多个渲染技术,但在实际渲染时,同一个材质实例只能应用其中一个技术。 + +以 `builtin-unlit.effect` 为例,其中包含了 4 个技术: +- opaque +- transparent +- add +- alpha-blend + +`opaque` 专门用于渲染不透明物体的渲染技术,`transparent`,`add`,`alpha-blend` 则用来渲染半透明物体。 + +每个渲染技术(`technique`)都包含了名称(`name`)和渲染过程(`pass`)。 + +名称用于标记渲染技术的用途,渲染过程则定义了一个完整的渲染流程所需要的全部信息。 + +## 渲染过程(Pass) + +渲染过程的特点如下: +1. 一个渲染技术可以包含多个渲染过程,渲染过程按定义的先后顺序逐一执行。 +2. 一个渲染过程必须包含一个顶点着色器(Vertex Shader,VS)和一个片元着色器(Fragment Shader,FS),其余都是可选配置项。详情请参考 [Pass 可选配置参数](pass-parameter-list.md)。 + +3. VS/FS 着色器需要指定使用的 **CCProgram** ,以及指定着色器的入口函数。 如果不指定入口函数,会默认使用 main。 + +**格式如下**: + +```glsl +CCEffect %{ + techniques: + - name: opaque # 定义一个不透明的渲染技术 + passes: + - vert: vs: entry # 选择一个 CCProgram 声明的顶点着色器 ‘vs’,入口函数是 ‘entry’ + frag: fs: entry # 选择一个 CCProgram 声明的片元着色器 ‘fs’,入口函数是 ‘entry’ + ... + ... +}% +``` + +每个渲染过程都只有 `vert` 和 `frag` 两个必填参数,分别用于声明当前渲染过程使用的顶点着色器和片元着色器,格式为 `片段名: 入口函数名`。 + +片段名可以是本文件中声明的 `CCProgram` 片段名,也可以是引擎提供的标准头文件。 + +> **注意**:自定义着色器的代码中不应该使用 `main` 函数,Cocos Shader 在编译时会自动添加一个 `main` 函数并调用渲染过程的入口函数(例如 `vert` 或 `frag`),`main` 函数会将入口函数的返回值作为当前 Shader 的输出(例如 `gl_Position` 或 `gl_FragColor`)。 + +## 渲染过程属性 + +渲染过程中的 properties 用于配置相关属性描述。通过它,可以定义了一个 uniform 在面板上的显示方式,如下所示: + +```glsl +CCEffect %{ + techniques: + - name: opaque # 定义一个不透明的渲染技术 + passes: + - vert: vs: entry # 选择一个 CCProgram 声明的顶点着色器 ‘vs’,入口函数是 ‘entry’ + frag: fs: entry # 选择一个 CCProgram 声明的片元着色器 ‘fs’,入口函数是 ‘entry’ + properties: + mainTexture: { value: grey } # 着色器中需要同步定义一个 ‘uniform mainTexture’,该属性可在编辑器的属性检查器中进行配置 + colorScale: { value: [1, 1, 1], target: colorScaleAndCutoff.xyz } # 基于 ‘target’ 属性配置机制,着色器中需要同步定义一个 ‘uniform colorScaleAndCutoff’,并选取它的 x、y、z 分量填充 ‘colorScale’ 设置的数据 + depthStencilState: # 配置深度测试、模板测试和写入状态 + depthTest: true + depthWrite: true + ... + ... +}% +``` + +详情请参考 [Pass 可选配置参数](pass-parameter-list.md)。 + +## CCProgram + +在 Cocos Shader 中由 `CCProgram` 包裹的部分是由 **GLSL 语法** 声明的 Shader 片段。建议在编写 CCProgram 之前,先了解 [GLSL 基础语法](./glsl.md) 。 + +它的结构如下: + +```glsl +CCProgram shader-name %{ + + + + + + vec4 entry(){ + // 需要返回一个 vec4 类型数据 + } +}% +``` + +## 预处理宏定义 + +通过预处理宏,可在 Cocos Shader 编译时控制代码分支和组合,以实现高效便捷的 Shader 代码管理。 + +更多详细内容请参考: + +- [预处理宏定义](macros.md) +- [GLSL 语法](glsl.md) + +## 更多 + +如果想要查看更为复杂的着色器示例,可以参考 [Surface Shader 结构](./surface-shader/surface-shader-structure.md) 以及 [内置 Surface Shader](./surface-shader/builtin-surface-shader.md)。 diff --git a/versions/4.0/zh/shader/forward-and-deferred.md b/versions/4.0/zh/shader/forward-and-deferred.md new file mode 100644 index 0000000000..29f36b4c1a --- /dev/null +++ b/versions/4.0/zh/shader/forward-and-deferred.md @@ -0,0 +1,26 @@ +# 前向渲染与延迟渲染 Shader 执行流程 + +Cocos Creator 引擎支持 前向渲染和延迟渲染。因此,在 Shader 架构上,也要为这两种渲染流程做兼容,并且让用户感知不到。 + +内置的 Legacy Shader 都是 PBR 材质,它们在渲染时都遵守以下流程: + +## 前向渲染 + +1. 调用 vs +2. 调用 fs -> surf -> 光照计算 + +## 延迟渲染 + +### Buffer 阶段 + +1. 调用 vs +2. 调用 fs -> surf -> GBuffer + +### Lighting 阶段 + +1. 从 GBuffer 还原 StandardSurface 信息 +2. 光照计算 + +可以看出,对于 PBR 材质来说,不管是前向渲染还是延迟渲染,用户能够控制的只有 vs 和 surf 函数。 + +这就统一了架构,使用户写的 Shader 可以不用修改就运行在 前向渲染管线 和 延迟渲染管线中。 diff --git a/versions/4.0/zh/shader/glsl.md b/versions/4.0/zh/shader/glsl.md new file mode 100644 index 0000000000..68ce96f4f4 --- /dev/null +++ b/versions/4.0/zh/shader/glsl.md @@ -0,0 +1,292 @@ +# GLSL 语法简介 + +GLSL 是为图形计算量身定制的用于编写着色器的语言,它包含一些针对向量和矩阵操作的特性,使渲染管线具有可编程性。本章主要介绍在编写 Shader 时常用的一些语法,包括以下几个方面: + +- 变量 +- 语句 +- 限定符 +- 预处理宏定义 + +## 变量 + +### 变量及变量类型 + +| 变量类型 | 说明 | Cocos Shader 中的默认值 | Cocos Shader 中的可选项 | +| :-- | :-- | :-- | :-- | +| bool | 布尔型标量数据类型 | false | 无 | +| int/ivec2/ivec3/ivec4 | 包含 1/2/3/4 个整型向量 | 0/[0, 0]/[0, 0, 0]/[0, 0, 0, 0] | 无 | +| float/vec2/vec3/vec4 | 包含 1,2,3,4 个浮点型向量 | 0/[0, 0]/[0, 0, 0]/[0, 0, 0, 0] | 无 | +| sampler2D | 表示 2D 纹理 | **default** | black、grey、white、normal、default | +| samplerCube | 表示立方体纹理 | **default-cube** | black-cube、white-cube、default-cube | +| mat[2..3] | 表示 2x2 和 3x3 的矩阵 | 不可用 | +| mat4 | 表示 4x4 的矩阵 | [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] | + +### 标量 + +构造标量的方式和 C 语言一致: + +```glsl +float floatValue = 1.0; +bool booleanValue = false; +``` + +### 向量 + +构造向量时的规则如下: + +- 若向向量构造器提供了一个标量,则向量的所有值都会设定为该标量值 +- 若提供多个标量值或向量,则从左到右使用提供的值赋值。前提是标量或向量的数量之和要等于向量构造器的数量 + +```glsl +vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0} +vec2 myVec2 = vec2(0.5, 0.5); // myVec2 = {0.5, 0.5} +vec4 newVec4 = vec4(1.0, 1.0, myVec2);// newVec4 = {1.0, 1.0, 0.5, 0.5} +``` + +向量可以通过 `r, g, b, a` 或 `x, y, z, w` 进行访问,也可以同时访问多个角标: + +```glsl +vec4 myVec4_0 = vec4(1.0); // myVec4_0 = { 1.0, 1.0, 1.0, 1.0 } +vec4 myVec4 = vec4(1.0, 2.0, 3.0, 4.0); // myVec4 = { 1.0, 2.0, 3.0, 4.0 } +float x = myVec4.x; // x = 1.0; +vec3 myVec3_0 = myVec4.xyz; // myVec3_0 = { 1.0, 2.0, 3.0 } +vec3 myVec3_1 = myVec4.rgb; // myVec3_1 = { 1.0, 2.0, 3.0 } +vec3 myVec3_2 = myVec4.zyx; // myVec3_2 = { 3.0, 2.0, 1.0 } +vec3 myVec3_3 = myVec4.xxx; // myVec3_3 = { 1.0, 1.0, 1.0 } +``` + +### 矩阵 + +在 GLSL 内可构造 mat[2..4] 来表示 2 阶到 4 阶的矩阵。 + +矩阵构造有如下的规则: + +- 若只为矩阵构造器提供了一个标量,则该值会构造矩阵对角线上的值 +- 矩阵可以由多个向量构造 +- 矩阵可以由单个标量从左到右进行构造 + +```glsl + +mat4 marixt4x4 = mat4(1.0); // marixt4x4 = { 1.0, 0.0, 0.0, 0.0, + // 0.0, 1.0, 0.0, 0.0 + // 0.0, 0.0, 1.0, 0.0 + // 0.0, 0.0, 0.0, 1.0 } + +vec2 col1 = vec2(1.0, 0.0); +vec2 col2 = vec2(1.0, 0.0); + +mat2 matrix2x2 = mat2(coll1, col2); + +// GLSL 是列矩阵存储,因此构造时,构造器会按照列顺序进行填充 +mat3 matrix3x3 = mat3(0.0, 0.0, 0.0, // 第一列 + 0.0, 0.0, 0.0, // 第二列 + 0.0, 0.0, 0.0); // 第三列 +``` + +> **注意**:为避免 implicit padding,引擎规定若要使用 Uniform 限定符的矩阵,必须是 4 阶矩阵,2 阶和 3 阶的矩阵不可作为 Uniform 变量。 + +矩阵的访问: + +矩阵可以通过索引访问不同的列: + +```glsl +mat2 matrix2x2 = mat2(0.0, 0.0, 0.0, 0.0); +vec4 myVec4 = vec4(matrix2x2[0], matrix2x2[1]); +vec2 myVec2 = matrix2x2[0]; + +// 访问第一列的第一个元素 +float value = matrix2x2[0][0]; +matrix2x2[1][1] = 2.0; +``` + +### 结构体 + +结构体的形成和 C 语言类似,可由不同数据类型聚合而成: + +```c +struct myStruct +{ + vec4 position; + vec4 color; + vec2 uv; +}; +``` + +构造结构体的代码示例如下: + +```glsl +myStruct structVar = myStruct(vec4(0.0, 0.0,0.0,0.0), vec4(1.0, 1.0, 1.0, 1.0), vec2(0.5, 0.5)); +``` + +结构体支持赋值(=)和比较(==,!=)运算符,但要求两个结构体拥有相同的类型且组件分量(component-wise)都必须相同。 + +### 数组 + +数组的用法和 C 语言类似,规则如下: + +- 数组必须声明长度 +- 数组不能在声明的同时初始化 +- 数组必须由常量表达式初始化 +- 数组不能用 `const` 修饰 +- 不支持多维数组 + +数组声明和初始化的代码示例如下: + +```glsl +float array[4]; +for(int i =0; i < 4; i ++) +{ + array[i] = 0.0; +} +``` + +## 语句 + +### 控制流程 + +GLSL 支持标准的 C/C++ 控制流程,包括: +- `if-else`/`switch-case` +- `for`/`while`/`do-while` +- `break`/`continue`/`return` +- 没有 `goto`,若要跳出可使用 `discard`。该语句仅在片元着色器下有效,需要注意的是使用该语句会导致管线放弃当前片元,不会写入帧缓存 + +`if-else` 的用法和 C 语言一致,代码示例如下: + +```glsl +if(v_uvMode >= 3.0) { + i.uv = v_uv0 * v_uvSizeOffset.xy + v_uvSizeOffset.zw; +} else if (v_uvMode >= 2.0) { + i.uv = fract(v_uv0) * v_uvSizeOffset.xy + v_uvSizeOffset.zw; +} else if (v_uvMode >= 1.0) { + i.uv = evalSlicedUV(v_uv0) * v_uvSizeOffset.xy + v_uvSizeOffset.zw; +} else { + i.uv = v_uv0; +} +``` + +在 GLSL 中,循环变量必须是常量或者编译时已知,代码示例如下: + + ```glsl +const float value = 10.; +for(float i = 0.0; i < value; i ++){ + ... +} + ``` + +错误示例: + + ```glsl +float value = 10.; +// 错误,value 不为常量 +for(float i =0.0; i < value; i ++){ + ... +} + ``` + +### 函数 + +GLSL 的函数由 **返回值**、**函数名** 和 **参数** 构成,其中返回值和函数名是必须的。若无返回值,需要使用 `void` 代替。 + +> **注意**:GLSL 的函数不能递归。 + +代码示例如下: + +```glsl +void scaleMatrix (inout mat4 m, float s){ + m[0].xyz *= s; + m[1].xyz *= s; + m[2].xyz *= s; +} +``` + +## 限定符 + +### 存储限定符 + +存储限定符用于描述变量在管线中的作用。 + +| 限定符 | 说明 | +|:--|:--| +|< none:default > | 无限定符或者使用 `default`,常用语局部变量,函数参数 +|const | 编译时为常量或作为参数时只读 +|attribute| 应用程序和顶点着色器间通信,用于确定顶点格式 +|uniform | 应用程序和着色器之间交互数据。在顶点着色器和片元着色器中保持一致 +|varying | 顶点着色器传输给片元着色器的插值 + +#### uniform + +在一个渲染过程内声明的 `uniform` 不能重复。例如在顶点着色器中定义了变量 `variableA`,`variableA` 也会存在于片元着色器且值相同,那么也就是 `variableA` 不能在片元着色器中再次定义。 + +引擎不支持离散声明的 `uniform` 变量,必须使用 UBO 并保持内存对齐,以避免 implicit padding。 + +#### varying + +`varying` 是由顶点着色器输出并传输给片元着色器的变量。在管线的作用下,变量值并不会和顶点着色器输出的保持一致,而是由管线进行插值,这就可能会出现顶点输出的法线没有归一化的情况。此时需要手动归一化,代码示例如下: + +```glsl +// 归一化法线 +vec3 normal = normalize(v_normal); +``` + +### 参数限定符 + +GLSL 中函数的参数限定符包括以下几种: + +| 限定符 | 说明 | +|:---|:---| +| < none:in > | 缺省限定符,和 C 语言的值传递类似,指明传入的参数传递的是值,函数内不会修改传入的值| +| inout| 类似于 C 语言的引用,参数的值会传入函数并返回函数内修改的值 | +| out| 参数的值不会传入函数,由函数内部修改并返回修改后的值| + +### 精度限定符 + +GLSL 引入了精度限定符,用于指定整型或浮点型变量的精度。精度限定符可使着色器的编写者明确定义着色器变量计算时使用的精度。在 +在 Shader 头部声明的精度应用于整个 Shader,是所有基于浮点型的变量的默认精度,同时也可以定义单个变量的精度。在 Shader 中如果没有指定默认精度,则所有的整型和浮点型变量都采用高精度计算。 + +GLSL 支持的精度限定符包括以下几种: + +|限定符|说明| +|:--|:--| +|highp | 高精度。
浮点型精度范围为 [-262, 262]
整型精度范围为 [-216, 216]。 +|mediump | 中精度。
浮点型精度范围为 [-214, 214]
整型精度范围为 [-210, 210]。 +|lowp | 低精度。
浮点型精度范围为 [-28, 28]
整型精度范围为 [-28, 28]。 + +代码示例如下: + +```glsl +highp mat4 cc_matWorld; +mediump vec2 dir; +lowp vec4 cc_shadowColor; +``` + +## 预处理宏定义 + +GLSL 允许定义和 C 语言类似的宏定义。 + +预处理宏定义允许着色器定义多样化的动态分支,确定最终的渲染效果。 + +在 GLSL 中使用预处理宏定义的代码示例如下: + +```glsl +#define +#undef +#if +#ifdef + +#ifndef +#else +#elif +#endif +``` + +下方代码示例若 `USE_VERTEX_COLOR` 条件为真,则声明一个名为 `v_color` 的四维向量: + +```glsl +#if USE_VERTEX_COLOR + in vec4 v_color; +#endif +``` + +预处理宏定义和引擎的交互部分可参考: [预处理宏定义](macros.md) + +> **注意**:在引擎中,材质的预处理宏定义在材质初始化完成后便不能修改。如果需要修改,请使用 `Material.initialize` 或 `Material.reset` 方法。代码示例可参考:[程序化使用材质](../material-system/material-script.md) diff --git a/versions/4.0/zh/shader/img/1-dot.jpg b/versions/4.0/zh/shader/img/1-dot.jpg new file mode 100644 index 0000000000..24b3e80eae Binary files /dev/null and b/versions/4.0/zh/shader/img/1-dot.jpg differ diff --git a/versions/4.0/zh/shader/img/2d-create-guild.png b/versions/4.0/zh/shader/img/2d-create-guild.png new file mode 100644 index 0000000000..4be4ff5213 Binary files /dev/null and b/versions/4.0/zh/shader/img/2d-create-guild.png differ diff --git a/versions/4.0/zh/shader/img/2d-gradient-vs.png b/versions/4.0/zh/shader/img/2d-gradient-vs.png new file mode 100644 index 0000000000..1a4e07caa6 Binary files /dev/null and b/versions/4.0/zh/shader/img/2d-gradient-vs.png differ diff --git a/versions/4.0/zh/shader/img/add-intensity.png b/versions/4.0/zh/shader/img/add-intensity.png new file mode 100644 index 0000000000..2d7daed5b6 Binary files /dev/null and b/versions/4.0/zh/shader/img/add-intensity.png differ diff --git a/versions/4.0/zh/shader/img/adjust-gradient-color.png b/versions/4.0/zh/shader/img/adjust-gradient-color.png new file mode 100644 index 0000000000..a04c22e87d Binary files /dev/null and b/versions/4.0/zh/shader/img/adjust-gradient-color.png differ diff --git a/versions/4.0/zh/shader/img/adjust-option.png b/versions/4.0/zh/shader/img/adjust-option.png new file mode 100644 index 0000000000..b75c8bc8db Binary files /dev/null and b/versions/4.0/zh/shader/img/adjust-option.png differ diff --git a/versions/4.0/zh/shader/img/albedo-map.png b/versions/4.0/zh/shader/img/albedo-map.png new file mode 100644 index 0000000000..620869ac7d Binary files /dev/null and b/versions/4.0/zh/shader/img/albedo-map.png differ diff --git a/versions/4.0/zh/shader/img/albedo.jpg b/versions/4.0/zh/shader/img/albedo.jpg new file mode 100644 index 0000000000..baf5db25fd Binary files /dev/null and b/versions/4.0/zh/shader/img/albedo.jpg differ diff --git a/versions/4.0/zh/shader/img/alpha-cutoff.png b/versions/4.0/zh/shader/img/alpha-cutoff.png new file mode 100644 index 0000000000..ab6c4de996 Binary files /dev/null and b/versions/4.0/zh/shader/img/alpha-cutoff.png differ diff --git a/versions/4.0/zh/shader/img/ao.jpg b/versions/4.0/zh/shader/img/ao.jpg new file mode 100644 index 0000000000..eed152f554 Binary files /dev/null and b/versions/4.0/zh/shader/img/ao.jpg differ diff --git a/versions/4.0/zh/shader/img/builtin-effect.png b/versions/4.0/zh/shader/img/builtin-effect.png new file mode 100644 index 0000000000..a8fc558e7b Binary files /dev/null and b/versions/4.0/zh/shader/img/builtin-effect.png differ diff --git a/versions/4.0/zh/shader/img/change-vs-fs.png b/versions/4.0/zh/shader/img/change-vs-fs.png new file mode 100644 index 0000000000..3d3d45188f Binary files /dev/null and b/versions/4.0/zh/shader/img/change-vs-fs.png differ diff --git a/versions/4.0/zh/shader/img/check-use-texture.png b/versions/4.0/zh/shader/img/check-use-texture.png new file mode 100644 index 0000000000..923b0360ef Binary files /dev/null and b/versions/4.0/zh/shader/img/check-use-texture.png differ diff --git a/versions/4.0/zh/shader/img/compile-binding.gif b/versions/4.0/zh/shader/img/compile-binding.gif new file mode 100644 index 0000000000..43a85238fe Binary files /dev/null and b/versions/4.0/zh/shader/img/compile-binding.gif differ diff --git a/versions/4.0/zh/shader/img/compute-shader-rt.png b/versions/4.0/zh/shader/img/compute-shader-rt.png new file mode 100644 index 0000000000..7b28251338 Binary files /dev/null and b/versions/4.0/zh/shader/img/compute-shader-rt.png differ diff --git a/versions/4.0/zh/shader/img/create-asset.png b/versions/4.0/zh/shader/img/create-asset.png new file mode 100644 index 0000000000..007c8a8649 Binary files /dev/null and b/versions/4.0/zh/shader/img/create-asset.png differ diff --git a/versions/4.0/zh/shader/img/create-chunk.png b/versions/4.0/zh/shader/img/create-chunk.png new file mode 100644 index 0000000000..12969f4fd1 Binary files /dev/null and b/versions/4.0/zh/shader/img/create-chunk.png differ diff --git a/versions/4.0/zh/shader/img/create-effect.png b/versions/4.0/zh/shader/img/create-effect.png new file mode 100644 index 0000000000..767e4ae0e7 Binary files /dev/null and b/versions/4.0/zh/shader/img/create-effect.png differ diff --git a/versions/4.0/zh/shader/img/create-sprite.png b/versions/4.0/zh/shader/img/create-sprite.png new file mode 100644 index 0000000000..b49f7a8131 Binary files /dev/null and b/versions/4.0/zh/shader/img/create-sprite.png differ diff --git a/versions/4.0/zh/shader/img/cull-front.png b/versions/4.0/zh/shader/img/cull-front.png new file mode 100644 index 0000000000..956303fe1e Binary files /dev/null and b/versions/4.0/zh/shader/img/cull-front.png differ diff --git a/versions/4.0/zh/shader/img/custom-material.png b/versions/4.0/zh/shader/img/custom-material.png new file mode 100644 index 0000000000..123b78b082 Binary files /dev/null and b/versions/4.0/zh/shader/img/custom-material.png differ diff --git a/versions/4.0/zh/shader/img/debug-view.jpg b/versions/4.0/zh/shader/img/debug-view.jpg new file mode 100644 index 0000000000..a410136d47 Binary files /dev/null and b/versions/4.0/zh/shader/img/debug-view.jpg differ diff --git a/versions/4.0/zh/shader/img/def-chunk.png b/versions/4.0/zh/shader/img/def-chunk.png new file mode 100644 index 0000000000..78d7a8bf3d Binary files /dev/null and b/versions/4.0/zh/shader/img/def-chunk.png differ diff --git a/versions/4.0/zh/shader/img/default-effect.png b/versions/4.0/zh/shader/img/default-effect.png new file mode 100644 index 0000000000..ff289bdd1f Binary files /dev/null and b/versions/4.0/zh/shader/img/default-effect.png differ diff --git a/versions/4.0/zh/shader/img/depth-write.png b/versions/4.0/zh/shader/img/depth-write.png new file mode 100644 index 0000000000..7f38f8363a Binary files /dev/null and b/versions/4.0/zh/shader/img/depth-write.png differ diff --git a/versions/4.0/zh/shader/img/dot.jpg b/versions/4.0/zh/shader/img/dot.jpg new file mode 100644 index 0000000000..2307d500ac Binary files /dev/null and b/versions/4.0/zh/shader/img/dot.jpg differ diff --git a/versions/4.0/zh/shader/img/effect-by-color.jpg b/versions/4.0/zh/shader/img/effect-by-color.jpg new file mode 100644 index 0000000000..2c4a60fceb Binary files /dev/null and b/versions/4.0/zh/shader/img/effect-by-color.jpg differ diff --git a/versions/4.0/zh/shader/img/effect-inspector.png b/versions/4.0/zh/shader/img/effect-inspector.png new file mode 100644 index 0000000000..328801a3bd Binary files /dev/null and b/versions/4.0/zh/shader/img/effect-inspector.png differ diff --git a/versions/4.0/zh/shader/img/effect-pass.png b/versions/4.0/zh/shader/img/effect-pass.png new file mode 100644 index 0000000000..b26034c32a Binary files /dev/null and b/versions/4.0/zh/shader/img/effect-pass.png differ diff --git a/versions/4.0/zh/shader/img/effect-show.jpg b/versions/4.0/zh/shader/img/effect-show.jpg new file mode 100644 index 0000000000..e42bfbdd86 Binary files /dev/null and b/versions/4.0/zh/shader/img/effect-show.jpg differ diff --git a/versions/4.0/zh/shader/img/effect.png b/versions/4.0/zh/shader/img/effect.png new file mode 100644 index 0000000000..9a9e72f402 Binary files /dev/null and b/versions/4.0/zh/shader/img/effect.png differ diff --git a/versions/4.0/zh/shader/img/emission-map.png b/versions/4.0/zh/shader/img/emission-map.png new file mode 100644 index 0000000000..04e1ae60c6 Binary files /dev/null and b/versions/4.0/zh/shader/img/emission-map.png differ diff --git a/versions/4.0/zh/shader/img/final_alarmclock.jpg b/versions/4.0/zh/shader/img/final_alarmclock.jpg new file mode 100644 index 0000000000..c717e4cac6 Binary files /dev/null and b/versions/4.0/zh/shader/img/final_alarmclock.jpg differ diff --git a/versions/4.0/zh/shader/img/fresnel.jpg b/versions/4.0/zh/shader/img/fresnel.jpg new file mode 100644 index 0000000000..58d814dc05 Binary files /dev/null and b/versions/4.0/zh/shader/img/fresnel.jpg differ diff --git a/versions/4.0/zh/shader/img/intensity.png b/versions/4.0/zh/shader/img/intensity.png new file mode 100644 index 0000000000..ee7fc366d9 Binary files /dev/null and b/versions/4.0/zh/shader/img/intensity.png differ diff --git a/versions/4.0/zh/shader/img/leaves.jpg b/versions/4.0/zh/shader/img/leaves.jpg new file mode 100644 index 0000000000..108e71b3ca Binary files /dev/null and b/versions/4.0/zh/shader/img/leaves.jpg differ diff --git a/versions/4.0/zh/shader/img/load-custom-effect.png b/versions/4.0/zh/shader/img/load-custom-effect.png new file mode 100644 index 0000000000..23e89898fc Binary files /dev/null and b/versions/4.0/zh/shader/img/load-custom-effect.png differ diff --git a/versions/4.0/zh/shader/img/macro-defined.png b/versions/4.0/zh/shader/img/macro-defined.png new file mode 100644 index 0000000000..efa5e1f23c Binary files /dev/null and b/versions/4.0/zh/shader/img/macro-defined.png differ diff --git a/versions/4.0/zh/shader/img/macro-options-example.png b/versions/4.0/zh/shader/img/macro-options-example.png new file mode 100644 index 0000000000..7d01f4c52e Binary files /dev/null and b/versions/4.0/zh/shader/img/macro-options-example.png differ diff --git a/versions/4.0/zh/shader/img/macro-pre-define.png b/versions/4.0/zh/shader/img/macro-pre-define.png new file mode 100644 index 0000000000..18291cef0a Binary files /dev/null and b/versions/4.0/zh/shader/img/macro-pre-define.png differ diff --git a/versions/4.0/zh/shader/img/macro-preview.png b/versions/4.0/zh/shader/img/macro-preview.png new file mode 100644 index 0000000000..d46e1d468e Binary files /dev/null and b/versions/4.0/zh/shader/img/macro-preview.png differ diff --git a/versions/4.0/zh/shader/img/macro-property.png b/versions/4.0/zh/shader/img/macro-property.png new file mode 100644 index 0000000000..620869ac7d Binary files /dev/null and b/versions/4.0/zh/shader/img/macro-property.png differ diff --git a/versions/4.0/zh/shader/img/macro-range-example.png b/versions/4.0/zh/shader/img/macro-range-example.png new file mode 100644 index 0000000000..ef0a5672bc Binary files /dev/null and b/versions/4.0/zh/shader/img/macro-range-example.png differ diff --git a/versions/4.0/zh/shader/img/macro-remapping.png b/versions/4.0/zh/shader/img/macro-remapping.png new file mode 100644 index 0000000000..6bac16ae5e Binary files /dev/null and b/versions/4.0/zh/shader/img/macro-remapping.png differ diff --git a/versions/4.0/zh/shader/img/macro-simple.png b/versions/4.0/zh/shader/img/macro-simple.png new file mode 100644 index 0000000000..65c8f24591 Binary files /dev/null and b/versions/4.0/zh/shader/img/macro-simple.png differ diff --git a/versions/4.0/zh/shader/img/metallic.jpg b/versions/4.0/zh/shader/img/metallic.jpg new file mode 100644 index 0000000000..b62c8e1c2a Binary files /dev/null and b/versions/4.0/zh/shader/img/metallic.jpg differ diff --git a/versions/4.0/zh/shader/img/metallicRoughnessMap.png b/versions/4.0/zh/shader/img/metallicRoughnessMap.png new file mode 100644 index 0000000000..2b3c60b222 Binary files /dev/null and b/versions/4.0/zh/shader/img/metallicRoughnessMap.png differ diff --git a/versions/4.0/zh/shader/img/new-chunk.png b/versions/4.0/zh/shader/img/new-chunk.png new file mode 100644 index 0000000000..836b124fc4 Binary files /dev/null and b/versions/4.0/zh/shader/img/new-chunk.png differ diff --git a/versions/4.0/zh/shader/img/new-effect.png b/versions/4.0/zh/shader/img/new-effect.png new file mode 100644 index 0000000000..e1cd07306c Binary files /dev/null and b/versions/4.0/zh/shader/img/new-effect.png differ diff --git a/versions/4.0/zh/shader/img/no-outline.png b/versions/4.0/zh/shader/img/no-outline.png new file mode 100644 index 0000000000..fdad671a9b Binary files /dev/null and b/versions/4.0/zh/shader/img/no-outline.png differ diff --git a/versions/4.0/zh/shader/img/normal-map.png b/versions/4.0/zh/shader/img/normal-map.png new file mode 100644 index 0000000000..f07a6d5a43 Binary files /dev/null and b/versions/4.0/zh/shader/img/normal-map.png differ diff --git a/versions/4.0/zh/shader/img/normal.jpg b/versions/4.0/zh/shader/img/normal.jpg new file mode 100644 index 0000000000..c0685f802b Binary files /dev/null and b/versions/4.0/zh/shader/img/normal.jpg differ diff --git a/versions/4.0/zh/shader/img/occluation.png b/versions/4.0/zh/shader/img/occluation.png new file mode 100644 index 0000000000..8fe893fc42 Binary files /dev/null and b/versions/4.0/zh/shader/img/occluation.png differ diff --git a/versions/4.0/zh/shader/img/occlusion-map.png b/versions/4.0/zh/shader/img/occlusion-map.png new file mode 100644 index 0000000000..d589c09058 Binary files /dev/null and b/versions/4.0/zh/shader/img/occlusion-map.png differ diff --git a/versions/4.0/zh/shader/img/opt-overview.jpg b/versions/4.0/zh/shader/img/opt-overview.jpg new file mode 100644 index 0000000000..47fb62fa9a Binary files /dev/null and b/versions/4.0/zh/shader/img/opt-overview.jpg differ diff --git a/versions/4.0/zh/shader/img/outline-off.png b/versions/4.0/zh/shader/img/outline-off.png new file mode 100644 index 0000000000..468a60727a Binary files /dev/null and b/versions/4.0/zh/shader/img/outline-off.png differ diff --git a/versions/4.0/zh/shader/img/outline-on.png b/versions/4.0/zh/shader/img/outline-on.png new file mode 100644 index 0000000000..56d187f365 Binary files /dev/null and b/versions/4.0/zh/shader/img/outline-on.png differ diff --git a/versions/4.0/zh/shader/img/outline.png b/versions/4.0/zh/shader/img/outline.png new file mode 100644 index 0000000000..52d11dbee9 Binary files /dev/null and b/versions/4.0/zh/shader/img/outline.png differ diff --git a/versions/4.0/zh/shader/img/pbr-maps.png b/versions/4.0/zh/shader/img/pbr-maps.png new file mode 100644 index 0000000000..4c56a2a198 Binary files /dev/null and b/versions/4.0/zh/shader/img/pbr-maps.png differ diff --git a/versions/4.0/zh/shader/img/pbr-param.png b/versions/4.0/zh/shader/img/pbr-param.png new file mode 100644 index 0000000000..fe15dca42c Binary files /dev/null and b/versions/4.0/zh/shader/img/pbr-param.png differ diff --git a/versions/4.0/zh/shader/img/pipeline-effect.png b/versions/4.0/zh/shader/img/pipeline-effect.png new file mode 100644 index 0000000000..a68b5d1959 Binary files /dev/null and b/versions/4.0/zh/shader/img/pipeline-effect.png differ diff --git a/versions/4.0/zh/shader/img/precompile.png b/versions/4.0/zh/shader/img/precompile.png new file mode 100644 index 0000000000..06a0787996 Binary files /dev/null and b/versions/4.0/zh/shader/img/precompile.png differ diff --git a/versions/4.0/zh/shader/img/preview-instensity.jpg b/versions/4.0/zh/shader/img/preview-instensity.jpg new file mode 100644 index 0000000000..50b0ff6ade Binary files /dev/null and b/versions/4.0/zh/shader/img/preview-instensity.jpg differ diff --git a/versions/4.0/zh/shader/img/ramp-texture.png b/versions/4.0/zh/shader/img/ramp-texture.png new file mode 100644 index 0000000000..b3b75d8bf9 Binary files /dev/null and b/versions/4.0/zh/shader/img/ramp-texture.png differ diff --git a/versions/4.0/zh/shader/img/render-to-xxx.png b/versions/4.0/zh/shader/img/render-to-xxx.png new file mode 100644 index 0000000000..0350fc8bb8 Binary files /dev/null and b/versions/4.0/zh/shader/img/render-to-xxx.png differ diff --git a/versions/4.0/zh/shader/img/rim-light-effect.png b/versions/4.0/zh/shader/img/rim-light-effect.png new file mode 100644 index 0000000000..6e0bc2d430 Binary files /dev/null and b/versions/4.0/zh/shader/img/rim-light-effect.png differ diff --git a/versions/4.0/zh/shader/img/rim-preview.jpg b/versions/4.0/zh/shader/img/rim-preview.jpg new file mode 100644 index 0000000000..28ea3bc33d Binary files /dev/null and b/versions/4.0/zh/shader/img/rim-preview.jpg differ diff --git a/versions/4.0/zh/shader/img/rim-slider.png b/versions/4.0/zh/shader/img/rim-slider.png new file mode 100644 index 0000000000..c38ee04a77 Binary files /dev/null and b/versions/4.0/zh/shader/img/rim-slider.png differ diff --git a/versions/4.0/zh/shader/img/roughness.jpg b/versions/4.0/zh/shader/img/roughness.jpg new file mode 100644 index 0000000000..c9e95ce275 Binary files /dev/null and b/versions/4.0/zh/shader/img/roughness.jpg differ diff --git a/versions/4.0/zh/shader/img/select-effect.png b/versions/4.0/zh/shader/img/select-effect.png new file mode 100644 index 0000000000..1893b2de28 Binary files /dev/null and b/versions/4.0/zh/shader/img/select-effect.png differ diff --git a/versions/4.0/zh/shader/img/shade-color.png b/versions/4.0/zh/shader/img/shade-color.png new file mode 100644 index 0000000000..cc3917df01 Binary files /dev/null and b/versions/4.0/zh/shader/img/shade-color.png differ diff --git a/versions/4.0/zh/shader/img/shade-map.png b/versions/4.0/zh/shader/img/shade-map.png new file mode 100644 index 0000000000..6907a46b62 Binary files /dev/null and b/versions/4.0/zh/shader/img/shade-map.png differ diff --git a/versions/4.0/zh/shader/img/surface-effects.png b/versions/4.0/zh/shader/img/surface-effects.png new file mode 100644 index 0000000000..35b70687cd Binary files /dev/null and b/versions/4.0/zh/shader/img/surface-effects.png differ diff --git a/versions/4.0/zh/shader/img/surface-node.png b/versions/4.0/zh/shader/img/surface-node.png new file mode 100644 index 0000000000..9253cf54f5 Binary files /dev/null and b/versions/4.0/zh/shader/img/surface-node.png differ diff --git a/versions/4.0/zh/shader/img/toon-depth-bias.png b/versions/4.0/zh/shader/img/toon-depth-bias.png new file mode 100644 index 0000000000..628388fdcd Binary files /dev/null and b/versions/4.0/zh/shader/img/toon-depth-bias.png differ diff --git a/versions/4.0/zh/shader/img/toon-pass.png b/versions/4.0/zh/shader/img/toon-pass.png new file mode 100644 index 0000000000..50f7743481 Binary files /dev/null and b/versions/4.0/zh/shader/img/toon-pass.png differ diff --git a/versions/4.0/zh/shader/img/toon-surf.png b/versions/4.0/zh/shader/img/toon-surf.png new file mode 100644 index 0000000000..e25a087509 Binary files /dev/null and b/versions/4.0/zh/shader/img/toon-surf.png differ diff --git a/versions/4.0/zh/shader/img/toon.png b/versions/4.0/zh/shader/img/toon.png new file mode 100644 index 0000000000..dfddf117f5 Binary files /dev/null and b/versions/4.0/zh/shader/img/toon.png differ diff --git a/versions/4.0/zh/shader/img/transparency.png b/versions/4.0/zh/shader/img/transparency.png new file mode 100644 index 0000000000..acc486f8cf Binary files /dev/null and b/versions/4.0/zh/shader/img/transparency.png differ diff --git a/versions/4.0/zh/shader/img/unlit-shademode.png b/versions/4.0/zh/shader/img/unlit-shademode.png new file mode 100644 index 0000000000..e06d447c07 Binary files /dev/null and b/versions/4.0/zh/shader/img/unlit-shademode.png differ diff --git a/versions/4.0/zh/shader/img/use-pbr-map.png b/versions/4.0/zh/shader/img/use-pbr-map.png new file mode 100644 index 0000000000..aaacd77c40 Binary files /dev/null and b/versions/4.0/zh/shader/img/use-pbr-map.png differ diff --git a/versions/4.0/zh/shader/img/use-shade-map1.png b/versions/4.0/zh/shader/img/use-shade-map1.png new file mode 100644 index 0000000000..ad68696c1f Binary files /dev/null and b/versions/4.0/zh/shader/img/use-shade-map1.png differ diff --git a/versions/4.0/zh/shader/img/view-direction.png b/versions/4.0/zh/shader/img/view-direction.png new file mode 100644 index 0000000000..ba1dc6676e Binary files /dev/null and b/versions/4.0/zh/shader/img/view-direction.png differ diff --git a/versions/4.0/zh/shader/img/view-x-gradient.png b/versions/4.0/zh/shader/img/view-x-gradient.png new file mode 100644 index 0000000000..92d07effca Binary files /dev/null and b/versions/4.0/zh/shader/img/view-x-gradient.png differ diff --git a/versions/4.0/zh/shader/img/vs-ext.png b/versions/4.0/zh/shader/img/vs-ext.png new file mode 100644 index 0000000000..26432d119a Binary files /dev/null and b/versions/4.0/zh/shader/img/vs-ext.png differ diff --git a/versions/4.0/zh/shader/index.md b/versions/4.0/zh/shader/index.md new file mode 100644 index 0000000000..db7ad949c8 --- /dev/null +++ b/versions/4.0/zh/shader/index.md @@ -0,0 +1,55 @@ +# 着色器(Cocos Shader) + +![effect-show](img/effect-show.jpg) + +在现代显卡中,若要正确地绘制物体,需要书写基于顶点(Vertex)和片元(Fragment)的代码片段,这些代码片段称为 Shader。在基于 OpenGL 系列驱动的硬件设备上,Shader 支持一种名为 GLSL(OpenGL Shading Language)的着色器语言。 + +为了适配工业化制作流,提升着色器片段的易用性,Cocos Creator 基于 GLSL 封装了一套着色器 — [Cocos Shader](./effect-syntax.md)。 + +本章主要介绍 Cocos Shader 的工作方式和使用方式。 + +## 内容 + +本章节主要包含以下内容: + +- [创建与使用](effect-inspector.md) +- [内置着色器](effect-builtin.md) + - [基于物理的光照模型 PBR](effect-builtin-pbr.md) + - [卡通渲染](effect-builtin-toon.md) + - [无光照](effect-builtin-unlit.md) +- [着色器语法](effect-syntax.md) + - [Pass 可选配置参数](pass-parameter-list.md) + - [YAML 101 语法简介](yaml-101.md) + - [GLSL 语法简介](glsl.md) + - [预处理宏定义](macros.md) + - [着色器片段(Chunk)](effect-chunk-index.md) +- [内置全局 Uniform](uniform.md) +- [公共函数库](./common-functions.md) +- [前向渲染与延迟渲染 Shader 执行流程](./forward-and-deferred.md) +- [表面着色器 - Surface Shader](surface-shader.md) + - [内置 Surface Shader 导读](./surface-shader/builtin-surface-shader.md) + - [Surface Shader 基本结构](./surface-shader/surface-shader-structure.md) + - [Surface Shader 执行流程](./surface-shader/shader-code-flow.md) + - [include 机制](./surface-shader/includes.md) + - [宏定义与重映射](./surface-shader/macro-remapping.md) + - [使用宏定义实现函数替换](./surface-shader/function-replace.md) + - [可替换的内置函数](./surface-shader/surface-function.md) + - [渲染用途](./surface-shader/render-usage.md) + - [光照模型](./surface-shader/lighting-mode.md) + - [表面材质数据结构](./surface-shader/surface-data-struct.md) + - [着色器类别](./surface-shader/shader-stage.md) + - [组装器](./surface-shader/shader-assembly.md) + - [VS 输入](./surface-shader/vs-input.md) + - [FS 输入](./surface-shader/fs-input.md) + - [自定义 Surface Shader](./surface-shader/customize-surface-shader.md) + - [渲染调试功能](./surface-shader/rendering-debug-view.md) +- [传统着色器 - Legacy Shader](./legacy-shader/legacy-shader.md) + - [内置 Legacy Shader 导读](./legacy-shader/legacy-shader-builtins.md) + - [Legacy Shader 主要函数与结构体](./legacy-shader/legacy-shader-func-struct.md) +- [自定义着色器](./write-effect-overview.md) + - [2D 精灵着色器:Gradient](./write-effect-2d-sprite-gradient.md) + - [3D 着色器:RimLight](./write-effect-3d-rim-light.md) +- [自定义几何体实例化属性](./instanced-attributes.md) +- [UBO 内存布局策略](./ubo-layout.md) +- [WebGL 1.0 向下兼容支持](./webgl-100-fallback.md) +- [VSCode 着色器插件](./vscode-plugin.md) diff --git a/versions/4.0/zh/shader/instanced-attributes.md b/versions/4.0/zh/shader/instanced-attributes.md new file mode 100644 index 0000000000..bfad761817 --- /dev/null +++ b/versions/4.0/zh/shader/instanced-attributes.md @@ -0,0 +1,81 @@ +# 自定义几何体实例化属性 + +通过 **几何体实例化** 特性(GPU Instancing)可使 GPU 批量绘制模型相同且材质相同的渲染对象。如果我们想在不打破这一特性的情况下单独修改某个对象的显示效果,就需要通过自定义几何体实例化属性。 + +我们以新增一个颜色属性为例。 + +## 定义变量 + +实例化属性需要单独定义,并且处于 `USE_INSTANCING` 宏定义之下,否则会出现编译错误。 + +```glsl +#if USE_INSTANCING // when instancing is enabled + #pragma format(RGBA8) // normalized unsigned byte + in vec4 a_instanced_color; +#endif +``` + +## 用 vs 传递 + +虽然 a_instanced_color 仅用在 fs 中修改物体颜色,但几何体实例化属性属于顶点属性,只能在 vs 中被访问。 因此需要在 vs 中声明一个输出到 fs 的变量。 代码示例如下: + +```glsl +CCProgram vs %{ + #if USE_INSTANCING + out vec4 instancedColor; + #endif + + vec4 vert(){ + ... + #if USE_INSTANCING + instancedColor = a_instanced_color; + #endif + ... + } +}% +``` + +## 在 fs 中使用 + +通过在 fs 声明对应的 in 变量,获取到 vs 中传递过来的几何体实例化属性,实现想要的功能。 + +```glsl +CCProgram fs %{ + #if USE_INSTANCING + in vec4 instancedColor; + #endif + + vec4 frag(){ + ... + vec4 o = mainColor; + #if USE_INSTANCING + o *= instancedColor; + #endif + ... + } +}% +``` + +## 在脚本中设置属性 + +几何体实例化属性属于具体的渲染对象实例,无法通过材质属性面板设置,只能通过模型组件 MeshRenderer 上的 setInstancedAttribute 方法进行设置。 示例代码如下: + +```ts +const comp = node.getComponent(MeshRenderer); +comp.setInstancedAttribute('a_instanced_color', [100, 150, 200, 255]); +``` + +## 注意事项 + +有以下几点需要注意: +1. `#pragma format(RGBA8)` 用于指定此属性的具体数据格式,参数可以为引擎 `GFXFormat` 中的任意枚举名[^1];如未声明则默认为 RGBA32F 类型。 + +2. 所有实例化属性都是从利用顶点着色器(vs)的 attribute 输入,如果要在片元着色器(fs)中使用,需要先在 vs 中声明,再传递给 fs。 + +3. 请确保代码在所有分支都能正常执行,无论 `USE_INSTANCING` 是否启用。 + +4. 实例化属性的值在运行时会初始化为 0。 + +5. 如果在 **MeshRenderer** 组件上更换了材质,那么所有的实例化属性值都会被重置,需要重新设置。 + +[^1]: WebGL 1.0 平台下不支持整型 attributes,如项目需要发布到此平台,应使用默认浮点类型。 diff --git a/versions/4.0/zh/shader/legacy-shader/legacy-shader-builtins.md b/versions/4.0/zh/shader/legacy-shader/legacy-shader-builtins.md new file mode 100644 index 0000000000..b5f1a6a364 --- /dev/null +++ b/versions/4.0/zh/shader/legacy-shader/legacy-shader-builtins.md @@ -0,0 +1,121 @@ +# 内置 Legacy Shader 导读 + +Legacy Shader 相关的源码,有两个目录: +- internal/chunks/legacy/ +- internal/effects/legacy/ + +在 `chunks/legacy/` 目录中,存放的是一些公共函数,如解码器、雾效、输入、输出、阴影、骨骼蒙皮等等。 + +Legacy Shader 和 Surface Shader 都会调用 internal/chunks/builtin/ 和 internal/chunks/common/ 提供的函数。 + +在 `effects/legacy/` 目录中,提供了三个内置的 Legacy Shader: +- standard: 标准材质 +- terrain:用于地形渲染 +- toon:用于卡通渲染 + +## 基本结构 + +Legacy Shader 代码通常由几个部分组成: +- 信息描述(`CCEffect`):描述此 Shader 的技术、渲染过程组成部分,以及每个渲染过程使用的 Shader、渲染状态、属性等。 +- 共享常量(`Shared UBOs`):把 vs 和 fs 都需要用到的 uniforms 定义在一起,方便管理。 +- 主体函数(`Shader Body`):用于实现具体的 Shader 主体。 + +Legacy Shader 中的 CCEffect 和 共享常量部分与 Surface Shader 一致,可前往 [内置 Surface Shader 导读](../surface-shader/builtin-surface-shader.md) 了解详情。 + +## 着色函数 + +为了更好地理解渲染流程,请先查看 [前向渲染与延迟渲染 Shader 执行流程](./../forward-and-deferred.md)。 + +### standard(PBR) + +在 legacy/standard.effect 中,定义了着色相关的Shader代码: + +```ts +CCProgram standard-vs %{ + //... + void main(){ + StandardVertInput In; + CCVertInput(In); + //... + gl_Position = cc_matProj * (cc_matView * matWorld) * In.position; + } +}% + +CCProgram standard-fs %{ + //... + void surf(out StandardSurface s){ + //s.albedo = ... + //s.occlusion = ... + //s.roughness = ... + //s.metallic = ... + //s.specularIntensity = ... + //s.normal = ... + } + CC_STANDARD_SURFACE_ENTRY() +}% +``` + +可以看到,在 vs 中,直接使用了 main 函数作为入口,而在 fs 中,只有一个 surf 函数。 + +这是因为 `CC_STANDARD_SURFACE_ENTRY` 宏展开后,就是 main 函数,这个 main 函数会调用 surf 函数。 + +### terrain + +terrain 使用 StandardSurface 作为材质表面数据结构,使用 `CC_STANDARD_SURFACE_ENTRY` 作为入口。这就说明,terrain 的渲染流程与光照计算和 standard 完全一致。 + +只是由于地形采用的是多层纹理混合,所以 terrain 使用的纹理以及 surf 函数实现细节与 standard 的有较大区别。 + +### toon + +在 legacy/toon.effect 中,我们可以看到: + +```ts +CCProgram toon-vs %{ + //... + void main(){ + StandardVertInput In; + CCVertInput(In); + //... + gl_Position = cc_matProj * (cc_matView * matWorld) * In.position; + } +}% + +CCProgram toon-fs %{ + //... + void surf(out ToonSurface s){ + //s.baseStep = ... + //s.baseFeather = ... + //s.shadeStep = ... + //s.shadeFeather = ... + //s.shadowCover = ... + } + + void frag(){ + ToonSurface s; surf(s); + vec4 color = CCToonShading(s); + return CCFragOutput(color); + } +}% +``` + +toon 最大的特征是在 CCEffect 中,多定义了一个 outline pass,outline pass 的代码在 chunks/legacy/main-functions/outline-vs(fs) 中。 + +toon 材质表面数据结构为 ToonSurface,与 standard 使用的不一样。 在 frag 函数中可以看到, toon 的光照计算使用了专门的 CCToonShading 函数。 + +并且,toon 自己定义了一个 frag 入口函数,未使用 `CC_STANDARD_SURFACE_ENTRY` 宏。这也意味着,toon 是不支持延迟渲染的。 + +### shadow-caster + +可以看到,standard,terrain,toon 都有关于 shadow 的代码片段: + +```ts +CCProgram shadow-caster-vs %{ + //... +}% + +CCProgram shadow-caster-fs %{ + //... +}% +``` + +这个套 vs/fs 用于阴影贴图生成,引擎渲染管线会在阴影贴图生成阶段,查找 phase 为 shadow-add 的 pass 进行绘制。 diff --git a/versions/4.0/zh/shader/legacy-shader/legacy-shader-func-struct.md b/versions/4.0/zh/shader/legacy-shader/legacy-shader-func-struct.md new file mode 100644 index 0000000000..5318f8be5d --- /dev/null +++ b/versions/4.0/zh/shader/legacy-shader/legacy-shader-func-struct.md @@ -0,0 +1,223 @@ +# Legacy Shader 主要函数与结构体 + +## CCVertInput[^1] + +- 为对接骨骼动画与数据解压流程,我们提供了 `CCVertInput` 工具函数,它有 `general` 和 `standard` 两个版本,内容如下: + + ```glsl + // 位于 ‘input.chunk’ 的通用顶点着色器输入 + #define CCVertInput(position) \ + CCDecode(position); \ + #if CC_USE_MORPH \ + applyMorph(position); \ + #endif \ + #if CC_USE_SKINNING \ + CCSkin(position); \ + #endif \ + #pragma // 空 ‘pragma’ 技巧,在编译时消除尾随分号 + + // 位于 ‘input-standard.chunk’ 的标准顶点着色器输入 + #define CCVertInput(In) \ + CCDecode(In); \ + #if CC_USE_MORPH \ + applyMorph(In); \ + #endif \ + #if CC_USE_SKINNING \ + CCSkin(In); \ + #endif \ + #pragma // 空 ‘pragma’ 技巧,在编译时消除尾随分号 + + ``` + +- 如果只需要获取 **顶点位置信息**,可以使用 **general** 版本,那么顶点着色器函数开头的代码示例如下: + + ```glsl + #include + vec4 vert () { + vec3 position; + CCVertInput(position); + // ... 对位置信息做自定义操作 + } + ``` + + 如果还需要法线等信息,可使用 `standard` 版本,像下面这样写: + + ```glsl + #include + vec4 vert () { + StandardVertInput In; + CCVertInput(In); + // ... 此时 ‘In.position’ 初始化完毕,并且可以在顶点着色器中使用 + } + ``` + +上面的示例代码中,`StandardVertInput` 对象 `In` 会返回模型空间的顶点位置(`position`)、法线(`normal`)和切空间(`tangent`)信息,并对骨骼动画模型做完蒙皮计算。 + +`StandardVertInput` 结构体的定义如下: + +```glsl +struct StandardVertInput { + highp vec4 position; + vec3 normal; + vec4 tangent; +}; +``` + +> **注意**:引用头文件后,不要在 Shader 内重复声明这些 attributes(`a_position`、`a_normal`、`a_tangent` 等)。对于其他顶点数据(如 uv 等)还是需要声明 attributes 后再使用。 + +如果要对接引擎动态 Mesh 合批和几何体实例化(GPU Instancing),需要包含 `cc-local-batch` 头文件,通过 `CCGetWorldMatrix` 工具函数获取世界矩阵,示例如下: + +```glsl +mat4 matWorld; +CCGetWorldMatrix(matWorld); + +mat4 matWorld, matWorldIT; +CCGetWorldMatrixFull(matWorld, matWorldIT); +``` + +更多细节,请参考 [Cocos Shader 内置全局 Uniform](uniform.md)。 + +## CCFragOutput + +Cocos Shader 提供了 `CCFragOutput` 工具函数用以简化片元着色器的输出,可用于直接返回片元着色器所需要的值,代码示例如下: + +```glsl +#include +vec4 frag () { + vec4 o = vec4(0.0); + // ... 编写片元着实代码 + return CCFragOutput(o); +} +``` + +`CCFragOutput` 会根据管线状态来决定是否需要做 `ToneMap` 转码处理,这样中间的颜色计算就不必区分当前渲染管线是否为 HDR 流程。 + +代码示例如下: + +```glsl +vec4 CCFragOutput (vec4 color) { + #if CC_USE_HDR + color.rgb = ACESToneMap(color.rgb); + #endif + color.rgb = LinearToSRGB(color.rgb); + return color; +} +``` + +**特别注意**: + +如果采用 `CCFragOutput` 作为片元输出,中间的颜色运算必须转到 `Linear` 空间,因为 `CCFragOutput` 认为传入的参数是在 `Linear` 空间的,总是会进行 `LinearToSRGB` 转码。 + +`CCFragOutput` 函数一般不需要自己实现,它只起到与渲染管线对接的作用,且对于这种含有光照计算的输出,因为计算结果已经在 HDR 范围,所以应该包含 `output-standard` 而非 `output` 头文件。 + +如需包含标准的 PBR 光照计算,可使用 `StandardSurface` 结构体与函数 `CCStandardShadingBase` 一起构成 PBR 着色流程。 + +`StandardSurface` 结构体内容如下: + +```glsl + +struct StandardSurface { + // albedo + vec4 albedo; + // these two need to be in the same coordinate system + vec3 position; + vec3 normal; + // emissive + vec3 emissive; + // light map + vec3 lightmap; + float lightmap_test; + // PBR params + float roughness; + float metallic; + float occlusion; +}; +``` + +代码示例如下: + +```glsl +#include +#include +void surf (out StandardSurface s) { + // fill in your data here +} +vec4 frag () { + StandardSurface s; surf(s); + vec4 color = CCStandardShadingBase(s); + return CCFragOutput(color); +} +``` + +也可以参考 `builtin-standard.effect` 中,使用 `surf` 函数与 `CC_STANDARD_SURFACE_ENTRY()` 宏组合。 + +`CC_STANDARD_SURFACE_ENTRY()` 是一个 wrapper,会根据渲染状态,利用 `surf` 函数构建出一个可用于片元的 `main` 函数,代码示例如下: + +```glsl +CCProgram shader-fs %{ + #include + + void surf (out StandardSurface s) { + // fill in your data here + } + + CC_STANDARD_SURFACE_ENTRY() +}% +``` + +## StandardSurface + +StandardSurface 为 PBR 材质信息结构体,记录了一个像素需要进行光照计算的表面信息。 Lagecy Shader 中的 surf 函数主要用于填充,它会在光照阶段被使用。 + +```ts +struct StandardSurface { + // albedo + vec4 albedo; + // these two need to be in the same coordinate system + HIGHP_VALUE_STRUCT_DEFINE(vec3, position); + vec3 normal; + // emissive + vec3 emissive; + // light map + vec3 lightmap; + float lightmap_test; + // PBR params + float roughness; + float metallic; + float occlusion; + float specularIntensity; + + #if CC_RECEIVE_SHADOW + vec2 shadowBias; + #endif +}; +``` + +## ToonSurface + +```ts +struct ToonSurface { + vec4 baseColor; + vec4 specular; + // these two need to be in the same coordinate system + HIGHP_VALUE_STRUCT_DEFINE(vec3, position); + vec3 normal; + // shading params + vec3 shade1; + vec3 shade2; + vec3 emissive; + float baseStep; + float baseFeather; + float shadeStep; + float shadeFeather; + float shadowCover; + + #if CC_RECEIVE_SHADOW + vec2 shadowBias; + #endif +}; +``` + +和 StandardSurface 一样,ToonSurface 也是用于记录像素表面信息,只是它是卡通材质专用的结构体。 + +[^1]: 不包含粒子、Sprite、后期效果等不基于 Mesh 渲染的 Shader。 diff --git a/versions/4.0/zh/shader/legacy-shader/legacy-shader.md b/versions/4.0/zh/shader/legacy-shader/legacy-shader.md new file mode 100644 index 0000000000..49f00e12e1 --- /dev/null +++ b/versions/4.0/zh/shader/legacy-shader/legacy-shader.md @@ -0,0 +1,17 @@ +# 传统着色器 Legacy Shader + +相对于传统着色器而言,表面着色器让流程更统一,暴露给用户的细节更少,因此从 3.7.2 开始,Surface Shader 作为默认的 builtin-standard 出现。 + +但传统着色器(Legacy Shader) 与 表面着色器(Surface Shader)各有优缺: + +| 类型 | 优点 | 缺点 | +| :------- | :--- | :--- | +| Legacy Shader | 在面对特殊需求时更加灵活 | 暴露过多细节给用户,引擎升级时不易维护 | +| Surface Shader | 统一的着色流程,无需关注细节;引擎升级时用户层代码更易维护 | 需要理解整个实现机制才能掌握;能够自定义的功能有限 | + +另外,引擎内置的 builtin-unlit 依然使用了部分 legacy shader 库。 + +掌握 Legacy Shader 也有助于理解更多实现细节。 + +- [内置 Legacy Shader 导读](./legacy-shader-builtins.md) +- [Legacy Shader 主要函数与结构体](./legacy-shader-func-struct.md) diff --git a/versions/4.0/zh/shader/macros.md b/versions/4.0/zh/shader/macros.md new file mode 100644 index 0000000000..935d1e0bff --- /dev/null +++ b/versions/4.0/zh/shader/macros.md @@ -0,0 +1,127 @@ +# 预处理宏定义 + +为了更好地管理代码内容,Cocos Shader 提供了预处理宏机制,它有几个特性: +1. 不同组合的宏会生成不同的代码 +2. 生成的代码无冗余、执行高效 +3. 使用过的宏定义会显示在材质面板上,方便调试 +4. 以 `CC_` 开头的宏不会显示在材质面板上 + +以默认的 Surface Shader 为例,当它被材质使用时,你在材质的 **属性检查器** 中,可以看到如下图所示的宏开关: + +![macro-simple](img/macro-simple.png) + +## 宏定义注意事项 + +宏定义有一些默认的规则。 + +### 默认值 + +所有的宏定义默认值都是 false,因此当定义一个简单宏定义(例如用于布尔开关的宏)时,无法指定其默认值,但可通过 **属性检查器** 或代码修改。 + +如果设计上某些宏之间存在互斥关系(不能同时为 true),可以通过使用 tag 声明的宏来处理,详情请参考下文 [**Macro Tags**](#macro-tags) 部分的内容。 + +### 宏都会被定义 + +Cocos Shader 在运行时会用默认值 0 来定义所有 Shader 中出现的自定义宏 + +```ts +#define USE_INSTANCING 0 +#define USE_TWOSIDE 0 +#define USE_ALBEDO_MAP 0 +``` + +> 仅限用户自定义宏,`GL_` 等开头的系统宏不在此列 + +所以,不能使用 `#ifdef` 或 `#if defined` 这样的形式来判断自定义宏是否生效,执行结果会始终为 true; + +### 宏的使用范围 + +宏定义不仅可以应用在 `CCProgram` 里,控制宏定义内的代码逻辑,还可以应用在 `CCEffect` 中,将可编辑属性的显示状态与宏定义关联。 + +如下所示,仅当 `USE_USE_` 预处理宏开启时,`mainTexture` 才会显示在 **属性检查器** 面板上: + +```glsl +CCEffect %{ + # ... + properties: + mainTexture: { value: grey, target: albedoMap, editor: { parent: USE_ALBEDO_MAP, displayName: AlbedoMap } } + # ... +}% + +CCProgram unlit-fs %{ + // ... + vec4 frag () { + #if USE_ALBEDO_MAP + //... + #endif + } +}% +``` + +![macro-property](img/macro-property.png) + +## Macro Tags + +当多个宏会互斥,或者一个宏有多种判定值时,我们可以将它们合成一组。 + +像下面这个例子中, FACTOR 的值会有许多种: + +```glsl +#if FACTOR == -3 + // ... +#elif FACTOR == -2 + // ... +#elif FACTOR == 5 + // ... +#endif +``` + +针对这类有固定取值范围或固定选项的宏定义,需要选择一个合适的 tag 显式声明: + +| Tag | 说明 | 默认值 | 备注 | +| :-- | :-- | :-- | :-- | +| range | 一个长度为 2 的数组。首元素为最小值,末元素为最大值 | [0, 3] | 针对连续数字类型的宏定义,显式指定它的取值范围。
范围应当控制到最小,有利于运行时的 shader 管理 | +| options | 一个任意长度的数组,每个元素都是一个可能的取值 | 如未显式声明则不会定义任何宏 | 针对有清晰选项的宏定义,显式指定它的可用选项 | + +### range 的用法 + +```glsl +#pragma define-meta FACTOR range([-5, 5]) +``` + +我们可以使用 `range` 来描述 `FACTOR` 宏的值,使它能够在界面上显示出 [-5, 5] 区间的所有值作为选项。 如下所示: + +![macro-range-example](./img/macro-range-example.png) + +### options 的用法 + +当 FACTOR 只有 -3,-2,5 这三个值的情况下,我们就可以用 `options` 来处理。 + +```glsl +#pragma define-meta FACTOR options([-3, -2, 5]) +``` + +材质面板上可以产生如下所示的选项效果: + +![macro-options-example](./img/macro-options-example.png) + +## 函数宏 + +函数调用总是有一些额外开销的,我们可以利用函数式宏定义来实现 inline 函数,从而提升代码效率。 + +有不少工具函数都是函数式宏定义,比如: + +```glsl +#pragma define CCDecode(position) \ + position = vec4(a_position, 1.0) +#pragma define CCVertInput(position) \ + CCDecode(position); \ + #if CC_USE_SKINNING \ + CCSkin(position); \ + #endif \ + #pragma // empty pragma trick to get rid of trailing semicolons at effect compile time +``` + +WebGL 1.0 不支持函数式宏定义 (Function-like Macros),但是 Cocos Shader 在编译时支持了函数式宏定义,在输出的 Shader 中就已经将此类宏定义展开,可以放心使用。 + +更多关于宏定义的内容,请前往 [宏定义与重映射](./surface-shader/macro-remapping.md)。 diff --git a/versions/4.0/zh/shader/pass-parameter-list.md b/versions/4.0/zh/shader/pass-parameter-list.md new file mode 100644 index 0000000000..2f9aa2868d --- /dev/null +++ b/versions/4.0/zh/shader/pass-parameter-list.md @@ -0,0 +1,273 @@ +# Pass 可选配置参数 + +Pass 中的参数主要分两个部分: +- 开发者可自定义的 **属性检查器** 面板参数 `properties` +- 引擎提供的用于控制渲染管线状态的 `PipelineStates` + +## Properties + +`properties` 用于将 Shader 中定义的 `uniform` 进行别名映射。这个映射可以是某个 `uniform` 的完整映射,也可以是具体某个分量的映射(使用 `target` 参数),代码示例如下: + +```yaml +properties: + albedo: { value: [1, 1, 1, 1] } # uniform vec4 albedo + roughness: { value: 0.8, target: pbrParams.g } # uniform vec4 pbrParams + offset: { value: [0, 0], target: tilingOffset.zw } # uniform vec4 tilingOffset +# say there is another uniform, vec4 emissive, that doesn't appear here +# so it will be assigned a default value of [0, 0, 0, 0] and will not appear in the inspector +``` + +默认情况下,`properties` 中定义的属性参数会暴露并显示在编辑器的 **属性检查器** 面板中,方便进行可视化控制。 + +如果不想显示在 **属性检查器** 面板上,可在定义属性时加上 `editor: { visible: false }`,代码示例如下: + +```yaml +properties: + factor: { value: 1.0, editor: { visible: false } } +``` + +在 TypeScript 中可以使用 `Material` 类的 `setProperty` 方法以及 `Pass` 的 `setUniform` 方法进行设置,代码示例如下: + +```js +mat.setProperty('emissive', Color.GREY); // 直接设置对应的 Uniform 变量 +mat.setProperty('albedo', Color.RED); +mat.setProperty('roughness', 0.2); // 仅设置对应的分量 +const h = mat.passes[0].getHandle('offset'); // 获取对应的 Uniform 的句柄 +mat.passes[0].setUniform(h, new Vec2(0.5, 0.5)); // 使用 ‘Pass.setUniform’ 设置 Uniform 属性 +``` + +> **注意**:在 Shader 中定义的 `uniform` 也可以使用上述代码进行设置,即使没有在 `properties` 中定义。 + +未指定的 `uniform`,引擎将会在运行时根据自动分析出的数据类型给予默认值。更多关于默认值的内容,请参考下文说明。 + +为方便声明各 `property` 子属性,可以直接在 `properties` 内声明 `__metadata__` 项,所有 `property` 都会继承它声明的内容,如: + +```yaml +properties: + __metadata__: { editor: { visible: false } } + a: { value: [1, 1, 0, 0] } + b: { editor: { type: color } } + c: { editor: { visible: true } } +``` + +这样 uniform `a` 和 `b` 已声明的各项参数都不会受到影响,但都不会显示在 **属性检查器** 中(visible 为 false),而 uniform `c` 仍会正常显示。 + +### Property 参数列表 + +Property 中可配置的参数如下表所示,任何可配置的字段如果和默认值相同都可以省掉。 + +| 参数 | 默认值 | 可选项 | 备注 | +| :--- | :---- | :---- | :-- | +| target | **undefined** | undefined | 任意有效的 uniform 通道,可指定连续的单个或多个,但不可随机重排 | +| value | | | 详见下文 **Default Values** 部分的介绍 | +| sampler.
minFilter | **linear** | none, point, linear, anisotropic | | +| sampler.
magFilter | **linear** | none, point, linear, anisotropic | | +| sampler.
mipFilter | **none** | none, point, linear, anisotropic | | +| sampler.
addressU | **wrap** | wrap, mirror, clamp, border | | +| sampler.
addressV | **wrap** | wrap, mirror, clamp, border | | +| sampler.
addressW | **wrap** | wrap, mirror, clamp, border | | +| sampler.
maxAnisotropy | **16** | 16 | | +| sampler.
cmpFunc | **never** | never, less, equal, less_equal, greater, not_equal, greater_equal, always | | +| sampler.
borderColor | **[0, 0, 0, 0]** | [0, 0, 0, 0] | | +| sampler.
minLOD | **0** | 0 | | +| sampler.
maxLOD | **0** | 0 | 如果允许 mipmap 则要根据贴图修改最大 mip 值 | +| sampler.
mipLODBias | **0** | 0 | | +| editor.
displayName | **\*property name** | \*property name | 任意字符串 | +| editor.
type | **vector** | vector, color | | +| editor.
visible | **true** | true, false | | +| editor.
tooltip | **\*property name** | \*property name | 任意字符串 | +| editor.
range | **undefined** | undefined, [ min, max, [step] ] | | +| editor.
deprecated | **false** | true, false | deprecated 标记的数据表示在导入时已更新或在当前版本已废弃,其内容在保存时会被自动删除 | + +### Property 默认值 + +对于 Property 的默认值, Cocos Shader 做出了如下的规定: + +| 类型 | 默认值 | 可选项 | +| :---------- | :----- | :------ | +| int | | 0 | +| ivec2 | | [0, 0] | +| ivec3 | | [0, 0, 0] | +| ivec4 | | [0, 0, 0, 0] | +| float | | 0 | +| vec2 | | [0, 0] | +| vec3 | | [0, 0, 0] | +| vec4 | | [0, 0, 0, 0] | +| sampler2D | **default** | black, grey, white, normal, default | +| samplerCube | **default-cube** | black-cube, white-cube, default-cube | + +对于 `defines`: +- boolean 类型默认值为 false。 +- number 类型默认值为 0,默认取值范围为 [0, 3]。 +- string 类型默认值为 options 数组第一个元素。 + +## PipelineStates + +以下为 `PipelineStates` 相关参数,所有参数不区分大小写。 + +| 参数名 | 说明 | 默认值 | 备注 | +| :---- | :-- | :----- | :--- | +| switch | 指定这个 pass 的执行依赖于哪个 define。可以是任意有效的宏名称,但不应与使用到的 shader 中定义的任何 define 重名 | 未定义 | 这个字段默认是不存在的,意味着这个 pass 是无条件执行的 | +| priority | 指定这个 pass 的渲染优先级,数值越小渲染优先级越高,取值范围为 **0 ~ 255** | 128 | 可结合四则运算符指定相对值 | +| stage | 指定这个 pass 归属于管线的哪个 stage。可以是运行时管线中任何注册的 Stage 名称 | **default** | 对于默认的 forward 管线,只有 `default` 一个 stage | +| phase | 指定这个 pass 归属于管线的哪个 phase。可以是运行时管线中任何注册的 Phase 名称 | **default** | 对于默认的 forward 管线,可以是 `default`、`forward-add` 或者 `shadow-caster` | +| propertyIndex | 指定这个 pass 运行时的 uniform 属性数据要和哪个 pass 保持一致,例如 forward add 等 pass 需要和 base pass 一致才能保证正确的渲染效果。可以是任意有效的 pass 索引 | 未定义 | 一旦指定了此参数,材质面板上就不会再显示这个 pass 的任何属性 | +| embeddedMacros | 指定在这个 pass 的 shader 基础上额外定义的常量宏,可以是一个包含任意宏键值对的对象 | 未定义 | 只有当宏定义不同时才能在多个 pass 中使用此参数来复用 shader 资源 | +| properties | Properties 存储着这个 pass 中需要显示在 **属性检查器** 上的可定制的参数 | | 详见上文 **Properties** 部分的内容 | +| migrations | 迁移旧的材质数据 | | 详见下文 **Migrations** 部分的介绍 | +| primitive | 创建材质顶点数据 | **triangle_list** | 可选项包括:point_list、line_list、line_strip、line_loop
**triangle_list**、triangle_strip、triangle_fan
line_list_adjacency、line_strip_adjacency
triangle_list_adjacency、triangle_strip_adjacency
triangle_patch_adjacency、quad_patch_list、iso_line_list | +| RasterizerState | 光栅化时的可选渲染状态 | | 详见下文 **RasterizerState** 部分的介绍 | +| DepthStencilState | 深度和模板缓存的测试与状态 | | 详见下文 **DepthStencilState** 部分的介绍 | +| BlendState | 材质混合状态 | **false** | 详见下文 **BlendState** 部分的介绍 | + +## Migrations + +一般情况下,在使用材质资源时都希望底层的 effect 接口能始终向前兼容,但有时面对新的需求最好的解决方案依然是含有一定 breaking change 的,这时为了保持项目中已有的材质资源数据不受影响,或者至少能够更平滑地升级,就可以使用 effect 的迁移系统。 + +在 effect 导入成功后会 **立即更新工程内所有** 依赖于此 effect 的材质资源,对每个材质资源,会尝试寻找所有指定的旧参数数据(包括 **property** 和 **宏定义** 两类),然后将其复制或迁移到新属性中。 + +> **注意**:使用迁移功能前请一定先备份好项目工程,以免丢失数据! + +对于一个现有的 effect,迁移字段声明如下: + +```yaml +migrations: + # macros: # macros follows the same rule as properties, without the component-wise features + # USE_MIAN_TEXTURE: { formerlySerializedAs: USE_MAIN_TEXTURE } + properties: + newFloat: { formerlySerializedAs: oldVec4.w } +``` + +对于一个依赖于这个 effect,并在对应 pass 中持有属性的材质: + +```json +{ + "oldVec4": { + "__type__": "cc.Vec4", + "x": 1, + "y": 1, + "z": 1, + "w": 0.5 + } +} +``` + +在 effect 导入成功后,这些数据会被立即转换成: + +```json +{ + "oldVec4": { + "__type__": "cc.Vec4", + "x": 1, + "y": 1, + "z": 1, + "w": 0.5 + }, + "newFloat": 0.5 +} +``` + +在 **编辑器** 内重新编辑并保存这个材质资源后会变成(假设 effect 和 property 数据本身并没有改变): + +```json +{ + "newFloat": 0.5 +} +``` + +当然如果希望在导入时就直接删除旧数据,可以再加一条迁移信息来专门指定这点: + +```yaml +oldVec4: { removeImmediately: true } +``` + +这对于在项目有大量旧材质,又能够确定这个属性的数据已经完全冗余时会比较有用。 + +更多地,注意这里的通道指令只是简单的取 `w` 分量,事实上还可以做任意的 shuffle: + +```yaml +newColor: { formerlySerializedAs: someOldColor.yxx } +``` + +甚至基于某个宏定义: + +```yaml +occlusion: { formerlySerializedAs: pbrParams. } +``` + +这里声明了新的 occlusion 属性会从旧的 `pbrParams` 中获取,而具体的分量取决于 `OCCLUSION_CHANNEL` 宏定义。并且如果材质资源中未定义这个宏,则默认取 `z` 通道。
+但如果某个材质在迁移升级前就已经存着 `newFloat` 字段的数据,则不会对其做任何修改,除非指定为强制更新模式: + +```yaml +newFloat: { formerlySerializedAs: oldVec4.w! } +``` + +强制更新模式会强制更新所有材质的属性,无论这个操作是否会覆盖数据。 + +**注意**:强制更新操作会在编辑器的每次资源事件中都执行(几乎对应每一次鼠标点击,相对高频),因此只是一个快速测试和调试的手段,一定不要将处于强制更新模式的 effect 提交到版本控制。 + +再次总结一下为防止数据丢失所设置的相关规则: +- 为避免有效旧数据丢失,只要没有显式指定 `removeImmediately` 规则,就不会在导入时自动删除旧数据; +- 为避免有效的新数据被覆盖,如果没有指定为强制更新模式,对于那些既有旧数据,又有对应的新数据的材质,不会做任何迁移操作。 + +## RasterizerState + +| 参数名 | 说明 | 默认值 | 可选项 | +| :--------- | :-- | :----- | :--- | +| isDiscard | 引擎预留 | **false** | true, false | +| polygonMode | 多边形绘制模式 | **fill** | point,line,fill| +| shadeModel | 着色模型 | **flat** | flat, gourand| +| cullMode | 光栅化时剔除模式 | **back** | front, back, none | +| isFrontFaceCCW| 是否逆时针(CCW)前向 | **true** | true,false| +| depthBias| 深度偏移 | **0** | +| depthBiasSlop | 深度偏差斜率 | **0** | +| depthBiasClamp | 深度截断 | **0** | | +| isDepthClip | 允许深度剪裁操作
[Vulkan](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VK_EXT_depth_clip_enable.html) 专用 | **true** | true, false +| isMultisample| 是否开启多重采样 | **false** | true, false +| lineWidth | 线宽 | 1 | + +## DepthStencilState + +| 参数名 | 说明 | 默认值 | 可选项 | +| :--------- | :-- | :----- | :--- | +| depthTest | 是否开启深度测试 | **true** | true,false | +| depthWrite | 是否开启深度缓存写入 | **true** |true, false | +| depthFunc | 深度缓存比较方法 | **less** | never, less, equal, less_equal, greater, not_equal, greater_equal, always | +| stencilTestFront | 是否开启正面模板缓存测试 | **false** | true, false | +| stencilFuncFront | 正面模板缓存比较方法 | **always** | never, less, equal, less_equal, greater, not_equal, greater_equal, always | +| stencilReadMaskFront | 正面模板缓存读取掩码 | **0xffffffff** | 0xffffffff, `[1, 1, 1, 1]` | +| stencilWriteMaskFront | 正面模板缓存写入掩码 | **0xffffffff** | 0xffffffff, `[1, 1, 1, 1]`| +| stencilFailOpFront | 正面模板缓存测试失败时,如何处理缓冲区的值 | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilZFailOpFront | 正面模板缓存深度测试失败时,如何处理缓冲区的值 | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilPassOpFront | 正面模板缓存测试通过时,如何处理缓冲区的值 | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilRefFront | 正面模板缓存中的比较函数用于比较的值 | **1** | 1, `[0, 0, 0, 1]` | +| stencilTestBack | 是否开启背面模板缓存测试 | **false** | true, false | +| stencilFuncBack | 背面模板缓存比较方法 | **always** | never, less, equal, less_equal, greater, not_equal, greater_equal, always | +| stencilReadMaskBack | 背面模板缓存读取掩码 | **0xffffffff** | 0xffffffff, `[1, 1, 1, 1]` +| stencilWriteMaskBack | 背面模板缓存写入掩码 | **0xffffffff** | 0xffffffff, `[1, 1, 1, 1]` | +| stencilFailOpBack | 背面模板缓存测试失败时,如何处理缓冲区的值 | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilZFailOpBack | 背面模板缓存深度测试失败时,如何处理缓冲区的值 | **keep** | keep, zero, replace, incr, incr_wrap, decr, decr_wrap, invert | +| stencilRefBack | 背面模板缓存中的比较函数用于比较的值 | **1** | 1, `[0, 0, 0, 1]` | + +## BlendState + +| 参数名 | 说明 | 默认值 | 可选项 | +| :--- | :--- |:--- | :--- | +| isA2C | 是否开启半透明反锯齿(Alpha To Coverage)| **false** | true,false +| isIndepend | RGB 和 Alpha 是否分开混合 | **false** | true,false +| blendColor | 指定混合颜色 | **0** | 0, `[0, 0, 0, 0]` | +| targets | 混合配置,请参考下方的 targets | [] | | + +### Targets + +| 参数名 | 说明 | 默认值 | 可选项 | +| :--------- | :--- | :----- | :--- | +| Targets[i].
blend | 是否开启 **混合** | **false** | true, false | +| Targets[i].
blendEq | 指定 **混合源** 和 **混合目标** 的 RGB 的混合方程 | **add** | add, sub, rev_sub | +| Targets[i].
blendSrc | 指定 **混合源** 的 RGB 混合因子 | **one** | one, zero, src_alpha_saturate,
src_alpha, one_minus_src_alpha,
dst_alpha, one_minus_dst_alpha,
src_color, one_minus_src_color,
dst_color, one_minus_dst_color,
constant_color, one_minus_constant_color,
constant_alpha, one_minus_constant_alpha | +| Targets[i].
blendDst | 指定 **混合目标** 的 RGB 混合因子 | **zero** | one, zero, src_alpha_saturate,
src_alpha, one_minus_src_alpha,
dst_alpha, one_minus_dst_alpha,
src_color, one_minus_src_color,
dst_color, one_minus_dst_color,
constant_color, one_minus_constant_color,
constant_alpha, one_minus_constant_alpha | +| Targets[i].
blendSrcAlpha | 指定 **混合源** 的 Alpha 混合因子 | **one** | one, zero, src_alpha_saturate,
src_alpha, one_minus_src_alpha,
dst_alpha, one_minus_dst_alpha,
src_color, one_minus_src_color,
dst_color, one_minus_dst_color,
constant_color, one_minus_constant_color,
constant_alpha, one_minus_constant_alpha | +| Targets[i].
blendDstAlpha | 指定 **混合目标** 的 Alpha 混合因子 | **zero** | one, zero, src_alpha_saturate,
src_alpha, one_minus_src_alpha,
dst_alpha, one_minus_dst_alpha,
src_color, one_minus_src_color,
dst_color, one_minus_dst_color,
constant_color, one_minus_constant_color,
constant_alpha, one_minus_constant_alpha | +| Targets[i].
blendAlphaEq | 指定 **混合源** 与 **混合目标** 的 Alpha 混合方法 | **add** | add, sub, rev_sub | +| Targets[i].
blendColorMask | 指定是否可将 RGB,Alpha 分量写入帧缓存 | **all** | all, none, r, g, b, a, rg, rb, ra, gb, ga, ba, rgb, rga, rba, gba | +| dynamics | 可动态更新的管线状态 | [] | LINE_WIDTH, DEPTH_BIAS, BLEND_CONSTANTS, DEPTH_BOUNDS, STENCIL_WRITE_MASK, STENCIL_COMPARE_MASK | diff --git a/versions/4.0/zh/shader/surface-shader.md b/versions/4.0/zh/shader/surface-shader.md new file mode 100644 index 0000000000..42bed4022a --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader.md @@ -0,0 +1,27 @@ +# 表面着色器(Surface Shader) + +Surface Shader 使用高效、统一的渲染流程,让用户以简洁的代码创建表面材质信息,指定用于组合的光照和着色模型。 + +相比于 [传统着色器(Legacy Shader)](./legacy-shader/legacy-shader.md) ,它的优点是更易书写和维护,有更好的版本兼容性,也更不容易产生渲染错误。并且可以从统一流程中获取很多公共特性,如统一的全场景光照和渲染调试功能等。 + +在 Cocos Creator v3.7.2 中,Surface Shader 已作为默认的 builtin-standard,用户可以在创建菜单查看。原来的 builtin-standard 相关的着色流程,统一归类到了 internal/effects/legacy/ 目录。 + +由于基于统一的着色流程,使用 Surface Shader 的代价是无法对光照和着色运算进行大幅度的定制。如有极为特殊的需求,建议使用 Legacy Shader。 + +Surface Shader 相关内容列表: +- [内置 Surface Shader 导读](./surface-shader/builtin-surface-shader.md) +- [Surface Shader 结构概览](./surface-shader/surface-shader-structure.md) +- [Surface Shader 执行流程](./surface-shader/shader-code-flow.md) +- [include 机制](./surface-shader/includes.md) +- [宏定义与重映射](./surface-shader/macro-remapping.md) +- [使用宏定义实现函数替换](./surface-shader/function-replace.md) +- [可替换的内置函数](./surface-shader/surface-function.md) +- [渲染用途](./surface-shader/render-usage.md) +- [光照模型](./surface-shader/lighting-mode.md) +- [表面材质数据结构](./surface-shader/surface-data-struct.md) +- [着色器类别](./surface-shader/shader-stage.md) +- [组装器](./surface-shader/shader-assembly.md) +- [VS 输入](./surface-shader/vs-input.md) +- [FS 输入](./surface-shader/fs-input.md) +- [自定义 Surface Shader](./surface-shader/customize-surface-shader.md) +- [渲染调试功能](./surface-shader/rendering-debug-view.md) diff --git a/versions/4.0/zh/shader/surface-shader/builtin-surface-shader.md b/versions/4.0/zh/shader/surface-shader/builtin-surface-shader.md new file mode 100644 index 0000000000..6568c9c2dc --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/builtin-surface-shader.md @@ -0,0 +1,358 @@ +# 内置 Surface Shader 导读 + +Cocos Creator 3.7.2 版本开始, builtin-standard.effect 使用 Surface Shader 架构实现。 + +本文以 builtin-standard.effect 作为典型案例,讲解 Surface Shader 细节。 + +你可以熟悉 Surface Shader 结构定义、语法细节以及渲染流程。 + +下面的内容,建议配合 internal/effects/builtin-standard.effect 一起阅读。 + +## 基本结构 + +Surface Shader 代码通常由几个部分组成: +- 信息描述(`CCEffect`):描述此 Shader 的技术、渲染过程组成部分,以及每个渲染过程使用的 Shader、渲染状态、属性等。 +- 共享常量(`Shared UBOs`):把 vs 和 fs 都需要用到的 uniforms 定义在一起,方便管理。 +- 宏映射(`Macro Remapping`):处理一些宏定义,以及映射一些内部宏,使其可以显示到材质面板上。 +- 函数(`Surface Functions`):用于声明表面材质信息相关的 Surface 函数。 +- 组装器(`Shader Assembly`):用于组装每个顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)的代码模块。 + +可前往 [Surface Shader 基本结构](./surface-shader-structure.md) 了解更多详情。 + +## CCEffect + +一个物体被渲染到屏幕上,需要以下信息: +- 模型数据(顶点、UV、法线等) +- 光照信息 +- 世界空间(旋转、平移、缩放)信息 +- 绘制过程(材质) +- 渲染状态(材质) +- 纹理(材质) +- Uniform(材质) +- Shader 代码(材质) + +其中模型数据、光照信息、世界空间信息与材质无关,而 纹理、Uniform、渲染状态、Shader 代码、绘制过程 则属于材质信息。 + +CCEffect 则描述了以上材质相关信息,并且与引擎渲染管线共同完成一个模型的渲染流程。 + +### 渲染技术(technique) + +内置的 Surface Shader 实现了 `opaque` 和 `transparent` 两种渲染技术,前者用于渲染非透明物体,后者用于渲染半透明物体。 + +### 渲染过程(passes) + +内置的 Surface Shader 的每一个技术,只有一个 pass,且均为 PBR。 + +抛开其余细节,我们可以看到整个 Surface Shader 的信息描述如下: + +``` +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: standard-vs + frag: standard-fs + properties: &props + ... + + - name: transparent + passes: + - vert: standard-vs + frag: standard-fs + ... + properties: *props +}% +``` + +### Shader 入口(vert 与 frag) + +`opaque` 和 `transparent` 在渲染效果上完全一致,差异仅仅是渲染状态。 + +可以看到,它们使用同样的 vert 和 frag 入口。 + +```glsl +- vert: standard-vs + frag: standard-fs +``` + +### 属性(properties) + +由于 `opaque` 和 `transparent` 在渲染效果上完全一致,Shader 代码也是用的同一样,所以涉及到的属性也就完全一样。 + +所有渲染过程中用到的属性集中放在了 properties 字段里。关于属性的语法,可以查看 [Pass 可选配置参数](./../pass-parameter-list.md) + +### 配置复用 + +在属性配置中,可以看到, opaque 的属性定义为 `properties: &props`,而 `transparent` 的属性定义为 `properties: *props`。 + +这是一个属性命名和复用机制。 + +`properties: &props` 的含义是为当前 `properties` 起名为 `props`。 + +`properties: *props` 的含义是使用名为 `props` 的属性块填充当前 `properties`。 + +上面的配置结果为:`transparent` 直接使用 `opaque` 的 `properties` 字段。 + +### 用于特定阶段(phase) + +默认情况下, Surface Shader 会参与到场景渲染阶段。 但也有一些特殊的场合,比如 阴影,反射探针烘焙 等。 + +针对这类需求,我们可以添加特定的渲染过程(pass),并标记参与的阶段(phase)来实现目的。 + +当 Cocos 引擎的渲染器在执行渲染时,会获取到材质中相应 phase 的 pass,用于渲染。 如果没有,表示此物体不参与这个阶段。 + +如 Surface Shader 中的: +- forward-add:用于附加光照阶段,当物体受主光源以外的光源影响时,会调用这个。 +- shadow-caster:用于阴影图渲染阶段 +- reflect-map:用于环境反射探针烘焙阶段 + +```ts +- &forward-add + vert: standard-vs + frag: standard-fs + phase: forward-add + ... +- &shadow-caster + vert: shadow-caster-vs + frag: shadow-caster-fs + phase: shadow-caster + ... +- &reflect-map + vert: standard-vs + frag: reflect-map-fs + phase: reflect-map + ... +``` + +如上面代码所示,phase 用于标记此 pass 的参与阶段。 而 `&forward-add`,`&shadow-caster`,`&reflect-map` 则是给这个 pass 起了一个名字,方便后面的技术对它进行复用。 + +比如,`transparent` 就直接复用了 `opaque` 的 `forward-add` 和 `shadow-caster` pass。 + +```ts +- name: transparent + passes: + - vert: standard-vs + frag: standard-fs + ... + properties: *props + - *forward-add + - *shadow-caster +``` + +### 渲染状态 + +如开头所说,为了完成一个模型的渲染,除了定义好渲染过程,所需要的属性以外,还需要配合渲染状态。 + +渲染状态主要涉及 模板测试、深度测试、光栅器状态、透明混合等。 + +同样的渲染流程、属性、Shader 代码,配合不同的渲染状态,就可以实现不同的效果。 + +```ts +//深度、模板测试 +depthStencilState: + depthFunc: equal + depthTest: true + depthWrite: false + +//透明混合状态 +blendState: + targets: + - blend: true + blendSrc: one + blendDst: one + blendSrcAlpha: zero + blendDstAlpha: one +//光栅器状态 +rasterizerState: + cullMode: front +``` + +渲染状态会有一套默认值,在有需要的时候进行修改即可。 + +比如 `opaque` 与 `transparent` 就只有渲染状态的区别。 + +### 内嵌宏 + +```ts + embeddedMacros: { CC_FORCE_FORWARD_SHADING: true } +``` + +有时候,我们想为某个pass单独开启或者关闭一些宏,可以使用 `embeddedMacros` 字段进行。 + +## 代码引用机制 + +Surface Shader 提供了两种代码块引用机制:头文件和CCProgram。详情请查看 [include 机制](./includes.md)。 + +## 共享常量 + +许多常量是 vs 和 fs 都会用到的,或者多个 technique 和 pass 都需要用到的,定义在一起方便管理。 + +共享常量本质上还是属于 Shader 代码片段,使用 GLSL 来编写。 + +```ts +CCProgram shared-ubos %{ + uniform Constants { + vec4 tilingOffset; + vec4 albedo; + vec4 albedoScaleAndCutoff; + vec4 pbrParams; + vec4 emissive; + vec4 emissiveScaleParam; + vec4 anisotropyParam; + }; +}% +``` + +在后面的组装环节,只需要 `#include ` 即可。 + +## 宏映射 + +关于宏映射详细信息,请参考 [宏定义与重映射](./macro-remapping.md)。 + +在 内置的 Surface Shader 中,使用 `CCProgram macro-remapping` 片段来组织所的宏映射工作,方便管理。 + +可以看到,在内置的 Surface Shader 中,使用 `#pragma define-meta` 将许多内置的宏重定向到了面板上。 + +```ts +CCProgram macro-remapping %{ + // ui displayed macros + #pragma define-meta HAS_SECOND_UV + #pragma define-meta USE_TWOSIDE + #pragma define-meta IS_ANISOTROPY + #pragma define-meta USE_VERTEX_COLOR + + #define CC_SURFACES_USE_SECOND_UV HAS_SECOND_UV + #define CC_SURFACES_USE_TWO_SIDED USE_TWOSIDE + #define CC_SURFACES_LIGHTING_ANISOTROPIC IS_ANISOTROPY + #define CC_SURFACES_USE_VERTEX_COLOR USE_VERTEX_COLOR + + // depend on UI macros +#if IS_ANISOTROPY || USE_NORMAL_MAP + #define CC_SURFACES_USE_TANGENT_SPACE 1 +#endif + + // functionality for each effect + #define CC_SURFACES_LIGHTING_ANISOTROPIC_ENVCONVOLUTION_COUNT 31 +}% +``` + +## Surface 函数段 + +在 Surface Shader 中,定义了两个 CCProgram 用于处理具体的Shader计算。 +- CCProgram surface-vertex:用于处理 vs 相关计算 +- CCProgram surface-fragment:用于处理 fs 相关计算 + +### CCProgram surface-vertex + +内置的 vs 流程基本上能满足 Surface Shader 的 vs 需求,导致 surface-vertex 非常简单,只做了少量的特殊处理。 + +我们以第二套 UV 的处理函数为例。 + +它先定义了 `CC_SURFACES_VERTEX_MODIFY_UV` 宏,然后实现了 `SurfacesVertexModifyUV` 方法。 + +```ts +#define CC_SURFACES_VERTEX_MODIFY_UV +void SurfacesVertexModifyUV(inout SurfacesStandardVertexIntermediate In) +{ + In.texCoord = In.texCoord * tilingOffset.xy + tilingOffset.zw; + #if CC_SURFACES_USE_SECOND_UV + In.texCoord1 = In.texCoord1 * tilingOffset.xy + tilingOffset.zw; + #endif +} +``` + +这就是 Surface Shader 的核心机制,可以通过宏定义改写内部函数,在不修改内部源码的情况下实现特定的渲染需求。 + +具体的机制请参考[使用宏定义实现函数替换](function-replace.md) 和 [Surface Shader 内置可替换函数列表](surface-function.md)。 + +### CCProgram surface-fragment + +surface-fragment 主要实现了 PBR 计算时需要的表面材质信息填充。 + +#### 宏开关 + +```ts +#if USE_ALBEDO_MAP + uniform sampler2D albedoMap; + #pragma define-meta ALBEDO_UV options([v_uv, v_uv1]) +#endif +``` + +可以看到,在内置的 Surface Shader 中,所有的贴图,都被宏定义包裹起来,这样的好处就是可以根据需求关闭对应的宏,以提升性能。 + +#### 材质面板可选择的宏 + +`#pragma define-meta` + 名称 + `options([item0,item1,....])` 可以定义一个供用户选择的宏。 + +以下面代码为例: + +```ts +#pragma define-meta ALBEDO_UV options([v_uv, v_uv1]) +``` + +材质面板上, ALBEDO_UV 会出现下拉选择框,Shader 编译时,会以用户选择值为准。 + +比如,用户如果选择了 `v_uv1`,这条语句编译出来的最终结果为: + +```ts +#define ALBEDO_UV v_uv1 +``` + +```ts +#if USE_ALPHA_TEST + #pragma define-meta ALPHA_TEST_CHANNEL options([a, r]) +#endif + +``` + +ALPHA_TEST_CHANNEL 也是如此,默认使用 `a` 通道,但也可以选择 r 通道。 + +#### PBR 通道 + +```ts +#pragma define OCCLUSION_CHANNEL r +#pragma define ROUGHNESS_CHANNEL g +#pragma define METALLIC_CHANNEL b +#pragma define SPECULAR_INTENSITY_CHANNEL a +``` + +Surface Shader 使用一张图作为 PBR 贴图,根据定义就可以知道,PBR 贴图各通道的含义: + +- r通道:环境遮蔽 +- g通道:粗糙度 +- b通道:金属度 +- a通道:高光强度 + +#### 具体实现 + +与 surface-vertex 一样, surface-fragment 中也通过函数替换方式,实现 PBR 参数填充。 + +想要了解更多具体的机制请参考以下文章: +- [使用宏定义实现函数替换](function-replace.md) +- [Surface Shader 内置可替换函数列表](surface-function.md) +- [Surface Shader 执行流程](./shader-code-flow.md) + +## Shader 组装 + +上面提到的几个 CCProgram: + +- shared-ubos +- macro-remapping +- surface-vertex +- surface-fragment + +只是一些实现 Surface Shader 必要的组成部分,想要实现一个完整的 Surface Shader,还需要将这些部分,配合Surface Shader 内置的其它模块进行组装。 + +具体的组装机制请查看:[Surface Shader 组装](./shader-assembly.md)。 + +最后组装出来的 CCProgram,才是 CCEffect 部分引用的内容。 + +- CCProgram standard-vs + +- CCProgram shadow-caster-vs + +- CCProgram standard-fs + +- CCProgram shadow-caster-fs + +- CCProgram reflect-map-fs diff --git a/versions/4.0/zh/shader/surface-shader/customize-surface-shader.md b/versions/4.0/zh/shader/surface-shader/customize-surface-shader.md new file mode 100644 index 0000000000..63583770d9 --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/customize-surface-shader.md @@ -0,0 +1,82 @@ +# 自定义表面着色器 + +虽然 Surface Shader 提供了大多数场景材质都能适配的光照模型,但其功能还是较为固定的。 + +有时候,**用户需要使用完全定制化的光照计算和色彩计算**,比如说:一些特殊的、风格化的材质,需要轮廓光、额外的补光、非真实的环境照明等等。 + +针对这类极为特殊的情况,Surface Shader 也提供了自定义能力。 + +但需要注意的是,由于干预了表面材质数据和光照计算过程,渲染效果可能会出现意料之外的效果。 + +## 1、自定义 VS 输出与 FS 输入 + +我们可以在 VS 阶段新定义一个传递变量之后,在某个 Surface 函数中计算并输出该变量值。 + +在 FS 阶段定义一个同名变量之后在某个 Surface 函数中获取并使用该变量值。 + +详情请参考 [Fragment Shader 的输入参数](./fs-input.md):**自定义传递值**。 + +## 2、自定义材质信息 + +在 VS 函数块中添加如下代码: + +```glsl +//PBR 光照模型 +#include +// toon 光照模型 +//#include +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + // set user-defined data to surfaceData +} +``` + +函数开头的 `#include` 用于决定使用的**材质数据结构名称**,根据不同的 include 文件, 会采用不同的 `SurfacesMaterialData` 结构体。 + +具体内容,可以查看 **internal/chunks/surfaces/data-structures/** 目录下的 **standard.chunk** 和 **toon.chunk** + +当定义了 `CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA` 宏后,Shader编译器会选择你自己写的 `SurfacesFragmentModifySharedData` 来替换默认的函数。 + +此函数会在 vs 阶段被调用,具体可以查看 **internal/chunks/shading-entries/main-functions/** 目录下的: **render-to-scene/vs.chunk** 和 **render-to-shadowmap/vs.chunk** 文件。 + +在这个函数中,我们可以直接修改 surfaceData 里的属性,为光照阶段做准备。 + +## 自定义光照计算结果 + +有了上面自定义的 SurfacesMaterialData,我们还需要配合光照阶段,才能实现我们想要的计算效果。 + +在 FS 中,添加下面的代码: + +```glsl +#include +#define CC_SURFACES_LIGHTING_MODIFY_FINAL_RESULT +void SurfacesLightingModifyFinalResult(inout LightingResult result, in LightingIntermediateData lightingData, in SurfacesMaterialData surfaceData, in LightingMiscData miscData) +{ + // use surfaceData and lightingData for customizing lighting result +} +``` + +这个函数会在 fs.chunk 中被调用。 + +可以看到函数有四个参数: +- LightingIntermediateData:计算光照时需要的信息,如法线、视线方向、视距等等 +- SurfacesMaterialData:颜色、世界空间法线、PBR参数等信息 +- LightingMiscData:光源类型、位置、方向、颜色、强度等 +- LightingResult:用于返回光照结果,如 diffuse, specular, shadow,ao 等等。 + +在这个函数中,可以利用光照和材质参数,计算出光照结果,并放入 result 中。 + +对于局部光源(点光、聚光灯等)而言,此函数会逐光源执行。也就是说,如果物体受 6 个光源影响,这个函数会被调用 6 次。 + +如果希望 在重载函数内可以直接调用现成的内置光照模块函数,可以将 lighting-models/includes/common 改为对应光照模型使用的头文件。 + +比如,如果想要在函数中使用 PBR 光照模型内置的光照函数,可以包含 lighting-models/includes/standard 头文件。 + +在这个头文件中,会包含 lighting-models/model-functions/standard 头文件。 + +PBR光照相关的内置函数都在这里,直接调用即可。 + +## 更多自定义 + +如果上面的自定义机制还不能满足需求,建议参考 chunks/shading-entries 构建自己的 main 函数,以控制整个着色流程和计算细节。 diff --git a/versions/4.0/zh/shader/surface-shader/fs-input.md b/versions/4.0/zh/shader/surface-shader/fs-input.md new file mode 100644 index 0000000000..f592d7c281 --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/fs-input.md @@ -0,0 +1,65 @@ +# Fragment Shader 的输入值 + +## 内置输入列表 + +Vertex Shader 向 Fragment Shader 传递了许多常用变量,列表如下: + +| Fragment Shader 输入值 | 类型 | 使用时需要开启的宏 | 含义 | +| --------------------- | ----- | ------------------------------ | ------------------ | +| FSInput_worldPos | vec3 | N/A | World Position 世界坐标 | +| FSInput_worldNormal | vec3 | N/A | World Normal 世界法线 | +| FSInput_faceSideSign | float | N/A | Two Side Sign 物理正反面标记,可用于双面材质 | +| FSInput_texcoord | vec2 | N/A | UV0 | +| FSInput_texcoord1 | vec2 | N/A | UV1 | +| FSInput_vertexColor | vec4 | N/A | Vertex Color 顶点颜色 | +| FSInput_worldTangent | vec3 | N/A | World Tangent 世界切线 | +| FSInput_mirrorNormal | float | N/A | Mirror Normal Sign 镜像法线标记 | +| FSInput_localPos | vec4 | CC_SURFACES_TRANSFER_LOCAL_POS | Local Position 局部坐标 | +| FSInput_clipPos | vec4 | CC_SURFACES_TRANSFER_CLIP_POS | Clip Position 投影/裁切空间坐标 | + +## 宏开关 + +当需要使用带有宏开关的输入参数时,需要在 macro-remapping 代码段中开启相应的宏,示例代码如下: + +```glsl +CCProgram macro-remapping %{ + ... + //使用 FSInput_localPos + #define CC_SURFACES_TRANSFER_LOCAL_POS 1 + //使用 FSInput_clipPos + #define CC_SURFACES_TRANSFER_CLIP_POS 1 + ... +} +``` + +## 使用说明 + +在任意函数中直接调用即可。 + +## 自定义传递值 + +在制作一些特殊效果时,需要 Vertex Shader 向 Fragment Shader 传递更多信息,此时需要我们自定义传递变量。 + +新增一个自定义的传递变量非常简单,我们以新增一个 testVec3 为例。 + +首先,在 Vertex Shader 中声明一个带 out 标记的变量,示例代码如下: + +```glsl +CCProgram surface-vertex %{ + ... + out vec3 testVec3; + ... +} +``` + +再在 Fragment Shader 声明对应的带 in 标记的变量即可,示例代码如下: + +```glsl +CCProgram surface-fragment %{ + ... + in vec3 testVec3; + ... +} +``` + +接下来就可以在 Fragment Shader 的代码中使用 testVec3 了。 diff --git a/versions/4.0/zh/shader/surface-shader/function-replace.md b/versions/4.0/zh/shader/surface-shader/function-replace.md new file mode 100644 index 0000000000..b85d535c2e --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/function-replace.md @@ -0,0 +1,67 @@ +# 使用宏定义实现函数替换 + +有时候需要制作一些公用的函数供不同的需求场景使用, 以降低代码量和维护成本。 + +但流程里的某些函数,我们希望在某些场合替换为特定版本,以满足特定需求。 + +可以通过宏定义机制来完成。 + +```glsl +//example.chunk +#ifndef CC_USER_MODIFY_SOMETHING +void ModifySomething(){ + //do something here +} +#endif + +void Before(){ + //do something here +} + +void After(){ + //do something here +} + +void myFunc(){ + Before(); + ModifySomething(); + After(); +} +``` + +可以看到,在上面的代码中,myFunc 拥有完整的调用流程。如果想修改 ModifySomething 的实现,只需要在 #include example.chunk 之前,定义 CC_USER_MODIFY_SOMETHING 宏,并实现自己的 ModifySomething 函数即可。 + +```glsl +#define CC_USER_MODIFY_SOMETHING +void ModifySomething(){ + //do what you want +} + +#include +``` + +> 注意,**重载函数定义要放在 include 前面**。 + +这个机制在 Surface Shader 系统中被广泛应用,比如前面提到的 lighting-models/includes/common 中的 SurfacesFragmentModifySharedData 函数。 + +```glsl +// user-defined-common-surface.chunk: +// base surface function +#ifndef CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + ................. +} +#endif + +// effect +// this function needs overriding +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + ............. +} +// base functions should place after override functions +#include +``` diff --git a/versions/4.0/zh/shader/surface-shader/includes.md b/versions/4.0/zh/shader/surface-shader/includes.md new file mode 100644 index 0000000000..c9e911f642 --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/includes.md @@ -0,0 +1,68 @@ +# include 机制 + +## 头文件 + +Surface Shader 中,包含了多个[光照模型](./lighting-mode.md),如 standard,toon 等。 + +同时,我们拥有不同的[渲染用途](./render-usage.md),如 render-to-scene, render-to-shadowmap 等。 + +不同的光照模型拥有不同的表面材质数据结构和光照计算函数,不同的渲染用途之间,流程上会有细微的差别。 + +如果为不同的光照模型和不同的渲染用途编写独立的代码,会使整个架构很难维护。 + +因此,在 Surface Shader 中,我们将不同光照模型的方法和结构体,以相同的名字定义在不同的头文件中,**再使用 include 进行组合**。 + +这样就可以很容易实现,在同样的流程下,通过切换头文件实现不同的光照模型。 + +比如,在下面的代码中,我们通过 **#include ** 引入 **SurfacesMaterialData** 的定义,函数中使用的结构体,就是 PBR 表面材质数据结构体。 + +```glsl +//PBR 表面材质 +#include +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + // set user-defined data to surfaceData +} +``` + +如果我们更换它的头文件: + +```glsl +//toon 表面材质 +#include +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + // set user-defined data to surfaceData +} +``` + +此时的 SurfacesMaterialData 使用的就是 surfaces/data-structures/toon 中定义的结构体。 + +## CCProgram + +在 Surface Shader 中,CCProgram 包含的内容就是纯粹的 GLSL 代码内容。 我们可以将 Shader 分割到不同的 CCProgram 里形成代码片段,然后通过 `include` 进行引用。 + +以下面代码为例: + +```ts +CCProgram shared-ubos %{ + ... +}% +CCProgram macro-remampping %{ + ... +}% + +CCProgram vs %{ + #include + #include +}% + +CCProgram fs %{ + #include + #include +}% +``` + +> 注意:使用 `include` 引用 `CCProgram` 定义的代码片段时,仅限本文件内。 diff --git a/versions/4.0/zh/shader/surface-shader/lighting-mode.md b/versions/4.0/zh/shader/surface-shader/lighting-mode.md new file mode 100644 index 0000000000..ab8f8222e0 --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/lighting-mode.md @@ -0,0 +1,12 @@ +# 光照模型 + +光照模型用于说明物体表面与是如何对光线产生影响和作用的。 + +目前支持的光照模型如下: + +| 光照模型名称 | 说明 | +| ------------ | ------------------------------------------------------------ | +| standard | PBR 光照,支持 GGX BRDF 分布的各向同性与各向异性光照,支持卷积环境光照 | +| toon | 简单的卡通光照,阶梯状的光照效果 | + +与光照模型相关的内置 Shader 函数放在了 internal/chunks/lighting-models/includes/ 目录下,在 [Surface Shader 组装](./shader-assembly.md) 时,通过 include 引入对应文件,就能完成光照计算。 diff --git a/versions/4.0/zh/shader/surface-shader/macro-remapping.md b/versions/4.0/zh/shader/surface-shader/macro-remapping.md new file mode 100644 index 0000000000..edf268d039 --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/macro-remapping.md @@ -0,0 +1,87 @@ +# 宏定义与重映射 + +Surface Shader 内部计算时会用到一些宏开关,这些宏以 `CC_SURFACES_` 开头。 + +>注意:以 `CC_SURFACES_` 开头的宏不会出现在材质面板上。 + +以下是完整的宏列表: + +| 宏名 | 类型 | 含义 | +| :---------------------------------------------------- | ---- | ------------------------------------------------------------ | +| CC_SURFACES_USE_VERTEX_COLOR | BOOL | 是否使用顶点色 | +| CC_SURFACES_USE_SECOND_UV | BOOL | 是否使用2uv | +| CC_SURFACES_USE_TWO_SIDED | BOOL | 是否使用双面法线,用于双面光照 | +| CC_SURFACES_USE_TANGENT_SPACE | BOOL | 是否使用切空间(使用法线图或各向异性时必须开启) | +| CC_SURFACES_TRANSFER_LOCAL_POS | BOOL | 是否在 FS 中访问模型空间坐标 | +| CC_SURFACES_LIGHTING_ANISOTROPIC | BOOL | 是否开启各向异性材质 | +| CC_SURFACES_LIGHTING_ANISOTROPIC_ENVCONVOLUTION_COUNT | UINT | 各向异性环境光卷积采样数,为 0 表示关闭卷积计算,仅当各向异性开启时有效 | +| CC_SURFACES_LIGHTING_USE_FRESNEL | BOOL | 是否通过相对折射率 ior 计算菲涅耳系数 | +| CC_SURFACES_LIGHTING_TRANSMIT_DIFFUSE | BOOL | 是否开启背面穿透漫射光(如头发、叶片、耳朵等) | +| CC_SURFACES_LIGHTING_TRANSMIT_SPECULAR | BOOL | 是否开启背面穿透高光(如水面、玻璃折射等) | +| CC_SURFACES_LIGHTING_TRT | BOOL | 是否开启透射后内部镜面反射出的光线(如头发材质等) | +| CC_SURFACES_LIGHTING_TT | BOOL | 是否开启透射后内部漫反射出的光线(用于头发材质) | +| CC_SURFACES_USE_REFLECTION_DENOISE | BOOL | 是否开启环境反射除噪,仅 legacy 兼容模式下生效 | +| CC_SURFACES_USE_LEGACY_COMPATIBLE_LIGHTING | BOOL | 是否开启 legacy 兼容光照模式,可使渲染效果和 legacy/standard.effect 完全一致,便于升级 | + +> **注意**: 如果未定义这些宏,系统内部会自动定义为默认值 0; + +搜索 `CCProgram macro-remapping` 一段,可以看到内容有如下三部分组成: + +![macro-remapping](../img/macro-remapping.png) + +## 仅供面板显示的宏 + +```glsl +// ui displayed macros not used in this effect file +#pragma define-meta HAS_SECOND_UV +#pragma define-meta USE_TWOSIDE +#pragma define-meta USE_REFLECTION_DENOISE +#pragma define-meta USE_COMPATIBLE_LIGHTING +``` + +默认情况下,以 `CC_` 开头的宏不会显示在材质面板上。 当我们想让某一个宏开关显示在材质面板上时,可以像下面这样操作: + +1、使用 `#pragma define-meta` 定义一个面板宏,我们以 `HAS_SECOND_UV` 为例: + +```glsl +#pragma define-meta HAS_SECOND_UV +``` + +2、将 `CC_SURFACES_` 开头的宏重定向到这个宏,示例如下: + +```glsl +#define CC_SURFACES_USE_SECOND_UV HAS_SECOND_UV +``` + +这样,在这个 Shader 对应的材质面板上,你就可以通过控制 `HAS_SECOND_UV` 来影响 `CC_SURFACES_USE_SECOND_UV` 宏的值。 + +## 在 Surface 函数中使用过的宏 + +如果有宏在 Shader 代码中被使用,且不是以 `CC_` 开头的,会自动显示在材质面板上。 比如: + +```glsl +// ui displayed macros used in this effect file +#define CC_SURFACES_USE_VERTEX_COLOR USE_VERTEX_COLOR +#if IS_ANISOTROPY || USE_NORMAL_MAP + #define CC_SURFACES_USE_TANGENT_SPACE 1 +#endif +``` + +这种情况下,IS_ANISOTROPY 和 USE_NORMAL_MAP 会显示在材质面板上,并可以通过材质面板进行开关。 + +## 内部功能性的宏 + +对于某一些宏,我们不想要面板控制它,直接定义它的值即可,例如: + +```glsl +// functionality for each effect +#define CC_SURFACES_LIGHTING_ANISOTROPIC_ENVCONVOLUTION_COUNT 31 +``` + +## 隐藏宏 + +如果你的 Shader 中写了一些宏,但是不想出现在材质面板上,可以用 `CC_` 开头。 + +为了和系统内部宏有区别,建议使用 `CC_USER_` 开头。 + +更多宏定义相关内容,可以查看 [预处理宏定义](../macros.md)。 diff --git a/versions/4.0/zh/shader/surface-shader/render-usage.md b/versions/4.0/zh/shader/surface-shader/render-usage.md new file mode 100644 index 0000000000..91625999be --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/render-usage.md @@ -0,0 +1,53 @@ +# 渲染用途 + +默认情况下,Surface Shader 都是以输出到屏幕上,显示场景为主。 + +但有时候,我们也有一些特殊需要,比如: +- 渲染为阴影贴图(ShadowMap) +- 渲染为环境反射图(Reflection Probe) + +不同的渲染用途,有不同的渲染流程和细节,因此需要特殊处理。 + +Surface Shader 框架中,预定义了常见的不同用途的流程,在 **资源管理器/internal/chunks/shading-entries/main-functions/** 目录下可以找到。 + +以下是内置的渲染用途: + +| 常用的渲染用途 | 文件位置 | 备注| +| -------------------- | -------------------- | --- | +| 渲染到场景(默认) | render-to-scene || +| 渲染到阴影贴图 | render-to-shadowmap || +| 渲染到环境贴图 | render-to-reflectmap | 可选 | +| 渲染卡通描边 | misc/silhouette-edge || +| 渲染天空 | misc/sky || +| 后期处理或通用计算 Pass | misc/quad | 引擎预留 | + +只需要在 [Surface Shader 组装](./shader-assembly.md) 环节引用对应的头文件,就可以完成渲染流程。 + +比如,在 internal/effects/builtin-standard.effect 中,我们可以看到应用案例: + +```glsl +CCPogram standard-vs %{ + ... + #include +}% + +CCProgram shadow-caster-vs %{ + ... + #include +}% + +CCPogram standard-fs %{ + ... + #include +}% + +CCProgram shadow-caster-fs %{ + ... + #include +}% + +CCProgram reflect-map-fs %{ + ... + #include +}% +``` diff --git a/versions/4.0/zh/shader/surface-shader/rendering-debug-view.md b/versions/4.0/zh/shader/surface-shader/rendering-debug-view.md new file mode 100644 index 0000000000..aade4589ff --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/rendering-debug-view.md @@ -0,0 +1,106 @@ +# 渲染调试功能 + +> **注意**:只有使用 Surface Shader 框架的材质,内置的渲染调试功能才可以生效。 + +通过在编辑器的场景预览窗口右上角按钮选择对应的调试模式即可同屏查看模型、材质、光照及其他计算数据,在渲染效果异常的时候可以快速定位问题。 + +![debug-view](../img/debug-view.jpg) + +为了方便逐像素对比,我们使用全屏调试而非画中画的显示方式,可以快速在同一幅画面中切换不同数据来定位渲染错误所在,也可使用取色器来探知像素的具体数值。 + +另外 Surface Shader 还内置了 **无理数可视化** 的功能,一旦有一些像素出现异常的 红色(255, 0, 51) 绿色(0, 255, 51) 交替闪烁,则说明这些像素的渲染计算出现了无理数,请使用单项调试模式来检查模型切线或其他相关数据。 + +渲染调试功能细分为如下三种:
+ +## 1、公共选项 + +无论单项还是组合模式中都生效的调试选项,包括: + +| 名称 | 功能 | 调试技巧 | +| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 光照信息带固有色 | 勾选则显示正常材质光照,勾掉则显示白模纯光照的效果。 | 可更明显的查看AO、GI等间接光相关的影响。 | +| 级联阴影染色 | 级联阴影从近到远逐层染色,分布为偏红、绿、蓝、黄,超出阴影范围的区域无染色。 | 可查看并确认场景阴影的精细度。
如果三四层占比太少说明阴影过于精细,应当增大阴影可视距离。
如果一二层占比太少说明阴影过于粗糙,应当减小阴影可视距离。 | + +## 2、单项模式 + +调试重点聚焦在某个需要测试的数据上,整个场景都将此数据可视化输出。 + +可调试的数据包括四大类: + +### I、原始模型数据 + +| 名称 | 功能 | 说明及调试技巧 | 依赖项 | +| ---------------- | ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 顶点色 | 同名称 | | 必须在材质中勾选**使用顶点色** | +| 世界空间顶点法线 | 同名称 | | | +| 世界空间顶点切线 | 同名称 | 如果计算中出现了无理数或异常光照效果可以重点检查此处,如果是黑色则说明未开启模型切线但开启了法线贴图或各向异性。 | 不能在模型的切线设置中选择排除
必须在材质中勾选**使用法线贴图**或**是各向异性** | +| 世界空间顶点坐标 | 可视化顶点坐标(无缩放) | rgb显示xyz轴坐标数据。
可通过色彩来判断世界空间轴的不同,通过明暗来判断模型大小。 | | +| 法线图镜像 | 显示法线图翻转复用标记 | 为了提高法线图利用率,有些对称的模型(如面部)只会烘焙一半的法线,另一半使用镜像标记复用法线图中的数据。
如果该标记的值在模型上没有任何变化,可能导致模型另一半的法线呈现凹凸翻转等异常效果。 | 必须在材质中勾选**使用法线贴图** | +| 正反面标记 | 显示正面或反面 | 默认的单面模型,法线不会自动区分,在模型的正面和反面看到的明暗是相同的。
当单面模型使用双面材质时,会自动区分正反面法线,在模型的正面和反面看到的明暗是不同的。
该标记为白色表示模型正面,黑色表示反面。可通过它来查验单面模型的光照明暗问题。 | | +| UV0 | 显示第一套UV | | | +| UV1 | 显示第二套UV | | 必须在材质中勾选**使用第二套UV** | +| 光照贴图UV | 显示光照贴图UV | 如果光照贴图效果不正确,可开启此选项,对比光照贴图的大致区域来查错。 | 必须烘焙场景 | +| 投影深度Z | 显示(0-1非线性变化)深度 | 远裁面过大会导致近景深度值也会偏高 | | +| 线性深度W | 显示(0-1线性变化)深度 | 远裁面过大会导致近景深度值也会偏高 | | + +### II、原始材质数据 + +| 名称 | 功能 | 说明及调试技巧 | 依赖项 | +| ----------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------ | +| 世界空间像素法线 | 同名称 | 可以和对应的顶点数据做对比以查看法线贴图的影响是否正确 | | +| 世界空间像素切线/副法线 | 同名称 | 可以和对应的顶点数据做对比以查看法线贴图的影响是否正确 | 不能在模型的切线设置中选择排除
必须在材质中勾选**使用法线贴图**或**是各向异性** | +| 固有色 | 同名称 | | | +| 漫反射颜色 | 从固有色和其他材质数据计算出的影响漫反射光照的基础色 | | | +| 镜面反射颜色 | 从固有色和其他材质数据计算出的影响镜面反射光照的基础色 | | | +| 不透明度 | 越小表示越透明 | | 必须在材质中开启**Alpha Blend** | +| 金属度 | 同名称 | | | +| 粗糙度 | 同名称 | | | +| 镜面反射强度 | 显示非金属基准镜面反射率F0的倍增 | 如果镜面反射都为黑色,请检查此项数据是否设置为0 | | + +### III、光照结果数据 + +| 名称 | 功能 | 说明及调试技巧 | 依赖项 | +| -------------- | ----------------------------------- | -------------- | ------------------------------------------------ | +| 直接光漫反射 | 同名称 | | | +| 直接光镜面反射 | 同名称 | | | +| 直接光照 | 相当于直接光漫反射 + 直接光镜面反射 | | | +| 环境光漫反射 | 同名称 | | | +| 环境光镜面反射 | 同名称 | | | +| 环境光照 | 相当于环境光漫反射 + 环境光镜面反射 | | | +| 自发光 | 显示材质中设置的自发光颜色 | | | +| 光照贴图 | 显示烘焙的光照贴图RGB颜色 | | 必须烘焙场景 | +| 阴影 | 显示平行光、聚光灯、点光的阴影 | | 必须在场景面板和光源中开启阴影,物体开启接收阴影 | +| 环境光遮蔽 | 显示材质中设置的AO贴图及实时AO的值 | | | + +### IV、其他数据 + +| 名称 | 功能 | 说明及调试技巧 | 依赖项 | +| ------ | ---------------------------- | -------------- | ------------------------ | +| 雾因子 | 显示雾效因子,越大说明雾越浓 | | 必须在场景面板中开启雾效 | + +## 3、组合模式 + +调试重点聚焦在总体的渲染表现上,可以屏蔽或打开每个模块,模块之间互不关联,可查看不同模块之间对渲染效果的影响。 + +单项模式优先级高于组合模式,如果想使用组合模式的话,必须选中无单项模式。 + +包括: + +| 名称 | 功能 | 说明及调试技巧 | 类别 | +| -------------- | ----------------------- | ------------------------------------------------------------ | -------- | +| 直接光漫反射 | 开启/禁用直接光漫反射 | 影响平行光、聚光灯和点光 | 光照 | +| 直接光镜面反射 | 开启/禁用直接光镜面反射 | 影响平行光、聚光灯和点光 | 光照 | +| 环境光漫反射 | 开启/禁用环境光漫反射 | 影响天光 | 光照 | +| 环境光镜面反射 | 开启/禁用环境光镜面反射 | 影响天光 | 光照 | +| 自发光 | 开启/禁用自发光 | 如果有些物体过亮或者曝掉,可尝试关闭此选项是不是材质中设置了不必要的自发光 | 光照 | +| 光照贴图 | 开启/禁用烘焙光照 | 影响烘焙 | 光照 | +| 阴影 | 开启/禁用直接光阴影 | 影响实时平行光/聚光灯/点光阴影和烘焙的平行光阴影 | 光照 | +| 环境光遮蔽 | 开启/禁用环境光遮蔽 | 影响天光 | 光照 | +| 法线贴图 | 开启/禁用法线图 | 如果光照异常散乱,可尝试关闭此选项以查看是否是法线图扰动强度不当所致
如果光照效果错误或出现无理数,可尝试关闭此选项以查看是否是模型未开启切线所致 | 材质 | +| 雾 | 开启/禁用雾效 | 如果场景颜色异常变灰,可尝试关闭此选项以查看是否是雾参数设置不当所致 | 环境 | +| 色调映射 | 开启/禁用色调映射 | 如果场景色彩与原材质差异过大,可尝试关闭此选项查看是否正常,说明场景面板中不该勾选UseHDR | 色彩空间 | +| 伽马矫正 | 开启/禁用伽马矫正 | 如果场景色彩异常浓艳与偏暗,可尝试关闭此选项查看是否正常,说明贴图资源可能被多次伽马矫正了 | 色彩空间 | + +## 4、运行时使用渲染调试 + +使用引擎内置资源中的预设体 `tools/debug-view-runtime-control`,将它拖到场景 Canvas 节点下即可在运行时使用 UI 来进行渲染调试。 diff --git a/versions/4.0/zh/shader/surface-shader/shader-assembly.md b/versions/4.0/zh/shader/surface-shader/shader-assembly.md new file mode 100644 index 0000000000..e886a19478 --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/shader-assembly.md @@ -0,0 +1,165 @@ +# Surface Shader 组装 + +## Shader片段组装器 + +在内置的 Surface Shader 文件中,你会看到下面的代码片段: + +```glsl +CCProgram standard-vs %{ + //includes +}% + +CCProgram shadow-caster-vs %{ + //includes +}% + +CCProgram standard-fs %{ + //includes +}% + +CCProgram shadow-caster-fs %{ + //includes +}% + +CCProgram reflect-map-fs %{ + //includes +}% +``` + +这类以 xxx-vs, xxx-fs 命名的 CCProgram 代码片段,就是我们的组装器。 + +在这些代码片段中,我们使用 #include 关键字,根据需要,引入不同模块头的文件,按顺序组装每个Shader。 + +## include 两种方式 + +在这些 include 中,你可以看到如下两种情况: + +```glsl +//include from CCProgram +#include + +//include from file +#include +``` + +我们可以引入一个外部 chunk 文件,也可以通过名字,引入一个先前定义好的 CCProgram,比如 macro-remapping。 + +## 主要部分 + +以 `standard-fs` 为例,我们可以看到整个 Fragment Shader 的组装分为以下 6 个部分。 + +## 1、宏 + +首先需要包含必要的内部宏映射和通用宏定义。 + +宏映射使用在 Macro Remapping 一段中描述的自定义 CCProgram 代码块或 chunk 文件。 + +接下来需要包含通用宏定义文件 `common-macros`,如下所示: + +```glsl +#include +#include +``` + +对于一些特殊渲染用途的 Shader 而言,建议直接包含对应渲染用途的宏定义文件,以 ShadowMap 为例: + +```glsl +CCProgram shadow-caster-fs %{ + ... + #include + ... +}% +``` + +## 2、通用头文件 + +根据 **当前的 Shader 类型(Shader Stage)** 来选择对应的通用头文件,如下所示: + +```glsl +//Vertex Shader +CCProgram standard-vs %{ + ... + #include + ... +}% + +//Fragment Shader +CCProgram standard-fs %{ + ... + #include + ... +}% +``` + +## 3、Surface Shader 主体 + +这个部分是 Surface Shader 中的主体部分。 + +比如外部常量 uniforms,shared-ubos 代码块。 + +以及 Surface Shader 中,用户可以控制的主体函数,比如内置着色器里的 surface-vertex 和 surface-fragment 代码段。 + +如下所示: + +```glsl +CCProgram standard-fs %{ + ... + #include + #include + ... +}% +``` + +## 4、光照模型 + +此部分为**可选项**,Vertex Shader ,以及渲染到 ShadowMap 时,不需要。 + +这个部分的作用,是使用 **光照模型名称** 来选择对应的头文件,如下所示: + +```glsl +//Standard PBR Lighting +#include + +//Toon Lighting +#include +``` + +## 5、表面材质数据结构 + +此部分为**可选项**,渲染到 ShadowMap 时,不需要。 + +选择与光照模型对应的表面材质数据结构,如下所示: + +```glsl +//Vertex Shader +//Standard +#include +//Toon +#include + +//Fragment Shader +//Standard +#include +//Toon +#include +``` + +## 6、主函数 + +使用**渲染用途名称 + Shader 类型(Shader Stage)** 来选择对应的主函数头文件,如下图所示: + +```glsl +//standard-vs: +#include + +//shadow-caster-vs: +#include + +//standard-fs: +#include + +//shadow-caster-fs: +#include +``` + +更多信息,可参考 [内置 Surface Shader 导读](./builtin-surface-shader.md)。 diff --git a/versions/4.0/zh/shader/surface-shader/shader-code-flow.md b/versions/4.0/zh/shader/surface-shader/shader-code-flow.md new file mode 100644 index 0000000000..c5a36b0a70 --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/shader-code-flow.md @@ -0,0 +1,225 @@ +# Surface Shader 执行流程 + +Surface Shader 统一了着色流程,同时为 vs 和 fs 提供了大量的自定义函数,大家可以根据自己的需求,重写相关函数。 + +请参考 [Surface Shader 内置函数](./surface-function.md) 和 [使用宏定义实现函数替换](./function-replace.md)。 + +本文主要目的在于帮助开发者熟悉 Surface Shader 执行流程,弄清楚各函数调用时机。 + +## 函数入口 + +我们先看一下内置的 Surface Shader 文件的 CCEffect 部分: + +```glsl +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: standard-vs + frag: standard-fs + ... +}% +``` + +可以看到, 每一个 pass 的 vert 和 frag 没有指定专有的入口函数,这就表示,它所用到的 vs 和 fs 的入口函数为 main。 + +从 [Surface Shader 结构](./surface-shader-structure.md) 中,我们可以了解到, 在 [Surface Shader 组装](./shader-assembly.md) 环节,每一个 Surface Shader 会根据不同的 [渲染用途](./render-usage.md),引入不同的头文件。 这个头文件就是我们的入口函数。 + +## VS 主函数 + +以内置 Surface Shader 的 standard-vs 为例: + +```glsl +CCProgram standard-vs %{ + #include +}% +``` + +可以看到,它引入的是 render-to-scene 下面的 vs.chunk。 + +打开 render-to-scene/vs.chunk,可以看到,它只有一个 main 函数。代码和注释如下: + +```glsl +void main() +{ + //声明一个表面材质数据结构 + SurfacesStandardVertexIntermediate In; + + //获取顶点基本数据 + CCSurfacesVertexInput(In); + //获取顶点动画数据 + CCSurfacesVertexAnimation(In); + // ===== 本地坐标相关 ==== + //处理本地坐标,用户可用宏替换 + In.position.xyz = SurfacesVertexModifyLocalPos(In); + //处理本地法线,用户可用宏替换 + In.normal.xyz = SurfacesVertexModifyLocalNormal(In); + //处理本地切线,用户可用宏替换 + #if CC_SURFACES_USE_TANGENT_SPACE + In.tangent = SurfacesVertexModifyLocalTangent(In); + #endif + //进一步处理自定义本地数据,用户可用宏替换 + SurfacesVertexModifyLocalSharedData(In); + + //==== 世界坐标相关====== + //进行世界坐标转换,并填充数据结构 + CCSurfacesVertexWorldTransform(In); + //额外的处理世界坐标函数,用户可用宏替换 + In.worldPos = SurfacesVertexModifyWorldPos(In); + + //投影空间坐标 + In.clipPos = cc_matProj * cc_matView * vec4(In.worldPos, 1.0); + //进一步处理投影空间坐标,用户可用宏替换 + In.clipPos = SurfacesVertexModifyClipPos(In); + + //其它一些数据变换 + vec3 viewDirect = normalize(cc_cameraPos.xyz - In.worldPos); + In.worldNormal.w = dot(In.worldNormal.xyz, viewDirect) < 0.0 ? -1.0 : 1.0; + //进一步处理世界法线,用户可用宏替换 + In.worldNormal.xyz = SurfacesVertexModifyWorldNormal(In); + + //进一步处理UV,用户可用宏替换 + SurfacesVertexModifyUV(In); + //进一步处理自定义数据,用户可用宏替换 + SurfacesVertexModifySharedData(In); + + //其它变换 + //UV + CCSurfacesVertexTransformUV(In); + //雾效 + CCSurfacesVertexTransferFog(In); + //阴影 + CCSurfacesVertexTransferShadow(In); + //光照贴图UV + CCSurfacesVertexTransferLightMapUV(In); + + //最终输出 + CCSurfacesVertexOutput(In); +} +``` + +## FS 主函数 + +同样的,我们以内置 Surface Shader 的 standard-fs 为例: + +```glsl +CCProgram standard-fs %{ + #include +}% +``` + +render-to-scene/fs.chunk 文件内容如下: + +```glsl +#if (CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_FORWARD || CC_FORCE_FORWARD_SHADING) + #include +#elif CC_PIPELINE_TYPE == CC_PIPELINE_TYPE_DEFERRED + #include +#endif +``` + +可以看到它区分了 forward 和 deferred 管线。 + +### forward-fs + +```glsl +//定义颜色输出目标 +layout(location = 0) out vec4 fragColorX; + +void main() { + #if CC_DISABLE_STRUCTURE_IN_FRAGMENT_SHADER + //获取基本颜色和透明度,用户可用宏替换 + vec4 color = SurfacesFragmentModifyBaseColorAndTransparency(); + #else + //获取表面材质数据 + SurfacesMaterialData surfaceData; + CCSurfacesFragmentGetMaterialData(surfaceData); + + //计算阴影参数 + vec2 shadowBias = vec2(0.0); + ... + + + //计算雾效参数 + #if !CC_FORWARD_ADD + float fogFactor = 1.0; + #endif + + //计算光照 + LightingResult lightingResult; + CCSurfacesLighting(lightingResult, surfaceData, shadowBias); + + + //渲染调试相关 + ... + + //像素着色计算 + vec4 color = CCSurfacesShading(surfaceData, lightingResult); + + ... + + //颜色输出 + #if CC_USE_RGBE_OUTPUT + fragColorX = packRGBE(color.rgb); // for reflection-map + return; + #endif + + //HDR,LinearToSRGB 等最终运算 + #if CC_USE_HDR + #if CC_USE_DEBUG_VIEW == CC_SURFACES_DEBUG_VIEW_COMPOSITE_AND_MISC && CC_SURFACES_ENABLE_DEBUG_VIEW + if (IS_DEBUG_VIEW_COMPOSITE_ENABLE_TONE_MAPPING) + #endif + color.rgb = ACESToneMap(color.rgb); + #endif + #if CC_USE_DEBUG_VIEW == CC_SURFACES_DEBUG_VIEW_COMPOSITE_AND_MISC + if (IS_DEBUG_VIEW_COMPOSITE_ENABLE_GAMMA_CORRECTION) + #endif + color.rgb = LinearToSRGB(color.rgb); + + #if !CC_FORWARD_ADD && CC_USE_FOG != CC_FOG_NONE + CC_APPLY_FOG_BASE(color, fogFactor); + #endif + + fragColorX = CCSurfacesDebugDisplayInvalidNumber(color); +} +``` + +### deferred-fs + +延迟渲染由于分成两个阶段:GBuffer 和 Lighting。 + +在 GBuffer 阶段主要是填充各个渲染目标,只需要收集对应的材质表面数据即可,代码如下: + +```glsl +//GBuffer 0,1,2 +layout(location = 0) out vec4 fragColor0; +layout(location = 1) out vec4 fragColor1; +layout(location = 2) out vec4 fragColor2; + +void main () { + //收集表面材质数据 + SurfacesMaterialData surfaceData; + CCSurfacesFragmentGetMaterialData(surfaceData); + + //填充 GBuffer + fragColor0 = CCSurfacesDeferredOutput0(surfaceData); + fragColor1 = CCSurfacesDeferredOutput1(surfaceData); + fragColor2 = CCSurfacesDeferredOutput2(surfaceData); + + //调试渲染相关 + #if CC_USE_DEBUG_VIEW == CC_SURFACES_DEBUG_VIEW_SINGLE && CC_SURFACES_ENABLE_DEBUG_VIEW + vec4 debugColor = vec4(0.0, 0.0, 0.0, 1.0); + CCSurfacesDebugViewMeshData(debugColor); + CCSurfacesDebugViewSurfaceData(debugColor, surfaceData); + if (IS_DEBUG_VIEW_ENABLE_WITH_CAMERA) { + fragColor0 = debugColor; + } + #endif +} +``` + +延迟渲染的 Lighting 阶段,是受引擎渲染管线控制的,会统一使用 GBuffer 进行光照计算,可参考 internal/effects/deferred-lighting.effect。 + +其余的渲染用途主函数同理,可以到 internal/chunks/shading-entries/ 目录下查看。 + +> 提示:可以被替换的代码,都以 Surface###Modify### 方式命名。 diff --git a/versions/4.0/zh/shader/surface-shader/shader-stage.md b/versions/4.0/zh/shader/surface-shader/shader-stage.md new file mode 100644 index 0000000000..7114807f53 --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/shader-stage.md @@ -0,0 +1,30 @@ +# Shader 类型 + +渲染是由不同的着色器来完成的,有处理顶点的、有处理像素的、有用于通用计算的。 + +在 Surface Shader 架构中,为了良好的可读性和可维护性,不同的 Shader 类型会有一个约定的代码标识, +如下表所示: + +| 着色器阶段 | 对应的 Surface Shader 代码标识 | +| --------------- | ---------------------------- | +| Vertex Shader | vs | +| Fragment Shader | fs | +| Computer Shader | cs | + +你可以在内置的 effect 和 chunk 文件中发现许多文件以 xxxx-vs 或者 xxxx-fs 命名。 + +而在 effect 和 chunk 文件中, 也有许多类似 `CCProgram xxx-vs %{}%` 和 `CCProgram xxx-fs %{}%` 的代码片段定义。 + +比如: + +```ts +CCProgram standard-vs %{ + //... +}% + +CCProgram standard-fs %{ + //... +}% +``` + +用户在编写自己的 Shader 时,最好也遵守这个约定,以维持源码的可读性与维护性。 diff --git a/versions/4.0/zh/shader/surface-shader/surface-data-struct.md b/versions/4.0/zh/shader/surface-shader/surface-data-struct.md new file mode 100644 index 0000000000..7af4f6e54a --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/surface-data-struct.md @@ -0,0 +1,34 @@ +# 表面材质数据结构 + +## 什么是表面材质数据 + +Surface Shader 中,根据不同[光照模型](./lighting-mode.md)的需求,提供了对应的表面材质数据结构 **SurfaceMaterialData** 。 + +表面材质数据结构体定义了一系列用于计算物体表面最终颜色的物理参数,如 反照率、粗糙度等。 + +> 注意:材质数据模型与光照模型必须关联使用 + +## 表面材质数据类型 + +| 材质数据类型 | 说明 | +| ------------ | ------------------------------------------------------------ | +| standard | 粗糙度和金属性描述的标准 PBR 材质,和 SP、Blender、Maya 等软件中的材质节点类似 | +| toon | 简单的卡通材质,有多种 shade 颜色处理 | | + +## 如何使用 + +对应的表面材质数据结构存放在 **internal/chunks/data-structures/** 目录下。 + +只需要 include 对应的表面材质数据结构头文件,就可以使用不同类别的数据结构。 + +```glsl +//PBR 表面材质 +#include +// toon 表面材质 +//#include +#define CC_SURFACES_FRAGMENT_MODIFY_SHARED_DATA +void SurfacesFragmentModifySharedData(inout SurfacesMaterialData surfaceData) +{ + // set user-defined data to surfaceData +} +``` diff --git a/versions/4.0/zh/shader/surface-shader/surface-function.md b/versions/4.0/zh/shader/surface-shader/surface-function.md new file mode 100644 index 0000000000..0b32e5529e --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/surface-function.md @@ -0,0 +1,75 @@ +# Surface Shader 可替换的内置函数 + +Surface Shader 统一了着色流程,同时向用户提供了大量的自定义函数,大家可以根据自己的需求,利用宏机制,重写相关函数。 + +## 1、原理 + +Surface Shader 提供的自定义函数,在内部都有一个默认版本,并且在适合的时候被调用。可以参考 [Surface Shader 执行流程](./shader-code-flow.md)了解详情。 + +这些函数通常以 `Surfaces+Shader类型名+Modify+属性` 方式全名,比如: +- SurfacesVertexModifyLocalPos +- SurfacesVertexModifyLocalNormal +- SurfacesVertexModifyLocalTangent + +所有函数可以在 [`internal/chunks/surfaces/default-functions/`](https://github.com/cocos/cocos4/tree/v4.0.0/editor/assets/chunks/surfaces/default-functions) 中查看。 + +**如果你想替换某函数的实现,可以通过预定义该函数对应的宏来完成**。 + +比如,可以预先定义 `CC_SURFACES_VERTEX_MODIFY_WORLD_POS` 宏,让 Surface Shader 使用你定义的函数来计算世界坐标,示例代码如下: + +```glsl +#define CC_SURFACES_VERTEX_MODIFY_WORLD_POS +vec3 SurfacesVertexModifyWorldPos(in SurfacesStandardVertexIntermediate In) +{ + vec3 worldPos = In.worldPos; + worldPos.x += sin(cc_time.x * worldPos.z); + worldPos.y += cos(cc_time.x * worldPos.z); + return worldPos; +} +``` + +如果对 Surface Shader 中的函数替换机制不熟悉,可以先参考 [使用宏定义实现函数替换](./function-replace.md)。 + +> 用这种方式的好处是可以方便地扩展多种不同的材质数据结构、光照模型和渲染用途,并且不用对内置的 Surface Shader 流程进行修改。 + +## 2、VS 对应的常用函数列表 + +可用宏替换的 VS 内置函数定义在:internal/chunks/surfaces/default-functions/common-vs.chunk 文件中。 + +在 VS 中的内置函数参数均为 `SurfacesStandardVertexIntermediate` 结构体,存放的是 VS 输入输出的数据。用户无需关心具体的顶点输入输出流程处理,只需要聚焦到某个数据的修改即可。 + +| 预先定义宏 | 对应的函数定义 | 对应的材质模型 | 功能说明 | +| ------------------------------------------- | ---------------------------------------- | -------------- | ------------------------------------------------------------ | +| CC_SURFACES_VERTEX_MODIFY_LOCAL_POS | vec3 SurfacesVertexModifyLocalPos | Common | 返回修改后的模型空间坐标 | +| CC_SURFACES_VERTEX_MODIFY_LOCAL_NORMAL | vec3 SurfacesVertexModifyLocalNormal | Common | 返回修改后的模型空间法线 | +| CC_SURFACES_VERTEX_MODIFY_LOCAL_TANGENT | vec4 SurfacesVertexModifyLocalTangent | Common | 返回修改后的模型空间切线和镜像法线标记 | +| CC_SURFACES_VERTEX_MODIFY_LOCAL_SHARED_DATA | void SurfacesVertexModifyLocalSharedData | Common | 如果某些贴图和计算需要在多个材质节点中使用,可在此函数中进行,在世界变换前调用,直接修改 SurfaceStandardVertexIntermediate 结构体内的三个Local参数 | +| CC_SURFACES_VERTEX_MODIFY_WORLD_POS | vec3 SurfacesVertexModifyWorldPos | Common | 返回修改后的世界空间坐标(世界空间动画) | +| CC_SURFACES_VERTEX_MODIFY_CLIP_POS | vec4 SurfacesVertexModifyClipPos | Common | 返回修改后的剪裁(NDC)空间坐标(通常用于修改深度) | +| CC_SURFACES_VERTEX_MODIFY_UV | void SurfacesVertexModifyUV | Common | 修改结构体内的 UV0 和 UV1 (使用 tiling 等) | +| CC_SURFACES_VERTEX_MODIFY_WORLD_NORMAL | vec3 SurfacesVertexModifyWorldNormal | Common | 返回修改后的世界空间法线(世界空间动画) | +| CC_SURFACES_VERTEX_MODIFY_ SHARED_DATA | void SurfacesVertexModify SharedData | Common | 如果某些贴图和计算需要在多个材质节点中使用,可在此函数中进行,直接修改 SurfaceStandardVertexIntermediate 结构体内的参数,减少性能耗费 | + +## 3、FS 对应的常用函数列表 + +FS 由 PBR 和 Toon 两个部分组成,分别在下面两个文件中: +- internal/chunks/surfaces/default-functions/standard-fs.chunk +- internal/chunks/surfaces/default-functions/toon-vs.chunk + +FS 中的函数,大部分为无参函数,用户需要结合 [FS 输入值](./fs-input.md) 来做处理。对于一些特殊用途的函数,也提供了对应的参数。具体属于哪种情况请参考函数定义。 + +| 预先定义宏 | 对应的函数定义 | 对应的材质模型 | 功能说明 | +| ------------------------------------------------------- | ---------------------------------------------------- | -------------- | ------------------------------------------------------------ | +| CC_SURFACES_FRAGMENT_MODIFY_ BASECOLOR_AND_TRANSPARENCY | vec4 SurfacesFragmentModify BaseColorAndTransparency | Common | 返回修改后的基础色(rgb 通道)和透明值(a 通道) | +| CC_SURFACES_FRAGMENT_ALPHA_CLIP_ONLY | vec4 SurfacesFragmentModify AlphaClipOnly | Common | 不需要获取颜色仅处理透贴的Pass中使用。如渲染到阴影图等,不重载此函数可能导致阴影没有透贴效果 | +| CC_SURFACES_FRAGMENT_MODIFY_ WORLD_NORMAL | vec3 SurfacesFragmentModify WorldNormal | Common | 返回修改后的像素法线(通常是法线贴图) | +| CC_SURFACES_FRAGMENT_MODIFY_ SHARED_DATA | void SurfacesFragmentModify SharedData | Common | 若某些贴图和计算需要在多个材质节点中使用,可在此函数中进行,直接修改 Surface 结构体内的参数,减少性能耗费,类似legacy shader中的surf()函数。**需要在定义函数前 include 必要的头文件** | +| CC_SURFACES_FRAGMENT_MODIFY_ WORLD_TANGENT_AND_BINORMAL | void SurfacesFragmentModify WorldTangentAndBinormal | Standard PBR | 修改 Surface 结构体内的世界切空间向量 | +| CC_SURFACES_FRAGMENT_MODIFY_ EMISSIVE | vec3 SurfacesFragmentModify Emissive | Standard PBR | 返回修改后的自发光颜色 | +| CC_SURFACES_FRAGMENT_MODIFY_ PBRPARAMS | vec4 SurfacesFragmentModify PBRParams | Standard PBR | 返回修改后的 PBR 参数(ao, roughness, metallic, specularIntensity) | +| CC_SURFACES_FRAGMENT_MODIFY_ ANISOTROPY_PARAMS | vec4 SurfacesFragmentModify AnisotropyParams | Standard PBR | 返回修改后的各向异性参数(rotation, shape, unused, unused) | +| CC_SURFACES_FRAGMENT_MODIFY_ BASECOLOR_AND_TOONSHADE | void SurfacesFragmentModify BaseColorAndToonShade | Toon | 修改卡通渲染基础色 | +| CC_SURFACES_FRAGMENT_MODIFY_ TOON_STEP_AND_FEATHER | vec4 SurfacesFragmentModify ToonStepAndFeather | Toon | 返回修改后的参数 | +| CC_SURFACES_FRAGMENT_MODIFY_ TOON_SHADOW_COVER | vec4 SurfacesFragmentModify ToonShadowCover | Toon | 返回修改后的参数 | +| CC_SURFACES_FRAGMENT_MODIFY_ TOON_SPECULAR | vec4 SurfacesFragmentModify ToonSpecular | Toon | 返回修改后的参数 | +| CC_SURFACES_LIGHTING_MODIFY_FINAL_RESULT | void SurfacesLightingModifyFinalResult | Common | 自定义光照模型,可以在之前计算的光照结果上再次修改,比如添加轮廓光等。**需要在定义函数前 include 必要的头文件** | diff --git a/versions/4.0/zh/shader/surface-shader/surface-shader-structure.md b/versions/4.0/zh/shader/surface-shader/surface-shader-structure.md new file mode 100644 index 0000000000..56fc92b7c9 --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/surface-shader-structure.md @@ -0,0 +1,106 @@ +# Surface Shader 结构 + +一个典型的 Surface Shader 通常由四个主要部分构成: + +1. CCEffect +2. 共享常量声明 +3. 宏映射 +4. 主体功能函数 +5. Shader组装器:用于将上面4个部分与内置的Surface Shader 功能进行级任 + +## 1、CCEffect + +CCEffect 用于描述 Surface Shader 的 techniques, pass , 属性以及渲染状态等信息。材质会根据 CCEffect 中的描述生成默认值,以及在材质面板上显示。 + +具体内容请参考 [Cocos Shader 语法](../effect-syntax.md)。 + +## 2、共享常量声明 + +共享常量声明会将所有 pass, vs 和 fs 都需要用到的常量写在一起,简化 Shader 编写。 + +这是一个可选项,但对于复杂的 Shader 来说,建议使用。以内置 Surface Shader 为例: + +```glsl +CCProgram shared-ubos %{ + uniform Constants { + vec4 titlingOffset; + ... + } +}% +``` + +> 注意:并不一定非要叫 `shared-ubos` 和 `Constants` ,只要方便记忆即可。 + +## 3、宏映射 + +Surface Shader 提供了大量的内置宏,这些宏默认是不显示在面板上的,如果想让这些宏显示在面板上,就需要重新映射它们的名字。 + +并且,你可以将一些宏集中放在这个代码片段中,方便管理,以内置 Surface Shader 为例: + +```glsl +CCProgram macro-remapping %{ + // ui displayed macros + #pragma define-meta HAS_SECOND_UV + #pragma define-meta USE_TWOSIDE + ... + #define CC_SURFACES_USE_SECOND_UV HAS_SECOND_UV + #define CC_SURFACES_USE_TWO_SIDED USE_TWOSIDE +}% +``` + +更多详情请参考 [宏定义与重映射](./macro-remapping.md)。 + +> 注意:并不一定非要叫 `macro-remapping` 只要方便记忆即可。 + +## 4、函数块 + +Surface Shader 统一了渲染流程,同时也暴露了许函数可供用户自定义渲染细节。 + +为了方便管理,我们一般至少需要声明两个 CCProgram 代码片段,用于 vs 和 fs。 + +以内置的 Surface Shader 为例: + +```glsl +CCProgram surface-vertex %{ + ... +}% + +CCProgram surface-fragment %{ + ... +}% +``` + +`surface-vertex` 用于 vs 相关函数的处理,`surface-fragment` 用于 fs 相关函数的处理。 + +在这两个代码片段中,通过宏定义机制,替换内部函数。也可以增加自己的 vs 到 fs 的输入。 + +详情请参考 [使用宏定义实现函数替换](./function-replace.md), [Vertex Shader 的输入值](./vs-input.md) 和 [Fragment Shader 的输入值](./fs-input.md)。 + +## 5、Shader 组装器 + +Surface Shader 中,最后一个部分,是 Shader 的组装。 + +我们以内置的 Surface Shader 为例: + +```glsl +CCProgram standard-vs %{ + ... +}% + +CCProgram shadow-caster-vs %{ + ... +}% + +CCProgram standard-fs %{ + ... +}% + +CCProgram shadow-caster-fs %{ + ... +}% +``` + +值得说明的是,上面三个部分(共享常量声明、宏映射 +主体功能函数)的代码片段,可以有零个或者多个。最后根据需求组装出最后的 Shader, 供 Surface Shader 开头部分的 CCEffect 引用。 + +详情请参考 [Surface Shader 组装](./shader-assembly.md)。 diff --git a/versions/4.0/zh/shader/surface-shader/vs-input.md b/versions/4.0/zh/shader/surface-shader/vs-input.md new file mode 100644 index 0000000000..d07d55e62a --- /dev/null +++ b/versions/4.0/zh/shader/surface-shader/vs-input.md @@ -0,0 +1,49 @@ +# Vertex Shader 的输入值 + +## 参数列表 + +Vertex Shader 输入值都在 `SurfacesStandardVertexIntermediate` 结构体中,作为函数参数传入。 + +| Vertex Shader 输入值 | 类型 | 使用时需要开启的宏 | 含义 | +| ------------------- | ---- | ----------------------------- | ------------------------------------ | +| position | vec4 | N/A | 本地坐标 | +| normal | vec3 | N/A | 本地法线 | +| tangent | vec4 | CC_SURFACES_USE_TANGENT_SPACE | 本地切线和镜像法线标记| +| color | vec4 | CC_SURFACES_USE_VERTEX_COLOR | 顶点颜色| +| texCoord | vec2 | N/A | UV0 | +| texCoord1 | vec2 | CC_SURFACES_USE_SECOND_UV | UV1 | +| clipPos | vec4 | N/A | 投影坐标 | +| worldPos | vec3 | N/A | 世界坐标 | +| worldNormal | vec4 | N/A | 世界法线和双面材质标记| +| worldTangent | vec3 | CC_SURFACES_USE_TANGENT_SPACE | 世界切线| +| worldBinormal | vec3 | CC_SURFACES_USE_TANGENT_SPACE | 世界副法线| + +## 宏开关 + +当需要使用带有宏开关的输入参数时,需要在 macro-remapping 代码段中开启相应的宏,示例代码如下: + +```glsl +CCProgram macro-remapping %{ + ... + //使用第二套 UV + #define CC_SURFACES_USE_SECOND_UV 1 + //使用世界副法线 + #define CC_SURFACES_USE_TANGENT_SPACE 1 + ... +} +``` + +## 使用示例 + +在任何带 SurfacesStandardVertexIntermediate 参数的函数中,都可以直接访问到相关参数,以 SurfacesVertexModifyWorldPos 函数为例: + +```glsl +#define CC_SURFACES_VERTEX_MODIFY_WORLD_POS +vec3 SurfacesVertexModifyWorldPos(in SurfacesStandardVertexIntermediate In) +{ + vec3 worldPos = In.worldPos; + worldPos.x += sin(cc_time.x * worldPos.z); + worldPos.y += cos(cc_time.x * worldPos.z); + return worldPos; +} +``` diff --git a/versions/4.0/zh/shader/ubo-layout.md b/versions/4.0/zh/shader/ubo-layout.md new file mode 100644 index 0000000000..48df12ec61 --- /dev/null +++ b/versions/4.0/zh/shader/ubo-layout.md @@ -0,0 +1,70 @@ +# UBO 内存布局策略 + +Cocos Shader 规定,所有非 sampler 类型的 uniform 都应以 UBO(Uniform Buffer Object/Uniform Block)形式声明。 + +以内置着色器 `builtin-standard.effect` 为例,其 uniform block 声明如下: + +```glsl +uniform Constants { + vec4 tilingOffset; + vec4 albedo; + vec4 albedoScaleAndCutoff; + vec4 pbrParams; + vec4 miscParams; + vec4 emissive; + vec4 emissiveScaleParam; + }; +``` + +并且所有的 UBO 应当遵守以下规则: +1. 不应出现 vec3 成员; +2. 对数组类型成员,每个元素 size 不能小于 vec4; +3. 不允许任何会引入 padding 的成员声明顺序。 + +Cocos Shader 在编译时会对上述规则进行检查,以便在导入错误(implicit padding 相关)时及时提醒修改。 + +这可能听起来有些过分严格,但背后有非常务实的考量:
+首先,UBO 是渲染管线内要做到高效数据复用的唯一基本单位,离散声明已不是一个选项;
+其次,WebGL2 的 UBO 只支持 std140 布局,它遵守一套比较原始的 padding 规则[^1]: + +- 所有 vec3 成员都会补齐至 vec4: + + ```glsl + uniform ControversialType { + vec3 v3_1; // offset 0, length 16 [IMPLICIT PADDING!] + }; // total of 16 bytes + ``` + +- 任意长度小于 vec4 类型的数组和结构体,都会将元素补齐至 vec4: + + ```glsl + uniform ProblematicArrays { + float f4_1[4]; // offset 0, stride 16, length 64 [IMPLICIT PADDING!] + }; // total of 64 bytes + ``` + +- 所有成员在 UBO 内的实际偏移都会按自身所占字节数对齐[^2]: + + ```glsl + uniform IncorrectUBOOrder { + float f1_1; // offset 0, length 4 (aligned to 4 bytes) + vec2 v2; // offset 8, length 8 (aligned to 8 bytes) [IMPLICIT PADDING!] + float f1_2; // offset 16, length 4 (aligned to 4 bytes) + }; // total of 32 bytes + + uniform CorrectUBOOrder { + float f1_1; // offset 0, length 4 (aligned to 4 bytes) + float f1_2; // offset 4, length 4 (aligned to 4 bytes) + vec2 v2; // offset 8, length 8 (aligned to 8 bytes) + }; // total of 16 bytes + ``` + +这意味着大量的空间浪费,且某些设备的驱动实现也并不完全符合此标准[^3],因此目前 Cocos Shader 选择限制这部分功能的使用,以帮助排除一部分非常隐晦的运行时问题。 + +>**再次提醒:uniform 的类型与 inspector 的显示和运行时参数赋值时的程序接口可以不直接对应,通过 [property target](pass-parameter-list.md#Properties) 机制,可以独立编辑任意 uniform 的具体分量。** + +[^1]: [OpenGL 4.5, Section 7.6.2.2, page 137](http://www.opengl.org/registry/doc/glspec45.core.pdf#page=159) + +[^2]: 注意在示例代码中,UBO IncorrectUBOOrder 的总长度为 32 字节,实际上这个数据到今天也依然是平台相关的,看起来是由于 GLSL 标准的疏忽,更多相关讨论可以参考 [这里](https://bugs.chromium.org/p/chromium/issues/detail?id=988988)。 + +[^3]: **Interface Block - OpenGL Wiki**: diff --git a/versions/4.0/zh/shader/uniform.md b/versions/4.0/zh/shader/uniform.md new file mode 100644 index 0000000000..b87e6f0068 --- /dev/null +++ b/versions/4.0/zh/shader/uniform.md @@ -0,0 +1,65 @@ +# Cocos Shader 内置全局 Uniform + +要在 Cocos Shader 中使用内置变量 Uniform,需要包含对应的着色器片段(Chunk)即可,如下代码所示: + +```ts +//local uniforms +#include +//global uniforms +#include +``` + +以下是常用内置变量,按所在着色器片段进行分组,列表如下: + +## `cc-local.chunk` + +| Name | Type | Info | +| :-- | :-- | :-- | +| `cc_matWorld` | mat4 | 模型空间转世界空间矩阵 | +| `cc_matWorldIT` | mat4 | 模型空间转世界空间逆转置矩阵 | + +## `cc-global.chunk` + +| Name | Type | Info | +| :-- | :-- | :-- | +| `cc_time` | vec4 | x:游戏运行时间(秒)
y:帧时间(秒)
z:游戏运行帧数
w:未使用 | +| `cc_screenSize` | vec4 | xy:屏幕尺寸
zw:屏幕尺寸倒数 | +| `cc_screenScale` | vec4 | xy:屏幕缩放
zw:屏幕缩放倒数 | +| `cc_nativeSize` | vec4 | xy:实际着色缓冲的尺寸
zw:实际着色缓冲的尺寸倒数 | +| `cc_matView` | mat4 | 视图矩阵 | +| `cc_matViewInv` | mat4 | 视图逆矩阵 | +| `cc_matProj` | mat4 | 投影矩阵 | +| `cc_matProjInv` | mat4 | 投影逆矩阵 | +| `cc_matViewProj` | mat4 | 视图投影矩阵 | +| `cc_matViewProjInv` | mat4 | 视图投影逆矩阵 | +| `cc_cameraPos` | vec4 | xyz:相机位置
w:combineSignY | +| `cc_exposure` | vec4 | x:相机曝光
y:相机曝光倒数
z:是否启用 HDR
w:HDR 转 LDR 缩放参数 | +| `cc_mainLitDir` | vec4 | xyz:主方向光源方向
w:是否启用阴影 | +| `cc_mainLitColor` | vec4 | xyz:主方向光颜色
w:主方向光强度 | +| `cc_ambientSky` | vec4 | xyz:天空颜色
w:亮度 | +| `cc_ambientGround` | vec4 | xyz:地面反射光颜色
w:环境贴图 Mipmap 等级 | + +## `cc-environment.chunk` + +| Name | Type | Info | +| :-- | :-- | :-- | +| `cc_environment` | samplerCube | IBL 环境贴图 | + +## `cc-forward-light.chunk` + +| Name | Type | Info | +| :--- | :-- | :-- | +| `cc_lightPos[LIGHTS_PER_PASS]` | vec4 | xyz:光源位置
w: 有效的光源类型0: 方向光,1: 球形光,2: 聚光灯,3: 点光源,4: 范围方向光,5: 未知 | +| `cc_lightColor[LIGHTS_PER_PASS]` | vec4 | xyz:球光颜色
w:球光强度 | +| `cc_lightSizeRangeAngle[LIGHTS_PER_PASS]` | vec4 | x:光源大小, y:光源范围, z: cos(外半角), w: 启用阴影 | +| `cc_lightDir[LIGHTS_PER_PASS]` | vec4 | xyz: 方向, w: 未使用 | +| `cc_lightBoundingSizeVS[LIGHTS_PER_PASS]` | vec4 | xyz: 范围方向光节点半比例 | + +## `cc-shadow.chunk` + +| Name | Type | Info | +| :-- | :-- | :-- | +| `cc_matLightPlaneProj` | mat4 | 平面阴影的变换矩阵 | +| `cc_shadowColor` | vec4 | 阴影颜色 | + +完整常量列表,请参考 **internal/builtin/uniforms/** 目录。 diff --git a/versions/4.0/zh/shader/vscode-plugin.md b/versions/4.0/zh/shader/vscode-plugin.md new file mode 100644 index 0000000000..9dbd3f7ae1 --- /dev/null +++ b/versions/4.0/zh/shader/vscode-plugin.md @@ -0,0 +1,37 @@ +# VSCode 着色器插件(实验性) + +在 VSCode 应用商店中搜索扩展 Cocos Effect 并安装,即可启用 .effect 文件的简易语法着色。 + +如果打开的 .effect 是 3.7.3 及之后版本引擎中的内置文件,或使用此版本编辑器打开过的工程中的文件,会自动新增如下功能: + +## 1、自动补全和语法着色 + +引擎内置函数、宏和全局变量的自动补全、语法着色等功能。 + +输入关键字会有列表提示,按上下键选择,同时会显示提示信息,按回车可以自动补全 + +鼠标移动到补全后的关键字上会有悬停提示信息,包括注释和所在文件位置等 + +> **注意**:由于是实验性功能,此功能目前仅支持部分引擎内置常用代码的感应,不支持用户代码自动感应。 + +## 2、语法检查及错误跳转 + +当前窗口打开一个 .effect 文件,然后在查看菜单--命令面板(Ctrl+Shift+P)....选择**Cocos Effect: compile effect**即可调用工具自动编译当前文件,并给出对应的错误提示,**在提示窗口红色错误的文件路径上按住 Ctrl 点击鼠标左键即可跳转到对应代码行**。 + +在此项右侧有一个小的齿轮图标,点击还可以添加快捷键绑定,此后只要在 .effect 文件中按快捷键即可调用语法检查器,完整操作流程见下图。 + +> **注意**:由于是实验性功能,此功能目前只支持 Windows 系统。 + +![](img/compile-binding.gif) + +## 3、其他设置 + +插件会自动根据打开的 .effect / .chunk 文件所在位置来查找引擎对应的位置和相应的补全提示信息,但只限引擎内置文件或被新版编辑器打开过的工程中的文件。 + +如果打开的文件是在外部文件夹或老版引擎及工程中,上述新增功能就会失效,此时可以下载一个新版本引擎,然后在插件设置中指定新版编辑器或引擎的文件夹,这样就能够支持所有的 .effect 了。 + +操作步骤: + +1. 文件菜单--首选项--设置 +2. 输入 cocos +3. Cocos-effect: Engine Path中填入新版引擎或编辑器所在的文件夹即可(会自动识别) diff --git a/versions/4.0/zh/shader/webgl-100-fallback.md b/versions/4.0/zh/shader/webgl-100-fallback.md new file mode 100644 index 0000000000..53463591a1 --- /dev/null +++ b/versions/4.0/zh/shader/webgl-100-fallback.md @@ -0,0 +1,19 @@ +# WebGL 1.0 向下兼容支持 + +由于 WebGL 1.0 仅支持 GLSL 100 标准语法,因此在 Cocos Shader 编译时会提供 GLSL 300 ES 转 GLSL 100 的向下兼容代码(fallback shader),开发者基本不需关心这层变化。 + +需要注意的是目前的自动向下兼容策略仅支持一些基本的格式转换,如果使用了 GLSL 300 ES 独有的函数(例如 `texelFetch`、`textureGrad`)或一些特有的扩展(`extensions`),推荐根据 `__VERSION__` 宏定义判断 GLSL 版本,自行实现更稳定精确的向下兼容,代码示例如下: + +```glsl +#if __VERSION__ < 300 +#ifdef GL_EXT_shader_texture_lod + vec4 color = textureCubeLodEXT(envmap, R, roughness); +#else + vec4 color = textureCube(envmap, R); +#endif +#else + vec4 color = textureLod(envmap, R, roughness); +#endif +``` + +Cocos Shader 在编译时会解析所有已经是常量的宏控制流,生成不同版本的 GLSL Shader 代码。 diff --git a/versions/4.0/zh/shader/write-effect-2d-sprite-gradient.md b/versions/4.0/zh/shader/write-effect-2d-sprite-gradient.md new file mode 100644 index 0000000000..485e8d7ead --- /dev/null +++ b/versions/4.0/zh/shader/write-effect-2d-sprite-gradient.md @@ -0,0 +1,265 @@ +# 2D 精灵着色器:Gradient + +默认的情况下,UI 和 2D 组件会使用引擎内置的着色器,这些着色器放在 **资源管理器** 面板的 **internal -> effects** 目录下,可查看 [内置着色器](effect-builtin.md) 来了解其作用。 + +对于任何持有 CustomMaterial 属性的 UI 和 2D 组件,都可在 **属性检查器** 内通过该属性的下拉框选择或者从 **资源管理器** 内拖拽实现自定义材质。 + +引擎规定 UI 组件的自定义材质只能有一个。 + +![内置的 UI 着色器](../material-system/img/ui-select.png) + +> **注意**: +> 1. 在使用自定义材质后,可能会打断合批。 +> 2. 关于 UI 和 2D 组件合批的使用,可以参考:[2D 渲染组件合批规则说明](../ui-system/components/engine/ui-batch.md) + +本文将通过实现一个精灵的渐变着色器来演示如何为 UI 和 2D 组件使用自定义着色器。 + +## 搭建工程 + +通过 CocosDashBoard 创建一个新的 2D 项目。 + +创建一个新的场景并在场景内添加一个 Sprite: + +![create sprite](img/create-sprite.png) + +在 **资源管理器** 内执行以下的操作: + +- 创建名为 gradient.effect 的着色器文件 +- 拷贝 **资源管理器 -> intenal -> effects** 内的 `builtin-sprite` 着色器的内容到 gradient.effect 内 +- 创建名为 gradient.mtl 的材质并在 **属性查看器** 内的 Effect 栏选择 gradient.effect +- 导入任意的纹理 + +材质和着色器的创建可通过在 **资源管理器** 内任意空白处点击鼠标右键,或单击 **资源管理** 上的 `+` 按钮 + +![create-mtl-effect](img/2d-create-guild.png) + +创建好的工程如图示: + +![create asset](img/create-asset.png) + +选中创建好的精灵,将 gradient.mtl 材质和导入的纹理分别赋予给精灵的对应属性: + +![custom-material](img/custom-material.png) + +## CCEffect + +通过观察可得知,渐变可以理解为在某个轴上,随着坐标变化,颜色发生变化的现象。因此在 CCEffect 段内给着色器的 properties 增加两个颜色属性分别代表渐变的起始颜色和结束颜色。 + +```yaml +startColor: { value: [1.0, 1.0, 1.0, 1.0], # gradientA 的 RGBA 通道的默认值 + editor: {type: color} } # 在属性查看器内展示的样式,此处展示为颜色 +endColor: { value: [1.0, 1.0, 1.0, 1.0], + editor: {type: color} }} +``` + +此时的 CCEffect: + +```yaml +CCEffect %{ + techniques: + - passes: + - vert: sprite-vs:vert + frag: sprite-fs:frag + depthStencilState: + depthTest: false + depthWrite: false + blendState: + targets: + - blend: true + blendSrc: src_alpha + blendDst: one_minus_src_alpha + blendDstAlpha: one_minus_src_alpha + rasterizerState: + cullMode: none + properties: + alphaThreshold: { value: 0.5 } + startColor: { value: [1.0, 1.0, 1.0, 1.0], editor: {type: color} } + endColor: { value: [1.0, 1.0, 1.0, 1.0], editor: {type: color} } +}% +``` + +注意这里定义了两个颜色值 `startColor` 和 `endColor`,如果要将这两个颜色正确的传入给着色器片段,则需要增加对应的 Uniform。 + +引擎规定,不允许离散使用 Uniform,因此在 `CCProgram sprite-fs` 段内添加下列的代码: + +```glsl +uniform Constant{ + vec4 startColor; + vec4 endColor; +}; +``` + +此时引擎会自动将 `properties` 内定义的属性和 `Constant` 内的 Uniform 进行关联。 + +## 顶点着色器 + +通常不用对顶点着色器做额外的处理,因此保留系统内置的 `sprite-vs`。 + +![顶点着色器](img/2d-gradient-vs.png) + +## 片元着色器 + +在默认的精灵着色器内,精灵顶点的 XY 轴和纹理坐标的 UV 是对应的,因此可考虑使用纹理坐标的变化来达成渐变,在 USE_TEXTURE 宏定义的范围内增加下列代码: + +```glsl +#if USE_TEXTURE + o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0); + #if IS_GRAY + float gray = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b; + o.r = o.g = o.b = gray; + #endif + + // 根据 UV 的变化来调整渐变色 + o.rgb *= mix(startColor, endColor, vec4(uv0.x)).rgb; +#endif +``` + +勾选原有材质上的 `USE_TEXTURE` 选项: + +![use-texture](img/check-use-texture.png) + +此时通过调整材质上的 **startColor** 和 **endColor** 则可以观察到不同的渐变: + +![material-color](img/adjust-gradient-color.png) + +精灵着色的变化: + +![preview](img/view-x-gradient.png) + +## 使用预处理宏定义 + +在上述的片元着色器内,只考虑了纹理坐标轴 U 的渐变,为了灵活性和支持更多的功能,通过预处理宏定义来实现不同轴的渐变,因此删掉下列代码: + +```glsl +o.rgb *= mix(startColor, endColor, vec4(uv0.x)).rgb; +``` + +添加下列代码: + +```glsl +#if USE_HORIZONTAL + o.rgb *= mix(startColor, endColor, vec4(uv0.x)).rgb; +#endif + +#if USE_VERTICAL + o.rgb *= mix(startColor, endColor, vec4(uv0.y)).rgb; +#endif +``` + +这里声明了 `USE_HORIZONTAL` 和 `USE_VERTICAL` 这两个预处理宏定义,分别代表了水平方向和垂直方向的渐变,可以方便地按需使用: + +![macro-preview](img/macro-preview.png) + +完整的着色器代码: + +```glsl +// Copyright (c) 2017-2020 Xiamen Yaji Software Co., Ltd. +CCEffect %{ + techniques: + - passes: + - vert: sprite-vs:vert + frag: sprite-fs:frag + depthStencilState: + depthTest: false + depthWrite: false + blendState: + targets: + - blend: true + blendSrc: src_alpha + blendDst: one_minus_src_alpha + blendDstAlpha: one_minus_src_alpha + rasterizerState: + cullMode: none + properties: + alphaThreshold: { value: 0.5 } + startColor: { value: [1.0, 1.0, 1.0, 1.0], editor: {type: color} } + endColor: { value: [1.0, 1.0, 1.0, 1.0], editor: {type: color} } +}% + +CCProgram sprite-vs %{ + precision highp float; + #include + #if USE_LOCAL + #include + #endif + #if SAMPLE_FROM_RT + #include + #endif + in vec3 a_position; + in vec2 a_texCoord; + in vec4 a_color; + + out vec4 color; + out vec2 uv0; + + vec4 vert () { + vec4 pos = vec4(a_position, 1); + + #if USE_LOCAL + pos = cc_matWorld * pos; + #endif + + #if USE_PIXEL_ALIGNMENT + pos = cc_matView * pos; + pos.xyz = floor(pos.xyz); + pos = cc_matProj * pos; + #else + pos = cc_matViewProj * pos; + #endif + + uv0 = a_texCoord; + #if SAMPLE_FROM_RT + CC_HANDLE_RT_SAMPLE_FLIP(uv0); + #endif + color = a_color; + + return pos; + } +}% + +CCProgram sprite-fs %{ + precision highp float; + #include + #include + + in vec4 color; + + uniform Constant{ + vec4 startColor; + vec4 endColor; + }; + + #if USE_TEXTURE + in vec2 uv0; + #pragma builtin(local) + layout(set = 2, binding = 11) uniform sampler2D cc_spriteTexture; + #endif + + vec4 frag () { + vec4 o = vec4(1, 1, 1, 1); + + #if USE_TEXTURE + o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0); + #if IS_GRAY + float gray = 0.2126 * o.r + 0.7152 * o.g + 0.0722 * o.b; + o.r = o.g = o.b = gray; + #endif + + #if USE_HORIZONTAL + o.rgb *= mix(startColor, endColor, vec4(uv0.x)).rgb; + #endif + + #if USE_VERTICAL + o.rgb *= mix(startColor, endColor, vec4(uv0.y)).rgb; + #endif + + #endif + + o *= color; + + ALPHA_TEST(o); + return o; + } +}% + +``` diff --git a/versions/4.0/zh/shader/write-effect-3d-rim-light.md b/versions/4.0/zh/shader/write-effect-3d-rim-light.md new file mode 100644 index 0000000000..15614b82fc --- /dev/null +++ b/versions/4.0/zh/shader/write-effect-3d-rim-light.md @@ -0,0 +1,443 @@ +# 3D 着色器:RimLight + +本文将通过实现一个 RimLight 效果,来演示如何编写一个在 Cocos 可用于 3D 模型渲染的 Cocos Shader。 + +**RimLight** 也称为“内发光”/“轮廓光”/“边缘光”(本文统一使用边缘光),是一种通过使物体的边缘发出高亮,让物体更加生动的技术。 + +**RimLight** 是菲涅尔现象[^1]的一种应用,通过计算物体法线和视角方向的夹角的大小,调整发光的位置和颜色,是一种简单且高效的提升渲染效果的着色器。 + +在边缘光的计算中,视线和法线的夹角越大,则边缘光越明显。 + +![rimlight preview](img/rim-preview.jpg) + +## 新建材质与着色器 + +首先参考 [着色器资源](./effect-inspector.md) 新建一个名为 **rimlight.effect** 的着色器,并创建一个使用该着色器的材质 **rimlight.mtl**。 + +![create rimlight](img/rim-light-effect.png) + +## CCEffect + +Cocos Shader 使用 YAML 作为解析器,因此 CCEffect 的写法需要遵守 YAML 的语法标准,对于这块的内容可以参考 [YAML 101](./yaml-101.md)。 + +在本示例中,将暂时不考虑半透明效果,此时可以将 `transparent` 部分删掉。 + +```yaml +# 删除如下的部分 +- name: transparent + passes: + - vert: general-vs:vert # builtin header + frag: rimlight-fs:frag + blendState: + targets: + - blend: true + blendSrc: src_alpha + blendDst: one_minus_src_alpha + blendSrcAlpha: src_alpha + blendDstAlpha: one_minus_src_alpha + properties: *props +``` + +将 `opaque` 部分的 `frag` 函数修改为: `rimlight-fs:frag`,这是接下来要实现的边缘光的片元着色器部分。 + +```yaml +- name: opaque + passes: + - vert: general-vs:vert # builtin header + frag: rimlight-fs:frag +``` + +为了方便调整边缘光的颜色,增加一个用于调整边缘光颜色的属性 `rimLightColor`,由于不考虑半透明,只使用该颜色的 RGB 通道: + +```yaml +rimLightColor: { value: [1.0, 1.0, 1.0], # RGB 的默认值 + target: rimColor.rgb, # 绑定到 Uniform rimColor 的 RGB 通道上 + editor: { # 在 material 的属性检查器内的样式定义 + displayName: Rim Color, # 显示 Rim Color 作为显示名称 + type: color } } # 该字段的类型为颜色值 +``` + +此时的 CCEffect 代码: + +```yaml +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: general-vs:vert # builtin header + frag: rimlight-fs:frag + properties: &props + mainTexture: { value: white } + mainColor: { value: [1, 1, 1, 1], editor: { type: color } } + # Rim Light 的颜色,只依赖 RGB 三个通道的分量 + rimLightColor: { value: [1.0, 1.0, 1.0], target: rimColor.rgb, editor: { displayName: Rim Color, type: color } } +}% +``` + +> **注意**:需要在片元着色器的 `uniform Constant` 内增加对应的 `rimColor` 字段: +> +> ```glsl +> uniform Constant { +> vec4 mainColor; +> vec4 rimColor; +> }; +> ``` + +这个绑定意味着着色器的 `rimLightColor` 的 RGB 分量的值会通过引擎传输到 Uniform `rimColor` 的 `rgb` 三个分量里。 + +> **注意**:引擎规定不能使用 vec3 类型的矢量来避免 [implict padding](./effect-syntax.md),因此在使用 3 维向量(vec3)时,可选择用 4 维向量(vec4)代替。不用担心,alpha 通道会被利用起来不被浪费。 + +## 顶点着色器 + +通常引擎内置的顶点着色器可以满足大部分的开发需求,因此可以直接采用引擎内置的通用顶点着色器: + +```yaml + - vert: general-vs:vert # builtin header +``` + +## 片元着色器 + +修改通过 **资源管理器** 创建的着色器中的片元着色器代码,将 `CCProgram unlit-fs` 修改为 `CCProgram rimlight-fs`。 + +修改前: + +```glsl +CCProgram unlit-fs %{ + precision highp float; + ... +}% +``` + +修改后: + +```glsl +CCProgram rimlight-fs %{ + precision highp float; + ... +}% +``` + +在光照计算中,通常都需要计算法线和视线的夹角,而视线的计算和 **摄像机位置** 紧密相关。 + +![view-direction](img/view-direction.png) + +如上图所示,如果要计算视线,需要通过 **摄像机位置** 减去 **物体的位置**。在着色器内,想要获取 **摄像机位置**,需要使用 [全局 Uniform](uniform.md) 中的 `cc_cameraPos`,该变量存放于 `cc-global` 着色器片段内。通过 [`include`](./effect-chunk-index.md) 关键字,可以方便的引入整个着色器片段。 + +```glsl +#include // 包含 Cocos Creator 内置全局变量 +``` + +着色器代码: + +```glsl +CCProgram rimlight-fs %{ + precision highp float; + #include // 包含 Cocos Creator 内置全局变量 + #include + #include + + ... +} +``` + +视线方向的计算是通过当前相机的位置(`cc_cameraPos`)减去片元着色器内由顶点着色器传入的位置信息 `in vec3 v_position`: + +```glsl +vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向 +``` + +我们不关心视线向量的长度,因此得到 `viewDirection` 后,通过 `normalize` 方法进行归一化处理: + +```glsl +vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化 +``` + +`cc_cameraPos` 的 xyz 分量表示了相机的位置。 + +此时的片元着色器代码: + +```glsl + vec4 frag(){ + vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向 + vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化 + vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色 + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); + } +``` + +接下来需要计算法线和视角的夹角,由于使用的是内置标准顶点着色器 `general-vs:vert`,法线已由顶点着色器传入到片元着色器,但是没有被声明,若要在片元着色器里面使用,只需在代码中增加: + +```glsl +in vec3 v_normal; +``` + +此时的片元着色器: + +```glsl +CCProgram rimlight-fs %{ + precision highp float; + #include + #include + #include + + in vec2 v_uv; + in vec3 v_normal; + in vec3 v_position; + + .... +} +``` + +法线由于管线的插值,不再处于归一化状态,因此需要对法线进行归一化处理,使用 `normalize` 函数进行归一化: + +```glsl +vec3 normal = normalize(v_normal); // 重新归一化法线。 +``` + +此时的 `frag` 函数: + +```glsl +vec4 frag(){ + vec3 normal = normalize(v_normal); // 重新归一化法线。 + vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向 + vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化 + vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色 + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); +} +``` + +这时可计算法线和视角的夹角,在线性代数里面,点积表示为两个向量的模乘以夹角的余弦值: + +```txt +a·b = |a|*|b|*cos(θ) +``` + +通过简单的交换律可得出: + +```txt +cos(θ) = a·b /(|a|*|b|) +``` + +由于法线和视角方向都已经归一化,因此他们的模为 1,点积的结果则表示为法线和视角的 cos 值。 + +```txt +cos(θ) = a·b +``` + +将其转化为代码: + +```glsl +dot(normal, normalizedViewDirection) +``` + +注意点积的计算可能会出现小于 0 的情况,而颜色是正值,通过 `max` 函数将其约束在 [0, 1] 这个范围内: + +```glsl +max(dot(normal, normalizedViewDirection), 0.0) +``` + +此时可根据点积的结果来调整 RimLight 的颜色: + +```glsl +float rimPower = max(dot(normal, normalizedViewDirection), 0.0);// 计算 RimLight 的亮度 +vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色 +col.rgb += rimPower * rimColor.rgb; // 增加边缘光 +``` + +着色器代码如下: + +```glsl +vec4 frag(){ + vec3 normal = normalize(v_normal);// 重新归一化法线。 + vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向 + vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化 + float rimPower = max(dot(normal, normalizedViewDirection), 0.0); // 计算 RimLight 的亮度 + vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色 + col.rgb += rimPower * rimColor.rgb; // 增加边缘光 + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); +} +``` + +可观察到物体中心比边缘更亮,这是因为边缘顶点的法线和视角的夹角更大,得到的余弦值更小。 + +> **注意**:此步骤若无法观察到效果,可调整 `MainColor` 使其不为白色。因为默认的 `MainColor` 颜色是白色,遮盖了边缘光的颜色。 + +![dot result](img/dot.jpg) + +要调整这个结果,只需用 1 减去点积的结果即可,删除下面的代码: + +~~``` float rimPower = max(dot(normal, normalizedViewDirection), 0.0); ```~~ + +并增加: + +```glsl +float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0); +``` + +片元着色器代码: + +```glsl +vec4 frag(){ + vec3 normal = normalize(v_normal); // 重新归一化法线。 + vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向 + vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化 + float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0); + vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色 + col.rgb += rimPower * rimColor.rgb; // 增加边缘光 + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); +} +``` + +![one minus dot result](img/1-dot.jpg) + +虽然可观察到边缘光效果,但是光照太强,并且不方便调整,可在着色器的 CCEffect 段内增加一个可调整的参数 rimIntensity。由于之前 rimColor 的 alpha 分量没有被使用到,因此借用该分量进行绑定可节约额外的 Uniform。 + +> **注意**:写着色器时,需要避免 implict padding,关于这点可以参考: [UBO 内存布局](./effect-syntax.md),这里使用未被使用的 alpha 通道来存储边缘光的强度可以最大限度的利用 `rimColor` 的字段。 + +在 CCEffect 内增加如下代码: + +```yaml +rimInstensity: { value: 1.0, # 默认值为 1 + target: rimColor.a, # 绑定到 rimColor 的 alpha 通道 + editor: { # 属性检查器的样式 + slide: true, # 使用滑动条来作为显示样式 + range: [0, 10], # 滑动条的值范围 + step: 0.1 } # 每次点击调整按钮时,数值的变化值 +``` + +此时的 CCEffect 代码: + +```yaml +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: general-vs:vert # builtin header + frag: rimlight-fs:frag + properties: &props + mainTexture: { value: white } + mainColor: { value: [1, 1, 1, 1], editor: { type: color } } + # Rim Light 的颜色,只依赖 rgb 三个通道的分量 + rimLightColor: { value: [1.0, 1.0, 1.0], target: rimColor.rgb, editor: { displayName: Rim Color, type: color } } + # rimLightColor 的 alpha 通道没有被用到,复用该通道用来描述 rimLightColor 的强度。 + rimInstensity: { value: 1.0, target: rimColor.a, editor: {slide: true, range: [0, 10], step: 0.1}} +}% + +``` + +增加此属性后,材质 **属性检查器** 上会增加可调整的 RimIntensity: + +![intensity](./img/add-intensity.png) + +通过 `pow` 函数调整边缘光,使其范围不是线性变化,可体现更好的效果,删除如下代码: + +~~``` col.rgb += rimPower * rimColor.rgb; ```~~ + +新增下列代码: + +```glsl +float rimInstensity = rimColor.a; // alpha 通道为亮度的指数 +col.rgb += pow(rimPower, rimInstensity) * rimColor.rgb; // 使用 ‘pow’ 函数对点积进行指数级修改 +``` + +`pow` 是 GLSL 的内置函数,其形式为:`pow(x, p)`,代表以 `x` 为底数,`p` 为指数的指数函数。 + +最终片元着色器代码: + +```glsl + vec4 frag(){ + vec3 normal = normalize(v_normal); // 重新归一化法线。 + vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向 + vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化 + float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0);// 计算 RimLight 的亮度 + vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色 + float rimInstensity = rimColor.a; // alpha 通道为亮度的指数 + col.rgb += pow(rimPower, rimInstensity) * rimColor.rgb; // 增加边缘光 + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); + } +``` + +之后将材质 **属性检查器** 上的 **rimIntensity** 的值修改为 3: + +![设置 intensity](img/intensity.png) + +此时可观察到边缘光照更自然: + +![增加亮度调整后](img/preview-instensity.jpg) + +通过 **Rim Color** 和 **rimIntensity** 可方便的调整边缘光的颜色和强度: + +![调整颜色值](img/adjust-option.png) + +![调整颜色结果](img/opt-overview.jpg) + +完整的着色器代码: + +```glsl +CCEffect %{ + techniques: + - name: opaque + passes: + - vert: legacy/main-functions/general-vs:vert # builtin header + frag: rimlight-fs:frag + properties: &props + mainTexture: { value: white } + mainColor: { value: [1, 1, 1, 1], editor: { type: color } } + # Rim Light 的颜色,只依赖 rgb 三个通道的分量 + rimLightColor: { value: [1.0, 1.0, 1.0], target: rimColor.rgb, editor: { displayName: Rim Color, type: color } } + # rimLightColor 的 alpha 通道没有被用到,复用该通道用来描述 rimLightColor 的强度。 + rimInstensity: { value: 1.0, target: rimColor.a, editor: {slide: true, range: [0, 10], step: 0.1}} +}% + +CCProgram rimlight-fs %{ + precision highp float; + #include + #include + #include + + in vec2 v_uv; + in vec3 v_normal; + in vec3 v_position; + + uniform sampler2D mainTexture; + + uniform Constant { + vec4 mainColor; + vec4 rimColor; + }; + vec4 frag(){ + vec3 normal = normalize(v_normal); // 重新归一化法线。 + vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向 + vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化 + float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0);// 计算 RimLight 的亮度 + vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色 + float rimInstensity = rimColor.a; // alpha 通道为亮度的指数 + col.rgb += pow(rimPower, rimInstensity) * rimColor.rgb; // 增加边缘光 + CC_APPLY_FOG(col, v_position); + return CCFragOutput(col); + } +}% +``` + +若要让边缘光的颜色受纹理颜色的影响,可将下列代码: + +```glsl +col.rgb += pow(rimPower, rimInstensity) * rimColor.rgb; // 增加边缘光 +``` + +改为: + +```glsl +col.rgb *= 1.0 + pow(rimPower, rimInstensity) * rimColor.rgb; // 边缘光受物体着色的影响 +``` + +此时的边缘光则会受到最终纹理和顶点颜色的影响: + +![color](img/effect-by-color.jpg) + +[^1]: 菲涅尔现象:奥古斯丁·让·菲涅耳是 18 世纪法国著名的物理学家,他提出的菲涅尔方程很好的解释了光线的反射和折射的关系。如果去观察阳光照射下的平静水面可以发现,距离观察点越远的水面反射越强烈,这种光线强度随着观察角度变化而变化的现象,被称为菲涅尔现象。![fresnel](img/fresnel.jpg) \ No newline at end of file diff --git a/versions/4.0/zh/shader/write-effect-overview.md b/versions/4.0/zh/shader/write-effect-overview.md new file mode 100644 index 0000000000..71f592fa21 --- /dev/null +++ b/versions/4.0/zh/shader/write-effect-overview.md @@ -0,0 +1,23 @@ +# 自定义着色器 + +若内置的着色器无法满足需求,可通过自定义着色器实现特例化渲染。 + +自定义着色器有以下两种方式: + +1. 参考 [着色器资源](./effect-inspector.md) 创建新的着色器。 + +2. 基于内置着色器。将 **资源管理器** 面板中 **internal/effects/** 目录下相应的内置着色器拷贝到 **assets** 目录下,然后对其进行自定义。 + +## 准备工作 + +由于着色器是使用 YAML 作为流程控制,GLSL 作为着色器语言,因此在自定义着色器之前需要对这些知识有一定程度上的熟悉和了解: + +对于不熟悉的开发者,我们也准备了一些简单的介绍: + +- [GLSL 语法简介](./glsl.md) +- [YAML 语法简介](./yaml-101.md) + +本节将以自定义 2D 着色器和 3D 着色器为例,详细介绍自定义流程,具体内容请参考: + +- [3D 着色器:RimLight](write-effect-3d-rim-light.md) +- [2D 着色器:Gradient](write-effect-2d-sprite-gradient.md) \ No newline at end of file diff --git a/versions/4.0/zh/shader/yaml-101.md b/versions/4.0/zh/shader/yaml-101.md new file mode 100644 index 0000000000..053f2255c0 --- /dev/null +++ b/versions/4.0/zh/shader/yaml-101.md @@ -0,0 +1,151 @@ +# YAML 101 + +Cocos Shader 使用的是符合 YAML 1.2 标准的解析器,这意味着 Cocos Shader 与 JSON 是完全兼容的,直接使用 JSON 也完全不会有问题,例如: + +```yaml +"techniques": + [{ + "passes": + [{ + "vert": "skybox-vs", + "frag": "skybox-fs", + "rasterizerState": + { + "cullMode": "none" + } + # ... + }] + }] +``` + +当然这也意味着繁琐的语法,所以 YAML 提供了一些更简洁的数据表示方式: + +- 所有的引号和逗号都可以省略 + + ```yaml + key1: 1 + key2: unquoted string + ``` + + **注意**:冒号后的空格不可省略 + +- 行首的空格缩进数量代表数据的层级[^1] + + ```yaml + object1: + key1: false + object2: + key2: 3.14 + key3: 0xdeadbeef + nestedObject: + key4: 'quoted string' + ``` + +- 以 **连字符** - **空格** 开头,表示数组元素 + + ```yaml + - 42 + - "double-quoted string" + - arrayElement3: + key1: punctuations? sure. + key2: you can even have {}s as long as they are not the first character + key3: { nested1: 'but no unquoted string allowed inside brackets', nested2: 'also notice the comma is back too' } + ``` + +综合以上几点,本文开头的内容就可以简化成下面这样: + +```yaml +techniques: +- passes: + - vert: skybox-vs + frag: skybox-fs + rasterizerState: + cullMode: none + # ... +``` + +另一个非常有用的 YAML 特性是数据间的引用与继承。 + +- **引用** + + ```yaml + object1: &o1 + key1: value1 + object2: + key2: value2 + key3: *o1 + ``` + + 这个数据解析出来是这样的: + + ```json + { + "object1": { + "key1": "value1" + }, + "object2": { + "key2": "value2", + "key3": { + "key1": "value1" + } + } + } + ``` + +- **继承** + + ```yaml + object1: &o1 + key1: value1 + key2: value2 + object2: + <<: *o1 + key3: value3 + ``` + + 这个数据解析出来是这样的: + + ```json + { + "object1": { + "key1": "value1", + "key2": "value2" + }, + "object2": { + "key1": "value1", + "key2": "value2", + "key3": "value3" + } + } + ``` + +对应到 Cocos Shader 中,比如多个 Pass 拥有相同的 `properties` 内容时(或更多其他需要复用数据的情况),可以很方便地复用数据,例如: + +```yaml +techniques: +- passes: + - # pass 1 specifications... + properties: &props # declare once... + p1: { value: [ 1, 1, 1, 1 ] } + p2: { sampler: { mipFilter: linear } } + p3: { inspector: { type: color } } + - # pass 2 specifications... + properties: *props # reference anywhere +``` + +最后需要注意的是:在 Cocos Shader 文件中使用 YAML 声明的所有流程,都要确保是在 **CCEffect** 语法块内: + +```yaml +CCEffect %{ + # YAML starts here +}% +``` + +若有疑问可复制代码示例到任何 [在线 YAML JSON 转换器](https://codebeautify.org/yaml-to-json-xml-csv) 观察生成的数据。 + +## 参考链接 + +- +- + +[^1]: 标准 YAML 并不支持制表符,但在解析 Cocos Shader 内容时,我们会先尝试把其中所有的制表符替换为 2 个空格,以避免偶然插入制表符带来的麻烦。但请尽量避免插入制表符以确保编译无误。 diff --git a/versions/4.0/zh/styles/website.css b/versions/4.0/zh/styles/website.css new file mode 100644 index 0000000000..c45e2c5327 --- /dev/null +++ b/versions/4.0/zh/styles/website.css @@ -0,0 +1,187 @@ +.navigation { + top: 90px; +} +.book-summary nav.autoshow a, +.book-summary nav.autoshow a:hover { + color: inherit; + text-decoration: none; +} +@media (min-width: 800px) { + .book-summary nav.autoshow { + display: none; + } +} +.cocos-navbar { + overflow: visible; + width: 100%; + min-width: 800px; + z-index: 2; + font-size: 0.85em; + color: #7e888b; + background: #f5f5f5; + display: flex; + position: absolute; + top: 0; +} +.cocos-navbar::after { + clear: both; + content: '.'; + display: block; + visibility: hidden; + height: 0; +} +.cocos-navbar a, +.cocos-navbar a:hover { + color: inherit; + text-decoration: none; +} +.cocos-navbar ul { + list-style-type: none; + margin: 0; + padding: 0; + width: 100%; +} +.cocos-navbar ul li { + margin-right: 1px; + position: relative; + float: left; +} +.cocos-navbar ul li a:hover + .hovershow, +.cocos-navbar ul li .hovershow:hover, +.cocos-navbar ul li .hovershow li:hover { + display: block; +} +.cocos-navbar ul li img { + margin-top: 3px; +} +.cocos-navbar ul li ul { + margin: -3px 0 0 0; + background: #f5f5f5; + display: none; + width: 100%; + z-index: 999; + position: absolute; +} +.cocos-navbar ul li ul li { + display: block; + float: none; +} +.cocos-navbar ul li ul li a { + width: auto; + min-width: 100px; + padding: 0 20px; +} +@media screen and (max-width: 800px) { + .cocos-navbar ul li { + /*Make dropdown links appear inline*/ + /*Create vertical spacing*/ + /*Make all menu links full width*/ + } + .cocos-navbar ul li ul { + position: static; + display: none; + } + .cocos-navbar ul li li { + margin-bottom: 1px; + } + .cocos-navbar ul li ul li, + .cocos-navbar ul li li a { + width: 100%; + } +} +.cocos-navbar .btn { + display: inline-block; + height: 40px; + padding: 0px 15px; + border-bottom: none; + color: #aaa; + text-transform: none; + line-height: 40px; + position: relative; + font-size: 14px; +} +.cocos-navbar .btn:hover { + position: relative; + text-decoration: none; + color: #444; + background: none; +} +.cocos-navbar .btn:focus { + outline: none; +} +.cocos-navbar:hover h1 { + opacity: 1; +} +@media (max-width: 800px) { + .cocos-navbar { + display: none; + } +} +@media (min-width: 800px) { + .page-wrapper { + min-width: 800px; + padding-top: 40px; + } +} +@media (max-width: 800px) { + .page-wrapper { + min-width: 800px; + padding-top: 0; + } +} +@media (min-width: 800px) and (max-width: 1000px) { + .cocos-navbar ul .version-link ul { + margin-left: 20px; + } + .cocos-navbar .nav { + list-style-type: none; + margin: 0; + padding: 0; + width: 100%; + } +} +@media (max-width: 1240px) { + .book-body .body-inner { + overflow: visible; + } +} +.cocos-navbar ul .version-link { + float: right; + z-index: 10; + padding-right: 15px; + text-align: right; + min-width: 180px; + display: flex; + align-items: center; +} +.version-link > ul { + top: 100%; + right: 0; + width: auto !important; + min-width: 180px; +} +.version-link ul li .btn { + padding-right: 35px; + padding-left: 0; +} +.version-link a span { + padding: 0; +} + +.version-badge { + display: inline-flex; + align-items: center; + + z-index: 10; + + color: #0C090A; + background-color: #cccccc; + + height: 20px; + padding: 0 8px; + border-radius: 20px; +} + +.book-header { + min-width: 800px; +} diff --git a/versions/4.0/zh/styles/website.less b/versions/4.0/zh/styles/website.less new file mode 100644 index 0000000000..223342511d --- /dev/null +++ b/versions/4.0/zh/styles/website.less @@ -0,0 +1,131 @@ +// Header +@header-height: 40px; +@header-color: hsl(194, 5%, 52%); +@header-background: #f5f5f5; +@header-border: rgba(0, 0, 0, 0.07); +@header-button-color: #aaa; +@header-button-hover-color: #444; +@header-button-hover-background: none; +// Font sizes +@font-size-base: 14px; +@font-size-large: ceil(@font-size-base * 1.25); // ~18px +@font-size-small: ceil(@font-size-base * 0.85); // ~12px +@line-height-base: 1.428571429; // 20/14 +@line-height-computed: floor(@font-size-base * @line-height-base); + +.navigation { + top: 90px; +} + +.book-summary nav.autoshow { + a, a:hover { + color: inherit; + text-decoration: none; + } + @media (min-width: 800px) { + display: none; + } +} + +.cocos-navbar { + overflow: visible; + height: @header-height; + padding: 0px 8px; + z-index: 2; + + font-size: 0.85em; + color: @header-color; + background: @header-background; + + display: flex; + + a, a:hover { + color: inherit; + text-decoration: none; + } + + ul { + list-style-type: none; + margin: 0; + padding: 0; + position: absolute; + li { + margin-right: 1px; + position: relative; + float: left; + a:hover + .hovershow, .hovershow:hover, .hovershow li:hover { + display:block; + } + img { + margin-top: 3px; + } + ul { + margin: -3px 0 0 0; + background: #f5f5f5; + display:none; + width: 100%; + z-index:999; + li { + display: block; + float: none; + a { + width: auto; + min-width: 100px; + padding: 0 20px; + + } + } + } + @media screen and (max-width: 800px){ + /*Make dropdown links appear inline*/ + ul { + position: static; + display: none; + } + /*Create vertical spacing*/ + li { + margin-bottom: 1px; + } + /*Make all menu links full width*/ + ul li, li a { + width: 100%; + } + } + } + } + + .btn { + display: inline-block; + height: @header-height; + padding: 0px 15px; + border-bottom: none; + color: @header-button-color; + text-transform: none; + line-height: @header-height; + position:relative; + font-size: @font-size-base; + + &:hover { + position: relative; + text-decoration: none; + color: @header-button-hover-color; + background: @header-button-hover-background; + } + + &:focus { + outline: none; + } + } + + .autohide{ + @media (max-width: 800px) { + display: none; + } + } + + &:hover { + h1 { + opacity: 1; + } + } +} \ No newline at end of file diff --git a/versions/4.0/zh/submit-pr/submit-pr.md b/versions/4.0/zh/submit-pr/submit-pr.md new file mode 100644 index 0000000000..c6876a1f70 --- /dev/null +++ b/versions/4.0/zh/submit-pr/submit-pr.md @@ -0,0 +1,130 @@ +# 如何向 Cocos 提交代码 + +Cocos Creator 是一个开源引擎,连同范例、文档都是开源的。
+在你开发游戏的过程中,当发现了引擎、文档或者范例不够完善的地方,如果仅仅是向官方团队提出建议,官方团队可能会因为人力资源的紧张而无法及时跟进。在此我们欢迎所有用户主动向我们提交 PR,帮助 Cocos 越做越好。引擎有 Bug?提 PR!范例难看?提 PR!API 注释不清晰?提 PR!文档有错别字?提 PR!想要把你的宝贵修改贡献给游戏社区?提 PR!以下几个是目前官方比较常用的开源仓库,这些仓库都可以提交 PR。 + +- **Cocos 引擎官方仓库**:[GitHub](https://github.com/cocos/cocos4/) | [Gitee](https://gitee.com/mirrors_cocos-creator/engine/) + +下面让我们来看一下,如何从零开始在 GitHub 上向 Cocos 提交代码。 + +## 注册一个 GitHub 账号 + +打开 [GitHub 网站](https://github.com/) 注册账号。若之前已经有注册过,那直接登录就可以了。 + +## 环境配置 + +### 安装 Git + +首先先确认电脑是否已经安装 Git,命令行输入 `git`,安装过则会输出以下内容: + +![git](submit-pr/git.png) + +未安装过则 [下载 Git](https://git-scm.com/download/) 并安装。安装过程中的所有选项保留默认就可以了,一直点 next,直到安装完成。 + +### 安装 Git 客户端 —— GitKraken + +GitKraken 是比较常用的 Git 客户端工具。如果不使用 GitKraken 的话,Git 操作全部要通过命令行操作完成,比较麻烦。下面以 Windows 版本为例进行演示。 + +首先需要下载 [GitKraken](https://www.gitkraken.com/) 并解压缩,进行安装。 + +![GitKraken install](submit-pr/sourcetree_install.png) + +安装完成后界面如下图所示: + +![GitKraken install complete](submit-pr/sourcetree_install_complete.png) + +## Fork 项目 + +以手册文档的代码仓库 **creator-docs**([GitHub](https://github.com/cocos/cocos-docs) | [Gitee](https://gitee.com/mirrors_cocos-creator/creator-docs/))为例。进入手册文档仓库页面,点击右上角的 Fork 按钮,如下图所示: + +![fork](submit-pr/fork.png) + +Fork 完成后,会自动跳转到你的 GitHub 仓库页面,可以看到已经生成了 docs-3d 项目副本,如下图所示: + +![repository](submit-pr/repository.png) + +## 将远程仓库克隆到本地仓库 + +1、首先需要到你的远程仓库复制 **远程仓库项目地址**,如下图所示: + +![copy](submit-pr/copy.png) + +2、切换到 GitKraken 后点击上方的 **Clone a repo** 按钮,跳转到 Clone 页面,粘贴刚才复制的 **远程仓库项目地址**,然后填入相关配置。如果想让本地的文件夹名称和项目名称一样,那么在本地存储路径后添加 `/docs-3d`。配置完成后点击 **Clone**。 + +![clone repository](submit-pr/clone_repository.png) + +克隆完成后就会在本地自动创建 docs-3d 文件夹并且在 GitKraken 上自动打开项目。 + +![clone finish](submit-pr/clone_finish.png) + +## 上传本地修改到远程仓库 + +1、检出需要的分支。在左侧的 **REMOTE** 目录下有一个 **origin** 仓库,这是你自己的远程仓库。例如要修改的分支是 next 分支,则点击 **origin** 后双击 **next** 分支。如下图所示: + +![checkout](submit-pr/checkout.png) + +**注意**:根据不同的版本,还需要切换不同的分支,例如: + +- **vX.Y** 分支:对应 X.Y 版本所用分支 +- **develop** 分支:开发分支 + +2、打开本地 docs-3d 项目进行修改,修改完成后查看仓库详情,如下图所示: + +![modification](submit-pr/modification.png) + +3、提交暂存区文件到本地仓库。将你要上传的修改文件提交到本地暂存区,然后在下方备注提交内容注释。完成之后点击 **Commit**,将暂存区文件提交到本地仓库。完成后点击 **Push**。步骤如下图所示: + +![commit changes](submit-pr/commit_changes.png) + +4、将本地仓库的修改推送到自己的远程仓库 origin。 + +![push](submit-pr/push.png) + +5、完成之后,到 GitHub 自己的 docs-3d 远程仓库查看(可以从 **右上方的头像 -> Your profile -> Repositories -> docs-3d** 进入你的远程仓库),可以看到已经有本次的提交信息了。然后点击 **New pull request**。 + +![push finish](submit-pr/push_finish.png) + +6、点击 **New pull request** 后会跳转到官方仓库的 **Open a pull request**。标题会自动填入刚才提交的信息,也可自行修改。下方的填写信息区域需要对提交内容进行适当的补充。特别是针对引擎本身的修改,请将问题描述、改动内容、涉及版本、相关平台等信息补充完整。如果有相关 Issue,或者论坛地址也可以贴上来。最下方的是本次提交的 PR 的改动详情。填写完成后点击 **Create pull request**。 + +![pull request](submit-pr/pull_request.png) + +7、创建完成后在官方仓库 docs-3d 的 pull requests 页面中可以看到创建了一个新的 PR。官方人员会收到提醒并将 PR 指派给相关人员进行 review 和合并。到此 PR 创建完成,若官方审核通过,就会把这个 PR 的修改合并到官方仓库中了。如果上方的Open图标变为Merged图标,则表示修改已合并到官网。若官方觉得有需要,也会在该 PR 上展开进一步讨论。请留意 GitHub 相关消息或关注 PR 所在页面,以免错过讨论。如果你需要修改 PR 提交的内容,请重复 **上传本地修改到远程仓库** 中的 2、3、4 步骤。 + +## 添加官方仓库 + +如果距离上次克隆仓库已经挺长时间,那么请在提交 PR 前先从官方仓库获取最新的修改以防和其他人的修改发生冲突。 + +1、添加官方仓库。点击 GitKraken 左边列表的 **REMOTE +**,如下图所示: + +![add upstream](submit-pr/add-upstream.png) + +2、在 GitKraken 的 docs-3d 本地仓库的左侧,可以看到有 origin 和 cocos-creator3D 两个远程仓库,分别是自己的远程仓库和官方仓库。可以看到 **远程 -> cocos-creator3D** 下已经有了官方仓库的各个分支,如下图所示: + +![upstream](submit-pr/upstream.png) + +3、从官方仓库拉取最新更新。切换到要拉取的分支,然后点击左上方的 **Pull**。如下图所示: + +![pull](submit-pr/pull.png) + +**注意**:在更新官方版本前,如果你完全不熟悉 Git 的操作,建议先确保没有在本地的 Git 仓库中进行任何文件的改动。如果有的话,建议先手动还原,然后等更新完毕后再手动把改动添加回来。 + +## 如何反馈文档有关的问题 + +针对文档本身的问题,建议通过 GitHub issue 进行反馈,下面我们简单演示一下。在提交问题之前请先确认: + +- 文档版本和 Creator 版本是否一致 +- 操作步骤是否正确 +- 是否是文档本身的问题,例如代码错误或者根据文档步骤执行出现异常等。 + +若以上问题都是确定的,那么,有以下两种提交方式: + +1、可点击 Creator 官方文档右下方的 **提交反馈** 按钮,进入提交 issue 界面。 + +![issue](submit-pr/issue.png) + +填写完成后点击 **Submit new issue** 提交 issue,就完成了。 + +2、或者也可以进入官方仓库 **creator-docs**([GitHub](https://github.com/cocos/cocos-docs) | [Gitee](https://gitee.com/mirrors_cocos-creator/creator-docs/)),选择 **Issue -> New issue** 进入提交 issue 界面,填写内容并提交。 + +本次提交 pr 和提交 issue 的教程到这里就结束了,若有不理解或者有误的地方请根据上述步骤向我们反馈。
+有些人可能会问,为什么要这么麻烦的提交问题。其实,正确的提交问题可以节省很多的沟通成本,而且有些问题可能在初步排查的时候就可以解决了,或者会发现只是由于自己粗心大意导致的。而官方节省下来的人力就可以更好的去服务于 Creator,让 Creator 能更好地为游戏开发者服务。 diff --git a/versions/4.0/zh/submit-pr/submit-pr/PR.png b/versions/4.0/zh/submit-pr/submit-pr/PR.png new file mode 100644 index 0000000000..789073bb52 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/PR.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/add-upstream.png b/versions/4.0/zh/submit-pr/submit-pr/add-upstream.png new file mode 100644 index 0000000000..34c1f7a178 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/add-upstream.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/add_ssh_key.png b/versions/4.0/zh/submit-pr/submit-pr/add_ssh_key.png new file mode 100644 index 0000000000..25bf9d733a Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/add_ssh_key.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/add_upstream.png b/versions/4.0/zh/submit-pr/submit-pr/add_upstream.png new file mode 100644 index 0000000000..8fb03959d0 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/add_upstream.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/check_out_next.png b/versions/4.0/zh/submit-pr/submit-pr/check_out_next.png new file mode 100644 index 0000000000..9491209123 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/check_out_next.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/checkout.png b/versions/4.0/zh/submit-pr/submit-pr/checkout.png new file mode 100644 index 0000000000..17d6d65368 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/checkout.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/clone.png b/versions/4.0/zh/submit-pr/submit-pr/clone.png new file mode 100644 index 0000000000..fbc65da927 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/clone.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/clone_copy.png b/versions/4.0/zh/submit-pr/submit-pr/clone_copy.png new file mode 100644 index 0000000000..26b19bf5ca Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/clone_copy.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/clone_finish.png b/versions/4.0/zh/submit-pr/submit-pr/clone_finish.png new file mode 100644 index 0000000000..98a7788858 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/clone_finish.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/clone_repository.png b/versions/4.0/zh/submit-pr/submit-pr/clone_repository.png new file mode 100644 index 0000000000..e3a0ad6f55 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/clone_repository.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/commit_changes.png b/versions/4.0/zh/submit-pr/submit-pr/commit_changes.png new file mode 100644 index 0000000000..93aa2d3ac0 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/commit_changes.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/copy.png b/versions/4.0/zh/submit-pr/submit-pr/copy.png new file mode 100644 index 0000000000..4a913d1630 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/copy.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/fork.png b/versions/4.0/zh/submit-pr/submit-pr/fork.png new file mode 100644 index 0000000000..5909a7b855 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/fork.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/git.png b/versions/4.0/zh/submit-pr/submit-pr/git.png new file mode 100644 index 0000000000..1d8e330d8a Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/git.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/issue.png b/versions/4.0/zh/submit-pr/submit-pr/issue.png new file mode 100644 index 0000000000..84e1f5bb0c Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/issue.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/mercurial.png b/versions/4.0/zh/submit-pr/submit-pr/mercurial.png new file mode 100644 index 0000000000..a26fb7656f Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/mercurial.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/merge.png b/versions/4.0/zh/submit-pr/submit-pr/merge.png new file mode 100644 index 0000000000..460e7fa410 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/merge.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/merge2.png b/versions/4.0/zh/submit-pr/submit-pr/merge2.png new file mode 100644 index 0000000000..770429c4b0 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/merge2.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/modification.png b/versions/4.0/zh/submit-pr/submit-pr/modification.png new file mode 100644 index 0000000000..142e972aba Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/modification.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/next.png b/versions/4.0/zh/submit-pr/submit-pr/next.png new file mode 100644 index 0000000000..029a8f2c7e Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/next.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/output.png b/versions/4.0/zh/submit-pr/submit-pr/output.png new file mode 100644 index 0000000000..0689432a06 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/output.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/paste_ssh_key.png b/versions/4.0/zh/submit-pr/submit-pr/paste_ssh_key.png new file mode 100644 index 0000000000..770653adab Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/paste_ssh_key.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/pull.png b/versions/4.0/zh/submit-pr/submit-pr/pull.png new file mode 100644 index 0000000000..f2379f2a66 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/pull.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/pull_request.png b/versions/4.0/zh/submit-pr/submit-pr/pull_request.png new file mode 100644 index 0000000000..65f4f5ada3 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/pull_request.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/pull_upstream.png b/versions/4.0/zh/submit-pr/submit-pr/pull_upstream.png new file mode 100644 index 0000000000..37780e9890 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/pull_upstream.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/push.png b/versions/4.0/zh/submit-pr/submit-pr/push.png new file mode 100644 index 0000000000..021d8b3e64 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/push.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/push_finish.png b/versions/4.0/zh/submit-pr/submit-pr/push_finish.png new file mode 100644 index 0000000000..6ecac6670b Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/push_finish.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/repository.png b/versions/4.0/zh/submit-pr/submit-pr/repository.png new file mode 100644 index 0000000000..c6471994ca Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/repository.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/setting_add.png b/versions/4.0/zh/submit-pr/submit-pr/setting_add.png new file mode 100644 index 0000000000..01df90c8ac Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/setting_add.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/setting_repository.png b/versions/4.0/zh/submit-pr/submit-pr/setting_repository.png new file mode 100644 index 0000000000..809a476364 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/setting_repository.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/sign_up_step.png b/versions/4.0/zh/submit-pr/submit-pr/sign_up_step.png new file mode 100644 index 0000000000..022492690c Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/sign_up_step.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/sourcetree.png b/versions/4.0/zh/submit-pr/submit-pr/sourcetree.png new file mode 100644 index 0000000000..ff84e7c0d7 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/sourcetree.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/sourcetree1.png b/versions/4.0/zh/submit-pr/submit-pr/sourcetree1.png new file mode 100644 index 0000000000..345db11d2c Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/sourcetree1.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/sourcetree2.png b/versions/4.0/zh/submit-pr/submit-pr/sourcetree2.png new file mode 100644 index 0000000000..4b0646cdb3 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/sourcetree2.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/sourcetree_configuration.png b/versions/4.0/zh/submit-pr/submit-pr/sourcetree_configuration.png new file mode 100644 index 0000000000..74e450f676 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/sourcetree_configuration.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/sourcetree_install.png b/versions/4.0/zh/submit-pr/submit-pr/sourcetree_install.png new file mode 100644 index 0000000000..388b93631b Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/sourcetree_install.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/sourcetree_install_complete.png b/versions/4.0/zh/submit-pr/submit-pr/sourcetree_install_complete.png new file mode 100644 index 0000000000..ebfdd364be Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/sourcetree_install_complete.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/sourcetree_verify.png b/versions/4.0/zh/submit-pr/submit-pr/sourcetree_verify.png new file mode 100644 index 0000000000..6c98b9c67d Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/sourcetree_verify.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/ssh_key.png b/versions/4.0/zh/submit-pr/submit-pr/ssh_key.png new file mode 100644 index 0000000000..f4d567a25d Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/ssh_key.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/upstream.png b/versions/4.0/zh/submit-pr/submit-pr/upstream.png new file mode 100644 index 0000000000..c7525d74bd Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/upstream.png differ diff --git a/versions/4.0/zh/submit-pr/submit-pr/verification.png b/versions/4.0/zh/submit-pr/submit-pr/verification.png new file mode 100644 index 0000000000..abec866260 Binary files /dev/null and b/versions/4.0/zh/submit-pr/submit-pr/verification.png differ diff --git a/versions/4.0/zh/summary.json b/versions/4.0/zh/summary.json new file mode 100644 index 0000000000..d8dbc399ed --- /dev/null +++ b/versions/4.0/zh/summary.json @@ -0,0 +1,2346 @@ +[ + { + "text": "用户手册 4.0 (LTS)", + "collapsed": false, + "items": [ + { + "text": "Cocos Creator 4.0 LTS", + "link": "index.md" + }, + { + "text": "关于 Cocos Creator", + "link": "getting-started/introduction/index.md" + }, + { + "text": "获取帮助和支持", + "link": "getting-started/support.md" + } + ] + }, + { + "text": "基础知识", + "collapsed": false, + "items": [ + { + "text": "新手入门", + "link": "getting-started/index.md", + "items": [ + { + "text": "安装和启动", + "link": "getting-started/install/index.md" + }, + { + "text": "使用 Dashboard", + "link": "getting-started/dashboard/index.md" + }, + { + "text": "Hello World!", + "link": "getting-started/helloworld/index.md" + }, + { + "text": "项目结构", + "link": "getting-started/project-structure/index.md" + } + ], + "collapsed": true + }, + { + "text": "编辑器界面", + "link": "editor/index.md", + "items": [ + { + "text": "场景编辑器", + "link": "editor/scene/index.md" + }, + { + "text": "层级管理器", + "link": "editor/hierarchy/index.md" + }, + { + "text": "资源管理器", + "link": "editor/assets/index.md" + }, + { + "text": "属性检查器", + "link": "editor/inspector/index.md" + }, + { + "text": "控制台", + "link": "editor/console/index.md" + }, + { + "text": "偏好设置", + "link": "editor/preferences/index.md" + }, + { + "text": "项目设置", + "link": "editor/project/index.md" + }, + { + "text": "主菜单", + "link": "editor/mainMenu/index.md" + }, + { + "text": "工具栏", + "link": "editor/toolbar/index.md" + }, + { + "text": "编辑器布局", + "link": "editor/editor-layout/index.md" + }, + { + "text": "预览调试", + "link": "editor/preview/index.md" + } + ], + "collapsed": true + }, + { + "text": "术语", + "link": "glossary/index.md" + }, + { + "text": "Unity 开发者快速入门指南", + "link": "guide/unity/index.md" + } + ] + }, + { + "text": "示例和教程", + "collapsed": false, + "items": [ + { + "text": "快速上手:制作第一个 2D 游戏", + "link": "getting-started/first-game-2d/index.md", + "items": [ + { + "text": "监听触摸事件", + "link": "getting-started/first-game-2d/touch.md" + } + ], + "collapsed": true + }, + { + "text": "快速上手:制作第一个 3D 游戏", + "link": "getting-started/first-game/index.md", + "items": [ + { + "text": "进阶篇 - 添加阴影、光照和骨骼动画", + "link": "getting-started/first-game/advance.md" + } + ], + "collapsed": true + }, + { + "text": "示例与教程", + "link": "cases-and-tutorials/index.md" + } + ] + }, + { + "text": "工作流", + "collapsed": false, + "items": [ + { + "text": "升级指南", + "link": "release-notes/index.md", + "items": [ + { + "text": "v3.0 升级指南", + "link": "release-notes/upgrade-guide-v3.0.md" + }, + { + "text": "v3.0 材质升级指南", + "link": "material-system/effect-2.x-to-3.0.md" + }, + { + "text": "v3.1 材质升级指南", + "link": "material-system/Material-upgrade-documentation-for-v3.0-to-v3.1.md" + }, + { + "text": "v3.3 动画剪辑数据升级指南", + "link": "animation/animation-clip-migration-3.3.x.md" + }, + { + "text": "v3.5 材质升级指南", + "link": "material-system/effect-upgrade-documentation-for-v3.4.2-to-v3.5.md" + }, + { + "text": "资源分包升级指南", + "link": "asset/subpackage-upgrade-guide.md" + }, + { + "text": "资源管理模块升级指南", + "link": "asset/asset-manager-upgrade-guide.md" + }, + { + "text": "v3.5 已构建工程升级指南", + "link": "engine/template/native-upgrade-to-v3.5.md" + }, + { + "text": "v3.6 已构建工程升级指南", + "link": "engine/template/native-upgrade-to-v3.6.md" + }, + { + "text": "v3.6 构建模板与 settings.json 升级指南", + "link": "release-notes/build-template-settings-upgrade-guide-v3.6.md" + }, + { + "text": "Cocos Creator 3.6 材质升级指南", + "link": "material-system/effect-upgrade-documentation-for-v3.5-to-v3.6.md" + }, + { + "text": "升级指南:粒子从 v3.5.x 升级到 v3.6.0", + "link": "particle-system/particle-upgrade-documentation-for-v3.5-to-v3.6.md" + }, + { + "text": "v3.8 Android 工程升级", + "link": "release-notes/upgrade-3.8-android.md" + } + ], + "collapsed": true + }, + { + "text": "场景制作", + "link": "concepts/scene/index.md", + "items": [ + { + "text": "场景资源", + "link": "asset/scene.md" + }, + { + "text": "节点和组件", + "link": "concepts/scene/node-component.md" + }, + { + "text": "坐标系和节点变换", + "link": "concepts/scene/coord.md" + }, + { + "text": "节点层级和渲染顺序", + "link": "concepts/scene/node-tree.md" + }, + { + "text": "使用场景编辑器搭建场景", + "link": "concepts/scene/scene-editing.md" + }, + { + "text": "多层次细节", + "link": "editor/rendering/lod.md" + } + ], + "collapsed": true + }, + { + "text": "资源系统", + "link": "asset/index.md", + "items": [ + { + "text": "资源工作流", + "link": "asset/asset-workflow.md" + }, + { + "text": "图像资源", + "link": "asset/image.md", + "items": [ + { + "text": "纹理贴图资源", + "link": "asset/texture.md" + }, + { + "text": "精灵帧资源", + "link": "asset/sprite-frame.md", + "items": [ + { + "text": "图像资源的自动剪裁", + "link": "ui-system/components/engine/trim.md" + } + ], + "collapsed": true + }, + { + "text": "压缩纹理资源", + "link": "asset/compress-texture.md" + }, + { + "text": "立方体贴图资源", + "link": "asset/texture-cube.md" + }, + { + "text": "图集资源", + "link": "asset/atlas.md" + }, + { + "text": "自动图集资源", + "link": "asset/auto-atlas.md" + }, + { + "text": "艺术数字资源", + "link": "asset/label-atlas.md" + } + ], + "collapsed": true + }, + { + "text": "预制资源", + "link": "asset/prefab.md" + }, + { + "text": "字体资源", + "link": "asset/font.md" + }, + { + "text": "音频资源", + "link": "asset/audio.md" + }, + { + "text": "材质资源", + "link": "asset/material.md", + "items": [ + { + "text": "FBX 智能材质导入", + "link": "importer/materials/fbx-materials.md" + } + ], + "collapsed": true + }, + { + "text": "模型资源", + "link": "asset/model/mesh.md", + "items": [ + { + "text": "从第三方工具导出模型资源", + "link": "asset/model/dcc-export-mesh.md" + }, + { + "text": "从 3ds Max 中导出 FBX 模型资源", + "link": "asset/model/max-export-fbx.md" + }, + { + "text": "从 Maya 中导出 FBX 模型资源", + "link": "asset/model/maya-export-fbx.md" + }, + { + "text": "glTF 模型", + "link": "asset/model/glTF.md" + }, + { + "text": "程序化创建网格", + "link": "asset/model/scripting-mesh.md" + } + ], + "collapsed": true + }, + { + "text": "Spine 骨骼动画资源", + "link": "asset/spine.md" + }, + { + "text": "DragonBones 骨骼动画资源", + "link": "asset/dragonbones.md" + }, + { + "text": "TiledMap 瓦片图资源", + "link": "asset/tiledmap.md" + }, + { + "text": "JSON 资源", + "link": "asset/json.md" + }, + { + "text": "文本资源", + "link": "asset/text.md" + } + ], + "collapsed": true + }, + { + "text": "脚本指南及事件机制", + "link": "scripting/index.md", + "items": [ + { + "text": "编程语言支持", + "link": "scripting/language-support.md" + }, + { + "text": "脚本基础", + "link": "scripting/script-basics.md", + "items": [ + { + "text": "创建脚本", + "link": "scripting/setup.md" + }, + { + "text": "配置代码编辑环境", + "link": "scripting/coding-setup.md" + }, + { + "text": "脚本运行环境", + "link": "scripting/basic.md" + }, + { + "text": "装饰器使用", + "link": "scripting/decorator.md" + }, + { + "text": "属性参数参考", + "link": "scripting/reference/attributes.md" + }, + { + "text": "生命周期回调", + "link": "scripting/life-cycle-callbacks.md" + }, + { + "text": "开发注意事项", + "link": "scripting/readonly.md" + } + ], + "collapsed": true + }, + { + "text": "脚本使用", + "link": "scripting/usage.md", + "items": [ + { + "text": "访问节点和其他组件", + "link": "scripting/access-node-component.md" + }, + { + "text": "常用节点和组件接口", + "link": "scripting/basic-node-api.md" + }, + { + "text": "创建和销毁节点", + "link": "scripting/create-destroy.md" + }, + { + "text": "使用计时器", + "link": "scripting/scheduler.md" + }, + { + "text": "组件和组件执行顺序", + "link": "scripting/component.md" + }, + { + "text": "加载和切换场景", + "link": "scripting/scene-managing.md" + }, + { + "text": "获取和加载资源", + "link": "scripting/load-assets.md" + }, + { + "text": "tsconfig 配置", + "link": "scripting/tsconfig.md" + } + ], + "collapsed": true + }, + { + "text": "脚本进阶", + "link": "scripting/reference-class.md" + }, + { + "text": "事件系统", + "link": "engine/event/index.md", + "items": [ + { + "text": "发射和监听事件", + "link": "engine/event/event-emit.md" + }, + { + "text": "输入事件系统", + "link": "engine/event/event-input.md" + }, + { + "text": "节点事件系统", + "link": "engine/event/event-node.md" + }, + { + "text": "屏幕事件系统", + "link": "engine/event/event-screen.md" + }, + { + "text": "事件 API", + "link": "engine/event/event-api.md" + } + ], + "collapsed": true + }, + { + "text": "模块规范与示例", + "link": "scripting/modules/index.md", + "items": [ + { + "text": "引擎模块", + "link": "scripting/modules/engine.md" + }, + { + "text": "外部模块使用案例", + "link": "scripting/modules/example.md" + }, + { + "text": "模块规范", + "link": "scripting/modules/spec.md" + }, + { + "text": "导入映射", + "link": "scripting/modules/import-map.md" + } + ], + "collapsed": true + }, + { + "text": "外部代码支持", + "link": "scripting/external-scripts.md" + } + ], + "collapsed": true + }, + { + "text": "跨平台发布", + "link": "editor/publish/index.md", + "items": [ + { + "text": "通用构建选项介绍", + "link": "editor/publish/build-options.md" + }, + { + "text": "发布到小游戏平台", + "link": "editor/publish/publish-mini-game.md", + "items": [ + { + "text": "发布到微信小游戏", + "link": "editor/publish/publish-wechatgame.md", + "items": [ + { + "text": "启用微信小游戏引擎插件", + "link": "editor/publish/wechatgame-plugin.md" + }, + { + "text": "接入微信 PC 小游戏", + "link": "editor/publish/publish-pc-wechatgame.md" + }, + { + "text": "iOS 微信小游戏内存与性能优化指南", + "link": "editor/publish/wechat-ios-optimize.md" + } + ], + "collapsed": true + }, + { + "text": "发布到淘宝小游戏", + "link": "editor/publish/publish-taobao-mini-game.md", + "items": [ + { + "text": "启用淘宝小游戏引擎插件", + "link": "editor/publish/taobaominigame-plugin.md" + } + ], + "collapsed": true + }, + { + "text": "发布到支付宝小游戏", + "link": "editor/publish/publish-alipay-mini-game.md" + }, + { + "text": "发布到抖音小游戏", + "link": "editor/publish/publish-bytedance-mini-game.md", + "items": [ + { + "text": "接入抖音 PC 小游戏", + "link": "editor/publish/publish-pc-bytedance.md" + }, + { + "text": "iOS 抖音小游戏内存与性能优化指南", + "link": "editor/publish/bytedance-ios-optimize.md" + } + ], + "collapsed": true + }, + { + "text": "发布到华为快游戏", + "link": "editor/publish/publish-huawei-quick-game.md" + }, + { + "text": "发布到荣耀小游戏", + "link": "editor/publish/publish-honor-mini-game.md" + }, + { + "text": "发布到 OPPO 小游戏", + "link": "editor/publish/publish-oppo-mini-game.md" + }, + { + "text": "发布到 vivo 小游戏", + "link": "editor/publish/publish-vivo-mini-game.md" + }, + { + "text": "开放数据域", + "link": "editor/publish/build-open-data-context.md" + }, + { + "text": "小游戏分包", + "link": "editor/publish/subpackage.md" + } + ], + "collapsed": true + }, + { + "text": "发布到 Web 平台", + "link": "editor/publish/publish-web.md" + }, + { + "text": "发布到 Facebook Instant Games 平台", + "link": "editor/publish/publish-fb-instant-games.md" + }, + { + "text": "原生平台发布通用基础", + "link": "editor/publish/publish-native-index.md", + "items": [ + { + "text": "安装配置原生环境", + "link": "editor/publish/setup-native-development.md" + }, + { + "text": "原生平台通用构建选项", + "link": "editor/publish/native-options.md" + }, + { + "text": "原生平台 JavaScript 调试", + "link": "editor/publish/debug-jsb.md" + }, + { + "text": "集成 Input SDK", + "link": "editor/publish/gpg-input-sdk.md" + } + ], + "collapsed": true + }, + { + "text": "发布到 HUAWEI AppGallery Connect", + "link": "editor/publish/publish-huawei-agc.md" + }, + { + "text": "发布 HUAWEI HarmonyOS 应用", + "link": "editor/publish/publish-huawei-ohos.md" + }, + { + "text": "发布 HarmonyOS Next 应用", + "link": "editor/publish/publish-openharmony.md" + }, + { + "text": "发布 Android 应用", + "link": "editor/publish/android/index.md", + "items": [ + { + "text": "Android 构建示例", + "link": "editor/publish/android/build-example-android.md" + }, + { + "text": "Android 构建选项", + "link": "editor/publish/android/build-options-android.md" + }, + { + "text": "v3.8 Android 工程升级", + "link": "release-notes/upgrade-3.8-android.md" + } + ], + "collapsed": true + }, + { + "text": "发布 Google Play 示例", + "link": "editor/publish/google-play/build-example-google-play.md" + }, + { + "text": "发布到谷歌 GPG 平台", + "link": "editor/publish/google-play-games/index.md", + "items": [ + { + "text": "发布和运行", + "link": "editor/publish/google-play-games/build-and-run.md" + }, + { + "text": "集成 Input SDK", + "link": "editor/publish/gpg-input-sdk.md" + } + ], + "collapsed": true + }, + { + "text": "发布 iOS 应用", + "link": "editor/publish/ios/index.md", + "items": [ + { + "text": "iOS 发布示例", + "link": "editor/publish/ios/build-example-ios.md" + }, + { + "text": "iOS 构建选项", + "link": "editor/publish/ios/build-options-ios.md" + } + ], + "collapsed": true + }, + { + "text": "发布 macOS 应用", + "link": "editor/publish/mac/index.md", + "items": [ + { + "text": "macOS 发布示例", + "link": "editor/publish/mac/build-example-mac.md" + }, + { + "text": "macOS 构建选项", + "link": "editor/publish/mac/build-options-mac.md" + } + ], + "collapsed": true + }, + { + "text": "发布 Windows 应用", + "link": "editor/publish/windows/index.md", + "items": [ + { + "text": "Windows 构建示例", + "link": "editor/publish/windows/build-example-windows.md" + }, + { + "text": "Windows 构建选项", + "link": "editor/publish/windows/build-options-windows.md" + } + ], + "collapsed": true + }, + { + "text": "命令行发布项目", + "link": "editor/publish/publish-in-command-line.md" + }, + { + "text": "定制项目的构建模版", + "link": "editor/publish/custom-project-build-template.md" + }, + { + "text": "构建流程简介与常见错误处理", + "link": "editor/publish/build-guide.md" + } + ], + "collapsed": true + } + ] + }, + { + "text": "功能模块", + "collapsed": false, + "items": [ + { + "text": "图形渲染", + "link": "module-map/graphics.md", + "items": [ + { + "text": "渲染管线", + "link": "render-pipeline/overview.md", + "items": [ + { + "text": "使用内置管线", + "link": "render-pipeline/use-builtin-pipeline.md" + }, + { + "text": "自定义渲染管线", + "link": "render-pipeline/custom-pipeline.md" + }, + { + "text": "使用后期效果", + "link": "render-pipeline/use-post-process.md" + }, + { + "text": "自定义后效", + "link": "render-pipeline/post-process/custom.md" + } + ], + "collapsed": true + }, + { + "text": "相机", + "link": "editor/components/camera-component.md" + }, + { + "text": "光照", + "link": "concepts/scene/light.md", + "items": [ + { + "text": "基于物理的光照", + "link": "concepts/scene/light/pbr-lighting.md" + }, + { + "text": "光源", + "link": "concepts/scene/light/lightType/index.md", + "items": [ + { + "text": "平行光", + "link": "concepts/scene/light/lightType/dir-light.md" + }, + { + "text": "球面光", + "link": "concepts/scene/light/lightType/sphere-light.md" + }, + { + "text": "聚光灯", + "link": "concepts/scene/light/lightType/spot-light.md" + }, + { + "text": "环境光", + "link": "concepts/scene/light/lightType/ambient.md" + } + ], + "collapsed": true + }, + { + "text": "基于多 Pass 的多光源支持", + "link": "concepts/scene/light/additive-per-pixel-lights.md" + }, + { + "text": "阴影", + "link": "concepts/scene/light/shadow.md" + }, + { + "text": "基于图像的光照", + "link": "concepts/scene/light/probe/index.md", + "items": [ + { + "text": "光照探针", + "link": "concepts/scene/light/probe/light-probe.md", + "items": [ + { + "text": "光照探针面板", + "link": "concepts/scene/light/probe/light-probe-panel.md" + } + ], + "collapsed": true + }, + { + "text": "反射探针", + "link": "concepts/scene/light/probe/reflection-probe.md", + "items": [ + { + "text": "反射探针面板", + "link": "concepts/scene/light/probe/reflection-probe-panel.md" + }, + { + "text": "反射探针美术工作流", + "link": "concepts/scene/light/probe/reflection-art-workflow.md" + } + ], + "collapsed": true + }, + { + "text": "基于图像的光照示例", + "link": "concepts/scene/light/probe/example.md" + }, + { + "text": "光照贴图", + "link": "concepts/scene/light/lightmap.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "网格", + "link": "module-map/mesh/index.md", + "items": [ + { + "text": "MeshRenderer 组件", + "link": "engine/renderable/model-component.md" + }, + { + "text": "SkinnedMeshRenderer 组件", + "link": "module-map/mesh/skinnedMeshRenderer.md" + }, + { + "text": "SkinnedMeshBatchRenderer 组件", + "link": "module-map/mesh/skinnedMeshBatchRenderer.md" + }, + { + "text": "从第三方工具导出模型资源", + "link": "asset/model/dcc-export-mesh.md" + }, + { + "text": "从 3ds Max 中导出 FBX 模型资源", + "link": "asset/model/max-export-fbx.md" + }, + { + "text": "从 Maya 中导出 FBX 模型资源", + "link": "asset/model/maya-export-fbx.md" + }, + { + "text": "glTF 模型", + "link": "asset/model/glTF.md" + }, + { + "text": "程序化创建网格", + "link": "asset/model/scripting-mesh.md" + }, + { + "text": "顶点动画贴图 (VAT)", + "link": "asset/model/vat.md" + } + ], + "collapsed": true + }, + { + "text": "纹理", + "link": "module-map/texture/index.md", + "items": [ + { + "text": "纹理贴图", + "link": "asset/texture.md" + }, + { + "text": "压缩纹理", + "link": "asset/compress-texture.md" + }, + { + "text": "立方体贴图", + "link": "asset/texture-cube.md" + }, + { + "text": "渲染纹理", + "link": "asset/render-texture.md" + } + ], + "collapsed": true + }, + { + "text": "材质系统", + "link": "material-system/overview.md", + "items": [ + { + "text": "在脚本中使用材质", + "link": "material-system/material-script.md" + }, + { + "text": "内置材质", + "link": "material-system/builtin-material.md" + }, + { + "text": "材质系统类图", + "link": "material-system/material-structure.md" + } + ], + "collapsed": true + }, + { + "text": "着色器", + "link": "shader/index.md", + "items": [ + { + "text": "创建与使用", + "link": "shader/effect-inspector.md" + }, + { + "text": "内置着色器", + "link": "shader/effect-builtin.md", + "items": [ + { + "text": "基于物理的光照模型 PBR", + "link": "shader/effect-builtin-pbr.md" + }, + { + "text": "卡通渲染", + "link": "shader/effect-builtin-toon.md" + }, + { + "text": "无光照", + "link": "shader/effect-builtin-unlit.md" + } + ], + "collapsed": true + }, + { + "text": "着色器语法", + "link": "shader/effect-syntax.md", + "items": [ + { + "text": "Pass 可选配置参数", + "link": "shader/pass-parameter-list.md" + }, + { + "text": "YAML 101 语法简介", + "link": "shader/yaml-101.md" + }, + { + "text": "GLSL 语法简介", + "link": "shader/glsl.md" + }, + { + "text": "预处理宏定义", + "link": "shader/macros.md" + }, + { + "text": "着色器片段(Chunk)", + "link": "shader/effect-chunk-index.md" + } + ], + "collapsed": true + }, + { + "text": "内置全局 Uniform", + "link": "shader/uniform.md" + }, + { + "text": "公共函数库", + "link": "shader/common-functions.md" + }, + { + "text": "前向渲染与延迟渲染 Shader 执行流程", + "link": "shader/forward-and-deferred.md" + }, + { + "text": "表面着色器 - Surface Shader", + "link": "shader/surface-shader.md", + "items": [ + { + "text": "内置 Surface Shader 导读", + "link": "shader/surface-shader/builtin-surface-shader.md" + }, + { + "text": "Surface Shader 基本结构", + "link": "shader/surface-shader/surface-shader-structure.md" + }, + { + "text": "Surface Shader 执行流程", + "link": "shader/surface-shader/shader-code-flow.md" + }, + { + "text": "include 机制", + "link": "shader/surface-shader/includes.md" + }, + { + "text": "宏定义与重映射", + "link": "shader/surface-shader/macro-remapping.md" + }, + { + "text": "使用宏定义实现函数替换", + "link": "shader/surface-shader/function-replace.md" + }, + { + "text": "可替换的内置函数", + "link": "shader/surface-shader/surface-function.md" + }, + { + "text": "渲染用途", + "link": "shader/surface-shader/render-usage.md" + }, + { + "text": "光照模型", + "link": "shader/surface-shader/lighting-mode.md" + }, + { + "text": "表面材质数据结构", + "link": "shader/surface-shader/surface-data-struct.md" + }, + { + "text": "着色器类别", + "link": "shader/surface-shader/shader-stage.md" + }, + { + "text": "组装器", + "link": "shader/surface-shader/shader-assembly.md" + }, + { + "text": "VS 输入", + "link": "shader/surface-shader/vs-input.md" + }, + { + "text": "FS 输入", + "link": "shader/surface-shader/fs-input.md" + }, + { + "text": "自定义 Surface Shader", + "link": "shader/surface-shader/customize-surface-shader.md" + }, + { + "text": "渲染调式功能", + "link": "shader/surface-shader/rendering-debug-view.md" + } + ], + "collapsed": true + }, + { + "text": "传统着色器 - Legacy Shader", + "link": "shader/legacy-shader/legacy-shader.md", + "items": [ + { + "text": "内置 Legacy Shader 导读", + "link": "shader/legacy-shader/legacy-shader-builtins.md" + }, + { + "text": "Legacy Shader 主要函数与结构体", + "link": "shader/legacy-shader/legacy-shader-func-struct.md" + } + ], + "collapsed": true + }, + { + "text": "自定义着色器", + "link": "shader/write-effect-overview.md", + "items": [ + { + "text": "2D 精灵着色器:Gradient", + "link": "shader/write-effect-2d-sprite-gradient.md" + }, + { + "text": "3D 着色器:RimLight", + "link": "shader/write-effect-3d-rim-light.md" + } + ], + "collapsed": true + }, + { + "text": "皮肤材质", + "link": "shader/advanced-shader/skin.md" + }, + { + "text": "自定义几何体实例化属性", + "link": "shader/instanced-attributes.md" + }, + { + "text": "UBO 内存布局策略", + "link": "shader/ubo-layout.md" + }, + { + "text": "WebGL 1.0 向下兼容支持", + "link": "shader/webgl-100-fallback.md" + }, + { + "text": "VSCode 着色器插件", + "link": "shader/vscode-plugin.md" + }, + { + "text": "计算着色器", + "link": "shader/compute-shader.md" + } + ], + "collapsed": true + }, + { + "text": "2D 渲染排序", + "link": "engine/rendering/sorting-2d.md" + }, + { + "text": "3D 渲染排序", + "link": "engine/rendering/sorting.md" + }, + { + "text": "特效组件", + "link": "module-map/effects/index.md", + "items": [ + { + "text": "广告牌", + "link": "particle-system/billboard-component.md" + }, + { + "text": "线段组件", + "link": "particle-system/line-component.md" + } + ], + "collapsed": true + }, + { + "text": "天空盒", + "link": "concepts/scene/skybox.md" + }, + { + "text": "全局雾", + "link": "concepts/scene/fog.md" + }, + { + "text": "几何渲染器", + "link": "geometry-renderer/index.md" + }, + { + "text": "调试渲染器(Debug-Renderer)", + "link": "debug-renderer/index.md" + } + ], + "collapsed": true + }, + { + "text": "2D 对象", + "link": "2d-object/index.md", + "items": [ + { + "text": "2D 渲染", + "link": "2d-object/2d-render/index.md", + "items": [ + { + "text": "渲染排序规则", + "link": "ui-system/components/engine/priority.md" + }, + { + "text": "2D 渲染组件合批说明", + "link": "ui-system/components/engine/ui-batch.md" + }, + { + "text": "2D 渲染对象自定义材质", + "link": "ui-system/components/engine/ui-material.md" + }, + { + "text": "2D 渲染组件", + "link": "ui-system/components/editor/render-component.md", + "items": [ + { + "text": "Sprite 组件参考", + "link": "ui-system/components/editor/sprite.md" + }, + { + "text": "Label 组件参考", + "link": "ui-system/components/editor/label.md" + }, + { + "text": "Mask 组件参考", + "link": "ui-system/components/editor/mask.md" + }, + { + "text": "Graphics 组件参考", + "link": "ui-system/components/editor/graphics.md" + }, + { + "text": "RichText 组件参考", + "link": "ui-system/components/editor/richtext.md" + }, + { + "text": "UIStaticBatch 组件参考", + "link": "ui-system/components/editor/ui-static.md" + }, + { + "text": "Spine Skeleton 组件参考", + "link": "editor/components/spine.md" + }, + { + "text": "DragonBones ArmatureDisplay 组件参考", + "link": "editor/components/dragonbones.md" + }, + { + "text": "TiledMap 组件参考", + "link": "editor/components/tiledmap.md" + }, + { + "text": "TiledTile 组件参考", + "link": "editor/components/tiledtile.md" + }, + { + "text": "MotionStreak 组件参考", + "link": "editor/components/motion-streak.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "UI 系统", + "link": "2d-object/ui-system/index.md", + "items": [ + { + "text": "UI 组件", + "link": "ui-system/components/editor/base-component.md", + "items": [ + { + "text": "Canvas 组件参考", + "link": "ui-system/components/editor/canvas.md" + }, + { + "text": "UITransform 组件参考", + "link": "ui-system/components/editor/ui-transform.md" + }, + { + "text": "Widget 组件参考", + "link": "ui-system/components/editor/widget.md" + }, + { + "text": "Button 组件参考", + "link": "ui-system/components/editor/button.md" + }, + { + "text": "Layout 组件参考", + "link": "ui-system/components/editor/layout.md" + }, + { + "text": "EditBox 组件参考", + "link": "ui-system/components/editor/editbox.md" + }, + { + "text": "ScrollView 组件参考", + "link": "ui-system/components/editor/scrollview.md" + }, + { + "text": "ScrollBar 组件参考", + "link": "ui-system/components/editor/scrollbar.md" + }, + { + "text": "ProgressBar 组件参考", + "link": "ui-system/components/editor/progress.md" + }, + { + "text": "LabelOutline 组件参考", + "link": "ui-system/components/editor/label-outline.md" + }, + { + "text": "LabelShadow 组件参考", + "link": "ui-system/components/editor/label-shadow.md" + }, + { + "text": "Toggle 组件参考", + "link": "ui-system/components/editor/toggle.md" + }, + { + "text": "ToggleContainer 组件参考", + "link": "ui-system/components/editor/toggleContainer.md" + }, + { + "text": "Slider 组件参考", + "link": "ui-system/components/editor/slider.md" + }, + { + "text": "PageView 组件参考", + "link": "ui-system/components/editor/pageview.md" + }, + { + "text": "PageViewIndicator 组件参考", + "link": "ui-system/components/editor/pageviewindicator.md" + }, + { + "text": "UIMeshRenderer 组件参考", + "link": "ui-system/components/editor/ui-model.md" + }, + { + "text": "UICoordinateTracker 组件参考", + "link": "ui-system/components/editor/ui-coordinate-tracker.md" + }, + { + "text": "UIOpacity 组件参考", + "link": "ui-system/components/editor/ui-opacity.md" + }, + { + "text": "UISkew 组件参考", + "link": "ui-system/components/editor/ui-skew.md" + }, + { + "text": "BlockInputEvents 组件参考", + "link": "ui-system/components/editor/block-input-events.md" + }, + { + "text": "WebView 组件参考", + "link": "ui-system/components/editor/webview.md" + }, + { + "text": "VideoPlayer 组件参考", + "link": "ui-system/components/editor/videoplayer.md" + }, + { + "text": "SafeArea 组件参考", + "link": "ui-system/components/editor/safearea.md" + } + ], + "collapsed": true + }, + { + "text": "UI 实践指南", + "link": "ui-system/components/engine/usage-ui.md", + "items": [ + { + "text": "多分辨率适配方案", + "link": "ui-system/components/engine/multi-resolution.md" + }, + { + "text": "对齐策略", + "link": "ui-system/components/engine/widget-align.md" + }, + { + "text": "文字排版", + "link": "ui-system/components/engine/label-layout.md" + }, + { + "text": "自动布局容器", + "link": "ui-system/components/engine/auto-layout.md" + }, + { + "text": "制作动态生成内容的列表", + "link": "ui-system/components/engine/list-with-data.md" + }, + { + "text": "制作可任意拉伸的 UI 图像", + "link": "ui-system/components/engine/sliced-sprite.md" + }, + { + "text": "安卓大屏幕适配", + "link": "ui-system/components/engine/large-screen.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "动画系统", + "link": "animation/index.md", + "items": [ + { + "text": "动画剪辑", + "link": "animation/animation-clip.md" + }, + { + "text": "动画组件参考", + "link": "animation/animation-comp.md" + }, + { + "text": "使用动画编辑器", + "link": "animation/animation.md", + "items": [ + { + "text": "创建 Animation 组件和动画剪辑", + "link": "animation/animation-create.md" + }, + { + "text": "动画编辑器面板介绍", + "link": "animation/animation-editor.md" + }, + { + "text": "编辑动画剪辑", + "link": "animation/edit-animation-clip.md", + "items": [ + { + "text": "关键帧编辑视图", + "link": "animation/animation-keyFrames.md" + }, + { + "text": "曲线编辑视图", + "link": "animation/animation-curve.md" + }, + { + "text": "曲线编辑器", + "link": "animation/curve-editor.md" + }, + { + "text": "辅助曲线编辑视图", + "link": "animation/animation-auxiliary-curve.md" + } + ], + "collapsed": true + }, + { + "text": "添加动画事件", + "link": "animation/animation-event.md" + }, + { + "text": "程序化编辑动画剪辑", + "link": "animation/use-animation-curve.md" + } + ], + "collapsed": true + }, + { + "text": "骨骼动画", + "link": "animation/skeletal-animation.md", + "items": [ + { + "text": "骨骼贴图布局设置", + "link": "animation/joint-texture-layout.md" + } + ], + "collapsed": true + }, + { + "text": "程序化控制动画", + "link": "animation/animation-component.md", + "items": [ + { + "text": "动画状态", + "link": "animation/animation-state.md" + } + ], + "collapsed": true + }, + { + "text": "变形动画", + "link": "animation/morph.md" + }, + { + "text": "嵌入播放器", + "link": "animation/embedded-player.md" + }, + { + "text": "Marionette 动画系统", + "link": "animation/marionette/index.md", + "items": [ + { + "text": "动画图资源", + "link": "animation/marionette/animation-graph.md" + }, + { + "text": "动画控制器组件参考", + "link": "animation/marionette/animation-controller.md" + }, + { + "text": "动画图面板", + "link": "animation/marionette/animation-graph-panel.md" + }, + { + "text": "动画图层级", + "link": "animation/marionette/animation-graph-layer.md" + }, + { + "text": "动画状态机", + "link": "animation/marionette/animation-graph-basics.md" + }, + { + "text": "状态过渡", + "link": "animation/marionette/state-transition.md" + }, + { + "text": "动画遮罩资源", + "link": "animation/marionette/animation-mask.md" + }, + { + "text": "动画图变体", + "link": "animation/marionette/animation-variant.md" + }, + { + "text": "程序式动画", + "link": "animation/marionette/procedural-animation/index.md", + "items": [ + { + "text": "程序式动画", + "link": "animation/marionette/procedural-animation/introduce.md", + "items": [ + { + "text": "启用程序式动画功能", + "link": "animation/marionette/procedural-animation/enabling.md" + } + ], + "collapsed": true + }, + { + "text": "姿态图", + "link": "animation/marionette/procedural-animation/pose-graph/index.md", + "items": [ + { + "text": "姿态图节点视图", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/node-operation.md" + }, + { + "text": "姿态结点", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/index.md" + }, + { + "text": "混合姿态", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/blend-poses.md" + }, + { + "text": "修改姿态", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/modify-pose.md" + }, + { + "text": "播放或采样动画", + "link": "animation/marionette/procedural-animation/pose-graph/pose-nodes/play-or-sample-motion.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "音频系统", + "link": "audio-system/overview.md", + "items": [ + { + "text": "AudioSource 组件参考", + "link": "audio-system/audiosource.md" + }, + { + "text": "全局音频管理器示例", + "link": "audio-system/audioExample.md" + }, + { + "text": "兼容性说明", + "link": "audio-system/audioLimit.md" + } + ], + "collapsed": true + }, + { + "text": "物理系统", + "link": "physics/index.md", + "items": [ + { + "text": "2D 物理", + "link": "physics-2d/physics-2d.md", + "items": [ + { + "text": "2D 物理系统", + "link": "physics-2d/physics-2d-system.md" + }, + { + "text": "2D 刚体组件", + "link": "physics-2d/physics-2d-rigid-body.md" + }, + { + "text": "2D 碰撞体", + "link": "physics-2d/physics-2d-collider.md" + }, + { + "text": "2D 碰撞回调", + "link": "physics-2d/physics-2d-contact-callback.md" + }, + { + "text": "2D 物理关节", + "link": "physics-2d/physics-2d-joint.md" + } + ], + "collapsed": true + }, + { + "text": "3D 物理系统", + "link": "physics/physics.md", + "items": [ + { + "text": "设置物理引擎", + "link": "physics/physics-engine.md" + }, + { + "text": "物理系统配置", + "link": "physics/physics-configs.md" + }, + { + "text": "分组和掩码", + "link": "physics/physics-group-mask.md" + }, + { + "text": "物理组件", + "link": "physics/physics-component.md", + "items": [ + { + "text": "碰撞体", + "link": "physics/physics-collider.md" + }, + { + "text": "刚体", + "link": "physics/physics-rigidbody.md" + }, + { + "text": "恒力组件", + "link": "physics/physics-constantForce.md" + }, + { + "text": "约束", + "link": "physics/physics-constraint.md" + } + ], + "collapsed": true + }, + { + "text": "物理材质", + "link": "physics/physics-material.md" + }, + { + "text": "物理事件", + "link": "physics/physics-event.md" + }, + { + "text": "射线检测", + "link": "physics/physics-raycast.md" + }, + { + "text": "几何投射检测", + "link": "physics/physics-sweep.md" + }, + { + "text": "连续碰撞检测", + "link": "physics/physics-ccd.md" + }, + { + "text": "角色控制器", + "link": "physics/character-controller/index.md" + }, + { + "text": "物理应用案例", + "link": "physics/physics-example.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "粒子系统", + "link": "particle-system/index.md", + "items": [ + { + "text": "2D 粒子", + "link": "particle-system/2d-particle/2d-particle.md" + }, + { + "text": "3D 粒子", + "link": "particle-system/overview.md", + "items": [ + { + "text": "粒子系统模块", + "link": "particle-system/module.md", + "items": [ + { + "text": "主模块", + "link": "particle-system/main-module.md" + }, + { + "text": "发射器模块", + "link": "particle-system/emitter.md" + }, + { + "text": "速度模块", + "link": "particle-system/velocity-module.md" + }, + { + "text": "加速度模块", + "link": "particle-system/force-module.md" + }, + { + "text": "大小模块", + "link": "particle-system/size-module.md" + }, + { + "text": "旋转模块", + "link": "particle-system/rotation-module.md" + }, + { + "text": "颜色模块", + "link": "particle-system/color-module.md" + }, + { + "text": "贴图动画模块", + "link": "particle-system/texture-animation-module.md" + }, + { + "text": "限速模块", + "link": "particle-system/limit-velocity-module.md" + }, + { + "text": "拖尾模块", + "link": "particle-system/trail-module.md" + }, + { + "text": "渲染模块", + "link": "particle-system/renderer.md" + } + ], + "collapsed": true + }, + { + "text": "粒子属性编辑", + "link": "particle-system/editor/index.md", + "items": [ + { + "text": "控制面板", + "link": "particle-system/editor/particle-effect-panel.md" + }, + { + "text": "粒子曲线编辑器", + "link": "particle-system/editor/curve-editor.md" + }, + { + "text": "渐变色编辑器", + "link": "particle-system/editor/gradient-editor.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "缓动系统", + "link": "tween/index.md", + "items": [ + { + "text": "缓动接口", + "link": "tween/tween-interface.md" + }, + { + "text": "缓动函数", + "link": "tween/tween-function.md" + }, + { + "text": "缓动示例", + "link": "tween/tween-example.md" + } + ], + "collapsed": true + }, + { + "text": "地形系统", + "link": "editor/terrain/index.md" + }, + { + "text": "资源管理", + "link": "asset/asset-manager.md", + "items": [ + { + "text": "loader 升级 assetManager 指南", + "link": "asset/asset-manager-upgrade-guide.md" + }, + { + "text": "子包升级 Asset Bundle 指南", + "link": "asset/subpackage-upgrade-guide.md" + }, + { + "text": "资源加载", + "link": "asset/dynamic-load-resources.md" + }, + { + "text": "Asset Bundle", + "link": "asset/bundle.md" + }, + { + "text": "资源释放", + "link": "asset/release-manager.md" + }, + { + "text": "下载与解析", + "link": "asset/downloader-parser.md" + }, + { + "text": "加载与预加载", + "link": "asset/preload-load.md" + }, + { + "text": "缓存管理器", + "link": "asset/cache-manager.md" + }, + { + "text": "可选参数", + "link": "asset/options.md" + }, + { + "text": "管线与任务", + "link": "asset/pipeline-task.md" + }, + { + "text": "资源管理注意事项 - meta 文件", + "link": "asset/meta.md" + } + ], + "collapsed": true + }, + { + "text": "多语言(L10N)", + "link": "editor/l10n/overview.md", + "items": [ + { + "text": "译文服务商", + "link": "editor/l10n/translation-service.md" + }, + { + "text": "收集并统计", + "link": "editor/l10n/collect-and-count.md" + }, + { + "text": "语言编译", + "link": "editor/l10n/compile-language.md" + }, + { + "text": "L10nLabel 组件", + "link": "editor/l10n/l10n-label.md" + }, + { + "text": "示例", + "link": "editor/l10n/script-using.md" + }, + { + "text": "API", + "link": "editor/l10n/localization-editor-api.md" + }, + { + "text": "EXCEL 导入示例", + "link": "editor/l10n/example-excel.md" + } + ], + "collapsed": true + }, + { + "text": "XR", + "link": "xr/index.md", + "items": [ + { + "text": "版本历史", + "link": "xr/version-history.md" + }, + { + "text": "架构", + "link": "xr/architecture/index.md", + "items": [ + { + "text": "内置资源与预制体", + "link": "xr/architecture/assets.md" + }, + { + "text": "XR 组件", + "link": "xr/architecture/component.md" + }, + { + "text": "预览", + "link": "xr/architecture/preview.md" + }, + { + "text": "XR 视频播放器", + "link": "xr/architecture/xr-video-player.md" + }, + { + "text": "XR 网页浏览器", + "link": "xr/architecture/xr-webview.md" + }, + { + "text": "XR 空间音频", + "link": "xr/architecture/xr-spatial-audio.md" + }, + { + "text": "XR 合成层", + "link": "xr/architecture/xr-composition-layer.md" + }, + { + "text": "透视", + "link": "xr/architecture/xr-pass-through.md" + }, + { + "text": "AR", + "link": "xr/architecture/ar-introduce.md", + "items": [ + { + "text": "AR 相机", + "link": "xr/architecture/ar-camera.md" + }, + { + "text": "AR Manager", + "link": "xr/architecture/ar-manager.md" + }, + { + "text": "AR 自动化行为编辑", + "link": "xr/architecture/ar-tracking-component.md" + }, + { + "text": "AR 交互", + "link": "xr/architecture/ar-interaction.md" + }, + { + "text": "AR 降级", + "link": "xr/architecture/xr-fallback.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "快速部署指南", + "link": "xr/project-deploy/index.md", + "items": [ + { + "text": "VR 项目创建", + "link": "xr/project-deploy/vr-proj-deploy.md" + }, + { + "text": "VR 项目构建与发布", + "link": "xr/project-deploy/vr-proj-pub.md" + }, + { + "text": "AR 项目创建", + "link": "xr/project-deploy/ar-proj-deploy.md" + }, + { + "text": "AR 项目构建与发布", + "link": "xr/project-deploy/ar-proj-pub.md" + }, + { + "text": "WebXR 项目配置", + "link": "xr/project-deploy/webxr-proj-deploy.md" + }, + { + "text": "WebXR 项目构建与发布", + "link": "xr/project-deploy/webxr-proj-pub.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "原生开发", + "link": "native/overview.md", + "items": [ + { + "text": "原生平台二次开发指南", + "link": "advanced-topics/native-secondary-development.md" + }, + { + "text": "Java 原生反射机制", + "link": "advanced-topics/java-reflection.md" + }, + { + "text": "Objective-C 原生反射机制", + "link": "advanced-topics/oc-reflection.md" + }, + { + "text": "ArkTS 原生反射机制", + "link": "advanced-topics/arkts-reflection.md" + }, + { + "text": "JsbBridge JS 与 JAVA 通信", + "link": "advanced-topics/js-java-bridge.md" + }, + { + "text": "JsbBridge JS 与 Objective-C 通信", + "link": "advanced-topics/js-oc-bridge.md" + }, + { + "text": "JsbBridgeWrapper 基于原生反射机制的事件处理", + "link": "advanced-topics/jsb-bridge-wrapper.md" + }, + { + "text": "JSB 2.0 使用指南", + "link": "advanced-topics/JSB2.0-learning.md", + "items": [ + { + "text": "JSB 手动绑定", + "link": "advanced-topics/jsb-manual-binding.md" + }, + { + "text": "JSB 自动绑定", + "link": "advanced-topics/jsb-auto-binding.md" + }, + { + "text": "Swig", + "link": "advanced-topics/jsb-swig.md" + }, + { + "text": "Swig 示例", + "link": "advanced-topics/jsb/swig/tutorial/index.md" + } + ], + "collapsed": true + }, + { + "text": "CMake 使用简介", + "link": "advanced-topics/cmake-learning.md" + }, + { + "text": "原生引擎内存泄漏检测系统", + "link": "advanced-topics/memory-leak-detector.md" + }, + { + "text": "原生场景剔除", + "link": "advanced-topics/native-scene-culling.md" + }, + { + "text": "原生性能剖析器", + "link": "advanced-topics/profiler.md" + }, + { + "text": "原生插件", + "link": "advanced-topics/native-plugins/brief.md", + "items": [ + { + "text": "原生插件创建范例", + "link": "advanced-topics/native-plugins/tutorial.md" + } + ], + "collapsed": true + }, + { + "text": "原生引擎跨语言调用优化", + "link": "advanced-topics/jsb-optimizations.md" + } + ], + "collapsed": true + } + ] + }, + { + "text": "进阶教程", + "collapsed": false, + "items": [ + { + "text": "扩展编辑器", + "link": "editor/extension/readme.md", + "items": [ + { + "text": "扩展管理器面板", + "link": "editor/extension/extension-manager.md" + }, + { + "text": "扩展模板与编译构建", + "link": "editor/extension/create-extension.md" + }, + { + "text": "入门示例-菜单", + "link": "editor/extension/first.md" + }, + { + "text": "入门示例-面板", + "link": "editor/extension/first-panel.md" + }, + { + "text": "入门示例-扩展间通信", + "link": "editor/extension/first-communication.md" + }, + { + "text": "扩展改名", + "link": "editor/extension/extension-change-name.md" + }, + { + "text": "安装与分享", + "link": "editor/extension/install.md" + }, + { + "text": "上架扩展到资源商店", + "link": "editor/extension/store/upload-store.md" + }, + { + "text": "增强已有功能", + "link": "editor/extension/contributions.md", + "items": [ + { + "text": "自定义主菜单", + "link": "editor/extension/contributions-menu.md" + }, + { + "text": "自定义消息", + "link": "editor/extension/contributions-messages.md" + }, + { + "text": "操作当前场景", + "link": "editor/extension/scene-script.md" + }, + { + "text": "增强资源管理器面板", + "link": "editor/assets/extension.md" + }, + { + "text": "自定义资源数据库", + "link": "editor/extension/contributions-database.md" + }, + { + "text": "自定义属性检查器面板", + "link": "editor/extension/inspector.md" + }, + { + "text": "自定义构建流程", + "link": "editor/publish/custom-build-plugin.md" + }, + { + "text": "自定义项目设置面板", + "link": "editor/extension/contributions-project.md" + }, + { + "text": "自定义偏好设置面板", + "link": "editor/extension/contributions-preferences.md" + }, + { + "text": "快捷键", + "link": "editor/extension/contributions-shortcuts.md" + } + ], + "collapsed": true + }, + { + "text": "扩展系统详解", + "link": "editor/extension/basic.md", + "items": [ + { + "text": "基础结构", + "link": "editor/extension/package.md" + }, + { + "text": "扩展包定义", + "link": "editor/extension/define.md" + }, + { + "text": "消息系统", + "link": "editor/extension/messages.md" + }, + { + "text": "多语言系统(i18n)", + "link": "editor/extension/i18n.md" + }, + { + "text": "配置系统", + "link": "editor/extension/profile.md" + }, + { + "text": "面板系统", + "link": "editor/extension/panel.md" + }, + { + "text": "UI 组件", + "link": "editor/extension/ui.md" + }, + { + "text": "使用第三方工具", + "link": "editor/npm.md" + } + ], + "collapsed": true + }, + { + "text": "Editor 接口说明", + "link": "editor/extension/editor-api.md", + "items": [ + { + "text": "App", + "link": "editor/extension/api/app.md" + }, + { + "text": "Clipboard", + "link": "editor/extension/api/clipboard.md" + }, + { + "text": "Dialog", + "link": "editor/extension/api/dialog.md" + }, + { + "text": "I18n", + "link": "editor/extension/api/i18n.md" + }, + { + "text": "Logger", + "link": "editor/extension/api/logger.md" + }, + { + "text": "Message", + "link": "editor/extension/api/message.md" + }, + { + "text": "Network", + "link": "editor/extension/api/network.md" + }, + { + "text": "Package", + "link": "editor/extension/api/package.md" + }, + { + "text": "Panel", + "link": "editor/extension/api/panel.md" + }, + { + "text": "Profile", + "link": "editor/extension/api/profile.md" + }, + { + "text": "Project", + "link": "editor/extension/api/project.md" + }, + { + "text": "Selection", + "link": "editor/extension/api/selection.md" + }, + { + "text": "Utils", + "link": "editor/extension/api/utils.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + }, + { + "text": "进阶主题", + "link": "advanced-topics/index.md", + "items": [ + { + "text": "如何向 Cocos 提交代码", + "link": "submit-pr/submit-pr.md" + }, + { + "text": "存储和读取用户数据", + "link": "advanced-topics/data-storage.md" + }, + { + "text": "自定义加载 Wasm/Asm 文件与模块", + "link": "advanced-topics/wasm-asm-load.md" + }, + { + "text": "使用 Emscripten 将原生代码转化为 Wasm/Asm 文件", + "link": "advanced-topics/wasm-asm-create.md" + }, + { + "text": "引擎定制工作流程", + "link": "advanced-topics/engine-customization.md" + }, + { + "text": "网页预览定制工作流程", + "link": "editor/preview/browser.md" + }, + { + "text": "i18n 游戏多语言支持", + "link": "advanced-topics/i18n.md" + }, + { + "text": "动态合图", + "link": "advanced-topics/dynamic-atlas.md" + }, + { + "text": "压缩引擎内部属性", + "link": "advanced-topics/mangle-properties.md" + }, + { + "text": "热更新范例教程", + "link": "advanced-topics/hot-update.md" + }, + { + "text": "热更新管理器", + "link": "advanced-topics/hot-update-manager.md" + }, + { + "text": "HTTP 请求", + "link": "advanced-topics/http.md" + }, + { + "text": "WebSocket", + "link": "advanced-topics/websocket-introduction.md", + "items": [ + { + "text": "WebSocket 客户端", + "link": "advanced-topics/websocket.md" + }, + { + "text": "WebSocket 服务器", + "link": "advanced-topics/websocket-server.md" + } + ], + "collapsed": true + } + ], + "collapsed": true + } + ] + } +] \ No newline at end of file diff --git a/versions/4.0/zh/tutorial/img/3d-parkour-title.jpg b/versions/4.0/zh/tutorial/img/3d-parkour-title.jpg new file mode 100644 index 0000000000..30ad0cd4c0 Binary files /dev/null and b/versions/4.0/zh/tutorial/img/3d-parkour-title.jpg differ diff --git a/versions/4.0/zh/tween/img/homeImgGame.png b/versions/4.0/zh/tween/img/homeImgGame.png new file mode 100644 index 0000000000..1e177e1eae Binary files /dev/null and b/versions/4.0/zh/tween/img/homeImgGame.png differ diff --git a/versions/4.0/zh/tween/img/tweener.png b/versions/4.0/zh/tween/img/tweener.png new file mode 100644 index 0000000000..fd693a69f7 Binary files /dev/null and b/versions/4.0/zh/tween/img/tweener.png differ diff --git a/versions/4.0/zh/tween/img/updateUntil.gif b/versions/4.0/zh/tween/img/updateUntil.gif new file mode 100644 index 0000000000..8c28707250 Binary files /dev/null and b/versions/4.0/zh/tween/img/updateUntil.gif differ diff --git a/versions/4.0/zh/tween/index.md b/versions/4.0/zh/tween/index.md new file mode 100644 index 0000000000..fae1427569 --- /dev/null +++ b/versions/4.0/zh/tween/index.md @@ -0,0 +1,20 @@ +# 缓动系统 + +![tween-index.png](img/homeImgGame.png) + +缓动系统被广泛的应用于游戏开发中,其主要目的之一是用于解决离线动画无法满足需求时的动态动画的问题。 + +在 Cocos Creator 中,缓动除了可以用于变换位置、旋转、缩放和颜色等常规动画信息,还支持延迟,队列,并行等动作行为。 + +## 内容 + +本章包含以下内容: + +- [缓动接口](./tween-interface.md) +- [缓动函数](./tween-function.md) +- [缓动示例](./tween-example.md) + +## 范例 + +更多关于 Tween 具体的使用方法,详情请参考范例 **Tween**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8.4/assets/cases/tween))。 + diff --git a/versions/4.0/zh/tween/tween-example.md b/versions/4.0/zh/tween/tween-example.md new file mode 100644 index 0000000000..0933bd81ba --- /dev/null +++ b/versions/4.0/zh/tween/tween-example.md @@ -0,0 +1,1078 @@ +# 缓动示例 + +本文将主要介绍 Cosos Creator 缓动中常见的一些用法和接口。 + +## 构造缓动 + +通过 `tween` 方法或使用 `new Tween(target: T)` 都可以构造缓动。 + +> **注意**:‘tween’ 是引擎提供的工具方法,并非 ‘Tween’ 的成员,请注意区分。关于这点可以参考接口说明: [缓动接口](tween-interface.md)。 + +## 链式 API + +大部分和动作相关的接口都会返回 `this` 或者一个新的 `Tween` 对象,因此可以方便的使用链式调用来进行组合: + +```ts +tween() + .target(this.node) + .to(1.0, { position: new Vec3(0, 10, 0) }) + .by(1.0, { position: new Vec3(0, -10, 0) }) + .delay(1.0) + .by(1.0, { position: new Vec3(0, -10, 0) }) + .start() +``` + +## to,by + +这里演示了如何使用一个 `to` 类型的缓动绑定节点的位置信息并将其位置沿 Y 轴偏移 10 个单位: + +```ts +let tweenDuration : number = 1.0; // 缓动的时长 +tween(this.node.position) + .to( tweenDuration, new Vec3(0, 10, 0), { // to 接口表示节点的绝对值 + onUpdate : (target:Vec3, ratio:number)=>{ // 实现 ITweenOption 的 onUpdate 回调,接受当前缓动的进度 + this.node.position = target; // 将缓动系统计算出的结果赋予 node 的位置 + } +}).start(); // 调用 start 方法,开启缓动 +``` + +### 通过函数返回属性目标值 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +```ts +tween(this.node).to(1, { angle: ()=>90 ).start(); +``` + +以上代码等价于: + +```ts +tween(this.node).to(1, { angle: 90 ).start(); +``` + +### 自定义插值函数 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +- 针对当前动作的某个属性 + +```ts +// 同时缓动 node 上的 angle 和 position 两个属性,但只给 angle 属性自定义插值函数 +tween(this.node).to(1, { + angle: { + value: 90, + progress(start: number, end: number, current: number, ratio: number): number { + return lerp(start, end, ratio); + }, + }, + position: v3(90, 90, 90), + } +).start(); +``` + +- 针对当前动作的所有属性 + +要同时处理 to 动作关联 node 上的 angle 和 position 属性的自定义插值函数,可以考虑设置 opt.progress 参数。 + +```ts +tween(this.node).to(1, { angle: 90, position: v3(90, 90, 90) }, { + progress(start: number, end: number, current: number, ratio: number): number { + return lerp(start, end, ratio); + }, +}).start(); +``` + +### 自定义缓动时间函数 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +- 针对当前动作的某个属性 + +```ts +// 1. 使用内置缓动时间函数 +tween(this.node).to(1, { + angle: { + value: 90, + easing: 'backIn', + }, + position: v3(90, 90, 90), +}).start(); + +// 或者 +// 2. 使用自定义缓动时间函数 +tween(this.node).to(1, { + angle: { + value: 90, + easing: (k: number): number => { + if (k === 1) { + return 1; + } + const s = 1.70158; + return k * k * ((s + 1) * k - s); + }, + }, + position: v3(90, 90, 90), +}).start(); +``` + +- 针对当前动作的所有属性 + +要同时处理 to 动作关联 node 上的 angle 和 position 属性的自定义时间函数,可以考虑设置 opt.easing 参数。 + +```ts +// 1. 使用内置缓动时间函数 +tween(this.node).to(1, { angle: 90, position: v3(90, 90, 90) }, { + easing: 'backIn', +}).start(); + +// 或者 +// 2. 使用自定义缓动时间函数 +tween(this.node).to(1, { angle: 90, position: v3(90, 90, 90) }, { + easing: (k: number): number => { + if (k === 1) { + return 1; + } + const s = 1.70158; + return k * k * ((s + 1) * k - s); + }, +}).start(); +``` + +### 缓动字符串 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +假设定义一个类: + +```ts +class StringTarget { + string = ''; +} +``` + +#### 用例一 (整型字符串) + +使用 to 接口,在一秒之内,string 属性从 '0' 过度到 '100' + +```ts +const t = new StringTarget(); +t.string = '0'; + +// 这里 string 的值的类型可以是 number 或能转换为 number 的字符串 +tween(t).to(1, { string: 100 }).start(); +tween(t).to(1, { string: '100' }).start(); +``` + +#### 用例二(浮点字符串) + +```ts +const t = new StringTarget(); +t.string = '10'; + +// 从 10 缓动到 110, 缓动过程中 t.string 的值始终保持小数点后两位有效数字 +// 比如:'10.00' -> '43.33' -> '76.67' -> '110.00' +tween(t).to(1, { string: { value: 110, toFixed: 2 } }).start(); // 注意,这里使用 value 表示目标值,toFixed 表示要保留的小数位 +``` + +#### 用例三(自定义处理多个字符串属性) + +```ts +const o = { + gold: "¥0.00", + exp: '1000/1000', + lv: 'Lv.100', + attack: '100 points', + health: '10.00', +}; + +const tweenFormat = { + currency(value: number): TTweenCustomProperty { + return { + value: `¥${value}`, + progress(start: number, end: number, current: string, ratio: number): string { // 自定义 progress 函数 + return `¥${lerp(start, end, ratio).toFixed(2)}`; // 保留小数点后 2 位有效数字 + }, + convert(v: string): number { // 提供自定义转换 string 为 number 的回调方法 + return Number(v.slice(1)); // 字符串开通的 ¥ 为前缀,不参与缓动,因此裁剪它,剩余数字部分转成 number + }, + }; + }, + + health(value: number): TTweenCustomProperty { + // health 为小数点后 2 位浮点,并且没有非数字的前缀与后缀,因此不需要指定自定义 progress 函数,直接使用内置的 progress 函数即可 + return { + value: `${value}`, + toFixed: 2, // 这里需要指定需要保留小数点后 2 位有效数字,若不指定,结果将被设置为整型字符串 + }; + }, + + exp(value: number): TTweenCustomProperty { + return { + value: () => `${value}/1000`, + progress(start: number, end: number, current: string, ratio: number): string { + return `${lerp(start, end, ratio).toFixed(0)}/1000`; + }, + convert(v: string): number { + return Number(v.slice(0, v.indexOf('/'))); + }, + // exp 为整型字符串,因此不需要指定 toFixed 参数 + }; + }, + + lv(value: number): TTweenCustomProperty { + return { + value: `Lv.${value}`, + progress(start: number, end: number, current: string, ratio: number): string { + return `Lv.${lerp(start, end, ratio).toFixed(0)}`; + }, + convert(v: string): number { + return Number(v.slice(v.indexOf('.') + 1)); + }, + }; + }, +}; + +tween(o).to(1, { + gold: tweenFormat.currency(100), + health: tweenFormat.health(1), + exp: tweenFormat.exp(0), + lv: tweenFormat.lv(0), +}).start(); +``` + +### 自定义任意类型对象的缓动流程 + +```ts +class MyProp { + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + } + + public static lerp (a: MyProp, b: MyProp, out: MyProp, t: number): MyProp { + const x = a.x; + const y = a.y; + out.x = x + t * (b.x - x); + out.y = y + t * (b.y - y); + return out; + } + + public static add (a: MyProp, b: MyProp): MyProp { + const out = new MyProp(); + out.x = a.x + b.x; + out.y = a.y + b.y; + return out; + } + + public static sub (a: MyProp, b: MyProp): MyProp { + const out = new MyProp(); + out.x = a.x - b.x; + out.y = a.y - b.y; + return out; + } + + clone(): MyProp { + return new MyProp(this.x, this.y); + } + + equals (other: MyProp, epsilon = EPSILON): boolean { + return ( + Math.abs(this.x - other.x) <= epsilon * Math.max(1.0, Math.abs(this.x), Math.abs(other.x)) + && Math.abs(this.y - other.y) <= epsilon * Math.max(1.0, Math.abs(this.y), Math.abs(other.y)) + ); + } + + x = 0; + y = 0; +} + +class MyObject { + angle = 0; + str = ''; + private _myProp = new MyProp(); + + set myProp(v) { + this._myProp.x = v.x; + this._myProp.y = v.y; + } + + get myProp() { + return this._myProp; + } +} + +const o = new MyObject(); +o.myProp.x = 1; +o.myProp.y = 1; + +tween(o) + .by(1, { myProp: { + value: new MyProp(100, 100), // 目标值 + progress: MyProp.lerp, // 提供自定义对象缓动过程 + clone: v => v.clone(), // 提供克隆函数 + add: MyProp.add, // 如果用 by 动作,则需要提供 add 方法 + sub: MyProp.sub, // 如果用 by 动作,并且有 reverse 操作,则除了 add 方法,还需要提供 sub 方法 + legacyProgress: false, // 设置为 false 表示使用新的基于对象参数类型的 progress 回调,即 MyProp.lerp 中的参数类型为 MyProp 对象自身,而不是默认的 number + } }).id(123) + .reverse(123) // 翻转 + .start(); +``` + +## 绑定不同对象 + +开发中使用 `Node` 作为绑定目标的情景会更多一些,代码示例如下: + +```ts +let quat : Quat = new Quat(); +Quat.fromEuler(quat, 0, 90, 0); +tween(this.node) + .to(tweenDuration, { + position: new Vec3(0, 10, 0), // 位置缓动 + scale: new Vec3(1.2, 3, 1), // 缩放缓动 + rotation:quat } // 旋转缓动 + ) + .start(); // 调用 start 方法,开启缓动 +``` + +实际上缓动可以绑定任意对象,代码示例如下: + +```ts +class BindTarget{ + color : Color +} + +let sprite : Sprite = this.node.getComponent(Sprite) ; +let bindTarget : BindTarget = new BindTarget(); +bindTarget.color = Color.BLACK; +tween(bindTarget) + .by( 1.0, { color: Color.RED }, { + onUpdate(tar:BindTarget){ + sprite.color = tar.color; // 设置精灵的为 BindTarget 内的颜色 + } +}) +.start() +``` + +## 多个动作 + +通常来说,一个缓动可由一个或多个 **动作** 组成,`Tween` 维护了一个由多个 **动作** 组成的数据结构用于管理当前缓动内的所有动作。 + +下面代码演示了将物体的位置沿 Y 轴移动 10 个单位后,沿 -Y 轴移动 10 个单位。 + +```ts +let tweenDuration : number = 1.0; +tween(this.node.position) +.to( tweenDuration, new Vec3(0, 10, 0), { + onUpdate : (target:Vec3, ratio:number)=>{ + this.node.position = target; + } +}) +.to( tweenDuration, new Vec3(0, -10, 0), { + onUpdate : (target:Vec3, ratio:number)=>{ + this.node.position = target; + } +}) // 此时 Tween 内的动作数量为 2 +``` + +多个缓动也可使用 `union`、`sequence`、`parallel` 接口来组织。通过提前创建好一些固定的缓动,并使用 `union`、`sequence`、`parallel` 来组合它们从而减少代码的编写。 + +## 整合多个缓动 + + `union` 方法会将当前所有的动作合并为一整个,代码示例如下: + +```ts +let tweenDuration : number = 1.0; +tween(this.node) + .to(tweenDuration, { position:new Vec3(0, 10, 0) }) // 这里以 node 为缓动的目标 + .to(tweenDuration, { position:new Vec3(0, -10, 0) }) // 此时 Tween 内的动作数量为 2 + .union() // 这里会将上述的两个缓动整合成一个,此时 Tween 内的动作数量为 1 + .start(); // 调用 start 方法,开启缓动 +``` + +> [!TIP] +> +> 从 v3.8.4 开始支持:`union(fromId)` + +`union(fromId)` 方法会将从某个标识的的动作到当前的所有动作合并为一个顺序动作(Sequence),常与 `id`、`repeat` 和 `repeatForever` 配合使用,示例代码如下: + +```ts +const node = new Node(); + +tween(node) + .to(1, { scale: new Vec3(10, 10, 10) }) + .by(1, { position: new Vec3(200, 0, 0) }).id(123) // 将 by 动作标识为 123 + .delay(1) // 延时一秒 + .reverse(123) // 翻转标识为 123 的动作,即前面的 by 动作 + .union(123) // 从标识为 123 的动作开始合并 + .repeat(3) // 将前面 union 打包后的动作重复执行 3 次 + .start(); +``` + +## 缓动队列 + +`sequence` 会将传入的缓动转化为队列形式并加入到当前的缓动内,代码示例如下: + +```ts +let tweenDuration: number = 1.0; +let t1 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + +let t2 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }) + +tween(this.node).sequence(t1, t2).start(); // 将 t1 和 t2 两个缓动加入到新的缓动队列内 +``` + +用 [then](#插入缓动) 接口也可以实现一样的效果: + +```ts +let tweenDuration: number = 1.0; +let t1 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + +let t2 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }) + +tween(this.node).then(t1).then(t2).start(); // 先将 t1 缓动加入队列,再将 t2 缓动加入队列 +``` + +## 同时执行多个缓动 + +`parallel` 会将传入的缓动转化为并行形式并加入到当前的缓动内,代码示例如下: + +```ts +let tweenDuration: number = 1.0; +let t1 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + +let t2 = tween(this.node) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }) + +tween(this.node).parallel(t1, t2).start(); // 将 t1 和 t2 转化为并行的缓动并加入当前的缓动 +``` + +## 插入缓动 + +`then` 接口允许传入新的缓动,并将该缓动整合后添加到当前缓动的动作内,代码示例如下: + +```ts +let tweenAfter = tween(this.node) + .to(1.0, { position: new Vec3(0, -10, 0) }) + +tween(this.node) + .by(1.0, { position: new Vec3(0, 10, 0) }) + .then(tweenAfter) + .start(); +``` + +## 延迟执行 + +`delay` 会在 **当前** 的动作 **后** 添加一个延时。 + +注意在下列代码示例中,`delay` 位置不同会造成完全不同的结果: + +- 延迟 1 秒后,开始进行运动,并连续运动两次。 + + ```ts + let tweenDuration: number = 1.0; + tween(this.node) + .delay(1.0) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }) + .start() + ``` + +- 在第一次运动后,会延迟 1 秒再做第二次运动。 + + ```ts + let tweenDuration: number = 1.0; + tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + .delay(1.0) + .to(tweenDuration, { position: new Vec3(0, -10, 0) }) + .start() + ``` + +## 重复执行 + +接口 `repeat` 可以为缓动添加一个重复次数,若 `embedTween` 参数为空,则会使用当前缓动的最后一个动作作为参数。 + +这意味着,如果当前缓动由多个缓动组成,则只会重复 **最后一个**,请注意下面的示例: + +```ts +let tweenDuration: number = 1.0; +tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + .by(tweenDuration, { position: new Vec3(0, -10, 0) }) + .repeat(3) // 注意这里会重复 by 这个缓动 3 次 + .start() +``` + +若第二个参数 `embedTween` 不为空,则会重复嵌入的缓动,代码示例如下: + +```ts +let tweenDuration: number = 1.0; +let embedTween = tween(this.node) + .by(tweenDuration, { position: new Vec3(0, -10, 0) }) + +tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }) + .repeat(3, embedTween) // 这里会重复 embedTween + .start() +``` + +`repeatForever` 接口和 `repeat` 类似,但是会变为永久重复。 + +## 动作标识 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +接口 `id` 用于给前一个动作添加一个 `number` 类型的标识,注意 **标识不要有重复** ,否则缓动系统内部搜索标识的时候将直接使用第一个表示的动作。 + +其经常与 union(fromId), reverse(id), reverse(tween, id) 配合使用。示例代码如下: + +```ts +tween(this.node) + .by(1, { position: new Vec3(100, 0, 0) }).id(123) // 给 by 动作添加标识,其值为 123 + .delay(1) // 延时一秒 + .reverse(123) // 翻转标识为 123 的动作,并将翻转后的动作添加进当前动作队列中,相当于 .by(1, { position: new Vec3(-100, 0, 0) }) + .start(); +``` + +## 翻转动作 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +`reverse` 接口有 3 个重载实现,分别为: + +```ts +// 返回「新创建」的缓动实例,其会翻转当前缓动中的所有动作。 +reverse (): Tween; + +/** + * 翻转当前缓动中特定标识的动作。 + * @param id 要翻转的当前缓动中的动作标识。 + * @return 返回该实例本身,以便于链式调用。 + */ +reverse (id: number): Tween; + +/** + * 翻转特定缓动中特定标识的动作 + * @param otherTween 根据标识在关联的缓动中查找动作 + * @param id 要翻转的动画标识 + * @return 返回该实例本身,以便于链式调用。 + */ +reverse (otherTween: Tween, id?: number): Tween; +``` + +### reverse () 示例 + +翻转当前缓动中的所有动作,并返回 **新创建** 的缓动实例。 + +```ts +const t1 = tween(this.node) + .by(1, { position: new Vec3(100, 0, 0) }) // 相对当前位置,节点 x 坐标 +100 + .by(1, { scale: new Vec3(2, 2, 2) }); // 相对当前缩放,节点整体缩放系数 +2 + +const t2 = t1.reverse(); // 翻转整个 t1 缓动,并返回新的缓动实例 t2,其相当于先将节点缩放系数 -2,再将节点 x 坐标 -100 +// 即上一行代码等价于: +// const t2 = tween(this.node) +// .by(1, { scale: new Vec3(-2, -2, -2) }) +// .by(1, { position: new Vec3(-100, 0, 0) }); + +tween(this.node) + .to(1, { position: new Vec3(200, 0, 0) }) + .then(t2) + .start(); +``` + +### reverse (id) 示例 + +翻转当前缓动中特定标识的动作。 + +```ts +tween(this.node) + .to(1, { scale: new Vec3(10, 10, 10) }) + .by(1, { position: new Vec3(200, 0, 0) }).id(123) // 给 by 动作添加标识 123 + .delay(1) + .reverse(123) // 翻转当前缓动中标识为 123 的动作,相当于 .by(1, { position: new Vec3(-200, 0, 0) }) + .start(); +``` + +### reverse (otherTween, id?) + +- 没有传递 id 时,可以翻转 otherTween 整个缓动,并将缓动的所有动作整合为一个 Sequence 动作,然后将此动作添加到当前缓动的动作队列中。可用如下代码实现 [reverse () 示例](#reverse--示例) 中一样的效果: + +```ts +const t = tween(this.node) + .by(1, { position: new Vec3(100, 0, 0) }) // 相对当前位置,节点 x 坐标 +100 + .by(1, { scale: new Vec3(2, 2, 2) }); // 相对当前缩放,节点整体缩放系数 +2 + +tween(this.node) + .to(1, { position: new Vec3(200, 0, 0) }) + .reverse(t) // 添加一个动作,此动作直接翻转整个缓动 t + .start(); +``` + +- 传递 id 时,用于翻转 otherTween 中某个标识的动作,并将此动作添加到当前缓动的动作队列中。 + +```ts +const t = tween(node) + .parallel( + tween(node).sequence( + tween(node).by(1, { position: new Vec3(100, 0, 0) }).id(123), // 为此 .by 动作添加标识 123 + tween(node).to(1, { position: new Vec3(1000, 0, 0) }), + ), + tween(node).delay(1), + ); + +tween(node) + .to(1, { position: new Vec3(200, 0, 0) }) + .reverse(t, 123) // 只翻转缓动 t 中的标识为 123 的动作,即 t 中的 .by 动作 + .start(); +``` + +## 节点相关的缓动 + +节点相关的方法只适用于 `target` 是 `Node` 的情况。 + +### 显示和隐藏节点 + +`show` 和 `hide` 接口可以控制绑定节点的显示和隐藏,下面示例中,节点会被隐藏并在延迟 1 秒后显示。 + +```ts +tween(this.node) + .hide() + .delay(1.0) + .show() + .start(); +``` + +### 删除节点 + +该方法会产生一个 **删除节点的动作**,该动作会将传入的节点从场景树内删除。 + +在下面的示例中,节点会在延迟 1 秒后从场景内删除。 + +```ts +tween(this.node) + .delay(1.0) + .removeSelf() + .start() +``` + +## 添加回调动作 + +`call` 接口允许给缓动添加一个回调动作,该接口在处理某些异步逻辑时非常有用,示例如下: + +```ts +tween(this.node) + .to(1.0, { position: new Vec3(0, 10, 0)}) + // to 动作完成后会调用该方法 + .call( ()=>{ + console.log("call"); + }) + .start() +``` + +## 设置目标属性 + +通过 `set` 可以设置目标的属性。下面的示例会在延迟 1 秒后将节点设置在 [0, 100, 0] 的位置。 + +```ts +tween(this.node) + .delay(1.0) + .set({ position: new Vec3(0, 100, 0) }) + .start(); +``` + +也可以同时设置多个不同的属性,代码示例如下: + +```ts +tween(this.node) + // 同时设置节点的位置,缩放和旋转 + .set({ position: new Vec3(0, 100, 0), scale: new Vec3(2, 2, 2), rotation: Quat.IDENTITY } ) + .start(); +``` + +## 复制缓动 + +`clone` 方法可将当前的缓动复制到目标参数上,注意在复制时源缓动和目前缓动的绑定对象要类型一致,即 `new Tween(target: T)` 中的 `T` 需要类型一致。代码示例如下: + +```ts +const srcTween = tween(this.node).delay(1.0).by(1.0, { position: new Vec3(0, 10, 0) }) +// 将 ‘srcTween’ 复制到名为 Cone 的节点上 +srcTween.clone(find("Cone")).start(); +``` + +> [!TIP] +> +> 从 v3.8.4 开始支持在 clone 的时候不传递 target 参数,即复制生成的新缓动直接使用原来的 target + +示例如下: + +```ts +const srcTween = tween(this.node).delay(1.0).by(1.0, { position: new Vec3(0, 10, 0) }); +const clonedTween = srcTween.clone(); // clonedTween 的 target 也是 this.node 对象 +``` + +## 为 sequence, parallel, then 接口指定不同目标对象的子缓动 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +如果要在一个缓动链式调用中同时处理 position, contentSize, color 动作,它们的目标分别为:节点、节点上的 UITransform 组件、节点上的 Sprite 组件,可以通过如下代码实现: + +```ts +tween(node) + .parallel( + tween(node).to(1, { position: new Vec3(100, 100, 0) }), + tween(node.getComponent(UITransform) as UITransform).to(1, { contentSize: size(100, 100) }), + tween(node.getComponent(Sprite) as Sprite).to(1, { color: color(100, 100, 100, 100) }), + ) + .start(); +``` + +sequence 和 then 接口也同理。 + +## 停止缓动 + +### stop + +实例方法,用于停止指定的缓动。 + +```ts +const t = tween(node) + .by(1, { position: v3(90, 90, 90) }); + .start(); // 开始缓动 + +// ...... + +t.stop(); // 停止缓动 +``` + +### stopAll + +**静态方法**,停止所有缓动。 + +```ts +Tween.stopAll(); +``` + +### stopAllByTag + +**静态方法**,停止所有指定标签的缓动。 + +- 不带 target 参数 + +```ts +const MY_TWEEN_TAG = 123; +const node1 = new Node(); +const node2 = new Node(); + +const t1 = tween(node1) + .tag(MY_TWEEN_TAG) + .by(1, { position: new Vec3(100, 0, 0) }) + .start(); + +const t2 = tween(node2) + .tag(MY_TWEEN_TAG) + .by(1, { scale: new Vec3(2, 2, 2) }) + .start(); + +Tween.stopAllByTag(MY_TWEEN_TAG); // t1 和 t2 的标签都是 MY_TWEEN_TAG,t1 和 t2 都将被停止 +``` + +- 带 target 参数 + +```ts +const MY_TWEEN_TAG = 123; +const node1 = new Node(); +const node2 = new Node(); + +const t1 = tween(node1) + .tag(MY_TWEEN_TAG) + .by(1, { position: new Vec3(100, 0, 0) }) + .start(); + +const t2 = tween(node2) + .tag(MY_TWEEN_TAG) + .by(1, { scale: new Vec3(2, 2, 2) }) + .start(); + +Tween.stopAllByTag(MY_TWEEN_TAG, node1); // t1 和 t2 的标签都是 MY_TWEEN_TAG,只有 t1 被停止,t2 的 target 为 node2,因此 t2 不会被停止 +``` + +### stopAllByTarget + +**静态方法**,停止指定对象的关联的所有缓动实例。 + +```ts +const node1 = new Node(); +const node2 = new Node(); + +const t1 = tween(node1) + .by(1, { position: new Vec3(100, 0, 0) }) + .start(); + +const t2 = tween(node2) + .by(1, { scale: new Vec3(2, 2, 2) }) + .start(); + +const t3 = tween(node1) + .by(1, { angle: 90 }) + .start(); + +Tween.stopAllByTarget(node1); // t1 和 t3 关联了 node1,因此它们将被停止,t2 继续运行 +``` + +## 暂停/恢复缓动 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +手动暂停恢复: + +```ts +const t = tween(this.node) + .by(1, { position: new Vec3(90, 0, 0) }).id(123) + .reverse(123) + .union() + .repeatForever() + .start(); // 开始缓动 + +// ...... +t.pause(); // 暂停缓动 t + +// ...... +t.resume(); // 恢复缓动 t +``` + +> [!IMPORTANT] +> +> 从 v3.8.4 开始,如果缓动目标是 Node 类型,那么缓动会根据 Node 的 active 状态自动暂停恢复。 + +## 缩放缓动时间 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +```ts +tween(this.node) + .to(1, { position: new Vec3(100, 100, 100) }) + .timeScale(0.5) + .start(); +``` + +上面这个例子将 timeScale 设置为 0.5,to 动作的时间为 1,因此最终缓动执行的真实总时长将为 duration / timeScale = 1 / 0.5 = 2 秒。 + +## 获取缓动总时长 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +```ts +const t = tween(this.node) + .to(1, { position: new Vec3(100, 100, 100) }) + .to(1, { scale: new Vec3(2, 2, 2) }) + .start(); + +console.log(t.duration); // 将输出 2 +``` + +## 自定义动作(固定时长) + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +`update` 接口用于添加一个固定时长的自定义动作。 + +其接口声明如下: + +```ts +export type TweenUpdateCallback = (target: T, ratio: number, ...args: Args) => void; + +/** + * 添加一个固定时长的自定义动作。 + * @param duration 缓动时间,单位为秒。 + * @param cb 动作回调函数。 + * @param args 传递给动作回调函数的参数。 + * @return 返回该实例本身,以便于链式调用。 + */ +update (duration: number, cb: TTweenUpdateCallback, ...args: Args): Tween { ... } +``` + +示例代码: + +```ts +let done = false; + +tween(this.node) + .delay(1) + .by(1, { position: v3(90, 90, 90) }) + .update(1, (target: Node, ratio: number, a: number, b: boolean, c: string, d: { 'world': () => number }): void =>{ + // ...... + // target, a, b, c, d 都是透传给 update 回调的参数,可指定 n 个参数 + // target 为 this.node + // a 为 123 + // b 为 true + // c 为 'hello' + // d 为 { 'world': (): number => 456 } + }, 123, true, 'hello', { 'world': (): number => 456 }) + .repeat(2) + .call(()=>{ done = true; }) + .start(); +``` + +## 自定义动作(不确定时长) + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +`updateUntil` 接口用于添加一个不确定时长的自定义动作。 + +其接口声明如下: + +```ts +export type TweenUpdateUntilCallback = (target: T, dt: number, ...args: Args) => boolean; + +/** + * 添加一个不确定时长的自定义动作。如果回调函数返回 true,表示当前动作结束。 + * @param cb 动作回调函数。如果回调函数返回 true,表示当前动作结束。 + * @param args 传递给动作回调函数的参数。 + * @return 返回该实例本身,以便于链式调用。 + */ +updateUntil (cb: TweenUpdateUntilCallback, ...args: Args): Tween { ... } +``` + +以下示例代码,用于追踪动态物体,当接近物体时候,放大并重置位置后重复执行。 + +```ts +import { _decorator, Component, Node, tween, v3, Vec3 } from 'cc'; +const { ccclass, property } = _decorator; + +const positionTmp = new Vec3(); +const positionTmp2 = new Vec3(); + +@ccclass('TweenTest') +export class TweenTest extends Component { + + @property(Node) + targetNode: Node | null = null; + + start() { + tween(this.node) + .updateUntil((curNode: Node, dt: number)=>{ + const d = Vec3.copy(positionTmp2, this.targetNode!.position).subtract(curNode.position); + const length = d.length(); + if (length < 10) { + return true; // 当前节点与目标节点距离小于 10 的时候,返回 true 表示为当前 updateUntil 过程结束 + } + + const newPos = Vec3.copy(positionTmp, curNode.position).add(d.normalize().multiplyScalar(length / 10 * dt * 10)); + curNode.setPosition(newPos); + return false; // 返回 false 表示 updateUntil 过程还需要继续执行 + }) + .by(0.25, { scale: v3(1, 1, 0) }, { easing: 'cubicInOut' }).id(1) + .reverse(1) + .call((curNode?: Node)=>{ + const newPos = v3((Math.random() - 0.5) * 400, (Math.random() - 0.5) * 400, 0); + if (newPos.y < 100 && newPos.y > 0) newPos.y = 100; + if (newPos.y > -100 && newPos.y < 0) newPos.y = -100; + + if (newPos.x < 100 && newPos.x > 0) newPos.x = 100; + if (newPos.x > -100 && newPos.x < 0) newPos.x = -100; + curNode?.setPosition(newPos); + }) + .union() + .repeatForever() + .start(); + + tween(this.node) + .by(1, { angle: 360 }) + .repeatForever() + .start(); + + } +} +``` + +运行结果: + +![](img/updateUntil.gif) + +## 从某个时间开始缓动 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +`start` 接口可接收 startTime 参数,单位为秒,用于从某个时间开始缓动,此时间前的所有缓动将被立马执行完毕。 + +```ts +const t = tween(this.node) + .to(1, { position: new Vec3(100, 100, 100) }) + .call(()=>{}) + .to(1, { scale: new Vec3(2, 2, 2) }) + .start(1); // 从第 1 秒开始 +``` + +上面示例,创建了两个 to 动作,总时长为 2 秒,由于从第 1 秒开始播放缓动,因此 this.node 的位置将立马被设置到 (100, 100, 100),call 的回调也会被立马调用。接下来再经过 1 秒的时间做 scale 放大到 2 的动画。 + +## 销毁 + +### 自动销毁 + +> [!TIP] +> +> 从 v3.8.4 开始支持 + +当缓动目标为 `Node` 时,将会监听其销毁事件进行缓动的自动销毁,调用 `target` 方法也会自动更新监听。 + +### 手动销毁 + +大部分缓动在最后一个动作完毕后,都会对自身进行销毁,但是对于未能正确销毁的缓动, 如 `repeatForever` 在切换场景后,会一直驻留在内存中。需要手动调用销毁接口进行销毁。 + +如果要停止并销毁缓动,有下列的方法: + +- 成员 `stop` 接口,销毁该缓动,代码示例如下: + + ```ts + let t = tween(this.node.position) + .to( 1.0, new Vec3(0, 10, 0), { + onUpdate : (target:Vec3, ratio:number)=>{ + this.node.position = target; + } + }) + t.stop(); + ``` + +- 使用静态接口 `stopAll`、`stopAllByTag` 和 `stopAllByTarget` 销毁所有或特定缓动,代码示例如下: + + ```ts + Tween.stopAll() // 销毁所有缓动 + + Tween.stopAllByTag(0); // 销毁所有以 0 为标签的缓动 + + Tween.stopAllByTarget(this.node); // 销毁该节点上的所有缓动 + ``` + +> **注意**:在 v3.8.4 之前的版本,切换场景时记得停止相应的缓动。v3.8.4 及之后的版本,引擎会自动处理。 diff --git a/versions/4.0/zh/tween/tween-function.md b/versions/4.0/zh/tween/tween-function.md new file mode 100644 index 0000000000..a3f1c5af79 --- /dev/null +++ b/versions/4.0/zh/tween/tween-function.md @@ -0,0 +1,81 @@ +# 缓动函数 + +引擎实现了一系列不同类型的缓动函数,通过这些缓动函数,可以实现不同的实时动画效果。这些缓动函数主要用于 `Tween.to` 和 `Tween.by` 这两个接口中。 + +## 内置缓动函数 + +目前引擎提供的缓动函数如下所示: + +```ts +export type TweenEasing = +'linear' | 'smooth' | 'fade' | 'constant' | +'quadIn' | 'quadOut' | 'quadInOut' | 'quadOutIn' | +'cubicIn' | 'cubicOut' | 'cubicInOut' | 'cubicOutIn' | +'quartIn' | 'quartOut' | 'quartInOut' | 'quartOutIn' | +'quintIn' | 'quintOut' | 'quintInOut' | 'quintOutIn' | +'sineIn' | 'sineOut' | 'sineInOut' | 'sineOutIn' | +'expoIn' | 'expoOut' | 'expoInOut' | 'expoOutIn' | +'circIn' | 'circOut' | 'circInOut' | 'circOutIn' | +'elasticIn' | 'elasticOut' | 'elasticInOut' | 'elasticOutIn' | +'backIn' | 'backOut' | 'backInOut' | 'backOutIn' | +'bounceIn' | 'bounceOut' | 'bounceInOut' | 'bounceOutIn'; +``` + +其缓动效果可参考下图: + +![tweener](img/tweener.png) + +图片源自 [http://hosted.zeh.com.br/tweener/docs/en-us/](http://hosted.zeh.com.br/tweener/docs/en-us/) + +通过 `ITweenOption` 接口,可以修改缓动函数。其代码示例如下: + +```ts +let tweenDuration: number = 1.0; // 缓动的时长 +tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }, { // 这里以node的位置信息坐标缓动的目标 + easing: "backIn", // 缓动函数,可以使用已有的,也可以传入自定义的函数。 + }) + .start(); // 调用 start 方法,开启缓动 +``` + +## ITweenOption 接口说明 + +`ITweenOption` 为缓动的可选属性接口定义。其接口皆为 **可选**,可按需使用。完整示例如下: + +```ts +let tweenDuration: number = 1.0; // 缓动的时长 +tween(this.node) + .to(tweenDuration, { position: new Vec3(0, 10, 0) }, { // 这里以node的位置信息坐标缓动的目标 + easing: "backIn", // 缓动函数,可以使用已有的,也可以传入自定义的函数。 + onStart: (target?: object) => { // 回调,当缓动动作启动时触发。 + + }, + onUpdate: (target: Vec3, ratio: number) => { // onUpdate 接受当前缓动的进度 + this.node.position = target; // 将缓动系统计算出的结果赋予 node 的位置 + }, + onComplete: (target?: object) => { // 回调,当缓动动作更新时触发。 + + }, + progress: (start: number, end: number, current: number, ratio: number): number => { + // 返回自定义插值进度 + return 0.0; + } + }) + .start(); // 调用 start 方法,开启缓动 +``` + +通过 `progress` 接口,也可以自定义缓动的进度,代码示例如下: + +```ts +tween(this.node) + .to(1.0, { + position: new Vec3(0, 100, 0), + }, { + progress: (start: number, end: number, current: number, ratio: number): number => { + return math.lerp(start, end, ratio); + } + }) + .start() +``` + +完整 API 描述请参考: [ITweenOption](%__APIDOC__%/zh/interface/ITweenOption) diff --git a/versions/4.0/zh/tween/tween-interface.md b/versions/4.0/zh/tween/tween-interface.md new file mode 100644 index 0000000000..f2655ea0e1 --- /dev/null +++ b/versions/4.0/zh/tween/tween-interface.md @@ -0,0 +1,107 @@ +# 缓动接口 + +## Tween 属性和接口说明 + +### 接口说明 + +`v3.8.4` 开始支持的接口有:reverse, id, union(fromId?: number), timeScale, pause, resume, pauseAllByTarget, resumeAllByTarget, update, start(time) , duration。 + +| 接口 | 功能说明 | +| :---------------- | :------------------------------------------ | +| **tag** | 为当前缓动添加一个数值类型(`number`)的标签 | +| **to** | 添加一个对属性进行 **绝对值** 计算的间隔动作 | +| **by** | 添加一个对属性进行 **相对值** 计算的间隔动作 | +| **set** | 添加一个 **直接设置目标属性** 的瞬时动作 | +| **delay** | 添加一个 **延迟时间** 的瞬时动作 | +| **call** | 添加一个 **调用回调** 的瞬时动作 | +| **target** | 添加一个 **直接设置缓动目标** 的瞬时动作 | +| **union** | 将之前所有的动作打包成一个,或者将从某个标识开始的动作打包成一个 | +| **then** | **插入一个新缓动到缓动队列中** | +| **repeat** | **执行几次**(此前为重复几次,请及时适配) | +| **repeatForever** | **一直重复执行** | +| **update** | 添加一个自定义动作 | +| id | 设置前一个动作的标识,常与 reverse,union 接口配套使用 | +| **reverse** | 翻转 **另外** 缓动中的 **所有** 或 **某个** 动作,然后添加到 **当前** 缓动中;或者翻转 **当前** 缓动中的 **某个** 动作 | +| **timeScale** | 设置缓动的缩放时间因子,其中 1 表示正常速度(默认值),0.5 表示减速一半,2 表示加速一倍,等等 | +| **sequence** | **添加一个顺序执行的缓动** | +| **parallel** | **添加一个同时进行的缓动** | +| **start** | **启动缓动** 或者从 **某个时间点(单位为:秒)** 开始缓动 | +| **stop** | **停止缓动** | +| **pause** | **暂停缓动** | +| **resume** | **恢复缓动** | +| **clone** | **克隆缓动**,可在克隆的时候重新设置目标对象 | +| **show** | **启用节点链上的渲染,缓动目标需要为 Node** | +| **hide** | **禁用节点链上的渲染,缓动目标需要为 Node** | +| **removeSelf** | **将节点移出场景树,缓动目标需要为 Node** | +| **destroySelf** | **将节点移出场景树,并且调用节点销毁函数,缓动目标需要为 Node** | +| **duration** | 获取当前缓动的总时长,此为 getter 调用方式,即 const d = tweenInstance.duration; | + +### 静态接口说明 + +这些方法为 `Tween` 的静态方法,调用方式示例: + +```ts +Tween.stopAll() +Tween.stopAllByTag(0); +Tween.stopAllByTarget(this.node); +``` + +| 接口 | 功能说明 | +| :--- | :--- | +| **stopAll** | 停止所有缓动
该接口会移除底层所有已注册的缓动动画
**注意**:该方法会影响所有对象 | +| **stopAllByTag** | 停止所有指定标签的缓动
该接口将移除通过 **tag** 方法指定的所有缓动
可通过指定第二个参数 `target?: object` 来指定是否仅移除该对象上带有某个标签的缓动 | +| **stopAllByTarget** | 停止所有指定对象的缓动 | +| **pauseAllByTarget** | 暂停目标对象关联的所有缓动实例 | +| **resumeAllByTarget** | 恢复目标对象关联的所有缓动实例 | + +## 工具函数说明 + +|接口| 功能说明 +|:-- |:--| +|**tween\**| 这是一个工具函数,帮助实例化 `Tween` 类
**注意**:该方法并非 `Tween` 类的成员,开发者也可自行调用 `new Tween(target:T)` 的方式实例化缓动。 + +### 示例 + +这里以一个的 `to` 缓动动画作为示例演示缓动的用法: + +```ts +let tweenDuration : number = 1.0; // 缓动的时长 +tween(this.node.position).to( tweenDuration, new Vec3(0, 10, 0), // 这里以node的位置信息坐标缓动的目标 + { // ITweenOption 的接口实现: + onUpdate : (target:Vec3, ratio:number)=>{ // onUpdate 接受当前缓动的进度 + this.node.position = target; // 将缓动系统计算出的结果赋予 node 的位置 + } +}).start(); // 调用 start 方法,开启缓动 +``` + +或者 + +```ts +let tweenDuration : number = 1.0; // 缓动的时长 +tween(this.node).to( // 这里直接以 node 作为缓动目标 + tweenDuration, + { position: new Vec3(0, 10, 0) } // 创建一个带 position 属性的对象 +).start(); // 调用 start 方法,开启缓动 +``` + +注意:若以 node 作为缓动目标,则不需要使用 onUpdate 中调用 `setPosition` 或者 `position` setter 更新 node 的位置,缓动系统内部会自动调用 `position` setter。 + +更多示例可查看 [缓动示例](tween-example.md) + +## 一些限制 + +为了降低更新 `Node Transform` 信息的频率,`Node` 内部维护了一个 `dirty` 状态,只有在调用了可能会改变 `Node Transform` 信息的接口,才会将 `dirty` 置为需要更新的状态。 + +但目前的接口存在一定的限制,例如:通过 `this.node.position` 获取到的 `position` 是一个通用的 `Vec3`。 + +当执行 `this.node.position.x = 1` 这段代码的时候,只执行了 `position` 的 `getter`,并没有执行 `position` 的 `setter`。由于 `dirty` 并没有更新,便会导致渲染时使用的节点的 `Transform` 信息没有更新。 + +目前,我们也不支持这样的调用,而是鼓励使用 `setPosition` 或 `position` 的 `setter`,如下所示: + +```typescript +let _pos = new Vec3(0, 1, 0); +this.node.position = _pos; // 这里将通过 position 的 setter +this.node.setPosition(_pos); // 这里将通过接口 setPosition +``` + +因此,如果是以 `node.position` 作为缓动目标,需要在 onUpdate 中调用 `setPosition` 或者 `position` setter 更新 node 的位置信息。 diff --git a/versions/4.0/zh/ui-system/components/editor/add-component.png b/versions/4.0/zh/ui-system/components/editor/add-component.png new file mode 100644 index 0000000000..7e986e4986 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/add-component.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/base-component.md b/versions/4.0/zh/ui-system/components/editor/base-component.md new file mode 100644 index 0000000000..0e5712ff8e --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/base-component.md @@ -0,0 +1,11 @@ +# UI 组件 + +一些常用的 UI 控件可通过添加节点的方式来创建。在 **层级管理器** 中点击左上角的 **+** 创建节点按钮,然后选择 **UI** 来创建所需的 UI 节点,相应的 UI 组件便会自动挂载到节点上: + +![create-ui](create-ui.png) + +其它的 UI 组件,可以手动在 **层级管理器** 中选中节点,然后在 **属性检查器** 中点击 **添加组件 -> UI** 的方式来添加: + +![add-component](add-component.png) + +各组件的具体说明,请参考相应的组件说明文档。 diff --git a/versions/4.0/zh/ui-system/components/editor/block-input-events.md b/versions/4.0/zh/ui-system/components/editor/block-input-events.md new file mode 100644 index 0000000000..fdf5ac108d --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/block-input-events.md @@ -0,0 +1,7 @@ +# BlockInputEvents 组件参考 + +BlockInputEvents 组件将拦截所属节点 bounding box 内的所有输入事件(鼠标和触摸),防止输入穿透到下层节点,一般用于上层 UI 的背景。 + +当我们制作一个弹出式的 UI 对话框时,对话框的背景默认不会截获事件。也就是说虽然它的背景挡住了游戏场景,但是在背景上点击或触摸时,下面被遮住的游戏元素仍然会响应点击事件。这时我们只要在背景所在的节点上添加这个组件,就能避免这种情况。 + +该组件没有任何 API 接口,直接添加到场景即可生效。 diff --git a/versions/4.0/zh/ui-system/components/editor/button.md b/versions/4.0/zh/ui-system/components/editor/button.md new file mode 100644 index 0000000000..82a2e890b6 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/button.md @@ -0,0 +1,130 @@ +# Button(按钮)组件参考 + +Button 组件可以响应用户的点击操作,当用户点击 Button 时,Button 自身会有状态变化。另外,Button 还可以让用户在完成点击操作后响应一个自定义的行为。 + +![button.png](./button/button.png) + +![button-color](./button/button-color.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后从 **添加 UI 组件** 中选择 **Button**,即可添加 Button 组件到节点上。 + +按钮的脚本接口请参考 [Button API](%__APIDOC__%/zh/class/Button)。 + +## Button 属性 + +| 属性 | 功能说明 | +| :------------- | :---------- | +| Target | Node 类型,当 Button 发生 Transition 的时候,会相应地修改 Target 节点的 SpriteFrame,颜色或者 Scale。 | +| Interactable | 布尔类型,设为 false 时,则 Button 组件进入禁用状态。 | +| Transition | 枚举类型,包括 NONE、COLOR、SPRITE 和 SCALE。每种类型对应不同的 Transition 设置。详情见下方的 **Button Transition** 部分。 | +| Click Event | 列表类型,默认为空,用户添加的每一个事件由节点引用,组件名称和一个响应函数组成。详情见下方的 **Button 点击事件** 部分。 | + +### Button Transition + +Button 的 Transition 用来指定当用户点击 Button 时的状态表现。目前主要有 NONE、COLOR、SPRITE 和 SCALE。 + +![transition](button/transition.png) + +#### Color Transition + +![color-transition](button/color-transition.png) + +| 属性 | 功能说明 | +| :------------- | :---------- | +| Normal | Button 在 Normal 状态下的颜色。 | +| Pressed | Button 在 Pressed 状态下的颜色。 | +| Hover | Button 在 Hover 状态下的颜色。 | +| Disabled | Button 在 Disabled 状态下的颜色。 | +| Duration | Button 状态切换需要的时间间隔。 | + +#### Sprite Transition + +![sprite-transition](button/sprite-transition.png) + +| 属性 | 功能说明 | +| :------------- | :---------- | +| Normal | Button 在 Normal 状态下的 SpriteFrame。 | +| Pressed | Button 在 Pressed 状态下的 SpriteFrame。 | +| Hover | Button 在 Hover 状态下的 SpriteFrame。 | +| Disabled | Button 在 Disabled 状态下的 SpriteFrame。 | + +#### Scale Transition + +![scaleTransition](button/scaleTransition.png) + +| 属性 | 功能 | +| :------------- | :---------- | +| Duration | Button 状态切换需要的时间间隔。 | +| ZoomScale | 当用户点击按钮后,按钮会缩放到一个值,这个值等于 Button 原始 **scale * zoomScale**,zoomScale 可以为负数 | + +### Button 点击事件 + +Button 目前只支持 Click 事件,即当用户点击并释放 Button 时才会触发相应的回调函数。 + +### 组件事件结构 + +![button-event](button/button-event.png) + +| 属性 | 功能说明 | +| :------------- | :---------- | +| Target | 带有脚本组件的节点。 | +| Component | 脚本组件名称。 | +| Handler | 指定一个回调函数,当用户点击 Button 并释放时会触发此函数。 | +| CustomEventData | 用户指定任意的字符串作为事件回调的最后一个参数传入。 | + +### 通过脚本代码添加回调 + +#### 方法一 + +这种方法添加的事件回调和使用编辑器添加的事件回调是一样的,都是通过代码添加。首先需要构造一个 `EventHandler` 对象,然后设置好对应的 `target`、`component`、`handler` 和 `customEventData` 参数。 + +```ts +import { _decorator, Component, Event, Node, Button, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad () { + const clickEventHandler = new EventHandler(); + clickEventHandler.target = this.node; // 这个 node 节点是你的事件处理代码组件所属的节点 + clickEventHandler.component = 'example';// 这个是脚本类名 + clickEventHandler.handler = 'callback'; + clickEventHandler.customEventData = 'foobar'; + + const button = this.node.getComponent(Button); + button.clickEvents.push(clickEventHandler); + } + + callback (event: Event, customEventData: string) { + // 这里 event 是一个 Touch Event 对象,你可以通过 event.target 取到事件的发送节点 + const node = event.target as Node; + const button = node.getComponent(Button); + console.log(customEventData); // foobar + } +} +``` + +#### 方法二 + +通过 `button.node.on('click', ...)` 的方式来添加,这是一种非常简便的方式,但是该方式有一定的局限性,在事件回调里面无法 +获得当前点击按钮的屏幕坐标点。 + +```ts +// 假设我们在一个组件的 onLoad 方法里面添加事件处理回调,在 callback 函数中进行事件处理 + +import { _decorator, Component, Button } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + @property(Button) + button: Button | null = null; + onLoad () { + this.button.node.on(Button.EventType.CLICK, this.callback, this); + } + + callback (button: Button) { + // 注意这种方式注册的事件,无法传递 customEventData + } +} +``` diff --git a/versions/4.0/zh/ui-system/components/editor/button/button-color.png b/versions/4.0/zh/ui-system/components/editor/button/button-color.png new file mode 100644 index 0000000000..75776eb16e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/button/button-color.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/button/button-event.png b/versions/4.0/zh/ui-system/components/editor/button/button-event.png new file mode 100644 index 0000000000..ac2a24bd96 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/button/button-event.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/button/button.png b/versions/4.0/zh/ui-system/components/editor/button/button.png new file mode 100644 index 0000000000..c67d09e7cc Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/button/button.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/button/color-transition.png b/versions/4.0/zh/ui-system/components/editor/button/color-transition.png new file mode 100644 index 0000000000..0433aa1aac Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/button/color-transition.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/button/scaleTransition.png b/versions/4.0/zh/ui-system/components/editor/button/scaleTransition.png new file mode 100644 index 0000000000..bb18f051bd Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/button/scaleTransition.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/button/sprite-transition.png b/versions/4.0/zh/ui-system/components/editor/button/sprite-transition.png new file mode 100644 index 0000000000..0ce4e9bc67 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/button/sprite-transition.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/button/transition.png b/versions/4.0/zh/ui-system/components/editor/button/transition.png new file mode 100644 index 0000000000..551c0ac71c Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/button/transition.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/canvas.md b/versions/4.0/zh/ui-system/components/editor/canvas.md new file mode 100644 index 0000000000..045a2c341f --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/canvas.md @@ -0,0 +1,22 @@ +# Canvas(画布)组件参考 + +![](canvas/canvas.png) + +**RenderRoot2D** 组件所在的节点是 2D 渲染组件数据收集的入口,而 **Canvas(画布)** 组件继承自 **RenderRoot2D** 组件,所以 **Canvas** 组件也是数据收集入口。场景中 Canvas 节点可以有多个,**所有 2D 渲染元素都必须作为 RenderRoot2D 的子节点才能被渲染**。 + +Canvas 节点除了继承自 RenderRoot2D 的数据入口能力,其本身还作为屏幕适配的重要组件,在游戏制作上面对多分辨率适配也起到关键作用,具体请参考 [多分辨率适配方案](../engine/multi-resolution.md)。Canvas 的设计分辨率和适配方案统一通过 **项目设置** 配置。 + +![](canvas/design-resolution.png) + +Canvas 本身和相机并无关系,其更主要的作用是上文叙述的屏幕适配,所以 Canvas 的渲染只取决于和其节点 layer 对应的 camera。所以可以通过控制 camera 的属性来决定 Canvas 下节点的渲染效果。 + +## Canvas 属性 + +| 属性 | 功能说明 | +| :------------- | :---------- | +| CameraComponent | Canvas 关联的相机,此相机不一定会渲染 Canvas 下内容,可以与 AlignCanvasWithScreen 属性配合自动改变 Camera 的一些参数使其与 Canvas 对齐 +| AlignCanvasWithScreen | Canvas 关联的相机是否要与 Canvas 对齐,如果想要自己控制相机位置请勿勾选此选项(卷轴游戏等) + +## 注意事项 + +如果遇到 UI 渲染出错、花屏、闪屏等现象,请参考 [注意事项](../engine/priority.md#注意事项)。 diff --git a/versions/4.0/zh/ui-system/components/editor/canvas/canvas.png b/versions/4.0/zh/ui-system/components/editor/canvas/canvas.png new file mode 100644 index 0000000000..18378b3de6 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/canvas/canvas.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/canvas/design-resolution.png b/versions/4.0/zh/ui-system/components/editor/canvas/design-resolution.png new file mode 100644 index 0000000000..c0faf90b08 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/canvas/design-resolution.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/create-ui.png b/versions/4.0/zh/ui-system/components/editor/create-ui.png new file mode 100644 index 0000000000..526666a018 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/create-ui.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/editbox.md b/versions/4.0/zh/ui-system/components/editor/editbox.md new file mode 100644 index 0000000000..5c22388404 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/editbox.md @@ -0,0 +1,122 @@ +# EditBox 组件参考 + +EditBox 是一种文本输入组件,该组件让你可以轻松获取用户输入的文本。 + +![editbox](editbox/editbox.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后从 **UI/EditBox** 即可添加 EditBox 组件到节点上。 + +EditBox 的脚本接口请参考 [EditBox API](%__APIDOC__%/zh/class/EditBox)。 + +关于使用可以参考范例 **EditBox**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/12.editbox) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/12.editbox))。 + +## EditBox 属性 + +| 属性 | 功能说明 | +| :------------- | :---------- | +| BackgroundImage | 输入框背景节点上挂载的 Sprite 组件对象 | +| FontColor | 输入框文本的颜色 | +| FontSize | 输入框文本的字体大小 | +| InputFlag | 指定输入标识:可以指定输入方式为密码或者单词首字母大写(仅支持 Android 平台)| +| InputMode | 指定输入模式:ANY 表示多行输入,其它都是单行输入,移动平台上还可以指定键盘样式。 | +| LineHeight | 输入框文本的行高 | +| MaxLength | 输入框最大允许输入的字符个数 | +| Placeholder | 输入框占位符的文本内容 | +| PlaceholderFontColor | 输入框占位符的文本字体颜色 | +| PlaceholderFontSize | 输入框占位符的文本字体大小 | +| PlaceholderLabel | 输入框占位符节点上挂载的 Label 组件对象 | +| ReturnType | 指定移动设备上面回车按钮的样式 +| String | 输入框的初始输入内容,如果为空则会显示占位符的文本 | +| TabIndex | 修改 DOM 输入元素的 tabIndex,这个属性只有在 Web 上面修改有意义。 | +| TextLabel | 输入框输入文本节点上挂载的 Label 组件对象 | + +## EditBox 事件 + +![editbox-event](./editbox/editbox-event.png) + +事件结构参考:[组件事件结构](./button.md#组件事件结构) + +- **Editing Did Began**:该事件在用户点击输入框获取焦点的时候被触发。 +- **Editing Did Ended**:在单行模式下面,一般是在用户按下回车或者点击屏幕输入框以外的地方调用该函数。
如果是多行输入,一般是在用户点击屏幕输入框以外的地方调用该函数。 +- **Text Changed**:该事件在用户每一次输入文字变化的时候被触发。但若是通过 `EditBox.string` 的 `setter` 设置时不会派发该事件。 + +## 详细说明 + +- 如果需要输入密码,则需要把 Input Flag 设置为 password,同时 Input Mode 必须是 Any 之外的选择,一般选择 Single Line。 +- 如果要输入多行,可以把 Input Mode 设置为 Any。 +- 背景图片支持九宫格缩放 + +## 通过脚本代码添加回调 + +### 方法一 + +这种方法添加的事件回调和使用编辑器添加的事件回调是一样的,都是通过代码添加。首先需要构造一个 `EventHandler` 对象,然后设置好对应的 `target`、`component`、`handler` 和 `customEventData` 参数。 + +```ts +import { _decorator, Component, EditBox, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad() { + const editboxEventHandler = new EventHandler(); + editboxEventHandler.target = this.node; // 这个 node 节点是你的事件处理代码组件所属的节点 + editboxEventHandler.component = 'example'; + editboxEventHandler.handler = 'onEditDidBegan'; + editboxEventHandler.customEventData = 'foobar'; + + const editbox = this.node.getComponent(EditBox); + editbox.editingDidBegan.push(editboxEventHandler); + // 你也可以通过类似的方式来注册其它回调函数 + // editbox.editingDidEnded.push(editboxEventHandler); + // editbox.textChanged.push(editboxEventHandler); + // editbox.editingReturn.push(editboxEventHandler); + } + + onEditDidBegan(editbox, customEventData) { + // 这里 editbox 是一个 EditBox 对象 + // 这里的 customEventData 参数就等于你之前设置的 "foobar" + } + // 假设这个回调是给 editingDidEnded 事件的 + onEditDidEnded(editbox, customEventData) { + // 这里 editbox 是一个 EditBox 对象 + // 这里的 customEventData 参数就等于你之前设置的 "foobar" + } + // 假设这个回调是给 textChanged 事件的 + onTextChanged(text, editbox, customEventData) { + // 这里的 text 表示修改完后的 EditBox 的文本内容 + // 这里 editbox 是一个 EditBox 对象 + // 这里的 customEventData 参数就等于你之前设置的 "foobar" + } + // 假设这个回调是给 editingReturn 事件的 + onEditingReturn(editbox, customEventData) { + // 这里 editbox 是一个 EditBox 对象 + // 这里的 customEventData 参数就等于你之前设置的 "foobar" + } +} +``` + +### 方法二 + +通过 `editbox.node.on('editing-did-began', ...)` 的方式来添加 + +```ts +// 假设我们在一个组件的 onLoad 方法里面添加事件处理回调,在 callback 函数中进行事件处理: +import { _decorator, Component, EditBox } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + @property(EditBox) + editbox: EditBox | null = null; + onLoad(){ + this.editbox.node.on('editing-did-began', this.callback, this); + } + + callback(editbox: EditBox){ + // 回调的参数是 editbox 组件,注意这种方式注册的事件,无法传递 customEventData + } +} +``` + +同样的,你也可以注册 `editing-did-ended`、`text-changed` 和 `editing-return` 事件,这些事件的回调函数的参数与 `editing-did-began` 的参数一致。 diff --git a/versions/4.0/zh/ui-system/components/editor/editbox/editbox-event.png b/versions/4.0/zh/ui-system/components/editor/editbox/editbox-event.png new file mode 100644 index 0000000000..63ac407fa3 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/editbox/editbox-event.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/editbox/editbox.png b/versions/4.0/zh/ui-system/components/editor/editbox/editbox.png new file mode 100644 index 0000000000..f0eeee963e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/editbox/editbox.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics.md b/versions/4.0/zh/ui-system/components/editor/graphics.md new file mode 100644 index 0000000000..2f22649825 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics.md @@ -0,0 +1,65 @@ +# Graphics 组件参考 + +Graphics 组件提供了一系列绘画接口,这些接口参考了 Canvas 的绘画接口来进行实现。 + +![graphics](graphics/graphics.png) + +新建一个空节点,然后点击 **属性检查器** 下方的 **添加组件** 按钮,从 **UI/Render** 中选择 **Graphics**,即可添加 Graphics 组件到节点上。 + +Graphics 脚本接口请参考 [Graphics API](%__APIDOC__%/zh/class/GraphicsComponent)。 + +关于使用可以参考范例 **Graphics**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/14.graphics) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/14.graphics))。 + +## Graphics 组件属性 + +| 属性 | 功能说明 | +| :------------- | :---------- | +| [CustomMaterial](../engine/ui-material.md)| 自定义材质,可用于实现溶解、外发光等渲染效果。 | +| [LineWidth](graphics/lineWidth.md) | 设置或返回当前的线条宽度 | +| [LineJoin](graphics/lineJoin.md) | 设置或返回两条线相交时,所创建的拐角类型 | +| [LineCap](graphics/lineCap.md) | 设置或返回线条的结束端点样式 | +| [StrokeColor](graphics/strokeColor.md) | 设置或返回笔触的颜色 | +| [FillColor](graphics/fillColor.md) | 设置或返回填充绘画的颜色 | +| [MiterLimit](graphics/miterLimit.md) | 设置或返回最大斜接长度 | + +## 绘图接口 + +### 路径 + +| 方法 | 功能说明 | +| :------------- | :---------- | +| [moveTo](graphics/moveTo.md) (x, y) | 把路径移动到画布中的指定点,不创建线条 | +| [lineTo](graphics/lineTo.md) (x, y) | 添加一个新点,然后在画布中创建从该点到最后指定点的线条 | +| [bezierCurveTo](graphics/bezierCurveTo.md) (c1x, c1y, c2x, c2y, x, y) | 创建三次方贝塞尔曲线 | +| [quadraticCurveTo](graphics/quadraticCurveTo.md) (cx, cy, x, y) | 创建二次贝塞尔曲线 | +| [arc](graphics/arc.md) (cx, cy, r, startAngle, endAngle, counterclockwise) | 创建弧/曲线(用于创建圆形或部分圆) | +| [ellipse](graphics/ellipse.md) (cx, cy, rx, ry) | 创建椭圆 | +| [circle](graphics/circle.md) (cx, cy, r) | 创建圆形 | +| [rect](graphics/rect.md) (x, y, w, h) | 创建矩形 | +| [close](graphics/close.md) () | 创建从当前点回到起始点的路径 | +| [stroke](graphics/stroke.md) () | 绘制已定义的路径 | +| [fill](graphics/fill.md) () | 填充当前绘图(路径) | +| [clear](graphics/clear.md) () | 清除所有路径 | + +### 通过脚本代码设置绘制图案 + +```ts +import { _decorator, Component, Graphics } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Example') +export class Example extends Component { + start () { + const g = this.getComponent(Graphics); + g.lineWidth = 10; + g.fillColor.fromHEX('#ff0000'); + g.moveTo(-40, 0); + g.lineTo(0, -80); + g.lineTo(40, 0); + g.lineTo(0, 80); + g.close(); + g.stroke(); + g.fill(); + } +} +``` diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/arc.md b/versions/4.0/zh/ui-system/components/editor/graphics/arc.md new file mode 100644 index 0000000000..b0bf30321c --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/arc.md @@ -0,0 +1,27 @@ +# arc + +`arc()` 方法创建弧/曲线(用于创建圆或部分圆)。 +提示:如需通过 arc() 来创建圆,请把起始角设置为 0,结束角设置为 2*Math.PI。 + +| 参数 | 描述 +| :-------------- | :----------- | +|x | 中心控制点的坐标 x 坐标。 +|y | 中心控制点的坐标 y 坐标。 +|r | 圆弧弧度。 +|startAngle | 开始弧度,从正 x 轴顺时针方向测量。 +|endAngle | 结束弧度,从正 x 轴顺时针方向测量。 +|counterclockwise | 确定绘图方向:
当设置为 `False` 时为 **顺时针**(默认)
当设置为 `true` 时为 **逆时针** + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.arc(100,75,50,0,1.5*Math.PI); +ctx.stroke(); +``` + + + +


+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/arc.png b/versions/4.0/zh/ui-system/components/editor/graphics/arc.png new file mode 100644 index 0000000000..991ae59d29 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/arc.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/bezierCurveTo.md b/versions/4.0/zh/ui-system/components/editor/graphics/bezierCurveTo.md new file mode 100644 index 0000000000..38945b1de8 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/bezierCurveTo.md @@ -0,0 +1,29 @@ +# bezierCurveTo + +`bezierCurveTo()` 方法通过使用表示三次贝塞尔曲线的指定控制点,向当前路径添加一个点。 + +**注意**:三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点,第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。 + +| 参数 | 描述 +| :-------------- | :----------- | +|cp1x | 第一个贝塞尔控制点的 x 坐标 +|cp1y | 第一个贝塞尔控制点的 y 坐标 +|cp2x | 第二个贝塞尔控制点的 x 坐标 +|cp2y | 第二个贝塞尔控制点的 y 坐标 +|x | 结束点的 x 坐标 +|y | 结束点的 y 坐标 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20,20); +ctx.bezierCurveTo(20,100,200,100,200,20); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/bezierCurveTo.png b/versions/4.0/zh/ui-system/components/editor/graphics/bezierCurveTo.png new file mode 100644 index 0000000000..886fbc3693 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/bezierCurveTo.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/circle.md b/versions/4.0/zh/ui-system/components/editor/graphics/circle.md new file mode 100644 index 0000000000..36dea14718 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/circle.md @@ -0,0 +1,23 @@ +# circle + +`circle()` 方法创建圆形。 + +| 参数 | 描述 | +| :-------------- | :----------- | +| cx | 圆的中心的 x 坐标。| +| cy | 圆的中心的 y 坐标。| +| r | 圆的半径。| + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.circle(200,200, 200); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/circle.png b/versions/4.0/zh/ui-system/components/editor/graphics/circle.png new file mode 100644 index 0000000000..ca3afb34d8 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/circle.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/clear.md b/versions/4.0/zh/ui-system/components/editor/graphics/clear.md new file mode 100644 index 0000000000..cae6f29383 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/clear.md @@ -0,0 +1,19 @@ +# clear + +`clear()` 清空所有路径。 + +## 实例 + +```ts +update: function (dt) { + const ctx = node.getComponent(Graphics); + ctx.clear(); + ctx.circle(200,200, 200); + ctx.stroke(); +} + +``` + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/close.md b/versions/4.0/zh/ui-system/components/editor/graphics/close.md new file mode 100644 index 0000000000..a6e250152a --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/close.md @@ -0,0 +1,21 @@ +# close + +`close()` 方法创建从当前点到开始点的路径。 + + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20,20); +ctx.lineTo(20,100); +ctx.lineTo(70,100); +ctx.close(); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/close.png b/versions/4.0/zh/ui-system/components/editor/graphics/close.png new file mode 100644 index 0000000000..629fc0ddc1 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/close.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/ellipse.md b/versions/4.0/zh/ui-system/components/editor/graphics/ellipse.md new file mode 100644 index 0000000000..9141e4f17d --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/ellipse.md @@ -0,0 +1,24 @@ +# ellipse + +`ellipse()` 方法创建椭圆。 + +| 参数 | 描述 +| :-------------- | :----------- | +|cx | 圆的中心的 x 坐标。 +|cy | 圆的中心的 y 坐标。 +|rx | 圆的 x 半径。 +|ry | 圆的 y 半径。 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.ellipse(200,100, 200,100); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/ellipse.png b/versions/4.0/zh/ui-system/components/editor/graphics/ellipse.png new file mode 100644 index 0000000000..b376bd1d62 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/ellipse.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/fill.md b/versions/4.0/zh/ui-system/components/editor/graphics/fill.md new file mode 100644 index 0000000000..0c901477d3 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/fill.md @@ -0,0 +1,19 @@ +# fill + +`fill()` 方法填充当前的图像(路径)。默认颜色是白色 +注释:如果路径未关闭,那么 fill() 方法会从路径结束点到开始点之间添加一条线,以关闭该路径,然后填充该路径。 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.rect(20,20,150,100); +ctx.fillColor = Color.GREEN; +ctx.fill(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/fill.png b/versions/4.0/zh/ui-system/components/editor/graphics/fill.png new file mode 100644 index 0000000000..e0f8558284 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/fill.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/fillColor.md b/versions/4.0/zh/ui-system/components/editor/graphics/fillColor.md new file mode 100644 index 0000000000..c1991902b2 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/fillColor.md @@ -0,0 +1,18 @@ +# fillColor + +`fillColor` 属性设置或返回用于填充的颜色。 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.fillColor = new Color().fromHEX('#0000ff'); +ctx.rect(20,20,250,200); +ctx.stroke(); +``` + +![](fillColor.png) + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/fillColor.png b/versions/4.0/zh/ui-system/components/editor/graphics/fillColor.png new file mode 100644 index 0000000000..b2909f54bf Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/fillColor.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/graphics.png b/versions/4.0/zh/ui-system/components/editor/graphics/graphics.png new file mode 100644 index 0000000000..6d86a1521d Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/graphics.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/lineCap.md b/versions/4.0/zh/ui-system/components/editor/graphics/lineCap.md new file mode 100644 index 0000000000..d04180506e --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/lineCap.md @@ -0,0 +1,27 @@ +# lineCap + +`lineCap` 属性设置或返回线条末端线帽的样式。 + +| 参数 | 描述 +| :-------------- | :----------- | +|Graphics.LineCap.BUTT | 默认。向线条的每个末端添加平直的边缘。 +|Graphics.LineCap.ROUND | 向线条的每个末端添加圆形线帽。 +|Graphics.LineCap.SQUARE | 向线条的每个末端添加正方形线帽。 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.lineCap = Graphics.LineCap.ROUND; +ctx.lineWidth = 10; +ctx.moveTo(100, 100); +ctx.lineTo(300, 100); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) + diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/lineCap.png b/versions/4.0/zh/ui-system/components/editor/graphics/lineCap.png new file mode 100644 index 0000000000..1791251ea7 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/lineCap.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/lineJoin.md b/versions/4.0/zh/ui-system/components/editor/graphics/lineJoin.md new file mode 100644 index 0000000000..bea330d239 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/lineJoin.md @@ -0,0 +1,26 @@ +# lineJoin + +`lineJoin` 属性设置或返回线条末端线帽的样式。 + +| 参数 | 描述 +| :-------------- | :----------- | +|Graphics.LineJoin.BEVEL | 创建斜角。 +|Graphics.LineJoin.ROUND | 创建圆角。 +|Graphics.LineJoin.MITER | 默认。创建尖角。 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.lineJoin = Graphics.LineJoin.ROUND; +ctx.moveTo(20,20); +ctx.lineTo(100,50); +ctx.lineTo(20,100); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/lineJoin.png b/versions/4.0/zh/ui-system/components/editor/graphics/lineJoin.png new file mode 100644 index 0000000000..1e4a39e6b2 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/lineJoin.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/lineTo.md b/versions/4.0/zh/ui-system/components/editor/graphics/lineTo.md new file mode 100644 index 0000000000..7ddb35703b --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/lineTo.md @@ -0,0 +1,24 @@ +# lineTo + +`lineTo()` 方法添加一个新点,然后创建从该点到画布中最后指定点的线条。 + +| 参数 | 描述 +| :-------------- | :----------- | +| x | 路径的目标位置的 x 坐标 +| y | 路径的目标位置的 y 坐标 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20,100); +ctx.lineTo(20,20); +ctx.lineTo(70,20); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/lineTo.png b/versions/4.0/zh/ui-system/components/editor/graphics/lineTo.png new file mode 100644 index 0000000000..6fb8b8e6db Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/lineTo.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/lineWidth.md b/versions/4.0/zh/ui-system/components/editor/graphics/lineWidth.md new file mode 100644 index 0000000000..6bfbe90082 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/lineWidth.md @@ -0,0 +1,22 @@ +# lineWidth + +`lineWidth()` 方法添加一个新点,然后创建从该点到画布中最后指定点的线条。 + +| 参数 | 描述 +| :-------------- | :----------- | +|number | 当前线条的宽度,以像素计。 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.lineWidth = 20; +ctx.rect(20,20,80,100); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/lineWidth.png b/versions/4.0/zh/ui-system/components/editor/graphics/lineWidth.png new file mode 100644 index 0000000000..b7c36f5d0d Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/lineWidth.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/miterLimit.md b/versions/4.0/zh/ui-system/components/editor/graphics/miterLimit.md new file mode 100644 index 0000000000..824c8ce2a5 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/miterLimit.md @@ -0,0 +1,29 @@ +# miterLimit + +`miterLimit` 属性设置或返回最大斜接长度。斜接长度指的是在两条线交汇处内角和外角之间的距离。 + +**注意**:只有当 lineJoin 属性为 `miter` 时,miterLimit 才有效。 + +边角的角度越小,斜接长度就会越大。为了避免斜接长度过长,我们可以使用 miterLimit 属性。如果斜接长度超过 miterLimit 的值,边角会以 lineJoin 的 `bevel` 类型来显示 + +| 参数 | 描述 +| :-------------- | :----------- | +|number | 正数。规定最大斜接长度。如果斜接长度超过 miterLimit 的值,边角会以 lineJoin 的 `bevel` 类型来显示。 | + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.miterLimit = 10; +ctx.moveTo(20,20); +ctx.lineTo(100,50); +ctx.lineTo(20,100); +ctx.stroke(); +``` + + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/miterLimit.png b/versions/4.0/zh/ui-system/components/editor/graphics/miterLimit.png new file mode 100644 index 0000000000..b67cd1d12d Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/miterLimit.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/moveTo.md b/versions/4.0/zh/ui-system/components/editor/graphics/moveTo.md new file mode 100644 index 0000000000..494eb7dddc --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/moveTo.md @@ -0,0 +1,23 @@ +# moveTo + +`moveTo` 表示一条路径的起点。 + +| 参数 | 描述 +| :-------------- | :----------- | +| x | 路径的目标位置的 x 坐标 +| y | 路径的目标位置的 y 坐标 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(0,0); +ctx.lineTo(300,150); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/moveTo.png b/versions/4.0/zh/ui-system/components/editor/graphics/moveTo.png new file mode 100644 index 0000000000..7cb6a62156 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/moveTo.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/quadraticCurveTo.md b/versions/4.0/zh/ui-system/components/editor/graphics/quadraticCurveTo.md new file mode 100644 index 0000000000..3b23cd3bd0 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/quadraticCurveTo.md @@ -0,0 +1,26 @@ +# quadraticCurveTo + +`quadraticCurveTo()` 方法通过使用表示二次贝塞尔曲线的指定控制点,向当前路径添加一个点。 +提示:二次贝塞尔曲线需要两个点。第一个点是用于二次贝塞尔计算中的控制点,第二个点是曲线的结束点。曲线的开始点是当前路径中最后一个点 + +| 参数 | 描述 +| :-------------- | :----------- | +|cpx | 贝塞尔控制点的 x 坐标 +|cpy | 贝塞尔控制点的 y 坐标 +|x | 结束点的 x 坐标 +|y | 结束点的 y 坐标 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20,20); +ctx.quadraticCurveTo(20,100,200,20); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/quadraticCurveTo.png b/versions/4.0/zh/ui-system/components/editor/graphics/quadraticCurveTo.png new file mode 100644 index 0000000000..c848e44ecd Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/quadraticCurveTo.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/rect.md b/versions/4.0/zh/ui-system/components/editor/graphics/rect.md new file mode 100644 index 0000000000..27b75d59be --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/rect.md @@ -0,0 +1,48 @@ +# rect + +`rect()` 方法创建矩形。 + +| 参数 | 描述 +| :-------------- | :----------- | +|x | 矩形左下点的 x 坐标。 +|y | 矩形左下点的中心的 y 坐标。 +|width | 矩形的宽度。 +|height | 矩形的高度。 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.rect(20,20,150,100); +ctx.stroke(); +``` + + + +```ts +const ctx = node.getComponent(Graphics); + +// 红色矩形 +ctx.lineWidth = 6; +ctx.strokeColor = Color.RED; +ctx.rect(5,5,290,140); +ctx.stroke(); + +// 绿色矩形 +ctx.lineWidth=4; +ctx.strokeColor = Color.GREEN; +ctx.rect(30,30,50,50); +ctx.stroke(); + +// 蓝色矩形 +ctx.lineWidth = 10; +ctx.strokeColor = Color.BLUE; +ctx.rect(50,50,150,80); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/rect.png b/versions/4.0/zh/ui-system/components/editor/graphics/rect.png new file mode 100644 index 0000000000..1d3f0513dd Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/rect.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/rect2.png b/versions/4.0/zh/ui-system/components/editor/graphics/rect2.png new file mode 100644 index 0000000000..0a7e468377 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/rect2.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/stroke.md b/versions/4.0/zh/ui-system/components/editor/graphics/stroke.md new file mode 100644 index 0000000000..4bd30e6702 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/stroke.md @@ -0,0 +1,19 @@ +# stroke + +`stroke()` 方法会实际地绘制出通过 moveTo() 和 lineTo() 等路径方法定义的路径。默认颜色是黑色。 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.moveTo(20, 100); +ctx.lineTo(20, 20); +ctx.lineTo(70, 20); +ctx.stroke(); +``` + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/stroke.png b/versions/4.0/zh/ui-system/components/editor/graphics/stroke.png new file mode 100644 index 0000000000..72c89a8428 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/stroke.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/strokeColor.md b/versions/4.0/zh/ui-system/components/editor/graphics/strokeColor.md new file mode 100644 index 0000000000..da59b00619 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/graphics/strokeColor.md @@ -0,0 +1,20 @@ +# strokeColor + +`strokeColor` 属性设置或返回用于笔触的颜色。 + +## 实例 + +```ts +const ctx = node.getComponent(Graphics); +ctx.lineWidth = 2; +ctx.strokeColor = hexToColor('#0000ff'); +ctx.rect(20,20,250,200); +ctx.stroke(); +``` + + + + +
+ +返回 [Graphics 组件参考](../graphics.md) diff --git a/versions/4.0/zh/ui-system/components/editor/graphics/strokeColor.png b/versions/4.0/zh/ui-system/components/editor/graphics/strokeColor.png new file mode 100644 index 0000000000..d68cbd6700 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/graphics/strokeColor.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/label-outline.md b/versions/4.0/zh/ui-system/components/editor/label-outline.md new file mode 100644 index 0000000000..498fad6381 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/label-outline.md @@ -0,0 +1,16 @@ +# LabelOutline 组件参考 + +LabelOutline 组件会为所在节点的 Label 添加描边效果,不支持 BMFont 字体。 + +![](label/labeloutline.png) + +点击 **属性检查器** 下方的 **添加组件** 按钮,然后从 **UI** 中选择 **LabelOutline**,即可添加 LabelOutline 组件到节点上。 + +**注意**: 3.8.2 版本开始 Label 组件已内置描边功能,无需再添加 LabelOutline 组件。 + +## LabelOutline 属性 + +| 属性 | 功能说明 | +| :-------------- | :----------- | +| Color | 字体发光边缘颜色 | +| Width | 字体发光边缘宽度 | diff --git a/versions/4.0/zh/ui-system/components/editor/label-shadow.md b/versions/4.0/zh/ui-system/components/editor/label-shadow.md new file mode 100644 index 0000000000..3e7f2e3fee --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/label-shadow.md @@ -0,0 +1,23 @@ +# LabelShadow 组件参考 + +LabelShadow 组件可以为 Label 组件添加阴影效果。 + +![label-shadow](label/label-shadow.png) + +点击 **属性检查器** 下方的 **添加组件** 按钮,然后从 **UI** 中选择 **LabelShadow**,即可添加 LabelShadow 组件到节点上。 + +> **注意**: +> 1. 3.8.2 版本开始 Label 组件已内置描边功能,无需再添加 LabelOutline 组件。 +> 2. LabelShadow 只能用于 **系统字体** 或者 **TTF 字体**。 +> 3. 当 Label 组件的 **CacheMode** 属性设置为 **CHAR** 时,LabelShadow 不生效。 +> 4. LabelShadow 不支持 Windows 和 Mac 原生 + +LabelShadow 相关脚本接口请参考 [LabelShadow API](%__APIDOC__%/zh/class/LabelShadow)。 + +## LabelShadow 属性 + +| 属性 | 说明 | +| :----- | :------------ | +| Color | 阴影的颜色 | +| Offset | 字体与阴影的偏移 | +| Blur | 阴影的模糊程度 | diff --git a/versions/4.0/zh/ui-system/components/editor/label.md b/versions/4.0/zh/ui-system/components/editor/label.md new file mode 100644 index 0000000000..3110dfa300 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/label.md @@ -0,0 +1,57 @@ +# Label 组件参考 + +Label 组件用来显示一段文字,文字可以是系统字体,TrueType 字体、BMFont 字体或艺术数字。另外,Label 还具有排版功能。 + +![label-property](./label/label-property.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后从 **UI** 中选择 **Label**,即可添加 Label 组件到节点上。 + +Label 的组件接口请参考 [Label API](%__APIDOC__%/zh/result?keyword=Label)。 + +具体使用方法可参考范例 **Label**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/02.label) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/02.label))。 + +## Label 属性 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| CustomMaterial | 自定义材质,使用方法参考 [自定义材质](../engine/ui-material.md) +| Color | 文字颜色。 +| String | 文本内容字符串。 +| HorizontalAlign | 文本的水平对齐方式。可选值有 LEFT、CENTER 和 RIGHT。 +| VerticalAlign | 文本的垂直对齐方式。可选值有 TOP、CENTER 和 BOTTOM。 +| FontSize | 文本字体大小。 +| FontFamily | 文字字体名字。在使用系统字体时生效。 +| LineHeight | 文本的行高。 +| Overflow | 文本的排版方式,目前支持 CLAMP、SHRINK 和 RESIZE_HEIGHT。详情见下方的 [Label 排版](#label-%E6%8E%92%E7%89%88) 或者 [文字排版](../engine/label-layout.md)。 +| EnableWrapText | 是否开启文本换行。(在排版方式设为 CLAMP、SHRINK 时生效) +| Font | 指定文本渲染需要的 [字体资源](../../../asset/font.md)。若要使用艺术数字字体,请参考 [艺术数字资源](../../../asset/label-atlas.md) 文档进行配置。
如果使用系统字体,则此属性可以为空。 +| UseSystemFont | 布尔值,是否使用系统字体。 +| CacheMode | 文本缓存类型,仅对 **系统字体** 或 **TTF** 字体有效,BMFont 字体无需进行这个优化。包括 **NONE**、**BITMAP**、**CHAR** 三种模式。详情见下方的 [文本缓存类型](#%E6%96%87%E6%9C%AC%E7%BC%93%E5%AD%98%E7%B1%BB%E5%9E%8B%EF%BC%88cache-mode%EF%BC%89)。 +| IsBold | 文字是否加粗,支持系统字体以及部分 TTF 字体。当 CacheMode 为 CHAR 模式时暂不生效。 +| IsItalic | 文字是否倾斜,支持系统字体以及 TTF 字体。当 CacheMode 为 CHAR 模式时暂不生效。 +| IsUnderline | 文字是否加下划线,支持系统字以及 TTF 字体。当 CacheMode 为 CHAR 模式时暂不生效。 + + + +## Label 排版 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| CLAMP | 文字尺寸不会根据 Content Size 的大小进行缩放。
Wrap Text 关闭的情况下,按照正常文字排列,超出 Content Size 的部分将不会显示。
Wrap Text 开启的情况下,会试图将本行超出范围的文字换行到下一行。如果纵向空间也不够时,也会隐藏无法完整显示的文字。 +| SHRINK | 文字尺寸会根据 Content Size 大小进行自动缩放(不会自动放大,最大显示 Font Size 规定的尺寸)。
Wrap Text 开启时,当宽度不足时会优先将文字换到下一行,如果换行后还无法完整显示,则会将文字进行自动适配 Content Size 的大小。
Wrap Text 关闭时,则直接按照当前文字进行排版,如果超出边界则会进行自动缩放。 +| RESIZE_HEIGHT | 文本的 Content Size 会根据文字排版进行适配,这个状态下用户无法手动修改文本的高度,文本的高度由内部算法自动计算出来。 + +## 文本缓存类型(Cache Mode) + +| 类型 | 功能说明 +| :-------------- | :----------- | +| NONE | 默认值,Label 中的整段文本将生成一张位图。 +| BITMAP | 选择后,Label 中的整段文本仍将生成一张位图,但是会尽量参与 [动态合图](../../../advanced-topics/dynamic-atlas.md)。只要满足动态合图的要求,就会和动态合图中的其它 Sprite 或者 Label 合并 Draw Call。由于动态合图会占用更多内存,**该模式只能用于文本不常更新的 Label**。此模式在节点安排合理的情况下可大幅降低 Draw Call,请酌情选择使用 +| CHAR | 原理类似 BMFont,Label 将以“字”为单位将文本缓存到全局共享的位图中,相同字体样式和字号的每个字符将在全局共享一份缓存。能支持文本的频繁修改,对性能和内存最友好。不过目前该模式还存在如下限制,我们将在后续的版本中进行优化:
1. **该模式只能用于字体样式和字号(通过记录字体的 fontSize、fontFamily、color、outline 为关键信息,以此进行字符的重复使用,其他有使用特殊自定义文本格式的需要注意)固定,并且不会频繁出现巨量未使用过的字符的 Label**。这是为了节约缓存,因为全局共享的位图尺寸为 **1024 * 1024**,只有场景切换时才会清除,一旦位图被占满后新出现的字符将无法渲染。
2. Overflow 不支持 SHRINK。
3. 不能参与动态合图(同样启用 CHAR 模式的多个 Label 在渲染顺序不被打断的情况下仍然能合并 Draw Call)
4. 目前暂不支持 **IsBold**、**IsItalic** 和 **IsUnderline** 属性。 + +> **注意**:Cache Mode 对所有平台都有优化效果。 + + diff --git a/versions/4.0/zh/ui-system/components/editor/label/label-property.png b/versions/4.0/zh/ui-system/components/editor/label/label-property.png new file mode 100644 index 0000000000..d5829c2a4b Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/label/label-property.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/label/label-shadow.png b/versions/4.0/zh/ui-system/components/editor/label/label-shadow.png new file mode 100644 index 0000000000..de99c0f9c4 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/label/label-shadow.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/label/labeloutline.png b/versions/4.0/zh/ui-system/components/editor/label/labeloutline.png new file mode 100644 index 0000000000..70c95a42eb Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/label/labeloutline.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/layout.md b/versions/4.0/zh/ui-system/components/editor/layout.md new file mode 100644 index 0000000000..48f5600015 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/layout.md @@ -0,0 +1,58 @@ +# Layout 组件参考 + +Layout 是一种容器组件,容器能够开启自动布局功能,自动按照规范排列所有子物体,方便用户制作列表、翻页等功能。 + +![layout](layout/layout.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI -> Layout** 即可添加 Layout 组件到节点上。 + +布局的组件接口请参考 [Layout API](%__APIDOC__%/zh/class/Layout)。 + +具体使用方法可参考范例 **Layout**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/05.layout) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/05.layout))。 + +## Layout 属性 + +| 属性 | 功能说明 | +| :----------------- | :--------- | +| **Type** | 布局类型,支持 **NONE**、**HORIZONTAL**、**VERTICAL** 和 **GRID**。 | +| **ResizeMode** | 缩放模式,支持 **NONE**、**CHILDREN** 和 **CONTAINER**。 | +| **PaddingLeft** | 排版时,子物体相对于容器左边框的距离。 | +| **PaddingRight** | 排版时,子物体相对于容器右边框的距离。 | +| **PaddingTop** | 排版时,子物体相对于容器上边框的距离。 | +| **PaddingBottom** | 排版时,子物体相对于容器下边框的距离。 | +| **SpacingX** | 水平排版时,子物体与子物体在水平方向上的间距。NONE 模式无此属性。 | +| **SpacingY** | 垂直排版时,子物体与子物体在垂直方向上的间距。NONE 模式无此属性。 | +| **HorizontalDirection** | 指定水平排版时,第一个子节点从容器的左边还是右边开始布局。当容器为 `GRID` 类型时,此属性和 `Start Axis` 属性一起决定 Grid 布局元素的起始水平排列方向。 | +| **VerticalDirection** | 指定垂直排版时,第一个子节点从容器的上面还是下面开始布局。当容器为 `GRID` 类型时,此属性和 `Start Axis` 属性一起决定 Grid 布局元素的起始垂直排列方向。 | +| **CellSize** | 此属性只在 `GRID` 布局、`Children` 缩放模式时存在,指定网格容器里面排版元素的大小。 | +| **StartAxis** | 此属性只在 `GRID` 布局时存在,指定网格容器里面元素排版指定的起始方向轴。 | +| **AffectedByScale** | 子节点的缩放是否影响布局。 | +| **AutoAlignment** | 自动对齐,在 `Type` 类型为 **HORIZONTAL** 或 **VERTICAL** 模式下,保证另外一个轴向值始终为 0。 | +| **Constraint** | 布局约束,可以在某个方向上约束排列数量,支持 **NONE**、**FIXED_ROW** 和 **FIXED_COL**。 | +| **ConstraintNum** | 布局约束值,在 `Constraint` 的类型为 **FIXED_ROW** 或 **FIXED_COL** 模式下有效。 | + +## 详细说明 + +添加 Layout 组件之后,默认的布局类型是 **NONE**,可以通过修改 **属性检查器** 里的 `Type` 切换容器排列类型。类型分为 **HORIZONTAL**(水平)、**VERTICAL**(垂直)以及 **GRID**(网格)布局。另外,除了 **NONE** 布局类型,其他都支持 `ResizeMode`。 + +- **ResizeMode** 模式: + + - 设置为 **NONE** 时,子物体和容器的大小变化互不影响。 + + - 设置为 **CHILDREN** 时,子物体大小会随着容器的大小而变化。 + + - 设置为 **CONTAINER** 时,容器的大小会随着子物体的大小变化。 + + 所有的排列都是根据容器大小进行计算的,如果需要固定排序,可以将 Type 设置为 **GRID**,然后设置 `Constraint` 和 `ConstraintNum` 来固定排序。 + +- **Constraint** 模式: + + - 设置为 **NONE** 时,自由布局。 + + - 设置为 **FIXED_ROW** 时,固定行数,搭配 `ConstraintNum` 使用。 + + - 设置为 **FIXED_COL** 时,固定列数,搭配 `ConstraintNum` 使用。 + +> **注意**:Layout 设置后的结果需要到下一帧才会更新,除非你设置完以后手动调用 `updateLayout` API。 + +更多布局案例,详情请参考 [自动布局](../engine/auto-layout.md)。 diff --git a/versions/4.0/zh/ui-system/components/editor/layout/layout.png b/versions/4.0/zh/ui-system/components/editor/layout/layout.png new file mode 100644 index 0000000000..b84347c343 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/layout/layout.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/mask.md b/versions/4.0/zh/ui-system/components/editor/mask.md new file mode 100644 index 0000000000..790f919b08 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/mask.md @@ -0,0 +1,80 @@ +# Mask(遮罩)组件参考 + +Mask 用于规定子节点可渲染的范围,默认带有 Mask 组件的节点会使用该节点的约束框(也就是 **属性检查器** 中 Node 组件的 **ContentSize** 规定的范围)创建一个矩形渲染遮罩,该节点的所有子节点都会依据这个遮罩进行裁剪,遮罩范围外的将不会渲染。 + +![mask](mask/mask.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后从 **UI** 中选择 **Mask**,即可添加 Mask 组件到节点上。注意该组件不能添加到有其他渲染组件(如 **Sprite**、**Label** 等)的节点上。 + +遮罩的组件接口请参考 [Mask API](%__APIDOC__%/zh/class/Mask)。 + + +添加 Mask 组件后,会自动添加 [Graphics](./graphics.md) 组件,请不要删除该组件。在 Type 为 **SPRITE_STENCIL** 时,Mask 组件会添加 Sprite 组件,该 Sprite 组件会为模板提供形状,同理请勿删除。 + +## Mask 属性 + +| 属性 | 功能说明 | +| :------------- | :---------- | +| Type | 遮罩类型。包括 **RECT**、**ELLIPSE**、**GRAPHICS_STENCIL**、**SPRITE_STENCIL** 四种类型。| +| Segments | 椭圆遮罩的曲线细分数,只在遮罩类型设为 **ELLIPSE** 时生效 | +| Inverted | 反向遮罩 | + +### Type + +#### RECT 矩形遮罩 + +![mask](mask/mask-rect.png) + +#### ELLIPSE 椭圆遮罩 + +![mask](mask/mask-ellipse.png) + +可以在运行时通过代码设置: + +```ts +const mask = this.getComponent(Mask); +mask.type = Mask.Type.ELLIPSE; +mask.segments = 32; +``` + +#### GRAPHICS_STENCIL 自定义图形遮罩 + +![mask](mask/mask-graphics.png) + +可以在运行时通过代码设置: + +```ts +const g = this.mask.node.getComponent(Graphics); +//const g = this.mask.graphics; +g.lineWidth = 10; +g.fillColor.fromHEX('#ff0000'); +g.moveTo(-40, 0); +g.lineTo(0, -80); +g.lineTo(40, 0); +g.lineTo(0, 80); +g.close(); +g.stroke(); +g.fill(); +``` + +#### SPRITE_STENCIL 自定义图片遮罩 + +![mask](mask/mask-image.png) + +可以在运行时通过代码设置: + +```ts +const mask = this.getComponent(Mask); +mask.type = Mask.Type.SPRITE_STENCIL; +const sprite = this.getComponent(Sprite); +sprite.spriteFrame = this.stencilSprite; +mask.alphaThreshold = 0.1; +``` + +**注意**: + +1. 节点添加了 Mask 组件之后,所有在该节点下的子节点,在渲染的时候都会受 Mask 影响。 + +2. **GRAPHICS_STENCIL** 类型在这里没有做任何引擎需要的事,只是放开了对 graphics 操控,用户可以使用 mask 组件里的 graphics 属性来绘制自定义图形,但是节点的点击事件还是根据节点的尺寸来计算。 + +3. **SPRITE_STENCIL** 类型默认需要一个图片资源,如果没有设置的话,相当于没有遮罩。 diff --git a/versions/4.0/zh/ui-system/components/editor/mask/mask-ellipse.png b/versions/4.0/zh/ui-system/components/editor/mask/mask-ellipse.png new file mode 100644 index 0000000000..fe95ed3311 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/mask/mask-ellipse.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/mask/mask-graphics.png b/versions/4.0/zh/ui-system/components/editor/mask/mask-graphics.png new file mode 100644 index 0000000000..ffa6be0cae Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/mask/mask-graphics.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/mask/mask-image.png b/versions/4.0/zh/ui-system/components/editor/mask/mask-image.png new file mode 100644 index 0000000000..e6d6174e26 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/mask/mask-image.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/mask/mask-rect.png b/versions/4.0/zh/ui-system/components/editor/mask/mask-rect.png new file mode 100644 index 0000000000..4a754cc1f9 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/mask/mask-rect.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/mask/mask.png b/versions/4.0/zh/ui-system/components/editor/mask/mask.png new file mode 100644 index 0000000000..2640bf6d59 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/mask/mask.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/pageview.md b/versions/4.0/zh/ui-system/components/editor/pageview.md new file mode 100644 index 0000000000..33d9b8dc4d --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/pageview.md @@ -0,0 +1,113 @@ +# PageView 组件参考 + +PageView 是一种页面视图容器. + +![pageview-inspector](./pageview/pageview-inspector.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/PageView** 即可添加 PageView 组件到节点上。 + +页面视图的脚本接口请参考 [PageView API](%__APIDOC__%/zh/class/PageView)。 + +关于使用可以参考范例 **PageView**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/15.pageview) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/15.pageview))。 + +## PageView 属性 + +| 属性 | 功能说明 | +| :------------- | :---------- | +| SizeMode | 页面视图中每个页面大小类型,目前有 Unified 和 Free 类型。 | +| Content | 它是一个节点引用,用来创建 PageView 的可滚动内容 | +| Direction | 页面视图滚动方向 | +| ScrollThreshold | 滚动临界值,默认单位百分比,当拖拽超出该数值时,松开会自动滚动下一页,小于时则还原 | +| AutoPageTurningThreshold | 快速滑动翻页临界值,当用户快速滑动时,会根据滑动开始和结束的距离与时间计算出一个速度值,该值与此临界值相比较,如果大于临界值,则进行自动翻页 | +| Inertia | 否开启滚动惯性 | +| Brake | 开启惯性后,在用户停止触摸后滚动多快停止,0 表示永不停止,1 表示立刻停止 | +| Elastic | 布尔值,是否回弹 | +| Bounce Duration | 浮点数,回弹所需要的时间。取值范围是 0-10 | +| Indicator | 页面视图指示器组件 | +| PageTurningEventTiming | 设置 PageView、PageTurning 事件的发送时机 | +| PageEvents | 数组,滚动视图的事件回调函数 | +| CancelInnerEvents | 布尔值,是否在滚动行为时取消子节点上注册的触摸事件 | + +### PageViewIndicator 设置 + +PageViewIndicator 是可选的,该组件是用来显示页面的个数和标记当前显示在哪一页。 + +建立关联可以通过在 **层级管理器** 里面拖拽一个带有 PageViewIndicator 组件的节点到 PageView 的相应字段完成。 + +### PageView 事件 + +![pageview-event](./pageview/pageview-event.png) + +| 属性 | 功能说明 | +| :------------- | :---------- | +| Target | 带有脚本组件的节点 | +| Component | 脚本组件名称 | +| Handler | 指定一个回调函数,当 PageView 的事件发生的时候会调用此函数 | +| CustomEventData | 用户指定任意的字符串作为事件回调的最后一个参数传入 | + +PageView 的事件回调有两个参数,第一个参数是 PageView 本身,第二个参数是 PageView 的事件类型。 + +## 详细说明 + +PageView 组件必须有指定的 content 节点才能起作用,content 中的每个子节点为一个单独页面,且每个页面的大小为 PageView 节点的大小,如果节点大小大于内容大小的话,可能会导致出现滚动不完整的现象。在 PageView 组件下有一个 view 节点对象,该对象结合 ScrollThreshold 决定了当前滑动的距离是否达到可以翻页的条件,操作效果分为以下两种: + +- **缓慢滑动**:通过拖拽视图中的页面到达指定的 ScrollThreshold 数值(该数值是页面大小的百分比)以后松开会自动滑动到下一页。 +- **快速滑动**:快速的向一个方向进行拖动,自动滑倒下一页,每次滑动最多只能一页。 + + + +## 通过脚本代码添加回调 + +### 方法一 + +这种方法添加的事件回调和使用编辑器添加的事件回调是一样的,都是通过代码添加。首先需要构造一个 `EventHandler` 对象,然后设置好对应的 `target`、`component`、`handler` 和 `customEventData` 参数。 + +```ts +import { _decorator, Component, Event, Node, PageView, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad(){ + const pageChangedEventHandler = new EventHandler(); + pageChangedEventHandler.target = this.node; // 这个 node 节点是你的事件处理代码组件所属的节点 + pageChangedEventHandler.component = 'example'; // 这个是脚本类名 + pageChangedEventHandler.handler = 'callback'; + pageChangedEventHandler.customEventData = 'foobar'; + + const page = this.node.getComponent(PageView); + page.clickEvents.push(pageChangedEventHandler); + } + + callback(event: Event, customEventData: string){ + // 这里 event 是一个 Touch Event 对象,你可以通过 event.target 取到事件的发送节点 + const node = event.target as Node; + const pageview = node.getComponent(PageView); + console.log(customEventData); // foobar + } +} +``` + +### 方法二 + +通过 `pageView.node.on('page-turning', ...)` 的方式来添加 + +```ts +// 假设我们在一个组件的 onLoad 方法里面添加事件处理回调,在 callback 函数中进行事件处理: +import { _decorator, Component, Event, Node, PageView } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad(){ + this.pageView.node.on('page-turning', this.callback, this); + } + + callback(pageView: PageView) { + // 回调的参数是 pageView 组件 + // 另外,注意这种方式注册的事件,也无法传递 customEventData + } +} +``` diff --git a/versions/4.0/zh/ui-system/components/editor/pageview/pageview-event.png b/versions/4.0/zh/ui-system/components/editor/pageview/pageview-event.png new file mode 100644 index 0000000000..4c4cad833b Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/pageview/pageview-event.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/pageview/pageview-hierarchy.png b/versions/4.0/zh/ui-system/components/editor/pageview/pageview-hierarchy.png new file mode 100644 index 0000000000..311908849a Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/pageview/pageview-hierarchy.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/pageview/pageview-inspector.png b/versions/4.0/zh/ui-system/components/editor/pageview/pageview-inspector.png new file mode 100644 index 0000000000..c50355d9aa Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/pageview/pageview-inspector.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/pageviewindicator.md b/versions/4.0/zh/ui-system/components/editor/pageviewindicator.md new file mode 100644 index 0000000000..818eae970f --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/pageviewindicator.md @@ -0,0 +1,24 @@ +# PageViewIndicator 组件参考 + +PageViewIndicator 用于显示 PageView 当前的页面数量和标记当前所在的页面。 + +![PageViewIndicator](./pageviewindicator/pageviewindicator.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI -> PageViewIndicator** 即可添加 PageViewIndicator 组件到节点上。 + +PageViewIndicator 的脚本接口请参考 [PageViewIndicator API](%__APIDOC__%/zh/class/PageViewIndicator)。 + +关于使用可以参考范例 **PageView**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/15.pageview) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/15.pageview))。 + +## PageViewIndicator 属性 + +| 属性 | 功能说明 | +| :-------------- | :----------- | +| spriteFrame | 每个页面标记显示的图片 | +| direction | 页面标记摆放方向,分别为 **水平方向** 和 **垂直方向** | +| cellSize | 每个页面标记的大小 | +| spacing | 每个页面标记之间的边距 | + +## 详细说明 + +PageViewIndicator 一般不会单独使用,它需要与 `PageView` 配合使用,可以通过相关属性,来进行创建相对应页面的数量的标记,当你滑动到某个页面的时,PageViewIndicator 就会高亮它对应的标记。 diff --git a/versions/4.0/zh/ui-system/components/editor/pageviewindicator/pageviewindicator.png b/versions/4.0/zh/ui-system/components/editor/pageviewindicator/pageviewindicator.png new file mode 100644 index 0000000000..6359a5719c Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/pageviewindicator/pageviewindicator.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/progress.md b/versions/4.0/zh/ui-system/components/editor/progress.md new file mode 100644 index 0000000000..1ecb9c1411 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/progress.md @@ -0,0 +1,29 @@ +# ProgressBar 组件参考 + +ProgressBar(进度条)经常被用于在游戏中显示某个操作的进度,在节点上添加 ProgressBar 组件,然后给该组件关联一个 Bar Sprite 就可以在场景中控制 Bar Sprite 来显示进度了。 + +![add-progressbar](progress/add-progressbar.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/ProgressBar** 即可添加 ProgressBar 组件到节点上。 + +进度条的脚本接口请参考 [ProgressBar API](%__APIDOC__%/zh/class/ProgressBar)。 + +关于使用可以参考范例 **Progress**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/11.progress) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/11.progress))。 + +## ProgressBar 属性 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| Bar Sprite | 进度条渲染所需要的 Sprite 组件,可以通过拖拽一个带有 **Sprite** 组件的节点到该属性上来建立关联。 +| Mode | 支持 **HORIZONTAL**(水平)、**VERTICAL**(垂直)和 **FILLED**(填充)三种模式,可以通过配合 **reverse** 属性来改变起始方向。 +| Total Length | 当进度条为 100% 时 Bar Sprite 的总长度/总宽度。在 **FILLED** 模式下 **Total Length** 表示取 Bar Sprite 总显示范围的百分比,取值范围从 0 ~ 1。 +|Progress | 浮点,取值范围是 0~1,不允许输入之外的数值。 +|Reverse | 布尔值,默认的填充方向是从左至右/从下到上,开启后变成从右到左/从上到下。 + +## 详细说明 + +添加 ProgressBar 组件之后,通过从 **层级管理器** 中拖拽一个带有 **Sprite** 组件的节点到 Bar Sprite 属性上,此时便可以通过拖动 progress 滑块来控制进度条的显示了。 + +Bar Sprite 可以是自身节点,子节点,或者任何一个带有 **Sprite** 组件的节点。另外,Bar Sprite 可以自由选择 Simple、Sliced 和 Filled 渲染模式。 + +进度条的模式选择 **FILLED** 的情况下,Bar Sprite 的 **Type** 也需要设置为 **FILLED**,否则会报警告。 diff --git a/versions/4.0/zh/ui-system/components/editor/progress/add-progressbar.png b/versions/4.0/zh/ui-system/components/editor/progress/add-progressbar.png new file mode 100644 index 0000000000..da6a81487d Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/progress/add-progressbar.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/render-component.md b/versions/4.0/zh/ui-system/components/editor/render-component.md new file mode 100644 index 0000000000..3d01a040c8 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/render-component.md @@ -0,0 +1,13 @@ +# 2D 渲染组件介绍 + +- [Sprite 组件参考](sprite.md) +- [Label 组件参考](label.md) +- [Mask 组件参考](mask.md) +- [Graphics 组件参考](graphics.md) +- [RichText 组件参考](richtext.md) +- [UIStaticBatch 组件参考](ui-static.md) +- [TiledMap 组件参考](../../../editor/components/tiledmap.md) +- [TiledTile 组件参考](../../../editor/components/tiledtile.md) +- [Spine(骨骼动画)Skeleton 组件参考](../../../editor/components/spine.md) +- [DragonBones(龙骨)ArmatureDisplay 组件参考](../../../editor/components/dragonbones.md) +- [MotionStreak 组件参考](../../../editor/components/motion-streak.md) diff --git a/versions/4.0/zh/ui-system/components/editor/renderroot2d.md b/versions/4.0/zh/ui-system/components/editor/renderroot2d.md new file mode 100644 index 0000000000..1ecc76bb3d --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/renderroot2d.md @@ -0,0 +1,28 @@ +# RenderRoot2D 组件 + +![renedertoot2d.png](renderroot2d\renedertoot2d.png) + +RenderRoot2D 组件是 [Canvas](./canvas.md) 的基类,任何需要显示的 2D/UI 节点都 **必须要挂载在 RenderRoot2D 下才** 可以正确的显示。 + +RenderRoot2D 只会搜集子节点下的所有和 2D/UI 相关组件的需要绘制的信息,并提交给渲染引擎。即使将 3D 对象如 Mesh,放在 RenderRoot2D 所在的节点下,RenderRoot2D 也不会搜集该 3D 对象的几何信息。 + +RenderRoot2D 必须要配合 UITransform 组件使用。在添加 RenderRoot2D 时,会自动添加 UITransform 组件。 + +典型的 RenderRoot2D 的应有两方面: + +- 继承类 [Canvas](./canvas.md) 用于处理 2D/UI 组件的绘制、适配等。 +- 在 3D 空间内绘制 2D/UI 元素 + +因此如果您尝试制作如 3D Label 如玩家血条之类的元素,可考虑使用 RenderRoot2D 作为父节点。 + +需要注意,如果手动创建 2D 元素,Cocos Creator 会自动创建 Canvas 节点。 + +![canvas-label.png](./renderroot2d/canvas-label.png) + +如果要创建在 3D 空间内的 Label,在创建后删除 Canvas、Camera 节点,并将 Label 拖拽到 RenderRoot2D 节点下即可正常显示。 + +![3dui.png](./renderroot2d/3dui.png) + +另外需要注意的是,通常场景内的默认相机,是不绘制 UI_2D 层级,需要修改 RenderRoot2D 下节点的层级为可见层级,通常为 UI_3D。 + +![preview.png](./renderroot2d/preview.png) \ No newline at end of file diff --git a/versions/4.0/zh/ui-system/components/editor/renderroot2d/3dui.png b/versions/4.0/zh/ui-system/components/editor/renderroot2d/3dui.png new file mode 100644 index 0000000000..58a3c86e8b Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/renderroot2d/3dui.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/renderroot2d/canvas-label.png b/versions/4.0/zh/ui-system/components/editor/renderroot2d/canvas-label.png new file mode 100644 index 0000000000..01acb65c79 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/renderroot2d/canvas-label.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/renderroot2d/create-label.png b/versions/4.0/zh/ui-system/components/editor/renderroot2d/create-label.png new file mode 100644 index 0000000000..280d4c5857 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/renderroot2d/create-label.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/renderroot2d/preview.png b/versions/4.0/zh/ui-system/components/editor/renderroot2d/preview.png new file mode 100644 index 0000000000..119ad685fa Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/renderroot2d/preview.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/renderroot2d/renedertoot2d.png b/versions/4.0/zh/ui-system/components/editor/renderroot2d/renedertoot2d.png new file mode 100644 index 0000000000..df28e8e531 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/renderroot2d/renedertoot2d.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/richText/example-components.png b/versions/4.0/zh/ui-system/components/editor/richText/example-components.png new file mode 100644 index 0000000000..f1dbc6d801 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/richText/example-components.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/richText/example-console.png b/versions/4.0/zh/ui-system/components/editor/richText/example-console.png new file mode 100644 index 0000000000..0522245ccd Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/richText/example-console.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/richText/example-running.png b/versions/4.0/zh/ui-system/components/editor/richText/example-running.png new file mode 100644 index 0000000000..c195175b0e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/richText/example-running.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/richText/example-string.png b/versions/4.0/zh/ui-system/components/editor/richText/example-string.png new file mode 100644 index 0000000000..76f2f6d6f4 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/richText/example-string.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/richText/richtext.png b/versions/4.0/zh/ui-system/components/editor/richText/richtext.png new file mode 100644 index 0000000000..a844dd55f8 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/richText/richtext.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/richtext.md b/versions/4.0/zh/ui-system/components/editor/richtext.md new file mode 100644 index 0000000000..ffbe9cf081 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/richtext.md @@ -0,0 +1,141 @@ +# RichText 组件参考 + +RichText 组件用来显示一段带有不同样式效果的文字,你可以通过一些简单的 BBCode 标签来设置文字的样式。目前支持的样式有:颜色(color)、字体大小(size)、字体描边(outline)、加粗(b)、斜体(i)、下划线(u)、换行(br)、图片(img)和点击事件(on),并且不同的 BBCode 标签是可以支持相互嵌套的。 + +更多关于 BBCode 标签的内容,请参考本文档的 [BBCode 标签格式说明](#bbcode-标签格式) 小节。 + +![richtext](richText/richtext.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/Render/RichText** 即可添加 RichText 组件到节点上。 + +富文本的脚本接口请参考 [RichText API](%__APIDOC__%/zh/class/RichText)。 + +关于使用可以参考范例 **RichText**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/07.richtext) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/07.richtext))。 + +## RichText 属性 + +| 属性 | 功能说明 | +| :------------- | :--------- | +| String | 富文本的内容字符串,你可以在里面使用 BBCode 来指定特定文本的样式 | +| Horizontal Align | 水平对齐方式 | +| Vertical Align | 竖直对齐方式 | +| Font Size | 字体大小,单位是 point(**注意**:该字段不会影响 BBCode 里面设置的字体大小) | +| Font | 富文本定制字体,所有的 label 片段都会使用这个定制的 TTF 字体 | +| Font Family | 富文本定制系统字体。 | +| Use System Font | 是否使用系统字体。 | +| Max Width | 富文本的最大宽度,传 0 的话意味着必须手动换行. | +| Line Height | 字体行高,单位是 point | +| Image Atlas | 对于 img 标签里面的 src 属性名称,都需要在 imageAtlas 里面找到一个有效的 spriteFrame,否则 img tag 会判定为无效。 | +| Handle Touch Event | 选中此选项后,RichText 将阻止节点边界框中的所有输入事件(鼠标和触摸),从而防止输入事件穿透到底层节点。 | + +## BBCode 标签格式 + +### 基本格式 + +目前支持的标签类型有:size、color、b、i、u、img 和 on,分别用来定制字体大小、字体颜色、加粗、斜体、下划线、图片和点击事件。每一个标签都有一个起始标签和一个结束标签,起始标签的名字和属性格式必要符合要求,且全部为小写。结束标签的名字不做任何检查,只需要满足结束标签的定义即可。 + +下面分别是应用 size 和 color 标签的一个例子: + +`你好,Creator` + +### 支持标签 + +**注意**:所有的 tag 名称必须是小写,且属性值是用 **=** 号赋值。 + +| 名称 | 描述 | 示例 | 注意事项 | +| :------ | :------ | :---- | :----- | +| color | 指定字体渲染颜色,颜色值可以是内置颜色,比如 white,black 等,也可以使用 16 进制颜色值,比如 `#ff0000` 表示红色 | `Red Text` | | +| size | 指定字体渲染大小,大小值必须是一个整数| `enlarge me` | Size 值必须使用等号赋值 | +| outline | 设置文本的描边颜色和描边宽度 | `A label with outline` | 如果你没有指定描边的颜色或者宽度的话,那么默认的颜色是白色 (#ffffff),默认的宽度是 1 | +| b | 指定使用粗体来渲染 | `This text will be rendered as bold` | 名字必须是小写,且不能写成 bold | +| i | 指定使用斜体来渲染 | `This text will be rendered as italic` | 名字必须是小写,且不能写成 italic | +| u | 给文本添加下划线 |`This text will have a underline` | 名字必须是小写,且不能写成 underline | +| on | 指定一个点击事件处理函数,当点击该 Tag 所在文本内容时,会调用该事件响应函数 | ` click me! ` | 除了 on 标签可以添加 click 属性,color 和 size 标签也可以添加,比如 `click me` | +| param | 当点击事件触发时,可以在回调函数的第二个参数获取该数值 | ` click me! ` | 依赖 click 事件 | +| br | 插入一个空行 | `
` | **注意**:`

` 和 `
` 都是不支持的。 | +| img | 给富文本添加图文混排功能,img 的 src 属性必须是 ImageAtlas 图集里面的一个有效的 spriteframe 名称 |`` | **注意**:只有 `` 这种写法是有效的。如果你指定一张很大的图片,那么该图片创建出来的精灵会被等比缩放,缩放的值等于富文本的行高除以精灵的高度。 | + +#### img 标签的可选属性 + +为了更好地排版,我们为 img 标签类型提供了可选属性,你可以使用 `width` 及 `height` 来指定 SpriteFrame 的大小,这将允许该图片可以大于或是小于行高(但此设定不会改变行高)。 + +当你改变了 SpriteFrame 的高度或宽度后,或许会需要使用 `align` 来调整该图片在行中的对齐方式。 + +| 属性 | 描述 | 示例 | 注意事项 | +| :-------- | :---------- | :------ | :------- | +| height | 指定 SpriteFrame 的渲染高度,大小值必须为整数 | `` | 如果你只使用了高度属性,该 SpriteFrame 会自动计算宽度以保持图片比例 | +| width | 指定 SpriteFrame 的渲染宽度,大小值必须为整数 | `` | 你可以同时使用高度及宽度属性 `` | +| align | 指定 SpriteFrame 在行中的对齐方式,值必需为 `bottom`、`top` 或 `center` | `` | 预设对齐方式为 bottom | + +为了支持定制图片排版,我们还提供了 `offset` 属性,用于微调 SpriteFrame 在 RichText 中的位置。设置 `offset` 时需注意属性值必须为整数,并且如设置不当将导致图片与文字重叠。 + +| offset 属性 | 示例 | 描述 | 注意事项 | +| :----- | :------ | :---------- | :------ | +| Y | `` | 指定 SpriteFrame 的 y 轴加上 5 | 当 offset 只设定一个值的时候,它代表 y 轴的偏移 | +| Y | `` | 指定 SpriteFrame 的 y 轴减少 5 | 你可以设定负整数来减少 y 轴 | +| X, Y | ``| 指定 SpriteFrame 的 x 轴加上 6,y 轴减少 5 | 偏移属性的值只能包含 `0-9`、`-` 和 `,` 字符 | + +### 标签嵌套 + +标签与标签是支持嵌套的,且嵌套规则跟 HTML 是一样的。比如下面的嵌套标签设置一个文本的渲染大小为 30,且颜色为绿色。 + +`I'm green` + +也可以实现为: + +`I'm green` + +## 文本缓存类型(Cache Mode) + +由于富文本组件是由多个 Label 节点拼装而成,所以对于复杂的富文本,drawcall 数量也会比较高,因此引擎为富文本组件提供了 Label 组件的文本缓存类型设置,来适当减少 drawcall 的增加。对于每种缓存类型的具体说明,参照 [Label 组件的文本缓存类型](./label.md) + +| 类型 | 功能说明 | +| :------------- | :---------- | +| NONE | 默认值,对富文本所拆分创建的每个 Label 节点,设置其 CacheMode 为 NONE 类型,即将每个 Label 的整段文本生成一张位图并单独进行渲染。 | +| BITMAP | 选择后,对富文本所拆分创建的每个 Label 节点,设置其 CacheMode 为 BITMAP 类型,即将每个 Label 的整段文本生成一张位图,并将该位图添加到动态图集中,再依据动态图集进行合并渲染。 | +| CHAR | 选择后,对富文本所拆分创建的每个 Label 节点,设置其 CacheMode 为 CHAR 类型,即将每个 Label 的文本以“字”为单位缓存到全局共享的位图中,相同字体样式和字号的每个字符将在全局共享一份缓存。 | + +## 详细说明 + +富文本组件全部由 JS 层实现,采用底层的 Label 节点拼装而成,并且在上层做排版逻辑。这意味着,你新建一个复杂的富文本,底层可能有十几个 label 节点,而这些 label 节点都是采用系统字体渲染的。 + +所以,一般情况下,你不应该在游戏的主循环里面频繁地修改富文本的文本内容,这可能会导致性能比较低。另外,如果能不使用富文本组件,就尽量使用普通的文本组件,并且 BMFont 的效率是最高的。 + +## 示例 + +### 事件的用法 + +我们以 `on` 标签作为示例,来说明下事件的用法。请在项目中新建任意一个带有 RichText 组件的节点。并创建一个自定义脚本,挂载在 RichText 组件上。 + +![richText](./richText/example-components.png) + +将如下的代码复制到 RichText 的 **String** 属性内。 + +```html +Click Me! +``` + +![example-string.png](./richText/example-string.png) + +复制下面的代码到您的自定义脚本中: + +```ts +import { _decorator, Component, EventTouch } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('ClickEventHandler') +export class ClickEventHandler extends Component { + + onClicked(eventTouch:EventTouch, param:string){ + console.log("onClicked", param); + } +} +``` + +运行游戏。 + +![example-running.png](./richText/example-running.png) + +使用鼠标点击该文本,可以查看到控制台的输出: + +![example-console.png](./richText/example-console.png) diff --git a/versions/4.0/zh/ui-system/components/editor/safearea.md b/versions/4.0/zh/ui-system/components/editor/safearea.md new file mode 100644 index 0000000000..13242c33f9 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/safearea.md @@ -0,0 +1,11 @@ +# SafeArea 组件参考 + +该组件会将所在节点的布局适配到 iPhone X 等异形屏手机的安全区域内,可适配 Android 和 iOS 设备,通常用于 UI 交互区域的顶层节点。 + +![Renderings](./safearea/renderings.png) + +开发者只需要将 SafeArea 组件添加到节点上。该组件在启用时会通过 `sys.getSafeAreaRect()` 获取到当前 iOS 或 Android 设备的安全区域,并通过 Widget 组件(如果没有 Widget 组件会自动添加)实现适配。 + +SafeArea 脚本接口请参考 [Mask API](%__APIDOC__%/zh/class/SafeArea)。 + +具体用法可参考官方范例 **SafeArea**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/23.safe-area) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/23.safe-area))。 diff --git a/versions/4.0/zh/ui-system/components/editor/safearea/renderings.png b/versions/4.0/zh/ui-system/components/editor/safearea/renderings.png new file mode 100644 index 0000000000..d17a5cba75 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/safearea/renderings.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/scroll/scrollbar.png b/versions/4.0/zh/ui-system/components/editor/scroll/scrollbar.png new file mode 100644 index 0000000000..724ba2e005 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/scroll/scrollbar.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-content.png b/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-content.png new file mode 100644 index 0000000000..83bc2c36fb Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-content.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-event.png b/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-event.png new file mode 100644 index 0000000000..f564ed3e69 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-event.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-hierarchy.png b/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-hierarchy.png new file mode 100644 index 0000000000..757b0c79a0 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-hierarchy.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-inspector.png b/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-inspector.png new file mode 100644 index 0000000000..cabc0a3a6a Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/scroll/scrollview-inspector.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/scrollbar.md b/versions/4.0/zh/ui-system/components/editor/scrollbar.md new file mode 100644 index 0000000000..d090912c78 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/scrollbar.md @@ -0,0 +1,24 @@ +# ScrollBar 组件参考 + +ScrollBar 允许用户通过拖动滑块来滚动一张图片,它与 `Slider` 组件有点类似,但是它主要是用于滚动而 Slider 则用来设置数值。 + +![scrollbar.png](scroll/scrollbar.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/ScrollBar** 即可添加 ScrollBar 组件到节点上。 + +滚动条的脚本接口请参考 [ScrollBar API](%__APIDOC__%/zh/class/ScrollBar)。 + +## ScrollBar 属性 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| Handle| ScrollBar 前景图片,它的长度/宽度会根据 ScrollView 的 content 的大小和实际显示区域的大小来计算。 +| Direction | 滚动方向,目前包含水平和垂直两个方向。 +| Enable Auto Hide | 是否开启自动隐藏,如果开启了,那么在 ScrollBar 显示后的 `Auto Hide Time` 时间内会自动消失。 +| Auto Hide Time | 自动隐藏时间,需要配合设置 `Enable Auto Hide` + +## 详细说明 + +ScrollBar 一般不会单独使用,它需要与 `ScrollView` 配合使用,另外 ScrollBar 需要指定一个 `Sprite` 组件,即属性面板里面的 `Handle`。 + +通常我们还会给 ScrollBar 指定一张背景图片,用来指示整个 ScrollBar 的长度或者宽度。 diff --git a/versions/4.0/zh/ui-system/components/editor/scrollview.md b/versions/4.0/zh/ui-system/components/editor/scrollview.md new file mode 100644 index 0000000000..234800ad7b --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/scrollview.md @@ -0,0 +1,109 @@ +# ScrollView 组件参考 + +ScrollView 是一种带滚动功能的容器,它提供一种方式可以在有限的显示区域内浏览更多的内容。通常 ScrollView 会与 [**Mask** 组件](./mask.md) 配合使用,同时也可以添加 [**ScrollBar** 组件](./scrollbar.md) 来显示浏览内容的位置。 + +![scrollview-content](scroll/scrollview-content.png) + +![scrollview-inspector](scroll/scrollview-inspector.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/ScrollView** 即可添加 ScrollView 组件到节点上。 + +滚动视图的脚本接口请参考 [ScrollView API](%__APIDOC__%/zh/class/ScrollView)。 + +关于使用可以参考范例 **ScrollView**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/06.scrollview) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/06.scrollview))。 + +## ScrollView 属性 + +| 属性 | 功能说明 | +| :---------- | :--------- | +| content | 它是一个节点引用,用来创建 ScrollView 的可滚动内容,通常这可能是一个包含一张巨大图片的节点。 | +| Horizontal | 布尔值,是否允许横向滚动。 | +| Vertical | 布尔值,是否允许纵向滚动。 | +| Inertia | 滚动的时候是否有加速度。 | +| Brake | 浮点数,滚动之后的减速系数。取值范围是 0-1,如果是 1 则立马停止滚动,如果是 0,则会一直滚动到 content 的边界。 | +| Elastic | 布尔值,是否回弹。 | +| BounceDuration | 浮点数,回弹所需要的时间。取值范围是 0-10。 | +| HorizontalScrollBar | 它是一个节点引用,用来创建一个滚动条来显示 content 在水平方向上的位置。 | +| VerticalScrollBar | 它是一个节点引用,用来创建一个滚动条来显示 content 在垂直方向上的位置 | +| ScrollEvents | 列表类型,默认为空,用户添加的每一个事件由节点引用,组件名称和一个响应函数组成。详情见下方的 ScrollView 事件 | +| CancelInnerEvents | 如果这个属性被设置为 true,那么滚动行为会取消子节点上注册的触摸事件,默认被设置为 true。 | + +### ScrollView 事件 + +![scrollview-event](scroll/scrollview-event.png) + +事件结构参考:[组件事件结构](./button.md#组件事件结构) + +ScrollView 的事件回调有两个参数,第一个参数是 ScrollView 本身,第二个参数是 ScrollView 的事件类型。 + +### ScrollBar 设置 + +ScrollBar 是可选的,你可以选择只设置 Horizontal ScrollBar 或者 Vertical ScrollBar,当然也可以两者都设置。建立关联可以通过在 **层级管理器** 里面拖拽一个带有 ScrollBar 组件的节点到 ScrollView 的相应字段完成。 + +## 详细说明 + +ScrollView 组件必须有指定的 content 节点才能起作用,通过指定滚动方向和 content 节点在此方向上的长度来计算滚动时的位置信息,Content 节点也可以通过添加 `Widget` 设置自动 resize,也可以通过添加 `Layout` 来完成子节点布局,但是这两个组件不应该同时添加到一个节点上以避免产生不可预料的后果。 + +通常一个 ScrollView 的节点树如下图: + +![scrollview-hierarchy](scroll/scrollview-hierarchy.png) + +这里的 view 用来定义一个可以显示的滚动区域,所以通常 [Mask 组件](./mask.md) 会被添加到 view 上。可以滚动的内容可以直接放到 content 节点或者添加节 content 的子节点上。 + +## 通过脚本代码添加回调 + +### 方法一 + +这种方法添加的事件回调和使用编辑器添加的事件回调是一样的,都是通过代码添加。首先需要构造一个 `EventHandler` 对象,然后设置好对应的 `target`、`component`、`handler` 和 `customEventData` 参数。 + +```ts +import { _decorator, Component, ScrollView, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad() { + const scrollViewEventHandler = new EventHandler(); + scrollViewEventHandler.target = this.node; // 这个 node 节点是你的事件处理代码组件所属的节点 + scrollViewEventHandler.component = 'example';// 这个是脚本类名 + scrollViewEventHandler.handler = 'callback'; + scrollViewEventHandler.customEventData = 'foobar'; + + const scrollview = this.node.getComponent(ScrollView); + scrollview.scrollEvents.push(scrollViewEventHandler); + } + + callback(scrollview, eventType, customEventData){ + // 这里 scrollview 是一个 Scrollview 组件对象实例 + // 这里的 eventType === ScrollView.EventType enum 里面的值 + // 这里的 customEventData 参数就等于你之前设置的 'foobar' + } +} +``` + +### 方法二 + +通过 `scrollview.node.on('scroll-to-top', ...)` 的方式来添加 + +```js +// 假设我们在一个组件的 onLoad 方法里面添加事件处理回调,在 callback 函数中进行事件处理: +import { _decorator, Component, ScrollView } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + @property(ScrollView) + scrollview: ScrollView | null = null; + onLoad(){ + this.scrollview.node.on('scroll-to-top', this.callback, this); + } + + callback(scrollView: ScrollView) { + // 回调的参数是 ScrollView 组件,注意这种方式注册的事件,无法传递 customEventData + } +} +``` + +同样的,你也可以注册 `scrolling`、`touch-up`、`scroll-began` 等事件,这些事件的回调函数的参数与 `scroll-to-top` 的参数一致。 + +关于完整的 ScrollView 的事件列表,可以参考 ScrollView 的 API 文档 [ScrollView API](%__APIDOC__%/zh/class/ScrollView)。 diff --git a/versions/4.0/zh/ui-system/components/editor/skew/skew-component.jpg b/versions/4.0/zh/ui-system/components/editor/skew/skew-component.jpg new file mode 100644 index 0000000000..d7ef4827e8 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/skew/skew-component.jpg differ diff --git a/versions/4.0/zh/ui-system/components/editor/skew/skew-rotational-false.jpg b/versions/4.0/zh/ui-system/components/editor/skew/skew-rotational-false.jpg new file mode 100644 index 0000000000..c86b026e9f Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/skew/skew-rotational-false.jpg differ diff --git a/versions/4.0/zh/ui-system/components/editor/skew/skew-rotational-true.jpg b/versions/4.0/zh/ui-system/components/editor/skew/skew-rotational-true.jpg new file mode 100644 index 0000000000..765529eae5 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/skew/skew-rotational-true.jpg differ diff --git a/versions/4.0/zh/ui-system/components/editor/slider.md b/versions/4.0/zh/ui-system/components/editor/slider.md new file mode 100644 index 0000000000..6d770261e9 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/slider.md @@ -0,0 +1,95 @@ +# Slider 组件参考 + +Slider 是一个滑动器组件。 + +![slider-inspector](slider/slider-inspector.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,选择 **UI -> Slider** 即可添加 Slider 组件到节点上。也可以直接在 **层级管理器** 中点击右上角的 **+** 按钮,然后选择 **UI 组件 -> Slider** 创建一个 Slider 节点。 + +滑动器的脚本接口请参考 [Slider API](%__APIDOC__%/zh/class/Slider)。 + +关于使用可以参考范例 **Slider**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/10.slider) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/10.slider))。 + +## Slider 属性 + +| 属性 | 功能说明 | +| :------------- | :---------- | +| handle | 滑块按钮部件,可以通过该按钮进行滑动调节 Slider 数值大小 | +| direction | 滑动器的方向,分为横向和竖向 | +| progress | 当前进度值,该数值的区间是 0-1 之间 | +| slideEvents | 滑动器组件事件回调函数 | + +## Slider 事件 + +![slider-event](slider/slider-event.png) + +事件结构参考:[组件事件结构](./button.md#组件事件结构) + +Slider 的事件回调有两个参数,第一个参数是 Slider 本身,第二个参数是 CustomEventData + +## 详细说明 + +Slider 通常用于调节 UI 的数值(例如音量调节),它主要的部件是一个滑块按钮,该部件用于用户交互,通过该部件可进行调节 Slider 的数值大小。 + +通常一个 Slider 的节点树如下图: + +![slider-hierarchy](slider/slider-hierarchy.png) + +## 通过脚本代码添加回调 + +### 方法一 + +这种方法添加的事件回调和使用编辑器添加的事件回调是一样的,都是通过代码添加。首先需要构造一个 `EventHandler` 对象,然后设置好对应的 `target`、`component`、`handler` 和 `customEventData` 参数。 + +```ts +import { _decorator, Component, Event, Node, Slider, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + + onLoad () { + const sliderEventHandler = new EventHandler(); + // 这个 node 节点是事件处理脚本组件所属的节点 + sliderEventHandler.target = this.node; + // 这个是脚本类名 + sliderEventHandler.component = 'example'; + sliderEventHandler.handler = 'callback'; + sliderEventHandler.customEventData = 'foobar'; + + const slider = this.node.getComponent(Slider); + slider!.slideEvents.push(sliderEventHandler); + } + + callback(slider: Slider, customEventData: string) { + //这里 event 是一个 Touch Event 对象,你可以通过 event.target 取到事件的发送节点 + // 这里的 customEventData 参数就等于之前设置的 'foobar' + } +} +``` + +### 方法二 + +通过 `slider.node.on('slide', ...)` 的方式来添加 + +```ts +// 假设我们在一个组件的 onLoad 方法里面添加事件处理回调,在 callback 函数中进行事件处理 + +import { _decorator, Component, Slider } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + + @property(Slider) + slider: Slider | null = null; + + onLoad () { + this.slider!.node.on('slide', this.callback, this); + } + + callback(slider: Slider) { + // 回调的参数是 slider 组件,注意这种方式注册的事件,无法传递 customEventData + } +} +``` diff --git a/versions/4.0/zh/ui-system/components/editor/slider/slider-content.png b/versions/4.0/zh/ui-system/components/editor/slider/slider-content.png new file mode 100644 index 0000000000..31e14d5087 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/slider/slider-content.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/slider/slider-event.png b/versions/4.0/zh/ui-system/components/editor/slider/slider-event.png new file mode 100644 index 0000000000..b85551981e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/slider/slider-event.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/slider/slider-hierarchy.png b/versions/4.0/zh/ui-system/components/editor/slider/slider-hierarchy.png new file mode 100644 index 0000000000..ef11b6224e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/slider/slider-hierarchy.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/slider/slider-inspector.png b/versions/4.0/zh/ui-system/components/editor/slider/slider-inspector.png new file mode 100644 index 0000000000..88d4c04771 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/slider/slider-inspector.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/sprite.md b/versions/4.0/zh/ui-system/components/editor/sprite.md new file mode 100644 index 0000000000..4bb4d6bdea --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/sprite.md @@ -0,0 +1,66 @@ +# Sprite 组件参考 + +Sprite(精灵)是 2D/3D 游戏最常见的显示图像的方式,在节点上添加 Sprite 组件,就可以在场景中显示项目资源中的图片。 + +![add sprite](sprite/sprite-component.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **2D/Sprite** 即可添加 Sprite 组件到节点上。 + +图像的组件接口请参考 [Sprite API](%__APIDOC__%/zh/class/Sprite)。 + +关于使用可以参考范例 **Sprite**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/01.sprite) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/01.sprite))。 + +## Sprite 属性 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| Type | 渲染模式,包括普通(Simple)、九宫格(Sliced)、平铺(Tiled)和填充(Filled)四种模式,详情请参考下文 **渲染模式** 部分的内容 +| CustomMaterial | 自定义材质,使用方法参考 [自定义材质](../engine/ui-material.md) +| Grayscale | 灰度模式,开启后 Sprite 会使用灰度模式渲染。 +| Color | 图片颜色 +| Sprite Atlas | Sprite 显示图片资源所属的图集(参考 [Atlas](../../../asset/atlas.md))。 +| Sprite Frame | 渲染 Sprite 使用的 [SpriteFrame 图片资源](../../../asset/sprite-frame.md)。 +| Size Mode | 指定 Sprite 的尺寸
**Trimmed** 表示会使用原始图片资源裁剪透明像素后的尺寸
**Raw** 表示会使用原始图片未经裁剪的尺寸
**Custom** 表示会使用自定义尺寸。当用户手动修改过 **Size** 属性后,**Size Mode** 会被自动设置为 **Custom**,除非再次指定为前两种尺寸。 +| Trim | 是否渲染原始图像周围的透明像素区域,详情请参考 [图像资源的自动剪裁](../engine/trim.md) + +添加 Sprite 组件之后,通过从 **资源管理器** 中拖拽 SpriteFrame 类型的资源到 **SpriteFrame** 属性引用中,就可以通过 Sprite 组件显示资源图像。 + +如果拖拽的 SpriteFrame 资源是包含在一个 Atlas 图集资源中的,那么 Sprite 的 **Atlas** 属性也会被一起设置。 + +**若要动态更换 SpriteFrame 则需要先动态加载图片资源,然后再进行替换,详情请参考 [获取和加载资源:动态加载](../../../asset/dynamic-load-resources.md#加载-spriteframe-或-texture2d)。** + +## 渲染模式 + +Sprite 组件支持以下几种渲染模式: + +- `普通模式(Simple)`:根据原始图片资源渲染 Sprite,一般在这个模式下我们不会手动修改节点的尺寸,来保证场景中显示的图像和美术人员生产的图片比例一致。 + +- `九宫格模式(Sliced)`:图像将被分割成九宫格,并按照一定规则进行缩放以适应可随意设置的尺寸(`size`)。通常用于 UI 元素,或将可以无限放大而不影响图像质量的图片制作成九宫格图来节省游戏资源空间。详细信息请阅读 [使用 Sprite 编辑器制作九宫格图像](../engine/sliced-sprite.md#-) 一节。 + +- `平铺模式(Tiled)`:当 Sprite 的尺寸增大时,图像不会被拉伸,而是会按照原始图片的大小不断重复,就像平铺瓦片一样将原始图片铺满整个 Sprite 规定的大小。 + + ![tiled](sprite/tiled.png) + +- `填充模式(Filled)`:根据原点和填充模式的设置,按照一定的方向和比例绘制原始图片的一部分。经常用于进度条的动态展示。 + + + +### 填充模式(Filled) + +**Type** 属性选择填充模式后,会出现一组新的属性可供配置: + +| 属性 | 功能说明 +| :-------------- | :----------- | +| Fill Type | 填充类型选择,有 **HORIZONTAL**(横向填充)、**VERTICAL**(纵向填充)和 **RADIAL**(扇形填充)三种。 +| Fill Start | 填充起始位置的标准化数值(从 0 ~ 1,表示填充总量的百分比),选择横向填充时,**Fill Start** 设为 0,就会从图像最左边开始填充 +| **Fill Range** | 填充范围的标准化数值(同样从 0 ~ 1),设为 1,就会填充最多整个原始图像的范围。 +| **Fill Center** | 填充中心点,该属性只有选择了 `RADIAL` 填充类型才能修改。决定了扇形填充时会环绕 Sprite 上的哪个点。 + + +![radial](sprite/radial.png) + +#### Fill Range 填充范围补充说明 + +在 **HORIZONTAL** 和 **VERTICAL** 这两种填充类型下,**Fill Start** 设置的数值将影响填充总量,如果 **Fill Start** 设为 0.5,那么即使 **Fill Range** 设为 1.0,实际填充的范围也仍然只有 Sprite 总大小的一半。 + +而 **RADIAL** 类型中 **Fill Start** 只决定开始填充的方向,**Fill Start** 为 0 时,从 x 轴正方向开始填充。**Fill Range** 决定填充总量,值为 1 时将填充整个圆形。**Fill Range** 为正值时逆时针填充,为负值时顺时针填充。 diff --git a/versions/4.0/zh/ui-system/components/editor/sprite/radial.png b/versions/4.0/zh/ui-system/components/editor/sprite/radial.png new file mode 100644 index 0000000000..3261ca3a4f Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/sprite/radial.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/sprite/sprite-component.png b/versions/4.0/zh/ui-system/components/editor/sprite/sprite-component.png new file mode 100644 index 0000000000..f97e64a65f Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/sprite/sprite-component.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/sprite/sprite_editor_add.png b/versions/4.0/zh/ui-system/components/editor/sprite/sprite_editor_add.png new file mode 100644 index 0000000000..8703173f32 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/sprite/sprite_editor_add.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/sprite/tiled.png b/versions/4.0/zh/ui-system/components/editor/sprite/tiled.png new file mode 100644 index 0000000000..962612685d Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/sprite/tiled.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/toggle.md b/versions/4.0/zh/ui-system/components/editor/toggle.md new file mode 100644 index 0000000000..88081e7b04 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/toggle.md @@ -0,0 +1,89 @@ +# Toggle 组件参考 + +Toggle 是一个 CheckBox,当它和 ToggleContainer 一起使用的时候,可以变成 RadioButton。 + +![toggle1](toggle/toggle.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/Toggle** 即可添加 Toggle 组件到节点上。 + +Toggle 的组件接口请参考 [Toggle API](%__APIDOC__%/zh/class/Toggle)。 + +关于使用可以参考范例 **Toggle**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/09.toggle) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/09.toggle))。 + +## Toggle 属性 + +| 属性 | 功能说明 | +| :------------- | :---------- | +| isChecked | 布尔类型,如果这个设置为 true,则 check mark 组件会处于 enabled 状态,否则处于 disabled 状态。 | +| checkMark | Sprite 类型,Toggle 处于选中状态时显示的图片 | +| Check Events | 列表类型,默认为空,用户添加的每一个事件由节点引用,组件名称和一个响应函数组成。详情见下方的 **Toggle 事件** 部分 | + +> **注意**:因为 Toggle 继承自 Button,所以关于 Toggle 的 Button 相关属性的详细说明和用法请参考 [Button 组件](button.md)。 + +## Toggle 事件 + +事件结构参考:[组件事件结构](./button.md#组件事件结构)。 + +Toggle 的事件回调有二个参数,第一个参数是 Toggle 本身,第二个参数是 customEventData。 + +## 详细说明 + +Toggle 组件的节点树一般为: + +![toggle-node-tree](toggle/toggle-node-tree.png) + +> **注意**:checkMark 组件所在的节点需要放在 background 节点的上面。 + +## 通过脚本代码添加回调 + +### 方法一 + +这种方法添加的事件回调和使用编辑器添加的事件回调是一样的,都是通过代码添加。首先需要构造一个 `EventHandler` 对象,然后设置好对应的 `target`、`component`、`handler` 和 `customEventData` 参数。 + +```ts +import { _decorator, Component, Event, Node, ToggleComponent, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad(){ + const checkEventHandler = new EventHandler(); + checkEventHandler.target = this.node; //这个 node 节点是你的事件处理代码组件所属的节点 + checkEventHandler.component = 'example';//这个是脚本类名 + checkEventHandler.handler = 'callback'; + checkEventHandler.customEventData = 'foobar'; + + const toggle = this.node.getComponent(ToggleComponent); + toggle.checkEvents.push(checkEventHandler); + } + + callback(event: Event, customEventData: string){ + //这里 event 是一个 Touch Event 对象,你可以通过 event.target 取到事件的发送节点 + // 这里的 customEventData 参数就等于之前设置的 'foobar' + } +} +``` + +### 方法二 + +通过 `toggle.node.on('toggle', ...)` 的方式来添加 + +```js +// 假设我们在一个组件的 onLoad 方法里面添加事件处理回调,在 callback 函数中进行事件处理 + +import { _decorator, Component, ToggleComponent } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + @property(ToggleComponent) + toggle: ToggleComponent | null = null; + onLoad(){ + this.toggle.node.on('toggle', this.callback, this); + } + + callback(toggle: ToggleComponent){ + // 回调的参数是 toggle 组件,注意这种方式注册的事件,无法传递 customEventData + } +} +``` diff --git a/versions/4.0/zh/ui-system/components/editor/toggle/toggle-container.png b/versions/4.0/zh/ui-system/components/editor/toggle/toggle-container.png new file mode 100644 index 0000000000..470140020e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/toggle/toggle-container.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/toggle/toggle-group.png b/versions/4.0/zh/ui-system/components/editor/toggle/toggle-group.png new file mode 100644 index 0000000000..551a8434fc Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/toggle/toggle-group.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/toggle/toggle-node-tree.png b/versions/4.0/zh/ui-system/components/editor/toggle/toggle-node-tree.png new file mode 100644 index 0000000000..5f7d957ffc Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/toggle/toggle-node-tree.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/toggle/toggle.png b/versions/4.0/zh/ui-system/components/editor/toggle/toggle.png new file mode 100644 index 0000000000..bff7a6ec3f Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/toggle/toggle.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/toggleContainer.md b/versions/4.0/zh/ui-system/components/editor/toggleContainer.md new file mode 100644 index 0000000000..acae8f8f99 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/toggleContainer.md @@ -0,0 +1,58 @@ +# ToggleContainer 组件参考 + +![toggle-container](toggle/toggle-container.png) + +ToggleContainer 不是一个可见的 UI 组件,它可以用来修改一组 Toggle 组件的行为。当一组 Toggle 属于同一个 ToggleContainer 的时候,任何时候只能有一个 Toggle 处于选中状态。 + +> **注意**:所有包含 Toggle 组件的一级子节点都会自动被添加到该容器中 + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/ToggleContainer** 即可添加 ToggleContainer 组件到节点上。 + +ToggleContainer 的组件接口请参考 [ToggleContainer API](%__APIDOC__%/zh/class/ToggleContainer)。 + +关于使用可以参考范例 **Toggle**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/09.toggle) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/09.toggle))。 + +## ToggleContainer 属性 + +| 属性 | 功能说明 | +| :------------- | :---------- | +| AllowSwitchOff | 如果这个设置为 true,那么 toggle 按钮在被点击的时候可以反复地被选中和未选中。 | +| CheckEvents | 选中事件。列表类型,默认为空,用户添加的每一个事件由节点引用,组件名称和一个响应函数组成。 | + +## ToggleContainer 事件 + +事件结构参考:[组件事件结构](./button.md#组件事件结构) + +ToggleContainer 的事件回调有二个参数,第一个参数是 Toggle 本身,第二个参数是 customEventData。 + +## 详细说明 + +ToggleContainer 一般不会单独使用,它需要与 `Toggle` 配合使用来实现 RadioButton 的单选效果。 + +## 通过脚本代码添加回调 + +这种方法添加的事件回调和使用编辑器添加的事件回调是一样的,都是通过代码添加。首先需要构造一个 `EventHandler` 对象,然后设置好对应的 `target`、`component`、`handler` 和 `customEventData` 参数。 + +```ts +import { _decorator, Component, Event, Node, ToggleContainer, EventHandler } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + onLoad(){ + const containerEventHandler = new EventHandler(); + containerEventHandler.target = this.node; // 这个 node 节点是你的事件处理代码组件所属的节点 + containerEventHandler.component = 'example';// 这个是脚本类名 + containerEventHandler.handler = 'callback'; + containerEventHandler.customEventData = 'foobar'; + + const container = this.node.getComponent(ToggleContainer); + container.checkEvents.push(containerEventHandler); + } + + callback(event: Event, customEventData: string) { + // 这里 event 是一个 Touch Event 对象,你可以通过 event.target 取到事件的发送节点 + // 这里的 customEventData 参数就等于之前设置的 'foobar' + } +} +``` diff --git a/versions/4.0/zh/ui-system/components/editor/ui-coordinate-tracker.md b/versions/4.0/zh/ui-system/components/editor/ui-coordinate-tracker.md new file mode 100644 index 0000000000..4884382502 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/ui-coordinate-tracker.md @@ -0,0 +1,25 @@ +# UICoordinateTracker 组件参考 + +UI 坐标跟踪映射组件是在 UI 上执行坐标转换以及模拟透视相机下 3D 物体近大远小效果。通过事件的方式将转换后的坐标以及物体在视口下的占比返回。适用于 3D 人物血条以及姓名条之类功能。 + +## UICoordinateTracker 属性 + +| 属性 | 功能说明 | +| :-------------- | :---------- | +| Target | 目标对象。需要转换到哪一个 UI 节点下。 | +| Camera | 照射相机。 | +| UseScale | 是否启用缩放。在透视相机下,根据 3D 节点坐标与相机的距离,调整映射后物体的缩放比例,实现近大远小效果。需要结合 distance 使用。 | +| Distance | 距相机多少距离为正常显示计算大小。根据模型在相机下的照射效果调整最佳位置,以该位置为分界线计算在视口占比。 | +| SyncEvents | 映射数据事件。回调的第一个参数是映射后的本地坐标,第二个是距相机距离比。 | + +## SyncEvents 事件 + +可以使用 `onSyncEvents(localUIPos:Vec3,distanceScale: number,customEventData: string)` 来响应 `SyncEvents` 事件。 + +参数含义如下: + +- `localUIPos:Vec3` 映射后的 UI 坐标 +- `distanceScale: number` 距离相机的比 +- `customEventData: string` 自定义数据,可在事件面板上配置 + +具体的使用方法可参考范例 **UI 展示 Demo**([GitHub](https://github.com/cocos/cocos-example-ui/) | [Gitee](https://gitee.com/mirrors_cocos-creator/demo-ui/))中的 rocker 场景。 diff --git a/versions/4.0/zh/ui-system/components/editor/ui-model.md b/versions/4.0/zh/ui-system/components/editor/ui-model.md new file mode 100644 index 0000000000..3d1f41e299 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/ui-model.md @@ -0,0 +1,9 @@ +# UIMeshRenderer 组件参考 + +UIMeshRenderer 是一个将 3D 模型从 3D 渲染管线转换到 2D 渲染管线的带有转换功能的渲染组件。该组件支持 3D 模型和粒子在 UI 上的显示,没有这个组件,即使模型和粒子节点在 UI 里也不会被渲染。 + +> **注意**:若 3D 模型无法在 UI 场景中正常显示,请尝试放大模型倍数。 + +该组件的添加方式是在 **层级管理器** 中选中带有或继承自 MeshRenderer 组件的节点,然后点击 **属性检查器** 下方的 **添加组件** 按钮,选择 **UI-> UIMeshRenderer** 即可。而粒子则是添加到粒子节点上。通常结构如下所示: + +![ui-model-hierarchy](uimodel/ui-model-hierarchy.png) diff --git a/versions/4.0/zh/ui-system/components/editor/ui-opacity.md b/versions/4.0/zh/ui-system/components/editor/ui-opacity.md new file mode 100644 index 0000000000..339a90e804 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/ui-opacity.md @@ -0,0 +1,26 @@ +# UIOpacity(透明度设置)组件参考 + +该组件会为节点记录一个透明度修改标识用来影响到后续的渲染节点。一般用于非渲染节点,如果作用在渲染节点上会形成透明度叠加现象。渲染节点可以通过设置 color 的 alpha 通道来设置透明度。 + +> **注意**:该组件仅对根节点上有 Canvas 或 RenderRoot2D 组件的节点生效。 + +使用方法如下: + +![ui-opacity](uiopacity/ui-opacity.png) + +也可以通过代码设置透明度: + +```ts +const opacityComp = this.getComponent(UIOpacity); +opacityComp.opacity = 157; +``` + +遮罩的组件接口请参考 [UIOpacity API](%__APIDOC__%/zh/class/UIOpacity)。 + +关于使用可以参考范例 **UIOpacity**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/other/opacity) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/other/opacity))。 + +## UIOpacity 属性 + +| 属性 | 功能说明 | +| :-------------- | :----------- | +| Opacity | 透明度 | diff --git a/versions/4.0/zh/ui-system/components/editor/ui-skew.md b/versions/4.0/zh/ui-system/components/editor/ui-skew.md new file mode 100644 index 0000000000..a4066cb6ff --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/ui-skew.md @@ -0,0 +1,53 @@ +# UISkew (斜切)组件参考 + +UI Skew 组件用于控制 UI 元素的斜切变换,可以通过设置水平和垂直方向的斜切角度来实现不同的视觉效果。 + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/UISkew** 即可添加 UISkew 组件到节点上。 + +UISkew 脚本接口请参考 [UISkew API](%__APIDOC__%/zh/class/UISkew)。 + +## UISkew 属性介绍 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| rotational | 是否使用旋转类型的斜切算法 (默认关闭,此时效果兼容 2.x 版本) +| skew | 水平与垂直方向的斜切角度(类型: Vec2,x、y 值分别设置 X、Y 轴斜切角度) + +--- + +## UISkew 组件示例 + +![ui-skew](./skew/skew-component.jpg) + +![skew-rotational-true](./skew/skew-rotational-true.jpg) + +![skew-rotational-false](./skew/skew-rotational-false.jpg) + +### 通过脚本代码修改斜切角度 + +```ts +import { _decorator, Component, Node, UISkew } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Example') +export class Example extends Component { + + start () { + const uiSkew = this.getComponent(UISkew); + if (uiSkew) { + // 方法一 + uiSkew.setSkew(new Vec2(30, 15)); + + // 方法二 + uiSkew.x = 30; + uiSkew.y = 15; + } + } +} +``` + +### 注意事项 + +1. 节点的斜切变换会影响子节点的渲染,但不会改变节点的尺寸属性 +2. 节点进行倾斜变换时会发送 `'transform-changed'` 事件,可用于监听节点变换 +3. 斜切变换的过程需要进行多次三角函数运算,频繁执行斜切变换会增加游戏运行开销 \ No newline at end of file diff --git a/versions/4.0/zh/ui-system/components/editor/ui-static.md b/versions/4.0/zh/ui-system/components/editor/ui-static.md new file mode 100644 index 0000000000..1016fc2a4e --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/ui-static.md @@ -0,0 +1,33 @@ +# UIStaticBatch 组件参考 + +> **注意**:我们在 v3.4.1 中采用了新的渲染合批策略,普通动态合批组件在性能上会得到有效提升,因此不再建议使用 UIStaticBatch 组件进行手动管理。 + +UI 静态合批组件是一个提升 UI 渲染性能的组件,脚本在初始化当前帧渲染的过程中会收集该 UI 节点树下的所有渲染数据(除了模型、Mask 和 Graphics),存储为一个静态的 IA 渲染数据。并在后续的渲染流程中使用固定数据进行渲染,不再遍历其节点树,此后的坐标变换将不再生效。当你需要修改静态数据的时候,可以调用 markAsDirty 接口来重新触发渲染数据收集标记。 + +遮罩的组件接口请参考 [UIStaticBatch API](%__APIDOC__%/zh/class/UIStaticBatch)。 + +关于使用可以参考范例 **UIStaticBatch**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/19.static-ui) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/19.static-ui))。 + +## 通过脚本代码开启静态合批 + +```ts +import { _decorator, Component } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass("example") +export class example extends Component { + start(){ + const uiStatic = this.node.getComponent(UIStaticBatch); + // 选择你要开始静态合批的时机,调用此接口开始静态合批 + uiStatic.markAsDirty(); + } +} +``` + +## 注意事项 + +使用该组件有以下几点需要注意: + +1. 不要频繁触发静态合批,因为会清空原先存储的 IA 数据重新采集,会有一定性能和内存损耗。 +2. 不适用于子节点树中包含 Mask、Graphics 和 Model 的情况。 +3. 对于节点树不会有任何改变的节点(例如 2D 地图),在 **开始静态合批之后** 即可将所有子节点删除,以得到最好的性能和内存表现。 diff --git a/versions/4.0/zh/ui-system/components/editor/ui-transform.md b/versions/4.0/zh/ui-system/components/editor/ui-transform.md new file mode 100644 index 0000000000..0d37b7b0ae --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/ui-transform.md @@ -0,0 +1,53 @@ +# UI 变换组件 + +定义了 UI 上的矩形信息,包括矩形的尺寸和锚点位置。开发者可以通过该组件任意地操作矩形的大小、位置。一般用于渲染、点击事件的计算、界面布局以及屏幕适配等。 + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/UITransform** 即可添加 UITransform 组件到节点上。 + +UITransform 脚本接口请参考 [UITransform API](%__APIDOC__%/zh/class/UITransform)。 + +## UITransform 属性介绍 + +| 属性 | 功能说明 +| :-------------- | :----------- | +| ContentSize | UI 矩形内容尺寸 +| AnchorPoint | UI 矩形锚点位置 + +--- + +### 通过脚本代码修改节点尺寸和锚点 + +```ts +import { _decorator, Component, Node, UITransform } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Example') +export class Example extends Component { + + start () { + const uiTransform = this.getComponent(UITransform); + // 方法一 + uiTransform.setContentSize(200, 120); + uiTransform.setAnchorPoint(0, 0.5); + + // 方法二 + uiTransform.width = 200; + uiTransform.height = 120; + uiTransform.anchorX = 0; + uiTransform.anchorY = 0.5; + } +} +``` + +### priority 属性的弃用说明 + +我们在 3.1 版本中弃用了 UITransform 组件的 priority 属性,用户可以通过使用 setSiblingIndex 来设置节点树的顺序来进行调整渲染顺序。 + +关于 priority 属性的移除及推荐使用的 SiblingIndex 属性的说明: +由于表意不明以及与引擎中其他属性的命名冲突,UITransform 组件上的 priority 属性在 v3.1 已被废弃。priority 属性的设计之初是为用户提供节点树排序的快捷方式,本身并无其他用途,和 priority 所表达的“优先级”也不相关,并且设置之后实际上还是通过更改节点树的顺序来调整渲染顺序。 + +在移除了 `priority` 属性之后,用户可以用 `setSiblingIndex` 方法来替换使用,`setSiblingIndex` 方法通过影响节点中的 `siblingIndex` 属性来调整节点树的顺序。不同之处在于,`priority` 属性存在默认值,而 node 的 `siblingIndex` 属性实际上就是这个节点在父节点中的位置,所以在节点树发生变化之后,节点的 `siblingIndex` 属性数值就会产生变化。这就要求在使用 `setSiblingIndex` 方法的时候,需要知道节点在父节点中的相对位置并做出控制,才能够获得预期的结果。 + +需要注意的是,不能直接将 `siblingIndex` 属性等同于 `priority`(已废弃)属性来理解使用,它们的意义是不同的,改变 `siblingIndex` 属性需要理解并清楚其代表的是在父节点下的位置,且在节点树变化时会发生变化,并且只能通过 `setSiblingIndex` 方法来修改 `siblingIndex` 属性。 + +考虑到节点快捷排序的需求,我们会在之后的版本中提供更方便快捷的接口供用户排列节点使用。 diff --git a/versions/4.0/zh/ui-system/components/editor/uimodel/ui-model-hierarchy.png b/versions/4.0/zh/ui-system/components/editor/uimodel/ui-model-hierarchy.png new file mode 100644 index 0000000000..928fd53f24 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/uimodel/ui-model-hierarchy.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/uiopacity/ui-opacity.png b/versions/4.0/zh/ui-system/components/editor/uiopacity/ui-opacity.png new file mode 100644 index 0000000000..2c1fbedd2e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/uiopacity/ui-opacity.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/videoplayer.md b/versions/4.0/zh/ui-system/components/editor/videoplayer.md new file mode 100644 index 0000000000..69523ee656 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/videoplayer.md @@ -0,0 +1,186 @@ +# VideoPlayer 组件参考 + +VideoPlayer 是一种视频播放组件,可通过该组件播放本地和远程视频。 + +**播放本地视频**: + +![video-player](videoplayer/videoplayer.png) + +**播放远程视频**: + +![video-player-remote](videoplayer/videoplayer-remote.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后从 **UI 组件** 中选择 **VideoPlayer**,即可添加 VideoPlayer 组件到节点上。 + +VideoPlayer 的脚本接口请参考 [VideoPlayer API](%__APIDOC__%/zh/class/VideoPlayer)。 + +## VideoPlayer 属性 + +| 属性 | 功能说明 | +| -------------------- | ------------------ | +| Resource Type | 视频来源的类型,目前支持本地(LOCAL)视频和远程(REMOTE)视频 URL | +| Remote URL | 当 Resource Type 为 REMOTE 时显示的字段,填入远程视频的 URL | +| Clip | 当 Resource Type 为 LOCAL 时显示的字段,拖拽本地视频的资源到此处来使用 | +| Play On Awake | 视频加载后是否自动开始播放?| +| Current Time | 指定从哪个时间点开始播放视频 | +| Volume | 视频的音量(0.0 ~ 1.0)| +| Mute | 是否静音视频。静音时设置音量为 0,取消静音时恢复原来的音量 | +| Keep Aspect Ratio | 是否保持视频原来的宽高比 | +| Full Screen On Awake | 是否全屏播放视频 | +| Stay On Bottom | 永远在游戏视图最底层(该属性仅在 Web 平台生效)| +| Video Player Event | 视频播放回调函数,该回调函数会在特定情况被触发,比如播放中,暂时,停止和完成播放。详情见下方的 **VideoPlayer 事件** 章节或者 [VideoPlayerEvent API](%__APIDOC__%/zh/class/VideoPlayer?id=videoPlayerEvent)。| + +> **注意**:在 **Video Player Event** 属性的 **Node** 中,应该填入的是一个挂载有用户脚本组件的节点,在用户脚本中便可以根据用户需要使用相关的 VideoPlayer 事件。 + +## VideoPlayer 事件 + +### VideoPlayerEvent 事件 + +| 属性 | 功能说明 | +| --------------- | --------------- | +| target | 带有脚本组件的节点。 | +| component | 脚本组件名称。 | +| handler | 指定一个回调函数,当视频开始播放后,暂停时或者结束时都会调用该函数,该函数会传一个事件类型参数进来。| +| customEventData | 用户指定任意的字符串作为事件回调的最后一个参数传入。 | + +详情可参考 API 文档 [Component.EventHandler](%__APIDOC__%/zh/class/EventHandler) + +### 事件回调参数 + +| 名称 | 功能说明 | +| -------------- | -----------| +| PLAYING | 表示视频正在播放中。| +| PAUSED | 表示视频暂停播放。| +| STOPPED | 表示视频已经停止播放。| +| COMPLETED | 表示视频播放完成。| +| META_LOADED | 表示视频的元信息已加载完成,你可以调用 getDuration 来获取视频总时长。 | +| READY_TO_PLAY | 表示视频准备好了,可以开始播放了。| +| ERROR | 处理视频时触发的错误 | +| CLICKED | 表示视频被用户点击了。(只支持 Web 平台)| + +> **注意**:在 iOS 平台的全屏模式下,点击视频无法发送 CLICKED 事件。如果需要让 iOS 全屏播放并正确接受 CLICKED 事件,可以使用 Widget 组件把视频控件撑满。 + +详情可参考 [VideoPlayer 事件](%__APIDOC__%/zh/class/VideoPlayer?id=videoPlayerEvent)。 + +使用方式可参考范例 **VideoPlayer**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/21.video-player) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/21.video-player))。 + +## 详细说明 + +VideoPlayer 支持的视频格式为 **mp4**。 + +### 通过脚本代码添加回调 + +#### 方法一 + +这种方法添加的事件回调和使用编辑器添加的事件回调是一样的。通过代码添加,首先你需要构造一个 `Component.EventHandler` 对象,然后设置好对应的 `target`、`component`、`handler` 和 `customEventData` 参数。 + +```ts +import { _decorator, Component, VideoPlayer } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + @type(VideoPlayer) + videoPlayer = null; + + start () { + const eventHandler = new Component.EventHandler(); + eventHandler.target = newTarget; // 这个对象是你的事件处理代码组件所属的节点 + eventHandler.component = "MyComponent"; + eventHandler.handler = "callback"; + eventHandler.customEventData = "foobar"; + this.videoplayer.videoPlayerEvent.push(eventHandler); + } + + // 注意参数的顺序和类型是固定的 + callback: function(videoplayer, eventType, customEventData) { + // 这里的 videoplayer 是一个 VideoPlayer 组件对象实例 + // 这里的 eventType === VideoPlayer.EventType enum 里面的值 + // 这里的 customEventData 参数就等于你之前设置的 "foobar" + } +} +``` + +#### 方法二 + +通过 `videoplayer.node.on(VideoPlayer.EventType.READY_TO_PLAY, ...)` 的方式来添加 + +```ts +//假设我们在一个组件的 onLoad 方法里面添加事件处理回调,在 callback 函数中进行事件处理: +import { _decorator, Component, VideoPlayer } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('VideoPlayerCtrl') +export class VideoPlayerCtrl extends Component { + @type(VideoPlayer) + videoPlayer = null; + + start () { + this.videoplayer.node.on(VideoPlayer.EventType.READY_TO_PLAY, this.callback, this); + } + + callback (videoplayer) { + // 这里的 videoplayer 表示的是 VideoPlayer 组件 + // 对 videoplayer 进行你想要的操作 + // 另外,注意这种方式注册的事件,也无法传递 customEventData + } +} +``` + +同样的,用户也可以注册 `meta-loaded`、`clicked`、`playing` 等事件,这些事件的回调函数的参数与 `ready-to-play` 的参数一致。 + +> **注意**:由于 VideoPlayer 是特殊的组件,所以它无法监听节点上的 **触摸** 和 **鼠标** 事件。 + +关于完整的 VideoPlayer 的事件列表,可以参考 [VideoPlayer API](%__APIDOC__%/zh/class/VideoPlayer)。 + +## 如何实现 UI 在 VideoPlayer 上渲染 + +可通过以下两个步骤实现 UI 在 VideoPlayer 上显示: + +1. 确保 **项目设置 -> Macro Config** 中的 **ENABLE_TRANSPARENT_CANVAS** 为勾选状态(设置 Canvas 背景支持 alpha 通道) + + ![ENABLE_TRANSPARENT_CANVAS](videoplayer/ENABLE_TRANSPARENT_CANVAS.png) + +2. 可在 **属性检查器** 中勾选 VideoPlayer 组件上的 **stayOnBottom** 属性。 + + ![stayonbuttom](videoplayer/stayonbuttom.png) + +> **注意**: +> +> 1. 该功能仅支持 **Web** 平台。 +> 2. 各个浏览器具体效果无法保证一致,跟浏览器是否支持与限制有关。 +> 3. 开启 **stayOnBottom** 后,将无法正常监听 `VideoPlayerEvent` 中的 `clicked` 事件。 + +详情可参考范例 **VideoPlayer**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/21.video-player) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/21.video-player))。 + +最终效果如下图所示: + +![videoplayer-stayOnButton](videoplayer/videoplayer-stayonbuttom.png) + +## 支持平台 + +由于不同平台对于 VideoPlayer 组件的授权、API、控制方式都不同,还没有形成统一的标准,所以目前只支持 Web、iOS、Android、微信小游戏、Facebook Instant Games 以及 Google Play Instant 平台。 + +### 关于自动播放的问题 + +一些移动端的浏览器或 **WebView** 不允许自动播放视频,用户需要在触摸事件中手动播放视频。 + +```ts +import { _decorator, Node, Component, find, VideoPlayer } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('VideoPlayerCtrl') +export class VideoPlayerCtrl extends Component { + @type(VideoPlayer) + videoPlayer = null; + + start () { + let canvas = find('Canvas'); + canvas.on(Node.EventType.TOUCH_START, this.playVideo, this); + } + + playVideo () { + this.videoplayer.play(); + } +} +``` diff --git a/versions/4.0/zh/ui-system/components/editor/videoplayer/ENABLE_TRANSPARENT_CANVAS.png b/versions/4.0/zh/ui-system/components/editor/videoplayer/ENABLE_TRANSPARENT_CANVAS.png new file mode 100644 index 0000000000..4dad25ba3a Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/videoplayer/ENABLE_TRANSPARENT_CANVAS.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/videoplayer/stayonbuttom.png b/versions/4.0/zh/ui-system/components/editor/videoplayer/stayonbuttom.png new file mode 100644 index 0000000000..5153c0b79c Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/videoplayer/stayonbuttom.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/videoplayer/videoplayer-remote.png b/versions/4.0/zh/ui-system/components/editor/videoplayer/videoplayer-remote.png new file mode 100644 index 0000000000..6c68b0a29e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/videoplayer/videoplayer-remote.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/videoplayer/videoplayer-stayonbuttom.png b/versions/4.0/zh/ui-system/components/editor/videoplayer/videoplayer-stayonbuttom.png new file mode 100644 index 0000000000..d501f7ac46 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/videoplayer/videoplayer-stayonbuttom.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/videoplayer/videoplayer.png b/versions/4.0/zh/ui-system/components/editor/videoplayer/videoplayer.png new file mode 100644 index 0000000000..c0a55c0557 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/videoplayer/videoplayer.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/webview.md b/versions/4.0/zh/ui-system/components/editor/webview.md new file mode 100644 index 0000000000..0686bc983b --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/webview.md @@ -0,0 +1,205 @@ +# WebView 组件参考 + +**WebView** 是一种显示网页的组件,该组件让你可以在游戏里面集成一个小的浏览器。由于不同平台对于 WebView 组件的授权、API、控制方式都不同,还没有形成统一的标准,所以目前只支持 Web、iOS 和 Android 平台。 + +![webview](./webview/webview.png) + +点击 **属性检查器** 下方的 **添加组件** 按钮,然后从 **UI 组件** 中选择 **WebView**,即可添加 WebView 组件到节点上。 + +WebView 的脚本接口请参考 [WebView API](%__APIDOC__%/zh/class/WebView)。 + +## WebView 属性 + +| 属性 | 功能说明 | +| -------------- | -------------- | +| Url | 指定一个 URL 地址,这个地址以 http 或者 https 开头,请填写一个有效的 URL 地址。 | +| WebView Events | WebView 的回调事件,当 webview 在加载网页过程中,加载网页结束后或者加载网页出错时会调用此函数。 | + +> **注意**:在 **WebView Events** 属性的 **Node** 中,应该填入的是一个挂载有用户脚本组件的节点,在用户脚本中便可以根据用户需要使用相关的 WebView 事件。 + +## WebView 事件 + +### WebViewEvents 事件 + +| 属性 | 功能说明 | +| -------------- | ----------- | +| Target | 带有脚本组件的节点。 | +| Component | 脚本组件名称。 | +| Handler | 指定一个回调函数,当网页加载过程中、加载完成后或者加载出错时会被调用,该函数会传一个事件类型参数进来。详情见下方的 **WebView 事件回调参数** 部分 | +| CustomEventData | 用户指定任意的字符串作为事件回调的最后一个参数传入。 | + +详情可参考 API 文档 [Component.EventHandler 类型](%__APIDOC__%/zh/class/EventHandler) + +### WebView 事件回调参数 + +| 名称 | 功能说明 | +| :-------------- | :----------- | +| LOADING | 表示网页正在加载过程中。 | +| LOADED | 表示网页加载已经完毕。 | +| ERROR | 表示网页加载出错了。 | + +详情可参考 API [WebView 事件](%__APIDOC__%/zh/class/WebView?id=webviewEvents)。 + +使用方式可参考范例 **Webview**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/22.webview) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/22.webview))。 + +## 详细说明 + +目前此组件只支持 Web(PC 和手机)、iOS 和 Android 平台(v2.0.0~2.0.6 版本不支持),Mac 和 Windows 平台暂时还不支持,如果在场景中使用此组件,那么在 PC 的模拟器里面预览的时候可能看不到效果。 + +> **注意**: +> +> 1. **WebView** 组件暂时不支持加载指定 HTML 文件或者执行 Javascript 脚本。 +> 2. 如果开发者在项目中未使用到 **WebView** 相关功能,请确保在 **项目 -> 项目设置 -> 功能裁剪** 中剔除 **WebView** 模块,以提高 iOS 的 App Store 机审成功率。如果开发者确实需要使用 **WebView**(或者添加的第三方 SDK 自带了 **WebView**),并因此 iOS 的 App Store 机审不通过,仍可尝试通过邮件进行申诉。 + +### 通过脚本代码添加回调 + +#### 方法一 + +这种方法添加的事件回调和使用编辑器添加的事件回调是一样的,通过代码添加,你需要首先构造一个 `Component.EventHandler` 对象,然后设置好对应的 `target`、`component`、`handler` 和 `customEventData` 参数。 + +```ts +import { _decorator, Component, WebView } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('MyComponent') +export class MyComponent extends Component { + @type(WebView) + webview = null; + + start () { + const eventHandler = new Component.EventHandler(); + eventHandler.target = newTarget; // 这个对象是你的事件处理代码组件所属的节点 + eventHandler.component = "MyComponent"; + eventHandler.handler = "callback"; + eventHandler.customEventData = "foobar"; + this.webview.webviewEvents.push(eventHandler); + } + + // 注意参数的顺序和类型是固定的 + callback: function(webview, eventType, customEventData) { + // webview:是一个 webview 组件对象实例 + // eventType:等于 WebView.EventType enum 里面的值 + // customEventData:参数就等于你之前设置的 "foobar" + } +} +``` + +#### 方法二 + +通过 `webview.node.on(WebView.EventType.LOADED, ...)` 的方式来添加 + +```ts +import { _decorator, Component, WebView } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('WebViewCtrl') +export class WebViewCtrl extends Component { + @type(WebView) + webview = null; + + start () { + this.webview.node.on(WebView.EventType.LOADED, this.callback, this); + } + + callback (webview) { + // 这里的 webview 是一个 WebView 组件对象 + // 对 webview 进行你想要的操作 + // 另外,需要注意的是通过这种方式注册的事件,无法传递 customEventData + } +} +``` + +同样的,你也可以注册 `WebView.EventType.LOADING`、`WebView.EventType.ERROR` 事件,这些事件的回调函数的参数与 `WebView.EventType.LOADED` 的参数一致。 + +## 如何与 WebView 内部页面进行交互 + +### 调用 WebView 内部页面 + +```ts +import { _decorator, Component, WebView } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('WebViewCtrl') +export class WebViewCtrl extends Component { + @type(WebView) + webview = null; + + start () { + // 这里的 Test 是你 webView 内部页面代码里定义的全局函数 + this.webview.evaluateJS('Test()'); + } +} +``` + +#### 注意:Web 平台上的跨域问题需要自行解决 + +### WebView 内部页面调用外部的代码 + +目前 Android 与 iOS 用的机制是,通过截获 URL 的跳转,判断 URL 前缀的关键字是否与之相同,如果相同则进行回调。 + +1. 通过 `setJavascriptInterfaceScheme` 设置 URL 前缀关键字 +2. 通过 `setOnJSCallback` 设置回调函数,函数参数为 URL + +```ts +import { _decorator, Component, WebView } from 'cc'; +const { ccclass, type } = _decorator; + +@ccclass('WebViewCtrl') +export class WebViewCtrl extends Component { + @type(WebView) + webview = null; + + start () { + // 这里是与内部页面约定的关键字,请不要使用大写字符,会导致 location 无法正确识别。 + let scheme = "testkey"; + + function jsCallback (target, url) { + // 这里的返回值是内部页面的 URL 数值,需要自行解析自己需要的数据。 + let str = url.replace(scheme + '://', ''); // str === 'a=1&b=2' + // webview target + console.log(target); + } + + this.webview.setJavascriptInterfaceScheme(scheme); + this.webview.setOnJSCallback(jsCallback); + } +} +``` + +因此当你需要通过内部页面交互 **WebView** 时,应当设置内部页面 URL:`testkey://(后面你想要回调到 WebView 的数据)`。**WebView** 内部页面代码如下: + +```html + + + + + + + + +``` + +由于 Web 平台的限制,导致无法通过这种机制去实现,但是内部页面可以通过以下方式进行交互: + +```html + + + + + + + + +``` + +#### 再强调一遍:Web 平台上的跨域问题需要自行解决 diff --git a/versions/4.0/zh/ui-system/components/editor/webview/webview.png b/versions/4.0/zh/ui-system/components/editor/webview/webview.png new file mode 100644 index 0000000000..ceacb92aef Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/webview/webview.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/widget.md b/versions/4.0/zh/ui-system/components/editor/widget.md new file mode 100644 index 0000000000..8794d8eed2 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/editor/widget.md @@ -0,0 +1,101 @@ +# Widget 组件参考 + +Widget (对齐挂件) 是一个很常用的 UI 布局组件。它能使当前节点自动对齐到父物体的任意位置,或者约束尺寸,让你的游戏可以方便地适配不同的分辨率。对齐方案详细说明请参考 [对齐方案](../engine/widget-align.md) + +![default](widget/widget-default.png) + +点击 **属性检查器** 下面的 **添加组件** 按钮,然后选择 **UI/Widget** 即可添加 Widget 组件到节点上。 + +对齐组件的脚本接口请参考 [Widget API](%__APIDOC__%/zh/class/Widget)。 + +关于使用可以参考范例 **Widget**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/ui/04.widget) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/ui/04.widget))。 + +## 选项 + +选项 | 说明 | 备注 +:-- | :-- | :-- +Top | 对齐上边界 | 选中后,将在旁边显示一个输入框,用于设定当前节点的上边界和父物体的上边界之间的距离。 +Bottom | 对齐下边界 | 选中后,将在旁边显示一个输入框,用于设定当前节点的下边界和父物体的下边界之间的距离。 +Left | 对齐左边界 | 选中后,将在旁边显示一个输入框,用于设定当前节点的左边界和父物体的左边界之间的距离。 +Right | 对齐右边界 | 选中后,将在旁边显示一个输入框,用于设定当前节点的右边界和父物体的右边界之间的距离。 +HorizontalCenter | 水平方向居中 | +VerticalCenter | 竖直方向居中 | +Target | 对齐目标 | 指定对齐参照的节点,当这里未指定目标时会使用直接父级节点作为对齐目标 +AlignMode | 指定 Widget 的对齐方式,用于决定运行时 Widget 应何时更新 | 通常设置为 **ALWAYS**,每次节点产生变动时重新对齐。
设置为 **ONCE** 时,仅在组件初始化时进行一次对齐。**ON_WINDOW_RESIZE** 时会在每次窗口变动时候更新一次| + +## 对齐边界 + +我们可以在 Canvas 下新建一个 sprite,在 sprite 节点上添加一个 Widget 组件,然后做如下一些测试: + +### 左对齐,左边界距离 100 px + +![left-100px](widget/widget-left-100px.png) + +### 下对齐,下边界距离 50% + +百分比将以父节点的宽或高作为基准。 + +![bottom-0.5](widget/widget-bottom-0.5.png) + +### 右下对齐,边界距离 0 px + +![bottom-right-0px](widget/widget-bottom-right-0px.png) + +## 居中对齐 + +### 水平方向居中 + +![bottom-right-0px](widget/widget-h-center.png) + +### 竖直方向居中,并且右边界距离 50% + +![v-center-right-0.5](widget/widget-v-center-right-0.5.png) + +## 约束尺寸 + +如果左右同时对齐,或者上下同时对齐,那么在相应方向上的尺寸就会被拉伸。 +下面演示一下,在场景中放置两个矩形 Sprite,大的作为对话框背景,小的作为对话框上的按钮。按钮节点作为对话框的子节点,并且按钮设置成 Sliced 模式以便展示拉伸效果。 + +### 宽度拉伸,左右边距 10% + +![h-stretch](widget/widget-h-stretch.png) + +### 高度拉伸,上下边距 0,同时水平居中 + +![v-stretch](widget/widget-v-stretch.png) + +### 水平和竖直同时拉伸,边距 50 px + +![margin-50px](widget/widget-margin-50px.png) + +## 对节点位置、尺寸的限制 + +如果 **Align Mode** 属性设为 **ALWAYS** 时,会在运行时每帧都按照设置的对齐策略进行对齐,组件所在节点的位置(position)和尺寸(width,height)属性可能会被限制,不能通过 API 或动画系统自由修改。这是因为通过 Widget 对齐是在每帧的最后阶段进行处理的,因此对 Widget 组件中已经设置了对齐的相关属性进行设置,最后都会被 Widget 组件本身的更新所重置。 + +如果需要同时满足对齐策略和可以在运行时改变位置和尺寸的需要,可以通过以下两种方式实现: + +1. 确保 **Widget** 组件的 **Align Mode** 属性设置为 **ONCE**,该属性只会负责在组件初始化(onEnable)时进行一次对齐,而不会每帧再进行一次对齐。可以在初始化时自动完成对齐,然后就可以通过 API 或动画系统对 UI 进行移动变换了。 +2. 通过调用 **Widget** 组件的对齐边距 API,包括 **top**、 **bottom**、 **left**、 **right**,直接修改 Widget 所在节点的位置或某一轴向的拉伸。这些属性也可以在动画编辑器中添加相应关键帧,保证对齐的同时实现各种丰富的 UI 动画。 + +### 通过脚本代码修改对齐距离 + +```ts +import { _decorator, Component, Widget } from 'cc'; +const { ccclass, property } = _decorator; + +@ccclass('Example') +export class Example extends Component { + start () { + const widget = this.getComponent(Widget); + // 设置默认对齐单位是 px + widget!.bottom = 50; + widget!.top = 50; + + // 设置对齐单位是 % + widget!.isAbsoluteTop = false; + widget!.isAbsoluteBottom = false; + widget!.bottom = 0.1; // 10% + widget!.top = 0.1; // 10% + } +} +``` diff --git a/versions/4.0/zh/ui-system/components/editor/widget/widget-bottom-0.5.png b/versions/4.0/zh/ui-system/components/editor/widget/widget-bottom-0.5.png new file mode 100644 index 0000000000..e9f5e686e6 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/widget/widget-bottom-0.5.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/widget/widget-bottom-right-0px.png b/versions/4.0/zh/ui-system/components/editor/widget/widget-bottom-right-0px.png new file mode 100644 index 0000000000..c6ab1cae17 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/widget/widget-bottom-right-0px.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/widget/widget-default.png b/versions/4.0/zh/ui-system/components/editor/widget/widget-default.png new file mode 100644 index 0000000000..a8831bcb91 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/widget/widget-default.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/widget/widget-h-center.png b/versions/4.0/zh/ui-system/components/editor/widget/widget-h-center.png new file mode 100644 index 0000000000..ad4cf214d9 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/widget/widget-h-center.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/widget/widget-h-stretch.png b/versions/4.0/zh/ui-system/components/editor/widget/widget-h-stretch.png new file mode 100644 index 0000000000..57bfcc4d85 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/widget/widget-h-stretch.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/widget/widget-left-100px.png b/versions/4.0/zh/ui-system/components/editor/widget/widget-left-100px.png new file mode 100644 index 0000000000..2fd59d3b41 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/widget/widget-left-100px.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/widget/widget-margin-50px.png b/versions/4.0/zh/ui-system/components/editor/widget/widget-margin-50px.png new file mode 100644 index 0000000000..5f4079f5b4 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/widget/widget-margin-50px.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/widget/widget-v-center-right-0.5.png b/versions/4.0/zh/ui-system/components/editor/widget/widget-v-center-right-0.5.png new file mode 100644 index 0000000000..b3d857843a Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/widget/widget-v-center-right-0.5.png differ diff --git a/versions/4.0/zh/ui-system/components/editor/widget/widget-v-stretch.png b/versions/4.0/zh/ui-system/components/editor/widget/widget-v-stretch.png new file mode 100644 index 0000000000..6716da0f19 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/editor/widget/widget-v-stretch.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/auto-layout.md b/versions/4.0/zh/ui-system/components/engine/auto-layout.md new file mode 100644 index 0000000000..8d9a4d7677 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/auto-layout.md @@ -0,0 +1,62 @@ +# 自动布局容器 + +Layout(自动布局)组件可以挂载在任何节点上,将节点变成一个有自动布局功能的容器。所谓自动布局容器,就是能够自动将子节点按照一定规律排列,并可以根据节点内容的约束框总和调整自身尺寸的容器型节点。 + +接下来说到的布局类型,节点结构都如下图: + +![layout-node](auto-layout/layout-node.png) + +## 布局类型 + +自动布局组件有几种基本的布局模式,可以通过 `Type` 属性进行设置,包括以下几种。 + +### 水平布局(Horizontal) + +![horizontal-no-align](auto-layout/horizontal-no-align.png) + +Layout `Type` 设为 `Horizontal` 时,所有子节点都会自动横向排列,组件默认会修改节点在 y 轴上的位置或高度,如果子节点需要放置在 Layout 节点的约束框高度范围之外,可以取消勾选 `AutoAlignment`(如上图)。 + +水平排序下容易出现内容超出容器的情况,可以根据需要采取如下措施: + + - 如果容器要自适应上内容的尺寸,可以通过设置 `ResizeMode` 为 `Container`,该模式会根据子节点的宽度(`Width`)总和设置 Layout 节点的宽度(下图左)。 + + - 如果内容对象始终要保持在容器内,可以通过设置 `ResizeMode` 为 `Children`,该模式会将内容对象的尺寸限制在容器内(下图右)。 + + - 如果需要子节点在 y 轴向上对齐,可以在子节点上添加 Widget 组件,并开启 Top 或 Bottom 的对齐模式。 + +![horizontal-resizemode](auto-layout/horizontal-resizemode.png) + +#### 水平排列方向(Horizontal Direction) + +在水平布局下,可以通过 `HorizontalDirection` 设置水平朝向。朝向分为 `LEFT_TO_RIGHT` 和 `RIGHT_TO_LEFT` 两种,前者会按照节点在 **层级管理器** 中显示顺序从左到右排列;后者会按照节点显示从右到左排列。 + +### 垂直布局(Vertical) + +垂直布局的布局方式和排列方向跟 **水平布局** 几乎一致,只是方向上的不同,这里就不再赘述。 + +### 网格布局(Grid) + +Layout `Type` 设为 `GRID` 会开始网格布局。网格布局会在固定容器大小内,根据 `HorizontalDirection` 和 `VerticalDirection` 组合决定布局起点,根据 `StartAxis` 属性决定布局方向。 + +#### 网格排列方向(Grid Direction) + +Layout 排列子节点时,是以子节点在 **层级管理器** 中显示顺序为基准,加上开始点和 `StartAxis` 属性设置的排列方向来排列的。 + +- 开始轴向(Start Axis) + - 可以设置 `HORIZONTAL` 或 `VERTICAL` 两种方向。前者会进行横向排列,后者是纵向排列。 +- 开始点 + - 开始点是通过 `HorizontalDirection` 和 `VerticalDirection` 组合而成。 + - 假设 HorizontalDirection 为 `LEFT_TO_RIGHT`,VerticalDirection 为 `TOP_TO_BOTTOM`,则开始点是 **左上角** + - 假设 HorizontalDirection 为 `RIGHT_TO_LEFT`,VerticalDirection 为 `BOTTOM_TO_TOP`,则开始点是 **右下角** + +结合排列方向,举两个例子说明: + +- 如果当前设置的 HorizontalDirection 为 `LEFT_TO_RIGHT`,VerticalDirection 为 `TOP_TO_BOTTOM`,StartAxis 为 `HORIZONTAL`,则是告知组件要的排序方式是从容器的 **左上角** 开始按水平方向排列(下图左)。 + +- 如果当前设置的 HorizontalDirection 为 `RIGHT_TO_LEFT`,VerticalDirection 为 `BOTTOM_TO_TOP`,StartAxis 为 `VERTICAL`,则是告知组件要的排序方式是从容器的 **右下角** 开始按垂直方向排列(下图右)。 + + ![grid-layout](auto-layout/grid-layout.png) + +Grid 排序也有可能出现内容超出容器的情况,可以参考上文中的 **水平布局**,通过调整 `ResizeMode` 属性解决。 + +关于 Layout 组件的属性的详细说明,请参考 [Layout 组件](../editor/layout.md) 文档。 diff --git a/versions/4.0/zh/ui-system/components/engine/auto-layout/grid-layout.png b/versions/4.0/zh/ui-system/components/engine/auto-layout/grid-layout.png new file mode 100644 index 0000000000..4bf27d53d6 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/auto-layout/grid-layout.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/auto-layout/horizontal-no-align.png b/versions/4.0/zh/ui-system/components/engine/auto-layout/horizontal-no-align.png new file mode 100644 index 0000000000..3fa0613ec5 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/auto-layout/horizontal-no-align.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/auto-layout/horizontal-resizemode.png b/versions/4.0/zh/ui-system/components/engine/auto-layout/horizontal-resizemode.png new file mode 100644 index 0000000000..ac983ffd0e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/auto-layout/horizontal-resizemode.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/auto-layout/layout-node.png b/versions/4.0/zh/ui-system/components/engine/auto-layout/layout-node.png new file mode 100644 index 0000000000..df3aee3bee Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/auto-layout/layout-node.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout.md b/versions/4.0/zh/ui-system/components/engine/label-layout.md new file mode 100644 index 0000000000..736cec9c06 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/label-layout.md @@ -0,0 +1,119 @@ +# 文字排版 + +**文字组件(Label)** 是核心渲染组件之一,您需要了解如何设置文字的排版,才能在 UI 系统进行多分辨率适配和对齐设置时显示完美的效果。 + +## 文字在约束框中对齐 + +和其他渲染组件一样,Label 组件的排版也是基于 [UITransform](../editor/ui-transform.md) 组件所拥有的尺寸信息(`contentSize`),也就是约束框(Bounding Box)所规定的范围。 + +![](label-layout/label_in_boundingbox.png) + +上图所示就是 Label 渲染的文字在蓝色约束框内显示的效果。Label 中以下的属性决定了文字在约束框中显示的位置: + +- `Horizontal Align`(水平对齐):文字在约束框中水平方向的对齐准线,可以从 Left、Right、Center 三种位置中选择。 +- `Vertical Align`(垂直对齐):文字在约束框中垂直方向的对齐准线,可以从 Top、Bottom、Center 三种位置中选择。 + + ![](label-layout/horizontal-vertical-align.png) + +上图中水平方向对齐位置设置为 `Right`,垂直方向的对齐位置设为了 `Bottom`,可以看到文字出现在约束框的底部且靠右对齐。开发者可以将以上两个属性修改为其他组合,文字会根据设置出现在蓝色约束框内相应的位置。 + +## 文字尺寸和行高 + +`Font Size`(文字尺寸)决定了文字的显示大小,单位是 Point(也称作“磅”),是大多数图像制作和文字处理软件中通用的字体大小单位。对于动态字体来说,`Font Size` 可以无损放大,但位图字体在将 `Font Size` 设置为超过字体标定的字号大小时,显示会变得越来越模糊。 + +`Line Height`(行高)决定了文字在多行显示时每行文字占据的空间高度,单位同样是 Point。多行文字显示可以通过两种方式实现: + +- 在 `String` 属性中输入文字时,手动输入回车或换行符 +- 开启 `Enable Wrap Text`(换行)属性,下文会详细介绍 + +**文字尺寸和行高的关系**: + +- 如果 `Font Size` 和 `Line Height` 设为相同数值,文字正好占据一行大部分的空间高度。 + + ![](label-layout/font_equal_line_height.png) + +- 如果 `Font Size` 小于 `Line Height`,多行文字之间间隔会加大 + + ![](label-layout/font_smaller.png) + +- 如果 `Font Size` 大于 `Line Height`,多行文字之间间隔会缩小,甚至出现文字相互重叠的情况。 + + ![](label-layout/font_bigger.png) + +## 排版模式(Overflow) + +**Overflow(排版模式)** 属性,决定了文字内容增加时,如何在约束框的范围内排布。共有 **NONE**、**CLAMP**、**SHRINK**、**RESIZE_HEIGHT** 四种模式,而只有在 **后三种模式** 下才能通过编辑器左上角的 **矩形变换工具**(也可以是按键盘按键 **T**)或者修改 **属性检查器** 中的 **Size** 大小或者添加 **Widget 组件** 来调整约束框的大小。 + +**NONE** 模式会自动根据文字尺寸、行高等固定约束框尺寸。 + +### 截断(Clamp) + +![](label-layout/clamp.png) + +截断模式下,文字首先按照对齐模式和尺寸的要求进行渲染,而超出约束框的部分会被隐藏(截断)。 + +### 自动缩小(Shrink) + +![](label-layout/shrink.png) + +自动缩小模式下,如果文字按照原定尺寸渲染会超出约束框时,会自动缩小文字尺寸以显示全部文字。 + +**注意**:自动缩小模式不会放大文字来适应约束框。 + +### 自动适应高度(Resize Height) + +![](label-layout/resize-height.png) + +自动适应高度模式会保证文字的约束框贴合文字的高度,不管文字有多少行。这个模式非常适合显示内容量不固定的大段文字,配合 [ScrollView 组件](../components/scrollview.md) 可以在任意 UI 区域中显示无限量的文字内容。 + +## 自动换行(Enable Wrap Text) + +Label 组件中的 `Enable Wrap Text`(自动换行)属性,可以切换文字的自动换行开关。在自动换行开启的状态下,不需要在输入文字时手动输入回车或换行符,文字也会根据约束框的宽度自动换行。 + +**注意**:自动换行属性只有在文字排版模式的 **截断(Clamp)** 和 **自动缩小(Shrink)** 这两种模式下才有。**自动适应高度(Resize Height)** 模式下,自动换行属性是强制开启的。 + +### 截断(Clamp)模式自动换行 + +截断模式开启自动换行后,会优先在约束框允许的范围内换行排列文字,如果换行之后仍无法显示全部文字时才发生截断。 + +![](label-layout/clamp_wrap.png) + +以下两幅图都是在 `Clamp` + `Enable Wrap Text` 开启情况下的,区别在于文字约束框的宽度不同: + +![](label-layout/clamp_wrap1.png) ![](label-layout/clamp_wrap2.png) + +在约束框宽度从左图变化到右图的过程中,文字将不断调整换行,最后由于约束框高度不足而产生了截断显示。 + +### 自动缩小(Shrink)模式自动换行 + +和截断模式类似,自动缩小模式下文字超出约束框宽度时也会优先试图换行,在约束框宽度和长度都已经完全排满的情况下才会自动缩小文字以适应约束框。 + +![](label-layout/shrink_wrap.png) + +### 中文自动换行 + +中文自动换行的行为和英文不同,英文是以单词为单位进行换行的,必须有空格才能作为换行调整的最小单位。中文是以字为单位进行换行,每个字都可以单独调整换行。 + +## 文字节点的锚点 + +文字节点的锚点和文字在约束框中的对齐模式是需要区分的两个概念。在需要靠文字内容将约束框撑大的排版模式中(如 `Resize Height`),要正确设置锚点位置,才能让约束框向我们期望的方向调整。 + +例如,如果希望文字约束框向下扩展,需要将锚点(`Anchor`)的 `y` 属性设为 `1`。如下图所示: + +![](label-layout/anchor1.png) + +![](label-layout/anchor2.png) + +## 文字配合对齐挂件(Widget) + +在 Label 组件所在节点上添加一个 **Widget(对齐挂件)** 组件,就可以让文字节点相对于父节点进行各式各样的排版。 + +![](label-layout/widget.png) + +上图中我们在背景节点上添加了两个 Label 子节点,分别为它们添加 Widget 组件后,设置左边文字 Widget 的 `Right` 属性为 `50%`,右边文字 Widget 的 `Left` 属性为 `60%`,就可以实现图中所示的多列布局式文字。 + +而且通过 Widget 上设置边距,加上文字本身的排版模式,可以让我们在不需要具体微调文字约束框大小的情况下轻松实现灵活美观的文字排版。 + +## 查看组件参考 + +关于 Label 组件的属性,也可以查阅 [Label 组件参考](../editor/label.md) 文档。 diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/anchor1.png b/versions/4.0/zh/ui-system/components/engine/label-layout/anchor1.png new file mode 100644 index 0000000000..156b1df484 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/anchor1.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/anchor2.png b/versions/4.0/zh/ui-system/components/engine/label-layout/anchor2.png new file mode 100644 index 0000000000..5a198a26e8 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/anchor2.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/clamp.png b/versions/4.0/zh/ui-system/components/engine/label-layout/clamp.png new file mode 100644 index 0000000000..ca127041bf Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/clamp.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/clamp_wrap.png b/versions/4.0/zh/ui-system/components/engine/label-layout/clamp_wrap.png new file mode 100644 index 0000000000..9a9321ff4b Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/clamp_wrap.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/clamp_wrap1.png b/versions/4.0/zh/ui-system/components/engine/label-layout/clamp_wrap1.png new file mode 100644 index 0000000000..013d5c39f9 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/clamp_wrap1.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/clamp_wrap2.png b/versions/4.0/zh/ui-system/components/engine/label-layout/clamp_wrap2.png new file mode 100644 index 0000000000..63c79058cd Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/clamp_wrap2.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/font_bigger.png b/versions/4.0/zh/ui-system/components/engine/label-layout/font_bigger.png new file mode 100644 index 0000000000..76560e1711 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/font_bigger.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/font_equal_line_height.png b/versions/4.0/zh/ui-system/components/engine/label-layout/font_equal_line_height.png new file mode 100644 index 0000000000..66daf33d9c Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/font_equal_line_height.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/font_smaller.png b/versions/4.0/zh/ui-system/components/engine/label-layout/font_smaller.png new file mode 100644 index 0000000000..2dc5a5daed Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/font_smaller.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/horizontal-vertical-align.png b/versions/4.0/zh/ui-system/components/engine/label-layout/horizontal-vertical-align.png new file mode 100644 index 0000000000..fcc2377979 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/horizontal-vertical-align.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/label_in_boundingbox.png b/versions/4.0/zh/ui-system/components/engine/label-layout/label_in_boundingbox.png new file mode 100644 index 0000000000..7672acf262 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/label_in_boundingbox.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/resize-height.png b/versions/4.0/zh/ui-system/components/engine/label-layout/resize-height.png new file mode 100644 index 0000000000..3907ed7116 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/resize-height.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/shrink.png b/versions/4.0/zh/ui-system/components/engine/label-layout/shrink.png new file mode 100644 index 0000000000..699f866d56 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/shrink.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/shrink_wrap.png b/versions/4.0/zh/ui-system/components/engine/label-layout/shrink_wrap.png new file mode 100644 index 0000000000..59f755d6b3 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/shrink_wrap.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/label-layout/widget.png b/versions/4.0/zh/ui-system/components/engine/label-layout/widget.png new file mode 100644 index 0000000000..06fbe2f0cd Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/label-layout/widget.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/large-screen.md b/versions/4.0/zh/ui-system/components/engine/large-screen.md new file mode 100644 index 0000000000..6036c278ec --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/large-screen.md @@ -0,0 +1,13 @@ +# 安卓大屏幕适配 + +![hyper-resolution.png](./large-screen/hyper-resolution.png) + +Cocos Creator 目前已经可以适配大屏幕设备以及折叠屏。 + +对于大屏幕的安卓设备,我们建议的适配方案是: + +- 对于 2D/UI 节点上的用于适配 Widget 组件的 Align Mode 设置为 On_WINDOW_RESIZE 或 ALWAYS。 + + ![scale-ui.png](./large-screen/scale-ui.png) + +- 参考 [适用于大屏设备开发的响应式布局](https://developer.android.com/large-screens)。 diff --git a/versions/4.0/zh/ui-system/components/engine/large-screen/hyper-resolution.png b/versions/4.0/zh/ui-system/components/engine/large-screen/hyper-resolution.png new file mode 100644 index 0000000000..5d733ecc7a Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/large-screen/hyper-resolution.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/large-screen/scale-ui.png b/versions/4.0/zh/ui-system/components/engine/large-screen/scale-ui.png new file mode 100644 index 0000000000..2d22bb6147 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/large-screen/scale-ui.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/list-with-data.md b/versions/4.0/zh/ui-system/components/engine/list-with-data.md new file mode 100644 index 0000000000..8af6174adc --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/list-with-data.md @@ -0,0 +1,148 @@ +# 制作动态生成内容的列表 + +UI 界面只有静态页面内容是不够的,我们会遇到很多需要由一组数据动态生成多个元素组成的 UI 面板,比如选人界面、物品栏、选择关卡等等。 + +## 准备数据 + +以物品栏为例,我们要动态生成一个物品,大概需要这样的一组数据: + +- 物品 id +- 图标 id,我们可以在另一张资源表中建立图标 id 到对应 spriteFrame 的索引 +- 物品名称 +- 出售价格 +- ... + +下面我们将会结合脚本介绍如何定义和使用数据,如果您对 Cocos Creator 的脚本系统还不熟悉,可以先从 [脚本开发指南](../../../scripting/index.md) 一章开始学习。 + +### 自定义数据类 + +对于大多数游戏来说,这些数据通常都来自于服务器或本地的数据库,现在我们为了展示流程,暂时把数据存在列表组件里就可以了。您可以新建一个脚本 `ItemList.ts`,并添加如下的属性: + +```ts +@ccclass('Item') +export class Item { + @property + id = 0; + @property + itemName = ''; + @property + itemPrice = 0; + @property(SpriteFrame) + iconSF: SpriteFrame | null = null; +} + +@ccclass +export class ItemList extends Component { + @property([Item]) + items: Item[] = []; + @property(Prefab) + itemPrefab: Prefab | null = null; + + onLoad() { + for (let i = 0; i < this.items.length; ++i) { + const item = instantiate(this.itemPrefab); + const data = this.items[i]; + this.node.addChild(item); + item.getComponent('ItemTemplate').init(data); + } + } +} +``` + +上面脚本的前半部分我们声明了一个叫做 `Item` 的数据类,用来存放我们展示物品需要的各种数据。注意这个类并没有继承 `Component`,因此它不是一个组件,但可以被组件使用。关于声明自定义类的更多内容,请查阅 [装饰器](../../../scripting/decorator.md) 文档。 + +下半部分是正常的组件声明方式,这个组件中只有一个 `items` 属性,上面的声明方式将会给我们一个由 `Item` 类组成的数组,我们可以在 **属性检查器** 中为每个 `Item` 元素设置数据。 + +新建一个节点并将 `ItemList.ts` 添加上去,我们可以在 **属性检查器** 里找到 `Items` 属性,要开始创建数据,需要先将数组的容量设为大于 0 的值。让我们将容量设为 3,并将每个元素的数据如下图设置。 + +![item list](list-with-data/itemlist.png) + +这样我们最基本的数据就准备好了,如果您在制作有很多内容的游戏,请务必使用 excel、数据库等更专业的系统来管理您的数据,将外部数据格式转化为 Cocos Creator 可以使用的 TypeScript 和 JSON 格式都非常容易。 + +## 制作表现:Prefab 模板 + +接下来我们还需要一个可以在运行时用来实例化每个物品的模板资源 —— [Prefab 预制](../../../asset/prefab.md)。这个 Prefab 的结构如下图所示: + +![item template](list-with-data/item-template.png) + +`icon`、`name`、`price` 子节点之后就会用来展示图标、物品名称和价格的数据。 + +### 模板组件绑定 + +您在拼装 Prefab 时可以根据自己的需要自由发挥,上图中展示的仅仅是一个结构的例子。有了物品的模板结构,接下来我们需要一个组件脚本来完成节点结构的绑定。新建一个 `ItemTemplate.ts` 的脚本,并将其添加到刚才制作的模板节点上。该脚本内容如下: + +```ts +@ccclass +export class ItemTemplate extends Component { + @property + public id = 0; + @property(Sprite) + public icon: Sprite | null = null; + @property(Label) + public itemName: Label | null = null; + @property(Label) + public itemPrice: Label | null = null; +} +``` + +接下来将对应的节点拖拽到该组件的各个属性上: + +![item binding](list-with-data/item-binding.png) + +注意 `id` 这个属性我们会直接通过数据赋值,不需要绑定节点。 + +### 通过数据更新模板表现 + +接下来我们需要继续修改 `ItemTemplate.ts`,为其添加接受数据后进行处理的逻辑。在上述脚本后面加入以下内容: + +```ts +// data: { id, iconSF, itemName, itemPrice } +init(data: Item) { + this.id = data.id; + this.icon.spriteFrame = data.iconSF; + this.itemName.string = data.itemName; + this.itemPrice.string = data.itemPrice; +} +``` + +`init` 方法接受一个数据对象,并使用这个对象里的数据更新各个负责表现组件的相应属性。现在我们可以将 `Item` 节点保存成一个 Prefab 了,这就是我们物品的模板。 + +## 根据数据生成列表内容 + +现在让我们回到 `ItemList.ts` 脚本,接下来要添加的是物品模板 Prefab 的引用,以及动态生成列表的逻辑。 + +```ts +//... +@property(Prefab) +itemPrefab: Prefab | null = null; + +onLoad () { + for (let i = 0; i < this.items.length; ++i) { + const item = instantiate(this.itemPrefab); + const data = this.items[i]; + this.node.addChild(item); + item.getComponent('ItemTemplate').init(data); + } +} +``` + +在 `onLoad` 回调方法里,我们依次遍历 `items` 里存储的每个数据,以 `itemPrefab` 为模板生成新节点并添加到 `ItemList.ts` 所在节点上。之后调用 `ItemTemplate.ts` 里的 `init` 方法,更新每个节点的表现。 + +现在我们可以为 `ItemList.ts` 所在的节点添加一个 **Layout** 组件,通过 **属性检查器** 下方的 **添加组件 -> UI -> Layout**,然后设置 **Layout** 组件的以下属性: + +- `Type`:`HORIZONTAL` +- `Resize Mode`:`CONTAINER` + +别忘了把 `item` Prefab 拖拽到 `ItemList` 组件的 `itemPrefab` 属性里。您还可以为这个节点添加一个 **Sprite** 组件,作为列表的背景。 + +完成后的 `itemList` 节点属性如下: + +![itemlist complete](list-with-data/itemlist-complete.png) + +## 预览效果 + +最后运行预览,可以看到类似这样的效果(具体效果和您制作的物品模板,以及输入的数据有关): + +![result](list-with-data/result.png) + +注意前面步骤中添加 **Layout** 组件并不是必须的,**Layout** 能够帮助您自动排列列表中的节点元素,但您也可以用脚本程序来控制节点的排列。我们通常还会配合 **ScrollView** 滚动视图组件一起使用,以便在有限的空间内展示大量内容。可以配合 [自动布局](auto-layout.md) 和 [滚动视图](../editor/scrollview.md) 一起学习。 diff --git a/versions/4.0/zh/ui-system/components/engine/list-with-data/item-binding.png b/versions/4.0/zh/ui-system/components/engine/list-with-data/item-binding.png new file mode 100644 index 0000000000..ecc0a57c1f Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/list-with-data/item-binding.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/list-with-data/item-template.png b/versions/4.0/zh/ui-system/components/engine/list-with-data/item-template.png new file mode 100644 index 0000000000..f79470545c Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/list-with-data/item-template.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/list-with-data/itemlist-complete.png b/versions/4.0/zh/ui-system/components/engine/list-with-data/itemlist-complete.png new file mode 100644 index 0000000000..44719c4f29 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/list-with-data/itemlist-complete.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/list-with-data/itemlist.png b/versions/4.0/zh/ui-system/components/engine/list-with-data/itemlist.png new file mode 100644 index 0000000000..16f1a8bb06 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/list-with-data/itemlist.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/list-with-data/result.png b/versions/4.0/zh/ui-system/components/engine/list-with-data/result.png new file mode 100644 index 0000000000..a9d37263d1 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/list-with-data/result.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/multi-resolution.md b/versions/4.0/zh/ui-system/components/engine/multi-resolution.md new file mode 100644 index 0000000000..57c19ff13b --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/multi-resolution.md @@ -0,0 +1,60 @@ +# 多分辨率适配方案 + +Cocos Creator 3.0 在整体设计上沿用了 Cocos Creator 2.x 一套资源适配多种分辨率屏幕的方案。简单概括来说,我们通过以下几个部分完成多分辨率适配解决方案: + +- **Canvas(画布)** 组件随时获得设备屏幕的实际分辨率并对场景中所有渲染元素进行适当的缩放。 +- **Widget(对齐挂件)** 组件添加给 UI 节点,能够根据需要将元素对齐目标节点(默认是父节点)的不同参考位置。 +- **Label(文字)** 组件内置了提供各种动态文字排版模式的功能,当文字的约束框由于 Widget 对齐要求发生变化时,文字会根据需要呈现完美的排版效果。 +- **Sliced Sprite(九宫格精灵图)** 则提供了可任意指定尺寸的图像,同样可以满足各式各样的对齐要求,在任何屏幕分辨率上都显示高精度的图像。 + +接下来我们首先了解设计分辨率、屏幕分辨率的概念,才能理解 [Canvas(画布)](../editor/canvas.md) 组件的缩放作用。 + +## 设计分辨率和屏幕分辨率 + +**设计分辨率** 是内容生产者在制作场景时使用的分辨率蓝本,而 **屏幕分辨率** 是游戏在设备上运行时的实际屏幕显示分辨率。 + +通常设计分辨率会采用市场目标群体中使用率最高的设备的屏幕分辨率,比如目前安卓设备中 `800 x 480` 和 `1280 x 720` 两种屏幕分辨率,或 iOS 设备中 `1136 x 640` 和 `960 x 640` 两种屏幕分辨率。这样当美术或策划使用设计分辨率设置好场景后,就可以自动适配最主要的目标人群设备。 + +那么当设计分辨率和屏幕分辨率出现差异时,会如何进行适配呢? + +假设我们的设计分辨率为 `800 x 480`,美术制作了一个同样分辨率大小的背景图像。 + +![design resolution](multi-resolution/design_resolution.png) + +### 设计分辨率和屏幕分辨率宽高比相同 + +在屏幕分辨率的宽高比和设计分辨率相同时,假如屏幕分辨率是 `1600 x 960`,正好将背景图像放大 1600/800 = **2 倍** 就可以完美适配屏幕。这是最简单的情况,这里不再赘述。 + +### 设计分辨率宽高比大于屏幕分辨率,适配高度避免黑边 + +假设屏幕分辨率是 `1024 x 768`,在下图中以红色方框表示设备屏幕可见区域。我们使用菜单栏 **项目 -> 项目设置 -> 项目数据** 面板中的 **适配高度**(`Fit Height`)模式,将设计分辨率的高度自动撑满屏幕高度,也就是将场景图像放大到 768/480 = **1.6 倍**。 + +![fit height](multi-resolution/fit_height.png) + +这是设计分辨率宽高比大于屏幕分辨率时比较理想的适配模式,如上图所示,虽然屏幕两边会裁剪掉一部分背景图,但能够保证屏幕可见区域内不出现任何穿帮或黑边。之后可以通过 Widget(对齐挂件)调整 UI 元素的位置,来保证 UI 元素出现在屏幕可见区域里,我们在下一节 [对齐策略](widget-align.md) 中将会详细介绍。 + +### 设计分辨率宽高比小于屏幕分辨率,适配宽度避免黑边 + +假设屏幕分辨率是 `1920 x 960`,同样在下图中以红色方框表示设备屏幕可见区域。我们使用菜单栏 **项目 -> 项目设置 -> 项目数据** 面板中的 **适配宽度**(`Fit Width`)模式,将设计分辨率的宽度自动撑满屏幕宽度,也就是将场景放大 1920/800 = **2.4 倍**。 + +![fit width](multi-resolution/fit_width.png) + +在设计分辨率宽高比较小时,使用这种模式会裁剪掉屏幕上下一部分背景图。 + +### 不管屏幕宽高比如何,完整显示设计分辨率中的所有内容,允许出现黑边 + +最后一个例子,我们屏幕分辨率假设为 `640 x 960` 的竖屏,如果要确保背景图像完整的在屏幕中显示,需要同时开启 **适配高度** 和 **适配宽度**,这时场景图像的缩放比例是按照 **屏幕宽/设计宽** 和 **屏幕高/设计高** 中较小的来计算的,在下图的例子中,由于 **屏幕宽/设计宽** 小于 **屏幕高/设计高**,就会以宽度为准计算缩放倍率,即 640/800 = **0.8 倍**。 + +![show all](multi-resolution/show_all.png) + +在这种显示模式下,屏幕上可能会出现黑边,或超出设计分辨率的场景图像(穿帮)。尽管一般情况下开发者会尽量避免黑边,但如果需要确保设计分辨率范围的所有内容都显示在屏幕上,也可以采用这种模式。 + +### 根据屏幕宽高比,自动选择适配宽度或适配高度 + +如果对于屏幕周围可能被剪裁的内容没有严格要求,也可以不开启任何适配模式,这时会根据屏幕宽高比自动选择 **适配高度** 或 **适配宽度** 来避免黑边。也就是说,设计分辨率宽高比大于屏幕分辨率时,会自动适配高度(上面第一张图);设计分辨率宽高比小于屏幕分辨率时,会自动适配宽度(上面第二张图)。 + +### 设计分辨率只能在项目设置中统一配置 + +当前的设计模式并没有加入多分辨率适配方式,所以在同一个项目里的多个 Canvas 的设计分辨率仍然采用同一套设计分辨率以及适配方案,开发者可以通过 **项目 -> 项目设置 -> 项目数据** 页面配置。 + +![resolution-config](multi-resolution/resolution_config.png) diff --git a/versions/4.0/zh/ui-system/components/engine/multi-resolution/canvas_property.png b/versions/4.0/zh/ui-system/components/engine/multi-resolution/canvas_property.png new file mode 100644 index 0000000000..d9ad901f04 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/multi-resolution/canvas_property.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/multi-resolution/design_resolution.png b/versions/4.0/zh/ui-system/components/engine/multi-resolution/design_resolution.png new file mode 100644 index 0000000000..6e9b88bcdb Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/multi-resolution/design_resolution.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/multi-resolution/fit_height.png b/versions/4.0/zh/ui-system/components/engine/multi-resolution/fit_height.png new file mode 100644 index 0000000000..648d8a35da Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/multi-resolution/fit_height.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/multi-resolution/fit_width.png b/versions/4.0/zh/ui-system/components/engine/multi-resolution/fit_width.png new file mode 100644 index 0000000000..e319d57a30 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/multi-resolution/fit_width.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/multi-resolution/resolution_config.png b/versions/4.0/zh/ui-system/components/engine/multi-resolution/resolution_config.png new file mode 100644 index 0000000000..6ab88f3dfe Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/multi-resolution/resolution_config.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/multi-resolution/show_all.png b/versions/4.0/zh/ui-system/components/engine/multi-resolution/show_all.png new file mode 100644 index 0000000000..1dbb36cf91 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/multi-resolution/show_all.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/priority.md b/versions/4.0/zh/ui-system/components/engine/priority.md new file mode 100644 index 0000000000..af67e17ed2 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/priority.md @@ -0,0 +1,35 @@ +# 渲染排序说明 + +## 2D 渲染节点排序 + +2D 渲染节点可分为在 Canvas 下的节点和不在 Canvas 下的节点两种: + +- 在 Canvas 下的节点默认使用 **[UI 节点排序](#ui-节点排序)** ,如需自定义渲染排序则使用 [Sorting2D 组件](../../../engine/rendering/sorting-2d.md)。 + +- 不在 Canvas 下的节点,用户可选择通过 [自定义材质](ui-material.md) 来开启深度检测实现和 3D 物体的遮挡显示,开启后会按照物体的 Z 轴坐标进行遮挡渲染(可参考范例 **2d-rendering-in-3d**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/2D) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/2d-rendering-in-3d))。 + +## UI 节点排序 + +UI 节点特指在 Canvas 节点下的 UI 节点,节点的混合是严格按照节点树进行排序的。UI 的渲染排序采用的是一个深度优先的排序方式,节点树的排序方式即为最终渲染数据提交的顺序。所以用户可以通过设置节点的 siblingIndex 来改变节点在父节点下的顺序,从而改变渲染顺序。 + +举个例子: + +![priority.png](priority/priority.png) + +上图中整体的渲染顺序则是:**A -> a1 -> a2 -> B -> b1 -> C**,在屏幕上的呈现状态为:**C -> b1 -> B -> a2 -> a1 -> A** 即 **从上往下**。 + +setSiblingIndex 的使用说明:此操作是用于变更当前节点在父节点 children 数组中的位置,如果通过脚本在运行时设置,则变更过后的节点树数据不会被序列化。传入的参数如果大于 children 数组的长度,则会设置到数组最后,如果在范围内,则会插入到对应的位置上。所以此操作和节点树的状态实时相关,用户需要知道节点树当前的状态并进行操作才能得到预期的效果。 + +## 注意事项 + +排序是一个很简单的功能,但是最终的呈现却是根据不同平台提供的渲染能力来的。因此,在这里说明一下,如果遇到了 UI 渲染出错,花屏,闪屏等现象,首先要检查的就是场景里所有相机(Camera 和 Canvas)的 **ClearFlag**,确保 **场景里必须有一个相机要执行 Solid_Color 清屏操作**。 + +具体如何设置 **ClearFlag**,可参考以下几种情况: + +- 如果场景中只有一个 UI Canvas 或者 3D Camera,那么 **ClearFlag** 属性设置为 `Solid_Color`。 +- 如果场景中包含 2D 背景层、3D 场景层、 2D UI 层,则: + - 用于 3D 场景渲染的摄像机,请确保第一个渲染的摄像机是 **SOLID_COLOR**(如果有配置天空盒,则设置为 **SKYBOX** ),其余摄像机根据项目需求决定。 + - 用于 UI 渲染(Canvas下面)的摄像机,要用 **DEPTH ONLY**。 + - 如果某个摄像机的设置了targetTexture,请设置为 **SOLID_COLOR**。 + + ![sort](./priority/sort.png) diff --git a/versions/4.0/zh/ui-system/components/engine/priority/intersperse.png b/versions/4.0/zh/ui-system/components/engine/priority/intersperse.png new file mode 100644 index 0000000000..47817c23aa Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/priority/intersperse.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/priority/overlay.png b/versions/4.0/zh/ui-system/components/engine/priority/overlay.png new file mode 100644 index 0000000000..95bd5f33f9 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/priority/overlay.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/priority/priority.png b/versions/4.0/zh/ui-system/components/engine/priority/priority.png new file mode 100644 index 0000000000..7405de82d2 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/priority/priority.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/priority/sort.png b/versions/4.0/zh/ui-system/components/engine/priority/sort.png new file mode 100644 index 0000000000..e9d544e6ee Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/priority/sort.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/sliced-sprite.md b/versions/4.0/zh/ui-system/components/engine/sliced-sprite.md new file mode 100644 index 0000000000..175555e178 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/sliced-sprite.md @@ -0,0 +1,34 @@ +# 制作可任意拉伸的 UI 图像 + +UI 系统核心的设计原则是能够自动适应各种不同的设备屏幕尺寸,因此我们在制作 UI 时需要正确设置每个控件元素的尺寸(size),并且让每个控件元素的尺寸能够根据设备屏幕的尺寸进行自动的拉伸适配。为了实现这一点,就需要使用九宫格格式的图像来渲染这些元素。这样即使使用很小的原始图片也能生成覆盖整个屏幕的背景图像,一方面节约游戏包体空间,另一方面能够灵活适配不同的排版需要。 + +![compare](sliced-sprite/compare.png) + +上图右边为原始贴图大小的显示,左边是选择 Sliced 模式并放大 `size` 属性后的显示效果。 + +## 编辑图像资源的九宫格切分 + +要使用可以无限放大的九宫格图像效果,我们需要先对图像资源进行九宫格切分。首先打开 **Sprite 编辑器**,在 **资源管理器** 中选中图像资源,然后点击 **属性检查器** 最下面的 **编辑** 按钮。如果您的窗口高度不够,可能需要向下滚动 **属性检查器** 才能看到下面的按钮。 + +打开 **Sprite 编辑器** 以后,可以看到图像周围有一圈绿色的线条,表示当前九宫格分割线的位置。将鼠标移动到分割线上,可以看到光标形状改变了,这时候就可以按下并拖拽鼠标来更改分割线的位置。 + +我们分别拖动上下左右四条分割线,将图像切分成九宫格,九个区域在 Sprite 尺寸(`size`)变化时会应用不同的缩放策略,见下图: + +![sliced](sliced-sprite/editing.png) + +而下图中描述了不同区域缩放时的示意(图片来自 [Yannick Loriot 的博客](http://yannickloriot.com/2011/12/create-buttons-in-cocos2d-by-using-cccontrolbutton/)): + +![scaling](sliced-sprite/scaling.png) + +完成切分后别忘记点击 **Sprite 编辑器** 右上角的绿色对勾来保存对资源的修改。 + +## 设置 Sprite 组件使用 Sliced 模式 + +准备好九宫格切分的资源后,就可以修改 Sprite 的显示模式并通过修改 `size` 来制作可任意指定尺寸的 UI 元素了。 + +1. 首先选中场景中的 Sprite 节点,将 Sprite 的 `Type` 属性设为 `Sliced`。 +2. 然后通过 [矩形变换工具](../../../editor/scene/index.md) 拖拽控制点使节点的 `size` 属性变大。您也可以直接在 **属性检查器** 中输入数值来修改 `size` 属性。如果图像资源是用九宫格的形式生产的,那么不管 Sprite 如何放大,都不会产生模糊或变形。 + +## 注意事项 + +在使用 **矩形变换工具** 或直接修改 Sliced Sprite 的 `size` 属性时,注意 `size` 的属性值不能为负数,否则不能以 Sliced 模式正常显示。 diff --git a/versions/4.0/zh/ui-system/components/engine/sliced-sprite/compare.png b/versions/4.0/zh/ui-system/components/engine/sliced-sprite/compare.png new file mode 100644 index 0000000000..7fb2c339ca Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/sliced-sprite/compare.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/sliced-sprite/edit_from_asest.png b/versions/4.0/zh/ui-system/components/engine/sliced-sprite/edit_from_asest.png new file mode 100644 index 0000000000..79516ede3b Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/sliced-sprite/edit_from_asest.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/sliced-sprite/editing.png b/versions/4.0/zh/ui-system/components/engine/sliced-sprite/editing.png new file mode 100644 index 0000000000..dda62cba72 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/sliced-sprite/editing.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/sliced-sprite/scaling.png b/versions/4.0/zh/ui-system/components/engine/sliced-sprite/scaling.png new file mode 100644 index 0000000000..12e4832345 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/sliced-sprite/scaling.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/trim.md b/versions/4.0/zh/ui-system/components/engine/trim.md new file mode 100644 index 0000000000..258bd25021 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/trim.md @@ -0,0 +1,35 @@ +# 图像资源的自动剪裁 + +导入图像资源后生成的 SpriteFrame 默认会进行自动剪裁,去除原始图片周围的透明像素区域。这样我们在使用 SpriteFrame 渲染 Sprite 时,将会获得有效图像更精确的大小。当 SpriteFrame 为自动剪裁时,下图中自动剪裁的相关信息为置灰状态,不可修改: + +![trim inspector](trim/trim_inspector.png) + +## Sprite 组件剪裁相关设置详解 + +和图片裁剪相关的 **Sprite** 组件设置有以下两个: + +- `Trim` 勾选后将在渲染 Sprite 图像时去除图像周围的透明像素,我们将看到刚好能把图像包裹住的约束框。取消勾选,Sprite 节点的约束框会包括透明像素的部分。 + +- `Size Mode` 用来将节点的尺寸设置为原图或原图裁剪透明像素后的大小,通常用于在序列帧动画中保证图像显示为正确的尺寸。有以下几种选择: + + - `TRIMMED` 选择该选项,会将节点的尺寸(size)设置为原始图片裁剪掉透明像素后的大小。 + + - `RAW` 选择该选项,会将节点尺寸设置为原始图片包括透明像素的大小。 + + - `CUSTOM` 自定义尺寸,用户在使用 **矩形变换工具** 拖拽改变节点的尺寸,或通过修改 `Size` 属性,或在脚本中修改 `width` 或 `height` 后,都会自动将 `Size Mode` 设为 `CUSTOM`。表示用户将自己决定节点的尺寸,而不需要考虑原始图片的大小。 + +下图中展示了两种常见组合的渲染效果: + +![trim compare](trim/trim-compare.png) + +## 自带位置信息的序列帧动画 + +有很多动画师在绘制序列帧动画时,会使用一张较大的画布,然后将角色在动画中的运动直接通过角色在画布上的位置变化表现出来。在使用这种素材时,我们需要将 **Sprite 组件** 的 `Trim` 设为 `false`,将 `Size Mode` 设为 `RAW`。这样动画在播放每个序列帧时,都将使用原始图片的尺寸,并保留图像周围透明像素的信息,这样才能正确显示绘制在动画中的角色位移。 + +而 `Trim` 设为 `true`,则是在位移完全由角色位置属性控制的动画中,更推荐使用的方式。 + +## TexturePacker 设置 + +在制作序列帧动画时,我们通常会使用 [TexturePacker](https://www.codeandweb.com/texturepacker) 这样的工具将序列帧打包成图集,并在导入后通过图集资源下的 `SpriteFrame` 来使用。在 TexturePacker 中输出图集资源时,Sprites 分类下的 **Trim mode** 请选择 `Trim`,一定不要选择 `Crop, flush position`,否则透明像素剪裁信息会丢失,您在使用图集里的资源时也就无法获得原始图片未剪裁的尺寸和偏移信息了。目前建议使用 TexturePacker **4.x** 以上版本进行打包,以防止低版本导出数据不一致造成的导入失败。 + +![trim texturePacker](trim/trim-texturepacker.png) diff --git a/versions/4.0/zh/ui-system/components/engine/trim/trim-compare.png b/versions/4.0/zh/ui-system/components/engine/trim/trim-compare.png new file mode 100644 index 0000000000..89d1254809 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/trim/trim-compare.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/trim/trim-texturepacker.png b/versions/4.0/zh/ui-system/components/engine/trim/trim-texturepacker.png new file mode 100644 index 0000000000..0ec775a8b8 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/trim/trim-texturepacker.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/trim/trim_inspector.png b/versions/4.0/zh/ui-system/components/engine/trim/trim_inspector.png new file mode 100644 index 0000000000..45cf0e0af9 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/trim/trim_inspector.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/ui-batch.md b/versions/4.0/zh/ui-system/components/engine/ui-batch.md new file mode 100644 index 0000000000..dce0a90caa --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/ui-batch.md @@ -0,0 +1,78 @@ +# 2D 渲染组件合批规则说明 + +## 合批条件说明 + +2D 渲染组件合批需要满足以下几点条件: + +| 条件 | 说明 | +| :-- | :--- | +| 节点的 Layer 相同 | 由于 Layer 与节点是否渲染相关,因此不同的 Layer 之间不能进行合批。 | +| 使用的材质相同 | **材质相同是合批的必要要求**。
由于 Creator 使用的是 **材质实例化** 的机制,所以当用户在设置了材质的 `uniform` 之后,材质会进行实例化,实例化之后的材质是无法进行合批的。
如果用户在自定义材质中设置了 `uniform`(对应组件无法进行合批),在 `uniform` 值使用完毕后想要该组件参与合批,那么可通过 `CustomMaterial` 接口将材质资源重新赋值给组件即可。 | +| 渲染组件上所添加的材质的 `BlendState` 和 `DepthStencilState` 属性设置相同 | `DepthStencilState` 属性用于控制组件的深度检测和模板缓冲,是由引擎自动控制(用于 Mask 的效果实现)的,一般来说用户不需关心该属性的设置。 | +| 渲染组件的顶点信息要在同一个 **buffer** 中上传(v3.4.1 新增) | 一般情况下顶点信息是由引擎统一进行分配和管理的,用户无需关心。若想要了解更多相关信息,请参考下文 **MeshBuffer 合批说明** 部分的内容。 | +| 贴图源以及贴图采样相同 | 一般情况下该条件是影响合批最主要的因素,尤其对于 Sprite 和 Label 来说,贴图很容易产生差别导致无法合批。Creator 提供了部分方法用于更好地实现合批,详情请参考下文 **合批方法说明** 部分的内容。 | + +## 合批方法说明 + +结合以上的合批条件说明,我们可以通过一些方法来实现更好的合批方法。需要额外说明的是,2D 渲染组件的渲染数据采集使用的是基于 **节点树** 的渲染方式,而有部分组件无法合批,且会打断其他组件合批,需要用户进行分模块管理节点树布局,以达到更好的合批效果。无法合批的组件包括: + +- 内置组件 Mask、Graphics 和 UIMeshRenderer 组件由于材质不同和数据组织方式的差异,无法与其他组件合批; +- TiledMap、Spine 和 DragonBones 这三个中间件组件则是遵循自己的内部合批机制。 + +对于 Sprite 和 Label 组件来说,因为贴图很容易产生差别,导致无法合批。因此我们提供了以下方法可以更好地实现合批,用户可以根据需要参考使用: + +- 对于 Sprite 组件,我们提供了 [静态合图](../../../asset/auto-atlas.md) 和 [动态合图](../../../advanced-topics/dynamic-atlas.md) 两种合批方案,通过将图片纹理合并,即可在其他条件满足的情况下进行合批。 +- 对于 Label 组件,我们提供了 Bitmap 的缓存方法,通过将 Label 的纹理合图,即可实现 Sprite 和 Label 组件的合批,但需要注意的是,使用 Bitmap 缓存方式的 Label 不可频繁变动文字内容。 + +一般来说,通过控制材质和节点树状态,然后配合合图方法,便能够达到较好的合批效果。 + +## MeshBuffer 合批说明 + +由于合批要求渲染对象的顶点在同一个 MeshBuffer 中,而以下几种情况则会造成 MeshBuffer 切换。 + +### v3.4.1 之前 + +场景中要绘制的顶点数量超过了 MeshBuffer 所能容纳的最大顶点数量(65535 个)。 + +### v3.4.1 之后 + +由于我们在 v3.4.1 中采用了新的渲染数据提交设计,所以需要注意以下几点: + +1. **项目设置 -> Macro Configurations** 面板中的 **BATCHER2D_MEM_INCREMENT** 的值会影响每个 MeshBuffer 可容纳的最大顶点数量,当这个值越大,同一个 MeshBuffer 可容纳的 2D 渲染对象也就越多。但与此同时,内存增加的幅度也会越大,请用户结合自身项目规模酌情设置大小。 + +2. **BATCHER2D_MEM_INCREMENT** 的单位为 **KB**,与可容纳的顶点数量之间的转换关系如下: + + 引擎内置标准的顶点格式为 [vfmtPosUvColor](https://github.com/cocos/cocos-engine/blob/v3.4.1/cocos/2d/renderer/vertex-format.ts#L43),在引擎中的定义为: + + ```ts + export const vfmtPosUvColor = [ + // RGB32F 表示 3 个 32 位的 float + new Attribute(AttributeName.ATTR_POSITION, Format.RGB32F), + // RG32F 表示 2 个 32 位的 float + new Attribute(AttributeName.ATTR_TEX_COORD, Format.RG32F), + // RGBA32F 表示 4 个 32 位的 float + new Attribute(AttributeName.ATTR_COLOR, Format.RGBA32F), + ]; + ``` + + 由此我们可以得到每个顶点占用 9 个 `float` 值,每个顶点占用的空间为:`1 * 9 * 4 / 1024(KB)`,其中: + + - **1** 表示顶点数; + + - **9** 表示 `vfmtPosUvColor` 条件下每个顶点占用的 `float` 值; + + - **4** 表示每个 `float` 占用的字节数; + + - **1024** 表示字节与 KB 转换单位。 + + 因此 **BATCHER2D_MEM_INCREMENT** 的默认值 144KB,表示可容纳 `144 * 1024 / (9 * 4)= 4096` 个标准格式的顶点。需要注意的是:同一 MeshBuffer 容纳的最大顶点数不可超过 **65535** 个,即 **BATCHER2D_MEM_INCREMENT** 的最大值不可大于 `65535 * 9 * 4 / 1024 ≈ 2303.96(KB)`。 + +3. 目前 2D 渲染使用的核心为 **static VB**,其主要内容包括: + + - **VB 固定** 伴随着组件的整个生命周期而存在,而决定渲染顺序的 IB 则放弃缓存,每帧进行录制。由于组件的 VB 信息多而复杂,直接保存下来可针对组件的内存段进行操作,而且保存下来可以避免无用的更新。 + + - IB 每帧填充。相对于 VB 来说,IB 的结构简单、数量较少,并且会直接决定渲染顺序,所以在每帧遍历时重新填充 IB 消耗较小,且无需管理复杂的缓存机制。 + + - 由于 VB 固定,所以在整个组件的生命周期中,VB 会在最开始就分配好,直到组件销毁,所以在组件加载时,便会向预先分配好的 MeshBuffer 中申请好想要使用的 VB,然后在销毁时归还。 + + - 当 MeshBuffer 已经无法分配出组件需要的 VB 时,系统便会新创建一个大小为 **BATCHER2D_MEM_INCREMENT** 的 MeshBuffer 来继续分配 VB。 diff --git a/versions/4.0/zh/ui-system/components/engine/ui-material.md b/versions/4.0/zh/ui-system/components/engine/ui-material.md new file mode 100644 index 0000000000..a69842c085 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/ui-material.md @@ -0,0 +1,29 @@ +# 2D 渲染对象自定义材质 + +2D 渲染对象的自定义材质是拓展 2D 渲染对象表现和提升 2D 渲染对象自身能力的最佳实践,可以通过自定义材质实现溶解、外发光等酷炫的渲染效果。 + +v3.0 的 2D 渲染组件大部分都支持使用自定义材质,其使用界面如下图(以 Sprite 组件为例): + +![UIMaterial](ui-material/UIMaterial.png) + +其使用方法与其他内置材质并无不同,将要使用的材质拖拽到 **CustomMaterial** 属性框中即可。 + +## 注意事项 + +1. 当未指定自定义材质时,会使用内置材质进行渲染,面板功能及使用方法可参考 [Sprite 组件参考](../editor/sprite.md) 文档。 +2. 2D 渲染对象并不支持多材质,自定义材质的数量最多为一个。 +3. 请使用 **builtin-spine** 或 **builtin-sprite** 等 2D 专用 Shader 来自定义材质,请勿选择其他 3D 组件使用的 shader。 +4. 当使用了 2D 渲染对象自定义材质之后,面板上的 **Grayscale** 属性功能将会失效,用户可选择自行在材质中实现此功能。 +5. 若代码中设置了 BlendFactor,在使用了自定义材质后,会以自定义材质中 BlendFactor 的设置为准。 +6. 使用了自定义材质之后,组件的深度检测信息会以材质为准。如果想要实现和 3D 物体的遮挡,请使用自定义材质并开启深度检测。可参考范例 **2d-rendering-in-3d**([GitHub](https://github.com/cocos/cocos-test-projects/tree/v3.8/assets/cases/2D) | [Gitee](https://gitee.com/mirrors_cocos-creator/test-cases-3d/tree/v3.8/assets/cases/2d-rendering-in-3d))。 +7. 如果用户希望修改自定义材质的属性,可通过获取 2D 渲染组件上的 **customMaterial** 来进行操作,如下代码所示:(以 Sprite 为例): + + ```ts + let spriteComp = this.node.getComponent(Sprite); + let material = spriteComp.customMaterial; + //material.setProperty(propName,val) + ``` + +## 自定义 2D Shader + +如果内置的Shader不满足需求,可参考 [2D 精灵着色器:Gradient](../../../shader/write-effect-2d-sprite-gradient.md) 自定义 Shader。 diff --git a/versions/4.0/zh/ui-system/components/engine/ui-material/UIMaterial.png b/versions/4.0/zh/ui-system/components/engine/ui-material/UIMaterial.png new file mode 100644 index 0000000000..94ee731b7e Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/ui-material/UIMaterial.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/ui-material/dissolve.png b/versions/4.0/zh/ui-system/components/engine/ui-material/dissolve.png new file mode 100644 index 0000000000..fb87d4121c Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/ui-material/dissolve.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/usage-ui.md b/versions/4.0/zh/ui-system/components/engine/usage-ui.md new file mode 100644 index 0000000000..459d8b93c7 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/usage-ui.md @@ -0,0 +1,8 @@ +# UI 实践指南 + +- [多分辨率适配方案](multi-resolution.md) +- [对齐策略](widget-align.md) +- [文字排版](label-layout.md) +- [自动布局容器](auto-layout.md) +- [制作动态生成内容的列表](list-with-data.md) +- [制作可任意拉伸的 UI 图像](sliced-sprite.md) diff --git a/versions/4.0/zh/ui-system/components/engine/widget-align.md b/versions/4.0/zh/ui-system/components/engine/widget-align.md new file mode 100644 index 0000000000..9984ced164 --- /dev/null +++ b/versions/4.0/zh/ui-system/components/engine/widget-align.md @@ -0,0 +1,88 @@ +# 对齐策略 + +要实现完美的多分辨率适配效果,UI 元素按照设计分辨率中规定的位置呈现是不够的,当屏幕宽度和高度发生变化时,UI 元素要能够智能感知屏幕边界的位置,才能保证出现在屏幕可见范围内,并且分布在合适的位置。我们通过 **Widget(对齐挂件)** 来实现这种效果。 + +下面我们根据要对齐元素的类别来划分不同的对齐工作流: + +## 需要贴边对齐的按钮和小元素 + +对于暂停菜单、游戏金币这一类面积较小的元素,通常只需要贴着屏幕边对齐就可以了。这时只要几个简单的步骤: + +1. 在 **层级管理器** 中创建 2D 对象时会默认自动创建一个 Canvas 节点作为其父节点,这些元素节点都需要放在 Canvas 节点下 +2. 在元素节点上添加 Widget 组件 +3. 以对齐左下角为例,开启 `Left` 和 `Bottom` 的对齐。 +4. 然后设置好节点和屏幕边缘的距离,下图中左边距设为 40px,下边距设为 30px。 + + ![align left bottom](widget-align/align-basic.png) + +这样设置好 Widget 组件后,不管实际屏幕分辨率是多少,这个节点元素都会保持在屏幕左下角,而且节点约束框左边和屏幕左边距离保持 40px,节点约束框下边和屏幕下边距离保持 30px。 + +> **注意**:Widget 组件提供的对齐距离是参照子节点和父节点相同方向的约束框边界的。比如上面例子里选择了 `Left` 对齐左边,那么子节点约束框左边和父节点,约束框左边的距离就是我们设置的 40px。其中父节点也就是 Canvas 节点,约束框永远等于屏幕大小,前提是在编辑器顶部菜单栏中的 **项目 -> 项目设置 -> 项目数据** 中只勾选 **适配屏幕宽度** 或者 **适配屏幕高度**。 + +## 嵌套对齐元素 + +上面介绍了对齐屏幕边缘的做法,由于 Widget 默认的对齐参照物是父节点,所以我们也可以添加不同的节点层级,并且让每一级节点都使用自动对齐的功能。 + +我们下面用一个简单的例子来说明,假设我们有这样的节点层级关系: + +![nested nodes](widget-align/hierarchy.png) + +其中 `parent` 是一个面板,`button` 是一个按钮。我们可以分别为这两个节点添加 Widget 组件,并且分别设置对齐距离。 + +对于 `parent` 节点来说,对齐 `Canvas` 节点的左上角,距离都是 80px: + +![nested outer element](widget-align/nested-outer.png) + +对于 `button` 节点来说,对齐 `parent` 节点的左上角,距离都是 50px: + +![nested inner element](widget-align/nested-inner.png) + +依照这样的工作流程,就可以将 UI 元素按照显示区域或功能进行分组,并且不同级别的元素都可以按照设计进行对齐。 + +## 根据对齐需要自动缩放节点尺寸 + +以上我们展示的例子里,并没有同时对齐在同一轴向相反方向的两个边,如果我们要做一个占满整个屏幕宽度的面板,就可以同时勾选 `Left` 和 `Right` 对齐开关: + +![stretch](widget-align/stretch.png) + +当同时勾选相反的两个方向的对齐开关时,Widget 就获得了根据对齐需要修改节点尺寸(`Size`)的能力,上图中我们勾选了左右两个方向并设置了边距,Widget 就会根据父节点的宽度来动态设置节点的 `Width` 属性,表现出来就是不管在多宽的屏幕上,我们的面板距离屏幕左右两边的距离永远保持 100px。 + +### 制作和屏幕大小保持一致的节点 + +利用自动缩放节点的特性,我们可以通过设置节点的 Widget 组件,使节点的尺寸和屏幕大小保持一致。 + +要制作这样的节点,首先要保证该节点的父节点尺寸能够保持和屏幕大小一致,Canvas 节点就是一个最好的选择。接下来按照下图的方式设置该节点的 Widget 组件: + +![full screen node](widget-align/full-screen.png) + +就可以在运行时时刻保持该节点和 Canvas 节点的尺寸完全一致,也就是和屏幕大小一致。经过这样设置的节点,其子节点也可以使用同样的设置来传递屏幕实际尺寸。 + +> **注意**:若要实现该功能,则需要在编辑器顶部菜单栏的 **项目 -> 项目设置 -> 项目数据** 中只勾选 **适配屏幕宽度** 或者 **适配屏幕高度**。 + +## 设置百分比对齐距离 + +Widget 组件上开启某个方向的对齐之后,除了指定以像素为单位的边距以外,我们还可以输入百分比数值(例如:通过 **单击** 方框内圈出的符号),这样 Widget 会以父节点相应轴向的宽度或高度乘以输入的百分比,计算出实际的边距值。 + +还是看看实际的例子,我们还是以一个直接放在 Canvas 下的子节点为例,我们希望这个节点面板保持在屏幕右侧,并且总是占据 60% 的屏幕总高度。那么按照下图所示设置 Widget 组件就可以实现这个效果: + +![percentage](widget-align/percentage.png) + +Widget 在对齐方向开启时输入边距值时,可以按照需要混合像素单位和百分比单位的使用。比如左边需要对齐屏幕中心,则 `Left` 方向输入 `50%`,右边需要对齐屏幕边缘,则 `Right` 方向输入 `20px`,最后计算子节点位置和尺寸时,所有的边距都会先根据父节点的尺寸换算成像素距离,然后再进行摆放。 + +利用百分比对齐距离,我们可以制作出根据屏幕大小无限缩放的 UI 元素,发挥你的想象力,一套资源适配数千种机型不是问题! + +## 运行时每帧更新对齐和优化策略 + +Widget 组件一般用于场景在目标设备上初始化时定位每个元素的位置,但一旦场景初始化完毕,很多时候我们就不需要 Widget 组件再进行对齐了。这里有个重要的属性 `alignOnce` 用于确保 Widget 组件只在初始化时执行对齐定位的逻辑,在运行时不再消耗时间来进行对齐。 + +![alignOnce](widget-align/align-once.png) + +若 **对齐模式(AlignMode)** 设置为 `ONCE` 或者 `ON_WINDOW_RESIZE`,且在组件初始化时执行过一次对齐定位,引擎就会自动将 Widget 组件的 `enabled` 属性设为 `false`,禁用 Widget 组件来关闭之后的每帧自动更新,避免重复定位。 + +若需要在运行时变更定位,则需要手动将 **AlignMode** 设置为 `ALWAYS`,或者在运行时需要进行每帧更新对齐时,手动遍历需要对齐的 Widget,并将它们的 `enabled` 属性设为 `true`。 + +对于有很多 UI 元素的场景,确保 Widget 组件的 **AlignMode** 属性设置为 `ON_WINDOW_RESIZE`,可以大幅提高场景运行性能。 + +## 对齐组件对节点位置、尺寸的限制 + +通过 **Widget** 组件开启一个或多个对齐设置后,节点的位置(`position`)和尺寸(`width`、`height`)属性可能会被限制,不能通过 API 或动画系统自由修改。如果需要在运行时修改对齐节点的位置或尺寸,请参考 [Widget 组件参考:对节点位置、尺寸的限制](../editor/widget.md#%E5%AF%B9%E8%8A%82%E7%82%B9%E4%BD%8D%E7%BD%AE%E3%80%81%E5%B0%BA%E5%AF%B8%E7%9A%84%E9%99%90%E5%88%B6) 相关内容。 diff --git a/versions/4.0/zh/ui-system/components/engine/widget-align/align-basic.png b/versions/4.0/zh/ui-system/components/engine/widget-align/align-basic.png new file mode 100644 index 0000000000..2377651743 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/widget-align/align-basic.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/widget-align/align-once.png b/versions/4.0/zh/ui-system/components/engine/widget-align/align-once.png new file mode 100644 index 0000000000..068c2d2e6d Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/widget-align/align-once.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/widget-align/full-screen.png b/versions/4.0/zh/ui-system/components/engine/widget-align/full-screen.png new file mode 100644 index 0000000000..91fbe1325c Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/widget-align/full-screen.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/widget-align/hierarchy.png b/versions/4.0/zh/ui-system/components/engine/widget-align/hierarchy.png new file mode 100644 index 0000000000..92154793c2 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/widget-align/hierarchy.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/widget-align/nested-inner.png b/versions/4.0/zh/ui-system/components/engine/widget-align/nested-inner.png new file mode 100644 index 0000000000..b6dd21e08c Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/widget-align/nested-inner.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/widget-align/nested-outer.png b/versions/4.0/zh/ui-system/components/engine/widget-align/nested-outer.png new file mode 100644 index 0000000000..1093817200 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/widget-align/nested-outer.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/widget-align/percentage.png b/versions/4.0/zh/ui-system/components/engine/widget-align/percentage.png new file mode 100644 index 0000000000..58fc1f7a93 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/widget-align/percentage.png differ diff --git a/versions/4.0/zh/ui-system/components/engine/widget-align/stretch.png b/versions/4.0/zh/ui-system/components/engine/widget-align/stretch.png new file mode 100644 index 0000000000..284f519a22 Binary files /dev/null and b/versions/4.0/zh/ui-system/components/engine/widget-align/stretch.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-camera.md b/versions/4.0/zh/xr/architecture/ar-camera.md new file mode 100644 index 0000000000..1b3831e4a4 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/ar-camera.md @@ -0,0 +1,15 @@ +# AR 相机 + +和头戴显示器一样,场景中为了能够抽象表式移动端设备带有 AR 能力的摄像机,XR 插件使用 AR Camera 组件封装一系列属性来映射物理设备的摄像头 AR 功能。 + +![ar-camera-node](ar-camera/ar-camera-node.png) + +AR Camera 对象包含三种必要的组件:cc.Camera、cc.PoseTracker和cc.ARCameraMgr。 + +ar-camera-comp + +cc.Camera 是 Cocos Creator 引擎提供的传统的摄像机组件,为了保证良好的体验,推荐将缓冲清除标志位(Clear Flags)设置为 SOLID_COLOR,近裁剪面(Near Plane)设置为0.01。更多相机参数介绍请查阅 [相机组件](../../editor/components/camera-component.md#相机组件) 介绍。 + +cc.PoseTracker 用于将物理设备的位姿信息同步至 AR Camera,保证摄像机能够正确渲染虚拟内容并和视频流叠加,与 XR HMD 不同,适配移动端的手持设备时 Tracking Type 要选择 VIEW_POSE_ACTIVE_HANDHELD。 + +cc.ARCameraMgr 是用于管理AR摄像机功能的组件,详细属性介绍请参考 **设备映射组件 -> ARCameraMgr**。 diff --git a/versions/4.0/zh/xr/architecture/ar-camera/ar-camera-comp.png b/versions/4.0/zh/xr/architecture/ar-camera/ar-camera-comp.png new file mode 100644 index 0000000000..f6c87447e3 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-camera/ar-camera-comp.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-camera/ar-camera-node.png b/versions/4.0/zh/xr/architecture/ar-camera/ar-camera-node.png new file mode 100644 index 0000000000..886ccc6a43 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-camera/ar-camera-node.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-feature-introduce.md b/versions/4.0/zh/xr/architecture/ar-feature-introduce.md new file mode 100644 index 0000000000..39942ad86a --- /dev/null +++ b/versions/4.0/zh/xr/architecture/ar-feature-introduce.md @@ -0,0 +1,3 @@ +# AR 功能模块 + +开发者往往需要面向不同的平台发布 AR 应用,为了抹平平台差异化对开发者带来的困扰,Cocos CreatorXR 提供了统一的上层 AR 功能组件来支持增强现实应用的设计开发。开发者在使用 AR 组件时,无需关心不同 AR SDK 在功能上的差异而可以专注于逻辑开发。 diff --git a/versions/4.0/zh/xr/architecture/ar-feature-support.md b/versions/4.0/zh/xr/architecture/ar-feature-support.md new file mode 100644 index 0000000000..fd12557704 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/ar-feature-support.md @@ -0,0 +1,29 @@ +# 支持的平台 AR 特性 + +当前版本 AR 插件支持以下几种 AR 特性。 + +| AR 特性 | 描述 | +| :-------------- | :----------------------------------------------------------- | +| AR Session | 在目标平台上启用、禁用和配置 AR Session,以控制 AR 应用的生命周期。 | +| Device Tracking | 跟踪设备在物理空间中的位移和旋转。 | +| AR Camera | 渲染设备相机传输的视频流背景图像并可以根据环境进行光照估计。 | +| Plane Tracking | 检测和跟踪物理世界中的平面。 | +| Image Tracking | 检测和跟踪物理世界中的图像。 | +| Hit Detection | 支持使用射线(Ray cast)与跟踪实体进行命中检测。 | +| Anchors | 跟踪场景空间中的固定点。 | +| Meshing | 将物理世界网格化。 | +| Light Estimate | 光照估计。 | + +Cocos CreatorXR v1.1.0 对各平台的AR特性支持如下: + +| AR 特性\平台 | ARKit | ARCore | AREngine | Spaces | +| :-------------- | :---- | :----- | :------- | :----- | +| AR Session | ✓ | ✓ | ✓ | ✓ | +| Device Tracking | ✓ | ✓ | ✓ | ✓ | +| AR Camera | ✓ | ✓ | ✓ | ✓ | +| Plane Tracking | ✓ | ✓ | ✓ | ✓ | +| Image Tracking | ✓ | ✓ | ✓ | ✓ | +| Hit Detection | ✓ | ✓ | ✓ | | +| Anchors | ✓ | ✓ | ✓ | | +| Meshing | ✓ | | | | +| Light Estimate | | ✓ | ✓ | | diff --git a/versions/4.0/zh/xr/architecture/ar-interaction.md b/versions/4.0/zh/xr/architecture/ar-interaction.md new file mode 100644 index 0000000000..324a169b40 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/ar-interaction.md @@ -0,0 +1,73 @@ +# AR 交互 + +AR 交互主要由 cc.ScreenTouchInteractor 组件驱动,该组件将触摸事件转换为点击、拖拽和捏合等手势,交互器将这些手势传递给可以交互的虚拟交互物,完成手势对应触发的行为。 + +## 手势交互 + +AR 手势交互器组件将屏幕触摸转换为手势。Cocos Creator 的输入系统将手势信号传递给交互物,然后交互物响应手势事件发生变换行为。交互物能发生交互行为的前提是必须绑定 **cc.Selectable** 组件,关于此组件的属性描述详见交互组件 [Selectable](component.md#Selectable)。 + +想要使用 **屏幕手势交互器**,在层级管理器中右键创建 **XR -> Screen Touch Interactor** 。 + +screen-touch-interactor-node + +随意创建一个 3D 物体(以 Cube 为例)。 + +修改 Cube 的 Scale 属性为(0.1,0.1,0.1)既实际大小为 1000cm³ ,修改 Position 属性为(0,-0.1,-0.5)即位于空间远点处 50cm 远且靠下10cm的位置,并添加组件 **XR > Interaction -> Selectable**。 + +![create-3d-obj](ar-interaction/create-3d-obj.png) + +add-selectable + +下面创建选中效果,在资源文件夹中创建一个预置体,命名为 Selected Visualizer。 + +![create-selected-visualizer](ar-interaction/create-selected-visualizer.png) + +在预置体根节点下创建一个同样的 Cube 对象,Scale 大小设置为基于父节点的 1.2 倍。 + +![set-selected-prefab](ar-interaction/set-selected-prefab.png) + +创建一个新的材质,突出表现选中态的效果。 + +![set-selected-material](ar-interaction/set-selected-material.png) + +调整材质效果,建议 Effect 选择 builtin-unlit,Technique 选择 1-transparent。 + +adjust-material + +材质创建完毕后,应用到预置体中 Cube 的 cc.MeshRenderer 中,即可完成选中效果的创建。 + +![refer-to-material](ar-interaction/refer-to-material.png) + +最后,将预置体应用到 cc.Selectable 的 Selected Visualization 属性中。 + +![refer-to-selected-visualizer](ar-interaction/refer-to-selected-visualizer.png) + +运行时效果如下,可以结合手势来移动、旋转和放缩虚拟物体。 + +![select-effect](ar-interaction/select-effect.png) + +## 放置 + +使用 **屏幕手势交互器** 时,会启用设备 AR Hit Test 能力,根据屏幕触碰位置坐标转换到摄像机使用 Ray Cast 与 AR Plane 发生碰撞计算,来获取碰撞点的位置,最终在平面的此坐标上放置虚拟对象。能够被放置的预置体对象必须要挂载**[cc.Placeable](component.md#Placeable)**组件。 + +以上述场景中制作的 Selectable 对象为例,以下对其赋予被放置交互能力。 + +选中场景中的 Cube 对象,为其添加组件 **XR -> Interaction -> Placeable**。 + +![add-placeable](ar-interaction/add-placeable.png) + +将此场景节点拖入资源管理器生成一份预置体,并删除场景中的此 Cube 对象。 + +![create-placement-prefab](ar-interaction/create-placement-prefab.png) + +将刚生成的Cube预置体引用到 **Screen Touch Interactor -> Place Action > Placement Prefab** 属性中,**Calculation Mode** 选择 **AR_HIT_DETECTION**。 + +![refer-to-placement-prefab](ar-interaction/refer-to-placement-prefab.png) + +放置对象的位置计算需要依赖于 AR Plane,所以还需创建一个 Plane Tracking 节点来请求设备激活 AR SDK 的平面识别能力。在编辑器的层级管理器中右键 **创建 > XR > Plane Tracking**,创建平面代理节点。 + +![create-plane-tracking](ar-interaction/create-plane-tracking.png) + +所有工作都完成后,即可打包发布,在运行时查看放置效果。 + +![interacion-effect](ar-interaction/interacion-effect.png) diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/add-placeable.png b/versions/4.0/zh/xr/architecture/ar-interaction/add-placeable.png new file mode 100644 index 0000000000..7674573609 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/add-placeable.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/add-selectable.png b/versions/4.0/zh/xr/architecture/ar-interaction/add-selectable.png new file mode 100644 index 0000000000..d265d65408 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/add-selectable.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/adjust-material.png b/versions/4.0/zh/xr/architecture/ar-interaction/adjust-material.png new file mode 100644 index 0000000000..a72989375c Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/adjust-material.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/create-3d-obj.png b/versions/4.0/zh/xr/architecture/ar-interaction/create-3d-obj.png new file mode 100644 index 0000000000..0e240f9e41 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/create-3d-obj.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/create-placement-prefab.png b/versions/4.0/zh/xr/architecture/ar-interaction/create-placement-prefab.png new file mode 100644 index 0000000000..1ed0837be8 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/create-placement-prefab.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/create-plane-tracking.png b/versions/4.0/zh/xr/architecture/ar-interaction/create-plane-tracking.png new file mode 100644 index 0000000000..9ff0af8f05 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/create-plane-tracking.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/create-selected-visualizer.png b/versions/4.0/zh/xr/architecture/ar-interaction/create-selected-visualizer.png new file mode 100644 index 0000000000..579055da93 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/create-selected-visualizer.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/interacion-effect.png b/versions/4.0/zh/xr/architecture/ar-interaction/interacion-effect.png new file mode 100644 index 0000000000..487a8e2b66 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/interacion-effect.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/refer-to-material.png b/versions/4.0/zh/xr/architecture/ar-interaction/refer-to-material.png new file mode 100644 index 0000000000..f242c5610c Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/refer-to-material.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/refer-to-placement-prefab.png b/versions/4.0/zh/xr/architecture/ar-interaction/refer-to-placement-prefab.png new file mode 100644 index 0000000000..bf195174f2 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/refer-to-placement-prefab.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/refer-to-selected-visualizer.png b/versions/4.0/zh/xr/architecture/ar-interaction/refer-to-selected-visualizer.png new file mode 100644 index 0000000000..e7ce669eed Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/refer-to-selected-visualizer.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/screen-touch-interactor-node.png b/versions/4.0/zh/xr/architecture/ar-interaction/screen-touch-interactor-node.png new file mode 100644 index 0000000000..b9cc8ed92b Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/screen-touch-interactor-node.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/select-effect.png b/versions/4.0/zh/xr/architecture/ar-interaction/select-effect.png new file mode 100644 index 0000000000..8f2da713f5 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/select-effect.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/selected-effect.png b/versions/4.0/zh/xr/architecture/ar-interaction/selected-effect.png new file mode 100644 index 0000000000..487a8e2b66 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/selected-effect.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/set-selected-material.png b/versions/4.0/zh/xr/architecture/ar-interaction/set-selected-material.png new file mode 100644 index 0000000000..a572e38c5a Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/set-selected-material.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-interaction/set-selected-prefab.png b/versions/4.0/zh/xr/architecture/ar-interaction/set-selected-prefab.png new file mode 100644 index 0000000000..4d5bcef1de Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-interaction/set-selected-prefab.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-introduce.md b/versions/4.0/zh/xr/architecture/ar-introduce.md new file mode 100644 index 0000000000..0bc319e1f0 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/ar-introduce.md @@ -0,0 +1,9 @@ +# AR + +Cocos CreatorXR 扩展允许您创建跨平台的增强现实(AR)应用程序。您可以给场景中的节点添加相应的 AR 功能组件来选择要启用哪些 AR 特性而无需关心平台的 AR SDK 之间的差异,当您在 AR 设备上构建和运行应用程序时,Cocos Creator 引擎底层的 AR 模块(AR Module)会调用设备原生的 AR SDK 以启用 AR 功能,因此您可以只进行一次开发并发布到多个 AR 平台。 + +**版本要求** + +编辑器请使用 Cocos Creator v3.7.1 或更高的版本。 + +插件请使用 xr-plugin v1.1.0 或更高的版本。 diff --git a/versions/4.0/zh/xr/architecture/ar-manager.md b/versions/4.0/zh/xr/architecture/ar-manager.md new file mode 100644 index 0000000000..01d1a08c2a --- /dev/null +++ b/versions/4.0/zh/xr/architecture/ar-manager.md @@ -0,0 +1,59 @@ +# AR Manager + +Cocos CreatorXR 的 AR 模块提供了一个全局管理器,用于收集当前项目使用到的 AR 特性并进行管理,特性管理器中每个特性的属性都是全局属性,调整参数会修改设备相关的或者项目全局的功能。cc.ARManager 默认挂载在 XR Agent 节点上,当您创建 AR 自动化行为节点时,ARManager 会收集此节点到其对应的特性列表中,方便后续管理和维护所有特性节点。 + +当前版本针对以支持的 AR 特性提供了对应的全局功能属性: + +## 平面追踪特性 + +当您在场景中创建一个或多个 Plane Tracking 节点,AR Manager 中的 Configuration 会新增 Plane Feature 属性。您可以调整特性下的各项参数,或定位到对应的特性节点。 + +![plane-tracking-node](ar-manager/plane-tracking-node.png) + +plane-feature-manager + +Direction Type 汇集了当前场景所有平面代理需要识别的平面朝向。 + +Tracking Visualizer 给所有平面代理创建默认的可视化模型。 + +Tracking Quality Condition 表示当前可追踪的平面代理的最差质量,当稳定性质量小于此值时,平面不被可视化。 + +Use Plane Shape 开启后可视化效果会根据现实环境中平面的真实物理性状使用多边形绘制,关闭后则使用四边形绘制代替。 + +Unsupported Event 会在设备不支持平面追踪时触发,用户可以根据需求添加事件。 + +## 图像追踪特性 + +当您在场景中创建一个或多个 Image Tracking 节点,AR Manager 中的 Configuration 会新增 Image Feature 属性。您可以调整特性下的各项参数,或定位到对应的特性节点。 + +![image-feature-node](ar-manager/image-feature-node.png) + +image-feature-manager + +Max Tracking Number 表示当前镜头内可同时追踪图片的最大数量,可以根据需要动态修改此值。 + +注:Max Tracking Number 的上限根据设备平台会有差异,目前已知 + +- ARCore 平台可同时追踪最多 20 张图,单图像库存最多 1000 张。 +- ARKit 平台可同时追踪最多 4 张图,单图像库最多 100张。 +- AREngine 平台可同时追踪最多 1 张图。 + +Unsupported Event 会在设备不支持图像追踪时触发,用户可以根据需求添加事件。 + +## 网格化特性(实验性) + +当您在场景中创建一个或多个 Meshing 节点,AR Manager 中的 Configuration 会新增 Meshing Feature 属性。由于 Meshing 功能处于实验性阶段且支持环境重构的设备硬件要求较高,暂不支持对此做特性做参数控制。 + +meshing-manager + +Normals 默认开启,可以根据 Mesh 信息获取法线向量。 + +## APIs + +AR Manager 还提供了控制特性开关以及特性可视化开关的接口: + +```typescript +public enableFeatureTracking (type: ARTrackingType, enable: boolean); +public showAllVisualizer (type: ARTrackingType); +public hideAllVisualizer (type: ARTrackingType); +``` diff --git a/versions/4.0/zh/xr/architecture/ar-manager/image-feature-manager.png b/versions/4.0/zh/xr/architecture/ar-manager/image-feature-manager.png new file mode 100644 index 0000000000..03eea3557f Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-manager/image-feature-manager.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-manager/image-feature-node.png b/versions/4.0/zh/xr/architecture/ar-manager/image-feature-node.png new file mode 100644 index 0000000000..991787e800 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-manager/image-feature-node.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-manager/meshing-manager.png b/versions/4.0/zh/xr/architecture/ar-manager/meshing-manager.png new file mode 100644 index 0000000000..fe579d0336 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-manager/meshing-manager.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-manager/plane-feature-manager.png b/versions/4.0/zh/xr/architecture/ar-manager/plane-feature-manager.png new file mode 100644 index 0000000000..77c7bc88b1 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-manager/plane-feature-manager.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-manager/plane-tracking-node.png b/versions/4.0/zh/xr/architecture/ar-manager/plane-tracking-node.png new file mode 100644 index 0000000000..442f2edae3 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-manager/plane-tracking-node.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-sdk-summary.md b/versions/4.0/zh/xr/architecture/ar-sdk-summary.md new file mode 100644 index 0000000000..c3784ea5dd --- /dev/null +++ b/versions/4.0/zh/xr/architecture/ar-sdk-summary.md @@ -0,0 +1,13 @@ +# 各平台 AR SDK 的设备支持情况 + +ARKit: + +适用于 iOS 12.0+ 系统的设备,Meshing 特性需要设备具备 LiDAR;开发环境需要 Xcode 12.4+。 + +ARCore: + +参考 Google 官方:[支持 ARCore 的设备 | Google Developers](https://developers.google.com/ar/devices?hl=zh-cn) 或者检查 Android 手机是否在官方系统版本里预装了 “Google Play 商店” 及 “Google Play Services for AR” 这两个应用,并且未处于 “停用” 状态。 + +AREngine: + +请参考华为官方:[业务介绍 AR Engine | 华为开发者联盟 (huawei.com)](https://developer.huawei.com/consumer/cn/doc/development/graphics-Guides/introduction-0000001050130900)。 diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component.md b/versions/4.0/zh/xr/architecture/ar-tracking-component.md new file mode 100644 index 0000000000..db51994ce9 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/ar-tracking-component.md @@ -0,0 +1,99 @@ +# AR 自动化行为编辑 + +AR 场景中,虚拟物体与现实实体间总是存在未知的依赖关系,如果能够清晰方便的描述现实实体的条件特征并针对此种条件执行匹配的行为,可以很大程度上简化开发者处理复杂的 AR 功能特性而专注于编写项目核心逻辑。Cocos CreatorXR 提供了 AR 自动化行为编辑组件,将常用的物理特征和逻辑行为抽象成元素供开发者自由搭配,图形化的操作极大程度降低了 AR 应用的开发成本和开发门槛。 + +每种特性的自动化行为编辑组件都有其独特的特征库和行为库,以下介绍了当前版本支持自动化行为的所有 AR 特性: + +## 平面追踪 + +在编辑器的层级管理器中右键 **创建 -> XR -> Plane Tracking**,创建平面代理节点,此节点可用于描述物理世界中的某一个平面实体。 + +create-plane-tracking-node + +选中创建好的 Plane Tracking 节点,在属性检查器中可以看到默认添加好的 cc.ARPlaneTracking,选择 Factor 或 Action 页签可以查看当前已有的特征或行为项。点击 Add Factor 或 Add Action 可以添加特征库/行为库中其他的新项。 + +plane-tracking-factors + +plane-tracking-actions + +在创建好的 Plane Tracking 节点下拖入需要展示的虚拟物体,调整为合适的大小比例。在 Action 中添加 Display 行为项(默认已经添加),即可在运行时识别到满足条件的平面后展示出虚拟物体。 + +![plane-tracking-display](ar-tracking-component/plane-tracking-display.png) + +![plane-tracking-effect](ar-tracking-component/plane-tracking-effect.png) + +## **图像追踪** + +图像追踪允许您在运行时使用设备 AR 能力识别出 2D 图像资源。 + +在编辑器的层级列表中右键 **创建 -> XR -> Image Tracking**,创建图像代理节点,此节点可用于描述物理世界中的某一个图像实体。 + +image-tracking-node + +选中创建好的 Plane Tracking 节点,在属性检查器中可以看到默认添加好的 cc.ARImageTracking,在 Factor 页签的 Image Source 属性中新增一个图像资源。 + +image-tacking-comp + +在 Image 属性中拖入或直接选择资源管理器中的图片资源,在编辑器场景中可以看到当前引用的图像,设置图像的默认的物理尺寸。 + +![set-image-source](ar-tracking-component/set-image-source.png) + +在创建好的 Image Tracking 节点下拖入需要展示的虚拟物体,调整为合适的大小比例。在 Action 中添加 Display 行为项(默认已经添加),即可在运行时识别到图像后展示出虚拟物体。 + +![image-tracking-display](ar-tracking-component/image-tracking-display.png) + +![image-tracking-effect](ar-tracking-component/image-tracking-effect.png) + +## **网格化(实验性)** + +目前网格化特性只对 iOS 平台中具有深度场景重建能力的设备上生效(带有 LiDAR 扫描仪的 iPhone/iPad Pro 系列),网格化允许您根据现实环境创建 3D 网格。 + +在编辑器的层级列表中右键 **创建 -> XR -> Meshing**,创建网格化代理节点。 + +meshing-node + +在 cc.ARMeshing 的 Mesh Visualizer 属性中选择需要展示的网格效果。 + +meshing-comp + +即可直接在运行时将现实环境的表面网格化。 + +![meshing-effect](ar-tracking-component/meshing-effect.jpeg) + +## 特征库和行为库 + +当前可以添加特征和行为如下: + +### 特征库 + +| 特征项 | 特征属性 | 描述 | +| --------------- | -------------------- | ------------------------------------------------------------ | +| Plane Direction | | 设置此平面需要满足给定的朝向。平面可以选择指定的朝向(水平,垂直或其他)。 | +| Plane Size | | 设置此平面必须匹配给定的尺寸。设置时可以选择限制平面的最小/最大尺寸范围。 | +| Image Source | Image | 设置图像追踪所依赖的图像资源。 | +| | Enable Physical Size | 开启后,识别到图像资源后,将默认以此物理尺寸作为现实图像的尺寸,而无需根据深度特征计算图像大小,可以加快识别的速度。 | +| | ImagePhysicalSize | 标定图像的物理尺寸限制(米为单位)。当改动宽或高任意一项数值时,另一项都会根据像素比自动计算数值。 | + +### 行为库 + +| 行为项 | 行为属性 | 描述 | +| --------------- | --------------------- | ------------------------------------------------------------ | +| Display | | 如果自动化行为编辑组件的所有特征与真实世界实体匹配成功,则激活子对象;否则禁用子对象。 | +| | Display Children Node | 开启后,默认展示 Tracking 节点下的子节点对象。 | +| | Stop Tracking | 满足当前节点的条件设定时,关闭此节点的AR追踪。 | +| | Reset When Loss | 追踪丢失时,子节点行为是否重置。 | +| Align | | 该代理节点的位置与真实世界实体的位置的对齐关系。 | +| | Towards | 子物体摆放朝向。如果设置为 Local_Up,则直接使用子物体的姿态。 如果设置为 World_Up,子物体 Y 轴将始终与世界坐标上方向对齐。 | +| | Face to Camera | 开启后,子物体Z轴朝向 AR Camera 的方向。 | +| | MatchTrackingUpdate | 当此节点匹配的真实世界数据更新时,布局和对齐效果也跟随刷新。 | +| Surface Overlay | | 满足条件后用指定的预置体覆盖原来的可视化效果。 | +| | Surface Offset | 设置覆盖在平面上的表面位置的偏移值。 | +| | Replace Visualizer | 创建后关闭并取代追踪可视化的效果。 | +| Adaptive Scale | | 根据匹配的 AR 对象的边界来放缩子物体。 | +| | Max Scale | 比例调整的最大上限。 | +| | Match Tracking Update | Scale 的行为是否跟随追踪而不断刷新。 | +| Track Event | | 在跟踪特征匹配期间调用的事件集。 | +| | on Track Success | 追踪成功时调用的事件。 | +| | on Track Refresh | 追踪信息刷新时调用的事件。 | +| | on Track Loss | 追踪丢失时调用的事件。 | +| | on Track Timeout | 追踪超时时调用的事件。可以设置保持追踪检查的时间,此时间范围内没有成功匹配追踪数据会触发追踪失败的事件。 | diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/create-plane-tracking-node.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/create-plane-tracking-node.png new file mode 100644 index 0000000000..2154341ddd Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/create-plane-tracking-node.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tacking-comp.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tacking-comp.png new file mode 100644 index 0000000000..e6bf216f3b Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tacking-comp.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tracking-display.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tracking-display.png new file mode 100644 index 0000000000..1b5133cc58 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tracking-display.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tracking-effect.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tracking-effect.png new file mode 100644 index 0000000000..9b2510de96 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tracking-effect.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tracking-node.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tracking-node.png new file mode 100644 index 0000000000..f33477cbfc Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/image-tracking-node.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/meshing-comp.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/meshing-comp.png new file mode 100644 index 0000000000..d086875bcd Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/meshing-comp.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/meshing-effect.jpeg b/versions/4.0/zh/xr/architecture/ar-tracking-component/meshing-effect.jpeg new file mode 100644 index 0000000000..75f830d5a7 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/meshing-effect.jpeg differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/meshing-node.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/meshing-node.png new file mode 100644 index 0000000000..cf2026b97b Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/meshing-node.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-actions.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-actions.png new file mode 100644 index 0000000000..c9789676c9 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-actions.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-display.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-display.png new file mode 100644 index 0000000000..232d4c0450 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-display.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-effect.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-effect.png new file mode 100644 index 0000000000..486e9f378f Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-effect.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-factors.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-factors.png new file mode 100644 index 0000000000..8e66685945 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/plane-tracking-factors.png differ diff --git a/versions/4.0/zh/xr/architecture/ar-tracking-component/set-image-source.png b/versions/4.0/zh/xr/architecture/ar-tracking-component/set-image-source.png new file mode 100644 index 0000000000..9fdb5dbc11 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/ar-tracking-component/set-image-source.png differ diff --git a/versions/4.0/zh/xr/architecture/assets.md b/versions/4.0/zh/xr/architecture/assets.md new file mode 100644 index 0000000000..3fdde9ea72 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/assets.md @@ -0,0 +1,43 @@ +# 内置资源与预制体 + +在 Cocos Creator 扩展管理器中 **开启XR扩展** 之后就可以允许在编辑器中使用传统创建对象的方式创建 XR 对象。 + +在 层级管理器 右键选择 **创建 -> XR**,右侧会出现当前可以创建的所有 XR 预制体。选择想要实例化生成的对象即可在场景中创建出来。 + +create_xr_node + +|名称|说明|包含组件| +| ---------------------------------------------- | --------------------------------------------------- | ----------------------------------- | +| XR Agent | 现实世界主角相关的信息在虚拟场景中的代理节点,同时具有用于控制虚拟世界中 XR 主角的生命周期的功能。 | TrackingOrigin | +| XR HMD | 头戴显示器设备在虚拟世界中的抽象节点,基于 Camera 对象进行改造生成,用于同步现实世界中头戴显示器的输入信号并将引擎渲染结果输出至设备。 | Camera
AudioSource
HMDCtrl
PoseTracker
TargetEye | +| AR Camera | 抽象表式移动端设备带有 AR 能力的摄像机,用于来映射物理设备的摄像头 AR 功能。 | Camera
PoseTracker
ARCameraMgr | +| Ray Interactor | 用于进行远距离交互的射线交互器,包含对 XR 设备手柄控制器的 I/O 映射以及射线交互功能。 | PoseTracker
XRController
RayInteractor
Line | +| Direct Interactor | 用于进行近距离直接交互的交互器,同时也包含了对 XR 设备手柄控制器的 I/O 映射以及交互功能 | PoseTracker
XRController
DirectInteractor | +| Gaze Pointer Interactor | 用于进行凝视点交互的交互器,跟随头动,按凝视时间来触发交互行为 | UITransform
RenderRoot2D
XRGazeInteractor | +| ScreenTouchInteractor | 适用于手持移动端设备的屏幕手势交互起,将屏幕手势转化为交互行为同场景中的对象进行交互。 | ScreenTouchInteractor | +| Locomotion Checker | 运动检查器,充当所有虚拟运动驱动访问 XR Agent 的仲裁者,可以保证固定时间内对唯一的运动状态的维持。 | LocomotionChecker | +| Teleportable | 支持与交互器发生传送交互行为的交互物,可以传送 XR Agent 到此对象相关的一个位置。 | Teleportable
InteractableEvents | +| Simple Interactable | 简易的交互物对象,用户可以在此对象上自定义扩展任意的交互行为 | InteractableEvents | +| Grab Interactable | 支持与交互器发生抓取行为的交互物。 | RigidBody
GrabInteractable
InteractableEvents | +| XR Simulator | 用于预览XR内容,提供Web端、无线串流两种方式。 | XRInteractiveSimulator | +| XR Video Player | XR视频播放器,支持在空间中播放窗口化、180 度、360 度模式的视频。 | XRVideoPlayer
XRVideoController
XRVideoCaption | +| XRUI | 可在空间中渲染和交互的 3D UI。 | RaycastChecker
RenderRoot2D
BoxCollider | +| Plane Tracking | 为应用赋能平面识别能力,在运行时使用设备 AR 能力识别出物理世界中的平面特征数据,并可以将这些平面数据可视化显示在应用程序中。 | ARPlaneTracking | +| Image Tracking | 为应用赋能图像识别能力,在运行时使用设备 AR 能力识别出 2D 图像资源。 | ARImageTracking | +| Meshing | 为应用赋能环境重构能力,根据现实环境创建 3D 网格。 | ARMeshing | + +## 内置资源 + +开启 XR 的扩展后,在内置资源数据库(xr-plugin)中会新增 XR 预制体、材质和模型等资源,可供用户直接使用。具体位置如下图所示。 + +### 预制体资源 + +![prefabs](assets/prefabs.png) + +### 材质资源 + +![material](assets/material.png) + +### 模型资源 + +![model](assets/model.png) diff --git a/versions/4.0/zh/xr/architecture/assets/create_xr_node.png b/versions/4.0/zh/xr/architecture/assets/create_xr_node.png new file mode 100644 index 0000000000..a68d841ba5 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/assets/create_xr_node.png differ diff --git a/versions/4.0/zh/xr/architecture/assets/material.png b/versions/4.0/zh/xr/architecture/assets/material.png new file mode 100644 index 0000000000..6fd008309b Binary files /dev/null and b/versions/4.0/zh/xr/architecture/assets/material.png differ diff --git a/versions/4.0/zh/xr/architecture/assets/model.png b/versions/4.0/zh/xr/architecture/assets/model.png new file mode 100644 index 0000000000..4afe8a3cb7 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/assets/model.png differ diff --git a/versions/4.0/zh/xr/architecture/assets/prefabs.png b/versions/4.0/zh/xr/architecture/assets/prefabs.png new file mode 100644 index 0000000000..638925080f Binary files /dev/null and b/versions/4.0/zh/xr/architecture/assets/prefabs.png differ diff --git a/versions/4.0/zh/xr/architecture/component.md b/versions/4.0/zh/xr/architecture/component.md new file mode 100644 index 0000000000..44d233daaa --- /dev/null +++ b/versions/4.0/zh/xr/architecture/component.md @@ -0,0 +1,509 @@ +# XR组件 + +Cocos CreatorXR 通过组件的组合封装为实体赋能,实体根据其不同特性又被不同的功能系统所管理。所以编辑器中所有 XR 相关的功能底层都是由封装好的特殊 XR 组件驱动的。 + +Cocos CreatorXR 的功能组件主要由 5 部分构成: + +- [设备映射](#设备映射组件) +- [交互组件](#交互组件) +- [交互限制组件](#交互限制组件) +- [虚拟移动组件](#虚拟移动组件) +- [XR UI](#xr-ui) + +开启了 xr-plugin 扩展之后,想要给场景中的对象添加 XR 相关的功能组件可以在 **属性检查器** 中点击 **添加组件** 按钮,在出现的组件列表中找到 **XR** 分类,选择 XR 分类下的想要添加的 XR 组件类别再找到类别下的对应组件即可。 + +## 设备映射组件 + +此类组件主要用以同步现实世界中物理设备和虚拟世界中的代理节点之间的 I/O 信息。确保在用户在 XR 设备的使用和虚拟世界中的反馈一致。 + +主要包括以下组件: + +### TrackingOrigin + +追踪原点代理组件。 + +![tracking_origin](component/tracking_origin.png) + +| 属性 | 说明 | +| -------------------- | ------------------------------------------------------------ | +| Offset Object | 指定需要竖直偏移的对象,如果选择的对象还有子对象,则偏移的效果是选中对象及其所有子对象进行纵向偏移。 | +| Tracking Origin Mode | 追踪的偏移方式。选择 Unbond 和 Device 时,下方出现 YOffsetValue,可手动输入数据;选择 Floor 时 YoffsetValue 隐藏。如果选择为 Floor,且设备开启了安全边界,则以设备离地面高度作为当前视角高度(暂时只支持 quest2);如果选择为 Device,则偏移的高度为输入的高度。 | +| YOffset Value | 设备数值偏移量。手动输入偏移的值,米为单位,默认为 1.36144m。如果为固定值,则 OffsetObject 选中的对象 Transform 属性的 Y 值为当前填入的值。 | + +### HMDCtrl + +HMD(Head Mounted Display)头戴显示设备控制器,可以认为所有具备双目立体绘图能力的 XR 眼镜设备都属于 HMD 的大范畴,因此此组件定义了 XR 眼镜的图像渲染输出相关参数。 + +hmd + +| 属性 | 说明 | +| -------------- | ------------------------------------------------------------ | +| Per Eye Camera | 开启单眼显示功能;勾选 PerEyeCamera 后下方出现 Sync With Main Camera 选项;XR HMD 下的两个子节点 LeftEye 和 RightEye 由隐藏变为显示 | +| IPDOffset | 调整瞳间距。下拉列表中选择 Manual 时下方出现 OffsetValue 输入框;在 PerEyeCamera 开启的情况下,调整 Manual 的参数可让 LeftEye 和 RightEye 的 TransForm 属性 X 值变化(变化的值为 ±IPDOffset/2) | + +### PoseTracker + +位姿追踪组件。 + +![pose_tracker](component/pose_tracker.png) + +| 属性 | 说明 | +| --------------- | ------------------------------------------------------------ | +| Tracking Source | 选择需要追踪的设备源 | +| Tracking Type | 追踪模式。选择 POSITION_AND_ROTATION 时追踪设备的平移 + 旋转;选择 POSITION 时只追踪平移的量;选择 ROTATION 时只追踪旋转的量。 | + +### TargetEye + +指定接受渲染相机的组件。 + +![target_eye](component/target_eye.png) + +| 属性 | 说明 | +| ---------- | ------------------------------------------------------------ | +| Target Eye | 指定渲染的目镜。Both 为左右眼都显示,left 为左眼,right 为右眼。 | + +### XRController + +控制器抽象组件。 + +![xr_controller](component/xr_controller.png) + +| 属性 | 说明 | +| -------------------- | ---------------------------------------- | +| InputDevice | 绑定输入手柄设备。 | +| SelectAction | 将 Select 行为映射到手柄的对应实体按键 | +| ActivateAction | 将 Activate 行为映射到手柄的对应实体按键 | +| UIPressAction | 将交互 UI 行为映射到手柄的对应实体按键 | +| AxisToPressThreshold | 行为触发时的阈值 | +| Model | 用于指定手柄的模型对象。 | + +### ARCameraMgr + +移动端手持设备摄像头AR属性 + +| 属性 | 说明 | +| ------------------------ | ------------------------------------------------------------ | +| Auto Focus | 开启或关闭相机自动对焦功能。关闭时,使用固定对焦模式。自动对焦功能是否可用取决于设备相机。 | +| Light Estimate(实验性) | 开启后,运行时估计环境光各项属性并实时调整场景光照,让虚拟物体具有与真实场景相同的光照效果(光照一致性)。 | + +> **注意**:v1.2.0 以上的版本光照估计支持两种模式:Basic 和 HDR。 + +若需要启用 Basic 模式,需要将场景中 Skybox 的 Envmap 属性设置为LDR。 + +若需要启用 HDR 模式,需要将场景中 Skybox 的 Envmap 属性设置为HDR。 + +> **注意**:当前 iOS 平台仅支持 Basic 模式。 + +## 交互组件 + +一次交互操作需要有两种对象协调完成:交互主体和被交互物,对应的交互组件由此也分为两类:Interactor(交互器)和 Interactable(可交互对象)。 + +### RayInteractor + +射线交互器组件。 + +ray_interactor + +| 属性 | 说明 | +| ------------------------ | ------------------------------------------------------------ | +| AttachTransform | 用此 AttachTransform 的位置作为抓取的物体最终落到的位置,如果为空就用当前 Interactor 的位置 | +| ForceGrab | 远距离抓起;开启时被抓对象吸附到 Attach Transform、关闭后抓取挂载在交互点的位置 | +| RayOriginTransform | 可以改变发出 Ray 的起始位置,为空就默认是当前 Interactor 的位置 | +| LineType | 改变射线检测和射线样式;StraightLine 是直线;Projectile Line 是抛物线;Bezier Line 是贝塞尔曲线 | +| MaxRayDistance | 射线交互可以触发的最远距离 | +| ReferenceNode | LineType 为 ProjectileLine 和 BezierLine 时出现此项。用曲线的参考系来定义地平面和向上向量。如果在启动时没有设置,它将尝试找到 XR Agent,如果没有引用,它将默认使用全局的上向量和原点。 | +| Velocity | LineType 为 ProjectileLine 时出现此项。初始速度。增加这个值将使曲线延伸的更远。 | +| Acceleration | LineType 为 ProjectileLine 时出现此项。重力加速度。 | +| Additional Ground Height | LineType 为 ProjectileLine 时出现此项。在地平面以下的额外高度,射线超过地平线会继续向下投射。增加这个值将使终点的位置高度下降。 | +| Additional Flight Time | LineType 为 ProjectileLine 时出现此项。在落地后的额外飞行时间,射线超过地平线会继续向下投射。增加这个值将使投射的飞行时间延长。 | +| End Point Distance | LineType 为 BezierLine 时出现此项。增加这个值的距离会使曲线的末端离起始点更远。 | +| End Point Height | LineType 为BezierLine 时出现此项。降低这个值将使曲线的末端相对于起点下降得更低。 | +| Control Point Distance | LineType 为 BezierLine 时出现此项。增加这个值将使曲线的峰值离起点更远。 | +| Control Point Height | LineType 为 BezierLine 时出现此项。增加这个值将使曲线的峰值相对于起点更高。 | +| Sample Frequency | LineType为ProjectileLine和BezierLine时出现此项。用于近似曲线路径的采样点的数目。数目越多近似效果越好,但性能越低。 | +| RaycastMask | 只能和此 Layer 类型的交互物发生交互。 | +| SelectActionTrigger | Select 行为触发机制,详情见交互功能介绍。 | + +> **注意**:抛物线和贝塞尔曲线的功能需要扩展版本 **>= v1.1.0**,编辑器版本 **>= 3.7.1**。 + +### DirectInteractor + +直接交互器组件。 + +![direct_interactor](component/direct_interactor.png) + +| 属性 | 说明 | +| ------------------- | ------------------------------------------------------------ | +| AttachTransform | 用此 AttachTransform 的位置作为抓取的物体最终落到的位置,如果为空就用当前 Interactor 的位置。 | +| SelectActionTrigger | Select 行为触发机制,详情见交互功能介绍。 | + +### XRGazeInteractor + +凝视交互器,如果没有手柄或者其他控制器的话,可以用 HMD 的中心点凝视交互物(UI),凝视一段时间后出发交互行为。 + +xr_gaze_interactor + +| 属性 | 说明 | +| ----------------------- | ---------------------------- | +| Gaze Pointer Offset | 凝视点竖直方向偏移(单位米) | +| Max Ray Distance | 射线最远投射距离(单位米) | +| Gaze Default Distance | 凝视点UI默认距离(单位米) | +| Gaze Timer Duration | 凝视交互触发时间(单位秒) | +| Gaze Reticle Outer Ring | 凝视点外环UI | + +> **注意**:凝视交互器功能需要扩展版本 **>=v1.1.0**,编辑器版本 **>=3.7.1**。 + +### **ScreenTouchInteractor** + +屏幕手势交互器 + +screen_touch_interactor + +| 行为 | 属性 | 说明 | +| ------------- | ----------------- | ------------------------------------------------------------ | +| Select Action | | 选择行为相关配置,可控制是否启用。 | +| | Gesture | 允许用户操作虚拟物体的可选收拾类型。 | +| | Double Tap Gap | Gesture 选择为 DoubleTap 时出现此项,当两次点击的时间间隔小于此值时,判定为双击。 | +| | HoldTouchDuration | Gesture 选择为 HoldTouch 时出现此项,当触碰屏幕时间大于此值时,判定为长按 | +| Move Action | | 移动行为相关配置,必须开启 Select Action,可控制是否启用。 | +| | Gesture | 绑定移动行为的手势 | +| RotateAction | | 旋转行为相关配置,必须开启 Select Action,可控制是否启用。 | +| | Gesture | 绑定旋转行为的手势 | +| | Drag Degree | Gesture选择为 2FingersDrag 时出现此项,双指拖动速率。 | +| | Twist Degree | Gesture选择为 2FingersRotate 时出现此项,双指旋转速率。 | +| Scale Action | | 放缩行为相关配置,必须开启 Select Action,可控制是否启用。 | +| | Gesture | 绑定放缩行为的手势 | +| | Sensitivity | 放缩的灵敏度 | +| Place Action | | 放置行为相关配置,可控制是否启用。 | +| | Gesture | 绑定放置行为的手势 | +| | Calculation Mode | 放置虚拟物体时,用于计算命中点位置的方法。有三种模式:
AR_HIT_DETECTION:使用 AR Hit Test 功能进行命中检测,将物体放置在命中点的位置。
SPATIAL_DISTANCE:基于屏幕交互器前方的固定距离来放置内容。
COLLISION_DETECTION:基于射线的碰撞检测返回碰撞结果,将物体放置在碰撞点的位置。 | +| | Distance | Calculation Mode 选择为 SPATIAL DISTANCE 时出现此项,设置交互物摆放位置与交互器的距离。 | +| | Avoid Occlusion | Calculation Mode 选择为 SPATIAL DISTANCE 时出现此项,设置交互物摆放时是否受距离内的遮挡物影响摆放结果。开启时忽略遮挡,将交互物放置在固定位置;关闭时,若距离内出现遮挡物则会将交互物放置在遮挡处。 | +| | Located Prefab | Calculation Mode 选择为 SPATIAL DISTANCE 时出现此项,用于定位虚拟物体的摆放位置。 | +| | Placement Prefab | 引用挂载 Placeable 组件的预置体。 | + +> **注意**: +> 1. 屏幕手势交互器功能需要扩展版本 **>=v1.1.0**,编辑器版本 **>=3.7.1**。各手势行为开关控制功能需要扩展版本 **>=v1.2.0**。 +> 2. WebXR平台暂时不支持接受双指手势输入。 + +### GrabInteractable + +可抓取交互对象组件。 + +![grab_interactable](component/grab_interactable.png) + +| 属性 | 说明 | +| ------------------------- | ------------------------------------------------------------ | +| AttachTransform | 用此 AttachTransform 的位置作为触碰点的位置,如果为空就用当前 node 的位置 | +| AttachEaseInTime | 对象被抓取到 AttachTransform 位置过程的时间(在该时间内,被抓取对象会有一个拖尾的效果,持续时间越久效果越不明显) | +| GrabTrigger | 两种方式用于触发抓取:当 Select 的行为触发时/当 Activate 行为触发时 | +| RayReticle | 当交互器与此交互物发生交互碰撞计算时,在碰撞点会显示此属性所引用的对象 | +| HideController | 开启时,此物体被抓取后会隐藏 XR Controller 所引用的 Model | +| ThrowOnDetach | 开启后,允许模拟抛物的动作。 | +| ThrowSimulationMode | 选择为 InheritRigidbody 时,物体抛出时继承刚体的速度;选择为 CurveComputation 时,出现下列两项ThrowSmoothingDuration、ThrowSmoothingCurve,允许自定义抛出速度的计算 | +| ThrowSmoothingDuration | 速度计算的采样时间段。使用此值作为采样区间,用于计算物体被抛出前的速度的加权平均值,作为物体抛出时的初速度 | +| ThrowSmoothingCurve | 速度采样曲线。根据绘制的曲线进行抛出时初速度的加权平均值计算 | +| ThrowVelocityScale | 初速度的权重系数。权重越大,抛物瞬时速度所乘的系数也越大,在继承或者加权计算出的初速度基础上乘以一个系数 | +| ThrowAngularVelocityScale | 初角速度的权重系数。权重越大,抛物瞬时角速度所乘的系数也越大,在继承或者加权计算出的初始角速度基础上乘以一个系数 | + +### Teleportable + +可传送对象组件。 + +![teleportable](component/teleportable.png) + +| 属性 | 说明 | +| -------------------- | ------------------------------------------------------------ | +| TeleportableType | 传送点的类型。选择为 Area 时,传送到射线与传送区域交互点的位置;选择为 Anchor 时,不受射线与传送区域的交互点的限制,之间传送到区域的固定位置。 | +| Teleport Anchor Node | TeleportableType 选择为 Anchor 时出现此项,用于标定传送的落点。此项为空时传送到传送区域的默认中心区域;此项引用了其他对象就传送到引用的对象的位置 | +| RayReticle | 当交互器与此交互物发生交互碰撞计算时,在碰撞点会显示此属性所引用的对象 | +| TeleportTrigger | 触发传送行为的事件:OnSelectExited 表示 Select 行为结束的时刻(按钮抬起)执行传送;OnSelectEntered 表示 Select 触发的时刻(按钮按下) | +| Teleporter | 指定需要被传送的主体(一般为XR Agent),主体需挂载 Teleporter | + +### Selectable + +挂载此组件的对象可以被选中,且可以在选中状态下发生位移、旋转或放缩行为。 + +selectable + +| 属性 | 说明 | +| ---------------------- | -------------------------------------------- | +| Allowed Actions | 选择此对象后允许进行的操作。 | +| Selection Events | 选择行为的回调,它将在特定事件发生时被触发。 | +| Selected Visualization | 对象被选中时将激活的可视化效果。 | + +> **注意**:选择交互功能需要扩展版本 **>=v1.1.0**,编辑器版本 **>=3.7.1**。 + +### Placeable + +挂载此组件的对象可以使用某种方式放置于空间、AR Plane 或 AR Mesh 上。 + +placeable + +| 属性 | 说明 | +| ------------------------ | -------------------------------------------- | +| Preview Placement Prefab | 在放置物生成之前要放置的预放置对象。 | +| Placement Offset | 偏移由交互器放置的预制件的位置。 | +| Placement Events | 放置行为的回调,它将在特定事件发生时被触发。 | + +> **注意**:放置交互功能需要扩展版本 **>=v1.1.0**,编辑器版本 **>=3.7.1**。 + +### 事件 + +在项目开发过程中用户会遇到很多依赖于 XR 组件的信号来触发的逻辑。为了方便用户专注于逻辑开发而不用关心两者之间的通知关系,Cocos CreatorXR 基于传统的事件系统在一部分常被事件依赖的交互组件中封装好了一部分事件信号,用户只需要在特定的信号上绑定一系列想要触发的对象和方法即可。 + +#### 事件信号 + +事件信号分为以下三种: + +- Hover:指的是 XR 的输入设备的射线覆盖到物体上时触发 +- Select:当在输入设备上按下 Select 映射的按键时触发 +- Active:当在输入设备上按下 Active 映射的按键时触发 + +其交互行为描述如下: + +| 交互行为 | 事件信号 | 说明 | +| -------- | ---------------- | --------------------- | +| Hover | OnHoverEntered | Hover 行为执行的时刻 | +| | OnHoverExited | Hover 行为退出的时刻 | +| | OnHoverCanceled | Hover 行为取消的时刻 | +| Select | OnSelectEntered | Select 行为执行的时刻 | +| | OnSelectExited | Select 行为退出的时刻 | +| | OnSelectCanceled | Select 行为取消的时刻 | +| Active | OnActivated | 激活的时刻 | +| | OnDeactivated | 取消激活的时刻 | + +这些事件信号,可以在下列组件中选择,用于处理特定情形下的输入状态。 + +#### 交互事件组件 + +交互组件分为: + +- InteractorEvents:交互器事件组件。 +- InteractableEvents:可交互对象事件组件。 + +其属性描述如下: + +#### InteractorEvents + +| 属性 | 说明 | +| ---------------- | ---------------------------------------- | +| AudioEvents | 开启后可以绑定事件触发时播放的音频 | +| HapticEvents | 开启后可以绑定事件触发时控制器的震动反馈 | +| InteractorEvents | 开启后可以绑定任意回调函数 | + +##### Audio Events + +Audio Events 可以根据 **事件信号** 选择不同类型的事件触发,触发后可以播放特定的音频。 + +![audio_events](component/audio_events.png) + +选中 **事件信号** 右侧的单选框可启用/禁用该事件信号,启用后可以通过 AudioClip 属性右侧的下拉菜单选择不同的音频资源。 + +![audio_clip](component/audio_clip.png) + +##### Haptic Events + +> **注意**:此事件要求扩展版本 **>=v1.1.0**,编辑器版本 **>=3.7.1** + +haptic_event + +选中 **事件信号** 右侧的单选框后,可以调整控制器的震动反馈,通过震动可以给与用户更真实的触感反馈。 + +![haptic_setting](component/haptic_setting.png) + +Haptic Intensity:震动的灵敏度 [0,1] + +Duration:持续的时长 + +##### Interactor Events + +![interactor_events](component/interactor_events.png) + +在右侧的输入框内,输入任意整数值,可在 Interactor Events 数组内添加元素。 + +![interactor_event_setting](component/interactor_event_setting.png) + +添加完成后,可对事件回调进行配置: + +- Node:回调接收节点 +- 组件:回调组件 +- 方法:回调方法 +- CustomEventData:自定义事件的数据,这些数据会被当做回调方法的参数,传入到上述的回调方法中。 + +#### InteractableEvents + +![interactable_events](component/interactable_events.png) + +## 交互限制组件 + +对于挂载了 Selectable 组件的交互物,可以使用交互限制组件限制他的交互效果。 + +### MinMaxScaleConstrain + +min_max_scale_constrain + +| 属性 | 说明 | +| --------- | ------------------------ | +| Min Scale | 可被缩小的最小尺寸比例。 | +| Max Scale | 可被放大的最大尺寸比例。 | + +### RotationAxisConstrain + +rotate_axis_constrain + +| 属性 | 说明 | +| --------------- | ------------------------------------------------------------ | +| Axis Flag | 限制交互物只能按所选旋转轴进行旋转。屏幕交互器最多只能控制双轴(x,y)旋转。 | +| Use Local Space | 是否按本地坐标轴进行旋转。默认关闭,以世界坐标轴做旋转。 | + +## 虚拟移动组件 + +在绝大多数的 VR 项目中,用户会用第一人称的角色视角在虚拟场景中进行移动。这种移动行为一般不会依赖于用户在真实空间的移动反馈,因为位姿的追踪会受到现实世界物理空间的限制。因此我们需要一种类似传统 3D 游戏中那种利用接受控制器输入信号的方式来驱动移动行为的组件,称之为虚拟移动组件(Locomotion Component)。 + +### LocomotionChecker + +运动检查器。 + +![locomotion_checker](component/locomotion_checker.png) + +| 属性 | 说明 | +| -------- | ------------------------------------------------------------ | +| XR Agent | 指定需要进行运动的 XR Agent(或其他对象)。添加该组件时,默认绑定遍历当前场景得到的第一个挂载了 TrackingOrigin 的节点,通常是 XR Agent;用户也可以自行指定需要进行 locomotion 操作的对象 | + +### Teleporter + +传送驱动组件。 + +![teleporter](component/teleporter.png) + +| 属性 | 说明 | +| ------- | ------------------------------------------------------------ | +| Checker | 添加该组件时,默认绑定遍历当前场景得到的第一个挂载 Locomotion Checker 组件的节点 用户也可以自己拖入需要指定的 Locomotion Checker | + +### SharpTurner + +瞬间转向驱动。 + +![sharp_turn](component/sharp_turn.png) + +| 属性 | 说明 | +| ----------------- | ------------------------------------------------------------ | +| Checker | 添加该组件时,默认绑定遍历当前场景得到的第一个挂载 Locomotion Checker 组件的节点 用户也可以自己拖入需要指定的 Locomotion Checker | +| InputDevice | 绑定挂载了 XRController 的控制器对象 | +| TurnAngle | 转动角度 | +| EnableTurnAround | 开启后按下摇杆允许旋转180°(点击摇杆的按钮) | +| ActivationTimeout | 执行连续的转弯时需要等待的时间 | + +### ContinuousTurner + +连续转弯驱动。 + +![ContinuousTurner](component/ContinuousTurner.png) + +| 属性 | 说明 | +| ------------ | ------------------------------------------------------------ | +| Checker | 添加该组件时,默认绑定遍历当前场景得到的第一个挂载 Locomotion Checker 组件的节点 用户也可以自己拖入需要指定的 Locomotion Checker | +| InputDevice | 绑定挂载了 XRController 的控制器对象 | +| InputControl | 绑定接受输入的摇杆 | +| TurnSpeed | 转动的角速度 | + +#### ContinuousMover + +平移运动驱动。 + +![ContinuousMover](component/ContinuousMover.png) + +| 属性 | 说明 | +| ------------- | ------------------------------------------------------------ | +| Checker | 添加该组件时,默认绑定遍历当前场景得到的第一个挂载 Locomotion Checker 组件的节点 用户也可以自己拖入需要指定的 Locomotion Checker | +| InputDevice | 绑定挂载了 XRController 的控制器对象 | +| InputControl | 绑定接受输入的摇杆 | +| MoveSpeed | 移动速度 | +| ForwardSource | 选择一个物体,用该物体节点的朝向作为移动的正方向 | + +## XR UI + +传统的生成 UI 控件的方式都是把 UI 画在画布(Canvas)上,而画布本身不具有深度信息(位置属性不可更改),导致了画布是贴在屏幕上的,只有与屏幕进行交互才能反馈作用于 UI 控件。由于 XR 设备的摄像头是两个目镜,不支持交互,这明显不满足于 XR 项目的需求。所以我们需要将 UI 的交互方式改为在空间中用交互器进行交互,因此需要将 UI 控件剥离出画布而能够单独存在于空间中,具有完整的位置属性并具有碰撞检测功能。 + +XR UI 是基于 2D UI 扩展而来,关于如何使用 2D/UI 组件可参考 2D 对象概述。 + +### 新建 UI + +在 **层级管理器 -> 创建 -> XR -> XRUI** 可以添加 XRUI。 + +create_xrui + +相比于传统的 UI 控件,XR UI 会新增一些组件用于计算碰撞检测以触发交互,如图示,RaycastChecker 和 BoxCollider 可使其能够接收 XR 输入:相比于传统的 UI 控件,XR UI 会新增一些组件用于计算碰撞检测以触发交互,如图示,RaycastChecker 和 BoxCollider 可使其能够接收 XR 输入: + +![xrui_comp](component/xrui_comp.png) + +注意:对于 3D 空间上的 UI,其根节点上需要 RenderRoot2D 组件才可以正常渲染。 + +#### 存量 UI 转换 + +如 UI 已制作完成,也参考下面的步骤将原本的 2D Canvas 下的 UI,转化为 XR 的 UI。 + +##### 方法一 + +在 **层级管理器** 右键创建一个空节点(如命名为 UIRoot,下文均使用 UIRoot),为节点添加组件 **RenderRoot2D** 组件,同时节点会自动添加 **UITransform** 组件: + +![add_renderroot](component/add_renderroot.png) + +将原有的 2D UI 控件分离出 Canvas,移动至 UIRoot 层级下。修改 Button 位置和 Layer 属性。同时将 Button 及其子节点的 Layer 属性,都修改为和 Camera 的 Layer 属性一致(此处均为DEFAULT)。 + +![change_layer](component/change_layer.gif) + +给 Button 及其子节点添加材质。在 资源管理器 中点击 **xr-plugin -> xr -> res -> default_materials**,选择 **xr-3dui-sprite-material** 拖拽至 Sprite 组件的 CustomMaterial 属性中。 + +![add_3d_sprite](component/add_3d_sprite.gif) + +给 Button 添加射线交互组件 RaycastChecker。点击 Button 节点,在 **属性检查器** 下方,点击 添加组件按钮,选择 **XR -> UI -> RaycastChecker**。 + +![add_raycastchecker](component/add_raycastchecker.png) + +在 **属性检查器** 中出现 RaycastChecker 组件和 BoxCollider 组件,且 BoxCollider 组件的 Size 属性的 xy 值与节点的 UITransfrom 的 Content Size 值一致,如下图:(此处可以将 BoxCollider 替换为其他所需 3D 碰撞体,能够贴合 UI 组件即可)。 + +![change_collider_size](component/change_collider_size.png) + +移动 Button 节点至场景中指定位置,调整 Rotation 和 Scale 的值以满足设计需求。 + +添加完所有 UI 组件后,删除旧有的 Canvas 节点。 + +到此为止,存量 UI 到 XR UI 的转化就完成了。 + +##### 方法二 + +**v1.1.0** 版本新增一键转换为 XR UI 功能。 + +在场景下右键传统 2DUI,菜单中会出现 2DUI 转为 XRUI 选项 + +![convert_2d_to_3d](component/convert_2d_to_3d.png) + +转换成功后UI节点会自动添加以下三个组件,并可以调节其 cc.Node 中的空间属性。 + +![auto_add_comp](component/auto_add_comp.png) + +> **注意**:已经为 XRUI 的 UI 不能出现转为 XRUI + +### 虚拟键盘(XR Keyboard) + +添加一个 EditBox 的 XR UI,同时给 EditBox 添加一个子节点,命名为 KeyboardRoot (命名随意),同时调整 KeyboardRoot 的位置信息(根据需求进行调整即可,可将 XR Keyboard 临时放在节点下进行调整)。 + +![xr_keyboard_root](component/xr_keyboard_root.png) + +创建 XR Keyboard 对象:在资源管理器中点击 xr-plugin-> xr -> res -> default_prefab,选择 XR Keyboard 拖拽至场景中。 + +![xr_keyboard_prefab](component/xr_keyboard_prefab.png) + +为 EditBox 节点添加 XRKeyboardInputField 组件,同时绑定 SuspendTransform 和 XRKeyboard,将节点拖拽进去。 + +![add_xr_keyboard_input_field](component/add_xr_keyboard_input_field.png) + +![bind_suspend](component/bind_suspend.png) + +### 射线材质 + +使用射线与 XR UI 进行交互时,需要给射线绑定材质 xr-default-line-material。位置在 **资源管理器 -> xr-plugin -> xr -> res -> default_materials**。 + +![xr_line_material](component/xr_line_material.png) diff --git a/versions/4.0/zh/xr/architecture/component/ContinuousMover.png b/versions/4.0/zh/xr/architecture/component/ContinuousMover.png new file mode 100644 index 0000000000..6964ee5b44 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/ContinuousMover.png differ diff --git a/versions/4.0/zh/xr/architecture/component/ContinuousTurner.png b/versions/4.0/zh/xr/architecture/component/ContinuousTurner.png new file mode 100644 index 0000000000..a983a2883a Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/ContinuousTurner.png differ diff --git a/versions/4.0/zh/xr/architecture/component/add_3d_sprite.gif b/versions/4.0/zh/xr/architecture/component/add_3d_sprite.gif new file mode 100644 index 0000000000..46e86f9bf1 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/add_3d_sprite.gif differ diff --git a/versions/4.0/zh/xr/architecture/component/add_raycastchecker.png b/versions/4.0/zh/xr/architecture/component/add_raycastchecker.png new file mode 100644 index 0000000000..0ff2a0d2bb Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/add_raycastchecker.png differ diff --git a/versions/4.0/zh/xr/architecture/component/add_renderroot.png b/versions/4.0/zh/xr/architecture/component/add_renderroot.png new file mode 100644 index 0000000000..82f73d3212 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/add_renderroot.png differ diff --git a/versions/4.0/zh/xr/architecture/component/add_xr_keyboard_input_field.png b/versions/4.0/zh/xr/architecture/component/add_xr_keyboard_input_field.png new file mode 100644 index 0000000000..4e841bf57f Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/add_xr_keyboard_input_field.png differ diff --git a/versions/4.0/zh/xr/architecture/component/audio_clip.png b/versions/4.0/zh/xr/architecture/component/audio_clip.png new file mode 100644 index 0000000000..68947fe852 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/audio_clip.png differ diff --git a/versions/4.0/zh/xr/architecture/component/audio_events.png b/versions/4.0/zh/xr/architecture/component/audio_events.png new file mode 100644 index 0000000000..55546db704 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/audio_events.png differ diff --git a/versions/4.0/zh/xr/architecture/component/auto_add_comp.png b/versions/4.0/zh/xr/architecture/component/auto_add_comp.png new file mode 100644 index 0000000000..0a4e309f59 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/auto_add_comp.png differ diff --git a/versions/4.0/zh/xr/architecture/component/bind_suspend.png b/versions/4.0/zh/xr/architecture/component/bind_suspend.png new file mode 100644 index 0000000000..786bdf490d Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/bind_suspend.png differ diff --git a/versions/4.0/zh/xr/architecture/component/change_collider_size.png b/versions/4.0/zh/xr/architecture/component/change_collider_size.png new file mode 100644 index 0000000000..59b5e1bd2f Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/change_collider_size.png differ diff --git a/versions/4.0/zh/xr/architecture/component/change_layer.gif b/versions/4.0/zh/xr/architecture/component/change_layer.gif new file mode 100644 index 0000000000..bd4ac2a4b0 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/change_layer.gif differ diff --git a/versions/4.0/zh/xr/architecture/component/convert_2d_to_3d.png b/versions/4.0/zh/xr/architecture/component/convert_2d_to_3d.png new file mode 100644 index 0000000000..60bbbdb5a7 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/convert_2d_to_3d.png differ diff --git a/versions/4.0/zh/xr/architecture/component/create_xrui.png b/versions/4.0/zh/xr/architecture/component/create_xrui.png new file mode 100644 index 0000000000..bf1747a4bc Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/create_xrui.png differ diff --git a/versions/4.0/zh/xr/architecture/component/direct_interactor.png b/versions/4.0/zh/xr/architecture/component/direct_interactor.png new file mode 100644 index 0000000000..d74974e79a Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/direct_interactor.png differ diff --git a/versions/4.0/zh/xr/architecture/component/grab_interactable.png b/versions/4.0/zh/xr/architecture/component/grab_interactable.png new file mode 100644 index 0000000000..573bc4c7a3 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/grab_interactable.png differ diff --git a/versions/4.0/zh/xr/architecture/component/haptic_event.png b/versions/4.0/zh/xr/architecture/component/haptic_event.png new file mode 100644 index 0000000000..f27c2cd1a2 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/haptic_event.png differ diff --git a/versions/4.0/zh/xr/architecture/component/haptic_setting.png b/versions/4.0/zh/xr/architecture/component/haptic_setting.png new file mode 100644 index 0000000000..536c4006ab Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/haptic_setting.png differ diff --git a/versions/4.0/zh/xr/architecture/component/hmd.png b/versions/4.0/zh/xr/architecture/component/hmd.png new file mode 100644 index 0000000000..0996aec66a Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/hmd.png differ diff --git a/versions/4.0/zh/xr/architecture/component/interactable_events.png b/versions/4.0/zh/xr/architecture/component/interactable_events.png new file mode 100644 index 0000000000..3c81ea449a Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/interactable_events.png differ diff --git a/versions/4.0/zh/xr/architecture/component/interactor_event_setting.png b/versions/4.0/zh/xr/architecture/component/interactor_event_setting.png new file mode 100644 index 0000000000..20cb255be4 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/interactor_event_setting.png differ diff --git a/versions/4.0/zh/xr/architecture/component/interactor_events.png b/versions/4.0/zh/xr/architecture/component/interactor_events.png new file mode 100644 index 0000000000..24cdd0edb0 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/interactor_events.png differ diff --git a/versions/4.0/zh/xr/architecture/component/locomotion_checker.png b/versions/4.0/zh/xr/architecture/component/locomotion_checker.png new file mode 100644 index 0000000000..faaba24c6c Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/locomotion_checker.png differ diff --git a/versions/4.0/zh/xr/architecture/component/min_max_scale_constrain.png b/versions/4.0/zh/xr/architecture/component/min_max_scale_constrain.png new file mode 100644 index 0000000000..a106093bdf Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/min_max_scale_constrain.png differ diff --git a/versions/4.0/zh/xr/architecture/component/placeable.png b/versions/4.0/zh/xr/architecture/component/placeable.png new file mode 100644 index 0000000000..5498a7ddfd Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/placeable.png differ diff --git a/versions/4.0/zh/xr/architecture/component/pose_tracker.png b/versions/4.0/zh/xr/architecture/component/pose_tracker.png new file mode 100644 index 0000000000..9232ac6134 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/pose_tracker.png differ diff --git a/versions/4.0/zh/xr/architecture/component/ray_interactor.png b/versions/4.0/zh/xr/architecture/component/ray_interactor.png new file mode 100644 index 0000000000..a665a52c96 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/ray_interactor.png differ diff --git a/versions/4.0/zh/xr/architecture/component/rotate_axis_constrain.png b/versions/4.0/zh/xr/architecture/component/rotate_axis_constrain.png new file mode 100644 index 0000000000..116b4c949e Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/rotate_axis_constrain.png differ diff --git a/versions/4.0/zh/xr/architecture/component/screen_touch_interactor.png b/versions/4.0/zh/xr/architecture/component/screen_touch_interactor.png new file mode 100644 index 0000000000..d9e6187347 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/screen_touch_interactor.png differ diff --git a/versions/4.0/zh/xr/architecture/component/selectable.png b/versions/4.0/zh/xr/architecture/component/selectable.png new file mode 100644 index 0000000000..e5ea50993a Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/selectable.png differ diff --git a/versions/4.0/zh/xr/architecture/component/sharp_turn.png b/versions/4.0/zh/xr/architecture/component/sharp_turn.png new file mode 100644 index 0000000000..6f2bfe367f Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/sharp_turn.png differ diff --git a/versions/4.0/zh/xr/architecture/component/target_eye.png b/versions/4.0/zh/xr/architecture/component/target_eye.png new file mode 100644 index 0000000000..11ffaa9f14 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/target_eye.png differ diff --git a/versions/4.0/zh/xr/architecture/component/teleportable.png b/versions/4.0/zh/xr/architecture/component/teleportable.png new file mode 100644 index 0000000000..9e78c20abb Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/teleportable.png differ diff --git a/versions/4.0/zh/xr/architecture/component/teleporter.png b/versions/4.0/zh/xr/architecture/component/teleporter.png new file mode 100644 index 0000000000..7adb384bbe Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/teleporter.png differ diff --git a/versions/4.0/zh/xr/architecture/component/tracking_origin.png b/versions/4.0/zh/xr/architecture/component/tracking_origin.png new file mode 100644 index 0000000000..8301705d09 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/tracking_origin.png differ diff --git a/versions/4.0/zh/xr/architecture/component/xr_controller.png b/versions/4.0/zh/xr/architecture/component/xr_controller.png new file mode 100644 index 0000000000..63d67b6406 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/xr_controller.png differ diff --git a/versions/4.0/zh/xr/architecture/component/xr_gaze_interactor.png b/versions/4.0/zh/xr/architecture/component/xr_gaze_interactor.png new file mode 100644 index 0000000000..a1442d3854 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/xr_gaze_interactor.png differ diff --git a/versions/4.0/zh/xr/architecture/component/xr_keyboard_prefab.png b/versions/4.0/zh/xr/architecture/component/xr_keyboard_prefab.png new file mode 100644 index 0000000000..6c9009cb39 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/xr_keyboard_prefab.png differ diff --git a/versions/4.0/zh/xr/architecture/component/xr_keyboard_root.png b/versions/4.0/zh/xr/architecture/component/xr_keyboard_root.png new file mode 100644 index 0000000000..77320867de Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/xr_keyboard_root.png differ diff --git a/versions/4.0/zh/xr/architecture/component/xr_line_material.png b/versions/4.0/zh/xr/architecture/component/xr_line_material.png new file mode 100644 index 0000000000..81747ec57e Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/xr_line_material.png differ diff --git a/versions/4.0/zh/xr/architecture/component/xrui_comp.png b/versions/4.0/zh/xr/architecture/component/xrui_comp.png new file mode 100644 index 0000000000..36a5e1bdc2 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/component/xrui_comp.png differ diff --git a/versions/4.0/zh/xr/architecture/index.md b/versions/4.0/zh/xr/architecture/index.md new file mode 100644 index 0000000000..0a042283e7 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/index.md @@ -0,0 +1,5 @@ +# 架构 + +插件架构是指插件的系统结构。作为 Cocos CreatorXR 的用户,您可以通过了解插件架构的组成元素、它们的职责以及它们如何相互组合联系,来快速完整的构建 XR 应用。 + +![](index/xr-framework.png) diff --git a/versions/4.0/zh/xr/architecture/index/xr-framework.png b/versions/4.0/zh/xr/architecture/index/xr-framework.png new file mode 100644 index 0000000000..32a771e0b8 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/index/xr-framework.png differ diff --git a/versions/4.0/zh/xr/architecture/preview.md b/versions/4.0/zh/xr/architecture/preview.md new file mode 100644 index 0000000000..7ac3b8734c --- /dev/null +++ b/versions/4.0/zh/xr/architecture/preview.md @@ -0,0 +1,47 @@ +# 预览 + +为了方便开发者在项目开发过程中实时调试,快速验证一些传统的功能逻辑来提高开发效率,Cocos CreatorXR 基于 Cocos Creator 的 Web Preview 功能开发了适用于 XR 项目的预览功能。 + +## 操作说明 + +在 xr-plugin 的资源库中找到 XR Simulator,将其拖拽至场景中。 + +xr_web_simulator_prefab + +在编辑器的预览选项中选择浏览器预览,并点击运行。 + +![preview_in_browser](preview/preview_in_browser.png) + +运行后即可在浏览器中进行模拟预览。 + +![effect_of_preview_in_browser](preview/effect_of_preview_in_browser.png) + +键盘 WASD 来控制角色整体(HMD + 手柄)进行前左后右移动,QE 控制整体上升和下降。 + +键盘 Latin 部分的数字键 123 功能分别为:1.鼠标键盘的控制对象切换至 XR Agent (角色自身)此时前后左右上下作用于整体角色,鼠标滑动控制 HMD(Camera)转动,射线发出位置位于 HMD 中央,空格键用于触发 click(点击),按住空格拖动鼠标触发 drag(点击);2 和 3 将鼠标和键盘的控制对象切换至左/右手柄,此时前后左右上下作用于单独的左/右手柄,射线从手柄位置发出,空格键用于触发 click(点击),按住空格拖动鼠标触发 drag(拖动)。 + +长按 B 键重置手柄位置。 + +控制手柄移动时,正前方向向量始终和 XR Agent 的前向保持一致。 + +## XR 设备无线串流调试 + +v1.1.0 版本的预览组件新增无线串流模式,内容验证是项目开发过程中极其耗时的一环,由于 XR 设备的终端独立性和串流工具的封闭性,使得在编辑器中针对 XR 设备的项目比传统移动端/PC 端项目的内容调试验证更为困难。为此,Cocos CreatorXR v1.1.0 推出了无线串流调试功能,开发者可以直接在 Web 浏览器中预览XR项目并同步所有来自 XR 设备的信号,正确渲染实时画面并反馈各种控制器信号触发逻辑,无需打包应用至设备即可快速完整地体验所有XR项目内容。 + +xr_preview_by_wireless + +将 XR Preview Type 选择为 REMOTE 模式,XR Connect Type 选择为 WIFI 模式,将电脑和设备处于同一 WIFI 下。 + +填写 XR 设备网络 IP 到 XR Device IP 属性中。 + +在构建面板中勾选 Remote Preview,打包项目至XR一体机。 + +remote_preview_type + +项目预览方式选择浏览器预览。 + +![preview_in_browser](preview/preview_in_browser.png) + +编辑器中点击运行进行浏览器预览,同时设备运行打包好的 APK。 + +> **注意**:无线串流调试功能需要扩展版本 **>=v1.1.0**,编辑器版本 **>=3.7.1**。 diff --git a/versions/4.0/zh/xr/architecture/preview/effect_of_preview_in_browser.png b/versions/4.0/zh/xr/architecture/preview/effect_of_preview_in_browser.png new file mode 100644 index 0000000000..ae2cb75a10 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/preview/effect_of_preview_in_browser.png differ diff --git a/versions/4.0/zh/xr/architecture/preview/preview_in_browser.png b/versions/4.0/zh/xr/architecture/preview/preview_in_browser.png new file mode 100644 index 0000000000..f50e8d151c Binary files /dev/null and b/versions/4.0/zh/xr/architecture/preview/preview_in_browser.png differ diff --git a/versions/4.0/zh/xr/architecture/preview/remote_preview_type.png b/versions/4.0/zh/xr/architecture/preview/remote_preview_type.png new file mode 100644 index 0000000000..e02f813ae6 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/preview/remote_preview_type.png differ diff --git a/versions/4.0/zh/xr/architecture/preview/xr_preview_by_wireless.png b/versions/4.0/zh/xr/architecture/preview/xr_preview_by_wireless.png new file mode 100644 index 0000000000..59bb85f4cd Binary files /dev/null and b/versions/4.0/zh/xr/architecture/preview/xr_preview_by_wireless.png differ diff --git a/versions/4.0/zh/xr/architecture/preview/xr_web_simulator_prefab.png b/versions/4.0/zh/xr/architecture/preview/xr_web_simulator_prefab.png new file mode 100644 index 0000000000..c0908e254f Binary files /dev/null and b/versions/4.0/zh/xr/architecture/preview/xr_web_simulator_prefab.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-composition-layer.md b/versions/4.0/zh/xr/architecture/xr-composition-layer.md new file mode 100644 index 0000000000..8f30c7bfb6 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/xr-composition-layer.md @@ -0,0 +1,63 @@ +# XR 合成层 + +在 XR 应用开发中,合成层(Composition Layer)是一种常用的技术,通常应用在混合现实场景,将虚拟现实场景和真实世界场景进行混合,合成层将根据用户设定的 layer 深度将不同的 layer 分别渲染到不同的图层中,然后将这些图层进行合成,形成一个完整的XR场景。同时,通过调整图层的透明度和深度,能达到虚拟对象与真实世界对象完美融合的效果。Composition Layer 技术可以实现高质量的XR渲染效果,为 XR 应用的开发和体验提供了很大的帮助和支持。 + +## 合成层功能 + +| 属性 | 说明 | +| ---------------- | ------------------------------------------------------------ | +| Layer Setting | 合成层效果设置。 | +| --Type | 合成层的类型: Overlay:将纹理呈现在 Eye Buffer 前面。 Underlay:将纹理呈现在 Eye Buffer 后面。 | +| --Shape | 提供两种种形状的合成层:
Quad:具有四个顶点的平面纹理,通常用来显示场景中的文本或信息。
Cylinder具有柱面弧度的圆柱形纹理,通常用于显示曲面 UI 界面。 | +| --Redius | 选择 Cylinder 时出现此项,设置曲面半径。 | +| --CentralAngle | 选择 Cylinder 时出现此项,设置中心角大小。 | +| --Depth | 定义合成层在场景中的顺序。数值越小,越靠近 Eye Buffer。 | +| TextureSetting | 材质效果设置。 | +| --Texture Type | 设置纹理类型:
Static:若需要渲染静态内容,需使用静态纹理。
Dynamic:若要将动态内容渲染至该合成层,即为合成层每帧更新纹理,则需要使用动态纹理。例如使用普通摄像机生成的 RenderTexture 图像。 | +| --Static Texture | 绑定静态纹理资源。 | +| --Camera | 用于绑定合成层要获取动态纹理的相机。 | +| --Width | 设置相机 RT 的宽。 | +| --Height | 设置相机 RT 的高。 | + +> **注意**: +> 1. 合成层功能对接 OpenXR 的核心 API 扩展,适用于所有对接 OpenXR 标准的设备。 +> 2. 必须将摄像机放置在圆柱内切球内。如果摄像机接近内切球表面,合成层将显示异常。 + +## 使用合成层 + +目前合成层功能可以渲染动态纹理,主要应用于渲染 Camera 所采集的画面。 + +以下案例实现一个 Overlay 表现的镜子对象,可以反射 XR 角色的动作表现。 + +### 配置步骤 + +先在场景中创建完整的 XR 代理节点用于设备追踪。并绑定简单的头显/手柄模型。 + +![](xr-composition-layer/create-xr-actor.png) + +场景中添加任意节点,以空节点为例。为其添加合成层组件,在属性检查器面板点击添加组件,找到 **XR > Extra > XRCompositionLayer**,添加组件。 + + + +add-composition-comp + +在场景中创建一个 Camera 节点,并将其位置、朝向调整为需要的表现。 + + + +![](xr-composition-layer/change-camera-pos.png) + +将 Camera 的 Clear Flags 设置为 SOLID_COLOR,并将 Clear Color 的透明度设置为 0。 + + + +将此 Camera 节点挂载到 Node 身上添加的 cc.XRCompositionLayer 组件的 Camera 属性中。并调整其渲染分辨率、Scale 大小和位置,尽量保证 Scale 的长宽比和渲染分辨率的长宽比相同,否则画面会出现拉伸。Layer Setting 的 Type 选为Overlay,Shape选为Quad。 + + + +打包后效果如下: + +![overlay-effect](xr-composition-layer/overlay-effect.gif) + +> **注意**:使用合成层功能需要扩展版本 **>=1.2.0**,编辑器版本 **>=3.7.3**。纹理类型的设置需要扩展版本 **>=1.2.1**,编辑器版本 **>=3.8.0**。 + diff --git a/versions/4.0/zh/xr/architecture/xr-composition-layer/add-camera.png b/versions/4.0/zh/xr/architecture/xr-composition-layer/add-camera.png new file mode 100644 index 0000000000..eae63c2a43 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-composition-layer/add-camera.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-composition-layer/add-composition-comp.png b/versions/4.0/zh/xr/architecture/xr-composition-layer/add-composition-comp.png new file mode 100644 index 0000000000..3716369ba1 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-composition-layer/add-composition-comp.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-composition-layer/add-empty-node.png b/versions/4.0/zh/xr/architecture/xr-composition-layer/add-empty-node.png new file mode 100644 index 0000000000..00196200de Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-composition-layer/add-empty-node.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-composition-layer/change-camera-pos.png b/versions/4.0/zh/xr/architecture/xr-composition-layer/change-camera-pos.png new file mode 100644 index 0000000000..4ccc47d0a5 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-composition-layer/change-camera-pos.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-composition-layer/config-compositionlayer.png b/versions/4.0/zh/xr/architecture/xr-composition-layer/config-compositionlayer.png new file mode 100644 index 0000000000..742c19d158 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-composition-layer/config-compositionlayer.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-composition-layer/create-xr-actor.png b/versions/4.0/zh/xr/architecture/xr-composition-layer/create-xr-actor.png new file mode 100644 index 0000000000..1afb3f9cde Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-composition-layer/create-xr-actor.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-composition-layer/overlay-effect.gif b/versions/4.0/zh/xr/architecture/xr-composition-layer/overlay-effect.gif new file mode 100644 index 0000000000..f9a9269206 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-composition-layer/overlay-effect.gif differ diff --git a/versions/4.0/zh/xr/architecture/xr-composition-layer/set-clear-flags.png b/versions/4.0/zh/xr/architecture/xr-composition-layer/set-clear-flags.png new file mode 100644 index 0000000000..ee46b380bd Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-composition-layer/set-clear-flags.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-fallback.md b/versions/4.0/zh/xr/architecture/xr-fallback.md new file mode 100644 index 0000000000..8ba6c1e20e --- /dev/null +++ b/versions/4.0/zh/xr/architecture/xr-fallback.md @@ -0,0 +1,97 @@ +# AR 降级 + +为了解决市面上部分安卓手机不支持AR功能以及WebXR浏览器和设备覆盖率不高的问题,Cocos CreatorXR提供了一系列解决方案: + +1. 移动设备原生端:引擎内部通过集成设备Sensor数据、相机视频流数据和 **OpenCV** 的图像识别库,来降级原生AR SDK的平面和图像追踪能力的表现效果。 +2. Web端:引擎内部通过集成浏览器API和 **MindAR** 图像追踪算法库,来降级WebXR的平面和图像追踪能力的表现效果。 + +这些方案允许AR应用在不带有AR SDK或不支持WebXR的设备上使用,但是表现效果会有所降低。 + +想了解设备是否支持AR,移动设备原生AR SDK支持情况的相关内容请参考[各平台 AR SDK 的设备支持情况](ar-sdk-summary.md);WebXR支持情况的相关内容请参考[ WebXR 的浏览器支持情况](../project-deploy/webxr-proj-pub.md#选择可用的设备和浏览器)。 + +## 降级功能说明 + +### 原生端 + +#### 平台 + +Android + +#### 系统 + +OS版本在Android8.0及以上 + +#### 平面追踪特性降级 + +1. 开启特性降级功能:选择一个Plane Tracking节点,开启cc.ARPlaneTracking下的Enable auto fallback。 + + ![](xr-fallback/native-enable-fallback.png) + +2. 添加降级行为效果:在cc.ARPlaneTracking下的Action中新增一个Fallback的行为。 + + + +3. 编辑降级效果: + + 设置Display Method为**Z_AXIS_TO_CAMERA**时,Plane Tracking下的子物体按Z轴朝向摄像机的方向进行摆放; + + 设置Display Method为**Y_AXIS_TO_CAMERA**时,Plane Tracking下的子物体按Y轴朝向摄像机的方向进行摆放; + + **Distance**控制子物体摆放位置和相机的距离。 + + 设置Display Method为**CHANGE_SCENE**时,Fallback Scene选项填写想要进行跳转的场景名称,即可在运行时直接跳转至此场景。 + + ![](xr-fallback/native-fallback-change-scene.png) + +#### 图像追踪特性降级 + +1. 开启特性在原生端降级功能:选择一个Image Tracking节点,开启cc.ARImageTracking下的**Enable auto fallback in Native**。这里的由于原生端与Web端所采用的降级方案不同,所以需要单独控制两者的降级功能开关。 + + ![](xr-fallback/native-enable-fallback-image.png) + +2. 添加降级行为效果:同理,在cc.ARImageTracking下的Action中新增一个Fallback的行为。 + +3. 编辑降级效果: + + 设置Display Method为**Z_AXIS_TO_CAMERA**时,Plane Tracking下的子物体按Z轴朝向摄像机的方向进行摆放; + + 设置Display Method为**Y_AXIS_TO_CAMERA**时,Plane Tracking下的子物体按Y轴朝向摄像机的方向进行摆放; + + **Distance**控制子物体摆放位置和相机的距离。 + + 设置Display Method为**CHANGE_SCENE**时,Fallback Scene选项填写想要进行跳转的场景名称,即可在运行时直接跳转至此场景。 + +### Web端 + +#### 平台 + +Android、iOS + +#### 平面追踪特性降级 + +1. 调整降级场景中的session模式:将XR Agent节点的cc.WebXRSessionController中的Default Session Mode改为IMMERSIVE_AR。 + + ![](xr-fallback/web-change-session.png) + +2. 同上开启特性降级功能:选择一个Plane Tracking节点,开启cc.ARPlaneTracking下的Enable auto fallback。 + +3. 同上在Action中新增Fallback行为。 + +#### 图像追踪特性降级 + +1. 同上,调整降级场景中的session模式:将XR Agent节点的cc.WebXRSessionController中的Default Session Mode改为IMMERSIVE_AR。 + +2. 开启Web端的降级方案:选择场景中的Image Tracking节点,开启cc.WebXRSessionController组件中的**Adaptive enable 3rd AR Library in Web**。 + + ![](xr-fallback/web-enable-fallback-img.png) + +3. 此时无需添加Fallback的Action,在Web端即可正常运行图像追踪场景,并达到与WebXR同样追踪效果。 + + + +## 提醒 + +- 除Web端的图像追踪以外,其他特性只能同步设备的传感器数据做3轴旋转追踪。 +- 由于降级方案采用了第三方库和设备自身的传感器来支持,稳定性低于正常AR功能。 +- Web端部分设备需要用户主动触发用户行为后才能访问设备权限。 +- Web端图像追踪降级方案使用了第三方提供的图像特征转换工具,构建后的包体大小会有一定量增长。 diff --git a/versions/4.0/zh/xr/architecture/xr-fallback/native-add-fallback.png b/versions/4.0/zh/xr/architecture/xr-fallback/native-add-fallback.png new file mode 100644 index 0000000000..af3c2bc920 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-fallback/native-add-fallback.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-fallback/native-enable-fallback-image.png b/versions/4.0/zh/xr/architecture/xr-fallback/native-enable-fallback-image.png new file mode 100644 index 0000000000..760a6042ef Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-fallback/native-enable-fallback-image.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-fallback/native-enable-fallback.png b/versions/4.0/zh/xr/architecture/xr-fallback/native-enable-fallback.png new file mode 100644 index 0000000000..4741eb9de1 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-fallback/native-enable-fallback.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-fallback/native-fallback-change-scene.png b/versions/4.0/zh/xr/architecture/xr-fallback/native-fallback-change-scene.png new file mode 100644 index 0000000000..e4e4356750 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-fallback/native-fallback-change-scene.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-fallback/web-change-session.png b/versions/4.0/zh/xr/architecture/xr-fallback/web-change-session.png new file mode 100644 index 0000000000..16c450f7f8 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-fallback/web-change-session.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-fallback/web-enable-fallback-img.png b/versions/4.0/zh/xr/architecture/xr-fallback/web-enable-fallback-img.png new file mode 100644 index 0000000000..f592c4449d Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-fallback/web-enable-fallback-img.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-pass-through.md b/versions/4.0/zh/xr/architecture/xr-pass-through.md new file mode 100644 index 0000000000..c9a2916efe --- /dev/null +++ b/versions/4.0/zh/xr/architecture/xr-pass-through.md @@ -0,0 +1,33 @@ +# 透视 + +透视(Pass Through)的实现方式是通过 XR 设备的摄像头捕捉现实世界的场景,再将其传输到显示器上,让用户可以看到现实世界的场景。在虚拟现实环境中,用户无法感知现实世界的环境,容易发生碰撞或者其他安全问题,使用 Pass Through 技术可以让用户感知现实世界的环境,不但可以更加安全地在虚拟现实环境中行动,还可以增强虚拟现实的沉浸感。 + +## 透视功能 + +XR 扩展提供了专门用于渲染透视图像的层,同时使用合成层技术控制透视图像与虚拟场景的融合显示关系。 + +| 属性 | 说明 | +| --------- | ----------------------------------------------------- | +| Placement | 指定 Pass Through Layer 的合成方式。 | +| Depth | 设置深度来指定 Pass Through在Composition Layer 的排序。 | +| Opacity | 设置 Pass Through 图像的不透明度。 | + +> **注意**:透视功能对接 OpenXR 的非核心扩展API。当前版本只对接了 Meta Quest 系列设备。 + +## 开启透视 + +调整 **XR HMD** 节点 Camera 组件的 Clear Flags 为 SOLID_COLOR,Clear Color 的不透明度调为 0。 + + + +为 XR HMD 节点添加透视组件,找到 **XR > Extra > XRPassThroughLayer**,点击添加。 + + + +要想将透视视频图像显示在所有3D内容之下,**Placement** 属性请选择为 **Underlay**。 + +打包即可看到透视效果。 + +![](xr-pass-through/pass-through-effect.png) + +> **注意**:使用透视功能需要扩展版本 **>=1.2.0**,编辑器版本 **>=3.7.3**。 diff --git a/versions/4.0/zh/xr/architecture/xr-pass-through/add-pass-throught-layer.png b/versions/4.0/zh/xr/architecture/xr-pass-through/add-pass-throught-layer.png new file mode 100644 index 0000000000..4f8228143b Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-pass-through/add-pass-throught-layer.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-pass-through/pass-through-effect.png b/versions/4.0/zh/xr/architecture/xr-pass-through/pass-through-effect.png new file mode 100644 index 0000000000..4640eedd17 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-pass-through/pass-through-effect.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-pass-through/set-hmd-camera.png b/versions/4.0/zh/xr/architecture/xr-pass-through/set-hmd-camera.png new file mode 100644 index 0000000000..0711878763 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-pass-through/set-hmd-camera.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-spatial-audio.md b/versions/4.0/zh/xr/architecture/xr-spatial-audio.md new file mode 100644 index 0000000000..15d25f3220 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/xr-spatial-audio.md @@ -0,0 +1,27 @@ +# XR 空间音频 + +XR 空间音频是虚拟现实领域中的一项重要技术,它可以模拟现实世界中的音频环境,让用户在虚拟现实环境中获得更加真实的听觉体验。基于头戴显示器的声音跟踪技术可以通过跟踪用户的头部运动来模拟现实世界中的音频环境。当用户在虚拟现实环境中移动头部时,系统可以根据用户的头部运动和位置来调整音频的位置和方向,从而模拟出现实世界中的音频环境。 + +## XR 空间音频功能 + +| 属性 | 描述 | +| ---------------------- | -------------------------------- | +| Clip | 引用需要挂载的音频文件。 | +| Loop | 是否循环播放音频。 | +| Play On Wake | 是否在启动项目时自动播放音频。 | +| Volume | 音量大小。 | +| Distance Rolloff Model | 音量根据距离效应进行衰减的模型。 | + +## XR 空间音频使用 + +选择想要添加音频的节点对象,在属性管理器中添加组件:**XR > Extra > XRSpatialAudioSource** + + + +## 声明 + +空间音频功能引用了 GoogleVR 库,许可凭证详见 licenses 文件夹中 LICENSE_googlevr.txt。 + +有关涵盖此 SDK 的谷歌 api 服务条款,请参阅 [https://developers.google.com/terms/](https://developers.google.com/terms/)。 + +> **注意**:使用空间音频功能需要扩展版本 **>=1.2.0**,编辑器版本 **>=3.7.3**。 diff --git a/versions/4.0/zh/xr/architecture/xr-spatial-audio/add-spatial-audio.png b/versions/4.0/zh/xr/architecture/xr-spatial-audio/add-spatial-audio.png new file mode 100644 index 0000000000..7eb0377314 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-spatial-audio/add-spatial-audio.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-video-player.md b/versions/4.0/zh/xr/architecture/xr-video-player.md new file mode 100644 index 0000000000..70aa0ab6c0 --- /dev/null +++ b/versions/4.0/zh/xr/architecture/xr-video-player.md @@ -0,0 +1,65 @@ +# XR 视频播放器 + +XR 头戴设备相较于传统的显示器拥有更为多样化的视频展示方式,结合设备自身的多轴向定位特性和双屏渲染画面,可以满足用户在 3D 场景中浏览全景视频或动态材质的需要。Cocos CreatorXR v1.1.0提供了通用化的 XR 视频播放器,针对 XR 设备优化了视频渲染管线并支持切换展示窗口、180 度、360 度多风格的视频。同时,播放器还提供了交互功能辅助您进行播放控制,您只需要添加或替换视频资源即可完成简易的视频播放功能的内容开发,简化创作步骤,降低开发门槛。 + +创建视频播放器,请在层级管理器右键 **创建 -> XR -> XR Video Player**。 + +![xr_video_player_node](xr-video-player/xr_video_player_node.png) + +其节点中包含的核心组件如下: + +## XR Video Player + +### cc.XRVideoPlayer + +用于调整视频的各项属性 + +| 属性 | 说明 | +| ------------------ | ------------------------------------------------------------ | +| Source Type | 视频来源:REMOTE 表示远程视频 URL,LOCAL 表示本地视频地址 | +| Remote URL | Source Type 为 REMOTE 时出现此项,远程视频的 URL | +| Clip | Source Type 为 LOCAL 时出现此项,本地视频剪辑 | +| Play On Awake | 视频加载后是否自动开始播放 | +| Playback Rate | 视频播放时的速率,范围:[0.0,2.5] | +| Volume | 视频的音量 [0.0,1.0] | +| Mute | 是否静音。静音时音量设置为0,取消静音时恢复原来的音量。 | +| Loop | 视频是否应在结束时再次播放 | +| Keep Aspect Ratio | 是否保持视频自身的宽高比(使用竖屏视频查看效果) | +| Shape | 视频样式。 | +| Content | 关联带有 MeshRenderer 组件的 VideoContent 作为视频材质渲染对象。 | +| Video Player Event | 视频播放回调函数,该回调函数会在特定情况被触发,比如播放中、暂停、停止和播放完毕。 | + +## XR Video Controller + +### cc.XRVideoController + +用于关联UI和视频功能。 + +| 属性 | 说明 | +| --------------------- | ------------------------------------------- | +| Player | 关联指定的 VideoPlayer,用于控制其播放功能。 | +| HMD Control | 绑定头戴显示器的控制器对象节点。 | +| Left Hand Controller | 绑定左手柄的控制器对象节点。 | +| Right Hand Controller | 绑定右手柄的控制器对象节点。 | +| Play Pause | 播放/暂停 UI。 | +| Progress Bar | 进度条 UI。 | +| Fast Forward | 快进按钮 UI。 | +| Rewind | 快退按钮 UI。 | +| Video Shape UI | 视频样式 UI。 | +| Player Back Rate Bar | 倍速 UI。 | +| Volume UI | 音量调节 UI。 | + +## Video Caption + +### cc.XRVideoCaption + +用于解析字幕文件,目前只支持解析 .srt 类型的字幕文件。 + +| 属性 | 说明 | +| ------------------- | ------------------------------------------------------------ | +| Caption Source Type | 字幕来源:REMOTE 表示 URL 里的文件并解析字幕,LOCAL 表示本地字幕文件。 | +| Remote URL | Source Type 为 REMOTE 时出现此项,字幕文件的 URL | +| Caption File | Source Type 为 LOCAL 时出现此项,本地字幕文件 | +| Video Player | 关联指定的 VideoPlayer,将字幕按时间同步于此视频。 | + +注:Vulkan 目前不支持视频播放器。 diff --git a/versions/4.0/zh/xr/architecture/xr-video-player/xr_video_player_node.png b/versions/4.0/zh/xr/architecture/xr-video-player/xr_video_player_node.png new file mode 100644 index 0000000000..764814dbf0 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-video-player/xr_video_player_node.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-webview.md b/versions/4.0/zh/xr/architecture/xr-webview.md new file mode 100644 index 0000000000..f0665cca0a --- /dev/null +++ b/versions/4.0/zh/xr/architecture/xr-webview.md @@ -0,0 +1,28 @@ +# XR 网页浏览器 + +在XR中,网页浏览器可以让用户在虚拟现实环境中访问和浏览网页。用户可以设备控制器与网页界面进行交互,如选择链接、滚动页面等等操作,以增强用户的沉浸感和体验。 + +## XR 网页浏览器功能 + +| 属性 | 描述 | +| ------- | ---------------------------------------- | +| Content | 指定用于渲染网页内容的MeshRenderer对象。 | +| Url | 网页链接。 | + +## 使用 XR 网页浏览器 + +层级管理器右键 **创建 > XR > XR Webview**。 + +默认创建一个带有 cc.MeshRenderer 组件的节点作为子节点 Webview Content。 + +![](xr-webview/create-webview.png) + +构建发布应用即可看到网页内容。 + +![](xr-webview/web-effect.png) + +注: + +- 使用网页浏览器功能需要扩展版本 **>=1.2.0**,编辑器版本 **>=3.7.3**。 + +- Vulkan 目前不支持网页播放器。 diff --git a/versions/4.0/zh/xr/architecture/xr-webview/create-webview.png b/versions/4.0/zh/xr/architecture/xr-webview/create-webview.png new file mode 100644 index 0000000000..437e9deecb Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-webview/create-webview.png differ diff --git a/versions/4.0/zh/xr/architecture/xr-webview/web-effect.png b/versions/4.0/zh/xr/architecture/xr-webview/web-effect.png new file mode 100644 index 0000000000..61eca84fa4 Binary files /dev/null and b/versions/4.0/zh/xr/architecture/xr-webview/web-effect.png differ diff --git a/versions/4.0/zh/xr/index.md b/versions/4.0/zh/xr/index.md new file mode 100644 index 0000000000..4a8b12f096 --- /dev/null +++ b/versions/4.0/zh/xr/index.md @@ -0,0 +1,3 @@ +# Cocos CreatorXR 介绍 + +Cocos CreatorXR 是基于 Cocos Creator 和 Cocos Engine 打造的一款 XR 内容创作工具。底层通过支持 OpenXR 标准协议来抹平不同 XR 设备之间的差异,可以一站式对创作内容进行开发并发布到不同的 XR 设备中而无需去适配不同设备的 I/O 项;中层封装了一系列不同功能的 XR 中间件来提供 XR 内容创作支持,并支持用户自定义扩展组件内容;上层基于 Cocos Creator 面板扩展出多种形式的 XR 功能菜单和组件样式,为用户提供更为便利的内容创作界面。 diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy.md b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy.md new file mode 100644 index 0000000000..ea938614ac --- /dev/null +++ b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy.md @@ -0,0 +1,77 @@ +# AR 项目创建 + +参照以下步骤完成对项目的 AR 相关特性配置。 + +以下提供三种方法,可以任选一种来配置扩展或直接打开内置 AR 项目。 + +### 方法一:将 xr-plugin 应用到项目 + +在 Cocos Store 中搜索 xr-plugin,获取扩展并安装,具体安装说明请参考 [说明](../../../editor/extension/install)。 + +安装完毕后将扩展添加至对应工程。 + +这种方式适合为存量 3D 项目做 XR 模式迁移。 + +![search-in-store](ar-proj-deploy/search-in-store.png) + +![install-for-proj](ar-proj-deploy/install-for-proj.png) + +### 方法二:创建 AR 模板工程 + +Dashboard 中新建项目,编辑器版本选择 v3.7.1 或更高,模板类别下选择 Empty(AR Mobile) 进行创建。 + +![create-by-template](ar-proj-deploy/create-by-template.png) + +打开项目,进入 scene 场景。场景已经包含初始的 AR Camera 配置,可以直接进行功能开发。 + +![open-ar-template](ar-proj-deploy/open-ar-template.png) + +创建 AR 案例 + +Dashboard 中新建项目,编辑器版本选择 v3.7.1 或更高,案例类别下选择 AR(移动端) 案例进行创建。 + +![create-by-ar-example](ar-proj-deploy/create-by-ar-example.png) + +案例项目中包含当前版本扩展完整的有关AR特性功能的内容,可以直接进行构建打包体验。AR 应用的构建发布相关说明请查阅 [构建与发布](ar-proj-pub.md)。 + +### 方法三:场景配置 + +若采用第一种将 xr-plugin 应用到项目的方式配置插件,还需进行以下步骤对普通的 3D 场景完成 AR 功能的基本配置。 + +应用中的每个 AR 场景中必须要包含两个关键对象:XR Agent和AR Camera。 + +推荐以下两种方式任选一种来进行场景配置: + +1. 右键单击 **层次管理器** 窗口,选择 创建 XR -> XR Agent。选中 XR Agent 节点,右键创建 Empty Node,并重命名为 TrackingSpace。选中 TrackingSpace 节点,右键创建 XR -> AR Camera。 + + ![create-ar-camera](ar-proj-deploy/create-ar-camera.png) + + 选中 XR Agent 节点,在属性检查器中点击“添加组件”,添加 XR -> AR Tracking -> ARSession 和 XR -> AR Tracking -> ARManager。 + + set-ar-comp + +2. 对于空场景或现有项目,可以直接选中场景中主摄像机,右键选择转为 AR Camera,即可得到上述默认的结构。 + + ![convert-to-ar-camera](ar-proj-deploy/convert-to-ar-camera.png) + +XR Agent 和 AR Camera 及其组件在 AR 项目中扮演着重要的角色。要更详细地了解它们,请分别查阅 [设备映射]( ../architecture/component.md#设备映射组件) 和 [AR相机](../architecture/ar-camera.md)。 + +## Spaces平台项目场景设置 + +新建空场景,将场景中 Main Camera 右键选择转为 XR HMD。 + +选中 XR Agent,点击 Add Component 添加 ARSession 和 ARManager 组件。 + +spaces-add-ar-comp + +可以参考 [平面追踪](../architecture/ar-tracking-component.md#平面追踪) 和 [图像追踪](../architecture/ar-tracking-component.md#图像追踪) 给应用做AR赋能。 + +功能开发完成后可直接打包发布。 + +在 Dashboard 的 VR 案例中,提供了一个简易的 Spaces 专用场景,可以直接打包应用并将此场景设为启动场景。 + +![open-spaces-example](ar-proj-deploy/open-spaces-example.png) + +![build-spaces](ar-proj-deploy/build-spaces.png) + +具体有关高通 Spaces 平台的 AR SDK 请参考 [这里](https://docs.spaces.qualcomm.com/)。 diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/build-spaces.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/build-spaces.png new file mode 100644 index 0000000000..5177084627 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/build-spaces.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/convert-to-ar-camera.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/convert-to-ar-camera.png new file mode 100644 index 0000000000..397459e564 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/convert-to-ar-camera.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/create-ar-camera.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/create-ar-camera.png new file mode 100644 index 0000000000..886ccc6a43 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/create-ar-camera.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/create-by-ar-example.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/create-by-ar-example.png new file mode 100644 index 0000000000..425b8d7f40 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/create-by-ar-example.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/create-by-template.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/create-by-template.png new file mode 100644 index 0000000000..b5bfb71669 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/create-by-template.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/install-for-proj.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/install-for-proj.png new file mode 100644 index 0000000000..1c3e3f7911 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/install-for-proj.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/open-ar-template.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/open-ar-template.png new file mode 100644 index 0000000000..e1027485de Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/open-ar-template.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/open-spaces-example.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/open-spaces-example.png new file mode 100644 index 0000000000..e1beaaa147 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/open-spaces-example.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/search-in-store.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/search-in-store.png new file mode 100644 index 0000000000..3510de4951 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/search-in-store.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/set-ar-comp.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/set-ar-comp.png new file mode 100644 index 0000000000..dfb6db5963 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/set-ar-comp.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/spaces-add-ar-comp.png b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/spaces-add-ar-comp.png new file mode 100644 index 0000000000..54c66f9dd4 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-deploy/spaces-add-ar-comp.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-pub.md b/versions/4.0/zh/xr/project-deploy/ar-proj-pub.md new file mode 100644 index 0000000000..a4607a4519 --- /dev/null +++ b/versions/4.0/zh/xr/project-deploy/ar-proj-pub.md @@ -0,0 +1,47 @@ +# AR 项目构建与发布 + +完成 AR 应用的项目设置并完成项目开发之后,即可打包 AR 应用。点击 **菜单栏 -> 项目 -> 构建发布**。 + +## ARCore、AREngine + +针对于安卓和华为平台的手机发布 AR 应用,新建构建任务,平台选择 **安卓**。 + +select-android-platform + +填写应用 ID 并勾选 **Enable AR**,连接好移动端设备后点击 **构建** -> **生成** -> **运行** 即可一键发布 AR 应用。 + +build-android-platform + +> **注意**:安卓平台 AR 应用的渲染后端不支持 VULKAN。 + +## ARKit + +iOS发布需要的各项配置属性请参考 [iOS 平台构建选项](../../editor/publish/native-options.md#iOS 平台构建选项),需要在 Xcode 中配置好开发者账户。 + +针对于 iOS 平台发布 AR 应用,新建构建任务,平台选择 **iOS**。 + +select-ios-platform + +应用 ID 名称第二节建议使用 Xcode 配置的同名开发者账户名,目标平台选择 **iPhone OS应用**,勾选 **Enable AR**。 + +点击构建,生成 Xcode 工程。 + +build-ios-platform + +> **注意**:目前 Cocos Creator 对 iOS 应用暂时只支持构建工程,编译和运行需要转移至 Xcode 中进行。 + +构建完成后,找到生成的 xcodeproj 文件,使用 Xcode 打开,配置好签名和开发者团队连接好设备点击运行即可。 + +open-ios-build-folder + +select-xcodeproj + +![compile-with-xcode](ar-proj-pub/compile-with-xcode.png) + +## Spaces + +针对于高通 Spaces 平台的设备发布 AR 应用,新建构建任务,平台选择 **XR Spaces**。 + +![select-spaces-platform](ar-proj-pub/select-spaces-platform.png) + +填写好应用 ID,连接好 Spaces 设备(如果是分体式设备请连接移动端)后点击 **构建 -> 生成 ->运行** 即可一键发布 AR 应用。 diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-pub/build-android-platform.png b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/build-android-platform.png new file mode 100644 index 0000000000..49cc6a4a98 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/build-android-platform.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-pub/build-ios-platform.png b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/build-ios-platform.png new file mode 100644 index 0000000000..cd772ded7d Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/build-ios-platform.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-pub/compile-with-xcode.png b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/compile-with-xcode.png new file mode 100644 index 0000000000..57e9aed143 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/compile-with-xcode.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-pub/open-ios-build-folder.png b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/open-ios-build-folder.png new file mode 100644 index 0000000000..dae479bfb0 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/open-ios-build-folder.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-android-platform.png b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-android-platform.png new file mode 100644 index 0000000000..55f3f61eb6 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-android-platform.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-ios-platform.png b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-ios-platform.png new file mode 100644 index 0000000000..dd5f9bdb95 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-ios-platform.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-spaces-platform.png b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-spaces-platform.png new file mode 100644 index 0000000000..83bd668b5f Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-spaces-platform.png differ diff --git a/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-xcodeproj.png b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-xcodeproj.png new file mode 100644 index 0000000000..aa54416421 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/ar-proj-pub/select-xcodeproj.png differ diff --git a/versions/4.0/zh/xr/project-deploy/index.md b/versions/4.0/zh/xr/project-deploy/index.md new file mode 100644 index 0000000000..68b29928fb --- /dev/null +++ b/versions/4.0/zh/xr/project-deploy/index.md @@ -0,0 +1,3 @@ +# 快速使用指南 + +这里提供建议的操作方式来创建和发布 XR 项目,使用发者可以参考以下流程快速上手使用 Cocos CreatorXR。 diff --git a/versions/4.0/zh/xr/project-deploy/vr-proj-deploy.md b/versions/4.0/zh/xr/project-deploy/vr-proj-deploy.md new file mode 100644 index 0000000000..23b2da6de6 --- /dev/null +++ b/versions/4.0/zh/xr/project-deploy/vr-proj-deploy.md @@ -0,0 +1,15 @@ +# VR 项目创建 + +Cocos CreatorXR 支持用户使用以下几种方式快速创建 VR 项目。 + +> **注意**:创建 XR 项目时务必保证编辑器版本 >= 3.6.1。 + +使用模板新建 VR 项目:在 Cocos Dashboard 中新建项目时,选择 v3.6.1 及以上的编辑器(若需要体验完整功能,引擎请选择v3.7.1及以上的版本),选择 Empty(VR) 模板创建。 + +![deploy-by-template](vr-proj-deploy/deploy-by-template.png) + +使用案例体验学习创建 VR 项目:在 Cocos Dashboard 中新建项目,选择 v3.6.1 及以上的编辑器(若需要体验完整功能,引擎请选择v3.7.1及以上的版本),选择 VR 案例创建。 + +![deploy-by-example](vr-proj-deploy/deploy-by-example.png) + +基于空项目或已有项目添加 XR 扩展:在 Dashboard 中的商场页面搜索 xr-plugin 下载安装并应用至项目或在 Cocos Creator 中的 **扩展** -> **商城** 中下载安装扩展至项目(不推荐安装到全局),扩展下载与安装请参考 [Cocos Creator扩展与安装教程](../../editor/extension/install.md)。 diff --git a/versions/4.0/zh/xr/project-deploy/vr-proj-deploy/deploy-by-example.png b/versions/4.0/zh/xr/project-deploy/vr-proj-deploy/deploy-by-example.png new file mode 100644 index 0000000000..693c961138 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/vr-proj-deploy/deploy-by-example.png differ diff --git a/versions/4.0/zh/xr/project-deploy/vr-proj-deploy/deploy-by-template.png b/versions/4.0/zh/xr/project-deploy/vr-proj-deploy/deploy-by-template.png new file mode 100644 index 0000000000..fc5ccee9e4 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/vr-proj-deploy/deploy-by-template.png differ diff --git a/versions/4.0/zh/xr/project-deploy/vr-proj-pub.md b/versions/4.0/zh/xr/project-deploy/vr-proj-pub.md new file mode 100644 index 0000000000..f02dffe8c3 --- /dev/null +++ b/versions/4.0/zh/xr/project-deploy/vr-proj-pub.md @@ -0,0 +1,31 @@ +# VR 项目构建与发布 + +当项目开发完成后,需要将项目打包到对应的平台上,在菜单栏中选中 **项目** -> **构建发布** 打开构建发布面板,通过 **发布平台** 属性下的下拉框中选择目标平台: + +![select-platform](vr-proj-pub/select-platform.png) + +## 属性 + +构建通用属性可以参考:[构建选项介绍](../../editor/publish/build-panel.md)。 + +目前支持的 VR 设备都采用了安卓系统,开发者需搭建对应的开发环境,详情可参考 [安装配置原生开发环境](../../editor/publish/setup-native-development.md)。 + +XR专用构建属性描述如下: + +**Rendering Scale**:渲染分辨率。 + +**MSAA**:调整多重采样抗锯齿等级。 + +**Remote Preview**:启用无限投屏预览。 + +**Foveation Level**:调整注视点渲染等级,级别越高时 GPU 负载越低,但双目渲染纹理边缘分辨率也会降低。(注:1、注视点渲染采用 OpenXR 通用接口,若设备 FFR 功能未对接 OpenXR 标准,此功能将不生效。2、此功能需要扩展版本 **>=1.2.0**,编辑器版本 **>=3.7.3**。) + +## 构建 + +之后在构建任务中根据需要选择 **构建**、**生成** 或 **运行** 即可。 + +![build](vr-proj-pub/build.png) + +## 发布 + +生成应用程序以后,可以通过 adb 命令或设备的传输文件功能将应用程序传输到目标设备上,之后运行则可以完成整个构建和发布过程。 diff --git a/versions/4.0/zh/xr/project-deploy/vr-proj-pub/build.png b/versions/4.0/zh/xr/project-deploy/vr-proj-pub/build.png new file mode 100644 index 0000000000..076fcb68f8 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/vr-proj-pub/build.png differ diff --git a/versions/4.0/zh/xr/project-deploy/vr-proj-pub/select-platform.png b/versions/4.0/zh/xr/project-deploy/vr-proj-pub/select-platform.png new file mode 100644 index 0000000000..c2a5502699 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/vr-proj-pub/select-platform.png differ diff --git a/versions/4.0/zh/xr/project-deploy/webxr-proj-deploy.md b/versions/4.0/zh/xr/project-deploy/webxr-proj-deploy.md new file mode 100644 index 0000000000..50648c2495 --- /dev/null +++ b/versions/4.0/zh/xr/project-deploy/webxr-proj-deploy.md @@ -0,0 +1,27 @@ +# WebXR 项目配置 + +WebXR 项目的创建和普通XR项目创建流程保持一致。 + +若需要创建沉浸式虚拟现实(VR)体验的工程,可以参考 [VR 项目创建](vr-proj-deploy.md)。 + +若需要创建沉浸式增强现实(AR)体验的工程,可以参考 [AR 项目创建](ar-proj-deploy.md)。 + +部署完毕之后需要为 **XR Agent** 节点添加 **cc.WebXRSessionController** 组件,组件位置为:**XR > Device > WebXRSessionController**。 + +根据需要选择默认 Session Mode: + +- IMMERSIVE_AR:Session 将独占访问沉浸式 XR 设备,渲染的内容将与现实世界的环境混合在一起。 +- IMMERSIVE_VR:Session 对场景的渲染不会被被覆盖或融入现实环境。 +- INLINE:3D 内容输出在标准 HTML 文档的元素上下文中内联显示,而不会占据整个视觉空间。inline session 既可以在单目渲染的设备呈现,也可以在双目立体渲染的设备中呈现;而且不关心设备是否可进行位姿追踪。inline session 不需要特殊的设备,在任何提供 WebXR API支持的 [用户代理](https://developer.mozilla.org/en-US/docs/Glossary/User_agent) 上都可以使用。 + + + +## WebXR 案例 + +Dashboard 中的两个案例都提供了专用于 WebXR 的场景。 + +VR案例的 WebXR 主场景为 webxr-main,启动时务必将此场景设为启动场景。 + +AR (移动端)案例的 WebXR 主场景为 webxr-main,启动时务必将此场景设为启动场景,其他场景如下所示。 + + diff --git a/versions/4.0/zh/xr/project-deploy/webxr-proj-deploy/add-wenxr-session-ctrl.png b/versions/4.0/zh/xr/project-deploy/webxr-proj-deploy/add-wenxr-session-ctrl.png new file mode 100644 index 0000000000..548e523afc Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/webxr-proj-deploy/add-wenxr-session-ctrl.png differ diff --git a/versions/4.0/zh/xr/project-deploy/webxr-proj-deploy/ar-scenes.png b/versions/4.0/zh/xr/project-deploy/webxr-proj-deploy/ar-scenes.png new file mode 100644 index 0000000000..06eb976be9 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/webxr-proj-deploy/ar-scenes.png differ diff --git a/versions/4.0/zh/xr/project-deploy/webxr-proj-pub.md b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub.md new file mode 100644 index 0000000000..ec17a753f1 --- /dev/null +++ b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub.md @@ -0,0 +1,44 @@ +# WebXR 项目构建与发布 + +完成 WebXR 应用的 [项目设置](webxr-proj-deploy.md) 并完成项目开发之后,即可打包 WebXR 应用。点击 **菜单栏 -> 项目 -> 构建发布**。 + +## 配置 WebXR 构建属性 + +构建平台选择 **Web 移动端**。 + + + +开启构建面板最后一个属性 **WebXR**。 + + + +点击构建即可。 + +## 配置 https 环境 + +WebXR 所使用的用于提供 Web 资源的服务必须使用 [安全上下文(secure context)](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts)。 + +而启动 https 的域名的服务一般需要配置 .pem(证书文件)。 + +由于当前版本暂时不支持启动内置 https 服务器,需要用户手动启动。 + +将 .pem 存放至构建文件夹的根目录。 + +![](webxr-proj-pub/https-license.png) + +在命令终端的此目录下输入:**http-server -S** 。启动 https 服务即可。 + + + +## 选择可用的设备和浏览器 + +支持 ARCore 的设备请参考 [ARCore官方文档](https://developers.google.com/ar/devices)。 + +支持 WebXR 的浏览器请参考 [这里](https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API#browser_compatibility)。 + +## 打开浏览器的webxr能力 + +使用 Chrome 浏览器访问 WebXR 应用前需要确认浏览器 webxr 的功能是否开启。 + +访问 **[chrome://flags](chrome://flags)** ,将 **webxr incubations** 改为 **Enable**。 + diff --git a/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/enable-webxr.png b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/enable-webxr.png new file mode 100644 index 0000000000..136bf706a4 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/enable-webxr.png differ diff --git a/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/https-license.png b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/https-license.png new file mode 100644 index 0000000000..8884d5879c Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/https-license.png differ diff --git a/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/select-web-mobile.png b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/select-web-mobile.png new file mode 100644 index 0000000000..3b31b607b1 Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/select-web-mobile.png differ diff --git a/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/start-https-server.png b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/start-https-server.png new file mode 100644 index 0000000000..9848b3f98f Binary files /dev/null and b/versions/4.0/zh/xr/project-deploy/webxr-proj-pub/start-https-server.png differ diff --git a/versions/4.0/zh/xr/version-history.md b/versions/4.0/zh/xr/version-history.md new file mode 100644 index 0000000000..62150309ae --- /dev/null +++ b/versions/4.0/zh/xr/version-history.md @@ -0,0 +1,62 @@ +# 版本历史 + +## V1.2.1 + +新增: + +- 移动端和Web端AR降级功能,支持更多设备。 +- Pico设备支持FFR、透视功能。 +- 透视纹理新增静态纹理类型。 + +## V1.2.0 + +新增: + +- iOS 支持基础模式光照估计,华为安卓平台支持基础和 HDR 模式光照估计。 +- 支持将 XR 应用构建发布至 WebXR 平台,会话支持 inline、immersive-vr、immersive-ar 三种模式。 +- 屏幕手势交互器新增行为控制功能,可自由选择控制交互物的手势行为。 +- 屏幕手势交互器组件放置功能新增放置位置的计算方式,可以基于屏幕交互器固定距离来放置内容。 +- 新增合成层(Composition Layer)功能,支持 Overlay、Underlay 两种渲染方式。 +- 新增透视(Pass Through)功能,支持 OpenXR 标准接口开启一体机视频透视功能,配合合成层可调整透视视频流的渲染方式(Overlay/Underlay)。 +- 新增 XR Web Preview 功能,支持在一体机端浏览 web 内容。 +- XR 视频播放器支持解析播放 3D 视频资源。 +- 支持空间音频,允许从各个方向渲染音频,模拟声音在物理世界中的表现,增加沉浸感。 +- 新增静态注视点渲染(FFR)调节,支持使用 OpenXR 标准接口开启 FFR 并调节渲染等级。 +- Snapdragon Spaces SDK 支持更新到 0.11.1,并新增 RGB Camera 、Meshing 特性的支持。 + +修复: + +- 屏幕手势交互的效果优化。 +- 华为 VR Glass 的手柄震动时间 0 无效问题。 +- 转换 AR Camera 报错问题修复。 +- Android Target API 高版本兼容性问题。 + +## v1.1.1 + +新增: + +- 适配 Cocos Creator 3.7.2。 + +## v1.1.0 + +新增: + +- 新增 AR 应用开发模块,可发布 AR 应用至 AREngine、ARCore、ARKit 和 Qualcomm Spaces 平台,提供自动化行为编辑组件来支持快速创建和体验 AR 内容,支持开启设备追踪(AR Camera)/平面追踪/图像追踪/锚点/Mesh 等 AR 特性。 +- 新增非缓冲式手柄控制器震动反馈,在 cc.InteractorEvent 的 Haptic Event 中选择想要开启震动的事件类型并调整震动参数。 +- XRUI 一键转换功能,传统 2D UI 可以一键转化为的带有空间属性的 XR UI。 +- 凝视交互器,根据头戴设备的注视中心位置进行交互行为。 +- XR 视频播放器,针对 XR 设备优化了视频渲染管线并支持切换展示窗口、180度、360度多风格的视频。可以满足用户在 3D 场景中浏览全景视频或动态材质的需要。 +- XR 内容预览新增无线串流方式,在 Web 浏览器中预览 XR 项目并同步所有来自 XR 设备的信号,无需打包应用至设备即可快速完整地体验所有 XR 项目内容。 +- 屏幕手势交互器,使用屏幕手势操作 AR 虚拟对象。 +- 新增 Rokid Air 设备支持手机作为 3DOF 空鼠的功能。 + +修复: + +- Huawei VR Glass(6Dof套装)对 6Dof 输入的完整支持; +- Huawei VR Glass配套的 6Dof 手柄在触摸摇杆时也产生随机信号的处理问题; + +## v1.0.1 + +- 支持发布 XR 应用至 Rokid Air、HuaweiVR、Meta Quest/Quest2、Pico Neo3/Pico4、Monado 五类设备。 +- 提供设备映射、交互、虚拟移动、XR UI 模块化组件支持 XR 内容创作。 +- 支持使用 Web 浏览器预览 XR 内容,可用键鼠操作模拟头显和手柄控制器设备。 diff --git a/versions/4.0/zh/xr/vr/build.md b/versions/4.0/zh/xr/vr/build.md new file mode 100644 index 0000000000..2c5650ed1b --- /dev/null +++ b/versions/4.0/zh/xr/vr/build.md @@ -0,0 +1,23 @@ +# 构建与发布 + +当项目开发完成后,需要将项目打包到对应的平台上,在菜单栏中选中 **项目** -> **构建发布** 打开 **构建发布面板**,通过 **发布平台** 属性下的下拉框中选择目标平台: + +![select-platform](build/select-platform.png) + +> **注意**:不同平台需要在 [扩展安装](extension.md) 启用对应的扩展才可以选择。 + +## 属性 + +构建通用属性可以参考:[构建选项介绍](../../editor/publish/build-options.md)。 + +目前支持的 VR 设备都采用了安卓系统,开发者需搭建对应的开发环境,详情可参考 [安装配置原生开发环境](../../editor/publish/setup-native-development.md)。 + +## 构建 + +之后在构建任务中根据需要选择 **构建**、**生成** 或 **运行** 即可。 + +![build](build/build.png) + +## 发布 + +生成应用程序以后,可以通过 adb 命令或设备的传输文件功能将应用程序传输到目标设备上,之后运行则可以完成整个构建和发布过程。 diff --git a/versions/4.0/zh/xr/vr/build/build.png b/versions/4.0/zh/xr/vr/build/build.png new file mode 100644 index 0000000000..076fcb68f8 Binary files /dev/null and b/versions/4.0/zh/xr/vr/build/build.png differ diff --git a/versions/4.0/zh/xr/vr/build/select-platform.png b/versions/4.0/zh/xr/vr/build/select-platform.png new file mode 100644 index 0000000000..c2a5502699 Binary files /dev/null and b/versions/4.0/zh/xr/vr/build/select-platform.png differ diff --git a/versions/4.0/zh/xr/vr/component.md b/versions/4.0/zh/xr/vr/component.md new file mode 100644 index 0000000000..d1525d4363 --- /dev/null +++ b/versions/4.0/zh/xr/vr/component.md @@ -0,0 +1,15 @@ +# XR 组件 + +Cocos CreatorXR 通过组件的组合封装为实体赋能,实体根据其不同特性又被不同的功能系统所管理。所以编辑器中所有XR相关的功能底层都是由封装好的特殊 XR 组件驱动的。 + +Cocos CreatorXR 的功能组件主要由 5 部分构成: + +- [设备映射](device-mapping.md) +- [交互组件](interaction.md) +- [事件系统](events.md) +- [虚拟移动组件](locomotion.md) +- [XR UI](xrui.md) + +开启了 xr-plugin 扩展之后,想要给场景中的对象添加 XR 相关的功能组件可以在 **属性检查器** 中点击 **添加组件** 按钮,在出现的组件列表中找到 **XR** 分类,选择 XR 分类下的想要添加的 XR 组件类别再找到类别下的对应组件即可。 + +![add-component](component/component.png) diff --git a/versions/4.0/zh/xr/vr/component/component.png b/versions/4.0/zh/xr/vr/component/component.png new file mode 100644 index 0000000000..6abfa20571 Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/component.png differ diff --git a/versions/4.0/zh/xr/vr/component/continuous_mover.png b/versions/4.0/zh/xr/vr/component/continuous_mover.png new file mode 100644 index 0000000000..6964ee5b44 Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/continuous_mover.png differ diff --git a/versions/4.0/zh/xr/vr/component/continuous_turner.png b/versions/4.0/zh/xr/vr/component/continuous_turner.png new file mode 100644 index 0000000000..a983a2883a Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/continuous_turner.png differ diff --git a/versions/4.0/zh/xr/vr/component/direct_interator.png b/versions/4.0/zh/xr/vr/component/direct_interator.png new file mode 100644 index 0000000000..d74974e79a Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/direct_interator.png differ diff --git a/versions/4.0/zh/xr/vr/component/grab_interactor.png b/versions/4.0/zh/xr/vr/component/grab_interactor.png new file mode 100644 index 0000000000..573bc4c7a3 Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/grab_interactor.png differ diff --git a/versions/4.0/zh/xr/vr/component/hdmctrl.png b/versions/4.0/zh/xr/vr/component/hdmctrl.png new file mode 100644 index 0000000000..2eaf3d1bbe Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/hdmctrl.png differ diff --git a/versions/4.0/zh/xr/vr/component/locomotion_checker.png b/versions/4.0/zh/xr/vr/component/locomotion_checker.png new file mode 100644 index 0000000000..faaba24c6c Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/locomotion_checker.png differ diff --git a/versions/4.0/zh/xr/vr/component/pose-tracker.png b/versions/4.0/zh/xr/vr/component/pose-tracker.png new file mode 100644 index 0000000000..9232ac6134 Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/pose-tracker.png differ diff --git a/versions/4.0/zh/xr/vr/component/ray_interactor.png b/versions/4.0/zh/xr/vr/component/ray_interactor.png new file mode 100644 index 0000000000..e585cf57e9 Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/ray_interactor.png differ diff --git a/versions/4.0/zh/xr/vr/component/sharp_turner.png b/versions/4.0/zh/xr/vr/component/sharp_turner.png new file mode 100644 index 0000000000..6f2bfe367f Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/sharp_turner.png differ diff --git a/versions/4.0/zh/xr/vr/component/target-eye.png b/versions/4.0/zh/xr/vr/component/target-eye.png new file mode 100644 index 0000000000..11ffaa9f14 Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/target-eye.png differ diff --git a/versions/4.0/zh/xr/vr/component/teleportable.png b/versions/4.0/zh/xr/vr/component/teleportable.png new file mode 100644 index 0000000000..9e78c20abb Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/teleportable.png differ diff --git a/versions/4.0/zh/xr/vr/component/teleporter.png b/versions/4.0/zh/xr/vr/component/teleporter.png new file mode 100644 index 0000000000..7adb384bbe Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/teleporter.png differ diff --git a/versions/4.0/zh/xr/vr/component/tracking-origin.png b/versions/4.0/zh/xr/vr/component/tracking-origin.png new file mode 100644 index 0000000000..8301705d09 Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/tracking-origin.png differ diff --git a/versions/4.0/zh/xr/vr/component/xrcontroller.png b/versions/4.0/zh/xr/vr/component/xrcontroller.png new file mode 100644 index 0000000000..63d67b6406 Binary files /dev/null and b/versions/4.0/zh/xr/vr/component/xrcontroller.png differ diff --git a/versions/4.0/zh/xr/vr/device-mapping.md b/versions/4.0/zh/xr/vr/device-mapping.md new file mode 100644 index 0000000000..74dfcb3b3c --- /dev/null +++ b/versions/4.0/zh/xr/vr/device-mapping.md @@ -0,0 +1,67 @@ +# 设备映射组件 + +此类组件主要用以同步现实世界中物理设备和虚拟世界中的代理节点之间的 I/O 信息。确保在用户在 XR 设备的使用和虚拟世界中的反馈一致。 + +主要包括以下组件: + +## TrackingOrigin + +追踪原点代理组件。 + +![tracking-origin.png](component/tracking-origin.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Offset Object** | 指定需要竖直偏移的对象,如果选择的对象还有子对象,则偏移的效果是选中对象及其所有子对象进行纵向偏移。 | +| **Tracking Origin Mode** | 追踪的偏移方式。选择 Unbond 和 Device 时,下方出现 YOffsetValue,可手动输入数据;选择 Floor 时YoffsetValue 隐藏。如果选择为 Floor,且设备开启了安全边界,则以设备离地面高度作为当前视角高度(暂时只支持 quest2 );如果选择为Device,则偏移的高度为输入的高度。 | +| **YOffset Value** | 设备数值偏移量。手动输入偏移的值,米为单位,默认为1.36144m。如果为固定值,则OffsetObject选中的对象Transform属性的Y值为当前填入的值。 | + +## HMDCtrl + +HMD(Head Mounted Display)头戴显示设备控制器。该组件抽象了头戴设备的渲染相关的参数。 + +![hmdctrl](component/hdmctrl.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Stereo Render** | | +| **Per Eye Camera** | 开启单眼显示功能;勾选 PerEyeCamera 后下方出现 Sync With Main Camera 选项;XR HMD 下的两个子节点 LeftEye 和 RightEye 由隐藏变为显示 | +| **Sync With Main Camera** | 同步主摄像头参数;设置为 on 时,XR HMD 下的两个子节点 LeftEye 和 RightEye 的 camera,会进行一次同步 XR HMD 的 camera 属性 | +| **IPDOffset** | 调整瞳间距。下拉列表中选择 Manual 时下方出现 OffsetValue 输入框;在 PerEyeCamera 开启的情况下,调整 Manual 的参数可让 LeftEye 和 RightEye 的 TransForm 属性 X 值变化(变化的值为 ±IPDOffset/2) | +| **Aspect Ratio** | | + +## PoseTracker + +位姿追踪组件。 + +![pose-tracker](component/pose-tracker.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Tracking Source** | 选择需要追踪的设备源 | +| **Tracking Type** | 追踪模式。选择 POSITION_AND_ROTATION 时追踪设备的平移 + 旋转;选择 POSITION 时只追踪平移的量;选择 ROTATION 时只追踪旋转的量。 | + +## TargetEye + +指定接受渲染相机的组件。 + +![target-eye](component/target-eye.png) + +| 属性 | 说明 | +| :--- | :--- | +| **Target Eye** | 指定渲染的目镜。Both 为左右眼都显示,left 为左眼,right 为右眼。| + +## XRController + +控制器抽象组件。 + +![xrcontroller](component/xrcontroller.png) + +| 属性 | 说明 | +| :--- | :--- | +| **InputDevice** | 绑定输入手柄设备。| +| **SelectAction** | 将 Select 行为映射到手柄的对应实体按键 | +| **ActivateAction** | 将 Activate 行为映射到手柄的对应实体按键 | +| **UIPressAction** | 将交互 UI 行为映射到手柄的对应实体按键 | +| **AxisToPressThreshold** | 行为触发时的阈值 | +| **Model** | 用于指定手柄的模型对象。| diff --git a/versions/4.0/zh/xr/vr/events.md b/versions/4.0/zh/xr/vr/events.md new file mode 100644 index 0000000000..5cebae4373 --- /dev/null +++ b/versions/4.0/zh/xr/vr/events.md @@ -0,0 +1,87 @@ +# 事件 + +在项目开发过程中用户会遇到很多依赖于 XR 组件的信号来触发的逻辑。为了方便用户专注于逻辑开发而不用关心两者之间的通知关系,Cocos CreatorXR 基于传统的事件系统在一部分常被事件依赖的交互组件中封装好了一部分事件信号,用户只需要在特定的信号上绑定一系列想要触发的对象和方法即可。 + +## 事件信号 + +事件信号分为以下三种: + +- **Hover**:指的是 XR 的输入设备的射线覆盖到物体上时触发 +- **Select**:当在输入设备上按下 Select 映射的按键时触发 +- **Active**:当在输入设备上按下 Active 映射的按键时触发 + +其交互行为描述如下: + +| 交互行为 | 事件信号 | 说明 | +| :------- | :--------------- | :-------------------- | +| Hover | OnHoverEntered | Hover 行为执行的时刻 | +| | OnHoverExited | Hover 行为退出的时刻 | +| | OnHoverCanceled | Hover 行为取消的时刻 | +| Select | OnSelectEntered | Select 行为执行的时刻 | +| | OnSelectExited | Select 行为退出的时刻 | +| | OnSelectCanceled | Select 行为取消的时刻 | +| Active | OnActivated | 激活的时刻 | +| | OnDeactivated | 取消激活的时刻 | + +这些事件信号,可以在下列组件中选择,用于处理特定情形下的输入状态。 + +## 交互事件组件 + +交互组件分为: + +- **InteractorEvents**:交互器事件组件。 +- **InteractableEvents**:可交互对象事件组件。 + +其属性描述如下: + +## InteractorEvents + +![interactor-event](events/interactor-event.png) + +| 属性 | 说明 | +| :--- | :--- | +| AudioEvents | 开启后可以绑定事件触发时播放的音频 | +| HapticEvents | 开启后可以绑定事件触发时控制器的震动反馈 | +| InteractorEvents | 开启后可以绑定任意回调函数| + +- **Audio Events**: + + Audio Events 可以根据 **事件信号** 选择不同类型的事件触发,触发后可以播放特定的音频。 + + ![audio-event](events/audio-event.png) + + 选中 **事件信号** 右侧的单选框可启用/禁用该事件信号,启用后可以通过 **AudioClip** 属性右侧的下拉菜单选择不同的音频资源。 + + ![audio-event-enabled](events/audio-event-enabled.png) + +- **Haptic Events**: + + ![haptic-event](events/haptic-event.png) + + 选中 **事件信号** 右侧的单选框后,可以调整控制器的震动反馈,通过震动可以给与用户更真实的触感反馈。 + + ![haptic-event-enable](events/haptic-event-enable.png) + + - **Haptic Intensity**:震动的灵敏度 [0,1] + - **Duration**:持续的时长 + +- **Interactor Events**: + + ![interactor-event](events/interactor-event.png) + + 在右侧的输入框内,输入任意整数值,可在 **Interactor Events** 数组内添加元素。 + + ![interactor-event-enable](events/interactor-event-enable.png) + + 添加完成后,可对事件回调进行配置: + + - **Node**:回调接收节点 + - **组件**:回调组件 + - **方法**:回调方法 + - **CustomEventData**:自定义事件的数据,这些数据会被当做回调方法的参数,传入到上述的回调方法中。 + +## InteractableEvents + +![interactable-event](events/interactable-event.png) + +该组件的事件使用方式和 **InteractorEvents** 组件的 **Interactor Events** 属性类似,也可以通过在右侧输入框修改整数值的方式修改事件的数组的长度,并配置响应的回调。 diff --git a/versions/4.0/zh/xr/vr/events/audio-event-enabled.png b/versions/4.0/zh/xr/vr/events/audio-event-enabled.png new file mode 100644 index 0000000000..68947fe852 Binary files /dev/null and b/versions/4.0/zh/xr/vr/events/audio-event-enabled.png differ diff --git a/versions/4.0/zh/xr/vr/events/audio-event.png b/versions/4.0/zh/xr/vr/events/audio-event.png new file mode 100644 index 0000000000..55546db704 Binary files /dev/null and b/versions/4.0/zh/xr/vr/events/audio-event.png differ diff --git a/versions/4.0/zh/xr/vr/events/haptic-event-enable.png b/versions/4.0/zh/xr/vr/events/haptic-event-enable.png new file mode 100644 index 0000000000..536c4006ab Binary files /dev/null and b/versions/4.0/zh/xr/vr/events/haptic-event-enable.png differ diff --git a/versions/4.0/zh/xr/vr/events/haptic-event.png b/versions/4.0/zh/xr/vr/events/haptic-event.png new file mode 100644 index 0000000000..56abb7e7b1 Binary files /dev/null and b/versions/4.0/zh/xr/vr/events/haptic-event.png differ diff --git a/versions/4.0/zh/xr/vr/events/interactable-event.png b/versions/4.0/zh/xr/vr/events/interactable-event.png new file mode 100644 index 0000000000..3c81ea449a Binary files /dev/null and b/versions/4.0/zh/xr/vr/events/interactable-event.png differ diff --git a/versions/4.0/zh/xr/vr/events/interactor-event-enable.png b/versions/4.0/zh/xr/vr/events/interactor-event-enable.png new file mode 100644 index 0000000000..20cb255be4 Binary files /dev/null and b/versions/4.0/zh/xr/vr/events/interactor-event-enable.png differ diff --git a/versions/4.0/zh/xr/vr/events/interactor-event.png b/versions/4.0/zh/xr/vr/events/interactor-event.png new file mode 100644 index 0000000000..24cdd0edb0 Binary files /dev/null and b/versions/4.0/zh/xr/vr/events/interactor-event.png differ diff --git a/versions/4.0/zh/xr/vr/extension.md b/versions/4.0/zh/xr/vr/extension.md new file mode 100644 index 0000000000..22926e0197 --- /dev/null +++ b/versions/4.0/zh/xr/vr/extension.md @@ -0,0 +1,26 @@ +# 扩展安装 + +Cocos CreatorXR 的所支持的预置、功能组件、内容打包和发布的依赖全部是基于扩展来进行开启/关闭的。 + +Cocos CreatorXR 的安装方式和普通的扩展一致,将对应的插件安装到指定的位置(如要作为项目扩展,可安装到项目内的 extension 目录)点击 **工具栏** 上的 **扩展** 按钮,在下拉列表中选择 **扩展管理器** 即可安装。 + +![extension_button](extension/extension_button.png) + +安装扩展的详情请参考:[安装与分享](../../editor/extension/install.md)。 + +## 扩展介绍 + +在项目或者全局分类下找到带有 xr- 前缀的扩展,将需要的扩展开启。 + +![extension](extension/extension.png) + +| 名称 | 说明 | +| :--- | :---| +| xr-plugin | 开启所有 XR 相关的 [预制体](prefab.md) 和 [组件](component.md)。
必须开启这些扩展才可以正常开发,如果禁用,则相关组件无法使用 | +| xr-meta | 开启支持 XR 内容发布至 [Meta Quest/Quest2 设备](https://store.facebook.com/quest/) 的功能。可选 | +| xr-pico | 开启支持 XR 内容发布至 [Pico Neo3 设备](https://www.pico-interactive.com/cn/neo3/) 的功能。可选 | +| xr-rokid | 开启支持 XR 内容发布至 [Rokid Air 设备](https://air.rokid.com/) 的功能。可选 | +| xr-monado | 开启支持 XR 内容发布至移动端 [Monado 平台](https://monado.dev/) 的功能。可选 | +| xr-huaweivr | 开启支持 XR 内容发布至 HuaweiVR Glass的功能。可选 | + +开启支持发布对应平台功能的扩展之后,在构建界面的发布平台下拉选项中出现设备选项,具体操作详见 [构建与发布](build.md)。 diff --git a/versions/4.0/zh/xr/vr/extension/extension.png b/versions/4.0/zh/xr/vr/extension/extension.png new file mode 100644 index 0000000000..38e8e6e2ed Binary files /dev/null and b/versions/4.0/zh/xr/vr/extension/extension.png differ diff --git a/versions/4.0/zh/xr/vr/extension/extension_button.png b/versions/4.0/zh/xr/vr/extension/extension_button.png new file mode 100644 index 0000000000..50fd649685 Binary files /dev/null and b/versions/4.0/zh/xr/vr/extension/extension_button.png differ diff --git a/versions/4.0/zh/xr/vr/index.md b/versions/4.0/zh/xr/vr/index.md new file mode 100644 index 0000000000..09a448f08e --- /dev/null +++ b/versions/4.0/zh/xr/vr/index.md @@ -0,0 +1,12 @@ +# Cocos CreatorXR——VR 插件 + +此部分主要做 Cocos CreatorXR 的 VR 插件部分的构成、使用说明和各部分组件功能的介绍。 + +## 内容 + +- [XR 项目创建](template.md) +- [扩展安装](extension.md) +- [内置资源与预制体](prefab.md) +- [XR 组件](component.md) +- [预览](preview.md) +- [构建与发布](build.md) diff --git a/versions/4.0/zh/xr/vr/interaction.md b/versions/4.0/zh/xr/vr/interaction.md new file mode 100644 index 0000000000..da919dfb3c --- /dev/null +++ b/versions/4.0/zh/xr/vr/interaction.md @@ -0,0 +1,64 @@ +# 交互组件 + +一次交互操作需要有两种对象协调完成:交互主体和被交互物,对应的交互组件由此也分为两类:Interactor(交互器)和 Interactable(可交互对象)。 + +## RayInteractor + +射线交互器组件。 + +![RayInteractor](component/ray_interactor.png) + +| 属性 | 说明 | +| :------------------ | :------------------ | +| AttachTransform | 用此 AttachTransform 的位置作为抓取的物体最终落到的位置,如果为空就用当前 Interactor 的位置 | +| ForceGrab | 远距离抓起;开启时被抓对象吸附到 Attach Transform、关闭后抓取挂载在交互点的位置 | +| RayOriginTransform | 可以改变发出 Ray 的起始位置,为空就默认是当前 Interactor 的位置 | +| LineType | 改变射线检测和射线样式;StraightLine 是直线;Projectile Line 是抛物线;Bezier Line 是贝塞尔曲线(目前只支持直线) | +| MaxRayDistance | 射线交互可以触发的最远距离 | +| RaycastMask | 只能和此 Layer 类型的交互物发生交互 | +| SelectActionTrigger | Select 行为触发机制,详情见交互功能介绍 | + +## DirectInteractor + +直接交互器组件。 + +![DirectInteractor](component/direct_interator.png) + +| 属性 | 说明 | +| :------------------ | :------------------ | +| AttachTransform | 用此 AttachTransform 的位置作为抓取的物体最终落到的位置,如果为空就用当前 Interactor 的位置 | +| SelectActionTrigger | Select 行为触发机制,详情见交互功能介绍 | + +## GrabInteractable + +可抓取交互对象组件。 + +![GrabInteractable](component/grab_interactor.png) + +| 属性 | 说明 | +| :------------------ | :------------------ | +| AttachTransform | 用此 AttachTransform 的位置作为触碰点的位置,如果为空就用当前 node 的位置 | +| AttachEaseInTime | 对象被抓取到 AttachTransform 位置过程的时间(在该时间内,被抓取对象会有一个拖尾的效果,持续时间越久效果越不明显) | +| GrabTrigger | 两种方式用于触发抓取:当 Select 的行为触发时/当 Activate 行为触发时 | +| RayReticle | 当交互器与此交互物发生交互碰撞计算时,在碰撞点会显示此属性所引用的对象 | +| HideController | 开启时,此物体被抓取后会隐藏 XR Controller 所引用的 Model | +| ThrowOnDetach | 开启后,允许模拟抛物的动作。 | +| ThrowSimulationMode | 选择为 InheritRigidbody 时,物体抛出时继承刚体的速度;选择为 CurveComputation 时,出现下列两项ThrowSmoothingDuration、ThrowSmoothingCurve,允许自定义抛出速度的计算 | +| ThrowSmoothingDuration | 速度计算的采样时间段。使用此值作为采样区间,用于计算物体被抛出前的速度的加权平均值,作为物体抛出时的初速度 | +| ThrowSmoothingCurve | 速度采样曲线。根据绘制的曲线进行抛出时初速度的加权平均值计算 | +| ThrowVelocityScale | 初速度的权重系数。权重越大,抛物瞬时速度所乘的系数也越大,在继承或者加权计算出的初速度基础上乘以一个系数 | +| ThrowAngularVelocityScale | 初角速度的权重系数。权重越大,抛物瞬时角速度所乘的系数也越大,在继承或者加权计算出的初始角速度基础上乘以一个系数 | + +## Teleportable + +可传送对象组件。 + +![Teleportable](component/teleportable.png) + +| 属性 | 说明 | +| :------------------- | :------------------ | +| TeleportableType | 传送点的类型。选择为Area时,传送到射线与传送区域交互点的位置;选择为 Anchor 时,不受射线与传送区域的交互点的限制,之间传送到区域的固定位置。 | +| Teleport Anchor Node | TeleportableType 选择为 Anchor 时出现此项,用于标定传送的落点。此项为空时传送到传送区域的默认中心区域;此项引用了其他对象就传送到引用的对象的位置 | +| RayReticle | 当交互器与此交互物发生交互碰撞计算时,在碰撞点会显示此属性所引用的对象 | +| TeleportTrigger | 触发传送行为的事件:OnSelectExited 表示 Select 行为结束的时刻(按钮抬起)执行传送;OnSelectEntered 表示Select触发的时刻(按钮按下) | +| Teleporter | 指定需要被传送的主体(一般为XR Agent),主体需挂载 **Teleporter** | diff --git a/versions/4.0/zh/xr/vr/locomotion.md b/versions/4.0/zh/xr/vr/locomotion.md new file mode 100644 index 0000000000..df42c829ac --- /dev/null +++ b/versions/4.0/zh/xr/vr/locomotion.md @@ -0,0 +1,64 @@ +# 虚拟移动组件 + +在绝大多数的 VR 项目中,用户会用第一人称的角色视角在虚拟场景中进行移动。这种移动行为一般不会依赖于用户在真实空间的移动反馈,因为位姿的追踪会受到现实世界物理空间的限制。因此我们需要一种类似传统 3D 游戏中那种利用接受控制器输入信号的方式来驱动移动行为的组件,称之为虚拟移动组件(Locomotion Component)。 + +## LocomotionChecker + +运动检查器。 + +![ContinuousMover](component/locomotion_checker.png) + +| 属性 | 说明 | +| :------- | :------- | +| XR Agent | 指定需要进行运动的XR Agent(或其他对象)。添加该组件时,默认绑定遍历当前场景得到的第一个挂载了TrackingOrigin的节点,通常是XR Agent;用户也可以自行指定需要进行locomotion操作的对象 | + +## Teleporter + +传送驱动组件。 + +![Teleporter](component/teleporter.png) + +| 属性 | 说明 | +| :------- | :------- | +| Checker | 添加该组件时,默认绑定遍历当前场景得到的第一个挂载Locomotion Checker组件的节点 用户也可以自己拖入需要指定的Locomotion Checker | + +## SharpTurner + +瞬间转向驱动。 + +![SharpTurner](component/sharp_turner.png) + +| 属性 | 说明 | +| :------------ | :------------ | +| Checker | 添加该组件时,默认绑定遍历当前场景得到的第一个挂载Locomotion Checker组件的节点 用户也可以自己拖入需要指定的Locomotion Checker | +| InputDevice | 绑定挂载了XRController的控制器对象 | +| TurnAngle | 转动角度 | +| EnableTurnAround | 开启后按下摇杆允许旋转180°(点击摇杆的按钮) | +| ActivationTimeout | 执行连续的转弯时需要等待的时间 | + +## ContinuousTurner + +连续转弯驱动。 + +![ContinuousTurner](component/continuous_turner.png) + +| 属性 | 说明 | +| :------------ | :------------ | +| Checker | 添加该组件时,默认绑定遍历当前场景得到的第一个挂载Locomotion Checker组件的节点 用户也可以自己拖入需要指定的Locomotion Checker | +| InputDevice | 绑定挂载了XRController的控制器对象 | +| InputControl | 绑定接受输入的摇杆 | +| TurnSpeed | 转动的角速度 | + +## ContinuousMover + +平移运动驱动。 + +![ContinuousMover](component/continuous_mover.png) + +| 属性 | 说明 | +| :------------ | :------------ | +| Checker | 添加该组件时,默认绑定遍历当前场景得到的第一个挂载Locomotion Checker组件的节点 用户也可以自己拖入需要指定的Locomotion Checker | +| InputDevice | 绑定挂载了XRController的控制器对象 | +| InputControl | 绑定接受输入的摇杆 | +| MoveSpeed | 移动速度 | +| ForwardSource | 选择一个物体,用该物体节点的朝向作为移动的正方向 | diff --git a/versions/4.0/zh/xr/vr/prefab.md b/versions/4.0/zh/xr/vr/prefab.md new file mode 100644 index 0000000000..8f775e5486 --- /dev/null +++ b/versions/4.0/zh/xr/vr/prefab.md @@ -0,0 +1,35 @@ +# 内置资源与预制体 + +在 Cocos 扩展管理器中 [开启 XR 扩展](extension.md) 之后就可以允许在编辑器中使用传统创建对象的方式创建 XR 对象。 + +在 **层级管理器** 右键选择 **创建** -> **XR**,右侧会出现当前可以创建的所有 XR 预制体。选择想要实例化生成的对象即可在场景中创建出来。 + +![prefab.png](prefab/prefab.png) + +## 内置预制体 + +| 名称 | 说明 | 包含组件 | +| :--- | :--- | :--- | +| XR Agent | 现实世界主角相关的信息在虚拟场景中的代理节点,同时具有用于控制虚拟世界中 XR 主角的生命周期的功能。 | TrackingOrigin | +| XR HMD | 头戴显示器设备在虚拟世界中的抽象节点,基于 Camera 对象进行改造生成,用于同步现实世界中头戴显示器的输入信号并将引擎渲染结果输出至设备。 | Camera
AudioSource
HMDCtrl
MRSight
PoseTracker
TargetEye | +| Ray Interactor | 用于进行远距离交互的射线交互器,包含对 XR 设备手柄控制器的 I/O 映射以及射线交互功能。 | PoseTracker
XRController
RayInteractor
Line | +| Direct Interactor | 用于进行近距离直接交互的交互器,同时也包含了对 XR 设备手柄控制器的 I/O 映射以及交互功能 | PoseTracker
XRController
DirectInteractor | +| Locomotion Checker | 运动检查器,充当所有虚拟运动驱动访问 XR Agent 的仲裁者,可以保证固定时间内对唯一的运动状态的维持。 | LocomotionChecker | +| Teleportable | 支持与交互器发生传送交互行为的交互物,可以传送 XR Agent 到此对象相关的一个位置。 | Teleportable
InteractableEvents | +| Simple Interactable | 简易的交互物对象,用户可以在此对象上自定义扩展任意的交互行为 | InteractableEvents | +| Grab Interactable | 支持与交互器发生抓取行为的交互物 | RigidBody
GrabInteractable
InteractableEvents | + +## 内置资源 + +开启 XR 的扩展后,在内置资源数据库(xr-plugin)中会新增 XR 预制体、材质和模型等资源,可供用户直接使用。具体位置如下图所示。 +- 预制体资源 + + ![default_prefabs_xr](prefab/default_prefabs_xr.png) + +- 材质资源 + + ![default_material_xr](prefab/default_material_xr.png) + +- 模型资源 + + ![prefab/default_model_xr](prefab/default_model_xr.png) diff --git a/versions/4.0/zh/xr/vr/prefab/default_material_xr.png b/versions/4.0/zh/xr/vr/prefab/default_material_xr.png new file mode 100644 index 0000000000..6fd008309b Binary files /dev/null and b/versions/4.0/zh/xr/vr/prefab/default_material_xr.png differ diff --git a/versions/4.0/zh/xr/vr/prefab/default_model_xr.png b/versions/4.0/zh/xr/vr/prefab/default_model_xr.png new file mode 100644 index 0000000000..4afe8a3cb7 Binary files /dev/null and b/versions/4.0/zh/xr/vr/prefab/default_model_xr.png differ diff --git a/versions/4.0/zh/xr/vr/prefab/default_prefabs_xr.png b/versions/4.0/zh/xr/vr/prefab/default_prefabs_xr.png new file mode 100644 index 0000000000..638925080f Binary files /dev/null and b/versions/4.0/zh/xr/vr/prefab/default_prefabs_xr.png differ diff --git a/versions/4.0/zh/xr/vr/prefab/prefab.png b/versions/4.0/zh/xr/vr/prefab/prefab.png new file mode 100644 index 0000000000..7c514d3cda Binary files /dev/null and b/versions/4.0/zh/xr/vr/prefab/prefab.png differ diff --git a/versions/4.0/zh/xr/vr/preview.md b/versions/4.0/zh/xr/vr/preview.md new file mode 100644 index 0000000000..379771b6db --- /dev/null +++ b/versions/4.0/zh/xr/vr/preview.md @@ -0,0 +1,22 @@ +# 预览 + +为了方便开发者在项目开发过程中实时调试,快速验证一些传统的功能逻辑来提高开发效率,Cocos CreatorXR 基于Cocos Creator的 Web Preview 功能开发了适用于 XR 项目的预览功能。 + +## 操作说明 + +在 xr-plugin 的资源库中找到 XR Simulator,将其拖拽至场景中。 + + + +在编辑器的预览选项中选择浏览器预览,并点击运行。 + + + +运行后即可在浏览器中进行模拟预览。 + +![web preview](preview/web preview.png) + +键盘 WASD 来控制角色整体(HMD + 手柄)进行前左后右移动,QE 控制整体上升和下降。 +键盘 Latin部 分的数字键 123 功能分别为:1.鼠标键盘的控制对象切换至 XR Agent (角色自身)此时前后左右上下作用于整体角色,鼠标滑动控制 HMD(Camera)转动,射线发出位置位于 HMD 中央,空格键用于触发click(点击),按住空格拖动鼠标触发drag(点击);2 和 3 将鼠标和键盘的控制对象切换至左/右手柄,此时前后左右上下作用于单独的左/右手柄,射线从手柄位置发出,空格键用于触发click(点击),按住空格拖动鼠标触发drag(拖动)。 +长按 B 键重置手柄位置。 +控制手柄移动时,正前方向向量始终和 XR Agent 的前向保持一致。 diff --git a/versions/4.0/zh/xr/vr/preview/run.png b/versions/4.0/zh/xr/vr/preview/run.png new file mode 100644 index 0000000000..f50e8d151c Binary files /dev/null and b/versions/4.0/zh/xr/vr/preview/run.png differ diff --git a/versions/4.0/zh/xr/vr/preview/web preview.png b/versions/4.0/zh/xr/vr/preview/web preview.png new file mode 100644 index 0000000000..ae2cb75a10 Binary files /dev/null and b/versions/4.0/zh/xr/vr/preview/web preview.png differ diff --git a/versions/4.0/zh/xr/vr/preview/xr-simulator.png b/versions/4.0/zh/xr/vr/preview/xr-simulator.png new file mode 100644 index 0000000000..c0908e254f Binary files /dev/null and b/versions/4.0/zh/xr/vr/preview/xr-simulator.png differ diff --git a/versions/4.0/zh/xr/vr/template.md b/versions/4.0/zh/xr/vr/template.md new file mode 100644 index 0000000000..f3af276c24 --- /dev/null +++ b/versions/4.0/zh/xr/vr/template.md @@ -0,0 +1,16 @@ +# XR项目创建 + +Cocos CreatorXR 支持用户使用一下几种方式快速创建 XR 项目。 + +> **注意**:创建 XR 项目时务必保证编辑器版本 >= 3.6.1。 + +- 使用模板新建 XR 项目:在 Cocos Dashboard 中新建项目时,选择 v3.6.1 及以上的编辑器,选择 Empty(XR) 模板创建。 + + ![template](template/template.png) + +- 使用案例体验学习创建 XR 项目:在 Cocos Dashboard 中新建项目,选择 v3.6.1 及以上的编辑器,选择 XRHelloWorld 案例创建。 + + ![example](template/example.png) + +- 基于空项目或已有项目添加 XR 扩展:在 Dashboard 中的商场页面搜索 xr-plugin 下载安装并应用至项目或在 Cocos Creator 中的 **扩展->商城** 中下载安装扩展至项目(不推荐安装到全局)。 + diff --git a/versions/4.0/zh/xr/vr/template/example.png b/versions/4.0/zh/xr/vr/template/example.png new file mode 100644 index 0000000000..f5586cd42d Binary files /dev/null and b/versions/4.0/zh/xr/vr/template/example.png differ diff --git a/versions/4.0/zh/xr/vr/template/template.png b/versions/4.0/zh/xr/vr/template/template.png new file mode 100644 index 0000000000..977ac0e5e2 Binary files /dev/null and b/versions/4.0/zh/xr/vr/template/template.png differ diff --git a/versions/4.0/zh/xr/vr/xrui.md b/versions/4.0/zh/xr/vr/xrui.md new file mode 100644 index 0000000000..e61a3bc82d --- /dev/null +++ b/versions/4.0/zh/xr/vr/xrui.md @@ -0,0 +1,69 @@ +# XR UI + +传统的生成 UI 控件的方式都是把 UI 画在画布(Canvas)上,而画布本身不具有深度信息(位置属性不可更改),导致了画布是贴在屏幕上的,只有与屏幕进行交互才能反馈作用于 UI 控件。由于 XR 设备的摄像头是两个目镜,不支持交互,这明显不满足于 XR 项目的需求。所以我们需要将 UI 的交互方式改为在空间中用交互器进行交互,因此需要将 UI 控件剥离出画布而能够单独存在于空间中,具有完整的位置属性并具有碰撞检测功能。 + +XR UI 是基于 2D UI 扩展而来,关于如何使用 2D/UI 组件可参考 [2D 对象概述](../../2d-object/index.md)。 + +## 新建 UI + +在 **层级管理器** -> **创建** -> **XR** -> **XR UI** 可以添加 XRUI。 + +![create](xrui/create.png) + +相比于传统的 UI 控件,XR UI 会新增一些组件用于计算碰撞检测以触发交互,如图示,RaycastChecker 和 BoxCollider 可使其能够接收 XR 输入: + +![components](xrui/xr-ui-comp.png) + +> **注意**:对于 3D 空间上的 UI,其根节点上需要 [RenderRoot2D 组件](../../2d-object/index.md) 才可以正常渲染。 + +## 存量 UI 转换 + +如 UI 已制作完成,也参考下面的步骤将原本的 2D Canvas 下的 UI,转化为 XR 的 UI。 + +- 在 **层级管理器** 右键创建一个空节点(如命名为 UIRoot,下文均使用 UIRoot),为节点添加组件 **RenderRoot2D** 组件,同时节点会自动添加 **UITransform** 组件: + + ![ui root](xrui/ui-root.png) + +- 将原有的2D UI控件分离出Canvas,移动至UIRoot层级下。修改 **Button** 位置和 **Layer** 属性。同时将 **Button** 及其子节点的 **Layer** 属性,都修改为和 **Camera** 的 **Layer** 属性一致(此处均为DEFAULT)。 + + ![convert](xrui/convert.gif) + +- 给 Button 及其子节点添加材质。在 **资源管理器** 中点击 **xr-plugin** -> **xr** -> **res** -> **default_materials**,选择 **xr-3dui-sprite-material** 拖拽至 **Sprite** 组件的 **CustomMaterial** 属性中。 + + ![sprite](xrui/def-spr.gif) + +- 给 Button 添加射线交互组件 RaycastChecker。点击 Button 节点,在 **属性检查器** 下方,点击 **添加组件** 按钮,选择 **XR** -> **UI** -> **RaycastChecker**。 + + ![ray](xrui/add-raychecker.png) + + 在 **属性检查器** 中出现 **RaycastChecker** 组件和 **BoxCollider** 组件,且 **BoxCollider** 组件的 **Size** 属性的 **xy** 值与节点的 **UITransfrom** 的 **Content Size** 值一致,如下图:(此处可以将 **BoxCollider** 替换为其他所需 3D 碰撞体,能够贴合 UI 组件即可)。 + + ![side](xrui/box-collider-size.png) + +- 移动 Button 节点至场景中指定位置,调整 Rotation 和 Scale 的值以满足设计需求。 + +- 添加完所有 UI 组件后,删除旧有的 Canvas 节点。 + +到此为止,存量 UI 到 XR UI 的转化就完成了。 + +## 虚拟键盘(XR Keyborad) + +添加一个 EditBox 的 XR UI,同时给 EditBox 添加一个子节点,命名为 KeyboardRoot (命名随意),同时调整 KeyboardRoot 的位置信息(根据需求进行调整即可,可将 XR Keyboard 临时放在节点下进行调整)。 + +![keboard root](xrui/keyboard-root.png) + +创建XR Keyboard对象:在资源管理器中点击 **xr-plugin**-> **xr** -> **res** -> **default_prefab**,选择 **XR Keyboard** 拖拽至场景中。 + +![prefab](xrui/keyboard.png) + +为 EditBox 节点添加 **XRKeyboardInputField** 组件,同时绑定 **SuspendTransform** 和 **XRKeyboard**,将节点拖拽进去。 + +![add xr keyboard](xrui/add-xr-keyboard.png) + +![suspend transform & keyboard](xrui/suspend-transform.png) + +## 射线材质 + +使用射线与 XR UI 进行交互时,需要给射线绑定材质 **xr-default-line-material**。位置在 **资源管理器**->**xr-plugin** -> **xr** -> **res** -> **default_materials**。 + +![default line material](xrui/def-line-mat.png) diff --git a/versions/4.0/zh/xr/vr/xrui/XR Keyboard.png b/versions/4.0/zh/xr/vr/xrui/XR Keyboard.png new file mode 100644 index 0000000000..a59e3ff0ea Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/XR Keyboard.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/add-raychecker.png b/versions/4.0/zh/xr/vr/xrui/add-raychecker.png new file mode 100644 index 0000000000..0ff2a0d2bb Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/add-raychecker.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/add-xr-keyboard.png b/versions/4.0/zh/xr/vr/xrui/add-xr-keyboard.png new file mode 100644 index 0000000000..4e841bf57f Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/add-xr-keyboard.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/box-collider-size.png b/versions/4.0/zh/xr/vr/xrui/box-collider-size.png new file mode 100644 index 0000000000..59b5e1bd2f Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/box-collider-size.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/convert.gif b/versions/4.0/zh/xr/vr/xrui/convert.gif new file mode 100644 index 0000000000..bd4ac2a4b0 Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/convert.gif differ diff --git a/versions/4.0/zh/xr/vr/xrui/create.png b/versions/4.0/zh/xr/vr/xrui/create.png new file mode 100644 index 0000000000..bf1747a4bc Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/create.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/def-line-mat.png b/versions/4.0/zh/xr/vr/xrui/def-line-mat.png new file mode 100644 index 0000000000..81747ec57e Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/def-line-mat.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/def-spr.gif b/versions/4.0/zh/xr/vr/xrui/def-spr.gif new file mode 100644 index 0000000000..46e86f9bf1 Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/def-spr.gif differ diff --git a/versions/4.0/zh/xr/vr/xrui/keyboard-root.png b/versions/4.0/zh/xr/vr/xrui/keyboard-root.png new file mode 100644 index 0000000000..77320867de Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/keyboard-root.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/keyboard.png b/versions/4.0/zh/xr/vr/xrui/keyboard.png new file mode 100644 index 0000000000..6c9009cb39 Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/keyboard.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/suspend-transform.png b/versions/4.0/zh/xr/vr/xrui/suspend-transform.png new file mode 100644 index 0000000000..786bdf490d Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/suspend-transform.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/ui-root.png b/versions/4.0/zh/xr/vr/xrui/ui-root.png new file mode 100644 index 0000000000..82f73d3212 Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/ui-root.png differ diff --git a/versions/4.0/zh/xr/vr/xrui/xr-ui-comp.png b/versions/4.0/zh/xr/vr/xrui/xr-ui-comp.png new file mode 100644 index 0000000000..36a5e1bdc2 Binary files /dev/null and b/versions/4.0/zh/xr/vr/xrui/xr-ui-comp.png differ